Rework pixel storage, manipulation, rendering

This commit is contained in:
Dávid Szabó 2016-08-24 23:07:36 +02:00 committed by Julian Descottes
parent 06abdca62e
commit f5c98cf0b3
14 changed files with 242 additions and 88 deletions

View File

@ -14,28 +14,57 @@
} }
}; };
ns.Frame.fromPixelGrid = function (pixels) { ns.Frame.fromPixelGrid = function (pixels, width, height) {
if (pixels.length && pixels[0].length) { if (pixels.length) {
if (pixels[0].length) {
var w = pixels.length; var w = pixels.length;
var h = pixels[0].length; var h = pixels[0].length;
var buffer = [];
for (var y = 0; y < h; y++) {
for (var x = 0; x < w; x++ ) {
if (typeof pixels[x][y] == 'string') {
buffer[y * w + x] = pskl.utils.colorToInt(pixels[x][y]);
} else {
buffer[y * w + x] = pixels[x][y];
}
}
}
} else if (width && height) {
var w = width;
var h = height;
buffer = pixels;
} else {
throw 'Bad arguments in pskl.model.frame.fromPixelGrid, missing width and height';
}
var frame = new pskl.model.Frame(w, h); var frame = new pskl.model.Frame(w, h);
frame.setPixels(pixels); frame.setPixels(buffer);
return frame; return frame;
} else { } else {
throw 'Bad arguments in pskl.model.Frame.fromPixelGrid : ' + pixels; console.error(pixels);
throw 'Bad arguments in pskl.model.Frame.fromPixelGrid';
} }
}; };
var _emptyPixelGridCache = {};
ns.Frame.createEmptyPixelGrid_ = function (width, height) { ns.Frame.createEmptyPixelGrid_ = function (width, height) {
var pixels = []; var pixels;
for (var columnIndex = 0 ; columnIndex < width ; columnIndex++) { var key = width+"-"+height;
var columnArray = []; if (_emptyPixelGridCache[key]) {
for (var heightIndex = 0 ; heightIndex < height ; heightIndex++) { pixels = _emptyPixelGridCache[key];
columnArray.push(Constants.TRANSPARENT_COLOR); } else {
pixels = _emptyPixelGridCache[key] = new Uint32Array(width * height);
var transparentColorInt = pskl.utils.colorToInt(Constants.TRANSPARENT_COLOR);
if (!pixels.fill) { // PhantomJS ='(
for (var i = 0; i < width * height; i++) {
pixels[i] = transparentColorInt;
} }
pixels[columnIndex] = columnArray; } else {
pixels.fill(transparentColorInt);
} }
return pixels; }
return new Uint32Array(pixels);
}; };
ns.Frame.createEmptyFromFrame = function (frame) { ns.Frame.createEmptyFromFrame = function (frame) {
@ -44,7 +73,7 @@
ns.Frame.prototype.clone = function () { ns.Frame.prototype.clone = function () {
var clone = new ns.Frame(this.width, this.height); var clone = new ns.Frame(this.width, this.height);
clone.setPixels(this.getPixels()); clone.setPixels(this.pixels);
return clone; return clone;
}; };
@ -64,8 +93,9 @@
}; };
ns.Frame.prototype.clear = function () { ns.Frame.prototype.clear = function () {
var pixels = ns.Frame.createEmptyPixelGrid_(this.getWidth(), this.getHeight()); this.pixels = ns.Frame.createEmptyPixelGrid_(this.getWidth(), this.getHeight());
this.setPixels(pixels); this.version++;
// this.setPixels(pixels);
}; };
/** /**
@ -73,11 +103,10 @@
* @private * @private
*/ */
ns.Frame.prototype.clonePixels_ = function (pixels) { ns.Frame.prototype.clonePixels_ = function (pixels) {
var clonedPixels = []; //npixels = new Uint32Array(pixels.length);
for (var col = 0 ; col < pixels.length ; col++) { //npixels.set(pixels);
clonedPixels[col] = pixels[col].slice(0 , pixels[col].length);
} return new Uint32Array(pixels);
return clonedPixels;
}; };
ns.Frame.prototype.getHash = function () { ns.Frame.prototype.getHash = function () {
@ -86,9 +115,14 @@
ns.Frame.prototype.setPixel = function (x, y, color) { ns.Frame.prototype.setPixel = function (x, y, color) {
if (this.containsPixel(x, y)) { if (this.containsPixel(x, y)) {
var p = this.pixels[x][y]; var index = y * this.width + x;
var p = this.pixels[index];
if (typeof color === 'string') {
color = pskl.utils.colorToInt(color);
}
if (p !== color) { if (p !== color) {
this.pixels[x][y] = color || Constants.TRANSPARENT_COLOR; this.pixels[index] = color || pskl.utils.colorToInt(Constants.TRANSPARENT_COLOR);
this.version++; this.version++;
} }
} }
@ -96,7 +130,7 @@
ns.Frame.prototype.getPixel = function (x, y) { ns.Frame.prototype.getPixel = function (x, y) {
if (this.containsPixel(x, y)) { if (this.containsPixel(x, y)) {
return this.pixels[x][y]; return this.pixels[y * this.width + x];
} else { } else {
return null; return null;
} }
@ -107,7 +141,7 @@
var height = this.getHeight(); var height = this.getHeight();
for (var x = 0 ; x < width ; x++) { for (var x = 0 ; x < width ; x++) {
for (var y = 0 ; y < height ; y++) { for (var y = 0 ; y < height ; y++) {
callback(this.pixels[x][y], x, y, this); callback(this.pixels[y * this.width + x], x, y, this);
} }
} }
}; };

View File

@ -30,12 +30,11 @@
ns.FramesheetRenderer.prototype.drawFrameInCanvas_ = function (frame, canvas, offsetWidth, offsetHeight) { ns.FramesheetRenderer.prototype.drawFrameInCanvas_ = function (frame, canvas, offsetWidth, offsetHeight) {
var context = canvas.getContext('2d'); var context = canvas.getContext('2d');
frame.forEachPixel(function (color, x, y) { var imageData = context.createImageData(frame.getWidth(), frame.getHeight());
if (color != Constants.TRANSPARENT_COLOR) { var pixels = frame.getPixels();
context.fillStyle = color; var data = new Uint8ClampedArray(pixels.buffer);
context.fillRect(x + offsetWidth, y + offsetHeight, 1, 1); imageData.data.set(data);
} context.putImageData(imageData, offsetWidth, offsetHeight);
});
}; };
ns.FramesheetRenderer.prototype.createCanvas_ = function (columns, rows) { ns.FramesheetRenderer.prototype.createCanvas_ = function (columns, rows) {

View File

@ -56,10 +56,9 @@
}; };
ns.ColorSwap.prototype.applyToolOnFrame_ = function (frame, oldColor, newColor) { ns.ColorSwap.prototype.applyToolOnFrame_ = function (frame, oldColor, newColor) {
oldColor = oldColor.toUpperCase();
frame.forEachPixel(function (color, col, row) { frame.forEachPixel(function (color, col, row) {
if (color && color.toUpperCase() == oldColor) { if (color && color == oldColor) {
frame.pixels[col][row] = newColor; frame.setPixel(col, row, newColor);
} }
}); });
frame.version++; frame.version++;

View File

@ -43,10 +43,10 @@
var overlayColor = overlay.getPixel(col, row); var overlayColor = overlay.getPixel(col, row);
var frameColor = frame.getPixel(col, row); var frameColor = frame.getPixel(col, row);
var isPixelModified = overlayColor !== Constants.TRANSPARENT_COLOR; var isPixelModified = overlayColor !== pskl.utils.colorToInt(Constants.TRANSPARENT_COLOR);
var pixelColor = isPixelModified ? overlayColor : frameColor; var pixelColor = isPixelModified ? overlayColor : frameColor;
var isTransparent = pixelColor === Constants.TRANSPARENT_COLOR; var isTransparent = pixelColor === pskl.utils.colorToInt(Constants.TRANSPARENT_COLOR);
if (isTransparent) { if (isTransparent) {
return Constants.TRANSPARENT_COLOR; return Constants.TRANSPARENT_COLOR;
} }
@ -61,9 +61,9 @@
var color; var color;
if (isDarken) { if (isDarken) {
color = window.tinycolor.darken(pixelColor, step); color = window.tinycolor.darken(pskl.utils.intToColor(pixelColor), step);
} else { } else {
color = window.tinycolor.lighten(pixelColor, step); color = window.tinycolor.lighten(pskl.utils.intToColor(pixelColor), step);
} }
// Convert tinycolor color to string format. // Convert tinycolor color to string format.

View File

@ -15,7 +15,7 @@
} else if (axis === ns.TransformUtils.HORIZONTAL) { } else if (axis === ns.TransformUtils.HORIZONTAL) {
y = h - y - 1; y = h - y - 1;
} }
frame.pixels[x][y] = color; frame.setPixel(x, y, color);
}); });
frame.version++; frame.version++;
return frame; return frame;
@ -55,9 +55,9 @@
x = x - xDelta; x = x - xDelta;
y = y - yDelta; y = y - yDelta;
if (clone.containsPixel(x, y)) { if (clone.containsPixel(x, y)) {
frame.pixels[_x][_y] = clone.getPixel(x, y); frame.setPixel(_x, _y, clone.getPixel(x, y));
} else { } else {
frame.pixels[_x][_y] = Constants.TRANSPARENT_COLOR; frame.setPixel(_x, _y, Constants.TRANSPARENT_COLOR);
} }
}); });
frame.version++; frame.version++;
@ -70,8 +70,9 @@
var miny = frame.height; var miny = frame.height;
var maxx = 0; var maxx = 0;
var maxy = 0; var maxy = 0;
var transparentColorInt = pskl.utils.colorToInt(Constants.TRANSPARENT_COLOR);
frame.forEachPixel(function (color, x, y) { frame.forEachPixel(function (color, x, y) {
if (color !== Constants.TRANSPARENT_COLOR) { if (color !== transparentColorInt) {
minx = Math.min(minx, x); minx = Math.min(minx, x);
maxx = Math.max(maxx, x); maxx = Math.max(maxx, x);
miny = Math.min(miny, y); miny = Math.min(miny, y);
@ -98,9 +99,9 @@
y -= dy; y -= dy;
if (clone.containsPixel(x, y)) { if (clone.containsPixel(x, y)) {
frame.pixels[_x][_y] = clone.getPixel(x, y); frame.setPixel(_x, _y, clone.getPixel(x, y));
} else { } else {
frame.pixels[_x][_y] = Constants.TRANSPARENT_COLOR; frame.setPixel(_x, _y, Constants.TRANSPARENT_COLOR);
} }
}); });
frame.version++; frame.version++;

View File

@ -1,6 +1,8 @@
(function () { (function () {
var ns = $.namespace('pskl.utils'); var ns = $.namespace('pskl.utils');
var colorCache = {}; var colorCache = {};
var offCanvasPool = {};
var imageDataPool = {};
ns.FrameUtils = { ns.FrameUtils = {
/** /**
* Render a Frame object as an image. * Render a Frame object as an image.
@ -28,7 +30,12 @@
* @param {String} globalAlpha (optional) global frame opacity * @param {String} globalAlpha (optional) global frame opacity
*/ */
drawToCanvas : function (frame, canvas, transparentColor, globalAlpha) { drawToCanvas : function (frame, canvas, transparentColor, globalAlpha) {
var context = canvas.getContext('2d'); var context;
if (canvas.context) {
context = canvas.context;
} else {
context = canvas.context = canvas.getContext('2d');
}
globalAlpha = isNaN(globalAlpha) ? 1 : globalAlpha; globalAlpha = isNaN(globalAlpha) ? 1 : globalAlpha;
context.globalAlpha = globalAlpha; context.globalAlpha = globalAlpha;
transparentColor = transparentColor || Constants.TRANSPARENT_COLOR; transparentColor = transparentColor || Constants.TRANSPARENT_COLOR;
@ -37,26 +44,49 @@
context.fillRect(transparentColor, 0, 0, frame.getWidth(), frame.getHeight()); context.fillRect(transparentColor, 0, 0, frame.getWidth(), frame.getHeight());
context.drawImage(frame.getRenderedFrame(), 0, 0); context.drawImage(frame.getRenderedFrame(), 0, 0);
} else { } else {
for (var x = 0, width = frame.getWidth() ; x < width ; x++) { var w = frame.getWidth();
for (var y = 0, height = frame.getHeight() ; y < height ; y++) { var h = frame.getHeight();
var color = frame.getPixel(x, y); var pixels = frame.pixels;
// accumulate all the pixels of the same color to speed up rendering // Replace transparent color
// by reducting fillRect calls var constantTransparentColorInt = pskl.utils.colorToInt(Constants.TRANSPARENT_COLOR);
var w = 1; var transparentColorInt = pskl.utils.colorToInt(transparentColor);
while (color === frame.getPixel(x, y + w) && (y + w) < height) { if (transparentColorInt != constantTransparentColorInt) {
w++; pixels = frame.getPixels();
for (var i = 0; i < pixels.length; i++) {
if (pixels[i] == constantTransparentColorInt) {
pixels[i] = transparentColorInt;
} }
if (color == Constants.TRANSPARENT_COLOR) {
color = transparentColor;
}
pskl.utils.FrameUtils.renderLine_(color, x, y, w, context);
y = y + w - 1;
} }
} }
// Imagedata from cache
var imageDataKey = w+"-"+h;
var imageData;
if (!imageDataPool[imageDataKey]) {
imageData = imageDataPool[imageDataKey] = context.createImageData(w, h);
} else {
imageData = imageDataPool[imageDataKey];
}
// Convert to uint8 and set the data
var data = new Uint8ClampedArray(pixels.buffer);
var imgDataData = imageData.data;
imgDataData.set(data);
// Offcanvas from cache
var offCanvasKey = w+"-"+h;
var offCanvas;
if (!offCanvasPool[offCanvasKey]) {
offCanvas = offCanvasPool[offCanvasKey] = pskl.utils.CanvasUtils.createCanvas(w, h);
offCanvas.context = offCanvas.getContext('2d');
} else {
offCanvas = offCanvasPool[offCanvasKey];
}
// Put pixel data to offcanvas and draw the offcanvas onto the canvas
offCanvas.context.putImageData(imageData, 0, 0);
context.drawImage(offCanvas, 0, 0, w, h);
context.globalAlpha = 1; context.globalAlpha = 1;
} }
}, },
@ -90,8 +120,9 @@
}, },
mergeFrames_ : function (frameA, frameB) { mergeFrames_ : function (frameA, frameB) {
var transparentColorInt = pskl.utils.colorToInt(Constants.TRANSPARENT_COLOR);
frameB.forEachPixel(function (color, col, row) { frameB.forEachPixel(function (color, col, row) {
if (color != Constants.TRANSPARENT_COLOR) { if (color != transparentColorInt) {
frameA.setPixel(col, row, color); frameA.setPixel(col, row, color);
} }
}); });

View File

@ -91,14 +91,13 @@ if (!Function.prototype.bind) {
}; };
ns.hashCode = function(str) { ns.hashCode = function(str) {
var hash = 0; var hash = 0, i, chr, len;
if (str.length !== 0) { if (str.length === 0) return hash;
for (var i = 0, l = str.length; i < l; i++) { for (i = 0, len = str.length; i < len; i++) {
var chr = str.charCodeAt(i); chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr; hash = hash * 31 + chr;
hash |= 0; // Convert to 32bit integer hash |= 0; // Convert to 32bit integer
} }
}
return hash; return hash;
}; };
@ -120,6 +119,89 @@ if (!Function.prototype.bind) {
}); });
}; };
var colorCache = {};
ns.colorToInt = function (color) {
if (typeof color === 'number') {
return color;
}
if (typeof colorCache[color] !== 'undefined') {
return colorCache[color];
}
var intValue = 0;
// Hexadecimal
if ((color.length == 9 || color.length == 7 || color.length == 3) && color[0] == '#') {
var hex = parseInt(color.substr(1), 16);
if (color.length == 9) {
r = hex >> 24 & 0xff;
g = hex >> 16 & 0xff;
b = hex >> 8 & 0xff;
a = hex & 0xff;
} else if (color.length == 7) {
r = hex >> 16 & 0xff;
g = hex >> 8 & 0xff;
b = hex & 0xff;
a = 255;
} else {
r = hex >> 8 & 0xf * 16;
g = hex >> 4 & 0xf * 16;
b = hex & 0xf * 16;
a = 255;
}
} else if (color.length > 5 && color.substr(0, 5) == 'rgba(') { // RGBA
var rgba = color.substr(5).slice(0, -1).split(',');
r = parseInt(rgba[0]);
g = parseInt(rgba[1]);
b = parseInt(rgba[2]);
a = Math.floor(parseFloat(rgba[3]) * 255);
} else if (color.length > 4 && color.substr(0, 4) == 'rgb(') { // RGB
var rgb = color.substr(4).slice(0, -1).split(',');
r = parseInt(rgb[0]);
g = parseInt(rgb[1]);
b = parseInt(rgb[2]);
} else { // NO HOPE
// Determine color by using the browser itself
d = document.createElement("div");
d.style.color = color;
document.body.appendChild(d);
// Color in RGB
color = window.getComputedStyle(d).color;
document.body.removeChild(d);
return pskl.utils.colorToInt(color);
}
intValue = (a << 24 >>> 0) + (b << 16) + (g << 8) + r;
colorCache[color] = intValue;
colorCacheReverse[intValue] = color;
return intValue;
};
var colorCacheReverse = {};
ns.intToColor = function(intValue) {
if (typeof colorCache[color] !== 'undefined') {
return colorCache[color];
}
if (typeof colorCacheReverse[intValue] !== 'undefined') {
return colorCacheReverse[intValue];
}
var r = intValue & 0xff;
var g = intValue >> 8 & 0xff;
var b = intValue >> 16 & 0xff;
var a = (intValue >> 24 >>> 0 & 0xff) / 255;
var color = 'rgba('+r+','+g+','+b+','+a+')';
colorCache[color] = intValue;
colorCacheReverse[intValue] = color;
return color;
};
var reEntityMap = {}; var reEntityMap = {};
ns.unescapeHtml = function (string) { ns.unescapeHtml = function (string) {
Object.keys(entityMap).forEach(function(key) { Object.keys(entityMap).forEach(function(key) {

View File

@ -60,10 +60,12 @@
}; };
ns.Deserializer.prototype.loadExpandedLayer = function (layerData, index) { ns.Deserializer.prototype.loadExpandedLayer = function (layerData, index) {
var width = this.piskel_.getWidth();
var height = this.piskel_.getHeight();
var layer = new pskl.model.Layer(layerData.name); var layer = new pskl.model.Layer(layerData.name);
layer.setOpacity(layerData.opacity); layer.setOpacity(layerData.opacity);
var frames = layerData.grids.map(function (grid) { var frames = layerData.grids.map(function (grid) {
return pskl.model.Frame.fromPixelGrid(grid); return pskl.model.Frame.fromPixelGrid(grid, width, height);
}); });
this.addFramesToLayer(frames, layer, index); this.addFramesToLayer(frames, layer, index);
return layer; return layer;

View File

@ -28,7 +28,7 @@
frameCount : frames.length frameCount : frames.length
}; };
if (expanded) { if (expanded) {
layerToSerialize.grids = frames.map(function (f) {return f.pixels;}); layerToSerialize.grids = frames.map(function (f) {return Array.prototype.slice.call(f.pixels);});
return layerToSerialize; return layerToSerialize;
} else { } else {
var renderer = new pskl.rendering.FramesheetRenderer(frames); var renderer = new pskl.rendering.FramesheetRenderer(frames);

View File

@ -2,7 +2,7 @@
var ns = $.namespace('pskl.worker.framecolors'); var ns = $.namespace('pskl.worker.framecolors');
ns.FrameColors = function (frame, onSuccess, onStep, onError) { ns.FrameColors = function (frame, onSuccess, onStep, onError) {
this.serializedFrame = JSON.stringify(frame.pixels); this.pixels = frame.pixels;
this.onStep = onStep; this.onStep = onStep;
this.onSuccess = onSuccess; this.onSuccess = onSuccess;
@ -13,9 +13,7 @@
}; };
ns.FrameColors.prototype.process = function () { ns.FrameColors.prototype.process = function () {
this.worker.postMessage({ this.worker.postMessage(this.pixels);
serializedFrame : this.serializedFrame
});
}; };
ns.FrameColors.prototype.onWorkerMessage = function (event) { ns.FrameColors.prototype.onWorkerMessage = function (event) {

View File

@ -36,10 +36,11 @@
var getFrameColors = function (frame) { var getFrameColors = function (frame) {
var frameColors = {}; var frameColors = {};
for (var x = 0 ; x < frame.length ; x++) { var transparentColorInt = 0; // TODO: Fix magic number
for (var y = 0 ; y < frame[x].length ; y++) { for (var i = 0; i < frame.length; i++) {
var color = frame[x][y]; var color = frame[i];
var hexColor = toHexString_(color); if (color !== transparentColorInt) {
var hexColor = rgbToHex(color & 0xff, color >> 16 & 0xff, color >> 8 & 0xff);
frameColors[hexColor] = true; frameColors[hexColor] = true;
} }
} }
@ -48,8 +49,7 @@
this.onmessage = function(event) { this.onmessage = function(event) {
try { try {
var data = event.data; var frame = event.data;
var frame = JSON.parse(data.serializedFrame);
var colors = getFrameColors(frame); var colors = getFrameColors(frame);
this.postMessage({ this.postMessage({
type : 'SUCCESS', type : 'SUCCESS',

View File

@ -18,9 +18,9 @@ describe("Canvas Renderer test", function() {
var frameFromCanvas = pskl.utils.FrameUtils.createFromImage(canvas); var frameFromCanvas = pskl.utils.FrameUtils.createFromImage(canvas);
expect(frameFromCanvas.getPixel(0,0)).toBe(BLACK); test.testutils.colorEqualsColor(frameFromCanvas.getPixel(0,0), BLACK);
expect(frameFromCanvas.getPixel(0,1)).toBe(WHITE); test.testutils.colorEqualsColor(frameFromCanvas.getPixel(0,1), WHITE);
expect(frameFromCanvas.getPixel(1,0)).toBe(WHITE); test.testutils.colorEqualsColor(frameFromCanvas.getPixel(1,0), WHITE);
expect(frameFromCanvas.getPixel(1,1)).toBe(BLACK); test.testutils.colorEqualsColor(frameFromCanvas.getPixel(1,1), BLACK);
}); });
}); });

View File

@ -198,7 +198,7 @@ describe("SelectionManager suite", function() {
var checkContainsPixel = function (pixels, row, col, color) { var checkContainsPixel = function (pixels, row, col, color) {
var containsPixel = pixels.some(function (pixel) { var containsPixel = pixels.some(function (pixel) {
return pixel.row == row && pixel.col == col && pixel.color == color; return pixel.row == row && pixel.col == col && test.testutils.compareColor(pixel.color, color);
}); });
expect(containsPixel).toBe(true); expect(containsPixel).toBe(true);
}; };

View File

@ -38,7 +38,7 @@
ns.frameEqualsGrid = function (frame, grid) { ns.frameEqualsGrid = function (frame, grid) {
frame.forEachPixel(function (color, col, row) { frame.forEachPixel(function (color, col, row) {
expect(color).toBe(grid[row][col]); ns.colorEqualsColor(color, grid[row][col]);
}); });
}; };
@ -47,10 +47,18 @@
for (var y = 0 ; y < grid[x].length ; y++) { for (var y = 0 ; y < grid[x].length ; y++) {
var expected = tinycolor(grid[x][y]).toRgbString(); var expected = tinycolor(grid[x][y]).toRgbString();
var color = tinycolor(ns.getRgbaAt(image, x, y)).toRgbString(); var color = tinycolor(ns.getRgbaAt(image, x, y)).toRgbString();
expect(color).toBe(expected); ns.colorEqualsColor(color, expected);
}
} }
} }
};
ns.compareColor = function(colorA, colorB) {
return pskl.utils.colorToInt(colorA) === pskl.utils.colorToInt(colorB);
};
ns.colorEqualsColor = function (color, expected) {
expect(pskl.utils.colorToInt(color)).toBe(pskl.utils.colorToInt(expected));
};
ns.getRgbaAt = function (image, x, y) { ns.getRgbaAt = function (image, x, y) {
var w = image.width; var w = image.width;