Merge branch 'master' into gh-pages

This commit is contained in:
juliandescottes 2012-09-19 13:17:44 +02:00
commit 10a7fd4890
11 changed files with 506 additions and 440 deletions

View File

@ -2,7 +2,7 @@
.preview-container { .preview-container {
position : absolute; position : absolute;
bottom : 0; right : 0; bottom : 0; right : 0;
height : 256px; height : 282px;
width : 256px; width : 256px;
background : white; background : white;
border : 0px Solid black; border : 0px Solid black;
@ -12,7 +12,6 @@
.preview-container canvas { .preview-container canvas {
border : 0px Solid transparent; border : 0px Solid transparent;
border-radius:5px 0px 0px 5px;
} }
#preview-fps { #preview-fps {

View File

@ -1,4 +1,11 @@
var Constants = { var Constants = {
DEFAULT_SIZE : {
height : 32,
width : 32
},
MAX_HEIGHT : 128,
MAX_WIDTH : 128,
DEFAULT_PEN_COLOR : '#000000', DEFAULT_PEN_COLOR : '#000000',
TRANSPARENT_COLOR : 'TRANSPARENT', TRANSPARENT_COLOR : 'TRANSPARENT',

View File

@ -35,6 +35,8 @@ Events = {
*/ */
FRAMESHEET_RESET: "FRAMESHEET_RESET", FRAMESHEET_RESET: "FRAMESHEET_RESET",
FRAME_SIZE_CHANGED : "FRAME_SIZE_CHANGED",
CURRENT_FRAME_SET: "CURRENT_FRAME_SET", CURRENT_FRAME_SET: "CURRENT_FRAME_SET",
SELECTION_CREATED: "SELECTION_CREATED", SELECTION_CREATED: "SELECTION_CREATED",

View File

@ -1,40 +1,57 @@
(function () { (function () {
var ns = $.namespace("pskl.controller"); var ns = $.namespace("pskl.controller");
ns.AnimatedPreviewController = function (framesheet, container, dpi) { ns.AnimatedPreviewController = function (framesheet, container, dpi) {
this.framesheet = framesheet; this.framesheet = framesheet;
this.container = container; this.container = container;
this.elapsedTime = 0; this.elapsedTime = 0;
this.currentIndex = 0; this.currentIndex = 0;
this.fps = parseInt($("#preview-fps")[0].value, 10); this.fps = parseInt($("#preview-fps")[0].value, 10);
var renderingOptions = { var renderingOptions = {
"dpi": dpi "dpi": this.calculateDPI_()
}; };
this.renderer = new pskl.rendering.FrameRenderer(this.container, renderingOptions); this.renderer = new pskl.rendering.FrameRenderer(this.container, renderingOptions);
};
ns.AnimatedPreviewController.prototype.init = function () { $.subscribe(Events.FRAME_SIZE_CHANGED, this.updateDPI_.bind(this));
$("#preview-fps")[0].addEventListener('change', this.onFPSSliderChange.bind(this));
};
ns.AnimatedPreviewController.prototype.onFPSSliderChange = function(evt) {
this.fps = parseInt($("#preview-fps")[0].value, 10);
$("#display-fps").html(this.fps + " FPS")
};
ns.AnimatedPreviewController.prototype.render = function (delta) {
this.elapsedTime += delta;
var index = Math.floor(this.elapsedTime / (1000/this.fps));
if (index != this.currentIndex) {
this.currentIndex = index;
if (!this.framesheet.hasFrameAtIndex(this.currentIndex)) {
this.currentIndex = 0;
this.elapsedTime = 0;
}
this.renderer.render(this.framesheet.getFrameByIndex(this.currentIndex));
}
}; };
ns.AnimatedPreviewController.prototype.init = function () {
$("#preview-fps")[0].addEventListener('change', this.onFPSSliderChange.bind(this));
};
ns.AnimatedPreviewController.prototype.onFPSSliderChange = function(evt) {
this.fps = parseInt($("#preview-fps")[0].value, 10);
$("#display-fps").html(this.fps + " FPS")
};
ns.AnimatedPreviewController.prototype.render = function (delta) {
this.elapsedTime += delta;
var index = Math.floor(this.elapsedTime / (1000/this.fps));
if (index != this.currentIndex) {
this.currentIndex = index;
if (!this.framesheet.hasFrameAtIndex(this.currentIndex)) {
this.currentIndex = 0;
this.elapsedTime = 0;
}
this.renderer.render(this.framesheet.getFrameByIndex(this.currentIndex));
}
};
/**
* Calculate the preview DPI depending on the framesheet size
*/
ns.AnimatedPreviewController.prototype.calculateDPI_ = function () {
var framePixelHeight = this.framesheet.getCurrentFrame().getHeight(),
framePixelWidth = this.framesheet.getCurrentFrame().getWidth();
// TODO (julz) : should have a utility to get a Size from framesheet easily (what about empty framesheets though ?)
return pskl.PixelUtils.calculateDPIForContainer($(".preview-container"), framePixelHeight, framePixelWidth);
};
ns.AnimatedPreviewController.prototype.updateDPI_ = function () {
this.dpi = this.calculateDPI_();
this.renderer.updateDPI(this.dpi);
}
})(); })();

View File

@ -1,12 +1,6 @@
(function () { (function () {
var ns = $.namespace("pskl.controller"); var ns = $.namespace("pskl.controller");
ns.DrawingController = function (framesheet, container, dpi) { ns.DrawingController = function (framesheet, container) {
// TODO(vincz): Store user prefs in a localstorage string ?
var renderingOptions = {
"dpi": dpi,
"hasGrid" : true
};
/** /**
* @public * @public
*/ */
@ -21,6 +15,12 @@
* @private * @private
*/ */
this.container = container; this.container = container;
// TODO(vincz): Store user prefs in a localstorage string ?
var renderingOptions = {
"dpi": this.calculateDPI_(),
"hasGrid" : true
};
this.renderer = new pskl.rendering.FrameRenderer(this.container, renderingOptions, "drawing-canvas"); this.renderer = new pskl.rendering.FrameRenderer(this.container, renderingOptions, "drawing-canvas");
this.overlayRenderer = new pskl.rendering.FrameRenderer(this.container, renderingOptions, "canvas-overlay"); this.overlayRenderer = new pskl.rendering.FrameRenderer(this.container, renderingOptions, "canvas-overlay");
@ -51,6 +51,11 @@
this.secondaryColor = color; this.secondaryColor = color;
} }
}, this)); }, this));
$(window).resize($.proxy(this.startDPIUpdateTimer_, this));
$.subscribe(Events.FRAME_SIZE_CHANGED, $.proxy(this.updateDPI_, this));
$.subscribe(Events.GRID_DISPLAY_STATE_CHANGED, $.proxy(this.forceRendering_, this));
}; };
ns.DrawingController.prototype.initMouseBehavior = function() { ns.DrawingController.prototype.initMouseBehavior = function() {
@ -63,6 +68,13 @@
body.contextmenu(this.onCanvasContextMenu_); body.contextmenu(this.onCanvasContextMenu_);
}; };
ns.DrawingController.prototype.startDPIUpdateTimer_ = function () {
if (this.dpiUpdateTimer) window.clearInterval(this.dpiUpdateTimer);
this.dpiUpdateTimer = window.setTimeout($.proxy(this.updateDPI_, this), 200);
},
/** /**
* @private * @private
*/ */
@ -185,12 +197,6 @@
event.cancelBubble = true; event.cancelBubble = true;
return false; return false;
}; };
ns.DrawingController.prototype.updateDPI = function (newDPI) {
this.renderer.updateDPI(newDPI);
this.overlayRenderer.updateDPI(newDPI);
this.forceRendering_();
};
ns.DrawingController.prototype.render = function () { ns.DrawingController.prototype.render = function () {
this.renderFrame(); this.renderFrame();
@ -216,5 +222,45 @@
ns.DrawingController.prototype.forceRendering_ = function () { ns.DrawingController.prototype.forceRendering_ = function () {
this.serializedFrame = this.serializedOverlay = null; this.serializedFrame = this.serializedOverlay = null;
} };
/**
* @private
*/
ns.DrawingController.prototype.calculateDPI_ = function() {
var userMessageGap = 80; // Reserve some height to show the user message at the bottom
var availableViewportHeight = $('.main-panel').height() - userMessageGap,
availableViewportWidth = $('.main-panel').width(),
previewHeight = $(".preview-container").height(),
previewWidth = $(".preview-container").width(),
framePixelHeight = this.framesheet.getCurrentFrame().getHeight(),
framePixelWidth = this.framesheet.getCurrentFrame().getWidth();
var dpi = pskl.PixelUtils.calculateDPI(availableViewportHeight, availableViewportWidth, framePixelHeight, framePixelWidth);
var drawingCanvasHeight = dpi * framePixelHeight;
var drawingCanvasWidth = dpi * framePixelWidth;
// Check if preview and drawing canvas overlap
var heightGap = drawingCanvasHeight + previewHeight - availableViewportHeight,
widthGap = drawingCanvasWidth + previewWidth - availableViewportWidth;
if (heightGap > 0 && widthGap > 0) {
// Calculate the DPI change needed to bridge height and width gap
var gapDPI = pskl.PixelUtils.calculateDPI(heightGap, widthGap, framePixelHeight, framePixelWidth);
// substract gap dpi to initial dpi
dpi -= (gapDPI + 1);
}
return dpi;
};
/**
* @private
*/
ns.DrawingController.prototype.updateDPI_ = function() {
var dpi = this.calculateDPI_();
console.log("dpi", dpi);
this.renderer.updateDPI(dpi);
this.overlayRenderer.updateDPI(dpi);
this.forceRendering_();
};
})(); })();

