@codexo/exojs 0.6.4 → 0.6.5

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 (43) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/dist/esm/core/Application.d.ts +0 -1
  3. package/dist/esm/core/Application.js +0 -1
  4. package/dist/esm/core/Application.js.map +1 -1
  5. package/dist/esm/core/SceneManager.js +11 -12
  6. package/dist/esm/core/SceneManager.js.map +1 -1
  7. package/dist/esm/index.js +0 -5
  8. package/dist/esm/index.js.map +1 -1
  9. package/dist/esm/math/geometry.d.ts +12 -8
  10. package/dist/esm/math/geometry.js +119 -72
  11. package/dist/esm/math/geometry.js.map +1 -1
  12. package/dist/esm/rendering/index.d.ts +0 -5
  13. package/dist/esm/rendering/primitives/Graphics.d.ts +3 -2
  14. package/dist/esm/rendering/primitives/Graphics.js +33 -25
  15. package/dist/esm/rendering/primitives/Graphics.js.map +1 -1
  16. package/dist/esm/rendering/webgl2/WebGl2Backend.js +1 -4
  17. package/dist/esm/rendering/webgl2/WebGl2Backend.js.map +1 -1
  18. package/dist/esm/rendering/webgpu/WebGpuBackend.js +0 -3
  19. package/dist/esm/rendering/webgpu/WebGpuBackend.js.map +1 -1
  20. package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.js +1 -2
  21. package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.js.map +1 -1
  22. package/dist/exo.esm.js +586 -1274
  23. package/dist/exo.esm.js.map +1 -1
  24. package/package.json +1 -1
  25. package/dist/esm/rendering/primitives/CircleGeometry.d.ts +0 -4
  26. package/dist/esm/rendering/primitives/CircleGeometry.js +0 -21
  27. package/dist/esm/rendering/primitives/CircleGeometry.js.map +0 -1
  28. package/dist/esm/rendering/primitives/DrawableShape.d.ts +0 -11
  29. package/dist/esm/rendering/primitives/DrawableShape.js +0 -21
  30. package/dist/esm/rendering/primitives/DrawableShape.js.map +0 -1
  31. package/dist/esm/rendering/primitives/Geometry.d.ts +0 -13
  32. package/dist/esm/rendering/primitives/Geometry.js +0 -16
  33. package/dist/esm/rendering/primitives/Geometry.js.map +0 -1
  34. package/dist/esm/rendering/webgl2/WebGl2PrimitiveRenderer.d.ts +0 -26
  35. package/dist/esm/rendering/webgl2/WebGl2PrimitiveRenderer.js +0 -222
  36. package/dist/esm/rendering/webgl2/WebGl2PrimitiveRenderer.js.map +0 -1
  37. package/dist/esm/rendering/webgl2/glsl/primitive.frag.js +0 -4
  38. package/dist/esm/rendering/webgl2/glsl/primitive.frag.js.map +0 -1
  39. package/dist/esm/rendering/webgl2/glsl/primitive.vert.js +0 -4
  40. package/dist/esm/rendering/webgl2/glsl/primitive.vert.js.map +0 -1
  41. package/dist/esm/rendering/webgpu/WebGpuPrimitiveRenderer.d.ts +0 -38
  42. package/dist/esm/rendering/webgpu/WebGpuPrimitiveRenderer.js +0 -488
  43. package/dist/esm/rendering/webgpu/WebGpuPrimitiveRenderer.js.map +0 -1
package/dist/exo.esm.js CHANGED
@@ -3642,37 +3642,117 @@ class Drawable extends RenderNode {
3642
3642
  }
3643
3643
  }
3644
3644
 
