Overriding CanvasRenderingContext2D.getImageData() - javascript

I am trying to override the built in method CanvasRenderingContext2D.getImageData(). I would like to override the implementation so that the modified function uses the canvas context to modify the canvas and then calls the original function which should return different data that if the function was not overridden. The reason I am doing this is to prevent browser fingerprinting.
canvas.js
(function(){
'use strict';
var originalGetImageData = CanvasRenderingContext2D.prototype.getImageData;
// This function just adds 1 to each RGBA component in the array for testing.
// Will add random values for the real thing.
function randomiseImageData(image) {
var imageData = image.data;
var imageLength = imageData.length;
for (var i = 0; i < imageLength; i++) {
imageData[i] += 1;
}
var modifiedImage = new ImageData(image.width, image.height);
return modifiedImage;
}
CanvasRenderingContext2D.prototype.getImageData = function(sx, sy, sw, sh) {
console.log("[ALERT] " + window.location.hostname + " called CanvasRenderingContext2D.getImageData()");
const origin = window.location.hostname;
Math.seedrandom(origin);
var image = originalGetImageData.call(this, sx, sy, sw, sh);
return randomiseImageData(image);
};
})();

You are returning a new empty ImageData object.
I guess what you want is to return the filled one.
Since you already modified the data array, you can simply return the original ImageData, your modifications will have been made.
// shortened version
(function(){
const ori = CanvasRenderingContext2D.prototype.getImageData;
CanvasRenderingContext2D.prototype.getImageData = function(){
let imageData = ori.apply(this, arguments);
// modify the Uint8Array
imageData.data.forEach((v, i, a) => a[i]+=1);
// return the now modified ImageData
return imageData;
};
})()
var ctx = document.createElement('canvas').getContext('2d');
console.log(ctx.getImageData(0,0,1,1));
If you really want to create a new ImageData, then it's
new ImageData(imageData, image.width, image.height);
// ^^
// pass the data to fill the new ImageData object
But note that browser support is not great, and that you won't win anything by doing so.

You can not remove the fingerprint.
For more The Web never forgets
You can not circumvent fingerprinting. The best you can do is return the most common fingerprint (which is not easily ascertained) increasing the set of devices you may belong to.
Returning a random set of pixels (or incrementing each pixel channel by one) is about the worst you can do if you are the only one doing it. It would absolutly mark your browser as unique and would let traking software know that the browser that returns changed data is just one, or one of a very small set.
The best way to stop fingerprinting is via a common and widely adopted data return strategy. If every browser returned all zero (transparent black) then there would be no uniqueness and thus no way to track the device based on the canvas.
Canvas fingerprinting is only part of a fingerprint, there are many more sources of data the help identify a device. The browser, browser version, OS, OS version, screen resolution, and a long list of others. Even if you eliminate the canvas as a source of uniqueness it is pointless unless you do the same with the rest of the information.
Mitigation
So with that said the code to return zeroed data is as follows.
(function () {
if (window.CanvasRenderingContext2D) {
const gid = CanvasRenderingContext2D.prototype.getImageData;
CanvasRenderingContext2D.prototype.getImageData = function (x, y, w, h) {
var data = gid.bind(this)(x, y, w, h);
data.data.fill(0); // fill with zero
return data;
}
// Token way to avoid JS from finding out that you have overwritten the prototype overwrite
// the toString method as well (note ctx.getImageData.toString.toString() will
// still show you have changed the prototype but over writing Object.toSting is not worth the problems)
CanvasRenderingContext2D.prototype.getImageData.toString = function () {
return "function getImageData() { [native code] }";
}
}
}());

Related

Is there any maps per page limitation in mapbox-gl?

