A classic yellow rubber duck, floating in water that does the storytelling: a calm two-frame bob at idle, a lower three-frame waddle with wings held out under load, and — at overload — sunk to just a periscope bill and eye above the surface. Nods to AWS acquiring DuckLabs and pulling the DuckDB team in-house.
Reuse
export/duck-sheet.png— 168×20 strip, seven 24×20 frames in order:calm-0,calm-1,waddle-0,waddle-1,waddle-2,submerged-0,submerged-1.duck-{calm,waddle,submerged}-*.png— the individual frames.duck@8x.png— prescaled 8× hero shot for docs/previews.
Render at integer scales with nearest-neighbor filtering or the edges
smear. calm plays at 2 fps, waddle at 5 fps, submerged at 1.5 fps
(ANIMATIONS in the module carries all three).
Source
No .aseprite — the canonical source is source/warehouse-duck.mjs: a
framework-free ES module holding the pixel grid as a character map (one
char per pixel, palette at the top). Edit the 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
One static DUCK grid — head, bill, eye, body, wing, tail — is painted at
a per-frame (duckTop, duckLeft) offset, then a water layer is drawn on
top from waterlineRow down, clipping whatever part of the duck sank
below the surface. Raising waterlineRow frame to frame is the entire
“submerging” effect — no separate underwater art. Rows 8–10 (the wing
band) swap to a wider dark patch when a frame sets wing: 'spread',
reading as wings held out during waddle without a second full body grid.
A shifting modulo pattern on ripple gives the water a moving-foam texture
at a fixed waterline.
Gotchas
validateGrids()throws if the duck grid is ragged or a frame’s waterline sits outside the canvas — call it after editing, or just runrender.mjs, which calls it first.demo/warehouse-duck.jsis generated. Editsource/warehouse-duck.mjsand re-render.- The demo draws the sprite from the character map at runtime, fine at
this size. A game should blit
duck-sheet.pnginstead.
Port notes
paintFrame is grid-walking with no DOM dependency beyond a 2D context —
same shape as court-ace’s and hot-dog-cart’s — so a DragonRuby port
translates directly: keep the grid as strings, map cells to
args.outputs.solids, or just consume duck-sheet.png as a spritesheet
with source_x = frame * 24.