export controller: add synchronized rows input, remove poweroftwo checkbox

This commit is contained in:
Julian Descottes
2016-07-02 12:45:48 +02:00
parent b39b3befa1
commit cd36a57a1a
11 changed files with 139 additions and 91 deletions

View File

@@ -14,6 +14,10 @@
height: 23px; height: 23px;
} }
.textfield[readonly="true"] {
background: transparent;
}
.textfield[disabled=disabled] { .textfield[disabled=disabled] {
background : #3a3a3a; background : #3a3a3a;
} }

View File

@@ -137,11 +137,14 @@
border-width: 1px; border-width: 1px;
} }
.export-info { .export-panel-header {
padding: 10px 5px 0px; padding: 10px 5px 0px;
font-weight: normal; }
text-shadow: none;
.export-info {
font-style: italic; font-style: italic;
text-shadow: none;
font-weight: normal;
} }
.export-panel-section { .export-panel-section {
@@ -167,13 +170,13 @@
width: 50px; width: 50px;
} }
.png-export-dimension-info { .png-export-dimension-info,
font-style: italic; .png-export-datauri-info {
font-weight: normal;
margin-left: 5px; margin-left: 5px;
} }
#png-export-columns { #png-export-columns,
#png-export-rows {
/* Override default textfield padding-right to keep the number spinners /* Override default textfield padding-right to keep the number spinners
aligned to the right. */ aligned to the right. */
padding-right: 0; padding-right: 0;

View File

