Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/pixel-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@typeonce/ritmo-tools": minor
"@typeonce/ritmo": patch
---

`ritmo-tools pixel`: review all of a game's pixel art with a person on one page, and read back what they said.

- **`pixel review <paths...>`** serves a local page (port 5193) with every PNG and text sprite under the folders named, by folder, each with its status. A person looks at each asset at whole-number zooms, opening as large as fits with 1× a key away, over a dark, light, checkerboard, or custom backdrop, plays sheets by animation with their durations, sees a tile repeated, lines the whole set up at one scale on a grid by size (picking several to compare or approve at once), approves or asks for changes, and drags over the art to pin a comment to an area of pixels on one frame. Each asset's lint against its nearest `pixel.config.json` shows beside it. What the tools write beside the art (previews, project PNGs, onion pictures, scaled copies, frame folders) is left out, and a fix shows as one row with what it was made from.
- **`pixel notes <paths...>`** prints a `summary` of what is left, then each asset's status, note, open comments (numbered, with their area and frame), how many are resolved, its lint, and `next`, changes asked first. **`pixel resolve <asset> <numbers...> --reply "..."`** marks comments done; the open page shows them with the reply.
- **Approval keeps the pixels the person saw**: a hash of each frame as decoded, and its duration. A re-export that looks the same is no change, and neither is a color hidden under a transparent pixel. When an approved asset later looks different or plays at other timings, `pixel notes` and the page say which frames under `changedSinceApproved`, and the page offers **Approve it as it is now**.
- The review lives in one `pixel.review.json` beside the game's `package.json`, keyed by path, out of the folder that ships.
- `review`, `notes`, and `resolve` exit 0 or 2 only.
- `pixel lint` no longer runs out of memory on a picture of thousands of colors (a smooth app icon took gigabytes comparing every pair): past 512 colors it says the picture is not pixel art yet instead. On the review page and in `pixel notes`, a sheet frame with nothing drawn in it is an `empty-frame` error.

The `ritmo-pixel-art` skill says when to open the review and how to act on it, and that approving on the page does not replace looking at the art in the game at display size.
1 change: 1 addition & 0 deletions packages/core/PIXEL-ART.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ Quotes were checked against the text of the linked page on 2026-11-01. A quote m
- Wesnoth wiki ([link][wesnoth]): "Test your unit early in the game."
- Yu ([link][yu]): "if you squint, a few large clusters of light and dark should still emerge."
- **Ritmo:** every Still Time review was written from phone captures of real rooms, and after lighting shipped the reviewer wrote "Judge your art under it. Append `?nolight` to the URL to compare against the unlit scene." In the `ritmo-prototype` skill one screenshot is the whole check in a round about looks; this skill asks for the same captures before art goes in. "When the target screen is out of reach, say so" is the `ritmo-prototype` rule of reporting what the check did not run.
- **Ritmo:** "and so does the review page" is the maintainer's direction for `pixel review` (2026-11-04): an approval on the page is the person's, but the page shows each asset alone, at whole-number zooms over a plain backdrop, so it settles the pixels and not the read; the game capture still does.

### A reference is a set of named qualities; change what was asked and nothing else; options at display size and 4× beside it

Expand Down
7 changes: 5 additions & 2 deletions packages/core/skills/ritmo-pixel-art/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: ritmo-pixel-art
description: "Apply whenever a game's pixel art is made, changed, laid out, or checked: the game's grid and scale, the camera, scaling, parallax, or text rendering of a pixel-art game, a sprite drawn with code, a picture from an image generator, a reference image to turn into game art, tiles and sprites judged together in a screenshot, or assets checked before they ship (effects: the `ritmo-fx` skill). Holds the principles that make a game's pixel art read as one game, and says which `ritmo-tools pixel` command to run for each case and how to read its report."
description: "Apply whenever a game's pixel art is made, changed, laid out, or checked: the game's grid and scale, the camera, scaling, parallax, or text rendering of a pixel-art game, a sprite drawn with code, a picture from an image generator, a reference image to turn into game art, tiles and sprites judged together in a screenshot, assets checked before they ship, or a game's art reviewed with a person on a page where they approve it and pin comments to areas of pixels (effects: the `ritmo-fx` skill). Holds the principles that make a game's pixel art read as one game, and says which `ritmo-tools pixel` command to run for each case and how to read its report."
---