I'm trying to have 17 small maps on the same page using mapbox-gl and facing:
WARNING: Too many active WebGL contexts. Oldest context will be lost.
Uncaught TypeError: Failed to execute 'shaderSource' on 'WebGLRenderingContext': parameter 1 is not of type 'WebGLShader'.
at new Program (mapbox-gl.js:182)
at Painter._createProgramCached (mapbox-gl.js:178)
at Painter.useProgram (mapbox-gl.js:178)
at setFillProgram (mapbox-gl.js:154)
at drawFillTile (mapbox-gl.js:154)
at drawFillTiles (mapbox-gl.js:154)
at Object.drawFill [as fill] (mapbox-gl.js:154)
at Painter.renderLayer (mapbox-gl.js:178)
at Painter.render (mapbox-gl.js:178)
at e._render (mapbox-gl.js:497)
I had the same issue when i tried to have many google streetview galleries on the same page, but as my streetview shouldn't be visible at the same moment i ended using the same streetview changing address dynamically.
But for maps list requirement is to show that many maps to user. Can't show them one by one. Not sure how i could work out that issue.
i'm using mapbox-gl#0.45.0, and testing it in chrome Version 66.0.3359.181 (Official Build) (64-bit) on Mac OS Sierra 10.12.6 (16G1036)
I'm going to guess you are out of luck. Browsers limit the number of WebGL instances. There are workarounds but to use them would probably require changes to the way mapbox-gl is implemented. I suggest you ask them if they'd consider implementing one of the workarounds assuming they haven't already.
There is one other possibility that comes to mind and that would be to do your own virtualization of WebGL in JavaScript. That's probably not a good solution though because it wouldn't share resources across maps and it might be too heavy.
Off the top of my head you'd have to create an offscreen canvas and override HTMLCanvasElement.prototype.getContext so that when someone makes a webgl context you return a virtual context. You'd wrap every function and if that virtual context doesn't match the last used virtual context you'd save all the webgl state and restore the state for the new context. You'd also have to keep framebuffers to match the drawingbuffer for each canvas, bind them when the current framebuffer binding is null and resize them if the canvas sized changed, and then render to the offscreen canvas and then canvas2d.drawImage to their respective canvases anytime the current event exits. It's that last part that would be heaviest.
In semi-pseudo code
// This is just off the top of my head and is just pseudo code
// but hopefully gives an idea of how to virtualize WebGL.
const canvasToVirtualContextMap = new Map();
let currentVirtualContext = null;
let sharedWebGLContext;
const baseState = makeDefaultState();
HTMLCanvasElement.prototype.getContext = (function(origFn) {
return function(type, contextAttributes) {
if (type === 'webgl') {
return createOrGetVirtualWebGLContext(this, type, contextAttributes);
}
return origFn.call(this, contextAttributes);
};
}(HTMLCanvasElement.prototype.getContext));
class VirutalWebGLContext {
constructor(cavnas, contextAttributes) {
this.canvas = canvas;
// based on context attributes and canvas.width, canvas.height
// create a texture and framebuffer
this._drawingbufferTexture = ...;
this._drawingbufferFramebuffer = ...;
// remember all WebGL state (default bindings, default texture units,
// default attributes and/or vertex shade object, default program,
// default blend, stencil, zbuffer, culling, viewport etc... state
this._state = makeDefaultState();
}
}
function makeDefaultState() {
const state ={};
state[WebGLRenderingContext.ARRAY_BUFFER] = null;
... tons more ...
}
// copy all WebGL constants and functions to the prototype of
// VirtualWebGLContext
for (let key in WebGLRenderingContext.protoype) {
const value = WebGLRenderingContext.prototype[key];
let newValue = value;
switch (key) {
case 'bindFramebuffer':
newValue = virutalBindFramebuffer;
break;
case 'clear':
case 'drawArrays':
case 'drawElements':
newValue = createDrawWrapper(value);
break;
default:
if (typeof value === 'function') {
newValue = createWrapper(value);
}
break;
}
VirtualWebGLContext.prototype[key] = newValue;
}
function virutalBindFramebuffer(bindpoint, framebuffer) {
if (bindpoint === WebGLRenderingContext.FRAMEBUFFER) {
if (target === null) {
// bind our drawingBuffer
sharedWebGLContext.bindFramebuffer(bindpoint, this._drawingbufferFramebuffer);
}
}
sharedWebGLContext.bindFramebuffer(bindpoint, framebuffer);
}
function createWrapper(origFn) {
// lots of optimization could happen here depending on specific functions
return function(...args) {
makeCurrentContext(this);
resizeCanvasIfChanged(this);
return origFn.call(sharedWebGLContext, ...args);
};
}
function createDrawWrapper(origFn) {
const newFn = createWrapper(origFn);
return function(...args) {
// a rendering function was called so we need to copy are drawingBuffer
// to the canvas for this context after the current event.
this._needComposite = true;
return newFn.call(this, ...args);
};
}
function makeCurrentContext(vctx) {
if (currentVirtualContext === vctx) {
return;
}
// save all current WebGL state on the previous current virtual context
saveAllState(currentVirutalContext._state);
// restore all state for the
restoreAllState(vctx._state);
// check if the current state is supposed to be rendering to the canvas.
// if so bind vctx._drawingbuffer
currentVirtualContext = vctx;
}
function resizeCanvasIfChanged(vctx) {
if (canvas.width !== vtx._width || canvas.height !== vctx._height) {
// resize this._drawingBuffer to match the new canvas size
}
}
function createOrGetVirtualWebGLContext(canvas, type, contextAttributes) {
// check if this canvas already has a context
const existingVirtualCtx = canvasToVirtualContextMap.get(canvas);
if (existingVirtualCtx) {
return existingVirtualCtx;
}
if (!sharedWebGLContext) {
sharedWebGLContext = document.createElement("canvas").getContext("webgl");
}
const newVirtualCtx = new VirtualWebGLContext(canvas, contextAttributes);
canvasToVirtualContextMap.set(canvas, newVirtualCtx);
return newVirtualCtx;
}
function saveAllState(state) {
// save all WebGL state (current bindings, current texture units,
// current attributes and/or vertex shade object, current program,
// current blend, stencil, zbuffer, culling, viewport etc... state
state[WebGLRenderingContext.ARRAY_BUFFER] = sharedGLState.getParameter(gl.ARRAY_BUFFER_BINDING);
state[WebGLRenderingContext.TEXTURE_2D] = sharedGLState.getParameter(gl.TEXTURE_BINDING_2D);
... tons more ...
}
function restoreAllState(state) {
// resture all WebGL state (current bindings, current texture units,
// current attributes and/or vertex shade object, current program,
// current blend, stencil, zbuffer, culling, viewport etc... state
gl.bindArray(gl.ARRAY_BUFFER, state[WebGLRenderingContext.ARRAY_BUFFER]);
gl.bindTexture(gl.TEXTURE_2D, state[WebGLRenderingContext.TEXTURE_2D]);
... tons more ...
}
function renderAllDirtyVirtualCanvas() {
let setup = false;
for (const vctx of canvasToVirtualContextMap.values()) {
if (!vctx._needComposite) {
continue;
}
vctx._needComposite = false;
if (!setup) {
setup = true;
// save all current WebGL state on the previous current virtual context
saveAllState(currentVirutalContext._state);
currentVirutalContext = null;
// set the state back to the default
restoreAllState(sharedGlContext, baseState);
// setup whatever state we need to render vctx._drawinbufferTexture
// to the canvas.
sharedWebGLContext.useProgram(programToRenderCanvas);
...
}
// draw the drawingbuffer's texture to the canvas
sharedWebGLContext.bindTexture(gl.TEXTURE_2D, vctx._drawingbufferTexture);
sharedWebGLContext.drawArrays(gl.TRIANGLES, 0, 6);
}
}
you'd also need to trap events that cause rendering which would be unique to each app. If the app uses requetsAnimationFrame to render then maybe something like
window.requestAnimationFrame = (function(origFn) {
return function(callback) {
return origFn.call(window, (time) {
const result = callback(time);
renderAllDirtyVirtualCanvases();
return result;
};
};
}(window.requestAnimationFrame));
If the app renders on other events, like say mousemove then maybe
something like this
let someContextNeedsRendering;
function createDrawWrapper(origFn) {
const newFn = createWrapper(origFn);
return function(...args) {
// a rendering function was called so we need to copy are drawingBuffer
// to the canvas for this context after the current event.
this._needComposite = true;
if (!someContextsNeedRendering) {
someContextsNeedRendering = true;
setTimeout(dealWithDirtyContexts, 0);
}
return newFn.call(this, ...args);
};
}
function dealWithDirtyContexts() {
someContextsNeedRendering = false;
renderAllDirtyVirtualCanvas();
});
Makes me wonder if someone else has already done this.

