From da89573679309ad3c8ed11b93a806ea6384ba6fb Mon Sep 17 00:00:00 2001 From: Freeyorp Date: Wed, 26 Jun 2013 02:58:35 +1200 Subject: Implement dyeImage Split loadImage into a common resource.js Add a compatability check for both canvas-node and browser functionality Note that the generated dyed image is off-by-one to the tested TMWW image. The algorithm needs verification and possibly correction. Either way, it's close enough by the eye. --- public/js/mp/dye.js | 39 +++++++++++++++++++++++++++++ public/js/mp/resource.js | 36 +++++++++++++++++++++++++++ public/playground.html | 32 ++++++++++++++++++++++++ test/load.js | 9 ++++++- test/mp/dye.js | 2 +- test/mp/future.js | 64 ++++++++++++++++++++++++++++-------------------- 6 files changed, 153 insertions(+), 29 deletions(-) create mode 100644 public/js/mp/resource.js create mode 100644 public/playground.html diff --git a/public/js/mp/dye.js b/public/js/mp/dye.js index 530b6c4..7cf8da5 100644 --- a/public/js/mp/dye.js +++ b/public/js/mp/dye.js @@ -28,11 +28,50 @@ var mp = function(mp) { return { channel: channel[idx], intensity: max }; } + /* + * Return a dye specification from a dye string. + */ function parseDyeString(dyeString) { /* TODO */ } + /* + * Dye the internal image data based on the specification provided by dyeData. + * The specification can be generated from a dyeString by parseDyeString. + * The array passed in will be modified. + */ function dyeImage(imageData, dyeData) { + for (var p = 0; p < imageData.length; p += 4) { + var pixel = [imageData[p], imageData[p + 1], imageData[p + 2]]; + var alpha = imageData[p + 3]; + if (!alpha) { + continue; + } + + var channel = getChannel(pixel); + var channelId = channel.channel; + + if (!channelId || !(channelId in dyeData) || !dyeData[channelId].length) { + continue; + } + + var intensity = channel.intensity; + var val = intensity * dyeData[channelId].length + var i = Math.floor(val / 255); + var t = val - i * 255; + if (!t) { + --i; + imageData[p ] = dyeData[channelId][i][0]; + imageData[p + 1] = dyeData[channelId][i][1]; + imageData[p + 2] = dyeData[channelId][i][2]; + continue; + } + + imageData[p ] = ((255 - t) * (i && dyeData[channelId][i - 1][0]) + t * dyeData[channelId][i][0]) / 255; + imageData[p + 1] = ((255 - t) * (i && dyeData[channelId][i - 1][1]) + t * dyeData[channelId][i][1]) / 255; + imageData[p + 2] = ((255 - t) * (i && dyeData[channelId][i - 1][2]) + t * dyeData[channelId][i][2]) / 255; + } /* TODO */ + return imageData; } return mp; }(mp || {}); diff --git a/public/js/mp/resource.js b/public/js/mp/resource.js new file mode 100644 index 0000000..f348a89 --- /dev/null +++ b/public/js/mp/resource.js @@ -0,0 +1,36 @@ +"use strict"; +var mp = function(mp) { + mp.resource = { + loadImage: loadImage + }; + + /* + * Quick compatability workaround + * node testing environment needs new Canvas() and won't tolerate document.createElement("canvas") + * A createCanvas method is therefore provided in its sandbox which can be quickly checked to determine the method needed + */ + var createCanvas = "createCanvas" in document ? document.createCanvas : function() { return document.createElement("canvas"); }; + + var canvas = createCanvas(); + var context = canvas.getContext("2d"); + + /* + * Load in an image given a URL. + * The provided callback will fire when loading is complete. + * The parameters will be false and the the imageData if successful, and false and the error otherwise. + */ + function loadImage(url, callback) { + var image = new Image(); + image.onload = function() { + canvas.width = image.width; + canvas.height = image.height; + context.drawImage(image, 0, 0); + callback(false, context.getImageData(0, 0, image.width, image.height)); + }; + image.onerror = function(err) { + callback(true, err); + }; + image.src = url; + } + return mp; +}(mp || {}); diff --git a/public/playground.html b/public/playground.html new file mode 100644 index 0000000..3099b83 --- /dev/null +++ b/public/playground.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + diff --git a/test/load.js b/test/load.js index 97bee69..57e838e 100644 --- a/test/load.js +++ b/test/load.js @@ -2,7 +2,8 @@ process.env.TZ = "UTC"; var smash = require("smash"), - jsdom = require("jsdom"); + jsdom = require("jsdom"), + Canvas = require("canvas"); module.exports = function() { var files = [].slice.call(arguments).map(function(d) { return "public/js/" + d; }), @@ -40,10 +41,16 @@ module.exports = function() { }; }; + document.createCanvas = function() { + return new Canvas(); + } + sandbox = { console: console, document: document, window: document.createWindow(), + Canvas: Canvas, + Image: Canvas.Image }; return topic; diff --git a/test/mp/dye.js b/test/mp/dye.js index 36acfa3..26b33c2 100644 --- a/test/mp/dye.js +++ b/test/mp/dye.js @@ -7,7 +7,7 @@ var suite = vows.describe("mp.dye"); suite.addBatch({ "The manaportal dye": { - topic: load("mp/dye").expression("mp.dye"), + topic: load("mp/dye").expression("mp.dye").document(), "getChannel": { topic: function(dye) { return dye.getChannel; }, "returns null given pure black": function(f) { diff --git a/test/mp/future.js b/test/mp/future.js index 6f491ae..cf0e68a 100644 --- a/test/mp/future.js +++ b/test/mp/future.js @@ -2,15 +2,10 @@ var vows = require("vows"), load = require("../load"), assert = require("assert"), - jsdom = require("jsdom"), - Canvas = require("canvas"), - Image = Canvas.Image; + jsdom = require("jsdom"); var suite = vows.describe("mp.dye"); -var canvas = new Canvas(32,32); -var context = canvas.getContext("2d"); - var dyeString = "R:#ede5b2,fff7bf;G:#cccccc,ffffff"; var dyeData = { "R": [ @@ -23,40 +18,55 @@ var dyeData = { ] }; -function loadImage(url, tests) { +function assertImageDataEqual(input, expected, actual, width) { + assert.equal(actual.length, expected.length, "expected same " + expected.length + " pixel components, found " + actual.length); + for (var i = 0; i != actual.length; i += 4) { + var p = i / 4; + var y = Math.floor(p / width); + var x = p - y * width; + var msg = "At (" + x + "," + y + "): " + + "Input rgba(" + input [i ] + "," + input [i + 1] + "," + input [i + 2] + "," + input [i + 3] + ") " + + "should dye to rgba(" + expected[i ] + "," + expected[i + 1] + "," + expected[i + 2] + "," + expected[i + 3] + "); " + + "found rgba(" + actual [i ] + "," + actual [i + 1] + "," + actual [i + 2] + "," + actual [i + 3] + ")"; + assert.equal(actual[i ], expected[i ], msg); + assert.equal(actual[i + 1], expected[i + 1], msg); + assert.equal(actual[i + 2], expected[i + 2], msg); + assert.equal(actual[i + 3], expected[i + 3], msg); + } +} + +function unshiftLoadImageBind(url, tests) { tests.topic = function() { - var image = new Image; + var mp = arguments[arguments.length - 1]; var tester = this; - var callback = this.callback; var args = arguments; - image.onload = function() { - canvas.width = image.width; - canvas.height = image.height; - context.drawImage(image, 0, 0); - callback = callback.bind(tester, false, context.getImageData(0, 0, image.width, image.height)); - callback.apply(tester, args); - } - image.onerror = function(err) { - throw new Error("Error loading '" + url + "': " + err); - } - image.src = url; + mp.resource.loadImage(url, function(err, data) { + if (err) { + throw new Error("Error loading '" + url + "': " + data); + } + tester.callback.bind(tester, err, data).apply(tester, args); + }); }; return tests; } suite.addBatch({ "The manaportal dye": { - topic: load("mp/dye").expression("mp.dye").document(), + topic: load("mp/dye", "mp/resource").expression("mp").document(), "parseDyeString": { - "Extracts a the dye channel data from the dyestring": function(dye) { - assert.equal(dye.parseDyeString(dyeString), dyeData); + "Extracts the dye channel data from the dyestring": function(mp) { + assert.equal(mp.dye.parseDyeString(dyeString), dyeData); } }, "dyeImage": { - "with the big recolorable cake": loadImage("test/data/bigcake.png", { - "to the big white cake": loadImage("test/data/whitecake.png", { - "dyes correctly when given the correct dye data": function(err1, whiteCake, dyeableCake, dye) { - assert.deepEqual(dye.dyeImage(dyeableCake.data, dyeData), whiteCake.data); + "with the big recolorable cake": unshiftLoadImageBind("test/data/bigcake.png", { + "to the big white cake": unshiftLoadImageBind("test/data/whitecake.png", { + "dyes correctly when given the correct dye data": function(err, whiteCake, dyeableCake, mp) { + var input = dyeableCake.data; + var expected = whiteCake.data; + var actual = new Uint8ClampedArray(input); + mp.dye.dyeImage(actual, dyeData); + assertImageDataEqual(input, expected, actual, whiteCake.width); } }) }) -- cgit v1.2.3-60-g2f50