@glissade/scene 0.54.0 → 0.55.0-pre.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.
- package/dist/describe.js +19 -1
- package/dist/identity.js +2 -1
- package/dist/motion.d.ts +105 -3
- package/dist/motion.js +272 -1
- package/dist/nodes.d.ts +12 -0
- package/dist/scene.js +2 -1
- package/package.json +2 -2
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.
|
|
26
|
+
const RAW_VERSION = "0.55.0-pre.1";
|
|
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 {
|
|
2
|
-
import {
|
|
1
|
+
import { C as Mat2x3, 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,105 @@ 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
|
-
|
|
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
|
+
declare function cameraLayerMatrix(size: {
|
|
186
|
+
w: number;
|
|
187
|
+
h: number;
|
|
188
|
+
}, centerRel: Vec2, zoom: number, roll: number, depth: number): Mat2x3;
|
|
189
|
+
declare class Camera extends Group {
|
|
190
|
+
#private;
|
|
191
|
+
get describeType(): string;
|
|
192
|
+
/** Focal / pan target, RELATIVE viewport coords. Track `cam/center(.x/.y)`. */
|
|
193
|
+
readonly center: Vec2Signal;
|
|
194
|
+
/** Scale about the focal point. Track `cam/zoom`. */
|
|
195
|
+
readonly zoom: BindableSignal<number>;
|
|
196
|
+
/** Camera roll, degrees. Track `cam/roll`. */
|
|
197
|
+
readonly roll: BindableSignal<number>;
|
|
198
|
+
/** Resolved layers (content + depth), parallel to `children`. */
|
|
199
|
+
readonly layers: readonly Required<CameraLayer>[];
|
|
200
|
+
/** The whole-frame shake spec, if any — read by exporters (render-only, so it is
|
|
201
|
+
* warned + not baked into Lottie keyframes). */
|
|
202
|
+
get shakeSpec(): ShakeSpec | undefined;
|
|
203
|
+
constructor(layers: CameraLayer[], props?: CameraProps);
|
|
204
|
+
protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Build a {@link Camera} rig (lowercase FACTORY — no `new`): `camera(layers, props?)`.
|
|
208
|
+
* `layers` are depth planes (`{ content, depth? }`); animate `cam/center(.x/.y)`,
|
|
209
|
+
* `cam/zoom`, `cam/roll` with tracks for push-ins, pans, and rolls.
|
|
210
|
+
*
|
|
211
|
+
* `children: [camera([{ content: bg, depth: 0.3 }, { content: fg }], { id: 'cam' }), caption]`
|
|
212
|
+
* — `caption` is a SIBLING (outside the rig), so it stays pinned while the camera moves.
|
|
213
|
+
*/
|
|
214
|
+
declare function camera(layers: CameraLayer[], props?: CameraProps): Camera;
|
|
215
|
+
//#endregion
|
|
216
|
+
export { Camera, CameraError, type CameraLayer, type CameraProps, FollowPath, type FollowPathProps, LookAt, type LookAtProps, OrientToPath, type OrientToPathProps, type PathSampler, type ShakeSpec, camera, cameraLayerMatrix, followPath, lookAt, motionPath, orientToPath, pathLength, pointAtLength, shake, shakeOffset };
|
package/dist/motion.js
CHANGED
|
@@ -1,2 +1,273 @@
|
|
|
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
|
-
|
|
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
|
+
/** The whole-frame shake spec, if any — read by exporters (render-only, so it is
|
|
200
|
+
* warned + not baked into Lottie keyframes). */
|
|
201
|
+
get shakeSpec() {
|
|
202
|
+
return this.#shake;
|
|
203
|
+
}
|
|
204
|
+
constructor(layers, props = {}) {
|
|
205
|
+
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).");
|
|
206
|
+
const resolved = layers.map((l, i) => {
|
|
207
|
+
if (l == null || !(l.content instanceof Node)) throw new CameraError(`camera(): layer ${i} has no \`content\` Node — each layer is { content: Node, depth?: number }.`);
|
|
208
|
+
const depth = l.depth ?? 1;
|
|
209
|
+
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).`);
|
|
210
|
+
return {
|
|
211
|
+
content: l.content,
|
|
212
|
+
depth
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
super({
|
|
216
|
+
...props,
|
|
217
|
+
children: resolved.map((l) => l.content)
|
|
218
|
+
});
|
|
219
|
+
this.layers = resolved;
|
|
220
|
+
this.center = vec2Signal([.5, .5]);
|
|
221
|
+
this.zoom = signal(1);
|
|
222
|
+
this.roll = signal(0);
|
|
223
|
+
initVec2(this.center, props.center);
|
|
224
|
+
initNum(this.zoom, props.zoom);
|
|
225
|
+
initNum(this.roll, props.roll);
|
|
226
|
+
this.registerTarget("center", this.center, "vec2");
|
|
227
|
+
this.registerTarget("center.x", this.center.x, "number");
|
|
228
|
+
this.registerTarget("center.y", this.center.y, "number");
|
|
229
|
+
this.registerTarget("zoom", this.zoom, "number");
|
|
230
|
+
this.registerTarget("roll", this.roll, "number");
|
|
231
|
+
this.#shake = props.shake;
|
|
232
|
+
}
|
|
233
|
+
draw(out, ctx) {
|
|
234
|
+
const size = ctx.size;
|
|
235
|
+
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 }.");
|
|
236
|
+
const [cx, cy] = this.center();
|
|
237
|
+
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.`);
|
|
238
|
+
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.`);
|
|
239
|
+
const zoom = this.zoom();
|
|
240
|
+
const roll = this.roll();
|
|
241
|
+
const centerRel = [cx, cy];
|
|
242
|
+
const shake = this.#shake ? shakeOffset(this.#shake, ctx.time) : {
|
|
243
|
+
dx: 0,
|
|
244
|
+
dy: 0,
|
|
245
|
+
dr: 0
|
|
246
|
+
};
|
|
247
|
+
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;
|
|
248
|
+
for (const layer of this.layers) {
|
|
249
|
+
const pose = cameraLayerMatrix(size, centerRel, zoom, roll, layer.depth);
|
|
250
|
+
const m = shakeM ? multiply(shakeM, pose) : pose;
|
|
251
|
+
out.push({ op: "save" });
|
|
252
|
+
out.push({
|
|
253
|
+
op: "transform",
|
|
254
|
+
m
|
|
255
|
+
});
|
|
256
|
+
layer.content.emit(out, ctx);
|
|
257
|
+
out.push({ op: "restore" });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* Build a {@link Camera} rig (lowercase FACTORY — no `new`): `camera(layers, props?)`.
|
|
263
|
+
* `layers` are depth planes (`{ content, depth? }`); animate `cam/center(.x/.y)`,
|
|
264
|
+
* `cam/zoom`, `cam/roll` with tracks for push-ins, pans, and rolls.
|
|
265
|
+
*
|
|
266
|
+
* `children: [camera([{ content: bg, depth: 0.3 }, { content: fg }], { id: 'cam' }), caption]`
|
|
267
|
+
* — `caption` is a SIBLING (outside the rig), so it stays pinned while the camera moves.
|
|
268
|
+
*/
|
|
269
|
+
function camera(layers, props = {}) {
|
|
270
|
+
return new Camera(layers, props);
|
|
271
|
+
}
|
|
272
|
+
//#endregion
|
|
273
|
+
export { Camera, CameraError, FollowPath, LookAt, OrientToPath, camera, cameraLayerMatrix, 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.
|
|
3
|
+
"version": "0.55.0-pre.1",
|
|
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.
|
|
80
|
+
"@glissade/core": "0.55.0-pre.1"
|
|
81
81
|
},
|
|
82
82
|
"repository": {
|
|
83
83
|
"type": "git",
|