@codexo/exojs 0.6.3 → 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 (46) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/dist/esm/core/Application.d.ts +8 -1
  3. package/dist/esm/core/Application.js +17 -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/core/capabilities.d.ts +30 -29
  8. package/dist/esm/core/capabilities.js +163 -61
  9. package/dist/esm/core/capabilities.js.map +1 -1
  10. package/dist/esm/index.js +1 -6
  11. package/dist/esm/index.js.map +1 -1
  12. package/dist/esm/math/geometry.d.ts +12 -8
  13. package/dist/esm/math/geometry.js +119 -72
  14. package/dist/esm/math/geometry.js.map +1 -1
  15. package/dist/esm/rendering/index.d.ts +0 -5
  16. package/dist/esm/rendering/primitives/Graphics.d.ts +3 -2
  17. package/dist/esm/rendering/primitives/Graphics.js +33 -25
  18. package/dist/esm/rendering/primitives/Graphics.js.map +1 -1
  19. package/dist/esm/rendering/webgl2/WebGl2Backend.js +1 -4
  20. package/dist/esm/rendering/webgl2/WebGl2Backend.js.map +1 -1
  21. package/dist/esm/rendering/webgpu/WebGpuBackend.js +0 -3
  22. package/dist/esm/rendering/webgpu/WebGpuBackend.js.map +1 -1
  23. package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.js +1 -2
  24. package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.js.map +1 -1
  25. package/dist/exo.esm.js +710 -1280
  26. package/dist/exo.esm.js.map +1 -1
  27. package/package.json +1 -1
  28. package/dist/esm/rendering/primitives/CircleGeometry.d.ts +0 -4
  29. package/dist/esm/rendering/primitives/CircleGeometry.js +0 -21
  30. package/dist/esm/rendering/primitives/CircleGeometry.js.map +0 -1
  31. package/dist/esm/rendering/primitives/DrawableShape.d.ts +0 -11
  32. package/dist/esm/rendering/primitives/DrawableShape.js +0 -21
  33. package/dist/esm/rendering/primitives/DrawableShape.js.map +0 -1
  34. package/dist/esm/rendering/primitives/Geometry.d.ts +0 -13
  35. package/dist/esm/rendering/primitives/Geometry.js +0 -16
  36. package/dist/esm/rendering/primitives/Geometry.js.map +0 -1
  37. package/dist/esm/rendering/webgl2/WebGl2PrimitiveRenderer.d.ts +0 -26
  38. package/dist/esm/rendering/webgl2/WebGl2PrimitiveRenderer.js +0 -222
  39. package/dist/esm/rendering/webgl2/WebGl2PrimitiveRenderer.js.map +0 -1
  40. package/dist/esm/rendering/webgl2/glsl/primitive.frag.js +0 -4
  41. package/dist/esm/rendering/webgl2/glsl/primitive.frag.js.map +0 -1
  42. package/dist/esm/rendering/webgl2/glsl/primitive.vert.js +0 -4
  43. package/dist/esm/rendering/webgl2/glsl/primitive.vert.js.map +0 -1
  44. package/dist/esm/rendering/webgpu/WebGpuPrimitiveRenderer.d.ts +0 -38
  45. package/dist/esm/rendering/webgpu/WebGpuPrimitiveRenderer.js +0 -488
  46. package/dist/esm/rendering/webgpu/WebGpuPrimitiveRenderer.js.map +0 -1
package/dist/exo.esm.js CHANGED
@@ -372,6 +372,202 @@ const canvasSourceToDataUrl = (source) => {
372
372
  return canvasElement.toDataURL();
373
373
  };
374
374
 
375
+ /// <reference types="@webgpu/types" />
376
+ // Browser-environment feature detection. Construction is private; the
377
+ // only public entry is `Capabilities.ready`, a lazy-cached `Promise<Capabilities>`
378
+ // that fires the (mostly) async probes on first access and returns the
379
+ // same Promise for every subsequent call. Once it resolves, the returned
380
+ // instance is frozen — every property is read once and never mutates.
381
+ //
382
+ // Synchronous callsites should keep the resolved instance in scope (e.g.,
383
+ // `app.capabilities` after `await app.start(...)`); there is no global
384
+ // sync mirror, by design.
385
+ const hasWindow = typeof window !== 'undefined';
386
+ const hasDocument = typeof document !== 'undefined';
387
+ const hasNavigator = typeof navigator !== 'undefined';
388
+ class Capabilities {
389
+ static _readyPromise = null;
390
+ /**
391
+ * Lazy-cached Promise that resolves to a frozen Capabilities instance.
392
+ *
393
+ * The first read kicks off the async probes (currently just the WebGPU
394
+ * adapter request); every subsequent read returns the same Promise.
395
+ * Concurrent callers share the in-flight detection — no double work.
396
+ *
397
+ * Early-warmup pattern for callers who want to overlap detection with
398
+ * other startup work:
399
+ *
400
+ * ```ts
401
+ * void Capabilities.ready; // fire-and-forget; starts probes now
402
+ * // ... unrelated bootstrap ...
403
+ * const caps = await Capabilities.ready; // typically already resolved
404
+ * ```
405
+ */
406
+ static get ready() {
407
+ if (Capabilities._readyPromise === null) {
408
+ Capabilities._readyPromise = Capabilities._detect();
409
+ }
410
+ return Capabilities._readyPromise;
411
+ }
412
+ webgl2;
413
+ webgpu;
414
+ webgpuAdapter;
415
+ webgpuVendor;
416
+ webgpuArchitecture;
417
+ pointer;
418
+ keyboard;
419
+ gamepad;
420
+ touch;
421
+ maxTouchPoints;
422
+ audio;
423
+ fullscreen;
424
+ vibration;
425
+ offscreenCanvas;
426
+ webWorkers;
427
+ devicePixelRatio;
428
+ constructor(values) {
429
+ this.webgl2 = values.webgl2;
430
+ this.webgpu = values.webgpu;
431
+ this.webgpuAdapter = values.webgpuAdapter;
432
+ this.webgpuVendor = values.webgpuVendor;
433
+ this.webgpuArchitecture = values.webgpuArchitecture;
434
+ this.pointer = values.pointer;
435
+ this.keyboard = values.keyboard;
436
+ this.gamepad = values.gamepad;
437
+ this.touch = values.touch;
438
+ this.maxTouchPoints = values.maxTouchPoints;
439
+ this.audio = values.audio;
440
+ this.fullscreen = values.fullscreen;
441
+ this.vibration = values.vibration;
442
+ this.offscreenCanvas = values.offscreenCanvas;
443
+ this.webWorkers = values.webWorkers;
444
+ this.devicePixelRatio = values.devicePixelRatio;
445
+ Object.freeze(this);
446
+ }
447
+ static async _detect() {
448
+ const [webgpuAdapter, webgpuInfo] = await probeWebGpu();
449
+ return new Capabilities({
450
+ webgl2: probeWebGl2(),
451
+ webgpu: probeWebGpuApiSurface(),
452
+ webgpuAdapter,
453
+ webgpuVendor: webgpuInfo?.vendor ?? null,
454
+ webgpuArchitecture: webgpuInfo?.architecture ?? null,
455
+ pointer: probePointer(),
456
+ keyboard: probeKeyboard(),
457
+ gamepad: probeGamepad(),
458
+ touch: probeTouchSupported(),
459
+ maxTouchPoints: probeMaxTouchPoints(),
460
+ audio: probeAudio(),
461
+ fullscreen: probeFullscreen(),
462
+ vibration: probeVibration(),
463
+ offscreenCanvas: probeOffscreenCanvas(),
464
+ webWorkers: probeWebWorkers(),
465
+ devicePixelRatio: hasWindow ? window.devicePixelRatio : 1,
466
+ });
467
+ }
468
+ }
469
+ // --- probes ---------------------------------------------------------------
470
+ function probeWebGl2() {
471
+ if (!hasDocument)
472
+ return false;
473
+ try {
474
+ const canvas = document.createElement('canvas');
475
+ const gl = canvas.getContext('webgl2');
476
+ return gl !== null;
477
+ }
478
+ catch {
479
+ return false;
480
+ }
481
+ }
482
+ function probeWebGpuApiSurface() {
483
+ return hasNavigator && 'gpu' in navigator;
484
+ }
485
+ async function probeWebGpu() {
486
+ if (!probeWebGpuApiSurface())
487
+ return [null, null];
488
+ const gpu = navigator.gpu;
489
+ if (!gpu || typeof gpu.requestAdapter !== 'function')
490
+ return [null, null];
491
+ try {
492
+ const adapter = await gpu.requestAdapter();
493
+ if (!adapter)
494
+ return [null, null];
495
+ // Modern path: GPUAdapter.info is a sync property (Chrome 116+,
496
+ // Safari 18+). Older browsers exposed a deprecated async
497
+ // requestAdapterInfo() instead. Try the modern path first, fall
498
+ // back if needed.
499
+ const adapterAny = adapter;
500
+ if (adapterAny.info) {
501
+ return [adapter, adapterAny.info];
502
+ }
503
+ if (typeof adapterAny.requestAdapterInfo === 'function') {
504
+ try {
505
+ return [adapter, await adapterAny.requestAdapterInfo()];
506
+ }
507
+ catch {
508
+ return [adapter, null];
509
+ }
510
+ }
511
+ return [adapter, null];
512
+ }
513
+ catch {
514
+ return [null, null];
515
+ }
516
+ }
517
+ function probePointer() {
518
+ return hasWindow && 'PointerEvent' in window;
519
+ }
520
+ function probeKeyboard() {
521
+ return hasWindow && 'KeyboardEvent' in window;
522
+ }
523
+ function probeGamepad() {
524
+ return hasNavigator && typeof navigator.getGamepads === 'function';
525
+ }
526
+ function probeTouchSupported() {
527
+ if (!hasWindow)
528
+ return false;
529
+ if ('ontouchstart' in window)
530
+ return true;
531
+ if (probeMaxTouchPoints() > 0)
532
+ return true;
533
+ return false;
534
+ }
535
+ function probeMaxTouchPoints() {
536
+ if (!hasNavigator)
537
+ return 0;
538
+ const points = navigator.maxTouchPoints;
539
+ return typeof points === 'number' ? points : 0;
540
+ }
541
+ function probeAudio() {
542
+ if (!hasWindow)
543
+ return false;
544
+ const w = window;
545
+ return typeof w.AudioContext !== 'undefined' || typeof w.webkitAudioContext !== 'undefined';
546
+ }
547
+ function probeFullscreen() {
548
+ if (!hasDocument)
549
+ return false;
550
+ const el = document.documentElement;
551
+ return typeof el.requestFullscreen === 'function' || typeof el.webkitRequestFullscreen === 'function';
552
+ }
553
+ function probeVibration() {
554
+ return hasNavigator && typeof navigator.vibrate === 'function';
555
+ }
556
+ function probeOffscreenCanvas() {
557
+ // The browser global is verbatim `OffscreenCanvas`; eslint's
558
+ // strict-camelCase rule rejects the property name even though we
559
+ // can't rename a web standard.
560
+ // eslint-disable-next-line @typescript-eslint/naming-convention
561
+ return hasWindow && typeof window.OffscreenCanvas !== 'undefined';
562
+ }
563
+ function probeWebWorkers() {
564
+ // The browser global is verbatim `Worker`; eslint's strict-camelCase
565
+ // rule rejects the property name even though we can't rename a web
566
+ // standard.
567
+ // eslint-disable-next-line @typescript-eslint/naming-convention
568
+ return hasWindow && typeof window.Worker === 'function';
569
+ }
570
+
375
571
  class Clock {
376
572
  _startTime;
377
573
  _elapsedTime = new Time(0);
@@ -3446,37 +3642,117 @@ class Drawable extends RenderNode {
3446
3642
  }
3447
3643
  }
