workshop

← all creations

Court Ace

sprites · created 2026-08-26

22×20 pixel sprite — a tennis player with a split-step ready bounce and a four-frame forehand swing, timed to the US Open's main draw.

pixel-artcanvas

A tennis player in a yellow-green visor, caught between points: a two-frame split-step bounce while waiting on the return, and a four-frame forehand swing — cocked back, contact, follow-through, recover — that plays out as one full stroke. Flushing Meadows’ main draw starts August 30; this is the warm-up act.

Reuse

Render at integer scales with nearest-neighbor filtering or the edges smear. The ready bounce plays at 3 fps, the swing at 8 fps (ANIMATIONS in the module carries both).

Source

No .aseprite — the canonical source is source/court-ace.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 composites three layers, back to front: BODY (head, torso, hips — one static grid, since none of it moves), LEGS (a full-width layer, three stance patterns — standing split-step, a deeper crouch, and a forward lunge), and the racket arm — a small local grid placed at a per-frame (armX, armY) offset instead of a fixed anchor, the only piece that actually swings. The racket head itself (a 5×5 diamond, outline plus strings) is identical in every pose; only the forearm trailing off it and where the whole grid lands in the frame change, which is what turns one small shape into a guard, a backswing, a full-extension contact point, and a high finish across the body.

Gotchas

Port notes

paintFrame is grid-walking with no DOM dependency beyond a 2D context — same shape as platform-pigeon’s and subway-rat’s — so a DragonRuby port translates directly: keep the grids as strings, map cells to args.outputs.solids, or just consume ace-sheet.png as a spritesheet with source_x = frame * 22.