diff --git a/src/css/settings-export.css b/src/css/settings-export.css index 1dbf14e2..593584b1 100644 --- a/src/css/settings-export.css +++ b/src/css/settings-export.css @@ -151,7 +151,7 @@ border-radius: 2px; } -.export-panel-gif .export-panel-section { +.export-panel-row { display: flex; align-items: center; overflow: hidden; @@ -162,3 +162,19 @@ width: 75px; flex-shrink: 0; } + +.export-panel-png .textfield { + width: 50px; +} + +.png-export-dimension-info { + font-style: italic; + font-weight: normal; + margin-left: 5px; +} + +#png-export-columns { + /* Override default textfield padding-right to keep the number spinners + aligned to the right. */ + padding-right: 0; +} diff --git a/src/js/Events.js b/src/js/Events.js index c00f29c8..dc2b9e16 100644 --- a/src/js/Events.js +++ b/src/js/Events.js @@ -72,6 +72,7 @@ var Events = { HIDE_PROGRESS: 'HIDE_PROGRESS', ZOOM_CHANGED : 'ZOOM_CHANGED', + EXPORT_SCALE_CHANGED : 'EXPORT_SCALE_CHANGED', CURRENT_COLORS_UPDATED : 'CURRENT_COLORS_UPDATED', diff --git a/src/js/controller/settings/exportimage/ExportController.js b/src/js/controller/settings/exportimage/ExportController.js index 68b282c9..32342cd6 100644 --- a/src/js/controller/settings/exportimage/ExportController.js +++ b/src/js/controller/settings/exportimage/ExportController.js @@ -35,7 +35,7 @@ this.widthInput = document.querySelector('.export-resize .resize-width'); this.heightInput = document.querySelector('.export-resize .resize-height'); - var scale = pskl.UserSettings.get(pskl.UserSettings.EXPORT_SCALING); + var scale = pskl.UserSettings.get(pskl.UserSettings.EXPORT_SCALE); this.sizeInputWidget = new pskl.widgets.SizeInput({ widthInput : this.widthInput, heightInput : this.heightInput, @@ -94,7 +94,7 @@ if (Math.round(this.getExportZoom()) != value) { this.sizeInputWidget.setWidth(this.piskelController.getWidth() * value); } - pskl.UserSettings.set(pskl.UserSettings.EXPORT_SCALING, value); + pskl.UserSettings.set(pskl.UserSettings.EXPORT_SCALE, value); } }; @@ -111,6 +111,7 @@ } this.updateScaleText_(zoom); + $.publish(Events.EXPORT_SCALE_CHANGED); this.scaleInput.value = Math.round(zoom); if (zoom >= 1 && zoom <= 32) { diff --git a/src/js/controller/settings/exportimage/GifExportController.js b/src/js/controller/settings/exportimage/GifExportController.js index eb6be504..75088aee 100644 --- a/src/js/controller/settings/exportimage/GifExportController.js +++ b/src/js/controller/settings/exportimage/GifExportController.js @@ -24,10 +24,6 @@ this.addEventListener(this.downloadButton, 'click', this.onDownloadButtonClick_); }; - ns.GifExportController.prototype.destroy = function () { - this.superclass.destroy.call(this); - }; - ns.GifExportController.prototype.getZoom_ = function () { return this.exportController.getExportZoom(); }; diff --git a/src/js/controller/settings/exportimage/MiscExportController.js b/src/js/controller/settings/exportimage/MiscExportController.js index 1c299a2c..f639d51f 100644 --- a/src/js/controller/settings/exportimage/MiscExportController.js +++ b/src/js/controller/settings/exportimage/MiscExportController.js @@ -10,12 +10,11 @@ pskl.utils.inherit(ns.MiscExportController, pskl.controller.settings.AbstractSettingController); ns.MiscExportController.prototype.init = function () { - - var downloadButton = document.querySelector('.c-download-button'); - this.addEventListener(downloadButton, 'click', this.onCDownloadButtonClick_); + var cDownloadButton = document.querySelector('.c-download-button'); + this.addEventListener(cDownloadButton, 'click', this.onDownloadCFileClick_); }; - ns.MiscExportController.prototype.onCDownloadButtonClick_ = function (evt) { + ns.MiscExportController.prototype.onDownloadCFileClick_ = function (evt) { var fileName = this.getPiskelName_() + '.c'; var cName = this.getPiskelName_().replace(' ','_'); var width = this.piskelController.getWidth(); diff --git a/src/js/controller/settings/exportimage/PngExportController.js b/src/js/controller/settings/exportimage/PngExportController.js index cf1944eb..838426be 100644 --- a/src/js/controller/settings/exportimage/PngExportController.js +++ b/src/js/controller/settings/exportimage/PngExportController.js @@ -1,41 +1,149 @@ (function () { var ns = $.namespace('pskl.controller.settings.exportimage'); + var dimensionInfoPattern = '{{width}} x {{height}} px, {{frames}}
{{rows}}, {{columns}}.'; + + // Shortcut to pskl.utils.Template.replace + var replace = pskl.utils.Template.replace; + + // Helper to return "X items" or "1 item" if X is 1. + var pluralize = function (word, count) { + if (count === 1) { + return '1 ' + word; + } + return count + ' ' + word + 's'; + }; + + // Compute the nearest power of two for the provided number. + var getNearestPowerOfTwo = function (number) { + return Math.pow(2, Math.ceil(Math.log(number) / Math.log(2))); + }; + ns.PngExportController = function (piskelController, exportController) { this.piskelController = piskelController; this.exportController = exportController; + this.onScaleChanged_ = this.onScaleChanged_.bind(this); }; pskl.utils.inherit(ns.PngExportController, pskl.controller.settings.AbstractSettingController); ns.PngExportController.prototype.init = function () { + this.layoutContainer = document.querySelector('.png-export-layout-section'); + this.dimensionInfo = document.querySelector('.png-export-dimension-info'); + this.columnsInput = document.querySelector('#png-export-columns'); + this.powerTwo = document.querySelector('#png-export-power-two'); var downloadButton = document.querySelector('.png-download-button'); - this.addEventListener(downloadButton, 'click', this.onPngDownloadButtonClick_); + + this.initLayoutSection_(); + this.updateDimensionLabel_(); + + this.addEventListener(downloadButton, 'click', this.onDownloadClick_); + this.addEventListener(this.columnsInput, 'input', this.onColumnsChanged_); + this.addEventListener(this.powerTwo, 'change', this.onPowerTwoChanged_); + $.subscribe(Events.EXPORT_SCALE_CHANGED, this.onScaleChanged_); }; - ns.PngExportController.prototype.onPngDownloadButtonClick_ = function (evt) { - var fileName = this.getPiskelName_() + '.png'; + ns.PngExportController.prototype.destroy = function () { + $.unsubscribe(Events.EXPORT_SCALE_CHANGED, this.onScaleChanged_); + this.superclass.destroy.call(this); + }; - var outputCanvas = this.getFramesheetAsCanvas(); + /** + * Initalize all controls related to the spritesheet layout. + */ + ns.PngExportController.prototype.initLayoutSection_ = function () { + var frames = this.piskelController.getFrameCount(); + if (frames === 1) { + // Hide the layout section if only one frame is defined. + this.layoutContainer.style.display = 'none'; + } else { + this.columnsInput.value = this.getBestFit_(); + this.powerTwo.checked = pskl.UserSettings.get('EXPORT_PNG_POWER_TWO'); + } + }; + + ns.PngExportController.prototype.updateDimensionLabel_ = function () { + var zoom = this.exportController.getExportZoom(); + var frames = this.piskelController.getFrameCount(); + var width = this.piskelController.getWidth() * zoom; + var height = this.piskelController.getHeight() * zoom; + + var columns = this.getColumns_(); + var rows = Math.ceil(frames / columns); + width = columns * width; + height = rows * height; + + if (this.isPowerTwoEnabled_()) { + width = getNearestPowerOfTwo(width); + height = getNearestPowerOfTwo(height); + } + + this.dimensionInfo.innerHTML = replace(dimensionInfoPattern, { + width: width, + height: height, + rows: pluralize('row', rows), + columns: pluralize('column', columns), + frames: pluralize('frame', frames), + }); + }; + + ns.PngExportController.prototype.getColumns_ = function () { + return parseInt(this.columnsInput.value || 1, 10); + }; + + ns.PngExportController.prototype.getBestFit_ = function () { + var ratio = this.piskelController.getWidth() / this.piskelController.getHeight(); + var frameCount = this.piskelController.getFrameCount(); + var bestFit = Math.round(Math.sqrt(frameCount / ratio)); + + return Math.max(1, Math.min(bestFit, frameCount)); + }; + + ns.PngExportController.prototype.isPowerTwoEnabled_ = function () { + return pskl.UserSettings.get('EXPORT_PNG_POWER_TWO'); + }; + + ns.PngExportController.prototype.onScaleChanged_ = function () { + this.updateDimensionLabel_(); + }; + + ns.PngExportController.prototype.onColumnsChanged_ = function () { + if (this.getColumns_() > this.piskelController.getFrameCount()) { + this.columnsInput.value = this.piskelController.getFrameCount(); + } else if (this.getColumns_() < 1) { + this.columnsInput.value = 1; + } + this.updateDimensionLabel_(); + }; + + ns.PngExportController.prototype.onPowerTwoChanged_ = function () { + pskl.UserSettings.set('EXPORT_PNG_POWER_TWO', this.powerTwo.checked); + this.updateDimensionLabel_(); + }; + + ns.PngExportController.prototype.onDownloadClick_ = function (evt) { + var name = this.piskelController.getPiskel().getDescriptor().name; + var fileName = name + '.png'; + + var renderer = new pskl.rendering.PiskelRenderer(this.piskelController); + var outputCanvas = renderer.renderAsCanvas(this.getColumns_()); + var width = outputCanvas.width; + var height = outputCanvas.height; var zoom = this.exportController.getExportZoom(); if (zoom != 1) { - var width = outputCanvas.width * zoom; - var height = outputCanvas.height * zoom; - outputCanvas = pskl.utils.ImageResizer.resize(outputCanvas, width, height, false); + outputCanvas = pskl.utils.ImageResizer.resize(outputCanvas, width * zoom, height * zoom, false); + } + + if (this.isPowerTwoEnabled_()) { + var paddingCanvas = pskl.utils.CanvasUtils.createCanvas( + getNearestPowerOfTwo(width * zoom), getNearestPowerOfTwo(height * zoom)); + paddingCanvas.getContext('2d').drawImage(outputCanvas, 0, 0); + outputCanvas = paddingCanvas; } pskl.utils.BlobUtils.canvasToBlob(outputCanvas, function(blob) { pskl.utils.FileUtils.downloadAsFile(blob, fileName); }); }; - - ns.PngExportController.prototype.getPiskelName_ = function () { - return this.piskelController.getPiskel().getDescriptor().name; - }; - - ns.PngExportController.prototype.getFramesheetAsCanvas = function () { - var renderer = new pskl.rendering.PiskelRenderer(this.piskelController); - return renderer.renderAsCanvas(); - }; })(); diff --git a/src/js/rendering/FramesheetRenderer.js b/src/js/rendering/FramesheetRenderer.js index d0694eaa..2e325e17 100644 --- a/src/js/rendering/FramesheetRenderer.js +++ b/src/js/rendering/FramesheetRenderer.js @@ -13,11 +13,17 @@ } }; - ns.FramesheetRenderer.prototype.renderAsCanvas = function () { - var canvas = this.createCanvas_(); + ns.FramesheetRenderer.prototype.renderAsCanvas = function (columns) { + columns = columns || this.frames.length; + var rows = Math.ceil(this.frames.length / columns); + + var canvas = this.createCanvas_(columns, rows); + for (var i = 0 ; i < this.frames.length ; i++) { var frame = this.frames[i]; - this.drawFrameInCanvas_(frame, canvas, i * frame.getWidth(), 0); + var posX = (i % columns) * frame.getWidth(); + var posY = Math.floor(i / columns) * frame.getHeight(); + this.drawFrameInCanvas_(frame, canvas, posX, posY); } return canvas; }; @@ -32,11 +38,10 @@ }); }; - ns.FramesheetRenderer.prototype.createCanvas_ = function () { + ns.FramesheetRenderer.prototype.createCanvas_ = function (columns, rows) { var sampleFrame = this.frames[0]; - var count = this.frames.length; - var width = count * sampleFrame.getWidth(); - var height = sampleFrame.getHeight(); + var width = columns * sampleFrame.getWidth(); + var height = rows * sampleFrame.getHeight(); return pskl.utils.CanvasUtils.createCanvas(width, height); }; })(); diff --git a/src/js/rendering/PiskelRenderer.js b/src/js/rendering/PiskelRenderer.js index 1cbfbe3c..79206c3c 100644 --- a/src/js/rendering/PiskelRenderer.js +++ b/src/js/rendering/PiskelRenderer.js @@ -11,11 +11,17 @@ this.frames = frames; }; - ns.PiskelRenderer.prototype.renderAsCanvas = function () { - var canvas = this.createCanvas_(); + ns.PiskelRenderer.prototype.renderAsCanvas = function (columns) { + columns = columns || this.frames.length; + var rows = Math.ceil(this.frames.length / columns); + + var canvas = this.createCanvas_(columns, rows); + for (var i = 0 ; i < this.frames.length ; i++) { var frame = this.frames[i]; - this.drawFrameInCanvas_(frame, canvas, i * this.piskelController.getWidth(), 0); + var posX = (i % columns) * this.piskelController.getWidth(); + var posY = Math.floor(i / columns) * this.piskelController.getHeight(); + this.drawFrameInCanvas_(frame, canvas, posX, posY); } return canvas; }; @@ -25,10 +31,9 @@ context.drawImage(frame, offsetWidth, offsetHeight, frame.width, frame.height); }; - ns.PiskelRenderer.prototype.createCanvas_ = function () { - var count = this.frames.length; - var width = count * this.piskelController.getWidth(); - var height = this.piskelController.getHeight(); + ns.PiskelRenderer.prototype.createCanvas_ = function (columns, rows) { + var width = columns * this.piskelController.getWidth(); + var height = rows * this.piskelController.getHeight(); return pskl.utils.CanvasUtils.createCanvas(width, height); }; })(); diff --git a/src/js/utils/UserSettings.js b/src/js/utils/UserSettings.js index bc056cf1..b05c495e 100644 --- a/src/js/utils/UserSettings.js +++ b/src/js/utils/UserSettings.js @@ -12,7 +12,8 @@ ONION_SKIN : 'ONION_SKIN', LAYER_PREVIEW : 'LAYER_PREVIEW', LAYER_OPACITY : 'LAYER_OPACITY', - EXPORT_SCALING: 'EXPORT_SCALING', + EXPORT_PNG_POWER_TWO: 'EXPORT_PNG_POWER_TWO', + EXPORT_SCALE: 'EXPORT_SCALE', EXPORT_TAB: 'EXPORT_TAB', PEN_SIZE : 'PEN_SIZE', RESIZE_SETTINGS: 'RESIZE_SETTINGS', @@ -30,8 +31,9 @@ 'ONION_SKIN' : false, 'LAYER_OPACITY' : 0.2, 'LAYER_PREVIEW' : true, - 'EXPORT_SCALING' : 1, - 'EXPORT_TAB' : 'png', + 'EXPORT_PNG_POWER_TWO' : false, + 'EXPORT_SCALE' : 1, + 'EXPORT_TAB' : 'gif', 'PEN_SIZE' : 1, 'RESIZE_SETTINGS': { maintainRatio : true, diff --git a/src/templates/settings/export.html b/src/templates/settings/export.html index 5feea30e..d16ea0da 100644 --- a/src/templates/settings/export.html +++ b/src/templates/settings/export.html @@ -15,8 +15,8 @@
-
PNG
GIF
+
PNG
Zip
Others
diff --git a/src/templates/settings/export/gif.html b/src/templates/settings/export/gif.html index 7162762c..73d7d566 100644 --- a/src/templates/settings/export/gif.html +++ b/src/templates/settings/export/gif.html @@ -3,11 +3,11 @@
Convert your sprite to an animated GIF. Opacity will not be preserved. Colors might be resampled.
-
+
Download as an animated GIF.
-
+
Upload as an animated GIF to a public URL.
diff --git a/src/templates/settings/export/png.html b/src/templates/settings/export/png.html index 39f176f0..7e4114d3 100644 --- a/src/templates/settings/export/png.html +++ b/src/templates/settings/export/png.html @@ -1,8 +1,22 @@ \ No newline at end of file