reframe-video 0.6.5 → 0.6.6

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/index.js CHANGED
@@ -357,6 +357,7 @@ var PROPS_BY_TYPE = {
357
357
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
358
358
  text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
359
359
  image: [...COMMON_PROPS, "src", "width", "height", "fit"],
360
+ video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart"],
360
361
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
361
362
  group: COMMON_PROPS
362
363
  };
@@ -654,6 +655,10 @@ function image(props) {
654
655
  const { id, ...rest } = props;
655
656
  return { type: "image", id, props: rest };
656
657
  }
658
+ function video(props) {
659
+ const { id, ...rest } = props;
660
+ return { type: "video", id, props: rest };
661
+ }
657
662
  function path(props) {
658
663
  const { id, ...rest } = props;
659
664
  return { type: "path", id, props: rest };
@@ -3187,6 +3192,33 @@ function evaluate(compiled, t) {
3187
3192
  });
3188
3193
  return;
3189
3194
  }
3195
+ case "video": {
3196
+ const width = num(id, "width", node.props.width);
3197
+ const height = num(id, "height", node.props.height);
3198
+ const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
3199
+ const fps = compiled.ir.fps ?? 30;
3200
+ const start = node.props.start ?? 0;
3201
+ const rate = node.props.rate ?? 1;
3202
+ const clipStart = node.props.clipStart ?? 0;
3203
+ const srcT = clipStart + Math.max(0, t - start) * rate;
3204
+ const frame = Math.max(0, Math.round(srcT * fps));
3205
+ ops.push({
3206
+ type: "video",
3207
+ id,
3208
+ transform: matrix,
3209
+ opacity,
3210
+ src: str(id, "src", node.props.src),
3211
+ width,
3212
+ height,
3213
+ offsetX: -width * ax,
3214
+ offsetY: -height * ay,
3215
+ frame,
3216
+ ...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
3217
+ ...fx,
3218
+ ...clipSpread
3219
+ });
3220
+ return;
3221
+ }
3190
3222
  case "path": {
3191
3223
  const ox = num(id, "originX", node.props.originX ?? 0);
3192
3224
  const oy = num(id, "originY", node.props.originY ?? 0);
@@ -3255,13 +3287,13 @@ function evaluate(compiled, t) {
3255
3287
  }
3256
3288
 
3257
3289
  // ../core/src/assets.ts
3258
- function collectImageSrcs(ir) {
3290
+ function collectSrcs(ir, type) {
3259
3291
  const srcs = /* @__PURE__ */ new Set();
3260
- const imageIds = /* @__PURE__ */ new Set();
3292
+ const ids = /* @__PURE__ */ new Set();
3261
3293
  const walkNodes = (nodes) => {
3262
3294
  for (const node of nodes) {
3263
- if (node.type === "image") {
3264
- imageIds.add(node.id);
3295
+ if (node.type === type) {
3296
+ ids.add(node.id);
3265
3297
  srcs.add(node.props.src);
3266
3298
  }
3267
3299
  if (node.type === "group") walkNodes(node.children);
@@ -3270,14 +3302,14 @@ function collectImageSrcs(ir) {
3270
3302
  walkNodes(ir.nodes);
3271
3303
  for (const overrides of Object.values(ir.states ?? {})) {
3272
3304
  for (const [nodeId, props] of Object.entries(overrides)) {
3273
- if (imageIds.has(nodeId) && typeof props.src === "string") srcs.add(props.src);
3305
+ if (ids.has(nodeId) && typeof props.src === "string") srcs.add(props.src);
3274
3306
  }
3275
3307
  }
3276
3308
  const walkTimeline = (step) => {
3277
3309
  if (!step) return;
3278
3310
  if (step.kind === "seq" || step.kind === "par" || step.kind === "stagger") {
3279
3311
  for (const child of step.children) walkTimeline(child);
3280
- } else if (step.kind === "tween" && imageIds.has(step.target)) {
3312
+ } else if (step.kind === "tween" && ids.has(step.target)) {
3281
3313
  const src = step.props.src;
3282
3314
  if (typeof src === "string") srcs.add(src);
3283
3315
  }
@@ -3285,6 +3317,12 @@ function collectImageSrcs(ir) {
3285
3317
  walkTimeline(ir.timeline);
3286
3318
  return [...srcs];
3287
3319
  }
3320
+ function collectImageSrcs(ir) {
3321
+ return collectSrcs(ir, "image");
3322
+ }
3323
+ function collectVideoSrcs(ir) {
3324
+ return collectSrcs(ir, "video");
3325
+ }
3288
3326
 
3289
3327
  // ../core/src/motion.ts
3290
3328
  var EASE_BY_CLASS = {
@@ -3351,6 +3389,7 @@ export {
3351
3389
  cameraTo,
3352
3390
  characterPreset,
3353
3391
  collectImageSrcs,
3392
+ collectVideoSrcs,
3354
3393
  compileComposition,
3355
3394
  compileScene,
3356
3395
  composeScene,
@@ -3417,6 +3456,7 @@ export {
3417
3456
  tween,
3418
3457
  validateComposition,
3419
3458
  validateScene,
3459
+ video,
3420
3460
  wait,
3421
3461
  wiggle
3422
3462
  };
package/dist/labels.js CHANGED
@@ -341,6 +341,7 @@ var PROPS_BY_TYPE = {
341
341
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
342
342
  text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
343
343
  image: [...COMMON_PROPS, "src", "width", "height", "fit"],
344
+ video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart"],
344
345
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
345
346
  group: COMMON_PROPS
346
347
  };
@@ -25,7 +25,7 @@ function resolvePaint(ctx, paint, box) {
25
25
  for (const s of paint.stops) g.addColorStop(Math.max(0, Math.min(1, s.offset)), s.color);
26
26
  return g;
27
27
  }
28
- function renderFrame(ctx, compiled, t, images) {
28
+ function renderFrame(ctx, compiled, t, images, videos) {
29
29
  const { size, background } = compiled.ir;
30
30
  ctx.setTransform(1, 0, 0, 1, 0, 0);
31
31
  ctx.clearRect(0, 0, size.width, size.height);
@@ -33,9 +33,9 @@ function renderFrame(ctx, compiled, t, images) {
33
33
  ctx.fillStyle = background;
34
34
  ctx.fillRect(0, 0, size.width, size.height);
35
35
  }
36
- drawDisplayList(ctx, evaluate(compiled, t), images);
36
+ drawDisplayList(ctx, evaluate(compiled, t), images, videos);
37
37
  }
38
- function drawDisplayList(ctx, ops, images) {
38
+ function drawDisplayList(ctx, ops, images, videos) {
39
39
  for (const op of ops) {
40
40
  ctx.save();
41
41
  if (op.clips) {
@@ -117,28 +117,11 @@ function drawDisplayList(ctx, ops, images) {
117
117
  break;
118
118
  }
119
119
  case "image": {
120
- const img = images?.get(op.src);
121
- if (img) {
122
- if (op.fit === "cover") {
123
- const [iw, ih] = intrinsicSize(img);
124
- const { sx, sy, sw, sh } = coverRect(iw, ih, op.width, op.height);
125
- ctx.drawImage(img, sx, sy, sw, sh, op.offsetX, op.offsetY, op.width, op.height);
126
- } else {
127
- ctx.drawImage(img, op.offsetX, op.offsetY, op.width, op.height);
128
- }
129
- } else {
130
- ctx.fillStyle = "#2A2A30";
131
- ctx.fillRect(op.offsetX, op.offsetY, op.width, op.height);
132
- ctx.strokeStyle = "#FF00FF";
133
- ctx.lineWidth = 2;
134
- ctx.strokeRect(op.offsetX, op.offsetY, op.width, op.height);
135
- ctx.beginPath();
136
- ctx.moveTo(op.offsetX, op.offsetY);
137
- ctx.lineTo(op.offsetX + op.width, op.offsetY + op.height);
138
- ctx.moveTo(op.offsetX + op.width, op.offsetY);
139
- ctx.lineTo(op.offsetX, op.offsetY + op.height);
140
- ctx.stroke();
141
- }
120
+ drawRaster(ctx, images?.get(op.src), op);
121
+ break;
122
+ }
123
+ case "video": {
124
+ drawRaster(ctx, videos?.frame(op.src, op.frame), op);
142
125
  break;
143
126
  }
144
127
  case "path": {
@@ -193,6 +176,29 @@ function intrinsicSize(img) {
193
176
  const a = img;
194
177
  return [a.naturalWidth || a.width || 0, a.naturalHeight || a.height || 0];
195
178
  }
179
+ function drawRaster(ctx, img, op) {
180
+ if (img) {
181
+ if (op.fit === "cover") {
182
+ const [iw, ih] = intrinsicSize(img);
183
+ const { sx, sy, sw, sh } = coverRect(iw, ih, op.width, op.height);
184
+ ctx.drawImage(img, sx, sy, sw, sh, op.offsetX, op.offsetY, op.width, op.height);
185
+ } else {
186
+ ctx.drawImage(img, op.offsetX, op.offsetY, op.width, op.height);
187
+ }
188
+ return;
189
+ }
190
+ ctx.fillStyle = "#2A2A30";
191
+ ctx.fillRect(op.offsetX, op.offsetY, op.width, op.height);
192
+ ctx.strokeStyle = "#FF00FF";
193
+ ctx.lineWidth = 2;
194
+ ctx.strokeRect(op.offsetX, op.offsetY, op.width, op.height);
195
+ ctx.beginPath();
196
+ ctx.moveTo(op.offsetX, op.offsetY);
197
+ ctx.lineTo(op.offsetX + op.width, op.offsetY + op.height);
198
+ ctx.moveTo(op.offsetX + op.width, op.offsetY);
199
+ ctx.lineTo(op.offsetX, op.offsetY + op.height);
200
+ ctx.stroke();
201
+ }
196
202
  function quoteFamily(family) {
197
203
  return family.includes(" ") && !family.includes('"') ? `"${family}"` : family;
198
204
  }
package/dist/trace-cli.js CHANGED
@@ -14,6 +14,7 @@ var PROPS_BY_TYPE = {
14
14
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
15
15
  text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
16
16
  image: [...COMMON_PROPS, "src", "width", "height", "fit"],
17
+ video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart"],
17
18
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
18
19
  group: COMMON_PROPS
19
20
  };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Asset discovery shared by every consumer that must preload images before
2
+ * Asset discovery shared by every consumer that must preload media before
3
3
  * rendering (the capture page and the preview). One walker means the two
4
4
  * sides can never disagree about which srcs a scene uses — including srcs
5
5
  * introduced only mid-scene by a state override or a tween.
@@ -7,3 +7,5 @@
7
7
  import type { SceneIR } from "./ir.js";
8
8
  /** All image srcs a scene can ever display, deduped, in discovery order. */
9
9
  export declare function collectImageSrcs(ir: SceneIR): string[];
10
+ /** All video srcs a scene can ever display, deduped, in discovery order. */
11
+ export declare function collectVideoSrcs(ir: SceneIR): string[];
@@ -2,7 +2,7 @@
2
2
  * The eDSL surface: thin factories that build plain IR objects.
3
3
  * `scene()` validates and returns the IR — the return value is the document.
4
4
  */
5
- import type { AudioIR, BehaviorIR, CameraIR, CompositionIR, CompositionSceneEntry, Ease, EllipseProps, GroupProps, ImageProps, LineProps, NodeIR, PathProps, PropValue, RectProps, SceneIR, Size, StateOverride, TextProps, TimelineIR } from "./ir.js";
5
+ import type { AudioIR, BehaviorIR, CameraIR, CompositionIR, CompositionSceneEntry, Ease, EllipseProps, GroupProps, ImageProps, LineProps, NodeIR, PathProps, PropValue, RectProps, SceneIR, Size, StateOverride, TextProps, TimelineIR, VideoProps } from "./ir.js";
6
6
  export interface SceneInput {
7
7
  id: string;
8
8
  size: Size;
@@ -42,6 +42,9 @@ export declare function text(props: {
42
42
  export declare function image(props: {
43
43
  id: string;
44
44
  } & ImageProps): NodeIR;
45
+ export declare function video(props: {
46
+ id: string;
47
+ } & VideoProps): NodeIR;
45
48
  export declare function path(props: {
46
49
  id: string;
47
50
  } & PathProps): NodeIR;
@@ -80,6 +80,18 @@ export type DisplayOp = (OpBase & {
80
80
  offsetY: number;
81
81
  /** Box-fit; present only when authored and not "fill". */
82
82
  fit?: ImageFit;
83
+ }) | (OpBase & {
84
+ type: "video";
85
+ /** Raw src string as authored in the IR — consumers resolve it. */
86
+ src: string;
87
+ width: number;
88
+ height: number;
89
+ offsetX: number;
90
+ offsetY: number;
91
+ /** Source frame index at scene-time t (renderer clamps to the extracted count). */
92
+ frame: number;
93
+ /** Box-fit; present only when authored and not "fill". */
94
+ fit?: ImageFit;
83
95
  }) | (OpBase & {
84
96
  type: "path";
85
97
  /** SVG path data, drawn via Path2D. */
@@ -21,5 +21,5 @@ export { resolveAudioPlan, resolveCompositionAudioPlan, SFX_DURATION, type Audio
21
21
  export { evaluate, sampleProp, nodeParentMatrix, type DisplayList, type DisplayOp, type Mat2D, type ClipRegion, type TextAlign, type TextBaseline, } from "./evaluate.js";
22
22
  export { resolveEase, lerpValue, isColor, EASE_NAMES } from "./interpolate.js";
23
23
  export { sampleBehavior } from "./behaviors.js";
24
- export { collectImageSrcs } from "./assets.js";
24
+ export { collectImageSrcs, collectVideoSrcs } from "./assets.js";
25
25
  export { sketchToTimeline, type MotionSketch, type MotionEvent, type MotionEventKind, type MotionRegion, } from "./motion.js";
@@ -192,6 +192,23 @@ export interface ImageProps extends BaseProps {
192
192
  }
193
193
  /** Image box-fit mode. `cover` = crop-to-fill at the image's aspect (centered). */
194
194
  export type ImageFit = "fill" | "cover";
195
+ export interface VideoProps extends BaseProps {
196
+ /** Video file path (absolute, or relative to the scene file). */
197
+ src: string;
198
+ width: number;
199
+ height: number;
200
+ /** Box-fit into width×height, like the image node. `"fill"` (default) | `"cover"`. */
201
+ fit?: ImageFit;
202
+ /**
203
+ * Scene-time (seconds) at which playback begins. Before it, frame 0 (clipStart) shows;
204
+ * the node's visibility is still controlled by opacity/timeline. Default 0.
205
+ */
206
+ start?: number;
207
+ /** Playback speed multiplier (2 = double speed). Default 1. */
208
+ rate?: number;
209
+ /** Source in-point (seconds) shown at `start`. Default 0. */
210
+ clipStart?: number;
211
+ }
195
212
  export type NodeIR = {
196
213
  type: "rect";
197
214
  id: string;
@@ -212,6 +229,10 @@ export type NodeIR = {
212
229
  type: "image";
213
230
  id: string;
214
231
  props: ImageProps;
232
+ } | {
233
+ type: "video";
234
+ id: string;
235
+ props: VideoProps;
215
236
  } | {
216
237
  type: "path";
217
238
  id: string;
@@ -317,6 +317,27 @@ scene({ size, nodes: [...m.nodes, ...titles], timeline: par(m.timeline, titleTra
317
317
  - Seeded + pure (same `(images, opts)` → identical IR). Note: image-node sources do
318
318
  not render in `reframe player` / artifacts — montage ships as mp4.
319
319
 
320
+ ## Video clips (`video`)
321
+
322
+ Draw a video clip as a layer. It plays on the scene clock — at scene-time `t` it
323
+ shows the source frame at `clipStart + max(0, t - start) * rate`.
324
+
325
+ ```ts
326
+ video({ id: "clip", src: "shot.mp4", x: 960, y: 540, width: 1920, height: 1080,
327
+ anchor: "center", fit: "cover", start: 0, rate: 1, clipStart: 0 })
328
+ tween("clip", { scale: 1.08 }, { duration: 5 }) // transform composes with playback (Ken Burns)
329
+ ```
330
+
331
+ - Props: `src` (mp4 / mov / webm / m4v / mkv, absolute or scene-relative), `width`/`height`,
332
+ `fit` (`"cover"` like the image node), `start` (scene-time playback begins), `rate`
333
+ (speed), `clipStart` (source in-point s). Transform/opacity/effects compose as usual.
334
+ - **Deterministic by frame extraction**: render-cli runs `ffmpeg -vf fps=<sceneFps>` to pull
335
+ the clip's frames, and the renderer draws frame `round(t·fps)` — no live `<video>` seek, so
336
+ it stays byte-identical (same machine).
337
+ - **v1 limitations**: visual-only (the clip's own audio is not muxed — use `scene.audio`);
338
+ all frames are pre-decoded so keep clips short (≤~5s); like images, not rendered in
339
+ `reframe player` / artifacts (mp4 only). See `examples/scenes/video-demo.ts`.
340
+
320
341
  ## Cursor (UI demos)
321
342
 
322
343
  A vector mouse pointer that glides across the scene and clicks things — for app
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reframe-video",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "description": "Declarative motion graphics that AI can write and humans can tweak — human edits survive AI regeneration. Deterministic mp4 renders from a plain-data scene format.",
5
5
  "keywords": [
6
6
  "motion-graphics",
@@ -551,7 +551,8 @@ function opCorners(op: DisplayOp): [number, number][] {
551
551
  switch (op.type) {
552
552
  case "rect":
553
553
  case "ellipse":
554
- case "image": {
554
+ case "image":
555
+ case "video": {
555
556
  const { offsetX: x, offsetY: y, width: w, height: h } = op;
556
557
  return [[x, y], [x + w, y], [x + w, y + h], [x, y + h]].map(([px, py]) =>
557
558
  applyMat(op.transform, px!, py!),