Issue #293: Add spritesheet layout options in PNG export tab

This commit is contained in:
Julian Descottes
2016-05-29 10:45:19 +02:00
parent 5a0d5da5f4
commit 169ce21556
11 changed files with 198 additions and 45 deletions

View File

@@ -151,7 +151,7 @@
border-radius: 2px; border-radius: 2px;
} }
.export-panel-gif .export-panel-section { .export-panel-row {
display: flex; display: flex;
align-items: center; align-items: center;
overflow: hidden; overflow: hidden;
@@ -162,3 +162,13 @@
width: 75px; width: 75px;
flex-shrink: 0; flex-shrink: 0;
} }
.export-panel-png .textfield {
width: 50px;
}
.png-export-dimension-info {
font-style: italic;
font-weight: normal;
margin-left: 5px;
}

View File

@@ -72,6 +72,7 @@ var Events = {
HIDE_PROGRESS: 'HIDE_PROGRESS', HIDE_PROGRESS: 'HIDE_PROGRESS',
ZOOM_CHANGED : 'ZOOM_CHANGED', ZOOM_CHANGED : 'ZOOM_CHANGED',
EXPORT_SCALE_CHANGED : 'EXPORT_SCALE_CHANGED',
CURRENT_COLORS_UPDATED : 'CURRENT_COLORS_UPDATED', CURRENT_COLORS_UPDATED : 'CURRENT_COLORS_UPDATED',

View File

@@ -35,7 +35,7 @@
this.widthInput = document.querySelector('.export-resize .resize-width'); this.widthInput = document.querySelector('.export-resize .resize-width');
this.heightInput = document.querySelector('.export-resize .resize-height'); 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({ this.sizeInputWidget = new pskl.widgets.SizeInput({
widthInput : this.widthInput, widthInput : this.widthInput,
heightInput : this.heightInput, heightInput : this.heightInput,
@@ -94,7 +94,7 @@
if (Math.round(this.getExportZoom()) != value) { if (Math.round(this.getExportZoom()) != value) {
this.sizeInputWidget.setWidth(this.piskelController.getWidth() * 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); this.updateScaleText_(zoom);
$.publish(Events.EXPORT_SCALE_CHANGED);
this.scaleInput.value = Math.round(zoom); this.scaleInput.value = Math.round(zoom);
if (zoom >= 1 && zoom <= 32) { if (zoom >= 1 && zoom <= 32) {

View File

@@ -24,10 +24,6 @@
this.addEventListener(this.downloadButton, 'click', this.onDownloadButtonClick_); this.addEventListener(this.downloadButton, 'click', this.onDownloadButtonClick_);
}; };
ns.GifExportController.prototype.destroy = function () {
this.superclass.destroy.call(this);
};
ns.GifExportController.prototype.getZoom_ = function () { ns.GifExportController.prototype.getZoom_ = function () {
return this.exportController.getExportZoom(); return this.exportController.getExportZoom();
}; };

View File

@@ -1,41 +1,158 @@
(function () { (function () {
var ns = $.namespace('pskl.controller.settings.exportimage'); var ns = $.namespace('pskl.controller.settings.exportimage');
var dimensionInfoPattern = '{{width}} x {{height}} px, {{frames}}<br/>{{rows}}, {{columns}}.';
var replace = pskl.utils.Template.replace;
var pluralize = function (word, count) {
if (count === 1) {
return '1 ' + word;
}
return count + ' ' + word + 's';
};
var getNearestPowerOfTwo = function (number) {
return Math.pow(2, Math.ceil(Math.log(number)/Math.log(2)));
};
ns.PngExportController = function (piskelController, exportController) { ns.PngExportController = function (piskelController, exportController) {
this.piskelController = piskelController; this.piskelController = piskelController;
this.exportController = exportController; this.exportController = exportController;
this.onScaleChanged_ = this.onScaleChanged_.bind(this);
}; };
pskl.utils.inherit(ns.PngExportController, pskl.controller.settings.AbstractSettingController); pskl.utils.inherit(ns.PngExportController, pskl.controller.settings.AbstractSettingController);
ns.PngExportController.prototype.init = function () { 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'); 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, 'keydown', this.onColumnsKeydown_);
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) { ns.PngExportController.prototype.destroy = function () {
var fileName = this.getPiskelName_() + '.png'; $.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.onColumnsKeydown_ = function (evt) {
var key = pskl.service.keyboard.KeycodeTranslator.toChar(evt.keyCode);
if (key === 'up') {
evt.preventDefault();
this.columnsInput.value = this.getColumns_() + 1;
this.onColumnsChanged_();
} else if (key === 'down') {
evt.preventDefault();
this.columnsInput.value = this.getColumns_() - 1;
this.onColumnsChanged_();
}
}
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(); var zoom = this.exportController.getExportZoom();
if (zoom != 1) { if (zoom != 1) {
var width = outputCanvas.width * zoom; outputCanvas = pskl.utils.ImageResizer.resize(outputCanvas, width * zoom, height * zoom, false);
var height = outputCanvas.height * zoom; }
outputCanvas = pskl.utils.ImageResizer.resize(outputCanvas, width, height, 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.BlobUtils.canvasToBlob(outputCanvas, function(blob) {
pskl.utils.FileUtils.downloadAsFile(blob, fileName); 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();
};
})(); })();

View File

@@ -13,11 +13,17 @@
} }
}; };
ns.FramesheetRenderer.prototype.renderAsCanvas = function () { ns.FramesheetRenderer.prototype.renderAsCanvas = function (columns) {
var canvas = this.createCanvas_(); columns = columns || 1;
var rows = Math.ceil(this.frames.length / columns);
var canvas = this.createCanvas_(columns, rows);
for (var i = 0 ; i < this.frames.length ; i++) { for (var i = 0 ; i < this.frames.length ; i++) {
var frame = this.frames[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; return canvas;
}; };
@@ -32,11 +38,10 @@
}); });
}; };
ns.FramesheetRenderer.prototype.createCanvas_ = function () { ns.FramesheetRenderer.prototype.createCanvas_ = function (columns, rows) {
var sampleFrame = this.frames[0]; var sampleFrame = this.frames[0];
var count = this.frames.length; var width = columns * sampleFrame.getWidth();
var width = count * sampleFrame.getWidth(); var height = rows * sampleFrame.getHeight();
var height = sampleFrame.getHeight();
return pskl.utils.CanvasUtils.createCanvas(width, height); return pskl.utils.CanvasUtils.createCanvas(width, height);
}; };
})(); })();

View File

@@ -11,11 +11,17 @@
this.frames = frames; this.frames = frames;
}; };
ns.PiskelRenderer.prototype.renderAsCanvas = function () { ns.PiskelRenderer.prototype.renderAsCanvas = function (columns) {
var canvas = this.createCanvas_(); columns = columns || 1;
var rows = Math.ceil(this.frames.length / columns);
var canvas = this.createCanvas_(columns, rows);
for (var i = 0 ; i < this.frames.length ; i++) { for (var i = 0 ; i < this.frames.length ; i++) {
var frame = this.frames[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; return canvas;
}; };
@@ -25,10 +31,9 @@
context.drawImage(frame, offsetWidth, offsetHeight, frame.width, frame.height); context.drawImage(frame, offsetWidth, offsetHeight, frame.width, frame.height);
}; };
ns.PiskelRenderer.prototype.createCanvas_ = function () { ns.PiskelRenderer.prototype.createCanvas_ = function (columns, rows) {
var count = this.frames.length; var width = columns * this.piskelController.getWidth();
var width = count * this.piskelController.getWidth(); var height = rows * this.piskelController.getHeight();
var height = this.piskelController.getHeight();
return pskl.utils.CanvasUtils.createCanvas(width, height); return pskl.utils.CanvasUtils.createCanvas(width, height);
}; };
})(); })();

