@codexo/exojs 0.15.0 → 0.15.2

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 (77) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/esm/animation/Tween.d.ts.map +1 -1
  3. package/dist/esm/animation/Tween.js +6 -4
  4. package/dist/esm/animation/Tween.js.map +1 -1
  5. package/dist/esm/audio/AudioManager.d.ts.map +1 -1
  6. package/dist/esm/audio/AudioManager.js +14 -4
  7. package/dist/esm/audio/AudioManager.js.map +1 -1
  8. package/dist/esm/audio/Playable.d.ts +6 -1
  9. package/dist/esm/audio/Playable.d.ts.map +1 -1
  10. package/dist/esm/core/Application.d.ts.map +1 -1
  11. package/dist/esm/core/Application.js +2 -0
  12. package/dist/esm/core/Application.js.map +1 -1
  13. package/dist/esm/core/BuildInfo.js +2 -2
  14. package/dist/esm/core/SceneNode.d.ts +10 -1
  15. package/dist/esm/core/SceneNode.d.ts.map +1 -1
  16. package/dist/esm/core/SceneNode.js +10 -1
  17. package/dist/esm/core/SceneNode.js.map +1 -1
  18. package/dist/esm/debug/BoundingBoxesLayer.d.ts.map +1 -1
  19. package/dist/esm/debug/BoundingBoxesLayer.js +2 -1
  20. package/dist/esm/debug/BoundingBoxesLayer.js.map +1 -1
  21. package/dist/esm/debug/HitTestLayer.js +4 -4
  22. package/dist/esm/debug/HitTestLayer.js.map +1 -1
  23. package/dist/esm/debug/PerformanceLayer.js +1 -1
  24. package/dist/esm/debug/PerformanceLayer.js.map +1 -1
  25. package/dist/esm/debug/RenderPassInspectorLayer.js +2 -2
  26. package/dist/esm/debug/RenderPassInspectorLayer.js.map +1 -1
  27. package/dist/esm/input/InputManager.d.ts.map +1 -1
  28. package/dist/esm/input/InputManager.js +12 -3
  29. package/dist/esm/input/InputManager.js.map +1 -1
  30. package/dist/esm/math/AbstractVector.d.ts +4 -3
  31. package/dist/esm/math/AbstractVector.d.ts.map +1 -1
  32. package/dist/esm/math/AbstractVector.js +5 -4
  33. package/dist/esm/math/AbstractVector.js.map +1 -1
  34. package/dist/esm/math/ObservableVector.d.ts +2 -0
  35. package/dist/esm/math/ObservableVector.d.ts.map +1 -1
  36. package/dist/esm/math/ObservableVector.js +9 -0
  37. package/dist/esm/math/ObservableVector.js.map +1 -1
  38. package/dist/esm/math/Polygon.d.ts.map +1 -1
  39. package/dist/esm/math/Polygon.js +4 -1
  40. package/dist/esm/math/Polygon.js.map +1 -1
  41. package/dist/esm/math/collision-detection.d.ts.map +1 -1
  42. package/dist/esm/math/collision-detection.js +13 -13
  43. package/dist/esm/math/collision-detection.js.map +1 -1
  44. package/dist/esm/math/collision-primitives.d.ts +1 -1
  45. package/dist/esm/math/collision-primitives.d.ts.map +1 -1
  46. package/dist/esm/math/collision-primitives.js +4 -1
  47. package/dist/esm/math/collision-primitives.js.map +1 -1
  48. package/dist/esm/rendering/primitives/Graphics.d.ts +7 -0
  49. package/dist/esm/rendering/primitives/Graphics.d.ts.map +1 -1
  50. package/dist/esm/rendering/primitives/Graphics.js +20 -6
  51. package/dist/esm/rendering/primitives/Graphics.js.map +1 -1
  52. package/dist/esm/rendering/sprite/AnimatedSprite.d.ts +1 -0
  53. package/dist/esm/rendering/sprite/AnimatedSprite.d.ts.map +1 -1
  54. package/dist/esm/rendering/sprite/AnimatedSprite.js +18 -1
  55. package/dist/esm/rendering/sprite/AnimatedSprite.js.map +1 -1
  56. package/dist/esm/rendering/sprite/Sprite.d.ts.map +1 -1
  57. package/dist/esm/rendering/sprite/Sprite.js +8 -0
  58. package/dist/esm/rendering/sprite/Sprite.js.map +1 -1
  59. package/dist/esm/rendering/text/TextLayout.d.ts.map +1 -1
  60. package/dist/esm/rendering/text/TextLayout.js +2 -3
  61. package/dist/esm/rendering/text/TextLayout.js.map +1 -1
  62. package/dist/esm/resources/Loader.d.ts.map +1 -1
  63. package/dist/esm/resources/Loader.js +20 -3
  64. package/dist/esm/resources/Loader.js.map +1 -1
  65. package/dist/esm/resources/coreAssetBindings.d.ts +11 -0
  66. package/dist/esm/resources/coreAssetBindings.d.ts.map +1 -1
  67. package/dist/esm/resources/coreAssetBindings.js +10 -2
  68. package/dist/esm/resources/coreAssetBindings.js.map +1 -1
  69. package/dist/exo.debug.esm.js +1 -1
  70. package/dist/exo.debug.esm.js.map +1 -1
  71. package/dist/exo.esm.js +1 -1
  72. package/dist/exo.esm.js.map +1 -1
  73. package/dist/exo.iife.js +157 -46
  74. package/dist/exo.iife.js.map +1 -1
  75. package/dist/exo.iife.min.js +1 -1
  76. package/dist/exo.iife.min.js.map +1 -1
  77. package/package.json +1 -1
