@vosjs/cli 0.52.0 → 0.52.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vosjs/cli",
3
- "version": "0.52.0",
3
+ "version": "0.52.1",
4
4
  "description": "The vos CLI: record the real product from a scripted browser flow, auto-zoom from the cursor track, cut as data in doc.json, render deterministic video and stills, deliver a release's media per destination spec, and sync with vos.so. One binary, every verb, MIT.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -49,14 +49,14 @@
49
49
  "dependencies": {
50
50
  "mediabunny": "^1.55.7",
51
51
  "playwright": "^1.49.0",
52
- "@vosjs/editor": "^1.3.1",
53
- "@vosjs/elements": "^0.8.1",
52
+ "@vosjs/elements": "^0.8.2",
53
+ "@vosjs/shared": "^0.4.1",
54
54
  "@vosjs/core": "^0.25.0",
55
55
  "@vosjs/render-core": "^0.3.6",
56
- "@vosjs/shared": "^0.4.1",
56
+ "@vosjs/editor": "^1.3.1",
57
57
  "@vosjs/timeline": "^0.4.1",
58
- "@vosjs/tween": "^0.8.2",
59
- "@vosjs/studio-core": "^0.32.0"
58
+ "@vosjs/studio-core": "^0.32.0",
59
+ "@vosjs/tween": "^0.8.2"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/node": "^22",
package/skills/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.1
1
+ 0.7.1
@@ -0,0 +1,152 @@
1
+ ---
2
+ name: vos-port
3
+ description: Bring a video someone made in Remotion or HyperFrames (or a hand-rolled HTML/canvas animation) into vos as a program whose words, colours and timings are knobs, so it keeps changing without code and lives on vos.so with versions, a studio and free export. Use when asked to "port", "convert", "move" or "bring" a Remotion or HyperFrames project to vos or vosso, to make such a video editable, or to put one on vos.so as more than a file.
4
+ license: MIT
5
+ ---
6
+
7
+ # Port a Remotion or HyperFrames video into vos
8
+
9
+ A port is a REWRITE into a vos program whose content is data. The source
10
+ keeps its look; what changes is that every word, colour and timing a person
11
+ would change becomes a value in `data` with a knob over it, so the next
12
+ change is a data edit (in the studio, or `vos push` of a patched value),
13
+ never another rewrite.
14
+
15
+ Two ways to get this wrong, both seen in real ports:
16
+
17
+ - **One canvas black box.** Painting the whole picture in one Canvas2D
18
+ function on a fullscreen quad matches the frames and leaves the studio
19
+ nothing to edit: every literal in the source becomes a literal in code.
20
+ - **A tabulated page.** Sampling the rendered DOM into CSS keyframes is
21
+ faithful and dead: thousands of sampled values, not one knob.
22
+
23
+ Read `references/intro-port.mjs` first: the Remotion showreel's intro scene,
24
+ ported by these rules and checked against Remotion's own render. It is the
25
+ shape every port takes: real functions, stringified into a `config.json`
26
+ (`node intro-port.mjs` writes one beside it).
27
+
28
+ ## The rules (the port grammar)
29
+
30
+ 1. **Content is data.** Every string, colour, number and asset a person
31
+ might change lives in `config.data`. Function strings hold none of it.
32
+ Timings live in data too (`data.t`), in SECONDS, converted from frames
33
+ once (`frames / fps`).
34
+ 2. **What people change is a knob.** `params` over those keys: `text` for
35
+ words, `color` for the palette (the source's own palette first), `font`
36
+ for faces. Prove each one moves the picture:
37
+ `vos still config.json k.png --time <t> --set data.<key>=<value>`.
38
+ 3. **Things are elements.** Text is a `text` element whose `content`,
39
+ `font.family` and `font.color` take `{ "$data": "<key>" }` (they
40
+ re-raster live on a data edit). Logos and shapes are `svg` or `image`
41
+ elements, footage a `video` element. Elements are what the studio selects
42
+ and moves.
43
+ 4. **Motion is the timeline.** `createTimeline` holds the tweens, on
44
+ `ctx.elements.get(id).props` (whole) or `.segments` (`split: { type:
45
+ 'chars' | 'words' | 'lines' }` units), in the GSAP dialect. Keep curves
46
+ exact: Remotion's `Easing.bezier(a, b, c, d)` is `ease: 'css-bezier(a, b,
47
+ c, d)'`; GSAP eases keep their names.
48
+ 5. **Scenes are labels.** One `tl.addLabel('<scene>', t)` per scene or
49
+ `<Sequence>`: the studio draws them on the program's timeline.
50
+ 6. **A painter only for procedural pixels.** Noise blobs, grain, a scramble,
51
+ a counter: `onFrame(ctx)` reads `ctx.time` and `ctx.data` and draws or
52
+ writes exactly those. Never the whole picture.
53
+ 7. **Sound is a track.** The score goes in `doc.json` beside the config
54
+ (`audio: [{ id, key: "score.ogg", name, start, in, out, duration, gain,
55
+ fadeIn, fadeOut }]`); `vos push` uploads it and `vos render` mixes it
56
+ (`@vosjs/cli` 0.52+). Never a `data:` URI in an audio element.
57
+ 8. **Faces from the catalog.** `fonts: [{ family, weight, url }]` with
58
+ `https://assets.vos.so/fonts/<slug>/<weight>.woff2` (`GET
59
+ https://vos.so/api/fonts`). A face the catalog lacks is SUBSTITUTED and
60
+ the substitution is said in the push note.
61
+
62
+ ## The coordinates, measured (these cost the reference port three rounds)
63
+
64
+ - **`props.x` / `props.y` are RENDER pixels, centre-relative, and `y` grows
65
+ DOWN**, like CSS `translateY`. A source offset in design pixels (a
66
+ 1080-high frame) is scaled: `const k = ctx.resolution.height / 1080`,
67
+ then `{ y: y0 + 190 * k }`. An unscaled offset looks right at 1080p and
68
+ doubles at 540p. Render at two sizes to catch one.
69
+ - **`rotation` is degrees, counter-clockwise**: CSS `rotate(12deg)` is
70
+ `rotation: -12`.
71
+ - **A static offset goes in the config**, `transform: { translateX,
72
+ translateY }` in design pixels, never a `tl.set` on `props.x/y`, for any
73
+ text whose content or colour changes live: every re-raster lays the
74
+ element out again from its config and drops a tweened position.
75
+ - **Bind a live text to its full words.** Text that `onFrame` writes each
76
+ frame (a scramble, a counter) starts as `{ "$data": "subtitle" }`, never
77
+ `' '`: a still capture (and so the vos.so thumbnail) misses what `onFrame`
78
+ writes, and then shows the bound words instead of nothing.
79
+ - **Transform origin is the centre.** Remotion's `transformOrigin: 'right
80
+ center'` with `scaleX` becomes a centre scale plus an `x` tween that keeps
81
+ the right edge still: `{ scaleX: 0, x: x0 + (width / 2) * k }`.
82
+
83
+ ## The mapping
84
+
85
+ - Remotion: `references/remotion.md`.
86
+ - HyperFrames: `references/hyperframes.md`.
87
+ - A hand-rolled page (a single HTML file, its own canvas engine): read it as
88
+ source with the same tables. What the page draws with DOM becomes
89
+ elements; what it paints in a canvas is a painter, kept to the procedural
90
+ parts.
91
+
92
+ ## The procedure
93
+
94
+ 1. **Inventory before writing.** From the source: size, fps, duration;
95
+ scenes and their frame windows; every visible string; every colour (CSS
96
+ custom properties, theme constants); fonts; media and audio files; the
97
+ variables or `defaultProps` (they become params). Write them into `data`
98
+ first. This IS the port's contract.
99
+ 2. **Get the source's own render.** Remotion: `npx remotion render <id>
100
+ out/source.mp4`. HyperFrames: `npx hyperframes render`. It is the
101
+ reference every check compares against.
102
+ 3. **Scaffold** a `build.mjs` from `references/intro-port.mjs`: real
103
+ functions stringified, a template-literal guard, `data`, `params`,
104
+ `fonts`, one element per string and shape, a label per scene.
105
+ 4. **Translate scene by scene** with the tables. One scene, then check it,
106
+ then the next.
107
+ 5. **Check** after every scene:
108
+
109
+ ```bash
110
+ node build.mjs && vos check config.json
111
+ vos still config.json v.png --times 0.5,1,2 --width 1920 --height 1080
112
+ ffmpeg -ss 1 -i out/source.mp4 -frames:v 1 r-1.png
113
+ ffmpeg -i r-1.png -i v-1.00s.png -filter_complex "[0:v][1:v]ssim" -f null - # SSIM
114
+ ffmpeg -i r-1.png -i v-1.00s.png -filter_complex hstack side.png # LOOK at it
115
+ vos render config.json small.mp4 --width 960 --height 540 # the scale check
116
+ ```
117
+
118
+ A number does not catch a missing element: look at every side-by-side.
119
+ Stills are for layout; text that `onFrame` writes shows in `vos render`
120
+ output, not in `vos still` (a known capture gap), so check it in a
121
+ rendered frame.
122
+ 6. **Knob honesty**: one `--set data.<key>=<value>` still per param.
123
+ 7. **Push** with the score: `vos push config.json --folder <slug> --label
124
+ "port of <source>" --note "<what was substituted, what is a painter>"
125
+ --wait`, and hand over the watch and studio links.
126
+
127
+ ## The honest gaps (say them in the push note)
128
+
129
+ These have no element equivalent today; each becomes a painter item or a
130
+ stated approximation:
131
+
132
+ | Source | In vos today |
133
+ | --- | --- |
134
+ | `overflow: hidden` masks, `clip-path` reveals | no element masks: approximate with opacity, or paint the item |
135
+ | a colour per split unit (`charStyle` making one letter red) | segments carry x/y/opacity/scale/rotation only: a separate element, or paint it |
136
+ | `mixBlendMode` | no blend modes on elements: paint it |
137
+ | a shape's colour as a knob | an svg's colours are compiled in (static `colors`): paint the shape if its colour must change live |
138
+ | `spring()` | no spring ease in the dialect: a `back.out(n)` of the same shape, checked by eye |
139
+ | `@remotion/noise`, SVG `feTurbulence` | the painter |
140
+ | a group transform (scale the whole scene) | no element groups: tween each element the same way |
141
+
142
+ Colours render as authored from `@vosjs/elements` 0.8.2 (older versions
143
+ drew text, svg and image elements lighter): match the source's hex values
144
+ exactly and never adjust a colour by eye.
145
+
146
+ ## When not to port
147
+
148
+ A piece that is mostly procedural canvas (a hand-rolled engine painting
149
+ every frame) has few elements to give: port it as a painter with its values
150
+ in `data`, and say that its knobs are the values, not the layout. A piece
151
+ whose owner only wants it hosted can be ingested as it is: `vos ingest
152
+ render.mp4` opens a finished render bare (0.52+).
@@ -0,0 +1,36 @@
1
+ # HyperFrames → vos
2
+
3
+ A HyperFrames composition is HTML and CSS moved by ONE paused GSAP timeline.
4
+ vos speaks the GSAP dialect, so most calls carry over nearly verbatim; what
5
+ changes is the target (an element's `props` instead of a DOM node) and the
6
+ units (render pixels, y down, degrees counter-clockwise).
7
+
8
+ | HyperFrames | vos |
9
+ | --- | --- |
10
+ | root `data-width`, `data-height`, `data-duration` | render size; `config.duration` |
11
+ | `data-composition-variables`, CSS custom properties on the root (`--bg`, `--accent`) | `data` keys and `params` (a colour variable is a `color` knob) |
12
+ | a text node or heading | a `text` element, `content: { "$data": key }` |
13
+ | inline `<svg>` / `<img>` | an `svg` element (`src` is the markup) / an `image` element, the file uploaded |
14
+ | `<video data-start data-media-start>` | a `video` element, windowed by opacity; no source in-point yet |
15
+ | `<audio data-start data-duration data-volume>` | `doc.json` `audio: [{ key, start, in: 0, out: duration, duration, gain: volume, fadeIn, fadeOut }]` |
16
+ | `gsap.timeline({ paused: true })` at `window.__timelines[id]` | `createTimeline` returns `ctx.gsap.timeline({ paused: true })`; vos drives it |
17
+ | `tl.to / from / fromTo / set` on a DOM node | the same call on `ctx.elements.get(id).props` |
18
+ | `x`, `y` in CSS pixels | `* k` (`k = ctx.resolution.height / 1080`); y grows down |
19
+ | `rotation` / `rotate` in degrees | negate it (vos is counter-clockwise) |
20
+ | SplitText-style per-character motion | `split: { type: 'chars' }` + tweens over `segments` |
21
+ | a sub-composition or scene `div` with a time window | `tl.addLabel(name, t)` and its elements tweened in and out inside the window |
22
+ | an `onUpdate` proxy clock that draws (`renderAll(t)`) | `onFrame(ctx)`, reading `ctx.time`, for exactly those procedural parts |
23
+ | GSAP ease names (`power3.out`, `back.out(1.7)`, `expo.inOut`) | the same names |
24
+ | CSS `--chrome` set by the timeline | a `data` value read in `onFrame`, or a tween on the element that shows it |
25
+ | faces resolved by the runtime from CSS families | `fonts: [{ family, weight, url }]` from the catalog |
26
+ | `mix-blend-mode`, `clip-path`, `overflow: hidden` reveals | GAPS: the painter, or an opacity approximation, said |
27
+
28
+ Two traps specific to HyperFrames sources:
29
+
30
+ - The runtime rewrites a sub-composition's CSS (it scopes selectors), which
31
+ can flip the cascade: the file you read is not always the document that
32
+ renders. When a colour or size disagrees with the render, believe the
33
+ render (`npx hyperframes render`), not the stylesheet.
34
+ - `window.__player.seek(t)` is the clock the renderer uses; seeking
35
+ `__timelines.main` does not drive a mounted sub-composition. Read times
36
+ from the render, not by scrubbing the main timeline.
@@ -0,0 +1,170 @@
1
+ // The Remotion showreel's INTRO scene (62 frames at 30 fps), ported to a
2
+ // data-first vos program. Authored as real functions, stringified here.
3
+ import { writeFileSync } from 'node:fs'
4
+
5
+ const FPS = 30
6
+ const f = (frames) => +(frames / FPS).toFixed(4) // Remotion frames → seconds
7
+
8
+ // Remotion Easing.bezier curves, spelled as the dialect's css-bezier.
9
+ const EXPO_OUT = 'css-bezier(0.16, 1, 0.3, 1)'
10
+ const EXPO_IN = 'css-bezier(0.7, 0, 0.84, 0)'
11
+ const IN_OUT = 'css-bezier(0.83, 0, 0.17, 1)'
12
+
13
+ function createContent(ctx) {
14
+ // The ground is a knob: read in onFrame, never baked.
15
+ ctx.scene.background = new ctx.THREE.Color(ctx.data.ink)
16
+ return { objects: [], refs: {}, dispose: () => {} }
17
+ }
18
+
19
+ function createTimeline(ctx, content, duration) {
20
+ const { gsap, elements, data } = ctx
21
+ const T = data.t
22
+ // Element props x/y are RENDER pixels; the source's offsets are design
23
+ // pixels on a 1080-high frame. Scale every offset by k.
24
+ const k = ctx.resolution.height / 1080
25
+ const tl = gsap.timeline({ paused: true })
26
+ tl.addLabel('intro', 0)
27
+
28
+ // The pill: a 40px dot that pops, stretches to a line, drops, and leaves
29
+ // toward its right end (Remotion's transformOrigin: right center).
30
+ const line = elements.get('line').props
31
+ const y0 = line.y
32
+ const x0 = line.x
33
+ tl.fromTo(line, { scale: 0 }, { scale: 1, duration: T.pop, ease: 'back.out(2.2)' }, 0)
34
+ tl.fromTo(line, { scaleX: 40 / 1180 }, { scaleX: 1, duration: T.stretch, ease: T.expoOut }, T.stretchAt)
35
+ tl.fromTo(line, { scaleY: 1 }, { scaleY: 0.2, duration: T.thin, ease: T.expoOut }, T.stretchAt)
36
+ tl.fromTo(line, { y: y0 }, { y: y0 + 190 * k, duration: T.drop, ease: T.expoOut }, T.dropAt)
37
+ tl.to(line, { scaleX: 0, x: x0 + 590 * k, duration: T.leave, ease: T.expoIn }, T.leaveAt)
38
+
39
+ // The title rises letter by letter, then leaves upward.
40
+ tl.addLabel('title', T.titleAt)
41
+ const segs = (elements.get('title') || {}).segments || []
42
+ segs.forEach((seg, i) => {
43
+ const at = T.titleAt + i * T.stagger
44
+ tl.fromTo(
45
+ seg,
46
+ { y: seg.y + 380 * k, rotation: -12, opacity: 0 },
47
+ { y: seg.y, rotation: 0, opacity: 1, duration: T.rise, ease: T.expoOut },
48
+ at,
49
+ )
50
+ tl.to(seg, { y: seg.y - 380 * k, opacity: 0, duration: T.exit, ease: T.expoIn }, T.leaveAt + i * T.exitStagger)
51
+ })
52
+
53
+ // The subtitle's words are onFrame's; its place is its config transform,
54
+ // because every content write re-lays it out from the config.
55
+ const sub = elements.get('subtitle').props
56
+ tl.to(sub, { opacity: 0, duration: T.leave, ease: T.expoIn }, T.leaveAt)
57
+
58
+ // The red wipe grows from the centre and covers the frame.
59
+ tl.addLabel('wipe', T.wipeAt)
60
+ const wipe = elements.get('wipe').props
61
+ tl.fromTo(wipe, { scale: 0 }, { scale: 1, duration: T.wipe, ease: T.inOut }, T.wipeAt)
62
+
63
+ tl.to({}, { duration: 0.01 }, duration)
64
+ return tl
65
+ }
66
+
67
+ function onFrame(ctx) {
68
+ const d = ctx.data
69
+ ctx.scene.background.set(d.ink)
70
+ // Scramble: a typewriter whose leading edge is glyph noise, a pure
71
+ // function of time and the words in data.
72
+ const el = ctx.elements.get('subtitle')
73
+ if (!el) return
74
+ const frame = ctx.time * 30
75
+ const n = Math.max(0, (frame - d.t.scrambleAt * 30) * 1.4)
76
+ const text = String(d.subtitle)
77
+ const glyphs = '!<>-_\\/[]{}=+*^?#01ABCDEFXYZ'
78
+ let out = ''
79
+ for (let i = 0; i < text.length; i++) {
80
+ if (i < Math.floor(n) - 3) out += text[i]
81
+ else if (i < n) out += text[i] === ' ' ? ' ' : glyphs[(i * 7 + Math.floor(frame)) % glyphs.length]
82
+ else out += ' '
83
+ }
84
+ if (el.props.content !== out) el.props.content = out
85
+ }
86
+
87
+ const config = {
88
+ version: 2,
89
+ duration: f(62),
90
+ camera: { preset: 'fullscreen' },
91
+ fonts: [
92
+ { family: 'Anton', weight: 400, url: 'https://assets.vos.so/fonts/anton/400.woff2' },
93
+ { family: 'JetBrains Mono', weight: 500, url: 'https://assets.vos.so/fonts/jetbrains-mono/500.woff2' },
94
+ ],
95
+ elements: [
96
+ {
97
+ id: 'line',
98
+ type: 'svg',
99
+ src: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1180 40"><rect width="1180" height="40" rx="20" fill="#F2EFE8"/></svg>',
100
+ position: 'center',
101
+ },
102
+ {
103
+ id: 'title',
104
+ type: 'text',
105
+ content: { $data: 'title' },
106
+ font: { family: 'Anton', size: 330, weight: 400, color: { $data: 'paper' }, letterSpacing: -4 },
107
+ split: { type: 'chars' },
108
+ position: 'center',
109
+ },
110
+ {
111
+ id: 'subtitle',
112
+ type: 'text',
113
+ // Bound to the full words: onFrame writes each frame's string, and a
114
+ // still capture, which misses onFrame's writes, shows these words.
115
+ content: { $data: 'subtitle' },
116
+ font: { family: 'JetBrains Mono', size: 26, weight: 500, color: { $data: 'paper' }, letterSpacing: 6 },
117
+ position: 'center',
118
+ transform: { translateY: 238 },
119
+ },
120
+ {
121
+ id: 'wipe',
122
+ type: 'svg',
123
+ src: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2500 2500"><circle cx="1250" cy="1250" r="1250" fill="#FF4D2E"/></svg>',
124
+ position: 'center',
125
+ },
126
+ ],
127
+ data: {
128
+ ink: '#0B0B0F',
129
+ paper: '#F2EFE8',
130
+ title: 'MOTION',
131
+ subtitle: 'A SHOWREEL WRITTEN ENTIRELY IN CODE',
132
+ // Every timing the scene has, in seconds, from Remotion's frames.
133
+ t: {
134
+ pop: f(8),
135
+ stretchAt: f(8),
136
+ stretch: f(16),
137
+ thin: f(8),
138
+ dropAt: f(14),
139
+ drop: f(14),
140
+ titleAt: f(14),
141
+ stagger: f(2.2),
142
+ rise: f(16),
143
+ scrambleAt: f(24),
144
+ leaveAt: f(42),
145
+ leave: f(12),
146
+ exit: f(10),
147
+ exitStagger: f(1.2),
148
+ wipeAt: f(46),
149
+ wipe: f(16),
150
+ expoOut: EXPO_OUT,
151
+ expoIn: EXPO_IN,
152
+ inOut: IN_OUT,
153
+ },
154
+ },
155
+ params: [
156
+ { key: 'title', kind: 'text', default: 'MOTION', hint: 'The word that rises' },
157
+ { key: 'subtitle', kind: 'text', default: 'A SHOWREEL WRITTEN ENTIRELY IN CODE', hint: 'The line typed under it' },
158
+ { key: 'paper', kind: 'color', default: '#F2EFE8', hint: 'Type colour' },
159
+ { key: 'ink', kind: 'color', default: '#0B0B0F', hint: 'Ground' },
160
+ ],
161
+ createContent: createContent.toString(),
162
+ createTimeline: createTimeline.toString(),
163
+ onFrame: onFrame.toString(),
164
+ }
165
+
166
+ for (const k of ['createContent', 'createTimeline', 'onFrame']) {
167
+ if (config[k].includes('${')) throw new Error(`${k} holds a template literal`)
168
+ }
169
+ writeFileSync(new URL('./config.json', import.meta.url), JSON.stringify(config, null, 2))
170
+ console.log('wrote config.json')
@@ -0,0 +1,47 @@
1
+ # Remotion → vos
2
+
3
+ Remotion's model is a pure function of the frame; vos's is a timeline the
4
+ engine seeks. Most of a port is turning `interpolate(frame, …)` calls into
5
+ tweens that start and end at the same seconds with the same curve.
6
+ Remotion's compositing (a persistent ground under transparent scenes that
7
+ cross-fade) is vos's, so cross-fades port with no compensation.
8
+
9
+ The interpolate, bezier, translate, rotate, origin, per-letter, scramble and
10
+ ground rows were checked against Remotion's render in `intro-port.mjs`; GAP
11
+ rows name what has no element equivalent yet.
12
+
13
+ | Remotion | vos |
14
+ | --- | --- |
15
+ | `<Composition width height fps durationInFrames defaultProps schema>` | `config.duration = durationInFrames / fps`; render at `--width/--height`; `defaultProps` → `data`; the zod `schema` → `params` kinds (`z.string()` text, a colour string `color`) |
16
+ | `useCurrentFrame()` | nothing in a tween (the timeline is the clock); `ctx.time * fps` inside `onFrame` |
17
+ | `interpolate(f, [a, b], [x, y], { easing, extrapolate: 'clamp' })` | `tl.fromTo(props, { p: x }, { p: y, duration: (b - a) / fps, ease }, a / fps)` |
18
+ | `Easing.bezier(x1, y1, x2, y2)` | `ease: 'css-bezier(x1, y1, x2, y2)'` (exact) |
19
+ | `spring({ frame, fps, config })` | GAP: no spring ease; `back.out(n)` of a similar overshoot, checked by eye |
20
+ | `<Sequence from durationInFrames>` | `tl.addLabel(name, from / fps)` and the scene's elements tweened in and out inside that window |
21
+ | `<TransitionSeries>` + `fade()` | opacity tweens across the overlap window |
22
+ | `<AbsoluteFill>` + flex centring | `position: 'center'`; offsets in `transform: { translateX, translateY }` (design px) |
23
+ | `translateY(Npx)` animated | `props.y` += `N * k` (`k = ctx.resolution.height / 1080`; y grows down, like CSS) |
24
+ | `rotate(Ndeg)` | `rotation: -N` (degrees, counter-clockwise) |
25
+ | `transformOrigin` other than centre | a centre transform plus a compensating `x`/`y` tween |
26
+ | a React text component per letter (`RiseText`) | `split: { type: 'chars' }` + one tween per `segments[i]` at `start + i * stagger` |
27
+ | `charStyle` giving one letter a colour | GAP: segments carry no colour; a separate element or a painter |
28
+ | `overflow: hidden` letter masks | GAP: no element masks; opacity approximates |
29
+ | a string rebuilt per frame (`Scramble`, a counter) | `onFrame` writes `ctx.elements.get(id).props.content`; the element sized by the full words first |
30
+ | `<Img src={staticFile('x.png')}>` | an `image` element; the file uploaded (`vos asset push x.png`) and its url in `data` |
31
+ | `<Audio src={staticFile('s.mp3')} />` | `doc.json` `audio: [{ key: 's.mp3', start, in, out, duration, gain, fadeIn, fadeOut }]` |
32
+ | `<Video>` / `<OffthreadVideo>` | a `video` element, windowed by opacity; a source in-point is not supported yet |
33
+ | `@remotion/google-fonts` `loadFont()` | `fonts: [{ family, weight, url }]` from `https://vos.so/api/fonts`; a face the catalog lacks is substituted and said |
34
+ | `@remotion/noise` `noise3D`, SVG `feTurbulence` grain | the painter, reading its seed and amounts from `data` |
35
+ | `mixBlendMode` | GAP: the painter |
36
+ | an inline `<svg>` shape | an `svg` element (`src` is the markup); its colours are static, so a shape colour that must be a knob is painted |
37
+ | a solid background | `ctx.scene.background = new ctx.THREE.Color(ctx.data.ink)` in `createContent`, `.set(ctx.data.ink)` in `onFrame` |
38
+
39
+ Two traps specific to Remotion sources:
40
+
41
+ - A side-effect-only font import can be tree-shaken by the project's
42
+ `sideEffects` field, so the SOURCE render may already be in a fallback
43
+ face. Compare the source render's headline width against the declared
44
+ face before blaming the port.
45
+ - `durationInFrames` counts frames, and `interpolate` ranges are frames:
46
+ convert every number once, in `data.t`, and never mix frames and seconds
47
+ inside a function.