Merge pull request #284 from JALissiak/spritesheetImport

Adding spritesheet import - fixes #188
This commit is contained in:
Julian Descottes 2015-06-04 08:09:13 +02:00
commit e773f9ae6d
5 changed files with 314 additions and 23 deletions

View File

@ -2,17 +2,51 @@
/* Import dialog */
/************************************************************************************************/
.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;
}
.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 +57,14 @@
display: inline-block;
overflow: hidden;
height: 2rem;
word-break : break-all;
vertical-align: middle;
font-style: italic;
font-weight: normal;
text-shadow: none;
width: 200px;
white-space: nowrap;
text-overflow: ellipsis;
}
[name=smooth-resize-checkbox] {
@ -37,11 +73,12 @@
.dialog-import-body {
padding:10px 20px;
font-size:1.5em
font-size:1.3em
}
.import-button {
font-size: 1em;
height: auto;
padding: 5px 10px;
margin-top: 15px;
}

View File

@ -66,8 +66,8 @@
}
#dialog-container.import-image {
width: 500px;
height: 300px;
width: 550px;
height: 350px;
top : 50%;
left : 50%;
position : absolute;

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;
@ -72,7 +140,13 @@
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 +154,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 +177,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 +211,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 +238,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 +247,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

@ -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

@ -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>
<div class="import-section import-subsection">
<span class="import-section-title">Smooth resize</span>
<input type="checkbox" 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>