@docyrus/animaguru 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/NOTICE.md +25 -1
  2. package/README.md +7 -3
  3. package/dist/animaguru.js +166 -3
  4. package/package.json +1 -1
  5. package/skills/animaguru-archive-light-table/SKILL.md +34 -0
  6. package/skills/animaguru-archive-light-table/references/recipe.md +26 -0
  7. package/skills/animaguru-briefing-board/SKILL.md +34 -0
  8. package/skills/animaguru-briefing-board/references/recipe.md +27 -0
  9. package/skills/animaguru-data-physicalization/SKILL.md +34 -0
  10. package/skills/animaguru-data-physicalization/references/recipe.md +27 -0
  11. package/skills/animaguru-exploded-teardown/SKILL.md +34 -0
  12. package/skills/animaguru-exploded-teardown/references/recipe.md +27 -0
  13. package/skills/animaguru-lidar-survey/SKILL.md +34 -0
  14. package/skills/animaguru-lidar-survey/references/recipe.md +26 -0
  15. package/skills/animaguru-machine-anatomy/LICENSE +21 -0
  16. package/skills/animaguru-machine-anatomy/SKILL.md +42 -0
  17. package/skills/animaguru-machine-anatomy/references/craft.md +97 -0
  18. package/skills/animaguru-machine-anatomy/references/recipe.md +52 -0
  19. package/skills/animaguru-microscope-story/SKILL.md +34 -0
  20. package/skills/animaguru-microscope-story/references/recipe.md +26 -0
  21. package/skills/animaguru-satellite-briefing/SKILL.md +34 -0
  22. package/skills/animaguru-satellite-briefing/references/recipe.md +27 -0
  23. package/skills/animaguru-white-model-projection/SKILL.md +34 -0
  24. package/skills/animaguru-white-model-projection/references/recipe.md +27 -0
  25. package/templates/video/README.md +1 -1
  26. package/templates/video/VIDEO-STYLE-NOTICES.txt +25 -1
  27. package/templates/video/shared/locales/en/style-studies.json +73 -0
  28. package/templates/video/src/kit/anatomy/LICENSE +21 -0
  29. package/templates/video/src/kit/anatomy/iso-kit.ts +842 -0
  30. package/templates/video/src/kit/anatomy/palette.ts +41 -0
  31. package/templates/video/src/kit/anatomy/pixi.ts +43 -0
  32. package/templates/video/src/studies/anatomy-plotter.ts +179 -0
  33. package/templates/video/src/studies/scenes.ts +171 -0
  34. package/templates/video/style-source-provenance.json +7 -2
