reframe-video 0.5.0 → 0.6.1

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.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The scene camera — a single affine viewport applied to the whole scene.
3
+ *
4
+ * Semantics (look-at): `camera.{x,y}` is the scene point centered in frame,
5
+ * `zoom` scales about it, `rotation` (degrees) turns about it. Defaults
6
+ * (`x=W/2, y=H/2, zoom=1, rotation=0`) are the identity, so a scene without a
7
+ * camera renders byte-identically.
8
+ *
9
+ * The camera is animated through the SAME machinery as nodes: tween / motionPath /
10
+ * behaviors targeting the reserved id `"camera"` with props x/y/zoom/rotation.
11
+ * `cameraTo` is a thin readable wrapper. Because those are ordinary labeled
12
+ * timeline steps, camera keyframes are overlay-addressable for free, so human
13
+ * edits survive AI regeneration.
14
+ */
15
+ import type { CameraIR, Ease, Size, TimelineIR } from "./ir.js";
16
+ import type { Mat2D } from "./evaluate.js";
17
+ /** Reserved timeline/behavior target id for the camera. */
18
+ export declare const CAMERA_ID = "camera";
19
+ /** The animatable camera props (look-at point + zoom + rotation). */
20
+ export declare const CAMERA_PROPS: readonly ["x", "y", "zoom", "rotation"];
21
+ /**
22
+ * The camera's affine matrix: `T(W/2,H/2) · R(rotation) · S(zoom) · T(-x,-y)`,
23
+ * i.e. center the focal point, then zoom/rotate about the frame centre. Defaults
24
+ * collapse to the identity.
25
+ */
26
+ export declare function cameraMatrix(cam: CameraIR, size: Size): Mat2D;
27
+ /** Keyframe the camera: a `tween` on the reserved "camera" target. */
28
+ export declare function cameraTo(props: CameraIR, opts?: {
29
+ duration?: number;
30
+ ease?: Ease;
31
+ label?: string;
32
+ }): TimelineIR;
@@ -49,5 +49,7 @@ export interface CompiledScene {
49
49
  labelTimes: Map<string, LabelSpan>;
50
50
  /** The subset of label spans that come from beat nodes — keyed by beat name. */
51
51
  beatTimes: Map<string, LabelSpan>;
52
+ /** True iff the scene declares or animates a `camera` (gates the camera matrix). */
53
+ hasCamera: boolean;
52
54
  }
53
55
  export declare function compileScene(ir: SceneIR): CompiledScene;
@@ -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, CompositionIR, CompositionSceneEntry, Ease, EllipseProps, GroupProps, ImageProps, LineProps, NodeIR, PathProps, PropValue, RectProps, SceneIR, Size, StateOverride, TextProps, TimelineIR } from "./ir.js";
5
+ import type { AudioIR, BehaviorIR, CameraIR, CompositionIR, CompositionSceneEntry, 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;
@@ -10,6 +10,7 @@ export interface SceneInput {
10
10
  duration?: number;
11
11
  background?: string;
12
12
  nodes: NodeIR[];
13
+ camera?: CameraIR;
13
14
  states?: Record<string, StateOverride>;
14
15
  initial?: string;
15
16
  timeline?: TimelineIR;
@@ -4,7 +4,7 @@
4
4
  * always. Renderers only draw; they never compute animation.
5
5
  */
6
6
  import type { CompiledScene } from "./compile.js";
7
- import type { ClipShape, PropValue } from "./ir.js";
7
+ import type { ClipShape, Paint, PropValue } from "./ir.js";
8
8
  /** Canvas-style 2D affine matrix [a, b, c, d, e, f]. */
9
9
  export type Mat2D = [number, number, number, number, number, number];
10
10
  /** A clip from an ancestor group: its shape in the group's coordinate space,
@@ -31,8 +31,8 @@ export type DisplayOp = (OpBase & {
31
31
  height: number;
32
32
  offsetX: number;
33
33
  offsetY: number;
34
- fill?: string;
35
- stroke?: string;
34
+ fill?: Paint;
35
+ stroke?: Paint;
36
36
  strokeWidth?: number;
37
37
  radius?: number;
38
38
  }) | (OpBase & {
@@ -41,8 +41,8 @@ export type DisplayOp = (OpBase & {
41
41
  height: number;
42
42
  offsetX: number;
43
43
  offsetY: number;
44
- fill?: string;
45
- stroke?: string;
44
+ fill?: Paint;
45
+ stroke?: Paint;
46
46
  strokeWidth?: number;
47
47
  }) | (OpBase & {
48
48
  type: "line";
@@ -76,9 +76,11 @@ export type DisplayOp = (OpBase & {
76
76
  d: string;
77
77
  /** 0..1 fraction of the stroke outline drawn (draw-on). */
78
78
  progress: number;
79
- fill?: string;
80
- stroke?: string;
79
+ fill?: Paint;
80
+ stroke?: Paint;
81
81
  strokeWidth?: number;
82
+ /** Local-space bbox [x,y,w,h] for mapping a gradient paint (set only when one is used). */
83
+ bbox?: [number, number, number, number];
82
84
  });
