@scenerok/sdk 0.27.0 → 0.29.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.
@@ -0,0 +1,111 @@
1
+ # Choosing tools for a beat
2
+
3
+ SceneRok's blocks, motion, transitions, filters and media are a repertoire,
4
+ not a checklist. Nothing is required, nothing is decoration by default: each
5
+ beat gets what its job calls for. `repertoire.md` says what every tool looks
6
+ like, what it is for, and what it fights. This file is how to use that.
7
+
8
+ ## The procedure
9
+
10
+ For each beat:
11
+
12
+ 1. **Name its job** with one word from the job list in `repertoire.md`
13
+ (announce, count, quote, code, prove, demo, establish, cta, ...), and the one
14
+ thing the viewer must take away.
15
+ 2. **Put the material on the table.** What do you actually have for this beat:
16
+ the source's photos, screenshots, footage, logo, the exact numbers and words?
17
+ You can only choose a picture you have looked at.
18
+ 3. **List the candidates** under that job in `repertoire.md` → Find tools by job.
19
+ Plain `text` and "nothing extra" are always candidates.
20
+ 4. **Drop what fights.** Read each candidate's *Fights* against your content
21
+ (length of the words, busyness of the picture), its neighbours (the beat
22
+ before and after, anything else moving) and the frame (9:16 or 16:9: read
23
+ *Fit*).
24
+ 5. **Choose by the look.** Read *Looks* for the survivors and pick the frame you
25
+ want the viewer to see. When two would do, take the simpler.
26
+ 6. **Write one line**: `<beat>: <tool> because <reason>`. If you can't finish
27
+ the sentence, choose something plainer.
28
+ 7. **Verify by looking.** After rendering, open a frame of that beat. If it
29
+ doesn't look like the *Looks* you chose for, the description or your
30
+ placement is wrong. Fix the placement, and report the description.
31
+
32
+ ## When plain text on a still frame is right, and when it is not
33
+
34
+ Right, when:
35
+
36
+ - the words are the content: a quote, a definition, a price, a command, a
37
+ legal line;
38
+ - the viewer needs reading time, and anything moving would compete;
39
+ - the design is typographic on purpose (a brand that speaks in type);
40
+ - there is no honest picture: an abstract claim, an internal change with no
41
+ UI. A stock or generated image would only pretend.
42
+
43
+ Wrong, when:
44
+
45
+ - the source has pictures that show the thing (a product page, an app, a repo
46
+ with a UI) and the beat's job is demo, prove or establish;
47
+ - several beats in a row are text on the same flat colour, so the video turns
48
+ into a slideshow;
49
+ - the text says what a picture could show ("sleek design", "easy to use").
50
+
51
+ The same goes for motion: a still frame is right when the eye should rest.
52
+ Motion is for arrival, emphasis and time passing, and one moving thing at a
53
+ time is easier to follow than three.
54
+
55
+ ## Worked choices
56
+
57
+ **Product promo, hook (9:16).** Job: establish. Material: three lifestyle
58
+ photos from the product page, a 500 px hero shot on white. Candidates: a page
59
+ photo, `xai.imagine`, `animations.liquid_glass_notification`, `text`.
60
+ Dropped: `xai.imagine` fights facts (it would invent a different robot); the
61
+ liquid-glass block is fixed artwork in rainbow colours. Chosen: the lifestyle
62
+ photo as a band over a blurred copy of itself, with a 3 s push, because a still
63
+ held for 3 s reads as a slide and the push gives it time. Headline: `text`
64
+ with `riseIn`.
65
+
66
+ **Customer review (any aspect).** Job: quote. Material: one two-sentence review
67
+ from the page. Candidates: `text_typewriter_quote`, `x_post`, `reddit_post`,
68
+ `text`. Dropped: `x_post` and `reddit_post` would dress the review as a post on
69
+ a platform it never appeared on; the typewriter takes three seconds to type
70
+ what a reader takes two to read. Chosen: plain `text` on a flat dark frame with
71
+ a muted attribution, no motion, held 5 s.
72
+
73
+ **Breaking change in release notes.** Job: announce. Material: one sentence
74
+ from the changelog. Chosen: plain `text`, no entrance, no grade change, held
75
+ 4 s. Gravity is the point; any flourish would make a warning read like
76
+ marketing. Nothing else was considered necessary, and that is a valid outcome
77
+ of step 3.
78
+
79
+ **A stat (9:16).** Job: count. Material: "12 bug fixes" counted from the
80
+ release body. Candidates: `text_counter_stat`, `apple_money_count`, `text` with
81
+ `popIn`. Dropped: `apple_money_count` is money on a white slab. Chosen:
82
+ `text_counter_stat` with `suffix: ""` (its default `+` would turn 12 into
83
+ "12+", which is false).
84
+
85
+ **Code in 9:16.** Job: code. Material: a five-line before/after. Candidates:
86
+ `code_typing`, `code_diff`, a `code_snippet_*` theme, `text` lines. Dropped:
87
+ `code_typing` (in 9:16 its code shrinks to about 17 px). Chosen: `code_diff`,
88
+ which has its own 9:16 layout and keeps the removed line on screen with
89
+ `keep_removed: true`, so the change reads as a before/after. Its *Looks* says it
90
+ sits in the upper half, so the beat's headline goes in the empty lower half
91
+ instead of on top of the code.
92
+
93
+ **Cut between two lifestyle shots in a calm promo.** Job: transition.
94
+ Candidates: every `tr.*`, and a hard cut. Dropped: everything whose *Fights*
95
+ says "calm or premium edits"; `flash` (flicker). Chosen: a hard cut, because
96
+ the shots differ enough to read as a new beat. `dissolve` only if the two shots
97
+ share a subject. A transition is a candidate, not a default.
98
+
99
+ **Talking head, key line.** Job: emphasize. Material: a 16:9 interview, the
100
+ line "it paid for itself". Candidates: a punch-in (scale the clip),
101
+ `text_kinetic_words`, `blend_caption`, `text_glitch_title`. Dropped: kinetic
102
+ words fight a sentence that must be read whole; glitch fights the tone.
103
+ Chosen: a punch-in on the speaker with a two-word caption card in the accent
104
+ colour.
105
+
106
+ ## Before you trust a description
107
+
108
+ The records come from the block source and the catalog previews. Some previews
109
+ predate a block's 9:16 layout, and a few are blank (see each record's *Fit*).
110
+ When a choice rests on a block you have never seen rendered, render a short
111
+ test or look at its preview before building the video around it.
@@ -29,13 +29,13 @@ Defaults unless the user says otherwise:
29
29
 
