@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/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
|
+
}
|