melonjs 20.1.1 → 20.2.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 (40) hide show
  1. package/README.md +2 -2
  2. package/build/application/settings.d.ts +3 -2
  3. package/build/application/settings.d.ts.map +1 -1
  4. package/build/index.js +1304 -219
  5. package/build/index.js.map +4 -4
  6. package/build/input/pointerevent.d.ts.map +1 -1
  7. package/build/particles/emitter.d.ts +81 -3
  8. package/build/particles/emitter.d.ts.map +1 -1
  9. package/build/particles/particle.d.ts +62 -1
  10. package/build/particles/particle.d.ts.map +1 -1
  11. package/build/particles/settings.d.ts +50 -7
  12. package/build/particles/settings.d.ts.map +1 -1
  13. package/build/renderable/container.d.ts +10 -0
  14. package/build/renderable/container.d.ts.map +1 -1
  15. package/build/renderable/entity/entity.d.ts +10 -0
  16. package/build/renderable/entity/entity.d.ts.map +1 -1
  17. package/build/renderable/renderable.d.ts +106 -22
  18. package/build/renderable/renderable.d.ts.map +1 -1
  19. package/build/video/blendmodes.d.ts +107 -0
  20. package/build/video/blendmodes.d.ts.map +1 -0
  21. package/build/video/canvas/canvas_renderer.d.ts +28 -24
  22. package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
  23. package/build/video/effects/blendEffect.d.ts +57 -0
  24. package/build/video/effects/blendEffect.d.ts.map +1 -0
  25. package/build/video/renderer.d.ts +3 -4
  26. package/build/video/renderer.d.ts.map +1 -1
  27. package/build/video/rendertarget/canvasrendertarget.d.ts +1 -0
  28. package/build/video/rendertarget/canvasrendertarget.d.ts.map +1 -1
  29. package/build/video/webgl/batchers/batcher.d.ts.map +1 -1
  30. package/build/video/webgl/glshader.d.ts.map +1 -1
  31. package/build/video/webgl/utils/program.d.ts.map +1 -1
  32. package/build/video/webgl/webgl_renderer.d.ts +116 -29
  33. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  34. package/build/video/webgpu/batchers/quad_batcher.d.ts +7 -0
  35. package/build/video/webgpu/batchers/quad_batcher.d.ts.map +1 -1
  36. package/build/video/webgpu/pipeline/cache.d.ts +0 -9
  37. package/build/video/webgpu/pipeline/cache.d.ts.map +1 -1
  38. package/build/video/webgpu/webgpu_renderer.d.ts +100 -6
  39. package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
  40. package/package.json +3 -3
package/build/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * melonJS Game Engine - 20.1.1
2
+ * melonJS Game Engine - 20.2.0
3
3
  * http://www.melonjs.org
4
4
  * melonjs is licensed under the MIT License.
5
5
  * http://www.opensource.org/licenses/mit-license