@@ -0,0 +1,41 @@
1
+ import type { LineTone } from "./iso-kit";
2
+
3
+ /** The vendored kit's ISO_CSS tokens as plain colours, for painting its paths outside a styled SVG (PixiJS, study nodes, raster previews). */
4
+ export type AnatomyTheme = "light" | "dark";
5
+ export type AnatomyPalette = {
6
+ page: string;
7
+ card: string;
8
+ text: string;
9
+ strong: string;
10
+ muted: string;
11
+ paper: string;
12
+ face: string;
13
+ top: string;
14
+ shades: readonly [string, string, string, string];
15
+ litTop: string;
16
+ litShade: string;
17
+ halo: string;
18
+ haloOpacity: number;
19
+ dot: string;
20
+ line: Record<LineTone, string>;
21
+ };
22
+
23
+ export const ANATOMY_PALETTES: Record<AnatomyTheme, AnatomyPalette> = {
24
+ light: {
25
+ page: "#f2f2ef", card: "#fbfbfa", text: "#57575a", strong: "#18181a", muted: "#8c8c90",
26
+ paper: "#fbfbfa", face: "#ffffff", top: "#ffffff",
27
+ shades: ["#dcdcd9", "#e5e5e2", "#ededea", "#f4f4f1"],
28
+ litTop: "#ffffff", litShade: "#eeeeeb", halo: "#8a8a80", haloOpacity: 0.32, dot: "#3a3a3e",
29
+ line: { hi: "#26262a", mid: "#8a8a8f", lo: "#b4b4b8", faint: "#d4d4d6", lit: "#000000", red: "#e5484d", green: "#30a46c", blue: "#3e63dd" },
30
+ },
31
+ dark: {
32
+ page: "#0b0b0b", card: "#131313", text: "#a3a3a3", strong: "#e6e6e6", muted: "#6e6e6e",
33
+ paper: "#131313", face: "#1b1b1b", top: "#1c1c1c",
34
+ shades: ["#0f0f0f", "#121212", "#151515", "#181818"],
35
+ litTop: "#262626", litShade: "#1d1d1d", halo: "#050505", haloOpacity: 0.9, dot: "#e6e6e6",
36
+ line: { hi: "#d4d4d4", mid: "#6a6a6a", lo: "#3e3e3e", faint: "#2a2a2a", lit: "#ffffff", red: "#ff6b6b", green: "#6be0a0", blue: "#6b9bff" },
37
+ },
38
+ };
39
+
40
+ /** Dot opacity per tone, as ISO_CSS draws screws and rivets. */
41
+ export const DOT_OPACITY: Record<LineTone, number> = { hi: 0.85, mid: 0.45, lo: 0.22, faint: 0.12, lit: 0.85, red: 0.85, green: 0.85, blue: 0.85 };
@@ -0,0 +1,43 @@
1
+ import type { Graphics } from "pixi.js";
2
+ import type { Flat, LineTone, SolidPaths, Tone } from "./iso-kit";
3
+ import { ANATOMY_PALETTES, DOT_OPACITY, type AnatomyTheme } from "./palette";
4
+
5
+ /** Draws the vendored kit's paths with PixiJS Graphics, in the order and tones its solidSvg, lineSvg and dotsSvg use. */
6
+ export type SolidStyle = { theme?: AnatomyTheme; tone?: Tone; crease?: Tone | "none"; lit?: boolean; flat?: boolean; width?: number };
7
+
8
+ /** Trace a kit path (M, L and Z commands, as pathOf writes them) onto the active Graphics path. */
9
+ export function tracePath(g: Graphics, d: string) {
10
+ const tokens = d.match(/[MLZ]|-?\d+(?:\.\d+)?/g) ?? [];
11
+ for (let i = 0; i < tokens.length;) {
12
+ const command = tokens[i++];
13
+ if (command === "Z") g.closePath();
14
+ else if (command === "M") g.moveTo(Number(tokens[i++]), Number(tokens[i++]));
15
+ else if (command === "L") g.lineTo(Number(tokens[i++]), Number(tokens[i++]));
16
+ else throw new Error(`Unsupported kit path command: ${command}`);
17
+ }
18
+ return g;
19
+ }
20
+
21
+ /** Paper fill, four side shades, top, crease and bevel, then the outline. A lit solid gets the white face and the lit edge. */
22
+ export function drawSolid(g: Graphics, solid: SolidPaths, { theme = "light", tone = "hi", crease = "faint", lit = false, flat = false, width = 1 }: SolidStyle = {}) {
23
+ const c = ANATOMY_PALETTES[theme];
24
+ tracePath(g, solid.fill).fill(lit ? c.face : c.paper);
25
+ if (!flat) {
26
+ solid.shades.forEach((d, shade) => { if (d) tracePath(g, d).fill(lit ? c.litShade : c.shades[shade]); });
27
+ tracePath(g, solid.top).fill(lit ? c.litTop : c.top);
28
+ }
29
+ if (crease !== "none") for (const d of [solid.crease, solid.bevel ?? ""]) if (d) drawLine(g, d, { theme, tone: crease, width });
30
+ drawLine(g, solid.outline, { theme, tone: lit ? "lit" : tone, width });
31
+ return g;
32
+ }
33
+
34
+ export function drawLine(g: Graphics, d: string, { theme = "light", tone = "lo", width = 1, alpha = 1 }: { theme?: AnatomyTheme; tone?: LineTone; width?: number; alpha?: number } = {}) {
35
+ if (d) tracePath(g, d).stroke({ color: ANATOMY_PALETTES[theme].line[tone], width, alpha, cap: "round", join: "round" });
36
+ return g;
37
+ }
38
+
39
+ /** Screws and rivets: small round dots in the dot colour, faded by tone. */
40
+ export function drawDots(g: Graphics, points: readonly Flat[], { theme = "light", tone = "mid", size = 0.5 }: { theme?: AnatomyTheme; tone?: LineTone; size?: number } = {}) {
41
+ for (const [x, y] of points) g.circle(x, y, size).fill({ color: ANATOMY_PALETTES[theme].dot, alpha: DOT_OPACITY[tone] });
42
+ return g;
43
+ }
@@ -0,0 +1,179 @@
1
+ import * as K from "../kit/anatomy/iso-kit";
2
+ import { ANATOMY_PALETTES, DOT_OPACITY, type AnatomyTheme } from "../kit/anatomy/palette";
3
+ import { clamp, lerp, spring } from "./timing";
4
+
5
+ /**
6
+ * Machine Anatomy study figure: one dispensing head on a gantry visits a 4 × 4 tray of wells in a serpentine pass.
7
+ * Original geometry built with the vendored anatomy kit. The readout, the reservoir level and every moving part read
8
+ * the same schedule, so the numbers on screen are the machine's own. Shared by the starter study and the gallery preview.
9
+ */
10
+ export type PlotterMark =
11
+ | { kind: "path"; d: string; fill?: string; stroke?: string; width?: number; opacity?: number; dy: number; halo?: boolean }
12
+ | { kind: "dot"; x: number; y: number; r: number; fill: string; opacity: number; dy: number };
13
+ export type PlotterOptions = { width: number; height: number; theme?: AnatomyTheme; line?: number; start?: number; step?: number; intro?: boolean };
14
+ export type PlotterFrame = { marks: PlotterMark[]; well: number; filled: number; dispensed: number; phase: "moving" | "dispensing" | "settled" };
15
+
16
+ export const WELL_COUNT = 16;
17
+ export const WELL_ML = 2;
18
+ const PREFILLED = 5;
19
+ const PLATE = { x: -150, y: -110, w: 300, d: 220, r: 10 };
20
+ const TRAY = { x: -84, y: -84, w: 168, d: 168, r: 8 };
21
+ const LABEL = { x: -114, y: 91, w: 58, d: 12, r: 1.5 };
22
+ const RULER = { x: 137, y: -92, w: 7, d: 184, r: 1 };
23
+ const PITCH = [-57, -19, 19, 57];
24
+ const RAIL_X = 128, TRAY_TOP = 10, BEAM_Z = 87, BEAM_TOP = 97, RAISED = 58, PLUNGED = 15, NOZZLE_Y = 14;
25
+
26
+ /** Serpentine order: along x across a row, then the gantry steps to the next row. */
27
+ export function wellAt(index: number): [number, number] {
28
+ const row = Math.floor(index / 4), column = row % 2 ? 3 - (index % 4) : index % 4;
29
+ return [PITCH[column], PITCH[row]];
30
+ }
31
+
32
+ const inOut = (k: number) => { const x = clamp(k); return x < 0.5 ? 4 * x ** 3 : 1 - (-2 * x + 2) ** 3 / 2; };
33
+ const projections = new Map<string, K.Projection>();
34
+ function projectionOf(width: number, height: number) {
35
+ const key = `${width}x${height}`;
36
+ let P = projections.get(key);
37
+ if (!P) {
38
+ // Fit what is really there: the plate on its feet and the gantry's full travel, not a box as tall as the head.
39
+ const reach = [...K.boxCorners(PLATE, -18, 0), ...K.boxCorners({ x: -136, y: -77, w: 272, d: 141, r: 0 }, 0, 104)];
40
+ projections.set(key, P = K.fitProjection(reach, width, height, { pad: 6, azimuth: 52, elevation: 33 }));
41
+ }
42
+ return P;
43
+ }
44
+
45
+ /** Where the head is and how full each well is at time t, from one schedule. */
46
+ function schedule(t: number, start: number, step: number) {
47
+ const fill = Array.from({ length: WELL_COUNT }, (_, i): number => (i < PREFILLED ? 1 : 0));
48
+ const visits = Math.min(WELL_COUNT - PREFILLED, Math.max(0, Math.floor((7.6 - start) / step)));
49
+ let [x, y] = wellAt(PREFILLED - 1), tip = RAISED, well = PREFILLED - 1, phase: PlotterFrame["phase"] = "settled";
50
+ for (let v = 0; v < visits; v++) {
51
+ const index = PREFILLED + v, at = start + v * step, k = (t - at) / step;
52
+ if (k < 0) break;
53
+ const from = wellAt(index - 1), to = wellAt(index), travel = inOut(k / 0.48);
54
+ [x, y] = [lerp(from[0], to[0], travel), lerp(from[1], to[1], travel)];
55
+ well = index;
56
+ const down = clamp((k - 0.48) / 0.14), up = clamp((k - 0.84) / 0.14);
57
+ tip = lerp(RAISED, PLUNGED, down * down * (3 - 2 * down) * (1 - up * up * (3 - 2 * up)));
58
+ fill[index] = clamp((k - 0.6) / 0.22);
59
+ phase = k < 0.48 ? "moving" : k < 0.84 ? "dispensing" : "settled";
60
+ }
61
+ return { x, y, tip, fill, well: well + 1, phase };
62
+ }
63
+
64
+ export function plotterFrame(t: number, { width, height, theme = "light", line = 1, start = 1.6, step = 0.72, intro = true }: PlotterOptions): PlotterFrame {
65
+ const P = projectionOf(width, height), c = ANATOMY_PALETTES[theme], marks: PlotterMark[] = [];
66
+ const s = schedule(t, start, step), gy = s.y - NOZZLE_Y, hx = s.x;
67
+ const dispensed = s.fill.reduce((sum, value) => sum + value, 0) * WELL_ML;
68
+ // Each assembly settles from above in turn; the mechanism only runs once everything has landed.
69
+ const lift = (at: number) => (intro ? -K.screenRise(spring(t - at, 34, 0, 9), P) : 0);
70
+ let dy = 0;
71
+ const path = (d: string, fill?: string, stroke?: string, width = line, opacity = 1) => { if (d) marks.push({ kind: "path", d, fill, stroke, width, opacity, dy }); };
72
+ const stroke = (d: string, tone: K.LineTone, width = line, opacity = 1) => path(d, undefined, c.line[tone], width, opacity);
73
+ const dots = (points: K.Point3[], tone: K.LineTone = "mid", r = 0.55) => {
74
+ for (const [x, y] of K.pointsAt(points, P)) marks.push({ kind: "dot", x, y, r: r * P.scale, fill: c.dot, opacity: DOT_OPACITY[tone], dy });
75
+ };
76
+ const solid = (paths: K.SolidPaths, { tone = "hi" as K.Tone, crease = "faint" as K.Tone | "none", lit = false, inner = [] as [string, K.LineTone][] } = {}) => {
77
+ path(paths.fill, lit ? c.face : c.paper);
78
+ paths.shades.forEach((d, shade) => path(d, lit ? c.litShade : c.shades[shade]));
79
+ path(paths.top, lit ? c.litTop : c.top);
80
+ if (crease !== "none") { stroke(paths.crease, crease); if (paths.bevel) stroke(paths.bevel, crease); }
81
+ for (const [d, tone] of inner) stroke(d, tone);
82
+ stroke(paths.outline, lit ? "lit" : tone, lit ? line * 1.25 : line);
83
+ };
84
+ const screw = (x: number, y: number, z: number) => { stroke(K.ring(x, y, 2.4, z, P, 16), "lo"); dots([[x, y, z]], "mid"); };
85
+ const byDepth = (points: [number, number][]) => [...points].sort((a, b) => K.depthOf([a[0], a[1], 0], P) - K.depthOf([b[0], b[1], 0], P));
86
+
87
+ // Base: halo, feet, plate on feet, groove, corner screws, ruler strip beside the front rail, label plate.
88
+ dy = lift(0);
89
+ marks.push({ kind: "path", d: K.haloOf(PLATE, -18, 12, P), fill: c.halo, opacity: c.haloOpacity, dy, halo: true });
90
+ for (const [x, y] of byDepth(K.corners(PLATE, 24))) {
91
+ solid(K.cylinder(x, y, 9, -18, 2, P, 28), { tone: "lo" });
92
+ solid(K.cylinder(x, y, 7, -16, 4, P, 28), { tone: "mid" });
93
+ }
94
+ solid(K.slabOf(PLATE, -12, 12, P, 8, 1.6), { tone: "mid", inner: [[K.sideSeam(PLATE, -5, P, 8), "faint"]] });
95
+ stroke(K.planOutline(K.insetPlan(PLATE, 5), 0, P), "faint");
96
+ for (const [x, y] of K.corners(PLATE, 10)) screw(x, y, 0);
97
+ solid(K.slabOf(RULER, 0, 0.6, P, 3), { tone: "lo", crease: "none" });
98
+ const ticks = K.topTicks(RULER.y + 4, RULER.y + RULER.d - 4, 4, 5, RULER.x + 0.6, 0.6, [2, 3.6], P, "y");
99
+ stroke(ticks.minor, "lo");
100
+ stroke(ticks.major, "mid");
101
+ solid(K.slabOf(LABEL, 0, 0.8, P, 4), { tone: "lo", crease: "none" });
102
+ stroke(K.planOutline(K.insetPlan(LABEL, 1.6), 0.8, P), "faint");
103
+ [[0.62, 3.4], [0.44, 6], [0.3, 8.6]].forEach(([share, y]) => stroke(K.lineOnTop([LABEL.x + 4.5, LABEL.y + y], [LABEL.x + 4.5 + (LABEL.w - 13) * share, LABEL.y + y], 0.8, P), "lo"));
104
+ dots([[LABEL.x + LABEL.w - 4, LABEL.y + 3.4, 0.8], [LABEL.x + LABEL.w - 4, LABEL.y + 8.6, 0.8]], "mid", 0.45);
105
+
106
+ const rail = (x: number) => {
107
+ dy = lift(0.18);
108
+ solid(K.slabOf({ x: x - 5, y: -104, w: 10, d: 208, r: 1.5 }, 0, 7, P, 3, 0.5), { tone: "mid" });
109
+ dots([-80, -40, 0, 40, 80].map((y) => [x, y, 7] as K.Point3), "mid", 0.45);
110
+ for (const y of [-104, 99]) {
111
+ solid(K.slabOf({ x: x - 7, y, w: 14, d: 5, r: 1.2 }, 0, 12, P, 3, 0.5), { tone: "mid" });
112
+ dots([[x - 3.5, y + 2.5, 12], [x + 3.5, y + 2.5, 12]], "mid", 0.45);
113
+ }
114
+ };
115
+ const tower = (x: number) => {
116
+ dy = lift(0.5);
117
+ const carriage = { x: x - 8, y: gy - 12, w: 16, d: 24, r: 2.5 };
118
+ solid(K.slabOf(carriage, 7, 6, P, 4, 0.6), { tone: "mid", inner: [[K.sideSeam(carriage, 10, P, 4), "faint"]] });
119
+ dots(K.corners(carriage, 3).map(([px, py]) => [px, py, 13] as K.Point3), "mid", 0.45);
120
+ // An index bracket hangs from the front carriage to just above the ruler and reads the gantry's travel.
121
+ if (x > 0) solid(K.slabOf({ x: x + 8, y: gy - 1.2, w: 1.8, d: 2.4, r: 0.3 }, 1.2, 11.8, P, 2), { tone: "hi", crease: "none" });
122
+ solid(K.cylinder(x, gy, 7, 13, 4, P, 28, 0.5), { tone: "lo" });
123
+ solid(K.cylinder(x, gy, 5, 17, BEAM_Z - 17, P, 28), { tone: "mid", crease: "none", inner: [[K.sideArc(x, gy, 5, 42, P, 16), "faint"], [K.sideArc(x, gy, 5, 66, P, 16), "faint"]] });
124
+ };
125
+
126
+ rail(-RAIL_X);
127
+ tower(-RAIL_X);
128
+
129
+ // The tray is the subject: sixteen shallow wells, each filled by the head in turn.
130
+ dy = lift(0.34);
131
+ solid(K.slabOf(TRAY, 0, TRAY_TOP, P, 8, 1), { tone: "hi", crease: "lo", inner: [[K.sideSeam(TRAY, 4, P, 8), "faint"]] });
132
+ for (const [x, y] of K.corners(TRAY, 7)) screw(x, y, TRAY_TOP);
133
+ for (let i = 0; i < WELL_COUNT; i++) {
134
+ const [x, y] = wellAt(i), k = s.fill[i];
135
+ path(K.onTop(K.circleRing(x, y, 13, 48), TRAY_TOP, P, true), c.shades[0]);
136
+ path(K.onTop(K.circleRing(x, y, 10.6, 40), TRAY_TOP - 2, P, true), c.shades[2]);
137
+ if (k > 0) path(K.onTop(K.circleRing(x, y, 10.4 * Math.sqrt(0.18 + 0.82 * k), 40), TRAY_TOP - 2 + 1.4 * k, P, true), c.line.blue, undefined, line, 0.35 + 0.55 * k);
138
+ stroke(K.ring(x, y, 13, TRAY_TOP, P, 48), "lo");
139
+ stroke(K.ring(x, y, 10.6, TRAY_TOP - 2, P, 40), "faint");
140
+ }
141
+
142
+ rail(RAIL_X);
143
+ tower(RAIL_X);
144
+
145
+ // The beam carries a guide rail and a ruler; the head runs on the guide.
146
+ dy = lift(0.66);
147
+ const beam = { x: -136, y: gy - 6, w: 272, d: 12, r: 2 };
148
+ solid(K.slabOf(beam, BEAM_Z, BEAM_TOP - BEAM_Z, P, 4, 0.8), { tone: "mid", inner: [[K.sideSeam(beam, BEAM_Z + 3, P, 4), "faint"]] });
149
+ const strip = { x: -122, y: gy + 0.8, w: 244, d: 4.6, r: 0.6 };
150
+ solid(K.slabOf(strip, BEAM_TOP, 0.4, P, 3), { tone: "lo", crease: "none" });
151
+ const scale = K.topTicks(strip.x + 2, strip.x + strip.w - 2, 6, 5, strip.y + strip.d - 0.4, BEAM_TOP + 0.4, [-1.6, -3], P, "x");
152
+ stroke(scale.minor, "lo");
153
+ stroke(scale.major, "mid");
154
+ for (const x of [-RAIL_X, RAIL_X]) screw(x, gy - 3, BEAM_TOP);
155
+ solid(K.slabOf({ x: -124, y: gy + 6, w: 248, d: 1.5, r: 0.4 }, BEAM_Z + 2, 6, P, 2), { tone: "lo" });
156
+
157
+ dy = lift(0.82);
158
+ const body = { x: hx - 12, y: gy + 7.5, w: 24, d: 13, r: 2 };
159
+ solid(K.cylinder(hx, gy + NOZZLE_Y, 2, s.tip, 68 - s.tip, P, 20), { lit: true });
160
+ solid(K.cylinder(hx, gy + NOZZLE_Y, 4.4, 68, 4, P, 24, 0.4), { tone: "lo" });
161
+ solid(K.slabOf(body, 72, 28, P, 4, 0.8), { tone: "hi", crease: "lo", inner: [[K.sideSeam(body, 77, P, 4), "faint"]] });
162
+ // Reservoir window on the front face: the ink drains as the wells fill.
163
+ const face = body.y + body.d, level = 1 - (dispensed - PREFILLED * WELL_ML) / ((WELL_COUNT - PREFILLED + 2) * WELL_ML);
164
+ const window = (z0: number, z1: number) => K.pathOf(K.pointsAt([[hx - 5, face, z0], [hx + 5, face, z0], [hx + 5, face, z1], [hx - 5, face, z1]], P), true);
165
+ path(window(80, 95), c.shades[0]);
166
+ path(window(80, 80 + 15 * clamp(level)), c.line.blue, undefined, line, 0.75);
167
+ stroke(window(80, 95), "lo");
168
+ dots([[hx - 8.5, face, 97.5], [hx + 8.5, face, 97.5]], "mid", 0.45);
169
+ solid(K.cylinder(hx, gy + NOZZLE_Y, 4.2, 100, 4, P, 28), { tone: "mid" });
170
+ stroke(K.knurl(hx, gy + NOZZLE_Y, 4.2, 100, 104, 28, P), "lo");
171
+ stroke(K.lineOnTop([hx - 2.6, gy + NOZZLE_Y], [hx + 3.4, gy + NOZZLE_Y], 104, P), "hi");
172
+
173
+ const filled = s.fill.filter((value) => value >= 1).length;
174
+ return { marks, well: s.well, filled, dispensed, phase: s.phase };
175
+ }
176
+
177
+ /** The live readout for the card's bottom-right corner; the pattern takes {well}, {count} and {ml}. */
178
+ export const plotterReadout = (frame: PlotterFrame, pattern = "well {well} / {count} · {ml} ml") =>
179
+ pattern.replace("{well}", String(frame.well).padStart(2, "0")).replace("{count}", String(WELL_COUNT)).replace("{ml}", frame.dispensed.toFixed(1));
@@ -1,6 +1,8 @@
1
1
  import copy from "../../shared/locales/en/style-studies.json";