@@ -1,12 +1,13 @@
(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 dimensionInfoPattern = '{{width}} x {{height}} px, {{frames}}<br/>{{columns}}, {{rows}}.';
// Shortcut to pskl.utils.Template.replace // Shortcut to pskl.utils.Template.replace
var replace = pskl.utils.Template.replace; var replace = pskl.utils.Template.replace;
// Helper to return "X items" or "1 item" if X is 1. // Helper to return "X items" or "1 item" if X is 1. Can be cnsidered as an overkill,
// but the one-liner equivalent is hard to read.
var pluralize = function (word, count) { var pluralize = function (word, count) {
if (count === 1) { if (count === 1) {
return '1 ' + word; return '1 ' + word;
@@ -14,11 +15,6 @@
return count + ' ' + word + 's'; 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) { ns.PngExportController = function (piskelController, exportController) {
this.piskelController = piskelController; this.piskelController = piskelController;
this.exportController = exportController; this.exportController = exportController;
@@ -30,16 +26,19 @@
ns.PngExportController.prototype.init = function () { ns.PngExportController.prototype.init = function () {
this.layoutContainer = document.querySelector('.png-export-layout-section'); this.layoutContainer = document.querySelector('.png-export-layout-section');
this.dimensionInfo = document.querySelector('.png-export-dimension-info'); this.dimensionInfo = document.querySelector('.png-export-dimension-info');
this.rowsInput = document.querySelector('#png-export-rows');
this.columnsInput = document.querySelector('#png-export-columns'); 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');
var dataUriButton = document.querySelector('.datauri-open-button');
this.initLayoutSection_(); this.initLayoutSection_();
this.updateDimensionLabel_(); this.updateDimensionLabel_();
this.addEventListener(this.columnsInput, 'input', this.onColumnsInput_);
this.addEventListener(downloadButton, 'click', this.onDownloadClick_); this.addEventListener(downloadButton, 'click', this.onDownloadClick_);
this.addEventListener(this.columnsInput, 'input', this.onColumnsChanged_); this.addEventListener(dataUriButton, 'click', this.onDataUriClick_);
this.addEventListener(this.powerTwo, 'change', this.onPowerTwoChanged_);
$.subscribe(Events.EXPORT_SCALE_CHANGED, this.onScaleChanged_); $.subscribe(Events.EXPORT_SCALE_CHANGED, this.onScaleChanged_);
}; };
@@ -57,8 +56,9 @@
// Hide the layout section if only one frame is defined. // Hide the layout section if only one frame is defined.
this.layoutContainer.style.display = 'none'; this.layoutContainer.style.display = 'none';
} else { } else {
this.columnsInput.setAttribute('max', frames);
this.columnsInput.value = this.getBestFit_(); this.columnsInput.value = this.getBestFit_();
this.powerTwo.checked = pskl.UserSettings.get('EXPORT_PNG_POWER_TWO'); this.onColumnsInput_();
} }
}; };
@@ -69,15 +69,10 @@
var height = this.piskelController.getHeight() * zoom; var height = this.piskelController.getHeight() * zoom;
var columns = this.getColumns_(); var columns = this.getColumns_();
var rows = Math.ceil(frames / columns); var rows = this.getRows_();
width = columns * width; width = columns * width;
height = rows * height; height = rows * height;
if (this.isPowerTwoEnabled_()) {
width = getNearestPowerOfTwo(width);
height = getNearestPowerOfTwo(height);
}
this.dimensionInfo.innerHTML = replace(dimensionInfoPattern, { this.dimensionInfo.innerHTML = replace(dimensionInfoPattern, {
width: width, width: width,
height: height, height: height,
@@ -91,42 +86,51 @@
return parseInt(this.columnsInput.value || 1, 10); return parseInt(this.columnsInput.value || 1, 10);
}; };
ns.PngExportController.prototype.getRows_ = function () {
return parseInt(this.rowsInput.value || 1, 10);
};
ns.PngExportController.prototype.getBestFit_ = function () { ns.PngExportController.prototype.getBestFit_ = function () {
var ratio = this.piskelController.getWidth() / this.piskelController.getHeight(); var ratio = this.piskelController.getWidth() / this.piskelController.getHeight();
var frameCount = this.piskelController.getFrameCount(); var frameCount = this.piskelController.getFrameCount();
var bestFit = Math.round(Math.sqrt(frameCount / ratio)); var bestFit = Math.round(Math.sqrt(frameCount / ratio));
return Math.max(1, Math.min(bestFit, frameCount)); return pskl.utils.Math.minmax(bestFit, 1, frameCount);
};
ns.PngExportController.prototype.isPowerTwoEnabled_ = function () {
return pskl.UserSettings.get('EXPORT_PNG_POWER_TWO');
}; };
ns.PngExportController.prototype.onScaleChanged_ = function () { ns.PngExportController.prototype.onScaleChanged_ = function () {
this.updateDimensionLabel_(); this.updateDimensionLabel_();
}; };
ns.PngExportController.prototype.onColumnsChanged_ = function () { /**
if (this.getColumns_() > this.piskelController.getFrameCount()) { * Synchronise column and row inputs, called everytime a user input updates one of the
this.columnsInput.value = this.piskelController.getFrameCount(); * two inputs by the SynchronizedInputs widget.
} else if (this.getColumns_() < 1) { */
this.columnsInput.value = 1; ns.PngExportController.prototype.onColumnsInput_ = function () {
var value = this.columnsInput.value;
if (value === '') {
// Skip the synchronization if the input is empty.
return;
} }
value = parseInt(value, 10);
if (isNaN(value)) {
value = 1;
}
// Force the value to be in bounds, in the user tried to update it by directly typing
// a value.
value = pskl.utils.Math.minmax(value, 1, this.piskelController.getFrameCount());
this.columnsInput.value = value;
// Update readonly rowsInput
this.rowsInput.value = Math.ceil(this.piskelController.getFrameCount() / value);
this.updateDimensionLabel_(); this.updateDimensionLabel_();
}; };
ns.PngExportController.prototype.onPowerTwoChanged_ = function () { ns.PngExportController.prototype.createPngSpritesheet_ = 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 renderer = new pskl.rendering.PiskelRenderer(this.piskelController);
var outputCanvas = renderer.renderAsCanvas(this.getColumns_()); var outputCanvas = renderer.renderAsCanvas(this.getColumns_(), this.getRows_());
var width = outputCanvas.width; var width = outputCanvas.width;
var height = outputCanvas.height; var height = outputCanvas.height;
@@ -135,15 +139,24 @@
outputCanvas = pskl.utils.ImageResizer.resize(outputCanvas, width * zoom, height * zoom, false); outputCanvas = pskl.utils.ImageResizer.resize(outputCanvas, width * zoom, height * zoom, false);
} }
if (this.isPowerTwoEnabled_()) { return outputCanvas;
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) { ns.PngExportController.prototype.onDownloadClick_ = function (evt) {
// Create PNG export.
var canvas = this.createPngSpritesheet_();
// Generate file name
var name = this.piskelController.getPiskel().getDescriptor().name;
var fileName = name + '.png';
// Transform to blob and start download.
pskl.utils.BlobUtils.canvasToBlob(canvas, function(blob) {
pskl.utils.FileUtils.downloadAsFile(blob, fileName); pskl.utils.FileUtils.downloadAsFile(blob, fileName);
}); });
}; };
ns.PngExportController.prototype.onDataUriClick_ = function (evt) {
window.open(this.createPngSpritesheet_().toDataURL('image/png'));
};
})(); })();

