Skip to content

ritmo-tools pixel: review a game's pixel art on one page, and read back what the person said - #187

Merged
SandroMaglione merged 10 commits into
mainfrom
claude/project-thread-tt0y2l
Nov 5, 2026
Merged

SandroMaglione merged 10 commits into
mainfrom
claude/project-thread-tt0y2l

Conversation

@SandroMaglione

@SandroMaglione SandroMaglione commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Requested by Sandro · project thread

ritmo-tools pixel gets the review the music has: pixel review <paths...> serves a local page with all of a game's pixel art, pixel notes reads back what the person said, and pixel resolve closes their comments. It follows #186 in shape (one review file at the game's root, a list by folder with a filter, approvals that notice a change), and the page is in the pixel fixer's look.

What the person gets

  • Every PNG and text sprite under the folders named, by folder, with its status, whether it changed since it was approved, its lint, and its open comments. The search box and the top bar narrow the list.
  • One asset opens as large as fits: the largest whole-number zoom up to 16×, or shrunk to fit if it's too big for the stage. 0 shows 1×, a zoom the person picks stays for the next asset, and F fits again. It sits over a dark, light, checkerboard, or custom backdrop, with a 3×3 repeat for tiles. A sheet plays each animation with its own durations, and the playback bar pauses and steps.
  • Set: every asset at one scale, taken from the set's median size, on a grid by size over one backdrop. A picture too big for the grid is smaller and says its own zoom. Picking several lets the person compare only those, or approve them all at once.
  • Dragging over the art pins a comment to that area of the frame shown. A click pins it to one pixel. Each comment's number rings its area at every zoom. Resolved comments show greyed with the agent's reply, and Reopen puts one back.
  • Beside the art: the lint against the asset's nearest pixel.config.json (each frame of a sheet linted on its own, and Show rings a rule's pixels), plus the files the asset comes from.
  • Approved keeps a hash of each frame's decoded pixels and its duration. When the art later looks different or plays at other timings, the page and pixel notes say which frames, and Approve it as it is now takes the new version.

What the agent gets

  • pixel notes prints { summary, assets, gone }. Each asset has file, status, note, its open comments numbered with where ("pixels 1,2 to 2,2"), resolved, changedSinceApproved, the lint as lines, and next. The order is changes asked, then changed since approved, then new, then approved.
  • pixel resolve <asset> <numbers...> --reply works like fx resolve, and the open page shows the result within a couple of seconds.
  • review, notes, and resolve exit 0 or 2, never 3 or 4.
  • The ritmo-pixel-art skill says when to open the review and how to act on it. Its Judging rule now says an approval on the page still leaves the art to be looked at in the game at display size.

Decisions to check

  • The page's scope. It doesn't edit pixels. Fixing by hand stays on the fixer's page, and its About section says so.
  • What counts as an asset.
    • Tool outputs are left out: *.preview.png, *.project.png, *.onion.png, *@Nx.png and its JSON, *.frames/, node_modules, and hidden folders.
    • X.fixed.png, .sheet.png, .pixel.png, and .render.png show as one row with their source named.
    • A .txt file beside a shown PNG with the same stem (X.txt or X.sprite.txt) is taken as that PNG's source, so it gets no row of its own.
    • A .sheet.png names X.png as its source when that file exists, even when X.png is also a .fixed.png's source.
  • Changes beyond the spec:
    • V switches between one asset and the whole set. I didn't use Tab, because Tab moves through the page's controls.
    • Empty sheet frames. In the review's lint, a sheet frame with nothing drawn in it is an empty-frame error on that frame. The sheet pixel sheet made from the fire-spirit sample has one, and on the page it plays as a blink.
    • Lint fix for many-color pictures. pixel lint compared every pair of colors for near-duplicates, which ran out of memory on a smooth 512 px app icon (tens of thousands of colors). Past 512 colors it now says the picture is not pixel art yet. This affects pixel lint and pixel check too, not just the review.
  • Concurrent writes. The page writes the whole review file with the version it last read. If the agent resolved something in between, the server answers 409 with the file as it is now, and the page merges. Per asset, the person's change wins, but comments the agent resolved stay resolved. Then it tries again.

