reframe-video 0.6.18 → 0.6.20
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/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +9 -0
- package/README.md +45 -0
- package/dist/bin.js +125 -31
- package/dist/cli.js +67 -25
- package/dist/compile-api.d.ts +6 -0
- package/dist/compile-api.js +477 -0
- package/dist/compile.js +477 -0
- package/dist/diff.js +67 -25
- package/dist/frame.js +1149 -0
- package/dist/labels.js +71 -31
- package/dist/renderer-canvas.d.ts +1 -0
- package/dist/renderer-canvas.js +1 -1
- package/dist/types-renderer/index.d.ts +34 -0
- package/package.json +14 -3
- package/skills/reframe/SKILL.md +91 -0
package/dist/labels.js
CHANGED
|
@@ -678,42 +678,76 @@ import { dirname, resolve } from "node:path";
|
|
|
678
678
|
import { fileURLToPath } from "node:url";
|
|
679
679
|
var HERE = dirname(fileURLToPath(import.meta.url));
|
|
680
680
|
var CORE_ENTRY = true ? resolve(HERE, "index.js") : resolve(HERE, "..", "..", "core", "src", "index.ts");
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
681
|
+
var SceneLoadError = class extends Error {
|
|
682
|
+
kind;
|
|
683
|
+
constructor(kind, message, options) {
|
|
684
|
+
super(message, options);
|
|
685
|
+
this.name = "SceneLoadError";
|
|
686
|
+
this.kind = kind;
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
var clean = (err) => (err instanceof Error ? err.message : String(err)).replace(
|
|
690
|
+
/data:text\/javascript;base64,[A-Za-z0-9+/=]+/g,
|
|
691
|
+
"<scene bundle>"
|
|
692
|
+
);
|
|
693
|
+
var ALIAS = { "@reframe/core": CORE_ENTRY, "reframe-video": CORE_ENTRY };
|
|
694
|
+
async function bundle(input) {
|
|
695
|
+
const common = {
|
|
696
|
+
bundle: true,
|
|
697
|
+
format: "esm",
|
|
698
|
+
platform: "neutral",
|
|
699
|
+
write: false,
|
|
700
|
+
logLevel: "silent",
|
|
701
|
+
sourcemap: "inline",
|
|
702
|
+
alias: ALIAS
|
|
703
|
+
};
|
|
684
704
|
try {
|
|
685
|
-
const out = await build(
|
|
686
|
-
entryPoints: [
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
platform: "neutral",
|
|
690
|
-
write: false,
|
|
691
|
-
logLevel: "silent",
|
|
692
|
-
sourcemap: "inline",
|
|
693
|
-
// both specifiers accepted: the guide's canonical "@reframe/core" and
|
|
694
|
-
// the published package name
|
|
695
|
-
alias: { "@reframe/core": CORE_ENTRY, "reframe-video": CORE_ENTRY }
|
|
696
|
-
});
|
|
697
|
-
code = out.outputFiles[0].text;
|
|
705
|
+
const out = await build(
|
|
706
|
+
"path" in input ? { ...common, entryPoints: [input.path] } : { ...common, stdin: { contents: input.code, resolveDir: input.resolveDir, loader: "ts", sourcefile: "scene.ts" } }
|
|
707
|
+
);
|
|
708
|
+
return out.outputFiles[0].text;
|
|
698
709
|
} catch (err) {
|
|
699
|
-
throw new
|
|
700
|
-
${err instanceof Error ? err.message : String(err)}`);
|
|
710
|
+
throw new SceneLoadError("bundle", clean(err), { cause: err });
|
|
701
711
|
}
|
|
702
|
-
|
|
703
|
-
|
|
712
|
+
}
|
|
713
|
+
async function importDefault(code, label) {
|
|
714
|
+
let mod;
|
|
715
|
+
try {
|
|
716
|
+
mod = await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`);
|
|
717
|
+
} catch (err) {
|
|
718
|
+
const kind = err instanceof Error && err.name === "SceneValidationError" ? "validation" : "eval";
|
|
719
|
+
throw new SceneLoadError(kind, clean(err), { cause: err });
|
|
720
|
+
}
|
|
721
|
+
if (mod.default === void 0) throw new SceneLoadError("eval", `${label} must default-export a scene or composition`);
|
|
704
722
|
return mod.default;
|
|
705
723
|
}
|
|
724
|
+
async function loadDefault(path3) {
|
|
725
|
+
if (path3.endsWith(".json")) {
|
|
726
|
+
try {
|
|
727
|
+
return JSON.parse(await readFile(path3, "utf8"));
|
|
728
|
+
} catch (err) {
|
|
729
|
+
throw new SceneLoadError("eval", `failed to read ${path3}: ${clean(err)}`, { cause: err });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return importDefault(await bundle({ path: path3 }), path3);
|
|
733
|
+
}
|
|
706
734
|
function isComposition(def) {
|
|
707
735
|
return typeof def === "object" && def !== null && Array.isArray(def.scenes);
|
|
708
736
|
}
|
|
709
|
-
|
|
710
|
-
const def = await loadDefault(path3);
|
|
737
|
+
function asScene(def, label) {
|
|
711
738
|
if (isComposition(def)) {
|
|
712
|
-
throw new
|
|
739
|
+
throw new SceneLoadError("validation", `${label} is a composition \u2014 render it directly, not as a single scene`);
|
|
740
|
+
}
|
|
741
|
+
try {
|
|
742
|
+
validateScene(def);
|
|
743
|
+
} catch (err) {
|
|
744
|
+
throw new SceneLoadError("validation", clean(err), { cause: err });
|
|
713
745
|
}
|
|
714
|
-
validateScene(def);
|
|
715
746
|
return def;
|
|
716
747
|
}
|
|
748
|
+
async function loadScene(path3) {
|
|
749
|
+
return asScene(await loadDefault(path3), path3);
|
|
750
|
+
}
|
|
717
751
|
|
|
718
752
|
// ../render-cli/src/labels.ts
|
|
719
753
|
var path2 = process.argv[2];
|
|
@@ -721,11 +755,17 @@ if (!path2) {
|
|
|
721
755
|
console.error("usage: reframe labels <scene.ts|.json>");
|
|
722
756
|
process.exit(1);
|
|
723
757
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
console.log(`# ${
|
|
729
|
-
|
|
730
|
-
|
|
758
|
+
async function main() {
|
|
759
|
+
const scene = await loadScene(path2);
|
|
760
|
+
const compiled = compileScene(scene);
|
|
761
|
+
const rows = [...compiled.labelTimes.entries()].sort((a, b) => a[1].t0 - b[1].t0 || a[0].localeCompare(b[0]));
|
|
762
|
+
console.log(`# ${scene.id} \u2014 ${rows.length} labels \xB7 ${compiled.duration.toFixed(2)}s @ ${scene.fps ?? 30}fps`);
|
|
763
|
+
console.log(`# ${"start".padStart(7)} ${"end".padStart(7)} label`);
|
|
764
|
+
for (const [name, { t0, t1 }] of rows) {
|
|
765
|
+
console.log(`${`${t0.toFixed(2)}s`.padStart(8)} ${`${t1.toFixed(2)}s`.padStart(8)} ${name}`);
|
|
766
|
+
}
|
|
731
767
|
}
|
|
768
|
+
main().catch((err) => {
|
|
769
|
+
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
770
|
+
process.exit(1);
|
|
771
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./types-renderer/index.js";
|
package/dist/renderer-canvas.js
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DisplayList -> Canvas 2D. Drawing only — all animation math lives in
|
|
3
|
+
* @reframe/core's evaluate(). Restricted to the plain Canvas 2D API so the
|
|
4
|
+
* same code runs in the browser (preview), under Playwright (export), and
|
|
5
|
+
* could port to skia-canvas later.
|
|
6
|
+
*/
|
|
7
|
+
import type { CompiledScene, DisplayList, SceneIR } from "../types/index.js";
|
|
8
|
+
/**
|
|
9
|
+
* Decoded images keyed by the RAW src string from the IR (never a resolved
|
|
10
|
+
* path/URL — the DisplayList stays machine-independent). Consumers populate
|
|
11
|
+
* it before the first frame; a plain Map satisfies the interface.
|
|
12
|
+
*/
|
|
13
|
+
export interface ImageRegistry {
|
|
14
|
+
get(src: string): CanvasImageSource | undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Decoded video frames keyed by the RAW src string + frame index. A video is
|
|
18
|
+
* rendered as a frame sequence (extracted at the scene fps): `frame(src, i)`
|
|
19
|
+
* returns the i-th source frame, clamped to the available range by the consumer.
|
|
20
|
+
*/
|
|
21
|
+
export interface VideoRegistry {
|
|
22
|
+
frame(src: string, index: number): CanvasImageSource | undefined;
|
|
23
|
+
}
|
|
24
|
+
export declare function renderFrame(ctx: CanvasRenderingContext2D, compiled: CompiledScene, t: number, images?: ImageRegistry, videos?: VideoRegistry): void;
|
|
25
|
+
export declare function drawDisplayList(ctx: CanvasRenderingContext2D, ops: DisplayList, images?: ImageRegistry, videos?: VideoRegistry): void;
|
|
26
|
+
/** Center cover-crop: the source rect (in image pixels) that fills a dw×dh box at
|
|
27
|
+
* the image's natural aspect — the larger axis is cropped equally on both sides. */
|
|
28
|
+
export declare function coverRect(iw: number, ih: number, dw: number, dh: number): {
|
|
29
|
+
sx: number;
|
|
30
|
+
sy: number;
|
|
31
|
+
sw: number;
|
|
32
|
+
sh: number;
|
|
33
|
+
};
|
|
34
|
+
export type { SceneIR };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reframe-video",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.20",
|
|
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",
|
|
@@ -28,13 +28,24 @@
|
|
|
28
28
|
".": {
|
|
29
29
|
"types": "./dist/index.d.ts",
|
|
30
30
|
"import": "./dist/index.js"
|
|
31
|
-
}
|
|
31
|
+
},
|
|
32
|
+
"./renderer": {
|
|
33
|
+
"types": "./dist/renderer-canvas.d.ts",
|
|
34
|
+
"import": "./dist/renderer-canvas.js"
|
|
35
|
+
},
|
|
36
|
+
"./compile": {
|
|
37
|
+
"types": "./dist/compile-api.d.ts",
|
|
38
|
+
"import": "./dist/compile-api.js"
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
32
41
|
},
|
|
33
42
|
"files": [
|
|
34
43
|
"dist",
|
|
35
44
|
"assets",
|
|
36
45
|
"guides",
|
|
37
|
-
"preview"
|
|
46
|
+
"preview",
|
|
47
|
+
".claude-plugin",
|
|
48
|
+
"skills"
|
|
38
49
|
],
|
|
39
50
|
"engines": {
|
|
40
51
|
"node": ">=20"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reframe
|
|
3
|
+
description: Create and iterate motion-graphics videos (mp4) — title cards, lower thirds, kinetic typography, product teasers, data-driven video batches. Use when the user asks to make, edit, retime, personalize, or add sound to an animated video. Scenes are declarative data; renders are deterministic; human edits survive regeneration.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# reframe — motion graphics as addressable data
|
|
7
|
+
|
|
8
|
+
All commands run through npx; no install or project setup is needed. The
|
|
9
|
+
runtime needs ffmpeg on PATH and a one-time `npx playwright install chromium`
|
|
10
|
+
(the render command prints an actionable hint if either is missing).
|
|
11
|
+
|
|
12
|
+
## Creating a scene
|
|
13
|
+
|
|
14
|
+
1. **Read the guide first** — it is the complete, current syntax (~1,700
|
|
15
|
+
tokens) and one read is enough to write valid scenes:
|
|
16
|
+
`npx -y reframe-video guide`
|
|
17
|
+
2. Write a single self-contained `<name>.ts` in the user's directory
|
|
18
|
+
(`npx -y reframe-video new <name>` scaffolds a documented starter).
|
|
19
|
+
Scenes must be pure functions of time: no `Math.random()`/`Date` — use
|
|
20
|
+
`wiggle` with a seed. Give every node a meaningful stable `id` and label
|
|
21
|
+
the key timeline moments — those names are addresses for everything below.
|
|
22
|
+
3. Render and verify: `npx -y reframe-video render <name>.ts` → `out/<name>.mp4`.
|
|
23
|
+
|
|
24
|
+
## Directing a high-end piece (cinematic / reference-faithful)
|
|
25
|
+
|
|
26
|
+
Simple jobs (a lower-third, a logo sting, a KPI card) just work from the guide.
|
|
27
|
+
But a CINEMATIC or REFERENCE-FAITHFUL piece (a product teaser, a UI/session
|
|
28
|
+
reproduction, a title sequence) needs a director's process — **read it first**:
|
|
29
|
+
`npx -y reframe-video guide --directing`. The short version:
|
|
30
|
+
|
|
31
|
+
1. Get the spec from the user: concept, **references** (screenshots / a reference
|
|
32
|
+
video / pasted real content — save them to disk), exact brand colors, length +
|
|
33
|
+
aspect, and tone. Vague prompts are why these take many rounds.
|
|
34
|
+
2. **Storyboard the beats** with `beat("setup"/"rising"/"climax"/…)` BEFORE animating.
|
|
35
|
+
3. **Match references with the `diff` tool** instead of eyeballing:
|
|
36
|
+
`npx -y reframe-video diff ref.png --mode grid` (measure a screenshot),
|
|
37
|
+
then `... diff ref.png scene.ts --mode side|diff` (compare a render) → fix → repeat.
|
|
38
|
+
4. Apply cinematic craft: camera push-in per beat (`cameraTo` in `par`), curved
|
|
39
|
+
entrances (`motionPath` + `easeOutBack`), fake/real depth, layered `oscillate`
|
|
40
|
+
idle, and label-anchored sound.
|
|
41
|
+
5. **Verify objectively**: `... labels` (exact beat seconds), `... motion out.mp4`
|
|
42
|
+
(makes "more dynamic" measurable), `... trace ref.mp4 --apply scene.ts` (borrow a
|
|
43
|
+
reference VIDEO's timing), `... preview` (hand-tune → overlay that survives regen).
|
|
44
|
+
|
|
45
|
+
## Modifying an existing scene — the contract
|
|
46
|
+
|
|
47
|
+
Before rewriting any existing scene, read the regeneration contract:
|
|
48
|
+
`npx -y reframe-video guide --regen`. The core rule: **never rename node ids,
|
|
49
|
+
state names, or timeline labels for concepts that survive the redesign** —
|
|
50
|
+
the user's overlay documents hold their hand edits at those addresses.
|
|
51
|
+
|
|
52
|
+
The user may keep personal edits in an overlay JSON and render with
|
|
53
|
+
`--overlay <file>`. Check the conversation for overlay usage. Two situations
|
|
54
|
+
to handle explicitly:
|
|
55
|
+
|
|
56
|
+
- After your rewrite, the render's compose report lists orphaned edits for
|
|
57
|
+
concepts that were genuinely removed — relay that report to the user; never
|
|
58
|
+
let an edit disappear silently.
|
|
59
|
+
- If the user asks you to change a property their overlay already overrides,
|
|
60
|
+
editing the scene alone will be invisible in their renders. Resolve the
|
|
61
|
+
mask (update the scene AND remove/update the superseded overlay entry) and
|
|
62
|
+
tell them why.
|
|
63
|
+
|
|
64
|
+
## Other capabilities
|
|
65
|
+
|
|
66
|
+
- **Batch**: `npx -y reframe-video batch scene.ts data.json` — one mp4 per
|
|
67
|
+
data row; row keys are overlay addresses (`nodes.<id>.<prop>`,
|
|
68
|
+
`timeline.<label>.duration`, ...). CSV works too (headers = addresses).
|
|
69
|
+
- **Preview editor**: `npx -y reframe-video preview` — scrub/play/knobs for
|
|
70
|
+
scenes in the current directory; the user's knob edits export as an overlay
|
|
71
|
+
JSON they can pass to render.
|
|
72
|
+
- **Audio**: `scene.audio` cues anchor to timeline labels, so sound follows
|
|
73
|
+
retiming and regeneration. Procedural sfx (whoosh/pop/tick/rise/shimmer/
|
|
74
|
+
thud) plus bundled CC0 samples (mechanical keypresses, clicks). The guide's
|
|
75
|
+
Audio section has the schema.
|
|
76
|
+
- **Motion check**: `npx -y reframe-video motion out/<name>.mp4` prints a
|
|
77
|
+
calibrated motion profile (speeds, static fraction, discontinuities) —
|
|
78
|
+
useful to verify a vague request like "make it more dynamic" objectively.
|
|
79
|
+
- **Image sequences** (the "glyph reveal" / stop-motion format): generated
|
|
80
|
+
stills become `image` nodes stacked in painter's order; hard cuts are
|
|
81
|
+
0.01s opacity steps every ~0.15s, a slow camera-group scale tween adds the
|
|
82
|
+
push-in, `wiggle` adds shake, and a label per cut anchors a tick sfx.
|
|
83
|
+
Keep frame ids stable (`frame-0..N`) so the user can swap any plate via
|
|
84
|
+
overlay or batch row (`nodes.frame-3.src`). Image `src` paths resolve
|
|
85
|
+
relative to the scene file.
|
|
86
|
+
|
|
87
|
+
## Verification habits
|
|
88
|
+
|
|
89
|
+
Render after every change. For visual checks, extract a few frames with
|
|
90
|
+
ffmpeg and look at them. Same input renders byte-identically, so "it changed"
|
|
91
|
+
or "it didn't change" is always provable.
|