@zakkster/lite-camera-pro 1.0.0 → 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,75 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@zakkster/lite-camera-pro` are documented here. The
4
+ format follows Keep a Changelog, and this project adheres to Semantic
5
+ Versioning. Version lives in three places at once -- `package.json`, the
6
+ `VERSION` const in `src/index.js`, and the `Version:` header in `llms.txt` --
7
+ bumped together or not at all.
8
+
9
+ ## [1.0.1] -- 2026-08-25
10
+
11
+ Foundation release: make the suite run, gate it, and land the fixes that cost
12
+ nothing at the public surface. No behavior change to any valid, finite,
13
+ in-bounds call -- CP-13 and CP-14 change only what defective input does.
14
+
15
+ ### Fixed
16
+
17
+ - **CP-1 -- the standalone functional API is now reachable from the package
18
+ entry.** `src/index.js` re-exports `createShakeState` (Shake Engine) and
19
+ `createMultiTargetState` (Multi-Target); before, every state-taking function
20
+ in the documented tree-shakeable layer took a state no consumer could
21
+ construct. Reported in `BRIEF.md` by the Las Vegas scratch-card consumer.
22
+ - **CP-8 -- use-after-destroy fails closed.** After
23
+ `CinematicCameraPro.destroy()`, EVERY public method throws an error with code
24
+ `ERR_CAMERA_DESTROYED` (fail closed) instead of a raw null deref. destroy()
25
+ calls `super.destroy()` for base parity, then rebinds the whole public surface
26
+ to the named-error sentinel, so the guarantee cannot drift as methods are
27
+ added. A double `destroy()` throws the same named error.
28
+ - **CP-13 -- uniform floor snap.** `apply()` and `applyParallaxLayer()` now
29
+ snap the world/layer scroll with `Math.floor` instead of `| 0`, matching the
30
+ base camera. `| 0` truncated toward zero, disagreeing with the base by a full
31
+ pixel at negative fractional positions and taking a double-length integer step
32
+ about the origin.
33
+ - **CP-14 + H-F -- trauma default fail-closed.** `addShake`/`addTraumaSimple`
34
+ replaced the falsy `profile.trauma || 0.5` default: `undefined` still means
35
+ 0.5, but a non-finite trauma/intensity now activates NO slot (fail closed) and
36
+ an explicit `0` fires nothing. The old `||` laundered `NaN` into 0.5 -- the
37
+ only reason a NaN could not poison the shake sum from the trauma side (H-F).
38
+ The laundering is removed and the poison door closed in one change. Guards live
39
+ in the cold entry functions only; the per-frame `updateShake`/`computeShake`
40
+ loops are unchanged.
41
+
42
+ ### Added
43
+
44
+ - **node:test suite.** Ported both vitest suites to `node:test` +
45
+ `node:assert/strict` (facade + standalone subsystems), added
46
+ `test/consumer.test.js` (the feel-freeze pinning the consumer's three-tier
47
+ shake ramp), `test/regressions.test.js` (one named test per finding above),
48
+ and `test/metadata.test.js` (three-place version law, packaging, CP-1 export
49
+ guard). Removed vitest and its config.
50
+ - **Torture gate.** `test/torture.mjs` plus a tiered harness
51
+ (`@zakkster/lite-gc-profiler` + `@zakkster/lite-leak` + `@zakkster/lite-signal`
52
+ dev-only): metamorphic laws (T0), degenerate known-bad pins (T1), lifecycle
53
+ abuse (T4), the zero-alloc budget gate (T6: 200k `update`+`apply` under active
54
+ shake, `maxMajor:0`/`maxPauseMs:4`, pool-not-reallocated), retention soak (T7),
55
+ and the BREAK control (T9). `CAMPRO_TORTURE_BREAK=1` exits non-zero.
56
+ - **Version law.** `export const VERSION = "1.0.1"` from `src/index.js`, this
57
+ `CHANGELOG.md`, and a `Version:` header in `llms.txt`.
58
+
59
+ ### Changed
60
+
61
+ - Corrected three stale dependency header comments: `src/index.js` no longer
62
+ credits `lite-random` (not a dependency); `src/BoundsSystem.js` and
63
+ `src/MultiTarget.js` no longer claim a `@zakkster/lite-lerp` dependency they
64
+ do not import (their clamps are inline).
65
+
66
+ ## [1.0.0] -- 2025-06-28
67
+
68
+ Initial release. Cinematic Canvas2D camera system extending
69
+ `@zakkster/lite-camera`'s `CinematicCamera`: smooth/eased zoom, five follow
70
+ modes, multi-target auto-framing, an 8-slot simplex-noise shake engine with
71
+ presets, fluent timeline sequences, a 16-layer parallax manager, per-edge
72
+ bounds, a debug HUD, and zero-alloc coordinate conversion.
73
+
74
+ [1.0.1]: https://github.com/PeshoVurtoleta/lite-camera-pro/releases/tag/v1.0.1
75
+ [1.0.0]: https://github.com/PeshoVurtoleta/lite-camera-pro/releases/tag/v1.0.0
package/llms.txt CHANGED
@@ -6,6 +6,8 @@
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.0.1
10
+
9
11
  ESM only. `import` from `@zakkster/lite-camera-pro`. Default export is the
10
12
  CinematicCameraPro class. All hot-path APIs avoid allocation; setup-time calls
11
13
  (constructor, createSequence, trackMultiple) may allocate.
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.0.0",
4
+ "version": "1.0.1",
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,
@@ -18,6 +18,7 @@
18
18
  "files": [
19
19
  "src/",
20
20
  "README.md",
21
+ "CHANGELOG.md",
21
22
  "llms.txt",
22
23
  "LICENSE"
23
24
  ],
@@ -46,14 +47,18 @@
46
47
  "@zakkster/lite-timeline": "^1.0.0"
47
48
  },
48
49
  "devDependencies": {
49
- "esbuild": "^0.25.0",
50
- "vitest": "^4.1.4"
50
+ "@zakkster/lite-gc-profiler": "^1.16.0",
51
+ "@zakkster/lite-leak": "^1.10.0",
52
+ "@zakkster/lite-signal": "^1.5.0",
53
+ "esbuild": "^0.25.0"
51
54
  },
52
55
  "scripts": {
53
- "test": "vitest run",
54
- "test:watch": "vitest",
56
+ "test": "node --test test/*.test.js",
57
+ "test:gc": "node --expose-gc --test test/*.test.js",
58
+ "torture": "node --expose-gc test/torture.mjs",
59
+ "verify": "npm run test:gc && npm run torture",
55
60
  "bundle-check": "esbuild ./src/index.js --bundle --format=esm --outfile=test-bundle.js",
56
- "prepublishOnly": "npm run test && npm run bundle-check"
61
+ "prepublishOnly": "npm run verify && npm run bundle-check"
57
62
  },
58
63
  "keywords": [
59
64
  "camera",
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * Zero allocations. All state is pre-allocated on the camera.
13
13
  *
14
- * Depends on: @zakkster/lite-lerp (clamp, smoothstep)
14
+ * Depends on: nothing (pure math -- clamp/smoothstep are inline).
15
15
  */
16
16
 
17
17
  /** @enum {number} */
@@ -46,6 +46,16 @@ import {
46
46
  } from './BoundsSystem.js';
47
47
  import {createDebugHUDConfig, drawDebugHUD, drawDebugWorld} from './DebugHUD.js';
48
48
 
49
+ // Cold-path sentinel: after destroy() every method that reads nulled internal
50
+ // state is rebound to this so a use-after-destroy fails closed with a named
51
+ // error instead of a raw null deref. Rebinding (not an in-body guard) keeps
52
+ // update()/apply() at zero per-frame cost. Matches base CinematicCamera (CP-8).
53
+ const _dead = () => {
54
+ const e = new Error("CinematicCameraPro: use after destroy()");
55
+ e.code = "ERR_CAMERA_DESTROYED";
56
+ throw e;
57
+ };
58
+
49
59
  export class CinematicCameraPro extends CinematicCamera {
50
60
 
51
61
  /**
@@ -798,8 +808,10 @@ export class CinematicCameraPro extends CinematicCamera {
798
808
  ctx.scale(this.zoom, this.zoom);
799
809
  ctx.translate(-this._halfW / this.zoom, -this._halfH / this.zoom);
800
810
 
801
- // 6. Scroll to camera world position
802
- ctx.translate(-(this.pos[0] | 0), -(this.pos[1] | 0));
811
+ // 6. Scroll to camera world position.
812
+ // int snap is deliberate: kills texture shimmer. floor (not | 0) so the
813
+ // snap is uniform about the origin -- | 0 truncates toward zero (CP-13).
814
+ ctx.translate(-Math.floor(this.pos[0]), -Math.floor(this.pos[1]));
803
815
  }
804
816
 
805
817
  // ─────────────────────────────────────────────────────
@@ -868,9 +880,10 @@ export class CinematicCameraPro extends CinematicCamera {
868
880
  }
869
881
 
870
882
  /**
871
- * Destroy the camera. Releases sequences, clears shake state, and
872
- * nulls all nested allocations so the GC can reclaim them. After
873
- * destroy(), the camera is unusable do not call any further methods.
883
+ * Destroy the camera. Releases sequences, clears shake state, and nulls all
884
+ * nested allocations so the GC can reclaim them. After destroy() the camera
885
+ * is unusable: EVERY public method throws an Error with code
886
+ * "ERR_CAMERA_DESTROYED" (fail closed) rather than a raw null deref.
874
887
  */
875
888
  destroy() {
876
889
  if (this._seq) {
@@ -878,16 +891,37 @@ export class CinematicCameraPro extends CinematicCamera {
878
891
  this._seq = null;
879
892
  }
880
893
 
894
+ // clearShakeState needs _shake still live -- run it before the null.
881
895
  clearShakeState(this._shake);
882
896
 
883
- // Release nested state so the slot pools / layer arrays can be GC'd
897
+ // Release nested Pro state so the slot pools / layer arrays can be GC'd.
884
898
  this._shake = null;
885
899
  this._mt = null;
886
900
  this._parallax = null;
887
901
  this._bounds = null;
888
902
  this.debugConfig = null;
889
- this.pos = this.target = this.look = null;
890
- this.rng = null;
903
+
904
+ // Null the base typed arrays + rng and rebind the base methods
905
+ // (update/apply/debug/addTrauma/resize/destroy) to the base sentinel.
906
+ // Ordered after the Pro teardown so nothing above reads a nulled base
907
+ // field. T8 asserts parity with the base's destroy contract.
908
+ super.destroy();
909
+
910
+ // CP-8: a destroyed camera fails closed on EVERY public method. Any call
911
+ // throws ERR_CAMERA_DESTROYED (Pro's sentinel -- consistent message)
912
+ // instead of a raw null deref. Rebinding the WHOLE public surface, not a
913
+ // curated subset, means the guarantee cannot silently drift as methods
914
+ // are added -- and re-stamps the base rebinds from super.destroy() with
915
+ // Pro's message for parity. Getters (sequencePlaying) are already
916
+ // null-safe. A double destroy() throws the same named error.
917
+ this.update = this.apply = this.debug = this.debugHUD =
918
+ this.addTrauma = this.shake = this.shakePreset = this.clearShakes =
919
+ this.setMode = this.trackMultiple = this.trackSingle = this.setTargetCount =
920
+ this.createSequence = this.playSequence = this.stopSequence =
921
+ this.addParallaxLayer = this.removeParallaxLayer = this.applyParallax =
922
+ this.setBoundsType = this.setBoundsEdges = this.setBoundsRect = this.clearBoundsRect =
923
+ this.setZoom = this.zoomAt = this.screenToWorld = this.worldToScreen =
924
+ this.getState = this.setState = this.resize = this.destroy = _dead;
891
925
  }
892
926
  }
893
927
 
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Used by boss fights, co-op, cutscenes tracking multiple actors.
8
8
  *
9
- * Depends on: @zakkster/lite-lerp (clamp)
9
+ * Depends on: nothing (pure math -- zoom/position clamps are inline).
10
10
  */
11
11
 
12
12
  /**
@@ -191,7 +191,10 @@ export function applyParallaxLayer(state, id, ctx) {
191
191
  for (let i = 0; i < state.layerCount; i++) {
192
192
  const layer = state.layers[i];
193
193
  if (layer.active && layer.id === id) {
194
- ctx.translate(-(layer.scrollX | 0), -(layer.scrollY | 0));
194
+ // int snap is deliberate: kills texture shimmer. floor (not | 0) so
195
+ // the snap is uniform about the origin -- | 0 truncates toward zero
196
+ // (CP-13), matching the base camera and CinematicCameraPro.apply().
197
+ ctx.translate(-Math.floor(layer.scrollX), -Math.floor(layer.scrollY));
195
198
  return true;
196
199
  }
197
200
  }
@@ -129,11 +129,24 @@ 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 trauma/intensity in this COLD entry so
133
+ // the per-frame updateShake/computeShake loops gain zero new branches.
134
+ // - trauma undefined -> 0.5 (documented default)
135
+ // - trauma or intensity non-finite (NaN/Inf) -> activate NOTHING, return
136
+ // early. The old `profile.trauma || 0.5` laundered NaN to 0.5, opening a
137
+ // poison door: a single NaN shake would drive every later frame to NaN.
138
+ // null is not zero; an unverified number does not get a default.
139
+ // - resulting trauma <= 0 -> inert (a zero-trauma shake fires nothing).
140
+ const rawTrauma = profile.trauma === undefined ? 0.5 : profile.trauma;
141
+ if (!Number.isFinite(rawTrauma) || !Number.isFinite(intensity)) return;
142
+ const trauma = Math.min(1, rawTrauma * intensity);
143
+ if (trauma <= 0) return;
144
+
132
145
  const slot = acquireSlot(state);
133
146
 
134
147
  slot.active = true;
135
148
  slot.isDefault = false;
136
- slot.trauma = Math.min(1, (profile.trauma || 0.5) * intensity);
149
+ slot.trauma = trauma;
137
150
  slot.decay = profile.decay !== undefined ? profile.decay : 1.0;
138
151
  slot.freq = profile.freq !== undefined ? profile.freq : 15;
139
152
  slot.maxOffset = profile.maxOffset !== undefined ? profile.maxOffset : 15;
@@ -166,6 +179,11 @@ export function addShake(state, profile, intensity = 1) {
166
179
  * @param {number} amount Trauma to add [0, 1]
167
180
  */
168
181
  export function addTraumaSimple(state, amount) {
182
+ // CP-14 + H-F (fail closed): a non-finite amount activates NOTHING and an
183
+ // amount <= 0 is inert. Same policy as addShake, in this cold entry only so
184
+ // the hot per-frame loops stay branch-for-branch unchanged.
185
+ if (!Number.isFinite(amount) || amount <= 0) return;
186
+
169
187
  // Try to find an existing default omni slot to stack onto.
170
188
  // Preset/profile slots are NEVER stacked onto — they have parameters
171
189
  // (freq, decay, etc.) that addTrauma's generic shake wouldn't match.
package/src/index.js CHANGED
@@ -2,32 +2,34 @@
2
2
  * @zakkster/lite-camera-pro
3
3
  * Commercial cinematic camera system for Canvas2D games.
4
4
  *
5
- * Built on: lite-camera, lite-lerp, lite-ease, lite-random, lite-noise, lite-timeline
5
+ * Built on: lite-camera, lite-lerp, lite-ease, lite-noise, lite-timeline
6
6
  * Zero external dependencies outside the @zakkster ecosystem.
7
7
  */
8
8
 
9
- // ── Core ──
9
+ export const VERSION = "1.0.1";
10
+
11
+ // -- Core --
10
12
  export { CinematicCameraPro } from './CinematicCameraPro.js';
11
13
  export { default } from './CinematicCameraPro.js';
12
14
 
13
- // ── Follow Modes ──
15
+ // -- Follow Modes --
14
16
  export { FollowMode, FOLLOW_STRATEGIES } from './FollowMode.js';
15
17
 
16
- // ── Multi-Target ──
17
- export { updateMultiTarget } from './MultiTarget.js';
18
+ // -- Multi-Target --
19
+ export { createMultiTargetState, updateMultiTarget } from './MultiTarget.js';
18
20
 
19
- // ── Shake Engine ──
20
- export { addShake, addTraumaSimple, updateShake, computeShake, clearShakes } from './ShakeEngine.js';
21
+ // -- Shake Engine --
22
+ export { createShakeState, addShake, addTraumaSimple, updateShake, computeShake, clearShakes } from './ShakeEngine.js';
21
23
  export { EXPLOSION, EARTHQUAKE, RECOIL, IMPACT, LANDING, DAMAGE, RUMBLE, HEAVY_IMPACT, getPreset, registerPreset, listPresets } from './ShakePresets.js';
22
24
 
23
- // ── Sequences ──
25
+ // -- Sequences --
24
26
  export { createCameraSequence, panTo, dramaticZoom, bossReveal, timedShake } from './CameraSequence.js';
25
27
 
26
- // ── Parallax ──
28
+ // -- Parallax --
27
29
  export { WrapMode, createParallaxState, addParallaxLayer, removeParallaxLayer, updateParallax, getLayerScroll, applyParallaxLayer } from './ParallaxManager.js';
28
30
 
29
- // ── Bounds ──
31
+ // -- Bounds --
30
32
  export { BoundsType, createBoundsState, setBoundsAll, setBoundsEdges, setBoundsRect, clearBoundsRect, applyBounds } from './BoundsSystem.js';
31
33
 
32
- // ── Debug HUD ──
34
+ // -- Debug HUD --
33
35
  export { createDebugHUDConfig, drawDebugHUD, drawDebugWorld } from './DebugHUD.js';