A hot shop at night, drawn as one sprite: brick furnace on the left with the glory hole cut into it, bench rails across the middle, the gaffer seated at the right end of the pipe, jacks and shears hanging off the rafter, a bucket on the floor that is about to be missed.
Three loops:
- gather — the pipe is in the mouth and the gaffer is rolling it on the rails, hands alternating, the gob growing between frames. The furnace flickers, so the whole shop breathes on a four-frame cycle.
- blow — pipe out on the rails, a duck to the mouthpiece, a puff, and the bubble opens up over five frames. Watch the room, not the glass: the light it throws shrinks with every frame, because the glass is cooling and the glass is the only lamp in here.
- sag — the joke, and it is a true one. The hands come off the rails and nothing else changes. The glass droops, necks, lets go, and lands on the floor about a foot from the bucket. Nothing in the shop failed. The gaffer stopped turning for two seconds.
What’s different about this one
Every other sprite in this catalog resolves a character to a fixed palette colour and that is the pixel. This one paints the whole shop in near-black — the palette has no warm entries at all — and then runs a light pass over the 48×36 buffer before it blits anything:
- The furnace mouth and the glass on the end of the pipe are the only two light sources in the room (plus whatever embers are in the air on a given frame).
- Falloff is quantised into six bands, and the band edges are broken up with a 4×4 ordered dither, so the gradient reads as pixel art instead of an airbrushed blob. The dither pattern is the art style here.
- The glass isn’t a palette entry either. It is a temperature —
heatper frame, warped by each pixel’s distance from the gob’s centroid — run through a nine-step ramp from dull red to white-hot.
That is why the cooling is legible rather than asserted. Nothing in the sag
frames says “this is going wrong”; the room just gets darker, band by band, as
the number in heat comes down.
The one composition rule everything else bends around: the glass must stay the
whitest thing on screen, even while it is sitting inside a furnace. The mouth
interior is deliberately capped well below the gob’s temperature (0.62 against
0.97) and the gather frames park the ball half in and half out of the opening,
or the subject disappears into its own light source.
Reuse
export/gaffer-sheet.png— 768×36 strip, sixteen 48×36 frames in sheet order:gather-0..3,blow-0..5,sag-0..5.gaffer-{gather,blow,sag}-*.png— the individual frames.gaffer@6x.png— prescaled 6× hero shot for docs/previews.
Render at integer scales with nearest-neighbour filtering or the dither bands
smear into the gradient they were there to avoid. gather plays at 4 fps,
blow at 4.5, sag at 3.5 (ANIMATIONS carries all three). blow and sag
are one-shots by intent — the demo plays either once and falls back to gather.
Source
No .aseprite — the canonical source is source/gaffer.mjs: a framework-free
ES module holding the shop, the gaffer’s six poses and the gob shapes as
character maps, plus the light pass. 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 a frame is built
SHOP, straight off the character map — flue, rafter, hanging tools, furnace masonry, bench rails, seat, bucket, floor. All of it near-black.- A steel collar ring, then the mouth disc painted inside it. Drawing the collar first is what makes the opening read as a hole in a wall rather than a second sun hovering in front of one.
- The blowpipe — before the gaffer, so his hands close over it.
- The gaffer: one of six 14×20 poses (
roll-a,roll-b,lean,puff,rest,slump). - The gob at the end of the pipe, then anything that has left it — a blob in the air, a splat on the floor.
- Embers, which are lights as well as pixels.
- The light pass over everything not marked emissive.
- Blit, one
fillRectper pixel.
A frame declaration is therefore a short list — pose, pipe position, gob shape, heat, how hard the furnace is breathing — not a second copy of the artwork.
Gotchas
validateGrids()throws on ragged rows, unknown palette characters, and the one that actually caught a bug: a sag shape long enough to hang the glass through the floor. Every sag shape looks fine in isolation; only the anchor arithmetic tells you where it lands. Call it after editing, or just runrender.mjs, which calls it first.demo/gaffer.jsis generated. Editsource/gaffer.mjsand re-render.- Gob shapes are anchored by
ax/ay— the cell that sits on the pipe end — not by their top-left, so a bubble grows away from the pipe and a droop hangs below the same point without a second set of coordinates. - The hot spot inside a gob is its centroid, not its anchor. That is what makes a drooping shape’s brightest point travel down with the mass instead of staying stuck up at the pipe.
- The light pass is O(pixels × lights) per frame — 1728 × 3 or so. Fine at this
size and fine in the demo at 60fps; a game with a dozen of these on screen
should blit
gaffer-sheet.pnginstead of repainting.
Port notes
paintFrame writes into a plain RGB buffer and only touches a 2D context at
the blit, so the light pass ports anywhere you can address pixels — same shape
as The Claw’s and lab-mouse’s grid walking, one step further. In DragonRuby,
keep the grids as strings and either run the same pass into args.outputs.solids
or precompute it: the frames are static, so the honest port is just consuming
the spritesheet with source_x = frame * 48.