@zakkster/lite-camera-pro 1.0.0 → 1.1.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 ADDED
@@ -0,0 +1,122 @@
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.1.0] -- 2026-08-26
10
+
11
+ Subpath exports. A consumer who needs only screen shake now imports
12
+ `@zakkster/lite-camera-pro/shake` and pays for the shake engine plus its
13
+ tree-shaken noise sampler -- nothing else. No runtime behavior changed: the hot
14
+ bodies (`computeShake`, `updateShake`, `apply`, `updateParallax`, `applyBounds`,
15
+ `updateMultiTarget`) are byte-identical, and the camera class imports the same
16
+ module files the subpaths expose (one engine, no fork -- proven by the T8
17
+ Object.is identity check).
18
+
19
+ Measured: the Las Vegas scratch-card consumer (BRIEF.md) reported a 73.5 KB gz
20
+ whole-package pull for a three-tier win-shake ramp that touches only the shake
21
+ API. Through `@zakkster/lite-camera-pro/shake` the same integration measures
22
+ **2.82 KB gz** (2,883 B; 8.79 KB raw, esbuild esm, minify=false, gzip -9) --
23
+ about **26x smaller**. Of that bundle, lite-noise's `Noise.js` contributes
24
+ 2,009 B (5.1% of its 39,613 B source), so tree-shaking through the noise
25
+ dependency works as intended.
26
+
27
+ ### Added
28
+
29
+ - **Subpath exports map.** Seven entries beside `.`: `./shake`, `./parallax`,
30
+ `./bounds`, `./multi`, `./follow`, `./sequence`, and `./package.json`. Each
31
+ runtime entry carries a sibling `types` condition (declared first, per the
32
+ TypeScript exports-map requirement). Per-subpath gz weights (esm, unminified,
33
+ gzip -9): `./shake` 2.82 KB, `./parallax` 0.89 KB, `./bounds` 1.13 KB,
34
+ `./multi` 0.89 KB, `./follow` 0.71 KB, `./sequence` 5.94 KB, `.` 21.70 KB.
35
+ `./sequence` drags `@zakkster/lite-timeline` + `@zakkster/lite-ease` by design
36
+ -- it is the only subpath that does.
37
+ - **`src/Shake.js` barrel.** A two-line re-export over `ShakeEngine.js` +
38
+ `ShakePresets.js` (never a copy), so a shake-only consumer gets the engine,
39
+ the presets, and `getPreset`/`registerPreset`/`listPresets` from one import.
40
+ - **Typed functional layer (CP-16a).** `src/index.d.ts` now re-exports six
41
+ per-subsystem sibling declarations (`Shake.d.ts`, `ParallaxManager.d.ts`,
42
+ `BoundsSystem.d.ts`, `MultiTarget.d.ts`, `FollowMode.d.ts`,
43
+ `CameraSequence.d.ts`), each declaring its module's complete runtime surface
44
+ -- state interfaces, enums, defaults, no `any`. The standalone functions are
45
+ now visible and typed at the main entry, not just the class.
46
+ - **Size gate.** `test/size.mjs` bundles every subpath with the esbuild JS API
47
+ and asserts `./shake` gz stays at or under the fixed 16,384 B charter budget.
48
+ Wired into `npm run verify`.
49
+ - **Dependency decision record (CP-17).** `decisions/0001-layout-and-deps.md`
50
+ documents the multi-file layout and the five first-party runtime deps, with
51
+ per-dep floor evidence. Repo-only; not shipped in the tarball.
52
+ - **TypeScript smoke.** `test/types-smoke/smoke.ts` exercises every subpath plus
53
+ the main entry under `strict` + `noImplicitAny` + node16 resolution;
54
+ `npm run typecheck` runs it and joins `prepublishOnly`.
55
+
56
+ ## [1.0.1] -- 2026-08-25
57
+
58
+ Foundation release: make the suite run, gate it, and land the fixes that cost
59
+ nothing at the public surface. No behavior change to any valid, finite,
60
+ in-bounds call -- CP-13 and CP-14 change only what defective input does.
61
+
62
+ ### Fixed
63
+
64
+ - **CP-1 -- the standalone functional API is now reachable from the package
65
+ entry.** `src/index.js` re-exports `createShakeState` (Shake Engine) and
66
+ `createMultiTargetState` (Multi-Target); before, every state-taking function
67
+ in the documented tree-shakeable layer took a state no consumer could
68
+ construct. Reported in `BRIEF.md` by the Las Vegas scratch-card consumer.
69
+ - **CP-8 -- use-after-destroy fails closed.** After
70
+ `CinematicCameraPro.destroy()`, EVERY public method throws an error with code
71
+ `ERR_CAMERA_DESTROYED` (fail closed) instead of a raw null deref. destroy()
72
+ calls `super.destroy()` for base parity, then rebinds the whole public surface
73
+ to the named-error sentinel, so the guarantee cannot drift as methods are
74
+ added. A double `destroy()` throws the same named error.
75
+ - **CP-13 -- uniform floor snap.** `apply()` and `applyParallaxLayer()` now
76
+ snap the world/layer scroll with `Math.floor` instead of `| 0`, matching the
77
+ base camera. `| 0` truncated toward zero, disagreeing with the base by a full
78
+ pixel at negative fractional positions and taking a double-length integer step
79
+ about the origin.
80
+ - **CP-14 + H-F -- trauma default fail-closed.** `addShake`/`addTraumaSimple`
81
+ replaced the falsy `profile.trauma || 0.5` default: `undefined` still means
82
+ 0.5, but a non-finite trauma/intensity now activates NO slot (fail closed) and
83
+ an explicit `0` fires nothing. The old `||` laundered `NaN` into 0.5 -- the
84
+ only reason a NaN could not poison the shake sum from the trauma side (H-F).
85
+ The laundering is removed and the poison door closed in one change. Guards live
86
+ in the cold entry functions only; the per-frame `updateShake`/`computeShake`
87
+ loops are unchanged.
88
+
89
+ ### Added
90
+
91
+ - **node:test suite.** Ported both vitest suites to `node:test` +
92
+ `node:assert/strict` (facade + standalone subsystems), added
93
+ `test/consumer.test.js` (the feel-freeze pinning the consumer's three-tier
94
+ shake ramp), `test/regressions.test.js` (one named test per finding above),
95
+ and `test/metadata.test.js` (three-place version law, packaging, CP-1 export
96
+ guard). Removed vitest and its config.
97
+ - **Torture gate.** `test/torture.mjs` plus a tiered harness
98
+ (`@zakkster/lite-gc-profiler` + `@zakkster/lite-leak` + `@zakkster/lite-signal`
99
+ dev-only): metamorphic laws (T0), degenerate known-bad pins (T1), lifecycle
100
+ abuse (T4), the zero-alloc budget gate (T6: 200k `update`+`apply` under active
101
+ shake, `maxMajor:0`/`maxPauseMs:4`, pool-not-reallocated), retention soak (T7),
102
+ and the BREAK control (T9). `CAMPRO_TORTURE_BREAK=1` exits non-zero.
103
+ - **Version law.** `export const VERSION = "1.0.1"` from `src/index.js`, this
104
+ `CHANGELOG.md`, and a `Version:` header in `llms.txt`.
105
+
106
+ ### Changed
107
+
108
+ - Corrected three stale dependency header comments: `src/index.js` no longer
109
+ credits `lite-random` (not a dependency); `src/BoundsSystem.js` and
110
+ `src/MultiTarget.js` no longer claim a `@zakkster/lite-lerp` dependency they
111
+ do not import (their clamps are inline).
112
+
113
+ ## [1.0.0] -- 2025-06-28
114
+
115
+ Initial release. Cinematic Canvas2D camera system extending
116
+ `@zakkster/lite-camera`'s `CinematicCamera`: smooth/eased zoom, five follow
117
+ modes, multi-target auto-framing, an 8-slot simplex-noise shake engine with
118
+ presets, fluent timeline sequences, a 16-layer parallax manager, per-edge
119
+ bounds, a debug HUD, and zero-alloc coordinate conversion.
120
+
121
+ [1.0.1]: https://github.com/PeshoVurtoleta/lite-camera-pro/releases/tag/v1.0.1
122
+ [1.0.0]: https://github.com/PeshoVurtoleta/lite-camera-pro/releases/tag/v1.0.0
package/README.md CHANGED
@@ -20,6 +20,13 @@
20
20
  npm install @zakkster/lite-camera-pro
