@codexo/exojs 0.6.10 → 0.6.12

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/dist/exo.esm.js CHANGED
@@ -1,3 +1,448 @@
1
+ const bounceOutFn = (t) => {
2
+ const n1 = 7.5625;
3
+ const d1 = 2.75;
4
+ if (t < 1 / d1) {
5
+ return n1 * t * t;
6
+ }
7
+ else if (t < 2 / d1) {
8
+ return n1 * (t -= 1.5 / d1) * t + 0.75;
9
+ }
10
+ else if (t < 2.5 / d1) {
11
+ return n1 * (t -= 2.25 / d1) * t + 0.9375;
12
+ }
13
+ else {
14
+ return n1 * (t -= 2.625 / d1) * t + 0.984375;
15
+ }
16
+ };
17
+ /**
18
+ * Standard Robert Penner easing functions as static methods.
19
+ * Each function accepts a normalized time `t` in [0, 1] and returns a value
20
+ * that equals 0 at t=0 and 1 at t=1 (overshoot functions like back/elastic
21
+ * may exceed that range between the endpoints).
22
+ *
23
+ * Usage: `Ease.cubicOut`, `Ease.bounceIn`, etc.
24
+ *
25
+ * Note: only scalar numeric properties are supported in v1. Vector, Color, and
26
+ * Matrix interpolation are out of scope.
27
+ */
28
+ class Ease {
29
+ static linear = (t) => t;
30
+ static quadIn = (t) => t * t;
31
+ static quadOut = (t) => t * (2 - t);
32
+ static quadInOut = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
33
+ static cubicIn = (t) => t * t * t;
34
+ static cubicOut = (t) => (--t) * t * t + 1;
35
+ static cubicInOut = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
36
+ static quartIn = (t) => t * t * t * t;
37
+ static quartOut = (t) => 1 - (--t) * t * t * t;
38
+ static quartInOut = (t) => t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2;
39
+ static quintIn = (t) => t * t * t * t * t;
40
+ static quintOut = (t) => 1 + (--t) * t * t * t * t;
41
+ static quintInOut = (t) => t < 0.5 ? 16 * t * t * t * t * t : 1 - Math.pow(-2 * t + 2, 5) / 2;
42
+ static sineIn = (t) => 1 - Math.cos((t * Math.PI) / 2);
43
+ static sineOut = (t) => Math.sin((t * Math.PI) / 2);
44
+ static sineInOut = (t) => -(Math.cos(Math.PI * t) - 1) / 2;
45
+ static expoIn = (t) => t === 0 ? 0 : Math.pow(2, 10 * t - 10);
46
+ static expoOut = (t) => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
47
+ static expoInOut = (t) => {
48
+ if (t === 0)
49
+ return 0;
50
+ if (t === 1)
51
+ return 1;
52
+ return t < 0.5
53
+ ? Math.pow(2, 20 * t - 10) / 2
54
+ : (2 - Math.pow(2, -20 * t + 10)) / 2;
55
+ };
56
+ static circIn = (t) => 1 - Math.sqrt(1 - Math.pow(t, 2));
57
+ static circOut = (t) => Math.sqrt(1 - Math.pow(t - 1, 2));
58
+ static circInOut = (t) => t < 0.5
59
+ ? (1 - Math.sqrt(1 - Math.pow(2 * t, 2))) / 2
60
+ : (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2;
61
+ static backIn = (t) => {
62
+ const c1 = 1.70158;
63
+ const c3 = c1 + 1;
64
+ return c3 * t * t * t - c1 * t * t;
65
+ };
66
+ static backOut = (t) => {
67
+ const c1 = 1.70158;
68
+ const c3 = c1 + 1;
69
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
70
+ };
71
+ static backInOut = (t) => {
72
+ const c1 = 1.70158;
73
+ const c2 = c1 * 1.525;
74
+ return t < 0.5
75
+ ? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
76
+ : (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (2 * t - 2) + c2) + 2) / 2;
77
+ };
78
+ static bounceOut = bounceOutFn;
79
+ static bounceIn = (t) => 1 - bounceOutFn(1 - t);
80
+ static bounceInOut = (t) => t < 0.5
81
+ ? (1 - bounceOutFn(1 - 2 * t)) / 2
82
+ : (1 + bounceOutFn(2 * t - 1)) / 2;
83
+ static elasticIn = (t) => {
84
+ if (t === 0)
85
+ return 0;
86
+ if (t === 1)
87
+ return 1;
88
+ const c4 = (2 * Math.PI) / 3;
89
+ return -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4);
90
+ };
91
+ static elasticOut = (t) => {
92
+ if (t === 0)
93
+ return 0;
94
+ if (t === 1)
95
+ return 1;
96
+ const c4 = (2 * Math.PI) / 3;
97
+ return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
98
+ };
99
+ static elasticInOut = (t) => {
100
+ if (t === 0)
101
+ return 0;
102
+ if (t === 1)
103
+ return 1;
104
+ const c5 = (2 * Math.PI) / 4.5;
105
+ return t < 0.5
106
+ ? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2
107
+ : (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + 1;
108
+ };
109
+ }
110
+
111
+ var TweenState;
112
+ (function (TweenState) {
113
+ TweenState["Idle"] = "idle";
114
+ TweenState["Active"] = "active";
115
+ TweenState["Paused"] = "paused";
116
+ TweenState["Complete"] = "complete";
117
+ TweenState["Stopped"] = "stopped";
118
+ })(TweenState || (TweenState = {}));
119
+
120
+ class Tween {
121
+ _target;
122
+ _state = TweenState.Idle;
123
+ _properties = {};
124
+ _startValues = null;
125
+ _duration = 0;
126
+ _delay = 0;
127
+ _easing = Ease.linear;
128
+ _elapsed = 0;
129
+ _delayElapsed = 0;
130
+ /**
131
+ * Remaining repeat cycles. -1 = infinite.
132
+ * At start this is set to _repeatTotal. Decremented on each cycle boundary.
133
+ */
134
+ _repeatCount = 0;
135
+ /** The value as configured by .repeat(). Preserved for reset. */
136
+ _repeatTotal = 0;
137
+ _yoyo = false;
138
+ /** Current playback direction: 1 = forward, -1 = reverse. */
139
+ _direction = 1;
140
+ _onStart = null;
141
+ _onUpdate = null;
142
+ _onComplete = null;
143
+ _onRepeat = null;
144
+ _chained = null;
145
+ _manager = null;
146
+ /** Whether onStart has already fired this tween lifecycle. */
147
+ _startFired = false;
148
+ constructor(target) {
149
+ this._target = target;
150
+ }
151
+ get target() {
152
+ return this._target;
153
+ }
154
+ get state() {
155
+ return this._state;
156
+ }
157
+ /**
158
+ * Current eased progress in 0..1. Reflects the eased t after applying the
159
+ * easing function, not the raw elapsed/duration ratio.
160
+ */
161
+ get progress() {
162
+ if (this._duration === 0)
163
+ return 1;
164
+ const rawT = Math.min(this._elapsed / this._duration, 1);
165
+ const t = this._direction === 1 ? rawT : 1 - rawT;
166
+ return this._easing(t);
167
+ }
168
+ /**
169
+ * Set target end-values and duration in seconds. Replaces any prior to().
170
+ * The starting values are captured lazily on first update() after start(),
171
+ * so mutating target between to() and start() is safe.
172
+ */
173
+ to(properties, duration) {
174
+ this._properties = { ...properties };
175
+ this._duration = duration;
176
+ this._startValues = null;
177
+ return this;
178
+ }
179
+ /** Delay in seconds before the tween begins interpolating. Default 0. */
180
+ delay(seconds) {
181
+ this._delay = seconds;
182
+ return this;
183
+ }
184
+ /** Easing function applied to the normalized time. Default Ease.linear. */
185
+ easing(fn) {
186
+ this._easing = fn;
187
+ return this;
188
+ }
189
+ /**
190
+ * Number of additional repeat cycles. -1 = infinite. Default 0 (runs once).
191
+ * Note: repeat(2) means the animation runs 3 times total.
192
+ */
193
+ repeat(count) {
194
+ this._repeatTotal = count;
195
+ return this;
196
+ }
197
+ /**
198
+ * Reverse playback direction on each repeat cycle. Only meaningful when
199
+ * combined with repeat(). Calling yoyo() without repeat() is a no-op.
200
+ */
201
+ yoyo(enabled = true) {
202
+ this._yoyo = enabled;
203
+ return this;
204
+ }
205
+ onStart(callback) {
206
+ this._onStart = callback;
207
+ return this;
208
+ }
209
+ onUpdate(callback) {
210
+ this._onUpdate = callback;
211
+ return this;
212
+ }
213
+ onComplete(callback) {
214
+ this._onComplete = callback;
215
+ return this;
216
+ }
217
+ onRepeat(callback) {
218
+ this._onRepeat = callback;
219
+ return this;
220
+ }
221
+ /**
222
+ * Start the tween. If a manager owns this tween it is already tracked;
223
+ * otherwise this is a stand-alone tween driven by manual update() calls.
224
+ */
225
+ start() {
226
+ this._state = TweenState.Active;
227
+ this._elapsed = 0;
228
+ this._delayElapsed = 0;
229
+ this._startValues = null;
230
+ this._startFired = false;
231
+ this._direction = 1;
232
+ this._repeatCount = this._repeatTotal;
233
+ return this;
234
+ }
235
+ /** Pause the tween. update() calls are ignored while paused. */
236
+ pause() {
237
+ if (this._state === TweenState.Active) {
238
+ this._state = TweenState.Paused;
239
+ }
240
+ return this;
241
+ }
242
+ /** Resume a paused tween from where it left off. */
243
+ resume() {
244
+ if (this._state === TweenState.Paused) {
245
+ this._state = TweenState.Active;
246
+ }
247
+ return this;
248
+ }
249
+ /**
250
+ * Stop the tween without finishing. Target properties stay at their
251
+ * current interpolated values. onComplete does NOT fire. The tween is
252
+ * removed from its manager if one is assigned.
253
+ */
254
+ stop() {
255
+ if (this._state === TweenState.Active || this._state === TweenState.Paused) {
256
+ this._state = TweenState.Stopped;
257
+ this._manager?.remove(this);
258
+ }
259
+ return this;
260
+ }
261
+ /**
262
+ * When this tween completes naturally, automatically start `next`.
263
+ * Returns `next` for fluent chaining:
264
+ * `fadeIn.chain(moveOut).start()` — note that start() here starts moveOut,
265
+ * so typically you only call start() on the first tween.
266
+ */
267
+ chain(next) {
268
+ this._chained = next;
269
+ return next;
270
+ }
271
+ /**
272
+ * Attach this tween to a manager. Called by TweenManager.create() and
273
+ * TweenManager.add(). Not part of the public fluent API.
274
+ * @internal
275
+ */
276
+ _attachManager(manager) {
277
+ this._manager = manager;
278
+ }
279
+ /**
280
+ * Advance the tween by deltaSeconds. Called by TweenManager each frame, or
281
+ * manually for stand-alone usage. No-ops when Paused, Stopped, or Complete.
282
+ */
283
+ update(deltaSeconds) {
284
+ if (this._state !== TweenState.Active)
285
+ return;
286
+ // Handle delay phase.
287
+ if (this._delayElapsed < this._delay) {
288
+ this._delayElapsed += deltaSeconds;
289
+ if (this._delayElapsed < this._delay)
290
+ return;
291
+ // Carry overflow past delay into elapsed.
292
+ const overflow = this._delayElapsed - this._delay;
293
+ this._delayElapsed = this._delay;
294
+ deltaSeconds = overflow;
295
+ }
296
+ // Lazy snapshot of start values on the first update after delay.
297
+ if (this._startValues === null) {
298
+ this._captureStartValues();
299
+ }
300
+ // Fire onStart once.
301
+ if (!this._startFired) {
302
+ this._startFired = true;
303
+ this._onStart?.();
304
+ }
305
+ this._elapsed += deltaSeconds;
306
+ // Clamp to duration for this cycle.
307
+ if (this._elapsed >= this._duration) {
308
+ this._elapsed = this._duration;
309
+ }
310
+ // Apply interpolation.
311
+ this._applyProgress();
312
+ if (this._elapsed >= this._duration) {
313
+ // Cycle complete.
314
+ const hasMoreRepeats = this._repeatCount === -1 || this._repeatCount > 0;
315
+ if (hasMoreRepeats) {
316
+ // Decrement repeat counter (skip for infinite).
317
+ if (this._repeatCount !== -1) {
318
+ this._repeatCount--;
319
+ }
320
+ this._onRepeat?.();
321
+ // Flip direction for yoyo.
322
+ if (this._yoyo) {
323
+ this._direction = this._direction === 1 ? -1 : 1;
324
+ }
325
+ // Reset elapsed for next cycle; carry overflow.
326
+ const overflow = this._elapsed - this._duration;
327
+ this._elapsed = overflow > 0 ? Math.min(overflow, this._duration) : 0;
328
+ this._startFired = false; // allow onStart to re-fire next cycle? No — spec says once.
329
+ // Actually spec says onStart fires when actual interpolation begins.
330
+ // For repeats it fires once total at the very first cycle.
331
+ // Re-reading spec: "onStart fires AFTER the delay (when actual interpolation begins)".
332
+ // We'll keep it as one-shot across the full lifecycle; don't reset _startFired.
333
+ this._startFired = true;
334
+ // Apply progress for any overflow.
335
+ if (overflow > 0) {
336
+ this._applyProgress();
337
+ }
338
+ }
339
+ else {
340
+ // All cycles done.
341
+ this._complete();
342
+ }
343
+ }
344
+ }
345
+ _captureStartValues() {
346
+ const snap = {};
347
+ const keys = Object.keys(this._properties);
348
+ for (const key of keys) {
349
+ const val = this._target[key];
350
+ if (typeof val !== 'number') {
351
+ console.warn(`Tween: property "${String(key)}" is not a number on target ` +
352
+ `(got ${typeof val}). It will be skipped.`);
353
+ continue;
354
+ }
355
+ snap[String(key)] = val;
356
+ }
357
+ this._startValues = snap;
358
+ }
359
+ _applyProgress() {
360
+ if (this._startValues === null)
361
+ return;
362
+ const rawT = this._duration === 0 ? 1 : Math.min(this._elapsed / this._duration, 1);
363
+ const t = this._direction === 1 ? rawT : 1 - rawT;
364
+ const easedT = this._easing(t);
365
+ const keys = Object.keys(this._startValues);
366
+ for (const key of keys) {
367
+ const start = this._startValues[key];
368
+ const end = this._properties[key];
369
+ this._target[key] = start + (end - start) * easedT;
370
+ }
371
+ this._onUpdate?.(easedT);
372
+ }
373
+ _complete() {
374
+ // Ensure the target is at its final interpolated position.
375
+ this._elapsed = this._duration;
376
+ this._applyProgress();
377
+ this._state = TweenState.Complete;
378
+ this._manager?.remove(this);
379
+ this._onComplete?.();
380
+ // Fire chained tween, if any.
381
+ this._chained?.start();
382
+ }
383
+ }
384
+
385
+ class TweenManager {
386
+ _tweens = [];
387
+ _destroyed = false;
388
+ /**
389
+ * Create a new Tween targeting `target`, register it with this manager, and
390
+ * return it. Call .to(...).start() on the result to begin animating.
391
+ */
392
+ create(target) {
393
+ const tween = new Tween(target);
394
+ tween._attachManager(this);
395
+ this._tweens.push(tween);
396
+ return tween;
397
+ }
398
+ /**
399
+ * Explicitly add a stand-alone Tween (created via `new Tween(target)`)
400
+ * to this manager so it participates in the update loop.
401
+ */
402
+ add(tween) {
403
+ tween._attachManager(this);
404
+ if (!this._tweens.includes(tween)) {
405
+ this._tweens.push(tween);
406
+ }
407
+ return this;
408
+ }
409
+ /** Remove a tween from the manager. Called automatically on stop/complete. */
410
+ remove(tween) {
411
+ const index = this._tweens.indexOf(tween);
412
+ if (index !== -1) {
413
+ this._tweens.splice(index, 1);
414
+ }
415
+ return this;
416
+ }
417
+ /**
418
+ * Advance all active tweens by deltaSeconds. Called once per frame by
419
+ * Application.update(). Uses a snapshot of the list so that callbacks that
420
+ * add or remove tweens do not corrupt mid-iteration.
421
+ */
422
+ update(deltaSeconds) {
423
+ if (this._destroyed)
424
+ return this;
425
+ const snapshot = this._tweens.slice();
426
+ for (const tween of snapshot) {
427
+ tween.update(deltaSeconds);
428
+ }
429
+ return this;
430
+ }
431
+ /**
432
+ * Remove all tweens immediately. No callbacks (onComplete etc.) fire.
433
+ * The tweens' states are left as-is; they are simply evicted from the list.
434
+ */
435
+ clear() {
436
+ this._tweens = [];
437
+ return this;
438
+ }
439
+ /** Tear down the manager. Clears tweens and makes subsequent updates no-ops. */
440
+ destroy() {
441
+ this.clear();
442
+ this._destroyed = true;
443
+ }
444
+ }
445
+
1
446
  let temp$9 = null;
