reframe-video 0.6.11 → 0.6.12

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/bin.js CHANGED
@@ -633,7 +633,7 @@ var init_validate = __esm({
633
633
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
634
634
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
635
635
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
636
- text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
636
+ text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
637
637
  image: [...COMMON_PROPS, "src", "width", "height", "fit"],
638
638
  video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
639
639
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
@@ -968,6 +968,13 @@ var init_effects = __esm({
968
968
  }
969
969
  });
970
970
 
971
+ // ../core/src/layout.ts
972
+ var init_layout = __esm({
973
+ "../core/src/layout.ts"() {
974
+ "use strict";
975
+ }
976
+ });
977
+
971
978
  // ../core/src/montage.ts
972
979
  var init_montage = __esm({
973
980
  "../core/src/montage.ts"() {
@@ -1467,6 +1474,7 @@ var init_src = __esm({
1467
1474
  init_camera();
1468
1475
  init_gradient();
1469
1476
  init_effects();
1477
+ init_layout();
1470
1478
  init_montage();
1471
1479
  init_presets();
1472
1480
  init_devicePreset();
@@ -2467,9 +2475,9 @@ __export(batch_exports, {
2467
2475
  import { mkdir as mkdir3, mkdtemp as mkdtemp4, readFile as readFile5, rm as rm4, writeFile as writeFile4 } from "node:fs/promises";
2468
2476
  import { tmpdir as tmpdir5 } from "node:os";
2469
2477
  import { join as join8, dirname as dirname5 } from "node:path";
2470
- function overlayFromFlat(row, name) {
2478
+ function overlayFromFlat(row2, name) {
2471
2479
  const doc = { reframeOverlay: 1, name };
2472
- for (const [key2, raw] of Object.entries(row)) {
2480
+ for (const [key2, raw] of Object.entries(row2)) {
2473
2481
  if (key2.startsWith("_")) continue;
2474
2482
  if (raw === null || raw === void 0 || raw === "") continue;
2475
2483
  const value = raw;
@@ -2530,13 +2538,13 @@ function parseCsv(text2) {
2530
2538
  const headers = split(lines[0]).map((h) => h.trim());
2531
2539
  return lines.slice(1).map((line) => {
2532
2540
  const cells = split(line);
2533
- const row = {};
2541
+ const row2 = {};
2534
2542
  headers.forEach((h, i) => {
2535
2543
  const cell = (cells[i] ?? "").trim();
2536
2544
  const asNumber = Number(cell);
2537
- row[h] = cell !== "" && !Number.isNaN(asNumber) ? asNumber : cell;
2545
+ row2[h] = cell !== "" && !Number.isNaN(asNumber) ? asNumber : cell;
2538
2546
  });
2539
- return row;
2547
+ return row2;
2540
2548
  });
2541
2549
  }
2542
2550
  async function loadRows(path2) {
@@ -2554,11 +2562,11 @@ async function runBatch(scene2, rows, opts) {
2554
2562
  for (; ; ) {
2555
2563
  const index = next++;
2556
2564
  if (index >= rows.length) return;
2557
- const row = rows[index];
2558
- const name = sanitize(String(row._name ?? `row-${index}`));
2565
+ const row2 = rows[index];
2566
+ const name = sanitize(String(row2._name ?? `row-${index}`));
2559
2567
  let result;
2560
2568
  try {
2561
- const rowOverlay = overlayFromFlat(row, name);
2569
+ const rowOverlay = overlayFromFlat(row2, name);
2562
2570
  const { ir, report } = composeScene(scene2, ...opts.baseOverlays, rowOverlay);
2563
2571
  const framesDir = await mkdtemp4(join8(tmpdir5(), `reframe-batch-${index}-`));
2564
2572
  const output = join8(opts.outDir, `${name}.mp4`);
@@ -343,7 +343,7 @@
343
343
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
344
344
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
345
345
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
346
- text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
346
+ text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
347
347
  image: [...COMMON_PROPS, "src", "width", "height", "fit"],
348
348
  video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
349
349
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
@@ -903,12 +903,14 @@
903
903
  0,
904
904
  Math.round(num(id, "contentDecimals", node.props.contentDecimals ?? 0))
905
905
  );
906
+ const body = typeof raw === "number" ? formatNumber(raw, decimals, node.props.contentThousands === true) : raw;
906
907
  ops.push({
907
908
  type: "text",
908
909
  id,
909
910
  transform: projDraw(matrix, 0, 0),
910
911
  opacity,
911
- content: typeof raw === "number" ? formatNumber(raw, decimals, node.props.contentThousands === true) : raw,
912
+ // static affixes wrap the (possibly counting-up) body; absent body unchanged
913
+ content: (node.props.prefix ?? "") + body + (node.props.suffix ?? ""),
912
914
  fontFamily: str(id, "fontFamily", node.props.fontFamily),
913
915
  fontSize: num(id, "fontSize", node.props.fontSize),
914
916
  fontWeight: num(id, "fontWeight", node.props.fontWeight ?? 400),
package/dist/cli.js CHANGED
@@ -348,7 +348,7 @@ var PROPS_BY_TYPE = {
348
348
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
349
349
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
350
350
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
351
- text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
351
+ text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
352
352
  image: [...COMMON_PROPS, "src", "width", "height", "fit"],
353
353
  video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
354
354
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
package/dist/index.js CHANGED
@@ -358,7 +358,7 @@ var PROPS_BY_TYPE = {
358
358
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
359
359
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
360
360
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
361
- text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
361
+ text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
362
362
  image: [...COMMON_PROPS, "src", "width", "height", "fit"],
363
363
  video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
364
364
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
@@ -1032,6 +1032,38 @@ function dropShadow(color, blur = 24, x = 0, y = 12) {
1032
1032
  return { shadowColor: color, shadowBlur: blur, shadowX: x, shadowY: y };
1033
1033
  }
1034
1034
 
1035
+ // ../core/src/layout.ts
1036
+ function row(count, opts = {}) {
1037
+ if (count <= 0) return [];
1038
+ const center = opts.center ?? 0;
1039
+ if (count === 1) return [center];
1040
+ if (opts.span !== void 0) {
1041
+ const start2 = center - opts.span / 2;
1042
+ const pitch2 = opts.span / (count - 1);
1043
+ return Array.from({ length: count }, (_, i) => start2 + i * pitch2);
1044
+ }
1045
+ const iw = opts.itemWidth ?? 0;
1046
+ const gap = opts.gap ?? 0;
1047
+ const pitch = iw + gap;
1048
+ const total = count * iw + (count - 1) * gap;
1049
+ const start = center - total / 2 + iw / 2;
1050
+ return Array.from({ length: count }, (_, i) => start + i * pitch);
1051
+ }
1052
+ var column = row;
1053
+ function grid(rows, cols, opts = {}) {
1054
+ const axis = (center, gap, item, span) => ({
1055
+ center,
1056
+ ...gap !== void 0 ? { gap } : {},
1057
+ ...item !== void 0 ? { itemWidth: item } : {},
1058
+ ...span !== void 0 ? { span } : {}
1059
+ });
1060
+ const xs = row(cols, axis(opts.center?.x ?? 0, opts.gapX, opts.cellW, opts.spanX));
1061
+ const ys = row(rows, axis(opts.center?.y ?? 0, opts.gapY, opts.cellH, opts.spanY));
1062
+ const out = [];
1063
+ for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) out.push({ x: xs[c], y: ys[r] });
1064
+ return out;
1065
+ }
1066
+
1035
1067
  // ../core/src/montage.ts
1036
1068
  var VIDEO_EXT = /\.(mp4|mov|webm|m4v|mkv)$/i;
1037
1069
  var isVideoSrc = (src) => VIDEO_EXT.test(src);
@@ -3345,12 +3377,14 @@ function evaluate(compiled, t) {
3345
3377
  0,
3346
3378
  Math.round(num(id, "contentDecimals", node.props.contentDecimals ?? 0))
3347
3379
  );
3380
+ const body = typeof raw === "number" ? formatNumber(raw, decimals, node.props.contentThousands === true) : raw;
3348
3381
  ops.push({
3349
3382
  type: "text",
3350
3383
  id,
3351
3384
  transform: projDraw(matrix, 0, 0),
3352
3385
  opacity,
3353
- content: typeof raw === "number" ? formatNumber(raw, decimals, node.props.contentThousands === true) : raw,
3386
+ // static affixes wrap the (possibly counting-up) body; absent body unchanged
3387
+ content: (node.props.prefix ?? "") + body + (node.props.suffix ?? ""),
3354
3388
  fontFamily: str(id, "fontFamily", node.props.fontFamily),
3355
3389
  fontSize: num(id, "fontSize", node.props.fontSize),
3356
3390
  fontWeight: num(id, "fontWeight", node.props.fontWeight ?? 400),
@@ -3486,6 +3520,7 @@ export {
3486
3520
  characterPreset,
3487
3521
  collectImageSrcs,
3488
3522
  collectVideoSrcs,
3523
+ column,
3489
3524
  compileComposition,
3490
3525
  compileScene,
3491
3526
  composeScene,
@@ -3507,6 +3542,7 @@ export {
3507
3542
  figure,
3508
3543
  formatComposeReport,
3509
3544
  glow,
3545
+ grid,
3510
3546
  group,
3511
3547
  humanoid,
3512
3548
  ikReach,
@@ -3536,6 +3572,7 @@ export {
3536
3572
  resolveEase,
3537
3573
  rig,
3538
3574
  rigPose,
3575
+ row,
3539
3576
  sampleBehavior,
3540
3577
  sampleProp,
3541
3578
  scene,
package/dist/labels.js CHANGED
@@ -342,7 +342,7 @@ var PROPS_BY_TYPE = {
342
342
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
343
343
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
344
344
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
345
- text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
345
+ text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
346
346
  image: [...COMMON_PROPS, "src", "width", "height", "fit"],
347
347
  video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
348
348
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
package/dist/trace-cli.js CHANGED
@@ -12,7 +12,7 @@ var PROPS_BY_TYPE = {
12
12
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
13
13
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
14
14
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
15
- text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
15
+ text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
16
16
  image: [...COMMON_PROPS, "src", "width", "height", "fit"],
17
17
  video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
18
18
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
@@ -525,7 +525,7 @@ var INPLACE_RATIO = 0.3;
525
525
  var mean2 = (xs) => xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
526
526
  function backgroundLevel(diff) {
527
527
  const flat = [];
528
- for (const row of diff) for (const v of row) flat.push(v);
528
+ for (const row2 of diff) for (const v of row2) flat.push(v);
529
529
  if (flat.length === 0) return 0;
530
530
  flat.sort((a, b) => a - b);
531
531
  return flat[Math.floor(flat.length / 2)];
@@ -8,6 +8,7 @@ export { pathPoint, pathTangentAngle, type Pt } from "./path.js";
8
8
  export { cameraTo, cameraMatrix, CAMERA_ID, CAMERA_PROPS } from "./camera.js";
9
9
  export { linearGradient, radialGradient, conicGradient, isGradient } from "./gradient.js";
10
10
  export { glow, dropShadow } from "./effects.js";
11
+ export { row, column, grid, type RowOpts, type GridOpts } from "./layout.js";
11
12
  export { photoMontage, videoMontage, type MontageImage, type MontageOpts, type MontageResult, type KenBurns } from "./montage.js";
12
13
  export { motionPreset, PRESET_NAMES, type PresetName, type PresetRig, type PresetOpts } from "./presets.js";
13
14
  export { devicePreset, deviceScreen, deviceScreenCenter, deviceBounds, deviceScreenPoint, DEVICE_PRESET_NAMES, type DevicePresetName, type DevicePresetOpts } from "./devicePreset.js";
@@ -139,6 +139,11 @@ export interface TextProps extends BaseProps {
139
139
  contentDecimals?: number;
140
140
  /** Group the integer part with thousands separators (e.g. 35,786). */
141
141
  contentThousands?: boolean;
142
+ /** Static affixes wrapped around the rendered content — so a count-up can read
143
+ * "$2.4M" or "+32%" from ONE node (prefix `"$"`, suffix `"M"`) instead of three
144
+ * hand-positioned ones. Absent ⇒ no change. */
145
+ prefix?: string;
146
+ suffix?: string;
142
147
  fontFamily: string;
143
148
  fontSize: number;
144
149
  fontWeight?: number;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Layout helpers — pure coordinate math for the common "evenly space N things"
3
+ * jobs (a row of cards, a grid of tiles) so authors don't hand-roll a `cx(i)`
4
+ * every time. They return coordinates you spread into node `x`/`y`; no nodes,
5
+ * no renderer involvement — authoring sugar only. Deterministic.
6
+ *
7
+ * const xs = row(3, { center: 960, gap: 60, itemWidth: 440 });
8
+ * xs.map((x, i) => rect({ id: `card-${i}`, x, y: 540, ... }));
9
+ */
10
+ export interface RowOpts {
11
+ /** Centre of the row/column (default 0). */
12
+ center?: number;
13
+ /** Gap between adjacent items, paired with `itemWidth` (packed layout). */
14
+ gap?: number;
15
+ /** Item extent along the axis, paired with `gap` (packed layout). */
16
+ itemWidth?: number;
17
+ /** Alternative to gap+itemWidth: spread the item CENTRES evenly across this span. */
18
+ span?: number;
19
+ }
20
+ /** N evenly-spaced positions along one axis, centred on `center`. Give either
21
+ * `span` (spread centres across it) or `gap`+`itemWidth` (pack fixed-width items). */
22
+ export declare function row(count: number, opts?: RowOpts): number[];
23
+ /** N evenly-spaced positions along the vertical axis — `row` for the y axis. */
24
+ export declare const column: typeof row;
25
+ export interface GridOpts {
26
+ center?: {
27
+ x: number;
28
+ y: number;
29
+ };
30
+ gapX?: number;
31
+ gapY?: number;
32
+ cellW?: number;
33
+ cellH?: number;
34
+ /** Alternatives to gap+cell: spread cell centres across these spans. */
35
+ spanX?: number;
36
+ spanY?: number;
37
+ }
38
+ /** `rows × cols` cell centres in row-major order, centred on `center`. */
39
+ export declare function grid(rows: number, cols: number, opts?: GridOpts): {
40
+ x: number;
41
+ y: number;
42
+ }[];
@@ -4,6 +4,10 @@ You write a motion-graphics scene as **declarative data** using the reframe
4
4
  TypeScript eDSL. Your output is a single `.ts` file that default-exports a
5
5
  `scene({...})` call. Everything imports from `@reframe/core`.
6
6
 
7
+ > `See examples/scenes/…` pointers below refer to the GitHub repo
8
+ > (github.com/kiyeonjeon21/reframe), not the installed npm package — this guide is
9
+ > self-contained; you don't need them to write a scene.
10
+
7
11
  ```ts
8
12
  import { scene, group, rect, ellipse, line, text,
9
13
  seq, par, stagger, to, tween, wait,
@@ -30,10 +34,13 @@ Factories return plain data. Every node needs a unique `id`.
30
34
  - `ellipse({ id, x, y, width, height, fill?, stroke?, strokeWidth?, ... })`
31
35
  - `line({ id, x1, y1, x2, y2, stroke, strokeWidth?, opacity?, progress? })` —
32
36
  `progress` 0..1 draws the line on (1 = full line).
33
- - `text({ id, x, y, content, contentDecimals?, fontFamily, fontSize, fontWeight?, fill?, letterSpacing?, ... })` —
37
+ - `text({ id, x, y, content, contentDecimals?, contentThousands?, prefix?, suffix?, fontFamily, fontSize, fontWeight?, fill?, letterSpacing?, ... })` —
34
38
  `content` may be a number; numeric content interpolates (count-up) and renders
35
39
  via `toFixed(contentDecimals ?? 0)`. For a "8.2"-style label, set
36
- `contentDecimals: 1`.
40
+ `contentDecimals: 1`; `contentThousands: true` groups the integer (35,786).
41
+ **`prefix`/`suffix`** wrap the value so a count-up reads `$2.4M` or `+32%` from
42
+ ONE node (`{ content: 2.4, contentDecimals: 1, prefix: "$", suffix: "M" }`) —
43
+ don't hand-position separate `$`/`%` nodes.
37
44
  - `path({ id, d, x, y, fill?, stroke?, strokeWidth?, progress?, originX?, originY?, opacity?, rotation?, scale?, anchor? })` —
38
45
  a true vector shape from an SVG path `d` string (crisp at any zoom; recolour by
39
46
  animating `fill`/`stroke`). `progress` 0..1 draws the stroke OUTLINE on (animate
@@ -63,6 +70,23 @@ Factories return plain data. Every node needs a unique `id`.
63
70
  Example: a bar that grows upward = `anchor: "bottom-left"` + animate `height`.
64
71
  Font: use `fontFamily: "Inter"` (weights 400/700/800 are available).
65
72
 
73
+ ### Layout helpers (evenly spacing things)
74
+
75
+ Positions are absolute pixels. For a row of cards or a grid of tiles, use the
76
+ layout helpers instead of hand-rolling the column math — they return coordinates
77
+ you spread into `x`/`y`:
78
+
79
+ ```ts
80
+ import { row, grid } from "@reframe/core";
81
+ // 3 cards, 440px wide, 60px apart, centred on the frame:
82
+ row(3, { center: 960, gap: 60, itemWidth: 440 }).map((x, i) =>
83
+ rect({ id: `card-${i}`, x, y: 540, width: 440, height: 300, anchor: "center", fill: "#1A1F2E" }));
84
+ // or spread centres across a span: row(3, { center: 960, span: 900 })
85
+ // grid(rows, cols, { center: {x,y}, gapX, gapY, cellW, cellH }) → { x, y }[] (row-major)
86
+ ```
87
+
88
+ `column` is `row` for the y axis.
89
+
66
90
  ## States: declare looks, not motion
67
91
 
68
92
  Base props on nodes describe the **finished design**. A state is a sparse
@@ -98,7 +122,8 @@ them with normal TS (`Object.fromEntries`, `.map`) for data-driven scenes.
98
122
  later `tween` can chain from there. Use it for swoops/arcs/orbits — straight
99
123
  `tween`s on x and y can't curve. `closed: true` loops the waypoints (orbit).
100
124
  `curviness` shapes the path: `1` smooth (default), `0` sharp corners, `>1` loopier.
101
- - `wait(seconds)` — hold.
125
+ - `wait(seconds, label?)` — hold; the optional `label` names the hold so audio
126
+ cues and overlay retiming can address it.
102
127
 
103
128
  Eases: `linear`, `easeIn/Out/InOutQuad`, `easeIn/Out/InOutCubic`,
104
129
  `easeIn/Out/InOutQuart`, `easeIn/Out/InOutExpo`, or `{ cubicBezier: [x1,y1,x2,y2] }`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reframe-video",
3
- "version": "0.6.11",
3
+ "version": "0.6.12",
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",