@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/LICENSE +21 -0
- package/README.md +692 -0
- package/llms.txt +115 -0
- package/package.json +69 -0
- package/src/BoundsSystem.js +220 -0
- package/src/CameraSequence.js +513 -0
- package/src/CinematicCameraPro.js +894 -0
- package/src/DebugHUD.js +290 -0
- package/src/FollowMode.js +179 -0
- package/src/MultiTarget.js +125 -0
- package/src/ParallaxManager.js +199 -0
- package/src/ShakeEngine.js +286 -0
- package/src/ShakePresets.js +177 -0
- package/src/index.d.ts +228 -0
- package/src/index.js +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
# @zakkster/lite-camera-pro
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@zakkster/lite-camera-pro)
|
|
4
|
+

|
|
5
|
+
[](https://github.com/sponsors/PeshoVurtoleta)
|
|
6
|
+
[](https://bundlephobia.com/result?p=@zakkster/lite-camera-pro)
|
|
7
|
+
[](https://www.npmjs.com/package/@zakkster/lite-camera-pro)
|
|
8
|
+
[](https://www.npmjs.com/package/@zakkster/lite-camera-pro)
|
|
9
|
+

|
|
10
|
+
[](https://opensource.org/licenses/MIT)
|
|
11
|
+
|
|
12
|
+
### Cinematic Camera System for Canvas2D Games
|
|
13
|
+
|
|
14
|
+
> Zero-GC. Zero external deps. Framework-agnostic.
|
|
15
|
+
> One import. Every camera feature a 2D game needs.
|
|
16
|
+
|
|
17
|
+
**2,916 lines of source · 10 modules · Full TypeScript · 105 unit tests**
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
npm install @zakkster/lite-camera-pro
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Why Pro?
|
|
26
|
+
|
|
27
|
+
You already ship games with `@zakkster/lite-camera`. It handles follow, deadzone, lookahead, and basic shake. That's enough to prototype.
|
|
28
|
+
|
|
29
|
+
**lite-camera-pro** is what you reach for when the prototype becomes a product:
|
|
30
|
+
|
|
31
|
+
- The boss reveal that zooms in, shakes, holds, then swoops back
|
|
32
|
+
- The co-op mode where the camera frames both players automatically
|
|
33
|
+
- The parallax layers that scroll at different depths
|
|
34
|
+
- The platformer camera that's smooth horizontally but pixel-locked vertically
|
|
35
|
+
- The explosion that layers three different shakes simultaneously
|
|
36
|
+
|
|
37
|
+
All of it runs at 60fps with **zero garbage collection** in the hot path.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Architecture
|
|
42
|
+
|
|
43
|
+
```mermaid
|
|
44
|
+
graph TB
|
|
45
|
+
subgraph "@zakkster/lite-camera-pro"
|
|
46
|
+
Core["CinematicCameraPro<br/><i>894 lines · main class</i>"]
|
|
47
|
+
Follow["FollowMode<br/><i>179 lines · 5 strategies</i>"]
|
|
48
|
+
Multi["MultiTarget<br/><i>125 lines · bbox framing</i>"]
|
|
49
|
+
Shake["ShakeEngine<br/><i>286 lines · 8-slot noise pool</i>"]
|
|
50
|
+
Presets["ShakePresets<br/><i>177 lines · 8 built-in profiles</i>"]
|
|
51
|
+
Seq["CameraSequence<br/><i>513 lines · timeline cinematics</i>"]
|
|
52
|
+
Parallax["ParallaxManager<br/><i>199 lines · 16 layers</i>"]
|
|
53
|
+
Bounds["BoundsSystem<br/><i>220 lines · per-edge behavior</i>"]
|
|
54
|
+
Debug["DebugHUD<br/><i>290 lines · toggleable overlay</i>"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
Core --> Follow
|
|
58
|
+
Core --> Multi
|
|
59
|
+
Core --> Shake
|
|
60
|
+
Core --> Seq
|
|
61
|
+
Core --> Parallax
|
|
62
|
+
Core --> Bounds
|
|
63
|
+
Core --> Debug
|
|
64
|
+
Shake --> Presets
|
|
65
|
+
|
|
66
|
+
style Core fill:#fbbf24,stroke:#92400e,color:#000
|
|
67
|
+
style Seq fill:#a78bfa,stroke:#5b21b6,color:#000
|
|
68
|
+
style Shake fill:#ef4444,stroke:#991b1b,color:#fff
|
|
69
|
+
style Follow fill:#22d3ee,stroke:#155e75,color:#000
|
|
70
|
+
style Multi fill:#34d399,stroke:#065f46,color:#000
|
|
71
|
+
style Parallax fill:#34d399,stroke:#065f46,color:#000
|
|
72
|
+
style Bounds fill:#22d3ee,stroke:#155e75,color:#000
|
|
73
|
+
style Presets fill:#f87171,stroke:#991b1b,color:#000
|
|
74
|
+
style Debug fill:#6b7280,stroke:#374151,color:#fff
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Every module is a separate file. Tree-shaking drops what you don't use.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Dependency Graph
|
|
82
|
+
|
|
83
|
+
```mermaid
|
|
84
|
+
graph LR
|
|
85
|
+
Pro["lite-camera-pro"]
|
|
86
|
+
Cam["lite-camera"]
|
|
87
|
+
Ease["lite-ease"]
|
|
88
|
+
Lerp["lite-lerp"]
|
|
89
|
+
Noise["lite-noise"]
|
|
90
|
+
TL["lite-timeline"]
|
|
91
|
+
|
|
92
|
+
Pro --> Cam
|
|
93
|
+
Pro --> Ease
|
|
94
|
+
Pro --> Lerp
|
|
95
|
+
Pro --> Noise
|
|
96
|
+
Pro --> TL
|
|
97
|
+
|
|
98
|
+
style Pro fill:#fbbf24,stroke:#92400e,color:#000,stroke-width:2px
|
|
99
|
+
style Cam fill:#1e1e2e,stroke:#fbbf24,color:#fbbf24
|
|
100
|
+
style Ease fill:#1e1e2e,stroke:#a78bfa,color:#a78bfa
|
|
101
|
+
style Lerp fill:#1e1e2e,stroke:#34d399,color:#34d399
|
|
102
|
+
style Noise fill:#1e1e2e,stroke:#ef4444,color:#ef4444
|
|
103
|
+
style TL fill:#1e1e2e,stroke:#a78bfa,color:#a78bfa
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
All `@zakkster` packages. Zero third-party dependencies in the final bundle.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## lite-camera vs lite-camera-pro
|
|
111
|
+
|
|
112
|
+
| Feature | lite-camera | lite-camera-pro |
|
|
113
|
+
|:---|:---:|:---:|
|
|
114
|
+
| Smooth follow + deadzone + lookahead | ✓ | ✓ |
|
|
115
|
+
| Basic RNG shake | ✓ | — |
|
|
116
|
+
| Canvas transform apply | ✓ | ✓ |
|
|
117
|
+
| Debug rectangle | ✓ | ✓ |
|
|
118
|
+
| | | |
|
|
119
|
+
| **Zoom** (smooth, eased, anchor-point) | | ✓ |
|
|
120
|
+
| **Dynamic zoom-at-target** (tracks moving objects) | | ✓ |
|
|
121
|
+
| **5 follow modes** (smooth / lock / predictive / cut / hybrid) | | ✓ |
|
|
122
|
+
| **Multi-target auto-framing** (bounding box + auto-zoom) | | ✓ |
|
|
123
|
+
| **Noise-based shake** (simplex, 8 simultaneous layers) | | ✓ |
|
|
124
|
+
| **Directional shake** (recoil, landing, per-axis) | | ✓ |
|
|
125
|
+
| **8 shake presets** + custom registry | | ✓ |
|
|
126
|
+
| **Cinematic sequences** (timeline-driven camera moves) | | ✓ |
|
|
127
|
+
| **Fluent sequence builder** (moveTo · zoomTo · shake · wait · call) | | ✓ |
|
|
128
|
+
| **Sequence presets** (panTo · dramaticZoom · bossReveal) | | ✓ |
|
|
129
|
+
| **Parallax layer manager** (16 layers, per-layer speed) | | ✓ |
|
|
130
|
+
| **Smart bounds** (hard / soft / elastic / none — per-edge) | | ✓ |
|
|
131
|
+
| **Dynamic bounds** (room transitions, arenas) | | ✓ |
|
|
132
|
+
| **Pro debug HUD** (toggleable panels, trauma bars, sequence progress) | | ✓ |
|
|
133
|
+
| **Zero-alloc coordinate conversion** (screenToWorld / worldToScreen) | | ✓ |
|
|
134
|
+
| **Save / Load** (getState / setState) | | ✓ |
|
|
135
|
+
| **TypeScript declarations** | | ✓ |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Quick Start
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
import { CinematicCameraPro, FollowMode } from '@zakkster/lite-camera-pro';
|
|
143
|
+
|
|
144
|
+
const cam = new CinematicCameraPro(
|
|
145
|
+
canvas.width, // viewport width
|
|
146
|
+
canvas.height, // viewport height
|
|
147
|
+
WORLD_W, // world width
|
|
148
|
+
WORLD_H // world height
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// ── Game loop ──
|
|
152
|
+
function update(dt) {
|
|
153
|
+
cam.update(dt, player.x, player.y, player.vx, player.vy);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function render(ctx) {
|
|
157
|
+
ctx.save();
|
|
158
|
+
cam.apply(ctx); // transforms the canvas
|
|
159
|
+
drawWorld(ctx);
|
|
160
|
+
cam.debug(ctx); // world-space overlay
|
|
161
|
+
ctx.restore();
|
|
162
|
+
cam.debugHUD(ctx); // screen-space HUD
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Feature Guide
|
|
169
|
+
|
|
170
|
+
### Follow Modes
|
|
171
|
+
|
|
172
|
+
```mermaid
|
|
173
|
+
stateDiagram-v2
|
|
174
|
+
direction LR
|
|
175
|
+
SMOOTH --> LOCK : setMode()
|
|
176
|
+
SMOOTH --> PREDICTIVE : setMode()
|
|
177
|
+
SMOOTH --> CUT : setMode()
|
|
178
|
+
SMOOTH --> HYBRID : setMode()
|
|
179
|
+
LOCK --> SMOOTH : setMode()
|
|
180
|
+
PREDICTIVE --> SMOOTH : setMode()
|
|
181
|
+
CUT --> SMOOTH : setMode()
|
|
182
|
+
HYBRID --> SMOOTH : setMode()
|
|
183
|
+
|
|
184
|
+
note right of SMOOTH : Deadzone + lookahead + lerp<br/>Default. Good for everything.
|
|
185
|
+
note right of LOCK : Instant snap. No interpolation.<br/>Top-down shooters.
|
|
186
|
+
note right of PREDICTIVE : Velocity extrapolation.<br/>Racing games, fast runners.
|
|
187
|
+
note right of CUT : Hard jump. Zero lerp.<br/>Cutscene transitions.
|
|
188
|
+
note right of HYBRID : Smooth-X, locked-Y.<br/>Platformer standard.
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Switch mid-gameplay. No position jumps (except CUT, which jumps by design).
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
cam.setMode(FollowMode.SMOOTH); // deadzone + lookahead + lerp
|
|
195
|
+
cam.setMode(FollowMode.LOCK); // instant snap, no interpolation
|
|
196
|
+
cam.setMode(FollowMode.PREDICTIVE); // velocity extrapolation
|
|
197
|
+
cam.setMode(FollowMode.CUT); // hard cut (cutscene transitions)
|
|
198
|
+
cam.setMode(FollowMode.HYBRID); // smooth horizontal, locked vertical
|
|
199
|
+
|
|
200
|
+
// Predictive tuning
|
|
201
|
+
cam.predictTime = 0.5; // seconds of velocity extrapolation
|
|
202
|
+
|
|
203
|
+
// Hybrid tuning
|
|
204
|
+
cam.hybridVerticalSnap = true; // instant vertical (default)
|
|
205
|
+
cam.hybridVerticalSnap = false; // fast-lerp vertical
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
### Zoom System
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
// Smooth zoom with easing
|
|
214
|
+
cam.setZoom(2.0, 0.5, easeOutExpo); // zoom to 2× over 0.5s
|
|
215
|
+
|
|
216
|
+
// Zoom toward a static world point
|
|
217
|
+
cam.zoomAt(400, 300, 1.8, 0.8, easeOutExpo);
|
|
218
|
+
|
|
219
|
+
// Zoom toward a MOVING target — anchor follows the object each frame
|
|
220
|
+
cam.zoomAt(boss, 1.8, 0.8, easeOutExpo);
|
|
221
|
+
|
|
222
|
+
// Zoom limits
|
|
223
|
+
cam.minZoom = 0.25;
|
|
224
|
+
cam.maxZoom = 4.0;
|
|
225
|
+
|
|
226
|
+
// Read visible area (cached, zero-alloc — use for frustum culling)
|
|
227
|
+
const w = cam.visibleW; // viewW / zoom
|
|
228
|
+
const h = cam.visibleH; // viewH / zoom
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Coordinate conversion** (zero-alloc, caller-owned `out` pattern):
|
|
232
|
+
```js
|
|
233
|
+
const pt = { x: 0, y: 0 }; // allocate once at init
|
|
234
|
+
cam.screenToWorld(mouseX, mouseY, pt);
|
|
235
|
+
cam.worldToScreen(enemy.x, enemy.y, pt);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
### Multi-Target Framing
|
|
241
|
+
|
|
242
|
+
```mermaid
|
|
243
|
+
graph LR
|
|
244
|
+
subgraph Viewport
|
|
245
|
+
direction TB
|
|
246
|
+
P1["Player 1"]
|
|
247
|
+
P2["Player 2"]
|
|
248
|
+
BB["Bounding Box<br/>+ padding"]
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
BB --> AutoZoom["Auto-Zoom<br/><i>fit bbox into viewport</i>"]
|
|
252
|
+
BB --> AutoCenter["Auto-Center<br/><i>track bbox midpoint</i>"]
|
|
253
|
+
AutoZoom --> Smooth["Exponential<br/>Damping"]
|
|
254
|
+
AutoCenter --> Smooth
|
|
255
|
+
|
|
256
|
+
style P1 fill:#fbbf24,stroke:#92400e,color:#000
|
|
257
|
+
style P2 fill:#22d3ee,stroke:#155e75,color:#000
|
|
258
|
+
style BB fill:none,stroke:#a78bfa,stroke-dasharray:5 5,color:#a78bfa
|
|
259
|
+
style AutoZoom fill:#a78bfa,stroke:#5b21b6,color:#000
|
|
260
|
+
style AutoCenter fill:#a78bfa,stroke:#5b21b6,color:#000
|
|
261
|
+
style Smooth fill:#34d399,stroke:#065f46,color:#000
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
```js
|
|
265
|
+
// Track two players — camera auto-zooms to keep both visible
|
|
266
|
+
cam.trackMultiple([player1, player2], {
|
|
267
|
+
padding: 120, // world-space padding around the bounding box
|
|
268
|
+
minZoom: 0.4,
|
|
269
|
+
maxZoom: 1.8,
|
|
270
|
+
zoomSpeed: 4.0, // zoom smoothing (higher = snappier)
|
|
271
|
+
followSpeed: 5.0, // position smoothing
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Add a third target dynamically
|
|
275
|
+
cam.trackMultiple([player1, player2, boss], { padding: 100 });
|
|
276
|
+
|
|
277
|
+
// Return to single-target follow (smooth transition)
|
|
278
|
+
cam.trackSingle();
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
### Shake System
|
|
284
|
+
|
|
285
|
+
```mermaid
|
|
286
|
+
graph TB
|
|
287
|
+
subgraph "Shake Engine — 8 simultaneous slots"
|
|
288
|
+
S1["Slot 1<br/>EXPLOSION<br/>trauma=0.8"]
|
|
289
|
+
S2["Slot 2<br/>RECOIL ↑<br/>trauma=0.5"]
|
|
290
|
+
S3["Slot 3<br/>RUMBLE<br/>trauma=0.2"]
|
|
291
|
+
S4["Slot 4–8<br/><i>available</i>"]
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
S1 --> Sum["Sum All Layers"]
|
|
295
|
+
S2 --> Sum
|
|
296
|
+
S3 --> Sum
|
|
297
|
+
Sum --> Noise["Simplex Noise<br/><i>smooth, organic</i>"]
|
|
298
|
+
Noise --> Out["offsetX · offsetY · angle"]
|
|
299
|
+
Out --> Canvas["ctx.translate() + ctx.rotate()"]
|
|
300
|
+
|
|
301
|
+
style S1 fill:#ef4444,stroke:#991b1b,color:#fff
|
|
302
|
+
style S2 fill:#a78bfa,stroke:#5b21b6,color:#000
|
|
303
|
+
style S3 fill:#f97316,stroke:#9a3412,color:#000
|
|
304
|
+
style S4 fill:#374151,stroke:#4b5563,color:#9ca3af
|
|
305
|
+
style Noise fill:#fbbf24,stroke:#92400e,color:#000
|
|
306
|
+
style Sum fill:#1e1e2e,stroke:#6b7280,color:#d1d5db
|
|
307
|
+
style Out fill:#1e1e2e,stroke:#6b7280,color:#d1d5db
|
|
308
|
+
style Canvas fill:#1e1e2e,stroke:#6b7280,color:#d1d5db
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Multiple shakes run simultaneously and sum together. Each slot has its own trauma, frequency, decay rate, and direction.
|
|
312
|
+
|
|
313
|
+
```js
|
|
314
|
+
// Backward-compatible simple trauma
|
|
315
|
+
cam.addTrauma(0.5);
|
|
316
|
+
|
|
317
|
+
// Named presets
|
|
318
|
+
cam.shakePreset('explosion'); // big boom
|
|
319
|
+
cam.shakePreset('earthquake'); // sustained rumble
|
|
320
|
+
cam.shakePreset('recoil'); // directional upward kick
|
|
321
|
+
cam.shakePreset('impact'); // sharp snappy jolt
|
|
322
|
+
cam.shakePreset('landing'); // vertical downward push
|
|
323
|
+
cam.shakePreset('damage'); // quick pulse, no rotation
|
|
324
|
+
cam.shakePreset('rumble'); // continuous low vibration
|
|
325
|
+
cam.shakePreset('heavy_impact'); // maximum everything
|
|
326
|
+
|
|
327
|
+
// Custom profile
|
|
328
|
+
cam.shake({
|
|
329
|
+
trauma: 0.6,
|
|
330
|
+
freq: 18, // noise frequency (higher = jittery)
|
|
331
|
+
decay: 1.5, // trauma units lost per second
|
|
332
|
+
maxOffset: 20, // max pixel displacement
|
|
333
|
+
maxAngle: 0.03, // max rotation (radians)
|
|
334
|
+
dirX: 1, // directional X (0 = omnidirectional)
|
|
335
|
+
dirY: 0, // directional Y
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Layer multiple shakes for complex events
|
|
339
|
+
cam.shakePreset('explosion');
|
|
340
|
+
cam.shakePreset('recoil', 0.7); // half intensity
|
|
341
|
+
cam.shakePreset('rumble');
|
|
342
|
+
|
|
343
|
+
// Register custom presets
|
|
344
|
+
import { registerPreset } from '@zakkster/lite-camera-pro';
|
|
345
|
+
|
|
346
|
+
registerPreset('sword_clash', {
|
|
347
|
+
trauma: 0.3, freq: 28, decay: 3.0,
|
|
348
|
+
maxOffset: 8, maxAngle: 0.03,
|
|
349
|
+
dirX: 1, dirY: 0,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
cam.shakePreset('sword_clash');
|
|
353
|
+
|
|
354
|
+
// Stop all shakes immediately
|
|
355
|
+
cam.clearShakes();
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
### Cinematic Sequences
|
|
361
|
+
|
|
362
|
+
```mermaid
|
|
363
|
+
sequenceDiagram
|
|
364
|
+
participant G as Gameplay
|
|
365
|
+
participant S as Sequence
|
|
366
|
+
participant C as Camera
|
|
367
|
+
|
|
368
|
+
G->>S: camera.playSequence(seq)
|
|
369
|
+
Note over G: Follow mode paused
|
|
370
|
+
|
|
371
|
+
S->>C: moveTo(boss.x, boss.y, 1200ms)
|
|
372
|
+
S->>C: zoomTo(1.8, 800ms)
|
|
373
|
+
S->>C: shake('explosion')
|
|
374
|
+
S->>C: wait(600ms)
|
|
375
|
+
S->>C: call(() => boss.startPhase2())
|
|
376
|
+
S->>C: moveAndZoom(player, 1.0, 1000ms)
|
|
377
|
+
|
|
378
|
+
S-->>G: onComplete callback
|
|
379
|
+
Note over G: Follow mode resumes<br/>smooth blend-back
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**The killer feature.** Chain camera moves with a fluent API. The sequence takes full control of position and zoom. When it ends, follow mode resumes with a smooth transition.
|
|
383
|
+
|
|
384
|
+
```js
|
|
385
|
+
const seq = cam.createSequence({ onComplete: () => showUI() })
|
|
386
|
+
.moveTo(boss.x, boss.y, 1200) // pan to boss
|
|
387
|
+
.zoomTo(1.8, 800) // zoom in
|
|
388
|
+
.shake('explosion') // screen shake
|
|
389
|
+
.wait(600) // hold for drama
|
|
390
|
+
.call(() => boss.startPhase2()) // trigger game event
|
|
391
|
+
.moveAndZoom(player.x, player.y, 1.0, 1000); // return to player
|
|
392
|
+
|
|
393
|
+
cam.playSequence(seq);
|
|
394
|
+
|
|
395
|
+
// Playback control
|
|
396
|
+
cam.stopSequence(); // cancel + smooth return to follow
|
|
397
|
+
seq.pause(); // freeze
|
|
398
|
+
seq.resume(); // continue
|
|
399
|
+
seq.seek(2000); // jump to 2s mark
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**Sequence presets** for common patterns:
|
|
403
|
+
```js
|
|
404
|
+
import { panTo, dramaticZoom, bossReveal, timedShake } from '@zakkster/lite-camera-pro';
|
|
405
|
+
|
|
406
|
+
// Simple pan
|
|
407
|
+
cam.playSequence(panTo(cam, 800, 400, 1500));
|
|
408
|
+
|
|
409
|
+
// Boss reveal: zoom in → shake → hold → return
|
|
410
|
+
cam.playSequence(bossReveal(cam, boss.x, boss.y, 3000));
|
|
411
|
+
|
|
412
|
+
// Dramatic zoom with overshoot easing
|
|
413
|
+
cam.playSequence(dramaticZoom(cam, boss.x, boss.y, 2.5, 1200));
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
### Parallax Layers
|
|
419
|
+
|
|
420
|
+
```mermaid
|
|
421
|
+
graph LR
|
|
422
|
+
subgraph "Scroll Speed"
|
|
423
|
+
Sky["☁ Sky<br/>speed: 0.1"]
|
|
424
|
+
Mountains["⛰ Mountains<br/>speed: 0.3"]
|
|
425
|
+
Trees["🌲 Trees<br/>speed: 0.7"]
|
|
426
|
+
Game["🎮 Game Layer<br/>speed: 1.0"]
|
|
427
|
+
Foreground["🌿 Foreground<br/>speed: 1.3"]
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
Sky ~~~ Mountains ~~~ Trees ~~~ Game ~~~ Foreground
|
|
431
|
+
|
|
432
|
+
style Sky fill:#1e3a5f,stroke:#2563eb,color:#93c5fd
|
|
433
|
+
style Mountains fill:#1e3a5f,stroke:#2563eb,color:#93c5fd
|
|
434
|
+
style Trees fill:#064e3b,stroke:#059669,color:#6ee7b7
|
|
435
|
+
style Game fill:#fbbf24,stroke:#92400e,color:#000
|
|
436
|
+
style Foreground fill:#064e3b,stroke:#059669,color:#6ee7b7
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Up to 16 layers. Each scrolls at its own speed relative to the camera.
|
|
440
|
+
|
|
441
|
+
```js
|
|
442
|
+
cam.addParallaxLayer('sky', 0.1); // barely moves
|
|
443
|
+
cam.addParallaxLayer('mountains', 0.3); // slow
|
|
444
|
+
cam.addParallaxLayer('trees', 0.7); // medium
|
|
445
|
+
// game layer is the normal camera (1.0)
|
|
446
|
+
cam.addParallaxLayer('foreground', 1.3); // moves faster than camera
|
|
447
|
+
|
|
448
|
+
// Render each layer with its own transform
|
|
449
|
+
ctx.save();
|
|
450
|
+
cam.applyParallax('sky', ctx);
|
|
451
|
+
drawSky(ctx);
|
|
452
|
+
ctx.restore();
|
|
453
|
+
|
|
454
|
+
ctx.save();
|
|
455
|
+
cam.applyParallax('mountains', ctx);
|
|
456
|
+
drawMountains(ctx);
|
|
457
|
+
ctx.restore();
|
|
458
|
+
|
|
459
|
+
ctx.save();
|
|
460
|
+
cam.apply(ctx); // normal game layer
|
|
461
|
+
drawWorld(ctx);
|
|
462
|
+
ctx.restore();
|
|
463
|
+
|
|
464
|
+
ctx.save();
|
|
465
|
+
cam.applyParallax('foreground', ctx);
|
|
466
|
+
drawForeground(ctx);
|
|
467
|
+
ctx.restore();
|
|
468
|
+
|
|
469
|
+
// Update or remove layers
|
|
470
|
+
cam.addParallaxLayer('sky', 0.15); // update speed by re-adding same id
|
|
471
|
+
cam.removeParallaxLayer('foreground');
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
### Smart Bounds
|
|
477
|
+
|
|
478
|
+
```mermaid
|
|
479
|
+
graph LR
|
|
480
|
+
subgraph "Boundary Behavior"
|
|
481
|
+
H["HARD<br/><i>stops at edge</i>"]
|
|
482
|
+
S["SOFT<br/><i>decelerates smoothly</i>"]
|
|
483
|
+
E["ELASTIC<br/><i>overshoot + spring back</i>"]
|
|
484
|
+
N["NONE<br/><i>no enforcement</i>"]
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
style H fill:#ef4444,stroke:#991b1b,color:#fff
|
|
488
|
+
style S fill:#fbbf24,stroke:#92400e,color:#000
|
|
489
|
+
style E fill:#a78bfa,stroke:#5b21b6,color:#000
|
|
490
|
+
style N fill:#374151,stroke:#4b5563,color:#9ca3af
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Configure boundary behavior per-edge. Mix and match.
|
|
494
|
+
|
|
495
|
+
```js
|
|
496
|
+
import { BoundsType } from '@zakkster/lite-camera-pro';
|
|
497
|
+
|
|
498
|
+
// All edges the same
|
|
499
|
+
cam.setBoundsType(BoundsType.SOFT);
|
|
500
|
+
|
|
501
|
+
// Per-edge configuration
|
|
502
|
+
cam.setBoundsEdges({
|
|
503
|
+
left: BoundsType.HARD,
|
|
504
|
+
right: BoundsType.SOFT,
|
|
505
|
+
top: BoundsType.ELASTIC,
|
|
506
|
+
bottom: BoundsType.HARD,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Tuning
|
|
510
|
+
cam._bounds.softZone = 80; // deceleration zone width (pixels)
|
|
511
|
+
cam._bounds.elasticMax = 30; // max overshoot (pixels)
|
|
512
|
+
cam._bounds.elasticStrength = 8.0; // spring-back speed
|
|
513
|
+
|
|
514
|
+
// Dynamic bounds for rooms / arenas
|
|
515
|
+
cam.setBoundsRect(200, 200, 1200, 800); // constrain to rectangle
|
|
516
|
+
cam.clearBoundsRect(); // revert to full world
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
### Debug HUD
|
|
522
|
+
|
|
523
|
+
The Pro debug overlay shows everything at a glance. Each panel is individually toggleable.
|
|
524
|
+
|
|
525
|
+
```js
|
|
526
|
+
// Toggle panels on/off
|
|
527
|
+
cam.debugConfig.show.shake = false;
|
|
528
|
+
cam.debugConfig.show.parallax = false;
|
|
529
|
+
cam.debugConfig.show.bounds = true;
|
|
530
|
+
|
|
531
|
+
// Render
|
|
532
|
+
ctx.save();
|
|
533
|
+
cam.apply(ctx);
|
|
534
|
+
cam.debug(ctx); // world-space: deadzone rect, lookahead vector, world bounds
|
|
535
|
+
ctx.restore();
|
|
536
|
+
cam.debugHUD(ctx); // screen-space: position, zoom, mode, shake bars, sequence %
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
**Panels:** position · zoom · follow mode · shake slots (per-slot trauma bars) · sequence progress · parallax layers · bounds type
|
|
540
|
+
|
|
541
|
+
The debug HUD uses **zero allocations per frame** — it draws directly to canvas with no intermediate objects.
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
### Save & Load
|
|
546
|
+
|
|
547
|
+
```js
|
|
548
|
+
// Capture snapshot
|
|
549
|
+
const snapshot = cam.getState();
|
|
550
|
+
// → { posX, posY, targetX, targetY, zoom, mode }
|
|
551
|
+
|
|
552
|
+
// Restore
|
|
553
|
+
cam.setState(snapshot);
|
|
554
|
+
// Updates position, zoom, mode, and recalculates visible dimensions
|
|
555
|
+
|
|
556
|
+
// Serialize for save files
|
|
557
|
+
localStorage.setItem('camera', JSON.stringify(cam.getState()));
|
|
558
|
+
cam.setState(JSON.parse(localStorage.getItem('camera')));
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
## Update Loop Integration
|
|
564
|
+
|
|
565
|
+
```mermaid
|
|
566
|
+
flowchart TB
|
|
567
|
+
Start["camera.update(dt, px, py, vx, vy)"] --> SeqCheck{Sequence<br/>active?}
|
|
568
|
+
|
|
569
|
+
SeqCheck -->|Yes| SeqPath["Read sequence state<br/><i>position + zoom from timeline</i>"]
|
|
570
|
+
SeqCheck -->|No| MTCheck{Multi-target<br/>active?}
|
|
571
|
+
|
|
572
|
+
MTCheck -->|Yes| MTPath["Compute bounding box<br/>Auto-zoom + center"]
|
|
573
|
+
MTCheck -->|No| SinglePath["Follow strategy<br/><i>SMOOTH / LOCK / PREDICTIVE / CUT / HYBRID</i>"]
|
|
574
|
+
|
|
575
|
+
SinglePath --> ZoomAnim["Zoom animation<br/><i>lerp + easing</i>"]
|
|
576
|
+
ZoomAnim --> BoundsCalc["Update visible dims"]
|
|
577
|
+
|
|
578
|
+
SeqPath --> Bounds
|
|
579
|
+
MTPath --> Bounds
|
|
580
|
+
BoundsCalc --> Bounds
|
|
581
|
+
|
|
582
|
+
Bounds["Apply bounds<br/><i>HARD / SOFT / ELASTIC / NONE</i>"]
|
|
583
|
+
Bounds --> Lerp["Smooth follow<br/><i>pos += (target - pos) × speed × dt</i>"]
|
|
584
|
+
Lerp --> Parallax["Update parallax layers"]
|
|
585
|
+
Parallax --> Shake["Update shake decay"]
|
|
586
|
+
Shake --> Done["Frame complete"]
|
|
587
|
+
|
|
588
|
+
style Start fill:#fbbf24,stroke:#92400e,color:#000
|
|
589
|
+
style SeqPath fill:#a78bfa,stroke:#5b21b6,color:#000
|
|
590
|
+
style MTPath fill:#34d399,stroke:#065f46,color:#000
|
|
591
|
+
style SinglePath fill:#22d3ee,stroke:#155e75,color:#000
|
|
592
|
+
style Shake fill:#ef4444,stroke:#991b1b,color:#fff
|
|
593
|
+
style Done fill:#1e1e2e,stroke:#6b7280,color:#d1d5db
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
One call to `update()` handles everything. The camera automatically dispatches to the right code path based on active state (sequence > multi-target > follow mode).
|
|
597
|
+
|
|
598
|
+
---
|
|
599
|
+
|
|
600
|
+
## Module Reference
|
|
601
|
+
|
|
602
|
+
| Module | Lines | Purpose |
|
|
603
|
+
|:---|---:|:---|
|
|
604
|
+
| `CinematicCameraPro.js` | 894 | Main class. Zoom, modes, multi-target, shake, sequences, parallax, bounds |
|
|
605
|
+
| `CameraSequence.js` | 513 | Fluent timeline builder + sequence presets |
|
|
606
|
+
| `DebugHUD.js` | 290 | Screen-space + world-space debug overlays |
|
|
607
|
+
| `ShakeEngine.js` | 286 | 8-slot noise-based shake pool |
|
|
608
|
+
| `BoundsSystem.js` | 220 | Per-edge boundary enforcement |
|
|
609
|
+
| `ParallaxManager.js` | 199 | 16-layer scroll manager |
|
|
610
|
+
| `FollowMode.js` | 179 | 5 pure follow strategies |
|
|
611
|
+
| `ShakePresets.js` | 177 | 8 frozen profiles + custom registry |
|
|
612
|
+
| `MultiTarget.js` | 125 | Bounding box framing + auto-zoom |
|
|
613
|
+
| `index.d.ts` | 228 | Full TypeScript declarations |
|
|
614
|
+
| `index.js` | 33 | Public exports (tree-shakeable) |
|
|
615
|
+
| **Total** | **3,144** | |
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
## Zero-GC Design
|
|
620
|
+
|
|
621
|
+
Every hot-path function in lite-camera-pro is allocation-free:
|
|
622
|
+
|
|
623
|
+
- **Coordinate conversion** uses caller-owned `out` objects (never returns `{ x, y }`)
|
|
624
|
+
- **Visible dimensions** are cached as `cam.visibleW` / `cam.visibleH` (no `getVisibleArea()`)
|
|
625
|
+
- **Shake slots** are pre-allocated in a fixed-size pool (8 slots, reused via steal)
|
|
626
|
+
- **Follow modes** are pure functions that mutate `cam.target[]` directly
|
|
627
|
+
- **Debug HUD** draws directly to canvas — no intermediate line objects
|
|
628
|
+
- **Parallax layers** are pre-allocated (16 slots, mutated in place)
|
|
629
|
+
- **Bounds state** is a single pre-allocated config object
|
|
630
|
+
|
|
631
|
+
The only allocations happen during **setup** (constructor, `createSequence()`, `trackMultiple()`) — never inside the 60fps update/render loop.
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
## TypeScript
|
|
636
|
+
|
|
637
|
+
Full declarations ship in `src/index.d.ts`:
|
|
638
|
+
|
|
639
|
+
```ts
|
|
640
|
+
import {
|
|
641
|
+
CinematicCameraPro,
|
|
642
|
+
FollowMode,
|
|
643
|
+
BoundsType,
|
|
644
|
+
WrapMode,
|
|
645
|
+
createCameraSequence,
|
|
646
|
+
EXPLOSION,
|
|
647
|
+
registerPreset,
|
|
648
|
+
} from '@zakkster/lite-camera-pro';
|
|
649
|
+
|
|
650
|
+
const cam = new CinematicCameraPro(800, 600, 3200, 2400);
|
|
651
|
+
cam.setMode(FollowMode.PREDICTIVE);
|
|
652
|
+
cam.setBoundsType(BoundsType.ELASTIC);
|
|
653
|
+
|
|
654
|
+
const seq: CameraSequence = cam.createSequence()
|
|
655
|
+
.moveTo(400, 300, 1200)
|
|
656
|
+
.zoomTo(2.0, 800)
|
|
657
|
+
.shake('explosion');
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
---
|
|
661
|
+
|
|
662
|
+
## Testing
|
|
663
|
+
|
|
664
|
+
```bash
|
|
665
|
+
npm test # vitest run — 105 tests across 2 files
|
|
666
|
+
npm run test:watch # vitest watch mode
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
`CinematicCameraPro.test.js` covers the facade: initialization, coordinate conversion, all 5 follow modes, multi-target framing (including overlapping-target edge cases), zoom animation, shake engine (slot stealing, directional normalization, decay), bounds enforcement, parallax management, sequences, save/load, and destruction. `subsystems.test.js` covers the directly-exported API: the DebugHUD draws (mock-context smoke tests), the functional shake / parallax / bounds helpers, the multi-target updater, the shake-preset registry, and the sequence preset helpers (panTo, dramaticZoom, bossReveal, timedShake).
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
## Migration from lite-camera
|
|
674
|
+
|
|
675
|
+
lite-camera-pro extends `CinematicCamera`. Drop-in replacement:
|
|
676
|
+
|
|
677
|
+
```diff
|
|
678
|
+
- import { CinematicCamera } from '@zakkster/lite-camera';
|
|
679
|
+
+ import { CinematicCameraPro as CinematicCamera } from '@zakkster/lite-camera-pro';
|
|
680
|
+
|
|
681
|
+
const cam = new CinematicCamera(800, 600, 3200, 2400);
|
|
682
|
+
// Everything from lite-camera still works.
|
|
683
|
+
// addTrauma(), update(), apply(), debug() — all backward-compatible.
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
Then add Pro features incrementally. Nothing breaks.
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
## License
|
|
691
|
+
|
|
692
|
+
MIT © Zahary Shinikchiev. See [LICENSE](LICENSE).
|