melonjs 19.2.0 → 19.3.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 (42) hide show
  1. package/README.md +12 -10
  2. package/build/camera/camera2d.d.ts.map +1 -1
  3. package/build/index.js +1623 -529
  4. package/build/index.js.map +4 -4
  5. package/build/physics/bounds.d.ts +5 -2
  6. package/build/physics/bounds.d.ts.map +1 -1
  7. package/build/renderable/container.d.ts +1 -1
  8. package/build/renderable/container.d.ts.map +1 -1
  9. package/build/renderable/light2d.d.ts +128 -18
  10. package/build/renderable/light2d.d.ts.map +1 -1
  11. package/build/renderable/sprite.d.ts +38 -6
  12. package/build/renderable/sprite.d.ts.map +1 -1
  13. package/build/state/stage.d.ts +65 -9
  14. package/build/state/stage.d.ts.map +1 -1
  15. package/build/video/buffer/vertex.d.ts +2 -1
  16. package/build/video/buffer/vertex.d.ts.map +1 -1
  17. package/build/video/canvas/canvas_renderer.d.ts +2 -0
  18. package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
  19. package/build/video/renderer.d.ts +62 -0
  20. package/build/video/renderer.d.ts.map +1 -1
  21. package/build/video/renderstate.d.ts +20 -0
  22. package/build/video/renderstate.d.ts.map +1 -1
  23. package/build/video/texture/atlas.d.ts +26 -2
  24. package/build/video/texture/atlas.d.ts.map +1 -1
  25. package/build/video/webgl/batchers/batcher.d.ts +6 -0
  26. package/build/video/webgl/batchers/batcher.d.ts.map +1 -1
  27. package/build/video/webgl/batchers/lit_quad_batcher.d.ts +109 -0
  28. package/build/video/webgl/batchers/lit_quad_batcher.d.ts.map +1 -0
  29. package/build/video/webgl/batchers/quad_batcher.d.ts +19 -1
  30. package/build/video/webgl/batchers/quad_batcher.d.ts.map +1 -1
  31. package/build/video/webgl/effects/radialGradient.d.ts +105 -0
  32. package/build/video/webgl/effects/radialGradient.d.ts.map +1 -0
  33. package/build/video/webgl/glshader.d.ts.map +1 -1
  34. package/build/video/webgl/lighting/constants.d.ts +13 -0
  35. package/build/video/webgl/lighting/constants.d.ts.map +1 -0
  36. package/build/video/webgl/lighting/pack.d.ts +76 -0
  37. package/build/video/webgl/lighting/pack.d.ts.map +1 -0
  38. package/build/video/webgl/shaders/multitexture-lit.d.ts +23 -0
  39. package/build/video/webgl/shaders/multitexture-lit.d.ts.map +1 -0
  40. package/build/video/webgl/webgl_renderer.d.ts +17 -0
  41. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  42. package/package.json +1 -1
package/build/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * melonJS Game Engine - 19.2.0
2
+ * melonJS Game Engine - 19.3.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
@@ -750,8 +750,8 @@ var require_weak_map_basic_detection = __commonJS({
750
750
  "use strict";
751
751
  var globalThis2 = require_global_this();
752
752
  var isCallable = require_is_callable();
753
- var WeakMap = globalThis2.WeakMap;
754
- module.exports = isCallable(WeakMap) && /native code/.test(String(WeakMap));
753
+ var WeakMap2 = globalThis2.WeakMap;
754
+ module.exports = isCallable(WeakMap2) && /native code/.test(String(WeakMap2));
755
755
  }
756
756
  });
757
757
 
