@docyrus/animaguru 0.23.0 → 0.25.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 +24 -0
- package/README.md +6 -2
- package/dist/animaguru.js +274 -3
- package/package.json +1 -1
- package/skills/animaguru-aurora-glass/SKILL.md +33 -0
- package/skills/animaguru-aurora-glass/references/recipe.md +27 -0
- package/skills/animaguru-bauhaus-geometry/SKILL.md +35 -0
- package/skills/animaguru-bauhaus-geometry/references/recipe.md +30 -0
- package/skills/animaguru-caption-news/SKILL.md +34 -0
- package/skills/animaguru-caption-news/references/recipe.md +26 -0
- package/skills/animaguru-city-symphony/SKILL.md +34 -0
- package/skills/animaguru-city-symphony/references/recipe.md +26 -0
- package/skills/animaguru-countdown/SKILL.md +34 -0
- package/skills/animaguru-countdown/references/recipe.md +26 -0
- package/skills/animaguru-kino-montage/SKILL.md +34 -0
- package/skills/animaguru-kino-montage/references/recipe.md +26 -0
- package/skills/animaguru-liquid-motion/SKILL.md +35 -0
- package/skills/animaguru-liquid-motion/references/recipe.md +24 -0
- package/skills/animaguru-news-package/SKILL.md +34 -0
- package/skills/animaguru-news-package/references/recipe.md +26 -0
- package/skills/animaguru-newsreel/SKILL.md +34 -0
- package/skills/animaguru-newsreel/references/recipe.md +26 -0
- package/skills/animaguru-one-line-art/SKILL.md +34 -0
- package/skills/animaguru-one-line-art/references/recipe.md +28 -0
- package/skills/animaguru-sticker-explainer/SKILL.md +37 -0
- package/skills/animaguru-sticker-explainer/references/recipe.md +27 -0
- package/skills/animaguru-synthwave-y2k/SKILL.md +35 -0
- package/skills/animaguru-synthwave-y2k/references/recipe.md +29 -0
- package/skills/animaguru-telestrator/SKILL.md +34 -0
- package/skills/animaguru-telestrator/references/recipe.md +26 -0
- package/skills/animaguru-variety-captions/SKILL.md +36 -0
- package/skills/animaguru-variety-captions/references/recipe.md +27 -0
- package/skills/animaguru-visual-investigation/SKILL.md +34 -0
- package/skills/animaguru-visual-investigation/references/recipe.md +26 -0
- package/templates/video/README.md +1 -1
- package/templates/video/VIDEO-STYLE-NOTICES.txt +24 -0
- package/templates/video/shared/locales/en/style-studies.json +120 -0
- package/templates/video/src/studies/scenes.ts +599 -0
- package/templates/video/style-source-provenance.json +7 -2
|
@@ -567,6 +567,605 @@ const scenes: Record<StudyId, (d: Drawing, t: number) => void> = {
|
|
|
567
567
|
if (t > 4.6) d.line([[700, 306], [850, 306]], "#ff5a1f", 2, progress(t, 4.6, .5));
|
|
568
568
|
d.text(d.lines[2], 680, 410, 17, "#15171a").opacity = progress(t, 3, .5);
|
|
569
569
|
},
|
|
570
|
+
"caption-news": (d, t) => {
|
|
571
|
+
d.background("#141210");
|
|
572
|
+
// A ledger card behind a rolling hook number; a chunk-slammed keyword block; a document highlighted last.
|
|
573
|
+
d.rect(0, 70, 960, 300, gradient("#232019", "#141210"));
|
|
574
|
+
for (let i = 0; i < 5; i++) d.line([[40, 110 + i * 54], [920, 110 + i * 54]], "#ffffff", 1, .06);
|
|
575
|
+
const hook = progress(t, .15, .5);
|
|
576
|
+
d.text(Math.round(1300000 * ease(hook)).toLocaleString(), 90, 230, 52, "#ffc21a", "left", "mono").opacity = hook;
|
|
577
|
+
d.text("colonies lost this year", 90, 270, 20, "#c9c7bd");
|
|
578
|
+
const chunk = progress(t, 2.6, .25), slam = spring(t - 2.6, 1.3, 1, 16);
|
|
579
|
+
if (chunk > 0) {
|
|
580
|
+
d.rect(80, 340, 220 * Math.min(1, slam), 56, "#ffc21a", 4, chunk);
|
|
581
|
+
d.text("{+6%}", 94, 378, 32, "#0e0e0c").opacity = chunk;
|
|
582
|
+
}
|
|
583
|
+
d.text(d.lines[2], 80, 432, 22, "#f7f5ef").opacity = progress(t, 3, .4);
|
|
584
|
+
if (t > 5) {
|
|
585
|
+
d.rect(560, 150, 340, 230, "#efe9da");
|
|
586
|
+
d.text("USDA NASS · 2026", 580, 185, 14, "#5b574a", "left", "mono");
|
|
587
|
+
d.rect(580, 206, 260 * progress(t, 5.3, .6), 20, "#ffd54a", 0, .6);
|
|
588
|
+
d.text("6% more managed colonies", 580, 236, 14, "#2a2a26");
|
|
589
|
+
}
|
|
590
|
+
d.title("#f7f5ef");
|
|
591
|
+
d.footer("#8d8b83");
|
|
592
|
+
},
|
|
593
|
+
"newsreel": (d, t) => {
|
|
594
|
+
d.background("#050504");
|
|
595
|
+
// A projected gate: the ident ritual, then a spinning headline, then an iris to a place card.
|
|
596
|
+
const gx = 180, gy = 40, gw = 600, gh = 460;
|
|
597
|
+
d.rect(gx, gy, gw, gh, "#201d16");
|
|
598
|
+
if (t < 2.4) {
|
|
599
|
+
const globe = spring(t - .3, 0, 1, 5);
|
|
600
|
+
d.poly([[gx + gw / 2, gy + gh], [gx + gw / 2 - 260, gy + 40], [gx + gw / 2, gy + 40]], "#efe7d6").opacity = progress(t, .05, .3) * .18;
|
|
601
|
+
d.poly([[gx + gw / 2, gy + gh], [gx + gw / 2 + 260, gy + 40], [gx + gw / 2, gy + 40]], "#efe7d6").opacity = progress(t, .05, .3) * .18;
|
|
602
|
+
d.ellipse(gx + gw / 2, gy + gh * .55, 90 * globe, 90 * globe, "#d9d4ca").opacity = progress(t, .3, .4);
|
|
603
|
+
ring(d, gx + gw / 2, gy + gh * .55, 90 * globe, "#9e978a", 2);
|
|
604
|
+
d.text("THE WORLD IN REVIEW", gx + gw / 2, gy + gh * .55 + 6, 24, "#161410", "center").opacity = progress(t, 1, .4);
|
|
605
|
+
} else if (t < 5) {
|
|
606
|
+
const spin = progress(t, 2.4, .6), n = d.rect(gx + gw / 2 - 150, gy + gh / 2 - 100, 300, 200, "#efe7d6", 2, progress(t, 2.4, .2));
|
|
607
|
+
n.transform = { x: gx + gw / 2, y: gy + gh / 2, rotation: (1 - spin) * Math.PI * 4, scaleX: .4 + .6 * ease(spin), scaleY: .4 + .6 * ease(spin) };
|
|
608
|
+
const headline = d.text("HONEY CRISIS?", 0, 10, 28, "#161410", "center");
|
|
609
|
+
headline.transform = n.transform;
|
|
610
|
+
headline.opacity = spin;
|
|
611
|
+
} else {
|
|
612
|
+
const iris = progress(t, 5, .4);
|
|
613
|
+
d.rect(gx, gy, gw, gh * (1 - iris), "#050504");
|
|
614
|
+
d.text("ALMOND VALLEY", gx + gw / 2, gy + gh * .82, 22, "#f3eee4", "center").opacity = progress(t, 5.6, .4);
|
|
615
|
+
d.text("· 2026 ·", gx + gw / 2, gy + gh * .82 + 30, 15, "#d6cfc0", "center").opacity = progress(t, 5.6, .4);
|
|
616
|
+
}
|
|
617
|
+
d.title("#ece7dc");
|
|
618
|
+
d.footer("#9a9488");
|
|
619
|
+
d.text(d.lines[2], gx + gw / 2, gy + gh + 34, 15, "#b7b0a2", "center", "mono").opacity = progress(t, 6.2, .4);
|
|
620
|
+
},
|
|
621
|
+
"countdown": (d, t) => {
|
|
622
|
+
d.background("#06080a");
|
|
623
|
+
// Cells multiply toward zero, then merge into one picture; the clock inverts at T plus.
|
|
624
|
+
const stages: [number, number][] = [[1, .05], [2, 1], [4, 2.2], [9, 3.6]];
|
|
625
|
+
let cells = 1;
|
|
626
|
+
for (const [n, at] of stages) if (t >= at) cells = n;
|
|
627
|
+
const zero = 6, merged = t >= zero, cols = cells <= 1 ? 1 : cells <= 2 ? 2 : 3, rows = Math.ceil(cells / cols);
|
|
628
|
+
if (!merged) {
|
|
629
|
+
const pad = 20, gw = (880 - pad * (cols - 1)) / cols, gh = (380 - pad * (rows - 1)) / rows;
|
|
630
|
+
for (let i = 0; i < cells; i++) {
|
|
631
|
+
const cx = 40 + (i % cols) * (gw + pad), cy = 110 + Math.floor(i / cols) * (gh + pad);
|
|
632
|
+
d.rect(cx, cy, gw, gh, "#0b0e11");
|
|
633
|
+
d.rect(cx, cy, gw, gh * (.4 + .3 * Math.abs(Math.sin(t * 2 + i))), "#14181c");
|
|
634
|
+
d.ellipse(cx + 16, cy + 16, 4, 4, "#ff5b1f", .9);
|
|
635
|
+
d.text(`CAM ${i + 1}`, cx + 28, cy + 20, 12, "#7c878d", "left", "mono");
|
|
636
|
+
}
|
|
637
|
+
} else {
|
|
638
|
+
d.rect(40, 110, 880, 380, "#101418");
|
|
639
|
+
d.text("LIVE", 480, 310, 30, "#f2f5f7", "center");
|
|
640
|
+
}
|
|
641
|
+
const span = merged ? t - zero : Math.max(0, zero - t);
|
|
642
|
+
if (merged) d.rect(330, 24, 300, 60, "#ff5b1f", 6, progress(t, zero, .12));
|
|
643
|
+
d.text(`${merged ? "T+" : "T−"}${String(Math.floor(span)).padStart(2, "0")}.${Math.floor((span % 1) * 10)}`, 480, 66, 44, merged ? "#0a0b0c" : "#ff5b1f", "center", "mono");
|
|
644
|
+
d.title("#f2f5f7");
|
|
645
|
+
d.footer("#7c878d");
|
|
646
|
+
d.text(d.lines[2], 480, 448, 16, "#ff5b1f", "center", "mono").opacity = progress(t, zero + .3, .4);
|
|
647
|
+
},
|
|
648
|
+
"city-symphony": (d, t) => {
|
|
649
|
+
const night = progress(t, 4.6, 1.2);
|
|
650
|
+
d.background(gradient("#bcd4e6", "#e8dcc2"));
|
|
651
|
+
d.rect(0, 0, 960, 540, gradient("#0b1020", "#05070d")).opacity = night;
|
|
652
|
+
// A skyline; traffic becomes streaks of light; a grid of streets multiplies with the score; a hard stop.
|
|
653
|
+
for (let i = 0; i < 9; i++) {
|
|
654
|
+
const h = 60 + hash(i) * 150, x = 60 + i * 96;
|
|
655
|
+
block(d, x, 420 - h, 70, h, 24, hash(i + 9) < .5 ? "#cfd3d6" : "#9aa0a6", "#e6e9ea", "#6c7278");
|
|
656
|
+
d.rect(x, 420 - h, 70, h, "#ffd27a", 0, night * (.15 + .5 * hash(i + 3)));
|
|
657
|
+
}
|
|
658
|
+
const speed = t < 2 ? 1 : t < 4 ? 2.4 : t < 4.6 ? 0 : 3.4;
|
|
659
|
+
if (speed > 0) for (let lane = 0; lane < 3; lane++) {
|
|
660
|
+
const y = 460 + lane * 10, x = ((t * speed * 140 + lane * 220) % 1040) - 60;
|
|
661
|
+
d.rect(x, y, 90, 4, "#ffb347", 2, .8);
|
|
662
|
+
d.rect(x - 40, y, 40, 4, "#ffb347", 2, .25);
|
|
663
|
+
}
|
|
664
|
+
if (t >= 4.6 && t < 7.6) {
|
|
665
|
+
const n = Math.min(16, 4 + Math.floor((t - 4.6) * 12)), cols = Math.ceil(Math.sqrt(n));
|
|
666
|
+
for (let i = 0; i < n; i++) {
|
|
667
|
+
const cx = 620 + (i % cols) * 70, cy = 60 + Math.floor(i / cols) * 70;
|
|
668
|
+
d.rect(cx, cy, 60, 60, hash(i + Math.floor(t * 8)) < .5 ? "#1a2230" : "#2a3a50", 2, .9);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (t > 7.6) d.rect(0, 0, 960, 540, "#000000", 0, progress(t, 7.6, .15));
|
|
672
|
+
d.title(night > .5 ? "#ede8df" : "#2b2a26");
|
|
673
|
+
d.footer(night > .5 ? "#b0aaa0" : "#6a6560");
|
|
674
|
+
d.text(d.lines[2], 480, 462, 18, "#d9772f", "center").opacity = progress(t, 1.6, .4) * (1 - progress(t, 7.4, .2));
|
|
675
|
+
},
|
|
676
|
+
"visual-investigation": (d, t) => {
|
|
677
|
+
d.background("#0b0c0e");
|
|
678
|
+
d.title("#eceae4");
|
|
679
|
+
d.footer("#8b9097");
|
|
680
|
+
// Three sources sync to one stopwatch; a frame is frozen, circled and magnified; red links converge on a map.
|
|
681
|
+
const panels: [number, number, number, number][] = [[60, 100, 230, 170], [320, 100, 230, 170], [580, 100, 230, 170]];
|
|
682
|
+
const labels = ["照片 · PHOTO", "FBI 电传 · 1937", "原声 · RADIO"];
|
|
683
|
+
panels.forEach(([x, y, w, h], i) => {
|
|
684
|
+
const on = progress(t, .4 + i * .6, .2);
|
|
685
|
+
if (on <= 0) return;
|
|
686
|
+
d.rect(x, y, w, h, "#141619", 0, on);
|
|
687
|
+
d.rect(x + 1, y + 1, w - 2, (h - 2) * (.3 + .5 * hash(i + Math.floor(t * 3))), "#2e3238", 0, on * .8);
|
|
688
|
+
d.text(labels[i], x + 10, y + h - 14, 13, "#8b9097", "left", "mono").opacity = on;
|
|
689
|
+
});
|
|
690
|
+
d.text(`T+${t.toFixed(1)}s`, 480, 70, 26, "#ff4a2e", "center", "mono").opacity = progress(t, .1, .4);
|
|
691
|
+
if (t > 2.6) {
|
|
692
|
+
ring(d, 205, 185, 40, "#ff4a2e", 3, progress(t, 2.6, .5));
|
|
693
|
+
if (t > 3.3) { d.ellipse(330, 185, 55, 55, "#141619", .95); ring(d, 330, 185, 55, "#ff4a2e", 2); d.ellipse(205, 185, 20, 20, "#2e3238", progress(t, 3.3, .3)); }
|
|
694
|
+
}
|
|
695
|
+
if (t > 5) {
|
|
696
|
+
const k = progress(t, 5, 1);
|
|
697
|
+
d.line([[175, 185], [480, 380]], "#ff4a2e", 2, k);
|
|
698
|
+
d.line([[435, 185], [480, 380]], "#ff4a2e", 2, k);
|
|
699
|
+
d.line([[695, 185], [480, 380]], "#ff4a2e", 2, k);
|
|
700
|
+
d.ellipse(480, 380, 10 * k, 10 * k, "#ff4a2e", k);
|
|
701
|
+
}
|
|
702
|
+
d.text(d.lines[2], 480, 448, 17, "#8b9097", "center").opacity = progress(t, 6.2, .4);
|
|
703
|
+
},
|
|
704
|
+
"kino-montage": (d, t) => {
|
|
705
|
+
const paper = t < 6.5;
|
|
706
|
+
d.background(paper ? "#ede4d0" : "#141210");
|
|
707
|
+
d.title(paper ? "#141210" : "#ede4d0");
|
|
708
|
+
d.footer(paper ? "#141210" : "#ede4d0");
|
|
709
|
+
// A grid lights cell by cell; cut-out figures stack into a tower; a red band wipes to the count.
|
|
710
|
+
const cells = Math.min(9, Math.floor(t * 4));
|
|
711
|
+
for (let i = 0; i < cells; i++) {
|
|
712
|
+
const cx = 560 + (i % 3) * 110, cy = 90 + Math.floor(i / 3) * 110;
|
|
713
|
+
d.rect(cx, cy, 96, 96, i === cells - 1 ? "#d52b1e" : "#141210", 0, .92);
|
|
714
|
+
}
|
|
715
|
+
const floors = Math.min(6, Math.floor(t * 1.1));
|
|
716
|
+
for (let f = 0; f < floors; f++) {
|
|
717
|
+
const fy = 460 - f * 54, side = f % 2 === 0 ? -1 : 1;
|
|
718
|
+
d.rect(140 + side * 6, fy, 160, 44, paper ? "#141210" : "#ede4d0");
|
|
719
|
+
d.poly([[160, fy + 6], [180, fy + 30], [200, fy + 6], [210, fy + 42], [150, fy + 42]], paper ? "#ede4d0" : "#141210");
|
|
720
|
+
}
|
|
721
|
+
const wipe = progress(t, 6.4, .3);
|
|
722
|
+
if (wipe > 0) d.rect(0, 540 - wipe * 540, 960, wipe * 540, "#d52b1e");
|
|
723
|
+
if (floors > 0) d.text(`${floors}`, 820, 500, 56, "#d52b1e", "center", "mono");
|
|
724
|
+
d.text(d.lines[2], 480, 40, 20, "#d52b1e", "center").opacity = progress(t, 6.8, .3);
|
|
725
|
+
},
|
|
726
|
+
"telestrator": (d, t) => {
|
|
727
|
+
d.background("#0c1411");
|
|
728
|
+
d.title("#f4f1e8");
|
|
729
|
+
d.footer("#9fb3aa");
|
|
730
|
+
// Watch the play, freeze it, draw on it, then flatten the picture into a top-down board.
|
|
731
|
+
d.rect(60, 80, 840, 360, gradient("#123324", "#0d2419"));
|
|
732
|
+
for (let i = 1; i < 6; i++) d.line([[60 + i * 140, 80], [60 + i * 140, 440]], "#1f5c3f", 1, .6);
|
|
733
|
+
const live = t < 3;
|
|
734
|
+
if (live) {
|
|
735
|
+
const ballx = 160 + Math.min(t, 2.4) * 260;
|
|
736
|
+
d.ellipse(ballx, 260 + Math.sin(t * 3) * 30, 8, 8, "#f4f1e8", 1);
|
|
737
|
+
d.ellipse(420, 300, 14, 14, "#3fd8ff", 1);
|
|
738
|
+
d.ellipse(560, 230, 14, 14, "#ffd400", 1);
|
|
739
|
+
}
|
|
740
|
+
if (t >= 3 && t < 3.3) d.rect(60, 80, 840, 360, "#ffffff", 0, 1 - (t - 3) / .3);
|
|
741
|
+
if (t >= 3) {
|
|
742
|
+
ring(d, 560, 230, 30, "#ffd400", 3, progress(t, 3, .5));
|
|
743
|
+
d.line([[560, 230], [700, 180]], "#ffd400", 4, progress(t, 3.4, .5));
|
|
744
|
+
if (t > 3.9) { d.rect(590, 150, 220, 34, "#080c0e"); d.text(d.lines[2], 600, 174, 15, "#ffd400").opacity = progress(t, 3.9, .3); }
|
|
745
|
+
}
|
|
746
|
+
const flatten = progress(t, 5.6, 1.2);
|
|
747
|
+
if (flatten > 0) {
|
|
748
|
+
d.rect(60, 80, 840, 360, "#0c1411", 0, flatten * .96);
|
|
749
|
+
d.ellipse(lerp(560, 480, flatten), lerp(230, 260, flatten), lerp(14, 10, flatten), lerp(14, 10, flatten) * .6, "#ffd400", flatten);
|
|
750
|
+
d.ellipse(lerp(420, 620, flatten), lerp(300, 260, flatten), lerp(14, 10, flatten), lerp(14, 10, flatten) * .6, "#3fd8ff", flatten);
|
|
751
|
+
}
|
|
752
|
+
d.text(`❚❚ ${t.toFixed(2)}s`, 90, 60, 15, "#f4f1e8", "left", "mono");
|
|
753
|
+
},
|
|
754
|
+
"news-package": (d, t) => {
|
|
755
|
+
d.background("#121417");
|
|
756
|
+
// A sting slams in and flies to the corner bug; rundown rows flip and tick; a split-flap board changes value.
|
|
757
|
+
const slam = spring(t, 1.3, 1, 10), fly = progress(t, 1.2, .5), px = lerp(480, 860, fly), py = lerp(270, 50, fly), ps = lerp(1, .33, fly) * (t < 1.2 ? slam : 1);
|
|
758
|
+
const plate = d.poly([[-130, -45], [130, -45], [150, 45], [-150, 45]], "#f2cf1d");
|
|
759
|
+
plate.transform = { x: px, y: py, scaleX: ps, scaleY: ps };
|
|
760
|
+
const title = d.text("THE WEEK", 0, 8, 40, "#121417", "center");
|
|
761
|
+
title.transform = plate.transform;
|
|
762
|
+
title.opacity = 1 - fly * .3;
|
|
763
|
+
["MODELS", "MARKETS", "POLICY"].forEach((label, i) => {
|
|
764
|
+
const at = 1.6 + i * .5, inK = progress(t, at, .3), tick = progress(t, 6.2 + i * .3, .3);
|
|
765
|
+
if (inK <= 0) return;
|
|
766
|
+
d.rect(120, 150 + i * 110, 720, 90, "#1b1e23", 4, inK);
|
|
767
|
+
d.text(label, 150, 205 + i * 110, 28, tick > 0 ? "#f2cf1d" : "#ecece4").opacity = inK;
|
|
768
|
+
if (tick > 0) d.text("✓", 790, 205 + i * 110, 28, "#f2cf1d").opacity = tick;
|
|
769
|
+
});
|
|
770
|
+
if (t > 4.6) {
|
|
771
|
+
d.rect(560, 470, 90, 56, "#22262c", 4);
|
|
772
|
+
d.text(progress(t, 4.6, .6) < 1 ? "142" : "156", 605, 506, 28, "#f2cf1d", "center", "mono");
|
|
773
|
+
}
|
|
774
|
+
d.title("#ecece4");
|
|
775
|
+
d.footer("#8b9097");
|
|
776
|
+
d.text(d.lines[2], 480, 40, 18, "#f2cf1d", "center").opacity = progress(t, 7, .4);
|
|
777
|
+
},
|
|
778
|
+
"aurora-glass": (d, t) => {
|
|
779
|
+
d.background(gradient("#07061a", "#0c0a28", 0, 0, 0, 540));
|
|
780
|
+
// Aurora: stacked translucent ellipses stand in for blurred colour fields; each drifts on its own slow loop.
|
|
781
|
+
const fields: [string, number, number, number, number][] = [["#7b4dff", 230, 170, 230, .3], ["#3c5bff", 560, 330, 260, 1.9], ["#2aa7ff", 790, 160, 200, 3.1], ["#1fd3c0", 380, 470, 230, 4.4]];
|
|
782
|
+
for (const [color, x, y, r, phase] of fields) {
|
|
783
|
+
const cx = x + Math.sin(t * .2 + phase) * 60, cy = y + Math.cos(t * .16 + phase) * 28;
|
|
784
|
+
for (let i = 0; i < 16; i++) d.ellipse(cx, cy, r * (1 - i * .058), r * .68 * (1 - i * .058), color, .045);
|
|
785
|
+
}
|
|
786
|
+
// Sine easing only: nothing overshoots. The cards fade back as the lens becomes the title capsule.
|
|
787
|
+
const calm = (at: number, duration: number) => .5 - .5 * Math.cos(Math.PI * clamp((t - at) / duration));
|
|
788
|
+
const widen = calm(6, .9), fade = 1 - .6 * widen, words = 1 - .95 * widen;
|
|
789
|
+
// Frosted cards: a lifted shadow, a faint white fill and a hairline edge; the nearer card drifts further.
|
|
790
|
+
const glass = (x: number, y: number, w: number, h: number, at: number, depth: number) => {
|
|
791
|
+
const k = calm(at, 1.2) * fade, px = x + Math.sin(t * .33 + depth) * 5 * depth, py = y + 12 * (1 - calm(at, 1.2));
|
|
792
|
+
d.rect(px + 4, py + 14, w - 8, h, "#040312", 24, .3 * k);
|
|
793
|
+
d.rect(px, py, w, h, "#ffffff", 24, .1 * k);
|
|
794
|
+
const edge = d.rect(px, py, w, h, "none", 24, .4 * k);
|
|
795
|
+
edge.stroke = "#ffffff";
|
|
796
|
+
edge.strokeWidth = 1;
|
|
797
|
+
return [px, py] as const;
|
|
798
|
+
};
|
|
799
|
+
// Text enters only with opacity and an 8 px rise.
|
|
800
|
+
const rise = (value: string, x: number, y: number, size: number, at: number, fill = "#ffffff", weight: "400" | "700" = "400") => {
|
|
801
|
+
const k = calm(at, .7), node = d.text(value, x, y + 8 * (1 - k), size, fill);
|
|
802
|
+
node.weight = weight;
|
|
803
|
+
node.opacity = k * words;
|
|
804
|
+
return node;
|
|
805
|
+
};
|
|
806
|
+
const [ax, ay] = glass(120, 150, 280, 210, .2, .5);
|
|
807
|
+
rise("TODAY", ax + 24, ay + 22, 11, .8, "#c9c2ff", "700");
|
|
808
|
+
["Deep work 09:30", "Design review 13:00", "Evening walk 18:15"].forEach((row, i) => rise(row, ax + 24, ay + 58 + i * 46, 17, .9 + i * .18, "#e4e0ff"));
|
|
809
|
+
const [bx, by] = glass(360, 190, 420, 190, .7, 1);
|
|
810
|
+
rise("What matters most today?", bx + 26, by + 40, 25, 1.3);
|
|
811
|
+
rise("Two quiet hours before noon.", bx + 26, by + 84, 17, 2.6, "#d6d2f5");
|
|
812
|
+
for (let i = 0; i < 22; i++) {
|
|
813
|
+
const h = 4 + 16 * (1 - .75 * calm(2.6, .8)) * (.5 + .5 * Math.sin(t * 5 + i * .6)) * Math.sin(Math.PI * (i + .5) / 22);
|
|
814
|
+
d.rect(bx + 30 + i * 7, by + 150 - h / 2, 3, h, "#9fc0ff", 1.5, calm(1.8, .7) * words);
|
|
815
|
+
}
|
|
816
|
+
// A clear lens glides across the question, then slows and widens into the title capsule.
|
|
817
|
+
if (t > 3.8) {
|
|
818
|
+
const glide = calm(3.8, 2.2), cx = lerp(-80, 480, glide), cy = lerp(240, 270, widen), hw = lerp(70, 210, widen), hh = lerp(70, 48, widen);
|
|
819
|
+
d.rect(cx - hw + 4, cy - hh + 12, hw * 2 - 8, hh * 2, "#040312", Math.min(hw, hh), .25);
|
|
820
|
+
const lens = d.rect(cx - hw, cy - hh, hw * 2, hh * 2, "#ffffff", Math.min(hw, hh), .07 + .1 * widen);
|
|
821
|
+
lens.stroke = "#ffffff";
|
|
822
|
+
lens.strokeWidth = 1.5;
|
|
823
|
+
d.ellipse(cx - hw * .42, cy - hh * .58, hw * .26, hh * .08, "#ffffff", .4 * (1 - widen));
|
|
824
|
+
const title = calm(6.4, .8), name = d.text(d.lines[2], 480, 248 + 8 * (1 - title), 34, "#ffffff", "center");
|
|
825
|
+
name.weight = "400";
|
|
826
|
+
name.opacity = title;
|
|
827
|
+
}
|
|
828
|
+
d.title("#f2efff");
|
|
829
|
+
d.footer("#a9a3d9");
|
|
830
|
+
},
|
|
831
|
+
"liquid-motion": (d, t) => {
|
|
832
|
+
// Drops hit a pool, three blobs tear free on thinning necks and fuse, two wave fronts flood across, and the
|
|
833
|
+
// fused drop stretches into a pill and pours into a word, then a drip snaps off its p.
|
|
834
|
+
const flung = (k: number) => 1 - (1 - clamp(k)) ** 4, violet = "#7a36ff", pool = 420;
|
|
835
|
+
const drops: [number, number, string][] = [[280, .4, "#ff3f8c"], [640, .7, violet], [470, .95, "#ffae2a"]];
|
|
836
|
+
d.background(gradient("#2c0f45", "#12061e", 0, 0, 0, 540));
|
|
837
|
+
const surface: Point[] = [[0, 540]];
|
|
838
|
+
for (let x = 0; x <= 960; x += 16) {
|
|
839
|
+
let y = pool + Math.sin(x * .012 + t * 1.4) * 5;
|
|
840
|
+
for (const [at, hit] of drops) if (t > hit) y += 16 * Math.exp(-(((x - at) / 34) ** 2) - 5 * (t - hit)) * Math.cos(14 * (t - hit));
|
|
841
|
+
surface.push([x, y]);
|
|
842
|
+
}
|
|
843
|
+
surface.push([960, 540]);
|
|
844
|
+
d.poly(surface, "#ff7b1c");
|
|
845
|
+
for (const [x, hit, color] of drops) {
|
|
846
|
+
const k = hit - t;
|
|
847
|
+
if (k > 0 && k < .5) d.ellipse(x, pool - 18 - 1800 * k * k, 14, 20, color);
|
|
848
|
+
// Crown droplets follow the splash a few frames late and fall back into the pool.
|
|
849
|
+
if (k <= -.08 && k > -.5) for (const side of [-1, 1]) d.ellipse(x + side * 110 * (-k - .08), pool - 8 - 150 * (-k - .08) + 400 * (k + .08) ** 2, 4.5, 4, color);
|
|
850
|
+
}
|
|
851
|
+
const blobs: [number, number, number, string, number][] = [[480, 225, 58, violet, 1.2], [370, 250, 34, "#ff3f8c", 1.32], [590, 246, 30, "#ffae2a", 1.4]];
|
|
852
|
+
const fuse = flung((t - 2.1) / .5);
|
|
853
|
+
// Followers are drawn first so they sink into the leader as they fuse.
|
|
854
|
+
if (t < 3.3) [2, 1, 0].forEach((i) => {
|
|
855
|
+
const [x, y, r, color, launch] = blobs[i];
|
|
856
|
+
if (t <= launch) return;
|
|
857
|
+
const k = flung((t - launch) / .8), after = t - launch - .3, base = lerp(x, 480, .5);
|
|
858
|
+
const cx = lerp(base, x, k), cy = lerp(pool + r * .4, y, k), size = i ? lerp(r, r * .2, fuse) : lerp(r, 72, fuse);
|
|
859
|
+
// The neck thins until it snaps; the blob then rings back from a stretch to round.
|
|
860
|
+
if (after < 0) d.line([[base, pool], [cx, cy + r * .6]], color, lerp(r * .5, r * .12, (t - launch) / .3));
|
|
861
|
+
const ring = after < 0 ? 1.15 : 1 + .12 * Math.exp(-9 * after) * Math.cos(26 * after);
|
|
862
|
+
d.ellipse(i ? lerp(cx, 480 + (i === 1 ? -14 : 14), fuse) : cx, i ? lerp(cy, 228, fuse) : cy, size / Math.sqrt(ring), size * ring, color).opacity = i ? 1 - fuse : 1;
|
|
863
|
+
});
|
|
864
|
+
// Two fronts with four bulges each that swell out of phase; the second one becomes the new ground.
|
|
865
|
+
for (const [color, start] of [["#ff4b45", 2.62], ["#f6ecdf", 2.86]] as const) {
|
|
866
|
+
if (t < start) continue;
|
|
867
|
+
const sweep = -240 + 1500 * flung((t - start) / 1), front: Point[] = [[-20, 0]];
|
|
868
|
+
for (let y = 0; y <= 540; y += 10) front.push([sweep + .2 * (y - 270) + [80, 210, 345, 470].reduce((sum, at, b) => sum + 70 * Math.exp(-(((y - at) / 45) ** 2)) * (.6 + .4 * Math.sin(8 * t + b * 1.7)), 0), y]);
|
|
869
|
+
front.push([-20, 540]);
|
|
870
|
+
d.poly(front, color);
|
|
871
|
+
}
|
|
872
|
+
if (t >= 3.3) {
|
|
873
|
+
// Drop, then a pill, then the word: a simple intermediate shape keeps the pour readable.
|
|
874
|
+
const pill = flung((t - 3.58) / .4), word = flung((t - 3.9) / .7), squash = progress(t, 3.36, .22) * (1 - pill);
|
|
875
|
+
if (word < 1) d.rect(lerp(408, 300, pill), lerp(156, 196, pill) + 20 * squash, lerp(144, 360, pill), lerp(144, 92, pill) * (1 - .3 * squash), violet, lerp(72, 46, pill), 1 - word);
|
|
876
|
+
const mark = d.text("pour.", 0, -60, 128, violet, "center");
|
|
877
|
+
mark.opacity = word;
|
|
878
|
+
mark.transform = { x: 480, y: 250, scaleX: 1 + .06 * Math.exp(-6 * Math.max(0, t - 4.6)) * Math.cos(14 * (t - 4.6)), scaleY: 1 };
|
|
879
|
+
if (t > 5.6) {
|
|
880
|
+
const sag = clamp((t - 5.85) / 1) ** 2.2 * 60, fall = Math.max(0, t - 6.9), y = 342 + sag + 1600 * fall * fall;
|
|
881
|
+
if (t < 6.9) d.line([[337, 328], [337, y - 8]], violet, lerp(9, 2.4, sag / 60));
|
|
882
|
+
if (y < 560) d.ellipse(337, y, 10, 12 + Math.min(6, fall * 30), violet);
|
|
883
|
+
}
|
|
884
|
+
d.text(d.lines[2], 480, 430, 18, "#3b1658", "center").opacity = progress(t, 5.15, .5);
|
|
885
|
+
}
|
|
886
|
+
d.title(t < 3.1 ? "#f3e9ff" : "#3b1658");
|
|
887
|
+
d.footer(t < 3.1 ? "#b9a4cf" : "#7a6488");
|
|
888
|
+
},
|
|
889
|
+
"bauhaus-geometry": (d, t) => {
|
|
890
|
+
d.background("#f1e9da");
|
|
891
|
+
d.title("#151413");
|
|
892
|
+
d.footer("#6d6558");
|
|
893
|
+
// A 16 × 8 grid of 40 px modules centred on the frame; one action per half-second beat, eased without overshoot.
|
|
894
|
+
const M = 40, gx = (c: number) => 160 + c * M, gy = (r: number) => 110 + r * M, beat = (k: number) => k * .5;
|
|
895
|
+
const quad = (x: number) => { const k = clamp(x); return k < .5 ? 2 * k * k : 1 - (-2 * k + 2) ** 2 / 2; };
|
|
896
|
+
const act = (k: number, length = .3) => quad((t - beat(k)) / length);
|
|
897
|
+
for (let r = 0; r <= 8; r++) d.line([[gx(0), gy(r)], [gx(16), gy(r)]], "#cdc2ae", 1, clamp((t - r * .03) / .3));
|
|
898
|
+
for (let c = 0; c <= 16; c++) d.line([[gx(c), gy(0)], [gx(c), gy(8)]], "#cdc2ae", 1, clamp((t - .1 - c * .02) / .3));
|
|
899
|
+
// Positions are the finished poster; until beat 8 everything is shown turned 180° about the frame centre.
|
|
900
|
+
const spin = (p: Point, a: number, o: Point): Point => [o[0] + (p[0] - o[0]) * Math.cos(a) - (p[1] - o[1]) * Math.sin(a), o[1] + (p[0] - o[0]) * Math.sin(a) + (p[1] - o[1]) * Math.cos(a)];
|
|
901
|
+
const turn = Math.PI * (1 - act(8, .5)), place = (points: Point[]) => points.map((p) => spin(p, turn, [gx(8), gy(4)]));
|
|
902
|
+
const quarter = (o: Point, r: number, from: number): Point[] => [o, ...Array.from({ length: 13 }, (_, i): Point => [o[0] + Math.cos(from + i / 24 * Math.PI) * r, o[1] + Math.sin(from + i / 24 * Math.PI) * r])];
|
|
903
|
+
const box = (x: number, y: number, w: number, h: number): Point[] => [[x, y], [x + w, y], [x + w, y + h], [x, y + h]];
|
|
904
|
+
// Beat 1 the circle slides eight modules in, beat 6 two more; beat 2 a bar runs out at constant speed.
|
|
905
|
+
if (t >= beat(1)) {
|
|
906
|
+
const [c] = place([[gx(t < beat(6) ? lerp(-6, 2, act(1)) : lerp(2, 4, act(6))), gy(3)]]);
|
|
907
|
+
d.ellipse(c[0], c[1], 2 * M, 2 * M, "#e03c31");
|
|
908
|
+
}
|
|
909
|
+
if (t >= beat(2)) d.poly(place(box(gx(-4), gy(5), (gx(10) - gx(-4)) * clamp((t - beat(2)) / .32), M / 2)), "#151413");
|
|
910
|
+
// Beat 3 the triangle swings about its right angle; beat 4 the quarter disc sweeps about its corner.
|
|
911
|
+
if (t >= beat(3)) {
|
|
912
|
+
const a: Point = [gx(10), gy(5)], swing = Math.PI / 2 * (1 - act(3));
|
|
913
|
+
d.poly(place([a, spin([gx(6), gy(5)], swing, a), spin([gx(10), gy(1)], swing, a)]), "#f2b705");
|
|
914
|
+
}
|
|
915
|
+
if (t >= beat(4)) d.poly(place(quarter([gx(0), gy(8)], 2 * M, -Math.PI / 2 - Math.PI / 2 * (1 - act(4)))), "#1e4fa3");
|
|
916
|
+
// Beat 5 an array of quarter discs lights column by column; beats 9–11 send three phase-offset waves through it.
|
|
917
|
+
const inks = ["#e03c31", "#f2b705", "#1e4fa3", "#151413"];
|
|
918
|
+
for (let j = 0; j < 2; j++) for (let i = 0; i < 8; i++) {
|
|
919
|
+
if (t < beat(5) + (i * 2 + j) * .028) continue;
|
|
920
|
+
let turns = 0, colour = ([[0, 1, 3, 3, 2, 0, 1, 1], [3, 2, 0, 1, 1, 3, 2, 0]][j][i] + 3) % 4;
|
|
921
|
+
for (const [k, dir, recolour] of [[9, 1, 0], [10, 1, 1], [11, -1, 0]]) {
|
|
922
|
+
const q = quad((t - beat(k) - (dir > 0 ? i : 7 - i) * .042 - j * .021) / .16);
|
|
923
|
+
turns += q;
|
|
924
|
+
if (recolour && q >= .5) colour++;
|
|
925
|
+
}
|
|
926
|
+
const corner = ([[2, 3, 1, 0, 2, 3, 1, 0], [1, 0, 3, 2, 1, 0, 3, 2]][j][i] + 1) % 4, x = gx(3 + i), y = gy(6 + j);
|
|
927
|
+
const corners: Point[] = [[x, y], [x + M, y], [x + M, y + M], [x, y + M]], centre: Point = [x + M / 2, y + M / 2];
|
|
928
|
+
const disc = quarter(corners[corner], M, corner * Math.PI / 2).map((p) => spin(p, turns * Math.PI / 2, centre));
|
|
929
|
+
d.poly(place(disc), inks[colour % 4]);
|
|
930
|
+
}
|
|
931
|
+
// Beat 7 a rule fences off the type column; beat 12 the title cuts in; beat 13 the small caps and a red bar.
|
|
932
|
+
if (t >= beat(7)) d.poly(place(box(gx(11.5) - 2, gy(0), 4, (gy(8) - gy(0)) * clamp((t - beat(7)) / .3))), "#151413");
|
|
933
|
+
if (t >= beat(12)) d.text("FORM", 0, 0, 92, "#151413").transform = { x: gx(12) + 34, y: gy(8), rotation: -Math.PI / 2 };
|
|
934
|
+
if (t >= beat(13)) {
|
|
935
|
+
d.text(d.lines[2].toUpperCase(), 0, 0, 13, "#151413", "left", "mono").transform = { x: gx(12) + 4, y: gy(8), rotation: -Math.PI / 2 };
|
|
936
|
+
d.rect(lerp(gx(3), gx(6), act(13)), gy(1) - 14, 3 * M, 6, "#e03c31");
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
"synthwave-y2k": (d, t) => {
|
|
940
|
+
// A neon grid runs at a slatted sun, a chrome title slams in, then a pop-up is dragged aside and the tape glitches.
|
|
941
|
+
const hz = 318, st = t > 5.4 && t < 6 ? sampled(t, 15) : t, sunY = hz + 30 - 150 * progress(st, .2, 2.4);
|
|
942
|
+
d.background(gradient("#05021a", "#c42b7f", 0, 0, 0, hz));
|
|
943
|
+
for (let i = 0; i < 70; i++)
|
|
944
|
+
d.ellipse(hash(i + 7) * 960, hash(i + 70) * 250, .8 + hash(i) * 1.1, .8 + hash(i) * 1.1, "#f4e8ff", (.3 + .5 * hash(i + 3)) * (.6 + .4 * Math.sin(st * 2 + i)));
|
|
945
|
+
d.ellipse(480, sunY, 112, 112, gradient("#fff4a3", "#ff2b9c", 0, sunY - 112, 0, sunY + 112));
|
|
946
|
+
// Slats cut into the sun's lower half, sliding down; each is painted in the sky colour behind it.
|
|
947
|
+
for (let i = 0; i < 6; i++) {
|
|
948
|
+
const y = sunY + 10 + i * 17 + (st * 10) % 17;
|
|
949
|
+
if (y < hz) d.rect(360, y, 240, 2 + i * 1.5, "#8a1f73");
|
|
950
|
+
}
|
|
951
|
+
d.poly([[0, hz], [0, 250], [70, 228], [150, 262], [230, 216], [330, hz]], "#14062d", "#38d8ff", 1.5);
|
|
952
|
+
d.poly([[960, hz], [960, 236], [880, 210], [800, 258], [720, 232], [630, hz]], "#14062d", "#38d8ff", 1.5);
|
|
953
|
+
d.rect(0, hz, 960, 540 - hz, gradient("#3d0d55", "#07021a", 0, hz, 0, 540));
|
|
954
|
+
for (let i = -12; i <= 12; i++)
|
|
955
|
+
d.line([[480 + i * 16, hz], [480 + i * 150, 540]], "#ff35c8", 1.4);
|
|
956
|
+
for (let k = 0; k < 14; k++) {
|
|
957
|
+
const z = k + 1 - (st * 2.4) % 1, y = hz + 230 / z;
|
|
958
|
+
if (y < 540) d.line([[0, y], [960, y]], "#ff35c8", 1.2 + 6 / z);
|
|
959
|
+
}
|
|
960
|
+
d.rect(0, hz - 1, 960, 2, "#ff9ad2");
|
|
961
|
+
// Chrome title: a stepped extrusion, a bright face and a hot outline, scaled down hard into place.
|
|
962
|
+
const slam = st < 2.8 ? 0 : spring(st - 2.8, 3, 1, 14), visible = progress(st, 2.8, .1);
|
|
963
|
+
for (let i = 7; i >= 0; i--) {
|
|
964
|
+
const n = d.text(d.lines[2], i * .8, i * 1.1 - 34, 62, i === 0 ? "#e8f6ff" : i === 7 ? "#ff2fb8" : "#3a1060", "center");
|
|
965
|
+
n.transform = { x: 480, y: 210, scaleX: slam, scaleY: slam };
|
|
966
|
+
n.opacity = visible;
|
|
967
|
+
}
|
|
968
|
+
if (st > 3.3 && st < 3.9)
|
|
969
|
+
d.ellipse(300 + 360 * progress(st, 3.3, .6), 182, 18, 2, "#ffffff", Math.sin((st - 3.3) / .6 * Math.PI));
|
|
970
|
+
// A pop-up window: dragged aside as a dashed outline, then closed like a tube switching off.
|
|
971
|
+
const pop = st < 4.6 ? 0 : spring(st - 4.6, 0, 1, 16), drag = progress(st, 5.4, .5), close = 1 - progress(st, 6.6, .25);
|
|
972
|
+
if (pop > .01 && close > .01) {
|
|
973
|
+
const x = 330 - 210 * drag, y = 330 + 40 * drag;
|
|
974
|
+
const win = d.rect(-150, -70, 300, 140, "#ece8ff", 8);
|
|
975
|
+
const bar = d.rect(-150, -70, 300, 22, gradient("#c8ff2e", "#8a2bff", -150, 0, 150, 0), 8);
|
|
976
|
+
const label = d.text("TAPE.EXE · Play the night?", -138, -34, 14, "#1b0d42");
|
|
977
|
+
const play = d.rect(30, 26, 100, 28, "#b6f22a", 14), go = d.text("PLAY", 80, 31, 14, "#1d3300", "center");
|
|
978
|
+
for (const n of [win, bar, label, play, go]) n.transform = { x: x + 150, y: y + 70, scaleX: pop, scaleY: pop * close };
|
|
979
|
+
}
|
|
980
|
+
d.title("#f4e8ff");
|
|
981
|
+
d.footer("#c9a8e8");
|
|
982
|
+
// The tape: scanlines everywhere, and a tracking tear once a second.
|
|
983
|
+
for (let y = 0; y < 540; y += 4)
|
|
984
|
+
d.rect(0, y, 960, 1.5, "#000000", 0, .12);
|
|
985
|
+
const tear = (t % 1) < .06 ? 1 : 0;
|
|
986
|
+
if (tear) d.rect(0, 380 + hash(Math.floor(t)) * 120, 960, 10, "#f4e8ff", 0, .35);
|
|
987
|
+
},
|
|
988
|
+
"sticker-explainer": (d, t) => {
|
|
989
|
+
d.background("#e6ebef");
|
|
990
|
+
// One board, one camera: a die-cut sticker, a chart, then the board pulled back to the sum. Board nodes share
|
|
991
|
+
// the camera transform; the title, footer and narration line stay fixed on screen.
|
|
992
|
+
const cup = [480, 270, 1], chart = [1300, 270, 1], board = [880, 460, .5];
|
|
993
|
+
const move = (from: number[], to: number[], at: number, length: number, dip: number) => {
|
|
994
|
+
const k = clamp((t - at) / length), e = k * k * (3 - 2 * k);
|
|
995
|
+
return [lerp(from[0], to[0], e), lerp(from[1], to[1], e), lerp(from[2], to[2], e) * (1 - dip * Math.sin(Math.PI * k))];
|
|
996
|
+
};
|
|
997
|
+
const [cx, cy, z] = t < 4.6 ? move(cup, chart, 2.2, 1, .3) : move(chart, board, 4.6, 1.2, 0);
|
|
998
|
+
const view = { x: 480 - cx * z, y: 270 - cy * z, scaleX: z, scaleY: z };
|
|
999
|
+
const on = (node: StudyNode) => { node.transform = view; return node; };
|
|
1000
|
+
for (let i = 0; i < 220; i++)
|
|
1001
|
+
on(d.ellipse((i % 22) * 80 + 20, Math.floor(i / 22) * 80 + 20, 2, 2, "#b9c4ce", .7));
|
|
1002
|
+
// The cup sticker drops onto the board: a white die-cut margin, a soft shadow, then the "photo" inside it.
|
|
1003
|
+
const s = t < .15 ? 0 : spring(t - .15, 1.3, 1, 14);
|
|
1004
|
+
if (s > 0) {
|
|
1005
|
+
const local = { x: view.x + 300 * z, y: view.y + 290 * z, rotation: -.06, scaleX: z * s, scaleY: z * s };
|
|
1006
|
+
const art = (node: StudyNode) => { node.transform = local; return node; };
|
|
1007
|
+
art(d.ellipse(7, 10, 100, 100, "#1d2834", .16));
|
|
1008
|
+
art(d.ellipse(0, 0, 98, 98, "#ffffff"));
|
|
1009
|
+
art(d.ellipse(0, 0, 86, 86, "#f3f1ec"));
|
|
1010
|
+
art(d.rect(44, -9, 34, 18, "#f7f5f0", 9));
|
|
1011
|
+
art(d.ellipse(-4, -4, 56, 56, "#fbfaf7"));
|
|
1012
|
+
art(d.ellipse(-4, -4, 47, 47, "#9a5a2e"));
|
|
1013
|
+
art(d.ellipse(-13, -10, 11, 11, "#f2e6d3"));
|
|
1014
|
+
art(d.ellipse(5, -10, 11, 11, "#f2e6d3"));
|
|
1015
|
+
art(d.poly([[-23, -6], [15, -6], [-4, 18]], "#f2e6d3"));
|
|
1016
|
+
}
|
|
1017
|
+
on(d.line([[384, 336], [440, 360]], "#1d2834", 2, progress(t, .6, .3)));
|
|
1018
|
+
on(d.text("cappuccino · one shot", 448, 350, 18, "#1d2834")).opacity = progress(t, .8, .3);
|
|
1019
|
+
on(d.text("How many beans", 470, 196, 44, "#1d2834")).opacity = progress(t, .3, .4);
|
|
1020
|
+
on(d.text("make one cup?", 470, 246, 44, "#1d2834")).opacity = progress(t, .4, .4);
|
|
1021
|
+
// A thread links the facts; the camera travels along it.
|
|
1022
|
+
on(d.line([[850, 250], [930, 200], [1010, 205], [1090, 240]], "#93a1ae", 3, progress(t, 2, 1.2)));
|
|
1023
|
+
// Exact bars on one zero-based scale, growing one after another.
|
|
1024
|
+
on(d.text("Top growers · million bags", 1110, 150, 20, "#1d2834")).opacity = progress(t, 2.8, .3);
|
|
1025
|
+
([["Brazil", 66], ["Vietnam", 27], ["Colombia", 12], ["Ethiopia", 8]] as const).forEach(([name, value], i) => {
|
|
1026
|
+
const grow = progress(t, 3 + i * .12, .6), y = 190 + i * 44;
|
|
1027
|
+
on(d.text(name, 1100, y + 4, 17, "#1d2834", "right")).opacity = grow;
|
|
1028
|
+
on(d.rect(1110, y, value * 5 * grow, 30, i === 0 ? "#ff5a36" : "#1d2834", 2));
|
|
1029
|
+
on(d.text(String(Math.round(value * grow)), 1120 + value * 5 * grow, y + 4, 17, i === 0 ? "#ff5a36" : "#1d2834")).opacity = grow;
|
|
1030
|
+
});
|
|
1031
|
+
// The sum waits in the middle of the board until the camera pulls back to it.
|
|
1032
|
+
const sum = progress(t, 5.6, 1);
|
|
1033
|
+
on(d.text(`≈ ${Math.round(6250 * sum).toLocaleString("en-US")}`, 880, 560, 110, "#1d2834", "center")).opacity = progress(t, 5.5, .3);
|
|
1034
|
+
on(d.rect(700, 690, 360 * progress(t, 6.4, .5), 10, "#ff5a36"));
|
|
1035
|
+
on(d.text("espressos from one 60 kg sack", 880, 712, 30, "#5a6b7c", "center")).opacity = progress(t, 6.2, .4);
|
|
1036
|
+
d.rect(250, 418, 460, 36, "#16202b", 18, .88);
|
|
1037
|
+
d.text(d.lines[2], 480, 426, 17, "#ffffff", "center");
|
|
1038
|
+
d.title("#1d2834");
|
|
1039
|
+
d.footer("#5a6b7c");
|
|
1040
|
+
},
|
|
1041
|
+
"variety-captions": (d, t) => {
|
|
1042
|
+
// Stand-in vlog footage (a cat, a mug, a counter edge) with a caption landing a beat after every gag.
|
|
1043
|
+
const freeze = t >= 3 && t < 4, ft = freeze ? 3 : t, beat = 2.5 / 24;
|
|
1044
|
+
const pop = (k: number) => k <= 0 ? 0 : k >= 1 ? 1 : k < .55 ? 1.15 * ease(k / .55) : 1.15 - .15 * ((k - .55) / .45);
|
|
1045
|
+
const wiggle = (seed: number) => Math.sin(t * 7.1 + seed * 2.3);
|
|
1046
|
+
// Three layers per caption: a coloured outline, a white outline and the fill, outlines as rings of offset copies.
|
|
1047
|
+
const caption = (text: string, x: number, y: number, size: number, fill: string, outer: string, k: number, rotation = 0) => {
|
|
1048
|
+
const s = pop(k);
|
|
1049
|
+
if (s <= 0) return;
|
|
1050
|
+
const transform = { x, y, rotation, scaleX: s, scaleY: s };
|
|
1051
|
+
for (const [color, r] of [[outer, size * .17], ["#ffffff", size * .09]] as const)
|
|
1052
|
+
for (let i = 0; i < 12; i++) d.text(text, Math.cos(i / 12 * Math.PI * 2) * r, Math.sin(i / 12 * Math.PI * 2) * r - size * .55, size, color, "center").transform = transform;
|
|
1053
|
+
d.text(text, 0, -size * .55, size, fill, "center").transform = transform;
|
|
1054
|
+
};
|
|
1055
|
+
d.background(gradient("#f1e9dc", "#dccfba", 0, 0, 0, 540));
|
|
1056
|
+
// Defocused kitchen and the counter.
|
|
1057
|
+
d.ellipse(150, 170, 80, 56, "#b9d39d", .55);
|
|
1058
|
+
d.ellipse(260, 240, 60, 60, "#e8a463", .5);
|
|
1059
|
+
d.ellipse(800, 220, 90, 44, "#f0b55c", .45);
|
|
1060
|
+
d.rect(0, 372, 960, 70, gradient("#e3c497", "#c9a476", 0, 372, 0, 442));
|
|
1061
|
+
d.rect(0, 440, 960, 100, "#7f978a");
|
|
1062
|
+
// The cat; its paw reaches for the mug, then goes up to its mouth after the cut.
|
|
1063
|
+
const groom = t >= 5.2, blink = groom ? .8 : hash(Math.floor(t * 2)) > .9 ? .9 : 0;
|
|
1064
|
+
d.ellipse(380, 320, 78, 72, "#f0a24a");
|
|
1065
|
+
d.ellipse(380, 336, 40, 52, "#fbf2e4");
|
|
1066
|
+
d.poly([[336, 168], [346, 112], [380, 150]], "#f0a24a");
|
|
1067
|
+
d.poly([[424, 168], [414, 112], [380, 150]], "#f0a24a");
|
|
1068
|
+
d.ellipse(380, 196, 56, 48, "#f0a24a");
|
|
1069
|
+
for (const ex of [358, 402]) {
|
|
1070
|
+
d.ellipse(ex, 192, 11, 12 * (1 - blink) + .5, "#d8df4a");
|
|
1071
|
+
d.ellipse(ex, 192, 2.5, 9 * (1 - blink) + .5, "#1b1408");
|
|
1072
|
+
}
|
|
1073
|
+
d.ellipse(380, 214, 6, 4, "#f2a3a6");
|
|
1074
|
+
const reach = t >= 4 ? 0 : clamp(Math.sin(Math.max(0, ft - 1.5) * 10.5) * .6 + (ft > 2.6 ? 1 : 0), 0, 1);
|
|
1075
|
+
const paw: Point = groom ? [392, 232 + Math.sin(t * 19) * 4] : [lerp(420, 492, reach), lerp(390, 352, reach)];
|
|
1076
|
+
d.line([[410, 300], paw], "#f0a24a", 22);
|
|
1077
|
+
d.ellipse(paw[0], paw[1], 13, 10, "#fbf2e4");
|
|
1078
|
+
// The mug slides to the edge and tips over; it is gone after the freeze.
|
|
1079
|
+
if (t < 4) {
|
|
1080
|
+
const slide = progress(ft, 2.62, .14), tip = clamp((ft - 2.76) / .3);
|
|
1081
|
+
const m = d.rect(-30, -70, 60, 70, "#e9897c", 8);
|
|
1082
|
+
m.transform = { x: 520 + slide * 60 + tip * 30, y: 410 + tip * tip * 40, rotation: tip * .7 };
|
|
1083
|
+
d.rect(-30, -44, 60, 9, "#f6e8d6").transform = m.transform;
|
|
1084
|
+
}
|
|
1085
|
+
if (freeze) {
|
|
1086
|
+
// Freeze frame: a white flash, converging speed lines, then the burst frame with the shock line.
|
|
1087
|
+
for (let i = 0; i < 40; i++) {
|
|
1088
|
+
const a = hash(i + Math.floor(t * 12) * 41) * Math.PI * 2, w = .012;
|
|
1089
|
+
d.poly([[560 + Math.cos(a) * 200, 330 + Math.sin(a) * 200], [560 + Math.cos(a + w) * 700, 330 + Math.sin(a + w) * 700], [560 + Math.cos(a - w) * 700, 330 + Math.sin(a - w) * 700]], "#ffffff");
|
|
1090
|
+
}
|
|
1091
|
+
d.rect(0, 0, 960, 540, "#ffffff", 0, clamp(1 - (t - 3) / .12));
|
|
1092
|
+
const k = clamp((t - 3 - beat) / .33), burst: Point[] = [];
|
|
1093
|
+
for (let i = 0; i < 36; i++) {
|
|
1094
|
+
const a = i / 36 * Math.PI * 2, r = i % 2 ? .82 : 1.12 + hash(i) * .2;
|
|
1095
|
+
burst.push([Math.cos(a) * 270 * r, Math.sin(a) * 82 * r]);
|
|
1096
|
+
}
|
|
1097
|
+
const shake = 10 * Math.exp(-(t - 3 - beat) * 9) * Math.sin(t * 90);
|
|
1098
|
+
d.poly(burst, "#ffe14a", "#ff2d55", 8).transform = { x: 480 + shake, y: 120, rotation: -.06, scaleX: pop(k), scaleY: pop(k) };
|
|
1099
|
+
caption("NOT THE MUG!!", 480 + shake, 146, 52, "#ff3d6a", "#a8001c", k, -.06);
|
|
1100
|
+
}
|
|
1101
|
+
// Curiosity: a question mark a beat after each tap.
|
|
1102
|
+
[1.68, 1.98, 2.28].forEach((tap, i) => { if (t < 2.62) caption("?", 600 + i * 70, 250 - i * 30, 46 + i * 8, "#3fd6ff", "#1d2b6b", clamp((t - tap - beat) / .33), (i % 2 ? .15 : -.18) + wiggle(i) * .05); });
|
|
1103
|
+
// Awkward: the deadpan line and a sliding sweat drop.
|
|
1104
|
+
if (t >= 4 && t < 5.2) {
|
|
1105
|
+
caption(d.lines[2], 480, 470, 44, "#c9b6ff", "#4a2f9e", clamp((t - 4.3 - beat) / .33), wiggle(3) * .03);
|
|
1106
|
+
const drop = clamp((t - 4.4) / .8);
|
|
1107
|
+
d.path(`M${466},${120 + drop * 50} C${476},${136 + drop * 50} ${478},${148 + drop * 50} ${466},${152 + drop * 50} C${454},${148 + drop * 50} ${456},${136 + drop * 50} ${466},${120 + drop * 50} Z`, "#7fd0ff", "#2a5bd7", 2).opacity = clamp((t - 4.35) * 6);
|
|
1108
|
+
}
|
|
1109
|
+
// Delight: sparkles, then the stamp slams down.
|
|
1110
|
+
if (groom) {
|
|
1111
|
+
caption("ZERO REGRETS", 720, 300, 50, "#ff5cb0", "#4b1c7a", clamp((t - 5.45) / .33), wiggle(5) * .04);
|
|
1112
|
+
for (let i = 0; i < 5; i++) {
|
|
1113
|
+
const r = 10 * pop(clamp((t - 5.7 - i * .08) / .3)) * (.7 + .3 * Math.sin(t * 10 + i));
|
|
1114
|
+
const [x, y] = [[580, 230], [880, 250], [600, 360], [860, 350], [500, 140]][i];
|
|
1115
|
+
d.path(`M${x},${y - r} Q${x},${y} ${x + r},${y} Q${x},${y} ${x},${y + r} Q${x},${y} ${x - r},${y} Q${x},${y} ${x},${y - r} Z`, "#fff6a8", "#ffffff", 1.5);
|
|
1116
|
+
}
|
|
1117
|
+
const ks = clamp((t - 6.6) / .12), sc = 1.8 - .8 * ease(ks);
|
|
1118
|
+
if (ks > 0) {
|
|
1119
|
+
const stamp = { x: 740, y: 140, rotation: -.16, scaleX: sc, scaleY: sc };
|
|
1120
|
+
const frame = d.rect(-110, -38, 220, 76, "#ffffff", 14, ks);
|
|
1121
|
+
frame.fill = "none"; frame.stroke = "#d4232b"; frame.strokeWidth = 6; frame.transform = stamp;
|
|
1122
|
+
d.text("CERTIFIED MENACE", 0, -12, 22, "#d4232b", "center").transform = stamp;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
d.title("#3a2a5a");
|
|
1126
|
+
d.footer("#6b5f7a");
|
|
1127
|
+
},
|
|
1128
|
+
"one-line-art": (d, t) => {
|
|
1129
|
+
d.background(gradient("#101a2b", "#070b13"));
|
|
1130
|
+
// One champagne line that never lifts: a sun on the horizon, three arches, a profile, then a ring that fills with gold.
|
|
1131
|
+
const path: Point[] = [[40, 400], [180, 400]];
|
|
1132
|
+
const arc = (cx: number, cy: number, r: number, a0: number, a1: number) => {
|
|
1133
|
+
for (let i = 1; i <= 40; i++) path.push([cx + Math.cos(lerp(a0, a1, i / 40)) * r, cy + Math.sin(lerp(a0, a1, i / 40)) * r]);
|
|
1134
|
+
};
|
|
1135
|
+
arc(180, 345, 55, Math.PI / 2, -Math.PI * 1.5);
|
|
1136
|
+
path.push([300, 400]);
|
|
1137
|
+
for (let i = 0; i < 3; i++) {
|
|
1138
|
+
const x = 300 + i * 80;
|
|
1139
|
+
path.push([x, 335]);
|
|
1140
|
+
arc(x + 30, 335, 30, Math.PI, Math.PI * 2);
|
|
1141
|
+
path.push([x + 60, 400]);
|
|
1142
|
+
if (i < 2) path.push([x + 80, 400]);
|
|
1143
|
+
}
|
|
1144
|
+
path.push([560, 400], [586, 398], [600, 388], [606, 370], [603, 352]);
|
|
1145
|
+
arc(592, 340, 12, 0.8, 0.8 - Math.PI * 2);
|
|
1146
|
+
arc(640, 312, 46, Math.PI * 0.95, Math.PI * 1.9);
|
|
1147
|
+
path.push([688, 305], [685, 313], [699, 329], [693, 333], [695, 341], [690, 343], [692, 349], [691, 358], [686, 366], [671, 371], [675, 400],
|
|
1148
|
+
[790, 400], [814, 398], [834, 388], [850, 365]);
|
|
1149
|
+
arc(850, 300, 65, Math.PI / 2, -Math.PI * 1.5);
|
|
1150
|
+
const letter: Point[] = [[850, 365], [838, 350], [836, 262], [844, 258], [842, 270], [842, 345], [880, 345], [888, 336]];
|
|
1151
|
+
path.push(...letter.slice(1));
|
|
1152
|
+
const drawn = clamp((t - 0.05) / 5.8), visible = strokePoints(path, drawn), tip = visible[visible.length - 1];
|
|
1153
|
+
d.line(path, "#e9d7a5", 2, drawn);
|
|
1154
|
+
const fill = progress(t, 5.9, 0.8);
|
|
1155
|
+
if (fill > 0) {
|
|
1156
|
+
d.ellipse(850, 300, 62 * fill, 62 * fill, gradient("#f6e6b8", "#9a7838", 790, 240, 910, 360));
|
|
1157
|
+
d.line(letter, "#0b1320", 2, 1).opacity = fill;
|
|
1158
|
+
}
|
|
1159
|
+
if (drawn < 1) {
|
|
1160
|
+
d.ellipse(tip[0], tip[1], 12, 12, "#fff4d6", 0.16);
|
|
1161
|
+
d.ellipse(tip[0], tip[1], 3, 3, "#fff8e6", 1);
|
|
1162
|
+
}
|
|
1163
|
+
const mark = progress(t, 6.5, 0.7);
|
|
1164
|
+
d.text("MAISON LIGNÉE", 480, 462, 24, "#ecdcb0", "center", "serif").opacity = mark;
|
|
1165
|
+
d.text(d.lines[2], 480, 486, 11, "#cdbb8e", "center").opacity = progress(t, 6.9, 0.6) * 0.8;
|
|
1166
|
+
d.title("#ecdcb0");
|
|
1167
|
+
d.footer("#8f8566");
|
|
1168
|
+
},
|
|
570
1169
|
};
|
|
571
1170
|
function ninety(t: number) { return 87 + 9 * (1 - progress(t, .2)); }
|
|
572
1171
|
export function studyFrame(id: StudyId, seconds: number): StudyFrame {
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
"lemo-opuscar": {
|
|
3
3
|
"url": "https://github.com/lemomo-ai/lemo-opuscar",
|
|
4
4
|
"commit": "b65efe6ea393fdc71212898ea4c57c754cfefa46",
|
|
5
|
-
"evidenceFilmsCommit": "b5e8ef93930b6d828f352055a689320f138237b1"
|
|
5
|
+
"evidenceFilmsCommit": "b5e8ef93930b6d828f352055a689320f138237b1",
|
|
6
|
+
"editDocumentaryCommit": "77ee8318df779b0736d7de0abe24387e51023735"
|
|
6
7
|
},
|
|
7
8
|
"awesome-opus5-5-videos": {
|
|
8
9
|
"url": "https://github.com/yihui-dev/awesome-opus5-5-videos",
|
|
@@ -12,5 +13,9 @@
|
|
|
12
13
|
"url": "https://github.com/wheresryan22/anatomy",
|
|
13
14
|
"commit": "1482e8469b0a9e0d8cc18ea2740a01423d29ae80"
|
|
14
15
|
},
|
|
15
|
-
"
|
|
16
|
+
"mg-styles-15": {
|
|
17
|
+
"url": "https://github.com/Vincentwei1021/mg-styles-15",
|
|
18
|
+
"commit": "49052d80bfaf9fa8412cc9710530e5e95a3b8d8c"
|
|
19
|
+
},
|
|
20
|
+
"scope": "Material and motion guidance; original scene study implementations. No source demo assets, data or creator footage bundled. The Machine Anatomy kit file src/kit/anatomy/iso-kit.ts is vendored unchanged from anatomy. editDocumentaryCommit pins lemo-opuscar's Edit & Documentary category (caption-news, newsreel, countdown, city-symphony, visual-investigation, kino-montage, telestrator, news-package); none of its archive footage, documents or audio are bundled, only guidance. mg-styles-15 supplied the signature features of aurora-glass, liquid-motion, bauhaus-geometry, synthwave-y2k, sticker-explainer, variety-captions and one-line-art; none of its code, films, photos, fonts or audio samples are bundled, only guidance."
|
|
16
21
|
}
|