package/dist/exo.iife.js CHANGED
@@ -419,8 +419,10 @@ var Exo = (function (exports) {
419
419
  this._onStart?.();
420
420
  }
421
421
  this._elapsed += deltaSeconds;
422
- // Clamp to duration for this cycle.
423
- if (this._elapsed >= this._duration) {
422
+ // Clamp to duration for this cycle, remembering the overshoot so it can
423
+ // carry into the next repeat cycle below.
424
+ const overflow = this._elapsed - this._duration;
425
+ if (overflow >= 0) {
424
426
  this._elapsed = this._duration;
425
427
  }
426
428
  // Apply interpolation.
@@ -438,8 +440,8 @@ var Exo = (function (exports) {
438
440
  if (this._yoyo) {
439
441
  this._direction = this._direction === 1 ? -1 : 1;
440
442
  }
441
- // Reset elapsed for next cycle; carry overflow.
442
- const overflow = this._elapsed - this._duration;
443
+ // Reset elapsed for next cycle; carry the overshoot (at most one
444
+ // extra cycle per update call).
443
445
  this._elapsed = overflow > 0 ? Math.min(overflow, this._duration) : 0;
444
446
  // Apply progress for any overflow.
445
447
  if (overflow > 0) {
@@ -1769,12 +1771,13 @@ var Exo = (function (exports) {
1769
1771
  */
1770
1772
  class AbstractVector {
1771
1773
  /**
1772
- * Angle of this vector in radians, measured from the positive Y-axis
1773
- * (clockwise). Setting this rotates the vector to the new angle while
1774
- * preserving its {@link length}. Mutates in place.
1774
+ * Angle of this vector in radians, measured from the positive X-axis
1775
+ * (the same convention as `PolarVector.phi`). Setting this rotates the
1776
+ * vector to the new angle while preserving its {@link length}. Mutates in
1777
+ * place.
1775
1778
  */
1776
1779
  get angle() {
1777
- return Math.atan2(this.x, this.y);
1780
+ return Math.atan2(this.y, this.x);
1778
1781
  }
1779
1782
  set angle(angle) {
1780
1783
  const length = this.length;
@@ -2089,7 +2092,10 @@ var Exo = (function (exports) {
2089
2092
  const normY = (y1 - y2) / ry;
2090
2093
  return normX * normX + normY * normY <= 1;
2091
2094
  };
2092
- const intersectionPointPoly$1 = (point, { points }) => polygonContainsPoint(point, points);
2095
+ const intersectionPointPoly$1 = (point, { x: offsetX, y: offsetY, points }) =>
2096
+ // Shift the point into the polygon's local space so its x/y position
2097
+ // offset is honoured (the local `points` never carry the offset).
2098
+ polygonContainsPoint({ x: point.x - offsetX, y: point.y - offsetY }, points);
2093
2099
  const intersectionLineLineSegments = (a1, a2, b1, b2) => {
2094
2100
  const denominator = (a2.x - a1.x) * (b2.y - b1.y) - (b2.x - b1.x) * (a2.y - a1.y);
2095
2101
  if (Math.abs(denominator) <= epsilon$1) {
@@ -2317,8 +2323,13 @@ var Exo = (function (exports) {
2317
2323
  if (getVoronoiRegion(edgeX, edgeY, pointX, pointY) !== VoronoiRegion.right) {
2318
2324
  return false;
2319
2325
  }
2320
- const region = getVoronoiRegion(nextEdge.x, nextEdge.y, circleX - nextPoint.x, circleY - nextPoint.y);
2321
- return region === VoronoiRegion.left && getVectorLength(pointX, pointY) > radius;
2326
+ // In the right region the closest polygon feature is the *next* vertex, so
2327
+ // both the region refinement and the distance test use `circle - nextPoint`
2328
+ // (mirrors the right-region branch of getCollisionPolygonCircle).
2329
+ const positionBx = circleX - nextPoint.x;
2330
+ const positionBy = circleY - nextPoint.y;
2331
+ const region = getVoronoiRegion(nextEdge.x, nextEdge.y, positionBx, positionBy);
2332
+ return region === VoronoiRegion.left && getVectorLength(positionBx, positionBy) > radius;
2322
2333
  };
2323
2334
  const shouldExcludeMiddleVoronoi = (pointX, pointY, radius, edgeX, edgeY) => {
2324
2335
  const normalX = edgeY;
@@ -2331,15 +2342,10 @@ var Exo = (function (exports) {
2331
2342
  return distance > 0 && Math.abs(distance) > radius;
2332
2343
  };
2333
2344
  const intersectionCirclePoly = ({ x: cx, y: cy, radius }, { x: px, y: py, points, edges }) => {
2334
- // Frame transform: express the circle's position relative to the polygon's
2335
- // local space, but with the sign inverted (poly.position - circle.position
2336
- // rather than the natural circle.position - poly.position). The poly's
2337
- // `points` are then in their local coordinates and the Voronoi-region tests
2338
- // below combine them with the negated-offset circle position to reach the
2339
- // same value as a positively-offset frame would. Don't flip without
2340
- // re-deriving the Voronoi math.
2341
- const circleX = px - cx;
2342
- const circleY = py - cy;
2345
+ // Frame transform: the circle centre expressed in the polygon's local space
2346
+ // (circle.position - poly.position), matching getCollisionPolygonCircle.
2347
+ const circleX = cx - px;
2348
+ const circleY = cy - py;
2343
2349
  const len = points.length;
2344
2350
  for (let i = 0; i < len; i++) {
2345
2351
  // i, prev, next are all valid indices into the parallel length-`len`
@@ -2428,8 +2434,8 @@ var Exo = (function (exports) {
2428
2434
  difference.destroy();
2429
2435
  return null;
2430
2436
  }
2431
- const projectionN = difference.clone().normalize();
2432
- const projectionV = difference.multiply(overlap);
2437
+ const projectionN = difference.normalize();
2438
+ const projectionV = projectionN.clone().multiply(overlap);
2433
2439
  return {
2434
2440
  shapeA: circleA,
2435
2441
  shapeB: circleB,
@@ -2942,10 +2948,19 @@ var Exo = (function (exports) {
2942
2948
  this._owner?._onObservableChange(this._channel);
2943
2949
  }
2944
2950
  }
2951
+ // The getters must be redeclared alongside the setter overrides: a
2952
+ // setter-only accessor on the subclass prototype would shadow the whole
2953
+ // inherited accessor pair, leaving `get` undefined.
2954
+ get angle() {
2955
+ return Math.atan2(this._y, this._x);
2956
+ }
2945
2957
  set angle(angle) {
2946
2958
  const length = this.length;
2947
2959
  this.set(Math.cos(angle) * length, Math.sin(angle) * length);
2948
2960
  }
2961
+ get length() {
2962
+ return Math.sqrt(this._x * this._x + this._y * this._y);
2963
+ }
2949
2964
  set length(magnitude) {
2950
2965
  const angle = this.angle;
2951
2966
  this.set(Math.cos(angle) * magnitude, Math.sin(angle) * magnitude);
@@ -4808,9 +4823,19 @@ var Exo = (function (exports) {
4808
4823
  this._registered.set('master', this.master);
4809
4824
  this._registered.set('music', this.music);
4810
4825
  this._registered.set('sound', this.sound);
4811
- onAudioContextReady.add(() => {
4812
- this.onUnlock.dispatch();
4813
- });
4826
+ if (isAudioContextReady()) {
4827
+ // The shared context is already running — this manager was constructed
4828
+ // after the one-shot ready signal fired (e.g. a second Application in
4829
+ // the same process; the buses above would otherwise consume the signal
4830
+ // before this handler registers). Dispatch the unlock on a microtask so
4831
+ // subscribers registered right after construction still observe it.
4832
+ queueMicrotask(() => this.onUnlock.dispatch());
4833
+ }
4834
+ else {
4835
+ onAudioContextReady.add(() => {
4836
+ this.onUnlock.dispatch();
4837
+ });
4838
+ }
4814
4839
  }
4815
4840
  /**
4816
4841
  * When `true`, the master bus is muted while `document.hidden` is true.
@@ -6607,6 +6632,9 @@ var Exo = (function (exports) {
6607
6632
  const length = Math.sqrt(axis.x * axis.x + axis.y * axis.y) || 1;
6608
6633
  const nx = axis.x / length;
6609
6634
  const ny = axis.y / length;
6635
+ // World-space projection: the polygon's x/y offset shifts every vertex by
6636
+ // the same amount, so it contributes a constant term along the axis.
6637
+ const offset = nx * this._position.x + ny * this._position.y;
6610
6638
  const points = this._points;
6611
6639
  let min = Infinity;
6612
6640
  let max = -Infinity;
@@ -6619,7 +6647,7 @@ var Exo = (function (exports) {
6619
6647
  if (projection > max)
6620
6648
  max = projection;
6621
6649
  }
6622
- return result.set(min, max);
6650
+ return result.set(min + offset, max + offset);
6623
6651
  }
6624
6652
  contains(x, y) {
6625
6653
  return intersectionPointPoly(Vector.temp.set(x, y), this);
@@ -7289,9 +7317,18 @@ var Exo = (function (exports) {
7289
7317
  this._invalidateSubtreeTransform();
7290
7318
  this._invalidateBoundsCascade();
7291
7319
  }
7320
+ /**
7321
+ * Re-derive `origin` from the fractional anchor and the CURRENT local
7322
+ * bounds. Uses local (untransformed) bounds on purpose: the transform
7323
+ * multiplies the origin by scale itself, so deriving from world bounds
7324
+ * would double-apply scale whenever the anchor is set after scaling.
7325
+ * Subclasses whose local bounds change after construction (e.g. a sprite
7326
+ * switching to a texture sub-frame) must call this to keep an anchored
7327
+ * node anchored.
7328
+ */
7292
7329
  _updateOrigin() {
7293
7330
  const { x, y } = this._anchor;
7294
- const { width, height } = this.getBounds();
7331
+ const { width, height } = this.getLocalBounds();
7295
7332
  this.setOrigin(width * x, height * y);
7296
7333
  }
7297
7334
  }
@@ -13108,9 +13145,18 @@ var Exo = (function (exports) {
13108
13145
  existing._refreshBrowserGamepad(browserGamepad);
13109
13146
  }
13110
13147
  }
13111
- for (const [browserIndex, pad] of [...this.gamepadsByBrowserIndex.entries()]) {
13112
- if (!seenBrowserIndices.has(browserIndex)) {
13113
- this.gamepadsByBrowserIndex.delete(browserIndex);
13148
+ // Two pads can vanish in the same poll, and the compact strategy's shift
13149
+ // re-points map entries while this loop runs — so resolve each browser
13150
+ // index against the LIVE map at dispatch time instead of a pre-loop
13151
+ // entries snapshot (a stale pad reference would disconnect the wrong,
13152
+ // already-repurposed slot and leave a ghost `connected` pad behind).
13153
+ for (const browserIndex of [...this.gamepadsByBrowserIndex.keys()]) {
13154
+ if (seenBrowserIndices.has(browserIndex)) {
13155
+ continue;
13156
+ }
13157
+ const pad = this.gamepadsByBrowserIndex.get(browserIndex);
13158
+ this.gamepadsByBrowserIndex.delete(browserIndex);
13159
+ if (pad !== undefined) {
13114
13160
  this.handleGamepadDisconnect(pad);
13115
13161
  }
13116
13162
  }
@@ -16342,6 +16388,14 @@ var Exo = (function (exports) {
16342
16388
  this.width = width;
16343
16389
  this.height = height;
16344
16390
  }
16391
+ // The local bounds changed size — re-derive the origin from the
16392
+ // fractional anchor, or an anchored sprite keeps the OLD bounds' pixel
16393
+ // origin and renders offset by the size difference (an anchored sprite
16394
+ // switching from the full atlas to its first animation frame used to
16395
+ // land hundreds of pixels off-canvas).
16396
+ if (this.anchor.x !== 0 || this.anchor.y !== 0) {
16397
+ this._updateOrigin();
16398
+ }
16345
16399
  this.invalidateCache();
16346
16400
  return this;
16347
16401
  }
@@ -16618,13 +16672,12 @@ var Exo = (function (exports) {
16618
16672
  const extraPerGap = (maxLineWidth - line.width) / gaps;
16619
16673
  let wordIdx = -1;
16620
16674
  let prevWasSpace = true;
16621
- const spaceAdv = provider.getGlyph(' ', fontSize).advance;
16622
16675
  for (const entry of line.placements) {
16623
- if (prevWasSpace && entry.info.advance !== spaceAdv) {
16676
+ if (prevWasSpace && entry.char !== ' ') {
16624
16677
  wordIdx++;
16625
16678
  prevWasSpace = false;
16626
16679
  }
16627
- else if (!prevWasSpace && entry.info.advance === spaceAdv) {
16680
+ else if (!prevWasSpace && entry.char === ' ') {
16628
16681
  prevWasSpace = true;
16629
16682
  }
16630
16683
  result.push({
@@ -33408,14 +33461,22 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
33408
33461
  * parent's source. `new URL(ref, source)` only works when `source` is an
33409
33462
  * absolute URL; loaders are frequently called with relative paths (e.g.
33410
33463
  * `assets/demo/fonts/x.fnt`), so fall back to a synthetic base and strip it.
33464
+ * A root-absolute source (`/assets/demo/fonts/x.fnt`) must yield a
33465
+ * root-absolute result again — dropping the leading slash would make the
33466
+ * browser re-resolve the page image against the document base URL.
33467
+ * @internal exported for tests
33411
33468
  */
33412
33469
  function resolveSubAssetPath(ref, source) {
33470
+ if (/^(?:[a-z][a-z\d+.-]*:|\/\/|\/)/i.test(ref)) {
33471
+ return ref;
33472
+ }
33413
33473
  try {
33414
33474
  return new URL(ref, source).href;
33415
33475
  }
33416
33476
  catch {
33417
33477
  const base = 'https://exojs.invalid/';
33418
- return new URL(ref, base + source.replace(/^\/+/, '')).href.slice(base.length);
33478
+ const resolved = new URL(ref, base + source.replace(/^\/+/, '')).href.slice(base.length);
33479
+ return source.startsWith('/') ? `/${resolved}` : resolved;
33419
33480
  }
33420
33481
  }
33421
33482
  // ---------------------------------------------------------------------------
@@ -34334,6 +34395,13 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
34334
34395
  * instead if you need to await completion.
34335
34396
  */
34336
34397
  backgroundLoad() {
34398
+ // Start a fresh progress batch only when idle; a re-entrant call while the
34399
+ // queue is draining extends the running batch instead (mirrors the
34400
+ // accounting in _loadSingleBackground).
34401
+ if (this._backgroundQueue.length === 0 && this._backgroundActive === 0) {
34402
+ this._backgroundLoaded = 0;
34403
+ this._backgroundTotal = 0;
34404
+ }
34337
34405
  for (const [type, entries] of this._manifest) {
34338
34406
  for (const [alias, entry] of entries) {
34339
34407
  if (this._hasResource(type, alias))
@@ -34341,16 +34409,17 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
34341
34409
  const key = this._key(type, alias);
34342
34410
  if (this._inFlight.has(key))
34343
34411
  continue;
34412
+ if (this._isQueuedInBackground(type, alias))
34413
+ continue;
34344
34414
  this._backgroundQueue.push({
34345
34415
  type,
34346
34416
  alias,
34347
34417
  path: entry.path,
34348
34418
  options: entry.options,
34349
34419
  });
34420
+ this._backgroundTotal++;
34350
34421
  }
34351
34422
  }
34352
- this._backgroundTotal = this._backgroundQueue.length;
34353
- this._backgroundLoaded = 0;
34354
34423
  this._drainBackground();
34355
34424
  }
34356
34425
  /**
@@ -35109,7 +35178,16 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
35109
35178
  if (leftPrototype !== rightPrototype) {
35110
35179
  return false;
35111
35180
  }
35112
- if (leftPrototype !== Object.prototype && leftPrototype !== null) {
35181
+ // Same-prototype instances compare structurally by their own enumerable
35182
+ // keys — the registerManifest contract allows deeply-equal options of any
35183
+ // shared class, not just plain objects. Built-ins whose state is NOT
35184
+ // carried in enumerable own keys need explicit handling: Dates compare by
35185
+ // timestamp; other exotic containers stay reference-only (Object.is above)
35186
+ // so two distinct-but-similar instances never count as equivalent.
35187
+ if (left instanceof Date) {
35188
+ return left.getTime() === right.getTime();
35189
+ }
35190
+ if (left instanceof Map || left instanceof Set || left instanceof RegExp || left instanceof ArrayBuffer || ArrayBuffer.isView(left)) {
35113
35191
  return false;
35114
35192
  }
35115
35193
  const leftObject = left;
@@ -36661,12 +36739,26 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
36661
36739
  this.addChild(this._createStrokeMesh(data));
36662
36740
  return this;
36663
36741
  }
36742
+ /**
36743
+ * Stroke a shape's perimeter. Shape builders return their outline OPEN
36744
+ * (first point not repeated), but `buildPath` only closes a path whose
36745
+ * first and last points coincide — so the closing segment (e.g. a
36746
+ * rectangle's left edge) went missing. Repeat the first point here.
36747
+ */
36748
+ _strokeClosedOutline(points) {
36749
+ if (points.length >= 4) {
36750
+ this.drawPath([...points, points[0], points[1]]);
36751
+ }
36752
+ else {
36753
+ this.drawPath(points);
36754
+ }
36755
+ }
36664
36756
  /** Fill a closed polygon defined by `[x0,y0, x1,y1, ...]` and optionally stroke its outline. */
36665
36757
  drawPolygon(path) {
36666
36758
  const data = buildPolygon(path);
36667
36759
  this._appendFill(data);
36668
36760
  if (this._lineWidth > 0) {
36669
- this.drawPath(data.points);
36761
+ this._strokeClosedOutline(data.points);
36670
36762
  }
36671
36763
  return this;
36672
36764
  }
@@ -36675,7 +36767,7 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
36675
36767
  const data = buildCircle(centerX, centerY, radius);
36676
36768
  this._appendFill(data);
36677
36769
  if (this._lineWidth > 0) {
36678
- this.drawPath(data.points);
36770
+ this._strokeClosedOutline(data.points);
36679
36771
  }
36680
36772
  return this;
36681
36773
  }
@@ -36684,7 +36776,7 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
36684
36776
  const data = buildEllipse(centerX, centerY, radiusX, radiusY);
36685
36777
  this._appendFill(data);
36686
36778
  if (this._lineWidth > 0) {
36687
- this.drawPath(data.points);
36779
+ this._strokeClosedOutline(data.points);
36688
36780
  }
36689
36781
  return this;
36690
36782
  }
@@ -36693,7 +36785,7 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
36693
36785
  const data = buildRectangle(x, y, width, height);
36694
36786
  this._appendFill(data);
36695
36787
  if (this._lineWidth > 0) {
36696
- this.drawPath(data.points);
36788
+ this._strokeClosedOutline(data.points);
36697
36789
  }
36698
36790
  return this;
36699
36791
  }
@@ -36706,7 +36798,7 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
36706
36798
  const data = buildRoundedRectangle(x, y, width, height, radius);
36707
36799
  this._appendFill(data);
36708
36800
  if (this._lineWidth > 0) {
36709
- this.drawPath(data.points);
36801
+ this._strokeClosedOutline(data.points);
36710
36802
  }
36711
36803
  return this;
36712
36804
  }
@@ -36718,7 +36810,7 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
36718
36810
  const data = buildStar(centerX, centerY, points, radius, innerRadius, rotation);
36719
36811
  this._appendFill(data);
36720
36812
  if (this._lineWidth > 0) {
36721
- this.drawPath(data.points);
36813
+ this._strokeClosedOutline(data.points);
36722
36814
  }
36723
36815
  return this;
36724
36816
  }
@@ -36885,6 +36977,7 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
36885
36977
  _clips = new Map();
36886
36978
  _currentClipName = null;
36887
36979
  _currentFrameIndex = 0;
36980
+ _hasAppliedFrame = false;
36888
36981
  _playing = false;
36889
36982
  _repeatOverride = null;
36890
36983
  _elapsedFrameTimeMs = 0;
@@ -37165,7 +37258,23 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
37165
37258
  */
37166
37259
  _applyFrame(clip, frameIndex) {
37167
37260
  // In-bounds by every call site's own guard.
37168
- this.setTextureFrame(clip.frames[frameIndex], false);
37261
+ if (this._hasAppliedFrame) {
37262
+ // Frame-to-frame advance: keep the current pixel size so differently
37263
+ // sized frames don't visibly pop.
37264
+ this.setTextureFrame(clip.frames[frameIndex], false);
37265
+ }
37266
+ else {
37267
+ // First application: the sprite still shows the full source texture
37268
+ // (usually the whole atlas), so "keep the pixel size" would inflate the
37269
+ // scale by atlasSize/frameSize and the sprite would render blown up far
37270
+ // beyond the canvas. Snap the logical size to the frame instead — while
37271
+ // preserving the user's scale, which `resetSize` would reset to 1.
37272
+ const scaleX = this.scale.x;
37273
+ const scaleY = this.scale.y;
37274
+ this.setTextureFrame(clip.frames[frameIndex], true);
37275
+ this.scale.set(scaleX, scaleY);
37276
+ this._hasAppliedFrame = true;
37277
+ }
37169
37278
  const offset = clip.frameOffsets?.[frameIndex];
37170
37279
  if (offset) {
37171
37280
  this.getLocalBounds().setPosition(offset.x, offset.y);
@@ -39574,6 +39683,8 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
39574
39683
  gamepadSlotStrategy: inputOptions.gamepadSlotStrategy ?? defaultInputSettings.gamepadSlotStrategy,
39575
39684
  pointerDistanceThreshold: inputOptions.pointerDistanceThreshold ?? defaultInputSettings.pointerDistanceThreshold,
39576
39685
  },
39686
+ ...(appSettings.seed !== undefined && { seed: appSettings.seed }),
39687
+ ...(appSettings.fixedTimeStep !== undefined && { fixedTimeStep: appSettings.fixedTimeStep }),
39577
39688
  };
39578
39689
  // Capture extension snapshot before constructing extension-sensitive subsystems.
39579
39690
  this._snapshot = appSettings.extensions === undefined ? getGlobalSnapshotInternal() : buildSnapshot([...(appSettings.extensions ?? [])]);
@@ -40139,8 +40250,8 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
40139
40250
  }
40140
40251
 
40141
40252
  const buildInfo = Object.freeze({
40142
- version: "0.15.0",
40143
- revision: "18cadeb",
40253
+ version: "0.15.2",
40254
+ revision: "e3df957",
40144
40255
  development: false,
40145
40256
  });
40146
40257