@@ -790,7 +790,7 @@ var require_internal_state = __commonJS({
790
790
  var hiddenKeys = require_hidden_keys();
791
791
  var OBJECT_ALREADY_INITIALIZED = "Object already initialized";
792
792
  var TypeError2 = globalThis2.TypeError;
793
- var WeakMap = globalThis2.WeakMap;
793
+ var WeakMap2 = globalThis2.WeakMap;
794
794
  var set;
795
795
  var get2;
796
796
  var has2;
@@ -807,7 +807,7 @@ var require_internal_state = __commonJS({
807
807
  };
808
808
  };
809
809
  if (NATIVE_WEAK_MAP || shared.state) {
810
- store = shared.state || (shared.state = new WeakMap());
810
+ store = shared.state || (shared.state = new WeakMap2());
811
811
  store.get = store.get;
812
812
  store.has = store.has;
813
813
  store.set = store.set;
@@ -4639,6 +4639,7 @@ var pointPool = createPool((x, y) => {
4639
4639
  });
4640
4640
 
4641
4641
  // src/physics/bounds.ts
4642
+ var _addFrameScratch = new Point();
4642
4643
  var Bounds = class _Bounds {
4643
4644
  _center;
4644
4645
  type;
@@ -4865,20 +4866,41 @@ var Bounds = class _Bounds {
4865
4866
  this.max.y = Math.max(this.max.y, p.y);
4866
4867
  }
4867
4868
  /**
4868
- * Adds the given quad coordinates to this bounds definition, multiplied by the given matrix.
4869
+ * Expands this bounds to include the axis-aligned bounding box of
4870
+ * the given rect's four corners, optionally transformed through `m`
4871
+ * first. With a non-identity `m` (rotation, scale, etc.) the result
4872
+ * is the AABB of the transformed quad, not a transformed AABB.
4869
4873
  * @param x0 - The left x coordinate of the quad.
4870
4874
  * @param y0 - The top y coordinate of the quad.
4871
4875
  * @param x1 - The right x coordinate of the quad.
4872
4876
  * @param y1 - The bottom y coordinate of the quad.
4873
- * @param [m] - An optional transform to apply to the given coordinates.
4877
+ * @param [m] - An optional transform applied to each corner before inclusion.
4874
4878
  */
4875
4879
  addFrame(x0, y0, x1, y1, m) {
4876
- const v = pointPool.get();
4880
+ if (m === void 0 || m.isIdentity()) {
4881
+ const minX = Math.min(x0, x1);
4882
+ const maxX = Math.max(x0, x1);
4883
+ const minY = Math.min(y0, y1);
4884
+ const maxY = Math.max(y0, y1);
4885
+ if (minX < this.min.x) {
4886
+ this.min.x = minX;
4887
+ }
4888
+ if (maxX > this.max.x) {
4889
+ this.max.x = maxX;
4890
+ }
4891
+ if (minY < this.min.y) {
4892
+ this.min.y = minY;
4893
+ }
4894
+ if (maxY > this.max.y) {
4895
+ this.max.y = maxY;
4896
+ }
4897
+ return;
4898
+ }
4899
+ const v = _addFrameScratch;
4877
4900
  this.addPoint(v.set(x0, y0), m);
4878
4901
  this.addPoint(v.set(x1, y0), m);
4879
4902
  this.addPoint(v.set(x0, y1), m);
4880
4903
  this.addPoint(v.set(x1, y1), m);
4881
- pointPool.release(v);
4882
4904
  }
4883
4905
  contains(xOrVectorOrBounds, y) {
4884
4906
  let _x1;
@@ -14138,10 +14160,10 @@ var Container = class _Container extends Renderable {
14138
14160
  draw(renderer2, viewport) {
14139
14161
  const bounds = this.getBounds();
14140
14162
  this.drawCount = 0;
14141
- if (this.root === false && this.clipping === true && bounds.isFinite() === true) {
14142
- renderer2.clipRect(bounds.left, bounds.top, bounds.width, bounds.height);
14143
- }
14144
14163
  renderer2.translate(this.pos.x, this.pos.y);
14164
+ if (this.root === false && this.clipping === true && bounds.isFinite() === true && Number.isFinite(this.width) === true && Number.isFinite(this.height) === true) {
14165
+ renderer2.clipRect(0, 0, this.width, this.height);
14166
+ }
14145
14167
  if (this.backgroundColor.alpha > 1 / 255) {
14146
14168
  renderer2.clearColor(this.backgroundColor);
14147
14169
  }
@@ -15366,6 +15388,32 @@ var RenderState = class {
15366
15388
  }
15367
15389
  this._stackDepth = depth + 1;
15368
15390
  }
15391
+ /**
15392
+ * Inspect the scissor box that the next `restore()` would install,
15393
+ * without mutating any state. Lets renderers detect whether a
15394
+ * pending `restore()` will actually change the scissor (and
15395
+ * decide, e.g., whether to flush GPU work first).
15396
+ *
15397
+ * Returns:
15398
+ * - `Int32Array` (length 4) — `[x, y, width, height]` of the
15399
+ * saved scissor when scissor was active at the matching `save()`
15400
+ * call.
15401
+ * - `null` — when the saved state had scissor disabled, or the
15402
+ * stack is empty. Treat this as "next scissor will be inactive".
15403
+ *
15404
+ * The returned array is a **live reference into the internal
15405
+ * stack** — zero allocation on a hot path. Callers MUST treat it
15406
+ * as read-only; mutating it corrupts subsequent `restore()` calls.
15407
+ * @ignore
15408
+ * @returns {Int32Array | null}
15409
+ */
15410
+ peekScissor() {
15411
+ const depth = this._stackDepth - 1;
15412
+ if (depth < 0 || !this._scissorActive[depth]) {
15413
+ return null;
15414
+ }
15415
+ return this._scissorStack[depth];
15416
+ }
15369
15417
  /**
15370
15418
  * Restore state from the stack.
15371
15419
  * Color, tint, transform, and scissor are restored in place.
@@ -15466,6 +15514,8 @@ var Renderer = class {
15466
15514
  this.maskLevel = 0;
15467
15515
  this.projectionMatrix = new Matrix3d();
15468
15516
  this.uvOffset = 0;
15517
+ this.currentNormalMap = null;
15518
+ this.activeLightCount = 0;
15469
15519
  }
15470
15520
  /**
15471
15521
  * @type {string}
@@ -15686,6 +15736,62 @@ var Renderer = class {
15686
15736
  setBlendMode(mode = "normal") {
15687
15737
  this.currentBlendMode = mode;
15688
15738
  }
15739
+ /**
15740
+ * Upload the active scene lights to the lit sprite pipeline.
15741
+ *
15742
+ * Called once per camera per frame by `Camera2d.draw()` (after the
15743
+ * FBO is bound, before the world tree walk fires `Sprite.draw` for
15744
+ * any normal-mapped sprite). The WebGL renderer overrides this to
15745
+ * pack the lights into the lit shader's uniform buffers; the Canvas
15746
+ * renderer cannot do per-pixel normal-map lighting and silently
15747
+ * ignores the call. The first time a non-empty light list is passed
15748
+ * in Canvas mode, a one-shot console warning is emitted.
15749
+ *
15750
+ * Stage stays renderer-agnostic by passing the raw scene data —
15751
+ * lights iterable and ambient color — and letting the renderer
15752
+ * decide how to encode them.
15753
+ * @param {Iterable<object>} [lights] - active `Light2d` instances; falsy/empty no-ops
15754
+ * @param {object} [ambient] - ambient lighting color (0..255 RGB)
15755
+ * @param {number} [translateX=0] - world-to-screen X translate (matches `Camera2d.draw()`)
15756
+ * @param {number} [translateY=0] - world-to-screen Y translate
15757
+ */
15758
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
15759
+ setLightUniforms(lights, ambient, translateX, translateY) {
15760
+ if (this._litPipelineWarned || !lights) {
15761
+ return;
15762
+ }
15763
+ const hasAny = typeof lights.size === "number" && lights.size > 0 || typeof lights.length === "number" && lights.length > 0 || typeof lights[Symbol.iterator] === "function" && !lights[Symbol.iterator]().next().done;
15764
+ if (hasAny) {
15765
+ this._litPipelineWarned = true;
15766
+ console.warn(
15767
+ "melonJS: Light2d normal-map lighting requires the WebGL renderer; the Canvas fallback renders sprites without per-pixel lighting. Switch to `video.WEBGL` or `video.AUTO` to enable the lit pipeline."
15768
+ );
15769
+ }
15770
+ }
15771
+ /**
15772
+ * Render a `Light2d` instance.
15773
+ *
15774
+ * Each renderer implements its own strategy: the WebGL renderer
15775
+ * draws lights as quads through a shared procedural radial-falloff
15776
+ * fragment shader (no per-light texture, color and intensity
15777
+ * encoded in the per-vertex tint so consecutive draws batch); the
15778
+ * Canvas renderer caches a small `Gradient` config object per
15779
+ * light in a `WeakMap` (rebuilt only when the light's radii /
15780
+ * color / intensity change), rasterizes it with `Gradient.toCanvas()`
15781
+ * into a single shared `CanvasRenderTarget`, and composites the
15782
+ * result via `drawImage`. The base implementation is a no-op so
15783
+ * renderers without a lighting path can be polymorphically
15784
+ * substituted.
15785
+ *
15786
+ * Light2d itself is renderer-agnostic — it just calls
15787
+ * `renderer.drawLight(this)` and relies on the renderer to pick
15788
+ * the right machinery.
15789
+ * @param {object} light - the Light2d instance to render
15790
+ * @see Light2d
15791
+ */
15792
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
15793
+ drawLight(light) {
15794
+ }
15689
15795
  /**
15690
15796
  * Set the current fill & stroke style color.
15691
15797
  * By default, or upon reset, the value is set to #000000.
@@ -16206,7 +16312,7 @@ var TextureAtlas = class {
16206
16312
  /**
16207
16313
  * @param {object|object[]} atlases - atlas information. See {@link loader.getJSON}
16208
16314
  * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|CompressedImage|string|OffscreenCanvas[]|HTMLImageElement[]|HTMLCanvasElement[]|string[]} [src=atlas.meta.image] - Image source
16209
- * @param {boolean} [cache=false] - Use true to skip caching this Texture
16315
+ * @param {boolean|object} [options] - either a boolean (legacy `cache` flag — `false` disables `TextureCache` registration; default behavior is to cache) or an options object `{ cache?: boolean, normalMap?: HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap|string }`. When `normalMap` is provided, the atlas exposes a paired normal-map texture sharing the same UVs as the color texture (used by the WebGL renderer's lit pipeline). `HTMLVideoElement` is intentionally not supported as a normal map (would freeze on frame 0 due to per-image GL texture caching).
16210
16316
  * @example
16211
16317
  * // create a texture atlas from a JSON Object
16212
16318
  * game.texture = new me.TextureAtlas(
@@ -16228,10 +16334,22 @@ var TextureAtlas = class {
16228
16334
  * anchorPoint : new me.Vector2d(0.5, 0.5)
16229
16335
  * },
16230
16336
  * me.loader.getImage("spritesheet")
16337
+ * );
16338
+ *
16339
+ * // SpriteIlluminator workflow: pair the color atlas with its normal map
16340
+ * game.texture = new me.TextureAtlas(
16341
+ * me.loader.getJSON("scene"),
16342
+ * me.loader.getImage("scene"),
16343
+ * { normalMap: me.loader.getImage("scene_n") }
16344
+ * );
16231
16345
  */
16232
- constructor(atlases, src, cache2) {
16346
+ constructor(atlases, src, options) {
16347
+ const opts = typeof options === "boolean" ? { cache: options } : options || {};
16348
+ const cache2 = opts.cache;
16349
+ const normalMap = opts.normalMap;
16233
16350
  this.format = null;
16234
16351
  this.sources = /* @__PURE__ */ new Map();
16352
+ this.normalSources = /* @__PURE__ */ new Map();
16235
16353
  this.atlases = /* @__PURE__ */ new Map();
16236
16354
  this.activeAtlas = void 0;
16237
16355
  this._uvCache = { sx: -1, sy: -1, sw: -1, sh: -1, uvs: null };
@@ -16321,6 +16439,31 @@ var TextureAtlas = class {
16321
16439
  game.renderer.cache.set(source, this);
16322
16440
  });
16323
16441
  }
16442
+ if (typeof normalMap !== "undefined" && normalMap !== null) {
16443
+ let resolved;
16444
+ if (typeof normalMap === "string") {
16445
+ resolved = getImage(normalMap);
16446
+ if (!resolved) {
16447
+ throw new Error(
16448
+ "TextureAtlas: normal map image '" + normalMap + "' not found"
16449
+ );
16450
+ }
16451
+ } else if (typeof normalMap === "object" && typeof normalMap.width === "number" && typeof normalMap.height === "number") {
16452
+ if (typeof normalMap.videoWidth === "number") {
16453
+ throw new TypeError(
16454
+ "TextureAtlas: options.normalMap does not support HTMLVideoElement (the lit pipeline caches the texture per image reference and would freeze on frame 0)"
16455
+ );
16456
+ }
16457
+ resolved = normalMap;
16458
+ } else {
16459
+ throw new TypeError(
16460
+ "TextureAtlas: options.normalMap must be an image-like, a loader key string, or null/undefined; got " + typeof normalMap
16461
+ );
16462
+ }
16463
+ this.sources.forEach((_source, key) => {
16464
+ this.normalSources.set(key, resolved);
16465
+ });
16466
+ }
16324
16467
  }
16325
16468
  /**
16326
16469
  * return the default or specified atlas dictionnary
@@ -16353,6 +16496,22 @@ var TextureAtlas = class {
16353
16496
  return this.sources.get(this.activeAtlas);
16354
16497
  }
16355
16498
  }
16499
+ /**
16500
+ * Return the paired normal-map texture for the given region, or `null`
16501
+ * if no normal map was provided to this atlas. The normal map shares
16502
+ * the same UV layout as the color texture returned by {@link TextureAtlas#getTexture}.
16503
+ * @param {object} [region] - region name in case of multipack textures
16504
+ * @returns {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap|null}
16505
+ */
16506
+ getNormalTexture(region) {
16507
+ if (this.normalSources.size === 0) {
16508
+ return null;
16509
+ }
16510
+ if (typeof region === "object" && typeof region.texture === "string") {
16511
+ return this.normalSources.get(region.texture) ?? null;
16512
+ }
16513
+ return this.normalSources.get(this.activeAtlas) ?? null;
16514
+ }
16356
16515
  /**
16357
16516
  * add a region to the atlas
16358
16517
  * @param {string} name - region mame
@@ -16851,6 +17010,7 @@ var CanvasRenderer = class extends Renderer {
16851
17010
  reset() {
16852
17011
  super.reset();
16853
17012
  this.clearColor(this.currentColor, this.settings.transparent !== true);
17013
+ this._lightCache = void 0;
16854
17014
  }
16855
17015
  /**
16856
17016
  * Reset the canvas transform to identity
@@ -17044,6 +17204,59 @@ var CanvasRenderer = class extends Renderer {
17044
17204
  }
17045
17205
  context.drawImage(source, sx, sy, sw, sh, dx, dy, dw, dh);
17046
17206
  }
17207
+ /**
17208
+ * @inheritdoc
17209
+ *
17210
+ * Renders the light by drawing a cached radial `Gradient` via
17211
+ * `Gradient.toCanvas()`. The Gradient instance is cached per-Light2d
17212
+ * in a `WeakMap` and rebuilt only when the light's radii / color /
17213
+ * intensity change. `toCanvas` itself shares a single
17214
+ * `CanvasRenderTarget` across all gradients in the engine, so memory
17215
+ * stays at O(1) regardless of how many lights are active.
17216
+ *
17217
+ * The cached Gradient is always circular (outer radius =
17218
+ * `max(radiusX, radiusY)`) — `drawImage`'s non-uniform stretch
17219
+ * produces the elliptical falloff for non-square lights, matching
17220
+ * the procedural shader's behavior on WebGL.
17221
+ * @param {object} light - the Light2d instance to render
17222
+ */
17223
+ drawLight(light) {
17224
+ if (this._lightCache === void 0) {
17225
+ this._lightCache = /* @__PURE__ */ new WeakMap();
17226
+ }
17227
+ let entry = this._lightCache.get(light);
17228
+ const c = light.color;
17229
+ if (entry === void 0 || entry.radiusX !== light.radiusX || entry.radiusY !== light.radiusY || entry.r !== c.r || entry.g !== c.g || entry.b !== c.b || entry.intensity !== light.intensity) {
17230
+ const r = Math.max(light.radiusX, light.radiusY);
17231
+ const gradient = this.createRadialGradient(r, r, 0, r, r, r);
17232
+ gradient.addColorStop(0, c.toRGBA(light.intensity));
17233
+ gradient.addColorStop(1, c.toRGBA(0));
17234
+ entry = {
17235
+ gradient,
17236
+ radius: r,
17237
+ radiusX: light.radiusX,
17238
+ radiusY: light.radiusY,
17239
+ r: c.r,
17240
+ g: c.g,
17241
+ b: c.b,
17242
+ intensity: light.intensity
17243
+ };
17244
+ this._lightCache.set(light, entry);
17245
+ }
17246
+ const r2 = entry.radius * 2;
17247
+ const canvas = entry.gradient.toCanvas(this, 0, 0, r2, r2);
17248
+ this.drawImage(
17249
+ canvas,
17250
+ 0,
17251
+ 0,
17252
+ r2,
17253
+ r2,
17254
+ light.pos.x,
17255
+ light.pos.y,
17256
+ light.width,
17257
+ light.height
17258
+ );
17259
+ }
17047
17260
  /**
17048
17261
  * Draw a pattern within the given rectangle.
17049
17262
  * @param {CanvasPattern} pattern - Pattern object
@@ -17778,14 +17991,15 @@ var CanvasRenderer = class extends Renderer {
17778
17991
  if (typeof mask !== "undefined") {
17779
17992
  context.beginPath();
17780
17993
  }
17994
+ this._maskInvertOuterAdded = false;
17781
17995
  }
17782
17996
  if (typeof mask !== "undefined") {
17783
17997
  switch (mask.type) {
17784
17998
  // RoundRect
17785
17999
  case "RoundRect":
17786
18000
  context.roundRect(
17787
- mask.top,
17788
18001
  mask.left,
18002
+ mask.top,
17789
18003
  mask.width,
17790
18004
  mask.height,
17791
18005
  mask.radius
@@ -17794,7 +18008,7 @@ var CanvasRenderer = class extends Renderer {
17794
18008
  // Rect or Bounds
17795
18009
  case "Rectangle":
17796
18010
  case "Bounds":
17797
- context.rect(mask.top, mask.left, mask.width, mask.height);
18011
+ context.rect(mask.left, mask.top, mask.width, mask.height);
17798
18012
  break;
17799
18013
  // Polygon or Line
17800
18014
  case "Polygon":
@@ -17837,7 +18051,10 @@ var CanvasRenderer = class extends Renderer {
17837
18051
  }
17838
18052
  this.maskLevel++;
17839
18053
  if (invert === true) {
17840
- context.rect(0, 0, this.getCanvas().width, this.getCanvas().height);
18054
+ if (this._maskInvertOuterAdded !== true) {
18055
+ context.rect(0, 0, this.getCanvas().width, this.getCanvas().height);
18056
+ this._maskInvertOuterAdded = true;
18057
+ }
17841
18058
  context.clip("evenodd");
17842
18059
  } else {
17843
18060
  context.clip();
@@ -21354,6 +21571,7 @@ var Sprite = class extends Renderable {
21354
21571
  * @param {number} [settings.flipX] - flip the sprite on the horizontal axis
21355
21572
  * @param {number} [settings.flipY] - flip the sprite on the vertical axis
21356
21573
  * @param {Vector2d} [settings.anchorPoint={x:0.5, y:0.5}] - Anchor point to draw the frame at (defaults to the center of the frame).
21574
+ * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap|string} [settings.normalMap] - optional normal-map texture used for per-pixel lighting (SpriteIlluminator-style). Same layout/UVs as `settings.image`. When omitted (default), the sprite renders unlit and pays no extra cost. Ignored by the Canvas renderer. Note: `HTMLVideoElement` is intentionally not supported — normal maps encode static surface directions in RGB, and the engine caches the GL texture per image reference (a video would freeze on frame 0).
21357
21575
  * @example
21358
21576
  * // create a single sprite from a standalone image, with anchor in the center
21359
21577
  * let sprite = new me.Sprite(0, 0, {
@@ -21390,6 +21608,7 @@ var Sprite = class extends Renderable {
21390
21608
  this.offset = vector2dPool.get(0, 0);
21391
21609
  this.isVideo = false;
21392
21610
  this.source = null;
21611
+ this._normalMap = null;
21393
21612
  this.anim = {};
21394
21613
  this.resetAnim = void 0;
21395
21614
  this.current = {
@@ -21463,6 +21682,25 @@ var Sprite = class extends Renderable {
21463
21682
  this.textureAtlas = this.source.getAtlas();
21464
21683
  }
21465
21684
  }
21685
+ if (settings.image instanceof TextureAtlas && typeof settings.image.getNormalTexture === "function") {
21686
+ const fromAtlas = settings.image.getNormalTexture();
21687
+ if (fromAtlas) {
21688
+ this.normalMap = fromAtlas;
21689
+ }
21690
+ }
21691
+ if (this.normalMap === null && typeof settings.normalMap !== "undefined" && settings.normalMap !== null) {
21692
+ if (typeof settings.normalMap === "string") {
21693
+ const resolved = getImage(settings.normalMap);
21694
+ if (!resolved) {
21695
+ throw new Error(
21696
+ "me.Sprite: '" + settings.normalMap + "' normal map image not found!"
21697
+ );
21698
+ }
21699
+ this.normalMap = resolved;
21700
+ } else {
21701
+ this.normalMap = settings.normalMap;
21702
+ }
21703
+ }
21466
21704
  if (typeof settings.atlas !== "undefined") {
21467
21705
  this.textureAtlas = settings.atlas;
21468
21706
  this.atlasIndices = settings.atlasIndices;
@@ -21505,6 +21743,37 @@ var Sprite = class extends Renderable {
21505
21743
  this.setCurrentAnimation("default");
21506
21744
  }
21507
21745
  }
21746
+ /**
21747
+ * The optional normal-map image paired with this sprite's color
21748
+ * texture (SpriteIlluminator workflow). When set, the WebGL
21749
+ * renderer's lit pipeline samples this texture for per-pixel
21750
+ * lighting using `Stage._activeLights`. `null` when unlit.
21751
+ * Setting any non-image value (or anything without numeric
21752
+ * `width`/`height`) throws — assign `null` to clear.
21753
+ *
21754
+ * Silently ignored by the Canvas renderer.
21755
+ * @type {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap|null}
21756
+ */
21757
+ get normalMap() {
21758
+ return this._normalMap;
21759
+ }
21760
+ set normalMap(value) {
21761
+ if (value === null || value === void 0) {
21762
+ this._normalMap = null;
21763
+ return;
21764
+ }
21765
+ if (typeof value !== "object" || typeof value.width !== "number" || typeof value.height !== "number") {
21766
+ throw new TypeError(
21767
+ "Sprite.normalMap must be null or an image-like object with numeric width/height (HTMLImageElement, HTMLCanvasElement, OffscreenCanvas, ImageBitmap)"
21768
+ );
21769
+ }
21770
+ if (typeof value.videoWidth === "number") {
21771
+ throw new TypeError(
21772
+ "Sprite.normalMap does not support HTMLVideoElement (the lit pipeline caches the texture per image reference and would freeze on frame 0)"
21773
+ );
21774
+ }
21775
+ this._normalMap = value;
21776
+ }
21508
21777
  /**
21509
21778
  * return the flickering state of the object
21510
21779
  * @returns {boolean}
@@ -21854,9 +22123,32 @@ var Sprite = class extends Renderable {
21854
22123
  }
21855
22124
  return super.update(dt);
21856
22125
  }
22126
+ /**
22127
+ * Prepare the rendering context before drawing this sprite (automatically called by melonJS).
22128
+ * Extends `Renderable.preDraw` to publish this sprite's `normalMap` (if any)
22129
+ * on the renderer so the WebGL lit pipeline can pair it with the next
22130
+ * `drawImage` call. Cleared back in `postDraw`.
22131
+ * @param {Renderer} renderer - a renderer instance
22132
+ */
22133
+ preDraw(renderer2) {
22134
+ super.preDraw(renderer2);
22135
+ if (this._normalMap !== null) {
22136
+ renderer2.currentNormalMap = this._normalMap;
22137
+ }
22138
+ }
22139
+ /**
22140
+ * restore the rendering context after drawing this sprite (automatically called by melonJS).
22141
+ * @param {Renderer} renderer - a renderer instance
22142
+ */
22143
+ postDraw(renderer2) {
22144
+ if (this._normalMap !== null) {
22145
+ renderer2.currentNormalMap = null;
22146
+ }
22147
+ super.postDraw(renderer2);
22148
+ }
21857
22149
  /**
21858
22150
  * draw this sprite (automatically called by melonJS)
21859
- * @param {CanvasRenderer|WebGLRenderer} renderer - a renderer instance
22151
+ * @param {Renderer} renderer - a renderer instance
21860
22152
  * @param {Camera2d} [viewport] - the viewport to (re)draw
21861
22153
  */
21862
22154
  draw(renderer2) {
@@ -21915,6 +22207,7 @@ var Sprite = class extends Renderable {
21915
22207
  this.image.currentTime = 0;
21916
22208
  }
21917
22209
  this.image = void 0;
22210
+ this._normalMap = null;
21918
22211
  super.destroy();
21919
22212
  }
21920
22213
  };
@@ -22343,8 +22636,6 @@ var GLShader = class {
22343
22636
  stride,
22344
22637
  element.offset
22345
22638
  );
22346
- } else {
22347
- gl.disableVertexAttribArray(index);
22348
22639
  }
22349
22640
  }
22350
22641
  }
@@ -23308,6 +23599,13 @@ var Camera2d = class extends Renderable {
23308
23599
  } else {
23309
23600
  renderer2.setProjection(this.projectionMatrix);
23310
23601
  }
23602
+ const stage = state_default.current();
23603
+ renderer2.setLightUniforms(
23604
+ stage?._activeLights,
23605
+ stage?.ambientLightingColor,
23606
+ translateX,
23607
+ translateY
23608
+ );
23311
23609
  container.preDraw(r);
23312
23610
  if (isNonDefault) {
23313
23611
  const view = this.worldView;
@@ -23324,6 +23622,9 @@ var Camera2d = class extends Renderable {
23324
23622
  this.drawFX(renderer2);
23325
23623
  container.postDraw(r);
23326
23624
  this.postDraw(r);
23625
+ if (stage) {
23626
+ stage.drawLighting(renderer2, this, translateX, translateY);
23627
+ }
23327
23628
  r.endPostEffect(this);
23328
23629
  if (this._colorMatrixEffect) {
23329
23630
  const idx = this.postEffects.indexOf(this._colorMatrixEffect);
@@ -23361,28 +23662,50 @@ var Stage = class {
23361
23662
  cameras;
23362
23663
  /**
23363
23664
  * The list of active lights in this stage.
23364
- * (Note: Canvas Rendering mode will only properly support one light per stage)
23665
+ *
23666
+ * Since 19.3.0, `Light2d` is a first-class world Renderable — the
23667
+ * recommended pattern is to add lights directly to `app.world` (or any
23668
+ * container, including a sprite, so the light follows it via parent
23669
+ * transforms). The `lights` Map remains for backward compatibility:
23670
+ * any entry added via `this.lights.set(name, light)` in
23671
+ * `onResetEvent()` is automatically adopted into the world tree at
23672
+ * stage reset time so it renders normally.
23365
23673
  * @see Light2d
23366
23674
  * @see Stage.ambientLight
23367
23675
  * @example
23368
- * // create a white spot light
23369
- * const whiteLight = new Light2d(0, 0, 140, "#fff", 0.7);
23370
- * // and add the light to this current stage
23676
+ * // recommended:
23677
+ * const whiteLight = new Light2d(100, 100, 140, 140, "#fff", 0.7);
23678
+ * app.world.addChild(whiteLight);
23679
+ *
23680
+ * // legacy (still works, auto-adopted into world):
23371
23681
  * this.lights.set("whiteLight", whiteLight);
23372
- * // set a dark ambient light
23682
+ *
23373
23683
  * this.ambientLight.parseCSS("#1117");
23374
- * // make the light follow the mouse
23375
- * input.registerPointerEvent("pointermove", app.viewport, (event) => {
23376
- * whiteLight.centerOn(event.gameX, event.gameY);
23377
- * });
23378
23684
  */
23379
23685
  lights;
23686
+ /**
23687
+ * Internal set of active lights, auto-populated by `Light2d`'s
23688
+ * `onActivateEvent` / `onDeactivateEvent` hooks. Used by Camera2d's
23689
+ * ambient-overlay pass to compute the cutouts.
23690
+ * @ignore
23691
+ */
23692
+ _activeLights;
23380
23693
  /**
23381
23694
  * an ambient light that will be added to the stage rendering
23382
23695
  * @default "#000000"
23383
23696
  * @see Light2d
23384
23697
  */
23385
23698
  ambientLight;
23699
+ /**
23700
+ * Base light level applied to every normal-mapped sprite in the
23701
+ * lit rendering path. Unlike {@link Stage#ambientLight} (which is
23702
+ * the dark overlay punched by each light's cutout), this color is
23703
+ * added to every lit pixel so unlit areas don't render pure
23704
+ * black. Defaults to black (0, 0, 0) — sprites without a
23705
+ * `normalMap` ignore it entirely.
23706
+ * @default "#000000"
23707
+ */
23708
+ ambientLightingColor;
23386
23709
  /**
23387
23710
  * The given constructor options
23388
23711
  */
@@ -23396,9 +23719,26 @@ var Stage = class {
23396
23719
  constructor(settings) {
23397
23720
  this.cameras = /* @__PURE__ */ new Map();
23398
23721
  this.lights = /* @__PURE__ */ new Map();
23722
+ this._activeLights = /* @__PURE__ */ new Set();
23399
23723
  this.ambientLight = new Color(0, 0, 0, 0);
23724
+ this.ambientLightingColor = new Color(0, 0, 0, 1);
23400
23725
  this.settings = Object.assign({}, default_settings, settings || {});
23401
23726
  }
23727
+ /**
23728
+ * Called by `Light2d.onActivateEvent` to register the light with the
23729
+ * stage's ambient-overlay cutout list. Users normally don't call this.
23730
+ * @ignore
23731
+ */
23732
+ _registerLight(light) {
23733
+ this._activeLights.add(light);
23734
+ }
23735
+ /**
23736
+ * Called by `Light2d.onDeactivateEvent` to deregister the light.
23737
+ * @ignore
23738
+ */
23739
+ _unregisterLight(light) {
23740
+ this._activeLights.delete(light);
23741
+ }
23402
23742
  /**
23403
23743
  * Object reset function
23404
23744
  * @ignore
@@ -23419,6 +23759,13 @@ var Stage = class {
23419
23759
  }
23420
23760
  emit(STAGE_RESET, this);
23421
23761
  this.onResetEvent(app, ...extraArgs);
23762
+ if (app && app.world) {
23763
+ this.lights.forEach((light) => {
23764
+ if (!light.ancestor) {
23765
+ app.world.addChild(light);
23766
+ }
23767
+ });
23768
+ }
23422
23769
  }
23423
23770
  /**
23424
23771
  * update function
@@ -23433,40 +23780,60 @@ var Stage = class {
23433
23780
  isDirty = true;
23434
23781
  }
23435
23782
  });
23436
- this.lights.forEach((light) => {
23437
- if (light.update()) {
23438
- isDirty = true;
23439
- }
23440
- });
23441
23783
  return isDirty;
23442
23784
  }
23443
23785
  /**
23444
23786
  * draw the current stage
23787
+ *
23788
+ * Lights are rendered as part of the world tree (they're now first-class
23789
+ * Renderables) and the ambient overlay pass runs inside each Camera's
23790
+ * post-effect FBO bracket via {@link Stage#drawLighting}.
23445
23791
  * @ignore
23446
23792
  * @param renderer - the renderer object to draw with
23447
23793
  * @param world - the world object to draw
23448
23794
  */
23449
23795
  draw(renderer2, world) {
23450
- const r = renderer2;
23451
23796
  this.cameras.forEach((camera) => {
23452
23797
  camera.draw(renderer2, world);
23453
- if (this.ambientLight.alpha !== 0) {
23454
- r.save();
23455
- this.lights.forEach((light) => {
23456
- r.setMask(light.getVisibleArea(), true);
23457
- });
23458
- r.setColor(this.ambientLight);
23459
- r.fillRect(0, 0, camera.width, camera.height);
23460
- r.clearMask();
23461
- r.restore();
23462
- }
23463
- this.lights.forEach((light) => {
23464
- light.preDraw(r);
23465
- light.draw(r);
23466
- light.postDraw(r);
23467
- });
23468
23798
  });
23469
23799
  }
23800
+ /**
23801
+ * Draw the stage's ambient-light overlay with cutouts for each active
23802
+ * light. Called from each `Camera2d` inside its post-effect FBO bracket —
23803
+ * lights themselves render via the world tree (they're standard
23804
+ * Renderables); this pass only paints the dark fill that the lights cut
23805
+ * holes through.
23806
+ *
23807
+ * Subclasses can override this method to implement custom lighting (e.g.
23808
+ * per-pixel normal-mapped lighting via a custom shader). Called once per
23809
+ * camera per frame.
23810
+ * @param renderer - the active renderer
23811
+ * @param camera - the camera currently rendering this stage
23812
+ * @param translateX - the same world-to-screen X translate that
23813
+ * `Camera2d.draw()` applies to the world container (i.e.
23814
+ * `camera.pos.x + camera.offset.x` for the default camera, plus
23815
+ * the container's own offset for non-default cameras)
23816
+ * @param translateY - the world-to-screen Y translate (see `translateX`)
23817
+ */
23818
+ drawLighting(renderer2, camera, translateX = camera.pos.x + camera.offset.x, translateY = camera.pos.y + camera.offset.y) {
23819
+ if (this.ambientLight.alpha === 0) {
23820
+ return;
23821
+ }
23822
+ const r = renderer2;
23823
+ r.save();
23824
+ const tx = translateX;
23825
+ const ty = translateY;
23826
+ if (tx !== 0 || ty !== 0) {
23827
+ r.translate(-tx, -ty);
23828
+ }
23829
+ this._activeLights.forEach((light) => {
23830
+ r.setMask(light.getVisibleArea(), true);
23831
+ });
23832
+ r.setColor(this.ambientLight);
23833
+ r.fillRect(tx, ty, camera.width, camera.height);
23834
+ r.clearMask();
23835
+ r.restore();
23836
+ }
23470
23837
  /**
23471
23838
  * destroy function
23472
23839
  * @ignore
@@ -23474,9 +23841,12 @@ var Stage = class {
23474
23841
  destroy(app) {
23475
23842
  this.cameras.clear();
23476
23843
  this.lights.forEach((light) => {
23477
- light.destroy();
23844
+ if (!light.ancestor) {
23845
+ light.destroy();
23846
+ }
23478
23847
  });
23479
23848
  this.lights.clear();
23849
+ this._activeLights.clear();
23480
23850
  this.onDestroyEvent(app);
23481
23851
  }
23482
23852
  /**
@@ -26825,64 +27195,33 @@ var ImageLayer = class extends Sprite {
26825
27195
  };
26826
27196
 
26827
27197
  // src/renderable/light2d.js
26828
- function createGradient(light) {
26829
- const context = light.texture.context;
26830
- const x1 = light.texture.width / 2;
26831
- const y1 = light.texture.height / 2;
26832
- const radiusX = light.radiusX;
26833
- const radiusY = light.radiusY;
26834
- let scaleX;
26835
- let scaleY;
26836
- let invScaleX;
26837
- let invScaleY;
26838
- let gradient;
26839
- light.texture.clear();
26840
- if (radiusX >= radiusY) {
26841
- scaleX = 1;
26842
- invScaleX = 1;
26843
- scaleY = radiusY / radiusX;
26844
- invScaleY = radiusX / radiusY;
26845
- gradient = context.createRadialGradient(
26846
- x1,
26847
- y1 * invScaleY,
26848
- 0,
26849
- x1,
26850
- radiusY * invScaleY,
26851
- radiusX
26852
- );
26853
- } else {
26854
- scaleY = 1;
26855
- invScaleY = 1;
26856
- scaleX = radiusX / radiusY;
26857
- invScaleX = radiusY / radiusX;
26858
- gradient = context.createRadialGradient(
26859
- x1 * invScaleX,
26860
- y1,
26861
- 0,
26862
- x1 * invScaleX,
26863
- y1,
26864
- radiusY
26865
- );
26866
- }
26867
- gradient.addColorStop(0, light.color.toRGBA(light.intensity));
26868
- gradient.addColorStop(1, light.color.toRGBA(0));
26869
- context.fillStyle = gradient;
26870
- context.setTransform(scaleX, 0, 0, scaleY, 0, 0);
26871
- context.fillRect(
26872
- 0,
26873
- 0,
26874
- light.texture.width * invScaleX,
26875
- light.texture.height * invScaleY
26876
- );
26877
- }
26878
27198
  var Light2d = class extends Renderable {
26879
27199
  /**
26880
- * @param {number} x - The horizontal position of the light.
26881
- * @param {number} y - The vertical position of the light.
27200
+ * Create a 2D point light.
27201
+ *
27202
+ * A `Light2d` is a first-class world Renderable: add it to a container
27203
+ * with `app.world.addChild(light)` (or any sub-container, including a
27204
+ * `Sprite`, so the light follows the parent via its transform). On
27205
+ * activation, the light auto-registers with the active `Stage`'s
27206
+ * lighting set so the ambient overlay (`Stage.ambientLight`) cuts a
27207
+ * hole at the light's visible area, and a radial gradient from the
27208
+ * given `color` (full intensity at center → fully transparent at the
27209
+ * radius) is composited additively on top — producing a soft spot
27210
+ * light. Rendering happens inside each `Camera2d`'s post-effect FBO
27211
+ * bracket so any camera shader (vignette, color-matrix, scanlines,
27212
+ * etc.) wraps the lighting output.
27213
+ *
27214
+ * Set `radiusY` to a different value than `radiusX` for a stretched
27215
+ * (elliptical) light. The `intensity` parameter scales the gradient's
27216
+ * inner alpha; the `Stage.ambientLight` color and alpha control how
27217
+ * dark the unlit areas are. Use `light.blendMode` to override the
27218
+ * default additive blend if needed.
27219
+ * @param {number} x - The horizontal position of the light's center (matches `Ellipse(x, y, w, h)` conventions).
27220
+ * @param {number} y - The vertical position of the light's center.
26882
27221
  * @param {number} radiusX - The horizontal radius of the light.
26883
27222
  * @param {number} [radiusY=radiusX] - The vertical radius of the light.
26884
- * @param {Color|string} [color="#FFF"] - the color of the light
26885
- * @param {number} [intensity=0.7] - The intensity of the light.
27223
+ * @param {Color|string} [color="#FFF"] - The color of the light at full intensity.
27224
+ * @param {number} [intensity=0.7] - The peak alpha of the radial gradient at the light's center (0–1).
26886
27225
  */
26887
27226
  constructor(x, y, radiusX, radiusY = radiusX, color = "#FFF", intensity = 0.7) {
26888
27227
  super(x, y, radiusX * 2, radiusY * 2);
@@ -26892,32 +27231,77 @@ var Light2d = class extends Renderable {
26892
27231
  this.intensity = intensity;
26893
27232
  this.blendMode = "lighter";
26894
27233
  this.visibleArea = ellipsePool.get(
26895
- this.centerX,
26896
- this.centerY,
27234
+ this.pos.x,
27235
+ this.pos.y,
26897
27236
  this.width,
26898
27237
  this.height
26899
27238
  );
26900
- this.texture = new canvasrendertarget_default(this.width, this.height, {
26901
- offscreenCanvas: false
26902
- });
26903
- this.anchorPoint.set(0, 0);
26904
- createGradient(this);
27239
+ this.anchorPoint.set(0.5, 0.5);
27240
+ this.illuminationOnly = false;
27241
+ this.lightHeight = Math.max(radiusX, radiusY) * 0.075;
26905
27242
  }
26906
27243
  /**
26907
- * returns a geometry representing the visible area of this light
27244
+ * the horizontal coordinate of this light's center.
27245
+ * Overrides Rect's getter, which assumes `pos` is the bbox top-left and
27246
+ * returns `pos.x + width/2`. Light2d uses `anchorPoint = (0.5, 0.5)`, so
27247
+ * `pos` already IS the center.
27248
+ * @type {number}
27249
+ */
27250
+ get centerX() {
27251
+ return this.pos.x;
27252
+ }
27253
+ set centerX(value) {
27254
+ this.pos.x = value;
27255
+ this.recalc();
27256
+ this.updateBounds();
27257
+ }
27258
+ /**
27259
+ * the vertical coordinate of this light's center.
27260
+ * @see Light2d#centerX
27261
+ * @type {number}
27262
+ */
27263
+ get centerY() {
27264
+ return this.pos.y;
27265
+ }
27266
+ set centerY(value) {
27267
+ this.pos.y = value;
27268
+ this.recalc();
27269
+ this.updateBounds();
27270
+ }
27271
+ /**
27272
+ * Set new radii for this light.
27273
+ *
27274
+ * Updates `radiusX`/`radiusY` and the underlying bbox (via
27275
+ * `Renderable.resize(width, height)`) so `getBounds()` and
27276
+ * `getVisibleArea()` — which feed the ambient-cutout pass — track the
27277
+ * new size. The Canvas renderer's gradient cache auto-invalidates on
27278
+ * next draw via its property comparison; the WebGL procedural shader
27279
+ * adapts to the new dimensions automatically.
27280
+ *
27281
+ * Named `setRadii` (not `resize`) so it does not shadow
27282
+ * `Renderable.resize(width, height)` — code that operates on a
27283
+ * generic `Renderable` and calls `.resize(w, h)` keeps working when
27284
+ * the instance happens to be a `Light2d`.
27285
+ * @param {number} radiusX - new horizontal radius
27286
+ * @param {number} [radiusY=radiusX] - new vertical radius
27287
+ */
27288
+ setRadii(radiusX, radiusY = radiusX) {
27289
+ this.radiusX = radiusX;
27290
+ this.radiusY = radiusY;
27291
+ this.resize(radiusX * 2, radiusY * 2);
27292
+ }
27293
+ /**
27294
+ * returns a geometry representing the visible area of this light, in
27295
+ * world-space coordinates (so it aligns with the rendered gradient
27296
+ * regardless of camera scroll or container parenting).
26908
27297
  * @returns {Ellipse} the light visible mask
26909
27298
  */
26910
27299
  getVisibleArea() {
26911
- return this.visibleArea.setShape(
26912
- this.getBounds().centerX,
26913
- this.getBounds().centerY,
26914
- this.width,
26915
- this.height
26916
- );
27300
+ const b = this.getBounds();
27301
+ return this.visibleArea.setShape(b.centerX, b.centerY, b.width, b.height);
26917
27302
  }
26918
27303
  /**
26919
27304
  * update function
26920
- * @param {number} dt - time since the last update in milliseconds.
26921
27305
  * @returns {boolean} true if dirty
26922
27306
  */
26923
27307
  update() {
@@ -26925,25 +27309,50 @@ var Light2d = class extends Renderable {
26925
27309
  }
26926
27310
  /**
26927
27311
  * preDraw this Light2d (automatically called by melonJS)
26928
- * Note: The renderer should set the blend mode again (after drawing other Light2d objects)
26929
- * to ensure colors blend correctly in the CanvasRenderer.
26930
- * @param {CanvasRenderer|WebGLRenderer} renderer - a renderer instance
27312
+ * @param {Renderer} renderer - a renderer instance
26931
27313
  */
26932
27314
  preDraw(renderer2) {
26933
27315
  super.preDraw(renderer2);
26934
27316
  renderer2.setBlendMode(this.blendMode);
26935
27317
  }
26936
27318
  /**
26937
- * draw this Light2d (automatically called by melonJS)
26938
- * @param {CanvasRenderer|WebGLRenderer} renderer - a renderer instance
26939
- * @param {Camera2d} [viewport] - the viewport to (re)draw
27319
+ * draw this Light2d (automatically called by melonJS).
27320
+ *
27321
+ * Delegates to `renderer.drawLight(this)` each renderer picks its
27322
+ * own implementation (procedural shader on WebGL; cached `Gradient`
27323
+ * rasterized into a shared `CanvasRenderTarget` on Canvas). Light2d
27324
+ * itself doesn't know which path is used.
27325
+ * @param {Renderer} renderer - a renderer instance
26940
27326
  */
26941
27327
  draw(renderer2) {
26942
- renderer2.drawImage(
26943
- this.texture.canvas,
26944
- this.getBounds().x,
26945
- this.getBounds().y
26946
- );
27328
+ if (this.illuminationOnly) {
27329
+ return;
27330
+ }
27331
+ renderer2.drawLight(this);
27332
+ }
27333
+ /**
27334
+ * Auto-register this light with the active Stage's lighting set when
27335
+ * added to a container. The Stage uses that set to build the ambient
27336
+ * overlay cutouts; rendering the light itself is handled normally as
27337
+ * part of the world tree walk.
27338
+ * @ignore
27339
+ */
27340
+ onActivateEvent() {
27341
+ const stage = state_default.current();
27342
+ if (stage && typeof stage._registerLight === "function") {
27343
+ stage._registerLight(this);
27344
+ }
27345
+ }
27346
+ /**
27347
+ * Auto-deregister this light from the active Stage's lighting set when
27348
+ * removed from a container.
27349
+ * @ignore
27350
+ */
27351
+ onDeactivateEvent() {
27352
+ const stage = state_default.current();
27353
+ if (stage && typeof stage._unregisterLight === "function") {
27354
+ stage._unregisterLight(this);
27355
+ }
26947
27356
  }
26948
27357
  /**
26949
27358
  * Destroy function<br>
@@ -26952,9 +27361,6 @@ var Light2d = class extends Renderable {
26952
27361
  destroy() {
26953
27362
  colorPool.release(this.color);
26954
27363
  this.color = void 0;
26955
- const renderer2 = this.parentApp?.renderer ?? game.renderer;
26956
- this.texture.destroy(renderer2);
26957
- this.texture = void 0;
26958
27364
  ellipsePool.release(this.visibleArea);
26959
27365
  this.visibleArea = void 0;
26960
27366
  super.destroy();
@@ -27406,7 +27812,7 @@ var Trigger = class extends Renderable {
27406
27812
  };
27407
27813
 
27408
27814
  // src/version.ts
27409
- var version = "19.2.0";
27815
+ var version = "19.3.0";
27410
27816
 
27411
27817
  // src/system/bootstrap.ts
27412
27818
  var initialized = false;
@@ -30008,114 +30414,92 @@ function generateJoinCircles(centers, radius) {
30008
30414
  return verts;
30009
30415
  }
30010
30416
 
30011
- // src/video/webgl/shaders/mesh.frag
30012
- var mesh_default = "uniform sampler2D uSampler;\nvarying vec4 vColor;\nvarying vec2 vRegion;\n\nvoid main(void) {\n gl_FragColor = texture2D(uSampler, vRegion) * vColor;\n}\n";
30013
-
30014
- // src/video/webgl/shaders/mesh.vert
30015
- var mesh_default2 = "// Current vertex point (3D position for mesh rendering)\nattribute vec3 aVertex;\nattribute vec2 aRegion;\nattribute vec4 aColor;\n\n// Projection matrix\nuniform mat4 uProjectionMatrix;\n\nvarying vec2 vRegion;\nvarying vec4 vColor;\n\nvoid main(void) {\n // Transform the vertex position by the projection matrix\n gl_Position = uProjectionMatrix * vec4(aVertex, 1.0);\n // Pass the remaining attributes to the fragment shader\n vColor = vec4(aColor.bgr * aColor.a, aColor.a);\n vRegion = aRegion;\n}\n";
30417
+ // src/video/webgl/lighting/constants.ts
30418
+ var MAX_LIGHTS = 8;
30016
30419
 
30017
- // src/video/buffer/vertex.js
30018
- var VertexArrayBuffer = class {
30019
- constructor(vertexSize, maxVertex) {
30020
- this.vertexSize = vertexSize;
30021
- this.maxVertex = maxVertex;
30022
- this.vertexCount = 0;
30023
- this.buffer = new ArrayBuffer(
30024
- this.maxVertex * this.vertexSize * Float32Array.BYTES_PER_ELEMENT
30025
- );
30026
- this.bufferF32 = new Float32Array(this.buffer);
30027
- this.bufferU32 = new Uint32Array(this.buffer);
30028
- }
30029
- /**
30030
- * clear the vertex array buffer
30031
- * @ignore
30032
- */
30033
- clear() {
30034
- this.vertexCount = 0;
30035
- }
30036
- /**
30037
- * return true if full
30038
- * @ignore
30039
- */
30040
- isFull(vertex) {
30041
- return this.vertexCount + vertex >= this.maxVertex;
30042
- }
30043
- /**
30044
- * push a new vertex to the buffer
30045
- * @param {number} x - x position
30046
- * @param {number} y - y position
30047
- * @param {number} u - texture U coordinate
30048
- * @param {number} v - texture V coordinate
30049
- * @param {number} tint - tint color in UINT32 (argb) format
30050
- * @param {number} [textureId] - texture unit index for multi-texture batching
30051
- * @ignore
30052
- */
30053
- push(x, y, u, v, tint, textureId) {
30054
- const offset = this.vertexCount * this.vertexSize;
30055
- this.bufferF32[offset] = x;
30056
- this.bufferF32[offset + 1] = y;
30057
- this.bufferF32[offset + 2] = u;
30058
- this.bufferF32[offset + 3] = v;
30059
- this.bufferU32[offset + 4] = tint;
30060
- if (this.vertexSize > 5) {
30061
- this.bufferF32[offset + 5] = textureId || 0;
30062
- }
30063
- this.vertexCount++;
30064
- return this;
30065
- }
30066
- /**
30067
- * push a new vertex with all-float data to the buffer
30068
- * @param {ArrayLike<number>} data - float values for one vertex
30069
- * @param {number} srcOffset - start index in the source data
30070
- * @param {number} count - number of floats to copy (should equal vertexSize)
30071
- * @ignore
30072
- */
30073
- pushFloats(data2, srcOffset, count) {
30074
- const offset = this.vertexCount * this.vertexSize;
30075
- for (let i = 0; i < count; i++) {
30076
- this.bufferF32[offset + i] = data2[srcOffset + i];
30077
- }
30078
- this.vertexCount++;
30079
- return this;
30080
- }
30081
- /**
30082
- * push a new vertex to the buffer (mesh format: x, y, z, u, v, tint)
30083
- * @ignore
30084
- */
30085
- pushMesh(x, y, z, u, v, tint) {
30086
- const offset = this.vertexCount * this.vertexSize;
30087
- this.bufferF32[offset] = x;
30088
- this.bufferF32[offset + 1] = y;
30089
- this.bufferF32[offset + 2] = z;
30090
- this.bufferF32[offset + 3] = u;
30091
- this.bufferF32[offset + 4] = v;
30092
- this.bufferU32[offset + 5] = tint;
30093
- this.vertexCount++;
30094
- return this;
30095
- }
30096
- /**
30097
- * return a reference to the data in Float32 format
30098
- * @ignore
30099
- */
30100
- toFloat32(begin, end) {
30101
- if (typeof end !== "undefined") {
30102
- return this.bufferF32.subarray(begin, end);
30420
+ // src/video/webgl/shaders/multitexture-lit.js
30421
+ function buildSamplerSelect(varName, samplerPrefix, count, target) {
30422
+ const lines = [];
30423
+ for (let i = 0; i < count; i++) {
30424
+ if (i === 0) {
30425
+ lines.push(" if (" + varName + " < 0.5) {");
30103
30426
  } else {
30104
- return this.bufferF32;
30427
+ lines.push(" } else if (" + varName + " < " + (i + 0.5) + ") {");
30105
30428
  }
30429
+ lines.push(
30430
+ " " + target + " = texture2D(" + samplerPrefix + i + ", vRegion);"
30431
+ );
30106
30432
  }
30107
- /**
30108
- * return a reference to the data in Uint32 format
30109
- * @ignore
30110
- */
30111
- toUint32(begin, end) {
30112
- if (typeof end !== "undefined") {
30113
- return this.bufferU32.subarray(begin, end);
30114
- } else {
30115
- return this.bufferU32;
30116
- }
30433
+ lines.push(" } else {");
30434
+ lines.push(
30435
+ " " + target + " = texture2D(" + samplerPrefix + "0, vRegion);"
30436
+ );
30437
+ lines.push(" }");
30438
+ return lines;
30439
+ }
30440
+ function buildLitMultiTextureFragment(maxTextures) {
30441
+ const count = Math.max(maxTextures, 1);
30442
+ const lines = [];
30443
+ for (let i = 0; i < count; i++) {
30444
+ lines.push("uniform sampler2D uSampler" + i + ";");
30117
30445
  }
30118
- };
30446
+ for (let i = 0; i < count; i++) {
30447
+ lines.push("uniform sampler2D uNormalSampler" + i + ";");
30448
+ }
30449
+ lines.push("uniform int uLightCount;");
30450
+ lines.push("uniform vec4 uLightPos[" + MAX_LIGHTS + "];");
30451
+ lines.push("uniform vec3 uLightColor[" + MAX_LIGHTS + "];");
30452
+ lines.push("uniform float uLightHeight[" + MAX_LIGHTS + "];");
30453
+ lines.push("uniform vec3 uAmbient;");
30454
+ lines.push("varying vec4 vColor;");
30455
+ lines.push("varying vec2 vRegion;");
30456
+ lines.push("varying float vTextureId;");
30457
+ lines.push("varying float vNormalTextureId;");
30458
+ lines.push("varying vec2 vWorldPos;");
30459
+ lines.push("");
30460
+ lines.push("void main(void) {");
30461
+ lines.push(" vec4 color;");
30462
+ lines.push(...buildSamplerSelect("vTextureId", "uSampler", count, "color"));
30463
+ lines.push(" if (vNormalTextureId < -0.5) {");
30464
+ lines.push(" gl_FragColor = color * vColor;");
30465
+ lines.push(" return;");
30466
+ lines.push(" }");
30467
+ lines.push(" vec4 normalSample;");
30468
+ lines.push(
30469
+ ...buildSamplerSelect(
30470
+ "vNormalTextureId",
30471
+ "uNormalSampler",
30472
+ count,
30473
+ "normalSample"
30474
+ )
30475
+ );
30476
+ lines.push(
30477
+ " vec3 normal = normalize(normalSample.rgb * 2.0 - vec3(1.0));"
30478
+ );
30479
+ lines.push(" normal.y = -normal.y;");
30480
+ lines.push(" vec3 lighting = uAmbient;");
30481
+ lines.push(" for (int i = 0; i < " + MAX_LIGHTS + "; i++) {");
30482
+ lines.push(" if (i >= uLightCount) break;");
30483
+ lines.push(" vec4 lp = uLightPos[i];");
30484
+ lines.push(" vec2 toLight = lp.xy - vWorldPos;");
30485
+ lines.push(" float dist = length(toLight);");
30486
+ lines.push(" float linear = max(0.0, 1.0 - dist / max(lp.z, 1.0));");
30487
+ lines.push(" float att = linear * linear;");
30488
+ lines.push(
30489
+ " vec3 lightDir = normalize(vec3(toLight, uLightHeight[i]));"
30490
+ );
30491
+ lines.push(" float NdotL = max(0.0, dot(normal, lightDir));");
30492
+ lines.push(" lighting += uLightColor[i] * (lp.w * att * NdotL);");
30493
+ lines.push(" }");
30494
+ lines.push(
30495
+ " gl_FragColor = vec4(color.rgb * lighting, color.a) * vColor;"
30496
+ );
30497
+ lines.push("}");
30498
+ return lines.join("\n");
30499
+ }
30500
+
30501
+ // src/video/webgl/shaders/quad-multi-lit.vert
30502
+ var quad_multi_lit_default = "// Lit-aware vertex shader used by `LitQuadBatcher` (the SpriteIlluminator\n// path). Carries a paired `aNormalTextureId` per vertex so the fragment\n// shader knows which `uNormalSampler<n>` to read, and `vWorldPos` so the\n// lit math can compute per-fragment `lightPos - pos` deltas.\nattribute vec2 aVertex;\nattribute vec2 aRegion;\nattribute vec4 aColor;\nattribute float aTextureId;\nattribute float aNormalTextureId;\n\nuniform mat4 uProjectionMatrix;\n\nvarying vec2 vRegion;\nvarying vec4 vColor;\nvarying float vTextureId;\nvarying float vNormalTextureId;\n// Pre-projection vertex position (in the renderer's pre-projection\n// space \u2014 typically camera-local for default cameras with the world\n// container's translate applied). Used by the lit fragment path to\n// compute `lightPos - fragmentPos` for each Light2d.\nvarying vec2 vWorldPos;\n\nvoid main(void) {\n gl_Position = uProjectionMatrix * vec4(aVertex, 0.0, 1.0);\n vColor = vec4(aColor.bgr * aColor.a, aColor.a);\n vRegion = aRegion;\n vTextureId = aTextureId;\n vNormalTextureId = aNormalTextureId;\n vWorldPos = aVertex;\n}\n";
30119
30503
 
30120
30504
  // src/video/buffer/index.js
30121
30505
  var IndexBuffer = class {
@@ -30216,6 +30600,145 @@ var WebGLIndexBuffer = class extends IndexBuffer {
30216
30600
  }
30217
30601
  };
30218
30602
 
30603
+ // src/video/webgl/shaders/multitexture.js
30604
+ function buildMultiTextureFragment(maxTextures) {
30605
+ const count = Math.max(maxTextures, 1);
30606
+ const lines = [];
30607
+ for (let i = 0; i < count; i++) {
30608
+ lines.push("uniform sampler2D uSampler" + i + ";");
30609
+ }
30610
+ lines.push("varying vec4 vColor;");
30611
+ lines.push("varying vec2 vRegion;");
30612
+ lines.push("varying float vTextureId;");
30613
+ lines.push("");
30614
+ lines.push("void main(void) {");
30615
+ lines.push(" vec4 color;");
30616
+ for (let i = 0; i < count; i++) {
30617
+ if (i === 0) {
30618
+ lines.push(" if (vTextureId < 0.5) {");
30619
+ } else {
30620
+ lines.push(" } else if (vTextureId < " + (i + 0.5) + ") {");
30621
+ }
30622
+ lines.push(" color = texture2D(uSampler" + i + ", vRegion);");
30623
+ }
30624
+ lines.push(" } else {");
30625
+ lines.push(" color = texture2D(uSampler0, vRegion);");
30626
+ lines.push(" }");
30627
+ lines.push(" gl_FragColor = color * vColor;");
30628
+ lines.push("}");
30629
+ return lines.join("\n");
30630
+ }
30631
+
30632
+ // src/video/webgl/shaders/quad-multi.vert
30633
+ var quad_multi_default = "// Current vertex point\nattribute vec2 aVertex;\nattribute vec2 aRegion;\nattribute vec4 aColor;\nattribute float aTextureId;\n\n// Projection matrix\nuniform mat4 uProjectionMatrix;\n\nvarying vec2 vRegion;\nvarying vec4 vColor;\nvarying float vTextureId;\n\nvoid main(void) {\n // Transform the vertex position by the projection matrix\n gl_Position = uProjectionMatrix * vec4(aVertex, 0.0, 1.0);\n // Pass the remaining attributes to the fragment shader\n vColor = vec4(aColor.bgr * aColor.a, aColor.a);\n vRegion = aRegion;\n vTextureId = aTextureId;\n}\n";
30634
+
30635
+ // src/video/buffer/vertex.js
30636
+ var VertexArrayBuffer = class {
30637
+ constructor(vertexSize, maxVertex) {
30638
+ this.vertexSize = vertexSize;
30639
+ this.maxVertex = maxVertex;
30640
+ this.vertexCount = 0;
30641
+ this.buffer = new ArrayBuffer(
30642
+ this.maxVertex * this.vertexSize * Float32Array.BYTES_PER_ELEMENT
30643
+ );
30644
+ this.bufferF32 = new Float32Array(this.buffer);
30645
+ this.bufferU32 = new Uint32Array(this.buffer);
30646
+ }
30647
+ /**
30648
+ * clear the vertex array buffer
30649
+ * @ignore
30650
+ */
30651
+ clear() {
30652
+ this.vertexCount = 0;
30653
+ }
30654
+ /**
30655
+ * return true if full
30656
+ * @ignore
30657
+ */
30658
+ isFull(vertex) {
30659
+ return this.vertexCount + vertex >= this.maxVertex;
30660
+ }
30661
+ /**
30662
+ * push a new vertex to the buffer
30663
+ * @param {number} x - x position
30664
+ * @param {number} y - y position
30665
+ * @param {number} u - texture U coordinate
30666
+ * @param {number} v - texture V coordinate
30667
+ * @param {number} tint - tint color in UINT32 (argb) format
30668
+ * @param {number} [textureId] - texture unit index for multi-texture batching
30669
+ * @param {number} [normalTextureId] - paired normal-map texture unit index, or `-1` for unlit quads
30670
+ * @ignore
30671
+ */
30672
+ push(x, y, u, v, tint, textureId, normalTextureId) {
30673
+ const offset = this.vertexCount * this.vertexSize;
30674
+ this.bufferF32[offset] = x;
30675
+ this.bufferF32[offset + 1] = y;
30676
+ this.bufferF32[offset + 2] = u;
30677
+ this.bufferF32[offset + 3] = v;
30678
+ this.bufferU32[offset + 4] = tint;
30679
+ if (this.vertexSize > 5) {
30680
+ this.bufferF32[offset + 5] = textureId || 0;
30681
+ if (this.vertexSize > 6) {
30682
+ this.bufferF32[offset + 6] = typeof normalTextureId === "number" ? normalTextureId : -1;
30683
+ }
30684
+ }
30685
+ this.vertexCount++;
30686
+ return this;
30687
+ }
30688
+ /**
30689
+ * push a new vertex with all-float data to the buffer
30690
+ * @param {ArrayLike<number>} data - float values for one vertex
30691
+ * @param {number} srcOffset - start index in the source data
30692
+ * @param {number} count - number of floats to copy (should equal vertexSize)
30693
+ * @ignore
30694
+ */
30695
+ pushFloats(data2, srcOffset, count) {
30696
+ const offset = this.vertexCount * this.vertexSize;
30697
+ for (let i = 0; i < count; i++) {
30698
+ this.bufferF32[offset + i] = data2[srcOffset + i];
30699
+ }
30700
+ this.vertexCount++;
30701
+ return this;
30702
+ }
30703
+ /**
30704
+ * push a new vertex to the buffer (mesh format: x, y, z, u, v, tint)
30705
+ * @ignore
30706
+ */
30707
+ pushMesh(x, y, z, u, v, tint) {
30708
+ const offset = this.vertexCount * this.vertexSize;
30709
+ this.bufferF32[offset] = x;
30710
+ this.bufferF32[offset + 1] = y;
30711
+ this.bufferF32[offset + 2] = z;
30712
+ this.bufferF32[offset + 3] = u;
30713
+ this.bufferF32[offset + 4] = v;
30714
+ this.bufferU32[offset + 5] = tint;
30715
+ this.vertexCount++;
30716
+ return this;
30717
+ }
30718
+ /**
30719
+ * return a reference to the data in Float32 format
30720
+ * @ignore
30721
+ */
30722
+ toFloat32(begin, end) {
30723
+ if (typeof end !== "undefined") {
30724
+ return this.bufferF32.subarray(begin, end);
30725
+ } else {
30726
+ return this.bufferF32;
30727
+ }
30728
+ }
30729
+ /**
30730
+ * return a reference to the data in Uint32 format
30731
+ * @ignore
30732
+ */
30733
+ toUint32(begin, end) {
30734
+ if (typeof end !== "undefined") {
30735
+ return this.bufferU32.subarray(begin, end);
30736
+ } else {
30737
+ return this.bufferU32;
30738
+ }
30739
+ }
30740
+ };
30741
+
30219
30742
  // src/video/webgl/batchers/batcher.js
30220
30743
  var DEFAULT_MAX_VERTICES = 4096;
30221
30744
  var Batcher = class {
@@ -30314,6 +30837,25 @@ var Batcher = class {
30314
30837
  this.useShader(this.defaultShader);
30315
30838
  }
30316
30839
  }
30840
+ /**
30841
+ * called by the WebGL renderer when this batcher is being replaced by another.
30842
+ * Disables this batcher's vertex attribute locations so they don't leak across
30843
+ * (otherwise stale stride/offset state can cause INVALID_OPERATION on the next draw).
30844
+ */
30845
+ unbind() {
30846
+ if (this.currentShader === void 0) {
30847
+ return;
30848
+ }
30849
+ const gl = this.gl;
30850
+ for (let i = 0; i < this.attributes.length; ++i) {
30851
+ const location = this.currentShader.getAttribLocation(
30852
+ this.attributes[i].name
30853
+ );
30854
+ if (location !== -1) {
30855
+ gl.disableVertexAttribArray(location);
30856
+ }
30857
+ }
30858
+ }
30317
30859
  /**
30318
30860
  * Select the shader to use for compositing
30319
30861
  * @see GLShader
@@ -30322,6 +30864,9 @@ var Batcher = class {
30322
30864
  useShader(shader) {
30323
30865
  if (this.currentShader !== shader || this.renderer.currentProgram !== shader.program) {
30324
30866
  this.flush();
30867
+ if (this.currentShader && this.currentShader !== shader) {
30868
+ this.unbind();
30869
+ }
30325
30870
  shader.bind();
30326
30871
  shader.setUniform(this.projectionUniform, this.renderer.projectionMatrix);
30327
30872
  shader.setVertexAttributes(this.gl, this.attributes, this.stride);
@@ -30667,6 +31212,514 @@ var MaterialBatcher = class extends Batcher {
30667
31212
  }
30668
31213
  };
30669
31214
 
31215
+ // src/video/webgl/batchers/quad_batcher.js
31216
+ var V_ARRAY = [
31217
+ new Vector2d(),
31218
+ new Vector2d(),
31219
+ new Vector2d(),
31220
+ new Vector2d()
31221
+ ];
31222
+ var QuadBatcher = class extends MaterialBatcher {
31223
+ /**
31224
+ * Initialize the compositor
31225
+ * @ignore
31226
+ */
31227
+ init(renderer2) {
31228
+ this.maxBatchTextures = Math.min(renderer2.maxTextures, 16);
31229
+ super.init(renderer2, {
31230
+ attributes: [
31231
+ {
31232
+ name: "aVertex",
31233
+ size: 2,
31234
+ type: renderer2.gl.FLOAT,
31235
+ normalized: false,
31236
+ offset: 0 * Float32Array.BYTES_PER_ELEMENT
31237
+ },
31238
+ {
31239
+ name: "aRegion",
31240
+ size: 2,
31241
+ type: renderer2.gl.FLOAT,
31242
+ normalized: false,
31243
+ offset: 2 * Float32Array.BYTES_PER_ELEMENT
31244
+ },
31245
+ {
31246
+ name: "aColor",
31247
+ size: 4,
31248
+ type: renderer2.gl.UNSIGNED_BYTE,
31249
+ normalized: true,
31250
+ offset: 4 * Float32Array.BYTES_PER_ELEMENT
31251
+ },
31252
+ {
31253
+ name: "aTextureId",
31254
+ size: 1,
31255
+ type: renderer2.gl.FLOAT,
31256
+ normalized: false,
31257
+ offset: 5 * Float32Array.BYTES_PER_ELEMENT
31258
+ }
31259
+ ],
31260
+ shader: {
31261
+ vertex: quad_multi_default,
31262
+ fragment: buildMultiTextureFragment(this.maxBatchTextures)
31263
+ }
31264
+ });
31265
+ this.bindColorSamplers();
31266
+ this.useMultiTexture = true;
31267
+ this.createIndexBuffer();
31268
+ }
31269
+ /**
31270
+ * (Re-)create the index buffer for quad batching (4 verts + 6 indices per quad).
31271
+ * Called from `init` and `reset` (after context loss).
31272
+ * @ignore
31273
+ */
31274
+ createIndexBuffer() {
31275
+ const maxQuads = this.vertexData.maxVertex / 4;
31276
+ this.indexBuffer = new WebGLIndexBuffer(
31277
+ this.gl,
31278
+ maxQuads * 6,
31279
+ this.renderer.WebGLVersion > 1
31280
+ );
31281
+ this.indexBuffer.fillQuadPattern(maxQuads);
31282
+ }
31283
+ /**
31284
+ * Bind the color sampler uniforms (`uSampler0..uSamplerN-1`) to their
31285
+ * respective texture units. Called from `init` and `reset`.
31286
+ * @ignore
31287
+ */
31288
+ bindColorSamplers() {
31289
+ for (let i = 0; i < this.maxBatchTextures; i++) {
31290
+ this.defaultShader.setUniform("uSampler" + i, i);
31291
+ }
31292
+ }
31293
+ /**
31294
+ * Select the shader to use for compositing.
31295
+ * Multi-texture batching is automatically enabled when the default
31296
+ * shader is active, and disabled for custom ShaderEffect shaders.
31297
+ * @see GLShader
31298
+ * @see ShaderEffect
31299
+ * @param {GLShader|ShaderEffect} shader - a reference to a GLShader or ShaderEffect instance
31300
+ */
31301
+ useShader(shader) {
31302
+ super.useShader(shader);
31303
+ this.useMultiTexture = shader === this.defaultShader;
31304
+ }
31305
+ /**
31306
+ * Reset compositor internal state
31307
+ * @ignore
31308
+ */
31309
+ reset() {
31310
+ super.reset();
31311
+ this.createIndexBuffer();
31312
+ this.bindColorSamplers();
31313
+ this.useMultiTexture = true;
31314
+ }
31315
+ /**
31316
+ * Flush batched texture data to the GPU using indexed drawing.
31317
+ * @param {number} [mode=gl.TRIANGLES] - the GL drawing mode
31318
+ */
31319
+ flush(mode = this.mode) {
31320
+ const vertex = this.vertexData;
31321
+ const vertexCount = vertex.vertexCount;
31322
+ if (vertexCount > 0) {
31323
+ const gl = this.gl;
31324
+ const vertexSize = vertex.vertexSize;
31325
+ this.indexBuffer.bind();
31326
+ if (this.renderer.WebGLVersion > 1) {
31327
+ gl.bufferData(
31328
+ gl.ARRAY_BUFFER,
31329
+ vertex.toFloat32(),
31330
+ gl.STREAM_DRAW,
31331
+ 0,
31332
+ vertexCount * vertexSize
31333
+ );
31334
+ } else {
31335
+ gl.bufferData(
31336
+ gl.ARRAY_BUFFER,
31337
+ vertex.toFloat32(0, vertexCount * vertexSize),
31338
+ gl.STREAM_DRAW
31339
+ );
31340
+ }
31341
+ const indexCount = vertexCount / 4 * 6;
31342
+ gl.drawElements(mode, indexCount, this.indexBuffer.type, 0);
31343
+ vertex.clear();
31344
+ }
31345
+ }
31346
+ /**
31347
+ * Draw a screen-aligned quad with the given raw WebGL texture through the given shader.
31348
+ * Binds the texture to unit 0, pushes 4 vertices (Y-flipped UVs), flushes,
31349
+ * then unbinds the texture.
31350
+ * @param {WebGLTexture} source - the raw GL texture to blit
31351
+ * @param {number} x - destination x
31352
+ * @param {number} y - destination y
31353
+ * @param {number} width - destination width
31354
+ * @param {number} height - destination height
31355
+ * @param {GLShader|ShaderEffect} shader - the shader effect to apply
31356
+ */
31357
+ blitTexture(source, x, y, width, height, shader) {
31358
+ const gl = this.gl;
31359
+ this.useShader(shader);
31360
+ gl.activeTexture(gl.TEXTURE0);
31361
+ gl.bindTexture(gl.TEXTURE_2D, source);
31362
+ this.currentTextureUnit = 0;
31363
+ this.boundTextures[0] = source;
31364
+ shader.setUniform("uSampler", 0);
31365
+ const m = this.viewMatrix;
31366
+ const vec0 = V_ARRAY[0].set(x, y);
31367
+ const vec1 = V_ARRAY[1].set(x + width, y);
31368
+ const vec2 = V_ARRAY[2].set(x, y + height);
31369
+ const vec3 = V_ARRAY[3].set(x + width, y + height);
31370
+ if (m && !m.isIdentity()) {
31371
+ m.apply(vec0);
31372
+ m.apply(vec1);
31373
+ m.apply(vec2);
31374
+ m.apply(vec3);
31375
+ }
31376
+ const tint = 4294967295;
31377
+ this.vertexData.push(vec0.x, vec0.y, 0, 1, tint, 0);
31378
+ this.vertexData.push(vec1.x, vec1.y, 1, 1, tint, 0);
31379
+ this.vertexData.push(vec2.x, vec2.y, 0, 0, tint, 0);
31380
+ this.vertexData.push(vec3.x, vec3.y, 1, 0, tint, 0);
31381
+ this.flush();
31382
+ gl.activeTexture(gl.TEXTURE0);
31383
+ gl.bindTexture(gl.TEXTURE_2D, null);
31384
+ this.currentTextureUnit = -1;
31385
+ delete this.boundTextures[0];
31386
+ this.useShader(this.defaultShader);
31387
+ }
31388
+ /**
31389
+ * Add a textured quad
31390
+ * @param {TextureAtlas} texture - Source texture atlas
31391
+ * @param {number} x - Destination x-coordinate
31392
+ * @param {number} y - Destination y-coordinate
31393
+ * @param {number} w - Destination width
31394
+ * @param {number} h - Destination height
31395
+ * @param {number} u0 - Texture UV (u0) value.
31396
+ * @param {number} v0 - Texture UV (v0) value.
31397
+ * @param {number} u1 - Texture UV (u1) value.
31398
+ * @param {number} v1 - Texture UV (v1) value.
31399
+ * @param {number} tint - tint color to be applied to the texture in UINT32 (argb) format
31400
+ * @param {boolean} [reupload=false] - Force the texture to be reuploaded even if already bound
31401
+ */
31402
+ addQuad(texture, x, y, w, h, u0, v0, u1, v1, tint, reupload = false) {
31403
+ const vertexData = this.vertexData;
31404
+ if (vertexData.isFull(4)) {
31405
+ this.flush();
31406
+ }
31407
+ let unit;
31408
+ if (this.useMultiTexture) {
31409
+ unit = this.uploadTexture(texture, w, h, reupload, false);
31410
+ if (unit >= this.maxBatchTextures) {
31411
+ this.flush();
31412
+ this.renderer.cache.resetUnitAssignments();
31413
+ unit = this.uploadTexture(texture, w, h, reupload, false);
31414
+ }
31415
+ } else {
31416
+ unit = this.uploadTexture(texture, w, h, reupload);
31417
+ if (unit !== this.currentSamplerUnit) {
31418
+ this.currentShader.setUniform("uSampler", unit);
31419
+ this.currentSamplerUnit = unit;
31420
+ }
31421
+ }
31422
+ const m = this.viewMatrix;
31423
+ const vec0 = V_ARRAY[0].set(x, y);
31424
+ const vec1 = V_ARRAY[1].set(x + w, y);
31425
+ const vec2 = V_ARRAY[2].set(x, y + h);
31426
+ const vec3 = V_ARRAY[3].set(x + w, y + h);
31427
+ if (!m.isIdentity()) {
31428
+ m.apply(vec0);
31429
+ m.apply(vec1);
31430
+ m.apply(vec2);
31431
+ m.apply(vec3);
31432
+ }
31433
+ const textureId = this.useMultiTexture ? unit : 0;
31434
+ vertexData.push(vec0.x, vec0.y, u0, v0, tint, textureId);
31435
+ vertexData.push(vec1.x, vec1.y, u1, v0, tint, textureId);
31436
+ vertexData.push(vec2.x, vec2.y, u0, v1, tint, textureId);
31437
+ vertexData.push(vec3.x, vec3.y, u1, v1, tint, textureId);
31438
+ }
31439
+ };
31440
+
31441
+ // src/video/webgl/batchers/lit_quad_batcher.js
31442
+ var LitQuadBatcher = class extends QuadBatcher {
31443
+ /**
31444
+ * @ignore
31445
+ */
31446
+ init(renderer2) {
31447
+ const halved = Math.min(
31448
+ Math.max(1, Math.floor(renderer2.maxTextures / 2)),
31449
+ 16
31450
+ );
31451
+ this.maxBatchTextures = halved;
31452
+ Object.getPrototypeOf(QuadBatcher.prototype).init.call(this, renderer2, {
31453
+ attributes: [
31454
+ {
31455
+ name: "aVertex",
31456
+ size: 2,
31457
+ type: renderer2.gl.FLOAT,
31458
+ normalized: false,
31459
+ offset: 0 * Float32Array.BYTES_PER_ELEMENT
31460
+ },
31461
+ {
31462
+ name: "aRegion",
31463
+ size: 2,
31464
+ type: renderer2.gl.FLOAT,
31465
+ normalized: false,
31466
+ offset: 2 * Float32Array.BYTES_PER_ELEMENT
31467
+ },
31468
+ {
31469
+ name: "aColor",
31470
+ size: 4,
31471
+ type: renderer2.gl.UNSIGNED_BYTE,
31472
+ normalized: true,
31473
+ offset: 4 * Float32Array.BYTES_PER_ELEMENT
31474
+ },
31475
+ {
31476
+ name: "aTextureId",
31477
+ size: 1,
31478
+ type: renderer2.gl.FLOAT,
31479
+ normalized: false,
31480
+ offset: 5 * Float32Array.BYTES_PER_ELEMENT
31481
+ },
31482
+ {
31483
+ name: "aNormalTextureId",
31484
+ size: 1,
31485
+ type: renderer2.gl.FLOAT,
31486
+ normalized: false,
31487
+ offset: 6 * Float32Array.BYTES_PER_ELEMENT
31488
+ }
31489
+ ],
31490
+ shader: {
31491
+ vertex: quad_multi_lit_default,
31492
+ fragment: buildLitMultiTextureFragment(halved)
31493
+ }
31494
+ });
31495
+ this.bindColorSamplers();
31496
+ this.bindNormalSamplers();
31497
+ this.createIndexBuffer();
31498
+ this.useMultiTexture = true;
31499
+ this.boundNormalMaps = new Array(halved).fill(null);
31500
+ this.normalMapTextures = /* @__PURE__ */ new Map();
31501
+ this._lightCount = 0;
31502
+ this._maxLights = MAX_LIGHTS;
31503
+ this.defaultShader.setUniform("uLightCount", 0);
31504
+ this.defaultShader.setUniform("uAmbient", [0, 0, 0]);
31505
+ }
31506
+ /**
31507
+ * Bind the paired normal sampler uniforms (`uNormalSampler0..N-1`)
31508
+ * to texture units `maxBatchTextures..2*maxBatchTextures-1`. Called
31509
+ * from `init` and `reset`.
31510
+ * @ignore
31511
+ */
31512
+ bindNormalSamplers() {
31513
+ for (let i = 0; i < this.maxBatchTextures; i++) {
31514
+ this.defaultShader.setUniform(
31515
+ "uNormalSampler" + i,
31516
+ this.maxBatchTextures + i
31517
+ );
31518
+ }
31519
+ }
31520
+ /**
31521
+ * @ignore
31522
+ */
31523
+ reset() {
31524
+ super.reset();
31525
+ this.bindNormalSamplers();
31526
+ this.boundNormalMaps.fill(null);
31527
+ this.normalMapTextures.clear();
31528
+ this._lightCount = 0;
31529
+ this.defaultShader.setUniform("uLightCount", 0);
31530
+ this.defaultShader.setUniform("uAmbient", [0, 0, 0]);
31531
+ }
31532
+ /**
31533
+ * Upload per-frame Light2d uniforms used by the lit fragment path.
31534
+ * Called once per camera per frame (before the world tree walk).
31535
+ * Lights past `MAX_LIGHTS` are silently ignored.
31536
+ *
31537
+ * Coordinates must be supplied in the same space as the renderer's
31538
+ * pre-projection vertex coords (i.e. camera-local / FBO-local),
31539
+ * matching `Stage.drawLighting`'s convention.
31540
+ * @param {object} uniforms
31541
+ * @param {Float32Array} uniforms.positions - flat array of `[x, y, radius, intensity]` per light, length = 4 * count
31542
+ * @param {Float32Array} uniforms.colors - flat array of `[r, g, b]` per light, length = 3 * count
31543
+ * @param {Float32Array} [uniforms.heights] - flat array of per-light height, length = MAX_LIGHTS
31544
+ * @param {number} uniforms.count - number of lights to render (clamped to MAX_LIGHTS)
31545
+ * @param {number[]} [uniforms.ambient] - `[r, g, b]` ambient floor (0..1 each)
31546
+ */
31547
+ setLightUniforms(uniforms) {
31548
+ const shader = this.defaultShader;
31549
+ const count = Math.min(uniforms.count | 0, this._maxLights);
31550
+ this._lightCount = count;
31551
+ shader.setUniform("uLightCount", count);
31552
+ if (count > 0) {
31553
+ shader.setUniform("uLightPos", uniforms.positions);
31554
+ shader.setUniform("uLightColor", uniforms.colors);
31555
+ if (uniforms.heights) {
31556
+ shader.setUniform("uLightHeight", uniforms.heights);
31557
+ }
31558
+ }
31559
+ if (uniforms.ambient) {
31560
+ shader.setUniform("uAmbient", uniforms.ambient);
31561
+ }
31562
+ }
31563
+ /**
31564
+ * Bind a normal-map image to the given GL texture unit. Uploads on
31565
+ * first use (via `uploadNormalMap`) and rebinds the cached
31566
+ * `WebGLTexture` on subsequent calls. Mirrors the
31567
+ * `bindTexture2D` / `createTexture2D` split used by `MaterialBatcher`,
31568
+ * but for normal-map textures which live outside the color
31569
+ * `TextureCache` (cached per-image in `normalMapTextures`).
31570
+ * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} image - normal-map source
31571
+ * @param {number} unit - GL texture unit (already offset by `maxBatchTextures`)
31572
+ */
31573
+ bindNormalMap(image, unit) {
31574
+ const cached = this.normalMapTextures.get(image);
31575
+ if (typeof cached !== "undefined") {
31576
+ this.bindTexture2D(cached, unit, false);
31577
+ return;
31578
+ }
31579
+ this.uploadNormalMap(image, unit);
31580
+ }
31581
+ /**
31582
+ * Upload a normal-map image to GL and cache the resulting `WebGLTexture`
31583
+ * for future `bindNormalMap` calls. Not meant to be called directly —
31584
+ * `bindNormalMap` invokes this on the first use of a given image.
31585
+ *
31586
+ * `premultipliedAlpha = false` — normal maps store linear-encoded
31587
+ * surface normals; multiplying through alpha would corrupt the
31588
+ * encoding for any non-opaque texel.
31589
+ * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} image - normal-map source
31590
+ * @param {number} unit - GL texture unit (already offset by `maxBatchTextures`)
31591
+ */
31592
+ uploadNormalMap(image, unit) {
31593
+ const gl = this.gl;
31594
+ this.createTexture2D(
31595
+ unit,
31596
+ image,
31597
+ this.renderer.settings.antiAlias ? gl.LINEAR : gl.NEAREST,
31598
+ "no-repeat",
31599
+ image.width,
31600
+ image.height,
31601
+ false,
31602
+ void 0,
31603
+ void 0,
31604
+ false
31605
+ );
31606
+ this.normalMapTextures.set(image, this.boundTextures[unit]);
31607
+ }
31608
+ /**
31609
+ * Add a textured quad with optional paired normal map.
31610
+ * @param {TextureAtlas} texture - Source texture atlas
31611
+ * @param {number} x - Destination x-coordinate
31612
+ * @param {number} y - Destination y-coordinate
31613
+ * @param {number} w - Destination width
31614
+ * @param {number} h - Destination height
31615
+ * @param {number} u0 - Texture UV (u0) value
31616
+ * @param {number} v0 - Texture UV (v0) value
31617
+ * @param {number} u1 - Texture UV (u1) value
31618
+ * @param {number} v1 - Texture UV (v1) value
31619
+ * @param {number} tint - tint color (UINT32 argb)
31620
+ * @param {boolean} [reupload=false] - Force the texture to be reuploaded
31621
+ * @param {HTMLImageElement|HTMLCanvasElement|null} [normalMap=null] - paired normal-map (SpriteIlluminator workflow)
31622
+ */
31623
+ addQuad(texture, x, y, w, h, u0, v0, u1, v1, tint, reupload = false, normalMap = null) {
31624
+ const vertexData = this.vertexData;
31625
+ if (vertexData.isFull(4)) {
31626
+ this.flush();
31627
+ }
31628
+ let unit;
31629
+ if (this.useMultiTexture) {
31630
+ unit = this.uploadTexture(texture, w, h, reupload, false);
31631
+ if (unit >= this.maxBatchTextures) {
31632
+ this.flush();
31633
+ this.renderer.cache.resetUnitAssignments();
31634
+ this.boundNormalMaps.fill(null);
31635
+ unit = this.uploadTexture(texture, w, h, reupload, false);
31636
+ }
31637
+ } else {
31638
+ unit = this.uploadTexture(texture, w, h, reupload);
31639
+ if (unit !== this.currentSamplerUnit) {
31640
+ this.currentShader.setUniform("uSampler", unit);
31641
+ this.currentSamplerUnit = unit;
31642
+ }
31643
+ }
31644
+ let normalTextureId = -1;
31645
+ if (normalMap !== null && this.useMultiTexture) {
31646
+ const normalUnit = this.maxBatchTextures + unit;
31647
+ const prev = this.boundNormalMaps[unit];
31648
+ if (prev !== normalMap) {
31649
+ if (prev !== null) {
31650
+ this.flush();
31651
+ }
31652
+ this.bindNormalMap(normalMap, normalUnit);
31653
+ this.boundNormalMaps[unit] = normalMap;
31654
+ }
31655
+ normalTextureId = unit;
31656
+ }
31657
+ const m = this.viewMatrix;
31658
+ const vec0 = V_ARRAY[0].set(x, y);
31659
+ const vec1 = V_ARRAY[1].set(x + w, y);
31660
+ const vec2 = V_ARRAY[2].set(x, y + h);
31661
+ const vec3 = V_ARRAY[3].set(x + w, y + h);
31662
+ if (!m.isIdentity()) {
31663
+ m.apply(vec0);
31664
+ m.apply(vec1);
31665
+ m.apply(vec2);
31666
+ m.apply(vec3);
31667
+ }
31668
+ const textureId = this.useMultiTexture ? unit : 0;
31669
+ vertexData.push(vec0.x, vec0.y, u0, v0, tint, textureId, normalTextureId);
31670
+ vertexData.push(vec1.x, vec1.y, u1, v0, tint, textureId, normalTextureId);
31671
+ vertexData.push(vec2.x, vec2.y, u0, v1, tint, textureId, normalTextureId);
31672
+ vertexData.push(vec3.x, vec3.y, u1, v1, tint, textureId, normalTextureId);
31673
+ }
31674
+ /**
31675
+ * Override `blitTexture` so the FBO blit pushes `-1` as the unlit
31676
+ * sentinel (this batcher's vertex layout includes `aNormalTextureId`).
31677
+ * @param {WebGLTexture} source - the raw GL texture to blit
31678
+ * @param {number} x - destination x
31679
+ * @param {number} y - destination y
31680
+ * @param {number} width - destination width
31681
+ * @param {number} height - destination height
31682
+ * @param {GLShader|ShaderEffect} shader - the shader effect to apply
31683
+ */
31684
+ blitTexture(source, x, y, width, height, shader) {
31685
+ const gl = this.gl;
31686
+ this.useShader(shader);
31687
+ gl.activeTexture(gl.TEXTURE0);
31688
+ gl.bindTexture(gl.TEXTURE_2D, source);
31689
+ this.currentTextureUnit = 0;
31690
+ this.boundTextures[0] = source;
31691
+ shader.setUniform("uSampler", 0);
31692
+ const m = this.viewMatrix;
31693
+ const vec0 = V_ARRAY[0].set(x, y);
31694
+ const vec1 = V_ARRAY[1].set(x + width, y);
31695
+ const vec2 = V_ARRAY[2].set(x, y + height);
31696
+ const vec3 = V_ARRAY[3].set(x + width, y + height);
31697
+ if (m && !m.isIdentity()) {
31698
+ m.apply(vec0);
31699
+ m.apply(vec1);
31700
+ m.apply(vec2);
31701
+ m.apply(vec3);
31702
+ }
31703
+ const tint = 4294967295;
31704
+ this.vertexData.push(vec0.x, vec0.y, 0, 1, tint, 0, -1);
31705
+ this.vertexData.push(vec1.x, vec1.y, 1, 1, tint, 0, -1);
31706
+ this.vertexData.push(vec2.x, vec2.y, 0, 0, tint, 0, -1);
31707
+ this.vertexData.push(vec3.x, vec3.y, 1, 0, tint, 0, -1);
31708
+ this.flush();
31709
+ gl.activeTexture(gl.TEXTURE0);
31710
+ gl.bindTexture(gl.TEXTURE_2D, null);
31711
+ this.currentTextureUnit = -1;
31712
+ delete this.boundTextures[0];
31713
+ this.useShader(this.defaultShader);
31714
+ }
31715
+ };
31716
+
31717
+ // src/video/webgl/shaders/mesh.frag
31718
+ var mesh_default = "uniform sampler2D uSampler;\nvarying vec4 vColor;\nvarying vec2 vRegion;\n\nvoid main(void) {\n gl_FragColor = texture2D(uSampler, vRegion) * vColor;\n}\n";
31719
+
31720
+ // src/video/webgl/shaders/mesh.vert
31721
+ var mesh_default2 = "// Current vertex point (3D position for mesh rendering)\nattribute vec3 aVertex;\nattribute vec2 aRegion;\nattribute vec4 aColor;\n\n// Projection matrix\nuniform mat4 uProjectionMatrix;\n\nvarying vec2 vRegion;\nvarying vec4 vColor;\n\nvoid main(void) {\n // Transform the vertex position by the projection matrix\n gl_Position = uProjectionMatrix * vec4(aVertex, 1.0);\n // Pass the remaining attributes to the fragment shader\n vColor = vec4(aColor.bgr * aColor.a, aColor.a);\n vRegion = aRegion;\n}\n";
31722
+
30670
31723
  // src/video/webgl/batchers/mesh_batcher.js
30671
31724
  var _v = new Vector2d();
30672
31725
  var MeshBatcher = class extends MaterialBatcher {
@@ -30940,241 +31993,122 @@ var PrimitiveBatcher = class extends Batcher {
30940
31993
  }
30941
31994
  };
30942
31995
 
30943
- // src/video/webgl/shaders/multitexture.js
30944
- function buildMultiTextureFragment(maxTextures) {
30945
- const count = Math.max(maxTextures, 1);
30946
- const lines = [];
30947
- for (let i = 0; i < count; i++) {
30948
- lines.push("uniform sampler2D uSampler" + i + ";");
30949
- }
30950
- lines.push("varying vec4 vColor;");
30951
- lines.push("varying vec2 vRegion;");
30952
- lines.push("varying float vTextureId;");
30953
- lines.push("");
30954
- lines.push("void main(void) {");
30955
- lines.push(" vec4 color;");
30956
- for (let i = 0; i < count; i++) {
30957
- if (i === 0) {
30958
- lines.push(" if (vTextureId < 0.5) {");
30959
- } else {
30960
- lines.push(" } else if (vTextureId < " + (i + 0.5) + ") {");
30961
- }
30962
- lines.push(" color = texture2D(uSampler" + i + ", vRegion);");
30963
- }
30964
- lines.push(" } else {");
30965
- lines.push(" color = texture2D(uSampler0, vRegion);");
30966
- lines.push(" }");
30967
- lines.push(" gl_FragColor = color * vColor;");
30968
- lines.push("}");
30969
- return lines.join("\n");
30970
- }
30971
-
30972
- // src/video/webgl/shaders/quad-multi.vert
30973
- var quad_multi_default = "// Current vertex point\nattribute vec2 aVertex;\nattribute vec2 aRegion;\nattribute vec4 aColor;\nattribute float aTextureId;\n\n// Projection matrix\nuniform mat4 uProjectionMatrix;\n\nvarying vec2 vRegion;\nvarying vec4 vColor;\nvarying float vTextureId;\n\nvoid main(void) {\n // Transform the vertex position by the projection matrix\n gl_Position = uProjectionMatrix * vec4(aVertex, 0.0, 1.0);\n // Pass the remaining attributes to the fragment shader\n vColor = vec4(aColor.bgr * aColor.a, aColor.a);\n vRegion = aRegion;\n vTextureId = aTextureId;\n}\n";
30974
-
30975
- // src/video/webgl/batchers/quad_batcher.js
30976
- var V_ARRAY = [
30977
- new Vector2d(),
30978
- new Vector2d(),
30979
- new Vector2d(),
30980
- new Vector2d()
30981
- ];
30982
- var QuadBatcher = class extends MaterialBatcher {
31996
+ // src/video/webgl/effects/radialGradient.js
31997
+ var RadialGradientEffect = class extends ShaderEffect {
30983
31998
  /**
30984
- * Initialize the compositor
30985
- * @ignore
31999
+ * @param {WebGLRenderer} renderer - the current renderer instance
32000
+ * @param {object} [options] - initial uniform values
32001
+ * @param {Color} [options.color] - center color (0..255 RGB); defaults to white
32002
+ * @param {number} [options.intensity=1] - peak alpha at the center (0..1+)
30986
32003
  */
30987
- init(renderer2) {
30988
- this.maxBatchTextures = Math.min(renderer2.maxTextures, 16);
30989
- super.init(renderer2, {
30990
- attributes: [
30991
- {
30992
- name: "aVertex",
30993
- size: 2,
30994
- type: renderer2.gl.FLOAT,
30995
- normalized: false,
30996
- offset: 0 * Float32Array.BYTES_PER_ELEMENT
30997
- },
30998
- {
30999
- name: "aRegion",
31000
- size: 2,
31001
- type: renderer2.gl.FLOAT,
31002
- normalized: false,
31003
- offset: 2 * Float32Array.BYTES_PER_ELEMENT
31004
- },
31005
- {
31006
- name: "aColor",
31007
- size: 4,
31008
- type: renderer2.gl.UNSIGNED_BYTE,
31009
- normalized: true,
31010
- offset: 4 * Float32Array.BYTES_PER_ELEMENT
31011
- },
31012
- {
31013
- name: "aTextureId",
31014
- size: 1,
31015
- type: renderer2.gl.FLOAT,
31016
- normalized: false,
31017
- offset: 5 * Float32Array.BYTES_PER_ELEMENT
31018
- }
31019
- ],
31020
- shader: {
31021
- vertex: quad_multi_default,
31022
- fragment: buildMultiTextureFragment(this.maxBatchTextures)
31023
- }
31024
- });
31025
- for (let i = 0; i < this.maxBatchTextures; i++) {
31026
- this.defaultShader.setUniform("uSampler" + i, i);
31027
- }
31028
- this.useMultiTexture = true;
31029
- const maxQuads = this.vertexData.maxVertex / 4;
31030
- this.indexBuffer = new WebGLIndexBuffer(
31031
- this.gl,
31032
- maxQuads * 6,
31033
- this.renderer.WebGLVersion > 1
31034
- );
31035
- this.indexBuffer.fillQuadPattern(maxQuads);
31036
- }
31037
- /**
31038
- * Select the shader to use for compositing.
31039
- * Multi-texture batching is automatically enabled when the default
31040
- * shader is active, and disabled for custom ShaderEffect shaders.
31041
- * @see GLShader
31042
- * @see ShaderEffect
31043
- * @param {GLShader|ShaderEffect} shader - a reference to a GLShader or ShaderEffect instance
31044
- */
31045
- useShader(shader) {
31046
- super.useShader(shader);
31047
- this.useMultiTexture = shader === this.defaultShader;
31048
- }
31049
- /**
31050
- * Reset compositor internal state
31051
- * @ignore
31052
- */
31053
- reset() {
31054
- super.reset();
31055
- const maxQuads = this.vertexData.maxVertex / 4;
31056
- this.indexBuffer = new WebGLIndexBuffer(
31057
- this.gl,
31058
- maxQuads * 6,
31059
- this.renderer.WebGLVersion > 1
32004
+ constructor(renderer2, options = {}) {
32005
+ super(
32006
+ renderer2,
32007
+ `
32008
+ uniform vec3 uColor;
32009
+ uniform float uIntensity;
32010
+ vec4 apply(vec4 color, vec2 uv) {
32011
+ // recenter to [-1, 1] across the quad. The quad's own aspect
32012
+ // ratio handles elliptical falloffs naturally \u2014 length(c) == 1
32013
+ // lies on the inscribed ellipse in world space.
32014
+ vec2 c = uv * 2.0 - 1.0;
32015
+ float d = length(c);
32016
+ // linear ramp matches Canvas createRadialGradient's two-stop output
32017
+ float f = clamp(1.0 - d, 0.0, 1.0);
32018
+ // 'color' is the per-vertex tint, already premultiplied by
32019
+ // alpha in the vertex shader (vColor = vec4(aColor.bgr *
32020
+ // aColor.a, aColor.a)). For standalone use the tint is
32021
+ // (1,1,1,1) and the uniforms drive the look; for the Light2d
32022
+ // batching path the uniforms stay at default and the tint
32023
+ // carries the per-light color + intensity.
32024
+ vec3 rgb = color.rgb * uColor * uIntensity * f;
32025
+ float a = color.a * uIntensity * f;
32026
+ return vec4(rgb, a);
32027
+ }
32028
+ `
31060
32029
  );
31061
- this.indexBuffer.fillQuadPattern(maxQuads);
31062
- for (let i = 0; i < this.maxBatchTextures; i++) {
31063
- this.defaultShader.setUniform("uSampler" + i, i);
32030
+ this._colorBuf = new Float32Array(3);
32031
+ const color = options.color;
32032
+ if (color) {
32033
+ this.setColor(color);
32034
+ } else {
32035
+ this._colorBuf[0] = 1;
32036
+ this._colorBuf[1] = 1;
32037
+ this._colorBuf[2] = 1;
32038
+ this.setUniform("uColor", this._colorBuf);
31064
32039
  }
31065
- this.useMultiTexture = true;
32040
+ this.setIntensity(options.intensity ?? 1);
31066
32041
  }
31067
32042
  /**
31068
- * Flush batched texture data to the GPU using indexed drawing.
31069
- * @param {number} [mode=gl.TRIANGLES] - the GL drawing mode
32043
+ * Set the center color. RGB only alpha is ignored (the radial
32044
+ * falloff supplies the per-pixel alpha).
32045
+ * @param {Color} color - 0..255 RGB color
31070
32046
  */
31071
- flush(mode = this.mode) {
31072
- const vertex = this.vertexData;
31073
- const vertexCount = vertex.vertexCount;
31074
- if (vertexCount > 0) {
31075
- const gl = this.gl;
31076
- const vertexSize = vertex.vertexSize;
31077
- this.indexBuffer.bind();
31078
- if (this.renderer.WebGLVersion > 1) {
31079
- gl.bufferData(
31080
- gl.ARRAY_BUFFER,
31081
- vertex.toFloat32(),
31082
- gl.STREAM_DRAW,
31083
- 0,
31084
- vertexCount * vertexSize
31085
- );
31086
- } else {
31087
- gl.bufferData(
31088
- gl.ARRAY_BUFFER,
31089
- vertex.toFloat32(0, vertexCount * vertexSize),
31090
- gl.STREAM_DRAW
31091
- );
31092
- }
31093
- const indexCount = vertexCount / 4 * 6;
31094
- gl.drawElements(mode, indexCount, this.indexBuffer.type, 0);
31095
- vertex.clear();
31096
- }
32047
+ setColor(color) {
32048
+ this._colorBuf[0] = color.r / 255;
32049
+ this._colorBuf[1] = color.g / 255;
32050
+ this._colorBuf[2] = color.b / 255;
32051
+ this.setUniform("uColor", this._colorBuf);
31097
32052
  }
31098
32053
  /**
31099
- * Draw a screen-aligned quad with the given raw WebGL texture through the given shader.
31100
- * Binds the texture to unit 0, pushes 4 vertices (Y-flipped UVs), flushes,
31101
- * then unbinds the texture.
31102
- * @param {WebGLTexture} source - the raw GL texture to blit
31103
- * @param {number} x - destination x
31104
- * @param {number} y - destination y
31105
- * @param {number} width - destination width
31106
- * @param {number} height - destination height
31107
- * @param {GLShader|ShaderEffect} shader - the shader effect to apply
32054
+ * Set the peak intensity. Acts as a brightness multiplier on the
32055
+ * falloff curve; values above 1 over-saturate the center of the gradient.
32056
+ * @param {number} intensity - 0..1+ multiplier
31108
32057
  */
31109
- blitTexture(source, x, y, width, height, shader) {
31110
- const gl = this.gl;
31111
- this.useShader(shader);
31112
- gl.activeTexture(gl.TEXTURE0);
31113
- gl.bindTexture(gl.TEXTURE_2D, source);
31114
- shader.setUniform("uSampler", 0);
31115
- const tint = 4294967295;
31116
- this.vertexData.push(x, y, 0, 1, tint, 0);
31117
- this.vertexData.push(x + width, y, 1, 1, tint, 0);
31118
- this.vertexData.push(x, y + height, 0, 0, tint, 0);
31119
- this.vertexData.push(x + width, y + height, 1, 0, tint, 0);
31120
- this.flush();
31121
- gl.activeTexture(gl.TEXTURE0);
31122
- gl.bindTexture(gl.TEXTURE_2D, null);
31123
- delete this.boundTextures[0];
31124
- this.useShader(this.defaultShader);
32058
+ setIntensity(intensity) {
32059
+ this.setUniform("uIntensity", intensity);
31125
32060
  }
31126
- /**
31127
- * Add a textured quad
31128
- * @param {TextureAtlas} texture - Source texture atlas
31129
- * @param {number} x - Destination x-coordinate
31130
- * @param {number} y - Destination y-coordinate
31131
- * @param {number} w - Destination width
31132
- * @param {number} h - Destination height
31133
- * @param {number} u0 - Texture UV (u0) value.
31134
- * @param {number} v0 - Texture UV (v0) value.
31135
- * @param {number} u1 - Texture UV (u1) value.
31136
- * @param {number} v1 - Texture UV (v1) value.
31137
- * @param {number} tint - tint color to be applied to the texture in UINT32 (argb) format
31138
- * @param {boolean} reupload - Force the texture to be reuploaded even if already bound
31139
- */
31140
- addQuad(texture, x, y, w, h, u0, v0, u1, v1, tint, reupload = false) {
31141
- const vertexData = this.vertexData;
31142
- if (vertexData.isFull(4)) {
31143
- this.flush();
31144
- }
31145
- let unit;
31146
- if (this.useMultiTexture) {
31147
- unit = this.uploadTexture(texture, w, h, reupload, false);
31148
- if (unit >= this.maxBatchTextures) {
31149
- this.flush();
31150
- this.renderer.cache.resetUnitAssignments();
31151
- unit = this.uploadTexture(texture, w, h, reupload, false);
31152
- }
31153
- } else {
31154
- unit = this.uploadTexture(texture, w, h, reupload);
31155
- if (unit !== this.currentSamplerUnit) {
31156
- this.currentShader.setUniform("uSampler", unit);
31157
- this.currentSamplerUnit = unit;
32061
+ };
32062
+
32063
+ // src/video/webgl/lighting/pack.ts
32064
+ function createLightUniformScratch() {
32065
+ return {
32066
+ positions: new Float32Array(MAX_LIGHTS * 4),
32067
+ colors: new Float32Array(MAX_LIGHTS * 3),
32068
+ heights: new Float32Array(MAX_LIGHTS),
32069
+ ambient: [0, 0, 0]
32070
+ };
32071
+ }
32072
+ function packLights(lights, ambient, translateX, translateY, scratch) {
32073
+ scratch.positions.fill(0);
32074
+ scratch.colors.fill(0);
32075
+ scratch.heights.fill(0);
32076
+ let i = 0;
32077
+ if (lights) {
32078
+ for (const light of lights) {
32079
+ if (i >= MAX_LIGHTS) {
32080
+ break;
31158
32081
  }
32082
+ const b = light.getBounds();
32083
+ const radius = Math.max(b.width, b.height) / 2;
32084
+ scratch.positions[i * 4 + 0] = b.centerX - translateX;
32085
+ scratch.positions[i * 4 + 1] = b.centerY - translateY;
32086
+ scratch.positions[i * 4 + 2] = radius;
32087
+ scratch.positions[i * 4 + 3] = light.intensity;
32088
+ scratch.colors[i * 3 + 0] = light.color.r / 255;
32089
+ scratch.colors[i * 3 + 1] = light.color.g / 255;
32090
+ scratch.colors[i * 3 + 2] = light.color.b / 255;
32091
+ scratch.heights[i] = light.lightHeight;
32092
+ i++;
31159
32093
  }
31160
- const m = this.viewMatrix;
31161
- const vec0 = V_ARRAY[0].set(x, y);
31162
- const vec1 = V_ARRAY[1].set(x + w, y);
31163
- const vec2 = V_ARRAY[2].set(x, y + h);
31164
- const vec3 = V_ARRAY[3].set(x + w, y + h);
31165
- if (!m.isIdentity()) {
31166
- m.apply(vec0);
31167
- m.apply(vec1);
31168
- m.apply(vec2);
31169
- m.apply(vec3);
31170
- }
31171
- const textureId = this.useMultiTexture ? unit : 0;
31172
- vertexData.push(vec0.x, vec0.y, u0, v0, tint, textureId);
31173
- vertexData.push(vec1.x, vec1.y, u1, v0, tint, textureId);
31174
- vertexData.push(vec2.x, vec2.y, u0, v1, tint, textureId);
31175
- vertexData.push(vec3.x, vec3.y, u1, v1, tint, textureId);
31176
32094
  }
31177
- };
32095
+ if (ambient) {
32096
+ scratch.ambient[0] = ambient.r / 255;
32097
+ scratch.ambient[1] = ambient.g / 255;
32098
+ scratch.ambient[2] = ambient.b / 255;
32099
+ } else {
32100
+ scratch.ambient[0] = 0;
32101
+ scratch.ambient[1] = 0;
32102
+ scratch.ambient[2] = 0;
32103
+ }
32104
+ return {
32105
+ positions: scratch.positions,
32106
+ colors: scratch.colors,
32107
+ heights: scratch.heights,
32108
+ ambient: scratch.ambient,
32109
+ count: i
32110
+ };
32111
+ }
31178
32112
 
31179
32113
  // src/video/webgl/webgl_renderer.js
31180
32114
  var _tempMatrix = new Matrix3d();
@@ -31205,6 +32139,7 @@ var WebGLRenderer = class extends Renderer {
31205
32139
  this._rectTriangles = Array.from({ length: 6 }, () => {
31206
32140
  return { x: 0, y: 0 };
31207
32141
  });
32142
+ this._clipAABB = new Bounds();
31208
32143
  this._polyVerts = [];
31209
32144
  this._currentGradient = null;
31210
32145
  this.currentTransform = this.renderState.currentTransform;
@@ -31214,6 +32149,9 @@ var WebGLRenderer = class extends Renderer {
31214
32149
  this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
31215
32150
  const CustomBatcher = this.settings.batcher || this.settings.compositor;
31216
32151
  this.addBatcher(new (CustomBatcher || QuadBatcher)(this), "quad", true);
32152
+ if (!CustomBatcher) {
32153
+ this.addBatcher(new LitQuadBatcher(this), "litQuad");
32154
+ }
31217
32155
  this.addBatcher(new (CustomBatcher || PrimitiveBatcher)(this), "primitive");
31218
32156
  this.addBatcher(new MeshBatcher(this), "mesh");
31219
32157
  this.gl.disable(this.gl.DEPTH_TEST);
@@ -31332,6 +32270,16 @@ var WebGLRenderer = class extends Renderer {
31332
32270
  this.gl.disable(this.gl.SCISSOR_TEST);
31333
32271
  this._scissorActive = false;
31334
32272
  this._renderTargetPool.destroy();
32273
+ if (this._lightShader !== void 0) {
32274
+ this._lightShader.destroy?.();
32275
+ this._lightShader = void 0;
32276
+ }
32277
+ if (this._lightAtlas !== void 0) {
32278
+ this._lightAtlas.sources.forEach((source) => {
32279
+ this.cache.delete?.(source);
32280
+ });
32281
+ this._lightAtlas = void 0;
32282
+ }
31335
32283
  }
31336
32284
  /**
31337
32285
  * add a new batcher to this renderer
@@ -31359,21 +32307,21 @@ var WebGLRenderer = class extends Renderer {
31359
32307
  if (typeof batcher === "undefined") {
31360
32308
  throw new Error("Invalid Batcher");
31361
32309
  }
31362
- if (this.currentBatcher === batcher && typeof shader !== "object") {
32310
+ const targetShader = shader != null ? shader : batcher.defaultShader;
32311
+ if (this.currentBatcher === batcher && batcher.currentShader === targetShader) {
31363
32312
  return this.currentBatcher;
31364
32313
  }
31365
32314
  if (this.currentBatcher !== batcher) {
31366
32315
  if (this.currentBatcher !== void 0) {
31367
32316
  this.currentBatcher.flush();
32317
+ this.currentBatcher.unbind();
31368
32318
  }
31369
32319
  this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
31370
32320
  this.currentBatcher = batcher;
31371
32321
  this.currentBatcher.bind();
31372
32322
  this.currentBatcher.setProjection(this.projectionMatrix);
31373
32323
  }
31374
- if (typeof shader === "object") {
31375
- this.currentBatcher.useShader(shader);
31376
- }
32324
+ this.currentBatcher.useShader(targetShader);
31377
32325
  return this.currentBatcher;
31378
32326
  }
31379
32327
  /**
@@ -31418,6 +32366,109 @@ var WebGLRenderer = class extends Renderer {
31418
32366
  flush() {
31419
32367
  this.currentBatcher.flush();
31420
32368
  }
32369
+ /**
32370
+ * Upload per-frame Light2d uniforms used by the lit sprite pipeline.
32371
+ *
32372
+ * Packs the active lights into pre-allocated scratch buffers, then
32373
+ * forwards to `LitQuadBatcher`. Light positions are translated from
32374
+ * world-space (where `light.getBounds().centerX/Y` lives) into the
32375
+ * renderer's pre-projection coords by subtracting `(translateX, translateY)`,
32376
+ * matching what `Stage.drawLighting` does for the cutout pass — so
32377
+ * the lit fragment's `lightPos - vWorldPos` math lines up with the
32378
+ * camera's view.
32379
+ *
32380
+ * Lights past `MAX_LIGHTS` (8) are silently dropped. Also caches the
32381
+ * active light count on the renderer so `drawImage` can dispatch
32382
+ * normal-mapped sprites to the lit batcher only when there's
32383
+ * something to light them with.
32384
+ * @param {Iterable<object>} [lights] - active `Light2d` instances; falsy/empty no-ops the lit pipeline
32385
+ * @param {object} [ambient] - ambient lighting color (0..255 RGB); defaults to black
32386
+ * @param {number} [translateX=0] - world-to-screen X translate (matches `Camera2d.draw()`)
32387
+ * @param {number} [translateY=0] - world-to-screen Y translate
32388
+ */
32389
+ setLightUniforms(lights, ambient, translateX = 0, translateY = 0) {
32390
+ if (this._lightUniformsScratch === void 0) {
32391
+ this._lightUniformsScratch = createLightUniformScratch();
32392
+ }
32393
+ const u = packLights(
32394
+ lights,
32395
+ ambient,
32396
+ translateX,
32397
+ translateY,
32398
+ this._lightUniformsScratch
32399
+ );
32400
+ this.activeLightCount = u.count;
32401
+ const lit = this.batchers.get("litQuad");
32402
+ if (lit && typeof lit.setLightUniforms === "function") {
32403
+ lit.setLightUniforms(u);
32404
+ if (this.currentBatcher && this.currentBatcher !== lit) {
32405
+ const shader = this.currentBatcher.currentShader || this.currentBatcher.defaultShader;
32406
+ if (shader) {
32407
+ this.gl.useProgram(shader.program);
32408
+ this.currentProgram = shader.program;
32409
+ }
32410
+ }
32411
+ }
32412
+ }
32413
+ /**
32414
+ * @inheritdoc
32415
+ *
32416
+ * Renders the light as a quad through a shared
32417
+ * {@link RadialGradientEffect} fragment shader (procedural — no
32418
+ * per-light texture). The shader and a shared 1×1 white-pixel atlas
32419
+ * are lazy-allocated on first call and reused for every Light2d on
32420
+ * this renderer. Each light's color and intensity are encoded into
32421
+ * the per-vertex tint so consecutive `drawLight` calls accumulate
32422
+ * into the quad batcher's buffer and flush together — N lights
32423
+ * become 1 program switch + 1 flush instead of 2N + N.
32424
+ * @param {object} light - the Light2d instance to render
32425
+ */
32426
+ drawLight(light) {
32427
+ if (this._lightShader === void 0) {
32428
+ this._lightShader = new RadialGradientEffect(this);
32429
+ }
32430
+ const batcher = this.setBatcher("quad", this._lightShader);
32431
+ batcher.addQuad(
32432
+ this._getLightAtlas(),
32433
+ light.pos.x,
32434
+ light.pos.y,
32435
+ light.width,
32436
+ light.height,
32437
+ 0,
32438
+ 0,
32439
+ 1,
32440
+ 1,
32441
+ // pack the light's color (RGB) and intensity (A) into the
32442
+ // vertex tint — the shader's `apply()` reads `color.rgb` and
32443
+ // `color.a` as the per-light values.
32444
+ light.color.toUint32(light.intensity)
32445
+ );
32446
+ }
32447
+ /**
32448
+ * Lazy-init a shared 1×1 white `TextureAtlas` used as the source
32449
+ * texture for `drawLight`'s procedural shader. The shader ignores
32450
+ * the sampled color, but `addQuad`'s vertex format includes a
32451
+ * texture-unit attribute so we still need a real texture; sharing
32452
+ * one across every light keeps them on the same multi-texture slot
32453
+ * (no flush on light switch).
32454
+ * @returns {TextureAtlas}
32455
+ * @ignore
32456
+ */
32457
+ _getLightAtlas() {
32458
+ if (this._lightAtlas === void 0) {
32459
+ const canvas = globalThis.document ? globalThis.document.createElement("canvas") : new OffscreenCanvas(1, 1);
32460
+ canvas.width = 1;
32461
+ canvas.height = 1;
32462
+ const ctx = canvas.getContext("2d");
32463
+ ctx.fillStyle = "#fff";
32464
+ ctx.fillRect(0, 0, 1, 1);
32465
+ this._lightAtlas = new TextureAtlas(
32466
+ createAtlas(1, 1, "lightWhite", "no-repeat"),
32467
+ canvas
32468
+ );
32469
+ }
32470
+ return this._lightAtlas;
32471
+ }
31421
32472
  /**
31422
32473
  * Begin capturing rendering to an offscreen FBO for post-effect processing.
31423
32474
  * @param {Renderable} renderable - the renderable requesting post-effect processing
@@ -31602,19 +32653,22 @@ var WebGLRenderer = class extends Renderer {
31602
32653
  */
31603
32654
  enableScissor(x, y, width, height) {
31604
32655
  const gl = this.gl;
32656
+ const canvas = this.getCanvas();
32657
+ const aabb = this._clipAABB;
32658
+ aabb.clear();
32659
+ aabb.addFrame(x, y, x + width, y + height, this.currentTransform);
32660
+ const sx = Math.floor(aabb.min.x);
32661
+ const sy = Math.floor(aabb.min.y);
32662
+ const sw = Math.ceil(aabb.max.x - sx);
32663
+ const sh = Math.ceil(aabb.max.y - sy);
31605
32664
  this.flush();
31606
32665
  gl.enable(gl.SCISSOR_TEST);
31607
32666
  this._scissorActive = true;
31608
- gl.scissor(
31609
- x + this.currentTransform.tx,
31610
- this.getCanvas().height - height - y - this.currentTransform.ty,
31611
- width,
31612
- height
31613
- );
31614
- this.currentScissor[0] = x;
31615
- this.currentScissor[1] = y;
31616
- this.currentScissor[2] = width;
31617
- this.currentScissor[3] = height;
32667
+ gl.scissor(sx, canvas.height - sh - sy, sw, sh);
32668
+ this.currentScissor[0] = sx;
32669
+ this.currentScissor[1] = sy;
32670
+ this.currentScissor[2] = sw;
32671
+ this.currentScissor[3] = sh;
31618
32672
  }
31619
32673
  /**
31620
32674
  * Disable the scissor test, allowing rendering to the full viewport.
@@ -31721,27 +32775,45 @@ var WebGLRenderer = class extends Renderer {
31721
32775
  dx |= 0;
31722
32776
  dy |= 0;
31723
32777
  }
31724
- this.setBatcher("quad");
32778
+ const useLit = this.batchers.has("litQuad") && this.activeLightCount > 0 && this.currentNormalMap !== null;
32779
+ this.setBatcher(useLit ? "litQuad" : "quad");
31725
32780
  const shader = this.customShader;
31726
- if (typeof shader === "object") {
32781
+ if (shader != null) {
31727
32782
  this.currentBatcher.useShader(shader);
31728
32783
  }
31729
32784
  const reupload = typeof image.videoWidth !== "undefined";
31730
32785
  const texture = this.cache.get(image);
31731
32786
  const uvs = texture.getUVs(sx, sy, sw, sh);
31732
- this.currentBatcher.addQuad(
31733
- texture,
31734
- dx,
31735
- dy,
31736
- dw,
31737
- dh,
31738
- uvs[0],
31739
- uvs[1],
31740
- uvs[2],
31741
- uvs[3],
31742
- this.currentTint.toUint32(this.getGlobalAlpha()),
31743
- reupload
31744
- );
32787
+ if (useLit) {
32788
+ this.currentBatcher.addQuad(
32789
+ texture,
32790
+ dx,
32791
+ dy,
32792
+ dw,
32793
+ dh,
32794
+ uvs[0],
32795
+ uvs[1],
32796
+ uvs[2],
32797
+ uvs[3],
32798
+ this.currentTint.toUint32(this.getGlobalAlpha()),
32799
+ reupload,
32800
+ this.currentNormalMap
32801
+ );
32802
+ } else {
32803
+ this.currentBatcher.addQuad(
32804
+ texture,
32805
+ dx,
32806
+ dy,
32807
+ dw,
32808
+ dh,
32809
+ uvs[0],
32810
+ uvs[1],
32811
+ uvs[2],
32812
+ uvs[3],
32813
+ this.currentTint.toUint32(this.getGlobalAlpha()),
32814
+ reupload
32815
+ );
32816
+ }
31745
32817
  if (typeof shader === "object") {
31746
32818
  this.currentBatcher.useShader(this.currentBatcher.defaultShader);
31747
32819
  }
@@ -31781,7 +32853,7 @@ var WebGLRenderer = class extends Renderer {
31781
32853
  drawMesh(mesh) {
31782
32854
  const gl = this.gl;
31783
32855
  this.setBatcher("mesh");
31784
- if (typeof this.customShader === "object") {
32856
+ if (this.customShader != null) {
31785
32857
  this.currentBatcher.useShader(this.customShader);
31786
32858
  }
31787
32859
  gl.enable(gl.DEPTH_TEST);
@@ -31806,7 +32878,7 @@ var WebGLRenderer = class extends Renderer {
31806
32878
  gl.enable(gl.BLEND);
31807
32879
  gl.disable(gl.DEPTH_TEST);
31808
32880
  gl.depthMask(false);
31809
- if (typeof this.customShader === "object") {
32881
+ if (this.customShader != null) {
31810
32882
  this.currentBatcher.useShader(this.currentBatcher.defaultShader);
31811
32883
  }
31812
32884
  }
@@ -32041,23 +33113,33 @@ var WebGLRenderer = class extends Renderer {
32041
33113
  */
32042
33114
  restore() {
32043
33115
  const canvas = this.getCanvas();
33116
+ const peek = this.renderState.peekScissor();
33117
+ const cur = this.currentScissor;
33118
+ const curActive = this._scissorActive === true;
33119
+ const willBeActive = peek !== null;
33120
+ const scissorChanging = curActive !== willBeActive || willBeActive && (cur[0] !== peek[0] || cur[1] !== peek[1] || cur[2] !== peek[2] || cur[3] !== peek[3]);
33121
+ if (scissorChanging) {
33122
+ this.flush();
33123
+ }
32044
33124
  const result = this.renderState.restore(canvas.width, canvas.height);
32045
33125
  if (result !== null) {
32046
33126
  this.setBlendMode(result.blendMode);
32047
- if (result.scissorActive) {
33127
+ if (scissorChanging) {
32048
33128
  const gl = this.gl;
32049
- const s = this.currentScissor;
32050
- gl.enable(gl.SCISSOR_TEST);
32051
- this._scissorActive = true;
32052
- gl.scissor(
32053
- s[0] + this.currentTransform.tx,
32054
- canvas.height - s[3] - s[1] - this.currentTransform.ty,
32055
- s[2],
32056
- s[3]
32057
- );
32058
- } else {
32059
- this.gl.disable(this.gl.SCISSOR_TEST);
32060
- this._scissorActive = false;
33129
+ if (result.scissorActive) {
33130
+ const next = this.currentScissor;
33131
+ gl.enable(gl.SCISSOR_TEST);
33132
+ this._scissorActive = true;
33133
+ gl.scissor(
33134
+ next[0],
33135
+ canvas.height - next[3] - next[1],
33136
+ next[2],
33137
+ next[3]
33138
+ );
33139
+ } else {
33140
+ gl.disable(gl.SCISSOR_TEST);
33141
+ this._scissorActive = false;
33142
+ }
32061
33143
  }
32062
33144
  }
32063
33145
  this._currentGradient = this.renderState.currentGradient;
@@ -32726,31 +33808,42 @@ var WebGLRenderer = class extends Renderer {
32726
33808
  clipRect(x, y, width, height) {
32727
33809
  const canvas = this.getCanvas();
32728
33810
  const gl = this.gl;
32729
- if (x !== 0 || y !== 0 || width !== canvas.width || height !== canvas.height) {
32730
- const currentScissor = this.currentScissor;
33811
+ const m = this.currentTransform;
33812
+ if (!Number.isFinite(m.tx) || !Number.isFinite(m.ty)) {
32731
33813
  if (this._scissorActive) {
32732
- if (currentScissor[0] === x && currentScissor[1] === y && currentScissor[2] === width && currentScissor[3] === height) {
32733
- return;
32734
- }
33814
+ this.flush();
33815
+ gl.disable(gl.SCISSOR_TEST);
33816
+ this._scissorActive = false;
32735
33817
  }
32736
- this.flush();
32737
- gl.enable(this.gl.SCISSOR_TEST);
32738
- this._scissorActive = true;
32739
- gl.scissor(
32740
- // scissor does not account for currentTransform, so manually adjust
32741
- x + this.currentTransform.tx,
32742
- canvas.height - height - y - this.currentTransform.ty,
32743
- width,
32744
- height
32745
- );
32746
- currentScissor[0] = x;
32747
- currentScissor[1] = y;
32748
- currentScissor[2] = width;
32749
- currentScissor[3] = height;
32750
- } else {
32751
- gl.disable(gl.SCISSOR_TEST);
32752
- this._scissorActive = false;
33818
+ return;
33819
+ }
33820
+ const aabb = this._clipAABB;
33821
+ aabb.clear();
33822
+ aabb.addFrame(x, y, x + width, y + height, m);
33823
+ const sx = Math.floor(aabb.min.x);
33824
+ const sy = Math.floor(aabb.min.y);
33825
+ const sw = Math.ceil(aabb.max.x - sx);
33826
+ const sh = Math.ceil(aabb.max.y - sy);
33827
+ if (sx <= 0 && sy <= 0 && sx + sw >= canvas.width && sy + sh >= canvas.height) {
33828
+ if (this._scissorActive) {
33829
+ this.flush();
33830
+ gl.disable(gl.SCISSOR_TEST);
33831
+ this._scissorActive = false;
33832
+ }
33833
+ return;
33834
+ }
33835
+ const cs = this.currentScissor;
33836
+ if (this._scissorActive && cs[0] === sx && cs[1] === sy && cs[2] === sw && cs[3] === sh) {
33837
+ return;
32753
33838
  }
33839
+ this.flush();
33840
+ gl.enable(gl.SCISSOR_TEST);
33841
+ this._scissorActive = true;
33842
+ gl.scissor(sx, canvas.height - sh - sy, sw, sh);
33843
+ cs[0] = sx;
33844
+ cs[1] = sy;
33845
+ cs[2] = sw;
33846
+ cs[3] = sh;
32754
33847
  }
32755
33848
  /**
32756
33849
  * A mask limits rendering elements to the shape and position of the given mask object.
@@ -32769,15 +33862,16 @@ var WebGLRenderer = class extends Renderer {
32769
33862
  }
32770
33863
  this.maskLevel++;
32771
33864
  gl.colorMask(false, false, false, false);
32772
- gl.stencilFunc(gl.EQUAL, this.maskLevel, 1);
32773
- gl.stencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE);
33865
+ gl.stencilMask(255);
33866
+ gl.stencilFunc(gl.ALWAYS, 0, 255);
33867
+ gl.stencilOp(gl.KEEP, gl.KEEP, gl.INCR);
32774
33868
  this.fill(mask);
32775
33869
  this.flush();
32776
33870
  gl.colorMask(true, true, true, true);
32777
33871
  if (invert === true) {
32778
- gl.stencilFunc(gl.EQUAL, this.maskLevel + 1, 1);
33872
+ gl.stencilFunc(gl.EQUAL, 0, 255);
32779
33873
  } else {
32780
- gl.stencilFunc(gl.NOTEQUAL, this.maskLevel + 1, 1);
33874
+ gl.stencilFunc(gl.EQUAL, this.maskLevel, 255);
32781
33875
  }
32782
33876
  gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
32783
33877
  }