@vectojs/core 0.2.6 → 0.2.7

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.
@@ -892,6 +892,19 @@ var Entity = (_class4 = class {
892
892
  getBatchRect() {
893
893
  return null;
894
894
  }
895
+ /**
896
+ * Opt into DOM content projection for entities that render static text.
897
+ * The {@link Scene} mirrors the returned text as a transparent DOM node
898
+ * positioned over the drawn glyphs, making canvas text findable (Ctrl+F),
899
+ * readable by screen readers and crawlers, translatable, and — when
900
+ * `selectable` is set — natively selectable. Returns `null` by default.
901
+ * Read on the a11y sync cadence, so text changes propagate automatically.
902
+ *
903
+ * @returns The projection descriptor, or `null` to project nothing.
904
+ */
905
+ getContentProjection() {
906
+ return null;
907
+ }
895
908
  /**
896
909
  * Whether this entity still has a queued/running tween animation, or an
897
910
  * active {@link setTransition}/{@link animateTo}/{@link springTo} property
@@ -1067,6 +1080,18 @@ var MSDFTextEntity = (_class6 = class extends Entity {
1067
1080
  }
1068
1081
  });
1069
1082
  }
1083
+ /**
1084
+ * Mirror the rendered text into the DOM content layer: find-in-page, screen
1085
+ * readers, crawlers, and translation see the same string the canvas draws.
1086
+ */
1087
+ getContentProjection() {
1088
+ if (!this.text) return null;
1089
+ return {
1090
+ text: this.text,
1091
+ font: `${this.fontSize}px ${this.fallbackFont}`,
1092
+ lineHeight: this.lineHeight
1093
+ };
1094
+ }
1070
1095
  isPointInside(globalX, globalY) {
1071
1096
  if (!this.layoutResult) return false;
1072
1097
  const local = this.worldToLocal(globalX, globalY);
@@ -34,6 +34,10 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
34
34
  this.height = _nullishCoalesce(_optionalChain([size, 'optionalAccess', _2 => _2.height]), () => ( (typeof window !== "undefined" ? window.innerHeight : canvas.height || 0)));
35
35
  canvas.width = this.width * dpr;
36
36
  canvas.height = this.height * dpr;
37
+ if (canvas.style) {
38
+ canvas.style.width = `${this.width}px`;
39
+ canvas.style.height = `${this.height}px`;
40
+ }
37
41
  const ctx = canvas.getContext("2d");
38
42
  this.ctx = ctx;
39
43
  if (ctx) ctx.scale(dpr, dpr);
@@ -34,6 +34,10 @@ var CanvasRenderer = class _CanvasRenderer {
34
34
  this.height = size?.height ?? (typeof window !== "undefined" ? window.innerHeight : canvas.height || 0);
35
35
  canvas.width = this.width * dpr;
36
36
  canvas.height = this.height * dpr;
37
+ if (canvas.style) {
38
+ canvas.style.width = `${this.width}px`;
39
+ canvas.style.height = `${this.height}px`;
40
+ }
37
41
  const ctx = canvas.getContext("2d");
38
42
  this.ctx = ctx;
39
43
  if (ctx) ctx.scale(dpr, dpr);
@@ -892,6 +892,19 @@ var Entity = class {
892
892
  getBatchRect() {
893
893
  return null;
894
894
  }
895
+ /**
896
+ * Opt into DOM content projection for entities that render static text.
897
+ * The {@link Scene} mirrors the returned text as a transparent DOM node
898
+ * positioned over the drawn glyphs, making canvas text findable (Ctrl+F),
899
+ * readable by screen readers and crawlers, translatable, and — when
900
+ * `selectable` is set — natively selectable. Returns `null` by default.
901
+ * Read on the a11y sync cadence, so text changes propagate automatically.
902
+ *
903
+ * @returns The projection descriptor, or `null` to project nothing.
904
+ */
905
+ getContentProjection() {
906
+ return null;
907
+ }
895
908
  /**
896
909
  * Whether this entity still has a queued/running tween animation, or an
897
910
  * active {@link setTransition}/{@link animateTo}/{@link springTo} property
@@ -1067,6 +1080,18 @@ var MSDFTextEntity = class extends Entity {
1067
1080
  }
1068
1081
  });
1069
1082
  }
1083
+ /**
1084
+ * Mirror the rendered text into the DOM content layer: find-in-page, screen
1085
+ * readers, crawlers, and translation see the same string the canvas draws.
1086
+ */
1087
+ getContentProjection() {
1088
+ if (!this.text) return null;
1089
+ return {
1090
+ text: this.text,
1091
+ font: `${this.fontSize}px ${this.fallbackFont}`,
1092
+ lineHeight: this.lineHeight
1093
+ };
1094
+ }
1070
1095
  isPointInside(globalX, globalY) {
1071
1096
  if (!this.layoutResult) return false;
1072
1097
  const local = this.worldToLocal(globalX, globalY);
@@ -1,4 +1,4 @@
1
- import { Entity } from '../tree/Entity';
1
+ import { Entity, type ContentProjection } from '../tree/Entity';
2
2
  import { IRenderer } from '../renderer/IRenderer';
3
3
  export declare class TextEntity extends Entity {
4
4
  text: string;
@@ -13,6 +13,11 @@ export declare class TextEntity extends Entity {
13
13
  lineWidth: number;
14
14
  private isHovered;
15
15
  constructor(text: string, atlas: any, maxWidth: number, fontSize?: number);
16
+ /**
17
+ * Mirror the rendered text into the DOM content layer: find-in-page, screen
18
+ * readers, crawlers, and translation see the same string the canvas draws.
19
+ */
20
+ getContentProjection(): ContentProjection | null;
16
21
  /**
17
22
  * Replace the text content. Runs the **cold** measurement pass (re-segment +
18
23
  * re-measure) since the glyphs changed, then re-lays out.
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ var _chunkBWLNEJJWjs = require('./chunk-BWLNEJJW.js');
12
12
 
13
13
 
14
14
 
15
- var _chunkGADBN53Zjs = require('./chunk-GADBN53Z.js');
15
+ var _chunkGTQRCY6Wjs = require('./chunk-GTQRCY6W.js');
16
16
 
17
17
 
18
18
 
@@ -24,7 +24,7 @@ var _chunkGADBN53Zjs = require('./chunk-GADBN53Z.js');
24
24
 
25
25
 
26
26
 
27
- var _chunk4BO5XBJLjs = require('./chunk-4BO5XBJL.js');
27
+ var _chunkDWMQYHNCjs = require('./chunk-DWMQYHNC.js');
28
28
 
29
29
 
30
30
 
@@ -41,7 +41,7 @@ var PARTICLE_OFFSET_ORIGIN_X = 4;
41
41
  var PARTICLE_OFFSET_ORIGIN_Y = 5;
42
42
  var PARTICLE_OFFSET_SIZE = 6;
43
43
  var PARTICLE_OFFSET_LIFE = 7;
44
- var ComputeParticleEntity = (_class = class extends _chunk4BO5XBJLjs.Entity {
44
+ var ComputeParticleEntity = (_class = class extends _chunkDWMQYHNCjs.Entity {
45
45
 
46
46
 
47
47
 
@@ -296,6 +296,11 @@ var ComputeParticleEntity = (_class = class extends _chunk4BO5XBJLjs.Entity {
296
296
 
297
297
  // src/tree/Scene.ts
298
298
  var REDUCED_MOTION_FPS = 30;
299
+ function parseInlinePx(value) {
300
+ if (!value || !value.endsWith("px")) return null;
301
+ const n = parseFloat(value);
302
+ return Number.isFinite(n) && n > 0 ? n : null;
303
+ }
299
304
  var Scene = (_class2 = class _Scene {
300
305
  static __initStatic() {this.webglCreator = null}
301
306
  static __initStatic2() {this.webgpuManagerClass = null}
@@ -350,69 +355,75 @@ var Scene = (_class2 = class _Scene {
350
355
  // server-side (e.g. headless layout / vector export) without jsdom.
351
356
 
352
357
  __init18() {this.a11yElements = /* @__PURE__ */ new Map()}
358
+ /** DOM nodes mirroring static text content, keyed by entity id. */
359
+ __init19() {this.contentElements = /* @__PURE__ */ new Map()}
360
+ __init20() {this.contentProjectionEnabled = true}
353
361
 
354
- __init19() {this.focusedA11yElement = null}
355
- __init20() {this.caretBlinkTimer = null}
356
- __init21() {this.a11yNeedsReorder = true}
357
- __init22() {this.portalRoot = null}
358
- __init23() {this.fullViewportElements = []}
359
- __init24() {this.normalElements = []}
360
- __init25() {this.activeIds = /* @__PURE__ */ new Set()}
361
- __init26() {this.activePortalsThisFrame = /* @__PURE__ */ new Set()}
362
- __init27() {this.activePortalsPrevFrame = /* @__PURE__ */ new Set()}
363
- __init28() {this.portalEntities = /* @__PURE__ */ new Map()}
364
- __init29() {this.renderOrderCounter = 0}
362
+ __init21() {this.focusedA11yElement = null}
363
+ __init22() {this.caretBlinkTimer = null}
364
+ __init23() {this.a11yNeedsReorder = true}
365
+ __init24() {this.portalRoot = null}
366
+ __init25() {this.fullViewportElements = []}
367
+ __init26() {this.normalElements = []}
368
+ __init27() {this.activeIds = /* @__PURE__ */ new Set()}
369
+ __init28() {this.activePortalsThisFrame = /* @__PURE__ */ new Set()}
370
+ __init29() {this.activePortalsPrevFrame = /* @__PURE__ */ new Set()}
371
+ __init30() {this.portalEntities = /* @__PURE__ */ new Map()}
372
+ __init31() {this.renderOrderCounter = 0}
365
373
  // Optional WebGL point-cloud layer (see SceneOptions.pointBackend).
366
- __init30() {this.pointRenderer = null}
367
- __init31() {this.glCanvas = null}
374
+ __init32() {this.pointRenderer = null}
375
+ __init33() {this.glCanvas = null}
368
376
 
369
377
 
370
378
 
371
- __init32() {this.disableWindowResize = false}
379
+ __init34() {this.disableWindowResize = false}
372
380
  // WebGPU properties
373
- __init33() {this.destroyed = false}
374
- __init34() {this.device = null}
375
- __init35() {this.deviceLost = false}
376
- __init36() {this.particleBackend = "auto"}
377
- __init37() {this._webgpuDisabled = false}
381
+ __init35() {this.destroyed = false}
382
+ __init36() {this.device = null}
383
+ __init37() {this.deviceLost = false}
384
+ __init38() {this.particleBackend = "auto"}
385
+ __init39() {this._webgpuDisabled = false}
378
386
  get webgpuDisabled() {
379
387
  return this._webgpuDisabled || this.particleBackend === "cpu";
380
388
  }
381
389
  set webgpuDisabled(value) {
382
390
  this._webgpuDisabled = value;
383
391
  }
384
- __init38() {this.recoveryTimerId = null}
385
- __init39() {this.manager = null}
386
- __init40() {this.initializingWebGPU = false}
387
- __init41() {this.gpuCanvas = null}
388
- __init42() {this.gpuContext = null}
392
+ __init40() {this.recoveryTimerId = null}
393
+ __init41() {this.manager = null}
394
+ __init42() {this.initializingWebGPU = false}
395
+ __init43() {this.gpuCanvas = null}
396
+ __init44() {this.gpuContext = null}
389
397
  /** True while the GPU canvas holds a presented particle frame (needs clearing when they leave). */
390
- __init43() {this.gpuHasContent = false}
391
- __init44() {this.mouseX = -9999}
392
- __init45() {this.mouseY = -9999}
393
- __init46() {this.pointerMoveListener = null}
394
- __init47() {this.pointerLeaveListener = null}
395
- __init48() {this.hasWarnedZeroSize = false}
396
- constructor(canvas, options = {}) {;_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);_class2.prototype.__init11.call(this);_class2.prototype.__init12.call(this);_class2.prototype.__init13.call(this);_class2.prototype.__init14.call(this);_class2.prototype.__init15.call(this);_class2.prototype.__init16.call(this);_class2.prototype.__init17.call(this);_class2.prototype.__init18.call(this);_class2.prototype.__init19.call(this);_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);_class2.prototype.__init27.call(this);_class2.prototype.__init28.call(this);_class2.prototype.__init29.call(this);_class2.prototype.__init30.call(this);_class2.prototype.__init31.call(this);_class2.prototype.__init32.call(this);_class2.prototype.__init33.call(this);_class2.prototype.__init34.call(this);_class2.prototype.__init35.call(this);_class2.prototype.__init36.call(this);_class2.prototype.__init37.call(this);_class2.prototype.__init38.call(this);_class2.prototype.__init39.call(this);_class2.prototype.__init40.call(this);_class2.prototype.__init41.call(this);_class2.prototype.__init42.call(this);_class2.prototype.__init43.call(this);_class2.prototype.__init44.call(this);_class2.prototype.__init45.call(this);_class2.prototype.__init46.call(this);_class2.prototype.__init47.call(this);_class2.prototype.__init48.call(this);
398
+ __init45() {this.gpuHasContent = false}
399
+ __init46() {this.mouseX = -9999}
400
+ __init47() {this.mouseY = -9999}
401
+ __init48() {this.pointerMoveListener = null}
402
+ __init49() {this.pointerLeaveListener = null}
403
+ __init50() {this.hasWarnedZeroSize = false}
404
+ constructor(canvas, options = {}) {;_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);_class2.prototype.__init11.call(this);_class2.prototype.__init12.call(this);_class2.prototype.__init13.call(this);_class2.prototype.__init14.call(this);_class2.prototype.__init15.call(this);_class2.prototype.__init16.call(this);_class2.prototype.__init17.call(this);_class2.prototype.__init18.call(this);_class2.prototype.__init19.call(this);_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);_class2.prototype.__init27.call(this);_class2.prototype.__init28.call(this);_class2.prototype.__init29.call(this);_class2.prototype.__init30.call(this);_class2.prototype.__init31.call(this);_class2.prototype.__init32.call(this);_class2.prototype.__init33.call(this);_class2.prototype.__init34.call(this);_class2.prototype.__init35.call(this);_class2.prototype.__init36.call(this);_class2.prototype.__init37.call(this);_class2.prototype.__init38.call(this);_class2.prototype.__init39.call(this);_class2.prototype.__init40.call(this);_class2.prototype.__init41.call(this);_class2.prototype.__init42.call(this);_class2.prototype.__init43.call(this);_class2.prototype.__init44.call(this);_class2.prototype.__init45.call(this);_class2.prototype.__init46.call(this);_class2.prototype.__init47.call(this);_class2.prototype.__init48.call(this);_class2.prototype.__init49.call(this);_class2.prototype.__init50.call(this);
397
405
  this.canvas = canvas;
398
406
  this.debugA11y = _nullishCoalesce(options.debugA11y, () => ( false));
399
407
  this.disableWindowResize = _nullishCoalesce(options.disableWindowResize, () => ( false));
400
408
  if (this.disableWindowResize) {
401
- this.width = canvas.width || canvas.clientWidth || 0;
402
- this.height = canvas.height || canvas.clientHeight || 0;
409
+ const styleWidth = parseInlinePx(_optionalChain([canvas, 'access', _15 => _15.style, 'optionalAccess', _16 => _16.width]));
410
+ const styleHeight = parseInlinePx(_optionalChain([canvas, 'access', _17 => _17.style, 'optionalAccess', _18 => _18.height]));
411
+ this.width = _nullishCoalesce(styleWidth, () => ( (canvas.width || canvas.clientWidth || 0)));
412
+ this.height = _nullishCoalesce(styleHeight, () => ( (canvas.height || canvas.clientHeight || 0)));
403
413
  } else {
404
414
  this.width = typeof window !== "undefined" ? window.innerWidth : canvas.clientWidth || canvas.width || 800;
405
415
  this.height = typeof window !== "undefined" ? window.innerHeight : canvas.clientHeight || canvas.height || 600;
406
416
  }
407
417
  const globalProcess = typeof globalThis !== "undefined" ? globalThis.process : void 0;
408
- const isTest = globalProcess && (_optionalChain([globalProcess, 'access', _15 => _15.env, 'optionalAccess', _16 => _16.NODE_ENV]) === "test" || _optionalChain([globalProcess, 'access', _17 => _17.env, 'optionalAccess', _18 => _18.VITEST]) === "true");
418
+ const isTest = globalProcess && (_optionalChain([globalProcess, 'access', _19 => _19.env, 'optionalAccess', _20 => _20.NODE_ENV]) === "test" || _optionalChain([globalProcess, 'access', _21 => _21.env, 'optionalAccess', _22 => _22.VITEST]) === "true");
409
419
  this.maxFPS = _nullishCoalesce(options.maxFPS, () => ( (isTest ? 0 : 60)));
410
420
  this.respectReducedMotion = _nullishCoalesce(options.respectReducedMotion, () => ( true));
411
421
  this.autoThrottle = _nullishCoalesce(options.autoThrottle, () => ( true));
412
422
  this.particleBackend = _nullishCoalesce(options.particleBackend, () => ( "auto"));
413
423
  this.a11ySyncInterval = _nullishCoalesce(options.a11ySyncInterval, () => ( 0));
424
+ this.contentProjectionEnabled = _nullishCoalesce(options.contentProjection, () => ( true));
414
425
  this.reducedMotionQuery = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
415
- this.root = new class RootEntity extends _chunk4BO5XBJLjs.Entity {
426
+ this.root = new class RootEntity extends _chunkDWMQYHNCjs.Entity {
416
427
  isPointInside() {
417
428
  return false;
418
429
  }
@@ -421,7 +432,7 @@ var Scene = (_class2 = class _Scene {
421
432
  }
422
433
  }("root");
423
434
  this.root._scene = this;
424
- this.overlayRoot = new class OverlayRoot extends _chunk4BO5XBJLjs.Entity {
435
+ this.overlayRoot = new class OverlayRoot extends _chunkDWMQYHNCjs.Entity {
425
436
  isPointInside() {
426
437
  return false;
427
438
  }
@@ -432,7 +443,7 @@ var Scene = (_class2 = class _Scene {
432
443
  if (options.renderer) {
433
444
  this.renderer = options.renderer;
434
445
  } else {
435
- this.renderer = new (0, _chunkGADBN53Zjs.CanvasRenderer)(
446
+ this.renderer = new (0, _chunkGTQRCY6Wjs.CanvasRenderer)(
436
447
  canvas,
437
448
  this.disableWindowResize ? { width: this.width, height: this.height } : void 0
438
449
  );
@@ -498,7 +509,7 @@ var Scene = (_class2 = class _Scene {
498
509
  }
499
510
  /** Convert browser viewport coordinates into this Scene's logical coordinates. */
500
511
  clientToScene(clientX, clientY) {
501
- const rect = _optionalChain([this, 'access', _19 => _19.canvas, 'access', _20 => _20.getBoundingClientRect, 'optionalCall', _21 => _21()]);
512
+ const rect = _optionalChain([this, 'access', _23 => _23.canvas, 'access', _24 => _24.getBoundingClientRect, 'optionalCall', _25 => _25()]);
502
513
  if (!rect) return { x: clientX, y: clientY };
503
514
  const cssWidth = rect.width || this.canvas.clientWidth || this.width;
504
515
  const cssHeight = rect.height || this.canvas.clientHeight || this.height;
@@ -564,6 +575,11 @@ var Scene = (_class2 = class _Scene {
564
575
  * @param entity - The subtree whose shadow nodes should be removed.
565
576
  */
566
577
  detachA11y(entity) {
578
+ const contentEl = this.contentElements.get(entity.id);
579
+ if (contentEl) {
580
+ contentEl.remove();
581
+ this.contentElements.delete(entity.id);
582
+ }
567
583
  this.removeA11yRecursively(entity);
568
584
  }
569
585
  /**
@@ -607,13 +623,15 @@ var Scene = (_class2 = class _Scene {
607
623
  this.canvas.removeEventListener("pointerleave", this.pointerLeaveListener);
608
624
  }
609
625
  }
610
- _optionalChain([this, 'access', _22 => _22.a11yRoot, 'optionalAccess', _23 => _23.remove, 'call', _24 => _24()]);
611
- _optionalChain([this, 'access', _25 => _25.portalRoot, 'optionalAccess', _26 => _26.remove, 'call', _27 => _27()]);
626
+ _optionalChain([this, 'access', _26 => _26.a11yRoot, 'optionalAccess', _27 => _27.remove, 'call', _28 => _28()]);
627
+ _optionalChain([this, 'access', _29 => _29.portalRoot, 'optionalAccess', _30 => _30.remove, 'call', _31 => _31()]);
612
628
  this.a11yElements.clear();
613
- _optionalChain([this, 'access', _28 => _28.pointRenderer, 'optionalAccess', _29 => _29.destroy, 'call', _30 => _30()]);
614
- _optionalChain([this, 'access', _31 => _31.renderer, 'access', _32 => _32.dispose, 'optionalCall', _33 => _33()]);
615
- _optionalChain([this, 'access', _34 => _34.glCanvas, 'optionalAccess', _35 => _35.remove, 'call', _36 => _36()]);
616
- _optionalChain([this, 'access', _37 => _37.gpuCanvas, 'optionalAccess', _38 => _38.remove, 'call', _39 => _39()]);
629
+ for (const el of this.contentElements.values()) el.remove();
630
+ this.contentElements.clear();
631
+ _optionalChain([this, 'access', _32 => _32.pointRenderer, 'optionalAccess', _33 => _33.destroy, 'call', _34 => _34()]);
632
+ _optionalChain([this, 'access', _35 => _35.renderer, 'access', _36 => _36.dispose, 'optionalCall', _37 => _37()]);
633
+ _optionalChain([this, 'access', _38 => _38.glCanvas, 'optionalAccess', _39 => _39.remove, 'call', _40 => _40()]);
634
+ _optionalChain([this, 'access', _41 => _41.gpuCanvas, 'optionalAccess', _42 => _42.remove, 'call', _43 => _43()]);
617
635
  this.gpuCanvas = null;
618
636
  this.gpuContext = null;
619
637
  if (this.recoveryTimerId) {
@@ -625,7 +643,7 @@ var Scene = (_class2 = class _Scene {
625
643
  this.manager = null;
626
644
  }
627
645
  if (this.device) {
628
- _optionalChain([this, 'access', _40 => _40.device, 'access', _41 => _41.destroy, 'optionalCall', _42 => _42()]);
646
+ _optionalChain([this, 'access', _44 => _44.device, 'access', _45 => _45.destroy, 'optionalCall', _46 => _46()]);
629
647
  this.device = null;
630
648
  }
631
649
  }
@@ -769,42 +787,42 @@ var Scene = (_class2 = class _Scene {
769
787
  el.style.background = "transparent";
770
788
  }
771
789
  el.addEventListener("click", (e) => {
772
- node.dispatchEvent(new (0, _chunk4BO5XBJLjs.VectoJSEvent)("click", node, e));
790
+ node.dispatchEvent(new (0, _chunkDWMQYHNCjs.VectoJSEvent)("click", node, e));
773
791
  });
774
792
  el.addEventListener("mouseenter", (e) => {
775
793
  if (this.debugA11y) el.style.backgroundColor = "rgba(56, 189, 248, 0.2)";
776
- node.dispatchEvent(new (0, _chunk4BO5XBJLjs.VectoJSEvent)("hover", node, e, false));
794
+ node.dispatchEvent(new (0, _chunkDWMQYHNCjs.VectoJSEvent)("hover", node, e, false));
777
795
  });
778
796
  el.addEventListener("mouseleave", (e) => {
779
797
  if (this.debugA11y) el.style.backgroundColor = "rgba(56, 189, 248, 0.05)";
780
- node.dispatchEvent(new (0, _chunk4BO5XBJLjs.VectoJSEvent)("pointerleave", node, e, false));
798
+ node.dispatchEvent(new (0, _chunkDWMQYHNCjs.VectoJSEvent)("pointerleave", node, e, false));
781
799
  });
782
800
  const capEl = el;
783
801
  el.addEventListener("pointerdown", (e) => {
784
802
  if (typeof capEl.setPointerCapture === "function") capEl.setPointerCapture(e.pointerId);
785
- node.dispatchEvent(new (0, _chunk4BO5XBJLjs.VectoJSEvent)("pointerdown", node, e));
803
+ node.dispatchEvent(new (0, _chunkDWMQYHNCjs.VectoJSEvent)("pointerdown", node, e));
786
804
  });
787
805
  el.addEventListener("pointerup", (e) => {
788
806
  if (typeof capEl.releasePointerCapture === "function")
789
807
  capEl.releasePointerCapture(e.pointerId);
790
- node.dispatchEvent(new (0, _chunk4BO5XBJLjs.VectoJSEvent)("pointerup", node, e));
808
+ node.dispatchEvent(new (0, _chunkDWMQYHNCjs.VectoJSEvent)("pointerup", node, e));
791
809
  });
792
810
  el.addEventListener(
793
811
  "pointermove",
794
- (e) => node.dispatchEvent(new (0, _chunk4BO5XBJLjs.VectoJSEvent)("pointermove", node, e))
812
+ (e) => node.dispatchEvent(new (0, _chunkDWMQYHNCjs.VectoJSEvent)("pointermove", node, e))
795
813
  );
796
814
  el.addEventListener(
797
815
  "wheel",
798
816
  (e) => {
799
- node.dispatchEvent(new (0, _chunk4BO5XBJLjs.VectoJSEvent)("wheel", node, e));
817
+ node.dispatchEvent(new (0, _chunkDWMQYHNCjs.VectoJSEvent)("wheel", node, e));
800
818
  },
801
819
  { passive: false }
802
820
  );
803
821
  el.addEventListener("keydown", (e) => {
804
- node.dispatchEvent(new (0, _chunk4BO5XBJLjs.VectoJSEvent)("keydown", node, e));
822
+ node.dispatchEvent(new (0, _chunkDWMQYHNCjs.VectoJSEvent)("keydown", node, e));
805
823
  });
806
824
  el.addEventListener("keyup", (e) => {
807
- node.dispatchEvent(new (0, _chunk4BO5XBJLjs.VectoJSEvent)("keyup", node, e));
825
+ node.dispatchEvent(new (0, _chunkDWMQYHNCjs.VectoJSEvent)("keyup", node, e));
808
826
  });
809
827
  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
810
828
  const input = el;
@@ -831,7 +849,7 @@ var Scene = (_class2 = class _Scene {
831
849
  });
832
850
  el.addEventListener("compositionupdate", (e) => {
833
851
  const data = _nullishCoalesce(e.data, () => ( ""));
834
- composition = { start: _nullishCoalesce(_optionalChain([composition, 'optionalAccess', _43 => _43.start]), () => ( 0)), length: data.length };
852
+ composition = { start: _nullishCoalesce(_optionalChain([composition, 'optionalAccess', _47 => _47.start]), () => ( 0)), length: data.length };
835
853
  forward();
836
854
  });
837
855
  el.addEventListener("compositionend", () => {
@@ -877,7 +895,7 @@ var Scene = (_class2 = class _Scene {
877
895
  el.addEventListener("keydown", (e) => {
878
896
  if (e.key === "Enter" || e.key === " ") {
879
897
  e.preventDefault();
880
- node.dispatchEvent(new (0, _chunk4BO5XBJLjs.VectoJSEvent)("click", node, e));
898
+ node.dispatchEvent(new (0, _chunkDWMQYHNCjs.VectoJSEvent)("click", node, e));
881
899
  }
882
900
  });
883
901
  }
@@ -902,7 +920,7 @@ var Scene = (_class2 = class _Scene {
902
920
  if (el.placeholder !== attrs.placeholder) el.placeholder = attrs.placeholder;
903
921
  }
904
922
  if (attrs.href !== void 0 && el instanceof HTMLAnchorElement) {
905
- const safeHref = _chunkGADBN53Zjs.sanitizeUrl.call(void 0, attrs.href);
923
+ const safeHref = _chunkGTQRCY6Wjs.sanitizeUrl.call(void 0, attrs.href);
906
924
  if (el.getAttribute("href") !== safeHref) el.setAttribute("href", safeHref);
907
925
  }
908
926
  if (el instanceof HTMLImageElement) {
@@ -972,11 +990,96 @@ var Scene = (_class2 = class _Scene {
972
990
  el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
973
991
  }
974
992
  }
993
+ this.syncContentProjection(node);
975
994
  for (const child of node.children) this.syncA11y(child);
976
995
  if (node === this.root) {
977
996
  for (const overlay of this.overlayRoot.children) this.syncA11y(overlay);
978
997
  }
979
998
  }
999
+ /**
1000
+ * Mirror one entity's static text ({@link Entity.getContentProjection}) as a
1001
+ * transparent DOM node positioned over the drawn glyphs. Runs on the a11y
1002
+ * sync cadence; all writes are dirty-checked. Off-viewport projections are
1003
+ * hidden (`display: none`) so text-heavy scenes only materialize what is
1004
+ * visible to the browser's text machinery anyway.
1005
+ */
1006
+ syncContentProjection(node) {
1007
+ if (!this.contentProjectionEnabled || !this.a11yRoot) return;
1008
+ const projection = node.getContentProjection();
1009
+ let el = this.contentElements.get(node.id);
1010
+ if (!projection || !projection.text) {
1011
+ if (el) {
1012
+ el.remove();
1013
+ this.contentElements.delete(node.id);
1014
+ }
1015
+ return;
1016
+ }
1017
+ if (!el) {
1018
+ el = document.createElement("div");
1019
+ el.setAttribute("data-vecto-content", node.id);
1020
+ const s = el.style;
1021
+ s.position = "absolute";
1022
+ s.transformOrigin = "0 0";
1023
+ s.margin = "0";
1024
+ s.padding = "0";
1025
+ s.color = "transparent";
1026
+ s.whiteSpace = "pre-wrap";
1027
+ s.overflow = "hidden";
1028
+ s.zIndex = "0";
1029
+ el.addEventListener(
1030
+ "wheel",
1031
+ (e2) => {
1032
+ node.dispatchEvent(new (0, _chunkDWMQYHNCjs.VectoJSEvent)("wheel", node, e2));
1033
+ },
1034
+ { passive: false }
1035
+ );
1036
+ this.a11yRoot.appendChild(el);
1037
+ this.contentElements.set(node.id, el);
1038
+ }
1039
+ if (el.textContent !== projection.text) el.textContent = projection.text;
1040
+ const font = _nullishCoalesce(projection.font, () => ( ""));
1041
+ if (el.style.font !== font) el.style.font = font;
1042
+ const lineHeight = projection.lineHeight !== void 0 ? `${projection.lineHeight}px` : "";
1043
+ if (el.style.lineHeight !== lineHeight) el.style.lineHeight = lineHeight;
1044
+ const hidden = node.interactive ? "true" : null;
1045
+ if (el.getAttribute("aria-hidden") !== hidden) {
1046
+ if (hidden) el.setAttribute("aria-hidden", hidden);
1047
+ else el.removeAttribute("aria-hidden");
1048
+ }
1049
+ const selectable = projection.selectable === true;
1050
+ const pointerEvents = selectable ? "auto" : "none";
1051
+ if (el.style.pointerEvents !== pointerEvents) {
1052
+ el.style.pointerEvents = pointerEvents;
1053
+ el.style.userSelect = selectable ? "text" : "none";
1054
+ el.style.cursor = selectable ? "text" : "";
1055
+ }
1056
+ const { a, b, c, d, e, f } = node.getWorldTransform();
1057
+ el.style.left = `${e}px`;
1058
+ el.style.top = `${f}px`;
1059
+ if (node.width > 0) el.style.width = `${node.width}px`;
1060
+ if (node.height > 0) el.style.height = `${node.height}px`;
1061
+ el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
1062
+ let visible = true;
1063
+ if (node.width > 0 && node.height > 0) {
1064
+ let minX = Infinity;
1065
+ let minY = Infinity;
1066
+ let maxX = -Infinity;
1067
+ let maxY = -Infinity;
1068
+ for (let i = 0; i < 4; i++) {
1069
+ const lx = i & 1 ? node.width : 0;
1070
+ const ly = i & 2 ? node.height : 0;
1071
+ const wx = a * lx + c * ly + e;
1072
+ const wy = b * lx + d * ly + f;
1073
+ if (wx < minX) minX = wx;
1074
+ if (wx > maxX) maxX = wx;
1075
+ if (wy < minY) minY = wy;
1076
+ if (wy > maxY) maxY = wy;
1077
+ }
1078
+ visible = maxX >= 0 && minX <= this.width && maxY >= 0 && minY <= this.height;
1079
+ }
1080
+ const display = visible ? "" : "none";
1081
+ if (el.style.display !== display) el.style.display = display;
1082
+ }
980
1083
  enforceA11yDomOrder() {
981
1084
  if (!this.a11yRoot) return;
982
1085
  this.fullViewportElements.length = 0;
@@ -1035,12 +1138,12 @@ var Scene = (_class2 = class _Scene {
1035
1138
  syncOverlayGeometry() {
1036
1139
  const parent = this.canvas.parentElement;
1037
1140
  if (!parent) return;
1038
- const canvasRect = _optionalChain([this, 'access', _44 => _44.canvas, 'access', _45 => _45.getBoundingClientRect, 'optionalCall', _46 => _46()]);
1039
- const parentRect = _optionalChain([parent, 'access', _47 => _47.getBoundingClientRect, 'optionalCall', _48 => _48()]);
1040
- const cssWidth = _optionalChain([canvasRect, 'optionalAccess', _49 => _49.width]) || this.canvas.clientWidth || this.width;
1041
- const cssHeight = _optionalChain([canvasRect, 'optionalAccess', _50 => _50.height]) || this.canvas.clientHeight || this.height;
1042
- const left = (_nullishCoalesce(_optionalChain([canvasRect, 'optionalAccess', _51 => _51.left]), () => ( 0))) - (_nullishCoalesce(_optionalChain([parentRect, 'optionalAccess', _52 => _52.left]), () => ( 0))) - (parent.clientLeft || 0) + parent.scrollLeft;
1043
- const top = (_nullishCoalesce(_optionalChain([canvasRect, 'optionalAccess', _53 => _53.top]), () => ( 0))) - (_nullishCoalesce(_optionalChain([parentRect, 'optionalAccess', _54 => _54.top]), () => ( 0))) - (parent.clientTop || 0) + parent.scrollTop;
1141
+ const canvasRect = _optionalChain([this, 'access', _48 => _48.canvas, 'access', _49 => _49.getBoundingClientRect, 'optionalCall', _50 => _50()]);
1142
+ const parentRect = _optionalChain([parent, 'access', _51 => _51.getBoundingClientRect, 'optionalCall', _52 => _52()]);
1143
+ const cssWidth = _optionalChain([canvasRect, 'optionalAccess', _53 => _53.width]) || this.canvas.clientWidth || this.width;
1144
+ const cssHeight = _optionalChain([canvasRect, 'optionalAccess', _54 => _54.height]) || this.canvas.clientHeight || this.height;
1145
+ const left = (_nullishCoalesce(_optionalChain([canvasRect, 'optionalAccess', _55 => _55.left]), () => ( 0))) - (_nullishCoalesce(_optionalChain([parentRect, 'optionalAccess', _56 => _56.left]), () => ( 0))) - (parent.clientLeft || 0) + parent.scrollLeft;
1146
+ const top = (_nullishCoalesce(_optionalChain([canvasRect, 'optionalAccess', _57 => _57.top]), () => ( 0))) - (_nullishCoalesce(_optionalChain([parentRect, 'optionalAccess', _58 => _58.top]), () => ( 0))) - (parent.clientTop || 0) + parent.scrollTop;
1044
1147
  const scaleX = this.width > 0 ? cssWidth / this.width : 1;
1045
1148
  const scaleY = this.height > 0 ? cssHeight / this.height : 1;
1046
1149
  for (const root of [this.a11yRoot, this.portalRoot]) {
@@ -1165,7 +1268,7 @@ var Scene = (_class2 = class _Scene {
1165
1268
  * (and {@link respectReducedMotion} is on). `0` means uncapped.
1166
1269
  */
1167
1270
  effectiveMaxFPS() {
1168
- const reduced = this.respectReducedMotion && !!_optionalChain([this, 'access', _55 => _55.reducedMotionQuery, 'optionalAccess', _56 => _56.matches]);
1271
+ const reduced = this.respectReducedMotion && !!_optionalChain([this, 'access', _59 => _59.reducedMotionQuery, 'optionalAccess', _60 => _60.matches]);
1169
1272
  if (reduced)
1170
1273
  return this.maxFPS > 0 ? Math.min(this.maxFPS, REDUCED_MOTION_FPS) : REDUCED_MOTION_FPS;
1171
1274
  return this.maxFPS;
@@ -1191,10 +1294,11 @@ var Scene = (_class2 = class _Scene {
1191
1294
  this.render(this.renderer, dt, time);
1192
1295
  const hasActiveAnimation = this.hasAnyPendingAnimation(this.root) || this.hasAnyPendingAnimation(this.overlayRoot);
1193
1296
  const hasInteractive = this.hasAnyInteractive(this.root) || this.hasAnyInteractive(this.overlayRoot);
1297
+ const wantsContentSync = this.contentProjectionEnabled;
1194
1298
  const shouldSyncInterval = this.a11ySyncInterval <= 0 || time - this.lastA11ySync >= this.a11ySyncInterval;
1195
- if ((hasInteractive || this.a11yElements.size > 0) && (shouldSyncInterval || this.a11yPendingSyncAfterAnimation)) {
1299
+ if ((hasInteractive || this.a11yElements.size > 0 || wantsContentSync) && (shouldSyncInterval || this.a11yPendingSyncAfterAnimation)) {
1196
1300
  this.lastA11ySync = time;
1197
- if (hasInteractive) {
1301
+ if (hasInteractive || wantsContentSync) {
1198
1302
  this.syncA11y(this.root);
1199
1303
  }
1200
1304
  this.enforceA11yDomOrder();
@@ -1342,7 +1446,7 @@ var Scene = (_class2 = class _Scene {
1342
1446
  }
1343
1447
  renderer.clear();
1344
1448
  if (isMainRenderer) {
1345
- _optionalChain([this, 'access', _57 => _57.pointRenderer, 'optionalAccess', _58 => _58.begin, 'call', _59 => _59()]);
1449
+ _optionalChain([this, 'access', _61 => _61.pointRenderer, 'optionalAccess', _62 => _62.begin, 'call', _63 => _63()]);
1346
1450
  }
1347
1451
  const vw = this.width;
1348
1452
  const vh = this.height;
@@ -1470,15 +1574,15 @@ var Scene = (_class2 = class _Scene {
1470
1574
  if (isMainRenderer) this.reconcilePortals();
1471
1575
  renderer.flush();
1472
1576
  if (isMainRenderer) {
1473
- _optionalChain([this, 'access', _60 => _60.pointRenderer, 'optionalAccess', _61 => _61.flush, 'call', _62 => _62()]);
1577
+ _optionalChain([this, 'access', _64 => _64.pointRenderer, 'optionalAccess', _65 => _65.flush, 'call', _66 => _66()]);
1474
1578
  }
1475
- _optionalChain([renderer, 'access', _63 => _63.present, 'optionalCall', _64 => _64()]);
1579
+ _optionalChain([renderer, 'access', _67 => _67.present, 'optionalCall', _68 => _68()]);
1476
1580
  }
1477
1581
  /**
1478
1582
  * Export the current scene state to a lightweight, flat SVG XML string.
1479
1583
  */
1480
1584
  toSVG() {
1481
- const renderer = new (0, _chunkGADBN53Zjs.SVGRenderer)(this.width, this.height);
1585
+ const renderer = new (0, _chunkGTQRCY6Wjs.SVGRenderer)(this.width, this.height);
1482
1586
  this.render(renderer, 0, 0);
1483
1587
  return renderer.toXMLString();
1484
1588
  }
@@ -1491,7 +1595,7 @@ var Scene = (_class2 = class _Scene {
1491
1595
  if (typeof this.renderer.resize === "function") {
1492
1596
  this.renderer.resize(width, height);
1493
1597
  }
1494
- _optionalChain([this, 'access', _65 => _65.pointRenderer, 'optionalAccess', _66 => _66.resize, 'call', _67 => _67(width, height)]);
1598
+ _optionalChain([this, 'access', _69 => _69.pointRenderer, 'optionalAccess', _70 => _70.resize, 'call', _71 => _71(width, height)]);
1495
1599
  if (this.gpuCanvas) {
1496
1600
  this.gpuCanvas.width = width;
1497
1601
  this.gpuCanvas.height = height;
@@ -1535,7 +1639,7 @@ var Scene = (_class2 = class _Scene {
1535
1639
  });
1536
1640
  pass.end();
1537
1641
  this.device.queue.submit([encoder.finish()]);
1538
- } catch (e2) {
1642
+ } catch (e3) {
1539
1643
  }
1540
1644
  this.gpuHasContent = false;
1541
1645
  }
@@ -1675,20 +1779,20 @@ function defaultMeasurer() {
1675
1779
  if (sharedMeasurer === void 0) sharedMeasurer = _chunkBWLNEJJWjs.createCanvasMeasurer.call(void 0, "sans-serif");
1676
1780
  return sharedMeasurer;
1677
1781
  }
1678
- var TextEntity = (_class3 = class extends _chunk4BO5XBJLjs.Entity {
1782
+ var TextEntity = (_class3 = class extends _chunkDWMQYHNCjs.Entity {
1679
1783
 
1680
1784
 
1681
1785
 
1682
1786
 
1683
- __init49() {this.nodes = []}
1787
+ __init51() {this.nodes = []}
1684
1788
 
1685
- __init50() {this.fillStyle = "#94a3b8"}
1686
- __init51() {this.strokeStyle = null}
1687
- __init52() {this.hoveredFillStyle = "#ffffff"}
1688
- __init53() {this.lineWidth = 1}
1689
- __init54() {this.isHovered = false}
1789
+ __init52() {this.fillStyle = "#94a3b8"}
1790
+ __init53() {this.strokeStyle = null}
1791
+ __init54() {this.hoveredFillStyle = "#ffffff"}
1792
+ __init55() {this.lineWidth = 1}
1793
+ __init56() {this.isHovered = false}
1690
1794
  constructor(text, atlas, maxWidth, fontSize = 24) {
1691
- super();_class3.prototype.__init49.call(this);_class3.prototype.__init50.call(this);_class3.prototype.__init51.call(this);_class3.prototype.__init52.call(this);_class3.prototype.__init53.call(this);_class3.prototype.__init54.call(this);;
1795
+ super();_class3.prototype.__init51.call(this);_class3.prototype.__init52.call(this);_class3.prototype.__init53.call(this);_class3.prototype.__init54.call(this);_class3.prototype.__init55.call(this);_class3.prototype.__init56.call(this);;
1692
1796
  this.text = text;
1693
1797
  this.atlas = atlas;
1694
1798
  this.fontSize = fontSize;
@@ -1699,6 +1803,14 @@ var TextEntity = (_class3 = class extends _chunk4BO5XBJLjs.Entity {
1699
1803
  this.on("hover", () => this.isHovered = true);
1700
1804
  this.on("pointerleave", () => this.isHovered = false);
1701
1805
  }
1806
+ /**
1807
+ * Mirror the rendered text into the DOM content layer: find-in-page, screen
1808
+ * readers, crawlers, and translation see the same string the canvas draws.
1809
+ */
1810
+ getContentProjection() {
1811
+ if (!this.text) return null;
1812
+ return { text: this.text, font: `${this.fontSize}px sans-serif` };
1813
+ }
1702
1814
  /**
1703
1815
  * Replace the text content. Runs the **cold** measurement pass (re-segment +
1704
1816
  * re-measure) since the glyphs changed, then re-lays out.
@@ -1773,17 +1885,17 @@ var TextEntity = (_class3 = class extends _chunk4BO5XBJLjs.Entity {
1773
1885
  }, _class3);
1774
1886
 
1775
1887
  // src/components/GridTextEntity.ts
1776
- var GridTextEntity = (_class4 = class extends _chunk4BO5XBJLjs.Entity {
1888
+ var GridTextEntity = (_class4 = class extends _chunkDWMQYHNCjs.Entity {
1777
1889
 
1778
- __init55() {this.fillStyle = "#ffffff"}
1779
- __init56() {this.grid = []}
1890
+ __init57() {this.fillStyle = "#ffffff"}
1891
+ __init58() {this.grid = []}
1780
1892
  // Array of rows
1781
- __init57() {this.cols = 0}
1782
- __init58() {this.rows = 0}
1893
+ __init59() {this.cols = 0}
1894
+ __init60() {this.rows = 0}
1783
1895
 
1784
1896
 
1785
1897
  constructor(_atlas, fontSize = 10) {
1786
- super();_class4.prototype.__init55.call(this);_class4.prototype.__init56.call(this);_class4.prototype.__init57.call(this);_class4.prototype.__init58.call(this);;
1898
+ super();_class4.prototype.__init57.call(this);_class4.prototype.__init58.call(this);_class4.prototype.__init59.call(this);_class4.prototype.__init60.call(this);;
1787
1899
  this.fontSize = fontSize;
1788
1900
  this.charWidth = fontSize * 1;
1789
1901
  this.charHeight = fontSize * 1.1;
@@ -1792,7 +1904,7 @@ var GridTextEntity = (_class4 = class extends _chunk4BO5XBJLjs.Entity {
1792
1904
  updateGrid(ascii) {
1793
1905
  this.grid = ascii;
1794
1906
  this.rows = ascii.length;
1795
- this.cols = _optionalChain([ascii, 'access', _68 => _68[0], 'optionalAccess', _69 => _69.length]) || 0;
1907
+ this.cols = _optionalChain([ascii, 'access', _72 => _72[0], 'optionalAccess', _73 => _73.length]) || 0;
1796
1908
  }
1797
1909
  isPointInside(_globalX, _globalY) {
1798
1910
  return false;
@@ -1867,7 +1979,7 @@ function distSqToSegment(px, py, x1, y1, x2, y2) {
1867
1979
  const ey = py - cy;
1868
1980
  return ex * ex + ey * ey;
1869
1981
  }
1870
- var SplineEntity = (_class5 = class extends _chunk4BO5XBJLjs.Entity {
1982
+ var SplineEntity = (_class5 = class extends _chunkDWMQYHNCjs.Entity {
1871
1983
 
1872
1984
 
1873
1985
 
@@ -1875,23 +1987,23 @@ var SplineEntity = (_class5 = class extends _chunk4BO5XBJLjs.Entity {
1875
1987
 
1876
1988
 
1877
1989
 
1878
- __init59() {this.offscreen = null}
1879
- __init60() {this.baked = false}
1990
+ __init61() {this.offscreen = null}
1991
+ __init62() {this.baked = false}
1880
1992
  /** Logical (CSS-pixel) size of the baked bitmap — the blit destination size. */
1881
- __init61() {this.bakedWidth = 0}
1882
- __init62() {this.bakedHeight = 0}
1993
+ __init63() {this.bakedWidth = 0}
1994
+ __init64() {this.bakedHeight = 0}
1883
1995
  /** Gradient strokes can't be baked to a solid-color bitmap; they render per-frame. */
1884
1996
 
1885
1997
  /** Lazily-flattened polylines (one Float32Array of [x,y,...] per segment) for hit-testing. */
1886
- __init63() {this.polylines = null}
1998
+ __init65() {this.polylines = null}
1887
1999
  /**
1888
2000
  * When `true`, the renderer draws a rounded-rect outline of the entity's
1889
2001
  * local bounds after painting the curves. Useful for drag feedback and
1890
2002
  * debugging hit areas. Defaults to `false`.
1891
2003
  */
1892
- __init64() {this.showBounds = false}
2004
+ __init66() {this.showBounds = false}
1893
2005
  constructor(doc, opts = {}) {
1894
- super();_class5.prototype.__init59.call(this);_class5.prototype.__init60.call(this);_class5.prototype.__init61.call(this);_class5.prototype.__init62.call(this);_class5.prototype.__init63.call(this);_class5.prototype.__init64.call(this);;
2006
+ super();_class5.prototype.__init61.call(this);_class5.prototype.__init62.call(this);_class5.prototype.__init63.call(this);_class5.prototype.__init64.call(this);_class5.prototype.__init65.call(this);_class5.prototype.__init66.call(this);;
1895
2007
  this.doc = doc;
1896
2008
  this.lineWidth = _nullishCoalesce(opts.lineWidth, () => ( 2));
1897
2009
  this.cache = _nullishCoalesce(opts.cache, () => ( true));
@@ -1902,7 +2014,7 @@ var SplineEntity = (_class5 = class extends _chunk4BO5XBJLjs.Entity {
1902
2014
  this.width = this.bounds.width;
1903
2015
  this.height = this.bounds.height;
1904
2016
  const isGradient = (c) => c !== null && !Array.isArray(c);
1905
- this.containsGradient = (_nullishCoalesce(_optionalChain([this, 'access', _70 => _70.doc, 'access', _71 => _71.equations, 'optionalAccess', _72 => _72.some, 'call', _73 => _73((eq) => isGradient(eq.color_rgb))]), () => ( false))) || (_nullishCoalesce(_optionalChain([this, 'access', _74 => _74.doc, 'access', _75 => _75.paths, 'optionalAccess', _76 => _76.some, 'call', _77 => _77((p) => isGradient(p.color_rgb))]), () => ( false)));
2017
+ this.containsGradient = (_nullishCoalesce(_optionalChain([this, 'access', _74 => _74.doc, 'access', _75 => _75.equations, 'optionalAccess', _76 => _76.some, 'call', _77 => _77((eq) => isGradient(eq.color_rgb))]), () => ( false))) || (_nullishCoalesce(_optionalChain([this, 'access', _78 => _78.doc, 'access', _79 => _79.paths, 'optionalAccess', _80 => _80.some, 'call', _81 => _81((p) => isGradient(p.color_rgb))]), () => ( false)));
1906
2018
  this.interactive = true;
1907
2019
  }
1908
2020
  computeBounds() {
@@ -2144,9 +2256,9 @@ async function loadSpline(url) {
2144
2256
  // src/math/SpatialHashGrid.ts
2145
2257
  var SpatialHashGrid = (_class6 = class {
2146
2258
 
2147
- __init65() {this.grid = /* @__PURE__ */ new Map()}
2148
- __init66() {this.entityCells = /* @__PURE__ */ new Map()}
2149
- constructor(cellSize = 64) {;_class6.prototype.__init65.call(this);_class6.prototype.__init66.call(this);
2259
+ __init67() {this.grid = /* @__PURE__ */ new Map()}
2260
+ __init68() {this.entityCells = /* @__PURE__ */ new Map()}
2261
+ constructor(cellSize = 64) {;_class6.prototype.__init67.call(this);_class6.prototype.__init68.call(this);
2150
2262
  this.cellSize = cellSize;
2151
2263
  }
2152
2264
  hash(cx, cy) {
@@ -2200,7 +2312,7 @@ var SpatialHashGrid = (_class6 = class {
2200
2312
  const keys = this.entityCells.get(id);
2201
2313
  if (!keys) return;
2202
2314
  for (const key of keys) {
2203
- _optionalChain([this, 'access', _78 => _78.grid, 'access', _79 => _79.get, 'call', _80 => _80(key), 'optionalAccess', _81 => _81.delete, 'call', _82 => _82(id)]);
2315
+ _optionalChain([this, 'access', _82 => _82.grid, 'access', _83 => _83.get, 'call', _84 => _84(key), 'optionalAccess', _85 => _85.delete, 'call', _86 => _86(id)]);
2204
2316
  }
2205
2317
  this.entityCells.delete(id);
2206
2318
  }
@@ -2236,20 +2348,20 @@ var SpatialHashGrid = (_class6 = class {
2236
2348
  }, _class6);
2237
2349
 
2238
2350
  // src/tree/DOMPortalEntity.ts
2239
- var DOMPortalEntity = (_class7 = class extends _chunk4BO5XBJLjs.Entity {
2351
+ var DOMPortalEntity = (_class7 = class extends _chunkDWMQYHNCjs.Entity {
2240
2352
 
2241
- __init67() {this.isDOMPortal = true}
2242
- __init68() {this.domListeners = []}
2243
- __init69() {this.resizeObserver = null}
2244
- __init70() {this.cachedWidth = 100}
2245
- __init71() {this.cachedHeight = 100}
2246
- __init72() {this.lastWidth = ""}
2247
- __init73() {this.lastHeight = ""}
2248
- __init74() {this.lastTransform = ""}
2249
- __init75() {this.lastZIndex = ""}
2250
- __init76() {this.lastOpacity = ""}
2353
+ __init69() {this.isDOMPortal = true}
2354
+ __init70() {this.domListeners = []}
2355
+ __init71() {this.resizeObserver = null}
2356
+ __init72() {this.cachedWidth = 100}
2357
+ __init73() {this.cachedHeight = 100}
2358
+ __init74() {this.lastWidth = ""}
2359
+ __init75() {this.lastHeight = ""}
2360
+ __init76() {this.lastTransform = ""}
2361
+ __init77() {this.lastZIndex = ""}
2362
+ __init78() {this.lastOpacity = ""}
2251
2363
  constructor(domElement, width, height, id) {
2252
- super(id);_class7.prototype.__init67.call(this);_class7.prototype.__init68.call(this);_class7.prototype.__init69.call(this);_class7.prototype.__init70.call(this);_class7.prototype.__init71.call(this);_class7.prototype.__init72.call(this);_class7.prototype.__init73.call(this);_class7.prototype.__init74.call(this);_class7.prototype.__init75.call(this);_class7.prototype.__init76.call(this);;
2364
+ super(id);_class7.prototype.__init69.call(this);_class7.prototype.__init70.call(this);_class7.prototype.__init71.call(this);_class7.prototype.__init72.call(this);_class7.prototype.__init73.call(this);_class7.prototype.__init74.call(this);_class7.prototype.__init75.call(this);_class7.prototype.__init76.call(this);_class7.prototype.__init77.call(this);_class7.prototype.__init78.call(this);;
2253
2365
  this.domElement = domElement;
2254
2366
  this.width = _nullishCoalesce(width, () => ( 0));
2255
2367
  this.height = _nullishCoalesce(height, () => ( 0));
@@ -2271,7 +2383,7 @@ var DOMPortalEntity = (_class7 = class extends _chunk4BO5XBJLjs.Entity {
2271
2383
  const events = ["click", "pointerdown", "pointerup", "pointermove", "wheel"];
2272
2384
  for (const type of events) {
2273
2385
  const handler = (e) => {
2274
- this.dispatchEvent(new (0, _chunk4BO5XBJLjs.VectoJSEvent)(type, this, e));
2386
+ this.dispatchEvent(new (0, _chunkDWMQYHNCjs.VectoJSEvent)(type, this, e));
2275
2387
  };
2276
2388
  this.domElement.addEventListener(type, handler);
2277
2389
  this.domListeners.push({ type, handler, capture: false });
@@ -2282,7 +2394,7 @@ var DOMPortalEntity = (_class7 = class extends _chunk4BO5XBJLjs.Entity {
2282
2394
  ];
2283
2395
  for (const { native, vecto } of hoverEvents) {
2284
2396
  const handler = (e) => {
2285
- this.dispatchEvent(new (0, _chunk4BO5XBJLjs.VectoJSEvent)(vecto, this, e, false));
2397
+ this.dispatchEvent(new (0, _chunkDWMQYHNCjs.VectoJSEvent)(vecto, this, e, false));
2286
2398
  };
2287
2399
  this.domElement.addEventListener(native, handler);
2288
2400
  this.domListeners.push({ type: native, handler, capture: false });
@@ -2290,7 +2402,7 @@ var DOMPortalEntity = (_class7 = class extends _chunk4BO5XBJLjs.Entity {
2290
2402
  const focusEvents = ["focus", "blur"];
2291
2403
  for (const type of focusEvents) {
2292
2404
  const handler = (e) => {
2293
- this.dispatchEvent(new (0, _chunk4BO5XBJLjs.VectoJSEvent)(type, this, e, true));
2405
+ this.dispatchEvent(new (0, _chunkDWMQYHNCjs.VectoJSEvent)(type, this, e, true));
2294
2406
  };
2295
2407
  this.domElement.addEventListener(type, handler, true);
2296
2408
  this.domListeners.push({ type, handler, capture: true });
@@ -2329,8 +2441,8 @@ var DOMPortalEntity = (_class7 = class extends _chunk4BO5XBJLjs.Entity {
2329
2441
  }, _class7);
2330
2442
 
2331
2443
  // src/index.ts
2332
- Scene.registerWebGLPointRendererCreator(_chunkGADBN53Zjs.createWebGLPointRenderer);
2333
- Scene.registerWebGPUParticleSystemManager(_chunkGADBN53Zjs.WebGPUParticleSystemManager);
2444
+ Scene.registerWebGLPointRendererCreator(_chunkGTQRCY6Wjs.createWebGLPointRenderer);
2445
+ Scene.registerWebGPUParticleSystemManager(_chunkGTQRCY6Wjs.WebGPUParticleSystemManager);
2334
2446
 
2335
2447
 
2336
2448
 
@@ -2375,4 +2487,4 @@ Scene.registerWebGPUParticleSystemManager(_chunkGADBN53Zjs.WebGPUParticleSystemM
2375
2487
 
2376
2488
 
2377
2489
 
2378
- exports.ArabicShaper = _chunkCTZQOM5Zjs.ArabicShaper; exports.BidiResolver = _chunkCTZQOM5Zjs.BidiResolver; exports.CanvasRenderer = _chunkGADBN53Zjs.CanvasRenderer; exports.ComputeParticleEntity = ComputeParticleEntity; exports.DOMPortalEntity = DOMPortalEntity; exports.Easing = _chunk4BO5XBJLjs.Easing; exports.Entity = _chunk4BO5XBJLjs.Entity; exports.GridTextEntity = GridTextEntity; exports.LayoutEngine = _chunkBWLNEJJWjs.LayoutEngine; exports.LayoutResultBuffer = _chunkBWLNEJJWjs.LayoutResultBuffer; exports.LayoutWorkerManager = _chunkCTZQOM5Zjs.LayoutWorkerManager; exports.MSDFFont = _chunk4BO5XBJLjs.MSDFFont; exports.MSDFTextEntity = _chunk4BO5XBJLjs.MSDFTextEntity; exports.PARTICLE_OFFSET_LIFE = PARTICLE_OFFSET_LIFE; exports.PARTICLE_OFFSET_ORIGIN_X = PARTICLE_OFFSET_ORIGIN_X; exports.PARTICLE_OFFSET_ORIGIN_Y = PARTICLE_OFFSET_ORIGIN_Y; exports.PARTICLE_OFFSET_POSITION_X = PARTICLE_OFFSET_POSITION_X; exports.PARTICLE_OFFSET_POSITION_Y = PARTICLE_OFFSET_POSITION_Y; exports.PARTICLE_OFFSET_SIZE = PARTICLE_OFFSET_SIZE; exports.PARTICLE_OFFSET_VELOCITY_X = PARTICLE_OFFSET_VELOCITY_X; exports.PARTICLE_OFFSET_VELOCITY_Y = PARTICLE_OFFSET_VELOCITY_Y; exports.PARTICLE_STRIDE_FLOATS = PARTICLE_STRIDE_FLOATS; exports.REDUCED_MOTION_FPS = REDUCED_MOTION_FPS; exports.SVGEntity = _chunk4BO5XBJLjs.SVGEntity; exports.SVGRenderer = _chunkGADBN53Zjs.SVGRenderer; exports.Scene = Scene; exports.SpatialHashGrid = SpatialHashGrid; exports.SplineEntity = SplineEntity; exports.SpringDriver = _chunk4BO5XBJLjs.SpringDriver; exports.SpringPhysics = _chunk4BO5XBJLjs.SpringPhysics; exports.TextEntity = TextEntity; exports.TweenDriver = _chunk4BO5XBJLjs.TweenDriver; exports.VectoJSEvent = _chunk4BO5XBJLjs.VectoJSEvent; exports.WebGPUParticleSystemManager = _chunkGADBN53Zjs.WebGPUParticleSystemManager; exports.computeLineSegments = _chunkBWLNEJJWjs.computeLineSegments; exports.createCanvasMeasurer = _chunkBWLNEJJWjs.createCanvasMeasurer; exports.createWebGLPointRenderer = _chunkGADBN53Zjs.createWebGLPointRenderer; exports.isSafeUrl = _chunkGADBN53Zjs.isSafeUrl; exports.isTweenConfig = _chunk4BO5XBJLjs.isTweenConfig; exports.loadSpline = loadSpline; exports.parseColorToRGBA = _chunkGADBN53Zjs.parseColorToRGBA; exports.polySegmentToBezier = polySegmentToBezier; exports.sanitizeUrl = _chunkGADBN53Zjs.sanitizeUrl;
2490
+ exports.ArabicShaper = _chunkCTZQOM5Zjs.ArabicShaper; exports.BidiResolver = _chunkCTZQOM5Zjs.BidiResolver; exports.CanvasRenderer = _chunkGTQRCY6Wjs.CanvasRenderer; exports.ComputeParticleEntity = ComputeParticleEntity; exports.DOMPortalEntity = DOMPortalEntity; exports.Easing = _chunkDWMQYHNCjs.Easing; exports.Entity = _chunkDWMQYHNCjs.Entity; exports.GridTextEntity = GridTextEntity; exports.LayoutEngine = _chunkBWLNEJJWjs.LayoutEngine; exports.LayoutResultBuffer = _chunkBWLNEJJWjs.LayoutResultBuffer; exports.LayoutWorkerManager = _chunkCTZQOM5Zjs.LayoutWorkerManager; exports.MSDFFont = _chunkDWMQYHNCjs.MSDFFont; exports.MSDFTextEntity = _chunkDWMQYHNCjs.MSDFTextEntity; exports.PARTICLE_OFFSET_LIFE = PARTICLE_OFFSET_LIFE; exports.PARTICLE_OFFSET_ORIGIN_X = PARTICLE_OFFSET_ORIGIN_X; exports.PARTICLE_OFFSET_ORIGIN_Y = PARTICLE_OFFSET_ORIGIN_Y; exports.PARTICLE_OFFSET_POSITION_X = PARTICLE_OFFSET_POSITION_X; exports.PARTICLE_OFFSET_POSITION_Y = PARTICLE_OFFSET_POSITION_Y; exports.PARTICLE_OFFSET_SIZE = PARTICLE_OFFSET_SIZE; exports.PARTICLE_OFFSET_VELOCITY_X = PARTICLE_OFFSET_VELOCITY_X; exports.PARTICLE_OFFSET_VELOCITY_Y = PARTICLE_OFFSET_VELOCITY_Y; exports.PARTICLE_STRIDE_FLOATS = PARTICLE_STRIDE_FLOATS; exports.REDUCED_MOTION_FPS = REDUCED_MOTION_FPS; exports.SVGEntity = _chunkDWMQYHNCjs.SVGEntity; exports.SVGRenderer = _chunkGTQRCY6Wjs.SVGRenderer; exports.Scene = Scene; exports.SpatialHashGrid = SpatialHashGrid; exports.SplineEntity = SplineEntity; exports.SpringDriver = _chunkDWMQYHNCjs.SpringDriver; exports.SpringPhysics = _chunkDWMQYHNCjs.SpringPhysics; exports.TextEntity = TextEntity; exports.TweenDriver = _chunkDWMQYHNCjs.TweenDriver; exports.VectoJSEvent = _chunkDWMQYHNCjs.VectoJSEvent; exports.WebGPUParticleSystemManager = _chunkGTQRCY6Wjs.WebGPUParticleSystemManager; exports.computeLineSegments = _chunkBWLNEJJWjs.computeLineSegments; exports.createCanvasMeasurer = _chunkBWLNEJJWjs.createCanvasMeasurer; exports.createWebGLPointRenderer = _chunkGTQRCY6Wjs.createWebGLPointRenderer; exports.isSafeUrl = _chunkGTQRCY6Wjs.isSafeUrl; exports.isTweenConfig = _chunkDWMQYHNCjs.isTweenConfig; exports.loadSpline = loadSpline; exports.parseColorToRGBA = _chunkGTQRCY6Wjs.parseColorToRGBA; exports.polySegmentToBezier = polySegmentToBezier; exports.sanitizeUrl = _chunkGTQRCY6Wjs.sanitizeUrl;
package/dist/index.mjs CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  isSafeUrl,
13
13
  parseColorToRGBA,
14
14
  sanitizeUrl
15
- } from "./chunk-PK5EYWIT.mjs";
15
+ } from "./chunk-HVJHXIKW.mjs";
16
16
  import {
17
17
  Easing,
18
18
  Entity,
@@ -24,7 +24,7 @@ import {
24
24
  TweenDriver,
25
25
  VectoJSEvent,
26
26
  isTweenConfig
27
- } from "./chunk-SL654OMF.mjs";
27
+ } from "./chunk-YVN3PJRC.mjs";
28
28
  import {
29
29
  ArabicShaper,
30
30
  BidiResolver,
@@ -296,6 +296,11 @@ var ComputeParticleEntity = class extends Entity {
296
296
 
297
297
  // src/tree/Scene.ts
298
298
  var REDUCED_MOTION_FPS = 30;
299
+ function parseInlinePx(value) {
300
+ if (!value || !value.endsWith("px")) return null;
301
+ const n = parseFloat(value);
302
+ return Number.isFinite(n) && n > 0 ? n : null;
303
+ }
299
304
  var Scene = class _Scene {
300
305
  static webglCreator = null;
301
306
  static webgpuManagerClass = null;
@@ -350,6 +355,9 @@ var Scene = class _Scene {
350
355
  // server-side (e.g. headless layout / vector export) without jsdom.
351
356
  a11yRoot;
352
357
  a11yElements = /* @__PURE__ */ new Map();
358
+ /** DOM nodes mirroring static text content, keyed by entity id. */
359
+ contentElements = /* @__PURE__ */ new Map();
360
+ contentProjectionEnabled = true;
353
361
  resizeHandler;
354
362
  focusedA11yElement = null;
355
363
  caretBlinkTimer = null;
@@ -398,8 +406,10 @@ var Scene = class _Scene {
398
406
  this.debugA11y = options.debugA11y ?? false;
399
407
  this.disableWindowResize = options.disableWindowResize ?? false;
400
408
  if (this.disableWindowResize) {
401
- this.width = canvas.width || canvas.clientWidth || 0;
402
- this.height = canvas.height || canvas.clientHeight || 0;
409
+ const styleWidth = parseInlinePx(canvas.style?.width);
410
+ const styleHeight = parseInlinePx(canvas.style?.height);
411
+ this.width = styleWidth ?? (canvas.width || canvas.clientWidth || 0);
412
+ this.height = styleHeight ?? (canvas.height || canvas.clientHeight || 0);
403
413
  } else {
404
414
  this.width = typeof window !== "undefined" ? window.innerWidth : canvas.clientWidth || canvas.width || 800;
405
415
  this.height = typeof window !== "undefined" ? window.innerHeight : canvas.clientHeight || canvas.height || 600;
@@ -411,6 +421,7 @@ var Scene = class _Scene {
411
421
  this.autoThrottle = options.autoThrottle ?? true;
412
422
  this.particleBackend = options.particleBackend ?? "auto";
413
423
  this.a11ySyncInterval = options.a11ySyncInterval ?? 0;
424
+ this.contentProjectionEnabled = options.contentProjection ?? true;
414
425
  this.reducedMotionQuery = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
415
426
  this.root = new class RootEntity extends Entity {
416
427
  isPointInside() {
@@ -564,6 +575,11 @@ var Scene = class _Scene {
564
575
  * @param entity - The subtree whose shadow nodes should be removed.
565
576
  */
566
577
  detachA11y(entity) {
578
+ const contentEl = this.contentElements.get(entity.id);
579
+ if (contentEl) {
580
+ contentEl.remove();
581
+ this.contentElements.delete(entity.id);
582
+ }
567
583
  this.removeA11yRecursively(entity);
568
584
  }
569
585
  /**
@@ -610,6 +626,8 @@ var Scene = class _Scene {
610
626
  this.a11yRoot?.remove();
611
627
  this.portalRoot?.remove();
612
628
  this.a11yElements.clear();
629
+ for (const el of this.contentElements.values()) el.remove();
630
+ this.contentElements.clear();
613
631
  this.pointRenderer?.destroy();
614
632
  this.renderer.dispose?.();
615
633
  this.glCanvas?.remove();
@@ -972,11 +990,96 @@ var Scene = class _Scene {
972
990
  el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
973
991
  }
974
992
  }
993
+ this.syncContentProjection(node);
975
994
  for (const child of node.children) this.syncA11y(child);
976
995
  if (node === this.root) {
977
996
  for (const overlay of this.overlayRoot.children) this.syncA11y(overlay);
978
997
  }
979
998
  }
999
+ /**
1000
+ * Mirror one entity's static text ({@link Entity.getContentProjection}) as a
1001
+ * transparent DOM node positioned over the drawn glyphs. Runs on the a11y
1002
+ * sync cadence; all writes are dirty-checked. Off-viewport projections are
1003
+ * hidden (`display: none`) so text-heavy scenes only materialize what is
1004
+ * visible to the browser's text machinery anyway.
1005
+ */
1006
+ syncContentProjection(node) {
1007
+ if (!this.contentProjectionEnabled || !this.a11yRoot) return;
1008
+ const projection = node.getContentProjection();
1009
+ let el = this.contentElements.get(node.id);
1010
+ if (!projection || !projection.text) {
1011
+ if (el) {
1012
+ el.remove();
1013
+ this.contentElements.delete(node.id);
1014
+ }
1015
+ return;
1016
+ }
1017
+ if (!el) {
1018
+ el = document.createElement("div");
1019
+ el.setAttribute("data-vecto-content", node.id);
1020
+ const s = el.style;
1021
+ s.position = "absolute";
1022
+ s.transformOrigin = "0 0";
1023
+ s.margin = "0";
1024
+ s.padding = "0";
1025
+ s.color = "transparent";
1026
+ s.whiteSpace = "pre-wrap";
1027
+ s.overflow = "hidden";
1028
+ s.zIndex = "0";
1029
+ el.addEventListener(
1030
+ "wheel",
1031
+ (e2) => {
1032
+ node.dispatchEvent(new VectoJSEvent("wheel", node, e2));
1033
+ },
1034
+ { passive: false }
1035
+ );
1036
+ this.a11yRoot.appendChild(el);
1037
+ this.contentElements.set(node.id, el);
1038
+ }
1039
+ if (el.textContent !== projection.text) el.textContent = projection.text;
1040
+ const font = projection.font ?? "";
1041
+ if (el.style.font !== font) el.style.font = font;
1042
+ const lineHeight = projection.lineHeight !== void 0 ? `${projection.lineHeight}px` : "";
1043
+ if (el.style.lineHeight !== lineHeight) el.style.lineHeight = lineHeight;
1044
+ const hidden = node.interactive ? "true" : null;
1045
+ if (el.getAttribute("aria-hidden") !== hidden) {
1046
+ if (hidden) el.setAttribute("aria-hidden", hidden);
1047
+ else el.removeAttribute("aria-hidden");
1048
+ }
1049
+ const selectable = projection.selectable === true;
1050
+ const pointerEvents = selectable ? "auto" : "none";
1051
+ if (el.style.pointerEvents !== pointerEvents) {
1052
+ el.style.pointerEvents = pointerEvents;
1053
+ el.style.userSelect = selectable ? "text" : "none";
1054
+ el.style.cursor = selectable ? "text" : "";
1055
+ }
1056
+ const { a, b, c, d, e, f } = node.getWorldTransform();
1057
+ el.style.left = `${e}px`;
1058
+ el.style.top = `${f}px`;
1059
+ if (node.width > 0) el.style.width = `${node.width}px`;
1060
+ if (node.height > 0) el.style.height = `${node.height}px`;
1061
+ el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
1062
+ let visible = true;
1063
+ if (node.width > 0 && node.height > 0) {
1064
+ let minX = Infinity;
1065
+ let minY = Infinity;
1066
+ let maxX = -Infinity;
1067
+ let maxY = -Infinity;
1068
+ for (let i = 0; i < 4; i++) {
1069
+ const lx = i & 1 ? node.width : 0;
1070
+ const ly = i & 2 ? node.height : 0;
1071
+ const wx = a * lx + c * ly + e;
1072
+ const wy = b * lx + d * ly + f;
1073
+ if (wx < minX) minX = wx;
1074
+ if (wx > maxX) maxX = wx;
1075
+ if (wy < minY) minY = wy;
1076
+ if (wy > maxY) maxY = wy;
1077
+ }
1078
+ visible = maxX >= 0 && minX <= this.width && maxY >= 0 && minY <= this.height;
1079
+ }
1080
+ const display = visible ? "" : "none";
1081
+ if (el.style.display !== display) el.style.display = display;
1082
+ }
980
1083
  enforceA11yDomOrder() {
981
1084
  if (!this.a11yRoot) return;
982
1085
  this.fullViewportElements.length = 0;
@@ -1191,10 +1294,11 @@ var Scene = class _Scene {
1191
1294
  this.render(this.renderer, dt, time);
1192
1295
  const hasActiveAnimation = this.hasAnyPendingAnimation(this.root) || this.hasAnyPendingAnimation(this.overlayRoot);
1193
1296
  const hasInteractive = this.hasAnyInteractive(this.root) || this.hasAnyInteractive(this.overlayRoot);
1297
+ const wantsContentSync = this.contentProjectionEnabled;
1194
1298
  const shouldSyncInterval = this.a11ySyncInterval <= 0 || time - this.lastA11ySync >= this.a11ySyncInterval;
1195
- if ((hasInteractive || this.a11yElements.size > 0) && (shouldSyncInterval || this.a11yPendingSyncAfterAnimation)) {
1299
+ if ((hasInteractive || this.a11yElements.size > 0 || wantsContentSync) && (shouldSyncInterval || this.a11yPendingSyncAfterAnimation)) {
1196
1300
  this.lastA11ySync = time;
1197
- if (hasInteractive) {
1301
+ if (hasInteractive || wantsContentSync) {
1198
1302
  this.syncA11y(this.root);
1199
1303
  }
1200
1304
  this.enforceA11yDomOrder();
@@ -1699,6 +1803,14 @@ var TextEntity = class extends Entity {
1699
1803
  this.on("hover", () => this.isHovered = true);
1700
1804
  this.on("pointerleave", () => this.isHovered = false);
1701
1805
  }
1806
+ /**
1807
+ * Mirror the rendered text into the DOM content layer: find-in-page, screen
1808
+ * readers, crawlers, and translation see the same string the canvas draws.
1809
+ */
1810
+ getContentProjection() {
1811
+ if (!this.text) return null;
1812
+ return { text: this.text, font: `${this.fontSize}px sans-serif` };
1813
+ }
1702
1814
  /**
1703
1815
  * Replace the text content. Runs the **cold** measurement pass (re-segment +
1704
1816
  * re-measure) since the glyphs changed, then re-lays out.
package/dist/renderer.js CHANGED
@@ -4,11 +4,11 @@
4
4
 
5
5
 
6
6
 
7
- var _chunkGADBN53Zjs = require('./chunk-GADBN53Z.js');
7
+ var _chunkGTQRCY6Wjs = require('./chunk-GTQRCY6W.js');
8
8
 
9
9
 
10
10
 
11
11
 
12
12
 
13
13
 
14
- exports.CanvasRenderer = _chunkGADBN53Zjs.CanvasRenderer; exports.SVGRenderer = _chunkGADBN53Zjs.SVGRenderer; exports.WebGPUParticleSystemManager = _chunkGADBN53Zjs.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkGADBN53Zjs.createWebGLPointRenderer; exports.parseColorToRGBA = _chunkGADBN53Zjs.parseColorToRGBA;
14
+ exports.CanvasRenderer = _chunkGTQRCY6Wjs.CanvasRenderer; exports.SVGRenderer = _chunkGTQRCY6Wjs.SVGRenderer; exports.WebGPUParticleSystemManager = _chunkGTQRCY6Wjs.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkGTQRCY6Wjs.createWebGLPointRenderer; exports.parseColorToRGBA = _chunkGTQRCY6Wjs.parseColorToRGBA;
package/dist/renderer.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  WebGPUParticleSystemManager,
5
5
  createWebGLPointRenderer,
6
6
  parseColorToRGBA
7
- } from "./chunk-PK5EYWIT.mjs";
7
+ } from "./chunk-HVJHXIKW.mjs";
8
8
  export {
9
9
  CanvasRenderer,
10
10
  SVGRenderer,
@@ -1,4 +1,4 @@
1
- import { Entity } from '../tree/Entity';
1
+ import { Entity, type ContentProjection } from '../tree/Entity';
2
2
  import { MSDFFont } from './MSDFFont';
3
3
  export interface MSDFTextEntityOptions {
4
4
  font: MSDFFont;
@@ -33,6 +33,11 @@ export declare class MSDFTextEntity extends Entity {
33
33
  setMaxWidth(maxWidth: number): void;
34
34
  setText(text: string): void;
35
35
  private queueLayout;
36
+ /**
37
+ * Mirror the rendered text into the DOM content layer: find-in-page, screen
38
+ * readers, crawlers, and translation see the same string the canvas draws.
39
+ */
40
+ getContentProjection(): ContentProjection | null;
36
41
  isPointInside(globalX: number, globalY: number): boolean;
37
42
  render(renderer: any): void;
38
43
  destroy(): void;
package/dist/text.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
 
4
4
 
5
- var _chunk4BO5XBJLjs = require('./chunk-4BO5XBJL.js');
5
+ var _chunkDWMQYHNCjs = require('./chunk-DWMQYHNC.js');
6
6
 
7
7
 
8
8
 
@@ -13,4 +13,4 @@ var _chunkCTZQOM5Zjs = require('./chunk-CTZQOM5Z.js');
13
13
 
14
14
 
15
15
 
16
- exports.ArabicShaper = _chunkCTZQOM5Zjs.ArabicShaper; exports.BidiResolver = _chunkCTZQOM5Zjs.BidiResolver; exports.MSDFFont = _chunk4BO5XBJLjs.MSDFFont; exports.MSDFTextEntity = _chunk4BO5XBJLjs.MSDFTextEntity; exports.SVGEntity = _chunk4BO5XBJLjs.SVGEntity;
16
+ exports.ArabicShaper = _chunkCTZQOM5Zjs.ArabicShaper; exports.BidiResolver = _chunkCTZQOM5Zjs.BidiResolver; exports.MSDFFont = _chunkDWMQYHNCjs.MSDFFont; exports.MSDFTextEntity = _chunkDWMQYHNCjs.MSDFTextEntity; exports.SVGEntity = _chunkDWMQYHNCjs.SVGEntity;
package/dist/text.mjs CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  MSDFFont,
3
3
  MSDFTextEntity,
4
4
  SVGEntity
5
- } from "./chunk-SL654OMF.mjs";
5
+ } from "./chunk-YVN3PJRC.mjs";
6
6
  import {
7
7
  ArabicShaper,
8
8
  BidiResolver
@@ -52,6 +52,26 @@ export interface BatchRect {
52
52
  /** CSS fill color. */
53
53
  color: string;
54
54
  }
55
+ /**
56
+ * Static text an {@link Entity} exposes for DOM content projection, returned
57
+ * from {@link Entity.getContentProjection}. The Scene mirrors it as a
58
+ * transparent, position-synced DOM node so browser-native text machinery —
59
+ * find-in-page, screen readers, SEO crawlers, translation, `#:~:text=`
60
+ * fragments — operates on canvas-rendered text.
61
+ */
62
+ export interface ContentProjection {
63
+ /** The plain text content as rendered (line breaks as `\n`). */
64
+ text: string;
65
+ /** CSS font shorthand matching the drawn glyphs, e.g. `'24px sans-serif'`. */
66
+ font?: string;
67
+ /** Line height in px, when it differs from the font's default. */
68
+ lineHeight?: number;
69
+ /**
70
+ * Allow native mouse selection on the projected text. Off by default so the
71
+ * projection never intercepts pointer input meant for the canvas.
72
+ */
73
+ selectable?: boolean;
74
+ }
55
75
  /**
56
76
  * Semantic attributes an {@link Entity} can project into the accessibility /
57
77
  * automation shadow layer maintained by {@link Scene}.
@@ -453,6 +473,17 @@ export declare abstract class Entity {
453
473
  * @returns The rectangle to batch, or `null` for the normal render path.
454
474
  */
455
475
  getBatchRect(): BatchRect | null;
476
+ /**
477
+ * Opt into DOM content projection for entities that render static text.
478
+ * The {@link Scene} mirrors the returned text as a transparent DOM node
479
+ * positioned over the drawn glyphs, making canvas text findable (Ctrl+F),
480
+ * readable by screen readers and crawlers, translatable, and — when
481
+ * `selectable` is set — natively selectable. Returns `null` by default.
482
+ * Read on the a11y sync cadence, so text changes propagate automatically.
483
+ *
484
+ * @returns The projection descriptor, or `null` to project nothing.
485
+ */
486
+ getContentProjection(): ContentProjection | null;
456
487
  /**
457
488
  * Whether this entity still has a queued/running tween animation, or an
458
489
  * active {@link setTransition}/{@link animateTo}/{@link springTo} property
@@ -75,6 +75,14 @@ export interface SceneOptions {
75
75
  * and not marked dirty) to save power/CPU. Default is `true`.
76
76
  */
77
77
  autoThrottle?: boolean;
78
+ /**
79
+ * Mirror static text from entities implementing
80
+ * {@link Entity.getContentProjection} as transparent, position-synced DOM
81
+ * nodes, so find-in-page, screen readers, crawlers, and translation work on
82
+ * canvas-rendered text. Default is `true`; disable for purely decorative
83
+ * scenes to skip the sync walk.
84
+ */
85
+ contentProjection?: boolean;
78
86
  }
79
87
  /** Frame-rate the loop is capped to when the OS requests reduced motion. */
80
88
  export declare const REDUCED_MOTION_FPS = 30;
@@ -147,6 +155,9 @@ export declare class Scene {
147
155
  private a11yPendingSyncAfterAnimation;
148
156
  private a11yRoot;
149
157
  private a11yElements;
158
+ /** DOM nodes mirroring static text content, keyed by entity id. */
159
+ private contentElements;
160
+ private contentProjectionEnabled;
150
161
  private resizeHandler;
151
162
  private focusedA11yElement;
152
163
  private caretBlinkTimer;
@@ -269,6 +280,14 @@ export declare class Scene {
269
280
  /** True when any node in the subtree is interactive (drives a11y sync). */
270
281
  private hasAnyInteractive;
271
282
  private syncA11y;
283
+ /**
284
+ * Mirror one entity's static text ({@link Entity.getContentProjection}) as a
285
+ * transparent DOM node positioned over the drawn glyphs. Runs on the a11y
286
+ * sync cadence; all writes are dirty-checked. Off-viewport projections are
287
+ * hidden (`display: none`) so text-heavy scenes only materialize what is
288
+ * visible to the browser's text machinery anyway.
289
+ */
290
+ private syncContentProjection;
272
291
  private enforceA11yDomOrder;
273
292
  /** Keep DOM/WebGL overlay layers aligned with the canvas's CSS box. */
274
293
  private syncOverlayGeometry;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectojs/core",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -53,7 +53,8 @@
53
53
  ],
54
54
  "scripts": {
55
55
  "build": "node scripts/build-worker.js && tsup && tsc -p tsconfig.build.json",
56
- "test": "vitest run"
56
+ "test": "vitest run",
57
+ "test:e2e": "bun e2e/hidpi.e2e.ts"
57
58
  },
58
59
  "dependencies": {},
59
60
  "devDependencies": {
@@ -61,6 +62,7 @@
61
62
  "esbuild": "^0.21.5",
62
63
  "jsdom": "^29.1.1",
63
64
  "tsup": "^8.3.5",
64
- "vitest": "^4.1.9"
65
+ "vitest": "^4.1.9",
66
+ "puppeteer-core": "^22.12.1"
65
67
  }
66
68
  }