# 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. Chunky, expressive crows with big squash-and-stretch reactions and readable silhouettes at 96 pixels. 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](https://rive.app/docs/editor/data-binding/migration-guide), [inputs](https://rive.app/docs/runtimes/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](https://rive.app/docs/editor/data-binding/view-models.md)). - Number, Boolean, Trigger, Enum, String and Color properties can drive state machine transition conditions or bind straight to scene elements ([property types](https://rive.app/docs/editor/data-binding/property-types.md), [transitions](https://rive.app/docs/editor/state-machine/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](https://rive.app/docs/editor/data-binding/enums.md)). - Layers play at the same time, one state per layer, and the lower layer wins when two layers animate the same property ([layers](https://rive.app/docs/editor/state-machine/layers.md)). Any State reaches a state from wherever the layer is. Exit State stops a layer ([states](https://rive.app/docs/editor/state-machine/states.md)). ## 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](https://rive.app/docs/editor/data-binding/lists.md)), plus flock-wide celebrations. | | `Splat` | later | A full-screen "gift on your windshield" overlay for the prank. | In v1, the web page puts one `Crow` canvas per visible crow and lays them out with HTML and CSS. 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 | Sitting dazed with stars circling. It means failure or timeout. | 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](https://rive.app/docs/runtimes/web/data-binding): ```js 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 `