30
30
  | Workflow | Load these slugs | Primary APIs (VidScript · SDK) |
31
31
  | --- | --- | --- |
32
- | **Product promo** | `recipes.md`, `look-and-feel.md`, `brand-setup.md` (+ `ai-models.md` when the page has no usable pictures, `sdk-guide.md` if TS) | page images as `input` with a scale push, `motion.riseIn` / `popIn`, `solid` CTA button, `filter "cdl"`, `eleven.music` |
32
+ | **Product promo** | `plugins.md` (run `listing-promo`), `recipes.md` for the visual moves, `look-and-feel.md`, `brand-setup.md` (+ `ai-models.md` when the page has no usable pictures, `sdk-guide.md` if TS) | page images as `input` with a scale push, `motion.riseIn` / `popIn`, `solid` CTA button, `filter "cdl"`, `eleven.music` |
33
33
  | **Motion graphic** | `animations-picker.md` (+ `animations-catalog.md` if needed) | `animations.*` (VidScript HyperFrames; prefer VidScript for catalog blocks) |
34
34
  | **Social proof card** | `animations-picker.md` | `animations.x_post`, `reddit_post`, … (VidScript) |
35
35
  | **Edit existing footage** | `editing-actions.md` (+ `media-stdlib.md` to probe the clips, `transitions-catalog.md` for cuts) | `Trim` / `Speed` / `Loop` / `Crop` / `Mask` / `Overlay` / `Composite`, `blend:`, `duck_under:`, `filter "lut"` (VidScript only) |
36
36
  | **Scene montage** | `transitions-catalog.md`, `ai-models.md` (+ `sdk-guide.md` if TS loops) | `tr.dissolve` · SDK: `import tr from '@scenerok/transitions'` + `t[start][end] = tr.dissolve()` |
37
37
  | **Narrated explainer** | `tts.md`, `text-styles.md`, `music.md`, `workflow.md` (+ `sdk-guide.md` if TS) | `xai.tts` · `await xai.tts(...)` + `t.push(vo)` |
38
- | **Code demo / PR / release notes** | `recipes.md` (release notes), `animations-picker.md` (code rows) | 16:9: `code_typing`, `code_diff`; 9:16: a code card of `text` lines with `motion.typewriter` (VidScript) |
38
+ | **Code demo / PR / release notes** | `plugins.md` (run `release-video`), `recipes.md` (the code beat), `animations-picker.md` (code rows) | 16:9: `code_typing`, `code_diff`; 9:16: a code card of `text` lines with `motion.typewriter` (VidScript) |
39
39
  | **Text animation / headline / stat** | `animations-picker.md` (text-anim rows) | `animations.text_*` (VidScript) · SDK kinetic: `text` + `fadeIn` / `popIn` |
40
40
  | **Kinetic title only** | `text-styles.md` (+ `sdk-guide.md` if TS) | `text` + `motion.*` · SDK: `text(..., { area })` + `.animate(fadeIn(...))` |
41
41
  | **Custom-authored animation (no block fits)** | `animations-catalog.md` → "Custom HTML" | `animations.custom_html` (VidScript) |
@@ -43,7 +43,7 @@ Defaults unless the user says otherwise:
43
43
 
44
44
  ## Staged routing
45
45
 