View File

@ -1,226 +1,243 @@
(function () { (function () {
var ns = $.namespace("pskl.controller"); var ns = $.namespace("pskl.controller");
ns.PreviewFilmController = function (framesheet, container, dpi) { ns.PreviewFilmController = function (framesheet, container, dpi) {
this.dpi = dpi; this.framesheet = framesheet;
this.framesheet = framesheet; this.container = container;
this.container = container; this.dpi = this.calculateDPI_();
this.redrawFlag = true; this.redrawFlag = true;
$.subscribe(Events.TOOL_RELEASED, this.flagForRedraw_.bind(this)); $.subscribe(Events.TOOL_RELEASED, this.flagForRedraw_.bind(this));
$.subscribe(Events.FRAMESHEET_RESET, this.flagForRedraw_.bind(this)); $.subscribe(Events.FRAMESHEET_RESET, this.flagForRedraw_.bind(this));
}; $.subscribe(Events.FRAMESHEET_RESET, this.refreshDPI_.bind(this));
};
ns.PreviewFilmController.prototype.init = function() { ns.PreviewFilmController.prototype.init = function() {
var addFrameButton = $('#add-frame-button')[0]; var addFrameButton = $('#add-frame-button')[0];
addFrameButton.addEventListener('mousedown', this.addFrame.bind(this)); addFrameButton.addEventListener('mousedown', this.addFrame.bind(this));
}; };
ns.PreviewFilmController.prototype.addFrame = function () { ns.PreviewFilmController.prototype.addFrame = function () {
this.framesheet.addEmptyFrame(); this.framesheet.addEmptyFrame();
this.framesheet.setCurrentFrameIndex(this.framesheet.getFrameCount() - 1); this.framesheet.setCurrentFrameIndex(this.framesheet.getFrameCount() - 1);
}; };
ns.PreviewFilmController.prototype.flagForRedraw_ = function () { ns.PreviewFilmController.prototype.flagForRedraw_ = function () {
this.redrawFlag = true; this.redrawFlag = true;
};
ns.PreviewFilmController.prototype.refreshDPI_ = function () {
this.dpi = this.calculateDPI_();
}; };
ns.PreviewFilmController.prototype.render = function () { ns.PreviewFilmController.prototype.render = function () {
if (this.redrawFlag) { if (this.redrawFlag) {
// TODO(vincz): Full redraw on any drawing modification, optimize. // TODO(vincz): Full redraw on any drawing modification, optimize.
this.createPreviews_(); this.createPreviews_();
this.redrawFlag = false; this.redrawFlag = false;
} }
}; };
ns.PreviewFilmController.prototype.createPreviews_ = function () { ns.PreviewFilmController.prototype.createPreviews_ = function () {
this.container.html(""); this.container.html("");
// Manually remove tooltips since mouseout events were shortcut by the DOM refresh: // Manually remove tooltips since mouseout events were shortcut by the DOM refresh:
$(".tooltip").remove(); $(".tooltip").remove();
var frameCount = this.framesheet.getFrameCount(); var frameCount = this.framesheet.getFrameCount();
for (var i = 0, l = frameCount; i < l ; i++) { for (var i = 0, l = frameCount; i < l ; i++) {
this.container.append(this.createInterstitialTile_(i)); this.container.append(this.createInterstitialTile_(i));
this.container.append(this.createPreviewTile_(i)); this.container.append(this.createPreviewTile_(i));
} }
this.container.append(this.createInterstitialTile_(frameCount)); this.container.append(this.createInterstitialTile_(frameCount));
var needDragndropBehavior = !!(frameCount > 1); var needDragndropBehavior = !!(frameCount > 1);
if(needDragndropBehavior) { if(needDragndropBehavior) {
this.initDragndropBehavior_(); this.initDragndropBehavior_();
} }
}; };
/** /**
* @private * @private
*/ */
ns.PreviewFilmController.prototype.createInterstitialTile_ = function (tileNumber) { ns.PreviewFilmController.prototype.createInterstitialTile_ = function (tileNumber) {
var interstitialTile = document.createElement("div"); var interstitialTile = document.createElement("div");
interstitialTile.className = "interstitial-tile" interstitialTile.className = "interstitial-tile"
interstitialTile.setAttribute("data-tile-type", "interstitial"); interstitialTile.setAttribute("data-tile-type", "interstitial");
interstitialTile.setAttribute("data-inject-drop-tile-at", tileNumber); interstitialTile.setAttribute("data-inject-drop-tile-at", tileNumber);
return interstitialTile; return interstitialTile;
}; };
/** /**
* @private * @private
*/ */
ns.PreviewFilmController.prototype.initDragndropBehavior_ = function () { ns.PreviewFilmController.prototype.initDragndropBehavior_ = function () {
var tiles = $(".preview-tile"); var tiles = $(".preview-tile");
// Each preview film tile is draggable. // Each preview film tile is draggable.
tiles.draggable( { tiles.draggable( {
//containment: '.left-nav', //containment: '.left-nav',
stack: '.preview-tile', stack: '.preview-tile',
cursor: 'move', cursor: 'move',
revert: true, revert: true,
start: function(event, ui) { start: function(event, ui) {
// We only show the fake interstitial tiles when starting the // We only show the fake interstitial tiles when starting the
// drag n drop interaction. We hide them when the DnD is done. // drag n drop interaction. We hide them when the DnD is done.
$('#preview-list').addClass("show-interstitial-tiles"); $('#preview-list').addClass("show-interstitial-tiles");
}, },
stop: function() { stop: function() {
$('#preview-list').removeClass("show-interstitial-tiles"); $('#preview-list').removeClass("show-interstitial-tiles");
} }
}); });
// Each preview film tile is a drop target. This allow us to swap two tiles. // Each preview film tile is a drop target. This allow us to swap two tiles.
// However, we want to be able to insert a tile between two other tiles. // However, we want to be able to insert a tile between two other tiles.
// For that we created fake interstitial tiles that are used as drop targets as well. // For that we created fake interstitial tiles that are used as drop targets as well.
var droppableTiles = $(".interstitial-tile"); var droppableTiles = $(".interstitial-tile");
$.merge(droppableTiles, tiles); $.merge(droppableTiles, tiles);
droppableTiles.droppable( { droppableTiles.droppable( {
accept: ".preview-tile", accept: ".preview-tile",
tolerance: "pointer", tolerance: "pointer",
activeClass: "droppable-active", activeClass: "droppable-active",
hoverClass: "droppable-hover-active", hoverClass: "droppable-hover-active",
drop: $.proxy(this.onDrop_, this) drop: $.proxy(this.onDrop_, this)
}); });
}; };
/** /**
* @private * @private
*/ */
ns.PreviewFilmController.prototype.onDrop_ = function( event, ui ) { ns.PreviewFilmController.prototype.onDrop_ = function( event, ui ) {
var activeFrame; var activeFrame;
// When we drag from an element, the drag could start from a nested DOM element // When we drag from an element, the drag could start from a nested DOM element
// inside the drag target. We normalize that by taking the correct ancestor: // inside the drag target. We normalize that by taking the correct ancestor:
var originTile = $(event.srcElement).closest(".preview-tile"); var originTile = $(event.srcElement).closest(".preview-tile");
var originFrameId = parseInt(originTile.data("tile-number"), 10); var originFrameId = parseInt(originTile.data("tile-number"), 10);
var dropTarget = $(event.target); var dropTarget = $(event.target);
if(dropTarget.data("tile-type") == "interstitial") { if(dropTarget.data("tile-type") == "interstitial") {
var targetInsertionId = parseInt(dropTarget.data("inject-drop-tile-at"), 10); var targetInsertionId = parseInt(dropTarget.data("inject-drop-tile-at"), 10);
// In case we drop outside of the tile container // In case we drop outside of the tile container
if(isNaN(originFrameId) || isNaN(targetInsertionId)) { if(isNaN(originFrameId) || isNaN(targetInsertionId)) {
return; return;
} }
//console.log("origin-frame: "+originFrameId+" - targetInsertionId: "+ targetInsertionId) //console.log("origin-frame: "+originFrameId+" - targetInsertionId: "+ targetInsertionId)
this.framesheet.moveFrame(originFrameId, targetInsertionId); this.framesheet.moveFrame(originFrameId, targetInsertionId);
activeFrame = targetInsertionId; activeFrame = targetInsertionId;
// The last fake interstitial tile is outside of the framesheet array bound. // The last fake interstitial tile is outside of the framesheet array bound.
// It allow us to append after the very last element in this fake slot. // It allow us to append after the very last element in this fake slot.
// However, when setting back the active frame, we have to make sure the // However, when setting back the active frame, we have to make sure the
// frame does exist. // frame does exist.
if(activeFrame > (this.framesheet.getFrameCount() - 1)) { if(activeFrame > (this.framesheet.getFrameCount() - 1)) {
activeFrame = targetInsertionId - 1; activeFrame = targetInsertionId - 1;
} }
} else { } else {
var targetSwapId = parseInt(dropTarget.data("tile-number"), 10); var targetSwapId = parseInt(dropTarget.data("tile-number"), 10);
// In case we drop outside of the tile container // In case we drop outside of the tile container
if(isNaN(originFrameId) || isNaN(targetSwapId)) { if(isNaN(originFrameId) || isNaN(targetSwapId)) {
return; return;
} }
//console.log("origin-frame: "+originFrameId+" - targetSwapId: "+ targetSwapId) //console.log("origin-frame: "+originFrameId+" - targetSwapId: "+ targetSwapId)
this.framesheet.swapFrames(originFrameId, targetSwapId); this.framesheet.swapFrames(originFrameId, targetSwapId);
activeFrame = targetSwapId; activeFrame = targetSwapId;
} }
$('#preview-list').removeClass("show-interstitial-tiles"); $('#preview-list').removeClass("show-interstitial-tiles");
this.framesheet.setCurrentFrameIndex(activeFrame); this.framesheet.setCurrentFrameIndex(activeFrame);
// TODO(vincz): move localstorage request to the model layer? // TODO(vincz): move localstorage request to the model layer?
$.publish(Events.LOCALSTORAGE_REQUEST); $.publish(Events.LOCALSTORAGE_REQUEST);
}; };
/** /**
* @private * @private
* TODO(vincz): clean this giant rendering function & remove listeners. * TODO(vincz): clean this giant rendering function & remove listeners.
*/ */
ns.PreviewFilmController.prototype.createPreviewTile_ = function(tileNumber) { ns.PreviewFilmController.prototype.createPreviewTile_ = function(tileNumber) {
var currentFrame = this.framesheet.getFrameByIndex(tileNumber); var currentFrame = this.framesheet.getFrameByIndex(tileNumber);
var previewTileRoot = document.createElement("li"); var previewTileRoot = document.createElement("li");
var classname = "preview-tile"; var classname = "preview-tile";
previewTileRoot.setAttribute("data-tile-number", tileNumber); previewTileRoot.setAttribute("data-tile-number", tileNumber);
if (this.framesheet.getCurrentFrame() == currentFrame) { if (this.framesheet.getCurrentFrame() == currentFrame) {
classname += " selected"; classname += " selected";
} }
previewTileRoot.className = classname; previewTileRoot.className = classname;
var canvasContainer = document.createElement("div"); var canvasContainer = document.createElement("div");
canvasContainer.className = "canvas-container"; canvasContainer.className = "canvas-container";
var canvasBackground = document.createElement("div"); var canvasBackground = document.createElement("div");
canvasBackground.className = "canvas-background"; canvasBackground.className = "canvas-background";
canvasContainer.appendChild(canvasBackground); canvasContainer.appendChild(canvasBackground);
previewTileRoot.addEventListener('click', this.onPreviewClick_.bind(this, tileNumber)); previewTileRoot.addEventListener('click', this.onPreviewClick_.bind(this, tileNumber));
var canvasPreviewDuplicateAction = document.createElement("button"); var canvasPreviewDuplicateAction = document.createElement("button");
canvasPreviewDuplicateAction.setAttribute('rel', 'tooltip'); canvasPreviewDuplicateAction.setAttribute('rel', 'tooltip');
canvasPreviewDuplicateAction.setAttribute('data-placement', 'right'); canvasPreviewDuplicateAction.setAttribute('data-placement', 'right');
canvasPreviewDuplicateAction.setAttribute('title', 'Duplicate this frame'); canvasPreviewDuplicateAction.setAttribute('title', 'Duplicate this frame');
canvasPreviewDuplicateAction.className = "tile-action duplicate-frame-action" canvasPreviewDuplicateAction.className = "tile-action duplicate-frame-action"
canvasPreviewDuplicateAction.addEventListener('click', this.onAddButtonClick_.bind(this, tileNumber)); canvasPreviewDuplicateAction.addEventListener('click', this.onAddButtonClick_.bind(this, tileNumber));
// TODO(vincz): Eventually optimize this part by not recreating a FrameRenderer. Note that the real optim // TODO(vincz): Eventually optimize this part by not recreating a FrameRenderer. Note that the real optim
// is to make this update function (#createPreviewTile) less aggressive. // is to make this update function (#createPreviewTile) less aggressive.
var renderingOptions = {"dpi": this.dpi }; var renderingOptions = {"dpi": this.dpi };
var currentFrameRenderer = new pskl.rendering.FrameRenderer($(canvasContainer), renderingOptions, "tile-view"); var currentFrameRenderer = new pskl.rendering.FrameRenderer($(canvasContainer), renderingOptions, "tile-view");
currentFrameRenderer.init(currentFrame); currentFrameRenderer.init(currentFrame);
previewTileRoot.appendChild(canvasContainer); previewTileRoot.appendChild(canvasContainer);
previewTileRoot.appendChild(canvasPreviewDuplicateAction); previewTileRoot.appendChild(canvasPreviewDuplicateAction);
if(tileNumber > 0 || this.framesheet.getFrameCount() > 1) { if(tileNumber > 0 || this.framesheet.getFrameCount() > 1) {
var canvasPreviewDeleteAction = document.createElement("button"); var canvasPreviewDeleteAction = document.createElement("button");
canvasPreviewDeleteAction.setAttribute('rel', 'tooltip'); canvasPreviewDeleteAction.setAttribute('rel', 'tooltip');
canvasPreviewDeleteAction.setAttribute('data-placement', 'right'); canvasPreviewDeleteAction.setAttribute('data-placement', 'right');
canvasPreviewDeleteAction.setAttribute('title', 'Delete this frame'); canvasPreviewDeleteAction.setAttribute('title', 'Delete this frame');
canvasPreviewDeleteAction.className = "tile-action delete-frame-action" canvasPreviewDeleteAction.className = "tile-action delete-frame-action"
canvasPreviewDeleteAction.addEventListener('click', this.onDeleteButtonClick_.bind(this, tileNumber)); canvasPreviewDeleteAction.addEventListener('click', this.onDeleteButtonClick_.bind(this, tileNumber));
previewTileRoot.appendChild(canvasPreviewDeleteAction); previewTileRoot.appendChild(canvasPreviewDeleteAction);
} }
return previewTileRoot; return previewTileRoot;
}; };
ns.PreviewFilmController.prototype.onPreviewClick_ = function (index, evt) { ns.PreviewFilmController.prototype.onPreviewClick_ = function (index, evt) {
// has not class tile-action: // has not class tile-action:
if(!evt.target.classList.contains('tile-action')) { if(!evt.target.classList.contains('tile-action')) {
this.framesheet.setCurrentFrameIndex(index); this.framesheet.setCurrentFrameIndex(index);
} }
}; };
ns.PreviewFilmController.prototype.onDeleteButtonClick_ = function (index, evt) { ns.PreviewFilmController.prototype.onDeleteButtonClick_ = function (index, evt) {
this.framesheet.removeFrameByIndex(index); this.framesheet.removeFrameByIndex(index);
$.publish(Events.LOCALSTORAGE_REQUEST); // Should come from model $.publish(Events.LOCALSTORAGE_REQUEST); // Should come from model
}; };
ns.PreviewFilmController.prototype.onAddButtonClick_ = function (index, evt) { ns.PreviewFilmController.prototype.onAddButtonClick_ = function (index, evt) {
this.framesheet.duplicateFrameByIndex(index); this.framesheet.duplicateFrameByIndex(index);
$.publish(Events.LOCALSTORAGE_REQUEST); // Should come from model $.publish(Events.LOCALSTORAGE_REQUEST); // Should come from model
this.framesheet.setCurrentFrameIndex(index + 1); this.framesheet.setCurrentFrameIndex(index + 1);
};
/**
* Calculate the preview DPI depending on the framesheet size
*/
ns.PreviewFilmController.prototype.calculateDPI_ = function () {
var previewSize = 128,
framePixelHeight = this.framesheet.getCurrentFrame().getHeight(),
framePixelWidth = this.framesheet.getCurrentFrame().getWidth();
// TODO (julz) : should have a utility to get a Size from framesheet easily (what about empty framesheets though ?)
return pskl.PixelUtils.calculateDPI(previewSize, previewSize, framePixelHeight, framePixelWidth);
}; };
})(); })();

