@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,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zakkster/lite-camera-pro — Camera Sequence System
|
|
3
|
+
*
|
|
4
|
+
* Fluent builder for scripted camera cinematics.
|
|
5
|
+
* Powered by @zakkster/lite-timeline under the hood.
|
|
6
|
+
*
|
|
7
|
+
* Each sequence step becomes a timeline track that writes directly
|
|
8
|
+
* to the camera's position/zoom state. While a sequence is playing,
|
|
9
|
+
* normal follow mode is paused.
|
|
10
|
+
*
|
|
11
|
+
* Zero external deps beyond the lite-* ecosystem.
|
|
12
|
+
*
|
|
13
|
+
* Depends on: @zakkster/lite-timeline, @zakkster/lite-lerp, @zakkster/lite-ease
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {createTimeline} from '@zakkster/lite-timeline';
|
|
17
|
+
import {lerp, clamp} from '@zakkster/lite-lerp';
|
|
18
|
+
import {easeInOutCubic, easeOutExpo} from '@zakkster/lite-ease';
|
|
19
|
+
import {addShake} from './ShakeEngine.js';
|
|
20
|
+
import {getPreset} from './ShakePresets.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a new camera sequence.
|
|
24
|
+
*
|
|
25
|
+
* The sequence does NOT play automatically — call .play() on the camera
|
|
26
|
+
* or let playSequence() handle it.
|
|
27
|
+
*
|
|
28
|
+
* @param {CinematicCameraPro} cam The camera to control
|
|
29
|
+
* @param {Object} [options]
|
|
30
|
+
* @param {boolean} [options.loop=false] Loop the sequence
|
|
31
|
+
* @param {Function} [options.onComplete] Called when sequence finishes
|
|
32
|
+
* @param {number} [options.blendOutTime=0.3] Seconds to blend back to follow after sequence ends
|
|
33
|
+
* @returns {CameraSequence}
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* const seq = createCameraSequence(camera)
|
|
37
|
+
* .moveTo(400, 200, 1200)
|
|
38
|
+
* .zoomTo(1.5, 800)
|
|
39
|
+
* .shake('explosion')
|
|
40
|
+
* .wait(500)
|
|
41
|
+
* .moveTo(800, 300, 1000);
|
|
42
|
+
*
|
|
43
|
+
* camera.playSequence(seq);
|
|
44
|
+
*/
|
|
45
|
+
export function createCameraSequence(cam, options = {}) {
|
|
46
|
+
|
|
47
|
+
const {
|
|
48
|
+
loop = false,
|
|
49
|
+
onComplete = null,
|
|
50
|
+
blendOutTime = 0.3,
|
|
51
|
+
} = options;
|
|
52
|
+
|
|
53
|
+
// ── Snapshot: captured when play() is called ──
|
|
54
|
+
let snapX = 0;
|
|
55
|
+
let snapY = 0;
|
|
56
|
+
let snapZoom = 1;
|
|
57
|
+
|
|
58
|
+
// ── Step queue (built during chaining, consumed when play() builds timeline) ──
|
|
59
|
+
const steps = [];
|
|
60
|
+
|
|
61
|
+
// ── The underlying timeline (created lazily on play) ──
|
|
62
|
+
let timeline = null;
|
|
63
|
+
let isPlaying = false;
|
|
64
|
+
let isDestroyed = false;
|
|
65
|
+
|
|
66
|
+
// ── Internal: the "current" animated state that steps write to ──
|
|
67
|
+
// These are read by updateSequence() to drive the camera.
|
|
68
|
+
const state = {
|
|
69
|
+
x: 0,
|
|
70
|
+
y: 0,
|
|
71
|
+
zoom: 1,
|
|
72
|
+
active: false,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// ─────────────────────────────────────────────────────
|
|
76
|
+
// STEP TYPES
|
|
77
|
+
// ─────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Internal: Add a step to the queue.
|
|
81
|
+
* @param {string} type
|
|
82
|
+
* @param {Object} params
|
|
83
|
+
* @param {string|number} [position] lite-timeline position syntax
|
|
84
|
+
*/
|
|
85
|
+
function pushStep(type, params, position) {
|
|
86
|
+
steps.push({type, params, position});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Snapshot current camera state and build a fresh timeline.
|
|
91
|
+
* Always destroys any prior timeline so play()/seek() are safe to call
|
|
92
|
+
* repeatedly — each (re)build re-snaps the current camera state.
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
function buildTimeline() {
|
|
96
|
+
if (timeline) timeline.destroy();
|
|
97
|
+
|
|
98
|
+
// Snapshot current camera state as the starting point
|
|
99
|
+
snapX = cam.pos[0] + cam.visibleW * 0.5; // center X in world
|
|
100
|
+
snapY = cam.pos[1] + cam.visibleH * 0.5; // center Y in world
|
|
101
|
+
snapZoom = cam.zoom;
|
|
102
|
+
|
|
103
|
+
state.x = snapX;
|
|
104
|
+
state.y = snapY;
|
|
105
|
+
state.zoom = snapZoom;
|
|
106
|
+
|
|
107
|
+
timeline = createTimeline({
|
|
108
|
+
loop,
|
|
109
|
+
onComplete: () => {
|
|
110
|
+
isPlaying = false;
|
|
111
|
+
state.active = false;
|
|
112
|
+
if (onComplete) onComplete();
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Track the "from" values as we chain — each step's start
|
|
117
|
+
// is the previous step's end target.
|
|
118
|
+
let curX = snapX;
|
|
119
|
+
let curY = snapY;
|
|
120
|
+
let curZoom = snapZoom;
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < steps.length; i++) {
|
|
123
|
+
const step = steps[i];
|
|
124
|
+
const pos = step.position; // lite-timeline position string
|
|
125
|
+
|
|
126
|
+
switch (step.type) {
|
|
127
|
+
|
|
128
|
+
case 'moveTo': {
|
|
129
|
+
const {x, y, duration, ease} = step.params;
|
|
130
|
+
const fromX = curX, fromY = curY;
|
|
131
|
+
curX = x;
|
|
132
|
+
curY = y;
|
|
133
|
+
|
|
134
|
+
timeline.add({
|
|
135
|
+
duration,
|
|
136
|
+
ease: ease || easeInOutCubic,
|
|
137
|
+
onUpdate: (t) => {
|
|
138
|
+
state.x = lerp(fromX, x, t);
|
|
139
|
+
state.y = lerp(fromY, y, t);
|
|
140
|
+
},
|
|
141
|
+
}, pos);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
case 'zoomTo': {
|
|
146
|
+
const {level, duration, ease} = step.params;
|
|
147
|
+
const fromZ = curZoom;
|
|
148
|
+
curZoom = level;
|
|
149
|
+
|
|
150
|
+
timeline.add({
|
|
151
|
+
duration,
|
|
152
|
+
ease: ease || easeOutExpo,
|
|
153
|
+
onUpdate: (t) => {
|
|
154
|
+
state.zoom = lerp(fromZ, level, t);
|
|
155
|
+
},
|
|
156
|
+
}, pos);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
case 'shake': {
|
|
161
|
+
const {profile, intensity} = step.params;
|
|
162
|
+
timeline.add({
|
|
163
|
+
duration: 0, // instant — fires once
|
|
164
|
+
onComplete: () => {
|
|
165
|
+
addShake(cam._shake, profile, intensity);
|
|
166
|
+
},
|
|
167
|
+
}, pos);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case 'wait': {
|
|
172
|
+
const {duration} = step.params;
|
|
173
|
+
timeline.add({duration}, pos);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case 'callback': {
|
|
178
|
+
const {fn} = step.params;
|
|
179
|
+
timeline.add({
|
|
180
|
+
duration: 0,
|
|
181
|
+
onComplete: fn,
|
|
182
|
+
}, pos);
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
case 'moveAndZoom': {
|
|
187
|
+
const {x, y, level, duration, ease} = step.params;
|
|
188
|
+
const fromX = curX, fromY = curY, fromZ = curZoom;
|
|
189
|
+
curX = x;
|
|
190
|
+
curY = y;
|
|
191
|
+
curZoom = level;
|
|
192
|
+
|
|
193
|
+
timeline.add({
|
|
194
|
+
duration,
|
|
195
|
+
ease: ease || easeInOutCubic,
|
|
196
|
+
onUpdate: (t) => {
|
|
197
|
+
state.x = lerp(fromX, x, t);
|
|
198
|
+
state.y = lerp(fromY, y, t);
|
|
199
|
+
state.zoom = lerp(fromZ, level, t);
|
|
200
|
+
},
|
|
201
|
+
}, pos);
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ─────────────────────────────────────────────────────
|
|
209
|
+
// PUBLIC: Fluent builder API
|
|
210
|
+
// ─────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
const seq = {
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Move camera center to world coordinates.
|
|
216
|
+
*
|
|
217
|
+
* @param {number} x World X
|
|
218
|
+
* @param {number} y World Y
|
|
219
|
+
* @param {number} duration Duration in ms
|
|
220
|
+
* @param {Object} [opts]
|
|
221
|
+
* @param {Function} [opts.ease] Easing function
|
|
222
|
+
* @param {string|number} [opts.at] Timeline position
|
|
223
|
+
* @returns {CameraSequence} this
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* seq.moveTo(400, 200, 1200)
|
|
227
|
+
* .moveTo(800, 300, 1000);
|
|
228
|
+
*/
|
|
229
|
+
moveTo(x, y, duration, opts) {
|
|
230
|
+
const ease = opts && opts.ease || null;
|
|
231
|
+
const at = opts && opts.at || undefined;
|
|
232
|
+
pushStep('moveTo', {x, y, duration, ease}, at);
|
|
233
|
+
return seq;
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Smoothly zoom to a level.
|
|
238
|
+
*
|
|
239
|
+
* @param {number} level Target zoom
|
|
240
|
+
* @param {number} duration Duration in ms
|
|
241
|
+
* @param {Object} [opts]
|
|
242
|
+
* @param {Function} [opts.ease]
|
|
243
|
+
* @param {string|number} [opts.at] Timeline position
|
|
244
|
+
* @returns {CameraSequence} this
|
|
245
|
+
*/
|
|
246
|
+
zoomTo(level, duration, opts) {
|
|
247
|
+
const ease = opts && opts.ease || null;
|
|
248
|
+
const at = opts && opts.at || undefined;
|
|
249
|
+
pushStep('zoomTo', {level, duration, ease}, at);
|
|
250
|
+
return seq;
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Simultaneously move and zoom. Single track, perfectly synchronized.
|
|
255
|
+
*
|
|
256
|
+
* @param {number} x World X
|
|
257
|
+
* @param {number} y World Y
|
|
258
|
+
* @param {number} level Target zoom
|
|
259
|
+
* @param {number} duration Duration in ms
|
|
260
|
+
* @param {Object} [opts]
|
|
261
|
+
* @returns {CameraSequence} this
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* seq.moveAndZoom(boss.x, boss.y, 1.8, 1500, { ease: easeOutExpo });
|
|
265
|
+
*/
|
|
266
|
+
moveAndZoom(x, y, level, duration, opts) {
|
|
267
|
+
const ease = opts && opts.ease || null;
|
|
268
|
+
const at = opts && opts.at || undefined;
|
|
269
|
+
pushStep('moveAndZoom', {x, y, level, duration, ease}, at);
|
|
270
|
+
return seq;
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Trigger a shake. Fires instantly at current timeline position.
|
|
275
|
+
*
|
|
276
|
+
* @param {string|Object} profileOrName Preset name or profile object
|
|
277
|
+
* @param {number} [intensity=1]
|
|
278
|
+
* @param {Object} [opts]
|
|
279
|
+
* @param {string|number} [opts.at] Timeline position
|
|
280
|
+
* @returns {CameraSequence} this
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* seq.shake('explosion')
|
|
284
|
+
* .shake({ trauma: 0.3, freq: 20, decay: 2, maxOffset: 10 });
|
|
285
|
+
*/
|
|
286
|
+
shake(profileOrName, intensity, opts) {
|
|
287
|
+
let profile;
|
|
288
|
+
if (typeof profileOrName === 'string') {
|
|
289
|
+
profile = getPreset(profileOrName);
|
|
290
|
+
if (!profile) profile = {trauma: 0.5, freq: 15, decay: 1, maxOffset: 15, maxAngle: 0.05};
|
|
291
|
+
} else {
|
|
292
|
+
profile = profileOrName;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const int = (typeof intensity === 'number') ? intensity : 1;
|
|
296
|
+
|
|
297
|
+
// Resolve `at`: prefer opts.at, then fall back to intensity.at if the
|
|
298
|
+
// caller used the 2-arg form `shake(name, { at: ... })`. Guard
|
|
299
|
+
// against `typeof null === 'object'` and treat `at: 0` as valid.
|
|
300
|
+
let at;
|
|
301
|
+
if (opts && opts.at !== undefined) {
|
|
302
|
+
at = opts.at;
|
|
303
|
+
} else if (intensity !== null && typeof intensity === 'object' && intensity.at !== undefined) {
|
|
304
|
+
at = intensity.at;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
pushStep('shake', {profile, intensity: int}, at);
|
|
308
|
+
return seq;
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Pause the sequence for a duration.
|
|
313
|
+
*
|
|
314
|
+
* @param {number} duration Wait time in ms
|
|
315
|
+
* @param {Object} [opts]
|
|
316
|
+
* @param {string|number} [opts.at] Timeline position
|
|
317
|
+
* @returns {CameraSequence} this
|
|
318
|
+
*/
|
|
319
|
+
wait(duration, opts) {
|
|
320
|
+
const at = opts && opts.at || undefined;
|
|
321
|
+
pushStep('wait', {duration}, at);
|
|
322
|
+
return seq;
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Execute an arbitrary callback at this point in the sequence.
|
|
327
|
+
* Great for triggering game events, spawning particles, etc.
|
|
328
|
+
*
|
|
329
|
+
* @param {Function} fn Callback function
|
|
330
|
+
* @param {Object} [opts]
|
|
331
|
+
* @param {string|number} [opts.at] Timeline position
|
|
332
|
+
* @returns {CameraSequence} this
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* seq.moveTo(boss.x, boss.y, 1000)
|
|
336
|
+
* .call(() => boss.startPhase2())
|
|
337
|
+
* .shake('heavy_impact');
|
|
338
|
+
*/
|
|
339
|
+
call(fn, opts) {
|
|
340
|
+
const at = opts && opts.at || undefined;
|
|
341
|
+
pushStep('callback', {fn}, at);
|
|
342
|
+
return seq;
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
// ─────────────────────────────────────────────────
|
|
346
|
+
// PLAYBACK CONTROL
|
|
347
|
+
// ─────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
/** Build and start the sequence. Usually called via camera.playSequence(). */
|
|
350
|
+
play() {
|
|
351
|
+
if (isDestroyed) return seq;
|
|
352
|
+
// Always rebuild — each play() captures the current camera state.
|
|
353
|
+
buildTimeline();
|
|
354
|
+
state.active = true;
|
|
355
|
+
isPlaying = true;
|
|
356
|
+
timeline.play();
|
|
357
|
+
return seq;
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
/** Pause the sequence. */
|
|
361
|
+
pause() {
|
|
362
|
+
if (timeline) timeline.pause();
|
|
363
|
+
return seq;
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
/** Resume after pause. */
|
|
367
|
+
resume() {
|
|
368
|
+
if (timeline) timeline.play();
|
|
369
|
+
return seq;
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
/** Stop the sequence and return camera to follow mode. */
|
|
373
|
+
stop() {
|
|
374
|
+
if (timeline) timeline.reset();
|
|
375
|
+
isPlaying = false;
|
|
376
|
+
state.active = false;
|
|
377
|
+
return seq;
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
/** Jump to a specific time (ms). */
|
|
381
|
+
seek(timeMs) {
|
|
382
|
+
if (!timeline) buildTimeline();
|
|
383
|
+
if (timeline) timeline.seek(timeMs);
|
|
384
|
+
return seq;
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Total sequence duration in ms. Computed from queued steps —
|
|
389
|
+
* does NOT build the timeline or take a camera snapshot.
|
|
390
|
+
* Caveat: `at`-positioned overlaps are not accounted for.
|
|
391
|
+
*/
|
|
392
|
+
get duration() {
|
|
393
|
+
if (timeline) return timeline.duration;
|
|
394
|
+
let total = 0;
|
|
395
|
+
|
|
396
|
+
for (let i = 0; i < steps.length; i++) {
|
|
397
|
+
total += (steps[i].params && steps[i].params.duration) || 0;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return total;
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
/** Current progress (0–1). */
|
|
404
|
+
get progress() {
|
|
405
|
+
return timeline ? timeline.progress : 0;
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
/** Whether the sequence is currently playing. */
|
|
409
|
+
get playing() {
|
|
410
|
+
return isPlaying;
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
/** Destroy the sequence and release timeline resources. */
|
|
414
|
+
destroy() {
|
|
415
|
+
if (isDestroyed) return;
|
|
416
|
+
isDestroyed = true;
|
|
417
|
+
if (timeline) timeline.destroy();
|
|
418
|
+
timeline = null;
|
|
419
|
+
state.active = false;
|
|
420
|
+
isPlaying = false;
|
|
421
|
+
steps.length = 0;
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
// ─────────────────────────────────────────────────
|
|
425
|
+
// INTERNAL: accessed by camera.update()
|
|
426
|
+
// ─────────────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
/** @internal */
|
|
429
|
+
_state: state,
|
|
430
|
+
|
|
431
|
+
/** @internal */
|
|
432
|
+
_blendOutTime: blendOutTime,
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
return seq;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ─────────────────────────────────────────────────────
|
|
439
|
+
// SEQUENCE PRESETS (Day 9)
|
|
440
|
+
// ─────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Simple pan from current position to a world point.
|
|
444
|
+
*
|
|
445
|
+
* @param {CinematicCameraPro} cam
|
|
446
|
+
* @param {number} x Target world X
|
|
447
|
+
* @param {number} y Target world Y
|
|
448
|
+
* @param {number} duration Duration in ms
|
|
449
|
+
* @param {Object} [opts]
|
|
450
|
+
* @returns {CameraSequence}
|
|
451
|
+
*/
|
|
452
|
+
export function panTo(cam, x, y, duration, opts) {
|
|
453
|
+
return createCameraSequence(cam, opts).moveTo(x, y, duration, opts);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Dramatic zoom: move + zoom simultaneously, great for boss reveals.
|
|
458
|
+
*
|
|
459
|
+
* @param {CinematicCameraPro} cam
|
|
460
|
+
* @param {number} x World X to focus on
|
|
461
|
+
* @param {number} y World Y to focus on
|
|
462
|
+
* @param {number} zoom Target zoom level
|
|
463
|
+
* @param {number} duration Duration in ms
|
|
464
|
+
* @param {Object} [opts]
|
|
465
|
+
* @returns {CameraSequence}
|
|
466
|
+
*/
|
|
467
|
+
export function dramaticZoom(cam, x, y, zoom, duration, opts) {
|
|
468
|
+
return createCameraSequence(cam, opts).moveAndZoom(x, y, zoom, duration, opts);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Boss reveal: pan to target, zoom in, shake, hold, then return.
|
|
473
|
+
*
|
|
474
|
+
* @param {CinematicCameraPro} cam
|
|
475
|
+
* @param {number} x Boss world X
|
|
476
|
+
* @param {number} y Boss world Y
|
|
477
|
+
* @param {number} [totalMs=3000] Total sequence duration
|
|
478
|
+
* @param {Object} [opts]
|
|
479
|
+
* @returns {CameraSequence}
|
|
480
|
+
*/
|
|
481
|
+
export function bossReveal(cam, x, y, totalMs = 3000, opts) {
|
|
482
|
+
const panTime = totalMs * 0.35;
|
|
483
|
+
const holdTime = totalMs * 0.30;
|
|
484
|
+
const backTime = totalMs * 0.35;
|
|
485
|
+
|
|
486
|
+
// Capture current center for return
|
|
487
|
+
const startX = cam.pos[0] + cam.visibleW * 0.5;
|
|
488
|
+
const startY = cam.pos[1] + cam.visibleH * 0.5;
|
|
489
|
+
|
|
490
|
+
return createCameraSequence(cam, opts)
|
|
491
|
+
.moveAndZoom(x, y, 1.8, panTime)
|
|
492
|
+
.shake('impact')
|
|
493
|
+
.wait(holdTime)
|
|
494
|
+
.moveAndZoom(startX, startY, 1.0, backTime);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Screen shake sequence with timed duration. Shake fires and
|
|
499
|
+
* the sequence waits for it to naturally decay.
|
|
500
|
+
*
|
|
501
|
+
* @param {CinematicCameraPro} cam
|
|
502
|
+
* @param {string|Object} presetOrProfile
|
|
503
|
+
* @param {number} [holdMs=500] How long to wait after shake fires
|
|
504
|
+
* @param {Object} [opts]
|
|
505
|
+
* @returns {CameraSequence}
|
|
506
|
+
*/
|
|
507
|
+
export function timedShake(cam, presetOrProfile, holdMs = 500, opts) {
|
|
508
|
+
return createCameraSequence(cam, opts)
|
|
509
|
+
.shake(presetOrProfile)
|
|
510
|
+
.wait(holdMs);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export default createCameraSequence;
|