@glissade/scene 0.54.0 → 0.55.0-pre.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/describe.js CHANGED
@@ -23,7 +23,7 @@ import { easings, listValueTypes } from "@glissade/core";
23
23
  * never pulled onto the base embed path — a scene that never calls `describe()`
24
24
  * pays zero bytes for it.
25
25
  */
26
- const RAW_VERSION = "0.54.0";
26
+ const RAW_VERSION = "0.55.0-pre.0";
27
27
  const PACKAGE_VERSION = RAW_VERSION.includes("GLISSADE_".concat("VERSION")) ? "0.0.0-dev" : RAW_VERSION;
28
28
  /**
29
29
  * Parse the documented positional-arg count from a helper `usage` string — the
@@ -570,6 +570,24 @@ const HELPERS = [
570
570
  import: "@glissade/scene",
571
571
  usage: "echo(child: Node, opts?: { id?, count?: number, spacing?: number, decay?: number }): Echo"
572
572
  },
573
+ {
574
+ name: "camera",
575
+ summary: "A cinematic camera rig (FACTORY, no `new`): a Group subclass that applies the inverse camera pose as a parent transform over layered content — push-ins, pans, rolls, and pan-only parallax by layer depth. The pose (center/zoom/roll) are keyframeable track targets; the world moves while nodes stay node-local (no double-apply with anchors). Captions belong as SIBLINGS of the camera (outside the rig) so they stay pinned. Tree-shakeable (@glissade/scene/motion).",
576
+ import: "@glissade/scene/motion",
577
+ usage: "camera(layers: { content: Node, depth? }[], props?: { id?, center?, zoom?, roll?, shake? }): Camera — center is RELATIVE viewport coords ([0.5,0.5]=center, never px); animate 'cam/center(.x/.y)', 'cam/zoom', 'cam/roll'. depth<1 = far (parallax pans less)."
578
+ },
579
+ {
580
+ name: "shake",
581
+ summary: "A standalone jitter driver (mutate-and-return, like orientToPath): wobbles ANY node’s pose with deterministic value noise, folded in at emit as a parent-space offset so it composes with whatever else drives the node. SEPARATE translate (px) / rotate (deg) / frequency (Hz) amplitudes; pure and byte-identical run-to-run (seeded, no Date/Math.random). Tree-shakeable (@glissade/scene/motion).",
582
+ import: "@glissade/scene/motion",
583
+ usage: "shake(node: Node, opts: { seed: number, translate?: number, rotate?: number, frequency?: number }): Node"
584
+ },
585
+ {
586
+ name: "valueNoise",
587
+ summary: "Closed-form smooth value noise: a PURE function of (seed, t) — lerp(rand(⌊t⌋), rand(⌊t⌋+1), smoothstep(fract t)) with core’s seeded hash. No state, no bake; deterministic by construction (byte-identical run-to-run), fps-independent, O(1), seekable — the closed-form sibling of a spring. Range [0,1); center a signed wobble with *2-1. The primitive behind shake + camera shake.",
588
+ import: "@glissade/core",
589
+ usage: "valueNoise(seed: number, t: number): number // jitterX = () => 3 * (valueNoise(7, t) * 2 - 1)"
590
+ },
573
591
  {
574
592
  name: "motionBlur",
575
593
  summary: "Real sampled motion blur: wrap a child so it renders at N sub-frame times across a shutter interval (centered on the frame) and AVERAGES them — tracks every animated prop, not a faked directional blur. A pure multi-time re-eval (playhead re-addressed per sample, running-mean opacity, restored), byte-exact on Skia; browser↔Skia is perceptual-tier for blur.",
package/dist/identity.js CHANGED
@@ -53,7 +53,8 @@ function emitWithIds(scene, doc, t) {
53
53
  time: t,
54
54
  frame: fps !== void 0 ? Math.round(t * fps) : -1,
55
55
  measurer: scene.textMeasurer,
56
- playhead: scene.playhead
56
+ playhead: scene.playhead,
57
+ size: scene.size
57
58
  };
58
59
  return evaluateAt(scene.playhead, t, () => {
59
60
  const builder = instrument(createDisplayListBuilder(scene.size));
package/dist/motion.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { B as Node, H as NodeProps, U as PropInit, l as Path } from "./nodes.js";
2
- import { BindableSignal, PathValue, Vec2 } from "@glissade/core";
1
+ import { r as DisplayListBuilder } from "./displayList.js";
2
+ import { B as Node, H as NodeProps, R as EvalContext, U as PropInit, a as Group, l as Path } from "./nodes.js";
3
+ import { BindableSignal, PathValue, Vec2, Vec2Signal } from "@glissade/core";
3
4
 
4
5
  //#region src/motionPath.d.ts
5
6
 
@@ -111,4 +112,99 @@ declare class LookAt extends Node {
111
112
  /** `children: [turret, mover, lookAt(turret, mover)]` — turret always faces the mover. */
