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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reframe",
3
- "version": "0.1.0",
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 scene-authoring guide / regeneration contract — **feed this to your AI** |
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|--directing] print a guide (--regen: stable-address contract; --directing: high-end workflow)
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 repoFile = { regen: join9(ROOT2, "docs", "regen-contract.md"), directing: join9(ROOT2, "benchmark", "guides", "directing-guide.md"), edsl: join9(ROOT2, "benchmark", "guides", "edsl-guide.md") };
3190
- const pkgFile = { regen: join9(ROOT2, "guides", "regen-contract.md"), directing: join9(ROOT2, "guides", "directing-guide.md"), edsl: join9(ROOT2, "guides", "edsl-guide.md") };
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(`${ROOT2}
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(ROOT2, "skills", "reframe", "SKILL.md"), "utf8"));
3214
+ process.stdout.write(await readFile7(join9(PLUGIN_DIR, "skills", "reframe", "SKILL.md"), "utf8"));
3204
3215
  return;
3205
3216
  }
3206
3217
  case "demo":
@@ -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.21",
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",
@@ -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
- }