reframe-video 0.1.3 → 0.3.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 (50) hide show
  1. package/assets/sfx/LICENSE.md +2 -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/footstep_001.ogg +0 -0
  8. package/assets/sfx/footstep_002.ogg +0 -0
  9. package/assets/sfx/footstep_003.ogg +0 -0
  10. package/assets/sfx/glass_001.ogg +0 -0
  11. package/assets/sfx/maximize_001.ogg +0 -0
  12. package/assets/sfx/maximize_002.ogg +0 -0
  13. package/assets/sfx/maximize_005.ogg +0 -0
  14. package/assets/sfx/maximize_009.ogg +0 -0
  15. package/assets/sfx/open_001.ogg +0 -0
  16. package/assets/sfx/pluck_001.ogg +0 -0
  17. package/assets/sfx/pluck_002.ogg +0 -0
  18. package/assets/sfx/select_001.ogg +0 -0
  19. package/assets/sfx/select_002.ogg +0 -0
  20. package/assets/sfx/select_003.ogg +0 -0
  21. package/dist/bin.js +271 -49
  22. package/dist/browserEntry.js +179 -68
  23. package/dist/cli.js +445 -85
  24. package/dist/index.js +1187 -116
  25. package/dist/labels.js +606 -0
  26. package/dist/renderer-canvas.js +15 -0
  27. package/dist/trace-cli.js +9 -9
  28. package/dist/types/audio.d.ts +9 -0
  29. package/dist/types/characterPreset.d.ts +39 -0
  30. package/dist/types/compile.d.ts +1 -0
  31. package/dist/types/compose.d.ts +18 -2
  32. package/dist/types/composeComposition.d.ts +27 -0
  33. package/dist/types/devicePreset.d.ts +65 -0
  34. package/dist/types/dsl.d.ts +12 -1
  35. package/dist/types/evaluate.d.ts +32 -0
  36. package/dist/types/figure.d.ts +32 -0
  37. package/dist/types/index.d.ts +9 -3
  38. package/dist/types/interpolate.d.ts +3 -2
  39. package/dist/types/ir.d.ts +68 -0
  40. package/dist/types/motionOps.d.ts +36 -0
  41. package/dist/types/path.d.ts +7 -3
  42. package/dist/types/rig.d.ts +87 -0
  43. package/dist/types/validate.d.ts +4 -1
  44. package/guides/edsl-guide.md +54 -1
  45. package/guides/regen-contract.md +11 -0
  46. package/package.json +1 -1
  47. package/preview/index.html +56 -3
  48. package/preview/src/main.ts +1132 -46
  49. package/preview/src/panel.ts +478 -8
  50. package/preview/src/store.ts +323 -6