3448
3644
 
3449
- class DrawableShape extends Drawable {
3450
- geometry;
3451
- drawMode;
3452
- color;
3453
- 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) {
3454
3680
  super();
3455
- this.geometry = geometry;
3456
- this.color = color.clone();
3457
- 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();
3458
3714
  }
3459
- destroy() {
3460
- super.destroy();
3461
- this.color.destroy();
3715
+ get vertexCount() {
3716
+ return this.vertices.length / 2;
3462
3717
  }
3463
- }
3464
-
3465
- class Geometry {
3466
- vertices;
3467
- indices;
3468
- points;
3469
- constructor({ vertices = [], indices = [], points = [], } = {}) {
3470
- this.vertices = new Float32Array(vertices);
3471
- this.indices = new Uint16Array(indices);
3472
- this.points = points;
3718
+ get indexCount() {
3719
+ return this.indices?.length ?? this.vertexCount;
3473
3720
  }
3474
- destroy() {
3475
- // 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;
3476
3752
  }
3477
3753
  }
3478
3754
 
3479
- class TransitionOverlayShape extends DrawableShape {
3755
+ class TransitionOverlayMesh extends Mesh {
3480
3756
  render(backend) {
3481
3757
  if (this.visible) {
3482
3758
  backend.draw(this);
@@ -3484,16 +3760,16 @@ class TransitionOverlayShape extends DrawableShape {
3484
3760
  return this;
3485
3761
  }
3486
3762
  }
3487
- const createOverlayGeometry = () => new Geometry({
3488
- vertices: [0, 0, 1, 0, 0, 1, 1, 1],
3489
- indices: [0, 1, 2, 3],
3490
- 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]),
3491
3767
  });
3492
3768
  const defaultFadeTransitionDuration = 220;
3493
3769
  class SceneManager {
3494
3770
  _app;
3495
3771
  _stack = [];
3496
- _transitionOverlay = new TransitionOverlayShape(createOverlayGeometry(), Color.black, RenderingPrimitives.TriangleStrip);
3772
+ _transitionOverlay = createOverlayMesh();
3497
3773
  _transition = null;
3498
3774
  onChangeScene = new Signal();
3499
3775
  onStartScene = new Signal();
@@ -3856,7 +4132,8 @@ class SceneManager {
3856
4132
  const overlayColor = transition ? transition.color : Color.black;
3857
4133
  const backend = this._app.backend;
3858
4134
  const bounds = backend.view.getBounds();
3859
- const vertices = this._transitionOverlay.geometry.vertices;
4135
+ const overlay = this._transitionOverlay;
4136
+ const vertices = overlay.vertices;
3860
4137
  vertices[0] = bounds.left;
3861
4138
  vertices[1] = bounds.top;
3862
4139
  vertices[2] = bounds.right;
@@ -3865,8 +4142,8 @@ class SceneManager {
3865
4142
  vertices[5] = bounds.bottom;
3866
4143
  vertices[6] = bounds.right;
3867
4144
  vertices[7] = bounds.bottom;
3868
- this._transitionOverlay.color.set(overlayColor.r, overlayColor.g, overlayColor.b, Math.max(0, Math.min(1, alpha)));
3869
- 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);
3870
4147
  }
3871
4148
  }
3872
4149
 
@@ -5638,9 +5915,9 @@ class WebGl2VertexArrayObject {
5638
5915
  }
5639
5916
  }
5640
5917
 
5641
- 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";
5642
5919
 
5643
- 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";
5644
5921
 
5645
5922
  /**
5646
5923
  * Instanced sprite renderer for WebGL2.
@@ -5688,7 +5965,7 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
5688
5965
  constructor(batchSize) {
5689
5966
  super();
5690
5967
  this._batchSize = batchSize;
5691
- this._shader = new Shader(vertexSource$4, fragmentSource$4);
5968
+ this._shader = new Shader(vertexSource$3, fragmentSource$3);
5692
5969
  this._instanceData = new ArrayBuffer(batchSize * instanceStrideBytes$3);
5693
5970
  this._instanceFloat32 = new Float32Array(this._instanceData);
5694
5971
  this._instanceUint32 = new Uint32Array(this._instanceData);
@@ -6113,26 +6390,26 @@ class Texture {
6113
6390
  }
6114
6391
  }
6115
6392
 
6116
- 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";
6117
6394
 
6118
- 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";
6119
6396
 
6120
6397
  // Per-vertex layout (20 bytes):
6121
6398
  // position: vec2 f32 (offset 0, 8 bytes)
6122
6399
  // texcoord: vec2 f32 (offset 8, 8 bytes)
6123
6400
  // color: u8x4 norm (offset 16, 4 bytes)
6124
- const vertexStrideBytes$5 = 20;
6125
- const vertexStrideWords$1 = vertexStrideBytes$5 / 4;
6401
+ const vertexStrideBytes$3 = 20;
6402
+ const vertexStrideWords = vertexStrideBytes$3 / 4;
6126
6403
  const initialVertexCapacity = 64;
6127
6404
  const initialIndexCapacity = 192;
6128
6405
  const defaultVertexColor = 0xFFFFFFFF; // white, full alpha
6129
6406
  class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
6130
- _shader = new Shader(vertexSource$3, fragmentSource$3);
6407
+ _shader = new Shader(vertexSource$2, fragmentSource$2);
6131
6408
  _tintScratch = new Float32Array(4);
6132
6409
  _textureUnitScratch = new Int32Array([0]);
6133
6410
  _vertexCapacity = initialVertexCapacity;
6134
6411
  _indexCapacity = initialIndexCapacity;
6135
- _vertexData = new ArrayBuffer(initialVertexCapacity * vertexStrideBytes$5);
6412
+ _vertexData = new ArrayBuffer(initialVertexCapacity * vertexStrideBytes$3);
6136
6413
  _float32View = new Float32Array(this._vertexData);
6137
6414
  _uint32View = new Uint32Array(this._vertexData);
6138
6415
  _indexData = new Uint16Array(initialIndexCapacity);
@@ -6179,7 +6456,7 @@ class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
6179
6456
  const uvs = mesh.uvs;
6180
6457
  const colors = mesh.colors;
6181
6458
  for (let i = 0; i < vertexCount; i++) {
6182
- const word = i * vertexStrideWords$1;
6459
+ const word = i * vertexStrideWords;
6183
6460
  const pair = i * 2;
6184
6461
  this._float32View[word] = positions[pair];
6185
6462
  this._float32View[word + 1] = positions[pair + 1];
@@ -6205,7 +6482,7 @@ class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
6205
6482
  }
6206
6483
  this._shader.sync();
6207
6484
  backend.bindVertexArrayObject(connection.vao);
6208
- connection.vertexBuffer.upload(this._float32View.subarray(0, vertexCount * vertexStrideWords$1));
6485
+ connection.vertexBuffer.upload(this._float32View.subarray(0, vertexCount * vertexStrideWords));
6209
6486
  connection.indexBuffer.upload(this._indexData.subarray(0, indexCount));
6210
6487
  connection.vao.draw(indexCount, 0, RenderingPrimitives.Triangles);
6211
6488
  backend.stats.batches++;
@@ -6238,9 +6515,9 @@ class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
6238
6515
  this._shader.sync();
6239
6516
  const vao = new WebGl2VertexArrayObject()
6240
6517
  .addIndex(indexBuffer)
6241
- .addAttribute(vertexBuffer, this._shader.getAttribute('a_position'), gl.FLOAT, false, vertexStrideBytes$5, 0)
6242
- .addAttribute(vertexBuffer, this._shader.getAttribute('a_texcoord'), gl.FLOAT, false, vertexStrideBytes$5, 8)
6243
- .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)
6244
6521
  .connect(this._createVaoRuntime(gl, vaoHandle));
6245
6522
  this._connection = { gl, vao, vertexBuffer, indexBuffer };
6246
6523
  }
@@ -6265,7 +6542,7 @@ class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
6265
6542
  while (this._vertexCapacity < vertexCount) {
6266
6543
  this._vertexCapacity *= 2;
6267
6544
  }
6268
- this._vertexData = new ArrayBuffer(this._vertexCapacity * vertexStrideBytes$5);
6545
+ this._vertexData = new ArrayBuffer(this._vertexCapacity * vertexStrideBytes$3);
6269
6546
  this._float32View = new Float32Array(this._vertexData);
6270
6547
  this._uint32View = new Uint32Array(this._vertexData);
6271
6548
  }
@@ -6347,9 +6624,9 @@ class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
6347
6624
  }
6348
6625
  }
6349
6626
 
6350
- 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";
6351
6628
 
6352
- 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";
6353
6630
 
6354
6631
  /**
6355
6632
  * Instanced particle renderer for WebGL2.
@@ -6401,7 +6678,7 @@ class WebGl2ParticleRenderer extends AbstractWebGl2Renderer {
6401
6678
  constructor(batchSize) {
6402
6679
  super();
6403
6680
  this._batchSize = batchSize;
6404
- this._shader = new Shader(vertexSource$2, fragmentSource$2);
6681
+ this._shader = new Shader(vertexSource$1, fragmentSource$1);
6405
6682
  this._instanceData = new ArrayBuffer(batchSize * instanceStrideBytes$2);
6406
6683
  this._instanceFloat32 = new Float32Array(this._instanceData);
6407
6684
  this._instanceUint32 = new Uint32Array(this._instanceData);
@@ -6641,277 +6918,62 @@ class WebGl2ParticleRenderer extends AbstractWebGl2Renderer {
6641
6918
  }
6642
6919
  }
6643
6920
 
6644
- 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";
6645
-
6646
- 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";
6647
-
6648
- const minBatchVertexSize = 4;
6649
- const vertexStrideBytes$4 = 12;
6650
- const vertexStrideWords = vertexStrideBytes$4 / 4;
6651
- class WebGl2PrimitiveRenderer extends AbstractWebGl2Renderer {
6652
- _vertexCapacity;
6653
- _indexCapacity;
6654
- _vertexData;
6655
- _indexData;
6656
- _float32View;
6657
- _uint32View;
6658
- _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]);
6659
6947
  _connection = null;
6660
- _currentBlendMode = null;
6661
- _currentView = null;
6662
- _viewId = -1;
6663
- constructor(batchSize) {
6664
- super();
6665
- this._vertexCapacity = Math.max(minBatchVertexSize, batchSize);
6666
- this._indexCapacity = Math.max(6, this._vertexCapacity * 3);
6667
- this._vertexData = new ArrayBuffer(this._vertexCapacity * vertexStrideBytes$4);
6668
- this._indexData = new Uint16Array(this._indexCapacity);
6669
- this._float32View = new Float32Array(this._vertexData);
6670
- this._uint32View = new Uint32Array(this._vertexData);
6948
+ connect(backend) {
6949
+ if (this._connection !== null) {
6950
+ return;
6951
+ }
6952
+ const gl = backend.context;
6953
+ const vaoHandle = gl.createVertexArray();
6954
+ if (vaoHandle === null) {
6955
+ throw new Error('WebGl2MaskCompositor: could not create vertex array object.');
6956
+ }
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));
6961
+ const vertexBuffer = new WebGl2RenderBuffer(BufferTypes.ArrayBuffer, this._vertexData, BufferUsage.DynamicDraw)
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.
6966
+ this._shader.sync();
6967
+ const vao = new WebGl2VertexArrayObject()
6968
+ .addIndex(indexBuffer)
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)
6971
+ .connect(this._createVaoRuntime(gl, vaoHandle));
6972
+ this._connection = { gl, vaoHandle, vao, indexBuffer, vertexBuffer, bufferHandles };
6671
6973
  }
6672
- render(shape) {
6974
+ disconnect() {
6673
6975
  const connection = this._connection;
6674
- if (!connection) {
6675
- throw new Error('Renderer not connected');
6676
- }
6677
- const backend = this.getBackend();
6678
- const { geometry, drawMode, color, blendMode } = shape;
6679
- const vertices = geometry.vertices;
6680
- const sourceIndices = geometry.indices;
6681
- const vertexCount = vertices.length / 2;
6682
- const indexCount = sourceIndices.length > 0 ? sourceIndices.length : vertexCount;
6683
- if (vertexCount === 0 || indexCount === 0) {
6684
- return;
6685
- }
6686
- this._ensureVertexCapacity(vertexCount);
6687
- this._ensureIndexCapacity(indexCount);
6688
- if (blendMode !== this._currentBlendMode) {
6689
- this._currentBlendMode = blendMode;
6690
- backend.setBlendMode(blendMode);
6691
- }
6692
- const view = backend.view;
6693
- if (this._currentView !== view || this._viewId !== view.updateId) {
6694
- this._currentView = view;
6695
- this._viewId = view.updateId;
6696
- this._shader.getUniform('u_projection').setValue(view.getTransform().toArray(false));
6697
- }
6698
- this._shader.getUniform('u_translation').setValue(shape.getGlobalTransform().toArray(false));
6699
- const packedColor = color.toRgba();
6700
- for (let i = 0; i < vertexCount; i++) {
6701
- const sourceIndex = i * 2;
6702
- const targetIndex = i * vertexStrideWords;
6703
- this._float32View[targetIndex] = vertices[sourceIndex];
6704
- this._float32View[targetIndex + 1] = vertices[sourceIndex + 1];
6705
- this._uint32View[targetIndex + 2] = packedColor;
6706
- }
6707
- if (sourceIndices.length > 0) {
6708
- this._indexData.set(sourceIndices, 0);
6709
- }
6710
- else {
6711
- for (let i = 0; i < vertexCount; i++) {
6712
- this._indexData[i] = i;
6713
- }
6714
- }
6715
- this._shader.sync();
6716
- backend.bindVertexArrayObject(connection.vao);
6717
- connection.vertexBuffer.upload(this._float32View.subarray(0, vertexCount * vertexStrideWords));
6718
- connection.indexBuffer.upload(this._indexData.subarray(0, indexCount));
6719
- connection.vao.draw(indexCount, 0, drawMode);
6720
- backend.stats.batches++;
6721
- backend.stats.drawCalls++;
6722
- }
6723
- flush() {
6724
- // Primitive rendering is immediate per shape in this bridge stage.
6725
- }
6726
- destroy() {
6727
- this.disconnect();
6728
- this._shader.destroy();
6729
- this._currentBlendMode = null;
6730
- this._currentView = null;
6731
- }
6732
- onConnect(backend) {
6733
- const gl = backend.context;
6734
- const vaoHandle = gl.createVertexArray();
6735
- this._shader.connect(createWebGl2ShaderProgram(gl));
6736
- if (vaoHandle === null) {
6737
- throw new Error('Could not create vertex array object.');
6738
- }
6739
- const buffers = new Map();
6740
- const indexBuffer = new WebGl2RenderBuffer(BufferTypes.ElementArrayBuffer, this._indexData, BufferUsage.DynamicDraw)
6741
- .connect(this._createBufferRuntime(gl, buffers));
6742
- const vertexBuffer = new WebGl2RenderBuffer(BufferTypes.ArrayBuffer, this._vertexData, BufferUsage.DynamicDraw)
6743
- .connect(this._createBufferRuntime(gl, buffers));
6744
- // Force shader finalize so the attribute table is populated. The
6745
- // async-compile path defers attribute extraction from initialize()
6746
- // to first sync(); without this nudge, getAttribute() below would
6747
- // throw "Attribute 'a_position' is not available".
6748
- this._shader.sync();
6749
- const vao = new WebGl2VertexArrayObject()
6750
- .addIndex(indexBuffer)
6751
- .addAttribute(vertexBuffer, this._shader.getAttribute('a_position'), gl.FLOAT, false, vertexStrideBytes$4, 0)
6752
- .addAttribute(vertexBuffer, this._shader.getAttribute('a_color'), gl.UNSIGNED_BYTE, true, vertexStrideBytes$4, 8)
6753
- .connect(this._createVaoRuntime(gl, vaoHandle));
6754
- this._connection = { gl, buffers, vaoHandle, vao, indexBuffer, vertexBuffer };
6755
- }
6756
- onDisconnect() {
6757
- const connection = this._connection;
6758
- if (!connection) {
6759
- return;
6760
- }
6761
- this._shader.disconnect();
6762
- connection.indexBuffer.destroy();
6763
- connection.vertexBuffer.destroy();
6764
- connection.vao.destroy();
6765
- this._connection = null;
6766
- this._currentBlendMode = null;
6767
- this._currentView = null;
6768
- this._viewId = -1;
6769
- }
6770
- _ensureVertexCapacity(vertexCount) {
6771
- if (vertexCount <= this._vertexCapacity) {
6772
- return;
6773
- }
6774
- while (this._vertexCapacity < vertexCount) {
6775
- this._vertexCapacity *= 2;
6776
- }
6777
- this._vertexData = new ArrayBuffer(this._vertexCapacity * vertexStrideBytes$4);
6778
- this._float32View = new Float32Array(this._vertexData);
6779
- this._uint32View = new Uint32Array(this._vertexData);
6780
- }
6781
- _ensureIndexCapacity(indexCount) {
6782
- if (indexCount <= this._indexCapacity) {
6783
- return;
6784
- }
6785
- while (this._indexCapacity < indexCount) {
6786
- this._indexCapacity *= 2;
6787
- }
6788
- this._indexData = new Uint16Array(this._indexCapacity);
6789
- }
6790
- _createBufferRuntime(gl, buffers) {
6791
- const handle = gl.createBuffer();
6792
- if (handle === null) {
6793
- throw new Error('Could not create render buffer.');
6794
- }
6795
- return {
6796
- bind: (buffer) => {
6797
- gl.bindBuffer(buffer.type, handle);
6798
- },
6799
- upload: (buffer, offset) => {
6800
- const state = buffers.get(buffer);
6801
- const data = buffer.data;
6802
- gl.bindBuffer(buffer.type, handle);
6803
- if (state && state.dataByteLength >= data.byteLength) {
6804
- gl.bufferSubData(buffer.type, offset, data);
6805
- state.dataByteLength = data.byteLength;
6806
- }
6807
- else {
6808
- gl.bufferData(buffer.type, data, buffer.usage);
6809
- buffers.set(buffer, { handle, dataByteLength: data.byteLength });
6810
- }
6811
- },
6812
- destroy: (buffer) => {
6813
- gl.deleteBuffer(handle);
6814
- buffers.delete(buffer);
6815
- buffer.disconnect();
6816
- },
6817
- };
6818
- }
6819
- _createVaoRuntime(gl, vaoHandle) {
6820
- let appliedVersion = -1;
6821
- return {
6822
- bind: (vao) => {
6823
- gl.bindVertexArray(vaoHandle);
6824
- if (appliedVersion !== vao.version) {
6825
- let lastBuffer = null;
6826
- for (const attribute of vao.attributes) {
6827
- if (lastBuffer !== attribute.buffer) {
6828
- attribute.buffer.bind();
6829
- lastBuffer = attribute.buffer;
6830
- }
6831
- gl.vertexAttribPointer(attribute.location, attribute.size, attribute.type, attribute.normalized, attribute.stride, attribute.start);
6832
- gl.enableVertexAttribArray(attribute.location);
6833
- }
6834
- if (vao.indexBuffer) {
6835
- vao.indexBuffer.bind();
6836
- }
6837
- appliedVersion = vao.version;
6838
- }
6839
- },
6840
- unbind: () => {
6841
- gl.bindVertexArray(null);
6842
- },
6843
- draw: (vao, size, start, type) => {
6844
- if (vao.indexBuffer) {
6845
- gl.drawElements(type, size, gl.UNSIGNED_SHORT, start);
6846
- }
6847
- else {
6848
- gl.drawArrays(type, start, size);
6849
- }
6850
- },
6851
- destroy: (vao) => {
6852
- gl.deleteVertexArray(vaoHandle);
6853
- vao.disconnect();
6854
- },
6855
- };
6856
- }
6857
- }
6858
-
6859
- 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";
6860
-
6861
- 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";
6862
-
6863
- // 4 floats per vertex: position(x, y) + texcoord(u, v).
6864
- const vertexStrideBytes$3 = 16;
6865
- const quadIndices$1 = new Uint16Array([0, 1, 2, 0, 2, 3]);
6866
- /**
6867
- * Single-quad two-texture compositor used by `WebGl2Backend.composeWithAlphaMask`.
6868
- *
6869
- * Renders the content texture onto the active render target with each
6870
- * output texel's alpha multiplied by the mask texture's alpha at the
6871
- * same UV. Both textures are sampled with stretched-fit UVs over the
6872
- * destination rectangle.
6873
- *
6874
- * Intentionally not a {@link AbstractWebGl2Renderer} subclass: this
6875
- * compositor is invoked directly by the manager for non-Drawable
6876
- * compositing operations and never participates in the renderer
6877
- * registry dispatch path.
6878
- */
6879
- class WebGl2MaskCompositor {
6880
- _shader = new Shader(vertexSource, fragmentSource);
6881
- _vertexData = new ArrayBuffer(4 * vertexStrideBytes$3);
6882
- _float32View = new Float32Array(this._vertexData);
6883
- _contentSamplerSlot = new Int32Array([0]);
6884
- _maskSamplerSlot = new Int32Array([1]);
6885
- _connection = null;
6886
- connect(backend) {
6887
- if (this._connection !== null) {
6888
- return;
6889
- }
6890
- const gl = backend.context;
6891
- const vaoHandle = gl.createVertexArray();
6892
- if (vaoHandle === null) {
6893
- throw new Error('WebGl2MaskCompositor: could not create vertex array object.');
6894
- }
6895
- this._shader.connect(createWebGl2ShaderProgram(gl));
6896
- const bufferHandles = new Map();
6897
- const indexBuffer = new WebGl2RenderBuffer(BufferTypes.ElementArrayBuffer, quadIndices$1, BufferUsage.StaticDraw)
6898
- .connect(this._createBufferRuntime(gl, bufferHandles));
6899
- const vertexBuffer = new WebGl2RenderBuffer(BufferTypes.ArrayBuffer, this._vertexData, BufferUsage.DynamicDraw)
6900
- .connect(this._createBufferRuntime(gl, bufferHandles));
6901
- // Force shader finalize so getAttribute() below sees a populated
6902
- // attribute table; the async-compile path defers extraction until
6903
- // the first sync() call.
6904
- this._shader.sync();
6905
- const vao = new WebGl2VertexArrayObject()
6906
- .addIndex(indexBuffer)
6907
- .addAttribute(vertexBuffer, this._shader.getAttribute('a_position'), gl.FLOAT, false, vertexStrideBytes$3, 0)
6908
- .addAttribute(vertexBuffer, this._shader.getAttribute('a_texcoord'), gl.FLOAT, false, vertexStrideBytes$3, 8)
6909
- .connect(this._createVaoRuntime(gl, vaoHandle));
6910
- this._connection = { gl, vaoHandle, vao, indexBuffer, vertexBuffer, bufferHandles };
6911
- }
6912
- disconnect() {
6913
- const connection = this._connection;
6914
- if (connection === null) {
6976
+ if (connection === null) {
6915
6977
  return;
6916
6978
  }
6917
6979
  connection.indexBuffer.destroy();
@@ -7197,116 +7259,6 @@ class Sprite extends Drawable {
7197
7259
  }
7198
7260
  RenderNode.setInternalSpriteFactory(() => new Sprite(null));
7199
7261
 
7200
- /**
7201
- * Arbitrary 2D triangle-mesh primitive.
7202
- *
7203
- * `Mesh` lives alongside {@link Sprite} as a public Drawable: it has the
7204
- * same transform (position/rotation/scale/origin), tint, blendMode,
7205
- * filters, masks, and cacheAsBitmap — but the geometry it renders is
7206
- * user-supplied rather than implied by a texture frame. The intended use
7207
- * cases are:
7208
- *
7209
- * - Custom-shape sprites whose silhouette isn't a quad (badges, speech
7210
- * bubbles, region overlays).
7211
- * - Deformable visuals (rope/ribbon, banner, water surface): mutate the
7212
- * vertex array between frames and the GPU re-tessellates nothing —
7213
- * only the transform changes per frame.
7214
- * - Particles or trails with custom geometry per emitter.
7215
- *
7216
- * The mesh data is **immutable after construction** in v1: vertex /
7217
- * index / UV / color arrays are exposed as readonly references. Mutate
7218
- * the underlying typed arrays in-place if you need per-frame updates,
7219
- * but the array lengths and topology cannot change. Texture is the only
7220
- * post-construction mutable property.
7221
- *
7222
- * The vertex stream is a flat `Float32Array` of (x, y) pairs in local
7223
- * space. The mesh's local bounds are computed once at construction from
7224
- * the AABB of those vertices and used by the cull pass. Re-computing
7225
- * after in-place mutation is the caller's responsibility (call
7226
- * `recomputeLocalBounds()`).
7227
- */
7228
- class Mesh extends Drawable {
7229
- vertices;
7230
- indices;
7231
- uvs;
7232
- colors;
7233
- _texture;
7234
- constructor(options) {
7235
- super();
7236
- const { vertices, indices = null, uvs = null, colors = null, texture = null } = options;
7237
- if (vertices.length === 0 || vertices.length % 2 !== 0) {
7238
- throw new Error(`Mesh vertices must be a non-empty flat array of (x,y) pairs (got length ${vertices.length}).`);
7239
- }
7240
- const vertexCount = vertices.length / 2;
7241
- if (vertexCount < 3) {
7242
- throw new Error(`Mesh requires at least 3 vertices (got ${vertexCount}).`);
7243
- }
7244
- if (uvs !== null && uvs.length !== vertices.length) {
7245
- throw new Error(`Mesh uvs length ${uvs.length} must equal vertices length ${vertices.length}.`);
7246
- }
7247
- if (colors !== null && colors.length !== vertexCount) {
7248
- throw new Error(`Mesh colors length ${colors.length} must equal vertex count ${vertexCount}.`);
7249
- }
7250
- if (indices !== null) {
7251
- if (indices.length === 0 || indices.length % 3 !== 0) {
7252
- throw new Error(`Mesh indices must be a non-empty multiple of 3 (got length ${indices.length}).`);
7253
- }
7254
- for (let i = 0; i < indices.length; i++) {
7255
- if (indices[i] >= vertexCount) {
7256
- throw new Error(`Mesh index ${indices[i]} at position ${i} is out of range for vertex count ${vertexCount}.`);
7257
- }
7258
- }
7259
- }
7260
- else if (vertexCount % 3 !== 0) {
7261
- throw new Error(`Non-indexed Mesh requires a vertex count that is a multiple of 3 (got ${vertexCount}).`);
7262
- }
7263
- this.vertices = vertices;
7264
- this.indices = indices;
7265
- this.uvs = uvs;
7266
- this.colors = colors;
7267
- this._texture = texture;
7268
- this.recomputeLocalBounds();
7269
- }
7270
- get vertexCount() {
7271
- return this.vertices.length / 2;
7272
- }
7273
- get indexCount() {
7274
- return this.indices?.length ?? this.vertexCount;
7275
- }
7276
- get texture() {
7277
- return this._texture;
7278
- }
7279
- set texture(texture) {
7280
- this._texture = texture;
7281
- this.invalidateCache();
7282
- }
7283
- /**
7284
- * Recompute the local AABB from the current vertex array. Call after
7285
- * mutating `vertices` in place to keep culling correct; otherwise the
7286
- * bounds the cull pass sees will be the AABB at construction time.
7287
- */
7288
- recomputeLocalBounds() {
7289
- let minX = Infinity;
7290
- let minY = Infinity;
7291
- let maxX = -Infinity;
7292
- let maxY = -Infinity;
7293
- for (let i = 0; i < this.vertices.length; i += 2) {
7294
- const x = this.vertices[i];
7295
- const y = this.vertices[i + 1];
7296
- if (x < minX)
7297
- minX = x;
7298
- if (x > maxX)
7299
- maxX = x;
7300
- if (y < minY)
7301
- minY = y;
7302
- if (y > maxY)
7303
- maxY = y;
7304
- }
7305
- this.localBounds.set(minX, minY, maxX - minX, maxY - minY);
7306
- return this;
7307
- }
7308
- }
7309
-
7310
7262
  class Particle {
7311
7263
  _totalLifetime = Time.oneSecond.clone();
7312
7264
  _elapsedLifetime = Time.zero.clone();
@@ -7749,7 +7701,7 @@ class WebGl2Backend {
7749
7701
  _boundFramebuffer = null;
7750
7702
  _stats = createRenderStats();
7751
7703
  constructor(app) {
7752
- const { width, height, clearColor, webglAttributes, debug, spriteRendererBatchSize, particleRendererBatchSize, primitiveRendererBatchSize, } = app.options;
7704
+ const { width, height, clearColor, webglAttributes, debug, spriteRendererBatchSize, particleRendererBatchSize, } = app.options;
7753
7705
  this._canvas = app.canvas;
7754
7706
  const gl = this._createContext(webglAttributes);
7755
7707
  if (!gl) {
@@ -7773,7 +7725,6 @@ class WebGl2Backend {
7773
7725
  this.rendererRegistry.registerRenderer(Sprite, new WebGl2SpriteRenderer(spriteRendererBatchSize));
7774
7726
  this.rendererRegistry.registerRenderer(Mesh, new WebGl2MeshRenderer());
7775
7727
  this.rendererRegistry.registerRenderer(ParticleSystem, new WebGl2ParticleRenderer(particleRendererBatchSize));
7776
- this.rendererRegistry.registerRenderer(DrawableShape, new WebGl2PrimitiveRenderer(primitiveRendererBatchSize));
7777
7728
  this.rendererRegistry.connect(this);
7778
7729
  this._bindRenderTarget(this._renderTarget);
7779
7730
  this.setBlendMode(BlendModes.Normal);
@@ -8246,661 +8197,180 @@ class WebGl2Backend {
8246
8197
  gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, texture.source);
8247
8198
  }
8248
8199
  }
8249
- if (texture.generateMipMap && (texture instanceof RenderTexture || texture.source !== null)) {
8250
- gl.generateMipmap(gl.TEXTURE_2D);
8251
- }
8252
- state.version = version;
8253
- state.width = texture.width;
8254
- state.height = texture.height;
8255
- }
8256
- return state;
8257
- }
8258
- _toClipPixels(bounds) {
8259
- const topLeft = this._renderTarget.mapCoordsToPixel(this._clipPointA.set(bounds.left, bounds.top));
8260
- const bottomRight = this._renderTarget.mapCoordsToPixel(this._clipPointB.set(bounds.right, bounds.bottom));
8261
- const minX = Math.min(topLeft.x, bottomRight.x);
8262
- const maxX = Math.max(topLeft.x, bottomRight.x);
8263
- const minY = Math.min(topLeft.y, bottomRight.y);
8264
- const maxY = Math.max(topLeft.y, bottomRight.y);
8265
- const targetWidth = this._renderTarget.width;
8266
- const targetHeight = this._renderTarget.height;
8267
- const x = Math.max(0, Math.min(targetWidth, Math.floor(minX)));
8268
- const right = Math.max(0, Math.min(targetWidth, Math.ceil(maxX)));
8269
- const yTop = Math.max(0, Math.min(targetHeight, Math.floor(minY)));
8270
- const bottom = Math.max(0, Math.min(targetHeight, Math.ceil(maxY)));
8271
- const width = Math.max(0, right - x);
8272
- const height = Math.max(0, bottom - yTop);
8273
- const y = Math.max(0, targetHeight - bottom);
8274
- return {
8275
- x,
8276
- y,
8277
- width,
8278
- height,
8279
- };
8280
- }
8281
- _intersectClips(first, second) {
8282
- const left = Math.max(first.x, second.x);
8283
- const bottom = Math.max(first.y, second.y);
8284
- const right = Math.min(first.x + first.width, second.x + second.width);
8285
- const top = Math.min(first.y + first.height, second.y + second.height);
8286
- return {
8287
- x: left,
8288
- y: bottom,
8289
- width: Math.max(0, right - left),
8290
- height: Math.max(0, top - bottom),
8291
- };
8292
- }
8293
- _applyClipState() {
8294
- const gl = this._context;
8295
- if (this._clipPixelStack.length === 0) {
8296
- gl.disable(gl.SCISSOR_TEST);
8297
- return;
8298
- }
8299
- const clip = this._clipPixelStack[this._clipPixelStack.length - 1];
8300
- gl.enable(gl.SCISSOR_TEST);
8301
- gl.scissor(clip.x, clip.y, clip.width, clip.height);
8302
- }
8303
- }
8304
-
8305
- /// <reference types="@webgpu/types" />
8306
- /**
8307
- * Base class for WebGPU renderers.
8308
- *
8309
- * Manages the connect/disconnect lifecycle and provides a safe
8310
- * `getBackend()` accessor that throws if the renderer is not connected.
8311
- *
8312
- * Subclasses must implement:
8313
- * - onConnect(backend): set up GPU resources (shader modules, pipelines, buffers)
8314
- * - onDisconnect(): tear down GPU resources
8315
- * - render(drawable): collect draw call data for the given drawable
8316
- * - flush(): encode and submit command buffers for all collected draw calls
8317
- */
8318
- class AbstractWebGpuRenderer {
8319
- backendType = RenderBackendType.WebGpu;
8320
- _backend = null;
8321
- connect(backend) {
8322
- if (this._backend !== null) {
8323
- return;
8324
- }
8325
- if (backend.backendType !== RenderBackendType.WebGpu) {
8326
- throw new Error(`${this.constructor.name} requires a WebGPU backend, `
8327
- + `but received backendType ${String(backend.backendType)}.`);
8328
- }
8329
- this._backend = backend;
8330
- this.onConnect(backend);
8331
- }
8332
- disconnect() {
8333
- if (this._backend === null) {
8334
- return;
8335
- }
8336
- this.flush();
8337
- this.onDisconnect();
8338
- this._backend = null;
8339
- }
8340
- getBackend() {
8341
- if (this._backend === null) {
8342
- throw new Error(`${this.constructor.name} is not connected to a backend.`);
8343
- }
8344
- return this._backend;
8345
- }
8346
- getBackendOrNull() {
8347
- return this._backend;
8348
- }
8349
- }
8350
-
8351
- /// <reference types="@webgpu/types" />
8352
- /**
8353
- * Returns the GPUBlendState for a given ExoJS blend mode.
8354
- * Shared by all WebGPU renderers to avoid duplication.
8355
- */
8356
- function getWebGpuBlendState(blendMode) {
8357
- switch (blendMode) {
8358
- case BlendModes.Additive:
8359
- return {
8360
- color: {
8361
- operation: 'add',
8362
- srcFactor: 'one',
8363
- dstFactor: 'one',
8364
- },
8365
- alpha: {
8366
- operation: 'add',
8367
- srcFactor: 'one',
8368
- dstFactor: 'one',
8369
- },
8370
- };
8371
- case BlendModes.Subtract:
8372
- return {
8373
- color: {
8374
- operation: 'add',
8375
- srcFactor: 'zero',
8376
- dstFactor: 'one-minus-src',
8377
- },
8378
- alpha: {
8379
- operation: 'add',
8380
- srcFactor: 'zero',
8381
- dstFactor: 'one-minus-src-alpha',
8382
- },
8383
- };
8384
- case BlendModes.Multiply:
8385
- return {
8386
- color: {
8387
- operation: 'add',
8388
- srcFactor: 'dst',
8389
- dstFactor: 'one-minus-src-alpha',
8390
- },
8391
- alpha: {
8392
- operation: 'add',
8393
- srcFactor: 'dst-alpha',
8394
- dstFactor: 'one-minus-src-alpha',
8395
- },
8396
- };
8397
- case BlendModes.Screen:
8398
- return {
8399
- color: {
8400
- operation: 'add',
8401
- srcFactor: 'one',
8402
- dstFactor: 'one-minus-src',
8403
- },
8404
- alpha: {
8405
- operation: 'add',
8406
- srcFactor: 'one',
8407
- dstFactor: 'one-minus-src-alpha',
8408
- },
8409
- };
8410
- default:
8411
- return {
8412
- color: {
8413
- operation: 'add',
8414
- srcFactor: 'one',
8415
- dstFactor: 'one-minus-src-alpha',
8416
- },
8417
- alpha: {
8418
- operation: 'add',
8419
- srcFactor: 'one',
8420
- dstFactor: 'one-minus-src-alpha',
8421
- },
8422
- };
8423
- }
8424
- }
8425
-
8426
- /// <reference types="@webgpu/types" />
8427
- const primitiveShaderSource = `
8428
- struct VertexInput {
8429
- @location(0) position: vec4<f32>,
8430
- @location(1) color: vec4<f32>,
8431
- };
8432
-
8433
- struct VertexOutput {
8434
- @builtin(position) position: vec4<f32>,
8435
- @location(0) color: vec4<f32>,
8436
- };
8437
-
8438
- @vertex
8439
- fn vertexMain(input: VertexInput) -> VertexOutput {
8440
- var output: VertexOutput;
8441
-
8442
- output.position = input.position;
8443
- output.color = vec4<f32>(input.color.rgb * input.color.a, input.color.a);
8444
-
8445
- return output;
8446
- }
8447
-
8448
- @fragment
8449
- fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
8450
- return input.color;
8451
- }
8452
- `;
8453
- // 4 floats (pre-transformed clip-space position) + 1 u32 (color) = 20 bytes.
8454
- // The CPU applies (view * shape.globalTransform) to each vertex before writing
8455
- // it into the vertex buffer, so the shader outputs the position as-is. This
8456
- // matches the sprite renderer's approach and eliminates the need for a per-
8457
- // drawcall uniform binding.
8458
- const vertexStrideBytes$2 = 20;
8459
- const wordsPerVertex$1 = vertexStrideBytes$2 / Uint32Array.BYTES_PER_ELEMENT;
8460
- class WebGpuPrimitiveRenderer extends AbstractWebGpuRenderer {
8461
- _combinedTransform = new Matrix();
8462
- _drawCalls = [];
8463
- _drawCallCount = 0;
8464
- _pipelines = new Map();
8465
- _device = null;
8466
- _shaderModule = null;
8467
- _pipelineLayout = null;
8468
- _vertexBuffer = null;
8469
- _indexBuffer = null;
8470
- _vertexBufferCapacity = 0;
8471
- _indexBufferCapacity = 0;
8472
- _vertexData = new ArrayBuffer(0);
8473
- _float32View = new Float32Array(this._vertexData);
8474
- _uint32View = new Uint32Array(this._vertexData);
8475
- _packedIndexData = new Uint16Array(0);
8476
- _generatedIndexData = new Uint16Array(0);
8477
- _sequentialIndexData = new Uint16Array(0);
8478
- render(shape) {
8479
- const backend = this._backend;
8480
- if (backend === null) {
8481
- throw new Error('Renderer not connected');
8482
- }
8483
- if (shape.drawMode !== RenderingPrimitives.Points
8484
- && shape.drawMode !== RenderingPrimitives.Lines
8485
- && shape.drawMode !== RenderingPrimitives.LineLoop
8486
- && shape.drawMode !== RenderingPrimitives.LineStrip
8487
- && shape.drawMode !== RenderingPrimitives.Triangles
8488
- && shape.drawMode !== RenderingPrimitives.TriangleFan
8489
- && shape.drawMode !== RenderingPrimitives.TriangleStrip) {
8490
- throw new Error(`WebGPU primitive renderer does not support draw mode "${shape.drawMode}" yet.`);
8491
- }
8492
- backend.setBlendMode(shape.blendMode);
8493
- if (shape.geometry.vertices.length === 0) {
8494
- return;
8495
- }
8496
- const drawCallIndex = this._drawCallCount++;
8497
- const drawCall = this._drawCalls[drawCallIndex];
8498
- if (drawCall) {
8499
- drawCall.shape = shape;
8500
- drawCall.blendMode = shape.blendMode;
8501
- }
8502
- else {
8503
- this._drawCalls.push({
8504
- shape,
8505
- blendMode: shape.blendMode,
8506
- });
8507
- }
8508
- }
8509
- flush() {
8510
- const backend = this._backend;
8511
- const device = this._device;
8512
- if (!backend || !device) {
8513
- return;
8514
- }
8515
- if (this._drawCallCount === 0 && !backend.clearRequested) {
8516
- return;
8517
- }
8518
- const scissor = backend.getScissorRect();
8519
- const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
8520
- // Phase 1: resolve drawcalls and record each one's offsets into the
8521
- // shared packed buffers. Transform gets baked into the vertex data
8522
- // during phase 2 so no per-drawcall uniform binding is needed.
8523
- const plan = [];
8524
- const resolvedDrawCalls = [];
8525
- let totalVertices = 0;
8526
- let totalIndices = 0;
8527
- if (this._drawCallCount > 0 && !maskClipsAll) {
8528
- for (let drawCallIndex = 0; drawCallIndex < this._drawCallCount; drawCallIndex++) {
8529
- const drawCall = this._drawCalls[drawCallIndex];
8530
- const shape = drawCall.shape;
8531
- const resolved = this._resolveDrawCall(shape);
8532
- resolvedDrawCalls.push(resolved);
8533
- if (resolved === null) {
8534
- continue;
8535
- }
8536
- const pipeline = this._getPipeline({
8537
- topology: resolved.topology,
8538
- usesStripIndex: resolved.usesStripIndex,
8539
- blendMode: drawCall.blendMode,
8540
- format: backend.renderTargetFormat,
8541
- });
8542
- plan.push({
8543
- pipeline,
8544
- vertexByteOffset: totalVertices * vertexStrideBytes$2,
8545
- vertexCount: resolved.vertexCount,
8546
- indexByteOffset: totalIndices * Uint16Array.BYTES_PER_ELEMENT,
8547
- indexCount: resolved.indexCount,
8548
- });
8549
- totalVertices += resolved.vertexCount;
8550
- totalIndices += resolved.indexCount;
8551
- }
8552
- }
8553
- // If nothing will actually render, still honor a pending clear with
8554
- // a single empty pass so createColorAttachment consumes the clear
8555
- // state exactly once.
8556
- if (plan.length === 0) {
8557
- if (backend.clearRequested) {
8558
- const encoder = device.createCommandEncoder();
8559
- const pass = encoder.beginRenderPass({
8560
- colorAttachments: [backend.createColorAttachment()],
8561
- });
8562
- backend.stats.renderPasses++;
8563
- pass.end();
8564
- backend.submit(encoder.finish());
8565
- }
8566
- this._drawCallCount = 0;
8567
- return;
8568
- }
8569
- // Phase 2: size GPU buffers for the whole-frame totals, then pack
8570
- // every drawcall's CPU-side data. _writeShapeVertices applies
8571
- // (view * shape.globalTransform) per-vertex so the shader simply
8572
- // outputs input.position unchanged.
8573
- this._ensureVertexCapacity(totalVertices);
8574
- if (totalIndices > 0) {
8575
- this._ensureIndexCapacity(totalIndices);
8576
- if (this._packedIndexData.length < totalIndices) {
8577
- this._packedIndexData = new Uint16Array(Math.max(totalIndices, this._packedIndexData.length === 0 ? 1 : this._packedIndexData.length * 2));
8578
- }
8579
- }
8580
- {
8581
- let vOffset = 0;
8582
- let iOffset = 0;
8583
- for (let i = 0; i < this._drawCallCount; i++) {
8584
- const resolved = resolvedDrawCalls[i];
8585
- if (resolved === null) {
8586
- continue;
8587
- }
8588
- const drawCall = this._drawCalls[i];
8589
- const shape = drawCall.shape;
8590
- this._writeShapeVertices(backend, shape, vOffset);
8591
- if (resolved.indices !== null && resolved.indexCount > 0) {
8592
- this._packedIndexData.set(resolved.indices.subarray(0, resolved.indexCount), iOffset);
8593
- iOffset += resolved.indexCount;
8594
- }
8595
- vOffset += resolved.vertexCount;
8596
- }
8597
- }
8598
- // Phase 3: single writeBuffer per GPU buffer covers the whole frame.
8599
- device.queue.writeBuffer(this._vertexBuffer, 0, this._vertexData, 0, totalVertices * vertexStrideBytes$2);
8600
- if (totalIndices > 0) {
8601
- device.queue.writeBuffer(this._indexBuffer, 0, this._packedIndexData.buffer, this._packedIndexData.byteOffset, totalIndices * Uint16Array.BYTES_PER_ELEMENT);
8602
- }
8603
- // Phase 4: single render pass. Per-draw state is just pipeline and
8604
- // vertex/index subrange offsets — the transform has already been
8605
- // baked into the vertex data.
8606
- const encoder = device.createCommandEncoder();
8607
- const pass = encoder.beginRenderPass({
8608
- colorAttachments: [backend.createColorAttachment()],
8609
- });
8610
- backend.stats.renderPasses++;
8611
- if (scissor !== null) {
8612
- pass.setScissorRect(scissor.x, scissor.y, scissor.width, scissor.height);
8613
- }
8614
- for (const planned of plan) {
8615
- pass.setPipeline(planned.pipeline);
8616
- pass.setVertexBuffer(0, this._vertexBuffer, planned.vertexByteOffset);
8617
- if (planned.indexCount > 0) {
8618
- pass.setIndexBuffer(this._indexBuffer, 'uint16', planned.indexByteOffset);
8619
- pass.drawIndexed(planned.indexCount);
8620
- }
8621
- else {
8622
- pass.draw(planned.vertexCount);
8623
- }
8624
- backend.stats.batches++;
8625
- backend.stats.drawCalls++;
8626
- }
8627
- pass.end();
8628
- backend.submit(encoder.finish());
8629
- this._drawCallCount = 0;
8630
- }
8631
- destroy() {
8632
- this.disconnect();
8633
- this._combinedTransform.destroy();
8634
- }
8635
- onConnect(backend) {
8636
- this._backend = backend;
8637
- this._device = this._backend.device;
8638
- this._shaderModule = this._device.createShaderModule({ code: primitiveShaderSource });
8639
- // Transform is applied per-vertex on the CPU, so no uniform binding
8640
- // is needed — the shader outputs input.position directly.
8641
- this._pipelineLayout = this._device.createPipelineLayout({
8642
- bindGroupLayouts: [],
8643
- });
8644
- }
8645
- onDisconnect() {
8646
- this.flush();
8647
- this._destroyBuffers();
8648
- this._pipelines.clear();
8649
- this._pipelineLayout = null;
8650
- this._shaderModule = null;
8651
- this._device = null;
8652
- this._backend = null;
8653
- this._drawCallCount = 0;
8654
- }
8655
- _writeShapeVertices(backend, shape, vertexStart) {
8656
- // Matrix.combine is `other * this` (see Matrix.rotate and
8657
- // SceneNode.getGlobalTransform, both of which chain via
8658
- // local.combine(parent.global) to yield parent.global * local).
8659
- //
8660
- // We need view * global applied to a local vertex, so start with
8661
- // global and combine with view — that gives
8662
- // _combinedTransform = view * global.
8663
- const matrix = this._combinedTransform
8664
- .copy(shape.getGlobalTransform())
8665
- .combine(backend.view.getTransform());
8666
- // Match the original uniform-based WGSL layout exactly.
8667
- //
8668
- // The shader packs the Matrix's 9 fields into a 4x4 mat (column-major
8669
- // in WGSL):
8670
- // col 0 = [a, c, 0, 0]
8671
- // col 1 = [b, d, 0, 0]
8672
- // col 2 = [0, 0, 1, 0]
8673
- // col 3 = [x, y, 0, z]
8674
- //
8675
- // Multiplied by vec4(px, py, 0, 1):
8676
- // out = col0*px + col1*py + col2*0 + col3*1
8677
- // out.x = a*px + b*py + x
8678
- // out.y = c*px + d*py + y
8679
- // out.z = 0
8680
- // out.w = z
8681
- //
8682
- // The Matrix class represents the affine matrix in the order
8683
- // [a b x]
8684
- // [c d y]
8685
- // [e f z]
8686
- // so a/b/c/d are rotation+scale (note: b on the TOP row, c on the
8687
- // LEFT column, not the other way around) and x/y/z the translation /
8688
- // w component. Matrix.toArray(false) confirms this layout.
8689
- const a = matrix.a;
8690
- const b = matrix.b;
8691
- const c = matrix.c;
8692
- const d = matrix.d;
8693
- const tx = matrix.x;
8694
- const ty = matrix.y;
8695
- const tw = matrix.z;
8696
- const color = shape.color.toRgba();
8697
- const vertices = shape.geometry.vertices;
8698
- const vertexCount = vertices.length / 2;
8699
- for (let i = 0; i < vertexCount; i++) {
8700
- const sourceIndex = i * 2;
8701
- const targetIndex = (vertexStart + i) * wordsPerVertex$1;
8702
- const px = vertices[sourceIndex];
8703
- const py = vertices[sourceIndex + 1];
8704
- this._float32View[targetIndex + 0] = a * px + b * py + tx;
8705
- this._float32View[targetIndex + 1] = c * px + d * py + ty;
8706
- this._float32View[targetIndex + 2] = 0;
8707
- this._float32View[targetIndex + 3] = tw;
8708
- this._uint32View[targetIndex + 4] = color;
8709
- }
8710
- }
8711
- _ensureVertexCapacity(vertexCount) {
8712
- const requiredBytes = vertexCount * vertexStrideBytes$2;
8713
- if (requiredBytes > this._vertexData.byteLength) {
8714
- const byteLength = Math.max(requiredBytes, this._vertexData.byteLength === 0 ? vertexStrideBytes$2 : this._vertexData.byteLength * 2);
8715
- this._vertexData = new ArrayBuffer(byteLength);
8716
- this._float32View = new Float32Array(this._vertexData);
8717
- this._uint32View = new Uint32Array(this._vertexData);
8718
- }
8719
- if (requiredBytes > this._vertexBufferCapacity) {
8720
- this._vertexBuffer?.destroy();
8721
- this._vertexBufferCapacity = Math.max(requiredBytes, this._vertexBufferCapacity === 0 ? vertexStrideBytes$2 : this._vertexBufferCapacity * 2);
8722
- this._vertexBuffer = this._device.createBuffer({
8723
- size: this._vertexBufferCapacity,
8724
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
8725
- });
8726
- }
8727
- }
8728
- _ensureIndexCapacity(indexCount) {
8729
- const requiredBytes = indexCount * Uint16Array.BYTES_PER_ELEMENT;
8730
- if (requiredBytes > this._indexBufferCapacity) {
8731
- this._indexBuffer?.destroy();
8732
- this._indexBufferCapacity = Math.max(requiredBytes, this._indexBufferCapacity === 0 ? Uint16Array.BYTES_PER_ELEMENT : this._indexBufferCapacity * 2);
8733
- this._indexBuffer = this._device.createBuffer({
8734
- size: this._indexBufferCapacity,
8735
- usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
8736
- });
8737
- }
8738
- }
8739
- _getPipeline(key) {
8740
- const pipelineKey = `${key.topology}:${key.usesStripIndex ? 1 : 0}:${key.blendMode}:${key.format}`;
8741
- const existingPipeline = this._pipelines.get(pipelineKey);
8742
- if (existingPipeline) {
8743
- return existingPipeline;
8744
- }
8745
- const pipeline = this._device.createRenderPipeline({
8746
- layout: this._pipelineLayout,
8747
- vertex: {
8748
- module: this._shaderModule,
8749
- entryPoint: 'vertexMain',
8750
- buffers: [{
8751
- arrayStride: vertexStrideBytes$2,
8752
- attributes: [{
8753
- shaderLocation: 0,
8754
- offset: 0,
8755
- format: 'float32x4',
8756
- }, {
8757
- shaderLocation: 1,
8758
- offset: 16,
8759
- format: 'unorm8x4',
8760
- }],
8761
- }],
8762
- },
8763
- fragment: {
8764
- module: this._shaderModule,
8765
- entryPoint: 'fragmentMain',
8766
- targets: [{
8767
- format: key.format,
8768
- blend: getWebGpuBlendState(key.blendMode),
8769
- writeMask: GPUColorWrite.ALL,
8770
- }],
8771
- },
8772
- primitive: {
8773
- topology: key.topology,
8774
- stripIndexFormat: key.usesStripIndex ? 'uint16' : undefined,
8775
- },
8776
- });
8777
- this._pipelines.set(pipelineKey, pipeline);
8778
- return pipeline;
8779
- }
8780
- _getTopology(drawMode) {
8781
- switch (drawMode) {
8782
- case RenderingPrimitives.Points:
8783
- return 'point-list';
8784
- case RenderingPrimitives.Lines:
8785
- return 'line-list';
8786
- case RenderingPrimitives.LineLoop:
8787
- case RenderingPrimitives.LineStrip:
8788
- return 'line-strip';
8789
- case RenderingPrimitives.Triangles:
8790
- case RenderingPrimitives.TriangleFan:
8791
- return 'triangle-list';
8792
- case RenderingPrimitives.TriangleStrip:
8793
- return 'triangle-strip';
8794
- default:
8795
- throw new Error(`WebGPU primitive renderer does not support draw mode "${drawMode}" yet.`);
8796
- }
8797
- }
8798
- _resolveDrawCall(shape) {
8799
- const vertices = shape.geometry.vertices;
8800
- const vertexCount = vertices.length / 2;
8801
- if (vertexCount === 0) {
8802
- return null;
8803
- }
8804
- switch (shape.drawMode) {
8805
- case RenderingPrimitives.LineLoop:
8806
- return this._resolveLineLoopDrawCall(shape.geometry.indices, vertexCount);
8807
- case RenderingPrimitives.TriangleFan:
8808
- return this._resolveTriangleFanDrawCall(shape.geometry.indices, vertexCount);
8809
- default: {
8810
- const indices = shape.geometry.indices;
8811
- const topology = this._getTopology(shape.drawMode);
8812
- const indexCount = indices.length;
8813
- const usesStripIndex = indexCount > 0 && (shape.drawMode === RenderingPrimitives.LineStrip
8814
- || shape.drawMode === RenderingPrimitives.TriangleStrip);
8815
- if (indexCount > 0) {
8816
- return {
8817
- topology,
8818
- usesStripIndex,
8819
- vertexCount,
8820
- indices,
8821
- indexCount,
8822
- };
8823
- }
8824
- return {
8825
- topology,
8826
- usesStripIndex,
8827
- vertexCount,
8828
- indices: null,
8829
- indexCount: 0,
8830
- };
8200
+ if (texture.generateMipMap && (texture instanceof RenderTexture || texture.source !== null)) {
8201
+ gl.generateMipmap(gl.TEXTURE_2D);
8831
8202
  }
8203
+ state.version = version;
8204
+ state.width = texture.width;
8205
+ state.height = texture.height;
8832
8206
  }
8207
+ return state;
8833
8208
  }
8834
- _resolveLineLoopDrawCall(indices, vertexCount) {
8835
- const sourceIndices = indices.length > 0 ? indices : this._getSequentialIndices(vertexCount);
8836
- const sourceCount = sourceIndices.length;
8837
- if (sourceCount < 2) {
8838
- return null;
8839
- }
8840
- const loopIndexCount = sourceCount + 1;
8841
- const generatedIndices = this._ensureGeneratedIndexCapacity(loopIndexCount);
8842
- generatedIndices.set(sourceIndices.subarray(0, sourceCount), 0);
8843
- generatedIndices[sourceCount] = sourceIndices[0];
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);
8844
8225
  return {
8845
- topology: 'line-strip',
8846
- usesStripIndex: true,
8847
- vertexCount,
8848
- indices: generatedIndices,
8849
- indexCount: loopIndexCount,
8226
+ x,
8227
+ y,
8228
+ width,
8229
+ height,
8850
8230
  };
8851
8231
  }
8852
- _resolveTriangleFanDrawCall(indices, vertexCount) {
8853
- const sourceIndices = indices.length > 0 ? indices : this._getSequentialIndices(vertexCount);
8854
- const sourceCount = sourceIndices.length;
8855
- if (sourceCount < 3) {
8856
- return null;
8857
- }
8858
- const indexCount = (sourceCount - 2) * 3;
8859
- const generatedIndices = this._ensureGeneratedIndexCapacity(indexCount);
8860
- let targetIndex = 0;
8861
- for (let index = 1; index < sourceCount - 1; index++) {
8862
- generatedIndices[targetIndex++] = sourceIndices[0];
8863
- generatedIndices[targetIndex++] = sourceIndices[index];
8864
- generatedIndices[targetIndex++] = sourceIndices[index + 1];
8865
- }
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);
8866
8237
  return {
8867
- topology: 'triangle-list',
8868
- usesStripIndex: false,
8869
- vertexCount,
8870
- indices: generatedIndices,
8871
- indexCount,
8238
+ x: left,
8239
+ y: bottom,
8240
+ width: Math.max(0, right - left),
8241
+ height: Math.max(0, top - bottom),
8872
8242
  };
8873
8243
  }
8874
- _getSequentialIndices(vertexCount) {
8875
- if (vertexCount > this._sequentialIndexData.length) {
8876
- let nextLength = Math.max(1, this._sequentialIndexData.length);
8877
- while (nextLength < vertexCount) {
8878
- nextLength *= 2;
8879
- }
8880
- 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;
8881
8275
  }
8882
- for (let index = 0; index < vertexCount; index++) {
8883
- 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)}.`);
8884
8279
  }
8885
- return this._sequentialIndexData.subarray(0, vertexCount);
8280
+ this._backend = backend;
8281
+ this.onConnect(backend);
8886
8282
  }
8887
- _ensureGeneratedIndexCapacity(indexCount) {
8888
- if (indexCount > this._generatedIndexData.length) {
8889
- let nextLength = Math.max(1, this._generatedIndexData.length);
8890
- while (nextLength < indexCount) {
8891
- nextLength *= 2;
8892
- }
8893
- this._generatedIndexData = new Uint16Array(nextLength);
8283
+ disconnect() {
8284
+ if (this._backend === null) {
8285
+ return;
8894
8286
  }
8895
- return this._generatedIndexData.subarray(0, indexCount);
8287
+ this.flush();
8288
+ this.onDisconnect();
8289
+ this._backend = null;
8896
8290
  }
8897
- _destroyBuffers() {
8898
- this._vertexBuffer?.destroy();
8899
- this._indexBuffer?.destroy();
8900
- this._vertexBuffer = null;
8901
- this._indexBuffer = null;
8902
- this._vertexBufferCapacity = 0;
8903
- this._indexBufferCapacity = 0;
8291
+ getBackend() {
8292
+ if (this._backend === null) {
8293
+ throw new Error(`${this.constructor.name} is not connected to a backend.`);
8294
+ }
8295
+ return this._backend;
8296
+ }
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
+ };
8904
8374
  }
8905
8375
  }
8906
8376
 
@@ -10550,8 +10020,7 @@ class WebGpuMaskCompositor {
10550
10020
  }
10551
10021
  _writeProjectionMatrix(viewMatrix) {
10552
10022
  // Pack the 3x3 affine view matrix into a 4x4 column-major mat4x4
10553
- // for WGSL, mirroring the layout used by WebGpuPrimitiveRenderer
10554
- // (see `_writeShapeVertices` in that file for the rationale).
10023
+ // for WGSL.
10555
10024
  const m = this._projectionMatrix.copy(viewMatrix);
10556
10025
  const data = this._projectionData;
10557
10026
  // col 0
@@ -10618,7 +10087,6 @@ class WebGpuBackend {
10618
10087
  if (clearColor) {
10619
10088
  this._clearColor.copy(clearColor);
10620
10089
  }
10621
- this.rendererRegistry.registerRenderer(DrawableShape, new WebGpuPrimitiveRenderer());
10622
10090
  this.rendererRegistry.registerRenderer(Sprite, new WebGpuSpriteRenderer());
10623
10091
  this.rendererRegistry.registerRenderer(Mesh, new WebGpuMeshRenderer());
10624
10092
  this.rendererRegistry.registerRenderer(ParticleSystem, new WebGpuParticleRenderer());
@@ -14619,7 +14087,6 @@ const defaultAppSettings = {
14619
14087
  debug: false,
14620
14088
  spriteRendererBatchSize: 4096, // ~ 262kb
14621
14089
  particleRendererBatchSize: 8192, // ~ 1.18mb
14622
- primitiveRendererBatchSize: 65536, // ~ 786kb
14623
14090
  gamepadDefinitions: [],
14624
14091
  pointerDistanceThreshold: 10,
14625
14092
  webglAttributes: {
@@ -14655,6 +14122,7 @@ class Application {
14655
14122
  _frameRequest = 0;
14656
14123
  _backendType;
14657
14124
  _backend;
14125
+ _capabilities = null;
14658
14126
  constructor(appSettings) {
14659
14127
  this.options = {
14660
14128
  canvas: appSettings?.canvas ?? createDefaultCanvas(),
@@ -14696,11 +14164,26 @@ class Application {
14696
14164
  get backend() {
14697
14165
  return this._backend;
14698
14166
  }
14167
+ /**
14168
+ * Resolved capabilities for the host browser. Available after
14169
+ * {@link Application.start} resolves; reading before that throws.
14170
+ * For pre-start access use {@link Capabilities.ready} directly.
14171
+ */
14172
+ get capabilities() {
14173
+ if (this._capabilities === null) {
14174
+ throw new Error('Application.capabilities is unavailable before start() resolves. Use `await Capabilities.ready` for pre-start checks.');
14175
+ }
14176
+ return this._capabilities;
14177
+ }
14699
14178
  async start(scene) {
14700
14179
  if (this._status === ApplicationStatus.Stopped) {
14701
14180
  this._status = ApplicationStatus.Loading;
14181
+ // Kick off capability detection in parallel with renderer init —
14182
+ // both are mostly-async startup work, no point serializing them.
14183
+ const capabilitiesPromise = Capabilities.ready;
14702
14184
  try {
14703
14185
  await this.initializeRenderManager();
14186
+ this._capabilities = await capabilitiesPromise;
14704
14187
  await this.sceneManager.setScene(scene);
14705
14188
  this._frameRequest = requestAnimationFrame(this._updateHandler);
14706
14189
  this._frameClock.restart();
@@ -14798,100 +14281,6 @@ class Application {
14798
14281
  }
14799
14282
  }
14800
14283
 
14801
- // Browser feature-detection probes evaluated once at module load. The
14802
- // resulting `capabilities` object is a `Readonly<Record<CapabilityName,
14803
- // boolean>>` and can be inspected directly or queried via `isSupported`.
14804
- //
14805
- // All probes are synchronous. Async questions ("can I actually acquire a
14806
- // WebGPU adapter?", "can the audio decoder play this OGG file?") are
14807
- // outside this module's scope — they're owned by the Application's
14808
- // backend selection and the Loader respectively. `capabilities.webgpu`
14809
- // returning `true` only guarantees that the browser advertises WebGPU,
14810
- // not that an adapter request will succeed.
14811
- const hasWindow = typeof window !== 'undefined';
14812
- const hasDocument = typeof document !== 'undefined';
14813
- const hasNavigator = typeof navigator !== 'undefined';
14814
- const probeWebGl2 = () => {
14815
- if (!hasDocument)
14816
- return false;
14817
- try {
14818
- const canvas = document.createElement('canvas');
14819
- const gl = canvas.getContext('webgl2');
14820
- return gl !== null;
14821
- }
14822
- catch {
14823
- return false;
14824
- }
14825
- };
14826
- const probeWebGpu = () => {
14827
- return hasNavigator && 'gpu' in navigator;
14828
- };
14829
- const probeAudio = () => {
14830
- if (!hasWindow)
14831
- return false;
14832
- const w = window;
14833
- return typeof w.AudioContext !== 'undefined' || typeof w.webkitAudioContext !== 'undefined';
14834
- };
14835
- const probePointer = () => {
14836
- return hasWindow && 'PointerEvent' in window;
14837
- };
14838
- const probeTouch = () => {
14839
- if (!hasWindow)
14840
- return false;
14841
- if ('ontouchstart' in window)
14842
- return true;
14843
- if (hasNavigator && typeof navigator.maxTouchPoints === 'number' && navigator.maxTouchPoints > 0)
14844
- return true;
14845
- return false;
14846
- };
14847
- const probeGamepad = () => {
14848
- return hasNavigator && typeof navigator.getGamepads === 'function';
14849
- };
14850
- const probeKeyboard = () => {
14851
- return hasWindow && 'KeyboardEvent' in window;
14852
- };
14853
- const probeFullscreen = () => {
14854
- if (!hasDocument)
14855
- return false;
14856
- const el = document.documentElement;
14857
- return typeof el.requestFullscreen === 'function' || typeof el.webkitRequestFullscreen === 'function';
14858
- };
14859
- const probeVibration = () => {
14860
- return hasNavigator && typeof navigator.vibrate === 'function';
14861
- };
14862
- const probeOffscreenCanvas = () => {
14863
- // The browser global is verbatim `OffscreenCanvas`; eslint's
14864
- // strict-camelCase rule rejects the property name even though we
14865
- // can't rename a web standard.
14866
- // eslint-disable-next-line @typescript-eslint/naming-convention
14867
- return hasWindow && typeof window.OffscreenCanvas !== 'undefined';
14868
- };
14869
- /**
14870
- * Synchronous, one-shot feature-detection results. Computed once at
14871
- * module load and frozen. Use either as a property bag (`capabilities.touch`)
14872
- * or via {@link isSupported} for typed lookup.
14873
- */
14874
- const capabilities = Object.freeze({
14875
- webgl2: probeWebGl2(),
14876
- webgpu: probeWebGpu(),
14877
- audio: probeAudio(),
14878
- pointer: probePointer(),
14879
- touch: probeTouch(),
14880
- gamepad: probeGamepad(),
14881
- keyboard: probeKeyboard(),
14882
- fullscreen: probeFullscreen(),
14883
- vibration: probeVibration(),
14884
- offscreenCanvas: probeOffscreenCanvas(),
14885
- });
14886
- /**
14887
- * Typed lookup over {@link capabilities}. Identical to
14888
- * `capabilities[name]` but the function form gives clearer call-sites
14889
- * when the name is computed.
14890
- */
14891
- function isSupported(name) {
14892
- return capabilities[name];
14893
- }
14894
-
14895
14284
  class Quadtree {
14896
14285
  static maxSceneNodes = 50;
14897
14286
  static maxLevel = 5;
@@ -16229,24 +15618,30 @@ function signedArea(data, start, end, dim) {
16229
15618
  return sum;
16230
15619
  }
16231
15620
 
16232
- const buildLine = (startX, startY, endX, endY, width, vertices = [], indices = []) => {
15621
+ const buildLine = (startX, startY, endX, endY, width) => {
16233
15622
  const points = [startX, startY, endX, endY];
16234
15623
  const distance = width / 2;
16235
- const index = vertices.length / 6;
16236
15624
  const perpA = new Vector(startX - endX, startY - endY).perp().normalize().multiply(distance);
16237
15625
  const perpB = new Vector(endX - startX, endY - startY).perp().normalize().multiply(distance);
16238
- vertices.push(startX - perpA.x, startY - perpA.y);
16239
- vertices.push(startX + perpA.x, startY + perpA.y);
16240
- vertices.push(endX - perpB.x, endY - perpB.y);
16241
- vertices.push(endX + perpB.x, endY + perpB.y);
16242
- indices.push(index, index, index + 1, index + 2, index + 3, index + 3);
16243
- 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 };
16244
15636
  };
16245
- const buildPath = (points, width, vertices = [], indices = []) => {
15637
+ const buildPath = (points, width) => {
16246
15638
  if (points.length < 4) {
16247
15639
  throw new Error('At least two X/Y pairs are required to build a line.');
16248
15640
  }
16249
- 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;
16250
15645
  if (firstPoint.x === lastPoint.x && firstPoint.y === lastPoint.y) {
16251
15646
  points = points.slice();
16252
15647
  points.pop();
@@ -16257,9 +15652,10 @@ const buildPath = (points, width, vertices = [], indices = []) => {
16257
15652
  points.unshift(midPointX, midPointY);
16258
15653
  points.push(midPointX, midPointY);
16259
15654
  }
15655
+ firstPoint.destroy();
15656
+ lastPoint.destroy();
16260
15657
  const length = points.length / 2;
16261
- let indexCount = points.length;
16262
- let indexStart = vertices.length / 6;
15658
+ const stripVertices = [];
16263
15659
  let p1x = points[0];
16264
15660
  let p1y = points[1];
16265
15661
  let p2x = points[2];
@@ -16277,8 +15673,8 @@ const buildPath = (points, width, vertices = [], indices = []) => {
16277
15673
  perpy /= dist;
16278
15674
  perpx *= lineWidth;
16279
15675
  perpy *= lineWidth;
16280
- vertices.push(p1x - perpx, p1y - perpy);
16281
- vertices.push(p1x + perpx, p1y + perpy);
15676
+ stripVertices.push(p1x - perpx, p1y - perpy);
15677
+ stripVertices.push(p1x + perpx, p1y + perpy);
16282
15678
  for (let i = 1; i < length - 1; i++) {
16283
15679
  p1x = points[(i - 1) * 2];
16284
15680
  p1y = points[((i - 1) * 2) + 1];
@@ -16309,8 +15705,8 @@ const buildPath = (points, width, vertices = [], indices = []) => {
16309
15705
  let denom = (a1 * b2) - (a2 * b1);
16310
15706
  if (Math.abs(denom) < 0.1) {
16311
15707
  denom += 10.1;
16312
- vertices.push(p2x - perpx, p2y - perpy);
16313
- vertices.push(p2x + perpx, p2y + perpy);
15708
+ stripVertices.push(p2x - perpx, p2y - perpy);
15709
+ stripVertices.push(p2x + perpx, p2y + perpy);
16314
15710
  continue;
16315
15711
  }
16316
15712
  const px = ((b1 * c2) - (b2 * c1)) / denom;
@@ -16324,14 +15720,13 @@ const buildPath = (points, width, vertices = [], indices = []) => {
16324
15720
  perp3y /= dist;
16325
15721
  perp3x *= lineWidth;
16326
15722
  perp3y *= lineWidth;
16327
- vertices.push(p2x - perp3x, p2y - perp3y);
16328
- vertices.push(p2x + perp3x, p2y + perp3y);
16329
- vertices.push(p2x - perp3x, p2y - perp3y);
16330
- indexCount++;
15723
+ stripVertices.push(p2x - perp3x, p2y - perp3y);
15724
+ stripVertices.push(p2x + perp3x, p2y + perp3y);
15725
+ stripVertices.push(p2x - perp3x, p2y - perp3y);
16331
15726
  }
16332
15727
  else {
16333
- vertices.push(px, py);
16334
- vertices.push(p2x - (px - p2x), p2y - (py - p2y));
15728
+ stripVertices.push(px, py);
15729
+ stripVertices.push(p2x - (px - p2x), p2y - (py - p2y));
16335
15730
  }
16336
15731
  }
16337
15732
  p1x = points[(length - 2) * 2];
@@ -16345,70 +15740,112 @@ const buildPath = (points, width, vertices = [], indices = []) => {
16345
15740
  perpy /= dist;
16346
15741
  perpx *= lineWidth;
16347
15742
  perpy *= lineWidth;
16348
- vertices.push(p2x - perpx, p2y - perpy);
16349
- vertices.push(p2x + perpx, p2y + perpy);
16350
- indices.push(indexStart);
16351
- for (let i = 0; i < indexCount; i++) {
16352
- indices.push(indexStart++);
16353
- }
16354
- indices.push(indexStart - 1);
16355
- 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 };
16356
15767
  };
16357
- const buildCircle = (centerX, centerY, radius, vertices = [], indices = []) => {
16358
- const length = Math.floor(15 * Math.sqrt(radius + radius)), segment = (Math.PI * 2) / length, points = [];
16359
- let index = vertices.length / 6;
16360
- indices.push(index);
16361
- for (let i = 0; i < length + 1; i++) {
16362
- 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);
16363
15779
  points.push(segmentX, segmentY);
16364
- vertices.push(centerX, centerY);
16365
- vertices.push(segmentX, segmentY);
16366
- 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;
16367
15790
  }
16368
- indices.push(index - 1);
16369
- return new Geometry({ vertices, indices, points });
15791
+ return { vertices, indices, points };
16370
15792
  };
16371
- const buildEllipse = (centerX, centerY, radiusX, radiusY, vertices = [], indices = []) => {
16372
- const length = Math.floor(15 * Math.sqrt(radiusX + radiusY)), segment = (Math.PI * 2) / length, points = [];
16373
- let index = vertices.length / 6;
16374
- indices.push(index);
16375
- for (let i = 0; i < length + 1; i++) {
16376
- 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);
16377
15803
  points.push(segmentX, segmentY);
16378
- vertices.push(centerX, centerY);
16379
- vertices.push(segmentX, segmentY);
16380
- 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;
16381
15814
  }
16382
- indices.push(index - 1);
16383
- return new Geometry({ vertices, indices, points });
15815
+ return { vertices, indices, points };
16384
15816
  };
16385
- const buildPolygon = (points, vertices = [], indices = []) => {
15817
+ const buildPolygon = (points) => {
16386
15818
  if (points.length < 6) {
16387
15819
  throw new Error('At least three X/Y pairs are required to build a polygon.');
16388
15820
  }
16389
- const index = vertices.length / 6, length = points.length / 2, triangles = earcut(points, [], 2);
16390
- if (triangles) {
16391
- for (let i = 0; i < triangles.length; i += 3) {
16392
- indices.push(triangles[i] + index);
16393
- indices.push(triangles[i] + index);
16394
- indices.push(triangles[i + 1] + index);
16395
- indices.push(triangles[i + 2] + index);
16396
- indices.push(triangles[i + 2] + index);
16397
- }
16398
- for (let i = 0; i < length; i++) {
16399
- vertices.push(points[i * 2], points[(i * 2) + 1]);
16400
- }
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];
16401
15827
  }
16402
- return new Geometry({ vertices, indices, points });
15828
+ const indices = triangles ? new Uint16Array(triangles) : new Uint16Array(0);
15829
+ return { vertices, indices, points };
16403
15830
  };
16404
- const buildRectangle = (x, y, width, height, vertices = [], indices = []) => {
16405
- const points = [x, y, x + width, y, x, y + height, x + width, y + height], index = vertices.length / 6;
16406
- vertices.push(...points);
16407
- indices.push(index, index, index + 1, index + 2, index + 3, index + 3);
16408
- 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 };
16409
15843
  };
16410
15844
  const buildStar = (centerX, centerY, points, radius, innerRadius = radius / 2, rotation = 0) => {
16411
- 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 = [];
16412
15849
  for (let i = 0; i < length; i++) {
16413
15850
  const angle = startAngle + (i * delta);
16414
15851
  const rad = i % 2 ? innerRadius : radius;
@@ -17251,23 +16688,6 @@ class UniversalEmitter {
17251
16688
  }
17252
16689
  }
17253
16690
 
17254
- class CircleGeometry extends Geometry {
17255
- constructor(centerX, centerY, radius) {
17256
- const length = Math.floor(15 * Math.sqrt(radius + radius)), segment = (Math.PI * 2) / length, vertices = [], indices = [], points = [];
17257
- let index = vertices.length / 6;
17258
- indices.push(index);
17259
- for (let i = 0; i < length + 1; i++) {
17260
- const segmentX = centerX + (Math.sin(segment * i) * radius), segmentY = centerY + (Math.cos(segment * i) * radius);
17261
- points.push(segmentX, segmentY);
17262
- vertices.push(centerX, centerY);
17263
- vertices.push(segmentX, segmentY);
17264
- indices.push(index++, index++);
17265
- }
17266
- indices.push(index - 1);
17267
- super({ vertices, indices, points });
17268
- }
17269
- }
17270
-
17271
16691
  class Graphics extends Container {
17272
16692
  _lineWidth = 0;
17273
16693
  _lineColor = new Color();
@@ -17298,14 +16718,14 @@ class Graphics extends Container {
17298
16718
  return super.getChildAt(index);
17299
16719
  }
17300
16720
  addChild(child) {
17301
- if (!(child instanceof DrawableShape)) {
17302
- throw new Error('Graphics can only contain DrawableShape children.');
16721
+ if (!(child instanceof Mesh)) {
16722
+ throw new Error('Graphics can only contain Mesh children.');
17303
16723
  }
17304
16724
  return super.addChild(child);
17305
16725
  }
17306
16726
  addChildAt(child, index) {
17307
- if (!(child instanceof DrawableShape)) {
17308
- throw new Error('Graphics can only contain DrawableShape children.');
16727
+ if (!(child instanceof Mesh)) {
16728
+ throw new Error('Graphics can only contain Mesh children.');
17309
16729
  }
17310
16730
  return super.addChildAt(child, index);
17311
16731
  }
@@ -17401,50 +16821,52 @@ class Graphics extends Container {
17401
16821
  return this;
17402
16822
  }
17403
16823
  drawLine(startX, startY, endX, endY) {
17404
- 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));
17405
16826
  return this;
17406
16827
  }
17407
16828
  drawPath(path) {
17408
- 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));
17409
16831
  return this;
17410
16832
  }
17411
16833
  drawPolygon(path) {
17412
- const polygon = buildPolygon(path);
17413
- this.addChild(new DrawableShape(polygon, this._fillColor, RenderingPrimitives.TriangleStrip));
16834
+ const data = buildPolygon(path);
16835
+ this.addChild(this._createMesh(data, this._fillColor));
17414
16836
  if (this._lineWidth > 0) {
17415
- this.drawPath(polygon.points);
16837
+ this.drawPath(data.points);
17416
16838
  }
17417
16839
  return this;
17418
16840
  }
17419
16841
  drawCircle(centerX, centerY, radius) {
17420
- const circle = new CircleGeometry(centerX, centerY, radius);
17421
- this.addChild(new DrawableShape(circle, this._fillColor, RenderingPrimitives.TriangleStrip));
16842
+ const data = buildCircle(centerX, centerY, radius);
16843
+ this.addChild(this._createMesh(data, this._fillColor));
17422
16844
  if (this._lineWidth > 0) {
17423
- this.drawPath(circle.points);
16845
+ this.drawPath(data.points);
17424
16846
  }
17425
16847
  return this;
17426
16848
  }
17427
16849
  drawEllipse(centerX, centerY, radiusX, radiusY) {
17428
- const ellipse = buildEllipse(centerX, centerY, radiusX, radiusY);
17429
- 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));
17430
16852
  if (this._lineWidth > 0) {
17431
- this.drawPath(ellipse.points);
16853
+ this.drawPath(data.points);
17432
16854
  }
17433
16855
  return this;
17434
16856
  }
17435
16857
  drawRectangle(x, y, width, height) {
17436
- const rectangle = buildRectangle(x, y, width, height);
17437
- 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));
17438
16860
  if (this._lineWidth > 0) {
17439
- this.drawPath(rectangle.points);
16861
+ this.drawPath(data.points);
17440
16862
  }
17441
16863
  return this;
17442
16864
  }
17443
16865
  drawStar(centerX, centerY, points, radius, innerRadius = radius / 2, rotation = 0) {
17444
- const star = buildStar(centerX, centerY, points, radius, innerRadius, rotation);
17445
- 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));
17446
16868
  if (this._lineWidth > 0) {
17447
- this.drawPath(star.points);
16869
+ this.drawPath(data.points);
17448
16870
  }
17449
16871
  return this;
17450
16872
  }
@@ -17463,6 +16885,14 @@ class Graphics extends Container {
17463
16885
  this._fillColor.destroy();
17464
16886
  this._currentPoint.destroy();
17465
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
+ }
17466
16896
  }
17467
16897
 
17468
16898
  class Spritesheet {
@@ -19079,5 +18509,5 @@ const createRapierPhysicsWorld = async (options = {}) => {
19079
18509
  return await RapierPhysicsWorld.create(options);
19080
18510
  };
19081
18511
 
19082
- export { AbstractAssetFactory, AbstractMedia, AbstractWebGl2BatchedRenderer, AbstractWebGl2Renderer, AbstractWebGpuRenderer, AnimatedSprite, Application, ApplicationStatus, ArcadeStickGamepadMapping, AudioAnalyser, BinaryFactory, BlendModes, BlurFilter, Bounds, BufferTypes, BufferUsage, BundleLoadError, CacheFirstStrategy, CallbackRenderPass, 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, capabilities, 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, isSupported, 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 };
19083
18513
  //# sourceMappingURL=exo.esm.js.map