2
2
  import { clamp, ease, hash, lerp, progress, sampled, spring, strokePoints, type Point } from "./timing";
3
3
  import { type Paint, type StudyNode, type StudyFrame, STUDY_DURATION } from "./model";
4
+ import { plotterFrame, plotterReadout } from "./anatomy-plotter";
5
+ import { ANATOMY_PALETTES } from "../kit/anatomy/palette";
4
6
  export const STUDY_IDS = Object.keys(copy.styles) as (keyof typeof copy.styles)[];
5
7
  export type StudyId = keyof typeof copy.styles;
6
8
  /** Original geometry and choreography; source inspirations are recorded in skill references. */
@@ -175,6 +177,25 @@ const scenes: Record<StudyId, (d: Drawing, t: number) => void> = {
175
177
  d.rect(740, 359, 148, 62, "#123b5a");
176
178
  d.text("FIG. 01", 755, 379, 18, "#dbedf0", "left", "mono");
177
179
  },
180
+ "machine-anatomy": (d, t) => {
181
+ const c = ANATOMY_PALETTES.light;
182
+ d.background(c.page);
183
+ d.rect(36, 28, 888, 470, c.card).resource = "material";
184
+ d.path("M36,28 L924,28 L924,498 L36,498 Z", "none", c.line.mid, 1).resource = "material";
185
+ const figure = plotterFrame(t, { width: 828, height: 368, start: .8, step: .82, intro: false });
186
+ for (const mark of figure.marks) {
187
+ if (mark.kind === "path" && mark.halo)
188
+ continue;
189
+ const node = mark.kind === "dot" ? d.ellipse(mark.x, mark.y, mark.r, mark.r, mark.fill, mark.opacity) : d.path(mark.d, mark.fill ?? "none", mark.stroke, mark.width, mark.opacity);
190
+ node.transform = { x: 66, y: 86 + mark.dy };
191
+ }
192
+ const label = (text: string, x: number, y: number, color: string, align: StudyNode["align"]) => { d.text(text, x, y, 12, color, align, "mono").weight = "400"; };
193
+ label("Fig 1", 60, 44, c.text, "left");
194
+ label(d.lines[0].replace(/\.$/, "").toUpperCase(), 900, 44, c.muted, "right");
195
+ label(d.lines[2].toUpperCase(), 60, 470, c.muted, "left");
196
+ label(plotterReadout(figure, copy.styles["machine-anatomy"].readout), 900, 470, c.text, "right");
197
+ label(d.lines[1], 36, 512, c.muted, "left");
198
+ },
178
199
  "holographic-scan": (d, t) => {
179
200
  d.background(gradient("#07262c", "#030d18"));
180
201
  d.title("#c7ece6");
@@ -396,6 +417,156 @@ const scenes: Record<StudyId, (d: Drawing, t: number) => void> = {
396
417
  }
397
418
  d.text(d.lines[2], 480, 437, 20, "#c3d9c6", "center");
398
419
  },
