@docyrus/animaguru 0.6.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 (198) hide show
  1. package/NOTICE.md +256 -0
  2. package/README.md +241 -0
  3. package/dist/animaguru.js +27338 -0
  4. package/package.json +37 -0
  5. package/skills/animaguru/SKILL.md +115 -0
  6. package/skills/animaguru-advertising/SKILL.md +25 -0
  7. package/skills/animaguru-advertising/references/brand-bumper.md +11 -0
  8. package/skills/animaguru-advertising/references/comparison.md +11 -0
  9. package/skills/animaguru-advertising/references/creator-testimonial.md +11 -0
  10. package/skills/animaguru-advertising/references/editorial-advertorial.md +11 -0
  11. package/skills/animaguru-advertising/references/game-reward.md +11 -0
  12. package/skills/animaguru-advertising/references/offer-promotion.md +11 -0
  13. package/skills/animaguru-advertising/references/product-launch.md +11 -0
  14. package/skills/animaguru-advertising/references/product-proof.md +11 -0
  15. package/skills/animaguru-analog-film-tape/SKILL.md +29 -0
  16. package/skills/animaguru-analog-film-tape/references/recipe.md +23 -0
  17. package/skills/animaguru-animated-data-visualization/SKILL.md +39 -0
  18. package/skills/animaguru-animated-data-visualization/references/data-contract.md +16 -0
  19. package/skills/animaguru-animated-data-visualization/references/variants.md +17 -0
  20. package/skills/animaguru-art-deco-motion/SKILL.md +29 -0
  21. package/skills/animaguru-art-deco-motion/references/recipe.md +22 -0
  22. package/skills/animaguru-ascii-crt/SKILL.md +29 -0
  23. package/skills/animaguru-ascii-crt/references/recipe.md +22 -0
  24. package/skills/animaguru-blueprint-drafting/SKILL.md +29 -0
  25. package/skills/animaguru-blueprint-drafting/references/recipe.md +22 -0
  26. package/skills/animaguru-cartoon-character-animation/SKILL.md +35 -0
  27. package/skills/animaguru-cartoon-character-animation/references/variants.md +17 -0
  28. package/skills/animaguru-cutout-animation/SKILL.md +33 -0
  29. package/skills/animaguru-cutout-animation/references/production-recipe.md +37 -0
  30. package/skills/animaguru-cutout-animation/references/variants.md +17 -0
  31. package/skills/animaguru-cutout-animation/references/visual-style.md +34 -0
  32. package/skills/animaguru-dark-tech-stage/SKILL.md +29 -0
  33. package/skills/animaguru-dark-tech-stage/references/recipe.md +23 -0
  34. package/skills/animaguru-diagram-formula-motion/SKILL.md +36 -0
  35. package/skills/animaguru-diagram-formula-motion/references/variants.md +17 -0
  36. package/skills/animaguru-engraved-relief-print/SKILL.md +29 -0
  37. package/skills/animaguru-engraved-relief-print/references/recipe.md +24 -0
  38. package/skills/animaguru-flat-vector-motion/SKILL.md +35 -0
  39. package/skills/animaguru-flat-vector-motion/references/variants.md +17 -0
  40. package/skills/animaguru-general-video/SKILL.md +20 -0
  41. package/skills/animaguru-general-video/references/ambient-loop.md +11 -0
  42. package/skills/animaguru-general-video/references/music-lyrics.md +11 -0
  43. package/skills/animaguru-general-video/references/short-film.md +11 -0
  44. package/skills/animaguru-general-video/references/titles-event.md +11 -0
  45. package/skills/animaguru-general-video/references/visual-essay.md +11 -0
  46. package/skills/animaguru-generative-light-signal/SKILL.md +29 -0
  47. package/skills/animaguru-generative-light-signal/references/recipe.md +23 -0
  48. package/skills/animaguru-glass-mosaic/SKILL.md +29 -0
  49. package/skills/animaguru-glass-mosaic/references/recipe.md +23 -0
  50. package/skills/animaguru-halftone-editorial/SKILL.md +29 -0
  51. package/skills/animaguru-halftone-editorial/references/recipe.md +23 -0
  52. package/skills/animaguru-holographic-scan/SKILL.md +29 -0
  53. package/skills/animaguru-holographic-scan/references/recipe.md +22 -0
  54. package/skills/animaguru-interface-motion/SKILL.md +36 -0
  55. package/skills/animaguru-interface-motion/references/screen-mapping.md +16 -0
  56. package/skills/animaguru-interface-motion/references/variants.md +17 -0
  57. package/skills/animaguru-isometric-worlds/SKILL.md +35 -0
  58. package/skills/animaguru-isometric-worlds/references/variants.md +17 -0
  59. package/skills/animaguru-kinetic-typography/SKILL.md +35 -0
  60. package/skills/animaguru-kinetic-typography/references/variants.md +17 -0
  61. package/skills/animaguru-miniature-diorama/SKILL.md +29 -0
  62. package/skills/animaguru-miniature-diorama/references/recipe.md +23 -0
  63. package/skills/animaguru-painterly-depth-animation/SKILL.md +35 -0
  64. package/skills/animaguru-painterly-depth-animation/references/variants.md +17 -0
  65. package/skills/animaguru-paper-popup/SKILL.md +29 -0
  66. package/skills/animaguru-paper-popup/references/recipe.md +22 -0
  67. package/skills/animaguru-photo-footage-editorial/SKILL.md +29 -0
  68. package/skills/animaguru-photo-footage-editorial/references/recipe.md +23 -0
  69. package/skills/animaguru-pixel-art-animation/SKILL.md +36 -0
  70. package/skills/animaguru-pixel-art-animation/references/variants.md +17 -0
  71. package/skills/animaguru-portal-zoom/SKILL.md +29 -0
  72. package/skills/animaguru-portal-zoom/references/recipe.md +21 -0
  73. package/skills/animaguru-risograph-print/SKILL.md +29 -0
  74. package/skills/animaguru-risograph-print/references/recipe.md +23 -0
  75. package/skills/animaguru-sand-drawing/SKILL.md +29 -0
  76. package/skills/animaguru-sand-drawing/references/recipe.md +21 -0
  77. package/skills/animaguru-shadow-theatre/SKILL.md +29 -0
  78. package/skills/animaguru-shadow-theatre/references/recipe.md +23 -0
  79. package/skills/animaguru-silkscreen-poster/SKILL.md +29 -0
  80. package/skills/animaguru-silkscreen-poster/references/recipe.md +22 -0
  81. package/skills/animaguru-stop-motion-objects/SKILL.md +29 -0
  82. package/skills/animaguru-stop-motion-objects/references/recipe.md +23 -0
  83. package/skills/animaguru-studio-product-reveal/SKILL.md +29 -0
  84. package/skills/animaguru-studio-product-reveal/references/recipe.md +23 -0
  85. package/skills/animaguru-video/SKILL.md +79 -0
  86. package/skills/animaguru-whiteboard-line-drawing/SKILL.md +34 -0
  87. package/skills/animaguru-whiteboard-line-drawing/references/variants.md +17 -0
  88. package/templates/video/AGENTS.md +34 -0
  89. package/templates/video/CLAUDE.md +19 -0
  90. package/templates/video/README.md +101 -0
  91. package/templates/video/VIDEO-STYLE-NOTICES.txt +54 -0
  92. package/templates/video/film.config.json +10 -0
  93. package/templates/video/gitignore +11 -0
  94. package/templates/video/index.html +14 -0
  95. package/templates/video/package.json +28 -0
  96. package/templates/video/public/brand/logo-mark.svg +5 -0
  97. package/templates/video/scripts/build.mjs +12 -0
  98. package/templates/video/scripts/export-html.mjs +12 -0
  99. package/templates/video/scripts/export-mp4.mjs +209 -0
  100. package/templates/video/scripts/html-export-plugin.mjs +35 -0
  101. package/templates/video/scripts/html-export.mjs +54 -0
  102. package/templates/video/scripts/install-example.mjs +32 -0
  103. package/templates/video/scripts/review-analysis.mjs +29 -0
  104. package/templates/video/scripts/review-audio.mjs +24 -0
  105. package/templates/video/scripts/review.mjs +37 -0
  106. package/templates/video/scripts/starter-film.sha256 +1 -0
  107. package/templates/video/shared/locales/en/export.json +10 -0
  108. package/templates/video/shared/locales/en/film.json +42 -0
  109. package/templates/video/shared/locales/en/player.json +31 -0
  110. package/templates/video/shared/locales/en/style-studies.json +168 -0
  111. package/templates/video/shared/locales/en/timeline.json +33 -0
  112. package/templates/video/src/assets.ts +4 -0
  113. package/templates/video/src/download.ts +27 -0
  114. package/templates/video/src/exporter.ts +70 -0
  115. package/templates/video/src/film-resources.ts +29 -0
  116. package/templates/video/src/film.ts +132 -0
  117. package/templates/video/src/main.ts +219 -0
  118. package/templates/video/src/resource-timeline.ts +193 -0
  119. package/templates/video/src/review.ts +50 -0
  120. package/templates/video/src/studies/film.ts +26 -0
  121. package/templates/video/src/studies/media.ts +46 -0
  122. package/templates/video/src/studies/model.ts +60 -0
  123. package/templates/video/src/studies/pixi-renderer.ts +121 -0
  124. package/templates/video/src/studies/scenes.ts +410 -0
  125. package/templates/video/src/studies/timing.ts +53 -0
  126. package/templates/video/src/studies/variants.ts +39 -0
  127. package/templates/video/src/style.css +111 -0
  128. package/templates/video/src/timeline-model.ts +55 -0
  129. package/templates/video/style-source-provenance.json +11 -0
  130. package/templates/video/tsconfig.json +15 -0
  131. package/templates/video/vite.config.mjs +9 -0
  132. package/vendor/pixijs-skills/LICENSE +21 -0
  133. package/vendor/pixijs-skills/provenance.json +79 -0
  134. package/vendor/pixijs-skills/skills/pixijs/SKILL.md +66 -0
  135. package/vendor/pixijs-skills/skills/pixijs/references/index.md +136 -0
  136. package/vendor/pixijs-skills/skills/pixijs-accessibility/SKILL.md +224 -0
  137. package/vendor/pixijs-skills/skills/pixijs-application/SKILL.md +293 -0
  138. package/vendor/pixijs-skills/skills/pixijs-application/references/application-options.md +314 -0
  139. package/vendor/pixijs-skills/skills/pixijs-assets/SKILL.md +303 -0
  140. package/vendor/pixijs-skills/skills/pixijs-assets/references/background.md +137 -0
  141. package/vendor/pixijs-skills/skills/pixijs-assets/references/bundles.md +122 -0
  142. package/vendor/pixijs-skills/skills/pixijs-assets/references/caching.md +143 -0
  143. package/vendor/pixijs-skills/skills/pixijs-assets/references/compressed-textures.md +120 -0
  144. package/vendor/pixijs-skills/skills/pixijs-assets/references/fonts.md +205 -0
  145. package/vendor/pixijs-skills/skills/pixijs-assets/references/gif.md +171 -0
  146. package/vendor/pixijs-skills/skills/pixijs-assets/references/manifests.md +175 -0
  147. package/vendor/pixijs-skills/skills/pixijs-assets/references/progress.md +142 -0
  148. package/vendor/pixijs-skills/skills/pixijs-assets/references/resolution.md +142 -0
  149. package/vendor/pixijs-skills/skills/pixijs-assets/references/spritesheet.md +119 -0
  150. package/vendor/pixijs-skills/skills/pixijs-assets/references/svg.md +132 -0
  151. package/vendor/pixijs-skills/skills/pixijs-assets/references/video.md +158 -0
  152. package/vendor/pixijs-skills/skills/pixijs-blend-modes/SKILL.md +215 -0
  153. package/vendor/pixijs-skills/skills/pixijs-color/SKILL.md +299 -0
  154. package/vendor/pixijs-skills/skills/pixijs-core-concepts/SKILL.md +132 -0
  155. package/vendor/pixijs-skills/skills/pixijs-core-concepts/references/render-loop.md +216 -0
  156. package/vendor/pixijs-skills/skills/pixijs-core-concepts/references/renderers.md +287 -0
  157. package/vendor/pixijs-skills/skills/pixijs-create/SKILL.md +276 -0
  158. package/vendor/pixijs-skills/skills/pixijs-custom-rendering/SKILL.md +405 -0
  159. package/vendor/pixijs-skills/skills/pixijs-custom-rendering/references/advanced-gpu.md +244 -0
  160. package/vendor/pixijs-skills/skills/pixijs-custom-rendering/references/uniform-types.md +94 -0
  161. package/vendor/pixijs-skills/skills/pixijs-environments/SKILL.md +243 -0
  162. package/vendor/pixijs-skills/skills/pixijs-events/SKILL.md +387 -0
  163. package/vendor/pixijs-skills/skills/pixijs-filters/SKILL.md +300 -0
  164. package/vendor/pixijs-skills/skills/pixijs-html-source/SKILL.md +228 -0
  165. package/vendor/pixijs-skills/skills/pixijs-math/SKILL.md +489 -0
  166. package/vendor/pixijs-skills/skills/pixijs-migration-v8/SKILL.md +415 -0
  167. package/vendor/pixijs-skills/skills/pixijs-performance/SKILL.md +489 -0
  168. package/vendor/pixijs-skills/skills/pixijs-scene-container/SKILL.md +345 -0
  169. package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/SKILL.md +243 -0
  170. package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/constructor-options.md +155 -0
  171. package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/container-hierarchy.md +232 -0
  172. package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/layers.md +145 -0
  173. package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/masking.md +129 -0
  174. package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/render-groups.md +140 -0
  175. package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/scene-management.md +169 -0
  176. package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/transforms.md +198 -0
  177. package/vendor/pixijs-skills/skills/pixijs-scene-dom-container/SKILL.md +193 -0
  178. package/vendor/pixijs-skills/skills/pixijs-scene-gif/SKILL.md +233 -0
  179. package/vendor/pixijs-skills/skills/pixijs-scene-graphics/SKILL.md +576 -0
  180. package/vendor/pixijs-skills/skills/pixijs-scene-mesh/SKILL.md +141 -0
  181. package/vendor/pixijs-skills/skills/pixijs-scene-mesh/references/mesh-perspective.md +189 -0
  182. package/vendor/pixijs-skills/skills/pixijs-scene-mesh/references/mesh-plane.md +159 -0
  183. package/vendor/pixijs-skills/skills/pixijs-scene-mesh/references/mesh-rope.md +176 -0
  184. package/vendor/pixijs-skills/skills/pixijs-scene-mesh/references/mesh-simple.md +144 -0
  185. package/vendor/pixijs-skills/skills/pixijs-scene-mesh/references/mesh.md +213 -0
  186. package/vendor/pixijs-skills/skills/pixijs-scene-particle-container/SKILL.md +323 -0
  187. package/vendor/pixijs-skills/skills/pixijs-scene-sprite/SKILL.md +120 -0
  188. package/vendor/pixijs-skills/skills/pixijs-scene-sprite/references/animated-sprite.md +262 -0
  189. package/vendor/pixijs-skills/skills/pixijs-scene-sprite/references/nineslice-sprite.md +202 -0
  190. package/vendor/pixijs-skills/skills/pixijs-scene-sprite/references/sprite.md +181 -0
  191. package/vendor/pixijs-skills/skills/pixijs-scene-sprite/references/tiling-sprite.md +216 -0
  192. package/vendor/pixijs-skills/skills/pixijs-scene-text/SKILL.md +136 -0
  193. package/vendor/pixijs-skills/skills/pixijs-scene-text/references/bitmap-text.md +232 -0
  194. package/vendor/pixijs-skills/skills/pixijs-scene-text/references/html-text.md +192 -0
  195. package/vendor/pixijs-skills/skills/pixijs-scene-text/references/split-bitmap-text.md +187 -0
  196. package/vendor/pixijs-skills/skills/pixijs-scene-text/references/split-text.md +168 -0
  197. package/vendor/pixijs-skills/skills/pixijs-scene-text/references/text.md +311 -0
  198. package/vendor/pixijs-skills/skills/pixijs-ticker/SKILL.md +292 -0
