@docyrus/animaguru 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/NOTICE.md +123 -0
  2. package/README.md +6 -2
  3. package/dist/animaguru.js +133 -4
  4. package/package.json +1 -1
  5. package/skills/animaguru-aurora-glass/SKILL.md +33 -0
  6. package/skills/animaguru-aurora-glass/references/recipe.md +27 -0
  7. package/skills/animaguru-bauhaus-geometry/SKILL.md +35 -0
  8. package/skills/animaguru-bauhaus-geometry/references/recipe.md +30 -0
  9. package/skills/animaguru-liquid-motion/SKILL.md +35 -0
  10. package/skills/animaguru-liquid-motion/references/recipe.md +24 -0
  11. package/skills/animaguru-one-line-art/SKILL.md +34 -0
  12. package/skills/animaguru-one-line-art/references/recipe.md +28 -0
  13. package/skills/animaguru-sticker-explainer/SKILL.md +37 -0
  14. package/skills/animaguru-sticker-explainer/references/recipe.md +27 -0
  15. package/skills/animaguru-synthwave-y2k/SKILL.md +35 -0
  16. package/skills/animaguru-synthwave-y2k/references/recipe.md +29 -0
  17. package/skills/animaguru-variety-captions/SKILL.md +36 -0
  18. package/skills/animaguru-variety-captions/references/recipe.md +27 -0
  19. package/templates/video/README.md +4 -2
  20. package/templates/video/VIDEO-STYLE-NOTICES.txt +24 -0
  21. package/templates/video/WORKSPACE-AGENTS.md +1 -1
  22. package/templates/video/scripts/export-mp4.mjs +91 -13
  23. package/templates/video/scripts/watermark/OFL.txt +93 -0
  24. package/templates/video/scripts/watermark/instrument-sans.woff2 +0 -0
  25. package/templates/video/scripts/watermark/logo-mark-512.png +0 -0
  26. package/templates/video/shared/locales/en/export.json +1 -0
  27. package/templates/video/shared/locales/en/style-studies.json +56 -0
  28. package/templates/video/src/exporter.ts +11 -1
  29. package/templates/video/src/studies/scenes.ts +391 -0
  30. package/templates/video/src/watermark.ts +50 -0
  31. package/templates/video/style-source-provenance.json +5 -1
@@ -300,6 +300,62 @@
300
300
  "Procedural graphics",
301
301
  "Rundown · tick · flip"
302
302
  ]
303
+ },
304
+ "aurora-glass": {
305
+ "name": "Aurora & Glass",
306
+ "lines": [
307
+ "Light, held in glass.",
308
+ "Original aurora and glass study · invented product",
309
+ "Halcyon"
310
+ ]
311
+ },
312
+ "liquid-motion": {
313
+ "name": "Liquid Motion",
314
+ "lines": [
315
+ "One drop becomes the mark.",
316
+ "Original liquid study",
317
+ "Pour · neck · snap · drip"
318
+ ]
319
+ },
320
+ "bauhaus-geometry": {
321
+ "name": "Bauhaus Geometry",
322
+ "lines": [
323
+ "Every move lands on the grid.",
324
+ "Original geometric study",
325
+ "Kreis · Dreieck · Quadrat"
326
+ ]
327
+ },
328
+ "synthwave-y2k": {
329
+ "name": "Synthwave & Y2K",
330
+ "lines": [
331
+ "Rewind to the future.",
332
+ "Original scene / tape variant",
333
+ "LASER COAST"
334
+ ]
335
+ },
336
+ "sticker-explainer": {
337
+ "name": "Sticker Explainer",
338
+ "lines": [
339
+ "One board, one camera, one fact per line.",
340
+ "Procedural stickers · illustrative figures",
341
+ "Every line brings a new sticker or chart"
342
+ ]
343
+ },
344
+ "variety-captions": {
345
+ "name": "Variety Captions",
346
+ "lines": [
347
+ "Every gag gets its caption.",
348
+ "Illustrative footage",
349
+ "...what mug?"
350
+ ]
351
+ },
352
+ "one-line-art": {
353
+ "name": "One-Line Art",
354
+ "lines": [
355
+ "One line, never lifted.",
356
+ "Invented brand",
357
+ "Sun · arches · profile · ring"
358
+ ]
303
359
  }
