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.
- package/assets/sfx/LICENSE.md +1 -0
- package/assets/sfx/footstep_001.ogg +0 -0
- package/assets/sfx/footstep_002.ogg +0 -0
- package/assets/sfx/footstep_003.ogg +0 -0
- package/dist/bin.js +48 -2
- package/dist/browserEntry.js +49 -0
- package/dist/index.js +1027 -37
- package/dist/types/characterPreset.d.ts +39 -0
- package/dist/types/figure.d.ts +32 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/interpolate.d.ts +3 -2
- package/dist/types/rig.d.ts +87 -0
- package/dist/types/textFx.d.ts +91 -0
- package/dist/types/textMetrics.d.ts +3 -0
- package/guides/edsl-guide.md +84 -0
- package/guides/regen-contract.md +11 -0
- package/package.json +1 -1
|
@@ -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;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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,
|
|
9
|
-
*
|
|
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 {};
|
package/guides/edsl-guide.md
CHANGED
|
@@ -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:
|
package/guides/regen-contract.md
CHANGED
|
@@ -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.
|
|
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",
|