littlejsengine 1.18.29 → 1.20.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.
Files changed (51) hide show
  1. package/FAQ.md +841 -815
  2. package/README.md +211 -199
  3. package/dist/littlejs.d.ts +4383 -812
  4. package/dist/littlejs.esm.js +31848 -16680
  5. package/dist/littlejs.esm.min.js +14 -1
  6. package/dist/littlejs.js +31230 -16166
  7. package/dist/littlejs.min.js +13 -1
  8. package/dist/littlejs.release.js +27419 -15457
  9. package/package.json +10 -5
  10. package/plugins/audioEffects.js +375 -0
  11. package/plugins/box2d.js +2516 -2073
  12. package/plugins/drawUtilities.js +206 -31
  13. package/plugins/gltf.js +740 -0
  14. package/plugins/jsconfig.json +19 -0
  15. package/plugins/lightSystem.js +719 -330
  16. package/plugins/math3d.js +911 -0
  17. package/plugins/medalSystem.js +118 -72
  18. package/plugins/newgrounds.js +400 -199
  19. package/plugins/pathFinder.js +208 -175
  20. package/plugins/pluginExport.js +196 -115
  21. package/plugins/postProcess.js +284 -166
  22. package/plugins/render3d.js +4072 -0
  23. package/plugins/render3dDebug.js +109 -0
  24. package/plugins/render3dExtras.js +1754 -0
  25. package/plugins/render3dVoxels.js +502 -0
  26. package/plugins/textureSheet.js +86 -48
  27. package/plugins/threejs.js +183 -158
  28. package/plugins/tweakables.js +411 -0
  29. package/plugins/tweenSystem.js +646 -526
  30. package/plugins/uiSystem.js +2024 -1687
  31. package/plugins/zzfxm.js +8 -15
  32. package/src/engine.js +345 -194
  33. package/src/engineAudio.js +1022 -777
  34. package/src/engineBuild.mjs +289 -253
  35. package/src/engineDebug.js +912 -735
  36. package/src/engineDraw.js +1774 -1510
  37. package/src/engineEditor.js +2431 -0
  38. package/src/engineExport.js +419 -396
  39. package/src/engineInput.js +1462 -1293
  40. package/src/engineLogo.js +4 -3
  41. package/src/engineMath.js +1274 -1183
  42. package/src/engineObject.js +674 -551
  43. package/src/engineParticles.js +583 -532
  44. package/src/engineRelease.js +67 -45
  45. package/src/engineSettings.js +808 -726
  46. package/src/engineTileLayer.js +995 -741
  47. package/src/engineUtilities.js +32 -13
  48. package/src/engineWebGL.js +1162 -941
  49. package/src/jsconfig.json +17 -13
  50. package/plugins/box2d.wasm.js +0 -630
  51. package/plugins/box2d.wasm.wasm +0 -0
