reframe-video 0.2.0 → 0.4.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.
@@ -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;
@@ -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;
@@ -7,6 +7,10 @@ export { compileScene, type CompiledScene, type PropertySegment, type LabelSpan,
7
7
  export { pathPoint, pathTangentAngle, type Pt } from "./path.js";
8
8
  export { motionPreset, PRESET_NAMES, type PresetName, type PresetRig, type PresetOpts } from "./presets.js";
9
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 { splitText, textIn, textLoop, textOut, textTypeCues, type SplitOpts, type Glyph, type TextBlock, type FontWeight, type TextInName, type TextLoopName, type TextOutName, type TextLoopOpts, type TextOutOpts, type TypeCueOpts, } from "./textFx.js";
10
14
  export { motionOp, motionOpLabel, MOTION_OPS, type MotionOpName, type MotionOpOpts, type MotionOpResult } from "./motionOps.js";
11
15
  export { resolveAudioPlan, resolveCompositionAudioPlan, SFX_DURATION, type AudioPlan, type ResolvedCue, } from "./audio.js";
12
16
  export { evaluate, sampleProp, nodeParentMatrix, type DisplayList, type DisplayOp, type Mat2D, type ClipRegion, type TextAlign, type TextBaseline, } from "./evaluate.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 {};
@@ -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;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Kinetic text — a deterministic per-glyph text splitter plus a library of
3
+ * seeded effect generators (entrance / sustained / exit). The text analog of
4
+ * `motionPreset` / `characterPreset`: `splitText()` lays a phrase out as
5
+ * center-anchored `text` nodes (advances measured from the real font, so layout
6
+ * matches the render), then `textIn` / `textLoop` / `textOut` animate the glyphs.
7
+ *
8
+ * const T = splitText("MOTION IS DATA", { id: "t", x: 960, y: 470, fontSize: 130 });
9
+ * // nodes: [...T.nodes]
10
+ * // timeline: seq(textIn("typewriter", T), wait(2), textOut("shatter", T, { seed: 3 }))
11
+ * // behaviors: textLoop("wave", T, { from: 1.6, until: 3.6 })
12
+ */
13
+ import { type BehaviorWindow } from "./dsl.js";
14
+ import type { AudioCueIR, BehaviorIR, NodeIR, TimelineIR } from "./ir.js";
15
+ export type FontWeight = 400 | 700 | 800;
16
+ export interface SplitOpts {
17
+ /** Id PREFIX → glyph ids `${id}-${i}`. */
18
+ id: string;
19
+ /** Anchor point of the line. */
20
+ x: number;
21
+ y: number;
22
+ fontSize: number;
23
+ fontWeight?: FontWeight;
24
+ fill?: string;
25
+ /** Extra px between glyphs (tracking). */
26
+ letterSpacing?: number;
27
+ /** Horizontal alignment about `x` (default "center"). */
28
+ align?: "left" | "center";
29
+ /** Animate per glyph or per word (default "glyph"). */
30
+ unit?: "glyph" | "word";
31
+ /** Starting opacity of the nodes (default 0, for entrances). */
32
+ opacity?: number;
33
+ }
34
+ export interface Glyph {
35
+ id: string;
36
+ /** The character (glyph unit) or word (word unit). */
37
+ ch: string;
38
+ /** Home centre (the laid-out resting position). */
39
+ x: number;
40
+ y: number;
41
+ /** This unit's advance width in px. */
42
+ advance: number;
43
+ /** Index in declaration order. */
44
+ i: number;
45
+ }
46
+ export interface TextBlock {
47
+ nodes: NodeIR[];
48
+ glyphs: Glyph[];
49
+ ids: string[];
50
+ /** Total laid-out width in px. */
51
+ width: number;
52
+ x: number;
53
+ y: number;
54
+ fontSize: number;
55
+ }
56
+ export declare function splitText(textStr: string, opts: SplitOpts): TextBlock;
57
+ interface FxOpts {
58
+ speed?: number;
59
+ energy?: number;
60
+ seed?: number;
61
+ stagger?: number;
62
+ label?: string;
63
+ }
64
+ export type TextInName = "typewriter" | "cascade" | "rise" | "bounce" | "assemble" | "decode";
65
+ export declare function textIn(name: TextInName, block: TextBlock, opts?: FxOpts): TimelineIR;
66
+ export type TextLoopName = "wave" | "shimmer" | "wobble" | "float";
67
+ export interface TextLoopOpts extends BehaviorWindow {
68
+ amplitude?: number;
69
+ frequency?: number;
70
+ /** phase offset per glyph (the travelling-wave speed). */
71
+ phaseStep?: number;
72
+ }
73
+ export declare function textLoop(name: TextLoopName, block: TextBlock, opts?: TextLoopOpts): BehaviorIR[];
74
+ export type TextOutName = "shatter" | "fly" | "dissolve" | "fall" | "collapse";
75
+ export interface TextOutOpts extends FxOpts {
76
+ /** direction for "fly" (default up). */
77
+ dir?: [number, number];
78
+ }
79
+ export declare function textOut(name: TextOutName, block: TextBlock, opts?: TextOutOpts): TimelineIR;
80
+ export interface TypeCueOpts {
81
+ /** the timeline label the typewriter `textIn` starts at. */
82
+ at: string | number;
83
+ /** seconds between keystrokes (match the textIn stagger / speed). */
84
+ interval?: number;
85
+ gain?: number;
86
+ /** offset of the first key from `at`. */
87
+ offset?: number;
88
+ }
89
+ /** Per-glyph CC0 keypress for `textIn("typewriter", …)`. */
90
+ export declare function textTypeCues(block: TextBlock, opts: TypeCueOpts): AudioCueIR[];
91
+ export {};
@@ -0,0 +1,3 @@
1
+ export declare const INTER_ADVANCE: Record<number, Record<string, number>>;
2
+ /** Average advance per weight — the fallback for glyphs outside the table. */
3
+ export declare const INTER_FALLBACK: Record<number, number>;
@@ -41,6 +41,12 @@ Factories return plain data. Every node needs a unique `id`.
41
41
  the art's centre (e.g. the viewBox centre) so `scale`/`rotation` happen about the
42
42
  middle. `d` is drawn in its own coords; `x`/`y` place that pivot. Classic logo
43
43
  reveal: a stroke path drawing on, then a fill path fading in over it.
44
+ **`d` is animatable (shape morph):** `tween(id, { d: otherShape }, …)` morphs
45
+ the path vertex-by-vertex (the Lottie-style shape tween) when both `d` strings
46
+ share the same command sequence and arg counts — author the two poses with the
47
+ same structure (e.g. both 4-cubic ovals). Arcs (`A`) can't morph (their 0/1
48
+ flags aren't interpolable) and incompatible shapes snap at the midpoint; build
49
+ morph targets from `M/L/C/Q/Z` only.
44
50
  - `image({ id, src, x, y, width, height, opacity?, rotation?, scale?, anchor? })` —
45
51
  `src` is a file path, absolute or relative to the scene file; drawn stretched
46
52
  to `width`×`height` (png/jpg/webp). `src` switches discretely (no crossfade) —
@@ -115,6 +121,84 @@ bound — e.g. a pulse only during the hold:
115
121
  `oscillate("title", "scale", { amplitude: 0.04, frequency: 1.2 }, { from: 1.5, until: 3.5 })`.
116
122
  Omit the window to run for the whole scene.
117
123
 
124
+ ## Character rig (skeleton, poses, IK)
125
+
126
+ A first-class, declarative character rig that **compiles to plain IR** (nested
127
+ `group` joints + bone paths) — the character analog of `devicePreset`. It needs
128
+ no new renderer concept, so overlays/preview/determinism all apply.
129
+
130
+ - `humanoid({ id, x, y, scale, opacity?, color?, fill?, glow? })` → a NodeIR: a
131
+ ready upright body. Joints (stable ids `${id}-${name}`): `chest`, `head`,
132
+ `armUpperL/armLowerL`, `armUpperR/armLowerR`, `legUpperL/legLowerL`,
133
+ `legUpperR/legLowerR`. Drop it in `nodes`.
134
+ - `rig(boneTree, opts)` → build your own skeleton. A `Bone` is
135
+ `{ name, at:[x,y], length?, width?, rotation?, shape?, children? }`. The joint
136
+ sits at the group origin; the bone extends **+Y at rotation 0**; a child's `at`
137
+ pivot is in the PARENT bone's local space (e.g. an elbow at `[0, upperLength]`).
138
+ Nested groups give forward kinematics — a child's rotation composes on its
139
+ parent's. Default bone = a bezier capsule (morphable); pass `shape` for custom art.
140
+ - A **pose** is `{ jointName: angleDeg }` (0 = bone points down). Animate it:
141
+ - `poseTo(id, pose, { duration, ease, stagger? })` → a timeline step (a `par`
142
+ of rotation tweens). Sequence poses for wave/jump/run.
143
+ - `rigPose(id, pose)` → a `states` fragment, to transition with `to(state, …)`.
144
+ - `ikReach(upper, lower, dx, dy, flip?)` → `[shoulderDeg, elbowDeg]` that place a
145
+ 2-bone limb's tip at `(dx,dy)` relative to its shoulder joint (law of cosines;
146
+ clamps when out of reach). Feed the two angles into a pose.
147
+ - Joint names are the **stable regen addresses** — never rename them across a
148
+ regen; each rig instance needs a distinct `id` (duplicates collide via scene
149
+ validation). Squash/stretch and expressions are per-bone `d` morphs (above),
150
+ composed on top of FK posing. Idle sway/breathing = `oscillate` on a joint.
151
+ - `figure(opts)` — a **dressed** character (the styled sibling of `humanoid`):
152
+ same skeleton, but coloured flat-design shapes. `style: "clean"` (corporate-flat
153
+ / undraw register, the default) or `"cute"` (mascot); `palette` knobs
154
+ (`skin`/`hair`/`top`/`pants`/`shoe`/`accent`) re-skin it — for `clean` the top
155
+ follows `accent`, so `figure({ palette: { accent: "#3B82F6" } })` recolours the
156
+ whole figure; `face: false` makes it faceless. It exposes the humanoid joint
157
+ ids, so `characterPreset` / `ikReach` drive it unchanged. Use it as the
158
+ supporting actor in a product promo (gesturing at a `devicePreset`), not the hero.
159
+ - `characterPreset(name, opts)` — a **seeded motion generator** for a `humanoid`
160
+ or `figure` rig (the character analog of `motionPreset`). Returns a composable `beat`;
161
+ drop it in the timeline: `seq(characterPreset("walk", { target: "hero", at:
162
+ [cx, cy], cycles: 4 }))`. Names: `walk`, `run`, `jump`, `dance`, `wave`,
163
+ `cheer`. Knobs: `target` (rig id), `energy` 0..1, `speed` (>0, divides
164
+ durations), `seed` (varies within the family), `cycles` (walk/run/dance),
165
+ `facing` (±1), `at: [x,y]` (the rig's scene position — needed for walk travel
166
+ & jump lift), `travel` (px/cycle, 0 = in place), `label` (unique beat name —
167
+ set it when the same preset is used more than once in a scene). Legs use
168
+ `ikReach`, arms FK; pure keyframes, so add continuous idle yourself with `oscillate`.
169
+
170
+ ## Kinetic text (split + effect presets)
171
+
172
+ reframe's `text` node renders a whole string as one node, so per-glyph effects
173
+ need the string split into per-character nodes. `splitText` does that once;
174
+ seeded effect generators animate the glyphs (the text analog of `motionPreset`).
175
+
176
+ - `splitText(text, { id, x, y, fontSize, fontWeight?, fill?, letterSpacing?,
177
+ align?, unit?, opacity? }) → TextBlock` — lays the phrase out as center-anchored
178
+ `text` nodes using **real Inter advance widths** (so layout matches the render).
179
+ Returns `{ nodes, glyphs, ids, width, ... }`; put `...block.nodes` in `nodes`.
180
+ Glyph ids are `${id}-${i}` (stable regen addresses). `unit: "word"` animates
181
+ whole words instead of letters; `opacity: 0` (default) starts hidden for entrances.
182
+ - `textIn(name, block, { speed?, energy?, seed?, stagger?, label? }) → TimelineIR`
183
+ (a `beat`) — entrance: `typewriter`, `cascade`, `rise`, `bounce`, `assemble`
184
+ (fly in from a seeded scatter), `decode` (scramble through random glyphs then lock).
185
+ - `textLoop(name, block, { from?, until?, ramp?, amplitude?, frequency?, phaseStep? })
186
+ → BehaviorIR[]` — sustained: `wave` (standing sine), `shimmer`, `wobble`, `float`.
187
+ Spread it into `behaviors`.
188
+ - `textOut(name, block, { …, dir? }) → TimelineIR` — exit: `shatter` (random
189
+ direction + spin + fade), `fly` (directional), `dissolve`, `fall`, `collapse`.
190
+ - `textTypeCues(block, { at, interval?, gain? }) → AudioCueIR[]` — per-glyph CC0
191
+ keypress for a typewriter entrance; spread into `audio.cues`.
192
+
193
+ ```ts
194
+ const T = splitText("MOTION IS DATA", { id: "t", x: 960, y: 470, fontSize: 130 });
195
+ // nodes: [...T.nodes]
196
+ // timeline: seq(textIn("cascade", T), wait(2), textOut("shatter", T, { seed: 3 }))
197
+ // behaviors: textLoop("wave", T, { from: 1.6, until: 3.6 })
198
+ ```
199
+ Every effect is seeded (same `seed` → identical) and pure keyframes. To time a
200
+ `textLoop` window, add up the `textIn` beat length (≈ `(n-1)·stagger + glyphDur`).
201
+
118
202
  ## Audio (optional)
119
203
 
120
204
  Label-anchored sound design — cues follow retiming and regeneration:
@@ -16,3 +16,14 @@ source):
16
16
  When the contract is broken anyway, `composeScene` skips the affected edits
17
17
  and reports them as orphans with the known-ids list — loud, diagnosable,
18
18
  never a silent drop and never a render failure.
19
+
20
+ ## Generated subtrees (devicePreset, rig/humanoid)
21
+
22
+ Generators emit nodes with deterministic ids under an instance prefix, and those
23
+ ids are stable addresses too. For `devicePreset(name,{id})` the screen/content
24
+ parts are `${id}-screen` / `${id}-content`. For `rig(...)` / `humanoid({id})`
25
+ each joint is `${id}-${jointName}` (e.g. `hero-armUpperR`) and its bone art is
26
+ `${id}-${jointName}-shape`. Across a regen, **keep the instance `id` and the
27
+ joint `name`s** for any character/device that survives the redesign — overlay
28
+ edits (a retimed wave, a nudged limb angle) reference those exact ids. Renaming a
29
+ joint orphans the edit, exactly like renaming a hand-authored node id.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reframe-video",
3
- "version": "0.2.0",
3
+ "version": "0.4.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",