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) {
if (pixels.length && pixels[0].length) {
var w = pixels.length;
var h = pixels[0].length;
ns.Frame.fromPixelGrid = function (pixels, width, height) {
if (pixels.length) {
if (pixels[0].length) {
var w = pixels.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);
frame.setPixels(pixels);
frame.setPixels(buffer);
return frame;
} 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) {
var pixels = [];
for (var columnIndex = 0 ; columnIndex < width ; columnIndex++) {
var columnArray = [];
for (var heightIndex = 0 ; heightIndex < height ; heightIndex++) {
columnArray.push(Constants.TRANSPARENT_COLOR);
var pixels;
var key = width+"-"+height;
if (_emptyPixelGridCache[key]) {
pixels = _emptyPixelGridCache[key];
} 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;
}
} else {
pixels.fill(transparentColorInt);
}
pixels[columnIndex] = columnArray;
}
return pixels;
return new Uint32Array(pixels);
};
ns.Frame.createEmptyFromFrame = function (frame) {
@ -44,7 +73,7 @@
ns.Frame.prototype.clone = function () {
var clone = new ns.Frame(this.width, this.height);
clone.setPixels(this.getPixels());
clone.setPixels(this.pixels);
return clone;
};
@ -64,8 +93,9 @@
};
ns.Frame.prototype.clear = function () {
var pixels = ns.Frame.createEmptyPixelGrid_(this.getWidth(), this.getHeight());
this.setPixels(pixels);
this.pixels = ns.Frame.createEmptyPixelGrid_(this.getWidth(), this.getHeight());
this.version++;
// this.setPixels(pixels);
};
/**
@ -73,11 +103,10 @@
* @private
*/
ns.Frame.prototype.clonePixels_ = function (pixels) {
var clonedPixels = [];
for (var col = 0 ; col < pixels.length ; col++) {
clonedPixels[col] = pixels[col].slice(0 , pixels[col].length);
}
return clonedPixels;
//npixels = new Uint32Array(pixels.length);
//npixels.set(pixels);
return new Uint32Array(pixels);
};
ns.Frame.prototype.getHash = function () {
@ -86,9 +115,14 @@
ns.Frame.prototype.setPixel = function (x, y, color) {
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) {
this.pixels[x][y] = color || Constants.TRANSPARENT_COLOR;
this.pixels[index] = color || pskl.utils.colorToInt(Constants.TRANSPARENT_COLOR);
this.version++;
}
}
@ -96,7 +130,7 @@
ns.Frame.prototype.getPixel = function (x, y) {
if (this.containsPixel(x, y)) {
return this.pixels[x][y];
return this.pixels[y * this.width + x];
} else {
return null;
}
@ -107,7 +141,7 @@
var height = this.getHeight();
for (var x = 0 ; x < width ; x++) {
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) {
var context = canvas.getContext('2d');
frame.forEachPixel(function (color, x, y) {
if (color != Constants.TRANSPARENT_COLOR) {
context.fillStyle = color;
context.fillRect(x + offsetWidth, y + offsetHeight, 1, 1);
}
});
var imageData = context.createImageData(frame.getWidth(), frame.getHeight());
var pixels = frame.getPixels();
var data = new Uint8ClampedArray(pixels.buffer);
imageData.data.set(data);
context.putImageData(imageData, offsetWidth, offsetHeight);
};
ns.FramesheetRenderer.prototype.createCanvas_ = function (columns, rows) {

View File

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

View File

@ -43,10 +43,10 @@
var overlayColor = overlay.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 isTransparent = pixelColor === Constants.TRANSPARENT_COLOR;
var isTransparent = pixelColor === pskl.utils.colorToInt(Constants.TRANSPARENT_COLOR);
if (isTransparent) {
return Constants.TRANSPARENT_COLOR;
}
@ -61,9 +61,9 @@
var color;
if (isDarken) {
color = window.tinycolor.darken(pixelColor, step);
color = window.tinycolor.darken(pskl.utils.intToColor(pixelColor), step);
} else {
color = window.tinycolor.lighten(pixelColor, step);
color = window.tinycolor.lighten(pskl.utils.intToColor(pixelColor), step);
}
// Convert tinycolor color to string format.

View File

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

View File

@ -1,6 +1,8 @@
(function () {
var ns = $.namespace('pskl.utils');
var colorCache = {};
var offCanvasPool = {};
var imageDataPool = {};
ns.FrameUtils = {
/**
* Render a Frame object as an image.
@ -28,7 +30,12 @@
* @param {String} globalAlpha (optional) global frame opacity
*/
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;
context.globalAlpha = globalAlpha;
transparentColor = transparentColor || Constants.TRANSPARENT_COLOR;
@ -37,26 +44,49 @@
context.fillRect(transparentColor, 0, 0, frame.getWidth(), frame.getHeight());
context.drawImage(frame.getRenderedFrame(), 0, 0);
} else {
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 = frame.getWidth();
var h = frame.getHeight();
var pixels = frame.pixels;
// 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++;
// Replace transparent color
var constantTransparentColorInt = pskl.utils.colorToInt(Constants.TRANSPARENT_COLOR);
var transparentColorInt = pskl.utils.colorToInt(transparentColor);
if (transparentColorInt != constantTransparentColorInt) {
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;
}
},
@ -90,8 +120,9 @@
},
mergeFrames_ : function (frameA, frameB) {
var transparentColorInt = pskl.utils.colorToInt(Constants.TRANSPARENT_COLOR);
frameB.forEachPixel(function (color, col, row) {
if (color != Constants.TRANSPARENT_COLOR) {
if (color != transparentColorInt) {
frameA.setPixel(col, row, color);
}
});

View File

@ -91,13 +91,12 @@ if (!Function.prototype.bind) {
};
ns.hashCode = function(str) {
var hash = 0;
if (str.length !== 0) {
for (var i = 0, l = str.length; i < l; i++) {
var chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
var hash = 0, i, chr, len;
if (str.length === 0) return hash;
for (i = 0, len = str.length; i < len; i++) {
chr = str.charCodeAt(i);
hash = hash * 31 + chr;
hash |= 0; // Convert to 32bit integer
}
}
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 = {};
ns.unescapeHtml = function (string) {
Object.keys(entityMap).forEach(function(key) {

View File

@ -60,10 +60,12 @@
};
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);
layer.setOpacity(layerData.opacity);
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);
return layer;

View File

@ -28,7 +28,7 @@
frameCount : frames.length
};
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;
} else {
var renderer = new pskl.rendering.FramesheetRenderer(frames);

View File

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

View File

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

View File

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

View File

@ -38,7 +38,7 @@
ns.frameEqualsGrid = function (frame, grid) {
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++) {
var expected = tinycolor(grid[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) {
var w = image.width;