Skip to content

Commit

Permalink
Merge pull request #3 from yeoji/feature/zip-download
Browse files Browse the repository at this point in the history
Implement support for zip download of multiple sprites
  • Loading branch information
yeoji authored Dec 21, 2023
2 parents 8a45beb + a7d9c62 commit f2cf529
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 36 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This project uses [Sprite Cow](https://github.com/jakearchibald/sprite-cow) as a
### Development

```
yarn install
yarn dev
```

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"sass": "^1.34.0"
},
"dependencies": {
"jquery": "^3.6.0"
"jquery": "^3.6.0",
"jszip": "^3.10.1"
}
}
9 changes: 9 additions & 0 deletions src/cutter/KeyboardEvents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const pressedKeys = {};
window.onkeyup = function(e) { pressedKeys[e.key] = false; }
window.onkeydown = function(e) { pressedKeys[e.key] = true; }

export function isKeyDown(key) {
return pressedKeys[key];
}

export const SHIFT_KEY = 'Shift';
59 changes: 44 additions & 15 deletions src/cutter/PreviewPanel.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import $ from 'jquery';
import JSZip from 'jszip';

import Rect from '../spritecow/Rect';
import InlineEdit from '../spritecow/InlineEdit';

class PreviewPanel {
Expand All @@ -16,8 +16,8 @@ class PreviewPanel {
this.$exportButton = this.$settings.find('#exportButton');

this.$spriteCanvas = spriteCanvas.canvas;
this.fileName = 'sprite.png';
this.rect = new Rect(0, 0, 0, 0);
this.fileName = 'sprite';
this.selectedSprites = [];
this._addEditEvents();

this.$exportButton.on('click', this.handleExport.bind(this));
Expand Down Expand Up @@ -48,17 +48,18 @@ class PreviewPanel {

$('<div class="panel-title">Settings</div>').appendTo(container);
$('<div>Name: <span id="fileName" data-inline-edit="file-name"/></div><br/>').appendTo(container);
$('<div><span id="selectedSpritesCount">0</span> sprite(s) selected!</div>').appendTo(container);
$('<div><input id="exportButton" type="button" value="Export" title="Export the selected sprite"></div>').appendTo(container);

return container;
}

update() {
var rect = this.rect;
var rect = this.selectedSprites[this.selectedSprites.length - 1].rect;
const previewCanvas = this.$previewCanvas;
const hiddenCanvas = this.$hiddenExportingCanvas;

this.$settings.find('#fileName').text(this.fileName);
this.$settings.find('#selectedSpritesCount').text(this.selectedSprites.length);
this.$properties.find('#topX').val(rect.x);
this.$properties.find('#topY').val(rect.y);
this.$properties.find('#width').val(rect.width);
Expand All @@ -71,27 +72,55 @@ class PreviewPanel {
this.$spriteCanvas, rect.x, rect.y, rect.width, rect.height,
0, 0, previewCanvas.width, previewCanvas.height
);
};

handleExport() {
if(this.selectedSprites.length > 1) {
this.createZipExport().then(base64 => {
const dataUrl = "data:application/zip;base64," + base64;
this.downloadExport(dataUrl, '.zip');
});
} else {
const image = this.createImageData(this.selectedSprites[0]);
this.downloadExport(image, '.png');
}
}

downloadExport(dataUrl, ext) {
var link = document.createElement('a');
link.download = this.fileName + ext;
link.href = dataUrl;

document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}

// we need a hidden canvas that will always match the current selected sprite's height/width so when exporting it is the correct size
createImageData(sprite) {
const rect = sprite.rect;
const hiddenCanvas = this.$hiddenExportingCanvas;
const hiddenCanvasContext = hiddenCanvas.getContext('2d');

hiddenCanvasContext.clearRect(0, 0, hiddenCanvas.width, hiddenCanvas.height);
hiddenCanvas.width = rect.width;
hiddenCanvas.height = rect.height;
hiddenCanvasContext.drawImage(
this.$spriteCanvas, rect.x, rect.y, rect.width, rect.height,
0, 0, hiddenCanvas.width, hiddenCanvas.height
);
};

handleExport() {
const image = this.$hiddenExportingCanvas.toDataURL("image/png");
var link = document.createElement('a');
link.download = this.fileName;
link.href = image;
return hiddenCanvas.toDataURL("image/png");
}

document.body.appendChild(link);
link.click();
document.body.removeChild(link);
createZipExport() {
var zip = new JSZip();

this.selectedSprites.forEach((sprite, i) => {
const base64Image = this.createImageData(sprite).split(';base64,')[1];
zip.file(`${i}.png`, base64Image, {base64: true});
});

return zip.generateAsync({type:"base64"});
}

_addEditEvents() {
Expand Down
54 changes: 44 additions & 10 deletions src/spritecow/SpriteCanvasView.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import $ from 'jquery';

import MicroEvent from './MicroEvent';
import Rect from './Rect';
import { SHIFT_KEY, isKeyDown } from '../cutter/KeyboardEvents';
import SelectedSprite from './extension/SelectedSprite';

var Highlight = (function() {
function Highlight($appendTo) {
Expand Down Expand Up @@ -30,7 +32,7 @@ var Highlight = (function() {
easing: 'easeOutQuad'
});
}
else {
else {
$container.vendorCss(destination);
}
}
Expand Down Expand Up @@ -61,6 +63,10 @@ var Highlight = (function() {
$container.css('display', 'none');
}
};

HighlightProto.remove = function() {
this._$container.remove();
}

HighlightProto.setHighVisOnDark = function(highVis) {
this._$container[highVis ? 'addClass' : 'removeClass']('high-vis');
Expand Down Expand Up @@ -187,6 +193,7 @@ var SelectArea = (function() {
if (!isDragging) { return; }
isDragging = false;
selectArea.trigger('select', rect);
selectArea._highlight.hide();
}
]);

Expand Down Expand Up @@ -216,25 +223,30 @@ class SpriteCanvasView {
// this cannot be $appendToElm, as browsers pick up clicks on scrollbars, some don't pick up mouseup http://code.google.com/p/chromium/issues/detail?id=14204#makechanges
highlight = new Highlight($container),
selectArea = new SelectArea($container, $canvas, highlight),
selectColor = new SelectColor($canvas, $canvas);
selectColor = new SelectColor($canvas, $canvas),
selectedSprites = [];

this._$container = $container;
this._$bgElm = $appendToElm;
this._spriteCanvas = spriteCanvas;
this._highlight = highlight;
this._selectArea = selectArea;
this._selectColor = selectColor;
this._selectedSprites = selectedSprites;

$container.appendTo($appendToElm);

selectArea.bind('select', function (rect) {
selectArea.bind('select', function (clickedRect) {
const rect = Object.assign({}, clickedRect);

var spriteRect = spriteCanvas.trimBg(rect);
if (spriteRect.width && spriteRect.height) { // false if clicked on bg pixel
spriteRect = spriteCanvas.expandToSpriteBoundry(rect);
spriteCanvasView._setCurrentRect(spriteRect);

spriteCanvasView._handleSelectedSprite(clickedRect, spriteRect);
}
else {
highlight.hide(true);
spriteCanvasView._unselectAllSprites();
}
});

Expand All @@ -251,16 +263,38 @@ class SpriteCanvasView {

var SpriteCanvasViewProto = SpriteCanvasView.prototype = new MicroEvent;

SpriteCanvasViewProto._setCurrentRect = function(rect) {
this._highlight.moveTo(rect, true);
this.trigger('rectChange', rect);
};
SpriteCanvasViewProto._handleSelectedSprite = function(clickedRect, spriteRect) {
if(isKeyDown(SHIFT_KEY)) {
const alreadySelectedSpriteIndex = this._selectedSprites.findIndex(sprite => JSON.stringify(sprite.rect) == JSON.stringify(spriteRect));
if(alreadySelectedSpriteIndex > -1) {
this._selectedSprites[alreadySelectedSpriteIndex].unselect();
this._selectedSprites.splice(alreadySelectedSpriteIndex, 1);
} else {
this._selectedSprites.push(this._selectSprite(clickedRect, spriteRect));
}
} else {
this._unselectAllSprites();
this._selectedSprites = [this._selectSprite(clickedRect, spriteRect)];
}

this.trigger('selectedSpritesChange', this._selectedSprites);
}

SpriteCanvasViewProto._selectSprite = function(clickedRect, spriteRect) {
const highlight = new Highlight(this._$container);
highlight.moveTo(clickedRect); // move to clicked area so the animation starts from click position

return new SelectedSprite(spriteRect, highlight);
}

SpriteCanvasViewProto._unselectAllSprites = function() {
this._selectedSprites.forEach(sprite => sprite.unselect());
}

SpriteCanvasViewProto.setTool = function(mode) {
var selectArea = this._selectArea,
selectColor = this._selectColor;

this._highlight.hide();
selectArea.deactivate();
selectColor.deactivate();

Expand Down
22 changes: 14 additions & 8 deletions src/spritecow/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,22 @@ import PreviewPanel from '../cutter/PreviewPanel';
pageLayout.toAppView();
});

spriteCanvasView.bind('rectChange', function(rect) {
previewPanel.rect = rect;
spriteCanvasView.bind('selectedSpritesChange', function(selectedSprites) {
if(selectedSprites.length === 0) {
return;
}

previewPanel.selectedSprites = selectedSprites;
previewPanel.update();

if (rect.width === spriteCanvas.canvas.width && rect.height === spriteCanvas.canvas.height) {
// if the rect is the same size as the whole canvas,
// it's probably because the background is set wrong
// let's be kind...
toolbarTop.feedback( 'Incorrect background colour set?', true );
}
selectedSprites.forEach(({rect}) => {
if (rect.width === spriteCanvas.canvas.width && rect.height === spriteCanvas.canvas.height) {
// if the rect is the same size as the whole canvas,
// it's probably because the background is set wrong
// let's be kind...
toolbarTop.feedback( 'Incorrect background colour set?', true );
}
});
});

spriteCanvasView.bind('bgColorHover', function(color) {
Expand Down
14 changes: 14 additions & 0 deletions src/spritecow/extension/SelectedSprite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class SelectedSprite {
constructor(rect, highlight) {
this.rect = rect;
this.highlight = highlight;

this.highlight.moveTo(rect, true);
}

unselect() {
this.highlight.remove();
}
}

export default SelectedSprite;
26 changes: 24 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2802,6 +2802,11 @@ ieee754@^1.1.4:
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==

immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==

import-fresh@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
Expand Down Expand Up @@ -3227,6 +3232,16 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"

jszip@^3.10.1:
version "3.10.1"
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2"
integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==
dependencies:
lie "~3.3.0"
pako "~1.0.2"
readable-stream "~2.3.6"
setimmediate "^1.0.5"

kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
Expand Down Expand Up @@ -3259,6 +3274,13 @@ levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"

lie@~3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
dependencies:
immediate "~3.0.5"

lodash.clone@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6"
Expand Down Expand Up @@ -3686,7 +3708,7 @@ pako@^0.2.5:
resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
integrity sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=

pako@~1.0.5:
pako@~1.0.2, pako@~1.0.5:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
Expand Down Expand Up @@ -4658,7 +4680,7 @@ set-value@^2.0.0, set-value@^2.0.1:
is-plain-object "^2.0.3"
split-string "^3.0.1"

setimmediate@^1.0.4:
setimmediate@^1.0.4, setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
Expand Down

0 comments on commit f2cf529

Please sign in to comment.