diff --git a/.gitignore b/.gitignore
index b54edf37..ac72e6ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,9 @@
# nodejs local installs
node_modules
+#build artifacts
+build
+
# sublime text stuff (the -project should actually be shared, but then we'd have to share the same disk location)
*.sublime-project
*.sublime-workspace
diff --git a/closure_compiled_binary.js b/closure_compiled_binary.js
new file mode 100644
index 00000000..382b2791
--- /dev/null
+++ b/closure_compiled_binary.js
@@ -0,0 +1,189 @@
+var Constants={DEFAULT:{HEIGHT:32,WIDTH:32,FPS:12},MODEL_VERSION:1,MAX_HEIGHT:128,MAX_WIDTH:128,PREVIEW_FILM_SIZE:120,DEFAULT_PEN_COLOR:"#000000",TRANSPARENT_COLOR:"TRANSPARENT",SELECTION_TRANSPARENT_COLOR:"rgba(255, 255, 255, 0.6)",TOOL_TARGET_HIGHLIGHT_COLOR:"rgba(255, 255, 255, 0.2)",STATIC:{URL:{SAVE:"http://3.piskel-app.appspot.com/store",GET:"http://3.piskel-app.appspot.com/get"}},APPENGINE:{URL:{SAVE:"save"}},IMAGE_SERVICE_UPLOAD_URL:"http://screenletstore.appspot.com/__/upload",IMAGE_SERVICE_GET_URL:"http://screenletstore.appspot.com/img/",
+GRID_STROKE_WIDTH:1,LEFT_BUTTON:"left_button_1",RIGHT_BUTTON:"right_button_2"};var Events={TOOL_SELECTED:"TOOL_SELECTED",TOOL_RELEASED:"TOOL_RELEASED",PRIMARY_COLOR_SELECTED:"PRIMARY_COLOR_SELECTED",PRIMARY_COLOR_UPDATED:"PRIMARY_COLOR_UPDATED",SECONDARY_COLOR_SELECTED:"SECONDARY_COLOR_SELECTED",SECONDARY_COLOR_UPDATED:"SECONDARY_COLOR_UPDATED",LOCALSTORAGE_REQUEST:"LOCALSTORAGE_REQUEST",CANVAS_RIGHT_CLICKED:"CANVAS_RIGHT_CLICKED",REFRESH:"REFRESH",REDRAW_PREVIEWFILM:"REDRAW_PREVIEWFILM",USER_SETTINGS_CHANGED:"USER_SETTINGS_CHANGED",CLOSE_SETTINGS_DRAWER:"CLOSE_SETTINGS_DRAWER",
+PISKEL_RESET:"PISKEL_RESET",FRAME_SIZE_CHANGED:"FRAME_SIZE_CHANGED",CURRENT_FRAME_SET:"CURRENT_FRAME_SET",SELECTION_CREATED:"SELECTION_CREATED",SELECTION_MOVE_REQUEST:"SELECTION_MOVE_REQUEST",SELECTION_DISMISSED:"SELECTION_DISMISSED",SHOW_NOTIFICATION:"SHOW_NOTIFICATION",HIDE_NOTIFICATION:"HIDE_NOTIFICATION",UNDO:"UNDO",REDO:"REDO",CUT:"CUT",COPY:"COPY",PASTE:"PASTE"};(function(){$.namespace("pskl").app={init:function(){var a=this.readSizeFromURL_(),d=new pskl.model.Piskel(a.width,a.height),b=new pskl.model.Layer("Layer 1"),a=new pskl.model.Frame(a.width,a.height);b.addFrame(a);d.addLayer(b);this.piskelController=new pskl.controller.PiskelController(d);this.drawingController=new pskl.controller.DrawingController(this.piskelController,$("#drawing-canvas-container"));this.drawingController.init();this.animationController=new pskl.controller.AnimatedPreviewController(this.piskelController,
+$("#preview-canvas-container"));this.animationController.init();this.previewsController=new pskl.controller.PreviewFilmController(this.piskelController,$("#preview-list"));this.previewsController.init();this.layersListController=new pskl.controller.LayersListController(this.piskelController);this.layersListController.init();this.settingsController=new pskl.controller.settings.SettingsController(this.piskelController);this.settingsController.init();this.selectionManager=new pskl.selection.SelectionManager(this.piskelController);
+this.selectionManager.init();this.historyService=new pskl.service.HistoryService(this.piskelController);this.historyService.init();this.keyboardEventService=new pskl.service.KeyboardEventService;this.keyboardEventService.init();this.notificationController=new pskl.controller.NotificationController;this.notificationController.init();this.localStorageService=new pskl.service.LocalStorageService(this.piskelController);this.localStorageService.init();this.imageUploadService=new pskl.service.ImageUploadService;
+this.imageUploadService.init();this.toolController=new pskl.controller.ToolController;this.toolController.init();this.paletteController=new pskl.controller.PaletteController;this.paletteController.init();d=new pskl.rendering.DrawingLoop;d.addCallback(this.render,this);d.start();this.initBootstrapTooltips_();(this.isStaticVersion=!pskl.appEngineToken_)?this.finishInitStatic_():this.finishInitAppEngine_()},finishInitStatic_:function(){var a=this.readFramesheetIdFromURL_();a?($.publish(Events.SHOW_NOTIFICATION,
+[{content:"Loading animation with id : ["+a+"]"}]),this.loadFramesheetFromService(a)):this.localStorageService.displayRestoreNotification()},finishInitAppEngine_:function(){if(pskl.framesheetData_&&pskl.framesheetData_.content){var a=pskl.utils.Serializer.createPiskel(pskl.framesheetData_.content);pskl.app.piskelController.setPiskel(a);pskl.app.animationController.setFPS(pskl.framesheetData_.fps)}},initBootstrapTooltips_:function(){$("body").tooltip({selector:"[rel=tooltip]"})},render:function(a){this.drawingController.render(a);
+this.animationController.render(a);this.previewsController.render(a)},readSizeFromURL_:function(){var a,d=this.readUrlParameter_("size").split("x");!d||2!=d.length||isNaN(d[0])||isNaN(d[1])?a={height:Constants.DEFAULT.HEIGHT,width:Constants.DEFAULT.WIDTH}:(a=parseInt(d[0],10),d=parseInt(d[1],10),a={height:Math.min(d,Constants.MAX_HEIGHT),width:Math.min(a,Constants.MAX_WIDTH)});return a},readFramesheetIdFromURL_:function(){return this.readUrlParameter_("frameId")},readUrlParameter_:function(a){var d,
+b,c=window.location.search.substring(1).split("&");for(d=0;d
Add new frame
"; + this.container.append(newFrameButton); + + $(newFrameButton).click(this.addFrame.bind(this)); + + var needDragndropBehavior = (frameCount > 1); + if(needDragndropBehavior) { + this.initDragndropBehavior_(); + } + this.updateScrollerOverflows(); + }; + + + /** + * @private + */ + ns.PreviewFilmController.prototype.initDragndropBehavior_ = function () { + + $("#preview-list").sortable({ + placeholder: "preview-tile-drop-proxy", + update: $.proxy(this.onUpdate_, this), + items: ".preview-tile" + }); + $("#preview-list").disableSelection(); + }; + + /** + * @private + */ + ns.PreviewFilmController.prototype.onUpdate_ = function( event, ui ) { + var originFrameId = parseInt(ui.item.data("tile-number"), 10); + var targetInsertionId = $('.preview-tile').index(ui.item); + + this.piskelController.moveFrame(originFrameId, targetInsertionId); + this.piskelController.setCurrentFrameIndex(targetInsertionId); + + // TODO(grosbouddha): move localstorage request to the model layer? + $.publish(Events.LOCALSTORAGE_REQUEST); + }; + + + /** + * @private + * TODO(vincz): clean this giant rendering function & remove listeners. + */ + ns.PreviewFilmController.prototype.createPreviewTile_ = function(tileNumber) { + var currentFrame = this.piskelController.getCurrentLayer().getFrameAt(tileNumber); + + var previewTileRoot = document.createElement("li"); + var classname = "preview-tile"; + previewTileRoot.setAttribute("data-tile-number", tileNumber); + + if (this.piskelController.getCurrentFrame() == currentFrame) { + classname += " selected"; + } + previewTileRoot.className = classname; + + var canvasContainer = document.createElement("div"); + canvasContainer.className = "canvas-container"; + + var canvasBackground = document.createElement("div"); + canvasBackground.className = "canvas-background"; + canvasContainer.appendChild(canvasBackground); + + previewTileRoot.addEventListener('click', this.onPreviewClick_.bind(this, tileNumber)); + + var cloneFrameButton = document.createElement("button"); + cloneFrameButton.setAttribute('rel', 'tooltip'); + cloneFrameButton.setAttribute('data-placement', 'right'); + cloneFrameButton.setAttribute('title', 'Duplicate this frame'); + cloneFrameButton.className = "tile-overlay duplicate-frame-action"; + previewTileRoot.appendChild(cloneFrameButton); + cloneFrameButton.addEventListener('click', this.onAddButtonClick_.bind(this, tileNumber)); + + // 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. + var renderingOptions = {"dpi": this.dpi }; + var currentFrameRenderer = new pskl.rendering.FrameRenderer($(canvasContainer), renderingOptions, ["tile-view"]); + currentFrameRenderer.render(currentFrame); + + previewTileRoot.appendChild(canvasContainer); + + if(tileNumber > 0 || this.piskelController.getFrameCount() > 1) { + // Add 'remove frame' button. + var deleteButton = document.createElement("button"); + deleteButton.setAttribute('rel', 'tooltip'); + deleteButton.setAttribute('data-placement', 'right'); + deleteButton.setAttribute('title', 'Delete this frame'); + deleteButton.className = "tile-overlay delete-frame-action"; + deleteButton.addEventListener('click', this.onDeleteButtonClick_.bind(this, tileNumber)); + previewTileRoot.appendChild(deleteButton); + + // Add 'dragndrop handle'. + var dndHandle = document.createElement("div"); + dndHandle.className = "tile-overlay dnd-action"; + previewTileRoot.appendChild(dndHandle); + } + var tileCount = document.createElement("div"); + tileCount.className = "tile-overlay tile-count"; + tileCount.innerHTML = tileNumber; + previewTileRoot.appendChild(tileCount); + + + return previewTileRoot; + }; + + ns.PreviewFilmController.prototype.onPreviewClick_ = function (index, evt) { + // has not class tile-action: + if(!evt.target.classList.contains('tile-overlay')) { + this.piskelController.setCurrentFrameIndex(index); + } + }; + + ns.PreviewFilmController.prototype.onDeleteButtonClick_ = function (index, evt) { + this.piskelController.removeFrameAt(index); + $.publish(Events.LOCALSTORAGE_REQUEST); // Should come from model + this.updateScrollerOverflows(); + }; + + ns.PreviewFilmController.prototype.onAddButtonClick_ = function (index, evt) { + this.piskelController.duplicateFrameAt(index); + $.publish(Events.LOCALSTORAGE_REQUEST); // Should come from model + this.piskelController.setCurrentFrameIndex(index + 1); + this.updateScrollerOverflows(); + }; + + /** + * Calculate the preview DPI depending on the piskel size + */ + ns.PreviewFilmController.prototype.calculateDPI_ = function () { + var curFrame = this.piskelController.getCurrentFrame(), + frameHeight = curFrame.getHeight(), + frameWidth = curFrame.getWidth(), + maxFrameDim = Math.max(frameWidth, frameHeight); + + var previewHeight = Constants.PREVIEW_FILM_SIZE * frameHeight / maxFrameDim; + var previewWidth = Constants.PREVIEW_FILM_SIZE * frameWidth / maxFrameDim; + + return pskl.PixelUtils.calculateDPI(previewHeight, previewWidth, frameHeight, frameWidth) || 1; + }; +})(); \ No newline at end of file diff --git a/preview/js/controller/ToolController.js b/preview/js/controller/ToolController.js new file mode 100644 index 00000000..4b38b2f9 --- /dev/null +++ b/preview/js/controller/ToolController.js @@ -0,0 +1,107 @@ +(function () { + var ns = $.namespace("pskl.controller"); + + + ns.ToolController = function () { + + this.toolInstances = { + "simplePen" : new pskl.drawingtools.SimplePen(), + "verticalMirrorPen" : new pskl.drawingtools.VerticalMirrorPen(), + "eraser" : new pskl.drawingtools.Eraser(), + "paintBucket" : new pskl.drawingtools.PaintBucket(), + "stroke" : new pskl.drawingtools.Stroke(), + "rectangle" : new pskl.drawingtools.Rectangle(), + "circle" : new pskl.drawingtools.Circle(), + "move" : new pskl.drawingtools.Move(), + "rectangleSelect" : new pskl.drawingtools.RectangleSelect(), + "shapeSelect" : new pskl.drawingtools.ShapeSelect(), + "colorPicker" : new pskl.drawingtools.ColorPicker() + }; + + this.currentSelectedTool = this.toolInstances.simplePen; + this.previousSelectedTool = this.toolInstances.simplePen; + }; + + /** + * @public + */ + ns.ToolController.prototype.init = function() { + this.createToolMarkup_(); + + // Initialize tool: + // Set SimplePen as default selected tool: + this.selectTool_(this.toolInstances.simplePen); + // Activate listener on tool panel: + $("#tool-section").click($.proxy(this.onToolIconClicked_, this)); + }; + + /** + * @private + */ + ns.ToolController.prototype.activateToolOnStage_ = function(tool) { + var stage = $("body"); + var previousSelectedToolClass = stage.data("selected-tool-class"); + if(previousSelectedToolClass) { + stage.removeClass(previousSelectedToolClass); + } + stage.addClass(tool.toolId); + stage.data("selected-tool-class", tool.toolId); + }; + + /** + * @private + */ + ns.ToolController.prototype.selectTool_ = function(tool) { + console.log("Selecting Tool:" , this.currentSelectedTool); + this.currentSelectedTool = tool; + this.activateToolOnStage_(this.currentSelectedTool); + $.publish(Events.TOOL_SELECTED, [tool]); + }; + + /** + * @private + */ + ns.ToolController.prototype.onToolIconClicked_ = function(evt) { + var target = $(evt.target); + var clickedTool = target.closest(".tool-icon"); + + if(clickedTool.length) { + var toolId = clickedTool.data().toolId; + var tool = this.getToolById_(toolId); + if (tool) { + this.selectTool_(tool); + + // Show tool as selected: + $('#tool-section .tool-icon.selected').removeClass('selected'); + clickedTool.addClass('selected'); + } + } + }; + + ns.ToolController.prototype.getToolById_ = function (toolId) { + for(var key in this.toolInstances) { + if (this.toolInstances[key].toolId == toolId) { + return this.toolInstances[key]; + } + } + return null; + }; + + /** + * @private + */ + ns.ToolController.prototype.createToolMarkup_ = function() { + var currentTool, toolMarkup = '', extraClass; + // TODO(vincz): Tools rendering order is not enforced by the data stucture (this.toolInstances), fix that. + for (var toolKey in this.toolInstances) { + currentTool = this.toolInstances[toolKey]; + extraClass = currentTool.toolId; + if (this.currentSelectedTool == currentTool) { + extraClass = extraClass + " selected"; + } + toolMarkup += ''; + } + $('#tools-container').html(toolMarkup); + }; +})(); \ No newline at end of file diff --git a/preview/js/controller/settings/ApplicationSettingsController.js b/preview/js/controller/settings/ApplicationSettingsController.js new file mode 100644 index 00000000..7329b596 --- /dev/null +++ b/preview/js/controller/settings/ApplicationSettingsController.js @@ -0,0 +1,38 @@ +(function () { + var ns = $.namespace("pskl.controller.settings"); + + ns.ApplicationSettingsController = function () {}; + + /** + * @public + */ + ns.ApplicationSettingsController.prototype.init = function() { + // Highlight selected background picker: + var backgroundClass = pskl.UserSettings.get(pskl.UserSettings.CANVAS_BACKGROUND); + $('#background-picker-wrapper') + .find('.background-picker[data-background-class=' + backgroundClass + ']') + .addClass('selected'); + + // Initial state for grid display: + var show_grid = pskl.UserSettings.get(pskl.UserSettings.SHOW_GRID); + $('#show-grid').prop('checked', show_grid); + + // Handle grid display changes: + $('#show-grid').change($.proxy(function(evt) { + var checked = $('#show-grid').prop('checked'); + pskl.UserSettings.set(pskl.UserSettings.SHOW_GRID, checked); + }, this)); + + // Handle canvas background changes: + $('#background-picker-wrapper').click(function(evt) { + var target = $(evt.target).closest('.background-picker'); + if (target.length) { + var backgroundClass = target.data('background-class'); + pskl.UserSettings.set(pskl.UserSettings.CANVAS_BACKGROUND, backgroundClass); + + $('.background-picker').removeClass('selected'); + target.addClass('selected'); + } + }); + }; +})(); \ No newline at end of file diff --git a/preview/js/controller/settings/GifExportController.js b/preview/js/controller/settings/GifExportController.js new file mode 100644 index 00000000..42e57d3d --- /dev/null +++ b/preview/js/controller/settings/GifExportController.js @@ -0,0 +1,129 @@ +(function () { + var ns = $.namespace("pskl.controller.settings"); + + ns.GifExportController = function (piskelController) { + this.piskelController = piskelController; + }; + + /** + * List of Resolutions applicable for Gif export + * @static + * @type {Array} array of Objects {dpi:{Number}, default:{Boolean}} + */ + ns.GifExportController.RESOLUTIONS = [ + { + 'dpi' : 1 + },{ + 'dpi' : 5 + },{ + 'dpi' : 10, + 'default' : true + },{ + 'dpi' : 20 + } + ]; + + ns.GifExportController.prototype.init = function () { + this.radioTemplate_ = pskl.utils.Template.get("gif-export-radio-template"); + + this.previewContainerEl = document.querySelectorAll(".gif-export-preview")[0]; + this.radioGroupEl = document.querySelectorAll(".gif-export-radio-group")[0]; + + this.uploadForm = $("[name=gif-export-upload-form]"); + this.uploadForm.submit(this.onUploadFormSubmit_.bind(this)); + + this.createRadioElements_(); + }; + + ns.GifExportController.prototype.onUploadFormSubmit_ = function (evt) { + evt.originalEvent.preventDefault(); + var selectedDpi = this.getSelectedDpi_(), + fps = this.piskelController.getFPS(), + dpi = selectedDpi; + + this.renderAsImageDataAnimatedGIF(dpi, fps, this.onGifRenderingCompleted_.bind(this)); + }; + + ns.GifExportController.prototype.onGifRenderingCompleted_ = function (imageData) { + this.updatePreview_(imageData); + this.previewContainerEl.classList.add("preview-upload-ongoing"); + pskl.app.imageUploadService.upload(imageData, this.onImageUploadCompleted_.bind(this)); + }; + + ns.GifExportController.prototype.onImageUploadCompleted_ = function (imageUrl) { + this.updatePreview_(imageUrl); + this.previewContainerEl.classList.remove("preview-upload-ongoing"); + }; + + ns.GifExportController.prototype.updatePreview_ = function (src) { + this.previewContainerEl.innerHTML = ""; + }; + + ns.GifExportController.prototype.getSelectedDpi_ = function () { + var radiosColl = this.uploadForm.get(0).querySelectorAll("[name=gif-dpi]"), + radios = Array.prototype.slice.call(radiosColl,0); + var selectedRadios = radios.filter(function(radio) {return !!radio.checked;}); + + if (selectedRadios.length == 1) { + return selectedRadios[0].value; + } else { + throw "Unexpected error when retrieving selected dpi"; + } + }; + + ns.GifExportController.prototype.createRadioElements_ = function () { + var resolutions = ns.GifExportController.RESOLUTIONS; + for (var i = 0 ; i < resolutions.length ; i++) { + var radio = this.createRadioForResolution_(resolutions[i]); + this.radioGroupEl.appendChild(radio); + } + }; + + ns.GifExportController.prototype.createRadioForResolution_ = function (resolution) { + var dpi = resolution.dpi; + var label = dpi*this.piskelController.getWidth() + "x" + dpi*this.piskelController.getHeight(); + var value = dpi; + + var radioHTML = pskl.utils.Template.replace(this.radioTemplate_, {value : value, label : label}); + var radioEl = pskl.utils.Template.createFromHTML(radioHTML); + + if (resolution['default']) { + var input = radioEl.getElementsByTagName("input")[0]; + input.setAttribute("checked", "checked"); + } + + return radioEl; + }; + + ns.GifExportController.prototype.blobToBase64_ = function(blob, cb) { + var reader = new FileReader(); + reader.onload = function() { + var dataUrl = reader.result; + cb(dataUrl); + }; + reader.readAsDataURL(blob); + }; + + ns.GifExportController.prototype.renderAsImageDataAnimatedGIF = function(dpi, fps, cb) { + var gif = new window.GIF({ + workers: 2, + quality: 10, + width: this.piskelController.getWidth()*dpi, + height: this.piskelController.getHeight()*dpi + }); + + for (var i = 0; i < this.piskelController.getFrameCount(); i++) { + var frame = this.piskelController.getFrameAt(i); + var renderer = new pskl.rendering.CanvasRenderer(frame, dpi); + gif.addFrame(renderer.render(), { + delay: 1000 / fps + }); + } + + gif.on('finished', function(blob) { + this.blobToBase64_(blob, cb); + }.bind(this)); + + gif.render(); + }; +})(); \ No newline at end of file diff --git a/preview/js/controller/settings/ImportController.js b/preview/js/controller/settings/ImportController.js new file mode 100644 index 00000000..f28166f9 --- /dev/null +++ b/preview/js/controller/settings/ImportController.js @@ -0,0 +1,168 @@ +(function () { + var ns = $.namespace('pskl.controller.settings'); + var DEFAULT_FILE_STATUS = 'No file selected ...'; + var PREVIEW_HEIGHT = 60; + + ns.ImportController = function (piskelController) { + this.piskelController = piskelController; + this.importedImage_ = null; + }; + + ns.ImportController.prototype.init = function () { + this.importForm = $("[name=import-form]"); + this.hiddenFileInput = $("[name=file-upload-input]"); + this.fileInputButton = $(".file-input-button"); + this.fileInputStatus = $(".file-input-status"); + this.fileInputStatus.html(DEFAULT_FILE_STATUS); + + this.importPreview = $(".import-section-preview"); + + this.resizeWidth = $("[name=resize-width]"); + this.resizeHeight = $("[name=resize-height]"); + this.smoothResize = $("[name=smooth-resize-checkbox]"); + this.submitButton = $("[name=import-submit]"); + + this.importForm.submit(this.onImportFormSubmit_.bind(this)); + this.hiddenFileInput.change(this.onFileUploadChange_.bind(this)); + this.fileInputButton.click(this.onFileInputClick_.bind(this)); + + this.resizeWidth.keyup(this.onResizeInputKeyUp_.bind(this, 'width')); + this.resizeHeight.keyup(this.onResizeInputKeyUp_.bind(this, 'height')); + }; + + ns.ImportController.prototype.reset_ = function () { + this.importForm.get(0).reset(); + this.fileInputStatus.html(DEFAULT_FILE_STATUS); + $.publish(Events.CLOSE_SETTINGS_DRAWER); + }; + + ns.ImportController.prototype.onResizeInputKeyUp_ = function (from, evt) { + if (this.importedImage_) { + this.synchronizeResizeFields_(evt.target.value, from); + } + }; + + ns.ImportController.prototype.synchronizeResizeFields_ = function (value, from) { + value = parseInt(value, 10); + if (isNaN(value)) { + value = 0; + } + var height = this.importedImage_.height, width = this.importedImage_.width; + if (from === 'width') { + this.resizeHeight.val(Math.round(value * height / width)); + } else { + this.resizeWidth.val(Math.round(value * width / height)); + } + }; + + ns.ImportController.prototype.onImportFormSubmit_ = function (evt) { + evt.originalEvent.preventDefault(); + this.importImageToPiskel_(); + }; + + ns.ImportController.prototype.onFileUploadChange_ = function (evt) { + this.importFromFile_(); + }; + + ns.ImportController.prototype.onFileInputClick_ = function (evt) { + this.hiddenFileInput.click(); + }; + + ns.ImportController.prototype.importFromFile_ = function () { + var files = this.hiddenFileInput.get(0).files; + if (files.length == 1) { + var file = files[0]; + if (this.isImage_(file)) { + this.readImageFile_(file); + this.enableDisabledSections_(); + } else { + this.reset_(); + throw "File is not an image : " + file.type; + } + } + }; + + ns.ImportController.prototype.enableDisabledSections_ = function () { + this.resizeWidth.removeAttr('disabled'); + this.resizeHeight.removeAttr('disabled'); + this.smoothResize.removeAttr('disabled'); + this.submitButton.removeAttr('disabled'); + + this.fileInputButton.removeClass('button-primary'); + this.fileInputButton.blur(); + + $('.import-section-disabled').removeClass('import-section-disabled'); + }; + + ns.ImportController.prototype.readImageFile_ = function (imageFile) { + pskl.utils.FileUtils.readFile(imageFile, this.processImageSource_.bind(this)); + }; + + /** + * Create an image from the given source (url or data-url), and onload forward to onImageLoaded + * TODO : should be a generic utility method, should take a callback + * @param {String} imageSource url or data-url, will be used as src for the image + */ + ns.ImportController.prototype.processImageSource_ = function (imageSource) { + this.importedImage_ = new Image(); + this.importedImage_.onload = this.onImageLoaded_.bind(this); + this.importedImage_.src = imageSource; + }; + + ns.ImportController.prototype.onImageLoaded_ = function (evt) { + var w = this.importedImage_.width, + h = this.importedImage_.height; + var filePath = this.hiddenFileInput.val(); + var fileName = this.extractFileNameFromPath_(filePath); + this.fileInputStatus.html(fileName); + + this.resizeWidth.val(w); + this.resizeHeight.val(h); + + this.importPreview.width("auto"); + this.importPreview.append(this.createImagePreview_()); + }; + + ns.ImportController.prototype.createImagePreview_ = function () { + var image = document.createElement('IMG'); + image.src = this.importedImage_.src; + image.setAttribute('height', PREVIEW_HEIGHT); + return image; + }; + + ns.ImportController.prototype.extractFileNameFromPath_ = function (path) { + var parts = []; + if (path.indexOf('/') !== -1) { + parts = path.split('/'); + } else if (path.indexOf('\\') !== -1) { + parts = path.split('\\'); + } else { + parts = [path]; + } + return parts[parts.length-1]; + }; + + ns.ImportController.prototype.importImageToPiskel_ = function () { + if (this.importedImage_) { + if (window.confirm("You are about to create a new Piskel, unsaved changes will be lost.")) { + var w = this.resizeWidth.val(), + h = this.resizeHeight.val(), + smoothing = !!this.smoothResize.prop('checked'); + + var image = pskl.utils.ImageResizer.resize(this.importedImage_, w, h, smoothing); + var frame = pskl.utils.FrameUtils.createFromImage(image); + + var piskel = pskl.utils.Serializer.createPiskel([frame]); + pskl.app.piskelController.setPiskel(piskel); + pskl.app.animationController.setFPS(Constants.DEFAULT.FPS); + + this.reset_(); + } + } + }; + + ns.ImportController.prototype.isImage_ = function (file) { + return file.type.indexOf('image') === 0; + }; + +})(); \ No newline at end of file diff --git a/preview/js/controller/settings/SettingsController.js b/preview/js/controller/settings/SettingsController.js new file mode 100644 index 00000000..708b3fa1 --- /dev/null +++ b/preview/js/controller/settings/SettingsController.js @@ -0,0 +1,76 @@ +(function () { + var ns = $.namespace("pskl.controller.settings"); + + var settings = { + 'user' : { + template : 'templates/settings/application.html', + controller : ns.ApplicationSettingsController + }, + 'gif' : { + template : 'templates/settings/export-gif.html', + controller : ns.GifExportController + }, + 'import' : { + template : 'templates/settings/import.html', + controller : ns.ImportController + } + }; + + var SEL_SETTING_CLS = 'has-expanded-drawer'; + var EXP_DRAWER_CLS = 'expanded'; + + ns.SettingsController = function (piskelController) { + this.piskelController = piskelController; + this.drawerContainer = document.getElementById("drawer-container"); + this.settingsContainer = $('[data-pskl-controller=settings]'); + this.expanded = false; + this.currentSetting = null; + }; + + /** + * @public + */ + ns.SettingsController.prototype.init = function() { + // Expand drawer when clicking 'Settings' tab. + $('[data-setting]').click(function(evt) { + var el = evt.originalEvent.currentTarget; + var setting = el.getAttribute("data-setting"); + if (this.currentSetting != setting) { + this.loadSetting(setting); + } else { + this.closeDrawer(); + } + }.bind(this)); + + $('body').click(function (evt) { + var isInSettingsContainer = $.contains(this.settingsContainer.get(0), evt.target); + if (this.expanded && !isInSettingsContainer) { + this.closeDrawer(); + } + }.bind(this)); + + $.subscribe(Events.CLOSE_SETTINGS_DRAWER, this.closeDrawer.bind(this)); + }; + + ns.SettingsController.prototype.loadSetting = function (setting) { + this.drawerContainer.innerHTML = pskl.utils.Template.get(settings[setting].template); + (new settings[setting].controller(this.piskelController)).init(); + + this.settingsContainer.addClass(EXP_DRAWER_CLS); + + $('.' + SEL_SETTING_CLS).removeClass(SEL_SETTING_CLS); + $('[data-setting='+setting+']').addClass(SEL_SETTING_CLS); + + this.expanded = true; + this.currentSetting = setting; + }; + + ns.SettingsController.prototype.closeDrawer = function () { + this.settingsContainer.removeClass(EXP_DRAWER_CLS); + $('.' + SEL_SETTING_CLS).removeClass(SEL_SETTING_CLS); + + this.expanded = false; + this.currentSetting = null; + }; + +})(); \ No newline at end of file diff --git a/preview/js/drawingtools/BaseTool.js b/preview/js/drawingtools/BaseTool.js new file mode 100644 index 00000000..8b482df2 --- /dev/null +++ b/preview/js/drawingtools/BaseTool.js @@ -0,0 +1,75 @@ +/** + * @provide pskl.drawingtools.BaseTool + * + * @require pskl.utils + */ +(function() { + var ns = $.namespace("pskl.drawingtools"); + + ns.BaseTool = function() {}; + + ns.BaseTool.prototype.applyToolAt = function(col, row, color, frame, overlay) {}; + + ns.BaseTool.prototype.moveToolAt = function(col, row, color, frame, overlay) {}; + + ns.BaseTool.prototype.moveUnactiveToolAt = function(col, row, color, frame, overlay) { + if (overlay.containsPixel(col, row)) { + if (!isNaN(this.highlightedPixelCol) && + !isNaN(this.highlightedPixelRow) && + (this.highlightedPixelRow != row || + this.highlightedPixelCol != col)) { + + // Clean the previously highlighted pixel: + overlay.clear(); + } + + // Show the current pixel targeted by the tool: + overlay.setPixel(col, row, Constants.TOOL_TARGET_HIGHLIGHT_COLOR); + + this.highlightedPixelCol = col; + this.highlightedPixelRow = row; + } + }; + + ns.BaseTool.prototype.releaseToolAt = function(col, row, color, frame, overlay) {}; + + /** + * Bresenham line algorihtm: Get an array of pixels from + * start and end coordinates. + * + * http://en.wikipedia.org/wiki/Bresenham's_line_algorithm + * http://stackoverflow.com/questions/4672279/bresenham-algorithm-in-javascript + * + * @private + */ + ns.BaseTool.prototype.getLinePixels_ = function(x0, x1, y0, y1) { + + var pixels = []; + var dx = Math.abs(x1-x0); + var dy = Math.abs(y1-y0); + var sx = (x0 < x1) ? 1 : -1; + var sy = (y0 < y1) ? 1 : -1; + var err = dx-dy; + + while(true){ + + // Do what you need to for this + pixels.push({"col": x0, "row": y0}); + + if ((x0==x1) && (y0==y1)) { + break; + } + + var e2 = 2*err; + if (e2>-dy){ + err -= dy; + x0 += sx; + } + if (e2 < dx) { + err += dx; + y0 += sy; + } + } + return pixels; + }; +})(); diff --git a/preview/js/drawingtools/Circle.js b/preview/js/drawingtools/Circle.js new file mode 100644 index 00000000..c5c0322e --- /dev/null +++ b/preview/js/drawingtools/Circle.js @@ -0,0 +1,85 @@ +/** + * @provide pskl.drawingtools.Circle + * + * @require pskl.utils + */ +(function() { + var ns = $.namespace("pskl.drawingtools"); + + ns.Circle = function() { + this.toolId = "tool-circle"; + this.helpText = "Circle tool"; + + // Circle's first point coordinates (set in applyToolAt) + this.startCol = null; + this.startRow = null; + }; + + pskl.utils.inherit(ns.Circle, ns.BaseTool); + + /** + * @override + */ + ns.Circle.prototype.applyToolAt = function(col, row, color, frame, overlay) { + this.startCol = col; + this.startRow = row; + + // Drawing the first point of the rectangle in the fake overlay canvas: + overlay.setPixel(col, row, color); + }; + + ns.Circle.prototype.moveToolAt = function(col, row, color, frame, overlay) { + overlay.clear(); + if(color == Constants.TRANSPARENT_COLOR) { + color = Constants.SELECTION_TRANSPARENT_COLOR; + } + + // draw in overlay + this.drawCircle_(col, row, color, overlay); + }; + + /** + * @override + */ + ns.Circle.prototype.releaseToolAt = function(col, row, color, frame, overlay) { + overlay.clear(); + if(frame.containsPixel(col, row)) { // cancel if outside of canvas + // draw in frame to finalize + this.drawCircle_(col, row, color, frame); + } + }; + + ns.Circle.prototype.drawCircle_ = function (col, row, color, targetFrame) { + var circlePoints = this.getCirclePixels_(this.startCol, this.startRow, col, row); + for(var i = 0; i< circlePoints.length; i++) { + // Change model: + targetFrame.setPixel(circlePoints[i].col, circlePoints[i].row, color); + } + }; + + ns.Circle.prototype.getCirclePixels_ = function (x0, y0, x1, y1) { + var coords = pskl.PixelUtils.getOrderedRectangleCoordinates(x0, y0, x1, y1); + var xC = (coords.x0 + coords.x1)/2; + var yC = (coords.y0 + coords.y1)/2; + + var rX = coords.x1 - xC; + var rY = coords.y1 - yC; + + var pixels = []; + var x, y, angle; + for (x = coords.x0 ; x < coords.x1 ; x++) { + angle = Math.acos((x - xC)/rX); + y = Math.round(rY * Math.sin(angle) + yC); + pixels.push({"col": x, "row": y}); + pixels.push({"col": 2*xC - x, "row": 2*yC - y}); + } + + for (y = coords.y0 ; y < coords.y1 ; y++) { + angle = Math.asin((y - yC)/rY); + x = Math.round(rX * Math.cos(angle) + xC); + pixels.push({"col": x, "row": y}); + pixels.push({"col": 2*xC - x, "row": 2*yC - y}); + } + return pixels; + }; +})(); diff --git a/preview/js/drawingtools/ColorPicker.js b/preview/js/drawingtools/ColorPicker.js new file mode 100644 index 00000000..8bbedf0d --- /dev/null +++ b/preview/js/drawingtools/ColorPicker.js @@ -0,0 +1,29 @@ +/** + * @provide pskl.drawingtools.ColorPicker + * + * @require pskl.utils + */ +(function() { + var ns = $.namespace("pskl.drawingtools"); + + ns.ColorPicker = function() { + this.toolId = "tool-colorpicker"; + this.helpText = "Color picker"; + }; + + pskl.utils.inherit(ns.ColorPicker, ns.BaseTool); + + /** + * @override + */ + ns.ColorPicker.prototype.applyToolAt = function(col, row, color, frame, overlay, context) { + if (frame.containsPixel(col, row)) { + var sampledColor = frame.getPixel(col, row); + if (context.button == Constants.LEFT_BUTTON) { + $.publish(Events.PRIMARY_COLOR_SELECTED, [sampledColor]); + } else if (context.button == Constants.RIGHT_BUTTON) { + $.publish(Events.SECONDARY_COLOR_SELECTED, [sampledColor]); + } + } + }; +})(); diff --git a/preview/js/drawingtools/Eraser.js b/preview/js/drawingtools/Eraser.js new file mode 100644 index 00000000..c66a4220 --- /dev/null +++ b/preview/js/drawingtools/Eraser.js @@ -0,0 +1,23 @@ +/** + * @provide pskl.drawingtools.Eraser + * + * @require Constants + * @require pskl.utils + */ +(function() { + var ns = $.namespace("pskl.drawingtools"); + + ns.Eraser = function() { + this.toolId = "tool-eraser"; + this.helpText = "Eraser tool"; + }; + + pskl.utils.inherit(ns.Eraser, ns.SimplePen); + + /** + * @override + */ + ns.Eraser.prototype.applyToolAt = function(col, row, color, frame, overlay) { + this.superclass.applyToolAt.call(this, col, row, Constants.TRANSPARENT_COLOR, frame, overlay); + }; +})(); \ No newline at end of file diff --git a/preview/js/drawingtools/Move.js b/preview/js/drawingtools/Move.js new file mode 100644 index 00000000..1155db21 --- /dev/null +++ b/preview/js/drawingtools/Move.js @@ -0,0 +1,54 @@ +/** + * @provide pskl.drawingtools.Move + * + * @require pskl.utils + */ +(function() { + var ns = $.namespace("pskl.drawingtools"); + + ns.Move = function() { + this.toolId = "tool-move"; + this.helpText = "Move tool"; + + // Stroke's first point coordinates (set in applyToolAt) + this.startCol = null; + this.startRow = null; + }; + + pskl.utils.inherit(ns.Move, ns.BaseTool); + + /** + * @override + */ + ns.Move.prototype.applyToolAt = function(col, row, color, frame, overlay) { + this.startCol = col; + this.startRow = row; + this.frameClone = frame.clone(); + }; + + ns.Move.prototype.moveToolAt = function(col, row, color, frame, overlay) { + var colDiff = col - this.startCol, rowDiff = row - this.startRow; + this.shiftFrame(colDiff, rowDiff, frame, this.frameClone); + }; + + ns.Move.prototype.shiftFrame = function (colDiff, rowDiff, frame, reference) { + var color; + for (var col = 0 ; col < frame.getWidth() ; col++) { + for (var row = 0 ; row < frame.getHeight() ; row++) { + if (reference.containsPixel(col - colDiff, row - rowDiff)) { + color = reference.getPixel(col - colDiff, row - rowDiff); + } else { + color = Constants.TRANSPARENT_COLOR; + } + frame.setPixel(col, row, color); + } + } + }; + + /** + * @override + */ + ns.Move.prototype.releaseToolAt = function(col, row, color, frame, overlay) { + this.moveToolAt(col, row, color, frame, overlay); + }; +})(); diff --git a/preview/js/drawingtools/PaintBucket.js b/preview/js/drawingtools/PaintBucket.js new file mode 100644 index 00000000..cb0ffa66 --- /dev/null +++ b/preview/js/drawingtools/PaintBucket.js @@ -0,0 +1,36 @@ +/** + * @provide pskl.drawingtools.PaintBucket + * + * @require pskl.utils + */ +(function() { + var ns = $.namespace("pskl.drawingtools"); + + ns.PaintBucket = function() { + this.toolId = "tool-paint-bucket"; + this.helpText = "Paint bucket tool"; + }; + + pskl.utils.inherit(ns.PaintBucket, ns.BaseTool); + + /** + * @override + */ + ns.PaintBucket.prototype.applyToolAt = function(col, row, color, frame, overlay) { + + pskl.PixelUtils.paintSimilarConnectedPixelsFromFrame(frame, col, row, color); + }; +})(); + + + + + + + + + + + + + diff --git a/preview/js/drawingtools/Rectangle.js b/preview/js/drawingtools/Rectangle.js new file mode 100644 index 00000000..dd2a57e5 --- /dev/null +++ b/preview/js/drawingtools/Rectangle.js @@ -0,0 +1,59 @@ +/** + * @provide pskl.drawingtools.Rectangle + * + * @require pskl.utils + */ +(function() { + var ns = $.namespace("pskl.drawingtools"); + + ns.Rectangle = function() { + this.toolId = "tool-rectangle"; + this.helpText = "Rectangle tool"; + + // Rectangle's first point coordinates (set in applyToolAt) + this.startCol = null; + this.startRow = null; + }; + + pskl.utils.inherit(ns.Rectangle, ns.BaseTool); + + /** + * @override + */ + ns.Rectangle.prototype.applyToolAt = function(col, row, color, frame, overlay) { + this.startCol = col; + this.startRow = row; + + // Drawing the first point of the rectangle in the fake overlay canvas: + overlay.setPixel(col, row, color); + }; + + ns.Rectangle.prototype.moveToolAt = function(col, row, color, frame, overlay) { + overlay.clear(); + if(color == Constants.TRANSPARENT_COLOR) { + color = Constants.SELECTION_TRANSPARENT_COLOR; + } + + // draw in overlay + this.drawRectangle_(col, row, color, overlay); + }; + + /** + * @override + */ + ns.Rectangle.prototype.releaseToolAt = function(col, row, color, frame, overlay) { + overlay.clear(); + if(frame.containsPixel(col, row)) { // cancel if outside of canvas + // draw in frame to finalize + this.drawRectangle_(col, row, color, frame); + } + }; + + ns.Rectangle.prototype.drawRectangle_ = function (col, row, color, targetFrame) { + var strokePoints = pskl.PixelUtils.getBoundRectanglePixels(this.startCol, this.startRow, col, row); + for(var i = 0; i< strokePoints.length; i++) { + // Change model: + targetFrame.setPixel(strokePoints[i].col, strokePoints[i].row, color); + } + }; +})(); diff --git a/preview/js/drawingtools/SimplePen.js b/preview/js/drawingtools/SimplePen.js new file mode 100644 index 00000000..a5dbbb0e --- /dev/null +++ b/preview/js/drawingtools/SimplePen.js @@ -0,0 +1,49 @@ +/** + * @provide pskl.drawingtools.SimplePen + * + * @require pskl.utils + */ +(function() { + var ns = $.namespace("pskl.drawingtools"); + + ns.SimplePen = function() { + this.toolId = "tool-pen"; + this.helpText = "Pen tool"; + + this.previousCol = null; + this.previousRow = null; + + }; + + pskl.utils.inherit(ns.SimplePen, ns.BaseTool); + + /** + * @override + */ + ns.SimplePen.prototype.applyToolAt = function(col, row, color, frame, overlay) { + if (frame.containsPixel(col, row)) { + frame.setPixel(col, row, color); + } + this.previousCol = col; + this.previousRow = row; + }; + + ns.SimplePen.prototype.moveToolAt = function(col, row, color, frame, overlay) { + if((Math.abs(col - this.previousCol) > 1) || (Math.abs(row - this.previousRow) > 1)) { + // The pen movement is too fast for the mousemove frequency, there is a gap between the + // current point and the previously drawn one. + // We fill the gap by calculating missing dots (simple linear interpolation) and draw them. + var interpolatedPixels = this.getLinePixels_(col, this.previousCol, row, this.previousRow); + for(var i=0, l=interpolatedPixels.length; it |