| 1 | /** |
| 2 | * The v2 whale performance, loaded in the browser only. The vendored files |
| 3 | * (vendor/whale-character-v2, see its PROVENANCE.md) attach `WhaleActing` |
| 4 | * and `WhaleHabitat` to window in dependency order — the same executable |
| 5 | * reference the desktop app and its web client draw, so the site never |
| 6 | * keeps a second motion implementation. |
| 7 | */ |
| 8 | import type { WhaleStateKey } from "./content/whale-states"; |
| 9 | |
| 10 | export type WhalePresence = "Offline" | "Idle" | "Listening" | "Thinking" | "Working" | "NeedsYou" | "Done"; |
| 11 | |
| 12 | export type WhaleActivity = { kind: string; observed: boolean; parallel?: number }; |
| 13 | export type WhaleContext = { freshness?: "Live" | "Stale"; turnId?: string; status?: "completed" }; |
| 14 | |
| 15 | export type WhaleDirector = { |
| 16 | f: number; |
| 17 | reduced: boolean; |
| 18 | acting: string; |
| 19 | set(presence: WhalePresence, activity?: WhaleActivity | null, context?: WhaleContext): void; |
| 20 | setReduced(value: boolean): void; |
| 21 | step(dt: number): void; |
| 22 | }; |
| 23 | |
| 24 | type View = { theme: "paper" | "charcoal"; size: number; dpr: number; lod: number; pose?: Record<string, number> }; |
| 25 | |
| 26 | export type WhaleReference = { |
| 27 | acting: { |
| 28 | Director: new (options?: { seed?: number; reduced?: boolean }) => WhaleDirector; |
| 29 | render(ctx: CanvasRenderingContext2D, director: WhaleDirector, view: View): void; |
| 30 | }; |
| 31 | habitat: { |
| 32 | pose(director: WhaleDirector): Record<string, number>; |
| 33 | observe(x: number, y: number): void; |
| 34 | leave(): void; |
| 35 | }; |
| 36 | }; |
| 37 | |
| 38 | let reference: Promise<WhaleReference> | null = null; |
| 39 | |
| 40 | export function loadWhaleReference(): Promise<WhaleReference> { |
| 41 | if (typeof window === "undefined") return Promise.reject(new Error("The whale renderer needs a browser.")); |
| 42 | if (!reference) reference = (async () => { |
| 43 | // @ts-expect-error The vendor reference has no TypeScript declarations. |
| 44 | await import("../vendor/whale-character-v2/mark-data.js"); |
| 45 | // @ts-expect-error The vendor reference has no TypeScript declarations. |
| 46 | await import("../vendor/whale-character-v2/rig.js"); |
| 47 | // @ts-expect-error The vendor reference has no TypeScript declarations. |
| 48 | await import("../vendor/whale-character-v2/props.js"); |
| 49 | // @ts-expect-error The vendor reference has no TypeScript declarations. |
| 50 | await import("../vendor/whale-character-v2/acting.js"); |
| 51 | // @ts-expect-error The vendor reference has no TypeScript declarations. |
| 52 | await import("../vendor/whale-character-v2/habitat.js"); |
| 53 | const globals = window as unknown as { WhaleActing?: WhaleReference["acting"]; WhaleHabitat?: WhaleReference["habitat"] }; |
| 54 | if (!globals.WhaleActing?.Director || !globals.WhaleHabitat?.pose) throw new Error("The whale motion reference did not load."); |
| 55 | return { acting: globals.WhaleActing, habitat: globals.WhaleHabitat }; |
| 56 | })(); |
| 57 | return reference; |
| 58 | } |
| 59 | |
| 60 | /** |
| 61 | * What the homepage whale is acting out. The terminal gallery sets it when a |
| 62 | * reader picks a view, so the character mirrors the screen beside it. This is |
| 63 | * illustration, not telemetry: no session is being observed. |
| 64 | */ |
| 65 | export type WhaleCue = { |
| 66 | presence: WhalePresence; |
| 67 | kind?: string; |
| 68 | parallel?: number; |
| 69 | /** The word shown under the whale (a WHALE_STATE_TEXT key). */ |
| 70 | label: WhaleStateKey; |
| 71 | /** A reader picked this (a terminal view); the autoplay yields to it. */ |
| 72 | chosen?: boolean; |
| 73 | }; |
| 74 | |
| 75 | const listeners = new Set<(cue: WhaleCue) => void>(); |
| 76 | let current: WhaleCue = { presence: "Idle", label: "rest" }; |
| 77 | |
| 78 | export function cueWhale(cue: WhaleCue): void { |
| 79 | current = cue; |
| 80 | listeners.forEach((listener) => listener(cue)); |
| 81 | } |
| 82 | |
| 83 | export function onWhaleCue(listener: (cue: WhaleCue) => void): () => void { |
| 84 | listeners.add(listener); |
| 85 | listener(current); |
| 86 | return () => { listeners.delete(listener); }; |
| 87 | } |
| 88 | |
| 89 | /** |
| 90 | * The session the hero whale acts out on its own: a request arrives, it |
| 91 | * plans, reads, edits, runs the tests, finishes, and rests. Seconds per step. |
| 92 | */ |
| 93 | export const WHALE_PERFORMANCE: { cue: WhaleCue; seconds: number }[] = [ |
| 94 | { cue: { presence: "Idle", label: "rest" }, seconds: 2.5 }, |
| 95 | { cue: { presence: "Listening", label: "listen" }, seconds: 2.8 }, |
| 96 | { cue: { presence: "Thinking", label: "think" }, seconds: 2.6 }, |
| 97 | { cue: { presence: "Working", kind: "reading", label: "read" }, seconds: 3.2 }, |
| 98 | { cue: { presence: "Working", kind: "editing", label: "write" }, seconds: 3.4 }, |
| 99 | { cue: { presence: "Working", kind: "testing", label: "run" }, seconds: 3.2 }, |
| 100 | { cue: { presence: "Done", label: "done" }, seconds: 3.6 }, |
| 101 | ]; |
| 102 |