View File

@ -1,6 +1,6 @@
(function () { (function () {
var ns = $.namespace("pskl.model"); var ns = $.namespace("pskl.model");
ns.FrameSheet = function (width, height) { ns.FrameSheet = function (height, width) {
this.width = width; this.width = width;
this.height = height; this.height = height;
this.frames = []; this.frames = [];
@ -68,6 +68,14 @@
var frameCfg = frameConfigurations[i]; var frameCfg = frameConfigurations[i];
this.addFrame(new ns.Frame(frameCfg)); this.addFrame(new ns.Frame(frameCfg));
} }
if (this.hasFrameAtIndex(0)) {
this.height = this.getFrameByIndex(0).getHeight();
this.width = this.getFrameByIndex(0).getWidth();
this.setCurrentFrameIndex(0);
$.publish(Events.FRAME_SIZE_CHANGED);
}
$.publish(Events.FRAMESHEET_RESET); $.publish(Events.FRAMESHEET_RESET);
} catch (e) { } catch (e) {
throw "Could not load serialized framesheet : " + e.message throw "Could not load serialized framesheet : " + e.message

View File

@ -9,46 +9,21 @@ $.namespace("pskl");
/** /**
* FrameSheetModel instance. * FrameSheetModel instance.
*/ */
var frameSheet, var frameSheet;
// Configuration:
// Canvas size in pixel size (not dpi related)
framePixelWidth = 32,
framePixelHeight = 32,
// Scaling factors for a given frameSheet rendering:
// Main drawing area dpi is calculated dynamically
// Canvas preview film canvases:
previewTileCanvasDpi = 4,
// Animated canvas preview:
previewAnimationCanvasDpi = 8;
/** /**
* Main application controller * Main application controller
*/ */
var piskel = { var piskel = {
init : function () { init : function () {
frameSheet = new pskl.model.FrameSheet(framePixelWidth, framePixelHeight); var frameSize = this.readSizeFromURL_();
frameSheet = new pskl.model.FrameSheet(frameSize.height, frameSize.width);
frameSheet.addEmptyFrame(); frameSheet.addEmptyFrame();
this.drawingController = new pskl.controller.DrawingController( this.drawingController = new pskl.controller.DrawingController(frameSheet, $('#drawing-canvas-container'));
frameSheet, this.animationController = new pskl.controller.AnimatedPreviewController(frameSheet, $('#preview-canvas-container'));
$('#drawing-canvas-container'), this.previewsController = new pskl.controller.PreviewFilmController(frameSheet, $('#preview-list'));
this.calculateDPIsForDrawingCanvas_()
);
this.animationController = new pskl.controller.AnimatedPreviewController(
frameSheet,
$('#preview-canvas-container'),
previewAnimationCanvasDpi
);
this.previewsController = new pskl.controller.PreviewFilmController(
frameSheet,
$('#preview-list'),
previewTileCanvasDpi
);
// To catch the current active frame, the selection manager have to be initialized before // To catch the current active frame, the selection manager have to be initialized before
// the 'frameSheet.setCurrentFrameIndex(0);' line below. // the 'frameSheet.setCurrentFrameIndex(0);' line below.
@ -57,8 +32,7 @@ $.namespace("pskl");
// - an event(s) triggering init // - an event(s) triggering init
// All listeners will be hook in a first step, then all event triggering inits will be called // All listeners will be hook in a first step, then all event triggering inits will be called
// in a second batch. // in a second batch.
this.selectionManager = this.selectionManager = new pskl.selection.SelectionManager(frameSheet, this.drawingController.overlayFrame);
new pskl.selection.SelectionManager(frameSheet, this.drawingController.overlayFrame);
// DO NOT MOVE THIS LINE (see comment above) // DO NOT MOVE THIS LINE (see comment above)
frameSheet.setCurrentFrameIndex(0); frameSheet.setCurrentFrameIndex(0);
@ -79,7 +53,7 @@ $.namespace("pskl");
this.localStorageService.init(); this.localStorageService.init();
// TODO: Add comments // TODO: Add comments
var framesheetId = this.getFramesheetIdFromUrl(); var framesheetId = this.readFramesheetIdFromURL_();
if (framesheetId) { if (framesheetId) {
$.publish(Events.SHOW_NOTIFICATION, [{"content": "Loading animation with id : [" + framesheetId + "]"}]); $.publish(Events.SHOW_NOTIFICATION, [{"content": "Loading animation with id : [" + framesheetId + "]"}]);
this.loadFramesheetFromService(framesheetId); this.loadFramesheetFromService(framesheetId);
@ -96,64 +70,13 @@ $.namespace("pskl");
$('body').tooltip({ $('body').tooltip({
selector: '[rel=tooltip]' selector: '[rel=tooltip]'
}); });
this.connectResizeToDpiUpdate_();
}, },
render : function (delta) { render : function (delta) {
this.drawingController.render(delta); this.drawingController.render(delta);
this.animationController.render(delta); this.animationController.render(delta);
this.previewsController.render(delta); this.previewsController.render(delta);
}, },
connectResizeToDpiUpdate_ : function () {
$(window).resize($.proxy(this.startDPIUpdateTimer_, this));
},
startDPIUpdateTimer_ : function () {
if (this.dpiUpdateTimer) window.clearInterval(this.dpiUpdateTimer);
this.dpiUpdateTimer = window.setTimeout($.proxy(this.updateDPIForViewport, this), 200);
},
updateDPIForViewport : function () {
var dpi = piskel.calculateDPIsForDrawingCanvas_();
this.drawingController.updateDPI(dpi);
},
/**
* @private
*/
calculateDPIsForDrawingCanvas_ : function() {
var userMessageGap = 80; // Reserve some height to show the user message at the bottom
var availableViewportHeight = $('.main-panel').height() - userMessageGap,
availableViewportWidth = $('.main-panel').width(),
previewHeight = $(".preview-container").height(),
previewWidth = $(".preview-container").width();
var heightBoundDpi = Math.floor(availableViewportHeight / framePixelHeight),
widthBoundDpi = Math.floor(availableViewportWidth / framePixelWidth);
var dpi = Math.min(heightBoundDpi, widthBoundDpi);
var drawingCanvasHeight = dpi * framePixelHeight;
var drawingCanvasWidth = dpi * framePixelWidth;
// Check if preview and drawing canvas overlap
var heightGap = drawingCanvasHeight + previewHeight - availableViewportHeight,
widthGap = drawingCanvasWidth + previewWidth - availableViewportWidth;
if (heightGap > 0 && widthGap > 0) {
// Calculate the DPI change needed to bridge height and width gap
var heightGapDpi = Math.ceil(heightGap / framePixelHeight),
widthGapDpi = Math.ceil(widthGap / framePixelWidth);
// substract smallest dpi change to initial dpi
dpi -= Math.min(heightGapDpi, widthGapDpi);
}
return dpi;
},
finishInit : function () { finishInit : function () {
var toolController = new pskl.controller.ToolController(); var toolController = new pskl.controller.ToolController();
@ -163,12 +86,39 @@ $.namespace("pskl");
paletteController.init(frameSheet); paletteController.init(frameSheet);
}, },
getFramesheetIdFromUrl : function() { readSizeFromURL_ : function () {
var href = window.location.href; var sizeParam = this.readUrlParameter_("size"), size;
// TODO: Change frameId to framesheetId on the backend // parameter expected as size=64x48 => size=widthxheight
if (href.indexOf('frameId=') != -1) { var parts = sizeParam.split("x");
return href.substring(href.indexOf('frameId=')+8); if (parts && parts.length == 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
var width = parseInt(parts[0], 10),
height = parseInt(parts[1], 10);
size = {
height : Math.min(height, Constants.MAX_HEIGHT),
width : Math.min(width, Constants.MAX_WIDTH)
};
} else {
size = Constants.DEFAULT_SIZE;
} }
return size;
},
readFramesheetIdFromURL_ : function() {
return this.readUrlParameter_("frameId");
},
readUrlParameter_ : function (paramName) {
var searchString = window.location.search.substring(1),
i, val, params = searchString.split("&");
for (i=0;i<params.length;i++) {
val = params[i].split("=");
if (val[0] == paramName) {
return unescape(val[1]);
}
}
return "";
}, },
loadFramesheetFromService : function (frameId) { loadFramesheetFromService : function (frameId) {

View File

@ -22,8 +22,6 @@
this.canvas = null; this.canvas = null;
this.hasGrid = renderingOptions.hasGrid; this.hasGrid = renderingOptions.hasGrid;
this.gridStrokeWidth = 0; this.gridStrokeWidth = 0;
this.lastRenderedFrame = null;
// Flag to know if the config was altered // Flag to know if the config was altered
this.canvasConfigDirty = true; this.canvasConfigDirty = true;
@ -51,10 +49,6 @@
} }
this.canvasConfigDirty = true; this.canvasConfigDirty = true;
if(this.lastRenderedFrame) {
this.render(this.lastRenderedFrame);
}
}; };
ns.FrameRenderer.prototype.render = function (frame) { ns.FrameRenderer.prototype.render = function (frame) {

View File

@ -30,8 +30,8 @@
*/ */
ns.LocalStorageService.prototype.persistToLocalStorage_ = function() { ns.LocalStorageService.prototype.persistToLocalStorage_ = function() {
console.log('[LocalStorage service]: Snapshot stored'); // console.log('[LocalStorage service]: Snapshot stored');
window.localStorage.snapShot = this.framesheet.serialize(); // window.localStorage.snapShot = this.framesheet.serialize();
}; };
/** /**

View File

@ -1,148 +1,174 @@
(function () { (function () {
var ns = $.namespace("pskl"); var ns = $.namespace("pskl");
ns.PixelUtils = { ns.PixelUtils = {
getRectanglePixels : function (x0, y0, x1, y1) { getRectanglePixels : function (x0, y0, x1, y1) {
var rectangle = this.getOrderedRectangleCoordinates(x0, y0, x1, y1); var rectangle = this.getOrderedRectangleCoordinates(x0, y0, x1, y1);
var pixels = []; var pixels = [];
for(var x = rectangle.x0; x <= rectangle.x1; x++) { for(var x = rectangle.x0; x <= rectangle.x1; x++) {
for(var y = rectangle.y0; y <= rectangle.y1; y++) { for(var y = rectangle.y0; y <= rectangle.y1; y++) {
pixels.push({"col": x, "row": y}); pixels.push({"col": x, "row": y});
} }
} }
return pixels; return pixels;
}, },
getBoundRectanglePixels : function (x0, y0, x1, y1) { getBoundRectanglePixels : function (x0, y0, x1, y1) {
var rectangle = this.getOrderedRectangleCoordinates(x0, y0, x1, y1); var rectangle = this.getOrderedRectangleCoordinates(x0, y0, x1, y1);
var pixels = []; var pixels = [];
// Creating horizontal sides of the rectangle: // Creating horizontal sides of the rectangle:
for(var x = rectangle.x0; x <= rectangle.x1; x++) { for(var x = rectangle.x0; x <= rectangle.x1; x++) {
pixels.push({"col": x, "row": rectangle.y0}); pixels.push({"col": x, "row": rectangle.y0});
pixels.push({"col": x, "row": rectangle.y1}); pixels.push({"col": x, "row": rectangle.y1});
} }
// Creating vertical sides of the rectangle: // Creating vertical sides of the rectangle:
for(var y = rectangle.y0; y <= rectangle.y1; y++) { for(var y = rectangle.y0; y <= rectangle.y1; y++) {
pixels.push({"col": rectangle.x0, "row": y}); pixels.push({"col": rectangle.x0, "row": y});
pixels.push({"col": rectangle.x1, "row": y}); pixels.push({"col": rectangle.x1, "row": y});
} }
return pixels; return pixels;
}, },
/** /**
* Return an object of ordered rectangle coordinate. * Return an object of ordered rectangle coordinate.
* In returned object {x0, y0} => top left corner - {x1, y1} => bottom right corner * In returned object {x0, y0} => top left corner - {x1, y1} => bottom right corner
* @private * @private
*/ */
getOrderedRectangleCoordinates : function (x0, y0, x1, y1) { getOrderedRectangleCoordinates : function (x0, y0, x1, y1) {
return { return {
x0 : Math.min(x0, x1), y0 : Math.min(y0, y1), x0 : Math.min(x0, x1), y0 : Math.min(y0, y1),
x1 : Math.max(x0, x1), y1 : Math.max(y0, y1), x1 : Math.max(x0, x1), y1 : Math.max(y0, y1),
}; };
}, },
/** /**
* Return the list of pixels that would have been filled by a paintbucket tool applied * Return the list of pixels that would have been filled by a paintbucket tool applied
* on pixel at coordinate (x,y). * on pixel at coordinate (x,y).
* This function is not altering the Frame object argument. * This function is not altering the Frame object argument.
* *
* @param frame pskl.model.Frame The frame target in which we want to paintbucket * @param frame pskl.model.Frame The frame target in which we want to paintbucket
* @param col number Column coordinate in the frame * @param col number Column coordinate in the frame
* @param row number Row coordinate in the frame * @param row number Row coordinate in the frame
* *
* @return an array of the pixel coordinates paint with the replacement color * @return an array of the pixel coordinates paint with the replacement color
*/ */
getSimilarConnectedPixelsFromFrame: function(frame, col, row) { getSimilarConnectedPixelsFromFrame: function(frame, col, row) {
// To get the list of connected (eg the same color) pixels, we will use the paintbucket algorithm // To get the list of connected (eg the same color) pixels, we will use the paintbucket algorithm
// in a fake cloned frame. The returned pixels by the paintbucket algo are the painted pixels // in a fake cloned frame. The returned pixels by the paintbucket algo are the painted pixels
// and are as well connected. // and are as well connected.
var fakeFrame = frame.clone(); // We just want to var fakeFrame = frame.clone(); // We just want to
var fakeFillColor = "sdfsdfsdf"; // A fake color that will never match a real color. var fakeFillColor = "sdfsdfsdf"; // A fake color that will never match a real color.
var paintedPixels = this.paintSimilarConnectedPixelsFromFrame(fakeFrame, col, row, fakeFillColor); var paintedPixels = this.paintSimilarConnectedPixelsFromFrame(fakeFrame, col, row, fakeFillColor);
return paintedPixels; return paintedPixels;
}, },
/** /**
* Apply the paintbucket tool in a frame at the (col, row) initial position * Apply the paintbucket tool in a frame at the (col, row) initial position
* with the replacement color. * with the replacement color.
* *
* @param frame pskl.model.Frame The frame target in which we want to paintbucket * @param frame pskl.model.Frame The frame target in which we want to paintbucket
* @param col number Column coordinate in the frame * @param col number Column coordinate in the frame
* @param row number Row coordinate in the frame * @param row number Row coordinate in the frame
* @param replacementColor string Hexadecimal color used to fill the area * @param replacementColor string Hexadecimal color used to fill the area
* *
* @return an array of the pixel coordinates paint with the replacement color * @return an array of the pixel coordinates paint with the replacement color
*/ */
paintSimilarConnectedPixelsFromFrame: function(frame, col, row, replacementColor) { paintSimilarConnectedPixelsFromFrame: function(frame, col, row, replacementColor) {
/** /**
* Queue linear Flood-fill (node, target-color, replacement-color): * Queue linear Flood-fill (node, target-color, replacement-color):
* 1. Set Q to the empty queue. * 1. Set Q to the empty queue.
* 2. If the color of node is not equal to target-color, return. * 2. If the color of node is not equal to target-color, return.
* 3. Add node to Q. * 3. Add node to Q.
* 4. For each element n of Q: * 4. For each element n of Q:
* 5. If the color of n is equal to target-color: * 5. If the color of n is equal to target-color:
* 6. Set w and e equal to n. * 6. Set w and e equal to n.
* 7. Move w to the west until the color of the node to the west of w no longer matches target-color. * 7. Move w to the west until the color of the node to the west of w no longer matches target-color.
* 8. Move e to the east until the color of the node to the east of e no longer matches target-color. * 8. Move e to the east until the color of the node to the east of e no longer matches target-color.
* 9. Set the color of nodes between w and e to replacement-color. * 9. Set the color of nodes between w and e to replacement-color.
* 10. For each node n between w and e: * 10. For each node n between w and e:
* 11. If the color of the node to the north of n is target-color, add that node to Q. * 11. If the color of the node to the north of n is target-color, add that node to Q.
* 12. If the color of the node to the south of n is target-color, add that node to Q. * 12. If the color of the node to the south of n is target-color, add that node to Q.
* 13. Continue looping until Q is exhausted. * 13. Continue looping until Q is exhausted.
* 14. Return. * 14. Return.
* */
* @private var paintedPixels = [];
*/ var queue = [];
var paintedPixels = []; var dy = [-1, 0, 1, 0];
var queue = []; var dx = [0, 1, 0, -1];
var dy = [-1, 0, 1, 0]; try {
var dx = [0, 1, 0, -1]; var targetColor = frame.getPixel(col, row);
try { } catch(e) {
var targetColor = frame.getPixel(col, row); // Frame out of bound exception.
} catch(e) { }
// Frame out of bound exception.
} if(targetColor == replacementColor) {
return;
if(targetColor == replacementColor) { }
return;
}
queue.push({"col": col, "row": row}); queue.push({"col": col, "row": row});
var loopCount = 0; var loopCount = 0;
var cellCount = frame.getWidth() * frame.getHeight(); var cellCount = frame.getWidth() * frame.getHeight();
while(queue.length > 0) { while(queue.length > 0) {
loopCount ++; loopCount ++;
var currentItem = queue.pop(); var currentItem = queue.pop();
frame.setPixel(currentItem.col, currentItem.row, replacementColor); frame.setPixel(currentItem.col, currentItem.row, replacementColor);
paintedPixels.push({"col": currentItem.col, "row": currentItem.row }); paintedPixels.push({"col": currentItem.col, "row": currentItem.row });
for (var i = 0; i < 4; i++) { for (var i = 0; i < 4; i++) {
var nextCol = currentItem.col + dx[i] var nextCol = currentItem.col + dx[i]
var nextRow = currentItem.row + dy[i] var nextRow = currentItem.row + dy[i]
try { try {
if (frame.containsPixel(nextCol, nextRow) && frame.getPixel(nextCol, nextRow) == targetColor) { if (frame.containsPixel(nextCol, nextRow) && frame.getPixel(nextCol, nextRow) == targetColor) {
queue.push({"col": nextCol, "row": nextRow }); queue.push({"col": nextCol, "row": nextRow });
} }
} catch(e) { } catch(e) {
// Frame out of bound exception. // Frame out of bound exception.
} }
} }
// Security loop breaker: // Security loop breaker:
if(loopCount > 10 * cellCount) { if(loopCount > 10 * cellCount) {
console.log("loop breaker called") console.log("loop breaker called")
break; break;
} }
} }
return paintedPixels; return paintedPixels;
} },
};
/**
* Calculate and return the maximal DPI to display a picture in a given container.
*
* @param container jQueryObject Container where the picture should be displayed
* @param number pictureHeight height in pixels of the picture to display
* @param number pictureWidth width in pixels of the picture to display
* @return number maximal dpi
*/
calculateDPIForContainer : function (container, pictureHeight, pictureWidth) {
return this.calculateDPI(container.height(), container.width(), pictureHeight, pictureWidth);
},
/**
* Calculate and return the maximal DPI to display a picture for a given height and width.
*
* @param height number Height available to display the picture
* @param width number Width available to display the picture
* @param number pictureHeight height in pixels of the picture to display
* @param number pictureWidth width in pixels of the picture to display
* @return number maximal dpi
*/
calculateDPI : function (height, width, pictureHeight, pictureWidth) {
var heightBoundDpi = Math.floor(height / pictureHeight),
widthBoundDpi = Math.floor(width / pictureWidth);
return Math.min(heightBoundDpi, widthBoundDpi);
},
};
})(); })();