If you’re not familiar with PuzzleScript, you should; it’s the best way of quickly creating grid-based games, and a great example of how focused, tiny game engines can provide a lot of value for gamedevs. Each game’s source code is a single text file, including the graphics. How? Each sprite is 5x5 pixels, defined as ascii:
This is a genius idea, so we’re going to poach it for our non-PuzzleScript games. The rest of this post is just implementation details; feel free to stop reading and grab the code from github or write it yourself.
If you’re using the Canvas API, the simplest way is to create a new canvas for each sprite. Then simply call .drawImage(), which accepts a canvas as a source image.
// Usage:
let wall_sprite = canvasFromAscii(
["brown", "darkbrown"], // either color names or hex color values
`
00010
11111
01000
11111
00010
`
);
// full source at https://github.com/knexator/engineless_webgames/blob/main/sprite_from_code/canvas.ts
main_ctx.drawImage(wall_sprite, 0, 0, TILE_SIZE, TILE_SIZE)
// Implementation
function canvasFromAscii(colors: string[], ascii: string): HTMLCanvasElement {
let ascii_lines = ascii.trim().split("\n").map(x => x.trim());
let spr_h = ascii_lines.length;
let spr_w = ascii_lines[0].length;
if (ascii_lines.some(line => line.length !== spr_w)) {
throw new Error(`The given ascii is not a proper rectangle: ${ascii}`);
}
let sprite_canvas = document.createElement("canvas");
sprite_canvas.width = spr_w;
sprite_canvas.height = spr_h;
let sprite_ctx = sprite_canvas.getContext("2d")!;
for (let j=0; j<spr_h; j++) {
for (let i=0; i<spr_w; i++) {
let char = ascii_lines[j][i];
if (char === ".") continue;
let color_name_or_hex = colors[Number(char)].toLowerCase();
sprite_ctx.fillStyle = color_name_or_hex;
sprite_ctx.fillRect(i, j, 1, 1);
}
}
return sprite_canvas;
}If you’re using the WebGL API, you probably want the raw pixel values. These can be fed into texImage2D(), or whatever wrapper you’re using: TWGL.js’ createTexture(), three.js’ DataTexture, etc. Easy peasy:
// Usage:
let wall_sprite: WebGLTexture = textureFromAscii(gl,
["#A46422", "#493C2B"],
`
00010
11111
01000
11111
00010
`
);
function rgbFromHex(hex_string: string): [number, number, number] {
// "#030102" -> 0x030102 -> [3, 1, 2]
let hex_number = Number(hex_string.replace("#", "0x"));
return [hex_number >> 16, (hex_number >> 8) & 0xff, hex_number & 0xff];
}
// Implementation
function textureFromAscii(
gl: WebGLRenderingContext,
colors: string[],
ascii: string
): WebGLTexture {
let ascii_lines = ascii.trim().split("\n").map(x => x.trim());
let spr_h = ascii_lines.length;
let spr_w = ascii_lines[0].length;
if (ascii_lines.some(line => line.length !== spr_w)) {
throw new Error(`The given ascii is not a proper rectangle: ${ascii}`);
}
let pixel_data = new Uint8Array(spr_h * spr_w * 4);
for (let j=0; j<spr_h; j++) {
for (let i=0; i<spr_w; i++) {
let char = ascii_lines[j][i];
if (char === ".") continue; // pixel_data is 0 by default
let color_hex_string = colors[Number(char)].toLowerCase();
let rgb_values = rgbFromHex(color_hex_string);
let base_index = (i + j * spr_w) * 4;
pixel_data[base_index + 0] = rgb_values[0];
pixel_data[base_index + 1] = rgb_values[1];
pixel_data[base_index + 2] = rgb_values[2];
pixel_data[base_index + 3] = 255;
}
}
// To use the raw pixel values:
// return {width: spr_w, height: spr_h, data: pixel_data};
let texture = gl.createTexture()!;
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA, spr_w, spr_h, 0,
gl.RGBA, gl.UNSIGNED_BYTE, pixel_data
);
// Required for pixel art textures:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// Not really needed but a good practice:
gl.bindTexture(gl.TEXTURE_2D, null);
return texture;
}Again, the full code is available on github, but feel free to implement your own version. Maybe you already have a Color class, or your own helper for reading ascii rectangles of data, or want to support fancier transparency. The world is your oyster!


