diff --git a/Gruntfile.js b/Gruntfile.js index 6872f830..ffa321b8 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -36,7 +36,7 @@ module.exports = function(grunt) { }, 'travis' : { suite : './test/casperjs/TravisTestSuite.js', - delay : 5000 + delay : 10000 } }; diff --git a/package.json b/package.json index 1da0caa1..679f704b 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "karma": "0.12.17", "karma-chrome-launcher": "^0.1.4", "karma-phantomjs-launcher": "^0.1.4", - "karma-jasmine": "^0.1.5", + "karma-jasmine": "^0.2.0", "nodewebkit": "~0.10.1" }, "window": { diff --git a/src/css/bootstrap/bootstrap.css b/src/css/bootstrap/bootstrap.css index 724ca00b..de0c1a0e 100755 --- a/src/css/bootstrap/bootstrap.css +++ b/src/css/bootstrap/bootstrap.css @@ -36,7 +36,7 @@ } .tooltip { position: absolute; - z-index: 1030; + z-index: 30000; display: block; visibility: visible; padding: 5px; diff --git a/src/css/color-picker-slider.css b/src/css/color-picker-slider.css new file mode 100644 index 00000000..8c3cdda6 --- /dev/null +++ b/src/css/color-picker-slider.css @@ -0,0 +1,96 @@ +.color-picker-slider * { + box-sizing: border-box; +} + +.color-picker-slider input[type="range"] { + -webkit-appearance: none; + -webkit-tap-highlight-color: rgba(255, 255, 255, 0); + width: 100%; + border: none; + padding: 1px 2px; + border-radius: 3px; + background-image: linear-gradient(to right, hsl(0, 30%, 70%) 0, hsl(359, 30%, 70%) 100%); + box-shadow: inset 0 1px 0 0 #0d0e0f, inset 0 -1px 0 0 #3a3d42; + outline: none; /* no focus outline */ +} + +/* thumb */ + +.color-picker-slider input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + cursor:pointer; + width: 7px; + height: 18px; + border: none; + border-radius: 2px; + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #529de1), color-stop(100%, #245e8f)); /* android <= 2.2 */ + background-image: -webkit-linear-gradient(top , #529de1 0, #245e8f 100%); /* older mobile safari and android > 2.2 */; + background-image: linear-gradient(to bottom, #529de1 0, #245e8f 100%); /* W3C */ +} +.color-picker-slider input[type="range"]::-moz-range-thumb { + width: 7px; + height: 18px; + border: none; + border-radius: 2px; + background-image: linear-gradient(to bottom, #529de1 0, #245e8f 100%); /* W3C */ +} + +.color-picker-slider input[type="range"]::-ms-thumb { + width: 7px; + height: 18px; + border-radius: 2px; + border: 0; + background-image: linear-gradient(to bottom, #529de1 0, #245e8f 100%); /* W3C */ +} + +/*CROSS BROWSER RESET*/ + + +.color-picker-slider input[type="range"]::-moz-range-track { + border: inherit; + background: transparent; +} + +.color-picker-slider input[type="range"]::-ms-track { + border: inherit; + color: transparent; /* don't drawn vertical reference line */ + background: transparent; +} + +.color-picker-slider input[type="range"]::-ms-fill-lower, +.color-picker-slider input[type="range"]::-ms-fill-upper { + background: transparent; +} + +.color-picker-slider input[type="range"]::-ms-tooltip { + display: none; +} + +.color-picker-slider { + padding: 0 10px; + height : 25px; + overflow: hidden; +} + +.color-picker-slider span{ + line-height : 25px; + width : 10px; + float:left; +} + +.color-picker-slider input[type="range"]{ + float:left; + height : 10px; + width : 100px; + margin: 7px 1px 7px 8px; +} + +.color-picker-slider input[type="text"]{ + float:left; + width : 47px; + margin-left: 5px; +} + +.color-picker-slider input[type="range"][data-dimension="h"] { + background-image:linear-gradient(to right, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); +} \ No newline at end of file diff --git a/src/css/dialogs-create-palette.css b/src/css/dialogs-create-palette.css new file mode 100644 index 00000000..6988b98a --- /dev/null +++ b/src/css/dialogs-create-palette.css @@ -0,0 +1,169 @@ +#dialog-container.create-palette { + width: 500px; + height: 600px; + top : 50%; + left : 50%; + position : absolute; + margin-left: -250px; +} + +.show #dialog-container.create-palette { + margin-top: -300px; +} + +.create-palette-section { + position: absolute; + left: 10px; + top: 50px; +} + +.create-palette-import-section { + display : inline-block; +} + +.colors-container { + position: absolute; + + left: 10px; + right: 10px; + top: 85px; + + height: 460px; + + border: 1px solid black; + background: #333; +} + +.color-picker-container { + position:absolute; + left : 280px; + top:0; + bottom:0; + right:0; + background: #222; +} + +.create-palette-actions { + position: absolute; + box-sizing: border-box; + + width:100%; + height: 45px; + + left: 0; + right: 0; + bottom: 0; + + padding:10px; + text-align:right; +} + +.color-preview { + width: 170px; + height: 76px; + margin: 11px; +} + +.colors-list { + overflow: auto; + width: 280px; + box-sizing: border-box; + height: 100%; + padding-bottom: 10px; +} + +.create-palette-color, .create-palette-new-color, .colors-list-drop-proxy{ + position:relative; + float : left; + + width : 44px; + height : 44px; + margin : 10px 0 0 10px; + + box-sizing : border-box; + + cursor : pointer; +} + +@-moz-document url-prefix() { + .create-palette-color, .create-palette-new-color, .colors-list-drop-proxy{ + margin : 7px 0 0 7px; + } +} + +@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + .create-palette-color, .create-palette-new-color, .colors-list-drop-proxy{ + margin : 7px 0 0 7px; + } +} + + +.create-palette-color { + border:1px solid #2c2c2c; + transition : border-color 0.2s; +} +.create-palette-color:hover { + border:1px solid gold; +} + +.colors-list-drop-proxy { + border:2px dotted #eee; +} + +.create-palette-new-color { + border:2px dotted gold; + + border-radius: 2px; + line-height: 40px; + text-align: center; + font-size: 20px; + color: gold; +} + +.create-palette-color.selected { + border:2px solid gold; +} + +.create-palette-remove-color { + position: absolute; + top: 0; + right: 0; + padding: 2px 4px 0 0; + opacity : 0.2; + + font-weight: bold; + color: rgb(255,255,255); + text-shadow : 0 0 1px rgb(0,0,0); + + transition : opacity 0.3s, color 0.1s; +} + +.light-color .create-palette-remove-color { + color: rgb(0,0,0); + text-shadow : 0 0 1px rgb(255,255,255); +} + +.selected .create-palette-remove-color { + top: -1px; + right: -1px; +} + +.create-palette-color:hover .create-palette-remove-color { + opacity: 0.6; +} + +.create-palette-color .create-palette-remove-color:hover { + opacity: 1; + color: rgb(240,80,80); + text-shadow : 0 0 1px rgb(0,0,0); +} + +/*SPECTRUM OVERRIDES*/ + +.create-palette .sp-container{ + background-color: transparent; + border: none; + box-shadow : none; + border-radius:0; + padding:5px; +} \ No newline at end of file diff --git a/src/css/dialogs-manage-palettes.css b/src/css/dialogs-manage-palettes.css deleted file mode 100644 index 3c0318fa..00000000 --- a/src/css/dialogs-manage-palettes.css +++ /dev/null @@ -1,222 +0,0 @@ -.palette-manager-wrapper { - height: 100%; - position: relative; -} - -.palette-manager-body { - position: absolute; - top: 45px; - bottom: 0; - right: 0; - left: 0; -} - -.palette-manager-head { - position: absolute; - width: 100%; - background: gold; - margin: 0; - padding: 10px; - color: black; - font-size: 1.8em; - box-sizing: border-box; - -moz-box-sizing: border-box; -} - -.palette-manager-close { - position: absolute; - top: 0; - right: 0; - line-height: 45px; - margin-right: 10px; - font-size: 1.3em; - cursor: pointer; -} - -.palette-manager-drawer { - width: 200px; - position: absolute; - top: 0; - bottom: 0; -} - -.palette-manager-list { - position: absolute; - top:40px; - right: 0; - bottom: 0; - left: 0; - overflow: auto; -} - -.palette-manager-actions { - position: absolute; - height:40px; - line-height:40px; - width: 100%; - text-align: center; -} - -.palette-manager-actions-button { - width: 80px; - margin: 5px; -} - -.palette-manager-palette-button, -.palette-manager-actions-button { - line-height: 20px; -} - -.palette-manager-list li { - height: 48px; - line-height: 48px; - padding-left:10px; - - font-size: 1.4em; - - box-sizing: border-box; - -moz-box-sizing: border-box; - - border-bottom: 1px solid #666; - cursor:pointer; -} - -.palette-manager-list li:hover { - background : #222; -} - -.palette-manager-list li.selected { - color : gold; - font-weight: bold; -} - -.palette-manager-list li:nth-child(1) { - border-top: 1px solid #666; -} - -.palette-manager-details { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 200px; - - box-sizing: border-box; - -moz-box-sizing: border-box; - - border-left:1px solid #666; -} - -.palette-manager-details-head { - position: absolute; - height:40px; - line-height:40px; - width: 100%; - - box-sizing: border-box; - -moz-box-sizing: border-box; -} - -.palette-manager-details-head-name { - padding: 0 10px 0 20px; - font-size: 1.5em; - font-weight: bold; -} - -.palette-manager-details-head .edit-icon { - width: 24px; - display: inline-block; - background-size: 16px; -} - -.palette-manager-details-head-actions { - float: right; - line-height: 40px; - padding-right: 10px; -} - -.palette-manager-details-body { - position: absolute; - top:40px; - right: 0; - bottom: 0; - left: 0; - overflow: auto; - - box-sizing: border-box; - -moz-box-sizing: border-box; -} - - - -.palette-manager-color-card { - width: 120px; - height: 180px; - display: inline-block; - position: relative; - margin: 20px 0 20px 20px; - box-shadow: 0 0 0px 0px gold; - transition: box-shadow 0.3s; -} - -.palette-manager-color-card:hover { - box-shadow: 0 0 4px 1px gold; -} - -.palette-manager-delete-card { - position: absolute; - top: 0; - right: 0; - width: 20px; - - text-align: center; - font-size: 1.6em; - font-weight: bold; - color: rgb(255,255,255); - text-shadow : 0 0 2px rgb(0,0,0); - cursor: pointer; - - opacity : 0.2; - transition : opacity 0.3s, color 0.1s; -} - -.palette-manager-color-card:hover .palette-manager-delete-card { - opacity : 0.6; -} - -.palette-manager-color-card .palette-manager-delete-card:hover { - opacity : 1; - color: rgb(240,80,80); -} - -.palette-manager-new-color .palette-manager-color-square { - border: 3px dotted #888; - border-bottom-width: 0; - box-sizing: border-box; - -moz-box-sizing: border-box; - border-radius: 3px 3px 0 0; - cursor: pointer; - text-align: center; - font-size: 24px; - color: #888; - line-height: 120px; -} - -.palette-manager-color-square { - width: 120px; - height: 120px; - cursor: pointer; - /*background-image:url(../img/tools/eyedropper.png);*/ -} - -.palette-manager-color-details { - color : #666; - background: #eee; - height: 60px; - padding-left: 5px; -} - -.palette-manager-color-details li{ - line-height: 20px; - font-weight: bold; -} \ No newline at end of file diff --git a/src/css/dialogs.css b/src/css/dialogs.css index c32e7f4f..85d735b7 100644 --- a/src/css/dialogs.css +++ b/src/css/dialogs.css @@ -79,6 +79,7 @@ } .dialog-wrapper { + height: 100%; position : relative; } @@ -101,4 +102,4 @@ margin-right: 10px; font-size: 1.3em; cursor: pointer; -} +} \ No newline at end of file diff --git a/src/css/font-icon.css b/src/css/font-icon.css index ec44f24a..f771603a 100644 --- a/src/css/font-icon.css +++ b/src/css/font-icon.css @@ -1,10 +1,10 @@ @font-face { font-family: 'piskel'; - src:url('fonts/piskel.eot?-3olv93'); - src:url('fonts/piskel.eot?#iefix-3olv93') format('embedded-opentype'), - url('fonts/piskel.woff?-3olv93') format('woff'), - url('fonts/piskel.ttf?-3olv93') format('truetype'), - url('fonts/piskel.svg?-3olv93#icomoon') format('svg'); + src:url('fonts/icomoon.eot?-3olv93'); + src:url('fonts/icomoon.eot?#iefix-3olv93') format('embedded-opentype'), + url('fonts/icomoon.woff?-3olv93') format('woff'), + url('fonts/icomoon.ttf?-3olv93') format('truetype'), + url('fonts/icomoon.svg?-3olv93#icomoon') format('svg'); font-weight: normal; font-style: normal; } @@ -31,3 +31,66 @@ content: "\e601"; } +.piskel-icon-download:before { + content: "\e600"; +} + +.piskel-icon-rotateleft:before { + content: "\e603"; +} + +.piskel-icon-rotateright:before { + content: "\e604"; +} + +.piskel-icon-fliph:before { + content: "\e605"; +} + +.piskel-icon-flipv:before { + content: "\e606"; +} + +.piskel-icon-trashplain:before { + content: "\e607"; +} + +.piskel-icon-trash:before { + content: "\e608"; +} + +.piskel-icon-merge:before { + content: "\e609"; +} + +.piskel-icon-pencil:before { + content: "\e610"; +} + +.piskel-icon-close:before { + content: "\e611"; +} + +.piskel-icon-minus:before { + content: "\e60a"; +} + +.piskel-icon-plus:before { + content: "\e60b"; +} + +.piskel-icon-arrow-up-fat:before { + content: "\e60c"; +} + +.piskel-icon-arrow-down-fat:before { + content: "\e60d"; +} + +.piskel-icon-arrow-up-thin:before { + content: "\e60e"; +} + +.piskel-icon-arrow-down-thin:before { + content: "\e60f"; +} diff --git a/src/css/fonts/icomoon.eot b/src/css/fonts/icomoon.eot new file mode 100644 index 00000000..f5314adf Binary files /dev/null and b/src/css/fonts/icomoon.eot differ diff --git a/src/css/fonts/icomoon.svg b/src/css/fonts/icomoon.svg new file mode 100644 index 00000000..bafc0fee --- /dev/null +++ b/src/css/fonts/icomoon.svg @@ -0,0 +1,28 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/css/fonts/icomoon.ttf b/src/css/fonts/icomoon.ttf new file mode 100644 index 00000000..5ae72b63 Binary files /dev/null and b/src/css/fonts/icomoon.ttf differ diff --git a/src/css/fonts/icomoon.woff b/src/css/fonts/icomoon.woff new file mode 100644 index 00000000..a9d1e6c2 Binary files /dev/null and b/src/css/fonts/icomoon.woff differ diff --git a/src/css/icons.css b/src/css/icons.css index dc1ddc1e..51e9c20d 100644 --- a/src/css/icons.css +++ b/src/css/icons.css @@ -5,6 +5,20 @@ background-position: 50%; } -.action-icon.edit-icon { +.edit-icon { background-image: url('../img/tools/pen.png'); + background-repeat: no-repeat; +} + +.merge-icon { + background-image: url('../img/merge-icon.png'); + background-repeat: no-repeat; +} + +.plus-icon { + font-size:15px; + text-align:center; +} + +.delete-icon { } \ No newline at end of file diff --git a/src/css/notifications.css b/src/css/notifications.css new file mode 100644 index 00000000..b1ab2885 --- /dev/null +++ b/src/css/notifications.css @@ -0,0 +1,74 @@ +.user-message { + position: absolute; + right: 0; + bottom: 0; + padding: 10px 47px; + max-width: 300px; + + border-top-left-radius: 7px; + border: #F0C36D 1px solid; + border-right: 0; + border-bottom: 0; + + color: #222; + background-color: #F9EDBE; + + font-weight: bold; + font-size: 13px; + + z-index: 30000; +} + +.user-message .close { + position: absolute; + top: 6px; + right: 17px; + + color: gray; + + font-size: 18px; + font-weight: bold; + + cursor: pointer; +} + +.user-message .close:hover { + color: black; +} + +.progress-bar-container { + position: absolute; + left: 0; + bottom: 0; + padding: 10px; + width: 360px; + border-top-right-radius: 2px; + border: gold 2px solid; + border-left: 0; + border-bottom: 0; + background-color: #444; + font-size: 14px; + z-index: 30000; + color: #eee; +} + +.progress-bar-item { + float: left; + height:20px; +} + +.progress-bar-status { + line-height: 20px; + width : 40px; + overflow : hidden; + margin: 0 0 0 10px; +} + +.progress-bar { + border : 1px solid grey; + margin-top: 8px; + height : 4px; + width : 300px; + background : linear-gradient(to left, gold, gold) no-repeat -300px 0; + background-color : black; +} \ No newline at end of file diff --git a/src/css/settings-export.css b/src/css/settings-export.css index e6dd2fe9..e02406ca 100644 --- a/src/css/settings-export.css +++ b/src/css/settings-export.css @@ -51,15 +51,4 @@ -moz-box-sizing:border-box; background: rgba(0,0,0,0.5); color: white; -} - -.gif-export-progress-status { - margin-left: 5px; -} - -.gif-export-progress-bar { - margin-top:5px; - height:3px; - width: 0; - background:gold; -} +} \ No newline at end of file diff --git a/src/css/style.css b/src/css/style.css index c4f2312e..deaa44a7 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -162,42 +162,6 @@ body { .canvas.onion-skin-canvas {z-index: 10;} .canvas.layers-above-canvas {z-index: 11;} - - -/** - * User messages - */ -.user-message { - position: absolute; - right: 0; - bottom: 0; - background-color: #F9EDBE; - padding: 10px 47px; - border-top-left-radius: 7px; - color: #222; - border: #F0C36D 1px solid; - border-right: 0; - border-bottom: 0; - font-weight: bold; - font-size: 13px; - z-index: 30000; - max-width: 300px; -} - -.user-message .close { - position: absolute; - top: 6px; - right: 17px; - color: gray; - font-weight: bold; - cursor: pointer; - font-size: 18px; -} - -.user-message .close:hover { - color: black; -} - .image-link { color : gold; } diff --git a/src/css/toolbox-layers-list.css b/src/css/toolbox-layers-list.css index 052a5013..eacfde75 100644 --- a/src/css/toolbox-layers-list.css +++ b/src/css/toolbox-layers-list.css @@ -5,18 +5,21 @@ .layers-list-container { } -.layers-title { +/*.layers-title { background-image: url('../img/layers.svg'); background-size: 22px; background-repeat: no-repeat; background-position: 97%; +}*/ + +.layers-title { position: relative; } .layers-toggle-preview { position: absolute; top: 0.3em; - right: 2em; + right: 0.5em; color: #999; font-size: 1.3em; @@ -46,22 +49,6 @@ cursor : pointer; } -.layer-item .edit-icon { - float: right; - width: 30px; - background-size: 12px; - opacity: 0; - transition : opacity 0.2s; -} - -.layer-item:hover .edit-icon { - opacity : 0.6; -} - -.layer-item:hover .edit-icon:hover { - opacity : 1; -} - .layer-item:hover { background : #222; } @@ -72,7 +59,21 @@ color: gold; } -.layers-button-arrow { - font-family : 'Lucida Grande', Calibri; - padding : 2px 6px 0 6px; +.layers-button-container { + overflow : hidden; +} + +.layers-button { + margin: 0; + width: 16.66667%; + float : left; +} + +/* @override */ +.layers-button-container .layers-button { + border-left-width: 0; +} + +.layers-button:last-child { + border-right-width: 0; } \ No newline at end of file diff --git a/src/css/toolbox-palettes-list.css b/src/css/toolbox-palettes-list.css index 2d8aa477..e77e10ad 100644 --- a/src/css/toolbox-palettes-list.css +++ b/src/css/toolbox-palettes-list.css @@ -1,63 +1,106 @@ -.palettes-list-select { - float:right; - max-width:90px; - margin-top: 3px; -} - .palettes-title { - background-size: 22px; - background-repeat: no-repeat; - background-position: 97%; + background-size: 22px; + background-repeat: no-repeat; + background-position: 97%; } - .palettes-list-colors { - overflow: auto; - max-height: 160px; + overflow: auto; + max-height: 160px; } - .palettes-list-color { - cursor : pointer; - float: left; - margin : 0 0 5px 5px; - width : 32px; - height : 32px; - position: relative; + cursor: pointer; + float: left; + margin: 0 0 5px 5px; + width: 32px; + height: 32px; + position: relative; } - .palettes-list-color:nth-child(-n+5) { - margin-top: 5px; + margin-top: 5px; } - -.palettes-list-color div{ - width : 32px; - height : 32px; +.palettes-list-color div { + width: 32px; + height: 32px; } - -.palettes-list-has-scrollbar .palettes-list-color, -.palettes-list-has-scrollbar .palettes-list-color div{ - width: 29px +.palettes-list-has-scrollbar .palettes-list-color, .palettes-list-has-scrollbar .palettes-list-color div { + width: 29px } - -.palettes-list-primary-color:before, -.palettes-list-secondary-color:before { - content: ""; - position: absolute; - bottom: 1px; - display: inline-block; - border: 7px solid gold; - border-top-color: transparent; - - width: 0px; - height: 0px; +.palettes-list-primary-color:before, .palettes-list-secondary-color:before { + content: ""; + position: absolute; + bottom: 1px; + display: inline-block; + border: 7px solid gold; + border-top-color: transparent; + width: 0px; + height: 0px; } - .palettes-list-primary-color:before { - left: 1px; - border-right-color: transparent; + left: 1px; + border-right-color: transparent; +} +.palettes-list-secondary-color:before { + right: 1px; + border-left-color: transparent; +} +.palettes-list-actions { + background-color: #3f3f3f; + border-bottom-color: #222; + height: 24px; + padding: 0; + overflow: hidden; } +.palettes-list-button, +.palettes-list-select { + margin: 0; + float: left; +} -.palettes-list-secondary-color:before { - right: 1px; - border-left-color: transparent; -} \ No newline at end of file +.palettes-list-button { + width: 16.66667%; +} +.palettes-list-select { + width: 66.66667%; + height: 100%; + padding: 0 5px 0 5px; + + border-style: solid; + border-width: 1px 0 1px 0; + + color: #aaa; + font-size : 0.75em; + + /*thanks firefox, you suck*/ + text-align:left; + /*text-shadow:none;*/ + font-weight: normal; + + transition : background-color 0.3s, color 0.3s; + cursor:pointer; +} + +.palettes-list-select:hover { + color: white; + background-color: #484848; +} + +.palettes-list-select:focus { + background-color: #484848; + color: white; + outline: none; +} + +.palettes-list-actions .edit-icon { + background-size: 15px; + background-position: 50%; +} +.palettes-list-no-colors { + height: 42px; + width: 100%; + color: grey; + font-size: 0.7em; + font-style: italic; + line-height: 42px; + text-align: center +} diff --git a/src/css/toolbox.css b/src/css/toolbox.css index e449eb8a..6f157486 100644 --- a/src/css/toolbox.css +++ b/src/css/toolbox.css @@ -14,23 +14,4 @@ margin: 0; font-size: 15px; background: #222; -} - -.toolbox-button-container { - overflow : hidden; -} - -.toolbox-button { - margin: 0; - width: 25%; - float : left; -} - -/* @override */ -.button.toolbox-button { - border-left-width: 0; -} - -.toolbox-button:last-child { - border-right-width: 0; } \ No newline at end of file diff --git a/src/img/merge-icon.png b/src/img/merge-icon.png new file mode 100644 index 00000000..3210c100 Binary files /dev/null and b/src/img/merge-icon.png differ diff --git a/src/index.html b/src/index.html index 468cb0a0..ad7ae0f9 100644 --- a/src/index.html +++ b/src/index.html @@ -67,13 +67,14 @@
- +
+ diff --git a/src/js/Constants.js b/src/js/Constants.js index f017ca5e..0ce59ab4 100644 --- a/src/js/Constants.js +++ b/src/js/Constants.js @@ -27,7 +27,6 @@ var Constants = { NO_PALETTE_ID : '__no-palette', CURRENT_COLORS_PALETTE_ID : '__current-colors', - MANAGE_PALETTE_ID : '__manage-palettes', // Used for Spectrum input PREFERRED_COLOR_FORMAT : 'rgb', diff --git a/src/js/Events.js b/src/js/Events.js index 5b9d1106..fb2203a3 100644 --- a/src/js/Events.js +++ b/src/js/Events.js @@ -47,6 +47,10 @@ var Events = { SHOW_NOTIFICATION: "SHOW_NOTIFICATION", HIDE_NOTIFICATION: "HIDE_NOTIFICATION", + SHOW_PROGRESS: "SHOW_PROGRESS", + UPDATE_PROGRESS: "UPDATE_PROGRESS", + HIDE_PROGRESS: "HIDE_PROGRESS", + ZOOM_CHANGED : "ZOOM_CHANGED", CURRENT_COLORS_UPDATED : "CURRENT_COLORS_UPDATED", diff --git a/src/js/app.js b/src/js/app.js index 90477e90..fe8eb764 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -11,7 +11,6 @@ init : function () { /** - * True when piskel is running in static mode (no back end needed). * When started from APP Engine, appEngineToken_ (Boolean) should be set on window.pskl */ this.isAppEngineVersion = !!pskl.appEngineToken_; @@ -39,6 +38,10 @@ this.piskelController = new pskl.controller.piskel.PublicPiskelController(this.corePiskelController); this.piskelController.init(); + this.paletteImportService = new pskl.service.palette.PaletteImportService(); + this.paletteService = new pskl.service.palette.PaletteService(); + this.paletteService.addDynamicPalette(new pskl.service.palette.CurrentColorsPalette()); + this.paletteController = new pskl.controller.PaletteController(); this.paletteController.init(); @@ -84,6 +87,9 @@ this.notificationController = new pskl.controller.NotificationController(); this.notificationController.init(); + this.progressBarController = new pskl.controller.ProgressBarController(); + this.progressBarController.init(); + this.canvasBackgroundController = new pskl.controller.CanvasBackgroundController(); this.canvasBackgroundController.init(); diff --git a/src/js/controller/LayersListController.js b/src/js/controller/LayersListController.js index 48b6e3e7..44bf10d3 100644 --- a/src/js/controller/LayersListController.js +++ b/src/js/controller/LayersListController.js @@ -28,6 +28,31 @@ this.layersListEl.innerHTML = ''; var layers = this.piskelController.getLayers(); layers.forEach(this.addLayerItem.bind(this)); + this.updateButtonStatus_(); + }; + + ns.LayersListController.prototype.updateButtonStatus_ = function () { + var layers = this.piskelController.getLayers(); + var currentLayer = this.piskelController.getCurrentLayer(); + var index = this.piskelController.getCurrentLayerIndex(); + + var isLast = index === 0; + var isOnly = layers.length === 1; + var isFirst = index === layers.length - 1; + + this.toggleButtonDisabledState_('up', isFirst); + this.toggleButtonDisabledState_('down', isLast); + this.toggleButtonDisabledState_('merge', isLast); + this.toggleButtonDisabledState_('delete', isOnly); + }; + + ns.LayersListController.prototype.toggleButtonDisabledState_ = function (buttonAction, isDisabled) { + var button = document.querySelector('.layers-button[data-action="'+buttonAction+'"]'); + if (isDisabled) { + button.setAttribute('disabled', 'disabled'); + } else { + button.removeAttribute('disabled'); + } }; ns.LayersListController.prototype.updateToggleLayerPreview_ = function () { @@ -64,21 +89,25 @@ } else if (el.classList.contains('layer-item')) { index = el.dataset.layerIndex; this.piskelController.setCurrentLayerIndex(parseInt(index, 10)); - } else if (el.classList.contains('edit-icon')) { - index = el.parentNode.dataset.layerIndex; - this.renameLayerAt_(index); } }; - ns.LayersListController.prototype.renameLayerAt_ = function (index) { - var layer = this.piskelController.getLayerAt(index); + ns.LayersListController.prototype.renameCurrentLayer_ = function () { + var layer = this.piskelController.getCurrentLayer(); var name = window.prompt("Please enter the layer name", layer.getName()); if (name) { + var index = this.piskelController.getCurrentLayerIndex(); this.piskelController.renameLayerAt(index, name); this.renderLayerList_(); } }; + ns.LayersListController.prototype.mergeDownCurrentLayer_ = function () { + var index = this.piskelController.getCurrentLayerIndex(); + this.piskelController.mergeDownLayerAt(index); + this.renderLayerList_(); + }; + ns.LayersListController.prototype.onButtonClick_ = function (button) { var action = button.getAttribute('data-action'); if (action == 'up') { @@ -89,6 +118,10 @@ this.piskelController.createLayer(); } else if (action == 'delete') { this.piskelController.removeCurrentLayer(); + } else if (action == 'merge') { + this.mergeDownCurrentLayer_(); + } else if (action == 'edit') { + this.renameCurrentLayer_(); } }; diff --git a/src/js/controller/PalettesListController.js b/src/js/controller/PalettesListController.js index 0901d3df..1f9e33ef 100644 --- a/src/js/controller/PalettesListController.js +++ b/src/js/controller/PalettesListController.js @@ -13,70 +13,82 @@ ns.PalettesListController = function (paletteController, usedColorService) { this.usedColorService = usedColorService; + this.paletteService = pskl.app.paletteService; this.paletteController = paletteController; }; ns.PalettesListController.prototype.init = function () { this.paletteColorTemplate_ = pskl.utils.Template.get('palette-color-template'); + this.colorListContainer_ = document.querySelector('.palettes-list-colors'); this.colorPaletteSelect_ = document.querySelector('.palettes-list-select'); - this.paletteListOptGroup_ = document.querySelector('.palettes-list-select-group'); + + var createPaletteButton_ = document.querySelector('.create-palette-button'); + var editPaletteButton_ = document.querySelector('.edit-palette-button'); this.colorPaletteSelect_.addEventListener('change', this.onPaletteSelected_.bind(this)); this.colorListContainer_.addEventListener('mouseup', this.onColorContainerMouseup.bind(this)); this.colorListContainer_.addEventListener('contextmenu', this.onColorContainerContextMenu.bind(this)); + createPaletteButton_.addEventListener('click', this.onCreatePaletteClick_.bind(this)); + editPaletteButton_.addEventListener('click', this.onEditPaletteClick_.bind(this)); + $.subscribe(Events.PALETTE_LIST_UPDATED, this.onPaletteListUpdated.bind(this)); $.subscribe(Events.CURRENT_COLORS_UPDATED, this.fillColorListContainer.bind(this)); $.subscribe(Events.PRIMARY_COLOR_SELECTED, this.highlightSelectedColors.bind(this)); $.subscribe(Events.SECONDARY_COLOR_SELECTED, this.highlightSelectedColors.bind(this)); + $.subscribe(Events.USER_SETTINGS_CHANGED, $.proxy(this.onUserSettingsChange_, this)); + + + pskl.app.shortcutService.addShortcuts(['>', 'shift+>'], this.selectNextColor_.bind(this)); + pskl.app.shortcutService.addShortcut('<', this.selectPreviousColor_.bind(this)); this.fillPaletteList(); - this.selectPaletteFromUserSettings(); + this.updateFromUserSettings(); this.fillColorListContainer(); }; ns.PalettesListController.prototype.fillPaletteList = function () { - var palettes = [{ - id : Constants.NO_PALETTE_ID, - name : 'No palette' - }]; - palettes = palettes.concat(this.retrievePalettes()); + var palettes = this.paletteService.getPalettes(); var html = palettes.map(function (palette) { return pskl.utils.Template.replace('', palette); }).join(''); - this.paletteListOptGroup_.innerHTML = html; + this.colorPaletteSelect_.innerHTML = html; }; ns.PalettesListController.prototype.fillColorListContainer = function () { + var colors = this.getSelectedPaletteColors_(); - var html = colors.map(function (color) { - return pskl.utils.Template.replace(this.paletteColorTemplate_, {color : color}); - }.bind(this)).join(''); - this.colorListContainer_.innerHTML = html; + if (colors.length > 0) { + var html = colors.map(function (color, index) { + return pskl.utils.Template.replace(this.paletteColorTemplate_, {color : color, index : index}); + }.bind(this)).join(''); + this.colorListContainer_.innerHTML = html; - this.highlightSelectedColors(); + this.highlightSelectedColors(); - var hasScrollbar = colors.length > NO_SCROLL_MAX_COLORS; - if (hasScrollbar && !pskl.utils.UserAgent.isChrome) { - this.colorListContainer_.classList.add(HAS_SCROLL_CLASSNAME); + var hasScrollbar = colors.length > NO_SCROLL_MAX_COLORS; + if (hasScrollbar && !pskl.utils.UserAgent.isChrome) { + this.colorListContainer_.classList.add(HAS_SCROLL_CLASSNAME); + } else { + this.colorListContainer_.classList.remove(HAS_SCROLL_CLASSNAME); + } } else { - this.colorListContainer_.classList.remove(HAS_SCROLL_CLASSNAME); + this.colorListContainer_.innerHTML = pskl.utils.Template.get('palettes-list-no-colors-partial'); } }; + ns.PalettesListController.prototype.selectPalette = function (paletteId) { + pskl.UserSettings.set(pskl.UserSettings.SELECTED_PALETTE, paletteId); + }; + ns.PalettesListController.prototype.getSelectedPaletteColors_ = function () { var colors = []; - var paletteId = this.colorPaletteSelect_.value; - if (paletteId === Constants.CURRENT_COLORS_PALETTE_ID) { - colors = this.usedColorService.getCurrentColors(); - } else { - var palette = this.getPaletteById(paletteId, this.retrievePalettes()); - if (palette) { - colors = palette.colors; - } + var palette = this.getSelectedPalette_(); + if (palette) { + colors = palette.getColors(); } if (colors.length > Constants.MAX_CURRENT_COLORS_DISPLAYED) { @@ -86,27 +98,65 @@ return colors; }; - ns.PalettesListController.prototype.selectPalette = function (paletteId) { - this.colorPaletteSelect_.value = paletteId; + ns.PalettesListController.prototype.getSelectedPalette_ = function () { + var paletteId = pskl.UserSettings.get(pskl.UserSettings.SELECTED_PALETTE); + return this.paletteService.getPaletteById(paletteId); }; - ns.PalettesListController.prototype.selectPaletteFromUserSettings = function () { - this.selectPalette(pskl.UserSettings.get(pskl.UserSettings.SELECTED_PALETTE)); + ns.PalettesListController.prototype.selectNextColor_ = function () { + this.selectColor_(this.getCurrentColorIndex_() + 1); + }; + + ns.PalettesListController.prototype.selectPreviousColor_ = function () { + this.selectColor_(this.getCurrentColorIndex_() - 1); + }; + + ns.PalettesListController.prototype.getCurrentColorIndex_ = function () { + var currentIndex = 0; + var selectedColor = document.querySelector('.' + PRIMARY_COLOR_CLASSNAME); + if (selectedColor) { + currentIndex = parseInt(selectedColor.dataset.colorIndex, 10); + } + return currentIndex; + }; + + ns.PalettesListController.prototype.selectColor_ = function (index) { + var colors = this.getSelectedPaletteColors_(); + var color = colors[index]; + if (color) { + $.publish(Events.SELECT_PRIMARY_COLOR, [color]); + } + }; + + ns.PalettesListController.prototype.onUserSettingsChange_ = function (evt, name, value) { + if (name == pskl.UserSettings.SELECTED_PALETTE) { + this.updateFromUserSettings(); + } + }; + + ns.PalettesListController.prototype.updateFromUserSettings = function () { + var paletteId = pskl.UserSettings.get(pskl.UserSettings.SELECTED_PALETTE); + this.fillColorListContainer(); + this.colorPaletteSelect_.value = paletteId; }; ns.PalettesListController.prototype.onPaletteSelected_ = function (evt) { var paletteId = this.colorPaletteSelect_.value; - if (paletteId === Constants.MANAGE_PALETTE_ID) { - $.publish(Events.DIALOG_DISPLAY, 'manage-palettes'); - this.selectPaletteFromUserSettings(); - } else { - pskl.UserSettings.set(pskl.UserSettings.SELECTED_PALETTE, paletteId); - } - - this.fillColorListContainer(); + this.selectPalette(paletteId); + this.colorPaletteSelect_.blur(); }; + ns.PalettesListController.prototype.onCreatePaletteClick_ = function (evt) { + $.publish(Events.DIALOG_DISPLAY, 'create-palette'); + }; + ns.PalettesListController.prototype.onEditPaletteClick_ = function (evt) { + var paletteId = this.colorPaletteSelect_.value; + $.publish(Events.DIALOG_DISPLAY, { + dialogId : 'create-palette', + initArgs : paletteId + }); + }; ns.PalettesListController.prototype.onColorContainerContextMenu = function (event) { event.preventDefault(); @@ -155,24 +205,6 @@ ns.PalettesListController.prototype.onPaletteListUpdated = function () { this.fillPaletteList(); - this.selectPaletteFromUserSettings(); - this.fillColorListContainer(); - }; - - ns.PalettesListController.prototype.getPaletteById = function (paletteId, palettes) { - var match = null; - - palettes.forEach(function (palette) { - if (palette.id === paletteId) { - match = palette; - } - }); - - return match; - }; - - ns.PalettesListController.prototype.retrievePalettes = function () { - var palettesString = window.localStorage.getItem('piskel.palettes'); - return JSON.parse(palettesString) || []; + this.updateFromUserSettings(); }; })(); \ No newline at end of file diff --git a/src/js/controller/PreviewFilmController.js b/src/js/controller/PreviewFilmController.js index 867c1859..0571c02e 100644 --- a/src/js/controller/PreviewFilmController.js +++ b/src/js/controller/PreviewFilmController.js @@ -218,7 +218,7 @@ }; ns.PreviewFilmController.prototype.clonePreviewCanvas_ = function (canvas) { - var clone = pskl.CanvasUtils.clone(canvas); + var clone = pskl.utils.CanvasUtils.clone(canvas); clone.classList.add('tile-view', 'canvas'); return clone; }; diff --git a/src/js/controller/ProgressBarController.js b/src/js/controller/ProgressBarController.js new file mode 100644 index 00000000..2681982b --- /dev/null +++ b/src/js/controller/ProgressBarController.js @@ -0,0 +1,61 @@ +(function () { + var ns = $.namespace('pskl.controller'); + + ns.ProgressBarController = function () { + this.template = pskl.utils.Template.get('progress-bar-template'); + this.progressBar = null; + this.progressBarStatus = null; + + this.showProgressTimer_ = 0; + }; + + ns.ProgressBarController.prototype.init = function () { + $.subscribe(Events.SHOW_PROGRESS, $.proxy(this.showProgress_, this)); + $.subscribe(Events.UPDATE_PROGRESS, $.proxy(this.updateProgress_, this)); + $.subscribe(Events.HIDE_PROGRESS, $.proxy(this.hideProgress_, this)); + }; + + ns.ProgressBarController.prototype.showProgress_ = function (event, progressInfo) { + this.removeProgressBar_(); + this.showProgressTimer_ = window.setTimeout(this.onTimerExpired_.bind(this, progressInfo), 300); + }; + + ns.ProgressBarController.prototype.onTimerExpired_ = function (progressInfo) { + var progressBarHtml = pskl.utils.Template.replace(this.template, { + name : progressInfo.name, + status : 0 + }); + + var progressBarEl = pskl.utils.Template.createFromHTML(progressBarHtml); + document.body.appendChild(progressBarEl); + + this.progressBar = document.querySelector('.progress-bar'); + this.progressBarStatus = document.querySelector('.progress-bar-status'); + }; + + ns.ProgressBarController.prototype.updateProgress_ = function (event, progressInfo) { + if (this.progressBar && this.progressBarStatus) { + var progress = progressInfo.progress; + var width = this.progressBar.offsetWidth; + var progressWidth = width - ((progress * width) / 100); + this.progressBar.style.backgroundPosition = (-progressWidth) + 'px 0'; + this.progressBarStatus.innerHTML = progress + '%'; + } + }; + + ns.ProgressBarController.prototype.hideProgress_ = function (event, progressInfo) { + if (this.showProgressTimer_) { + window.clearTimeout(this.showProgressTimer_); + } + this.removeProgressBar_(); + }; + + ns.ProgressBarController.prototype.removeProgressBar_ = function () { + var progressBarContainer = document.querySelector('.progress-bar-container'); + if (progressBarContainer) { + progressBarContainer.parentNode.removeChild(progressBarContainer); + this.progressBar = null; + this.progressBarStatus = null; + } + }; +})(); \ No newline at end of file diff --git a/src/js/controller/dialogs/AbstractDialogController.js b/src/js/controller/dialogs/AbstractDialogController.js index 1c8c7715..296b78e9 100644 --- a/src/js/controller/dialogs/AbstractDialogController.js +++ b/src/js/controller/dialogs/AbstractDialogController.js @@ -12,7 +12,15 @@ ns.AbstractDialogController.prototype.destroy = function () {}; ns.AbstractDialogController.prototype.closeDialog = function () { + this.destroy(); $.publish(Events.DIALOG_HIDE); }; + ns.AbstractDialogController.prototype.setTitle = function (title) { + var dialogTitle = document.querySelector('.dialog-title'); + if (dialogTitle) { + dialogTitle.innerText = title; + } + }; + })(); \ No newline at end of file diff --git a/src/js/controller/dialogs/CreatePaletteController.js b/src/js/controller/dialogs/CreatePaletteController.js new file mode 100644 index 00000000..fa777fcc --- /dev/null +++ b/src/js/controller/dialogs/CreatePaletteController.js @@ -0,0 +1,127 @@ +(function () { + var ns = $.namespace('pskl.controller.dialogs'); + + ns.CreatePaletteController = function (piskelController) { + this.paletteService = pskl.app.paletteService; + this.paletteImportService = pskl.app.paletteImportService; + }; + + pskl.utils.inherit(ns.CreatePaletteController, ns.AbstractDialogController); + + ns.CreatePaletteController.prototype.init = function (paletteId) { + this.superclass.init.call(this); + + this.hiddenFileInput = document.querySelector('.create-palette-import-input'); + this.nameInput = document.querySelector('input[name="palette-name"]'); + + var buttonsContainer = document.querySelector('.create-palette-actions'); + var deleteButton = document.querySelector('.create-palette-delete'); + var downloadButton = document.querySelector('.create-palette-download-button'); + var importFileButton = document.querySelector('.create-palette-import-button'); + + this.nameInput.addEventListener('input', this.onNameInputChange_.bind(this)); + this.hiddenFileInput.addEventListener('change', this.onFileInputChange_.bind(this)); + + buttonsContainer.addEventListener('click', this.onButtonClick_.bind(this)); + downloadButton.addEventListener('click', this.onDownloadButtonClick_.bind(this)); + importFileButton.addEventListener('click', this.onImportFileButtonClick_.bind(this)); + + var colorsListContainer = document.querySelector('.colors-container'); + this.colorsListWidget = new pskl.controller.widgets.ColorsList(colorsListContainer); + + var palette; + var isCurrentColorsPalette = paletteId == Constants.CURRENT_COLORS_PALETTE_ID; + if (paletteId && !isCurrentColorsPalette) { + importFileButton.style.display = 'none'; + this.setTitle('Edit Palette'); + + var paletteObject = this.paletteService.getPaletteById(paletteId); + palette = pskl.model.Palette.fromObject(paletteObject); + } else { + downloadButton.style.display = 'none'; + deleteButton.style.display = 'none'; + this.setTitle('Create Palette'); + + var uuid = pskl.utils.Uuid.generate(); + if (isCurrentColorsPalette) { + palette = new pskl.model.Palette(uuid, 'Current colors clone', this.getCurrentColors_()); + } else { + palette = new pskl.model.Palette(uuid, 'New palette', []); + } + } + + this.setPalette_(palette); + }; + + ns.CreatePaletteController.prototype.getCurrentColors_ = function () { + var palette = this.paletteService.getPaletteById(Constants.CURRENT_COLORS_PALETTE_ID); + return palette.getColors(); + }; + + ns.CreatePaletteController.prototype.setPalette_ = function (palette) { + this.palette = palette; + this.nameInput.value = pskl.utils.unescapeHtml(palette.name); + this.colorsListWidget.setColors(palette.getColors()); + }; + + ns.CreatePaletteController.prototype.destroy = function () { + this.colorsListWidget.destroy(); + this.nameInput = null; + }; + + ns.CreatePaletteController.prototype.onButtonClick_ = function (evt) { + var target = evt.target; + if (target.dataset.action === 'submit') { + this.saveAndSelectPalette_(); + } else if (target.dataset.action === 'cancel') { + this.closeDialog(); + } else if (target.dataset.action === 'delete') { + this.deletePalette_(); + } + }; + + ns.CreatePaletteController.prototype.saveAndSelectPalette_ = function () { + this.palette.setColors(this.colorsListWidget.getColors()); + this.paletteService.savePalette(this.palette); + pskl.UserSettings.set(pskl.UserSettings.SELECTED_PALETTE, this.palette.id); + this.closeDialog(); + }; + + ns.CreatePaletteController.prototype.deletePalette_ = function () { + if (window.confirm('Are you sure you want to delete palette ' + this.palette.name)) { + this.paletteService.deletePaletteById(this.palette.id); + pskl.UserSettings.set(pskl.UserSettings.SELECTED_PALETTE, Constants.CURRENT_COLORS_PALETTE_ID); + this.closeDialog(); + } + }; + + ns.CreatePaletteController.prototype.onDownloadButtonClick_ = function () { + var paletteWriter = new pskl.service.palette.PaletteGplWriter(this.palette); + var paletteAsString = paletteWriter.write(); + + pskl.utils.BlobUtils.stringToBlob(paletteAsString, function(blob) { + pskl.utils.FileUtils.downloadAsFile(blob, this.palette.name + '.gpl'); + }.bind(this), "application/json"); + }; + + ns.CreatePaletteController.prototype.onImportFileButtonClick_ = function () { + this.hiddenFileInput.click(); + }; + + ns.CreatePaletteController.prototype.onFileInputChange_ = function (evt) { + var files = this.hiddenFileInput.files; + if (files.length == 1) { + this.paletteImportService.read(files[0], this.setPalette_.bind(this), this.displayErrorMessage_.bind(this)); + } + }; + + ns.CreatePaletteController.prototype.displayErrorMessage_ = function (message) { + message = "Could not import palette : " + message; + $.publish(Events.SHOW_NOTIFICATION, [{"content": message}]); + window.setTimeout($.publish.bind($, Events.HIDE_NOTIFICATION), 2000); + }; + + ns.CreatePaletteController.prototype.onNameInputChange_ = function (evt) { + this.palette.name = pskl.utils.escapeHtml(this.nameInput.value); + }; +})(); \ No newline at end of file diff --git a/src/js/controller/dialogs/DialogsController.js b/src/js/controller/dialogs/DialogsController.js index 2f52a021..eac69e4d 100644 --- a/src/js/controller/dialogs/DialogsController.js +++ b/src/js/controller/dialogs/DialogsController.js @@ -2,9 +2,9 @@ var ns = $.namespace('pskl.controller.dialogs'); var dialogs = { - 'manage-palettes' : { - template : 'templates/dialogs/manage-palettes.html', - controller : ns.PaletteManagerController + 'create-palette' : { + template : 'templates/dialogs/create-palette.html', + controller : ns.CreatePaletteController }, 'browse-local' : { template : 'templates/dialogs/browse-local.html', @@ -27,7 +27,8 @@ $.subscribe(Events.DIALOG_DISPLAY, this.onDialogDisplayEvent_.bind(this)); $.subscribe(Events.DIALOG_HIDE, this.onDialogHideEvent_.bind(this)); - pskl.app.shortcutService.addShortcut('alt+P', this.onDialogDisplayEvent_.bind(this, null, 'manage-palettes')); + pskl.app.shortcutService.addShortcut('alt+P', this.onDialogDisplayEvent_.bind(this, null, 'create-palette')); + this.dialogWrapper_.classList.add('animated'); }; @@ -42,8 +43,8 @@ if (!this.isDisplayed()) { var config = dialogs[dialogId]; if (config) { - this.dialogContainer_.innerHTML = pskl.utils.Template.get(config.template); this.dialogContainer_.classList.add(dialogId); + this.dialogContainer_.innerHTML = pskl.utils.Template.get(config.template); var controller = new config.controller(this.piskelController); controller.init(initArgs); diff --git a/src/js/controller/dialogs/ImportImageController.js b/src/js/controller/dialogs/ImportImageController.js index 08f6d417..564fa116 100644 --- a/src/js/controller/dialogs/ImportImageController.js +++ b/src/js/controller/dialogs/ImportImageController.js @@ -28,7 +28,7 @@ this.importImageForm = $('[name=import-image-form]'); this.importImageForm.submit(this.onImportFormSubmit_.bind(this)); - pskl.utils.FileUtils.readFile(this.file_, this.processImageSource_.bind(this)); + pskl.utils.FileUtils.readImageFile(this.file_, this.onImageLoaded_.bind(this)); }; ns.ImportImageController.prototype.onImportFormSubmit_ = function (evt) { @@ -55,18 +55,9 @@ } }; - /** - * Create an image from the given source (url or data-url), and onload forward to onImageLoaded - * TODO : should be a generic utility method, should take a callback - * @param {String} imageSource url or data-url, will be used as src for the image - */ - ns.ImportImageController.prototype.processImageSource_ = function (imageSource) { - this.importedImage_ = new Image(); - this.importedImage_.onload = this.onImageLoaded_.bind(this); - this.importedImage_.src = imageSource; - }; + ns.ImportImageController.prototype.onImageLoaded_ = function (image) { + this.importedImage_ = image; - ns.ImportImageController.prototype.onImageLoaded_ = function (evt) { var w = this.importedImage_.width, h = this.importedImage_.height; @@ -115,7 +106,7 @@ gifLoader.load({ success : function(){ var images = gifLoader.getFrames().map(function (frame) { - return pskl.CanvasUtils.createFromImageData(frame.data); + return pskl.utils.CanvasUtils.createFromImageData(frame.data); }); this.createPiskelFromImages_(images); this.closeDialog(); diff --git a/src/js/controller/dialogs/PaletteManagerController.js b/src/js/controller/dialogs/PaletteManagerController.js deleted file mode 100644 index dc16ba4d..00000000 --- a/src/js/controller/dialogs/PaletteManagerController.js +++ /dev/null @@ -1,382 +0,0 @@ -(function () { - var ns = $.namespace('pskl.controller.dialogs'); - - var tinycolor = window.tinycolor; - - var SELECTED_CLASSNAME = 'selected'; - var NEW_COLOR_CLASS = 'palette-manager-new-color'; - var CLOSE_ICON_CLASS = 'palette-manager-delete-card'; - var EDIT_NAME_CLASS = 'edit-icon'; - - ns.PaletteManagerController = function (piskelController) { - this.piskelController = piskelController; - this.palettes = this.retrieveUserPalettes(); - this.originalPalettes = this.retrieveUserPalettes(); - this.selectedPaletteId = null; - - // Keep track of all spectrum instances created, to dispose them when closing the popup - this.spectrumContainers = []; - }; - - pskl.utils.inherit(ns.PaletteManagerController, ns.AbstractDialogController); - - ns.PaletteManagerController.prototype.init = function () { - this.superclass.init.call(this); - - this.palettesList = document.querySelector('.palette-manager-list'); - this.paletteBody = document.querySelector('.palette-manager-details-body'); - this.paletteHead = document.querySelector('.palette-manager-details-head'); - this.createButton = document.querySelector('.palette-manager-actions-button[data-action="create"]'); - this.saveAllButton = document.querySelector('.palette-manager-actions-button[data-action="save-all"]'); - - this.colorCardTemplate = pskl.utils.Template.get('palette-color-card-template'); - this.newColorTemplate = pskl.utils.Template.get('palette-new-color-template'); - this.paletteHeadTemplate = pskl.utils.Template.get('palette-details-head-template'); - - // Events - this.palettesList.addEventListener('click', this.onPaletteListClick.bind(this)); - // Delegated event listener for events repeated on all cards - this.paletteBody.addEventListener('click', this.delegatedPaletteBodyClick.bind(this)); - this.paletteHead.addEventListener('click', this.delegatedPaletteHeadClick.bind(this)); - this.createButton.addEventListener('click', this.onCreateClick_.bind(this)); - this.saveAllButton.addEventListener('click', this.saveAll.bind(this)); - - // Init markup - this.createPaletteListMarkup(); - if (this.palettes.length > 0) { - this.selectPalette(this.palettes[0].id); - } else { - this.createPalette('New palette'); - } - }; - - ns.PaletteManagerController.prototype.destroy = function () { - this.destroySpectrumPickers(); - }; - - ns.PaletteManagerController.prototype.onCreateClick_ = function (evt) { - this.createPalette(); - }; - - ns.PaletteManagerController.prototype.createPalette = function (name) { - if (!name) { - name = window.prompt('Please enter a name for your palette', 'New palette'); - } - if (name) { - var palette = this.createPaletteObject(name); - this.palettes.push(palette); - this.createPaletteListMarkup(); - this.selectPalette(palette.id); - } - }; - - ns.PaletteManagerController.prototype.createPaletteObject = function (name) { - return { - id : 'palette-' + Date.now() + '-' + Math.floor(Math.random()*1000), - name : name, - colors : [] - }; - }; - - ns.PaletteManagerController.prototype.redraw = function () { - this.createPaletteListMarkup(); - this.selectPalette(this.selectedPaletteId); - }; - - ns.PaletteManagerController.prototype.selectPalette = function (paletteId) { - this.deselectCurrentPalette(); - var paletteListItem = this.palettesList.querySelector('[data-palette-id='+paletteId+']'); - if (paletteListItem) { - this.selectedPaletteId = paletteId; - paletteListItem.classList.add(SELECTED_CLASSNAME); - this.refreshPaletteDetails(); - } - }; - - ns.PaletteManagerController.prototype.refreshPaletteDetails = function () { - this.createPaletteHeadMarkup(); - this.createPaletteBodyMarkup(); - this.initPaletteDetailsEvents(); - this.initPaletteCardsSpectrum(); - }; - - ns.PaletteManagerController.prototype.createPaletteListMarkup = function () { - var html = this.palettes.map(function (palette) { - var paletteCopy = { - id : palette.id, - name : this.isPaletteModified(palette) ? palette.name + " *" : palette.name - }; - return pskl.utils.Template.replace('
  • {{name}}
  • ', paletteCopy); - }.bind(this)).join(''); - this.palettesList.innerHTML = html; - }; - - /** - * Fill the palette body container with color cards for the selected palette - */ - ns.PaletteManagerController.prototype.createPaletteHeadMarkup = function () { - var palette = this.getSelectedPalette(); - var dict = { - 'name' : palette.name, - 'save:disabled' : !this.isPaletteModified(palette), - 'revert:disabled' : !this.isPaletteModified(palette), - 'delete:disabled' : this.palettes.length < 2 - }; - var html = pskl.utils.Template.replace(this.paletteHeadTemplate, dict); - - this.paletteHead.innerHTML = html; - }; - - ns.PaletteManagerController.prototype.isPaletteModified = function (palette) { - var isModified = false; - var originalPalette = this.getPaletteById(palette.id, this.originalPalettes); - if (originalPalette) { - var differentName = originalPalette.name !== palette.name; - var differentColors = palette.colors.join('') !== originalPalette.colors.join(''); - isModified = differentName || differentColors; - } else { - isModified = true; - } - return isModified; - }; - - /** - * Fill the palette body container with color cards for the selected palette - */ - ns.PaletteManagerController.prototype.createPaletteBodyMarkup = function () { - var palette = this.getSelectedPalette(); - - var html = this.getColorCardsMarkup(palette.colors); - html += pskl.utils.Template.replace(this.newColorTemplate, {classname : NEW_COLOR_CLASS}); - - this.paletteBody.innerHTML = html; - }; - - ns.PaletteManagerController.prototype.initPaletteDetailsEvents = function () { - // New Card click event - var newCard = this.paletteBody.querySelector('.' + NEW_COLOR_CLASS); - newCard.addEventListener('click', this.onNewCardClick.bind(this)); - - if (this.palettes.length < 2) { - var deleteButton = this.paletteHead.querySelector('.palette-manager-palette-button[data-action="delete"]'); - deleteButton.setAttribute("disabled", "disabled"); - } - }; - - ns.PaletteManagerController.prototype.onNewCardClick = function () { - var color; - var palette = this.getSelectedPalette(); - if (palette && palette.colors.length > 0) { - color = palette.colors[palette.colors.length-1]; - } else { - color = '#FFFFFF'; - } - this.addColorInSelectedPalette(color); - }; - - ns.PaletteManagerController.prototype.delegatedPaletteBodyClick = function (event) { - var target = event.target; - if (target.classList.contains(CLOSE_ICON_CLASS)) { - var colorId = parseInt(target.parentNode.dataset.colorId, 10); - this.removeColorInSelectedPalette(colorId); - } - }; - - ns.PaletteManagerController.prototype.delegatedPaletteHeadClick = function (event) { - var target = event.target; - if (target.classList.contains(EDIT_NAME_CLASS)) { - this.renameSelectedPalette(); - } else if (target.classList.contains('palette-manager-palette-button')) { - var action = target.dataset.action; - if (action === 'save') { - this.savePalette(this.getSelectedPalette().id); - this.redraw(); - } else if (action === 'revert') { - this.revertChanges(); - } else if (action === 'delete') { - this.deleteSelectedPalette(); - } - } - }; - - ns.PaletteManagerController.prototype.getSpectrumSelector_ = function () { - return ':not(.' + NEW_COLOR_CLASS + ')>.palette-manager-color-square'; - }; - - ns.PaletteManagerController.prototype.initPaletteCardsSpectrum = function () { - var oSelf = this; - var container = $(this.getSpectrumSelector_()); - container.spectrum({ - clickoutFiresChange : true, - showInput: true, - showButtons: false, - change : function (color) { - var target = this; - var colorId = parseInt(target.parentNode.dataset.colorId, 10); - oSelf.updateColorInSelectedPalette(colorId, color); - }, - beforeShow : function() { - var target = this; - var colorId = parseInt(target.parentNode.dataset.colorId, 10); - var palette = oSelf.getSelectedPalette(); - var color = palette.colors[colorId]; - container.spectrum("set", color); - } - }); - - this.spectrumContainers.push(container); - }; - - /** - * Destroy all spectrum instances generated by the palette manager - */ - ns.PaletteManagerController.prototype.destroySpectrumPickers = function () { - this.spectrumContainers.forEach(function (container) { - container.spectrum("destroy"); - }); - this.spectrumContainers = []; - }; - - ns.PaletteManagerController.prototype.updateColorInSelectedPalette = function (colorId, color) { - var palette = this.getSelectedPalette(); - var hexColor = '#' + (color.toHex().toUpperCase()); - palette.colors.splice(colorId, 1, hexColor); - - this.redraw(); - }; - - ns.PaletteManagerController.prototype.addColorInSelectedPalette = function (color) { - var selectedPalette = this.getSelectedPalette(); - selectedPalette.colors.push(color); - - this.redraw(); - }; - - ns.PaletteManagerController.prototype.removeColorInSelectedPalette = function (colorId) { - var palette = this.getSelectedPalette(); - palette.colors.splice(colorId, 1); - - this.redraw(); - }; - - ns.PaletteManagerController.prototype.renameSelectedPalette = function () { - var palette = this.getSelectedPalette(); - var name = window.prompt('Please enter a new name for palette "' + palette.name + '"', palette.name); - if (name) { - palette.name = name; - this.redraw(); - } - }; - - ns.PaletteManagerController.prototype.getSelectedPalette = function () { - return this.getPaletteById(this.selectedPaletteId, this.palettes); - }; - - ns.PaletteManagerController.prototype.getColorCardsMarkup = function (colors) { - var html = colors.map(function (color, index) { - var dict = { - colorId : index, - hex : color, - rgb : tinycolor(color).toRgbString(), - hsl : tinycolor(color).toHslString() - }; - return pskl.utils.Template.replace(this.colorCardTemplate, dict); - }.bind(this)).join(''); - return html; - }; - - ns.PaletteManagerController.prototype.getPaletteById = function (paletteId, palettes) { - var match = null; - - palettes.forEach(function (palette) { - if (palette.id === paletteId) { - match = palette; - } - }); - - return match; - }; - - ns.PaletteManagerController.prototype.removePaletteById = function (paletteId, palettes) { - var palette = this.getPaletteById(paletteId, palettes); - if (palette) { - var index = palettes.indexOf(palette); - palettes.splice(index, 1); - } - }; - - ns.PaletteManagerController.prototype.deselectCurrentPalette = function () { - var selectedItem = this.palettesList.querySelector('.' + SELECTED_CLASSNAME); - if (selectedItem) { - this.selectedPaletteId = null; - selectedItem.classList.remove(SELECTED_CLASSNAME); - } - }; - - ns.PaletteManagerController.prototype.revertChanges = function () { - var palette = this.getSelectedPalette(); - var originalPalette = this.getPaletteById(palette.id, this.originalPalettes); - palette.name = originalPalette.name; - palette.colors = originalPalette.colors.slice(0); - - this.redraw(); - }; - - ns.PaletteManagerController.prototype.deleteSelectedPalette = function () { - var palette = this.getSelectedPalette(); - if (this.palettes.length > 1) { - if (window.confirm('Are you sure you want to delete "' + palette.name + '" ?')) { - this.removePaletteById(palette.id, this.palettes); - this.removePaletteById(palette.id, this.originalPalettes); - - this.persistToLocalStorage(); - - this.createPaletteListMarkup(); - this.selectPalette(this.palettes[0].id); - } - } - }; - - ns.PaletteManagerController.prototype.onPaletteListClick = function (event) { - var target = event.target; - if (target.dataset.paletteId) { - this.selectPalette(target.dataset.paletteId); - } - }; - - ns.PaletteManagerController.prototype.saveAll = function () { - this.palettes.forEach(function (palette) { - this.savePalette(palette.id); - }.bind(this)); - - this.redraw(); - }; - - ns.PaletteManagerController.prototype.savePalette = function (paletteId) { - var palette = this.getPaletteById(paletteId, this.palettes); - var originalPalette = this.getPaletteById(paletteId, this.originalPalettes); - if (originalPalette) { - originalPalette.name = palette.name; - originalPalette.colors = palette.colors; - } else { - this.originalPalettes.push(palette); - } - - this.persistToLocalStorage(); - - $.publish(Events.SHOW_NOTIFICATION, [{"content": "Palette " + palette.name + " successfully saved !"}]); - window.setTimeout($.publish.bind($, Events.HIDE_NOTIFICATION), 2000); - }; - - ns.PaletteManagerController.prototype.persistToLocalStorage = function () { - window.localStorage.setItem('piskel.palettes', JSON.stringify(this.originalPalettes)); - this.originalPalettes = this.retrieveUserPalettes(); - $.publish(Events.PALETTE_LIST_UPDATED); - }; - - ns.PaletteManagerController.prototype.retrieveUserPalettes = function () { - var palettesString = window.localStorage.getItem('piskel.palettes'); - return JSON.parse(palettesString) || []; - }; - -})(); \ No newline at end of file diff --git a/src/js/controller/piskel/PiskelController.js b/src/js/controller/piskel/PiskelController.js index ed03a303..d4eb301e 100644 --- a/src/js/controller/piskel/PiskelController.js +++ b/src/js/controller/piskel/PiskelController.js @@ -158,7 +158,7 @@ ns.PiskelController.prototype.getFrameCount = function () { var layer = this.piskel.getLayerAt(0); - return layer.length(); + return layer.size(); }; ns.PiskelController.prototype.setCurrentFrameIndex = function (index) { @@ -205,6 +205,18 @@ } }; + ns.PiskelController.prototype.mergeDownLayerAt = function (index) { + var layer = this.getLayerByIndex(index); + var downLayer = this.getLayerByIndex(index-1); + if (layer && downLayer) { + var mergedLayer = pskl.utils.LayerUtils.mergeLayers(layer, downLayer); + this.removeLayerAt(index); + this.piskel.addLayerAt(mergedLayer, index); + this.removeLayerAt(index-1); + this.selectLayer(mergedLayer); + } + }; + ns.PiskelController.prototype.generateLayerName_ = function () { var name = "Layer " + this.layerIdCounter; while (this.hasLayerForName_(name)) { diff --git a/src/js/controller/piskel/PublicPiskelController.js b/src/js/controller/piskel/PublicPiskelController.js index 39264cef..e5da46bf 100644 --- a/src/js/controller/piskel/PublicPiskelController.js +++ b/src/js/controller/piskel/PublicPiskelController.js @@ -99,6 +99,12 @@ $.publish(Events.PISKEL_RESET); }; + ns.PublicPiskelController.prototype.mergeDownLayerAt = function (index) { + this.raiseSaveStateEvent_(this.piskelController.mergeDownLayerAt, [index]); + this.piskelController.mergeDownLayerAt(index); + $.publish(Events.PISKEL_RESET); + }; + ns.PublicPiskelController.prototype.moveLayerUp = function () { this.raiseSaveStateEvent_(this.piskelController.moveLayerUp, []); this.piskelController.moveLayerUp(); diff --git a/src/js/controller/settings/GifExportController.js b/src/js/controller/settings/GifExportController.js index c0b68eaf..02618498 100644 --- a/src/js/controller/settings/GifExportController.js +++ b/src/js/controller/settings/GifExportController.js @@ -36,9 +36,6 @@ this.downloadButton = $(".gif-download-button"); this.downloadButton.click(this.onDownloadButtonClick_.bind(this)); - this.exportProgressStatusEl = document.querySelector('.gif-export-progress-status'); - this.exportProgressBarEl = document.querySelector('.gif-export-progress-bar'); - this.createOptionElements_(); }; @@ -123,29 +120,19 @@ }); } + $.publish(Events.SHOW_PROGRESS, [{"name": 'Building animated GIF ...'}]); gif.on('progress', function(percentage) { - this.updateProgressStatus_((percentage*100).toFixed(2)); + $.publish(Events.UPDATE_PROGRESS, [{"progress": (percentage*100).toFixed(1)}]); }.bind(this)); gif.on('finished', function(blob) { - this.hideProgressStatus_(); + $.publish(Events.HIDE_PROGRESS); pskl.utils.FileUtils.readFile(blob, cb); }.bind(this)); gif.render(); }; - ns.GifExportController.prototype.updateProgressStatus_ = function (percentage) { - this.exportProgressStatusEl.innerHTML = percentage + '%'; - this.exportProgressBarEl.style.width = percentage + "%"; - - }; - - ns.GifExportController.prototype.hideProgressStatus_ = function () { - this.exportProgressStatusEl.innerHTML = ''; - this.exportProgressBarEl.style.width = "0"; - }; - // FIXME : HORRIBLE COPY/PASTA ns.GifExportController.prototype.updateStatus_ = function (imageUrl, error) { diff --git a/src/js/controller/settings/PngExportController.js b/src/js/controller/settings/PngExportController.js index d4aab120..db74da66 100644 --- a/src/js/controller/settings/PngExportController.js +++ b/src/js/controller/settings/PngExportController.js @@ -34,7 +34,7 @@ var canvas = this.getFrameAsCanvas_(frame); var basename = this.pngFilePrefixInput.value; var filename = basename + (i+1) + ".png"; - zip.file(filename, pskl.CanvasUtils.getBase64FromCanvas(canvas) + '\n', {base64: true}); + zip.file(filename, pskl.utils.CanvasUtils.getBase64FromCanvas(canvas) + '\n', {base64: true}); } var fileName = this.getPiskelName_() + '.zip'; diff --git a/src/js/controller/widgets/ColorsList.js b/src/js/controller/widgets/ColorsList.js new file mode 100644 index 00000000..1c137b5c --- /dev/null +++ b/src/js/controller/widgets/ColorsList.js @@ -0,0 +1,145 @@ +(function () { + var ns = $.namespace('pskl.controller.widgets'); + + var DEFAULT_COLOR = '#000000'; + + ns.ColorsList = function (container) { + this.selectedIndex = -1; + this.palette = new pskl.model.Palette('tmp', 'tmp', []); + this.container = container; + + this.colorsList = this.container.querySelector('.colors-list'); + this.colorPreviewEl = this.container.querySelector('.color-preview'); + + $(container).sortable({ + placeholder: 'colors-list-drop-proxy', + update: this.onColorDrop_.bind(this), + items: '.create-palette-color' + }); + + this.colorsList.addEventListener('click', this.onColorContainerClick_.bind(this)); + + var colorPickerContainer = container.querySelector('.color-picker-container'); + this.hslRgbColorPicker = new pskl.controller.widgets.HslRgbColorPicker(colorPickerContainer, this.onColorUpdated_.bind(this)); + this.hslRgbColorPicker.init(); + }; + + ns.ColorsList.prototype.setColors = function (colors) { + if (colors.length === 0) { + colors.push(DEFAULT_COLOR); + } + + this.palette.setColors(colors); + + this.selectColor_(0); + this.refresh_(); + }; + + ns.ColorsList.prototype.getColors = function () { + return this.palette.getColors(); + }; + + ns.ColorsList.prototype.destroy = function () { + this.hslRgbColorPicker.destroy(); + this.container = null; + this.colorsList = null; + this.colorPreviewEl = null; + }; + + /** + * Lightweight refresh only changing the color of one element of the palette color list + */ + ns.ColorsList.prototype.refreshColorElement_ = function (index) { + var color = this.palette.get(this.selectedIndex); + var element = document.querySelector('[data-palette-index="'+index+'"]'); + if (element) { + element.style.background = color; + element.classList.toggle('light-color', this.isLight_(color)); + } + }; + + ns.ColorsList.prototype.onColorContainerClick_ = function (evt) { + var target = evt.target; + if (target.classList.contains('create-palette-color')) { + this.onPaletteColorClick_(evt, target); + } else if (target.classList.contains('create-palette-new-color')) { + this.onNewColorClick_(evt, target); + } else if (target.classList.contains('create-palette-remove-color')) { + this.onRemoveColorClick_(evt, target); + } + this.refresh_(); + }; + + ns.ColorsList.prototype.onColorUpdated_ = function (color) { + var rgbColor = color.toRgbString(); + this.colorPreviewEl.style.background = rgbColor; + if (this.palette) { + this.palette.set(this.selectedIndex, rgbColor); + this.refreshColorElement_(this.selectedIndex); + } + }; + + ns.ColorsList.prototype.onPaletteColorClick_ = function (evt, target) { + var index = parseInt(target.dataset.paletteIndex,10); + this.selectColor_(index); + }; + + ns.ColorsList.prototype.onRemoveColorClick_ = function (evt, target) { + var colorElement = target.parentNode; + var index = parseInt(colorElement.dataset.paletteIndex,10); + this.removeColor_(index); + }; + + ns.ColorsList.prototype.onNewColorClick_ = function (evt, target) { + var newColor = this.palette.get(this.selectedIndex) || '#000000'; + this.palette.add(newColor); + this.selectColor_(this.palette.size()-1); + }; + + ns.ColorsList.prototype.refresh_ = function () { + var html = ""; + var tpl = pskl.utils.Template.get('create-palette-color-template'); + var colors = this.palette.getColors(); + + colors.forEach(function (color, index) { + var isSelected = (index === this.selectedIndex); + + html += pskl.utils.Template.replace(tpl, { + 'color':color, index:index, + ':selected':isSelected, + ':light-color':this.isLight_(color) + }); + }.bind(this)); + + html += '
  • +
  • '; + + this.colorsList.innerHTML = html; + }; + + ns.ColorsList.prototype.selectColor_ = function (index) { + this.selectedIndex = index; + this.hslRgbColorPicker.setColor(this.palette.get(index)); + }; + + ns.ColorsList.prototype.removeColor_ = function (index) { + this.palette.removeAt(index); + this.refresh_(); + }; + + ns.ColorsList.prototype.isLight_ = function (color) { + var rgb = window.tinycolor(color).toRgb(); + return rgb.r+rgb.b+rgb.g > 128*3; + }; + + ns.ColorsList.prototype.onColorDrop_ = function (evt, drop) { + var colorElement = drop.item.get(0); + + var oldIndex = parseInt(colorElement.dataset.paletteIndex, 10); + var newIndex = $('.create-palette-color').index(drop.item); + this.palette.move(oldIndex, newIndex); + + this.selectedIndex = newIndex; + + this.refresh_(); + }; +})(); \ No newline at end of file diff --git a/src/js/controller/widgets/HslRgbColorPicker.js b/src/js/controller/widgets/HslRgbColorPicker.js new file mode 100644 index 00000000..f7e539d5 --- /dev/null +++ b/src/js/controller/widgets/HslRgbColorPicker.js @@ -0,0 +1,194 @@ +(function () { + var ns = $.namespace('pskl.controller.widgets'); + + ns.HslRgbColorPicker = function (container, colorUpdatedCallback) { + this.container = container; + this.colorUpdatedCallback = colorUpdatedCallback; + this.lastInputTimestamp_ = 0; + }; + + ns.HslRgbColorPicker.prototype.init = function () { + var isChromeOrFirefox = pskl.utils.UserAgent.isChrome || pskl.utils.UserAgent.isFirefox; + var changeEvent = isChromeOrFirefox ? 'input' : 'change'; + this.container.addEventListener(changeEvent, this.onPickerChange_.bind(this)); + this.container.addEventListener('keydown', this.onKeydown_.bind(this)); + + this.spectrumEl = this.container.querySelector('.color-picker-spectrum'); + + $(this.spectrumEl).spectrum({ + flat: true, + showInput: true, + showButtons: false, + move : this.setColor.bind(this), + change : this.setColor.bind(this), + preferredFormat: 'hex' + }); + + this.setColor("#000000"); + }; + + ns.HslRgbColorPicker.prototype.destroy = function () { + this.container = null; + this.spectrumEl = null; + }; + + ns.HslRgbColorPicker.prototype.onPickerChange_ = function (evt) { + var target = evt.target; + + var model = target.dataset.model; + var dimension = target.dataset.dimension; + + var value = parseInt(target.value, 10); + if (dimension === 'v' || dimension === 's') { + value = value/100; + } + + var color; + if (model === 'rgb') { + color = this.tinyColor.toRgb(); + } else if (model === 'hsv') { + color = this.hsvColor; + } + + if (isNaN(value)) { + value = color[dimension]; + } else { + color[dimension] = value; + } + + this.setColor(color); + }; + + ns.HslRgbColorPicker.prototype.onKeydown_ = function (evt) { + var target = evt.target; + + if (target.getAttribute('type').toLowerCase() === 'text') { + var value = parseInt(target.value, 10); + var dimension = target.dataset.dimension; + + var key = pskl.service.keyboard.KeycodeTranslator.toChar(evt.keyCode); + if (key === 'up') { + value = value + 1; + } else if (key === 'down') { + value = value - 1; + } + + value = this.normalizeDimension_(value, dimension); + + target.value = value; + this.onPickerChange_(evt); + } + }; + + ns.HslRgbColorPicker.prototype.setColor = function (inputColor) { + if (!this.unplugged) { + this.unplugged = true; + + this.hsvColor = this.toHsvColor_(inputColor); + this.tinyColor = this.toTinyColor_(inputColor); + + this.updateInputs(); + $(".color-picker-spectrum").spectrum("set", this.tinyColor); + + this.colorUpdatedCallback(this.tinyColor); + + this.unplugged = false; + } + }; + + ns.HslRgbColorPicker.prototype.updateInputs = function () { + var inputs = this.container.querySelectorAll('input'); + var rgb = this.tinyColor.toRgb(); + + + for (var i = 0 ; i < inputs.length ; i++) { + var input = inputs[i]; + var dimension = input.dataset.dimension; + var model = input.dataset.model; + + if (model === 'rgb') { + input.value = rgb[dimension]; + } else if (model === 'hsv') { + var value = this.hsvColor[dimension]; + if (dimension === 'v' || dimension === 's') { + value = 100 * value; + } + input.value = Math.round(value); + } + + if (input.getAttribute('type') === 'range') { + this.updateSliderBackground(input); + } + } + }; + + ns.HslRgbColorPicker.prototype.updateSliderBackground = function (slider) { + var dimension = slider.dataset.dimension; + var model = slider.dataset.model; + + var start, end; + var isHueSlider = dimension === 'h'; + if (!isHueSlider) { + var colors = this.getSliderBackgroundColors_(model, dimension); + slider.style.backgroundImage = "linear-gradient(to right, " + colors.start + " 0, " + colors.end + " 100%)"; + } + }; + + ns.HslRgbColorPicker.prototype.getSliderBackgroundColors_ = function (model, dimension) { + var start, end; + if (model === 'hsv') { + start = JSON.parse(JSON.stringify(this.hsvColor)); + start[dimension] = 0; + + end = JSON.parse(JSON.stringify(this.hsvColor)); + end[dimension] = 1; + } else { + start = this.tinyColor.toRgb(); + start[dimension] = 0; + + end = this.tinyColor.toRgb(); + end[dimension] = 255; + } + + return { + start : window.tinycolor(start).toRgbString(), + end : window.tinycolor(end).toRgbString() + }; + }; + + ns.HslRgbColorPicker.prototype.toTinyColor_ = function (color) { + if (typeof color == "object" && color.hasOwnProperty("_tc_id")) { + return color; + } else { + return window.tinycolor(JSON.parse(JSON.stringify(color))); + } + }; + + ns.HslRgbColorPicker.prototype.toHsvColor_ = function (color) { + var isHsvColor = ['h','s','v'].every(color.hasOwnProperty.bind(color)); + if (isHsvColor) { + return { + h : Math.max(0, Math.min(359, color.h)), + s : Math.max(0, Math.min(1, color.s)), + v : Math.max(0, Math.min(1, color.v)) + }; + } else { + return this.toTinyColor_(color).toHsv(); + } + }; + + ns.HslRgbColorPicker.prototype.normalizeDimension_ = function (value, dimension) { + var ranges = { + 'h' : [0, 359], + 's' : [0, 100], + 'v' : [0, 100], + 'r' : [0, 255], + 'g' : [0, 255], + 'b' : [0, 255] + }; + var range = ranges[dimension]; + return Math.max(range[0], Math.min(range[1], value)); + } ; + + +})(); \ No newline at end of file diff --git a/src/js/devtools/DrawingTestPlayer.js b/src/js/devtools/DrawingTestPlayer.js index 82dfbf3c..6c77d4d0 100644 --- a/src/js/devtools/DrawingTestPlayer.js +++ b/src/js/devtools/DrawingTestPlayer.js @@ -47,7 +47,7 @@ var then = function () {}; image.onload = function () { - this.referencePng = pskl.CanvasUtils.createFromImage(image).toDataURL(); + this.referencePng = pskl.utils.CanvasUtils.createFromImage(image).toDataURL(); then(); }.bind(this); image.src = this.referencePng; @@ -101,6 +101,10 @@ var screenCoordinates = pskl.app.drawingController.getScreenCoordinates(recordEvent.coords.x, recordEvent.coords.y); event.clientX = screenCoordinates.x; event.clientY = screenCoordinates.y; + if (pskl.utils.UserAgent.isMac && event.ctrlKey) { + event.metaKey = true; + } + if (event.type == 'mousedown') { pskl.app.drawingController.onMousedown_(event); } else if (event.type == 'mouseup') { diff --git a/src/js/lib/gif/libgif.js b/src/js/lib/gif/libgif.js index ef1d6ab4..157f4dee 100644 --- a/src/js/lib/gif/libgif.js +++ b/src/js/lib/gif/libgif.js @@ -624,6 +624,7 @@ var SuperGif = function ( opts ) { }; var load_callback = false; + var step_callback = false; var error_callback = false; var tmpCanvas = document.createElement('canvas'); @@ -632,6 +633,7 @@ var SuperGif = function ( opts ) { load: function (callback) { load_callback = callback.success; + step_callback = callback.step; error_callback = callback.error; loading = true; diff --git a/src/js/lib/spectrum/spectrum.js b/src/js/lib/spectrum/spectrum.js index 731b0ce1..b60d3894 100644 --- a/src/js/lib/spectrum/spectrum.js +++ b/src/js/lib/spectrum/spectrum.js @@ -504,8 +504,10 @@ $(doc).bind("mousedown.spectrum", onMousedown); - // Piskel-specific : change the color as soon as the user does a mouseup - $(doc).bind("mouseup.spectrum", updateColor); + if (!flat) { + // Piskel-specific : change the color as soon as the user does a mouseup + $(doc).bind("mouseup.spectrum", updateColor); + } $(window).bind("resize.spectrum", resize); replacer.addClass("sp-active"); @@ -667,10 +669,9 @@ } } - // Update the text entry input as it changes happen if (opts.showInput) { - textInput.val(realColor.toString(Constants.PREFERRED_COLOR_FORMAT || format)); + textInput.val(realColor.toString(format)); } if (opts.showPalette) { diff --git a/src/js/model/Layer.js b/src/js/model/Layer.js index 13d5977e..206aa565 100644 --- a/src/js/model/Layer.js +++ b/src/js/model/Layer.js @@ -56,7 +56,7 @@ if (this.frames[index]) { this.frames.splice(index, 1); } else { - throw 'Invalid index in removeFrameAt : ' + index + ' (size : ' + this.length() + ')'; + throw 'Invalid index in removeFrameAt : ' + index + ' (size : ' + this.size() + ')'; } }; @@ -93,7 +93,7 @@ } }; - ns.Layer.prototype.length = function () { + ns.Layer.prototype.size = function () { return this.frames.length; }; diff --git a/src/js/model/Palette.js b/src/js/model/Palette.js new file mode 100644 index 00000000..eaadedae --- /dev/null +++ b/src/js/model/Palette.js @@ -0,0 +1,46 @@ +(function () { + var ns = $.namespace('pskl.model'); + + ns.Palette = function (id, name, colors) { + this.id = id; + this.name = name; + this.colors = colors; + }; + + ns.Palette.fromObject = function (paletteObj) { + var colors = paletteObj.colors.slice(0 , paletteObj.colors.length); + return new ns.Palette(paletteObj.id, paletteObj.name, colors); + }; + + ns.Palette.prototype.getColors = function () { + return this.colors; + }; + + ns.Palette.prototype.setColors = function (colors) { + this.colors = colors; + }; + + ns.Palette.prototype.get = function (index) { + return this.colors[index]; + }; + + ns.Palette.prototype.set = function (index, color) { + this.colors[index] = color; + }; + + ns.Palette.prototype.add = function (color) { + this.colors.push(color); + }; + + ns.Palette.prototype.size = function () { + return this.colors.length; + }; + + ns.Palette.prototype.removeAt = function (index) { + this.colors.splice(index, 1); + }; + + ns.Palette.prototype.move = function (oldIndex, newIndex) { + this.colors.splice(newIndex, 0, this.colors.splice(oldIndex, 1)[0]); + }; +})(); \ No newline at end of file diff --git a/src/js/model/Piskel.js b/src/js/model/Piskel.js index ab28ec14..fcc1ebdd 100644 --- a/src/js/model/Piskel.js +++ b/src/js/model/Piskel.js @@ -33,7 +33,7 @@ */ ns.Piskel.fromLayers = function (layers, descriptor) { var piskel = null; - if (layers.length > 0 && layers[0].length() > 0) { + if (layers.length > 0 && layers[0].size() > 0) { var sampleFrame = layers[0].getFrameAt(0); piskel = new pskl.model.Piskel(sampleFrame.getWidth(), sampleFrame.getHeight(), descriptor); layers.forEach(piskel.addLayer.bind(piskel)); @@ -73,6 +73,10 @@ this.layers.push(layer); }; + ns.Piskel.prototype.addLayerAt = function (layer, index) { + this.layers.splice(index, 0, layer); + }; + ns.Piskel.prototype.moveLayerUp = function (layer) { var index = this.layers.indexOf(layer); if (index > -1 && index < this.layers.length-1) { diff --git a/src/js/rendering/CanvasRenderer.js b/src/js/rendering/CanvasRenderer.js index ffdea8af..c124c73b 100644 --- a/src/js/rendering/CanvasRenderer.js +++ b/src/js/rendering/CanvasRenderer.js @@ -25,7 +25,7 @@ var scaledCanvas = this.createCanvas_(this.zoom); var scaledContext = scaledCanvas.getContext('2d'); - pskl.CanvasUtils.disableImageSmoothing(scaledCanvas); + pskl.utils.CanvasUtils.disableImageSmoothing(scaledCanvas); scaledContext.scale(this.zoom, this.zoom); scaledContext.drawImage(canvas, 0, 0); @@ -44,6 +44,6 @@ zoom = zoom || 1; var width = this.frame.getWidth() * zoom; var height = this.frame.getHeight() * zoom; - return pskl.CanvasUtils.createCanvas(width, height); + return pskl.utils.CanvasUtils.createCanvas(width, height); }; })(); \ No newline at end of file diff --git a/src/js/rendering/FramesheetRenderer.js b/src/js/rendering/FramesheetRenderer.js index 06eb4d99..3f30b0b3 100644 --- a/src/js/rendering/FramesheetRenderer.js +++ b/src/js/rendering/FramesheetRenderer.js @@ -37,7 +37,7 @@ var count = this.frames.length; var width = count * sampleFrame.getWidth(); var height = sampleFrame.getHeight(); - return pskl.CanvasUtils.createCanvas(width, height); + return pskl.utils.CanvasUtils.createCanvas(width, height); }; })(); \ No newline at end of file diff --git a/src/js/rendering/frame/FrameRenderer.js b/src/js/rendering/frame/FrameRenderer.js index 94d5a440..03dc0c8e 100644 --- a/src/js/rendering/frame/FrameRenderer.js +++ b/src/js/rendering/frame/FrameRenderer.js @@ -70,8 +70,8 @@ }; ns.FrameRenderer.prototype.clear = function () { - pskl.CanvasUtils.clear(this.canvas); - pskl.CanvasUtils.clear(this.displayCanvas); + pskl.utils.CanvasUtils.clear(this.canvas); + pskl.utils.CanvasUtils.clear(this.displayCanvas); }; ns.FrameRenderer.prototype.setZoom = function (zoom) { @@ -153,8 +153,8 @@ var height = this.displayHeight; var width = this.displayWidth; - this.displayCanvas = pskl.CanvasUtils.createCanvas(width, height, this.classes); - pskl.CanvasUtils.disableImageSmoothing(this.displayCanvas); + this.displayCanvas = pskl.utils.CanvasUtils.createCanvas(width, height, this.classes); + pskl.utils.CanvasUtils.disableImageSmoothing(this.displayCanvas); this.container.append(this.displayCanvas); }; @@ -223,7 +223,7 @@ */ ns.FrameRenderer.prototype.renderFrame_ = function (frame) { if (!this.canvas || frame.getWidth() != this.canvas.width || frame.getHeight() != this.canvas.height) { - this.canvas = pskl.CanvasUtils.createCanvas(frame.getWidth(), frame.getHeight()); + this.canvas = pskl.utils.CanvasUtils.createCanvas(frame.getWidth(), frame.getHeight()); } var context = this.canvas.getContext('2d'); diff --git a/src/js/service/CurrentColorsService.js b/src/js/service/CurrentColorsService.js index 4aed52e7..11a02536 100644 --- a/src/js/service/CurrentColorsService.js +++ b/src/js/service/CurrentColorsService.js @@ -5,37 +5,59 @@ this.piskelController = piskelController; this.currentColors = []; this.cachedFrameProcessor = new pskl.model.frame.CachedFrameProcessor(); - this.cachedFrameProcessor.setFrameProcessor(this.frameToColors_.bind(this)); + this.cachedFrameProcessor.setFrameProcessor(this.getFrameColors_.bind(this)); - this.framesColorsCache_ = {}; + this.colorSorter = new pskl.service.color.ColorSorter(); + this.paletteService = pskl.app.paletteService; }; ns.CurrentColorsService.prototype.init = function () { $.subscribe(Events.PISKEL_RESET, this.onPiskelUpdated_.bind(this)); $.subscribe(Events.TOOL_RELEASED, this.onPiskelUpdated_.bind(this)); + $.subscribe(Events.USER_SETTINGS_CHANGED, this.onUserSettingsChange_.bind(this)); }; ns.CurrentColorsService.prototype.getCurrentColors = function () { return this.currentColors; }; - ns.CurrentColorsService.prototype.frameToColors_ = function (frame) { - var frameColors = {}; - frame.forEachPixel(function (color, x, y) { - frameColors[color] = (frameColors[color] || 0) + 1; - }); - return frameColors; + ns.CurrentColorsService.prototype.setCurrentColors = function (colors) { + if (colors.join('') !== this.currentColors.join('')) { + this.currentColors = colors; + $.publish(Events.CURRENT_COLORS_UPDATED); + } }; + ns.CurrentColorsService.prototype.onUserSettingsChange_ = function (evt, name, value) { + if (name == pskl.UserSettings.SELECTED_PALETTE) { + if (this.isCurrentColorsPaletteSelected_()) { + this.updateCurrentColors_(); + } + } + }; ns.CurrentColorsService.prototype.onPiskelUpdated_ = function (evt) { + if (this.isCurrentColorsPaletteSelected_()) { + this.updateCurrentColors_(); + } + }; + + ns.CurrentColorsService.prototype.isCurrentColorsPaletteSelected_ = function () { + var paletteId = pskl.UserSettings.get(pskl.UserSettings.SELECTED_PALETTE); + var palette = this.paletteService.getPaletteById(paletteId); + + return palette.id === Constants.CURRENT_COLORS_PALETTE_ID; + }; + + ns.CurrentColorsService.prototype.updateCurrentColors_ = function () { var layers = this.piskelController.getLayers(); var frames = layers.map(function (l) {return l.getFrames();}).reduce(function (p, n) {return p.concat(n);}); var colors = {}; + frames.forEach(function (f) { var frameColors = this.cachedFrameProcessor.get(f); Object.keys(frameColors).slice(0, Constants.MAX_CURRENT_COLORS_DISPLAYED).forEach(function (color) { - colors[color] = (colors[color] || 0) + frameColors[color]; + colors[color] = true; }); }.bind(this)); @@ -43,14 +65,36 @@ delete colors[Constants.TRANSPARENT_COLOR]; // limit the array to the max colors to display - this.currentColors = Object.keys(colors).slice(0, Constants.MAX_CURRENT_COLORS_DISPLAYED); + var colorsArray = Object.keys(colors).slice(0, Constants.MAX_CURRENT_COLORS_DISPLAYED); + var currentColors = this.colorSorter.sort(colorsArray); - // sort by most frequent color - this.currentColors = this.currentColors.sort(function (c1, c2) { - return colors[c2] - colors[c1]; - }); + this.setCurrentColors(currentColors); + }; - // TODO : only fire if there was a change - $.publish(Events.CURRENT_COLORS_UPDATED, colors); + ns.CurrentColorsService.prototype.getFrameColors_ = function (frame) { + var frameColors = {}; + frame.forEachPixel(function (color, x, y) { + var hexColor = this.toHexString_(color); + frameColors[hexColor] = true; + }.bind(this)); + return frameColors; + }; + + ns.CurrentColorsService.prototype.toHexString_ = function (color) { + if (color === Constants.TRANSPARENT_COLOR) { + return color; + } else { + color = color.replace(/\s/g, ''); + var hexRe = (/^#([a-f0-9]{3}){1,2}$/i); + var rgbRe = (/^rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)$/i); + if (hexRe.test(color)) { + return color.toUpperCase(); + } else if (rgbRe.test(color)) { + var exec = rgbRe.exec(color); + return pskl.utils.rgbToHex(exec[1] * 1, exec[2] * 1, exec[3] * 1); + } else { + console.error('Could not convert color to hex : ', color); + } + } }; })(); \ No newline at end of file diff --git a/src/js/service/FileDropperService.js b/src/js/service/FileDropperService.js index bf9104ad..5bc2a3c3 100644 --- a/src/js/service/FileDropperService.js +++ b/src/js/service/FileDropperService.js @@ -31,14 +31,27 @@ for (var i = 0; i < files.length ; i++) { var file = files[i]; var isImage = file.type.indexOf('image') === 0; + var isPiskel = /\.piskel$/i.test(file.name); + var isPalette = /\.(gpl|txt)$/i.test(file.name); if (isImage) { this.readImageFile_(file); - } else if (/\.piskel$/i.test(file.name)) { + } else if (isPiskel) { pskl.utils.PiskelFileUtils.loadFromFile(file, this.onPiskelFileLoaded_); + } else if (isPalette) { + pskl.app.paletteImportService.read(file, this.onPaletteLoaded_.bind(this)); } } }; + ns.FileDropperService.prototype.readImageFile_ = function (imageFile) { + pskl.utils.FileUtils.readFile(imageFile, this.processImageSource_.bind(this)); + }; + + ns.FileDropperService.prototype.onPaletteLoaded_ = function (palette) { + pskl.app.paletteService.savePalette(palette); + pskl.UserSettings.set(pskl.UserSettings.SELECTED_PALETTE, palette.id); + }; + ns.FileDropperService.prototype.onPiskelFileLoaded_ = function (piskel, descriptor, fps) { if (window.confirm('This will replace your current animation')) { piskel.setDescriptor(descriptor); @@ -47,10 +60,6 @@ } }; - ns.FileDropperService.prototype.readImageFile_ = function (imageFile) { - pskl.utils.FileUtils.readFile(imageFile, this.processImageSource_.bind(this)); - }; - ns.FileDropperService.prototype.processImageSource_ = function (imageSource) { this.importedImage_ = new Image(); this.importedImage_.onload = this.onImageLoaded_.bind(this); diff --git a/src/js/service/color/ColorSorter.js b/src/js/service/color/ColorSorter.js new file mode 100644 index 00000000..0f8e80c1 --- /dev/null +++ b/src/js/service/color/ColorSorter.js @@ -0,0 +1,105 @@ +(function () { + var ns = $.namespace('pskl.service.color'); + + var LOW_SAT = 0.1; + var LOW_LUM = 0.1; + var HI_LUM = 0.9; + + + var HUE_STEP = 36; + var HUE_BAGS = 10; + var HUE_BOUNDS = []; + for (var i = 0 ; i < HUE_BAGS ; i++) { + HUE_BOUNDS.push(i * HUE_STEP); + } + + ns.ColorSorter = function () { + this.colorsHslMap_ = {}; + }; + + ns.ColorSorter.prototype.sort = function (colors) { + this.colorsHslMap_ = {}; + + colors.forEach(function (color) { + this.colorsHslMap_[color] = window.tinycolor(color).toHsl(); + }.bind(this)); + + // sort by most frequent color + var darkColors = colors.filter(function (c) { + var hsl = this.colorsHslMap_[c]; + return hsl.l <= LOW_LUM; + }.bind(this)); + + var brightColors = colors.filter(function (c) { + var hsl = this.colorsHslMap_[c]; + return hsl.l >= HI_LUM; + }.bind(this)); + + var desaturatedColors = colors.filter(function (c) { + return brightColors.indexOf(c) === -1 && darkColors.indexOf(c) === -1; + }).filter(function (c) { + var hsl = this.colorsHslMap_[c]; + return hsl.s <= LOW_SAT; + }.bind(this)); + + darkColors = this.sortOnHslProperty_(darkColors, 'l'); + brightColors = this.sortOnHslProperty_(brightColors, 'l'); + desaturatedColors = this.sortOnHslProperty_(desaturatedColors, 'h'); + + var sortedColors = darkColors.concat(brightColors, desaturatedColors); + + var regularColors = colors.filter(function (c) { + return sortedColors.indexOf(c) === -1; + }); + + var regularColorsBags = HUE_BOUNDS.map(function (hue) { + var bagColors = regularColors.filter(function (color) { + var hsl = this.colorsHslMap_[color]; + return (hsl.h >= hue && hsl.h < hue + HUE_STEP); + }.bind(this)); + + return this.sortRegularColors_(bagColors); + }.bind(this)); + + return Array.prototype.concat.apply(sortedColors, regularColorsBags); + }; + + ns.ColorSorter.prototype.sortRegularColors_ = function (colors) { + var sortedColors = colors.sort(function (c1, c2) { + var hsl1 = this.colorsHslMap_[c1]; + var hsl2 = this.colorsHslMap_[c2]; + var hDiff = Math.abs(hsl1.h - hsl2.h); + var sDiff = Math.abs(hsl1.s - hsl2.s); + var lDiff = Math.abs(hsl1.l - hsl2.l); + if (hDiff < 10) { + if (sDiff > lDiff) { + return this.compareValues_(hsl1.s, hsl2.s); + } else { + return this.compareValues_(hsl1.l, hsl2.l); + } + } else { + return this.compareValues_(hsl1.h, hsl2.h); + } + }.bind(this)); + + return sortedColors; + }; + + ns.ColorSorter.prototype.sortOnHslProperty_ = function (colors, property) { + return colors.sort(function (c1, c2) { + var hsl1 = this.colorsHslMap_[c1]; + var hsl2 = this.colorsHslMap_[c2]; + return this.compareValues_(hsl1[property], hsl2[property]); + }.bind(this)); + }; + + ns.ColorSorter.prototype.compareValues_ = function (v1, v2) { + if (v1 > v2) { + return 1; + } else if (v1 < v2) { + return -1; + } + return 0; + }; + +})(); \ No newline at end of file diff --git a/src/js/service/keyboard/CheatsheetService.js b/src/js/service/keyboard/CheatsheetService.js index 03c085bc..d319784c 100644 --- a/src/js/service/keyboard/CheatsheetService.js +++ b/src/js/service/keyboard/CheatsheetService.js @@ -11,12 +11,12 @@ throw 'cheatsheetEl_ DOM element could not be retrieved'; } this.initMarkup_(); - pskl.app.shortcutService.addShortcut('shift+?', this.toggleCheatsheet_.bind(this)); - pskl.app.shortcutService.addShortcut('?', this.toggleCheatsheet_.bind(this)); + pskl.app.shortcutService.addShortcuts(['?', 'shift+?'], this.toggleCheatsheet_.bind(this)); var link = $('.cheatsheet-link'); link.click(this.toggleCheatsheet_.bind(this)); + $.subscribe(Events.TOGGLE_HELP, this.toggleCheatsheet_.bind(this)); $.subscribe(Events.ESCAPE, this.onEscape_.bind(this)); }; @@ -106,7 +106,8 @@ this.toDescriptor_('N', 'Create new frame'), this.toDescriptor_('shift + N', 'Duplicate selected frame'), this.toDescriptor_('shift + ?', 'Open/Close this popup'), - this.toDescriptor_('alt + P', 'Open the Palette Manager'), + this.toDescriptor_('alt + P', 'Create a Palette'), + this.toDescriptor_('</>', 'Select previous/next palette color'), this.toDescriptor_('alt + O', 'Toggle Onion Skin'), this.toDescriptor_('alt + L', 'Toggle Layer Preview') ]; diff --git a/src/js/service/keyboard/KeycodeTranslator.js b/src/js/service/keyboard/KeycodeTranslator.js index 23269a20..a9e352ad 100644 --- a/src/js/service/keyboard/KeycodeTranslator.js +++ b/src/js/service/keyboard/KeycodeTranslator.js @@ -7,7 +7,9 @@ 40 : "down", 46 : "del", 189 : "-", - 187 : "+" + 187 : "+", + 188 : "<", + 190 : ">" }; var ns = $.namespace('pskl.service.keyboard'); diff --git a/src/js/service/keyboard/ShortcutService.js b/src/js/service/keyboard/ShortcutService.js index bc0f77b3..960deb18 100644 --- a/src/js/service/keyboard/ShortcutService.js +++ b/src/js/service/keyboard/ShortcutService.js @@ -35,6 +35,12 @@ } }; + ns.ShortcutService.prototype.addShortcuts = function (keys, callback) { + keys.forEach(function (key) { + this.addShortcut(key, callback); + }.bind(this)); + }; + ns.ShortcutService.prototype.removeShortcut = function (rawKey) { var parsedKey = this.parseKey_(rawKey.toLowerCase()); diff --git a/src/js/service/palette/CurrentColorsPalette.js b/src/js/service/palette/CurrentColorsPalette.js new file mode 100644 index 00000000..2120872b --- /dev/null +++ b/src/js/service/palette/CurrentColorsPalette.js @@ -0,0 +1,12 @@ +(function () { + var ns = $.namespace('pskl.service.palette'); + + ns.CurrentColorsPalette = function () { + this.name = 'Current colors'; + this.id = Constants.CURRENT_COLORS_PALETTE_ID; + }; + + ns.CurrentColorsPalette.prototype.getColors = function () { + return pskl.app.currentColorsService.getCurrentColors(); + }; +})(); \ No newline at end of file diff --git a/src/js/service/palette/PaletteGplReader.js b/src/js/service/palette/PaletteGplReader.js new file mode 100644 index 00000000..76173859 --- /dev/null +++ b/src/js/service/palette/PaletteGplReader.js @@ -0,0 +1,50 @@ +(function () { + var ns = $.namespace('pskl.service.palette'); + + var RE_COLOR_LINE = /^(\s*\d{1,3})(\s*\d{1,3})(\s*\d{1,3})/; + var RE_EXTRACT_NAME = /^name\s*\:\s*(.*)$/i; + + ns.PaletteGplReader = function (file, onSuccess, onError) { + this.file = file; + this.onSuccess = onSuccess; + this.onError = onError; + }; + + ns.PaletteGplReader.prototype.read = function () { + pskl.utils.FileUtils.readFile(this.file, this.onFileLoaded_.bind(this)); + }; + + ns.PaletteGplReader.prototype.onFileLoaded_ = function (content) { + var text = pskl.utils.Base64.toText(content); + var lines = text.match(/[^\r\n]+/g); + + var name = lines.map(function (l) { + var matches = l.match(RE_EXTRACT_NAME); + return matches ? matches[1] : ''; + }).join(''); + + var colorLines = lines.filter(function (l) { + return RE_COLOR_LINE.test(l); + }); + + var colors = colorLines.map(function (l) { + var matches = l.match(RE_COLOR_LINE); + var color = window.tinycolor({ + r : parseInt(matches[1], 10), + g : parseInt(matches[2], 10), + b : parseInt(matches[3], 10) + }); + + return color.toRgbString(); + }); + + if (name && colors.length) { + var uuid = pskl.utils.Uuid.generate(); + var palette = new pskl.model.Palette(uuid, name, colors); + this.onSuccess(palette); + } else { + this.onError(); + } + + }; +})(); \ No newline at end of file diff --git a/src/js/service/palette/PaletteGplWriter.js b/src/js/service/palette/PaletteGplWriter.js new file mode 100644 index 00000000..4d8ae0ae --- /dev/null +++ b/src/js/service/palette/PaletteGplWriter.js @@ -0,0 +1,40 @@ +(function () { + var ns = $.namespace('pskl.service.palette'); + + ns.PaletteGplWriter = function (palette) { + this.palette = palette; + }; + + ns.PaletteGplWriter.prototype.write = function () { + var lines = []; + lines.push('GIMP Palette'); + lines.push('Name: ' + this.palette.name); + lines.push('Columns: 0'); + lines.push('#'); + this.palette.getColors().forEach(function (color) { + lines.push(this.writeColorLine(color)); + }.bind(this)); + lines.push('\r\n'); + + return lines.join('\r\n'); + }; + + ns.PaletteGplWriter.prototype.writeColorLine = function (color) { + var tinycolor = window.tinycolor(color); + var rgb = tinycolor.toRgb(); + var strBuffer = []; + strBuffer.push(this.padString(rgb.r, 3)); + strBuffer.push(this.padString(rgb.g, 3)); + strBuffer.push(this.padString(rgb.b, 3)); + strBuffer.push('Untitled'); + + return strBuffer.join(' '); + }; + + ns.PaletteGplWriter.prototype.padString = function (str, size) { + str = str.toString(); + var pad = (new Array(1+size-str.length)).join(' '); + return pad + str; + }; + +})(); diff --git a/src/js/service/palette/PaletteImageReader.js b/src/js/service/palette/PaletteImageReader.js new file mode 100644 index 00000000..d96b66ea --- /dev/null +++ b/src/js/service/palette/PaletteImageReader.js @@ -0,0 +1,55 @@ +(function () { + var ns = $.namespace('pskl.service.palette'); + + ns.PaletteImageReader = function (file, onSuccess, onError) { + this.file = file; + this.onSuccess = onSuccess; + this.onError = onError; + + this.colorSorter_ = new pskl.service.color.ColorSorter(); + }; + + ns.PaletteImageReader.prototype.read = function () { + pskl.utils.FileUtils.readImageFile(this.file, this.onImageLoaded_.bind(this)); + }; + + ns.PaletteImageReader.prototype.onImageLoaded_ = function (image) { + var imageProcessor = new pskl.worker.ImageProcessor(image, + this.onWorkerSuccess_.bind(this), + this.onWorkerStep_.bind(this), + this.onWorkerError_.bind(this)); + + + $.publish(Events.SHOW_PROGRESS, [{"name": 'Processing image colors ...'}]); + + imageProcessor.process(); + }; + + ns.PaletteImageReader.prototype.onWorkerSuccess_ = function (event) { + var data = event.data; + var colorsMap = data.colorsMap; + + var colors = Object.keys(colorsMap); + + if (colors.length > 200) { + this.onError('Too many colors : ' + colors.length); + } else { + var uuid = pskl.utils.Uuid.generate(); + var sortedColors = this.colorSorter_.sort(colors); + var palette = new pskl.model.Palette(uuid, this.file.name + ' palette', sortedColors); + + this.onSuccess(palette); + } + $.publish(Events.HIDE_PROGRESS); + }; + + ns.PaletteImageReader.prototype.onWorkerStep_ = function (event) { + var progress = event.data.progress; + $.publish(Events.UPDATE_PROGRESS, [{"progress": progress}]); + }; + + ns.PaletteImageReader.prototype.onWorkerError_ = function (event) { + $.publish(Events.HIDE_PROGRESS); + this.onError('Unable to process the image : ' + event.data.message); + }; +})(); \ No newline at end of file diff --git a/src/js/service/palette/PaletteImportService.js b/src/js/service/palette/PaletteImportService.js new file mode 100644 index 00000000..fe618dac --- /dev/null +++ b/src/js/service/palette/PaletteImportService.js @@ -0,0 +1,49 @@ +(function () { + var ns = $.namespace('pskl.service.palette'); + + var fileReaders = { + 'gpl' : ns.PaletteGplReader, + 'txt' : ns.PaletteTxtReader + }; + + ns.PaletteImportService = function () {}; + + ns.PaletteImportService.prototype.read = function (file, onSuccess, onError) { + var reader = this.getReader_(file, onSuccess, onError); + if (reader) { + reader.read(); + } else { + throw 'Could not find reader for file : ' + file.name; + } + }; + + ns.PaletteImportService.prototype.isImage_ = function (file) { + return file.type.indexOf('image') === 0; + }; + + ns.PaletteImportService.prototype.getReader_ = function (file, onSuccess, onError) { + var readerClass = this.getReaderClass_(file); + if (readerClass) { + return new readerClass(file, onSuccess, onError); + } else { + return null; + } + }; + + ns.PaletteImportService.prototype.getReaderClass_ = function (file) { + var readerClass; + if (this.isImage_(file)) { + readerClass = ns.PaletteImageReader; + } else { + var extension = this.getExtension_(file); + readerClass = fileReaders[extension]; + } + return readerClass; + }; + + ns.PaletteImportService.prototype.getExtension_ = function (file) { + var parts = file.name.split('.'); + var extension = parts[parts.length-1]; + return extension.toLowerCase(); + }; +})(); \ No newline at end of file diff --git a/src/js/service/palette/PaletteService.js b/src/js/service/palette/PaletteService.js new file mode 100644 index 00000000..d02e7708 --- /dev/null +++ b/src/js/service/palette/PaletteService.js @@ -0,0 +1,72 @@ +(function () { + var ns = $.namespace('pskl.service.palette'); + + ns.PaletteService = function () { + this.dynamicPalettes = []; + this.localStorageService = window.localStorage; + }; + + ns.PaletteService.prototype.getPalettes = function () { + var palettesString = this.localStorageService.getItem('piskel.palettes'); + var palettes = JSON.parse(palettesString) || []; + palettes = palettes.map(function (palette) { + return pskl.model.Palette.fromObject(palette); + }); + + return this.dynamicPalettes.concat(palettes); + }; + + ns.PaletteService.prototype.getPaletteById = function (paletteId) { + var palettes = this.getPalettes(); + return this.findPaletteInArray_(paletteId, palettes); + }; + + ns.PaletteService.prototype.savePalette = function (palette) { + var palettes = this.getPalettes(); + var existingPalette = this.findPaletteInArray_(palette.id, palettes); + if (existingPalette) { + var currentIndex = palettes.indexOf(existingPalette); + palettes.splice(currentIndex, 1, palette); + } else { + palettes.push(palette); + } + + this.savePalettes_(palettes); + + $.publish(Events.SHOW_NOTIFICATION, [{"content": "Palette " + palette.name + " successfully saved !"}]); + window.setTimeout($.publish.bind($, Events.HIDE_NOTIFICATION), 2000); + }; + + ns.PaletteService.prototype.addDynamicPalette = function (palette) { + this.dynamicPalettes.push(palette); + }; + + ns.PaletteService.prototype.deletePaletteById = function (id) { + var palettes = this.getPalettes(); + var filteredPalettes = palettes.filter(function (palette) { + return palette.id !== id; + }); + + this.savePalettes_(filteredPalettes); + }; + + ns.PaletteService.prototype.savePalettes_ = function (palettes) { + palettes = palettes.filter(function (palette) { + return this.dynamicPalettes.indexOf(palette) === -1; + }.bind(this)); + this.localStorageService.setItem('piskel.palettes', JSON.stringify(palettes)); + $.publish(Events.PALETTE_LIST_UPDATED); + }; + + ns.PaletteService.prototype.findPaletteInArray_ = function (paletteId, palettes) { + var match = null; + + palettes.forEach(function (palette) { + if (palette.id === paletteId) { + match = palette; + } + }); + + return match; + }; +})(); \ No newline at end of file diff --git a/src/js/service/palette/PaletteTxtReader.js b/src/js/service/palette/PaletteTxtReader.js new file mode 100644 index 00000000..ee7dcf66 --- /dev/null +++ b/src/js/service/palette/PaletteTxtReader.js @@ -0,0 +1,38 @@ +(function () { + var ns = $.namespace('pskl.service.palette'); + + var RE_COLOR_LINE = /^[A-F0-9]{2}([A-F0-9]{2})([A-F0-9]{2})([A-F0-9]{2})/; + + ns.PaletteTxtReader = function (file, onSuccess, onError) { + this.file = file; + this.onSuccess = onSuccess; + this.onError = onError; + }; + + ns.PaletteTxtReader.prototype.read = function () { + pskl.utils.FileUtils.readFile(this.file, this.onFileLoaded_.bind(this)); + }; + + ns.PaletteTxtReader.prototype.onFileLoaded_ = function (content) { + var text = pskl.utils.Base64.toText(content); + var lines = text.match(/[^\r\n]+/g); + + var colorLines = lines.filter(function (l) { + return RE_COLOR_LINE.test(l); + }); + + var colors = colorLines.map(function (l) { + var matches = l.match(RE_COLOR_LINE); + var color = "#" + matches[1] + matches[2] + matches[3]; + return color; + }); + + if (colors.length) { + var uuid = pskl.utils.Uuid.generate(); + var palette = new pskl.model.Palette(uuid, 'Imported palette', colors); + this.onSuccess(palette); + } else { + this.onError(); + } + }; +})(); \ No newline at end of file diff --git a/src/js/utils/Base64.js b/src/js/utils/Base64.js index 8e8273f8..6652f635 100644 --- a/src/js/utils/Base64.js +++ b/src/js/utils/Base64.js @@ -13,6 +13,10 @@ } ns.Base64 = { + toText : function (base64) { + return window.atob(base64.replace(/data\:.*?\;base64\,/,'')); + }, + decode : function(base64) { var outptr = 0; var last = [0, 0]; diff --git a/src/js/utils/BlobUtils.js b/src/js/utils/BlobUtils.js index 8ef595bf..169c2f96 100644 --- a/src/js/utils/BlobUtils.js +++ b/src/js/utils/BlobUtils.js @@ -1,6 +1,8 @@ (function () { var ns = $.namespace('pskl.utils'); + + var BASE64_REGEX = /\s*;\s*base64\s*(?:;|$)/i; ns.BlobUtils = { diff --git a/src/js/utils/CanvasUtils.js b/src/js/utils/CanvasUtils.js index 3edbe7c7..da3c1927 100644 --- a/src/js/utils/CanvasUtils.js +++ b/src/js/utils/CanvasUtils.js @@ -1,13 +1,13 @@ (function () { - var ns = $.namespace("pskl"); + var ns = $.namespace('pskl.utils'); ns.CanvasUtils = { createCanvas : function (width, height, classList) { - var canvas = document.createElement("canvas"); - canvas.setAttribute("width", width); - canvas.setAttribute("height", height); + var canvas = document.createElement('canvas'); + canvas.setAttribute('width', width); + canvas.setAttribute('height', height); - if (typeof classList == "string") { + if (typeof classList == 'string') { classList = [classList]; } if (Array.isArray(classList)) { @@ -20,14 +20,14 @@ }, createFromImageData : function (imageData) { - var canvas = pskl.CanvasUtils.createCanvas(imageData.width, imageData.height); + var canvas = pskl.utils.CanvasUtils.createCanvas(imageData.width, imageData.height); var context = canvas.getContext('2d'); context.putImageData(imageData, 0, 0); return canvas; }, createFromImage : function (image) { - var canvas = pskl.CanvasUtils.createCanvas(image.width, image.height); + var canvas = pskl.utils.CanvasUtils.createCanvas(image.width, image.height); var context = canvas.getContext('2d'); context.drawImage(image, 0, 0); return canvas; @@ -51,12 +51,12 @@ clear : function (canvas) { if (canvas) { - canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height); + canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height); } }, clone : function (canvas) { - var clone = pskl.CanvasUtils.createCanvas(canvas.width, canvas.height); + var clone = pskl.utils.CanvasUtils.createCanvas(canvas.width, canvas.height); //apply the old canvas to the new one clone.getContext('2d').drawImage(canvas, 0, 0); @@ -71,8 +71,8 @@ }, getBase64FromCanvas : function (canvas, format) { - format = format || "png"; - var data = canvas.toDataURL("image/" + format); + format = format || 'png'; + var data = canvas.toDataURL('image/' + format); return data.substr(data.indexOf(',')+1); } }; diff --git a/src/js/utils/FileUtils.js b/src/js/utils/FileUtils.js index 645ddc2d..4189e325 100644 --- a/src/js/utils/FileUtils.js +++ b/src/js/utils/FileUtils.js @@ -14,6 +14,14 @@ reader.readAsDataURL(file); }, + readImageFile : function (file, callback) { + ns.FileUtils.readFile(file, function (content) { + var image = new Image(); + image.onload = callback.bind(null, image); + image.src = content; + }); + }, + downloadAsFile : function (content, filename) { var saveAs = window.saveAs || (navigator.msSaveBlob && navigator.msSaveBlob.bind(navigator)); if (saveAs) { diff --git a/src/js/utils/FrameUtils.js b/src/js/utils/FrameUtils.js index b98c2f43..a2094d43 100644 --- a/src/js/utils/FrameUtils.js +++ b/src/js/utils/FrameUtils.js @@ -2,6 +2,14 @@ var ns = $.namespace('pskl.utils'); var colorCache = {}; ns.FrameUtils = { + toImage : function (frame, zoom, bgColor) { + zoom = zoom || 1; + bgColor = bgColor || Constants.TRANSPARENT_COLOR; + var canvasRenderer = new pskl.rendering.CanvasRenderer(frame, zoom); + canvasRenderer.drawTransparentAs(bgColor); + return canvasRenderer.render(); + }, + merge : function (frames) { var merged = null; if (frames.length) { @@ -28,6 +36,45 @@ return pskl.utils.FrameUtils.createFromImage(resizedImage); }, + /* + * Create a pskl.model.Frame from an Image object. + * Transparent pixels will either be converted to completely opaque or completely transparent pixels. + * @param {Image} image source image + * @return {pskl.model.Frame} corresponding frame + */ + createFromImage : function (image) { + var w = image.width, + h = image.height; + var canvas = pskl.utils.CanvasUtils.createCanvas(w, h); + var context = canvas.getContext('2d'); + + context.drawImage(image, 0,0,w,h,0,0,w,h); + var imgData = context.getImageData(0,0,w,h).data; + return pskl.utils.FrameUtils.createFromImageData_(imgData, w, h); + }, + + createFromImageData_ : function (imageData, width, height) { + // Draw the zoomed-up pixels to a different canvas context + var grid = []; + for (var x = 0 ; x < width ; x++){ + grid[x] = []; + for (var y = 0 ; y < height ; y++){ + // Find the starting index in the one-dimensional image data + var i = (y * width + x)*4; + var r = imageData[i ]; + var g = imageData[i+1]; + var b = imageData[i+2]; + var a = imageData[i+3]; + if (a < 125) { + grid[x][y] = Constants.TRANSPARENT_COLOR; + } else { + grid[x][y] = pskl.utils.rgbToHex(r,g,b); + } + } + } + return pskl.model.Frame.fromPixelGrid(grid); + }, + /** * Alpha compositing using porter duff algorithm : * http://en.wikipedia.org/wiki/Alpha_compositing @@ -36,9 +83,9 @@ * @param {String} strColor2 color under * @return {String} the composite color */ - mergePixels : function (strColor1, strColor2, globalOpacity1) { - var col1 = pskl.utils.FrameUtils.toRgba(strColor1); - var col2 = pskl.utils.FrameUtils.toRgba(strColor2); + mergePixels__ : function (strColor1, strColor2, globalOpacity1) { + var col1 = pskl.utils.FrameUtils.toRgba__(strColor1); + var col2 = pskl.utils.FrameUtils.toRgba__(strColor2); if (typeof globalOpacity1 == 'number') { col1 = JSON.parse(JSON.stringify(col1)); col1.a = globalOpacity1 * col1.a; @@ -58,7 +105,7 @@ * @param {String} c color as a string * @return {Object} {r:Number,g:Number,b:Number,a:Number} */ - toRgba : function (c) { + toRgba__ : function (c) { if (colorCache[c]) { return colorCache[c]; } @@ -97,74 +144,6 @@ } colorCache[c] = color; return color; - }, - - /* - * Create a pskl.model.Frame from an Image object. - * Transparent pixels will either be converted to completely opaque or completely transparent pixels. - * @param {Image} image source image - * @return {pskl.model.Frame} corresponding frame - */ - createFromImage : function (image) { - var w = image.width, - h = image.height; - var canvas = pskl.CanvasUtils.createCanvas(w, h); - var context = canvas.getContext('2d'); - - context.drawImage(image, 0,0,w,h,0,0,w,h); - var imgData = context.getImageData(0,0,w,h).data; - return pskl.utils.FrameUtils.createFromImageData(imgData, w, h); - }, - - createFromImageData : function (imageData, width, height) { - // Draw the zoomed-up pixels to a different canvas context - var grid = []; - for (var x = 0 ; x < width ; x++){ - grid[x] = []; - for (var y = 0 ; y < height ; y++){ - // Find the starting index in the one-dimensional image data - var i = (y * width + x)*4; - var r = imageData[i ]; - var g = imageData[i+1]; - var b = imageData[i+2]; - var a = imageData[i+3]; - if (a < 125) { - grid[x][y] = Constants.TRANSPARENT_COLOR; - } else { - grid[x][y] = pskl.utils.FrameUtils.rgbToHex(r,g,b); - } - } - } - return pskl.model.Frame.fromPixelGrid(grid); - }, - - /** - * Convert a rgb(Number, Number, Number) color to hexadecimal representation - * @param {Number} r red value, between 0 and 255 - * @param {Number} g green value, between 0 and 255 - * @param {Number} b blue value, between 0 and 255 - * @return {String} hex representation of the color '#ABCDEF' - */ - rgbToHex : function (r, g, b) { - return "#" + this.componentToHex(r) + this.componentToHex(g) + this.componentToHex(b); - }, - - /** - * Convert a color component (as a Number between 0 and 255) to its string hexa representation - * @param {Number} c component value, between 0 and 255 - * @return {String} eg. '0A' - */ - componentToHex : function (c) { - var hex = c.toString(16); - return hex.length == 1 ? "0" + hex : hex; - }, - - toImage : function (frame, zoom, bgColor) { - zoom = zoom || 1; - bgColor = bgColor || Constants.TRANSPARENT_COLOR; - var canvasRenderer = new pskl.rendering.CanvasRenderer(frame, zoom); - canvasRenderer.drawTransparentAs(bgColor); - return canvasRenderer.render(); } }; })(); diff --git a/src/js/utils/ImageResizer.js b/src/js/utils/ImageResizer.js index 0e9d4749..dea39e69 100644 --- a/src/js/utils/ImageResizer.js +++ b/src/js/utils/ImageResizer.js @@ -3,12 +3,12 @@ ns.ImageResizer = { resize : function (image, targetWidth, targetHeight, smoothingEnabled) { - var canvas = pskl.CanvasUtils.createCanvas(targetWidth, targetHeight); + var canvas = pskl.utils.CanvasUtils.createCanvas(targetWidth, targetHeight); var context = canvas.getContext('2d'); context.save(); if (!smoothingEnabled) { - pskl.CanvasUtils.disableImageSmoothing(canvas); + pskl.utils.CanvasUtils.disableImageSmoothing(canvas); } context.translate(canvas.width / 2, canvas.height / 2); @@ -34,10 +34,10 @@ */ resizeNearestNeighbour : function (source, zoom, margin, marginColor) { margin = margin || 0; - var canvas = pskl.CanvasUtils.createCanvas(zoom*source.width, zoom*source.height); + var canvas = pskl.utils.CanvasUtils.createCanvas(zoom*source.width, zoom*source.height); var context = canvas.getContext('2d'); - var imgData = pskl.CanvasUtils.getImageDataFromCanvas(source); + var imgData = pskl.utils.CanvasUtils.getImageDataFromCanvas(source); var yRanges = {}, xOffset = 0, diff --git a/src/js/utils/LayerUtils.js b/src/js/utils/LayerUtils.js index 38a08e5a..91bab7e0 100644 --- a/src/js/utils/LayerUtils.js +++ b/src/js/utils/LayerUtils.js @@ -8,23 +8,35 @@ * @param {Image} image source image * @return {pskl.model.Frame} corresponding frame */ - createFromImage : function (image, frameCount) { - var w = image.width, - h = image.height, - frameWidth = w / frameCount; + createLayerFromSpritesheet : function (image, frameCount) { + var width = image.width, + height = image.height, + frameWidth = width / frameCount; - var canvas = pskl.CanvasUtils.createCanvas(w, h); + var canvas = pskl.utils.CanvasUtils.createCanvas(frameWidth, height); var context = canvas.getContext('2d'); - context.drawImage(image, 0,0,w,h,0,0,w,h); // Draw the zoomed-up pixels to a different canvas context var frames = []; for (var i = 0 ; i < frameCount ; i++) { - var imgData = context.getImageData(frameWidth*i,0,frameWidth,h).data; - var frame = pskl.utils.FrameUtils.createFromImageData(imgData, frameWidth, h); + context.clearRect(0, 0 , frameWidth, height); + context.drawImage(image, frameWidth * i, 0, frameWidth, height, 0, 0, frameWidth, height); + var frame = pskl.utils.FrameUtils.createFromImage(canvas); frames.push(frame); } return frames; + }, + + mergeLayers : function (layerA, layerB) { + var framesA = layerA.getFrames(); + var framesB = layerB.getFrames(); + var mergedFrames = []; + framesA.forEach(function (frame, index) { + var otherFrame = framesB[index]; + mergedFrames.push(pskl.utils.FrameUtils.merge([otherFrame, frame])); + }); + var mergedLayer = pskl.model.Layer.fromFrames(layerA.getName(), mergedFrames); + return mergedLayer; } }; diff --git a/src/js/utils/PiskelFileUtils.js b/src/js/utils/PiskelFileUtils.js index 901aa4b6..59d64aed 100644 --- a/src/js/utils/PiskelFileUtils.js +++ b/src/js/utils/PiskelFileUtils.js @@ -12,7 +12,7 @@ */ loadFromFile : function (file, onSuccess, onError) { pskl.utils.FileUtils.readFile(file, function (content) { - var rawPiskel = window.atob(content.replace(/data\:.*?\;base64\,/,'')); + var rawPiskel = pskl.utils.Base64.toText(content); var serializedPiskel = JSON.parse(rawPiskel); var fps = serializedPiskel.piskel.fps; var descriptor = new pskl.model.piskel.Descriptor(serializedPiskel.piskel.name, serializedPiskel.piskel.description, true); diff --git a/src/js/utils/Uuid.js b/src/js/utils/Uuid.js new file mode 100644 index 00000000..685d315c --- /dev/null +++ b/src/js/utils/Uuid.js @@ -0,0 +1,17 @@ +(function(){ + var ns = $.namespace('pskl.utils'); + + var s4 = function () { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + }; + + ns.Uuid = { + generate : function () { + return 'ss-s-s-s-sss'.replace(/s/g, function () { + return s4(); + }); + } + }; +})(); \ No newline at end of file diff --git a/src/js/utils/WorkerUtils.js b/src/js/utils/WorkerUtils.js new file mode 100644 index 00000000..0cd421a5 --- /dev/null +++ b/src/js/utils/WorkerUtils.js @@ -0,0 +1,17 @@ +(function () { + var ns = $.namespace('pskl.utils'); + + var workers = {}; + + ns.WorkerUtils = { + createWorker : function (worker, workerId) { + if (!workers[workerId]) { + var typedArray = [(worker+"").replace(/function \(\)\s?\{/,"").replace(/\}[^}]*$/, "")]; + var blob = new Blob(typedArray, {type: "application/javascript"}); // pass a useful mime type here + workers[workerId] = window.URL.createObjectURL(blob); + } + + return new Worker(workers[workerId]); + } + }; +})(); \ No newline at end of file diff --git a/src/js/utils/core.js b/src/js/utils/core.js index cd4226d5..2fd92c14 100644 --- a/src/js/utils/core.js +++ b/src/js/utils/core.js @@ -46,12 +46,25 @@ if (!Function.prototype.bind) { var ns = $.namespace("pskl.utils"); - ns.rgbToHex = function(r, g, b) { - if (r > 255 || g > 255 || b > 255) { - throw "Invalid color component"; - } + /** + * Convert a rgb(Number, Number, Number) color to hexadecimal representation + * @param {Number} r red value, between 0 and 255 + * @param {Number} g green value, between 0 and 255 + * @param {Number} b blue value, between 0 and 255 + * @return {String} hex representation of the color '#ABCDEF' + */ + ns.rgbToHex = function (r, g, b) { + return "#" + pskl.utils.componentToHex(r) + pskl.utils.componentToHex(g) + pskl.utils.componentToHex(b); + }; - return ((r << 16) | (g << 8) | b).toString(16); + /** + * Convert a color component (as a Number between 0 and 255) to its string hexa representation + * @param {Number} c component value, between 0 and 255 + * @return {String} eg. '0A' + */ + ns.componentToHex = function (c) { + var hex = c.toString(16); + return hex.length == 1 ? "0" + hex : hex; }; ns.normalize = function (value, def) { @@ -76,5 +89,29 @@ if (!Function.prototype.bind) { } }; + var entityMap = { + "&": "&", + "<": "<", + ">": ">", + '"': '"', + "'": ''', + "/": '/' + }; + + ns.escapeHtml= function (string) { + return String(string).replace(/[&<>"'\/]/g, function (s) { + return entityMap[s]; + }); + }; + + var reEntityMap = {}; + ns.unescapeHtml= function (string) { + Object.keys(entityMap).forEach(function(key) { + reEntityMap[key] = reEntityMap[key] || new RegExp(entityMap[key], "g"); + string = string.replace(reEntityMap[key], key); + }); + return string; + }; + })(); diff --git a/src/js/utils/serialization/Deserializer.js b/src/js/utils/serialization/Deserializer.js index 8c0b43e4..c18808e5 100644 --- a/src/js/utils/serialization/Deserializer.js +++ b/src/js/utils/serialization/Deserializer.js @@ -47,7 +47,7 @@ // 2 - attach the onload callback that will be triggered asynchronously image.onload = function () { // 5 - extract the frames from the loaded image - var frames = pskl.utils.LayerUtils.createFromImage(image, layerData.frameCount); + var frames = pskl.utils.LayerUtils.createLayerFromSpritesheet(image, layerData.frameCount); // 6 - add each image to the layer this.addFramesToLayer(frames, layer); }.bind(this); diff --git a/src/js/worker/ImageProcessor.js b/src/js/worker/ImageProcessor.js new file mode 100644 index 00000000..82b17695 --- /dev/null +++ b/src/js/worker/ImageProcessor.js @@ -0,0 +1,177 @@ +(function () { + var ns = $.namespace('pskl.worker'); + + var imageProcessorWorker = function () { + var currentStep, currentProgress, currentTotal; + + var initStepCounter_ = function (total) { + currentStep = 0; + currentProgress = 0; + currentTotal = total; + }; + + var postStep_ = function () { + currentStep = currentStep + 1; + var progress = ((currentStep / currentTotal) *100).toFixed(1); + if (progress != currentProgress) { + currentProgress = progress; + this.postMessage({ + type : 'STEP', + progress : currentProgress, + currentStep : currentStep, + total : currentTotal + }); + } + }; + + var rgbToHex = function (r, g, b) { + return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b); + }; + + var componentToHex = function (c) { + var hex = c.toString(16); + return hex.length == 1 ? "0" + hex : hex; + }; + + var imageDataToGrid = function (imageData, width, height, transparent) { + // Draw the zoomed-up pixels to a different canvas context + var grid = []; + for (var x = 0 ; x < width ; x++){ + grid[x] = []; + postStep_(); + for (var y = 0 ; y < height ; y++){ + // Find the starting index in the one-dimensional image data + var i = (y * width + x)*4; + var r = imageData[i ]; + var g = imageData[i+1]; + var b = imageData[i+2]; + var a = imageData[i+3]; + if (a < 125) { + grid[x][y] = transparent; + } else { + grid[x][y] = rgbToHex(r,g,b); + } + } + } + return grid; + }; + + var getColorsMapFromImageData = function (imageData, width, height) { + var grid = imageDataToGrid(imageData, width, height, 'transparent'); + + var colorsMap = {}; + for (var i = 0 ; i < grid.length ; i++) { + postStep_(); + for (var j = 0 ; j < grid[i].length ; j++) { + var color = grid[i][j]; + if (color != 'transparent') { + colorsMap[color] = true; + } + } + } + return colorsMap; + }; + + this.onmessage = function(event) { + try { + var data = event.data; + + initStepCounter_(data.width * 2); + + var colorsMap = getColorsMapFromImageData(data.imageData, data.width, data.height); + + this.postMessage({ + type : 'SUCCESS', + colorsMap : colorsMap + }); + } catch(e) { + this.postMessage({ + type : 'ERROR', + message : e.message + }); + } + }; + }; + + ns.ImageProcessor = function (image, onSuccess, onStep, onError) { + this.image = image; + + this.onStep = onStep; + this.onSuccess = onSuccess; + this.onError = onError; + + // var worker = pskl.utils.WorkerUtils.addPartialWorker(imageProcessorWorker, 'step-counter'); + this.worker = pskl.utils.WorkerUtils.createWorker(imageProcessorWorker, 'image-colors-processor'); + this.worker.onmessage = this.onWorkerMessage.bind(this); + }; + + ns.ImageProcessor.prototype.process = function () { + var canvas = pskl.utils.CanvasUtils.createFromImage(this.image); + var imageData = pskl.utils.CanvasUtils.getImageDataFromCanvas(canvas); + this.worker.postMessage({ + imageData : imageData, + width : this.image.width, + height : this.image.height + }); + }; + + ns.ImageProcessor.prototype.createNamespace = function (name) { + var createNamespace = (function () { + var parts = name.split('.'); + if (parts.length > 0) { + var node = this; + for (var i = 0 ; i < parts.length ; i++) { + if (!node[parts[i]]) { + node[parts[i]] = {}; + } + node = node[parts[i]]; + } + } + }); + var script = createNamespace + ""; + script = script.replace(/function \(\) \{/,"").replace(/\}[^}]*$/, ""); + script = "var name = '" + name + "';" + script; + + this.runScript(script); + }; + + ns.ImageProcessor.prototype.onWorkerMessage = function (event) { + if (event.data.type === 'STEP') { + this.onStep(event); + } else if (event.data.type === 'SUCCESS') { + this.onSuccess(event); + this.worker.terminate(); + } else if (event.data.type === 'ERROR') { + this.onError(event); + this.worker.terminate(); + } + }; + + ns.ImageProcessor.prototype.importAll__ = function (classToImport, classpath) { + this.createNamespace(classpath); + for (var key in classToImport) { + if (classToImport.hasOwnProperty(key)) { + this.addMethod(classToImport[key], classpath + '.' + key); + } + } + }; + + ns.ImageProcessor.prototype.addMethod__ = function (method, name) { + this.runScript(name + "=" + method); + }; + + ns.ImageProcessor.prototype.runScript__ = function (script) { + this.worker.postMessage({ + type : 'RUN_SCRIPT', + script : this.getScriptAsUrl(script) + }); + }; + + ns.ImageProcessor.prototype.getScriptAsUrl__ = function (script) { + var blob = new Blob([script], {type: "application/javascript"}); // pass a useful mime type here + return window.URL.createObjectURL(blob); + }; +})(); + + + diff --git a/src/piskel-script-list.js b/src/piskel-script-list.js index bd66a181..1e9e38cb 100644 --- a/src/piskel-script-list.js +++ b/src/piskel-script-list.js @@ -25,6 +25,8 @@ "js/utils/PiskelFileUtils.js", "js/utils/Template.js", "js/utils/UserSettings.js", + "js/utils/Uuid.js", + "js/utils/WorkerUtils.js", "js/utils/Xhr.js", "js/utils/serialization/Serializer.js", "js/utils/serialization/Deserializer.js", @@ -50,6 +52,7 @@ "js/model/Layer.js", "js/model/piskel/Descriptor.js", "js/model/frame/CachedFrameProcessor.js", + "js/model/Palette.js", "js/model/Piskel.js", // Selection @@ -82,6 +85,7 @@ "js/controller/ToolController.js", "js/controller/PaletteController.js", "js/controller/PalettesListController.js", + "js/controller/ProgressBarController.js", "js/controller/NotificationController.js", "js/controller/CanvasBackgroundController.js", @@ -99,13 +103,18 @@ // Dialogs sub-controllers "js/controller/dialogs/AbstractDialogController.js", - "js/controller/dialogs/PaletteManagerController.js", + "js/controller/dialogs/CreatePaletteController.js", "js/controller/dialogs/ImportImageController.js", "js/controller/dialogs/BrowseLocalController.js", + // Dialogs controller "js/controller/dialogs/DialogsController.js", + // Widget controller + "js/controller/widgets/ColorsList.js", + "js/controller/widgets/HslRgbColorPicker.js", + // Services "js/service/LocalStorageService.js", "js/service/GithubStorageService.js", @@ -113,6 +122,14 @@ "js/service/BackupService.js", "js/service/BeforeUnloadService.js", "js/service/HistoryService.js", + "js/service/color/ColorSorter.js", + "js/service/palette/CurrentColorsPalette.js", + "js/service/palette/PaletteService.js", + "js/service/palette/PaletteTxtReader.js", + "js/service/palette/PaletteGplReader.js", + "js/service/palette/PaletteGplWriter.js", + "js/service/palette/PaletteImageReader.js", + "js/service/palette/PaletteImportService.js", "js/service/SavedStatusService.js", "js/service/keyboard/ShortcutService.js", "js/service/keyboard/KeycodeTranslator.js", @@ -149,6 +166,9 @@ "js/devtools/TestRecordController.js", "js/devtools/init.js", + // Workers + "js/worker/ImageProcessor.js", + // Application controller and initialization "js/app.js", // Bonus features !! diff --git a/src/piskel-style-list.js b/src/piskel-style-list.js index ab92a308..fdb8e0c9 100644 --- a/src/piskel-style-list.js +++ b/src/piskel-style-list.js @@ -11,10 +11,12 @@ "css/tools.css", "css/icons.css", "css/cheatsheet.css", + "css/color-picker-slider.css", "css/dialogs.css", "css/dialogs-import-image.css", - "css/dialogs-manage-palettes.css", "css/dialogs-browse-local.css", + "css/dialogs-create-palette.css", + "css/notifications.css", "css/toolbox.css", "css/toolbox-layers-list.css", "css/toolbox-palettes-list.css", diff --git a/src/templates/dialogs/browse-local.html b/src/templates/dialogs/browse-local.html index 6758c5c6..cd28bdad 100644 --- a/src/templates/dialogs/browse-local.html +++ b/src/templates/dialogs/browse-local.html @@ -1,7 +1,7 @@

    Browse Local Piskels - X + X

    diff --git a/src/templates/dialogs/create-palette.html b/src/templates/dialogs/create-palette.html new file mode 100644 index 00000000..eb8c26ea --- /dev/null +++ b/src/templates/dialogs/create-palette.html @@ -0,0 +1,80 @@ +
    +

    + Create palette + X +

    +
    +
    + Name + +
    + + + +
    +
    +
    +
      +
      +
      +
      + H + + +
      +
      + S + + +
      +
      + V + + +
      +
      +
      + R + + +
      +
      + G + + +
      +
      + B + + +
      +
      +
      +
      +
      + + + + +
      +
      + + + +
      \ No newline at end of file diff --git a/src/templates/dialogs/import-image.html b/src/templates/dialogs/import-image.html index aa7ddd65..95795799 100644 --- a/src/templates/dialogs/import-image.html +++ b/src/templates/dialogs/import-image.html @@ -1,7 +1,7 @@

      Import Image - X + X

      diff --git a/src/templates/dialogs/manage-palettes.html b/src/templates/dialogs/manage-palettes.html deleted file mode 100644 index 795cbcaa..00000000 --- a/src/templates/dialogs/manage-palettes.html +++ /dev/null @@ -1,57 +0,0 @@ -
      -

      - Palette manager - X -

      -
      -
      -
      - - -
      -
        -
      -
      -
      -
      -
      -
      -
      -
      - - - - - - \ No newline at end of file diff --git a/src/templates/layers-list.html b/src/templates/layers-list.html index 7208e10d..8930f9f4 100644 --- a/src/templates/layers-list.html +++ b/src/templates/layers-list.html @@ -5,16 +5,36 @@ data-placement="top" class="layers-toggle-preview piskel-icon-eye">
      -
      - - - - +
      + + + + + + + + + + + +
      - +
        + +
        diff --git a/src/templates/misc-templates.html b/src/templates/misc-templates.html new file mode 100644 index 00000000..56d5a0b7 --- /dev/null +++ b/src/templates/misc-templates.html @@ -0,0 +1,9 @@ +
        + +
        \ No newline at end of file diff --git a/src/templates/palettes-list.html b/src/templates/palettes-list.html index 8cc447e5..09e4ce16 100644 --- a/src/templates/palettes-list.html +++ b/src/templates/palettes-list.html @@ -1,19 +1,30 @@
        -

        - Palettes - +

        + Palettes

        +
        + + + +
        + + +
        diff --git a/src/templates/settings/export.html b/src/templates/settings/export.html index 56865939..ff640f3c 100644 --- a/src/templates/settings/export.html +++ b/src/templates/settings/export.html @@ -40,7 +40,5 @@
        - -
        diff --git a/test/drawing/DrawingTests.browser.js b/test/drawing/DrawingTests.browser.js index fa7a32d7..7db7ef2b 100644 --- a/test/drawing/DrawingTests.browser.js +++ b/test/drawing/DrawingTests.browser.js @@ -4,6 +4,7 @@ "color.picker.json", "frames.fun.json", "layers.fun.json", + "layers.merge.json", "lighten.darken.json", "move.json", "pen.secondary.color.json", diff --git a/test/drawing/DrawingTests.casper.js b/test/drawing/DrawingTests.casper.js index 68e8b83f..6dd57335 100644 --- a/test/drawing/DrawingTests.casper.js +++ b/test/drawing/DrawingTests.casper.js @@ -3,6 +3,7 @@ "color.picker.json", "frames.fun.json", "layers.fun.json", + "layers.merge.json", "move.json", "pen.secondary.color.json", "squares.circles.json", diff --git a/test/drawing/tests/layers.merge.json b/test/drawing/tests/layers.merge.json new file mode 100644 index 00000000..5262e828 --- /dev/null +++ b/test/drawing/tests/layers.merge.json @@ -0,0 +1 @@ +{"events":[{"event":{"type":"mousedown","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":0,"y":0},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":0,"y":0},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":0,"y":1},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":1,"y":1},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":1,"y":2},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":1,"y":1},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":1,"y":0},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":1,"y":1},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":0,"y":1},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":0,"y":2},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":1,"y":2},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":2,"y":2},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":2,"y":1},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":2,"y":0},"type":"mouse-event"},{"event":{"type":"mouseup","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":2,"y":0},"type":"mouse-event"},{"type":"instrumented-event","methodName":"createLayer","args":[]},{"type":"color-event","color":"#942d2d","isPrimary":true},{"event":{"type":"mousedown","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":1,"y":1},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":1,"y":1},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":1,"y":2},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":1,"y":3},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":2,"y":3},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":3,"y":3},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":3,"y":2},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":3,"y":1},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":2,"y":1},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":2,"y":2},"type":"mouse-event"},{"event":{"type":"mouseup","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":2,"y":2},"type":"mouse-event"},{"type":"instrumented-event","methodName":"createLayer","args":[]},{"type":"color-event","color":"#2d9430","isPrimary":true},{"event":{"type":"mousedown","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":2,"y":2},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":2,"y":2},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":2,"y":3},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":2,"y":4},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":3,"y":4},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":4,"y":4},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":4,"y":3},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":4,"y":2},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":3,"y":2},"type":"mouse-event"},{"event":{"type":"mousemove","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":3,"y":3},"type":"mouse-event"},{"event":{"type":"mouseup","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":3,"y":3},"type":"mouse-event"},{"type":"instrumented-event","methodName":"moveLayerDown","args":[]},{"type":"instrumented-event","methodName":"mergeDownLayerAt","args":["2"]},{"type":"instrumented-event","methodName":"mergeDownLayerAt","args":["1"]},{"type":"tool-event","toolId":"tool-paint-bucket"},{"type":"color-event","color":"#2d5994","isPrimary":true},{"event":{"type":"mousedown","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":0,"y":4},"type":"mouse-event"},{"event":{"type":"mouseup","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":0,"y":4},"type":"mouse-event"},{"event":{"type":"mousedown","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":4,"y":0},"type":"mouse-event"},{"event":{"type":"mouseup","button":0,"shiftKey":false,"altKey":false,"ctrlKey":false},"coords":{"x":4,"y":0},"type":"mouse-event"}],"initialState":{"size":{"width":5,"height":5},"primaryColor":"#000000","secondaryColor":"rgba(0, 0, 0, 0)","selectedTool":"tool-pen"},"png":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAN0lEQVQIW2NkYGD4D8RgoBs5BUwzggSn6OrCxBlm6qRjEUxnZmAEavmffmUmQiVMECxiPQcuAQAoxRAISNM8EgAAAABJRU5ErkJggg=="} \ No newline at end of file diff --git a/test/js/model/PaletteTest.js b/test/js/model/PaletteTest.js new file mode 100644 index 00000000..ae710017 --- /dev/null +++ b/test/js/model/PaletteTest.js @@ -0,0 +1,23 @@ +describe("Palette", function() { + + beforeEach(function() {}); + afterEach(function() {}); + + it("moves colors correctly", function() { + // when + var colors = [ + '#000000', + '#111111', + '#222222' + ]; + var palette = new pskl.model.Palette('id', 'name', colors); + + // then + palette.move(2,0); + + // verify + expect(palette.get(0)).toBe('#222222'); + expect(palette.get(1)).toBe('#000000'); + expect(palette.get(2)).toBe('#111111'); + }); +}); \ No newline at end of file diff --git a/test/js/service/palette/PaletteServiceTest.js b/test/js/service/palette/PaletteServiceTest.js new file mode 100644 index 00000000..a2b5d38a --- /dev/null +++ b/test/js/service/palette/PaletteServiceTest.js @@ -0,0 +1,144 @@ +describe("Palette Service", function() { + var paletteService = null; + var localStorage = {}; + + var localStorageService; + + + var addPalette = function (id, name, color) { + var palette = new pskl.model.Palette(id, name, [color]); + paletteService.savePalette(palette); + }; + + var verifyPaletteIsStored = function (paletteId) { + var palette = paletteService.getPaletteById(paletteId); + expect(palette).not.toBeNull(); + return palette; + }; + + var verifyPaletteIsNotStored = function (paletteId) { + var palette = paletteService.getPaletteById(paletteId); + expect(palette).toBeNull(); + }; + + beforeEach(function() { + localStorage = {}; + + localStorageService = { + getItem : function (key) { + if (localStorage.hasOwnProperty(key)) { + return localStorage[key]; + } else { + return null; + } + }, + setItem : function (key, item) { + localStorage[key] = item; + } + }; + + paletteService = new pskl.service.palette.PaletteService(); + paletteService.localStorageService = localStorageService; + }); + + it("returns an empty array when no palette is stored", function() { + spyOn(localStorageService, 'getItem').and.callThrough(); + + var palettes = paletteService.getPalettes(); + expect(Array.isArray(palettes)).toBe(true); + expect(palettes.length).toBe(0); + expect(localStorageService.getItem).toHaveBeenCalled(); + }); + + it("can store a palette", function() { + // when + spyOn(localStorageService, 'setItem').and.callThrough(); + + var paletteId = 'palette-id'; + var paletteName = 'palette-name'; + var paletteColor = '#001122'; + + // then + addPalette(paletteId, paletteName, paletteColor); + var palettes = paletteService.getPalettes(); + + // verify + expect(localStorageService.setItem).toHaveBeenCalled(); + + expect(Array.isArray(palettes)).toBe(true); + expect(palettes.length).toBe(1); + + var retrievedPalette = paletteService.getPaletteById(paletteId); + expect(retrievedPalette).toBeDefined(); + expect(retrievedPalette.id).toBe(paletteId); + expect(retrievedPalette.name).toBe(paletteName); + + var colors = retrievedPalette.getColors(); + expect(Array.isArray(colors)).toBe(true); + expect(colors.length).toBe(1); + + var color = colors[0]; + expect(color).toBe(paletteColor); + }); + + it("updates a palette", function() { + // when + var paletteId = 'palette-id'; + var paletteName = 'palette-name'; + var paletteColor1 = '#001122'; + var paletteColor2 = '#334455'; + + // then + addPalette(paletteId, paletteName, paletteColor1); + addPalette(paletteId, paletteName, paletteColor2); + + // verify + var palettes = paletteService.getPalettes(); + expect(palettes.length).toBe(1); + + var retrievedPalette = paletteService.getPaletteById(paletteId); + var color = retrievedPalette.get(0); + expect(color).toBe(paletteColor2); + }); + + it("can delete a palette", function() { + // when + addPalette('palette-id', 'palette-name', ['#001122']); + + // then + paletteService.deletePaletteById('palette-id'); + + // verify + var palettes = paletteService.getPalettes(); + expect(palettes.length).toBe(0); + }); + + it("attempts to delete unexisting palette without side effect", function() { + // when + addPalette('palette-id', 'palette-name', ['#001122']); + + // then + var palettes = paletteService.getPalettes(); + paletteService.deletePaletteById('some-other-palette-id'); + + // verify + expect(palettes.length).toBe(1); + }); + + it("deletes the correct palette when several palettes are stored", function() { + // when + addPalette('palette-id-0', 'palette-name-0', ['#000000']); + addPalette('palette-id-1', 'palette-name-1', ['#111111']); + addPalette('palette-id-2', 'palette-name-2', ['#222222']); + + // then + paletteService.deletePaletteById('palette-id-1'); + + // verify + var palettes = paletteService.getPalettes(); + expect(palettes.length).toBe(2); + verifyPaletteIsStored('palette-id-0'); + verifyPaletteIsNotStored('palette-id-1'); + verifyPaletteIsStored('palette-id-2'); + }); +}); \ No newline at end of file diff --git a/test/js/utils/FrameUtilsTest.js b/test/js/utils/FrameUtilsTest.js new file mode 100644 index 00000000..ca9a165c --- /dev/null +++ b/test/js/utils/FrameUtilsTest.js @@ -0,0 +1,145 @@ +describe("FrameUtils suite", function() { + var black = '#000000'; + var red = '#ff0000'; + var transparent = Constants.TRANSPARENT_COLOR; + + it("merges 2 frames", function () { + var frame1 = pskl.model.Frame.fromPixelGrid([ + [black, transparent], + [transparent, black] + ]); + + var frame2 = pskl.model.Frame.fromPixelGrid([ + [transparent, red], + [red, transparent] + ]); + + var mergedFrame = pskl.utils.FrameUtils.merge([frame1, frame2]); + expect(mergedFrame.getPixel(0,0)).toBe(black); + expect(mergedFrame.getPixel(0,1)).toBe(red); + expect(mergedFrame.getPixel(1,0)).toBe(red); + expect(mergedFrame.getPixel(1,1)).toBe(black); + }); + + it("returns same frame when merging single frame", function () { + var frame1 = pskl.model.Frame.fromPixelGrid([ + [black, transparent], + [transparent, black] + ]); + + var mergedFrame = pskl.utils.FrameUtils.merge([frame1]); + expect(mergedFrame.getPixel(0,0)).toBe(black); + expect(mergedFrame.getPixel(0,1)).toBe(transparent); + expect(mergedFrame.getPixel(1,0)).toBe(transparent); + expect(mergedFrame.getPixel(1,1)).toBe(black); + }); + + var checkPixelsColor = function (frame, pixels, color) { + pixels.forEach(function (pixel) { + var pixelColor = frame.getPixel(pixel[0], pixel[1]); + expect(pixelColor).toBe(color); + }); + }; + + it ("converts an image to a frame", function () { + var frame1 = pskl.model.Frame.fromPixelGrid([ + [black, transparent], + [transparent, black] + ]); + + var image = pskl.utils.FrameUtils.toImage(frame1); + expect(image.width).toBe(2); + expect(image.height).toBe(2); + + var biggerImage = pskl.utils.FrameUtils.toImage(frame1, 3); + expect(biggerImage.width).toBe(6); + expect(biggerImage.height).toBe(6); + + var biggerFrame = pskl.utils.FrameUtils.createFromImage(biggerImage); + + checkPixelsColor(biggerFrame, [ + [0,0],[0,1],[0,2], + [1,0],[1,1],[1,2], + [2,0],[2,1],[2,2], + [3,3],[3,4],[3,5], + [4,3],[4,4],[4,5], + [5,3],[5,4],[5,5] + ], black); + + checkPixelsColor(biggerFrame, [ + [0,3],[0,4],[0,5], + [1,3],[1,4],[1,5], + [2,3],[2,4],[2,5], + [3,0],[3,1],[3,2], + [4,0],[4,1],[4,2], + [5,0],[5,1],[5,2] + ], transparent); + }); + + it ("[LayerUtils] creates a layer from a simple spritesheet", function () { + var frame = pskl.model.Frame.fromPixelGrid([ + [black, red], + [red, black], + [black, black], + [red, red] + ]); + var spritesheet = pskl.utils.FrameUtils.toImage(frame); + + var frames = pskl.utils.LayerUtils.createLayerFromSpritesheet(spritesheet, 4); + expect(frames.length).toBe(4); + + expect(frames[0].getPixel(0,0)).toBe(black); + expect(frames[0].getPixel(0,1)).toBe(red); + + expect(frames[1].getPixel(0,0)).toBe(red); + expect(frames[1].getPixel(0,1)).toBe(black); + + expect(frames[2].getPixel(0,0)).toBe(black); + expect(frames[2].getPixel(0,1)).toBe(black); + + expect(frames[3].getPixel(0,0)).toBe(red); + expect(frames[3].getPixel(0,1)).toBe(red); + + }); + + // it("starts at -1", function() { + // historyService = createMockHistoryService(); + // expect(historyService.currentIndex).toBe(-1); + // }); + + // it("is at 0 after init", function() { + // historyService = createMockHistoryService(); + // historyService.init(); + // expect(historyService.currentIndex).toBe(0); + // }); + + // it("stores a piskel snapshot after 5 SAVE", function () { + // // BEFORE + // var SNAPSHOT_PERIOD_BACKUP = pskl.service.HistoryService.SNAPSHOT_PERIOD; + // pskl.service.HistoryService.SNAPSHOT_PERIOD = 5; + + // historyService = createMockHistoryService(); + // historyService.init(); + + // sendSaveEvents(pskl.service.HistoryService.REPLAY).times(5); + + // expect(historyService.currentIndex).toBe(5); + + // expect(getLastState().piskel).toBe(SERIALIZED_PISKEL); + + // sendSaveEvents(pskl.service.HistoryService.REPLAY).times(4); + + // sendSaveEvents(pskl.service.HistoryService.REPLAY_NO_SNAPSHOT).once(); + // expect(getLastState().piskel).toBeUndefined(); + + // sendSaveEvents(pskl.service.HistoryService.REPLAY_NO_SNAPSHOT).once(); + // expect(getLastState().piskel).toBeUndefined(); + + // sendSaveEvents(pskl.service.HistoryService.REPLAY).once(); + // expect(getLastState().piskel).toBe(SERIALIZED_PISKEL); + + // // AFTER + // pskl.service.HistoryService.SNAPSHOT_PERIOD = SNAPSHOT_PERIOD_BACKUP; + + // }) +}); \ No newline at end of file diff --git a/test/js/utils/UuidTest.js b/test/js/utils/UuidTest.js new file mode 100644 index 00000000..ce8204a5 --- /dev/null +++ b/test/js/utils/UuidTest.js @@ -0,0 +1,27 @@ +describe("UUID Generator", function() { + + beforeEach(function() {}); + afterEach(function() {}); + + it("returns valid uuids", function() { + // when + + // then + var uuid1 = pskl.utils.Uuid.generate(); + var uuid2 = pskl.utils.Uuid.generate(); + + // verify + expect(typeof uuid1).toBe("string"); + expect(uuid1.length).toBe(36); + var splits = uuid1.split('-'); + expect(splits.length).toBe(5); + + expect(splits[0].length).toBe(8); + expect(splits[1].length).toBe(4); + expect(splits[2].length).toBe(4); + expect(splits[3].length).toBe(4); + expect(splits[4].length).toBe(12); + + expect(uuid1).not.toBe(uuid2); + }); +}); \ No newline at end of file