46
- 1. **Classify** — workflow from table above, then the matching script in `recipes.md`
46
+ 1. **Classify** — workflow from table above, then the matching pattern in `recipes.md` (a plugin's README owns its beat order)
47
47
  2. **Choose authoring** — VidScript by default and for every editing action (masks, blends, crops, ducking, LUTs, timecode); `@scenerok/sdk` (`sdk-guide.md`) only for loops or computed timing (it has none of the editing actions)
48
48
  3. **Load catalogs** — only what that workflow needs (conditional loading)
49
49
  4. **Name block or API** — VidScript: pick `animations.<fn>` from `animations-picker.md`. SDK: factories (`video` / `loadVideo`, `text`, `fadeIn`) + AI imports (`@scenerok/xai`, …)
@@ -58,13 +58,16 @@ than it predicts, so check the rendered frame before trusting a line to fit.
58
58
 
59
59
  ## Pictures
60
60
 
61
- Every beat has a picture under its type. In order of preference:
62
-
63
- 1. **Real**: product photos, logos, app screenshots, footage, screen recordings.
64
- 2. **Generated**: `xai.image`, `xai.imageToVideo` from a real photo, `xai.imagine`
65
- (`ai-models.md`; prompts forbid text).
61
+ Whether a beat gets a picture is a choice (`choosing.md`). When it does, the
62
+ sources rank like this:
63
+
64
+ 1. **Real**: product photos, logos, app screenshots, footage, screen
65
+ recordings. They prove things; nothing else does.
66
+ 2. **Generated**: `xai.image`, `xai.imageToVideo` from a real photo,
67
+ `xai.imagine` (`ai-models.md`; prompts forbid text). They set a mood and
68
+ invent everything else.
66
69
  3. **Made**: a brand-colour glow or gradient rendered with ffmpeg (recipes →
67
- Release notes), as a background behind real pictures, never as the only picture.
70
+ Release notes). Atmosphere without a subject.
68
71
 
69
72
  Placing stills in a frame of a different shape:
70
73
 
@@ -112,15 +115,17 @@ which needs no file.
112
115
 
113
116
  ## Motion
114
117
 
115
- | Phase | Use | Timing |
118
+ When you do use motion, these timings read well. What each primitive looks
119
+ like is in `repertoire.md`.
120
+
121
+ | Phase | Typical choice | Timing |
116
122
  | --- | --- | --- |
117
123
  | Entrance | `motion.riseIn(0.5s, 40-80)` for copy and cards, `motion.popIn(0.4-0.6s)` for prices, buttons, logos, `motion.typewriter` for code, `motion.fadeIn(0.4s)` for secondary lines | Stagger items 0.3-0.6 s apart |
118
- | Hold | Stills: a push `{ type: "scale", from: { scale: 1 }, to: { scale: 1.08 }, easing: "linear" }`. Logos and products: `motion.float(3s, 10-14)` | A static hold lasts 3 s at most |
119
- | Exit | `motion.fadeOut(0.3s)` on everything that leaves before the end | The last 0.3 s of the block |
124
+ | Hold | Nothing (rest the eye), or a push on a still `{ type: "scale", from: { scale: 1 }, to: { scale: 1.08 }, easing: "linear" }`, or `motion.float(3s, 10-14)` on a logo or product | A still hold beyond ~3 s should be one you meant: reading time, gravity |
125
+ | Exit | A cut, or `motion.fadeOut(0.3s)` when the element should leave softly | The last 0.3 s of the block |
120
126
 
121
- A push on a soft gradient doesn't count as motion: give it a still, footage,
122
- or a floating element to move. Fast moves can take `motion_blur:`
123
- (`editing-actions.md`).
127
+ A push on a soft gradient is invisible, so it adds nothing. Fast moves can take
128
+ `motion_blur:` (`editing-actions.md`).
124
129
 
125
130
  ## Pacing
126
131
 
@@ -129,11 +134,13 @@ or a floating element to move. Fast moves can take `motion_blur:`
129
134
  - One idea per beat: the hook, the product, three features, the price, and the call to action.
130
135
  - Close on the call to action for at least 2.5 s.
131
136
 
132
- ## What premium means here
137
+ ## What premium tends to mean
138
+
139
+ Tendencies, not requirements; each is a choice you can argue against.
133
140
 
134
- - A real picture fills or anchors every beat; no beat is text on a flat colour.
141
+ - Real pictures where the subject has them; type alone where the words are the point.
135
142
  - One accent, used sparingly; everything else is ink and muted on dark (or the reverse).
136
- - Big type with tight copy: 3-6 words a line, never a paragraph.
137
- - Every element arrives, holds with a little motion, and leaves.
138
- - One grade ties it together: `filter "cdl"` warmth or coolness plus a `vignette` of about 0.3.
139
- - Sound under all of it: a music bed, a voiceover, or both.
143
+ - Big type with tight copy: 3-6 words a line, rarely a paragraph.
144
+ - Restraint: one thing moving at a time, consistent entrances, stillness where the eye should rest.
145
+ - A grade only if the pictures need tying together: `filter "cdl"` warmth or coolness, a `vignette` of about 0.3.
146
+ - Sound that fits the use: many feeds play muted, so the picture must work without it.
@@ -7,10 +7,11 @@ cheaper than hand-composing. **Never assume which plugins exist** — discover
7
7
  them at run time; the catalog grows and changes.
8
8
 
9
9
  Plugins are the reuse-first path: check `intent-router.md` for the workflow