@@ -0,0 +1,39 @@
1
+ /**
2
+ * characterPreset — a SEEDED motion generator for the humanoid rig. The
3
+ * character analog of `motionPreset`: `characterPreset(name, opts)` returns a
4
+ * `beat` (a TimelineIR) that drives a `humanoid()` rig's joints through a named
5
+ * performance (walk/run/jump/dance/wave/cheer). Same `(name, knobs, seed)` →
6
+ * identical IR; a different `seed` varies it within the family.
7
+ *
8
+ * seq(characterPreset("walk", { target: "hero", at: [CX, BASE_Y], cycles: 4 }))
9
+ *
10
+ * Pure keyframe timeline (a beat can't hold behaviors): secondary motion is
11
+ * baked into poses; continuous idle stays the author's `oscillate`. Legs use the
12
+ * 2-bone `ikReach` solver (foot targets relative to the hip → natural knee bend);
13
+ * arms swing via FK. Assumes the `humanoid()` joint names.
14
+ */
15
+ import type { TimelineIR } from "./ir.js";
16
+ export declare const CHARACTER_PRESET_NAMES: readonly ["walk", "run", "jump", "dance", "wave", "cheer"];
17
+ export type CharacterPresetName = (typeof CHARACTER_PRESET_NAMES)[number];
18
+ export interface CharacterPresetOpts {
19
+ /** humanoid rig id — joints are `${target}-${name}`, outer group = `${target}`. */
20
+ target: string;
21
+ /** 0..1 — stride / swing / bounce / jump-height amplitude (default 0.5). */
22
+ energy?: number;
23
+ /** >0 — tempo; durations divide by it (default 1, min 0.25). */
24
+ speed?: number;
25
+ /** Deterministic within-family variation (default 0). */
26
+ seed?: number;
27
+ /** Repeats for cyclic motions walk/run/dance (default 4). */
28
+ cycles?: number;
29
+ /** 1 = faces/moves right (default 1). */
30
+ facing?: 1 | -1;
31
+ /** The rig's scene position — needed to translate the body (walk travel, jump lift). Default [0,0]. */
32
+ at?: [number, number];
33
+ /** px travelled per cycle for walk/run (default ~stride·2; 0 = walk in place). */
34
+ travel?: number;
35
+ /** Override the beat name (overlay address) — set this when the same preset is
36
+ * used more than once in a scene so the beat labels stay unique. */
37
+ label?: string;
38
+ }
39
+ export declare function characterPreset(name: CharacterPresetName, opts: CharacterPresetOpts): TimelineIR;
@@ -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 {};
@@ -0,0 +1,32 @@
1
+ /**
2
+ * figure() — a parametric DRESSED character, the sibling of `humanoid()`. Same
3
+ * skeleton geometry (so `characterPreset` / `ikReach` / overlays apply unchanged),
4
+ * but each bone carries a coloured, designed flat shape instead of a neon capsule.
5
+ * Two styles — `clean` (corporate-flat, undraw register: one accent + neutrals,
6
+ * minimal face, slim adult proportions) and `cute` (mascot: big head, full face).
7
+ * Palette knobs re-skin it in one line; shadows are derived so a single `accent`
8
+ * recolours the whole figure.
9
+ *
10
+ * figure({ id: "fig", style: "clean", palette: { accent: "#3B82F6" } })
11
+ * characterPreset("walk", { target: "fig", at: [x, y] }) // drives it
12
+ */
13
+ import type { NodeIR } from "./ir.js";
14
+ import { type RigOpts } from "./rig.js";
15
+ export type FigureStyle = "clean" | "cute";
16
+ export interface FigurePalette {
17
+ skin?: string;
18
+ hair?: string;
19
+ top?: string;
20
+ pants?: string;
21
+ shoe?: string;
22
+ accent?: string;
23
+ }
24
+ export interface FigureOpts extends RigOpts {
25
+ /** "clean" (corporate-flat, default) | "cute" (mascot). */
26
+ style?: FigureStyle;
27
+ /** Colour overrides merged onto the per-style defaults. */
28
+ palette?: FigurePalette;
29
+ /** Draw minimal facial features (default true). `false` = faceless (pure undraw). */
30
+ face?: boolean;
31
+ }
32
+ export declare function figure(opts?: FigureOpts): NodeIR;
@@ -1,12 +1,18 @@
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 { rig, rigPose, poseTo, ikReach, humanoid, ovalPath, type Bone, type RigOpts, type Pose, type HumanoidOpts } from "./rig.js";
11
+ export { characterPreset, CHARACTER_PRESET_NAMES, type CharacterPresetName, type CharacterPresetOpts } from "./characterPreset.js";
12
+ export { figure, type FigureStyle, type FigureOpts, type FigurePalette } from "./figure.js";
13
+ export { motionOp, motionOpLabel, MOTION_OPS, type MotionOpName, type MotionOpOpts, type MotionOpResult } from "./motionOps.js";
14
+ export { resolveAudioPlan, resolveCompositionAudioPlan, SFX_DURATION, type AudioPlan, type ResolvedCue, } from "./audio.js";
15
+ export { evaluate, sampleProp, nodeParentMatrix, type DisplayList, type DisplayOp, type Mat2D, type ClipRegion, type TextAlign, type TextBaseline, } from "./evaluate.js";
10
16
  export { resolveEase, lerpValue, isColor, EASE_NAMES } from "./interpolate.js";
11
17
  export { sampleBehavior } from "./behaviors.js";
12
18
  export { collectImageSrcs } from "./assets.js";
@@ -5,8 +5,9 @@ export declare function resolveEase(ease: Ease | undefined): EaseFn;
5
5
  export declare function isColor(v: PropValue): v is string;
6
6
  /**
7
7
  * Interpolate two prop values at progress u (already eased).
8
- * number↔number lerps, color↔color lerps in RGB, anything else switches
9
- * discretely at the start of the segment.
8
+ * number↔number lerps, color↔color lerps in RGB, two *compatible* SVG path
9
+ * `d` strings morph vertex-by-vertex (the Lottie-style shape tween), anything
10
+ * else switches discretely.
10
11
  */
11
12
  export declare function lerpValue(from: PropValue, to: PropValue, u: number): PropValue;
12
13
  export {};