Verification

  • pnpm run check, pnpm --dir website build, and pnpm pack:check pass. The tarball has dist/pixel/page/index.html and leaves out dist/pixel/review/.
  • test/pixel-review.test.ts has 26 tests:
    • finding art: pairs, tool outputs, a non-sprite .txt skipped in a folder and refused when named outright, a foreign atlas.json shown as a still;
    • the review file's place and keys, round trips, and keeping reviews of art outside the paths;
    • approval hashes: an identical re-export, a color hidden under transparent pixels, changed pixels, changed durations;
    • comments, merging, and Aseprite's JSON;
    • the server over HTTP: listing, opening by id, 404 for an unknown id and for path traversal, a 409 on a stale write;
    • notes and resolve through the CLI, with their exit codes.
  • Browser scenarios ran in Chromium against the built page (build and build:page, then node packages/tools/dist/cli.js) and against the source through Vite. The fixture game was a temp folder with its own package.json: 95 assets, made from the website's town tiles, characters and nature art, text sprites under nested configs, a sprite rendered with pixel render, a picture fixed with pixel fix, and a sheet made with pixel sheet from the fire-spirit sample. All 28 checks pass on both. They check:
    • a note typed just before switching assets lands on its own asset;
    • clicking the open row asks the server for nothing;
    • approving sets aria-pressed and saves hashes;
    • the agent rewriting approved art flags it on the row, in the panel, and in pixel notes, and Approve it as it is now clears the flag;
    • an identical re-export (different compression, different RGB under transparent pixels) doesn't flag;
    • a pinned comment's ring sits on the right pixel at every zoom from 1× to 8× (the canvas color under the ring's center was checked against the PNG), and the row counts it;
    • pixel resolve shows on the open page, and the count drops;
    • a sheet with one frame held at 500 ms plays it for 500 ms, and the others for 125 ms on average;
    • a broken asset is flagged and says why while the others still draw;
    • an asset the agent adds shows up;
    • a reload stays on the same asset;
    • on the built page, losing the server shows "offline", and a note typed meanwhile is saved once the server is back;
    • at 390 px wide, nothing goes past the screen in either view;
    • the worst stall while going through 40 assets fast and opening the whole set of 95 was 36 ms (measured as the longest gap between animation frames, and checked against a forced 120 ms task).
  • I looked at the screenshots myself. That's how I found the long file paths running out of the side panel, the playback bar wrapping on a phone, and "1 errors".
  • The real game folders were never used. pixel notes ran on a copy of Spirit Call's public/ with its own package.json. That run found the out-of-memory crash on its app icons, and it now finishes in about 2 seconds. Spirit Call's checkout has no changes.

Tried on the real games' art

After the review above, this ran on copies of the art folders of Still Time, Warden, Spirit Call, Dungeon Paint, Pixel Probe, and Style Probe (208 assets on one page), in Chromium and WebKit, in dark and light. The real game folders were not changed. Sandro looked at the page, which led to the last commit: the zoom that fits, the grid, and nothing moving when it needn't.

All 208 draw, none scrolls by default, each game's review lands at its own root, a re-save of the same pixels doesn't flag an approved asset, and one changed pixel does.

Known problems, left for a follow-up

  • Saving stops after an asset's name goes away. When the agent renames a file, or pixel fix puts X.fixed.png in place of X.png, the page still holds the old name. Every later save then fails with no such asset until a reload, which loses what wasn't saved.
  • A comment being typed is lost when the person clicks a status, switches asset, or pixel resolve reaches the open page.
  • pixel notes misleads on open comments. An approved asset with an open comment says "Leave it as it is", and an asset with a comment but no status click counts as not reviewed.
  • A very large folder blocks the page. On spirit-call/art (2128 PNGs, mostly 3D renders) the server can't answer for about 5 minutes, and pixel notes takes 314 s. Nothing warns about it.
  • source only shows when the source sits beside the PNG. Still Time keeps its sources in another folder, so its notes don't name them.
  • Safari allows 100 URL updates in 10 seconds, and the page makes one per asset opened. Going through assets very fast hit it, and one asset didn't draw.

