From c14a8ff08d314419f86df101ea6c44c789d23c14 Mon Sep 17 00:00:00 2001 From: Julian Descottes Date: Wed, 22 Aug 2012 15:53:24 -0700 Subject: [PATCH 01/18] Initial commit --- README.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..1d352baf --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +piskel +====== + +Animated sprite editor \ No newline at end of file From 1b44e3fd48b1ffe889cd7e17f077b99a7b4d3297 Mon Sep 17 00:00:00 2001 From: Julian Descottes Date: Thu, 23 Aug 2012 00:57:35 +0200 Subject: [PATCH 02/18] Initial commit from dropbox Basic features : Can create frames Can edit frames (black and white only) Can select the frame to edit Animated preview And that's it. Really tired and code is extremely ugly, just needed to do something that _works_ --- css/piskel.css | 36 +++++++++++++ css/style.css | 83 +++++++++++++++++++++++++++++ index.html | 36 +++++++++++++ js/piskel.js | 142 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 297 insertions(+) create mode 100644 css/piskel.css create mode 100644 css/style.css create mode 100644 index.html create mode 100644 js/piskel.js diff --git a/css/piskel.css b/css/piskel.css new file mode 100644 index 00000000..c384327b --- /dev/null +++ b/css/piskel.css @@ -0,0 +1,36 @@ +html, body { + height : 100%; +} + +.debug { + border : 1px Solid black; +} + +.left-nav { + position:absolute; + top : 0; + bottom : 0; + width : 200px; + background : #000; +} + +.main-panel { + position:absolute; + top : 0; + bottom : 0; + left : 200px; + right : 0; + background : #ccc; +} + +.preview-container { + position : absolute; + top : 30px; + right : 0; + height : 200px; + width : 200px; + background : white; + border : 0px Solid black; + border-radius:5px 0px 0px 5px; + box-shadow : 0px 0px 2px rgba(0,0,0,0.2); +} \ No newline at end of file diff --git a/css/style.css b/css/style.css new file mode 100644 index 00000000..33cf7319 --- /dev/null +++ b/css/style.css @@ -0,0 +1,83 @@ +html, body { + height : 100%; + margin : 0; + cursor : default; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +ul, li { + margin : 0; + padding : 0; +} + +.debug { + border : 1px Solid black; +} + +.left-nav { + position:absolute; + top : 0; + bottom : 0; + width : 200px; + background : #000; + padding : 10px; +} + +.main-panel { + position:absolute; + top : 0; + bottom : 0; + left : 200px; + right : 0; + background : #ccc; +} + +.preview-container { + position : absolute; + top : 30px; + right : 0; + height : 256px; + width : 256px; + background : white; + border : 0px Solid black; + border-radius:5px 0px 0px 5px; + box-shadow : 0px 0px 2px rgba(0,0,0,0.2); +} + +.preview-container canvas{ + border : 0px Solid transparent; + border-radius:5px 0px 0px 5px; +} + +#cursorInfo { + position : fixed; + cursor : default; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.action-button { + background-color : white; + width : 150px; + display : inline-block; +} + +#preview-list li{ + margin : 10px 0; + width : 128px; + height : 128px; +} + +#preview-list li.selected{ + margin : 8px -2px; + border : 2px Solid red; +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 00000000..f8f0d7c9 --- /dev/null +++ b/index.html @@ -0,0 +1,36 @@ + + + + + + + Piskel + + + + + + + + + +
+ + Add a Frame + +
    +