View File

@@ -12,7 +12,9 @@
ONION_SKIN : 'ONION_SKIN', ONION_SKIN : 'ONION_SKIN',
LAYER_PREVIEW : 'LAYER_PREVIEW', LAYER_PREVIEW : 'LAYER_PREVIEW',
LAYER_OPACITY : 'LAYER_OPACITY', LAYER_OPACITY : 'LAYER_OPACITY',
EXPORT_SCALING: 'EXPORT_SCALING', EXPORT_PNG_BEST_FIT: 'EXPORT_PNG_BEST_FIT',
EXPORT_PNG_POWER_TWO: 'EXPORT_PNG_POWER_TWO',
EXPORT_SCALE: 'EXPORT_SCALE',
EXPORT_TAB: 'EXPORT_TAB', EXPORT_TAB: 'EXPORT_TAB',
PEN_SIZE : 'PEN_SIZE', PEN_SIZE : 'PEN_SIZE',
RESIZE_SETTINGS: 'RESIZE_SETTINGS', RESIZE_SETTINGS: 'RESIZE_SETTINGS',
@@ -30,8 +32,10 @@
'ONION_SKIN' : false, 'ONION_SKIN' : false,
'LAYER_OPACITY' : 0.2, 'LAYER_OPACITY' : 0.2,
'LAYER_PREVIEW' : true, 'LAYER_PREVIEW' : true,
'EXPORT_SCALING' : 1, 'EXPORT_PNG_BEST_FIT' : false,
'EXPORT_TAB' : 'png', 'EXPORT_PNG_POWER_TWO' : false,
'EXPORT_SCALE' : 1,
'EXPORT_TAB' : 'gif',
'PEN_SIZE' : 1, 'PEN_SIZE' : 1,
'RESIZE_SETTINGS': { 'RESIZE_SETTINGS': {
maintainRatio : true, maintainRatio : true,

View File

@@ -15,8 +15,8 @@
<input type="text" class="textfield resize-field resize-height" autocomplete="off" name="resize-height"/> <input type="text" class="textfield resize-field resize-height" autocomplete="off" name="resize-height"/>
</div> </div>
<div class="export-tabs"> <div class="export-tabs">
<div class="export-tab" data-tab-id="png">PNG</div>
<div class="export-tab" data-tab-id="gif">GIF</div> <div class="export-tab" data-tab-id="gif">GIF</div>
<div class="export-tab" data-tab-id="png">PNG</div>
<div class="export-tab" data-tab-id="zip">Zip</div> <div class="export-tab" data-tab-id="zip">Zip</div>
<div class="export-tab" data-tab-id="misc">Others</div> <div class="export-tab" data-tab-id="misc">Others</div>
</div> </div>

View File

@@ -3,11 +3,11 @@
<div class="export-info"> <div class="export-info">
Convert your sprite to an animated GIF. Opacity will not be preserved. Colors might be resampled. Convert your sprite to an animated GIF. Opacity will not be preserved. Colors might be resampled.
</div> </div>
<div class="export-panel-section"> <div class="export-panel-section export-panel-row">
<button type="button" class="button button-primary gif-download-button">Download</button> <button type="button" class="button button-primary gif-download-button">Download</button>
<div>Download as an animated GIF.</div> <div>Download as an animated GIF.</div>
</div> </div>
<div class="export-panel-section"> <div class="export-panel-section export-panel-row">
<button type="button" class="button button-primary gif-upload-button">Upload</button> <button type="button" class="button button-primary gif-upload-button">Upload</button>
<div>Upload as an animated GIF to a public URL.</div> <div>Upload as an animated GIF to a public URL.</div>
</div> </div>

View File

@@ -1,8 +1,22 @@
<script type="text/html" id="templates/settings/export/png.html"> <script type="text/html" id="templates/settings/export/png.html">
<div class="export-panel-png"> <div class="export-panel-png">
<div class="export-info">Export your animation as a PNG spritesheet containing all frames.</div> <div class="export-info">Export your animation as a PNG spritesheet containing all frames.</div>
<div class="export-panel-section"> <div class="export-panel-section png-export-layout-section">
<button type="button" class="button button-primary png-download-button">Download PNG</button> <div style="color: gold; padding-bottom: 5px;">Spritesheet layout options:</div>
<div style="display: flex;">
<div style="flex: 1; line-height: 20px;">
<span>Columns</span>
<input type="text" class="textfield" id="png-export-columns" name="png-export-columns">
</div>
<div style="flex: 1; line-height: 20px;">
<label for="png-export-power-two">Power of 2</label>
<input type="checkbox" id="png-export-power-two" name="png-export-power-two">
</div>
</div>
</div>
<div class="export-panel-section export-panel-row">
<button type="button" class="button button-primary png-download-button">Download</button>
<span class="png-export-dimension-info"></span>
</div> </div>
</div> </div>
</script> </script>