10
- match. A plugin's video must still pass the quality gate in `SKILL.md`. Draft
11
- first, read the filled script, and if a beat is text over a flat colour, add
12
- the pictures and motion from `recipes.md` before `render-draft`. Today that
13
- applies to the first-party `listing-promo` and `release-video` templates.
10
+ match. Review a plugin's video with the same questions as your own (`SKILL.md` →
11
+ Review). Draft first and read the filled script. Today the first-party
12
+ `listing-promo` and `release-video` templates set every beat as text over a
13
+ flat colour and leave the product images unused. When the request calls for
14
+ more, change those beats before `render-draft`.
14
15
 
15
16
  ## Workflow (MCP)
16
17
 
@@ -1,221 +1,90 @@
1
- # Recipes
1
+ # Visual patterns
2
2
 
3
- Three complete scripts that pass the quality gate in `SKILL.md`. Each uses
4
- the look system in `look-and-feel.md`. Copy the one that fits, then swap in
5
- your own assets, copy, and palette. Keep the structure: a picture under every
6
- beat, motion on everything, and a grade and sound over the whole video.
3
+ Composition moves that lift any script, whichever plugin produced it. They are
4
+ not a story: a **plugin owns its domain's story** — the order of beats, the copy
5
+ rules and the template — and this file owns how a beat is *made to look*.
7
6
 
