Pixel Art with JavaScript

Jason Sturges
4 min readDec 1, 2021
Photo by Viswanath V Pai on Unsplash

Canvas 2D API enables manipulating individual pixels via ImageData — a pixel ArrayBuffer containing all the raw pixel values in the canvas.

This Uint8ClampedArray starts from the top-left and runs row by row from left to right, top down through the entire canvas representing every single pixel in totaling 32-bit RGBA as follows:

  • 8-bit Red
  • 8-bit Green
  • 8-bit Blue
  • 8-bit Alpha

All values are unsigned from 0 through 255, and not premultiplied as you would find in Uint32Array bitmap data.

While similar to ImageBitmap, this Canvas 2D API enables manipulation of individual pixels. It’s not as fast, as image data needs read and transferred to the GPU, whereas image bitmap data is painted directly by the GPU. We’ll explore transferring data between these types as well as canvas and img elements in a future story.

First, we’ll need a canvas element to obtain ImageData from:

<canvas id="canvas"></canvas>

Get the canvas element, and 2D context:

const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d");

Next, size the width and height of the image data to be created, as well as the canvas itself. Note that by default the canvas is 300x150 and will scale instead of growing in resolution unless you explicitly set its dimensions.

Here, I’m going to create 500x500 square image data:

const imageData = ctx.createImageData(500, 500);
canvas.width = 500;
canvas.height = 500;

To set each pixel, we need:

  • Image data we just created
  • Coordinates of the pixel to update
  • Red, green, blue, and alpha transparency values to apply to the pixel

As this buffer is just a long byte array, we need to find our position in the array by using x for the column, and multiplying y by width to account for the row. Then, over the next four bytes, apply RGBA values for that pixel:

function setPixel(imageData, x, y, r, g, b, a) {
const index = x + y * imageData.width;
imageData.data[index * 4] = r;
imageData.data[index * 4 + 1] = g;
imageData.data[index * 4 + 2] = b;
imageData.data[index * 4 + 3] = a;
}

So, to set the top left pixel to red, we would execute:

setPixel(imageData, 0, 0, 255, 0, 0, 255)

After updating our image data, we need apply it back to the context as:

ctx.putImageData(imageData, 0, 0);

Example: Sierpinski Triangle

Let’s try applying this with an example of generative art, chaos theory fractals — the Sierpinski Triangle.

To compute this, we need three points of a triangle:

  • First point, lower left corner (x: 0, y: 500)
  • Second point, top center (x: 250, y: 0)
  • Third point, lower right corner (x: 500, y: 500)

In code, we’ll store this as:

const points = [
{ x: 0, y: 500 },
{ x: 250, y: 0 },
{ x: 500, y: 500 }
];

This algorithm randomly choses a corner from our points array, and places a dot halfway from our current point to the corner.

We’ll store our current coordinate position as:

let x = 0;
let y = 0;

Then, for 10,000 iterations, we’ll run this loop:

for (let i = 0; i < 10000; i++) {
const n = Math.floor(Math.random() * 3);
x = Math.floor((x + points[n].x) / 2);
y = Math.floor((y + points[n].y) / 2);
setPixel(imageData, x, y, 0, 0, 0, 255);
}
ctx.putImageData(imageData, 0, 0);

Remember too long of rendering cycle will freeze the browser — this type of computation is best to throttle though animation frames, or I’ll show a simple interval function in our finished result:

Putting it all together, the source code is:

const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(500, 500);
canvas.width = 500;
canvas.height = 500;
function setPixel(imageData, x, y, r, g, b, a) {
const index = x + y * imageData.width;
imageData.data[index * 4] = r;
imageData.data[index * 4 + 1] = g;
imageData.data[index * 4 + 2] = b;
imageData.data[index * 4 + 3] = a;
}
const points = [
{ x: 0, y: 500 },
{ x: 250, y: 0 },
{ x: 500, y: 500 }
];
let x = 0,
y = 0;
for (let i = 0; i < 1000; i++) {
const n = Math.floor(Math.random() * 3);
x = Math.floor((x + points[n].x) / 2);
y = Math.floor((y + points[n].y) / 2);
setPixel(imageData, x, y, 0, 0, 0, 255);
}
ctx.putImageData(imageData, 0, 0);

CodeSandbox below:

If you like this story and want to see more graphics and visualization here on Medium, please become a subscriber:

--

--

Jason Sturges

Avant-garde experimental artist — creative professional leveraging technology for immersive experiences