112
113
  declare function lookAt(target: Node, at: Node, props?: Omit<LookAtProps, 'target' | 'at'>): LookAt;
113
114
  //#endregion
114
- export { FollowPath, type FollowPathProps, LookAt, type LookAtProps, OrientToPath, type OrientToPathProps, type PathSampler, followPath, lookAt, motionPath, orientToPath, pathLength, pointAtLength };
115
+ //#region src/shake.d.ts
116
+ interface ShakeSpec {
117
+ /** Seed for the deterministic noise — same seed ⇒ same wobble, every run. */
118
+ seed: number;
119
+ /** Peak translation amplitude in px (±); default 0 (no positional jitter). */
120
+ translate?: number;
121
+ /** Peak rotation amplitude in degrees (±); default 0 (no rotational jitter). */
122
+ rotate?: number;
123
+ /** Noise cycles per second (higher = twitchier); default 8. */
124
+ frequency?: number;
125
+ }
126
+ /**
127
+ * The pure per-time shake offset for a spec: `{ dx, dy }` px + `dr` degrees, each
128
+ * a deterministic function of `(seed, t)`. Both the {@link shake} node driver and
129
+ * the Camera whole-frame shake fold this in.
130
+ */
131
+ declare function shakeOffset(spec: ShakeSpec, t: number): {
132
+ dx: number;
133
+ dy: number;
134
+ dr: number;
135
+ };
136
+ /**
137
+ * A shake transform about the point `p` (parent space): translate by `(dx, dy)`
138
+ * then rotate `dr` degrees about `p`, so a rotational jitter spins the node around
139
+ * its own origin rather than the parent's.
140
+ */
141
+
142
+ /**
143
+ * Jitter `node`'s pose with deterministic value noise, then return it (mutate-and-
144
+ * return, like Grid/orientToPath). SEPARATE `translate` (px) / `rotate` (deg) /
145
+ * `frequency` (Hz) amplitudes; pass at least one nonzero amplitude. The jitter is
146
+ * a parent-space offset applied at emit, so it composes with any existing driver.
147
+ *
148
+ * `children: [shake(cursor, { seed: 7, translate: 3 })]` — the cursor wobbles ±3px
149
+ * around wherever else it is (its position track, a followPath, …).
150
+ */
151
+ declare function shake(node: Node, spec: ShakeSpec): Node;
152
+ //#endregion
153
+ //#region src/camera.d.ts
154
+ /** Thrown for a mis-built or off-safe-area camera (fail loud, never a silent no-op). */
155
+ declare class CameraError extends Error {
156
+ constructor(message: string);
157
+ }
158
+ /**
159
+ * One depth layer of a camera rig. `depth` lives on the WRAPPER (not a per-Node
160
+ * prop, so the base Node/golden contract is untouched): 1 = the focal plane
161
+ * (default), <1 = farther (parallax: pans less), >1 = nearer (pans more).
162
+ */
163
+ interface CameraLayer {
164
+ content: Node;
165
+ depth?: number;
166
+ }
167
+ interface CameraProps extends NodeProps {
168
+ /** Focal / pan target in RELATIVE viewport coords ([0.5,0.5]=center); default center. */
169
+ center?: PropInit<Vec2>;
170
+ /** Scale about the focal point; default 1. */
171
+ zoom?: PropInit<number>;
172
+ /** Camera roll in degrees; default 0. */
173
+ roll?: PropInit<number>;
174
+ /** Optional whole-frame shake folded into the pose. */
175
+ shake?: ShakeSpec;
176
+ }
177
+ /**
178
+ * The per-layer inverse-camera-pose matrix (pure — the render math, exported for
179
+ * unit tests). Maps WORLD → SCREEN as
180
+ * T(screenCenter) · scale(zoom) · rotate(roll) · T(−effectiveCenter)
181
+ * where `effectiveCenter = screenCenter + (focalPx − screenCenter)·depth` scales
182
+ * the PAN by the layer's depth (far layers, depth<1, pan less). `centerRel` is the
183
+ * RELATIVE focal point ([0.5,0.5]=screen center); `roll` is degrees.
184
+ */
185
+
186
+ declare class Camera extends Group {
187
+ #private;
188
+ get describeType(): string;
189
+ /** Focal / pan target, RELATIVE viewport coords. Track `cam/center(.x/.y)`. */
190
+ readonly center: Vec2Signal;
191
+ /** Scale about the focal point. Track `cam/zoom`. */
192
+ readonly zoom: BindableSignal<number>;
193
+ /** Camera roll, degrees. Track `cam/roll`. */
194
+ readonly roll: BindableSignal<number>;
195
+ /** Resolved layers (content + depth), parallel to `children`. */
196
+ readonly layers: readonly Required<CameraLayer>[];
197
+ constructor(layers: CameraLayer[], props?: CameraProps);
198
+ protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
199
+ }
200
+ /**
201
+ * Build a {@link Camera} rig (lowercase FACTORY — no `new`): `camera(layers, props?)`.
202
+ * `layers` are depth planes (`{ content, depth? }`); animate `cam/center(.x/.y)`,
203
+ * `cam/zoom`, `cam/roll` with tracks for push-ins, pans, and rolls.
204
+ *
205
+ * `children: [camera([{ content: bg, depth: 0.3 }, { content: fg }], { id: 'cam' }), caption]`
206
+ * — `caption` is a SIBLING (outside the rig), so it stays pinned while the camera moves.
207
+ */
208
+ declare function camera(layers: CameraLayer[], props?: CameraProps): Camera;
209
+ //#endregion
210
+ export { Camera, CameraError, type CameraLayer, type CameraProps, FollowPath, type FollowPathProps, LookAt, type LookAtProps, OrientToPath, type OrientToPathProps, type PathSampler, type ShakeSpec, camera, followPath, lookAt, motionPath, orientToPath, pathLength, pointAtLength, shake, shakeOffset };
package/dist/motion.js CHANGED
@@ -1,2 +1,268 @@
1
+ import { $ as multiply, C as Node, X as fromTRS, r as Group } from "./nodes.js";
1
2
  import { a as FollowPath, c as pathLength, i as orientToPath, l as pointAtLength, n as OrientToPath, o as followPath, r as lookAt, s as motionPath, t as LookAt } from "./orient.js";
