melonjs 19.2.0 → 19.4.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 (81) hide show
  1. package/README.md +13 -10
  2. package/build/application/application.d.ts.map +1 -1
  3. package/build/application/defaultApplicationSettings.d.ts +1 -0
  4. package/build/application/defaultApplicationSettings.d.ts.map +1 -1
  5. package/build/application/settings.d.ts +12 -0
  6. package/build/application/settings.d.ts.map +1 -1
  7. package/build/camera/camera2d.d.ts.map +1 -1
  8. package/build/index.js +2665 -629
  9. package/build/index.js.map +4 -4
  10. package/build/input/pointerevent.d.ts.map +1 -1
  11. package/build/level/tiled/TMXLayer.d.ts +72 -5
  12. package/build/level/tiled/TMXLayer.d.ts.map +1 -1
  13. package/build/level/tiled/TMXTile.d.ts +19 -1
  14. package/build/level/tiled/TMXTile.d.ts.map +1 -1
  15. package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
  16. package/build/level/tiled/TMXTileset.d.ts +15 -0
  17. package/build/level/tiled/TMXTileset.d.ts.map +1 -1
  18. package/build/level/tiled/renderer/TMXHexagonalRenderer.d.ts +7 -1
  19. package/build/level/tiled/renderer/TMXHexagonalRenderer.d.ts.map +1 -1
  20. package/build/level/tiled/renderer/TMXIsometricRenderer.d.ts +7 -1
  21. package/build/level/tiled/renderer/TMXIsometricRenderer.d.ts.map +1 -1
  22. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts.map +1 -1
  23. package/build/level/tiled/renderer/TMXOrthogonalRenderer.d.ts +7 -1
  24. package/build/level/tiled/renderer/TMXOrthogonalRenderer.d.ts.map +1 -1
  25. package/build/physics/bounds.d.ts +5 -2
  26. package/build/physics/bounds.d.ts.map +1 -1
  27. package/build/physics/sat.d.ts.map +1 -1
  28. package/build/physics/world.d.ts +16 -0
  29. package/build/physics/world.d.ts.map +1 -1
  30. package/build/renderable/container.d.ts +1 -1
  31. package/build/renderable/container.d.ts.map +1 -1
  32. package/build/renderable/imagelayer.d.ts.map +1 -1
  33. package/build/renderable/light2d.d.ts +128 -18
  34. package/build/renderable/light2d.d.ts.map +1 -1
  35. package/build/renderable/sprite.d.ts +38 -6
  36. package/build/renderable/sprite.d.ts.map +1 -1
  37. package/build/state/stage.d.ts +65 -9
  38. package/build/state/stage.d.ts.map +1 -1
  39. package/build/utils/function.d.ts +1 -1
  40. package/build/utils/function.d.ts.map +1 -1
  41. package/build/video/buffer/vertex.d.ts +2 -1
  42. package/build/video/buffer/vertex.d.ts.map +1 -1
  43. package/build/video/canvas/canvas_renderer.d.ts +2 -0
  44. package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
  45. package/build/video/renderer.d.ts +77 -1
  46. package/build/video/renderer.d.ts.map +1 -1
  47. package/build/video/renderstate.d.ts +20 -0
  48. package/build/video/renderstate.d.ts.map +1 -1
  49. package/build/video/texture/atlas.d.ts +26 -2
  50. package/build/video/texture/atlas.d.ts.map +1 -1
  51. package/build/video/texture/cache.d.ts.map +1 -1
  52. package/build/video/texture/resource.d.ts +113 -0
  53. package/build/video/texture/resource.d.ts.map +1 -0
  54. package/build/video/webgl/batchers/batcher.d.ts +6 -0
  55. package/build/video/webgl/batchers/batcher.d.ts.map +1 -1
  56. package/build/video/webgl/batchers/lit_quad_batcher.d.ts +109 -0
  57. package/build/video/webgl/batchers/lit_quad_batcher.d.ts.map +1 -0
  58. package/build/video/webgl/batchers/material_batcher.d.ts +8 -1
  59. package/build/video/webgl/batchers/material_batcher.d.ts.map +1 -1
  60. package/build/video/webgl/batchers/quad_batcher.d.ts +19 -1
  61. package/build/video/webgl/batchers/quad_batcher.d.ts.map +1 -1
  62. package/build/video/webgl/effects/radialGradient.d.ts +105 -0
  63. package/build/video/webgl/effects/radialGradient.d.ts.map +1 -0
  64. package/build/video/webgl/glshader.d.ts.map +1 -1
  65. package/build/video/webgl/lighting/constants.d.ts +13 -0
  66. package/build/video/webgl/lighting/constants.d.ts.map +1 -0
  67. package/build/video/webgl/lighting/pack.d.ts +76 -0
  68. package/build/video/webgl/lighting/pack.d.ts.map +1 -0
  69. package/build/video/webgl/renderers/tmxlayer/orthogonal.d.ts +108 -0
  70. package/build/video/webgl/renderers/tmxlayer/orthogonal.d.ts.map +1 -0
  71. package/build/video/webgl/shaders/multitexture-lit.d.ts +23 -0
  72. package/build/video/webgl/shaders/multitexture-lit.d.ts.map +1 -0
  73. package/build/video/webgl/utils/attributes.d.ts +7 -0
  74. package/build/video/webgl/utils/attributes.d.ts.map +1 -1
  75. package/build/video/webgl/utils/precision.d.ts +1 -1
  76. package/build/video/webgl/utils/precision.d.ts.map +1 -1
  77. package/build/video/webgl/utils/uniforms.d.ts +13 -0
  78. package/build/video/webgl/utils/uniforms.d.ts.map +1 -1
  79. package/build/video/webgl/webgl_renderer.d.ts +36 -0
  80. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  81. 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.4.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;
@@ -6227,6 +6249,7 @@ var defaultApplicationSettings = {
6227
6249
  consoleHeader: true,
6228
6250
  blendMode: "normal",
6229
6251
  physic: "builtin",
6252
+ gpuTilemap: true,
6230
6253
  failIfMajorPerformanceCaveat: true,
6231
6254
  highPrecisionShader: true,
6232
6255
  subPixel: false,
@@ -14138,10 +14161,10 @@ var Container = class _Container extends Renderable {
14138
14161
  draw(renderer2, viewport) {
14139
14162
  const bounds = this.getBounds();
14140
14163
  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
14164
  renderer2.translate(this.pos.x, this.pos.y);
14165
+ if (this.root === false && this.clipping === true && bounds.isFinite() === true && Number.isFinite(this.width) === true && Number.isFinite(this.height) === true) {
14166
+ renderer2.clipRect(0, 0, this.width, this.height);
14167
+ }
14145
14168
  if (this.backgroundColor.alpha > 1 / 255) {
14146
14169
  renderer2.clearColor(this.backgroundColor);
14147
14170
  }
@@ -15366,6 +15389,32 @@ var RenderState = class {
15366
15389
  }
15367
15390
  this._stackDepth = depth + 1;
15368
15391
  }
15392
+ /**
15393
+ * Inspect the scissor box that the next `restore()` would install,
15394
+ * without mutating any state. Lets renderers detect whether a
15395
+ * pending `restore()` will actually change the scissor (and
15396
+ * decide, e.g., whether to flush GPU work first).
15397
+ *
15398
+ * Returns:
15399
+ * - `Int32Array` (length 4) — `[x, y, width, height]` of the
15400
+ * saved scissor when scissor was active at the matching `save()`
15401
+ * call.
15402
+ * - `null` — when the saved state had scissor disabled, or the
15403
+ * stack is empty. Treat this as "next scissor will be inactive".
15404
+ *
15405
+ * The returned array is a **live reference into the internal
15406
+ * stack** — zero allocation on a hot path. Callers MUST treat it
15407
+ * as read-only; mutating it corrupts subsequent `restore()` calls.
15408
+ * @ignore
15409
+ * @returns {Int32Array | null}
15410
+ */
15411
+ peekScissor() {
15412
+ const depth = this._stackDepth - 1;
15413
+ if (depth < 0 || !this._scissorActive[depth]) {
15414
+ return null;
15415
+ }
15416
+ return this._scissorStack[depth];
15417
+ }
15369
15418
  /**
15370
15419
  * Restore state from the stack.
15371
15420
  * Color, tint, transform, and scissor are restored in place.
@@ -15466,6 +15515,8 @@ var Renderer = class {
15466
15515
  this.maskLevel = 0;
15467
15516
  this.projectionMatrix = new Matrix3d();
15468
15517
  this.uvOffset = 0;
15518
+ this.currentNormalMap = null;
15519
+ this.activeLightCount = 0;
15469
15520
  }
15470
15521
  /**
15471
15522
  * @type {string}
@@ -15686,6 +15737,97 @@ var Renderer = class {
15686
15737
  setBlendMode(mode = "normal") {
15687
15738
  this.currentBlendMode = mode;
15688
15739
  }
15740
+ /**
15741
+ * Upload the active scene lights to the lit sprite pipeline.
15742
+ *
15743
+ * Called once per camera per frame by `Camera2d.draw()` (after the
15744
+ * FBO is bound, before the world tree walk fires `Sprite.draw` for
15745
+ * any normal-mapped sprite). The WebGL renderer overrides this to
15746
+ * pack the lights into the lit shader's uniform buffers; the Canvas
15747
+ * renderer cannot do per-pixel normal-map lighting and silently
15748
+ * ignores the call. The first time a non-empty light list is passed
15749
+ * in Canvas mode, a one-shot console warning is emitted.
15750
+ *
15751
+ * Stage stays renderer-agnostic by passing the raw scene data —
15752
+ * lights iterable and ambient color — and letting the renderer
15753
+ * decide how to encode them.
15754
+ * @param {Iterable<object>} [lights] - active `Light2d` instances; falsy/empty no-ops
15755
+ * @param {object} [ambient] - ambient lighting color (0..255 RGB)
15756
+ * @param {number} [translateX=0] - world-to-screen X translate (matches `Camera2d.draw()`)
15757
+ * @param {number} [translateY=0] - world-to-screen Y translate
15758
+ */
15759
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
15760
+ setLightUniforms(lights, ambient, translateX, translateY) {
15761
+ if (this._litPipelineWarned || !lights) {
15762
+ return;
15763
+ }
15764
+ 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;
15765
+ if (hasAny) {
15766
+ this._litPipelineWarned = true;
15767
+ console.warn(
15768
+ "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."
15769
+ );
15770
+ }
15771
+ }
15772
+ /**
15773
+ * Render a `Light2d` instance.
15774
+ *
15775
+ * Each renderer implements its own strategy: the WebGL renderer
15776
+ * draws lights as quads through a shared procedural radial-falloff
15777
+ * fragment shader (no per-light texture, color and intensity
15778
+ * encoded in the per-vertex tint so consecutive draws batch); the
15779
+ * Canvas renderer caches a small `Gradient` config object per
15780
+ * light in a `WeakMap` (rebuilt only when the light's radii /
15781
+ * color / intensity change), rasterizes it with `Gradient.toCanvas()`
15782
+ * into a single shared `CanvasRenderTarget`, and composites the
15783
+ * result via `drawImage`. The base implementation is a no-op so
15784
+ * renderers without a lighting path can be polymorphically
15785
+ * substituted.
15786
+ *
15787
+ * Light2d itself is renderer-agnostic — it just calls
15788
+ * `renderer.drawLight(this)` and relies on the renderer to pick
15789
+ * the right machinery.
15790
+ * @param {object} light - the Light2d instance to render
15791
+ * @see Light2d
15792
+ */
15793
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
15794
+ drawLight(light) {
15795
+ }
15796
+ /**
15797
+ * Draw a TMX tile layer. Default behavior:
15798
+ * - if `layer.canvasRenderer` is set (preRender bake), blit the cached
15799
+ * offscreen canvas in a single `drawImage` call;
15800
+ * - otherwise delegate to the layer's TMX orientation renderer for
15801
+ * the per-tile loop.
15802
+ *
15803
+ * `WebGLRenderer` overrides this to add the procedural shader fast
15804
+ * path on top (when `layer.renderMode === "shader"`) and fall through
15805
+ * to this base behavior for all other layers.
15806
+ * @param {object} layer - the TMXLayer to draw
15807
+ * @param {Rect} rect - the visible region in world coords
15808
+ */
15809
+ drawTileLayer(layer, rect) {
15810
+ if (layer.canvasRenderer) {
15811
+ const width = Math.min(rect.width, layer.width - rect.pos.x);
15812
+ const height = Math.min(rect.height, layer.height - rect.pos.y);
15813
+ if (width <= 0 || height <= 0) {
15814
+ return;
15815
+ }
15816
+ this.drawImage(
15817
+ layer.canvasRenderer.getCanvas(),
15818
+ rect.pos.x,
15819
+ rect.pos.y,
15820
+ width,
15821
+ height,
15822
+ rect.pos.x,
15823
+ rect.pos.y,
15824
+ width,
15825
+ height
15826
+ );
15827
+ return;
15828
+ }
15829
+ layer.getRenderer().drawTileLayer(this, layer, rect);
15830
+ }
15689
15831
  /**
15690
15832
  * Set the current fill & stroke style color.
15691
15833
  * By default, or upon reset, the value is set to #000000.
@@ -16206,7 +16348,7 @@ var TextureAtlas = class {
16206
16348
  /**
16207
16349
  * @param {object|object[]} atlases - atlas information. See {@link loader.getJSON}
16208
16350
  * @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
16351
+ * @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
16352
  * @example
16211
16353
  * // create a texture atlas from a JSON Object
16212
16354
  * game.texture = new me.TextureAtlas(
@@ -16228,10 +16370,22 @@ var TextureAtlas = class {
16228
16370
  * anchorPoint : new me.Vector2d(0.5, 0.5)
16229
16371
  * },
16230
16372
  * me.loader.getImage("spritesheet")
16373
+ * );
16374
+ *
16375
+ * // SpriteIlluminator workflow: pair the color atlas with its normal map
16376
+ * game.texture = new me.TextureAtlas(
16377
+ * me.loader.getJSON("scene"),
16378
+ * me.loader.getImage("scene"),
16379
+ * { normalMap: me.loader.getImage("scene_n") }
16380
+ * );
16231
16381
  */
16232
- constructor(atlases, src, cache2) {
16382
+ constructor(atlases, src, options) {
16383
+ const opts = typeof options === "boolean" ? { cache: options } : options || {};
16384
+ const cache2 = opts.cache;
16385
+ const normalMap = opts.normalMap;
16233
16386
  this.format = null;
16234
16387
  this.sources = /* @__PURE__ */ new Map();
16388
+ this.normalSources = /* @__PURE__ */ new Map();
16235
16389
  this.atlases = /* @__PURE__ */ new Map();
16236
16390
  this.activeAtlas = void 0;
16237
16391
  this._uvCache = { sx: -1, sy: -1, sw: -1, sh: -1, uvs: null };
@@ -16321,6 +16475,31 @@ var TextureAtlas = class {
16321
16475
  game.renderer.cache.set(source, this);
16322
16476
  });
16323
16477
  }
16478
+ if (typeof normalMap !== "undefined" && normalMap !== null) {
16479
+ let resolved;
16480
+ if (typeof normalMap === "string") {
16481
+ resolved = getImage(normalMap);
16482
+ if (!resolved) {
16483
+ throw new Error(
16484
+ "TextureAtlas: normal map image '" + normalMap + "' not found"
16485
+ );
16486
+ }
16487
+ } else if (typeof normalMap === "object" && typeof normalMap.width === "number" && typeof normalMap.height === "number") {
16488
+ if (typeof normalMap.videoWidth === "number") {
16489
+ throw new TypeError(
16490
+ "TextureAtlas: options.normalMap does not support HTMLVideoElement (the lit pipeline caches the texture per image reference and would freeze on frame 0)"
16491
+ );
16492
+ }
16493
+ resolved = normalMap;
16494
+ } else {
16495
+ throw new TypeError(
16496
+ "TextureAtlas: options.normalMap must be an image-like, a loader key string, or null/undefined; got " + typeof normalMap
16497
+ );
16498
+ }
16499
+ this.sources.forEach((_source, key) => {
16500
+ this.normalSources.set(key, resolved);
16501
+ });
16502
+ }
16324
16503
  }
16325
16504
  /**
16326
16505
  * return the default or specified atlas dictionnary
@@ -16353,6 +16532,22 @@ var TextureAtlas = class {
16353
16532
  return this.sources.get(this.activeAtlas);
16354
16533
  }
16355
16534
  }
16535
+ /**
16536
+ * Return the paired normal-map texture for the given region, or `null`
16537
+ * if no normal map was provided to this atlas. The normal map shares
16538
+ * the same UV layout as the color texture returned by {@link TextureAtlas#getTexture}.
16539
+ * @param {object} [region] - region name in case of multipack textures
16540
+ * @returns {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap|null}
16541
+ */
16542
+ getNormalTexture(region) {
16543
+ if (this.normalSources.size === 0) {
16544
+ return null;
16545
+ }
16546
+ if (typeof region === "object" && typeof region.texture === "string") {
16547
+ return this.normalSources.get(region.texture) ?? null;
16548
+ }
16549
+ return this.normalSources.get(this.activeAtlas) ?? null;
16550
+ }
16356
16551
  /**
16357
16552
  * add a region to the atlas
16358
16553
  * @param {string} name - region mame
@@ -16731,14 +16926,6 @@ var TextureCache = class {
16731
16926
  * cache the textureAltas for the given image
16732
16927
  */
16733
16928
  set(image, textureAtlas) {
16734
- const width = image.width || image.videoWidth;
16735
- const height = image.height || image.videoHeight;
16736
- if (this.renderer.WebGLVersion === 1 && (!isPowerOfTwo(width) || !isPowerOfTwo(height))) {
16737
- const src = typeof image.src !== "undefined" ? image.src : image;
16738
- console.warn(
16739
- "[Texture] " + src + " is not a POT texture (" + width + "x" + height + ")"
16740
- );
16741
- }
16742
16929
  return this.cache.put(image, textureAtlas);
16743
16930
  }
16744
16931
  /**
@@ -16851,6 +17038,7 @@ var CanvasRenderer = class extends Renderer {
16851
17038
  reset() {
16852
17039
  super.reset();
16853
17040
  this.clearColor(this.currentColor, this.settings.transparent !== true);
17041
+ this._lightCache = void 0;
16854
17042
  }
16855
17043
  /**
16856
17044
  * Reset the canvas transform to identity
@@ -17044,6 +17232,59 @@ var CanvasRenderer = class extends Renderer {
17044
17232
  }
17045
17233
  context.drawImage(source, sx, sy, sw, sh, dx, dy, dw, dh);
17046
17234
  }
17235
+ /**
17236
+ * @inheritdoc
17237
+ *
17238
+ * Renders the light by drawing a cached radial `Gradient` via
17239
+ * `Gradient.toCanvas()`. The Gradient instance is cached per-Light2d
17240
+ * in a `WeakMap` and rebuilt only when the light's radii / color /
17241
+ * intensity change. `toCanvas` itself shares a single
17242
+ * `CanvasRenderTarget` across all gradients in the engine, so memory
17243
+ * stays at O(1) regardless of how many lights are active.
17244
+ *
17245
+ * The cached Gradient is always circular (outer radius =
17246
+ * `max(radiusX, radiusY)`) — `drawImage`'s non-uniform stretch
17247
+ * produces the elliptical falloff for non-square lights, matching
17248
+ * the procedural shader's behavior on WebGL.
17249
+ * @param {object} light - the Light2d instance to render
17250
+ */
17251
+ drawLight(light) {
17252
+ if (this._lightCache === void 0) {
17253
+ this._lightCache = /* @__PURE__ */ new WeakMap();
17254
+ }
17255
+ let entry = this._lightCache.get(light);
17256
+ const c = light.color;
17257
+ 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) {
17258
+ const r = Math.max(light.radiusX, light.radiusY);
17259
+ const gradient = this.createRadialGradient(r, r, 0, r, r, r);
17260
+ gradient.addColorStop(0, c.toRGBA(light.intensity));
17261
+ gradient.addColorStop(1, c.toRGBA(0));
17262
+ entry = {
17263
+ gradient,
17264
+ radius: r,
17265
+ radiusX: light.radiusX,
17266
+ radiusY: light.radiusY,
17267
+ r: c.r,
17268
+ g: c.g,
17269
+ b: c.b,
17270
+ intensity: light.intensity
17271
+ };
17272
+ this._lightCache.set(light, entry);
17273
+ }
17274
+ const r2 = entry.radius * 2;
17275
+ const canvas = entry.gradient.toCanvas(this, 0, 0, r2, r2);
17276
+ this.drawImage(
17277
+ canvas,
17278
+ 0,
17279
+ 0,
17280
+ r2,
17281
+ r2,
17282
+ light.pos.x,
17283
+ light.pos.y,
17284
+ light.width,
17285
+ light.height
17286
+ );
17287
+ }
17047
17288
  /**
17048
17289
  * Draw a pattern within the given rectangle.
17049
17290
  * @param {CanvasPattern} pattern - Pattern object
@@ -17778,14 +18019,15 @@ var CanvasRenderer = class extends Renderer {
17778
18019
  if (typeof mask !== "undefined") {
17779
18020
  context.beginPath();
17780
18021
  }
18022
+ this._maskInvertOuterAdded = false;
17781
18023
  }
17782
18024
  if (typeof mask !== "undefined") {
17783
18025
  switch (mask.type) {
17784
18026
  // RoundRect
17785
18027
  case "RoundRect":
17786
18028
  context.roundRect(
17787
- mask.top,
17788
18029
  mask.left,
18030
+ mask.top,
17789
18031
  mask.width,
17790
18032
  mask.height,
17791
18033
  mask.radius
@@ -17794,7 +18036,7 @@ var CanvasRenderer = class extends Renderer {
17794
18036
  // Rect or Bounds
17795
18037
  case "Rectangle":
17796
18038
  case "Bounds":
17797
- context.rect(mask.top, mask.left, mask.width, mask.height);
18039
+ context.rect(mask.left, mask.top, mask.width, mask.height);
17798
18040
  break;
17799
18041
  // Polygon or Line
17800
18042
  case "Polygon":
@@ -17837,7 +18079,10 @@ var CanvasRenderer = class extends Renderer {
17837
18079
  }
17838
18080
  this.maskLevel++;
17839
18081
  if (invert === true) {
17840
- context.rect(0, 0, this.getCanvas().width, this.getCanvas().height);
18082
+ if (this._maskInvertOuterAdded !== true) {
18083
+ context.rect(0, 0, this.getCanvas().width, this.getCanvas().height);
18084
+ this._maskInvertOuterAdded = true;
18085
+ }
17841
18086
  context.clip("evenodd");
17842
18087
  } else {
17843
18088
  context.clip();
@@ -17856,6 +18101,30 @@ var CanvasRenderer = class extends Renderer {
17856
18101
  };
17857
18102
 
17858
18103
  // src/level/tiled/TMXTile.js
18104
+ var FLIP_H_BIT = 1 << 0;
18105
+ var FLIP_V_BIT = 1 << 1;
18106
+ var FLIP_AD_BIT = 1 << 2;
18107
+ function buildFlipTransform(transform, flipMask, width, height) {
18108
+ const halfW = width / 2;
18109
+ const halfH = height / 2;
18110
+ const flippedH = (flipMask & FLIP_H_BIT) !== 0;
18111
+ const flippedV = (flipMask & FLIP_V_BIT) !== 0;
18112
+ const flippedAD = (flipMask & FLIP_AD_BIT) !== 0;
18113
+ transform.identity();
18114
+ transform.translate(halfW, halfH);
18115
+ if (flippedAD) {
18116
+ transform.rotate(degToRad(-90));
18117
+ transform.scale(-1, 1);
18118
+ }
18119
+ if (flippedH) {
18120
+ transform.scale(flippedAD ? 1 : -1, flippedAD ? -1 : 1);
18121
+ }
18122
+ if (flippedV) {
18123
+ transform.scale(flippedAD ? -1 : 1, flippedAD ? 1 : -1);
18124
+ }
18125
+ transform.translate(-halfW, -halfH);
18126
+ return transform;
18127
+ }
17859
18128
  var Tile = class extends Bounds {
17860
18129
  /**
17861
18130
  * @param {number} x - x index of the Tile in the map
@@ -18406,12 +18675,17 @@ function applyTMXProperties(obj, data2) {
18406
18675
  }
18407
18676
 
18408
18677
  // src/level/tiled/TMXLayer.js
18409
- function initArray(rows, cols) {
18410
- const array = new Array(cols);
18411
- for (let col = 0; col < cols; col++) {
18412
- array[col] = new Array(rows).fill(null);
18413
- }
18414
- return array;
18678
+ var FLIP_H_BIT2 = 1 << 0;
18679
+ var FLIP_V_BIT2 = 1 << 1;
18680
+ var FLIP_AD_BIT2 = 1 << 2;
18681
+ function flipMaskFromGid(gid) {
18682
+ return (gid & TMX_FLIP_H ? FLIP_H_BIT2 : 0) | (gid & TMX_FLIP_V ? FLIP_V_BIT2 : 0) | (gid & TMX_FLIP_AD ? FLIP_AD_BIT2 : 0);
18683
+ }
18684
+ function flipMaskFromTile(tile) {
18685
+ return (tile.flippedX ? FLIP_H_BIT2 : 0) | (tile.flippedY ? FLIP_V_BIT2 : 0) | (tile.flippedAD ? FLIP_AD_BIT2 : 0);
18686
+ }
18687
+ function gidWithFlips(gid, flipMask) {
18688
+ return gid | (flipMask & FLIP_H_BIT2 ? TMX_FLIP_H : 0) | (flipMask & FLIP_V_BIT2 ? TMX_FLIP_V : 0) | (flipMask & FLIP_AD_BIT2 ? TMX_FLIP_AD : 0);
18415
18689
  }
18416
18690
  function setLayerData(layer, bounds, data2) {
18417
18691
  let idx = 0;
@@ -18424,18 +18698,30 @@ function setLayerData(layer, bounds, data2) {
18424
18698
  width = bounds.cols;
18425
18699
  height = bounds.rows;
18426
18700
  }
18701
+ const cols = layer.cols;
18702
+ const layerData = layer.layerData;
18703
+ const offsetX = bounds.x;
18704
+ const offsetY = bounds.y;
18705
+ let overflowedGid = 0;
18427
18706
  for (let y = 0; y < height; y++) {
18428
18707
  for (let x = 0; x < width; x++) {
18429
- const gid = data2[idx++];
18430
- if (gid !== 0) {
18431
- layer.layerData[x + bounds.x][y + bounds.y] = layer.getTileById(
18432
- gid,
18433
- x + bounds.x,
18434
- y + bounds.y
18435
- );
18708
+ const rawGid = data2[idx++];
18709
+ if (rawGid !== 0) {
18710
+ const flatIdx = ((y + offsetY) * cols + (x + offsetX)) * 2;
18711
+ const cleanGid = rawGid & TMX_CLEAR_BIT_MASK;
18712
+ if (cleanGid > 65535 && overflowedGid === 0) {
18713
+ overflowedGid = cleanGid;
18714
+ }
18715
+ layerData[flatIdx] = cleanGid;
18716
+ layerData[flatIdx + 1] = flipMaskFromGid(rawGid);
18436
18717
  }
18437
18718
  }
18438
18719
  }
18720
+ if (overflowedGid !== 0) {
18721
+ console.warn(
18722
+ "melonJS: TMX layer contains GID " + overflowedGid + " which exceeds the 16-bit cell capacity (max 65535). Tiles will be truncated and render incorrectly."
18723
+ );
18724
+ }
18439
18725
  }
18440
18726
  var TMXLayer = class extends Renderable {
18441
18727
  /**
@@ -18495,7 +18781,10 @@ var TMXLayer = class extends Renderable {
18495
18781
  }
18496
18782
  applyTMXProperties(this, data2);
18497
18783
  this.setRenderer(map.getRenderer());
18498
- this.layerData = initArray(this.rows, this.cols);
18784
+ this.layerData = new Uint16Array(this.cols * this.rows * 2);
18785
+ this.cachedTile = null;
18786
+ this.dataVersion = 0;
18787
+ this.renderMode = "auto";
18499
18788
  if (map.infinite === 0) {
18500
18789
  setLayerData(
18501
18790
  this,
@@ -18525,12 +18814,8 @@ var TMXLayer = class extends Renderable {
18525
18814
  }
18526
18815
  }
18527
18816
  this.isAnimated = this.animatedTilesets.length > 0;
18528
- if (typeof this.preRender === "undefined" && this.isAnimated === false) {
18529
- this.preRender = this.ancestor.getRootAncestor().preRender;
18530
- } else {
18531
- this.preRender = false;
18532
- }
18533
- if (this.preRender === true && !this.canvasRenderer) {
18817
+ this._resolveRenderMode();
18818
+ if (this.renderMode === "prerender" && !this.canvasRenderer) {
18534
18819
  this.canvasRenderer = new CanvasRenderer({
18535
18820
  canvas: createCanvas(this.width, this.height),
18536
18821
  width: this.width,
@@ -18539,8 +18824,109 @@ var TMXLayer = class extends Renderable {
18539
18824
  });
18540
18825
  this.getRenderer().drawTileLayer(this.canvasRenderer, this, this);
18541
18826
  }
18827
+ this.preRender = this.renderMode === "prerender";
18542
18828
  this.isDirty = true;
18543
18829
  }
18830
+ /**
18831
+ * Resolve `this.renderMode` to one of "shader" / "prerender" / "perTile"
18832
+ * based on eligibility checks and user/world hints. Emits a single
18833
+ * `console.warn` at activation when a forced mode is ineligible, or
18834
+ * when an auto-eligible mode falls back due to a layer feature the GPU
18835
+ * path doesn't support (orientation, collection-of-image tileset, etc.).
18836
+ * @ignore
18837
+ */
18838
+ _resolveRenderMode() {
18839
+ const root = this.ancestor?.getRootAncestor?.();
18840
+ const renderer2 = this.parentApp?.renderer;
18841
+ const gpuAllowed = root?.gpuTilemap !== false;
18842
+ const preRenderHint = typeof this.preRender === "boolean" ? this.preRender : root?.preRender;
18843
+ const elig = this._checkShaderEligibility(renderer2, gpuAllowed);
18844
+ const requested = this.renderMode;
18845
+ if (requested === "shader") {
18846
+ if (elig.ok) {
18847
+ return;
18848
+ }
18849
+ console.warn(
18850
+ `melonJS: layer "${this.name}" forced renderMode "shader" not available (${elig.reason}) \u2014 falling back to perTile`
18851
+ );
18852
+ this.renderMode = "perTile";
18853
+ return;
18854
+ }
18855
+ if (requested === "prerender") {
18856
+ if (this.isAnimated) {
18857
+ console.warn(
18858
+ `melonJS: layer "${this.name}" forced renderMode "prerender" disabled (layer has animated tiles) \u2014 falling back to perTile`
18859
+ );
18860
+ this.renderMode = "perTile";
18861
+ }
18862
+ return;
18863
+ }
18864
+ if (requested === "perTile") {
18865
+ return;
18866
+ }
18867
+ if (elig.ok) {
18868
+ this.renderMode = "shader";
18869
+ return;
18870
+ }
18871
+ if (gpuAllowed && elig.reason !== "no-webgl2-renderer") {
18872
+ console.warn(
18873
+ `melonJS: layer "${this.name}" using legacy tile renderer (${elig.reason})`
18874
+ );
18875
+ }
18876
+ if (preRenderHint && !this.isAnimated) {
18877
+ this.renderMode = "prerender";
18878
+ return;
18879
+ }
18880
+ this.renderMode = "perTile";
18881
+ }
18882
+ /**
18883
+ * Check whether this layer is eligible for the WebGL2 shader path.
18884
+ * @param {object} renderer
18885
+ * @param {boolean} gpuAllowed - whether `gpuTilemap` is enabled at the world level
18886
+ * @returns {{ok: boolean, reason?: string}}
18887
+ * @ignore
18888
+ */
18889
+ _checkShaderEligibility(renderer2, gpuAllowed) {
18890
+ if (!gpuAllowed) {
18891
+ return { ok: false, reason: "gpuTilemap disabled" };
18892
+ }
18893
+ if (!renderer2 || renderer2.WebGLVersion !== 2) {
18894
+ return { ok: false, reason: "no-webgl2-renderer" };
18895
+ }
18896
+ if (this.orientation !== "orthogonal") {
18897
+ return {
18898
+ ok: false,
18899
+ reason: `no gpu renderer supported yet for "${this.orientation}" orientation`
18900
+ };
18901
+ }
18902
+ if (!this.tilesets || this.tilesets.tilesets.length === 0) {
18903
+ return { ok: false, reason: "no tilesets" };
18904
+ }
18905
+ const MAX_OVERFLOW_CELLS = 4;
18906
+ for (const ts of this.tilesets.tilesets) {
18907
+ if (ts.isCollection) {
18908
+ return { ok: false, reason: "collection-of-image tileset" };
18909
+ }
18910
+ if (ts.tilerendersize !== "tile") {
18911
+ return {
18912
+ ok: false,
18913
+ reason: `tilerendersize "${ts.tilerendersize}" not supported`
18914
+ };
18915
+ }
18916
+ if (ts.tileoffset.x !== 0 || ts.tileoffset.y !== 0) {
18917
+ return { ok: false, reason: "non-zero tileoffset" };
18918
+ }
18919
+ const overflowX = Math.ceil(ts.tilewidth / this.tilewidth) - 1;
18920
+ const overflowY = Math.ceil(ts.tileheight / this.tileheight) - 1;
18921
+ if (overflowX > MAX_OVERFLOW_CELLS || overflowY > MAX_OVERFLOW_CELLS) {
18922
+ return {
18923
+ ok: false,
18924
+ reason: `tile overflow exceeds shader limit (${MAX_OVERFLOW_CELLS} cells)`
18925
+ };
18926
+ }
18927
+ }
18928
+ return { ok: true };
18929
+ }
18544
18930
  // called when the layer is removed from the game world or a container
