reframe-video 0.6.11 → 0.6.13

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
@@ -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
+ }[];
@@ -0,0 +1,96 @@
1
+ # reframe directing guide — high-end, reference-heavy pieces
2
+
3
+ Read this (after the syntax guide, `reframe guide`) when the ask is a CINEMATIC or
4
+ REFERENCE-FAITHFUL piece — a product teaser, a UI/session reproduction, a title
5
+ sequence, a data story — not a simple lower-third or KPI card (those you just write).
6
+ Simple jobs render first-try; the ceiling needs a process. This is that process.
7
+
8
+ ## What to get from the user (before writing anything)
9
+
10
+ Ask for / confirm these — vague prompts are why these pieces take many rounds:
11
+
12
+ - **Concept** in one line ("a faithful Claude Code session that builds a logo", "an app
13
+ that goes viral, everywhere").
14
+ - **References** — screenshots / a reference video / pasted real content (terminal output,
15
+ copy, data). For fidelity work, the reference IS the spec. Save them to disk so you can
16
+ `diff` against them.
17
+ - **Brand** — exact colors (hex), the wordmark, the font feel.
18
+ - **Format** — length (~10–20s is a good ceiling clip), aspect (16:9 / 9:16), with or
19
+ without sound.
20
+ - **Tone** — "Apple teaser" (slow, premium, lots of negative space) vs "faithful UI sim"
21
+ (exact, dense) vs "kinetic/energetic". This sets pacing and camera.
22
+
23
+ ## The loop
24
+
25
+ ### 1. Storyboard the beats FIRST (structure, not a flat timeline)
26
+
27
+ Name the acts with `beat("...", {}, [ ... ])` before animating. A beat is a labeled,
28
+ retimable narrative unit; its label anchors audio and lets you restructure whole sections.
29
+ A reliable arc: **setup → inciting beat → rising → climax → resolution.** Decide what each
30
+ beat shows and how long, THEN fill in motion. (See `device-hero.ts`: `beat("ki"/"seung"/
31
+ "jeon"/"gyeol", …)` — entrance → it-takes-off → everywhere → resolve.)
32
+
33
+ ### 2. Match references with `diff` (stop eyeballing)
34
+
35
+ Reproducing a screenshot pixel-faithfully is the hardest part. Use the tool:
36
+
37
+ ```
38
+ reframe diff ref.png --mode grid # labelled 100px grid over the screenshot → read coords, place nodes
39
+ reframe diff ref.png scene.ts --mode side # reference | your render, side by side
40
+ reframe diff ref.png scene.ts --mode diff # absolute difference — bright where you're off
41
+ reframe diff ref.png scene.ts --mode blend # 50% overlay — spot drift
42
+ ```
43
+
44
+ Loop: `--mode grid` to measure → write the node tree → `--mode side`/`diff` to compare →
45
+ fix coordinates/sizes/colors → repeat until faithful. Pick the frame with `--t <sec>`.
46
+
47
+ ### 3. Apply the cinematic-craft checklist
48
+
49
+ These are what make a piece read as premium, not a slideshow. Patterns proven in the
50
+ flagship scenes — reuse the technique, vary the content:
51
+
52
+ - **Camera moves with the story.** Push in on each beat: a `cameraTo(...)` running in `par`
53
+ with the beat's content. Frame the detail that matters, pull back to resolve. (See
54
+ `terminal-claude.ts` helpers `cam()`/`scroll()`/`show()` — focus + scroll + reveal as
55
+ parameterized eased moves.)
56
+ - **Curved entrances, not straight slides.** A hero enters on a `motionPath` arc with
57
+ `easeOutBack` (overshoot, then settle). (`device-hero.ts` `motionPath("phone-cam", [[…]],
58
+ { ease: "easeOutBack" })`.)
59
+ - **Fake depth.** Layer a backdrop of many faint concentric ellipses (a smooth glow, no hard
60
+ edge) + a spotlight + a cast shadow that tracks the hero + an impact ring on landing.
61
+ (`device-hero.ts` backdrop/spot/shadow/ring rig.) Or use real depth: `camera.perspective`
62
+ + per-node `z` (see the syntax guide's "Depth & perspective").
63
+ - **Layered idle motion.** Nothing should sit perfectly still. `oscillate` a few nodes at
64
+ DIFFERENT frequencies (slow float, slower tilt, a fast accent) for life during holds.
65
+ - **Sound on the beats.** `scene.audio` cues anchor to your beat/timeline labels, so they
66
+ survive retiming: `{ at: "land", file: "bong_001.ogg" }`, `{ at: "viral", offset: 0.4,
67
+ sfx: "pop" }`. An `ambient-pad` bgm with `duck` under the hits. Quote `reframe labels` to
68
+ see exact seconds.
69
+
70
+ ### 4. Verify objectively (don't argue about "more dynamic")
71
+
72
+ - `reframe labels scene.ts` — every label → exact seconds. The timing source for audio + a
73
+ sanity check that beats land when you think.
74
+ - `reframe motion out.mp4` — speeds, static fraction, oscillation rhythm, spikes. A vague
75
+ note like "make it punchier" becomes measurable: compare `meanSpeed`/`peakSpeed` before
76
+ and after; `staticFraction` too high = it drags.
77
+ - `reframe trace ref.mp4 --apply scene.ts` — when you have a reference VIDEO (not image),
78
+ extract its timing/easing and re-apply it onto YOUR node ids. Borrow the motion, keep your
79
+ assets.
80
+
81
+ ### 5. Hand-tune via preview → overlay
82
+
83
+ `reframe preview` to scrub, drag motionPath waypoints, and retime steps; export the overlay
84
+ JSON and render with `--overlay`. Those nudges survive a later regeneration (stable
85
+ addresses), so the human's polish isn't lost when you redo the base.
86
+
87
+ ## Pitfalls
88
+
89
+ - Don't animate before the structure is right — fixing pacing after everything is keyframed
90
+ is painful. Beats first.
91
+ - Reference fidelity is coordinates + color + type, mostly STATIC layout; get the held frame
92
+ matching with `diff` before adding motion.
93
+ - Keep `id`s/labels stable across rewrites (see `reframe guide --regen`) so the user's
94
+ overlay edits survive.
95
+ - It's still iterative. The tools cut the rounds; they don't remove the loop. Render, look,
96
+ adjust — the agent should render frames and read them, not guess.
@@ -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.13",
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",