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/bin.js +43 -11
- package/dist/browserEntry.js +4 -2
- package/dist/cli.js +1 -1
- package/dist/diff.js +1188 -0
- package/dist/index.js +39 -2
- package/dist/labels.js +1 -1
- package/dist/trace-cli.js +2 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/ir.d.ts +5 -0
- package/dist/types/layout.d.ts +42 -0
- package/guides/directing-guide.md +96 -0
- package/guides/edsl-guide.md +28 -3
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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)];
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/types/ir.d.ts
CHANGED
|
@@ -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.
|
package/guides/edsl-guide.md
CHANGED
|
@@ -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.
|
|
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",
|