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.
Files changed (42) hide show
  1. package/assets/sfx/LICENSE.md +1 -1
  2. package/assets/sfx/bong_001.ogg +0 -0
  3. package/assets/sfx/click_001.ogg +0 -0
  4. package/assets/sfx/confirmation_002.ogg +0 -0
  5. package/assets/sfx/confirmation_003.ogg +0 -0
  6. package/assets/sfx/confirmation_004.ogg +0 -0
  7. package/assets/sfx/glass_001.ogg +0 -0
  8. package/assets/sfx/maximize_001.ogg +0 -0
  9. package/assets/sfx/maximize_002.ogg +0 -0
  10. package/assets/sfx/maximize_005.ogg +0 -0
  11. package/assets/sfx/maximize_009.ogg +0 -0
  12. package/assets/sfx/open_001.ogg +0 -0
  13. package/assets/sfx/pluck_001.ogg +0 -0
  14. package/assets/sfx/pluck_002.ogg +0 -0
  15. package/assets/sfx/select_001.ogg +0 -0
  16. package/assets/sfx/select_002.ogg +0 -0
  17. package/assets/sfx/select_003.ogg +0 -0
  18. package/dist/bin.js +724 -131
  19. package/dist/browserEntry.js +130 -68
  20. package/dist/cli.js +445 -85
  21. package/dist/index.js +674 -86
  22. package/dist/labels.js +606 -0
  23. package/dist/renderer-canvas.js +15 -0
  24. package/dist/trace-cli.js +9 -9
  25. package/dist/types/audio.d.ts +9 -0
  26. package/dist/types/compile.d.ts +1 -0
  27. package/dist/types/compose.d.ts +18 -2
  28. package/dist/types/composeComposition.d.ts +27 -0
  29. package/dist/types/devicePreset.d.ts +65 -0
  30. package/dist/types/dsl.d.ts +12 -1
  31. package/dist/types/evaluate.d.ts +32 -0
  32. package/dist/types/index.d.ts +6 -3
  33. package/dist/types/ir.d.ts +68 -0
  34. package/dist/types/motionOps.d.ts +36 -0
  35. package/dist/types/path.d.ts +7 -3
  36. package/dist/types/validate.d.ts +4 -1
  37. package/guides/edsl-guide.md +2 -1
  38. package/package.json +1 -1
  39. package/preview/index.html +56 -3
  40. package/preview/src/main.ts +1132 -46
  41. package/preview/src/panel.ts +478 -8
  42. package/preview/src/store.ts +323 -6
@@ -25,6 +25,7 @@ export interface MotionDriver {
25
25
  ease?: Ease;
26
26
  points: [number, number][];
27
27
  closed: boolean;
28
+ curviness: number;
28
29
  autoRotate: boolean;
29
30
  rotateOffset: number;
30
31
  }
@@ -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;
@@ -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;
@@ -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 {};
@@ -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 { resolveAudioPlan, SFX_DURATION, type AudioPlan, type ResolvedCue, } from "./audio.js";
9
- export { evaluate, type DisplayList, type DisplayOp, type Mat2D, type TextAlign, type TextBaseline, } from "./evaluate.js";
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";
@@ -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;
@@ -9,7 +9,11 @@
9
9
  * spacing ever overshoots.
10
10
  */
11
11
  export type Pt = [number, number];
12
- /** Position on the spline at progress u in [0,1]. */
13
- export declare function pathPoint(points: Pt[], closed: boolean, u: number): Pt;
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;
@@ -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;
@@ -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.1.2",
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",
@@ -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: 12px; align-items: center; padding: 12px 16px; background: #232329; }
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
- <input id="scrub" type="range" min="0" max="1" step="0.001" value="0" />
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>