@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.
- package/NOTICE.md +256 -0
- package/README.md +241 -0
- package/dist/animaguru.js +27338 -0
- package/package.json +37 -0
- package/skills/animaguru/SKILL.md +115 -0
- package/skills/animaguru-advertising/SKILL.md +25 -0
- package/skills/animaguru-advertising/references/brand-bumper.md +11 -0
- package/skills/animaguru-advertising/references/comparison.md +11 -0
- package/skills/animaguru-advertising/references/creator-testimonial.md +11 -0
- package/skills/animaguru-advertising/references/editorial-advertorial.md +11 -0
- package/skills/animaguru-advertising/references/game-reward.md +11 -0
- package/skills/animaguru-advertising/references/offer-promotion.md +11 -0
- package/skills/animaguru-advertising/references/product-launch.md +11 -0
- package/skills/animaguru-advertising/references/product-proof.md +11 -0
- package/skills/animaguru-analog-film-tape/SKILL.md +29 -0
- package/skills/animaguru-analog-film-tape/references/recipe.md +23 -0
- package/skills/animaguru-animated-data-visualization/SKILL.md +39 -0
- package/skills/animaguru-animated-data-visualization/references/data-contract.md +16 -0
- package/skills/animaguru-animated-data-visualization/references/variants.md +17 -0
- package/skills/animaguru-art-deco-motion/SKILL.md +29 -0
- package/skills/animaguru-art-deco-motion/references/recipe.md +22 -0
- package/skills/animaguru-ascii-crt/SKILL.md +29 -0
- package/skills/animaguru-ascii-crt/references/recipe.md +22 -0
- package/skills/animaguru-blueprint-drafting/SKILL.md +29 -0
- package/skills/animaguru-blueprint-drafting/references/recipe.md +22 -0
- package/skills/animaguru-cartoon-character-animation/SKILL.md +35 -0
- package/skills/animaguru-cartoon-character-animation/references/variants.md +17 -0
- package/skills/animaguru-cutout-animation/SKILL.md +33 -0
- package/skills/animaguru-cutout-animation/references/production-recipe.md +37 -0
- package/skills/animaguru-cutout-animation/references/variants.md +17 -0
- package/skills/animaguru-cutout-animation/references/visual-style.md +34 -0
- package/skills/animaguru-dark-tech-stage/SKILL.md +29 -0
- package/skills/animaguru-dark-tech-stage/references/recipe.md +23 -0
- package/skills/animaguru-diagram-formula-motion/SKILL.md +36 -0
- package/skills/animaguru-diagram-formula-motion/references/variants.md +17 -0
- package/skills/animaguru-engraved-relief-print/SKILL.md +29 -0
- package/skills/animaguru-engraved-relief-print/references/recipe.md +24 -0
- package/skills/animaguru-flat-vector-motion/SKILL.md +35 -0
- package/skills/animaguru-flat-vector-motion/references/variants.md +17 -0
- package/skills/animaguru-general-video/SKILL.md +20 -0
- package/skills/animaguru-general-video/references/ambient-loop.md +11 -0
- package/skills/animaguru-general-video/references/music-lyrics.md +11 -0
- package/skills/animaguru-general-video/references/short-film.md +11 -0
- package/skills/animaguru-general-video/references/titles-event.md +11 -0
- package/skills/animaguru-general-video/references/visual-essay.md +11 -0
- package/skills/animaguru-generative-light-signal/SKILL.md +29 -0
- package/skills/animaguru-generative-light-signal/references/recipe.md +23 -0
- package/skills/animaguru-glass-mosaic/SKILL.md +29 -0
- package/skills/animaguru-glass-mosaic/references/recipe.md +23 -0
- package/skills/animaguru-halftone-editorial/SKILL.md +29 -0
- package/skills/animaguru-halftone-editorial/references/recipe.md +23 -0
- package/skills/animaguru-holographic-scan/SKILL.md +29 -0
- package/skills/animaguru-holographic-scan/references/recipe.md +22 -0
- package/skills/animaguru-interface-motion/SKILL.md +36 -0
- package/skills/animaguru-interface-motion/references/screen-mapping.md +16 -0
- package/skills/animaguru-interface-motion/references/variants.md +17 -0
- package/skills/animaguru-isometric-worlds/SKILL.md +35 -0
- package/skills/animaguru-isometric-worlds/references/variants.md +17 -0
- package/skills/animaguru-kinetic-typography/SKILL.md +35 -0
- package/skills/animaguru-kinetic-typography/references/variants.md +17 -0
- package/skills/animaguru-miniature-diorama/SKILL.md +29 -0
- package/skills/animaguru-miniature-diorama/references/recipe.md +23 -0
- package/skills/animaguru-painterly-depth-animation/SKILL.md +35 -0
- package/skills/animaguru-painterly-depth-animation/references/variants.md +17 -0
- package/skills/animaguru-paper-popup/SKILL.md +29 -0
- package/skills/animaguru-paper-popup/references/recipe.md +22 -0
- package/skills/animaguru-photo-footage-editorial/SKILL.md +29 -0
- package/skills/animaguru-photo-footage-editorial/references/recipe.md +23 -0
- package/skills/animaguru-pixel-art-animation/SKILL.md +36 -0
- package/skills/animaguru-pixel-art-animation/references/variants.md +17 -0
- package/skills/animaguru-portal-zoom/SKILL.md +29 -0
- package/skills/animaguru-portal-zoom/references/recipe.md +21 -0
- package/skills/animaguru-risograph-print/SKILL.md +29 -0
- package/skills/animaguru-risograph-print/references/recipe.md +23 -0
- package/skills/animaguru-sand-drawing/SKILL.md +29 -0
- package/skills/animaguru-sand-drawing/references/recipe.md +21 -0
- package/skills/animaguru-shadow-theatre/SKILL.md +29 -0
- package/skills/animaguru-shadow-theatre/references/recipe.md +23 -0
- package/skills/animaguru-silkscreen-poster/SKILL.md +29 -0
- package/skills/animaguru-silkscreen-poster/references/recipe.md +22 -0
- package/skills/animaguru-stop-motion-objects/SKILL.md +29 -0
- package/skills/animaguru-stop-motion-objects/references/recipe.md +23 -0
- package/skills/animaguru-studio-product-reveal/SKILL.md +29 -0
- package/skills/animaguru-studio-product-reveal/references/recipe.md +23 -0
- package/skills/animaguru-video/SKILL.md +79 -0
- package/skills/animaguru-whiteboard-line-drawing/SKILL.md +34 -0
- package/skills/animaguru-whiteboard-line-drawing/references/variants.md +17 -0
- package/templates/video/AGENTS.md +34 -0
- package/templates/video/CLAUDE.md +19 -0
- package/templates/video/README.md +101 -0
- package/templates/video/VIDEO-STYLE-NOTICES.txt +54 -0
- package/templates/video/film.config.json +10 -0
- package/templates/video/gitignore +11 -0
- package/templates/video/index.html +14 -0
- package/templates/video/package.json +28 -0
- package/templates/video/public/brand/logo-mark.svg +5 -0
- package/templates/video/scripts/build.mjs +12 -0
- package/templates/video/scripts/export-html.mjs +12 -0
- package/templates/video/scripts/export-mp4.mjs +209 -0
- package/templates/video/scripts/html-export-plugin.mjs +35 -0
- package/templates/video/scripts/html-export.mjs +54 -0
- package/templates/video/scripts/install-example.mjs +32 -0
- package/templates/video/scripts/review-analysis.mjs +29 -0
- package/templates/video/scripts/review-audio.mjs +24 -0
- package/templates/video/scripts/review.mjs +37 -0
- package/templates/video/scripts/starter-film.sha256 +1 -0
- package/templates/video/shared/locales/en/export.json +10 -0
- package/templates/video/shared/locales/en/film.json +42 -0
- package/templates/video/shared/locales/en/player.json +31 -0
- package/templates/video/shared/locales/en/style-studies.json +168 -0
- package/templates/video/shared/locales/en/timeline.json +33 -0
- package/templates/video/src/assets.ts +4 -0
- package/templates/video/src/download.ts +27 -0
- package/templates/video/src/exporter.ts +70 -0
- package/templates/video/src/film-resources.ts +29 -0
- package/templates/video/src/film.ts +132 -0
- package/templates/video/src/main.ts +219 -0
- package/templates/video/src/resource-timeline.ts +193 -0
- package/templates/video/src/review.ts +50 -0
- package/templates/video/src/studies/film.ts +26 -0
- package/templates/video/src/studies/media.ts +46 -0
- package/templates/video/src/studies/model.ts +60 -0
- package/templates/video/src/studies/pixi-renderer.ts +121 -0
- package/templates/video/src/studies/scenes.ts +410 -0
- package/templates/video/src/studies/timing.ts +53 -0
- package/templates/video/src/studies/variants.ts +39 -0
- package/templates/video/src/style.css +111 -0
- package/templates/video/src/timeline-model.ts +55 -0
- package/templates/video/style-source-provenance.json +11 -0
- package/templates/video/tsconfig.json +15 -0
- package/templates/video/vite.config.mjs +9 -0
- package/vendor/pixijs-skills/LICENSE +21 -0
- package/vendor/pixijs-skills/provenance.json +79 -0
- package/vendor/pixijs-skills/skills/pixijs/SKILL.md +66 -0
- package/vendor/pixijs-skills/skills/pixijs/references/index.md +136 -0
- package/vendor/pixijs-skills/skills/pixijs-accessibility/SKILL.md +224 -0
- package/vendor/pixijs-skills/skills/pixijs-application/SKILL.md +293 -0
- package/vendor/pixijs-skills/skills/pixijs-application/references/application-options.md +314 -0
- package/vendor/pixijs-skills/skills/pixijs-assets/SKILL.md +303 -0
- package/vendor/pixijs-skills/skills/pixijs-assets/references/background.md +137 -0
- package/vendor/pixijs-skills/skills/pixijs-assets/references/bundles.md +122 -0
- package/vendor/pixijs-skills/skills/pixijs-assets/references/caching.md +143 -0
- package/vendor/pixijs-skills/skills/pixijs-assets/references/compressed-textures.md +120 -0
- package/vendor/pixijs-skills/skills/pixijs-assets/references/fonts.md +205 -0
- package/vendor/pixijs-skills/skills/pixijs-assets/references/gif.md +171 -0
- package/vendor/pixijs-skills/skills/pixijs-assets/references/manifests.md +175 -0
- package/vendor/pixijs-skills/skills/pixijs-assets/references/progress.md +142 -0
- package/vendor/pixijs-skills/skills/pixijs-assets/references/resolution.md +142 -0
- package/vendor/pixijs-skills/skills/pixijs-assets/references/spritesheet.md +119 -0
- package/vendor/pixijs-skills/skills/pixijs-assets/references/svg.md +132 -0
- package/vendor/pixijs-skills/skills/pixijs-assets/references/video.md +158 -0
- package/vendor/pixijs-skills/skills/pixijs-blend-modes/SKILL.md +215 -0
- package/vendor/pixijs-skills/skills/pixijs-color/SKILL.md +299 -0
- package/vendor/pixijs-skills/skills/pixijs-core-concepts/SKILL.md +132 -0
- package/vendor/pixijs-skills/skills/pixijs-core-concepts/references/render-loop.md +216 -0
- package/vendor/pixijs-skills/skills/pixijs-core-concepts/references/renderers.md +287 -0
- package/vendor/pixijs-skills/skills/pixijs-create/SKILL.md +276 -0
- package/vendor/pixijs-skills/skills/pixijs-custom-rendering/SKILL.md +405 -0
- package/vendor/pixijs-skills/skills/pixijs-custom-rendering/references/advanced-gpu.md +244 -0
- package/vendor/pixijs-skills/skills/pixijs-custom-rendering/references/uniform-types.md +94 -0
- package/vendor/pixijs-skills/skills/pixijs-environments/SKILL.md +243 -0
- package/vendor/pixijs-skills/skills/pixijs-events/SKILL.md +387 -0
- package/vendor/pixijs-skills/skills/pixijs-filters/SKILL.md +300 -0
- package/vendor/pixijs-skills/skills/pixijs-html-source/SKILL.md +228 -0
- package/vendor/pixijs-skills/skills/pixijs-math/SKILL.md +489 -0
- package/vendor/pixijs-skills/skills/pixijs-migration-v8/SKILL.md +415 -0
- package/vendor/pixijs-skills/skills/pixijs-performance/SKILL.md +489 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-container/SKILL.md +345 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/SKILL.md +243 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/constructor-options.md +155 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/container-hierarchy.md +232 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/layers.md +145 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/masking.md +129 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/render-groups.md +140 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/scene-management.md +169 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-core-concepts/references/transforms.md +198 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-dom-container/SKILL.md +193 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-gif/SKILL.md +233 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-graphics/SKILL.md +576 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-mesh/SKILL.md +141 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-mesh/references/mesh-perspective.md +189 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-mesh/references/mesh-plane.md +159 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-mesh/references/mesh-rope.md +176 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-mesh/references/mesh-simple.md +144 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-mesh/references/mesh.md +213 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-particle-container/SKILL.md +323 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-sprite/SKILL.md +120 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-sprite/references/animated-sprite.md +262 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-sprite/references/nineslice-sprite.md +202 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-sprite/references/sprite.md +181 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-sprite/references/tiling-sprite.md +216 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-text/SKILL.md +136 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-text/references/bitmap-text.md +232 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-text/references/html-text.md +192 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-text/references/split-bitmap-text.md +187 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-text/references/split-text.md +168 -0
- package/vendor/pixijs-skills/skills/pixijs-scene-text/references/text.md +311 -0
- 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
|
+
}
|