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
export/ace-sheet.png— 132×20 strip, six 22×20 frames in order:ready-0,ready-1,swing-0,swing-1,swing-2,swing-3.ace-{ready,swing}-*.png— the individual frames.ace@8x.png— prescaled 8× hero shot for docs/previews.
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
validateGrids()throws if any row isn’t the expected width, a frame references a missing leg stance or arm pose, or an arm offset would draw outside the frame — call it after editing, or just runrender.mjs, which calls it first.demo/court-ace.jsis generated. Editsource/court-ace.mjsand re-render.- The demo draws the sprite from the character maps at runtime, fine at
this size. A game should blit
ace-sheet.pnginstead.
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.