# Pixel art for a game
Expand Down Expand Up @@ -37,7 +37,7 @@ Pixel art is a decision about the whole game, not one sprite at a time: one grid

### Judging

- **Judge in the game, at display size, on the target screen.** A sprite is one piece of a crowded screen: look at captures of real scenes at display size, on the smallest screen the game targets, under the game's lighting if it has any, still and in motion, before anything goes in. Squint: the shapes that matter must still be there. A zoomed-in canvas shows the pixels, not the read. When the target screen is out of reach, say so; the person who plays judges there.
- **Judge in the game, at display size, on the target screen.** A sprite is one piece of a crowded screen: look at captures of real scenes at display size, on the smallest screen the game targets, under the game's lighting if it has any, still and in motion, before anything goes in. Squint: the shapes that matter must still be there. A zoomed-in canvas shows the pixels, not the read, and so does the review page: an asset the person approved there, alone at whole-number zooms, is still looked at in the game at display size. When the target screen is out of reach, say so; the person who plays judges there.
- **A reference is a set of named qualities, not a picture to copy.** Say what the reference gives (scale, proportion, palette, mood, detail level) and what the game keeps regardless; say what is not wanted too. Change what was asked for and nothing else, and show two or three options at display size and zoomed 4× beside the reference and in a game capture before integrating one.
- **An exception is a decision, written down.** A second pixel size, a rotating sprite, a smooth parallax, a broken constraint: each is fine when the person chooses it for a reason and the game's design notes say why, and wrong when it happens to one asset. A clean lint, or two agents agreeing, never settles a look; the person who plays does.

Expand All @@ -54,6 +54,8 @@ Every command prints a JSON report on stdout and exits with what to do next:
| 3 | `fixable`: rules broken | Run `pixel fix` (a picture) or edit the source (a sprite), then check again. |
| 4 | `needs-person`: the grid is unsure | Open `<name>.preview.png`; give the grid with `--cell` and `--offset` if you can read it, or hand the recipe to the person (it opens in the web page). Never ship a needs-person fix as done. |

`pixel review`, `pixel notes`, and `pixel resolve` exit 0 or 2 only: the lint they carry is for you to weigh.

### The project's rules

