melonjs 19.1.0 → 19.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +7 -5
  2. package/build/application/application.d.ts +33 -0
  3. package/build/application/application.d.ts.map +1 -1
  4. package/build/application/settings.d.ts +1 -0
  5. package/build/application/settings.d.ts.map +1 -1
  6. package/build/camera/camera2d.d.ts +45 -22
  7. package/build/camera/camera2d.d.ts.map +1 -1
  8. package/build/camera/effects/camera_effect.d.ts +45 -0
  9. package/build/camera/effects/camera_effect.d.ts.map +1 -0
  10. package/build/camera/effects/fade_effect.d.ts +60 -0
  11. package/build/camera/effects/fade_effect.d.ts.map +1 -0
  12. package/build/camera/effects/mask_effect.d.ts +88 -0
  13. package/build/camera/effects/mask_effect.d.ts.map +1 -0
  14. package/build/camera/effects/shake_effect.d.ts +47 -0
  15. package/build/camera/effects/shake_effect.d.ts.map +1 -0
  16. package/build/index.d.ts +10 -2
  17. package/build/index.d.ts.map +1 -1
  18. package/build/index.js +2788 -914
  19. package/build/index.js.map +4 -4
  20. package/build/level/tiled/TMXObjectFactory.d.ts.map +1 -1
  21. package/build/math/color_matrix.d.ts +51 -0
  22. package/build/math/color_matrix.d.ts.map +1 -0
  23. package/build/math/matrix2d.d.ts +6 -6
  24. package/build/math/matrix3d.d.ts +17 -0
  25. package/build/math/matrix3d.d.ts.map +1 -1
  26. package/build/particles/emitter.d.ts +19 -5
  27. package/build/particles/emitter.d.ts.map +1 -1
  28. package/build/particles/particle.d.ts +4 -1
  29. package/build/particles/particle.d.ts.map +1 -1
  30. package/build/particles/settings.d.ts +200 -31
  31. package/build/particles/settings.d.ts.map +1 -1
  32. package/build/physics/quadtree.d.ts.map +1 -1
  33. package/build/renderable/imagelayer.d.ts +13 -2
  34. package/build/renderable/imagelayer.d.ts.map +1 -1
  35. package/build/renderable/renderable.d.ts +54 -35
  36. package/build/renderable/renderable.d.ts.map +1 -1
  37. package/build/renderable/text/bitmaptext.d.ts +33 -5
  38. package/build/renderable/text/bitmaptext.d.ts.map +1 -1
  39. package/build/renderable/text/text.d.ts +27 -1
  40. package/build/renderable/text/text.d.ts.map +1 -1
  41. package/build/renderable/trigger.d.ts +44 -19
  42. package/build/renderable/trigger.d.ts.map +1 -1
  43. package/build/state/state.d.ts +44 -2
  44. package/build/state/state.d.ts.map +1 -1
  45. package/build/tweens/tween.d.ts +9 -0
  46. package/build/tweens/tween.d.ts.map +1 -1
  47. package/build/video/buffer/index.d.ts +40 -0
  48. package/build/video/buffer/index.d.ts.map +1 -0
  49. package/build/video/{webgl/buffer → buffer}/vertex.d.ts +3 -2
  50. package/build/video/buffer/vertex.d.ts.map +1 -0
  51. package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
  52. package/build/video/renderer.d.ts +79 -0
  53. package/build/video/renderer.d.ts.map +1 -1
  54. package/build/video/renderstate.d.ts +7 -0
  55. package/build/video/renderstate.d.ts.map +1 -1
  56. package/build/video/rendertarget/canvasrendertarget.d.ts +26 -63
  57. package/build/video/rendertarget/canvasrendertarget.d.ts.map +1 -1
  58. package/build/video/rendertarget/render_target_pool.d.ts +73 -0
  59. package/build/video/rendertarget/render_target_pool.d.ts.map +1 -0
  60. package/build/video/rendertarget/rendertarget.d.ts +77 -0
  61. package/build/video/rendertarget/rendertarget.d.ts.map +1 -0
  62. package/build/video/rendertarget/webglrendertarget.d.ts +30 -0
  63. package/build/video/rendertarget/webglrendertarget.d.ts.map +1 -0
  64. package/build/video/webgl/batchers/batcher.d.ts +4 -4
  65. package/build/video/webgl/batchers/batcher.d.ts.map +1 -1
  66. package/build/video/webgl/batchers/quad_batcher.d.ts +15 -2
  67. package/build/video/webgl/batchers/quad_batcher.d.ts.map +1 -1
  68. package/build/video/webgl/buffer/index.d.ts +5 -34
  69. package/build/video/webgl/buffer/index.d.ts.map +1 -1
  70. package/build/video/webgl/effects/colorMatrix.d.ts +92 -0
  71. package/build/video/webgl/effects/colorMatrix.d.ts.map +1 -0
  72. package/build/video/webgl/effects/desaturate.d.ts +2 -4
  73. package/build/video/webgl/effects/desaturate.d.ts.map +1 -1
  74. package/build/video/webgl/effects/invert.d.ts +2 -2
  75. package/build/video/webgl/effects/invert.d.ts.map +1 -1
  76. package/build/video/webgl/effects/sepia.d.ts +2 -2
  77. package/build/video/webgl/effects/sepia.d.ts.map +1 -1
  78. package/build/video/webgl/effects/vignette.d.ts +42 -0
  79. package/build/video/webgl/effects/vignette.d.ts.map +1 -0
  80. package/build/video/webgl/webgl_renderer.d.ts +8 -8
  81. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  82. package/package.json +1 -1
  83. package/build/video/webgl/buffer/vertex.d.ts.map +0 -1
package/build/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * melonJS Game Engine - 19.1.0
2
+ * melonJS Game Engine - 19.2.0
3
3
  * http://www.melonjs.org
4
4
  * melonjs is licensed under the MIT License.
5
5
  * http://www.opensource.org/licenses/mit-license
@@ -7060,31 +7060,8 @@ function unbindKey(keyCode) {
7060
7060
  delete _preventDefaultForKeys[keyCode];
7061
7061
  }
7062
7062
 
7063
- // src/lang/console.js
7064
- function warning(deprecated, replacement, version2) {
7065
- const msg = "melonJS: %s is deprecated since version %s, please use %s";
7066
- const stack = new Error().stack;
7067
- if (console.groupCollapsed) {
7068
- console.groupCollapsed(
7069
- "%c" + msg,
7070
- "font-weight:normal;color:yellow;",
7071
- deprecated,
7072
- version2,
7073
- replacement
7074
- );
7075
- } else {
7076
- console.warn(msg, deprecated, version2, replacement);
7077
- }
7078
- if (typeof stack !== "undefined") {
7079
- console.warn(stack);
7080
- }
7081
- if (console.groupCollapsed) {
7082
- console.groupEnd();
7083
- }
7084
- }
7085
-
7086
7063
  // src/geometries/ellipse.ts