83
85
  export type DisplayList = DisplayOp[];
84
86
  /**
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Gradient paints — the structured alternative to a solid color `fill`/`stroke`.
3
+ *
4
+ * Coordinates are normalized to the node's bounding box (0..1), so a gradient is
5
+ * just an angle + color stops, independent of the node's size. The renderer maps
6
+ * 0..1 → the node's local box and the paint is applied in node-local space, so
7
+ * rotating/scaling the NODE moves the gradient with it (the "animated gradient"
8
+ * idiom — gradients themselves are static this version). Pure + deterministic.
9
+ */
10
+ import type { ColorStop, Gradient, Paint } from "./ir.js";
11
+ /** True when a paint is a gradient object (vs a plain color string). */
12
+ export declare function isGradient(p: Paint | undefined): p is Gradient;
13
+ /** A linear gradient. `angle` in degrees: 0 = left→right, 90 = top→bottom. */
14
+ export declare function linearGradient(stops: (string | ColorStop)[], opts?: {
15
+ angle?: number;
16
+ }): Gradient;
17
+ /** A radial gradient. `cx/cy` centre (0..1 of the box, default 0.5), `r` radius (0..1, default 0.5). */
18
+ export declare function radialGradient(stops: (string | ColorStop)[], opts?: {
19
+ cx?: number;
20
+ cy?: number;
21
+ r?: number;
22
+ }): Gradient;
23
+ /** A conic (angular sweep) gradient. `angle` start in degrees, `cx/cy` centre (0..1). */
24
+ export declare function conicGradient(stops: (string | ColorStop)[], opts?: {
25
+ angle?: number;
26
+ cx?: number;
27
+ cy?: number;
28
+ }): Gradient;
@@ -5,6 +5,8 @@ export { compileComposition, type CompiledComposition, type ScenePlacement, } fr
5
5
  export { composeScene, formatComposeReport, type OverlayDoc, type ComposeReport, } from "./compose.js";
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
+ export { cameraTo, cameraMatrix, CAMERA_ID, CAMERA_PROPS } from "./camera.js";
9
+ export { linearGradient, radialGradient, conicGradient, isGradient } from "./gradient.js";
8
10
  export { motionPreset, PRESET_NAMES, type PresetName, type PresetRig, type PresetOpts } from "./presets.js";
9
11
  export { devicePreset, deviceScreen, deviceScreenCenter, deviceBounds, deviceScreenPoint, DEVICE_PRESET_NAMES, type DevicePresetName, type DevicePresetOpts } from "./devicePreset.js";
10
12
  export { cursor, cursorTo, cursorPath, cursorClick, cursorDouble, type CursorStyle, type CursorOpts, type CursorToOpts, type CursorPathOpts, type CursorClickOpts } from "./cursor.js";
@@ -31,20 +31,54 @@ export interface BaseProps {
31
31
  skewX?: number;
32
32
  skewY?: number;
33
33
  anchor?: Anchor;
34
+ /**
35
+ * Pin a TOP-LEVEL node to the screen so the scene `camera` does not move it —
36
+ * for HUD / titles / watermark layers. No-op when the scene has no camera.
37
+ */
38
+ fixed?: boolean;
39
+ }
40
+ /**
41
+ * A paint is a solid color string OR a gradient. Coordinates are normalized to the
42
+ * node's bounding box (0..1, SVG `objectBoundingBox` style) so a gradient is just an
43
+ * angle + stops, size-independent. Applied in node-local space, so animating the
44
+ * node's transform (rotation/scale) moves the gradient with it. Build with
45
+ * `linearGradient`/`radialGradient`/`conicGradient` (`gradient.ts`).
46
+ */
47
+ export interface ColorStop {
48
+ offset: number;
49
+ color: string;
34
50
  }