3645
- class DrawableShape extends Drawable {
3646
- geometry;
3647
- drawMode;
3648
- color;
3649
- constructor(geometry, color, drawMode = RenderingPrimitives.Triangles) {
3645
+ /**
3646
+ * Arbitrary 2D triangle-mesh primitive.
3647
+ *
3648
+ * `Mesh` lives alongside {@link Sprite} as a public Drawable: it has the
3649
+ * same transform (position/rotation/scale/origin), tint, blendMode,
3650
+ * filters, masks, and cacheAsBitmap — but the geometry it renders is
3651
+ * user-supplied rather than implied by a texture frame. The intended use
3652
+ * cases are:
3653
+ *
3654
+ * - Custom-shape sprites whose silhouette isn't a quad (badges, speech
3655
+ * bubbles, region overlays).
3656
+ * - Deformable visuals (rope/ribbon, banner, water surface): mutate the
3657
+ * vertex array between frames and the GPU re-tessellates nothing —
3658
+ * only the transform changes per frame.
3659
+ * - Particles or trails with custom geometry per emitter.
3660
+ *
3661
+ * The mesh data is **immutable after construction** in v1: vertex /
3662
+ * index / UV / color arrays are exposed as readonly references. Mutate
3663
+ * the underlying typed arrays in-place if you need per-frame updates,
3664
+ * but the array lengths and topology cannot change. Texture is the only
3665
+ * post-construction mutable property.
3666
+ *
3667
+ * The vertex stream is a flat `Float32Array` of (x, y) pairs in local
3668
+ * space. The mesh's local bounds are computed once at construction from
3669
+ * the AABB of those vertices and used by the cull pass. Re-computing
3670
+ * after in-place mutation is the caller's responsibility (call
3671
+ * `recomputeLocalBounds()`).
3672
+ */
3673
+ class Mesh extends Drawable {
3674
+ vertices;
3675
+ indices;
3676
+ uvs;
3677
+ colors;
3678
+ _texture;
3679
+ constructor(options) {
3650
3680
  super();
3651
- this.geometry = geometry;
3652
- this.color = color.clone();
3653
- this.drawMode = drawMode;
3681
+ const { vertices, indices = null, uvs = null, colors = null, texture = null } = options;
3682
+ if (vertices.length === 0 || vertices.length % 2 !== 0) {
3683
+ throw new Error(`Mesh vertices must be a non-empty flat array of (x,y) pairs (got length ${vertices.length}).`);
3684
+ }
3685
+ const vertexCount = vertices.length / 2;
3686
+ if (vertexCount < 3) {
3687
+ throw new Error(`Mesh requires at least 3 vertices (got ${vertexCount}).`);
3688
+ }
3689
+ if (uvs !== null && uvs.length !== vertices.length) {
3690
+ throw new Error(`Mesh uvs length ${uvs.length} must equal vertices length ${vertices.length}.`);
3691
+ }
3692
+ if (colors !== null && colors.length !== vertexCount) {
3693
+ throw new Error(`Mesh colors length ${colors.length} must equal vertex count ${vertexCount}.`);
3694
+ }
3695
+ if (indices !== null) {
3696
+ if (indices.length === 0 || indices.length % 3 !== 0) {
3697
+ throw new Error(`Mesh indices must be a non-empty multiple of 3 (got length ${indices.length}).`);
3698
+ }
3699
+ for (let i = 0; i < indices.length; i++) {
3700
+ if (indices[i] >= vertexCount) {
3701
+ throw new Error(`Mesh index ${indices[i]} at position ${i} is out of range for vertex count ${vertexCount}.`);
3702
+ }
3703
+ }
3704
+ }
3705
+ else if (vertexCount % 3 !== 0) {
3706
+ throw new Error(`Non-indexed Mesh requires a vertex count that is a multiple of 3 (got ${vertexCount}).`);
3707
+ }
3708
+ this.vertices = vertices;
3709
+ this.indices = indices;
3710
+ this.uvs = uvs;
3711
+ this.colors = colors;
3712
+ this._texture = texture;
3713
+ this.recomputeLocalBounds();
3654
3714
  }
3655
- destroy() {
3656
- super.destroy();
3657
- this.color.destroy();
3715
+ get vertexCount() {
3716
+ return this.vertices.length / 2;
3658
3717
  }
3659
- }
3660
-
3661
- class Geometry {
3662
- vertices;
3663
- indices;
3664
- points;
3665
- constructor({ vertices = [], indices = [], points = [], } = {}) {
3666
- this.vertices = new Float32Array(vertices);
3667
- this.indices = new Uint16Array(indices);
3668
- this.points = points;
3718
+ get indexCount() {
3719
+ return this.indices?.length ?? this.vertexCount;
3669
3720
  }
3670
- destroy() {
3671
- // todo - check if destroy is needed
3721
+ get texture() {
3722
+ return this._texture;
3723
+ }
3724
+ set texture(texture) {
3725
+ this._texture = texture;
3726
+ this.invalidateCache();
3727
+ }
3728
+ /**
3729
+ * Recompute the local AABB from the current vertex array. Call after
3730
+ * mutating `vertices` in place to keep culling correct; otherwise the
3731
+ * bounds the cull pass sees will be the AABB at construction time.
3732
+ */
3733
+ recomputeLocalBounds() {
3734
+ let minX = Infinity;
3735
+ let minY = Infinity;
3736
+ let maxX = -Infinity;
3737
+ let maxY = -Infinity;
3738
+ for (let i = 0; i < this.vertices.length; i += 2) {
3739
+ const x = this.vertices[i];
3740
+ const y = this.vertices[i + 1];
3741
+ if (x < minX)
3742
+ minX = x;
3743
+ if (x > maxX)
3744
+ maxX = x;
3745
+ if (y < minY)
3746
+ minY = y;
3747
+ if (y > maxY)
3748
+ maxY = y;
3749
+ }
3750
+ this.localBounds.set(minX, minY, maxX - minX, maxY - minY);
3751
+ return this;
3672
3752
  }
3673
3753
  }
3674
3754
 
3675
- class TransitionOverlayShape extends DrawableShape {
3755
+ class TransitionOverlayMesh extends Mesh {
3676
3756
  render(backend) {
3677
3757
  if (this.visible) {
3678
3758
  backend.draw(this);
@@ -3680,16 +3760,16 @@ class TransitionOverlayShape extends DrawableShape {
3680
3760
  return this;
3681
3761
  }
3682
3762
  }
3683
- const createOverlayGeometry = () => new Geometry({
3684
- vertices: [0, 0, 1, 0, 0, 1, 1, 1],
3685
- indices: [0, 1, 2, 3],
3686
- points: [0, 0, 1, 0, 0, 1, 1, 1],
3763
+ const createOverlayMesh = () => new TransitionOverlayMesh({
3764
+ // 4 vertices (TL, TR, BL, BR) with 2 indexed triangles forming a screen quad.
3765
+ vertices: new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]),
3766
+ indices: new Uint16Array([0, 1, 2, 1, 3, 2]),
3687
3767
  });
3688
3768
  const defaultFadeTransitionDuration = 220;
3689
3769
  class SceneManager {
3690
3770
  _app;
3691
3771
  _stack = [];
3692
- _transitionOverlay = new TransitionOverlayShape(createOverlayGeometry(), Color.black, RenderingPrimitives.TriangleStrip);
3772
+ _transitionOverlay = createOverlayMesh();
3693
3773
  _transition = null;
3694
3774
  onChangeScene = new Signal();
3695
3775
  onStartScene = new Signal();
@@ -4052,7 +4132,8 @@ class SceneManager {
4052
4132
  const overlayColor = transition ? transition.color : Color.black;
4053
4133
  const backend = this._app.backend;
4054
4134
  const bounds = backend.view.getBounds();
4055
- const vertices = this._transitionOverlay.geometry.vertices;
4135
+ const overlay = this._transitionOverlay;
4136
+ const vertices = overlay.vertices;
4056
4137
  vertices[0] = bounds.left;
4057
4138
  vertices[1] = bounds.top;
4058
4139
  vertices[2] = bounds.right;
@@ -4061,8 +4142,8 @@ class SceneManager {
4061
4142
  vertices[5] = bounds.bottom;
4062
4143
  vertices[6] = bounds.right;
4063
4144
  vertices[7] = bounds.bottom;
4064
- this._transitionOverlay.color.set(overlayColor.r, overlayColor.g, overlayColor.b, Math.max(0, Math.min(1, alpha)));
4065
- this._transitionOverlay.render(backend);
4145
+ overlay.tint.set(overlayColor.r, overlayColor.g, overlayColor.b, Math.max(0, Math.min(1, alpha)));
4146
+ overlay.render(backend);
4066
4147
  }
4067
4148
  }
4068
4149
 
@@ -5834,9 +5915,9 @@ class WebGl2VertexArrayObject {
5834
5915
  }
5835
5916
  }
5836
5917
 
5837
- var vertexSource$4 = "#version 300 es\nprecision lowp float;\nprecision lowp int;\n\n// Per-instance attributes (divisor = 1). Each Sprite contributes one entry\n// to the per-instance buffer; gl_VertexID 0..3 selects which corner of the\n// quad this invocation is computing.\nlayout(location = 0) in vec4 a_localBounds; // left, top, right, bottom (local space)\nlayout(location = 1) in vec3 a_transformAB; // a, b, x — first row of 2D affine\nlayout(location = 2) in vec3 a_transformCD; // c, d, y — second row\nlayout(location = 3) in vec4 a_uvBounds; // uMin, vMin, uMax, vMax (normalised, already flipY-swapped)\nlayout(location = 4) in vec4 a_color; // RGBA tint\nlayout(location = 5) in uint a_textureSlot;\n\nuniform mat3 u_projection;\n\nout vec2 v_texcoord;\nout vec4 v_color;\nflat out uint v_textureSlot;\n\nvoid main(void) {\n // gl_VertexID 0..3 → corner: 0=TL, 1=TR, 2=BL, 3=BR (TRIANGLE_STRIP order)\n int vid = gl_VertexID;\n int cornerX = vid & 1;\n int cornerY = (vid >> 1) & 1;\n\n // Local-space corner: pick from the bounds rectangle.\n float localX = (cornerX == 0) ? a_localBounds.x : a_localBounds.z;\n float localY = (cornerY == 0) ? a_localBounds.y : a_localBounds.w;\n\n // Apply the per-instance affine transform: world = M * (localX, localY, 1)\n float worldX = (a_transformAB.x * localX) + (a_transformAB.y * localY) + a_transformAB.z;\n float worldY = (a_transformCD.x * localX) + (a_transformCD.y * localY) + a_transformCD.z;\n\n gl_Position = vec4((u_projection * vec3(worldX, worldY, 1.0)).xy, 0.0, 1.0);\n\n // UV: pick from the bounds rectangle. The CPU pre-swaps Y bounds when\n // the texture is flipY, so the shader doesn't have to know.\n float u = (cornerX == 0) ? a_uvBounds.x : a_uvBounds.z;\n float v = (cornerY == 0) ? a_uvBounds.y : a_uvBounds.w;\n v_texcoord = vec2(u, v);\n\n v_color = vec4(a_color.rgb * a_color.a, a_color.a);\n v_textureSlot = a_textureSlot;\n}\n";
5918
+ var vertexSource$3 = "#version 300 es\nprecision lowp float;\nprecision lowp int;\n\n// Per-instance attributes (divisor = 1). Each Sprite contributes one entry\n// to the per-instance buffer; gl_VertexID 0..3 selects which corner of the\n// quad this invocation is computing.\nlayout(location = 0) in vec4 a_localBounds; // left, top, right, bottom (local space)\nlayout(location = 1) in vec3 a_transformAB; // a, b, x — first row of 2D affine\nlayout(location = 2) in vec3 a_transformCD; // c, d, y — second row\nlayout(location = 3) in vec4 a_uvBounds; // uMin, vMin, uMax, vMax (normalised, already flipY-swapped)\nlayout(location = 4) in vec4 a_color; // RGBA tint\nlayout(location = 5) in uint a_textureSlot;\n\nuniform mat3 u_projection;\n\nout vec2 v_texcoord;\nout vec4 v_color;\nflat out uint v_textureSlot;\n\nvoid main(void) {\n // gl_VertexID 0..3 → corner: 0=TL, 1=TR, 2=BL, 3=BR (TRIANGLE_STRIP order)\n int vid = gl_VertexID;\n int cornerX = vid & 1;\n int cornerY = (vid >> 1) & 1;\n\n // Local-space corner: pick from the bounds rectangle.\n float localX = (cornerX == 0) ? a_localBounds.x : a_localBounds.z;\n float localY = (cornerY == 0) ? a_localBounds.y : a_localBounds.w;\n\n // Apply the per-instance affine transform: world = M * (localX, localY, 1)\n float worldX = (a_transformAB.x * localX) + (a_transformAB.y * localY) + a_transformAB.z;\n float worldY = (a_transformCD.x * localX) + (a_transformCD.y * localY) + a_transformCD.z;\n\n gl_Position = vec4((u_projection * vec3(worldX, worldY, 1.0)).xy, 0.0, 1.0);\n\n // UV: pick from the bounds rectangle. The CPU pre-swaps Y bounds when\n // the texture is flipY, so the shader doesn't have to know.\n float u = (cornerX == 0) ? a_uvBounds.x : a_uvBounds.z;\n float v = (cornerY == 0) ? a_uvBounds.y : a_uvBounds.w;\n v_texcoord = vec2(u, v);\n\n v_color = vec4(a_color.rgb * a_color.a, a_color.a);\n v_textureSlot = a_textureSlot;\n}\n";
5838
5919
 
5839
- var fragmentSource$4 = "#version 300 es\nprecision lowp float;\nprecision lowp int;\n\n// Multi-texture sprite batching: up to 8 textures bound per draw call,\n// each fragment selects its source via a flat-interpolated slot index.\n//\n// GLSL ES 3.0 forbids non-constant array-of-sampler indexing unless the\n// expression is dynamically uniform — which a per-vertex slot is not\n// once different triangles in the same batch carry different slots. The\n// if/else chain below dispatches statically and dodges that constraint.\n\nuniform sampler2D u_texture0;\nuniform sampler2D u_texture1;\nuniform sampler2D u_texture2;\nuniform sampler2D u_texture3;\nuniform sampler2D u_texture4;\nuniform sampler2D u_texture5;\nuniform sampler2D u_texture6;\nuniform sampler2D u_texture7;\n\nin vec2 v_texcoord;\nin vec4 v_color;\nflat in uint v_textureSlot;\n\nlayout(location = 0) out vec4 fragColor;\n\nvoid main(void) {\n vec4 sampleColor;\n\n if (v_textureSlot == 0u) {\n sampleColor = texture(u_texture0, v_texcoord);\n } else if (v_textureSlot == 1u) {\n sampleColor = texture(u_texture1, v_texcoord);\n } else if (v_textureSlot == 2u) {\n sampleColor = texture(u_texture2, v_texcoord);\n } else if (v_textureSlot == 3u) {\n sampleColor = texture(u_texture3, v_texcoord);\n } else if (v_textureSlot == 4u) {\n sampleColor = texture(u_texture4, v_texcoord);\n } else if (v_textureSlot == 5u) {\n sampleColor = texture(u_texture5, v_texcoord);\n } else if (v_textureSlot == 6u) {\n sampleColor = texture(u_texture6, v_texcoord);\n } else {\n sampleColor = texture(u_texture7, v_texcoord);\n }\n\n fragColor = sampleColor * v_color;\n}\n";
5920
+ var fragmentSource$3 = "#version 300 es\nprecision lowp float;\nprecision lowp int;\n\n// Multi-texture sprite batching: up to 8 textures bound per draw call,\n// each fragment selects its source via a flat-interpolated slot index.\n//\n// GLSL ES 3.0 forbids non-constant array-of-sampler indexing unless the\n// expression is dynamically uniform — which a per-vertex slot is not\n// once different triangles in the same batch carry different slots. The\n// if/else chain below dispatches statically and dodges that constraint.\n\nuniform sampler2D u_texture0;\nuniform sampler2D u_texture1;\nuniform sampler2D u_texture2;\nuniform sampler2D u_texture3;\nuniform sampler2D u_texture4;\nuniform sampler2D u_texture5;\nuniform sampler2D u_texture6;\nuniform sampler2D u_texture7;\n\nin vec2 v_texcoord;\nin vec4 v_color;\nflat in uint v_textureSlot;\n\nlayout(location = 0) out vec4 fragColor;\n\nvoid main(void) {\n vec4 sampleColor;\n\n if (v_textureSlot == 0u) {\n sampleColor = texture(u_texture0, v_texcoord);\n } else if (v_textureSlot == 1u) {\n sampleColor = texture(u_texture1, v_texcoord);\n } else if (v_textureSlot == 2u) {\n sampleColor = texture(u_texture2, v_texcoord);\n } else if (v_textureSlot == 3u) {\n sampleColor = texture(u_texture3, v_texcoord);\n } else if (v_textureSlot == 4u) {\n sampleColor = texture(u_texture4, v_texcoord);\n } else if (v_textureSlot == 5u) {\n sampleColor = texture(u_texture5, v_texcoord);\n } else if (v_textureSlot == 6u) {\n sampleColor = texture(u_texture6, v_texcoord);\n } else {\n sampleColor = texture(u_texture7, v_texcoord);\n }\n\n fragColor = sampleColor * v_color;\n}\n";
5840
5921
 
5841
5922
  /**
5842
5923
  * Instanced sprite renderer for WebGL2.
@@ -5884,7 +5965,7 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
5884
5965
  constructor(batchSize) {
5885
5966
  super();
5886
5967
  this._batchSize = batchSize;
5887
- this._shader = new Shader(vertexSource$4, fragmentSource$4);
5968
+ this._shader = new Shader(vertexSource$3, fragmentSource$3);
5888
5969
  this._instanceData = new ArrayBuffer(batchSize * instanceStrideBytes$3);
5889
5970
  this._instanceFloat32 = new Float32Array(this._instanceData);
5890
5971
  this._instanceUint32 = new Uint32Array(this._instanceData);
@@ -6309,26 +6390,26 @@ class Texture {
6309
6390
  }
6310
6391
  }
6311
6392
 
6312
- var vertexSource$3 = "#version 300 es\nprecision lowp float;\n\nlayout(location = 0) in vec2 a_position;\nlayout(location = 1) in vec2 a_texcoord;\nlayout(location = 2) in vec4 a_color;\n\nuniform mat3 u_projection;\nuniform mat3 u_translation;\n\nout vec2 v_texcoord;\nout vec4 v_color;\n\nvoid main(void) {\n gl_Position = vec4((u_projection * u_translation * vec3(a_position, 1.0)).xy, 0.0, 1.0);\n v_texcoord = a_texcoord;\n v_color = a_color;\n}\n";
6393
+ var vertexSource$2 = "#version 300 es\nprecision lowp float;\n\nlayout(location = 0) in vec2 a_position;\nlayout(location = 1) in vec2 a_texcoord;\nlayout(location = 2) in vec4 a_color;\n\nuniform mat3 u_projection;\nuniform mat3 u_translation;\n\nout vec2 v_texcoord;\nout vec4 v_color;\n\nvoid main(void) {\n gl_Position = vec4((u_projection * u_translation * vec3(a_position, 1.0)).xy, 0.0, 1.0);\n v_texcoord = a_texcoord;\n v_color = a_color;\n}\n";
6313
6394
 
6314
- var fragmentSource$3 = "#version 300 es\nprecision lowp float;\n\nuniform sampler2D u_texture;\nuniform vec4 u_tint;\n\nin vec2 v_texcoord;\nin vec4 v_color;\n\nlayout(location = 0) out vec4 fragColor;\n\nvoid main(void) {\n vec4 base = texture(u_texture, v_texcoord) * v_color * u_tint;\n fragColor = vec4(base.rgb * base.a, base.a);\n}\n";
6395
+ var fragmentSource$2 = "#version 300 es\nprecision lowp float;\n\nuniform sampler2D u_texture;\nuniform vec4 u_tint;\n\nin vec2 v_texcoord;\nin vec4 v_color;\n\nlayout(location = 0) out vec4 fragColor;\n\nvoid main(void) {\n vec4 base = texture(u_texture, v_texcoord) * v_color * u_tint;\n fragColor = vec4(base.rgb * base.a, base.a);\n}\n";
6315
6396
 
6316
6397
  // Per-vertex layout (20 bytes):
6317
6398
  // position: vec2 f32 (offset 0, 8 bytes)
6318
6399
  // texcoord: vec2 f32 (offset 8, 8 bytes)
6319
6400
  // color: u8x4 norm (offset 16, 4 bytes)
6320
- const vertexStrideBytes$5 = 20;
6321
- const vertexStrideWords$1 = vertexStrideBytes$5 / 4;
6401
+ const vertexStrideBytes$3 = 20;
6402
+ const vertexStrideWords = vertexStrideBytes$3 / 4;
6322
6403
  const initialVertexCapacity = 64;
6323
6404
  const initialIndexCapacity = 192;
6324
6405
  const defaultVertexColor = 0xFFFFFFFF; // white, full alpha
6325
6406
  class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
6326
- _shader = new Shader(vertexSource$3, fragmentSource$3);
6407
+ _shader = new Shader(vertexSource$2, fragmentSource$2);
6327
6408
  _tintScratch = new Float32Array(4);
6328
6409
  _textureUnitScratch = new Int32Array([0]);
6329
6410
  _vertexCapacity = initialVertexCapacity;
6330
6411
  _indexCapacity = initialIndexCapacity;
6331
- _vertexData = new ArrayBuffer(initialVertexCapacity * vertexStrideBytes$5);
6412
+ _vertexData = new ArrayBuffer(initialVertexCapacity * vertexStrideBytes$3);
6332
6413
  _float32View = new Float32Array(this._vertexData);
6333
6414
  _uint32View = new Uint32Array(this._vertexData);
6334
6415
  _indexData = new Uint16Array(initialIndexCapacity);
@@ -6375,7 +6456,7 @@ class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
6375
6456
  const uvs = mesh.uvs;
6376
6457
  const colors = mesh.colors;
6377
6458
  for (let i = 0; i < vertexCount; i++) {
6378
- const word = i * vertexStrideWords$1;
6459
+ const word = i * vertexStrideWords;
6379
6460
  const pair = i * 2;
6380
6461
  this._float32View[word] = positions[pair];
6381
6462
  this._float32View[word + 1] = positions[pair + 1];
@@ -6401,7 +6482,7 @@ class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
6401
6482
  }
6402
6483
  this._shader.sync();
6403
6484
  backend.bindVertexArrayObject(connection.vao);
6404
- connection.vertexBuffer.upload(this._float32View.subarray(0, vertexCount * vertexStrideWords$1));
6485
+ connection.vertexBuffer.upload(this._float32View.subarray(0, vertexCount * vertexStrideWords));
6405
6486
  connection.indexBuffer.upload(this._indexData.subarray(0, indexCount));
6406
6487
  connection.vao.draw(indexCount, 0, RenderingPrimitives.Triangles);
6407
6488
  backend.stats.batches++;
@@ -6434,9 +6515,9 @@ class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
6434
6515
  this._shader.sync();
6435
6516
  const vao = new WebGl2VertexArrayObject()
6436
6517
  .addIndex(indexBuffer)
6437
- .addAttribute(vertexBuffer, this._shader.getAttribute('a_position'), gl.FLOAT, false, vertexStrideBytes$5, 0)
6438
- .addAttribute(vertexBuffer, this._shader.getAttribute('a_texcoord'), gl.FLOAT, false, vertexStrideBytes$5, 8)
6439
- .addAttribute(vertexBuffer, this._shader.getAttribute('a_color'), gl.UNSIGNED_BYTE, true, vertexStrideBytes$5, 16)
6518
+ .addAttribute(vertexBuffer, this._shader.getAttribute('a_position'), gl.FLOAT, false, vertexStrideBytes$3, 0)
6519
+ .addAttribute(vertexBuffer, this._shader.getAttribute('a_texcoord'), gl.FLOAT, false, vertexStrideBytes$3, 8)
6520
+ .addAttribute(vertexBuffer, this._shader.getAttribute('a_color'), gl.UNSIGNED_BYTE, true, vertexStrideBytes$3, 16)
6440
6521
  .connect(this._createVaoRuntime(gl, vaoHandle));
6441
6522
  this._connection = { gl, vao, vertexBuffer, indexBuffer };
6442
6523
  }
@@ -6461,7 +6542,7 @@ class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
6461
6542
  while (this._vertexCapacity < vertexCount) {
6462
6543
  this._vertexCapacity *= 2;
6463
6544
  }
6464
- this._vertexData = new ArrayBuffer(this._vertexCapacity * vertexStrideBytes$5);
6545
+ this._vertexData = new ArrayBuffer(this._vertexCapacity * vertexStrideBytes$3);
6465
6546
  this._float32View = new Float32Array(this._vertexData);
6466
6547
  this._uint32View = new Uint32Array(this._vertexData);
6467
6548
  }
@@ -6543,9 +6624,9 @@ class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
6543
6624
  }
6544
6625
  }
6545
6626
 
6546
- var vertexSource$2 = "#version 300 es\nprecision lowp float;\nprecision lowp int;\n\n// Per-instance attributes (one entry per particle, 24 bytes total).\nlayout(location = 0) in vec2 a_translation; // particle position in system-local space\nlayout(location = 1) in vec2 a_scale; // particle scale\nlayout(location = 2) in float a_rotation; // particle rotation in degrees\nlayout(location = 3) in vec4 a_color; // RGBA tint\n\nuniform mat3 u_projection;\nuniform mat3 u_systemTransform;\nuniform vec4 u_localBounds; // left, top, right, bottom (system.vertices)\nuniform vec4 u_uvBounds; // uMin, vMin, uMax, vMax (flipY-swapped)\n\nout vec2 v_texcoord;\nout vec4 v_color;\n\nvoid main(void) {\n // Static index buffer is [0,1,2,0,2,3] (triangle-list), so gl_VertexID 0..3\n // maps to TL, TR, BR, BL via the same bit math the sprite renderer uses.\n int vid = gl_VertexID;\n int cornerX = ((vid + 1) >> 1) & 1;\n int cornerY = vid >> 1;\n\n float localX = (cornerX == 0) ? u_localBounds.x : u_localBounds.z;\n float localY = (cornerY == 0) ? u_localBounds.y : u_localBounds.w;\n\n // Per-particle scale + rotation.\n vec2 rotation = vec2(sin(radians(a_rotation)), cos(radians(a_rotation)));\n vec2 transformed = vec2(\n (localX * (a_scale.x * rotation.y)) + (localY * (a_scale.y * rotation.x)),\n (localX * (a_scale.x * -rotation.x)) + (localY * (a_scale.y * rotation.y))\n );\n\n vec3 worldPos = vec3(transformed + a_translation, 1.0);\n\n gl_Position = vec4((u_projection * u_systemTransform * worldPos).xy, 0.0, 1.0);\n\n float u = (cornerX == 0) ? u_uvBounds.x : u_uvBounds.z;\n float v = (cornerY == 0) ? u_uvBounds.y : u_uvBounds.w;\n v_texcoord = vec2(u, v);\n\n v_color = vec4(a_color.rgb * a_color.a, a_color.a);\n}\n";
6627
+ var vertexSource$1 = "#version 300 es\nprecision lowp float;\nprecision lowp int;\n\n// Per-instance attributes (one entry per particle, 24 bytes total).\nlayout(location = 0) in vec2 a_translation; // particle position in system-local space\nlayout(location = 1) in vec2 a_scale; // particle scale\nlayout(location = 2) in float a_rotation; // particle rotation in degrees\nlayout(location = 3) in vec4 a_color; // RGBA tint\n\nuniform mat3 u_projection;\nuniform mat3 u_systemTransform;\nuniform vec4 u_localBounds; // left, top, right, bottom (system.vertices)\nuniform vec4 u_uvBounds; // uMin, vMin, uMax, vMax (flipY-swapped)\n\nout vec2 v_texcoord;\nout vec4 v_color;\n\nvoid main(void) {\n // Static index buffer is [0,1,2,0,2,3] (triangle-list), so gl_VertexID 0..3\n // maps to TL, TR, BR, BL via the same bit math the sprite renderer uses.\n int vid = gl_VertexID;\n int cornerX = ((vid + 1) >> 1) & 1;\n int cornerY = vid >> 1;\n\n float localX = (cornerX == 0) ? u_localBounds.x : u_localBounds.z;\n float localY = (cornerY == 0) ? u_localBounds.y : u_localBounds.w;\n\n // Per-particle scale + rotation.\n vec2 rotation = vec2(sin(radians(a_rotation)), cos(radians(a_rotation)));\n vec2 transformed = vec2(\n (localX * (a_scale.x * rotation.y)) + (localY * (a_scale.y * rotation.x)),\n (localX * (a_scale.x * -rotation.x)) + (localY * (a_scale.y * rotation.y))\n );\n\n vec3 worldPos = vec3(transformed + a_translation, 1.0);\n\n gl_Position = vec4((u_projection * u_systemTransform * worldPos).xy, 0.0, 1.0);\n\n float u = (cornerX == 0) ? u_uvBounds.x : u_uvBounds.z;\n float v = (cornerY == 0) ? u_uvBounds.y : u_uvBounds.w;\n v_texcoord = vec2(u, v);\n\n v_color = vec4(a_color.rgb * a_color.a, a_color.a);\n}\n";
6547
6628
 
6548
- var fragmentSource$2 = "#version 300 es\nprecision lowp float;\n\nuniform sampler2D u_texture;\n\nin vec2 v_texcoord;\nin vec4 v_color;\n\nlayout(location = 0) out vec4 fragColor;\n\nvoid main(void) {\n fragColor = texture(u_texture, v_texcoord) * v_color;\n}\n";
6629
+ var fragmentSource$1 = "#version 300 es\nprecision lowp float;\n\nuniform sampler2D u_texture;\n\nin vec2 v_texcoord;\nin vec4 v_color;\n\nlayout(location = 0) out vec4 fragColor;\n\nvoid main(void) {\n fragColor = texture(u_texture, v_texcoord) * v_color;\n}\n";
6549
6630
 
6550
6631
  /**
6551
6632
  * Instanced particle renderer for WebGL2.
@@ -6597,7 +6678,7 @@ class WebGl2ParticleRenderer extends AbstractWebGl2Renderer {
6597
6678
  constructor(batchSize) {
6598
6679
  super();
6599
6680
  this._batchSize = batchSize;
6600
- this._shader = new Shader(vertexSource$2, fragmentSource$2);
6681
+ this._shader = new Shader(vertexSource$1, fragmentSource$1);
6601
6682
  this._instanceData = new ArrayBuffer(batchSize * instanceStrideBytes$2);
6602
6683
  this._instanceFloat32 = new Float32Array(this._instanceData);
6603
6684
  this._instanceUint32 = new Uint32Array(this._instanceData);
@@ -6837,315 +6918,100 @@ class WebGl2ParticleRenderer extends AbstractWebGl2Renderer {
6837
6918
  }
6838
6919
  }
6839
6920
 
6840
- var vertexSource$1 = "#version 300 es\nprecision lowp float;\n\nlayout(location = 0) in vec2 a_position;\nlayout(location = 1) in vec4 a_color;\n\nuniform mat3 u_projection;\nuniform mat3 u_translation;\n\nout vec4 v_color;\n\nvoid main(void) {\n gl_Position = vec4((u_projection * u_translation * vec3(a_position, 1.0)).xy, 0.0, 1.0);\n v_color = vec4(a_color.rgb * a_color.a, a_color.a);\n}\n";
6841
-
6842
- var fragmentSource$1 = "#version 300 es\nprecision lowp float;\n\nlayout(location = 0) out vec4 fragColor;\n\nin vec4 v_color;\n\nvoid main(void) {\n fragColor = v_color;\n}\n";
6843
-
6844
- const minBatchVertexSize = 4;
6845
- const vertexStrideBytes$4 = 12;
6846
- const vertexStrideWords = vertexStrideBytes$4 / 4;
6847
- class WebGl2PrimitiveRenderer extends AbstractWebGl2Renderer {
6848
- _vertexCapacity;
6849
- _indexCapacity;
6850
- _vertexData;
6851
- _indexData;
6852
- _float32View;
6853
- _uint32View;
6854
- _shader = new Shader(vertexSource$1, fragmentSource$1);
6921
+ var vertexSource = "#version 300 es\nprecision lowp float;\n\nlayout(location = 0) in vec2 a_position;\nlayout(location = 1) in vec2 a_texcoord;\n\nuniform mat3 u_projection;\n\nout vec2 v_texcoord;\n\nvoid main(void) {\n gl_Position = vec4((u_projection * vec3(a_position, 1.0)).xy, 0.0, 1.0);\n v_texcoord = a_texcoord;\n}\n";
6922
+
6923
+ var fragmentSource = "#version 300 es\nprecision lowp float;\n\nuniform sampler2D u_content;\nuniform sampler2D u_mask;\n\nin vec2 v_texcoord;\n\nlayout(location = 0) out vec4 fragColor;\n\nvoid main(void) {\n vec4 contentColor = texture(u_content, v_texcoord);\n float maskAlpha = texture(u_mask, v_texcoord).a;\n\n fragColor = vec4(contentColor.rgb * maskAlpha, contentColor.a * maskAlpha);\n}\n";
6924
+
6925
+ // 4 floats per vertex: position(x, y) + texcoord(u, v).
6926
+ const vertexStrideBytes$2 = 16;
6927
+ const quadIndices$1 = new Uint16Array([0, 1, 2, 0, 2, 3]);
6928
+ /**
6929
+ * Single-quad two-texture compositor used by `WebGl2Backend.composeWithAlphaMask`.
6930
+ *
6931
+ * Renders the content texture onto the active render target with each
6932
+ * output texel's alpha multiplied by the mask texture's alpha at the
6933
+ * same UV. Both textures are sampled with stretched-fit UVs over the
6934
+ * destination rectangle.
6935
+ *
6936
+ * Intentionally not a {@link AbstractWebGl2Renderer} subclass: this
6937
+ * compositor is invoked directly by the manager for non-Drawable
6938
+ * compositing operations and never participates in the renderer
6939
+ * registry dispatch path.
6940
+ */
6941
+ class WebGl2MaskCompositor {
6942
+ _shader = new Shader(vertexSource, fragmentSource);
6943
+ _vertexData = new ArrayBuffer(4 * vertexStrideBytes$2);
6944
+ _float32View = new Float32Array(this._vertexData);
6945
+ _contentSamplerSlot = new Int32Array([0]);
6946
+ _maskSamplerSlot = new Int32Array([1]);
6855
6947
  _connection = null;
6856
- _currentBlendMode = null;
6857
- _currentView = null;
6858
- _viewId = -1;
6859
- constructor(batchSize) {
6860
- super();
6861
- this._vertexCapacity = Math.max(minBatchVertexSize, batchSize);
6862
- this._indexCapacity = Math.max(6, this._vertexCapacity * 3);
6863
- this._vertexData = new ArrayBuffer(this._vertexCapacity * vertexStrideBytes$4);
6864
- this._indexData = new Uint16Array(this._indexCapacity);
6865
- this._float32View = new Float32Array(this._vertexData);
6866
- this._uint32View = new Uint32Array(this._vertexData);
6867
- }
6868
- render(shape) {
6869
- const connection = this._connection;
6870
- if (!connection) {
6871
- throw new Error('Renderer not connected');
6872
- }
6873
- const backend = this.getBackend();
6874
- const { geometry, drawMode, color, blendMode } = shape;
6875
- const vertices = geometry.vertices;
6876
- const sourceIndices = geometry.indices;
6877
- const vertexCount = vertices.length / 2;
6878
- const indexCount = sourceIndices.length > 0 ? sourceIndices.length : vertexCount;
6879
- if (vertexCount === 0 || indexCount === 0) {
6948
+ connect(backend) {
6949
+ if (this._connection !== null) {
6880
6950
  return;
6881
6951
  }
6882
- this._ensureVertexCapacity(vertexCount);
6883
- this._ensureIndexCapacity(indexCount);
6884
- if (blendMode !== this._currentBlendMode) {
6885
- this._currentBlendMode = blendMode;
6886
- backend.setBlendMode(blendMode);
6887
- }
6888
- const view = backend.view;
6889
- if (this._currentView !== view || this._viewId !== view.updateId) {
6890
- this._currentView = view;
6891
- this._viewId = view.updateId;
6892
- this._shader.getUniform('u_projection').setValue(view.getTransform().toArray(false));
6893
- }
6894
- this._shader.getUniform('u_translation').setValue(shape.getGlobalTransform().toArray(false));
6895
- const packedColor = color.toRgba();
6896
- for (let i = 0; i < vertexCount; i++) {
6897
- const sourceIndex = i * 2;
6898
- const targetIndex = i * vertexStrideWords;
6899
- this._float32View[targetIndex] = vertices[sourceIndex];
6900
- this._float32View[targetIndex + 1] = vertices[sourceIndex + 1];
6901
- this._uint32View[targetIndex + 2] = packedColor;
6902
- }
6903
- if (sourceIndices.length > 0) {
6904
- this._indexData.set(sourceIndices, 0);
6905
- }
6906
- else {
6907
- for (let i = 0; i < vertexCount; i++) {
6908
- this._indexData[i] = i;
6909
- }
6910
- }
6911
- this._shader.sync();
6912
- backend.bindVertexArrayObject(connection.vao);
6913
- connection.vertexBuffer.upload(this._float32View.subarray(0, vertexCount * vertexStrideWords));
6914
- connection.indexBuffer.upload(this._indexData.subarray(0, indexCount));
6915
- connection.vao.draw(indexCount, 0, drawMode);
6916
- backend.stats.batches++;
6917
- backend.stats.drawCalls++;
6918
- }
6919
- flush() {
6920
- // Primitive rendering is immediate per shape in this bridge stage.
6921
- }
6922
- destroy() {
6923
- this.disconnect();
6924
- this._shader.destroy();
6925
- this._currentBlendMode = null;
6926
- this._currentView = null;
6927
- }
6928
- onConnect(backend) {
6929
6952
  const gl = backend.context;
6930
6953
  const vaoHandle = gl.createVertexArray();
6931
- this._shader.connect(createWebGl2ShaderProgram(gl));
6932
6954
  if (vaoHandle === null) {
6933
- throw new Error('Could not create vertex array object.');
6955
+ throw new Error('WebGl2MaskCompositor: could not create vertex array object.');
6934
6956
  }
6935
- const buffers = new Map();
6936
- const indexBuffer = new WebGl2RenderBuffer(BufferTypes.ElementArrayBuffer, this._indexData, BufferUsage.DynamicDraw)
6937
- .connect(this._createBufferRuntime(gl, buffers));
6957
+ this._shader.connect(createWebGl2ShaderProgram(gl));
6958
+ const bufferHandles = new Map();
6959
+ const indexBuffer = new WebGl2RenderBuffer(BufferTypes.ElementArrayBuffer, quadIndices$1, BufferUsage.StaticDraw)
6960
+ .connect(this._createBufferRuntime(gl, bufferHandles));
6938
6961
  const vertexBuffer = new WebGl2RenderBuffer(BufferTypes.ArrayBuffer, this._vertexData, BufferUsage.DynamicDraw)
6939
- .connect(this._createBufferRuntime(gl, buffers));
6940
- // Force shader finalize so the attribute table is populated. The
6941
- // async-compile path defers attribute extraction from initialize()
6942
- // to first sync(); without this nudge, getAttribute() below would
6943
- // throw "Attribute 'a_position' is not available".
6962
+ .connect(this._createBufferRuntime(gl, bufferHandles));
6963
+ // Force shader finalize so getAttribute() below sees a populated
6964
+ // attribute table; the async-compile path defers extraction until
6965
+ // the first sync() call.
6944
6966
  this._shader.sync();
6945
6967
  const vao = new WebGl2VertexArrayObject()
6946
6968
  .addIndex(indexBuffer)
6947
- .addAttribute(vertexBuffer, this._shader.getAttribute('a_position'), gl.FLOAT, false, vertexStrideBytes$4, 0)
6948
- .addAttribute(vertexBuffer, this._shader.getAttribute('a_color'), gl.UNSIGNED_BYTE, true, vertexStrideBytes$4, 8)
6969
+ .addAttribute(vertexBuffer, this._shader.getAttribute('a_position'), gl.FLOAT, false, vertexStrideBytes$2, 0)
6970
+ .addAttribute(vertexBuffer, this._shader.getAttribute('a_texcoord'), gl.FLOAT, false, vertexStrideBytes$2, 8)
6949
6971
  .connect(this._createVaoRuntime(gl, vaoHandle));
6950
- this._connection = { gl, buffers, vaoHandle, vao, indexBuffer, vertexBuffer };
6972
+ this._connection = { gl, vaoHandle, vao, indexBuffer, vertexBuffer, bufferHandles };
6951
6973
  }
6952
- onDisconnect() {
6974
+ disconnect() {
6953
6975
  const connection = this._connection;
6954
- if (!connection) {
6976
+ if (connection === null) {
6955
6977
  return;
6956
6978
  }
6957
- this._shader.disconnect();
6958
6979
  connection.indexBuffer.destroy();
6959
6980
  connection.vertexBuffer.destroy();
6960
6981
  connection.vao.destroy();
6982
+ this._shader.disconnect();
6961
6983
  this._connection = null;
6962
- this._currentBlendMode = null;
6963
- this._currentView = null;
6964
- this._viewId = -1;
6965
6984
  }
6966
- _ensureVertexCapacity(vertexCount) {
6967
- if (vertexCount <= this._vertexCapacity) {
6968
- return;
6969
- }
6970
- while (this._vertexCapacity < vertexCount) {
6971
- this._vertexCapacity *= 2;
6985
+ compose(backend, content, mask, x, y, width, height, blendMode) {
6986
+ const connection = this._connection;
6987
+ if (connection === null) {
6988
+ throw new Error('WebGl2MaskCompositor: not connected.');
6972
6989
  }
6973
- this._vertexData = new ArrayBuffer(this._vertexCapacity * vertexStrideBytes$4);
6974
- this._float32View = new Float32Array(this._vertexData);
6975
- this._uint32View = new Uint32Array(this._vertexData);
6976
- }
6977
- _ensureIndexCapacity(indexCount) {
6978
- if (indexCount <= this._indexCapacity) {
6979
- return;
6980
- }
6981
- while (this._indexCapacity < indexCount) {
6982
- this._indexCapacity *= 2;
6983
- }
6984
- this._indexData = new Uint16Array(this._indexCapacity);
6985
- }
6986
- _createBufferRuntime(gl, buffers) {
6987
- const handle = gl.createBuffer();
6988
- if (handle === null) {
6989
- throw new Error('Could not create render buffer.');
6990
- }
6991
- return {
6992
- bind: (buffer) => {
6993
- gl.bindBuffer(buffer.type, handle);
6994
- },
6995
- upload: (buffer, offset) => {
6996
- const state = buffers.get(buffer);
6997
- const data = buffer.data;
6998
- gl.bindBuffer(buffer.type, handle);
6999
- if (state && state.dataByteLength >= data.byteLength) {
7000
- gl.bufferSubData(buffer.type, offset, data);
7001
- state.dataByteLength = data.byteLength;
7002
- }
7003
- else {
7004
- gl.bufferData(buffer.type, data, buffer.usage);
7005
- buffers.set(buffer, { handle, dataByteLength: data.byteLength });
7006
- }
7007
- },
7008
- destroy: (buffer) => {
7009
- gl.deleteBuffer(handle);
7010
- buffers.delete(buffer);
7011
- buffer.disconnect();
7012
- },
7013
- };
7014
- }
7015
- _createVaoRuntime(gl, vaoHandle) {
7016
- let appliedVersion = -1;
7017
- return {
7018
- bind: (vao) => {
7019
- gl.bindVertexArray(vaoHandle);
7020
- if (appliedVersion !== vao.version) {
7021
- let lastBuffer = null;
7022
- for (const attribute of vao.attributes) {
7023
- if (lastBuffer !== attribute.buffer) {
7024
- attribute.buffer.bind();
7025
- lastBuffer = attribute.buffer;
7026
- }
7027
- gl.vertexAttribPointer(attribute.location, attribute.size, attribute.type, attribute.normalized, attribute.stride, attribute.start);
7028
- gl.enableVertexAttribArray(attribute.location);
7029
- }
7030
- if (vao.indexBuffer) {
7031
- vao.indexBuffer.bind();
7032
- }
7033
- appliedVersion = vao.version;
7034
- }
7035
- },
7036
- unbind: () => {
7037
- gl.bindVertexArray(null);
7038
- },
7039
- draw: (vao, size, start, type) => {
7040
- if (vao.indexBuffer) {
7041
- gl.drawElements(type, size, gl.UNSIGNED_SHORT, start);
7042
- }
7043
- else {
7044
- gl.drawArrays(type, start, size);
7045
- }
7046
- },
7047
- destroy: (vao) => {
7048
- gl.deleteVertexArray(vaoHandle);
7049
- vao.disconnect();
7050
- },
7051
- };
7052
- }
7053
- }
7054
-
7055
- var vertexSource = "#version 300 es\nprecision lowp float;\n\nlayout(location = 0) in vec2 a_position;\nlayout(location = 1) in vec2 a_texcoord;\n\nuniform mat3 u_projection;\n\nout vec2 v_texcoord;\n\nvoid main(void) {\n gl_Position = vec4((u_projection * vec3(a_position, 1.0)).xy, 0.0, 1.0);\n v_texcoord = a_texcoord;\n}\n";
7056
-
7057
- var fragmentSource = "#version 300 es\nprecision lowp float;\n\nuniform sampler2D u_content;\nuniform sampler2D u_mask;\n\nin vec2 v_texcoord;\n\nlayout(location = 0) out vec4 fragColor;\n\nvoid main(void) {\n vec4 contentColor = texture(u_content, v_texcoord);\n float maskAlpha = texture(u_mask, v_texcoord).a;\n\n fragColor = vec4(contentColor.rgb * maskAlpha, contentColor.a * maskAlpha);\n}\n";
7058
-
7059
- // 4 floats per vertex: position(x, y) + texcoord(u, v).
7060
- const vertexStrideBytes$3 = 16;
7061
- const quadIndices$1 = new Uint16Array([0, 1, 2, 0, 2, 3]);
7062
- /**
7063
- * Single-quad two-texture compositor used by `WebGl2Backend.composeWithAlphaMask`.
7064
- *
7065
- * Renders the content texture onto the active render target with each
7066
- * output texel's alpha multiplied by the mask texture's alpha at the
7067
- * same UV. Both textures are sampled with stretched-fit UVs over the
7068
- * destination rectangle.
7069
- *
7070
- * Intentionally not a {@link AbstractWebGl2Renderer} subclass: this
7071
- * compositor is invoked directly by the manager for non-Drawable
7072
- * compositing operations and never participates in the renderer
7073
- * registry dispatch path.
7074
- */
7075
- class WebGl2MaskCompositor {
7076
- _shader = new Shader(vertexSource, fragmentSource);
7077
- _vertexData = new ArrayBuffer(4 * vertexStrideBytes$3);
7078
- _float32View = new Float32Array(this._vertexData);
7079
- _contentSamplerSlot = new Int32Array([0]);
7080
- _maskSamplerSlot = new Int32Array([1]);
7081
- _connection = null;
7082
- connect(backend) {
7083
- if (this._connection !== null) {
7084
- return;
7085
- }
7086
- const gl = backend.context;
7087
- const vaoHandle = gl.createVertexArray();
7088
- if (vaoHandle === null) {
7089
- throw new Error('WebGl2MaskCompositor: could not create vertex array object.');
7090
- }
7091
- this._shader.connect(createWebGl2ShaderProgram(gl));
7092
- const bufferHandles = new Map();
7093
- const indexBuffer = new WebGl2RenderBuffer(BufferTypes.ElementArrayBuffer, quadIndices$1, BufferUsage.StaticDraw)
7094
- .connect(this._createBufferRuntime(gl, bufferHandles));
7095
- const vertexBuffer = new WebGl2RenderBuffer(BufferTypes.ArrayBuffer, this._vertexData, BufferUsage.DynamicDraw)
7096
- .connect(this._createBufferRuntime(gl, bufferHandles));
7097
- // Force shader finalize so getAttribute() below sees a populated
7098
- // attribute table; the async-compile path defers extraction until
7099
- // the first sync() call.
7100
- this._shader.sync();
7101
- const vao = new WebGl2VertexArrayObject()
7102
- .addIndex(indexBuffer)
7103
- .addAttribute(vertexBuffer, this._shader.getAttribute('a_position'), gl.FLOAT, false, vertexStrideBytes$3, 0)
7104
- .addAttribute(vertexBuffer, this._shader.getAttribute('a_texcoord'), gl.FLOAT, false, vertexStrideBytes$3, 8)
7105
- .connect(this._createVaoRuntime(gl, vaoHandle));
7106
- this._connection = { gl, vaoHandle, vao, indexBuffer, vertexBuffer, bufferHandles };
7107
- }
7108
- disconnect() {
7109
- const connection = this._connection;
7110
- if (connection === null) {
7111
- return;
7112
- }
7113
- connection.indexBuffer.destroy();
7114
- connection.vertexBuffer.destroy();
7115
- connection.vao.destroy();
7116
- this._shader.disconnect();
7117
- this._connection = null;
7118
- }
7119
- compose(backend, content, mask, x, y, width, height, blendMode) {
7120
- const connection = this._connection;
7121
- if (connection === null) {
7122
- throw new Error('WebGl2MaskCompositor: not connected.');
7123
- }
7124
- // Update the quad vertices for this destination rect. UVs are 0..1
7125
- // mapped over (left, top) → (right, bottom) with an explicit Y-flip
7126
- // for render-texture sampling: the mask path samples textures
7127
- // already authored as RGBA in render-texture orientation.
7128
- this._writeQuadVertices(x, y, x + width, y + height);
7129
- // Bind the compositor program. Setting projection + sampler uniforms
7130
- // each call because they need to match the current render target.
7131
- backend.bindShader(this._shader);
7132
- const view = backend.view;
7133
- const projection = view.getTransform().toArray(false);
7134
- this._shader.getUniform('u_projection').setValue(projection);
7135
- this._shader.getUniform('u_content').setValue(this._contentSamplerSlot);
7136
- this._shader.getUniform('u_mask').setValue(this._maskSamplerSlot);
7137
- this._shader.sync();
7138
- backend.bindTexture(content, 0);
7139
- backend.bindTexture(mask, 1);
7140
- backend.setBlendMode(blendMode);
7141
- backend.bindVertexArrayObject(connection.vao);
7142
- connection.vertexBuffer.upload(this._float32View);
7143
- connection.vao.draw(6, 0);
7144
- backend.stats.batches++;
7145
- backend.stats.drawCalls++;
7146
- // Reset the active texture unit to 0 to avoid leaking unit 1 into
7147
- // subsequent renderer state.
7148
- backend.bindTexture(null, 1);
6990
+ // Update the quad vertices for this destination rect. UVs are 0..1
6991
+ // mapped over (left, top) → (right, bottom) with an explicit Y-flip
6992
+ // for render-texture sampling: the mask path samples textures
6993
+ // already authored as RGBA in render-texture orientation.
6994
+ this._writeQuadVertices(x, y, x + width, y + height);
6995
+ // Bind the compositor program. Setting projection + sampler uniforms
6996
+ // each call because they need to match the current render target.
6997
+ backend.bindShader(this._shader);
6998
+ const view = backend.view;
6999
+ const projection = view.getTransform().toArray(false);
7000
+ this._shader.getUniform('u_projection').setValue(projection);
7001
+ this._shader.getUniform('u_content').setValue(this._contentSamplerSlot);
7002
+ this._shader.getUniform('u_mask').setValue(this._maskSamplerSlot);
7003
+ this._shader.sync();
7004
+ backend.bindTexture(content, 0);
7005
+ backend.bindTexture(mask, 1);
7006
+ backend.setBlendMode(blendMode);
7007
+ backend.bindVertexArrayObject(connection.vao);
7008
+ connection.vertexBuffer.upload(this._float32View);
7009
+ connection.vao.draw(6, 0);
7010
+ backend.stats.batches++;
7011
+ backend.stats.drawCalls++;
7012
+ // Reset the active texture unit to 0 to avoid leaking unit 1 into
7013
+ // subsequent renderer state.
7014
+ backend.bindTexture(null, 1);
7149
7015
  }
7150
7016
  _writeQuadVertices(left, top, right, bottom) {
7151
7017
  const view = this._float32View;
@@ -7393,134 +7259,24 @@ class Sprite extends Drawable {
7393
7259
  }
7394
7260
  RenderNode.setInternalSpriteFactory(() => new Sprite(null));
7395
7261
 
7396
- /**
7397
- * Arbitrary 2D triangle-mesh primitive.
7398
- *
7399
- * `Mesh` lives alongside {@link Sprite} as a public Drawable: it has the
7400
- * same transform (position/rotation/scale/origin), tint, blendMode,
7401
- * filters, masks, and cacheAsBitmap — but the geometry it renders is
7402
- * user-supplied rather than implied by a texture frame. The intended use
7403
- * cases are:
7404
- *
7405
- * - Custom-shape sprites whose silhouette isn't a quad (badges, speech
7406
- * bubbles, region overlays).
7407
- * - Deformable visuals (rope/ribbon, banner, water surface): mutate the
7408
- * vertex array between frames and the GPU re-tessellates nothing —
7409
- * only the transform changes per frame.
7410
- * - Particles or trails with custom geometry per emitter.
7411
- *
7412
- * The mesh data is **immutable after construction** in v1: vertex /
7413
- * index / UV / color arrays are exposed as readonly references. Mutate
7414
- * the underlying typed arrays in-place if you need per-frame updates,
7415
- * but the array lengths and topology cannot change. Texture is the only
7416
- * post-construction mutable property.
7417
- *
7418
- * The vertex stream is a flat `Float32Array` of (x, y) pairs in local
7419
- * space. The mesh's local bounds are computed once at construction from
7420
- * the AABB of those vertices and used by the cull pass. Re-computing
7421
- * after in-place mutation is the caller's responsibility (call
7422
- * `recomputeLocalBounds()`).
7423
- */
7424
- class Mesh extends Drawable {
7425
- vertices;
7426
- indices;
7427
- uvs;
7428
- colors;
7429
- _texture;
7430
- constructor(options) {
7431
- super();
7432
- const { vertices, indices = null, uvs = null, colors = null, texture = null } = options;
7433
- if (vertices.length === 0 || vertices.length % 2 !== 0) {
7434
- throw new Error(`Mesh vertices must be a non-empty flat array of (x,y) pairs (got length ${vertices.length}).`);
7435
- }
7436
- const vertexCount = vertices.length / 2;
7437
- if (vertexCount < 3) {
7438
- throw new Error(`Mesh requires at least 3 vertices (got ${vertexCount}).`);
7439
- }
7440
- if (uvs !== null && uvs.length !== vertices.length) {
7441
- throw new Error(`Mesh uvs length ${uvs.length} must equal vertices length ${vertices.length}.`);
7442
- }
7443
- if (colors !== null && colors.length !== vertexCount) {
7444
- throw new Error(`Mesh colors length ${colors.length} must equal vertex count ${vertexCount}.`);
7445
- }
7446
- if (indices !== null) {
7447
- if (indices.length === 0 || indices.length % 3 !== 0) {
7448
- throw new Error(`Mesh indices must be a non-empty multiple of 3 (got length ${indices.length}).`);
7449
- }
7450
- for (let i = 0; i < indices.length; i++) {
7451
- if (indices[i] >= vertexCount) {
7452
- throw new Error(`Mesh index ${indices[i]} at position ${i} is out of range for vertex count ${vertexCount}.`);
7453
- }
7454
- }
7455
- }
7456
- else if (vertexCount % 3 !== 0) {
7457
- throw new Error(`Non-indexed Mesh requires a vertex count that is a multiple of 3 (got ${vertexCount}).`);
7458
- }
7459
- this.vertices = vertices;
7460
- this.indices = indices;
7461
- this.uvs = uvs;
7462
- this.colors = colors;
7463
- this._texture = texture;
7464
- this.recomputeLocalBounds();
7262
+ class Particle {
7263
+ _totalLifetime = Time.oneSecond.clone();
7264
+ _elapsedLifetime = Time.zero.clone();
7265
+ _position = Vector.zero.clone();
7266
+ _velocity = Vector.zero.clone();
7267
+ _scale = Vector.one.clone();
7268
+ _rotation = 0;
7269
+ _rotationSpeed = 0;
7270
+ _textureIndex = 0;
7271
+ _tint = Color.white.clone();
7272
+ get totalLifetime() {
7273
+ return this._totalLifetime;
7465
7274
  }
7466
- get vertexCount() {
7467
- return this.vertices.length / 2;
7275
+ set totalLifetime(totalLifetime) {
7276
+ this._totalLifetime.copy(totalLifetime);
7468
7277
  }
7469
- get indexCount() {
7470
- return this.indices?.length ?? this.vertexCount;
7471
- }
7472
- get texture() {
7473
- return this._texture;
7474
- }
7475
- set texture(texture) {
7476
- this._texture = texture;
7477
- this.invalidateCache();
7478
- }
7479
- /**
7480
- * Recompute the local AABB from the current vertex array. Call after
7481
- * mutating `vertices` in place to keep culling correct; otherwise the
7482
- * bounds the cull pass sees will be the AABB at construction time.
7483
- */
7484
- recomputeLocalBounds() {
7485
- let minX = Infinity;
7486
- let minY = Infinity;
7487
- let maxX = -Infinity;
7488
- let maxY = -Infinity;
7489
- for (let i = 0; i < this.vertices.length; i += 2) {
7490
- const x = this.vertices[i];
7491
- const y = this.vertices[i + 1];
7492
- if (x < minX)
7493
- minX = x;
7494
- if (x > maxX)
7495
- maxX = x;
7496
- if (y < minY)
7497
- minY = y;
7498
- if (y > maxY)
7499
- maxY = y;
7500
- }
7501
- this.localBounds.set(minX, minY, maxX - minX, maxY - minY);
7502
- return this;
7503
- }
7504
- }
7505
-
7506
- class Particle {
7507
- _totalLifetime = Time.oneSecond.clone();
7508
- _elapsedLifetime = Time.zero.clone();
7509
- _position = Vector.zero.clone();
7510
- _velocity = Vector.zero.clone();
7511
- _scale = Vector.one.clone();
7512
- _rotation = 0;
7513
- _rotationSpeed = 0;
7514
- _textureIndex = 0;
7515
- _tint = Color.white.clone();
7516
- get totalLifetime() {
7517
- return this._totalLifetime;
7518
- }
7519
- set totalLifetime(totalLifetime) {
7520
- this._totalLifetime.copy(totalLifetime);
7521
- }
7522
- get elapsedLifetime() {
7523
- return this._elapsedLifetime;
7278
+ get elapsedLifetime() {
7279
+ return this._elapsedLifetime;
7524
7280
  }
7525
7281
  set elapsedLifetime(elapsedLifetime) {
7526
7282
  this._elapsedLifetime.copy(elapsedLifetime);
@@ -7945,7 +7701,7 @@ class WebGl2Backend {
7945
7701
  _boundFramebuffer = null;
7946
7702
  _stats = createRenderStats();
7947
7703
  constructor(app) {
7948
- const { width, height, clearColor, webglAttributes, debug, spriteRendererBatchSize, particleRendererBatchSize, primitiveRendererBatchSize, } = app.options;
7704
+ const { width, height, clearColor, webglAttributes, debug, spriteRendererBatchSize, particleRendererBatchSize, } = app.options;
7949
7705
  this._canvas = app.canvas;
7950
7706
  const gl = this._createContext(webglAttributes);
7951
7707
  if (!gl) {
@@ -7969,7 +7725,6 @@ class WebGl2Backend {
7969
7725
  this.rendererRegistry.registerRenderer(Sprite, new WebGl2SpriteRenderer(spriteRendererBatchSize));
7970
7726
  this.rendererRegistry.registerRenderer(Mesh, new WebGl2MeshRenderer());
7971
7727
  this.rendererRegistry.registerRenderer(ParticleSystem, new WebGl2ParticleRenderer(particleRendererBatchSize));
7972
- this.rendererRegistry.registerRenderer(DrawableShape, new WebGl2PrimitiveRenderer(primitiveRendererBatchSize));
7973
7728
  this.rendererRegistry.connect(this);
7974
7729
  this._bindRenderTarget(this._renderTarget);
7975
7730
  this.setBlendMode(BlendModes.Normal);
@@ -8384,719 +8139,238 @@ class WebGl2Backend {
8384
8139
  this._boundFramebuffer = state.framebuffer;
8385
8140
  state.version = target.version;
8386
8141
  }
8387
- if (this._clipPixelStack.length > 0) {
8388
- this._applyClipState();
8389
- }
8390
- }
8391
- _setActiveRenderer(renderer) {
8392
- if (this._renderer !== renderer) {
8393
- this._flushActiveRenderer();
8394
- this._renderer = renderer;
8395
- }
8396
- }
8397
- _flushActiveRenderer() {
8398
- if (this._renderer && !this._contextLost) {
8399
- this._bindRenderTarget(this._renderTarget);
8400
- this._renderer.flush();
8401
- }
8402
- }
8403
- _prepareRenderTarget(target) {
8404
- const state = this._getRenderTargetState(target);
8405
- if (target instanceof RenderTexture && state.framebuffer) {
8406
- const previousFramebuffer = this._boundFramebuffer;
8407
- const textureState = this._syncTexture(target);
8408
- if (state.attachedTexture !== textureState.handle) {
8409
- const gl = this._context;
8410
- gl.bindFramebuffer(gl.FRAMEBUFFER, state.framebuffer);
8411
- gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, textureState.handle, 0);
8412
- gl.bindFramebuffer(gl.FRAMEBUFFER, previousFramebuffer);
8413
- state.attachedTexture = textureState.handle;
8414
- }
8415
- }
8416
- return state;
8417
- }
8418
- _syncTexture(texture) {
8419
- const gl = this._context;
8420
- const state = this._getTextureState(texture);
8421
- const version = texture instanceof RenderTexture ? texture.textureVersion : texture.version;
8422
- gl.bindTexture(gl.TEXTURE_2D, state.handle);
8423
- if (state.version !== version) {
8424
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, texture.scaleMode);
8425
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, texture.scaleMode);
8426
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, texture.wrapMode);
8427
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, texture.wrapMode);
8428
- gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, texture.premultiplyAlpha);
8429
- if (texture instanceof RenderTexture) {
8430
- if (state.version === -1 || state.width !== texture.width || state.height !== texture.height || texture.source === null) {
8431
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, texture.width, texture.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, texture.source);
8432
- }
8433
- else {
8434
- gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, texture.width, texture.height, gl.RGBA, gl.UNSIGNED_BYTE, texture.source);
8435
- }
8436
- }
8437
- else if (texture.source) {
8438
- if (state.version === -1 || state.width !== texture.width || state.height !== texture.height) {
8439
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.source);
8440
- }
8441
- else {
8442
- gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, texture.source);
8443
- }
8444
- }
8445
- if (texture.generateMipMap && (texture instanceof RenderTexture || texture.source !== null)) {
8446
- gl.generateMipmap(gl.TEXTURE_2D);
8447
- }
8448
- state.version = version;
8449
- state.width = texture.width;
8450
- state.height = texture.height;
8451
- }
8452
- return state;
8453
- }
8454
- _toClipPixels(bounds) {
8455
- const topLeft = this._renderTarget.mapCoordsToPixel(this._clipPointA.set(bounds.left, bounds.top));
8456
- const bottomRight = this._renderTarget.mapCoordsToPixel(this._clipPointB.set(bounds.right, bounds.bottom));
8457
- const minX = Math.min(topLeft.x, bottomRight.x);
8458
- const maxX = Math.max(topLeft.x, bottomRight.x);
8459
- const minY = Math.min(topLeft.y, bottomRight.y);
8460
- const maxY = Math.max(topLeft.y, bottomRight.y);
8461
- const targetWidth = this._renderTarget.width;
8462
- const targetHeight = this._renderTarget.height;
8463
- const x = Math.max(0, Math.min(targetWidth, Math.floor(minX)));
8464
- const right = Math.max(0, Math.min(targetWidth, Math.ceil(maxX)));
8465
- const yTop = Math.max(0, Math.min(targetHeight, Math.floor(minY)));
8466
- const bottom = Math.max(0, Math.min(targetHeight, Math.ceil(maxY)));
8467
- const width = Math.max(0, right - x);
8468
- const height = Math.max(0, bottom - yTop);
8469
- const y = Math.max(0, targetHeight - bottom);
8470
- return {
8471
- x,
8472
- y,
8473
- width,
8474
- height,
8475
- };
8476
- }
8477
- _intersectClips(first, second) {
8478
- const left = Math.max(first.x, second.x);
8479
- const bottom = Math.max(first.y, second.y);
8480
- const right = Math.min(first.x + first.width, second.x + second.width);
8481
- const top = Math.min(first.y + first.height, second.y + second.height);
8482
- return {
8483
- x: left,
8484
- y: bottom,
8485
- width: Math.max(0, right - left),
8486
- height: Math.max(0, top - bottom),
8487
- };
8488
- }
8489
- _applyClipState() {
8490
- const gl = this._context;
8491
- if (this._clipPixelStack.length === 0) {
8492
- gl.disable(gl.SCISSOR_TEST);
8493
- return;
8494
- }
8495
- const clip = this._clipPixelStack[this._clipPixelStack.length - 1];
8496
- gl.enable(gl.SCISSOR_TEST);
8497
- gl.scissor(clip.x, clip.y, clip.width, clip.height);
8498
- }
8499
- }
8500
-
8501
- /// <reference types="@webgpu/types" />
8502
- /**
8503
- * Base class for WebGPU renderers.
8504
- *
8505
- * Manages the connect/disconnect lifecycle and provides a safe
8506
- * `getBackend()` accessor that throws if the renderer is not connected.
8507
- *
8508
- * Subclasses must implement:
8509
- * - onConnect(backend): set up GPU resources (shader modules, pipelines, buffers)
8510
- * - onDisconnect(): tear down GPU resources
8511
- * - render(drawable): collect draw call data for the given drawable
8512
- * - flush(): encode and submit command buffers for all collected draw calls
8513
- */
8514
- class AbstractWebGpuRenderer {
8515
- backendType = RenderBackendType.WebGpu;
8516
- _backend = null;
8517
- connect(backend) {
8518
- if (this._backend !== null) {
8519
- return;
8520
- }
8521
- if (backend.backendType !== RenderBackendType.WebGpu) {
8522
- throw new Error(`${this.constructor.name} requires a WebGPU backend, `
8523
- + `but received backendType ${String(backend.backendType)}.`);
8524
- }
8525
- this._backend = backend;
8526
- this.onConnect(backend);
8527
- }
8528
- disconnect() {
8529
- if (this._backend === null) {
8530
- return;
8531
- }
8532
- this.flush();
8533
- this.onDisconnect();
8534
- this._backend = null;
8535
- }
8536
- getBackend() {
8537
- if (this._backend === null) {
8538
- throw new Error(`${this.constructor.name} is not connected to a backend.`);
8539
- }
8540
- return this._backend;
8541
- }
8542
- getBackendOrNull() {
8543
- return this._backend;
8544
- }
8545
- }
8546
-
8547
- /// <reference types="@webgpu/types" />
8548
- /**
8549
- * Returns the GPUBlendState for a given ExoJS blend mode.
8550
- * Shared by all WebGPU renderers to avoid duplication.
8551
- */
8552
- function getWebGpuBlendState(blendMode) {
8553
- switch (blendMode) {
8554
- case BlendModes.Additive:
8555
- return {
8556
- color: {
8557
- operation: 'add',
8558
- srcFactor: 'one',
8559
- dstFactor: 'one',
8560
- },
8561
- alpha: {
8562
- operation: 'add',
8563
- srcFactor: 'one',
8564
- dstFactor: 'one',
8565
- },
8566
- };
8567
- case BlendModes.Subtract:
8568
- return {
8569
- color: {
8570
- operation: 'add',
8571
- srcFactor: 'zero',
8572
- dstFactor: 'one-minus-src',
8573
- },
8574
- alpha: {
8575
- operation: 'add',
8576
- srcFactor: 'zero',
8577
- dstFactor: 'one-minus-src-alpha',
8578
- },
8579
- };
8580
- case BlendModes.Multiply:
8581
- return {
8582
- color: {
8583
- operation: 'add',
8584
- srcFactor: 'dst',
8585
- dstFactor: 'one-minus-src-alpha',
8586
- },
8587
- alpha: {
8588
- operation: 'add',
8589
- srcFactor: 'dst-alpha',
8590
- dstFactor: 'one-minus-src-alpha',
8591
- },
8592
- };
8593
- case BlendModes.Screen:
8594
- return {
8595
- color: {
8596
- operation: 'add',
8597
- srcFactor: 'one',
8598
- dstFactor: 'one-minus-src',
8599
- },
8600
- alpha: {
8601
- operation: 'add',
8602
- srcFactor: 'one',
8603
- dstFactor: 'one-minus-src-alpha',
8604
- },
8605
- };
8606
- default:
8607
- return {
8608
- color: {
8609
- operation: 'add',
8610
- srcFactor: 'one',
8611
- dstFactor: 'one-minus-src-alpha',
8612
- },
8613
- alpha: {
8614
- operation: 'add',
8615
- srcFactor: 'one',
8616
- dstFactor: 'one-minus-src-alpha',
8617
- },
8618
- };
8619
- }
8620
- }
8621
-
8622
- /// <reference types="@webgpu/types" />
8623
- const primitiveShaderSource = `
8624
- struct VertexInput {
8625
- @location(0) position: vec4<f32>,
8626
- @location(1) color: vec4<f32>,
8627
- };
8628
-
8629
- struct VertexOutput {
8630
- @builtin(position) position: vec4<f32>,
8631
- @location(0) color: vec4<f32>,
8632
- };
8633
-
8634
- @vertex
8635
- fn vertexMain(input: VertexInput) -> VertexOutput {
8636
- var output: VertexOutput;
8637
-
8638
- output.position = input.position;
8639
- output.color = vec4<f32>(input.color.rgb * input.color.a, input.color.a);
8640
-
8641
- return output;
8642
- }
8643
-
8644
- @fragment
8645
- fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
8646
- return input.color;
8647
- }
8648
- `;
8649
- // 4 floats (pre-transformed clip-space position) + 1 u32 (color) = 20 bytes.
8650
- // The CPU applies (view * shape.globalTransform) to each vertex before writing
8651
- // it into the vertex buffer, so the shader outputs the position as-is. This
8652
- // matches the sprite renderer's approach and eliminates the need for a per-
8653
- // drawcall uniform binding.
8654
- const vertexStrideBytes$2 = 20;
8655
- const wordsPerVertex$1 = vertexStrideBytes$2 / Uint32Array.BYTES_PER_ELEMENT;
8656
- class WebGpuPrimitiveRenderer extends AbstractWebGpuRenderer {
8657
- _combinedTransform = new Matrix();
8658
- _drawCalls = [];
8659
- _drawCallCount = 0;
8660
- _pipelines = new Map();
8661
- _device = null;
8662
- _shaderModule = null;
8663
- _pipelineLayout = null;
8664
- _vertexBuffer = null;
8665
- _indexBuffer = null;
8666
- _vertexBufferCapacity = 0;
8667
- _indexBufferCapacity = 0;
8668
- _vertexData = new ArrayBuffer(0);
8669
- _float32View = new Float32Array(this._vertexData);
8670
- _uint32View = new Uint32Array(this._vertexData);
8671
- _packedIndexData = new Uint16Array(0);
8672
- _generatedIndexData = new Uint16Array(0);
8673
- _sequentialIndexData = new Uint16Array(0);
8674
- render(shape) {
8675
- const backend = this._backend;
8676
- if (backend === null) {
8677
- throw new Error('Renderer not connected');
8678
- }
8679
- if (shape.drawMode !== RenderingPrimitives.Points
8680
- && shape.drawMode !== RenderingPrimitives.Lines
8681
- && shape.drawMode !== RenderingPrimitives.LineLoop
8682
- && shape.drawMode !== RenderingPrimitives.LineStrip
8683
- && shape.drawMode !== RenderingPrimitives.Triangles
8684
- && shape.drawMode !== RenderingPrimitives.TriangleFan
8685
- && shape.drawMode !== RenderingPrimitives.TriangleStrip) {
8686
- throw new Error(`WebGPU primitive renderer does not support draw mode "${shape.drawMode}" yet.`);
8687
- }
8688
- backend.setBlendMode(shape.blendMode);
8689
- if (shape.geometry.vertices.length === 0) {
8690
- return;
8691
- }
8692
- const drawCallIndex = this._drawCallCount++;
8693
- const drawCall = this._drawCalls[drawCallIndex];
8694
- if (drawCall) {
8695
- drawCall.shape = shape;
8696
- drawCall.blendMode = shape.blendMode;
8697
- }
8698
- else {
8699
- this._drawCalls.push({
8700
- shape,
8701
- blendMode: shape.blendMode,
8702
- });
8703
- }
8704
- }
8705
- flush() {
8706
- const backend = this._backend;
8707
- const device = this._device;
8708
- if (!backend || !device) {
8709
- return;
8710
- }
8711
- if (this._drawCallCount === 0 && !backend.clearRequested) {
8712
- return;
8713
- }
8714
- const scissor = backend.getScissorRect();
8715
- const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
8716
- // Phase 1: resolve drawcalls and record each one's offsets into the
8717
- // shared packed buffers. Transform gets baked into the vertex data
8718
- // during phase 2 so no per-drawcall uniform binding is needed.
8719
- const plan = [];
8720
- const resolvedDrawCalls = [];
8721
- let totalVertices = 0;
8722
- let totalIndices = 0;
8723
- if (this._drawCallCount > 0 && !maskClipsAll) {
8724
- for (let drawCallIndex = 0; drawCallIndex < this._drawCallCount; drawCallIndex++) {
8725
- const drawCall = this._drawCalls[drawCallIndex];
8726
- const shape = drawCall.shape;
8727
- const resolved = this._resolveDrawCall(shape);
8728
- resolvedDrawCalls.push(resolved);
8729
- if (resolved === null) {
8730
- continue;
8731
- }
8732
- const pipeline = this._getPipeline({
8733
- topology: resolved.topology,
8734
- usesStripIndex: resolved.usesStripIndex,
8735
- blendMode: drawCall.blendMode,
8736
- format: backend.renderTargetFormat,
8737
- });
8738
- plan.push({
8739
- pipeline,
8740
- vertexByteOffset: totalVertices * vertexStrideBytes$2,
8741
- vertexCount: resolved.vertexCount,
8742
- indexByteOffset: totalIndices * Uint16Array.BYTES_PER_ELEMENT,
8743
- indexCount: resolved.indexCount,
8744
- });
8745
- totalVertices += resolved.vertexCount;
8746
- totalIndices += resolved.indexCount;
8747
- }
8748
- }
8749
- // If nothing will actually render, still honor a pending clear with
8750
- // a single empty pass so createColorAttachment consumes the clear
8751
- // state exactly once.
8752
- if (plan.length === 0) {
8753
- if (backend.clearRequested) {
8754
- const encoder = device.createCommandEncoder();
8755
- const pass = encoder.beginRenderPass({
8756
- colorAttachments: [backend.createColorAttachment()],
8757
- });
8758
- backend.stats.renderPasses++;
8759
- pass.end();
8760
- backend.submit(encoder.finish());
8761
- }
8762
- this._drawCallCount = 0;
8763
- return;
8764
- }
8765
- // Phase 2: size GPU buffers for the whole-frame totals, then pack
8766
- // every drawcall's CPU-side data. _writeShapeVertices applies
8767
- // (view * shape.globalTransform) per-vertex so the shader simply
8768
- // outputs input.position unchanged.
8769
- this._ensureVertexCapacity(totalVertices);
8770
- if (totalIndices > 0) {
8771
- this._ensureIndexCapacity(totalIndices);
8772
- if (this._packedIndexData.length < totalIndices) {
8773
- this._packedIndexData = new Uint16Array(Math.max(totalIndices, this._packedIndexData.length === 0 ? 1 : this._packedIndexData.length * 2));
8774
- }
8775
- }
8776
- {
8777
- let vOffset = 0;
8778
- let iOffset = 0;
8779
- for (let i = 0; i < this._drawCallCount; i++) {
8780
- const resolved = resolvedDrawCalls[i];
8781
- if (resolved === null) {
8782
- continue;
8783
- }
8784
- const drawCall = this._drawCalls[i];
8785
- const shape = drawCall.shape;
8786
- this._writeShapeVertices(backend, shape, vOffset);
8787
- if (resolved.indices !== null && resolved.indexCount > 0) {
8788
- this._packedIndexData.set(resolved.indices.subarray(0, resolved.indexCount), iOffset);
8789
- iOffset += resolved.indexCount;
8790
- }
8791
- vOffset += resolved.vertexCount;
8792
- }
8793
- }
8794
- // Phase 3: single writeBuffer per GPU buffer covers the whole frame.
8795
- device.queue.writeBuffer(this._vertexBuffer, 0, this._vertexData, 0, totalVertices * vertexStrideBytes$2);
8796
- if (totalIndices > 0) {
8797
- device.queue.writeBuffer(this._indexBuffer, 0, this._packedIndexData.buffer, this._packedIndexData.byteOffset, totalIndices * Uint16Array.BYTES_PER_ELEMENT);
8798
- }
8799
- // Phase 4: single render pass. Per-draw state is just pipeline and
8800
- // vertex/index subrange offsets — the transform has already been
8801
- // baked into the vertex data.
8802
- const encoder = device.createCommandEncoder();
8803
- const pass = encoder.beginRenderPass({
8804
- colorAttachments: [backend.createColorAttachment()],
8805
- });
8806
- backend.stats.renderPasses++;
8807
- if (scissor !== null) {
8808
- pass.setScissorRect(scissor.x, scissor.y, scissor.width, scissor.height);
8809
- }
8810
- for (const planned of plan) {
8811
- pass.setPipeline(planned.pipeline);
8812
- pass.setVertexBuffer(0, this._vertexBuffer, planned.vertexByteOffset);
8813
- if (planned.indexCount > 0) {
8814
- pass.setIndexBuffer(this._indexBuffer, 'uint16', planned.indexByteOffset);
8815
- pass.drawIndexed(planned.indexCount);
8816
- }
8817
- else {
8818
- pass.draw(planned.vertexCount);
8819
- }
8820
- backend.stats.batches++;
8821
- backend.stats.drawCalls++;
8822
- }
8823
- pass.end();
8824
- backend.submit(encoder.finish());
8825
- this._drawCallCount = 0;
8826
- }
8827
- destroy() {
8828
- this.disconnect();
8829
- this._combinedTransform.destroy();
8830
- }
8831
- onConnect(backend) {
8832
- this._backend = backend;
8833
- this._device = this._backend.device;
8834
- this._shaderModule = this._device.createShaderModule({ code: primitiveShaderSource });
8835
- // Transform is applied per-vertex on the CPU, so no uniform binding
8836
- // is needed — the shader outputs input.position directly.
8837
- this._pipelineLayout = this._device.createPipelineLayout({
8838
- bindGroupLayouts: [],
8839
- });
8840
- }
8841
- onDisconnect() {
8842
- this.flush();
8843
- this._destroyBuffers();
8844
- this._pipelines.clear();
8845
- this._pipelineLayout = null;
8846
- this._shaderModule = null;
8847
- this._device = null;
8848
- this._backend = null;
8849
- this._drawCallCount = 0;
8850
- }
8851
- _writeShapeVertices(backend, shape, vertexStart) {
8852
- // Matrix.combine is `other * this` (see Matrix.rotate and
8853
- // SceneNode.getGlobalTransform, both of which chain via
8854
- // local.combine(parent.global) to yield parent.global * local).
8855
- //
8856
- // We need view * global applied to a local vertex, so start with
8857
- // global and combine with view — that gives
8858
- // _combinedTransform = view * global.
8859
- const matrix = this._combinedTransform
8860
- .copy(shape.getGlobalTransform())
8861
- .combine(backend.view.getTransform());
8862
- // Match the original uniform-based WGSL layout exactly.
8863
- //
8864
- // The shader packs the Matrix's 9 fields into a 4x4 mat (column-major
8865
- // in WGSL):
8866
- // col 0 = [a, c, 0, 0]
8867
- // col 1 = [b, d, 0, 0]
8868
- // col 2 = [0, 0, 1, 0]
8869
- // col 3 = [x, y, 0, z]
8870
- //
8871
- // Multiplied by vec4(px, py, 0, 1):
8872
- // out = col0*px + col1*py + col2*0 + col3*1
8873
- // out.x = a*px + b*py + x
8874
- // out.y = c*px + d*py + y
8875
- // out.z = 0
8876
- // out.w = z
8877
- //
8878
- // The Matrix class represents the affine matrix in the order
8879
- // [a b x]
8880
- // [c d y]
8881
- // [e f z]
8882
- // so a/b/c/d are rotation+scale (note: b on the TOP row, c on the
8883
- // LEFT column, not the other way around) and x/y/z the translation /
8884
- // w component. Matrix.toArray(false) confirms this layout.
8885
- const a = matrix.a;
8886
- const b = matrix.b;
8887
- const c = matrix.c;
8888
- const d = matrix.d;
8889
- const tx = matrix.x;
8890
- const ty = matrix.y;
8891
- const tw = matrix.z;
8892
- const color = shape.color.toRgba();
8893
- const vertices = shape.geometry.vertices;
8894
- const vertexCount = vertices.length / 2;
8895
- for (let i = 0; i < vertexCount; i++) {
8896
- const sourceIndex = i * 2;
8897
- const targetIndex = (vertexStart + i) * wordsPerVertex$1;
8898
- const px = vertices[sourceIndex];
8899
- const py = vertices[sourceIndex + 1];
8900
- this._float32View[targetIndex + 0] = a * px + b * py + tx;
8901
- this._float32View[targetIndex + 1] = c * px + d * py + ty;
8902
- this._float32View[targetIndex + 2] = 0;
8903
- this._float32View[targetIndex + 3] = tw;
8904
- this._uint32View[targetIndex + 4] = color;
8905
- }
8906
- }
8907
- _ensureVertexCapacity(vertexCount) {
8908
- const requiredBytes = vertexCount * vertexStrideBytes$2;
8909
- if (requiredBytes > this._vertexData.byteLength) {
8910
- const byteLength = Math.max(requiredBytes, this._vertexData.byteLength === 0 ? vertexStrideBytes$2 : this._vertexData.byteLength * 2);
8911
- this._vertexData = new ArrayBuffer(byteLength);
8912
- this._float32View = new Float32Array(this._vertexData);
8913
- this._uint32View = new Uint32Array(this._vertexData);
8914
- }
8915
- if (requiredBytes > this._vertexBufferCapacity) {
8916
- this._vertexBuffer?.destroy();
8917
- this._vertexBufferCapacity = Math.max(requiredBytes, this._vertexBufferCapacity === 0 ? vertexStrideBytes$2 : this._vertexBufferCapacity * 2);
8918
- this._vertexBuffer = this._device.createBuffer({
8919
- size: this._vertexBufferCapacity,
8920
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
8921
- });
8922
- }
8923
- }
8924
- _ensureIndexCapacity(indexCount) {
8925
- const requiredBytes = indexCount * Uint16Array.BYTES_PER_ELEMENT;
8926
- if (requiredBytes > this._indexBufferCapacity) {
8927
- this._indexBuffer?.destroy();
8928
- this._indexBufferCapacity = Math.max(requiredBytes, this._indexBufferCapacity === 0 ? Uint16Array.BYTES_PER_ELEMENT : this._indexBufferCapacity * 2);
8929
- this._indexBuffer = this._device.createBuffer({
8930
- size: this._indexBufferCapacity,
8931
- usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
8932
- });
8933
- }
8934
- }
8935
- _getPipeline(key) {
8936
- const pipelineKey = `${key.topology}:${key.usesStripIndex ? 1 : 0}:${key.blendMode}:${key.format}`;
8937
- const existingPipeline = this._pipelines.get(pipelineKey);
8938
- if (existingPipeline) {
8939
- return existingPipeline;
8940
- }
8941
- const pipeline = this._device.createRenderPipeline({
8942
- layout: this._pipelineLayout,
8943
- vertex: {
8944
- module: this._shaderModule,
8945
- entryPoint: 'vertexMain',
8946
- buffers: [{
8947
- arrayStride: vertexStrideBytes$2,
8948
- attributes: [{
8949
- shaderLocation: 0,
8950
- offset: 0,
8951
- format: 'float32x4',
8952
- }, {
8953
- shaderLocation: 1,
8954
- offset: 16,
8955
- format: 'unorm8x4',
8956
- }],
8957
- }],
8958
- },
8959
- fragment: {
8960
- module: this._shaderModule,
8961
- entryPoint: 'fragmentMain',
8962
- targets: [{
8963
- format: key.format,
8964
- blend: getWebGpuBlendState(key.blendMode),
8965
- writeMask: GPUColorWrite.ALL,
8966
- }],
8967
- },
8968
- primitive: {
8969
- topology: key.topology,
8970
- stripIndexFormat: key.usesStripIndex ? 'uint16' : undefined,
8971
- },
8972
- });
8973
- this._pipelines.set(pipelineKey, pipeline);
8974
- return pipeline;
8142
+ if (this._clipPixelStack.length > 0) {
8143
+ this._applyClipState();
8144
+ }
8975
8145
  }
8976
- _getTopology(drawMode) {
8977
- switch (drawMode) {
8978
- case RenderingPrimitives.Points:
8979
- return 'point-list';
8980
- case RenderingPrimitives.Lines:
8981
- return 'line-list';
8982
- case RenderingPrimitives.LineLoop:
8983
- case RenderingPrimitives.LineStrip:
8984
- return 'line-strip';
8985
- case RenderingPrimitives.Triangles:
8986
- case RenderingPrimitives.TriangleFan:
8987
- return 'triangle-list';
8988
- case RenderingPrimitives.TriangleStrip:
8989
- return 'triangle-strip';
8990
- default:
8991
- throw new Error(`WebGPU primitive renderer does not support draw mode "${drawMode}" yet.`);
8146
+ _setActiveRenderer(renderer) {
8147
+ if (this._renderer !== renderer) {
8148
+ this._flushActiveRenderer();
8149
+ this._renderer = renderer;
8992
8150
  }
8993
8151
  }
8994
- _resolveDrawCall(shape) {
8995
- const vertices = shape.geometry.vertices;
8996
- const vertexCount = vertices.length / 2;
8997
- if (vertexCount === 0) {
8998
- return null;
8152
+ _flushActiveRenderer() {
8153
+ if (this._renderer && !this._contextLost) {
8154
+ this._bindRenderTarget(this._renderTarget);
8155
+ this._renderer.flush();
8999
8156
  }
9000
- switch (shape.drawMode) {
9001
- case RenderingPrimitives.LineLoop:
9002
- return this._resolveLineLoopDrawCall(shape.geometry.indices, vertexCount);
9003
- case RenderingPrimitives.TriangleFan:
9004
- return this._resolveTriangleFanDrawCall(shape.geometry.indices, vertexCount);
9005
- default: {
9006
- const indices = shape.geometry.indices;
9007
- const topology = this._getTopology(shape.drawMode);
9008
- const indexCount = indices.length;
9009
- const usesStripIndex = indexCount > 0 && (shape.drawMode === RenderingPrimitives.LineStrip
9010
- || shape.drawMode === RenderingPrimitives.TriangleStrip);
9011
- if (indexCount > 0) {
9012
- return {
9013
- topology,
9014
- usesStripIndex,
9015
- vertexCount,
9016
- indices,
9017
- indexCount,
9018
- };
9019
- }
9020
- return {
9021
- topology,
9022
- usesStripIndex,
9023
- vertexCount,
9024
- indices: null,
9025
- indexCount: 0,
9026
- };
8157
+ }
8158
+ _prepareRenderTarget(target) {
8159
+ const state = this._getRenderTargetState(target);
8160
+ if (target instanceof RenderTexture && state.framebuffer) {
8161
+ const previousFramebuffer = this._boundFramebuffer;
8162
+ const textureState = this._syncTexture(target);
8163
+ if (state.attachedTexture !== textureState.handle) {
8164
+ const gl = this._context;
8165
+ gl.bindFramebuffer(gl.FRAMEBUFFER, state.framebuffer);
8166
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, textureState.handle, 0);
8167
+ gl.bindFramebuffer(gl.FRAMEBUFFER, previousFramebuffer);
8168
+ state.attachedTexture = textureState.handle;
9027
8169
  }
9028
8170
  }
8171
+ return state;
9029
8172
  }
9030
- _resolveLineLoopDrawCall(indices, vertexCount) {
9031
- const sourceIndices = indices.length > 0 ? indices : this._getSequentialIndices(vertexCount);
9032
- const sourceCount = sourceIndices.length;
9033
- if (sourceCount < 2) {
9034
- return null;
8173
+ _syncTexture(texture) {
8174
+ const gl = this._context;
8175
+ const state = this._getTextureState(texture);
8176
+ const version = texture instanceof RenderTexture ? texture.textureVersion : texture.version;
8177
+ gl.bindTexture(gl.TEXTURE_2D, state.handle);
8178
+ if (state.version !== version) {
8179
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, texture.scaleMode);
8180
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, texture.scaleMode);
8181
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, texture.wrapMode);
8182
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, texture.wrapMode);
8183
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, texture.premultiplyAlpha);
8184
+ if (texture instanceof RenderTexture) {
8185
+ if (state.version === -1 || state.width !== texture.width || state.height !== texture.height || texture.source === null) {
8186
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, texture.width, texture.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, texture.source);
8187
+ }
8188
+ else {
8189
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, texture.width, texture.height, gl.RGBA, gl.UNSIGNED_BYTE, texture.source);
8190
+ }
8191
+ }
8192
+ else if (texture.source) {
8193
+ if (state.version === -1 || state.width !== texture.width || state.height !== texture.height) {
8194
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.source);
8195
+ }
8196
+ else {
8197
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, texture.source);
8198
+ }
8199
+ }
8200
+ if (texture.generateMipMap && (texture instanceof RenderTexture || texture.source !== null)) {
8201
+ gl.generateMipmap(gl.TEXTURE_2D);
8202
+ }
8203
+ state.version = version;
8204
+ state.width = texture.width;
8205
+ state.height = texture.height;
9035
8206
  }
9036
- const loopIndexCount = sourceCount + 1;
9037
- const generatedIndices = this._ensureGeneratedIndexCapacity(loopIndexCount);
9038
- generatedIndices.set(sourceIndices.subarray(0, sourceCount), 0);
9039
- generatedIndices[sourceCount] = sourceIndices[0];
8207
+ return state;
8208
+ }
8209
+ _toClipPixels(bounds) {
8210
+ const topLeft = this._renderTarget.mapCoordsToPixel(this._clipPointA.set(bounds.left, bounds.top));
8211
+ const bottomRight = this._renderTarget.mapCoordsToPixel(this._clipPointB.set(bounds.right, bounds.bottom));
8212
+ const minX = Math.min(topLeft.x, bottomRight.x);
8213
+ const maxX = Math.max(topLeft.x, bottomRight.x);
8214
+ const minY = Math.min(topLeft.y, bottomRight.y);
8215
+ const maxY = Math.max(topLeft.y, bottomRight.y);
8216
+ const targetWidth = this._renderTarget.width;
8217
+ const targetHeight = this._renderTarget.height;
8218
+ const x = Math.max(0, Math.min(targetWidth, Math.floor(minX)));
8219
+ const right = Math.max(0, Math.min(targetWidth, Math.ceil(maxX)));
8220
+ const yTop = Math.max(0, Math.min(targetHeight, Math.floor(minY)));
8221
+ const bottom = Math.max(0, Math.min(targetHeight, Math.ceil(maxY)));
8222
+ const width = Math.max(0, right - x);
8223
+ const height = Math.max(0, bottom - yTop);
8224
+ const y = Math.max(0, targetHeight - bottom);
9040
8225
  return {
9041
- topology: 'line-strip',
9042
- usesStripIndex: true,
9043
- vertexCount,
9044
- indices: generatedIndices,
9045
- indexCount: loopIndexCount,
8226
+ x,
8227
+ y,
8228
+ width,
8229
+ height,
9046
8230
  };
9047
8231
  }
9048
- _resolveTriangleFanDrawCall(indices, vertexCount) {
9049
- const sourceIndices = indices.length > 0 ? indices : this._getSequentialIndices(vertexCount);
9050
- const sourceCount = sourceIndices.length;
9051
- if (sourceCount < 3) {
9052
- return null;
9053
- }
9054
- const indexCount = (sourceCount - 2) * 3;
9055
- const generatedIndices = this._ensureGeneratedIndexCapacity(indexCount);
9056
- let targetIndex = 0;
9057
- for (let index = 1; index < sourceCount - 1; index++) {
9058
- generatedIndices[targetIndex++] = sourceIndices[0];
9059
- generatedIndices[targetIndex++] = sourceIndices[index];
9060
- generatedIndices[targetIndex++] = sourceIndices[index + 1];
9061
- }
8232
+ _intersectClips(first, second) {
8233
+ const left = Math.max(first.x, second.x);
8234
+ const bottom = Math.max(first.y, second.y);
8235
+ const right = Math.min(first.x + first.width, second.x + second.width);
8236
+ const top = Math.min(first.y + first.height, second.y + second.height);
9062
8237
  return {
9063
- topology: 'triangle-list',
9064
- usesStripIndex: false,
9065
- vertexCount,
9066
- indices: generatedIndices,
9067
- indexCount,
8238
+ x: left,
8239
+ y: bottom,
8240
+ width: Math.max(0, right - left),
8241
+ height: Math.max(0, top - bottom),
9068
8242
  };
9069
8243
  }
9070
- _getSequentialIndices(vertexCount) {
9071
- if (vertexCount > this._sequentialIndexData.length) {
9072
- let nextLength = Math.max(1, this._sequentialIndexData.length);
9073
- while (nextLength < vertexCount) {
9074
- nextLength *= 2;
9075
- }
9076
- this._sequentialIndexData = new Uint16Array(nextLength);
8244
+ _applyClipState() {
8245
+ const gl = this._context;
8246
+ if (this._clipPixelStack.length === 0) {
8247
+ gl.disable(gl.SCISSOR_TEST);
8248
+ return;
8249
+ }
8250
+ const clip = this._clipPixelStack[this._clipPixelStack.length - 1];
8251
+ gl.enable(gl.SCISSOR_TEST);
8252
+ gl.scissor(clip.x, clip.y, clip.width, clip.height);
8253
+ }
8254
+ }
8255
+
8256
+ /// <reference types="@webgpu/types" />
8257
+ /**
8258
+ * Base class for WebGPU renderers.
8259
+ *
8260
+ * Manages the connect/disconnect lifecycle and provides a safe
8261
+ * `getBackend()` accessor that throws if the renderer is not connected.
8262
+ *
8263
+ * Subclasses must implement:
8264
+ * - onConnect(backend): set up GPU resources (shader modules, pipelines, buffers)
8265
+ * - onDisconnect(): tear down GPU resources
8266
+ * - render(drawable): collect draw call data for the given drawable
8267
+ * - flush(): encode and submit command buffers for all collected draw calls
8268
+ */
8269
+ class AbstractWebGpuRenderer {
8270
+ backendType = RenderBackendType.WebGpu;
8271
+ _backend = null;
8272
+ connect(backend) {
8273
+ if (this._backend !== null) {
8274
+ return;
9077
8275
  }
9078
- for (let index = 0; index < vertexCount; index++) {
9079
- this._sequentialIndexData[index] = index;
8276
+ if (backend.backendType !== RenderBackendType.WebGpu) {
8277
+ throw new Error(`${this.constructor.name} requires a WebGPU backend, `
8278
+ + `but received backendType ${String(backend.backendType)}.`);
9080
8279
  }
9081
- return this._sequentialIndexData.subarray(0, vertexCount);
8280
+ this._backend = backend;
8281
+ this.onConnect(backend);
9082
8282
  }
9083
- _ensureGeneratedIndexCapacity(indexCount) {
9084
- if (indexCount > this._generatedIndexData.length) {
9085
- let nextLength = Math.max(1, this._generatedIndexData.length);
9086
- while (nextLength < indexCount) {
9087
- nextLength *= 2;
9088
- }
9089
- this._generatedIndexData = new Uint16Array(nextLength);
8283
+ disconnect() {
8284
+ if (this._backend === null) {
8285
+ return;
8286
+ }
8287
+ this.flush();
8288
+ this.onDisconnect();
8289
+ this._backend = null;
8290
+ }
8291
+ getBackend() {
8292
+ if (this._backend === null) {
8293
+ throw new Error(`${this.constructor.name} is not connected to a backend.`);
9090
8294
  }
9091
- return this._generatedIndexData.subarray(0, indexCount);
8295
+ return this._backend;
9092
8296
  }
9093
- _destroyBuffers() {
9094
- this._vertexBuffer?.destroy();
9095
- this._indexBuffer?.destroy();
9096
- this._vertexBuffer = null;
9097
- this._indexBuffer = null;
9098
- this._vertexBufferCapacity = 0;
9099
- this._indexBufferCapacity = 0;
8297
+ getBackendOrNull() {
8298
+ return this._backend;
8299
+ }
8300
+ }
8301
+
8302
+ /// <reference types="@webgpu/types" />
8303
+ /**
8304
+ * Returns the GPUBlendState for a given ExoJS blend mode.
8305
+ * Shared by all WebGPU renderers to avoid duplication.
8306
+ */
8307
+ function getWebGpuBlendState(blendMode) {
8308
+ switch (blendMode) {
8309
+ case BlendModes.Additive:
8310
+ return {
8311
+ color: {
8312
+ operation: 'add',
8313
+ srcFactor: 'one',
8314
+ dstFactor: 'one',
8315
+ },
8316
+ alpha: {
8317
+ operation: 'add',
8318
+ srcFactor: 'one',
8319
+ dstFactor: 'one',
8320
+ },
8321
+ };
8322
+ case BlendModes.Subtract:
8323
+ return {
8324
+ color: {
8325
+ operation: 'add',
8326
+ srcFactor: 'zero',
8327
+ dstFactor: 'one-minus-src',
8328
+ },
8329
+ alpha: {
8330
+ operation: 'add',
8331
+ srcFactor: 'zero',
8332
+ dstFactor: 'one-minus-src-alpha',
8333
+ },
8334
+ };
8335
+ case BlendModes.Multiply:
8336
+ return {
8337
+ color: {
8338
+ operation: 'add',
8339
+ srcFactor: 'dst',
8340
+ dstFactor: 'one-minus-src-alpha',
8341
+ },
8342
+ alpha: {
8343
+ operation: 'add',
8344
+ srcFactor: 'dst-alpha',
8345
+ dstFactor: 'one-minus-src-alpha',
8346
+ },
8347
+ };
8348
+ case BlendModes.Screen:
8349
+ return {
8350
+ color: {
8351
+ operation: 'add',
8352
+ srcFactor: 'one',
8353
+ dstFactor: 'one-minus-src',
8354
+ },
8355
+ alpha: {
8356
+ operation: 'add',
8357
+ srcFactor: 'one',
8358
+ dstFactor: 'one-minus-src-alpha',
8359
+ },
8360
+ };
8361
+ default:
8362
+ return {
8363
+ color: {
8364
+ operation: 'add',
8365
+ srcFactor: 'one',
8366
+ dstFactor: 'one-minus-src-alpha',
8367
+ },
8368
+ alpha: {
8369
+ operation: 'add',
8370
+ srcFactor: 'one',
8371
+ dstFactor: 'one-minus-src-alpha',
8372
+ },
8373
+ };
9100
8374
  }
9101
8375
  }
9102
8376
 
@@ -10746,8 +10020,7 @@ class WebGpuMaskCompositor {
10746
10020
  }
10747
10021
  _writeProjectionMatrix(viewMatrix) {
10748
10022
  // Pack the 3x3 affine view matrix into a 4x4 column-major mat4x4
10749
- // for WGSL, mirroring the layout used by WebGpuPrimitiveRenderer
10750
- // (see `_writeShapeVertices` in that file for the rationale).
10023
+ // for WGSL.
10751
10024
  const m = this._projectionMatrix.copy(viewMatrix);
10752
10025
  const data = this._projectionData;
10753
10026
  // col 0
@@ -10814,7 +10087,6 @@ class WebGpuBackend {
10814
10087
  if (clearColor) {
10815
10088
  this._clearColor.copy(clearColor);
10816
10089
  }
10817
- this.rendererRegistry.registerRenderer(DrawableShape, new WebGpuPrimitiveRenderer());
10818
10090
  this.rendererRegistry.registerRenderer(Sprite, new WebGpuSpriteRenderer());
10819
10091
  this.rendererRegistry.registerRenderer(Mesh, new WebGpuMeshRenderer());
10820
10092
  this.rendererRegistry.registerRenderer(ParticleSystem, new WebGpuParticleRenderer());
@@ -14815,7 +14087,6 @@ const defaultAppSettings = {
14815
14087
  debug: false,
14816
14088
  spriteRendererBatchSize: 4096, // ~ 262kb
14817
14089
  particleRendererBatchSize: 8192, // ~ 1.18mb
14818
- primitiveRendererBatchSize: 65536, // ~ 786kb
14819
14090
  gamepadDefinitions: [],
14820
14091
  pointerDistanceThreshold: 10,
14821
14092
  webglAttributes: {
@@ -16347,24 +15618,30 @@ function signedArea(data, start, end, dim) {
16347
15618
  return sum;
16348
15619
  }
16349
15620
 
16350
- const buildLine = (startX, startY, endX, endY, width, vertices = [], indices = []) => {
15621
+ const buildLine = (startX, startY, endX, endY, width) => {
16351
15622
  const points = [startX, startY, endX, endY];
16352
15623
  const distance = width / 2;
16353
- const index = vertices.length / 6;
16354
15624
  const perpA = new Vector(startX - endX, startY - endY).perp().normalize().multiply(distance);
16355
15625
  const perpB = new Vector(endX - startX, endY - startY).perp().normalize().multiply(distance);
16356
- vertices.push(startX - perpA.x, startY - perpA.y);
16357
- vertices.push(startX + perpA.x, startY + perpA.y);
16358
- vertices.push(endX - perpB.x, endY - perpB.y);
16359
- vertices.push(endX + perpB.x, endY + perpB.y);
16360
- indices.push(index, index, index + 1, index + 2, index + 3, index + 3);
16361
- return new Geometry({ vertices, indices, points });
15626
+ const vertices = new Float32Array([
15627
+ startX - perpA.x, startY - perpA.y, // 0: start-left
15628
+ startX + perpA.x, startY + perpA.y, // 1: start-right
15629
+ endX - perpB.x, endY - perpB.y, // 2: end-left
15630
+ endX + perpB.x, endY + perpB.y, // 3: end-right
15631
+ ]);
15632
+ perpA.destroy();
15633
+ perpB.destroy();
15634
+ const indices = new Uint16Array([0, 1, 3, 0, 3, 2]);
15635
+ return { vertices, indices, points };
16362
15636
  };
16363
- const buildPath = (points, width, vertices = [], indices = []) => {
15637
+ const buildPath = (points, width) => {
16364
15638
  if (points.length < 4) {
16365
15639
  throw new Error('At least two X/Y pairs are required to build a line.');
16366
15640
  }
16367
- const lineWidth = width / 2, firstPoint = new Vector(points[0], points[1]), lastPoint = new Vector(points[points.length - 2], points[points.length - 1]);
15641
+ const lineWidth = width / 2;
15642
+ const firstPoint = new Vector(points[0], points[1]);
15643
+ const lastPoint = new Vector(points[points.length - 2], points[points.length - 1]);
15644
+ const outlinePoints = points;
16368
15645
  if (firstPoint.x === lastPoint.x && firstPoint.y === lastPoint.y) {
16369
15646
  points = points.slice();
16370
15647
  points.pop();
@@ -16375,9 +15652,10 @@ const buildPath = (points, width, vertices = [], indices = []) => {
16375
15652
  points.unshift(midPointX, midPointY);
16376
15653
  points.push(midPointX, midPointY);
16377
15654
  }
15655
+ firstPoint.destroy();
15656
+ lastPoint.destroy();
16378
15657
  const length = points.length / 2;
16379
- let indexCount = points.length;
16380
- let indexStart = vertices.length / 6;
15658
+ const stripVertices = [];
16381
15659
  let p1x = points[0];
16382
15660
  let p1y = points[1];
16383
15661
  let p2x = points[2];
@@ -16395,8 +15673,8 @@ const buildPath = (points, width, vertices = [], indices = []) => {
16395
15673
  perpy /= dist;
16396
15674
  perpx *= lineWidth;
16397
15675
  perpy *= lineWidth;
16398
- vertices.push(p1x - perpx, p1y - perpy);
16399
- vertices.push(p1x + perpx, p1y + perpy);
15676
+ stripVertices.push(p1x - perpx, p1y - perpy);
15677
+ stripVertices.push(p1x + perpx, p1y + perpy);
16400
15678
  for (let i = 1; i < length - 1; i++) {
16401
15679
  p1x = points[(i - 1) * 2];
16402
15680
  p1y = points[((i - 1) * 2) + 1];
@@ -16427,8 +15705,8 @@ const buildPath = (points, width, vertices = [], indices = []) => {
16427
15705
  let denom = (a1 * b2) - (a2 * b1);
16428
15706
  if (Math.abs(denom) < 0.1) {
16429
15707
  denom += 10.1;
16430
- vertices.push(p2x - perpx, p2y - perpy);
16431
- vertices.push(p2x + perpx, p2y + perpy);
15708
+ stripVertices.push(p2x - perpx, p2y - perpy);
15709
+ stripVertices.push(p2x + perpx, p2y + perpy);
16432
15710
  continue;
16433
15711
  }
16434
15712
  const px = ((b1 * c2) - (b2 * c1)) / denom;
@@ -16442,14 +15720,13 @@ const buildPath = (points, width, vertices = [], indices = []) => {
16442
15720
  perp3y /= dist;
16443
15721
  perp3x *= lineWidth;
16444
15722
  perp3y *= lineWidth;
16445
- vertices.push(p2x - perp3x, p2y - perp3y);
16446
- vertices.push(p2x + perp3x, p2y + perp3y);
16447
- vertices.push(p2x - perp3x, p2y - perp3y);
16448
- indexCount++;
15723
+ stripVertices.push(p2x - perp3x, p2y - perp3y);
15724
+ stripVertices.push(p2x + perp3x, p2y + perp3y);
15725
+ stripVertices.push(p2x - perp3x, p2y - perp3y);
16449
15726
  }
16450
15727
  else {
16451
- vertices.push(px, py);
16452
- vertices.push(p2x - (px - p2x), p2y - (py - p2y));
15728
+ stripVertices.push(px, py);
15729
+ stripVertices.push(p2x - (px - p2x), p2y - (py - p2y));
16453
15730
  }
16454
15731
  }
16455
15732
  p1x = points[(length - 2) * 2];
@@ -16463,70 +15740,112 @@ const buildPath = (points, width, vertices = [], indices = []) => {
16463
15740
  perpy /= dist;
16464
15741
  perpx *= lineWidth;
16465
15742
  perpy *= lineWidth;
16466
- vertices.push(p2x - perpx, p2y - perpy);
16467
- vertices.push(p2x + perpx, p2y + perpy);
16468
- indices.push(indexStart);
16469
- for (let i = 0; i < indexCount; i++) {
16470
- indices.push(indexStart++);
16471
- }
16472
- indices.push(indexStart - 1);
16473
- return new Geometry({ vertices, indices, points });
15743
+ stripVertices.push(p2x - perpx, p2y - perpy);
15744
+ stripVertices.push(p2x + perpx, p2y + perpy);
15745
+ // Convert strip-style vertex sequence to triangle-list indices.
15746
+ // For N strip vertices (N = stripVertices.length / 2), each i in [0, N-3]
15747
+ // produces a triangle. Even i: (i, i+1, i+2). Odd i: (i+1, i, i+2).
15748
+ // This preserves the same winding the original triangle-strip pipeline saw.
15749
+ const stripVertexCount = stripVertices.length / 2;
15750
+ const vertices = new Float32Array(stripVertices);
15751
+ const triangleCount = stripVertexCount >= 3 ? stripVertexCount - 2 : 0;
15752
+ const indices = new Uint16Array(triangleCount * 3);
15753
+ for (let i = 0; i < triangleCount; i++) {
15754
+ const base = i * 3;
15755
+ if ((i & 1) === 0) {
15756
+ indices[base] = i;
15757
+ indices[base + 1] = i + 1;
15758
+ indices[base + 2] = i + 2;
15759
+ }
15760
+ else {
15761
+ indices[base] = i + 1;
15762
+ indices[base + 1] = i;
15763
+ indices[base + 2] = i + 2;
15764
+ }
15765
+ }
15766
+ return { vertices, indices, points: outlinePoints };
16474
15767
  };
16475
- const buildCircle = (centerX, centerY, radius, vertices = [], indices = []) => {
16476
- const length = Math.floor(15 * Math.sqrt(radius + radius)), segment = (Math.PI * 2) / length, points = [];
16477
- let index = vertices.length / 6;
16478
- indices.push(index);
16479
- for (let i = 0; i < length + 1; i++) {
16480
- const segmentX = centerX + (Math.sin(segment * i) * radius), segmentY = centerY + (Math.cos(segment * i) * radius);
15768
+ const buildCircle = (centerX, centerY, radius) => {
15769
+ const length = Math.floor(15 * Math.sqrt(radius + radius));
15770
+ const segment = (Math.PI * 2) / length;
15771
+ const points = [];
15772
+ // 1 center vertex + N perimeter vertices.
15773
+ const vertices = new Float32Array((length + 1) * 2);
15774
+ vertices[0] = centerX;
15775
+ vertices[1] = centerY;
15776
+ for (let i = 0; i < length; i++) {
15777
+ const segmentX = centerX + (Math.sin(segment * i) * radius);
15778
+ const segmentY = centerY + (Math.cos(segment * i) * radius);
16481
15779
  points.push(segmentX, segmentY);
16482
- vertices.push(centerX, centerY);
16483
- vertices.push(segmentX, segmentY);
16484
- indices.push(index++, index++);
15780
+ const offset = (i + 1) * 2;
15781
+ vertices[offset] = segmentX;
15782
+ vertices[offset + 1] = segmentY;
15783
+ }
15784
+ const indices = new Uint16Array(length * 3);
15785
+ for (let i = 0; i < length; i++) {
15786
+ const base = i * 3;
15787
+ indices[base] = 0;
15788
+ indices[base + 1] = i + 1;
15789
+ indices[base + 2] = i + 2 > length ? 1 : i + 2;
16485
15790
  }
16486
- indices.push(index - 1);
16487
- return new Geometry({ vertices, indices, points });
15791
+ return { vertices, indices, points };
16488
15792
  };
16489
- const buildEllipse = (centerX, centerY, radiusX, radiusY, vertices = [], indices = []) => {
16490
- const length = Math.floor(15 * Math.sqrt(radiusX + radiusY)), segment = (Math.PI * 2) / length, points = [];
16491
- let index = vertices.length / 6;
16492
- indices.push(index);
16493
- for (let i = 0; i < length + 1; i++) {
16494
- const segmentX = centerX + (Math.sin(segment * i) * radiusX), segmentY = centerY + (Math.cos(segment * i) * radiusY);
15793
+ const buildEllipse = (centerX, centerY, radiusX, radiusY) => {
15794
+ const length = Math.floor(15 * Math.sqrt(radiusX + radiusY));
15795
+ const segment = (Math.PI * 2) / length;
15796
+ const points = [];
15797
+ const vertices = new Float32Array((length + 1) * 2);
15798
+ vertices[0] = centerX;
15799
+ vertices[1] = centerY;
15800
+ for (let i = 0; i < length; i++) {
15801
+ const segmentX = centerX + (Math.sin(segment * i) * radiusX);
15802
+ const segmentY = centerY + (Math.cos(segment * i) * radiusY);
16495
15803
  points.push(segmentX, segmentY);
16496
- vertices.push(centerX, centerY);
16497
- vertices.push(segmentX, segmentY);
16498
- indices.push(index++, index++);
15804
+ const offset = (i + 1) * 2;
15805
+ vertices[offset] = segmentX;
15806
+ vertices[offset + 1] = segmentY;
15807
+ }
15808
+ const indices = new Uint16Array(length * 3);
15809
+ for (let i = 0; i < length; i++) {
15810
+ const base = i * 3;
15811
+ indices[base] = 0;
15812
+ indices[base + 1] = i + 1;
15813
+ indices[base + 2] = i + 2 > length ? 1 : i + 2;
16499
15814
  }
16500
- indices.push(index - 1);
16501
- return new Geometry({ vertices, indices, points });
15815
+ return { vertices, indices, points };
16502
15816
  };
16503
- const buildPolygon = (points, vertices = [], indices = []) => {
15817
+ const buildPolygon = (points) => {
16504
15818
  if (points.length < 6) {
16505
15819
  throw new Error('At least three X/Y pairs are required to build a polygon.');
16506
15820
  }
16507
- const index = vertices.length / 6, length = points.length / 2, triangles = earcut(points, [], 2);
16508
- if (triangles) {
16509
- for (let i = 0; i < triangles.length; i += 3) {
16510
- indices.push(triangles[i] + index);
16511
- indices.push(triangles[i] + index);
16512
- indices.push(triangles[i + 1] + index);
16513
- indices.push(triangles[i + 2] + index);
16514
- indices.push(triangles[i + 2] + index);
16515
- }
16516
- for (let i = 0; i < length; i++) {
16517
- vertices.push(points[i * 2], points[(i * 2) + 1]);
16518
- }
15821
+ const length = points.length / 2;
15822
+ const triangles = earcut(points, [], 2);
15823
+ const vertices = new Float32Array(points.length);
15824
+ for (let i = 0; i < length; i++) {
15825
+ vertices[i * 2] = points[i * 2];
15826
+ vertices[(i * 2) + 1] = points[(i * 2) + 1];
16519
15827
  }
16520
- return new Geometry({ vertices, indices, points });
15828
+ const indices = triangles ? new Uint16Array(triangles) : new Uint16Array(0);
15829
+ return { vertices, indices, points };
16521
15830
  };
16522
- const buildRectangle = (x, y, width, height, vertices = [], indices = []) => {
16523
- const points = [x, y, x + width, y, x, y + height, x + width, y + height], index = vertices.length / 6;
16524
- vertices.push(...points);
16525
- indices.push(index, index, index + 1, index + 2, index + 3, index + 3);
16526
- return new Geometry({ vertices, indices, points });
15831
+ const buildRectangle = (x, y, width, height) => {
15832
+ // 4 vertices: TL, TR, BL, BR. Triangles [0, 1, 2, 1, 3, 2] (clockwise).
15833
+ const vertices = new Float32Array([
15834
+ x, y, // 0 TL
15835
+ x + width, y, // 1 TR
15836
+ x, y + height, // 2 BL
15837
+ x + width, y + height, // 3 BR
15838
+ ]);
15839
+ const indices = new Uint16Array([0, 1, 2, 1, 3, 2]);
15840
+ // Outline points walk the perimeter (TL -> TR -> BR -> BL).
15841
+ const points = [x, y, x + width, y, x + width, y + height, x, y + height];
15842
+ return { vertices, indices, points };
16527
15843
  };
16528
15844
  const buildStar = (centerX, centerY, points, radius, innerRadius = radius / 2, rotation = 0) => {
16529
- const startAngle = (Math.PI / -2) + rotation, length = points * 2, delta = tau / length, path = [];
15845
+ const startAngle = (Math.PI / -2) + rotation;
15846
+ const length = points * 2;
15847
+ const delta = tau / length;
15848
+ const path = [];
16530
15849
  for (let i = 0; i < length; i++) {
16531
15850
  const angle = startAngle + (i * delta);
16532
15851
  const rad = i % 2 ? innerRadius : radius;
@@ -17369,23 +16688,6 @@ class UniversalEmitter {
17369
16688
  }
17370
16689
  }
17371
16690
 
17372
- class CircleGeometry extends Geometry {
17373
- constructor(centerX, centerY, radius) {
17374
- const length = Math.floor(15 * Math.sqrt(radius + radius)), segment = (Math.PI * 2) / length, vertices = [], indices = [], points = [];
17375
- let index = vertices.length / 6;
17376
- indices.push(index);
17377
- for (let i = 0; i < length + 1; i++) {
17378
- const segmentX = centerX + (Math.sin(segment * i) * radius), segmentY = centerY + (Math.cos(segment * i) * radius);
17379
- points.push(segmentX, segmentY);
17380
- vertices.push(centerX, centerY);
17381
- vertices.push(segmentX, segmentY);
17382
- indices.push(index++, index++);
17383
- }
17384
- indices.push(index - 1);
17385
- super({ vertices, indices, points });
17386
- }
17387
- }
17388
-
17389
16691
  class Graphics extends Container {
17390
16692
  _lineWidth = 0;
17391
16693
  _lineColor = new Color();
@@ -17416,14 +16718,14 @@ class Graphics extends Container {
17416
16718
  return super.getChildAt(index);
17417
16719
  }
17418
16720
  addChild(child) {
17419
- if (!(child instanceof DrawableShape)) {
17420
- throw new Error('Graphics can only contain DrawableShape children.');
16721
+ if (!(child instanceof Mesh)) {
16722
+ throw new Error('Graphics can only contain Mesh children.');
17421
16723
  }
17422
16724
  return super.addChild(child);
17423
16725
  }
17424
16726
  addChildAt(child, index) {
17425
- if (!(child instanceof DrawableShape)) {
17426
- throw new Error('Graphics can only contain DrawableShape children.');
16727
+ if (!(child instanceof Mesh)) {
16728
+ throw new Error('Graphics can only contain Mesh children.');
17427
16729
  }
17428
16730
  return super.addChildAt(child, index);
17429
16731
  }
@@ -17519,50 +16821,52 @@ class Graphics extends Container {
17519
16821
  return this;
17520
16822
  }
17521
16823
  drawLine(startX, startY, endX, endY) {
17522
- this.addChild(new DrawableShape(buildLine(startX, startY, endX, endY, this._lineWidth), this._lineColor, RenderingPrimitives.TriangleStrip));
16824
+ const data = buildLine(startX, startY, endX, endY, this._lineWidth);
16825
+ this.addChild(this._createMesh(data, this._lineColor));
17523
16826
  return this;
17524
16827
  }
17525
16828
  drawPath(path) {
17526
- this.addChild(new DrawableShape(buildPath(path, this._lineWidth), this._lineColor, RenderingPrimitives.TriangleStrip));
16829
+ const data = buildPath(path, this._lineWidth);
16830
+ this.addChild(this._createMesh(data, this._lineColor));
17527
16831
  return this;
17528
16832
  }
17529
16833
  drawPolygon(path) {
17530
- const polygon = buildPolygon(path);
17531
- this.addChild(new DrawableShape(polygon, this._fillColor, RenderingPrimitives.TriangleStrip));
16834
+ const data = buildPolygon(path);
16835
+ this.addChild(this._createMesh(data, this._fillColor));
17532
16836
  if (this._lineWidth > 0) {
17533
- this.drawPath(polygon.points);
16837
+ this.drawPath(data.points);
17534
16838
  }
17535
16839
  return this;
17536
16840
  }
17537
16841
  drawCircle(centerX, centerY, radius) {
17538
- const circle = new CircleGeometry(centerX, centerY, radius);
17539
- this.addChild(new DrawableShape(circle, this._fillColor, RenderingPrimitives.TriangleStrip));
16842
+ const data = buildCircle(centerX, centerY, radius);
16843
+ this.addChild(this._createMesh(data, this._fillColor));
17540
16844
  if (this._lineWidth > 0) {
17541
- this.drawPath(circle.points);
16845
+ this.drawPath(data.points);
17542
16846
  }
17543
16847
  return this;
17544
16848
  }
17545
16849
  drawEllipse(centerX, centerY, radiusX, radiusY) {
17546
- const ellipse = buildEllipse(centerX, centerY, radiusX, radiusY);
17547
- this.addChild(new DrawableShape(ellipse, this._fillColor, RenderingPrimitives.TriangleStrip));
16850
+ const data = buildEllipse(centerX, centerY, radiusX, radiusY);
16851
+ this.addChild(this._createMesh(data, this._fillColor));
17548
16852
  if (this._lineWidth > 0) {
17549
- this.drawPath(ellipse.points);
16853
+ this.drawPath(data.points);
17550
16854
  }
17551
16855
  return this;
17552
16856
  }
17553
16857
  drawRectangle(x, y, width, height) {
17554
- const rectangle = buildRectangle(x, y, width, height);
17555
- this.addChild(new DrawableShape(rectangle, this._fillColor, RenderingPrimitives.TriangleStrip));
16858
+ const data = buildRectangle(x, y, width, height);
16859
+ this.addChild(this._createMesh(data, this._fillColor));
17556
16860
  if (this._lineWidth > 0) {
17557
- this.drawPath(rectangle.points);
16861
+ this.drawPath(data.points);
17558
16862
  }
17559
16863
  return this;
17560
16864
  }
17561
16865
  drawStar(centerX, centerY, points, radius, innerRadius = radius / 2, rotation = 0) {
17562
- const star = buildStar(centerX, centerY, points, radius, innerRadius, rotation);
17563
- this.addChild(new DrawableShape(star, this._fillColor, RenderingPrimitives.TriangleStrip));
16866
+ const data = buildStar(centerX, centerY, points, radius, innerRadius, rotation);
16867
+ this.addChild(this._createMesh(data, this._fillColor));
17564
16868
  if (this._lineWidth > 0) {
17565
- this.drawPath(star.points);
16869
+ this.drawPath(data.points);
17566
16870
  }
17567
16871
  return this;
17568
16872
  }
@@ -17581,6 +16885,14 @@ class Graphics extends Container {
17581
16885
  this._fillColor.destroy();
17582
16886
  this._currentPoint.destroy();
17583
16887
  }
16888
+ _createMesh(data, color) {
16889
+ const mesh = new Mesh({
16890
+ vertices: data.vertices,
16891
+ indices: data.indices,
16892
+ });
16893
+ mesh.tint = color;
16894
+ return mesh;
16895
+ }
17584
16896
  }
17585
16897
 
17586
16898
  class Spritesheet {
@@ -19197,5 +18509,5 @@ const createRapierPhysicsWorld = async (options = {}) => {
19197
18509
  return await RapierPhysicsWorld.create(options);
19198
18510
  };
19199
18511
 
19200
- export { AbstractAssetFactory, AbstractMedia, AbstractWebGl2BatchedRenderer, AbstractWebGl2Renderer, AbstractWebGpuRenderer, AnimatedSprite, Application, ApplicationStatus, ArcadeStickGamepadMapping, AudioAnalyser, BinaryFactory, BlendModes, BlurFilter, Bounds, BufferTypes, BufferUsage, BundleLoadError, CacheFirstStrategy, CallbackRenderPass, Capabilities, ChannelOffset, ChannelSize, Circle, CircleGeometry, Clock, CollisionType, Color, ColorAffector, ColorFilter, Container, Drawable, DrawableShape, Ellipse, FactoryRegistry, Filter, Flags, FontFactory, ForceAffector, GameCubeGamepadMapping, Gamepad, GamepadChannel, GamepadControl, GamepadMapping, GamepadMappingFamily, GamepadPromptLayouts, GenericDualAnalogGamepadMapping, Geometry, Graphics, ImageFactory, IndexedDbDatabase, IndexedDbStore, Input, InputManager, Interval, JoyConLeftGamepadMapping, JoyConRightGamepadMapping, Json, JsonFactory, Keyboard, Line, Loader, Matrix, Mesh, Music, MusicFactory, NetworkOnlyStrategy, ObservableSize, ObservableVector, Particle, ParticleOptions, ParticleSystem, PlayStationGamepadMapping, Pointer, PointerState, PointerStateFlag, PolarVector, Polygon, Quadtree, Random, RapierPhysicsBinding, RapierPhysicsWorld, Rectangle, RenderBackendType, RenderNode, RenderTarget, RenderTargetPass, RenderTexture, RendererRegistry, RenderingPrimitives, Sampler, ScaleAffector, ScaleModes, Scene, SceneManager, SceneNode, Segment, Shader, ShaderAttribute, ShaderPrimitives, ShaderUniform, Signal, Size, Sound, SoundFactory, Sprite, SpriteFlags, Spritesheet, SteamControllerGamepadMapping, SvgAsset, SvgFactory, SwitchProGamepadMapping, Text, TextAsset, TextFactory, TextStyle, Texture, TextureFactory, Time, Timer, TorqueAffector, UniversalEmitter, Vector, Video, VideoFactory, View, ViewFlags, VoronoiRegion, VttAsset, VttFactory, WasmFactory, WebGl2Backend, WebGl2MeshRenderer, WebGl2ParticleRenderer, WebGl2PrimitiveRenderer, WebGl2RenderBuffer, WebGl2ShaderBlock, WebGl2SpriteRenderer, WebGl2VertexArrayObject, WebGpuBackend, WebGpuMeshRenderer, WebGpuParticleRenderer, WebGpuPrimitiveRenderer, WebGpuSpriteRenderer, WrapModes, XboxGamepadMapping, bezierCurveTo, buildCircle, buildEllipse, buildLine, buildPath, buildPolygon, buildRectangle, buildStar, builtInGamepadDefinitions, canvasSourceToDataUrl, clamp, createRapierPhysicsWorld, createRenderStats, createWebGl2ShaderProgram, decodeAudioData, defineAssetManifest, degreesPerRadian, degreesToRadians, determineMimeType, emptyArrayBuffer, getAudioContext, getCanvasSourceSize, getCollisionCircleCircle, getCollisionCircleRectangle, getCollisionPolygonCircle, getCollisionRectangleRectangle, getCollisionSat, getDistance, getOfflineAudioContext, getPreciseTime, getTextureSourceSize, getVoronoiRegion$1 as getVoronoiRegion, getWebGpuBlendState, hours, inRange, intersectionCircleCircle, intersectionCircleEllipse, intersectionCirclePoly, intersectionEllipseEllipse, intersectionEllipsePoly, intersectionLineCircle, intersectionLineEllipse, intersectionLineLine, intersectionLinePoly, intersectionLineRect, intersectionPointCircle, intersectionPointEllipse, intersectionPointLine, intersectionPointPoint, intersectionPointPoly, intersectionPointRect, intersectionPolyPoly, intersectionRectCircle, intersectionRectEllipse, intersectionRectPoly, intersectionRectRect, intersectionSat, isAudioContextReady, isPowerOfTwo, lerp, matchesIds, milliseconds, minutes, noop$1 as noop, normalizeIds, onAudioContextReady, parseGamepadDescriptor, quadraticCurveTo, radiansPerDegree, radiansToDegrees, rand, removeArrayItems, resetRenderStats, resolveDefinition, resolveGamepadDefinition, seconds, sign$1 as sign, stopEvent, supportsCodec, supportsEventOptions, supportsIndexedDb, supportsPointerEvents, supportsTouchEvents, supportsWebAudio, tau, trimRotation, webGl2PrimitiveArrayConstructors, webGl2PrimitiveByteSizeMapping, webGl2PrimitiveTypeNames };
18512
+ export { AbstractAssetFactory, AbstractMedia, AbstractWebGl2BatchedRenderer, AbstractWebGl2Renderer, AbstractWebGpuRenderer, AnimatedSprite, Application, ApplicationStatus, ArcadeStickGamepadMapping, AudioAnalyser, BinaryFactory, BlendModes, BlurFilter, Bounds, BufferTypes, BufferUsage, BundleLoadError, CacheFirstStrategy, CallbackRenderPass, Capabilities, ChannelOffset, ChannelSize, Circle, Clock, CollisionType, Color, ColorAffector, ColorFilter, Container, Drawable, Ellipse, FactoryRegistry, Filter, Flags, FontFactory, ForceAffector, GameCubeGamepadMapping, Gamepad, GamepadChannel, GamepadControl, GamepadMapping, GamepadMappingFamily, GamepadPromptLayouts, GenericDualAnalogGamepadMapping, Graphics, ImageFactory, IndexedDbDatabase, IndexedDbStore, Input, InputManager, Interval, JoyConLeftGamepadMapping, JoyConRightGamepadMapping, Json, JsonFactory, Keyboard, Line, Loader, Matrix, Mesh, Music, MusicFactory, NetworkOnlyStrategy, ObservableSize, ObservableVector, Particle, ParticleOptions, ParticleSystem, PlayStationGamepadMapping, Pointer, PointerState, PointerStateFlag, PolarVector, Polygon, Quadtree, Random, RapierPhysicsBinding, RapierPhysicsWorld, Rectangle, RenderBackendType, RenderNode, RenderTarget, RenderTargetPass, RenderTexture, RendererRegistry, RenderingPrimitives, Sampler, ScaleAffector, ScaleModes, Scene, SceneManager, SceneNode, Segment, Shader, ShaderAttribute, ShaderPrimitives, ShaderUniform, Signal, Size, Sound, SoundFactory, Sprite, SpriteFlags, Spritesheet, SteamControllerGamepadMapping, SvgAsset, SvgFactory, SwitchProGamepadMapping, Text, TextAsset, TextFactory, TextStyle, Texture, TextureFactory, Time, Timer, TorqueAffector, UniversalEmitter, Vector, Video, VideoFactory, View, ViewFlags, VoronoiRegion, VttAsset, VttFactory, WasmFactory, WebGl2Backend, WebGl2MeshRenderer, WebGl2ParticleRenderer, WebGl2RenderBuffer, WebGl2ShaderBlock, WebGl2SpriteRenderer, WebGl2VertexArrayObject, WebGpuBackend, WebGpuMeshRenderer, WebGpuParticleRenderer, WebGpuSpriteRenderer, WrapModes, XboxGamepadMapping, bezierCurveTo, buildCircle, buildEllipse, buildLine, buildPath, buildPolygon, buildRectangle, buildStar, builtInGamepadDefinitions, canvasSourceToDataUrl, clamp, createRapierPhysicsWorld, createRenderStats, createWebGl2ShaderProgram, decodeAudioData, defineAssetManifest, degreesPerRadian, degreesToRadians, determineMimeType, emptyArrayBuffer, getAudioContext, getCanvasSourceSize, getCollisionCircleCircle, getCollisionCircleRectangle, getCollisionPolygonCircle, getCollisionRectangleRectangle, getCollisionSat, getDistance, getOfflineAudioContext, getPreciseTime, getTextureSourceSize, getVoronoiRegion$1 as getVoronoiRegion, getWebGpuBlendState, hours, inRange, intersectionCircleCircle, intersectionCircleEllipse, intersectionCirclePoly, intersectionEllipseEllipse, intersectionEllipsePoly, intersectionLineCircle, intersectionLineEllipse, intersectionLineLine, intersectionLinePoly, intersectionLineRect, intersectionPointCircle, intersectionPointEllipse, intersectionPointLine, intersectionPointPoint, intersectionPointPoly, intersectionPointRect, intersectionPolyPoly, intersectionRectCircle, intersectionRectEllipse, intersectionRectPoly, intersectionRectRect, intersectionSat, isAudioContextReady, isPowerOfTwo, lerp, matchesIds, milliseconds, minutes, noop$1 as noop, normalizeIds, onAudioContextReady, parseGamepadDescriptor, quadraticCurveTo, radiansPerDegree, radiansToDegrees, rand, removeArrayItems, resetRenderStats, resolveDefinition, resolveGamepadDefinition, seconds, sign$1 as sign, stopEvent, supportsCodec, supportsEventOptions, supportsIndexedDb, supportsPointerEvents, supportsTouchEvents, supportsWebAudio, tau, trimRotation, webGl2PrimitiveArrayConstructors, webGl2PrimitiveByteSizeMapping, webGl2PrimitiveTypeNames };
19201
18513
  //# sourceMappingURL=exo.esm.js.map