reframe-video 0.4.0 → 0.5.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/dist/bin.js +9 -0
- package/dist/index.js +84 -6
- package/dist/types/cursor.d.ts +57 -0
- package/dist/types/devicePreset.d.ts +4 -0
- package/dist/types/index.d.ts +2 -1
- package/guides/edsl-guide.md +27 -0
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1044,6 +1044,14 @@ var init_devicePreset = __esm({
|
|
|
1044
1044
|
}
|
|
1045
1045
|
});
|
|
1046
1046
|
|
|
1047
|
+
// ../core/src/cursor.ts
|
|
1048
|
+
var init_cursor = __esm({
|
|
1049
|
+
"../core/src/cursor.ts"() {
|
|
1050
|
+
"use strict";
|
|
1051
|
+
init_dsl();
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1047
1055
|
// ../core/src/rig.ts
|
|
1048
1056
|
var init_rig = __esm({
|
|
1049
1057
|
"../core/src/rig.ts"() {
|
|
@@ -1304,6 +1312,7 @@ var init_src = __esm({
|
|
|
1304
1312
|
init_path();
|
|
1305
1313
|
init_presets();
|
|
1306
1314
|
init_devicePreset();
|
|
1315
|
+
init_cursor();
|
|
1307
1316
|
init_rig();
|
|
1308
1317
|
init_characterPreset();
|
|
1309
1318
|
init_figure();
|
package/dist/index.js
CHANGED
|
@@ -1089,6 +1089,11 @@ function deviceBounds(name, opts = {}) {
|
|
|
1089
1089
|
const b = BOUNDS[name];
|
|
1090
1090
|
return isLandscape(name, opts) ? { width: b.height, height: b.width } : { ...b };
|
|
1091
1091
|
}
|
|
1092
|
+
function deviceScreenPoint(name, opts, local) {
|
|
1093
|
+
const c = deviceScreenCenter(name, opts);
|
|
1094
|
+
const s = opts.scale ?? 1;
|
|
1095
|
+
return [(opts.x ?? 0) + s * (c.x + local[0]), (opts.y ?? 0) + s * (c.y + local[1])];
|
|
1096
|
+
}
|
|
1092
1097
|
function screenGroup(id, p, o, cx, cy, dims, content) {
|
|
1093
1098
|
return group({ id: `${id}-screen`, x: cx, y: cy, clip: { kind: "rect", x: -dims.width / 2, y: -dims.height / 2, width: dims.width, height: dims.height, radius: dims.radius } }, [
|
|
1094
1099
|
rect({ id: `${id}-screenbg`, x: 0, y: 0, anchor: "center", width: dims.width, height: dims.height, fill: o.screen ?? p.screen }),
|
|
@@ -1263,6 +1268,73 @@ function devicePreset(name, opts = {}) {
|
|
|
1263
1268
|
);
|
|
1264
1269
|
}
|
|
1265
1270
|
|
|
1271
|
+
// ../core/src/cursor.ts
|
|
1272
|
+
var ARROW_D = "M0 0 L0 30 L8 23 L12.6 33 L17 31 L12.4 21.4 L21 21.4 Z";
|
|
1273
|
+
function cursor(opts = {}) {
|
|
1274
|
+
const id = opts.id ?? "cursor";
|
|
1275
|
+
const style = opts.style ?? "arrow";
|
|
1276
|
+
const fill = opts.fill ?? "#FFFFFF";
|
|
1277
|
+
const accent = opts.accent ?? "#FF5A1F";
|
|
1278
|
+
const art = style === "arrow" ? [path({ id: `${id}-arrow`, d: ARROW_D, x: 0, y: 0, fill, stroke: "#15171E", strokeWidth: 2 })] : style === "dot" ? [ellipse({ id: `${id}-dot`, x: 0, y: 0, width: 18, height: 18, fill: accent, anchor: "center" })] : [ellipse({ id: `${id}-ring`, x: 0, y: 0, width: 22, height: 22, fill: "none", stroke: accent, strokeWidth: 3, anchor: "center" })];
|
|
1279
|
+
return group(
|
|
1280
|
+
{ id, x: opts.x ?? 0, y: opts.y ?? 0, scale: opts.scale ?? 1, opacity: opts.opacity ?? 1 },
|
|
1281
|
+
[
|
|
1282
|
+
// ripple ring (behind the pointer), emanates from the hotspot on click
|
|
1283
|
+
ellipse({ id: `${id}-ripple`, x: 0, y: 0, width: 30, height: 30, fill: "none", stroke: accent, strokeWidth: 3, opacity: 0, scale: 0, anchor: "center" }),
|
|
1284
|
+
// the pointer art lives in its own group so a click "tap" can scale it
|
|
1285
|
+
// independently of the cursor's resting scale
|
|
1286
|
+
group({ id: `${id}-art`, x: 0, y: 0 }, art)
|
|
1287
|
+
]
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
1291
|
+
function cursorTo(id, from, to2, opts = {}) {
|
|
1292
|
+
const dx = to2[0] - from[0], dy = to2[1] - from[1];
|
|
1293
|
+
const dist = Math.hypot(dx, dy) || 1;
|
|
1294
|
+
const arc = opts.arc ?? 0.12;
|
|
1295
|
+
const mid = [(from[0] + to2[0]) / 2 + -dy / dist * arc * dist, (from[1] + to2[1]) / 2 + dx / dist * arc * dist];
|
|
1296
|
+
const duration = opts.duration ?? clamp(dist / 1400, 0.4, 0.9);
|
|
1297
|
+
return motionPath(id, [from, mid, to2], { duration, ease: opts.ease ?? "easeInOutCubic", curviness: 1, ...opts.label && { label: opts.label } });
|
|
1298
|
+
}
|
|
1299
|
+
function cursorPath(id, points, opts = {}) {
|
|
1300
|
+
return motionPath(id, points, {
|
|
1301
|
+
duration: opts.duration ?? clamp(points.length * 0.5, 0.5, 4),
|
|
1302
|
+
ease: opts.ease ?? "easeInOutCubic",
|
|
1303
|
+
curviness: opts.curviness ?? 1,
|
|
1304
|
+
...opts.label && { label: opts.label }
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
function clickBody(id, o) {
|
|
1308
|
+
const sp = Math.max(0.25, o.speed ?? 1);
|
|
1309
|
+
const d = (b) => b / sp;
|
|
1310
|
+
const out = [
|
|
1311
|
+
// the pointer taps
|
|
1312
|
+
seq(tween(`${id}-art`, { scale: 0.82 }, { duration: d(0.08), ease: "easeOutQuad" }), tween(`${id}-art`, { scale: 1 }, { duration: d(0.1), ease: "easeOutBack" }))
|
|
1313
|
+
];
|
|
1314
|
+
if (o.ripple !== false) {
|
|
1315
|
+
out.push(seq(
|
|
1316
|
+
tween(`${id}-ripple`, { scale: 0.2, opacity: 0.55 }, { duration: 1e-3 }),
|
|
1317
|
+
par(
|
|
1318
|
+
tween(`${id}-ripple`, { scale: 5 }, { duration: d(0.5), ease: "easeOutCubic" }),
|
|
1319
|
+
tween(`${id}-ripple`, { opacity: 0 }, { duration: d(0.5), ease: "easeOutQuad" })
|
|
1320
|
+
)
|
|
1321
|
+
));
|
|
1322
|
+
}
|
|
1323
|
+
if (o.press) {
|
|
1324
|
+
out.push(seq(tween(o.press, { scale: 0.94 }, { duration: d(0.08), ease: "easeOutQuad" }), tween(o.press, { scale: 1 }, { duration: d(0.14), ease: "easeOutBack" })));
|
|
1325
|
+
}
|
|
1326
|
+
return out;
|
|
1327
|
+
}
|
|
1328
|
+
function cursorClick(id, opts = {}) {
|
|
1329
|
+
return beat(opts.label ?? "cursor-click", {}, [par(...clickBody(id, opts))]);
|
|
1330
|
+
}
|
|
1331
|
+
function cursorDouble(id, opts = {}) {
|
|
1332
|
+
const sp = Math.max(0.25, opts.speed ?? 1);
|
|
1333
|
+
return beat(opts.label ?? "cursor-double", {}, [
|
|
1334
|
+
seq(par(...clickBody(id, { ...opts, ripple: false })), wait(0.12 / sp), par(...clickBody(id, opts)))
|
|
1335
|
+
]);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1266
1338
|
// ../core/src/rig.ts
|
|
1267
1339
|
var DEFAULT_LINE = "#FFE3D2";
|
|
1268
1340
|
var DEFAULT_FILL = "#0E1424";
|
|
@@ -2008,7 +2080,7 @@ function splitText(textStr, opts) {
|
|
|
2008
2080
|
chars.forEach((ch, i) => {
|
|
2009
2081
|
total += advance(ch, weight, fontSize) + (i < chars.length - 1 ? ls : 0);
|
|
2010
2082
|
});
|
|
2011
|
-
let
|
|
2083
|
+
let cursor2 = align === "center" ? x - total / 2 : x;
|
|
2012
2084
|
const glyphs = [];
|
|
2013
2085
|
const nodes = [];
|
|
2014
2086
|
const mk = (ch, cx, adv, lsProp) => {
|
|
@@ -2034,13 +2106,13 @@ function splitText(textStr, opts) {
|
|
|
2034
2106
|
let i = 0;
|
|
2035
2107
|
while (i < chars.length) {
|
|
2036
2108
|
if (chars[i] === " ") {
|
|
2037
|
-
|
|
2109
|
+
cursor2 += advance(" ", weight, fontSize) + ls;
|
|
2038
2110
|
i++;
|
|
2039
2111
|
continue;
|
|
2040
2112
|
}
|
|
2041
2113
|
let word = "";
|
|
2042
2114
|
let w = 0;
|
|
2043
|
-
const startCursor =
|
|
2115
|
+
const startCursor = cursor2;
|
|
2044
2116
|
while (i < chars.length && chars[i] !== " ") {
|
|
2045
2117
|
const a = advance(chars[i], weight, fontSize);
|
|
2046
2118
|
word += chars[i];
|
|
@@ -2048,13 +2120,13 @@ function splitText(textStr, opts) {
|
|
|
2048
2120
|
i++;
|
|
2049
2121
|
}
|
|
2050
2122
|
mk(word, startCursor + w / 2, w, ls);
|
|
2051
|
-
|
|
2123
|
+
cursor2 = startCursor + w + ls;
|
|
2052
2124
|
}
|
|
2053
2125
|
} else {
|
|
2054
2126
|
chars.forEach((ch) => {
|
|
2055
2127
|
const a = advance(ch, weight, fontSize);
|
|
2056
|
-
if (ch !== " ") mk(ch,
|
|
2057
|
-
|
|
2128
|
+
if (ch !== " ") mk(ch, cursor2 + a / 2, a);
|
|
2129
|
+
cursor2 += a + ls;
|
|
2058
2130
|
});
|
|
2059
2131
|
}
|
|
2060
2132
|
return { nodes, glyphs, ids: glyphs.map((g) => g.id), width: total, x, y, fontSize };
|
|
@@ -2963,10 +3035,16 @@ export {
|
|
|
2963
3035
|
compileScene,
|
|
2964
3036
|
composeScene,
|
|
2965
3037
|
composition,
|
|
3038
|
+
cursor,
|
|
3039
|
+
cursorClick,
|
|
3040
|
+
cursorDouble,
|
|
3041
|
+
cursorPath,
|
|
3042
|
+
cursorTo,
|
|
2966
3043
|
deviceBounds,
|
|
2967
3044
|
devicePreset,
|
|
2968
3045
|
deviceScreen,
|
|
2969
3046
|
deviceScreenCenter,
|
|
3047
|
+
deviceScreenPoint,
|
|
2970
3048
|
ellipse,
|
|
2971
3049
|
evaluate,
|
|
2972
3050
|
figure,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor / pointer motion — a vector mouse pointer that glides across the scene
|
|
3
|
+
* and clicks things (the UI-demo staple). `cursor()` returns a NodeIR (like
|
|
4
|
+
* `devicePreset`); `cursorTo` / `cursorPath` / `cursorClick` return TimelineIR
|
|
5
|
+
* (like `characterPreset`). The pointer's HOTSPOT is the group origin (0,0), so a
|
|
6
|
+
* move lands the tip exactly on a target. Pairs with `deviceScreenPoint` to click
|
|
7
|
+
* UI inside a `devicePreset` screen.
|
|
8
|
+
*
|
|
9
|
+
* nodes: [devicePreset("browser", { id: "d", x, y, scale, content }), cursor({ id: "cur" })]
|
|
10
|
+
* timeline: seq(cursorTo("cur", [start], deviceScreenPoint("browser", dOpts, [lx, ly])),
|
|
11
|
+
* cursorClick("cur", { press: "d-ui-cta" }))
|
|
12
|
+
*/
|
|
13
|
+
import type { Ease, NodeIR, TimelineIR } from "./ir.js";
|
|
14
|
+
export type CursorStyle = "arrow" | "dot" | "ring";
|
|
15
|
+
export interface CursorOpts {
|
|
16
|
+
id?: string;
|
|
17
|
+
x?: number;
|
|
18
|
+
y?: number;
|
|
19
|
+
scale?: number;
|
|
20
|
+
opacity?: number;
|
|
21
|
+
style?: CursorStyle;
|
|
22
|
+
/** Pointer body colour (default white for arrow). */
|
|
23
|
+
fill?: string;
|
|
24
|
+
/** Accent for dot/ring body and the click ripple. */
|
|
25
|
+
accent?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function cursor(opts?: CursorOpts): NodeIR;
|
|
28
|
+
export interface CursorToOpts {
|
|
29
|
+
duration?: number;
|
|
30
|
+
ease?: Ease;
|
|
31
|
+
/** perpendicular bow as a fraction of distance (default 0.12; 0 = straight). */
|
|
32
|
+
arc?: number;
|
|
33
|
+
label?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Glide the cursor from `from` to `to` along a gentle human arc. */
|
|
36
|
+
export declare function cursorTo(id: string, from: [number, number], to: [number, number], opts?: CursorToOpts): TimelineIR;
|
|
37
|
+
export interface CursorPathOpts {
|
|
38
|
+
duration?: number;
|
|
39
|
+
ease?: Ease;
|
|
40
|
+
curviness?: number;
|
|
41
|
+
label?: string;
|
|
42
|
+
}
|
|
43
|
+
/** Move the cursor through a tour of waypoints (one smooth path). */
|
|
44
|
+
export declare function cursorPath(id: string, points: [number, number][], opts?: CursorPathOpts): TimelineIR;
|
|
45
|
+
export interface CursorClickOpts {
|
|
46
|
+
/** overall click duration scale (default 1). */
|
|
47
|
+
speed?: number;
|
|
48
|
+
/** node id to "press" (a quick scale dip) when the cursor clicks it. */
|
|
49
|
+
press?: string;
|
|
50
|
+
/** show the expanding ripple ring (default true). */
|
|
51
|
+
ripple?: boolean;
|
|
52
|
+
label?: string;
|
|
53
|
+
}
|
|
54
|
+
/** A click: the pointer taps, a ripple ring expands, and an optional target presses. */
|
|
55
|
+
export declare function cursorClick(id: string, opts?: CursorClickOpts): TimelineIR;
|
|
56
|
+
/** Two quick clicks. */
|
|
57
|
+
export declare function cursorDouble(id: string, opts?: CursorClickOpts): TimelineIR;
|
|
@@ -61,5 +61,9 @@ export declare function deviceBounds(name: DevicePresetName, opts?: DevicePreset
|
|
|
61
61
|
width: number;
|
|
62
62
|
height: number;
|
|
63
63
|
};
|
|
64
|
+
/** Map a SCREEN-LOCAL point (origin = screen centre, the coords `content` is
|
|
65
|
+
* authored in) to absolute SCENE coords, given the same `opts` passed to
|
|
66
|
+
* `devicePreset`. For aiming a `cursor` at on-screen UI. */
|
|
67
|
+
export declare function deviceScreenPoint(name: DevicePresetName, opts: DevicePresetOpts, local: [number, number]): [number, number];
|
|
64
68
|
/** Build a device-mockup frame (a group) with a clipped screen content slot. */
|
|
65
69
|
export declare function devicePreset(name: DevicePresetName, opts?: DevicePresetOpts): NodeIR;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ export { composeScene, formatComposeReport, type OverlayDoc, type ComposeReport,
|
|
|
6
6
|
export { compileScene, type CompiledScene, type PropertySegment, type LabelSpan, type MotionDriver } from "./compile.js";
|
|
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
|
-
export { devicePreset, deviceScreen, deviceScreenCenter, deviceBounds, DEVICE_PRESET_NAMES, type DevicePresetName, type DevicePresetOpts } from "./devicePreset.js";
|
|
9
|
+
export { devicePreset, deviceScreen, deviceScreenCenter, deviceBounds, deviceScreenPoint, DEVICE_PRESET_NAMES, type DevicePresetName, type DevicePresetOpts } from "./devicePreset.js";
|
|
10
|
+
export { cursor, cursorTo, cursorPath, cursorClick, cursorDouble, type CursorStyle, type CursorOpts, type CursorToOpts, type CursorPathOpts, type CursorClickOpts } from "./cursor.js";
|
|
10
11
|
export { rig, rigPose, poseTo, ikReach, humanoid, ovalPath, type Bone, type RigOpts, type Pose, type HumanoidOpts } from "./rig.js";
|
|
11
12
|
export { characterPreset, CHARACTER_PRESET_NAMES, type CharacterPresetName, type CharacterPresetOpts } from "./characterPreset.js";
|
|
12
13
|
export { figure, type FigureStyle, type FigureOpts, type FigurePalette } from "./figure.js";
|
package/guides/edsl-guide.md
CHANGED
|
@@ -199,6 +199,33 @@ const T = splitText("MOTION IS DATA", { id: "t", x: 960, y: 470, fontSize: 130 }
|
|
|
199
199
|
Every effect is seeded (same `seed` → identical) and pure keyframes. To time a
|
|
200
200
|
`textLoop` window, add up the `textIn` beat length (≈ `(n-1)·stagger + glyphDur`).
|
|
201
201
|
|
|
202
|
+
## Cursor (UI demos)
|
|
203
|
+
|
|
204
|
+
A vector mouse pointer that glides across the scene and clicks things — for app
|
|
205
|
+
walkthroughs. `cursor()` returns a node; the moves/clicks return timeline steps.
|
|
206
|
+
The pointer's **hotspot is the group origin**, so a move lands the tip on a target.
|
|
207
|
+
|
|
208
|
+
- `cursor({ id, x, y, scale?, opacity?, style?, accent? }) → NodeIR` — styles
|
|
209
|
+
`arrow` (default), `dot`, `ring`. Draw it LAST so it sits on top. Carries a
|
|
210
|
+
hidden `${id}-ripple` ring for clicks.
|
|
211
|
+
- `cursorTo(id, from, to, { duration?, ease?, arc? }) → TimelineIR` — glide along
|
|
212
|
+
a gentle human arc (`arc` is the bow, default 0.12). Thread the position: start
|
|
213
|
+
= the node's `x/y`, each `to` becomes the next `from`.
|
|
214
|
+
- `cursorPath(id, points, opts)` — a multi-stop tour through waypoints.
|
|
215
|
+
- `cursorClick(id, { press?, ripple?, label? })` / `cursorDouble(...)` — the
|
|
216
|
+
pointer taps, a ripple ring expands, and the `press` node (a button) dips. Pass
|
|
217
|
+
a unique `label` when you click more than once in a scene.
|
|
218
|
+
- `deviceScreenPoint(name, deviceOpts, [lx, ly]) → [x, y]` — map a UI element's
|
|
219
|
+
screen-local coords (the coords `devicePreset` `content` is authored in) to
|
|
220
|
+
scene coords, so the cursor clicks on-screen UI precisely (account for the
|
|
221
|
+
device's `scale` at click time and any `slot` offset).
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
// nodes: devicePreset("browser", { id:"d", x, y, scale:0.88, content }), cursor({ id:"cur" })
|
|
225
|
+
const cta = deviceScreenPoint("browser", { x, y, scale: 0.88 }, [lx, ly]);
|
|
226
|
+
seq(cursorTo("cur", [sx, sy], cta), cursorClick("cur", { press: "browser-ui-cta" }))
|
|
227
|
+
```
|
|
228
|
+
|
|
202
229
|
## Audio (optional)
|
|
203
230
|
|
|
204
231
|
Label-anchored sound design — cues follow retiming and regeneration:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reframe-video",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|