40 Commits

Author SHA1 Message Date
4e1f6bee3f Build : add grunt desktop-mac target 2015-08-15 16:29:13 +02:00
6a4d3cb106 Keyboard Cheatsheet : extract color shortcuts to dedicated category 2015-08-14 00:01:47 +02:00
7048e1fd42 Palette toolbox : 1-9 shortcut styling update 2015-08-13 01:01:20 +02:00
cd36c07a45 Add shortcut numbers for 1-9 palette colors 2015-08-13 00:44:47 +02:00
9f0aaceb5f Merge pull request #292 from juliandescottes/copy-paste-oob-crash
Copy paste oob crash
2015-08-09 15:55:12 +02:00
99da69553c Copy paste out of bounds : added SelectionManager unit tests 2015-08-09 15:42:46 +02:00
fdb5483e87 JSCS fixes 2015-08-09 12:51:25 +02:00
1208324d4d Copy paste bug : add unit tests for FrameUtils with null value 2015-08-09 12:37:03 +02:00
5437ad8651 Merge branch 'copy-paste-oob-crash' of https://github.com/juliandescottes/piskel into copy-paste-oob-crash 2015-08-09 01:49:47 +02:00
c074217047 Add macos specific nodewebkit configuration 2015-08-09 01:22:25 +02:00
e0c9a46ed3 wip : needs tests 2015-08-07 08:37:13 +02:00
d962217f90 Issue #281 : Add app.settings & user pref for layer preview opacity 2015-07-26 02:00:46 +02:00
9800d85cb7 Add keyboard shortcuts 1 to 9 to quickly select palette colors 2015-07-24 01:16:47 +02:00
011b07c735 Palette editor : Fix blur delegation on Firefox 2015-07-22 00:10:56 +02:00
2fdc85556b Palette Editor : Fix color change from hex input 2015-07-21 23:40:55 +02:00
7a8efc56b0 Import dialog : image-preview style : display changed to block 2015-06-07 13:19:44 +02:00
0d81865f3b Adding button to reset.css font rules 2015-06-07 12:45:51 +02:00
12cfe16cb4 Cleaning up settings and dialogs CSS 2015-06-07 12:40:40 +02:00
e773f9ae6d Merge pull request #284 from JALissiak/spritesheetImport
Adding spritesheet import - fixes #188
2015-06-04 08:09:13 +02:00
5c46cfe20a Updating for pull request feedback
- Using labels for the import type radio buttons
- Non animated gifs can now be imported as a spritesheet
- Fixing frame slicing to ignore a partial frame while looping
2015-06-03 19:48:29 -07:00
2d9001db6e Updating the spritesheet import to use size
- The import dialog now allows users to select an option between single image or spritesheet importing
- The spritesheet option allows setting of the size of an indivdual frame and the offset from the left/top from which to start slicing frames
- Selecting the spritesheet option will display a frame slice grid over the preview image to give a quick view of where the frames will be made
- When importing the spritesheet blank (transparent) frames and also partial frames will be ignored
- This allows users to import spritesheets that have been packed into a larger image with excess padding
2015-06-02 21:54:26 -07:00
8ff15fd0e1 Fixing the preview frame grid stroke
- The width/height of the canvas used to draw the frame grid in the preview was incorrect, so the stroke width was too thick
- This change fixes it so the stroke width remains nice and thin by applying the correct canvas size
2015-06-01 10:50:58 -07:00
8e4ea8437f Fixing unnecessary whitespace changes
- My editor added additional whitespace to several unchanged lines, so I just reverted them
2015-06-01 10:38:10 -07:00
48f24c0cf3 Adding spritesheet import
- Updated the import dialog to allow users to specify the number of frames in the image (which defaults to 1 x and 1 y)
- Setting the frame count for x and y will draw a dotted line in the preview that shows where the image will be split into individual frames
- When imported with a frame count above 1, the source image will be split into the different frames and loaded just as if it were an animated gif
- This allows users to import existing spritesheet pngs, including those produced by the piskel export function
2015-06-01 10:29:52 -07:00
8d85093874 Fix GIF issue 2015-05-18 11:51:28 +02:00
1beeb8d6e4 Revert "Fix a GIF bug ... again"
This reverts commit f9b07b29a9.
2015-05-18 11:47:48 +02:00
f9b07b29a9 Fix a GIF bug ... again 2015-05-18 10:40:05 +02:00
9bc330e5e8 Merge branch 'master' of https://github.com/juliandescottes/piskel 2015-05-13 11:21:24 +02:00
a51e20b370 Fix #282 : Clean build of gif.js made the issue disappear ... 2015-05-13 11:20:44 +02:00
ef6ef6256e Merge branch 'master' of https://github.com/juliandescottes/piskel 2015-05-08 22:36:03 +02:00
4edbc29e72 Fix #281 : Zoom keyboard shortcuts on Firefox
Keycode not consistent on FF :
https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode

Also added support for numpad +/- keys.
2015-05-08 22:34:29 +02:00
b72c775a04 Merge pull request #275 from MaxEden/master
Zip-Export: Split by layers option
2015-05-04 16:05:42 +02:00
0c9f04bc71 Prepare release 0.5.2 2015-05-03 11:43:38 +02:00
034057dcd2 Moved subfunctions to prototype 2015-04-30 13:17:56 +06:00
016316518d Zip-Export: Split by layers option 2015-04-29 20:20:35 +06:00
ac9ccd04e2 Fix currentcolors on sprites with many frames 2015-04-29 04:00:46 +02:00
ce8d71f47e Fix : resize panel : maintain ratio is always on 2015-04-28 13:32:55 +02:00
29cd0d80f3 Prepare 0.5.1 release 2015-04-28 07:29:01 +02:00
d3f5a41c0d Fix popup preview on Firefox 2015-04-27 22:34:50 +02:00
3f181c6248 Fix gif export transparency issue 2015-04-27 22:23:35 +02:00
56 changed files with 1085 additions and 360 deletions

View File

