@vectojs/core 0.2.6 → 0.2.8

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.
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  LayoutResultBuffer,
4
4
  computeLineSegments,
5
5
  createCanvasMeasurer
6
- } from "./chunk-SSOP3F5F.mjs";
6
+ } from "./chunk-FNXOS7IA.mjs";
7
7
  import {
8
8
  CanvasRenderer,
9
9
  SVGRenderer,
@@ -12,7 +12,7 @@ import {
12
12
  isSafeUrl,
13
13
  parseColorToRGBA,
14
14
  sanitizeUrl
15
- } from "./chunk-PK5EYWIT.mjs";
15
+ } from "./chunk-YBW7ECZP.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-G6GQO7W2.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,14 @@ 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;
361
+ // Animation/interactive flags collected during the render walk (tree-walk
362
+ // fusion): the loop reads last frame's answers instead of re-walking the
363
+ // tree up to 4× per tick. Start true so the first tick stays conservative.
364
+ frameHadAnimation = true;
365
+ frameHadInteractive = true;
353
366
  resizeHandler;
354
367
  focusedA11yElement = null;
355
368
  caretBlinkTimer = null;
@@ -398,8 +411,10 @@ var Scene = class _Scene {
398
411
  this.debugA11y = options.debugA11y ?? false;
399
412
  this.disableWindowResize = options.disableWindowResize ?? false;
400
413
  if (this.disableWindowResize) {
401
- this.width = canvas.width || canvas.clientWidth || 0;
402
- this.height = canvas.height || canvas.clientHeight || 0;
414
+ const styleWidth = parseInlinePx(canvas.style?.width);
415
+ const styleHeight = parseInlinePx(canvas.style?.height);
416
+ this.width = styleWidth ?? (canvas.width || canvas.clientWidth || 0);
417
+ this.height = styleHeight ?? (canvas.height || canvas.clientHeight || 0);
403
418
  } else {
404
419
  this.width = typeof window !== "undefined" ? window.innerWidth : canvas.clientWidth || canvas.width || 800;
405
420
  this.height = typeof window !== "undefined" ? window.innerHeight : canvas.clientHeight || canvas.height || 600;
@@ -411,6 +426,7 @@ var Scene = class _Scene {
411
426
  this.autoThrottle = options.autoThrottle ?? true;
412
427
  this.particleBackend = options.particleBackend ?? "auto";
413
428
  this.a11ySyncInterval = options.a11ySyncInterval ?? 0;
429
+ this.contentProjectionEnabled = options.contentProjection ?? true;
414
430
  this.reducedMotionQuery = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
415
431
  this.root = new class RootEntity extends Entity {
416
432
  isPointInside() {
@@ -564,6 +580,11 @@ var Scene = class _Scene {
564
580
  * @param entity - The subtree whose shadow nodes should be removed.
565
581
  */
566
582
  detachA11y(entity) {
583
+ const contentEl = this.contentElements.get(entity.id);
584
+ if (contentEl) {
585
+ contentEl.remove();
586
+ this.contentElements.delete(entity.id);
587
+ }
567
588
  this.removeA11yRecursively(entity);
568
589
  }
569
590
  /**
@@ -610,6 +631,8 @@ var Scene = class _Scene {
610
631
  this.a11yRoot?.remove();
611
632
  this.portalRoot?.remove();
612
633
  this.a11yElements.clear();
634
+ for (const el of this.contentElements.values()) el.remove();
635
+ this.contentElements.clear();
613
636
  this.pointRenderer?.destroy();
614
637
  this.renderer.dispose?.();
615
638
  this.glCanvas?.remove();
@@ -693,6 +716,18 @@ var Scene = class _Scene {
693
716
  * Essential for deterministic rendering (e.g. video export).
694
717
  * Note: You should call `scene.stop()` before using this to avoid conflict with the rAF loop.
695
718
  */
719
+ /**
720
+ * The scene-graph root entity. Exposed read-only for tooling — the devtools
721
+ * inspector walks it to build the Virtual Math Tree view. Mutate the graph
722
+ * through {@link add}/{@link remove}, not by editing this node directly.
723
+ */
724
+ get rootEntity() {
725
+ return this.root;
726
+ }
727
+ /** The overlay layer root (see {@link showOverlay}), read-only for tooling. */
728
+ get overlayRootEntity() {
729
+ return this.overlayRoot;
730
+ }
696
731
  step(dt) {
697
732
  const time = this.lastTime + dt;
698
733
  this.lastTime = time;
@@ -709,21 +744,7 @@ var Scene = class _Scene {
709
744
  this.dirty = true;
710
745
  }
711
746
  /** True when any node in the subtree has a pending animation. */
712
- hasAnyPendingAnimation(node) {
713
- if (node.hasPendingAnimations()) return true;
714
- for (const child of node.children) {
715
- if (this.hasAnyPendingAnimation(child)) return true;
716
- }
717
- return false;
718
- }
719
747
  /** True when any node in the subtree is interactive (drives a11y sync). */
720
- hasAnyInteractive(node) {
721
- if (node.interactive) return true;
722
- for (const child of node.children) {
723
- if (this.hasAnyInteractive(child)) return true;
724
- }
725
- return false;
726
- }
727
748
  syncA11y(node) {
728
749
  if (!this.a11yRoot) return;
729
750
  if (node.isDOMPortal) {
@@ -972,11 +993,96 @@ var Scene = class _Scene {
972
993
  el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
973
994
  }
974
995
  }
996
+ this.syncContentProjection(node);
975
997
  for (const child of node.children) this.syncA11y(child);
976
998
  if (node === this.root) {
977
999
  for (const overlay of this.overlayRoot.children) this.syncA11y(overlay);
978
1000
  }
979
1001
  }
1002
+ /**
1003
+ * Mirror one entity's static text ({@link Entity.getContentProjection}) as a
1004
+ * transparent DOM node positioned over the drawn glyphs. Runs on the a11y
1005
+ * sync cadence; all writes are dirty-checked. Off-viewport projections are
1006
+ * hidden (`display: none`) so text-heavy scenes only materialize what is
1007
+ * visible to the browser's text machinery anyway.
1008
+ */
1009
+ syncContentProjection(node) {
1010
+ if (!this.contentProjectionEnabled || !this.a11yRoot) return;
1011
+ const projection = node.getContentProjection();
1012
+ let el = this.contentElements.get(node.id);
1013
+ if (!projection || !projection.text) {
1014
+ if (el) {
1015
+ el.remove();
1016
+ this.contentElements.delete(node.id);
1017
+ }
1018
+ return;
1019
+ }
1020
+ if (!el) {
1021
+ el = document.createElement("div");
1022
+ el.setAttribute("data-vecto-content", node.id);
1023
+ const s = el.style;
1024
+ s.position = "absolute";
1025
+ s.transformOrigin = "0 0";
1026
+ s.margin = "0";
1027
+ s.padding = "0";
1028
+ s.color = "transparent";
1029
+ s.whiteSpace = "pre-wrap";
1030
+ s.overflow = "hidden";
1031
+ s.zIndex = "0";
1032
+ el.addEventListener(
1033
+ "wheel",
1034
+ (e2) => {
1035
+ node.dispatchEvent(new VectoJSEvent("wheel", node, e2));
1036
+ },
1037
+ { passive: false }
1038
+ );
1039
+ this.a11yRoot.appendChild(el);
1040
+ this.contentElements.set(node.id, el);
1041
+ }
1042
+ if (el.textContent !== projection.text) el.textContent = projection.text;
1043
+ const font = projection.font ?? "";
1044
+ if (el.style.font !== font) el.style.font = font;
1045
+ const lineHeight = projection.lineHeight !== void 0 ? `${projection.lineHeight}px` : "";
1046
+ if (el.style.lineHeight !== lineHeight) el.style.lineHeight = lineHeight;
1047
+ const hidden = node.interactive ? "true" : null;
1048
+ if (el.getAttribute("aria-hidden") !== hidden) {
1049
+ if (hidden) el.setAttribute("aria-hidden", hidden);
1050
+ else el.removeAttribute("aria-hidden");
1051
+ }
1052
+ const selectable = projection.selectable === true;
1053
+ const pointerEvents = selectable ? "auto" : "none";
1054
+ if (el.style.pointerEvents !== pointerEvents) {
1055
+ el.style.pointerEvents = pointerEvents;
1056
+ el.style.userSelect = selectable ? "text" : "none";
1057
+ el.style.cursor = selectable ? "text" : "";
1058
+ }
1059
+ const { a, b, c, d, e, f } = node.getWorldTransform();
1060
+ el.style.left = `${e}px`;
1061
+ el.style.top = `${f}px`;
1062
+ if (node.width > 0) el.style.width = `${node.width}px`;
1063
+ if (node.height > 0) el.style.height = `${node.height}px`;
1064
+ el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
1065
+ let visible = true;
1066
+ if (node.width > 0 && node.height > 0) {
1067
+ let minX = Infinity;
1068
+ let minY = Infinity;
1069
+ let maxX = -Infinity;
1070
+ let maxY = -Infinity;
1071
+ for (let i = 0; i < 4; i++) {
1072
+ const lx = i & 1 ? node.width : 0;
1073
+ const ly = i & 2 ? node.height : 0;
1074
+ const wx = a * lx + c * ly + e;
1075
+ const wy = b * lx + d * ly + f;
1076
+ if (wx < minX) minX = wx;
1077
+ if (wx > maxX) maxX = wx;
1078
+ if (wy < minY) minY = wy;
1079
+ if (wy > maxY) maxY = wy;
1080
+ }
1081
+ visible = maxX >= 0 && minX <= this.width && maxY >= 0 && minY <= this.height;
1082
+ }
1083
+ const display = visible ? "" : "none";
1084
+ if (el.style.display !== display) el.style.display = display;
1085
+ }
980
1086
  enforceA11yDomOrder() {
981
1087
  if (!this.a11yRoot) return;
982
1088
  this.fullViewportElements.length = 0;
@@ -1173,7 +1279,7 @@ var Scene = class _Scene {
1173
1279
  loop(time) {
1174
1280
  if (!this.isRunning) return;
1175
1281
  let cap = this.effectiveMaxFPS();
1176
- const isIdle = !this.dirty && !this.hasAnyPendingAnimation(this.root) && !this.hasAnyPendingAnimation(this.overlayRoot);
1282
+ const isIdle = !this.dirty && !this.frameHadAnimation;
1177
1283
  if (isIdle && this.autoThrottle && this.renderMode === "always" && this.maxFPS > 0) {
1178
1284
  cap = Math.min(cap, 2);
1179
1285
  }
@@ -1189,12 +1295,13 @@ var Scene = class _Scene {
1189
1295
  }
1190
1296
  this.dirty = false;
1191
1297
  this.render(this.renderer, dt, time);
1192
- const hasActiveAnimation = this.hasAnyPendingAnimation(this.root) || this.hasAnyPendingAnimation(this.overlayRoot);
1193
- const hasInteractive = this.hasAnyInteractive(this.root) || this.hasAnyInteractive(this.overlayRoot);
1298
+ const hasActiveAnimation = this.frameHadAnimation;
1299
+ const hasInteractive = this.frameHadInteractive;
1300
+ const wantsContentSync = this.contentProjectionEnabled;
1194
1301
  const shouldSyncInterval = this.a11ySyncInterval <= 0 || time - this.lastA11ySync >= this.a11ySyncInterval;
1195
- if ((hasInteractive || this.a11yElements.size > 0) && (shouldSyncInterval || this.a11yPendingSyncAfterAnimation)) {
1302
+ if ((hasInteractive || this.a11yElements.size > 0 || wantsContentSync) && (shouldSyncInterval || this.a11yPendingSyncAfterAnimation)) {
1196
1303
  this.lastA11ySync = time;
1197
- if (hasInteractive) {
1304
+ if (hasInteractive || wantsContentSync) {
1198
1305
  this.syncA11y(this.root);
1199
1306
  }
1200
1307
  this.enforceA11yDomOrder();
@@ -1346,8 +1453,14 @@ var Scene = class _Scene {
1346
1453
  }
1347
1454
  const vw = this.width;
1348
1455
  const vh = this.height;
1456
+ let walkHadAnimation = false;
1457
+ let walkHadInteractive = false;
1349
1458
  const renderNode = (node, pa, pb, pc, pd, pe, pf, parentOpacity) => {
1350
- if (isMainRenderer) node.update(dt, time);
1459
+ if (isMainRenderer) {
1460
+ node.update(dt, time);
1461
+ if (!walkHadAnimation && node.hasPendingAnimations()) walkHadAnimation = true;
1462
+ if (!walkHadInteractive && node.interactive) walkHadInteractive = true;
1463
+ }
1351
1464
  const cos = Math.cos(node.rotation);
1352
1465
  const sin = Math.sin(node.rotation);
1353
1466
  const te = pa * node.x + pc * node.y + pe;
@@ -1467,7 +1580,11 @@ var Scene = class _Scene {
1467
1580
  for (const overlay of this.overlayRoot.children) {
1468
1581
  renderNode(overlay, 1, 0, 0, 1, 0, 0, 1);
1469
1582
  }
1470
- if (isMainRenderer) this.reconcilePortals();
1583
+ if (isMainRenderer) {
1584
+ this.frameHadAnimation = walkHadAnimation;
1585
+ this.frameHadInteractive = walkHadInteractive;
1586
+ this.reconcilePortals();
1587
+ }
1471
1588
  renderer.flush();
1472
1589
  if (isMainRenderer) {
1473
1590
  this.pointRenderer?.flush();
@@ -1699,6 +1816,14 @@ var TextEntity = class extends Entity {
1699
1816
  this.on("hover", () => this.isHovered = true);
1700
1817
  this.on("pointerleave", () => this.isHovered = false);
1701
1818
  }
1819
+ /**
1820
+ * Mirror the rendered text into the DOM content layer: find-in-page, screen
1821
+ * readers, crawlers, and translation see the same string the canvas draws.
1822
+ */
1823
+ getContentProjection() {
1824
+ if (!this.text) return null;
1825
+ return { text: this.text, font: `${this.fontSize}px sans-serif` };
1826
+ }
1702
1827
  /**
1703
1828
  * Replace the text content. Runs the **cold** measurement pass (re-segment +
1704
1829
  * re-measure) since the glyphs changed, then re-lays out.
@@ -1723,6 +1848,26 @@ var TextEntity = class extends Entity {
1723
1848
  this.applyLayout();
1724
1849
  return this;
1725
1850
  }
1851
+ /**
1852
+ * Set horizontal alignment (`'justify'` stretches wrapped lines flush to
1853
+ * the wrap width; the last line stays ragged) and reflow.
1854
+ */
1855
+ setTextAlign(align) {
1856
+ this.layout.textAlign = align;
1857
+ this.applyLayout();
1858
+ return this;
1859
+ }
1860
+ /**
1861
+ * Plug a hyphenator (word → parts). Break opportunities are baked in during
1862
+ * the cold pass, so this re-prepares the current text. Soft hyphens
1863
+ * (U+00AD) in the text work without one.
1864
+ */
1865
+ setHyphenator(fn) {
1866
+ this.layout.hyphenate = fn;
1867
+ this.prepared = this.layout.prepare(this.text, this.atlas, this.fontSize);
1868
+ this.applyLayout();
1869
+ return this;
1870
+ }
1726
1871
  /** Hot pass: place the cached {@link PreparedText} and refresh the a11y box. */
1727
1872
  applyLayout() {
1728
1873
  const result = this.layout.layoutPrepared(this.prepared);
@@ -88,6 +88,11 @@ export interface PreparedWord {
88
88
  isWordLike: boolean | undefined;
89
89
  /** Pre-computed `word.trim().length === 0`. */
90
90
  isWhitespace: boolean;
91
+ /**
92
+ * Glyph indices where the word may break with a visible hyphen — from
93
+ * soft hyphens (U+00AD) in the source or the engine's `hyphenate` hook.
94
+ */
95
+ breakPoints?: number[];
91
96
  }
92
97
  /** A measured paragraph; `isEmpty` marks a blank line (forced newline). */
93
98
  export interface PreparedParagraph {
@@ -107,6 +112,8 @@ export interface PreparedText {
107
112
  paragraphs: PreparedParagraph[];
108
113
  fontSize: number;
109
114
  fallbackToCanvas?: boolean;
115
+ /** Advance width of '-' at `fontSize`, for wrap-time hyphen insertion. */
116
+ hyphenWidth?: number;
110
117
  }
111
118
  /**
112
119
  * A rectangular region (in the text's local coordinate space) that text must
@@ -140,6 +147,13 @@ export declare function computeLineSegments(top: number, bottom: number, maxWidt
140
147
  */
141
148
  export declare class LayoutEngine {
142
149
  maxWidth: number;
150
+ /**
151
+ * Horizontal alignment. `'justify'` stretches inter-word spaces (or, for
152
+ * space-less CJK lines, inter-character gaps) so wrapped lines end flush at
153
+ * `maxWidth`; the last line of each paragraph stays ragged. Only applies to
154
+ * the object layout path without exclusion shapes.
155
+ */
156
+ textAlign: 'left' | 'justify';
143
157
  maxHeight: number;
144
158
  preserveLeadingSpaces: boolean;
145
159
  private wordSegmenter;
@@ -150,6 +164,16 @@ export declare class LayoutEngine {
150
164
  private richParagraphCache;
151
165
  private lastAtlas;
152
166
  private measurer;
167
+ private _hyphenate;
168
+ /**
169
+ * Optional hyphenator: given a word, return its break parts (e.g.
170
+ * `['hyphen', 'ation']`). Used at wrap time when a word doesn't fit; a
171
+ * visible '-' is drawn at the chosen break. Soft hyphens (U+00AD) in the
172
+ * source work without any hyphenator. Setting this clears the prepared
173
+ * caches (break opportunities are baked in during prepare()).
174
+ */
175
+ get hyphenate(): ((word: string) => string[]) | null;
176
+ set hyphenate(fn: ((word: string) => string[]) | null);
153
177
  constructor(maxWidth: number, maxHeight: number, measurer?: GlyphMeasurer | null);
154
178
  private getWordSegments;
155
179
  /**
package/dist/layout.js CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
 
5
5
 
6
- var _chunkBWLNEJJWjs = require('./chunk-BWLNEJJW.js');
6
+ var _chunkXLZDOFSBjs = require('./chunk-XLZDOFSB.js');
7
7
 
8
8
 
9
9
  var _chunkCTZQOM5Zjs = require('./chunk-CTZQOM5Z.js');
@@ -13,4 +13,4 @@ var _chunkCTZQOM5Zjs = require('./chunk-CTZQOM5Z.js');
13
13
 
14
14
 
15
15
 
16
- exports.LayoutEngine = _chunkBWLNEJJWjs.LayoutEngine; exports.LayoutResultBuffer = _chunkBWLNEJJWjs.LayoutResultBuffer; exports.LayoutWorkerManager = _chunkCTZQOM5Zjs.LayoutWorkerManager; exports.computeLineSegments = _chunkBWLNEJJWjs.computeLineSegments; exports.createCanvasMeasurer = _chunkBWLNEJJWjs.createCanvasMeasurer;
16
+ exports.LayoutEngine = _chunkXLZDOFSBjs.LayoutEngine; exports.LayoutResultBuffer = _chunkXLZDOFSBjs.LayoutResultBuffer; exports.LayoutWorkerManager = _chunkCTZQOM5Zjs.LayoutWorkerManager; exports.computeLineSegments = _chunkXLZDOFSBjs.computeLineSegments; exports.createCanvasMeasurer = _chunkXLZDOFSBjs.createCanvasMeasurer;
package/dist/layout.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  LayoutResultBuffer,
4
4
  computeLineSegments,
5
5
  createCanvasMeasurer
6
- } from "./chunk-SSOP3F5F.mjs";
6
+ } from "./chunk-FNXOS7IA.mjs";
7
7
  import {
8
8
  LayoutWorkerManager
9
9
  } from "./chunk-STWPTWO4.mjs";
package/dist/renderer.js CHANGED
@@ -4,11 +4,11 @@
4
4
 
5
5
 
6
6
 
7
- var _chunkGADBN53Zjs = require('./chunk-GADBN53Z.js');
7
+ var _chunkXB3ZY5MFjs = require('./chunk-XB3ZY5MF.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 = _chunkXB3ZY5MFjs.CanvasRenderer; exports.SVGRenderer = _chunkXB3ZY5MFjs.SVGRenderer; exports.WebGPUParticleSystemManager = _chunkXB3ZY5MFjs.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkXB3ZY5MFjs.createWebGLPointRenderer; exports.parseColorToRGBA = _chunkXB3ZY5MFjs.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-YBW7ECZP.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 _chunkXRWZS2DMjs = require('./chunk-XRWZS2DM.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 = _chunkXRWZS2DMjs.MSDFFont; exports.MSDFTextEntity = _chunkXRWZS2DMjs.MSDFTextEntity; exports.SVGEntity = _chunkXRWZS2DMjs.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-G6GQO7W2.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,11 @@ 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;
161
+ private frameHadAnimation;
162
+ private frameHadInteractive;
150
163
  private resizeHandler;
151
164
  private focusedA11yElement;
152
165
  private caretBlinkTimer;
@@ -256,6 +269,14 @@ export declare class Scene {
256
269
  * Essential for deterministic rendering (e.g. video export).
257
270
  * Note: You should call `scene.stop()` before using this to avoid conflict with the rAF loop.
258
271
  */
272
+ /**
273
+ * The scene-graph root entity. Exposed read-only for tooling — the devtools
274
+ * inspector walks it to build the Virtual Math Tree view. Mutate the graph
275
+ * through {@link add}/{@link remove}, not by editing this node directly.
276
+ */
277
+ get rootEntity(): Entity;
278
+ /** The overlay layer root (see {@link showOverlay}), read-only for tooling. */
279
+ get overlayRootEntity(): Entity;
259
280
  step(dt: number): void;
260
281
  /**
261
282
  * Mark the scene as needing a redraw on the next frame.
@@ -265,10 +286,16 @@ export declare class Scene {
265
286
  */
266
287
  markDirty(): void;
267
288
  /** True when any node in the subtree has a pending animation. */
268
- private hasAnyPendingAnimation;
269
289
  /** True when any node in the subtree is interactive (drives a11y sync). */
270
- private hasAnyInteractive;
271
290
  private syncA11y;
291
+ /**
292
+ * Mirror one entity's static text ({@link Entity.getContentProjection}) as a
293
+ * transparent DOM node positioned over the drawn glyphs. Runs on the a11y
294
+ * sync cadence; all writes are dirty-checked. Off-viewport projections are
295
+ * hidden (`display: none`) so text-heavy scenes only materialize what is
296
+ * visible to the browser's text machinery anyway.
297
+ */
298
+ private syncContentProjection;
272
299
  private enforceA11yDomOrder;
273
300
  /** Keep DOM/WebGL overlay layers aligned with the canvas's CSS box. */
274
301
  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.8",
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
  }