@vosjs/cli 0.52.0 → 0.52.2

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,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.