A front-facing bipedal walker: visor cockpit with an antenna, slab pauldrons over tapered forearms, a pulsing reactor core in the chest, and heavy piston legs. Six frames — a 2-frame idle bob and a 4-frame walk cycle — in two team palettes (steel and crimson).
Reuse
Pull PNGs straight from export/, or import the module and paint frames directly.
mech-sheet.png— 192×32 strip, six 32×32 frames in order:idle-0,idle-1,walk-0,walk-1,walk-2,walk-3.mech-sheet-crimson.pngis the same strip in the alternate palette.mech-idle-{0,1}.png,mech-walk-{0..3}.png— the individual frames.mech@8x.png/mech-crimson@8x.png— prescaled hero shots for docs.
Render at integer scales with nearest-neighbor filtering or the edges smear.
Walk plays at 8 fps, idle at 2 fps (ANIMATIONS in the module carries both).
The mech is symmetric enough to flip horizontally for facing — the antenna and
the teal muzzle on one forearm are the only asymmetries, and they read fine
mirrored. paintFrame(ctx, name, { flip: true }) does it.
Source
No .aseprite — the canonical source is source/mech-sprite.mjs: a framework-free
ES module holding the pixel grids as character maps (one char per pixel, palette
at the top). Edit a grid and regenerate everything with:
node source/render.mjs
(needs site/node_modules installed — the script resolves Playwright from there.)
That one command rewrites export/, media/, thumb.png, and the demo’s
bundled copy of the module, so nothing drifts.
How the frames are built
A frame is two layers composited on the 32×32 canvas: a leg pose drawn first
(anchored at a fixed LEG_Y — the legs are the ground contact), then the torso
drawn over it at the frame’s bob offset. The torso’s waist is transparent, so
the hips show through and a bobbing torso overlaps the hips instead of tearing a
gap. That’s why the walk cycle needs only three leg grids (stand, stepL,
stepR) rather than six full-body ones, and why a new pose is cheap to add.
The reactor core (E cells) picks its color from the frame’s core: 'hot' | 'dim'
rather than living in a second copy of the grid — that’s the pulse in the idle
loop. Team palettes work the same way: one grid, a swapped color table.
Gotchas
validateGrids()throws if any row isn’t exactly 32 chars — call it after editing, or just runrender.mjs, which calls it first. A short row silently truncates that scanline otherwise.demo/mech-sprite.jsis generated. Editsource/mech-sprite.mjsand re-render.- The demo draws the sprite from the character maps at runtime (~500
fillRects per frame at 6×, fine at this size). A game should blitmech-sheet.pnginstead.
Port notes
paintFrame is ~20 lines of grid walking with no DOM dependency beyond a 2D
context, so DragonRuby ports translate directly: keep the grids as strings, map
cells to args.outputs.solids, or just consume mech-sheet.png as a spritesheet
with source_x = frame * 32.