Tessera
Code-native pixel-art editor — the canvas is a JSON document with an AI editing agent proposing reviewable pixel diffs on top.
- Services
- Product DesignEngineering
- Industries
- AICreative Coding
- Tools
- TypeScriptReactCanvasLLM APIs
- Highlight
- A drawing is a document an AI can edit.
- Live
- tessera-brown-pi.vercel.app
A drawing with no vocabulary for what changed
Pixel art is edited one pixel at a time, and every tool that ships stores the result as pixels — a grid of colours, opaque to everything except the tool that wrote it. That is fine until there is a second author. Then you find the file has no way to say what changed: two versions of a sprite are two images, and the difference between them is a third image.
That is the constraint. Not that a model cannot draw — models draw fine — but that a drawing produced by one is unreviewable. You cannot approve half a brushstroke, and you cannot read a diff of two PNGs.
The file is the drawing
So the canvas is stored as a document, and the document is designed to be read. A palette of named colours, then each layer as an array of strings — one row per string, one character per pixel: a dot is transparent, and 1–9 and a–z are palette indices. A sixteen-colour sprite is sixteen lines of text.
It is a deliberately unclever format and every property that matters follows from it. A row is a line, so a change is a line change, so git diff works on a drawing. A palette entry has a name, so an edit can say wine rather than #b13e53 — and a model asked to darken the outline has a word for the thing it is darkening. Frames are a list, so animation is the same document with more of it. And the same JSON is both the file you export and the panel you read, so there is no second representation to keep in step.
The agent never touches the canvas. It proposes an edit to the file, and the edit passes ten validation gates and lands on a clone before anyone sees it.
Why that makes the model useful
This is the part worth taking away even if you never touch pixel art. A model asked to paint is producing a high-dimensional artefact to an underspecified brief, and it will be confidently mediocre at it. A model asked to edit a text document with a known shape is doing the thing it is most reliable at.
What renders is a preview over the current state, and it lands only when someone accepts it. Rejecting it costs nothing, which is the property that makes it usable at all — a suggestion you cannot cheaply refuse is not a suggestion.
- Document
- JSON, versionedNamed paletteOne character per pixel
- Editing
- Layers and blend modesMulti-frame animationOne gesture, one undo
- Agent
- Gemini, free tierTen validation gatesApplied to a clone first
- Exports
- SVGCSSReactPNGASCII
What it cost
Addressing was harder than it looks. A pixel has coordinates; a region worth talking about — a cap, a highlight, an outline — does not, and inventing stable names for parts of a drawing the author never named is most of the problem. Bounds plus layer is the current answer: coarse, and honest about being coarse.
Undo was the other one. Treating one gesture as one step sounds obvious and is not: a drag across forty cells is one step, an agent patch of four hundred is one step, and a preview that was never accepted is no step at all. The renderer holds two states while a proposal is on screen and the history has to not know about the one that was refused.
Where it does not work yet
The README says it plainly, so this page will too: the AI produces valid edits that are not good edits. A Phase 0 test scored 0 of 9. Every proposal passed the gates, applied cleanly, and could be read as a diff — and not one was a change an artist would have kept.
That is a useful failure rather than an embarrassing one, because it separates two problems that look like one. Making a model's output reviewable is solved here. Making it good is a different problem, it is still open, and the hypotheses are written down in the repo rather than quietly dropped.
Sharing is unfinished for the same reason — it was the next thing, and the next thing was not the interesting thing. The deploy below runs two free requests before it asks for your own API key.