Fix : Export to GIF : download option

In the GIF export panel, user can now choose between :
- export online (previous 'upload' feature)
- download GIF

Labels have been updated in the PNG export panel to follow the same
convention.

CanvasToBlob library was modified and moved to dedicated utils to handle
not only canvas, but also any base64 dateURI.
This commit is contained in:
jdescottes 2014-06-14 14:21:26 +02:00
parent d501129e8e
commit 2809a551d7
12 changed files with 152 additions and 264 deletions

View File

@ -19,7 +19,10 @@
this.shortcutService = new pskl.service.keyboard.ShortcutService();
this.shortcutService.init();
var size = this.readSizeFromURL_();
var size = {
height : Constants.DEFAULT.HEIGHT,
width : Constants.DEFAULT.WIDTH
};
var descriptor = new pskl.model.piskel.Descriptor('New Piskel', '');
var piskel = new pskl.model.Piskel(size.width, size.height, descriptor);
@ -111,35 +114,27 @@
this.initTooltips_();
if (this.isAppEngineVersion) {
this.finishInitAppEngine_();
} else {
this.finishInitGithub_();
var piskelData = this.getPiskelInitData_();
if (piskelData && piskelData.piskel) {
this.loadPiskel_(piskelData.piskel, piskelData.descriptor, piskelData.fps);
}
},
finishInitGithub_ : function () {
var framesheetId = this.readFramesheetIdFromURL_();
if (framesheetId) {
$.publish(Events.SHOW_NOTIFICATION, [{
"content" : "Loading animation with id : [" + framesheetId + "]"
}]);
this.loadFramesheetFromService(framesheetId);
}
},
finishInitAppEngine_ : function () {
if (pskl.appEnginePiskelData_ && pskl.appEnginePiskelData_.piskel) {
pskl.utils.serialization.Deserializer.deserialize(pskl.appEnginePiskelData_.piskel, function (piskel) {
piskel.setDescriptor(pskl.appEnginePiskelData_.descriptor);
loadPiskel_ : function (serializedPiskel, descriptor, fps) {
pskl.utils.serialization.Deserializer.deserialize(serializedPiskel, function (piskel) {
piskel.setDescriptor(descriptor);
pskl.app.piskelController.setPiskel(piskel);
pskl.app.animationController.setFPS(pskl.appEnginePiskelData_.fps);
pskl.app.animationController.setFPS(fps);
});
}
},
getPiskelInitData_ : function () {
return pskl.appEnginePiskelData_;
},
isLoggedIn : function () {
return pskl.appEnginePiskelData_ && pskl.appEnginePiskelData_.isLoggedIn;
var piskelData = this.getPiskelInitData_();
return piskelData && piskelData.isLoggedIn;
},
initTooltips_ : function () {
@ -154,69 +149,6 @@
this.previewFilmController.render(delta);
},
readSizeFromURL_ : function () {
var sizeParam = this.readUrlParameter_("size");
var size;
// parameter expected as size=64x48 => size=widthxheight
var parts = sizeParam.split("x");
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 = {
height : Constants.DEFAULT.HEIGHT,
width : Constants.DEFAULT.WIDTH
};
}
return size;
},
readFramesheetIdFromURL_ : function () {
return this.readUrlParameter_("frameId");
},
readUrlParameter_ : function (paramName) {
var searchString = window.location.search.substring(1);
var params = searchString.split("&");
for (var i = 0; i < params.length; i++) {
var param = params[i].split("=");
if (param[0] == paramName) {
return window.unescape(param[1]);
}
}
return "";
},
loadFramesheetFromService : function (frameId) {
var xhr = new XMLHttpRequest();
xhr.open('GET', Constants.STATIC.URL.GET + '?l=' + frameId, true);
xhr.responseType = 'text';
xhr.onload = function (e) {
var res = JSON.parse(this.responseText);
pskl.utils.serialization.Deserializer.deserialize(res.framesheet, function (piskel) {
pskl.app.piskelController.setPiskel(piskel);
pskl.app.animationController.setFPS(res.fps);
$.publish(Events.HIDE_NOTIFICATION);
});
};
xhr.onerror = function () {
$.publish(Events.HIDE_NOTIFICATION);
};
xhr.send();
},
store : function (callbacks) {
this.storageService.store(callbacks);
},
getFirstFrameAsPng : function () {
var firstFrame = this.piskelController.getFrameAt(0);
var canvasRenderer = new pskl.rendering.CanvasRenderer(firstFrame, 1);
@ -229,21 +161,6 @@
var renderer = new pskl.rendering.PiskelRenderer(this.piskelController);
var framesheetCanvas = renderer.renderAsCanvas();
return framesheetCanvas.toDataURL("image/png");
},
uploadAsSpritesheetPNG : function () {
var imageData = this.getFramesheetAsPng();
this.imageUploadService.upload(imageData, this.openWindow.bind(this));
},
openWindow : function (url) {
var options = [
"dialog=yes", "scrollbars=no", "status=no",
"width=" + this.piskelController.getWidth() * this.piskelController.getFrameCount(),
"height=" + this.piskelController.getHeight()
].join(",");
window.open(url, "piskel-export", options);
}
};
})();

