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.
- package/AGENTS.md +37 -0
- package/LICENSE +21 -0
- package/README.md +492 -2
- package/SECURITY.md +40 -0
- package/THIRD_PARTY_NOTICES.md +48 -0
- package/blocks/bar-chart.html +63 -0
- package/blocks/browser-window.html +52 -0
- package/blocks/callout.html +57 -0
- package/blocks/captions.html +120 -0
- package/blocks/checklist.html +32 -0
- package/blocks/code-window.html +126 -0
- package/blocks/compare.html +33 -0
- package/blocks/cursor.html +157 -0
- package/blocks/end-card.html +28 -0
- package/blocks/gradient-bg.html +48 -0
- package/blocks/grid-bg.html +18 -0
- package/blocks/highlight.html +13 -0
- package/blocks/line-chart.html +36 -0
- package/blocks/logo-reveal.html +33 -0
- package/blocks/lottie.html +34 -0
- package/blocks/lower-third.html +24 -0
- package/blocks/phone.html +18 -0
- package/blocks/quote.html +19 -0
- package/blocks/stat.html +24 -0
- package/blocks/steps.html +40 -0
- package/blocks/title-card.html +22 -0
- package/blocks/word-pop.html +40 -0
- package/examples/bench/index.html +24 -0
- package/examples/data-story/data.json +9 -0
- package/examples/data-story/index.html +361 -0
- package/examples/data-story/other.json +46 -0
- package/examples/generative/index.html +60 -0
- package/examples/made-by-agents/README.md +5 -0
- package/examples/made-by-agents/photosynthesis/blocks/checklist.html +32 -0
- package/examples/made-by-agents/photosynthesis/blocks/end-card.html +25 -0
- package/examples/made-by-agents/photosynthesis/blocks/gradient-bg.html +48 -0
- package/examples/made-by-agents/photosynthesis/blocks/steps.html +40 -0
- package/examples/made-by-agents/photosynthesis/blocks/title-card.html +22 -0
- package/examples/made-by-agents/photosynthesis/index.html +217 -0
- package/examples/personalized/index.html +28 -0
- package/llms.txt +34 -0
- package/package.json +80 -4
- package/skills/givemotion/SKILL.md +152 -0
- package/skills/givemotion/references/api.md +40 -0
- package/skills/givemotion/references/camera.md +72 -0
- package/skills/givemotion/references/charts.md +138 -0
- package/skills/givemotion/references/check-codes.md +94 -0
- package/skills/givemotion/references/data-templates.md +61 -0
- package/skills/givemotion/references/diagrams.md +75 -0
- package/skills/givemotion/references/genres/code-explainer.md +15 -0
- package/skills/givemotion/references/genres/data-story.md +21 -0
- package/skills/givemotion/references/genres/explainer.md +20 -0
- package/skills/givemotion/references/genres/hand-drawn.md +18 -0
- package/skills/givemotion/references/genres/logo-sting.md +20 -0
- package/skills/givemotion/references/genres/promo.md +18 -0
- package/skills/givemotion/references/genres/ui-demo.md +21 -0
- package/skills/givemotion/references/genres/vertical.md +19 -0
- package/skills/givemotion/references/genres/year-recap.md +18 -0
- package/skills/givemotion/references/javascript.md +72 -0
- package/skills/givemotion/references/media-audio.md +57 -0
- package/skills/givemotion/references/motion-design.md +183 -0
- package/skills/givemotion/references/output.md +64 -0
- package/skills/givemotion/references/review.md +60 -0
- package/skills/givemotion/references/styles.md +133 -0
- package/skills/givemotion/references/techniques/backgrounds-and-ambient.md +85 -0
- package/skills/givemotion/references/techniques/camera-moves.md +88 -0
- package/skills/givemotion/references/techniques/captions-vertical.md +89 -0
- package/skills/givemotion/references/techniques/code-on-screen.md +125 -0
- package/skills/givemotion/references/techniques/hand-drawn.md +87 -0
- package/skills/givemotion/references/techniques/impacts-and-energy.md +106 -0
- package/skills/givemotion/references/techniques/kinetic-type.md +92 -0
- package/skills/givemotion/references/techniques/light-and-depth.md +91 -0
- package/skills/givemotion/references/techniques/logo-reveal.md +94 -0
- package/skills/givemotion/references/techniques/numbers-and-charts.md +92 -0
- package/skills/givemotion/references/techniques/paths-and-maps.md +99 -0
- package/skills/givemotion/references/techniques/scene-transitions.md +85 -0
- package/skills/givemotion/references/techniques/shapes-and-motion-graphics.md +92 -0
- package/skills/givemotion/references/techniques/text-reveals.md +94 -0
- package/skills/givemotion/references/techniques/ui-demo.md +110 -0
- package/skills/givemotion/references/timeline.md +85 -0
- package/src/analyze.js +1485 -0
- package/src/api.js +36 -0
- package/src/args.js +33 -0
- package/src/audio-cli.js +58 -0
- package/src/bench.js +153 -0
- package/src/browser.js +381 -0
- package/src/bundle.js +79 -0
- package/src/cdp.js +94 -0
- package/src/check.js +1818 -0
- package/src/cli.js +611 -0
- package/src/dev.js +26 -0
- package/src/ffmpeg.js +330 -0
- package/src/parts.js +380 -0
- package/src/player/gm-player.js +175 -0
- package/src/render.js +772 -0
- package/src/runtime/audio.js +306 -0
- package/src/runtime/chart.js +665 -0
- package/src/runtime/fonts/BebasNeue-OFL.txt +93 -0
- package/src/runtime/fonts/Caveat-OFL.txt +93 -0
- package/src/runtime/fonts/Inter-OFL.txt +93 -0
- package/src/runtime/fonts/JetBrainsMono-OFL.txt +93 -0
- package/src/runtime/fonts/Nunito-OFL.txt +93 -0
- package/src/runtime/fonts/PlayfairDisplay-OFL.txt +93 -0
- package/src/runtime/fonts/SpaceGrotesk-OFL.txt +93 -0
- package/src/runtime/fonts/bebas-neue-latin-400-normal.woff2 +0 -0
- package/src/runtime/fonts/bebas-neue-latin-ext-400-normal.woff2 +0 -0
- package/src/runtime/fonts/caveat-latin-ext-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/caveat-latin-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/inter-latin-ext-wght-italic.woff2 +0 -0
- package/src/runtime/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/inter-latin-wght-italic.woff2 +0 -0
- package/src/runtime/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/jetbrains-mono-latin-ext-wght-italic.woff2 +0 -0
- package/src/runtime/fonts/jetbrains-mono-latin-ext-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/jetbrains-mono-latin-wght-italic.woff2 +0 -0
- package/src/runtime/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/nunito-latin-ext-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/nunito-latin-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/playfair-display-latin-ext-wght-italic.woff2 +0 -0
- package/src/runtime/fonts/playfair-display-latin-ext-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/playfair-display-latin-wght-italic.woff2 +0 -0
- package/src/runtime/fonts/playfair-display-latin-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/space-grotesk-latin-ext-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/space-grotesk-latin-wght-normal.woff2 +0 -0
- package/src/runtime/motion.js +2674 -0
- package/src/runtime/preset.css +97 -0
- package/src/runtime/runtime.js +2141 -0
- package/src/runtime/sketch.js +1249 -0
- package/src/server.js +402 -0
- package/src/studio/index.html +238 -0
- package/src/types/api.d.ts +354 -0
- 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 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> 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 < 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.
|