HTML5 Canvas, Having Trouble Running Multiple Animations At Once

I have written code that takes two arrays, both of which contain co-ordinates for a four-cornered shape (effectively a start frame and an end frame), a canvas ID and a time value. The function then calculates dX and dY of each corner and uses window.performance.now() to create a timestamp. Then, on every requestAnimationFrame(), it calculates what the co-ordinates should be by using dX, dY, the old timestamp, a new timestamp and the time value from the function call. It looks like this:
function doAnim(cv, startFrame, endFrame, animTime)
{
this.canvas = document.getElementById(cv);
this.ctx = this.canvas.getContext('2d');
if(startFrame.length != endFrame.length)
{
return('Error: Keyframe arrays do not match in length');
};
this.animChange = new Array();
for(i=1;i<=startFrame.length;i++)
{
var a = startFrame[i];
var b = endFrame[i]
var c = b - a;
this.animChange[i] = c;
}
this.timerStart = window.performance.now();
function draw()
{
this.requestAnimationFrame(draw, cv);
this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height);
this.currentFrame = new Array();
for(i=1;i<=startFrame.length;i++)
{
this.currentFrame[i] = startFrame[i]+(this.animChange[i]*((window.performance.now()-this.timerStart)/animTime));
}
if((window.performance.now()-this.timerStart)>=animTime)
{
this.ctx.beginPath()
this.ctx.moveTo(endFrame[1], endFrame[2]);
this.ctx.lineTo(endFrame[3], endFrame[4]);
this.ctx.lineTo(endFrame[5], endFrame[6]);
this.ctx.lineTo(endFrame[7], endFrame[8]);
this.ctx.fill();
return;
}
else
{
this.ctx.beginPath()
this.ctx.moveTo(this.currentFrame[1], this.currentFrame[2]);
this.ctx.lineTo(this.currentFrame[3], this.currentFrame[4]);
this.ctx.lineTo(this.currentFrame[5], this.currentFrame[6]);
this.ctx.lineTo(this.currentFrame[7], this.currentFrame[8]);
this.ctx.fill();
}
}
draw();
}
The goal is to have multiple animations of objects happening at once. I took the whole co-ordinate approach because I want the objects to appear as if they are coming from the horizon, creating a fake 3D perspective effect (all objects' starting frames would be a single point at the center of the canvas), and I do not want to warp the objects' textures.
Well, it works great for a single animation, but if I try to start a new animation on a completely different canvas while the first one is running, then the first animation stops dead in its tracks.
As you can see from my JS, I've tried getting around this with gratuitous use of this (I do not fully understand how this works yet, and every explanation I've read has left me even more confused), but it has not worked. I also tried a horrific approach which stored all the functions' own variables in one global array (the first time the function runs, all the variables are put in entries 1-30, the second time they're put in 31-60, etc). Unsurprisingly, that did not work either.
Here is a JSFiddle so you can see this scenario for yourself and play with my code. I am officially out of ideas. Any help will be greatly appreciated.
Like markE linked too, trying to call requestAnimationFrame multiple times won't work.
Instead you make multiple objects and then call some sort of function on them each frame.
I have created an example using your code:
https://jsfiddle.net/samcarlin/2bxn1r79/7/
var anim0frame1 = new Array();
anim0frame1[1] = 0;
anim0frame1[2] = 0;
anim0frame1[3] = 50;
anim0frame1[4] = 0;
anim0frame1[5] = 50;
anim0frame1[6] = 150;
anim0frame1[7] = 0;
anim0frame1[8] = 150;
var anim0frame2 = new Array();
anim0frame2[1] = 200;
anim0frame2[2] = 200;
anim0frame2[3] = 300;
anim0frame2[4] = 250;
anim0frame2[5] = 300;
anim0frame2[6] = 300;
anim0frame2[7] = 200;
anim0frame2[8] = 250;
//Call global
animations = [];
requestAnimationFrame( GlobalStep );
function GlobalStep(delta){
//Functions called by request animation frame have the new time as an argument
//so delta should be approximately the same as window.performance.now()
//especially in realtime applications, which this is
//Check if we have any animation objects
if(animations.length > 0){
//Iterate through and call draw on all animations
for(var i=0; i<animations.length; i++){
if(animations[i].draw(delta)){
//Basically we have it so if the draw function returns true we stop animating the object
//And remove it from the array, so have the draw function return true when animation is complete
animations[i].splice(i, 0);
//We removed an object from the array, so we decrement i
i--;
}
}
}
//And of course call requestAnimationFrame
requestAnimationFrame( GlobalStep );
}
function AnimationObject(cv, startFrame, endFrame, animTime){
//Add this object to the objects arrays
animations.push(this);
//We need to store start and end frame
this.startFrame = startFrame;
this.endFrame = endFrame;
this.animTime = animTime;
//Your code
this.canvas = document.getElementById(cv);
this.ctx = this.canvas.getContext('2d');
if (startFrame.length != endFrame.length) {
return ('Error: Keyframe arrays do not match in length');
};
this.animChange = new Array();
for (i = 1; i <= startFrame.length; i++) {
var a = startFrame[i];
var b = endFrame[i]
var c = b - a;
this.animChange[i] = c;
}
this.timerStart = window.performance.now();
}
//This adds a function to an object, but in such a way that every object shares the same function
//Imagine a kitchen, each object is a person, and this function is a spoon
//by defining this function in this manner Object.prototype.function_name = function(arguments){}
//We make it so one function definition is needed, essentially allowing all the people to share one spoon,
//the 'this' variable still refers to whichever object we call this method, and we save memory etc.
AnimationObject.prototype.draw = function(newTime){
//I added this to start frame so we get what we stored earlier
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.currentFrame = new Array();
for (i = 1; i <= this.startFrame.length; i++) {
this.currentFrame[i] = this.startFrame[i] + (this.animChange[i] * ((newTime - this.timerStart) / this.animTime));
}
if ((newTime - this.timerStart) >= this.animTime) {
this.ctx.beginPath()
this.ctx.moveTo(this.endFrame[1], this.endFrame[2]);
this.ctx.lineTo(this.endFrame[3], this.endFrame[4]);
this.ctx.lineTo(this.endFrame[5], this.endFrame[6]);
this.ctx.lineTo(this.endFrame[7], this.endFrame[8]);
this.ctx.fill();
return;
} else {
this.ctx.beginPath()
this.ctx.moveTo(this.currentFrame[1], this.currentFrame[2]);
this.ctx.lineTo(this.currentFrame[3], this.currentFrame[4]);
this.ctx.lineTo(this.currentFrame[5], this.currentFrame[6]);
this.ctx.lineTo(this.currentFrame[7], this.currentFrame[8]);
this.ctx.fill();
}
}
Notes:
Everytime you press the button a new object is added and simply overwrites previous ones for each frame, you should implement your program so that it checks if a specific animation has already started, you could also use the builtin mechanism to stop animation when complete (read the comments in the code)
You also need to change the on button click code
<button onclick="new AnimationObject('canvas1', anim0frame1, anim0frame2, 3000);">
Lastly if you have further questions feel free to contact me