51
+ export type Gradient = {
52
+ kind: "linear";
53
+ angle?: number;
54
+ stops: ColorStop[];
55
+ } | {
56
+ kind: "radial";
57
+ cx?: number;
58
+ cy?: number;
59
+ r?: number;
60
+ stops: ColorStop[];
61
+ } | {
62
+ kind: "conic";
63
+ angle?: number;
64
+ cx?: number;
65
+ cy?: number;
66
+ stops: ColorStop[];
67
+ };
68
+ export type Paint = string | Gradient;
35
69
  export interface RectProps extends BaseProps {
36
70
  width: number;
37
71
  height: number;
38
- fill?: string;
39
- stroke?: string;
72
+ fill?: Paint;
73
+ stroke?: Paint;
40
74
  strokeWidth?: number;
41
75
  radius?: number;
42
76
  }
43
77
  export interface EllipseProps extends BaseProps {
44
78
  width: number;
45
79
  height: number;
46
- fill?: string;
47
- stroke?: string;
80
+ fill?: Paint;
81
+ stroke?: Paint;
48
82
  strokeWidth?: number;
49
83
  }
50
84
  export interface LineProps {
@@ -57,12 +91,16 @@ export interface LineProps {
57
91
  opacity?: number;
58
92
  /** 0..1 — how much of the line is drawn (for draw-on effects). */
59
93
  progress?: number;
94
+ /** Pin to the screen so the scene `camera` does not move it (top-level only). */
95
+ fixed?: boolean;
60
96
  }
61
97
  export interface TextProps extends BaseProps {
62
98
  /** Numbers interpolate (count-up) and render via toFixed(contentDecimals). */
63
99
  content: string | number;
64
100
  /** Decimal places when content is numeric (default 0). */
65
101
  contentDecimals?: number;
102
+ /** Group the integer part with thousands separators (e.g. 35,786). */
103
+ contentThousands?: boolean;
66
104
  fontFamily: string;
67
105
  fontSize: number;
68
106
  fontWeight?: number;
@@ -95,8 +133,8 @@ export interface GroupProps extends BaseProps {
95
133
  export interface PathProps extends BaseProps {
96
134
  /** SVG path data (the `d` attribute). Drawn as a true vector — crisp at any zoom. */
97
135
  d: string;
98
- fill?: string;
99
- stroke?: string;
136
+ fill?: Paint;
137
+ stroke?: Paint;
100
138
  strokeWidth?: number;
101
139
  /**
102
140
  * 0..1 — fraction of the OUTLINE drawn, for a self-drawing "draw-on" effect
@@ -302,6 +340,19 @@ export interface AudioIR {
302
340
  };
303
341
  cues?: AudioCueIR[];
304
342
  }
343
+ /**
344
+ * The scene camera: a viewport over the whole scene. `(x,y)` is the scene point
345
+ * centered in frame (defaults to the frame centre), `zoom` scales about it,
346
+ * `rotation` (degrees) turns about it. Defaults (`x=W/2, y=H/2, zoom=1, rotation=0`)
347
+ * are the identity. Animate it by tweening the reserved target `"camera"`
348
+ * (or the `cameraTo` helper); pin layers out of it with a node's `fixed` flag.
349
+ */
350
+ export interface CameraIR {
351
+ x?: number;
352
+ y?: number;
353
+ zoom?: number;
354
+ rotation?: number;
355
+ }
305
356
  export interface SceneIR {
306
357
  version: 1;
307
358
  id: string;
@@ -312,6 +363,8 @@ export interface SceneIR {
312
363
  duration?: number;
313
364
  background?: string;
314
365
  nodes: NodeIR[];
366
+ /** A viewport over the scene, keyframable via the reserved target "camera". */
367
+ camera?: CameraIR;
315
368
  states?: Record<string, StateOverride>;
316
369
  /** State applied at t=0. */
317
370
  initial?: string;
@@ -9,6 +9,12 @@
9
9
  * spacing ever overshoots.
10
10
  */
11
11
  export type Pt = [number, number];
12
+ /**
13
+ * A loose bounding box `[x, y, w, h]` from a path `d`'s coordinate extents — used
14
+ * only to map a gradient across the shape. Exact for M/L/C/Q/S/T paths (every
15
+ * number is an x/y coord, control points included); loose for the rare H/V/A.
16
+ */
17
+ export declare function pathBBox(d: string): [number, number, number, number];
12
18
  /**
13
19
  * Position on the spline at progress u in [0,1]. `curviness` scales the
14
20
  * Catmull-Rom tangents (GSAP's idea): 1 = standard smooth (the default and the
@@ -121,6 +121,57 @@ bound — e.g. a pulse only during the hold:
121
121
  `oscillate("title", "scale", { amplitude: 0.04, frequency: 1.2 }, { from: 1.5, until: 3.5 })`.
122
122
  Omit the window to run for the whole scene.
123
123
 
124
+ ## Camera (one keyframable viewport)
125
+
126
+ A scene-level camera moves the whole scene at once: a look-at point + zoom +
127
+ rotation, animated over the timeline. Add it as a top-level `camera` field and
128
+ keyframe it with `cameraTo` (or by tweening the reserved target `"camera"`):
129
+
130
+ ```ts
131
+ scene({
132
+ // ...
133
+ camera: { x: W/2, y: H/2, zoom: 1, rotation: 0 }, // (x,y) = scene point centred in frame; defaults = frame centre, zoom 1, rot 0 (= no camera)
134
+ timeline: seq(
135
+ cameraTo({ x: 300, y: 400, zoom: 4 }, { duration: 1.5, ease: "easeInOutCubic", label: "push-in" }), // zoom into a detail
136
+ cameraTo({ x: 800, y: 200, zoom: 2, rotation: -5 }, { duration: 1.2 }), // pan + slight bank
137
+ cameraTo({ x: W/2, y: H/2, zoom: 1, rotation: 0 }, { duration: 1.6 }), // pull back
138
+ ),
139
+ })
140
+ ```
141
+
142
+ - `cameraTo(props, { duration?, ease?, label? })` keyframes the camera; it is a
143
+ `tween` on the `"camera"` target, so `motionPath("camera", pts, …)` (pan along
144
+ a curve) and `oscillate/wiggle("camera", "rotation"|"x"|…)` (handheld drift)
145
+ also work.
146
+ - **Pin HUD/titles to the screen** with `fixed: true` on a TOP-LEVEL node — the
147
+ camera won't move it (for overlays, watermarks, captions).
148
+ - Defaults are the identity, so a scene without a camera is unchanged. Don't name
149
+ a node `"camera"` if you use the scene camera (the id can't be both).
150
+
151
+ See `examples/scenes/camera-demo.ts`.
152
+
153
+ ## Gradients (fill / stroke)
154
+
155
+ `fill` and `stroke` on **rect / ellipse / path** accept a gradient as well as a
156
+ color string. Coordinates are normalized to the node's bounding box (0..1), so a
157
+ gradient is just an angle + stops:
158
+
159
+ ```ts
160
+ rect({ id: "card", x, y, width: 300, height: 300, anchor: "center",
161
+ fill: linearGradient(["#FF5C3A", "#FFC24B"], { angle: 60 }) }) // 0=L→R, 90=T→B
162
+ ellipse({ id: "orb", /* … */ fill: radialGradient(["#9B7CFF", "#221A4A"], { cx: 0.4, cy: 0.4, r: 0.6 }) })
163
+ path({ id: "star", d, fill: conicGradient(["#00C2A8", "#3AA0FF", "#7C5CFF", "#00C2A8"], { angle: -90 }) })
164
+ ellipse({ id: "ring", /* … */ fill: "none", stroke: linearGradient(["#3AA0FF", "#46E5A0"]), strokeWidth: 10 })
165
+ ```
166
+
167
+ - `linearGradient(stops, { angle })`, `radialGradient(stops, { cx, cy, r })`,
168
+ `conicGradient(stops, { angle, cx, cy })`. `stops` is a color array (even offsets)
169
+ or `[{ offset, color }]`. `cx/cy/r` are 0..1 of the box (centre defaults to 0.5).
170
+ - **Gradients are static** (not keyframed). The gradient lives in the node's local
171
+ space, so **animate the NODE** (`tween(id, { rotation: 360 })`, scale, move) and the
172
+ gradient sweeps/stretches with it. Color-string fills still tween as today.
173
+ - text fill and line stroke are color-only for now. See `examples/scenes/gradient-demo.ts`.
174
+
124
175
  ## Character rig (skeleton, poses, IK)
125
176
 
126
177
  A first-class, declarative character rig that **compiles to plain IR** (nested
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reframe-video",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
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",