@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/{chunk-SSOP3F5F.mjs → chunk-FNXOS7IA.mjs} +161 -15
- package/dist/{chunk-SL654OMF.mjs → chunk-G6GQO7W2.mjs} +26 -0
- package/dist/{chunk-GADBN53Z.js → chunk-XB3ZY5MF.js} +82 -1
- package/dist/{chunk-BWLNEJJW.js → chunk-XLZDOFSB.js} +175 -29
- package/dist/{chunk-4BO5XBJL.js → chunk-XRWZS2DM.js} +34 -8
- package/dist/{chunk-PK5EYWIT.mjs → chunk-YBW7ECZP.mjs} +82 -1
- package/dist/components/TextEntity.d.ts +17 -1
- package/dist/index.js +288 -143
- package/dist/index.mjs +171 -26
- package/dist/layout/LayoutEngine.d.ts +24 -0
- package/dist/layout.js +2 -2
- package/dist/layout.mjs +1 -1
- package/dist/renderer.js +2 -2
- package/dist/renderer.mjs +1 -1
- package/dist/text/MSDFTextEntity.d.ts +6 -1
- package/dist/text.js +2 -2
- package/dist/text.mjs +1 -1
- package/dist/tree/Entity.d.ts +31 -0
- package/dist/tree/Scene.d.ts +29 -2
- package/package.json +5 -3
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
LayoutResultBuffer,
|
|
4
4
|
computeLineSegments,
|
|
5
5
|
createCanvasMeasurer
|
|
6
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
402
|
-
|
|
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.
|
|
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.
|
|
1193
|
-
const hasInteractive = this.
|
|
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)
|
|
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)
|
|
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
|
|
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 =
|
|
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
package/dist/renderer.js
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
var
|
|
7
|
+
var _chunkXB3ZY5MFjs = require('./chunk-XB3ZY5MF.js');
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
exports.CanvasRenderer =
|
|
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
|
@@ -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
|
|
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 =
|
|
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
package/dist/tree/Entity.d.ts
CHANGED
|
@@ -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
|
package/dist/tree/Scene.d.ts
CHANGED
|
@@ -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.
|
|
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
|
}
|