@@ -7770,6 +7770,20 @@ var CanvasRenderTarget = class extends RenderTarget {
7770
7770
  * @param {CanvasRenderer|WebGLRenderer} renderer - the renderer to which this canvas texture is attached
7771
7771
  */
7772
7772
  invalidate(renderer) {
7773
+ const busy = renderer._advancedBlendBusy;
7774
+ if (typeof busy === "number") {
7775
+ renderer._advancedBlendBusy = busy + 1;
7776
+ }
7777
+ try {
7778
+ this.#invalidate(renderer);
7779
+ } finally {
7780
+ if (typeof busy === "number") {
7781
+ renderer._advancedBlendBusy = busy;
7782
+ }
7783
+ }
7784
+ }
7785
+ /** @ignore */
7786
+ #invalidate(renderer) {
7773
7787
  if (renderer.type.startsWith("WebGL")) {
7774
7788
  renderer.flush();
7775
7789
  renderer.setBatcher("quad");
@@ -10807,11 +10821,10 @@ var Renderer = class _Renderer {
10807
10821
  return this.currentBlendMode;
10808
10822
  }
10809
10823
  /**
10810
- * set the current blend mode.
10811
- * Subclasses (CanvasRenderer, WebGLRenderer, WebGPURenderer once it
10812
- * lands) implement the actual backend logic.
10824
+ * set the current blend mode. Every renderer supports the same set —
10825
+ * see {@link CanvasRenderer#setBlendMode} for the list and what each does.
10813
10826
  * @param {string} [mode="normal"] - blend mode
10814
- * @param {boolean} [premultipliedAlpha=true] - whether textures use premultiplied alpha (GPU renderers only)
10827
+ * @param {boolean} [premultipliedAlpha=true] - whether textures use premultiplied alpha
10815
10828
  */
10816
10829
  setBlendMode(mode = "normal") {
10817
10830
  this.currentBlendMode = mode;
@@ -17552,6 +17565,8 @@ var Body = class {
17552
17565
  };
17553
17566
 
17554
17567
  // src/renderable/renderable.js
17568
+ var _chain = [];
17569
+ var _level = new Matrix3d();
17555
17570
  var Renderable = class _Renderable extends Rect {
17556
17571
  /**
17557
17572
  * @param {number} x - position of the renderable object (accessible through inherited pos.x property)
@@ -17986,13 +18001,33 @@ var Renderable = class _Renderable extends Rect {
17986
18001
  }
17987
18002
  }
17988
18003
  /**
17989
- * return the renderable absolute position in the game world. The
17990
- * returned vector is a {@link Vector3d} so the z component is summed
17991
- * across the ancestor chain too — important for {@link Camera3d}'s
17992
- * frustum culling, which previously read `obj.depth` (local
17993
- * `pos.z`) and mis-culled children nested under a container with
17994
- * its own non-zero depth.
17995
- * @returns {Vector3d}
18004
+ * Where this renderable IS in the game world — its own `pos` plus every
18005
+ * ancestor's, as a {@link Vector3d} so the z component is summed across
18006
+ * the chain too (important for {@link Camera3d}'s frustum culling, which
18007
+ * previously read `obj.depth` — local `pos.z` — and mis-culled children
18008
+ * nested under a container with its own non-zero depth).
18009
+ *
18010
+ * **Reach for this** for anything positional: culling, distance checks,
18011
+ * hit tests, placing one renderable relative to another. It is cheap, and
18012
+ * it is what the engine's own culling uses.
18013
+ *
18014
+ * **Reach for {@link Renderable#getWorldTransform} instead** when a
18015
+ * position is not enough — when rotation, scale or flip along the ancestor
18016
+ * chain matters, or when you need to map an arbitrary point rather than
18017
+ * just the origin. This method sums translations only, so under a rotated
18018
+ * or scaled ancestor it reports where the renderable's *pivot* is and
18019
+ * nothing about how its content is oriented.
18020
+ *
18021
+ * Note the two also frame the question differently. This one is "where am
18022
+ * I"; `getWorldTransform()` is "what space is my content drawn in". For a
18023
+ * {@link Container} those coincide, because a container offsets its
18024
+ * children by its own position. For a leaf they differ by exactly that
18025
+ * position, which a leaf applies inside its own `draw()`.
18026
+ *
18027
+ * The returned vector is pooled and reused — copy it if you need to hold
18028
+ * onto the value across another call.
18029
+ * @returns {Vector3d} this renderable's absolute position
18030
+ * @see Renderable#getWorldTransform
17996
18031
  */
17997
18032
  getAbsolutePosition() {
17998
18033
  if (typeof this._absPos === "undefined") {
@@ -18004,6 +18039,121 @@ var Renderable = class _Renderable extends Rect {
18004
18039
  }
18005
18040
  return this._absPos;
18006
18041
  }
18042
+ /**
18043
+ * The transform this renderable interposes between its ancestor's frame
18044
+ * and the frame its own content is drawn in — a mirror of what
18045
+ * {@link Renderable#preDraw} applies to the renderer, as a matrix.
18046
+ *
18047
+ * **This is not {@link Renderable#currentTransform}.** A renderable's
18048
+ * placement is split across two members: `pos` holds where it is, and
18049
+ * `currentTransform` holds only what `rotate()` / `scale()` / `translate()`
18050
+ * accumulate — it never contains the position. `preDraw` composes the two
18051
+ * by conjugation, so a rotation pivots about the renderable's position
18052
+ * rather than the origin. On a renderable you never rotated,
18053
+ * `currentTransform` is therefore the *identity* and says nothing about
18054
+ * where its content lands, while this method returns the translation that
18055
+ * actually places it.
18056
+ *
18057
+ * {@link Container} extends this with the offset it applies to its
18058
+ * children, which a leaf renderable does not have: a leaf's own `draw()`
18059
+ * places itself from `pos`.
18060
+ * @protected
18061
+ * @param {Matrix3d} out - matrix to write into; nothing is stored on the
18062
+ * renderable itself, so callers own the lifetime
18063
+ * @returns {Matrix3d} `out`, for chaining
18064
+ * @see Renderable#getWorldTransform
18065
+ */
18066
+ getLocalTransform(out) {
18067
+ const ax = Number.isFinite(this.width) ? this.width * this.anchorPoint.x : 0;
18068
+ const ay = Number.isFinite(this.height) ? this.height * this.anchorPoint.y : 0;
18069
+ out.identity();
18070
+ if (this._flip.x || this._flip.y) {
18071
+ const dx = this._flip.x ? this.centerX - ax : 0;
18072
+ const dy = this._flip.y ? this.centerY - ay : 0;
18073
+ out.translate(dx, dy);
18074
+ out.scale(this._flip.x ? -1 : 1, this._flip.y ? -1 : 1);
18075
+ out.translate(-dx, -dy);
18076
+ }
18077
+ if (this.autoTransform === true && !this.currentTransform.isIdentity()) {
18078
+ out.translate(this.pos.x, this.pos.y);
18079
+ out.multiply(this.currentTransform);
18080
+ out.translate(-this.pos.x, -this.pos.y);
18081
+ }
18082
+ if (this.applyAnchorTransform !== false) {
18083
+ out.translate(-ax, -ay);
18084
+ }
18085
+ return out;
18086
+ }
18087
+ /**
18088
+ * The space this renderable's content is drawn IN, as a matrix — the full
18089
+ * form of {@link Renderable#getAbsolutePosition}, which sums positions up
18090
+ * the ancestor chain and therefore cannot represent the rotation, scale or
18091
+ * flip accumulated along the way.
18092
+ *
18093
+ * **Reach for `getAbsolutePosition()` instead** for ordinary positional
18094
+ * work — culling, distance checks, hit tests. It is cheaper and it is what
18095
+ * the engine culls with. **Use this** when a position is not enough:
18096
+ *
18097
+ * - an ancestor is rotated or scaled, so a translation cannot describe the
18098
+ * result
18099
+ * - you need to map an arbitrary point, not just the origin — a corner, a
18100
+ * click position, one renderable's coordinates into another's space
18101
+ * - you need to compose or invert the transform (`inv(A) · B` converts
18102
+ * between two frames, which is how `ParticleEmitter.referenceSpace`
18103
+ * measures particles against a container that is not their parent)
18104
+ *
18105
+ * The two also frame the question differently, and it shows on a leaf.
18106
+ * `getAbsolutePosition()` is "where am I"; this is "what space is my
18107
+ * content drawn in". For a {@link Container} those coincide, because a
18108
+ * container offsets its children by its own position. For a leaf they
18109
+ * differ by exactly that position, which a leaf applies inside its own
18110
+ * `draw()`. So with no rotation, scale or flip anywhere, a container's
18111
+ * translation column equals its `getAbsolutePosition()` while a leaf's
18112
+ * equals its PARENT's.
18113
+ *
18114
+ * The walk stops at a `floating` ancestor, because a floating renderable
18115
+ * draws in screen space: {@link Container#draw} resets the transform
18116
+ * outright for those, so the chain genuinely ends there rather than
18117
+ * continuing to the root.
18118
+ *
18119
+ * The camera needs no special handling — {@link Camera2d} folds its view
18120
+ * transform into the root container's `currentTransform`, so it is picked
18121
+ * up like any other level.
18122
+ * @param {Matrix3d} out - matrix to write into; nothing is stored on the
18123
+ * renderable itself, so callers own the lifetime
18124
+ * @returns {Matrix3d} `out`, for chaining
18125
+ * @see Renderable#getAbsolutePosition
18126
+ * @example
18127
+ * // map a point from one renderable's space into another's
18128
+ * const from = a.getWorldTransform(new Matrix3d());
18129
+ * const into = b.getWorldTransform(new Matrix3d()).invert();
18130
+ * const point = new Vector2d(10, 20); // in a's space
18131
+ * from.apply(point); // -> world
18132
+ * into.apply(point); // -> b's space
18133
+ * @example
18134
+ * // just need to know where something is? use the cheaper call
18135
+ * const where = renderable.getAbsolutePosition();
18136
+ */
18137
+ getWorldTransform(out) {
18138
+ _chain.length = 0;
18139
+ _chain.push(this);
18140
+ if (this.floating !== true) {
18141
+ let node = this.ancestor;
18142
+ while (typeof node !== "undefined" && node !== null) {
18143
+ _chain.push(node);
18144
+ if (node.floating === true) {
18145
+ break;
18146
+ }
18147
+ node = node.ancestor;
18148
+ }
18149
+ }
18150
+ out.identity();
18151
+ for (let i = _chain.length; i-- > 0; ) {
18152
+ out.multiply(_chain[i].getLocalTransform(_level));
18153
+ }
18154
+ _chain.length = 0;
18155
+ return out;
18156
+ }
18007
18157
  /**
18008
18158
  * Prepare the rendering context before drawing (automatically called by melonJS).
18009
18159
  * This will apply any defined transforms, anchor point, tint or blend mode and translate the context accordingly to this renderable position.
@@ -18298,6 +18448,7 @@ function compileProgram(gl, vertex, fragment2, attributes) {
18298
18448
  throw new Error(error_msg);
18299
18449
  }
18300
18450
  gl.useProgram(program);
18451
+ gl.__meCurrentProgram = program;
18301
18452
  gl.deleteShader(vertShader);
18302
18453
  gl.deleteShader(fragShader);
18303
18454
  return program;
@@ -18595,6 +18746,7 @@ var GLShader = class _GLShader {
18595
18746
  return;
18596
18747
  }
18597
18748
  this.gl.useProgram(this.program);
18749
+ this.gl.__meCurrentProgram = this.program;
18598
18750
  }
18599
18751
  /**
18600
18752
  * returns the location of an attribute variable in this shader program
@@ -26779,6 +26931,20 @@ var Container = class _Container extends Renderable {
26779
26931
  }
26780
26932
  return super.update(dt);
26781
26933
  }
26934
+ /**
26935
+ * A container additionally offsets its children by its own position — the
26936
+ * `translate()` at the top of {@link Container#draw} — which is what makes
26937
+ * a child's coordinates mean "relative to my parent" rather than naming a
26938
+ * place in the world. A leaf renderable has no such term.
26939
+ * @protected
26940
+ * @param {Matrix3d} out - matrix to write into
26941
+ * @returns {Matrix3d} `out`, for chaining
26942
+ */
26943
+ getLocalTransform(out) {
26944
+ super.getLocalTransform(out);
26945
+ out.translate(this.pos.x, this.pos.y);
26946
+ return out;
26947
+ }
26782
26948
  /**
26783
26949
  * draw this renderable (automatically called by melonJS)
26784
26950
  * @param {CanvasRenderer|WebGLRenderer} renderer - a renderer instance
@@ -35760,7 +35926,7 @@ var CanvasRenderer = class extends Renderer {
35760
35926
  }
35761
35927
  /**
35762
35928
  * set the current blend mode for this renderer. <br>
35763
- * All renderers support: <br>
35929
+ * Every renderer supports the full set: <br>
35764
35930
  * - "normal" : draws new content on top of the existing content <br>
35765
35931
  * <img src="../images/normal-blendmode.png" width="180"/> <br>
35766
35932
  * - "add", "additive", or "lighter" : color values are added together <br>
@@ -35769,26 +35935,30 @@ var CanvasRenderer = class extends Renderer {
35769
35935
  * <img src="../images/multiply-blendmode.png" width="180"/> <br>
35770
35936
  * - "screen" : pixels are inverted, multiplied, and inverted again (opposite of multiply) <br>
35771
35937
  * <img src="../images/screen-blendmode.png" width="180"/> <br>
35772
- * Canvas (browser-dependent) and WebGL2: <br>
35773
35938
  * - "darken" : retains the darkest pixels of both layers <br>
35774
35939
  * <img src="../images/darken-blendmode.png" width="180"/> <br>
35775
35940
  * - "lighten" : retains the lightest pixels of both layers <br>
35776
35941
  * <img src="../images/lighten-blendmode.png" width="180"/> <br>
35777
- * Canvas only, browser-dependent (falls back to "normal" if unsupported or in WebGL): <br>
35778
- * - "overlay" <br>
35942
+ * - "overlay" : multiplies or screens, depending on the backdrop <br>
35779
35943
  * <img src="../images/overlay-blendmode.png" width="180"/> <br>
35780
- * - "color-dodge" <br>
35781
- * <img src="../images/color-dodge-blendmode.png" width="180"/> <br>
35782
- * - "color-burn" <br>
35783
- * <img src="../images/color-burn-blendmode.png" width="180"/> <br>
35784
- * - "hard-light" <br>
35944
+ * - "hard-light" : overlay with the layers swapped — a harsh spotlight <br>
35785
35945
  * <img src="../images/hard-light-blendmode.png" width="180"/> <br>
35786
- * - "soft-light" <br>
35946
+ * - "soft-light" : a diffused spotlight, gentler than hard-light <br>
35787
35947
  * <img src="../images/soft-light-blendmode.png" width="180"/> <br>
35788
- * - "difference" <br>
35948
+ * - "color-dodge" : brightens the backdrop to reflect the source <br>
35949
+ * <img src="../images/color-dodge-blendmode.png" width="180"/> <br>
35950
+ * - "color-burn" : darkens the backdrop to reflect the source <br>
35951
+ * <img src="../images/color-burn-blendmode.png" width="180"/> <br>
35952
+ * - "difference" : the absolute difference of the two layers <br>
35789
35953
  * <img src="../images/difference-blendmode.png" width="180"/> <br>
35790
- * - "exclusion" <br>
35954
+ * - "exclusion" : like difference, but lower in contrast <br>
35791
35955
  * <img src="../images/exclusion-blendmode.png" width="180"/> <br>
35956
+ * - "none" : blending disabled — the source replaces the destination
35957
+ * outright, alpha included <br>
35958
+ * A few draw types cannot honour every mode — 3D meshes, and fills using
35959
+ * a {@link Gradient} — and fall back to "normal" with a one-time console
35960
+ * warning. `setBlendMode` returns what it actually applied, so comparing
35961
+ * the result against your request detects that case.
35792
35962
  * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
35793
35963
  * @param {string} [mode="normal"] - blend mode
35794
35964
  * @returns {string} the blend mode actually applied (may differ if the requested mode is unsupported)
@@ -36661,11 +36831,11 @@ var CanvasRenderer = class extends Renderer {
36661
36831
  * Reset (overrides) the renderer transformation matrix to the
36662
36832
  * identity one, and then apply the given transformation matrix.
36663
36833
  * @param {Matrix2d|Matrix3d|number} a - a matrix to transform by, or the a component to multiply the current matrix by
36664
- * @param {number} b - the b component to multiply the current matrix by
36665
- * @param {number} c - the c component to multiply the current matrix by
36666
- * @param {number} d - the d component to multiply the current matrix by
36667
- * @param {number} e - the e component to multiply the current matrix by
36668
- * @param {number} f - the f component to multiply the current matrix by
36834
+ * @param {number} [b] - the b component to multiply the current matrix by; omitted when `a` is a matrix
36835
+ * @param {number} [c] - the c component to multiply the current matrix by; omitted when `a` is a matrix
36836
+ * @param {number} [d] - the d component to multiply the current matrix by; omitted when `a` is a matrix
36837
+ * @param {number} [e] - the e component to multiply the current matrix by; omitted when `a` is a matrix
36838
+ * @param {number} [f] - the f component to multiply the current matrix by; omitted when `a` is a matrix
36669
36839
  */
36670
36840
  setTransform(a, b, c, d, e, f) {
36671
36841
  this.resetTransform();
@@ -36675,11 +36845,11 @@ var CanvasRenderer = class extends Renderer {
36675
36845
  * Multiply given matrix into the renderer tranformation matrix
36676
36846
  * @see {@link CanvasRenderer.setTransform} which will reset the current transform matrix prior to performing the new transformation
36677
36847
  * @param {Matrix2d|Matrix3d|number} a - a matrix to transform by, or the a component to multiply the current matrix by
36678
- * @param {number} b - the b component to multiply the current matrix by
36679
- * @param {number} c - the c component to multiply the current matrix by
36680
- * @param {number} d - the d component to multiply the current matrix by
36681
- * @param {number} e - the e component to multiply the current matrix by
36682
- * @param {number} f - the f component to multiply the current matrix by
36848
+ * @param {number} [b] - the b component to multiply the current matrix by; omitted when `a` is a matrix
36849
+ * @param {number} [c] - the c component to multiply the current matrix by; omitted when `a` is a matrix
36850
+ * @param {number} [d] - the d component to multiply the current matrix by; omitted when `a` is a matrix
36851
+ * @param {number} [e] - the e component to multiply the current matrix by; omitted when `a` is a matrix
36852
+ * @param {number} [f] - the f component to multiply the current matrix by; omitted when `a` is a matrix
36683
36853
  */
36684
36854
  transform(a, b, c, d, e, f) {
36685
36855
  if (typeof a === "object") {
@@ -37725,7 +37895,7 @@ var WebGLBatcher = class extends Batcher {
37725
37895
  const gl = this.gl;
37726
37896
  this.vertexState.bind();
37727
37897
  gl.bindBuffer(gl.ARRAY_BUFFER, this.uploadBuffer);
37728
- if (this.renderer.currentProgram !== this.defaultShader.program) {
37898
+ if (this.currentShader !== this.defaultShader || this.renderer.currentProgram !== this.defaultShader.program) {
37729
37899
  this.useShader(this.defaultShader);
37730
37900
  }
37731
37901
  }
@@ -38985,6 +39155,222 @@ var QuadBatcher = class extends MaterialBatcher {
38985
39155
  }
38986
39156
  };
38987
39157
 
39158
+ // src/video/blendmodes.js
39159
+ var ADVANCED_BLEND_MODES = [
39160
+ "difference",
39161
+ "overlay",
39162
+ "hard-light",
39163
+ "color-dodge",
39164
+ "color-burn",
39165
+ "soft-light",
39166
+ // `darken` and `lighten` joined the shader path in 20.2: fixed-function
39167
+ // MIN/MAX compute `min(src, dst)` and nothing else, so there is nowhere to
39168
+ // put the `(1 - srcAlpha) * dst` term source-over contributes after the
39169
+ // blend. A translucent source lost the backdrop's share entirely — a white
39170
+ // `lighten` glow over a light backdrop rendered invisible. APPENDED, never
39171
+ // inserted: the index is the shader's branch value.
39172
+ "darken",
39173
+ "lighten"
39174
+ ];
39175
+ function isAdvancedBlendMode(mode) {
39176
+ return ADVANCED_BLEND_MODES.indexOf(mode) !== -1;
39177
+ }
39178
+ function advancedBlendModeIndex(mode) {
39179
+ return ADVANCED_BLEND_MODES.indexOf(mode);
39180
+ }
39181
+ function normalizeBlendMode(mode) {
39182
+ switch (mode) {
39183
+ case "none":
39184
+ case "multiply":
39185
+ case "screen":
39186
+ case "exclusion":
39187
+ case "normal":
39188
+ return mode;
39189
+ case "additive":
39190
+ case "add":
39191
+ case "lighter":
39192
+ return "additive";
39193
+ default:
39194
+ return "normal";
39195
+ }
39196
+ }
39197
+ function isSupportedBlendMode(mode) {
39198
+ return isAdvancedBlendMode(mode) || mode === "normal" || normalizeBlendMode(mode) !== "normal";
39199
+ }
39200
+ function reportedBlendMode(mode) {
39201
+ return isSupportedBlendMode(mode) ? mode : "normal";
39202
+ }
39203
+ function blendStateFor(mode, premultipliedAlpha) {
39204
+ const src = premultipliedAlpha ? "one" : "src-alpha";
39205
+ switch (mode) {
39206
+ case "none":
39207
+ return void 0;
39208
+ case "additive":
39209
+ return { operation: "add", srcFactor: src, dstFactor: "one" };
39210
+ case "multiply":
39211
+ return {
39212
+ operation: "add",
39213
+ srcFactor: "dst",
39214
+ dstFactor: "one-minus-src-alpha"
39215
+ };
39216
+ case "exclusion":
39217
+ return {
39218
+ operation: "add",
39219
+ srcFactor: "one-minus-dst",
39220
+ dstFactor: "one-minus-src"
39221
+ };
39222
+ case "screen":
39223
+ return {
39224
+ operation: "add",
39225
+ srcFactor: "one",
39226
+ dstFactor: "one-minus-src"
39227
+ };
39228
+ default:
39229
+ return {
39230
+ operation: "add",
39231
+ srcFactor: src,
39232
+ dstFactor: "one-minus-src-alpha"
39233
+ };
39234
+ }
39235
+ }
39236
+
39237
+ // src/video/effects/blendEffect.js
39238
+ var wgslFragment2 = `
39239
+ struct BlendUniforms {
39240
+ uBlendMode : f32,
39241
+ };
39242
+ @group(3) @binding(0) var<uniform> fx : BlendUniforms;
39243
+ @group(3) @binding(1) var backdrop : texture_2d<f32>;
39244
+ @group(3) @binding(2) var backdrop_sampler : sampler;
39245
+
39246
+ // HardLight(Cb, Cs) \u2014 also the kernel for Overlay, which the W3C spec
39247
+ // defines as HardLight with the arguments swapped
39248
+ fn ME_hardLight(b : vec3f, s : vec3f) -> vec3f {
39249
+ let lo = 2.0 * b * s;
39250
+ let hi = 1.0 - 2.0 * (1.0 - b) * (1.0 - s);
39251
+ return mix(lo, hi, step(vec3f(0.5), s));
39252
+ }
39253
+
39254
+ fn ME_softLight(b : vec3f, s : vec3f) -> vec3f {
39255
+ let d = mix(((16.0 * b - 12.0) * b + 4.0) * b, sqrt(b), step(vec3f(0.25), b));
39256
+ let lo = b - (1.0 - 2.0 * s) * b * (1.0 - b);
39257
+ let hi = b + (2.0 * s - 1.0) * (d - b);
39258
+ return mix(lo, hi, step(vec3f(0.5), s));
39259
+ }
39260
+
39261
+ // Cb == 0 wins over Cs == 1, so it is applied last
39262
+ fn ME_colorDodge(b : vec3f, s : vec3f) -> vec3f {
39263
+ var r = min(vec3f(1.0), b / max(1.0 - s, vec3f(1e-4)));
39264
+ r = mix(r, vec3f(1.0), step(vec3f(1.0), s));
39265
+ return mix(r, vec3f(0.0), step(b, vec3f(0.0)));
39266
+ }
39267
+
39268
+ // Cb == 1 wins over Cs == 0, so it is applied last
39269
+ fn ME_colorBurn(b : vec3f, s : vec3f) -> vec3f {
39270
+ var r = 1.0 - min(vec3f(1.0), (1.0 - b) / max(s, vec3f(1e-4)));
39271
+ r = mix(r, vec3f(0.0), step(s, vec3f(0.0)));
39272
+ return mix(r, vec3f(1.0), step(vec3f(1.0), b));
39273
+ }
39274
+
39275
+ fn ME_blend(b : vec3f, s : vec3f) -> vec3f {
39276
+ if (fx.uBlendMode < 0.5) { return abs(b - s); }
39277
+ if (fx.uBlendMode < 1.5) { return ME_hardLight(s, b); }
39278
+ if (fx.uBlendMode < 2.5) { return ME_hardLight(b, s); }
39279
+ if (fx.uBlendMode < 3.5) { return ME_colorDodge(b, s); }
39280
+ if (fx.uBlendMode < 4.5) { return ME_colorBurn(b, s); }
39281
+ if (fx.uBlendMode < 5.5) { return ME_softLight(b, s); }
39282
+ if (fx.uBlendMode < 6.5) { return min(b, s); }
39283
+ return max(b, s);
39284
+ }
39285
+
39286
+ fn apply(color : vec4f, uv : vec2f) -> vec4f {
39287
+ // sampled BEFORE any branch, so plain textureSampleLevel is safe here
39288
+ let Cb = textureSampleLevel(backdrop, backdrop_sampler, screen_uv, 0.0).rgb;
39289
+ let a = color.a;
39290
+ let Cs = color.rgb / max(a, 1e-4);
39291
+ let B = clamp(ME_blend(Cb, Cs), vec3f(0.0), vec3f(1.0));
39292
+ return vec4f(a * B, a);
39293
+ }
39294
+ `;
39295
+ var BlendEffect = class extends ShaderEffect {
39296
+ /**
39297
+ * @param {import("../webgl/webgl_renderer.js").default|import("../webgpu/webgpu_renderer.js").default} renderer - the renderer that owns this effect
39298
+ */
39299
+ constructor(renderer) {
39300
+ super(renderer, {
39301
+ glsl: `
39302
+ uniform sampler2D backdrop;
39303
+ uniform float uBlendMode;
39304
+
39305
+ // HardLight(Cb, Cs) \u2014 also the kernel for Overlay, which the W3C
39306
+ // spec defines as HardLight with the arguments swapped
39307
+ vec3 ME_hardLight(vec3 b, vec3 s) {
39308
+ vec3 lo = 2.0 * b * s;
39309
+ vec3 hi = 1.0 - 2.0 * (1.0 - b) * (1.0 - s);
39310
+ return mix(lo, hi, step(vec3(0.5), s));
39311
+ }
39312
+
39313
+ vec3 ME_softLight(vec3 b, vec3 s) {
39314
+ vec3 d = mix(((16.0 * b - 12.0) * b + 4.0) * b, sqrt(b), step(vec3(0.25), b));
39315
+ vec3 lo = b - (1.0 - 2.0 * s) * b * (1.0 - b);
39316
+ vec3 hi = b + (2.0 * s - 1.0) * (d - b);
39317
+ return mix(lo, hi, step(vec3(0.5), s));
39318
+ }
39319
+
39320
+ // Cb == 0 wins over Cs == 1, so it is applied last
39321
+ vec3 ME_colorDodge(vec3 b, vec3 s) {
39322
+ vec3 r = min(vec3(1.0), b / max(1.0 - s, vec3(1e-4)));
39323
+ r = mix(r, vec3(1.0), step(vec3(1.0), s));
39324
+ return mix(r, vec3(0.0), step(b, vec3(0.0)));
39325
+ }
39326
+
39327
+ // Cb == 1 wins over Cs == 0, so it is applied last
39328
+ vec3 ME_colorBurn(vec3 b, vec3 s) {
39329
+ vec3 r = 1.0 - min(vec3(1.0), (1.0 - b) / max(s, vec3(1e-4)));
39330
+ r = mix(r, vec3(0.0), step(s, vec3(0.0)));
39331
+ return mix(r, vec3(1.0), step(vec3(1.0), b));
39332
+ }
39333
+
39334
+ vec3 ME_blend(vec3 b, vec3 s) {
39335
+ if (uBlendMode < 0.5) { return abs(b - s); }
39336
+ if (uBlendMode < 1.5) { return ME_hardLight(s, b); }
39337
+ if (uBlendMode < 2.5) { return ME_hardLight(b, s); }
39338
+ if (uBlendMode < 3.5) { return ME_colorDodge(b, s); }
39339
+ if (uBlendMode < 4.5) { return ME_colorBurn(b, s); }
39340
+ if (uBlendMode < 5.5) { return ME_softLight(b, s); }
39341
+ if (uBlendMode < 6.5) { return min(b, s); }
39342
+ return max(b, s);
39343
+ }
39344
+
39345
+ vec4 apply(vec4 color, vec2 uv) {
39346
+ vec3 Cb = texture2D(backdrop, screen_uv).rgb;
39347
+ float a = color.a;
39348
+ vec3 Cs = color.rgb / max(a, 1e-4);
39349
+ vec3 B = clamp(ME_blend(Cb, Cs), 0.0, 1.0);
39350
+ return vec4(a * B, a);
39351
+ }
39352
+ `,
39353
+ wgsl: wgslFragment2
39354
+ });
39355
+ this.setUniform("uBlendMode", 0);
39356
+ }
39357
+ /**
39358
+ * Select which of the six formulas the shader evaluates.
39359
+ * @param {string} mode - one of the modes in {@link ADVANCED_BLEND_MODES}
39360
+ * @returns {boolean} false when the mode is not an advanced blend mode, in
39361
+ * which case the uniform is left untouched
39362
+ * @ignore
39363
+ */
39364
+ setBlendMode(mode) {
39365
+ const index = advancedBlendModeIndex(mode);
39366
+ if (index === -1) {
39367
+ return false;
39368
+ }
39369
+ this.setUniform("uBlendMode", index);
39370
+ return true;
39371
+ }
39372
+ };
39373
+
38988
39374
  // src/video/effects/radialGradient.js
38989
39375
  var RadialGradientEffect = class extends ShaderEffect {
38990
39376
  /**
@@ -42690,7 +43076,30 @@ var GLSamplerCache = class {
42690
43076
  var _tempMatrix = new Matrix3d();
42691
43077
  var _savedTransform = new Matrix3d();
42692
43078
  var _savedProjection = new Matrix3d();
42693
- var WebGLRenderer = class extends Renderer {
43079
+ var GL_BLEND_OP;
43080
+ var GL_BLEND_FACTOR;
43081
+ function initBlendEnums(gl) {
43082
+ GL_BLEND_OP = {
43083
+ add: gl.FUNC_ADD,
43084
+ subtract: gl.FUNC_SUBTRACT,
43085
+ "reverse-subtract": gl.FUNC_REVERSE_SUBTRACT,
43086
+ min: gl.MIN,
43087
+ max: gl.MAX
43088
+ };
43089
+ GL_BLEND_FACTOR = {
43090
+ zero: gl.ZERO,
43091
+ one: gl.ONE,
43092
+ src: gl.SRC_COLOR,
43093
+ "one-minus-src": gl.ONE_MINUS_SRC_COLOR,
43094
+ "src-alpha": gl.SRC_ALPHA,
43095
+ "one-minus-src-alpha": gl.ONE_MINUS_SRC_ALPHA,
43096
+ dst: gl.DST_COLOR,
43097
+ "one-minus-dst": gl.ONE_MINUS_DST_COLOR,
43098
+ "dst-alpha": gl.DST_ALPHA,
43099
+ "one-minus-dst-alpha": gl.ONE_MINUS_DST_ALPHA
43100
+ };
43101
+ }
43102
+ var WebGLRenderer = class _WebGLRenderer extends Renderer {
42694
43103
  /**
42695
43104
  * @param {ApplicationSettings} [options] - optional parameters for the renderer
42696
43105
  */
@@ -42707,6 +43116,14 @@ var WebGLRenderer = class extends Renderer {
42707
43116
  });
42708
43117
  this._effectProjectionStack = [];
42709
43118
  this._effectPassDepth = 0;
43119
+ this._advancedBlendEffect = void 0;
43120
+ this._advancedBlendCapture = void 0;
43121
+ this._advancedBlendTarget = void 0;
43122
+ this._advancedBlendOpen = false;
43123
+ this._advancedBlendBusy = 0;
43124
+ this._advancedBlendParent = null;
43125
+ this._advancedBlendStencil = false;
43126
+ this._advancedBlendWarned = /* @__PURE__ */ new Set();
42710
43127
  this.lineWidth = 1;
42711
43128
  this.lineJoin = "round";
42712
43129
  this.vertexBuffer = this.gl.createBuffer();
@@ -42885,6 +43302,14 @@ var WebGLRenderer = class extends Renderer {
42885
43302
  off(GAME_RESET, this.onGameReset);
42886
43303
  off(ONCONTEXT_RESTORED, this.onContextRestoredInvalidate);
42887
43304
  off(CANVAS_ONRESIZE, this.onCanvasResize);
43305
+ this._advancedBlendEffect?.destroy();
43306
+ this._advancedBlendEffect = void 0;
43307
+ this._advancedBlendCapture?.destroy();
43308
+ this._advancedBlendCapture = void 0;
43309
+ this._advancedBlendTarget?.destroy?.();
43310
+ this._advancedBlendTarget = void 0;
43311
+ this._advancedBlendOpen = false;
43312
+ this._advancedBlendBusy = 0;
42888
43313
  releaseShadowQuads(this);
42889
43314
  if (this.batchers) {
42890
43315
  this.batchers.forEach((batcher) => {
@@ -43013,6 +43438,18 @@ var WebGLRenderer = class extends Renderer {
43013
43438
  if (typeof batcher === "undefined") {
43014
43439
  throw new Error("Invalid Batcher");
43015
43440
  }
43441
+ if (this._advancedBlendBusy === 0) {
43442
+ if (this._advancedBlendOpen === true) {
43443
+ this._closeAdvancedBlend();
43444
+ }
43445
+ if (isAdvancedBlendMode(this.currentBlendMode) === true) {
43446
+ if (name === "mesh" || name === "litMesh") {
43447
+ this._warnAdvancedBlendFallback("3D meshes");
43448
+ } else {
43449
+ this._openAdvancedBlend();
43450
+ }
43451
+ }
43452
+ }
43016
43453
  const targetShader = shader != null ? shader : batcher.defaultShader;
43017
43454
  if (this.currentBatcher === batcher && batcher.currentShader === targetShader) {
43018
43455
  return this.currentBatcher;
@@ -43082,6 +43519,10 @@ var WebGLRenderer = class extends Renderer {
43082
43519
  * Flush the batcher to the frame buffer
43083
43520
  */
43084
43521
  flush() {
43522
+ if (this._advancedBlendOpen === true && this._advancedBlendBusy === 0) {
43523
+ this._closeAdvancedBlend();
43524
+ return;
43525
+ }
43085
43526
  this.currentBatcher.flush();
43086
43527
  }
43087
43528
  /**
@@ -43257,6 +43698,16 @@ var WebGLRenderer = class extends Renderer {
43257
43698
  * }
43258
43699
  */
43259
43700
  toFrameTexture(options = {}) {
43701
+ const busy = this._advancedBlendBusy;
43702
+ this._advancedBlendBusy = (busy ?? 0) + 1;
43703
+ try {
43704
+ return _WebGLRenderer.prototype._toFrameTexture.call(this, options);
43705
+ } finally {
43706
+ this._advancedBlendBusy = busy;
43707
+ }
43708
+ }
43709
+ /** @ignore */
43710
+ _toFrameTexture(options = {}) {
43260
43711
  const gl = this.gl;
43261
43712
  const canvas = this.getCanvas();
43262
43713
  let x = 0;
@@ -43492,6 +43943,125 @@ var WebGLRenderer = class extends Renderer {
43492
43943
  );
43493
43944
  this.currentBatcher.setProjection(this.projectionMatrix);
43494
43945
  }
43946
+ /**
43947
+ * Say once, per excluded path and mode, that an advanced blend mode is
43948
+ * falling back to `"normal"`. A silent data-dependent fallback is exactly
43949
+ * what the advanced-blend work exists to remove, so the exclusions are
43950
+ * loud.
43951
+ * @param {string} what - the excluded path, named for the message
43952
+ * @ignore
43953
+ */
43954
+ _warnAdvancedBlendFallback(what) {
43955
+ const key = `${what}:${this.currentBlendMode}`;
43956
+ if (this._advancedBlendWarned.has(key)) {
43957
+ return;
43958
+ }
43959
+ this._advancedBlendWarned.add(key);
43960
+ console.warn(
43961
+ `WebGLRenderer: blend mode "${this.currentBlendMode}" is not supported for ${what} and falls back to "normal"`
43962
+ );
43963
+ }
43964
+ /**
43965
+ * Close a pending advanced-blend bracket, if one is open and we are not
43966
+ * already inside the bracket machinery. The safe form to call from any
43967
+ * path that is about to disturb the destination.
43968
+ * @ignore
43969
+ */
43970
+ _drainAdvancedBlend() {
43971
+ if (this._advancedBlendOpen === true && this._advancedBlendBusy === 0) {
43972
+ this._closeAdvancedBlend();
43973
+ }
43974
+ }
43975
+ /**
43976
+ * Open an advanced-blend bracket: capture the destination, then redirect
43977
+ * this draw into an offscreen target. Closed by {@link _closeAdvancedBlend}
43978
+ * at the next draw, flush or clear.
43979
+ * @returns {boolean} false when the effect could not be realized, in which
43980
+ * case the draw proceeds unbracketed (plain source-over)
43981
+ * @ignore
43982
+ */
43983
+ _openAdvancedBlend() {
43984
+ const gl = this.gl;
43985
+ const canvas = this.getCanvas();
43986
+ const w = canvas.width;
43987
+ const h = canvas.height;
43988
+ this._advancedBlendBusy++;
43989
+ if (typeof this._advancedBlendEffect === "undefined") {
43990
+ this._advancedBlendEffect = new BlendEffect(this);
43991
+ }
43992
+ if (this._advancedBlendEffect.enabled !== true) {
43993
+ this._advancedBlendBusy--;
43994
+ this._warnAdvancedBlendFallback("this device");
43995
+ return false;
43996
+ }
43997
+ this._advancedBlendEffect.setBlendMode(this.currentBlendMode);
43998
+ this.currentBatcher.flush();
43999
+ this._advancedBlendCapture = this.toFrameTexture({
44000
+ target: this._advancedBlendCapture ?? null
44001
+ });
44002
+ this._advancedBlendEffect.setTexture(
44003
+ "backdrop",
44004
+ this._advancedBlendCapture
44005
+ );
44006
+ this._advancedBlendParent = this._effectPassDepth > 0 ? this._renderTargetPool.getCaptureTarget() : null;
44007
+ if (typeof this._advancedBlendTarget === "undefined") {
44008
+ this._advancedBlendTarget = new WebGLRenderTarget(gl, w, h, {
44009
+ samples: 0
44010
+ });
44011
+ } else {
44012
+ this._advancedBlendTarget.resize(w, h);
44013
+ }
44014
+ this.invalidateTextureUnit(0);
44015
+ this._advancedBlendTarget.bind();
44016
+ this.setViewport(0, 0, w, h);
44017
+ this.clearRenderTarget();
44018
+ this._advancedBlendStencil = gl.isEnabled(gl.STENCIL_TEST);
44019
+ if (this._advancedBlendStencil === true) {
44020
+ gl.disable(gl.STENCIL_TEST);
44021
+ }
44022
+ this._advancedBlendOpen = true;
44023
+ this._advancedBlendBusy--;
44024
+ return true;
44025
+ }
44026
+ /**
44027
+ * Close an advanced-blend bracket: drain the offscreen, return to the
44028
+ * parent target, and composite through {@link BlendEffect}.
44029
+ * @ignore
44030
+ */
44031
+ _closeAdvancedBlend() {
44032
+ const gl = this.gl;
44033
+ const canvas = this.getCanvas();
44034
+ const w = canvas.width;
44035
+ const h = canvas.height;
44036
+ this._advancedBlendBusy++;
44037
+ this._advancedBlendOpen = false;
44038
+ this.currentBatcher.flush();
44039
+ this._advancedBlendTarget.unbind();
44040
+ if (this._advancedBlendParent !== null) {
44041
+ this._advancedBlendParent.bind();
44042
+ }
44043
+ this._advancedBlendParent = null;
44044
+ this.invalidateTextureUnit(0);
44045
+ this.setViewport(0, 0, w, h);
44046
+ emit(RENDER_TARGET_CHANGED, this);
44047
+ if (this._advancedBlendStencil === true) {
44048
+ gl.enable(gl.STENCIL_TEST);
44049
+ }
44050
+ const mode = this.currentBlendMode;
44051
+ this.setBlendMode("normal", true);
44052
+ this.blitEffect(
44053
+ this._advancedBlendTarget.texture,
44054
+ 0,
44055
+ 0,
44056
+ w,
44057
+ h,
44058
+ this._advancedBlendEffect,
44059
+ true
44060
+ );
44061
+ this.currentBatcher.flush();
44062
+ this.currentBlendMode = mode;
44063
+ this._advancedBlendBusy--;
44064
+ }
43495
44065
  /**
43496
44066
  * Blit a texture to the screen through a shader effect.
43497
44067
  * Draws a screen-aligned quad using the given texture as source
@@ -43505,6 +44075,7 @@ var WebGLRenderer = class extends Renderer {
43505
44075
  * @param {boolean} [keepBlend=false] - if true, keep current blend mode (for sprite compositing)
43506
44076
  */
43507
44077
  blitEffect(source, x, y, width, height, shader, keepBlend = false) {
44078
+ this._advancedBlendBusy++;
43508
44079
  this.flush();
43509
44080
  const batcher = this.setBatcher("quad");
43510
44081
  _savedTransform.copy(this.currentTransform);
@@ -43522,6 +44093,7 @@ var WebGLRenderer = class extends Renderer {
43522
44093
  this.currentTransform.copy(_savedTransform);
43523
44094
  this.projectionMatrix.copy(_savedProjection);
43524
44095
  batcher.setProjection(this.projectionMatrix);
44096
+ this._advancedBlendBusy--;
43525
44097
  }
43526
44098
  /**
43527
44099
  * set/change the current projection matrix (WebGL only)
@@ -43608,6 +44180,7 @@ var WebGLRenderer = class extends Renderer {
43608
44180
  * Clear the frame buffer
43609
44181
  */
43610
44182
  clear() {
44183
+ this._drainAdvancedBlend();
43611
44184
  const gl = this.gl;
43612
44185
  const clearColor = this.backgroundColor.toArray();
43613
44186
  gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]);
@@ -43621,6 +44194,7 @@ var WebGLRenderer = class extends Renderer {
43621
44194
  * @param {boolean} [opaque=false] - Allow transparency [default] or clear the surface completely [true]
43622
44195
  */
43623
44196
  clearColor(color = "#000000", opaque = false) {
44197
+ this._drainAdvancedBlend();
43624
44198
  let glArray;
43625
44199
  const gl = this.gl;
43626
44200
  if (color instanceof Color) {
@@ -43985,12 +44559,31 @@ var WebGLRenderer = class extends Renderer {
43985
44559
  * return a reference to the system 2d Context
43986
44560
  * @returns {WebGLRenderingContext} the current WebGL context
43987
44561
  */
44562
+ /**
44563
+ * The shader program the GL context actually has bound.
44564
+ *
44565
+ * Backed by the CONTEXT rather than by a field on this renderer, because
44566
+ * `GLShader` binds programs too — writing a uniform calls `useProgram`,
44567
+ * and a freshly linked program is left bound — and it is constructed with
44568
+ * a bare `gl`, so it cannot reach a renderer-owned cache. A field here
44569
+ * would silently drift out of step with GL, and every batcher that
44570
+ * compares against it (`syncProgram`, the mesh and primitive paths) would
44571
+ * skip a rebind it actually needed, drawing a pending batch through the
44572
+ * wrong program.
44573
+ * @type {WebGLProgram|undefined}
44574
+ */
44575
+ get currentProgram() {
44576
+ return this.gl.__meCurrentProgram;
44577
+ }
44578
+ set currentProgram(value) {
44579
+ this.gl.__meCurrentProgram = value;
44580
+ }
43988
44581
  getContext() {
43989
44582
  return this.gl;
43990
44583
  }
43991
44584
  /**
43992
44585
  * set the current blend mode for this renderer. <br>
43993
- * All renderers support: <br>
44586
+ * Every renderer supports the full set: <br>
43994
44587
  * - "normal" : draws new content on top of the existing content <br>
43995
44588
  * <img src="../images/normal-blendmode.png" width="180"/> <br>
43996
44589
  * - "add", "additive", or "lighter" : color values are added together <br>
@@ -43999,71 +44592,58 @@ var WebGLRenderer = class extends Renderer {
43999
44592
  * <img src="../images/multiply-blendmode.png" width="180"/> <br>
44000
44593
  * - "screen" : pixels are inverted, multiplied, and inverted again (opposite of multiply) <br>
44001
44594
  * <img src="../images/screen-blendmode.png" width="180"/> <br>
44002
- * WebGL2 additionally supports: <br>
44003
44595
  * - "darken" : retains the darkest pixels of both layers <br>
44004
44596
  * <img src="../images/darken-blendmode.png" width="180"/> <br>
44005
44597
  * - "lighten" : retains the lightest pixels of both layers <br>
44006
44598
  * <img src="../images/lighten-blendmode.png" width="180"/> <br>
44599
+ * - "overlay" : multiplies or screens, depending on the backdrop <br>
44600
+ * <img src="../images/overlay-blendmode.png" width="180"/> <br>
44601
+ * - "hard-light" : overlay with the layers swapped — a harsh spotlight <br>
44602
+ * <img src="../images/hard-light-blendmode.png" width="180"/> <br>
44603
+ * - "soft-light" : a diffused spotlight, gentler than hard-light <br>
44604
+ * <img src="../images/soft-light-blendmode.png" width="180"/> <br>
44605
+ * - "color-dodge" : brightens the backdrop to reflect the source <br>
44606
+ * <img src="../images/color-dodge-blendmode.png" width="180"/> <br>
44607
+ * - "color-burn" : darkens the backdrop to reflect the source <br>
44608
+ * <img src="../images/color-burn-blendmode.png" width="180"/> <br>
44609
+ * - "difference" : the absolute difference of the two layers <br>
44610
+ * <img src="../images/difference-blendmode.png" width="180"/> <br>
44611
+ * - "exclusion" : like difference, but lower in contrast <br>
44612
+ * <img src="../images/exclusion-blendmode.png" width="180"/> <br>
44007
44613
  * - "none" : blending disabled — the source replaces the destination
44008
- * outright, alpha included (matches the WebGPU renderer's "none") <br>
44009
- * Other CSS blend modes ("overlay", "color-dodge", "color-burn", "hard-light", "soft-light",
44010
- * "difference", "exclusion") may be supported by the Canvas renderer (browser-dependent)
44011
- * and will always fall back to "normal" in WebGL. <br>
44614
+ * outright, alpha included <br>
44615
+ * A few draw types cannot honour every mode — 3D meshes, and fills using
44616
+ * a {@link Gradient} — and fall back to "normal" with a one-time console
44617
+ * warning. `setBlendMode` returns what it actually applied, so comparing
44618
+ * the result against your request detects that case.
44012
44619
  * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
44013
44620
  * @param {string} [mode="normal"] - blend mode
44014
- * @param {boolean} [premultipliedAlpha=true] - whether textures use premultiplied alpha (affects source blend factor)
44621
+ * @param {boolean} [premultipliedAlpha=true] - whether textures use premultiplied alpha (affects the source blend factor)
44015
44622
  * @returns {string} the blend mode actually applied (may differ if the requested mode is unsupported)
44016
44623
  */
44017
44624
  setBlendMode(mode = "normal", premultipliedAlpha = true) {
44018
44625
  if (this.currentBlendMode !== mode || this.currentPremultipliedAlpha !== premultipliedAlpha) {
44019
44626
  const gl = this.gl;
44627
+ this._drainAdvancedBlend();
44020
44628
  this.flush();
44021
- gl.enable(gl.BLEND);
44022
44629
  this.currentBlendMode = mode;
44023
44630
  this.currentPremultipliedAlpha = premultipliedAlpha;
44024
- const srcAlpha = premultipliedAlpha ? gl.ONE : gl.SRC_ALPHA;
44025
- switch (mode) {
44026
- case "screen":
44027
- gl.blendEquation(gl.FUNC_ADD);
44028
- gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_COLOR);
44029
- break;
44030
- // exclusion(a, b) = a + b - 2ab. Unlike the other CSS advanced
44031
- // modes this needs no per-pixel branch — it falls straight out
44032
- // of `src * sfactor + dst * dfactor`:
44033
- // s*(1-d) + d*(1-s) = s + d - 2sd
44034
- // Same caveat as `screen` above: the identity is exact for an
44035
- // opaque source and approximate for a translucent one, because
44036
- // the factors act on the premultiplied colour.
44037
- case "exclusion":
44038
- gl.blendEquation(gl.FUNC_ADD);
44039
- gl.blendFunc(gl.ONE_MINUS_DST_COLOR, gl.ONE_MINUS_SRC_COLOR);
44040
- break;
44041
- case "lighter":
44042
- case "additive":
44043
- case "add":
44044
- gl.blendEquation(gl.FUNC_ADD);
44045
- gl.blendFunc(srcAlpha, gl.ONE);
44046
- break;
44047
- case "multiply":
44048
- gl.blendEquation(gl.FUNC_ADD);
44049
- gl.blendFunc(gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA);
44050
- break;
44051
- case "darken":
44052
- gl.blendEquation(gl.MIN);
44053
- gl.blendFunc(gl.ONE, gl.ONE);
44054
- break;
44055
- case "lighten":
44056
- gl.blendEquation(gl.MAX);
44057
- gl.blendFunc(gl.ONE, gl.ONE);
44058
- break;
44059
- case "none":
44060
- gl.disable(gl.BLEND);
44061
- break;
44062
- default:
44063
- gl.blendEquation(gl.FUNC_ADD);
44064
- gl.blendFunc(srcAlpha, gl.ONE_MINUS_SRC_ALPHA);
44065
- this.currentBlendMode = "normal";
44066
- break;
44631
+ if (typeof GL_BLEND_OP === "undefined") {
44632
+ initBlendEnums(gl);
44633
+ }
44634
+ const state3 = blendStateFor(normalizeBlendMode(mode), premultipliedAlpha);
44635
+ if (typeof state3 === "undefined") {
44636
+ gl.disable(gl.BLEND);
44637
+ } else {
44638
+ gl.enable(gl.BLEND);
44639
+ gl.blendEquation(GL_BLEND_OP[state3.operation]);
44640
+ gl.blendFunc(
44641
+ GL_BLEND_FACTOR[state3.srcFactor],
44642
+ GL_BLEND_FACTOR[state3.dstFactor]
44643
+ );
44644
+ }
44645
+ if (isSupportedBlendMode(mode) === false) {
44646
+ this.currentBlendMode = "normal";
44067
44647
  }
44068
44648
  }
44069
44649
  return this.currentBlendMode;
@@ -44704,6 +45284,12 @@ var WebGLRenderer = class extends Renderer {
44704
45284
  const grad = this._currentGradient;
44705
45285
  const hasMask = this.maskLevel > 0;
44706
45286
  this._currentGradient = null;
45287
+ const bracketed = isAdvancedBlendMode(this.currentBlendMode);
45288
+ if (bracketed === true) {
45289
+ this._warnAdvancedBlendFallback("gradient fills");
45290
+ this._drainAdvancedBlend();
45291
+ }
45292
+ this._advancedBlendBusy++;
44707
45293
  this.flush();
44708
45294
  gl.enable(gl.STENCIL_TEST);
44709
45295
  gl.colorMask(false, false, false, false);
@@ -44740,16 +45326,17 @@ var WebGLRenderer = class extends Renderer {
44740
45326
  } else {
44741
45327
  gl.disable(gl.STENCIL_TEST);
44742
45328
  }
45329
+ this._advancedBlendBusy--;
44743
45330
  }
44744
45331
  /**
44745
45332
  * Reset (overrides) the renderer transformation matrix to the
44746
45333
  * identity one, and then apply the given transformation matrix.
44747
45334
  * @param {Matrix2d|Matrix3d|number} a - a matrix to transform by, or the a component to multiply the current matrix by
44748
- * @param {number} b - the b component to multiply the current matrix by
44749
- * @param {number} c - the c component to multiply the current matrix by
44750
- * @param {number} d - the d component to multiply the current matrix by
44751
- * @param {number} e - the e component to multiply the current matrix by
44752
- * @param {number} f - the f component to multiply the current matrix by
45335
+ * @param {number} [b] - the b component to multiply the current matrix by; omitted when `a` is a matrix
45336
+ * @param {number} [c] - the c component to multiply the current matrix by; omitted when `a` is a matrix
45337
+ * @param {number} [d] - the d component to multiply the current matrix by; omitted when `a` is a matrix
45338
+ * @param {number} [e] - the e component to multiply the current matrix by; omitted when `a` is a matrix
45339
+ * @param {number} [f] - the f component to multiply the current matrix by; omitted when `a` is a matrix
44753
45340
  */
44754
45341
  setTransform(a, b, c, d, e, f) {
44755
45342
  this.resetTransform();
@@ -44759,11 +45346,11 @@ var WebGLRenderer = class extends Renderer {
44759
45346
  * Multiply given matrix into the renderer tranformation matrix
44760
45347
  * @see {@link WebGLRenderer.setTransform} which will reset the current transform matrix prior to performing the new transformation
44761
45348
  * @param {Matrix2d|Matrix3d|number} a - a matrix to transform by, or the a component to multiply the current matrix by
44762
- * @param {number} b - the b component to multiply the current matrix by
44763
- * @param {number} c - the c component to multiply the current matrix by
44764
- * @param {number} d - the d component to multiply the current matrix by
44765
- * @param {number} e - the e component to multiply the current matrix by
44766
- * @param {number} f - the f component to multiply the current matrix by
45349
+ * @param {number} [b] - the b component to multiply the current matrix by; omitted when `a` is a matrix
45350
+ * @param {number} [c] - the c component to multiply the current matrix by; omitted when `a` is a matrix
45351
+ * @param {number} [d] - the d component to multiply the current matrix by; omitted when `a` is a matrix
45352
+ * @param {number} [e] - the e component to multiply the current matrix by; omitted when `a` is a matrix
45353
+ * @param {number} [f] - the f component to multiply the current matrix by; omitted when `a` is a matrix
44767
45354
  */
44768
45355
  transform(a, b, c, d, e, f) {
44769
45356
  if (typeof a === "object") {
@@ -45752,6 +46339,9 @@ function dispatchEvent(normalizedEvents2) {
45752
46339
  }
45753
46340
  lastTimeStamp = pointer2.event.timeStamp;
45754
46341
  }
46342
+ const worldOffset = _app2.world.pos;
46343
+ const absoluteWorldX = pointer2.gameWorldX + worldOffset.x;
46344
+ const absoluteWorldY = pointer2.gameWorldY + worldOffset.y;
45755
46345
  currentPointer.pos.set(pointer2.gameWorldX, pointer2.gameWorldY);
45756
46346
  currentPointer.setSize(pointer2.width, pointer2.height);
45757
46347
  if (POINTER_MOVE.includes(pointer2.type)) {
@@ -45764,6 +46354,22 @@ function dispatchEvent(normalizedEvents2) {
45764
46354
  (a, b) => _app2.world._sortReverseZ(a, b),
45765
46355
  void 0
45766
46356
  );
46357
+ if (worldOffset.x !== 0 || worldOffset.y !== 0) {
46358
+ currentPointer.pos.set(absoluteWorldX, absoluteWorldY);
46359
+ const absoluteCandidates = _app2.world.broadphase.retrieve(
46360
+ currentPointer,
46361
+ (a, b) => _app2.world._sortReverseZ(a, b),
46362
+ void 0
46363
+ );
46364
+ for (const candidate of candidates) {
46365
+ if (candidate.isFloating === true && !absoluteCandidates.includes(candidate)) {
46366
+ absoluteCandidates.push(candidate);
46367
+ }
46368
+ }
46369
+ candidates = absoluteCandidates.sort(
46370
+ (a, b) => _app2.world._sortReverseZ(a, b)
46371
+ );
46372
+ }
45767
46373
  candidates = candidates.concat([_app2.viewport]);
45768
46374
  for (let c = candidates.length, candidate; c--, candidate = candidates[c]; ) {
45769
46375
  if (eventHandlers.has(candidate) && candidate.isKinematic !== true) {
@@ -45780,10 +46386,15 @@ function dispatchEvent(normalizedEvents2) {
45780
46386
  }
45781
46387
  if (typeof ancestor !== "undefined") {
45782
46388
  const parentBounds = ancestor.getBounds();
45783
- pointer2.gameLocalX = pointer2.gameX - parentBounds.x;
45784
- pointer2.gameLocalY = pointer2.gameY - parentBounds.y;
46389
+ if (region.isFloating === true) {
46390
+ pointer2.gameLocalX = pointer2.gameX - parentBounds.x;
46391
+ pointer2.gameLocalY = pointer2.gameY - parentBounds.y;
46392
+ } else {
46393
+ pointer2.gameLocalX = absoluteWorldX - parentBounds.x;
46394
+ pointer2.gameLocalY = absoluteWorldY - parentBounds.y;
46395
+ }
45785
46396
  }
45786
- const eventInBounds = bounds.contains(pointer2.gameX, pointer2.gameY);
46397
+ const eventInBounds = region.isFloating === true ? bounds.contains(pointer2.gameX, pointer2.gameY) : bounds.contains(absoluteWorldX, absoluteWorldY);
45787
46398
  switch (pointer2.type) {
45788
46399
  case POINTER_MOVE[0]:
45789
46400
  case POINTER_MOVE[1]:
@@ -49468,6 +50079,7 @@ var Light2d = class extends Renderable {
49468
50079
  };
49469
50080
 
49470
50081
  // src/particles/particle.ts
50082
+ var _spawn = new Vector2d();
49471
50083
  var Particle = class extends Renderable {
49472
50084
  vel;
49473
50085
  image;
@@ -49479,7 +50091,6 @@ var Particle = class extends Renderable {
49479
50091
  wind;
49480
50092
  followTrajectory;
49481
50093
  onlyInViewport;
49482
- accurateBounds;
49483
50094
  _deltaInv;
49484
50095
  _halfW;
49485
50096
  _halfH;
@@ -49499,13 +50110,40 @@ var Particle = class extends Renderable {
49499
50110
  this.vel = vector2dPool.get();
49500
50111
  this.onResetEvent(emitter, true);
49501
50112
  }
50113
+ /**
50114
+ * Whether the bounds need recomputing before anyone reads them.
50115
+ *
50116
+ * A `Renderable` refreshes its bounds eagerly, from a callback fired on
50117
+ * every `pos` assignment. That is the right trade for a scene object, and
50118
+ * the wrong one for a particle: `update()` writes `pos.x` and `pos.y`
50119
+ * separately, so the callback fires TWICE per particle per frame, and both
50120
+ * runs happen before `currentTransform` is rebuilt — deriving bounds from
50121
+ * the previous frame's matrix and then throwing that away. Measured, the
50122
+ * redundant pass was about two thirds of the whole particle update loop.
50123
+ *
50124
+ * So a particle invalidates instead of recomputing, and pays once, on
50125
+ * read, for particles something actually looks at.
50126
+ *
50127
+ * Deliberately not a `#private` field: `updateBounds()` is reached from the
50128
+ * base constructor chain (`Polygon.setVertices`) before a subclass's field
50129
+ * initializers have run, and writing an undeclared private field throws.
50130
+ * @ignore
50131
+ */
50132
+ _boundsDirty = true;
49502
50133
  /**
49503
50134
  * @ignore
49504
50135
  */
49505
50136
  onResetEvent(emitter, newInstance = false) {
49506
50137
  const image = emitter.settings.image;
49507
- if (!newInstance) {
50138
+ const map = emitter._spawnMap;
50139
+ if (typeof map !== "undefined") {
50140
+ _spawn.set(emitter.getRandomPointX(), emitter.getRandomPointY());
50141
+ map.apply(_spawn);
50142
+ this.pos.set(_spawn.x, _spawn.y);
50143
+ } else {
49508
50144
  this.pos.set(emitter.getRandomPointX(), emitter.getRandomPointY());
50145
+ }
50146
+ if (!newInstance) {
49509
50147
  this.resize(image.width, image.height);
49510
50148
  this.currentTransform.identity();
49511
50149
  }
@@ -49513,7 +50151,13 @@ var Particle = class extends Renderable {
49513
50151
  this._halfW = this.width / 2;
49514
50152
  this._halfH = this.height / 2;
49515
50153
  this.alwaysUpdate = true;
50154
+ this.pos.setCallback(() => {
50155
+ this._boundsDirty = true;
50156
+ this.isDirty = true;
50157
+ });
50158
+ this._boundsDirty = true;
49516
50159
  this.anchorPoint.set(0, 0);
50160
+ this.autoTransform = false;
49517
50161
  if (typeof emitter.settings.tint === "string") {
49518
50162
  this.tint.parseCSS(emitter.settings.tint);
49519
50163
  }
@@ -49538,7 +50182,6 @@ var Particle = class extends Renderable {
49538
50182
  this.wind = emitter.settings.wind;
49539
50183
  this.followTrajectory = emitter.settings.followTrajectory;
49540
50184
  this.onlyInViewport = emitter.settings.onlyInViewport;
49541
- this.accurateBounds = emitter.settings.accurateBounds;
49542
50185
  this._deltaInv = emitter._deltaInv;
49543
50186
  if (!emitter.settings.followTrajectory) {
49544
50187
  this._angle = randomFloat(
@@ -49603,12 +50246,108 @@ var Particle = class extends Renderable {
49603
50246
  0,
49604
50247
  1
49605
50248
  );
49606
- if (this.accurateBounds) {
49607
- this.updateBounds();
49608
- }
49609
50249
  this.isDirty = this.inViewport || !this.onlyInViewport;
49610
50250
  return super.update(dt);
49611
50251
  }
50252
+ /**
50253
+ * `autoTransform` is off (see `onResetEvent`), so the base `preDraw` will
50254
+ * not apply the matrix — append it here instead, unconjugated. Appending
50255
+ * after `super` rather than splicing into it is order-equivalent for a
50256
+ * particle specifically: no flip, no mask, and the anchor is zeroed, so
50257
+ * nothing the base method emits interacts with this.
50258
+ * @ignore
50259
+ */
50260
+ preDraw(renderer) {
50261
+ super.preDraw(renderer);
50262
+ if (!this.currentTransform.isIdentity()) {
50263
+ renderer.transform(this.currentTransform);
50264
+ }
50265
+ }
50266
+ /**
50267
+ * `currentTransform` already places the particle, so the frame it
50268
+ * produces is positioned — only the ancestors' contribution is still
50269
+ * missing. The base implementation would add this particle's own `pos` on
50270
+ * top of a matrix that already contains it, counting it twice.
50271
+ * @ignore
50272
+ */
50273
+ /**
50274
+ * Bounds are recomputed here rather than when `pos` moves, so a particle
50275
+ * nothing looks at this frame pays nothing at all.
50276
+ *
50277
+ * `updateBounds()` deliberately keeps its eager contract — a caller that
50278
+ * asks for a recompute, such as {@link Container} aggregating child bounds
50279
+ * under `enableChildBoundsUpdate`, still gets fresh values back.
50280
+ * @ignore
50281
+ */
50282
+ getBounds() {
50283
+ const bounds = super.getBounds();
50284
+ if (this._boundsDirty) {
50285
+ this.updateBounds();
50286
+ }
50287
+ return bounds;
50288
+ }
50289
+ updateBounds(absolute = true) {
50290
+ this._boundsDirty = false;
50291
+ if (!this.isRenderable) {
50292
+ return super.updateBounds(absolute);
50293
+ }
50294
+ const bounds = this.getBounds();
50295
+ bounds.clear();
50296
+ bounds.addFrame(0, 0, this.width, this.height, this.currentTransform);
50297
+ if (absolute && this.ancestor) {
50298
+ const absPos = this.#frameOrigin().getAbsolutePosition();
50299
+ bounds.centerOn(
50300
+ absPos.x + bounds.x + bounds.width / 2,
50301
+ absPos.y + bounds.y + bounds.height / 2
50302
+ );
50303
+ }
50304
+ return bounds;
50305
+ }
50306
+ /**
50307
+ * The container this particle's position is measured from — its emitter
50308
+ * under the default local reference space, something else otherwise.
50309
+ * @ignore
50310
+ */
50311
+ #frameOrigin() {
50312
+ const emitter = this.ancestor;
50313
+ const space = emitter?.settings?.referenceSpace;
50314
+ if (typeof space === "undefined" || space === "local") {
50315
+ return emitter;
50316
+ }
50317
+ if (space === "world") {
50318
+ return emitter.ancestor ?? emitter;
50319
+ }
50320
+ const target = space;
50321
+ return target && typeof target.pos !== "undefined" ? target : emitter;
50322
+ }
50323
+ /**
50324
+ * A particle is positioned within its reference frame, which is not
50325
+ * necessarily its parent — so summing up the ancestor chain, as the base
50326
+ * implementation does, would measure from the wrong place.
50327
+ * @ignore
50328
+ */
50329
+ getAbsolutePosition() {
50330
+ const origin = this.#frameOrigin();
50331
+ if (origin === this.ancestor) {
50332
+ return super.getAbsolutePosition();
50333
+ }
50334
+ if (typeof this._absPos === "undefined") {
50335
+ this._absPos = vector3dPool.get();
50336
+ }
50337
+ this._absPos.set(this.pos.x, this.pos.y, this.depth);
50338
+ if (!this.floating) {
50339
+ this._absPos.add(origin.getAbsolutePosition());
50340
+ }
50341
+ return this._absPos;
50342
+ }
50343
+ /**
50344
+ * With the placement in `currentTransform` and `autoTransform` off, the
50345
+ * base composition would describe a transform this class never applies.
50346
+ * @ignore
50347
+ */
50348
+ getLocalTransform(out) {
50349
+ return out.copy(this.currentTransform);
50350
+ }
49612
50351
  /**
49613
50352
  * @ignore
49614
50353
  */
@@ -49847,6 +50586,27 @@ var Entity = class _Entity extends Renderable {
49847
50586
  );
49848
50587
  }
49849
50588
  }
50589
+ /**
50590
+ * An entity replaces {@link Renderable#preDraw} wholesale — no flip, no
50591
+ * conjugation, no anchor offset, just its position biased by the body
50592
+ * bounds — so the inherited composition would describe a transform this
50593
+ * class never applies. Mirrors the `preDraw` above instead.
50594
+ * @protected
50595
+ * @param {Matrix3d} out - matrix to write into
50596
+ * @returns {Matrix3d} `out`, for chaining
50597
+ */
50598
+ getLocalTransform(out) {
50599
+ const bounds = this.body.getBounds();
50600
+ out.identity();
50601
+ out.translate(this.pos.x + bounds.x, this.pos.y + bounds.y);
50602
+ if (this.renderable instanceof Renderable) {
50603
+ out.translate(
50604
+ this.anchorPoint.x * bounds.width,
50605
+ this.anchorPoint.y * bounds.height
50606
+ );
50607
+ }
50608
+ return out;
50609
+ }
49850
50610
  /**
49851
50611
  * draw this entity (automatically called by melonJS)
49852
50612
  * @param {CanvasRenderer|WebGLRenderer} renderer - a renderer instance
@@ -50477,7 +51237,7 @@ var Trigger = class extends Renderable {
50477
51237
  };
50478
51238
 
50479
51239
  // src/version.ts
50480
- var version = "20.1.1";
51240
+ var version = "20.2.0";
50481
51241
 
50482
51242
  // src/system/bootstrap.ts
50483
51243
  var initialized = false;
@@ -52593,72 +53353,12 @@ var FRAME_UNIFORM_SIZE = 80;
52593
53353
  var CLEAR_UNIFORM_SIZE = 16;
52594
53354
 
52595
53355
  // src/video/webgpu/pipeline/cache.js
53356
+ function gpuBlendState(mode, premultipliedAlpha) {
53357
+ const component = blendStateFor(mode, premultipliedAlpha);
53358
+ return typeof component === "undefined" ? void 0 : { color: component, alpha: component };
53359
+ }
52596
53360
  var MAX_QUAD_TEXTURES = 8;
52597
53361
  var DEPTH_STENCIL_FORMAT = "depth24plus-stencil8";
52598
- function blendStateFor(mode, premultipliedAlpha) {
52599
- const src = premultipliedAlpha ? "one" : "src-alpha";
52600
- let component;
52601
- switch (mode) {
52602
- case "none":
52603
- return void 0;
52604
- case "additive":
52605
- component = { operation: "add", srcFactor: src, dstFactor: "one" };
52606
- break;
52607
- case "multiply":
52608
- component = {
52609
- operation: "add",
52610
- srcFactor: "dst",
52611
- dstFactor: "one-minus-src-alpha"
52612
- };
52613
- break;
52614
- case "exclusion":
52615
- component = {
52616
- operation: "add",
52617
- srcFactor: "one-minus-dst",
52618
- dstFactor: "one-minus-src"
52619
- };
52620
- break;
52621
- case "screen":
52622
- component = {
52623
- operation: "add",
52624
- srcFactor: "one",
52625
- dstFactor: "one-minus-src"
52626
- };
52627
- break;
52628
- case "darken":
52629
- component = { operation: "min", srcFactor: "one", dstFactor: "one" };
52630
- break;
52631
- case "lighten":
52632
- component = { operation: "max", srcFactor: "one", dstFactor: "one" };
52633
- break;
52634
- default:
52635
- component = {
52636
- operation: "add",
52637
- srcFactor: src,
52638
- dstFactor: "one-minus-src-alpha"
52639
- };
52640
- break;
52641
- }
52642
- return { color: component, alpha: component };
52643
- }
52644
- function normalizeBlendMode(mode) {
52645
- switch (mode) {
52646
- case "none":
52647
- case "multiply":
52648
- case "screen":
52649
- case "darken":
52650
- case "lighten":
52651
- case "exclusion":
52652
- case "normal":
52653
- return mode;
52654
- case "additive":
52655
- case "add":
52656
- case "lighter":
52657
- return "additive";
52658
- default:
52659
- return "normal";
52660
- }
52661
- }
52662
53362
  var STENCIL_STATES = {
52663
53363
  none: {
52664
53364
  stencil: {
@@ -53004,7 +53704,7 @@ var WebGPUPipelineCache = class _WebGPUPipelineCache {
53004
53704
  targets: [
53005
53705
  {
53006
53706
  format: this.format,
53007
- blend: shaderKey === "clear" ? void 0 : blendStateFor(blend, pma),
53707
+ blend: shaderKey === "clear" ? void 0 : gpuBlendState(blend, pma),
53008
53708
  writeMask: stencil.colorWriteMask
53009
53709
  }
53010
53710
  ]
@@ -53085,9 +53785,16 @@ var WebGPUQuadBatcher = class extends WebGPUBatcher {
53085
53785
  },
53086
53786
  onEvict: (slot) => {
53087
53787
  this.segmentEntries[slot] = void 0;
53788
+ if (this._memoSlot === slot) {
53789
+ this._memoView = null;
53790
+ }
53088
53791
  }
53089
53792
  });
53090
53793
  this.segmentEntries = [];
53794
+ this._memoView = null;
53795
+ this._memoFilter = null;
53796
+ this._memoWrap = null;
53797
+ this._memoSlot = 0;
53091
53798
  this.segmentGroup = null;
53092
53799
  this.composedGroups = /* @__PURE__ */ new Map();
53093
53800
  this.resourceIds = /* @__PURE__ */ new WeakMap();
@@ -53186,17 +53893,24 @@ var WebGPUQuadBatcher = class extends WebGPUBatcher {
53186
53893
  const record = store.getResidentRecord(texture, { force: reupload });
53187
53894
  const wrap = texture.repeat ?? "no-repeat";
53188
53895
  const filter = typeof texture.filter === "string" ? texture.filter : renderer.getDefaultTextureFilter();
53896
+ if (record.view === this._memoView && filter === this._memoFilter && wrap === this._memoWrap) {
53897
+ return this._memoSlot;
53898
+ }
53189
53899
  const slotKey = `${this.resourceId(record.view)}|${filter}|${wrap}`;
53190
- if (this.slotTable.peek(slotKey) === void 0) {
53191
- const slot = this.slotTable.slotFor(slotKey);
53192
- this.segmentEntries[slot] = {
53900
+ let resolved = this.slotTable.peek(slotKey);
53901
+ if (resolved === void 0) {
53902
+ resolved = this.slotTable.slotFor(slotKey);
53903
+ this.segmentEntries[resolved] = {
53193
53904
  view: record.view,
53194
53905
  sampler: store.getSampler(filter, wrap)
53195
53906
  };
53196
53907
  this.segmentGroup = null;
53197
- return slot;
53198
53908
  }
53199
- return this.slotTable.peek(slotKey);
53909
+ this._memoView = record.view;
53910
+ this._memoFilter = filter;
53911
+ this._memoWrap = wrap;
53912
+ this._memoSlot = resolved;
53913
+ return resolved;
53200
53914
  }
53201
53915
  /**
53202
53916
  * a stable id for a GPU resource object (bind-group composition keys)
@@ -55236,7 +55950,7 @@ function resolveDepthOps(meshDepthActive, pendingDepthClear) {
55236
55950
  depthStoreOp: "store"
55237
55951
  };
55238
55952
  }
55239
- var WebGPURenderer = class extends Renderer {
55953
+ var WebGPURenderer = class _WebGPURenderer extends Renderer {
55240
55954
  /**
55241
55955
  * @param {ApplicationSettings} options - The renderer parameters
55242
55956
  */
@@ -55273,6 +55987,13 @@ var WebGPURenderer = class extends Renderer {
55273
55987
  this.commandEncoder = null;
55274
55988
  this.renderPass = null;
55275
55989
  this.currentRenderTarget = null;
55990
+ this._advancedBlendEffect = void 0;
55991
+ this._advancedBlendCapture = void 0;
55992
+ this._advancedBlendTarget = void 0;
55993
+ this._advancedBlendOpen = false;
55994
+ this._advancedBlendBusy = 0;
55995
+ this._advancedBlendParent = null;
55996
+ this._advancedBlendWarned = /* @__PURE__ */ new Set();
55276
55997
  this.pendingColorClear = false;
55277
55998
  this.pendingClearValue = null;
55278
55999
  this.pendingStencilClear = false;
@@ -55811,6 +56532,16 @@ var WebGPURenderer = class extends Renderer {
55811
56532
  * frame, or null when no device is available
55812
56533
  */
55813
56534
  toFrameTexture(options = {}) {
56535
+ const busy = this._advancedBlendBusy;
56536
+ this._advancedBlendBusy = (busy ?? 0) + 1;
56537
+ try {
56538
+ return _WebGPURenderer.prototype._toFrameTexture.call(this, options);
56539
+ } finally {
56540
+ this._advancedBlendBusy = busy;
56541
+ }
56542
+ }
56543
+ /** @ignore */
56544
+ _toFrameTexture(options = {}) {
55814
56545
  if (typeof this.device === "undefined") {
55815
56546
  return null;
55816
56547
  }
@@ -56026,6 +56757,7 @@ var WebGPURenderer = class extends Renderer {
56026
56757
  * @override
56027
56758
  */
56028
56759
  blitEffect(source, x, y, width, height, effect, keepBlend = false) {
56760
+ this._advancedBlendBusy++;
56029
56761
  const batcher = this.setBatcher("quad");
56030
56762
  blitSavedProjection.copy(this.projectionMatrix);
56031
56763
  this.projectionMatrix.ortho(0, width, height, 0, -1, 1);
@@ -56033,6 +56765,106 @@ var WebGPURenderer = class extends Renderer {
56033
56765
  batcher.blitTexture(source, x, y, width, height, effect, keepBlend);
56034
56766
  this.projectionMatrix.copy(blitSavedProjection);
56035
56767
  this.pushFrameGlobals();
56768
+ this._advancedBlendBusy--;
56769
+ }
56770
+ /**
56771
+ * Say once, per excluded path and mode, that an advanced blend mode is
56772
+ * falling back to `"normal"`. A silent data-dependent fallback is exactly
56773
+ * what the advanced-blend work exists to remove, so the exclusions are
56774
+ * loud.
56775
+ * @param {string} what - the excluded path, named for the message
56776
+ * @ignore
56777
+ */
56778
+ _warnAdvancedBlendFallback(what) {
56779
+ const key = `${what}:${this.currentBlendMode}`;
56780
+ if (this._advancedBlendWarned.has(key)) {
56781
+ return;
56782
+ }
56783
+ this._advancedBlendWarned.add(key);
56784
+ console.warn(
56785
+ `WebGPURenderer: blend mode "${this.currentBlendMode}" is not supported for ${what} and falls back to "normal"`
56786
+ );
56787
+ }
56788
+ /**
56789
+ * Close a pending advanced-blend bracket, if one is open and we are not
56790
+ * already inside the bracket machinery.
56791
+ * @ignore
56792
+ */
56793
+ _drainAdvancedBlend() {
56794
+ if (this._advancedBlendOpen === true && this._advancedBlendBusy === 0) {
56795
+ this._closeAdvancedBlend();
56796
+ }
56797
+ }
56798
+ /**
56799
+ * Open an advanced-blend bracket: capture the destination, then redirect
56800
+ * this draw into an offscreen target.
56801
+ * @returns {boolean} false when the effect could not be realized, in which
56802
+ * case the draw proceeds unbracketed (plain source-over)
56803
+ * @ignore
56804
+ */
56805
+ _openAdvancedBlend() {
56806
+ const canvas = this.getCanvas();
56807
+ const w = canvas.width;
56808
+ const h = canvas.height;
56809
+ this._advancedBlendBusy++;
56810
+ if (typeof this._advancedBlendEffect === "undefined") {
56811
+ this._advancedBlendEffect = new BlendEffect(this);
56812
+ }
56813
+ if (this._advancedBlendEffect.enabled !== true) {
56814
+ this._advancedBlendBusy--;
56815
+ this._warnAdvancedBlendFallback("this device");
56816
+ return false;
56817
+ }
56818
+ this._advancedBlendEffect.setBlendMode(this.currentBlendMode);
56819
+ this.currentBatcher?.flush();
56820
+ this._advancedBlendCapture = this.toFrameTexture({
56821
+ target: this._advancedBlendCapture ?? null
56822
+ });
56823
+ this._advancedBlendEffect.setTexture(
56824
+ "backdrop",
56825
+ this._advancedBlendCapture
56826
+ );
56827
+ this._advancedBlendParent = this.currentRenderTarget;
56828
+ if (typeof this._advancedBlendTarget === "undefined") {
56829
+ this._advancedBlendTarget = new WebGPURenderTarget(this, w, h, {
56830
+ sampleCount: 1
56831
+ });
56832
+ } else {
56833
+ this._advancedBlendTarget.resize(w, h);
56834
+ }
56835
+ this.setRenderTarget(this._advancedBlendTarget, { clear: true });
56836
+ this._advancedBlendOpen = true;
56837
+ this._advancedBlendBusy--;
56838
+ return true;
56839
+ }
56840
+ /**
56841
+ * Close an advanced-blend bracket: drain the offscreen, retarget to the
56842
+ * parent, and composite through {@link BlendEffect}.
56843
+ * @ignore
56844
+ */
56845
+ _closeAdvancedBlend() {
56846
+ const canvas = this.getCanvas();
56847
+ const w = canvas.width;
56848
+ const h = canvas.height;
56849
+ this._advancedBlendBusy++;
56850
+ this._advancedBlendOpen = false;
56851
+ this.currentBatcher?.flush();
56852
+ this.setRenderTarget(this._advancedBlendParent);
56853
+ this._advancedBlendParent = null;
56854
+ const mode = this.currentBlendMode;
56855
+ this.setBlendMode("normal", true);
56856
+ this.blitEffect(
56857
+ this._advancedBlendTarget,
56858
+ 0,
56859
+ 0,
56860
+ w,
56861
+ h,
56862
+ this._advancedBlendEffect,
56863
+ true
56864
+ );
56865
+ this.currentBatcher?.flush();
56866
+ this.currentBlendMode = mode;
56867
+ this._advancedBlendBusy--;
56036
56868
  }
56037
56869
  /**
56038
56870
  * a draw is about to be recorded outside the clear()/flush() bracket
@@ -56056,6 +56888,9 @@ var WebGPURenderer = class extends Renderer {
56056
56888
  * @override
56057
56889
  */
56058
56890
  flush() {
56891
+ if (this._advancedBlendOpen === true && this._advancedBlendBusy === 0) {
56892
+ this._closeAdvancedBlend();
56893
+ }
56059
56894
  this.currentBatcher?.flush();
56060
56895
  if (this.renderPass !== null) {
56061
56896
  this.renderPass.end();
@@ -56090,6 +56925,9 @@ var WebGPURenderer = class extends Renderer {
56090
56925
  this.pendingColorClear = false;
56091
56926
  this.pendingDepthClear = false;
56092
56927
  this.currentPipeline = null;
56928
+ for (const batcher of this.batchers.values()) {
56929
+ batcher.reset();
56930
+ }
56093
56931
  this.destroyRetiredTextures();
56094
56932
  }
56095
56933
  /**
@@ -56145,6 +56983,7 @@ var WebGPURenderer = class extends Renderer {
56145
56983
  if (typeof this.device === "undefined") {
56146
56984
  return;
56147
56985
  }
56986
+ this._drainAdvancedBlend();
56148
56987
  this.currentBatcher?.flush();
56149
56988
  const pass = this.ensurePass();
56150
56989
  let rgba;
@@ -56241,6 +57080,18 @@ var WebGPURenderer = class extends Renderer {
56241
57080
  if (typeof batcher === "undefined") {
56242
57081
  throw new Error("Invalid Batcher");
56243
57082
  }
57083
+ if (this._advancedBlendBusy === 0) {
57084
+ if (this._advancedBlendOpen === true) {
57085
+ this._closeAdvancedBlend();
57086
+ }
57087
+ if (isAdvancedBlendMode(this.currentBlendMode) === true) {
57088
+ if (name === "mesh" || name === "litMesh") {
57089
+ this._warnAdvancedBlendFallback("3D meshes");
57090
+ } else {
57091
+ this._openAdvancedBlend();
57092
+ }
57093
+ }
57094
+ }
56244
57095
  if (this.currentBatcher !== batcher) {
56245
57096
  if (name !== "mesh" && name !== "litMesh" && // see WebGLRenderer#_canDrainShadows: a transition raised while
56246
57097
  // a mask is being stencilled in (colour writes off) or inside a
@@ -56477,22 +57328,56 @@ var WebGPURenderer = class extends Renderer {
56477
57328
  }
56478
57329
  }
56479
57330
  /**
56480
- * Set the blend mode for subsequent draws. Under WebGPU blending is
56481
- * pipeline state: the change is recorded (after draining vertices
56482
- * queued under the previous mode) and the next batcher flush picks the
56483
- * matching pipeline variant.
56484
- * @param {string} [mode="normal"] - blend mode ("normal", "multiply", "additive"/"lighter", "screen", "darken", "lighten", "none")
57331
+ * set the current blend mode for this renderer. <br>
57332
+ * Every renderer supports the full set: <br>
57333
+ * - "normal" : draws new content on top of the existing content <br>
57334
+ * <img src="../images/normal-blendmode.png" width="180"/> <br>
57335
+ * - "add", "additive", or "lighter" : color values are added together <br>
57336
+ * <img src="../images/add-blendmode.png" width="180"/> <br>
57337
+ * - "multiply" : pixels are multiplied, resulting in a darker picture <br>
57338
+ * <img src="../images/multiply-blendmode.png" width="180"/> <br>
57339
+ * - "screen" : pixels are inverted, multiplied, and inverted again (opposite of multiply) <br>
57340
+ * <img src="../images/screen-blendmode.png" width="180"/> <br>
57341
+ * - "darken" : retains the darkest pixels of both layers <br>
57342
+ * <img src="../images/darken-blendmode.png" width="180"/> <br>
57343
+ * - "lighten" : retains the lightest pixels of both layers <br>
57344
+ * <img src="../images/lighten-blendmode.png" width="180"/> <br>
57345
+ * - "overlay" : multiplies or screens, depending on the backdrop <br>
57346
+ * <img src="../images/overlay-blendmode.png" width="180"/> <br>
57347
+ * - "hard-light" : overlay with the layers swapped — a harsh spotlight <br>
57348
+ * <img src="../images/hard-light-blendmode.png" width="180"/> <br>
57349
+ * - "soft-light" : a diffused spotlight, gentler than hard-light <br>
57350
+ * <img src="../images/soft-light-blendmode.png" width="180"/> <br>
57351
+ * - "color-dodge" : brightens the backdrop to reflect the source <br>
57352
+ * <img src="../images/color-dodge-blendmode.png" width="180"/> <br>
57353
+ * - "color-burn" : darkens the backdrop to reflect the source <br>
57354
+ * <img src="../images/color-burn-blendmode.png" width="180"/> <br>
57355
+ * - "difference" : the absolute difference of the two layers <br>
57356
+ * <img src="../images/difference-blendmode.png" width="180"/> <br>
57357
+ * - "exclusion" : like difference, but lower in contrast <br>
57358
+ * <img src="../images/exclusion-blendmode.png" width="180"/> <br>
57359
+ * - "none" : blending disabled — the source replaces the destination
57360
+ * outright, alpha included <br>
57361
+ * A few draw types cannot honour every mode — 3D meshes, and fills using
57362
+ * a {@link Gradient} — and fall back to "normal" with a one-time console
57363
+ * warning. `setBlendMode` returns what it actually applied, so comparing
57364
+ * the result against your request detects that case.
57365
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
57366
+ * @param {string} [mode="normal"] - blend mode
56485
57367
  * @param {boolean} [premultipliedAlpha=true] - whether textures use premultiplied alpha (affects the source blend factor)
56486
- * @returns {string} the blend mode actually applied
57368
+ * @returns {string} the blend mode actually applied (may differ if the requested mode is unsupported)
56487
57369
  */
56488
57370
  setBlendMode(mode = "normal", premultipliedAlpha = true) {
56489
57371
  const normalized = normalizeBlendMode(mode);
56490
- if (this.currentBlendMode !== normalized || this.premultipliedAlpha !== premultipliedAlpha) {
57372
+ const advanced = isAdvancedBlendMode(mode);
57373
+ const tracked = advanced === true ? mode : normalized;
57374
+ if (this.currentBlendMode !== tracked || this.premultipliedAlpha !== premultipliedAlpha) {
57375
+ this._drainAdvancedBlend();
56491
57376
  this.currentBatcher?.flush();
56492
- this.currentBlendMode = normalized;
57377
+ this.currentBlendMode = tracked;
56493
57378
  this.premultipliedAlpha = premultipliedAlpha;
56494
57379
  }
56495
- return normalized === "normal" && mode !== "normal" ? "normal" : mode;
57380
+ return reportedBlendMode(mode);
56496
57381
  }
56497
57382
  /**
56498
57383
  * Set the projection matrix — pushes a fresh frame-globals slot so
@@ -56980,6 +57865,11 @@ var WebGPURenderer = class extends Renderer {
56980
57865
  const grad = this.currentGradient;
56981
57866
  const hasMask = this.maskLevel > 0;
56982
57867
  this.currentGradient = null;
57868
+ if (isAdvancedBlendMode(this.currentBlendMode) === true) {
57869
+ this._warnAdvancedBlendFallback("gradient fills");
57870
+ this._drainAdvancedBlend();
57871
+ }
57872
+ this._advancedBlendBusy++;
56983
57873
  this.currentBatcher?.flush();
56984
57874
  const visibleRef = this.maskVisibleRef;
56985
57875
  let markRef = 1;
@@ -57015,6 +57905,7 @@ var WebGPURenderer = class extends Renderer {
57015
57905
  this.stencilMode = "none";
57016
57906
  }
57017
57907
  this.ensurePass().setStencilReference(this.maskVisibleRef);
57908
+ this._advancedBlendBusy--;
57018
57909
  }
57019
57910
  /**
57020
57911
  * Stroke an arc at the specified coordinates with given radius, start and end points
@@ -57672,6 +58563,14 @@ var WebGPURenderer = class extends Renderer {
57672
58563
  * @override
57673
58564
  */
57674
58565
  destroy() {
58566
+ this._advancedBlendEffect?.destroy();
58567
+ this._advancedBlendEffect = void 0;
58568
+ this._advancedBlendCapture?.destroy();
58569
+ this._advancedBlendCapture = void 0;
58570
+ this._advancedBlendTarget?.destroy?.();
58571
+ this._advancedBlendTarget = void 0;
58572
+ this._advancedBlendOpen = false;
58573
+ this._advancedBlendBusy = 0;
57675
58574
  releaseShadowQuads(this);
57676
58575
  off(GAME_RESET, this.onGameReset);
57677
58576
  off(CANVAS_ONRESIZE, this.onCanvasResize);
@@ -58552,6 +59451,7 @@ var defaultParticleEmitterSettings = {
58552
59451
  blendMode: "normal",
58553
59452
  onlyInViewport: true,
58554
59453
  floating: false,
59454
+ referenceSpace: "local",
58555
59455
  maxParticles: 10,
58556
59456
  frequency: 100,
58557
59457
  duration: Infinity,
@@ -58563,6 +59463,10 @@ var defaultParticleEmitterSettings = {
58563
59463
  var settings_default = defaultParticleEmitterSettings;
58564
59464
 
58565
59465
  // src/particles/emitter.ts
59466
+ var _m1 = new Matrix3d();
59467
+ var _m2 = new Matrix3d();
59468
+ var _correction = new Matrix3d();
59469
+ var _rebase = new Vector2d();
58566
59470
  function createDefaultParticleTexture(w = 8, h = 8) {
58567
59471
  const defaultParticleTexture = new canvasrendertarget_default(w, h, {
58568
59472
  offscreenCanvas: true
@@ -58595,6 +59499,12 @@ var ParticleEmitter = class extends Container {
58595
59499
  _dt;
58596
59500
  /** @ignore */
58597
59501
  _defaultParticle;
59502
+ /**
59503
+ * The blend mode last fanned out to the particles, so a change to
59504
+ * {@link ParticleEmitter#blendMode} can be detected per frame.
59505
+ * @ignore
59506
+ */
59507
+ #appliedBlendMode = "normal";
58598
59508
  /**
58599
59509
  * whether at least one particle has been spawned by this emitter — used as
58600
59510
  * the precondition for completion detection (a brand-new emitter with zero
@@ -58608,6 +59518,19 @@ var ParticleEmitter = class extends Container {
58608
59518
  * @ignore
58609
59519
  */
58610
59520
  _deltaInv;
59521
+ /**
59522
+ * Maps an emitter-local spawn point into the reference frame the particles
59523
+ * live in, so a particle is BORN where the emitter is even though its
59524
+ * position is thereafter measured from somewhere else. `undefined` in
59525
+ * local mode, where the two frames coincide and no mapping is needed.
59526
+ *
59527
+ * Held per emitter (not a module scratch) because particles read it during
59528
+ * their own reset, after `addParticles` has computed it. One matrix per
59529
+ * emitter that actually uses a non-local space — emitters are few, unlike
59530
+ * particles, which allocate nothing.
59531
+ * @ignore
59532
+ */
59533
+ _spawnMap;
58611
59534
  /**
58612
59535
  * @param x - x position of the particle emitter
58613
59536
  * @param y - y position of the particle emitter
@@ -58654,11 +59577,8 @@ var ParticleEmitter = class extends Container {
58654
59577
  this._deltaInv = timer_default.maxfps / 1e3;
58655
59578
  this.reset(settings);
58656
59579
  }
58657
- /**
58658
- * Reset the emitter with particle emitter settings.
58659
- * @param settings - object with emitter settings. See {@link ParticleEmitterSettings}
58660
- */
58661
59580
  reset(settings = {}) {
59581
+ const previousFrame = this.#frameOf(this.settings?.referenceSpace);
58662
59582
  Object.assign(this.settings, settings_default, settings);
58663
59583
  clampMinToMax(this.settings, "minLife", "maxLife");
58664
59584
  clampMinToMax(this.settings, "minStartScale", "maxStartScale");
@@ -58677,8 +59597,131 @@ var ParticleEmitter = class extends Container {
58677
59597
  this.settings.image = this._defaultParticle.canvas;
58678
59598
  }
58679
59599
  this.floating = this.settings.floating;
59600
+ this.blendMode = this.settings.blendMode;
59601
+ this.#appliedBlendMode = this.settings.blendMode;
59602
+ this.#rebase(previousFrame);
59603
+ this.isDirty = true;
59604
+ }
59605
+ /**
59606
+ * What a particle's position is measured against — see
59607
+ * {@link ParticleEmitterSettings.referenceSpace}.
59608
+ *
59609
+ * Assigning this re-bases every particle already alive into the new frame,
59610
+ * so nothing jumps: the cloud stays exactly where it is on screen and only
59611
+ * its subsequent motion differs. Passing it through
59612
+ * {@link ParticleEmitter#reset} does the same.
59613
+ * @default "local"
59614
+ * @example
59615
+ * emitter.referenceSpace = "world"; // start leaving a trail
59616
+ */
59617
+ get referenceSpace() {
59618
+ return this.settings.referenceSpace;
59619
+ }
59620
+ set referenceSpace(space) {
59621
+ if (space === this.settings.referenceSpace) {
59622
+ return;
59623
+ }
59624
+ const previous = this.#frameOf(this.settings.referenceSpace);
59625
+ this.settings.referenceSpace = space;
59626
+ this.#rebase(previous);
59627
+ }
59628
+ /**
59629
+ * Map the particles already alive out of the frame they were simulating
59630
+ * in and into the current one, so a change of reference space is
59631
+ * invisible at the instant it happens: the cloud stays exactly where it
59632
+ * is on screen and only its subsequent motion differs.
59633
+ *
59634
+ * Called from the accessor and from {@link ParticleEmitter#reset} alike —
59635
+ * `reset()` assigns `settings` wholesale and would otherwise leave live
59636
+ * particles holding coordinates measured against a frame that is no
59637
+ * longer theirs, teleporting the lot.
59638
+ * @ignore
59639
+ * @param previous - the frame the live particles are currently in
59640
+ */
59641
+ #rebase(previous) {
59642
+ const next = this.#frameOf(this.settings.referenceSpace);
59643
+ if (previous !== next) {
59644
+ _m1.identity();
59645
+ _m1.multiply(this.#worldFrame(next, _m2).invert());
59646
+ _m1.multiply(this.#worldFrame(previous, _m2));
59647
+ for (const particle of this.getChildren()) {
59648
+ _rebase.set(particle.pos.x, particle.pos.y);
59649
+ _m1.apply(_rebase);
59650
+ particle.pos.x = _rebase.x;
59651
+ particle.pos.y = _rebase.y;
59652
+ }
59653
+ }
59654
+ this._spawnMap = void 0;
58680
59655
  this.isDirty = true;
58681
59656
  }
59657
+ /**
59658
+ * Resolve a reference-space value to the container whose frame the
59659
+ * particles live in. Returns `this` whenever the answer is "the emitter
59660
+ * itself" — including the degenerate cases (`"world"` on an emitter with no
59661
+ * parent, or a custom target that IS the emitter), which then take the
59662
+ * local fast path with no correction at all.
59663
+ * @ignore
59664
+ */
59665
+ #frameOf(space) {
59666
+ if (space === "local") {
59667
+ return this;
59668
+ }
59669
+ if (space === "world") {
59670
+ return this.ancestor ?? this;
59671
+ }
59672
+ if (!space || typeof space.pos === "undefined") {
59673
+ return this;
59674
+ }
59675
+ return space;
59676
+ }
59677
+ /**
59678
+ * The world transform of a reference frame — i.e. of the space that
59679
+ * container's children are drawn in.
59680
+ * @ignore
59681
+ */
59682
+ #worldFrame(frame, out) {
59683
+ return frame.getWorldTransform(out);
59684
+ }
59685
+ /**
59686
+ * The transform to insert before walking the children so they are drawn in
59687
+ * the reference frame instead of the emitter's own.
59688
+ *
59689
+ * At the insertion point the renderer holds `W_ancestor · preContrib`, and
59690
+ * `Container.draw` appends `T(pos)` afterwards, so what we need is
59691
+ *
59692
+ * ```
59693
+ * K = inv(preContrib) · inv(W_ancestor) · W_target · T(−pos)
59694
+ * ```
59695
+ *
59696
+ * expressed below via `inv(preContrib) = T(pos) · inv(L)`. Note this is
59697
+ * NOT `inv(W_emitter) · W_target` — matrices do not commute, and that form
59698
+ * only coincides with this one when both chains share the same linear
59699
+ * part, which hides the difference until something upstream is rotated.
59700
+ *
59701
+ * When the target is the emitter's own parent — the `"world"` case — the
59702
+ * whole ancestor chain cancels and no walk happens at all.
59703
+ * @ignore
59704
+ * @returns the correction, or `undefined` in local mode
59705
+ */
59706
+ #correctionMatrix() {
59707
+ const target = this.#frameOf(this.settings.referenceSpace);
59708
+ if (target === this) {
59709
+ return void 0;
59710
+ }
59711
+ _correction.identity();
59712
+ _correction.translate(this.pos.x, this.pos.y);
59713
+ _correction.multiply(this.getLocalTransform(_m1).invert());
59714
+ if (target !== this.ancestor) {
59715
+ if (this.ancestor) {
59716
+ _correction.multiply(
59717
+ this.ancestor.getWorldTransform(_m1).invert()
59718
+ );
59719
+ }
59720
+ _correction.multiply(target.getWorldTransform(_m2));
59721
+ }
59722
+ _correction.translate(-this.pos.x, -this.pos.y);
59723
+ return _correction;
59724
+ }
58682
59725
  /**
58683
59726
  * returns a random point on the x axis within the bounds of this emitter
58684
59727
  * @returns a random x position within the emitter bounds
@@ -58693,10 +59736,39 @@ var ParticleEmitter = class extends Container {
58693
59736
  getRandomPointY() {
58694
59737
  return randomFloat(0, this.getBounds().height);
58695
59738
  }
59739
+ /**
59740
+ * Draw the particles in their reference frame rather than the emitter's.
59741
+ *
59742
+ * The correction goes in before `super.draw()` because that is where the
59743
+ * child walk happens; translations and the correction compose, and in
59744
+ * local mode there is no correction and this is the inherited path
59745
+ * untouched.
59746
+ * @ignore
59747
+ */
59748
+ draw(renderer, viewport) {
59749
+ const correction = this.#correctionMatrix();
59750
+ if (correction !== void 0) {
59751
+ renderer.transform(correction);
59752
+ }
59753
+ super.draw(renderer, viewport);
59754
+ }
58696
59755
  // Add count particles in the game world
58697
59756
  /** @ignore */
58698
59757
  addParticles(count) {
58699
59758
  const z = this.depth;
59759
+ const target = this.#frameOf(this.settings.referenceSpace);
59760
+ if (target === this) {
59761
+ this._spawnMap = void 0;
59762
+ } else {
59763
+ const map = this._spawnMap ?? (this._spawnMap = new Matrix3d());
59764
+ if (target === this.ancestor) {
59765
+ map.copy(this.getLocalTransform(_m1));
59766
+ } else {
59767
+ map.identity();
59768
+ map.multiply(target.getWorldTransform(_m1).invert());
59769
+ map.multiply(this.getWorldTransform(_m2));
59770
+ }
59771
+ }
58700
59772
  for (let i = 0; i < count; i++) {
58701
59773
  this.addChild(particlePool.get(this), z);
58702
59774
  }
@@ -58744,6 +59816,14 @@ var ParticleEmitter = class extends Container {
58744
59816
  * @ignore
58745
59817
  */
58746
59818
  update(dt) {
59819
+ if (this.blendMode !== this.#appliedBlendMode) {
59820
+ this.#appliedBlendMode = this.blendMode;
59821
+ this.settings.blendMode = this.blendMode;
59822
+ for (const particle of this.children ?? []) {
59823
+ particle.blendMode = this.blendMode;
59824
+ }
59825
+ this.isDirty = true;
59826
+ }
58747
59827
  if (this.settings.framesToSkip > 0) {
58748
59828
  if (++this._updateCount > this.settings.framesToSkip) {
58749
59829
  this._updateCount = 0;
@@ -58757,6 +59837,9 @@ var ParticleEmitter = class extends Container {
58757
59837
  }
58758
59838
  const childrenDirty = super.update(dt);
58759
59839
  this.isDirty = this.isDirty || childrenDirty;
59840
+ if (this.#frameOf(this.settings.referenceSpace) !== this && this.getChildren().length > 0) {
59841
+ this.inViewport = true;
59842
+ }
58760
59843
  if (this._enabled && this._stream) {
58761
59844
  if (this._durationTimer !== Infinity) {
58762
59845
  this._durationTimer -= dt;
@@ -58800,6 +59883,8 @@ var ParticleEmitter = class extends Container {
58800
59883
  this._defaultParticle = void 0;
58801
59884
  }
58802
59885
  this.settings.image = void 0;
59886
+ this.settings.referenceSpace = "local";
59887
+ this._spawnMap = void 0;
58803
59888
  }
58804
59889
  };
58805
59890
 
@@ -60383,7 +61468,7 @@ var UITextButton = class extends UIBaseElement {
60383
61468
  };
60384
61469
 
60385
61470
  // src/video/effects/blur.js
60386
- var wgslFragment2 = `
61471
+ var wgslFragment3 = `
60387
61472
  struct BlurUniforms {
60388
61473
  uBlurStrength : f32,
60389
61474
  uTextureSize : vec2f,
@@ -60434,7 +61519,7 @@ var BlurEffect = class extends ShaderEffect {
60434
61519
  return (sum / 9.0) * vColor;
60435
61520
  }
60436
61521
  `,
60437
- wgsl: wgslFragment2
61522
+ wgsl: wgslFragment3
60438
61523
  });
60439
61524
  this.strength = options.strength ?? 1;
60440
61525
  const texSize = options.textureSize ?? [256, 256];
@@ -60460,7 +61545,7 @@ var BlurEffect = class extends ShaderEffect {
60460
61545
  };
60461
61546
 
60462
61547
  // src/video/effects/chromaticAberration.js
60463
- var wgslFragment3 = `
61548
+ var wgslFragment4 = `
60464
61549
  struct AberrationUniforms {
60465
61550
  uOffset : f32,
60466
61551
  uTextureSize : vec2f,
@@ -60497,7 +61582,7 @@ var ChromaticAberrationEffect = class extends ShaderEffect {
60497
61582
  return vec4(r, g, b, a) * vColor;
60498
61583
  }
60499
61584
  `,
60500
- wgsl: wgslFragment3
61585
+ wgsl: wgslFragment4
60501
61586
  });
60502
61587
  this.offset = options.offset ?? 3;
60503
61588
  const texSize = options.textureSize ?? [256, 256];
@@ -60545,7 +61630,7 @@ var DesaturateEffect = class extends ColorMatrixEffect {
60545
61630
  };
60546
61631
 
60547
61632
  // src/video/effects/dissolve.js
60548
- var wgslFragment4 = `
61633
+ var wgslFragment5 = `
60549
61634
  struct DissolveUniforms {
60550
61635
  uDissolveProgress : f32,
60551
61636
  uEdgeColor : vec3f,
@@ -60657,7 +61742,7 @@ var DissolveEffect = class extends ShaderEffect {
60657
61742
  return color;
60658
61743
  }
60659
61744
  `,
60660
- wgsl: wgslFragment4
61745
+ wgsl: wgslFragment5
60661
61746
  });
60662
61747
  this.progress = options.progress ?? 0;
60663
61748
  this.setUniform("uDissolveProgress", this.progress);
@@ -60685,7 +61770,7 @@ var DissolveEffect = class extends ShaderEffect {
60685
61770
  };
60686
61771
 
60687
61772
  // src/video/effects/dropShadow.js
60688
- var wgslFragment5 = `
61773
+ var wgslFragment6 = `
60689
61774
  struct ShadowUniforms {
60690
61775
  uShadowOffset : vec2f,
60691
61776
  uShadowColor : vec3f,
@@ -60749,7 +61834,7 @@ var DropShadowEffect = class extends ShaderEffect {
60749
61834
  return color;
60750
61835
  }
60751
61836
  `,
60752
- wgsl: wgslFragment5
61837
+ wgsl: wgslFragment6
60753
61838
  });
60754
61839
  const texSize = options.textureSize ?? [256, 256];
60755
61840
  this.setUniform(
@@ -60783,7 +61868,7 @@ var DropShadowEffect = class extends ShaderEffect {
60783
61868
  };
60784
61869
 
60785
61870
  // src/video/effects/flash.js
60786
- var wgslFragment6 = `
61871
+ var wgslFragment7 = `
60787
61872
  struct FlashUniforms {
60788
61873
  uFlashColor : vec3f,
60789
61874
  uFlashIntensity : f32,
@@ -60810,7 +61895,7 @@ var FlashEffect = class extends ShaderEffect {
60810
61895
  return vec4(mix(color.rgb, uFlashColor * color.a, uFlashIntensity), color.a);
60811
61896
  }
60812
61897
  `,
60813
- wgsl: wgslFragment6
61898
+ wgsl: wgslFragment7
60814
61899
  });
60815
61900
  const color = options.color ?? [1, 1, 1];
60816
61901
  this.intensity = options.intensity ?? 0;
@@ -60835,7 +61920,7 @@ var FlashEffect = class extends ShaderEffect {
60835
61920
  };
60836
61921
 
60837
61922
  // src/video/effects/glow.js
60838
- var wgslFragment7 = `
61923
+ var wgslFragment8 = `
60839
61924
  struct GlowUniforms {
60840
61925
  uGlowColor : vec3f,
60841
61926
  uGlowWidth : f32,
@@ -60896,7 +61981,7 @@ var GlowEffect = class extends ShaderEffect {
60896
61981
  return color;
60897
61982
  }
60898
61983
  `,
60899
- wgsl: wgslFragment7
61984
+ wgsl: wgslFragment8
60900
61985
  });
60901
61986
  const color = options.color ?? [1, 1, 1];
60902
61987
  const width = options.width ?? 3;
@@ -60933,7 +62018,7 @@ var GlowEffect = class extends ShaderEffect {
60933
62018
  };
60934
62019
 
60935
62020
  // src/video/effects/hologram.js
60936
- var wgslFragment8 = `
62021
+ var wgslFragment9 = `
60937
62022
  struct HologramUniforms {
60938
62023
  uHoloColor : vec3f,
60939
62024
  uHoloIntensity : f32,
@@ -60976,7 +62061,7 @@ var HologramEffect = class extends ShaderEffect {
60976
62061
  return vec4(holo, color.a * flicker);
60977
62062
  }
60978
62063
  `,
60979
- wgsl: wgslFragment8
62064
+ wgsl: wgslFragment9
60980
62065
  });
60981
62066
  const color = options.color ?? [0.1, 0.7, 1];
60982
62067
  this.setUniform("uHoloColor", new Float32Array(color));
@@ -61015,7 +62100,7 @@ var InvertEffect = class extends ColorMatrixEffect {
61015
62100
  };
61016
62101
 
61017
62102
  // src/video/effects/outline.js
61018
- var wgslFragment9 = `
62103
+ var wgslFragment10 = `
61019
62104
  struct OutlineUniforms {
61020
62105
  uOutlineColor : vec3f,
61021
62106
  uOutlineWidth : f32,
@@ -61079,7 +62164,7 @@ var OutlineEffect = class extends ShaderEffect {
61079
62164
  return color;
61080
62165
  }
61081
62166
  `,
61082
- wgsl: wgslFragment9
62167
+ wgsl: wgslFragment10
61083
62168
  });
61084
62169
  const color = options.color ?? [1, 1, 1];
61085
62170
  const width = options.width ?? 1;
@@ -61113,7 +62198,7 @@ var OutlineEffect = class extends ShaderEffect {
61113
62198
  };
61114
62199
 
61115
62200
  // src/video/effects/pixelate.js
61116
- var wgslFragment10 = `
62201
+ var wgslFragment11 = `
61117
62202
  struct PixelateUniforms {
61118
62203
  uPixelSize : f32,
61119
62204
  uTextureSize : vec2f,
@@ -61144,7 +62229,7 @@ var PixelateEffect = class extends ShaderEffect {
61144
62229
  return texture2D(uSampler, snapped) * vColor;
61145
62230
  }
61146
62231
  `,
61147
- wgsl: wgslFragment10
62232
+ wgsl: wgslFragment11
61148
62233
  });
61149
62234
  this.size = options.size ?? 4;
61150
62235
  const texSize = options.textureSize ?? [256, 256];
@@ -61170,7 +62255,7 @@ var PixelateEffect = class extends ShaderEffect {
61170
62255
  };
61171
62256
 
61172
62257
  // src/video/effects/scanline.js
61173
- var wgslFragment11 = `
62258
+ var wgslFragment12 = `
61174
62259
  struct ScanlineUniforms {
61175
62260
  uScanlineOpacity : f32,
61176
62261
  uCurvature : f32,
@@ -61249,7 +62334,7 @@ var ScanlineEffect = class extends ShaderEffect {
61249
62334
  return color;
61250
62335
  }
61251
62336
  `,
61252
- wgsl: wgslFragment11
62337
+ wgsl: wgslFragment12
61253
62338
  });
61254
62339
  this.setUniform("uScanlineOpacity", options.opacity ?? 0.25);
61255
62340
  this.setUniform("uCurvature", options.curvature ?? 0);
@@ -61301,7 +62386,7 @@ var SepiaEffect = class extends ColorMatrixEffect {
61301
62386
  };
61302
62387
 
61303
62388
  // src/video/effects/shine.js
61304
- var wgslFragment12 = `
62389
+ var wgslFragment13 = `
61305
62390
  struct ShineUniforms {
61306
62391
  uShineColor : vec3f,
61307
62392
  uShineWidth : f32,
@@ -61397,7 +62482,7 @@ var ShineEffect = class extends ShaderEffect {
61397
62482
  return vec4(result, color.a);
61398
62483
  }
61399
62484
  `,
61400
- wgsl: wgslFragment12
62485
+ wgsl: wgslFragment13
61401
62486
  });
61402
62487
  this.setUniform(
61403
62488
  "uShineColor",
@@ -61444,7 +62529,7 @@ var ShineEffect = class extends ShaderEffect {
61444
62529
  };
61445
62530
 
61446
62531
  // src/video/effects/tintPulse.js
61447
- var wgslFragment13 = `
62532
+ var wgslFragment14 = `
61448
62533
  struct TintPulseUniforms {
61449
62534
  uPulseColor : vec3f,
61450
62535
  uPulseSpeed : f32,
@@ -61478,7 +62563,7 @@ var TintPulseEffect = class extends ShaderEffect {
61478
62563
  return vec4(mix(color.rgb, uPulseColor * color.a, pulse), color.a);
61479
62564
  }
61480
62565
  `,
61481
- wgsl: wgslFragment13
62566
+ wgsl: wgslFragment14
61482
62567
  });
61483
62568
  const color = options.color ?? [1, 0, 0];
61484
62569
  this.setUniform("uPulseColor", new Float32Array(color));
@@ -61512,7 +62597,7 @@ var fragment = `
61512
62597
  return vec4(color.rgb * v, color.a);
61513
62598
  }
61514
62599
  `;
61515
- var wgslFragment14 = `
62600
+ var wgslFragment15 = `
61516
62601
  struct VignetteUniforms {
61517
62602
  uStrength : f32,
61518
62603
  uSize : f32,
@@ -61533,7 +62618,7 @@ var VignetteEffect = class extends ShaderEffect {
61533
62618
  * @param {number} [options.size=25.0] - vignette spread multiplier (higher = smaller dark area)
61534
62619
  */
61535
62620
  constructor(renderer, options = {}) {
61536
- super(renderer, { glsl: fragment, wgsl: wgslFragment14 });
62621
+ super(renderer, { glsl: fragment, wgsl: wgslFragment15 });
61537
62622
  this.strength = options.strength ?? 0.15;
61538
62623
  this.size = options.size ?? 25;
61539
62624
  this.setUniform("uStrength", this.strength);
@@ -61558,7 +62643,7 @@ var VignetteEffect = class extends ShaderEffect {
61558
62643
  };
61559
62644
 
61560
62645
  // src/video/effects/wave.js
61561
- var wgslFragment15 = `
62646
+ var wgslFragment16 = `
61562
62647
  struct WaveUniforms {
61563
62648
  uAmplitude : f32,
61564
62649
  uFrequency : f32,
@@ -61594,7 +62679,7 @@ var WaveEffect = class extends ShaderEffect {
61594
62679
  return texture2D(uSampler, distorted) * vColor;
61595
62680
  }
61596
62681
  `,
61597
- wgsl: wgslFragment15
62682
+ wgsl: wgslFragment16
61598
62683
  });
61599
62684
  this.setUniform("uAmplitude", options.amplitude ?? 0.01);
61600
62685
  this.setUniform("uFrequency", options.frequency ?? 10);