7087
- var Ellipse = class _Ellipse {
7064
+ var Ellipse = class {
7088
7065
  /**
7089
7066
  * the center coordinates of the ellipse
7090
7067
  */
@@ -7352,7 +7329,7 @@ var Ellipse = class _Ellipse {
7352
7329
  * @returns new Ellipse
7353
7330
  */
7354
7331
  clone() {
7355
- const clone = new _Ellipse(
7332
+ const clone = ellipsePool.get(
7356
7333
  this.pos.x,
7357
7334
  this.pos.y,
7358
7335
  this.radiusV.x * 2,
@@ -8986,6 +8963,73 @@ var Matrix3d = class {
8986
8963
  a[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
8987
8964
  return this;
8988
8965
  }
8966
+ transform(b00, b01, b02, b03, b10, b11, b12, b13, b20, b21, b22, b23, b30, b31, b32, b33) {
8967
+ const argc = arguments.length;
8968
+ if (argc !== 6 && argc !== 16) {
8969
+ throw new Error(`transform() requires 6 or 16 arguments, got ${argc}`);
8970
+ }
8971
+ let c0, c1, c2, c3;
8972
+ let c4, c5, c6, c7;
8973
+ let c8, c9, c10, c11;
8974
+ let c12, c13, c14, c15;
8975
+ if (argc === 6) {
8976
+ c0 = b00;
8977
+ c1 = b01;
8978
+ c2 = 0;
8979
+ c3 = 0;
8980
+ c4 = b02;
8981
+ c5 = b03;
8982
+ c6 = 0;
8983
+ c7 = 0;
8984
+ c8 = 0;
8985
+ c9 = 0;
8986
+ c10 = 1;
8987
+ c11 = 0;
8988
+ c12 = b10;
8989
+ c13 = b11;
8990
+ c14 = 0;
8991
+ c15 = 1;
8992
+ } else {
8993
+ c0 = b00;
8994
+ c1 = b01;
8995
+ c2 = b02;
8996
+ c3 = b03;
8997
+ c4 = b10;
8998
+ c5 = b11;
8999
+ c6 = b12;
9000
+ c7 = b13;
9001
+ c8 = b20;
9002
+ c9 = b21;
9003
+ c10 = b22;
9004
+ c11 = b23;
9005
+ c12 = b30;
9006
+ c13 = b31;
9007
+ c14 = b32;
9008
+ c15 = b33;
9009
+ }
9010
+ const a = this.val;
9011
+ const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3];
9012
+ const a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7];
9013
+ const a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11];
9014
+ const a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
9015
+ a[0] = c0 * a00 + c1 * a10 + c2 * a20 + c3 * a30;
9016
+ a[1] = c0 * a01 + c1 * a11 + c2 * a21 + c3 * a31;
9017
+ a[2] = c0 * a02 + c1 * a12 + c2 * a22 + c3 * a32;
9018
+ a[3] = c0 * a03 + c1 * a13 + c2 * a23 + c3 * a33;
9019
+ a[4] = c4 * a00 + c5 * a10 + c6 * a20 + c7 * a30;
9020
+ a[5] = c4 * a01 + c5 * a11 + c6 * a21 + c7 * a31;
9021
+ a[6] = c4 * a02 + c5 * a12 + c6 * a22 + c7 * a32;
9022
+ a[7] = c4 * a03 + c5 * a13 + c6 * a23 + c7 * a33;
9023
+ a[8] = c8 * a00 + c9 * a10 + c10 * a20 + c11 * a30;
9024
+ a[9] = c8 * a01 + c9 * a11 + c10 * a21 + c11 * a31;
9025
+ a[10] = c8 * a02 + c9 * a12 + c10 * a22 + c11 * a32;
9026
+ a[11] = c8 * a03 + c9 * a13 + c10 * a23 + c11 * a33;
9027
+ a[12] = c12 * a00 + c13 * a10 + c14 * a20 + c15 * a30;
9028
+ a[13] = c12 * a01 + c13 * a11 + c14 * a21 + c15 * a31;
9029
+ a[14] = c12 * a02 + c13 * a12 + c14 * a22 + c15 * a32;
9030
+ a[15] = c12 * a03 + c13 * a13 + c14 * a23 + c15 * a33;
9031
+ return this;
9032
+ }
8989
9033
  /**
8990
9034
  * Transpose the value of this matrix.
8991
9035
  * @returns Reference to this object for method chaining
@@ -9436,12 +9480,12 @@ var Matrix2d = class {
9436
9480
  }
9437
9481
  /**
9438
9482
  * Multiplies the current transformation with the matrix described by the arguments of this method.
9439
- * @param a a component
9440
- * @param b b component
9441
- * @param c c component
9442
- * @param d d component
9443
- * @param e e component
9444
- * @param f f component
9483
+ * @param a - a component (scale x / cos)
9484
+ * @param b - b component (skew y / sin)
9485
+ * @param c - c component (skew x / -sin)
9486
+ * @param d - d component (scale y / cos)
9487
+ * @param e - e component (translate x)
9488
+ * @param f - f component (translate y)
9445
9489
  * @returns Reference to this object for method chaining
9446
9490
  */
9447
9491
  transform(a, b, c, d, e, f) {
@@ -10244,6 +10288,238 @@ function unloadAll() {
10244
10288
  }
10245
10289
  }
10246
10290
 
10291
+ // src/camera/effects/camera_effect.ts
10292
+ var CameraEffect = class {
10293
+ /**
10294
+ * the camera this effect is attached to
10295
+ */
10296
+ camera;
10297
+ /**
10298
+ * whether this effect has finished and should be removed
10299
+ */
10300
+ isComplete;
10301
+ /**
10302
+ * whether this effect should persist across camera/game resets
10303
+ * (e.g. transition effects that span state changes)
10304
+ * @default false
10305
+ */
10306
+ isPersistent;
10307
+ /**
10308
+ * @param camera - the camera this effect is attached to
10309
+ */
10310
+ constructor(camera) {
10311
+ this.camera = camera;
10312
+ this.isComplete = false;
10313
+ this.isPersistent = false;
10314
+ }
10315
+ /**
10316
+ * Called each frame to update the effect state (e.g. modify camera offset, countdown duration).
10317
+ * @param _dt - time elapsed since last frame in milliseconds
10318
+ */
10319
+ update(_dt) {
10320
+ }
10321
+ // eslint-disable-line @typescript-eslint/no-unused-vars
10322
+ /**
10323
+ * Called after the scene renders to draw visual overlays (e.g. color fills for fading).
10324
+ * @param _renderer - the renderer to draw with
10325
+ * @param _width - the camera viewport width
10326
+ * @param _height - the camera viewport height
10327
+ */
10328
+ draw(_renderer2, _width, _height) {
10329
+ }
10330
+ // eslint-disable-line @typescript-eslint/no-unused-vars
10331
+ /**
10332
+ * Called when the effect is removed from the camera. Override to clean up resources.
10333
+ */
10334
+ destroy() {
10335
+ }
10336
+ };
10337
+
10338
+ // src/camera/effects/fade_effect.ts
10339
+ var FadeEffect = class extends CameraEffect {
10340
+ /**
10341
+ * the overlay color
10342
+ */
10343
+ color;
10344
+ /**
10345
+ * the tween controlling alpha transition
10346
+ */
10347
+ tween;
10348
+ /**
10349
+ * fade direction: "in" fades to the color, "out" fades from the color back to transparent
10350
+ */
10351
+ direction;
10352
+ /**
10353
+ * target alpha value for completion check
10354
+ * @ignore
10355
+ */
10356
+ _targetAlpha;
10357
+ /**
10358
+ * @param camera - the camera to apply the fade to
10359
+ * @param options - fade parameters
10360
+ * @param options.color - CSS color value or Color instance
10361
+ * @param [options.duration=1000] - fade duration in milliseconds
10362
+ * @param [options.direction="in"] - "in" fades to color, "out" fades from color to transparent
10363
+ * @param [options.onComplete] - callback when the fade finishes
10364
+ */
10365
+ constructor(camera, options) {
10366
+ super(camera);
10367
+ const duration = options.duration ?? 1e3;
10368
+ this.direction = options.direction ?? "in";
10369
+ this.color = colorPool.get(options.color);
10370
+ if (this.direction === "in") {
10371
+ this._targetAlpha = this.color.alpha;
10372
+ this.color.alpha = 0;
10373
+ this.tween = tweenPool.get(this.color).to({ alpha: this._targetAlpha }, { duration });
10374
+ } else {
10375
+ this._targetAlpha = 0;
10376
+ this.tween = tweenPool.get(this.color).to({ alpha: 0 }, { duration });
10377
+ }
10378
+ if (options.onComplete) {
10379
+ this.tween.onComplete(options.onComplete);
10380
+ }
10381
+ this.tween.isPersistent = true;
10382
+ this.tween.start();
10383
+ }
10384
+ draw(renderer2, width, height) {
10385
+ if (this.color.alpha === 0) {
10386
+ return;
10387
+ }
10388
+ const r = renderer2;
10389
+ r.save();
10390
+ r.resetTransform();
10391
+ r.setColor(this.color);
10392
+ r.fillRect(0, 0, width, height);
10393
+ r.restore();
10394
+ }
10395
+ update() {
10396
+ if (this.direction === "in" && this.color.alpha >= this._targetAlpha) {
10397
+ this.isComplete = true;
10398
+ } else if (this.direction === "out" && this.color.alpha <= 0) {
10399
+ this.isComplete = true;
10400
+ }
10401
+ }
10402
+ destroy() {
10403
+ this.tween.stop();
10404
+ tweenPool.release(this.tween);
10405
+ colorPool.release(this.color);
10406
+ }
10407
+ };
10408
+
10409
+ // src/camera/effects/mask_effect.ts
10410
+ var MaskEffect = class extends CameraEffect {
10411
+ /**
10412
+ * the transition fill color
10413
+ */
10414
+ color;
10415
+ /**
10416
+ * the mask shape template (unit-sized, centered at origin)
10417
+ */
10418
+ shape;
10419
+ /**
10420
+ * current progress value (0 = fully covered, 1 = fully visible)
10421
+ */
10422
+ progress;
10423
+ /**
10424
+ * the tween controlling progress
10425
+ */
10426
+ tween;
10427
+ /**
10428
+ * transition direction
10429
+ */
10430
+ direction;
10431
+ /**
10432
+ * optional callback when transition completes
10433
+ */
10434
+ onComplete;
10435
+ /**
10436
+ * pooled shape used for rendering (avoids per-frame allocation)
10437
+ * @ignore
10438
+ */
10439
+ _maskShape;
10440
+ /**
10441
+ * @param camera - the camera to apply the transition to
10442
+ * @param options - transition parameters
10443
+ * @param options.shape - an Ellipse or Polygon (unit-sized, centered at origin) defining the mask shape
10444
+ * @param options.color - CSS color value or Color instance for the transition fill
10445
+ * @param [options.duration=500] - transition duration in milliseconds
10446
+ * @param [options.direction="hide"] - "hide" shrinks the visible area, "reveal" grows it
10447
+ * @param [options.onComplete] - callback when the transition finishes
10448
+ */
10449
+ constructor(camera, options) {
10450
+ super(camera);
10451
+ this.color = colorPool.get(options.color);
10452
+ this.direction = options.direction ?? "hide";
10453
+ this.onComplete = options.onComplete;
10454
+ this.shape = options.shape;
10455
+ this._maskShape = this.shape.clone();
10456
+ const duration = options.duration ?? 500;
10457
+ if (this.direction === "hide") {
10458
+ this.progress = { value: 1 };
10459
+ this.tween = tweenPool.get(this.progress).to({ value: 0 }, { duration });
10460
+ } else {
10461
+ this.progress = { value: 0 };
10462
+ this.tween = tweenPool.get(this.progress).to({ value: 1 }, { duration });
10463
+ }
10464
+ this.tween.isPersistent = true;
10465
+ this.tween.start();
10466
+ }
10467
+ update() {
10468
+ if (this.direction === "hide" && this.progress.value <= 0 || this.direction === "reveal" && this.progress.value >= 1) {
10469
+ this.progress.value = this.direction === "hide" ? 0 : 1;
10470
+ this.isComplete = true;
10471
+ if (typeof this.onComplete === "function") {
10472
+ this.onComplete();
10473
+ }
10474
+ return;
10475
+ }
10476
+ const width = this.camera.width;
10477
+ const height = this.camera.height;
10478
+ const maxRadius = Math.sqrt(width * width + height * height) / 2;
10479
+ const scale2 = this.progress.value * maxRadius;
10480
+ const cx = width / 2;
10481
+ const cy = height / 2;
10482
+ if (this._maskShape.type === "Ellipse") {
10483
+ this._maskShape.setShape(cx, cy, scale2 * 2, scale2 * 2);
10484
+ } else {
10485
+ const mask = this._maskShape;
10486
+ const srcPoints = this.shape.points;
10487
+ for (let i = 0; i < srcPoints.length; i++) {
10488
+ mask.points[i].set(srcPoints[i].x * scale2, srcPoints[i].y * scale2);
10489
+ }
10490
+ mask.pos.set(cx, cy);
10491
+ }
10492
+ }
10493
+ draw(renderer2, width, height) {
10494
+ if (this.isComplete && this.direction === "reveal") {
10495
+ return;
10496
+ }
10497
+ const r = renderer2;
10498
+ r.save();
10499
+ r.resetTransform();
10500
+ if (this.progress.value <= 0) {
10501
+ r.setColor(this.color);
10502
+ r.fillRect(0, 0, width, height);
10503
+ } else {
10504
+ r.setMask(this._maskShape, true);
10505
+ r.setColor(this.color);
10506
+ r.fillRect(0, 0, width, height);
10507
+ r.clearMask();
10508
+ }
10509
+ r.restore();
10510
+ }
10511
+ destroy() {
10512
+ this.tween.stop();
10513
+ tweenPool.release(this.tween);
10514
+ colorPool.release(this.color);
10515
+ if (this._maskShape.type === "Ellipse") {
10516
+ ellipsePool.release(this._maskShape);
10517
+ } else {
10518
+ polygonPool.release(this._maskShape);
10519
+ }
10520
+ }
10521
+ };
10522
+
10247
10523
  // src/geometries/observablePoint.ts
10248
10524
  var ObservablePoint = class _ObservablePoint {
10249
10525
  _callback;
@@ -11461,7 +11737,8 @@ var Renderable = class _Renderable extends Rect {
11461
11737
  this.alpha = 1;
11462
11738
  this.ancestor = void 0;
11463
11739
  this.mask = void 0;
11464
- this.shader = void 0;
11740
+ this.postEffects = [];
11741
+ this._postEffectManaged = false;
11465
11742
  this.blendMode = "normal";
11466
11743
  this.name = "";
11467
11744
  this.isRenderable = true;
@@ -11539,6 +11816,82 @@ var Renderable = class _Renderable extends Rect {
11539
11816
  }
11540
11817
  }
11541
11818
  }
11819
+ /**
11820
+ * @deprecated since 19.2.0 — use {@link addPostEffect} / {@link getPostEffect} / {@link removePostEffect} instead
11821
+ * @type {GLShader|ShaderEffect|undefined}
11822
+ */
11823
+ get shader() {
11824
+ return this.postEffects[0];
11825
+ }
11826
+ set shader(value) {
11827
+ for (const effect of this.postEffects) {
11828
+ if (typeof effect.destroy === "function") {
11829
+ effect.destroy();
11830
+ }
11831
+ }
11832
+ if (typeof value === "undefined") {
11833
+ this.postEffects.length = 0;
11834
+ } else {
11835
+ this.postEffects = [value];
11836
+ }
11837
+ }
11838
+ /**
11839
+ * Add a post-processing shader effect to this renderable.
11840
+ * @param {GLShader|ShaderEffect} effect - the effect to add
11841
+ * @returns {GLShader|ShaderEffect} the added effect
11842
+ * @example
11843
+ * mySprite.addPostEffect(new DesaturateEffect(renderer));
11844
+ */
11845
+ addPostEffect(effect) {
11846
+ this.postEffects.push(effect);
11847
+ return effect;
11848
+ }
11849
+ /**
11850
+ * Get post-processing shader effects.
11851
+ * When called with a class, returns the first effect matching the given class.
11852
+ * When called without arguments, returns the full effects array.
11853
+ * @param {Function} [effectClass] - the effect class to search for
11854
+ * @returns {GLShader|ShaderEffect|Array|undefined} the matching effect, the effects array, or undefined
11855
+ * @example
11856
+ * const desat = sprite.getPostEffect(DesaturateEffect);
11857
+ * const allEffects = sprite.getPostEffect();
11858
+ */
11859
+ getPostEffect(effectClass) {
11860
+ if (typeof effectClass === "undefined") {
11861
+ return this.postEffects;
11862
+ }
11863
+ return this.postEffects.find((fx) => {
11864
+ return fx instanceof effectClass;
11865
+ });
11866
+ }
11867
+ /**
11868
+ * Remove all post-processing shader effects.
11869
+ * @example
11870
+ * sprite.clearPostEffects();
11871
+ */
11872
+ clearPostEffects() {
11873
+ for (const effect of this.postEffects) {
11874
+ if (typeof effect.destroy === "function") {
11875
+ effect.destroy();
11876
+ }
11877
+ }
11878
+ this.postEffects.length = 0;
11879
+ }
11880
+ /**
11881
+ * Remove a specific post-processing shader effect.
11882
+ * @param {GLShader|ShaderEffect} effect - the effect to remove
11883
+ * @example
11884
+ * sprite.removePostEffect(effect);
11885
+ */
11886
+ removePostEffect(effect) {
11887
+ const idx = this.postEffects.indexOf(effect);
11888
+ if (idx !== -1) {
11889
+ this.postEffects.splice(idx, 1);
11890
+ if (typeof effect.destroy === "function") {
11891
+ effect.destroy();
11892
+ }
11893
+ }
11894
+ }
11542
11895
  /**
11543
11896
  * returns true if this renderable is flipped on the horizontal axis
11544
11897
  * @public
@@ -11820,8 +12173,8 @@ var Renderable = class _Renderable extends Rect {
11820
12173
  renderer2.setMask(this.mask);
11821
12174
  renderer2.translate(-this.pos.x, -this.pos.y);
11822
12175
  }
11823
- if (this.shader) {
11824
- renderer2.customShader = this.shader;
12176
+ if (!this._postEffectManaged) {
12177
+ renderer2.beginPostEffect(this);
11825
12178
  }
11826
12179
  if (this.autoTransform === true && !this.currentTransform.isIdentity()) {
11827
12180
  renderer2.translate(this.pos.x, this.pos.y);
@@ -11861,8 +12214,8 @@ var Renderable = class _Renderable extends Rect {
11861
12214
  if (this.mask) {
11862
12215
  renderer2.clearMask();
11863
12216
  }
11864
- if (this.shader) {
11865
- renderer2.customShader = void 0;
12217
+ if (!this._postEffectManaged) {
12218
+ renderer2.endPostEffect(this);
11866
12219
  }
11867
12220
  renderer2.restore();
11868
12221
  this.isDirty = false;
@@ -11924,10 +12277,12 @@ var Renderable = class _Renderable extends Rect {
11924
12277
  }
11925
12278
  releaseAllPointerEvents(this);
11926
12279
  this.onDestroyEvent.apply(this, arguments);
11927
- if (this.shader && typeof this.shader.destroy === "function") {
11928
- this.shader.destroy();
11929
- this.shader = void 0;
12280
+ for (const effect of this.postEffects) {
12281
+ if (typeof effect.destroy === "function") {
12282
+ effect.destroy();
12283
+ }
11930
12284
  }
12285
+ this.postEffects.length = 0;
11931
12286
  }
11932
12287
  /**
11933
12288
  * OnDestroy Notification function<br>
@@ -11966,6 +12321,29 @@ __export(loader_exports, {
11966
12321
  withCredentials: () => withCredentials
11967
12322
  });
11968
12323
 
12324
+ // src/lang/console.js
12325
+ function warning(deprecated, replacement, version2) {
12326
+ const msg = "melonJS: %s is deprecated since version %s, please use %s";
12327
+ const stack = new Error().stack;
12328
+ if (console.groupCollapsed) {
12329
+ console.groupCollapsed(
12330
+ "%c" + msg,
12331
+ "font-weight:normal;color:yellow;",
12332
+ deprecated,
12333
+ version2,
12334
+ replacement
12335
+ );
12336
+ } else {
12337
+ console.warn(msg, deprecated, version2, replacement);
12338
+ }
12339
+ if (typeof stack !== "undefined") {
12340
+ console.warn(stack);
12341
+ }
12342
+ if (console.groupCollapsed) {
12343
+ console.groupEnd();
12344
+ }
12345
+ }
12346
+
11969
12347
  // src/utils/file.ts
11970
12348
  var file_exports = {};
11971
12349
  __export(file_exports, {
@@ -13805,6 +14183,97 @@ var TMX_FLIP_AD = 536870912;
13805
14183
  var TMX_CLEAR_BIT_MASK = ~(TMX_FLIP_H | TMX_FLIP_V | TMX_FLIP_AD);
13806
14184
  var COLLISION_GROUP = "collision";
13807
14185
 
14186
+ // src/video/rendertarget/rendertarget.ts
14187
+ var RenderTarget = class {
14188
+ /**
14189
+ * Bind this render target as the active draw destination.
14190
+ * All subsequent draw calls will render into this target until {@link unbind} is called.
14191
+ * No-op by default — subclasses override for GPU render targets (e.g. WebGL FBOs).
14192
+ */
14193
+ bind() {
14194
+ }
14195
+ /**
14196
+ * Unbind this render target, restoring the default (screen) output.
14197
+ * No-op by default — subclasses override for GPU render targets.
14198
+ */
14199
+ unbind() {
14200
+ }
14201
+ /**
14202
+ * Creates a Blob object representing the image contained in this render target.
14203
+ * @param type - a string indicating the image format (default "image/png")
14204
+ * @param quality - a number between 0 and 1 for lossy formats (e.g. image/jpeg)
14205
+ * @returns a Promise resolving to a Blob
14206
+ */
14207
+ toBlob(type = "image/png", quality) {
14208
+ const imageData = this.getImageData();
14209
+ if (typeof OffscreenCanvas !== "undefined") {
14210
+ const canvas2 = new OffscreenCanvas(this.width, this.height);
14211
+ const ctx2 = canvas2.getContext("2d");
14212
+ if (!ctx2) {
14213
+ return Promise.reject(new Error("Failed to get 2d context"));
14214
+ }
14215
+ ctx2.putImageData(imageData, 0, 0);
14216
+ const options = { type };
14217
+ if (typeof quality !== "undefined") {
14218
+ options.quality = quality;
14219
+ }
14220
+ return canvas2.convertToBlob(options);
14221
+ }
14222
+ const canvas = document.createElement("canvas");
14223
+ canvas.width = this.width;
14224
+ canvas.height = this.height;
14225
+ const ctx = canvas.getContext("2d");
14226
+ if (!ctx) {
14227
+ return Promise.reject(new Error("Failed to get 2d context"));
14228
+ }
14229
+ ctx.putImageData(imageData, 0, 0);
14230
+ return new Promise((resolve, reject) => {
14231
+ canvas.toBlob(
14232
+ (blob) => {
14233
+ if (blob) {
14234
+ resolve(blob);
14235
+ } else {
14236
+ reject(new Error(`toBlob failed for type "${type}"`));
14237
+ }
14238
+ },
14239
+ type,
14240
+ quality
14241
+ );
14242
+ });
14243
+ }
14244
+ /**
14245
+ * Creates an ImageBitmap object from the current contents of this render target.
14246
+ * @returns a Promise resolving to an ImageBitmap
14247
+ */
14248
+ toImageBitmap() {
14249
+ const imageData = this.getImageData();
14250
+ return globalThis.createImageBitmap(imageData);
14251
+ }
14252
+ /**
14253
+ * Returns a data URL containing a representation of the current contents.
14254
+ * @param type - a string indicating the image format (default "image/png")
14255
+ * @param quality - a number between 0 and 1 for lossy formats (e.g. image/jpeg)
14256
+ * @returns a Promise resolving to a data URL string
14257
+ */
14258
+ toDataURL(type = "image/png", quality) {
14259
+ return this.toBlob(type, quality).then((blob) => {
14260
+ const reader = new FileReader();
14261
+ return new Promise((resolve, reject) => {
14262
+ reader.onload = () => {
14263
+ resolve(reader.result);
14264
+ };
14265
+ reader.onerror = () => {
14266
+ reject(new Error(reader.error?.message ?? "FileReader failed"));
14267
+ };
14268
+ reader.onabort = () => {
14269
+ reject(new Error("FileReader aborted"));
14270
+ };
14271
+ reader.readAsDataURL(blob);
14272
+ });
14273
+ });
14274
+ }
14275
+ };
14276
+
13808
14277
  // src/video/rendertarget/canvasrendertarget.js
13809
14278
  var defaultAttributes = {
13810
14279
  offscreenCanvas: false,
@@ -13855,7 +14324,7 @@ function createContext(canvas, attributes) {
13855
14324
  }
13856
14325
  return context;
13857
14326
  }
13858
- var CanvasRenderTarget = class {
14327
+ var CanvasRenderTarget = class extends RenderTarget {
13859
14328
  /**
13860
14329
  * @param {number} width - the desired width of the canvas
13861
14330
  * @param {number} height - the desired height of the canvas
@@ -13868,6 +14337,7 @@ var CanvasRenderTarget = class {
13868
14337
  * @param {boolean} [attributes.antiAlias=false] - Whether to enable anti-aliasing, use false (default) for a pixelated effect.
13869
14338
  */
13870
14339
  constructor(width, height, attributes = defaultAttributes) {
14340
+ super();
13871
14341
  this.attributes = Object.assign({}, defaultAttributes, attributes);
13872
14342
  if (typeof attributes.context === "undefined") {
13873
14343
  attributes.context = "2d";
@@ -13947,64 +14417,48 @@ var CanvasRenderTarget = class {
13947
14417
  return this.context.getImageData(x, y, width, height);
13948
14418
  }
13949
14419
  /**
13950
- * creates a Blob object representing the image contained in this canvas texture
14420
+ * Returns a data URL directly from the canvas (avoids the getImageData/Blob round-trip).
14421
+ * Note: not supported by OffscreenCanvas — falls back to the base implementation.
13951
14422
  * @param {string} [type="image/png"] - A string indicating the image format
13952
- * @param {number} [quality] - A Number between 0 and 1 indicating the image quality to be used when creating images using file formats that support lossy compression (such as image/jpeg or image/webp). A user agent will use its default quality value if this option is not specified, or if the number is outside the allowed range.
13953
- * @returns {Promise} A Promise returning a Blob object representing the image contained in this canvas texture
13954
- * @example
13955
- * renderTarget.convertToBlob().then((blob) => console.log(blob));
14423
+ * @param {number} [quality] - A number between 0 and 1 for lossy formats (image/jpeg, image/webp)
14424
+ * @returns {Promise<string>} A Promise resolving to a data URL string
13956
14425
  */
13957
- toBlob(type = "image/png", quality) {
13958
- if (typeof this.canvas.convertToBlob === "function") {
13959
- return this.canvas.convertToBlob({ type, quality });
13960
- } else {
13961
- return new Promise((resolve) => {
13962
- this.canvas.toBlob(
13963
- (blob) => {
13964
- resolve(blob);
13965
- },
13966
- type,
13967
- quality
13968
- );
13969
- });
14426
+ toDataURL(type = "image/png", quality) {
14427
+ if (typeof this.canvas.toDataURL === "function") {
14428
+ return Promise.resolve(this.canvas.toDataURL(type, quality));
13970
14429
  }
14430
+ return super.toDataURL(type, quality);
13971
14431
  }
13972
14432
  /**
13973
- * creates an ImageBitmap object from the most recently rendered image of this canvas texture
14433
+ * Creates a Blob directly from the canvas (avoids the getImageData round-trip).
13974
14434
  * @param {string} [type="image/png"] - A string indicating the image format
13975
- * @param {number} [quality] - A Number between 0 and 1 indicating the image quality to be used when creating images using file formats that support lossy compression (such as image/jpeg or image/webp). A user agent will use its default quality value if this option is not specified, or if the number is outside the allowed range.
13976
- * @returns {Promise} A Promise returning an ImageBitmap.
13977
- * @example
13978
- * renderTarget.transferToImageBitmap().then((bitmap) => console.log(bitmap));
14435
+ * @param {number} [quality] - A number between 0 and 1 for lossy formats (image/jpeg, image/webp)
14436
+ * @returns {Promise<Blob>} A Promise resolving to a Blob
13979
14437
  */
13980
- toImageBitmap(type = "image/png", quality) {
13981
- return new Promise((resolve) => {
13982
- if (typeof this.canvas.transferToImageBitmap === "function") {
13983
- resolve(this.canvas.transferToImageBitmap());
13984
- } else {
13985
- const image = new Image();
13986
- image.src = this.canvas.toDataURL(type, quality);
13987
- image.onload = () => {
13988
- globalThis.createImageBitmap(image).then((bitmap) => {
13989
- return resolve(bitmap);
13990
- });
13991
- };
13992
- }
14438
+ toBlob(type = "image/png", quality) {
14439
+ if (typeof this.canvas.convertToBlob === "function") {
14440
+ return this.canvas.convertToBlob({ type, quality });
14441
+ }
14442
+ return new Promise((resolve, reject) => {
14443
+ this.canvas.toBlob(
14444
+ (blob) => {
14445
+ if (blob) {
14446
+ resolve(blob);
14447
+ } else {
14448
+ reject(new Error(`toBlob failed for type "${type}"`));
14449
+ }
14450
+ },
14451
+ type,
14452
+ quality
14453
+ );
13993
14454
  });
13994
14455
  }
13995
14456
  /**
13996
- * returns a data URL containing a representation of the most recently rendered image of this canvas texture
13997
- * (not supported by OffscreenCanvas)
13998
- * @param {string} [type="image/png"] - A string indicating the image format
13999
- * @param {number} [quality] - A Number between 0 and 1 indicating the image quality to be used when creating images using file formats that support lossy compression (such as image/jpeg or image/webp). A user agent will use its default quality value if this option is not specified, or if the number is outside the allowed range.
14000
- * @returns {Promise} A Promise returning a string containing the requested data URL.
14001
- * @example
14002
- * renderer.toDataURL().then((dataURL) => console.log(dataURL));
14457
+ * Creates an ImageBitmap directly from the canvas (avoids the getImageData round-trip).
14458
+ * @returns {Promise<ImageBitmap>} A Promise resolving to an ImageBitmap
14003
14459
  */
14004
- toDataURL(type = "image/png", quality) {
14005
- return new Promise((resolve) => {
14006
- resolve(this.canvas.toDataURL(type, quality));
14007
- });
14460
+ toImageBitmap() {
14461
+ return globalThis.createImageBitmap(this.canvas);
14008
14462
  }
14009
14463
  /**
14010
14464
  * invalidate the current CanvasRenderTarget, and force a reupload of the corresponding texture
@@ -14867,6 +15321,7 @@ var RenderState = class {
14867
15321
  this.currentGradient = null;
14868
15322
  this.lineDash = [];
14869
15323
  this.currentBlendMode = "none";
15324
+ this.currentShader = void 0;
14870
15325
  this._stackCapacity = 32;
14871
15326
  this._stackDepth = 0;
14872
15327
  this._colorStack = Array.from({ length: this._stackCapacity }, () => {
@@ -14885,6 +15340,7 @@ var RenderState = class {
14885
15340
  this._scissorActive = new Uint8Array(this._stackCapacity);
14886
15341
  this._gradientStack = new Array(this._stackCapacity);
14887
15342
  this._blendStack = new Array(this._stackCapacity);
15343
+ this._shaderStack = new Array(this._stackCapacity);
14888
15344
  }
14889
15345
  /**
14890
15346
  * Save the current state onto the stack (zero allocations).
@@ -14901,6 +15357,7 @@ var RenderState = class {
14901
15357
  this._gradientStack[depth] = this.currentGradient;
14902
15358
  this._lineDashStack[depth] = this.lineDash;
14903
15359
  this._blendStack[depth] = this.currentBlendMode;
15360
+ this._shaderStack[depth] = this.currentShader;
14904
15361
  if (scissorTestActive) {
14905
15362
  this._scissorStack[depth].set(this.currentScissor);
14906
15363
  this._scissorActive[depth] = 1;
@@ -14926,6 +15383,7 @@ var RenderState = class {
14926
15383
  this.currentTransform.copy(this._matrixStack[depth]);
14927
15384
  this.currentGradient = this._gradientStack[depth];
14928
15385
  this.lineDash = this._lineDashStack[depth];
15386
+ this.currentShader = this._shaderStack[depth];
14929
15387
  const scissorActive = !!this._scissorActive[depth];
14930
15388
  if (scissorActive) {
14931
15389
  this.currentScissor.set(this._scissorStack[depth]);
@@ -14951,6 +15409,7 @@ var RenderState = class {
14951
15409
  this.currentScissor[1] = 0;
14952
15410
  this.currentScissor[2] = width;
14953
15411
  this.currentScissor[3] = height;
15412
+ this.currentShader = void 0;
14954
15413
  }
14955
15414
  /** @private — doubles stack capacity when exceeded */
14956
15415
  _growStacks() {
@@ -14964,6 +15423,7 @@ var RenderState = class {
14964
15423
  this._gradientStack.push(null);
14965
15424
  this._lineDashStack.push([]);
14966
15425
  this._blendStack.push(void 0);
15426
+ this._shaderStack.push(void 0);
14967
15427
  }
14968
15428
  const newScissorActive = new Uint8Array(newCap);
14969
15429
  newScissorActive.set(this._scissorActive);
@@ -14990,6 +15450,7 @@ var Renderer = class {
14990
15450
  this.isContextValid = true;
14991
15451
  this.GPURenderer = void 0;
14992
15452
  this.customShader = void 0;
15453
+ this._renderTargetPool = null;
14993
15454
  this.path2D = new path2d_default();
14994
15455
  this.type = "Generic";
14995
15456
  this.backgroundColor = new Color(
@@ -15120,6 +15581,95 @@ var Renderer = class {
15120
15581
  }
15121
15582
  return false;
15122
15583
  }
15584
+ /**
15585
+ * Begin capturing rendering to an offscreen buffer for post-effect processing.
15586
+ * Call endPostEffect() after rendering to blit the result to the screen.
15587
+ * No-op on Canvas renderer.
15588
+ * @param {Renderable} renderable - the renderable with postEffects to apply
15589
+ * @returns {boolean} false (Canvas renderer does not support post-effect processing)
15590
+ * @ignore
15591
+ */
15592
+ beginPostEffect(renderable) {
15593
+ const effects = renderable.postEffects.filter((fx) => {
15594
+ return fx.enabled !== false;
15595
+ });
15596
+ if (effects.length === 1) {
15597
+ this.customShader = effects[0];
15598
+ } else {
15599
+ this.customShader = void 0;
15600
+ }
15601
+ return false;
15602
+ }
15603
+ /**
15604
+ * End post-effect capture and blit the offscreen buffer to the screen
15605
+ * through the renderable's post-effects.
15606
+ * No-op on Canvas renderer.
15607
+ * @param {Renderable} renderable - the renderable with postEffects to apply
15608
+ * @ignore
15609
+ */
15610
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
15611
+ endPostEffect(renderable) {
15612
+ }
15613
+ /**
15614
+ * Blit a texture to the screen through a shader effect.
15615
+ * Draws a screen-aligned quad using the given texture as source
15616
+ * and the given shader for post-processing (e.g. scanlines, desaturation).
15617
+ * No-op on Canvas renderer.
15618
+ * @param {WebGLTexture} source - the source texture to blit
15619
+ * @param {number} x - destination x position
15620
+ * @param {number} y - destination y position
15621
+ * @param {number} width - destination width
15622
+ * @param {number} height - destination height
15623
+ * @param {ShaderEffect} shader - the shader effect to apply
15624
+ * @param {boolean} [keepBlend=false] - if true, keep current blend mode (for sprite compositing)
15625
+ */
15626
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
15627
+ blitEffect(source, x, y, width, height, shader, keepBlend) {
15628
+ }
15629
+ /**
15630
+ * Sets the viewport for the renderer.
15631
+ * Defines the affine transformation from normalized device coordinates to window coordinates.
15632
+ * No-op on Canvas renderer.
15633
+ * @param {number} [x=0] - x coordinate of the viewport origin
15634
+ * @param {number} [y=0] - y coordinate of the viewport origin
15635
+ * @param {number} [w] - width of the viewport (defaults to canvas width)
15636
+ * @param {number} [h] - height of the viewport (defaults to canvas height)
15637
+ */
15638
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
15639
+ setViewport(x = 0, y = 0, w, h) {
15640
+ }
15641
+ /**
15642
+ * Clear the current render target to transparent black (color + stencil).
15643
+ * Used to prepare FBOs for post-effect capture.
15644
+ * No-op on Canvas renderer.
15645
+ */
15646
+ clearRenderTarget() {
15647
+ }
15648
+ /**
15649
+ * Enable the scissor test with the given rectangle.
15650
+ * No-op on Canvas renderer.
15651
+ * @param {number} x - x coordinate of the scissor rectangle
15652
+ * @param {number} y - y coordinate of the scissor rectangle
15653
+ * @param {number} width - width of the scissor rectangle
15654
+ * @param {number} height - height of the scissor rectangle
15655
+ */
15656
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
15657
+ enableScissor(x, y, width, height) {
15658
+ }
15659
+ /**
15660
+ * Disable the scissor test, allowing rendering to the full viewport.
15661
+ * No-op on Canvas renderer.
15662
+ */
15663
+ disableScissor() {
15664
+ }
15665
+ /**
15666
+ * Enable or disable alpha blending.
15667
+ * No-op on Canvas renderer (Canvas always blends).
15668
+ * @param {boolean} enable - true to enable blending, false to disable
15669
+ */
15670
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
15671
+ setBlendEnabled(enable2) {
15672
+ }
15123
15673
  /**
15124
15674
  * returns the current blend mode for this renderer
15125
15675
  * @returns {string} blend mode
@@ -17014,6 +17564,7 @@ var CanvasRenderer = class extends Renderer {
17014
17564
  if (result !== null) {
17015
17565
  this.setBlendMode(result.blendMode);
17016
17566
  }
17567
+ this.customShader = this.renderState.currentShader;
17017
17568
  if (typeof context.fillStyle === "string") {
17018
17569
  this.currentColor.copy(context.fillStyle);
17019
17570
  }
@@ -17038,6 +17589,7 @@ var CanvasRenderer = class extends Renderer {
17038
17589
  */
17039
17590
  save() {
17040
17591
  this.getContext().save();
17592
+ this.renderState.currentShader = this.customShader;
17041
17593
  this.renderState.save();
17042
17594
  }
17043
17595
  /**
@@ -17285,9 +17837,8 @@ var CanvasRenderer = class extends Renderer {
17285
17837
  }
17286
17838
  this.maskLevel++;
17287
17839
  if (invert === true) {
17288
- context.closePath();
17289
- context.globalCompositeOperation = "destination-atop";
17290
- context.fill();
17840
+ context.rect(0, 0, this.getCanvas().width, this.getCanvas().height);
17841
+ context.clip("evenodd");
17291
17842
  } else {
17292
17843
  context.clip();
17293
17844
  }
@@ -21368,6 +21919,723 @@ var Sprite = class extends Renderable {
21368
21919
  }
21369
21920
  };
21370
21921
 
21922
+ // src/math/color_matrix.ts
21923
+ var ColorMatrix = class extends Matrix3d {
21924
+ /**
21925
+ * Apply a brightness adjustment.
21926
+ * @param amount - brightness multiplier (1.0 = normal, >1 brighter, <1 darker)
21927
+ * @returns this instance for chaining
21928
+ */
21929
+ brightness(amount) {
21930
+ return this.transform(
21931
+ amount,
21932
+ 0,
21933
+ 0,
21934
+ 0,
21935
+ 0,
21936
+ amount,
21937
+ 0,
21938
+ 0,
21939
+ 0,
21940
+ 0,
21941
+ amount,
21942
+ 0,
21943
+ 0,
21944
+ 0,
21945
+ 0,
21946
+ 1
21947
+ );
21948
+ }
21949
+ /**
21950
+ * Apply a contrast adjustment.
21951
+ * @param amount - contrast multiplier (1.0 = normal, >1 more contrast, <1 less)
21952
+ * @returns this instance for chaining
21953
+ */
21954
+ contrast(amount) {
21955
+ const o = (1 - amount) / 2;
21956
+ return this.transform(
21957
+ amount,
21958
+ 0,
21959
+ 0,
21960
+ 0,
21961
+ 0,
21962
+ amount,
21963
+ 0,
21964
+ 0,
21965
+ 0,
21966
+ 0,
21967
+ amount,
21968
+ 0,
21969
+ o,
21970
+ o,
21971
+ o,
21972
+ 1
21973
+ );
21974
+ }
21975
+ /**
21976
+ * Apply a saturation adjustment.
21977
+ * @param amount - saturation level (0.0 = grayscale, 1.0 = normal, >1 over-saturated)
21978
+ * @returns this instance for chaining
21979
+ */
21980
+ saturate(amount) {
21981
+ const ir = 0.299 * (1 - amount);
21982
+ const ig = 0.587 * (1 - amount);
21983
+ const ib = 0.114 * (1 - amount);
21984
+ return this.transform(
21985
+ ir + amount,
21986
+ ir,
21987
+ ir,
21988
+ 0,
21989
+ ig,
21990
+ ig + amount,
21991
+ ig,
21992
+ 0,
21993
+ ib,
21994
+ ib,
21995
+ ib + amount,
21996
+ 0,
21997
+ 0,
21998
+ 0,
21999
+ 0,
22000
+ 1
22001
+ );
22002
+ }
22003
+ /**
22004
+ * Apply a hue rotation.
22005
+ * @param angle - rotation angle in radians
22006
+ * @returns this instance for chaining
22007
+ */
22008
+ hueRotate(angle) {
22009
+ const cos = Math.cos(angle);
22010
+ const sin = Math.sin(angle);
22011
+ const lumR = 0.299;
22012
+ const lumG = 0.587;
22013
+ const lumB = 0.114;
22014
+ return this.transform(
22015
+ lumR + cos * (1 - lumR) + sin * -lumR,
22016
+ lumR + cos * -lumR + sin * 0.143,
22017
+ lumR + cos * -lumR + sin * -(1 - lumR),
22018
+ 0,
22019
+ lumG + cos * -lumG + sin * -lumG,
22020
+ lumG + cos * (1 - lumG) + sin * 0.14,
22021
+ lumG + cos * -lumG + sin * lumG,
22022
+ 0,
22023
+ lumB + cos * -lumB + sin * (1 - lumB),
22024
+ lumB + cos * -lumB + sin * -0.283,
22025
+ lumB + cos * (1 - lumB) + sin * lumB,
22026
+ 0,
22027
+ 0,
22028
+ 0,
22029
+ 0,
22030
+ 1
22031
+ );
22032
+ }
22033
+ /**
22034
+ * Apply a sepia tone.
22035
+ * @param amount - sepia intensity (0.0 = original, 1.0 = full sepia)
22036
+ * @returns this instance for chaining
22037
+ */
22038
+ sepia(amount = 1) {
22039
+ if (amount >= 1) {
22040
+ return this.transform(
22041
+ 0.393,
22042
+ 0.349,
22043
+ 0.272,
22044
+ 0,
22045
+ 0.769,
22046
+ 0.686,
22047
+ 0.534,
22048
+ 0,
22049
+ 0.189,
22050
+ 0.168,
22051
+ 0.131,
22052
+ 0,
22053
+ 0,
22054
+ 0,
22055
+ 0,
22056
+ 1
22057
+ );
22058
+ }
22059
+ const a = amount;
22060
+ const b = 1 - a;
22061
+ return this.transform(
22062
+ b + 0.393 * a,
22063
+ 0.349 * a,
22064
+ 0.272 * a,
22065
+ 0,
22066
+ 0.769 * a,
22067
+ b + 0.686 * a,
22068
+ 0.534 * a,
22069
+ 0,
22070
+ 0.189 * a,
22071
+ 0.168 * a,
22072
+ b + 0.131 * a,
22073
+ 0,
22074
+ 0,
22075
+ 0,
22076
+ 0,
22077
+ 1
22078
+ );
22079
+ }
22080
+ /**
22081
+ * Apply a color inversion.
22082
+ * @param amount - inversion amount (0.0 = original, 1.0 = fully inverted)
22083
+ * @returns this instance for chaining
22084
+ */
22085
+ invertColors(amount = 1) {
22086
+ const c = 1 - 2 * amount;
22087
+ return this.transform(
22088
+ c,
22089
+ 0,
22090
+ 0,
22091
+ 0,
22092
+ 0,
22093
+ c,
22094
+ 0,
22095
+ 0,
22096
+ 0,
22097
+ 0,
22098
+ c,
22099
+ 0,
22100
+ amount,
22101
+ amount,
22102
+ amount,
22103
+ 1
22104
+ );
22105
+ }
22106
+ };
22107
+
22108
+ // src/video/webgl/utils/attributes.js
22109
+ function extractAttributes(gl, shader) {
22110
+ const attributes = {};
22111
+ const attrRx = /attribute\s+\w+\s+(\w+)/g;
22112
+ let match;
22113
+ let i = 0;
22114
+ while (match = attrRx.exec(shader.vertex)) {
22115
+ attributes[match[1]] = i++;
22116
+ }
22117
+ return attributes;
22118
+ }
22119
+
22120
+ // src/video/webgl/utils/precision.js
22121
+ function setPrecision(src, precision) {
22122
+ if (src.substring(0, 9) !== "precision") {
22123
+ return "precision " + precision + " float;\n" + src;
22124
+ }
22125
+ return src;
22126
+ }
22127
+ function getMaxShaderPrecision(gl, highPrecision = true) {
22128
+ if (highPrecision && gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT).precision > 0 && gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT).precision > 0) {
22129
+ return "highp";
22130
+ }
22131
+ if (gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_FLOAT).precision > 0 && gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT).precision > 0) {
22132
+ return "mediump";
22133
+ }
22134
+ return "lowp";
22135
+ }
22136
+
22137
+ // src/video/webgl/utils/program.js
22138
+ function compileShader(gl, type, source) {
22139
+ const shader = gl.createShader(type);
22140
+ gl.shaderSource(shader, source);
22141
+ gl.compileShader(shader);
22142
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
22143
+ throw new Error(gl.getShaderInfoLog(shader));
22144
+ }
22145
+ return shader;
22146
+ }
22147
+ function compileProgram(gl, vertex, fragment, attributes) {
22148
+ const vertShader = compileShader(gl, gl.VERTEX_SHADER, vertex);
22149
+ const fragShader = compileShader(gl, gl.FRAGMENT_SHADER, fragment);
22150
+ const program = gl.createProgram();
22151
+ gl.attachShader(program, vertShader);
22152
+ gl.attachShader(program, fragShader);
22153
+ for (const location in attributes) {
22154
+ gl.bindAttribLocation(program, attributes[location], location);
22155
+ }
22156
+ gl.linkProgram(program);
22157
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
22158
+ const error_msg = "Error initializing Shader " + this + "\ngl.VALIDATE_STATUS: " + gl.getProgramParameter(program, gl.VALIDATE_STATUS) + "\ngl.getError()" + gl.getError() + "\ngl.getProgramInfoLog()" + gl.getProgramInfoLog(program);
22159
+ gl.deleteProgram(program);
22160
+ throw new Error(error_msg);
22161
+ }
22162
+ gl.useProgram(program);
22163
+ gl.deleteShader(vertShader);
22164
+ gl.deleteShader(fragShader);
22165
+ return program;
22166
+ }
22167
+
22168
+ // src/video/webgl/utils/string.js
22169
+ function minify(src) {
22170
+ src = src.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1");
22171
+ src = src.replace(/(\\n\s+)|(\s+\\n)/g, "");
22172
+ src = src.replace(/(\\r|\\n)+/g, "");
22173
+ src = src.replace(/[ \t]*([;,[\](){}\\\/\-+*|^&!=<>?~%])[ \t]*/g, "$1");
22174
+ return src;
22175
+ }
22176
+
22177
+ // src/video/webgl/utils/uniforms.js
22178
+ var fnHash = {
22179
+ bool: "1i",
22180
+ int: "1i",
22181
+ float: "1f",
22182
+ vec2: "2fv",
22183
+ vec3: "3fv",
22184
+ vec4: "4fv",
22185
+ bvec2: "2iv",
22186
+ bvec3: "3iv",
22187
+ bvec4: "4iv",
22188
+ ivec2: "2iv",
22189
+ ivec3: "3iv",
22190
+ ivec4: "4iv",
22191
+ mat2: "Matrix2fv",
22192
+ mat3: "Matrix3fv",
22193
+ mat4: "Matrix4fv",
22194
+ sampler2D: "1i"
22195
+ };
22196
+ function extractUniforms(gl, shader) {
22197
+ const uniforms = {};
22198
+ const uniRx = /uniform\s+(\w+)\s+(\w+)/g;
22199
+ const uniformsData = {};
22200
+ const descriptor = {};
22201
+ const locations = {};
22202
+ let match;
22203
+ [shader.vertex, shader.fragment].forEach((shader2) => {
22204
+ while (match = uniRx.exec(shader2)) {
22205
+ uniformsData[match[2]] = match[1];
22206
+ }
22207
+ });
22208
+ Object.keys(uniformsData).forEach((name) => {
22209
+ const type = uniformsData[name];
22210
+ locations[name] = gl.getUniformLocation(shader.program, name);
22211
+ descriptor[name] = {
22212
+ get: /* @__PURE__ */ (function(name2) {
22213
+ return function() {
22214
+ return locations[name2];
22215
+ };
22216
+ })(name),
22217
+ set: (function(name2, type2, fn) {
22218
+ if (/^mat/.test(type2)) {
22219
+ return function(val) {
22220
+ gl[fn](locations[name2], false, val);
22221
+ };
22222
+ } else {
22223
+ return function(val) {
22224
+ let fnv = fn;
22225
+ if (val.length && !/v$/.test(fn)) {
22226
+ fnv += "v";
22227
+ }
22228
+ gl[fnv](locations[name2], val);
22229
+ };
22230
+ }
22231
+ })(name, type, "uniform" + fnHash[type])
22232
+ };
22233
+ });
22234
+ Object.defineProperties(uniforms, descriptor);
22235
+ return uniforms;
22236
+ }
22237
+
22238
+ // src/video/webgl/glshader.js
22239
+ var GLShader = class {
22240
+ /**
22241
+ * @param {WebGLRenderingContext} gl - the current WebGL rendering context
22242
+ * @param {string} vertex - a string containing the GLSL source code to set
22243
+ * @param {string} fragment - a string containing the GLSL source code to set
22244
+ * @param {string} [precision=auto detected] - float precision ('lowp', 'mediump' or 'highp').
22245
+ * @see https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_on_the_web/GLSL_Shaders
22246
+ * @example
22247
+ * // create a basic shader
22248
+ * let myShader = new me.GLShader(
22249
+ * // WebGL rendering context
22250
+ * gl,
22251
+ * // vertex shader
22252
+ * [
22253
+ * "void main() {",
22254
+ * " gl_Position = doMathToMakeClipspaceCoordinates;",
22255
+ * "}"
22256
+ * ].join("\n"),
22257
+ * // fragment shader
22258
+ * [
22259
+ * "void main() {",
22260
+ * " gl_FragColor = doMathToMakeAColor;",
22261
+ * "}"
22262
+ * ].join("\n")
22263
+ * )
22264
+ * // use the shader
22265
+ * myShader.bind();
22266
+ */
22267
+ constructor(gl, vertex, fragment, precision) {
22268
+ this.gl = gl;
22269
+ this.vertex = setPrecision(
22270
+ minify(vertex),
22271
+ precision || getMaxShaderPrecision(this.gl)
22272
+ );
22273
+ this.fragment = setPrecision(
22274
+ minify(fragment),
22275
+ precision || getMaxShaderPrecision(this.gl)
22276
+ );
22277
+ this.attributes = extractAttributes(this.gl, this);
22278
+ this.program = compileProgram(
22279
+ this.gl,
22280
+ this.vertex,
22281
+ this.fragment,
22282
+ this.attributes
22283
+ );
22284
+ this.uniforms = extractUniforms(this.gl, this);
22285
+ on(ONCONTEXT_LOST, this.destroy, this);
22286
+ }
22287
+ /**
22288
+ * Installs this shader program as part of current rendering state
22289
+ */
22290
+ bind() {
22291
+ this.gl.useProgram(this.program);
22292
+ }
22293
+ /**
22294
+ * returns the location of an attribute variable in this shader program
22295
+ * @param {string} name - the name of the attribute variable whose location to get.
22296
+ * @returns {GLint} number indicating the location of the variable name if found. Returns -1 otherwise
22297
+ */
22298
+ getAttribLocation(name) {
22299
+ const attr = this.attributes[name];
22300
+ if (typeof attr !== "undefined") {
22301
+ return attr;
22302
+ } else {
22303
+ return -1;
22304
+ }
22305
+ }
22306
+ /**
22307
+ * Set the uniform to the given value
22308
+ * @param {string} name - the uniform name
22309
+ * @param {object|Float32Array} value - the value to assign to that uniform
22310
+ * @example
22311
+ * myShader.setUniform("uProjectionMatrix", this.projectionMatrix);
22312
+ */
22313
+ setUniform(name, value) {
22314
+ const uniforms = this.uniforms;
22315
+ if (typeof uniforms[name] !== "undefined") {
22316
+ this.bind();
22317
+ if (typeof value === "object" && typeof value.toArray === "function") {
22318
+ uniforms[name] = value.toArray();
22319
+ } else {
22320
+ uniforms[name] = value;
22321
+ }
22322
+ } else {
22323
+ throw new Error("undefined (" + name + ") uniform for shader " + this);
22324
+ }
22325
+ }
22326
+ /**
22327
+ * activate the given vertex attribute for this shader
22328
+ * @param {WebGLRenderingContext} gl - the current WebGL rendering context
22329
+ * @param {object[]} attributes - an array of vertex attributes
22330
+ * @param {number} stride - the size of a single vertex in bytes
22331
+ */
22332
+ setVertexAttributes(gl, attributes, stride) {
22333
+ for (let index = 0; index < attributes.length; ++index) {
22334
+ const element = attributes[index];
22335
+ const location = this.getAttribLocation(element.name);
22336
+ if (location !== -1) {
22337
+ gl.enableVertexAttribArray(location);
22338
+ gl.vertexAttribPointer(
22339
+ location,
22340
+ element.size,
22341
+ element.type,
22342
+ element.normalized,
22343
+ stride,
22344
+ element.offset
22345
+ );
22346
+ } else {
22347
+ gl.disableVertexAttribArray(index);
22348
+ }
22349
+ }
22350
+ }
22351
+ /**
22352
+ * destroy this shader objects resources (program, attributes, uniforms)
22353
+ */
22354
+ destroy() {
22355
+ this.uniforms = null;
22356
+ this.attributes = null;
22357
+ this.gl.deleteProgram(this.program);
22358
+ this.vertex = null;
22359
+ this.fragment = null;
22360
+ }
22361
+ };
22362
+
22363
+ // src/video/webgl/shaders/quad.vert
22364
+ var quad_default = "// Current vertex point\nattribute vec2 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, 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}\n";
22365
+
22366
+ // src/video/webgl/shadereffect.js
22367
+ var ShaderEffect = class {
22368
+ /**
22369
+ * whether this effect is active (false in Canvas mode)
22370
+ * @type {boolean}
22371
+ */
22372
+ enabled = false;
22373
+ /**
22374
+ * @param {WebGLRenderer|CanvasRenderer} renderer - the current renderer instance
22375
+ * @param {string} fragmentBody - GLSL code containing a `vec4 apply(vec4 color, vec2 uv)` function
22376
+ * that receives the sampled pixel color and UV coordinates, and returns the modified color.
22377
+ * You can declare additional `uniform` variables before the `apply()` function.
22378
+ * @param {string} [precision=auto detected] - float precision ('lowp', 'mediump' or 'highp')
22379
+ */
22380
+ constructor(renderer2, fragmentBody, precision) {
22381
+ if (typeof renderer2.gl === "undefined") {
22382
+ console.warn(
22383
+ "ShaderEffect requires WebGL and is disabled in Canvas mode"
22384
+ );
22385
+ return;
22386
+ }
22387
+ const fragment = [
22388
+ "uniform sampler2D uSampler;",
22389
+ "varying vec4 vColor;",
22390
+ "varying vec2 vRegion;",
22391
+ // user-provided fragment body (uniforms + apply function)
22392
+ fragmentBody,
22393
+ "void main(void) {",
22394
+ " vec4 texColor = texture2D(uSampler, vRegion) * vColor;",
22395
+ " gl_FragColor = apply(texColor, vRegion);",
22396
+ "}"
22397
+ ].join("\n");
22398
+ this._shader = new GLShader(
22399
+ renderer2.gl,
22400
+ quad_default,
22401
+ fragment,
22402
+ precision || renderer2.shaderPrecision
22403
+ );
22404
+ this.enabled = true;
22405
+ }
22406
+ /**
22407
+ * Set the uniform to the given value
22408
+ * @param {string} name - the uniform name
22409
+ * @param {object|Float32Array} value - the value to assign to that uniform
22410
+ */
22411
+ setUniform(name, value) {
22412
+ if (this.enabled) {
22413
+ this._shader.setUniform(name, value);
22414
+ }
22415
+ }
22416
+ /** @ignore */
22417
+ bind() {
22418
+ if (this.enabled) {
22419
+ this._shader.bind();
22420
+ }
22421
+ }
22422
+ /** @ignore */
22423
+ getAttribLocation(name) {
22424
+ return this.enabled ? this._shader.getAttribLocation(name) : -1;
22425
+ }
22426
+ /** @ignore */
22427
+ setVertexAttributes(gl, attributes, stride) {
22428
+ if (this.enabled) {
22429
+ this._shader.setVertexAttributes(gl, attributes, stride);
22430
+ }
22431
+ }
22432
+ /** @ignore */
22433
+ get program() {
22434
+ return this.enabled ? this._shader.program : null;
22435
+ }
22436
+ /** @ignore */
22437
+ get vertex() {
22438
+ return this.enabled ? this._shader.vertex : null;
22439
+ }
22440
+ /** @ignore */
22441
+ get fragment() {
22442
+ return this.enabled ? this._shader.fragment : null;
22443
+ }
22444
+ /** @ignore */
22445
+ get attributes() {
22446
+ return this.enabled ? this._shader.attributes : {};
22447
+ }
22448
+ /** @ignore */
22449
+ get uniforms() {
22450
+ return this.enabled ? this._shader.uniforms : {};
22451
+ }
22452
+ /**
22453
+ * destroy this shader effect
22454
+ */
22455
+ destroy() {
22456
+ if (this.enabled) {
22457
+ this._shader.destroy();
22458
+ this.enabled = false;
22459
+ }
22460
+ }
22461
+ };
22462
+
22463
+ // src/video/webgl/effects/colorMatrix.js
22464
+ var ColorMatrixEffect = class extends ShaderEffect {
22465
+ /**
22466
+ * @param {import("../webgl_renderer.js").default} renderer - the current renderer instance
22467
+ * @param {object} [options] - effect options
22468
+ * @param {ColorMatrix} [options.matrix] - an initial color matrix. Defaults to identity.
22469
+ */
22470
+ constructor(renderer2, options = {}) {
22471
+ super(
22472
+ renderer2,
22473
+ `
22474
+ uniform mat4 uColorMatrix;
22475
+ vec4 apply(vec4 color, vec2 uv) {
22476
+ return uColorMatrix * color;
22477
+ }
22478
+ `
22479
+ );
22480
+ this._matrix = options.matrix || new ColorMatrix();
22481
+ this._syncUniform();
22482
+ }
22483
+ /**
22484
+ * Push the current matrix values to the GPU.
22485
+ * @ignore
22486
+ */
22487
+ _syncUniform() {
22488
+ this.setUniform("uColorMatrix", this._matrix.val);
22489
+ }
22490
+ /**
22491
+ * Reset the color matrix to identity (no color change).
22492
+ * @returns {this} this instance for chaining
22493
+ */
22494
+ reset() {
22495
+ this._matrix.identity();
22496
+ this._syncUniform();
22497
+ return this;
22498
+ }
22499
+ /**
22500
+ * Apply a brightness adjustment.
22501
+ * @param {number} amount - brightness multiplier (1.0 = normal, >1 brighter, <1 darker)
22502
+ * @returns {this} this instance for chaining
22503
+ */
22504
+ brightness(amount) {
22505
+ this._matrix.brightness(amount);
22506
+ this._syncUniform();
22507
+ return this;
22508
+ }
22509
+ /**
22510
+ * Apply a contrast adjustment.
22511
+ * @param {number} amount - contrast multiplier (1.0 = normal, >1 more contrast, <1 less)
22512
+ * @returns {this} this instance for chaining
22513
+ */
22514
+ contrast(amount) {
22515
+ this._matrix.contrast(amount);
22516
+ this._syncUniform();
22517
+ return this;
22518
+ }
22519
+ /**
22520
+ * Apply a saturation adjustment.
22521
+ * @param {number} amount - saturation level (0.0 = grayscale, 1.0 = normal, >1 over-saturated)
22522
+ * @returns {this} this instance for chaining
22523
+ */
22524
+ saturate(amount) {
22525
+ this._matrix.saturate(amount);
22526
+ this._syncUniform();
22527
+ return this;
22528
+ }
22529
+ /**
22530
+ * Apply a hue rotation.
22531
+ * @param {number} angle - rotation angle in radians
22532
+ * @returns {this} this instance for chaining
22533
+ */
22534
+ hueRotate(angle) {
22535
+ this._matrix.hueRotate(angle);
22536
+ this._syncUniform();
22537
+ return this;
22538
+ }
22539
+ /**
22540
+ * Apply a sepia tone.
22541
+ * @param {number} [amount=1.0] - sepia intensity (0.0 = original, 1.0 = full sepia)
22542
+ * @returns {this} this instance for chaining
22543
+ */
22544
+ sepia(amount = 1) {
22545
+ this._matrix.sepia(amount);
22546
+ this._syncUniform();
22547
+ return this;
22548
+ }
22549
+ /**
22550
+ * Apply a color inversion.
22551
+ * @param {number} [amount=1.0] - inversion amount (0.0 = original, 1.0 = fully inverted)
22552
+ * @returns {this} this instance for chaining
22553
+ */
22554
+ invertColors(amount = 1) {
22555
+ this._matrix.invertColors(amount);
22556
+ this._syncUniform();
22557
+ return this;
22558
+ }
22559
+ /**
22560
+ * Multiply the current matrix by another color matrix.
22561
+ * @param {ColorMatrix} matrix - the matrix to multiply with
22562
+ * @returns {this} this instance for chaining
22563
+ */
22564
+ multiply(matrix) {
22565
+ this._matrix.multiply(matrix);
22566
+ this._syncUniform();
22567
+ return this;
22568
+ }
22569
+ /**
22570
+ * Multiplies the current matrix with a transform described by individual values.
22571
+ * Accepts either 6 values (2D affine: a, b, c, d, e, f) or 16 values (full 4x4 column-major).
22572
+ * @param {...number} args - 6 or 16 numeric values
22573
+ * @returns {this} this instance for chaining
22574
+ */
22575
+ transform(...args) {
22576
+ this._matrix.transform(...args);
22577
+ this._syncUniform();
22578
+ return this;
22579
+ }
22580
+ };
22581
+
22582
+ // src/camera/effects/shake_effect.ts
22583
+ var ShakeEffect = class extends CameraEffect {
22584
+ /**
22585
+ * maximum pixel offset during shake
22586
+ */
22587
+ intensity;
22588
+ /**
22589
+ * remaining duration in milliseconds
22590
+ */
22591
+ duration;
22592
+ /**
22593
+ * which axes to shake (use camera.AXIS constants)
22594
+ */
22595
+ axis;
22596
+ /**
22597
+ * optional callback when shake completes
22598
+ */
22599
+ onComplete;
22600
+ /**
22601
+ * @param camera - the camera to shake
22602
+ * @param options - shake parameters
22603
+ * @param options.intensity - maximum offset in pixels
22604
+ * @param options.duration - duration in milliseconds
22605
+ * @param [options.axis=3] - which axes (NONE=0, HORIZONTAL=1, VERTICAL=2, BOTH=3)
22606
+ * @param [options.onComplete] - callback when the shake finishes
22607
+ */
22608
+ constructor(camera, options) {
22609
+ super(camera);
22610
+ this.intensity = options.intensity;
22611
+ this.duration = options.duration;
22612
+ this.axis = options.axis ?? camera.AXIS.BOTH;
22613
+ this.onComplete = options.onComplete;
22614
+ }
22615
+ update(dt) {
22616
+ this.duration -= dt;
22617
+ if (this.duration <= 0) {
22618
+ this.duration = 0;
22619
+ this.camera.offset.setZero();
22620
+ this.isComplete = true;
22621
+ if (typeof this.onComplete === "function") {
22622
+ this.onComplete();
22623
+ }
22624
+ } else {
22625
+ const axis = this.camera.AXIS;
22626
+ if (this.axis === axis.BOTH || this.axis === axis.HORIZONTAL) {
22627
+ this.camera.offset.x = (Math.random() - 0.5) * this.intensity;
22628
+ }
22629
+ if (this.axis === axis.BOTH || this.axis === axis.VERTICAL) {
22630
+ this.camera.offset.y = (Math.random() - 0.5) * this.intensity;
22631
+ }
22632
+ }
22633
+ }
22634
+ destroy() {
22635
+ this.camera.offset.setZero();
22636
+ }
22637
+ };
22638
+
21371
22639
  // src/camera/camera2d.ts
21372
22640
  var targetV = new Vector2d();
21373
22641
  var Camera2d = class extends Renderable {
@@ -21458,20 +22726,23 @@ var Camera2d = class extends Renderable {
21458
22726
  /** default value follow */
21459
22727
  follow_axis;
21460
22728
  /**
21461
- * shake variables
21462
- * @ignore
21463
- */
21464
- _shake;
21465
- /**
21466
- * flash variables
21467
- * @ignore
22729
+ * active camera effects (shake, fade, etc.)
21468
22730
  */
21469
- _fadeOut;
22731
+ cameraEffects;
21470
22732
  /**
21471
- * fade variables
21472
- * @ignore
22733
+ * A built-in color transformation matrix applied as the final post-processing pass.
22734
+ * Provides convenient color grading (brightness, contrast, saturation, etc.)
22735
+ * that is always applied after any effects added via {@link addPostEffect}.
22736
+ * When set to identity (default), no effect is applied and there is zero overhead.
22737
+ * @example
22738
+ * // warm HDR-like color grading
22739
+ * camera.colorMatrix.contrast(1.2).saturate(1.1);
22740
+ * // reset to no color grading
22741
+ * camera.colorMatrix.identity();
21473
22742
  */
21474
- _fadeIn;
22743
+ colorMatrix;
22744
+ /** @ignore */
22745
+ _colorMatrixEffect;
21475
22746
  /** the camera deadzone */
21476
22747
  deadzone;
21477
22748
  /**
@@ -21498,20 +22769,7 @@ var Camera2d = class extends Renderable {
21498
22769
  this.offset = new Vector2d();
21499
22770
  this.target = null;
21500
22771
  this.follow_axis = this.AXIS.NONE;
21501
- this._shake = {
21502
- intensity: 0,
21503
- duration: 0,
21504
- axis: this.AXIS.BOTH,
21505
- onComplete: null
21506
- };
21507
- this._fadeOut = {
21508
- color: null,
21509
- tween: null
21510
- };
21511
- this._fadeIn = {
21512
- color: null,
21513
- tween: null
21514
- };
22772
+ this.cameraEffects = [];
21515
22773
  this.screenX = 0;
21516
22774
  this.screenY = 0;
21517
22775
  this.zoom = 1;
@@ -21523,6 +22781,9 @@ var Camera2d = class extends Renderable {
21523
22781
  this.setDeadzone(this.width / 6, this.height / 6);
21524
22782
  this.anchorPoint.set(0, 0);
21525
22783
  this.isKinematic = false;
22784
+ this._postEffectManaged = true;
22785
+ this.colorMatrix = new ColorMatrix();
22786
+ this._colorMatrixEffect = null;
21526
22787
  this.bounds.setMinMax(minX, minY, maxX, maxY);
21527
22788
  this._updateProjectionMatrix();
21528
22789
  on(GAME_RESET, this.reset, this);
@@ -21638,6 +22899,12 @@ var Camera2d = class extends Renderable {
21638
22899
  this.worldProjection.identity();
21639
22900
  this.screenProjection.identity();
21640
22901
  this._updateProjectionMatrix();
22902
+ for (let i = this.cameraEffects.length - 1; i >= 0; i--) {
22903
+ if (!this.cameraEffects[i].isPersistent) {
22904
+ this.cameraEffects[i].destroy();
22905
+ this.cameraEffects.splice(i, 1);
22906
+ }
22907
+ }
21641
22908
  }
21642
22909
  /**
21643
22910
  * change the deadzone settings.
@@ -21796,20 +23063,14 @@ var Camera2d = class extends Renderable {
21796
23063
  /** @ignore */
21797
23064
  update(dt) {
21798
23065
  this.updateTarget(dt);
21799
- if (this._shake.duration > 0) {
21800
- this._shake.duration -= dt ?? 0;
21801
- if (this._shake.duration <= 0) {
21802
- this._shake.duration = 0;
21803
- this.offset.setZero();
21804
- if (typeof this._shake.onComplete === "function") {
21805
- this._shake.onComplete();
21806
- }
21807
- } else {
21808
- if (this._shake.axis === this.AXIS.BOTH || this._shake.axis === this.AXIS.HORIZONTAL) {
21809
- this.offset.x = (Math.random() - 0.5) * this._shake.intensity;
21810
- }
21811
- if (this._shake.axis === this.AXIS.BOTH || this._shake.axis === this.AXIS.VERTICAL) {
21812
- this.offset.y = (Math.random() - 0.5) * this._shake.intensity;
23066
+ if (this.cameraEffects.length > 0) {
23067
+ for (const fx of this.cameraEffects) {
23068
+ fx.update(dt ?? 0);
23069
+ }
23070
+ for (let i = this.cameraEffects.length - 1; i >= 0; i--) {
23071
+ if (this.cameraEffects[i].isComplete) {
23072
+ this.cameraEffects[i].destroy();
23073
+ this.cameraEffects.splice(i, 1);
21813
23074
  }
21814
23075
  }
21815
23076
  this.isDirty = true;
@@ -21817,9 +23078,6 @@ var Camera2d = class extends Renderable {
21817
23078
  if (this.isDirty) {
21818
23079
  emit(VIEWPORT_ONCHANGE, this.pos);
21819
23080
  }
21820
- if (this._fadeIn.tween != null || this._fadeOut.tween != null) {
21821
- this.isDirty = true;
21822
- }
21823
23081
  if (!this.currentTransform.isIdentity()) {
21824
23082
  this.invCurrentTransform.copy(this.currentTransform).invert();
21825
23083
  } else {
@@ -21840,12 +23098,21 @@ var Camera2d = class extends Renderable {
21840
23098
  * app.viewport.shake(10, 500, app.viewport.AXIS.BOTH);
21841
23099
  */
21842
23100
  shake(intensity, duration, axis, onComplete, force) {
21843
- if (this._shake.duration === 0 || force === true) {
21844
- this._shake.intensity = intensity;
21845
- this._shake.duration = duration;
21846
- this._shake.axis = axis || this.AXIS.BOTH;
21847
- this._shake.onComplete = typeof onComplete === "function" ? onComplete : void 0;
23101
+ const existing = this.getCameraEffect(ShakeEffect);
23102
+ if (existing && force !== true) {
23103
+ return;
23104
+ }
23105
+ if (existing) {
23106
+ this.removeCameraEffect(existing);
21848
23107
  }
23108
+ this.addCameraEffect(
23109
+ new ShakeEffect(this, {
23110
+ intensity,
23111
+ duration,
23112
+ axis: axis || this.AXIS.BOTH,
23113
+ onComplete
23114
+ })
23115
+ );
21849
23116
  }
21850
23117
  /**
21851
23118
  * fadeOut(flash) effect<p>
@@ -21862,13 +23129,14 @@ var Camera2d = class extends Renderable {
21862
23129
  * });
21863
23130
  */
21864
23131
  fadeOut(color, duration = 1e3, onComplete) {
21865
- this._fadeOut.color = colorPool.get(color);
21866
- this._fadeOut.tween = tweenPool.get(this._fadeOut.color).to({ alpha: 0 }, { duration });
21867
- if (onComplete) {
21868
- this._fadeOut.tween.onComplete(onComplete);
21869
- }
21870
- this._fadeOut.tween.isPersistent = true;
21871
- this._fadeOut.tween.start();
23132
+ this.addCameraEffect(
23133
+ new FadeEffect(this, {
23134
+ color,
23135
+ duration,
23136
+ direction: "out",
23137
+ onComplete
23138
+ })
23139
+ );
21872
23140
  }
21873
23141
  /**
21874
23142
  * fadeIn effect <p>
@@ -21881,15 +23149,49 @@ var Camera2d = class extends Renderable {
21881
23149
  * app.viewport.fadeIn("#FFFFFF", 75);
21882
23150
  */
21883
23151
  fadeIn(color, duration = 1e3, onComplete) {
21884
- this._fadeIn.color = colorPool.get(color);
21885
- const _alpha = this._fadeIn.color.alpha;
21886
- this._fadeIn.color.alpha = 0;
21887
- this._fadeIn.tween = tweenPool.get(this._fadeIn.color).to({ alpha: _alpha }, { duration });
21888
- if (onComplete) {
21889
- this._fadeIn.tween.onComplete(onComplete);
23152
+ this.addCameraEffect(
23153
+ new FadeEffect(this, {
23154
+ color,
23155
+ duration,
23156
+ direction: "in",
23157
+ onComplete
23158
+ })
23159
+ );
23160
+ }
23161
+ /**
23162
+ * Add a camera effect to this camera.
23163
+ * @param effect - the camera effect to add
23164
+ * @returns the added effect
23165
+ * @example
23166
+ * camera.addCameraEffect(new ShakeEffect(camera, { intensity: 10, duration: 500 }));
23167
+ */
23168
+ addCameraEffect(effect) {
23169
+ this.cameraEffects.push(effect);
23170
+ return effect;
23171
+ }
23172
+ /**
23173
+ * Get the first camera effect matching the given class.
23174
+ * @param effectClass - the effect class to search for
23175
+ * @returns the first matching effect, or undefined
23176
+ * @example
23177
+ * const shake = camera.getCameraEffect(ShakeEffect);
23178
+ */
23179
+ getCameraEffect(effectClass) {
23180
+ return this.cameraEffects.find((fx) => fx instanceof effectClass);
23181
+ }
23182
+ /**
23183
+ * Remove a specific camera effect instance.
23184
+ * @param effect - the effect to remove
23185
+ * @example
23186
+ * const fade = camera.getCameraEffect(FadeEffect);
23187
+ * if (fade) camera.removeCameraEffect(fade);
23188
+ */
23189
+ removeCameraEffect(effect) {
23190
+ const idx = this.cameraEffects.indexOf(effect);
23191
+ if (idx !== -1) {
23192
+ effect.destroy();
23193
+ this.cameraEffects.splice(idx, 1);
21890
23194
  }
21891
- this._fadeIn.tween.isPersistent = true;
21892
- this._fadeIn.tween.start();
21893
23195
  }
21894
23196
  /**
21895
23197
  * set the camera position around the specified object
@@ -21953,30 +23255,8 @@ var Camera2d = class extends Renderable {
21953
23255
  * @ignore
21954
23256
  */
21955
23257
  drawFX(renderer2) {
21956
- const r = renderer2;
21957
- if (this._fadeIn.tween) {
21958
- r.save();
21959
- r.resetTransform();
21960
- r.setColor(this._fadeIn.color);
21961
- r.fillRect(0, 0, this.width, this.height);
21962
- r.restore();
21963
- if (this._fadeIn.color.alpha === 1) {
21964
- this._fadeIn.tween = null;
21965
- colorPool.release(this._fadeIn.color);
21966
- this._fadeIn.color = null;
21967
- }
21968
- }
21969
- if (this._fadeOut.tween) {
21970
- r.save();
21971
- r.resetTransform();
21972
- r.setColor(this._fadeOut.color);
21973
- r.fillRect(0, 0, this.width, this.height);
21974
- r.restore();
21975
- if (this._fadeOut.color.alpha === 0) {
21976
- this._fadeOut.tween = null;
21977
- colorPool.release(this._fadeOut.color);
21978
- this._fadeOut.color = null;
21979
- }
23258
+ for (const fx of this.cameraEffects) {
23259
+ fx.draw(renderer2, this.width, this.height);
21980
23260
  }
21981
23261
  }
21982
23262
  /**
@@ -21990,9 +23270,19 @@ var Camera2d = class extends Renderable {
21990
23270
  const containerOffsetY = isNonDefault ? container.pos.y : 0;
21991
23271
  const translateX = this.pos.x + this.offset.x + containerOffsetX;
21992
23272
  const translateY = this.pos.y + this.offset.y + containerOffsetY;
23273
+ if (!this.colorMatrix.isIdentity()) {
23274
+ if (!this._colorMatrixEffect) {
23275
+ this._colorMatrixEffect = new ColorMatrixEffect(renderer2);
23276
+ }
23277
+ this._colorMatrixEffect.reset().multiply(this.colorMatrix);
23278
+ this.postEffects.push(this._colorMatrixEffect);
23279
+ }
23280
+ const usePostEffect = r.beginPostEffect(this);
21993
23281
  container.translate(-translateX, -translateY);
21994
23282
  this.preDraw(r);
21995
- r.clipRect(this.screenX, this.screenY, this.width, this.height);
23283
+ if (!usePostEffect) {
23284
+ r.clipRect(this.screenX, this.screenY, this.width, this.height);
23285
+ }
21996
23286
  if (isNonDefault) {
21997
23287
  const left = -this.screenX / this.zoom;
21998
23288
  const top = -this.screenY / this.zoom;
@@ -22034,8 +23324,27 @@ var Camera2d = class extends Renderable {
22034
23324
  this.drawFX(renderer2);
22035
23325
  container.postDraw(r);
22036
23326
  this.postDraw(r);
23327
+ r.endPostEffect(this);
23328
+ if (this._colorMatrixEffect) {
23329
+ const idx = this.postEffects.indexOf(this._colorMatrixEffect);
23330
+ if (idx !== -1) {
23331
+ this.postEffects.splice(idx, 1);
23332
+ }
23333
+ }
22037
23334
  container.translate(translateX, translateY);
22038
23335
  }
23336
+ /**
23337
+ * @ignore
23338
+ */
23339
+ destroy() {
23340
+ if (this._colorMatrixEffect) {
23341
+ if (typeof this._colorMatrixEffect.destroy === "function") {
23342
+ this._colorMatrixEffect.destroy();
23343
+ }
23344
+ this._colorMatrixEffect = null;
23345
+ }
23346
+ super.destroy();
23347
+ }
22039
23348
  };
22040
23349
 
22041
23350
  // src/state/stage.ts
@@ -22327,13 +23636,19 @@ var _state = -1;
22327
23636
  var _animFrameId = -1;
22328
23637
  var _isPaused = false;
22329
23638
  var _stages = {};
22330
- var _fade = {
23639
+ var _transitionType = "fade";
23640
+ var _transitionConfig = {
22331
23641
  color: "",
22332
23642
  duration: 0
22333
23643
  };
22334
23644
  var _onSwitchComplete = null;
22335
23645
  var _extraArgs = null;
22336
23646
  var _pauseTime = 0;
23647
+ var _freezeTimer = null;
23648
+ var _freezeEndsAt = 0;
23649
+ var _freezeMusic = false;
23650
+ var _freezeStartedPause = false;
23651
+ var _freezeResolvers = [];
22337
23652
  function _startRunLoop() {
22338
23653
  if (_animFrameId === -1 && _state !== -1) {
22339
23654
  _animFrameId = globalThis.requestAnimationFrame(_renderFrame);
@@ -22347,6 +23662,32 @@ function _resumeRunLoop() {
22347
23662
  function _pauseRunLoop() {
22348
23663
  _isPaused = true;
22349
23664
  }
23665
+ function _resolveFreezeWaiters() {
23666
+ const resolvers = _freezeResolvers;
23667
+ _freezeResolvers = [];
23668
+ for (const resolve of resolvers) {
23669
+ resolve();
23670
+ }
23671
+ }
23672
+ function _endFreeze() {
23673
+ _freezeTimer = null;
23674
+ _freezeEndsAt = 0;
23675
+ const wasOwnedPause = _freezeStartedPause;
23676
+ _freezeStartedPause = false;
23677
+ if (wasOwnedPause) {
23678
+ state.resume(_freezeMusic);
23679
+ }
23680
+ _resolveFreezeWaiters();
23681
+ }
23682
+ function _cancelFreeze() {
23683
+ if (_freezeTimer !== null) {
23684
+ clearTimeout(_freezeTimer);
23685
+ _freezeTimer = null;
23686
+ _freezeEndsAt = 0;
23687
+ _freezeStartedPause = false;
23688
+ _resolveFreezeWaiters();
23689
+ }
23690
+ }
22350
23691
  function _renderFrame(time) {
22351
23692
  emit(TICK, time);
22352
23693
  if (_animFrameId !== -1) {
@@ -22373,6 +23714,7 @@ function _switchState(stateId) {
22373
23714
  emit(STATE_CHANGE);
22374
23715
  if (_onSwitchComplete) {
22375
23716
  _onSwitchComplete();
23717
+ _onSwitchComplete = null;
22376
23718
  }
22377
23719
  }
22378
23720
  }
@@ -22386,6 +23728,9 @@ once(BOOT, () => {
22386
23728
  once(VIDEO_INIT, () => {
22387
23729
  state.change(state.DEFAULT, true);
22388
23730
  });
23731
+ on(BLUR, () => {
23732
+ _cancelFreeze();
23733
+ });
22389
23734
  });
22390
23735
  var state = {
22391
23736
  /**
@@ -22444,6 +23789,7 @@ var state = {
22444
23789
  */
22445
23790
  stop(shouldPauseTrack = false) {
22446
23791
  if (_state !== this.LOADING && this.isRunning()) {
23792
+ _cancelFreeze();
22447
23793
  _stopRunLoop();
22448
23794
  if (shouldPauseTrack) {
22449
23795
  pauseTrack();
@@ -22486,6 +23832,7 @@ var state = {
22486
23832
  */
22487
23833
  resume(music = false) {
22488
23834
  if (this.isPaused()) {
23835
+ _cancelFreeze();
22489
23836
  _resumeRunLoop();
22490
23837
  if (music) {
22491
23838
  resumeTrack();
@@ -22494,6 +23841,59 @@ var state = {
22494
23841
  emit(STATE_RESUME, _pauseTime);
22495
23842
  }
22496
23843
  },
23844
+ /**
23845
+ * Freeze the current stage for a fixed duration, then automatically resume.
23846
+ * Useful for hit-stop / hit-pause effects on impact.
23847
+ *
23848
+ * Behaviour notes:
23849
+ * - If `freeze()` is called again while a freeze is already active, the
23850
+ * freeze is *extended* to whichever end-time is later (calls do not
23851
+ * stack). The `music` flag from the initial call is preserved.
23852
+ * - If the game was already paused when `freeze()` was called, the freeze
23853
+ * timer will *not* unpause it on expiry — the game stays paused.
23854
+ * - Calling `state.resume()` or `state.stop()` while a freeze is active
23855
+ * cancels the timer and resolves the returned promise immediately.
23856
+ * - The freeze is also cancelled when the window loses focus (BLUR) since
23857
+ * the hit-stop's "moment" is short enough that the user has missed it
23858
+ * by the time they return. The regular `pauseOnBlur` behaviour still
23859
+ * keeps the game paused while away.
23860
+ * - Negative, `NaN`, and `Infinity` durations silently no-op.
23861
+ * @param duration - duration of the freeze in milliseconds
23862
+ * @param [music=false] - also pause the current music track during the freeze
23863
+ * @returns a Promise that resolves once the freeze ends (or is cancelled)
23864
+ * @example
23865
+ * // simple hit-stop on impact
23866
+ * state.freeze(80);
23867
+ *
23868
+ * // chain VFX after the freeze
23869
+ * await state.freeze(120);
23870
+ * spawnImpactParticles();
23871
+ */
23872
+ freeze(duration, music = false) {
23873
+ if (!Number.isFinite(duration) || duration < 0) {
23874
+ return Promise.resolve();
23875
+ }
23876
+ const now = globalThis.performance.now();
23877
+ const newEndsAt = now + duration;
23878
+ if (_freezeTimer !== null) {
23879
+ if (newEndsAt > _freezeEndsAt) {
23880
+ clearTimeout(_freezeTimer);
23881
+ _freezeEndsAt = newEndsAt;
23882
+ _freezeTimer = setTimeout(_endFreeze, newEndsAt - now);
23883
+ }
23884
+ } else {
23885
+ _freezeMusic = music;
23886
+ _freezeEndsAt = newEndsAt;
23887
+ _freezeStartedPause = !this.isPaused();
23888
+ if (_freezeStartedPause) {
23889
+ this.pause(music);
23890
+ }
23891
+ _freezeTimer = setTimeout(_endFreeze, duration);
23892
+ }
23893
+ return new Promise((resolve) => {
23894
+ _freezeResolvers.push(resolve);
23895
+ });
23896
+ },
22497
23897
  /**
22498
23898
  * return the running state of the state manager
22499
23899
  * @returns true if a "process is running"
@@ -22571,15 +23971,28 @@ var state = {
22571
23971
  },
22572
23972
  /**
22573
23973
  * specify a global transition effect
22574
- * @param effect - (only "fade" is supported for now)
23974
+ * @param effect - "fade" for a color fade, "mask" for a shape-based mask transition
22575
23975
  * @param color - a CSS color value
22576
23976
  * @param [duration=1000] - expressed in milliseconds
22577
- */
22578
- transition(effect, color, duration) {
22579
- if (effect === "fade") {
22580
- _fade.color = color;
22581
- _fade.duration = duration;
22582
- }
23977
+ * @param [shape] - an Ellipse or Polygon defining the mask shape (required when effect is "mask")
23978
+ * @example
23979
+ * // classic fade to black
23980
+ * state.transition("fade", "#000", 500);
23981
+ * @example
23982
+ * // iris (circle) mask transition
23983
+ * state.transition("mask", "#000", 500, new Ellipse(0, 0, 1, 1));
23984
+ * @example
23985
+ * // diamond mask transition
23986
+ * state.transition("mask", "#000", 400, new Polygon(0, 0, [
23987
+ * { x: 0, y: -1 }, { x: 1, y: 0 },
23988
+ * { x: 0, y: 1 }, { x: -1, y: 0 },
23989
+ * ]));
23990
+ */
23991
+ transition(effect, color, duration = 1e3, shape) {
23992
+ _transitionType = effect;
23993
+ _transitionConfig.color = color;
23994
+ _transitionConfig.duration = duration;
23995
+ _transitionConfig.shape = shape;
22583
23996
  },
22584
23997
  /**
22585
23998
  * enable/disable the transition to a particular state (by default enabled for all)
@@ -22605,21 +24018,74 @@ var state = {
22605
24018
  }
22606
24019
  if (!this.isCurrent(stateId)) {
22607
24020
  _extraArgs = extraArgs.length > 0 ? extraArgs : null;
22608
- if (_fade.duration && _stages[stateId].transition) {
22609
- _onSwitchComplete = () => {
22610
- _app.viewport.fadeOut(_fade.color, _fade.duration);
24021
+ if (_transitionConfig.duration && _stages[stateId].transition) {
24022
+ const onComplete = () => {
24023
+ defer(
24024
+ _switchState,
24025
+ state,
24026
+ stateId
24027
+ );
22611
24028
  };
22612
- _app.viewport.fadeIn(
22613
- _fade.color,
22614
- _fade.duration,
22615
- function() {
22616
- defer(
22617
- _switchState,
22618
- this,
22619
- stateId
22620
- );
24029
+ switch (_transitionType) {
24030
+ case "fade": {
24031
+ const color = _transitionConfig.color;
24032
+ const duration = _transitionConfig.duration;
24033
+ _onSwitchComplete = () => {
24034
+ _app.viewport.addCameraEffect(
24035
+ new FadeEffect(_app.viewport, {
24036
+ color,
24037
+ duration,
24038
+ direction: "out"
24039
+ })
24040
+ );
24041
+ };
24042
+ const fadeEffect = new FadeEffect(_app.viewport, {
24043
+ color,
24044
+ duration,
24045
+ direction: "in",
24046
+ onComplete
24047
+ });
24048
+ fadeEffect.isPersistent = true;
24049
+ _app.viewport.addCameraEffect(fadeEffect);
24050
+ break;
22621
24051
  }
22622
- );
24052
+ case "mask": {
24053
+ if (!_transitionConfig.shape) {
24054
+ console.warn(
24055
+ "melonJS: mask transition requires a shape, falling back to direct switch"
24056
+ );
24057
+ onComplete();
24058
+ break;
24059
+ }
24060
+ const shape = _transitionConfig.shape;
24061
+ const color = _transitionConfig.color;
24062
+ const duration = _transitionConfig.duration;
24063
+ _onSwitchComplete = () => {
24064
+ _app.viewport.addCameraEffect(
24065
+ new MaskEffect(_app.viewport, {
24066
+ shape,
24067
+ color,
24068
+ duration,
24069
+ direction: "reveal"
24070
+ })
24071
+ );
24072
+ };
24073
+ const maskEffect = new MaskEffect(_app.viewport, {
24074
+ shape,
24075
+ color,
24076
+ duration,
24077
+ direction: "hide",
24078
+ onComplete
24079
+ });
24080
+ maskEffect.isPersistent = true;
24081
+ _app.viewport.addCameraEffect(maskEffect);
24082
+ break;
24083
+ }
24084
+ default:
24085
+ throw new Error(
24086
+ `Invalid transition type: ${_transitionType}`
24087
+ );
24088
+ }
22623
24089
  } else {
22624
24090
  if (forceChange) {
22625
24091
  _switchState(stateId);
@@ -23143,6 +24609,7 @@ var Tween = class {
23143
24609
  _yoyo;
23144
24610
  _reversed;
23145
24611
  _delayTime;
24612
+ _repeatDelayTime;
23146
24613
  _startTime;
23147
24614
  _easingFunction;
23148
24615
  _interpolationFunction;
@@ -23192,6 +24659,7 @@ var Tween = class {
23192
24659
  this._yoyo = false;
23193
24660
  this._reversed = false;
23194
24661
  this._delayTime = 0;
24662
+ this._repeatDelayTime = void 0;
23195
24663
  this._startTime = null;
23196
24664
  this._easingFunction = Easing.Linear.None;
23197
24665
  this._interpolationFunction = Interpolation.Linear;
@@ -23279,6 +24747,7 @@ var Tween = class {
23279
24747
  * @param [options.delay] - delay before starting, in milliseconds
23280
24748
  * @param [options.yoyo] - bounce back to original values when finished (use with `repeat`)
23281
24749
  * @param [options.repeat] - number of times to repeat (use `Infinity` for endless loops)
24750
+ * @param [options.repeatDelay] - delay in milliseconds before each repeat cycle
23282
24751
  * @param [options.interpolation] - interpolation function for array values
23283
24752
  * @param [options.autoStart] - start the tween immediately without calling `start()`
23284
24753
  * @returns this instance for object chaining
@@ -23301,6 +24770,9 @@ var Tween = class {
23301
24770
  if (options.delay !== void 0) {
23302
24771
  this.delay(options.delay);
23303
24772
  }
24773
+ if (options.repeatDelay !== void 0) {
24774
+ this.repeatDelay(options.repeatDelay);
24775
+ }
23304
24776
  if (options.interpolation !== void 0) {
23305
24777
  this.interpolation(options.interpolation);
23306
24778
  }
@@ -23367,6 +24839,15 @@ var Tween = class {
23367
24839
  this._repeat = times;
23368
24840
  return this;
23369
24841
  }
24842
+ /**
24843
+ * Set a delay before each repeat.
24844
+ * @param amount - delay in milliseconds before each repeat cycle
24845
+ * @returns this instance for object chaining
24846
+ */
24847
+ repeatDelay(amount) {
24848
+ this._repeatDelayTime = amount;
24849
+ return this;
24850
+ }
23370
24851
  /**
23371
24852
  * Allows the tween to bounce back to their original value when finished.
23372
24853
  * To be used together with repeat to create endless loops.
@@ -23490,7 +24971,7 @@ var Tween = class {
23490
24971
  if (this._yoyo) {
23491
24972
  this._reversed = !this._reversed;
23492
24973
  }
23493
- this._startTime = time + this._delayTime;
24974
+ this._startTime = time + (this._repeatDelayTime ?? this._delayTime);
23494
24975
  return true;
23495
24976
  } else {
23496
24977
  this._unsubscribe();
@@ -23783,7 +25264,7 @@ var Text = class extends Renderable {
23783
25264
  * @param {number} [settings.wordWrapWidth] - the maximum length in CSS pixel for a single segment of text
23784
25265
  * @param {(string|string[])} [settings.text=""] - a string, or an array of strings
23785
25266
  * @example
23786
- * let font = new me.Text(0, 0, {font: "Arial", size: 8, fillStyle: this.color});
25267
+ * let font = new Text(0, 0, {font: "Arial", size: 8, fillStyle: this.color});
23787
25268
  */
23788
25269
  constructor(x, y, settings) {
23789
25270
  super(x, y, settings.width || 0, settings.height || 0);
@@ -23844,6 +25325,7 @@ var Text = class extends Renderable {
23844
25325
  this.canvasTexture = new canvasrendertarget_default(2, 2, {
23845
25326
  offscreenCanvas: false
23846
25327
  });
25328
+ this._visibleCharacters = -1;
23847
25329
  this.metrics = new textmetrics_default(this);
23848
25330
  this.setText(settings.text);
23849
25331
  }
@@ -23942,6 +25424,53 @@ var Text = class extends Renderable {
23942
25424
  this.isDirty = true;
23943
25425
  return this;
23944
25426
  }
25427
+ /**
25428
+ * the number of characters to display (use -1 to show all).
25429
+ * Useful for typewriter effects combined with Tween.
25430
+ * @public
25431
+ * @type {number}
25432
+ * @default -1
25433
+ * @see Text#visibleRatio
25434
+ * @example
25435
+ * // typewriter effect
25436
+ * text.visibleCharacters = 0;
25437
+ * new Tween(text).to({ visibleRatio: 1.0 }, { duration: 2000 }).start();
25438
+ */
25439
+ get visibleCharacters() {
25440
+ return this._visibleCharacters;
25441
+ }
25442
+ set visibleCharacters(value) {
25443
+ if (this._visibleCharacters !== value) {
25444
+ this._visibleCharacters = value;
25445
+ this.isDirty = true;
25446
+ }
25447
+ }
25448
+ /**
25449
+ * the ratio of visible characters (0.0 to 1.0).
25450
+ * Setting this automatically updates {@link visibleCharacters}.
25451
+ * @public
25452
+ * @type {number}
25453
+ */
25454
+ get visibleRatio() {
25455
+ if (this._visibleCharacters === -1) {
25456
+ return 1;
25457
+ }
25458
+ const total = this._text.reduce((sum, line) => {
25459
+ return sum + line.length;
25460
+ }, 0);
25461
+ return total > 0 ? this._visibleCharacters / total : 1;
25462
+ }
25463
+ set visibleRatio(value) {
25464
+ const clamped = Math.max(0, Math.min(1, isFinite(value) ? value : 1));
25465
+ if (clamped >= 1) {
25466
+ this.visibleCharacters = -1;
25467
+ } else {
25468
+ const total = this._text.reduce((sum, line) => {
25469
+ return sum + line.length;
25470
+ }, 0);
25471
+ this.visibleCharacters = Math.floor(clamped * total);
25472
+ }
25473
+ }
23945
25474
  /**
23946
25475
  * update the bounding box for this Text, accounting for textAlign and textBaseline.
23947
25476
  * @param {boolean} [absolute=true] - update in absolute coordinates
@@ -23998,6 +25527,16 @@ var Text = class extends Renderable {
23998
25527
  * @param {CanvasRenderer|WebGLRenderer} renderer - Reference to the destination renderer instance
23999
25528
  */
24000
25529
  draw(renderer2) {
25530
+ if (this.isDirty) {
25531
+ this.canvasTexture.invalidate(renderer2);
25532
+ this.canvasTexture.clear();
25533
+ this._drawFont(
25534
+ this.canvasTexture.context,
25535
+ this._text,
25536
+ this.pos.x - this.metrics.x,
25537
+ this.pos.y - this.metrics.y
25538
+ );
25539
+ }
24001
25540
  let x = this.metrics.x;
24002
25541
  let y = this.metrics.y;
24003
25542
  if (renderer2.settings.subPixel === false) {
@@ -24011,8 +25550,16 @@ var Text = class extends Renderable {
24011
25550
  */
24012
25551
  _drawFont(context, text, x, y) {
24013
25552
  setContextStyle(context, this);
25553
+ let remaining = this.visibleCharacters;
24014
25554
  for (let i = 0; i < text.length; i++) {
24015
- const string = text[i].trimEnd();
25555
+ let string = text[i].trimEnd();
25556
+ if (remaining !== -1) {
25557
+ if (remaining <= 0) {
25558
+ break;
25559
+ }
25560
+ string = string.substring(0, remaining);
25561
+ remaining -= string.length;
25562
+ }
24016
25563
  if (this.fillStyle.alpha > 0) {
24017
25564
  context.fillText(string, x, y);
24018
25565
  }
@@ -24192,13 +25739,13 @@ var BitmapText = class extends Renderable {
24192
25739
  * @param {number} [settings.wordWrapWidth] - the maximum length in CSS pixel for a single segment of text
24193
25740
  * @param {(string|string[])} [settings.text] - a string, or an array of strings
24194
25741
  * @example
24195
- * // Use me.loader.preload or me.loader.load to load assets
24196
- * me.loader.preload([
24197
- * { name: "arial", type: "binary" src: "data/font/arial.fnt" },
24198
- * { name: "arial", type: "image" src: "data/font/arial.png" },
25742
+ * // Use loader.preload or loader.load to load assets
25743
+ * loader.preload([
25744
+ * { name: "arial", type: "binary", src: "data/font/arial.fnt" },
25745
+ * { name: "arial", type: "image", src: "data/font/arial.png" },
24199
25746
  * ])
24200
25747
  * // Then create an instance of your bitmap font:
24201
- * let myFont = new me.BitmapText(x, y, {font:"arial", text:"Hello"});
25748
+ * let myFont = new BitmapText(x, y, {font:"arial", text:"Hello"});
24202
25749
  * // two possibilities for using "myFont"
24203
25750
  * // either call the draw function from your Renderable draw function
24204
25751
  * myFont.draw(renderer, "Hello!", 0, 0);
@@ -24233,6 +25780,7 @@ var BitmapText = class extends Renderable {
24233
25780
  } else {
24234
25781
  this.anchorPoint.set(0, 0);
24235
25782
  }
25783
+ this._visibleCharacters = -1;
24236
25784
  this.metrics = new textmetrics_default(this);
24237
25785
  if (typeof settings.size === "number" && settings.size !== 1) {
24238
25786
  this.resize(settings.size);
@@ -24274,6 +25822,55 @@ var BitmapText = class extends Renderable {
24274
25822
  this.updateBounds();
24275
25823
  return this;
24276
25824
  }
25825
+ /**
25826
+ * the number of characters to display (use -1 to show all).
25827
+ * Useful for typewriter effects combined with Tween.
25828
+ * @public
25829
+ * @type {number}
25830
+ * @default -1
25831
+ * @see BitmapText#visibleRatio
25832
+ * @example
25833
+ * // show only the first 5 characters
25834
+ * bitmapText.visibleCharacters = 5;
25835
+ * // typewriter effect
25836
+ * bitmapText.visibleCharacters = 0;
25837
+ * new Tween(bitmapText).to({ visibleRatio: 1.0 }, { duration: 2000 }).start();
25838
+ */
25839
+ get visibleCharacters() {
25840
+ return this._visibleCharacters;
25841
+ }
25842
+ set visibleCharacters(value) {
25843
+ if (this._visibleCharacters !== value) {
25844
+ this._visibleCharacters = value;
25845
+ this.isDirty = true;
25846
+ }
25847
+ }
25848
+ /**
25849
+ * the ratio of visible characters (0.0 to 1.0).
25850
+ * Setting this automatically updates {@link visibleCharacters}.
25851
+ * @public
25852
+ * @type {number}
25853
+ */
25854
+ get visibleRatio() {
25855
+ if (this._visibleCharacters === -1) {
25856
+ return 1;
25857
+ }
25858
+ const total = this._text.reduce((sum, line) => {
25859
+ return sum + line.length;
25860
+ }, 0);
25861
+ return total > 0 ? this._visibleCharacters / total : 1;
25862
+ }
25863
+ set visibleRatio(value) {
25864
+ const clamped = Math.max(0, Math.min(1, isFinite(value) ? value : 1));
25865
+ if (clamped >= 1) {
25866
+ this.visibleCharacters = -1;
25867
+ } else {
25868
+ const total = this._text.reduce((sum, line) => {
25869
+ return sum + line.length;
25870
+ }, 0);
25871
+ this.visibleCharacters = Math.floor(clamped * total);
25872
+ }
25873
+ }
24277
25874
  /**
24278
25875
  * update the bounding box for this Bitmap Text.
24279
25876
  * @param {boolean} [absolute=true] - update the bounds size and position in (world) absolute coordinates
@@ -24376,6 +25973,8 @@ var BitmapText = class extends Renderable {
24376
25973
  default:
24377
25974
  break;
24378
25975
  }
25976
+ let charCount = 0;
25977
+ const maxChars = this.visibleCharacters;
24379
25978
  for (let i = 0; i < this._text.length; i++) {
24380
25979
  x = lX;
24381
25980
  const string = this._text[i].trimEnd();
@@ -24392,6 +25991,9 @@ var BitmapText = class extends Renderable {
24392
25991
  }
24393
25992
  let lastGlyph = null;
24394
25993
  for (let c = 0, len = string.length; c < len; c++) {
25994
+ if (maxChars !== -1 && charCount >= maxChars) {
25995
+ return;
25996
+ }
24395
25997
  const ch = string.charCodeAt(c);
24396
25998
  const glyph = this.fontData.glyphs[ch];
24397
25999
  if (typeof glyph !== "undefined") {
@@ -24420,6 +26022,7 @@ var BitmapText = class extends Renderable {
24420
26022
  "BitmapText: no defined Glyph in for " + String.fromCharCode(ch)
24421
26023
  );
24422
26024
  }
26025
+ charCount++;
24423
26026
  }
24424
26027
  y += stringHeight;
24425
26028
  }
@@ -24496,7 +26099,9 @@ function registerTiledObjectFactory(type, factory) {
24496
26099
  throw new Error("invalid factory function for " + type);
24497
26100
  }
24498
26101
  if (typeof factories.get(type) !== "undefined") {
24499
- warning("overriding Tiled object factory for " + type + " type");
26102
+ console.warn(
26103
+ "melonJS: overriding Tiled object factory for " + type + " type"
26104
+ );
24500
26105
  }
24501
26106
  factories.set(type, factory);
24502
26107
  }
@@ -24587,18 +26192,22 @@ var Particle = class extends Renderable {
24587
26192
  wind;
24588
26193
  followTrajectory;
24589
26194
  onlyInViewport;
26195
+ accurateBounds;
24590
26196
  _deltaInv;
26197
+ _halfW;
26198
+ _halfH;
24591
26199
  _angle;
24592
26200
  alive;
24593
26201
  /**
24594
26202
  * @param emitter - the particle emitter
24595
26203
  */
24596
26204
  constructor(emitter) {
26205
+ const image = emitter.settings.image;
24597
26206
  super(
24598
26207
  emitter.getRandomPointX(),
24599
26208
  emitter.getRandomPointY(),
24600
- emitter.settings.image.width,
24601
- emitter.settings.image.height
26209
+ image.width,
26210
+ image.height
24602
26211
  );
24603
26212
  this.vel = vector2dPool.get();
24604
26213
  this.onResetEvent(emitter, true);
@@ -24607,13 +26216,17 @@ var Particle = class extends Renderable {
24607
26216
  * @ignore
24608
26217
  */
24609
26218
  onResetEvent(emitter, newInstance = false) {
26219
+ const image = emitter.settings.image;
24610
26220
  if (!newInstance) {
24611
26221
  this.pos.set(emitter.getRandomPointX(), emitter.getRandomPointY());
24612
- this.resize(emitter.settings.image.width, emitter.settings.image.height);
26222
+ this.resize(image.width, image.height);
24613
26223
  this.currentTransform.identity();
24614
26224
  }
24615
- this.image = emitter.settings.image;
26225
+ this.image = image;
26226
+ this._halfW = this.width / 2;
26227
+ this._halfH = this.height / 2;
24616
26228
  this.alwaysUpdate = true;
26229
+ this.anchorPoint.set(0, 0);
24617
26230
  if (typeof emitter.settings.tint === "string") {
24618
26231
  this.tint.parseCSS(emitter.settings.tint);
24619
26232
  }
@@ -24621,21 +26234,16 @@ var Particle = class extends Renderable {
24621
26234
  if (emitter.settings.blendMode !== "normal") {
24622
26235
  this.blendMode = emitter.settings.blendMode;
24623
26236
  }
24624
- const angle = emitter.settings.angle + (emitter.settings.angleVariation > 0 ? (randomFloat(0, 2) - 1) * emitter.settings.angleVariation : 0);
24625
- const speed = emitter.settings.speed + (emitter.settings.speedVariation > 0 ? (randomFloat(0, 2) - 1) * emitter.settings.speedVariation : 0);
26237
+ const angle = emitter.settings.angle + (Math.random() * 2 - 1) * emitter.settings.angleVariation;
26238
+ const speed = emitter.settings.speed + (Math.random() * 2 - 1) * emitter.settings.speedVariation;
24626
26239
  this.vel.set(speed * Math.cos(angle), -speed * Math.sin(angle));
24627
26240
  this.life = randomFloat(emitter.settings.minLife, emitter.settings.maxLife);
24628
26241
  this.startLife = this.life;
24629
- this.startScale = clamp(
24630
- randomFloat(
24631
- emitter.settings.minStartScale,
24632
- emitter.settings.maxStartScale
24633
- ),
26242
+ this.startScale = randomFloat(
24634
26243
  emitter.settings.minStartScale,
24635
26244
  emitter.settings.maxStartScale
24636
26245
  );
24637
- this.endScale = clamp(
24638
- randomFloat(emitter.settings.minEndScale, emitter.settings.maxEndScale),
26246
+ this.endScale = randomFloat(
24639
26247
  emitter.settings.minEndScale,
24640
26248
  emitter.settings.maxEndScale
24641
26249
  );
@@ -24643,7 +26251,8 @@ var Particle = class extends Renderable {
24643
26251
  this.wind = emitter.settings.wind;
24644
26252
  this.followTrajectory = emitter.settings.followTrajectory;
24645
26253
  this.onlyInViewport = emitter.settings.onlyInViewport;
24646
- this._deltaInv = timer_default.maxfps / 1e3;
26254
+ this.accurateBounds = emitter.settings.accurateBounds;
26255
+ this._deltaInv = emitter._deltaInv;
24647
26256
  if (!emitter.settings.followTrajectory) {
24648
26257
  this._angle = randomFloat(
24649
26258
  emitter.settings.minRotation,
@@ -24683,24 +26292,33 @@ var Particle = class extends Renderable {
24683
26292
  const angle = this.followTrajectory ? Math.atan2(this.vel.y, this.vel.x) : this._angle;
24684
26293
  this.pos.x += this.vel.x * skew;
24685
26294
  this.pos.y += this.vel.y * skew;
26295
+ const halfW = this._halfW;
26296
+ const halfH = this._halfH;
26297
+ const cos = Math.cos(angle);
26298
+ const sin = Math.sin(angle);
26299
+ const sCos = scale2 * cos;
26300
+ const sSin = scale2 * sin;
24686
26301
  this.currentTransform.setTransform(
24687
- scale2,
24688
- 0,
26302
+ sCos,
26303
+ sSin,
24689
26304
  0,
24690
26305
  0,
24691
- 0,
24692
- scale2,
26306
+ -sSin,
26307
+ sCos,
24693
26308
  0,
24694
26309
  0,
24695
26310
  0,
24696
26311
  0,
24697
26312
  1,
24698
26313
  0,
24699
- this.pos.x,
24700
- this.pos.y,
26314
+ this.pos.x - scale2 * (halfW * cos - halfH * sin),
26315
+ this.pos.y - scale2 * (halfW * sin + halfH * cos),
24701
26316
  0,
24702
26317
  1
24703
- ).rotate(angle);
26318
+ );
26319
+ if (this.accurateBounds) {
26320
+ this.updateBounds();
26321
+ }
24704
26322
  this.isDirty = this.inViewport || !this.onlyInViewport;
24705
26323
  return super.update(dt);
24706
26324
  }
@@ -24710,7 +26328,7 @@ var Particle = class extends Renderable {
24710
26328
  draw(renderer2) {
24711
26329
  const w = this.width;
24712
26330
  const h = this.height;
24713
- renderer2.drawImage(this.image, 0, 0, w, h, -w / 2, -h / 2, w, h);
26331
+ renderer2.drawImage(this.image, 0, 0, w, h, 0, 0, w, h);
24714
26332
  }
24715
26333
  };
24716
26334
  var particlePool = createPool(
@@ -25112,13 +26730,27 @@ var ImageLayer = class extends Sprite {
25112
26730
  this.isDirty = true;
25113
26731
  }
25114
26732
  /**
25115
- * override the default predraw function
25116
- * as repeat and anchor are managed directly in the draw method
26733
+ * Override the default preDraw to skip the base class's anchor offset
26734
+ * translation and `autoTransform` application `ImageLayer.draw()`
26735
+ * computes its own world-space position from the viewport, the parallax
26736
+ * `ratio`, anchor, and zoom, so applying anchor/transform here would
26737
+ * double-translate the layer.
26738
+ *
26739
+ * Coordinate-sensitive setup (flip, stencil mask) is intentionally
26740
+ * deferred to `draw()` where it runs *after* the per-camera zoom
26741
+ * translate/scale — that way it stays correctly aligned at any
26742
+ * `viewport.zoom` and across multiple cameras. Coordinate-independent
26743
+ * setup (alpha, post-effects, tint, blend) stays here. The inherited
26744
+ * `Renderable.postDraw` cleans up symmetrically (`clearTint` /
26745
+ * `clearMask` / `endPostEffect` / `restore`).
25117
26746
  * @ignore
25118
26747
  */
25119
26748
  preDraw(renderer2) {
25120
26749
  renderer2.save();
25121
26750
  renderer2.setGlobalAlpha(renderer2.globalAlpha() * this.getOpacity());
26751
+ if (!this._postEffectManaged) {
26752
+ renderer2.beginPostEffect(this);
26753
+ }
25122
26754
  renderer2.setTint(this.tint);
25123
26755
  if (this.blendMode !== renderer2.getBlendMode()) {
25124
26756
  renderer2.setBlendMode(this.blendMode);
@@ -25156,6 +26788,16 @@ var ImageLayer = class extends Sprite {
25156
26788
  }
25157
26789
  renderer2.translate(x * vZoom, y * vZoom);
25158
26790
  renderer2.scale(vZoom, vZoom);
26791
+ if (this._flip.x || this._flip.y) {
26792
+ const px = viewport.width;
26793
+ const py = viewport.height;
26794
+ renderer2.translate(px, py);
26795
+ renderer2.scale(this._flip.x ? -1 : 1, this._flip.y ? -1 : 1);
26796
+ renderer2.translate(-px, -py);
26797
+ }
26798
+ if (this.mask) {
26799
+ renderer2.setMask(this.mask);
26800
+ }
25159
26801
  renderer2.drawPattern(
25160
26802
  this._pattern,
25161
26803
  0,
@@ -25578,8 +27220,10 @@ var Trigger = class extends Renderable {
25578
27220
  * @param {number} [settings.width] - width of the trigger area
25579
27221
  * @param {number} [settings.height] - height of the trigger area
25580
27222
  * @param {Rect[]|Polygon[]|Line[]|Ellipse[]} [settings.shapes] - collision shape(s) that will trigger the event
25581
- * @param {string} [settings.duration] - Fade duration (in ms)
25582
- * @param {string|Color} [settings.color] - Fade color
27223
+ * @param {number} [settings.duration] - Transition duration (in ms)
27224
+ * @param {string|Color} [settings.color] - Transition color (also accepts legacy `fade` property)
27225
+ * @param {string} [settings.transition="fade"] - Transition type: "fade" for a color fade, "mask" for a shape-based mask transition
27226
+ * @param {Ellipse|Polygon} [settings.shape] - Mask shape for "mask" transition type (e.g. an Ellipse for iris, a Polygon for diamond/star)
25583
27227
  * @param {string} [settings.event="level"] - the type of event to trigger (only "level" supported for now)
25584
27228
  * @param {string} [settings.to] - level to load if level trigger
25585
27229
  * @param {string|Container} [settings.container] - Target container. See {@link level.load}
@@ -25587,41 +27231,64 @@ var Trigger = class extends Renderable {
25587
27231
  * @param {boolean} [settings.flatten] - Flatten all objects into the target container. See {@link level.load}
25588
27232
  * @param {boolean} [settings.setViewportBounds] - Resize the viewport to match the level. See {@link level.load}
25589
27233
  * @example
25590
- * world.addChild(new me.Trigger(
25591
- * x, y, {
25592
- * shapes: [new me.Rect(0, 0, 100, 100)],
25593
- * "duration" : 250,
25594
- * "color" : "#000",
25595
- * "to" : "mymap2"
25596
- * }
25597
- * ));
27234
+ * // fade transition (default)
27235
+ * world.addChild(new Trigger(x, y, {
27236
+ * shapes: [new Rect(0, 0, 100, 100)],
27237
+ * color: "#000",
27238
+ * duration: 250,
27239
+ * to: "mymap2",
27240
+ * }));
27241
+ * @example
27242
+ * // mask transition with iris (ellipse) shape
27243
+ * world.addChild(new Trigger(x, y, {
27244
+ * shapes: [new Rect(0, 0, 100, 100)],
27245
+ * transition: "mask",
27246
+ * shape: new Ellipse(0, 0, 1, 1),
27247
+ * color: "#000",
27248
+ * duration: 500,
27249
+ * to: "mymap2",
27250
+ * }));
27251
+ * @example
27252
+ * // mask transition with diamond polygon
27253
+ * world.addChild(new Trigger(x, y, {
27254
+ * shapes: [new Rect(0, 0, 100, 100)],
27255
+ * transition: "mask",
27256
+ * shape: new Polygon(0, 0, [
27257
+ * { x: 0, y: -1 }, { x: 1, y: 0 },
27258
+ * { x: 0, y: 1 }, { x: -1, y: 0 },
27259
+ * ]),
27260
+ * color: "#000",
27261
+ * duration: 400,
27262
+ * to: "mymap2",
27263
+ * }));
25598
27264
  */
25599
27265
  constructor(x, y, settings) {
25600
27266
  super(x, y, settings.width || 0, settings.height || 0);
25601
27267
  this.anchorPoint.set(0, 0);
25602
- this.fade = settings.fade;
27268
+ this.color = settings.color || settings.fade;
25603
27269
  this.duration = settings.duration;
27270
+ this.transition = settings.transition || "fade";
27271
+ this.transitionShape = settings.shape;
25604
27272
  this.fading = false;
25605
27273
  this.name = "Trigger";
25606
27274
  this.type = settings.type;
25607
27275
  this.id = settings.id;
25608
27276
  this.gotolevel = settings.to;
25609
27277
  this.triggerSettings = {
25610
- // the default (and only for now) action
25611
27278
  event: "level"
25612
27279
  };
25613
- [
27280
+ for (const property of [
25614
27281
  "type",
25615
27282
  "container",
25616
27283
  "onLoaded",
25617
27284
  "flatten",
25618
27285
  "setViewportBounds",
25619
27286
  "to"
25620
- ].forEach((property) => {
27287
+ ]) {
25621
27288
  if (typeof settings[property] !== "undefined") {
25622
27289
  this.triggerSettings[property] = settings[property];
25623
27290
  }
25624
- });
27291
+ }
25625
27292
  let shape = settings.shapes;
25626
27293
  if (typeof shape === "undefined") {
25627
27294
  shape = polygonPool.get(0, 0, [
@@ -25649,28 +27316,73 @@ var Trigger = class extends Renderable {
25649
27316
  return this.triggerSettings;
25650
27317
  }
25651
27318
  /**
25652
- * @ignore
25653
- */
25654
- onFadeComplete() {
25655
- const world = this.ancestor.getRootAncestor();
25656
- level.load(this.gotolevel, this.getTriggerSettings());
25657
- world.app.viewport.fadeOut(this.fade, this.duration);
25658
- }
25659
- /**
25660
- * trigger this event
27319
+ * Trigger this event. Override in subclasses to customize behavior.
25661
27320
  * @protected
25662
27321
  */
25663
27322
  triggerEvent() {
25664
27323
  const triggerSettings = this.getTriggerSettings();
25665
27324
  const world = this.ancestor.getRootAncestor();
27325
+ const app = world.app;
27326
+ const viewport = app.viewport;
25666
27327
  if (triggerSettings.event === "level") {
25667
27328
  this.gotolevel = triggerSettings.to;
25668
- if (this.fade && this.duration) {
27329
+ if (this.color && this.duration) {
25669
27330
  if (!this.fading) {
25670
27331
  this.fading = true;
25671
- world.app.viewport.fadeIn(this.fade, this.duration, () => {
25672
- return this.onFadeComplete();
25673
- });
27332
+ const gotolevel = this.gotolevel;
27333
+ const settings = this.getTriggerSettings();
27334
+ const color = this.color;
27335
+ const duration = this.duration;
27336
+ const useMask = this.transition === "mask" && this.transitionShape;
27337
+ const shape = this.transitionShape;
27338
+ const userOnLoaded = settings.onLoaded;
27339
+ settings.onLoaded = function(levelId) {
27340
+ const vp = app.viewport;
27341
+ if (useMask) {
27342
+ vp.addCameraEffect(
27343
+ new MaskEffect(vp, {
27344
+ shape,
27345
+ color,
27346
+ duration,
27347
+ direction: "reveal"
27348
+ })
27349
+ );
27350
+ } else {
27351
+ vp.addCameraEffect(
27352
+ new FadeEffect(vp, {
27353
+ color,
27354
+ duration,
27355
+ direction: "out"
27356
+ })
27357
+ );
27358
+ }
27359
+ if (typeof userOnLoaded === "function") {
27360
+ userOnLoaded.call(this, levelId);
27361
+ }
27362
+ };
27363
+ const onComplete = () => {
27364
+ level.load(gotolevel, settings);
27365
+ };
27366
+ if (useMask) {
27367
+ viewport.addCameraEffect(
27368
+ new MaskEffect(viewport, {
27369
+ shape,
27370
+ color,
27371
+ duration,
27372
+ direction: "hide",
27373
+ onComplete
27374
+ })
27375
+ );
27376
+ } else {
27377
+ viewport.addCameraEffect(
27378
+ new FadeEffect(viewport, {
27379
+ color,
27380
+ duration,
27381
+ direction: "in",
27382
+ onComplete
27383
+ })
27384
+ );
27385
+ }
25674
27386
  }
25675
27387
  } else {
25676
27388
  level.load(this.gotolevel, triggerSettings);
@@ -25687,14 +27399,14 @@ var Trigger = class extends Renderable {
25687
27399
  */
25688
27400
  onCollision() {
25689
27401
  if (this.name === "Trigger") {
25690
- this.triggerEvent.apply(this);
27402
+ this.triggerEvent();
25691
27403
  }
25692
27404
  return false;
25693
27405
  }
25694
27406
  };
25695
27407
 
25696
27408
  // src/version.ts
25697
- var version = "19.1.0";
27409
+ var version = "19.2.0";
25698
27410
 
25699
27411
  // src/system/bootstrap.ts
25700
27412
  var initialized = false;
@@ -27558,7 +29270,7 @@ var QuadTree = class {
27558
29270
  * @param {Container} container - group of objects to be added
27559
29271
  */
27560
29272
  insertContainer(container) {
27561
- const children = container.children;
29273
+ const children = container.getChildren();
27562
29274
  const childrenLength = children.length;
27563
29275
  for (let i = childrenLength, child; i--, child = children[i]; ) {
27564
29276
  if (child.isKinematic !== true) {
@@ -27834,6 +29546,327 @@ var World = class extends Container {
27834
29546
  }
27835
29547
  };
27836
29548
 
29549
+ // src/video/rendertarget/render_target_pool.js
29550
+ var RenderTargetPool = class {
29551
+ /**
29552
+ * @param {function(number, number): RenderTarget} factory - creates a RenderTarget with the given width and height
29553
+ */
29554
+ constructor(factory) {
29555
+ this._factory = factory;
29556
+ this._pool = [];
29557
+ this._activeBase = -1;
29558
+ this._previousBase = -1;
29559
+ }
29560
+ /**
29561
+ * Get or create a render target at the given pool index, resized to the given dimensions.
29562
+ * @param {number} index - pool index
29563
+ * @param {number} width - desired width in pixels
29564
+ * @param {number} height - desired height in pixels
29565
+ * @returns {RenderTarget} the render target
29566
+ */
29567
+ get(index, width, height) {
29568
+ if (!this._pool[index]) {
29569
+ this._pool[index] = this._factory(width, height);
29570
+ } else {
29571
+ this._pool[index].resize(width, height);
29572
+ }
29573
+ return this._pool[index];
29574
+ }
29575
+ /**
29576
+ * Prepare render targets for a post-effect pass.
29577
+ * Allocates/resizes the capture target and optionally the ping-pong target.
29578
+ * @param {boolean} isCamera - true for camera effects (indices 0+1), false for sprite (indices 2+3)
29579
+ * @param {number} effectCount - number of enabled effects
29580
+ * @param {number} width - target width in pixels
29581
+ * @param {number} height - target height in pixels
29582
+ * @returns {RenderTarget} the capture target (ready to bind)
29583
+ */
29584
+ begin(isCamera, effectCount, width, height) {
29585
+ const newBase = isCamera ? 0 : 2;
29586
+ if (!isCamera && this._activeBase === newBase) {
29587
+ return this.get(this._activeBase, width, height);
29588
+ }
29589
+ this._previousBase = this._activeBase;
29590
+ this._activeBase = newBase;
29591
+ const rt = this.get(this._activeBase, width, height);
29592
+ if (effectCount > 1) {
29593
+ this.get(this._activeBase + 1, width, height);
29594
+ }
29595
+ return rt;
29596
+ }
29597
+ /**
29598
+ * Get the capture render target for the current active pass.
29599
+ * @returns {RenderTarget|undefined} the capture target, or undefined if no active pass
29600
+ */
29601
+ getCaptureTarget() {
29602
+ if (this._activeBase < 0) {
29603
+ return void 0;
29604
+ }
29605
+ return this._pool[this._activeBase];
29606
+ }
29607
+ /**
29608
+ * Get the ping-pong render target for the current active pass.
29609
+ * @returns {RenderTarget|undefined} the ping-pong target, or undefined if no active pass
29610
+ */
29611
+ getPingPongTarget() {
29612
+ if (this._activeBase < 0) {
29613
+ return void 0;
29614
+ }
29615
+ return this._pool[this._activeBase + 1];
29616
+ }
29617
+ /**
29618
+ * End the current pass and restore the previous active base.
29619
+ * Returns the parent render target to rebind (or null for screen).
29620
+ * @returns {RenderTarget|null} the parent target, or null if returning to screen
29621
+ */
29622
+ end() {
29623
+ this._activeBase = this._previousBase;
29624
+ this._previousBase = -1;
29625
+ if (this._activeBase >= 0 && this._pool[this._activeBase]) {
29626
+ return this._pool[this._activeBase];
29627
+ }
29628
+ return null;
29629
+ }
29630
+ /**
29631
+ * Resize all existing render targets in the pool to the given dimensions.
29632
+ * @param {number} width - new width in pixels
29633
+ * @param {number} height - new height in pixels
29634
+ */
29635
+ resizeAll(width, height) {
29636
+ for (const rt of this._pool) {
29637
+ if (rt) {
29638
+ rt.resize(width, height);
29639
+ }
29640
+ }
29641
+ }
29642
+ /**
29643
+ * Destroy all render targets and clear the pool.
29644
+ */
29645
+ destroy() {
29646
+ for (const rt of this._pool) {
29647
+ if (rt) {
29648
+ rt.destroy();
29649
+ }
29650
+ }
29651
+ this._pool.length = 0;
29652
+ this._activeBase = -1;
29653
+ this._previousBase = -1;
29654
+ }
29655
+ };
29656
+
29657
+ // src/video/rendertarget/webglrendertarget.js
29658
+ var DEPTH_STENCIL = 34041;
29659
+ var DEPTH_STENCIL_ATTACHMENT = 33306;
29660
+ function attachDepthStencil(gl, framebuffer, renderbuffer, width, height) {
29661
+ const depthStencil = gl.DEPTH_STENCIL ?? DEPTH_STENCIL;
29662
+ const depthStencilAttachment = gl.DEPTH_STENCIL_ATTACHMENT ?? DEPTH_STENCIL_ATTACHMENT;
29663
+ gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);
29664
+ gl.renderbufferStorage(gl.RENDERBUFFER, depthStencil, width, height);
29665
+ gl.framebufferRenderbuffer(
29666
+ gl.FRAMEBUFFER,
29667
+ depthStencilAttachment,
29668
+ gl.RENDERBUFFER,
29669
+ renderbuffer
29670
+ );
29671
+ if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE) {
29672
+ return { hasStencil: true, isComplete: true };
29673
+ }
29674
+ gl.framebufferRenderbuffer(
29675
+ gl.FRAMEBUFFER,
29676
+ depthStencilAttachment,
29677
+ gl.RENDERBUFFER,
29678
+ null
29679
+ );
29680
+ gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height);
29681
+ gl.framebufferRenderbuffer(
29682
+ gl.FRAMEBUFFER,
29683
+ gl.DEPTH_ATTACHMENT,
29684
+ gl.RENDERBUFFER,
29685
+ renderbuffer
29686
+ );
29687
+ const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
29688
+ return {
29689
+ hasStencil: false,
29690
+ isComplete: status === gl.FRAMEBUFFER_COMPLETE
29691
+ };
29692
+ }
29693
+ var WebGLRenderTarget = class extends RenderTarget {
29694
+ /**
29695
+ * @param {WebGLRenderingContext|WebGL2RenderingContext} gl - the WebGL context
29696
+ * @param {number} width - initial width in pixels
29697
+ * @param {number} height - initial height in pixels
29698
+ */
29699
+ constructor(gl, width, height) {
29700
+ super();
29701
+ this.gl = gl;
29702
+ this.width = width;
29703
+ this.height = height;
29704
+ this.framebuffer = gl.createFramebuffer();
29705
+ const prevUnit = gl.getParameter(gl.ACTIVE_TEXTURE);
29706
+ this.texture = gl.createTexture();
29707
+ gl.activeTexture(gl.TEXTURE0);
29708
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
29709
+ gl.texImage2D(
29710
+ gl.TEXTURE_2D,
29711
+ 0,
29712
+ gl.RGBA,
29713
+ width,
29714
+ height,
29715
+ 0,
29716
+ gl.RGBA,
29717
+ gl.UNSIGNED_BYTE,
29718
+ null
29719
+ );
29720
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
29721
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
29722
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
29723
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
29724
+ this.depthStencilBuffer = gl.createRenderbuffer();
29725
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
29726
+ gl.framebufferTexture2D(
29727
+ gl.FRAMEBUFFER,
29728
+ gl.COLOR_ATTACHMENT0,
29729
+ gl.TEXTURE_2D,
29730
+ this.texture,
29731
+ 0
29732
+ );
29733
+ this._applyDepthStencil(width, height);
29734
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
29735
+ gl.bindRenderbuffer(gl.RENDERBUFFER, null);
29736
+ gl.bindTexture(gl.TEXTURE_2D, null);
29737
+ gl.activeTexture(prevUnit);
29738
+ }
29739
+ /**
29740
+ * (Re)attach the depth/stencil renderbuffer at the given size and
29741
+ * update `_hasStencil` based on the resulting framebuffer status.
29742
+ * Falls back to depth-only when packed depth+stencil fails; warns
29743
+ * once if even depth-only is incomplete.
29744
+ * @ignore
29745
+ */
29746
+ _applyDepthStencil(width, height) {
29747
+ const result = attachDepthStencil(
29748
+ this.gl,
29749
+ this.framebuffer,
29750
+ this.depthStencilBuffer,
29751
+ width,
29752
+ height
29753
+ );
29754
+ this._hasStencil = result.hasStencil;
29755
+ if (!result.isComplete) {
29756
+ console.warn(
29757
+ "WebGLRenderTarget: framebuffer incomplete after depth-only fallback \u2014 rendering into this target may fail"
29758
+ );
29759
+ } else if (!result.hasStencil) {
29760
+ console.warn(
29761
+ "WebGLRenderTarget: depth+stencil attachment failed; using depth-only \u2014 stencil masking disabled for this target"
29762
+ );
29763
+ }
29764
+ }
29765
+ /**
29766
+ * Bind this FBO as the active render target.
29767
+ * All subsequent draw calls will render into this FBO's texture.
29768
+ */
29769
+ bind() {
29770
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.framebuffer);
29771
+ }
29772
+ /**
29773
+ * Unbind this FBO and restore rendering to the screen.
29774
+ */
29775
+ unbind() {
29776
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
29777
+ }
29778
+ /**
29779
+ * Resize the FBO texture and renderbuffer.
29780
+ * @param {number} width - new width in pixels
29781
+ * @param {number} height - new height in pixels
29782
+ */
29783
+ resize(width, height) {
29784
+ if (this.width === width && this.height === height) {
29785
+ return;
29786
+ }
29787
+ const gl = this.gl;
29788
+ this.width = width;
29789
+ this.height = height;
29790
+ const prevUnit = gl.getParameter(gl.ACTIVE_TEXTURE);
29791
+ gl.activeTexture(gl.TEXTURE0);
29792
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
29793
+ gl.texImage2D(
29794
+ gl.TEXTURE_2D,
29795
+ 0,
29796
+ gl.RGBA,
29797
+ width,
29798
+ height,
29799
+ 0,
29800
+ gl.RGBA,
29801
+ gl.UNSIGNED_BYTE,
29802
+ null
29803
+ );
29804
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
29805
+ this._applyDepthStencil(width, height);
29806
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
29807
+ gl.bindTexture(gl.TEXTURE_2D, null);
29808
+ gl.bindRenderbuffer(gl.RENDERBUFFER, null);
29809
+ gl.activeTexture(prevUnit);
29810
+ }
29811
+ /**
29812
+ * Clear the FBO contents to transparent black.
29813
+ */
29814
+ clear() {
29815
+ const gl = this.gl;
29816
+ this.bind();
29817
+ gl.clearColor(0, 0, 0, 0);
29818
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
29819
+ this.unbind();
29820
+ }
29821
+ /**
29822
+ * Returns an ImageData object representing the pixel contents of this render target.
29823
+ * @param {number} [x=0] - x coordinate of the top-left corner
29824
+ * @param {number} [y=0] - y coordinate of the top-left corner
29825
+ * @param {number} [width=this.width] - width of the area to read
29826
+ * @param {number} [height=this.height] - height of the area to read
29827
+ * @returns {ImageData} the pixel data
29828
+ */
29829
+ getImageData(x = 0, y = 0, width = this.width, height = this.height) {
29830
+ const gl = this.gl;
29831
+ x = Math.max(0, Math.min(Math.floor(x), this.width - 1));
29832
+ y = Math.max(0, Math.min(Math.floor(y), this.height - 1));
29833
+ width = Math.max(1, Math.min(width, this.width - x));
29834
+ height = Math.max(1, Math.min(height, this.height - y));
29835
+ const pixels = new Uint8ClampedArray(width * height * 4);
29836
+ this.bind();
29837
+ gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
29838
+ this.unbind();
29839
+ const rowSize = width * 4;
29840
+ const temp = new Uint8ClampedArray(rowSize);
29841
+ for (let i = 0; i < Math.floor(height / 2); i++) {
29842
+ const topOffset = i * rowSize;
29843
+ const bottomOffset = (height - 1 - i) * rowSize;
29844
+ temp.set(pixels.subarray(topOffset, topOffset + rowSize));
29845
+ pixels.copyWithin(topOffset, bottomOffset, bottomOffset + rowSize);
29846
+ pixels.set(temp, bottomOffset);
29847
+ }
29848
+ return new ImageData(pixels, width, height);
29849
+ }
29850
+ /**
29851
+ * Release all GPU resources.
29852
+ */
29853
+ destroy() {
29854
+ const gl = this.gl;
29855
+ if (this.framebuffer) {
29856
+ gl.deleteFramebuffer(this.framebuffer);
29857
+ this.framebuffer = null;
29858
+ }
29859
+ if (this.texture) {
29860
+ gl.deleteTexture(this.texture);
29861
+ this.texture = null;
29862
+ }
29863
+ if (this.depthStencilBuffer) {
29864
+ gl.deleteRenderbuffer(this.depthStencilBuffer);
29865
+ this.depthStencilBuffer = null;
29866
+ }
29867
+ }
29868
+ };
29869
+
27837
29870
  // src/video/utils/dash.js
27838
29871
  var _dashPathResult = [];
27839
29872
  function dashPath(pts, pattern) {
@@ -27981,96 +30014,7 @@ var mesh_default = "uniform sampler2D uSampler;\nvarying vec4 vColor;\nvarying v
27981
30014
  // src/video/webgl/shaders/mesh.vert
27982
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";
27983
30016
 
27984
- // src/video/webgl/buffer/index.js
27985
- var IndexBuffer = class {
27986
- /**
27987
- * @param {WebGLRenderingContext|WebGL2RenderingContext} gl - the WebGL context
27988
- * @param {number} maxIndices - maximum number of indices this buffer can hold
27989
- * @param {boolean} [useUint32=false] - use Uint32 indices (WebGL2) instead of Uint16 (WebGL1)
27990
- * @param {boolean} [dynamic=false] - if true, use STREAM_DRAW for frequent updates; if false, use STATIC_DRAW
27991
- */
27992
- constructor(gl, maxIndices, useUint32 = false, dynamic = false) {
27993
- this.gl = gl;
27994
- this.dynamic = dynamic;
27995
- if (useUint32) {
27996
- this.type = gl.UNSIGNED_INT;
27997
- this.data = new Uint32Array(maxIndices);
27998
- } else {
27999
- this.type = gl.UNSIGNED_SHORT;
28000
- this.data = new Uint16Array(maxIndices);
28001
- }
28002
- this.length = 0;
28003
- this.buffer = gl.createBuffer();
28004
- }
28005
- /**
28006
- * Fill the buffer with a repeating quad index pattern [0,1,2, 2,1,3, 4,5,6, ...]
28007
- * and upload as a static buffer.
28008
- * @param {number} maxQuads - number of quads to generate indices for
28009
- */
28010
- fillQuadPattern(maxQuads) {
28011
- for (let i = 0, vertex = 0; i < maxQuads * 6; i += 6, vertex += 4) {
28012
- this.data[i] = vertex;
28013
- this.data[i + 1] = vertex + 1;
28014
- this.data[i + 2] = vertex + 2;
28015
- this.data[i + 3] = vertex + 2;
28016
- this.data[i + 4] = vertex + 1;
28017
- this.data[i + 5] = vertex + 3;
28018
- }
28019
- this.length = maxQuads * 6;
28020
- this.bind();
28021
- this.gl.bufferData(
28022
- this.gl.ELEMENT_ARRAY_BUFFER,
28023
- this.data,
28024
- this.gl.STATIC_DRAW
28025
- );
28026
- }
28027
- /**
28028
- * Reset the index count (for dynamic buffers)
28029
- */
28030
- clear() {
28031
- this.length = 0;
28032
- }
28033
- /**
28034
- * Add indices to the buffer, rebased by the given vertex offset
28035
- * @param {number[]} indices - source indices to add
28036
- * @param {number} vertexOffset - value to add to each index (vertex count at time of insertion)
28037
- */
28038
- add(indices, vertexOffset) {
28039
- for (let i = 0; i < indices.length; i++) {
28040
- this.data[this.length + i] = indices[i] + vertexOffset;
28041
- }
28042
- this.length += indices.length;
28043
- }
28044
- /**
28045
- * Add pre-computed absolute indices to the buffer (no rebasing)
28046
- * @param {number[]} indices - absolute index values to add
28047
- */
28048
- addRaw(indices) {
28049
- for (let i = 0; i < indices.length; i++) {
28050
- this.data[this.length + i] = indices[i];
28051
- }
28052
- this.length += indices.length;
28053
- }
28054
- /**
28055
- * Upload the current index data to the GPU (for dynamic buffers)
28056
- */
28057
- upload() {
28058
- this.bind();
28059
- this.gl.bufferData(
28060
- this.gl.ELEMENT_ARRAY_BUFFER,
28061
- this.data.subarray(0, this.length),
28062
- this.gl.STREAM_DRAW
28063
- );
28064
- }
28065
- /**
28066
- * bind this index buffer
28067
- */
28068
- bind() {
28069
- this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.buffer);
28070
- }
28071
- };
28072
-
28073
- // src/video/webgl/buffer/vertex.js
30017
+ // src/video/buffer/vertex.js
28074
30018
  var VertexArrayBuffer = class {
28075
30019
  constructor(vertexSize, maxVertex) {
28076
30020
  this.vertexSize = vertexSize;
@@ -28173,258 +30117,102 @@ var VertexArrayBuffer = class {
28173
30117
  }
28174
30118
  };
28175
30119
 
28176
- // src/video/webgl/utils/attributes.js
28177
- function extractAttributes(gl, shader) {
28178
- const attributes = {};
28179
- const attrRx = /attribute\s+\w+\s+(\w+)/g;
28180
- let match;
28181
- let i = 0;
28182
- while (match = attrRx.exec(shader.vertex)) {
28183
- attributes[match[1]] = i++;
28184
- }
28185
- return attributes;
28186
- }
28187
-
28188
- // src/video/webgl/utils/precision.js
28189
- function setPrecision(src, precision) {
28190
- if (src.substring(0, 9) !== "precision") {
28191
- return "precision " + precision + " float;\n" + src;
28192
- }
28193
- return src;
28194
- }
28195
- function getMaxShaderPrecision(gl, highPrecision = true) {
28196
- if (highPrecision && gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT).precision > 0 && gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT).precision > 0) {
28197
- return "highp";
28198
- }
28199
- if (gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_FLOAT).precision > 0 && gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT).precision > 0) {
28200
- return "mediump";
28201
- }
28202
- return "lowp";
28203
- }
28204
-
28205
- // src/video/webgl/utils/program.js
28206
- function compileShader(gl, type, source) {
28207
- const shader = gl.createShader(type);
28208
- gl.shaderSource(shader, source);
28209
- gl.compileShader(shader);
28210
- if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
28211
- throw new Error(gl.getShaderInfoLog(shader));
28212
- }
28213
- return shader;
28214
- }
28215
- function compileProgram(gl, vertex, fragment, attributes) {
28216
- const vertShader = compileShader(gl, gl.VERTEX_SHADER, vertex);
28217
- const fragShader = compileShader(gl, gl.FRAGMENT_SHADER, fragment);
28218
- const program = gl.createProgram();
28219
- gl.attachShader(program, vertShader);
28220
- gl.attachShader(program, fragShader);
28221
- for (const location in attributes) {
28222
- gl.bindAttribLocation(program, attributes[location], location);
28223
- }
28224
- gl.linkProgram(program);
28225
- if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
28226
- const error_msg = "Error initializing Shader " + this + "\ngl.VALIDATE_STATUS: " + gl.getProgramParameter(program, gl.VALIDATE_STATUS) + "\ngl.getError()" + gl.getError() + "\ngl.getProgramInfoLog()" + gl.getProgramInfoLog(program);
28227
- gl.deleteProgram(program);
28228
- throw new Error(error_msg);
30120
+ // src/video/buffer/index.js
30121
+ var IndexBuffer = class {
30122
+ /**
30123
+ * @param {number} maxIndices - maximum number of indices this buffer can hold
30124
+ * @param {boolean} [useUint32=false] - use Uint32 indices instead of Uint16
30125
+ */
30126
+ constructor(maxIndices, useUint32 = false) {
30127
+ this.data = useUint32 ? new Uint32Array(maxIndices) : new Uint16Array(maxIndices);
30128
+ this.length = 0;
28229
30129
  }
28230
- gl.useProgram(program);
28231
- gl.deleteShader(vertShader);
28232
- gl.deleteShader(fragShader);
28233
- return program;
28234
- }
28235
-
28236
- // src/video/webgl/utils/string.js
28237
- function minify(src) {
28238
- src = src.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1");
28239
- src = src.replace(/(\\n\s+)|(\s+\\n)/g, "");
28240
- src = src.replace(/(\\r|\\n)+/g, "");
28241
- src = src.replace(/[ \t]*([;,[\](){}\\\/\-+*|^&!=<>?~%])[ \t]*/g, "$1");
28242
- return src;
28243
- }
28244
-
28245
- // src/video/webgl/utils/uniforms.js
28246
- var fnHash = {
28247
- bool: "1i",
28248
- int: "1i",
28249
- float: "1f",
28250
- vec2: "2fv",
28251
- vec3: "3fv",
28252
- vec4: "4fv",
28253
- bvec2: "2iv",
28254
- bvec3: "3iv",
28255
- bvec4: "4iv",
28256
- ivec2: "2iv",
28257
- ivec3: "3iv",
28258
- ivec4: "4iv",
28259
- mat2: "Matrix2fv",
28260
- mat3: "Matrix3fv",
28261
- mat4: "Matrix4fv",
28262
- sampler2D: "1i"
28263
- };
28264
- function extractUniforms(gl, shader) {
28265
- const uniforms = {};
28266
- const uniRx = /uniform\s+(\w+)\s+(\w+)/g;
28267
- const uniformsData = {};
28268
- const descriptor = {};
28269
- const locations = {};
28270
- let match;
28271
- [shader.vertex, shader.fragment].forEach((shader2) => {
28272
- while (match = uniRx.exec(shader2)) {
28273
- uniformsData[match[2]] = match[1];
28274
- }
28275
- });
28276
- Object.keys(uniformsData).forEach((name) => {
28277
- const type = uniformsData[name];
28278
- locations[name] = gl.getUniformLocation(shader.program, name);
28279
- descriptor[name] = {
28280
- get: /* @__PURE__ */ (function(name2) {
28281
- return function() {
28282
- return locations[name2];
28283
- };
28284
- })(name),
28285
- set: (function(name2, type2, fn) {
28286
- if (/^mat/.test(type2)) {
28287
- return function(val) {
28288
- gl[fn](locations[name2], false, val);
28289
- };
28290
- } else {
28291
- return function(val) {
28292
- let fnv = fn;
28293
- if (val.length && !/v$/.test(fn)) {
28294
- fnv += "v";
28295
- }
28296
- gl[fnv](locations[name2], val);
28297
- };
28298
- }
28299
- })(name, type, "uniform" + fnHash[type])
28300
- };
28301
- });
28302
- Object.defineProperties(uniforms, descriptor);
28303
- return uniforms;
28304
- }
28305
-
28306
- // src/video/webgl/glshader.js
28307
- var GLShader = class {
28308
30130
  /**
28309
- * @param {WebGLRenderingContext} gl - the current WebGL rendering context
28310
- * @param {string} vertex - a string containing the GLSL source code to set
28311
- * @param {string} fragment - a string containing the GLSL source code to set
28312
- * @param {string} [precision=auto detected] - float precision ('lowp', 'mediump' or 'highp').
28313
- * @see https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_on_the_web/GLSL_Shaders
28314
- * @example
28315
- * // create a basic shader
28316
- * let myShader = new me.GLShader(
28317
- * // WebGL rendering context
28318
- * gl,
28319
- * // vertex shader
28320
- * [
28321
- * "void main() {",
28322
- * " gl_Position = doMathToMakeClipspaceCoordinates;",
28323
- * "}"
28324
- * ].join("\n"),
28325
- * // fragment shader
28326
- * [
28327
- * "void main() {",
28328
- * " gl_FragColor = doMathToMakeAColor;",
28329
- * "}"
28330
- * ].join("\n")
28331
- * )
28332
- * // use the shader
28333
- * myShader.bind();
30131
+ * Fill the buffer with a repeating quad index pattern [0,1,2, 2,1,3, 4,5,6, ...]
30132
+ * @param {number} maxQuads - number of quads to generate indices for
28334
30133
  */
28335
- constructor(gl, vertex, fragment, precision) {
28336
- this.gl = gl;
28337
- this.vertex = setPrecision(
28338
- minify(vertex),
28339
- precision || getMaxShaderPrecision(this.gl)
28340
- );
28341
- this.fragment = setPrecision(
28342
- minify(fragment),
28343
- precision || getMaxShaderPrecision(this.gl)
28344
- );
28345
- this.attributes = extractAttributes(this.gl, this);
28346
- this.program = compileProgram(
28347
- this.gl,
28348
- this.vertex,
28349
- this.fragment,
28350
- this.attributes
28351
- );
28352
- this.uniforms = extractUniforms(this.gl, this);
28353
- on(ONCONTEXT_LOST, this.destroy, this);
30134
+ fillQuadPattern(maxQuads) {
30135
+ for (let i = 0, vertex = 0; i < maxQuads * 6; i += 6, vertex += 4) {
30136
+ this.data[i] = vertex;
30137
+ this.data[i + 1] = vertex + 1;
30138
+ this.data[i + 2] = vertex + 2;
30139
+ this.data[i + 3] = vertex + 2;
30140
+ this.data[i + 4] = vertex + 1;
30141
+ this.data[i + 5] = vertex + 3;
30142
+ }
30143
+ this.length = maxQuads * 6;
28354
30144
  }
28355
30145
  /**
28356
- * Installs this shader program as part of current rendering state
30146
+ * Reset the index count
28357
30147
  */
28358
- bind() {
28359
- this.gl.useProgram(this.program);
30148
+ clear() {
30149
+ this.length = 0;
28360
30150
  }
28361
30151
  /**
28362
- * returns the location of an attribute variable in this shader program
28363
- * @param {string} name - the name of the attribute variable whose location to get.
28364
- * @returns {GLint} number indicating the location of the variable name if found. Returns -1 otherwise
30152
+ * Add indices to the buffer, rebased by the given vertex offset
30153
+ * @param {number[]} indices - source indices to add
30154
+ * @param {number} vertexOffset - value to add to each index
28365
30155
  */
28366
- getAttribLocation(name) {
28367
- const attr = this.attributes[name];
28368
- if (typeof attr !== "undefined") {
28369
- return attr;
28370
- } else {
28371
- return -1;
30156
+ add(indices, vertexOffset) {
30157
+ for (let i = 0; i < indices.length; i++) {
30158
+ this.data[this.length + i] = indices[i] + vertexOffset;
28372
30159
  }
30160
+ this.length += indices.length;
28373
30161
  }
28374
30162
  /**
28375
- * Set the uniform to the given value
28376
- * @param {string} name - the uniform name
28377
- * @param {object|Float32Array} value - the value to assign to that uniform
28378
- * @example
28379
- * myShader.setUniform("uProjectionMatrix", this.projectionMatrix);
30163
+ * Add pre-computed absolute indices to the buffer (no rebasing)
30164
+ * @param {number[]} indices - absolute index values to add
28380
30165
  */
28381
- setUniform(name, value) {
28382
- const uniforms = this.uniforms;
28383
- if (typeof uniforms[name] !== "undefined") {
28384
- this.bind();
28385
- if (typeof value === "object" && typeof value.toArray === "function") {
28386
- uniforms[name] = value.toArray();
28387
- } else {
28388
- uniforms[name] = value;
28389
- }
28390
- } else {
28391
- throw new Error("undefined (" + name + ") uniform for shader " + this);
30166
+ addRaw(indices) {
30167
+ for (let i = 0; i < indices.length; i++) {
30168
+ this.data[this.length + i] = indices[i];
28392
30169
  }
30170
+ this.length += indices.length;
28393
30171
  }
30172
+ };
30173
+
30174
+ // src/video/webgl/buffer/index.js
30175
+ var WebGLIndexBuffer = class extends IndexBuffer {
28394
30176
  /**
28395
- * activate the given vertex attribute for this shader
28396
- * @param {WebGLRenderingContext} gl - the current WebGL rendering context
28397
- * @param {object[]} attributes - an array of vertex attributes
28398
- * @param {number} stride - the size of a single vertex in bytes
30177
+ * @param {WebGLRenderingContext|WebGL2RenderingContext} gl - the WebGL context
30178
+ * @param {number} maxIndices - maximum number of indices this buffer can hold
30179
+ * @param {boolean} [useUint32=false] - use Uint32 indices (WebGL2) instead of Uint16 (WebGL1)
28399
30180
  */
28400
- setVertexAttributes(gl, attributes, stride) {
28401
- for (let index = 0; index < attributes.length; ++index) {
28402
- const element = attributes[index];
28403
- const location = this.getAttribLocation(element.name);
28404
- if (location !== -1) {
28405
- gl.enableVertexAttribArray(location);
28406
- gl.vertexAttribPointer(
28407
- location,
28408
- element.size,
28409
- element.type,
28410
- element.normalized,
28411
- stride,
28412
- element.offset
28413
- );
28414
- } else {
28415
- gl.disableVertexAttribArray(index);
28416
- }
28417
- }
30181
+ constructor(gl, maxIndices, useUint32 = false) {
30182
+ super(maxIndices, useUint32);
30183
+ this.gl = gl;
30184
+ this.type = useUint32 ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT;
30185
+ this.buffer = gl.createBuffer();
28418
30186
  }
28419
30187
  /**
28420
- * destroy this shader objects resources (program, attributes, uniforms)
30188
+ * Fill the buffer with a repeating quad index pattern and upload as static.
30189
+ * @param {number} maxQuads - number of quads to generate indices for
28421
30190
  */
28422
- destroy() {
28423
- this.uniforms = null;
28424
- this.attributes = null;
28425
- this.gl.deleteProgram(this.program);
28426
- this.vertex = null;
28427
- this.fragment = null;
30191
+ fillQuadPattern(maxQuads) {
30192
+ super.fillQuadPattern(maxQuads);
30193
+ this.bind();
30194
+ this.gl.bufferData(
30195
+ this.gl.ELEMENT_ARRAY_BUFFER,
30196
+ this.data,
30197
+ this.gl.STATIC_DRAW
30198
+ );
30199
+ }
30200
+ /**
30201
+ * Upload the current index data to the GPU (for dynamic buffers)
30202
+ */
30203
+ upload() {
30204
+ this.bind();
30205
+ this.gl.bufferData(
30206
+ this.gl.ELEMENT_ARRAY_BUFFER,
30207
+ this.data.subarray(0, this.length),
30208
+ this.gl.STREAM_DRAW
30209
+ );
30210
+ }
30211
+ /**
30212
+ * Bind this index buffer
30213
+ */
30214
+ bind() {
30215
+ this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.buffer);
28428
30216
  }
28429
30217
  };
28430
30218
 
@@ -28497,7 +30285,7 @@ var Batcher = class {
28497
30285
  if (this.useIndexBuffer) {
28498
30286
  const gl = this.gl;
28499
30287
  this.glVertexBuffer = gl.createBuffer();
28500
- this.indexBuffer = new IndexBuffer(gl, maxVertices * 3, false, true);
30288
+ this.indexBuffer = new WebGLIndexBuffer(gl, maxVertices * 3, false);
28501
30289
  }
28502
30290
  }
28503
30291
  /**
@@ -29239,7 +31027,7 @@ var QuadBatcher = class extends MaterialBatcher {
29239
31027
  }
29240
31028
  this.useMultiTexture = true;
29241
31029
  const maxQuads = this.vertexData.maxVertex / 4;
29242
- this.indexBuffer = new IndexBuffer(
31030
+ this.indexBuffer = new WebGLIndexBuffer(
29243
31031
  this.gl,
29244
31032
  maxQuads * 6,
29245
31033
  this.renderer.WebGLVersion > 1
@@ -29251,7 +31039,8 @@ var QuadBatcher = class extends MaterialBatcher {
29251
31039
  * Multi-texture batching is automatically enabled when the default
29252
31040
  * shader is active, and disabled for custom ShaderEffect shaders.
29253
31041
  * @see GLShader
29254
- * @param {GLShader} shader - a reference to a GLShader instance
31042
+ * @see ShaderEffect
31043
+ * @param {GLShader|ShaderEffect} shader - a reference to a GLShader or ShaderEffect instance
29255
31044
  */
29256
31045
  useShader(shader) {
29257
31046
  super.useShader(shader);
@@ -29264,7 +31053,7 @@ var QuadBatcher = class extends MaterialBatcher {
29264
31053
  reset() {
29265
31054
  super.reset();
29266
31055
  const maxQuads = this.vertexData.maxVertex / 4;
29267
- this.indexBuffer = new IndexBuffer(
31056
+ this.indexBuffer = new WebGLIndexBuffer(
29268
31057
  this.gl,
29269
31058
  maxQuads * 6,
29270
31059
  this.renderer.WebGLVersion > 1
@@ -29306,6 +31095,34 @@ var QuadBatcher = class extends MaterialBatcher {
29306
31095
  vertex.clear();
29307
31096
  }
29308
31097
  }
31098
+ /**
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
31108
+ */
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);
31125
+ }
29309
31126
  /**
29310
31127
  * Add a textured quad
29311
31128
  * @param {TextureAtlas} texture - Source texture atlas
@@ -29361,6 +31178,8 @@ var QuadBatcher = class extends MaterialBatcher {
29361
31178
 
29362
31179
  // src/video/webgl/webgl_renderer.js
29363
31180
  var _tempMatrix = new Matrix3d();
31181
+ var _savedTransform = new Matrix3d();
31182
+ var _savedProjection = new Matrix3d();
29364
31183
  var supportedCompressedTextureFormats;
29365
31184
  var WebGLRenderer = class extends Renderer {
29366
31185
  /**
@@ -29371,6 +31190,10 @@ var WebGLRenderer = class extends Renderer {
29371
31190
  this.GPUVendor = void 0;
29372
31191
  this.GPURenderer = void 0;
29373
31192
  this.gl = this.renderTarget.context;
31193
+ this._renderTargetPool = new RenderTargetPool((w, h) => {
31194
+ return new WebGLRenderTarget(this.gl, w, h);
31195
+ });
31196
+ this._savedEffectProjection = new Matrix3d();
29374
31197
  this.lineWidth = 1;
29375
31198
  this.lineJoin = "round";
29376
31199
  this.vertexBuffer = this.gl.createBuffer();
@@ -29508,6 +31331,7 @@ var WebGLRenderer = class extends Renderer {
29508
31331
  this.setBatcher("quad");
29509
31332
  this.gl.disable(this.gl.SCISSOR_TEST);
29510
31333
  this._scissorActive = false;
31334
+ this._renderTargetPool.destroy();
29511
31335
  }
29512
31336
  /**
29513
31337
  * add a new batcher to this renderer
@@ -29594,6 +31418,148 @@ var WebGLRenderer = class extends Renderer {
29594
31418
  flush() {
29595
31419
  this.currentBatcher.flush();
29596
31420
  }
31421
+ /**
31422
+ * Begin capturing rendering to an offscreen FBO for post-effect processing.
31423
+ * @param {Renderable} renderable - the renderable requesting post-effect processing
31424
+ * @returns {boolean} true if FBO capture started, false if skipped
31425
+ * @ignore
31426
+ */
31427
+ beginPostEffect(renderable) {
31428
+ const effects = renderable.postEffects.filter((fx) => {
31429
+ return fx.enabled !== false;
31430
+ });
31431
+ if (effects.length === 0) {
31432
+ this.customShader = void 0;
31433
+ return false;
31434
+ }
31435
+ if (effects.length === 1 && !renderable._postEffectManaged) {
31436
+ this.customShader = effects[0];
31437
+ return false;
31438
+ }
31439
+ this.customShader = void 0;
31440
+ const isCamera = renderable._postEffectManaged;
31441
+ const canvas = this.getCanvas();
31442
+ const w = canvas.width;
31443
+ const h = canvas.height;
31444
+ this.flush();
31445
+ this.save();
31446
+ this._savedEffectProjection.copy(this.projectionMatrix);
31447
+ const rt = this._renderTargetPool.begin(isCamera, effects.length, w, h);
31448
+ if (this.currentBatcher && this.currentBatcher.boundTextures) {
31449
+ delete this.currentBatcher.boundTextures[0];
31450
+ }
31451
+ rt.bind();
31452
+ this.setViewport(0, 0, w, h);
31453
+ this.disableScissor();
31454
+ this.setGlobalAlpha(1);
31455
+ this.setBlendMode("normal");
31456
+ if (isCamera) {
31457
+ this.clear();
31458
+ } else {
31459
+ this.clearRenderTarget();
31460
+ }
31461
+ return true;
31462
+ }
31463
+ /** @ignore */
31464
+ endPostEffect(renderable) {
31465
+ const effects = renderable.postEffects.filter((fx) => {
31466
+ return fx.enabled !== false;
31467
+ });
31468
+ if (effects.length === 0) {
31469
+ return;
31470
+ }
31471
+ if (effects.length === 1 && !renderable._postEffectManaged) {
31472
+ return;
31473
+ }
31474
+ const isCamera = renderable._postEffectManaged;
31475
+ const rt1 = this._renderTargetPool.getCaptureTarget();
31476
+ const rt2 = this._renderTargetPool.getPingPongTarget();
31477
+ const keepBlend = !isCamera;
31478
+ const canvas = this.getCanvas();
31479
+ const w = canvas.width;
31480
+ const h = canvas.height;
31481
+ this.flush();
31482
+ rt1.unbind();
31483
+ const parentRT = this._renderTargetPool.end();
31484
+ if (isCamera && renderable.isDefault === false) {
31485
+ this.clipRect(
31486
+ renderable.screenX,
31487
+ renderable.screenY,
31488
+ renderable.width,
31489
+ renderable.height
31490
+ );
31491
+ }
31492
+ if (parentRT) {
31493
+ parentRT.bind();
31494
+ }
31495
+ this.setViewport(0, 0, w, h);
31496
+ if (effects.length === 1) {
31497
+ this.blitEffect(rt1.texture, 0, 0, w, h, effects[0], keepBlend);
31498
+ } else {
31499
+ let src = rt1;
31500
+ let dst = rt2;
31501
+ for (let i = 0; i < effects.length - 1; i++) {
31502
+ dst.bind();
31503
+ this.setViewport(0, 0, w, h);
31504
+ this.clearRenderTarget();
31505
+ this.blitEffect(src.texture, 0, 0, w, h, effects[i]);
31506
+ dst.unbind();
31507
+ const tmp = src;
31508
+ src = dst;
31509
+ dst = tmp;
31510
+ }
31511
+ if (parentRT) {
31512
+ parentRT.bind();
31513
+ }
31514
+ this.setViewport(0, 0, w, h);
31515
+ this.blitEffect(
31516
+ src.texture,
31517
+ 0,
31518
+ 0,
31519
+ w,
31520
+ h,
31521
+ effects[effects.length - 1],
31522
+ keepBlend
31523
+ );
31524
+ }
31525
+ if (isCamera && renderable.isDefault === false) {
31526
+ this.disableScissor();
31527
+ }
31528
+ this.restore();
31529
+ this.projectionMatrix.copy(this._savedEffectProjection);
31530
+ this.currentBatcher.setProjection(this.projectionMatrix);
31531
+ }
31532
+ /**
31533
+ * Blit a texture to the screen through a shader effect.
31534
+ * Draws a screen-aligned quad using the given texture as source
31535
+ * and the given shader for post-processing (e.g. scanlines, desaturation).
31536
+ * @param {WebGLTexture} source - the source texture to blit
31537
+ * @param {number} x - destination x position
31538
+ * @param {number} y - destination y position
31539
+ * @param {number} width - destination width
31540
+ * @param {number} height - destination height
31541
+ * @param {ShaderEffect} shader - the shader effect to apply
31542
+ * @param {boolean} [keepBlend=false] - if true, keep current blend mode (for sprite compositing)
31543
+ */
31544
+ blitEffect(source, x, y, width, height, shader, keepBlend = false) {
31545
+ this.flush();
31546
+ const batcher = this.setBatcher("quad");
31547
+ _savedTransform.copy(this.currentTransform);
31548
+ _savedProjection.copy(this.projectionMatrix);
31549
+ this.currentTransform.identity();
31550
+ this.projectionMatrix.ortho(0, width, height, 0, -1, 1);
31551
+ batcher.setProjection(this.projectionMatrix);
31552
+ if (!keepBlend) {
31553
+ this.setBlendEnabled(false);
31554
+ }
31555
+ batcher.blitTexture(source, x, y, width, height, shader);
31556
+ if (!keepBlend) {
31557
+ this.setBlendEnabled(true);
31558
+ }
31559
+ this.currentTransform.copy(_savedTransform);
31560
+ this.projectionMatrix.copy(_savedProjection);
31561
+ batcher.setProjection(this.projectionMatrix);
31562
+ }
29597
31563
  /**
29598
31564
  * set/change the current projection matrix (WebGL only)
29599
31565
  * @param {Matrix3d} matrix - the new projection matrix
@@ -29613,6 +31579,61 @@ var WebGLRenderer = class extends Renderer {
29613
31579
  setViewport(x = 0, y = 0, w = this.getCanvas().width, h = this.getCanvas().height) {
29614
31580
  this.gl.viewport(x, y, w, h);
29615
31581
  }
31582
+ /**
31583
+ * Clear the current render target to transparent black.
31584
+ *
31585
+ * Only clears the color buffer — `setMask()` clears `STENCIL_BUFFER_BIT`
31586
+ * itself when starting a mask, and including it here would emit a WebGL
31587
+ * `Clear called for non-existing buffers` warning when the active FBO
31588
+ * has no stencil attachment (rare, but happens on drivers that fail
31589
+ * the depth+stencil renderbuffer attachment for the FBO).
31590
+ */
31591
+ clearRenderTarget() {
31592
+ const gl = this.gl;
31593
+ gl.clearColor(0, 0, 0, 0);
31594
+ gl.clear(gl.COLOR_BUFFER_BIT);
31595
+ }
31596
+ /**
31597
+ * Enable the scissor test with the given rectangle.
31598
+ * @param {number} x - x coordinate of the scissor rectangle
31599
+ * @param {number} y - y coordinate of the scissor rectangle
31600
+ * @param {number} width - width of the scissor rectangle
31601
+ * @param {number} height - height of the scissor rectangle
31602
+ */
31603
+ enableScissor(x, y, width, height) {
31604
+ const gl = this.gl;
31605
+ this.flush();
31606
+ gl.enable(gl.SCISSOR_TEST);
31607
+ 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;
31618
+ }
31619
+ /**
31620
+ * Disable the scissor test, allowing rendering to the full viewport.
31621
+ */
31622
+ disableScissor() {
31623
+ this.gl.disable(this.gl.SCISSOR_TEST);
31624
+ this._scissorActive = false;
31625
+ }
31626
+ /**
31627
+ * Enable or disable alpha blending.
31628
+ * @param {boolean} enable - true to enable blending, false to disable
31629
+ */
31630
+ setBlendEnabled(enable2) {
31631
+ if (enable2) {
31632
+ this.gl.enable(this.gl.BLEND);
31633
+ } else {
31634
+ this.gl.disable(this.gl.BLEND);
31635
+ }
31636
+ }
29616
31637
  /**
29617
31638
  * Clear the frame buffer
29618
31639
  */
@@ -30040,6 +32061,7 @@ var WebGLRenderer = class extends Renderer {
30040
32061
  }
30041
32062
  }
30042
32063
  this._currentGradient = this.renderState.currentGradient;
32064
+ this.customShader = this.renderState.currentShader;
30043
32065
  }
30044
32066
  /**
30045
32067
  * saves the entire state of the renderer by pushing the current state onto a stack.
@@ -30055,6 +32077,7 @@ var WebGLRenderer = class extends Renderer {
30055
32077
  * renderer.restore();
30056
32078
  */
30057
32079
  save() {
32080
+ this.renderState.currentShader = this.customShader;
30058
32081
  this.renderState.save(this._scissorActive === true);
30059
32082
  }
30060
32083
  /**
@@ -31218,6 +33241,45 @@ var Application = class {
31218
33241
  repaint() {
31219
33242
  this.isDirty = true;
31220
33243
  }
33244
+ /**
33245
+ * Pause the current stage. Convenience proxy for {@link state.pause}.
33246
+ * @param [music=false] - also pause the current music track
33247
+ * @example
33248
+ * app.pause(); // pause game updates, keep music playing
33249
+ * app.pause(true); // pause game updates and music
33250
+ */
33251
+ pause(music = false) {
33252
+ state_default.pause(music);
33253
+ }
33254
+ /**
33255
+ * Resume the current stage. Convenience proxy for {@link state.resume}.
33256
+ * @param [music=false] - also resume the current music track
33257
+ */
33258
+ resume(music = false) {
33259
+ state_default.resume(music);
33260
+ }
33261
+ /**
33262
+ * Freeze the current stage for a fixed duration, then automatically resume.
33263
+ * Useful for hit-stop / hit-pause effects on impact.
33264
+ *
33265
+ * Convenience proxy for {@link state.freeze}; see that method's
33266
+ * documentation for the full behaviour matrix (extend-not-stack semantics,
33267
+ * interaction with manual `state.pause()` / `state.resume()`, automatic
33268
+ * cancellation on window blur, etc.).
33269
+ * @param duration - duration of the freeze in milliseconds
33270
+ * @param [music=false] - also pause the current music track during the freeze
33271
+ * @returns a Promise that resolves once the freeze ends (or is cancelled)
33272
+ * @example
33273
+ * // simple hit-stop on impact
33274
+ * app.freeze(80);
33275
+ *
33276
+ * // chain VFX after the freeze
33277
+ * await app.freeze(120);
33278
+ * spawnImpactParticles();
33279
+ */
33280
+ freeze(duration, music = false) {
33281
+ return state_default.freeze(duration, music);
33282
+ }
31221
33283
  /** @ignore */
31222
33284
  _tick(time) {
31223
33285
  this.update(time);
@@ -31290,197 +33352,42 @@ function setDefaultGame(app) {
31290
33352
  game = app;
31291
33353
  }
31292
33354
 
31293
- // src/particles/settings.js
31294
- var ParticleEmitterSettings = {
31295
- /**
31296
- * Width of the particle spawn area.
31297
- * @type {number}
31298
- * @default 1
31299
- */
33355
+ // src/particles/settings.ts
33356
+ var defaultParticleEmitterSettings = {
31300
33357
  width: 1,
31301
- /**
31302
- * Height of the particle spawn area
31303
- * @type {number}
31304
- * @default 1
31305
- */
31306
33358
  height: 1,
31307
- /**
31308
- * image used for particles texture
31309
- * (by default melonJS will create a white 8x8 texture image)
31310
- * @type {HTMLCanvasElement}
31311
- * @default undefined
31312
- * @see {@link textureSize}
31313
- */
31314
33359
  image: void 0,
31315
- /**
31316
- * default texture size used for particles if no image is specified
31317
- * (by default melonJS will create a white 8x8 texture image)
31318
- * @type {number}
31319
- * @default 8
31320
- * @see {@link image}
31321
- */
31322
33360
  textureSize: 8,
31323
- /**
31324
- * tint to be applied to particles
31325
- * @type {string}
31326
- * @default "#fff"
31327
- */
31328
33361
  tint: "#fff",
31329
- /**
31330
- * Total number of particles in the emitter
31331
- * @type {number}
31332
- * @default 50
31333
- */
31334
33362
  totalParticles: 50,
31335
- /**
31336
- * Start angle for particle launch in Radians
31337
- * @type {number}
31338
- * @default Math.PI / 2
31339
- */
31340
33363
  angle: Math.PI / 2,
31341
- /**
31342
- * letiation in the start angle for particle launch in Radians.
31343
- * @type {number}
31344
- * @default 0
31345
- */
31346
33364
  angleVariation: 0,
31347
- /**
31348
- * Minimum time each particle lives once it is emitted in ms.
31349
- * @type {number}
31350
- * @default 1000
31351
- */
31352
33365
  minLife: 1e3,
31353
- /**
31354
- * Maximum time each particle lives once it is emitted in ms.
31355
- * @type {number}
31356
- * @default 3000
31357
- */
31358
33366
  maxLife: 3e3,
31359
- /**
31360
- * Start speed of particles.<br>
31361
- * @type {number}
31362
- * @default 2
31363
- */
31364
33367
  speed: 2,
31365
- /**
31366
- * letiation in the start speed of particles
31367
- * @type {number}
31368
- * @default 1
31369
- */
31370
33368
  speedVariation: 1,
31371
- /**
31372
- * Minimum start rotation for particles sprites in Radians
31373
- * @type {number}
31374
- * @default 0
31375
- */
31376
33369
  minRotation: 0,
31377
- /**
31378
- * Maximum start rotation for particles sprites in Radians
31379
- * @type {number}
31380
- * @default 0
31381
- */
31382
33370
  maxRotation: 0,
31383
- /**
31384
- * Minimum start scale ratio for particles (1 = no scaling)
31385
- * @type {number}
31386
- * @default 1
31387
- */
31388
33371
  minStartScale: 1,
31389
- /**
31390
- * Maximum start scale ratio for particles (1 = no scaling)
31391
- * @type {number}
31392
- * @default 1
31393
- */
31394
33372
  maxStartScale: 1,
31395
- /**
31396
- * Minimum end scale ratio for particles
31397
- * @type {number}
31398
- * @default 0
31399
- */
31400
33373
  minEndScale: 0,
31401
- /**
31402
- * Maximum end scale ratio for particles
31403
- * @type {number}
31404
- * @default 0
31405
- */
31406
33374
  maxEndScale: 0,
31407
- /**
31408
- * Vertical force (Gravity) for each particle
31409
- * @type {number}
31410
- * @default 0
31411
- * @see {@link World.gravity}
31412
- */
31413
33375
  gravity: 0,
31414
- /**
31415
- * Horizontal force (like a Wind) for each particle
31416
- * @type {number}
31417
- * @default 0
31418
- */
31419
33376
  wind: 0,
31420
- /**
31421
- * Update the rotation of particle in accordance the particle trajectory.<br>
31422
- * The particle sprite should aim at zero angle (draw from left to right).<br>
31423
- * Override the particle minRotation and maxRotation.<br>
31424
- * @type {boolean}
31425
- * @default false
31426
- */
31427
33377
  followTrajectory: false,
31428
- /**
31429
- * Enable the Texture Additive by composite operation ("additive" blendMode)
31430
- * @type {boolean}
31431
- * @default false
31432
- * @see {@link blendMode}
31433
- */
31434
33378
  textureAdditive: false,
31435
- /**
31436
- * the blend mode to be applied when rendering particles.
31437
- * (note: this will superseed the `textureAdditive` setting if different than "normal")
31438
- * @type {string}
31439
- * @default normal
31440
- * @see {@link CanvasRenderer#setBlendMode}
31441
- * @see {@link WebGLRenderer#setBlendMode}
31442
- */
31443
33379
  blendMode: "normal",
31444
- /**
31445
- * Update particles only in the viewport, remove it when out of viewport.
31446
- * @type {boolean}
31447
- * @default true
31448
- */
31449
33380
  onlyInViewport: true,
31450
- /**
31451
- * Render particles in screen space.
31452
- * @type {boolean}
31453
- * @default false
31454
- */
31455
33381
  floating: false,
31456
- /**
31457
- * Maximum number of particles launched each time in this emitter (used only if emitter is Stream).
31458
- * @type {number}
31459
- * @default 10
31460
- */
31461
33382
  maxParticles: 10,
31462
- /**
31463
- * How often a particle is emitted in ms (used only if emitter is a Stream).
31464
- * @type {number}
31465
- * @default 100
31466
- */
31467
33383
  frequency: 100,
31468
- /**
31469
- * Duration that the emitter releases particles in ms (used only if emitter is Stream).
31470
- * After this period, the emitter stop the launch of particles.
31471
- * @type {number}
31472
- * @default Infinity
31473
- */
31474
33384
  duration: Infinity,
31475
- /**
31476
- * Skip n frames after updating the particle system once.
31477
- * This can be used to reduce the performance impact of emitters with many particles.
31478
- * @type {number}
31479
- * @default 0
31480
- */
31481
- framesToSkip: 0
33385
+ framesToSkip: 0,
33386
+ accurateBounds: false,
33387
+ autoDestroyOnComplete: false,
33388
+ onComplete: void 0
31482
33389
  };
31483
- var settings_default = ParticleEmitterSettings;
33390
+ var settings_default = defaultParticleEmitterSettings;
31484
33391
 
31485
33392
  // src/particles/emitter.ts
31486
33393
  function createDefaultParticleTexture(w = 8, h = 8) {
@@ -31491,9 +33398,14 @@ function createDefaultParticleTexture(w = 8, h = 8) {
31491
33398
  defaultParticleTexture.context.fillRect(0, 0, w, h);
31492
33399
  return defaultParticleTexture;
31493
33400
  }
33401
+ function clampMinToMax(settings, minKey, maxKey) {
33402
+ if (settings[minKey] > settings[maxKey]) {
33403
+ settings[minKey] = settings[maxKey];
33404
+ }
33405
+ }
31494
33406
  var ParticleEmitter = class extends Container {
31495
33407
  /**
31496
- * the current (active) emitter settings
33408
+ * the current (active) emitter settings (with defaults merged in)
31497
33409
  */
31498
33410
  settings;
31499
33411
  /** @ignore */
@@ -31510,6 +33422,19 @@ var ParticleEmitter = class extends Container {
31510
33422
  _dt;
31511
33423
  /** @ignore */
31512
33424
  _defaultParticle;
33425
+ /**
33426
+ * whether at least one particle has been spawned by this emitter — used as
33427
+ * the precondition for completion detection (a brand-new emitter with zero
33428
+ * children must not count as "complete")
33429
+ * @ignore
33430
+ */
33431
+ _hasSpawned;
33432
+ /**
33433
+ * cached `timer.maxfps / 1000` — particles read this directly instead of
33434
+ * recomputing it on every spawn.
33435
+ * @ignore
33436
+ */
33437
+ _deltaInv;
31513
33438
  /**
31514
33439
  * @param x - x position of the particle emitter
31515
33440
  * @param y - y position of the particle emitter
@@ -31541,8 +33466,8 @@ var ParticleEmitter = class extends Container {
31541
33466
  * app.world.removeChild(emitter);
31542
33467
  */
31543
33468
  constructor(x, y, settings = {}) {
31544
- super(x, y, settings.width | 1, settings.height | 1);
31545
- this.settings = {};
33469
+ super(x, y, settings.width ?? 1, settings.height ?? 1);
33470
+ this.settings = { ...settings_default };
31546
33471
  this.centerOn(x, y);
31547
33472
  this._stream = false;
31548
33473
  this._frequencyTimer = 0;
@@ -31552,18 +33477,29 @@ var ParticleEmitter = class extends Container {
31552
33477
  this.autoSort = false;
31553
33478
  this._updateCount = 0;
31554
33479
  this._dt = 0;
33480
+ this._hasSpawned = false;
33481
+ this._deltaInv = timer_default.maxfps / 1e3;
31555
33482
  this.reset(settings);
31556
33483
  }
31557
33484
  /**
31558
33485
  * Reset the emitter with particle emitter settings.
31559
- * @param settings - [optional] object with emitter settings. See {@link ParticleEmitterSettings}
33486
+ * @param settings - object with emitter settings. See {@link ParticleEmitterSettings}
31560
33487
  */
31561
33488
  reset(settings = {}) {
31562
33489
  Object.assign(this.settings, settings_default, settings);
33490
+ clampMinToMax(this.settings, "minLife", "maxLife");
33491
+ clampMinToMax(this.settings, "minStartScale", "maxStartScale");
33492
+ clampMinToMax(this.settings, "minEndScale", "maxEndScale");
33493
+ clampMinToMax(this.settings, "minRotation", "maxRotation");
33494
+ this._deltaInv = timer_default.maxfps / 1e3;
33495
+ if (typeof this._defaultParticle !== "undefined") {
33496
+ this._defaultParticle.destroy();
33497
+ this._defaultParticle = void 0;
33498
+ }
31563
33499
  if (typeof this.settings.image === "undefined") {
31564
33500
  this._defaultParticle = createDefaultParticleTexture(
31565
- settings.textureSize,
31566
- settings.textureSize
33501
+ this.settings.textureSize,
33502
+ this.settings.textureSize
31567
33503
  );
31568
33504
  this.settings.image = this._defaultParticle.canvas;
31569
33505
  }
@@ -31587,8 +33523,12 @@ var ParticleEmitter = class extends Container {
31587
33523
  // Add count particles in the game world
31588
33524
  /** @ignore */
31589
33525
  addParticles(count) {
33526
+ const z = this.pos.z;
31590
33527
  for (let i = 0; i < count; i++) {
31591
- this.addChild(particlePool.get(this), this.pos.z);
33528
+ this.addChild(particlePool.get(this), z);
33529
+ }
33530
+ if (count > 0) {
33531
+ this._hasSpawned = true;
31592
33532
  }
31593
33533
  this.isDirty = true;
31594
33534
  }
@@ -31631,15 +33571,17 @@ var ParticleEmitter = class extends Container {
31631
33571
  * @ignore
31632
33572
  */
31633
33573
  update(dt) {
31634
- if (++this._updateCount > this.settings.framesToSkip) {
31635
- this._updateCount = 0;
31636
- }
31637
- if (this._updateCount > 0) {
31638
- this._dt += dt;
31639
- return this.isDirty;
33574
+ if (this.settings.framesToSkip > 0) {
33575
+ if (++this._updateCount > this.settings.framesToSkip) {
33576
+ this._updateCount = 0;
33577
+ }
33578
+ if (this._updateCount > 0) {
33579
+ this._dt += dt;
33580
+ return this.isDirty;
33581
+ }
33582
+ dt += this._dt;
33583
+ this._dt = 0;
31640
33584
  }
31641
- dt += this._dt;
31642
- this._dt = 0;
31643
33585
  this.isDirty = this.isDirty || super.update(dt);
31644
33586
  if (this._enabled && this._stream) {
31645
33587
  if (this._durationTimer !== Infinity) {
@@ -31650,17 +33592,27 @@ var ParticleEmitter = class extends Container {
31650
33592
  }
31651
33593
  }
31652
33594
  this._frequencyTimer += dt;
31653
- const particlesCount = this.children?.length ?? 0;
33595
+ const particlesCount = this.getChildren().length;
31654
33596
  if (particlesCount < this.settings.totalParticles && this._frequencyTimer >= this.settings.frequency) {
31655
- if (particlesCount + this.settings.maxParticles <= this.settings.totalParticles) {
31656
- this.addParticles(this.settings.maxParticles);
31657
- } else {
31658
- this.addParticles(this.settings.totalParticles - particlesCount);
31659
- }
33597
+ this.addParticles(
33598
+ Math.min(
33599
+ this.settings.maxParticles,
33600
+ this.settings.totalParticles - particlesCount
33601
+ )
33602
+ );
31660
33603
  this._frequencyTimer = 0;
31661
33604
  this.isDirty = true;
31662
33605
  }
31663
33606
  }
33607
+ if (this._hasSpawned && !this._enabled && this.getChildren().length === 0) {
33608
+ this._hasSpawned = false;
33609
+ if (typeof this.settings.onComplete === "function") {
33610
+ this.settings.onComplete.call(this);
33611
+ }
33612
+ if (this.settings.autoDestroyOnComplete && this.ancestor) {
33613
+ this.ancestor.removeChild(this);
33614
+ }
33615
+ }
31664
33616
  return this.isDirty;
31665
33617
  }
31666
33618
  /**
@@ -33068,106 +35020,6 @@ var UITextButton = class extends UIBaseElement {
33068
35020
  }
33069
35021
  };
33070
35022
 
33071
- // src/video/webgl/shaders/quad.vert
33072
- var quad_default = "// Current vertex point\nattribute vec2 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, 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}\n";
33073
-
33074
- // src/video/webgl/shadereffect.js
33075
- var ShaderEffect = class {
33076
- /**
33077
- * whether this effect is active (false in Canvas mode)
33078
- * @type {boolean}
33079
- */
33080
- enabled = false;
33081
- /**
33082
- * @param {WebGLRenderer|CanvasRenderer} renderer - the current renderer instance
33083
- * @param {string} fragmentBody - GLSL code containing a `vec4 apply(vec4 color, vec2 uv)` function
33084
- * that receives the sampled pixel color and UV coordinates, and returns the modified color.
33085
- * You can declare additional `uniform` variables before the `apply()` function.
33086
- * @param {string} [precision=auto detected] - float precision ('lowp', 'mediump' or 'highp')
33087
- */
33088
- constructor(renderer2, fragmentBody, precision) {
33089
- if (typeof renderer2.gl === "undefined") {
33090
- console.warn(
33091
- "ShaderEffect requires WebGL and is disabled in Canvas mode"
33092
- );
33093
- return;
33094
- }
33095
- const fragment = [
33096
- "uniform sampler2D uSampler;",
33097
- "varying vec4 vColor;",
33098
- "varying vec2 vRegion;",
33099
- // user-provided fragment body (uniforms + apply function)
33100
- fragmentBody,
33101
- "void main(void) {",
33102
- " vec4 texColor = texture2D(uSampler, vRegion) * vColor;",
33103
- " gl_FragColor = apply(texColor, vRegion);",
33104
- "}"
33105
- ].join("\n");
33106
- this._shader = new GLShader(
33107
- renderer2.gl,
33108
- quad_default,
33109
- fragment,
33110
- precision || renderer2.shaderPrecision
33111
- );
33112
- this.enabled = true;
33113
- }
33114
- /**
33115
- * Set the uniform to the given value
33116
- * @param {string} name - the uniform name
33117
- * @param {object|Float32Array} value - the value to assign to that uniform
33118
- */
33119
- setUniform(name, value) {
33120
- if (this.enabled) {
33121
- this._shader.setUniform(name, value);
33122
- }
33123
- }
33124
- /** @ignore */
33125
- bind() {
33126
- if (this.enabled) {
33127
- this._shader.bind();
33128
- }
33129
- }
33130
- /** @ignore */
33131
- getAttribLocation(name) {
33132
- return this.enabled ? this._shader.getAttribLocation(name) : -1;
33133
- }
33134
- /** @ignore */
33135
- setVertexAttributes(gl, attributes, stride) {
33136
- if (this.enabled) {
33137
- this._shader.setVertexAttributes(gl, attributes, stride);
33138
- }
33139
- }
33140
- /** @ignore */
33141
- get program() {
33142
- return this.enabled ? this._shader.program : null;
33143
- }
33144
- /** @ignore */
33145
- get vertex() {
33146
- return this.enabled ? this._shader.vertex : null;
33147
- }
33148
- /** @ignore */
33149
- get fragment() {
33150
- return this.enabled ? this._shader.fragment : null;
33151
- }
33152
- /** @ignore */
33153
- get attributes() {
33154
- return this.enabled ? this._shader.attributes : {};
33155
- }
33156
- /** @ignore */
33157
- get uniforms() {
33158
- return this.enabled ? this._shader.uniforms : {};
33159
- }
33160
- /**
33161
- * destroy this shader effect
33162
- */
33163
- destroy() {
33164
- if (this.enabled) {
33165
- this._shader.destroy();
33166
- this.enabled = false;
33167
- }
33168
- }
33169
- };
33170
-
33171
35023
  // src/video/webgl/effects/blur.js
33172
35024
  var BlurEffect = class extends ShaderEffect {
33173
35025
  /**
@@ -33270,25 +35122,16 @@ var ChromaticAberrationEffect = class extends ShaderEffect {
33270
35122
  };
33271
35123
 
33272
35124
  // src/video/webgl/effects/desaturate.js
33273
- var DesaturateEffect = class extends ShaderEffect {
35125
+ var DesaturateEffect = class extends ColorMatrixEffect {
33274
35126
  /**
33275
35127
  * @param {import("../webgl_renderer.js").default} renderer - the current renderer instance
33276
35128
  * @param {object} [options] - effect options
33277
35129
  * @param {number} [options.intensity=1.0] - desaturation intensity (0.0 = full color, 1.0 = grayscale)
33278
35130
  */
33279
35131
  constructor(renderer2, options = {}) {
33280
- super(
33281
- renderer2,
33282
- `
33283
- uniform float uDesatIntensity;
33284
- vec4 apply(vec4 color, vec2 uv) {
33285
- float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
33286
- return vec4(mix(color.rgb, vec3(gray), uDesatIntensity), color.a);
33287
- }
33288
- `
33289
- );
35132
+ super(renderer2);
33290
35133
  this.intensity = typeof options.intensity === "number" ? options.intensity : 1;
33291
- this.setUniform("uDesatIntensity", this.intensity);
35134
+ this.saturate(1 - this.intensity);
33292
35135
  }
33293
35136
  /**
33294
35137
  * set the desaturation intensity
@@ -33296,7 +35139,7 @@ var DesaturateEffect = class extends ShaderEffect {
33296
35139
  */
33297
35140
  setIntensity(value) {
33298
35141
  this.intensity = Math.max(0, Math.min(1, value));
33299
- this.setUniform("uDesatIntensity", this.intensity);
35142
+ this.reset().saturate(1 - this.intensity);
33300
35143
  }
33301
35144
  };
33302
35145
 
@@ -33607,25 +35450,16 @@ var HologramEffect = class extends ShaderEffect {
33607
35450
  };
33608
35451
 
33609
35452
  // src/video/webgl/effects/invert.js
33610
- var InvertEffect = class extends ShaderEffect {
35453
+ var InvertEffect = class extends ColorMatrixEffect {
33611
35454
  /**
33612
35455
  * @param {import("../webgl_renderer.js").default} renderer - the current renderer instance
33613
35456
  * @param {object} [options] - effect options
33614
35457
  * @param {number} [options.intensity=1.0] - inversion intensity (0.0 = original, 1.0 = fully inverted)
33615
35458
  */
33616
35459
  constructor(renderer2, options = {}) {
33617
- super(
33618
- renderer2,
33619
- `
33620
- uniform float uInvertIntensity;
33621
- vec4 apply(vec4 color, vec2 uv) {
33622
- vec3 inverted = vec3(color.a) - color.rgb;
33623
- return vec4(mix(color.rgb, inverted, uInvertIntensity), color.a);
33624
- }
33625
- `
33626
- );
35460
+ super(renderer2);
33627
35461
  this.intensity = typeof options.intensity === "number" ? options.intensity : 1;
33628
- this.setUniform("uInvertIntensity", this.intensity);
35462
+ this.invertColors(this.intensity);
33629
35463
  }
33630
35464
  /**
33631
35465
  * set the inversion intensity
@@ -33633,7 +35467,7 @@ var InvertEffect = class extends ShaderEffect {
33633
35467
  */
33634
35468
  setIntensity(value) {
33635
35469
  this.intensity = Math.max(0, Math.min(1, value));
33636
- this.setUniform("uInvertIntensity", this.intensity);
35470
+ this.reset().invertColors(this.intensity);
33637
35471
  }
33638
35472
  };
33639
35473
 
@@ -33823,28 +35657,16 @@ var ScanlineEffect = class extends ShaderEffect {
33823
35657
  };
33824
35658
 
33825
35659
  // src/video/webgl/effects/sepia.js
33826
- var SepiaEffect = class extends ShaderEffect {
35660
+ var SepiaEffect = class extends ColorMatrixEffect {
33827
35661
  /**
33828
35662
  * @param {import("../webgl_renderer.js").default} renderer - the current renderer instance
33829
35663
  * @param {object} [options] - effect options
33830
35664
  * @param {number} [options.intensity=1.0] - sepia intensity (0.0 = original, 1.0 = full sepia)
33831
35665
  */
33832
35666
  constructor(renderer2, options = {}) {
33833
- super(
33834
- renderer2,
33835
- `
33836
- uniform float uSepiaIntensity;
33837
- vec4 apply(vec4 color, vec2 uv) {
33838
- vec3 sepia;
33839
- sepia.r = dot(color.rgb, vec3(0.393, 0.769, 0.189));
33840
- sepia.g = dot(color.rgb, vec3(0.349, 0.686, 0.168));
33841
- sepia.b = dot(color.rgb, vec3(0.272, 0.534, 0.131));
33842
- return vec4(mix(color.rgb, sepia, uSepiaIntensity), color.a);
33843
- }
33844
- `
33845
- );
35667
+ super(renderer2);
33846
35668
  this.intensity = typeof options.intensity === "number" ? options.intensity : 1;
33847
- this.setUniform("uSepiaIntensity", this.intensity);
35669
+ this.sepia(this.intensity);
33848
35670
  }
33849
35671
  /**
33850
35672
  * set the sepia intensity
@@ -33852,7 +35674,7 @@ var SepiaEffect = class extends ShaderEffect {
33852
35674
  */
33853
35675
  setIntensity(value) {
33854
35676
  this.intensity = Math.max(0, Math.min(1, value));
33855
- this.setUniform("uSepiaIntensity", this.intensity);
35677
+ this.reset().sepia(this.intensity);
33856
35678
  }
33857
35679
  };
33858
35680
 
@@ -33901,6 +35723,50 @@ var TintPulseEffect = class extends ShaderEffect {
33901
35723
  }
33902
35724
  };
33903
35725
 
35726
+ // src/video/webgl/effects/vignette.js
35727
+ var VignetteEffect = class extends ShaderEffect {
35728
+ /**
35729
+ * @param {import("../webgl_renderer.js").default} renderer - the current renderer instance
35730
+ * @param {object} [options] - effect options
35731
+ * @param {number} [options.strength=0.15] - edge darkening power (lower = stronger darkening, higher = softer falloff)
35732
+ * @param {number} [options.size=25.0] - vignette spread multiplier (higher = smaller dark area)
35733
+ */
35734
+ constructor(renderer2, options = {}) {
35735
+ super(
35736
+ renderer2,
35737
+ `
35738
+ uniform float uStrength;
35739
+ uniform float uSize;
35740
+ vec4 apply(vec4 color, vec2 uv) {
35741
+ vec2 vig = uv * (1.0 - uv);
35742
+ float v = clamp(pow(vig.x * vig.y * uSize, uStrength), 0.0, 1.0);
35743
+ return vec4(color.rgb * v, color.a);
35744
+ }
35745
+ `
35746
+ );
35747
+ this.strength = options.strength ?? 0.15;
35748
+ this.size = options.size ?? 25;
35749
+ this.setUniform("uStrength", this.strength);
35750
+ this.setUniform("uSize", this.size);
35751
+ }
35752
+ /**
35753
+ * set the vignette strength
35754
+ * @param {number} strength - edge darkening power (lower = stronger, higher = softer)
35755
+ */
35756
+ setStrength(strength) {
35757
+ this.strength = Math.max(0, strength);
35758
+ this.setUniform("uStrength", this.strength);
35759
+ }
35760
+ /**
35761
+ * set the vignette size
35762
+ * @param {number} size - spread multiplier (higher = smaller dark area)
35763
+ */
35764
+ setSize(size) {
35765
+ this.size = Math.max(0, size);
35766
+ this.setUniform("uSize", this.size);
35767
+ }
35768
+ };
35769
+
33904
35770
  // src/video/webgl/effects/wave.js
33905
35771
  var WaveEffect = class extends ShaderEffect {
33906
35772
  /**
@@ -34485,6 +36351,7 @@ export {
34485
36351
  Bounds,
34486
36352
  CANVAS,
34487
36353
  Camera2d,
36354
+ CameraEffect,
34488
36355
  canvasrendertarget_default as CanvasRenderTarget,
34489
36356
  CanvasRenderer,
34490
36357
  CanvasTexture,
@@ -34492,6 +36359,8 @@ export {
34492
36359
  Collectable,
34493
36360
  Color,
34494
36361
  ColorLayer,
36362
+ ColorMatrix,
36363
+ ColorMatrixEffect,
34495
36364
  Compositor,
34496
36365
  Container,
34497
36366
  DesaturateEffect,
@@ -34501,6 +36370,7 @@ export {
34501
36370
  DropTarget,
34502
36371
  Ellipse,
34503
36372
  Entity,
36373
+ FadeEffect,
34504
36374
  FlashEffect,
34505
36375
  GLShader,
34506
36376
  GlowEffect,
@@ -34510,6 +36380,7 @@ export {
34510
36380
  InvertEffect,
34511
36381
  Light2d,
34512
36382
  Line,
36383
+ MaskEffect,
34513
36384
  math_exports as Math,
34514
36385
  Matrix2d,
34515
36386
  Matrix3d,
@@ -34533,6 +36404,7 @@ export {
34533
36404
  QuadTree,
34534
36405
  Rect,
34535
36406
  RenderState,
36407
+ RenderTarget,
34536
36408
  Renderable,
34537
36409
  Renderer,
34538
36410
  RoundRect,
@@ -34540,6 +36412,7 @@ export {
34540
36412
  ScanlineEffect,
34541
36413
  SepiaEffect,
34542
36414
  ShaderEffect,
36415
+ ShakeEffect,
34543
36416
  Sprite,
34544
36417
  Stage,
34545
36418
  TMXHexagonalRenderer,
@@ -34564,6 +36437,7 @@ export {
34564
36437
  UITextButton,
34565
36438
  Vector2d,
34566
36439
  Vector3d,
36440
+ VignetteEffect,
34567
36441
  WEBGL,
34568
36442
  WaveEffect,
34569
36443
  WebGLRenderer,