this getting lost again

I thought I fixed it but looks like not. Heres whats happening:
The canvas.mousemove event is handled by viewport.onMouseMove.bind(viewport) function (viewport is an instance of a class).
At the end of the onMouseMove function it calls this.Draw() (referring to viewport.Draw() function).
viewport.Draw() loops through all the items and calls Items[i].Draw(ctx) on each of them where ctx is a back buffer canvas context.
Now if If the item that is being drawn goes ahead and uses the ctx to draw something right there and then (in its Draw function), using this to refer to itself, everything works fine. For example
this.Draw = function(ctx) {
ctx.beginPath();
ctx.moveTo(this.x1, this.y1);
ctx.lineTo(this.x2, this.y2);
ctx.lineWidth = 1;
ctx.strokeStyle = "#000000";
ctx.stroke();
};
However, if the object is a container that has items in itself and tries to loop and draw them like this
this.Draw = function(ctx) {
for (j = 0; j < this.Items.length; j++) {
this.Items[j].Draw(ctx);
}
};
When it gets into the Items[j].Draw, "this" loses all meaning. alert(this) produces "object object" and I cant figure out what its referring to (it's not the viewport nor the container nor the item it needs to be). Also another weird thing - I had to change the container object loop to use j instead of i because otherwise it would create a perpetual loop (like the i's of the viewport.draw and item[i].draw were the same).
Your question is somewhat unclear. Is this.Items an array of objects with the same prototype as this? ie. nested? Also, is the j counter intended to be shared?
Regardless, function contexts' this values can be changed rather easily to whatever you need them to be with the .apply and .call functions:
this.Draw = function(ctx) {
for (var j = 0; j < this.Items.length; j++) {
// These two are the same as what you have in the question
this.Draw.call(this.Items[j], ctx);
this.Draw.apply(this.Items[j], [ctx]);
// This is what you had in the question if Draw is different for Items:
this.Items[j].Draw(ctx);
this.Items[j].Draw.call(this.Items[j], ctx);
// Will preserve the this reference within the nested call
this.Items[j].Draw.call(this, ctx);
}
};
Not sure what the problem is but as my comment suggest this is the invoking object:
//this in someFunction is window
setTimeout(myObject.someFunction, 200);
//this in someFunction is button
button.onClick=myObject.someFunction;
Not sure what you would like this to be when it's called but if it has to be Items[j] then your code is fine and maybe something else is causing you problems. I suggest console.log objects in Chrome or Firefox with firebug, use F12 to open the console and inspect the logged objects.
Here is sample code of items that can be Square or Circle;
var Shape = function Shape(args){
//args.x1 or y1 can be 0, defaults to 2
this.x1 = (args.x1 === undefined)? 2:args.x1;
this.y1 = (args.y1 === undefined)? 2:args.y1;
this.name = args.name||"unnamed";
}
//in this example Square and Cirle draw does the same
// so they can inherit it from Shape
Shape.prototype.draw=function(){
console.log("this x1:",this.x1,"this y1:",this.y1,"name",this.name);
//you can log complex values as well and click on them in the console
// to inspect the details of the complex values (objects)
// the above can be done in the following log
console.log("in draw, this is:",this);
}
var Square = function Square(args){
//re use parent constructor (parent is Shape)
Shape.call(this,args);
}
//set prototype part of inheritance and repair constructor
Square.prototype=Object.create(Shape.prototype);
Square.prototype.constructor=Square;
var Circle = function Circle(args){
//re use parent constructor (parent is Shape)
Shape.call(this,args);
}
//set prototype part of inheritance
Circle.prototype=Object.create(Shape.prototype);
Circle.prototype.constructor=Circle;
//there is only one app so will define it as object literal
var app = {
items:[],
init:function(){
var i = -1;
while(++i<10){
this.items.push(new Circle({x1:i,y1:i,name:"circle"+i}));
}
while(++i<20){
this.items.push(new Square({x1:i,y1:i,name:"square"+i}));
}
},
draw:function(){
var i = -1;len=this.items.length;
while(++i<len){
this.items[i].draw();
}
}
}
app.init();
app.draw();//causes console.logs

