givemotion 0.0.0-stage → 0.1.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 (132) hide show
  1. package/AGENTS.md +37 -0
  2. package/LICENSE +21 -0
  3. package/README.md +492 -2
  4. package/SECURITY.md +40 -0
  5. package/THIRD_PARTY_NOTICES.md +48 -0
  6. package/blocks/bar-chart.html +63 -0
  7. package/blocks/browser-window.html +52 -0
  8. package/blocks/callout.html +57 -0
  9. package/blocks/captions.html +120 -0
  10. package/blocks/checklist.html +32 -0
  11. package/blocks/code-window.html +126 -0
  12. package/blocks/compare.html +33 -0
  13. package/blocks/cursor.html +157 -0
  14. package/blocks/end-card.html +28 -0
  15. package/blocks/gradient-bg.html +48 -0
  16. package/blocks/grid-bg.html +18 -0
  17. package/blocks/highlight.html +13 -0
  18. package/blocks/line-chart.html +36 -0
  19. package/blocks/logo-reveal.html +33 -0
  20. package/blocks/lottie.html +34 -0
  21. package/blocks/lower-third.html +24 -0
  22. package/blocks/phone.html +18 -0
  23. package/blocks/quote.html +19 -0
  24. package/blocks/stat.html +24 -0
  25. package/blocks/steps.html +40 -0
  26. package/blocks/title-card.html +22 -0
  27. package/blocks/word-pop.html +40 -0
  28. package/examples/bench/index.html +24 -0
  29. package/examples/data-story/data.json +9 -0
  30. package/examples/data-story/index.html +361 -0
  31. package/examples/data-story/other.json +46 -0
  32. package/examples/generative/index.html +60 -0
  33. package/examples/made-by-agents/README.md +5 -0
  34. package/examples/made-by-agents/photosynthesis/blocks/checklist.html +32 -0
  35. package/examples/made-by-agents/photosynthesis/blocks/end-card.html +25 -0
  36. package/examples/made-by-agents/photosynthesis/blocks/gradient-bg.html +48 -0
  37. package/examples/made-by-agents/photosynthesis/blocks/steps.html +40 -0
  38. package/examples/made-by-agents/photosynthesis/blocks/title-card.html +22 -0
  39. package/examples/made-by-agents/photosynthesis/index.html +217 -0
  40. package/examples/personalized/index.html +28 -0
  41. package/llms.txt +34 -0
  42. package/package.json +80 -4
  43. package/skills/givemotion/SKILL.md +152 -0
  44. package/skills/givemotion/references/api.md +40 -0
  45. package/skills/givemotion/references/camera.md +72 -0
  46. package/skills/givemotion/references/charts.md +138 -0
  47. package/skills/givemotion/references/check-codes.md +94 -0
  48. package/skills/givemotion/references/data-templates.md +61 -0
  49. package/skills/givemotion/references/diagrams.md +75 -0
  50. package/skills/givemotion/references/genres/code-explainer.md +15 -0
  51. package/skills/givemotion/references/genres/data-story.md +21 -0
  52. package/skills/givemotion/references/genres/explainer.md +20 -0
  53. package/skills/givemotion/references/genres/hand-drawn.md +18 -0
  54. package/skills/givemotion/references/genres/logo-sting.md +20 -0
  55. package/skills/givemotion/references/genres/promo.md +18 -0
  56. package/skills/givemotion/references/genres/ui-demo.md +21 -0
  57. package/skills/givemotion/references/genres/vertical.md +19 -0
  58. package/skills/givemotion/references/genres/year-recap.md +18 -0
  59. package/skills/givemotion/references/javascript.md +72 -0
  60. package/skills/givemotion/references/media-audio.md +57 -0
  61. package/skills/givemotion/references/motion-design.md +183 -0
  62. package/skills/givemotion/references/output.md +64 -0
  63. package/skills/givemotion/references/review.md +60 -0
  64. package/skills/givemotion/references/styles.md +133 -0
  65. package/skills/givemotion/references/techniques/backgrounds-and-ambient.md +85 -0
  66. package/skills/givemotion/references/techniques/camera-moves.md +88 -0
  67. package/skills/givemotion/references/techniques/captions-vertical.md +89 -0
  68. package/skills/givemotion/references/techniques/code-on-screen.md +125 -0
  69. package/skills/givemotion/references/techniques/hand-drawn.md +87 -0
  70. package/skills/givemotion/references/techniques/impacts-and-energy.md +106 -0
  71. package/skills/givemotion/references/techniques/kinetic-type.md +92 -0
  72. package/skills/givemotion/references/techniques/light-and-depth.md +91 -0
  73. package/skills/givemotion/references/techniques/logo-reveal.md +94 -0
  74. package/skills/givemotion/references/techniques/numbers-and-charts.md +92 -0
  75. package/skills/givemotion/references/techniques/paths-and-maps.md +99 -0
  76. package/skills/givemotion/references/techniques/scene-transitions.md +85 -0
  77. package/skills/givemotion/references/techniques/shapes-and-motion-graphics.md +92 -0
  78. package/skills/givemotion/references/techniques/text-reveals.md +94 -0
  79. package/skills/givemotion/references/techniques/ui-demo.md +110 -0
  80. package/skills/givemotion/references/timeline.md +85 -0
  81. package/src/analyze.js +1485 -0
  82. package/src/api.js +36 -0
  83. package/src/args.js +33 -0
  84. package/src/audio-cli.js +58 -0
  85. package/src/bench.js +153 -0
  86. package/src/browser.js +381 -0
  87. package/src/bundle.js +79 -0
  88. package/src/cdp.js +94 -0
  89. package/src/check.js +1818 -0
  90. package/src/cli.js +611 -0
  91. package/src/dev.js +26 -0
  92. package/src/ffmpeg.js +330 -0
  93. package/src/parts.js +380 -0
  94. package/src/player/gm-player.js +175 -0
  95. package/src/render.js +772 -0
  96. package/src/runtime/audio.js +306 -0
  97. package/src/runtime/chart.js +665 -0
  98. package/src/runtime/fonts/BebasNeue-OFL.txt +93 -0
  99. package/src/runtime/fonts/Caveat-OFL.txt +93 -0
  100. package/src/runtime/fonts/Inter-OFL.txt +93 -0
  101. package/src/runtime/fonts/JetBrainsMono-OFL.txt +93 -0
  102. package/src/runtime/fonts/Nunito-OFL.txt +93 -0
  103. package/src/runtime/fonts/PlayfairDisplay-OFL.txt +93 -0
  104. package/src/runtime/fonts/SpaceGrotesk-OFL.txt +93 -0
  105. package/src/runtime/fonts/bebas-neue-latin-400-normal.woff2 +0 -0
  106. package/src/runtime/fonts/bebas-neue-latin-ext-400-normal.woff2 +0 -0
  107. package/src/runtime/fonts/caveat-latin-ext-wght-normal.woff2 +0 -0
  108. package/src/runtime/fonts/caveat-latin-wght-normal.woff2 +0 -0
  109. package/src/runtime/fonts/inter-latin-ext-wght-italic.woff2 +0 -0
  110. package/src/runtime/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
  111. package/src/runtime/fonts/inter-latin-wght-italic.woff2 +0 -0
  112. package/src/runtime/fonts/inter-latin-wght-normal.woff2 +0 -0
  113. package/src/runtime/fonts/jetbrains-mono-latin-ext-wght-italic.woff2 +0 -0
  114. package/src/runtime/fonts/jetbrains-mono-latin-ext-wght-normal.woff2 +0 -0
  115. package/src/runtime/fonts/jetbrains-mono-latin-wght-italic.woff2 +0 -0
  116. package/src/runtime/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  117. package/src/runtime/fonts/nunito-latin-ext-wght-normal.woff2 +0 -0
  118. package/src/runtime/fonts/nunito-latin-wght-normal.woff2 +0 -0
  119. package/src/runtime/fonts/playfair-display-latin-ext-wght-italic.woff2 +0 -0
  120. package/src/runtime/fonts/playfair-display-latin-ext-wght-normal.woff2 +0 -0
  121. package/src/runtime/fonts/playfair-display-latin-wght-italic.woff2 +0 -0
  122. package/src/runtime/fonts/playfair-display-latin-wght-normal.woff2 +0 -0
  123. package/src/runtime/fonts/space-grotesk-latin-ext-wght-normal.woff2 +0 -0
  124. package/src/runtime/fonts/space-grotesk-latin-wght-normal.woff2 +0 -0
  125. package/src/runtime/motion.js +2674 -0
  126. package/src/runtime/preset.css +97 -0
  127. package/src/runtime/runtime.js +2141 -0
  128. package/src/runtime/sketch.js +1249 -0
  129. package/src/server.js +402 -0
  130. package/src/studio/index.html +238 -0
  131. package/src/types/api.d.ts +354 -0
  132. package/templates/starter/index.html +70 -0
