reframe-video 0.1.1 → 0.1.2
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/analyze.js +44 -1
- package/dist/bin.js +262 -7
- package/dist/browserEntry.js +259 -3
- package/dist/cli.js +222 -5
- package/dist/index.js +510 -16
- package/dist/renderer-canvas.js +36 -0
- package/dist/trace-cli.js +677 -0
- package/dist/types/compile.d.ts +15 -1
- package/dist/types/compose.d.ts +12 -2
- package/dist/types/dsl.d.ts +27 -1
- package/dist/types/evaluate.d.ts +9 -0
- package/dist/types/index.d.ts +4 -1
- package/dist/types/ir.d.ts +67 -1
- package/dist/types/motion.d.ts +54 -0
- package/dist/types/path.d.ts +15 -0
- package/dist/types/presets.d.ts +41 -0
- package/guides/edsl-guide.md +19 -0
- package/package.json +1 -1
- package/preview/src/main.ts +62 -0
- package/preview/src/panel.ts +37 -3
- package/preview/src/store.ts +28 -2
package/dist/types/compile.d.ts
CHANGED
|
@@ -18,6 +18,16 @@ export interface PropertySegment {
|
|
|
18
18
|
to: PropValue;
|
|
19
19
|
ease?: Ease;
|
|
20
20
|
}
|
|
21
|
+
/** A path driver overrides a node's x/y (and rotation, if autoRotate) over [t0, t1], holding the end. */
|
|
22
|
+
export interface MotionDriver {
|
|
23
|
+
t0: number;
|
|
24
|
+
t1: number;
|
|
25
|
+
ease?: Ease;
|
|
26
|
+
points: [number, number][];
|
|
27
|
+
closed: boolean;
|
|
28
|
+
autoRotate: boolean;
|
|
29
|
+
rotateOffset: number;
|
|
30
|
+
}
|
|
21
31
|
export interface LabelSpan {
|
|
22
32
|
t0: number;
|
|
23
33
|
t1: number;
|
|
@@ -27,12 +37,16 @@ export interface CompiledScene {
|
|
|
27
37
|
duration: number;
|
|
28
38
|
/** Keyed by `${target}.${prop}`, sorted by t0. */
|
|
29
39
|
segments: Map<string, PropertySegment[]>;
|
|
40
|
+
/** Path drivers per target node, sorted by t0 — override x/y/rotation. */
|
|
41
|
+
motionPaths: Map<string, MotionDriver[]>;
|
|
30
42
|
/** Base props merged with the initial state, keyed by `${target}.${prop}`. */
|
|
31
43
|
initialValues: Map<string, PropValue>;
|
|
32
44
|
nodeById: Map<string, NodeIR>;
|
|
33
45
|
/** Declaration order — defines stagger order. */
|
|
34
46
|
nodeOrder: string[];
|
|
35
|
-
/** Absolute [start, end] of every labeled timeline step. */
|
|
47
|
+
/** Absolute [start, end] of every labeled timeline step (beat names included). */
|
|
36
48
|
labelTimes: Map<string, LabelSpan>;
|
|
49
|
+
/** The subset of label spans that come from beat nodes — keyed by beat name. */
|
|
50
|
+
beatTimes: Map<string, LabelSpan>;
|
|
37
51
|
}
|
|
38
52
|
export declare function compileScene(ir: SceneIR): CompiledScene;
|
package/dist/types/compose.d.ts
CHANGED
|
@@ -35,13 +35,23 @@ export interface OverlayDoc {
|
|
|
35
35
|
/** Complete nodes appended at the scene root, owned by this overlay. */
|
|
36
36
|
addNodes?: NodeIR[];
|
|
37
37
|
/**
|
|
38
|
-
* Parameter patches on labeled timeline steps. Patchable
|
|
39
|
-
* to -> duration/ease/stagger, tween -> duration/ease,
|
|
38
|
+
* Parameter patches on labeled timeline steps (or beats by name). Patchable
|
|
39
|
+
* per kind: to -> duration/ease/stagger, tween -> duration/ease,
|
|
40
|
+
* wait -> duration, motionPath -> points/duration/ease, beat ->
|
|
41
|
+
* at/gap/scale/duration/order. A beat move is rigid, so child labels inside
|
|
42
|
+
* it keep their relative timing and any overlay edits on those children
|
|
43
|
+
* survive. A dragged motionPath waypoint is a `points` patch — it survives a
|
|
44
|
+
* knob-driven base regen because the step label is stable.
|
|
40
45
|
*/
|
|
41
46
|
timeline?: Record<string, {
|
|
42
47
|
duration?: number;
|
|
43
48
|
ease?: Ease;
|
|
44
49
|
stagger?: number;
|
|
50
|
+
at?: number;
|
|
51
|
+
gap?: number;
|
|
52
|
+
scale?: number;
|
|
53
|
+
order?: number;
|
|
54
|
+
points?: [number, number][];
|
|
45
55
|
}>;
|
|
46
56
|
}
|
|
47
57
|
export interface ComposeReport {
|
package/dist/types/dsl.d.ts
CHANGED
|
@@ -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, PropValue, RectProps, SceneIR, Size, StateOverride, TextProps, TimelineIR } from "./ir.js";
|
|
5
|
+
import type { AudioIR, BehaviorIR, 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;
|
|
@@ -33,12 +33,30 @@ export declare function text(props: {
|
|
|
33
33
|
export declare function image(props: {
|
|
34
34
|
id: string;
|
|
35
35
|
} & ImageProps): NodeIR;
|
|
36
|
+
export declare function path(props: {
|
|
37
|
+
id: string;
|
|
38
|
+
} & PathProps): NodeIR;
|
|
36
39
|
export declare function group(props: {
|
|
37
40
|
id: string;
|
|
38
41
|
} & GroupProps, children: NodeIR[]): NodeIR;
|
|
39
42
|
export declare function seq(...children: TimelineIR[]): TimelineIR;
|
|
40
43
|
export declare function par(...children: TimelineIR[]): TimelineIR;
|
|
41
44
|
export declare function stagger(interval: number, ...children: TimelineIR[]): TimelineIR;
|
|
45
|
+
export interface BeatOpts {
|
|
46
|
+
/** Group children in parallel instead of sequence. */
|
|
47
|
+
parallel?: boolean;
|
|
48
|
+
/** Absolute start (rigid placement). */
|
|
49
|
+
at?: number;
|
|
50
|
+
/** Relative shift before the beat. */
|
|
51
|
+
gap?: number;
|
|
52
|
+
/** Interior time-stretch factor. */
|
|
53
|
+
scale?: number;
|
|
54
|
+
/** Target total duration (→ scale). */
|
|
55
|
+
duration?: number;
|
|
56
|
+
/** Sort key within a parent seq (reorder). */
|
|
57
|
+
order?: number;
|
|
58
|
+
}
|
|
59
|
+
export declare function beat(name: string, opts: BeatOpts, children: TimelineIR[]): TimelineIR;
|
|
42
60
|
export declare function to(state: string, opts?: {
|
|
43
61
|
duration?: number;
|
|
44
62
|
ease?: Ease;
|
|
@@ -52,6 +70,14 @@ export declare function tween(target: string, props: Record<string, PropValue>,
|
|
|
52
70
|
label?: string;
|
|
53
71
|
}): TimelineIR;
|
|
54
72
|
export declare function wait(duration: number, label?: string): TimelineIR;
|
|
73
|
+
export declare function motionPath(target: string, points: [number, number][], opts?: {
|
|
74
|
+
duration?: number;
|
|
75
|
+
ease?: Ease;
|
|
76
|
+
closed?: boolean;
|
|
77
|
+
autoRotate?: boolean;
|
|
78
|
+
rotateOffset?: number;
|
|
79
|
+
label?: string;
|
|
80
|
+
}): TimelineIR;
|
|
55
81
|
export interface BehaviorWindow {
|
|
56
82
|
from?: number;
|
|
57
83
|
until?: number;
|
package/dist/types/evaluate.d.ts
CHANGED
|
@@ -61,6 +61,15 @@ export type DisplayOp = (OpBase & {
|
|
|
61
61
|
height: number;
|
|
62
62
|
offsetX: number;
|
|
63
63
|
offsetY: number;
|
|
64
|
+
}) | (OpBase & {
|
|
65
|
+
type: "path";
|
|
66
|
+
/** SVG path data, drawn via Path2D. */
|
|
67
|
+
d: string;
|
|
68
|
+
/** 0..1 fraction of the stroke outline drawn (draw-on). */
|
|
69
|
+
progress: number;
|
|
70
|
+
fill?: string;
|
|
71
|
+
stroke?: string;
|
|
72
|
+
strokeWidth?: number;
|
|
64
73
|
});
|
|
65
74
|
export type DisplayList = DisplayOp[];
|
|
66
75
|
export declare function evaluate(compiled: CompiledScene, t: number): DisplayList;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -2,9 +2,12 @@ export * from "./ir.js";
|
|
|
2
2
|
export * from "./dsl.js";
|
|
3
3
|
export { validateScene, SceneValidationError, PROPS_BY_TYPE } from "./validate.js";
|
|
4
4
|
export { composeScene, formatComposeReport, type OverlayDoc, type ComposeReport, } from "./compose.js";
|
|
5
|
-
export { compileScene, type CompiledScene, type PropertySegment, type LabelSpan } from "./compile.js";
|
|
5
|
+
export { compileScene, type CompiledScene, type PropertySegment, type LabelSpan, type MotionDriver } from "./compile.js";
|
|
6
|
+
export { pathPoint, pathTangentAngle, type Pt } from "./path.js";
|
|
7
|
+
export { motionPreset, PRESET_NAMES, type PresetName, type PresetRig, type PresetOpts } from "./presets.js";
|
|
6
8
|
export { resolveAudioPlan, SFX_DURATION, type AudioPlan, type ResolvedCue, } from "./audio.js";
|
|
7
9
|
export { evaluate, type DisplayList, type DisplayOp, type Mat2D, type TextAlign, type TextBaseline, } from "./evaluate.js";
|
|
8
10
|
export { resolveEase, lerpValue, isColor, EASE_NAMES } from "./interpolate.js";
|
|
9
11
|
export { sampleBehavior } from "./behaviors.js";
|
|
10
12
|
export { collectImageSrcs } from "./assets.js";
|
|
13
|
+
export { sketchToTimeline, type MotionSketch, type MotionEvent, type MotionEventKind, type MotionRegion, } from "./motion.js";
|
package/dist/types/ir.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Semantics: a scene is evaluated as a pure function of continuous time
|
|
9
9
|
* `evaluate(scene, tSeconds) -> DisplayList`. `fps` is a render hint only.
|
|
10
10
|
*/
|
|
11
|
-
export type EaseName = "linear" | "easeInQuad" | "easeOutQuad" | "easeInOutQuad" | "easeInCubic" | "easeOutCubic" | "easeInOutCubic" | "easeInQuart" | "easeOutQuart" | "easeInOutQuart" | "easeInExpo" | "easeOutExpo" | "easeInOutExpo";
|
|
11
|
+
export type EaseName = "linear" | "easeInQuad" | "easeOutQuad" | "easeInOutQuad" | "easeInCubic" | "easeOutCubic" | "easeInOutCubic" | "easeInQuart" | "easeOutQuart" | "easeInOutQuart" | "easeInExpo" | "easeOutExpo" | "easeInOutExpo" | "easeInBack" | "easeOutBack" | "easeInOutBack" | "easeInElastic" | "easeOutElastic" | "easeInOutElastic" | "easeInBounce" | "easeOutBounce" | "easeInOutBounce";
|
|
12
12
|
export type Ease = EaseName | {
|
|
13
13
|
cubicBezier: [number, number, number, number];
|
|
14
14
|
};
|
|
@@ -65,6 +65,26 @@ export interface TextProps extends BaseProps {
|
|
|
65
65
|
}
|
|
66
66
|
export interface GroupProps extends BaseProps {
|
|
67
67
|
}
|
|
68
|
+
export interface PathProps extends BaseProps {
|
|
69
|
+
/** SVG path data (the `d` attribute). Drawn as a true vector — crisp at any zoom. */
|
|
70
|
+
d: string;
|
|
71
|
+
fill?: string;
|
|
72
|
+
stroke?: string;
|
|
73
|
+
strokeWidth?: number;
|
|
74
|
+
/**
|
|
75
|
+
* 0..1 — fraction of the OUTLINE drawn, for a self-drawing "draw-on" effect
|
|
76
|
+
* (animate 0→1). Applies to the stroke; pair a stroke path (draw-on) with a
|
|
77
|
+
* separate fill path (fade-in) for the classic logo reveal. Default 1.
|
|
78
|
+
*/
|
|
79
|
+
progress?: number;
|
|
80
|
+
/**
|
|
81
|
+
* Local pivot in the path's own coordinate space — scale/rotation happen
|
|
82
|
+
* around this point. Set it to the art's centre (e.g. the viewBox centre) so
|
|
83
|
+
* a logo zooms/spins about its middle. Default (0,0).
|
|
84
|
+
*/
|
|
85
|
+
originX?: number;
|
|
86
|
+
originY?: number;
|
|
87
|
+
}
|
|
68
88
|
export interface ImageProps extends BaseProps {
|
|
69
89
|
/**
|
|
70
90
|
* Image file path: absolute, or relative to the scene file. Drawn
|
|
@@ -96,6 +116,10 @@ export type NodeIR = {
|
|
|
96
116
|
type: "image";
|
|
97
117
|
id: string;
|
|
98
118
|
props: ImageProps;
|
|
119
|
+
} | {
|
|
120
|
+
type: "path";
|
|
121
|
+
id: string;
|
|
122
|
+
props: PathProps;
|
|
99
123
|
} | {
|
|
100
124
|
type: "group";
|
|
101
125
|
id: string;
|
|
@@ -141,6 +165,47 @@ export type TimelineIR = {
|
|
|
141
165
|
kind: "wait";
|
|
142
166
|
duration: number;
|
|
143
167
|
label?: string;
|
|
168
|
+
} | {
|
|
169
|
+
/**
|
|
170
|
+
* Drive a node's x/y along a Catmull-Rom spline through `points`
|
|
171
|
+
* (absolute coords in the node's parent space). With `autoRotate`, the
|
|
172
|
+
* node's rotation tracks the path tangent (plus `rotateOffset`). The
|
|
173
|
+
* position HOLDS at the final point after the path completes, so a swoop
|
|
174
|
+
* is a positioning move, not a one-shot that snaps back.
|
|
175
|
+
*/
|
|
176
|
+
kind: "motionPath";
|
|
177
|
+
target: string;
|
|
178
|
+
points: [number, number][];
|
|
179
|
+
closed?: boolean;
|
|
180
|
+
duration?: number;
|
|
181
|
+
ease?: Ease;
|
|
182
|
+
autoRotate?: boolean;
|
|
183
|
+
/** Degrees added to the tangent angle (e.g. 90 if the art faces "up"). */
|
|
184
|
+
rotateOffset?: number;
|
|
185
|
+
label?: string;
|
|
186
|
+
} | {
|
|
187
|
+
/**
|
|
188
|
+
* A named, retimable, reorderable span wrapping timeline steps — the
|
|
189
|
+
* semantic unit ("brand-reveal", "feature-cascade") humans and AI revise.
|
|
190
|
+
* Lowers to its grouping (seq, or par if `parallel`) before timing, so
|
|
191
|
+
* `beat(name, {}, children)` is byte-identical to `seq(children)`.
|
|
192
|
+
* Ops are RIGID: they translate/stretch the whole span, preserving the
|
|
193
|
+
* interior's relative timing (so sub-beat overlay edits survive a move).
|
|
194
|
+
*/
|
|
195
|
+
kind: "beat";
|
|
196
|
+
name: string;
|
|
197
|
+
parallel?: boolean;
|
|
198
|
+
/** Absolute start (rigid placement). Overrides sequential flow. */
|
|
199
|
+
at?: number;
|
|
200
|
+
/** Relative shift: a leading delay before the beat (and everything after). */
|
|
201
|
+
gap?: number;
|
|
202
|
+
/** Interior time-stretch factor (every child offset and duration ×scale). */
|
|
203
|
+
scale?: number;
|
|
204
|
+
/** Target total duration → scale = duration / natural duration. */
|
|
205
|
+
duration?: number;
|
|
206
|
+
/** Sort key within a parent seq (reorder); default = declaration index. */
|
|
207
|
+
order?: number;
|
|
208
|
+
children: TimelineIR[];
|
|
144
209
|
};
|
|
145
210
|
export interface BehaviorIR {
|
|
146
211
|
target: string;
|
|
@@ -225,4 +290,5 @@ export interface SceneIR {
|
|
|
225
290
|
}
|
|
226
291
|
export declare const DEFAULT_TO_DURATION = 0.5;
|
|
227
292
|
export declare const DEFAULT_TWEEN_DURATION = 0.5;
|
|
293
|
+
export declare const DEFAULT_MOTIONPATH_DURATION = 1;
|
|
228
294
|
export declare const DEFAULT_FPS = 30;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MotionSketch — the intermediate representation between a video's pixel
|
|
3
|
+
* motion signal and a reframe timeline. Extraction (pixels → sketch) lives in
|
|
4
|
+
* the motion harness (it needs the profiler); this module owns the sketch
|
|
5
|
+
* *vocabulary* and the *emission* (sketch → TimelineIR), both of which are
|
|
6
|
+
* pure and dependency-free so core can host them.
|
|
7
|
+
*
|
|
8
|
+
* Reconstruction is deliberately geometry-free: events emit opacity/scale
|
|
9
|
+
* tweens whose targets are absolute (1, or 1+magnitude), so a sketch re-applies
|
|
10
|
+
* to nodes without knowing their rest positions — which is what makes the
|
|
11
|
+
* round-trip verifier independent of node layout.
|
|
12
|
+
*/
|
|
13
|
+
import type { TimelineIR } from "./ir.js";
|
|
14
|
+
export type MotionEventKind = "enter" | "exit" | "move" | "scale" | "emphasis";
|
|
15
|
+
/** Normalized 0..1 activity bounding box — a coarse region, NOT a tracked object. */
|
|
16
|
+
export interface MotionRegion {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
w: number;
|
|
20
|
+
h: number;
|
|
21
|
+
}
|
|
22
|
+
export interface MotionEvent {
|
|
23
|
+
/** Onset / settle, seconds. */
|
|
24
|
+
t0: number;
|
|
25
|
+
t1: number;
|
|
26
|
+
kind: MotionEventKind;
|
|
27
|
+
region: MotionRegion;
|
|
28
|
+
/** Displacement or scale delta, normalized to the frame. */
|
|
29
|
+
magnitude: number;
|
|
30
|
+
easing: {
|
|
31
|
+
class: string;
|
|
32
|
+
thirdsRatio: number | null;
|
|
33
|
+
reliable: boolean;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export interface MotionSketch {
|
|
37
|
+
duration: number;
|
|
38
|
+
fps: number;
|
|
39
|
+
events: MotionEvent[];
|
|
40
|
+
/** Global cadence; periodicityHz null when no clear oscillation. */
|
|
41
|
+
rhythm: {
|
|
42
|
+
periodicityHz: number | null;
|
|
43
|
+
beatCount: number;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Emit a reframe timeline reproducing a sketch's timing, applied to `nodeIds`.
|
|
48
|
+
* Events are assigned to nodes in onset order (v1: region→node mapping is
|
|
49
|
+
* goal-2's concern; here ids are given). Geometry-free: enter/exit drive
|
|
50
|
+
* opacity, emphasis/scale drive scale, so the emitted timeline needs no node
|
|
51
|
+
* coordinates. For an `enter` to read as an entrance the target node should
|
|
52
|
+
* start hidden (opacity 0) — idiomatic reframe `initial` state.
|
|
53
|
+
*/
|
|
54
|
+
export declare function sketchToTimeline(sketch: MotionSketch, nodeIds: string[]): TimelineIR;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MotionPath geometry — a Catmull-Rom spline through waypoints, evaluated as a
|
|
3
|
+
* pure function of progress u in [0,1]. Used by evaluate() to drive a node's
|
|
4
|
+
* x/y (and, with autoRotate, rotation along the tangent) along a curve.
|
|
5
|
+
*
|
|
6
|
+
* Pure math, no DOM: same result in Node (tests/golden) and the browser
|
|
7
|
+
* renderer. Uniform parameterisation — each segment owns an equal slice of u.
|
|
8
|
+
* Good for hand-authored sting waypoints; centripetal can come later if uneven
|
|
9
|
+
* spacing ever overshoots.
|
|
10
|
+
*/
|
|
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;
|
|
14
|
+
/** 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;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion presets — a small NAMED vocabulary of logo-sting motions, each a
|
|
3
|
+
* SEEDED GENERATOR (not a frozen template). `motionPreset(name, opts)` returns
|
|
4
|
+
* a `beat` (goal-2's addressable, retimable unit) composed from the existing
|
|
5
|
+
* primitives (motionPath, tween, stagger, path draw-on, expressive eases).
|
|
6
|
+
*
|
|
7
|
+
* Two-determinisms: same (name, knobs, seed) → identical IR (reproducible);
|
|
8
|
+
* a different `seed` perturbs waypoints / timing / accents WITHIN BOUNDED
|
|
9
|
+
* ranges → a measurably different motion that is still the same family. The
|
|
10
|
+
* seed feeds a deterministic PRNG — no Math.random, ever.
|
|
11
|
+
*
|
|
12
|
+
* Universal knobs (every preset): `energy` 0..1 (settle ↔ springy overshoot),
|
|
13
|
+
* `speed` (duration multiplier, >1 faster). Plus a signature knob `intensity`
|
|
14
|
+
* 0..1 (the preset's amplitude) and, where spatial, `from`.
|
|
15
|
+
*/
|
|
16
|
+
import type { TimelineIR } from "./ir.js";
|
|
17
|
+
export type PresetName = "draw-bloom" | "punch-in" | "rise-settle" | "slide-bank" | "reveal-orbit" | "spin-forge";
|
|
18
|
+
export declare const PRESET_NAMES: PresetName[];
|
|
19
|
+
/** The rig a preset drives: a group at `center` scaled to `baseScale`, with
|
|
20
|
+
* fill layers (bloom) and ink layers (draw-on outline). */
|
|
21
|
+
export interface PresetRig {
|
|
22
|
+
group: string;
|
|
23
|
+
center: [number, number];
|
|
24
|
+
baseScale: number;
|
|
25
|
+
fills: string[];
|
|
26
|
+
inks: string[];
|
|
27
|
+
}
|
|
28
|
+
export interface PresetOpts {
|
|
29
|
+
target: PresetRig;
|
|
30
|
+
/** 0..1 — clean settle ↔ springy overshoot. Default 0.5. */
|
|
31
|
+
energy?: number;
|
|
32
|
+
/** Duration multiplier; >1 faster, <1 slower. Default 1. */
|
|
33
|
+
speed?: number;
|
|
34
|
+
/** 0..1 — the signature amplitude (orbit radius / rise distance / spins). Default 0.5. */
|
|
35
|
+
intensity?: number;
|
|
36
|
+
/** Entry direction for spatial presets. Default per preset. */
|
|
37
|
+
from?: "left" | "right" | "top" | "bottom";
|
|
38
|
+
/** Deterministic variation. Same seed → identical; different seed → same family. Default 0. */
|
|
39
|
+
seed?: number;
|
|
40
|
+
}
|
|
41
|
+
export declare function motionPreset(name: PresetName, opts: PresetOpts): TimelineIR;
|
package/guides/edsl-guide.md
CHANGED
|
@@ -34,6 +34,13 @@ Factories return plain data. Every node needs a unique `id`.
|
|
|
34
34
|
`content` may be a number; numeric content interpolates (count-up) and renders
|
|
35
35
|
via `toFixed(contentDecimals ?? 0)`. For a "8.2"-style label, set
|
|
36
36
|
`contentDecimals: 1`.
|
|
37
|
+
- `path({ id, d, x, y, fill?, stroke?, strokeWidth?, progress?, originX?, originY?, opacity?, rotation?, scale?, anchor? })` —
|
|
38
|
+
a true vector shape from an SVG path `d` string (crisp at any zoom; recolour by
|
|
39
|
+
animating `fill`/`stroke`). `progress` 0..1 draws the stroke OUTLINE on (animate
|
|
40
|
+
0→1 for a self-drawing logo). `originX`/`originY` is the local pivot — set it to
|
|
41
|
+
the art's centre (e.g. the viewBox centre) so `scale`/`rotation` happen about the
|
|
42
|
+
middle. `d` is drawn in its own coords; `x`/`y` place that pivot. Classic logo
|
|
43
|
+
reveal: a stroke path drawing on, then a fill path fading in over it.
|
|
37
44
|
- `image({ id, src, x, y, width, height, opacity?, rotation?, scale?, anchor? })` —
|
|
38
45
|
`src` is a file path, absolute or relative to the scene file; drawn stretched
|
|
39
46
|
to `width`×`height` (png/jpg/webp). `src` switches discretely (no crossfade) —
|
|
@@ -75,11 +82,23 @@ them with normal TS (`Object.fromEntries`, `.map`) for data-driven scenes.
|
|
|
75
82
|
- `to(stateName, opts)` — transition into a named state (see above).
|
|
76
83
|
- `tween(nodeId, { prop: value, ... }, { duration, ease })` — low-level escape hatch
|
|
77
84
|
for one node. Colors (`"#rrggbb"`) interpolate; numbers interpolate.
|
|
85
|
+
- `motionPath(nodeId, [[x,y], ...], { duration, ease, autoRotate?, rotateOffset?, closed? })`
|
|
86
|
+
— drive a node's `x`/`y` along a smooth Catmull-Rom curve through the waypoints
|
|
87
|
+
(parent-space coords). `autoRotate: true` banks the node along the path tangent
|
|
88
|
+
(`rotateOffset` degrees if the art faces "up", e.g. `-90`). The node HOLDS at the
|
|
89
|
+
final point after the path finishes (a positioning move, not a one-shot), so a
|
|
90
|
+
later `tween` can chain from there. Use it for swoops/arcs/orbits — straight
|
|
91
|
+
`tween`s on x and y can't curve. `closed: true` loops the waypoints (orbit).
|
|
78
92
|
- `wait(seconds)` — hold.
|
|
79
93
|
|
|
80
94
|
Eases: `linear`, `easeIn/Out/InOutQuad`, `easeIn/Out/InOutCubic`,
|
|
81
95
|
`easeIn/Out/InOutQuart`, `easeIn/Out/InOutExpo`, or `{ cubicBezier: [x1,y1,x2,y2] }`.
|
|
82
96
|
Decelerating entrances = `easeOut*`, accelerating exits = `easeIn*`.
|
|
97
|
+
Expressive eases for a premium feel: `easeIn/Out/InOutBack` (overshoots past the
|
|
98
|
+
target then settles — a pop/snap), `easeIn/Out/InOutElastic` (rings around the
|
|
99
|
+
target — a playful spring), `easeIn/Out/InOutBounce` (drops and bounces to rest).
|
|
100
|
+
A logo or card "popping" in usually wants `easeOutBack`; a stamp landing,
|
|
101
|
+
`easeOutBounce`.
|
|
83
102
|
Scene duration is inferred from the timeline.
|
|
84
103
|
|
|
85
104
|
## Behaviors: continuous motion during holds
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reframe-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|
package/preview/src/main.ts
CHANGED
|
@@ -134,6 +134,9 @@ function opCorners(op: DisplayOp): [number, number][] {
|
|
|
134
134
|
applyMat(op.transform, px!, py!),
|
|
135
135
|
);
|
|
136
136
|
}
|
|
137
|
+
case "path":
|
|
138
|
+
// No cheap bbox for an arbitrary `d`; mark the origin for selection.
|
|
139
|
+
return [applyMat(op.transform, 0, 0)];
|
|
137
140
|
}
|
|
138
141
|
}
|
|
139
142
|
|
|
@@ -158,11 +161,70 @@ function draw() {
|
|
|
158
161
|
ctx.restore();
|
|
159
162
|
}
|
|
160
163
|
|
|
164
|
+
// motionPath waypoint handles — drag to reshape the curve (writes a
|
|
165
|
+
// timeline.<label>.points overlay patch that survives base regeneration).
|
|
166
|
+
// Points are in the target's parent space; correct for top-level targets.
|
|
167
|
+
ctx.save();
|
|
168
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
169
|
+
for (const mp of store.motionPaths()) {
|
|
170
|
+
ctx.strokeStyle = "rgba(125,154,255,0.4)";
|
|
171
|
+
ctx.lineWidth = 1.5;
|
|
172
|
+
ctx.setLineDash([4, 4]);
|
|
173
|
+
ctx.beginPath();
|
|
174
|
+
mp.points.forEach(([x, y], i) => (i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y)));
|
|
175
|
+
ctx.stroke();
|
|
176
|
+
ctx.setLineDash([]);
|
|
177
|
+
for (const [x, y] of mp.points) {
|
|
178
|
+
ctx.beginPath();
|
|
179
|
+
ctx.arc(x, y, HANDLE_R, 0, Math.PI * 2);
|
|
180
|
+
ctx.fillStyle = "#7d9aff";
|
|
181
|
+
ctx.fill();
|
|
182
|
+
ctx.strokeStyle = "#0b0b12";
|
|
183
|
+
ctx.lineWidth = 2;
|
|
184
|
+
ctx.stroke();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
ctx.restore();
|
|
188
|
+
|
|
161
189
|
const duration = store.compiled.duration;
|
|
162
190
|
scrub.value = String(duration ? t / duration : 0);
|
|
163
191
|
timeLabel.textContent = `${t.toFixed(3)} / ${duration.toFixed(3)}`;
|
|
164
192
|
}
|
|
165
193
|
|
|
194
|
+
const HANDLE_R = 9;
|
|
195
|
+
|
|
196
|
+
/** Map a mouse event to scene coordinates (canvas is rendered at scene size, displayed scaled). */
|
|
197
|
+
function clientToScene(ev: MouseEvent): [number, number] {
|
|
198
|
+
const r = canvas.getBoundingClientRect();
|
|
199
|
+
return [((ev.clientX - r.left) * canvas.width) / r.width, ((ev.clientY - r.top) * canvas.height) / r.height];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let drag: { label: string; index: number; points: [number, number][] } | null = null;
|
|
203
|
+
canvas.addEventListener("mousedown", (ev) => {
|
|
204
|
+
if (!store) return;
|
|
205
|
+
const [x, y] = clientToScene(ev);
|
|
206
|
+
for (const mp of store.motionPaths()) {
|
|
207
|
+
const i = mp.points.findIndex(([px, py]) => Math.hypot(px - x, py - y) <= HANDLE_R + 4);
|
|
208
|
+
if (i >= 0) {
|
|
209
|
+
drag = { label: mp.label, index: i, points: mp.points.map((p) => [...p] as [number, number]) };
|
|
210
|
+
playing = false;
|
|
211
|
+
playBtn.textContent = "play";
|
|
212
|
+
ev.preventDefault();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
window.addEventListener("mousemove", (ev) => {
|
|
218
|
+
if (!drag || !store) return;
|
|
219
|
+
const [x, y] = clientToScene(ev);
|
|
220
|
+
drag.points[drag.index] = [Math.round(x), Math.round(y)];
|
|
221
|
+
store.setMotionPathPoints(drag.label, drag.points);
|
|
222
|
+
draw();
|
|
223
|
+
});
|
|
224
|
+
window.addEventListener("mouseup", () => {
|
|
225
|
+
drag = null;
|
|
226
|
+
});
|
|
227
|
+
|
|
166
228
|
function tick(now: number) {
|
|
167
229
|
if (playing && store) {
|
|
168
230
|
t += (now - lastTick) / 1000;
|
package/preview/src/panel.ts
CHANGED
|
@@ -200,6 +200,39 @@ export function buildPanel(store: EditorStore, root: HTMLElement) {
|
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
// --- beats (semantic groups) ---
|
|
204
|
+
const beats: Extract<TimelineIR, { kind: "beat" }>[] = [];
|
|
205
|
+
const beatOf = new Map<string, string>();
|
|
206
|
+
const walkBeats = (tl: TimelineIR, owner?: string) => {
|
|
207
|
+
if (tl.kind === "beat") {
|
|
208
|
+
beats.push(tl);
|
|
209
|
+
tl.children.forEach((c) => walkBeats(c, tl.name));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if ("label" in tl && tl.label !== undefined && owner) beatOf.set(tl.label, owner);
|
|
213
|
+
if ("children" in tl) tl.children.forEach((c) => walkBeats(c, owner));
|
|
214
|
+
};
|
|
215
|
+
if (ir.timeline) walkBeats(ir.timeline);
|
|
216
|
+
if (beats.length > 0) {
|
|
217
|
+
root.append(el("h3", {}, "Beats"));
|
|
218
|
+
for (const b of beats) {
|
|
219
|
+
const card = el("div", { class: "step-card beat-card" },
|
|
220
|
+
el("div", {}, `${b.name} `, el("span", { class: "kind" }, "(beat)")),
|
|
221
|
+
);
|
|
222
|
+
const gapRow = makeControl("gap", b.gap ?? 0, store.hasTimelineEdit(b.name, "gap"),
|
|
223
|
+
(v) => store.setTimelineParam(b.name, "gap", Number(v)),
|
|
224
|
+
() => store.unsetTimelineParam(b.name, "gap"));
|
|
225
|
+
gapRow.prepend(el("label", {}, "gap"));
|
|
226
|
+
card.append(gapRow);
|
|
227
|
+
const scaleRow = makeControl("scale", b.scale ?? 1, store.hasTimelineEdit(b.name, "scale"),
|
|
228
|
+
(v) => store.setTimelineParam(b.name, "scale", Number(v)),
|
|
229
|
+
() => store.unsetTimelineParam(b.name, "scale"));
|
|
230
|
+
scaleRow.prepend(el("label", {}, "scale"));
|
|
231
|
+
card.append(scaleRow);
|
|
232
|
+
root.append(card);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
203
236
|
// --- labeled timeline steps ---
|
|
204
237
|
const steps: Extract<TimelineIR, { label?: string }>[] = [];
|
|
205
238
|
const walkTl = (tl: TimelineIR) => {
|
|
@@ -211,9 +244,10 @@ export function buildPanel(store: EditorStore, root: HTMLElement) {
|
|
|
211
244
|
root.append(el("h3", {}, "Timeline"));
|
|
212
245
|
for (const step of steps) {
|
|
213
246
|
const label = step.label!;
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
);
|
|
247
|
+
const beatName = beatOf.get(label);
|
|
248
|
+
const head = el("div", {}, `${label} `, el("span", { class: "kind" }, `(${step.kind})`));
|
|
249
|
+
if (beatName) head.append(el("span", { class: "badge" }, ` ↳ ${beatName}`));
|
|
250
|
+
const card = el("div", { class: "step-card" }, head);
|
|
217
251
|
const durRow = makeControl(
|
|
218
252
|
"duration",
|
|
219
253
|
"duration" in step ? (step.duration ?? 0.5) : 0.5,
|
package/preview/src/store.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type OverlayDoc,
|
|
16
16
|
type PropValue,
|
|
17
17
|
type SceneIR,
|
|
18
|
+
type TimelineIR,
|
|
18
19
|
} from "@reframe/core";
|
|
19
20
|
|
|
20
21
|
export type ChangeKind = "value" | "structure";
|
|
@@ -91,12 +92,37 @@ export class EditorStore {
|
|
|
91
92
|
this.recompose("structure");
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
setTimelineParam(
|
|
95
|
+
setTimelineParam(
|
|
96
|
+
label: string,
|
|
97
|
+
key: "duration" | "ease" | "stagger" | "at" | "gap" | "scale" | "order",
|
|
98
|
+
value: number | string,
|
|
99
|
+
) {
|
|
95
100
|
((this.draft.timeline ??= {})[label] ??= {})[key] = value as never;
|
|
96
101
|
this.recompose("value");
|
|
97
102
|
}
|
|
98
103
|
|
|
99
|
-
|
|
104
|
+
/** Labeled motionPath steps with their (possibly overlay-edited) waypoints —
|
|
105
|
+
* drives the preview's draggable handles and is exposed on window.__store. */
|
|
106
|
+
motionPaths(): { label: string; points: [number, number][] }[] {
|
|
107
|
+
const out: { label: string; points: [number, number][] }[] = [];
|
|
108
|
+
const walk = (tl: TimelineIR) => {
|
|
109
|
+
if (tl.kind === "motionPath" && tl.label) out.push({ label: tl.label, points: tl.points });
|
|
110
|
+
if ("children" in tl) tl.children.forEach(walk);
|
|
111
|
+
};
|
|
112
|
+
if (this.compiled.ir.timeline) walk(this.compiled.ir.timeline);
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A dragged waypoint writes the whole points array as a timeline patch. */
|
|
117
|
+
setMotionPathPoints(label: string, points: [number, number][]) {
|
|
118
|
+
((this.draft.timeline ??= {})[label] ??= {}).points = points;
|
|
119
|
+
this.recompose("value");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
unsetTimelineParam(
|
|
123
|
+
label: string,
|
|
124
|
+
key: "duration" | "ease" | "stagger" | "at" | "gap" | "scale" | "order",
|
|
125
|
+
) {
|
|
100
126
|
delete this.draft.timeline?.[label]?.[key];
|
|
101
127
|
this.prune();
|
|
102
128
|
this.recompose("structure");
|