@scenerok/sdk 0.9.0 → 0.11.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/dist/media.js CHANGED
@@ -1,38 +1,161 @@
1
1
  import { toSeconds } from './duration.js';
2
2
  import { getAudioLength, getVideoLength } from './stdlib.js';
3
+ import { isPictureNode, } from './types.js';
4
+ /** Length of a video whose length is not known, when it is appended with `push`. */
5
+ const UNKNOWN_VIDEO_LENGTH = 5;
6
+ /** Length of a still or text appended with `push` and no explicit duration. */
7
+ export const DEFAULT_STILL_LENGTH = 3;
3
8
  function normalizeAnims(animate) {
4
9
  if (!animate)
5
10
  return [];
6
11
  return Array.isArray(animate) ? [...animate] : [animate];
7
12
  }
8
- function resolveDeclaredDuration(opts, fallback) {
9
- if (opts?.duration !== undefined)
10
- return toSeconds(opts.duration);
11
- return fallback;
13
+ function requireFinite(value, method, what) {
14
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
15
+ throw new TypeError(`${method}() expected ${what}, got ${value === undefined ? 'nothing' : JSON.stringify(value)}`);
16
+ }
17
+ return value;
12
18
  }
13
- /** Create a video clip from a path/URL. Pass `duration` when the asset is not probed yet. */
14
- export function video(source, options = {}) {
19
+ /**
20
+ * A trim on a trimmed value is relative to what the first kept:
21
+ * `clip.trim(10, 20).trim(2, 4)` plays source 12–14 s. Past the kept range is an error.
22
+ */
23
+ export function composeTrim(previous, start, end) {
24
+ const base = previous.trimStart ?? 0;
25
+ const keptEnd = previous.trimEnd ?? 0;
26
+ const trimStart = start !== undefined ? base + toSeconds(start) : base;
27
+ const trimEnd = end !== undefined ? base + toSeconds(end) : keptEnd;
28
+ if (keptEnd > base && (trimEnd > keptEnd || trimStart > keptEnd)) {
29
+ throw new RangeError(`trim() is relative to the previous trim, which kept ${keptEnd - base}s; this one runs to ${Math.max(trimStart, trimEnd) - base}s`);
30
+ }
31
+ if (trimEnd > 0 && trimEnd <= trimStart) {
32
+ throw new RangeError(`trim() end (${trimEnd - base}s) must be after its start (${trimStart - base}s)`);
33
+ }
34
+ return { trimStart, trimEnd };
35
+ }
36
+ /** A crop on a cropped value is relative to the first, like `trim`. */
37
+ export function composeCrop(previous, x, y, width, height) {
38
+ for (const [value, what] of [[x, 'an x offset'], [y, 'a y offset'], [width, 'a width'], [height, 'a height']]) {
39
+ requireFinite(value, 'crop', what);
40
+ }
41
+ if (x < 0 || y < 0) {
42
+ throw new RangeError(`crop() offsets count from the top-left and cannot be negative, got (${x}, ${y})`);
43
+ }
44
+ if (width <= 0 || height <= 0) {
45
+ throw new RangeError(`crop() needs a positive width and height, got ${width}x${height}`);
46
+ }
47
+ if (previous && (x + width > previous.width || y + height > previous.height)) {
48
+ throw new RangeError(`crop() is relative to the previous crop, which kept ${previous.width}x${previous.height}; this one reaches ${x + width}x${y + height}`);
49
+ }
50
+ return { x: (previous?.x ?? 0) + x, y: (previous?.y ?? 0) + y, width, height };
51
+ }
52
+ const MASK_MODES = ['alpha', 'luma', 'inverted'];
53
+ export function makeMask(matte, options = {}) {
54
+ if (!isPictureNode(matte)) {
55
+ throw new TypeError('mask() needs a picture for its matte (video, image, text, solid, or timeline)');
56
+ }
57
+ const mode = options.mode ?? 'alpha';
58
+ if (!MASK_MODES.includes(mode)) {
59
+ throw new RangeError(`Unknown mask mode '${String(mode)}'. Use one of: ${MASK_MODES.join(', ')}.`);
60
+ }
61
+ return { matte, mode };
62
+ }
63
+ /** A grade scoped to one value. LUTs read a file at compile time, so they stay a VidScript statement. */
64
+ export function makeClipEffect(name, params = {}) {
65
+ if (!name || !name.trim())
66
+ throw new TypeError('filter() needs a filter name');
67
+ if (name.trim().toLowerCase() === 'lut') {
68
+ throw new TypeError('filter("lut") reads a .cube file at compile time; use VidScript: filter "lut", file: "look.cube"');
69
+ }
70
+ return { type: 'builtin', id: `fx_${name}`, name, start: 0, end: 0, params: { ...params } };
71
+ }
72
+ /** How long a clip plays: the source range at `|speed|`, `loop` times; 0 when unknown. */
73
+ export function playedLength(edits, mediaDuration) {
74
+ const trimStart = edits.trimStart ?? 0;
75
+ const trimEnd = edits.trimEnd ?? 0;
76
+ const speed = edits.speed ?? 1;
77
+ const loop = edits.loop ?? 1;
78
+ const sourceEnd = trimEnd > trimStart ? trimEnd : ((mediaDuration ?? 0) > trimStart ? mediaDuration : 0);
79
+ const span = sourceEnd - trimStart;
80
+ if (span <= 0 || speed === 0)
81
+ return 0;
82
+ return (span / Math.abs(speed)) * Math.max(1, Math.floor(loop));
83
+ }
84
+ function editsFromOptions(options) {
85
+ const edits = {};
86
+ if (options.trimStart !== undefined)
87
+ edits.trimStart = toSeconds(options.trimStart);
88
+ if (options.trimEnd !== undefined)
89
+ edits.trimEnd = toSeconds(options.trimEnd);
90
+ if (options.speed !== undefined)
91
+ edits.speed = options.speed;
92
+ if (options.loop !== undefined)
93
+ edits.loop = options.loop;
94
+ return edits;
95
+ }
96
+ function buildMedia(state) {
97
+ const { kind, source, options, edits, sequence } = state;
15
98
  const animations = normalizeAnims(options.animate);
16
- const duration = resolveDeclaredDuration(options, 5);
17
- const clip = {
99
+ const mediaDuration = kind === 'video' && options.duration !== undefined ? toSeconds(options.duration) : undefined;
100
+ const next = (patch) => buildMedia({ ...state, ...patch });
101
+ const withEdits = (patch) => next({ edits: { ...edits, ...patch } });
102
+ const node = {
18
103
  __sdk: true,
19
- kind: 'video',
104
+ kind,
20
105
  source,
21
106
  options: { ...options },
22
- duration,
107
+ edits: { ...edits },
108
+ ...(sequence ? { sequence } : {}),
109
+ mediaDuration,
110
+ get duration() {
111
+ if (kind === 'image')
112
+ return DEFAULT_STILL_LENGTH;
113
+ const played = playedLength(edits, mediaDuration);
114
+ return played > 0 ? played : UNKNOWN_VIDEO_LENGTH;
115
+ },
23
116
  animations,
24
- animate(anim) {
25
- const next = normalizeAnims(anim);
26
- return video(source, {
27
- ...options,
28
- animate: [...animations, ...next],
117
+ trim(start, end) {
118
+ return withEdits(composeTrim(edits, start, end));
119
+ },
120
+ speed(factor) {
121
+ return withEdits({ speed: requireFinite(factor, 'speed', 'a speed factor') });
122
+ },
123
+ loop(count) {
124
+ return withEdits({ loop: requireFinite(count, 'loop', 'a repeat count') });
125
+ },
126
+ resize(width, height) {
127
+ return withEdits({
128
+ width: requireFinite(width, 'resize', 'a width'),
129
+ height: requireFinite(height, 'resize', 'a height'),
29
130
  });
30
131
  },
132
+ opacity(value) {
133
+ return withEdits({ opacity: requireFinite(value, 'opacity', 'an opacity') });
134
+ },
135
+ filter(name, params) {
136
+ return withEdits({ effects: [...(edits.effects ?? []), makeClipEffect(name, params)] });
137
+ },
138
+ crop(x, y, width, height) {
139
+ return withEdits({ crop: composeCrop(edits.crop, x, y, width, height) });
140
+ },
141
+ mask(matte, maskOptions) {
142
+ if (edits.mask)
143
+ throw new Error('mask() is already applied to this value; one matte per value');
144
+ return withEdits({ mask: makeMask(matte, maskOptions) });
145
+ },
146
+ animate(anim) {
147
+ return next({ options: { ...options, animate: [...animations, ...normalizeAnims(anim)] } });
148
+ },
31
149
  with(opts) {
32
- return video(source, { ...options, ...opts, animate: opts.animate ?? options.animate });
150
+ const merged = { ...options, ...opts, animate: opts.animate ?? options.animate };
151
+ return next({ options: merged, edits: { ...edits, ...editsFromOptions(opts) } });
33
152
  },
34
153
  };
35
- return clip;
154
+ return node;
155
+ }
156
+ /** A video clip from a path/URL. Pass `duration` when the asset is not probed yet. */
157
+ export function video(source, options = {}) {
158
+ return buildMedia({ kind: 'video', source, options: { ...options }, edits: editsFromOptions(options) });
36
159
  }
37
160
  /** Probe a video duration when omitted, then create a video clip. */
38
161
  export async function loadVideo(source, options = {}) {
@@ -41,42 +164,81 @@ export async function loadVideo(source, options = {}) {
41
164
  const duration = await getVideoLength(source);
42
165
  return video(source, { ...options, duration });
43
166
  }
44
- /** Create a text layer. */
167
+ /** A still image. It fills its placement window (`image ≤ video`). */
168
+ export function image(source, options = {}) {
169
+ return buildMedia({ kind: 'image', source, options: { ...options }, edits: {} });
170
+ }
171
+ /**
172
+ * A numbered-frame source (`input frames = "shot.%04d.png", count: 24, rate: 12`).
173
+ * Plays `count / rate` seconds; `rate` defaults to the timeline fps.
174
+ */
175
+ export function imageSequence(pattern, options) {
176
+ const { first = 1, count, rate, ...rest } = options;
177
+ if (!/%0?\d*d/.test(pattern)) {
178
+ throw new TypeError(`imageSequence() needs a numbered pattern like "shot.%04d.png", got "${pattern}"`);
179
+ }
180
+ if (!Number.isInteger(count) || count <= 0) {
181
+ throw new RangeError(`imageSequence() needs a positive frame count, got ${String(count)}`);
182
+ }
183
+ const sequence = { first, count, ...(rate !== undefined ? { rate } : {}) };
184
+ const duration = rate !== undefined ? count / rate : undefined;
185
+ return buildMedia({
186
+ kind: 'video',
187
+ source: pattern,
188
+ options: { ...rest, ...(duration !== undefined ? { duration } : {}) },
189
+ edits: editsFromOptions(rest),
190
+ sequence,
191
+ });
192
+ }
193
+ /** A text layer. */
45
194
  export function text(content, options = {}) {
46
195
  const animations = normalizeAnims(options.animate);
47
- const layer = {
196
+ return {
48
197
  __sdk: true,
49
198
  kind: 'text',
50
199
  content,
51
200
  options: { ...options },
52
201
  animations,
53
202
  animate(anim) {
54
- const next = normalizeAnims(anim);
55
- return text(content, {
56
- ...options,
57
- animate: [...animations, ...next],
58
- });
203
+ return text(content, { ...options, animate: [...animations, ...normalizeAnims(anim)] });
59
204
  },
60
205
  with(opts) {
61
206
  return text(content, { ...options, ...opts, animate: opts.animate ?? options.animate });
62
207
  },
63
208
  };
64
- return layer;
65
209
  }
66
- /** Create an audio track. */
67
- export function audio(source, options = {}) {
68
- const duration = resolveDeclaredDuration(options, 5);
69
- const track = {
210
+ function buildAudio(state) {
211
+ const { source, options, trimEnd } = state;
212
+ const trimStart = options.trimStart !== undefined ? toSeconds(options.trimStart) : 0;
213
+ const declared = options.duration !== undefined ? toSeconds(options.duration) : undefined;
214
+ const known = trimEnd > trimStart ? trimEnd - trimStart : declared;
215
+ return {
70
216
  __sdk: true,
71
217
  kind: 'audio',
72
218
  source,
73
219
  options: { ...options },
74
- duration,
220
+ mediaDuration: known,
221
+ duration: known ?? UNKNOWN_VIDEO_LENGTH,
222
+ trim(start, end) {
223
+ const composed = composeTrim({ trimStart, trimEnd }, start, end);
224
+ // Without an out-point the length follows the in-point.
225
+ const remaining = composed.trimEnd > composed.trimStart
226
+ ? undefined
227
+ : (declared !== undefined ? Math.max(0, declared - (composed.trimStart - trimStart)) : undefined);
228
+ return buildAudio({
229
+ source,
230
+ options: { ...options, trimStart: composed.trimStart, ...(remaining !== undefined ? { duration: remaining } : {}) },
231
+ trimEnd: composed.trimEnd,
232
+ });
233
+ },
75
234
  with(opts) {
76
- return audio(source, { ...options, ...opts });
235
+ return buildAudio({ ...state, options: { ...options, ...opts } });
77
236
  },
78
237
  };
79
- return track;
238
+ }
239
+ /** An audio track. */
240
+ export function audio(source, options = {}) {
241
+ return buildAudio({ source, options: { ...options }, trimEnd: 0 });
80
242
  }
81
243
  /** Probe an audio duration when omitted, then create an audio track. */
82
244
  export async function loadAudio(source, options = {}) {
@@ -85,7 +247,35 @@ export async function loadAudio(source, options = {}) {
85
247
  const duration = await getAudioLength(source);
86
248
  return audio(source, { ...options, duration });
87
249
  }
88
- /** Builtin post-processing filter (e.g. glow, blur). */
250
+ /** A flat-colour rectangle — full frame unless sized. With `radius`/`corners` it is a shape matte. */
251
+ export function solid(color, options = {}) {
252
+ if (typeof color !== 'string' || !color.trim())
253
+ throw new TypeError('solid() needs a colour');
254
+ const animations = normalizeAnims(options.animate);
255
+ return {
256
+ __sdk: true,
257
+ kind: 'solid',
258
+ color,
259
+ options: { ...options },
260
+ animate(anim) {
261
+ return solid(color, { ...options, animate: [...animations, ...normalizeAnims(anim)] });
262
+ },
263
+ with(opts) {
264
+ return solid(color, { ...options, ...opts, animate: opts.animate ?? options.animate });
265
+ },
266
+ };
267
+ }
268
+ /** A transparent gap: `t.push(gap(), 2)` advances the playhead by 2 s. */
269
+ export function gap() {
270
+ return { __sdk: true, kind: 'gap' };
271
+ }
272
+ /** A named instant (`t[4][4] = marker('act 2')`). Exported as an OTIO marker; never rendered. */
273
+ export function marker(name, metadata) {
274
+ if (typeof name !== 'string' || !name.trim())
275
+ throw new TypeError('marker() needs a name');
276
+ return { __sdk: true, kind: 'marker', name, ...(metadata ? { metadata: { ...metadata } } : {}) };
277
+ }
278
+ /** Builtin post-processing filter over the frame (e.g. glow, blur). */
89
279
  export function filter(name, params = {}) {
90
280
  return {
91
281
  __sdk: true,
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Animation → motion-track lowering — the SDK's copy of `animationToMotionTracks`
3
+ * in the platform's `src/lib/motion/ir-motion.ts`. Renderers read motion tracks
4
+ * by target (`opacity`, `transform.position.y`, …), so an SDK animation must
5
+ * lower to the same tracks a VidScript `animate:` does.
6
+ */
7
+ import type { IRAnimation, IRMotionTrack } from './ir.js';
8
+ export declare function animationToMotionTracks(animation: IRAnimation): IRMotionTrack[];
9
+ export declare function animationsToMotionTracks(animations: IRAnimation[] | undefined): IRMotionTrack[];
10
+ //# sourceMappingURL=motion-tracks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"motion-tracks.d.ts","sourceRoot":"","sources":["../src/motion-tracks.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAiB,MAAM,SAAS,CAAC;AAsEzE,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,WAAW,GAAG,aAAa,EAAE,CA6F/E;AAED,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,WAAW,EAAE,GAAG,SAAS,GAAG,aAAa,EAAE,CAE/F"}
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Animation → motion-track lowering — the SDK's copy of `animationToMotionTracks`
3
+ * in the platform's `src/lib/motion/ir-motion.ts`. Renderers read motion tracks
4
+ * by target (`opacity`, `transform.position.y`, …), so an SDK animation must
5
+ * lower to the same tracks a VidScript `animate:` does.
6
+ */
7
+ const EPSILON = 0.0001;
8
+ function finiteNumber(value, fallback) {
9
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
10
+ }
11
+ function recordValue(value) {
12
+ return value && typeof value === 'object' && !Array.isArray(value)
13
+ ? value
14
+ : {};
15
+ }
16
+ function normalizeEasingName(easing) {
17
+ return (easing ?? '').toLowerCase().replace(/[-_\s]/g, '');
18
+ }
19
+ function motionTrack(animation, suffix, target, from, to, behavior) {
20
+ const springEasing = normalizeEasingName(animation.easing) === 'spring';
21
+ const keyframeEasing = springEasing ? 'linear' : animation.easing;
22
+ const resolvedBehavior = behavior
23
+ || (springEasing ? { type: 'spring' } : undefined)
24
+ || (animation.loop ? { type: 'loop' } : undefined);
25
+ return {
26
+ id: `${animation.id}_${suffix}`,
27
+ target,
28
+ keyframes: [
29
+ { time: animation.start, value: from },
30
+ { time: animation.end, value: to, easing: keyframeEasing },
31
+ ],
32
+ behavior: resolvedBehavior,
33
+ plugin: animation.plugin,
34
+ sourceAnimationId: animation.id,
35
+ };
36
+ }
37
+ function customTargetForKey(key) {
38
+ switch (key) {
39
+ case 'opacity':
40
+ return 'opacity';
41
+ case 'x':
42
+ case 'offsetX':
43
+ case 'translateX':
44
+ return 'transform.position.x';
45
+ case 'y':
46
+ case 'offsetY':
47
+ case 'translateY':
48
+ return 'transform.position.y';
49
+ case 'scale':
50
+ return 'transform.scale';
51
+ case 'scaleX':
52
+ return 'transform.scale.x';
53
+ case 'scaleY':
54
+ return 'transform.scale.y';
55
+ case 'rotation':
56
+ case 'degrees':
57
+ return 'transform.rotation.z';
58
+ default:
59
+ return key.startsWith('effect.') ? key : `custom.${key}`;
60
+ }
61
+ }
62
+ export function animationToMotionTracks(animation) {
63
+ const from = recordValue(animation.from);
64
+ const to = recordValue(animation.to);
65
+ const tracks = [];
66
+ switch (animation.type) {
67
+ case 'fade':
68
+ case 'opacity':
69
+ tracks.push(motionTrack(animation, 'opacity', 'opacity', finiteNumber(from.opacity, 0), finiteNumber(to.opacity, 1)));
70
+ break;
71
+ case 'slideX':
72
+ case 'translateX':
73
+ tracks.push(motionTrack(animation, 'x', 'transform.position.x', finiteNumber(from.x, 0), finiteNumber(to.x, 0)));
74
+ break;
75
+ case 'slideY':
76
+ case 'translateY':
77
+ tracks.push(motionTrack(animation, 'y', 'transform.position.y', finiteNumber(from.y, 0), finiteNumber(to.y, 0)));
78
+ break;
79
+ case 'scale': {
80
+ const fromScale = finiteNumber(from.scale ?? from.x, 1);
81
+ const toScale = finiteNumber(to.scale ?? to.x, 1);
82
+ tracks.push(motionTrack(animation, 'scale_x', 'transform.scale.x', fromScale, toScale));
83
+ tracks.push(motionTrack(animation, 'scale_y', 'transform.scale.y', finiteNumber(from.y, fromScale), finiteNumber(to.y, toScale)));
84
+ break;
85
+ }
86
+ case 'rotate':
87
+ case 'rotation':
88
+ tracks.push(motionTrack(animation, 'rotation', 'transform.rotation.z', finiteNumber(from.rotation ?? from.degrees, 0), finiteNumber(to.rotation ?? to.degrees, 0)));
89
+ break;
90
+ case 'pop': {
91
+ const fromScale = finiteNumber(from.scale, 0.55);
92
+ const overshootScale = finiteNumber(to.scale, 1.06);
93
+ const settleScale = finiteNumber(to.settleScale ?? to.finalScale, 1);
94
+ const overshootTime = animation.start + Math.max(EPSILON, animation.end - animation.start) * 0.72;
95
+ const behavior = animation.loop ? { type: 'loop' } : undefined;
96
+ for (const axis of ['x', 'y']) {
97
+ tracks.push({
98
+ id: `${animation.id}_scale_${axis}`,
99
+ target: `transform.scale.${axis}`,
100
+ keyframes: [
101
+ { time: animation.start, value: fromScale },
102
+ { time: overshootTime, value: overshootScale, easing: animation.easing },
103
+ { time: animation.end, value: settleScale, easing: 'easeOutCubic' },
104
+ ],
105
+ behavior,
106
+ plugin: animation.plugin,
107
+ sourceAnimationId: animation.id,
108
+ });
109
+ }
110
+ tracks.push(motionTrack(animation, 'opacity', 'opacity', finiteNumber(from.opacity, 0), finiteNumber(to.opacity, 1)));
111
+ break;
112
+ }
113
+ case 'rise':
114
+ tracks.push(motionTrack(animation, 'y', 'transform.position.y', finiteNumber(from.y, 80), finiteNumber(to.y, 0)));
115
+ tracks.push(motionTrack(animation, 'opacity', 'opacity', finiteNumber(from.opacity, 0), finiteNumber(to.opacity, 1)));
116
+ break;
117
+ case 'swing':
118
+ tracks.push(motionTrack(animation, 'rotation', 'transform.rotation.z', finiteNumber(from.rotation, -14), finiteNumber(to.rotation, 0)));
119
+ tracks.push(motionTrack(animation, 'y', 'transform.position.y', finiteNumber(from.y, 35), finiteNumber(to.y, 0)));
120
+ tracks.push(motionTrack(animation, 'opacity', 'opacity', finiteNumber(from.opacity, 0), finiteNumber(to.opacity, 1)));
121
+ break;
122
+ case 'glitch':
123
+ tracks.push(motionTrack(animation, 'opacity', 'opacity', finiteNumber(from.opacity, 0.2), finiteNumber(to.opacity, 1)));
124
+ tracks.push(motionTrack(animation, 'jitter_x', 'transform.position.x', 0, 0, { type: 'wiggle', params: { amplitude: 3.5, frequency: 30 } }));
125
+ tracks.push(motionTrack(animation, 'jitter_y', 'transform.position.y', 0, 0, { type: 'wiggle', params: { amplitude: 2.8, frequency: 34, phase: 0.25 } }));
126
+ break;
127
+ case 'float': {
128
+ const amplitude = Math.abs(finiteNumber(to.y, 12) - finiteNumber(from.y, -12)) / 2;
129
+ const duration = Math.max(EPSILON, animation.end - animation.start);
130
+ tracks.push(motionTrack(animation, 'float_y', 'transform.position.y', 0, 0, {
131
+ type: 'wiggle',
132
+ params: { amplitude, frequency: 1 / duration },
133
+ }));
134
+ break;
135
+ }
136
+ case 'typewriter':
137
+ tracks.push(motionTrack(animation, 'reveal', 'text.reveal', 0, 1, { type: 'typewriter' }));
138
+ break;
139
+ default: {
140
+ const keys = new Set([...Object.keys(from), ...Object.keys(to)]);
141
+ for (const key of keys) {
142
+ tracks.push(motionTrack(animation, key, customTargetForKey(key), (from[key] ?? 0), (to[key] ?? 1)));
143
+ }
144
+ }
145
+ }
146
+ return tracks;
147
+ }
148
+ export function animationsToMotionTracks(animations) {
149
+ return (animations || []).flatMap(animationToMotionTracks);
150
+ }
package/dist/motion.d.ts CHANGED
@@ -1,9 +1,43 @@
1
+ /**
2
+ * Animation descriptors — the same set as VidScript's `@scenerok/basic-animations`
3
+ * (`import fx from "@scenerok/basic-animations"`, `animate: fx.fadeIn(0.5s)`),
4
+ * with the same types and defaults, so an SDK `animate` lowers to the same IR.
5
+ */
1
6
  import { type TimeInput } from './duration.js';
2
7
  import type { AnimationDescriptor } from './types.js';
8
+ export interface AnimationOptions {
9
+ /** Easing name (`easeOutCubic`, `spring`, `linear`, …). */
10
+ easing?: string;
11
+ /** `end` aligns the animation to the end of the placement window (exits). */
12
+ align?: 'start' | 'end';
13
+ loop?: boolean;
14
+ }
15
+ /**
16
+ * Any animation by type — the SDK spelling of a VidScript object literal
17
+ * (`animate: { type: "kenBurns", from: { scale: 1 }, to: { scale: 1.1 } }`).
18
+ * A `duration` of 0 spans the whole placement window.
19
+ */
20
+ export declare function animation(type: string, from: Record<string, unknown>, to: Record<string, unknown>, duration?: TimeInput, options?: AnimationOptions): AnimationDescriptor;
3
21
  export declare function fadeIn(duration?: TimeInput): AnimationDescriptor;
4
22
  export declare function fadeOut(duration?: TimeInput): AnimationDescriptor;
5
23
  export declare function slideY(fromY?: number, toY?: number, duration?: TimeInput): AnimationDescriptor;
6
24
  export declare function slideX(fromX?: number, toX?: number, duration?: TimeInput): AnimationDescriptor;
25
+ /** Uniform scale from → to. */
7
26
  export declare function scale(from?: number, to?: number, duration?: TimeInput): AnimationDescriptor;
27
+ /** Scale + fade entrance with a slight overshoot. */
8
28
  export declare function popIn(duration?: TimeInput): AnimationDescriptor;
29
+ /** Upward entrance with a fade. */
30
+ export declare function riseIn(duration?: TimeInput, distance?: number): AnimationDescriptor;
31
+ /** Rotates in while sliding and fading. */
32
+ export declare function swingIn(duration?: TimeInput): AnimationDescriptor;
33
+ /** Jittery entrance with a flash. */
34
+ export declare function glitchIn(duration?: TimeInput): AnimationDescriptor;
35
+ /** Gentle looping vertical bob. */
36
+ export declare function float(duration?: TimeInput, amplitude?: number): AnimationDescriptor;
37
+ /** Reveals text character by character (`stagger` on the text picks words or chars). */
38
+ export declare function typewriter(duration?: TimeInput): AnimationDescriptor;
39
+ /** Constant rotation in degrees over the whole window (`rotation:` in VidScript). */
40
+ export declare function rotate(from: number, to?: number, duration?: TimeInput): AnimationDescriptor;
41
+ /** Gain automation for an audio track (`animate: { type: "volume", … }`). */
42
+ export declare function volumeRamp(from: number, to: number, duration?: TimeInput, options?: AnimationOptions): AnimationDescriptor;
9
43
  //# sourceMappingURL=motion.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"motion.d.ts","sourceRoot":"","sources":["../src/motion.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAqBtD,wBAAgB,MAAM,CAAC,QAAQ,GAAE,SAAe,GAAG,mBAAmB,CAErE;AAED,wBAAgB,OAAO,CAAC,QAAQ,GAAE,SAAe,GAAG,mBAAmB,CAEtE;AAED,wBAAgB,MAAM,CAAC,KAAK,SAAK,EAAE,GAAG,SAAI,EAAE,QAAQ,GAAE,SAAe,GAAG,mBAAmB,CAE1F;AAED,wBAAgB,MAAM,CAAC,KAAK,SAAM,EAAE,GAAG,SAAI,EAAE,QAAQ,GAAE,SAAe,GAAG,mBAAmB,CAE3F;AAED,wBAAgB,KAAK,CAAC,IAAI,SAAM,EAAE,EAAE,SAAI,EAAE,QAAQ,GAAE,SAAe,GAAG,mBAAmB,CAExF;AAED,wBAAgB,KAAK,CAAC,QAAQ,GAAE,SAAgB,GAAG,mBAAmB,CAErE"}
1
+ {"version":3,"file":"motion.d.ts","sourceRoot":"","sources":["../src/motion.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD,MAAM,WAAW,gBAAgB;IAC/B,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IACxB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3B,QAAQ,GAAE,SAAa,EACvB,OAAO,GAAE,gBAAqB,GAC7B,mBAAmB,CAWrB;AAED,wBAAgB,MAAM,CAAC,QAAQ,GAAE,SAAe,GAAG,mBAAmB,CAErE;AAED,wBAAgB,OAAO,CAAC,QAAQ,GAAE,SAAe,GAAG,mBAAmB,CAEtE;AAED,wBAAgB,MAAM,CAAC,KAAK,SAAK,EAAE,GAAG,SAAI,EAAE,QAAQ,GAAE,SAAe,GAAG,mBAAmB,CAE1F;AAED,wBAAgB,MAAM,CAAC,KAAK,SAAM,EAAE,GAAG,SAAI,EAAE,QAAQ,GAAE,SAAe,GAAG,mBAAmB,CAE3F;AAED,+BAA+B;AAC/B,wBAAgB,KAAK,CAAC,IAAI,SAAM,EAAE,EAAE,SAAI,EAAE,QAAQ,GAAE,SAAe,GAAG,mBAAmB,CAExF;AAED,qDAAqD;AACrD,wBAAgB,KAAK,CAAC,QAAQ,GAAE,SAAe,GAAG,mBAAmB,CAEpE;AAED,mCAAmC;AACnC,wBAAgB,MAAM,CAAC,QAAQ,GAAE,SAAgB,EAAE,QAAQ,SAAK,GAAG,mBAAmB,CAErF;AAED,2CAA2C;AAC3C,wBAAgB,OAAO,CAAC,QAAQ,GAAE,SAAe,GAAG,mBAAmB,CAEtE;AAED,qCAAqC;AACrC,wBAAgB,QAAQ,CAAC,QAAQ,GAAE,SAAgB,GAAG,mBAAmB,CAExE;AAED,mCAAmC;AACnC,wBAAgB,KAAK,CAAC,QAAQ,GAAE,SAAe,EAAE,SAAS,SAAK,GAAG,mBAAmB,CAEpF;AAED,wFAAwF;AACxF,wBAAgB,UAAU,CAAC,QAAQ,GAAE,SAAe,GAAG,mBAAmB,CAEzE;AAED,qFAAqF;AACrF,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAO,EAAE,QAAQ,GAAE,SAAa,GAAG,mBAAmB,CAE5F;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,GAAE,SAAa,EAAE,OAAO,GAAE,gBAAqB,GAAG,mBAAmB,CAEjI"}
package/dist/motion.js CHANGED
@@ -1,31 +1,71 @@
1
+ /**
2
+ * Animation descriptors — the same set as VidScript's `@scenerok/basic-animations`
3
+ * (`import fx from "@scenerok/basic-animations"`, `animate: fx.fadeIn(0.5s)`),
4
+ * with the same types and defaults, so an SDK `animate` lowers to the same IR.
5
+ */
1
6
  import { toSeconds } from './duration.js';
2
- function anim(type, from, to, duration, extra) {
7
+ /**
8
+ * Any animation by type — the SDK spelling of a VidScript object literal
9
+ * (`animate: { type: "kenBurns", from: { scale: 1 }, to: { scale: 1.1 } }`).
10
+ * A `duration` of 0 spans the whole placement window.
11
+ */
12
+ export function animation(type, from, to, duration = 0, options = {}) {
3
13
  return {
4
14
  __sdk: true,
5
15
  kind: 'animation',
6
16
  type,
7
- from,
8
- to,
17
+ from: { ...from },
18
+ to: { ...to },
9
19
  duration: toSeconds(duration),
10
20
  plugin: '@scenerok/basic-animations',
11
- ...extra,
21
+ ...options,
12
22
  };
13
23
  }
14
24
  export function fadeIn(duration = 0.5) {
15
- return anim('fade', { opacity: 0 }, { opacity: 1 }, duration);
25
+ return animation('fade', { opacity: 0 }, { opacity: 1 }, duration);
16
26
  }
17
27
  export function fadeOut(duration = 0.5) {
18
- return anim('fade', { opacity: 1 }, { opacity: 0 }, duration, { align: 'end' });
28
+ return animation('fade', { opacity: 1 }, { opacity: 0 }, duration, { align: 'end' });
19
29
  }
20
30
  export function slideY(fromY = 30, toY = 0, duration = 0.6) {
21
- return anim('translateY', { y: fromY }, { y: toY }, duration);
31
+ return animation('translateY', { y: fromY }, { y: toY }, duration);
22
32
  }
23
33
  export function slideX(fromX = -30, toX = 0, duration = 0.6) {
24
- return anim('translateX', { x: fromX }, { x: toX }, duration);
34
+ return animation('translateX', { x: fromX }, { x: toX }, duration);
25
35
  }
36
+ /** Uniform scale from → to. */
26
37
  export function scale(from = 0.8, to = 1, duration = 0.5) {
27
- return anim('scale', { scale: from }, { scale: to }, duration);
38
+ return animation('scale', { scale: from }, { scale: to }, duration);
28
39
  }
29
- export function popIn(duration = 0.45) {
30
- return anim('scale', { scale: 0.6, opacity: 0 }, { scale: 1, opacity: 1 }, duration);
40
+ /** Scale + fade entrance with a slight overshoot. */
41
+ export function popIn(duration = 0.7) {
42
+ return animation('pop', { scale: 0.55, opacity: 0 }, { scale: 1.06, opacity: 1 }, duration);
43
+ }
44
+ /** Upward entrance with a fade. */
45
+ export function riseIn(duration = 0.85, distance = 80) {
46
+ return animation('rise', { y: distance, opacity: 0 }, { y: 0, opacity: 1 }, duration);
47
+ }
48
+ /** Rotates in while sliding and fading. */
49
+ export function swingIn(duration = 0.9) {
50
+ return animation('swing', { rotation: -14, y: 35, opacity: 0 }, { rotation: 0, y: 0, opacity: 1 }, duration);
51
+ }
52
+ /** Jittery entrance with a flash. */
53
+ export function glitchIn(duration = 0.65) {
54
+ return animation('glitch', { opacity: 0 }, { opacity: 1 }, duration);
55
+ }
56
+ /** Gentle looping vertical bob. */
57
+ export function float(duration = 2.2, amplitude = 12) {
58
+ return animation('float', { y: -amplitude }, { y: amplitude }, duration, { loop: true });
59
+ }
60
+ /** Reveals text character by character (`stagger` on the text picks words or chars). */
61
+ export function typewriter(duration = 1.8) {
62
+ return animation('typewriter', {}, {}, duration);
63
+ }
64
+ /** Constant rotation in degrees over the whole window (`rotation:` in VidScript). */
65
+ export function rotate(from, to = from, duration = 0) {
66
+ return animation('rotation', { rotation: from }, { rotation: to }, duration);
67
+ }
68
+ /** Gain automation for an audio track (`animate: { type: "volume", … }`). */
69
+ export function volumeRamp(from, to, duration = 0, options = {}) {
70
+ return animation('volume', { volume: from }, { volume: to }, duration, options);
31
71
  }
@@ -1 +1 @@
1
- {"version":3,"file":"invoke.d.ts","sourceRoot":"","sources":["../../src/plugins/invoke.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC;AACvC,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,UAAU,GAAG,gBAAgB,CAAC;AAE1E,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CACJ,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,SAAS,OAAO,EAAE,EAC9B,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB;AAsBD,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG;IACzD,UAAU,EAAE,OAAO,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAiBA;AA8BD,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,iBAAiB,CAwBlE;AAgBD,gEAAgE;AAChE,wBAAsB,iBAAiB,CACrC,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,GAAG,IAAI,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,SAAS,CAAC,CAMpB;AAED,kEAAkE;AAClE,wBAAsB,iBAAiB,CACrC,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,GAAG,IAAI,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,UAAU,CAAC,CAMrB;AAED,mEAAmE;AACnE,wBAAsB,gBAAgB,CAAC,CAAC,GAAG,OAAO,EAChD,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,GAAG,IAAI,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,CAAC,CAAC,CAGZ;AAED,mFAAmF;AACnF,wBAAsB,YAAY,CAChC,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,GAAG,IAAI,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,iBAAiB,CAAC,CAE5B"}
1
+ {"version":3,"file":"invoke.d.ts","sourceRoot":"","sources":["../../src/plugins/invoke.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC;AACvC,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,UAAU,GAAG,gBAAgB,CAAC;AAE1E,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CACJ,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,SAAS,OAAO,EAAE,EAC9B,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB;AAsBD,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG;IACzD,UAAU,EAAE,OAAO,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAiBA;AA+BD,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,iBAAiB,CA2BlE;AAgBD,gEAAgE;AAChE,wBAAsB,iBAAiB,CACrC,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,GAAG,IAAI,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,SAAS,CAAC,CAMpB;AAED,kEAAkE;AAClE,wBAAsB,iBAAiB,CACrC,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,GAAG,IAAI,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,UAAU,CAAC,CAMrB;AAED,mEAAmE;AACnE,wBAAsB,gBAAgB,CAAC,CAAC,GAAG,OAAO,EAChD,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,GAAG,IAAI,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,CAAC,CAAC,CAGZ;AAED,mFAAmF;AACnF,wBAAsB,YAAY,CAChC,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,GAAG,IAAI,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,iBAAiB,CAAC,CAE5B"}
@@ -55,10 +55,13 @@ export function mapPluginResult(result) {
55
55
  return result;
56
56
  const r = result;
57
57
  if (r.type === 'video' && typeof r.url === 'string') {
58
+ // Like VidScript: the media's pixel size is not a placement size, and a
59
+ // generated clip with sound brings it along.
60
+ const metadata = (r.metadata && typeof r.metadata === 'object' ? r.metadata : {});
61
+ const nativeAudio = metadata.native_audio === true || metadata.has_audio === true;
58
62
  return video(r.url, {
59
63
  duration: typeof r.duration === 'number' ? r.duration : 5,
60
- width: typeof r.width === 'number' ? r.width : undefined,
61
- height: typeof r.height === 'number' ? r.height : undefined,
64
+ ...(nativeAudio ? { nativeAudio: true } : {}),
62
65
  });
63
66
  }
64
67
  if (r.type === 'audio' && typeof r.url === 'string') {
@@ -21,6 +21,9 @@ export interface Timeline extends TimelineLike {
21
21
  * t.push(text('Tail'))
22
22
  * return t.toIR()
23
23
  * ```
24
+ *
25
+ * Without `width`/`height`/`fps` it takes the host canvas (the importing
26
+ * VidScript's output, or its parent's when nested), else 1080×1920 at 30 fps.
24
27
  */
25
28
  export declare function timeline(options?: TimelineOptions): Timeline;
26
29
  export declare function isTimeline(value: unknown): value is Timeline;
@@ -1 +1 @@
1
- {"version":3,"file":"timeline.d.ts","sourceRoot":"","sources":["../src/timeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAU7E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,YAAY,EACZ,eAAe,EAEhB,MAAM,YAAY,CAAC;AAkCpB,MAAM,WAAW,QAAS,SAAQ,YAAY;IAC5C,mDAAmD;IACnD,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,WAAW,GAAG,OAAO,CAAC;IACvE,IAAI,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3B,KAAK,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAClE,IAAI,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACrD,IAAI,IAAI,UAAU,CAAC;CACpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,QAAQ,CA2EhE;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,QAAQ,CAO5D"}
1
+ {"version":3,"file":"timeline.d.ts","sourceRoot":"","sources":["../src/timeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0C,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAYvF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,KAAK,EAGV,WAAW,EACX,WAAW,EACX,YAAY,EACZ,eAAe,EAIhB,MAAM,YAAY,CAAC;AAmEpB,MAAM,WAAW,QAAS,SAAQ,YAAY;IAC5C,mDAAmD;IACnD,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,WAAW,GAAG,OAAO,CAAC;IACvE,IAAI,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3B,KAAK,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAClE,IAAI,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACrD,IAAI,IAAI,UAAU,CAAC;CACpB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,QAAQ,CAoGhE;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,QAAQ,CAQ5D"}