reframe-video 0.6.21 → 0.6.23
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/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/dist/bin.js +18 -7
- package/guides/edsl-guide.md +25 -0
- package/guides/html-guide.md +180 -0
- package/package.json +1 -1
- package/skills/reframe/SKILL.md +3 -0
- package/.claude-plugin/marketplace.json +0 -14
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reframe",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Create and iterate motion-graphics videos as addressable data: deterministic mp4 renders, human edits that survive AI regeneration, label-anchored audio, data-driven batch rendering.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Kiyeon Jeon",
|
package/README.md
CHANGED
|
@@ -23,10 +23,12 @@ npx reframe-video render hello.ts # → out/hello.mp4
|
|
|
23
23
|
| `reframe render <scene.ts> [--overlay edits.json] [-o out.mp4]` | deterministic mp4 |
|
|
24
24
|
| `reframe batch <scene.ts> <data.json\|csv>` | one mp4 per data row (row keys are overlay addresses) |
|
|
25
25
|
| `reframe compile <scene.ts> [-o out.json] [--json]` | bundle + validate a scene to SceneIR JSON, no render (fast; no ffmpeg/chromium) |
|
|
26
|
+
| `reframe frame <scene.ts> [--t <sec>] [-o out.png]` | render one frame at time `t` to a PNG (chromium only, no mux) — for a render-and-look loop |
|
|
26
27
|
| `reframe preview` | scrub/play/edit UI for scenes in the current directory; edits export as overlay JSON |
|
|
27
28
|
| `reframe new <name>` | scaffold a documented starter scene |
|
|
28
29
|
| `reframe motion <mp4>` | calibrated motion profile of a rendered clip |
|
|
29
|
-
| `reframe guide [--regen]` | the
|
|
30
|
+
| `reframe guide [--directing\|--regen\|--html]` | the authoring guide (default eDSL syntax; directing workflow; regeneration contract; HTML/GSAP) — **feed this to your AI** |
|
|
31
|
+
| `reframe skill [--path]` | print the authoring skill for an agent; `--path` prints the plugin dir to load |
|
|
30
32
|
|
|
31
33
|
(Installed as both `reframe` and `reframe-video`; with npx use `npx reframe-video <cmd>`.)
|
|
32
34
|
|
package/dist/bin.js
CHANGED
|
@@ -2804,6 +2804,18 @@ var PLAYER = PACKAGED ? join9(ROOT2, "dist", "player.js") : join9(ROOT2, "packag
|
|
|
2804
2804
|
var ANALYZE = PACKAGED ? join9(ROOT2, "dist", "analyze.js") : join9(ROOT2, "benchmark", "harness", "motion", "analyze.ts");
|
|
2805
2805
|
var TRACE = PACKAGED ? join9(ROOT2, "dist", "trace-cli.js") : join9(ROOT2, "benchmark", "harness", "motion", "trace-cli.ts");
|
|
2806
2806
|
var CMD = PACKAGED ? "reframe" : "pnpm reframe";
|
|
2807
|
+
var GUIDE = PACKAGED ? {
|
|
2808
|
+
regen: join9(ROOT2, "guides", "regen-contract.md"),
|
|
2809
|
+
directing: join9(ROOT2, "guides", "directing-guide.md"),
|
|
2810
|
+
html: join9(ROOT2, "guides", "html-guide.md"),
|
|
2811
|
+
edsl: join9(ROOT2, "guides", "edsl-guide.md")
|
|
2812
|
+
} : {
|
|
2813
|
+
regen: join9(ROOT2, "docs", "guides", "regen-contract.md"),
|
|
2814
|
+
directing: join9(ROOT2, "docs", "guides", "directing-guide.md"),
|
|
2815
|
+
html: join9(ROOT2, "docs", "guides", "html-guide.md"),
|
|
2816
|
+
edsl: join9(ROOT2, "docs", "guides", "edsl-guide.md")
|
|
2817
|
+
};
|
|
2818
|
+
var PLUGIN_DIR = PACKAGED ? ROOT2 : join9(ROOT2, "plugin");
|
|
2807
2819
|
var USAGE = `reframe \u2014 declarative motion graphics
|
|
2808
2820
|
|
|
2809
2821
|
usage:
|
|
@@ -2824,7 +2836,7 @@ usage:
|
|
|
2824
2836
|
${CMD} motion <mp4|framesDir> motion-profile a rendered clip
|
|
2825
2837
|
${CMD} trace <ref.mp4> [--apply scene.ts] extract a video's motion structure \u2192 MotionSketch / timeline
|
|
2826
2838
|
${CMD} diff <ref-image> [<scene.ts>] [--t S] [--mode side|blend|diff|grid] compare/measure a render against a reference image
|
|
2827
|
-
${CMD} guide [--regen|--
|
|
2839
|
+
${CMD} guide [--directing|--regen|--html] print a guide (default: eDSL syntax; --directing: high-end workflow; --regen: stable-address contract; --html: HTML/GSAP scenes)
|
|
2828
2840
|
${CMD} demo run the edit-survival demo (3 mp4s into out/)
|
|
2829
2841
|
`;
|
|
2830
2842
|
var userPath = (p) => isAbsolute5(p) ? p : resolve6(USER_CWD, p);
|
|
@@ -3185,22 +3197,21 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
|
|
|
3185
3197
|
);
|
|
3186
3198
|
}
|
|
3187
3199
|
case "guide": {
|
|
3188
|
-
const which = rest.includes("--regen") ? "regen" : rest.includes("--directing") ? "directing" : "edsl";
|
|
3189
|
-
const
|
|
3190
|
-
|
|
3191
|
-
const file = (PACKAGED ? pkgFile : repoFile)[which];
|
|
3200
|
+
const which = rest.includes("--regen") ? "regen" : rest.includes("--directing") ? "directing" : rest.includes("--html") ? "html" : "edsl";
|
|
3201
|
+
const file = GUIDE[which];
|
|
3202
|
+
if (!existsSync6(file)) fail(`guide not found: ${file}`);
|
|
3192
3203
|
const { readFile: readFile7 } = await import("node:fs/promises");
|
|
3193
3204
|
process.stdout.write(await readFile7(file, "utf8"));
|
|
3194
3205
|
return;
|
|
3195
3206
|
}
|
|
3196
3207
|
case "skill": {
|
|
3197
3208
|
if (rest.includes("--path")) {
|
|
3198
|
-
process.stdout.write(`${
|
|
3209
|
+
process.stdout.write(`${PLUGIN_DIR}
|
|
3199
3210
|
`);
|
|
3200
3211
|
return;
|
|
3201
3212
|
}
|
|
3202
3213
|
const { readFile: readFile7 } = await import("node:fs/promises");
|
|
3203
|
-
process.stdout.write(await readFile7(join9(
|
|
3214
|
+
process.stdout.write(await readFile7(join9(PLUGIN_DIR, "skills", "reframe", "SKILL.md"), "utf8"));
|
|
3204
3215
|
return;
|
|
3205
3216
|
}
|
|
3206
3217
|
case "demo":
|
package/guides/edsl-guide.md
CHANGED
|
@@ -475,6 +475,31 @@ group({ id: "burst", x, y, blend: "screen" }, [ disc1, disc2, disc3 ])
|
|
|
475
475
|
with the effect). It wraps a matte group and nests. The effects are screen-pixel space.
|
|
476
476
|
See `examples/scenes/group-fx-demo.ts`.
|
|
477
477
|
|
|
478
|
+
## Device frames (phone / browser / laptop …)
|
|
479
|
+
|
|
480
|
+
To put a **phone, browser, laptop, …** on screen, use the preset — don't hand-draw
|
|
481
|
+
a device out of rects. `devicePreset(name, opts) → NodeIR` returns a parametric
|
|
482
|
+
vector frame (bezel, rounded body, phone notch / dynamic island, browser chrome).
|
|
483
|
+
|
|
484
|
+
- `devicePreset(name, { id, x, y, scale?, opacity?, orientation?, content })` —
|
|
485
|
+
names: `phone` `tablet` `laptop` `browser` `watch` `monitor` `tv` `foldable`
|
|
486
|
+
`terminal` `car`. **There is no `"iphone"` — `"phone"` IS the iOS-style frame**
|
|
487
|
+
(notch + dynamic island). `browser`/`terminal` take an `address` string.
|
|
488
|
+
- `content` nodes are authored in **screen-LOCAL centre coords** (0,0 = screen
|
|
489
|
+
centre) and clipped to the screen. Stable ids `${id}-screen` / `${id}-content`
|
|
490
|
+
(overlay/regen addresses) — keep `id` across rewrites.
|
|
491
|
+
- It's one node: animate the device group for the float/entrance (`tween`/
|
|
492
|
+
`motionPath` its `x`/`y`/`scale`/`rotation`, `oscillate` for an idle drift).
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
// a phone floating centre, a chat bubble inside the screen:
|
|
496
|
+
devicePreset("phone", { id: "hero", x: 960, y: 540, scale: 0.92, opacity: 0,
|
|
497
|
+
content: [ rect({ id: "b1", x: 80, y: -120, width: 300, height: 64, radius: 22, fill: "#2563EB" }) ] })
|
|
498
|
+
// timeline: par(tween("hero", { opacity: 1, scale: 1 }, { ease: "easeOutBack" }))
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Pair with `cursor` + `deviceScreenPoint` (below) to click UI *inside* the device.
|
|
502
|
+
|
|
478
503
|
## Cursor (UI demos)
|
|
479
504
|
|
|
480
505
|
A vector mouse pointer that glides across the scene and clicks things — for app
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# HTML + GSAP motion guide
|
|
2
|
+
|
|
3
|
+
You write a motion-graphics scene as a **single self-contained `.html` file**
|
|
4
|
+
using HTML, CSS, and GSAP. The page is rendered to video frame-by-frame by a
|
|
5
|
+
deterministic capture harness.
|
|
6
|
+
|
|
7
|
+
## Required structure
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<!DOCTYPE html>
|
|
11
|
+
<html>
|
|
12
|
+
<head>
|
|
13
|
+
<meta charset="utf-8" />
|
|
14
|
+
<style>
|
|
15
|
+
body { margin: 0; }
|
|
16
|
+
#stage {
|
|
17
|
+
position: relative; width: 1920px; height: 1080px;
|
|
18
|
+
background: #101014; overflow: hidden;
|
|
19
|
+
font-family: Inter, sans-serif;
|
|
20
|
+
}
|
|
21
|
+
/* static styling for your elements */
|
|
22
|
+
</style>
|
|
23
|
+
</head>
|
|
24
|
+
<body>
|
|
25
|
+
<div id="stage">
|
|
26
|
+
<!-- your elements -->
|
|
27
|
+
</div>
|
|
28
|
+
<script src="./gsap.min.js"></script>
|
|
29
|
+
<script>
|
|
30
|
+
// your animation code
|
|
31
|
+
</script>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- The video frame is exactly the `#stage` element — keep it `1920px × 1080px`
|
|
37
|
+
with `overflow: hidden`, and put everything inside it.
|
|
38
|
+
- GSAP 3 is available locally at `./gsap.min.js` (already next to your file).
|
|
39
|
+
Do not load anything else from the network — no CDNs, no images, no iframes.
|
|
40
|
+
- Font: use `font-family: Inter` (weights 400/700/800 are pre-installed by the
|
|
41
|
+
harness; no @font-face needed). Fallback `sans-serif`.
|
|
42
|
+
|
|
43
|
+
## Animation rules (important)
|
|
44
|
+
|
|
45
|
+
The harness virtualizes time: `requestAnimationFrame`, `setTimeout`,
|
|
46
|
+
`setInterval`, `performance.now()` and `Date.now()` all follow a virtual
|
|
47
|
+
clock, so GSAP timelines and hand-rolled rAF loops are captured exactly.
|
|
48
|
+
|
|
49
|
+
- **All motion must be driven by GSAP or JavaScript.** CSS `animation`,
|
|
50
|
+
CSS `transition`, and SMIL run on the compositor clock and will NOT be
|
|
51
|
+
captured — a scene that relies on them renders as a frozen frame.
|
|
52
|
+
Static CSS styling (layout, colors, border-radius, flex...) is fine.
|
|
53
|
+
- No `Math.random()` unless seeded yourself deterministically; no `<video>`;
|
|
54
|
+
no user interaction. The page must play by itself from t=0.
|
|
55
|
+
- Match the brief's total duration: time your timeline so the action completes
|
|
56
|
+
within it (trailing hold is fine).
|
|
57
|
+
|
|
58
|
+
## GSAP quick reference
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
const tl = gsap.timeline({ delay: 0.2 });
|
|
62
|
+
tl.to("#el", { x: 300, opacity: 1, duration: 0.5, ease: "power2.out" })
|
|
63
|
+
.fromTo("#el2", { scale: 0.5 }, { scale: 1.1, duration: 0.2, ease: "power2.out" })
|
|
64
|
+
.to("#el2", { scale: 1, duration: 0.1, ease: "power1.inOut" }) // settle
|
|
65
|
+
.to("#el3", { y: -40, duration: 0.3 }, "<") // "<" = start with previous
|
|
66
|
+
.to("#el4", { opacity: 0, duration: 0.3 }, "+=1.5"); // "+=" = gap after previous
|
|
67
|
+
|
|
68
|
+
gsap.to(".items", { opacity: 1, stagger: 0.1, duration: 0.4 }); // staggered
|
|
69
|
+
gsap.to("#el", { rotation: 3, yoyo: true, repeat: -1, duration: 0.6,
|
|
70
|
+
ease: "sine.inOut" }); // continuous wiggle during holds
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Eases: `power1/2/3/4.out` (decelerate — entrances), `.in` (accelerate — exits),
|
|
74
|
+
`.inOut`, `sine.inOut`, `back.out(1.7)` (overshoot), `expo.out`.
|
|
75
|
+
Position params: `"<"` start with previous, `"+=0.5"` gap, absolute `1.2`.
|
|
76
|
+
|
|
77
|
+
Useful patterns:
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
// Count-up number label
|
|
81
|
+
const counter = { value: 0 };
|
|
82
|
+
gsap.to(counter, { value: 14.0, duration: 1, ease: "power2.out",
|
|
83
|
+
onUpdate: () => { el.textContent = counter.value.toFixed(1); } });
|
|
84
|
+
|
|
85
|
+
// Center an element at a point (so scale/rotation pivot around its center)
|
|
86
|
+
// CSS: position:absolute; left:960px; top:540px; transform:translate(-50%,-50%)
|
|
87
|
+
// Then animate with xPercent/yPercent preserved:
|
|
88
|
+
gsap.fromTo("#el", { xPercent: -50, yPercent: -50, scale: 0.5 },
|
|
89
|
+
{ xPercent: -50, yPercent: -50, scale: 1, duration: 0.3 });
|
|
90
|
+
|
|
91
|
+
// Grow a bar upward: anchor it with bottom CSS, animate height
|
|
92
|
+
gsap.fromTo("#bar", { height: 0 }, { height: 320, duration: 0.7, ease: "power3.out" });
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Worked example A — countdown (3, 2, 1, GO!)
|
|
96
|
+
|
|
97
|
+
```html
|
|
98
|
+
<!DOCTYPE html>
|
|
99
|
+
<html>
|
|
100
|
+
<head>
|
|
101
|
+
<meta charset="utf-8" />
|
|
102
|
+
<style>
|
|
103
|
+
body { margin: 0; }
|
|
104
|
+
#stage { position: relative; width: 1920px; height: 1080px;
|
|
105
|
+
background: #101014; overflow: hidden; font-family: Inter, sans-serif; }
|
|
106
|
+
#ring { position: absolute; left: 960px; top: 540px;
|
|
107
|
+
width: 360px; height: 360px; margin: -180px 0 0 -180px;
|
|
108
|
+
border: 10px solid #3B82F6; border-radius: 50%; opacity: 0; }
|
|
109
|
+
.num, #go { position: absolute; left: 960px; top: 540px;
|
|
110
|
+
transform: translate(-50%, -50%); font-weight: 800; color: #fff; opacity: 0; }
|
|
111
|
+
.num { font-size: 220px; }
|
|
112
|
+
#go { font-size: 320px; color: #FF4D00; }
|
|
113
|
+
</style>
|
|
114
|
+
</head>
|
|
115
|
+
<body>
|
|
116
|
+
<div id="stage">
|
|
117
|
+
<div id="ring"></div>
|
|
118
|
+
<div class="num" id="num-3">3</div>
|
|
119
|
+
<div class="num" id="num-2">2</div>
|
|
120
|
+
<div class="num" id="num-1">1</div>
|
|
121
|
+
<div id="go">GO!</div>
|
|
122
|
+
</div>
|
|
123
|
+
<script src="./gsap.min.js"></script>
|
|
124
|
+
<script>
|
|
125
|
+
const tl = gsap.timeline();
|
|
126
|
+
tl.to("#ring", { opacity: 1, duration: 0.3, ease: "power2.out" });
|
|
127
|
+
for (const n of ["3", "2", "1"]) {
|
|
128
|
+
tl.fromTo(`#num-${n}`,
|
|
129
|
+
{ opacity: 0, scale: 0.5 },
|
|
130
|
+
{ opacity: 1, scale: 1.1, duration: 0.2, ease: "power2.out" })
|
|
131
|
+
.to(`#num-${n}`, { scale: 1, duration: 0.1, ease: "power1.inOut" })
|
|
132
|
+
.to(`#num-${n}`, { opacity: 0, scale: 0.7, duration: 0.1, ease: "power1.in" }, "+=0.45");
|
|
133
|
+
}
|
|
134
|
+
tl.fromTo("#go",
|
|
135
|
+
{ opacity: 0, scale: 0.5 },
|
|
136
|
+
{ opacity: 1, scale: 1.15, duration: 0.25, ease: "power2.out" })
|
|
137
|
+
.to("#ring", { scale: 1.6, opacity: 0, duration: 0.4, ease: "power2.out" }, "<")
|
|
138
|
+
.to("#go", { scale: 1, duration: 0.15, ease: "power1.inOut" });
|
|
139
|
+
</script>
|
|
140
|
+
</body>
|
|
141
|
+
</html>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Worked example B — badge pop (overshoot + wiggle + drop)
|
|
145
|
+
|
|
146
|
+
```html
|
|
147
|
+
<!DOCTYPE html>
|
|
148
|
+
<html>
|
|
149
|
+
<head>
|
|
150
|
+
<meta charset="utf-8" />
|
|
151
|
+
<style>
|
|
152
|
+
body { margin: 0; }
|
|
153
|
+
#stage { position: relative; width: 1920px; height: 1080px;
|
|
154
|
+
background: #15151A; overflow: hidden; font-family: Inter, sans-serif; }
|
|
155
|
+
#badge { position: absolute; left: 960px; top: 540px;
|
|
156
|
+
width: 420px; height: 160px; margin: -80px 0 0 -210px;
|
|
157
|
+
background: #E11D48; border-radius: 28px;
|
|
158
|
+
display: flex; align-items: center; justify-content: center;
|
|
159
|
+
opacity: 0; transform: scale(0); }
|
|
160
|
+
#badge span { color: #fff; font-size: 88px; font-weight: 800; letter-spacing: 6px; }
|
|
161
|
+
</style>
|
|
162
|
+
</head>
|
|
163
|
+
<body>
|
|
164
|
+
<div id="stage">
|
|
165
|
+
<div id="badge"><span>NEW</span></div>
|
|
166
|
+
</div>
|
|
167
|
+
<script src="./gsap.min.js"></script>
|
|
168
|
+
<script>
|
|
169
|
+
const tl = gsap.timeline({ delay: 0.2 });
|
|
170
|
+
tl.to("#badge", { opacity: 1, duration: 0.15, ease: "power1.out" })
|
|
171
|
+
.to("#badge", { scale: 1.18, duration: 0.28, ease: "power2.out" }, "<")
|
|
172
|
+
.to("#badge", { scale: 1, duration: 0.18, ease: "power1.inOut" })
|
|
173
|
+
.to("#badge", { y: 180, opacity: 0, duration: 0.35, ease: "power2.in" }, "+=1.6");
|
|
174
|
+
|
|
175
|
+
gsap.to("#badge", { rotation: 2.5, duration: 0.625, yoyo: true, repeat: -1,
|
|
176
|
+
ease: "sine.inOut" });
|
|
177
|
+
</script>
|
|
178
|
+
</body>
|
|
179
|
+
</html>
|
|
180
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reframe-video",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.23",
|
|
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",
|
package/skills/reframe/SKILL.md
CHANGED
|
@@ -74,6 +74,9 @@ to handle explicitly:
|
|
|
74
74
|
- **Batch**: `npx -y reframe-video batch scene.ts data.json` — one mp4 per
|
|
75
75
|
data row; row keys are overlay addresses (`nodes.<id>.<prop>`,
|
|
76
76
|
`timeline.<label>.duration`, ...). CSV works too (headers = addresses).
|
|
77
|
+
- **HTML/GSAP scenes**: `render` also accepts a self-contained `.html` scene and
|
|
78
|
+
captures it deterministically via a virtual clock — read
|
|
79
|
+
`npx -y reframe-video guide --html` before writing one.
|
|
77
80
|
- **Preview editor**: `npx -y reframe-video preview` — scrub/play/knobs for
|
|
78
81
|
scenes in the current directory; the user's knob edits export as an overlay
|
|
79
82
|
JSON they can pass to render.
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "reframe",
|
|
3
|
-
"owner": {
|
|
4
|
-
"name": "Kiyeon Jeon",
|
|
5
|
-
"url": "https://github.com/kiyeonjeon21"
|
|
6
|
-
},
|
|
7
|
-
"plugins": [
|
|
8
|
-
{
|
|
9
|
-
"name": "reframe",
|
|
10
|
-
"source": "./",
|
|
11
|
-
"description": "Motion-graphics videos as addressable data — generate, tweak, regenerate without losing human edits."
|
|
12
|
-
}
|
|
13
|
-
]
|
|
14
|
-
}
|