From f5c98cf0b38a84562a068463aec3d4326b1fe371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Szab=C3=B3?= Date: Wed, 24 Aug 2016 23:07:36 +0200 Subject: [PATCH] Rework pixel storage, manipulation, rendering --- src/js/model/Frame.js | 84 ++++++++++++----- src/js/rendering/FramesheetRenderer.js | 11 +-- src/js/tools/drawing/ColorSwap.js | 5 +- src/js/tools/drawing/Lighten.js | 8 +- src/js/tools/transform/TransformUtils.js | 13 +-- src/js/utils/FrameUtils.js | 65 +++++++++---- src/js/utils/core.js | 94 +++++++++++++++++-- src/js/utils/serialization/Deserializer.js | 4 +- src/js/utils/serialization/Serializer.js | 2 +- src/js/worker/framecolors/FrameColors.js | 6 +- .../worker/framecolors/FrameColorsWorker.js | 14 +-- test/js/rendering/CanvasRendererTest.js | 8 +- test/js/selection/SelectionManagerTest.js | 2 +- test/js/testutils/TestUtils.js | 14 ++- 14 files changed, 242 insertions(+), 88 deletions(-) diff --git a/src/js/model/Frame.js b/src/js/model/Frame.js index 51958fa7..bc327ba9 100644 --- a/src/js/model/Frame.js +++ b/src/js/model/Frame.js @@ -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); } } }; diff --git a/src/js/rendering/FramesheetRenderer.js b/src/js/rendering/FramesheetRenderer.js index 2e325e17..6f36f361 100644 --- a/src/js/rendering/FramesheetRenderer.js +++ b/src/js/rendering/FramesheetRenderer.js @@ -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) { diff --git a/src/js/tools/drawing/ColorSwap.js b/src/js/tools/drawing/ColorSwap.js index 42c648c9..20184500 100644 --- a/src/js/tools/drawing/ColorSwap.js +++ b/src/js/tools/drawing/ColorSwap.js @@ -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++; diff --git a/src/js/tools/drawing/Lighten.js b/src/js/tools/drawing/Lighten.js index 2ad6e3c3..3af0f062 100644 --- a/src/js/tools/drawing/Lighten.js +++ b/src/js/tools/drawing/Lighten.js @@ -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. diff --git a/src/js/tools/transform/TransformUtils.js b/src/js/tools/transform/TransformUtils.js index 042b58e0..28c002c0 100644 --- a/src/js/tools/transform/TransformUtils.js +++ b/src/js/tools/transform/TransformUtils.js @@ -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++; diff --git a/src/js/utils/FrameUtils.js b/src/js/utils/FrameUtils.js index bc5d97b2..af2c74e8 100644 --- a/src/js/utils/FrameUtils.js +++ b/src/js/utils/FrameUtils.js @@ -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); } }); diff --git a/src/js/utils/core.js b/src/js/utils/core.js index 7525d013..56a71d2d 100644 --- a/src/js/utils/core.js +++ b/src/js/utils/core.js @@ -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) { diff --git a/src/js/utils/serialization/Deserializer.js b/src/js/utils/serialization/Deserializer.js index 01db4abe..e02320a0 100644 --- a/src/js/utils/serialization/Deserializer.js +++ b/src/js/utils/serialization/Deserializer.js @@ -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; diff --git a/src/js/utils/serialization/Serializer.js b/src/js/utils/serialization/Serializer.js index b946ad8e..9666fa06 100644 --- a/src/js/utils/serialization/Serializer.js +++ b/src/js/utils/serialization/Serializer.js @@ -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); diff --git a/src/js/worker/framecolors/FrameColors.js b/src/js/worker/framecolors/FrameColors.js index ea4eb15c..fbf9fcbd 100644 --- a/src/js/worker/framecolors/FrameColors.js +++ b/src/js/worker/framecolors/FrameColors.js @@ -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) { diff --git a/src/js/worker/framecolors/FrameColorsWorker.js b/src/js/worker/framecolors/FrameColorsWorker.js index 65a87a2d..33a80006 100644 --- a/src/js/worker/framecolors/FrameColorsWorker.js +++ b/src/js/worker/framecolors/FrameColorsWorker.js @@ -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', diff --git a/test/js/rendering/CanvasRendererTest.js b/test/js/rendering/CanvasRendererTest.js index a4a04e62..31183ad8 100644 --- a/test/js/rendering/CanvasRendererTest.js +++ b/test/js/rendering/CanvasRendererTest.js @@ -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); }); }); \ No newline at end of file diff --git a/test/js/selection/SelectionManagerTest.js b/test/js/selection/SelectionManagerTest.js index 44b20c39..88cb5dbc 100644 --- a/test/js/selection/SelectionManagerTest.js +++ b/test/js/selection/SelectionManagerTest.js @@ -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); }; diff --git a/test/js/testutils/TestUtils.js b/test/js/testutils/TestUtils.js index 156f2be6..c7ae05b6 100644 --- a/test/js/testutils/TestUtils.js +++ b/test/js/testutils/TestUtils.js @@ -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;