@docyrus/animaguru 0.22.0 → 0.24.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 (50) hide show
  1. package/NOTICE.md +25 -1
  2. package/README.md +9 -3
  3. package/dist/animaguru.js +311 -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-caption-news/SKILL.md +34 -0
  10. package/skills/animaguru-caption-news/references/recipe.md +26 -0
  11. package/skills/animaguru-city-symphony/SKILL.md +34 -0
  12. package/skills/animaguru-city-symphony/references/recipe.md +26 -0
  13. package/skills/animaguru-countdown/SKILL.md +34 -0
  14. package/skills/animaguru-countdown/references/recipe.md +26 -0
  15. package/skills/animaguru-data-physicalization/SKILL.md +34 -0
  16. package/skills/animaguru-data-physicalization/references/recipe.md +27 -0
  17. package/skills/animaguru-exploded-teardown/SKILL.md +34 -0
  18. package/skills/animaguru-exploded-teardown/references/recipe.md +27 -0
  19. package/skills/animaguru-kino-montage/SKILL.md +34 -0
  20. package/skills/animaguru-kino-montage/references/recipe.md +26 -0
  21. package/skills/animaguru-lidar-survey/SKILL.md +34 -0
  22. package/skills/animaguru-lidar-survey/references/recipe.md +26 -0
  23. package/skills/animaguru-machine-anatomy/LICENSE +21 -0
  24. package/skills/animaguru-machine-anatomy/SKILL.md +42 -0
  25. package/skills/animaguru-machine-anatomy/references/craft.md +97 -0
  26. package/skills/animaguru-machine-anatomy/references/recipe.md +52 -0
  27. package/skills/animaguru-microscope-story/SKILL.md +34 -0
  28. package/skills/animaguru-microscope-story/references/recipe.md +26 -0
  29. package/skills/animaguru-news-package/SKILL.md +34 -0
  30. package/skills/animaguru-news-package/references/recipe.md +26 -0
  31. package/skills/animaguru-newsreel/SKILL.md +34 -0
  32. package/skills/animaguru-newsreel/references/recipe.md +26 -0
  33. package/skills/animaguru-satellite-briefing/SKILL.md +34 -0
  34. package/skills/animaguru-satellite-briefing/references/recipe.md +27 -0
  35. package/skills/animaguru-telestrator/SKILL.md +34 -0
  36. package/skills/animaguru-telestrator/references/recipe.md +26 -0
  37. package/skills/animaguru-visual-investigation/SKILL.md +34 -0
  38. package/skills/animaguru-visual-investigation/references/recipe.md +26 -0
  39. package/skills/animaguru-white-model-projection/SKILL.md +34 -0
  40. package/skills/animaguru-white-model-projection/references/recipe.md +27 -0
  41. package/templates/video/README.md +1 -1
  42. package/templates/video/VIDEO-STYLE-NOTICES.txt +25 -1
  43. package/templates/video/shared/locales/en/style-studies.json +137 -0
  44. package/templates/video/src/kit/anatomy/LICENSE +21 -0
  45. package/templates/video/src/kit/anatomy/iso-kit.ts +842 -0
  46. package/templates/video/src/kit/anatomy/palette.ts +41 -0
  47. package/templates/video/src/kit/anatomy/pixi.ts +43 -0
  48. package/templates/video/src/studies/anatomy-plotter.ts +179 -0
  49. package/templates/video/src/studies/scenes.ts +379 -0
  50. package/templates/video/style-source-provenance.json +8 -2