2
447
  class Size {
3
448
  _width;
@@ -14578,6 +15023,7 @@ class Application {
14578
15023
  loader;
14579
15024
  inputManager;
14580
15025
  sceneManager;
15026
+ tweens = new TweenManager();
14581
15027
  onResize = new Signal();
14582
15028
  _updateHandler;
14583
15029
  _startupClock = new Clock();
@@ -14669,6 +15115,7 @@ class Application {
14669
15115
  const frameStart = performance.now();
14670
15116
  this.backend.resetStats();
14671
15117
  this.inputManager.update();
15118
+ this.tweens.update(frameDelta.seconds);
14672
15119
  const runtimeView = this.backend.view;
14673
15120
  if (runtimeView && typeof runtimeView.update === 'function') {
14674
15121
  runtimeView.update(frameDelta.milliseconds);
@@ -14704,6 +15151,7 @@ class Application {
14704
15151
  this.stop();
14705
15152
  this.loader.destroy();
14706
15153
  this.inputManager.destroy();
15154
+ this.tweens.destroy();
14707
15155
  this._backend.destroy();
14708
15156
  this.sceneManager.destroy();
14709
15157
  this._startupClock.destroy();
@@ -16434,6 +16882,257 @@ class PolarVector {
16434
16882
  }
16435
16883
  }
16436
16884
 
16885
+ // ---------------------------------------------------------------------------
16886
+ // sweepRectangle — AABB vs AABB slab method
16887
+ // ---------------------------------------------------------------------------
16888
+ /**
16889
+ * Swept axis-aligned box vs. axis-aligned box.
16890
+ *
16891
+ * Uses the separating-axis slab method: for each axis we compute the entry
16892
+ * and exit times of the moving box's slab vs the static box's slab, then
16893
+ * combine. `t` is the fraction of the requested move at which first contact
16894
+ * occurs (0 = already overlapping at start, 1 = just barely reaches).
16895
+ *
16896
+ * Already-overlapping case (tEntry < 0 overall): returns `t = 0` with the
16897
+ * normal of the deepest-penetration axis, allowing callers to handle the
16898
+ * "I'm already inside" situation without a separate discrete test.
16899
+ */
16900
+ function sweepRectangle(moving, deltaX, deltaY, target) {
16901
+ const movMinX = moving.x;
16902
+ const movMaxX = moving.x + moving.width;
16903
+ const movMinY = moving.y;
16904
+ const movMaxY = moving.y + moving.height;
16905
+ const tarMinX = target.x;
16906
+ const tarMaxX = target.x + target.width;
16907
+ const tarMinY = target.y;
16908
+ const tarMaxY = target.y + target.height;
16909
+ // X axis
16910
+ let tEntryX = -Infinity;
16911
+ let tExitX = Infinity;
16912
+ if (deltaX > 0) {
16913
+ tEntryX = (tarMinX - movMaxX) / deltaX;
16914
+ tExitX = (tarMaxX - movMinX) / deltaX;
16915
+ }
16916
+ else if (deltaX < 0) {
16917
+ tEntryX = (tarMaxX - movMinX) / deltaX;
16918
+ tExitX = (tarMinX - movMaxX) / deltaX;
16919
+ }
16920
+ else if (movMaxX <= tarMinX || movMinX >= tarMaxX) {
16921
+ // No movement on X and no static overlap — can never collide
16922
+ return null;
16923
+ }
16924
+ // Y axis
16925
+ let tEntryY = -Infinity;
16926
+ let tExitY = Infinity;
16927
+ if (deltaY > 0) {
16928
+ tEntryY = (tarMinY - movMaxY) / deltaY;
16929
+ tExitY = (tarMaxY - movMinY) / deltaY;
16930
+ }
16931
+ else if (deltaY < 0) {
16932
+ tEntryY = (tarMaxY - movMinY) / deltaY;
16933
+ tExitY = (tarMinY - movMaxY) / deltaY;
16934
+ }
16935
+ else if (movMaxY <= tarMinY || movMinY >= tarMaxY) {
16936
+ // No movement on Y and no static overlap — can never collide
16937
+ return null;
16938
+ }
16939
+ const tEntry = Math.max(tEntryX, tEntryY);
16940
+ const tExit = Math.min(tExitX, tExitY);
16941
+ // No overlap window
16942
+ if (tEntry > tExit || tExit < 0 || tEntry > 1) {
16943
+ return null;
16944
+ }
16945
+ const t = Math.max(0, tEntry);
16946
+ const hitX = moving.x + deltaX * t;
16947
+ const hitY = moving.y + deltaY * t;
16948
+ // Normal is on the axis whose slab entry was latest.
16949
+ // Already-overlapping: use the deepest-penetration axis normal.
16950
+ let normalX = 0;
16951
+ let normalY = 0;
16952
+ if (tEntry <= 0) {
16953
+ // Already overlapping — pick the axis with least penetration
16954
+ const overlapX = Math.min(movMaxX - tarMinX, tarMaxX - movMinX);
16955
+ const overlapY = Math.min(movMaxY - tarMinY, tarMaxY - movMinY);
16956
+ if (overlapX < overlapY) {
16957
+ normalX = movMinX < tarMinX ? -1 : 1;
16958
+ }
16959
+ else {
16960
+ normalY = movMinY < tarMinY ? -1 : 1;
16961
+ }
16962
+ }
16963
+ else if (tEntryX > tEntryY) {
16964
+ // X axis had the latest entry
16965
+ normalX = deltaX > 0 ? -1 : 1;
16966
+ }
16967
+ else {
16968
+ // Y axis had the latest entry
16969
+ normalY = deltaY > 0 ? -1 : 1;
16970
+ }
16971
+ return { t, x: hitX, y: hitY, normalX, normalY };
16972
+ }
16973
+ // ---------------------------------------------------------------------------
16974
+ // sweepCircleVsRectangle — expanded-AABB simple fallback (V1)
16975
+ // ---------------------------------------------------------------------------
16976
+ /**
16977
+ * Swept circle vs. axis-aligned box.
16978
+ *
16979
+ * **V1 implementation** uses the simple Minkowski expansion fallback:
16980
+ * the target rectangle is expanded by `circle.radius` on all sides, then
16981
+ * `sweepRectangle` is run treating the circle centre as a zero-sized moving
16982
+ * box. This over-collides at rectangle corners (the circle collides with the
16983
+ * expanded-rect's flat face when geometrically it should curve around the
16984
+ * corner), producing slightly early hits in corner-quadrant trajectories —
16985
+ * a known and acceptable accuracy trade-off for V1.
16986
+ *
16987
+ * TODO (V2): Replace with the full Minkowski rounded-rectangle formulation
16988
+ * that handles the four corner quadrants with per-corner circle-vs-circle
16989
+ * sub-tests.
16990
+ */
16991
+ function sweepCircleVsRectangle(moving, deltaX, deltaY, target) {
16992
+ const r = moving.radius;
16993
+ // Expanded target: grow each side by the circle radius
16994
+ const expanded = new Rectangle(target.x - r, target.y - r, target.width + r * 2, target.height + r * 2);
16995
+ // Treat the circle centre as a zero-sized moving box
16996
+ const centreBox = new Rectangle(moving.x, moving.y, 0, 0);
16997
+ return sweepRectangle(centreBox, deltaX, deltaY, expanded);
16998
+ }
16999
+ // ---------------------------------------------------------------------------
17000
+ // sweepCircleVsCircle — quadratic equation
17001
+ // ---------------------------------------------------------------------------
17002
+ /**
17003
+ * Swept circle vs. stationary circle.
17004
+ *
17005
+ * Solves `|(moving.centre + delta*t) − target.centre|² = (r1+r2)²` for t,
17006
+ * yielding a quadratic. Returns the smaller root if it is in [0, 1].
17007
+ *
17008
+ * Already-overlapping case: returns `{ t: 0 }` with the normal pointing from
17009
+ * target → moving (or an arbitrary normal if both centres coincide).
17010
+ */
17011
+ function sweepCircleVsCircle(moving, deltaX, deltaY, target) {
17012
+ const dx = moving.x - target.x;
17013
+ const dy = moving.y - target.y;
17014
+ const r = moving.radius + target.radius;
17015
+ const a = deltaX * deltaX + deltaY * deltaY;
17016
+ const b = 2 * (dx * deltaX + dy * deltaY);
17017
+ const c = dx * dx + dy * dy - r * r;
17018
+ // Already overlapping at start
17019
+ if (c <= 0) {
17020
+ const dist = Math.sqrt(dx * dx + dy * dy);
17021
+ const normalX = dist > 0 ? dx / dist : 1;
17022
+ const normalY = dist > 0 ? dy / dist : 0;
17023
+ return { t: 0, x: moving.x, y: moving.y, normalX, normalY };
17024
+ }
17025
+ // No movement
17026
+ if (a === 0) {
17027
+ return null;
17028
+ }
17029
+ const disc = b * b - 4 * a * c;
17030
+ if (disc < 0) {
17031
+ return null;
17032
+ }
17033
+ const t = (-b - Math.sqrt(disc)) / (2 * a);
17034
+ if (t < 0 || t > 1) {
17035
+ return null;
17036
+ }
17037
+ const hitX = moving.x + deltaX * t;
17038
+ const hitY = moving.y + deltaY * t;
17039
+ // Normal points from target centre → hit circle centre
17040
+ const normalX = (hitX - target.x) / r;
17041
+ const normalY = (hitY - target.y) / r;
17042
+ return { t, x: hitX, y: hitY, normalX, normalY };
17043
+ }
17044
+ // ---------------------------------------------------------------------------
17045
+ // Batch helpers — sweep a shape against multiple targets
17046
+ // ---------------------------------------------------------------------------
17047
+ /**
17048
+ * Returns the earliest `SweptHit` against an array of rectangle targets, or
17049
+ * `null` if none are hit.
17050
+ *
17051
+ * Optimisation: before testing each target individually the swept AABB of the
17052
+ * moving rectangle is computed once; targets whose AABB does not overlap the
17053
+ * swept AABB are skipped.
17054
+ */
17055
+ function sweepRectangleAgainst(moving, deltaX, deltaY, targets) {
17056
+ if (targets.length === 0) {
17057
+ return null;
17058
+ }
17059
+ // Swept AABB of the moving rectangle (broad-phase skip)
17060
+ const sweptMinX = Math.min(moving.x, moving.x + deltaX);
17061
+ const sweptMaxX = Math.max(moving.x + moving.width, moving.x + moving.width + deltaX);
17062
+ const sweptMinY = Math.min(moving.y, moving.y + deltaY);
17063
+ const sweptMaxY = Math.max(moving.y + moving.height, moving.y + moving.height + deltaY);
17064
+ let earliest = null;
17065
+ for (const target of targets) {
17066
+ // Broad-phase: skip if swept AABB doesn't overlap target AABB
17067
+ if (sweptMaxX <= target.x
17068
+ || sweptMinX >= target.x + target.width
17069
+ || sweptMaxY <= target.y
17070
+ || sweptMinY >= target.y + target.height) {
17071
+ continue;
17072
+ }
17073
+ const hit = sweepRectangle(moving, deltaX, deltaY, target);
17074
+ if (hit !== null && (earliest === null || hit.t < earliest.t)) {
17075
+ earliest = hit;
17076
+ }
17077
+ }
17078
+ return earliest;
17079
+ }
17080
+ /**
17081
+ * Returns the earliest `SweptHit` against an array of circle targets, or
17082
+ * `null` if none are hit.
17083
+ *
17084
+ * Optimisation: the swept AABB of the moving circle is computed once and used
17085
+ * to skip targets that cannot possibly be reached.
17086
+ */
17087
+ function sweepCircleAgainst(moving, deltaX, deltaY, targets) {
17088
+ if (targets.length === 0) {
17089
+ return null;
17090
+ }
17091
+ // Swept AABB of the moving circle
17092
+ const sweptMinX = Math.min(moving.x, moving.x + deltaX) - moving.radius;
17093
+ const sweptMaxX = Math.max(moving.x, moving.x + deltaX) + moving.radius;
17094
+ const sweptMinY = Math.min(moving.y, moving.y + deltaY) - moving.radius;
17095
+ const sweptMaxY = Math.max(moving.y, moving.y + deltaY) + moving.radius;
17096
+ let earliest = null;
17097
+ for (const target of targets) {
17098
+ // Broad-phase: skip if swept AABB doesn't overlap target's AABB
17099
+ if (sweptMaxX <= target.x - target.radius
17100
+ || sweptMinX >= target.x + target.radius
17101
+ || sweptMaxY <= target.y - target.radius
17102
+ || sweptMinY >= target.y + target.radius) {
17103
+ continue;
17104
+ }
17105
+ const hit = sweepCircleVsCircle(moving, deltaX, deltaY, target);
17106
+ if (hit !== null && (earliest === null || hit.t < earliest.t)) {
17107
+ earliest = hit;
17108
+ }
17109
+ }
17110
+ return earliest;
17111
+ }
17112
+ // ---------------------------------------------------------------------------
17113
+ // substepSweep — generic fallback iterator
17114
+ // ---------------------------------------------------------------------------
17115
+ /**
17116
+ * Generator that yields evenly-spaced position snapshots along a movement
17117
+ * vector so the caller can run their own discrete intersection check at each
17118
+ * step. Useful for arbitrary shape pairs that lack a closed-form swept test.
17119
+ *
17120
+ * `maxStepSize` controls the step granularity — smaller values produce more
17121
+ * accurate detection but more iterations. Use the smallest dimension of the
17122
+ * smaller collider as a sensible default.
17123
+ *
17124
+ * Always yields at least 2 snapshots (t=0 and t=1), even for zero-length
17125
+ * deltas.
17126
+ */
17127
+ function* substepSweep(fromX, fromY, deltaX, deltaY, maxStepSize) {
17128
+ const length = Math.hypot(deltaX, deltaY);
17129
+ const stepCount = Math.max(1, Math.ceil(length / maxStepSize));
17130
+ for (let i = 0; i <= stepCount; i++) {
17131
+ const t = i / stepCount;
17132
+ yield { x: fromX + deltaX * t, y: fromY + deltaY * t, t };
17133
+ }
17134
+ }
17135
+
16437
17136
  class ColorAffector {
16438
17137
  _fromColor;
16439
17138
  _toColor;
@@ -18289,5 +18988,5 @@ class IndexedDbStore {
18289
18988
  }
18290
18989
  }
18291
18990
 
18292
- export { AbstractAssetFactory, AbstractMedia, AbstractWebGl2BatchedRenderer, AbstractWebGl2Renderer, AbstractWebGpuRenderer, AnimatedSprite, Application, ApplicationStatus, ArcadeStickGamepadMapping, AudioAnalyser, BinaryFactory, BlendModes, BlurFilter, Bounds, BufferTypes, BufferUsage, BundleLoadError, CacheFirstStrategy, CallbackRenderPass, Capabilities, ChannelOffset, ChannelSize, Circle, Clock, CollisionType, Color, ColorAffector, ColorFilter, Container, Drawable, DynamicGlyphAtlas, Ellipse, FactoryRegistry, Filter, Flags, FontFactory, ForceAffector, GameCubeGamepadMapping, Gamepad, GamepadChannel, GamepadControl, GamepadMapping, GamepadMappingFamily, GamepadPromptLayouts, GenericDualAnalogGamepadMapping, Graphics, ImageFactory, IndexedDbDatabase, IndexedDbStore, Input, InputManager, Interval, JoyConLeftGamepadMapping, JoyConRightGamepadMapping, Json, JsonFactory, Keyboard, Line, Loader, Matrix, Mesh, Music, MusicFactory, NetworkOnlyStrategy, ObservableSize, ObservableVector, Particle, ParticleOptions, ParticleSystem, PlayStationGamepadMapping, Pointer, PointerState, PointerStateFlag, PolarVector, Polygon, Quadtree, Random, Rectangle, RenderBackendType, RenderNode, RenderTarget, RenderTargetPass, RenderTexture, RendererRegistry, RenderingPrimitives, Sampler, ScaleAffector, ScaleModes, Scene, SceneManager, SceneNode, Segment, Shader, ShaderAttribute, ShaderPrimitives, ShaderUniform, Signal, Size, Sound, SoundFactory, Sprite, SpriteFlags, Spritesheet, SteamControllerGamepadMapping, SvgAsset, SvgFactory, SwitchProGamepadMapping, Text, TextAsset, TextFactory, TextStyle, Texture, TextureFactory, Time, Timer, TorqueAffector, UniversalEmitter, Vector, Video, VideoFactory, View, ViewFlags, VoronoiRegion, VttAsset, VttFactory, WasmFactory, WebGl2Backend, WebGl2MeshRenderer, WebGl2ParticleRenderer, WebGl2RenderBuffer, WebGl2ShaderBlock, WebGl2SpriteRenderer, WebGl2VertexArrayObject, WebGpuBackend, WebGpuMeshRenderer, WebGpuParticleRenderer, WebGpuSpriteRenderer, WrapModes, XboxGamepadMapping, bezierCurveTo, buildCircle, buildEllipse, buildLine, buildPath, buildPolygon, buildRectangle, buildStar, builtInGamepadDefinitions, canvasSourceToDataUrl, clamp, createRenderStats, createWebGl2ShaderProgram, decodeAudioData, defineAssetManifest, degreesPerRadian, degreesToRadians, determineMimeType, emptyArrayBuffer, getAudioContext, getCanvasSourceSize, getCollisionCircleCircle, getCollisionCircleRectangle, getCollisionPolygonCircle, getCollisionRectangleRectangle, getCollisionSat, getDistance, getOfflineAudioContext, getPreciseTime, getTextureSourceSize, getVoronoiRegion$1 as getVoronoiRegion, getWebGpuBlendState, hours, inRange, intersectionCircleCircle, intersectionCircleEllipse, intersectionCirclePoly, intersectionEllipseEllipse, intersectionEllipsePoly, intersectionLineCircle, intersectionLineEllipse, intersectionLineLine, intersectionLinePoly, intersectionLineRect, intersectionPointCircle, intersectionPointEllipse, intersectionPointLine, intersectionPointPoint, intersectionPointPoly, intersectionPointRect, intersectionPolyPoly, intersectionRectCircle, intersectionRectEllipse, intersectionRectPoly, intersectionRectRect, intersectionSat, isAudioContextReady, isPowerOfTwo, layoutText, lerp, matchesIds, maxPointers, milliseconds, minutes, noop$1 as noop, normalizeIds, onAudioContextReady, parseGamepadDescriptor, pointerSlotSize, quadraticCurveTo, radiansPerDegree, radiansToDegrees, rand, removeArrayItems, resetRenderStats, resolveDefinition, resolveGamepadDefinition, seconds, sign, stopEvent, supportsCodec, supportsEventOptions, supportsIndexedDb, supportsPointerEvents, supportsTouchEvents, supportsWebAudio, tau, trimRotation, webGl2PrimitiveArrayConstructors, webGl2PrimitiveByteSizeMapping, webGl2PrimitiveTypeNames };
18991
+ export { AbstractAssetFactory, AbstractMedia, AbstractWebGl2BatchedRenderer, AbstractWebGl2Renderer, AbstractWebGpuRenderer, AnimatedSprite, Application, ApplicationStatus, ArcadeStickGamepadMapping, AudioAnalyser, BinaryFactory, BlendModes, BlurFilter, Bounds, BufferTypes, BufferUsage, BundleLoadError, CacheFirstStrategy, CallbackRenderPass, Capabilities, ChannelOffset, ChannelSize, Circle, Clock, CollisionType, Color, ColorAffector, ColorFilter, Container, Drawable, DynamicGlyphAtlas, Ease, Ellipse, FactoryRegistry, Filter, Flags, FontFactory, ForceAffector, GameCubeGamepadMapping, Gamepad, GamepadChannel, GamepadControl, GamepadMapping, GamepadMappingFamily, GamepadPromptLayouts, GenericDualAnalogGamepadMapping, Graphics, ImageFactory, IndexedDbDatabase, IndexedDbStore, Input, InputManager, Interval, JoyConLeftGamepadMapping, JoyConRightGamepadMapping, Json, JsonFactory, Keyboard, Line, Loader, Matrix, Mesh, Music, MusicFactory, NetworkOnlyStrategy, ObservableSize, ObservableVector, Particle, ParticleOptions, ParticleSystem, PlayStationGamepadMapping, Pointer, PointerState, PointerStateFlag, PolarVector, Polygon, Quadtree, Random, Rectangle, RenderBackendType, RenderNode, RenderTarget, RenderTargetPass, RenderTexture, RendererRegistry, RenderingPrimitives, Sampler, ScaleAffector, ScaleModes, Scene, SceneManager, SceneNode, Segment, Shader, ShaderAttribute, ShaderPrimitives, ShaderUniform, Signal, Size, Sound, SoundFactory, Sprite, SpriteFlags, Spritesheet, SteamControllerGamepadMapping, SvgAsset, SvgFactory, SwitchProGamepadMapping, Text, TextAsset, TextFactory, TextStyle, Texture, TextureFactory, Time, Timer, TorqueAffector, Tween, TweenManager, TweenState, UniversalEmitter, Vector, Video, VideoFactory, View, ViewFlags, VoronoiRegion, VttAsset, VttFactory, WasmFactory, WebGl2Backend, WebGl2MeshRenderer, WebGl2ParticleRenderer, WebGl2RenderBuffer, WebGl2ShaderBlock, WebGl2SpriteRenderer, WebGl2VertexArrayObject, WebGpuBackend, WebGpuMeshRenderer, WebGpuParticleRenderer, WebGpuSpriteRenderer, WrapModes, XboxGamepadMapping, bezierCurveTo, buildCircle, buildEllipse, buildLine, buildPath, buildPolygon, buildRectangle, buildStar, builtInGamepadDefinitions, canvasSourceToDataUrl, clamp, createRenderStats, createWebGl2ShaderProgram, decodeAudioData, defineAssetManifest, degreesPerRadian, degreesToRadians, determineMimeType, emptyArrayBuffer, getAudioContext, getCanvasSourceSize, getCollisionCircleCircle, getCollisionCircleRectangle, getCollisionPolygonCircle, getCollisionRectangleRectangle, getCollisionSat, getDistance, getOfflineAudioContext, getPreciseTime, getTextureSourceSize, getVoronoiRegion$1 as getVoronoiRegion, getWebGpuBlendState, hours, inRange, intersectionCircleCircle, intersectionCircleEllipse, intersectionCirclePoly, intersectionEllipseEllipse, intersectionEllipsePoly, intersectionLineCircle, intersectionLineEllipse, intersectionLineLine, intersectionLinePoly, intersectionLineRect, intersectionPointCircle, intersectionPointEllipse, intersectionPointLine, intersectionPointPoint, intersectionPointPoly, intersectionPointRect, intersectionPolyPoly, intersectionRectCircle, intersectionRectEllipse, intersectionRectPoly, intersectionRectRect, intersectionSat, isAudioContextReady, isPowerOfTwo, layoutText, lerp, matchesIds, maxPointers, milliseconds, minutes, noop$1 as noop, normalizeIds, onAudioContextReady, parseGamepadDescriptor, pointerSlotSize, quadraticCurveTo, radiansPerDegree, radiansToDegrees, rand, removeArrayItems, resetRenderStats, resolveDefinition, resolveGamepadDefinition, seconds, sign, stopEvent, substepSweep, supportsCodec, supportsEventOptions, supportsIndexedDb, supportsPointerEvents, supportsTouchEvents, supportsWebAudio, sweepCircleAgainst, sweepCircleVsCircle, sweepCircleVsRectangle, sweepRectangle, sweepRectangleAgainst, tau, trimRotation, webGl2PrimitiveArrayConstructors, webGl2PrimitiveByteSizeMapping, webGl2PrimitiveTypeNames };
18293
18992
  //# sourceMappingURL=exo.esm.js.map