View File

@@ -12,7 +12,6 @@
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_PNG_POWER_TWO: 'EXPORT_PNG_POWER_TWO',
EXPORT_SCALE: 'EXPORT_SCALE', EXPORT_SCALE: 'EXPORT_SCALE',
EXPORT_TAB: 'EXPORT_TAB', EXPORT_TAB: 'EXPORT_TAB',
PEN_SIZE : 'PEN_SIZE', PEN_SIZE : 'PEN_SIZE',
@@ -31,7 +30,6 @@
'ONION_SKIN' : false, 'ONION_SKIN' : false,
'LAYER_OPACITY' : 0.2, 'LAYER_OPACITY' : 0.2,
'LAYER_PREVIEW' : true, 'LAYER_PREVIEW' : true,
'EXPORT_PNG_POWER_TWO' : false,
'EXPORT_SCALE' : 1, 'EXPORT_SCALE' : 1,
'EXPORT_TAB' : 'gif', 'EXPORT_TAB' : 'gif',
'PEN_SIZE' : 1, 'PEN_SIZE' : 1,

View File

@@ -19,31 +19,24 @@
this.initHeight = options.initHeight; this.initHeight = options.initHeight;
this.onChange = options.onChange; this.onChange = options.onChange;
this.syncEnabled = true; this.synchronizedInputs = new ns.SynchronizedInputs({
this.lastInput = this.widthInput; leftInput: this.widthInput,
rightInput: this.heightInput,
synchronize: this.synchronize_.bind(this)
});
this.disableSync = this.synchronizedInputs.disableSync.bind(this.synchronizedInputs);
this.enableSync = this.synchronizedInputs.enableSync.bind(this.synchronizedInputs);
this.widthInput.value = this.initWidth; this.widthInput.value = this.initWidth;
this.heightInput.value = this.initHeight; this.heightInput.value = this.initHeight;
pskl.utils.Event.addEventListener(this.widthInput, 'keyup', this.onSizeInputKeyUp_, this);
pskl.utils.Event.addEventListener(this.heightInput, 'keyup', this.onSizeInputKeyUp_, this);
}; };
ns.SizeInput.prototype.destroy = function () { ns.SizeInput.prototype.destroy = function () {
pskl.utils.Event.removeAllEventListeners(this); this.synchronizedInputs.destroy();
this.widthInput = null; this.widthInput = null;
this.heightInput = null; this.heightInput = null;
this.lastInput = null;
};
ns.SizeInput.prototype.enableSync = function () {
this.syncEnabled = true;
this.synchronize_(this.lastInput);
};
ns.SizeInput.prototype.disableSync = function () {
this.syncEnabled = false;
}; };
ns.SizeInput.prototype.setWidth = function (width) { ns.SizeInput.prototype.setWidth = function (width) {
@@ -56,14 +49,6 @@
this.synchronize_(this.heightInput); this.synchronize_(this.heightInput);
}; };
ns.SizeInput.prototype.onSizeInputKeyUp_ = function (evt) {
var target = evt.target;
if (this.syncEnabled) {
this.synchronize_(target);
}
this.lastInput = target;
};
/** /**
* Based on the value of the provided sizeInput (considered as emitter) * Based on the value of the provided sizeInput (considered as emitter)
* update the value of the other sizeInput to match the current width/height ratio * update the value of the other sizeInput to match the current width/height ratio

View File

@@ -0,0 +1,40 @@
(function () {
var ns = $.namespace('pskl.widgets');
ns.SynchronizedInputs = function (options) {
this.leftInput = options.leftInput;
this.rightInput = options.rightInput;
this.synchronize = options.synchronize;
this.syncEnabled = true;
this.lastInput = this.leftInput;
pskl.utils.Event.addEventListener(this.leftInput, 'input', this.onInput_, this);
pskl.utils.Event.addEventListener(this.rightInput, 'input', this.onInput_, this);
};
ns.SynchronizedInputs.prototype.destroy = function () {
pskl.utils.Event.removeAllEventListeners(this);
this.leftInput = null;
this.rightInput = null;
this.lastInput = null;
};
ns.SynchronizedInputs.prototype.enableSync = function () {
this.syncEnabled = true;
this.synchronize(this.lastInput);
};
ns.SynchronizedInputs.prototype.disableSync = function () {
this.syncEnabled = false;
};
ns.SynchronizedInputs.prototype.onInput_ = function (evt) {
var target = evt.target;
if (this.syncEnabled) {
this.synchronize(target);
}
this.lastInput = target;
};
})();

View File

@@ -142,6 +142,7 @@
"js/widgets/ColorsList.js", "js/widgets/ColorsList.js",
"js/widgets/HslRgbColorPicker.js", "js/widgets/HslRgbColorPicker.js",
"js/widgets/SizeInput.js", "js/widgets/SizeInput.js",
"js/widgets/SynchronizedInputs.js",
// Services // Services
"js/service/storage/StorageService.js", "js/service/storage/StorageService.js",

View File

@@ -1,15 +1,15 @@
<script type="text/html" id="templates/settings/export/gif.html"> <script type="text/html" id="templates/settings/export/gif.html">
<div class="export-panel-gif"> <div class="export-panel-gif">
<div class="export-info"> <div class="export-panel-header 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 export-panel-row"> <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 class="export-info">Download as an animated GIF.</div>
</div> </div>
<div class="export-panel-section export-panel-row"> <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 class="export-info">Upload as an animated GIF to a public URL.</div>
</div> </div>
<div class="gif-upload"> <div class="gif-upload">
<div class="gif-export-preview"></div> <div class="gif-export-preview"></div>

View File

@@ -1,12 +1,12 @@
<script type="text/html" id="templates/settings/export/misc.html"> <script type="text/html" id="templates/settings/export/misc.html">
<div class="export-panel-misc"> <div class="export-panel-misc">
<div class="export-info"> <div class="export-panel-header export-info">
Less usual export formats. Feedback and improvements welcome. Less usual export formats. Feedback and improvements welcome.
</div> </div>
<div class="export-panel-section"> <div class="export-panel-section">
<div style="padding-bottom: 5px"> <div style="padding-bottom: 5px">
<span style="color: gold;">Export as C File: </span> <span style="color: gold;">Export as C File: </span>
<span style="font-weight: normal; text-shadow: none; font-style: italic"> <span class="export-info">
C file with frame rendered as array. C file with frame rendered as array.
</span> </span>
</div> </div>

View File

@@ -1,22 +1,26 @@
<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-panel-header export-info">Export your animation as a PNG spritesheet containing all frames.</div>
<div class="export-panel-section png-export-layout-section"> <div class="export-panel-section png-export-layout-section">
<div style="color: gold; padding-bottom: 5px;">Spritesheet layout options:</div> <div style="color: gold; padding-bottom: 5px;">Spritesheet layout options:</div>
<div style="display: flex;"> <div style="display: flex; line-height: 20px;">
<div style="flex: 1; line-height: 20px;"> <div style="flex: 1;">
<span>Columns</span> <span>Columns</span>
<input type="number" class="textfield" id="png-export-columns" name="png-export-columns"> <input type="number" min="1" class="textfield" id="png-export-columns" name="png-export-columns">
</div> </div>
<div style="flex: 1; line-height: 20px;"> <div style="flex: 1;">
<label for="png-export-power-two">Power of 2</label> <span>Rows</span>
<input type="checkbox" id="png-export-power-two" name="png-export-power-two"> <input readonly="true" class="textfield" id="png-export-rows" name="png-export-rows">
</div> </div>
</div> </div>
</div> </div>
<div class="export-panel-section export-panel-row"> <div class="export-panel-section export-panel-row">
<button type="button" class="button button-primary png-download-button">Download</button> <button type="button" class="button button-primary png-download-button">Download</button>
<span class="png-export-dimension-info"></span> <span class="png-export-dimension-info export-info"></span>
</div>
<div class="export-panel-section export-panel-row png-export-datauri-section">
<button type="button" style="white-space: nowrap;" class="button button-primary datauri-open-button">To data-uri</button>
<span class="png-export-datauri-info export-info">Open the PNG export in your browser as a data-uri</span>
</div> </div>
</div> </div>
</script> </script>

View File

@@ -1,6 +1,6 @@
<script type="text/html" id="templates/settings/export/zip.html"> <script type="text/html" id="templates/settings/export/zip.html">
<div class="export-panel-zip"> <div class="export-panel-zip">
<div class="export-info">ZIP archive containing one PNG for each frame. File names will start with the prefix below.</div> <div class="export-panel-header export-info">ZIP archive containing one PNG for each frame. File names will start with the prefix below.</div>
<div class="export-panel-section"> <div class="export-panel-section">
<div style="display: flex; line-height: 23px;"> <div style="display: flex; line-height: 23px;">
<label style="flex-shrink: 0; padding-right:5px;">Prefix</label> <label style="flex-shrink: 0; padding-right:5px;">Prefix</label>