2
- export { FollowPath, LookAt, OrientToPath, followPath, lookAt, motionPath, orientToPath, pathLength, pointAtLength };
3
+ import { signal, valueNoise, vec2Signal } from "@glissade/core";
4
+ //#region src/shake.ts
5
+ /**
6
+ * `shake` (0.55) — a standalone jitter driver that wobbles ANY node's pose with
7
+ * deterministic value noise. It subsumes the hand-rolled per-element jitters
8
+ * (desk-cursor jitterX/Y, glitch shakeAmp, typewriter jitterRate) behind one
9
+ * primitive with SEPARATE translate / rotate / frequency amplitudes.
10
+ *
11
+ * The jitter is realized AT EMIT (the Echo/MotionBlur idiom): `shake` overrides
12
+ * the node's `emit` to wrap it in a save → shake-transform → emit → restore, where
13
+ * the transform is a pure function of `ctx.time` via `valueNoise` — so it composes
14
+ * on top of WHATEVER already drives the node (keyframes, layout, followPath) as a
15
+ * parent-space offset, and stays byte-identical across two `evaluate()` passes (no
16
+ * cross-frame state, no `Date.now`/`Math.random`). The camera whole-frame shake
17
+ * reuses {@link shakeOffset} directly on its pose.
18
+ *
19
+ * Lives on `@glissade/scene/motion` (off the base embed). Note (like Echo/
20
+ * MotionBlur, both emit-time re-eval): the offset is not a Timeline TRACK, so it
21
+ * is a runtime/render effect — it is NOT emitted as animated Lottie keyframes.
22
+ */
23
+ /** Default temporal frequency (noise cycles per second) when `frequency` is unset. */
24
+ const DEFAULT_FREQUENCY = 8;
25
+ const K_Y = 101;
26
+ const K_ROT = 211;
27
+ /** Signed value noise in [-1, 1) — the bipolar form a shake offset needs. */
28
+ function snoise(seed, t) {
29
+ return valueNoise(seed, t) * 2 - 1;
30
+ }
31
+ /**
32
+ * The pure per-time shake offset for a spec: `{ dx, dy }` px + `dr` degrees, each
33
+ * a deterministic function of `(seed, t)`. Both the {@link shake} node driver and
34
+ * the Camera whole-frame shake fold this in.
35
+ */
36
+ function shakeOffset(spec, t) {
37
+ const tr = spec.translate ?? 0;
38
+ const rot = spec.rotate ?? 0;
39
+ const tf = t * (spec.frequency ?? DEFAULT_FREQUENCY);
40
+ return {
41
+ dx: tr === 0 ? 0 : tr * snoise(spec.seed, tf),
42
+ dy: tr === 0 ? 0 : tr * snoise(spec.seed + K_Y, tf),
43
+ dr: rot === 0 ? 0 : rot * snoise(spec.seed + K_ROT, tf)
44
+ };
45
+ }
46
+ /**
47
+ * A shake transform about the point `p` (parent space): translate by `(dx, dy)`
48
+ * then rotate `dr` degrees about `p`, so a rotational jitter spins the node around
49
+ * its own origin rather than the parent's.
50
+ */
51
+ function shakeMatrix(p, dx, dy, dr) {
52
+ const translate = [
53
+ 1,
54
+ 0,
55
+ 0,
56
+ 1,
57
+ dx,
58
+ dy
59
+ ];
60
+ if (dr === 0) return translate;
61
+ const toP = [
62
+ 1,
63
+ 0,
64
+ 0,
65
+ 1,
66
+ p[0],
67
+ p[1]
68
+ ];
69
+ const fromP = [
70
+ 1,
71
+ 0,
72
+ 0,
73
+ 1,
74
+ -p[0],
75
+ -p[1]
76
+ ];
77
+ return multiply(translate, multiply(toP, multiply(fromTRS([0, 0], dr, [1, 1]), fromP)));
78
+ }
79
+ /**
80
+ * Jitter `node`'s pose with deterministic value noise, then return it (mutate-and-
81
+ * return, like Grid/orientToPath). SEPARATE `translate` (px) / `rotate` (deg) /
82
+ * `frequency` (Hz) amplitudes; pass at least one nonzero amplitude. The jitter is
83
+ * a parent-space offset applied at emit, so it composes with any existing driver.
84
+ *
85
+ * `children: [shake(cursor, { seed: 7, translate: 3 })]` — the cursor wobbles ±3px
86
+ * around wherever else it is (its position track, a followPath, …).
87
+ */
88
+ function shake(node, spec) {
89
+ const tr = spec.translate ?? 0;
90
+ const rot = spec.rotate ?? 0;
91
+ if (tr === 0 && rot === 0) throw new Error("shake(): pass a nonzero `translate` (px) or `rotate` (deg) amplitude — both are 0/omitted, so nothing would move.");
92
+ const origEmit = node.emit.bind(node);
93
+ node.emit = (out, ctx) => {
94
+ const { dx, dy, dr } = shakeOffset(spec, ctx.time);
95
+ if (dx === 0 && dy === 0 && dr === 0) {
96
+ origEmit(out, ctx);
97
+ return;
98
+ }
99
+ out.push({ op: "save" });
100
+ out.push({
101
+ op: "transform",
102
+ m: shakeMatrix(node.position(), dx, dy, dr)
103
+ });
104
+ origEmit(out, ctx);
105
+ out.push({ op: "restore" });
106
+ };
107
+ return node;
108
+ }
109
+ //#endregion
110
+ //#region src/camera.ts
111
+ /**
112
+ * `Camera` (0.55) — a cinematic camera rig for cuts, push-ins, pans, rolls, and
113
+ * parallax over a layered scene. It is a `Group` subclass whose `emit` applies the
114
+ * INVERSE camera pose as a parent transform on its layers, so the WHOLE world moves
115
+ * under a fixed screen while every node stays NODE-LOCAL (its anchor lives in its
116
+ * own localMatrix) — the "camera transforms the world, nodes stay node-local"
117
+ * composition contract, no double-apply with the 0.53 anchor by construction.
118
+ *
119
+ * Pose (all keyframeable track targets — `cam/center(.x/.y)`, `cam/zoom`,
120
+ * `cam/roll`):
121
+ * - `center` — the focal / pan target in RELATIVE viewport coords ([0.5,0.5] =
122
+ * screen center), NEVER px (responsive landscape↔portrait). Resolved to world
123
+ * px at emit against `ctx.size`.
124
+ * - `zoom` — scale about the focal point (push-in when animated up).
125
+ * - `roll` — camera rotation in degrees.
126
+ * - `shake` — an optional whole-frame {@link ShakeSpec} folded into the pose.
127
+ *
128
+ * The per-layer transform is
129
+ * T(screenCenter) · scale(zoom) · rotate(roll) · T(−effectiveCenter)
130
+ * where a layer's `effectiveCenter` scales the PAN by its `depth` — far layers
131
+ * (depth<1) translate less (v1 parallax is pan-only; DoF-from-depth is deferred).
132
+ *
133
+ * CAPTION-PIN is STRUCTURAL, not a flag: captions belong as SIBLINGS of the Camera
134
+ * (outside the rig), so the camera transform never touches them — a lower-third
135
+ * stays pinned by construction. See golden-camera for the pattern.
136
+ *
137
+ * Lives on `@glissade/scene/motion` (off the base embed).
138
+ */
139
+ /** Thrown for a mis-built or off-safe-area camera (fail loud, never a silent no-op). */
140
+ var CameraError = class extends Error {
141
+ constructor(message) {
142
+ super(message);
143
+ this.name = "CameraError";
144
+ }
145
+ };
146
+ /**
147
+ * The per-layer inverse-camera-pose matrix (pure — the render math, exported for
148
+ * unit tests). Maps WORLD → SCREEN as
149
+ * T(screenCenter) · scale(zoom) · rotate(roll) · T(−effectiveCenter)
150
+ * where `effectiveCenter = screenCenter + (focalPx − screenCenter)·depth` scales
151
+ * the PAN by the layer's depth (far layers, depth<1, pan less). `centerRel` is the
152
+ * RELATIVE focal point ([0.5,0.5]=screen center); `roll` is degrees.
153
+ */
154
+ function cameraLayerMatrix(size, centerRel, zoom, roll, depth) {
155
+ const pw = size.w / 2;
156
+ const ph = size.h / 2;
157
+ const focalX = centerRel[0] * size.w;
158
+ const focalY = centerRel[1] * size.h;
159
+ const ecx = pw + (focalX - pw) * depth;
160
+ const ecy = ph + (focalY - ph) * depth;
161
+ const scaleRoll = fromTRS([0, 0], roll, [zoom, zoom]);
162
+ return multiply([
163
+ 1,
164
+ 0,
165
+ 0,
166
+ 1,
167
+ pw,
168
+ ph
169
+ ], multiply(scaleRoll, [
170
+ 1,
171
+ 0,
172
+ 0,
173
+ 1,
174
+ -ecx,
175
+ -ecy
176
+ ]));
177
+ }
178
+ function initVec2(sig, init) {
179
+ if (typeof init === "function") sig.bindSource(init);
180
+ else if (init !== void 0) sig.set(init);
181
+ }
182
+ function initNum(sig, init) {
183
+ if (typeof init === "function") sig.bindSource(init);
184
+ else if (init !== void 0) sig.set(init);
185
+ }
186
+ var Camera = class extends Group {
187
+ get describeType() {
188
+ return "Camera";
189
+ }
190
+ /** Focal / pan target, RELATIVE viewport coords. Track `cam/center(.x/.y)`. */
191
+ center;
192
+ /** Scale about the focal point. Track `cam/zoom`. */
193
+ zoom;
194
+ /** Camera roll, degrees. Track `cam/roll`. */
195
+ roll;
196
+ /** Resolved layers (content + depth), parallel to `children`. */
197
+ layers;
198
+ #shake;
199
+ constructor(layers, props = {}) {
200
+ if (!Array.isArray(layers) || layers.length === 0) throw new CameraError("camera(layers, props?): needs at least one layer — pass [{ content }] (a node per depth plane).");
201
+ const resolved = layers.map((l, i) => {
202
+ if (l == null || !(l.content instanceof Node)) throw new CameraError(`camera(): layer ${i} has no \`content\` Node — each layer is { content: Node, depth?: number }.`);
203
+ const depth = l.depth ?? 1;
204
+ if (!Number.isFinite(depth) || depth < 0) throw new CameraError(`camera(): layer ${i} has an invalid depth ${String(l.depth)} — depth must be a finite number >= 0 (1 = focal plane, <1 = far).`);
205
+ return {
206
+ content: l.content,
207
+ depth
208
+ };
209
+ });
210
+ super({
211
+ ...props,
212
+ children: resolved.map((l) => l.content)
213
+ });
214
+ this.layers = resolved;
215
+ this.center = vec2Signal([.5, .5]);
216
+ this.zoom = signal(1);
217
+ this.roll = signal(0);
218
+ initVec2(this.center, props.center);
219
+ initNum(this.zoom, props.zoom);
220
+ initNum(this.roll, props.roll);
221
+ this.registerTarget("center", this.center, "vec2");
222
+ this.registerTarget("center.x", this.center.x, "number");
223
+ this.registerTarget("center.y", this.center.y, "number");
224
+ this.registerTarget("zoom", this.zoom, "number");
225
+ this.registerTarget("roll", this.roll, "number");
226
+ this.#shake = props.shake;
227
+ }
228
+ draw(out, ctx) {
229
+ const size = ctx.size;
230
+ if (size === void 0) throw new CameraError("Camera needs the scene viewport — evaluate(scene, timeline, t) supplies ctx.size; a bare hand-built ctx must set { size }.");
231
+ const [cx, cy] = this.center();
232
+ if (!Number.isFinite(cx) || !Number.isFinite(cy)) throw new CameraError(`Camera center resolved to a non-finite value [${String(cx)}, ${String(cy)}] — center is RELATIVE viewport coords (e.g. [0.5,0.5]); check the bound source/track.`);
233
+ if (cx < 0 || cx > 1 || cy < 0 || cy > 1) throw new CameraError(`Camera center [${cx}, ${cy}] is outside the safe area [0,1]² — center is RELATIVE viewport coords; keep the pan target on-screen.`);
234
+ const zoom = this.zoom();
235
+ const roll = this.roll();
236
+ const centerRel = [cx, cy];
237
+ const shake = this.#shake ? shakeOffset(this.#shake, ctx.time) : {
238
+ dx: 0,
239
+ dy: 0,
240
+ dr: 0
241
+ };
242
+ const shakeM = shake.dx !== 0 || shake.dy !== 0 || shake.dr !== 0 ? shakeMatrix([size.w / 2, size.h / 2], shake.dx, shake.dy, shake.dr) : void 0;
243
+ for (const layer of this.layers) {
244
+ const pose = cameraLayerMatrix(size, centerRel, zoom, roll, layer.depth);
245
+ const m = shakeM ? multiply(shakeM, pose) : pose;
246
+ out.push({ op: "save" });
247
+ out.push({
248
+ op: "transform",
249
+ m
250
+ });
251
+ layer.content.emit(out, ctx);
252
+ out.push({ op: "restore" });
253
+ }
254
+ }
255
+ };
256
+ /**
257
+ * Build a {@link Camera} rig (lowercase FACTORY — no `new`): `camera(layers, props?)`.
258
+ * `layers` are depth planes (`{ content, depth? }`); animate `cam/center(.x/.y)`,
259
+ * `cam/zoom`, `cam/roll` with tracks for push-ins, pans, and rolls.
260
+ *
261
+ * `children: [camera([{ content: bg, depth: 0.3 }, { content: fg }], { id: 'cam' }), caption]`
262
+ * — `caption` is a SIBLING (outside the rig), so it stays pinned while the camera moves.
263
+ */
264
+ function camera(layers, props = {}) {
265
+ return new Camera(layers, props);
266
+ }
267
+ //#endregion
268
+ export { Camera, CameraError, FollowPath, LookAt, OrientToPath, camera, followPath, lookAt, motionPath, orientToPath, pathLength, pointAtLength, shake, shakeOffset };
package/dist/nodes.d.ts CHANGED
@@ -135,6 +135,18 @@ interface EvalContext {
135
135
  * playhead-dependent node degrades gracefully (Echo → a plain group).
136
136
  */
137
137
  readonly playhead?: Playhead;
138
+ /**
139
+ * The scene VIEWPORT size (`scene.size`) this evaluate targets — the only
140
+ * ambient-frame datum a node may read (a Camera needs the screen center to pan
141
+ * about / zoom into a RELATIVE focal point). OPTIONAL: `evaluate()`/
142
+ * `emitWithIds()` supply it; a bare hand-built ctx (a unit test emitting one
143
+ * node) may omit it, and a size-dependent node fails loud when it's absent.
144
+ * Reading it is byte-neutral for every existing node — nothing else consults it.
145
+ */
146
+ readonly size?: {
147
+ readonly w: number;
148
+ readonly h: number;
149
+ };
138
150
  }
139
151
  /** A property initializer: a value, or a computed source (§2.1). */
140
152
  type PropInit<T> = T | (() => T);
package/dist/scene.js CHANGED
@@ -110,7 +110,8 @@ function evaluate(scene, doc, t = 0) {
110
110
  time: t,
111
111
  frame: fps !== void 0 ? Math.round(t * fps) : -1,
112
112
  measurer: scene.textMeasurer,
113
- playhead: scene.playhead
113
+ playhead: scene.playhead,
114
+ size: scene.size
114
115
  };
115
116
  return evaluateAt(scene.playhead, t, () => {
116
117
  const out = createDisplayListBuilder(scene.size);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/scene",
3
- "version": "0.54.0",
3
+ "version": "0.55.0-pre.0",
4
4
  "description": "glissade scene graph: nodes, transforms, DisplayList emission. Renderer-agnostic; zero DOM/Node dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -77,7 +77,7 @@
77
77
  ],
78
78
  "dependencies": {
79
79
  "yoga-layout": "^3.2.1",
80
- "@glissade/core": "0.54.0"
80
+ "@glissade/core": "0.55.0-pre.0"
81
81
  },
82
82
  "repository": {
83
83
  "type": "git",