Not covered

  • Server loss from source. From source, Vite's client reloads the page when its server comes back, so a change made while the server was down is lost there. Only the built page keeps it. This is the same as the other review pages when run from the repo.
  • Phone touch. Drawing an area by touch was not tried with real touch events. Pointer events cover it, and the canvas sets touch-action: none.

Generated by Claude Code

The music review walked folders, named files by their path, and found the
game's root on its own. The pixel review needs the same, so the three move
to src/internal/files.ts and music uses them from there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HendNec6exyZNa2zBVD977
…eet back from its JSON

A review holds a status, a note, and comments pinned to an area of one
frame. Approving keeps a hash of each frame's decoded pixels and its
duration, so a re-export that looks the same is no change while new
pixels or new timing are named, frame by frame. A transparent pixel
hashes the same whatever color it hides, since nobody can see it.

Sheets are read back from the JSON pixel sheet writes, or from
Aseprite's, so the page can play each animation with its durations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HendNec6exyZNa2zBVD977
pixel review serves the art under the named folders on a local page.
The server finds, decodes, hashes, and lints every asset (against its
nearest pixel.config.json, each frame of a sheet apart), so the page only
draws. An id names an asset the server found, never a path it reads.

pixel notes reads back what the person said, the changes first, and
pixel resolve closes comments by number, the way fx resolve does. The
review is one pixel.review.json at the game's root, out of the folder
that ships.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HendNec6exyZNa2zBVD977
Every asset by folder with its status, one asset at a whole-number zoom
over a backdrop of the person's choosing, sheets playing by animation,
and the whole set side by side at its own size. Dragging over the art
pins a comment to that area, and the agent's lint results sit beside it.

Each panel redraws on its own, so typing is never interrupted when the
agent writes, and the review stays owed until the server has it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HendNec6exyZNa2zBVD977
…ing out of memory

A game's folder holds more than pixel art: a smooth 512 px app icon has
tens of thousands of colors, and comparing every pair of them for
near-duplicates took gigabytes and never finished, so the review of a
game with an icon in it crashed. Past 512 colors the lint now says the
picture is not pixel art yet, and only the first few pairs are kept as
text below that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HendNec6exyZNa2zBVD977
A sheet frame with nothing drawn in it is an error on that frame, since
it plays as a blink. The read kept for each asset holds its size, not
its pixels. A broken asset still names the rules it would be linted
against. Long file paths wrap in the side panel, and the playback bar
stays one row on a phone. V, not Tab, switches between one asset and the
whole set, since Tab moves through the page's controls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HendNec6exyZNa2zBVD977
The built review page ships in dist/pixel/page and its compiled source
is left out, as for the other review pages; pack-check holds both. The
README, the ritmo-pixel-art skill, and the website's tools page say what
the review is for and how an agent acts on it, and the skill's Judging
rule now says an approval on the page still leaves the art to be seen in
the game at display size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HendNec6exyZNa2zBVD977
… PNG or a JPEG

The decoder's own words ("unrecognised content at end of stream") named
nothing the person could act on; the other pixel commands already say it
this way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HendNec6exyZNa2zBVD977
@SandroMaglione SandroMaglione self-assigned this Nov 4, 2026
SandroMaglione and others added 2 commits November 5, 2026 10:16
…out on a grid

Tried on the real games' art: at 1× a 24×24 sprite is too small to judge or
to press on, and big pictures made the stage scroll.

- An asset opens at the largest whole zoom that fits, up to 16×, and a picture
  too big for the stage shrinks to fit. 0 shows it at 1×, a zoom the person
  picks stays for the next asset, and F fits again. A zoom kept from a small
  asset no longer makes a canvas too big to draw.
- It fits again when the stage changes size: WebKit first measures it before
  it has its full height.
- The set is a grid by size, at one scale taken from its median asset. A
  picture too big for the grid is smaller and says its own zoom.
- Nothing moves when it needn't: 1× and Fit stay in the bar, and the picks bar
  stays above the set, stuck 8px under the top bar as it scrolls.
- The top bar's two pills line up, and so do the icons in the list and tiles.
  The View section starts closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@SandroMaglione
SandroMaglione merged commit 02a402f into main Nov 5, 2026
5 checks passed
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.

2 participants