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
@@ -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) —
@@ -82,13 +88,14 @@ them with normal TS (`Object.fromEntries`, `.map`) for data-driven scenes.
82
88
  - `to(stateName, opts)` — transition into a named state (see above).
83
89
  - `tween(nodeId, { prop: value, ... }, { duration, ease })` — low-level escape hatch
84
90
  for one node. Colors (`"#rrggbb"`) interpolate; numbers interpolate.
85
- - `motionPath(nodeId, [[x,y], ...], { duration, ease, autoRotate?, rotateOffset?, closed? })`
91
+ - `motionPath(nodeId, [[x,y], ...], { duration, ease, curviness?, autoRotate?, rotateOffset?, closed? })`
86
92
  — drive a node's `x`/`y` along a smooth Catmull-Rom curve through the waypoints
87
93
  (parent-space coords). `autoRotate: true` banks the node along the path tangent
88
94
  (`rotateOffset` degrees if the art faces "up", e.g. `-90`). The node HOLDS at the
89
95
  final point after the path finishes (a positioning move, not a one-shot), so a
90
96
  later `tween` can chain from there. Use it for swoops/arcs/orbits — straight
91
97
  `tween`s on x and y can't curve. `closed: true` loops the waypoints (orbit).
98
+ `curviness` shapes the path: `1` smooth (default), `0` sharp corners, `>1` loopier.
92
99
  - `wait(seconds)` — hold.
93
100
 
94
101
  Eases: `linear`, `easeIn/Out/InOutQuad`, `easeIn/Out/InOutCubic`,
@@ -114,6 +121,52 @@ bound — e.g. a pulse only during the hold:
114
121
  `oscillate("title", "scale", { amplitude: 0.04, frequency: 1.2 }, { from: 1.5, until: 3.5 })`.
115
122
  Omit the window to run for the whole scene.
116
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
+
117
170
  ## Audio (optional)
118
171
 
119
172
  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.1.3",
3
+ "version": "0.3.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>