reframe-video 0.1.0 → 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/assets/sfx/LICENSE.md +5 -0
- package/assets/sfx/bgm-song21.mp3 +0 -0
- package/assets/sfx/pop.wav +0 -0
- package/assets/sfx/rise.wav +0 -0
- package/assets/sfx/shimmer.wav +0 -0
- package/assets/sfx/thud.wav +0 -0
- package/assets/sfx/tick.wav +0 -0
- package/assets/sfx/whoosh.wav +0 -0
- package/dist/analyze.js +44 -1
- package/dist/bin.js +371 -35
- package/dist/browserEntry.js +311 -8
- package/dist/cli.js +305 -21
- package/dist/index.js +566 -16
- package/dist/renderer-canvas.js +58 -3
- package/dist/trace-cli.js +677 -0
- package/dist/types/assets.d.ts +9 -0
- package/dist/types/compile.d.ts +15 -1
- package/dist/types/compose.d.ts +12 -2
- package/dist/types/dsl.d.ts +30 -1
- package/dist/types/evaluate.d.ts +17 -0
- package/dist/types/index.d.ts +5 -1
- package/dist/types/ir.d.ts +82 -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 +24 -0
- package/package.json +1 -1
- package/preview/src/main.ts +111 -6
- package/preview/src/panel.ts +37 -3
- package/preview/src/store.ts +28 -2
- package/preview/src/virtual.d.ts +9 -1
- package/preview/vite.config.ts +3 -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, 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;
|
|
@@ -30,12 +30,33 @@ export declare function line(props: {
|
|
|
30
30
|
export declare function text(props: {
|
|
31
31
|
id: string;
|
|
32
32
|
} & TextProps): NodeIR;
|
|
33
|
+
export declare function image(props: {
|
|
34
|
+
id: string;
|
|
35
|
+
} & ImageProps): NodeIR;
|
|
36
|
+
export declare function path(props: {
|
|
37
|
+
id: string;
|
|
38
|
+
} & PathProps): NodeIR;
|
|
33
39
|
export declare function group(props: {
|
|
34
40
|
id: string;
|
|
35
41
|
} & GroupProps, children: NodeIR[]): NodeIR;
|
|
36
42
|
export declare function seq(...children: TimelineIR[]): TimelineIR;
|
|
37
43
|
export declare function par(...children: TimelineIR[]): TimelineIR;
|
|
38
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;
|
|
39
60
|
export declare function to(state: string, opts?: {
|
|
40
61
|
duration?: number;
|
|
41
62
|
ease?: Ease;
|
|
@@ -49,6 +70,14 @@ export declare function tween(target: string, props: Record<string, PropValue>,
|
|
|
49
70
|
label?: string;
|
|
50
71
|
}): TimelineIR;
|
|
51
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;
|
|
52
81
|
export interface BehaviorWindow {
|
|
53
82
|
from?: number;
|
|
54
83
|
until?: number;
|
package/dist/types/evaluate.d.ts
CHANGED
|
@@ -53,6 +53,23 @@ export type DisplayOp = (OpBase & {
|
|
|
53
53
|
letterSpacing: number;
|
|
54
54
|
align: TextAlign;
|
|
55
55
|
baseline: TextBaseline;
|
|
56
|
+
}) | (OpBase & {
|
|
57
|
+
type: "image";
|
|
58
|
+
/** Raw src string as authored in the IR — consumers resolve it. */
|
|
59
|
+
src: string;
|
|
60
|
+
width: number;
|
|
61
|
+
height: number;
|
|
62
|
+
offsetX: number;
|
|
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;
|
|
56
73
|
});
|
|
57
74
|
export type DisplayList = DisplayOp[];
|
|
58
75
|
export declare function evaluate(compiled: CompiledScene, t: number): DisplayList;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -2,8 +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";
|
|
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,37 @@ 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
|
+
}
|
|
88
|
+
export interface ImageProps extends BaseProps {
|
|
89
|
+
/**
|
|
90
|
+
* Image file path: absolute, or relative to the scene file. Drawn
|
|
91
|
+
* stretched to width×height. As a string prop it switches discretely at
|
|
92
|
+
* segment start (no crossfade) — for hard-cut sequences stack image
|
|
93
|
+
* nodes and step their opacity instead.
|
|
94
|
+
*/
|
|
95
|
+
src: string;
|
|
96
|
+
width: number;
|
|
97
|
+
height: number;
|
|
98
|
+
}
|
|
68
99
|
export type NodeIR = {
|
|
69
100
|
type: "rect";
|
|
70
101
|
id: string;
|
|
@@ -81,6 +112,14 @@ export type NodeIR = {
|
|
|
81
112
|
type: "text";
|
|
82
113
|
id: string;
|
|
83
114
|
props: TextProps;
|
|
115
|
+
} | {
|
|
116
|
+
type: "image";
|
|
117
|
+
id: string;
|
|
118
|
+
props: ImageProps;
|
|
119
|
+
} | {
|
|
120
|
+
type: "path";
|
|
121
|
+
id: string;
|
|
122
|
+
props: PathProps;
|
|
84
123
|
} | {
|
|
85
124
|
type: "group";
|
|
86
125
|
id: string;
|
|
@@ -126,6 +165,47 @@ export type TimelineIR = {
|
|
|
126
165
|
kind: "wait";
|
|
127
166
|
duration: number;
|
|
128
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[];
|
|
129
209
|
};
|
|
130
210
|
export interface BehaviorIR {
|
|
131
211
|
target: string;
|
|
@@ -210,4 +290,5 @@ export interface SceneIR {
|
|
|
210
290
|
}
|
|
211
291
|
export declare const DEFAULT_TO_DURATION = 0.5;
|
|
212
292
|
export declare const DEFAULT_TWEEN_DURATION = 0.5;
|
|
293
|
+
export declare const DEFAULT_MOTIONPATH_DURATION = 1;
|
|
213
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,18 @@ 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.
|
|
44
|
+
- `image({ id, src, x, y, width, height, opacity?, rotation?, scale?, anchor? })` —
|
|
45
|
+
`src` is a file path, absolute or relative to the scene file; drawn stretched
|
|
46
|
+
to `width`×`height` (png/jpg/webp). `src` switches discretely (no crossfade) —
|
|
47
|
+
for hard-cut frame sequences stack image nodes and step their `opacity`; for
|
|
48
|
+
a dissolve, crossfade two nodes' opacity.
|
|
37
49
|
- `group({ id, x, y, opacity?, rotation?, scale?, anchor? }, children)` — children's
|
|
38
50
|
coordinates are relative to the group; group opacity/transform multiply down.
|
|
39
51
|
|
|
@@ -70,11 +82,23 @@ them with normal TS (`Object.fromEntries`, `.map`) for data-driven scenes.
|
|
|
70
82
|
- `to(stateName, opts)` — transition into a named state (see above).
|
|
71
83
|
- `tween(nodeId, { prop: value, ... }, { duration, ease })` — low-level escape hatch
|
|
72
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).
|
|
73
92
|
- `wait(seconds)` — hold.
|
|
74
93
|
|
|
75
94
|
Eases: `linear`, `easeIn/Out/InOutQuad`, `easeIn/Out/InOutCubic`,
|
|
76
95
|
`easeIn/Out/InOutQuart`, `easeIn/Out/InOutExpo`, or `{ cubicBezier: [x1,y1,x2,y2] }`.
|
|
77
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`.
|
|
78
102
|
Scene duration is inferred from the timeline.
|
|
79
103
|
|
|
80
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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* path never uses wall-clock time.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { evaluate, type DisplayOp, type SceneIR } from "@reframe/core";
|
|
8
|
+
import { collectImageSrcs, evaluate, type DisplayOp, type SceneIR } from "@reframe/core";
|
|
9
9
|
import { renderFrame } from "@reframe/renderer-canvas";
|
|
10
10
|
import { userScenes } from "virtual:reframe-user-scenes";
|
|
11
11
|
import { buildPanel } from "./panel.js";
|
|
@@ -13,17 +13,23 @@ import { EditorStore } from "./store.js";
|
|
|
13
13
|
|
|
14
14
|
interface SceneEntry {
|
|
15
15
|
label: string;
|
|
16
|
+
/** Absolute directory of the scene file — relative image srcs resolve here. */
|
|
17
|
+
dir: string;
|
|
16
18
|
load: () => Promise<{ default: SceneIR }>;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
const exampleModules = ({} as Record<string, () => Promise<{ default: SceneIR }>>);
|
|
20
22
|
const modules: Record<string, SceneEntry> = {};
|
|
21
23
|
for (const path of Object.keys(exampleModules).sort()) {
|
|
22
|
-
modules[path] = {
|
|
24
|
+
modules[path] = {
|
|
25
|
+
label: path.split("/").pop()!.replace(".ts", ""),
|
|
26
|
+
dir: __REFRAME_EXAMPLES_DIR__,
|
|
27
|
+
load: exampleModules[path]!,
|
|
28
|
+
};
|
|
23
29
|
}
|
|
24
30
|
// scenes from the directory `reframe preview` was invoked in
|
|
25
|
-
for (const { name, load } of userScenes) {
|
|
26
|
-
modules[`user:${name}`] ??= { label: `${name} (cwd)`, load };
|
|
31
|
+
for (const { name, dir, load } of userScenes) {
|
|
32
|
+
modules[`user:${name}`] ??= { label: `${name} (cwd)`, dir, load };
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
|
|
@@ -47,9 +53,43 @@ for (const [key, entry] of Object.entries(modules)) {
|
|
|
47
53
|
select.appendChild(option);
|
|
48
54
|
}
|
|
49
55
|
|
|
56
|
+
// decoded images keyed by raw src; missing entries render as a placeholder
|
|
57
|
+
const images = new Map<string, CanvasImageSource>();
|
|
58
|
+
const imageLoads = new Map<string, Promise<void>>();
|
|
59
|
+
let sceneDir = "";
|
|
60
|
+
|
|
61
|
+
/** Load any not-yet-loaded srcs of the current scene via /@fs. */
|
|
62
|
+
function ensureImages(): Promise<void> {
|
|
63
|
+
if (!store) return Promise.resolve();
|
|
64
|
+
const pending: Promise<void>[] = [];
|
|
65
|
+
for (const src of collectImageSrcs(store.compiled.ir)) {
|
|
66
|
+
if (images.has(src) || imageLoads.has(src)) continue;
|
|
67
|
+
const url = `/@fs${src.startsWith("/") ? src : `${sceneDir}/${src}`}`;
|
|
68
|
+
const load = new Promise<void>((done) => {
|
|
69
|
+
const img = new Image();
|
|
70
|
+
img.onload = () => {
|
|
71
|
+
images.set(src, img);
|
|
72
|
+
done();
|
|
73
|
+
draw();
|
|
74
|
+
};
|
|
75
|
+
img.onerror = () => {
|
|
76
|
+
console.warn(`image "${src}" failed to load (${url}) — rendering placeholder`);
|
|
77
|
+
done();
|
|
78
|
+
};
|
|
79
|
+
img.src = url;
|
|
80
|
+
});
|
|
81
|
+
imageLoads.set(src, load);
|
|
82
|
+
pending.push(load);
|
|
83
|
+
}
|
|
84
|
+
return Promise.all(pending).then(() => undefined);
|
|
85
|
+
}
|
|
86
|
+
|
|
50
87
|
async function loadScene(path: string) {
|
|
51
88
|
const mod = await modules[path]!.load();
|
|
52
89
|
store = new EditorStore(mod.default);
|
|
90
|
+
sceneDir = modules[path]!.dir;
|
|
91
|
+
images.clear();
|
|
92
|
+
imageLoads.clear();
|
|
53
93
|
(window as unknown as { __store: EditorStore }).__store = store; // debug/testing hook
|
|
54
94
|
panel = buildPanel(store, panelRoot);
|
|
55
95
|
canvas.width = store.compiled.ir.size.width;
|
|
@@ -58,9 +98,11 @@ async function loadScene(path: string) {
|
|
|
58
98
|
t = Math.min(t, store!.compiled.duration);
|
|
59
99
|
if (kind === "structure") panel!.rebuild();
|
|
60
100
|
else panel!.refreshReport();
|
|
101
|
+
void ensureImages(); // an edited src loads lazily, then redraws
|
|
61
102
|
draw();
|
|
62
103
|
});
|
|
63
104
|
await document.fonts.ready;
|
|
105
|
+
await ensureImages();
|
|
64
106
|
t = 0;
|
|
65
107
|
panel.rebuild();
|
|
66
108
|
draw();
|
|
@@ -73,7 +115,8 @@ function applyMat(m: number[], x: number, y: number): [number, number] {
|
|
|
73
115
|
function opCorners(op: DisplayOp): [number, number][] {
|
|
74
116
|
switch (op.type) {
|
|
75
117
|
case "rect":
|
|
76
|
-
case "ellipse":
|
|
118
|
+
case "ellipse":
|
|
119
|
+
case "image": {
|
|
77
120
|
const { offsetX: x, offsetY: y, width: w, height: h } = op;
|
|
78
121
|
return [[x, y], [x + w, y], [x + w, y + h], [x, y + h]].map(([px, py]) =>
|
|
79
122
|
applyMat(op.transform, px!, py!),
|
|
@@ -91,12 +134,15 @@ function opCorners(op: DisplayOp): [number, number][] {
|
|
|
91
134
|
applyMat(op.transform, px!, py!),
|
|
92
135
|
);
|
|
93
136
|
}
|
|
137
|
+
case "path":
|
|
138
|
+
// No cheap bbox for an arbitrary `d`; mark the origin for selection.
|
|
139
|
+
return [applyMat(op.transform, 0, 0)];
|
|
94
140
|
}
|
|
95
141
|
}
|
|
96
142
|
|
|
97
143
|
function draw() {
|
|
98
144
|
if (!store) return;
|
|
99
|
-
renderFrame(ctx, store.compiled, t);
|
|
145
|
+
renderFrame(ctx, store.compiled, t, images);
|
|
100
146
|
|
|
101
147
|
if (store.selectedId) {
|
|
102
148
|
const ops = evaluate(store.compiled, t).filter((op) => op.id === store!.selectedId);
|
|
@@ -115,11 +161,70 @@ function draw() {
|
|
|
115
161
|
ctx.restore();
|
|
116
162
|
}
|
|
117
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
|
+
|
|
118
189
|
const duration = store.compiled.duration;
|
|
119
190
|
scrub.value = String(duration ? t / duration : 0);
|
|
120
191
|
timeLabel.textContent = `${t.toFixed(3)} / ${duration.toFixed(3)}`;
|
|
121
192
|
}
|
|
122
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
|
+
|
|
123
228
|
function tick(now: number) {
|
|
124
229
|
if (playing && store) {
|
|
125
230
|
t += (now - lastTick) / 1000;
|