+
+
+
+
+ +
+
+
+ + + diff --git a/js/piskel.js b/js/piskel.js new file mode 100644 index 00000000..62c719e5 --- /dev/null +++ b/js/piskel.js @@ -0,0 +1,142 @@ +(function ($) { + var frames = [], isClicked = false, brushSize = 10, index = -1, animIndex = 0, button = 0; + + + var piskel = { + init : function () { + this.addFrame(); + + setInterval(this.refreshAnimatedPreview, 500); + }, + + getCurrentCanvas : function () { + return frames[index]; + }, + + onCanvasMousemove : function (event) { + //this.updateCursorInfo(event); + if (isClicked) { + var coords = this.getRelativeCoordinates(event.clientX, event.clientY); + this.drawAt(coords.x, coords.y); + } + }, + + createPreviews : function () { + var container = $('preview-list'); + container.innerHTML = ""; + for (var i = 0 ; i < frames.length ; i++) { + var preview = document.createElement("li"); + if (index == i) { + preview.className = "selected"; + } + var canvasPreview = document.createElement("canvas"); + + canvasPreview.setAttribute('width', '128'); + canvasPreview.setAttribute('height', '128'); + + canvasPreview.setAttribute('onclick', 'piskel.setFrame('+i+')'); + + canvasPreview.getContext('2d').drawImage(frames[i], 0, 0, 320, 320, 0, 0 , 128, 128); + preview.appendChild(canvasPreview); + + + container.appendChild(preview); + + } + }, + + refreshAnimatedPreview : function () { + var context = $('animated-preview').getContext('2d'); + // erase canvas, verify proper way + context.fillStyle = "white"; + context.fillRect(0, 0, 256, 256); + + context.drawImage(frames[animIndex++], 0, 0, 320, 320, 0, 0 , 256, 256); + if (animIndex == frames.length) { + animIndex = 0; + } + }, + + setFrame : function (frameIndex) { + index = frameIndex; + $('canvas-container').innerHTML = ""; + $('canvas-container').appendChild(this.getCurrentCanvas()); + this.createPreviews(); + }, + + updateCursorInfo : function (event) { + var cursor = $('cursorInfo'); + cursor.style.top = event.clientY + 10 + "px"; + cursor.style.left = event.clientX + 10 + "px"; + + var coordinates = this.getRelativeCoordinates(event.clientX, event.clientY) + cursor.innerHTML = [ + "X : " + coordinates.x, + "Y : " + coordinates.y + ].join(", "); + }, + + onCanvasMousedown : function (event) { + isClicked = true; + button = event.button; + var coords = this.getRelativeCoordinates(event.clientX, event.clientY); + this.drawAt(coords.x, coords.y); + }, + + onCanvasMouseup : function (event) { + isClicked = false; + }, + + drawAt : function (x, y) { + if (x < 0 || y < 0 || x > 320 || y > 320) return; + var context = this.getCurrentCanvas().getContext('2d'); + if (button == 0) { + context.fillStyle = "black"; + } else { + context.fillStyle = "white"; + } + + context.fillRect(x - x%brushSize, y - y%brushSize, brushSize, brushSize); + this.createPreviews(); + }, + + onCanvasContextMenu : function (event) { + event.preventDefault(); + event.stopPropagation(); + event.cancelBubble = true; + return false; + }, + getRelativeCoordinates : function (x, y) { + var canvas = this.getCurrentCanvas(); + var canvasRect = canvas.getBoundingClientRect(); + return { + x : x - canvasRect.left, + y : y - canvasRect.top + } + }, + + addFrame : function () { + var canvas = document.createElement("canvas"); + canvas.setAttribute('width', '320'); + canvas.setAttribute('height', '320'); + canvas.setAttribute('onmousemove', 'piskel.onCanvasMousemove(arguments[0])'); + canvas.setAttribute('oncontextmenu', 'piskel.onCanvasContextMenu(arguments[0])'); + canvas.setAttribute('onclick', 'piskel.onCanvasClick(arguments[0])'); + var context = canvas.getContext('2d'); + + context.fillStyle = "white"; + context.fillRect(0, 0, 320, 320); + + if(frames[index]) { //is a valid canvas + context.drawImage(frames[index], 0, 0, 320, 320, 0, 0 , 320, 320); + } + + frames.push(canvas); + this.setFrame(frames.length - 1); + } + }; + + window.piskel = piskel; + piskel.init(); + +})(function(id){return document.getElementById(id)}); \ No newline at end of file From 8a9dbf5c77f23576ef1babff21b9d0d99be2b22d Mon Sep 17 00:00:00 2001 From: Julian Descottes Date: Thu, 23 Aug 2012 08:46:07 +0300 Subject: [PATCH 03/18] Update README.md --- README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1d352baf..c94f9122 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,18 @@ -piskel +Piskel ====== -Animated sprite editor \ No newline at end of file +The goal is to create an easy-to-use/in-the-cloud/web-based 2d animation editor. + +v0.0whatever (aka the thing I did last night) +------------------------------------ +You can : +* create small animations in black (left click) and white (right click) +* and actually animations are always in 32x32 zoomed 10 times +* you can not even save them ! +* add new frames for your animation +* do small ridiculous characters + +Looks like this : +![Alt text](https://dl.dropbox.com/u/17803671/screen_piskel.png "Optional title") + +On the left, the list of frames for the animation, in the middle the editable canvas, on the right the LIVE-ANIMATED-PREVIEW (rocket science stuff going on here). From d6da94b4a0d7f4a0564e19e845e1131a572a8aa7 Mon Sep 17 00:00:00 2001 From: Julian Descottes Date: Thu, 23 Aug 2012 08:51:42 +0300 Subject: [PATCH 04/18] Update README.md --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c94f9122..2da0900c 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,17 @@ The goal is to create an easy-to-use/in-the-cloud/web-based 2d animation editor. v0.0whatever (aka the thing I did last night) ------------------------------------ You can : -* create small animations in black (left click) and white (right click) -* and actually animations are always in 32x32 zoomed 10 times -* you can not even save them ! +* create small animations in __black__ (left click) and __white__ (right click) +* and actually animations are always in __32x32__ zoomed 10 times +* you can __not even save them__ ! * add new frames for your animation -* do small ridiculous characters +* do small __ridiculous__ characters Looks like this : ![Alt text](https://dl.dropbox.com/u/17803671/screen_piskel.png "Optional title") -On the left, the list of frames for the animation, in the middle the editable canvas, on the right the LIVE-ANIMATED-PREVIEW (rocket science stuff going on here). +**On the left**, the list of frames for the animation. +**In the 'middle'**, the editable canvas. +**On the right**, the LIVE-ANIMATED-PREVIEW (rocket science stuff going on here). + +Try it at : http://juliandescottes.github.com/piskel/ \ No newline at end of file From 1f95b8611bc29356c33b31001aacd4e4e62878a2 Mon Sep 17 00:00:00 2001 From: Vince Date: Sat, 25 Aug 2012 02:25:44 +0200 Subject: [PATCH 05/18] Delete/Duplicate actions for tile preview + FPS tuner for animated preview --- css/style.css | 49 +++++++++++++++++---- index.html | 7 ++- js/piskel.js | 119 +++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 144 insertions(+), 31 deletions(-) diff --git a/css/style.css b/css/style.css index 33cf7319..7008f476 100644 --- a/css/style.css +++ b/css/style.css @@ -24,6 +24,7 @@ ul, li { top : 0; bottom : 0; width : 200px; + overflow-y: scroll; background : #000; padding : 10px; } @@ -32,7 +33,7 @@ ul, li { position:absolute; top : 0; bottom : 0; - left : 200px; + left : 220px; right : 0; background : #ccc; } @@ -49,7 +50,7 @@ ul, li { box-shadow : 0px 0px 2px rgba(0,0,0,0.2); } -.preview-container canvas{ +.preview-container canvas { border : 0px Solid transparent; border-radius:5px 0px 0px 5px; } @@ -71,13 +72,43 @@ ul, li { display : inline-block; } -#preview-list li{ - margin : 10px 0; - width : 128px; - height : 128px; +#preview-list { + list-style-type: none; } -#preview-list li.selected{ - margin : 8px -2px; - border : 2px Solid red; +#preview-list .preview-tile { + padding : 10px; + overflow: hidden; +} + +.preview-tile .tile-view { + float: left; +} + +.preview-tile .tile-action { + display: none; + float: right; +} + +.preview-tile:hover .tile-action { + display: block; +} + +.preview-tile:hover { + background-color: lightgray; +} + +#preview-list .preview-tile.selected { + background-color: gray; +} + +::-webkit-scrollbar { + -webkit-appearance: none; + width: 7px; +} + +::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: rgba(180,180,180,.7); + -webkit-box-shadow: 0 0 1px rgba(255,255,255,.5); } \ No newline at end of file diff --git a/index.html b/index.html index f8f0d7c9..bd5efe8b 100644 --- a/index.html +++ b/index.html @@ -17,10 +17,10 @@ onmousedown="piskel.onCanvasMousedown(arguments[0])" onmouseup="piskel.onCanvasMouseup(arguments[0])">
- Add a Frame - +
@@ -28,6 +28,9 @@
+
+ +
diff --git a/js/piskel.js b/js/piskel.js index 62c719e5..f35d0629 100644 --- a/js/piskel.js +++ b/js/piskel.js @@ -5,8 +5,40 @@ var piskel = { init : function () { this.addFrame(); + this.initPreview(); + }, - setInterval(this.refreshAnimatedPreview, 500); + initPreview : function() { + var scope = this; + + var animFPSTuner = document.getElementById("preview-fps"); + var animPreviewFPS = parseInt(animFPSTuner.value, 10); + var startPreviewRefresh = function() { + return setInterval(scope.refreshAnimatedPreview, 1000/animPreviewFPS); + }; + var refreshUpdater = startPreviewRefresh(); + + animFPSTuner.addEventListener('keyup', function(evt) { + window.clearInterval(refreshUpdater); + animPreviewFPS = parseInt(animFPSTuner.value, 10); + if(isNaN(animPreviewFPS)) { + animPreviewFPS = 1; + } + if(evt.keyCode == 38) { + animPreviewFPS++; + } + else if (evt.keyCode == 40) { + animPreviewFPS--; + } + if(animPreviewFPS < 1) { + animPreviewFPS = 1; + } + if(animPreviewFPS > 100) { + animPreviewFPS = 100; + } + animFPSTuner.value = animPreviewFPS; + refreshUpdater = startPreviewRefresh(); + }); }, getCurrentCanvas : function () { @@ -22,29 +54,50 @@ }, createPreviews : function () { - var container = $('preview-list'); + var container = $('preview-list'), previewTile; container.innerHTML = ""; for (var i = 0 ; i < frames.length ; i++) { - var preview = document.createElement("li"); - if (index == i) { - preview.className = "selected"; - } - var canvasPreview = document.createElement("canvas"); - - canvasPreview.setAttribute('width', '128'); - canvasPreview.setAttribute('height', '128'); - - canvasPreview.setAttribute('onclick', 'piskel.setFrame('+i+')'); - - canvasPreview.getContext('2d').drawImage(frames[i], 0, 0, 320, 320, 0, 0 , 128, 128); - preview.appendChild(canvasPreview); - - - container.appendChild(preview); - + previewTile = this.createPreviewTile(i); + container.appendChild(previewTile); } }, + createPreviewTile: function(tileNumber) { + var preview = document.createElement("li"); + var classname = "preview-tile"; + + if (index == tileNumber) { + classname += " selected"; + } + preview.className = classname; + + var canvasPreview = document.createElement("canvas"); + canvasPreview.className = "tile-view" + + canvasPreview.setAttribute('width', '128'); + canvasPreview.setAttribute('height', '128'); + canvasPreview.setAttribute('onclick', 'piskel.setFrame('+ tileNumber +')'); + + var canvasPreviewDuplicateAction = document.createElement("button"); + canvasPreviewDuplicateAction.className = "tile-action" + canvasPreviewDuplicateAction.innerHTML = "dup" + canvasPreviewDuplicateAction.setAttribute('onclick', 'piskel.duplicateFrame('+ tileNumber +')'); + + canvasPreview.getContext('2d').drawImage(frames[tileNumber], 0, 0, 320, 320, 0, 0 , 128, 128); + preview.appendChild(canvasPreview); + preview.appendChild(canvasPreviewDuplicateAction); + + if(frames.length > 1) { + var canvasPreviewDeleteAction = document.createElement("button"); + canvasPreviewDeleteAction.className = "tile-action" + canvasPreviewDeleteAction.innerHTML = "del" + canvasPreviewDeleteAction.setAttribute('onclick', 'piskel.removeFrame('+ tileNumber +')'); + preview.appendChild(canvasPreviewDeleteAction); + } + + return preview; + }, + refreshAnimatedPreview : function () { var context = $('animated-preview').getContext('2d'); // erase canvas, verify proper way @@ -64,6 +117,30 @@ this.createPreviews(); }, + removeFrame: function(frameIndex) { + index = frameIndex - 1 < 0 ? 0 : frameIndex - 1; + animIndex = 0; + frames.splice(frameIndex, 1); + $('canvas-container').innerHTML = ""; + $('canvas-container').appendChild(this.getCurrentCanvas()); + this.createPreviews(); + }, + + duplicateFrame: function(frameIndex) { + index = frameIndex + 1; + animIndex = 0; + var duplicateCanvas = frames[frameIndex].cloneNode(true); + // Copy canvas content: + var context = duplicateCanvas.getContext('2d'); + context.drawImage(frames[frameIndex], 0, 0); + + // Insert cloned node into frame collection: + frames.splice(frameIndex + 1, 0, duplicateCanvas); + $('canvas-container').innerHTML = ""; + $('canvas-container').appendChild(this.getCurrentCanvas()); + this.createPreviews(); + }, + updateCursorInfo : function (event) { var cursor = $('cursorInfo'); cursor.style.top = event.clientY + 10 + "px"; @@ -121,7 +198,7 @@ canvas.setAttribute('height', '320'); canvas.setAttribute('onmousemove', 'piskel.onCanvasMousemove(arguments[0])'); canvas.setAttribute('oncontextmenu', 'piskel.onCanvasContextMenu(arguments[0])'); - canvas.setAttribute('onclick', 'piskel.onCanvasClick(arguments[0])'); + //canvas.setAttribute('onclick', 'piskel.onCanvasClick(arguments[0])'); var context = canvas.getContext('2d'); context.fillStyle = "white"; @@ -131,6 +208,8 @@ context.drawImage(frames[index], 0, 0, 320, 320, 0, 0 , 320, 320); } + // TODO: We should probably store some metadata or enhance a domain object instead + // of the rendered view ? It will allow to decouple view and model and clean a bunch of code above. frames.push(canvas); this.setFrame(frames.length - 1); } From 651563f793796fd8128b7875c04f504e7a6c9f72 Mon Sep 17 00:00:00 2001 From: Vince Date: Mon, 27 Aug 2012 02:05:13 +0200 Subject: [PATCH 06/18] FrameSheet model object migration Migration to a Domain object (currently a FrameSheetModel, feel free to change its name). The model is being used by the slideshow (drawing each tiles), animation preview (drawing animation) and drawing (update model and redraw current tile). Now the rendering information are not stored in a canvas element that you paste from canvas to canvas but centralize in this model. The frame is described as an array of array: that will allow different rendering using the dpi constants and more flexibility (e.g. drawing a grid, serializing the data). Some minor modifications: - cleaning markup - adding background image to highlight transparent area --- css/style.css | 30 +++- img/transparent_background.png | Bin 0 -> 48266 bytes index.html | 16 +- js/frameSheetModel.js | 76 ++++++++ js/piskel.js | 313 ++++++++++++++++++++++----------- 5 files changed, 326 insertions(+), 109 deletions(-) create mode 100644 img/transparent_background.png create mode 100644 js/frameSheetModel.js diff --git a/css/style.css b/css/style.css index 7008f476..9136c5f0 100644 --- a/css/style.css +++ b/css/style.css @@ -76,13 +76,19 @@ ul, li { list-style-type: none; } -#preview-list .preview-tile { +.preview-tile { padding : 10px; overflow: hidden; + background-color: gray; +} + +.preview-tile .canvas-container { + float: left; } .preview-tile .tile-view { float: left; + border: blue 1px solid; } .preview-tile .tile-action { @@ -99,9 +105,29 @@ ul, li { } #preview-list .preview-tile.selected { - background-color: gray; + background-color: lightyellow; } +.canvas-container { + position: relative; + display: block; +} + +.canvas-container .canvas-background { + background: url(../img/transparent_background.png) repeat; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.canvas { + position: relative; + z-index: 1; +} + +/* Force apparition of scrollbars on leopard */ ::-webkit-scrollbar { -webkit-appearance: none; width: 7px; diff --git a/img/transparent_background.png b/img/transparent_background.png new file mode 100644 index 0000000000000000000000000000000000000000..4a1f7a8e591c8d0cffd8b628b21c0a52a230ce58 GIT binary patch literal 48266 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�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 xWt1^R_}r8?VSHyqQi8R07q{Hg+&p38#N4G5 - +
-
-
+
+
+
- +
+
+
+ diff --git a/js/frameSheetModel.js b/js/frameSheetModel.js new file mode 100644 index 00000000..5ea52a0d --- /dev/null +++ b/js/frameSheetModel.js @@ -0,0 +1,76 @@ + +var FrameSheetModel = (function() { + + var inst; + var frames = []; + var width; + var height; + + var createEmptyFrame_ = function() { + var emptyFrame = new Array(width); + for (var columnIndex=0; columnIndex < width; columnIndex++) { + emptyFrame[columnIndex] = new Array(height); + } + return emptyFrame; + }; + + return { + validate: function() { + return true; // I'm always right dude + }, + + // Could be use to pass around model using long GET param (good enough for simple models) and + // do some temporary locastorage + serialize: function() { + throw "FrameSheet.serialize Not implemented" + }, + + addEmptyFrame: function() { + frames.push(createEmptyFrame_()); + }, + + getFrameCount: function() { + return frames.length; + }, + + getFrameByIndex: function(index) { + if (isNaN(index)) { + throw "Bad argument value for getFrameByIndex method: <" + index + ">" + } else if (index < 0 || index > frames.length) { + throw "Out of bound index for frameSheet object." + } + + return frames[index]; + }, + + removeFrameByIndex: function(index) { + if(index < 0 || index > inst.getFrameCount()) { + throw "Bad index value for removeFrameByIndex."; + } + frames.splice(index, 1); + }, + + duplicateFrameByIndex: function(frameToDuplicateIndex) { + var frame = inst.getFrameByIndex(frameToDuplicateIndex); + var clonedFrame = []; + for(var i=0, l=frame.length; i 1) { + if(tileNumber > 0 || frameSheet.getFrameCount() > 1) { var canvasPreviewDeleteAction = document.createElement("button"); canvasPreviewDeleteAction.className = "tile-action" canvasPreviewDeleteAction.innerHTML = "del" - canvasPreviewDeleteAction.setAttribute('onclick', 'piskel.removeFrame('+ tileNumber +')'); - preview.appendChild(canvasPreviewDeleteAction); + canvasPreviewDeleteAction.addEventListener('click', function(evt) { + frameSheet.removeFrameByIndex(tileNumber); + animIndex = 0; + piskel.createPreviews(); + }); + previewTileRoot.appendChild(canvasPreviewDeleteAction); } - return preview; + return previewTileRoot; }, refreshAnimatedPreview : function () { - var context = $('animated-preview').getContext('2d'); - // erase canvas, verify proper way - context.fillStyle = "white"; - context.fillRect(0, 0, 256, 256); - - context.drawImage(frames[animIndex++], 0, 0, 320, 320, 0, 0 , 256, 256); - if (animIndex == frames.length) { + piskel.drawFrameToCanvas(frameSheet.getFrameByIndex(animIndex), previewCanvas, previewAnimationCanvasDpi); + animIndex++; + if (animIndex == frameSheet.getFrameCount()) { animIndex = 0; } }, - setFrame : function (frameIndex) { - index = frameIndex; - $('canvas-container').innerHTML = ""; - $('canvas-container').appendChild(this.getCurrentCanvas()); - this.createPreviews(); - }, + removeFrame: function(frameIndex) { + frameSheet.removeFrameByIndex(frameIndex); - removeFrame: function(frameIndex) { - index = frameIndex - 1 < 0 ? 0 : frameIndex - 1; - animIndex = 0; - frames.splice(frameIndex, 1); - $('canvas-container').innerHTML = ""; - $('canvas-container').appendChild(this.getCurrentCanvas()); - this.createPreviews(); + this.setActiveFrameAndRedraw(frameIndex - 1); }, duplicateFrame: function(frameIndex) { - index = frameIndex + 1; - animIndex = 0; - var duplicateCanvas = frames[frameIndex].cloneNode(true); - // Copy canvas content: - var context = duplicateCanvas.getContext('2d'); - context.drawImage(frames[frameIndex], 0, 0); + frameSheet.duplicateFrameByIndex(frameIndex); - // Insert cloned node into frame collection: - frames.splice(frameIndex + 1, 0, duplicateCanvas); - $('canvas-container').innerHTML = ""; - $('canvas-container').appendChild(this.getCurrentCanvas()); - this.createPreviews(); + this.setActiveFrameAndRedraw(frameIndex + 1); }, updateCursorInfo : function (event) { @@ -155,26 +243,73 @@ onCanvasMousedown : function (event) { isClicked = true; - button = event.button; var coords = this.getRelativeCoordinates(event.clientX, event.clientY); - this.drawAt(coords.x, coords.y); + if(event.button == 0) { + this.drawAt(coords.x, coords.y, penColor); + } else { + // Right click used to delete. + isRightClicked = true; + this.drawAt(coords.x, coords.y, TRANSPARENT_COLOR); + } + }, + + onCanvasMousemove : function (event) { + //this.updateCursorInfo(event); + if (isClicked) { + var coords = this.getRelativeCoordinates(event.clientX, event.clientY); + if(isRightClicked) { + this.drawAt(coords.x, coords.y, TRANSPARENT_COLOR); + } else { + this.drawAt(coords.x, coords.y, penColor); + } + } }, onCanvasMouseup : function (event) { + if(isClicked || isRightClicked) { + // A mouse button was clicked on the drawing canvas before this mouseup event, + // the user was probably drawing on the canvas. + // Note: The mousemove movement (and the mouseup) may end up outside + // of the drawing canvas. + this.createPreviews(); + } isClicked = false; + isRightClicked = false; }, - drawAt : function (x, y) { - if (x < 0 || y < 0 || x > 320 || y > 320) return; - var context = this.getCurrentCanvas().getContext('2d'); - if (button == 0) { - context.fillStyle = "black"; - } else { - context.fillStyle = "white"; - } + drawAt : function (x, y, color) { + var pixelWidthIndex = (x - x%drawingCanvasDpi) / 10; + var pixelHeightIndex = (y - y%drawingCanvasDpi) / 10; + + // Update model: + var currentFrame = frameSheet.getFrameByIndex(this.getActiveFrameIndex()); + + // TODO: make a better accessor for pixel state update: + // TODO: Make pen color dynamic: + currentFrame[pixelWidthIndex][pixelHeightIndex] = color; + + // Update view: + // TODO: Create a per pixel update function for perf ? + this.drawFrameToCanvas(currentFrame, drawingAreaCanvas, drawingCanvasDpi); + }, - context.fillRect(x - x%brushSize, y - y%brushSize, brushSize, brushSize); - this.createPreviews(); + // TODO: move that to a FrameRenderer (/w cache) ? + drawFrameToCanvas: function(frame, canvasElement, dpi) { + var pixelColor, context = canvasElement.getContext('2d'); + for(var col = 0, num_col = frame.length; col < num_col; col++) { + for(var row = 0, num_row = frame[col].length; row < num_row; row++) { + pixelColor = frame[col][row]; + + if(pixelColor == undefined || pixelColor == TRANSPARENT_COLOR) { + context.clearRect(col * dpi, row * dpi, dpi, dpi); + } else { + context.fillStyle = pixelColor; + context.fillRect(col * dpi, row * dpi, dpi, dpi); + } + + + } + } }, onCanvasContextMenu : function (event) { @@ -183,35 +318,13 @@ event.cancelBubble = true; return false; }, + getRelativeCoordinates : function (x, y) { - var canvas = this.getCurrentCanvas(); - var canvasRect = canvas.getBoundingClientRect(); + var canvasRect = drawingAreaCanvas.getBoundingClientRect(); return { x : x - canvasRect.left, y : y - canvasRect.top } - }, - - addFrame : function () { - var canvas = document.createElement("canvas"); - canvas.setAttribute('width', '320'); - canvas.setAttribute('height', '320'); - canvas.setAttribute('onmousemove', 'piskel.onCanvasMousemove(arguments[0])'); - canvas.setAttribute('oncontextmenu', 'piskel.onCanvasContextMenu(arguments[0])'); - //canvas.setAttribute('onclick', 'piskel.onCanvasClick(arguments[0])'); - var context = canvas.getContext('2d'); - - context.fillStyle = "white"; - context.fillRect(0, 0, 320, 320); - - if(frames[index]) { //is a valid canvas - context.drawImage(frames[index], 0, 0, 320, 320, 0, 0 , 320, 320); - } - - // TODO: We should probably store some metadata or enhance a domain object instead - // of the rendered view ? It will allow to decouple view and model and clean a bunch of code above. - frames.push(canvas); - this.setFrame(frames.length - 1); } }; From bfaea76eb62ffd0a69ea0244aea72b4424963512 Mon Sep 17 00:00:00 2001 From: Julian Descottes Date: Tue, 28 Aug 2012 02:18:56 +0300 Subject: [PATCH 07/18] Update README.md --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2da0900c..7a87fff7 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,18 @@ Piskel The goal is to create an easy-to-use/in-the-cloud/web-based 2d animation editor. +Try it at : http://juliandescottes.github.com/piskel/ + +v0.0something (with 2 pull requests from grosbouddha) +------------------------------------ +**28 Aug 2012** : Thanks to grosbouddha, new features added to Piskel : +* modify preview speed ! +* remove (shitty) frames +* transparent background + v0.0whatever (aka the thing I did last night) ------------------------------------ -You can : +**24 Aug 2012** : You can : * create small animations in __black__ (left click) and __white__ (right click) * and actually animations are always in __32x32__ zoomed 10 times * you can __not even save them__ ! @@ -19,4 +28,3 @@ Looks like this : **In the 'middle'**, the editable canvas. **On the right**, the LIVE-ANIMATED-PREVIEW (rocket science stuff going on here). -Try it at : http://juliandescottes.github.com/piskel/ \ No newline at end of file From 6616ec2870143c32a107cce5bb72fc64f0565747 Mon Sep 17 00:00:00 2001 From: Vince Date: Tue, 28 Aug 2012 22:24:58 +0200 Subject: [PATCH 08/18] Adding basic color picker --- index.html | 2 + js/lib/jsColor_1_4_0/arrow.gif | Bin 0 -> 66 bytes js/lib/jsColor_1_4_0/cross.gif | Bin 0 -> 83 bytes js/lib/jsColor_1_4_0/demo.html | 12 + js/lib/jsColor_1_4_0/hs.png | Bin 0 -> 2684 bytes js/lib/jsColor_1_4_0/hv.png | Bin 0 -> 2865 bytes js/lib/jsColor_1_4_0/jscolor.js | 953 ++++++++++++++++++++++++++++++++ js/piskel.js | 9 + 8 files changed, 976 insertions(+) create mode 100644 js/lib/jsColor_1_4_0/arrow.gif create mode 100644 js/lib/jsColor_1_4_0/cross.gif create mode 100644 js/lib/jsColor_1_4_0/demo.html create mode 100644 js/lib/jsColor_1_4_0/hs.png create mode 100644 js/lib/jsColor_1_4_0/hv.png create mode 100644 js/lib/jsColor_1_4_0/jscolor.js diff --git a/index.html b/index.html index 6f9ae98c..39e3a207 100644 --- a/index.html +++ b/index.html @@ -25,6 +25,7 @@
+
@@ -35,6 +36,7 @@
+ diff --git a/js/lib/jsColor_1_4_0/arrow.gif b/js/lib/jsColor_1_4_0/arrow.gif new file mode 100644 index 0000000000000000000000000000000000000000..246478a864f812d93d4cd0e0d0ad1c8e2d09c825 GIT binary patch literal 66 zcmZ?wbhEHbWM|-JSjfZx1poj4Utv=LM2bII7`Ygj7<53QAbAERVV@R>f}L*~a`L;Z PKCZl?^}@uHk--`OML81Y literal 0 HcmV?d00001 diff --git a/js/lib/jsColor_1_4_0/cross.gif b/js/lib/jsColor_1_4_0/cross.gif new file mode 100644 index 0000000000000000000000000000000000000000..0ee9c7ac517bee40b78aea03afa809631f0b69fa GIT binary patch literal 83 zcmZ?wbhEHb + + jscolor demo + + + + + + Click here: + + + diff --git a/js/lib/jsColor_1_4_0/hs.png b/js/lib/jsColor_1_4_0/hs.png new file mode 100644 index 0000000000000000000000000000000000000000..3d94486cedaf7111d010fdaf2cefad45fd1f878b GIT binary patch literal 2684 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 YzFqsNg>|5V=vOIWZ|#VBZRvIQe;Bk#LI3~& literal 0 HcmV?d00001 diff --git a/js/lib/jsColor_1_4_0/hv.png b/js/lib/jsColor_1_4_0/hv.png new file mode 100644 index 0000000000000000000000000000000000000000..1c5e01f8bcecc4cf835e8eeeaa43ef2c06789022 GIT binary patch literal 2865 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 E0LKozl>h($ literal 0 HcmV?d00001 diff --git a/js/lib/jsColor_1_4_0/jscolor.js b/js/lib/jsColor_1_4_0/jscolor.js new file mode 100644 index 00000000..47cd4b1e --- /dev/null +++ b/js/lib/jsColor_1_4_0/jscolor.js @@ -0,0 +1,953 @@ +/** + * jscolor, JavaScript Color Picker + * + * @version 1.4.0 + * @license GNU Lesser General Public License, http://www.gnu.org/copyleft/lesser.html + * @author Jan Odvarko, http://odvarko.cz + * @created 2008-06-15 + * @updated 2012-07-06 + * @link http://jscolor.com + */ + + +var jscolor = { + + + dir : '', // location of jscolor directory (leave empty to autodetect) + bindClass : 'color', // class name + binding : true, // automatic binding via + preloading : true, // use image preloading? + + + install : function() { + jscolor.addEvent(window, 'load', jscolor.init); + }, + + + init : function() { + if(jscolor.binding) { + jscolor.bind(); + } + if(jscolor.preloading) { + jscolor.preload(); + } + }, + + + getDir : function() { + if(!jscolor.dir) { + var detected = jscolor.detectDir(); + jscolor.dir = detected!==false ? detected : 'jscolor/'; + } + return jscolor.dir; + }, + + + detectDir : function() { + var base = location.href; + + var e = document.getElementsByTagName('base'); + for(var i=0; i vs[a] ? + (-vp[a]+tp[a]+ts[a]/2 > vs[a]/2 && tp[a]+ts[a]-ps[a] >= 0 ? tp[a]+ts[a]-ps[a] : tp[a]) : + tp[a], + -vp[b]+tp[b]+ts[b]+ps[b]-l+l*c > vs[b] ? + (-vp[b]+tp[b]+ts[b]/2 > vs[b]/2 && tp[b]+ts[b]-l-l*c >= 0 ? tp[b]+ts[b]-l-l*c : tp[b]+ts[b]-l+l*c) : + (tp[b]+ts[b]-l+l*c >= 0 ? tp[b]+ts[b]-l+l*c : tp[b]+ts[b]-l-l*c) + ]; + } + drawPicker(pp[a], pp[b]); + } + }; + + + this.importColor = function() { + if(!valueElement) { + this.exportColor(); + } else { + if(!this.adjust) { + if(!this.fromString(valueElement.value, leaveValue)) { + styleElement.style.backgroundImage = styleElement.jscStyle.backgroundImage; + styleElement.style.backgroundColor = styleElement.jscStyle.backgroundColor; + styleElement.style.color = styleElement.jscStyle.color; + this.exportColor(leaveValue | leaveStyle); + } + } else if(!this.required && /^\s*$/.test(valueElement.value)) { + valueElement.value = ''; + styleElement.style.backgroundImage = styleElement.jscStyle.backgroundImage; + styleElement.style.backgroundColor = styleElement.jscStyle.backgroundColor; + styleElement.style.color = styleElement.jscStyle.color; + this.exportColor(leaveValue | leaveStyle); + + } else if(this.fromString(valueElement.value)) { + // OK + } else { + this.exportColor(); + } + } + }; + + + this.exportColor = function(flags) { + if(!(flags & leaveValue) && valueElement) { + var value = this.toString(); + if(this.caps) { value = value.toUpperCase(); } + if(this.hash) { value = '#'+value; } + valueElement.value = value; + } + if(!(flags & leaveStyle) && styleElement) { + styleElement.style.backgroundImage = "none"; + styleElement.style.backgroundColor = + '#'+this.toString(); + styleElement.style.color = + 0.213 * this.rgb[0] + + 0.715 * this.rgb[1] + + 0.072 * this.rgb[2] + < 0.5 ? '#FFF' : '#000'; + } + if(!(flags & leavePad) && isPickerOwner()) { + redrawPad(); + } + if(!(flags & leaveSld) && isPickerOwner()) { + redrawSld(); + } + }; + + + this.fromHSV = function(h, s, v, flags) { // null = don't change + if(h !== null) { h = Math.max(0.0, this.minH, Math.min(6.0, this.maxH, h)); } + if(s !== null) { s = Math.max(0.0, this.minS, Math.min(1.0, this.maxS, s)); } + if(v !== null) { v = Math.max(0.0, this.minV, Math.min(1.0, this.maxV, v)); } + + this.rgb = HSV_RGB( + h===null ? this.hsv[0] : (this.hsv[0]=h), + s===null ? this.hsv[1] : (this.hsv[1]=s), + v===null ? this.hsv[2] : (this.hsv[2]=v) + ); + + this.exportColor(flags); + }; + + + this.fromRGB = function(r, g, b, flags) { // null = don't change + if(r !== null) { r = Math.max(0.0, Math.min(1.0, r)); } + if(g !== null) { g = Math.max(0.0, Math.min(1.0, g)); } + if(b !== null) { b = Math.max(0.0, Math.min(1.0, b)); } + + var hsv = RGB_HSV( + r===null ? this.rgb[0] : r, + g===null ? this.rgb[1] : g, + b===null ? this.rgb[2] : b + ); + if(hsv[0] !== null) { + this.hsv[0] = Math.max(0.0, this.minH, Math.min(6.0, this.maxH, hsv[0])); + } + if(hsv[2] !== 0) { + this.hsv[1] = hsv[1]===null ? null : Math.max(0.0, this.minS, Math.min(1.0, this.maxS, hsv[1])); + } + this.hsv[2] = hsv[2]===null ? null : Math.max(0.0, this.minV, Math.min(1.0, this.maxV, hsv[2])); + + // update RGB according to final HSV, as some values might be trimmed + var rgb = HSV_RGB(this.hsv[0], this.hsv[1], this.hsv[2]); + this.rgb[0] = rgb[0]; + this.rgb[1] = rgb[1]; + this.rgb[2] = rgb[2]; + + this.exportColor(flags); + }; + + + this.fromString = function(hex, flags) { + var m = hex.match(/^\W*([0-9A-F]{3}([0-9A-F]{3})?)\W*$/i); + if(!m) { + return false; + } else { + if(m[1].length === 6) { // 6-char notation + this.fromRGB( + parseInt(m[1].substr(0,2),16) / 255, + parseInt(m[1].substr(2,2),16) / 255, + parseInt(m[1].substr(4,2),16) / 255, + flags + ); + } else { // 3-char notation + this.fromRGB( + parseInt(m[1].charAt(0)+m[1].charAt(0),16) / 255, + parseInt(m[1].charAt(1)+m[1].charAt(1),16) / 255, + parseInt(m[1].charAt(2)+m[1].charAt(2),16) / 255, + flags + ); + } + return true; + } + }; + + + this.toString = function() { + return ( + (0x100 | Math.round(255*this.rgb[0])).toString(16).substr(1) + + (0x100 | Math.round(255*this.rgb[1])).toString(16).substr(1) + + (0x100 | Math.round(255*this.rgb[2])).toString(16).substr(1) + ); + }; + + + function RGB_HSV(r, g, b) { + var n = Math.min(Math.min(r,g),b); + var v = Math.max(Math.max(r,g),b); + var m = v - n; + if(m === 0) { return [ null, 0, v ]; } + var h = r===n ? 3+(b-g)/m : (g===n ? 5+(r-b)/m : 1+(g-r)/m); + return [ h===6?0:h, m/v, v ]; + } + + + function HSV_RGB(h, s, v) { + if(h === null) { return [ v, v, v ]; } + var i = Math.floor(h); + var f = i%2 ? h-i : 1-(h-i); + var m = v * (1 - s); + var n = v * (1 - s*f); + switch(i) { + case 6: + case 0: return [v,n,m]; + case 1: return [n,v,m]; + case 2: return [m,v,n]; + case 3: return [m,n,v]; + case 4: return [n,m,v]; + case 5: return [v,m,n]; + } + } + + + function removePicker() { + delete jscolor.picker.owner; + document.getElementsByTagName('body')[0].removeChild(jscolor.picker.boxB); + } + + + function drawPicker(x, y) { + if(!jscolor.picker) { + jscolor.picker = { + box : document.createElement('div'), + boxB : document.createElement('div'), + pad : document.createElement('div'), + padB : document.createElement('div'), + padM : document.createElement('div'), + sld : document.createElement('div'), + sldB : document.createElement('div'), + sldM : document.createElement('div'), + btn : document.createElement('div'), + btnS : document.createElement('span'), + btnT : document.createTextNode(THIS.pickerCloseText) + }; + for(var i=0,segSize=4; i Date: Tue, 28 Aug 2012 23:30:55 +0200 Subject: [PATCH 09/18] Code cleaning: removeFrame & canvas DPI issue Making the drawing canvas really scalable Cleaning the removeFrame code --- js/piskel.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/js/piskel.js b/js/piskel.js index c74870d1..b033649f 100644 --- a/js/piskel.js +++ b/js/piskel.js @@ -13,7 +13,7 @@ // Scaling factors for a given frameSheet rendering: // Main drawing area: - drawingCanvasDpi = 10, + drawingCanvasDpi = 20, // Canvas previous in the slideshow: previewTileCanvasDpi = 4, // Ainmated canvas preview: @@ -208,9 +208,7 @@ canvasPreviewDeleteAction.className = "tile-action" canvasPreviewDeleteAction.innerHTML = "del" canvasPreviewDeleteAction.addEventListener('click', function(evt) { - frameSheet.removeFrameByIndex(tileNumber); - animIndex = 0; - piskel.createPreviews(); + piskel.removeFrame(tileNumber); }); previewTileRoot.appendChild(canvasPreviewDeleteAction); } @@ -287,8 +285,8 @@ }, drawAt : function (x, y, color) { - var pixelWidthIndex = (x - x%drawingCanvasDpi) / 10; - var pixelHeightIndex = (y - y%drawingCanvasDpi) / 10; + var pixelWidthIndex = (x - x%drawingCanvasDpi) / drawingCanvasDpi; + var pixelHeightIndex = (y - y%drawingCanvasDpi) / drawingCanvasDpi; // Update model: var currentFrame = frameSheet.getFrameByIndex(this.getActiveFrameIndex()); From 4366fc42ccbdb23bae0c2dd1974db9fdcb859faa Mon Sep 17 00:00:00 2001 From: Julian Descottes Date: Wed, 29 Aug 2012 00:57:55 +0200 Subject: [PATCH 10/18] Added serialization/deserialization capabilities Two changes to Framesheet : * serialize : basic implementation using JSON.stringify * deserialize : added method --- js/frameSheetModel.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/js/frameSheetModel.js b/js/frameSheetModel.js index 5ea52a0d..2de4c2b9 100644 --- a/js/frameSheetModel.js +++ b/js/frameSheetModel.js @@ -19,10 +19,23 @@ var FrameSheetModel = (function() { return true; // I'm always right dude }, - // Could be use to pass around model using long GET param (good enough for simple models) and + // Could be used to pass around model using long GET param (good enough for simple models) and // do some temporary locastorage serialize: function() { - throw "FrameSheet.serialize Not implemented" + return JSON.stringify(frames); + }, + + /** + * Load a framesheet from a string that might have been persisted in db / localstorage + * Overrides existing frames. + * @param {String} serialized + */ + deserializeFramesheet : function (serialized) { + try { + frames = eval(serialized); + } catch (e) { + throw "Could not load serialized framesheet." + e.getMessage() + } }, addEmptyFrame: function() { From b64f3d46bccfa660e5d09039294df15c5c3bb397 Mon Sep 17 00:00:00 2001 From: Julian Descottes Date: Wed, 29 Aug 2012 02:00:45 +0300 Subject: [PATCH 11/18] Update js/frameSheetModel.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit forgot a small change ...  --- js/frameSheetModel.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/js/frameSheetModel.js b/js/frameSheetModel.js index 2de4c2b9..1403cc88 100644 --- a/js/frameSheetModel.js +++ b/js/frameSheetModel.js @@ -1,4 +1,3 @@ - var FrameSheetModel = (function() { var inst; @@ -30,7 +29,7 @@ var FrameSheetModel = (function() { * Overrides existing frames. * @param {String} serialized */ - deserializeFramesheet : function (serialized) { + deserialize : function (serialized) { try { frames = eval(serialized); } catch (e) { From c1635b9d65faa4d519448258734bc64f4b3aa771 Mon Sep 17 00:00:00 2001 From: Vince Date: Wed, 29 Aug 2012 01:14:28 +0200 Subject: [PATCH 12/18] Shit cleaning :) --- js/frameSheetModel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/frameSheetModel.js b/js/frameSheetModel.js index 2de4c2b9..d54259db 100644 --- a/js/frameSheetModel.js +++ b/js/frameSheetModel.js @@ -32,9 +32,9 @@ var FrameSheetModel = (function() { */ deserializeFramesheet : function (serialized) { try { - frames = eval(serialized); + frames = JSON.parse(serialized); } catch (e) { - throw "Could not load serialized framesheet." + e.getMessage() + throw "Could not load serialized framesheet." + e.message() } }, From 6ea7f74d0033c3aaf877998f0f80880d8ee62f10 Mon Sep 17 00:00:00 2001 From: Julian Descottes Date: Wed, 29 Aug 2012 08:39:03 +0200 Subject: [PATCH 13/18] adding palette basics --- index.html | 2 ++ js/frameSheetModel.js | 4 ++++ js/piskel.js | 38 ++++++++++++++++++++++++++++++-------- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/index.html b/index.html index 39e3a207..f167a51d 100644 --- a/index.html +++ b/index.html @@ -26,6 +26,8 @@
+
    +
diff --git a/js/frameSheetModel.js b/js/frameSheetModel.js index cb533d1c..38232494 100644 --- a/js/frameSheetModel.js +++ b/js/frameSheetModel.js @@ -18,6 +18,10 @@ var FrameSheetModel = (function() { return true; // I'm always right dude }, + getUsedColors : function () { + return ["#000", "#fff"] + }, + // Could be used to pass around model using long GET param (good enough for simple models) and // do some temporary locastorage serialize: function() { diff --git a/js/piskel.js b/js/piskel.js index b033649f..bdb582f9 100644 --- a/js/piskel.js +++ b/js/piskel.js @@ -29,7 +29,8 @@ isRightClicked = false, activeFrameIndex = -1, animIndex = 0, - penColor = DEFAULT_PEN_COLOR; + penColor = DEFAULT_PEN_COLOR, + paletteColors = []; var piskel = { @@ -42,6 +43,7 @@ this.initPreviewSlideshow(); this.initAnimationPreview(); this.initColorPicker(); + this.initPalette(); }, setActiveFrame: function(index) { @@ -69,11 +71,25 @@ }, initColorPicker: function() { - var colorPicker = document.getElementById('color-picker'); - colorPicker.value = DEFAULT_PEN_COLOR; - colorPicker.addEventListener('change', function(evt) { + this.colorPicker = $('color-picker'); + this.colorPicker.value = DEFAULT_PEN_COLOR; + this.colorPicker.addEventListener('change', this.onPickerChange.bind(this)); + }, + + onPickerChange : function(evt) { penColor = colorPicker.value; - }); + }, + + initPalette : function (color) { + var colors = frameSheet.getUsedColors(); + var paletteEl = $('palette'); + paletteEl.innerHTML = ""; + for (var i = 0 ; i < colors.length ; i++) { + var color = colors[i]; + var colorEl = document.createElement("li"); + colorEl.setAttribute("data-color", color); + paletteEl.appendChild(colorEl); + } }, initDrawingArea : function() { @@ -87,13 +103,13 @@ drawingAreaContainer.setAttribute('style', 'width:' + framePixelWidth * drawingCanvasDpi + 'px; height:' + framePixelHeight * drawingCanvasDpi + 'px;'); - drawingAreaCanvas.setAttribute('oncontextmenu', 'piskel.onCanvasContextMenu(arguments[0])'); + drawingAreaCanvas.setAttribute('oncontextmenu', 'piskel.onCanvasContextMenu(event)'); drawingAreaContainer.appendChild(drawingAreaCanvas); var body = document.getElementsByTagName('body')[0]; body.setAttribute('onmouseup', 'piskel.onCanvasMouseup(arguments[0])'); - drawingAreaContainer.setAttribute('onmousedown', 'piskel.onCanvasMousedown(arguments[0])'); - drawingAreaContainer.setAttribute('onmousemove', 'piskel.onCanvasMousemove(arguments[0])'); + drawingAreaContainer.setAttribute('onmousedown', 'piskel.onCanvasMousedown(event)'); + drawingAreaContainer.setAttribute('onmousemove', 'piskel.onCanvasMousemove(event)'); this.drawFrameToCanvas(frameSheet.getFrameByIndex(this.getActiveFrameIndex()), drawingAreaCanvas, drawingCanvasDpi); }, @@ -325,6 +341,12 @@ event.cancelBubble = true; return false; }, + + onPaletteClick : function (event) { + var color = event.target.getAttribute("data-color"); + var colorPicker = $('color-picker'); + colorPicker.color.fromString(color); + }, getRelativeCoordinates : function (x, y) { var canvasRect = drawingAreaCanvas.getBoundingClientRect(); From 3664d87751f4672c703841d82fb3555cee02115d Mon Sep 17 00:00:00 2001 From: Firstname Lastname Date: Wed, 29 Aug 2012 10:15:18 +0200 Subject: [PATCH 14/18] temp commit --- js/frameSheetModel.js | 29 ++++++++++++++++++++++++++++- js/piskel.js | 3 ++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/js/frameSheetModel.js b/js/frameSheetModel.js index 38232494..3a833d6c 100644 --- a/js/frameSheetModel.js +++ b/js/frameSheetModel.js @@ -19,7 +19,34 @@ var FrameSheetModel = (function() { }, getUsedColors : function () { - return ["#000", "#fff"] + var colors = []; + var pixels = this.getAllPixels(); + for (var i = 0 ; i < pixels.length ; i++) { + var pixel = pixels[i]; + if (pixel && colors.indexOf(pixel)) { + colors.push(pixel); + } + } + return colors; + }, + + getAllPixels : function () { + var pixels = []; + for (var i = 0 ; i < frames.length ; i++) { + pixels = pixels.concat(this.getFramePixels(frames[i])); + } + return pixels; + }, + + getFramePixels : function (frame) { + var pixels = []; + for (var i = 0 ; i < frame.length ; i++) { + var line = frame[i]; + for (var j = 0 ; j < line.length ; j++) { + pixels.push(line[j]); + } + } + return pixels; }, // Could be used to pass around model using long GET param (good enough for simple models) and diff --git a/js/piskel.js b/js/piskel.js index bdb582f9..090535d6 100644 --- a/js/piskel.js +++ b/js/piskel.js @@ -77,7 +77,7 @@ }, onPickerChange : function(evt) { - penColor = colorPicker.value; + penColor = "#" + this.colorPicker.value; }, initPalette : function (color) { @@ -88,6 +88,7 @@ var color = colors[i]; var colorEl = document.createElement("li"); colorEl.setAttribute("data-color", color); + colorEl.innerHTML = color; paletteEl.appendChild(colorEl); } }, From 3a4ef0d6eb8a4d1fd0a3f6439c072dad330a3152 Mon Sep 17 00:00:00 2001 From: Firstname Lastname Date: Wed, 29 Aug 2012 10:46:25 +0200 Subject: [PATCH 15/18] palette finished + firefox fix --- css/piskel.css | 36 ------------------------------------ css/style.css | 7 +++++++ js/frameSheetModel.js | 16 ++-------------- js/piskel.js | 39 ++++++++++++++++++++++++--------------- 4 files changed, 33 insertions(+), 65 deletions(-) delete mode 100644 css/piskel.css diff --git a/css/piskel.css b/css/piskel.css deleted file mode 100644 index c384327b..00000000 --- a/css/piskel.css +++ /dev/null @@ -1,36 +0,0 @@ -html, body { - height : 100%; -} - -.debug { - border : 1px Solid black; -} - -.left-nav { - position:absolute; - top : 0; - bottom : 0; - width : 200px; - background : #000; -} - -.main-panel { - position:absolute; - top : 0; - bottom : 0; - left : 200px; - right : 0; - background : #ccc; -} - -.preview-container { - position : absolute; - top : 30px; - right : 0; - height : 200px; - width : 200px; - background : white; - border : 0px Solid black; - border-radius:5px 0px 0px 5px; - box-shadow : 0px 0px 2px rgba(0,0,0,0.2); -} \ No newline at end of file diff --git a/css/style.css b/css/style.css index 9136c5f0..3914911a 100644 --- a/css/style.css +++ b/css/style.css @@ -127,6 +127,13 @@ ul, li { z-index: 1; } +#palette li { + display : inline-block; + height : 20px; + width : 20px; + margin : 5px; +} + /* Force apparition of scrollbars on leopard */ ::-webkit-scrollbar { -webkit-appearance: none; diff --git a/js/frameSheetModel.js b/js/frameSheetModel.js index 3a833d6c..e322cda4 100644 --- a/js/frameSheetModel.js +++ b/js/frameSheetModel.js @@ -18,27 +18,15 @@ var FrameSheetModel = (function() { return true; // I'm always right dude }, - getUsedColors : function () { - var colors = []; - var pixels = this.getAllPixels(); - for (var i = 0 ; i < pixels.length ; i++) { - var pixel = pixels[i]; - if (pixel && colors.indexOf(pixel)) { - colors.push(pixel); - } - } - return colors; - }, - getAllPixels : function () { var pixels = []; for (var i = 0 ; i < frames.length ; i++) { - pixels = pixels.concat(this.getFramePixels(frames[i])); + pixels = pixels.concat(this._getFramePixels(frames[i])); } return pixels; }, - getFramePixels : function (frame) { + _getFramePixels : function (frame) { var pixels = []; for (var i = 0 ; i < frame.length ; i++) { var line = frame[i]; diff --git a/js/piskel.js b/js/piskel.js index 090535d6..619af33d 100644 --- a/js/piskel.js +++ b/js/piskel.js @@ -4,7 +4,7 @@ // Constants: TRANSPARENT_COLOR = 'tc', //TRANSPARENT_COLOR = 'pink', - DEFAULT_PEN_COLOR = '#000', + DEFAULT_PEN_COLOR = '#000000', // Configuration: // Canvas size in pixel size (not dpi related) @@ -39,11 +39,11 @@ frameSheet.addEmptyFrame(); this.setActiveFrame(0); + this.initPalette(); this.initDrawingArea(); this.initPreviewSlideshow(); this.initAnimationPreview(); this.initColorPicker(); - this.initPalette(); }, setActiveFrame: function(index) { @@ -77,19 +77,25 @@ }, onPickerChange : function(evt) { - penColor = "#" + this.colorPicker.value; + penColor = this.colorPicker.value; }, initPalette : function (color) { - var colors = frameSheet.getUsedColors(); - var paletteEl = $('palette'); - paletteEl.innerHTML = ""; - for (var i = 0 ; i < colors.length ; i++) { - var color = colors[i]; + var pixels = frameSheet.getAllPixels(); + this.paletteEl = $('palette'); + for (var i = 0 ; i < pixels.length ; i++) { + this.addColorToPalette(pixels[i]); + } + }, + + addColorToPalette : function (color) { + if (color && color != TRANSPARENT_COLOR && paletteColors.indexOf(color) == -1) { var colorEl = document.createElement("li"); colorEl.setAttribute("data-color", color); - colorEl.innerHTML = color; - paletteEl.appendChild(colorEl); + colorEl.setAttribute("title", color); + colorEl.style.background = color; + this.paletteEl.appendChild(colorEl); + paletteColors.push(color); } }, @@ -323,15 +329,15 @@ for(var col = 0, num_col = frame.length; col < num_col; col++) { for(var row = 0, num_row = frame[col].length; row < num_row; row++) { pixelColor = frame[col][row]; - if(pixelColor == undefined || pixelColor == TRANSPARENT_COLOR) { context.clearRect(col * dpi, row * dpi, dpi, dpi); } else { + if (pixelColor.indexOf("#") != 0) + pixelColor = "#" + pixelColor; + this.addColorToPalette(pixelColor); context.fillStyle = pixelColor; context.fillRect(col * dpi, row * dpi, dpi, dpi); } - - } } }, @@ -345,8 +351,11 @@ onPaletteClick : function (event) { var color = event.target.getAttribute("data-color"); - var colorPicker = $('color-picker'); - colorPicker.color.fromString(color); + if (null !== color) { + var colorPicker = $('color-picker'); + colorPicker.color.fromString(color); + this.onPickerChange(); + } }, getRelativeCoordinates : function (x, y) { From 6d2b1944fac07a4bacff840f56f81fed54f82d75 Mon Sep 17 00:00:00 2001 From: juliandescottes Date: Wed, 29 Aug 2012 13:46:57 +0200 Subject: [PATCH 16/18] Use slider for choosing FPS --- css/style.css | 4 ++++ index.html | 3 ++- js/piskel.js | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/css/style.css b/css/style.css index 3914911a..554be333 100644 --- a/css/style.css +++ b/css/style.css @@ -134,6 +134,10 @@ ul, li { margin : 5px; } +#preview-fps { + width : 200px; +} + /* Force apparition of scrollbars on leopard */ ::-webkit-scrollbar { -webkit-appearance: none; diff --git a/index.html b/index.html index f167a51d..4800f151 100644 --- a/index.html +++ b/index.html @@ -33,7 +33,8 @@
- + + 12 fps
diff --git a/js/piskel.js b/js/piskel.js index 619af33d..527989a0 100644 --- a/js/piskel.js +++ b/js/piskel.js @@ -149,7 +149,7 @@ }; var refreshUpdater = startPreviewRefresh(); - animFPSTuner.addEventListener('keyup', function(evt) { + animFPSTuner.addEventListener('change', function(evt) { window.clearInterval(refreshUpdater); animPreviewFPS = parseInt(animFPSTuner.value, 10); if(isNaN(animPreviewFPS)) { @@ -167,6 +167,7 @@ if(animPreviewFPS > 100) { animPreviewFPS = 100; } + $("display-fps").innerHTML = animPreviewFPS + " fps"; animFPSTuner.value = animPreviewFPS; refreshUpdater = startPreviewRefresh(); }); From 71c732665f32494c631e89a977a16138a7444b8f Mon Sep 17 00:00:00 2001 From: Julian Descottes Date: Wed, 29 Aug 2012 20:32:23 +0300 Subject: [PATCH 17/18] Update js/frameSheetModel.js Small mistake in catch block --- js/frameSheetModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/frameSheetModel.js b/js/frameSheetModel.js index e322cda4..f6d215e3 100644 --- a/js/frameSheetModel.js +++ b/js/frameSheetModel.js @@ -52,7 +52,7 @@ var FrameSheetModel = (function() { try { frames = JSON.parse(serialized); } catch (e) { - throw "Could not load serialized framesheet." + e.message() + throw "Could not load serialized framesheet." + e.message } }, From 33954364f9e1ad1c54c48d5fa2d009df97251e5a Mon Sep 17 00:00:00 2001 From: Julian Descottes Date: Wed, 29 Aug 2012 20:34:34 +0200 Subject: [PATCH 18/18] per pixel redraw --- js/piskel.js | 64 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/js/piskel.js b/js/piskel.js index 527989a0..8c4c0ba2 100644 --- a/js/piskel.js +++ b/js/piskel.js @@ -23,6 +23,7 @@ drawingAreaContainer, drawingAreaCanvas, previewCanvas, + paletteEl, // States: isClicked = false, @@ -30,7 +31,17 @@ activeFrameIndex = -1, animIndex = 0, penColor = DEFAULT_PEN_COLOR, - paletteColors = []; + paletteColors = [], + + //utility + _normalizeColor = function (color) { + if(color == undefined || color == TRANSPARENT_COLOR || color.indexOf("#") == 0) { + return color; + } else { + return "#" + color; + } + } + ; var piskel = { @@ -77,15 +88,11 @@ }, onPickerChange : function(evt) { - penColor = this.colorPicker.value; + penColor = _normalizeColor(this.colorPicker.value); }, initPalette : function (color) { - var pixels = frameSheet.getAllPixels(); - this.paletteEl = $('palette'); - for (var i = 0 ; i < pixels.length ; i++) { - this.addColorToPalette(pixels[i]); - } + paletteEl = $('palette'); }, addColorToPalette : function (color) { @@ -94,7 +101,7 @@ colorEl.setAttribute("data-color", color); colorEl.setAttribute("title", color); colorEl.style.background = color; - this.paletteEl.appendChild(colorEl); + paletteEl.appendChild(colorEl); paletteColors.push(color); } }, @@ -114,7 +121,7 @@ drawingAreaContainer.appendChild(drawingAreaCanvas); var body = document.getElementsByTagName('body')[0]; - body.setAttribute('onmouseup', 'piskel.onCanvasMouseup(arguments[0])'); + body.setAttribute('onmouseup', 'piskel.onCanvasMouseup(event)'); drawingAreaContainer.setAttribute('onmousedown', 'piskel.onCanvasMousedown(event)'); drawingAreaContainer.setAttribute('onmousemove', 'piskel.onCanvasMousemove(event)'); @@ -309,36 +316,39 @@ }, drawAt : function (x, y, color) { - var pixelWidthIndex = (x - x%drawingCanvasDpi) / drawingCanvasDpi; - var pixelHeightIndex = (y - y%drawingCanvasDpi) / drawingCanvasDpi; + var col = (x - x%drawingCanvasDpi) / drawingCanvasDpi; + var row = (y - y%drawingCanvasDpi) / drawingCanvasDpi; // Update model: var currentFrame = frameSheet.getFrameByIndex(this.getActiveFrameIndex()); // TODO: make a better accessor for pixel state update: // TODO: Make pen color dynamic: - currentFrame[pixelWidthIndex][pixelHeightIndex] = color; - - // Update view: - // TODO: Create a per pixel update function for perf ? - this.drawFrameToCanvas(currentFrame, drawingAreaCanvas, drawingCanvasDpi); + var color = _normalizeColor(color); + if (color != currentFrame[col][row]) { + currentFrame[col][row] = color; + this.drawPixelInCanvas(row, col, color, drawingAreaCanvas, drawingCanvasDpi); + } + }, + + drawPixelInCanvas : function (row, col, color, canvas, dpi) { + var context = canvas.getContext('2d'); + if(color == undefined || color == TRANSPARENT_COLOR) { + context.clearRect(col * dpi, row * dpi, dpi, dpi); + } else { + this.addColorToPalette(color); + context.fillStyle = color; + context.fillRect(col * dpi, row * dpi, dpi, dpi); + } }, // TODO: move that to a FrameRenderer (/w cache) ? drawFrameToCanvas: function(frame, canvasElement, dpi) { - var pixelColor, context = canvasElement.getContext('2d'); + var color; for(var col = 0, num_col = frame.length; col < num_col; col++) { for(var row = 0, num_row = frame[col].length; row < num_row; row++) { - pixelColor = frame[col][row]; - if(pixelColor == undefined || pixelColor == TRANSPARENT_COLOR) { - context.clearRect(col * dpi, row * dpi, dpi, dpi); - } else { - if (pixelColor.indexOf("#") != 0) - pixelColor = "#" + pixelColor; - this.addColorToPalette(pixelColor); - context.fillStyle = pixelColor; - context.fillRect(col * dpi, row * dpi, dpi, dpi); - } + color = _normalizeColor(frame[col][row]); + this.drawPixelInCanvas(row, col, color, canvasElement, dpi); } } },