drawImage() not drawing pre-loaded image object. Using OO javascript

I'm writing a simple 2D side scroller game using JavaScript + Canvas. I am trying to do this while learning OO JavaScript. I'm encountering an issue where my image will not draw to the canvas, despite being loaded correctly. Here's the code:
//===================================
// class - Canvas
//===================================
var classCanvas = function(id){
canvas = document.getElementById(id);
context = canvas.getContext("2d");
}
//===================================
// abstract class - Image Drawer
//===================================
var classImageDrawer = function(that){
this.draw = function(){
context.drawImage(that.img,
that.spriteCoordsX,
that.spriteCoordsY,
that.width,
that.height,
that.posX,
that.posY,
that.width,
that.height);
}
}
//===================================
// class - Level
//===================================
var classLevel = function(name, src){
this.name = name;
this.img = new Image();
this.img.src = src;
this.img.onload = function(){ };
this.spriteCoordsX = 0;
this.spriteCoordsY = 0;
this.posY = 0;
this.posX = 0;
this.height = 400;
this.width = 600;
this.drawer = new classImageDrawer(this);
}
//==================================
// Begin
//==================================
var game = new classCanvas("playground");
game.LevelOne = new classLevel("LevelOne", "images/foreground-land.png");
game.LevelOne.drawer.draw();
I have checked the code, and I know the image is getting loaded correctly. No errors occur during runtime, the image "foreground-land.png" simply DOESN'T show up on the canvas!
Your image may be loading fine, but as far as I can tell your call to game.LevelOne.drawer.draw() has no guarantee that foreground-land.png has loaded at that point (remember, images are loaded asynchronously without blocking the execution of other code).
Your onload function for the image object in your classLevel class is empty and anonymous. Nothing happens when the image is finished loading. I'm not sure how you want to structure your code given everything else you have going on, but you should only allow the call to game.LevelOne.drawer.draw() to succeed if the resources it depends upon have been fully loaded. The best way to do this would be to hook into the onload callback for all resources you require for that object (in this case, it's just the one image).
Loading images is an async operation, you are (basically) ignoring the loading process, and acting as though it is sync operations.
One thing you could look at is using a 3rd parameter as a callback ("LevelOne", "images/foreground-land.png", function() { this.drawer.draw() }) and doing that as the onload

