@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
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zakkster/lite-camera-pro — Parallax Layer Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages multiple scroll layers at different speeds.
|
|
5
|
+
* Each layer has a scroll multiplier relative to the camera.
|
|
6
|
+
*
|
|
7
|
+
* speed 1.0 = scrolls with camera (normal game layer)
|
|
8
|
+
* speed 0.5 = scrolls at half speed (distant background)
|
|
9
|
+
* speed 1.5 = scrolls faster (foreground)
|
|
10
|
+
* speed 0.0 = fixed (UI, sky)
|
|
11
|
+
*
|
|
12
|
+
* Zero allocations per frame. All layer state is pre-allocated.
|
|
13
|
+
*
|
|
14
|
+
* Depends on: nothing (pure math)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ── Maximum layers ──
|
|
18
|
+
const MAX_LAYERS = 16;
|
|
19
|
+
|
|
20
|
+
/** Wrap modes for layer tiling */
|
|
21
|
+
export const WrapMode = {
|
|
22
|
+
NONE: 0, // no wrapping
|
|
23
|
+
REPEAT_X: 1, // tile horizontally
|
|
24
|
+
REPEAT_Y: 2, // tile vertically
|
|
25
|
+
REPEAT_BOTH: 3, // tile both axes
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a single parallax layer (pre-allocated).
|
|
30
|
+
* @returns {Object}
|
|
31
|
+
*/
|
|
32
|
+
function createLayer() {
|
|
33
|
+
return {
|
|
34
|
+
active: false,
|
|
35
|
+
id: '',
|
|
36
|
+
|
|
37
|
+
// Scroll speed multipliers (1.0 = normal camera speed)
|
|
38
|
+
speedX: 1.0,
|
|
39
|
+
speedY: 1.0,
|
|
40
|
+
|
|
41
|
+
// Manual offset (for fine-tuning layer position)
|
|
42
|
+
offsetX: 0,
|
|
43
|
+
offsetY: 0,
|
|
44
|
+
|
|
45
|
+
// Wrap mode
|
|
46
|
+
wrap: WrapMode.NONE,
|
|
47
|
+
|
|
48
|
+
// Computed scroll position (updated each frame)
|
|
49
|
+
scrollX: 0,
|
|
50
|
+
scrollY: 0,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create the parallax manager state. Allocated once per camera.
|
|
56
|
+
*
|
|
57
|
+
* @returns {Object} ParallaxState
|
|
58
|
+
*/
|
|
59
|
+
export function createParallaxState() {
|
|
60
|
+
const layers = new Array(MAX_LAYERS);
|
|
61
|
+
for (let i = 0; i < MAX_LAYERS; i++) {
|
|
62
|
+
layers[i] = createLayer();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
layers,
|
|
67
|
+
layerCount: MAX_LAYERS,
|
|
68
|
+
activeCount: 0,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Add or update a parallax layer.
|
|
74
|
+
*
|
|
75
|
+
* @param {Object} state ParallaxState (cam._parallax)
|
|
76
|
+
* @param {string} id Unique layer identifier
|
|
77
|
+
* @param {number} speedX Horizontal scroll multiplier
|
|
78
|
+
* @param {number} [speedY] Vertical scroll multiplier (defaults to speedX)
|
|
79
|
+
* @param {Object} [opts]
|
|
80
|
+
* @param {number} [opts.offsetX=0] Manual X offset
|
|
81
|
+
* @param {number} [opts.offsetY=0] Manual Y offset
|
|
82
|
+
* @param {number} [opts.wrap=0] WrapMode enum
|
|
83
|
+
* @returns {Object} The layer object (for direct mutation if needed)
|
|
84
|
+
*/
|
|
85
|
+
export function addParallaxLayer(state, id, speedX, speedY, opts) {
|
|
86
|
+
if (speedY === undefined) speedY = speedX;
|
|
87
|
+
|
|
88
|
+
// Check if layer already exists
|
|
89
|
+
for (let i = 0; i < state.layerCount; i++) {
|
|
90
|
+
if (state.layers[i].active && state.layers[i].id === id) {
|
|
91
|
+
const layer = state.layers[i];
|
|
92
|
+
layer.speedX = speedX;
|
|
93
|
+
layer.speedY = speedY;
|
|
94
|
+
if (opts) {
|
|
95
|
+
if (opts.offsetX !== undefined) layer.offsetX = opts.offsetX;
|
|
96
|
+
if (opts.offsetY !== undefined) layer.offsetY = opts.offsetY;
|
|
97
|
+
if (opts.wrap !== undefined) layer.wrap = opts.wrap;
|
|
98
|
+
}
|
|
99
|
+
return layer;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Find first inactive slot
|
|
104
|
+
for (let i = 0; i < state.layerCount; i++) {
|
|
105
|
+
if (!state.layers[i].active) {
|
|
106
|
+
const layer = state.layers[i];
|
|
107
|
+
layer.active = true;
|
|
108
|
+
layer.id = id;
|
|
109
|
+
layer.speedX = speedX;
|
|
110
|
+
layer.speedY = speedY;
|
|
111
|
+
layer.offsetX = (opts && opts.offsetX) || 0;
|
|
112
|
+
layer.offsetY = (opts && opts.offsetY) || 0;
|
|
113
|
+
layer.wrap = (opts && opts.wrap) || WrapMode.NONE;
|
|
114
|
+
layer.scrollX = 0;
|
|
115
|
+
layer.scrollY = 0;
|
|
116
|
+
state.activeCount++;
|
|
117
|
+
return layer;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return null; // all slots full
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Remove a parallax layer by id.
|
|
126
|
+
*
|
|
127
|
+
* @param {Object} state ParallaxState
|
|
128
|
+
* @param {string} id Layer id
|
|
129
|
+
*/
|
|
130
|
+
export function removeParallaxLayer(state, id) {
|
|
131
|
+
for (let i = 0; i < state.layerCount; i++) {
|
|
132
|
+
if (state.layers[i].active && state.layers[i].id === id) {
|
|
133
|
+
state.layers[i].active = false;
|
|
134
|
+
state.layers[i].id = '';
|
|
135
|
+
state.activeCount--;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Update all layer scroll positions based on camera position and zoom.
|
|
143
|
+
* Called once per frame from camera.update().
|
|
144
|
+
*
|
|
145
|
+
* @param {Object} state ParallaxState
|
|
146
|
+
* @param {number} camX Camera top-left X (cam.pos[0])
|
|
147
|
+
* @param {number} camY Camera top-left Y (cam.pos[1])
|
|
148
|
+
* @param {number} zoom Camera zoom level
|
|
149
|
+
*/
|
|
150
|
+
export function updateParallax(state, camX, camY, zoom) {
|
|
151
|
+
for (let i = 0; i < state.layerCount; i++) {
|
|
152
|
+
const layer = state.layers[i];
|
|
153
|
+
if (!layer.active) continue;
|
|
154
|
+
|
|
155
|
+
// Parallax scroll = camera position × speed multiplier
|
|
156
|
+
// Zoom scaling: faster layers should scale more with zoom
|
|
157
|
+
layer.scrollX = camX * layer.speedX * zoom + layer.offsetX;
|
|
158
|
+
layer.scrollY = camY * layer.speedY * zoom + layer.offsetY;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get a layer's scroll offset by id. Zero-alloc via out parameter.
|
|
164
|
+
*
|
|
165
|
+
* @param {Object} state ParallaxState
|
|
166
|
+
* @param {string} id Layer id
|
|
167
|
+
* @param {{x:number,y:number}} out Pre-allocated output
|
|
168
|
+
* @returns {{x:number,y:number}|null} out or null if not found
|
|
169
|
+
*/
|
|
170
|
+
export function getLayerScroll(state, id, out) {
|
|
171
|
+
for (let i = 0; i < state.layerCount; i++) {
|
|
172
|
+
if (state.layers[i].active && state.layers[i].id === id) {
|
|
173
|
+
out.x = state.layers[i].scrollX;
|
|
174
|
+
out.y = state.layers[i].scrollY;
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Apply a parallax layer's transform to a canvas context.
|
|
183
|
+
* Call between ctx.save() and ctx.restore() per layer.
|
|
184
|
+
*
|
|
185
|
+
* @param {Object} state ParallaxState
|
|
186
|
+
* @param {string} id Layer id
|
|
187
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
188
|
+
* @returns {boolean} true if layer was found and applied
|
|
189
|
+
*/
|
|
190
|
+
export function applyParallaxLayer(state, id, ctx) {
|
|
191
|
+
for (let i = 0; i < state.layerCount; i++) {
|
|
192
|
+
const layer = state.layers[i];
|
|
193
|
+
if (layer.active && layer.id === id) {
|
|
194
|
+
ctx.translate(-(layer.scrollX | 0), -(layer.scrollY | 0));
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zakkster/lite-camera-pro — Noise-Based Shake Engine
|
|
3
|
+
*
|
|
4
|
+
* Replaces RNG trauma shake with simplex noise for smooth, organic screen shake.
|
|
5
|
+
* Supports layered shakes: multiple simultaneous shake sources sum together.
|
|
6
|
+
*
|
|
7
|
+
* Zero-GC: Pre-allocated shake slot pool. No allocations in update/compute.
|
|
8
|
+
*
|
|
9
|
+
* Depends on: @zakkster/lite-noise (simplex2)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {simplex2} from '@zakkster/lite-noise';
|
|
13
|
+
|
|
14
|
+
// ── Maximum simultaneous shake layers ──
|
|
15
|
+
const MAX_SHAKE_SLOTS = 8;
|
|
16
|
+
|
|
17
|
+
// ── Unique noise offsets so each slot/axis samples different noise ──
|
|
18
|
+
// Slot i, axis j → noise offset = NOISE_SEED_OFFSET * (i * 3 + j)
|
|
19
|
+
const NOISE_SEED_OFFSET = 1000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A single shake slot. Pre-allocated, reused via pool.
|
|
23
|
+
* All fields are primitives — zero GC.
|
|
24
|
+
*/
|
|
25
|
+
function createShakeSlot() {
|
|
26
|
+
return {
|
|
27
|
+
active: false,
|
|
28
|
+
|
|
29
|
+
// True when slot was created by addTraumaSimple (generic omni shake).
|
|
30
|
+
// addTrauma only stacks onto isDefault slots — preset/profile slots
|
|
31
|
+
// have their own freq/decay/maxOffset and shouldn't be polluted with
|
|
32
|
+
// generic trauma added on top.
|
|
33
|
+
isDefault: false,
|
|
34
|
+
|
|
35
|
+
// ── Trauma model ──
|
|
36
|
+
trauma: 0, // Current trauma [0, 1] — decays over time
|
|
37
|
+
decay: 1.0, // Trauma units lost per second
|
|
38
|
+
|
|
39
|
+
// ── Noise parameters ──
|
|
40
|
+
freq: 15, // Noise sample frequency (higher = more jittery)
|
|
41
|
+
time: 0, // Accumulated time for noise sampling
|
|
42
|
+
|
|
43
|
+
// ── Output amplitude ──
|
|
44
|
+
maxOffset: 15, // Maximum pixel offset at trauma=1
|
|
45
|
+
maxAngle: 0.05, // Maximum rotation (radians) at trauma=1
|
|
46
|
+
|
|
47
|
+
// ── Direction constraint ──
|
|
48
|
+
// If dirX/dirY are non-zero, shake is constrained to that axis.
|
|
49
|
+
// (0,0) = omnidirectional, (1,0) = horizontal only, (0,1) = vertical only
|
|
50
|
+
dirX: 0,
|
|
51
|
+
dirY: 0,
|
|
52
|
+
isDirectional: false,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Shake engine state. Allocated once per camera.
|
|
58
|
+
* Contains a pool of shake slots and the computed output.
|
|
59
|
+
*
|
|
60
|
+
* @param {number} [seedOffset=0] Per-camera offset added to every noise
|
|
61
|
+
* sample base. Lets two cameras with different
|
|
62
|
+
* seeds produce distinct, deterministic shake
|
|
63
|
+
* patterns without touching the global perm table.
|
|
64
|
+
*/
|
|
65
|
+
export function createShakeState(seedOffset = 0) {
|
|
66
|
+
const slots = new Array(MAX_SHAKE_SLOTS);
|
|
67
|
+
for (let i = 0; i < MAX_SHAKE_SLOTS; i++) {
|
|
68
|
+
slots[i] = createShakeSlot();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
slots,
|
|
73
|
+
slotCount: MAX_SHAKE_SLOTS,
|
|
74
|
+
|
|
75
|
+
// Multiply by a prime so adjacent seeds land far apart in noise space.
|
|
76
|
+
seedOffset: (seedOffset | 0) * 7919,
|
|
77
|
+
|
|
78
|
+
// ── Computed output (read by apply()) ──
|
|
79
|
+
offsetX: 0,
|
|
80
|
+
offsetY: 0,
|
|
81
|
+
angle: 0,
|
|
82
|
+
|
|
83
|
+
// ── Global shake scale (0 = no shake, 1 = normal) ──
|
|
84
|
+
globalScale: 1.0,
|
|
85
|
+
|
|
86
|
+
// ── Whether any slot is active (quick check in apply) ──
|
|
87
|
+
active: false,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Find the first inactive slot, or the slot with lowest trauma to steal.
|
|
93
|
+
*
|
|
94
|
+
* @param {Object} state ShakeState
|
|
95
|
+
* @returns {Object} A shake slot
|
|
96
|
+
*/
|
|
97
|
+
function acquireSlot(state) {
|
|
98
|
+
let minTrauma = Infinity;
|
|
99
|
+
let minIdx = 0;
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < state.slotCount; i++) {
|
|
102
|
+
if (!state.slots[i].active) return state.slots[i];
|
|
103
|
+
if (state.slots[i].trauma < minTrauma) {
|
|
104
|
+
minTrauma = state.slots[i].trauma;
|
|
105
|
+
minIdx = i;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// All slots full — steal the weakest
|
|
110
|
+
return state.slots[minIdx];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─────────────────────────────────────────────────────
|
|
114
|
+
// PUBLIC API
|
|
115
|
+
// ─────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Add a shake impulse. Acquires a slot from the pool and configures it.
|
|
119
|
+
*
|
|
120
|
+
* @param {Object} state ShakeState (cam._shake)
|
|
121
|
+
* @param {Object} profile Shake profile (from presets or custom)
|
|
122
|
+
* @param {number} profile.trauma Initial trauma [0, 1]
|
|
123
|
+
* @param {number} [profile.freq=15] Noise frequency
|
|
124
|
+
* @param {number} [profile.decay=1] Trauma decay per second
|
|
125
|
+
* @param {number} [profile.maxOffset=15] Max pixel offset
|
|
126
|
+
* @param {number} [profile.maxAngle=0.05] Max rotation (radians)
|
|
127
|
+
* @param {number} [profile.dirX=0] Directional X component
|
|
128
|
+
* @param {number} [profile.dirY=0] Directional Y component
|
|
129
|
+
* @param {number} [profile.intensity=1] Scale multiplier for the profile
|
|
130
|
+
*/
|
|
131
|
+
export function addShake(state, profile, intensity = 1) {
|
|
132
|
+
const slot = acquireSlot(state);
|
|
133
|
+
|
|
134
|
+
slot.active = true;
|
|
135
|
+
slot.isDefault = false;
|
|
136
|
+
slot.trauma = Math.min(1, (profile.trauma || 0.5) * intensity);
|
|
137
|
+
slot.decay = profile.decay !== undefined ? profile.decay : 1.0;
|
|
138
|
+
slot.freq = profile.freq !== undefined ? profile.freq : 15;
|
|
139
|
+
slot.maxOffset = profile.maxOffset !== undefined ? profile.maxOffset : 15;
|
|
140
|
+
slot.maxAngle = profile.maxAngle !== undefined ? profile.maxAngle : 0.05;
|
|
141
|
+
slot.time = 0; // reset time for fresh noise sampling
|
|
142
|
+
|
|
143
|
+
// Directional
|
|
144
|
+
const dx = profile.dirX || 0;
|
|
145
|
+
const dy = profile.dirY || 0;
|
|
146
|
+
slot.isDirectional = (dx !== 0 || dy !== 0);
|
|
147
|
+
|
|
148
|
+
if (slot.isDirectional) {
|
|
149
|
+
// Normalize direction
|
|
150
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
151
|
+
slot.dirX = dx / len;
|
|
152
|
+
slot.dirY = dy / len;
|
|
153
|
+
} else {
|
|
154
|
+
slot.dirX = 0;
|
|
155
|
+
slot.dirY = 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
state.active = true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Add simple trauma to the first active slot, or create one with defaults.
|
|
163
|
+
* Backward-compatible with the original camera.addTrauma(amount) API. Zero-GC.
|
|
164
|
+
*
|
|
165
|
+
* @param {Object} state ShakeState
|
|
166
|
+
* @param {number} amount Trauma to add [0, 1]
|
|
167
|
+
*/
|
|
168
|
+
export function addTraumaSimple(state, amount) {
|
|
169
|
+
// Try to find an existing default omni slot to stack onto.
|
|
170
|
+
// Preset/profile slots are NEVER stacked onto — they have parameters
|
|
171
|
+
// (freq, decay, etc.) that addTrauma's generic shake wouldn't match.
|
|
172
|
+
for (let i = 0; i < state.slotCount; i++) {
|
|
173
|
+
const s = state.slots[i];
|
|
174
|
+
if (s.active && s.isDefault) {
|
|
175
|
+
s.trauma = Math.min(1, s.trauma + amount);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// No active omni slot — populate one inline. No intermediate object literal
|
|
181
|
+
// (which would otherwise allocate per call and violate the zero-GC contract).
|
|
182
|
+
const slot = acquireSlot(state);
|
|
183
|
+
slot.active = true;
|
|
184
|
+
slot.isDefault = true; // explicit profile/preset — not a generic trauma slot
|
|
185
|
+
slot.trauma = Math.min(1, amount);
|
|
186
|
+
slot.decay = 1.0;
|
|
187
|
+
slot.freq = 15;
|
|
188
|
+
slot.time = 0;
|
|
189
|
+
slot.maxOffset = 15;
|
|
190
|
+
slot.maxAngle = 0.05;
|
|
191
|
+
slot.dirX = 0;
|
|
192
|
+
slot.dirY = 0;
|
|
193
|
+
slot.isDirectional = false;
|
|
194
|
+
state.active = true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Update all active shake slots: advance time, decay trauma.
|
|
199
|
+
* Called once per frame from camera.update().
|
|
200
|
+
*
|
|
201
|
+
* @param {Object} state ShakeState
|
|
202
|
+
* @param {number} dt Delta time in seconds
|
|
203
|
+
*/
|
|
204
|
+
export function updateShake(state, dt) {
|
|
205
|
+
let anyActive = false;
|
|
206
|
+
|
|
207
|
+
for (let i = 0; i < state.slotCount; i++) {
|
|
208
|
+
const s = state.slots[i];
|
|
209
|
+
if (!s.active) continue;
|
|
210
|
+
|
|
211
|
+
s.time += dt;
|
|
212
|
+
s.trauma -= s.decay * dt;
|
|
213
|
+
|
|
214
|
+
if (s.trauma <= 0) {
|
|
215
|
+
s.trauma = 0;
|
|
216
|
+
s.active = false;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
anyActive = true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
state.active = anyActive;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Compute the final shake offset and rotation by summing all active layers.
|
|
228
|
+
* Uses simplex noise for smooth, organic motion. Zero allocation.
|
|
229
|
+
*
|
|
230
|
+
* Called once per frame from camera.apply() AFTER updateShake().
|
|
231
|
+
*
|
|
232
|
+
* @param {Object} state ShakeState
|
|
233
|
+
*/
|
|
234
|
+
export function computeShake(state) {
|
|
235
|
+
let totalOX = 0;
|
|
236
|
+
let totalOY = 0;
|
|
237
|
+
let totalAngle = 0;
|
|
238
|
+
|
|
239
|
+
for (let i = 0; i < state.slotCount; i++) {
|
|
240
|
+
const s = state.slots[i];
|
|
241
|
+
if (!s.active) continue;
|
|
242
|
+
|
|
243
|
+
// trauma² for perceptual scaling (small trauma = barely visible)
|
|
244
|
+
const shake = s.trauma * s.trauma;
|
|
245
|
+
const t = s.time * s.freq;
|
|
246
|
+
|
|
247
|
+
// Sample noise at 3 different offsets for X, Y, angle
|
|
248
|
+
const noiseBase = NOISE_SEED_OFFSET * (i * 3) + state.seedOffset;
|
|
249
|
+
const nx = simplex2(t, noiseBase); // [-1, 1]
|
|
250
|
+
const ny = simplex2(t, noiseBase + 1);
|
|
251
|
+
const na = simplex2(t, noiseBase + 2);
|
|
252
|
+
|
|
253
|
+
if (s.isDirectional) {
|
|
254
|
+
// Project shake onto the direction vector
|
|
255
|
+
const mag = s.maxOffset * shake * nx;
|
|
256
|
+
totalOX += mag * s.dirX;
|
|
257
|
+
totalOY += mag * s.dirY;
|
|
258
|
+
} else {
|
|
259
|
+
totalOX += s.maxOffset * shake * nx;
|
|
260
|
+
totalOY += s.maxOffset * shake * ny;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
totalAngle += s.maxAngle * shake * na;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Apply global scale
|
|
267
|
+
state.offsetX = Math.fround(totalOX * state.globalScale);
|
|
268
|
+
state.offsetY = Math.fround(totalOY * state.globalScale);
|
|
269
|
+
state.angle = Math.fround(totalAngle * state.globalScale);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Stop all active shakes immediately.
|
|
274
|
+
*
|
|
275
|
+
* @param {Object} state ShakeState
|
|
276
|
+
*/
|
|
277
|
+
export function clearShakes(state) {
|
|
278
|
+
for (let i = 0; i < state.slotCount; i++) {
|
|
279
|
+
state.slots[i].active = false;
|
|
280
|
+
state.slots[i].trauma = 0;
|
|
281
|
+
}
|
|
282
|
+
state.active = false;
|
|
283
|
+
state.offsetX = 0;
|
|
284
|
+
state.offsetY = 0;
|
|
285
|
+
state.angle = 0;
|
|
286
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zakkster/lite-camera-pro — Shake Presets
|
|
3
|
+
*
|
|
4
|
+
* Frozen profile objects for common game shake scenarios.
|
|
5
|
+
* Each preset is a plain object matching the ShakeEngine profile shape.
|
|
6
|
+
* Developers can register custom presets via registerPreset().
|
|
7
|
+
*
|
|
8
|
+
* Zero dependencies. Pure data.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ─────────────────────────────────────────────────────
|
|
12
|
+
// BUILT-IN PRESETS
|
|
13
|
+
// ─────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* EXPLOSION — Big boom. High trauma, low frequency for heavy sway,
|
|
17
|
+
* large offset, medium rotation. Slow decay for lingering feel.
|
|
18
|
+
*/
|
|
19
|
+
export const EXPLOSION = Object.freeze({
|
|
20
|
+
trauma: 0.8,
|
|
21
|
+
freq: 12,
|
|
22
|
+
decay: 0.7,
|
|
23
|
+
maxOffset: 25,
|
|
24
|
+
maxAngle: 0.06,
|
|
25
|
+
dirX: 0,
|
|
26
|
+
dirY: 0,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* EARTHQUAKE — Sustained rumble. Medium trauma, very low frequency
|
|
31
|
+
* for slow, heavy rolling. Large offset, minimal rotation. Slow decay.
|
|
32
|
+
*/
|
|
33
|
+
export const EARTHQUAKE = Object.freeze({
|
|
34
|
+
trauma: 0.5,
|
|
35
|
+
freq: 6,
|
|
36
|
+
decay: 0.3,
|
|
37
|
+
maxOffset: 30,
|
|
38
|
+
maxAngle: 0.02,
|
|
39
|
+
dirX: 0,
|
|
40
|
+
dirY: 0,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* RECOIL — Gun/weapon kickback. Directional (upward by default).
|
|
45
|
+
* Short, sharp burst with fast decay.
|
|
46
|
+
*/
|
|
47
|
+
export const RECOIL = Object.freeze({
|
|
48
|
+
trauma: 0.5,
|
|
49
|
+
freq: 20,
|
|
50
|
+
decay: 2.5,
|
|
51
|
+
maxOffset: 12,
|
|
52
|
+
maxAngle: 0.02,
|
|
53
|
+
dirX: 0,
|
|
54
|
+
dirY: -1, // upward kick
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* IMPACT — Something hit the player/world. Sharp trauma spike,
|
|
59
|
+
* high frequency for a snappy jolt, fast decay.
|
|
60
|
+
*/
|
|
61
|
+
export const IMPACT = Object.freeze({
|
|
62
|
+
trauma: 0.7,
|
|
63
|
+
freq: 25,
|
|
64
|
+
decay: 2.0,
|
|
65
|
+
maxOffset: 18,
|
|
66
|
+
maxAngle: 0.04,
|
|
67
|
+
dirX: 0,
|
|
68
|
+
dirY: 0,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* LANDING — Player lands from a height. Vertical-only shake.
|
|
73
|
+
* Medium trauma, medium frequency, moderate decay.
|
|
74
|
+
*/
|
|
75
|
+
export const LANDING = Object.freeze({
|
|
76
|
+
trauma: 0.4,
|
|
77
|
+
freq: 18,
|
|
78
|
+
decay: 1.5,
|
|
79
|
+
maxOffset: 10,
|
|
80
|
+
maxAngle: 0.01,
|
|
81
|
+
dirX: 0,
|
|
82
|
+
dirY: 1, // downward push
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* DAMAGE — Player takes a hit. Quick pulse, low offset,
|
|
87
|
+
* no rotation. Feels like a screen flash without the flash.
|
|
88
|
+
*/
|
|
89
|
+
export const DAMAGE = Object.freeze({
|
|
90
|
+
trauma: 0.35,
|
|
91
|
+
freq: 22,
|
|
92
|
+
decay: 3.0,
|
|
93
|
+
maxOffset: 6,
|
|
94
|
+
maxAngle: 0,
|
|
95
|
+
dirX: 0,
|
|
96
|
+
dirY: 0,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* RUMBLE — Continuous low-level vibration. Low trauma, high frequency
|
|
101
|
+
* for a "motor hum" feel. Very slow decay (lingers).
|
|
102
|
+
* Good for approaching boss, earthquake precursor, engine vibration.
|
|
103
|
+
*/
|
|
104
|
+
export const RUMBLE = Object.freeze({
|
|
105
|
+
trauma: 0.2,
|
|
106
|
+
freq: 30,
|
|
107
|
+
decay: 0.15,
|
|
108
|
+
maxOffset: 3,
|
|
109
|
+
maxAngle: 0,
|
|
110
|
+
dirX: 0,
|
|
111
|
+
dirY: 0,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* HEAVY_IMPACT — Boss stomp, meteor hit, critical attack.
|
|
116
|
+
* Maximum everything. The "oh no" shake.
|
|
117
|
+
*/
|
|
118
|
+
export const HEAVY_IMPACT = Object.freeze({
|
|
119
|
+
trauma: 1.0,
|
|
120
|
+
freq: 10,
|
|
121
|
+
decay: 0.5,
|
|
122
|
+
maxOffset: 35,
|
|
123
|
+
maxAngle: 0.08,
|
|
124
|
+
dirX: 0,
|
|
125
|
+
dirY: 0,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ─────────────────────────────────────────────────────
|
|
129
|
+
// PRESET REGISTRY
|
|
130
|
+
// ─────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
/** @type {Object<string, Object>} */
|
|
133
|
+
const _registry = {
|
|
134
|
+
explosion: EXPLOSION,
|
|
135
|
+
earthquake: EARTHQUAKE,
|
|
136
|
+
recoil: RECOIL,
|
|
137
|
+
impact: IMPACT,
|
|
138
|
+
landing: LANDING,
|
|
139
|
+
damage: DAMAGE,
|
|
140
|
+
rumble: RUMBLE,
|
|
141
|
+
heavy_impact: HEAVY_IMPACT,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get a preset by name.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} name Preset name (case-insensitive)
|
|
148
|
+
* @returns {Object|null} Shake profile or null
|
|
149
|
+
*/
|
|
150
|
+
export function getPreset(name) {
|
|
151
|
+
return _registry[name.toLowerCase()] || null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Register a custom preset. Overwrites existing presets with the same name.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} name Preset name
|
|
158
|
+
* @param {Object} profile Shake profile object
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* registerPreset('sword_clash', {
|
|
162
|
+
* trauma: 0.3, freq: 28, decay: 3.0,
|
|
163
|
+
* maxOffset: 8, maxAngle: 0.03,
|
|
164
|
+
* dirX: 1, dirY: 0, // horizontal only
|
|
165
|
+
* });
|
|
166
|
+
*/
|
|
167
|
+
export function registerPreset(name, profile) {
|
|
168
|
+
_registry[name.toLowerCase()] = Object.freeze({ ...profile });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* List all registered preset names.
|
|
173
|
+
* @returns {string[]}
|
|
174
|
+
*/
|
|
175
|
+
export function listPresets() {
|
|
176
|
+
return Object.keys(_registry);
|
|
177
|
+
}
|