@zakkster/lite-camera-pro 1.1.0 → 1.2.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/CHANGELOG.md +77 -0
- package/README.md +1 -1
- package/llms.txt +43 -4
- package/package.json +1 -1
- package/src/CinematicCameraPro.js +169 -14
- package/src/MultiTarget.js +8 -0
- package/src/Shake.d.ts +6 -0
- package/src/ShakeEngine.js +43 -19
- package/src/ShakePresets.js +15 -0
- package/src/index.d.ts +27 -0
- package/src/index.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,82 @@ Versioning. Version lives in three places at once -- `package.json`, the
|
|
|
6
6
|
`VERSION` const in `src/index.js`, and the `Version:` header in `llms.txt` --
|
|
7
7
|
bumped together or not at all.
|
|
8
8
|
|
|
9
|
+
## [1.2.0] -- 2026-08-30
|
|
10
|
+
|
|
11
|
+
Fail-closed doors. Three reproduced ways ordinary runtime garbage permanently
|
|
12
|
+
broke the render are closed at the entry, not patched in the body: a NaN dt that
|
|
13
|
+
poisoned shake and camera forever (CP-3), a frame-time spike that diverged the
|
|
14
|
+
position lerp (CP-4), and garbage into seven facade entries that crashed or froze
|
|
15
|
+
at frame N+1 (CP-12, CP-19). The hot bodies are untouched: the only additions to
|
|
16
|
+
an update path are two 2-line entry doors (`update()`, `updateShake()`), so the
|
|
17
|
+
T6 alloc gate still holds at maxMajor 0 / maxPauseMs 4 with the shake and parallax
|
|
18
|
+
pools pinned by identity. No new exports; the T8 main-entry surface is unchanged
|
|
19
|
+
but for the VERSION value. Full policy in `decisions/0002-dt-policy.md` (repo-only).
|
|
20
|
+
|
|
21
|
+
Credit: the ROADMAP audit catalogued CP-3/CP-4/CP-12; the PRO1 qa pass surfaced
|
|
22
|
+
the CP-19 facade over-reads.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **`cam.maxDt` tunable (default 0.1s).** `update()` clamps a finite dt above this
|
|
27
|
+
ceiling before integrating so a frame-time spike cannot diverge the position
|
|
28
|
+
lerp; a dt exactly == maxDt passes untouched. A plain field beside `lerpSpeed`,
|
|
29
|
+
not a per-frame-validated input (H-C).
|
|
30
|
+
- **Fail-closed doors** on `setMode`, `setState`, `setZoom`, `zoomAt`,
|
|
31
|
+
`trackMultiple`, `setTargetCount` (facade) and `registerPreset` (shake
|
|
32
|
+
registry). Setters validate at the call so a defect fails loud there instead of
|
|
33
|
+
as a raw crash on the next frame. Shake profiles now finiteness-check every
|
|
34
|
+
numeric (decay/freq/maxOffset/maxAngle/dirX/dirY, not just trauma/intensity) in
|
|
35
|
+
the cold entry; the `profile.dirX || 0` NaN-laundering is removed.
|
|
36
|
+
- **Five error codes** (house style, `.code` on a named Error): `ERR_CAMERA_MODE`,
|
|
37
|
+
`ERR_CAMERA_STATE`, `ERR_CAMERA_ZOOM`, `ERR_CAMERA_TARGETS`, `ERR_SHAKE_PRESET`.
|
|
38
|
+
Documented in `llms.txt`; a metadata drift guard asserts every code greppable in
|
|
39
|
+
`src/` is documented and vice versa (both directions, fail closed).
|
|
40
|
+
- **dt policy decision record.** `decisions/0002-dt-policy.md` adopts reject
|
|
41
|
+
(Policy A) + clamp (Policy B) and records the rejection of an exponential-damping
|
|
42
|
+
rewrite (Policy C) by measurement. Repo-only; not shipped in the tarball.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **Measured door cost in the subpath weights** (esm, unminified, gzip -9):
|
|
47
|
+
`./shake` 2.82 -> 3.01 KB gz (the addShake full-profile guard + the preset
|
|
48
|
+
registry doors), `./sequence` 5.94 -> 6.01 KB gz (drags the preset registry),
|
|
49
|
+
`.` 21.70 -> 23.19 KB gz (all doors + their JSDoc). `./parallax`, `./bounds`,
|
|
50
|
+
`./multi`, `./follow` unchanged. The `./shake` budget gate (16384 B) holds at
|
|
51
|
+
3082 B.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- **CP-3 -- a NaN dt no longer poisons the shake engine forever.** Before:
|
|
56
|
+
`updateShake(state, NaN)` drove `time`/`trauma` to NaN, the `trauma <= 0` test
|
|
57
|
+
never fired, and `computeShake` emitted NaN every later frame; after one poison
|
|
58
|
+
frame plus 10k good frames the slot was still active with a NaN offset. Now the
|
|
59
|
+
reject door makes that frame a no-op and the slot decays to `active === false`,
|
|
60
|
+
`offsetX === 0`. The same poison via a profile (a NaN `decay`) is closed by the
|
|
61
|
+
addShake full-profile finiteness check.
|
|
62
|
+
- **CP-4 -- a dt spike no longer diverges the position lerp.** Before: 40 frames
|
|
63
|
+
of `dt = 0.5` with `BoundsType.NONE` blew `pos` past 1e6 (the explicit
|
|
64
|
+
integrator is unstable for `lerpSpeed * dt > 2`). Now `update()` clamps dt to
|
|
65
|
+
`maxDt`, bounding `lerpSpeed * dt <= 0.5` at defaults; pos stays in the world
|
|
66
|
+
envelope. The exponential-damping alternative was rejected: measured against the
|
|
67
|
+
linear lerp at `lerpSpeed = 5` over 600 frames it drifts 15.884 px at dt = 1/60
|
|
68
|
+
and 32.981 px at dt = 1/30 -- four to five orders of magnitude above the f32
|
|
69
|
+
position-storage noise (~1.19e-4 px at a 1000 px offset), a visible change to how
|
|
70
|
+
valid frames feel (`decisions/0002-dt-policy.md`).
|
|
71
|
+
- **CP-12 -- garbage into the facade fails loud, not at frame N+1.** Before:
|
|
72
|
+
`setMode(99)` left the strategy lookup undefined and the next `update()` threw a
|
|
73
|
+
raw un-coded TypeError; `setState({ zoom: 0 })` skipped the setZoom clamp and set
|
|
74
|
+
`visibleW` to Infinity; `setState({ zoom: NaN })` emitted `scale(NaN)` (a black
|
|
75
|
+
screen, no error); `shakePreset(undefined)` threw a raw TypeError from
|
|
76
|
+
`name.toLowerCase()`; `setZoom(NaN)` set zoom to NaN. Now each rejects at its door
|
|
77
|
+
(`ERR_CAMERA_MODE` / `ERR_CAMERA_STATE` / `ERR_CAMERA_ZOOM`) or is a documented
|
|
78
|
+
no-op (`shakePreset` unknown name), and `setState({ zoom: 0 })` clamps to 0.25.
|
|
79
|
+
- **CP-19 -- multi-target over-reads are unreachable.** Before: `setTargetCount(64)`
|
|
80
|
+
on 2 targets, or `trackMultiple` with a garbage entry, crashed `updateMultiTarget`
|
|
81
|
+
at frame N+1 reading `.x` on undefined. Now `trackMultiple` validates the array and
|
|
82
|
+
every entry at call time and `setTargetCount` bounds the count to the array length,
|
|
83
|
+
both throwing `ERR_CAMERA_TARGETS`; the facade over-read is unreachable.
|
|
84
|
+
|
|
9
85
|
## [1.1.0] -- 2026-08-26
|
|
10
86
|
|
|
11
87
|
Subpath exports. A consumer who needs only screen shake now imports
|
|
@@ -118,5 +194,6 @@ modes, multi-target auto-framing, an 8-slot simplex-noise shake engine with
|
|
|
118
194
|
presets, fluent timeline sequences, a 16-layer parallax manager, per-edge
|
|
119
195
|
bounds, a debug HUD, and zero-alloc coordinate conversion.
|
|
120
196
|
|
|
197
|
+
[1.2.0]: https://github.com/PeshoVurtoleta/lite-camera-pro/releases/tag/v1.2.0
|
|
121
198
|
[1.0.1]: https://github.com/PeshoVurtoleta/lite-camera-pro/releases/tag/v1.0.1
|
|
122
199
|
[1.0.0]: https://github.com/PeshoVurtoleta/lite-camera-pro/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ npm install @zakkster/lite-camera-pro
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Need only screen shake? Import the `./shake` subpath and pull just the engine +
|
|
24
|
-
presets (
|
|
24
|
+
presets (3.01 KB gz -- esm, unminified, gzip -9):
|
|
25
25
|
|
|
26
26
|
```js
|
|
27
27
|
import { createShakeState, addShake, updateShake, computeShake, getPreset } from '@zakkster/lite-camera-pro/shake';
|
package/llms.txt
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
> with presets, fluent timeline sequences, a 16-layer parallax manager, per-edge
|
|
7
7
|
> bounds, a debug HUD, and zero-alloc coordinate conversion.
|
|
8
8
|
|
|
9
|
-
Version: 1.
|
|
9
|
+
Version: 1.2.0
|
|
10
10
|
|
|
11
11
|
ESM only. `import` from `@zakkster/lite-camera-pro`. Default export is the
|
|
12
12
|
CinematicCameraPro class. All hot-path APIs avoid allocation; setup-time calls
|
|
@@ -112,19 +112,58 @@ Enums: FollowMode, FOLLOW_STRATEGIES, BoundsType, WrapMode
|
|
|
112
112
|
## Subpath exports (import only what you use; gz, esm, unminified, gzip -9)
|
|
113
113
|
Beside the "." main entry, six subsystem subpaths + "./package.json". Same runtime
|
|
114
114
|
identities as the main entry (one engine, no fork). Per-subpath gz weight:
|
|
115
|
-
./shake
|
|
115
|
+
./shake 3.01 KB createShakeState, addShake, addTraumaSimple, updateShake, computeShake, clearShakes + presets/getPreset/registerPreset/listPresets
|
|
116
116
|
./parallax 0.89 KB createParallaxState, addParallaxLayer, removeParallaxLayer, updateParallax, getLayerScroll, applyParallaxLayer, WrapMode
|
|
117
117
|
./bounds 1.13 KB createBoundsState, setBoundsAll, setBoundsEdges, setBoundsRect, clearBoundsRect, applyBounds, BoundsType
|
|
118
118
|
./multi 0.89 KB createMultiTargetState, updateMultiTarget
|
|
119
119
|
./follow 0.71 KB FollowMode, FOLLOW_STRATEGIES
|
|
120
|
-
./sequence
|
|
121
|
-
.
|
|
120
|
+
./sequence 6.01 KB createCameraSequence, panTo, dramaticZoom, bossReveal, timedShake (drags lite-timeline + lite-ease by design -- the only subpath that does)
|
|
121
|
+
. 23.19 KB full class + functional layer
|
|
122
122
|
Self-reference example (shake only):
|
|
123
123
|
import { createShakeState, addShake, updateShake, computeShake, getPreset } from '@zakkster/lite-camera-pro/shake';
|
|
124
124
|
|
|
125
125
|
## Dependencies (all @zakkster, first-party)
|
|
126
126
|
@zakkster/lite-camera (base CinematicCamera), lite-ease, lite-lerp, lite-noise, lite-timeline.
|
|
127
127
|
|
|
128
|
+
## Fail-closed doors (1.2.0)
|
|
129
|
+
Ordinary runtime garbage (a NaN dt, a frame-time spike, a bad save file) can no longer
|
|
130
|
+
break the render at frame N+1. Setters validate at the call; the update path validates at
|
|
131
|
+
its entry; every unverified numeric fails loud or is a documented no-op. Hot bodies pay
|
|
132
|
+
nothing (the doors are two comparisons on the update path; errors build only on cold throws).
|
|
133
|
+
|
|
134
|
+
dt policy (cam.update(dt, ...) and standalone updateShake(state, dt)):
|
|
135
|
+
- Non-finite (NaN/+-Infinity/null) or negative dt -> documented no-op, nothing mutated.
|
|
136
|
+
- dt 0 and -0 -> legal no-advance frames (zero deltas everywhere).
|
|
137
|
+
- cam.update clamps a finite dt above cam.maxDt (default 0.1s, a plain tunable) to cam.maxDt;
|
|
138
|
+
a dt exactly == maxDt passes untouched. updateShake is reject-only (no maxDt; a large finite
|
|
139
|
+
dt is self-limiting). Set cam.maxDt directly to retune; it is not validated per frame.
|
|
140
|
+
|
|
141
|
+
Shake profiles (cam.shake / addShake): every numeric (trauma, decay, freq, maxOffset,
|
|
142
|
+
maxAngle, dirX, dirY) is resolved to its default then finiteness-checked in the cold entry;
|
|
143
|
+
a non-finite field activates NO slot (a NaN decay would otherwise leave a slot alive forever).
|
|
144
|
+
|
|
145
|
+
Error codes (all throw a house-style Error with a .code):
|
|
146
|
+
- ERR_CAMERA_MODE setMode(mode): mode must be an integer FollowMode in [0, 4].
|
|
147
|
+
- ERR_CAMERA_STATE setState(snapshot): non-null object; posX/posY both-or-neither,
|
|
148
|
+
targetX/targetY both-or-neither; every present numeric finite; zoom
|
|
149
|
+
finite then clamped (zoom 0 -> minZoom 0.25, not an error); mode an
|
|
150
|
+
integer FollowMode in range. Validated in full before any write
|
|
151
|
+
(a rejected snapshot mutates nothing). Snapshot is pose-only
|
|
152
|
+
(pos/target/zoom/mode) -- shake, sequences, zoom animations are not
|
|
153
|
+
serialized.
|
|
154
|
+
- ERR_CAMERA_ZOOM setZoom(level, duration): level finite; duration finite and >= 0
|
|
155
|
+
(0 = instant). zoomAt: anchor x/y, level, duration finite (>= 0);
|
|
156
|
+
a non-function ease normalizes to null (both call forms).
|
|
157
|
+
- ERR_CAMERA_TARGETS trackMultiple(targets): array; every entry an object with finite x/y
|
|
158
|
+
(validated at call time; empty array legal, count 0). setTargetCount(n):
|
|
159
|
+
integer in [0, targets.length].
|
|
160
|
+
- ERR_SHAKE_PRESET registerPreset(name, profile): non-empty string name + object profile
|
|
161
|
+
(setup path fails loud).
|
|
162
|
+
- ERR_CAMERA_DESTROYED every public method after destroy() (1.0.1).
|
|
163
|
+
getPreset(name): a non-string or unknown name returns null (never throws) -- the event path.
|
|
164
|
+
cam.shakePreset(name): an unknown or non-string name is a no-op returning this; use
|
|
165
|
+
listPresets() to enumerate valid names.
|
|
166
|
+
|
|
128
167
|
## Notes
|
|
129
168
|
- Migration: CinematicCameraPro is a drop-in superset of lite-camera's CinematicCamera.
|
|
130
169
|
- Zero-GC: coordinate conversion uses caller-owned out objects; visible dims are cached properties (no getVisibleArea()); shake (8) and parallax (16) slots are pre-allocated and reused.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-camera-pro",
|
|
3
3
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"description": "Cinematic camera system for Canvas2D games. Zero-GC, multi-target framing, noise-based shake, and timeline sequences.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -101,6 +101,13 @@ export class CinematicCameraPro extends CinematicCamera {
|
|
|
101
101
|
// ── Hybrid mode config ──
|
|
102
102
|
this.hybridVerticalSnap = true; // true = instant, false = fast lerp
|
|
103
103
|
|
|
104
|
+
// -- dt policy tunable (see decisions/0002-dt-policy.md) --
|
|
105
|
+
// update() clamps a finite dt above this ceiling before integrating, so
|
|
106
|
+
// a frame-time spike cannot diverge the position lerp (CP-4). Plain knob,
|
|
107
|
+
// not a per-frame-validated input -- writing garbage here is out of
|
|
108
|
+
// contract (D-f). A dt exactly == maxDt passes unclamped (H-D).
|
|
109
|
+
this.maxDt = 0.1; // seconds
|
|
110
|
+
|
|
104
111
|
// ── Multi-target framing ──
|
|
105
112
|
this._mt = createMultiTargetState();
|
|
106
113
|
|
|
@@ -136,6 +143,14 @@ export class CinematicCameraPro extends CinematicCamera {
|
|
|
136
143
|
* camera.setMode(FollowMode.PREDICTIVE);
|
|
137
144
|
*/
|
|
138
145
|
setMode(mode) {
|
|
146
|
+
// Fail-closed door (CP-12): an out-of-range mode makes the update()
|
|
147
|
+
// strategy lookup undefined and crashes at frame N+1 with a raw
|
|
148
|
+
// TypeError. Reject at the setter with a named error instead.
|
|
149
|
+
if (!Number.isInteger(mode) || mode < 0 || mode >= FOLLOW_STRATEGIES.length) {
|
|
150
|
+
const e = new Error("CinematicCameraPro: setMode(mode) requires an integer FollowMode in [0, " + (FOLLOW_STRATEGIES.length - 1) + "]");
|
|
151
|
+
e.code = "ERR_CAMERA_MODE";
|
|
152
|
+
throw e;
|
|
153
|
+
}
|
|
139
154
|
this.mode = mode;
|
|
140
155
|
return this;
|
|
141
156
|
}
|
|
@@ -167,6 +182,25 @@ export class CinematicCameraPro extends CinematicCamera {
|
|
|
167
182
|
* camera.trackSingle();
|
|
168
183
|
*/
|
|
169
184
|
trackMultiple(targets, options) {
|
|
185
|
+
// Fail-closed door (CP-19). Validate the array and every entry at CALL
|
|
186
|
+
// time -- a garbage target would otherwise crash updateMultiTarget at
|
|
187
|
+
// frame N+1 reading .x on undefined. Live mutation of the array/entries
|
|
188
|
+
// after this call is out of contract (no per-frame validation, H-C).
|
|
189
|
+
// An empty array is legal (count 0; update skips).
|
|
190
|
+
if (!Array.isArray(targets)) {
|
|
191
|
+
const e = new Error("CinematicCameraPro: trackMultiple(targets) requires an array");
|
|
192
|
+
e.code = "ERR_CAMERA_TARGETS";
|
|
193
|
+
throw e;
|
|
194
|
+
}
|
|
195
|
+
for (let i = 0; i < targets.length; i++) {
|
|
196
|
+
const t = targets[i];
|
|
197
|
+
if (t === null || typeof t !== 'object' || !Number.isFinite(t.x) || !Number.isFinite(t.y)) {
|
|
198
|
+
const e = new Error("CinematicCameraPro: trackMultiple targets[" + i + "] must be an object with finite x and y");
|
|
199
|
+
e.code = "ERR_CAMERA_TARGETS";
|
|
200
|
+
throw e;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
170
204
|
const mt = this._mt;
|
|
171
205
|
mt.active = true;
|
|
172
206
|
mt.targets = targets;
|
|
@@ -209,7 +243,17 @@ export class CinematicCameraPro extends CinematicCamera {
|
|
|
209
243
|
* @returns {CinematicCameraPro} this
|
|
210
244
|
*/
|
|
211
245
|
setTargetCount(count) {
|
|
212
|
-
|
|
246
|
+
// Fail-closed door (CP-19). count must be an integer in [0, targets
|
|
247
|
+
// length]; an over-count would make updateMultiTarget read past the
|
|
248
|
+
// array end and crash at frame N+1. n=0 with null targets is legal.
|
|
249
|
+
const mt = this._mt;
|
|
250
|
+
const max = mt.targets ? mt.targets.length : 0;
|
|
251
|
+
if (!Number.isInteger(count) || count < 0 || count > max) {
|
|
252
|
+
const e = new Error("CinematicCameraPro: setTargetCount(count) must be an integer in [0, " + max + "]");
|
|
253
|
+
e.code = "ERR_CAMERA_TARGETS";
|
|
254
|
+
throw e;
|
|
255
|
+
}
|
|
256
|
+
mt.count = count;
|
|
213
257
|
return this;
|
|
214
258
|
}
|
|
215
259
|
|
|
@@ -262,6 +306,10 @@ export class CinematicCameraPro extends CinematicCamera {
|
|
|
262
306
|
* Built-in presets: explosion, earthquake, recoil, impact,
|
|
263
307
|
* landing, damage, rumble, heavy_impact.
|
|
264
308
|
*
|
|
309
|
+
* Fail-closed (CP-12/CP-19): an unknown name OR a non-string name is a
|
|
310
|
+
* documented no-op -- getPreset returns null, nothing is activated, and
|
|
311
|
+
* `this` is returned. Use listPresets() to enumerate valid names.
|
|
312
|
+
*
|
|
265
313
|
* @param {string} name Preset name (case-insensitive)
|
|
266
314
|
* @param {number} [intensity=1] Scale multiplier
|
|
267
315
|
* @returns {CinematicCameraPro} this
|
|
@@ -491,6 +539,20 @@ export class CinematicCameraPro extends CinematicCamera {
|
|
|
491
539
|
* camera.setZoom(2.0, 0.5, easeOutExpo);
|
|
492
540
|
*/
|
|
493
541
|
setZoom(level, duration = 0, ease = null) {
|
|
542
|
+
// Fail-closed door (CP-12). Finiteness precedes the clamp: clamp(NaN)
|
|
543
|
+
// returns NaN, so a NaN level would sail past the clamp and poison the
|
|
544
|
+
// zoom (F5). A non-finite or negative duration is defective input --
|
|
545
|
+
// duration 0 stays instant.
|
|
546
|
+
if (!Number.isFinite(level)) {
|
|
547
|
+
const e = new Error("CinematicCameraPro: setZoom(level) requires a finite number");
|
|
548
|
+
e.code = "ERR_CAMERA_ZOOM";
|
|
549
|
+
throw e;
|
|
550
|
+
}
|
|
551
|
+
if (!Number.isFinite(duration) || duration < 0) {
|
|
552
|
+
const e = new Error("CinematicCameraPro: setZoom duration must be a finite number >= 0");
|
|
553
|
+
e.code = "ERR_CAMERA_ZOOM";
|
|
554
|
+
throw e;
|
|
555
|
+
}
|
|
494
556
|
level = clamp(level, this.minZoom, this.maxZoom);
|
|
495
557
|
|
|
496
558
|
if (duration <= 0) {
|
|
@@ -533,27 +595,44 @@ export class CinematicCameraPro extends CinematicCamera {
|
|
|
533
595
|
* camera.zoomAt(boss, 1.8, 0.8, easeOutExpo);
|
|
534
596
|
*/
|
|
535
597
|
zoomAt(targetOrX, yOrLevel, levelOrDur, duration = 0, ease = null) {
|
|
536
|
-
|
|
598
|
+
// Fail-closed door (CP-12/CP-19). Resolve BOTH call forms into locals,
|
|
599
|
+
// validate them, and ONLY THEN write any this._ state -- a rejected call
|
|
600
|
+
// must mutate nothing. .x/.y are read and validated at CALL time only
|
|
601
|
+
// (live anchor mutation afterwards is out of contract). Finiteness
|
|
602
|
+
// precedes the clamp (F5). A non-function ease normalizes to null in
|
|
603
|
+
// both forms -- the static form gains it, closing a frame-N+1
|
|
604
|
+
// "this._zoomEase is not a function" crash.
|
|
605
|
+
let target, anchorX, anchorY, level, dur, easeFn;
|
|
537
606
|
|
|
538
607
|
if (typeof targetOrX === 'object' && targetOrX !== null) {
|
|
539
608
|
// zoomAt(target, level, duration, ease)
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
609
|
+
target = targetOrX;
|
|
610
|
+
anchorX = targetOrX.x;
|
|
611
|
+
anchorY = targetOrX.y;
|
|
543
612
|
level = yOrLevel;
|
|
544
|
-
dur = levelOrDur
|
|
613
|
+
dur = levelOrDur !== undefined ? levelOrDur : 0;
|
|
545
614
|
easeFn = duration; // shifted arg position — duration slot holds ease
|
|
546
|
-
if (typeof easeFn !== 'function') easeFn = null;
|
|
547
615
|
} else {
|
|
548
616
|
// zoomAt(x, y, level, duration, ease)
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
617
|
+
target = null;
|
|
618
|
+
anchorX = targetOrX;
|
|
619
|
+
anchorY = yOrLevel;
|
|
552
620
|
level = levelOrDur;
|
|
553
621
|
dur = duration;
|
|
554
622
|
easeFn = ease;
|
|
555
623
|
}
|
|
624
|
+
if (typeof easeFn !== 'function') easeFn = null;
|
|
625
|
+
|
|
626
|
+
if (!Number.isFinite(anchorX) || !Number.isFinite(anchorY) ||
|
|
627
|
+
!Number.isFinite(level) || !Number.isFinite(dur) || dur < 0) {
|
|
628
|
+
const e = new Error("CinematicCameraPro: zoomAt requires finite anchor x/y, a finite level, and a finite duration >= 0");
|
|
629
|
+
e.code = "ERR_CAMERA_ZOOM";
|
|
630
|
+
throw e;
|
|
631
|
+
}
|
|
556
632
|
|
|
633
|
+
this._zoomTarget = target;
|
|
634
|
+
this._zoomAnchorX = anchorX;
|
|
635
|
+
this._zoomAnchorY = anchorY;
|
|
557
636
|
this._hasAnchor = true;
|
|
558
637
|
level = clamp(level, this.minZoom, this.maxZoom);
|
|
559
638
|
|
|
@@ -658,6 +737,15 @@ export class CinematicCameraPro extends CinematicCamera {
|
|
|
658
737
|
/**
|
|
659
738
|
* Advance the camera by one frame.
|
|
660
739
|
*
|
|
740
|
+
* dt policy (fail closed -- see decisions/0002-dt-policy.md):
|
|
741
|
+
* - Non-finite (NaN/+-Infinity/null) or negative dt is REJECTED: the call
|
|
742
|
+
* is a documented no-op, nothing is mutated, and it returns. A poisoned
|
|
743
|
+
* frame is invisible (CP-3/CP-4).
|
|
744
|
+
* - dt === 0 and -0 are legal no-advance frames (zero deltas everywhere).
|
|
745
|
+
* - A finite dt above this.maxDt (default 0.1) is clamped to this.maxDt so
|
|
746
|
+
* a frame-time spike cannot diverge the position lerp; a dt exactly ==
|
|
747
|
+
* maxDt passes untouched.
|
|
748
|
+
*
|
|
661
749
|
* @param {number} dt Delta time in seconds
|
|
662
750
|
* @param {number} px Player world X
|
|
663
751
|
* @param {number} py Player world Y
|
|
@@ -665,6 +753,11 @@ export class CinematicCameraPro extends CinematicCamera {
|
|
|
665
753
|
* @param {number} [pvy=0] Player velocity Y (for lookahead)
|
|
666
754
|
*/
|
|
667
755
|
update(dt, px, py, pvx = 0, pvy = 0) {
|
|
756
|
+
// Fail-closed dt door (CP-3/CP-4, D-k). Two comparisons at the very top;
|
|
757
|
+
// the whole body below is byte-identical to 1.1.0 (H-C: zero new
|
|
758
|
+
// branches on the hot path). A rejected frame mutates nothing.
|
|
759
|
+
if (!Number.isFinite(dt) || dt < 0) return;
|
|
760
|
+
if (dt > this.maxDt) dt = this.maxDt;
|
|
668
761
|
|
|
669
762
|
const mt = this._mt;
|
|
670
763
|
const seq = this._seq;
|
|
@@ -861,20 +954,82 @@ export class CinematicCameraPro extends CinematicCamera {
|
|
|
861
954
|
|
|
862
955
|
/**
|
|
863
956
|
* Restore camera state from a snapshot.
|
|
957
|
+
*
|
|
958
|
+
* Fail-closed contract (CP-12/CP-19 -- see decisions/0002-dt-policy.md
|
|
959
|
+
* siblings): the snapshot is validated in full BEFORE any field is written,
|
|
960
|
+
* so a rejected snapshot mutates nothing.
|
|
961
|
+
* - snapshot must be a non-null object.
|
|
962
|
+
* - posX/posY are both-or-neither; targetX/targetY are both-or-neither.
|
|
963
|
+
* - every present numeric must be finite (the error names the field).
|
|
964
|
+
* - zoom is finite-checked then clamped to minZoom..maxZoom exactly as
|
|
965
|
+
* setZoom does -- zoom 0 clamps to minZoom (0.25), not an error.
|
|
966
|
+
* - mode, if present, must be an integer FollowMode in range.
|
|
967
|
+
* The snapshot is pose-only (pos/target/zoom/mode); shake, sequences, and
|
|
968
|
+
* zoom animations are deliberately not serialized. Any violation throws
|
|
969
|
+
* ERR_CAMERA_STATE.
|
|
970
|
+
*
|
|
864
971
|
* @param {Object} snapshot
|
|
865
972
|
* @returns {CinematicCameraPro} this
|
|
866
973
|
*/
|
|
867
974
|
setState(snapshot) {
|
|
868
|
-
if (snapshot
|
|
975
|
+
if (typeof snapshot !== 'object' || snapshot === null) {
|
|
976
|
+
const e = new Error("CinematicCameraPro: setState requires a snapshot object");
|
|
977
|
+
e.code = "ERR_CAMERA_STATE";
|
|
978
|
+
throw e;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const hasPosX = snapshot.posX !== undefined;
|
|
982
|
+
const hasPosY = snapshot.posY !== undefined;
|
|
983
|
+
const hasTargetX = snapshot.targetX !== undefined;
|
|
984
|
+
const hasTargetY = snapshot.targetY !== undefined;
|
|
985
|
+
const hasZoom = snapshot.zoom !== undefined;
|
|
986
|
+
const hasMode = snapshot.mode !== undefined;
|
|
987
|
+
|
|
988
|
+
// Pairing rule: a lone posX would write pos[1] = undefined -> NaN (F9).
|
|
989
|
+
if (hasPosX !== hasPosY) {
|
|
990
|
+
const e = new Error("CinematicCameraPro: setState posX and posY must be provided together");
|
|
991
|
+
e.code = "ERR_CAMERA_STATE";
|
|
992
|
+
throw e;
|
|
993
|
+
}
|
|
994
|
+
if (hasTargetX !== hasTargetY) {
|
|
995
|
+
const e = new Error("CinematicCameraPro: setState targetX and targetY must be provided together");
|
|
996
|
+
e.code = "ERR_CAMERA_STATE";
|
|
997
|
+
throw e;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Finiteness of every present numeric (validate ALL before mutating ANY).
|
|
1001
|
+
if (hasPosX && (!Number.isFinite(snapshot.posX) || !Number.isFinite(snapshot.posY))) {
|
|
1002
|
+
const e = new Error("CinematicCameraPro: setState posX/posY must be finite numbers");
|
|
1003
|
+
e.code = "ERR_CAMERA_STATE";
|
|
1004
|
+
throw e;
|
|
1005
|
+
}
|
|
1006
|
+
if (hasTargetX && (!Number.isFinite(snapshot.targetX) || !Number.isFinite(snapshot.targetY))) {
|
|
1007
|
+
const e = new Error("CinematicCameraPro: setState targetX/targetY must be finite numbers");
|
|
1008
|
+
e.code = "ERR_CAMERA_STATE";
|
|
1009
|
+
throw e;
|
|
1010
|
+
}
|
|
1011
|
+
if (hasZoom && !Number.isFinite(snapshot.zoom)) {
|
|
1012
|
+
const e = new Error("CinematicCameraPro: setState zoom must be a finite number");
|
|
1013
|
+
e.code = "ERR_CAMERA_STATE";
|
|
1014
|
+
throw e;
|
|
1015
|
+
}
|
|
1016
|
+
if (hasMode && (!Number.isInteger(snapshot.mode) || snapshot.mode < 0 || snapshot.mode >= FOLLOW_STRATEGIES.length)) {
|
|
1017
|
+
const e = new Error("CinematicCameraPro: setState mode must be an integer FollowMode in [0, " + (FOLLOW_STRATEGIES.length - 1) + "]");
|
|
1018
|
+
e.code = "ERR_CAMERA_STATE";
|
|
1019
|
+
throw e;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// All validated -> apply. zoom takes the same clamp as setZoom.
|
|
1023
|
+
if (hasPosX) {
|
|
869
1024
|
this.pos[0] = snapshot.posX;
|
|
870
1025
|
this.pos[1] = snapshot.posY;
|
|
871
1026
|
}
|
|
872
|
-
if (
|
|
1027
|
+
if (hasTargetX) {
|
|
873
1028
|
this.target[0] = snapshot.targetX;
|
|
874
1029
|
this.target[1] = snapshot.targetY;
|
|
875
1030
|
}
|
|
876
|
-
if (
|
|
877
|
-
if (
|
|
1031
|
+
if (hasZoom) this.zoom = clamp(snapshot.zoom, this.minZoom, this.maxZoom);
|
|
1032
|
+
if (hasMode) this.mode = snapshot.mode;
|
|
878
1033
|
this._updateBoundsForZoom();
|
|
879
1034
|
return this;
|
|
880
1035
|
}
|
package/src/MultiTarget.js
CHANGED
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Mutates cam.target[], cam.zoom directly. Zero allocations.
|
|
17
17
|
*
|
|
18
|
+
* Count contract (caller-owned, standalone ./multi callers included): count
|
|
19
|
+
* must satisfy 0 <= count <= targets.length and every targets[0..count-1] must
|
|
20
|
+
* be an object with finite x/y. This loop reads targets[0..count-1] without
|
|
21
|
+
* per-frame validation (zero-GC hot path) -- an out-of-range count or a garbage
|
|
22
|
+
* entry is undefined behavior here. The CinematicCameraPro facade enforces the
|
|
23
|
+
* contract at its trackMultiple/setTargetCount doors (ERR_CAMERA_TARGETS);
|
|
24
|
+
* direct callers of this function own that guarantee themselves.
|
|
25
|
+
*
|
|
18
26
|
* @param {CinematicCameraPro} cam The camera instance
|
|
19
27
|
* @param {number} dt Delta time in seconds
|
|
20
28
|
* @param {{x:number,y:number}[]} targets Array of target objects
|
package/src/Shake.d.ts
CHANGED
|
@@ -77,6 +77,12 @@ export declare const RUMBLE: Readonly<ShakeProfile>;
|
|
|
77
77
|
export declare const HEAVY_IMPACT: Readonly<ShakeProfile>;
|
|
78
78
|
|
|
79
79
|
// -- Preset registry (ShakePresets.js) --
|
|
80
|
+
/** Case-insensitive lookup. Unknown or non-string name returns null (1.2.0). */
|
|
80
81
|
export declare function getPreset(name: string): Readonly<ShakeProfile> | null;
|
|
82
|
+
/**
|
|
83
|
+
* Register (or overwrite) a named preset. Setup path: fails loud.
|
|
84
|
+
* @throws Error with code "ERR_SHAKE_PRESET" if name is not a non-empty
|
|
85
|
+
* string or profile is not a non-null object (1.2.0).
|
|
86
|
+
*/
|
|
81
87
|
export declare function registerPreset(name: string, profile: ShakeProfile): void;
|
|
82
88
|
export declare function listPresets(): string[];
|
package/src/ShakeEngine.js
CHANGED
|
@@ -129,16 +129,34 @@ function acquireSlot(state) {
|
|
|
129
129
|
* @param {number} [profile.intensity=1] Scale multiplier for the profile
|
|
130
130
|
*/
|
|
131
131
|
export function addShake(state, profile, intensity = 1) {
|
|
132
|
-
// CP-14 + H-F (fail closed): validate
|
|
133
|
-
// the per-frame updateShake/computeShake loops gain zero new
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
132
|
+
// CP-14 + CP-3 + H-F (fail closed): validate the WHOLE profile in this COLD
|
|
133
|
+
// entry so the per-frame updateShake/computeShake loops gain zero new
|
|
134
|
+
// branches. Every numeric is resolved to its documented default FIRST (the
|
|
135
|
+
// `!== undefined ? : default` form -- including dirX/dirY, replacing the old
|
|
136
|
+
// `|| 0` that laundered a NaN direction to 0), then one combined finiteness
|
|
137
|
+
// check activates NOTHING and returns BEFORE acquireSlot on any failure:
|
|
138
|
+
// - trauma undefined -> 0.5; a non-finite trauma/intensity fires nothing.
|
|
139
|
+
// The old `profile.trauma || 0.5` laundered NaN to 0.5, a poison door.
|
|
140
|
+
// - decay/freq/maxOffset/maxAngle/dirX/dirY non-finite -> reject too. A
|
|
141
|
+
// NaN decay would leave the slot's trauma <= 0 test false forever, so
|
|
142
|
+
// the slot never deactivates (CP-3 via a poisoned profile).
|
|
143
|
+
// - null is not zero; an unverified number does not get a default.
|
|
139
144
|
// - resulting trauma <= 0 -> inert (a zero-trauma shake fires nothing).
|
|
145
|
+
// Valid, all-finite profiles resolve to the SAME slot values as before --
|
|
146
|
+
// only the ORDER of the default resolution moved (H-A).
|
|
140
147
|
const rawTrauma = profile.trauma === undefined ? 0.5 : profile.trauma;
|
|
141
|
-
|
|
148
|
+
const decay = profile.decay !== undefined ? profile.decay : 1.0;
|
|
149
|
+
const freq = profile.freq !== undefined ? profile.freq : 15;
|
|
150
|
+
const maxOffset = profile.maxOffset !== undefined ? profile.maxOffset : 15;
|
|
151
|
+
const maxAngle = profile.maxAngle !== undefined ? profile.maxAngle : 0.05;
|
|
152
|
+
const dirX = profile.dirX !== undefined ? profile.dirX : 0;
|
|
153
|
+
const dirY = profile.dirY !== undefined ? profile.dirY : 0;
|
|
154
|
+
|
|
155
|
+
if (!Number.isFinite(rawTrauma) || !Number.isFinite(intensity) ||
|
|
156
|
+
!Number.isFinite(decay) || !Number.isFinite(freq) ||
|
|
157
|
+
!Number.isFinite(maxOffset) || !Number.isFinite(maxAngle) ||
|
|
158
|
+
!Number.isFinite(dirX) || !Number.isFinite(dirY)) return;
|
|
159
|
+
|
|
142
160
|
const trauma = Math.min(1, rawTrauma * intensity);
|
|
143
161
|
if (trauma <= 0) return;
|
|
144
162
|
|
|
@@ -147,22 +165,20 @@ export function addShake(state, profile, intensity = 1) {
|
|
|
147
165
|
slot.active = true;
|
|
148
166
|
slot.isDefault = false;
|
|
149
167
|
slot.trauma = trauma;
|
|
150
|
-
slot.decay =
|
|
151
|
-
slot.freq =
|
|
152
|
-
slot.maxOffset =
|
|
153
|
-
slot.maxAngle =
|
|
168
|
+
slot.decay = decay;
|
|
169
|
+
slot.freq = freq;
|
|
170
|
+
slot.maxOffset = maxOffset;
|
|
171
|
+
slot.maxAngle = maxAngle;
|
|
154
172
|
slot.time = 0; // reset time for fresh noise sampling
|
|
155
173
|
|
|
156
|
-
// Directional
|
|
157
|
-
|
|
158
|
-
const dy = profile.dirY || 0;
|
|
159
|
-
slot.isDirectional = (dx !== 0 || dy !== 0);
|
|
174
|
+
// Directional. dirX/dirY are already resolved + finite-checked above.
|
|
175
|
+
slot.isDirectional = (dirX !== 0 || dirY !== 0);
|
|
160
176
|
|
|
161
177
|
if (slot.isDirectional) {
|
|
162
178
|
// Normalize direction
|
|
163
|
-
const len = Math.sqrt(
|
|
164
|
-
slot.dirX =
|
|
165
|
-
slot.dirY =
|
|
179
|
+
const len = Math.sqrt(dirX * dirX + dirY * dirY);
|
|
180
|
+
slot.dirX = dirX / len;
|
|
181
|
+
slot.dirY = dirY / len;
|
|
166
182
|
} else {
|
|
167
183
|
slot.dirX = 0;
|
|
168
184
|
slot.dirY = 0;
|
|
@@ -220,6 +236,14 @@ export function addTraumaSimple(state, amount) {
|
|
|
220
236
|
* @param {number} dt Delta time in seconds
|
|
221
237
|
*/
|
|
222
238
|
export function updateShake(state, dt) {
|
|
239
|
+
// CP-3 + H-C (fail closed): a non-finite or negative dt is rejected as a
|
|
240
|
+
// no-op in this entry so the per-slot loop below stays branch-for-branch
|
|
241
|
+
// unchanged. A NaN dt would drive s.time/s.trauma to NaN, the trauma <= 0
|
|
242
|
+
// test would never fire, and computeShake would emit NaN forever. No maxDt
|
|
243
|
+
// clamp here: a large finite dt is self-limiting (trauma decays past 0, the
|
|
244
|
+
// slot deactivates in one step). cam.update() hands an already-clamped dt.
|
|
245
|
+
if (!Number.isFinite(dt) || dt < 0) return;
|
|
246
|
+
|
|
223
247
|
let anyActive = false;
|
|
224
248
|
|
|
225
249
|
for (let i = 0; i < state.slotCount; i++) {
|
package/src/ShakePresets.js
CHANGED
|
@@ -144,10 +144,15 @@ const _registry = {
|
|
|
144
144
|
/**
|
|
145
145
|
* Get a preset by name.
|
|
146
146
|
*
|
|
147
|
+
* Fail-closed (CP-12): a non-string name returns null (the event path -- e.g.
|
|
148
|
+
* cam.shakePreset(undefined) -- must not crash on name.toLowerCase()). An
|
|
149
|
+
* unknown string returns null too. Case-insensitive for valid strings.
|
|
150
|
+
*
|
|
147
151
|
* @param {string} name Preset name (case-insensitive)
|
|
148
152
|
* @returns {Object|null} Shake profile or null
|
|
149
153
|
*/
|
|
150
154
|
export function getPreset(name) {
|
|
155
|
+
if (typeof name !== 'string') return null;
|
|
151
156
|
return _registry[name.toLowerCase()] || null;
|
|
152
157
|
}
|
|
153
158
|
|
|
@@ -165,6 +170,16 @@ export function getPreset(name) {
|
|
|
165
170
|
* });
|
|
166
171
|
*/
|
|
167
172
|
export function registerPreset(name, profile) {
|
|
173
|
+
// Fail-closed (CP-12, setup path fails loud): a non-string/empty name or a
|
|
174
|
+
// non-object profile is a defective registration -- reject it rather than
|
|
175
|
+
// poison the registry with a key that getPreset can never resolve or a
|
|
176
|
+
// profile addShake would spread into garbage.
|
|
177
|
+
if (typeof name !== 'string' || name === '' ||
|
|
178
|
+
typeof profile !== 'object' || profile === null) {
|
|
179
|
+
const e = new Error("lite-camera-pro: registerPreset(name, profile) requires a non-empty string name and a profile object");
|
|
180
|
+
e.code = "ERR_SHAKE_PRESET";
|
|
181
|
+
throw e;
|
|
182
|
+
}
|
|
168
183
|
_registry[name.toLowerCase()] = Object.freeze({ ...profile });
|
|
169
184
|
}
|
|
170
185
|
|
package/src/index.d.ts
CHANGED
|
@@ -75,14 +75,24 @@ export declare class CinematicCameraPro {
|
|
|
75
75
|
predictTime: number;
|
|
76
76
|
hybridVerticalSnap: boolean;
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* update() clamps a finite dt above this ceiling before integrating so a
|
|
80
|
+
* frame-time spike cannot diverge the position lerp (default 0.1s). Plain
|
|
81
|
+
* tunable; not validated per frame. See update() for the full dt policy.
|
|
82
|
+
*/
|
|
83
|
+
maxDt: number;
|
|
84
|
+
|
|
78
85
|
debugConfig: DebugHUDConfig;
|
|
79
86
|
|
|
80
87
|
// Follow mode
|
|
88
|
+
/** @throws Error `code = "ERR_CAMERA_MODE"` if mode is not an integer FollowMode in range. */
|
|
81
89
|
setMode(mode: number): this;
|
|
82
90
|
|
|
83
91
|
// Multi-target
|
|
92
|
+
/** @throws Error `code = "ERR_CAMERA_TARGETS"` if targets is not an array or any entry lacks finite x/y (validated at call time). */
|
|
84
93
|
trackMultiple(targets: Vec2[], options?: MultiTargetOptions): this;
|
|
85
94
|
trackSingle(): this;
|
|
95
|
+
/** @throws Error `code = "ERR_CAMERA_TARGETS"` if count is not an integer in [0, targets.length]. */
|
|
86
96
|
setTargetCount(count: number): this;
|
|
87
97
|
|
|
88
98
|
// Shake
|
|
@@ -98,7 +108,9 @@ export declare class CinematicCameraPro {
|
|
|
98
108
|
readonly sequencePlaying: boolean;
|
|
99
109
|
|
|
100
110
|
// Zoom
|
|
111
|
+
/** @throws Error `code = "ERR_CAMERA_ZOOM"` if level is non-finite or duration is non-finite/negative. */
|
|
101
112
|
setZoom(level: number, duration?: number, ease?: (t: number) => number): this;
|
|
113
|
+
/** @throws Error `code = "ERR_CAMERA_ZOOM"` if anchor x/y, level, or duration is non-finite (or duration negative). A non-function ease normalizes to null. */
|
|
102
114
|
zoomAt(targetOrX: number | Vec2, yOrLevel: number, levelOrDur?: number, duration?: number, ease?: (t: number) => number): this;
|
|
103
115
|
|
|
104
116
|
// Coordinate conversion
|
|
@@ -117,6 +129,12 @@ export declare class CinematicCameraPro {
|
|
|
117
129
|
clearBoundsRect(): this;
|
|
118
130
|
|
|
119
131
|
// Core
|
|
132
|
+
/**
|
|
133
|
+
* Advance the camera one frame. dt policy (fail closed): a non-finite or
|
|
134
|
+
* negative dt is a no-op (nothing mutated); dt 0/-0 is a legal no-advance
|
|
135
|
+
* frame; a finite dt above maxDt is clamped to maxDt (a dt exactly == maxDt
|
|
136
|
+
* passes untouched).
|
|
137
|
+
*/
|
|
120
138
|
update(dt: number, px: number, py: number, pvx?: number, pvy?: number): void;
|
|
121
139
|
apply(ctx: CanvasRenderingContext2D): void;
|
|
122
140
|
|
|
@@ -126,6 +144,15 @@ export declare class CinematicCameraPro {
|
|
|
126
144
|
|
|
127
145
|
// Save / load
|
|
128
146
|
getState(): { posX: number; posY: number; targetX: number; targetY: number; zoom: number; mode: number };
|
|
147
|
+
/**
|
|
148
|
+
* Restore a pose-only snapshot (pos/target/zoom/mode). Validated in full
|
|
149
|
+
* before any field is written -- a rejected snapshot mutates nothing.
|
|
150
|
+
* posX/posY and targetX/targetY are both-or-neither; every present numeric
|
|
151
|
+
* must be finite; zoom is clamped to minZoom..maxZoom (zoom 0 -> minZoom);
|
|
152
|
+
* mode must be an integer FollowMode in range. Shake, sequences, and zoom
|
|
153
|
+
* animations are deliberately not serialized.
|
|
154
|
+
* @throws Error `code = "ERR_CAMERA_STATE"` on any violation.
|
|
155
|
+
*/
|
|
129
156
|
setState(snapshot: { posX?: number; posY?: number; targetX?: number; targetY?: number; zoom?: number; mode?: number }): this;
|
|
130
157
|
|
|
131
158
|
// Lifecycle
|