HTML Canvas Unit testing

How can I unit-test Javascript that draws on an HTML canvas? Drawing on the canvas should be checked.
I wrote an example for unit-testing canvas and other image-y types with Jasmine and js-imagediff.
Jasmine Canvas Unit Testing
I find this to be better than making sure specific methods on a mock Canvas have been invoked because different series of methods may produce the same method. Typically, I will create a canvas with the expected value or use a known-stable version of the code to test a development version against.
As discussed in the question comments it's important to check that certain functions have been invoked with suitable parameters. pcjuzer proposed the usage of proxy pattern. The following example (RightJS code) shows one way to do this:
var Context = new Class({
initialize: function($canvasElem) {
this._ctx = $canvasElem._.getContext('2d');
this._calls = []; // names/args of recorded calls
this._initMethods();
},
_initMethods: function() {
// define methods to test here
// no way to introspect so we have to do some extra work :(
var methods = {
fill: function() {
this._ctx.fill();
},
lineTo: function(x, y) {
this._ctx.lineTo(x, y);
},
moveTo: function(x, y) {
this._ctx.moveTo(x, y);
},
stroke: function() {
this._ctx.stroke();
}
// and so on
};
// attach methods to the class itself
var scope = this;
var addMethod = function(name, method) {
scope[methodName] = function() {
scope.record(name, arguments);
method.apply(scope, arguments);
};
}
for(var methodName in methods) {
var method = methods[methodName];
addMethod(methodName, method);
}
},
assign: function(k, v) {
this._ctx[k] = v;
},
record: function(methodName, args) {
this._calls.push({name: methodName, args: args});
},
getCalls: function() {
return this._calls;
}
// TODO: expand API as needed
});
// Usage
var ctx = new Context($('myCanvas'));
ctx.moveTo(34, 54);
ctx.lineTo(63, 12);
ctx.assign('strokeStyle', "#FF00FF");
ctx.stroke();
var calls = ctx.getCalls();
console.log(calls);
You can find a functional demo here.
I have used a similar pattern to implement some features missing from the API. You might need to hack it a bit to fit your purposes. Good luck!
I make really simple canvases and test them with mocha. I do it similarly to Juho Vepsäläinen but mine looks a little simpler. I wrote it in ec2015.
CanvasMock class:
import ContextMock from './ContextMock.js'
export default class {
constructor (width, height)
{
this.mock = [];
this.width = width;
this.height = height;
this.context = new ContextMock(this.mock);
}
getContext (string)
{
this.mock.push('[getContext ' + string + ']')
return this.context
}
}
ContextMock class:
export default class {
constructor(mock)
{
this.mock = mock
}
beginPath()
{
this.mock.push('[beginPath]')
}
moveTo(x, y)
{
this.mock.push('[moveTo ' + x + ', ' + y + ']')
}
lineTo(x, y)
{
this.mock.push('[lineTo ' + x + ', ' + y + ']')
}
stroke()
{
this.mock.push('[stroke]')
}
}
some mocha tests that evaluates the functionality of the mock itself:
describe('CanvasMock and ContextMock', ()=> {
it('should be able to return width and height', ()=> {
let canvas = new CanvasMock(500,600)
assert.equal(canvas.width, 500)
assert.equal(canvas.height, 600)
})
it('should be able to update mock for getContext', ()=> {
let canvas = new CanvasMock(500,600)
let ctx = canvas.getContext('2d')
assert.equal(canvas.mock, '[getContext 2d]')
})
})
A mocha tests that evaluates the functionality of a function that returns a canvas:
import Myfunction from 'MyFunction.js'
describe('MyFuntion', ()=> {
it('should be able to return correct canvas', ()=> {
let testCanvas = new CanvasMock(500,600)
let ctx = testCanvas.getContext('2d')
ctx.beginPath()
ctx.moveTo(0,0)
ctx.lineTo(8,8)
ctx.stroke()
assert.deepEqual(MyFunction(new CanvasMock(500,600), 8, 8), canvas.mock, [ '[getContext 2d]', '[beginPath]', '[moveTo 0, 0]', [lineTo 8, 8]', '[stroke]' ])
})
so in this example myfunction takes the canvas you passed in as an argument ( Myfunction(new CanvasMock(500,600), 8, 8) ) and writes a line on it from 0,0 to whatever you pass in as the arguments ( Myfunction(new CanvasMock(500,600),** 8, 8**) ) and then returns the edited canvas.
so when you use the function in real life you can pass in an actual canvas, not a canvas mock and then it will run those same methods but do actual canvas things.
read about mocks here
Since the "shapes" and "lines" drawn on a canvas are not actual objects (it's like ink on paper), it would be very hard (impossible?) to do a normal unit test on that.
The best you can do with standard canvas it analyze the pixel data (from the putImageData/getImageData. Like what bedraw was saying).
Now, I haven't tried this yet, but it might be more what you need. Cake is a library for the canvas. It's using alot of the putImageData/getImageData. This example might help with what you are trying to do with a test.
Hope that helps answer your question.
I've been looking at canvas testing recently and I've now thought about a page that allows comparing the canvas to a "known good" image version of what the canvas should look like. This would make a visual comparison quick and easy.
And maybe have a button that, assuming the output is OK, updates the image version on the server (by sending the toDataUrl() output to it). This new version can then be used for future comparisons.
Not exactly (at all) automated - but it does make comparing the output of your code easy.
Edit:
Now I've made this:
The left chart is the real canvas whilst the right is an image stored in a database of what it should look like (taken from when I know the code is working). There'll be lots of these to test all (eventually) aspects of my code.
From a developer's point of view the canvas is almost write-only because once drawn it's difficult to programmatically get something useful back. Sure one can do a point by point recognition but that's too tedious and such tests are hard to be written and maintained.
It's better to intercept the calls made to a canvas object and investigate those. Here are a few options:
Create a wrapper object that records all the calls. Juho Vepsäläinen posted a such example.
If possible use a library like frabric.js that offers a higher level of abstraction for drawing. The "drawings" are JS objects that can be inspected directly or converted to SVG which is easier to inspect and test.
Use Canteen to intercept all the function calls and attribute changes of a canvas object. This is similar with option 1.
Use Canteen with rabbit which offers you a few Jasmine custom matchers for size and alignment and a function getBBox() that can be used to determine the size and the position of the stuff being drawn on the canvas.

Categories

Resources