@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.
- package/dist/{chunk-F2QCWV4U.js → chunk-4Z6GQCXQ.js} +8 -2
- package/dist/{chunk-F2QCWV4U.js.map → chunk-4Z6GQCXQ.js.map} +1 -1
- package/dist/{chunk-KSEU3YK5.js → chunk-PJX3XE7G.js} +7 -2
- package/dist/{chunk-KSEU3YK5.js.map → chunk-PJX3XE7G.js.map} +1 -1
- package/dist/cli.js +71 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{run-ASYY2YTV.js → run-T5RKKGLE.js} +3 -3
- package/package.json +5 -5
- package/skills/VERSION +1 -1
- package/skills/vos-port/SKILL.md +152 -0
- package/skills/vos-port/references/hyperframes.md +36 -0
- package/skills/vos-port/references/intro-port.mjs +170 -0
- package/skills/vos-port/references/remotion.md +47 -0
- /package/dist/{run-ASYY2YTV.js.map → run-T5RKKGLE.js.map} +0 -0
|
@@ -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.
|
|
File without changes
|