SpriteMark
A painted character driven by the same states as the vector ones.
Two sprite sets and one procedural character, all answering the same protocol id. A host that has built its state logic against CharacterMark can swap in a mascot without touching it.
When to reach for this
The vector characters are the point of Aora Bot: they are procedural, themeable, and weightless. SpriteMark is the escape hatch for the one thing they cannot be — a specific illustration. A brand mascot, a pet drawn by hand, a photograph-derived creature: things with a look that cannot be generated.
The cost is real. A sheet is tens of PNG files instead of a few kilobytes of maths; it does not recolour with the theme; it has exactly the states someone drew. Treat it as an asset, not a component you configure.
The manifest
A sprite-mate manifest is a folder of PNG frames with a manifest.json keyed by protocol id:
framesmaps an id to one or more file names relative to the manifest. One file is a still; several play atfps.fallbacklets a sheet answer ids it has not drawn by pointing at ones it has. Chains are followed for at most four hops, so a typo cannot hang the page.anchoris where the character stands within the frame, as fractions. The image isobject-fit: containand positioned by it, so a tall sheet keeps its feet where the box's feet are.
Resolution order for an id: the id itself; the manifest's fallback chain; the segment fallback — an unknown agent-work id shows the sheet's "busy" frames, an unknown feeling shows idle; and finally idle (02), which every valid sheet must have.
spriteCoverage(manifest, EMOTION_IDS) audits a sheet: how many ids it serves directly, how many by fallback, how many not at all, and any authoring problems (a missing idle, a dangling fallback, a cycle). Run it in a test against your own sheet.
Loading
With src the manifest is fetched on the client and cached per URL for the page's lifetime. Until it arrives — and if it fails — the mark shows a procedural character in the same state, so the layout never has a hole in it and the state is never lost. Pass fallback="none" for an empty box instead.
For server rendering, or when the manifest is bundled, pass it directly with the directory its frames live in. The first frame is emitted as plain markup.
An imported JSON file types kind as string, so narrow it with isSpriteManifest first:
Motion
Frames are shown with a single <img> whose src swaps on a timer. There is no canvas and no per-frame JavaScript beyond that assignment, and the other frames of a clip are preloaded so a swap never flashes an empty box. Under prefers-reduced-motion only the first frame is shown. paused holds the first frame; fps overrides the manifest's rate.
The bundled sheets
The docs ship two sets under /sprites/ for demonstration, both project original and under the Aora Bot License: bao, 32 ids with two frames each, rendered from the procedural bao character (the bun shape), and tapkin, 12 ids with one frame each, of a prompt-designed hamster. Their provenance blocks record how each was made.
API
| Prop | Type | Default | Description |
|---|---|---|---|
alt | string | — | Accessible name. Defaults to the manifest's English name. |
baseUrl | string | — | Directory the manifest's frame files live in. Required with `manifest`. |
className | string | — | — |
color | "black" | "brown" | "red" | "orange" | "yellow" | "green" | "cyan" | "blue" | "violet" | "magenta" | "pink" | "gray" | — | — |
emotionId | string | — | Protocol id. Ignored when `state` is given. |
face | "cute" | "eyes" | "line" | — | — |
fallback | SpriteFallback | 'character' | Shown until a `src` manifest arrives, and if it fails: a procedural character in the same state, or an empty box of the final size. |
fps | number | — | Override the manifest's frame rate. |
manifest | SpriteManifest | — | An already-loaded manifest. Takes precedence over `src`. |
nose | "slant" | "angular" | "hook" | "long" | "curve" | "bow" | null | — | — |
paused | boolean | false | Hold the first frame. |
preset | "mochi" | "sprout" | "boba" | "dot" | "pip" | "byte" | "momo" | "bao" | "teal" | "tato" | "hexa" | "tri" | "gemma" | "nimbo" | "dew" | "leafy" | "bloom" | "lucky" | "shroom" | "oak" | "pokey" | "nashi" | "pomme" | "berry" | "cheri" | "twinkle" | "luv" | "ember" | "chime" | "jelly" | "beep" | "luna" | "finn" | "inky" | "miso" | "hop" | "bruno" | "chirp" | "waddle" | "oink" | "rusty" | "kiki" | "maru" | "zip" | "fuzz" | "lulu" | — | A named character from the cast, supplying the four above together. |
shape | "mochi" | "circle" | "blob" | "pebble" | "egg" | "bun" | "tofu" | "pill" | "hex" | "onigiri" | "gem" | "cloud" | "drop" | "leaf" | "flower" | "clover" | "sprout" | "mushroom" | "acorn" | "cactus" | "pear" | "apple" | "strawberry" | "cherry" | "star" | "heart" | "flame" | "bell" | "ghost" | "bot" | "crescent" | "boba" | "fish" | "octopus" | "cat" | "bunny" | "bear" | "chick" | "penguin" | "pig" | "fox" | "bob" | "topknot" | "spiky" | "curly" | "twinbuns" | — | — |
sizePx | number | 28 | — |
src | string | — | URL of a `manifest.json`. Frames resolve relative to it. Fetched on the client; use `manifest` + `baseUrl` instead to render on the server. |
state | "idle" | "sleeping" | "drowsy" | "bored" | "waking" | "powering-down" | "listening" | "thinking" | "searching" | "reading" | "writing" | "working" | "loading" | "progress" | "radar" | "orbit" | "dictating" | "uploading" | "sending" | "receiving" | "messaging" | "waiting" | "spawning" | "composing" | "dragging" | "happy" | "excited" | "laughing" | "playful" | "proud" | "celebrate" | "bouncing" | "shy" | "sad" | "angry" | "confused" | "suspicious" | "surprised" | "scared" | "curious" | "humming" | "alerting" | "notifying" | "refusing" | — | — |
style | CSSProperties | — | — |
Helpers
Next
- Cast — the named characters
- Agent integration — wiring an agent to a face