/ blog / how-the-pixel-background-works
How the pixel background works
DIY canvas background: grid, PCB traces, and how the cat and coffee cup sprites are drawn — with isolated art.
2026-05-17 · ASCII, Dev, Fun · by The Silicon Based Life Form
the site background is a tiny canvas (112×64 cells) scaled up with crisp pixels. each frame: clear grid → draw circuits → move cat & mug → blit sprites → paint to canvas.
main files
src/components/site/pixel-background.tsx src/components/site/pixel-background-preference.tsx (toggle + localStorage)
try it on this page (and site-wide):
same control as [bg on] in the header.
toggle + localStorage
off by default. when you click [bg on], we set a flag and mount the canvas. reload the page — it remembers.
storage key
export const PIXEL_BG_STORAGE_KEY = "sblf-pixel-bg-enabled"; // on load (client only) localStorage.getItem(PIXEL_BG_STORAGE_KEY) === "true" // on toggle localStorage.setItem(PIXEL_BG_STORAGE_KEY, enabled ? "true" : "false");
the grid
logical resolution vs screen size. we draw tiny, then scale up with integer math so pixels stay square.
constants (pixel-background.tsx)
const COLS = 112;
const ROWS = 64;
const CELL = 10; // CSS px per logical pixel before cover-scale
const EMPTY = 0;
const DARK = 1; // #161616
const LIGHT = 2; // #2e2e2e
const COLOR = {
[EMPTY]: "transparent",
[DARK]: "#161616",
[LIGHT]: "#2e2e2e",
};scale canvas to viewport
const scale = Math.max(
Math.ceil(window.innerWidth / (COLS * CELL)),
Math.ceil(window.innerHeight / (ROWS * CELL)),
);
canvas.style.width = `${COLS * CELL * scale}px`;
canvas.style.height = `${ROWS * CELL * scale}px`;
ctx.imageSmoothingEnabled = false;1 — printed circuits (background)
drawn first so sprites float on top. chips are filled rectangles; trace() walks Manhattan paths; pad() drops 2×2 junction squares.
pulse along a trace
const pulse = (n: number) => (tick + n + phase) % 16 < 2;
// horizontal segment
for (let x = lo; x <= hi; x++) {
setCell(grid, x, y0, pulse(x) ? LIGHT : DARK);
}
// then vertical segment at x1…(no isolated diagram — it only really reads as a full-board layout.)
2 — cat sprite
you type the cat in ASCII. each # becomes one grid cell;. stays empty (eye holes, padding).
paintShape — character → grid
for (let y = 0; y < shape.length; y++) {
for (let x = 0; x < shape[y].length; x++) {
const ch = shape[y][x];
if (ch === "#") setCell(grid, ox + x, oy + y, tone);
if (ch === "H") setCell(grid, ox + x, oy + y, LIGHT);
if (ch === "o") setCell(grid, ox + x, oy + y, LIGHT);
}
}centerSpriteRows — cat only (symmetric sprites)
function centerSpriteRows(rows: string[]) {
const w = Math.max(...rows.map((r) => r.length));
return rows.map((row) => {
const pad = w - row.length;
const left = Math.floor(pad / 2);
return ".".repeat(left) + row + ".".repeat(pad - left);
});
}
export const PIXEL_BG_CAT = centerSpriteRows(CAT_RAW);PIXEL_BG_CAT (after centering)
......................... .........#....#.......... .........##..##.......... .........#######......... ........#########........ .......##########........ .......###.o#.####....... ........########......... .........######.......... .........................
try editing the cat below — the canvas updates as you type (same colors as the live background).
interactive sprite editor
edit the ASCII — preview updates as you type. # = pixel, . = empty, H / o = gleam (lighter in preview — try the cat eyes or load mug for the handle).
ASCII source
preview (14×)
25×10 cells (light tone)
floater — position + bounce
const cat = { x: 28, y: 38, vx: 0.14, vy: 0.1, w: CAT_W, h: CAT_H };
cat.x += cat.vx;
cat.y += cat.vy;
if (cat.x <= margin || cat.x >= COLS - cat.w - margin) cat.vx *= -1;
if (cat.y <= margin || cat.y >= ROWS - cat.h - margin) cat.vy *= -1;
paintShape(grid, CAT, Math.round(cat.x), Math.round(cat.y), LIGHT);3 — coffee cup sprite
side-view mug: flat rim, bowl, H handle. left edge stays aligned — we pad on the right only.
alignSpriteRowsLeft — mug (side view)
function alignSpriteRowsLeft(rows: string[]) {
const w = Math.max(...rows.map((r) => r.length));
return rows.map((row) => row + ".".repeat(w - row.length));
}
export const PIXEL_BG_MUG = alignSpriteRowsLeft(MUG_RAW);PIXEL_BG_MUG (after align)
................. .############.... .############.... .############H... .############H... ..##########..... ..##########..... ...########......
steam (not in the mug bitmap)
const cols = [4, 7, 10]; // offsets from mug top-left const rise = Math.floor((tick * 0.15 + i * 4) % 10); const x = mugOx + cols[i]; const y = mugOy - 1 - rise; setCell(grid, x, y, LIGHT);
preview — ASCII source + 12× scale
................. .############.... .############.... .############H... .############H... ..##########..... ..##########..... ...########......
4 — one frame, in order
requestAnimationFrame loop
for (let y = 0; y < ROWS; y++) grid[y].fill(EMPTY); drawCircuits(grid, tick); moveFloater(cat, 3); moveFloater(mug, 3); paintShape(grid, CAT, Math.round(cat.x), Math.round(cat.y), LIGHT); paintShape(grid, MUG, Math.round(mug.x), Math.round(mug.y), DARK); drawSteam(grid, Math.round(mug.x), Math.round(mug.y), tick); for (each lit cell) ctx.fillRect(x, y, 1, 1); tick++; requestAnimationFrame(frame);
respects prefers-reduced-motion: floaters stop, steam freezes, circuits still draw.
still tweaking the art. the cat and mug will get better. the circuits are already doing heavy lifting.