返回 CodeWhale
whale-motion.ts
根目录 / web / lib / whale-motion.ts
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
102 lines TYPESCRIPT