mirror of
https://github.com/piskelapp/piskel.git
synced 2023-08-10 21:12:52 +03:00
![Vince](/assets/img/avatar_default.png)
Partial cleanup of piskel: new events and constant move palette and color picker in ToolSelector create Notifcation.js and move user message in it create LocalStorageService and move LS stuff in it
456 lines
15 KiB
JavaScript
456 lines
15 KiB
JavaScript
/**
|
|
* @require Constants
|
|
* @require Events
|
|
*/
|
|
$.namespace("pskl");
|
|
|
|
(function () {
|
|
|
|
/**
|
|
* FrameSheetModel instance.
|
|
*/
|
|
var frameSheet,
|
|
|
|
// Temporary zoom implementation to easily get bigger canvases to
|
|
// see how good perform critical algorithms on big canvas.
|
|
zoom = 1,
|
|
|
|
// Configuration:
|
|
// Canvas size in pixel size (not dpi related)
|
|
framePixelWidth = 32 * zoom,
|
|
framePixelHeight = 32 * zoom,
|
|
|
|
|
|
// Scaling factors for a given frameSheet rendering:
|
|
// Main drawing area:
|
|
drawingCanvasDpi = Math.ceil(20/ zoom),
|
|
// Canvas previous in the slideshow:
|
|
previewTileCanvasDpi = Math.ceil(4 / zoom),
|
|
// Ainmated canvas preview:
|
|
previewAnimationCanvasDpi = Math.ceil(8 / zoom),
|
|
|
|
// DOM references:
|
|
drawingAreaContainer,
|
|
drawingAreaCanvas,
|
|
previewCanvas,
|
|
|
|
// States:
|
|
isClicked = false,
|
|
isRightClicked = false,
|
|
activeFrameIndex = -1,
|
|
animIndex = 0,
|
|
penColor = Constants.DEFAULT_PEN_COLOR,
|
|
currentFrame = null;
|
|
currentToolBehavior = null,
|
|
previousMousemoveTime = 0,
|
|
|
|
//utility
|
|
_normalizeColor = function (color) {
|
|
if(color == undefined || color == Constants.TRANSPARENT_COLOR || color.indexOf("#") == 0) {
|
|
return color;
|
|
} else {
|
|
return "#" + color;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Main application controller
|
|
*/
|
|
var piskel = {
|
|
|
|
init : function () {
|
|
frameSheet = pskl.FrameSheetModel.getInstance(framePixelWidth, framePixelHeight);
|
|
frameSheet.addEmptyFrame();
|
|
this.setActiveFrame(0);
|
|
|
|
pskl.NotificationService.init();
|
|
pskl.LocalStorageService.init(frameSheet);
|
|
|
|
// TODO: Add comments
|
|
var frameId = this.getFrameIdFromUrl();
|
|
if (frameId) {
|
|
$.publish(Events.SHOW_NOTIFICATION, [{"content": "Loading animation with id : [" + frameId + "]"}]);
|
|
this.loadFramesheetFromService(frameId);
|
|
} else {
|
|
this.finishInit();
|
|
pskl.LocalStorageService.displayRestoreNotification();
|
|
}
|
|
},
|
|
|
|
finishInit : function () {
|
|
|
|
$.subscribe(Events.TOOL_SELECTED, function(evt, toolBehavior) {
|
|
console.log("Tool selected: ", toolBehavior);
|
|
currentToolBehavior = toolBehavior;
|
|
});
|
|
|
|
$.subscribe(Events.COLOR_SELECTED, function(evt, color) {
|
|
console.log("Color selected: ", color);
|
|
penColor = color;
|
|
});
|
|
|
|
$.subscribe(Events.REFRESH, function() {
|
|
piskel.setActiveFrameAndRedraw(0);
|
|
});
|
|
|
|
// TODO: Move this into their service or behavior files:
|
|
this.initDrawingArea();
|
|
this.initPreviewSlideshow();
|
|
this.initAnimationPreview();
|
|
this.startAnimation();
|
|
|
|
pskl.ToolSelector.init();
|
|
},
|
|
|
|
getFrameIdFromUrl : function() {
|
|
var href = window.location.href;
|
|
if (href.indexOf('frameId=') != -1) {
|
|
return href.substring(href.indexOf('frameId=')+8);
|
|
}
|
|
},
|
|
|
|
loadFramesheetFromService : function (frameId) {
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open('GET', Constants.PISKEL_SERVICE_URL + '/get?l=' + frameId, true);
|
|
xhr.responseType = 'text';
|
|
|
|
xhr.onload = function(e) {
|
|
frameSheet.deserialize(this.responseText);
|
|
$.publish(Events.HIDE_NOTIFICATION);
|
|
piskel.finishInit();
|
|
};
|
|
|
|
xhr.onerror = function () {
|
|
$.publish(Events.HIDE_NOTIFICATION);
|
|
piskel.finishInit();
|
|
};
|
|
|
|
xhr.send();
|
|
},
|
|
|
|
setActiveFrame: function(index) {
|
|
activeFrameIndex = index;
|
|
currentFrame = frameSheet.getFrameByIndex(activeFrameIndex)
|
|
},
|
|
|
|
setActiveFrameAndRedraw: function(index) {
|
|
this.setActiveFrame(index);
|
|
|
|
// Update drawing canvas:
|
|
this.drawFrameToCanvas(currentFrame, drawingAreaCanvas, drawingCanvasDpi);
|
|
|
|
// Update slideshow:
|
|
this.createPreviews();
|
|
|
|
// Update animation preview:
|
|
animIndex = 0;
|
|
},
|
|
|
|
getActiveFrameIndex: function() {
|
|
if(-1 == activeFrameIndex) {
|
|
throw "Bad active frane initialization."
|
|
}
|
|
return activeFrameIndex;
|
|
},
|
|
|
|
initDrawingArea : function() {
|
|
drawingAreaContainer = $('#drawing-canvas-container')[0];
|
|
|
|
drawingAreaCanvas = document.createElement("canvas");
|
|
drawingAreaCanvas.className = 'canvas';
|
|
drawingAreaCanvas.setAttribute('width', '' + framePixelWidth * drawingCanvasDpi);
|
|
drawingAreaCanvas.setAttribute('height', '' + framePixelHeight * drawingCanvasDpi);
|
|
|
|
drawingAreaContainer.setAttribute('style',
|
|
'width:' + framePixelWidth * drawingCanvasDpi + 'px; height:' + framePixelHeight * drawingCanvasDpi + 'px;');
|
|
|
|
drawingAreaCanvas.setAttribute('oncontextmenu', 'piskel.onCanvasContextMenu(event)');
|
|
drawingAreaContainer.appendChild(drawingAreaCanvas);
|
|
|
|
var body = document.getElementsByTagName('body')[0];
|
|
body.setAttribute('onmouseup', 'piskel.onDocumentBodyMouseup(event)');
|
|
drawingAreaContainer.setAttribute('onmousedown', 'piskel.onCanvasMousedown(event)');
|
|
drawingAreaContainer.setAttribute('onmousemove', 'piskel.onCanvasMousemove(event)');
|
|
this.drawFrameToCanvas(currentFrame, drawingAreaCanvas, drawingCanvasDpi);
|
|
},
|
|
|
|
initPreviewSlideshow: function() {
|
|
var addFrameButton = $('#add-frame-button')[0];
|
|
addFrameButton.addEventListener('mousedown', function() {
|
|
frameSheet.addEmptyFrame();
|
|
piskel.setActiveFrameAndRedraw(frameSheet.getFrameCount() - 1);
|
|
});
|
|
this.createPreviews();
|
|
},
|
|
|
|
initAnimationPreview : function() {
|
|
|
|
var previewAnimationContainer = $('#preview-canvas-container')[0];
|
|
previewCanvas = document.createElement('canvas');
|
|
previewCanvas.className = 'canvas';
|
|
previewAnimationContainer.setAttribute('style',
|
|
'width:' + framePixelWidth * previewAnimationCanvasDpi + 'px; height:' + framePixelHeight * previewAnimationCanvasDpi + 'px;');
|
|
previewAnimationContainer.appendChild(previewCanvas);
|
|
previewCanvas.setAttribute('width', framePixelWidth * previewAnimationCanvasDpi);
|
|
previewCanvas.setAttribute('height', framePixelHeight * previewAnimationCanvasDpi);
|
|
},
|
|
|
|
startAnimation : function () {
|
|
var scope = this;
|
|
var animFPSTuner = $("#preview-fps")[0];
|
|
var animPreviewFPS = parseInt(animFPSTuner.value, 10);
|
|
var startPreviewRefresh = function() {
|
|
return setInterval(scope.refreshAnimatedPreview, 1000/animPreviewFPS);
|
|
};
|
|
var refreshUpdater = startPreviewRefresh();
|
|
|
|
animFPSTuner.addEventListener('change', function(evt) {
|
|
window.clearInterval(refreshUpdater);
|
|
animPreviewFPS = parseInt(animFPSTuner.value, 10);
|
|
$("#display-fps").html(animPreviewFPS + " fps");
|
|
refreshUpdater = startPreviewRefresh();
|
|
});
|
|
},
|
|
|
|
createPreviews : function () {
|
|
var container = $('#preview-list')[0], previewTile;
|
|
container.innerHTML = "";
|
|
for (var i = 0, l = frameSheet.getFrameCount(); i < l ; i++) {
|
|
previewTile = this.createPreviewTile(i);
|
|
container.appendChild(previewTile);
|
|
}
|
|
},
|
|
|
|
createPreviewTile: function(tileNumber) {
|
|
var previewTileRoot = document.createElement("li");
|
|
var classname = "preview-tile";
|
|
|
|
if (this.getActiveFrameIndex() == tileNumber) {
|
|
classname += " selected";
|
|
}
|
|
previewTileRoot.className = classname;
|
|
|
|
var canvasContainer = document.createElement("div");
|
|
canvasContainer.className = "canvas-container";
|
|
canvasContainer.setAttribute('style',
|
|
'width:' + framePixelWidth * previewTileCanvasDpi + 'px; height:' + framePixelHeight * previewTileCanvasDpi + 'px;');
|
|
|
|
var canvasBackground = document.createElement("div");
|
|
canvasBackground.className = "canvas-background";
|
|
canvasContainer.appendChild(canvasBackground);
|
|
|
|
var canvasPreview = document.createElement("canvas");
|
|
canvasPreview.className = "canvas tile-view"
|
|
|
|
canvasPreview.setAttribute('width', framePixelWidth * previewTileCanvasDpi);
|
|
canvasPreview.setAttribute('height', framePixelHeight * previewTileCanvasDpi);
|
|
|
|
previewTileRoot.addEventListener('click', function(evt) {
|
|
// has not class tile-action:
|
|
// TODO: let me know when you want to start using a framework :)
|
|
if(!evt.target.className.match(new RegExp('(\\s|^)'+ 'tile-action' +'(\\s|$)'))) {
|
|
piskel.setActiveFrameAndRedraw(tileNumber);
|
|
}
|
|
});
|
|
|
|
var canvasPreviewDuplicateAction = document.createElement("button");
|
|
canvasPreviewDuplicateAction.className = "tile-action"
|
|
canvasPreviewDuplicateAction.innerHTML = "dup"
|
|
|
|
canvasPreviewDuplicateAction.addEventListener('click', function(evt) {
|
|
piskel.duplicateFrame(tileNumber);
|
|
});
|
|
|
|
this.drawFrameToCanvas(frameSheet.getFrameByIndex(tileNumber), canvasPreview, previewTileCanvasDpi);
|
|
canvasContainer.appendChild(canvasPreview);
|
|
previewTileRoot.appendChild(canvasContainer);
|
|
previewTileRoot.appendChild(canvasPreviewDuplicateAction);
|
|
|
|
if(tileNumber > 0 || frameSheet.getFrameCount() > 1) {
|
|
var canvasPreviewDeleteAction = document.createElement("button");
|
|
canvasPreviewDeleteAction.className = "tile-action"
|
|
canvasPreviewDeleteAction.innerHTML = "del"
|
|
canvasPreviewDeleteAction.addEventListener('click', function(evt) {
|
|
piskel.removeFrame(tileNumber);
|
|
});
|
|
previewTileRoot.appendChild(canvasPreviewDeleteAction);
|
|
}
|
|
|
|
return previewTileRoot;
|
|
},
|
|
|
|
refreshAnimatedPreview : function () {
|
|
piskel.drawFrameToCanvas(frameSheet.getFrameByIndex(animIndex), previewCanvas, previewAnimationCanvasDpi);
|
|
animIndex++;
|
|
if (animIndex == frameSheet.getFrameCount()) {
|
|
animIndex = 0;
|
|
}
|
|
},
|
|
|
|
removeFrame: function(frameIndex) {
|
|
frameSheet.removeFrameByIndex(frameIndex);
|
|
|
|
this.setActiveFrameAndRedraw(frameIndex - 1);
|
|
},
|
|
|
|
duplicateFrame: function(frameIndex) {
|
|
frameSheet.duplicateFrameByIndex(frameIndex);
|
|
|
|
this.setActiveFrameAndRedraw(frameIndex + 1);
|
|
},
|
|
|
|
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;
|
|
|
|
if(event.button == 2) { // right click
|
|
isRightClicked = true;
|
|
$.publish(Events.CANVAS_RIGHT_CLICKED);
|
|
}
|
|
var spriteCoordinate = this.getSpriteCoordinate(event);
|
|
currentToolBehavior.applyToolAt(
|
|
spriteCoordinate.col,
|
|
spriteCoordinate.row,
|
|
currentFrame,
|
|
penColor,
|
|
drawingAreaCanvas,
|
|
drawingCanvasDpi);
|
|
|
|
$.publish(Events.LOCALSTORAGE_REQUEST);
|
|
},
|
|
|
|
onCanvasMousemove : function (event) {
|
|
|
|
//this.updateCursorInfo(event);
|
|
var currentTime = new Date().getTime();
|
|
// Throttling of the mousemove event:
|
|
if ((currentTime - previousMousemoveTime) > 40 ) {
|
|
if (isClicked) {
|
|
var spriteCoordinate = this.getSpriteCoordinate(event);
|
|
currentToolBehavior.moveToolAt(
|
|
spriteCoordinate.col,
|
|
spriteCoordinate.row,
|
|
currentFrame,
|
|
penColor,
|
|
drawingAreaCanvas,
|
|
drawingCanvasDpi);
|
|
|
|
// TODO(vincz): Find a way to move that to the model instead of being at the interaction level.
|
|
// Eg when drawing, it may make sense to have it here. However for a non drawing tool,
|
|
// you don't need to draw anything when mousemoving and you request useless localStorage.
|
|
$.publish(Events.LOCALSTORAGE_REQUEST);
|
|
}
|
|
previousMousemoveTime = currentTime;
|
|
}
|
|
},
|
|
|
|
onDocumentBodyMouseup : 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.
|
|
// TODO: Remove that when we have the centralized redraw loop
|
|
this.createPreviews();
|
|
}
|
|
|
|
if(isRightClicked) {
|
|
$.publish(Events.CANVAS_RIGHT_CLICK_RELEASED);
|
|
}
|
|
isClicked = false;
|
|
isRightClicked = false;
|
|
var spriteCoordinate = this.getSpriteCoordinate(event);
|
|
currentToolBehavior.releaseToolAt(
|
|
spriteCoordinate.col,
|
|
spriteCoordinate.row,
|
|
currentFrame,
|
|
penColor,
|
|
drawingAreaCanvas,
|
|
drawingCanvasDpi);
|
|
},
|
|
|
|
// TODO(vincz/julz): Refactor to make this disappear in a big event-driven redraw loop
|
|
drawFrameToCanvas: function(frame, canvasElement, dpi) {
|
|
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++) {
|
|
color = _normalizeColor(frame[col][row]);
|
|
this.drawPixelInCanvas(row, col, color, canvasElement, dpi);
|
|
}
|
|
}
|
|
},
|
|
|
|
// TODO(vincz/julz): Refactor to make this disappear in a big event-driven redraw loop
|
|
drawPixelInCanvas : function (row, col, color, canvas, dpi) {
|
|
var context = canvas.getContext('2d');
|
|
if(color == undefined || color == Constants.TRANSPARENT_COLOR) {
|
|
context.clearRect(col * dpi, row * dpi, dpi, dpi);
|
|
} else {
|
|
context.fillStyle = color;
|
|
context.fillRect(col * dpi, row * dpi, dpi, dpi);
|
|
}
|
|
},
|
|
|
|
onCanvasContextMenu : function (event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
event.cancelBubble = true;
|
|
return false;
|
|
},
|
|
|
|
getRelativeCoordinates : function (x, y) {
|
|
var canvasRect = drawingAreaCanvas.getBoundingClientRect();
|
|
return {
|
|
x : x - canvasRect.left,
|
|
y : y - canvasRect.top
|
|
}
|
|
},
|
|
|
|
getSpriteCoordinate : function(event) {
|
|
var coord = this.getRelativeCoordinates(event.x, event.y);
|
|
var coords = this.getRelativeCoordinates(event.clientX, event.clientY);
|
|
return {
|
|
"col" : (coords.x - coords.x%drawingCanvasDpi) / drawingCanvasDpi,
|
|
"row" : (coords.y - coords.y%drawingCanvasDpi) / drawingCanvasDpi
|
|
}
|
|
},
|
|
|
|
// TODO(julz): Create package ?
|
|
storeSheet : function (event) {
|
|
// TODO Refactor using jquery ?
|
|
var xhr = new XMLHttpRequest();
|
|
var formData = new FormData();
|
|
formData.append('framesheet_content', frameSheet.serialize());
|
|
formData.append('fps_speed', $('#preview-fps').val());
|
|
xhr.open('POST', Constants.PISKEL_SERVICE_URL + "/store", true);
|
|
xhr.onload = function(e) {
|
|
if (this.status == 200) {
|
|
var baseUrl = window.location.href.replace(window.location.search, "");
|
|
window.location.href = baseUrl + "?frameId=" + this.responseText;
|
|
}
|
|
};
|
|
|
|
xhr.send(formData);
|
|
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
return false;
|
|
}
|
|
};
|
|
|
|
window.piskel = piskel;
|
|
piskel.init();
|
|
|
|
})();
|