420
+ "satellite-briefing": (d, t) => {
421
+ d.background(gradient("#3a3f42", "#121619"));
422
+ d.grain("#d6dbde", .07, 320);
423
+ // Each dated pass leaves its shoreline; the current one is white, earlier ones stay as dimmer history.
424
+ const shore = (r: number): Point[] => Array.from({ length: 49 }, (_, i) => { const a = i / 48 * Math.PI * 2, k = r * (1 + .17 * Math.sin(a * 3 + 1) + .07 * Math.sin(a * 7 + 2)); return [470 + Math.cos(a) * k * 1.45, 278 + Math.sin(a) * k]; });
425
+ const sizes = [118, 96, 76, 58], passes = [.5, 1.8, 3, 4.1];
426
+ const current = passes.filter((at) => t >= at).length - 1, scan = progress(t, passes[Math.max(0, current)], .7);
427
+ d.poly(shore(sizes[Math.max(0, current)]), "#0c1418");
428
+ for (let i = 0; i <= current; i++)
429
+ d.line(shore(sizes[i]), i === current ? "#f2f5f6" : "#9aa4a9", i === current ? 2 : 1.2, i === current ? scan : 1);
430
+ if (scan < 1) d.rect(0, 100 + scan * 350, 960, 22, "#ffffff", 0, .07);
431
+ const grow = progress(t, 5.1, 1.2), from = 470 + sizes[0] * 1.45 * 1.05, to = 470 + sizes[3] * 1.45 * .95;
432
+ if (grow > 0) d.line([[from, 278], [lerp(from, to, grow), 278]], "#ffbe55", 3);
433
+ d.text(`${Math.round(64 * grow)} km`, 690, 168, 40, "#ffbe55").opacity = progress(t, 5.1, .3);
434
+ d.title("#e9eef0");
435
+ d.footer("#9aa6ab");
436
+ d.text(d.lines[2], 480, 448, 17, "#c9d1d5", "center", "mono");
437
+ },
438
+ "briefing-board": (d, t) => {
439
+ d.background(gradient("#f4f4f0", "#e3e4e0"));
440
+ d.rect(18, 18, 924, 504, "#d4d6d8", 4, .25);
441
+ d.title("#2b2d31");
442
+ d.footer("#5d6066");
443
+ // Graphite for rules, blue for measurement, red only for the decision.
444
+ const limit = 640;
445
+ if (t > .4) d.line([[limit, 120], [limit, 430]], "#1f5fbf", 2, progress(t, .4, .6));
446
+ const ranges = [330, 560, 760];
447
+ ranges.forEach((end, i) => {
448
+ const y = 170 + i * 105, at = .9 + i * 1.1;
449
+ d.rect(60, y - 28, 110, 56, "#fbfbf8", 3).opacity = progress(t, at, .3);
450
+ d.ellipse(115, y - 30, 7, 7, "#2a2c30", progress(t, at + .1, .1));
451
+ if (t > at + .2) d.line([[200, y], [200 + (end - 200) * progress(t, at + .2, .6), y]], "#1f5fbf", 4);
452
+ if (t <= at + .8) return;
453
+ if (end < limit) d.line([[70, y - 20], [160, y + 20]], "#d8322b", 4, progress(t, at + .8, .25));
454
+ else d.line(Array.from({ length: 33 }, (_, k): Point => [end + Math.cos(k / 32 * Math.PI * 2) * 34, y + Math.sin(k / 32 * Math.PI * 2) * 22]), "#d8322b", 3, progress(t, at + .8, .4));
455
+ });
456
+ const stamp = spring(t - 5, 1.6, 1, 12), n = d.rect(-120, -34, 240, 68, "#d8322b", 6, progress(t, 5, .1) * .9);
457
+ n.transform = { x: 790, y: 120, rotation: -.14, scaleX: stamp, scaleY: stamp };
458
+ const label = d.text(d.lines[2], 0, -12, 17, "#fff6f2", "center");
459
+ label.transform = n.transform;
460
+ label.opacity = n.opacity;
461
+ },
462
+ "lidar-survey": (d, t) => {
463
+ d.background("#05080a");
464
+ d.title("#e8efe9");
465
+ d.footer("#7f948a");
466
+ // Returns colour by height; a cutting plane lifts the canopy away, then the ground condenses behind a front.
467
+ const plane = 60 - 58 * progress(t, 1.6, 2.2), front = 120 + 760 * progress(t, 4, 1.6), colors = ["#1f3f35", "#3f7a62", "#7fb894", "#e2f3dc"];
468
+ for (let i = 0; i < 900; i++) {
469
+ const x = 90 + hash(i) * 780, base = 360 + Math.sin(x * .012) * 26, h = hash(i + 700) < .55 ? hash(i + 1400) * 70 : 0;
470
+ if (h > plane) continue;
471
+ if (x < front && h === 0) continue;
472
+ d.ellipse(x, base - h, 1.4, 1.4, h ? colors[Math.min(3, Math.floor(h / 18))] : "#c8b38c", Math.abs(h - plane) < 3 ? 1 : .8);
473
+ }
474
+ const ground: Point[] = Array.from({ length: 41 }, (_, i) => { const x = 90 + i * 19.5; return [x, 360 + Math.sin(x * .012) * 26]; });
475
+ if (t > 4) d.poly([...ground.filter(([x]) => x <= front), [Math.min(front, 870), 430], [90, 430]], gradient("#d6cfc0", "#7b7466", 0, 330, 0, 430));
476
+ if (t > 5.6) d.line([[220, 352], [420, 368], [610, 345]], "#ffb347", 3, progress(t, 5.6, 1));
477
+ d.line([[40, 140], [40, 400]], "#9fb3aa", 1);
478
+ d.line([[34, 400 - 4 * plane], [70, 400 - 4 * plane]], "#d4fff0", 2, t < 4 ? 1 : 0);
479
+ d.text(d.lines[2], 700, 168, 18, "#ffb347").opacity = progress(t, 5.6, .4);
480
+ },
481
+ "white-model-projection": (d, t) => {
482
+ d.background(gradient("#14171b", "#050608"));
483
+ d.title("#f2f2ee");
484
+ d.footer("#9a9fa6");
485
+ // House lights cut; each block switches on in its decade, then a level of light climbs every facade.
486
+ const house = t < 1.3 ? 1 : .18, ramp = ["#2f5bff", "#2aa7ff", "#6fe7f5", "#f2fdff"];
487
+ for (let i = 0; i < 12; i++) {
488
+ const col = i % 4, row = Math.floor(i / 4), x = 250 + col * 112 - row * 46, y = 340 + row * 46, h = 30 + hash(i) * 90, decade = Math.floor(hash(i + 20) * 4);
489
+ const lit = progress(t, 1.6 + decade * .55, .15) * (1 - progress(t, 4.2, .3)), level = Math.min(h, 150 * progress(t, 4.6, 1.6));
490
+ const white = Math.round(40 + 200 * house).toString(16).padStart(2, "0");
491
+ block(d, x, y - h, 70, h, 34, `#${white}${white}${white}`, "#e8e8e4", "#9a9a96");
492
+ d.rect(x, y - h, 70, h, ramp[decade], 0, lit * .85);
493
+ d.rect(x, y - level, 70, level, gradient("#ffb38a", "#6a3cff", 0, y - 150, 0, y), 0, progress(t, 4.6, .3) * (1 - progress(t, 6.6, .4)) * .9);
494
+ }
495
+ d.text(d.lines[2], 680, 168, 18, "#e5e8ec").opacity = progress(t, 1.6, .4);
496
+ },
497
+ "data-physicalization": (d, t) => {
498
+ d.background(gradient("#e6e0d6", "#c4beb4"));
499
+ d.title("#1c1d20");
500
+ d.footer("#6c6a66");
501
+ d.poly([[110, 420], [850, 420], [890, 460], [70, 460]], "#5b3a26");
502
+ // Plates land one by one; every tenth is brass, so each stack is its own ruler.
503
+ const series = [[22, 20, 18, 14, 13, 12], [5, 8, 11, 15, 19, 26]];
504
+ series.forEach((values, s) => values.forEach((count, year) => {
505
+ const x = 150 + year * 120 + s * 46;
506
+ for (let i = 0; i < count; i++) {
507
+ const at = .4 + year * .55 + s * .2 + i * .02, drop = (1 - progress(t, at, .2)) * 30;
508
+ if (t < at - .2) continue;
509
+ d.rect(x, 410 - i * 9 - drop, 40, 7, (i + 1) % 10 === 0 ? "#d2b07a" : s ? "#8fcfd2" : "#c8ccd1", 1);
510
+ }
511
+ }));
512
+ d.text(d.lines[2], 120, 150, 20, "#d4521c").opacity = progress(t, 4.8, .4);
513
+ },
514
+ "exploded-teardown": (d, t) => {
515
+ d.background(gradient("#34363a", "#0d0e10"));
516
+ d.title("#f1efe9");
517
+ d.footer("#8b8984");
518
+ // Layers lift on one axis, top first; labels land in a fixed column, then the product closes again.
519
+ const layers = [["#e9e6df", 5], ["#3d4046", 4], ["#d9dfe2", 3], ["#1b5238", 2], ["#c3c6cc", 1], ["#3d4046", 0]] as const;
520
+ layers.forEach(([color, index], k) => {
521
+ const open = progress(t, 1 + (5 - index) * .15, 1) * (1 - progress(t, 5.6 + index * .1, .4)), y = 360 - index * 12 - index * 34 * open;
522
+ d.poly([[260, y], [520, y - 40], [640, y], [380, y + 40]], color, "#00000055", 1);
523
+ if (index === 2) for (let i = 0; i < 6; i++) d.ellipse(340 + i * 38, y + 6 - i * 6, 10, 5, "#ffb340", progress(t, 2.8, .5) * open * .8);
524
+ const label = progress(t, 2.6 + k * .2, .3) * (1 - progress(t, 5.2, .3));
525
+ if (label > 0) d.line([[640, y], [700, 130 + k * 52]], "#f1efe9", 1, label);
526
+ d.text(`0${k + 1}`, 712, 120 + k * 52, 14, "#ff6b3d", "left", "mono").opacity = label;
527
+ });
528
+ d.text(d.lines[2], 740, 120, 18, "#f1efe9").opacity = progress(t, 2.6, .3) * (1 - progress(t, 5.2, .3));
529
+ },
530
+ "microscope-story": (d, t) => {
531
+ d.background(gradient("#2a3036", "#0b0d0f"));
532
+ d.title("#f4f1ec");
533
+ d.footer("#9ea6ad");
534
+ // One continuous dive about a fixed point: cells grow from specks to bodies with nuclei, then a circle is counted.
535
+ const zoom = Math.exp(Math.log(5) * progress(t, .8, 3.2)), field = { x: 120, y: 96, width: 720, height: 356 };
536
+ d.rect(120, 96, 720, 356, "#f4f1ec").clip = field;
537
+ for (let row = -16; row <= 16; row++)
538
+ for (let col = -30; col <= 30; col++) {
539
+ const i = (row + 40) * 100 + col + 50, x = 480 + (col * 24 + (hash(i) - .5) * 10) * zoom, y = 274 + (row * 24 + (hash(i + 7) - .5) * 10) * zoom, r = 10 * zoom;
540
+ if (x < 120 - r || x > 840 + r || y < 96 - r || y > 452 + r) continue;
541
+ const septum = Math.abs(Math.sin(col * .35 + row * .2)) < .22;
542
+ d.ellipse(x, y, r, r * .9, septum ? "#4a6cc0" : "#c44e5c", .9).clip = field;
543
+ if (zoom > 2 && !septum && hash(i + 3) < .75) d.ellipse(x, y, r * .36, r * .34, "#34244f").clip = field;
544
+ }
545
+ const ring = progress(t, 4.4, .6);
546
+ if (ring > 0) d.line(Array.from({ length: 49 }, (_, i): Point => [480 + Math.cos(i / 48 * Math.PI * 2) * 120, 274 + Math.sin(i / 48 * Math.PI * 2) * 120]), "#ffffff", 2, ring);
547
+ d.rect(140, 410, 120, 3, "#ffffff");
548
+ d.text(`${Math.round(2 + 3 * progress(t, .8, 3.2))}x`, 268, 400, 14, "#ffffff", "left", "mono");
549
+ d.text(d.lines[2], 480, 462, 17, "#78f0cd", "center").opacity = progress(t, 5.2, .4);
550
+ },
551
+ "archive-light-table": (d, t) => {
552
+ d.background(gradient("#f7f9fb", "#e3e8ec"));
553
+ d.title("#15171a");
554
+ d.footer("#6a7076");
555
+ // Objects lie on the light; the loupe lands on one and the negative turns positive inside its rim.
556
+ const power = t < .3 ? .4 : 1;
557
+ landscape(d, 90, 150, 240, 180, 0);
558
+ d.rect(90, 150, 240, 180, "#ffffff", 0, 1 - power);
559
+ d.rect(380, 130, 260, 200, "#1d1f22");
560
+ landscape(d, 395, 145, 230, 170, 2);
561
+ d.rect(395, 145, 230, 170, "#f0ece4", 0, .8 * (1 - progress(t, 3, 1.4)));
562
+ d.rect(680, 230, 190, 140, "#d9d4c8", 2);
563
+ for (let i = 0; i < 5; i++) d.rect(700, 255 + i * 20, 150 - (i % 2) * 40, 5, "#3a3d44", 1, i === 2 ? 1 : .55);
564
+ const land = spring(t - 2, 1.35, 1, 14), loupe = d.ellipse(510, 232, 70 * land, 70 * land, "#ffffff", progress(t, 1.95, .1) * .12);
565
+ loupe.stroke = "#111214";
566
+ loupe.strokeWidth = 9;
567
+ if (t > 4.6) d.line([[700, 306], [850, 306]], "#ff5a1f", 2, progress(t, 4.6, .5));
568
+ d.text(d.lines[2], 680, 410, 17, "#15171a").opacity = progress(t, 3, .5);
569
+ },
399
570
  };
