Files
so-many-crows/RIVE_CONTRACT.md
T
SmokeyandClaude Opus 5.5 0733ba1d9c Chat layout: roster, channel feed, composer
- The flock is a member list with live status (perched, flying,
  waiting on you, grounded); a scrolling strip on phones
- The Sentry and crows post into one channel; approvals are inline
  buttons on their messages; verdicts show as reactions
- "Who's flying" line above the composer; Ctrl/Cmd+Enter to send
- Consecutive messages from one author group under one name
- RIVE_CONTRACT.md: roster size and still message avatars

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQnSw4RRWp8uqpj3nsnnXF
2026-10-27 12:36:31 -06:00

12 KiB
Raw Blame History

Rive contract: the crows.riv file

The animator builds crows.riv against this file, and the web code drives it from the events in EVENTS.md. Both sides build against this file, so change it first and then the code. Each animation exists only because an EVENTS.md event fires it.

Style: Duolingo-like behavior, pen-and-ink look: cream paper, ink linework, stipple shading (see ui/src/style.css). Smokey's crow art (art/crow.svg) is the character; keep its silhouette. Expressive crows that react through motion (hops, tilts, spins) and never change size, with readable silhouettes at 40 pixels (the chat roster size). Sparkles mean celebration only. The approve and reject buttons stay in HTML for v1 (see "Why buttons stay in HTML").

Rive approach: data binding, not inputs

Rive now marks state machine inputs as deprecated and recommends data binding for new work (migration guide, inputs). So this file uses view models in place of inputs:

  • A view model defines the data shape. A view model instance holds the values, one per crow on screen (view models).
  • Number, Boolean, Trigger, Enum, String and Color properties can drive state machine transition conditions or bind straight to scene elements (property types, transitions).
  • An enum can drive a Solo when its values match the Solo children's names and order, and those names must be exported (enums).
  • Layers play at the same time, one state per layer, and the lower layer wins when two layers animate the same property (layers). Any State reaches a state from wherever the layer is. Exit State stops a layer (states).

1. Artboards

Artboard v1? Purpose
Crow yes One reusable character. It covers all 8 crows plus the Sentry and Elder, using a role enum that switches the costume.
Perch later A scene with a branch of crows rendered as an Artboard List of Crow items (lists), plus flock-wide celebrations.
Splat later A full-screen "gift on your windshield" overlay for the prank.

In v1, the web page is a chat room: one animated Crow canvas per crow in the member roster (sidebar on desktop, a scrolling strip on phones), about 40 pixels. Message avatars are still images, not Rive. The Sentry and Elder are variants of Crow, not separate artboards, so the whole cast shares one rig.

Artboard spec: 256 × 256, transparent background, crow centered with its feet on a baseline 40 pixels from the bottom, so the crows line up in a row.

2. Crow artboard

State machine: CrowMachine. View model: CrowModel, set as the artboard's default view model.

CrowModel properties

Property Type Values / range Drives
role Enum Role crow, sentry, elder Solo Costume (children named crow, sentry, elder, in that order). The Sentry wears a lookout cap and the Elder wears spectacles and a gray ruff.
name String e.g. Hugin, Sentry Text run nameplate under the feet
pose Enum Pose perched, flying, waiting, grounded Body layer
trust Number integer 0–2 (0 stranger, 1 regular, 2 trusted) Badge layer
cheer Trigger Reaction layer: happy hop with a wing pump
sulk Trigger Reaction layer: droop, single feather falls. Sad, never angry.
levelUp Trigger Reaction layer: the level-up celebration (spin, confetti, badge shine). It's the biggest moment in the file.
levelDown Trigger Reaction layer: badge dims, sheepish shrug
prank Trigger Reaction layer: mischievous grin, flies off frame, a "plop" sound cue, returns smug

The 8 crows tell themselves apart by name in v1. A plumage Color property for per-crow tints is later polish.

Layers, states and transitions

Layer Body, a loop that's always running:

State Loop Notes
perched yes Entry state. Idle breathing, blinking, a head tilt every few seconds.
flying yes Wings flapping, a slight bob. It reads as "working".
waiting yes Holds a rolled scroll up toward the viewer and taps a foot. It reads as "your turn".
grounded yes Tipped over and faded. It means failure or timeout. No circling stars (they read as drunk).

Transitions: Any State → each state when pose equals that value, with a duration of 200 milliseconds (ms). There's no Exit Time, so a pose change is instant to read.

Layer Reaction, one-shots on top of Body:

  • Entry → none, an empty state.
  • Any State → cheer, sulk, levelUp, levelDown or prank on the trigger of the same name.
  • Each one-shot → none with Exit Time at 100%.
  • Lengths: cheer and sulk at most 0.8 seconds, levelUp at most 2 seconds, levelDown at most 1 second, prank at most 2.5 seconds.
  • Animate only what Body doesn't need to own (face, wings, effects), or put Reaction below Body in the layer list so the reaction wins while it plays.