@ -217,15 +217,24 @@ module.exports = function(grunt) {
}
},
nodewebkit: {
options: {
version : "0.11.5",
build_dir: './dest/desktop/', // destination folder of releases.
mac: true,
win: true,
linux32: true,
linux64: true
windows : {
options: {
version : "0.11.5",
build_dir: './dest/desktop/', // destination folder of releases.
win: true,
linux32: true,
linux64: true
},
src: ['./dest/**/*', "./package.json", "!./dest/desktop/"]
},
src: ['./dest/**/*', "./package.json", "!./dest/desktop/"]
macos : {
options: {
platforms : ['osx64'],
version : "0.10.5",
build_dir: './dest/desktop/'
},
src: ['./dest/**/*', "./package.json", "!./dest/desktop/"]
}
}
});
@ -250,7 +259,8 @@ module.exports = function(grunt) {
grunt.registerTask('default', ['clean:before', 'lint', 'build']);
// Build stand alone app with nodewebkit
grunt.registerTask('desktop', ['default', 'nodewebkit']);
grunt.registerTask('desktop', ['default', 'nodewebkit:windows']);
grunt.registerTask('desktop-mac', ['default', 'nodewebkit:macos']);
// Start webserver and watch for changes
grunt.registerTask('serve', ['build', 'express:regular', 'open:regular', 'express-keepalive', 'watch']);

View File

@ -6,7 +6,9 @@ module.exports = function(config) {
var mapToSrcFolder = function (path) {return ['src', path].join('/');};
var piskelScripts = require('./src/piskel-script-list.js').scripts.map(mapToSrcFolder);
piskelScripts.push('test/js/testutils/**/*.js');
piskelScripts.push('test/js/**/*.js');
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)

View File

@ -10,7 +10,7 @@ SETLOCAL
set MISC_FOLDER=%PISKEL_HOME%\misc
set RELEASES_FOLDER=%PISKEL_HOME%\dest\desktop
set DEST_FOLDER=%RELEASES_FOLDER%\piskel\win64
set DEST_FOLDER=%RELEASES_FOLDER%\piskel\win32
ECHO "Updating Piskel icon -- Using Resource Hacker"
%RESOURCE_HACKER_PATH%\ResHacker -addoverwrite "%DEST_FOLDER%\piskel.exe", "%DEST_FOLDER%\piskel-logo.exe", "%MISC_FOLDER%\desktop\logo.ico", ICONGROUP, IDR_MAINFRAME, 1033

View File

@ -3,7 +3,7 @@
"name": "piskel",
"main": "./dest/index.html",
"description": "Web based 2d animations editor",
"version": "0.5.0",
"version": "0.5.2",
"homepage": "http://github.com/juliandescottes/piskel",
"repository": {
"type": "git",

View File

@ -51,11 +51,15 @@
overflow: auto;
}
.cheatsheet-container h3 {
.cheatsheet-container .cheatsheet-title {
font-size:24px;
margin-top: 0;
}
.cheatsheet-container .cheatsheet-title:nth-of-type(2) {
margin-top: 30px;
}
.cheatsheet-section {
float: left;
width : 33%;

View File

@ -2,17 +2,65 @@
/* Import dialog */
/************************************************************************************************/
#dialog-container.import-image {
width: 550px;
height: 360px;
top : 50%;
left : 50%;
position : absolute;
margin-left: -250px;
}
.show #dialog-container.import-image {
margin-top: -150px;
}
.import-subsection {
margin-left: 25px;
}
.import-section:not(.import-subsection) > .dialog-section-title {
width: 50px;
}
.import-section-preview-title {
position: absolute;
margin-left: 50%;
margin-top: -28px;
}
.import-section-preview {
display : inline-block;
height : 60px;
width: 60px;
position: absolute;
display: inline-block;
border: 1px dashed #999;
border-radius: 3px;
margin-left: 50%;
}
.import-section-preview img {
max-width: 220px;
max-height: 220px;
display: block;
}
.import-section-preview.no-border {
border-color: transparent;
}
.import-section-preview canvas {
position: absolute;
left: 0;
top: 0;
}
.dialog-section-title {
display : inline-block;
width: 55px;
width: 80px;
}
.dialog-section-radio {
margin-top: 15px;
vertical-align: sub;
}
.import-size-field:nth-of-type(2) {
@ -23,12 +71,17 @@
display: inline-block;
overflow: hidden;
height: 2rem;
word-break : break-all;
width: 200px;
vertical-align: middle;
word-break : break-all;
white-space: nowrap;
text-overflow: ellipsis;
font-style: italic;
font-weight: normal;
text-shadow: none;
color: gold;
}
[name=smooth-resize-checkbox] {
@ -36,12 +89,13 @@
}
.dialog-import-body {
padding:10px 20px;
font-size:1.5em
padding: 10px 20px;
font-size:1.3em
}
.import-button {
font-size: 1em;
height: auto;
padding: 5px 10px;
height: 28px;
padding: 0px 10px;
margin-top: 15px;
}

View File

@ -65,19 +65,6 @@
margin-top: -250px;
}
#dialog-container.import-image {
width: 500px;
height: 300px;
top : 50%;
left : 50%;
position : absolute;
margin-left: -250px;
}
.show #dialog-container.import-image {
margin-top: -150px;
}
.dialog-wrapper {
height: 100%;
position : relative;

View File

@ -10,6 +10,8 @@
border-radius : 2px;
padding : 3px 10px;
color : white;
height: 23px;
}
.textfield[disabled=disabled] {

View File

@ -23,8 +23,8 @@
-o-transition: all 500ms ease-out;
transition: all 500ms ease-out;
background-image: linear-gradient(45deg, rgba(0,0,0, 0.8) 25%, transparent 25%, transparent 75%, rgba(0,0,0, 0.8) 75%, rgba(0,0,0, 0.8)),
linear-gradient(-45deg, rgba(0,0,0, 0.8) 25%, transparent 25%, transparent 75%, rgba(0,0,0, 0.8) 75%, rgba(0,0,0, 0.8));
background-image: linear-gradient(45deg, #1D1D1D 20%, transparent 25%, transparent 75%, #1D1D1D 80%, #1D1D1D),
linear-gradient(-45deg, #1D1D1D 20%, transparent 25%, transparent 75%, #1D1D1D 80%, #1D1D1D);
background-size: 29px 45px;
background-repeat: repeat-x;
background-position-x: 3px;

View File

@ -121,7 +121,6 @@
background: url(../img/canvas_background/lowcont_dark_canvas_background.png) repeat;
}
.layers-canvas,
.canvas.onion-skin-canvas {
opacity: 0.2;
}

View File

@ -3,7 +3,7 @@ html, body {
margin : 0;
overflow: hidden;
cursor : default;
font-family: arial;
font-family: Arial;
font-size: 11px;
-webkit-touch-callout: none;
-webkit-user-select: none;
@ -19,6 +19,12 @@ ul, li {
list-style-type: none;
}
/** Firefox overrides this with -moz-use-system-font */
button,
input,
input[type="submit"] {
font-family: Arial;
}
/* Force apparition of scrollbars on leopard */
::-webkit-scrollbar {

View File

@ -1,4 +1,61 @@
.tiled-preview-checkbox {
vertical-align: -2px;
margin-left: 0;
/*******************************/
/* Application Setting panel */
/*******************************/
.background-picker-wrapper {
overflow: hidden;
padding: 5px 5px 2px 5px;
}
.background-picker {
cursor: pointer;
float: left;
height: 35px;
width: 35px;
background-color: transparent;
margin-right: 15px;
padding: 1px;
position: relative;
}
.background-picker:after {
content: " ";
position: absolute;
top: -2px;
right: -2px;
bottom: -2px;
left: -2px;
}
.background-picker:hover:after {
border: #eee 1px solid;
}
.background-picker.selected:after {
border: gold 1px solid;
}
.layer-opacity-input {
margin: 5px;
vertical-align: middle;
width: 145px;
}
.layer-opacity-text {
height: 31px;
display: inline-block;
line-height: 30px;
width: 40px;
border: 1px solid grey;
box-sizing: border-box;
border-radius: 3px;
text-align: center;
}
.grid-width-select {
margin: 5px;
}
.settings-section--application-general > .settings-item > label {
display: block;
}

View File

@ -5,17 +5,6 @@
width: 25%;
}
.resize-content-checkbox {
margin-left: 0;
}
.resize-ratio-checkbox,
.resize-smooth-checkbox {
vertical-align: -2px;
margin-left: 0;
}
/*****************/
/* ANCHOR WIDGET */
/*****************/

View File

@ -147,43 +147,6 @@
vertical-align: middle;
}
/************************************************************************************************/
/* Application settings */
/************************************************************************************************/
.background-picker-wrapper {
overflow: hidden;
padding: 5px;
}
.background-picker {
cursor: pointer;
float: left;
height: 35px;
width: 35px;
background-color: transparent;
margin-right: 15px;
padding: 1px;
position: relative;
}
.background-picker:after {
content: " ";
position: absolute;
top: -2px;
right: -2px;
bottom: -2px;
left: -2px;
}
.background-picker:hover:after {
border: #eee 1px solid;
}
.background-picker.selected:after {
border: gold 1px solid;
}
/************************************************************************************************/
/* Save panel */
/************************************************************************************************/

View File

@ -50,3 +50,8 @@ body {
.uppercase {
text-transform: uppercase;
}
.checkbox-fix {
vertical-align: -2px;
margin-left: 0;
}

View File

@ -15,6 +15,52 @@
height: 32px;
position: relative;
}
.palettes-list-color:nth-child(-n+10):after {
position: absolute;
top: 0;
right: 0;
background-color: black;
color: gold;
font-family: Tahoma;
font-size: 0.5em;
font-weight: bold;
padding: 2px 3px 2px 3px;
border-radius: 0 0 0 2px;
}
.palettes-list-color:nth-child(1):after {
content: "1";
}
.palettes-list-color:nth-child(2):after {
content: "2";
}
.palettes-list-color:nth-child(3):after {
content: "3";
}
.palettes-list-color:nth-child(4):after {
content: "4";
}
.palettes-list-color:nth-child(5):after {
content: "5";
}
.palettes-list-color:nth-child(6):after {
content: "6";
}
.palettes-list-color:nth-child(7):after {
content: "7";
}
.palettes-list-color:nth-child(8):after {
content: "8";
}
.palettes-list-color:nth-child(9):after {
content: "9";
}
.palettes-list-color:nth-child(-n+5) {
margin-top: 5px;
}

View File

@ -3,7 +3,8 @@ var Constants = {
DEFAULT : {
HEIGHT : 32,
WIDTH : 32,
FPS : 12
FPS : 12,
LAYER_OPACITY : 0.2
},
MODEL_VERSION : 2,

View File

@ -192,9 +192,7 @@
getFirstFrameAsPng : function () {
var firstFrame = this.piskelController.getFrameAt(0);
var canvasRenderer = new pskl.rendering.CanvasRenderer(firstFrame, 1);
canvasRenderer.drawTransparentAs('rgba(0,0,0,0)');
var firstFrameCanvas = canvasRenderer.render();
var firstFrameCanvas = pskl.utils.FrameUtils.toImage(firstFrame);
return firstFrameCanvas.toDataURL('image/png');
},

View File

@ -127,6 +127,13 @@
ns.LayersListController.prototype.toggleLayerPreview_ = function () {
var currentValue = pskl.UserSettings.get(pskl.UserSettings.LAYER_PREVIEW);
pskl.UserSettings.set(pskl.UserSettings.LAYER_PREVIEW, !currentValue);
var currentLayerOpacity = pskl.UserSettings.get(pskl.UserSettings.LAYER_OPACITY);
var showLayerPreview = !currentValue;
pskl.UserSettings.set(pskl.UserSettings.LAYER_PREVIEW, showLayerPreview);
if (showLayerPreview && currentLayerOpacity === 0) {
pskl.UserSettings.set(pskl.UserSettings.LAYER_OPACITY, Constants.DEFAULT.LAYER_OPACITY);
}
};
})();

View File

@ -40,6 +40,7 @@
pskl.app.shortcutService.addShortcuts(['>', 'shift+>'], this.selectNextColor_.bind(this));
pskl.app.shortcutService.addShortcut('<', this.selectPreviousColor_.bind(this));
pskl.app.shortcutService.addShortcuts('123465789'.split(''), this.selectColorForKey_.bind(this));
this.fillPaletteList();
this.updateFromUserSettings();
@ -118,6 +119,12 @@
return currentIndex;
};
ns.PalettesListController.prototype.selectColorForKey_ = function (key) {
var index = parseInt(key, 10);
index = (index + 9) % 10;
this.selectColor_(index);
};
ns.PalettesListController.prototype.selectColor_ = function (index) {
var colors = this.getSelectedPaletteColors_();
var color = colors[index];

View File

@ -1,6 +1,5 @@
(function () {
var ns = $.namespace('pskl.controller.dialogs');
var PREVIEW_HEIGHT = 60;
ns.ImportImageController = function (piskelController) {
this.importedImage_ = null;
@ -18,12 +17,25 @@
this.fileNameContainer = $('.import-image-file-name');
this.importType = $('[name=import-type]');
this.resizeWidth = $('[name=resize-width]');
this.resizeHeight = $('[name=resize-height]');
this.smoothResize = $('[name=smooth-resize-checkbox]');
this.frameSizeX = $('[name=frame-size-x]');
this.frameSizeY = $('[name=frame-size-y]');
this.frameOffsetX = $('[name=frame-offset-x]');
this.frameOffsetY = $('[name=frame-offset-y]');
this.importType.change(this.onImportTypeChange_.bind(this));
this.resizeWidth.keyup(this.onResizeInputKeyUp_.bind(this, 'width'));
this.resizeHeight.keyup(this.onResizeInputKeyUp_.bind(this, 'height'));
this.frameSizeX.keyup(this.onFrameInputKeyUp_.bind(this, 'frameSizeX'));
this.frameSizeY.keyup(this.onFrameInputKeyUp_.bind(this, 'frameSizeY'));
this.frameOffsetX.keyup(this.onFrameInputKeyUp_.bind(this, 'frameOffsetX'));
this.frameOffsetY.keyup(this.onFrameInputKeyUp_.bind(this, 'frameOffsetY'));
this.importImageForm = $('[name=import-image-form]');
this.importImageForm.submit(this.onImportFormSubmit_.bind(this));
@ -31,6 +43,20 @@
pskl.utils.FileUtils.readImageFile(this.file_, this.onImageLoaded_.bind(this));
};
ns.ImportImageController.prototype.onImportTypeChange_ = function (evt) {
if (this.getImportType_() === 'single') {
// Using single image, so remove the frame grid
this.hideFrameGrid_();
} else {
// Using spritesheet import, so draw the frame grid in the preview
var x = this.sanitizeInputValue_(this.frameOffsetX, 0);
var y = this.sanitizeInputValue_(this.frameOffsetY, 0);
var w = this.sanitizeInputValue_(this.frameSizeX, 1);
var h = this.sanitizeInputValue_(this.frameSizeY, 1);
this.drawFrameGrid_(x, y, w, h);
}
};
ns.ImportImageController.prototype.onImportFormSubmit_ = function (evt) {
evt.originalEvent.preventDefault();
this.importImageToPiskel_();
@ -42,6 +68,12 @@
}
};
ns.ImportImageController.prototype.onFrameInputKeyUp_ = function (from, evt) {
if (this.importedImage_) {
this.synchronizeFrameFields_(evt.target.value, from);
}
};
ns.ImportImageController.prototype.synchronizeResizeFields_ = function (value, from) {
value = parseInt(value, 10);
if (isNaN(value)) {
@ -49,6 +81,10 @@
}
var height = this.importedImage_.height;
var width = this.importedImage_.width;
// Select single image import type since the user changed a value here
this.importType.filter('[value="single"]').attr('checked', 'checked');
if (from === 'width') {
this.resizeHeight.val(Math.round(value * height / width));
} else {
@ -56,6 +92,38 @@
}
};
ns.ImportImageController.prototype.synchronizeFrameFields_ = function (value, from) {
value = parseInt(value, 10);
if (isNaN(value)) {
value = 0;
}
// Parse the frame input values
var frameSizeX = this.sanitizeInputValue_(this.frameSizeX, 1);
var frameSizeY = this.sanitizeInputValue_(this.frameSizeY, 1);
var frameOffsetX = this.sanitizeInputValue_(this.frameOffsetX, 0);
var frameOffsetY = this.sanitizeInputValue_(this.frameOffsetY, 0);
// Select spritesheet import type since the user changed a value here
this.importType.filter('[value="sheet"]').attr('checked', 'checked');
// Draw the grid
this.drawFrameGrid_(frameOffsetX, frameOffsetY, frameSizeX, frameSizeY);
};
ns.ImportImageController.prototype.sanitizeInputValue_ = function(input, minValue) {
var value = parseInt(input.val(), 10);
if (value <= minValue || isNaN(value)) {
input.val(minValue);
value = minValue;
}
return value;
};
ns.ImportImageController.prototype.getImportType_ = function () {
return this.importType.filter(':checked').val();
};
ns.ImportImageController.prototype.onImageLoaded_ = function (image) {
this.importedImage_ = image;
@ -68,11 +136,18 @@
var fileName = this.extractFileNameFromPath_(this.file_.name);
this.fileNameContainer.html(fileName);
this.fileNameContainer.attr('title', fileName);
this.resizeWidth.val(w);
this.resizeHeight.val(h);
this.frameSizeX.val(w);
this.frameSizeY.val(h);
this.frameOffsetX.val(0);
this.frameOffsetY.val(0);
this.importPreview.width('auto');
this.importPreview.height('auto');
this.importPreview.html('');
this.importPreview.append(this.createImagePreview_());
};
@ -80,7 +155,6 @@
ns.ImportImageController.prototype.createImagePreview_ = function () {
var image = document.createElement('IMG');
image.src = this.importedImage_.src;
image.setAttribute('height', PREVIEW_HEIGHT);
return image;
};
@ -104,16 +178,32 @@
gif : image
});
var resizeW = this.resizeWidth.val();
var resizeH = this.resizeHeight.val();
gifLoader.load({
success : function () {
var images = gifLoader.getFrames().map(function (frame) {
return pskl.utils.CanvasUtils.createFromImageData(frame.data);
});
this.createPiskelFromImages_(images);
if (this.getImportType_() === 'single' || images.length > 1) {
// Single image import or animated gif
this.createPiskelFromImages_(images, resizeW, resizeH);
} else {
// Spritesheet
this.createImagesFromSheet_(images[0]);
}
this.closeDialog();
}.bind(this),
error : function () {
this.createPiskelFromImages_([image]);
error: function () {
if (this.getImportType_() === 'single') {
// Single image
this.createPiskelFromImages_([image], resizeW, resizeH);
} else {
// Spritesheet
this.createImagesFromSheet_(image);
}
this.closeDialog();
}.bind(this)
});
@ -122,9 +212,24 @@
}
};
ns.ImportImageController.prototype.createFramesFromImages_ = function (images) {
var w = this.resizeWidth.val();
var h = this.resizeHeight.val();
ns.ImportImageController.prototype.createImagesFromSheet_ = function (image) {
var x = this.sanitizeInputValue_(this.frameOffsetX, 0);
var y = this.sanitizeInputValue_(this.frameOffsetY, 0);
var w = this.sanitizeInputValue_(this.frameSizeX, 1);
var h = this.sanitizeInputValue_(this.frameSizeY, 1);
var images = pskl.utils.CanvasUtils.createFramesFromImage(
image,
x,
y,
w,
h,
/*useHorizonalStrips=*/ true,
/*ignoreEmptyFrames=*/ true);
this.createPiskelFromImages_(images, w, h);
};
ns.ImportImageController.prototype.createFramesFromImages_ = function (images, w, h) {
var smoothing = !!this.smoothResize.prop('checked');
var frames = images.map(function (image) {
@ -134,8 +239,8 @@
return frames;
};
ns.ImportImageController.prototype.createPiskelFromImages_ = function (images) {
var frames = this.createFramesFromImages_(images);
ns.ImportImageController.prototype.createPiskelFromImages_ = function (images, w, h) {
var frames = this.createFramesFromImages_(images, w, h);
var layer = pskl.model.Layer.fromFrames('Layer 1', frames);
var descriptor = new pskl.model.piskel.Descriptor('Imported piskel', '');
var piskel = pskl.model.Piskel.fromLayers([layer], descriptor);
@ -143,4 +248,72 @@
pskl.app.piskelController.setPiskel(piskel);
pskl.app.previewController.setFPS(Constants.DEFAULT.FPS);
};
ns.ImportImageController.prototype.drawFrameGrid_ = function (frameX, frameY, frameW, frameH) {
if (!this.importedImage_) {
return;
}
// Grab the sizes of the source and preview images
var width = this.importedImage_.width;
var height = this.importedImage_.height;
var previewWidth = this.importPreview.width();
var previewHeight = this.importPreview.height();
var canvasWrapper = this.importPreview.children('canvas');
var canvas = canvasWrapper.get(0);
if (!canvasWrapper.length) {
// Create a new canvas for the grid
canvas = pskl.utils.CanvasUtils.createCanvas(
previewWidth + 1,
previewHeight + 1);
this.importPreview.append(canvas);
canvasWrapper = $(canvas);
}
var context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
// Calculate the number of whole frames
var countX = Math.floor((width - frameX) / frameW);
var countY = Math.floor((height - frameY) / frameH);
if (countX > 0 && countY > 0) {
var scaleX = previewWidth / width;
var scaleY = previewHeight / height;
var maxWidth = countX * frameW + frameX;
var maxHeight = countY * frameH + frameY;
// Draw the vertical lines
for (var x = frameX + 0.5; x < maxWidth + 1 && x < width + 1; x += frameW) {
context.moveTo(x * scaleX, frameY * scaleY);
context.lineTo(x * scaleX, maxHeight * scaleY);
}
// Draw the horizontal lines
for (var y = frameY + 0.5; y < maxHeight + 1 && y < height + 1; y += frameH) {
context.moveTo(frameX * scaleX, y * scaleY);
context.lineTo(maxWidth * scaleX, y * scaleY);
}
// Set the line style to dashed
context.lineWidth = 1;
context.setLineDash([2, 1]);
context.strokeStyle = '#000000';
context.stroke();
// Show the canvas
canvasWrapper.show();
this.importPreview.addClass('no-border');
} else {
this.hideFrameGrid_();
}
};
ns.ImportImageController.prototype.hideFrameGrid_ = function() {
this.importPreview.children('canvas').hide();
this.importPreview.removeClass('no-border');
};
})();

View File

@ -21,7 +21,6 @@
ns.PopupPreviewController.prototype.open = function () {
if (!this.isOpen()) {
this.popup = window.open('about:blank', '', 'width=320,height=320');
this.popup.document.body.innerHTML = pskl.utils.Template.get('popup-preview-partial');
window.setTimeout(this.onPopupLoaded.bind(this), 500);
} else {
this.popup.focus();
@ -30,6 +29,7 @@
ns.PopupPreviewController.prototype.onPopupLoaded = function () {
this.popup.document.title = POPUP_TITLE;
this.popup.document.body.innerHTML = pskl.utils.Template.get('popup-preview-partial');
pskl.utils.Event.addEventListener(this.popup, 'resize', this.onWindowResize_, this);
pskl.utils.Event.addEventListener(this.popup, 'unload', this.onPopupClosed_, this);
var container = this.popup.document.querySelector('.preview-container');

View File

@ -25,9 +25,6 @@
};
ns.PreviewController.prototype.init = function () {
// the oninput event won't work on IE10 unfortunately, but at least will provide a
// consistent behavior across all other browsers that support the input type range
// see https://bugzilla.mozilla.org/show_bug.cgi?id=853670
this.fpsRangeInput.on('input change', this.onFPSSliderChange.bind(this));
document.querySelector('.right-column').style.width = Constants.ANIMATED_PREVIEW_WIDTH + 'px';

View File

@ -39,6 +39,12 @@
maxFpsInput.value = pskl.UserSettings.get(pskl.UserSettings.MAX_FPS);
this.addEventListener(maxFpsInput, 'change', this.onMaxFpsChange_);
// Layer preview opacity
var layerOpacityInput = document.querySelector('.layer-opacity-input');
layerOpacityInput.value = pskl.UserSettings.get(pskl.UserSettings.LAYER_OPACITY);
this.addEventListener(layerOpacityInput, 'change', this.onLayerOpacityChange_);
this.updateLayerOpacityText_(layerOpacityInput.value);
// Form
this.applicationSettingsForm = document.querySelector('[name="application-settings-form"]');
this.addEventListener(this.applicationSettingsForm, 'submit', this.onFormSubmit_);
@ -76,6 +82,23 @@
}
};
ns.ApplicationSettingsController.prototype.onLayerOpacityChange_ = function (evt) {
var target = evt.target;
var opacity = parseFloat(target.value);
if (!isNaN(opacity)) {
pskl.UserSettings.set(pskl.UserSettings.LAYER_OPACITY, opacity);
pskl.UserSettings.set(pskl.UserSettings.LAYER_PREVIEW, opacity !== 0);
this.updateLayerOpacityText_(opacity);
} else {
target.value = pskl.UserSettings.get(pskl.UserSettings.LAYER_OPACITY);
}
};
ns.ApplicationSettingsController.prototype.updateLayerOpacityText_ = function (opacity) {
var layerOpacityText = document.querySelector('.layer-opacity-text');
layerOpacityText.innerHTML = opacity;
};
ns.ApplicationSettingsController.prototype.onFormSubmit_ = function (evt) {
evt.preventDefault();
$.publish(Events.CLOSE_SETTINGS_DRAWER);

View File

@ -26,7 +26,6 @@
}
ns.GifExportController.prototype.init = function () {
this.optionTemplate_ = pskl.utils.Template.get('gif-export-option-template');
this.uploadStatusContainerEl = document.querySelector('.gif-upload-status');
this.previewContainerEl = document.querySelector('.gif-export-preview');

View File

@ -10,9 +10,11 @@
pskl.utils.inherit(ns.PngExportController, pskl.controller.settings.AbstractSettingController);
ns.PngExportController.prototype.init = function () {
this.pngFilePrefixInput = document.getElementById('zip-prefix-name');
this.pngFilePrefixInput = document.querySelector('.zip-prefix-name');
this.pngFilePrefixInput.value = 'sprite_';
this.splitByLayersCheckbox = document.querySelector('.zip-split-layers-checkbox');
var downloadButton = document.querySelector('.png-download-button');
this.addEventListener(downloadButton, 'click', this.onPngDownloadButtonClick_);
@ -30,12 +32,10 @@
ns.PngExportController.prototype.onZipButtonClick_ = function () {
var zip = new window.JSZip();
for (var i = 0 ; i < this.piskelController.getFrameCount() ; i++) {
var frame = this.piskelController.getFrameAt(i);
var canvas = this.getFrameAsCanvas_(frame);
var basename = this.pngFilePrefixInput.value;
var filename = basename + (i + 1) + '.png';
zip.file(filename, pskl.utils.CanvasUtils.getBase64FromCanvas(canvas) + '\n', {base64: true});
if (this.splitByLayersCheckbox.checked) {
this.splittedExport_(zip);
} else {
this.mergedExport_(zip);
}
var fileName = this.getPiskelName_() + '.zip';
@ -47,6 +47,30 @@
pskl.utils.FileUtils.downloadAsFile(blob, fileName);
};
ns.PngExportController.prototype.mergedExport_ = function (zip) {
for (var i = 0; i < this.piskelController.getFrameCount(); i++) {
var frame = this.piskelController.getFrameAt(i);
var canvas = this.getFrameAsCanvas_(frame);
var basename = this.pngFilePrefixInput.value;
var filename = basename + (i + 1) + '.png';
zip.file(filename, pskl.utils.CanvasUtils.getBase64FromCanvas(canvas) + '\n', {base64: true});
}
};
ns.PngExportController.prototype.splittedExport_ = function (zip) {
var layers = this.piskelController.getLayers();
for (var j = 0; this.piskelController.hasLayerAt(j); j++) {
var layer = this.piskelController.getLayerAt(j);
for (var i = 0; i < this.piskelController.getFrameCount(); i++) {
var frame = layer.getFrameAt(i);
var canvas = this.getFrameAsCanvas_(frame);
var basename = this.pngFilePrefixInput.value;
var filename = 'l' + j + '_' + basename + (i + 1) + '.png';
zip.file(filename, pskl.utils.CanvasUtils.getBase64FromCanvas(canvas) + '\n', {base64: true});
}
}
};
ns.PngExportController.prototype.getFrameAsCanvas_ = function (frame) {
var canvasRenderer = new pskl.rendering.CanvasRenderer(frame, 1);
canvasRenderer.drawTransparentAs(Constants.TRANSPARENT_COLOR);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -88,7 +88,7 @@
if (this.containsPixel(x, y)) {
var p = this.pixels[x][y];
if (p !== color) {
this.pixels[x][y] = color;
this.pixels[x][y] = color || Constants.TRANSPARENT_COLOR;
this.version++;
}
}

View File

@ -18,19 +18,9 @@
ns.CanvasRenderer.prototype.render = function () {
var canvas = this.createCanvas_();
var context = canvas.getContext('2d');
for (var x = 0, width = this.frame.getWidth(); x < width; x++) {
for (var y = 0, height = this.frame.getHeight(); y < height; y++) {
var color = this.frame.getPixel(x, y);
var w = 1;
while (color === this.frame.getPixel(x, y + w)) {
w++;
}
this.renderLine_(color, x, y, w, context);
y = y + w - 1;
}
}
// Draw in canvas
pskl.utils.FrameUtils.drawToCanvas(this.frame, canvas, this.transparentColor_);
var scaledCanvas = this.createCanvas_(this.zoom);
var scaledContext = scaledCanvas.getContext('2d');
@ -41,22 +31,6 @@
return scaledCanvas;
};
ns.CanvasRenderer.prototype.renderPixel_ = function (color, x, y, context) {
if (color == Constants.TRANSPARENT_COLOR) {
color = this.transparentColor_;
}
context.fillStyle = color;
context.fillRect(x, y, 1, 1);
};
ns.CanvasRenderer.prototype.renderLine_ = function (color, x, y, width, context) {
if (color == Constants.TRANSPARENT_COLOR) {
color = this.transparentColor_;
}
context.fillStyle = color;
context.fillRect(x, y, 1, width);
};
ns.CanvasRenderer.prototype.createCanvas_ = function (zoom) {
zoom = zoom || 1;
var width = this.frame.getWidth() * zoom;

View File

@ -219,18 +219,8 @@
this.canvas = pskl.utils.CanvasUtils.createCanvas(frame.getWidth(), frame.getHeight());
}
var context = this.canvas.getContext('2d');
for (var x = 0, width = frame.getWidth() ; x < width ; x++) {
for (var y = 0, height = frame.getHeight() ; y < height ; y++) {
var color = frame.getPixel(x, y);
var w = 1;
while (color === frame.getPixel(x, y + w)) {
w++;
}
this.renderLine_(color, x, y, w, context);
y = y + w - 1;
}
}
// Draw in canvas
pskl.utils.FrameUtils.drawToCanvas(frame, this.canvas);
this.updateMargins_(frame);
@ -264,18 +254,4 @@
}
displayContext.restore();
};
ns.FrameRenderer.prototype.renderPixel_ = function (color, x, y, context) {
if (color != Constants.TRANSPARENT_COLOR) {
context.fillStyle = color;
context.fillRect(x, y, 1, 1);
}
};
ns.FrameRenderer.prototype.renderLine_ = function (color, x, y, width, context) {
if (color != Constants.TRANSPARENT_COLOR) {
context.fillStyle = color;
context.fillRect(x, y, 1, width);
}
};
})();

View File

@ -18,7 +18,12 @@
this.serializedRendering = '';
this.stylesheet_ = document.createElement('style');
document.head.appendChild(this.stylesheet_);
this.updateLayersCanvasOpacity_(pskl.UserSettings.get(pskl.UserSettings.LAYER_OPACITY));
$.subscribe(Events.PISKEL_RESET, this.flush.bind(this));
$.subscribe(Events.USER_SETTINGS_CHANGED, $.proxy(this.onUserSettingsChange_, this));
};
pskl.utils.inherit(pskl.rendering.layer.LayersRenderer, pskl.rendering.CompositeRenderer);
@ -30,8 +35,8 @@
var currentFrameIndex = this.piskelController.getCurrentFrameIndex();
var currentLayerIndex = this.piskelController.getCurrentLayerIndex();
var downLayers = layers.slice(0, currentLayerIndex);
var upLayers = layers.slice(currentLayerIndex + 1, layers.length);
var belowLayers = layers.slice(0, currentLayerIndex);
var aboveLayers = layers.slice(currentLayerIndex + 1, layers.length);
var serializedRendering = [
this.getZoom(),
@ -40,8 +45,8 @@
offset.y,
size.width,
size.height,
this.getHashForLayersAt_(currentFrameIndex, downLayers),
this.getHashForLayersAt_(currentFrameIndex, upLayers),
this.getHashForLayersAt_(currentFrameIndex, belowLayers),
this.getHashForLayersAt_(currentFrameIndex, aboveLayers),
layers.length
].join('-');
@ -50,14 +55,14 @@
this.clear();
if (downLayers.length > 0) {
var downFrame = this.getFrameForLayersAt_(currentFrameIndex, downLayers);
this.belowRenderer.render(downFrame);
if (belowLayers.length > 0) {
var belowFrame = this.getFrameForLayersAt_(currentFrameIndex, belowLayers);
this.belowRenderer.render(belowFrame);
}
if (upLayers.length > 0) {
var upFrame = this.getFrameForLayersAt_(currentFrameIndex, upLayers);
this.aboveRenderer.render(upFrame);
if (aboveLayers.length > 0) {
var aboveFrame = this.getFrameForLayersAt_(currentFrameIndex, aboveLayers);
this.aboveRenderer.render(aboveFrame);
}
}
};
@ -89,6 +94,16 @@
return hash.join('-');
};
ns.LayersRenderer.prototype.onUserSettingsChange_ = function (evt, settingsName, settingsValue) {
if (settingsName == pskl.UserSettings.LAYER_OPACITY) {
this.updateLayersCanvasOpacity_(settingsValue);
}
};
ns.LayersRenderer.prototype.updateLayersCanvasOpacity_ = function (opacity) {
this.stylesheet_.innerHTML = '.layers-canvas { opacity : ' + opacity + '}';
};
ns.LayersRenderer.prototype.flush = function () {
this.serializedRendering = '';
};

View File

@ -25,8 +25,10 @@
ns.BaseSelection.prototype.fillSelectionFromFrame = function (targetFrame) {
this.pixels.forEach(function (pixel) {
pixel.color = targetFrame.getPixel(pixel.col, pixel.row);
var color = targetFrame.getPixel(pixel.col, pixel.row);
pixel.color = color || Constants.TRANSPARENT_COLOR;
});
this.hasPastedContent = true;
};
})();