@@ -1,526 +1,646 @@
1
- /**
2
- * LittleJS Tween System Plugin
3
- * - Lightweight tweens for numbers, Vector2, Color, or any .lerp-able type
4
- * - Chainable easing, looping, and ping-pong
5
- * - Property-path helper for the common case of animating an object field
6
- * - Auto-updates via engineAddPlugin; pauses with the game by default
7
- * @namespace TweenSystem
8
- */
9
-
10
- 'use strict';
11
-
12
- ///////////////////////////////////////////////////////////////////////////////
13
-
14
- // Module-private list of tweens currently running.
15
- const tweenActive = [];
16
-
17
- // Time tracking for delta computation between engine plugin calls.
18
- let lastTime = 0;
19
- let lastTimeReal = 0;
20
-
21
- // True if the value is an instance of a class that exposes a numeric-percent
22
- // `lerp(other, percent)` method (Vector2, Color, or any future class).
23
- function isLerpable(v) { return v && typeof v.lerp === 'function'; }
24
-
25
- ///////////////////////////////////////////////////////////////////////////////
26
-
27
- /** A numeric tween: drives a callback with a value interpolated between
28
- * `start` and `end` over `duration` seconds. Pauses with the game by default.
29
- * @memberof TweenSystem
30
- * @example
31
- * // Animate a fade-out over 2 seconds with an ease-out sine curve.
32
- * new Tween((v) => obj.alpha = v, 1, 0, 2, { ease: Ease.OUT(Ease.SINE) });
33
- */
34
- class Tween
35
- {
36
- /** Create a new tween. The callback fires immediately with `start` so the
37
- * target snaps to the start value on the same frame the tween is created.
38
- *
39
- * `start` and `end` may be numbers, Vector2 instances, Color instances, or
40
- * any object exposing a `lerp(other, percent) => sameType` method. The
41
- * callback receives the interpolated value (a number, or a fresh instance
42
- * for lerp-able types). Both endpoints must be the same type.
43
- * @param {function(number|Vector2|Color):void} callback - Called with the interpolated value each frame
44
- * @param {number|Vector2|Color} [start=0] - Starting value
45
- * @param {number|Vector2|Color} [end=1] - Ending value
46
- * @param {number} [duration=1] - Duration in seconds
47
- * @param {Object} [options]
48
- * @param {function(number):number} [options.ease] - Easing function (defaults to LINEAR)
49
- * @param {boolean} [options.useRealTime=false] - Advance even when the game is paused (matches Timer's useRealTime)
50
- * @param {boolean} [options.paused=false] - Start in paused state */
51
- constructor(callback, start = 0, end = 1, duration = 1, options = {})
52
- {
53
- ASSERT(typeof callback === 'function', 'Tween callback must be a function');
54
- if (isLerpable(start))
55
- {
56
- ASSERT(start.constructor === end.constructor,
57
- 'Tween start and end must be the same type');
58
- }
59
- else
60
- {
61
- ASSERT(isNumber(start), 'Tween start must be a number or have a .lerp method');
62
- ASSERT(isNumber(end), 'Tween end must be a number when start is a number');
63
- }
64
- ASSERT(isNumber(duration) && duration > 0, 'Tween duration must be > 0');
65
-
66
- /** @property {function(number|Vector2|Color):void} - Called with the interpolated value each frame */
67
- this.callback = callback;
68
- /** @property {number|Vector2|Color} - Starting value */
69
- this.start = start;
70
- /** @property {number|Vector2|Color} - Ending value */
71
- this.end = end;
72
- /** @property {number} - Total duration in seconds */
73
- this.duration = duration;
74
- /** @property {number} - Remaining time in seconds (counts down from duration to 0) */
75
- this.life = duration;
76
- /** @property {function(number):number} - Easing curve mapping [0,1] -> [0,1] */
77
- this.ease = options.ease || Ease.LINEAR;
78
- /** @property {boolean} - If true, advance even when the game is paused */
79
- this.useRealTime = !!options.useRealTime;
80
- /** @property {boolean} - If true, stop advancing until cleared */
81
- this.paused = !!options.paused;
82
-
83
- /** @private completion callback set by then(), loop(), pingPong(). */
84
- this.thenCallback = undefined;
85
- /** @private remaining iterations including the current run (loop/pingPong only). */
86
- this.loopRemaining = 0;
87
-
88
- tweenActive.push(this);
89
- // Snap target to start immediately.
90
- callback(this.interp(duration));
91
- }
92
-
93
- /** Set the easing curve and return this for chaining.
94
- * @param {function(number):number} easeFn
95
- * @returns {Tween}
96
- * @memberof TweenSystem */
97
- setEase(easeFn)
98
- {
99
- this.ease = easeFn;
100
- return this;
101
- }
102
-
103
- /** Set a single completion callback. Calling `then` again replaces the
104
- * previous callback. Returns this for chaining.
105
- *
106
- * Calling `then` after `loop` or `pingPong` overrides the loop chain
107
- * (last call wins).
108
- * @param {function():void} callback
109
- * @returns {Tween}
110
- * @memberof TweenSystem */
111
- then(callback)
112
- {
113
- this.thenCallback = callback;
114
- this.loopRemaining = 0;
115
- return this;
116
- }
117
-
118
- /** Repeat this tween `n` total times. After each iteration finishes, a
119
- * fresh tween with the same parameters takes over via the `then` slot.
120
- * `loop()` with no argument loops forever.
121
- *
122
- * Mutually exclusive with `pingPong`; calling either replaces the other,
123
- * and calling `then` after either clears the loop (last call wins).
124
- * @param {number} [count=Infinity]
125
- * @returns {Tween}
126
- * @memberof TweenSystem */
127
- loop(count = Infinity)
128
- {
129
- this.loopRemaining = count;
130
- this.thenCallback = () => loopContinuation(this);
131
- return this;
132
- }
133
-
134
- /** Like `loop`, but swap `start` and `end` between iterations so the value
135
- * bounces back and forth. `pingPong()` with no argument bounces forever.
136
- *
137
- * Mutually exclusive with `loop`; calling either replaces the other, and
138
- * calling `then` after either clears the loop (last call wins).
139
- * @param {number} [count=Infinity]
140
- * @returns {Tween}
141
- * @memberof TweenSystem */
142
- pingPong(count = Infinity)
143
- {
144
- this.loopRemaining = count;
145
- this.thenCallback = () => pingPongContinuation(this);
146
- return this;
147
- }
148
-
149
- /** Pause this tween. While paused, tweenUpdate skips it.
150
- * @memberof TweenSystem */
151
- pause() { this.paused = true; }
152
-
153
- /** Resume a paused tween.
154
- * @memberof TweenSystem */
155
- resume() { this.paused = false; }
156
-
157
- /** Reset this tween to the start: life back to duration, pause cleared,
158
- * re-added to the active list if previously stopped, and the callback
159
- * re-fired with the start value.
160
- * @memberof TweenSystem */
161
- restart()
162
- {
163
- this.life = this.duration;
164
- this.paused = false;
165
- if (tweenActive.indexOf(this) < 0) tweenActive.push(this);
166
- this.callback(this.interp(this.duration));
167
- }
168
-
169
- /** True if this tween is in the active list and not paused.
170
- * @returns {boolean}
171
- * @memberof TweenSystem */
172
- isActive()
173
- {
174
- return !this.paused && tweenActive.indexOf(this) >= 0;
175
- }
176
-
177
- /** Get how far this tween has progressed, from 0 (just started) to 1
178
- * (completed). Clamped — overshoot past completion still reads 1.
179
- * @returns {number}
180
- * @memberof TweenSystem */
181
- getPercent()
182
- {
183
- return percent(this.duration - this.life, 0, this.duration);
184
- }
185
-
186
- /** Get the current interpolated value (the value most recently passed to
187
- * the callback). Returns a number, Vector2, or Color depending on the
188
- * tween's start/end types.
189
- * @returns {number|Vector2|Color}
190
- * @memberof TweenSystem */
191
- getValue()
192
- {
193
- return this.interp(this.life);
194
- }
195
-
196
- /** Compute the interpolated value at the given remaining `life`.
197
- * At life === duration the result is `start`; at life === 0 it is `end`.
198
- * @param {number} life
199
- * @returns {number}
200
- * @memberof TweenSystem */
201
- interp(life)
202
- {
203
- const x = this.ease((this.duration - life) / this.duration);
204
- if (isLerpable(this.start))
205
- return this.start.lerp(this.end, x);
206
- return this.start + (this.end - this.start) * x;
207
- }
208
-
209
- /** Remove this tween from the active list and prevent any pending then-callback.
210
- * @memberof TweenSystem */
211
- stop()
212
- {
213
- const i = tweenActive.indexOf(this);
214
- if (i >= 0) tweenActive.splice(i, 1);
215
- this.thenCallback = undefined;
216
- }
217
- }
218
-
219
- /** Library of named easing curves and direction modifiers.
220
- * All curves accept `x` in [0,1] and return [0,1] (with possible overshoot
221
- * for ELASTIC/BACK/SPRING/BOUNCE). Curves are values you pass to `setEase`
222
- * or compose via the IN/OUT/IN_OUT/PIECEWISE/BEZIER modifiers.
223
- * @memberof TweenSystem
224
- * @example
225
- * // Use a basic curve
226
- * new Tween(callback, 0, 10, 1).setEase(Ease.SINE);
227
- * // Use a modifier on a curve
228
- * new Tween(callback, 0, 10, 1).setEase(Ease.OUT(Ease.BACK));
229
- */
230
- const Ease =
231
- {
232
- /** Linear (identity) curve.
233
- * @param {number} x
234
- * @returns {number}
235
- * @memberof TweenSystem */
236
- LINEAR: (x) => x,
237
-
238
- /** Power curve factory: `Ease.POWER(n)` returns `x => x**n`.
239
- * Use n=2 for quadratic, n=3 for cubic, etc.
240
- * @param {number} n
241
- * @returns {function(number):number}
242
- * @memberof TweenSystem */
243
- POWER: (n) => (x) => x ** n,
244
-
245
- /** Sine ease-in curve: starts slow, ends fast.
246
- * @param {number} x
247
- * @returns {number}
248
- * @memberof TweenSystem */
249
- SINE: (x) => 1 - cos(x * (PI / 2)),
250
-
251
- /** Circular ease-in curve.
252
- * @param {number} x
253
- * @returns {number}
254
- * @memberof TweenSystem */
255
- CIRC: (x) => 1 - (1 - x * x)**.5,
256
-
257
- /** Exponential ease-in curve (`2^(10x-10)`).
258
- * @param {number} x
259
- * @returns {number}
260
- * @memberof TweenSystem */
261
- EXPO: (x) => x === 0 ? 0 : 2 ** (10 * x - 10),
262
-
263
- /** Back ease-in: overshoots backward at the start before snapping forward.
264
- * @param {number} x
265
- * @returns {number}
266
- * @memberof TweenSystem */
267
- BACK: (x) => x * x * (2.70158 * x - 1.70158),
268
-
269
- /** Elastic ease-in: oscillates with decreasing amplitude.
270
- * @param {number} x
271
- * @returns {number}
272
- * @memberof TweenSystem */
273
- ELASTIC: (x) =>
274
- x === 0 ? 0 :
275
- x === 1 ? 1 :
276
- -(2 ** (10 * x - 10)) * sin(((37 - 40 * x) * PI) / 6),
277
-
278
- /** Spring-like ease-out: oscillates outward after passing the target.
279
- * @param {number} x
280
- * @returns {number}
281
- * @memberof TweenSystem */
282
- SPRING: (x) =>
283
- 1 -
284
- (sin(PI * (1 - x) * (0.2 + 2.5 * (1 - x) ** 3)) *
285
- x ** 2.2 +
286
- (1 - x)) *
287
- (1.0 + 1.2 * x),
288
-
289
- /** Bouncing ease-in: slow ramp with bouncing impacts near the end.
290
- * Symmetric with the other base curves, which are all ease-in. To get the
291
- * classic "object falls and hits the ground" shape (bounces near x=1),
292
- * wrap with `Ease.OUT`: `Ease.OUT(Ease.BOUNCE)`.
293
- * @param {number} x
294
- * @returns {number}
295
- * @memberof TweenSystem
296
- * @example
297
- * Ease.BOUNCE // ease-in bounce (slow, then bouncy at end)
298
- * Ease.OUT(Ease.BOUNCE) // ease-out bounce (object hits ground)
299
- * Ease.IN_OUT(Ease.BOUNCE) // bounces at both ends
300
- */
301
- BOUNCE: (x) =>
302
- {
303
- // Inverted form of the standard easeOutBounce: 1 - bounceOut(1 - x).
304
- let t = 1 - x, f;
305
- if (t < 4 / 11) f = 7.5625 * t * t;
306
- else if (t < 8 / 11) f = 7.5625 * (t -= 6 / 11) * t + 0.75;
307
- else if (t < 10 / 11) f = 7.5625 * (t -= 9 / 11) * t + 0.9375;
308
- else f = 7.5625 * (t -= 10.5 / 11) * t + 0.984375;
309
- return 1 - f;
310
- },
311
-
312
- /** Ease-in direction modifier: returns the curve unchanged. Symmetric
313
- * with `OUT` and `IN_OUT`. Base curves are already ease-in by
314
- * convention, so wrapping a curve in `IN` is a no-op — useful when
315
- * picking the direction programmatically.
316
- * @param {function(number):number} f - Curve to use as ease-in (returned unchanged)
317
- * @returns {function(number):number}
318
- * @memberof TweenSystem
319
- * @example
320
- * // Pick direction at runtime
321
- * const dir = bouncyMode ? Ease.OUT : Ease.IN;
322
- * new Tween(cb, 0, 10, 1).setEase(dir(Ease.BACK));
323
- */
324
- IN: (f) => f,
325
-
326
- /** Reverse a curve so it eases out instead of in: `x => 1 - f(1 - x)`.
327
- * @param {function(number):number} f
328
- * @returns {function(number):number}
329
- * @memberof TweenSystem
330
- * @example
331
- * Ease.OUT(Ease.POWER(2)) // ease-out quadratic
332
- */
333
- OUT: (f) => (x) => 1 - f(1 - x),
334
-
335
- /** Combine the first half of `f` with `Ease.OUT(f)` for a symmetric curve.
336
- * Bug-fix vs the original library: the original referenced an undefined
337
- * global `Piecewise`; this implementation routes through `Ease.PIECEWISE`.
338
- * @param {function(number):number} f
339
- * @returns {function(number):number}
340
- * @memberof TweenSystem */
341
- IN_OUT: (f) => Ease.PIECEWISE(f, Ease.OUT(f)),
342
-
343
- /** Split [0,1] into N equal sections and run a different curve in each.
344
- * Each curve is mapped to its section: section i runs over [i/n, (i+1)/n]
345
- * and its output is mapped to [i/n, (i+1)/n] of the overall range.
346
- * @param {...function(number):number} fns
347
- * @returns {function(number):number}
348
- * @memberof TweenSystem */
349
- PIECEWISE: (...fns) =>
350
- {
351
- const n = fns.length;
352
- return (x) =>
353
- {
354
- const i = (x * n - 1e-9) >> 0;
355
- return (fns[i]((x - i / n) * n) + i) / n;
356
- };
357
- },
358
-
359
- /** Cubic Bezier curve solver in the style of CSS `cubic-bezier`.
360
- * Control points (0,0), (x1,y1), (x2,y2), (1,1).
361
- * @param {number} x1
362
- * @param {number} y1
363
- * @param {number} x2
364
- * @param {number} y2
365
- * @returns {function(number):number}
366
- * @memberof TweenSystem
367
- * @example
368
- * Ease.BEZIER(0.25, 0.1, 0.25, 1) // CSS "ease"
369
- */
370
- BEZIER: (x1, y1, x2, y2) =>
371
- {
372
- // Parametric cubic Bezier with implicit (0,0) and (1,1) endpoints.
373
- const curve = (t) =>
374
- {
375
- const u = 1 - t;
376
- const c1 = 3 * u * u * t;
377
- const c2 = 3 * u * t * t;
378
- const t3 = t ** 3;
379
- return [c1 * x1 + c2 * x2 + t3, c1 * y1 + c2 * y2 + t3];
380
- };
381
- return (x) =>
382
- {
383
- // Binary search for t such that curve(t).x ≈ x, then return curve(t).y.
384
- let t0 = 0, t1 = 1;
385
- for (let i = 0; i < 128; i++)
386
- {
387
- const tMid = (t0 + t1) / 2;
388
- const [bx, by] = curve(tMid);
389
- if (abs(bx - x) < 1e-5) return by;
390
- if (bx < x) t0 = tMid; else t1 = tMid;
391
- }
392
- return curve((t0 + t1) / 2)[1];
393
- };
394
- },
395
- };
396
-
397
- /** Tween a property on an object by dot-path. Returns the underlying Tween
398
- * so all chaining methods (`setEase`, `then`, `loop`, `pingPong`, etc.)
399
- * remain available.
400
- *
401
- * `start` and `end` may be numbers, Vector2 instances, Color instances, or
402
- * any object with a `lerp(other, percent) => sameType` method.
403
- * @param {Object} target - The object whose property is being animated
404
- * @param {string} propertyPath - Dot-separated path, e.g. `'pos.x'` or `'color'`
405
- * @param {number|Vector2|Color} start - Starting value
406
- * @param {number|Vector2|Color} end - Ending value
407
- * @param {number} [duration=1] - Duration in seconds
408
- * @param {Object} [options] - Same options as the Tween constructor
409
- * @returns {Tween}
410
- * @memberof TweenSystem
411
- * @example
412
- * // Numeric: slide an object's x with an ease-out sine curve
413
- * tweenProperty(player, 'pos.x', 0, 10, 2).setEase(Ease.OUT(Ease.SINE));
414
- * // Vector2: animate a position diagonally
415
- * tweenProperty(player, 'pos', vec2(-5, 0), vec2(5, 3), 2);
416
- * // Color: pulse between two colors
417
- * tweenProperty(sprite, 'color', RED, BLUE, 1).pingPong();
418
- */
419
- function tweenProperty(target, propertyPath, start, end, duration = 1, options = {})
420
- {
421
- ASSERT(target != null && typeof target === 'object', 'tweenProperty target must be an object');
422
- ASSERT(isStringLike(propertyPath) && propertyPath.length > 0, 'tweenProperty propertyPath must be a non-empty string');
423
-
424
- const parts = propertyPath.split('.');
425
- const lastKey = parts.pop();
426
- const callback = (value) =>
427
- {
428
- let obj = target;
429
- for (const k of parts)
430
- {
431
- obj = obj[k];
432
- ASSERT(obj != null, 'tweenProperty path does not resolve: ' + propertyPath);
433
- }
434
- obj[lastKey] = value;
435
- };
436
- return new Tween(callback, start, end, duration, options);
437
- }
438
-
439
- // Continuation that schedules the next loop iteration when one finishes.
440
- // Reuses the same Tween object across iterations so the user's handle
441
- // from `.loop()` keeps working — calling `.stop()` mid-loop now cancels
442
- // the entire chain instead of just the current iteration.
443
- function loopContinuation(tween)
444
- {
445
- if (tween.loopRemaining !== Infinity && tween.loopRemaining <= 1) return;
446
- if (tween.loopRemaining !== Infinity) tween.loopRemaining -= 1;
447
- tween.life = tween.duration;
448
- tween.thenCallback = () => loopContinuation(tween);
449
- tweenActive.push(tween);
450
- // snap to start for the new iteration (matches Tween constructor behavior)
451
- tween.callback(tween.interp(tween.duration));
452
- }
453
-
454
- // Continuation for pingPong: swaps start and end on the same tween each iteration.
455
- function pingPongContinuation(tween)
456
- {
457
- if (tween.loopRemaining !== Infinity && tween.loopRemaining <= 1) return;
458
- if (tween.loopRemaining !== Infinity) tween.loopRemaining -= 1;
459
- const tmp = tween.start;
460
- tween.start = tween.end;
461
- tween.end = tmp;
462
- tween.life = tween.duration;
463
- tween.thenCallback = () => pingPongContinuation(tween);
464
- tweenActive.push(tween);
465
- tween.callback(tween.interp(tween.duration));
466
- }
467
-
468
- /** Engine plugin hook: advance every active tween by the appropriate delta.
469
- * Called once per render frame by the engine (no arguments). May also be
470
- * called explicitly with `(gameDelta, realDelta)` to drive tweens manually
471
- * — useful for headless tests or custom replay/scrubbing systems.
472
- * @param {number} [gameDelta] - Game-time delta in seconds; default: time - lastTime
473
- * @param {number} [realDelta] - Real-time delta in seconds; default: timeReal - lastTimeReal
474
- * @memberof TweenSystem */
475
- function tweenUpdate(gameDelta, realDelta)
476
- {
477
- if (gameDelta === undefined)
478
- {
479
- // Engine path: compute deltas from engine time globals.
480
- gameDelta = time - lastTime;
481
- realDelta = timeReal - lastTimeReal;
482
- lastTime = time;
483
- lastTimeReal = timeReal;
484
- }
485
- else if (realDelta === undefined)
486
- {
487
- // Manual path with one arg: real and game advance together.
488
- realDelta = gameDelta;
489
- }
490
-
491
- // Iterate in reverse so removals don't disturb iteration.
492
- for (let i = tweenActive.length; i--;)
493
- {
494
- const t = tweenActive[i];
495
- if (t.paused) continue;
496
- const dt = t.useRealTime ? realDelta : gameDelta;
497
- if (dt <= 0) continue;
498
-
499
- t.life -= dt;
500
- if (t.life > 0)
501
- {
502
- t.callback(t.interp(t.life));
503
- }
504
- else
505
- {
506
- // Completion: fire end value, remove from active, fire then-callback.
507
- t.callback(t.interp(0));
508
- tweenActive.splice(i, 1);
509
- const cb = t.thenCallback;
510
- t.thenCallback = undefined;
511
- if (cb) cb();
512
- }
513
- }
514
- }
515
-
516
- /** Stop every active tween and clear their then-callbacks. Useful for resets
517
- * on level transitions or when changing scenes.
518
- * @memberof TweenSystem */
519
- function tweenStopAll()
520
- {
521
- for (const t of tweenActive) t.thenCallback = undefined;
522
- tweenActive.length = 0;
523
- }
524
-
525
- // Register with the engine so tweens auto-advance.
526
- engineAddPlugin(tweenUpdate);
1
+ /**
2
+ * LittleJS Tween System Plugin
3
+ * - Lightweight tweens for numbers, Vector2, Color, or any .lerp-able type
4
+ * - Chainable easing, looping, and ping-pong
5
+ * - Property-path helper for the common case of animating an object field
6
+ * - Auto-updates via engineAddPlugin; pauses with the game by default
7
+ * @namespace TweenSystem
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ ///////////////////////////////////////////////////////////////////////////////
13
+
14
+ // Module-private list of tweens currently running, each with its active flag set while it is in it.
15
+ const tweenActive = [];
16
+ const tweenUpdateList = []; // the tweens an update moves, the ones active when it began
17
+ let tweenUpdatePass = 0; // counts the updates, a tween started during one waits for the next
18
+
19
+ // put a tween in the active list, or take it out, keeping its flag in step so a check costs nothing
20
+ function tweenActivate(tween)
21
+ {
22
+ tween.activePass = tweenUpdatePass; // started again, even while active, so this update leaves it alone
23
+ if (tween.active) return;
24
+ tween.active = true;
25
+ tweenActive.push(tween);
26
+ }
27
+ function tweenDeactivate(tween)
28
+ {
29
+ if (!tween.active) return;
30
+ tween.active = false;
31
+ tweenActive.splice(tweenActive.indexOf(tween), 1);
32
+ }
33
+
34
+ // True if the value is an instance of a class that exposes a numeric-percent
35
+ // `lerp(other, percent)` method (Vector2, Color, or any future class).
36
+ function tweenIsLerpable(v) { return v && typeof v.lerp === 'function'; }
37
+
38
+ ///////////////////////////////////////////////////////////////////////////////
39
+
40
+ /** A tween: drives a callback with a value interpolated between
41
+ * `start` and `end` over `duration` seconds. Pauses with the game by default.
42
+ * - In TypeScript it is a `Tween<T>` of the type it tweens, which comes from `start` and `end` or
43
+ * the callback's parameter, so `(v: number)=> ...` takes a number
44
+ * @template [T=any]
45
+ * @memberof TweenSystem
46
+ * @example
47
+ * // Animate a fade-out over 2 seconds with an ease-out sine curve.
48
+ * new Tween((v) => obj.alpha = v, 1, 0, 2, { ease: Ease.OUT(Ease.SINE) });
49
+ */
50
+ class Tween
51
+ {
52
+ /** Create a new tween. The callback fires immediately with `start` so the
53
+ * target snaps to the start value on the same frame the tween is created.
54
+ *
55
+ * `start` and `end` may be numbers, Vector2, Vector3 or Color instances, or
56
+ * any object exposing a `lerp(other, percent) => sameType` method. The
57
+ * callback receives the interpolated value (a number, or a fresh instance
58
+ * for lerp-able types). Both endpoints must be the same type.
59
+ * @param {function(NonNullable<T>):void} callback - Called with the interpolated value each frame
60
+ * @param {T} [start=0] - Starting value
61
+ * @param {T} [end=1] - Ending value
62
+ * @param {number} [duration=1] - Duration in seconds
63
+ * @param {Object} [options]
64
+ * @param {function(number):number} [options.ease] - Easing function (defaults to LINEAR)
65
+ * @param {boolean} [options.useRealTime=false] - Advance even when the game is paused (matches Timer's useRealTime)
66
+ * @param {boolean} [options.paused=false] - Start in paused state */
67
+ constructor(callback, start = /** @type {T} */ (0), end = /** @type {T} */ (1), duration = 1, options = {})
68
+ {
69
+ ASSERT(typeof callback === 'function', 'Tween callback must be a function');
70
+ if (tweenIsLerpable(start))
71
+ {
72
+ ASSERT(start.constructor === end.constructor,
73
+ 'Tween start and end must be the same type');
74
+ }
75
+ else
76
+ {
77
+ ASSERT(isNumber(start), 'Tween start must be a number or have a .lerp method');
78
+ ASSERT(isNumber(end), 'Tween end must be a number when start is a number');
79
+ }
80
+ ASSERT(isNumber(duration) && duration > 0, 'Tween duration must be > 0');
81
+
82
+ // the callback's type is NonNullable<T>, which is T, so that TypeScript takes the type from start and end
83
+ // first: a typed callback like (v: number)=> with start 10 then makes a Tween<number>, not a Tween<0|10>
84
+ /** @property {function(T):void} - Called with the interpolated value each frame
85
+ * @type {function(T):void} */
86
+ this.callback = callback;
87
+ /** @property {T} - Starting value
88
+ * @type {T} */
89
+ this.start = start;
90
+ /** @property {T} - Ending value
91
+ * @type {T} */
92
+ this.end = end;
93
+ /** @property {number} - Total duration in seconds */
94
+ this.duration = duration;
95
+ /** @property {number} - Remaining time in seconds (counts down from duration to 0) */
96
+ this.life = duration;
97
+ /** @property {function(number):number} - Easing curve mapping [0,1] -> [0,1] */
98
+ this.ease = options.ease || Ease.LINEAR;
99
+ /** @property {boolean} - If true, advance even when the game is paused */
100
+ this.useRealTime = !!options.useRealTime;
101
+ /** @property {boolean} - If true, stop advancing until cleared */
102
+ this.paused = !!options.paused;
103
+ /** @property {undefined|function():void} - Called once the tween completes: when its last pass ends,
104
+ * the last iteration of a loop or pingPong, and again each time a restart plays through; then() sets it
105
+ * @type {undefined|function():void} */
106
+ this.onComplete = undefined;
107
+
108
+ /** Continuation when a pass ends, set by loop() and pingPong() to start the next iteration.
109
+ * @private */
110
+ this.thenCallback = undefined;
111
+ /** Remaining iterations including the current run (loop/pingPong only).
112
+ * @private */
113
+ this.loopRemaining = 0;
114
+ /** Whether it is in the active list, see isActive
115
+ * @private */
116
+ this.active = false;
117
+ /** The update it was started in, it first moves on the one after
118
+ * @private */
119
+ this.activePass = 0;
120
+ /** Engine time and real time of its last engine update, it moves by what passed since
121
+ * @private */
122
+ this.lastTime = time;
123
+ /** @private */
124
+ this.lastTimeReal = timeReal;
125
+ /** @property {Object|undefined} - The object tweenProperty animates, the tween stops once it is destroyed,
126
+ * even while paused
127
+ * @type {{destroyed?: boolean}|undefined} */
128
+ this.target = undefined;
129
+
130
+ tweenActivate(this);
131
+ // Snap target to start immediately.
132
+ callback(this.interp(duration));
133
+ }
134
+
135
+ /** Set the easing curve and return this for chaining.
136
+ * @param {function(number):number} easeFn
137
+ * @returns {Tween<T>} */
138
+ setEase(easeFn)
139
+ {
140
+ this.ease = easeFn;
141
+ return this;
142
+ }
143
+
144
+ /** Set the completion callback, `onComplete`, and return this for chaining.
145
+ * It is called once the tween completes: when its pass ends, or for a
146
+ * `loop` or `pingPong` when its last iteration ends, so an endless one
147
+ * never calls it. Calling `then` again replaces the previous callback.
148
+ * - It works with `loop` and `pingPong` in either order, neither replaces the other
149
+ * - It is kept by `restart`, so a restarted tween calls it again when it completes
150
+ * - `stop` and `tweenStopAll` end a tween without calling it
151
+ * @param {function():void} callback
152
+ * @returns {Tween<T>} */
153
+ then(callback)
154
+ {
155
+ this.onComplete = callback;
156
+ return this;
157
+ }
158
+
159
+ /** Repeat this tween `n` total times. After each iteration finishes, the
160
+ * same tween starts over, so the handle returned stays good for the whole
161
+ * loop: pause or stop it to pause or stop every iteration left.
162
+ * `loop()` with no argument loops forever.
163
+ *
164
+ * Mutually exclusive with `pingPong`; calling either replaces the other.
165
+ * A `then` callback, set before or after, is called when the last
166
+ * iteration ends.
167
+ * @param {number} [count=Infinity]
168
+ * @returns {Tween<T>} */
169
+ loop(count = Infinity)
170
+ {
171
+ this.loopRemaining = count;
172
+ this.thenCallback = () => tweenLoopContinuation(this);
173
+ return this;
174
+ }
175
+
176
+ /** Like `loop`, but swap `start` and `end` between iterations so the value
177
+ * bounces back and forth. `pingPong()` with no argument bounces forever.
178
+ *
179
+ * Mutually exclusive with `loop`; calling either replaces the other.
180
+ * A `then` callback, set before or after, is called when the last
181
+ * iteration ends.
182
+ * @param {number} [count=Infinity]
183
+ * @returns {Tween<T>} */
184
+ pingPong(count = Infinity)
185
+ {
186
+ this.loopRemaining = count;
187
+ this.thenCallback = () => tweenPingPongContinuation(this);
188
+ return this;
189
+ }
190
+
191
+ /** Pause this tween. While paused, tweenUpdate skips it. */
192
+ pause() { this.paused = true; }
193
+
194
+ /** Resume a paused tween. */
195
+ resume() { this.paused = false; }
196
+
197
+ /** Reset this tween to the start: life back to duration, pause cleared,
198
+ * re-added to the active list if previously stopped, and the callback
199
+ * re-fired with the start value.
200
+ * It replays one pass: a loop or pingPong that has finished is not started
201
+ * over, a pingPong that ended on its way back plays that way again, and a
202
+ * restart mid loop keeps the iterations left. Call loop or pingPong again
203
+ * after restart to repeat it. The `then` callback is kept and is called
204
+ * again when it completes. */
205
+ restart()
206
+ {
207
+ this.life = this.duration;
208
+ this.paused = false;
209
+ this.lastTime = time;
210
+ this.lastTimeReal = timeReal;
211
+ tweenActivate(this);
212
+ this.callback(this.interp(this.duration));
213
+ }
214
+
215
+ /** True if this tween is in the active list and not paused.
216
+ * @returns {boolean} */
217
+ isActive()
218
+ {
219
+ return !this.paused && this.active;
220
+ }
221
+
222
+ /** Get how far this tween has progressed, from 0 (just started) to 1
223
+ * (completed). Clamped — overshoot past completion still reads 1.
224
+ * @returns {number} */
225
+ getPercent()
226
+ {
227
+ return percent(this.duration - this.life, 0, this.duration);
228
+ }
229
+
230
+ /** Get the current interpolated value (the value most recently passed to
231
+ * the callback). Returns a number, Vector2, Vector3 or Color depending on the
232
+ * tween's start/end types.
233
+ * @returns {T} */
234
+ getValue()
235
+ {
236
+ return this.interp(this.life);
237
+ }
238
+
239
+ /** Compute the interpolated value at the given remaining `life`.
240
+ * At life === duration the result is `start`; at life === 0 it is `end`.
241
+ * - At life 0 it is the end value exactly
242
+ * - A vector goes past its ends as far as the easing does, as a number does; a Color stays between them,
243
+ * so its channels stay in range, and any other type goes as far as its own lerp takes it
244
+ * @param {number} life
245
+ * @returns {T} */
246
+ interp(life)
247
+ {
248
+ // the ends of whatever type it tweens, each kind is handled below
249
+ const s = /** @type {any} */ (this.start), e = /** @type {any} */ (this.end);
250
+ if (life <= 0) // the end exactly, an easing curve may land a rounding error short of it
251
+ return typeof e.copy === 'function' ? e.copy() : e;
252
+ const x = this.ease((this.duration - life) / this.duration);
253
+ // the vectors as their lerp does it, which lands on the end exactly, but without its clamp
254
+ const y = 1 - x;
255
+ if (s instanceof Vector2)
256
+ return /** @type {T} */ (vec2(e.x * x + s.x * y, e.y * x + s.y * y));
257
+ if (typeof Vector3 !== 'undefined' && s instanceof Vector3) // a build may leave out the 3D math
258
+ return /** @type {T} */ (vec3(e.x * x + s.x * y, e.y * x + s.y * y, e.z * x + s.z * y));
259
+ if (tweenIsLerpable(s))
260
+ return s.lerp(e, x);
261
+ return s + (e - s) * x;
262
+ }
263
+
264
+ /** Remove this tween from the active list, ending a loop or pingPong too, without calling
265
+ * the then-callback. It keeps the then-callback, so a restart calls it when it completes. */
266
+ stop()
267
+ {
268
+ tweenDeactivate(this);
269
+ this.thenCallback = undefined;
270
+ }
271
+ }
272
+
273
+ /** Library of named easing curves and direction modifiers.
274
+ * All curves accept `x` in [0,1] and return [0,1] (with possible overshoot
275
+ * for ELASTIC/BACK/SPRING/BOUNCE). Curves are values you pass to `setEase`
276
+ * or compose via the IN/OUT/IN_OUT/PIECEWISE/BEZIER modifiers.
277
+ * @memberof TweenSystem
278
+ * @example
279
+ * // Use a basic curve
280
+ * new Tween(callback, 0, 10, 1).setEase(Ease.SINE);
281
+ * // Use a modifier on a curve
282
+ * new Tween(callback, 0, 10, 1).setEase(Ease.OUT(Ease.BACK));
283
+ */
284
+ const Ease =
285
+ {
286
+ /** Linear (identity) curve.
287
+ * @param {number} x
288
+ * @returns {number}
289
+ * @memberof TweenSystem.Ease */
290
+ LINEAR: (x) => x,
291
+
292
+ /** Power curve factory: `Ease.POWER(n)` returns `x => x**n`.
293
+ * Use n=2 for quadratic, n=3 for cubic, etc.
294
+ * @param {number} n
295
+ * @returns {function(number):number}
296
+ * @memberof TweenSystem.Ease */
297
+ POWER: (n) => (x) => x ** n,
298
+
299
+ /** Sine ease-in curve: starts slow, ends fast.
300
+ * @param {number} x
301
+ * @returns {number}
302
+ * @memberof TweenSystem.Ease */
303
+ SINE: (x) => 1 - cos(x * (PI / 2)),
304
+
305
+ /** Circular ease-in curve.
306
+ * @param {number} x
307
+ * @returns {number}
308
+ * @memberof TweenSystem.Ease */
309
+ CIRC: (x) => 1 - (1 - x * x)**.5,
310
+
311
+ /** Exponential ease-in curve (`2^(10x-10)`).
312
+ * @param {number} x
313
+ * @returns {number}
314
+ * @memberof TweenSystem.Ease */
315
+ EXPO: (x) => x === 0 ? 0 : 2 ** (10 * x - 10),
316
+
317
+ /** Back ease-in: overshoots backward at the start before snapping forward.
318
+ * @param {number} x
319
+ * @returns {number}
320
+ * @memberof TweenSystem.Ease */
321
+ BACK: (x) => x * x * (2.70158 * x - 1.70158),
322
+
323
+ /** Elastic ease-in: oscillations that grow toward the end.
324
+ * @param {number} x
325
+ * @returns {number}
326
+ * @memberof TweenSystem.Ease */
327
+ ELASTIC: (x) =>
328
+ x === 0 ? 0 :
329
+ x === 1 ? 1 :
330
+ -(2 ** (10 * x - 10)) * sin(((37 - 40 * x) * PI) / 6),
331
+
332
+ /** Spring ease-in: wobbles around the start before springing to the end;
333
+ * `Ease.OUT(Ease.SPRING)` overshoots and settles on the target.
334
+ * @param {number} x
335
+ * @returns {number}
336
+ * @memberof TweenSystem.Ease */
337
+ SPRING: (x) =>
338
+ 1 -
339
+ (sin(PI * (1 - x) * (0.2 + 2.5 * (1 - x) ** 3)) *
340
+ x ** 2.2 +
341
+ (1 - x)) *
342
+ (1.0 + 1.2 * x),
343
+
344
+ /** Bouncing ease-in: small bounces near the start, then a rise to the end.
345
+ * Symmetric with the other base curves, which are all ease-in. To get the
346
+ * classic "object falls and hits the ground" shape (bounces near x=1),
347
+ * wrap with `Ease.OUT`: `Ease.OUT(Ease.BOUNCE)`.
348
+ * @param {number} x
349
+ * @returns {number}
350
+ * @memberof TweenSystem.Ease
351
+ * @example
352
+ * Ease.BOUNCE // ease-in bounce (bouncy at start)
353
+ * Ease.OUT(Ease.BOUNCE) // ease-out bounce (object hits ground)
354
+ * Ease.IN_OUT(Ease.BOUNCE) // bounces at both ends
355
+ */
356
+ BOUNCE: (x) =>
357
+ {
358
+ // Inverted form of the standard easeOutBounce: 1 - bounceOut(1 - x).
359
+ let t = 1 - x, f;
360
+ if (t < 4 / 11) f = 7.5625 * t * t;
361
+ else if (t < 8 / 11) f = 7.5625 * (t -= 6 / 11) * t + 0.75;
362
+ else if (t < 10 / 11) f = 7.5625 * (t -= 9 / 11) * t + 0.9375;
363
+ else f = 7.5625 * (t -= 10.5 / 11) * t + 0.984375;
364
+ return 1 - f;
365
+ },
366
+
367
+ /** Ease-in direction modifier: returns the curve unchanged. Symmetric
368
+ * with `OUT` and `IN_OUT`. Base curves are already ease-in by
369
+ * convention, so wrapping a curve in `IN` is a no-op — useful when
370
+ * picking the direction programmatically.
371
+ * @param {function(number):number} f - Curve to use as ease-in (returned unchanged)
372
+ * @returns {function(number):number}
373
+ * @memberof TweenSystem.Ease
374
+ * @example
375
+ * // Pick direction at runtime
376
+ * const dir = bouncyMode ? Ease.OUT : Ease.IN;
377
+ * new Tween(cb, 0, 10, 1).setEase(dir(Ease.BACK));
378
+ */
379
+ IN: (f) => f,
380
+
381
+ /** Reverse a curve so it eases out instead of in: `x => 1 - f(1 - x)`.
382
+ * @param {function(number):number} f
383
+ * @returns {function(number):number}
384
+ * @memberof TweenSystem.Ease
385
+ * @example
386
+ * Ease.OUT(Ease.POWER(2)) // ease-out quadratic
387
+ */
388
+ OUT: (f) => (x) => 1 - f(1 - x),
389
+
390
+ /** Combine the first half of `f` with `Ease.OUT(f)` for a symmetric curve.
391
+ * @param {function(number):number} f
392
+ * @returns {function(number):number}
393
+ * @memberof TweenSystem.Ease */
394
+ IN_OUT: (f) => Ease.PIECEWISE(f, Ease.OUT(f)),
395
+
396
+ /** Split [0,1] into N equal sections and run a different curve in each.
397
+ * Each curve is mapped to its section: section i runs over [i/n, (i+1)/n]
398
+ * and its output is mapped to [i/n, (i+1)/n] of the overall range.
399
+ * @param {...function(number):number} fns
400
+ * @returns {function(number):number}
401
+ * @memberof TweenSystem.Ease */
402
+ PIECEWISE: (...fns) =>
403
+ {
404
+ const n = fns.length;
405
+ return (x) =>
406
+ {
407
+ const i = (x * n - 1e-9) >> 0;
408
+ return (fns[i]((x - i / n) * n) + i) / n;
409
+ };
410
+ },
411
+
412
+ /** Cubic Bezier curve solver in the style of CSS `cubic-bezier`.
413
+ * Control points (0,0), (x1,y1), (x2,y2), (1,1).
414
+ * @param {number} x1
415
+ * @param {number} y1
416
+ * @param {number} x2
417
+ * @param {number} y2
418
+ * @returns {function(number):number}
419
+ * @memberof TweenSystem.Ease
420
+ * @example
421
+ * Ease.BEZIER(0.25, 0.1, 0.25, 1) // CSS "ease"
422
+ */
423
+ BEZIER: (x1, y1, x2, y2) =>
424
+ {
425
+ // Parametric cubic Bezier with implicit (0,0) and (1,1) endpoints.
426
+ const curve = (t) =>
427
+ {
428
+ const u = 1 - t;
429
+ const c1 = 3 * u * u * t;
430
+ const c2 = 3 * u * t * t;
431
+ const t3 = t ** 3;
432
+ return [c1 * x1 + c2 * x2 + t3, c1 * y1 + c2 * y2 + t3];
433
+ };
434
+ return (x) =>
435
+ {
436
+ // the ends are exact, a tween starts and ends on its values
437
+ if (x <= 0) return 0;
438
+ if (x >= 1) return 1;
439
+
440
+ // binary search for t such that curve(t).x = x, then return curve(t).y; a fixed count, since stopping
441
+ // once x is close can leave y far off where the curve is steep, 32 halvings put t within 1e-9
442
+ let t0 = 0, t1 = 1;
443
+ for (let i = 32; i--;)
444
+ {
445
+ const tMid = (t0 + t1) / 2;
446
+ if (curve(tMid)[0] < x) t0 = tMid; else t1 = tMid;
447
+ }
448
+ return curve((t0 + t1) / 2)[1];
449
+ };
450
+ },
451
+ };
452
+
453
+ /** Tween a property on an object by dot-path. Returns the underlying Tween
454
+ * so all chaining methods (`setEase`, `then`, `loop`, `pingPong`, etc.)
455
+ * remain available.
456
+ *
457
+ * `start` and `end` may be numbers, Vector2, Vector3 or Color instances, or
458
+ * any object with a `lerp(other, percent) => sameType` method.
459
+ *
460
+ * It stops on its own once the target is destroyed, so a looping tween on an object ends with it; a tween on a
461
+ * value inside the object, like `tweenProperty(obj.pos, 'x')`, or a new Tween with its own callback, has to be
462
+ * stopped by the game.
463
+ * @template [T=any]
464
+ * @param {Object} target - The object whose property is being animated
465
+ * @param {string} propertyPath - Dot-separated path, e.g. `'pos.x'` or `'color'`
466
+ * @param {T} start - Starting value
467
+ * @param {T} end - Ending value
468
+ * @param {number} [duration=1] - Duration in seconds
469
+ * @param {Object} [options] - Same options as the Tween constructor
470
+ * @param {function(number):number} [options.ease] - Easing function (defaults to LINEAR)
471
+ * @param {boolean} [options.useRealTime=false] - Advance even when the game is paused
472
+ * @param {boolean} [options.paused=false] - Start in paused state
473
+ * @returns {Tween<T>}
474
+ * @memberof TweenSystem
475
+ * @example
476
+ * // Numeric: slide an object's x with an ease-out sine curve
477
+ * tweenProperty(player, 'pos.x', 0, 10, 2).setEase(Ease.OUT(Ease.SINE));
478
+ * // Vector2: animate a position diagonally
479
+ * tweenProperty(player, 'pos', vec2(-5, 0), vec2(5, 3), 2);
480
+ * // Color: pulse between two colors
481
+ * tweenProperty(sprite, 'color', RED, BLUE, 1).pingPong();
482
+ */
483
+ function tweenProperty(target, propertyPath, start, end, duration = 1, options = {})
484
+ {
485
+ ASSERT(target != null && typeof target === 'object', 'tweenProperty target must be an object');
486
+ ASSERT(isStringLike(propertyPath) && propertyPath.length > 0, 'tweenProperty propertyPath must be a non-empty string');
487
+
488
+ const parts = propertyPath.split('.');
489
+ const lastKey = parts.pop();
490
+ let tween;
491
+ const callback = (value) =>
492
+ {
493
+ // a destroyed object ends it, a loop or pingPong would run on it for good and keep it from being freed
494
+ if (target.destroyed) return void tween?.stop();
495
+ let obj = target;
496
+ for (const k of parts)
497
+ {
498
+ obj = obj[k];
499
+ ASSERT(obj != null, 'tweenProperty path does not resolve: ' + propertyPath);
500
+ }
501
+ obj[lastKey] = value;
502
+ };
503
+ tween = new Tween(callback, start, end, duration, options);
504
+ tween.target = target;
505
+ return tween;
506
+ }
507
+
508
+ // Start the next iteration with the time the last one ran over already spent, so a loop keeps its
509
+ // pace; an update that ran over by more than a whole iteration lands where the cycle is, not owing it
510
+ function tweenCarryOvershoot(tween)
511
+ {
512
+ const duration = tween.duration;
513
+ tween.life = duration ? min(tween.life, 0) % duration + duration : 1e-9;
514
+ }
515
+
516
+ // How many iterations the update that finished one ran through: that one and every whole one after it
517
+ function tweenPassed(tween)
518
+ {
519
+ const duration = tween.duration;
520
+ return duration ? 1 + floor(-min(tween.life, 0) / duration) : 1;
521
+ }
522
+
523
+ // start the next iteration of a loop or pingPong, the time the last one ran over already spent, true
524
+ function tweenNextIteration(tween, passed, continuation)
525
+ {
526
+ tween.loopRemaining -= passed;
527
+ tweenCarryOvershoot(tween);
528
+ tween.thenCallback = continuation;
529
+ tweenActivate(tween);
530
+ tween.callback(tween.interp(tween.life)); // snap to where the new iteration is
531
+ return true;
532
+ }
533
+
534
+ // Continuation that schedules the next loop iteration when one finishes, true if it did.
535
+ // Reuses the same Tween across iterations, so the handle from `.loop()` pauses or stops the whole loop.
536
+ function tweenLoopContinuation(tween)
537
+ {
538
+ // count every iteration that went by, a finite loop ends once they run out
539
+ const passed = tweenPassed(tween);
540
+ if (tween.loopRemaining <= passed) return false; // Infinity never runs out
541
+ return tweenNextIteration(tween, passed, () => tweenLoopContinuation(tween));
542
+ }
543
+
544
+ // Continuation for pingPong: swaps start and end on the same tween each iteration, true if it started another.
545
+ function tweenPingPongContinuation(tween)
546
+ {
547
+ // swap the ends once for each iteration that went by, but when they run out the last one keeps its
548
+ // direction, so it ends on the end it really reached and a restart plays that way again
549
+ const passed = tweenPassed(tween);
550
+ const done = tween.loopRemaining <= passed; // Infinity never runs out
551
+ const swaps = done ? max(tween.loopRemaining - 1, 0) : passed;
552
+ if (swaps & 1)
553
+ {
554
+ const tmp = tween.start;
555
+ tween.start = tween.end;
556
+ tween.end = tmp;
557
+ }
558
+ if (done)
559
+ {
560
+ // the completion gave the other end, give the one it finished on
561
+ if (swaps & 1)
562
+ tween.callback(tween.interp(0));
563
+ return false;
564
+ }
565
+ return tweenNextIteration(tween, passed, () => tweenPingPongContinuation(tween));
566
+ }
567
+
568
+ /** Engine plugin hook: advance every active tween by the appropriate delta.
569
+ * The engine calls it with no arguments on every fixed update, so it can run
570
+ * more than once in a rendered frame, and on paused updates too, where only
571
+ * real time tweens move. May also be called with `(gameDelta, realDelta)` to drive tweens without the engine
572
+ * loop, for headless tests or an engine in manual step that is not stepped; in a running game such a call adds to
573
+ * the engine's own update, and a delta of 0 or less does nothing.
574
+ * @param {number} [gameDelta] - Game-time delta in seconds; default: game time since the tween's last engine update
575
+ * @param {number} [realDelta] - Real-time delta in seconds; default: real time since the tween's last engine update
576
+ * @memberof TweenSystem */
577
+ function tweenUpdate(gameDelta, realDelta)
578
+ {
579
+ // Engine path: each tween moves by the time since its own last update, so one made
580
+ // this update, before or after this call, first moves on the next, like a Timer
581
+ const enginePath = gameDelta === undefined;
582
+ if (!enginePath && realDelta === undefined)
583
+ {
584
+ // Manual path with one arg: real and game advance together.
585
+ realDelta = gameDelta;
586
+ }
587
+
588
+ // Move the tweens that were active when this update began, each once: a callback
589
+ // may stop any tween, even all of them, and one that is stopped is skipped; a tween
590
+ // made or started again during the update, like the next turn of a loop, moves on
591
+ // from the next update. Newest first, as the list has always been walked.
592
+ // a callback that calls tweenUpdate itself gets a list of its own, the outer update is still walking this one
593
+ const list = tweenUpdateList.length ? [] : tweenUpdateList, pass = ++tweenUpdatePass;
594
+ for (const t of tweenActive)
595
+ list.push(t);
596
+ for (let i = list.length; i--;)
597
+ {
598
+ const t = list[i];
599
+ if (!t.active || t.activePass === pass) continue; // stopped, or started again by a callback this update
600
+ let dt;
601
+ if (enginePath)
602
+ {
603
+ // a paused tween keeps count too, so it does not jump when resumed
604
+ dt = t.useRealTime ? timeReal - t.lastTimeReal : time - t.lastTime;
605
+ t.lastTime = time;
606
+ t.lastTimeReal = timeReal;
607
+ }
608
+ else
609
+ dt = t.useRealTime ? realDelta : gameDelta;
610
+ if (t.target?.destroyed) { t.stop(); continue; } // its object is gone, paused or not
611
+ if (t.paused || dt <= 0) continue;
612
+
613
+ t.life -= dt;
614
+ if (t.life > 1e-9) // the engine's deltas add up a rounding error short of the duration
615
+ {
616
+ t.callback(t.interp(t.life));
617
+ }
618
+ else
619
+ {
620
+ // Completion: fire end value, remove from active, start the next iteration
621
+ // of a loop or pingPong, or when there is none it has completed, fire onComplete
622
+ t.callback(t.interp(0));
623
+ if (!t.active || t.activePass === pass)
624
+ continue; // stopped or restarted by its own callback, the run it was on ends without completing
625
+ tweenDeactivate(t);
626
+ const next = t.thenCallback;
627
+ t.thenCallback = undefined;
628
+ if (!(next && next()) && t.onComplete)
629
+ t.onComplete();
630
+ }
631
+ }
632
+ list.length = 0;
633
+ }
634
+
635
+ /** Stop every active tween, ending loops too, without calling their then-callbacks.
636
+ * Useful for resets on level transitions or when changing scenes.
637
+ * @memberof TweenSystem */
638
+ function tweenStopAll()
639
+ {
640
+ for (const t of tweenActive)
641
+ t.thenCallback = undefined, t.active = false;
642
+ tweenActive.length = 0;
643
+ }
644
+
645
+ // Register with the engine so tweens auto-advance.
646
+ engineAddPlugin(tweenUpdate);