Layer Badge:

  • Any State → badge0, badge1 or badge2 when trust equals 0, 1 or 2, with a duration of 150 ms.
  • badge0 shows no badge, badge1 a bronze chest feather, badge2 a gold chest feather with a gentle shimmer loop.

3. Event → animation mapping

The web code keeps a map from item_id to crows[] (from item.answered) so that it can aim verdict at the right crows. The crow field values Sentry and Elder select the instances with role set to sentry and elder.

EVENTS.md event Condition Instance(s) Set
list.started Sentry Create and show the Sentry. pose = perched.
item.triaged needs_approval true Sentry pose = waiting
item.triaged needs_approval false No animation. The HTML shows the mode and reason.
crow.flying crow (create on first sight) pose = flying
crow.landed ok true crow pose = perched
crow.landed ok false crow pose = grounded
item.answered needs_approval true each of crows[] pose = waiting
item.answered needs_approval false No animation. A verdict: auto follows.
verdict target triage, approved Sentry cheer, then pose = perched
verdict target triage, rejected or timeout Sentry sulk, then pose = perched
verdict target triage, auto No animation. A trusted Sentry just keeps going.
verdict target output, approved or auto each of the item's crows[] cheer, then pose = perched
verdict target output, rejected or timeout each of the item's crows[] sulk, then pose = perched
trust.changed holder is a crow, to > from holder trust = to, then levelUp
trust.changed holder is a crow, to < from holder trust = to, then levelDown
trust.changed holder is flock No animation in v1 (HTML banner). Later: a Perch celebration.
item.skipped No animation. The HTML strikes the item through, and any instance still flying for that item goes to pose = perched.
requester.pranked one random crow on screen, or the Sentry if it's alone prank. Later: the Splat overlay. The HTML shows message and a cooldown_s countdown.
list.finished every crow on screen pose = perched, then cheer

Two web-side notes:

  • Pranks. POST /api/lists answers a pranked requester with a 429 error and no list_id, so there may be no event stream to deliver requester.pranked. The web code treats that 429 response the same as the event and fires prank on the Sentry.
  • Badges. trust is per crow and per task type, so a badge shows the level for the task type of the item the crow is working on now. The web code seeds trust from GET /api/trust when it creates an instance, and updates it on trust.changed only when task_type matches.

4. Naming conventions

The web code matches these strings exactly, so they're case-sensitive.

Thing Case Names
File lowercase crows.riv
Artboards PascalCase Crow, Perch, Splat
State machines PascalCase + Machine CrowMachine, PerchMachine
View models PascalCase + Model CrowModel
Enum types PascalCase Role, Pose
Enum values lowercase, one word, matching server strings where they exist crow, sentry, elder, perched, flying, waiting, grounded
Properties (including triggers) camelCase role, name, pose, trust, cheer, sulk, levelUp, levelDown, prank
States and layers camelCase states, PascalCase layers perched, badge2, Body, Reaction, Badge
Exported objects as listed Solo Costume (children crow, sentry, elder), text run nameplate

The web code binds one blank instance per crow, using API names from the web data binding docs:

const r = new rive.Rive({ src: "crows.riv", canvas, artboard: "Crow",
  stateMachines: "CrowMachine", autoplay: true, onLoad: () => {
    const vmi = r.viewModelByName("CrowModel").instance();
    r.bindViewModelInstance(vmi);
    vmi.enum("role").value = "crow";
    vmi.string("name").value = "Hugin";
    vmi.number("trust").value = 1;
    vmi.enum("pose").value = "flying";   // crow.flying
    vmi.trigger("cheer").trigger();      // verdict approved
  } });

Keep autoBind off. Every crow needs its own instance, or setting one crow's pose would move them all.

5. v1 minimum vs. later polish

v1 minimum, the smallest set that makes the web UI work:

  1. The Crow artboard, the CrowMachine state machine and the CrowModel view model, named exactly as in section 4.
  2. The role Solo with all three costumes. The Sentry and Elder must read as different from the crows at a glance.
  3. The name text run.
  4. The Body layer with all four pose states.
  5. The Reaction layer with cheer, sulk and levelUp.
  6. The Badge layer with all three trust states.

If levelDown or prank doesn't exist yet, the web code falls back to sulk and cheer, so they can ship a beat later without breaking anything.

Later polish, in rough order:

  1. levelDown and prank reactions.
  2. A plumage Color property so each of the 8 crows has a distinct tint.
  3. The Splat artboard for the windshield gift.
  4. The Perch scene: an Artboard List of crows, murder huddles and split formations, and a flock levelUp on trust.changed with holder set to flock.
  5. A streak ring, a Duolingo-style progress bar toward promotion at 10 clean approvals. EVENTS.md carries no streak count today, so this needs a streak field added to verdict or trust.changed first.
  6. A Rive-drawn approve or reject button, if HTML buttons ever feel flat.

Why buttons stay in HTML

Approval is the safety gate, and the server treats anything other than an explicit yes as a no. Real <button> elements give keyboard, screen reader and focus support for free, and an animation glitch can never swallow a click. Rive decorates the moment by having the crow hold up its scroll. The click itself belongs to the browser.