html5-canvasimagedata

Animating with the `ImageData` data array causes very unexpected and glitch-like results


I recently stretched a gradient across the canvas using the ImageData data array; ie the ctx.getImageData() and ctx.putImageData() methods, and thought to myself, "this could be a really efficient way to animate a canvas full of moving objects". So I wrapped it into my main function along with the requestAnimationFrame(callback) statement, but that's when things got weird. The best I can do at describing is to say it's like the left most column of pixels on the canvas is duplicated in the right most column of pixels, and based on what coordinates you specify for the get and put ctx methods, this can have bizarre consequences.

I started out with the get and put methods targeting the canvas at 0, 0 like so:

imageData = ctx.getImageData( 0, 0, cvs.width, cvs.height );

//  here I set the pixel colors according to their location
//  on the canvas using nested for loops to target the
//  the correct imageData array indexes.

ctx.putImageData( imageData, 0, 0 );

But I immediately noticed the right side of the canvas was wrong. Like the gradient has started over, and the last pixel just didn't get touched for some reason:

enter image description here

So scaled back my draw region changed the put ImageData coordinates to get some space between the drawn image and the edge of the canvas, and I changed the get coordinated to eliminate that line on the right edge of the canvas:

imageData = ctx.getImageData( 1, 1, cvs.width, cvs.height );

for ( var x = 0; x < cvs.width - 92; x++ ) {
    for ( var y = 0; y < cvs.height - 92; y++ ) {
        // array[ x + y * width ] = value / x; // or similar
    }
}

ctx.putImageData( imageData, 2, 2 );

enter image description here

Pretty! But wrong... So I reproduced it in codepen. Can someone help me understand and overcome this behavior?

Note: The codepen has the scaled back draw area. If you change the get coordinates to 0 you'll see it basically behaves the same way as the first example but with white-space in between the expected square and the unexpected line. That said, I left the get at 1 and the put at zero for the most interesting behavior yet.


Solution

  • I've changed your code a little. In your double loop I am declaring a variable var i = (x + y*cvs.width)*4; This is only reducing the verbosity of your code so that I can see it better. The i variable represents the index of your pixel in the imageData.data array. Since you are doing

            imageData.data[i - 4 ] ...
            imageData.data[i - 3 ] ...
            imageData.data[i - 2 ] ...
            imageData.data[i - 1 ] ...
    

    you are going one pixel backwards and the first pixel from every row appears as the last pixel of the previous row. So I've changed it from var i = (x + y*cvs.width)*4; to var i = 4 + (x + y*cvs.width)*4;. When you are animating it, since the imageData is inside the test() function, you are recalculating the values of the imageData.data array in base of the last frame. So in the second frame you have that 1px line from the first frame copied again and moved 1px upward and 1px to the left.

    I hope this is what you were asking.

    var ctx, cvs, imageData;
    	cvs = document.getElementById('canv');
    	ctx = cvs.getContext('2d');
    
    function test() {
    	
    	// imageData = ctx.getImageData( 0, 0, cvs.width, cvs.height );
    	  // produces a line on the right side of the screen
    	
    	imageData = ctx.getImageData( 1, 1, cvs.width, cvs.height );
    	  // bizzar reverse cascading
    	
    	for (var x=0;x<cvs.width-92;x++) {
    		for (var y=0;y<cvs.height-92;y++) {
    			var i = 4+(x + y*cvs.width)*4;
    			imageData.data[i - 4 ] = Math.floor((255-y)-Math.floor(x/55)*55);
    			imageData.data[i - 3 ] = Math.floor(255/(cvs.height-92)*y);
    			imageData.data[i - 2 ] = Math.floor(255/(cvs.width-92)*x);
    			imageData.data[i - 1 ] = 255;
    		}
    	}
    	
    	ctx.putImageData( imageData, 0, 0 );
    	
    	requestAnimationFrame( test );
    	
    }
    
    test();
    canvas {
    	box-shadow: 0 0 2.5px 0 black;
    }
    <canvas id="canv" height="256" width="256"></canvas>