When creating a 2d game on canvas I like to render the sprites on an offscreen canvas and access them via retrieving their ImageData.

Usually this will look something like the following, when working on a 2d grid based game.

function getSprite(spritesheetCtx, x, y, size) {
	return spritesheetCtx.getImageData(size * x, size * y, size, size)
}

Below are three useful methods that build on this technique.

Clone ImageData

When ctx.getImageData is called, it returns an ImageData object. This ImageData object can then be rendered and manipulated. When manipulating the ImageData object we will want to create a copy so that the original is not altered. To do this we use the following method.

function cloneImageData(imageData) {
	const data = new Uint8ClampedArray(imageData.data)
	return new ImageData(data, imageData.width, imageData.height)
}

The data held by an ImageData instance is a Uint8ClampedArray. To copy the data a new Uint8ClampedArray is instantiated from the original ImageData object and passed as a constructor argument to the new one.

Color Mask

I like to use Kenny's massive one bit tile pack when creating small 2d games. I use the non-colored white version. This allows me to add a color mask to the sprites, so that I can have them be any color I like.

Again I will render the spritesheet on an offscreen canvas and pre-fetch the sprites before game starts. I use the following method in order to apply a color mask to the sprites.

function applyColorMaskToImageData(imageData, maskColor) {
	const { data } = imageData
	const [maskR, maskG, maskB, maskA] = maskColor;

	for (let i = 0; i < data.length; i += 4) {
		const r = data[i];
		const g = data[i + 1];
		const b = data[i + 2];
		const a = data[i + 3];

		data[i] = (r * maskR) / 255;
		data[i + 1] = (g * maskG) / 255;
		data[i + 2] = (b * maskB) / 255;
		data[i + 3] = (a * maskA) / 255;
	}

	return imageData
}

This method takes and ImageData object, which should be a copy from cloneImageData, and applies the passed in rgba values as a mask. This can be used like so.

// initial sprite to copy
const skeletonSprite = getSprite(spritesheetCtx, 1, 1)
// sprite with red color mask applied
const redSkeletonSprite = applyColorMaskToImageData(cloneImageData(skeletonSprite), [255, 0, 0, 0])

Flip Horizontally

Tile packs usually only contain sprites facing in one direction. This means that if a sprite is facing left and you want it to face right, you will need to flip the sprite on a horizontal access. Again this is achieved by modifying the ImageData.

function flipImageDataHorizontally(imageData) {
	const { width, height, data } = imageData;
	const flippedData = new Uint8ClampedArray(data.length);

	for (let y = 0; y < height; y++) {
		for (let x = 0; x < width; x++) {
			const srcIndex = (y * width + x) * 4;
			const destIndex = (y * width + (width - x - 1)) * 4;

			flippedData[destIndex] = data[srcIndex];
			flippedData[destIndex + 1] = data[srcIndex + 1];
			flippedData[destIndex + 2] = data[srcIndex + 2];
			flippedData[destIndex + 3] = data[srcIndex + 3];
		}
	}

	return new ImageData(flippedData, width, height);
}

With the sprite now flipped on it's horizontal acces it can be used like so.

// initial sprite to copy
const skeletonSprite = getSprite(spritesheetCtx, 1, 1)
// left facing instance of ImageData
const leftFacingSkeletonSprite = flipImageDataHorizontally(cloneImageData(skeletonSprite))