reframe-video 0.1.2 → 0.2.0
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/assets/sfx/LICENSE.md +1 -1
- package/assets/sfx/bong_001.ogg +0 -0
- package/assets/sfx/click_001.ogg +0 -0
- package/assets/sfx/confirmation_002.ogg +0 -0
- package/assets/sfx/confirmation_003.ogg +0 -0
- package/assets/sfx/confirmation_004.ogg +0 -0
- package/assets/sfx/glass_001.ogg +0 -0
- package/assets/sfx/maximize_001.ogg +0 -0
- package/assets/sfx/maximize_002.ogg +0 -0
- package/assets/sfx/maximize_005.ogg +0 -0
- package/assets/sfx/maximize_009.ogg +0 -0
- package/assets/sfx/open_001.ogg +0 -0
- package/assets/sfx/pluck_001.ogg +0 -0
- package/assets/sfx/pluck_002.ogg +0 -0
- package/assets/sfx/select_001.ogg +0 -0
- package/assets/sfx/select_002.ogg +0 -0
- package/assets/sfx/select_003.ogg +0 -0
- package/dist/bin.js +724 -131
- package/dist/browserEntry.js +130 -68
- package/dist/cli.js +445 -85
- package/dist/index.js +674 -86
- package/dist/labels.js +606 -0
- package/dist/renderer-canvas.js +15 -0
- package/dist/trace-cli.js +9 -9
- package/dist/types/audio.d.ts +9 -0
- package/dist/types/compile.d.ts +1 -0
- package/dist/types/compose.d.ts +18 -2
- package/dist/types/composeComposition.d.ts +27 -0
- package/dist/types/devicePreset.d.ts +65 -0
- package/dist/types/dsl.d.ts +12 -1
- package/dist/types/evaluate.d.ts +32 -0
- package/dist/types/index.d.ts +6 -3
- package/dist/types/ir.d.ts +68 -0
- package/dist/types/motionOps.d.ts +36 -0
- package/dist/types/path.d.ts +7 -3
- package/dist/types/validate.d.ts +4 -1
- package/guides/edsl-guide.md +2 -1
- package/package.json +1 -1
- package/preview/index.html +56 -3
- package/preview/src/main.ts +1132 -46
- package/preview/src/panel.ts +478 -8
- package/preview/src/store.ts +323 -6
package/dist/types/compile.d.ts
CHANGED
package/dist/types/compose.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* validation errors on the composed result. Silent failure is the one
|
|
9
9
|
* behavior this module must never have.
|
|
10
10
|
*/
|
|
11
|
-
import type { BehaviorIR, Ease, NodeIR, PropValue, SceneIR } from "./ir.js";
|
|
11
|
+
import type { BehaviorIR, Ease, NodeIR, PropValue, SceneIR, TimelineIR } from "./ir.js";
|
|
12
12
|
export interface OverlayDoc {
|
|
13
13
|
reframeOverlay: 1;
|
|
14
14
|
/** Shown in reports; falls back to "overlay-<index>". */
|
|
@@ -34,6 +34,20 @@ export interface OverlayDoc {
|
|
|
34
34
|
};
|
|
35
35
|
/** Complete nodes appended at the scene root, owned by this overlay. */
|
|
36
36
|
addNodes?: NodeIR[];
|
|
37
|
+
/**
|
|
38
|
+
* Remove nodes by id. Only nodes added by an overlay (via `addNodes`) can be
|
|
39
|
+
* removed — a node owned by the BASE scene is refused and reported as an
|
|
40
|
+
* orphan (hide it with `opacity: 0` instead, so the regenerated design is
|
|
41
|
+
* never silently dropped). An unknown id is likewise an orphan.
|
|
42
|
+
*/
|
|
43
|
+
removeNodes?: string[];
|
|
44
|
+
/**
|
|
45
|
+
* Motion fragments (e.g. `motionOp(...)` beats) APPENDED to the scene
|
|
46
|
+
* timeline — composed in `par` with the base under their own beat labels, so
|
|
47
|
+
* the editor can ADD motion to a node, not just patch existing motion. A
|
|
48
|
+
* fragment whose target id is gone is skipped and reported as an orphan.
|
|
49
|
+
*/
|
|
50
|
+
addTimeline?: TimelineIR[];
|
|
37
51
|
/**
|
|
38
52
|
* Parameter patches on labeled timeline steps (or beats by name). Patchable
|
|
39
53
|
* per kind: to -> duration/ease/stagger, tween -> duration/ease,
|
|
@@ -52,13 +66,15 @@ export interface OverlayDoc {
|
|
|
52
66
|
scale?: number;
|
|
53
67
|
order?: number;
|
|
54
68
|
points?: [number, number][];
|
|
69
|
+
curviness?: number;
|
|
70
|
+
autoRotate?: boolean;
|
|
55
71
|
}>;
|
|
56
72
|
}
|
|
57
73
|
export interface ComposeReport {
|
|
58
74
|
applied: {
|
|
59
75
|
layer: string;
|
|
60
76
|
address: string;
|
|
61
|
-
action: "set" | "unset" | "add-node" | "behavior-set" | "behavior-remove";
|
|
77
|
+
action: "set" | "unset" | "add-node" | "remove-node" | "behavior-set" | "behavior-remove" | "add-timeline";
|
|
62
78
|
}[];
|
|
63
79
|
orphans: {
|
|
64
80
|
layer: string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composition layout: place each scene on a single timeline and report the
|
|
3
|
+
* total duration. Pure data in, pure data out — like compileScene, this never
|
|
4
|
+
* renders. Each scene is compiled independently (compileScene), so a scene's
|
|
5
|
+
* frames inside a composition equal rendering it alone, offset by its `start`.
|
|
6
|
+
*/
|
|
7
|
+
import { type CompiledScene } from "./compile.js";
|
|
8
|
+
import { type CompositionIR, type SceneIR, type SceneTransition } from "./ir.js";
|
|
9
|
+
export interface ScenePlacement {
|
|
10
|
+
id: string;
|
|
11
|
+
scene: SceneIR;
|
|
12
|
+
compiled: CompiledScene;
|
|
13
|
+
/** Absolute start on the composition timeline (s). */
|
|
14
|
+
start: number;
|
|
15
|
+
/** The scene's own duration (s). */
|
|
16
|
+
duration: number;
|
|
17
|
+
transition: SceneTransition;
|
|
18
|
+
/** Overlap with the previous scene (s); 0 for a cut. */
|
|
19
|
+
overlap: number;
|
|
20
|
+
}
|
|
21
|
+
export interface CompiledComposition {
|
|
22
|
+
ir: CompositionIR;
|
|
23
|
+
scenes: ScenePlacement[];
|
|
24
|
+
/** Total composition duration (s) — the end of the last scene. */
|
|
25
|
+
duration: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function compileComposition(comp: CompositionIR): CompiledComposition;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-mockup presets: a parametric vector frame (phone/laptop/browser/…) with
|
|
3
|
+
* a CLIPPED screen "content slot". The sibling of motionPreset — that generates
|
|
4
|
+
* a TimelineIR, this generates a NodeIR subtree. Pure primitives + clip, so no
|
|
5
|
+
* assets and fully deterministic (plain JSON, no Date/random). A 2.5D vector
|
|
6
|
+
* tier — no true perspective.
|
|
7
|
+
*
|
|
8
|
+
* devicePreset("phone", { id: "hero", content: [ ...your UI nodes ] })
|
|
9
|
+
*
|
|
10
|
+
* Each instance needs a distinct `id` (it prefixes every generated node id);
|
|
11
|
+
* two with the same prefix collide via the scene's duplicate-id validation.
|
|
12
|
+
*
|
|
13
|
+
* Layout/teardown helpers: `deviceScreen` (content-local screen bounds),
|
|
14
|
+
* `deviceScreenCenter` (device-local screen origin — slide `${id}-screen` against
|
|
15
|
+
* it to eject the panel for an exploded view), `deviceBounds` (full frame
|
|
16
|
+
* footprint — for laying many devices on a grid).
|
|
17
|
+
*/
|
|
18
|
+
import type { NodeIR } from "./ir.js";
|
|
19
|
+
export declare const DEVICE_PRESET_NAMES: readonly ["phone", "tablet", "laptop", "browser", "watch", "monitor", "tv", "foldable", "terminal", "car"];
|
|
20
|
+
export type DevicePresetName = (typeof DEVICE_PRESET_NAMES)[number];
|
|
21
|
+
export interface DevicePresetOpts {
|
|
22
|
+
/** Id PREFIX for every generated node (default "device"). Make it unique per instance. */
|
|
23
|
+
id?: string;
|
|
24
|
+
/** Device-center placement (default 0,0). */
|
|
25
|
+
x?: number;
|
|
26
|
+
y?: number;
|
|
27
|
+
/** Uniform scale (default 1). */
|
|
28
|
+
scale?: number;
|
|
29
|
+
/** Outer-group opacity (default 1) — handy to start hidden for an entrance. */
|
|
30
|
+
opacity?: number;
|
|
31
|
+
/** Body palette (default "dark"). */
|
|
32
|
+
color?: "dark" | "light";
|
|
33
|
+
/** Screen background fill override (default per palette). */
|
|
34
|
+
screen?: string;
|
|
35
|
+
/** Portrait/landscape — phone & tablet only (default "portrait"). */
|
|
36
|
+
orientation?: "portrait" | "landscape";
|
|
37
|
+
/** Nodes placed inside the screen (authored in screen-LOCAL centre coords), clipped. */
|
|
38
|
+
content?: NodeIR[];
|
|
39
|
+
/** Browser/terminal address-bar text. */
|
|
40
|
+
url?: string;
|
|
41
|
+
}
|
|
42
|
+
/** The screen's content area (content-local coords: origin 0,0 = screen centre,
|
|
43
|
+
* pre-scale). Author/scroll `content` against these bounds. */
|
|
44
|
+
export declare function deviceScreen(name: DevicePresetName, opts?: DevicePresetOpts): {
|
|
45
|
+
x: number;
|
|
46
|
+
y: number;
|
|
47
|
+
width: number;
|
|
48
|
+
height: number;
|
|
49
|
+
radius: number;
|
|
50
|
+
};
|
|
51
|
+
/** The screen group's centre in device-LOCAL coords. Slide `${id}-screen` against
|
|
52
|
+
* this (e.g. `y: deviceScreenCenter(name).y + 160`) to eject the panel from the
|
|
53
|
+
* frame for an exploded / teardown view. */
|
|
54
|
+
export declare function deviceScreenCenter(name: DevicePresetName, opts?: DevicePresetOpts): {
|
|
55
|
+
x: number;
|
|
56
|
+
y: number;
|
|
57
|
+
};
|
|
58
|
+
/** Full frame footprint (width/height incl. chrome & stands) in device-local
|
|
59
|
+
* units — use it to scale many devices onto a shared grid. */
|
|
60
|
+
export declare function deviceBounds(name: DevicePresetName, opts?: DevicePresetOpts): {
|
|
61
|
+
width: number;
|
|
62
|
+
height: number;
|
|
63
|
+
};
|
|
64
|
+
/** Build a device-mockup frame (a group) with a clipped screen content slot. */
|
|
65
|
+
export declare function devicePreset(name: DevicePresetName, opts?: DevicePresetOpts): NodeIR;
|
package/dist/types/dsl.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The eDSL surface: thin factories that build plain IR objects.
|
|
3
3
|
* `scene()` validates and returns the IR — the return value is the document.
|
|
4
4
|
*/
|
|
5
|
-
import type { AudioIR, BehaviorIR, Ease, EllipseProps, GroupProps, ImageProps, LineProps, NodeIR, PathProps, PropValue, RectProps, SceneIR, Size, StateOverride, TextProps, TimelineIR } from "./ir.js";
|
|
5
|
+
import type { AudioIR, BehaviorIR, CompositionIR, CompositionSceneEntry, Ease, EllipseProps, GroupProps, ImageProps, LineProps, NodeIR, PathProps, PropValue, RectProps, SceneIR, Size, StateOverride, TextProps, TimelineIR } from "./ir.js";
|
|
6
6
|
export interface SceneInput {
|
|
7
7
|
id: string;
|
|
8
8
|
size: Size;
|
|
@@ -18,6 +18,14 @@ export interface SceneInput {
|
|
|
18
18
|
meta?: Record<string, unknown>;
|
|
19
19
|
}
|
|
20
20
|
export declare function scene(input: SceneInput): SceneIR;
|
|
21
|
+
export interface CompositionInput {
|
|
22
|
+
id: string;
|
|
23
|
+
scenes: CompositionSceneEntry[];
|
|
24
|
+
audio?: AudioIR;
|
|
25
|
+
meta?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
/** Build a composition: an ordered list of independent scenes + transitions. */
|
|
28
|
+
export declare function composition(input: CompositionInput): CompositionIR;
|
|
21
29
|
export declare function rect(props: {
|
|
22
30
|
id: string;
|
|
23
31
|
} & RectProps): NodeIR;
|
|
@@ -43,6 +51,8 @@ export declare function seq(...children: TimelineIR[]): TimelineIR;
|
|
|
43
51
|
export declare function par(...children: TimelineIR[]): TimelineIR;
|
|
44
52
|
export declare function stagger(interval: number, ...children: TimelineIR[]): TimelineIR;
|
|
45
53
|
export interface BeatOpts {
|
|
54
|
+
/** Node ids this beat owns (the intent graph) — additive metadata only. */
|
|
55
|
+
nodes?: string[];
|
|
46
56
|
/** Group children in parallel instead of sequence. */
|
|
47
57
|
parallel?: boolean;
|
|
48
58
|
/** Absolute start (rigid placement). */
|
|
@@ -74,6 +84,7 @@ export declare function motionPath(target: string, points: [number, number][], o
|
|
|
74
84
|
duration?: number;
|
|
75
85
|
ease?: Ease;
|
|
76
86
|
closed?: boolean;
|
|
87
|
+
curviness?: number;
|
|
77
88
|
autoRotate?: boolean;
|
|
78
89
|
rotateOffset?: number;
|
|
79
90
|
label?: string;
|
package/dist/types/evaluate.d.ts
CHANGED
|
@@ -4,8 +4,15 @@
|
|
|
4
4
|
* always. Renderers only draw; they never compute animation.
|
|
5
5
|
*/
|
|
6
6
|
import type { CompiledScene } from "./compile.js";
|
|
7
|
+
import type { ClipShape, PropValue } from "./ir.js";
|
|
7
8
|
/** Canvas-style 2D affine matrix [a, b, c, d, e, f]. */
|
|
8
9
|
export type Mat2D = [number, number, number, number, number, number];
|
|
10
|
+
/** A clip from an ancestor group: its shape in the group's coordinate space,
|
|
11
|
+
* plus the matrix mapping that space to the scene. The renderer intersects it. */
|
|
12
|
+
export interface ClipRegion {
|
|
13
|
+
transform: Mat2D;
|
|
14
|
+
shape: ClipShape;
|
|
15
|
+
}
|
|
9
16
|
export type TextAlign = "left" | "center" | "right";
|
|
10
17
|
export type TextBaseline = "top" | "middle" | "bottom";
|
|
11
18
|
interface OpBase {
|
|
@@ -15,6 +22,8 @@ interface OpBase {
|
|
|
15
22
|
transform: Mat2D;
|
|
16
23
|
/** Cumulative opacity, parent-multiplied. */
|
|
17
24
|
opacity: number;
|
|
25
|
+
/** Clip regions from ancestor groups (intersected by the renderer). */
|
|
26
|
+
clips?: ClipRegion[];
|
|
18
27
|
}
|
|
19
28
|
export type DisplayOp = (OpBase & {
|
|
20
29
|
type: "rect";
|
|
@@ -72,5 +81,28 @@ export type DisplayOp = (OpBase & {
|
|
|
72
81
|
strokeWidth?: number;
|
|
73
82
|
});
|
|
74
83
|
export type DisplayList = DisplayOp[];
|
|
84
|
+
/**
|
|
85
|
+
* The node's local affine matrix: Translate(x,y) ∘ Rotate ∘ Skew ∘ Scale, around
|
|
86
|
+
* the anchor. `scaleX/scaleY` are per-axis multipliers on `scale`; `skewX/skewY`
|
|
87
|
+
* are shear angles in degrees (a 2.5D "tilt" — no true perspective). The fast
|
|
88
|
+
* path returns the exact uniform formula at defaults, so existing scenes stay
|
|
89
|
+
* byte-identical (the determinism/golden contract).
|
|
90
|
+
*/
|
|
91
|
+
export declare function localMatrix(x: number, y: number, rotationDeg: number, scale: number, scaleX?: number, scaleY?: number, skewXDeg?: number, skewYDeg?: number): Mat2D;
|
|
92
|
+
/**
|
|
93
|
+
* Sample one node prop at time t — the single source of animated values shared
|
|
94
|
+
* by `evaluate` (rendering) and `nodeParentMatrix` (editor hit/drag math), so
|
|
95
|
+
* both agree to the last bit. Pure; the determinism contract rests on it.
|
|
96
|
+
*/
|
|
97
|
+
export declare function sampleProp(compiled: CompiledScene, t: number, target: string, prop: string, fallback: PropValue): PropValue;
|
|
98
|
+
/**
|
|
99
|
+
* The accumulated transform of a node's ANCESTORS at time t — the coordinate
|
|
100
|
+
* space its `x/y` live in (identity for a top-level node). The editor inverts
|
|
101
|
+
* this to convert a scene-space drag delta into the node's parent space, so a
|
|
102
|
+
* nested child can be dragged and the overlay still writes `nodes.<id>.x/y`.
|
|
103
|
+
* Walks groups exactly as `evaluate` does (same sampler, no opacity culling so
|
|
104
|
+
* an invisible-at-t node is still positionable). Returns null if id is unknown.
|
|
105
|
+
*/
|
|
106
|
+
export declare function nodeParentMatrix(compiled: CompiledScene, id: string, t: number): Mat2D | null;
|
|
75
107
|
export declare function evaluate(compiled: CompiledScene, t: number): DisplayList;
|
|
76
108
|
export {};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
export * from "./ir.js";
|
|
2
2
|
export * from "./dsl.js";
|
|
3
|
-
export { validateScene, SceneValidationError, PROPS_BY_TYPE } from "./validate.js";
|
|
3
|
+
export { validateScene, validateComposition, SceneValidationError, PROPS_BY_TYPE } from "./validate.js";
|
|
4
|
+
export { compileComposition, type CompiledComposition, type ScenePlacement, } from "./composeComposition.js";
|
|
4
5
|
export { composeScene, formatComposeReport, type OverlayDoc, type ComposeReport, } from "./compose.js";
|
|
5
6
|
export { compileScene, type CompiledScene, type PropertySegment, type LabelSpan, type MotionDriver } from "./compile.js";
|
|
6
7
|
export { pathPoint, pathTangentAngle, type Pt } from "./path.js";
|
|
7
8
|
export { motionPreset, PRESET_NAMES, type PresetName, type PresetRig, type PresetOpts } from "./presets.js";
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
9
|
+
export { devicePreset, deviceScreen, deviceScreenCenter, deviceBounds, DEVICE_PRESET_NAMES, type DevicePresetName, type DevicePresetOpts } from "./devicePreset.js";
|
|
10
|
+
export { motionOp, motionOpLabel, MOTION_OPS, type MotionOpName, type MotionOpOpts, type MotionOpResult } from "./motionOps.js";
|
|
11
|
+
export { resolveAudioPlan, resolveCompositionAudioPlan, SFX_DURATION, type AudioPlan, type ResolvedCue, } from "./audio.js";
|
|
12
|
+
export { evaluate, sampleProp, nodeParentMatrix, type DisplayList, type DisplayOp, type Mat2D, type ClipRegion, type TextAlign, type TextBaseline, } from "./evaluate.js";
|
|
10
13
|
export { resolveEase, lerpValue, isColor, EASE_NAMES } from "./interpolate.js";
|
|
11
14
|
export { sampleBehavior } from "./behaviors.js";
|
|
12
15
|
export { collectImageSrcs } from "./assets.js";
|
package/dist/types/ir.d.ts
CHANGED
|
@@ -24,6 +24,12 @@ export interface BaseProps {
|
|
|
24
24
|
opacity?: number;
|
|
25
25
|
rotation?: number;
|
|
26
26
|
scale?: number;
|
|
27
|
+
/** Per-axis scale multipliers on `scale` (default 1) — a 2.5D squash/tilt. */
|
|
28
|
+
scaleX?: number;
|
|
29
|
+
scaleY?: number;
|
|
30
|
+
/** Shear angles in degrees (default 0) — a 2.5D lean. No true perspective. */
|
|
31
|
+
skewX?: number;
|
|
32
|
+
skewY?: number;
|
|
27
33
|
anchor?: Anchor;
|
|
28
34
|
}
|
|
29
35
|
export interface RectProps extends BaseProps {
|
|
@@ -63,7 +69,28 @@ export interface TextProps extends BaseProps {
|
|
|
63
69
|
fill?: string;
|
|
64
70
|
letterSpacing?: number;
|
|
65
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* A clip region (in a group's local coordinate space) that masks its children —
|
|
74
|
+
* e.g. a rounded-rect phone screen so content inside stays within it. A rect
|
|
75
|
+
* with `radius` covers most cases; ellipse is a bonus.
|
|
76
|
+
*/
|
|
77
|
+
export type ClipShape = {
|
|
78
|
+
kind: "rect";
|
|
79
|
+
x: number;
|
|
80
|
+
y: number;
|
|
81
|
+
width: number;
|
|
82
|
+
height: number;
|
|
83
|
+
radius?: number;
|
|
84
|
+
} | {
|
|
85
|
+
kind: "ellipse";
|
|
86
|
+
x: number;
|
|
87
|
+
y: number;
|
|
88
|
+
width: number;
|
|
89
|
+
height: number;
|
|
90
|
+
};
|
|
66
91
|
export interface GroupProps extends BaseProps {
|
|
92
|
+
/** Clip the group's children to this shape (group-local coords). */
|
|
93
|
+
clip?: ClipShape;
|
|
67
94
|
}
|
|
68
95
|
export interface PathProps extends BaseProps {
|
|
69
96
|
/** SVG path data (the `d` attribute). Drawn as a true vector — crisp at any zoom. */
|
|
@@ -179,6 +206,8 @@ export type TimelineIR = {
|
|
|
179
206
|
closed?: boolean;
|
|
180
207
|
duration?: number;
|
|
181
208
|
ease?: Ease;
|
|
209
|
+
/** Tangent scale: 1 = smooth (default), 0 = sharp corners, >1 = loopier. */
|
|
210
|
+
curviness?: number;
|
|
182
211
|
autoRotate?: boolean;
|
|
183
212
|
/** Degrees added to the tangent angle (e.g. 90 if the art faces "up"). */
|
|
184
213
|
rotateOffset?: number;
|
|
@@ -194,6 +223,13 @@ export type TimelineIR = {
|
|
|
194
223
|
*/
|
|
195
224
|
kind: "beat";
|
|
196
225
|
name: string;
|
|
226
|
+
/**
|
|
227
|
+
* Node ids this beat semantically OWNS (the intent graph). Purely additive
|
|
228
|
+
* metadata — compile/evaluate ignore it, so `beat(name, { nodes }, …)` is
|
|
229
|
+
* byte-identical to `beat(name, {}, …)`. The preview groups these nodes'
|
|
230
|
+
* lanes under the beat; overlay/regen address the beat by its stable name.
|
|
231
|
+
*/
|
|
232
|
+
nodes?: string[];
|
|
197
233
|
parallel?: boolean;
|
|
198
234
|
/** Absolute start (rigid placement). Overrides sequential flow. */
|
|
199
235
|
at?: number;
|
|
@@ -288,6 +324,38 @@ export interface SceneIR {
|
|
|
288
324
|
/** Editor-only data (Theatre.js state.json pattern). */
|
|
289
325
|
meta?: Record<string, unknown>;
|
|
290
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Composition — the layer ABOVE a scene: an ordered list of independent scenes
|
|
329
|
+
* with transitions, rendered to one deterministic mp4. Each `scene` stays a
|
|
330
|
+
* normal SceneIR (renders/previews/overlays standalone, unchanged); the
|
|
331
|
+
* composition only lays out their start times and concatenates. No single-scene
|
|
332
|
+
* compile/evaluate path is touched.
|
|
333
|
+
*/
|
|
334
|
+
export type SceneTransition = "cut" | "crossfade";
|
|
335
|
+
export interface CompositionSceneEntry {
|
|
336
|
+
scene: SceneIR;
|
|
337
|
+
/** How this scene enters from the previous one. Default "cut". A crossfade
|
|
338
|
+
* overlaps the previous scene by `at` (or a default) and blends. */
|
|
339
|
+
transition?: SceneTransition;
|
|
340
|
+
/**
|
|
341
|
+
* Placement relative to the sequential append point (the previous scene's
|
|
342
|
+
* end): a number is an ABSOLUTE start (seconds); a string "-0.5"/"+0.5" shifts
|
|
343
|
+
* the sequential point (overlap / gap). Omitted = sequential (or, for a
|
|
344
|
+
* crossfade, overlap by the default crossfade duration).
|
|
345
|
+
*/
|
|
346
|
+
at?: number | string;
|
|
347
|
+
}
|
|
348
|
+
export interface CompositionIR {
|
|
349
|
+
version: 1;
|
|
350
|
+
id: string;
|
|
351
|
+
scenes: CompositionSceneEntry[];
|
|
352
|
+
/** Composition-level sound: a bed spanning scenes (e.g. kokoro narration) +
|
|
353
|
+
* absolute-time cues, layered over each scene's own offset cues. */
|
|
354
|
+
audio?: AudioIR;
|
|
355
|
+
meta?: Record<string, unknown>;
|
|
356
|
+
}
|
|
357
|
+
/** Default crossfade/overlap length (s) when a crossfade gives no explicit `at`. */
|
|
358
|
+
export declare const DEFAULT_CROSSFADE = 0.5;
|
|
291
359
|
export declare const DEFAULT_TO_DURATION = 0.5;
|
|
292
360
|
export declare const DEFAULT_TWEEN_DURATION = 0.5;
|
|
293
361
|
export declare const DEFAULT_MOTIONPATH_DURATION = 1;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion ops — a small GSAP-style library of everyday motions that apply to ANY
|
|
3
|
+
* node (text, logo paths, shapes), composed from the existing primitives
|
|
4
|
+
* (tween / motionPath / beat). `motionOp(name, target, opts)` returns a labeled
|
|
5
|
+
* beat (+ optional `setup` base-prop overrides for entrances) you can author in
|
|
6
|
+
* code or ADD to a scene from the editor via the `addTimeline` overlay verb.
|
|
7
|
+
*
|
|
8
|
+
* Ops compute absolute targets from `opts.base` (the node's current transform),
|
|
9
|
+
* so they're correct on nodes that aren't at scale 1 / origin 0.
|
|
10
|
+
*/
|
|
11
|
+
import type { PropValue, TimelineIR } from "./ir.js";
|
|
12
|
+
export type MotionOpName = "rotate" | "zoom" | "ken-burns" | "slide-in" | "fade" | "draw-on" | "pulse";
|
|
13
|
+
export declare const MOTION_OPS: MotionOpName[];
|
|
14
|
+
export interface MotionOpOpts {
|
|
15
|
+
energy?: number;
|
|
16
|
+
speed?: number;
|
|
17
|
+
amount?: number;
|
|
18
|
+
from?: "left" | "right" | "top" | "bottom";
|
|
19
|
+
/** The target node's current transform — lets scale/position ops be correct
|
|
20
|
+
* on nodes that aren't at scale 1 / origin 0. */
|
|
21
|
+
base?: {
|
|
22
|
+
scale?: number;
|
|
23
|
+
x?: number;
|
|
24
|
+
y?: number;
|
|
25
|
+
rotation?: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface MotionOpResult {
|
|
29
|
+
/** Base-prop overrides the op needs (e.g. start hidden for a fade). */
|
|
30
|
+
setup?: Record<string, Record<string, PropValue>>;
|
|
31
|
+
/** The op's animation, a labeled beat. */
|
|
32
|
+
timeline: TimelineIR;
|
|
33
|
+
}
|
|
34
|
+
/** A stable beat label for an op on a node (so it's patchable + foldable). */
|
|
35
|
+
export declare const motionOpLabel: (name: MotionOpName, target: string) => string;
|
|
36
|
+
export declare function motionOp(name: MotionOpName, target: string, opts?: MotionOpOpts): MotionOpResult;
|
package/dist/types/path.d.ts
CHANGED
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
* spacing ever overshoots.
|
|
10
10
|
*/
|
|
11
11
|
export type Pt = [number, number];
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Position on the spline at progress u in [0,1]. `curviness` scales the
|
|
14
|
+
* Catmull-Rom tangents (GSAP's idea): 1 = standard smooth (the default and the
|
|
15
|
+
* byte-exact original), 0 = straight lines / sharp corners, >1 = looser/loopier.
|
|
16
|
+
*/
|
|
17
|
+
export declare function pathPoint(points: Pt[], closed: boolean, u: number, curviness?: number): Pt;
|
|
14
18
|
/** Tangent angle (degrees) at progress u — the direction of travel along the path. */
|
|
15
|
-
export declare function pathTangentAngle(points: Pt[], closed: boolean, u: number): number;
|
|
19
|
+
export declare function pathTangentAngle(points: Pt[], closed: boolean, u: number, curviness?: number): number;
|
package/dist/types/validate.d.ts
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
* feedback loop for LLM-generated scenes, so they name the exact location
|
|
4
4
|
* and suggest what valid input looks like.
|
|
5
5
|
*/
|
|
6
|
-
import type { NodeIR, SceneIR } from "./ir.js";
|
|
6
|
+
import type { CompositionIR, NodeIR, SceneIR } from "./ir.js";
|
|
7
7
|
export declare const PROPS_BY_TYPE: Record<NodeIR["type"], string[]>;
|
|
8
8
|
export declare class SceneValidationError extends Error {
|
|
9
9
|
problems: string[];
|
|
10
10
|
constructor(problems: string[]);
|
|
11
11
|
}
|
|
12
12
|
export declare function validateScene(ir: SceneIR): void;
|
|
13
|
+
/** Validate a composition: each scene is valid, scene ids are unique, transitions
|
|
14
|
+
* are known, and `at` strings parse. Throws SceneValidationError on any problem. */
|
|
15
|
+
export declare function validateComposition(comp: CompositionIR): void;
|
package/guides/edsl-guide.md
CHANGED
|
@@ -82,13 +82,14 @@ them with normal TS (`Object.fromEntries`, `.map`) for data-driven scenes.
|
|
|
82
82
|
- `to(stateName, opts)` — transition into a named state (see above).
|
|
83
83
|
- `tween(nodeId, { prop: value, ... }, { duration, ease })` — low-level escape hatch
|
|
84
84
|
for one node. Colors (`"#rrggbb"`) interpolate; numbers interpolate.
|
|
85
|
-
- `motionPath(nodeId, [[x,y], ...], { duration, ease, autoRotate?, rotateOffset?, closed? })`
|
|
85
|
+
- `motionPath(nodeId, [[x,y], ...], { duration, ease, curviness?, autoRotate?, rotateOffset?, closed? })`
|
|
86
86
|
— drive a node's `x`/`y` along a smooth Catmull-Rom curve through the waypoints
|
|
87
87
|
(parent-space coords). `autoRotate: true` banks the node along the path tangent
|
|
88
88
|
(`rotateOffset` degrees if the art faces "up", e.g. `-90`). The node HOLDS at the
|
|
89
89
|
final point after the path finishes (a positioning move, not a one-shot), so a
|
|
90
90
|
later `tween` can chain from there. Use it for swoops/arcs/orbits — straight
|
|
91
91
|
`tween`s on x and y can't curve. `closed: true` loops the waypoints (orbit).
|
|
92
|
+
`curviness` shapes the path: `1` smooth (default), `0` sharp corners, `>1` loopier.
|
|
92
93
|
- `wait(seconds)` — hold.
|
|
93
94
|
|
|
94
95
|
Eases: `linear`, `easeIn/Out/InOutQuad`, `easeIn/Out/InOutCubic`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reframe-video",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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/preview/index.html
CHANGED
|
@@ -11,8 +11,11 @@
|
|
|
11
11
|
#content { flex: 1; display: flex; min-height: 0; }
|
|
12
12
|
#stage-wrap { flex: 1; display: flex; align-items: center; justify-content: center; min-width: 0; padding: 16px; }
|
|
13
13
|
canvas { max-width: 100%; max-height: 100%; box-shadow: 0 4px 32px rgba(0,0,0,.5); }
|
|
14
|
-
#bar { display: flex; gap:
|
|
15
|
-
#scrub { flex: 1; }
|
|
14
|
+
#bar { display: flex; gap: 8px; align-items: center; padding: 12px 16px; background: #232329; }
|
|
15
|
+
#scrub-wrap { flex: 1; position: relative; display: flex; align-items: center; }
|
|
16
|
+
#scrub { width: 100%; }
|
|
17
|
+
#loop-band { position: absolute; top: 50%; transform: translateY(-50%); height: 6px; background: rgba(125,154,255,.35); border-radius: 3px; pointer-events: none; display: none; }
|
|
18
|
+
button.on { background: #3a4a86; border-color: #7d9aff; color: #fff; }
|
|
16
19
|
select, button, input[type=text], input[type=number] { background: #2e2e36; color: #ddd; border: 1px solid #444; border-radius: 4px; padding: 3px 8px; font: 12px system-ui; }
|
|
17
20
|
input[type=number] { width: 64px; }
|
|
18
21
|
input[type=color] { width: 40px; height: 22px; padding: 0; border: 1px solid #444; background: none; }
|
|
@@ -42,6 +45,42 @@
|
|
|
42
45
|
#report details { color: #8a8a96; }
|
|
43
46
|
#io { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
|
|
44
47
|
#overlay-name { width: 100%; margin-bottom: 6px; box-sizing: border-box; }
|
|
48
|
+
.beat-group { margin: 4px 0 2px; border-left: 2px solid #3a4a86; padding-left: 6px; }
|
|
49
|
+
.beat-lane { padding: 1px 4px; border-radius: 3px; cursor: pointer; color: #b9c2da; font-size: 12px; }
|
|
50
|
+
.beat-lane:hover { background: #2a2a32; }
|
|
51
|
+
.beat-lane.selected { background: #31313c; color: #fff; }
|
|
52
|
+
.beat-lane.missing { color: #ff7b72; cursor: default; }
|
|
53
|
+
.beat-markers { color: #7d9aff; font-size: 11px; margin-top: 3px; opacity: 0.85; }
|
|
54
|
+
/* bottom timeline: scene bands (composition) or top-level beat bands (one scene) */
|
|
55
|
+
#comp-timeline { display: none; padding: 8px 16px 10px; background: #1f1f25; border-top: 1px solid #333; }
|
|
56
|
+
#comp-timeline.on { display: block; }
|
|
57
|
+
#comp-timeline .ct-title { color: #8a8a96; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
|
|
58
|
+
.ct-bandrow { display: flex; align-items: flex-start; }
|
|
59
|
+
.ct-bandrow .tk-label { padding-top: 4px; }
|
|
60
|
+
#comp-track { position: relative; flex: 1; min-width: 0; background: #16161b; border-radius: 6px; }
|
|
61
|
+
.ct-scene { position: absolute; background: #2a2a32; border: 1px solid #3a3a44; border-radius: 5px; cursor: pointer; overflow: hidden; box-sizing: border-box; padding: 4px 8px; white-space: nowrap; }
|
|
62
|
+
.ct-scene:hover { border-color: #7d9aff; }
|
|
63
|
+
.ct-scene.active { background: #31313c; border-color: #7d9aff; color: #fff; }
|
|
64
|
+
.ct-scene.beat { border-style: dashed; }
|
|
65
|
+
.ct-scene .ct-range { color: #8a8a96; font-size: 10px; margin-left: 6px; font-variant-numeric: tabular-nums; }
|
|
66
|
+
#ct-playhead { position: absolute; top: -2px; bottom: -2px; width: 2px; background: #ff4d00; pointer-events: none; }
|
|
67
|
+
/* node tracks (dope sheet): each node a lane, its motion segments as bars */
|
|
68
|
+
.tk-toggle { background: none; border: 1px solid #3a3a44; color: #8a8a96; font-size: 11px; border-radius: 4px; padding: 2px 8px; cursor: pointer; margin-top: 8px; }
|
|
69
|
+
.tk-toggle:hover { border-color: #7d9aff; color: #ddd; }
|
|
70
|
+
#comp-tracks { position: relative; margin-top: 6px; max-height: 150px; overflow-y: auto; display: none; }
|
|
71
|
+
#comp-tracks.on { display: block; }
|
|
72
|
+
.tk-row { display: flex; align-items: center; height: 18px; }
|
|
73
|
+
.tk-label { width: 120px; flex: none; color: #99a; font-size: 11px; padding: 0 6px; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; box-sizing: border-box; }
|
|
74
|
+
.tk-label:hover { color: #fff; }
|
|
75
|
+
.tk-row.selected .tk-label { color: #fff; font-weight: 600; }
|
|
76
|
+
.tk-lane { position: relative; flex: 1; height: 12px; background: #16161b; border-radius: 3px; }
|
|
77
|
+
.tk-bar { position: absolute; top: 1px; height: 10px; background: #3a4a86; border-radius: 2px; cursor: pointer; min-width: 3px; box-sizing: border-box; }
|
|
78
|
+
.tk-bar:hover { background: #7d9aff; }
|
|
79
|
+
.tk-bar.path { background: #b06a2a; }
|
|
80
|
+
.tk-bar.group { background: #4a4a58; }
|
|
81
|
+
.tk-bar.group:hover { background: #5e5e70; }
|
|
82
|
+
.tk-row.selected .tk-bar { background: #7d9aff; }
|
|
83
|
+
#tk-playhead { position: absolute; top: 0; bottom: 0; width: 2px; background: #ff4d00; pointer-events: none; }
|
|
45
84
|
</style>
|
|
46
85
|
</head>
|
|
47
86
|
<body>
|
|
@@ -52,9 +91,23 @@
|
|
|
52
91
|
<div id="bar">
|
|
53
92
|
<select id="scene-select"></select>
|
|
54
93
|
<button id="play">play</button>
|
|
55
|
-
<
|
|
94
|
+
<button id="play-all" title="play the whole composition across scenes" style="display:none">play all</button>
|
|
95
|
+
<button id="mark-in" title="set loop start to current time">[</button>
|
|
96
|
+
<button id="loop" title="loop the in/out range">loop</button>
|
|
97
|
+
<button id="mark-out" title="set loop end to current time">]</button>
|
|
98
|
+
<select id="speed" title="playback speed">
|
|
99
|
+
<option value="0.25">0.25×</option>
|
|
100
|
+
<option value="0.5">0.5×</option>
|
|
101
|
+
<option value="1" selected>1×</option>
|
|
102
|
+
<option value="2">2×</option>
|
|
103
|
+
</select>
|
|
104
|
+
<div id="scrub-wrap">
|
|
105
|
+
<div id="loop-band"></div>
|
|
106
|
+
<input id="scrub" type="range" min="0" max="1" step="0.001" value="0" />
|
|
107
|
+
</div>
|
|
56
108
|
<span id="time">0.000 / 0.000</span>
|
|
57
109
|
</div>
|
|
110
|
+
<div id="comp-timeline"></div>
|
|
58
111
|
<script type="module" src="/src/main.ts"></script>
|
|
59
112
|
</body>
|
|
60
113
|
</html>
|