8
- ## Product promo (9:16, 15 s)
7
+ | Domain | Its story lives in |
8
+ | --- | --- |
9
+ | Product promo | [`plugins/listing-promo/README.md`](../../../plugins/listing-promo/README.md#story-and-copy) |
10
+ | Changelog / release notes | [`plugins/release-video/README.md`](../../../plugins/release-video/README.md#story) |
11
+ | Talking head with captions | [`plugins/voiceover-booth/README.md`](../../../plugins/voiceover-booth/README.md#story) |
9
12
 
10
- Four beats: hook, hero, features, then price and CTA. The images are the
11
- product page's own (Amazon `landingImage`, `og:image`, or JSON-LD `image`;
12
- see `look-and-feel.md` → Sourcing imagery). Prepare them before you write the
13
- script. Amazon stills are 500 px squares with baked-in captions, so crop to a
14
- clean region and make a blurred 9:16 backdrop from the same shot:
13
+ Use the plugin for the story, then use these patterns — and the records in
14
+ [repertoire.md](repertoire.md) — to make each beat read on a phone. Every
15
+ pattern says when it earns its place and what it fights; none of them is
16
+ required.
15
17
 
16
- ```bash
17
- ffmpeg -i room.jpg -vf "crop=500:185:0:95,scale=1080:-2" room-band.jpg
18
- ffmpeg -i room.jpg -vf "crop=104:185:140:95,scale=1080:1920,boxblur=40:2,eq=brightness=-0.18" room-bg.jpg
19
- sips -g pixelWidth -g pixelHeight room-band.jpg # 1080x398: place at 984x363
20
- ```
18
+ ## A held still needs a push
19
+
20
+ A still that sits unchanged for three seconds reads as a slide, however good the
21
+ photo is. A 1.08 scale over the hold keeps the eye engaged without movement you
22
+ notice. Fights: a second moving element in the same beat, or a photo whose edges
23
+ carry detail you want still.
21
24
 
22
25
  ```vidscript
23
- import motion from "@scenerok/basic-animations"
24
- import eleven from "@elevenlabs/music"
25
-
26
- # Look: one palette, two fonts, one accent.
27
- let bg = "#0A0C10"
28
- let ink = "#F4F6FA"
29
- let muted = "#A3ADBD"
30
- let accent = "#3D8BFF"
31
- let display = "Archivo Black"
32
- let body = "Inter"
33
-
34
- # Assets: the product page's own images, cropped and blurred with ffmpeg first.
35
- input hero = "hero.jpg" # 483x472, product on white
36
- input roomBand = "room-band.jpg" # 1080x398, crop of a lifestyle shot
37
- input roomBg = "room-bg.jpg" # 1080x1920, same shot, blurred + darkened
38
- input closeBand = "closeup-band.jpg" # 1080x368
39
- input closeBg = "closeup-bg.jpg" # 1080x1920
40
-
41
- # Slow push-in for every still, so no frame is ever frozen.
26
+ input still = "https://cdn.example.com/photo.jpg"
27
+
28
+ # 8% over four seconds is enough to read as alive and not as a zoom.
42
29
  let push = { type: "scale", from: { scale: 1 }, to: { scale: 1.08 }, easing: "linear" }
30
+ [0s .. 4s] = still, width: 1080, height: 1920, x: 0, y: 0, animate: push
43
31
 
44
- # 1. Hook (0-3 s): lifestyle shot; the headline lands in the first second.
45
- [0s .. 3s] = roomBg, width: 1080, height: 1920, x: 0, y: 0, animate: push
46
- [0s .. 3s] = roomBand, width: 984, height: 363, x: 48, y: 820, radius: 36, blend: "normal", animate: [motion.riseIn(0.5s, 80), push, motion.fadeOut(0.3s)]
47
- [0.2s .. 3s] = text "Vacuums. Mops.\nEmpties itself.", font: display, size: 104, color: ink, line_height: 1.05, x: "50%", y: "27%", max_width: "86%", align: center, shadow_color: "#000000", shadow_blur: 24, animate: [motion.riseIn(0.6s, 50), motion.fadeOut(0.3s)]
48
-
49
- # 2. Hero (3-7 s): the product on its own white card.
50
- [3s .. 7s] = solid(bg)
51
- [3s .. 7s] = hero, width: 900, height: 880, x: 90, y: 300, radius: 48, blend: "normal", animate: [motion.fadeIn(0.4s), push, motion.fadeOut(0.3s)]
52
- [3.3s .. 7s] = text "ECOVACS", font: body, size: 44, color: accent, letter_spacing: 10, x: "50%", y: "64.5%", align: center, animate: [motion.fadeIn(0.4s), motion.fadeOut(0.3s)]
53
- [3.5s .. 7s] = text "DEEBOT T30e OMNI", font: display, size: 72, color: ink, x: "50%", y: "71%", max_width: "88%", align: center, animate: [motion.riseIn(0.5s, 40), motion.fadeOut(0.3s)]
54
-
55
- # 3. Features (7-11 s): a close-up band, then one bullet every 0.6 s.
56
- [7s .. 11s] = closeBg, width: 1080, height: 1920, x: 0, y: 0, animate: push
57
- [7s .. 11s] = closeBand, width: 984, height: 335, x: 48, y: 300, radius: 36, blend: "normal", animate: [motion.fadeIn(0.4s), push, motion.fadeOut(0.3s)]
58
- [7.4s .. 11s] = solid(accent, width: 24, height: 24, x: 96, y: 868, radius: 12), animate: [motion.popIn(0.4s), motion.fadeOut(0.3s)]
59
- [7.4s .. 11s] = text "25,000 Pa suction", font: display, size: 64, color: ink, x: 150, y: 880, anchor: "left", align: left, animate: [motion.riseIn(0.5s, 40), motion.fadeOut(0.3s)]
60
- [8.0s .. 11s] = solid(accent, width: 24, height: 24, x: 96, y: 1028, radius: 12), animate: [motion.popIn(0.4s), motion.fadeOut(0.3s)]
61
- [8.0s .. 11s] = text "ZeroTangle 3.0 brush", font: display, size: 64, color: ink, x: 150, y: 1040, anchor: "left", align: left, animate: [motion.riseIn(0.5s, 40), motion.fadeOut(0.3s)]
62
- [8.6s .. 11s] = solid(accent, width: 24, height: 24, x: 96, y: 1188, radius: 12), animate: [motion.popIn(0.4s), motion.fadeOut(0.3s)]
63
- [8.6s .. 11s] = text "Self-emptying dock", font: display, size: 64, color: ink, x: 150, y: 1200, max_width: "80%", anchor: "left", align: left, animate: [motion.riseIn(0.5s, 40), motion.fadeOut(0.3s)]
64
-
65
- # 4. Price and CTA (11-15 s). The button is a solid with a radius under the label.
66
- [11s .. 15s] = solid(bg)
67
- [11s .. 15s] = hero, width: 560, height: 548, x: 260, y: 260, radius: 40, blend: "normal", animate: [motion.popIn(0.5s), motion.float(3s, 10)]
68
- [11.3s .. 15s] = text "₹44,999", font: display, size: 150, color: ink, x: "50%", y: "50%", align: center, animate: motion.popIn(0.5s)
69
- [11.6s .. 15s] = text "★ 4.4 · 166 ratings", font: body, size: 48, color: muted, x: "50%", y: "58.5%", align: center, animate: motion.fadeIn(0.4s)
70
- [12s .. 15s] = solid(accent, width: 660, height: 132, x: 210, y: 1210, radius: 66), animate: motion.popIn(0.5s)
71
- [12s .. 15s] = text "Shop on Amazon", font: display, size: 56, color: "#FFFFFF", x: "50%", y: 1276, align: center, animate: motion.fadeIn(0.4s)
72
-
73
- # Grade: warm the whole frame, pull the edges down.
74
- [0s .. 15s] = filter "cdl", slope: "1.04 1.0 0.96", offset: "0 0 0", power: "1 1 1", saturation: 1.08
75
- [0s .. 15s] = filter "vignette", intensity: 0.3
76
-
77
- # Sound: a bed under everything.
78
- let bed = eleven.music("Bright, modern electronic product-ad bed, confident pulse, clean ending", duration: 15, instrumental: true)
79
- [0s .. 15s] = audio bed, volume: 0.5, fade_out: 1s
80
-
81
- output to "promo.mp4", resolution: "1080x1920", fps: 30, background: "#0A0C10"
32
+ output to "beat.mp4", resolution: "1080x1920", fps: 30
82
33
  ```
83
34
 
84
- `validate` warns `too-many-animations` at the bullets: the three bullets
85
- leave together on one 0.3 s fade, and the linter counts each exit as motion.
86
- Accept that warning here and fix any other.
35
+ ## A product belongs on a card over its own context
87
36
 
88
- Variations: replace the hook band with
89
- `xai.imageToVideo(hero, "slow orbit around the product, …", aspect_ratio: "9:16", duration: 3)`
90
- for a moving hero shot (`ai-models.md`). For 16:9, use the 16:9 type scale
91
- and put the product on the right and the copy on the left.
37
+ Cutting straight from a lifestyle shot to a plain background loses the thing the
38
+ shot was selling. Keep a blurred, darkened copy as the field and put the product
39
+ on a rounded card above it: the eye reads "this object, in that place".
92
40
 
93
- ## Release notes / changelog (9:16, 18 s)
41
+ ```vidscript
42
+ input hero = "https://cdn.example.com/product.jpg"
43
+ input room = "https://cdn.example.com/room.jpg"
94
44
 
95
- Four beats: version hook, the one new feature as typed code with a
96
- highlight, a stat, and an outro. Facts come from the release body
97
- (`gh api repos/<owner>/<repo>/releases/latest --jq .body`); count the
98
- `fix`/`feat` lines rather than estimating. The logo is the project's own
99
- (`https://avatars.githubusercontent.com/u/<id>`, or the README image). The
100
- background is a brand glow made with ffmpeg:
45
+ # The background is the same shot, blurred and darkened ahead of time with
46
+ # ffmpeg, so the card sits in context instead of on a flat colour.
47
+ input roomBg = "https://cdn.example.com/room-bg.jpg"
101
48
 
102
- ```bash
103
- ffmpeg -f lavfi -i "color=c=black:s=1080x1920,format=rgb24" -frames:v 1 \
104
- -vf "geq=r='11+90*exp(-((X-760)^2+(Y-520)^2)/180000)':g='11+30*exp(-((X-760)^2+(Y-520)^2)/180000)':b='15+4*exp(-((X-760)^2+(Y-520)^2)/180000)'" glow.png
105
- ```
49
+ [0s .. 4s] = roomBg, width: 1080, height: 1920, x: 0, y: 0
50
+ [0.4s .. 4s] = hero, width: 900, height: 880, x: 90, y: 300, radius: 48
51
+ [0.8s .. 4s] = text "The product name", size: 72, color: "#F5F5F7", x: "50%", y: "71%", max_width: "88%", align: center
106
52
 
107
- ```vidscript
108
- import motion from "@scenerok/basic-animations"
109
- import animations from "@scenerok/animations"
110
- import eleven from "@elevenlabs/music"
111
-
112
- let ink = "#F5F5F7"
113
- let muted = "#9CA3AF"
114
- let accent = "#FF5B11"
115
- let display = "Archivo Black"
116
- let mono = "JetBrains Mono"
117
-
118
- input glow = "glow.png" # 1080x1920, made with ffmpeg: brand-colour glow on near-black
119
- input flame = "flame.png" # 460x460 RGBA, the project's logo
120
-
121
- # Ambient layer: the background drifts for the whole video.
122
- [0s .. 18s] = glow, width: 1080, height: 1920, x: 0, y: 0, animate: { type: "scale", from: { scale: 1 }, to: { scale: 1.15 }, easing: "linear" }
123
-
124
- # 1. Hook (0-3.5 s)
125
- [0s .. 3.5s] = flame, width: 460, height: 460, x: 310, y: 340, blend: "normal", animate: [motion.popIn(0.6s), motion.float(2.9s, 14), motion.fadeOut(0.3s)]
126
- [0.3s .. 3.5s] = text "RELEASE NOTES", font: mono, size: 44, color: accent, letter_spacing: 8, x: "50%", y: "50%", align: center, animate: [motion.fadeIn(0.4s), motion.fadeOut(0.3s)]
127
- [0.5s .. 3.5s] = text "hono v4.13.14", font: display, size: 120, color: ink, x: "50%", y: "57%", max_width: "88%", align: center, animate: [motion.riseIn(0.6s, 60), motion.fadeOut(0.3s)]
128
-
129
- # 2. The new feature as code (3.5-10 s): headline, card, then the code types itself.
130
- [3.5s .. 10s] = text "NEW IN BEARER-AUTH", font: mono, size: 44, color: accent, letter_spacing: 6, x: 96, y: 300, anchor: "left", align: left, animate: [motion.fadeIn(0.4s), motion.fadeOut(0.3s)]
131
- [3.6s .. 10s] = text "Error messages can\nreturn a Response", font: display, size: 84, color: ink, line_height: 1.1, x: 96, y: 440, anchor: "left", align: left, animate: [motion.riseIn(0.5s, 40), motion.fadeOut(0.3s)]
132
- [4s .. 10s] = solid("#16161D", width: 960, height: 700, x: 60, y: 620, radius: 32), animate: [motion.riseIn(0.5s, 60), motion.fadeOut(0.3s)]
133
- # One text layer per code line: a tall multi-line block can clip. Indent with x, not spaces.
134
- [4.4s .. 10s] = text "bearerAuth({ token,", font: mono, size: 44, color: ink, x: 108, y: 760, anchor: "left", align: left, animate: [motion.typewriter(0.5s), motion.fadeOut(0.3s)]
135
- [4.9s .. 10s] = text "invalidToken: {", font: mono, size: 44, color: ink, x: 164, y: 836, anchor: "left", align: left, animate: [motion.typewriter(0.5s), motion.fadeOut(0.3s)]
136
- [5.4s .. 10s] = text "message: (c) =>", font: mono, size: 44, color: accent, x: 220, y: 912, anchor: "left", align: left, animate: [motion.typewriter(0.5s), motion.fadeOut(0.3s)]
137
- [5.9s .. 10s] = text "c.json(problem, 401),", font: mono, size: 44, color: accent, x: 276, y: 988, anchor: "left", align: left, animate: [motion.typewriter(0.5s), motion.fadeOut(0.3s)]
138
- [6.4s .. 10s] = text "}})", font: mono, size: 44, color: ink, x: 108, y: 1064, anchor: "left", align: left, animate: [motion.typewriter(0.3s), motion.fadeOut(0.3s)]
139
- # Highlight beat: a bar sweeps under the changed lines once the code is typed.
140
- [7.2s .. 10s] = solid(accent, width: 900, height: 158, x: 90, y: 872, radius: 16, opacity: 0.16), animate: [{ type: "slideX", from: { x: -1000 }, to: { x: 0 }, start: 0, end: 0.5, easing: "easeOutCubic" }, motion.fadeOut(0.3s)]
141
- [7.4s .. 10s] = text "Any content type, e.g. problem+json", font: mono, size: 44, color: muted, x: 108, y: 1220, anchor: "left", align: left, animate: [motion.fadeIn(0.4s), motion.fadeOut(0.3s)]
142
-
143
- # 3. Highlight (10-14 s): one number, then three rows. suffix: "" because the block defaults to "+".
144
- [10s .. 14s] = animations.text_counter_stat("12", "BUG FIXES", suffix: "", accent: accent)
145
- [11s .. 14s] = text "serve-static: GET and HEAD only", font: mono, size: 44, color: ink, x: "50%", y: "70%", align: center, animate: [motion.riseIn(0.4s, 30), motion.fadeOut(0.3s)]
146
- [11.4s .. 14s] = text "css: safer url() minifying", font: mono, size: 44, color: ink, x: "50%", y: "74.5%", align: center, animate: [motion.riseIn(0.4s, 30), motion.fadeOut(0.3s)]
147
- [11.8s .. 14s] = text "trie-router: keep first param", font: mono, size: 44, color: ink, x: "50%", y: "79%", align: center, animate: [motion.riseIn(0.4s, 30), motion.fadeOut(0.3s)]
148
-
149
- # 4. Outro (14-18 s)
150
- [14s .. 18s] = flame, width: 320, height: 320, x: 380, y: 520, blend: "normal", animate: [motion.popIn(0.5s), motion.float(3s, 12)]
151
- [14.3s .. 18s] = solid("#1C1C24", width: 860, height: 120, x: 110, y: 950, radius: 60), animate: motion.popIn(0.5s)
152
- [14.3s .. 18s] = text "npm i hono@4.13.14", font: mono, size: 48, color: ink, x: "50%", y: 1010, align: center, animate: motion.fadeIn(0.4s)
153
- [14.8s .. 18s] = text "github.com/honojs/hono", font: mono, size: 44, color: muted, x: "50%", y: 1150, align: center, animate: motion.fadeIn(0.4s)
154
-
155
- # Grade
156
- [0s .. 18s] = filter "cdl", slope: "1.03 1.0 0.97", offset: "0 0 0", power: "1 1 1", saturation: 1.05
157
- [0s .. 18s] = filter "vignette", intensity: 0.35
158
-
159
- let bed = eleven.music("Minimal driving synth pulse for a developer product update, clean and modern", duration: 18, instrumental: true)
160
- [0s .. 18s] = audio bed, volume: 0.5, fade_out: 1.5s
161
-
162
- output to "release.mp4", resolution: "1080x1920", fps: 30, background: "#0B0B0F"
53
+ output to "card.mp4", resolution: "1080x1920", fps: 30
163
54
  ```
164
55
 
165
- `validate` warns `crowded-text-overlays` and `too-many-animations` on the
166
- code card, because each code line counts as an overlay. Accept those two
167
- there; fix any other warning. The `code_*` catalog blocks are 1920x1080
168
- designs: use them in 16:9, where they fill the frame. In 9:16 they shrink to
169
- 56 % with empty bands above and below.
56
+ Fights: a busy background behind the card (blur it more), or a product shot that
57
+ is already on white — then the card is redundant and a plain field is better.
170
58
 
171
- ## Talking head with captions (9:16, 14 s)
59
+ ## A code beat needs one line to land
172
60
 
173
- A 16:9 interview cut for a vertical feed: fill the frame by oversizing the
174
- clip (no `Crop`, so transitions stay legal), punch in on the key line, cut
175
- away to b-roll with a whip pan, and burn in short, timed caption cards.
61
+ A file on screen is not a beat; the line that matters is. Put the code in a
62
+ card, keep four to six lines, and let one line carry the highlight. Use
63
+ `animations.code_snippet_dark_2026` (or a portrait variant — see the record) for
64
+ the card, and place the changed line as its own text layer when it must be the
65
+ loudest thing in the frame. Use the `code` job in
66
+ [repertoire.md](repertoire.md#find-tools-by-job) to compare the card options.
176
67
 
177
- ```vidscript
178
- import motion from "@scenerok/basic-animations"
179
- import animations from "@scenerok/animations"
180
- import tr from "@scenerok/transitions"
181
-
182
- let ink = "#FFFFFF"
183
- let accent = "#FFD43B"
184
- let display = "Archivo Black"
185
-
186
- input talk = "https://cdn.example.com/interview-16x9.mp4" # 1920x1080, probed
187
- input broll = "https://cdn.example.com/broll-16x9.mp4" # 1920x1080, probed
188
- input bed = "https://cdn.example.com/music.mp3"
189
-
190
- # Fill 9:16 from 16:9: scale to 1920 tall (3413 wide) and centre it, x = (1080 - 3413) / 2.
191
- # Cut 1: wide, with a slow push. Cut 2: punch in on the key line. Cut 3: b-roll. Cut 4: back to the speaker.
192
- [0s .. 4s] = talk.Trim(12s, 16s), width: 3413, height: 1920, x: -1166, y: 0, animate: { type: "scale", from: { scale: 1 }, to: { scale: 1.05 }, easing: "linear" }
193
- [4s .. 7s] = talk.Trim(16s, 19s), width: 3413, height: 1920, x: -1166, y: 0, animate: { type: "scale", from: { scale: 1.18 }, to: { scale: 1.22 }, easing: "linear" }
194
- [6.8s .. 7.2s] = tr.whip_pan()
195
- [7.2s .. 10s] = broll.Trim(0s, 2.8s), width: 3413, height: 1920, x: -1166, y: 0, animate: { type: "scale", from: { scale: 1.1 }, to: { scale: 1 }, easing: "linear" }
196
- [9.8s .. 10.2s] = tr.dissolve()
197
- [10.2s .. 14s] = talk.Trim(22.2s, 26s), width: 3413, height: 1920, x: -1166, y: 0, animate: { type: "scale", from: { scale: 1 }, to: { scale: 1.05 }, easing: "linear" }
198
-
199
- # Name card from the catalog (it has a native 9:16 layout).
200
- [0.6s .. 4s] = animations.lt_kicker_name("FOUNDER", "Asha Rao", accent: accent)
201
-
202
- # Captions: 2-4 words per card, timed to the transcript, the key words in the accent colour.
203
- [0.2s .. 1.4s] = text "We shipped it", font: display, size: 72, color: ink, stroke: "#000000", stroke_width: 6, x: "50%", y: "70%", max_width: "84%", align: center, animate: motion.popIn(0.25s)
204
- [1.4s .. 2.8s] = text "in nine days", font: display, size: 72, color: accent, stroke: "#000000", stroke_width: 6, x: "50%", y: "70%", max_width: "84%", align: center, animate: motion.popIn(0.25s)
205
- [4s .. 5.6s] = text "and it paid", font: display, size: 72, color: ink, stroke: "#000000", stroke_width: 6, x: "50%", y: "70%", max_width: "84%", align: center, animate: motion.popIn(0.25s)
206
- [5.6s .. 7s] = text "for itself", font: display, size: 72, color: accent, stroke: "#000000", stroke_width: 6, x: "50%", y: "70%", max_width: "84%", align: center, animate: motion.popIn(0.25s)
207
-
208
- # Sound: the speaker's own audio, with music ducked under it.
209
- [0s .. 7s] = audio talk.Trim(12s, 19s)
210
- [10.2s .. 14s] = audio talk.Trim(22.2s, 26s)
211
- [0s .. 14s] = audio bed, volume: 0.5, duck_under: talk, fade_out: 1s
212
-
213
- [0s .. 14s] = filter "cdl", slope: "1.02 1.0 0.98", offset: "0 0 0", power: "1 1 1", saturation: 1.05
214
-
215
- output to "talk.mp4", resolution: "1080x1920", fps: 30
216
- ```
68
+ Fights: more than ~7 lines on a phone, a second animation in the same beat, or
69
+ copy that repeats the code's meaning.
70
+
71
+ ## A number deserves a count
72
+
73
+ When the beat is "how much", the number is the picture. A counter block
74
+ (`text_counter_stat`, `apple_money_count`) makes the eye follow the change;
75
+ static type makes it a caption. Fights: numbers that are not the point, or two
76
+ counters in one video.
77
+
78
+ ## A type-led beat is a legitimate beat
79
+
80
+ Some content *is* the words: a quote, a price, a definition, a breaking change,
81
+ a legal line. Still type on a quiet field, with generous margins and no motion,
82
+ respects the reader. Reach for it when motion would compete; do not treat it as
83
+ the safe default for a beat that has a real picture available.
84
+
85
+ ## Cut, unless the cut is the problem
217
86
 
218
- With a transcript in an SRT file, `captions("talk.srt", font: display, size: 72, area: "caption")`
219
- replaces the caption lines (`editing-actions.md` → Text and titles). On a
220
- hosted render the file must be an HTTPS URL; see `look-and-feel.md` → Files the
221
- server must read.
87
+ A hard cut is the strongest transition and the one an audience never notices. A
88
+ transition earns its place when it carries meaning — a wipe across a change of
89
+ place, a match on shape, a dissolve on a memory. Otherwise it delays the beat.
90
+ Compare the `transition` records before choosing one.