2020-06-06

Javascript canvas pixel manipulation and performance

The basics

This article is meant as a reminder to myself on how canvas work is done in javascript as I don't do it that often.

Set canvas size

I just wasted some time figuring out that I was sizing my canvas the wrong way. If you resize the canvas using css, it will actually just scale the canvas, just like if you were to scale an image using css.

WRONG:

<canvas style="position: relative;" id= "canvas" style="width: 600px; height: 300px"></canvas>

This will work when you do basic javascript updates, like drawing a line using the context object. But when using pixel manipulation which I will do in the following, it will actually stretch the result in an unintended way.

CORRECT Way:

<canvas style="position: relative;" id= "canvas" width="600" height="300"></canvas>

The slow 2D context

When updating the canvas you can either use convenient drawing methods with a context object or you can do manual pixel manipulation. Both methods have pros and cons and whenever performance is not an issue, you may stick to context drawing for cleaner code.

However, when performance is an issue, using manual pixel manipulation will get you further.

Drawing a line using context:

See the Pen Step 1: Basic drawing by Stephan Ryer (@sryer) on CodePen.

The fast pixel manipulation

Instead of using the context, you can get an array of pixel information. The flow is as follows:

  1. Get an array of pixel data, either the existing pixels on the canvas or a blank sheet
  2. Manipulate the array using you own custom logic
  3. Update the canvas with the updated pixel data

Getting the pixel data from the canvas is done this way:

var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var imgData = ctx.getImageData(0, 0, width, height);
var data = imgData.data; // The actual array

Say we have a 10 x 7 pixel canvas. Then the data array will be of size 10 x 7 x 4 = 280. The 'x 4' is because every pixel is represented as rgba data, having an int representing each value.

Pixel data format

The array will contain data in this format:

data[0]; // Pixel 0,0,RED
data[1]; // Pixel 0,0,GREEN
data[2]; // Pixel 0,0,BLUE
data[3]; // Pixel 0,0,ALPHA

data[4]; // Pixel 1,0,RED
data[5]; // Pixel 1,0,GREEN
data[6]; // Pixel 1,0,BLUE
data[7]; // Pixel 1,0,ALPHA

data[8]; // Pixel 2,0,RED
data[9]; // Pixel 2,0,GREEN
data[10];// Pixel 2,0,BLUE
data[11];// Pixel 2,0,ALPHA

...

data[44];// Pixel 0,1,RED
data[45];// Pixel 0,1,GREEN
data[46];// Pixel 0,1,BLUE
data[47];// Pixel 0,1,ALPHA

Converting from x,y to a specific position in the array is done using the following operation:


var width = 10;
var height = 7;

var x = 5;
var y = 2;

var index = (y * width + x) * 4;

data[index + 0] // Red value of the x,y pixel (+0 is done for pure readability)
data[index + 1] // Green value of the x,y pixel
data[index + 2] // Blue value of the x,y pixel
data[index + 3] // Alpha (transparency) value of the x,y pixel. 0 = invisible, 255 is solid color.

The following code will reproduce the line-drawing from previous, but this time using pixel manipulation:

See the Pen Step 2: Drawing using pixel manipulation by Stephan Ryer (@sryer) on CodePen.

Update on mousemove

Updating the canvas is done on mousemove event this way:

canvas.addEventListener("mousemove", function (e) {
    var mousePosition = {x: e.layerX, y: e.layerY};
    updateCanvas(mousePosition);
});

var updateCanvas = function(mousePosition){
    // Some update logic
};

The event object 'e' contains layerX and layerY which is the position of the mouse.

NOTE: You MUST add style="position: relative;" for this to work in firefox and edge. If not, the layerX and layerY will be the mouse position relative to the window.

A working example of drawing a circle at the mouse position will look like this:

See the Pen Step 3: Reacting on input by Stephan Ryer (@sryer) on CodePen.

Creating a liquid effect

Creating a simple light

Instead of draw/dont draw depending on whether the distance exceeds 50 (indicating a radius of 50 for the circle), we can change the logic to fade out the longer the distance from a pixel to the mouse cursor:

See the Pen Step 4: Light effect by Stephan Ryer (@sryer) on CodePen.

Changing looks of light

We can now make the light a little bigger by changing the logic to this:

See the Pen Step 5: Changing properties on light by Stephan Ryer (@sryer) on CodePen.

Multiple light sources

We can change the logic to be based on a list of light source; some static light source as well as a light source at the cursor position. We will then determine the pixel light based on the distance to the nearest light source:

See the Pen MWjdyEg by Stephan Ryer (@sryer) on CodePen.

Changing to "sum"-logic

Now, instead of using the closest distance to determine a pixels light level, we will now give a pixel a score based on all distances to all light sources.

See the Pen Step 7: enhancing logic to "sum"-logic by Stephan Ryer (@sryer) on CodePen.

Binary drawing

A different, cool effect can be achieved by changing back logic to be draw/dont draw instead of fading light levels:

See the Pen Step 8: Switching to binary mode by Stephan Ryer (@sryer) on CodePen.

Using 3-level-light

See the Pen Step 9: 3-level-light model by Stephan Ryer (@sryer) on CodePen.

And that's it!