304
360
  }
305
361
  }
@@ -24,11 +24,13 @@ export async function exportMp4(api: CaptureApi, token: string) {
24
24
  status.style.cssText = "position:fixed;z-index:100;left:24px;bottom:24px;padding:16px 22px;border-radius:12px;background:#162224ed;color:white;font:15px Arial,sans-serif;box-shadow:0 8px 30px #0002;max-width:calc(100vw - 48px)";
25
25
  status.textContent = copy.preparing;
26
26
  document.body.append(status);
27
+ // With --watermark, finishing also encodes the watermarked copy, which takes longer.
28
+ let finishTimeout = 180000;
27
29
  const request = async (route: string, body?: BodyInit, type = "application/json") => {
28
30
  const attempts = route.startsWith("frame?") ? 3 : 1;
29
31
  for (let attempt = 0; attempt < attempts; attempt++) {
30
32
  try {
31
- const response = await fetch(`/__export/${route}`, { method: "POST", headers: { "Content-Type": type, "X-AnimaGuru-Export": token }, body, signal: AbortSignal.timeout(route === "finish" ? 180000 : 45000) });
33
+ const response = await fetch(`/__export/${route}`, { method: "POST", headers: { "Content-Type": type, "X-AnimaGuru-Export": token }, body, signal: AbortSignal.timeout(route === "finish" ? finishTimeout : 45000) });
32
34
  if (!response.ok) throw new Error(await response.text());
33
35
  return await response.json();
34
36
  } catch (error) {
@@ -46,6 +48,14 @@ export async function exportMp4(api: CaptureApi, token: string) {
46
48
  const settings = await request("start", JSON.stringify({ width: api.width, height: api.height, duration: api.duration, fps: api.fps, language, subtitles }));
47
49
  // The export command decides: captions drawn into the picture, or saved as .srt/.vtt files beside a clean MP4.
48
50
  api.setCaptions(settings.burnCaptions === true);
51
+ if (settings.watermark) {
52
+ // --watermark: the badge is drawn here once, before any frame, and the export command lays it over a copy.
53
+ status.textContent = copy.watermark;
54
+ const { watermarkBadge } = await import("./watermark");
55
+ const badge = await watermarkBadge(api.width, api.height, settings.watermark);
56
+ await request(`watermark?x=${badge.x}&y=${badge.y}`, badge.image, "image/png");
57
+ finishTimeout = 15 * 60000;
58
+ }
49
59
  state.total = settings.frames;
50
60
  state.status = "rendering";
51
61
  for (let frame = 0; frame < settings.frames; frame++) {
@@ -775,6 +775,397 @@ const scenes: Record<StudyId, (d: Drawing, t: number) => void> = {
775
775
  d.footer("#8b9097");
776
776
  d.text(d.lines[2], 480, 40, 18, "#f2cf1d", "center").opacity = progress(t, 7, .4);
777
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
+ },
778
1169
  };
779
1170
  function ninety(t: number) { return 87 + 9 * (1 - progress(t, .2)); }
780
1171
  export function studyFrame(id: StudyId, seconds: number): StudyFrame {
@@ -0,0 +1,50 @@
1
+ /*
2
+ The "Made with AnimaGuru" badge of an AnimaGuru lead film, drawn the way AnimaGuru's web app draws it in the viewer's
3
+ browser, with the same Instrument Sans and logo files, which the export command sends. `npm run export:mp4 --
4
+ --watermark` lays it over a copy of the film, so the copy matches one made on animaguru.com.
5
+ */
6
+ const FAMILY = "AnimaGuru Watermark Sans";
7
+ export const WATERMARK_LEAD = "Made with ";
8
+ export const WATERMARK_NAME = "AnimaGuru";
9
+
10
+ /** The badge's sizes for a width×height picture, from its shorter side so portrait and landscape videos match. */
11
+ export function watermarkMetrics(width: number, height: number) {
12
+ const short = Math.min(width, height);
13
+ const font = Math.min(64, Math.max(12, Math.round(short * 0.028)));
14
+ const badge = Math.round(font * 2.1);
15
+ return { font, badge, logo: Math.round(font * 1.4), gap: Math.round(font * 0.45), padding: Math.round(font * 0.75), margin: Math.max(8, Math.round(short * 0.035)) };
16
+ }
17
+ /** Where a badge of `size` goes in the picture: the bottom-right corner, shrunk when it would cover too much of a small video. */
18
+ export function watermarkPlacement(width: number, height: number, size: { width: number; height: number }, margin: number) {
19
+ const scale = Math.min(1, (width * 0.6) / size.width, (height * 0.2) / size.height);
20
+ const w = Math.round(size.width * scale), h = Math.round(size.height * scale);
21
+ return { x: Math.round(width - margin - w), y: Math.round(height - margin - h), width: w, height: h };
22
+ }
23
+
24
+ const bytes = (base64: string) => Uint8Array.from(atob(base64), (character) => character.charCodeAt(0));
25
+
26
+ /** The badge for a width×height film as a PNG at its final size, and where it goes. `files` holds the font and logo as base64. */
27
+ export async function watermarkBadge(width: number, height: number, files: { font: string; logo: string }) {
28
+ const size = watermarkMetrics(width, height);
29
+ document.fonts.add(await new FontFace(FAMILY, bytes(files.font), { weight: "400 700" }).load());
30
+ const lead = `500 ${size.font}px "${FAMILY}"`, name = `700 ${size.font}px "${FAMILY}"`;
31
+ const logo = await createImageBitmap(new Blob([bytes(files.logo)], { type: "image/png" }));
32
+ const measure = new OffscreenCanvas(1, 1).getContext("2d")!;
33
+ measure.font = lead; const leadWidth = measure.measureText(WATERMARK_LEAD).width;
34
+ measure.font = name; const nameWidth = measure.measureText(WATERMARK_NAME).width;
35
+ const inset = (size.badge - size.logo) / 2;
36
+ const canvas = new OffscreenCanvas(Math.ceil(inset + size.logo + size.gap + leadWidth + nameWidth + size.padding), size.badge);
37
+ const context = canvas.getContext("2d")!;
38
+ context.fillStyle = "rgba(12, 14, 18, 0.58)";
39
+ context.beginPath(); context.roundRect(0, 0, canvas.width, canvas.height, canvas.height / 2); context.fill();
40
+ context.drawImage(logo, inset, inset, size.logo, size.logo); logo.close();
41
+ context.textBaseline = "middle";
42
+ const x = inset + size.logo + size.gap, y = canvas.height / 2 + size.font * 0.04;
43
+ context.font = lead; context.fillStyle = "rgba(255, 255, 255, 0.82)"; context.fillText(WATERMARK_LEAD, x, y);
44
+ context.font = name; context.fillStyle = "#ffffff"; context.fillText(WATERMARK_NAME, x + leadWidth, y);
45
+ // The web app scales the badge as it draws it on each frame; here it is scaled once, so FFmpeg lays it over as it is.
46
+ const place = watermarkPlacement(width, height, canvas, size.margin);
47
+ const placed = new OffscreenCanvas(place.width, place.height);
48
+ placed.getContext("2d")!.drawImage(canvas, 0, 0, place.width, place.height);
49
+ return { image: await placed.convertToBlob({ type: "image/png" }), x: place.x, y: place.y, width: place.width, height: place.height };
50
+ }
@@ -13,5 +13,9 @@
13
13
  "url": "https://github.com/wheresryan22/anatomy",
14
14
  "commit": "1482e8469b0a9e0d8cc18ea2740a01423d29ae80"
15
15
  },
16
- "scope": "Material and motion guidance; original scene study implementations. No source demo assets, data or creator footage bundled. The Machine Anatomy kit file src/kit/anatomy/iso-kit.ts is vendored unchanged from anatomy. editDocumentaryCommit pins lemo-opuscar's Edit & Documentary category (caption-news, newsreel, countdown, city-symphony, visual-investigation, kino-montage, telestrator, news-package); none of its archive footage, documents or audio are bundled, only guidance."
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."
17
21
  }