@@ -0,0 +1,133 @@
1
+ # Styles: eight named looks
2
+
3
+ Purpose: ready-made looks (palette tokens, fonts, background, motion personality) so a video is consistent from the first scene to the last. Pick one, adapt it to the brand if the brief gives colors or a logo, and stay with it.
4
+
5
+ Contents
6
+ - How to use a look
7
+ - Fonts that work everywhere
8
+ - 1 Midnight Tech · 2 Paper Whiteboard · 3 Editorial · 4 Bold Pop · 5 Swiss Grid · 6 Neon Night · 7 Soft SaaS · 8 Terminal
9
+ - Choosing a look
10
+
11
+ ## How to use a look
12
+
13
+ 1. Pick the look that fits the brief (table at the end). If the brief names brand colors, keep the look's structure (background, ink, muted, one or two accents) and swap the hex values.
14
+ 2. Put the tokens on `:root` and use only them:
15
+ ```css
16
+ :root { --bg: #0b0d12; --surface: #151a23; --ink: #f2f4f8; --muted: #9aa3b2; --accent: #5b8cff; --accent-2: #38e1c7; }
17
+ body { background: var(--bg); color: var(--ink); }
18
+ ```
19
+ 3. Use the look's motion personality for every scene: the same easing family, the same transition family, the same camera behaviour. Change layouts and camera framing between scenes, not the style.
20
+ 4. Contrast: the ink and muted colors below pass 4.5:1 on their background; accents pass 3:1 or better, so use accents for big type, shapes and strokes, and ink for small text.
21
+
22
+ ## Fonts that work everywhere
23
+
24
+ Built into the engine (no file, no network, the same on every machine, and they win over fonts installed on the machine):
25
+ `Inter` (sans, 100-900, italic), `JetBrains Mono` (mono, 100-800, italic; ligatures are off inside `code`/`pre`), `Playfair Display`
26
+ (display serif, 400-900, italic), `Space Grotesk` (grotesk display, 300-700), `Bebas Neue` (condensed caps, 400), `Nunito`
27
+ (rounded, friendly, 200-1000), `Caveat` (handwriting, 400-700). Use these names first in every stack. For a brand font, put the file next to the HTML and load it with
28
+ `@font-face`. A Google Fonts `<link>` also works (the render waits for it) but needs the network on every render.
29
+
30
+ - Sans: `Inter, system-ui, sans-serif`
31
+ - Display serif: `"Playfair Display", Georgia, serif`
32
+ - Mono: `"JetBrains Mono", Menlo, monospace`
33
+ - Grotesk display: `"Space Grotesk", Inter, sans-serif`
34
+ - Condensed/impact: `"Bebas Neue", Impact, sans-serif`
35
+
36
+ Your own `@font-face` with a built-in name (say a local `Inter`): **yours wins for every weight and style it declares**
37
+ (it comes after the engine's faces). Weights or styles it does not declare still come from the built-in face of that name:
38
+ a file declared only for 400 gives you your 400, and `font-weight: 700` or italic text uses the built-in Inter 700 / italic.
39
+ That is fine when your file is the same font. If it is a different font, give it its own name (`font-family: 'Brand'`)
40
+ so nothing mixes, and declare every weight you use (or one variable file with `font-weight: 100 900`).
41
+
42
+ Text weight: the preset draws text with grayscale antialiasing and `text-rendering: geometricPrecision` (exact glyph
43
+ sizes, so text that scales or zooms does not jump between sizes). It looks a little lighter than a browser or app on
44
+ a desktop. To match a brand's own screenshots or renders, set `body { -webkit-font-smoothing: auto; text-rendering: auto; }`
45
+ (or use the next weight up); keep the default for text the camera zooms.
46
+
47
+ Numbers that change (`data-count`, clocks, scores): in natural motion a `data-count` element gets
48
+ `font-variant-numeric: tabular-nums` (all digits as wide) unless your CSS sets `font-variant-numeric`. That matters: Inter and
49
+ Space Grotesk digits are proportional by default (a 1 is a third narrower than an 8) and have tabular forms; JetBrains Mono,
50
+ Bebas Neue and Caveat digits are already all as wide; Playfair Display has none (use `data-count-fixed`; classic motion keeps
51
+ the font's default digits). The number still grows wider as digits are added
52
+ (9 → 10 → 1,000), which moves centred text. Add `data-count-fixed` to stop all sideways movement: each digit sits in a cell as
53
+ wide as the font's widest digit (works with any font, also ones without tabular digits like Playfair's old-style figures), and
54
+ the box keeps the width of the longest number from the first frame, with the digits right-aligned in it (the units digit
55
+ never moves; prefix and suffix stay put). For your own counters in `gm.onFrame`, set `font-variant-numeric: tabular-nums`.
56
+
57
+ ## 1 Midnight Tech
58
+
59
+ For: product launches, AI/dev tools, SaaS promos, tech explainers.
60
+ - Palette: `--bg #0b0d12` `--surface #151a23` `--ink #f2f4f8` `--muted #9aa3b2` `--accent #5b8cff` `--accent-2 #38e1c7` `--alert #ff5c7a`.
61
+ - Type: sans, titles weight 800, `letter-spacing: -0.03em`, 120–200 px; mono for code and numbers.
62
+ - Background: 2–3 large blurred glow blobs in the accents at 20–35% opacity (still blobs are almost free to render), fine grain, or `grid-bg` with `glow` in the accent. Screenshots tilted in 3D with a soft shadow and a 1 px light border.
63
+ - Motion: crisp. Entrances `out-expo` 0.5–0.7 s, `mask-up` word reveals, `push-*`/`whip-*`/`zoom-through` transitions, camera push 1.06 on every scene, one burst + shake hit on the reveal.
64
+
65
+ ## 2 Paper Whiteboard
66
+
67
+ For: whiteboard explainers, "how it works", teaching, sketch-note stories.
68
+ - Palette: `--bg #fbfaf6` `--ink #1e1e1e` `--muted #5c5f66` `--accent #1971c2` `--accent-2 #e8590c` `--good #2f9e44` `--highlight #ffd43b` (hatch fills: `#ffd43b`, `#74c0fc`, `#ffc9c9`).
69
+ - Type: `Caveat` 600–700 for titles and labels (≥ 48 px in world units, ≥ 40 px on screen), sans only for tiny UI text inside drawn screens.
70
+ - Background: plain paper; optional very faint dot grid. Everything on one big world.
71
+ - Motion: drawn, not animated: `data-sketch` shapes with `data-enter="draw"`, `write` for text, `data-sketch-mark` highlights; strokes 5–7 px; one `gm.camera` moving close from step to step, a short pull-back at the end. Easing `in-out-sine`/`in-out-cubic` for the camera, no bouncing.
72
+
73
+ ## 3 Editorial
74
+
75
+ For: data stories, documentaries, reports, premium brands, quotes.
76
+ - Palette: `--bg #f3eee4` (warm paper) `--ink #1b1a17` `--muted #5f5a50` `--accent #c2410c` (burnt orange) `--accent-2 #1e40af` (ink blue).
77
+ - Type: display serif for headlines (96–180 px, weight 600–700, tight leading 0.95), sans for labels and numbers; big numbers in the serif.
78
+ - Background: flat paper with a subtle vignette; thin rules (2–3 px) and a strict margin grid; photos in duotone or with a thin frame.
79
+ - Motion: calm and confident. `out-quart` 0.8–1.0 s, line-by-line `mask-up` (`data-split="lines"`), `wipe-*` and `push-*` transitions, slow camera push 1.03–1.05, charts that draw with holds on the key points. No elastic or bouncy eases.
80
+
81
+ ## 4 Bold Pop
82
+
83
+ For: vertical social clips, year recaps, celebrations, youth brands, music-driven edits.
84
+ - Palette: `--bg #fff7e8` `--ink #1a1a2e` `--pink #e11d74` `--blue #2a6df4` `--teal #0f8a7e` `--yellow #ffd23f`. Color-blocked scenes: whole backgrounds in pink, blue or yellow (ink on yellow, white on pink and blue).
85
+ - Type: heavy condensed or sans weight 900, UPPERCASE for short words, 140–260 px, one word per beat.
86
+ - Background: flat color blocks, big geometric shapes (circles, stars, blobs) drifting and rotating behind the type.
87
+ - Motion: punchy. `pop`, `drop`, `elastic`, `back.out(1.8)`, fast `cover-*`/`push-*` transitions (0.35–0.5 s) on the beat, `data-pulse="beat 0.06"` on the hero, confetti on the payoff. A change every 0.4–0.8 s.
88
+
89
+ ## 5 Swiss Grid
90
+
91
+ For: corporate explainers, finance, architecture, B2B, clean information design.
92
+ - Palette: `--bg #f5f5f2` `--ink #111111` `--muted #5a5a5a` `--accent #d62828` (one red) `--navy #1d3557`.
93
+ - Type: sans (Helvetica/Inter), flush left, strong size contrast (a 200 px number next to 36 px labels), lots of alignment to a 12-column grid.
94
+ - Background: flat; thin black rules that `grow-x`; solid red blocks as the only decoration.
95
+ - Motion: precise and mechanical. `in-out-cubic`/`out-quart`, `wipe-*`, `grow-x`/`grow-y`, `mask-left`; transitions `wipe-left` or `push-left`; camera moves in straight lines; no overshoot.
96
+
97
+ ## 6 Neon Night
98
+
99
+ For: gaming, music, events, hype trailers, energetic promos.
100
+ - Palette: `--bg #07060d` `--ink #f5f3ff` `--muted #a9a3c7` `--magenta #ff2bd6` `--cyan #00e5ff` `--lime #b6ff3b`.
101
+ - Type: wide or condensed sans weight 800–900; glow with `text-shadow: 0 0 calc(var(--px)*24) var(--magenta)`; mono for HUD details.
102
+ - Background: dark with neon lines, a perspective grid, light streaks; grain and a vignette.
103
+ - Motion: aggressive. Beat-cut `whip-*` and `zoom-through`, `scramble` and `steps(4)` glitches, shake on hits, `sparks` bursts, `--motion-blur 8` for the final render.
104
+
105
+ ## 7 Soft SaaS
106
+
107
+ For: friendly app and UI demos, onboarding, consumer products, feature tours.
108
+ - Palette: `--bg #f5f3ff` `--surface #ffffff` `--ink #1e1b4b` `--muted #5b5891` `--accent #4f46e5` `--pink #db2777` `--success #15803d`.
109
+ - Type: sans weight 700 for titles (96–140 px), 500 for UI; rounded corners 20–28 px everywhere.
110
+ - Background: a soft gradient (`gradient-bg` with light `c1/c2/c3` and dark ink on top) or white with large pastel blobs; cards with soft long shadows.
111
+ - Motion: friendly. `spring`/`gentle`, `pop` for chips and toasts, `gm.fly` and `morph` to move UI between states, `cursor` with `show="auto"`, the camera gliding over the UI and pushing in on the part that changes.
112
+
113
+ ## 8 Terminal
114
+
115
+ For: code explainers, developer tools, CLI demos, security and infra topics.
116
+ - Palette: `--bg #0d1117` `--surface #161b22` `--ink #e6edf3` `--muted #8b949e` `--green #3fb950` `--purple #d2a8ff` `--orange #ffa657` `--blue #58a6ff`.
117
+ - Type: mono for code and labels (code ≥ 32 px on screen), sans or mono weight 800 for titles.
118
+ - Background: near-black with a faint grid or scanline texture; the `code-window` block restyled to these tokens.
119
+ - Motion: `data-type` typing, `scramble` reveals, `steps()` blinking carets; the camera pushes into the line that matters while the other lines dim (`focus` on `code-window`); hand-drawn arrows (`gm.connect` with `sketch: true`) or numbered callouts for annotations.
120
+
121
+ ## Choosing a look
122
+
123
+ | Brief | Look |
124
+ |---|---|
125
+ | Launch, AI tool, SaaS promo | Midnight Tech (or Soft SaaS for a consumer app) |
126
+ | Whiteboard, "how X works" | Paper Whiteboard |
127
+ | Data story, report, documentary | Editorial (or Midnight Tech for a startup's metrics) |
128
+ | Vertical short, year recap, celebration | Bold Pop |
129
+ | Corporate, finance, B2B explainer | Swiss Grid |
130
+ | Music, gaming, event hype | Neon Night |
131
+ | App or UI walkthrough | Soft SaaS |
132
+ | Code explainer, CLI, devtools | Terminal |
133
+ | Logo sting | The brand's own colors on Midnight Tech or Swiss Grid structure |
@@ -0,0 +1,85 @@
1
+ # Living backgrounds and ambient motion
2
+ Use for: every video that should never look frozen: title cards, promos, captions, holds and end cards; dark or light brands.
3
+
4
+ ## What makes it look premium
5
+ - **Three depths, three speeds.** Far: 2-3 huge soft color blobs (1300 px, 7-11 s loops). Middle: a faint grid drifting one cell per 8 s. Near: 20-30 dust dots rising 12-40 px/s. Different speeds read as depth.
6
+ - **Periods that never line up.** Blob loops of 7, 9 and 11 s (`sine.inOut`, yoyo) and 160-280 px travel, so the pattern never visibly repeats. Constant drifts (grid, dust) are `linear`; nothing else is.
7
+ - **Quiet by default.** Blobs at 50-60% opacity on dark, 70% on light; grid 6-7%; dust 15-45% with a slow twinkle (`gm.noise`). If you notice the background before the headline, halve it.
8
+ - **It reacts to the story.** On a scene change, blobs swing to a new layout (1.4 s `power3.inOut`, the second one 50 ms later), a thin ring pulses out (scale 0.4 → 2.2, 1.1 s `expo.out`) and the grid flashes 6% → 14% → 6%. One reaction per cut; between cuts it only drifts.
9
+ - **Persistent across cuts.** The background lives outside the `data-sequence`, so scenes push over it and it keeps flowing: one world, not a reset per scene.
10
+ - **Edges fade.** Mask the grid with a radial gradient so it dissolves toward the frame edges; add 4-5% film grain (`overlay` on dark, `multiply` on light) to kill banding.
11
+ - **Cheap to render.** Soft blobs are `radial-gradient(closest-side, color, transparent)`, not `filter: blur()`; dust uses `transform` only.
12
+
13
+ ## Recipe
14
+ ```html
15
+ <html data-width="1920" data-height="1080" data-fps="30">
16
+ <style>
17
+ :root { --bg:#0a0c14; --ink:#f2f4f8; --c1:#5b4bff; --c2:#00b3ff; --c3:#ff3d8b; --line:#ffffff; --glow:.55; --grid:.06; }
18
+ body { background: var(--bg); color: var(--ink); font-family: Inter, system-ui, sans-serif; }
19
+ #ambient { position:absolute; inset:0; overflow:hidden; }
20
+ .blob { position:absolute; width:1300px; height:1300px; opacity: var(--glow); background: radial-gradient(closest-side, var(--c), transparent); }
21
+ #b1 { --c: var(--c1); left:-450px; top:-500px; } #b2 { --c: var(--c2); left:1050px; top:-150px; }
22
+ #b3 { --c: var(--c3); left:300px; top:450px; opacity: calc(var(--glow) * .7); }
23
+ #grid { position:absolute; inset:-80px; opacity: var(--grid);
24
+ background: linear-gradient(var(--line) 1px, transparent 1px) 0 0 / 80px 80px, linear-gradient(90deg, var(--line) 1px, transparent 1px) 0 0 / 80px 80px;
25
+ mask-image: radial-gradient(ellipse 60% 60% at 50% 50%, #000 30%, transparent 100%); }
26
+ .dot { position:absolute; width:6px; height:6px; border-radius:50%; background: var(--line); }
27
+ #ring { position:absolute; left:960px; top:540px; width:700px; height:700px; margin:-350px; border-radius:50%; border:2px solid var(--c2); opacity:0; }
28
+ #grain { position:absolute; inset:0; opacity:.05; mix-blend-mode: overlay;
29
+ background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence baseFrequency='.9'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E"); }
30
+ .scene { display:grid; place-items:center; } h1 { font-size:150px; font-weight:800; letter-spacing:-.035em; margin:0; }
31
+ </style>
32
+ <div id="ambient">
33
+ <div class="blob" id="b1"></div><div class="blob" id="b2"></div><div class="blob" id="b3"></div>
34
+ <div id="grid"></div><div id="dots"></div><div id="ring"></div><div id="grain"></div>
35
+ </div>
36
+ <main data-sequence data-transition="push-left 0.7">
37
+ <section class="scene" id="s1" data-duration="4" data-camera="push 1.05"><h1 data-split="words" data-enter="mask-up" data-stagger="0.07">Always moving</h1></section>
38
+ <section class="scene" id="s2" data-duration="4" data-camera="push 1.06, rotate -1"><div style="text-align:center">
39
+ <h1 data-split="words" data-enter="mask-up" data-stagger="0.07">Never loud</h1>
40
+ <p style="font-size:44px;opacity:.75;margin:24px 0 0" data-enter="fade-up 0.6 1.4">The background supports, the subject leads.</p></div></section>
41
+ </main>
42
+ <script>
43
+ // 1. Far layer: each blob on its own loop (7/9/11 s), the grid drifts one cell per 8 s (seamless).
44
+ [['#b1', 240, 160, 9], ['#b2', -260, 180, 11], ['#b3', 280, -150, 7]].forEach(([el, x, y, d]) =>
45
+ gm.to(el, { x, y, scale: 1.15, duration: d / 2, ease: 'sine.inOut', repeat: -1, yoyo: true }));
46
+ gm.to('#grid', { x: -80, y: -80, duration: 8, ease: 'none', repeat: -1 });
47
+ // 2. Near layer: dust rises, sways and twinkles; everything computed from t (seekable).
48
+ const dots = Array.from({ length: 28 }, (_, i) => {
49
+ const d = document.createElement('i'); d.className = 'dot'; document.getElementById('dots').append(d);
50
+ return { d, x: gm.random('x' + i) * 1920, y: gm.random('y' + i) * 1080, v: 12 + gm.random('v' + i) * 28, s: .5 + gm.random('s' + i) };
51
+ });
52
+ gm.onFrame(t => dots.forEach((p, i) => {
53
+ p.d.style.transform = `translate(${p.x + Math.sin(t * .4 + i) * 20}px, ${gm.wrap(p.y - t * p.v, -20, 1100)}px) scale(${p.s})`;
54
+ p.d.style.opacity = .15 + .3 * gm.noise(t * .5 + i * 7);
55
+ }));
56
+ // 3. React to the cut: blobs swing to a new layout, a ring pulses out, the grid flashes.
57
+ const cut = gm.timing('#s2').start; // video time the push starts
58
+ gm.timeline()
59
+ .to('#b1', { xPercent: 45, yPercent: 30, duration: 1.4, ease: 'power3.inOut' }, cut)
60
+ .to('#b2', { xPercent: -50, yPercent: 35, duration: 1.4, ease: 'power3.inOut' }, cut + .05)
61
+ .fromTo('#ring', { opacity: .5, scale: .4 }, { opacity: 0, scale: 2.2, duration: 1.1, ease: 'expo.out' }, cut + .2)
62
+ .to('#grid', { opacity: .14, duration: .2, ease: 'power2.out' }, cut + .15)
63
+ .to('#grid', { opacity: .06, duration: .9, ease: 'sine.inOut' }, cut + .35);
64
+ </script>
65
+ ```
66
+
67
+ ## Make it yours
68
+ - **Light brand:** `--bg:#f5f2ec; --ink:#16181d; --c1:#ffb38a; --c2:#8ec5ff; --c3:#c7a6ff; --line:#16181d; --glow:.7; --grid:.07`, grain `mix-blend-mode: multiply`. Pastel blobs on off-white; dark ink keeps contrast.
69
+ - **Calm / corporate:** one or two blobs, 11-15 s loops, no dust, no ring; the cut reaction is only the blob swing.
70
+ - **Energetic / tech:** 5-7 s loops, grid 10%, dust 40-60 dots at 30-60 px/s, ring + `gm.burst(..., { kind: 'sparks', global: true, at: cut })` on the cut, blobs pulse on the beat (`data-pulse="beat 0.04"` on `#ambient`).
71
+ - **Luxury:** near-black or ivory, one warm blob at 30%, no grid, dust 10 dots at 8 px/s, 2% grain, slow 1.8 s swings.
72
+
73
+ ## Variations
74
+ - Ready-made: `<gm-include src="blocks/gradient-bg.html" c1="…" c2="…" c3="…">` or `blocks/grid-bg.html` (drift on global time, behind every scene).
75
+ - Living blob shape: `gm.onFrame(t => path.setAttribute('d', gm.shape.blob(420, { t: t * .3 })))` on an SVG path with a gradient fill.
76
+ - Inside a camera world, put the ambient layer next to the world with `data-parallax="0.3"` so it drifts slower than the content.
77
+ - Brand color shift per scene: tween `--c1` at each cut, e.g. `tl.to('#b1', { '--c': '#ff7a1a', duration: 1.2 }, cut)`.
78
+
79
+ ## Pitfalls
80
+ - A living background does not hide a frozen foreground: `check` still reports `still` when only the ambient layer moves for 2.5 s (1.9 s at the end). Give each scene a second beat (a subline, a counter, a camera move).
81
+ - `filter: blur(150px)` on big layers renders slowly at 1080p and 4K; use radial gradients or a small canvas scaled up (see `blocks/gradient-bg.html`).
82
+ - Saturated blobs behind colored text fail contrast; keep blobs mid-dark under white text and pastel under dark text, or add a soft scrim behind the headline.
83
+ - Particles crossing the headline read as dirt: keep dust small (≤ 6 px), low opacity, or mask it out of the text area.
84
+ - One shared loop period for everything (all 8 s) makes the frame "breathe" in sync and look mechanical; use co-prime periods.
85
+ - The grid flash ends at a fixed `.06`; if you change `--grid`, change that number too.
@@ -0,0 +1,88 @@
1
+ # Camera moves
2
+ Use for: any scene that should feel filmed, not laid out: slow pushes on a title, a whip to the next idea, a push into a detail, following something that travels, an impact.
3
+
4
+ ## What makes it look premium
5
+ - **Every move has a reason.** The camera pushes because something matters, follows because something moves, shakes because something hits. Start a move when its reason appears, not before.
6
+ - **Slow push/pull: 1.04–1.10 over 2–4 s, `in-out-sine`.** It should be felt more than seen. A push that is visible as a zoom (> 1.15 in 2 s) reads as a slideshow effect.
7
+ - **Whip pan: 0.35–0.5 s, `in-out-expo`, plus directional blur** that peaks mid-move (40–50 px along the move, 0 across it). Without blur a fast pan strobes; uniform `blur()` looks out of focus instead of fast.
8
+ - **Push in to a detail and back out: 0.8–1 s in, hold ≥ 0.5 s, 0.6–0.8 s out** (`in-out-cubic`). Pair it with a focus pull: the rest blurs 8–12 px and dims to 30–40% while you are close.
9
+ - **Follow with lead room and lag.** `follow` + `offset` 300–450 px ahead of the direction of travel, `lag: 0.2–0.3` so the frame trails like an operator. The subject moves with `in-out-quad`, the camera with its own lag: two curves, not one.
10
+ - **Impact shake: 10–16 px, 0.3–0.4 s**, on the frame of the hit, with a ring (scale 0.5→2.2, opacity 0.9→0, 0.7 s `out-cubic`) and a 1.15× pop on the target. Shake alone looks like a glitch.
11
+ - **Ambient layer.** A background outside the world with `data-parallax="0.2–0.3"` drifts less than the world, so every move shows depth.
12
+
13
+ ## Recipe
14
+ One camera world, five moves in 10 s: push, whip, push-in with focus pull, follow, impact.
15
+ ```html
16
+ <html data-width="1920" data-height="1080" data-fps="30" data-duration="10"><head><style>
17
+ :root { --bg: #0d1017; --ink: #f2efe8; --muted: #8a90a0; --accent: #ff6a3d; }
18
+ body { background: var(--bg); color: var(--ink); font-family: 'Space Grotesk', sans-serif; }
19
+ .glow { position: absolute; inset: -20%; background: radial-gradient(40% 35% at 30% 40%, color-mix(in oklab, var(--accent) 22%, transparent), transparent 70%); }
20
+ #world { position: absolute; left: 0; top: 0; width: 3400px; height: 1800px; filter: url(#whipblur); }
21
+ #title { position: absolute; left: 140px; top: 430px; margin: 0; font-size: 170px; line-height: .95; letter-spacing: -.04em; }
22
+ #title em { color: var(--accent); font-style: normal; }
23
+ #card { position: absolute; left: 1750px; top: 260px; width: 1000px; height: 600px; border-radius: 28px;
24
+ background: color-mix(in oklab, var(--ink) 6%, var(--bg)); box-shadow: 0 40px 90px -30px #000; }
25
+ #card .rest { position: absolute; left: 70px; top: 70px; font-size: 48px; color: var(--muted); }
26
+ #card .bars { position: absolute; left: 70px; bottom: 70px; display: flex; gap: 18px; align-items: end; }
27
+ #card .bars i { width: 48px; border-radius: 8px; background: color-mix(in oklab, var(--ink) 25%, transparent); }
28
+ #detail { position: absolute; right: 80px; top: 150px; font-size: 200px; font-weight: 700; color: var(--accent); }
29
+ #lane, #fill { position: absolute; left: 1700px; top: 1418px; width: 1600px; height: 6px; background: color-mix(in oklab, var(--ink) 18%, transparent); }
30
+ #fill { background: var(--accent); transform-origin: left; }
31
+ #dot { position: absolute; left: 1650px; top: 1371px; width: 100px; height: 100px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 70px var(--accent); }
32
+ #goal { position: absolute; left: 3300px; top: 1421px; }
33
+ #goal b, #ring { position: absolute; left: -60px; top: -60px; width: 120px; height: 120px; border-radius: 50%; border: 6px solid var(--ink); }
34
+ #ring { border-color: var(--accent); opacity: 0; }
35
+ #goal span { position: absolute; left: -200px; width: 400px; top: -230px; text-align: center; font-size: 110px; font-weight: 700; }
36
+ </style></head><body>
37
+ <svg width="0" height="0" style="position:absolute"><filter id="whipblur" x="-10%" width="120%"><feGaussianBlur id="whipk" stdDeviation="0 0"/></filter></svg>
38
+ <section class="scene" data-duration="10">
39
+ <div class="glow" data-parallax="0.25"></div>
40
+ <div id="world">
41
+ <h1 id="title">Move with<br><em>intent.</em></h1>
42
+ <div id="card"><div class="rest">Weekly retention</div>
43
+ <div class="bars"><i style="height:120px"></i><i style="height:180px"></i><i style="height:150px"></i><i style="height:240px"></i><i style="height:300px"></i></div>
44
+ <div id="detail">92%</div></div>
45
+ <div id="lane"></div><div id="fill"></div>
46
+ <div id="goal"><b></b><div id="ring"></div><span>Launch</span></div><div id="dot"></div>
47
+ </div>
48
+ </section>
49
+ <script>
50
+ gm.camera('#world')
51
+ .shot(0, { fit: '#title', padding: 260 }) // a touch wide
52
+ .shot(0.05, { fit: '#title', padding: 150, duration: 2.3, ease: 'in-out-sine' }) // slow push, felt not seen
53
+ .shot(2.4, { fit: '#card', padding: 120, duration: 0.45, ease: 'in-out-expo' }) // whip pan
54
+ .shot(3.6, { fit: '#detail', padding: 90, duration: 0.9, ease: 'in-out-cubic' }) // push in to the detail
55
+ .shot(5.0, { fit: '#card', padding: 120, duration: 0.8, ease: 'in-out-cubic' }) // and back out
56
+ .shot(6.0, { follow: '#dot', zoom: 1.25, offset: [420, -120], duration: 0.9, lag: 0.25 }) // lead room
57
+ .shake(8.55, { amount: 16, duration: 0.4 }); // impact on arrival
58
+ const k = document.getElementById('whipk'); // horizontal-only blur, peaks mid-whip (2.4 → 2.85)
59
+ gm.onFrame(t => k.setAttribute('stdDeviation', gm.interpolate(t, [2.4, 2.6, 2.85], [0, 46, 0]) + ' 0'));
60
+ gm.timeline('#world')
61
+ .to('#card .rest, #card .bars', { filter: 'blur(10px)', opacity: 0.35, duration: 0.7 }, 3.7) // focus pull
62
+ .to('#card .rest, #card .bars', { filter: 'blur(0px)', opacity: 1, duration: 0.6 }, 5.0)
63
+ .to('#dot', { x: 1600, duration: 2.4, ease: 'in-out-quad' }, 6.15) // the subject motivates the move
64
+ .fromTo('#fill', { scaleX: 0 }, { scaleX: 1, duration: 2.4, ease: 'in-out-quad' }, 6.15)
65
+ .fromTo('#ring', { opacity: 0.9, scale: 0.5 }, { opacity: 0, scale: 2.2, duration: 0.7, ease: 'out-cubic' }, 8.55)
66
+ .fromTo('#goal span', { scale: 1 }, { keyframes: [{ scale: 1.15, duration: 0.12 }, { scale: 1, duration: 0.4, ease: 'out-back' }] }, 8.55);
67
+ </script></body></html>
68
+ ```
69
+
70
+ ## Make it yours
71
+ - **Calm / corporate:** pushes 1.04 over 3–4 s, no whip (use a 1.2 s `in-out-cubic` move), no shake, `float 8` while holding.
72
+ - **Energetic / punchy:** whips 0.3 s with 50–60 px blur, push-ins 0.5 s `expo.out`, shake 16–20 px on every hit, `handheld 6` between moves.
73
+ - **Luxury:** very slow (push 1.03 over 5 s), long focus pulls (1–1.2 s, 14 px blur), no shake, slight `rotate: -2` on a shot.
74
+ - **Playful:** overshooting arrivals (leave the shot's default overshoot on), bank 3–5° into whips.
75
+
76
+ ## Variations
77
+ - Bank/rotate into a move: `.shot(4, { fit: '#b', rotate: -4, duration: 0.8 })`, then `rotate: 0` on the next shot.
78
+ - No world needed: `data-camera="push 1.06, float"` on a scene; `"pull 1.08"` for a reveal; `"handheld 6"` for documentary.
79
+ - Impact without a world: `data-camera="shake 14 2.1-2.5"` (window in seconds of the clip).
80
+ - Vertical whip: blur `'0 46'` instead of `46 + ' 0'`, and arrange the world top to bottom.
81
+
82
+ ## Pitfalls
83
+ - **Whip without blur** strobes (the text jumps 300+ px per frame). Add the SVG directional blur; a uniform `filter: blur()` reads as defocus.
84
+ - **Moves with no reason** (a push on nothing, a pan across empty space) look like a screensaver. Keep subjects close in the world; check the mid-whip frame with `snapshot --every 0.1 --from A --to B`.
85
+ - **Push-in that cuts its neighbour:** a label half in frame reads as a mistake. Fit the detail with its label, or blur/dim the rest.
86
+ - **`camera-stop` note** after a push-in hold is fine when the hold is meant (≥ 0.5 s, as here); chain shots back-to-back when it is not.
87
+ - **Shake on calm brands** or on every beat cheapens it; one or two hits per video.
88
+ - `filter: url(#…)` on a very large world costs render time; turn the blur to `0 0` outside whips (the interpolate above does).
@@ -0,0 +1,89 @@
1
+ # Word-by-word captions and vertical clips
2
+ Use for: TikTok/Reels/Shorts, voice-over clips, any brief with word-timed captions, a hook title, or "subtitles that pop".
3
+
4
+ ## What makes it look premium
5
+ - **Phrases, not sentences.** 1-3 words per caption, broken at pauses > 0.3 s and after `. ! ? ,`. 92 px, weight 850, centered; never more than ~6 words on screen.
6
+ - **Each word pops 60 ms before it is spoken** (y 36 → 0, scale 0.7 → 1, 0.28 s `back.out(2.2)`). Exactly on the timestamp reads late; 60 ms early reads in sync.
7
+ - **One highlight pill per phrase that slides** to the spoken word (0.12 s `power3.out`, starting 0.1 s early), not a box per word fading on and off (two half-boxes at once look muddy). The spoken word turns dark on the pill and scales 1.06; it hands back over 0.06 s.
8
+ - **Fast exits.** The phrase leaves 0.14 s `power2.in` upward, 0.16 s before the next one. Two phrases never share the screen.
9
+ - **Hook on frame 0.** The title is visible at frame 0 (it is the thumbnail), lands by 0.5 s, then steps back (scale 0.86, 0.6 s `power3.inOut`) to hand focus to the captions.
10
+ - **The background answers the voice.** On each phrase change the big glow jumps (0.9 s `power3.out`) and the hue turns 25°. The beat is felt without anything new to read.
11
+ - **Layers:** a slow camera on the whole frame (`push 1.04, float 10`), a second glow breathing (2.4 s `sine.inOut` yoyo), soft text-shadow on idle words.
12
+ - **Safe zones (1080×1920):** keep key text out of the top 220 px and bottom 380 px, and 60 px in from the sides. Hook at y ≈ 300, captions at y ≈ 1180; the middle band is for the subject.
13
+
14
+ ## Recipe
15
+ ```html
16
+ <html data-width="1080" data-height="1920" data-fps="30" data-duration="5" data-safe-zone="shorts">
17
+ <style>
18
+ :root { --bg:#0d0f1a; --ink:#f4f5fa; --accent:#ffd23f; --accent-2:#6d5dfc; --font: Inter, system-ui, sans-serif; }
19
+ body { background: var(--bg); color: var(--ink); font-family: var(--font); overflow: hidden; }
20
+ .glow { position:absolute; width:1300px; height:1300px; background: radial-gradient(closest-side, var(--c), transparent); }
21
+ #g1 { --c: var(--accent-2); left:-500px; top:0; opacity:.6; } #g2 { --c: var(--accent); left:300px; top:900px; opacity:.22; }
22
+ #hook { position:absolute; left:90px; right:90px; top:300px; margin:0; font-size:128px; font-weight:900; line-height:.95;
23
+ letter-spacing:-.04em; text-transform:uppercase; transform-origin:0 0; }
24
+ #hook .w { display:inline-block; } #hook em { font-style:normal; color: var(--accent); }
25
+ .cap { position:absolute; left:70px; right:70px; top:1180px; text-align:center; isolation:isolate;
26
+ font-size:92px; font-weight:850; line-height:1.1; letter-spacing:-.02em; }
27
+ .cap .w { display:inline-block; padding:0 .14em; --hl:0; /* --hl: 0 idle, 1 spoken */
28
+ color: color-mix(in oklab, var(--bg) calc(var(--hl) * 100%), var(--ink));
29
+ text-shadow: 0 6px 24px rgb(0 0 0 / calc(.5 - var(--hl) * .5)); }
30
+ .pill { position:absolute; left:0; top:.02em; height:1.06em; border-radius:.2em; z-index:-1; background: var(--accent);
31
+ box-shadow: 0 10px 40px color-mix(in oklab, var(--accent) 45%, transparent); }
32
+ </style>
33
+ <main class="scene" data-camera="push 1.04, float 10">
34
+ <div id="bg"><div class="glow" id="g1"></div><div class="glow" id="g2"></div></div>
35
+ <h1 id="hook"><span class="w">Stop</span> <span class="w">editing</span> <span class="w"><em>by&nbsp;hand</em></span></h1>
36
+ <div id="caps"></div>
37
+ </main>
38
+ <script>
39
+ const words = [['Your',.5,.7],['video',.7,1],['is',1,1.1],['just',1.1,1.35],['HTML.',1.35,1.9],['Write',2.3,2.55],
40
+ ['it',2.55,2.65],['once,',2.65,3.1],['render',3.25,3.6],['anywhere.',3.6,4.3]].map(([text, start, end]) => ({ text, start, end }));
41
+ const phrases = [[]]; // 1. break on a pause > 0.3 s, after . ! ? , or at 3 words
42
+ words.forEach((w, i) => { const cur = phrases.at(-1), prev = words[i - 1];
43
+ if (cur.length && (cur.length === 3 || w.start - prev.end > .3 || /[.!?,]$/.test(prev.text))) phrases.push([]);
44
+ phrases.at(-1).push(w); });
45
+ const tl = gm.timeline();
46
+ // 2. Hook: visible at frame 0 and already moving, then it steps back.
47
+ tl.from('#hook .w', { y: 70, scale: 1.25, opacity: .35, filter: 'blur(10px)', duration: .5, ease: 'expo.out', stagger: .06 }, 0)
48
+ .to('#hook', { scale: .86, y: -40, duration: .6, ease: 'power3.inOut' }, .45);
49
+ const caps = phrases.map(ph => { const el = document.createElement('div'); el.className = 'cap';
50
+ el.innerHTML = '<i class="pill"></i>' + ph.map(w => `<span class="w">${w.text}</span>`).join(' ');
51
+ document.getElementById('caps').append(el); return el; });
52
+ gm.wait(document.fonts.ready.then(() => phrases.forEach((ph, p) => { // measure words once fonts are in
53
+ const el = caps[p], spans = el.querySelectorAll('.w'), pill = el.querySelector('.pill'), next = phrases[p + 1];
54
+ const box = i => ({ x: spans[i].offsetLeft, width: spans[i].offsetWidth });
55
+ tl.fromTo(pill, { ...box(0), scale: .5, opacity: 0 }, { scale: 1, opacity: 1, duration: .22, ease: 'back.out(2)' }, ph[0].start - .04);
56
+ ph.forEach((w, i) => {
57
+ tl.fromTo(spans[i], { opacity: 0, y: 36, scale: .7 }, { opacity: 1, y: 0, scale: 1, duration: .28, ease: 'back.out(2.2)' }, w.start - .06);
58
+ if (i) tl.to(pill, { ...box(i), duration: .12, ease: 'power3.out' }, w.start - .1); // 3. pill slides
59
+ tl.to(spans[i], { '--hl': 1, scale: 1.06, duration: .06 }, w.start - .03) // 4. spoken word
60
+ .to(spans[i], { '--hl': 0, scale: 1, duration: .06 }, ph[i + 1] ? ph[i + 1].start - .1 : w.end + .3);
61
+ });
62
+ if (!next) tl.to(pill, { opacity: 0, scale: .9, duration: .2, ease: 'power2.in' }, ph.at(-1).end + .3);
63
+ else tl.to(el, { y: -50, opacity: 0, duration: .14, ease: 'power2.in' }, next[0].start - .16); // 5. fast exit
64
+ tl.to('#g1', { x: gm.random('x' + p) * 500 - 150, y: gm.random('y' + p) * 600 - 300, duration: .9, ease: 'power3.out' }, ph[0].start - .1)
65
+ .to('#bg', { filter: `hue-rotate(${p * 25}deg)`, duration: .6, ease: 'sine.inOut' }, ph[0].start - .1); // 6. bg reacts
66
+ })));
67
+ gm.to('#g2', { scale: 1.25, duration: 2.4, ease: 'sine.inOut', repeat: -1, yoyo: true }); // 7. ambient
68
+ </script>
69
+ ```
70
+ Real timestamps: load a whisper-style JSON with `gm.wait(fetch('words.json').then(r => r.json()).then(build))` and build the same timeline inside.
71
+
72
+ ## Make it yours
73
+ - **Calm / corporate:** no overshoot (`power3.out`), 0.35 s pops, pill as an underline (`height: .12em; top: 1em`), hue turn 0, Inter 700 mixed case.
74
+ - **Energetic / punchy:** `back.out(3)`, pop from scale 0.4 with rotate ±6°, 1-2 words per phrase, `text-transform: uppercase`, hue turn 40°, add a `shake 8 2.1-2.4` (window around the key word) to the `data-camera` list.
75
+ - **Playful:** a pill color per phrase (`gm.pick(['#ffd23f','#5ef2c2','#ff7ab6'], 'c' + p)`), `bouncy` ease, rotate the pill ±3°.
76
+ - **Luxury:** no pill; the spoken word goes from 40% to 100% opacity, `"Playfair Display"` 80 px, slower 0.45 s rises, `sine.out`.
77
+
78
+ ## Variations
79
+ - **Captions only, no code:** `<gm-include src="blocks/captions.html" file="captions.json" words="3">` (pages and highlights for you; less control).
80
+ - **Phrase lands as one unit** (fast talkers, > 3.5 words/s): pop the whole `.cap` and only slide the pill per word.
81
+ - **Keyword emphasis:** for words in a set, `tl.to(span, { scale: 1.3, rotate: -4, duration: .2, ease: 'back.out(3)' }, w.start)` and fire `gm.burst(..., { kind: 'sparks', global: true, at: w.start })`.
82
+ - **Line that re-centres as it grows:** the word-grow pattern in `references/motion-design.md` §6.
83
+
84
+ ## Pitfalls
85
+ - `phrases[p + 1]` is an array: use `next[0].start`. A wrong time silently leaves old phrases on screen (`text-overlap` everywhere).
86
+ - Measuring `offsetLeft` before fonts load puts the pill in the wrong place: measure inside `gm.wait(document.fonts.ready…)`.
87
+ - Captions over the subject or in the bottom 380 px: `check` warns `unsafe-zone` with `data-safe-zone="shorts"`; keep the caption band at y 1100-1400.
88
+ - A `filter: blur(140px)` glow on a 1080×1920 frame is slow to render; a `radial-gradient(closest-side, …)` looks the same and costs almost nothing.
89
+ - Words that pop in place leave a one-word phrase off-centre for a moment; that is fine for 1-3 words. For longer phrases use the re-centring variation.
@@ -0,0 +1,125 @@
1
+ # Code on screen
2
+ Use for: code explainers and dev-tool videos: code types into an editor, the key line is singled out, a diagram shows what it does.
3
+
4
+ ## What makes it look premium
5
+ - **Big, readable code.** 34 px JetBrains Mono, line-height 1.62, 8–12 lines, in an editor window with a tab bar and dim line numbers (60% of `--mute`). Never shrink the window to fit; let the camera frame it.
6
+ - **Typing with a human rhythm, not a constant ticker:** ~90 chars/s with ±45% seeded jitter per char, +50 ms after `( { ;`, +100 ms at each line end, auto-indent appears at once. A 3 px accent caret blinks and rides the last char; it leaves when explaining starts. 10 lines take ~3.5 s.
7
+ - **The camera pushes while it types** (`zoom: 1.1` over 3.4 s, `in-out-sine`), so the frame is never still.
8
+ - **Single out one line in three layers (0.5 s):** a highlight bar sweeps in from the left (`scaleX` 0→1, `power3.out`, 16% accent + 5 px accent edge), the other lines dim to 28% rippling outward from it (20 ms stagger `from` that line), then the key token turns accent 0.5 s later.
9
+ - **Zoom on the characters, not the line box.** `fit: '.ln[data-n="6"] i'` frames the text itself (a block line is full width, so fitting it barely zooms). ~2.5× here: code reads at ~85 px.
10
+ - **The diagram is in the same world**, to the right; the camera slides there (1.3 s `in-out-quart`). The timeline axis draws at constant speed (`ease: 'none'`: it is time passing) and each marker pops (`back.out(2.2)`) exactly when the line reaches it.
11
+ - **End back on the code line**, with the highlight breathing (opacity 1↔0.5 yoyo): cause and effect, and no frozen end.
12
+
13
+ ## Recipe
14
+ ```html
15
+ <!doctype html>
16
+ <html data-width="1920" data-height="1080" data-duration="11">
17
+ <head><style>
18
+ :root { --bg:#0d1017; --panel:#141923; --line:#232a37; --ink:#e6e9f0; --mute:#6c7689; --accent:#ffb454;
19
+ --kw:#c792ea; --fn:#82aaff; --str:#c3e88d; --num:#f78c6c; --mono:"JetBrains Mono", monospace; }
20
+ body { background:radial-gradient(120% 90% at 30% 20%, #18202e, var(--bg)); color:var(--ink); font-family:Inter, sans-serif; }
21
+ #world { position:absolute; inset:0; width:2900px; }
22
+ #editor { position:absolute; left:160px; top:170px; width:1180px; border-radius:24px; background:var(--panel); border:1px solid var(--line); box-shadow:0 50px 110px #0008; overflow:hidden; }
23
+ .tab { height:72px; display:flex; align-items:center; gap:14px; padding:0 30px; border-bottom:1px solid var(--line); font:500 28px var(--mono); color:var(--mute); }
24
+ .tab i { width:14px; height:14px; border-radius:50%; background:var(--line); }
25
+ pre { position:relative; margin:0; padding:34px 40px 40px 0; font:400 34px/1.62 var(--mono); }
26
+ .ln { display:block; position:relative; padding-left:110px; white-space:pre; }
27
+ .ln::before { content:attr(data-n); position:absolute; left:0; width:76px; text-align:right; color:var(--mute); opacity:.6; }
28
+ .k { color:var(--kw); } .f { color:var(--fn); } .s { color:var(--str); } .n { color:var(--num); }
29
+ #hl { position:absolute; top:0; left:0; right:0; height:55px; background:color-mix(in srgb, var(--accent) 16%, transparent); border-left:5px solid var(--accent); transform-origin:left; opacity:0; }
30
+ #caret { position:absolute; left:0; top:0; width:3px; height:42px; background:var(--accent); animation:blink 1s steps(1) infinite; }
31
+ .code i { font-style:normal; visibility:hidden; } .code i.on { visibility:visible; }
32
+ #diagram { position:absolute; left:1560px; top:260px; width:1180px; height:560px; }
33
+ #diagram h3 { font:600 44px Inter; margin:0 0 30px; }
34
+ #axis { position:absolute; left:0; top:300px; }
35
+ .hit { position:absolute; top:268px; width:64px; height:64px; margin-left:-32px; border-radius:50%; background:var(--panel); border:5px solid var(--mute); }
36
+ .hit.ok { border-color:var(--str); background:var(--str); }
37
+ .gap { position:absolute; top:150px; text-align:center; font:600 34px var(--mono); color:var(--accent); }
38
+ .gap::after { content:""; display:block; height:18px; margin-top:10px; border:3px solid var(--accent); border-bottom:0; border-radius:8px 8px 0 0; }
39
+ </style></head>
40
+ <body>
41
+ <section class="scene" data-duration="11">
42
+ <div id="world">
43
+ <div id="editor">
44
+ <div class="tab"><i></i><i></i><i></i>&nbsp;retry.ts</div>
45
+ <pre class="code"><div id="hl"></div><span id="caret"></span><span class="ln" data-n="1"><span class="k">async function</span> <span class="f">retry</span>(fn, tries = <span class="n">4</span>) {</span><span class="ln" data-n="2"> <span class="k">for</span> (<span class="k">let</span> i = <span class="n">0</span>; i &lt; tries; i++) {</span><span class="ln" data-n="3"> <span class="k">try</span> {</span><span class="ln" data-n="4"> <span class="k">return await</span> <span class="f">fn</span>();</span><span class="ln" data-n="5"> } <span class="k">catch</span> {</span><span class="ln" data-n="6"> <span class="k">await</span> <span class="f">sleep</span>(<span class="n">2</span> ** i * <span class="n">250</span>);</span><span class="ln" data-n="7"> }</span><span class="ln" data-n="8"> }</span><span class="ln" data-n="9"> <span class="k">throw new</span> <span class="f">Error</span>(<span class="s">"gave up"</span>);</span><span class="ln" data-n="10">}</span></pre>
46
+ </div>
47
+ <div id="diagram">
48
+ <h3>Each retry waits twice as long</h3>
49
+ <svg id="axis" width="1180" height="20"><path id="axisPath" d="M0 10H1160" stroke="#6c7689" stroke-width="4" stroke-linecap="round"/></svg>
50
+ <div class="gap" style="left:60px;width:150px">250</div><div class="gap" style="left:210px;width:300px">500</div><div class="gap" style="left:510px;width:600px">1000 ms</div>
51
+ <div class="hit" style="left:60px"></div><div class="hit" style="left:210px"></div><div class="hit" style="left:510px"></div><div class="hit ok" style="left:1110px"></div>
52
+ </div>
53
+ </div>
54
+ </section>
55
+ <script>
56
+ // 1. split the code into chars; give each a reveal time with a human rhythm
57
+ const chars = [];
58
+ for (const ln of document.querySelectorAll('.code .ln')) {
59
+ const walk = (node) => [...node.childNodes].forEach((c) => {
60
+ if (c.nodeType === 3) { const f = document.createDocumentFragment(); for (const ch of c.data) { const i = document.createElement('i'); i.textContent = ch; f.append(i); chars.push(i); } c.replaceWith(f); }
61
+ else walk(c);
62
+ });
63
+ walk(ln); chars.push(null); // null = end of line
64
+ }
65
+ let t = 0.6; const times = []; // typing starts at 0.6 s
66
+ chars.forEach((c, k) => {
67
+ if (!c) { t += 0.1; return; } // breath at each line end
68
+ const indent = c.textContent === ' ' && (!chars[k - 1] || chars[k - 1].dataset.indent);
69
+ if (indent) { c.dataset.indent = 1; times.push(t); return; } // auto-indent appears at once
70
+ t += (1 / 90) * (0.55 + gm.random('c' + k) * 0.9); // ~90 chars/s, ±45% jitter
71
+ if ('({;'.includes(c.textContent)) t += 0.05;
72
+ times.push(t);
73
+ });
74
+ const typed = chars.filter(Boolean);
75
+ const caret = document.querySelector('#caret');
76
+ gm.onFrame('.scene', (now) => {
77
+ let n = 0; while (n < typed.length && times[n] <= now) n++;
78
+ typed.forEach((c, k) => c.classList.toggle('on', k < n));
79
+ const last = typed[Math.max(0, n - 1)];
80
+ const ln = last.closest('.ln'); // char offsets are relative to their line
81
+ caret.style.translate = `${ln.offsetLeft + last.offsetLeft + (n ? last.offsetWidth : 0)}px ${ln.offsetTop + 6}px`;
82
+ caret.style.opacity = now > 4.4 ? 0 : 1; // caret leaves when the explaining starts
83
+ });
84
+ // 2. choreography
85
+ const L = (n) => `.ln[data-n="${n}"]`, line6 = document.querySelector(L(6));
86
+ gm.set('#hl', { y: line6.offsetTop });
87
+ gm.timeline('.scene')
88
+ .from('#editor', { y: 120, rotateY: 14, rotateX: 8, transformPerspective: 2000, duration: 1.1, ease: 'expo.out' }, 0)
89
+ .from('#editor', { opacity: 0.3, duration: 0.4 }, 0)
90
+ .fromTo('#hl', { opacity: 1, scaleX: 0 }, { scaleX: 1, duration: 0.5, ease: 'power3.out' }, 4.5)
91
+ .to('.ln:not([data-n="6"])', { opacity: 0.28, duration: 0.5, stagger: { each: 0.02, from: 5 } }, 4.5)
92
+ .to(L(6) + ' .n', { color: '#ffb454', duration: 0.3 }, 5.0)
93
+ .from('#diagram h3', { y: 40, opacity: 0, duration: 0.6 }, 6.5)
94
+ .fromTo('#axisPath', { draw: 0 }, { draw: 1, duration: 1.8, ease: 'none' }, 6.7) // constant speed: it is time passing
95
+ .from('.hit', { scale: 0, duration: 0.45, ease: 'back.out(2.2)', stagger: (i, el) => 1.8 * el.offsetLeft / 1160 }, 6.7) // each dot pops as the line reaches it
96
+ .from('.gap', { opacity: 0, y: 20, scaleX: 0.4, duration: 0.5, stagger: (i, el) => 1.8 * (el.offsetLeft + el.offsetWidth) / 1160 }, 6.6)
97
+ .fromTo('#hl', { opacity: 1 }, { opacity: 0.5, duration: 0.3, repeat: 3, yoyo: true, ease: 'sine.inOut' }, 9.8);
98
+ gm.camera('#world')
99
+ .shot(0, { fit: '#editor', padding: 110 })
100
+ .shot(0.8, { fit: '#editor', padding: 110, zoom: 1.1, duration: 3.4, ease: 'in-out-sine' }) // slow push while it types
101
+ .shot(4.4, { fit: L(6) + ' i', padding: [240, 200, 240, 260], duration: 1.1 }) // fit the line's characters, not the full-width line box
102
+ .shot(6.1, { fit: '#diagram', padding: 110, duration: 1.3, ease: 'in-out-quart' })
103
+ .shot(9.2, { fit: L(6) + ' i', padding: [240, 200, 240, 260], duration: 1.2 }); // back to the line it explains
104
+ </script>
105
+ </body></html>
106
+ ```
107
+ Color the code by hand with spans (`.k` keyword, `.f` function, `.s` string, `.n` number); the splitter keeps them.
108
+
109
+ ## Make it yours
110
+ - Theme: swap the `:root` tokens. Light editor: `--panel:#fbfbfd; --ink:#1d2330; --kw:#7c3aed; --fn:#2563eb; --str:#15803d; --num:#c2410c`, highlight 12%.
111
+ - Calm/tutorial: 45–60 chars/s, line-end pause 0.25 s, camera moves 1.5 s. Energetic/social: 140 chars/s or paste lines in whole (`.from('.ln', { opacity: 0, x: -30, stagger: 0.06 })`), camera 0.6 s, a `whip-left` cut to the diagram.
112
+ - Luxury/keynote: no caret, lines fade up one by one, very slow push (`zoom: 1.05` over 5 s).
113
+
114
+ ## Variations
115
+ - Code that changes: type a new value into a token with `.to('#val', { text: '500', duration: 0.3, ease: 'none' }, t)` after a 0.15 s red flash on the old one.
116
+ - Several lines in turn: move the bar `.to('#hl', { y: line9.offsetTop, duration: 0.5, ease: 'power3.inOut' }, t)` and re-dim; camera `fit` the next line's `i`.
117
+ - Arrow from a note to a token: `gm.connect` with `sketch: true` (`references/diagrams.md`).
118
+ - Quick version without the splitter: `npx givemotion add code-window` (`typing`, `highlight`, `focus`, `.gm-line[data-line]`); less control over rhythm.
119
+
120
+ ## Pitfalls
121
+ - Char offsets are relative to their line (`.ln` is `position: relative`): add `ln.offsetLeft/offsetTop` when placing the caret.
122
+ - An absolute bar inside `pre` without `top: 0` starts below the padding, so `y: line.offsetTop` lands half a line low.
123
+ - Typing that runs into the explaining: count chars ÷ rate + line pauses and finish ≥ 0.3 s before the highlight.
124
+ - A final wide shot of code + diagram makes the code ~19 px (`tiny-text`). End close on the code instead.
125
+ - Hide not-yet-typed chars with `visibility`, not `display`, so lines keep their width and the camera fit does not change.