@zakkster/lite-camera-pro 1.0.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/llms.txt ADDED
@@ -0,0 +1,115 @@
1
+ # @zakkster/lite-camera-pro
2
+
3
+ > Cinematic Canvas2D camera system for games. Zero-GC in the update/render hot
4
+ > path. Extends @zakkster/lite-camera's CinematicCamera. Adds smooth/eased zoom,
5
+ > 5 follow modes, multi-target auto-framing, an 8-slot noise-based shake engine
6
+ > with presets, fluent timeline sequences, a 16-layer parallax manager, per-edge
7
+ > bounds, a debug HUD, and zero-alloc coordinate conversion.
8
+
9
+ ESM only. `import` from `@zakkster/lite-camera-pro`. Default export is the
10
+ CinematicCameraPro class. All hot-path APIs avoid allocation; setup-time calls
11
+ (constructor, createSequence, trackMultiple) may allocate.
12
+
13
+ ## Install
14
+ npm install @zakkster/lite-camera-pro
15
+
16
+ ## Core class
17
+ new CinematicCameraPro(viewW, viewH, worldW, worldH, seed = 42)
18
+
19
+ Game loop (once per frame):
20
+ cam.update(dt, playerX, playerY, playerVX = 0, playerVY = 0)
21
+ Render:
22
+ ctx.save(); cam.apply(ctx); drawWorld(ctx); cam.debug(ctx); ctx.restore();
23
+ cam.debugHUD(ctx);
24
+ - apply(ctx): translates/scales/rotates the canvas to the camera transform (includes shake).
25
+ - debug(ctx): WORLD-space overlay (deadzone, lookahead, world bounds). Call inside the apply transform.
26
+ - debugHUD(ctx): SCREEN-space overlay (position, zoom, mode, per-slot trauma bars, sequence %). Call after restore.
27
+ update() dispatches by priority: active sequence > multi-target > single-target follow mode.
28
+
29
+ ## Follow modes
30
+ cam.setMode(FollowMode.SMOOTH | LOCK | PREDICTIVE | CUT | HYBRID) // returns this
31
+ - SMOOTH: deadzone + lookahead + lerp (default).
32
+ - LOCK: instant snap, no interpolation.
33
+ - PREDICTIVE: velocity extrapolation; tune cam.predictTime (seconds).
34
+ - CUT: hard jump, zero lerp (cutscene transitions).
35
+ - HYBRID: smooth horizontal, locked vertical; cam.hybridVerticalSnap = true|false.
36
+
37
+ ## Zoom
38
+ cam.setZoom(level, duration = 0, ease = null) // smooth eased zoom; duration 0 = instant
39
+ cam.zoomAt(x, y, level, duration = 0, ease = null) // zoom toward a static world point
40
+ cam.zoomAt(targetObj, level, duration = 0, ease = null) // zoom toward a moving {x,y}; anchor re-read each frame
41
+ cam.minZoom, cam.maxZoom // clamp limits
42
+ cam.visibleW, cam.visibleH // cached visible area (viewW/zoom, viewH/zoom); use for culling
43
+
44
+ ## Coordinate conversion (zero-alloc, caller-owned out)
45
+ const pt = { x: 0, y: 0 }; // allocate once
46
+ cam.screenToWorld(sx, sy, pt); // mutates pt, returns pt
47
+ cam.worldToScreen(wx, wy, pt); // mutates pt, returns pt
48
+
49
+ ## Multi-target framing
50
+ cam.trackMultiple(targets, { padding, minZoom, maxZoom, zoomSpeed, followSpeed }) // auto-zoom + center on bbox
51
+ cam.trackSingle() // back to single-target follow
52
+ cam.setTargetCount(n) // resize without reallocating
53
+ targets: array of objects with numeric x, y.
54
+
55
+ ## Shake (8 simultaneous slots, summed; simplex-noise driven)
56
+ cam.addTrauma(amount) // simple omnidirectional trauma (backward compatible)
57
+ cam.shake(profile, intensity = 1) // custom: { trauma, freq, decay, maxOffset, maxAngle, dirX, dirY }
58
+ cam.shakePreset(name, intensity = 1) // named preset
59
+ cam.clearShakes()
60
+ Preset names: 'explosion','earthquake','recoil','impact','landing','damage','rumble','heavy_impact'.
61
+ Registry (named exports): registerPreset(name, profile), listPresets(), getPreset(name).
62
+ Preset constants exported: EXPLOSION, EARTHQUAKE, RECOIL, IMPACT, LANDING, DAMAGE, RUMBLE, HEAVY_IMPACT.
63
+
64
+ ## Sequences (fluent timeline; takes over position + zoom, blends back to follow on completion)
65
+ const seq = cam.createSequence({ onComplete })
66
+ .moveTo(x, y, durationMs)
67
+ .zoomTo(level, durationMs)
68
+ .moveAndZoom(x, y, level, durationMs, { ease })
69
+ .shake(presetName)
70
+ .wait(ms)
71
+ .call(fn);
72
+ cam.playSequence(seq);
73
+ cam.stopSequence(); // cancel + smooth return to follow
74
+ seq.pause(); seq.resume(); seq.seek(ms);
75
+ Sequence preset helpers (named exports, return a sequence): panTo(cam, x, y, ms),
76
+ dramaticZoom(cam, x, y, level, ms), bossReveal(cam, x, y, ms), timedShake(cam, presetName, ms).
77
+ Read cam.sequencePlaying (boolean).
78
+
79
+ ## Parallax (up to 16 layers)
80
+ cam.addParallaxLayer(id, speedX, speedY = speedX, opts) // re-adding same id updates it
81
+ cam.removeParallaxLayer(id)
82
+ cam.applyParallax(id, ctx) // per-layer transform; wrap in ctx.save()/restore()
83
+ WrapMode exported for layer wrapping behavior.
84
+
85
+ ## Bounds (per-edge: HARD | SOFT | ELASTIC | NONE)
86
+ import { BoundsType } from '@zakkster/lite-camera-pro';
87
+ cam.setBoundsType(BoundsType.SOFT) // returns this
88
+ cam.setBoundsEdges({ left, right, top, bottom }) // each a BoundsType
89
+ cam.setBoundsRect(x, y, w, h) // constrain to a rectangle (rooms/arenas)
90
+ cam.clearBoundsRect() // revert to full world
91
+ Tuning: cam._bounds.softZone, cam._bounds.elasticMax, cam._bounds.elasticStrength.
92
+
93
+ ## Save / load
94
+ const snap = cam.getState(); // { posX, posY, targetX, targetY, zoom, mode }
95
+ cam.setState(snap); // restores and recomputes visible dims
96
+ cam.destroy(); // releases internal state
97
+
98
+ ## Debug HUD
99
+ cam.debugConfig.show.shake | parallax | bounds | ... = true|false // toggle panels
100
+ createDebugHUDConfig() exported to build a config; drawDebugWorld(cam, ctx, config) and drawDebugHUD(cam, ctx, config) are the underlying draws (cam.debug / cam.debugHUD wrap them).
101
+
102
+ ## Standalone functional API (tree-shakeable; the class composes these)
103
+ Shake: createShakeState, addShake, addTraumaSimple, updateShake, computeShake, clearShakes
104
+ Parallax: createParallaxState, addParallaxLayer, removeParallaxLayer, updateParallax, getLayerScroll, applyParallaxLayer
105
+ Bounds: createBoundsState, setBoundsAll, setBoundsEdges, setBoundsRect, clearBoundsRect, applyBounds
106
+ Multi: createMultiTargetState, updateMultiTarget
107
+ Sequence: createCameraSequence, panTo, dramaticZoom, bossReveal, timedShake
108
+ Enums: FollowMode, FOLLOW_STRATEGIES, BoundsType, WrapMode
109
+
110
+ ## Dependencies (all @zakkster, first-party)
111
+ @zakkster/lite-camera (base CinematicCamera), lite-ease, lite-lerp, lite-noise, lite-timeline.
112
+
113
+ ## Notes
114
+ - Migration: CinematicCameraPro is a drop-in superset of lite-camera's CinematicCamera.
115
+ - 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 ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@zakkster/lite-camera-pro",
3
+ "author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
4
+ "version": "1.0.0",
5
+ "description": "Cinematic camera system for Canvas2D games. Zero-GC, multi-target framing, noise-based shake, and timeline sequences.",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./src/index.js",
9
+ "types": "./src/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./src/index.d.ts",
13
+ "node": "./src/index.js",
14
+ "import": "./src/index.js",
15
+ "default": "./src/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "src/",
20
+ "README.md",
21
+ "llms.txt",
22
+ "LICENSE"
23
+ ],
24
+ "license": "MIT",
25
+ "homepage": "https://github.com/PeshoVurtoleta/lite-camera-pro#readme",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/PeshoVurtoleta/lite-camera-pro.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/PeshoVurtoleta/lite-camera-pro/issues",
32
+ "email": "shinikchiev@yahoo.com"
33
+ },
34
+ "funding": {
35
+ "type": "github",
36
+ "url": "https://github.com/sponsors/PeshoVurtoleta"
37
+ },
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
41
+ "dependencies": {
42
+ "@zakkster/lite-camera": "^1.0.0",
43
+ "@zakkster/lite-ease": "^1.0.0",
44
+ "@zakkster/lite-lerp": "^1.0.0",
45
+ "@zakkster/lite-noise": "^1.0.0",
46
+ "@zakkster/lite-timeline": "^1.0.0"
47
+ },
48
+ "devDependencies": {
49
+ "esbuild": "^0.25.0",
50
+ "vitest": "^4.1.4"
51
+ },
52
+ "scripts": {
53
+ "test": "vitest run",
54
+ "test:watch": "vitest",
55
+ "bundle-check": "esbuild ./src/index.js --bundle --format=esm --outfile=test-bundle.js",
56
+ "prepublishOnly": "npm run test && npm run bundle-check"
57
+ },
58
+ "keywords": [
59
+ "camera",
60
+ "cinematic",
61
+ "canvas2d",
62
+ "zero-gc",
63
+ "gamedev",
64
+ "html5",
65
+ "performance",
66
+ "screen-shake",
67
+ "parallax"
68
+ ]
69
+ }
@@ -0,0 +1,220 @@
1
+ /**
2
+ * @zakkster/lite-camera-pro — Smart Bounds System
3
+ *
4
+ * Replaces hard edge clamping with configurable per-edge behavior.
5
+ *
6
+ * Boundary types:
7
+ * HARD — stops at edge (default, same as lite-camera base)
8
+ * SOFT — decelerates smoothly near edge using smoothstep
9
+ * ELASTIC — allows slight overshoot, springs back
10
+ * NONE — no boundary enforcement
11
+ *
12
+ * Zero allocations. All state is pre-allocated on the camera.
13
+ *
14
+ * Depends on: @zakkster/lite-lerp (clamp, smoothstep)
15
+ */
16
+
17
+ /** @enum {number} */
18
+ export const BoundsType = {
19
+ HARD: 0,
20
+ SOFT: 1,
21
+ ELASTIC: 2,
22
+ NONE: 3,
23
+ };
24
+
25
+ /**
26
+ * Create the bounds system state. Allocated once per camera.
27
+ * @returns {Object}
28
+ */
29
+ export function createBoundsState() {
30
+ return {
31
+ // Per-edge boundary type
32
+ left: BoundsType.HARD,
33
+ right: BoundsType.HARD,
34
+ top: BoundsType.HARD,
35
+ bottom: BoundsType.HARD,
36
+
37
+ // Soft zone width: how far from edge deceleration starts (pixels)
38
+ softZone: 80,
39
+
40
+ // Elastic overshoot: max pixels past the boundary
41
+ elasticMax: 30,
42
+
43
+ // Elastic spring-back strength (higher = snappier return)
44
+ elasticStrength: 8.0,
45
+
46
+ // Custom world bounds (null = use camera worldW/worldH)
47
+ boundsX: 0,
48
+ boundsY: 0,
49
+ boundsW: 0,
50
+ boundsH: 0,
51
+ customBounds: false,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Set the boundary type for all edges at once.
57
+ *
58
+ * @param {Object} state BoundsState (cam._bounds)
59
+ * @param {number} type BoundsType enum
60
+ */
61
+ export function setBoundsAll(state, type) {
62
+ state.left = state.right = state.top = state.bottom = type;
63
+ }
64
+
65
+ /**
66
+ * Set boundary types per edge.
67
+ *
68
+ * @param {Object} state BoundsState
69
+ * @param {Object} config
70
+ * @param {number} [config.left]
71
+ * @param {number} [config.right]
72
+ * @param {number} [config.top]
73
+ * @param {number} [config.bottom]
74
+ */
75
+ export function setBoundsEdges(state, config) {
76
+ if (config.left !== undefined) state.left = config.left;
77
+ if (config.right !== undefined) state.right = config.right;
78
+ if (config.top !== undefined) state.top = config.top;
79
+ if (config.bottom !== undefined) state.bottom = config.bottom;
80
+ }
81
+
82
+ /**
83
+ * Set a custom bounds rectangle (for dynamic bounds during gameplay).
84
+ *
85
+ * @param {Object} state BoundsState
86
+ * @param {number} x
87
+ * @param {number} y
88
+ * @param {number} w
89
+ * @param {number} h
90
+ */
91
+ export function setBoundsRect(state, x, y, w, h) {
92
+ state.boundsX = x;
93
+ state.boundsY = y;
94
+ state.boundsW = w;
95
+ state.boundsH = h;
96
+ state.customBounds = true;
97
+ }
98
+
99
+ /**
100
+ * Clear custom bounds, reverting to full world size.
101
+ * @param {Object} state BoundsState
102
+ */
103
+ export function clearBoundsRect(state) {
104
+ state.customBounds = false;
105
+ }
106
+
107
+ // ── Smoothstep (inlined to avoid import for single use) ──
108
+ function smoothstep(edge0, edge1, x) {
109
+ const t = x < edge0 ? 0 : (x > edge1 ? 1 : (x - edge0) / (edge1 - edge0));
110
+ return t * t * (3 - 2 * t);
111
+ }
112
+
113
+ /**
114
+ * Apply boundary enforcement to camera target position.
115
+ * Replaces the simple clamp in camera.update().
116
+ *
117
+ * @param {Object} state BoundsState
118
+ * @param {Float32Array} target cam.target (mutated)
119
+ * @param {Float32Array} pos cam.pos (mutated for elastic)
120
+ * @param {number} maxX Default maximum X (worldW - visibleW)
121
+ * @param {number} maxY Default maximum Y (worldH - visibleH)
122
+ * @param {number} visW Visible width (viewW / zoom)
123
+ * @param {number} visH Visible height (viewH / zoom)
124
+ * @param {number} dt Delta time
125
+ */
126
+ export function applyBounds(state, target, pos, maxX, maxY, visW, visH, dt) {
127
+ let minBX = 0, maxBX = maxX;
128
+ let minBY = 0, maxBY = maxY;
129
+
130
+ if (state.customBounds) {
131
+ // Custom bounds define a world-space rectangle.
132
+ // Camera target is the top-left of the visible area.
133
+ // Min = bounds origin. Max = bounds origin + bounds size - visible size.
134
+ minBX = state.boundsX;
135
+ minBY = state.boundsY;
136
+ maxBX = state.boundsX + state.boundsW - visW;
137
+ maxBY = state.boundsY + state.boundsH - visH;
138
+
139
+ // If visible area exceeds bounds, center the camera within the bounds
140
+ if (maxBX < minBX) { const mid = (minBX + maxBX) * 0.5; minBX = maxBX = mid; }
141
+ if (maxBY < minBY) { const mid = (minBY + maxBY) * 0.5; minBY = maxBY = mid; }
142
+ }
143
+
144
+ const sz = state.softZone;
145
+ const eMax = state.elasticMax;
146
+ const eStr = state.elasticStrength;
147
+
148
+ _applyEdge(state.left, target, pos, 0, minBX, true, sz, eMax, eStr, dt);
149
+ _applyEdge(state.right, target, pos, 0, maxBX, false, sz, eMax, eStr, dt);
150
+ _applyEdge(state.top, target, pos, 1, minBY, true, sz, eMax, eStr, dt);
151
+ _applyEdge(state.bottom, target, pos, 1, maxBY, false, sz, eMax, eStr, dt);
152
+ }
153
+
154
+ /**
155
+ * Apply a single edge constraint.
156
+ *
157
+ * @param {number} type BoundsType
158
+ * @param {Float32Array} target
159
+ * @param {Float32Array} pos
160
+ * @param {number} axis 0=X, 1=Y
161
+ * @param {number} edge The boundary value
162
+ * @param {boolean} isMin true = left/top (target must be >= edge), false = right/bottom (<=)
163
+ * @param {number} sz Soft zone width
164
+ * @param {number} eMax Elastic max overshoot
165
+ * @param {number} eStr Elastic spring strength
166
+ * @param {number} dt
167
+ */
168
+ function _applyEdge(type, target, pos, axis, edge, isMin, sz, eMax, eStr, dt) {
169
+ const val = target[axis];
170
+
171
+ switch (type) {
172
+ case BoundsType.HARD:
173
+ if (isMin && val < edge) target[axis] = edge;
174
+ if (!isMin && val > edge) target[axis] = edge;
175
+ break;
176
+
177
+ case BoundsType.SOFT: {
178
+ // Decelerate smoothly as we approach the edge
179
+ if (isMin && val < edge + sz) {
180
+ // How deep into the soft zone (0 = at edge, 1 = at zone boundary)
181
+ const t = smoothstep(edge, edge + sz, val);
182
+ // Blend target toward the edge
183
+ target[axis] = edge + (val - edge) * t;
184
+ if (target[axis] < edge) target[axis] = edge;
185
+ }
186
+ if (!isMin && val > edge - sz) {
187
+ const t = smoothstep(edge, edge - sz, val);
188
+ target[axis] = edge + (val - edge) * t;
189
+ if (target[axis] > edge) target[axis] = edge;
190
+ }
191
+ break;
192
+ }
193
+
194
+ case BoundsType.ELASTIC: {
195
+ // Allow overshoot up to eMax, then spring back
196
+ if (isMin && val < edge) {
197
+ const overshoot = edge - val;
198
+ if (overshoot > eMax) {
199
+ target[axis] = edge - eMax;
200
+ }
201
+ // Safe, frame-rate independent spring back
202
+ const springLerp = 1 - Math.exp(-eStr * dt);
203
+ pos[axis] += (edge - pos[axis]) * springLerp;
204
+ }
205
+ if (!isMin && val > edge) {
206
+ const overshoot = val - edge;
207
+ if (overshoot > eMax) {
208
+ target[axis] = edge + eMax;
209
+ }
210
+ const springLerp = 1 - Math.exp(-eStr * dt);
211
+ pos[axis] += (edge - pos[axis]) * springLerp;
212
+ }
213
+ break;
214
+ }
215
+
216
+ case BoundsType.NONE:
217
+ // No enforcement
218
+ break;
219
+ }
220
+ }