diff --git a/RIVE_CONTRACT.md b/RIVE_CONTRACT.md index 435d6c9..bd8e1a7 100644 --- a/RIVE_CONTRACT.md +++ b/RIVE_CONTRACT.md @@ -2,7 +2,7 @@ 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"). +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 96 pixels. 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 @@ -54,7 +54,7 @@ The 8 crows tell themselves apart by `name` in v1. A `plumage` Color property fo | `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. | +| `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. diff --git a/ui/src/style.css b/ui/src/style.css index 9689a96..09235b7 100644 --- a/ui/src/style.css +++ b/ui/src/style.css @@ -117,15 +117,10 @@ main { max-width: 680px; margin: 0 auto; padding: 0 16px 64px; } .pose-flying .jaw { transform: rotate(-10deg); } .pose-waiting .crow-body { animation: tilt 1.4s ease-in-out infinite; } .pose-waiting::after { - content: ""; position: absolute; top: -14px; left: 0; width: 34px; height: 24px; - background: var(--sparkle) 0 2px / 18px 18px no-repeat, var(--glint) 22px 0 / 11px 11px no-repeat; - animation: twinkle-sparkle 1.4s ease-in-out infinite; + content: "?"; position: absolute; top: -12px; left: 4px; font: 400 26px/1 var(--display); + animation: pop 1.4s ease-in-out infinite; } .pose-grounded .crow-body { transform: rotate(-18deg) translateY(6px); opacity: 0.5; } -.pose-grounded::after { - content: ""; position: absolute; top: -8px; width: 40px; height: 14px; animation: spin 1.6s linear infinite; - background: var(--sparkle) 0 2px / 10px 10px no-repeat, var(--glint) 15px 0 / 9px 9px no-repeat, var(--sparkle) 28px 3px / 9px 9px no-repeat; -} /* Reaction layer: one-shots on top */ .r-cheer .crow-body { animation: hop 0.8s ease-out; } @@ -140,19 +135,19 @@ main { max-width: 680px; margin: 0 auto; padding: 0 16px 64px; } .r-levelDown .crow-body { animation: shrug 1s ease-out; } .r-prank .crow-body { animation: prank 2.5s ease-in-out; } -@keyframes breathe { 50% { transform: scale(1.03, 0.98); } } +@keyframes breathe { 50% { transform: translateY(-2px); } } @keyframes flap { from { transform: translateY(0) rotate(-3deg); } to { transform: translateY(-8px) rotate(3deg); } } @keyframes tilt { 50% { transform: rotate(8deg); } } @keyframes pop { 50% { transform: translateY(-4px); } } @keyframes spin { to { transform: rotate(360deg); } } @keyframes twinkle-sparkle { 50% { transform: scale(1.15) rotate(8deg); } } @keyframes twinkle { 50% { transform: rotate(-12deg) scale(1.15); } } -@keyframes hop { 30% { transform: translateY(-22px) scale(0.95, 1.08); } 60% { transform: translateY(0) scale(1.08, 0.92); } } -@keyframes droop { 40%, 70% { transform: translateY(6px) rotate(-6deg) scale(1, 0.94); } } -@keyframes levelup { 20% { transform: scale(1.25) rotate(0); } 60% { transform: scale(1.25) rotate(360deg); } } +@keyframes hop { 35% { transform: translateY(-20px); } 60% { transform: translateY(0); } } +@keyframes droop { 40%, 70% { transform: translateY(6px) rotate(-6deg); } } +@keyframes levelup { 15% { transform: translateY(-14px); } 60% { transform: translateY(-14px) rotate(360deg); } } @keyframes rise { from { opacity: 1; transform: translateY(0) scale(0.6); } to { opacity: 0; transform: translateY(-30px) scale(1.4); } } -@keyframes shrug { 30% { transform: scale(0.9) rotate(-4deg); } 60% { transform: scale(0.9) rotate(4deg); } } -@keyframes prank { 25% { transform: translate(-120px, -60px) rotate(-30deg); opacity: 0; } 55% { transform: translate(120px, -60px); opacity: 0; } 80% { transform: scale(1.1); opacity: 1; } } +@keyframes shrug { 30% { transform: rotate(-5deg); } 60% { transform: rotate(5deg); } } +@keyframes prank { 25% { transform: translate(-120px, -60px) rotate(-30deg); opacity: 0; } 55% { transform: translate(120px, -60px); opacity: 0; } 80% { transform: none; opacity: 1; } } @media (prefers-reduced-motion: reduce) { .crow *, .crow::before, .crow::after { animation: none !important; transition: none !important; }