View File

@ -88,20 +88,18 @@
};
ns.SelectionManager.prototype.paste = function() {
if (this.currentSelection && this.currentSelection.hasPastedContent) {
var pixels = this.currentSelection.pixels;
var opaquePixels = pixels.filter(function (p) {
return p.color !== Constants.TRANSPARENT_COLOR;
});
this.pastePixels(opaquePixels);
if (!this.currentSelection || !this.currentSelection.hasPastedContent) {
return;
}
};
ns.SelectionManager.prototype.pastePixels = function(pixels) {
var currentFrame = this.piskelController.getCurrentFrame();
var pixels = this.currentSelection.pixels;
var frame = this.piskelController.getCurrentFrame();
pixels.forEach(function (pixel) {
currentFrame.setPixel(pixel.col, pixel.row, pixel.color);
if (pixel.color === Constants.TRANSPARENT_COLOR || pixel.color === null) {
return;
}
frame.setPixel(pixel.col, pixel.row, pixel.color);
});
$.publish(Events.PISKEL_SAVE_STATE, {
@ -115,8 +113,7 @@
};
ns.SelectionManager.prototype.replay = function (frame, replayData) {
var pixels = replayData.pixels;
pixels.forEach(function (pixel) {
replayData.pixels.forEach(function (pixel) {
var color = replayData.type === SELECTION_REPLAY.PASTE ? pixel.color : Constants.TRANSPARENT_COLOR;
frame.setPixel(pixel.col, pixel.row, color);
});

View File

@ -48,15 +48,32 @@
}
};
var batchAll = function (frames, job) {
var batches = [];
frames = frames.slice(0);
while (frames.length) {
batches.push(frames.splice(0, 10));
}
var result = Q([]);
batches.forEach(function (batch) {
result = result.then(function (results) {
return Q.all(batch.map(job)).then(function (partials) {
return results.concat(partials);
});
});
});
return result;
};
ns.CurrentColorsService.prototype.updateCurrentColors_ = function () {
var layers = this.piskelController.getLayers();
var frames = layers.map(function (l) {return l.getFrames();}).reduce(function (p, n) {return p.concat(n);});
Q.all(
frames.map(function (frame) {
return this.cachedFrameProcessor.get(frame);
}.bind(this))
).done(function (results) {
var job = function (frame) {
return this.cachedFrameProcessor.get(frame);
}.bind(this);
batchAll(frames, job).then(function (results) {
var colors = {};
results.forEach(function (result) {
Object.keys(result).forEach(function (color) {

View File

@ -60,6 +60,7 @@
ns.CheatsheetService.prototype.initMarkup_ = function () {
this.initMarkupForTools_();
this.initMarkupForMisc_();
this.initMarkupForColors_();
this.initMarkupForSelection_();
};
@ -76,8 +77,6 @@
var descriptors = [
this.toDescriptor_('0', 'Reset zoom level'),
this.toDescriptor_('+/-', 'Zoom in/Zoom out'),
this.toDescriptor_('X', 'Swap primary/secondary colors'),
this.toDescriptor_('D', 'Reset default colors'),
this.toDescriptor_('ctrl + Z', 'Undo'),
this.toDescriptor_('ctrl + Y', 'Redo'),
this.toDescriptor_('&#65514;', 'Select previous frame'), /* ASCII for up-arrow */
@ -85,8 +84,6 @@
this.toDescriptor_('N', 'Create new frame'),
this.toDescriptor_('shift + N', 'Duplicate selected frame'),
this.toDescriptor_('shift + ?', 'Open/Close this popup'),
this.toDescriptor_('alt + P', 'Create a Palette'),
this.toDescriptor_('&lt;/&gt;', 'Select previous/next palette color'),
this.toDescriptor_('alt + O', 'Toggle Onion Skin'),
this.toDescriptor_('alt + L', 'Toggle Layer Preview')
];
@ -95,6 +92,19 @@
this.initMarkupForDescriptors_(descriptors, container);
};
ns.CheatsheetService.prototype.initMarkupForColors_ = function () {
var descriptors = [
this.toDescriptor_('X', 'Swap primary/secondary colors'),
this.toDescriptor_('D', 'Reset default colors'),
this.toDescriptor_('alt + P', 'Create a Palette'),
this.toDescriptor_('&lt;/&gt;', 'Select prev/next palette color'),
this.toDescriptor_('1 to 9', 'Select palette color at index')
];
var container = this.cheatsheetEl.querySelector('.cheatsheet-colors-shortcuts');
this.initMarkupForDescriptors_(descriptors, container);
};
ns.CheatsheetService.prototype.initMarkupForSelection_ = function () {
var descriptors = [
this.toDescriptor_('ctrl + X', 'Cut selection'),

View File

@ -7,7 +7,15 @@
40 : 'down',
46 : 'del',
189 : '-',
// 109 for numpad -
109 : '-',
// 173 on Firefox for minus key
173 : '-',
187 : '+',
// 107 for numpad +
107 : '+',
// 61 on Firefox for =/+ key
61 : '+',
188 : '<',
190 : '>'
};
@ -19,6 +27,9 @@
if (keycode >= 48 && keycode <= 57) {
// key is 0-9
return (keycode - 48) + '';
} else if (keycode >= 96 && keycode <= 105) {
// key is numpad 0-9
return (keycode - 96) + '';
} else if (keycode >= 65 && keycode <= 90) {
// key is a-z, use base 36 to get the string representation
return (keycode - 65 + 10).toString(36);

View File

@ -1,43 +1 @@
(function () {
var flipFrame = function (frame, horizontal, vertical) {
var clone = frame.clone();
var w = frame.getWidth();
var h = frame.getHeight();
clone.forEachPixel(function (color, x, y) {
if (horizontal) {
x = w - x - 1;
}
if (vertical) {
y = h - y - 1;
}
frame.pixels[x][y] = color;
});
frame.version++;
};
window.flip = function (horizontal, vertical) {
var currentFrameIndex = pskl.app.piskelController.getCurrentFrameIndex();
var layers = pskl.app.piskelController.getLayers();
layers.forEach(function (layer) {
flipFrame(layer.getFrameAt(currentFrameIndex), horizontal, vertical);
});
$.publish(Events.PISKEL_RESET);
$.publish(Events.PISKEL_SAVE_STATE, {
type : pskl.service.HistoryService.SNAPSHOT
});
};
window.copyToAll = function () {
var ref = pskl.app.piskelController.getCurrentFrame();
var layer = pskl.app.piskelController.getCurrentLayer();
layer.getFrames().forEach(function (frame) {
if (frame !== ref) {
frame.setPixels(ref.getPixels());
}
});
$.publish(Events.PISKEL_RESET);
$.publish(Events.PISKEL_SAVE_STATE, {
type : pskl.service.HistoryService.SNAPSHOT
});
};
})();
(function () {})();

View File

@ -8,7 +8,7 @@
ns.BaseSelect = function() {
this.secondaryToolId = pskl.tools.drawing.Move.TOOL_ID;
this.BodyRoot = $('body');
this.bodyRoot = $('body');
// Select's first point coordinates (set in applyToolAt)
this.startCol = null;
@ -81,12 +81,12 @@
if (overlay.containsPixel(col, row)) {
if (this.isInSelection(col, row)) {
// We're hovering the selection, show the move tool:
this.BodyRoot.addClass(this.secondaryToolId);
this.BodyRoot.removeClass(this.toolId);
this.bodyRoot.addClass(this.secondaryToolId);
this.bodyRoot.removeClass(this.toolId);
} else {
// We're not hovering the selection, show create selection tool:
this.BodyRoot.addClass(this.toolId);
this.BodyRoot.removeClass(this.secondaryToolId);
this.bodyRoot.addClass(this.toolId);
this.bodyRoot.removeClass(this.secondaryToolId);
}
}
};

View File

@ -33,6 +33,66 @@
return canvas;
},
/**
* Splits the specified image into several new canvas elements based on the
* supplied offset and frame sizes
* @param image The source image that will be split
* @param {Number} offsetX The padding from the left side of the source image
* @param {Number} offsetY The padding from the top side of the source image
* @param {Number} width The width of an individual frame
* @param {Number} height The height of an individual frame
* @param {Boolean} useHorizonalStrips True if the frames should be layed out from left to
* right, False if it should use top to bottom
* @param {Boolean} ignoreEmptyFrames True to ignore empty frames, false to keep them
* @returns {Array} An array of canvas elements that contain the split frames
*/
createFramesFromImage : function (image, offsetX, offsetY, width, height, useHorizonalStrips, ignoreEmptyFrames) {
var canvasArray = [];
var x = offsetX;
var y = offsetY;
var blankData = pskl.utils.CanvasUtils.createCanvas(width, height).toDataURL();
while (x + width <= image.width && y + height <= image.height) {
// Create a new canvas element
var canvas = pskl.utils.CanvasUtils.createCanvas(width, height);
var context = canvas.getContext('2d');
// Blit the correct part of the source image into the new canvas
context.drawImage(
image,
x,
y,
width,
height,
0,
0,
width,
height);
if (!ignoreEmptyFrames || canvas.toDataURL() !== blankData) {
canvasArray.push(canvas);
}
if (useHorizonalStrips) {
// Move from left to right
x += width;
if (x + width > image.width) {
x = offsetX;
y += height;
}
} else {
// Move from top to bottom
y += height;
if (y + height > image.height) {
x += width;
y = offsetY;
}
}
}
return canvasArray;
},
/**
* By default, all scaling operations on a Canvas 2D Context are performed using antialiasing.
* Resizing a 32x32 image to 320x320 will lead to a blurry output.

View File

@ -14,7 +14,7 @@
var color = {
r : 255,
g : 255,
b : 255
b : 0
};
var match = null;
while (true) {

View File

@ -5,8 +5,8 @@
/**
* Render a Frame object as an image.
* Can optionally scale it (zoom)
* @param {Frame} frame
* @param {Number} zoom
* @param frame {Frame} frame
* @param zoom {Number} zoom
* @return {Image}
*/
toImage : function (frame, zoom) {
@ -16,6 +16,55 @@
return canvasRenderer.render();
},
/**
* Draw the provided frame in a 2d canvas
*
* @param frame {pskl.model.Frame} frame the frame to draw
* @param canvas {Canvas} canvas the canvas target
* @param transparentColor {String} transparentColor (optional) color to use to represent transparent pixels.
*/
drawToCanvas : function (frame, canvas, transparentColor) {
var context = canvas.getContext('2d');
transparentColor = transparentColor || Constants.TRANSPARENT_COLOR;
for (var x = 0, width = frame.getWidth() ; x < width ; x++) {
for (var y = 0, height = frame.getHeight() ; y < height ; y++) {
var color = frame.getPixel(x, y);
// accumulate all the pixels of the same color to speed up rendering
// by reducting fillRect calls
var w = 1;
while (color === frame.getPixel(x, y + w) && (y + w) < height) {
w++;
}
if (color == Constants.TRANSPARENT_COLOR) {
color = transparentColor;
}
pskl.utils.FrameUtils.renderLine_(color, x, y, w, context);
y = y + w - 1;
}
}
},
/**
* Render a line of a single color in a given canvas 2D context.
*
* @param color {String} color to draw
* @param x {Number} x coordinate
* @param y {Number} y coordinate
* @param width {Number} width of the line to draw, in pixels
* @param context {CanvasRenderingContext2D} context of the canvas target
*/
renderLine_ : function (color, x, y, width, context) {
if (color === Constants.TRANSPARENT_COLOR || color === null) {
return;
}
context.fillStyle = color;
context.fillRect(x, y, 1, width);
},
merge : function (frames) {
var merged = null;
if (frames.length) {

View File

@ -128,7 +128,7 @@
var nextCol = currentItem.col + dx[i];
var nextRow = currentItem.row + dy[i];
try {
if (frame.containsPixel(nextCol, nextRow) && frame.getPixel(nextCol, nextRow) == targetColor) {
if (frame.containsPixel(nextCol, nextRow) && frame.getPixel(nextCol, nextRow) == targetColor) {
queue.push({'col': nextCol, 'row': nextRow});
}
} catch (e) {

View File

@ -10,6 +10,7 @@
TILED_PREVIEW : 'TILED_PREVIEW',
ONION_SKIN : 'ONION_SKIN',
LAYER_PREVIEW : 'LAYER_PREVIEW',
LAYER_OPACITY : 'LAYER_OPACITY',
KEY_TO_DEFAULT_VALUE_MAP_ : {
'GRID_WIDTH' : 0,
@ -22,6 +23,7 @@
'SELECTED_PALETTE' : Constants.CURRENT_COLORS_PALETTE_ID,
'TILED_PREVIEW' : false,
'ONION_SKIN' : false,
'LAYER_OPACITY' : 0.2,
'LAYER_PREVIEW' : true
},

View File

@ -13,11 +13,13 @@
};
ns.HslRgbColorPicker.prototype.init = function () {
var isChromeOrFirefox = pskl.utils.UserAgent.isChrome || pskl.utils.UserAgent.isFirefox;
var changeEvent = isChromeOrFirefox ? 'input' : 'change';
var isFirefox = pskl.utils.UserAgent.isFirefox;
var isChrome = pskl.utils.UserAgent.isChrome;
var changeEvent = (isChrome || isFirefox) ? 'input' : 'change';
this.container.addEventListener(changeEvent, this.onPickerChange_.bind(this));
this.container.addEventListener('keydown', this.onKeydown_.bind(this));
this.container.addEventListener('focusout', this.onBlur_.bind(this));
this.container.addEventListener('blur', this.onBlur_.bind(this), true);
this.spectrumEl = this.container.querySelector('.color-picker-spectrum');
@ -36,6 +38,9 @@
this.spectrumEl = null;
};
/**
* Handle change event on all color inputs
*/
ns.HslRgbColorPicker.prototype.onPickerChange_ = function (evt) {
var target = evt.target;
if (target.dataset.dimension) {
@ -47,6 +52,9 @@
}
};
/**
* Handle up/down arrow keydown on text inputs
*/
ns.HslRgbColorPicker.prototype.onKeydown_ = function (evt) {
var target = evt.target;
@ -90,7 +98,7 @@
this.setColor(color);
}
} else if (model === 'hex') {
if (/^#([a-f0-9]{3}) {1,2}$/i.test(value)) {
if (/^#([a-f0-9]{3}){1,2}$/i.test(value)) {
this.setColor(value);
}
}

View File

@ -28,7 +28,7 @@
this.synchronize_(this.lastInput);
};
ns.SizeInput.prototype.enableSync = function () {
ns.SizeInput.prototype.disableSync = function () {
this.syncEnabled = false;
};

View File

@ -2,9 +2,10 @@
(typeof exports != "undefined" ? exports : pskl_exports).scripts = [
// Core libraries
"js/lib/jquery-1.8.0.js","js/lib/jquery-ui-1.10.3.custom.js","js/lib/pubsub.js","js/lib/bootstrap/bootstrap.js",
"js/lib/jquery-1.8.0.js",
"js/lib/jquery-ui-1.10.3.custom.js",
"js/lib/pubsub.js",
"js/lib/bootstrap/bootstrap.js",
// Application wide configuration
"js/Constants.js",
@ -126,7 +127,6 @@
"js/controller/dialogs/ImportImageController.js",
"js/controller/dialogs/BrowseLocalController.js",
// Dialogs controller
"js/controller/dialogs/DialogsController.js",

View File

@ -1,16 +1,18 @@
<div id="cheatsheet-wrapper" style="display:none">
<div class="cheatsheet-container">
<div class="cheatsheet-section">
<h3>Tool shortcuts</h3>
<h3 class="cheatsheet-title">Tool shortcuts</h3>
<ul class="cheatsheet-tool-shortcuts"></ul>
</div>
<div class="cheatsheet-section">
<h3>Misc shortcuts</h3>
<h3 class="cheatsheet-title">Misc shortcuts</h3>
<ul class="cheatsheet-misc-shortcuts"></ul>
</div>
<div class="cheatsheet-section">
<h3>Selection shortcuts</h3>
<h3 class="cheatsheet-title">Selection shortcuts</h3>
<ul class="cheatsheet-selection-shortcuts"></ul>
<h3 class="cheatsheet-title">Color shortcuts</h3>
<ul class="cheatsheet-colors-shortcuts"></ul>
</div>
</div>
</div>

View File

@ -9,19 +9,41 @@
<span class="dialog-section-title">Name :</span><span class="import-image-file-name"></span>
</div>
<div class="import-section">
<span class="dialog-section-title" style="vertical-align:top">Info :</span>
<div class="import-section-preview-title">Preview :</div>
<div class="import-section-preview"></div>
</div>
<div class="import-section">
<span class="dialog-section-title">Size :</span>
<label class="dialog-section-radio-label">
<input class="dialog-section-radio" name="import-type" value="single" type="radio" checked="checked">
Import as single image
</label>
</div>
<div class="import-section import-subsection">
<span class="dialog-section-title">Resize to</span>
<input type="text" class="textfield import-size-field" name="resize-width"/>x
<input type="text" class="textfield import-size-field" name="resize-height"/>
</div>
<div class="import-section">
<span class="import-section-title">Smooth resize :</span>
<input type="checkbox" checked="checked" name="smooth-resize-checkbox" value="1"/>
<div class="import-section import-subsection">
<span class="import-section-title">Smooth resize</span>
<input type="checkbox" class="checkbox-fix" checked="checked" name="smooth-resize-checkbox" value="1"/>
</div>
<input type="submit" name="import-submit" class="button button-primary import-button" value="Import" />
<div class="import-section">
<label class="dialog-section-radio-label">
<input class="dialog-section-radio" name="import-type" value="sheet" type="radio">
Import as spritesheet
</label>
</div>
<div class="import-section import-subsection">
<span class="dialog-section-title">Frame size</span>
<input type="text" class="textfield import-size-field" name="frame-size-x"/>x
<input type="text" class="textfield import-size-field" name="frame-size-y"/>
</div>
<div class="import-section import-subsection">
<span class="dialog-section-title">Offset</span>
<input type="text" class="textfield import-size-field" name="frame-offset-x"/>x
<input type="text" class="textfield import-size-field" name="frame-offset-y"/>
</div>
<input type="submit" name="import-submit" class="button button-primary import-button" value="Import"/>
</form>
</div>
</div>

View File

@ -1,10 +1,10 @@
<form action="" method="POST" name="application-settings-form">
<div class="settings-section">
<div class="settings-section settings-section--application-general">
<div class="settings-title">
General
</div>
<div class="settings-item">
<label>Background:</label>
<label>Background</label>
<div class="background-picker-wrapper">
<div class="background-picker light-picker-background" data-background="light-canvas-background"
rel="tooltip" data-placement="bottom" title="light / high contrast">
@ -20,8 +20,9 @@
</div>
</div>
</div>
<div class="settings-item">
<label for="grid-width">Grid :</label>
<label for="grid-width">Grid</label>
<select id="grid-width" class="grid-width-select">
<option value="0">Disabled</option>
<option value="1">1px</option>
@ -30,6 +31,13 @@
<option value="4">4px</option>
</select>
</div>
<div class="settings-item">
<label for="tiled-preview">Layer Preview Opacity</label>
<input type="range" class="layer-opacity-input" name="layer-opacity" min="0" max="1" step="0.05"/>
<span class="layer-opacity-text"></span>
</div>
</div>
<div class="settings-section">
@ -39,14 +47,14 @@
<div class="settings-item">
<label>
<input type="checkbox" value="1" class="tiled-preview-checkbox" name="tiled-preview-checkbox"/>
<input type="checkbox" value="1" class="tiled-preview-checkbox checkbox-fix" name="tiled-preview-checkbox"/>
Repeated preview
</label>
</div>
<div class="settings-item">
<label for="tiled-preview">Maximum FPS </label>
<input type="text" class="textfield textfield-small max-fps-input" name="max-fps"/>
<input type="text" class="textfield textfield-small max-fps-input" name="max-fps"/>
</div>
<input type="submit" class="button button-primary" value="Save" />

View File

@ -13,8 +13,14 @@
<span class="settings-description">ZIP with one PNG file per frame.</span>
<span class="settings-description" style="display:block">File names will start with the prefix below.</span>
<div class="settings-item">
<label for="zip-prefix-name">Prefix:</label>
<input id="zip-prefix-name" type="text" class="textfield" placeholder="PNG file prefix ...">
<label>Prefix</label>
<input class="zip-prefix-name textfield" type="text" placeholder="PNG file prefix ...">
</div>
<div class="settings-item">
<label>
<input class="zip-split-layers-checkbox checkbox-fix" type="checkbox" />
Split by layers
</label>
</div>
<button type="button" class="button button-primary zip-generate-button"/>Download ZIP</button>
</div>

View File

@ -17,13 +17,13 @@
</div>
<div class="resize-section">
<label>
<input type="checkbox" class="resize-ratio-checkbox" value="true" checked="true"/>
<input type="checkbox" class="resize-ratio-checkbox checkbox-fix" value="true" checked="true"/>
<span>Maintain aspect ratio</span>
</label>
</div>
<div class="resize-section">
<label>
<input type="checkbox" class="resize-content-checkbox" value="true"/>
<input type="checkbox" class="resize-content-checkbox checkbox-fix" value="true"/>
<span>Resize canvas content</span>
</label>
</div>

View File

@ -0,0 +1,206 @@
describe("SelectionManager suite", function() {
var black = '#000000';
var red = '#ff0000';
var transparent = Constants.TRANSPARENT_COLOR;
var B = black, R = red, T = transparent;
// shortcuts
var toFrameGrid = test.testutils.toFrameGrid;
var frameEqualsGrid = test.testutils.frameEqualsGrid;
// test objects
var selectionManager;
var selection;
var currentFrame;
/**
* @Mock
*/
pskl.app.shortcutService = {
addShortcut : function () {}
};
/**
* @Mock
*/
var piskelController = {
getCurrentFrame : function () {
return currentFrame;
}
};
beforeEach(function() {
currentFrame = pskl.model.Frame.fromPixelGrid([
[B, R, T],
[R, B, R],
[T, R, B]
]);
selectionManager = new pskl.selection.SelectionManager(piskelController);
selectionManager.init();
selection = new pskl.selection.BaseSelection();
selection.pixels = [];
});
/**
* Check a basic copy paste scenario
*/
it("copy/paste OK", function () {
console.log('[SelectionManager] copy/paste OK');
selectMiddleLine();
console.log('[SelectionManager] ... copy');
selectionManager.copy();
console.log('[SelectionManager] ... check selection content after copy contains correct colors');
expect(selection.pixels.length).toBe(3); // or not to be ... lalalala ... french-only joke \o/
checkContainsPixel(selection.pixels, 1, 0, R);
checkContainsPixel(selection.pixels, 1, 1, B);
checkContainsPixel(selection.pixels, 1, 2, R);
console.log('[SelectionManager] ... move 1 row down');
selection.move(0, 1);
console.log('[SelectionManager] ... check pixels were shifted by two columns forward');
checkContainsPixel(selection.pixels, 2, 0, R);
checkContainsPixel(selection.pixels, 2, 1, B);
checkContainsPixel(selection.pixels, 2, 2, R);
console.log('[SelectionManager] ... paste');
selectionManager.paste();
console.log('[SelectionManager] ... check last line is identical to middle line after paste');
frameEqualsGrid(currentFrame, [
[B, R, T],
[R, B, R],
[R, B, R]
]);
});
/**
* Check a basic cut paste scenario
*/
it("cut OK", function () {
console.log('[SelectionManager] cut OK');
selectMiddleLine();
console.log('[SelectionManager] ... cut');
selectionManager.cut();
console.log('[SelectionManager] ... check middle line was cut in the source frame');
frameEqualsGrid(currentFrame, [
[B, R, T],
[T, T, T],
[T, R, B]
]);
console.log('[SelectionManager] ... paste');
selectionManager.paste();
console.log('[SelectionManager] ... check middle line was restored by paste');
frameEqualsGrid(currentFrame, [
[B, R, T],
[R, B, R],
[T, R, B]
]);
});
/**
* Check a copy paste scenario that goes out of the frame boundaries for copying and for pasting.
*/
it("copy/paste OK out of bounds", function () {
console.log('[SelectionManager] copy/paste OK out of bounds');
selectMiddleLine();
console.log('[SelectionManager] ... move 2 columns to the right');
selection.move(2, 0);
console.log('[SelectionManager] ... copy out of bounds');
selectionManager.copy();
console.log('[SelectionManager] ... check out of bound pixels were replaced by transparent pixels');
checkContainsPixel(selection.pixels, 1, 2, R);
checkContainsPixel(selection.pixels, 1, 3, T);
checkContainsPixel(selection.pixels, 1, 4, T);
console.log('[SelectionManager] ... move one column to the left');
selection.move(-1, 0);
console.log('[SelectionManager] ... check pixels were shifted by one column back');
checkContainsPixel(selection.pixels, 1, 1, R);
checkContainsPixel(selection.pixels, 1, 2, T);
checkContainsPixel(selection.pixels, 1, 3, T);
console.log('[SelectionManager] ... paste out of bounds');
selectionManager.paste();
console.log('[SelectionManager] ... check pixel at (1,1) is red after paste');
frameEqualsGrid(currentFrame, [
[B, R, T],
[R, R, R],
[T, R, B]
]);
});
/**
* Check a cut paste scenario that goes out of the frame boundaries for cutting and for pasting.
*/
it("cut OK out of bounds", function () {
console.log('[SelectionManager] cut OK');
selectMiddleLine();
console.log('[SelectionManager] ... move 2 columns to the right');
selection.move(2, 0);
console.log('[SelectionManager] ... cut out of bounds');
selectionManager.cut();
console.log('[SelectionManager] ... check last pixel of midle line was cut in the source frame');
frameEqualsGrid(currentFrame, [
[B, R, T],
[R, B, T],
[T, R, B]
]);
selection.move(-1, 0);
console.log('[SelectionManager] ... paste out of bounds');
selectionManager.paste();
console.log('[SelectionManager] ... check middle line final state');
frameEqualsGrid(currentFrame, [
[B, R, T],
[R, R, T],
[T, R, B]
]);
});
// Private helpers
var createPixel = function(row, col, color) {
return {
row : row,
col : col,
color : color
};
};
var selectMiddleLine = function () {
console.log('[SelectionManager] ... select middle line');
selection.pixels.push(createPixel(1, 0));
selection.pixels.push(createPixel(1, 1));
selection.pixels.push(createPixel(1, 2));
expect(selectionManager.currentSelection).toBe(null);
console.log('[SelectionManager] ... send SELECTION_CREATED event for the test selection');
$.publish(Events.SELECTION_CREATED, [selection]);
expect(selectionManager.currentSelection).toBe(selection);
};
var checkContainsPixel = function (pixels, row, col, color) {
var containsPixel = pixels.some(function (pixel) {
return pixel.row == row && pixel.col == col && pixel.color == color;
});
expect(containsPixel).toBe(true);
};
});

View File

@ -1,6 +1,28 @@
(function () {
var ns = $.namespace('test.testutils');
/**
* Frame.createFromGrid accepts grids that are rotated by 90deg from
* the visual/usual way. (column-based grid)
*
* For testing, it's easier for be able to specify a row-based grid, because
* it visually matches what the image will look like.
*
* For instance :
*
* [[black, black, black],
* [white, white, white]]
*
* we expect this to be a 3x2 image, one black line above a white line.
*
* However Frame.createFromGrid needs the following input to create such an image :
*
* [[black, white],
* [black, white],
* [black, white]]
*
* This helper will build the second array from the first array.
*/
ns.toFrameGrid = function (normalGrid) {
var frameGrid = [];
var w = normalGrid[0].length;

View File

@ -5,25 +5,25 @@ describe("Color utils", function() {
it("returns a color when provided with array of colors", function() {
// when/then
var unusedColor = pskl.utils.ColorUtils.getUnusedColor(['#ffffff', '#feffff', '#fdffff']);
var unusedColor = pskl.utils.ColorUtils.getUnusedColor(['#ffff00', '#feff00', '#fdff00']);
// verify
expect(unusedColor).toBe('#FCFFFF');
expect(unusedColor).toBe('#FCFF00');
// when/then
unusedColor = pskl.utils.ColorUtils.getUnusedColor(['#fcffff', '#feffff', '#fdffff']);
unusedColor = pskl.utils.ColorUtils.getUnusedColor(['#fcff00', '#feff00', '#fdff00']);
// verify
expect(unusedColor).toBe('#FFFFFF');
expect(unusedColor).toBe('#FFFF00');
});
it("returns a color for an empty array", function() {
// when/then
var unusedColor = pskl.utils.ColorUtils.getUnusedColor([]);
// verify
expect(unusedColor).toBe('#FFFFFF');
expect(unusedColor).toBe('#FFFF00');
// when/then
unusedColor = pskl.utils.ColorUtils.getUnusedColor();
// verify
expect(unusedColor).toBe('#FFFFFF');
expect(unusedColor).toBe('#FFFF00');
});
});

View File

@ -3,35 +3,41 @@ describe("FrameUtils suite", function() {
var red = '#ff0000';
var transparent = Constants.TRANSPARENT_COLOR;
// shortcuts
var toFrameGrid = test.testutils.toFrameGrid;
var frameEqualsGrid = test.testutils.frameEqualsGrid;
it("merges 2 frames", function () {
var B = black, R = red, T = transparent;
var frame1 = pskl.model.Frame.fromPixelGrid([
[black, transparent],
[transparent, black]
[B, T],
[T, B]
]);
var frame2 = pskl.model.Frame.fromPixelGrid([
[transparent, red],
[red, transparent]
[T, R],
[R, T]
]);
var mergedFrame = pskl.utils.FrameUtils.merge([frame1, frame2]);
expect(mergedFrame.getPixel(0,0)).toBe(black);
expect(mergedFrame.getPixel(0,1)).toBe(red);
expect(mergedFrame.getPixel(1,0)).toBe(red);
expect(mergedFrame.getPixel(1,1)).toBe(black);
frameEqualsGrid(mergedFrame, [
[B, R],
[R, B]
]);
});
it("returns same frame when merging single frame", function () {
var frame1 = pskl.model.Frame.fromPixelGrid([
[black, transparent],
[transparent, black]
]);
var B = black, T = transparent;
var frame1 = pskl.model.Frame.fromPixelGrid(toFrameGrid([
[B, T],
[B, T]
]));
var mergedFrame = pskl.utils.FrameUtils.merge([frame1]);
expect(mergedFrame.getPixel(0,0)).toBe(black);
expect(mergedFrame.getPixel(0,1)).toBe(transparent);
expect(mergedFrame.getPixel(1,0)).toBe(transparent);
expect(mergedFrame.getPixel(1,1)).toBe(black);
frameEqualsGrid(mergedFrame, [
[B, T],
[B, T]
]);
});
var checkPixelsColor = function (frame, pixels, color) {
@ -42,9 +48,10 @@ describe("FrameUtils suite", function() {
};
it ("converts an image to a frame", function () {
var B = black, T = transparent;
var frame1 = pskl.model.Frame.fromPixelGrid([
[black, transparent],
[transparent, black]
[B, T],
[T, B]
]);
var image = pskl.utils.FrameUtils.toImage(frame1);
@ -57,48 +64,66 @@ describe("FrameUtils suite", function() {
var biggerFrame = pskl.utils.FrameUtils.createFromImage(biggerImage);
checkPixelsColor(biggerFrame, [
[0,0],[0,1],[0,2],
[1,0],[1,1],[1,2],
[2,0],[2,1],[2,2],
[3,3],[3,4],[3,5],
[4,3],[4,4],[4,5],
[5,3],[5,4],[5,5]
], black);
checkPixelsColor(biggerFrame, [
[0,3],[0,4],[0,5],
[1,3],[1,4],[1,5],
[2,3],[2,4],[2,5],
[3,0],[3,1],[3,2],
[4,0],[4,1],[4,2],
[5,0],[5,1],[5,2]
], transparent);
frameEqualsGrid(biggerFrame, [
[B, B, B, T, T, T],
[B, B, B, T, T, T],
[B, B, B, T, T, T],
[T, T, T, B, B, B],
[T, T, T, B, B, B],
[T, T, T, B, B, B]
]);
});
it ("[LayerUtils] creates a layer from a simple spritesheet", function () {
var frame = pskl.model.Frame.fromPixelGrid([
[black, red],
[red, black],
[black, black],
[red, red]
]);
var B = black, R = red;
// original image in 4x2
var frame = pskl.model.Frame.fromPixelGrid(toFrameGrid([
[B, R, B, R],
[R, B, B, R]
]));
var spritesheet = pskl.utils.FrameUtils.toImage(frame);
// split the spritesheet by 4
var frames = pskl.utils.LayerUtils.createLayerFromSpritesheet(spritesheet, 4);
// expect 4 frames of 1x2
expect(frames.length).toBe(4);
expect(frames[0].getPixel(0,0)).toBe(black);
expect(frames[0].getPixel(0,1)).toBe(red);
// verify frame content
frameEqualsGrid(frames[0], [
[B],
[R]
]);
frameEqualsGrid(frames[1], [
[R],
[B]
]);
frameEqualsGrid(frames[2], [
[B],
[B]
]);
frameEqualsGrid(frames[3], [
[R],
[R]
]);
});
expect(frames[1].getPixel(0,0)).toBe(red);
expect(frames[1].getPixel(0,1)).toBe(black);
it ("supports null values in frame array", function () {
var B = black, T = transparent;
var frame = pskl.model.Frame.fromPixelGrid([
[B, null],
[null, B]
]);
expect(frames[2].getPixel(0,0)).toBe(black);
expect(frames[2].getPixel(0,1)).toBe(black);
expect(frames[3].getPixel(0,0)).toBe(red);
expect(frames[3].getPixel(0,1)).toBe(red);
var image = pskl.utils.FrameUtils.toImage(frame);
// transform back to frame for ease of testing
var testFrame = pskl.utils.FrameUtils.createFromImage(image);
frameEqualsGrid(testFrame, [
[B, T],
[T, B]
]);
});
});