Skip to content

Let painted frames override a composed pose - #23

Merged
JOhnsonKC201 merged 2 commits into
mainfrom
feat/baked-pose-frames
Jul 31, 2026
Merged

Let painted frames override a composed pose#23
JOhnsonKC201 merged 2 commits into
mainfrom
feat/baked-pose-frames

Conversation

@JOhnsonKC201

Copy link
Copy Markdown
Owner

What

Adds a way to replace a composed pose with painted art, and the importer that gets a PNG into that shape.

Every pose is composed in code, which is why one sit covers 28 coats and a new coat costs nine hex values instead of an art pass. The trade is that changing how the pet looks means editing geometry, and there was no way in for hand drawn or generated art at all. src/art-frames.js is the escape hatch.

How it fits together

  • src/art-frames.js (generated, empty by default) holds role-letter grids. Lookup runs coat name, then build name, then *, then the composer, so a pack of one frame is a valid pack and a half finished one still runs.
  • posed() in renderer.js stamps a baked grid through buildSprite rather than around it, so it picks up outlineHalo(), the eye boxes and the muzzle anchor exactly as a composed pose does. The halo is never authored, a patchy outline gets its gaps filled, and drawCat needs no special case. A grid that does not match the pose's canvas is ignored rather than trusted, because the layout maths around it is built on those constants.
  • scripts/import-frames.js turns painted PNGs into that table. It carries its own PNG decoder (colour types 0/2/3/4/6, every filter) rather than adding a dependency, the same call pet-sheet.js makes for its encoder. Each cell takes an area majority vote over the pixels covering it, so art that does not land exactly on the grid still imports.
npm run frames:import -- <dir> --dry   # report only
npm run frames:import -- <dir>         # writes src/art-frames.js

Named <species>-<pose>.png, or <species>-<pose>--<key>.png to target one coat or build.

Held poses vs animation rigs

Only five poses are importable. The other six are parameterised rigs and a single still would freeze them mid swing, so the importer refuses them on purpose:

Activity Rig Frames per coat
Rope climb climbSpriteFor(i, hand, dir) 4, the high paw alternates every step
Groom, ponder, play pawSpriteFor(i, lift, out) up to 45, PAW_STEPS 8 by 4
Bat overhead batSpriteFor(i, up, ph) 14, two sides by seven arc steps

Generated art for those is still worth having as a reference to redraw the composer against, which is how the dog was built in the first place.

Nothing is trusted on the way in

Every frame is checked before it is written, and a frame that fails is dropped with a reason:

  • the canvas matches the pose
  • an eye block sits either side of the seam eyeBox() splits on, or exactly one for the two profile poses. A face drawn off centre puts one eye in both boxes and drawCat paints the pupil as a bar across the muzzle. That has bitten this project twice.
  • a nose exists, because the whiskers and the panting tongue anchor to it
  • the bottom two rows are occupied, so the pose stands on the same floor line as the others. The crouch and the play bow are exempt: both are drawn clear of the bottom of their own canvas and placed by height.

Test plan

  • npm test 81 green, up from 69
  • Round trip: compose a real pose, paint it to a PNG, read it back through the importer, assert the grid is identical. Covers decode, resample and colour snap against real sprite shapes rather than a hand written fixture. Runs for cat sit, cat loaf and dog sit, plus a non integer scale.
  • Renderer hook driven end to end through scripts/overlay-vm.js: a baked frame replaces the composed one, an unbaked coat keeps composing, the halo and outline are still regrown, both eye boxes survive, and a wrong sized grid is ignored.
  • PNG decoder: indexed with tRNS, and all five filter types.
  • npm run lint clean
  • npm run test:boot green
  • Importer CLI run against real fixtures painted from live poses
  • Nobody has actually painted a frame yet, so the pipeline is proven end to end on generated fixtures rather than on real art. The empty default table means this is inert until someone does.

Every pose is composed in code, which is why one sit covers 28 coats and a
new coat costs nine hex values instead of an art pass. The trade is that
changing how the pet looks means editing geometry, and there was no way in
for hand drawn or generated art at all.

src/art-frames.js is the escape hatch: a table of role-letter grids that
wins over the composer for the pose and the coats it names, and nothing
else. Lookup runs coat name, then build name, then '*', then the composer,
so a pack of one frame is a valid pack and a half finished one still runs.

Baked grids are stamped THROUGH buildSprite rather than around it, so they
pick up outlineHalo(), the eye boxes and the muzzle anchor exactly as a
composed pose does. That means the halo is never authored, a patchy outline
gets its gaps filled, and drawCat needs no special case. A grid that does
not match the pose's canvas is ignored rather than trusted, because the
layout maths around it is built on those constants.

scripts/import-frames.js turns painted PNGs into that table. It carries its
own PNG decoder (colour types 0/2/3/4/6, every filter) rather than adding a
dependency, the way pet-sheet.js hand-rolls its encoder. Each cell takes an
area majority vote over the pixels covering it, so art that does not land
exactly on the grid still imports, and every frame is checked before it is
written: canvas size, an eye block either side of the seam eyeBox() splits
on, a nose for the whiskers to anchor to, a floor row to stand on. A frame
that fails is dropped with a reason instead of shipped.

Only the five held poses are importable. The raised-limb activities are
parameterised rigs (4 climb frames, up to 45 paw frames, 14 bat frames per
coat) and a single still would freeze them mid swing, so the importer
refuses them on purpose.

The load-bearing test is a round trip: compose a real pose, paint it out to
a PNG, read it back through the importer and assert the grid is identical.
That covers decode, resample and colour snap against real sprite shapes
rather than a hand written fixture.
Covers the role grid model, the placeholder palette to paint against, the
three things the engine draws on top that must never be authored (the halo,
the pupils, the dog tongue), the file naming, and the checks the importer
runs. Also spells out which activities are held poses and which are
animation rigs, with the frame count behind each rig, because that is the
distinction that decides whether generated art is a drop in or a reference
to redraw the composer against.
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pixelcat Ready Ready Preview Jul 31, 2026 8:10pm

@JOhnsonKC201
JOhnsonKC201 merged commit ae08cbc into main Jul 31, 2026
4 checks passed
@JOhnsonKC201
JOhnsonKC201 deleted the feat/baked-pose-frames branch July 31, 2026 20:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant