Skip to content
Steel Tide
All posts

Every sprite drawn in code, and the PNG pipeline that came later

·by Rene Wang·3 min read

How Steel Tide shipped with zero binary assets, and what it took to let generated sprite sheets override the procedural art without shimmer.

Steel Tide's first build had no image files at all. Every tank, building, shell and crater was drawn by TypeScript at boot into an atlas, every sound was synthesized from oscillators, and every map came out of a generator or an ASCII grid. The constraint was deliberate: a repository of pure source is easy to diff, easy to fork, and impossible to leave half-updated when a unit changes shape.

The procedural atlas

The palette is DB32, the 32-color set that a lot of pixel art shares, plus a handful of faction colors. Each sprite is a small program: a hull is a rounded rectangle with a rim light, treads are two striped columns, a turret is a circle and a barrel. Vehicles are drawn once facing up and then baked in 24 rotation steps, so the renderer never rotates at draw time and every angle has crisp pixels. Team colors are a palette swap at bake time, which is why there are four copies of each unit in the atlas and none of them cost a draw call.

Buildings get animation the same way: a rotating radar dish is a drawing routine called with a frame index. Effects are generated too, down to the smoke, and the whole set bakes in well under a second on a phone.

Why PNGs anyway

Code-drawn sprites are consistent and cheap. They are also, frankly, limited: a 22×26 pixel tank has only so many ways to look interesting, and image models became good enough at pixel art that ignoring them was the wrong trade. So the engine grew a second path. Drop a sheet into public/sprites/ with a line in manifest.json and it overrides the procedural art for that key at boot. No manifest, or a failed load, and the game silently keeps its own drawing.

The interesting work was making generated sheets behave, because a model will not repeat itself exactly. Frame two of a factory is a fresh painting of frame one with a few pixels of drift and a slightly different shade on every wall.

Killing the shimmer

The loader treats every sheet as untrusted input:

  • Backgrounds are flood-filled away from the edges, up to three dominant shades, stopping at the dark outlines of the art. JPEGs work.
  • Frame borders drawn between animation frames are detected and cropped.
  • Stabilization aligns each frame to the first using the static lower body of the silhouette, so the loop does not jitter.
  • Static freeze: the manifest declares where the animation lives (animRegion, say the top 30 % of the frame) and every pixel outside it is composited to the cross-frame median, making the body pixel-identical in every frame.
  • Scaling uses a deterministic area-average resampler, because the browser's smoothed drawImage leaks tiny cross-frame differences that read as shimmer.

Rotating parts follow the same rule as the procedural art: generate one up-facing image, mark it rotated, and the engine bakes the 24 directions and spins it live. A tank is two entries, a hull without its turret and the turret alone, each with a pivot point.

Team colors, again

The procedural sprites swap palette indices; a painted sheet has no indices. The convention is magenta: faction parts are painted in the #FF00FF family, and the loader recolors that family per player at load time, bright to bright, dark to dark. The wiki uses exactly the same rule to paint its thumbnails blue.

The result is a game that still runs with no assets at all, and looks a great deal better when it has them.