View File

@ -34,8 +34,13 @@
this.previewContainerEl = document.querySelector(".gif-export-preview");
this.radioGroupEl = document.querySelector(".gif-export-radio-group");
this.uploadForm = $("[name=gif-export-upload-form]");
this.uploadForm.submit(this.onUploadFormSubmit_.bind(this));
this.uploadButton = $(".gif-upload-button");
this.uploadButton.click(this.onUploadButtonClick_.bind(this));
this.downloadButton = $(".gif-download-button");
this.downloadButton.click(this.onDownloadButtonClick_.bind(this));
this.exportForm = $(".gif-export-form");
this.exportProgressStatusEl = document.querySelector('.gif-export-progress-status');
this.exportProgressBarEl = document.querySelector('.gif-export-progress-bar');
@ -43,15 +48,27 @@
this.createRadioElements_();
};
ns.GifExportController.prototype.onUploadFormSubmit_ = function (evt) {
ns.GifExportController.prototype.onUploadButtonClick_ = function (evt) {
evt.originalEvent.preventDefault();
var selectedZoom = this.getSelectedZoom_(),
fps = this.piskelController.getFPS(),
zoom = selectedZoom;
var zoom = this.getSelectedZoom_(),
fps = this.piskelController.getFPS();
this.renderAsImageDataAnimatedGIF(zoom, fps, this.onGifRenderingCompleted_.bind(this));
};
ns.GifExportController.prototype.onDownloadButtonClick_ = function (evt) {
var fileName = this.piskelController.getPiskel().getDescriptor().name + '.gif';
var zoom = this.getSelectedZoom_(),
fps = this.piskelController.getFPS();
this.renderAsImageDataAnimatedGIF(zoom, fps, function (imageData) {
pskl.app.imageUploadService.upload(imageData, this.onImageUploadCompleted_.bind(this));
pskl.utils.ImageToBlob.imageDataToBlob(imageData, "image/gif", function(blob) {
pskl.utils.FileUtils.downloadAsFile(fileName, blob);
});
}.bind(this));
};
ns.GifExportController.prototype.onGifRenderingCompleted_ = function (imageData) {
this.updatePreview_(imageData);
this.previewContainerEl.classList.add("preview-upload-ongoing");
@ -62,7 +79,6 @@
this.updatePreview_(imageUrl);
this.updateStatus_(imageUrl);
this.previewContainerEl.classList.remove("preview-upload-ongoing");
};
ns.GifExportController.prototype.updatePreview_ = function (src) {
@ -70,7 +86,7 @@
};
ns.GifExportController.prototype.getSelectedZoom_ = function () {
var radiosColl = this.uploadForm.get(0).querySelectorAll("[name=gif-zoom-level]"),
var radiosColl = this.exportForm.get(0).querySelectorAll("[name=gif-zoom-level]"),
radios = Array.prototype.slice.call(radiosColl,0);
var selectedRadios = radios.filter(function(radio) {return !!radio.checked;});
@ -105,15 +121,6 @@
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(zoom, fps, cb) {
var colorCount = pskl.app.currentColorsService.getCurrentColors().length;
var preserveColors = colorCount < MAX_GIF_COLORS;
@ -140,7 +147,7 @@
gif.on('finished', function(blob) {
this.hideProgressStatus_();
this.blobToBase64_(blob, cb);
pskl.utils.FileUtils.readFile(blob, cb);
}.bind(this));
gif.render();

View File

@ -16,21 +16,19 @@
document.querySelector(".zip-generate-button").addEventListener('click', this.onZipButtonClick_.bind(this));
this.updatePreview_(this.getFramesheetAsBase64Png());
this.setPreviewSrc_(this.getFramesheetAsCanvas().toDataURL("image/png"));
};
ns.PngExportController.prototype.onPngDownloadButtonClick_ = function (evt) {
var fileName = this.getPiskelName_() + '.png';
var renderer = new pskl.rendering.PiskelRenderer(this.piskelController);
var canvas = renderer.renderAsCanvas();
canvas.toBlob(function(blob) {
pskl.utils.ImageToBlob.canvasToBlob(this.getFramesheetAsCanvas(), function(blob) {
pskl.utils.FileUtils.downloadAsFile(fileName, blob);
});
};
ns.PngExportController.prototype.onPngUploadButtonClick_ = function (evt) {
this.previewContainerEl.classList.add("preview-upload-ongoing");
pskl.app.imageUploadService.upload(this.getFramesheetAsBase64Png(), this.onImageUploadCompleted_.bind(this));
pskl.app.imageUploadService.upload(this.getFramesheetAsCanvas().toDataURL("image/png"), this.onImageUploadCompleted_.bind(this));
};
ns.PngExportController.prototype.onZipButtonClick_ = function () {
@ -59,10 +57,9 @@
return this.piskelController.getPiskel().getDescriptor().name;
};
ns.PngExportController.prototype.getFramesheetAsBase64Png = function () {
ns.PngExportController.prototype.getFramesheetAsCanvas = function () {
var renderer = new pskl.rendering.PiskelRenderer(this.piskelController);
var framesheetCanvas = renderer.renderAsCanvas();
return framesheetCanvas.toDataURL("image/png");
return renderer.renderAsCanvas();
};
ns.PngExportController.prototype.onImageUploadCompleted_ = function (imageUrl) {
@ -84,7 +81,7 @@
}
};
ns.PngExportController.prototype.updatePreview_ = function (src) {
ns.PngExportController.prototype.setPreviewSrc_ = function (src) {
this.previewContainerEl.innerHTML = "<img class='light-picker-background' style='max-width:240px;' src='"+src+"'/>";
};

View File

@ -49,7 +49,7 @@
this.piskelController.getPiskel().setDescriptor(descriptor);
this.beforeSaving_();
pskl.app.store({
pskl.app.storageService.store({
success : this.onSaveSuccess_.bind(this),
error : this.onSaveError_.bind(this),
after : this.afterSaving_.bind(this)

View File

@ -1,107 +0,0 @@
/* canvas-toBlob.js
* A canvas.toBlob() implementation.
* 2011-07-13
*
* By Eli Grey, http://eligrey.com and Devin Samarin, https://github.com/eboyjr
* License: X11/MIT
* See LICENSE.md
*/
/*global self */
/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
plusplus: true */
/*! @source http://purl.eligrey.com/github/canvas-toBlob.js/blob/master/canvas-toBlob.js */
(function(view) {
"use strict";
var
Uint8Array = view.Uint8Array
, HTMLCanvasElement = view.HTMLCanvasElement
, is_base64_regex = /\s*;\s*base64\s*(?:;|$)/i
, base64_ranks
, decode_base64 = function(base64) {
var
len = base64.length
, buffer = new Uint8Array(len / 4 * 3 | 0)
, i = 0
, outptr = 0
, last = [0, 0]
, state = 0
, save = 0
, rank
, code
, undef
;
while (len--) {
code = base64.charCodeAt(i++);
rank = base64_ranks[code-43];
if (rank !== 255 && rank !== undef) {
last[1] = last[0];
last[0] = code;
save = (save << 6) | rank;
state++;
if (state === 4) {
buffer[outptr++] = save >>> 16;
if (last[1] !== 61 /* padding character */) {
buffer[outptr++] = save >>> 8;
}
if (last[0] !== 61 /* padding character */) {
buffer[outptr++] = save;
}
state = 0;
}
}
}
// 2/3 chance there's going to be some null bytes at the end, but that
// doesn't really matter with most image formats.
// If it somehow matters for you, truncate the buffer up outptr.
return buffer.buffer;
}
;
if (Uint8Array) {
base64_ranks = new Uint8Array([
62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1
, -1, -1, 0, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25
, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35
, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
]);
}
if (HTMLCanvasElement && !HTMLCanvasElement.prototype.toBlob) {
HTMLCanvasElement.prototype.toBlob = function(callback, type /*, ...args*/) {
if (!type) {
type = "image/png";
} if (this.mozGetAsFile) {
callback(this.mozGetAsFile("canvas", type));
return;
}
var
args = Array.prototype.slice.call(arguments, 1)
, dataURI = this.toDataURL.apply(this, args)
, header_end = dataURI.indexOf(",")
, data = dataURI.substring(header_end + 1)
, is_base64 = is_base64_regex.test(dataURI.substring(0, header_end))
, blob
;
if (Blob.fake) {
// no reason to decode a data: URI that's just going to become a data URI again
blob = new Blob
if (is_base64) {
blob.encoding = "base64";
} else {
blob.encoding = "URI";
}
blob.data = data;
blob.size = data.length;
} else if (Uint8Array) {
if (is_base64) {
blob = new Blob([decode_base64(data)], {type: type});
} else {
blob = new Blob([decodeURIComponent(data)], {type: type});
}
}
callback(blob);
};
}
}(self));

View File

@ -8,24 +8,6 @@
ns.GithubStorageService.prototype.init = function () {};
ns.GithubStorageService.prototype.store = function (callbacks) {
var xhr = new XMLHttpRequest();
var formData = new FormData();
formData.append('framesheet_content', this.piskelController.serialize());
formData.append('fps_speed', this.piskelController.getFPS());
xhr.open('POST', Constants.STATIC.URL.SAVE, true);
xhr.onload = function(e) {
if (this.status == 200) {
var baseUrl = window.location.href.replace(window.location.search, "");
window.location.href = baseUrl + "?frameId=" + this.responseText;
} else {
this.onerror(e);
}
};
xhr.onerror = function(e) {
$.publish(Events.SHOW_NOTIFICATION, [{"content": "Saving failed ("+this.status+")"}]);
};
xhr.send(formData);
throw "Github save is no longer available. Use local save instead";
};
})();

51
src/js/utils/Base64.js Normal file
View File

@ -0,0 +1,51 @@
(function () {
var ns = $.namespace('pskl.utils');
var base64_ranks;
if (Uint8Array) {
base64_ranks = new Uint8Array([
62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1,
-1, -1, 0, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
-1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
]);
}
ns.Base64 = {
decode : function(base64) {
var outptr = 0;
var last = [0, 0];
var state = 0;
var save = 0;
var undef;
var len = base64.length, i = 0;
var buffer = new Uint8Array(len / 4 * 3 | 0);
while (len--) {
var code = base64.charCodeAt(i++);
var rank = base64_ranks[code-43];
if (rank !== 255 && rank !== undef) {
last[1] = last[0];
last[0] = code;
save = (save << 6) | rank;
state++;
if (state === 4) {
buffer[outptr++] = save >>> 16;
if (last[1] !== 61 /* padding character */) {
buffer[outptr++] = save >>> 8;
}
if (last[0] !== 61 /* padding character */) {
buffer[outptr++] = save;
}
state = 0;
}
}
}
// 2/3 chance there's going to be some null bytes at the end, but that
// doesn't really matter with most image formats.
// If it somehow matters for you, truncate the buffer up outptr.
return buffer.buffer;
}
};
})();

View File

@ -0,0 +1,38 @@
(function () {
var ns = $.namespace('pskl.utils');
var BASE64_REGEX = /\s*;\s*base64\s*(?:;|$)/i;
ns.ImageToBlob = {
imageDataToBlob : function(dataURI, type, callback) {
var header_end = dataURI.indexOf(","),
data = dataURI.substring(header_end + 1),
isBase64 = BASE64_REGEX.test(dataURI.substring(0, header_end)),
blob;
if (Blob.fake) {
// no reason to decode a data: URI that's just going to become a data URI again
blob = new Blob();
blob.encoding = isBase64 ? "base64" : "URI";
blob.data = data;
blob.size = data.length;
} else if (Uint8Array) {
var blobData = isBase64 ? pskl.utils.Base64.decode(data) : decodeURIComponent(data);
blob = new Blob([blobData], {type: type});
}
callback(blob);
},
canvasToBlob : function(canvas, callback, type /*, ...args*/) {
type = type || "image/png";
if (this.mozGetAsFile) {
callback(this.mozGetAsFile("canvas", type));
} else {
var args = Array.prototype.slice.call(arguments, 2);
var dataURI = this.toDataURL.apply(this, args);
pskl.utils.ImageToBlob.imageDataToBlob(dataURI, type, callback);
}
}
};
})();

View File

@ -11,7 +11,6 @@
// JSZip https://github.com/Stuk/jszip
"js/lib/jszip/jszip.min.js",
"js/lib/canvastoblob/canvasToBlob.js",
// Spectrum color-picker library
"js/lib/spectrum/spectrum.js",
@ -24,12 +23,14 @@
// Libraries
"js/utils/core.js",
"js/utils/UserAgent.js",
"js/utils/Base64.js",
"js/utils/CanvasUtils.js",
"js/utils/Dom.js",
"js/utils/Math.js",
"js/utils/FileUtils.js",
"js/utils/FrameUtils.js",
"js/utils/LayerUtils.js",
"js/utils/ImageToBlob.js",
"js/utils/ImageResizer.js",
"js/utils/PixelUtils.js",
"js/utils/Template.js",

View File

@ -29,9 +29,9 @@
<option value="4">Enabled - 4px wide</option>
</select>
</div>
<div class="settings-item">
<!-- <div class="settings-item">
<label for="tiled-preview">Display tiled preview :</label>
<input type="checkbox" value="1" id="tiled-preview" name="tiled-preview-checkbox"/>
</div>
</div> -->
</div>

View File

@ -3,17 +3,19 @@
Export to Animated GIF
</div>
<div class="settings-item">
<form action="" method="POST" class="gif-export-form">
<label>Select resolution:</label>
<form action="" method="POST" name="gif-export-upload-form">
<script type="text/template" id="gif-export-radio-template">
<label style="display:block"><input type="radio" name="gif-zoom-level" value="{{value}}"/>
{{label}}</label>
</script>
<div class="gif-export-radio-group"></div>
<input type="submit" class="button button-primary gif-upload-button" value="Upload" />
<button type="button" class="button button-primary gif-download-button">Download GIF</button>
<button type="button" class="button button gif-upload-button">Export online</button>
</form>
<span class="gif-export-progress-status"></span>
<div class="gif-export-progress-bar"></div>
</form>
<div class="gif-export-preview"></div>
<div class="gif-upload-status"></div>
</div>

View File

@ -6,8 +6,8 @@
<span>Preview : </span>
<div class="png-export-preview"></div>
<div class="png-export-radio-group"></div>
<input type="button" class="button button-primary png-download-button" value="Download PNG" />
<input type="button" class="button png-upload-button" value="Upload PNG" />
<button type="button" class="button button-primary png-download-button">Download PNG</button>
<button type="button" class="button png-upload-button">Export online</button>
<!-- <input type="button" class="button png-download-button" value="Download" /> -->
<div class="png-upload-status"></div>
</div>