Manipulating Canvas Pixel Data with JavaScript
The HTML5 canvas API exposes methods for drawing simple shapes on a canvas, and there are several libraries out there that provide high-level abstractions to make drawing complicated shapes easier (e.g. Fabric.js). Sometimes, though, you may need to manipulate your image's pixel data directly. This requires a low-level canvas API and isn't very intuitive - so in this post, I'll explain how it works.
Accessing pixel data
To access the pixel data of a canvas, you need to get the canvas context and call getImageData on it like this:
const canvas = document.getElementById('canvas'); const canvasContext = canvas.getContext('2d'); // this gets image data for the whole canvas, but you can fetch it for regions of the canvas as well. const canvasData = canvasContext.getImageData(0, 0, imageWidth, imageHeight); // canvasData.data contains the canvas' pixel data.
The object it returns has a property called data, which is a one-dimensional array of numbers that represents all the canvas’ pixel data.
Yes, it’s a one-dimensional array of numbers and nothing else. This is where things get a little weird.
Working with the image data array
In this array, each pixel in the canvas is represented by four array elements – one for red, one for blue, one for green, and one for alpha.
Since the array is one-dimensional, you need to know the dimensions of the canvas to infer where columns begin and end. In a 4-by-4 canvas, for example, the first four elements of the array (indexes 0-3) represent the first pixel in the first column of pixels on the canvas, the next four (4-7) represent the second pixel in the first column, and so on. Then, the elements 16-19 represent the first pixel in the second column.
To change individual pixels on the canvas, just modify the canvasData.data array directly. Then, when you're ready to update the canvas with your changes, call canvasContext.putImageData with the canvasData object like this:
canvasContext.putImageData(canvasData, 0, 0);
Writing a wrapper
If you need to manipulate pixel data directly in your project, it’s a good idea to write a simple wrapper for the API that handles its confusing aspects transparently. I decided to do this for one my own projects recently, and ended up with this:
export default (params) => { const canvas = params.canvas, imageWidth = params.imageWidth, imageHeight = params.imageHeight; const canvasContext = canvas.getContext('2d'); const canvasData = canvasContext.getImageData(0, 0, imageWidth, imageHeight); const computeImageDataOffset = (x, y) => { return (y + x * imageHeight) * 4; }; const setPixel = (x, y, r, g, b, a) => { const index = computeImageDataOffset(x, y); canvasData.data[index] = r; canvasData.data[index + 1] = g; canvasData.data[index + 2] = b; canvasData.data[index + 3] = a; }; const updateCanvas = () => { canvasContext.putImageData(canvasData, 0, 0); }; const getPixel = (x, y) => { const index = computeImageDataOffset(x, y); return { r: canvasData.data[index], g: canvasData.data[index + 1], b: canvasData.data[index + 2], a: canvasData.data[index + 3] }; }; return { setPixel, getPixel, updateCanvas }; };
The important part here is the computeImageDataOffset function. Note how the imageData array index, for both reads and writes, is calculated, and how the red, green, blue, and alpha values for the pixel are offset from the index.