21
21
  ```
22
22
 
23
+ Need only screen shake? Import the `./shake` subpath and pull just the engine +
24
+ presets (2.82 KB gz -- esm, unminified, gzip -9):
25
+
26
+ ```js
27
+ import { createShakeState, addShake, updateShake, computeShake, getPreset } from '@zakkster/lite-camera-pro/shake';
28
+ ```
29
+
23
30
  ---
24
31
 
25
32
  ## Why Pro?
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.1.0
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.
@@ -107,6 +109,19 @@ Multi: createMultiTargetState, updateMultiTarget
107
109
  Sequence: createCameraSequence, panTo, dramaticZoom, bossReveal, timedShake
108
110
  Enums: FollowMode, FOLLOW_STRATEGIES, BoundsType, WrapMode
109
111
 
112
+ ## Subpath exports (import only what you use; gz, esm, unminified, gzip -9)
113
+ Beside the "." main entry, six subsystem subpaths + "./package.json". Same runtime
114
+ identities as the main entry (one engine, no fork). Per-subpath gz weight:
115
+ ./shake 2.82 KB createShakeState, addShake, addTraumaSimple, updateShake, computeShake, clearShakes + presets/getPreset/registerPreset/listPresets
116
+ ./parallax 0.89 KB createParallaxState, addParallaxLayer, removeParallaxLayer, updateParallax, getLayerScroll, applyParallaxLayer, WrapMode
117
+ ./bounds 1.13 KB createBoundsState, setBoundsAll, setBoundsEdges, setBoundsRect, clearBoundsRect, applyBounds, BoundsType
118
+ ./multi 0.89 KB createMultiTargetState, updateMultiTarget
119
+ ./follow 0.71 KB FollowMode, FOLLOW_STRATEGIES
120
+ ./sequence 5.94 KB createCameraSequence, panTo, dramaticZoom, bossReveal, timedShake (drags lite-timeline + lite-ease by design -- the only subpath that does)
121
+ . 21.70 KB full class + functional layer
122
+ Self-reference example (shake only):
123
+ import { createShakeState, addShake, updateShake, computeShake, getPreset } from '@zakkster/lite-camera-pro/shake';
124
+
110
125
  ## Dependencies (all @zakkster, first-party)
111
126
  @zakkster/lite-camera (base CinematicCamera), lite-ease, lite-lerp, lite-noise, lite-timeline.
112
127
 
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.1.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,
@@ -13,11 +13,49 @@
13
13
  "node": "./src/index.js",
14
14
  "import": "./src/index.js",
15
15
  "default": "./src/index.js"
16
- }
16
+ },
17
+ "./shake": {
18
+ "types": "./src/Shake.d.ts",
19
+ "node": "./src/Shake.js",
20
+ "import": "./src/Shake.js",
21
+ "default": "./src/Shake.js"
22
+ },
23
+ "./parallax": {
24
+ "types": "./src/ParallaxManager.d.ts",
25
+ "node": "./src/ParallaxManager.js",
26
+ "import": "./src/ParallaxManager.js",
27
+ "default": "./src/ParallaxManager.js"
28
+ },
29
+ "./bounds": {
30
+ "types": "./src/BoundsSystem.d.ts",
31
+ "node": "./src/BoundsSystem.js",
32
+ "import": "./src/BoundsSystem.js",
33
+ "default": "./src/BoundsSystem.js"
34
+ },
35
+ "./multi": {
36
+ "types": "./src/MultiTarget.d.ts",
37
+ "node": "./src/MultiTarget.js",
38
+ "import": "./src/MultiTarget.js",
39
+ "default": "./src/MultiTarget.js"
40
+ },
41
+ "./follow": {
42
+ "types": "./src/FollowMode.d.ts",
43
+ "node": "./src/FollowMode.js",
44
+ "import": "./src/FollowMode.js",
45
+ "default": "./src/FollowMode.js"
46
+ },
47
+ "./sequence": {
48
+ "types": "./src/CameraSequence.d.ts",
49
+ "node": "./src/CameraSequence.js",
50
+ "import": "./src/CameraSequence.js",
51
+ "default": "./src/CameraSequence.js"
52
+ },
53
+ "./package.json": "./package.json"
17
54
  },
18
55
  "files": [
19
56
  "src/",
20
57
  "README.md",
58
+ "CHANGELOG.md",
21
59
  "llms.txt",
22
60
  "LICENSE"
23
61
  ],
@@ -46,14 +84,20 @@
46
84
  "@zakkster/lite-timeline": "^1.0.0"
47
85
  },
48
86
  "devDependencies": {
87
+ "@zakkster/lite-gc-profiler": "^1.16.0",
88
+ "@zakkster/lite-leak": "^1.10.0",
89
+ "@zakkster/lite-signal": "^1.5.0",
49
90
  "esbuild": "^0.25.0",
50
- "vitest": "^4.1.4"
91
+ "typescript": "7.0.2"
51
92
  },
52
93
  "scripts": {
53
- "test": "vitest run",
54
- "test:watch": "vitest",
94
+ "test": "node --test test/*.test.js",
95
+ "test:gc": "node --expose-gc --test test/*.test.js",
96
+ "torture": "node --expose-gc test/torture.mjs",
97
+ "verify": "npm run test:gc && npm run torture && node test/size.mjs",
98
+ "typecheck": "tsc -p test/types-smoke/tsconfig.json",
55
99
  "bundle-check": "esbuild ./src/index.js --bundle --format=esm --outfile=test-bundle.js",
56
- "prepublishOnly": "npm run test && npm run bundle-check"
100
+ "prepublishOnly": "npm run verify && npm run typecheck && npm run bundle-check"
57
101
  },
58
102
  "keywords": [
59
103
  "camera",
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @zakkster/lite-camera-pro/bounds -- TypeScript declarations.
3
+ *
4
+ * Configurable per-edge boundary behavior. Pure math, no dependencies.
5
+ * Complete runtime surface, no `any`.
6
+ */
7
+
8
+ // -- Boundary types --
9
+ export declare const BoundsType: {
10
+ readonly HARD: 0;
11
+ readonly SOFT: 1;
12
+ readonly ELASTIC: 2;
13
+ readonly NONE: 3;
14
+ };
15
+
16
+ // -- Bounds system state (one per camera) --
17
+ export interface BoundsState {
18
+ left: number;
19
+ right: number;
20
+ top: number;
21
+ bottom: number;
22
+ softZone: number;
23
+ elasticMax: number;
24
+ elasticStrength: number;
25
+ boundsX: number;
26
+ boundsY: number;
27
+ boundsW: number;
28
+ boundsH: number;
29
+ customBounds: boolean;
30
+ }
31
+
32
+ // -- Per-edge config for setBoundsEdges --
33
+ export interface BoundsEdgesConfig {
34
+ left?: number;
35
+ right?: number;
36
+ top?: number;
37
+ bottom?: number;
38
+ }
39
+
40
+ export declare function createBoundsState(): BoundsState;
41
+ export declare function setBoundsAll(state: BoundsState, type: number): void;
42
+ export declare function setBoundsEdges(state: BoundsState, config: BoundsEdgesConfig): void;
43
+ export declare function setBoundsRect(state: BoundsState, x: number, y: number, w: number, h: number): void;
44
+ export declare function clearBoundsRect(state: BoundsState): void;
45
+ export declare function applyBounds(
46
+ state: BoundsState,
47
+ target: Float32Array,
48
+ pos: Float32Array,
49
+ maxX: number,
50
+ maxY: number,
51
+ visW: number,
52
+ visH: number,
53
+ dt: number,
54
+ ): void;
@@ -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} */
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @zakkster/lite-camera-pro/sequence -- TypeScript declarations.
3
+ *
4
+ * Timeline-driven camera choreography. NOTE: this subpath drags
5
+ * @zakkster/lite-timeline and @zakkster/lite-ease by design -- it is the only
6
+ * subpath that does. Complete runtime surface, no `any`.
7
+ */
8
+
9
+ import type { CinematicCameraPro } from './index.js';
10
+ import type { ShakeProfile } from './Shake.js';
11
+
12
+ // -- Sequence construction options --
13
+ export interface CameraSequenceOptions {
14
+ loop?: boolean;
15
+ onComplete?: () => void;
16
+ blendOutTime?: number;
17
+ }
18
+
19
+ // -- Per-step options --
20
+ export interface StepOptions {
21
+ ease?: (t: number) => number;
22
+ at?: string | number;
23
+ }
24
+
25
+ // -- A fluent camera sequence handle --
26
+ export interface CameraSequence {
27
+ moveTo(x: number, y: number, duration: number, opts?: StepOptions): CameraSequence;
28
+ zoomTo(level: number, duration: number, opts?: StepOptions): CameraSequence;
29
+ moveAndZoom(x: number, y: number, level: number, duration: number, opts?: StepOptions): CameraSequence;
30
+ shake(profileOrName: string | ShakeProfile, intensity?: number, opts?: StepOptions): CameraSequence;
31
+ wait(duration: number, opts?: StepOptions): CameraSequence;
32
+ call(fn: () => void, opts?: StepOptions): CameraSequence;
33
+ play(): CameraSequence;
34
+ pause(): CameraSequence;
35
+ resume(): CameraSequence;
36
+ stop(): CameraSequence;
37
+ seek(timeMs: number): CameraSequence;
38
+ destroy(): void;
39
+ readonly duration: number;
40
+ readonly progress: number;
41
+ readonly playing: boolean;
42
+ }
43
+
44
+ export declare function createCameraSequence(cam: CinematicCameraPro, options?: CameraSequenceOptions): CameraSequence;
45
+ export declare function panTo(
46
+ cam: CinematicCameraPro,
47
+ x: number,
48
+ y: number,
49
+ duration: number,
50
+ opts?: CameraSequenceOptions & StepOptions,
51
+ ): CameraSequence;
52
+ export declare function dramaticZoom(
53
+ cam: CinematicCameraPro,
54
+ x: number,
55
+ y: number,
56
+ zoom: number,
57
+ duration: number,
58
+ opts?: CameraSequenceOptions & StepOptions,
59
+ ): CameraSequence;
60
+ export declare function bossReveal(
61
+ cam: CinematicCameraPro,
62
+ x: number,
63
+ y: number,
64
+ totalMs?: number,
65
+ opts?: CameraSequenceOptions,
66
+ ): CameraSequence;
67
+ export declare function timedShake(
68
+ cam: CinematicCameraPro,
69
+ presetOrProfile: string | ShakeProfile,
70
+ holdMs?: number,
71
+ opts?: CameraSequenceOptions,
72
+ ): CameraSequence;
73
+
74
+ declare const _default: typeof createCameraSequence;
75
+ export default _default;
@@ -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
 
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @zakkster/lite-camera-pro/follow -- TypeScript declarations.
3
+ *
4
+ * Follow-mode enum and the strategy dispatch table. Complete runtime surface,
5
+ * no `any`.
6
+ */
7
+
8
+ import type { CinematicCameraPro } from './index.js';
9
+
10
+ // -- Follow modes --
11
+ export declare const FollowMode: {
12
+ readonly SMOOTH: 0;
13
+ readonly LOCK: 1;
14
+ readonly PREDICTIVE: 2;
15
+ readonly CUT: 3;
16
+ readonly HYBRID: 4;
17
+ };
18
+
19
+ // -- Strategy dispatch table, indexed by FollowMode value --
20
+ export declare const FOLLOW_STRATEGIES: ReadonlyArray<
21
+ (cam: CinematicCameraPro, dt: number, px: number, py: number, pvx: number, pvy: number) => void
22
+ >;
23
+
24
+ declare const _default: typeof FollowMode;
25
+ export default _default;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @zakkster/lite-camera-pro/multi -- TypeScript declarations.
3
+ *
4
+ * Multi-target framing: fit several targets in view. Pure math, no
5
+ * dependencies. Complete runtime surface, no `any`.
6
+ */
7
+
8
+ import type { CinematicCameraPro } from './index.js';
9
+
10
+ // -- A 2D point (target position) --
11
+ export interface Vec2 {
12
+ x: number;
13
+ y: number;
14
+ }
15
+
16
+ // -- Multi-target config/state (one per camera) --
17
+ export interface MultiTargetState {
18
+ active: boolean;
19
+ targets: Vec2[] | null;
20
+ count: number;
21
+ paddingX: number;
22
+ paddingY: number;
23
+ minZoom: number;
24
+ maxZoom: number;
25
+ zoomSpeed: number;
26
+ followSpeed: number;
27
+ }
28
+
29
+ // -- Options accepted by CinematicCameraPro.trackMultiple --
30
+ export interface MultiTargetOptions {
31
+ paddingX?: number;
32
+ paddingY?: number;
33
+ padding?: number;
34
+ minZoom?: number;
35
+ maxZoom?: number;
36
+ zoomSpeed?: number;
37
+ followSpeed?: number;
38
+ }
39
+
40
+ export declare function updateMultiTarget(
41
+ cam: CinematicCameraPro,
42
+ dt: number,
43
+ targets: Vec2[],
44
+ count: number,
45
+ ): void;
46
+ export declare function createMultiTargetState(): MultiTargetState;
47
+
48
+ declare const _default: typeof updateMultiTarget;
49
+ export default _default;
@@ -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
  /**
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @zakkster/lite-camera-pro/parallax -- TypeScript declarations.
3
+ *
4
+ * Multi-layer scroll manager. Pure math, no dependencies. Complete runtime
5
+ * surface, no `any`.
6
+ */
7
+
8
+ // -- Wrap modes for layer tiling --
9
+ export declare const WrapMode: {
10
+ readonly NONE: 0;
11
+ readonly REPEAT_X: 1;
12
+ readonly REPEAT_Y: 2;
13
+ readonly REPEAT_BOTH: 3;
14
+ };
15
+
16
+ // -- A single pre-allocated parallax layer --
17
+ export interface ParallaxLayer {
18
+ active: boolean;
19
+ id: string;
20
+ speedX: number;
21
+ speedY: number;
22
+ offsetX: number;
23
+ offsetY: number;
24
+ wrap: number;
25
+ scrollX: number;
26
+ scrollY: number;
27
+ }
28
+
29
+ // -- Parallax manager state (one per camera) --
30
+ export interface ParallaxState {
31
+ layers: ParallaxLayer[];
32
+ layerCount: number;
33
+ activeCount: number;
34
+ }
35
+
36
+ // -- Options accepted by addParallaxLayer --
37
+ export interface ParallaxLayerOptions {
38
+ offsetX?: number;
39
+ offsetY?: number;
40
+ wrap?: number;
41
+ }
42
+
43
+ // -- Output shape for getLayerScroll --
44
+ export interface ScrollOut {
45
+ x: number;
46
+ y: number;
47
+ }
48
+
49
+ export declare function createParallaxState(): ParallaxState;
50
+ export declare function addParallaxLayer(
51
+ state: ParallaxState,
52
+ id: string,
53
+ speedX: number,
54
+ speedY?: number,
55
+ opts?: ParallaxLayerOptions,
56
+ ): ParallaxLayer | null;
57
+ export declare function removeParallaxLayer(state: ParallaxState, id: string): void;
58
+ export declare function updateParallax(state: ParallaxState, camX: number, camY: number, zoom: number): void;
59
+ export declare function getLayerScroll(state: ParallaxState, id: string, out: ScrollOut): ScrollOut | null;
60
+ export declare function applyParallaxLayer(state: ParallaxState, id: string, ctx: CanvasRenderingContext2D): boolean;
@@ -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
  }
package/src/Shake.d.ts ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @zakkster/lite-camera-pro/shake -- TypeScript declarations.
3
+ *
4
+ * The ./shake subpath barrel: the noise-based shake engine (ShakeEngine.js)
5
+ * plus the built-in presets and registry (ShakePresets.js). Complete runtime
6
+ * surface, no `any`.
7
+ */
8
+
9
+ // -- Shake profile (input) --
10
+ export interface ShakeProfile {
11
+ /** Initial trauma [0, 1]. Undefined defaults to 0.5 in addShake. */
12
+ trauma?: number;
13
+ /** Noise sample frequency (higher = more jittery). Default 15. */
14
+ freq?: number;
15
+ /** Trauma units lost per second. Default 1. */
16
+ decay?: number;
17
+ /** Maximum pixel offset at trauma=1. Default 15. */
18
+ maxOffset?: number;
19
+ /** Maximum rotation (radians) at trauma=1. Default 0.05. */
20
+ maxAngle?: number;
21
+ /** Directional X component; (0,0) = omnidirectional. Default 0. */
22
+ dirX?: number;
23
+ /** Directional Y component. Default 0. */
24
+ dirY?: number;
25
+ /** Per-profile intensity multiplier used by addShake. Default 1. */
26
+ intensity?: number;
27
+ }
28
+
29
+ // -- A single pre-allocated shake slot --
30
+ export interface ShakeSlot {
31
+ active: boolean;
32
+ isDefault: boolean;
33
+ trauma: number;
34
+ decay: number;
35
+ freq: number;
36
+ time: number;
37
+ maxOffset: number;
38
+ maxAngle: number;
39
+ dirX: number;
40
+ dirY: number;
41
+ isDirectional: boolean;
42
+ }
43
+
44
+ // -- Shake engine state (one per camera) --
45
+ export interface ShakeState {
46
+ slots: ShakeSlot[];
47
+ slotCount: number;
48
+ seedOffset: number;
49
+ /** Computed X offset in pixels (read by apply()). */
50
+ offsetX: number;
51
+ /** Computed Y offset in pixels. */
52
+ offsetY: number;
53
+ /** Computed rotation in radians. */
54
+ angle: number;
55
+ /** Global shake scale (0 = no shake, 1 = normal). */
56
+ globalScale: number;
57
+ /** True when any slot is active. */
58
+ active: boolean;
59
+ }
60
+
61
+ // -- Shake engine functions (ShakeEngine.js) --
62
+ export declare function createShakeState(seedOffset?: number): ShakeState;
63
+ export declare function addShake(state: ShakeState, profile: ShakeProfile, intensity?: number): void;
64
+ export declare function addTraumaSimple(state: ShakeState, amount: number): void;
65
+ export declare function updateShake(state: ShakeState, dt: number): void;
66
+ export declare function computeShake(state: ShakeState): void;
67
+ export declare function clearShakes(state: ShakeState): void;
68
+
69
+ // -- Built-in presets (ShakePresets.js) --
70
+ export declare const EXPLOSION: Readonly<ShakeProfile>;
71
+ export declare const EARTHQUAKE: Readonly<ShakeProfile>;
72
+ export declare const RECOIL: Readonly<ShakeProfile>;
73
+ export declare const IMPACT: Readonly<ShakeProfile>;
74
+ export declare const LANDING: Readonly<ShakeProfile>;
75
+ export declare const DAMAGE: Readonly<ShakeProfile>;
76
+ export declare const RUMBLE: Readonly<ShakeProfile>;
77
+ export declare const HEAVY_IMPACT: Readonly<ShakeProfile>;
78
+
79
+ // -- Preset registry (ShakePresets.js) --
80
+ export declare function getPreset(name: string): Readonly<ShakeProfile> | null;
81
+ export declare function registerPreset(name: string, profile: ShakeProfile): void;
82
+ export declare function listPresets(): string[];
package/src/Shake.js ADDED
@@ -0,0 +1,7 @@
1
+ // @zakkster/lite-camera-pro -- the ./shake subpath barrel.
2
+ // Re-export only; never a copy. The camera class and this subpath import the
3
+ // SAME module files, so createShakeState et al. have one runtime identity.
4
+ // ShakePresets is folded in so a shake-only consumer gets presets + getPreset
5
+ // from a single import.
6
+ export * from './ShakeEngine.js';
7
+ export * from './ShakePresets.js';
@@ -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.d.ts CHANGED
@@ -1,68 +1,27 @@
1
1
  /**
2
- * @zakkster/lite-camera-pro TypeScript Declarations
2
+ * @zakkster/lite-camera-pro -- TypeScript declarations (main entry).
3
+ *
4
+ * The full functional layer is re-exported from the per-subsystem sibling
5
+ * declarations (one declaration per type -- no duplicates). This file adds the
6
+ * CinematicCameraPro class, the debug-HUD facade, and the VERSION const.
3
7
  */
4
8
 
5
- // ── Follow Modes ──
6
- export declare const FollowMode: {
7
- readonly SMOOTH: 0;
8
- readonly LOCK: 1;
9
- readonly PREDICTIVE: 2;
10
- readonly CUT: 3;
11
- readonly HYBRID: 4;
12
- };
13
-
14
- export declare const FOLLOW_STRATEGIES: ReadonlyArray<
15
- (cam: CinematicCameraPro, dt: number, px: number, py: number, pvx: number, pvy: number) => void
16
- >;
17
-
18
- // ── Wrap Modes ──
19
- export declare const WrapMode: {
20
- readonly NONE: 0;
21
- readonly REPEAT_X: 1;
22
- readonly REPEAT_Y: 2;
23
- readonly REPEAT_BOTH: 3;
24
- };
25
-
26
- // ── Bounds Types ──
27
- export declare const BoundsType: {
28
- readonly HARD: 0;
29
- readonly SOFT: 1;
30
- readonly ELASTIC: 2;
31
- readonly NONE: 3;
32
- };
33
-
34
- // ── Shake Profile ──
35
- export interface ShakeProfile {
36
- trauma?: number;
37
- freq?: number;
38
- decay?: number;
39
- maxOffset?: number;
40
- maxAngle?: number;
41
- dirX?: number;
42
- dirY?: number;
43
- }
44
-
45
- // ── Shake Presets ──
46
- export declare const EXPLOSION: Readonly<ShakeProfile>;
47
- export declare const EARTHQUAKE: Readonly<ShakeProfile>;
48
- export declare const RECOIL: Readonly<ShakeProfile>;
49
- export declare const IMPACT: Readonly<ShakeProfile>;
50
- export declare const LANDING: Readonly<ShakeProfile>;
51
- export declare const DAMAGE: Readonly<ShakeProfile>;
52
- export declare const RUMBLE: Readonly<ShakeProfile>;
53
- export declare const HEAVY_IMPACT: Readonly<ShakeProfile>;
54
-
55
- export declare function getPreset(name: string): ShakeProfile | null;
56
- export declare function registerPreset(name: string, profile: ShakeProfile): void;
57
- export declare function listPresets(): string[];
58
-
59
- // ── Vec2 ──
60
- export interface Vec2 {
61
- x: number;
62
- y: number;
63
- }
64
-
65
- // ── Debug HUD Config ──
9
+ // -- Functional layer (mirrors the ./shake, ./parallax, ./bounds, ./multi,
10
+ // ./follow, ./sequence subpaths; same runtime identities) --
11
+ export * from './Shake.js';
12
+ export * from './ParallaxManager.js';
13
+ export * from './BoundsSystem.js';
14
+ export * from './MultiTarget.js';
15
+ export * from './FollowMode.js';
16
+ export * from './CameraSequence.js';
17
+
18
+ // -- Types needed locally by the class declaration --
19
+ import type { ShakeProfile } from './Shake.js';
20
+ import type { Vec2, MultiTargetOptions } from './MultiTarget.js';
21
+ import type { BoundsEdgesConfig } from './BoundsSystem.js';
22
+ import type { CameraSequence, CameraSequenceOptions } from './CameraSequence.js';
23
+
24
+ // -- Debug HUD (DebugHUD.js) -- facade only, no dedicated subpath --
66
25
  export interface DebugHUDConfig {
67
26
  show: {
68
27
  position: boolean;
@@ -83,66 +42,14 @@ export declare function createDebugHUDConfig(): DebugHUDConfig;
83
42
  export declare function drawDebugHUD(cam: CinematicCameraPro, ctx: CanvasRenderingContext2D, config?: DebugHUDConfig): void;
84
43
  export declare function drawDebugWorld(cam: CinematicCameraPro, ctx: CanvasRenderingContext2D, config?: DebugHUDConfig): void;
85
44
 
86
- // ── Camera Sequence ──
87
- export interface CameraSequenceOptions {
88
- loop?: boolean;
89
- onComplete?: () => void;
90
- blendOutTime?: number;
91
- }
92
-
93
- export interface StepOptions {
94
- ease?: (t: number) => number;
95
- at?: string | number;
96
- }
97
-
98
- export interface CameraSequence {
99
- moveTo(x: number, y: number, duration: number, opts?: StepOptions): CameraSequence;
100
- zoomTo(level: number, duration: number, opts?: StepOptions): CameraSequence;
101
- moveAndZoom(x: number, y: number, level: number, duration: number, opts?: StepOptions): CameraSequence;
102
- shake(profileOrName: string | ShakeProfile, intensity?: number, opts?: StepOptions): CameraSequence;
103
- wait(duration: number, opts?: StepOptions): CameraSequence;
104
- call(fn: () => void, opts?: StepOptions): CameraSequence;
105
- play(): CameraSequence;
106
- pause(): CameraSequence;
107
- resume(): CameraSequence;
108
- stop(): CameraSequence;
109
- seek(timeMs: number): CameraSequence;
110
- destroy(): void;
111
- readonly duration: number;
112
- readonly progress: number;
113
- readonly playing: boolean;
114
- }
115
-
116
- export declare function createCameraSequence(cam: CinematicCameraPro, options?: CameraSequenceOptions): CameraSequence;
117
- export declare function panTo(cam: CinematicCameraPro, x: number, y: number, duration: number, opts?: CameraSequenceOptions & StepOptions): CameraSequence;
118
- export declare function dramaticZoom(cam: CinematicCameraPro, x: number, y: number, zoom: number, duration: number, opts?: CameraSequenceOptions & StepOptions): CameraSequence;
119
- export declare function bossReveal(cam: CinematicCameraPro, x: number, y: number, totalMs?: number, opts?: CameraSequenceOptions): CameraSequence;
120
- export declare function timedShake(cam: CinematicCameraPro, presetOrProfile: string | ShakeProfile, holdMs?: number, opts?: CameraSequenceOptions): CameraSequence;
121
-
122
- // ── Multi-Target Options ──
123
- export interface MultiTargetOptions {
124
- paddingX?: number;
125
- paddingY?: number;
126
- padding?: number;
127
- minZoom?: number;
128
- maxZoom?: number;
129
- zoomSpeed?: number;
130
- followSpeed?: number;
131
- }
132
-
133
- // ── Bounds Config ──
134
- export interface BoundsEdgesConfig {
135
- left?: number;
136
- right?: number;
137
- top?: number;
138
- bottom?: number;
139
- }
45
+ // -- Package version (bumped in lockstep with package.json + llms.txt) --
46
+ export declare const VERSION: string;
140
47
 
141
- // ── Main Camera Class ──
48
+ // -- Main camera class --
142
49
  export declare class CinematicCameraPro {
143
50
  constructor(viewW: number, viewH: number, worldW: number, worldH: number, seed?: number);
144
51
 
145
- // ── Public state ──
52
+ // Public state
146
53
  readonly pos: Float32Array;
147
54
  readonly target: Float32Array;
148
55
  readonly look: Float32Array;
@@ -170,58 +77,58 @@ export declare class CinematicCameraPro {
170
77
 
171
78
  debugConfig: DebugHUDConfig;
172
79
 
173
- // ── Follow Mode ──
80
+ // Follow mode
174
81
  setMode(mode: number): this;
175
82
 
176
- // ── Multi-Target ──
83
+ // Multi-target
177
84
  trackMultiple(targets: Vec2[], options?: MultiTargetOptions): this;
178
85
  trackSingle(): this;
179
86
  setTargetCount(count: number): this;
180
87
 
181
- // ── Shake ──
88
+ // Shake
182
89
  addTrauma(amount: number): this;
183
90
  shake(profile: ShakeProfile, intensity?: number): this;
184
91
  shakePreset(name: string, intensity?: number): this;
185
92
  clearShakes(): this;
186
93
 
187
- // ── Sequences ──
94
+ // Sequences
188
95
  createSequence(options?: CameraSequenceOptions): CameraSequence;
189
96
  playSequence(seq: CameraSequence): this;
190
97
  stopSequence(): this;
191
98
  readonly sequencePlaying: boolean;
192
99
 
193
- // ── Zoom ──
100
+ // Zoom
194
101
  setZoom(level: number, duration?: number, ease?: (t: number) => number): this;
195
102
  zoomAt(targetOrX: number | Vec2, yOrLevel: number, levelOrDur?: number, duration?: number, ease?: (t: number) => number): this;
196
103
 
197
- // ── Coordinate Conversion ──
104
+ // Coordinate conversion
198
105
  screenToWorld(sx: number, sy: number, out: Vec2): Vec2;
199
106
  worldToScreen(wx: number, wy: number, out: Vec2): Vec2;
200
107
 
201
- // ── Parallax ──
108
+ // Parallax
202
109
  addParallaxLayer(id: string, speedX: number, speedY?: number, opts?: { offsetX?: number; offsetY?: number; wrap?: number }): this;
203
110
  removeParallaxLayer(id: string): this;
204
111
  applyParallax(id: string, ctx: CanvasRenderingContext2D): boolean;
205
112
 
206
- // ── Bounds ──
113
+ // Bounds
207
114
  setBoundsType(type: number): this;
208
115
  setBoundsEdges(config: BoundsEdgesConfig): this;
209
116
  setBoundsRect(x: number, y: number, w: number, h: number): this;
210
117
  clearBoundsRect(): this;
211
118
 
212
- // ── Core ──
119
+ // Core
213
120
  update(dt: number, px: number, py: number, pvx?: number, pvy?: number): void;
214
121
  apply(ctx: CanvasRenderingContext2D): void;
215
122
 
216
- // ── Debug ──
123
+ // Debug
217
124
  debug(ctx: CanvasRenderingContext2D): void;
218
125
  debugHUD(ctx: CanvasRenderingContext2D): void;
219
126
 
220
- // ── Save / Load ──
127
+ // Save / load
221
128
  getState(): { posX: number; posY: number; targetX: number; targetY: number; zoom: number; mode: number };
222
129
  setState(snapshot: { posX?: number; posY?: number; targetX?: number; targetY?: number; zoom?: number; mode?: number }): this;
223
130
 
224
- // ── Lifecycle ──
131
+ // Lifecycle
225
132
  destroy(): void;
226
133
  }
227
134
 
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.1.0";
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';