400
571
  function ninety(t: number) { return 87 + 9 * (1 - progress(t, .2)); }
401
572
  export function studyFrame(id: StudyId, seconds: number): StudyFrame {
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "lemo-opuscar": {
3
3
  "url": "https://github.com/lemomo-ai/lemo-opuscar",
4
- "commit": "b65efe6ea393fdc71212898ea4c57c754cfefa46"
4
+ "commit": "b65efe6ea393fdc71212898ea4c57c754cfefa46",
5
+ "evidenceFilmsCommit": "b5e8ef93930b6d828f352055a689320f138237b1"
5
6
  },
6
7
  "awesome-opus5-5-videos": {
7
8
  "url": "https://github.com/yihui-dev/awesome-opus5-5-videos",
8
9
  "commit": "3d54892e2ae5b0e8d337171e6508bba4cec01ab8"
9
10
  },
10
- "scope": "Material and motion guidance; original scene study implementations. No source demo assets or creator footage bundled."
11
+ "anatomy": {
12
+ "url": "https://github.com/wheresryan22/anatomy",
13
+ "commit": "1482e8469b0a9e0d8cc18ea2740a01423d29ae80"
14
+ },
15
+ "scope": "Material and motion guidance; original scene study implementations. No source demo assets, data or creator footage bundled. The Machine Anatomy kit file src/kit/anatomy/iso-kit.ts is vendored unchanged from anatomy."
11
16
  }