A `pixel.config.json` at the game's root (`pixel init -p <palette>` writes one) holds the palette, the allowed sizes, the color limit, and whether the background must be transparent. Every command reads it from the working directory up, so never repeat them on the command line; change the config instead when the rules change. `pixel palettes` lists the presets.
Expand All @@ -69,6 +71,7 @@ A `pixel.config.json` at the game's root (`pixel init -p <palette>` writes one)
You cannot watch an animation: read each `.onion.png`, a panel per step with the frame before in red and the frame after in blue. A thick band is a jump, a drift, or a change of size, so fix it with `--anchor` or a nudge in the recipe, or redo that frame; the first panel's red is the last frame, where the loop closes. The person watches the `.gif` and judges the motion.
- **A reference image (a drawing, a render, a photo) to turn into game art.** `pixel pixelate ref.png -w 32` cuts it into 32 pixels across on the project's palette (or `--colors N` proposed from the picture). Pixelating loses detail by design: compare widths by their previews, keep the smallest one that still reads, then touch up the `.sprite.txt` and `render` it.
- **A tile that repeats.** `pixel lint tile.png --tile both` (`x` for one that only repeats side by side, `y` one over another) warns of a `seam` when its edges do not meet; `pixel preview tile.png --tile both` writes it repeated. Read that picture whatever the lint says: it finds hard seams only, and whether a repeat hides the grid is judged by eye ([Tiles](#one-style)).
- **Reviewing the art with the person.** `pixel review <paths...>` with the folders the game's art is in (`public/art`, `assets/sprites`), not the whole game; it runs until stopped, so start it in the background (`--host` for a phone), and send the person the `url` it prints. The page lists every asset by folder, shows each at whole-number zooms over a backdrop the person picks, plays sheets by animation, lines the whole set up side by side, and lints each against its nearest `pixel.config.json`. Read back with `pixel notes <paths...>`, the same paths: a `summary`, then each asset's status, note, and open comments, each numbered on an area of one frame (`where`), changes first. Look at that area before you change anything, fix the asset where it is made (a `source` in the notes is the sprite or picture its PNG comes from: change that, and make the PNG again), and mark each comment done with `pixel resolve <asset> <numbers...> --reply "what changed"`. `changedSinceApproved` means an approved asset no longer looks as the person saw it (which frames, or their timing): tell them what changed and ask them to look again; approving is theirs. `gone` lists reviews of files that moved: move or remove each in `pixel.review.json`.
- **Checking assets before they ship.** `pixel lint` each PNG at its true size (exit 0 for all of them), or `pixel check` for pictures that may be upscaled. `pixel compare a.png b.png` says which of two versions is better and why.

### Sprite sources
Expand Down
83 changes: 79 additions & 4 deletions packages/tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ tool per kind of asset:

| Tool | For |
| --- | --- |
| `ritmo-tools pixel` | Pixel art: fixes AI-made or blurry pictures and animation sheets onto a true pixel grid and a palette, renders sprites drawn as text, and checks a project's art against its rules. |
| `ritmo-tools pixel` | Pixel art: fixes AI-made or blurry pictures and animation sheets onto a true pixel grid and a palette, renders sprites drawn as text, checks a project's art against its rules, and opens a local page with all of a game's art by folder, where a person looks at each piece at whole-number zooms, plays its animations, sees the whole set side by side, approves, and pins comments to an area of pixels. |
| `ritmo-tools sound` | Sound effects: keeps them as JSON recipes, measures and varies them, writes WAV files, and opens a local page where a person listens, picks, tunes, and notes. |
| `ritmo-tools fx` | Pixel art effects: keeps each effect as a few drafts written as code, checks them for pixel art craft and the project's rules, draws contact sheets an agent can look at, writes sprite sheets and GIFs, and opens a local page where a person watches the drafts side by side, picks, tunes, and pins comments to a pixel. |
| `ritmo-tools music` | Game music: hears a person's hum for its tune, tempo, and key, keeps songs as JSON tracks of notes, checks them, writes WAV and MIDI files, and opens a local page with all of a game's music on one list, songs and plain audio files, where a person plays, edits notes, records, approves, and comments. |
Expand Down Expand Up @@ -38,7 +38,9 @@ sheet with its JSON, a GIF, and each frame.
It comes in two parts that share one core: a web page for people tuning art
by hand (it runs entirely in the browser; pictures never leave the device),
and the `ritmo-tools pixel` command for agents that check, fix, and make art on their own.
A recipe saved by one runs in the other.
A recipe saved by one runs in the other. The command also opens a review
page on localhost, where a person goes through all of a game's art and the
agent reads back what to change (see "The review page" below).

The page wears Ritmo's brand (`brand/README.md`): its colors, its mark as the
logo, and its Tick orange only on what is active or selected. The palette
Expand Down Expand Up @@ -296,6 +298,12 @@ the source), 4 needs a person (the grid is unsure: look at the preview).
| `compare <a> <b>` | Which of two versions is better, and why. |
| `palettes [name]` | The presets, or one palette's colors. |
| `init` | Writes a starting `pixel.config.json`. |
| `review <paths...> [--port 5193] [--host]` | Serves the review page for the PNGs, text sprites, and folders named, and prints its address; runs until stopped. |
| `notes <paths...>` | What the person said about the same paths: a `summary` of what is left, then `assets`, each one's status, note, open comments (numbered, with their area and frame), how many are resolved, `changedSinceApproved`, the lint, and `next`, changes asked first; `gone` lists reviews whose file is no longer found. |
| `resolve <asset> <numbers...> [--reply "what changed"]` | Marks comments as resolved: `pixel notes` stops listing them, and the page shows them done with the reply. The person can reopen one. |

`review`, `notes`, and `resolve` exit 0 or 2 only: what the lint says about
the art is in the notes, never a failure of them.

`fix`, `check`, `pixelate`, and `preview` take the page's settings as flags
(`--palette`, `--cell 8x8 --offset 2,3`, `--follow straight` or `bend`,
Expand Down Expand Up @@ -380,6 +388,68 @@ of the true size, which are as clean but have half their lines mid-cell.
browser. If it fails, download the `.hex` file from lospec.com and import it.
- It fixes pixels and colors, not the drawing itself.

### The review page

`ritmo-tools pixel review public/art assets/sprites` opens a game's art on a
local page, where a person goes through it and says what to change. Name the
folders the art is in: a folder is searched for PNGs and text sprites, and a
folder with none, or a path that is not there, exits 2. Left out of a folder:
`node_modules`, hidden folders, and what the tools write beside the art
(`.preview.png`, `.project.png`, `.onion.png`, `@Nx.png` and its JSON,
`.frames/` folders). A fix and what it was made from (`hero.fixed.png` and
`hero.png`; likewise `.sheet.png`, `.pixel.png`, and a sprite's
`.render.png`) show as one row: the file the game uses, with its source
named. A `.txt` file is art only when it reads as a sprite source (others,
notes or prompts, are passed over); named outright, one that does not read
exits 2. A PNG with a JSON beside it in the format `pixel sheet` writes (or
Aseprite's) is a sheet, played by animation with each frame's duration;
with a JSON in a format of its own, it shows as a still.

The list on the left shows every asset by folder with its status, whether it
changed since it was approved, its lint, and its open comments; the top bar
narrows it to what is to review, has changes asked, or is approved. The art
opens at the largest whole-number zoom that fits, up to 16×, so a small sprite
is big enough to judge and to pin a comment on, and a picture too big for the
stage shrinks to fit, so nothing scrolls until asked; 0 shows it at 1×, the size the
game draws it before any scaling, and a zoom the person picks stays for the
next asset until F fits again. It sits over a backdrop: dark, light, a
checkerboard, or any color.
**Repeat** draws it three by three, to see a tile meet itself. A sheet
plays each animation with its durations, and the playback bar pauses and
steps through the frames. **Set** shows every asset at one scale, chosen from the set's typical size, on
a grid by size over one backdrop, to see whether they belong together; a
picture too big for the grid is smaller and says its own zoom. Pick several
and **Compare them** shows only those, or **Approve them** approves them
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. On the right are the review (status and note), the comments (resolved
ones greyed, with the agent's reply, and **Reopen**), the lint against the
asset's nearest `pixel.config.json` (**Show** rings a rule's pixels on its
frame), and the files the asset comes from. **Approved** keeps a hash of
each frame's pixels as decoded and its duration: a re-export that looks the
same is no change, and neither is a color hidden under a transparent pixel.
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; the status stays approved, since only the person changes it.

↑ and ↓ change asset, Space plays, ← and → step frames, + and − zoom, 0 goes
back to 1×, F fits each asset again, V switches to the whole set and back, and Ctrl/Cmd+Z undoes.
Every change is saved as it happens. Art the agent rewrites or adds, and
comments it resolves, show up while the page is open; what the person is
typing is kept.

The review is not kept beside the art, which usually sits in a folder that
ships with the game: all of it lives in one `pixel.review.json`, keyed by the
path from there, in the nearest folder above the art with a `package.json`
(or the art's own, when there is none).

Approving on the page is not judging the art in the game: the page shows it
alone at whole-number zooms, and the game shows it among the rest, scaled,
moving, and over its own ground. Look at it in the game at the size it is
drawn too.

### Code

- `src/pixel/core/`: the headless core, tested in `test/` with fake AI pictures made
Expand All @@ -398,7 +468,8 @@ of the true size, which are as clean but have half their lines mid-cell.
`preview.ts` (pictures to look at), `sheet.ts` (clearing a sheet, finding
its frames, anchors, and laying frames on one canvas), `animation.ts` (the
whole fix for a sheet), `spritesheet.ts` (sheet layouts and their JSON),
`gif.ts` (animated GIFs), `zip.ts` (stored ZIP files). The outline
`gif.ts` (animated GIFs), `zip.ts` (stored ZIP files), `review.ts` (the
review file, approval hashes, and the notes). The outline
handling, bent lines, and the outline-keeping shrink follow
[ToPixel](https://github.com/daisuqe/ToPixel).
- `src/pixel/ui/`: the page (plain DOM and a canvas, no framework; `player.ts` plays
Expand All @@ -408,7 +479,11 @@ of the true size, which are as clean but have half their lines mid-cell.
- `src/pixel/cli/`: the `pixel` command (Effect), with PNG files through `pngjs` and
JPEG through `jpeg-js`;
`src/cli.ts` is `ritmo-tools` itself, and `bin/ritmo-tools.mjs` runs it from
its TypeScript source inside the repository.
its TypeScript source inside the repository. `library.ts` finds a game's art,
reads, hashes, and lints each asset, and keeps the review file; `serve.ts` is
the review page's server.
- `src/pixel/review/`: the review page (`lab.ts`), which only draws what the
server decoded, and the store it reads and writes through (`store.ts`).

## Effects lab (`ritmo-tools fx`)

Expand Down
Loading
Loading