piskel/js/piskel.js

434 lines
14 KiB
JavaScript
Raw Normal View History

/**
* @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;
/**
* Main application controller
*/
var piskel = {
init : function () {
frameSheet = pskl.FrameSheetModel.getInstance(framePixelWidth, framePixelHeight);
2012-08-29 23:16:13 +00:00
frameSheet.addEmptyFrame();
this.setActiveFrame(0);
pskl.NotificationService.init();
pskl.LocalStorageService.init(frameSheet);
// TODO: Add comments
var framesheetId = this.getFramesheetIdFromUrl();
if (framesheetId) {
$.publish(Events.SHOW_NOTIFICATION, [{"content": "Loading animation with id : [" + framesheetId + "]"}]);
this.loadFramesheetFromService(framesheetId);
2012-08-29 23:16:13 +00:00
} else {
this.finishInit();
pskl.LocalStorageService.displayRestoreNotification();
2012-08-29 23:16:13 +00:00
}
},
2012-08-29 23:16:13 +00:00
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();
2012-09-02 23:39:21 +00:00
this.initPreviewSlideshow();
this.initAnimationPreview();
2012-08-29 23:16:13 +00:00
this.startAnimation();
var frame = frameSheet.getFrameByIndex(this.getActiveFrameIndex());
this.drawer = new pskl.rendering.DrawingController(
frame,
$('#drawing-canvas-container')[0],
drawingCanvasDpi
);
pskl.ToolSelector.init();
pskl.Palette.init(frameSheet);
2012-08-29 23:16:13 +00:00
},
getFramesheetIdFromUrl : function() {
2012-08-29 23:16:13 +00:00
var href = window.location.href;
// TODO: Change frameId to framesheetId on the backend
2012-08-29 23:16:13 +00:00
if (href.indexOf('frameId=') != -1) {
return href.substring(href.indexOf('frameId=')+8);
}
},
loadFramesheetFromService : function (frameId) {
var xhr = new XMLHttpRequest();
// TODO: Change frameId to framesheetId on the backend
xhr.open('GET', Constants.PISKEL_SERVICE_URL + '/get?l=' + frameId, true);
2012-08-29 23:16:13 +00:00
xhr.responseType = 'text';
xhr.onload = function(e) {
frameSheet.deserialize(this.responseText);
piskel.setActiveFrame(0);
$.publish(Events.HIDE_NOTIFICATION);
2012-08-29 23:16:13 +00:00
piskel.finishInit();
piskel.setActiveFrameAndRedraw(0);
2012-08-29 23:16:13 +00:00
};
xhr.onerror = function () {
$.publish(Events.HIDE_NOTIFICATION);
2012-08-29 23:16:13 +00:00
piskel.finishInit();
piskel.setActiveFrameAndRedraw(0);
2012-08-29 23:16:13 +00:00
};
xhr.send();
2012-08-29 21:32:51 +00:00
},
2012-08-29 21:01:04 +00:00
setActiveFrame: function(index) {
activeFrameIndex = index;
currentFrame = frameSheet.getFrameByIndex(activeFrameIndex)
},
setActiveFrameAndRedraw: function(index) {
this.setActiveFrame(index);
// When redraw engine is refactored, remove the following crap and
// trigger an event instead:
// 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];
var body = document.getElementsByTagName('body')[0];
body.setAttribute('onmouseup', 'piskel.onDocumentBodyMouseup(event)');
drawingAreaContainer.style.width = framePixelWidth * drawingCanvasDpi + "px";
drawingAreaContainer.style.height = framePixelHeight * drawingCanvasDpi + "px";
drawingAreaContainer.setAttribute('oncontextmenu', 'piskel.onCanvasContextMenu(event)');
2012-08-29 06:39:03 +00:00
drawingAreaContainer.setAttribute('onmousedown', 'piskel.onCanvasMousedown(event)');
drawingAreaContainer.setAttribute('onmousemove', 'piskel.onCanvasMousemove(event)');
},
2012-08-29 21:32:51 +00:00
initPreviewSlideshow: function() {
var addFrameButton = $('#add-frame-button')[0];
2012-08-29 21:32:51 +00:00
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);
2012-08-29 23:16:13 +00:00
},
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();
2012-08-29 11:46:57 +00:00
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);
},
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,
penColor,
this.drawer
);
$.publish(Events.LOCALSTORAGE_REQUEST);
},
onCanvasMousemove : function (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,
penColor,
this.drawer
);
// 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,
penColor,
this.drawer
);
},
// 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 = frame[col][row];
this.drawPixelInCanvas(row, col, color, canvasElement, dpi);
}
2012-08-29 21:01:04 +00:00
}
2012-08-29 18:34:34 +00:00
},
// TODO(vincz/julz): Refactor to make this disappear in a big event-driven redraw loop
2012-08-29 18:34:34 +00:00
drawPixelInCanvas : function (row, col, color, canvas, dpi) {
var context = canvas.getContext('2d');
if(color == undefined || color == Constants.TRANSPARENT_COLOR) {
2012-08-29 18:34:34 +00:00
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;
},
2012-08-29 06:39:03 +00:00
getRelativeCoordinates : function (x, y) {
var canvasRect = $(".canvas-main")[0].getBoundingClientRect();
return {
x : x - canvasRect.left,
y : y - canvasRect.top
}
2012-08-29 23:16:13 +00:00
},
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 ?
2012-08-29 23:16:13 +00:00
storeSheet : function (event) {
// TODO Refactor using jquery ?
2012-08-29 23:16:13 +00:00
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);
2012-08-29 23:16:13 +00:00
xhr.onload = function(e) {
if (this.status == 200) {
2012-08-29 23:26:14 +00:00
var baseUrl = window.location.href.replace(window.location.search, "");
window.location.href = baseUrl + "?frameId=" + this.responseText;
2012-08-29 23:16:13 +00:00
}
};
xhr.send(formData);
event.stopPropagation();
event.preventDefault();
return false;
}
};
window.piskel = piskel;
piskel.init();
})();