18545
18931
  onDeactivateEvent() {
18546
18932
  this.animatedTilesets = void 0;
@@ -18601,12 +18987,29 @@ var TMXLayer = class extends Renderable {
18601
18987
  /**
18602
18988
  * assign the given Tile object to the specified position
18603
18989
  * @param {Tile} tile - the tile object to be assigned
18604
- * @param {number} x - x coordinate (in world/pixels coordinates)
18605
- * @param {number} y - y coordinate (in world/pixels coordinates)
18990
+ * @param {number} x - x coordinate (in tile/column coordinates)
18991
+ * @param {number} y - y coordinate (in tile/row coordinates)
18606
18992
  * @returns {Tile} the tile object
18607
18993
  */
18608
18994
  setTile(tile, x, y) {
18609
- this.layerData[x][y] = tile;
18995
+ if (x < 0 || x >= this.cols || y < 0 || y >= this.rows) {
18996
+ return tile;
18997
+ }
18998
+ const slot = y * this.cols + x;
18999
+ const idx = slot * 2;
19000
+ const cleanGid = tile.tileId & TMX_CLEAR_BIT_MASK;
19001
+ if (cleanGid > 65535 && !this._truncationWarned) {
19002
+ this._truncationWarned = true;
19003
+ console.warn(
19004
+ "melonJS: setTile received GID " + cleanGid + " which exceeds the 16-bit cell capacity (max 65535). Tile will be truncated and render incorrectly."
19005
+ );
19006
+ }
19007
+ this.layerData[idx] = cleanGid;
19008
+ this.layerData[idx + 1] = flipMaskFromTile(tile);
19009
+ if (this.cachedTile !== null) {
19010
+ this.cachedTile[slot] = tile;
19011
+ }
19012
+ this.dataVersion++;
18610
19013
  this.isDirty = true;
18611
19014
  return tile;
18612
19015
  }
@@ -18636,12 +19039,27 @@ var TMXLayer = class extends Renderable {
18636
19039
  cellAt(x, y, boundsCheck) {
18637
19040
  const _x = ~~x;
18638
19041
  const _y = ~~y;
18639
- const renderer2 = this.getRenderer();
18640
- if (boundsCheck === false || _x >= 0 && _x < renderer2.cols && _y >= 0 && _y < renderer2.rows) {
18641
- return this.layerData[_x][_y];
18642
- } else {
19042
+ if (boundsCheck !== false && (_x < 0 || _x >= this.cols || _y < 0 || _y >= this.rows)) {
18643
19043
  return null;
18644
19044
  }
19045
+ const slot = _y * this.cols + _x;
19046
+ const idx = slot * 2;
19047
+ const gid = this.layerData[idx];
19048
+ if (!gid) {
19049
+ return null;
19050
+ }
19051
+ if (this.cachedTile === null) {
19052
+ this.cachedTile = new Array(this.cols * this.rows).fill(null);
19053
+ } else {
19054
+ const cached = this.cachedTile[slot];
19055
+ if (cached !== null) {
19056
+ return cached;
19057
+ }
19058
+ }
19059
+ const flipMask = this.layerData[idx + 1];
19060
+ const tile = this.getTileById(gidWithFlips(gid, flipMask), _x, _y);
19061
+ this.cachedTile[slot] = tile;
19062
+ return tile;
18645
19063
  }
18646
19064
  /**
18647
19065
  * clear the tile at the specified position
@@ -18654,7 +19072,16 @@ var TMXLayer = class extends Renderable {
18654
19072
  * });
18655
19073
  */
18656
19074
  clearTile(x, y) {
18657
- this.layerData[x][y] = null;
19075
+ if (x < 0 || x >= this.cols || y < 0 || y >= this.rows) {
19076
+ return;
19077
+ }
19078
+ const slot = y * this.cols + x;
19079
+ const idx = slot * 2;
19080
+ this.layerData[idx] = 0;
19081
+ this.layerData[idx + 1] = 0;
19082
+ if (this.cachedTile !== null) {
19083
+ this.cachedTile[slot] = null;
19084
+ }
18658
19085
  if (this.preRender) {
18659
19086
  this.canvasRenderer.clearRect(
18660
19087
  x * this.tilewidth,
@@ -18663,6 +19090,7 @@ var TMXLayer = class extends Renderable {
18663
19090
  this.tileheight
18664
19091
  );
18665
19092
  }
19093
+ this.dataVersion++;
18666
19094
  this.isDirty = true;
18667
19095
  }
18668
19096
  /**
@@ -18683,27 +19111,7 @@ var TMXLayer = class extends Renderable {
18683
19111
  * @ignore
18684
19112
  */
18685
19113
  draw(renderer2, rect) {
18686
- if (this.preRender) {
18687
- const width = Math.min(rect.width, this.width);
18688
- const height = Math.min(rect.height, this.height);
18689
- renderer2.drawImage(
18690
- this.canvasRenderer.getCanvas(),
18691
- rect.pos.x,
18692
- rect.pos.y,
18693
- // sx,sy
18694
- width,
18695
- height,
18696
- // sw,sh
18697
- rect.pos.x,
18698
- rect.pos.y,
18699
- // dx,dy
18700
- width,
18701
- height
18702
- // dw,dh
18703
- );
18704
- } else {
18705
- this.getRenderer().drawTileLayer(renderer2, this, rect);
18706
- }
19114
+ renderer2.drawTileLayer(this, rect);
18707
19115
  }
18708
19116
  };
18709
19117
 
@@ -19067,7 +19475,7 @@ var TMXHexagonalRenderer = class extends TMXRenderer {
19067
19475
  return ret;
19068
19476
  }
19069
19477
  /**
19070
- * draw the tile map
19478
+ * draw the tile map (legacy entry point — accepts a fully-constructed Tile)
19071
19479
  * @ignore
19072
19480
  */
19073
19481
  drawTile(renderer2, x, y, tmxTile) {
@@ -19081,12 +19489,27 @@ var TMXHexagonalRenderer = class extends TMXRenderer {
19081
19489
  );
19082
19490
  vector2dPool.release(point);
19083
19491
  }
19492
+ /**
19493
+ * draw a tile from raw (gid, flipMask, tileset) data — used by the hot
19494
+ * rendering loop to bypass Tile construction
19495
+ * @ignore
19496
+ */
19497
+ drawTileRaw(renderer2, x, y, gid, flipMask, tileset) {
19498
+ const point = this.tileToPixelCoords(x, y, vector2dPool.get());
19499
+ tileset.drawTileRaw(
19500
+ renderer2,
19501
+ tileset.tileoffset.x + point.x,
19502
+ tileset.tileoffset.y + point.y + (this.tileheight - tileset.tileheight),
19503
+ gid,
19504
+ flipMask
19505
+ );
19506
+ vector2dPool.release(point);
19507
+ }
19084
19508
  /**
19085
19509
  * draw the tile map
19086
19510
  * @ignore
19087
19511
  */
19088
19512
  drawTileLayer(renderer2, layer, rect) {
19089
- let tile;
19090
19513
  const startTile = this.pixelToTileCoords(
19091
19514
  rect.pos.x,
19092
19515
  rect.pos.y,
@@ -19110,6 +19533,15 @@ var TMXHexagonalRenderer = class extends TMXRenderer {
19110
19533
  }
19111
19534
  const endX = layer.cols;
19112
19535
  const endY = layer.rows;
19536
+ const layerCols = layer.cols;
19537
+ const data2 = layer.layerData;
19538
+ const tilesets = layer.tilesets;
19539
+ let tilesetCache = layer.tileset;
19540
+ if (tilesetCache === null) {
19541
+ vector2dPool.release(startTile);
19542
+ vector2dPool.release(startPos);
19543
+ return;
19544
+ }
19113
19545
  if (this.staggerX) {
19114
19546
  startTile.x = Math.max(0, startTile.x);
19115
19547
  startTile.y = Math.max(0, startTile.y);
@@ -19123,9 +19555,22 @@ var TMXHexagonalRenderer = class extends TMXRenderer {
19123
19555
  rowTile.setV(startTile);
19124
19556
  rowPos.setV(startPos);
19125
19557
  for (; rowPos.x < rect.right && rowTile.x < endX; rowTile.x += 2) {
19126
- tile = layer.cellAt(rowTile.x, rowTile.y, false);
19127
- if (tile) {
19128
- tile.tileset.drawTile(renderer2, rowPos.x, rowPos.y, tile);
19558
+ if (rowTile.x >= 0 && rowTile.y >= 0 && rowTile.y < layer.rows) {
19559
+ const idx = (rowTile.y * layerCols + rowTile.x) * 2;
19560
+ const gid = data2[idx];
19561
+ if (gid) {
19562
+ const flipMask = data2[idx + 1];
19563
+ if (!tilesetCache.contains(gid)) {
19564
+ tilesetCache = tilesets.getTilesetByGid(gid);
19565
+ }
19566
+ tilesetCache.drawTileRaw(
19567
+ renderer2,
19568
+ rowPos.x,
19569
+ rowPos.y,
19570
+ gid,
19571
+ flipMask
19572
+ );
19573
+ }
19129
19574
  }
19130
19575
  rowPos.x += this.tilewidth + this.sidelengthx;
19131
19576
  }
@@ -19161,9 +19606,22 @@ var TMXHexagonalRenderer = class extends TMXRenderer {
19161
19606
  rowPos.x += this.columnwidth;
19162
19607
  }
19163
19608
  for (; rowPos.x < rect.right && rowTile.x < endX; rowTile.x++) {
19164
- tile = layer.cellAt(rowTile.x, rowTile.y, false);
19165
- if (tile) {
19166
- tile.tileset.drawTile(renderer2, rowPos.x, rowPos.y, tile);
19609
+ if (rowTile.x >= 0 && rowTile.y >= 0 && rowTile.y < layer.rows) {
19610
+ const idx = (rowTile.y * layerCols + rowTile.x) * 2;
19611
+ const gid = data2[idx];
19612
+ if (gid) {
19613
+ const flipMask = data2[idx + 1];
19614
+ if (!tilesetCache.contains(gid)) {
19615
+ tilesetCache = tilesets.getTilesetByGid(gid);
19616
+ }
19617
+ tilesetCache.drawTileRaw(
19618
+ renderer2,
19619
+ rowPos.x,
19620
+ rowPos.y,
19621
+ gid,
19622
+ flipMask
19623
+ );
19624
+ }
19167
19625
  }
19168
19626
  rowPos.x += this.tilewidth + this.sidelengthx;
19169
19627
  }
@@ -19246,7 +19704,7 @@ var TMXIsometricRenderer = class extends TMXRenderer {
19246
19704
  vector2dPool.release(isoPos);
19247
19705
  }
19248
19706
  /**
19249
- * draw the tile map
19707
+ * draw the tile map (legacy entry point — accepts a fully-constructed Tile)
19250
19708
  * @ignore
19251
19709
  */
19252
19710
  drawTile(renderer2, x, y, tmxTile) {
@@ -19258,6 +19716,20 @@ var TMXIsometricRenderer = class extends TMXRenderer {
19258
19716
  tmxTile
19259
19717
  );
19260
19718
  }
19719
+ /**
19720
+ * draw a tile from raw (gid, flipMask, tileset) data — used by the hot
19721
+ * rendering loop to bypass Tile construction
19722
+ * @ignore
19723
+ */
19724
+ drawTileRaw(renderer2, x, y, gid, flipMask, tileset) {
19725
+ tileset.drawTileRaw(
19726
+ renderer2,
19727
+ (this.cols - 1) * tileset.tilewidth + (x - y) * tileset.tilewidth >> 1,
19728
+ -tileset.tilewidth + (x + y) * tileset.tileheight >> 2,
19729
+ gid,
19730
+ flipMask
19731
+ );
19732
+ }
19261
19733
  /**
19262
19734
  * draw the tile map
19263
19735
  * @ignore
@@ -19300,19 +19772,41 @@ var TMXIsometricRenderer = class extends TMXRenderer {
19300
19772
  }
19301
19773
  let shifted = inUpperHalf ^ inLeftHalf;
19302
19774
  const columnItr = vector2dPool.get().setV(rowItr);
19775
+ const layerCols = layer.cols;
19776
+ const layerRows = layer.rows;
19777
+ const data2 = layer.layerData;
19778
+ const tilesets = layer.tilesets;
19779
+ let tilesetCache = tileset;
19780
+ if (tilesetCache === null) {
19781
+ vector2dPool.release(columnItr);
19782
+ vector2dPool.release(rowItr);
19783
+ vector2dPool.release(tileEnd);
19784
+ vector2dPool.release(rectEnd);
19785
+ vector2dPool.release(startPos);
19786
+ return;
19787
+ }
19303
19788
  for (let y = startPos.y * 2; y - this.tileheight * 2 < rectEnd.y * 2; y += this.tileheight) {
19304
19789
  columnItr.setV(rowItr);
19305
19790
  for (let x = startPos.x; x < rectEnd.x; x += this.tilewidth) {
19306
- const tmxTile = layer.cellAt(columnItr.x, columnItr.y);
19307
- if (tmxTile) {
19308
- tileset = tmxTile.tileset;
19309
- const offset = tileset.tileoffset;
19310
- tileset.drawTile(
19311
- renderer2,
19312
- offset.x + x,
19313
- offset.y + y / 2 - tileset.tileheight,
19314
- tmxTile
19315
- );
19791
+ const cx = columnItr.x;
19792
+ const cy = columnItr.y;
19793
+ if (cx >= 0 && cx < layerCols && cy >= 0 && cy < layerRows) {
19794
+ const idx = (cy * layerCols + cx) * 2;
19795
+ const gid = data2[idx];
19796
+ if (gid !== 0) {
19797
+ const flipMask = data2[idx + 1];
19798
+ if (!tilesetCache.contains(gid)) {
19799
+ tilesetCache = tilesets.getTilesetByGid(gid);
19800
+ }
19801
+ const offset = tilesetCache.tileoffset;
19802
+ tilesetCache.drawTileRaw(
19803
+ renderer2,
19804
+ offset.x + x,
19805
+ offset.y + y / 2 - tilesetCache.tileheight,
19806
+ gid,
19807
+ flipMask
19808
+ );
19809
+ }
19316
19810
  }
19317
19811
  columnItr.x++;
19318
19812
  columnItr.y--;
@@ -19367,7 +19861,7 @@ var TMXOrthogonalRenderer = class extends TMXRenderer {
19367
19861
  return ret.set(x * this.tilewidth, y * this.tileheight);
19368
19862
  }
19369
19863
  /**
19370
- * draw the tile map
19864
+ * draw the tile map (legacy entry point — accepts a fully-constructed Tile)
19371
19865
  * @ignore
19372
19866
  */
19373
19867
  drawTile(renderer2, x, y, tmxTile) {
@@ -19379,6 +19873,20 @@ var TMXOrthogonalRenderer = class extends TMXRenderer {
19379
19873
  tmxTile
19380
19874
  );
19381
19875
  }
19876
+ /**
19877
+ * draw a tile from raw (gid, flipMask, tileset) data — used by the hot
19878
+ * rendering loop to bypass Tile construction
19879
+ * @ignore
19880
+ */
19881
+ drawTileRaw(renderer2, x, y, gid, flipMask, tileset) {
19882
+ tileset.drawTileRaw(
19883
+ renderer2,
19884
+ tileset.tileoffset.x + x * this.tilewidth,
19885
+ tileset.tileoffset.y + (y + 1) * this.tileheight - tileset.tileheight,
19886
+ gid,
19887
+ flipMask
19888
+ );
19889
+ }
19382
19890
  /**
19383
19891
  * draw the tile map
19384
19892
  * @ignore
@@ -19416,12 +19924,27 @@ var TMXOrthogonalRenderer = class extends TMXRenderer {
19416
19924
  default:
19417
19925
  break;
19418
19926
  }
19927
+ const cols = layer.cols;
19928
+ const data2 = layer.layerData;
19929
+ const tilesets = layer.tilesets;
19930
+ let tilesetCache = layer.tileset;
19931
+ if (tilesetCache === null) {
19932
+ vector2dPool.release(start);
19933
+ vector2dPool.release(end);
19934
+ return;
19935
+ }
19419
19936
  for (let y = start.y; y !== end.y; y += incY) {
19420
19937
  for (let x = start.x; x !== end.x; x += incX) {
19421
- const tmxTile = layer.cellAt(x, y, false);
19422
- if (tmxTile) {
19423
- this.drawTile(renderer2, x, y, tmxTile);
19938
+ const idx = (y * cols + x) * 2;
19939
+ const gid = data2[idx];
19940
+ if (!gid) {
19941
+ continue;
19424
19942
  }
19943
+ const flipMask = data2[idx + 1];
19944
+ if (!tilesetCache.contains(gid)) {
19945
+ tilesetCache = tilesets.getTilesetByGid(gid);
19946
+ }
19947
+ this.drawTileRaw(renderer2, x, y, gid, flipMask, tilesetCache);
19425
19948
  }
19426
19949
  }
19427
19950
  vector2dPool.release(start);
@@ -19490,7 +20013,7 @@ var TMXObliqueRenderer = class extends TMXOrthogonalRenderer {
19490
20013
  return ret.set(px + this.shearX * py, this.shearY * px + py);
19491
20014
  }
19492
20015
  /**
19493
- * draw the tile map
20016
+ * draw the tile map (legacy entry point — accepts a fully-constructed Tile)
19494
20017
  * @ignore
19495
20018
  */
19496
20019
  drawTile(renderer2, x, y, tmxTile) {
@@ -19499,6 +20022,16 @@ var TMXObliqueRenderer = class extends TMXOrthogonalRenderer {
19499
20022
  const dy = tileset.tileoffset.y + (y + 1) * this.tileheight - tileset.tileheight + this.skewY * x;
19500
20023
  tileset.drawTile(renderer2, dx, dy, tmxTile);
19501
20024
  }
20025
+ /**
20026
+ * draw a tile from raw (gid, flipMask, tileset) data — used by the hot
20027
+ * rendering loop to bypass Tile construction
20028
+ * @ignore
20029
+ */
20030
+ drawTileRaw(renderer2, x, y, gid, flipMask, tileset) {
20031
+ const dx = tileset.tileoffset.x + x * this.tilewidth + this.skewX * y;
20032
+ const dy = tileset.tileoffset.y + (y + 1) * this.tileheight - tileset.tileheight + this.skewY * x;
20033
+ tileset.drawTileRaw(renderer2, dx, dy, gid, flipMask);
20034
+ }
19502
20035
  /**
19503
20036
  * draw the given TMX Layer for the given area
19504
20037
  * @ignore
@@ -19569,12 +20102,25 @@ var TMXObliqueRenderer = class extends TMXOrthogonalRenderer {
19569
20102
  default:
19570
20103
  break;
19571
20104
  }
20105
+ const cols = layer.cols;
20106
+ const data2 = layer.layerData;
20107
+ const tilesets = layer.tilesets;
20108
+ let tilesetCache = layer.tileset;
20109
+ if (tilesetCache === null) {
20110
+ return;
20111
+ }
19572
20112
  for (let y = startY; y !== endY; y += incY) {
19573
20113
  for (let x = startX; x !== endX; x += incX) {
19574
- const tmxTile = layer.cellAt(x, y, false);
19575
- if (tmxTile) {
19576
- this.drawTile(renderer2, x, y, tmxTile);
20114
+ const idx = (y * cols + x) * 2;
20115
+ const gid = data2[idx];
20116
+ if (!gid) {
20117
+ continue;
19577
20118
  }
20119
+ const flipMask = data2[idx + 1];
20120
+ if (!tilesetCache.contains(gid)) {
20121
+ tilesetCache = tilesets.getTilesetByGid(gid);
20122
+ }
20123
+ this.drawTileRaw(renderer2, x, y, gid, flipMask, tilesetCache);
19578
20124
  }
19579
20125
  }
19580
20126
  }
@@ -19930,6 +20476,7 @@ var TMXGroup = class {
19930
20476
  };
19931
20477
 
19932
20478
  // src/level/tiled/TMXTileset.js
20479
+ var SCRATCH_MATRIX = new Matrix2d();
19933
20480
  var TMXTileset = class {
19934
20481
  /**
19935
20482
  * @param {object} tileset - tileset data in JSON format ({@link http://docs.mapeditor.org/en/stable/reference/tmx-map-format/#tileset})
@@ -20260,6 +20807,96 @@ var TMXTileset = class {
20260
20807
  renderer2.restore();
20261
20808
  }
20262
20809
  }
20810
+ /**
20811
+ * draw a tile at the specified position from raw (gid, flipMask) data
20812
+ *
20813
+ * Like {@link drawTile} but bypasses the {@link Tile} object entirely:
20814
+ * the renderer hot loop can pass the GID and flip mask straight from
20815
+ * `layer.layerData` without ever allocating a Tile instance.
20816
+ *
20817
+ * @param {CanvasRenderer|WebGLRenderer} renderer - a renderer instance
20818
+ * @param {number} dx - destination x position
20819
+ * @param {number} dy - destination y position
20820
+ * @param {number} gid - the tile's global id (with flip bits already stripped)
20821
+ * @param {number} flipMask - 3-bit packed flip mask (H=1, V=2, AD=4)
20822
+ * @ignore
20823
+ */
20824
+ drawTileRaw(renderer2, dx, dy, gid, flipMask) {
20825
+ let dw, dh;
20826
+ let tileImage;
20827
+ if (this.isCollection) {
20828
+ tileImage = this.imageCollection.get(gid);
20829
+ const tileWidth = tileImage.width;
20830
+ const tileHeight = tileImage.height;
20831
+ if (this.tilerendersize === "grid") {
20832
+ let scaleX = this.mapTilewidth / tileWidth;
20833
+ let scaleY = this.mapTileheight / tileHeight;
20834
+ if (this.fillmode === "preserve-aspect-fit") {
20835
+ const scale2 = Math.min(scaleX, scaleY);
20836
+ scaleX = scale2;
20837
+ scaleY = scale2;
20838
+ }
20839
+ dw = tileWidth * scaleX;
20840
+ dh = tileHeight * scaleY;
20841
+ dy += this.tileheight - dh;
20842
+ if (this.fillmode === "preserve-aspect-fit") {
20843
+ dx += (this.mapTilewidth - dw) / 2;
20844
+ dy -= (this.mapTileheight - dh) / 2;
20845
+ }
20846
+ } else {
20847
+ dw = tileWidth;
20848
+ dh = tileHeight;
20849
+ }
20850
+ } else {
20851
+ dw = this._renderDw;
20852
+ dh = this._renderDh;
20853
+ dy += this._renderDyOffset;
20854
+ dx += this._renderDxCenter;
20855
+ dy += this._renderDyCenter;
20856
+ }
20857
+ if (flipMask !== 0) {
20858
+ renderer2.save();
20859
+ renderer2.translate(dx, dy);
20860
+ renderer2.transform(
20861
+ buildFlipTransform(
20862
+ SCRATCH_MATRIX,
20863
+ flipMask,
20864
+ this.isCollection ? tileImage.width : this.tilewidth,
20865
+ this.isCollection ? tileImage.height : this.tileheight
20866
+ )
20867
+ );
20868
+ dx = dy = 0;
20869
+ }
20870
+ if (this.isCollection) {
20871
+ renderer2.drawImage(
20872
+ tileImage,
20873
+ 0,
20874
+ 0,
20875
+ tileImage.width,
20876
+ tileImage.height,
20877
+ dx,
20878
+ dy,
20879
+ dw,
20880
+ dh
20881
+ );
20882
+ } else {
20883
+ const offset = this.atlas[this.getViewTileId(gid)].offset;
20884
+ renderer2.drawImage(
20885
+ this.image,
20886
+ offset.x,
20887
+ offset.y,
20888
+ this.tilewidth,
20889
+ this.tileheight,
20890
+ dx,
20891
+ dy,
20892
+ dw + renderer2.uvOffset,
20893
+ dh + renderer2.uvOffset
20894
+ );
20895
+ }
20896
+ if (flipMask !== 0) {
20897
+ renderer2.restore();
20898
+ }
20899
+ }
20263
20900
  };
20264
20901
 
20265
20902
  // src/level/tiled/TMXTilesetGroup.js
@@ -20378,6 +21015,14 @@ function readTileset(data2, mapTilewidth, mapTileheight) {
20378
21015
  function readObjectGroup(map, data2, z) {
20379
21016
  return new TMXGroup(map, data2, z);
20380
21017
  }
21018
+ function refreshAbsoluteBounds(container) {
21019
+ container.forEach((child) => {
21020
+ child.updateBounds(true);
21021
+ if (child instanceof Container) {
21022
+ refreshAbsoluteBounds(child);
21023
+ }
21024
+ });
21025
+ }
20381
21026
  var TMXTileMap = class {
20382
21027
  /**
20383
21028
  * @param {string} levelId - name of TMX map
@@ -20539,12 +21184,12 @@ var TMXTileMap = class {
20539
21184
  Math.max(levelBounds.width, width),
20540
21185
  Math.max(levelBounds.height, height)
20541
21186
  );
20542
- container.pos.set(
20543
- Math.max(0, ~~((width - levelBounds.width) / 2)),
20544
- Math.max(0, ~~((height - levelBounds.height) / 2)),
20545
- // don't change the container z position if defined
20546
- container.pos.z
20547
- );
21187
+ const newX = Math.max(0, ~~((width - levelBounds.width) / 2));
21188
+ const newY = Math.max(0, ~~((height - levelBounds.height) / 2));
21189
+ if (container.pos.x !== newX || container.pos.y !== newY) {
21190
+ container.pos.set(newX, newY, container.pos.z);
21191
+ refreshAbsoluteBounds(container);
21192
+ }
20548
21193
  };
20549
21194
  var _setBounds = _setBounds2;
20550
21195
  const app = container.getRootAncestor().app;
@@ -21354,6 +21999,7 @@ var Sprite = class extends Renderable {
21354
21999
  * @param {number} [settings.flipX] - flip the sprite on the horizontal axis
21355
22000
  * @param {number} [settings.flipY] - flip the sprite on the vertical axis
21356
22001
  * @param {Vector2d} [settings.anchorPoint={x:0.5, y:0.5}] - Anchor point to draw the frame at (defaults to the center of the frame).
22002
+ * @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
22003
  * @example
21358
22004
  * // create a single sprite from a standalone image, with anchor in the center
21359
22005
  * let sprite = new me.Sprite(0, 0, {
@@ -21390,6 +22036,7 @@ var Sprite = class extends Renderable {
21390
22036
  this.offset = vector2dPool.get(0, 0);
21391
22037
  this.isVideo = false;
21392
22038
  this.source = null;
22039
+ this._normalMap = null;
21393
22040
  this.anim = {};
21394
22041
  this.resetAnim = void 0;
21395
22042
  this.current = {
@@ -21463,6 +22110,25 @@ var Sprite = class extends Renderable {
21463
22110
  this.textureAtlas = this.source.getAtlas();
21464
22111
  }
21465
22112
  }
22113
+ if (settings.image instanceof TextureAtlas && typeof settings.image.getNormalTexture === "function") {
22114
+ const fromAtlas = settings.image.getNormalTexture();
22115
+ if (fromAtlas) {
22116
+ this.normalMap = fromAtlas;
22117
+ }
22118
+ }
22119
+ if (this.normalMap === null && typeof settings.normalMap !== "undefined" && settings.normalMap !== null) {
22120
+ if (typeof settings.normalMap === "string") {
22121
+ const resolved = getImage(settings.normalMap);
22122
+ if (!resolved) {
22123
+ throw new Error(
22124
+ "me.Sprite: '" + settings.normalMap + "' normal map image not found!"
22125
+ );
22126
+ }
22127
+ this.normalMap = resolved;
22128
+ } else {
22129
+ this.normalMap = settings.normalMap;
22130
+ }
22131
+ }
21466
22132
  if (typeof settings.atlas !== "undefined") {
21467
22133
  this.textureAtlas = settings.atlas;
21468
22134
  this.atlasIndices = settings.atlasIndices;
@@ -21505,6 +22171,37 @@ var Sprite = class extends Renderable {
21505
22171
  this.setCurrentAnimation("default");
21506
22172
  }
21507
22173
  }
22174
+ /**
22175
+ * The optional normal-map image paired with this sprite's color
22176
+ * texture (SpriteIlluminator workflow). When set, the WebGL
22177
+ * renderer's lit pipeline samples this texture for per-pixel
22178
+ * lighting using `Stage._activeLights`. `null` when unlit.
22179
+ * Setting any non-image value (or anything without numeric
22180
+ * `width`/`height`) throws — assign `null` to clear.
22181
+ *
22182
+ * Silently ignored by the Canvas renderer.
22183
+ * @type {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap|null}
22184
+ */
22185
+ get normalMap() {
22186
+ return this._normalMap;
22187
+ }
22188
+ set normalMap(value) {
22189
+ if (value === null || value === void 0) {
22190
+ this._normalMap = null;
22191
+ return;
22192
+ }
22193
+ if (typeof value !== "object" || typeof value.width !== "number" || typeof value.height !== "number") {
22194
+ throw new TypeError(
22195
+ "Sprite.normalMap must be null or an image-like object with numeric width/height (HTMLImageElement, HTMLCanvasElement, OffscreenCanvas, ImageBitmap)"
22196
+ );
22197
+ }
22198
+ if (typeof value.videoWidth === "number") {
22199
+ throw new TypeError(
22200
+ "Sprite.normalMap does not support HTMLVideoElement (the lit pipeline caches the texture per image reference and would freeze on frame 0)"
22201
+ );
22202
+ }
22203
+ this._normalMap = value;
22204
+ }
21508
22205
  /**
21509
22206
  * return the flickering state of the object
21510
22207
  * @returns {boolean}
@@ -21854,9 +22551,32 @@ var Sprite = class extends Renderable {
21854
22551
  }
21855
22552
  return super.update(dt);
21856
22553
  }
22554
+ /**
22555
+ * Prepare the rendering context before drawing this sprite (automatically called by melonJS).
22556
+ * Extends `Renderable.preDraw` to publish this sprite's `normalMap` (if any)
22557
+ * on the renderer so the WebGL lit pipeline can pair it with the next
22558
+ * `drawImage` call. Cleared back in `postDraw`.
22559
+ * @param {Renderer} renderer - a renderer instance
22560
+ */
22561
+ preDraw(renderer2) {
22562
+ super.preDraw(renderer2);
22563
+ if (this._normalMap !== null) {
22564
+ renderer2.currentNormalMap = this._normalMap;
22565
+ }
22566
+ }
22567
+ /**
22568
+ * restore the rendering context after drawing this sprite (automatically called by melonJS).
22569
+ * @param {Renderer} renderer - a renderer instance
22570
+ */
22571
+ postDraw(renderer2) {
22572
+ if (this._normalMap !== null) {
22573
+ renderer2.currentNormalMap = null;
22574
+ }
22575
+ super.postDraw(renderer2);
22576
+ }
21857
22577
  /**
21858
22578
  * draw this sprite (automatically called by melonJS)
21859
- * @param {CanvasRenderer|WebGLRenderer} renderer - a renderer instance
22579
+ * @param {Renderer} renderer - a renderer instance
21860
22580
  * @param {Camera2d} [viewport] - the viewport to (re)draw
21861
22581
  */
21862
22582
  draw(renderer2) {
@@ -21915,6 +22635,7 @@ var Sprite = class extends Renderable {
21915
22635
  this.image.currentTime = 0;
21916
22636
  }
21917
22637
  this.image = void 0;
22638
+ this._normalMap = null;
21918
22639
  super.destroy();
21919
22640
  }
21920
22641
  };
@@ -22108,7 +22829,7 @@ var ColorMatrix = class extends Matrix3d {
22108
22829
  // src/video/webgl/utils/attributes.js
22109
22830
  function extractAttributes(gl, shader) {
22110
22831
  const attributes = {};
22111
- const attrRx = /attribute\s+\w+\s+(\w+)/g;
22832
+ const attrRx = /(?:^|\n)[ \t]*(?:attribute|in)[ \t]+(?:\w+[ \t]+)+(\w+)/g;
22112
22833
  let match;
22113
22834
  let i = 0;
22114
22835
  while (match = attrRx.exec(shader.vertex)) {
@@ -22119,10 +22840,18 @@ function extractAttributes(gl, shader) {
22119
22840
 
22120
22841
  // src/video/webgl/utils/precision.js
22121
22842
  function setPrecision(src, precision) {
22122
- if (src.substring(0, 9) !== "precision") {
22123
- return "precision " + precision + " float;\n" + src;
22843
+ if (/^\s*(?:#version[^\n]*\n)?\s*precision\b/.test(src)) {
22844
+ return src;
22124
22845
  }
22125
- return src;
22846
+ if (src.substring(0, 8) === "#version") {
22847
+ const inject = "\nprecision " + precision + " float;\nprecision " + precision + " int;";
22848
+ const nl = src.indexOf("\n");
22849
+ if (nl < 0) {
22850
+ return src + inject;
22851
+ }
22852
+ return src.substring(0, nl) + inject + src.substring(nl);
22853
+ }
22854
+ return "precision " + precision + " float;\n" + src;
22126
22855
  }
22127
22856
  function getMaxShaderPrecision(gl, highPrecision = true) {
22128
22857
  if (highPrecision && gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT).precision > 0 && gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT).precision > 0) {
@@ -22188,17 +22917,56 @@ var fnHash = {
22188
22917
  ivec2: "2iv",
22189
22918
  ivec3: "3iv",
22190
22919
  ivec4: "4iv",
22920
+ uvec2: "2uiv",
22921
+ uvec3: "3uiv",
22922
+ uvec4: "4uiv",
22191
22923
  mat2: "Matrix2fv",
22192
22924
  mat3: "Matrix3fv",
22193
22925
  mat4: "Matrix4fv",
22194
- sampler2D: "1i"
22926
+ sampler2D: "1i",
22927
+ // WebGL2 integer-typed samplers — bound to a unit just like a
22928
+ // `sampler2D`; the GLSL `usampler2D` / `isampler2D` types let the
22929
+ // shader read raw integer values via `texelFetch` instead of the
22930
+ // normalized-float `texture()` path.
22931
+ usampler2D: "1i",
22932
+ isampler2D: "1i"
22195
22933
  };
22934
+ function valuesMatch(cached, val) {
22935
+ if (cached === void 0) {
22936
+ return false;
22937
+ }
22938
+ if (val !== null && typeof val === "object" && typeof val.length === "number") {
22939
+ if (cached.length !== val.length) {
22940
+ return false;
22941
+ }
22942
+ for (let i = 0; i < val.length; i++) {
22943
+ if (cached[i] !== val[i]) {
22944
+ return false;
22945
+ }
22946
+ }
22947
+ return true;
22948
+ }
22949
+ return cached === val;
22950
+ }
22951
+ function captureValue(prev, val) {
22952
+ if (val === null || typeof val !== "object" || typeof val.length !== "number") {
22953
+ return val;
22954
+ }
22955
+ if (prev !== void 0 && typeof prev === "object" && typeof prev.length === "number" && prev.length === val.length) {
22956
+ for (let i = 0; i < val.length; i++) {
22957
+ prev[i] = val[i];
22958
+ }
22959
+ return prev;
22960
+ }
22961
+ return typeof val.slice === "function" ? val.slice() : Array.from(val);
22962
+ }
22196
22963
  function extractUniforms(gl, shader) {
22197
22964
  const uniforms = {};
22198
22965
  const uniRx = /uniform\s+(\w+)\s+(\w+)/g;
22199
22966
  const uniformsData = {};
22200
22967
  const descriptor = {};
22201
22968
  const locations = {};
22969
+ const cache2 = {};
22202
22970
  let match;
22203
22971
  [shader.vertex, shader.fragment].forEach((shader2) => {
22204
22972
  while (match = uniRx.exec(shader2)) {
@@ -22217,6 +22985,10 @@ function extractUniforms(gl, shader) {
22217
22985
  set: (function(name2, type2, fn) {
22218
22986
  if (/^mat/.test(type2)) {
22219
22987
  return function(val) {
22988
+ if (valuesMatch(cache2[name2], val)) {
22989
+ return;
22990
+ }
22991
+ cache2[name2] = captureValue(cache2[name2], val);
22220
22992
  gl[fn](locations[name2], false, val);
22221
22993
  };
22222
22994
  } else {
@@ -22225,6 +22997,10 @@ function extractUniforms(gl, shader) {
22225
22997
  if (val.length && !/v$/.test(fn)) {
22226
22998
  fnv += "v";
22227
22999
  }
23000
+ if (valuesMatch(cache2[name2], val)) {
23001
+ return;
23002
+ }
23003
+ cache2[name2] = captureValue(cache2[name2], val);
22228
23004
  gl[fnv](locations[name2], val);
22229
23005
  };
22230
23006
  }
@@ -22343,8 +23119,6 @@ var GLShader = class {
22343
23119
  stride,
22344
23120
  element.offset
22345
23121
  );
22346
- } else {
22347
- gl.disableVertexAttribArray(index);
22348
23122
  }
22349
23123
  }
22350
23124
  }
@@ -23308,6 +24082,13 @@ var Camera2d = class extends Renderable {
23308
24082
  } else {
23309
24083
  renderer2.setProjection(this.projectionMatrix);
23310
24084
  }
24085
+ const stage = state_default.current();
24086
+ renderer2.setLightUniforms(
24087
+ stage?._activeLights,
24088
+ stage?.ambientLightingColor,
24089
+ translateX,
24090
+ translateY
24091
+ );
23311
24092
  container.preDraw(r);
23312
24093
  if (isNonDefault) {
23313
24094
  const view = this.worldView;
@@ -23324,6 +24105,9 @@ var Camera2d = class extends Renderable {
23324
24105
  this.drawFX(renderer2);
23325
24106
  container.postDraw(r);
23326
24107
  this.postDraw(r);
24108
+ if (stage) {
24109
+ stage.drawLighting(renderer2, this, translateX, translateY);
24110
+ }
23327
24111
  r.endPostEffect(this);
23328
24112
  if (this._colorMatrixEffect) {
23329
24113
  const idx = this.postEffects.indexOf(this._colorMatrixEffect);
@@ -23361,28 +24145,50 @@ var Stage = class {
23361
24145
  cameras;
23362
24146
  /**
23363
24147
  * The list of active lights in this stage.
23364
- * (Note: Canvas Rendering mode will only properly support one light per stage)
24148
+ *
24149
+ * Since 19.3.0, `Light2d` is a first-class world Renderable — the
24150
+ * recommended pattern is to add lights directly to `app.world` (or any
24151
+ * container, including a sprite, so the light follows it via parent
24152
+ * transforms). The `lights` Map remains for backward compatibility:
24153
+ * any entry added via `this.lights.set(name, light)` in
24154
+ * `onResetEvent()` is automatically adopted into the world tree at
24155
+ * stage reset time so it renders normally.
23365
24156
  * @see Light2d
23366
24157
  * @see Stage.ambientLight
23367
24158
  * @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
24159
+ * // recommended:
24160
+ * const whiteLight = new Light2d(100, 100, 140, 140, "#fff", 0.7);
24161
+ * app.world.addChild(whiteLight);
24162
+ *
24163
+ * // legacy (still works, auto-adopted into world):
23371
24164
  * this.lights.set("whiteLight", whiteLight);
23372
- * // set a dark ambient light
24165
+ *
23373
24166
  * 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
24167
  */
23379
24168
  lights;
24169
+ /**
24170
+ * Internal set of active lights, auto-populated by `Light2d`'s
24171
+ * `onActivateEvent` / `onDeactivateEvent` hooks. Used by Camera2d's
24172
+ * ambient-overlay pass to compute the cutouts.
24173
+ * @ignore
24174
+ */
24175
+ _activeLights;
23380
24176
  /**
23381
24177
  * an ambient light that will be added to the stage rendering
23382
24178
  * @default "#000000"
23383
24179
  * @see Light2d
23384
24180
  */
23385
24181
  ambientLight;
24182
+ /**
24183
+ * Base light level applied to every normal-mapped sprite in the
24184
+ * lit rendering path. Unlike {@link Stage#ambientLight} (which is
24185
+ * the dark overlay punched by each light's cutout), this color is
24186
+ * added to every lit pixel so unlit areas don't render pure
24187
+ * black. Defaults to black (0, 0, 0) — sprites without a
24188
+ * `normalMap` ignore it entirely.
24189
+ * @default "#000000"
24190
+ */
24191
+ ambientLightingColor;
23386
24192
  /**
23387
24193
  * The given constructor options
23388
24194
  */
@@ -23396,9 +24202,26 @@ var Stage = class {
23396
24202
  constructor(settings) {
23397
24203
  this.cameras = /* @__PURE__ */ new Map();
23398
24204
  this.lights = /* @__PURE__ */ new Map();
24205
+ this._activeLights = /* @__PURE__ */ new Set();
23399
24206
  this.ambientLight = new Color(0, 0, 0, 0);
24207
+ this.ambientLightingColor = new Color(0, 0, 0, 1);
23400
24208
  this.settings = Object.assign({}, default_settings, settings || {});
23401
24209
  }
24210
+ /**
24211
+ * Called by `Light2d.onActivateEvent` to register the light with the
24212
+ * stage's ambient-overlay cutout list. Users normally don't call this.
24213
+ * @ignore
24214
+ */
24215
+ _registerLight(light) {
24216
+ this._activeLights.add(light);
24217
+ }
24218
+ /**
24219
+ * Called by `Light2d.onDeactivateEvent` to deregister the light.
24220
+ * @ignore
24221
+ */
24222
+ _unregisterLight(light) {
24223
+ this._activeLights.delete(light);
24224
+ }
23402
24225
  /**
23403
24226
  * Object reset function
23404
24227
  * @ignore
@@ -23419,6 +24242,13 @@ var Stage = class {
23419
24242
  }
23420
24243
  emit(STAGE_RESET, this);
23421
24244
  this.onResetEvent(app, ...extraArgs);
24245
+ if (app && app.world) {
24246
+ this.lights.forEach((light) => {
24247
+ if (!light.ancestor) {
24248
+ app.world.addChild(light);
24249
+ }
24250
+ });
24251
+ }
23422
24252
  }
23423
24253
  /**
23424
24254
  * update function
@@ -23433,40 +24263,60 @@ var Stage = class {
23433
24263
  isDirty = true;
23434
24264
  }
23435
24265
  });
23436
- this.lights.forEach((light) => {
23437
- if (light.update()) {
23438
- isDirty = true;
23439
- }
23440
- });
23441
24266
  return isDirty;
23442
24267
  }
23443
24268
  /**
23444
24269
  * draw the current stage
24270
+ *
24271
+ * Lights are rendered as part of the world tree (they're now first-class
24272
+ * Renderables) and the ambient overlay pass runs inside each Camera's
24273
+ * post-effect FBO bracket via {@link Stage#drawLighting}.
23445
24274
  * @ignore
23446
24275
  * @param renderer - the renderer object to draw with
23447
24276
  * @param world - the world object to draw
23448
24277
  */
23449
24278
  draw(renderer2, world) {
23450
- const r = renderer2;
23451
24279
  this.cameras.forEach((camera) => {
23452
24280
  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
24281
  });
23469
24282
  }
24283
+ /**
24284
+ * Draw the stage's ambient-light overlay with cutouts for each active
24285
+ * light. Called from each `Camera2d` inside its post-effect FBO bracket —
24286
+ * lights themselves render via the world tree (they're standard
24287
+ * Renderables); this pass only paints the dark fill that the lights cut
24288
+ * holes through.
24289
+ *
24290
+ * Subclasses can override this method to implement custom lighting (e.g.
24291
+ * per-pixel normal-mapped lighting via a custom shader). Called once per
24292
+ * camera per frame.
24293
+ * @param renderer - the active renderer
24294
+ * @param camera - the camera currently rendering this stage
24295
+ * @param translateX - the same world-to-screen X translate that
24296
+ * `Camera2d.draw()` applies to the world container (i.e.
24297
+ * `camera.pos.x + camera.offset.x` for the default camera, plus
24298
+ * the container's own offset for non-default cameras)
24299
+ * @param translateY - the world-to-screen Y translate (see `translateX`)
24300
+ */
24301
+ drawLighting(renderer2, camera, translateX = camera.pos.x + camera.offset.x, translateY = camera.pos.y + camera.offset.y) {
24302
+ if (this.ambientLight.alpha === 0) {
24303
+ return;
24304
+ }
24305
+ const r = renderer2;
24306
+ r.save();
24307
+ const tx = translateX;
24308
+ const ty = translateY;
24309
+ if (tx !== 0 || ty !== 0) {
24310
+ r.translate(-tx, -ty);
24311
+ }
24312
+ this._activeLights.forEach((light) => {
24313
+ r.setMask(light.getVisibleArea(), true);
24314
+ });
24315
+ r.setColor(this.ambientLight);
24316
+ r.fillRect(tx, ty, camera.width, camera.height);
24317
+ r.clearMask();
24318
+ r.restore();
24319
+ }
23470
24320
  /**
23471
24321
  * destroy function
23472
24322
  * @ignore
@@ -23474,9 +24324,12 @@ var Stage = class {
23474
24324
  destroy(app) {
23475
24325
  this.cameras.clear();
23476
24326
  this.lights.forEach((light) => {
23477
- light.destroy();
24327
+ if (!light.ancestor) {
24328
+ light.destroy();
24329
+ }
23478
24330
  });
23479
24331
  this.lights.clear();
24332
+ this._activeLights.clear();
23480
24333
  this.onDestroyEvent(app);
23481
24334
  }
23482
24335
  /**
@@ -26798,13 +27651,9 @@ var ImageLayer = class extends Sprite {
26798
27651
  if (this.mask) {
26799
27652
  renderer2.setMask(this.mask);
26800
27653
  }
26801
- renderer2.drawPattern(
26802
- this._pattern,
26803
- 0,
26804
- 0,
26805
- viewport.width * 2,
26806
- viewport.height * 2
26807
- );
27654
+ const drawW = this.repeatX ? viewport.width * 2 : width;
27655
+ const drawH = this.repeatY ? viewport.height * 2 : height;
27656
+ renderer2.drawPattern(this._pattern, 0, 0, drawW, drawH);
26808
27657
  }
26809
27658
  // called when the layer is removed from the game world or a container
26810
27659
  onDeactivateEvent() {
@@ -26825,64 +27674,33 @@ var ImageLayer = class extends Sprite {
26825
27674
  };
26826
27675
 
26827
27676
  // 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
27677
  var Light2d = class extends Renderable {
26879
27678
  /**
26880
- * @param {number} x - The horizontal position of the light.
26881
- * @param {number} y - The vertical position of the light.
27679
+ * Create a 2D point light.
27680
+ *
27681
+ * A `Light2d` is a first-class world Renderable: add it to a container
27682
+ * with `app.world.addChild(light)` (or any sub-container, including a
27683
+ * `Sprite`, so the light follows the parent via its transform). On
27684
+ * activation, the light auto-registers with the active `Stage`'s
27685
+ * lighting set so the ambient overlay (`Stage.ambientLight`) cuts a
27686
+ * hole at the light's visible area, and a radial gradient from the
27687
+ * given `color` (full intensity at center → fully transparent at the
27688
+ * radius) is composited additively on top — producing a soft spot
27689
+ * light. Rendering happens inside each `Camera2d`'s post-effect FBO
27690
+ * bracket so any camera shader (vignette, color-matrix, scanlines,
27691
+ * etc.) wraps the lighting output.
27692
+ *
27693
+ * Set `radiusY` to a different value than `radiusX` for a stretched
27694
+ * (elliptical) light. The `intensity` parameter scales the gradient's
27695
+ * inner alpha; the `Stage.ambientLight` color and alpha control how
27696
+ * dark the unlit areas are. Use `light.blendMode` to override the
27697
+ * default additive blend if needed.
27698
+ * @param {number} x - The horizontal position of the light's center (matches `Ellipse(x, y, w, h)` conventions).
27699
+ * @param {number} y - The vertical position of the light's center.
26882
27700
  * @param {number} radiusX - The horizontal radius of the light.
26883
27701
  * @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.
27702
+ * @param {Color|string} [color="#FFF"] - The color of the light at full intensity.
27703
+ * @param {number} [intensity=0.7] - The peak alpha of the radial gradient at the light's center (0–1).
26886
27704
  */
26887
27705
  constructor(x, y, radiusX, radiusY = radiusX, color = "#FFF", intensity = 0.7) {
26888
27706
  super(x, y, radiusX * 2, radiusY * 2);
@@ -26892,32 +27710,77 @@ var Light2d = class extends Renderable {
26892
27710
  this.intensity = intensity;
26893
27711
  this.blendMode = "lighter";
26894
27712
  this.visibleArea = ellipsePool.get(
26895
- this.centerX,
26896
- this.centerY,
27713
+ this.pos.x,
27714
+ this.pos.y,
26897
27715
  this.width,
26898
27716
  this.height
26899
27717
  );
26900
- this.texture = new canvasrendertarget_default(this.width, this.height, {
26901
- offscreenCanvas: false
26902
- });
26903
- this.anchorPoint.set(0, 0);
26904
- createGradient(this);
27718
+ this.anchorPoint.set(0.5, 0.5);
27719
+ this.illuminationOnly = false;
27720
+ this.lightHeight = Math.max(radiusX, radiusY) * 0.075;
27721
+ }
27722
+ /**
27723
+ * the horizontal coordinate of this light's center.
27724
+ * Overrides Rect's getter, which assumes `pos` is the bbox top-left and
27725
+ * returns `pos.x + width/2`. Light2d uses `anchorPoint = (0.5, 0.5)`, so
27726
+ * `pos` already IS the center.
27727
+ * @type {number}
27728
+ */
27729
+ get centerX() {
27730
+ return this.pos.x;
27731
+ }
27732
+ set centerX(value) {
27733
+ this.pos.x = value;
27734
+ this.recalc();
27735
+ this.updateBounds();
27736
+ }
27737
+ /**
27738
+ * the vertical coordinate of this light's center.
27739
+ * @see Light2d#centerX
27740
+ * @type {number}
27741
+ */
27742
+ get centerY() {
27743
+ return this.pos.y;
27744
+ }
27745
+ set centerY(value) {
27746
+ this.pos.y = value;
27747
+ this.recalc();
27748
+ this.updateBounds();
26905
27749
  }
26906
27750
  /**
26907
- * returns a geometry representing the visible area of this light
27751
+ * Set new radii for this light.
27752
+ *
27753
+ * Updates `radiusX`/`radiusY` and the underlying bbox (via
27754
+ * `Renderable.resize(width, height)`) so `getBounds()` and
27755
+ * `getVisibleArea()` — which feed the ambient-cutout pass — track the
27756
+ * new size. The Canvas renderer's gradient cache auto-invalidates on
27757
+ * next draw via its property comparison; the WebGL procedural shader
27758
+ * adapts to the new dimensions automatically.
27759
+ *
27760
+ * Named `setRadii` (not `resize`) so it does not shadow
27761
+ * `Renderable.resize(width, height)` — code that operates on a
27762
+ * generic `Renderable` and calls `.resize(w, h)` keeps working when
27763
+ * the instance happens to be a `Light2d`.
27764
+ * @param {number} radiusX - new horizontal radius
27765
+ * @param {number} [radiusY=radiusX] - new vertical radius
27766
+ */
27767
+ setRadii(radiusX, radiusY = radiusX) {
27768
+ this.radiusX = radiusX;
27769
+ this.radiusY = radiusY;
27770
+ this.resize(radiusX * 2, radiusY * 2);
27771
+ }
27772
+ /**
27773
+ * returns a geometry representing the visible area of this light, in
27774
+ * world-space coordinates (so it aligns with the rendered gradient
27775
+ * regardless of camera scroll or container parenting).
26908
27776
  * @returns {Ellipse} the light visible mask
26909
27777
  */
26910
27778
  getVisibleArea() {
26911
- return this.visibleArea.setShape(
26912
- this.getBounds().centerX,
26913
- this.getBounds().centerY,
26914
- this.width,
26915
- this.height
26916
- );
27779
+ const b = this.getBounds();
27780
+ return this.visibleArea.setShape(b.centerX, b.centerY, b.width, b.height);
26917
27781
  }
26918
27782
  /**
26919
27783
  * update function
26920
- * @param {number} dt - time since the last update in milliseconds.
26921
27784
  * @returns {boolean} true if dirty
26922
27785
  */
26923
27786
  update() {
@@ -26925,25 +27788,50 @@ var Light2d = class extends Renderable {
26925
27788
  }
26926
27789
  /**
26927
27790
  * 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
27791
+ * @param {Renderer} renderer - a renderer instance
26931
27792
  */
26932
27793
  preDraw(renderer2) {
26933
27794
  super.preDraw(renderer2);
26934
27795
  renderer2.setBlendMode(this.blendMode);
26935
27796
  }
26936
27797
  /**
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
27798
+ * draw this Light2d (automatically called by melonJS).
27799
+ *
27800
+ * Delegates to `renderer.drawLight(this)` each renderer picks its
27801
+ * own implementation (procedural shader on WebGL; cached `Gradient`
27802
+ * rasterized into a shared `CanvasRenderTarget` on Canvas). Light2d
27803
+ * itself doesn't know which path is used.
27804
+ * @param {Renderer} renderer - a renderer instance
26940
27805
  */
26941
27806
  draw(renderer2) {
26942
- renderer2.drawImage(
26943
- this.texture.canvas,
26944
- this.getBounds().x,
26945
- this.getBounds().y
26946
- );
27807
+ if (this.illuminationOnly) {
27808
+ return;
27809
+ }
27810
+ renderer2.drawLight(this);
27811
+ }
27812
+ /**
27813
+ * Auto-register this light with the active Stage's lighting set when
27814
+ * added to a container. The Stage uses that set to build the ambient
27815
+ * overlay cutouts; rendering the light itself is handled normally as
27816
+ * part of the world tree walk.
27817
+ * @ignore
27818
+ */
27819
+ onActivateEvent() {
27820
+ const stage = state_default.current();
27821
+ if (stage && typeof stage._registerLight === "function") {
27822
+ stage._registerLight(this);
27823
+ }
27824
+ }
27825
+ /**
27826
+ * Auto-deregister this light from the active Stage's lighting set when
27827
+ * removed from a container.
27828
+ * @ignore
27829
+ */
27830
+ onDeactivateEvent() {
27831
+ const stage = state_default.current();
27832
+ if (stage && typeof stage._unregisterLight === "function") {
27833
+ stage._unregisterLight(this);
27834
+ }
26947
27835
  }
26948
27836
  /**
26949
27837
  * Destroy function<br>
@@ -26952,9 +27840,6 @@ var Light2d = class extends Renderable {
26952
27840
  destroy() {
26953
27841
  colorPool.release(this.color);
26954
27842
  this.color = void 0;
26955
- const renderer2 = this.parentApp?.renderer ?? game.renderer;
26956
- this.texture.destroy(renderer2);
26957
- this.texture = void 0;
26958
27843
  ellipsePool.release(this.visibleArea);
26959
27844
  this.visibleArea = void 0;
26960
27845
  super.destroy();
@@ -27406,7 +28291,7 @@ var Trigger = class extends Renderable {
27406
28291
  };
27407
28292
 
27408
28293
  // src/version.ts
27409
- var version = "19.2.0";
28294
+ var version = "19.4.0";
27410
28295
 
27411
28296
  // src/system/bootstrap.ts
27412
28297
  var initialized = false;
@@ -28267,10 +29152,7 @@ function enablePointerEvent() {
28267
29152
  if (activeEventList.indexOf(events[i]) !== -1) {
28268
29153
  pointerEventTarget.addEventListener(
28269
29154
  events[i],
28270
- throttle(
28271
- onMoveEvent,
28272
- throttlingInterval
28273
- ),
29155
+ throttle(onMoveEvent, throttlingInterval),
28274
29156
  { passive: true }
28275
29157
  // do not preventDefault on Move events
28276
29158
  );
@@ -28783,7 +29665,7 @@ function testEllipseEllipse(a, ellipseA, b, ellipseB, response) {
28783
29665
  return testEllipsePolygon(a, ellipseA, b, ellipseB.toPolygon(), response);
28784
29666
  }
28785
29667
  }
28786
- const differenceV = T_VECTORS[--T_VECTORS_IDX].copy(b.pos).add(b.ancestor.getAbsolutePosition()).add(ellipseB.pos).sub(a.pos).add(a.ancestor.getAbsolutePosition()).sub(ellipseA.pos);
29668
+ const differenceV = T_VECTORS[--T_VECTORS_IDX].copy(b.pos).add(b.ancestor.getAbsolutePosition()).add(ellipseB.pos).sub(a.pos).sub(a.ancestor.getAbsolutePosition()).sub(ellipseA.pos);
28787
29669
  const radiusA = ellipseA.radius;
28788
29670
  const radiusB = ellipseB.radius;
28789
29671
  const totalRadius = radiusA + radiusB;
@@ -28810,7 +29692,7 @@ function testPolygonEllipse(a, polyA, b, ellipseB, response) {
28810
29692
  if (ellipseB.radiusV.x !== ellipseB.radiusV.y) {
28811
29693
  return testPolygonPolygon(a, polyA, b, ellipseB.toPolygon(), response);
28812
29694
  }
28813
- const circlePos = T_VECTORS[--T_VECTORS_IDX].copy(b.pos).add(b.ancestor.getAbsolutePosition()).add(ellipseB.pos).sub(a.pos).add(a.ancestor.getAbsolutePosition()).sub(polyA.pos);
29695
+ const circlePos = T_VECTORS[--T_VECTORS_IDX].copy(b.pos).add(b.ancestor.getAbsolutePosition()).add(ellipseB.pos).sub(a.pos).sub(a.ancestor.getAbsolutePosition()).sub(polyA.pos);
28814
29696
  const radius = ellipseB.radius;
28815
29697
  const radius2 = radius * radius;
28816
29698
  const points = polyA.points;
@@ -29436,6 +30318,7 @@ var World = class extends Container {
29436
30318
  this.fps = 60;
29437
30319
  this.gravity = new Vector2d(0, 0.98);
29438
30320
  this.preRender = false;
30321
+ this.gpuTilemap = true;
29439
30322
  this.bodies = /* @__PURE__ */ new Set();
29440
30323
  this.broadphase = new QuadTree(
29441
30324
  this,
@@ -30008,114 +30891,92 @@ function generateJoinCircles(centers, radius) {
30008
30891
  return verts;
30009
30892
  }
30010
30893
 
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";
30894
+ // src/video/webgl/lighting/constants.ts
30895
+ var MAX_LIGHTS = 8;
30016
30896
 
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);
30897
+ // src/video/webgl/shaders/multitexture-lit.js
30898
+ function buildSamplerSelect(varName, samplerPrefix, count, target) {
30899
+ const lines = [];
30900
+ for (let i = 0; i < count; i++) {
30901
+ if (i === 0) {
30902
+ lines.push(" if (" + varName + " < 0.5) {");
30103
30903
  } else {
30104
- return this.bufferF32;
30904
+ lines.push(" } else if (" + varName + " < " + (i + 0.5) + ") {");
30105
30905
  }
30906
+ lines.push(
30907
+ " " + target + " = texture2D(" + samplerPrefix + i + ", vRegion);"
30908
+ );
30106
30909
  }
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
- }
30910
+ lines.push(" } else {");
30911
+ lines.push(
30912
+ " " + target + " = texture2D(" + samplerPrefix + "0, vRegion);"
30913
+ );
30914
+ lines.push(" }");
30915
+ return lines;
30916
+ }
30917
+ function buildLitMultiTextureFragment(maxTextures) {
30918
+ const count = Math.max(maxTextures, 1);
30919
+ const lines = [];
30920
+ for (let i = 0; i < count; i++) {
30921
+ lines.push("uniform sampler2D uSampler" + i + ";");
30117
30922
  }
30118
- };
30923
+ for (let i = 0; i < count; i++) {
30924
+ lines.push("uniform sampler2D uNormalSampler" + i + ";");
30925
+ }
30926
+ lines.push("uniform int uLightCount;");
30927
+ lines.push("uniform vec4 uLightPos[" + MAX_LIGHTS + "];");
30928
+ lines.push("uniform vec3 uLightColor[" + MAX_LIGHTS + "];");
30929
+ lines.push("uniform float uLightHeight[" + MAX_LIGHTS + "];");
30930
+ lines.push("uniform vec3 uAmbient;");
30931
+ lines.push("varying vec4 vColor;");
30932
+ lines.push("varying vec2 vRegion;");
30933
+ lines.push("varying float vTextureId;");
30934
+ lines.push("varying float vNormalTextureId;");
30935
+ lines.push("varying vec2 vWorldPos;");
30936
+ lines.push("");
30937
+ lines.push("void main(void) {");
30938
+ lines.push(" vec4 color;");
30939
+ lines.push(...buildSamplerSelect("vTextureId", "uSampler", count, "color"));
30940
+ lines.push(" if (vNormalTextureId < -0.5) {");
30941
+ lines.push(" gl_FragColor = color * vColor;");
30942
+ lines.push(" return;");
30943
+ lines.push(" }");
30944
+ lines.push(" vec4 normalSample;");
30945
+ lines.push(
30946
+ ...buildSamplerSelect(
30947
+ "vNormalTextureId",
30948
+ "uNormalSampler",
30949
+ count,
30950
+ "normalSample"
30951
+ )
30952
+ );
30953
+ lines.push(
30954
+ " vec3 normal = normalize(normalSample.rgb * 2.0 - vec3(1.0));"
30955
+ );
30956
+ lines.push(" normal.y = -normal.y;");
30957
+ lines.push(" vec3 lighting = uAmbient;");
30958
+ lines.push(" for (int i = 0; i < " + MAX_LIGHTS + "; i++) {");
30959
+ lines.push(" if (i >= uLightCount) break;");
30960
+ lines.push(" vec4 lp = uLightPos[i];");
30961
+ lines.push(" vec2 toLight = lp.xy - vWorldPos;");
30962
+ lines.push(" float dist = length(toLight);");
30963
+ lines.push(" float linear = max(0.0, 1.0 - dist / max(lp.z, 1.0));");
30964
+ lines.push(" float att = linear * linear;");
30965
+ lines.push(
30966
+ " vec3 lightDir = normalize(vec3(toLight, uLightHeight[i]));"
30967
+ );
30968
+ lines.push(" float NdotL = max(0.0, dot(normal, lightDir));");
30969
+ lines.push(" lighting += uLightColor[i] * (lp.w * att * NdotL);");
30970
+ lines.push(" }");
30971
+ lines.push(
30972
+ " gl_FragColor = vec4(color.rgb * lighting, color.a) * vColor;"
30973
+ );
30974
+ lines.push("}");
30975
+ return lines.join("\n");
30976
+ }
30977
+
30978
+ // src/video/webgl/shaders/quad-multi-lit.vert
30979
+ 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
30980
 
30120
30981
  // src/video/buffer/index.js
30121
30982
  var IndexBuffer = class {
@@ -30216,6 +31077,145 @@ var WebGLIndexBuffer = class extends IndexBuffer {
30216
31077
  }
30217
31078
  };
30218
31079
 
31080
+ // src/video/webgl/shaders/multitexture.js
31081
+ function buildMultiTextureFragment(maxTextures) {
31082
+ const count = Math.max(maxTextures, 1);
31083
+ const lines = [];
31084
+ for (let i = 0; i < count; i++) {
31085
+ lines.push("uniform sampler2D uSampler" + i + ";");
31086
+ }
31087
+ lines.push("varying vec4 vColor;");
31088
+ lines.push("varying vec2 vRegion;");
31089
+ lines.push("varying float vTextureId;");
31090
+ lines.push("");
31091
+ lines.push("void main(void) {");
31092
+ lines.push(" vec4 color;");
31093
+ for (let i = 0; i < count; i++) {
31094
+ if (i === 0) {
31095
+ lines.push(" if (vTextureId < 0.5) {");
31096
+ } else {
31097
+ lines.push(" } else if (vTextureId < " + (i + 0.5) + ") {");
31098
+ }
31099
+ lines.push(" color = texture2D(uSampler" + i + ", vRegion);");
31100
+ }
31101
+ lines.push(" } else {");
31102
+ lines.push(" color = texture2D(uSampler0, vRegion);");
31103
+ lines.push(" }");
31104
+ lines.push(" gl_FragColor = color * vColor;");
31105
+ lines.push("}");
31106
+ return lines.join("\n");
31107
+ }
31108
+
31109
+ // src/video/webgl/shaders/quad-multi.vert
31110
+ 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";
31111
+
31112
+ // src/video/buffer/vertex.js
31113
+ var VertexArrayBuffer = class {
31114
+ constructor(vertexSize, maxVertex) {
31115
+ this.vertexSize = vertexSize;
31116
+ this.maxVertex = maxVertex;
31117
+ this.vertexCount = 0;
31118
+ this.buffer = new ArrayBuffer(
31119
+ this.maxVertex * this.vertexSize * Float32Array.BYTES_PER_ELEMENT
31120
+ );
31121
+ this.bufferF32 = new Float32Array(this.buffer);
31122
+ this.bufferU32 = new Uint32Array(this.buffer);
31123
+ }
31124
+ /**
31125
+ * clear the vertex array buffer
31126
+ * @ignore
31127
+ */
31128
+ clear() {
31129
+ this.vertexCount = 0;
31130
+ }
31131
+ /**
31132
+ * return true if full
31133
+ * @ignore
31134
+ */
31135
+ isFull(vertex) {
31136
+ return this.vertexCount + vertex >= this.maxVertex;
31137
+ }
31138
+ /**
31139
+ * push a new vertex to the buffer
31140
+ * @param {number} x - x position
31141
+ * @param {number} y - y position
31142
+ * @param {number} u - texture U coordinate
31143
+ * @param {number} v - texture V coordinate
31144
+ * @param {number} tint - tint color in UINT32 (argb) format
31145
+ * @param {number} [textureId] - texture unit index for multi-texture batching
31146
+ * @param {number} [normalTextureId] - paired normal-map texture unit index, or `-1` for unlit quads
31147
+ * @ignore
31148
+ */
31149
+ push(x, y, u, v, tint, textureId, normalTextureId) {
31150
+ const offset = this.vertexCount * this.vertexSize;
31151
+ this.bufferF32[offset] = x;
31152
+ this.bufferF32[offset + 1] = y;
31153
+ this.bufferF32[offset + 2] = u;
31154
+ this.bufferF32[offset + 3] = v;
31155
+ this.bufferU32[offset + 4] = tint;
31156
+ if (this.vertexSize > 5) {
31157
+ this.bufferF32[offset + 5] = textureId || 0;
31158
+ if (this.vertexSize > 6) {
31159
+ this.bufferF32[offset + 6] = typeof normalTextureId === "number" ? normalTextureId : -1;
31160
+ }
31161
+ }
31162
+ this.vertexCount++;
31163
+ return this;
31164
+ }
31165
+ /**
31166
+ * push a new vertex with all-float data to the buffer
31167
+ * @param {ArrayLike<number>} data - float values for one vertex
31168
+ * @param {number} srcOffset - start index in the source data
31169
+ * @param {number} count - number of floats to copy (should equal vertexSize)
31170
+ * @ignore
31171
+ */
31172
+ pushFloats(data2, srcOffset, count) {
31173
+ const offset = this.vertexCount * this.vertexSize;
31174
+ for (let i = 0; i < count; i++) {
31175
+ this.bufferF32[offset + i] = data2[srcOffset + i];
31176
+ }
31177
+ this.vertexCount++;
31178
+ return this;
31179
+ }
31180
+ /**
31181
+ * push a new vertex to the buffer (mesh format: x, y, z, u, v, tint)
31182
+ * @ignore
31183
+ */
31184
+ pushMesh(x, y, z, u, v, tint) {
31185
+ const offset = this.vertexCount * this.vertexSize;
31186
+ this.bufferF32[offset] = x;
31187
+ this.bufferF32[offset + 1] = y;
31188
+ this.bufferF32[offset + 2] = z;
31189
+ this.bufferF32[offset + 3] = u;
31190
+ this.bufferF32[offset + 4] = v;
31191
+ this.bufferU32[offset + 5] = tint;
31192
+ this.vertexCount++;
31193
+ return this;
31194
+ }
31195
+ /**
31196
+ * return a reference to the data in Float32 format
31197
+ * @ignore
31198
+ */
31199
+ toFloat32(begin, end) {
31200
+ if (typeof end !== "undefined") {
31201
+ return this.bufferF32.subarray(begin, end);
31202
+ } else {
31203
+ return this.bufferF32;
31204
+ }
31205
+ }
31206
+ /**
31207
+ * return a reference to the data in Uint32 format
31208
+ * @ignore
31209
+ */
31210
+ toUint32(begin, end) {
31211
+ if (typeof end !== "undefined") {
31212
+ return this.bufferU32.subarray(begin, end);
31213
+ } else {
31214
+ return this.bufferU32;
31215
+ }
31216
+ }
31217
+ };
31218
+
30219
31219
  // src/video/webgl/batchers/batcher.js
30220
31220
  var DEFAULT_MAX_VERTICES = 4096;
30221
31221
  var Batcher = class {
@@ -30314,6 +31314,25 @@ var Batcher = class {
30314
31314
  this.useShader(this.defaultShader);
30315
31315
  }
30316
31316
  }
31317
+ /**
31318
+ * called by the WebGL renderer when this batcher is being replaced by another.
31319
+ * Disables this batcher's vertex attribute locations so they don't leak across
31320
+ * (otherwise stale stride/offset state can cause INVALID_OPERATION on the next draw).
31321
+ */
31322
+ unbind() {
31323
+ if (this.currentShader === void 0) {
31324
+ return;
31325
+ }
31326
+ const gl = this.gl;
31327
+ for (let i = 0; i < this.attributes.length; ++i) {
31328
+ const location = this.currentShader.getAttribLocation(
31329
+ this.attributes[i].name
31330
+ );
31331
+ if (location !== -1) {
31332
+ gl.disableVertexAttribArray(location);
31333
+ }
31334
+ }
31335
+ }
30317
31336
  /**
30318
31337
  * Select the shader to use for compositing
30319
31338
  * @see GLShader
@@ -30322,6 +31341,9 @@ var Batcher = class {
30322
31341
  useShader(shader) {
30323
31342
  if (this.currentShader !== shader || this.renderer.currentProgram !== shader.program) {
30324
31343
  this.flush();
31344
+ if (this.currentShader && this.currentShader !== shader) {
31345
+ this.unbind();
31346
+ }
30325
31347
  shader.bind();
30326
31348
  shader.setUniform(this.projectionUniform, this.renderer.projectionMatrix);
30327
31349
  shader.setVertexAttributes(this.gl, this.attributes, this.stride);
@@ -30492,8 +31514,15 @@ var MaterialBatcher = class extends Batcher {
30492
31514
  createTexture2D(unit, pixels = null, filter, repeat = "no-repeat", w = pixels.width, h = pixels.height, premultipliedAlpha = true, mipmap = true, texture, flush = true) {
30493
31515
  const gl = this.gl;
30494
31516
  const isPOT = isPowerOfTwo(w) && isPowerOfTwo(h);
30495
- const rs = repeat.search(/^repeat(-x)?$/) === 0 && (isPOT || this.renderer.WebGLVersion > 1) ? gl.REPEAT : gl.CLAMP_TO_EDGE;
30496
- const rt = repeat.search(/^repeat(-y)?$/) === 0 && (isPOT || this.renderer.WebGLVersion > 1) ? gl.REPEAT : gl.CLAMP_TO_EDGE;
31517
+ const wantsRepeat = repeat !== "no-repeat";
31518
+ const canRepeat = isPOT || this.renderer.WebGLVersion > 1;
31519
+ const rs = repeat.search(/^repeat(-x)?$/) === 0 && canRepeat ? gl.REPEAT : gl.CLAMP_TO_EDGE;
31520
+ const rt = repeat.search(/^repeat(-y)?$/) === 0 && canRepeat ? gl.REPEAT : gl.CLAMP_TO_EDGE;
31521
+ if (wantsRepeat && !canRepeat) {
31522
+ console.warn(
31523
+ "melonJS: repeat wrap (" + repeat + ") requested on a non-power-of-two texture (" + w + "x" + h + ") under WebGL 1 \u2014 downgrading to clamp-to-edge"
31524
+ );
31525
+ }
30497
31526
  let currentTexture = texture;
30498
31527
  if (!currentTexture) {
30499
31528
  currentTexture = gl.createTexture();
@@ -30504,7 +31533,9 @@ var MaterialBatcher = class extends Batcher {
30504
31533
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
30505
31534
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
30506
31535
  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, premultipliedAlpha);
30507
- if (pixels !== null && pixels.compressed === true) {
31536
+ if (pixels !== null && typeof pixels.upload === "function") {
31537
+ pixels.upload(gl, gl.TEXTURE_2D);
31538
+ } else if (pixels !== null && pixels.compressed === true) {
30508
31539
  const mipmaps = pixels.mipmaps;
30509
31540
  for (let i = 0; i < mipmaps.length; i++) {
30510
31541
  gl.compressedTexImage2D(
@@ -30565,7 +31596,9 @@ var MaterialBatcher = class extends Batcher {
30565
31596
  pixels
30566
31597
  );
30567
31598
  }
30568
- if (isPOT && mipmap === true && (pixels === null || pixels.compressed !== true)) {
31599
+ if (isPOT && mipmap === true && pixels !== null && pixels.compressed !== true && typeof pixels.upload !== "function") {
31600
+ gl.generateMipmap(gl.TEXTURE_2D);
31601
+ } else if (pixels === null && isPOT && mipmap === true) {
30569
31602
  gl.generateMipmap(gl.TEXTURE_2D);
30570
31603
  }
30571
31604
  return currentTexture;
@@ -30643,18 +31676,29 @@ var MaterialBatcher = class extends Batcher {
30643
31676
  }
30644
31677
  /**
30645
31678
  * @ignore
31679
+ * @param {TextureAtlas|TextureResource} texture
31680
+ * @param {number} [w] - ignored when the source has its own `width` (the
31681
+ * common case); kept for the legacy signature where callers passed a
31682
+ * destination size. Forwarded only as a last-resort default.
31683
+ * @param {number} [h] - same as `w`.
31684
+ * @param {boolean} [force=false]
31685
+ * @param {boolean} [flush=true]
30646
31686
  */
30647
31687
  uploadTexture(texture, w, h, force = false, flush = true) {
30648
31688
  const unit = this.renderer.cache.getUnit(texture);
30649
31689
  const texture2D = this.boundTextures[unit];
30650
31690
  if (typeof texture2D === "undefined" || force) {
31691
+ const filter = typeof texture.filter !== "undefined" ? texture.filter : this.renderer.settings.antiAlias ? this.gl.LINEAR : this.gl.NEAREST;
31692
+ const source = texture.getTexture();
31693
+ const texW = source.width || source.videoWidth || w;
31694
+ const texH = source.height || source.videoHeight || h;
30651
31695
  this.createTexture2D(
30652
31696
  unit,
30653
- texture.getTexture(),
30654
- this.renderer.settings.antiAlias ? this.gl.LINEAR : this.gl.NEAREST,
31697
+ source,
31698
+ filter,
30655
31699
  texture.repeat,
30656
- w,
30657
- h,
31700
+ texW,
31701
+ texH,
30658
31702
  texture.premultipliedAlpha,
30659
31703
  void 0,
30660
31704
  texture2D,
@@ -30667,6 +31711,514 @@ var MaterialBatcher = class extends Batcher {
30667
31711
  }
30668
31712
  };
30669
31713
 
31714
+ // src/video/webgl/batchers/quad_batcher.js
31715
+ var V_ARRAY = [
31716
+ new Vector2d(),
31717
+ new Vector2d(),
31718
+ new Vector2d(),
31719
+ new Vector2d()
31720
+ ];
31721
+ var QuadBatcher = class extends MaterialBatcher {
31722
+ /**
31723
+ * Initialize the compositor
31724
+ * @ignore
31725
+ */
31726
+ init(renderer2) {
31727
+ this.maxBatchTextures = Math.min(renderer2.maxTextures, 16);
31728
+ super.init(renderer2, {
31729
+ attributes: [
31730
+ {
31731
+ name: "aVertex",
31732
+ size: 2,
31733
+ type: renderer2.gl.FLOAT,
31734
+ normalized: false,
31735
+ offset: 0 * Float32Array.BYTES_PER_ELEMENT
31736
+ },
31737
+ {
31738
+ name: "aRegion",
31739
+ size: 2,
31740
+ type: renderer2.gl.FLOAT,
31741
+ normalized: false,
31742
+ offset: 2 * Float32Array.BYTES_PER_ELEMENT
31743
+ },
31744
+ {
31745
+ name: "aColor",
31746
+ size: 4,
31747
+ type: renderer2.gl.UNSIGNED_BYTE,
31748
+ normalized: true,
31749
+ offset: 4 * Float32Array.BYTES_PER_ELEMENT
31750
+ },
31751
+ {
31752
+ name: "aTextureId",
31753
+ size: 1,
31754
+ type: renderer2.gl.FLOAT,
31755
+ normalized: false,
31756
+ offset: 5 * Float32Array.BYTES_PER_ELEMENT
31757
+ }
31758
+ ],
31759
+ shader: {
31760
+ vertex: quad_multi_default,
31761
+ fragment: buildMultiTextureFragment(this.maxBatchTextures)
31762
+ }
31763
+ });
31764
+ this.bindColorSamplers();
31765
+ this.useMultiTexture = true;
31766
+ this.createIndexBuffer();
31767
+ }
31768
+ /**
31769
+ * (Re-)create the index buffer for quad batching (4 verts + 6 indices per quad).
31770
+ * Called from `init` and `reset` (after context loss).
31771
+ * @ignore
31772
+ */
31773
+ createIndexBuffer() {
31774
+ const maxQuads = this.vertexData.maxVertex / 4;
31775
+ this.indexBuffer = new WebGLIndexBuffer(
31776
+ this.gl,
31777
+ maxQuads * 6,
31778
+ this.renderer.WebGLVersion > 1
31779
+ );
31780
+ this.indexBuffer.fillQuadPattern(maxQuads);
31781
+ }
31782
+ /**
31783
+ * Bind the color sampler uniforms (`uSampler0..uSamplerN-1`) to their
31784
+ * respective texture units. Called from `init` and `reset`.
31785
+ * @ignore
31786
+ */
31787
+ bindColorSamplers() {
31788
+ for (let i = 0; i < this.maxBatchTextures; i++) {
31789
+ this.defaultShader.setUniform("uSampler" + i, i);
31790
+ }
31791
+ }
31792
+ /**
31793
+ * Select the shader to use for compositing.
31794
+ * Multi-texture batching is automatically enabled when the default
31795
+ * shader is active, and disabled for custom ShaderEffect shaders.
31796
+ * @see GLShader
31797
+ * @see ShaderEffect
31798
+ * @param {GLShader|ShaderEffect} shader - a reference to a GLShader or ShaderEffect instance
31799
+ */
31800
+ useShader(shader) {
31801
+ super.useShader(shader);
31802
+ this.useMultiTexture = shader === this.defaultShader;
31803
+ }
31804
+ /**
31805
+ * Reset compositor internal state
31806
+ * @ignore
31807
+ */
31808
+ reset() {
31809
+ super.reset();
31810
+ this.createIndexBuffer();
31811
+ this.bindColorSamplers();
31812
+ this.useMultiTexture = true;
31813
+ }
31814
+ /**
31815
+ * Flush batched texture data to the GPU using indexed drawing.
31816
+ * @param {number} [mode=gl.TRIANGLES] - the GL drawing mode
31817
+ */
31818
+ flush(mode = this.mode) {
31819
+ const vertex = this.vertexData;
31820
+ const vertexCount = vertex.vertexCount;
31821
+ if (vertexCount > 0) {
31822
+ const gl = this.gl;
31823
+ const vertexSize = vertex.vertexSize;
31824
+ this.indexBuffer.bind();
31825
+ if (this.renderer.WebGLVersion > 1) {
31826
+ gl.bufferData(
31827
+ gl.ARRAY_BUFFER,
31828
+ vertex.toFloat32(),
31829
+ gl.STREAM_DRAW,
31830
+ 0,
31831
+ vertexCount * vertexSize
31832
+ );
31833
+ } else {
31834
+ gl.bufferData(
31835
+ gl.ARRAY_BUFFER,
31836
+ vertex.toFloat32(0, vertexCount * vertexSize),
31837
+ gl.STREAM_DRAW
31838
+ );
31839
+ }
31840
+ const indexCount = vertexCount / 4 * 6;
31841
+ gl.drawElements(mode, indexCount, this.indexBuffer.type, 0);
31842
+ vertex.clear();
31843
+ }
31844
+ }
31845
+ /**
31846
+ * Draw a screen-aligned quad with the given raw WebGL texture through the given shader.
31847
+ * Binds the texture to unit 0, pushes 4 vertices (Y-flipped UVs), flushes,
31848
+ * then unbinds the texture.
31849
+ * @param {WebGLTexture} source - the raw GL texture to blit
31850
+ * @param {number} x - destination x
31851
+ * @param {number} y - destination y
31852
+ * @param {number} width - destination width
31853
+ * @param {number} height - destination height
31854
+ * @param {GLShader|ShaderEffect} shader - the shader effect to apply
31855
+ */
31856
+ blitTexture(source, x, y, width, height, shader) {
31857
+ const gl = this.gl;
31858
+ this.useShader(shader);
31859
+ gl.activeTexture(gl.TEXTURE0);
31860
+ gl.bindTexture(gl.TEXTURE_2D, source);
31861
+ this.currentTextureUnit = 0;
31862
+ this.boundTextures[0] = source;
31863
+ shader.setUniform("uSampler", 0);
31864
+ const m = this.viewMatrix;
31865
+ const vec0 = V_ARRAY[0].set(x, y);
31866
+ const vec1 = V_ARRAY[1].set(x + width, y);
31867
+ const vec2 = V_ARRAY[2].set(x, y + height);
31868
+ const vec3 = V_ARRAY[3].set(x + width, y + height);
31869
+ if (m && !m.isIdentity()) {
31870
+ m.apply(vec0);
31871
+ m.apply(vec1);
31872
+ m.apply(vec2);
31873
+ m.apply(vec3);
31874
+ }
31875
+ const tint = 4294967295;
31876
+ this.vertexData.push(vec0.x, vec0.y, 0, 1, tint, 0);
31877
+ this.vertexData.push(vec1.x, vec1.y, 1, 1, tint, 0);
31878
+ this.vertexData.push(vec2.x, vec2.y, 0, 0, tint, 0);
31879
+ this.vertexData.push(vec3.x, vec3.y, 1, 0, tint, 0);
31880
+ this.flush();
31881
+ gl.activeTexture(gl.TEXTURE0);
31882
+ gl.bindTexture(gl.TEXTURE_2D, null);
31883
+ this.currentTextureUnit = -1;
31884
+ delete this.boundTextures[0];
31885
+ this.useShader(this.defaultShader);
31886
+ }
31887
+ /**
31888
+ * Add a textured quad
31889
+ * @param {TextureAtlas} texture - Source texture atlas
31890
+ * @param {number} x - Destination x-coordinate
31891
+ * @param {number} y - Destination y-coordinate
31892
+ * @param {number} w - Destination width
31893
+ * @param {number} h - Destination height
31894
+ * @param {number} u0 - Texture UV (u0) value.
31895
+ * @param {number} v0 - Texture UV (v0) value.
31896
+ * @param {number} u1 - Texture UV (u1) value.
31897
+ * @param {number} v1 - Texture UV (v1) value.
31898
+ * @param {number} tint - tint color to be applied to the texture in UINT32 (argb) format
31899
+ * @param {boolean} [reupload=false] - Force the texture to be reuploaded even if already bound
31900
+ */
31901
+ addQuad(texture, x, y, w, h, u0, v0, u1, v1, tint, reupload = false) {
31902
+ const vertexData = this.vertexData;
31903
+ if (vertexData.isFull(4)) {
31904
+ this.flush();
31905
+ }
31906
+ let unit;
31907
+ if (this.useMultiTexture) {
31908
+ unit = this.uploadTexture(texture, w, h, reupload, false);
31909
+ if (unit >= this.maxBatchTextures) {
31910
+ this.flush();
31911
+ this.renderer.cache.resetUnitAssignments();
31912
+ unit = this.uploadTexture(texture, w, h, reupload, false);
31913
+ }
31914
+ } else {
31915
+ unit = this.uploadTexture(texture, w, h, reupload);
31916
+ if (unit !== this.currentSamplerUnit) {
31917
+ this.currentShader.setUniform("uSampler", unit);
31918
+ this.currentSamplerUnit = unit;
31919
+ }
31920
+ }
31921
+ const m = this.viewMatrix;
31922
+ const vec0 = V_ARRAY[0].set(x, y);
31923
+ const vec1 = V_ARRAY[1].set(x + w, y);
31924
+ const vec2 = V_ARRAY[2].set(x, y + h);
31925
+ const vec3 = V_ARRAY[3].set(x + w, y + h);
31926
+ if (!m.isIdentity()) {
31927
+ m.apply(vec0);
31928
+ m.apply(vec1);
31929
+ m.apply(vec2);
31930
+ m.apply(vec3);
31931
+ }
31932
+ const textureId = this.useMultiTexture ? unit : 0;
31933
+ vertexData.push(vec0.x, vec0.y, u0, v0, tint, textureId);
31934
+ vertexData.push(vec1.x, vec1.y, u1, v0, tint, textureId);
31935
+ vertexData.push(vec2.x, vec2.y, u0, v1, tint, textureId);
31936
+ vertexData.push(vec3.x, vec3.y, u1, v1, tint, textureId);
31937
+ }
31938
+ };
31939
+
31940
+ // src/video/webgl/batchers/lit_quad_batcher.js
31941
+ var LitQuadBatcher = class extends QuadBatcher {
31942
+ /**
31943
+ * @ignore
31944
+ */
31945
+ init(renderer2) {
31946
+ const halved = Math.min(
31947
+ Math.max(1, Math.floor(renderer2.maxTextures / 2)),
31948
+ 16
31949
+ );
31950
+ this.maxBatchTextures = halved;
31951
+ Object.getPrototypeOf(QuadBatcher.prototype).init.call(this, renderer2, {
31952
+ attributes: [
31953
+ {
31954
+ name: "aVertex",
31955
+ size: 2,
31956
+ type: renderer2.gl.FLOAT,
31957
+ normalized: false,
31958
+ offset: 0 * Float32Array.BYTES_PER_ELEMENT
31959
+ },
31960
+ {
31961
+ name: "aRegion",
31962
+ size: 2,
31963
+ type: renderer2.gl.FLOAT,
31964
+ normalized: false,
31965
+ offset: 2 * Float32Array.BYTES_PER_ELEMENT
31966
+ },
31967
+ {
31968
+ name: "aColor",
31969
+ size: 4,
31970
+ type: renderer2.gl.UNSIGNED_BYTE,
31971
+ normalized: true,
31972
+ offset: 4 * Float32Array.BYTES_PER_ELEMENT
31973
+ },
31974
+ {
31975
+ name: "aTextureId",
31976
+ size: 1,
31977
+ type: renderer2.gl.FLOAT,
31978
+ normalized: false,
31979
+ offset: 5 * Float32Array.BYTES_PER_ELEMENT
31980
+ },
31981
+ {
31982
+ name: "aNormalTextureId",
31983
+ size: 1,
31984
+ type: renderer2.gl.FLOAT,
31985
+ normalized: false,
31986
+ offset: 6 * Float32Array.BYTES_PER_ELEMENT
31987
+ }
31988
+ ],
31989
+ shader: {
31990
+ vertex: quad_multi_lit_default,
31991
+ fragment: buildLitMultiTextureFragment(halved)
31992
+ }
31993
+ });
31994
+ this.bindColorSamplers();
31995
+ this.bindNormalSamplers();
31996
+ this.createIndexBuffer();
31997
+ this.useMultiTexture = true;
31998
+ this.boundNormalMaps = new Array(halved).fill(null);
31999
+ this.normalMapTextures = /* @__PURE__ */ new Map();
32000
+ this._lightCount = 0;
32001
+ this._maxLights = MAX_LIGHTS;
32002
+ this.defaultShader.setUniform("uLightCount", 0);
32003
+ this.defaultShader.setUniform("uAmbient", [0, 0, 0]);
32004
+ }
32005
+ /**
32006
+ * Bind the paired normal sampler uniforms (`uNormalSampler0..N-1`)
32007
+ * to texture units `maxBatchTextures..2*maxBatchTextures-1`. Called
32008
+ * from `init` and `reset`.
32009
+ * @ignore
32010
+ */
32011
+ bindNormalSamplers() {
32012
+ for (let i = 0; i < this.maxBatchTextures; i++) {
32013
+ this.defaultShader.setUniform(
32014
+ "uNormalSampler" + i,
32015
+ this.maxBatchTextures + i
32016
+ );
32017
+ }
32018
+ }
32019
+ /**
32020
+ * @ignore
32021
+ */
32022
+ reset() {
32023
+ super.reset();
32024
+ this.bindNormalSamplers();
32025
+ this.boundNormalMaps.fill(null);
32026
+ this.normalMapTextures.clear();
32027
+ this._lightCount = 0;
32028
+ this.defaultShader.setUniform("uLightCount", 0);
32029
+ this.defaultShader.setUniform("uAmbient", [0, 0, 0]);
32030
+ }
32031
+ /**
32032
+ * Upload per-frame Light2d uniforms used by the lit fragment path.
32033
+ * Called once per camera per frame (before the world tree walk).
32034
+ * Lights past `MAX_LIGHTS` are silently ignored.
32035
+ *
32036
+ * Coordinates must be supplied in the same space as the renderer's
32037
+ * pre-projection vertex coords (i.e. camera-local / FBO-local),
32038
+ * matching `Stage.drawLighting`'s convention.
32039
+ * @param {object} uniforms
32040
+ * @param {Float32Array} uniforms.positions - flat array of `[x, y, radius, intensity]` per light, length = 4 * count
32041
+ * @param {Float32Array} uniforms.colors - flat array of `[r, g, b]` per light, length = 3 * count
32042
+ * @param {Float32Array} [uniforms.heights] - flat array of per-light height, length = MAX_LIGHTS
32043
+ * @param {number} uniforms.count - number of lights to render (clamped to MAX_LIGHTS)
32044
+ * @param {number[]} [uniforms.ambient] - `[r, g, b]` ambient floor (0..1 each)
32045
+ */
32046
+ setLightUniforms(uniforms) {
32047
+ const shader = this.defaultShader;
32048
+ const count = Math.min(uniforms.count | 0, this._maxLights);
32049
+ this._lightCount = count;
32050
+ shader.setUniform("uLightCount", count);
32051
+ if (count > 0) {
32052
+ shader.setUniform("uLightPos", uniforms.positions);
32053
+ shader.setUniform("uLightColor", uniforms.colors);
32054
+ if (uniforms.heights) {
32055
+ shader.setUniform("uLightHeight", uniforms.heights);
32056
+ }
32057
+ }
32058
+ if (uniforms.ambient) {
32059
+ shader.setUniform("uAmbient", uniforms.ambient);
32060
+ }
32061
+ }
32062
+ /**
32063
+ * Bind a normal-map image to the given GL texture unit. Uploads on
32064
+ * first use (via `uploadNormalMap`) and rebinds the cached
32065
+ * `WebGLTexture` on subsequent calls. Mirrors the
32066
+ * `bindTexture2D` / `createTexture2D` split used by `MaterialBatcher`,
32067
+ * but for normal-map textures which live outside the color
32068
+ * `TextureCache` (cached per-image in `normalMapTextures`).
32069
+ * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} image - normal-map source
32070
+ * @param {number} unit - GL texture unit (already offset by `maxBatchTextures`)
32071
+ */
32072
+ bindNormalMap(image, unit) {
32073
+ const cached = this.normalMapTextures.get(image);
32074
+ if (typeof cached !== "undefined") {
32075
+ this.bindTexture2D(cached, unit, false);
32076
+ return;
32077
+ }
32078
+ this.uploadNormalMap(image, unit);
32079
+ }
32080
+ /**
32081
+ * Upload a normal-map image to GL and cache the resulting `WebGLTexture`
32082
+ * for future `bindNormalMap` calls. Not meant to be called directly —
32083
+ * `bindNormalMap` invokes this on the first use of a given image.
32084
+ *
32085
+ * `premultipliedAlpha = false` — normal maps store linear-encoded
32086
+ * surface normals; multiplying through alpha would corrupt the
32087
+ * encoding for any non-opaque texel.
32088
+ * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} image - normal-map source
32089
+ * @param {number} unit - GL texture unit (already offset by `maxBatchTextures`)
32090
+ */
32091
+ uploadNormalMap(image, unit) {
32092
+ const gl = this.gl;
32093
+ this.createTexture2D(
32094
+ unit,
32095
+ image,
32096
+ this.renderer.settings.antiAlias ? gl.LINEAR : gl.NEAREST,
32097
+ "no-repeat",
32098
+ image.width,
32099
+ image.height,
32100
+ false,
32101
+ void 0,
32102
+ void 0,
32103
+ false
32104
+ );
32105
+ this.normalMapTextures.set(image, this.boundTextures[unit]);
32106
+ }
32107
+ /**
32108
+ * Add a textured quad with optional paired normal map.
32109
+ * @param {TextureAtlas} texture - Source texture atlas
32110
+ * @param {number} x - Destination x-coordinate
32111
+ * @param {number} y - Destination y-coordinate
32112
+ * @param {number} w - Destination width
32113
+ * @param {number} h - Destination height
32114
+ * @param {number} u0 - Texture UV (u0) value
32115
+ * @param {number} v0 - Texture UV (v0) value
32116
+ * @param {number} u1 - Texture UV (u1) value
32117
+ * @param {number} v1 - Texture UV (v1) value
32118
+ * @param {number} tint - tint color (UINT32 argb)
32119
+ * @param {boolean} [reupload=false] - Force the texture to be reuploaded
32120
+ * @param {HTMLImageElement|HTMLCanvasElement|null} [normalMap=null] - paired normal-map (SpriteIlluminator workflow)
32121
+ */
32122
+ addQuad(texture, x, y, w, h, u0, v0, u1, v1, tint, reupload = false, normalMap = null) {
32123
+ const vertexData = this.vertexData;
32124
+ if (vertexData.isFull(4)) {
32125
+ this.flush();
32126
+ }
32127
+ let unit;
32128
+ if (this.useMultiTexture) {
32129
+ unit = this.uploadTexture(texture, w, h, reupload, false);
32130
+ if (unit >= this.maxBatchTextures) {
32131
+ this.flush();
32132
+ this.renderer.cache.resetUnitAssignments();
32133
+ this.boundNormalMaps.fill(null);
32134
+ unit = this.uploadTexture(texture, w, h, reupload, false);
32135
+ }
32136
+ } else {
32137
+ unit = this.uploadTexture(texture, w, h, reupload);
32138
+ if (unit !== this.currentSamplerUnit) {
32139
+ this.currentShader.setUniform("uSampler", unit);
32140
+ this.currentSamplerUnit = unit;
32141
+ }
32142
+ }
32143
+ let normalTextureId = -1;
32144
+ if (normalMap !== null && this.useMultiTexture) {
32145
+ const normalUnit = this.maxBatchTextures + unit;
32146
+ const prev = this.boundNormalMaps[unit];
32147
+ if (prev !== normalMap) {
32148
+ if (prev !== null) {
32149
+ this.flush();
32150
+ }
32151
+ this.bindNormalMap(normalMap, normalUnit);
32152
+ this.boundNormalMaps[unit] = normalMap;
32153
+ }
32154
+ normalTextureId = unit;
32155
+ }
32156
+ const m = this.viewMatrix;
32157
+ const vec0 = V_ARRAY[0].set(x, y);
32158
+ const vec1 = V_ARRAY[1].set(x + w, y);
32159
+ const vec2 = V_ARRAY[2].set(x, y + h);
32160
+ const vec3 = V_ARRAY[3].set(x + w, y + h);
32161
+ if (!m.isIdentity()) {
32162
+ m.apply(vec0);
32163
+ m.apply(vec1);
32164
+ m.apply(vec2);
32165
+ m.apply(vec3);
32166
+ }
32167
+ const textureId = this.useMultiTexture ? unit : 0;
32168
+ vertexData.push(vec0.x, vec0.y, u0, v0, tint, textureId, normalTextureId);
32169
+ vertexData.push(vec1.x, vec1.y, u1, v0, tint, textureId, normalTextureId);
32170
+ vertexData.push(vec2.x, vec2.y, u0, v1, tint, textureId, normalTextureId);
32171
+ vertexData.push(vec3.x, vec3.y, u1, v1, tint, textureId, normalTextureId);
32172
+ }
32173
+ /**
32174
+ * Override `blitTexture` so the FBO blit pushes `-1` as the unlit
32175
+ * sentinel (this batcher's vertex layout includes `aNormalTextureId`).
32176
+ * @param {WebGLTexture} source - the raw GL texture to blit
32177
+ * @param {number} x - destination x
32178
+ * @param {number} y - destination y
32179
+ * @param {number} width - destination width
32180
+ * @param {number} height - destination height
32181
+ * @param {GLShader|ShaderEffect} shader - the shader effect to apply
32182
+ */
32183
+ blitTexture(source, x, y, width, height, shader) {
32184
+ const gl = this.gl;
32185
+ this.useShader(shader);
32186
+ gl.activeTexture(gl.TEXTURE0);
32187
+ gl.bindTexture(gl.TEXTURE_2D, source);
32188
+ this.currentTextureUnit = 0;
32189
+ this.boundTextures[0] = source;
32190
+ shader.setUniform("uSampler", 0);
32191
+ const m = this.viewMatrix;
32192
+ const vec0 = V_ARRAY[0].set(x, y);
32193
+ const vec1 = V_ARRAY[1].set(x + width, y);
32194
+ const vec2 = V_ARRAY[2].set(x, y + height);
32195
+ const vec3 = V_ARRAY[3].set(x + width, y + height);
32196
+ if (m && !m.isIdentity()) {
32197
+ m.apply(vec0);
32198
+ m.apply(vec1);
32199
+ m.apply(vec2);
32200
+ m.apply(vec3);
32201
+ }
32202
+ const tint = 4294967295;
32203
+ this.vertexData.push(vec0.x, vec0.y, 0, 1, tint, 0, -1);
32204
+ this.vertexData.push(vec1.x, vec1.y, 1, 1, tint, 0, -1);
32205
+ this.vertexData.push(vec2.x, vec2.y, 0, 0, tint, 0, -1);
32206
+ this.vertexData.push(vec3.x, vec3.y, 1, 0, tint, 0, -1);
32207
+ this.flush();
32208
+ gl.activeTexture(gl.TEXTURE0);
32209
+ gl.bindTexture(gl.TEXTURE_2D, null);
32210
+ this.currentTextureUnit = -1;
32211
+ delete this.boundTextures[0];
32212
+ this.useShader(this.defaultShader);
32213
+ }
32214
+ };
32215
+
32216
+ // src/video/webgl/shaders/mesh.frag
32217
+ 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";
32218
+
32219
+ // src/video/webgl/shaders/mesh.vert
32220
+ 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";
32221
+
30670
32222
  // src/video/webgl/batchers/mesh_batcher.js
30671
32223
  var _v = new Vector2d();
30672
32224
  var MeshBatcher = class extends MaterialBatcher {
@@ -30940,239 +32492,509 @@ var PrimitiveBatcher = class extends Batcher {
30940
32492
  }
30941
32493
  };
30942
32494
 
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) {");
32495
+ // src/video/webgl/effects/radialGradient.js
32496
+ var RadialGradientEffect = class extends ShaderEffect {
32497
+ /**
32498
+ * @param {WebGLRenderer} renderer - the current renderer instance
32499
+ * @param {object} [options] - initial uniform values
32500
+ * @param {Color} [options.color] - center color (0..255 RGB); defaults to white
32501
+ * @param {number} [options.intensity=1] - peak alpha at the center (0..1+)
32502
+ */
32503
+ constructor(renderer2, options = {}) {
32504
+ super(
32505
+ renderer2,
32506
+ `
32507
+ uniform vec3 uColor;
32508
+ uniform float uIntensity;
32509
+ vec4 apply(vec4 color, vec2 uv) {
32510
+ // recenter to [-1, 1] across the quad. The quad's own aspect
32511
+ // ratio handles elliptical falloffs naturally \u2014 length(c) == 1
32512
+ // lies on the inscribed ellipse in world space.
32513
+ vec2 c = uv * 2.0 - 1.0;
32514
+ float d = length(c);
32515
+ // linear ramp matches Canvas createRadialGradient's two-stop output
32516
+ float f = clamp(1.0 - d, 0.0, 1.0);
32517
+ // 'color' is the per-vertex tint, already premultiplied by
32518
+ // alpha in the vertex shader (vColor = vec4(aColor.bgr *
32519
+ // aColor.a, aColor.a)). For standalone use the tint is
32520
+ // (1,1,1,1) and the uniforms drive the look; for the Light2d
32521
+ // batching path the uniforms stay at default and the tint
32522
+ // carries the per-light color + intensity.
32523
+ vec3 rgb = color.rgb * uColor * uIntensity * f;
32524
+ float a = color.a * uIntensity * f;
32525
+ return vec4(rgb, a);
32526
+ }
32527
+ `
32528
+ );
32529
+ this._colorBuf = new Float32Array(3);
32530
+ const color = options.color;
32531
+ if (color) {
32532
+ this.setColor(color);
30959
32533
  } else {
30960
- lines.push(" } else if (vTextureId < " + (i + 0.5) + ") {");
32534
+ this._colorBuf[0] = 1;
32535
+ this._colorBuf[1] = 1;
32536
+ this._colorBuf[2] = 1;
32537
+ this.setUniform("uColor", this._colorBuf);
30961
32538
  }
30962
- lines.push(" color = texture2D(uSampler" + i + ", vRegion);");
32539
+ this.setIntensity(options.intensity ?? 1);
30963
32540
  }
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 {
30983
32541
  /**
30984
- * Initialize the compositor
30985
- * @ignore
32542
+ * Set the center color. RGB only — alpha is ignored (the radial
32543
+ * falloff supplies the per-pixel alpha).
32544
+ * @param {Color} color - 0..255 RGB color
30986
32545
  */
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)
32546
+ setColor(color) {
32547
+ this._colorBuf[0] = color.r / 255;
32548
+ this._colorBuf[1] = color.g / 255;
32549
+ this._colorBuf[2] = color.b / 255;
32550
+ this.setUniform("uColor", this._colorBuf);
32551
+ }
32552
+ /**
32553
+ * Set the peak intensity. Acts as a brightness multiplier on the
32554
+ * falloff curve; values above 1 over-saturate the center of the gradient.
32555
+ * @param {number} intensity - 0..1+ multiplier
32556
+ */
32557
+ setIntensity(intensity) {
32558
+ this.setUniform("uIntensity", intensity);
32559
+ }
32560
+ };
32561
+
32562
+ // src/video/webgl/lighting/pack.ts
32563
+ function createLightUniformScratch() {
32564
+ return {
32565
+ positions: new Float32Array(MAX_LIGHTS * 4),
32566
+ colors: new Float32Array(MAX_LIGHTS * 3),
32567
+ heights: new Float32Array(MAX_LIGHTS),
32568
+ ambient: [0, 0, 0]
32569
+ };
32570
+ }
32571
+ function packLights(lights, ambient, translateX, translateY, scratch) {
32572
+ scratch.positions.fill(0);
32573
+ scratch.colors.fill(0);
32574
+ scratch.heights.fill(0);
32575
+ let i = 0;
32576
+ if (lights) {
32577
+ for (const light of lights) {
32578
+ if (i >= MAX_LIGHTS) {
32579
+ break;
31023
32580
  }
31024
- });
31025
- for (let i = 0; i < this.maxBatchTextures; i++) {
31026
- this.defaultShader.setUniform("uSampler" + i, i);
32581
+ const b = light.getBounds();
32582
+ const radius = Math.max(b.width, b.height) / 2;
32583
+ scratch.positions[i * 4 + 0] = b.centerX - translateX;
32584
+ scratch.positions[i * 4 + 1] = b.centerY - translateY;
32585
+ scratch.positions[i * 4 + 2] = radius;
32586
+ scratch.positions[i * 4 + 3] = light.intensity;
32587
+ scratch.colors[i * 3 + 0] = light.color.r / 255;
32588
+ scratch.colors[i * 3 + 1] = light.color.g / 255;
32589
+ scratch.colors[i * 3 + 2] = light.color.b / 255;
32590
+ scratch.heights[i] = light.lightHeight;
32591
+ i++;
31027
32592
  }
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
32593
  }
32594
+ if (ambient) {
32595
+ scratch.ambient[0] = ambient.r / 255;
32596
+ scratch.ambient[1] = ambient.g / 255;
32597
+ scratch.ambient[2] = ambient.b / 255;
32598
+ } else {
32599
+ scratch.ambient[0] = 0;
32600
+ scratch.ambient[1] = 0;
32601
+ scratch.ambient[2] = 0;
32602
+ }
32603
+ return {
32604
+ positions: scratch.positions,
32605
+ colors: scratch.colors,
32606
+ heights: scratch.heights,
32607
+ ambient: scratch.ambient,
32608
+ count: i
32609
+ };
32610
+ }
32611
+
32612
+ // src/video/texture/resource.js
32613
+ var TextureResource = class {
31037
32614
  /**
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
32615
+ * @param {object} options
32616
+ * @param {number} options.width - pixel width of the texture
32617
+ * @param {number} options.height - pixel height of the texture
32618
+ * @param {boolean} [options.premultipliedAlpha=false]
32619
+ * @param {string} [options.repeat="no-repeat"] - "no-repeat" | "repeat" | "repeat-x" | "repeat-y"
32620
+ * @param {number} [options.filter] - `gl.NEAREST` or `gl.LINEAR`; when
32621
+ * omitted the batcher falls back to the renderer's `antiAlias` setting
31044
32622
  */
31045
- useShader(shader) {
31046
- super.useShader(shader);
31047
- this.useMultiTexture = shader === this.defaultShader;
32623
+ constructor({
32624
+ width,
32625
+ height,
32626
+ premultipliedAlpha = false,
32627
+ repeat = "no-repeat",
32628
+ filter
32629
+ } = {}) {
32630
+ this.width = width;
32631
+ this.height = height;
32632
+ this.premultipliedAlpha = premultipliedAlpha;
32633
+ this.repeat = repeat;
32634
+ this.filter = filter;
32635
+ this.sources = /* @__PURE__ */ new Map([["default", this]]);
32636
+ this.activeAtlas = "default";
31048
32637
  }
31049
32638
  /**
31050
- * Reset compositor internal state
32639
+ * Returns the upload "source" the batcher hands to `createTexture2D`.
32640
+ * For a resource this is the resource itself — `createTexture2D`
32641
+ * dispatches to `resource.upload(gl, target)`.
31051
32642
  * @ignore
31052
32643
  */
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
31060
- );
31061
- this.indexBuffer.fillQuadPattern(maxQuads);
31062
- for (let i = 0; i < this.maxBatchTextures; i++) {
31063
- this.defaultShader.setUniform("uSampler" + i, i);
31064
- }
31065
- this.useMultiTexture = true;
32644
+ getTexture() {
32645
+ return this;
31066
32646
  }
31067
32647
  /**
31068
- * Flush batched texture data to the GPU using indexed drawing.
31069
- * @param {number} [mode=gl.TRIANGLES] - the GL drawing mode
32648
+ * Issue the `gl.texImage2D` (or equivalent) call that uploads this
32649
+ * resource's data into the currently-bound `TEXTURE_2D` slot.
32650
+ * Subclasses MUST override.
32651
+ * @abstract
32652
+ * @param {WebGLRenderingContext|WebGL2RenderingContext} gl
32653
+ * @param {number} target - `gl.TEXTURE_2D` (or future cube-map targets)
31070
32654
  */
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
32655
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
32656
+ upload(gl, target) {
32657
+ throw new Error("TextureResource subclasses must implement upload()");
32658
+ }
32659
+ };
32660
+ var BufferTextureResource = class extends TextureResource {
32661
+ /**
32662
+ * @param {ArrayBufferView} data - the pixel data; size must be
32663
+ * `width * height * 4` bytes for the default RGBA / UNSIGNED_BYTE
32664
+ * format
32665
+ * @param {object} options
32666
+ * @param {number} options.width
32667
+ * @param {number} options.height
32668
+ * @param {boolean} [options.premultipliedAlpha=false]
32669
+ * @param {string} [options.repeat="no-repeat"]
32670
+ * @param {number} [options.filter]
32671
+ * @param {"rgba8"|"rgba8ui"} [options.format="rgba8"] - storage format.
32672
+ * `"rgba8"` (default): normalized RGBA, sampled via `sampler2D` /
32673
+ * `texture()`. `"rgba8ui"`: unsigned-integer RGBA, sampled via
32674
+ * `usampler2D` / `texelFetch()` — requires WebGL2. Use the integer
32675
+ * form for raw-data lookups (GID tables, palette indices, etc.) to
32676
+ * skip the float-decode round trip and gain exact integer reads.
32677
+ */
32678
+ constructor(data2, options) {
32679
+ super(options);
32680
+ this.data = data2;
32681
+ this.format = options.format || "rgba8";
32682
+ }
32683
+ /** @ignore */
32684
+ upload(gl, target) {
32685
+ if (this.format === "rgba8ui") {
32686
+ if (typeof gl.RGBA8UI === "undefined") {
32687
+ throw new Error(
32688
+ 'BufferTextureResource: format "rgba8ui" requires a WebGL 2 context'
31085
32689
  );
32690
+ }
32691
+ gl.texImage2D(
32692
+ target,
32693
+ 0,
32694
+ gl.RGBA8UI,
32695
+ this.width,
32696
+ this.height,
32697
+ 0,
32698
+ gl.RGBA_INTEGER,
32699
+ gl.UNSIGNED_BYTE,
32700
+ this.data
32701
+ );
32702
+ } else {
32703
+ gl.texImage2D(
32704
+ target,
32705
+ 0,
32706
+ gl.RGBA,
32707
+ this.width,
32708
+ this.height,
32709
+ 0,
32710
+ gl.RGBA,
32711
+ gl.UNSIGNED_BYTE,
32712
+ this.data
32713
+ );
32714
+ }
32715
+ }
32716
+ };
32717
+
32718
+ // src/video/webgl/shaders/orthogonal-tmxlayer.frag
32719
+ var orthogonal_tmxlayer_default = "#version 300 es\n\n// Fragment shader for the orthogonal TMX layer GPU renderer (WebGL2 /\n// GLSL ES 3.00).\n//\n// Per fragment the shader:\n// 1. recovers the world-pixel position from the host UV,\n// 2. walks candidate cells (geometric cell + cells whose oversized,\n// bottom-aligned tiles could reach this fragment),\n// 3. fetches GIDs from the per-layer index texture, and\n// 4. samples the tileset atlas at the correct sub-region.\n//\n// Fast path: when the tileset has no oversized tiles (`uOverflow == (0, 0)`)\n// \u2014 the common case for grid-aligned maps \u2014 only the geometric cell can\n// hold this fragment's tile, so we skip the candidate loop entirely and\n// run a single `tryRenderCell` call. The loop branch is on a uniform\n// value, coherent across the wave, and trims 25 worst-case loop\n// iterations + their guard checks to a single inlined function call.\n//\n// Slow path: tiles drawing larger than the cell are bottom-aligned\n// vertically and left-aligned horizontally. Render order is \"right-down\":\n// later cells end up on top, so the candidate loop scans dy high\u2192low,\n// dx low\u2192high and picks the FIRST match.\n//\n// Index texture encoding (`RGBA8`, one cell per texel):\n// R = GID low byte\n// G = GID high byte (combined: R | (G << 8) = 16-bit GID)\n// B = flip mask (bit 0 = H, bit 1 = V, bit 2 = AD)\n// A = unused\n//\n// Animation lookup (`RGBA8`, 1 row, `tileCount` texels wide): per local\n// tile id, the CURRENT frame's local id, same R/G byte-pair encoding.\n//\n// Why `sampler2D` + float decode rather than `usampler2D`: the engine's\n// multi-texture default shader declares `uSampler0..uSamplerN-1` as\n// `sampler2D` \u2014 all of them are active for WebGL's draw-time validation.\n// A `usampler2D`-backed `RGBA8UI` texture cached at any of those units\n// (units 0..15 on a typical 16-unit fragment stage) would mismatch when\n// the default shader next draws sprites, killing every quad with\n// `GL_INVALID_OPERATION`. Staying on regular RGBA8 keeps the cache\n// path coherent \u2014 the cost is one `floor(c * 255 + 0.5)` per fetch.\n//\n// `texelFetch` is still used (vs `texture()`) for byte-exact reads \u2014\n// it bypasses interpolation, so the integer byte values come out\n// unmolested even on a normalized-float sampler.\n\n// The engine's `setPrecision` step injects precision declarations for\n// float and int after the `#version` line, using whatever precision the\n// renderer was configured with (`highPrecisionShader` setting on the\n// Application). Individual shader files don't hardcode precision so the\n// engine-wide preference applies.\n\nin vec2 vRegion;\nin vec4 vColor;\n\nuniform sampler2D uSampler; // tileset atlas (RGBA)\nuniform sampler2D uTileIndex; // per-layer GID index (RGBA8)\nuniform sampler2D uAnimLookup; // per-tileset animation table (RGBA8)\n\nuniform vec2 uMapSize;\nuniform vec2 uCellSize;\nuniform vec2 uTileSize;\nuniform vec2 uOverflow;\nuniform vec2 uTilesetCols;\nuniform vec2 uInvTilesetSize;\nuniform vec4 uTilesetMargin; // (marginX, marginY, spacingX, spacingY)\nuniform vec2 uGidRange; // (firstgid, lastgid)\nuniform vec2 uVisibleStart;\nuniform vec2 uVisibleSize;\nuniform int uAnimSize; // number of entries in uAnimLookup, 0 if disabled\nuniform float uOpacity;\nuniform vec4 uTint;\n\nout vec4 fragColor;\n\nconst int MAX_OVERFLOW = 4;\n\n// Try to render the tile at cell (cx, cy) for the current fragment.\n// Returns true and writes the sampled color to `outColor` when the cell\n// contains a visible, in-range tile whose pixel covers this fragment.\n// Identical logic for both fast and slow paths \u2014 GLSL inlines this\n// trivially so there's no function-call overhead at runtime.\nbool tryRenderCell(int cx, int cy, vec2 worldPx, out vec4 outColor) {\n int mapW = int(uMapSize.x);\n int mapH = int(uMapSize.y);\n if (cx < 0 || cx >= mapW || cy < 0 || cy >= mapH) {\n return false;\n }\n\n // `texelFetch` skips filtering \u2014 the 8-bit channel values come back\n // as normalized floats, decoded to byte ints below\n vec4 cellF = texelFetch(uTileIndex, ivec2(cx, cy), 0);\n uvec4 cell = uvec4(cellF * 255.0 + 0.5);\n int firstGid = int(uGidRange.x);\n int gid = int(cell.r) | (int(cell.g) << 8);\n if (gid < firstGid || gid > int(uGidRange.y)) {\n return false;\n }\n\n vec2 tileWorldOrigin = vec2(\n float(cx) * uCellSize.x,\n (float(cy) + 1.0) * uCellSize.y - uTileSize.y\n );\n vec2 inTile = (worldPx - tileWorldOrigin) / uTileSize;\n if (inTile.x < 0.0 || inTile.x >= 1.0 || inTile.y < 0.0 || inTile.y >= 1.0) {\n return false;\n }\n\n // flip mask + axis-swap trick: see TMX shader-path flip spec.\n // AD performs a transpose; with AD set, H and V swap their effective\n // axes (matches the legacy `buildFlipTransform`).\n int flipMask = int(cell.b);\n float flipH = float(flipMask & 1);\n float flipV = float((flipMask >> 1) & 1);\n float flipAD = float((flipMask >> 2) & 1);\n inTile = mix(inTile, inTile.yx, flipAD);\n float effH = mix(flipH, flipV, flipAD);\n float effV = mix(flipV, flipH, flipAD);\n inTile.x = mix(inTile.x, 1.0 - inTile.x, effH);\n inTile.y = mix(inTile.y, 1.0 - inTile.y, effV);\n\n int localId = gid - firstGid;\n\n // animation: if the tileset has animated tiles, swap the local id for\n // its current frame's id via the lookup texture (CPU updates the\n // lookup in lockstep with `tileset.update(dt)`).\n if (uAnimSize > 0) {\n vec4 animF = texelFetch(uAnimLookup, ivec2(localId, 0), 0);\n uvec4 animTexel = uvec4(animF * 255.0 + 0.5);\n localId = int(animTexel.r) | (int(animTexel.g) << 8);\n }\n\n float row = floor(float(localId) / uTilesetCols.x);\n float col = float(localId) - row * uTilesetCols.x;\n vec2 tileOriginPx = uTilesetMargin.xy\n + vec2(col, row) * (uTileSize + uTilesetMargin.zw);\n vec2 texelPx = tileOriginPx + inTile * uTileSize;\n vec2 texelUV = texelPx * uInvTilesetSize;\n\n vec4 sampled = texture(uSampler, texelUV);\n if (sampled.a <= 0.0) {\n return false;\n }\n outColor = sampled;\n return true;\n}\n\nvoid main(void) {\n vec2 tileCoord = uVisibleStart + vRegion * uVisibleSize;\n vec2 geomCell = floor(tileCoord);\n vec2 worldPx = tileCoord * uCellSize;\n\n int gx = int(geomCell.x);\n int gy = int(geomCell.y);\n int overflowX = int(uOverflow.x + 0.5);\n int overflowY = int(uOverflow.y + 0.5);\n\n vec4 result;\n\n // Fast path: tiles fit the cell exactly (the common case) \u2014 only the\n // geometric cell can contain this fragment's tile.\n if (overflowX == 0 && overflowY == 0) {\n if (!tryRenderCell(gx, gy, worldPx, result)) {\n discard;\n }\n result.a *= uOpacity;\n fragColor = result * uTint;\n return;\n }\n\n // Slow path: oversized tiles. Walk candidates dy high\u2192low, dx low\u2192high\n // and pick the FIRST match \u2014 render order is \"right-down\" so later\n // cells go on top.\n bool found = false;\n for (int idy = 0; idy <= MAX_OVERFLOW; idy++) {\n int dy = MAX_OVERFLOW - idy;\n if (dy > overflowY) continue;\n for (int dx = 0; dx <= MAX_OVERFLOW; dx++) {\n if (dx > overflowX) break;\n if (tryRenderCell(gx - dx, gy + dy, worldPx, result)) {\n found = true;\n break;\n }\n }\n if (found) break;\n }\n if (!found) discard;\n result.a *= uOpacity;\n fragColor = result * uTint;\n}\n";
32720
+
32721
+ // src/video/webgl/shaders/orthogonal-tmxlayer.vert
32722
+ var orthogonal_tmxlayer_default2 = '#version 300 es\n\n// Vertex shader for the orthogonal TMX layer GPU renderer.\n//\n// Matches the quad batcher\'s vertex layout (`aVertex`, `aRegion`, `aColor`,\n// `uProjectionMatrix`) so the standard `setBatcher("quad", this.shader)` +\n// `addQuad()` flow drives it like any other quad. Same attribute names,\n// same uniforms \u2014 just expressed in GLSL ES 3.00 (`in`/`out` in place of\n// `attribute`/`varying`) so the program can pair with the 3.00 fragment\n// shader that uses `usampler2D` / `texelFetch` for integer-typed lookups.\n\nin vec2 aVertex;\nin vec2 aRegion;\nin vec4 aColor;\n\nuniform mat4 uProjectionMatrix;\n\nout vec2 vRegion;\nout vec4 vColor;\n\nvoid main(void) {\n gl_Position = uProjectionMatrix * vec4(aVertex, 0.0, 1.0);\n // premultiplied-alpha + bgra \u2192 rgba swap, same convention the batcher\n // uses for its default sprite shader\n vColor = vec4(aColor.bgr * aColor.a, aColor.a);\n vRegion = aRegion;\n}\n';
32723
+
32724
+ // src/video/webgl/renderers/tmxlayer/orthogonal.js
32725
+ var DEFAULT_TINT = new Float32Array([1, 1, 1, 1]);
32726
+ var OrthogonalTMXLayerGPURenderer = class {
32727
+ /**
32728
+ * @param {WebGLRenderer} renderer - the WebGL renderer instance
32729
+ */
32730
+ constructor(renderer2) {
32731
+ this.renderer = renderer2;
32732
+ this.gl = renderer2.gl;
32733
+ this.shader = new GLShader(
32734
+ renderer2.gl,
32735
+ orthogonal_tmxlayer_default2,
32736
+ orthogonal_tmxlayer_default,
32737
+ renderer2.shaderPrecision
32738
+ );
32739
+ this.resources = /* @__PURE__ */ new Map();
32740
+ this.animLookups = /* @__PURE__ */ new Map();
32741
+ this._v2 = new Float32Array(2);
32742
+ this._v4 = new Float32Array(4);
32743
+ }
32744
+ /**
32745
+ * Free every cached per-layer index texture and empty the local
32746
+ * resource map. Called from `WebGLRenderer.reset()` (which
32747
+ * `GAME_RESET` triggers) so each level transition starts clean.
32748
+ * @ignore
32749
+ */
32750
+ reset() {
32751
+ const batcher = this.renderer.currentBatcher;
32752
+ const cache2 = this.renderer.cache;
32753
+ const drop = (resource) => {
32754
+ if (batcher !== void 0) {
32755
+ batcher.deleteTexture2D(resource);
31086
32756
  } else {
31087
- gl.bufferData(
31088
- gl.ARRAY_BUFFER,
31089
- vertex.toFloat32(0, vertexCount * vertexSize),
31090
- gl.STREAM_DRAW
31091
- );
32757
+ cache2.freeTextureUnit(resource);
32758
+ cache2.delete(resource);
31092
32759
  }
31093
- const indexCount = vertexCount / 4 * 6;
31094
- gl.drawElements(mode, indexCount, this.indexBuffer.type, 0);
31095
- vertex.clear();
32760
+ };
32761
+ for (const resource of this.resources.values()) {
32762
+ drop(resource);
31096
32763
  }
32764
+ for (const entry of this.animLookups.values()) {
32765
+ drop(entry.resource);
32766
+ }
32767
+ this.resources.clear();
32768
+ this.animLookups.clear();
31097
32769
  }
31098
32770
  /**
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
32771
+ * Write a `vec2` uniform without allocating a fresh Float32Array per
32772
+ * call. Both components flow into the shared `_v2` scratch buffer,
32773
+ * which `setUniform` reads synchronously and forwards to
32774
+ * `gl.uniform2fv` so reusing the buffer across calls is safe.
32775
+ * @param {string} name
32776
+ * @param {number} x
32777
+ * @param {number} y
32778
+ * @private
31108
32779
  */
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);
32780
+ _setV2(name, x, y) {
32781
+ this._v2[0] = x;
32782
+ this._v2[1] = y;
32783
+ this.shader.setUniform(name, this._v2);
31125
32784
  }
31126
32785
  /**
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
32786
+ * `vec4` counterpart to {@link _setV2}.
32787
+ * @param {string} name
32788
+ * @param {number} x
32789
+ * @param {number} y
32790
+ * @param {number} z
32791
+ * @param {number} w
32792
+ * @private
31139
32793
  */
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();
32794
+ _setV4(name, x, y, z, w) {
32795
+ this._v4[0] = x;
32796
+ this._v4[1] = y;
32797
+ this._v4[2] = z;
32798
+ this._v4[3] = w;
32799
+ this.shader.setUniform(name, this._v4);
32800
+ }
32801
+ /**
32802
+ * Get-or-create the per-tileset animation-lookup entry. Returns
32803
+ * `undefined` for tilesets that have no animated tiles (the shader's
32804
+ * `uAnimEnabled` uniform is then set to 0 and the lookup texture is
32805
+ * not bound).
32806
+ *
32807
+ * The entry holds a `tileCount × 1` RGBA8 `BufferTextureResource`
32808
+ * where texel `localId` encodes the CURRENT frame's local id as
32809
+ * `R = lo byte, G = hi byte` (same encoding as the GID index
32810
+ * texture). Each call walks `tileset.animations` and rewrites
32811
+ * dirty texels — `tileset.update(dt)` (driven by the layer) advances
32812
+ * `anim.cur.tileid` independently of this renderer.
32813
+ *
32814
+ * @param {object} tileset
32815
+ * @param {number} tileCount - tiles in the tileset's atlas grid
32816
+ * (`atlasCols * atlasRows`)
32817
+ * @returns {{resource: BufferTextureResource, data: Uint8Array,
32818
+ * tileCount: number, dirty: boolean}|undefined}
32819
+ */
32820
+ _getOrUpdateAnimLookup(tileset, tileCount) {
32821
+ if (!tileset.isAnimated || tileset.animations.size === 0) {
32822
+ return void 0;
31144
32823
  }
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;
32824
+ let entry = this.animLookups.get(tileset);
32825
+ if (entry === void 0) {
32826
+ const data3 = new Uint8Array(tileCount * 4);
32827
+ for (let id = 0; id < tileCount; id++) {
32828
+ data3[id * 4 + 0] = id & 255;
32829
+ data3[id * 4 + 1] = id >> 8 & 255;
32830
+ }
32831
+ const resource = new BufferTextureResource(data3, {
32832
+ width: tileCount,
32833
+ height: 1,
32834
+ premultipliedAlpha: false,
32835
+ repeat: "no-repeat",
32836
+ // NEAREST so the shader's `texelFetch` reads byte-exact
32837
+ // channel values back as normalized floats
32838
+ filter: this.gl.NEAREST,
32839
+ format: "rgba8"
32840
+ });
32841
+ entry = { resource, data: data3, tileCount, dirty: false };
32842
+ this.animLookups.set(tileset, entry);
32843
+ }
32844
+ const data2 = entry.data;
32845
+ for (const [localId, anim] of tileset.animations) {
32846
+ const off2 = localId * 4;
32847
+ const cur = anim.cur.tileid;
32848
+ const lo = cur & 255;
32849
+ const hi = cur >> 8 & 255;
32850
+ if (data2[off2] !== lo || data2[off2 + 1] !== hi) {
32851
+ data2[off2] = lo;
32852
+ data2[off2 + 1] = hi;
32853
+ entry.dirty = true;
31158
32854
  }
31159
32855
  }
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);
32856
+ return entry;
32857
+ }
32858
+ /**
32859
+ * Get-or-create the per-layer index `BufferTextureResource`.
32860
+ * @param {TMXLayer} layer
32861
+ * @returns {BufferTextureResource}
32862
+ */
32863
+ _getResource(layer) {
32864
+ let resource = this.resources.get(layer);
32865
+ if (resource === void 0) {
32866
+ resource = new BufferTextureResource(
32867
+ new Uint8Array(layer.layerData.buffer),
32868
+ {
32869
+ width: layer.cols,
32870
+ height: layer.rows,
32871
+ // raw GID bytes — must NOT have alpha pre-multiplied
32872
+ // into RGB, otherwise A=0 cells wipe their R/G/B
32873
+ premultipliedAlpha: false,
32874
+ repeat: "no-repeat",
32875
+ // NEAREST so `texelFetch` returns the original byte
32876
+ // values (as normalized floats) for the GID/flip
32877
+ // decode below
32878
+ filter: this.gl.NEAREST,
32879
+ format: "rgba8"
32880
+ }
32881
+ );
32882
+ resource._uploadedVersion = -1;
32883
+ this.resources.set(layer, resource);
32884
+ }
32885
+ return resource;
32886
+ }
32887
+ /**
32888
+ * Draw an orthogonal TMX layer through the shader path.
32889
+ * @param {TMXLayer} layer
32890
+ * @param {object} rect - the visible viewport rect (world coords)
32891
+ */
32892
+ draw(layer, rect) {
32893
+ const renderer2 = this.renderer;
32894
+ const tileWidth = layer.tilewidth;
32895
+ const tileHeight = layer.tileheight;
32896
+ const cols = layer.cols;
32897
+ const rows = layer.rows;
32898
+ const startTileX = Math.max(0, Math.floor(rect.pos.x / tileWidth));
32899
+ const startTileY = Math.max(0, Math.floor(rect.pos.y / tileHeight));
32900
+ const endTileX = Math.min(
32901
+ cols,
32902
+ Math.ceil((rect.pos.x + rect.width) / tileWidth)
32903
+ );
32904
+ const endTileY = Math.min(
32905
+ rows,
32906
+ Math.ceil((rect.pos.y + rect.height) / tileHeight)
32907
+ );
32908
+ if (endTileX <= startTileX || endTileY <= startTileY) {
32909
+ return;
32910
+ }
32911
+ const worldX = startTileX * tileWidth;
32912
+ const worldY = startTileY * tileHeight;
32913
+ const worldW = (endTileX - startTileX) * tileWidth;
32914
+ const worldH = (endTileY - startTileY) * tileHeight;
32915
+ const visStartX = startTileX;
32916
+ const visStartY = startTileY;
32917
+ const visSizeX = endTileX - startTileX;
32918
+ const visSizeY = endTileY - startTileY;
32919
+ const batcher = renderer2.setBatcher("quad", this.shader);
32920
+ const resource = this._getResource(layer);
32921
+ const indexUnit = batcher.uploadTexture(
32922
+ resource,
32923
+ cols,
32924
+ rows,
32925
+ resource._uploadedVersion !== layer.dataVersion
32926
+ );
32927
+ resource._uploadedVersion = layer.dataVersion;
32928
+ this.shader.setUniform("uTileIndex", indexUnit);
32929
+ this._setV2("uMapSize", cols, rows);
32930
+ this._setV2("uVisibleStart", visStartX, visStartY);
32931
+ this._setV2("uVisibleSize", visSizeX, visSizeY);
32932
+ this._setV2("uCellSize", tileWidth, tileHeight);
32933
+ this.shader.setUniform("uOpacity", layer.getOpacity());
32934
+ this.shader.setUniform(
32935
+ "uTint",
32936
+ layer.tint ? layer.tint.toArray() : DEFAULT_TINT
32937
+ );
32938
+ const tilesets = layer.tilesets.tilesets;
32939
+ for (let i = 0; i < tilesets.length; i++) {
32940
+ const tileset = tilesets[i];
32941
+ if (tileset.isCollection || tileset.image === void 0) {
32942
+ continue;
32943
+ }
32944
+ const tsW = tileset.tilewidth;
32945
+ const tsH = tileset.tileheight;
32946
+ const margin = tileset.margin;
32947
+ const spacing = tileset.spacing;
32948
+ const atlasW = tileset.image.width;
32949
+ const atlasH = tileset.image.height;
32950
+ const atlasCols = Math.max(
32951
+ 1,
32952
+ Math.floor((atlasW - margin * 2 + spacing) / (tsW + spacing))
32953
+ );
32954
+ const atlasRows = Math.max(
32955
+ 1,
32956
+ Math.floor((atlasH - margin * 2 + spacing) / (tsH + spacing))
32957
+ );
32958
+ const tileCount = atlasCols * atlasRows;
32959
+ const animEntry = this._getOrUpdateAnimLookup(tileset, tileCount);
32960
+ if (animEntry !== void 0) {
32961
+ const animUnit = batcher.uploadTexture(
32962
+ animEntry.resource,
32963
+ animEntry.tileCount,
32964
+ 1,
32965
+ animEntry.dirty
32966
+ );
32967
+ animEntry.dirty = false;
32968
+ this.shader.setUniform("uAnimLookup", animUnit);
32969
+ this.shader.setUniform("uAnimSize", animEntry.tileCount);
32970
+ } else {
32971
+ this.shader.setUniform("uAnimLookup", indexUnit);
32972
+ this.shader.setUniform("uAnimSize", 0);
32973
+ }
32974
+ this._setV2("uTileSize", tsW, tsH);
32975
+ this._setV2("uTilesetCols", atlasCols, atlasRows);
32976
+ this._setV2("uInvTilesetSize", 1 / atlasW, 1 / atlasH);
32977
+ this._setV4("uTilesetMargin", margin, margin, spacing, spacing);
32978
+ this._setV2(
32979
+ "uOverflow",
32980
+ Math.max(0, Math.ceil(tsW / tileWidth) - 1),
32981
+ Math.max(0, Math.ceil(tsH / tileHeight) - 1)
32982
+ );
32983
+ this._setV2("uGidRange", tileset.firstgid, tileset.lastgid);
32984
+ batcher.addQuad(
32985
+ tileset.texture,
32986
+ worldX,
32987
+ worldY,
32988
+ worldW,
32989
+ worldH,
32990
+ 0,
32991
+ 0,
32992
+ 1,
32993
+ 1,
32994
+ 4294967295
32995
+ );
32996
+ batcher.flush();
31170
32997
  }
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
32998
  }
31177
32999
  };
31178
33000
 
@@ -31205,6 +33027,7 @@ var WebGLRenderer = class extends Renderer {
31205
33027
  this._rectTriangles = Array.from({ length: 6 }, () => {
31206
33028
  return { x: 0, y: 0 };
31207
33029
  });
33030
+ this._clipAABB = new Bounds();
31208
33031
  this._polyVerts = [];
31209
33032
  this._currentGradient = null;
31210
33033
  this.currentTransform = this.renderState.currentTransform;
@@ -31214,6 +33037,9 @@ var WebGLRenderer = class extends Renderer {
31214
33037
  this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
31215
33038
  const CustomBatcher = this.settings.batcher || this.settings.compositor;
31216
33039
  this.addBatcher(new (CustomBatcher || QuadBatcher)(this), "quad", true);
33040
+ if (!CustomBatcher) {
33041
+ this.addBatcher(new LitQuadBatcher(this), "litQuad");
33042
+ }
31217
33043
  this.addBatcher(new (CustomBatcher || PrimitiveBatcher)(this), "primitive");
31218
33044
  this.addBatcher(new MeshBatcher(this), "mesh");
31219
33045
  this.gl.disable(this.gl.DEPTH_TEST);
@@ -31314,6 +33140,7 @@ var WebGLRenderer = class extends Renderer {
31314
33140
  reset() {
31315
33141
  super.reset();
31316
33142
  this.clear();
33143
+ this._orthogonalTMXGPURenderer?.reset();
31317
33144
  this.setViewport();
31318
33145
  if (this.gl.getParameter(this.gl.ARRAY_BUFFER_BINDING) !== this.vertexBuffer) {
31319
33146
  this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
@@ -31332,6 +33159,63 @@ var WebGLRenderer = class extends Renderer {
31332
33159
  this.gl.disable(this.gl.SCISSOR_TEST);
31333
33160
  this._scissorActive = false;
31334
33161
  this._renderTargetPool.destroy();
33162
+ if (this._lightShader !== void 0) {
33163
+ this._lightShader.destroy?.();
33164
+ this._lightShader = void 0;
33165
+ }
33166
+ if (this._lightAtlas !== void 0) {
33167
+ this._lightAtlas.sources.forEach((source) => {
33168
+ this.cache.delete?.(source);
33169
+ });
33170
+ this._lightAtlas = void 0;
33171
+ }
33172
+ if (this.isContextValid === false) {
33173
+ this._orthogonalTMXGPURenderer = void 0;
33174
+ }
33175
+ }
33176
+ /**
33177
+ * Draw a TMX tile layer through whichever path the layer's `renderMode`
33178
+ * resolves to. WebGL2-eligible layers (`renderMode === "shader"`) take
33179
+ * the procedural shader path — one quad per tileset, GID lookup in a
33180
+ * per-layer data texture. All other layers fall through to the base
33181
+ * `Renderer.drawTileLayer` (preRender blit or per-tile loop).
33182
+ * @param {object} layer - the TMXLayer to draw
33183
+ * @param {object} rect - the visible region in world coords
33184
+ */
33185
+ drawTileLayer(layer, rect) {
33186
+ if (layer.renderMode === "shader") {
33187
+ const gpu = this._getTMXGPURendererFor(layer.orientation);
33188
+ if (gpu !== void 0) {
33189
+ gpu.draw(layer, rect);
33190
+ return;
33191
+ }
33192
+ }
33193
+ super.drawTileLayer(layer, rect);
33194
+ }
33195
+ /**
33196
+ * Lazy-init the orientation-specific GPU tilemap renderer.
33197
+ * @param {string} orientation
33198
+ * @returns {object|undefined}
33199
+ * @ignore
33200
+ */
33201
+ _getTMXGPURendererFor(orientation2) {
33202
+ if (orientation2 === "orthogonal") {
33203
+ if (this._orthogonalTMXGPURenderer === void 0) {
33204
+ try {
33205
+ this._orthogonalTMXGPURenderer = new OrthogonalTMXLayerGPURenderer(
33206
+ this
33207
+ );
33208
+ } catch (err) {
33209
+ console.warn(
33210
+ "melonJS: GPU tilemap shader failed to compile, falling back to legacy renderer",
33211
+ err
33212
+ );
33213
+ this._orthogonalTMXGPURenderer = null;
33214
+ }
33215
+ }
33216
+ return this._orthogonalTMXGPURenderer || void 0;
33217
+ }
33218
+ return void 0;
31335
33219
  }
31336
33220
  /**
31337
33221
  * add a new batcher to this renderer
@@ -31359,21 +33243,21 @@ var WebGLRenderer = class extends Renderer {
31359
33243
  if (typeof batcher === "undefined") {
31360
33244
  throw new Error("Invalid Batcher");
31361
33245
  }
31362
- if (this.currentBatcher === batcher && typeof shader !== "object") {
33246
+ const targetShader = shader != null ? shader : batcher.defaultShader;
33247
+ if (this.currentBatcher === batcher && batcher.currentShader === targetShader) {
31363
33248
  return this.currentBatcher;
31364
33249
  }
31365
33250
  if (this.currentBatcher !== batcher) {
31366
33251
  if (this.currentBatcher !== void 0) {
31367
33252
  this.currentBatcher.flush();
33253
+ this.currentBatcher.unbind();
31368
33254
  }
31369
33255
  this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
31370
33256
  this.currentBatcher = batcher;
31371
33257
  this.currentBatcher.bind();
31372
33258
  this.currentBatcher.setProjection(this.projectionMatrix);
31373
33259
  }
31374
- if (typeof shader === "object") {
31375
- this.currentBatcher.useShader(shader);
31376
- }
33260
+ this.currentBatcher.useShader(targetShader);
31377
33261
  return this.currentBatcher;
31378
33262
  }
31379
33263
  /**
@@ -31418,6 +33302,109 @@ var WebGLRenderer = class extends Renderer {
31418
33302
  flush() {
31419
33303
  this.currentBatcher.flush();
31420
33304
  }
33305
+ /**
33306
+ * Upload per-frame Light2d uniforms used by the lit sprite pipeline.
33307
+ *
33308
+ * Packs the active lights into pre-allocated scratch buffers, then
33309
+ * forwards to `LitQuadBatcher`. Light positions are translated from
33310
+ * world-space (where `light.getBounds().centerX/Y` lives) into the
33311
+ * renderer's pre-projection coords by subtracting `(translateX, translateY)`,
33312
+ * matching what `Stage.drawLighting` does for the cutout pass — so
33313
+ * the lit fragment's `lightPos - vWorldPos` math lines up with the
33314
+ * camera's view.
33315
+ *
33316
+ * Lights past `MAX_LIGHTS` (8) are silently dropped. Also caches the
33317
+ * active light count on the renderer so `drawImage` can dispatch
33318
+ * normal-mapped sprites to the lit batcher only when there's
33319
+ * something to light them with.
33320
+ * @param {Iterable<object>} [lights] - active `Light2d` instances; falsy/empty no-ops the lit pipeline
33321
+ * @param {object} [ambient] - ambient lighting color (0..255 RGB); defaults to black
33322
+ * @param {number} [translateX=0] - world-to-screen X translate (matches `Camera2d.draw()`)
33323
+ * @param {number} [translateY=0] - world-to-screen Y translate
33324
+ */
33325
+ setLightUniforms(lights, ambient, translateX = 0, translateY = 0) {
33326
+ if (this._lightUniformsScratch === void 0) {
33327
+ this._lightUniformsScratch = createLightUniformScratch();
33328
+ }
33329
+ const u = packLights(
33330
+ lights,
33331
+ ambient,
33332
+ translateX,
33333
+ translateY,
33334
+ this._lightUniformsScratch
33335
+ );
33336
+ this.activeLightCount = u.count;
33337
+ const lit = this.batchers.get("litQuad");
33338
+ if (lit && typeof lit.setLightUniforms === "function") {
33339
+ lit.setLightUniforms(u);
33340
+ if (this.currentBatcher && this.currentBatcher !== lit) {
33341
+ const shader = this.currentBatcher.currentShader || this.currentBatcher.defaultShader;
33342
+ if (shader) {
33343
+ this.gl.useProgram(shader.program);
33344
+ this.currentProgram = shader.program;
33345
+ }
33346
+ }
33347
+ }
33348
+ }
33349
+ /**
33350
+ * @inheritdoc
33351
+ *
33352
+ * Renders the light as a quad through a shared
33353
+ * {@link RadialGradientEffect} fragment shader (procedural — no
33354
+ * per-light texture). The shader and a shared 1×1 white-pixel atlas
33355
+ * are lazy-allocated on first call and reused for every Light2d on
33356
+ * this renderer. Each light's color and intensity are encoded into
33357
+ * the per-vertex tint so consecutive `drawLight` calls accumulate
33358
+ * into the quad batcher's buffer and flush together — N lights
33359
+ * become 1 program switch + 1 flush instead of 2N + N.
33360
+ * @param {object} light - the Light2d instance to render
33361
+ */
33362
+ drawLight(light) {
33363
+ if (this._lightShader === void 0) {
33364
+ this._lightShader = new RadialGradientEffect(this);
33365
+ }
33366
+ const batcher = this.setBatcher("quad", this._lightShader);
33367
+ batcher.addQuad(
33368
+ this._getLightAtlas(),
33369
+ light.pos.x,
33370
+ light.pos.y,
33371
+ light.width,
33372
+ light.height,
33373
+ 0,
33374
+ 0,
33375
+ 1,
33376
+ 1,
33377
+ // pack the light's color (RGB) and intensity (A) into the
33378
+ // vertex tint — the shader's `apply()` reads `color.rgb` and
33379
+ // `color.a` as the per-light values.
33380
+ light.color.toUint32(light.intensity)
33381
+ );
33382
+ }
33383
+ /**
33384
+ * Lazy-init a shared 1×1 white `TextureAtlas` used as the source
33385
+ * texture for `drawLight`'s procedural shader. The shader ignores
33386
+ * the sampled color, but `addQuad`'s vertex format includes a
33387
+ * texture-unit attribute so we still need a real texture; sharing
33388
+ * one across every light keeps them on the same multi-texture slot
33389
+ * (no flush on light switch).
33390
+ * @returns {TextureAtlas}
33391
+ * @ignore
33392
+ */
33393
+ _getLightAtlas() {
33394
+ if (this._lightAtlas === void 0) {
33395
+ const canvas = globalThis.document ? globalThis.document.createElement("canvas") : new OffscreenCanvas(1, 1);
33396
+ canvas.width = 1;
33397
+ canvas.height = 1;
33398
+ const ctx = canvas.getContext("2d");
33399
+ ctx.fillStyle = "#fff";
33400
+ ctx.fillRect(0, 0, 1, 1);
33401
+ this._lightAtlas = new TextureAtlas(
33402
+ createAtlas(1, 1, "lightWhite", "no-repeat"),
33403
+ canvas
33404
+ );
33405
+ }
33406
+ return this._lightAtlas;
33407
+ }
31421
33408
  /**
31422
33409
  * Begin capturing rendering to an offscreen FBO for post-effect processing.
31423
33410
  * @param {Renderable} renderable - the renderable requesting post-effect processing
@@ -31602,19 +33589,22 @@ var WebGLRenderer = class extends Renderer {
31602
33589
  */
31603
33590
  enableScissor(x, y, width, height) {
31604
33591
  const gl = this.gl;
33592
+ const canvas = this.getCanvas();
33593
+ const aabb = this._clipAABB;
33594
+ aabb.clear();
33595
+ aabb.addFrame(x, y, x + width, y + height, this.currentTransform);
33596
+ const sx = Math.floor(aabb.min.x);
33597
+ const sy = Math.floor(aabb.min.y);
33598
+ const sw = Math.ceil(aabb.max.x - sx);
33599
+ const sh = Math.ceil(aabb.max.y - sy);
31605
33600
  this.flush();
31606
33601
  gl.enable(gl.SCISSOR_TEST);
31607
33602
  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;
33603
+ gl.scissor(sx, canvas.height - sh - sy, sw, sh);
33604
+ this.currentScissor[0] = sx;
33605
+ this.currentScissor[1] = sy;
33606
+ this.currentScissor[2] = sw;
33607
+ this.currentScissor[3] = sh;
31618
33608
  }
31619
33609
  /**
31620
33610
  * Disable the scissor test, allowing rendering to the full viewport.
@@ -31721,27 +33711,45 @@ var WebGLRenderer = class extends Renderer {
31721
33711
  dx |= 0;
31722
33712
  dy |= 0;
31723
33713
  }
31724
- this.setBatcher("quad");
33714
+ const useLit = this.batchers.has("litQuad") && this.activeLightCount > 0 && this.currentNormalMap !== null;
33715
+ this.setBatcher(useLit ? "litQuad" : "quad");
31725
33716
  const shader = this.customShader;
31726
- if (typeof shader === "object") {
33717
+ if (shader != null) {
31727
33718
  this.currentBatcher.useShader(shader);
31728
33719
  }
31729
33720
  const reupload = typeof image.videoWidth !== "undefined";
31730
33721
  const texture = this.cache.get(image);
31731
33722
  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
- );
33723
+ if (useLit) {
33724
+ this.currentBatcher.addQuad(
33725
+ texture,
33726
+ dx,
33727
+ dy,
33728
+ dw,
33729
+ dh,
33730
+ uvs[0],
33731
+ uvs[1],
33732
+ uvs[2],
33733
+ uvs[3],
33734
+ this.currentTint.toUint32(this.getGlobalAlpha()),
33735
+ reupload,
33736
+ this.currentNormalMap
33737
+ );
33738
+ } else {
33739
+ this.currentBatcher.addQuad(
33740
+ texture,
33741
+ dx,
33742
+ dy,
33743
+ dw,
33744
+ dh,
33745
+ uvs[0],
33746
+ uvs[1],
33747
+ uvs[2],
33748
+ uvs[3],
33749
+ this.currentTint.toUint32(this.getGlobalAlpha()),
33750
+ reupload
33751
+ );
33752
+ }
31745
33753
  if (typeof shader === "object") {
31746
33754
  this.currentBatcher.useShader(this.currentBatcher.defaultShader);
31747
33755
  }
@@ -31781,7 +33789,7 @@ var WebGLRenderer = class extends Renderer {
31781
33789
  drawMesh(mesh) {
31782
33790
  const gl = this.gl;
31783
33791
  this.setBatcher("mesh");
31784
- if (typeof this.customShader === "object") {
33792
+ if (this.customShader != null) {
31785
33793
  this.currentBatcher.useShader(this.customShader);
31786
33794
  }
31787
33795
  gl.enable(gl.DEPTH_TEST);
@@ -31806,7 +33814,7 @@ var WebGLRenderer = class extends Renderer {
31806
33814
  gl.enable(gl.BLEND);
31807
33815
  gl.disable(gl.DEPTH_TEST);
31808
33816
  gl.depthMask(false);
31809
- if (typeof this.customShader === "object") {
33817
+ if (this.customShader != null) {
31810
33818
  this.currentBatcher.useShader(this.currentBatcher.defaultShader);
31811
33819
  }
31812
33820
  }
@@ -32041,23 +34049,33 @@ var WebGLRenderer = class extends Renderer {
32041
34049
  */
32042
34050
  restore() {
32043
34051
  const canvas = this.getCanvas();
34052
+ const peek = this.renderState.peekScissor();
34053
+ const cur = this.currentScissor;
34054
+ const curActive = this._scissorActive === true;
34055
+ const willBeActive = peek !== null;
34056
+ const scissorChanging = curActive !== willBeActive || willBeActive && (cur[0] !== peek[0] || cur[1] !== peek[1] || cur[2] !== peek[2] || cur[3] !== peek[3]);
34057
+ if (scissorChanging) {
34058
+ this.flush();
34059
+ }
32044
34060
  const result = this.renderState.restore(canvas.width, canvas.height);
32045
34061
  if (result !== null) {
32046
34062
  this.setBlendMode(result.blendMode);
32047
- if (result.scissorActive) {
34063
+ if (scissorChanging) {
32048
34064
  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;
34065
+ if (result.scissorActive) {
34066
+ const next = this.currentScissor;
34067
+ gl.enable(gl.SCISSOR_TEST);
34068
+ this._scissorActive = true;
34069
+ gl.scissor(
34070
+ next[0],
34071
+ canvas.height - next[3] - next[1],
34072
+ next[2],
34073
+ next[3]
34074
+ );
34075
+ } else {
34076
+ gl.disable(gl.SCISSOR_TEST);
34077
+ this._scissorActive = false;
34078
+ }
32061
34079
  }
32062
34080
  }
32063
34081
  this._currentGradient = this.renderState.currentGradient;
@@ -32726,31 +34744,42 @@ var WebGLRenderer = class extends Renderer {
32726
34744
  clipRect(x, y, width, height) {
32727
34745
  const canvas = this.getCanvas();
32728
34746
  const gl = this.gl;
32729
- if (x !== 0 || y !== 0 || width !== canvas.width || height !== canvas.height) {
32730
- const currentScissor = this.currentScissor;
34747
+ const m = this.currentTransform;
34748
+ if (!Number.isFinite(m.tx) || !Number.isFinite(m.ty)) {
32731
34749
  if (this._scissorActive) {
32732
- if (currentScissor[0] === x && currentScissor[1] === y && currentScissor[2] === width && currentScissor[3] === height) {
32733
- return;
32734
- }
34750
+ this.flush();
34751
+ gl.disable(gl.SCISSOR_TEST);
34752
+ this._scissorActive = false;
32735
34753
  }
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;
34754
+ return;
34755
+ }
34756
+ const aabb = this._clipAABB;
34757
+ aabb.clear();
34758
+ aabb.addFrame(x, y, x + width, y + height, m);
34759
+ const sx = Math.floor(aabb.min.x);
34760
+ const sy = Math.floor(aabb.min.y);
34761
+ const sw = Math.ceil(aabb.max.x - sx);
34762
+ const sh = Math.ceil(aabb.max.y - sy);
34763
+ if (sx <= 0 && sy <= 0 && sx + sw >= canvas.width && sy + sh >= canvas.height) {
34764
+ if (this._scissorActive) {
34765
+ this.flush();
34766
+ gl.disable(gl.SCISSOR_TEST);
34767
+ this._scissorActive = false;
34768
+ }
34769
+ return;
34770
+ }
34771
+ const cs = this.currentScissor;
34772
+ if (this._scissorActive && cs[0] === sx && cs[1] === sy && cs[2] === sw && cs[3] === sh) {
34773
+ return;
32753
34774
  }
34775
+ this.flush();
34776
+ gl.enable(gl.SCISSOR_TEST);
34777
+ this._scissorActive = true;
34778
+ gl.scissor(sx, canvas.height - sh - sy, sw, sh);
34779
+ cs[0] = sx;
34780
+ cs[1] = sy;
34781
+ cs[2] = sw;
34782
+ cs[3] = sh;
32754
34783
  }
32755
34784
  /**
32756
34785
  * A mask limits rendering elements to the shape and position of the given mask object.
@@ -32769,15 +34798,16 @@ var WebGLRenderer = class extends Renderer {
32769
34798
  }
32770
34799
  this.maskLevel++;
32771
34800
  gl.colorMask(false, false, false, false);
32772
- gl.stencilFunc(gl.EQUAL, this.maskLevel, 1);
32773
- gl.stencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE);
34801
+ gl.stencilMask(255);
34802
+ gl.stencilFunc(gl.ALWAYS, 0, 255);
34803
+ gl.stencilOp(gl.KEEP, gl.KEEP, gl.INCR);
32774
34804
  this.fill(mask);
32775
34805
  this.flush();
32776
34806
  gl.colorMask(true, true, true, true);
32777
34807
  if (invert === true) {
32778
- gl.stencilFunc(gl.EQUAL, this.maskLevel + 1, 1);
34808
+ gl.stencilFunc(gl.EQUAL, 0, 255);
32779
34809
  } else {
32780
- gl.stencilFunc(gl.NOTEQUAL, this.maskLevel + 1, 1);
34810
+ gl.stencilFunc(gl.EQUAL, this.maskLevel, 255);
32781
34811
  }
32782
34812
  gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
32783
34813
  }
@@ -33108,6 +35138,14 @@ var Application = class {
33108
35138
  this.world = new World(0, 0, this.settings.width, this.settings.height);
33109
35139
  this.world.app = this;
33110
35140
  this.world.physic = this.settings.physic;
35141
+ this.world.gpuTilemap = this.settings.gpuTilemap;
35142
+ if (this.settings.gpuTilemap && // duck-type rather than `instanceof WebGLRenderer` to avoid a
35143
+ // runtime import; only the WebGL renderer carries `WebGLVersion`
35144
+ this.renderer.WebGLVersion !== 2) {
35145
+ console.warn(
35146
+ "melonJS: gpuTilemap is enabled but the active renderer is not WebGL 2 \u2014 falling back to the legacy tile renderer for every tile layer"
35147
+ );
35148
+ }
33111
35149
  this.lastUpdate = globalThis.performance.now();
33112
35150
  if (!this.isInitialized) {
33113
35151
  on(STATE_CHANGE, this.repaint, this);
@@ -33129,9 +35167,7 @@ var Application = class {
33129
35167
  reset() {
33130
35168
  const current = state_default.get();
33131
35169
  if (typeof current !== "undefined") {
33132
- this.viewport = current.cameras.get(
33133
- "default"
33134
- );
35170
+ this.viewport = current.cameras.get("default");
33135
35171
  }
33136
35172
  emit(GAME_RESET);
33137
35173
  this.updateFrameRate();