@@ -0,0 +1,46 @@
1
+ export type MediaCue = {
2
+ start: number;
3
+ end: number;
4
+ sourceStart: number;
5
+ sourceDuration: number;
6
+ rate?: number;
7
+ loop?: boolean;
8
+ };
9
+ /** Map film seconds to a source frame without using the video's independent clock. */
10
+ export function mediaTime(cue: MediaCue, seconds: number) {
11
+ const rate = cue.rate ?? 1;
12
+ if (![cue.start, cue.end, cue.sourceStart, cue.sourceDuration, rate, seconds].every(Number.isFinite) || cue.end <= cue.start || cue.sourceStart < 0 || cue.sourceDuration <= 0 || rate <= 0)
13
+ throw new Error("Invalid media cue.");
14
+ const local = Math.max(0, Math.min(cue.end - cue.start, seconds - cue.start)) * rate;
15
+ const available = cue.sourceDuration - cue.sourceStart;
16
+ if (available <= 0)
17
+ throw new Error("Media trim starts outside its source.");
18
+ return cue.sourceStart + (cue.loop ? local % available : Math.min(local, Math.max(0, available - .000001)));
19
+ }
20
+ /** Await seek and decoder readiness; callers upload their Pixi video texture afterwards. */
21
+ export async function seekVideo(video: HTMLVideoElement, seconds: number) {
22
+ if (!Number.isFinite(seconds) || seconds < 0)
23
+ throw new Error("Invalid video time.");
24
+ if (video.readyState < 1)
25
+ await new Promise<void>((resolve, reject) => {
26
+ const timeout = setTimeout(() => { cleanup(); reject(new Error("Video metadata timed out.")); }, 15000);
27
+ const cleanup = () => { clearTimeout(timeout); video.removeEventListener("loadedmetadata", done); video.removeEventListener("error", fail); };
28
+ const done = () => { cleanup(); resolve(); };
29
+ const fail = () => { cleanup(); reject(new Error("Video metadata failed.")); };
30
+ video.addEventListener("loadedmetadata", done, { once: true });
31
+ video.addEventListener("error", fail, { once: true });
32
+ });
33
+ video.pause();
34
+ if (Math.abs(video.currentTime - seconds) > .0001)
35
+ await new Promise<void>((resolve, reject) => {
36
+ const timeout = setTimeout(() => { cleanup(); reject(new Error("Video seek timed out.")); }, 15000);
37
+ const cleanup = () => { clearTimeout(timeout); video.removeEventListener("seeked", done); video.removeEventListener("error", fail); };
38
+ const done = () => { cleanup(); resolve(); };
39
+ const fail = () => { cleanup(); reject(new Error("Video seek failed.")); };
40
+ video.addEventListener("seeked", done, { once: true });
41
+ video.addEventListener("error", fail, { once: true });
42
+ video.currentTime = seconds;
43
+ });
44
+ if (video.readyState < 2)
45
+ throw new Error("The requested video frame is not decoded.");
46
+ }
@@ -0,0 +1,60 @@
1
+ export type Gradient = {
2
+ from: [
3
+ number,
4
+ number
5
+ ];
6
+ to: [
7
+ number,
8
+ number
9
+ ];
10
+ stops: [
11
+ number,
12
+ string
13
+ ][];
14
+ };
15
+ export type Paint = string | Gradient;
16
+ export type Transform = {
17
+ x?: number;
18
+ y?: number;
19
+ rotation?: number;
20
+ scaleX?: number;
21
+ scaleY?: number;
22
+ };
23
+ export type StudyNode = {
24
+ id: string;
25
+ kind: "rect" | "ellipse" | "path" | "text";
26
+ x: number;
27
+ y: number;
28
+ width?: number;
29
+ height?: number;
30
+ radius?: number;
31
+ d?: string;
32
+ text?: string;
33
+ size?: number;
34
+ font?: "sans" | "serif" | "mono";
35
+ weight?: "400" | "700";
36
+ align?: "left" | "center" | "right";
37
+ fill?: Paint;
38
+ stroke?: string;
39
+ strokeWidth?: number;
40
+ opacity?: number;
41
+ transform?: Transform;
42
+ clip?: {
43
+ x: number;
44
+ y: number;
45
+ width: number;
46
+ height: number;
47
+ };
48
+ resource?: string;
49
+ };
50
+ export type StudyFrame = {
51
+ nodes: StudyNode[];
52
+ cues: {
53
+ id: string;
54
+ time: number;
55
+ label: string;
56
+ }[];
57
+ };
58
+ export const STUDY_WIDTH = 960;
59
+ export const STUDY_HEIGHT = 540;
60
+ export const STUDY_DURATION = 8;
@@ -0,0 +1,121 @@
1
+ import { Container, FillGradient, Graphics, Matrix, Text } from "pixi.js";
2
+ import type { Gradient, Paint, StudyFrame, StudyNode } from "./model";
3
+ type Slot = {
4
+ root: Container;
5
+ shape: Graphics;
6
+ text: Text;
7
+ mask: Graphics;
8
+ paintKey?: string;
9
+ gradient?: FillGradient;
10
+ };
11
+ /** Retained Pixi objects consume the same geometry used by the gallery studies. */
12
+ export class StudyRenderer {
13
+ readonly stage = new Container();
14
+ private readonly slots: Slot[] = [];
15
+ private readonly matrix = new Matrix();
16
+ private textBounds: {
17
+ id: string;
18
+ text: string;
19
+ x: number;
20
+ y: number;
21
+ width: number;
22
+ height: number;
23
+ }[] = [];
24
+ constructor(parent: Container) { parent.addChild(this.stage); }
25
+ private slot(index: number) {
26
+ if (!this.slots[index]) {
27
+ const root = new Container(), shape = new Graphics(), mask = new Graphics();
28
+ const text = new Text({ text: "", style: { fontFamily: "Arial", fontSize: 24, fill: "#20302d", fontWeight: "700" } });
29
+ root.addChild(shape, text, mask);
30
+ this.stage.addChild(root);
31
+ this.slots[index] = { root, shape, text, mask };
32
+ }
33
+ return this.slots[index];
34
+ }
35
+ private paint(slot: Slot, paint: Paint) {
36
+ if (typeof paint === "string")
37
+ return paint;
38
+ const key = JSON.stringify(paint);
39
+ if (slot.paintKey !== key) {
40
+ slot.gradient?.destroy();
41
+ slot.gradient = new FillGradient({ type: "linear", textureSpace: "global", start: { x: paint.from[0], y: paint.from[1] }, end: { x: paint.to[0], y: paint.to[1] }, colorStops: paint.stops.map(([offset, color]) => ({ offset, color })) });
42
+ slot.paintKey = key;
43
+ }
44
+ return slot.gradient!;
45
+ }
46
+ render(frame: StudyFrame) {
47
+ this.textBounds = [];
48
+ for (let i = 0; i < frame.nodes.length; i++) {
49
+ const node = frame.nodes[i], slot = this.slot(i), { root, shape, text, mask } = slot;
50
+ root.visible = (node.opacity ?? 1) > .0001;
51
+ root.alpha = node.opacity ?? 1;
52
+ const tr = node.transform ?? {}, r = tr.rotation ?? 0, sx = tr.scaleX ?? 1, sy = tr.scaleY ?? 1;
53
+ this.matrix.set(Math.cos(r) * sx, Math.sin(r) * sx, -Math.sin(r) * sy, Math.cos(r) * sy, tr.x ?? 0, tr.y ?? 0);
54
+ root.setFromMatrix(this.matrix);
55
+ root.mask = null;
56
+ mask.clear();
57
+ if (node.clip) {
58
+ const c = node.clip;
59
+ mask.rect(c.x, c.y, c.width, c.height).fill("#ffffff");
60
+ root.mask = mask;
61
+ }
62
+ shape.visible = node.kind !== "text";
63
+ text.visible = node.kind === "text";
64
+ if (node.kind === "text") {
65
+ text.text = node.text ?? "";
66
+ text.style.fontFamily = node.font === "mono" ? "Courier New, monospace" : node.font === "serif" ? "Georgia, serif" : "Arial, sans-serif";
67
+ text.style.fontSize = node.size ?? 24;
68
+ text.style.fontWeight = node.weight ?? "700";
69
+ text.style.fill = typeof node.fill === "string" ? node.fill : "#20302d";
70
+ text.anchor.set(node.align === "center" ? .5 : node.align === "right" ? 1 : 0, 0);
71
+ text.position.set(node.x, node.y);
72
+ if (root.visible && root.alpha >= .95) {
73
+ const b = text.getBounds();
74
+ this.textBounds.push({ id: node.id, text: text.text, x: b.x, y: b.y, width: b.width, height: b.height });
75
+ }
76
+ }
77
+ else {
78
+ shape.clear();
79
+ if (node.kind === "rect")
80
+ shape.roundRect(node.x, node.y, node.width ?? 0, node.height ?? 0, node.radius ?? 0);
81
+ if (node.kind === "ellipse")
82
+ shape.ellipse(node.x, node.y, node.width ?? 0, node.height ?? 0);
83
+ if (node.kind === "path")
84
+ drawPath(shape, node.d ?? "M0,0");
85
+ if (node.fill && node.fill !== "none")
86
+ shape.fill(this.paint(slot, node.fill));
87
+ if (node.stroke)
88
+ shape.stroke({ color: node.stroke, width: node.strokeWidth ?? 2, cap: "round", join: "round" });
89
+ }
90
+ }
91
+ for (let i = frame.nodes.length; i < this.slots.length; i++)
92
+ this.slots[i].root.visible = false;
93
+ }
94
+ get texts() { return this.textBounds; }
95
+ destroy() {
96
+ for (const slot of this.slots)
97
+ slot.gradient?.destroy();
98
+ this.stage.destroy({ children: true });
99
+ this.slots.length = 0;
100
+ }
101
+ }
102
+ function drawPath(g: Graphics, d: string) {
103
+ const tokens = d.match(/[MLCQZ]|-?\d+(?:\.\d+)?(?:e[+-]?\d+)?/gi) ?? [];
104
+ let i = 0;
105
+ const n = () => Number(tokens[i++]);
106
+ while (i < tokens.length) {
107
+ const command = tokens[i++];
108
+ if (command === "M")
109
+ g.moveTo(n(), n());
110
+ else if (command === "L")
111
+ g.lineTo(n(), n());
112
+ else if (command === "C")
113
+ g.bezierCurveTo(n(), n(), n(), n(), n(), n());
114
+ else if (command === "Q")
115
+ g.quadraticCurveTo(n(), n(), n(), n());
116
+ else if (command === "Z")
117
+ g.closePath();
118
+ else
119
+ throw new Error(`Unsupported study path command: ${command}`);
120
+ }
121
+ }
@@ -0,0 +1,410 @@
1
+ import copy from "../../shared/locales/en/style-studies.json";
2
+ import { clamp, ease, hash, lerp, progress, sampled, spring, strokePoints, type Point } from "./timing";
3
+ import { type Paint, type StudyNode, type StudyFrame, STUDY_DURATION } from "./model";
4
+ export const STUDY_IDS = Object.keys(copy.styles) as (keyof typeof copy.styles)[];
5
+ export type StudyId = keyof typeof copy.styles;
6
+ /** Original geometry and choreography; source inspirations are recorded in skill references. */
7
+ class Drawing {
8
+ nodes: StudyNode[] = [];
9
+ private count = 0;
10
+ constructor(readonly lines: readonly string[]) { }
11
+ add(node: Omit<StudyNode, "id">) { const result = { ...node, id: `n${this.count++}` }; this.nodes.push(result); return result; }
12
+ rect(x: number, y: number, w: number, h: number, fill: Paint, radius = 0, opacity = 1) {
13
+ return this.add({ kind: "rect", x, y, width: Math.max(0, w), height: Math.max(0, h), radius, fill, opacity });
14
+ }
15
+ ellipse(x: number, y: number, rx: number, ry: number, fill: Paint, opacity = 1) {
16
+ return this.add({ kind: "ellipse", x, y, width: rx, height: ry, fill, opacity });
17
+ }
18
+ path(d: string, fill = "none", stroke?: string, strokeWidth = 2, opacity = 1) {
19
+ return this.add({ kind: "path", x: 0, y: 0, d, fill, stroke, strokeWidth, opacity });
20
+ }
21
+ poly(points: readonly Point[], fill: Paint, stroke?: string, width = 2) {
22
+ const node = this.path(`M${points.map((p) => p.join(",")).join(" L")} Z`, "none", stroke, width);
23
+ node.fill = fill;
24
+ return node;
25
+ }
26
+ line(points: readonly Point[], color: string, width = 3, amount = 1) {
27
+ const p = strokePoints(points, amount);
28
+ return this.path(p.length ? `M${p.map((point) => point.join(",")).join(" L")}` : "M0,0", "none", color, width);
29
+ }
30
+ text(value: string, x: number, y: number, size = 24, fill = "#20302d", align: StudyNode["align"] = "left", font: StudyNode["font"] = "sans") {
31
+ return this.add({ kind: "text", x, y, size, text: value, fill, align, font, weight: "700", resource: "typography" });
32
+ }
33
+ title(color = "#26362e") { this.text(this.lines[0], 54, 55, 30, color); }
34
+ footer(color = "#6b786a") { this.text(this.lines[1], 54, 482, 13, color, "left", "mono"); }
35
+ grain(color: string, opacity = 0.08, count = 200) {
36
+ for (let i = 0; i < count; i++)
37
+ this.ellipse(hash(i + 2) * 960, hash(i + 905) * 540, 0.7 + hash(i) * 1.4, 0.7, color, opacity);
38
+ }
39
+ background(fill: Paint) { this.rect(0, 0, 960, 540, fill).resource = "material"; }
40
+ }
41
+ const gradient = (a: string, b: string, x = 0, y = 0, w = 960, h = 540): Paint => ({ from: [x, y], to: [w, h], stops: [[0, a], [1, b]] });
42
+ const ring = (d: Drawing, x: number, y: number, r: number, color: string, width = 2, amount = 1) => {
43
+ const p: Point[] = Array.from({ length: 81 }, (_, i) => [x + Math.cos(i / 80 * Math.PI * 2) * r, y + Math.sin(i / 80 * Math.PI * 2) * r]);
44
+ d.line(p, color, width, amount);
45
+ };
46
+ function landscape(d: Drawing, x: number, y: number, w: number, h: number, hue = 0) {
47
+ const colors = [["#b8d7d6", "#729e98", "#315e61"], ["#e8c49c", "#bd8265", "#76554b"], ["#c8bbd7", "#8a88b1", "#585777"]][hue % 3];
48
+ d.rect(x, y, w, h, colors[0]);
49
+ d.ellipse(x + w * .75, y + h * .24, h * .1, h * .1, "#f8ecd0");
50
+ d.poly([[x, y + h * .8], [x + w * .26, y + h * .3], [x + w * .65, y + h * .8], [x + w * .8, y + h * .45], [x + w, y + h * .85], [x + w, y + h], [x, y + h]], colors[1]);
51
+ d.poly([[x, y + h * .9], [x + w * .5, y + h * .62], [x + w, y + h * .94], [x + w, y + h], [x, y + h]], colors[2]);
52
+ }
53
+ function block(d: Drawing, x: number, y: number, w: number, h: number, depth: number, color: string, top = "#e6dcc8", side = "#536f63") {
54
+ d.poly([[x, y], [x + w, y], [x + w + depth, y - depth * .55], [x + depth, y - depth * .55]], top);
55
+ d.poly([[x + w, y], [x + w + depth, y - depth * .55], [x + w + depth, y + h - depth * .55], [x + w, y + h]], side);
56
+ d.rect(x, y, w, h, color);
57
+ }
58
+ const scenes: Record<StudyId, (d: Drawing, t: number) => void> = {
59
+ "dark-tech-stage": (d, t) => {
60
+ d.background(gradient("#122423", "#061110"));
61
+ d.title("#ecf0e5");
62
+ d.footer("#91b9a6");
63
+ for (let i = 0; i < 10; i++)
64
+ d.line([[70 + i * 92, 110], [70 + i * 92, 433]], "#24403a", 1);
65
+ for (let i = 0; i < 12; i++) {
66
+ const p = progress(t, .15 + i * .12, .9), settle = progress(t, 2.4, 1.5);
67
+ const x = lerp(480 + Math.sin(i * 2.1) * 240, 110 + (i % 4) * 190, settle), y = lerp(245 + Math.cos(i * 1.7) * 100, 125 + Math.floor(i / 4) * 94, settle);
68
+ d.rect(x, y + 45 * (1 - p), 164, 74, "#213e36", 9, p);
69
+ d.rect(x + 12, y + 14, 26, 5, "#a7d08f", 2, p);
70
+ d.rect(x + 12, y + 31, 115, 4, "#698b7c", 2, p);
71
+ d.rect(x + 12, y + 46, 70, 4, "#45665c", 2, p);
72
+ }
73
+ const focus = progress(t, 4.2);
74
+ d.rect(87, 112, 754, 303, "#9ed699", 12, .07 * focus);
75
+ d.text(d.lines[2], 480, 450, 20, "#cae4bf", "center");
76
+ },
77
+ "studio-product-reveal": (d, t) => {
78
+ d.background(gradient("#14232c", "#040a12"));
79
+ d.title("#e5ecf1");
80
+ d.footer("#849db2");
81
+ d.ellipse(480, 409, 174, 23, "#2c4d62", .5);
82
+ const split = progress(t, 2.4) - progress(t, 5.1, .7), rise = progress(t, 0, 1.2);
83
+ d.rect(397, 171 - 65 * split + 80 * (1 - rise), 166, 47, gradient("#dde9ec", "#637f94", 397, 171, 563, 218), 17);
84
+ d.rect(414, 207 + 25 * split, 132, 184, gradient("#7996a7", "#1c3449", 414, 207, 546, 391), 30);
85
+ d.rect(438, 227 + 25 * split, 84, 140, gradient("#3a657a", "#102538", 438, 227, 522, 367), 15);
86
+ const sweep = 405 + ((t * .24) % 1) * 130;
87
+ d.rect(sweep, 224 + 25 * split, 9, 134, "#d7f3ef", 4, .35);
88
+ d.line([[414, 345], [414, 232], [431, 214]], "#b1d0d7", 2);
89
+ d.text(d.lines[2], 680, 258, 19, "#b9d5df");
90
+ d.line([[654, 277], [578, 277], [550, 301]], "#628fa4", 1);
91
+ },
92
+ "risograph-print": (d, t) => {
93
+ d.background("#f5efdd");
94
+ d.title();
95
+ d.footer();
96
+ const p = progress(t, .3, 1.8), q = progress(t, 1.7, 1.8), shift = 8 * Math.exp(-Math.max(0, t - 4) * 5) * Math.sin(t * 9);
97
+ d.poly([[80, 415], [242, 168], [417, 415]], "#168c9e").opacity = p;
98
+ d.ellipse(560 + shift, 262, 155, 155, "#ed8159", q);
99
+ d.poly([[405, 415], [542, 189], [720, 415]], "#984f64").opacity = Math.min(p, q);
100
+ for (let y = 170; y < 395; y += 13)
101
+ for (let x = 99; x < 773; x += 13) {
102
+ if ((x - 560) ** 2 + (y - 262) ** 2 < 155 ** 2)
103
+ d.ellipse(x + shift, y, 1.3, 1.3, "#f5efdd", .34 * q);
104
+ }
105
+ d.grain("#5d5146", .07, 160);
106
+ d.text(d.lines[2], 795, 291, 16, "#514451", "center");
107
+ },
108
+ "halftone-editorial": (d, t) => {
109
+ d.background("#f0e5c7");
110
+ d.title();
111
+ d.footer();
112
+ d.rect(48, 107, 865, 340, "#273637");
113
+ d.rect(65, 122, 548, 306, "#ebcc80");
114
+ const p = progress(t, .2, 1);
115
+ for (let y = 150; y < 402; y += 15)
116
+ for (let x = 82; x < 594; x += 15)
117
+ d.ellipse(x, y, Math.max(.5, 3.4 * (x - 82) / 512), Math.max(.5, 3.4 * (x - 82) / 512), "#493b37", .55);
118
+ d.text("01", ninety(t), 170 + 75 * (1 - p), 124, "#273637");
119
+ d.text(d.lines[2], 87, 345, 22, "#273637");
120
+ const stamp = spring(t - 3, 1.8, 1, 11);
121
+ const n = d.rect(-104, -43, 208, 86, "#da6553", 3, progress(t, 3, .15));
122
+ n.transform = { x: 748, y: 285, rotation: -.12, scaleX: stamp, scaleY: stamp };
123
+ const label = d.text("PRINT", 0, -17, 30, "#f8eed3", "center");
124
+ label.transform = n.transform;
125
+ label.opacity = n.opacity;
126
+ },
127
+ "silkscreen-poster": (d, t) => {
128
+ d.background("#f6e8c9");
129
+ d.title();
130
+ d.footer();
131
+ d.rect(258, 100, 446, 340, "#e8d9b8");
132
+ d.rect(275, 115, 410, 310, "#edb954").clip = { x: 275, y: 115, width: 410, height: 310 * clamp((t - .3) / 1.4) };
133
+ const mountain = d.poly([[277, 415], [407, 176], [479, 293], [531, 235], [683, 415]], "#3d7770");
134
+ mountain.clip = { x: 275, y: 115, width: 410 * clamp((t - 1.9) / 1.5), height: 310 };
135
+ const dark = d.poly([[276, 415], [407, 176], [429, 334], [500, 416]], "#254d4c");
136
+ dark.clip = mountain.clip;
137
+ d.ellipse(581, 164, 29, 29, "#e68149", progress(t, 3.5, .3));
138
+ d.text(d.lines[2], 480, 383, 19, "#f4e5c3", "center").opacity = progress(t, 4.3);
139
+ const pull = 115 + 310 * clamp((t - .3) / 1.4);
140
+ d.rect(266, pull - 4, 429, 8, "#273e37", 2, 1 - progress(t, 1.7, .2));
141
+ d.grain("#614d35", .065, 180);
142
+ },
143
+ "art-deco-motion": (d, t) => {
144
+ d.background("#17241d");
145
+ d.title("#edd3a0");
146
+ d.footer("#b89b6b");
147
+ const open = progress(t, .4, 1.8);
148
+ const gold = "#d9b96e";
149
+ for (let i = 0; i < 17; i++) {
150
+ const a = Math.PI + i / 16 * Math.PI;
151
+ d.line([[480, 410], [480 + Math.cos(a) * 340 * open, 410 + Math.sin(a) * 290 * open]], gold, 2);
152
+ }
153
+ for (let i = 0; i < 5; i++)
154
+ d.rect(325 + i * 21, 156 + i * 21, 310 - i * 42, 247 - i * 21, "#1e3127", 0, progress(t, 1 + i * .12));
155
+ for (let i = 0; i < 5; i++)
156
+ d.line([[325 + i * 21, 403], [325 + i * 21, 156 + i * 21], [635 - i * 21, 156 + i * 21], [635 - i * 21, 403]], gold, 2);
157
+ d.ellipse(480, 300, 54, 54, gradient("#695a34", "#e8d492", 426, 245, 534, 355));
158
+ d.line([[480, 300], [480 + Math.sin(t * .6) * 35, 300 - Math.cos(t * .6) * 35]], "#25372b", 3);
159
+ d.text(d.lines[2], 480, 427, 20, "#ebd5a6", "center", "serif");
160
+ },
161
+ "blueprint-drafting": (d, t) => {
162
+ d.background("#17476b");
163
+ d.title("#e9eadd");
164
+ d.footer("#a5c3d2");
165
+ for (let i = 0; i < 23; i++)
166
+ d.line([[40 + i * 40, 97], [40 + i * 40, 454]], "#275678", 1);
167
+ for (let i = 0; i < 9; i++)
168
+ d.line([[40, 97 + i * 40], [920, 97 + i * 40]], "#275678", 1);
169
+ const parts = [[[262, 349], [262, 226], [376, 184], [579, 184], [676, 229], [676, 349], [262, 349]], [[306, 307], [306, 246], [599, 246], [599, 307], [306, 307]], [[340, 377], [340, 349]], [[601, 377], [601, 349]]] as Point[][];
170
+ parts.forEach((p, i) => d.line(p, "#d3e8e9", i === 0 ? 3 : 2, progress(t, .2 + i * .6, 1.1)));
171
+ const offset = 56 * (progress(t, 3.3) - progress(t, 5.8));
172
+ d.line([[304, 222 - offset], [599, 222 - offset]], "#b4d7e2", 4, progress(t, 2));
173
+ d.line([[696, 184], [712, 184], [712, 349], [696, 349]], "#8eb7c9", 1, progress(t, 2.6));
174
+ d.text(d.lines[2], 726, 253, 14, "#c6dfeb", "left", "mono").opacity = progress(t, 2.6);
175
+ d.rect(740, 359, 148, 62, "#123b5a");
176
+ d.text("FIG. 01", 755, 379, 18, "#dbedf0", "left", "mono");
177
+ },
178
+ "holographic-scan": (d, t) => {
179
+ d.background(gradient("#07262c", "#030d18"));
180
+ d.title("#c7ece6");
181
+ d.footer("#73b9b5");
182
+ const color = "#6be3dc", scan = 400 - 300 * clamp(t / 2.7);
183
+ d.ellipse(480, 417, 230, 17, "#173c44");
184
+ for (let i = 0; i < 7; i++)
185
+ d.line([[283 + i * 56, 417], [480, 389]], "#31696b", 1);
186
+ const yaw = .2 * Math.sin(t * .65), explode = progress(t, 3.6) - progress(t, 6.2);
187
+ const project = (x: number, y: number, z: number): Point => [480 + x * Math.cos(yaw) + z * Math.sin(yaw), 286 + y + z * .32];
188
+ for (let part = 0; part < 3; part++) {
189
+ const cx = (part - 1) * 99 * (1 + explode * .6), w = part === 1 ? 125 : 75, h = part === 1 ? 146 : 104;
190
+ const v = [project(cx - w / 2, -h / 2, -30), project(cx + w / 2, -h / 2, -30), project(cx + w / 2, h / 2, -30), project(cx - w / 2, h / 2, -30), project(cx - w / 2, -h / 2, 30), project(cx + w / 2, -h / 2, 30), project(cx + w / 2, h / 2, 30), project(cx - w / 2, h / 2, 30)];
191
+ for (const [a, b] of [[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4], [0, 4], [1, 5], [2, 6], [3, 7]])
192
+ d.line([v[a], v[b]], color, 2, clamp((Math.max(v[a][1], v[b][1]) - scan) / 20));
193
+ }
194
+ d.line([[244, scan], [717, scan]], "#b5ffff", 2, 1 - progress(t, 2.8, .2));
195
+ d.line([[600, 249], [694, 181], [811, 181]], color, 1, progress(t, 3));
196
+ d.text(d.lines[2], 704, 150, 16, "#b5ece7").opacity = progress(t, 3);
197
+ },
198
+ "paper-popup": (d, t) => {
199
+ d.background("#e7dfcd");
200
+ d.title();
201
+ d.footer();
202
+ d.poly([[177, 363], [488, 433], [805, 358], [488, 272]], "#c9bfa8");
203
+ d.poly([[180, 346], [488, 413], [796, 343], [488, 267]], "#f5edda", "#b7ac95", 2);
204
+ const fold = progress(t, .3, 1.6), angle = lerp(.05, 1, fold);
205
+ for (let i = 0; i < 5; i++) {
206
+ const p = progress(t, .6 + i * .22), x = 295 + i * 92, y = 350 - i * 7;
207
+ d.ellipse(x, y + 4, 44, 10, "#aea590", .2 * p);
208
+ const n = d.poly([[-31, 0], [-31, -80 * angle * p], [0, -111 * angle * p], [31, -80 * angle * p], [31, 0]], i % 2 ? "#dba763" : "#749987", "#fbf5e5", 5);
209
+ n.transform = { x, y, rotation: .08 * Math.sin(t * .8 + i) };
210
+ d.line([[x - 24, y], [x + 24, y]], "#c9bca0", 2);
211
+ }
212
+ d.text(d.lines[2], 480, 447, 20, "#5b6652", "center");
213
+ d.grain("#70644e", .06, 130);
214
+ },
215
+ "shadow-theatre": (d, t) => {
216
+ d.background(gradient("#e8c488", "#c08e62"));
217
+ d.title("#503f33");
218
+ d.footer("#665145");
219
+ d.rect(70, 104, 820, 336, "#ecce99", 8);
220
+ d.rect(88, 121, 784, 303, "#f5ddb1");
221
+ d.grain("#a0794d", .08, 180);
222
+ d.poly([[92, 425], [242, 370], [365, 389], [551, 333], [716, 382], [872, 416]], "#7c6448");
223
+ const x = 365 + Math.sin(t * .5) * 27, y = 301;
224
+ d.ellipse(x, y - 61, 28, 28, "#45382f");
225
+ d.poly([[x - 20, y - 37], [x + 20, y - 37], [x + 31, y + 44], [x - 32, y + 44]], "#45382f");
226
+ const hand: Point = [x + 70, y - 35 + Math.sin(t * 1.1) * 23];
227
+ d.line([[x + 14, y - 30], [x + 43, y - 13], hand], "#45382f", 13);
228
+ d.line([[x - 14, y + 43], [x - 31, y + 94]], "#45382f", 12);
229
+ d.line([[x + 14, y + 43], [x + 34, y + 94]], "#45382f", 12);
230
+ d.poly([[587, 404], [576, 252], [587, 150], [600, 253], [611, 404]], "#4e4333");
231
+ for (let i = 0; i < 5; i++)
232
+ d.ellipse(590 + Math.sin(i * 2.1) * 53, 220 + i * 15, 66, 35, "#5e4e37");
233
+ for (let i = 0; i < 16; i++)
234
+ d.ellipse(640 + hash(i) * 128, 220 + hash(i + 30) * 113, 3, 3, "#f7e4b7");
235
+ d.text(d.lines[2], 660, 388, 17, "#f8e8c9", "center");
236
+ },
237
+ "engraved-relief-print": (d, t) => {
238
+ d.background("#efe4c8");
239
+ d.title();
240
+ d.footer();
241
+ d.rect(205, 104, 550, 336, "#e1d5b7");
242
+ d.rect(214, 114, 531, 315, "#f7efd9");
243
+ const outer: Point[] = Array.from({ length: 101 }, (_, i) => { const a = i / 100 * Math.PI * 2; return [480 + Math.cos(a) * 155 * (1 + .06 * Math.sin(a * 6)), 272 + Math.sin(a) * 118]; });
244
+ d.line(outer, "#493f36", 2.5, progress(t, .2, 2));
245
+ for (let i = 0; i < 57; i++) {
246
+ const y = 170 + i * 3.6, q = (y - 272) / 117, x = Math.sqrt(Math.max(0, 1 - q * q)) * 153;
247
+ const p = progress(t, 1.5 + i * .045, .9);
248
+ d.line([[480 - x, y], [480 + x * .65, y + Math.sin(i * .2) * 9]], "#685346", .8, p);
249
+ }
250
+ for (let i = 0; i < 23; i++)
251
+ d.line([[378 + i * 8, 197], [395 + i * 7, 357]], "#765d49", .7, progress(t, 4 + i * .03));
252
+ d.text(d.lines[2], 480, 396, 18, "#4e4436", "center", "serif");
253
+ d.grain("#6b5943", .075, 170);
254
+ },
255
+ "ascii-crt": (d, t) => {
256
+ d.background("#111c16");
257
+ d.title("#d5c482");
258
+ d.footer("#a79457");
259
+ const glyphs = " .:-=+*#@", on = progress(t, .1, .7);
260
+ for (let row = 0; row < 22; row++)
261
+ for (let col = 0; col < 60; col++) {
262
+ const x = 180 + col * 10, y = 135 + row * 12, r = Math.hypot((x - 480) / 1.3, y - 262);
263
+ const density = clamp((1 - Math.abs(r - (82 + 18 * Math.sin(t * .8))) / 33) * on);
264
+ d.text(glyphs[Math.floor(density * (glyphs.length - 1))], x, y, 13, "#dcb46b", "left", "mono").opacity = .2 + .8 * density;
265
+ }
266
+ const text = d.lines[2].slice(0, Math.floor(Math.max(0, t - 1) * 14));
267
+ d.text(text, 480, 428, 20, "#e5c580", "center", "mono");
268
+ for (let y = 108; y < 443; y += 5)
269
+ d.rect(114, y, 731, 1, "#030803", 0, .18);
270
+ d.rect(118, 105, 725, 337, "#d8bb62", 20, .025);
271
+ },
272
+ "analog-film-tape": (d, t) => {
273
+ d.background("#302c28");
274
+ d.title("#e2d7c0");
275
+ d.footer("#c5b99f");
276
+ const st = sampled(t, 12), jitter = hash(Math.floor(t * 24)) * 2 - 1;
277
+ d.rect(158 + jitter, 103, 641, 338, "#c6baa0");
278
+ d.poly([[160, 413], [275, 265], [397, 345], [564, 182], [797, 414]], "#716c60");
279
+ d.ellipse(611, 166, 35, 35, "#e7dbc0");
280
+ d.rect(320 + Math.sin(st * .9) * 65, 245, 68, 122, "#3f443c", 8);
281
+ d.ellipse(354 + Math.sin(st * .9) * 65, 234, 25, 25, "#3f443c");
282
+ for (let i = 0; i < 70; i++)
283
+ d.ellipse(160 + hash(i + Math.floor(t * 24)) * 638, 103 + hash(i + 200) * 334, 1.1, .6, "#efeadb", .12);
284
+ for (let i = 0; i < 3; i++)
285
+ d.line([[227 + i * 201 + hash(Math.floor(t * 8)) * 8, 105], [229 + i * 201, 437]], "#e2d6bb", .8);
286
+ d.rect(165, 413, 626, 28, "#1e211d", 0, .83);
287
+ d.text(d.lines[2], 480, 417, 14, "#ece0c7", "center", "serif");
288
+ },
289
+ "glass-mosaic": (d, t) => {
290
+ d.background("#172c31");
291
+ d.title("#dce4d3");
292
+ d.footer("#b8c9bc");
293
+ const colors = ["#2d888b", "#7da883", "#ddb165", "#d27856", "#54758c"];
294
+ for (let row = 0; row < 5; row++)
295
+ for (let col = 0; col < 9; col++) {
296
+ const i = row * 9 + col, p = progress(t, hash(i) * 1.8, 1.1), x = 202 + col * 62, y = 118 + row * 60;
297
+ const n = d.poly([[0, 0], [57, 0], [57, 55], [0, 55]], colors[(row + col) % 5], "#10262b", 3);
298
+ n.transform = { x: x + ((col - 4) * 28) * (1 - p), y: y + ((row - 2) * 45) * (1 - p), rotation: (hash(i + 42) - .5) * (1 - p) };
299
+ n.opacity = p;
300
+ const light = .09 + .19 * Math.max(0, 1 - Math.abs(x - (170 + t * 81)) / 160);
301
+ d.rect(x + 6, y + 5, 42, 44, "#ffe8a0", 1, light * p);
302
+ }
303
+ d.text(d.lines[2], 480, 443, 18, "#c9dacb", "center");
304
+ },
305
+ "portal-zoom": (d, t) => {
306
+ d.background("#e3dfca");
307
+ d.title();
308
+ d.footer();
309
+ const segment = Math.floor(t / 4), u = (t / 4) % 1, scale = Math.exp(u * Math.log(830 / 200)), w = 200 * scale, h = w * 340 / 830;
310
+ landscape(d, 65, 106, 830, 340, segment);
311
+ const clip = { x: 65, y: 106, width: 830, height: 340 };
312
+ d.rect(480 - w / 2 - 12, 275 - h / 2 - 12, w + 24, h + 24, "#efe4c9", 14).clip = clip;
313
+ const before = d.nodes.length;
314
+ landscape(d, 480 - w / 2, 276 - h / 2, w, h, 1 + segment);
315
+ d.nodes.slice(before).forEach((n) => n.clip = clip);
316
+ const iw = w * 200 / 830, ih = h * 200 / 830;
317
+ d.rect(480 - iw / 2 - 7, 276 - ih / 2 - 7, iw + 14, ih + 14, "#eae5d4", 8).clip = clip;
318
+ const begin = d.nodes.length;
319
+ landscape(d, 480 - iw / 2, 276 - ih / 2, iw, ih, 2 + segment);
320
+ d.nodes.slice(begin).forEach((n) => n.clip = clip);
321
+ d.text(d.lines[2], 480, 451, 17, "#526351", "center");
322
+ },
323
+ "photo-footage-editorial": (d, t) => {
324
+ d.background("#eee9dc");
325
+ d.title();
326
+ d.footer();
327
+ const shift = progress(t, 2, 1.4) * 220;
328
+ for (let i = 0; i < 5; i++) {
329
+ const x = 57 + i * 204 - shift, y = 145 + Math.sin(i) * 9;
330
+ const clip = { x: 57, y: 112, width: 848, height: 328 };
331
+ d.rect(x - 7, y - 7, 188, 252, "#fcf8ee", 3).clip = clip;
332
+ const start = d.nodes.length;
333
+ landscape(d, x, y, 174, 193, i);
334
+ d.nodes.slice(start).forEach((n) => n.clip = clip);
335
+ d.rect(x + 12, y + 211, 97, 4, "#98a698", 2).clip = clip;
336
+ d.rect(x + 12, y + 223, 134, 3, "#c1c7b6", 2).clip = clip;
337
+ }
338
+ d.text(d.lines[2], 480, 434, 19, "#59675a", "center");
339
+ },
340
+ "stop-motion-objects": (d, t) => {
341
+ d.background("#ebe4d5");
342
+ d.title();
343
+ d.footer();
344
+ d.ellipse(480, 418, 259, 24, "#9c927a", .2);
345
+ const st = sampled(t, 10), colors = ["#cc875c", "#80997a", "#517877", "#dab47b"];
346
+ for (let i = 0; i < 7; i++) {
347
+ const p = progress(st, .3 + i * .4, .7), x = 338 + (i % 3) * 92, y = 365 - Math.floor(i / 3) * 61;
348
+ block(d, x, y - 95 * (1 - p), 82, 56, 17, colors[i % 4], "#e9cfa5", "#547264");
349
+ for (let j = 0; j < 3; j++)
350
+ d.ellipse(x + 18 + j * 22, y - 8 - 95 * (1 - p), 8, 4, "#ead2aa");
351
+ }
352
+ d.text(d.lines[2], 480, 453, 18, "#607059", "center");
353
+ },
354
+ "miniature-diorama": (d, t) => {
355
+ d.background(gradient("#bad5d0", "#e8e6d3"));
356
+ d.title();
357
+ d.footer();
358
+ d.ellipse(480, 417, 233, 28, "#668e88", .24);
359
+ d.poly([[218, 346], [480, 238], [747, 344], [480, 447]], "#83aaa0");
360
+ d.poly([[218, 346], [480, 447], [480, 466], [218, 365]], "#5d837b");
361
+ d.poly([[480, 447], [747, 344], [747, 363], [480, 466]], "#47796f");
362
+ for (let i = 0; i < 6; i++) {
363
+ const p = progress(t, .2 + i * .28), x = 330 + (i % 3) * 91, y = 314 + Math.floor(i / 3) * 68;
364
+ block(d, x, y - 60 * p, 59, 60 * p, 28, ["#d2b882", "#d19778", "#dfcba2"][i % 3], "#eee1c0", "#887a62");
365
+ if (p > .1)
366
+ d.rect(x + 23, y - 37 * p, 13, 23 * p, "#587c76", 2);
367
+ }
368
+ d.text(d.lines[2], 480, 483, 16, "#5b7367", "center");
369
+ },
370
+ "sand-drawing": (d, t) => {
371
+ d.background(gradient("#c4a36d", "#e1c38b"));
372
+ d.title("#654f30");
373
+ d.footer("#795e38");
374
+ const p = clamp(t / 3), sweep = progress(t, 4.4, 1.8);
375
+ for (let i = 0; i < 1400; i++) {
376
+ const x = 150 + hash(i) * 660, y = 124 + hash(i + 3100) * 303;
377
+ const silhouette = ((x - 480) / 176) ** 2 + ((y - 278) / 111) ** 2;
378
+ const aperture = ((x - 480) / 89) ** 2 + ((y - 278) / 57) ** 2;
379
+ const cut = silhouette < 1 && aperture > 1 && (x - 300) / 360 < p;
380
+ const swept = x < 150 + 660 * sweep && y > 275 + Math.sin(x * .014) * 58;
381
+ d.ellipse(x, y, .7 + hash(i + 22) * 1.7, .6 + hash(i + 44), cut && !swept ? "#5c4326" : "#af8a51", cut && !swept ? .94 : .27);
382
+ }
383
+ d.text(d.lines[2], 480, 443, 19, "#6f5734", "center");
384
+ },
385
+ "generative-light-signal": (d, t) => {
386
+ d.background("#101b25");
387
+ d.title("#d7e8db");
388
+ d.footer("#98b9b0");
389
+ for (let j = 0; j < 7; j++) {
390
+ const points: Point[] = Array.from({ length: 121 }, (_, i) => { const x = 76 + i / 120 * 808; return [x, 275 + Math.sin(i * .12 - t * 1.1 + j * .24) * (35 + j * 9) + Math.sin(i * .052 + t * .6) * 20]; });
391
+ d.line(points, ["#729997", "#84b8ac", "#bddcae"][j % 3], 1.3 + j * .16, progress(t, .15 + j * .08, 1.5));
392
+ }
393
+ for (let i = 0; i < 25; i++) {
394
+ const u = (hash(i) + t * .045) % 1, x = 76 + u * 808, y = 275 + Math.sin(u * 14 - t * 1.1) * 90;
395
+ d.ellipse(x, y, 3.1, 3.1, "#c9e7b6", .8);
396
+ }
397
+ d.text(d.lines[2], 480, 437, 20, "#c3d9c6", "center");
398
+ },
399
+ };
400
+ function ninety(t: number) { return 87 + 9 * (1 - progress(t, .2)); }
401
+ export function studyFrame(id: StudyId, seconds: number): StudyFrame {
402
+ if (!(id in scenes))
403
+ throw new Error(`Unknown study: ${id}`);
404
+ const d = new Drawing(copy.styles[id].lines);
405
+ const t = clamp(Number.isFinite(seconds) ? seconds : 0, 0, STUDY_DURATION - 0.000001);
406
+ scenes[id](d, t);
407
+ for (const node of d.nodes)
408
+ node.resource ??= "artwork";
409
+ return { nodes: d.nodes, cues: [{ id: "reveal", time: .3, label: d.lines[0] }, { id: "detail", time: 3, label: d.lines[2] }, { id: "hold", time: 6, label: d.lines[1] }] };
410
+ }