@@ -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;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Character rig: a first-class, declarative skeleton that COMPILES to plain IR.
3
+ * The character analog of `devicePreset` (generates a NodeIR subtree) and
4
+ * `motionPreset` (motion vocabulary). A `Bone` tree → nested `group` joints with
5
+ * stable ids, each holding the bone's vector art; posing is forward-kinematics
6
+ * (tween a joint group's `rotation`). Because it lowers to groups + paths, it
7
+ * inherits the renderer, evaluate, overlay editing, preview, validation and the
8
+ * determinism/golden contract for free — purely additive.
9
+ *
10
+ * const body = humanoid({ id: "hero" }); // one-call skeleton
11
+ * poseTo("hero", { armUpperR: -150 }, { duration: 0.4 }); // wave
12
+ *
13
+ * Bone convention: the joint sits at the group origin (0,0); the bone extends
14
+ * along +Y at rotation 0. A child joint's `at` pivot is in the PARENT bone's
15
+ * local space (e.g. an elbow at [0, upperLength]). Joint names are the STABLE
16
+ * regen addresses — id `${id}-${name}`; never rename them across a regen. Each
17
+ * rig instance needs a distinct `id` (duplicate joints collide via the scene's
18
+ * duplicate-id validation, exactly like `devicePreset`).
19
+ */
20
+ import type { Ease, NodeIR, TimelineIR } from "./ir.js";
21
+ export interface Bone {
22
+ /** Stable joint name → group id `${id}-${name}`. */
23
+ name: string;
24
+ /** Joint pivot, in the PARENT bone's local space (root: usually [0,0]). */
25
+ at: [number, number];
26
+ /** Bone length along +Y — drives the default capsule shape and IK. */
27
+ length?: number;
28
+ /** Default-capsule width (default 20). */
29
+ width?: number;
30
+ /** Rest-pose angle (deg); 0 = points down (+Y). */
31
+ rotation?: number;
32
+ /** Custom bone art (joint at origin, extends +Y) — overrides the default capsule. */
33
+ shape?: NodeIR[];
34
+ children?: Bone[];
35
+ }
36
+ export interface RigOpts {
37
+ /** Id PREFIX for the outer group and every joint (default "rig"); unique per instance. */
38
+ id?: string;
39
+ /** Root placement (default 0,0). */
40
+ x?: number;
41
+ y?: number;
42
+ /** Uniform scale (default 1). */
43
+ scale?: number;
44
+ /** Outer-group opacity (default 1) — start hidden for an entrance. */
45
+ opacity?: number;
46
+ /** Bone line colour (default warm). */
47
+ color?: string;
48
+ /** Bone fill (default near-bg, so overlapping joints occlude cleanly). */
49
+ fill?: string;
50
+ /** Glow accent for the double-path look on default bones (default off). */
51
+ glow?: string | false;
52
+ }
53
+ /** jointName → angle (deg). */
54
+ export type Pose = Record<string, number>;
55
+ /** A 4-cubic oval (morphable) centred at (cx,cy). Handy for heads/torsos/hands. */
56
+ export declare function ovalPath(a: number, b: number, cx?: number, cy?: number): string;
57
+ /** Compile a skeleton to a NodeIR group tree. Outer group id = `${id}`; each
58
+ * joint = `${id}-${name}` (the stable pose/overlay address). */
59
+ export declare function rig(root: Bone, opts?: RigOpts): NodeIR;
60
+ /** A pose as a `states` fragment: `{ "${id}-${joint}": { rotation } }`. Merge
61
+ * into scene `states` and transition with the existing `to(state, …)`. */
62
+ export declare function rigPose(id: string, pose: Pose): Record<string, {
63
+ rotation: number;
64
+ }>;
65
+ /** Pose-to-pose on the timeline: a `par` (or `stagger`) of rotation tweens. */
66
+ export declare function poseTo(id: string, pose: Pose, opts?: {
67
+ duration?: number;
68
+ ease?: Ease;
69
+ stagger?: number;
70
+ }): TimelineIR;
71
+ /**
72
+ * 2-bone inverse kinematics. Returns `[shoulderDeg, elbowDeg]` (the +Y-down bone
73
+ * convention) that place the chain's tip at `(dx,dy)` relative to the root joint.
74
+ * Exact for in-reach targets; clamps gracefully (no NaN) when out of reach.
75
+ * `flip` chooses the elbow-up vs elbow-down solution.
76
+ *
77
+ * Derivation: with R(θ) the canvas rotation, the tip is
78
+ * R(θ1)·[ (0,upper) + R(θ2)·(0,lower) ]. The bracket has length D=hypot(dx,dy),
79
+ * giving cosθ2 = (D²−u²−l²)/(2ul); then θ1 rotates that bracket onto the target.
80
+ */
81
+ export declare function ikReach(upper: number, lower: number, dx: number, dy: number, flip?: boolean): [number, number];
82
+ export interface HumanoidOpts extends Omit<RigOpts, never> {
83
+ }
84
+ /** A ready upright humanoid skeleton — the one-call body. Joints:
85
+ * chest, head, armUpper/LowerL, armUpper/LowerR, legUpper/LowerL, legUpper/LowerR.
86
+ * Rooted at the chest so every limb extends naturally along +Y. */
87
+ export declare function humanoid(opts?: HumanoidOpts): NodeIR;
@@ -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;