@@ -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,364 @@ 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
+ },
570
+ "caption-news": (d, t) => {
571
+ d.background("#141210");
572
+ // A ledger card behind a rolling hook number; a chunk-slammed keyword block; a document highlighted last.
573
+ d.rect(0, 70, 960, 300, gradient("#232019", "#141210"));
574
+ for (let i = 0; i < 5; i++) d.line([[40, 110 + i * 54], [920, 110 + i * 54]], "#ffffff", 1, .06);
575
+ const hook = progress(t, .15, .5);
576
+ d.text(Math.round(1300000 * ease(hook)).toLocaleString(), 90, 230, 52, "#ffc21a", "left", "mono").opacity = hook;
577
+ d.text("colonies lost this year", 90, 270, 20, "#c9c7bd");
578
+ const chunk = progress(t, 2.6, .25), slam = spring(t - 2.6, 1.3, 1, 16);
579
+ if (chunk > 0) {
580
+ d.rect(80, 340, 220 * Math.min(1, slam), 56, "#ffc21a", 4, chunk);
581
+ d.text("{+6%}", 94, 378, 32, "#0e0e0c").opacity = chunk;
582
+ }
583
+ d.text(d.lines[2], 80, 432, 22, "#f7f5ef").opacity = progress(t, 3, .4);
584
+ if (t > 5) {
585
+ d.rect(560, 150, 340, 230, "#efe9da");
586
+ d.text("USDA NASS · 2026", 580, 185, 14, "#5b574a", "left", "mono");
587
+ d.rect(580, 206, 260 * progress(t, 5.3, .6), 20, "#ffd54a", 0, .6);
588
+ d.text("6% more managed colonies", 580, 236, 14, "#2a2a26");
589
+ }
590
+ d.title("#f7f5ef");
591
+ d.footer("#8d8b83");
592
+ },
593
+ "newsreel": (d, t) => {
594
+ d.background("#050504");
595
+ // A projected gate: the ident ritual, then a spinning headline, then an iris to a place card.
596
+ const gx = 180, gy = 40, gw = 600, gh = 460;
597
+ d.rect(gx, gy, gw, gh, "#201d16");
598
+ if (t < 2.4) {
599
+ const globe = spring(t - .3, 0, 1, 5);
600
+ d.poly([[gx + gw / 2, gy + gh], [gx + gw / 2 - 260, gy + 40], [gx + gw / 2, gy + 40]], "#efe7d6").opacity = progress(t, .05, .3) * .18;
601
+ d.poly([[gx + gw / 2, gy + gh], [gx + gw / 2 + 260, gy + 40], [gx + gw / 2, gy + 40]], "#efe7d6").opacity = progress(t, .05, .3) * .18;
602
+ d.ellipse(gx + gw / 2, gy + gh * .55, 90 * globe, 90 * globe, "#d9d4ca").opacity = progress(t, .3, .4);
603
+ ring(d, gx + gw / 2, gy + gh * .55, 90 * globe, "#9e978a", 2);
604
+ d.text("THE WORLD IN REVIEW", gx + gw / 2, gy + gh * .55 + 6, 24, "#161410", "center").opacity = progress(t, 1, .4);
605
+ } else if (t < 5) {
606
+ const spin = progress(t, 2.4, .6), n = d.rect(gx + gw / 2 - 150, gy + gh / 2 - 100, 300, 200, "#efe7d6", 2, progress(t, 2.4, .2));
607
+ n.transform = { x: gx + gw / 2, y: gy + gh / 2, rotation: (1 - spin) * Math.PI * 4, scaleX: .4 + .6 * ease(spin), scaleY: .4 + .6 * ease(spin) };
608
+ const headline = d.text("HONEY CRISIS?", 0, 10, 28, "#161410", "center");
609
+ headline.transform = n.transform;
610
+ headline.opacity = spin;
611
+ } else {
612
+ const iris = progress(t, 5, .4);
613
+ d.rect(gx, gy, gw, gh * (1 - iris), "#050504");
614
+ d.text("ALMOND VALLEY", gx + gw / 2, gy + gh * .82, 22, "#f3eee4", "center").opacity = progress(t, 5.6, .4);
615
+ d.text("· 2026 ·", gx + gw / 2, gy + gh * .82 + 30, 15, "#d6cfc0", "center").opacity = progress(t, 5.6, .4);
616
+ }
617
+ d.title("#ece7dc");
618
+ d.footer("#9a9488");
619
+ d.text(d.lines[2], gx + gw / 2, gy + gh + 34, 15, "#b7b0a2", "center", "mono").opacity = progress(t, 6.2, .4);
620
+ },
621
+ "countdown": (d, t) => {
622
+ d.background("#06080a");
623
+ // Cells multiply toward zero, then merge into one picture; the clock inverts at T plus.
624
+ const stages: [number, number][] = [[1, .05], [2, 1], [4, 2.2], [9, 3.6]];
625
+ let cells = 1;
626
+ for (const [n, at] of stages) if (t >= at) cells = n;
627
+ const zero = 6, merged = t >= zero, cols = cells <= 1 ? 1 : cells <= 2 ? 2 : 3, rows = Math.ceil(cells / cols);
628
+ if (!merged) {
629
+ const pad = 20, gw = (880 - pad * (cols - 1)) / cols, gh = (380 - pad * (rows - 1)) / rows;
630
+ for (let i = 0; i < cells; i++) {
631
+ const cx = 40 + (i % cols) * (gw + pad), cy = 110 + Math.floor(i / cols) * (gh + pad);
632
+ d.rect(cx, cy, gw, gh, "#0b0e11");
633
+ d.rect(cx, cy, gw, gh * (.4 + .3 * Math.abs(Math.sin(t * 2 + i))), "#14181c");
634
+ d.ellipse(cx + 16, cy + 16, 4, 4, "#ff5b1f", .9);
635
+ d.text(`CAM ${i + 1}`, cx + 28, cy + 20, 12, "#7c878d", "left", "mono");
636
+ }
637
+ } else {
638
+ d.rect(40, 110, 880, 380, "#101418");
639
+ d.text("LIVE", 480, 310, 30, "#f2f5f7", "center");
640
+ }
641
+ const span = merged ? t - zero : Math.max(0, zero - t);
642
+ if (merged) d.rect(330, 24, 300, 60, "#ff5b1f", 6, progress(t, zero, .12));
643
+ d.text(`${merged ? "T+" : "T−"}${String(Math.floor(span)).padStart(2, "0")}.${Math.floor((span % 1) * 10)}`, 480, 66, 44, merged ? "#0a0b0c" : "#ff5b1f", "center", "mono");
644
+ d.title("#f2f5f7");
645
+ d.footer("#7c878d");
646
+ d.text(d.lines[2], 480, 448, 16, "#ff5b1f", "center", "mono").opacity = progress(t, zero + .3, .4);
647
+ },
648
+ "city-symphony": (d, t) => {
649
+ const night = progress(t, 4.6, 1.2);
650
+ d.background(gradient("#bcd4e6", "#e8dcc2"));
651
+ d.rect(0, 0, 960, 540, gradient("#0b1020", "#05070d")).opacity = night;
652
+ // A skyline; traffic becomes streaks of light; a grid of streets multiplies with the score; a hard stop.
653
+ for (let i = 0; i < 9; i++) {
654
+ const h = 60 + hash(i) * 150, x = 60 + i * 96;
655
+ block(d, x, 420 - h, 70, h, 24, hash(i + 9) < .5 ? "#cfd3d6" : "#9aa0a6", "#e6e9ea", "#6c7278");
656
+ d.rect(x, 420 - h, 70, h, "#ffd27a", 0, night * (.15 + .5 * hash(i + 3)));
657
+ }
658
+ const speed = t < 2 ? 1 : t < 4 ? 2.4 : t < 4.6 ? 0 : 3.4;
659
+ if (speed > 0) for (let lane = 0; lane < 3; lane++) {
660
+ const y = 460 + lane * 10, x = ((t * speed * 140 + lane * 220) % 1040) - 60;
661
+ d.rect(x, y, 90, 4, "#ffb347", 2, .8);
662
+ d.rect(x - 40, y, 40, 4, "#ffb347", 2, .25);
663
+ }
664
+ if (t >= 4.6 && t < 7.6) {
665
+ const n = Math.min(16, 4 + Math.floor((t - 4.6) * 12)), cols = Math.ceil(Math.sqrt(n));
666
+ for (let i = 0; i < n; i++) {
667
+ const cx = 620 + (i % cols) * 70, cy = 60 + Math.floor(i / cols) * 70;
668
+ d.rect(cx, cy, 60, 60, hash(i + Math.floor(t * 8)) < .5 ? "#1a2230" : "#2a3a50", 2, .9);
669
+ }
670
+ }
671
+ if (t > 7.6) d.rect(0, 0, 960, 540, "#000000", 0, progress(t, 7.6, .15));
672
+ d.title(night > .5 ? "#ede8df" : "#2b2a26");
673
+ d.footer(night > .5 ? "#b0aaa0" : "#6a6560");
674
+ d.text(d.lines[2], 480, 462, 18, "#d9772f", "center").opacity = progress(t, 1.6, .4) * (1 - progress(t, 7.4, .2));
675
+ },
676
+ "visual-investigation": (d, t) => {
677
+ d.background("#0b0c0e");
678
+ d.title("#eceae4");
679
+ d.footer("#8b9097");
680
+ // Three sources sync to one stopwatch; a frame is frozen, circled and magnified; red links converge on a map.
681
+ const panels: [number, number, number, number][] = [[60, 100, 230, 170], [320, 100, 230, 170], [580, 100, 230, 170]];
682
+ const labels = ["照片 · PHOTO", "FBI 电传 · 1937", "原声 · RADIO"];
683
+ panels.forEach(([x, y, w, h], i) => {
684
+ const on = progress(t, .4 + i * .6, .2);
685
+ if (on <= 0) return;
686
+ d.rect(x, y, w, h, "#141619", 0, on);
687
+ d.rect(x + 1, y + 1, w - 2, (h - 2) * (.3 + .5 * hash(i + Math.floor(t * 3))), "#2e3238", 0, on * .8);
688
+ d.text(labels[i], x + 10, y + h - 14, 13, "#8b9097", "left", "mono").opacity = on;
689
+ });
690
+ d.text(`T+${t.toFixed(1)}s`, 480, 70, 26, "#ff4a2e", "center", "mono").opacity = progress(t, .1, .4);
691
+ if (t > 2.6) {
692
+ ring(d, 205, 185, 40, "#ff4a2e", 3, progress(t, 2.6, .5));
693
+ if (t > 3.3) { d.ellipse(330, 185, 55, 55, "#141619", .95); ring(d, 330, 185, 55, "#ff4a2e", 2); d.ellipse(205, 185, 20, 20, "#2e3238", progress(t, 3.3, .3)); }
694
+ }
695
+ if (t > 5) {
696
+ const k = progress(t, 5, 1);
697
+ d.line([[175, 185], [480, 380]], "#ff4a2e", 2, k);
698
+ d.line([[435, 185], [480, 380]], "#ff4a2e", 2, k);
699
+ d.line([[695, 185], [480, 380]], "#ff4a2e", 2, k);
700
+ d.ellipse(480, 380, 10 * k, 10 * k, "#ff4a2e", k);
701
+ }
702
+ d.text(d.lines[2], 480, 448, 17, "#8b9097", "center").opacity = progress(t, 6.2, .4);
703
+ },
704
+ "kino-montage": (d, t) => {
705
+ const paper = t < 6.5;
706
+ d.background(paper ? "#ede4d0" : "#141210");
707
+ d.title(paper ? "#141210" : "#ede4d0");
708
+ d.footer(paper ? "#141210" : "#ede4d0");
709
+ // A grid lights cell by cell; cut-out figures stack into a tower; a red band wipes to the count.
710
+ const cells = Math.min(9, Math.floor(t * 4));
711
+ for (let i = 0; i < cells; i++) {
712
+ const cx = 560 + (i % 3) * 110, cy = 90 + Math.floor(i / 3) * 110;
713
+ d.rect(cx, cy, 96, 96, i === cells - 1 ? "#d52b1e" : "#141210", 0, .92);
714
+ }
715
+ const floors = Math.min(6, Math.floor(t * 1.1));
716
+ for (let f = 0; f < floors; f++) {
717
+ const fy = 460 - f * 54, side = f % 2 === 0 ? -1 : 1;
718
+ d.rect(140 + side * 6, fy, 160, 44, paper ? "#141210" : "#ede4d0");
719
+ d.poly([[160, fy + 6], [180, fy + 30], [200, fy + 6], [210, fy + 42], [150, fy + 42]], paper ? "#ede4d0" : "#141210");
720
+ }
721
+ const wipe = progress(t, 6.4, .3);
722
+ if (wipe > 0) d.rect(0, 540 - wipe * 540, 960, wipe * 540, "#d52b1e");
723
+ if (floors > 0) d.text(`${floors}`, 820, 500, 56, "#d52b1e", "center", "mono");
724
+ d.text(d.lines[2], 480, 40, 20, "#d52b1e", "center").opacity = progress(t, 6.8, .3);
725
+ },
726
+ "telestrator": (d, t) => {
727
+ d.background("#0c1411");
728
+ d.title("#f4f1e8");
729
+ d.footer("#9fb3aa");
730
+ // Watch the play, freeze it, draw on it, then flatten the picture into a top-down board.
731
+ d.rect(60, 80, 840, 360, gradient("#123324", "#0d2419"));
732
+ for (let i = 1; i < 6; i++) d.line([[60 + i * 140, 80], [60 + i * 140, 440]], "#1f5c3f", 1, .6);
733
+ const live = t < 3;
734
+ if (live) {
735
+ const ballx = 160 + Math.min(t, 2.4) * 260;
736
+ d.ellipse(ballx, 260 + Math.sin(t * 3) * 30, 8, 8, "#f4f1e8", 1);
737
+ d.ellipse(420, 300, 14, 14, "#3fd8ff", 1);
738
+ d.ellipse(560, 230, 14, 14, "#ffd400", 1);
739
+ }
740
+ if (t >= 3 && t < 3.3) d.rect(60, 80, 840, 360, "#ffffff", 0, 1 - (t - 3) / .3);
741
+ if (t >= 3) {
742
+ ring(d, 560, 230, 30, "#ffd400", 3, progress(t, 3, .5));
743
+ d.line([[560, 230], [700, 180]], "#ffd400", 4, progress(t, 3.4, .5));
744
+ if (t > 3.9) { d.rect(590, 150, 220, 34, "#080c0e"); d.text(d.lines[2], 600, 174, 15, "#ffd400").opacity = progress(t, 3.9, .3); }
745
+ }
746
+ const flatten = progress(t, 5.6, 1.2);
747
+ if (flatten > 0) {
748
+ d.rect(60, 80, 840, 360, "#0c1411", 0, flatten * .96);
749
+ d.ellipse(lerp(560, 480, flatten), lerp(230, 260, flatten), lerp(14, 10, flatten), lerp(14, 10, flatten) * .6, "#ffd400", flatten);
750
+ d.ellipse(lerp(420, 620, flatten), lerp(300, 260, flatten), lerp(14, 10, flatten), lerp(14, 10, flatten) * .6, "#3fd8ff", flatten);
751
+ }
752
+ d.text(`❚❚ ${t.toFixed(2)}s`, 90, 60, 15, "#f4f1e8", "left", "mono");
753
+ },
754
+ "news-package": (d, t) => {
755
+ d.background("#121417");
756
+ // A sting slams in and flies to the corner bug; rundown rows flip and tick; a split-flap board changes value.
757
+ const slam = spring(t, 1.3, 1, 10), fly = progress(t, 1.2, .5), px = lerp(480, 860, fly), py = lerp(270, 50, fly), ps = lerp(1, .33, fly) * (t < 1.2 ? slam : 1);
758
+ const plate = d.poly([[-130, -45], [130, -45], [150, 45], [-150, 45]], "#f2cf1d");
759
+ plate.transform = { x: px, y: py, scaleX: ps, scaleY: ps };
760
+ const title = d.text("THE WEEK", 0, 8, 40, "#121417", "center");
761
+ title.transform = plate.transform;
762
+ title.opacity = 1 - fly * .3;
763
+ ["MODELS", "MARKETS", "POLICY"].forEach((label, i) => {
764
+ const at = 1.6 + i * .5, inK = progress(t, at, .3), tick = progress(t, 6.2 + i * .3, .3);
765
+ if (inK <= 0) return;
766
+ d.rect(120, 150 + i * 110, 720, 90, "#1b1e23", 4, inK);
767
+ d.text(label, 150, 205 + i * 110, 28, tick > 0 ? "#f2cf1d" : "#ecece4").opacity = inK;
768
+ if (tick > 0) d.text("✓", 790, 205 + i * 110, 28, "#f2cf1d").opacity = tick;
769
+ });
770
+ if (t > 4.6) {
771
+ d.rect(560, 470, 90, 56, "#22262c", 4);
772
+ d.text(progress(t, 4.6, .6) < 1 ? "142" : "156", 605, 506, 28, "#f2cf1d", "center", "mono");
773
+ }
774
+ d.title("#ecece4");
775
+ d.footer("#8b9097");
776
+ d.text(d.lines[2], 480, 40, 18, "#f2cf1d", "center").opacity = progress(t, 7, .4);
777
+ },
399
778
  };
400
779
  function ninety(t: number) { return 87 + 9 * (1 - progress(t, .2)); }
401
780
  export function studyFrame(id: StudyId, seconds: number): StudyFrame {
@@ -1,11 +1,17 @@
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",
6
+ "editDocumentaryCommit": "77ee8318df779b0736d7de0abe24387e51023735"
5
7
  },
6
8
  "awesome-opus5-5-videos": {
7
9
  "url": "https://github.com/yihui-dev/awesome-opus5-5-videos",
8
10
  "commit": "3d54892e2ae5b0e8d337171e6508bba4cec01ab8"
9
11
  },
10
- "scope": "Material and motion guidance; original scene study implementations. No source demo assets or creator footage bundled."
12
+ "anatomy": {
13
+ "url": "https://github.com/wheresryan22/anatomy",
14
+ "commit": "1482e8469b0a9e0d8cc18ea2740a01423d29ae80"
15
+ },
16
+ "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. editDocumentaryCommit pins lemo-opuscar's Edit & Documentary category (caption-news, newsreel, countdown, city-symphony, visual-investigation, kino-montage, telestrator, news-package); none of its archive footage, documents or audio are bundled, only guidance."
11
17
  }