@vectojs/core 1.28.1 → 1.30.0
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-MVFPN4Y5.mjs → chunk-2WXZCPYQ.mjs} +47 -1
- package/dist/{chunk-Z3HFY75R.js → chunk-7PI7LVWW.js} +124 -78
- package/dist/{chunk-TI5EMJGL.mjs → chunk-IFSFHYF7.mjs} +44 -1
- package/dist/{chunk-K2UGEIT6.js → chunk-M73XB4ZK.js} +98 -55
- package/dist/index.js +432 -223
- package/dist/index.mjs +225 -16
- package/dist/renderer/CanvasRenderer.d.ts +23 -0
- package/dist/renderer/GlyphRasterAtlas.d.ts +12 -0
- package/dist/renderer/IRenderer.d.ts +17 -0
- package/dist/renderer.js +2 -2
- package/dist/renderer.mjs +1 -1
- package/dist/text.js +2 -2
- package/dist/text.mjs +1 -1
- package/dist/tree/Entity.d.ts +78 -1
- package/dist/tree/Scene.d.ts +88 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -11,13 +11,14 @@ import {
|
|
|
11
11
|
parseColorToRGBA,
|
|
12
12
|
sanitizeUrl,
|
|
13
13
|
setRendererDevMode
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-IFSFHYF7.mjs";
|
|
15
15
|
import {
|
|
16
16
|
Entity,
|
|
17
17
|
MSDFTextEntity,
|
|
18
18
|
SVGEntity,
|
|
19
|
-
VectoJSEvent
|
|
20
|
-
|
|
19
|
+
VectoJSEvent,
|
|
20
|
+
contentLineInHint
|
|
21
|
+
} from "./chunk-2WXZCPYQ.mjs";
|
|
21
22
|
|
|
22
23
|
// src/tree/Scene.ts
|
|
23
24
|
import { SpringDriver, TweenDriver } from "@vectojs/animation";
|
|
@@ -1430,6 +1431,45 @@ function parseInlinePx(value) {
|
|
|
1430
1431
|
const n = parseFloat(value);
|
|
1431
1432
|
return Number.isFinite(n) && n > 0 ? n : null;
|
|
1432
1433
|
}
|
|
1434
|
+
function projectionGridLineWindow(grid, projectionLines, band) {
|
|
1435
|
+
const count = grid.lines.length;
|
|
1436
|
+
const all = { start: 0, end: count, gated: false };
|
|
1437
|
+
if (!band || count === 0) return all;
|
|
1438
|
+
const lines = [];
|
|
1439
|
+
for (let i = 0; i < count; i++) {
|
|
1440
|
+
const projected = projectionLines?.[i];
|
|
1441
|
+
lines.push({
|
|
1442
|
+
y: projected?.y ?? i * grid.lineHeight,
|
|
1443
|
+
lineHeight: projected?.lineHeight ?? grid.lineHeight
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
return projectionLineWindow(lines, band, grid.lineHeight);
|
|
1447
|
+
}
|
|
1448
|
+
function projectionLineWindow(lines, band, fallbackLineHeight) {
|
|
1449
|
+
const all = {
|
|
1450
|
+
start: 0,
|
|
1451
|
+
end: lines.length,
|
|
1452
|
+
gated: false
|
|
1453
|
+
};
|
|
1454
|
+
if (!band || lines.length === 0) return all;
|
|
1455
|
+
let start = -1;
|
|
1456
|
+
let end = -1;
|
|
1457
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1458
|
+
const line = lines[i];
|
|
1459
|
+
const h = line.lineHeight ?? fallbackLineHeight;
|
|
1460
|
+
if (line.y + h >= band.minY && line.y <= band.maxY) {
|
|
1461
|
+
if (start === -1) start = i;
|
|
1462
|
+
end = i + 1;
|
|
1463
|
+
} else if (start !== -1 && line.y > band.maxY) {
|
|
1464
|
+
break;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
if (start === -1) {
|
|
1468
|
+
return { start: 0, end: Math.min(1, lines.length), gated: true };
|
|
1469
|
+
}
|
|
1470
|
+
if (start === 0 && end === lines.length) return all;
|
|
1471
|
+
return { start, end, gated: true };
|
|
1472
|
+
}
|
|
1433
1473
|
function collectTextNodes(root) {
|
|
1434
1474
|
const out = [];
|
|
1435
1475
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
@@ -1988,6 +2028,14 @@ var Scene = class _Scene {
|
|
|
1988
2028
|
* mid-hover synthesize the `pointerleave` the browser never sends for a
|
|
1989
2029
|
* detached element, so the entity doesn't keep its hover state. */
|
|
1990
2030
|
hoveredA11yElements = /* @__PURE__ */ new WeakSet();
|
|
2031
|
+
/**
|
|
2032
|
+
* Entity ids the application has pinned via {@link requestA11yProjection}.
|
|
2033
|
+
*
|
|
2034
|
+
* Ids rather than entities so a removed entity cannot be retained by this set;
|
|
2035
|
+
* a stale id simply never matches. Cleared per-entity by
|
|
2036
|
+
* {@link releaseA11yProjection}.
|
|
2037
|
+
*/
|
|
2038
|
+
a11yProjectionRequests = /* @__PURE__ */ new Set();
|
|
1991
2039
|
/** Persistent tabindex=-1 element in a11yRoot. When the focused a11y mirror is
|
|
1992
2040
|
* pruned (virtualization/streaming/removal) while it holds focus, we move
|
|
1993
2041
|
* focus here instead of letting the browser drop it to <body> — keeping the
|
|
@@ -2941,7 +2989,9 @@ var Scene = class _Scene {
|
|
|
2941
2989
|
const el = this.contentElements?.get(node.id);
|
|
2942
2990
|
if (el) {
|
|
2943
2991
|
const projectedText = el.textContent || "";
|
|
2944
|
-
|
|
2992
|
+
const windowed = el.dataset.vectoProjectionWindow !== void 0;
|
|
2993
|
+
const mismatched = windowed ? !proj.text.includes(projectedText) : projectedText !== proj.text;
|
|
2994
|
+
if (projectedText !== "" && mismatched) {
|
|
2945
2995
|
this._devWarn(
|
|
2946
2996
|
`Content projection mismatch for entity "${node.id}": projection says "${proj.text.slice(0, 60)}" but DOM shows "${projectedText.slice(0, 60)}". Ensure getContentProjection() output matches what drawSelf renders.`
|
|
2947
2997
|
);
|
|
@@ -3885,7 +3935,92 @@ var Scene = class _Scene {
|
|
|
3885
3935
|
* predicate, which is only tractable while it has one home.
|
|
3886
3936
|
*/
|
|
3887
3937
|
shouldProjectA11y(node) {
|
|
3888
|
-
|
|
3938
|
+
if (!node.interactive) return false;
|
|
3939
|
+
if (!(node.width > 0 || node.a11yFullViewport)) return false;
|
|
3940
|
+
switch (node.a11yProjection) {
|
|
3941
|
+
case "never":
|
|
3942
|
+
return false;
|
|
3943
|
+
case "onDemand":
|
|
3944
|
+
return this.a11yEngaged(node);
|
|
3945
|
+
default:
|
|
3946
|
+
return true;
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
/**
|
|
3950
|
+
* Whether an `a11yProjection: 'onDemand'` entity is currently engaged enough to
|
|
3951
|
+
* deserve a shadow node.
|
|
3952
|
+
*
|
|
3953
|
+
* Deliberately **not** hover alone. A keyboard or assistive-technology user
|
|
3954
|
+
* generates no pointer events, so a hover-only trigger would withhold the
|
|
3955
|
+
* semantic node from precisely the users it exists for. Three signals, any of
|
|
3956
|
+
* which counts:
|
|
3957
|
+
*
|
|
3958
|
+
* - **Focus.** Covers keyboard traversal and AT-driven focus. Checked against
|
|
3959
|
+
* the live element so a node keeps its own focus rather than being pruned out
|
|
3960
|
+
* from under the user mid-interaction.
|
|
3961
|
+
* - **Pointer target.** The entity under the pointer, so a mouse user gets the
|
|
3962
|
+
* same node a hover-gated design would have given them.
|
|
3963
|
+
* - **Explicit request.** {@link Scene.requestA11yProjection}, for anything the
|
|
3964
|
+
* app knows is significant — the selected item, a search hit, a
|
|
3965
|
+
* just-announced element. This is the escape hatch that keeps the mode usable
|
|
3966
|
+
* when neither focus nor pointer applies.
|
|
3967
|
+
*
|
|
3968
|
+
* The entity stays hit-testable on canvas regardless, so a click always reaches
|
|
3969
|
+
* it and promotes it on the next sync.
|
|
3970
|
+
*/
|
|
3971
|
+
a11yEngaged(node) {
|
|
3972
|
+
if (this.a11yProjectionRequests.has(node.id)) return true;
|
|
3973
|
+
if (this.mouseX > -9e3 && !this.projectsSelectableText(node)) {
|
|
3974
|
+
if (node.isPointInside(this.mouseX, this.mouseY)) return true;
|
|
3975
|
+
}
|
|
3976
|
+
const existing = this.a11yElements?.get(node.id);
|
|
3977
|
+
if (existing && typeof document !== "undefined" && document.activeElement === existing) {
|
|
3978
|
+
return true;
|
|
3979
|
+
}
|
|
3980
|
+
return false;
|
|
3981
|
+
}
|
|
3982
|
+
/**
|
|
3983
|
+
* Whether `node` mirrors selectable text of its own.
|
|
3984
|
+
*
|
|
3985
|
+
* Such an entity must not be promoted by the pointer: its interactive a11y node
|
|
3986
|
+
* would sit above the text mirror and eat the mousedown that starts a native
|
|
3987
|
+
* selection.
|
|
3988
|
+
*/
|
|
3989
|
+
projectsSelectableText(node) {
|
|
3990
|
+
const projection = node.getContentProjection?.();
|
|
3991
|
+
return !!projection?.text && projection.selectable !== false;
|
|
3992
|
+
}
|
|
3993
|
+
/**
|
|
3994
|
+
* Keep `entity`'s a11y shadow node projected while it has
|
|
3995
|
+
* `a11yProjection: 'onDemand'`.
|
|
3996
|
+
*
|
|
3997
|
+
* For anything the application knows matters but the engine cannot infer — the
|
|
3998
|
+
* selected danmaku, a search hit, a node just announced in a live region.
|
|
3999
|
+
* Without this, `'onDemand'` would be reachable only by focus or pointer, and
|
|
4000
|
+
* an app-driven selection change would leave the selected entity semantically
|
|
4001
|
+
* invisible.
|
|
4002
|
+
*
|
|
4003
|
+
* Idempotent. Has no effect on an `'eager'` entity, which is always projected.
|
|
4004
|
+
*/
|
|
4005
|
+
requestA11yProjection(entity) {
|
|
4006
|
+
const id = typeof entity === "string" ? entity : entity.id;
|
|
4007
|
+
if (this.a11yProjectionRequests.has(id)) return;
|
|
4008
|
+
this.a11yProjectionRequests.add(id);
|
|
4009
|
+
this.a11yNeedsReorder = true;
|
|
4010
|
+
this.markDirty({ entity: id, reason: "a11y-reorder" });
|
|
4011
|
+
}
|
|
4012
|
+
/**
|
|
4013
|
+
* Drop a projection request made by {@link requestA11yProjection}.
|
|
4014
|
+
*
|
|
4015
|
+
* The node is not removed immediately: it survives while it is focused or under
|
|
4016
|
+
* the pointer, and is pruned on the next sync that finds it unengaged. Releasing
|
|
4017
|
+
* a request the scene does not hold is a no-op.
|
|
4018
|
+
*/
|
|
4019
|
+
releaseA11yProjection(entity) {
|
|
4020
|
+
const id = typeof entity === "string" ? entity : entity.id;
|
|
4021
|
+
if (!this.a11yProjectionRequests.delete(id)) return;
|
|
4022
|
+
this.a11yNeedsReorder = true;
|
|
4023
|
+
this.markDirty({ entity: id, reason: "a11y-reorder" });
|
|
3889
4024
|
}
|
|
3890
4025
|
syncA11y(node, container = null) {
|
|
3891
4026
|
if (!this.a11yRoot) return;
|
|
@@ -4359,6 +4494,57 @@ var Scene = class _Scene {
|
|
|
4359
4494
|
}
|
|
4360
4495
|
return true;
|
|
4361
4496
|
}
|
|
4497
|
+
/**
|
|
4498
|
+
* The band of an entity's own y coordinates that is worth projecting, or
|
|
4499
|
+
* `null` to project everything.
|
|
4500
|
+
*
|
|
4501
|
+
* {@link projectionBoxVisible} answers "is this entity near the viewport",
|
|
4502
|
+
* which frees whole blocks that scroll away. It cannot help a single entity
|
|
4503
|
+
* *taller* than the viewport: that entity's box always intersects, so every
|
|
4504
|
+
* one of its visual lines was materialized — a `<span>` per line and, on the
|
|
4505
|
+
* grid path, a `<span>` per glyph cluster. That is where "14.8k elements for a
|
|
4506
|
+
* 346KB Markdown doc" comes from, and it is O(document) rather than
|
|
4507
|
+
* O(viewport) in both element count and per-frame walk cost.
|
|
4508
|
+
*
|
|
4509
|
+
* Measured on one entity scrolled to its middle, real headed browsers
|
|
4510
|
+
* (`benchmarks/projection-per-line/`): at 4000 lines, materializing every line
|
|
4511
|
+
* costs 6.28 ms/frame on Chrome and 6.51 ms on Firefox with 36,000 child
|
|
4512
|
+
* elements, against 0.28/0.16 ms and 963 elements when only the visible band
|
|
4513
|
+
* is emitted. The gated cost is *flat* across a 20x document-size range, so
|
|
4514
|
+
* this converts an asymptote rather than shaving a constant.
|
|
4515
|
+
*
|
|
4516
|
+
* Returns local-y bounds in the entity's own coordinate space, already
|
|
4517
|
+
* expanded by `margin` and intersected with every `clipChildren` ancestor, so
|
|
4518
|
+
* a line inside a scrolled container is measured against the container rather
|
|
4519
|
+
* than the window. `null` means "no useful bound" — a degenerate transform, a
|
|
4520
|
+
* rotation/skew that makes a y-band meaningless, or a boundless entity — and
|
|
4521
|
+
* the caller must then project every line, because emitting nothing would
|
|
4522
|
+
* silently drop text from selection, find-in-page and screen readers.
|
|
4523
|
+
*/
|
|
4524
|
+
projectionVisibleLocalYBand(node, tf, margin) {
|
|
4525
|
+
const { b, d, f } = tf;
|
|
4526
|
+
if (b !== 0 || d === 0 || !Number.isFinite(d) || !Number.isFinite(f)) return null;
|
|
4527
|
+
const top = (-margin - f) / d;
|
|
4528
|
+
const bottom = (this.height + margin - f) / d;
|
|
4529
|
+
let minY = Math.min(top, bottom);
|
|
4530
|
+
let maxY = Math.max(top, bottom);
|
|
4531
|
+
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
4532
|
+
if (!ancestor.clipChildren || ancestor.width <= 0 || ancestor.height <= 0) continue;
|
|
4533
|
+
const originWorldY = f;
|
|
4534
|
+
const unitWorldY = d + f;
|
|
4535
|
+
const originLocal = ancestor.worldToLocal(tf.e, originWorldY);
|
|
4536
|
+
const unitLocal = ancestor.worldToLocal(tf.e + tf.c, unitWorldY);
|
|
4537
|
+
if (!originLocal || !unitLocal) return null;
|
|
4538
|
+
const slope = unitLocal.y - originLocal.y;
|
|
4539
|
+
if (slope === 0 || !Number.isFinite(slope)) return null;
|
|
4540
|
+
const a1 = (-margin - originLocal.y) / slope;
|
|
4541
|
+
const a2 = (ancestor.height + margin - originLocal.y) / slope;
|
|
4542
|
+
minY = Math.max(minY, Math.min(a1, a2));
|
|
4543
|
+
maxY = Math.min(maxY, Math.max(a1, a2));
|
|
4544
|
+
}
|
|
4545
|
+
if (!Number.isFinite(minY) || !Number.isFinite(maxY) || maxY < minY) return null;
|
|
4546
|
+
return { minY, maxY };
|
|
4547
|
+
}
|
|
4362
4548
|
syncContentProjection(node) {
|
|
4363
4549
|
if (!this.contentProjectionEnabled || !this.a11yRoot) return;
|
|
4364
4550
|
let el = this.contentElements.get(node.id);
|
|
@@ -4376,7 +4562,10 @@ var Scene = class _Scene {
|
|
|
4376
4562
|
releaseProjectionEl();
|
|
4377
4563
|
return;
|
|
4378
4564
|
}
|
|
4379
|
-
const
|
|
4565
|
+
const lineBand = Number.isFinite(margin) ? this.projectionVisibleLocalYBand(node, worldTf, margin) : null;
|
|
4566
|
+
const projection = node.getContentProjection(
|
|
4567
|
+
lineBand ? { minY: lineBand.minY, maxY: lineBand.maxY } : void 0
|
|
4568
|
+
);
|
|
4380
4569
|
if (!projection || !projection.text) {
|
|
4381
4570
|
releaseProjectionEl();
|
|
4382
4571
|
return;
|
|
@@ -4411,18 +4600,24 @@ var Scene = class _Scene {
|
|
|
4411
4600
|
}
|
|
4412
4601
|
if (projection.grid) {
|
|
4413
4602
|
const gridSyncStart = this._phaseTiming ? performance.now() : 0;
|
|
4414
|
-
this.syncContentGridProjection(node, el, projection, projection.grid);
|
|
4603
|
+
this.syncContentGridProjection(node, el, projection, projection.grid, lineBand);
|
|
4415
4604
|
if (this._phaseTiming) this._recordPhase("gridSync", performance.now() - gridSyncStart);
|
|
4416
4605
|
} else if (lines && lines.length > 0) {
|
|
4606
|
+
const lineWindow = projectionLineWindow(lines, lineBand, projection.lineHeight ?? 16);
|
|
4417
4607
|
const signature = JSON.stringify({
|
|
4418
4608
|
lines,
|
|
4419
4609
|
fallbackFont: projection.font ?? "",
|
|
4420
|
-
fallbackLineHeight: projection.lineHeight ?? 16
|
|
4610
|
+
fallbackLineHeight: projection.lineHeight ?? 16,
|
|
4611
|
+
// Part of the signature, or scrolling would not rebuild the carriers and
|
|
4612
|
+
// the window would stay frozen where it was first built. Quantized to
|
|
4613
|
+
// whole line indices by construction, so a sub-pixel scroll inside one
|
|
4614
|
+
// line does not churn the DOM.
|
|
4615
|
+
window: lineWindow.gated ? `${lineWindow.start}-${lineWindow.end}` : "all"
|
|
4421
4616
|
});
|
|
4422
4617
|
if (el.dataset.vectoProjectionLines !== signature) {
|
|
4423
4618
|
this.preserveContentSelectionAcrossRebuild(el, () => {
|
|
4424
4619
|
el.replaceChildren();
|
|
4425
|
-
for (let index =
|
|
4620
|
+
for (let index = lineWindow.start; index < lineWindow.end; index++) {
|
|
4426
4621
|
const line = lines[index];
|
|
4427
4622
|
const lineElement = document.createElement("span");
|
|
4428
4623
|
const lineFont = line.font ?? projection.font ?? "";
|
|
@@ -4463,6 +4658,11 @@ var Scene = class _Scene {
|
|
|
4463
4658
|
}
|
|
4464
4659
|
});
|
|
4465
4660
|
el.dataset.vectoProjectionLines = signature;
|
|
4661
|
+
if (lineWindow.gated) {
|
|
4662
|
+
el.dataset.vectoProjectionWindow = `${lineWindow.start}-${lineWindow.end}/${lines.length}`;
|
|
4663
|
+
} else {
|
|
4664
|
+
delete el.dataset.vectoProjectionWindow;
|
|
4665
|
+
}
|
|
4466
4666
|
}
|
|
4467
4667
|
} else {
|
|
4468
4668
|
if (el.textContent !== projection.text) {
|
|
@@ -4511,11 +4711,12 @@ var Scene = class _Scene {
|
|
|
4511
4711
|
* carrier from the shared canvas geometry. Browser font measurement happens
|
|
4512
4712
|
* later in one cold read/write batch, never inside projection synchronization.
|
|
4513
4713
|
*/
|
|
4514
|
-
syncContentGridProjection(node, el, projection, grid) {
|
|
4714
|
+
syncContentGridProjection(node, el, projection, grid, lineBand) {
|
|
4515
4715
|
if (grid.source !== projection.text) {
|
|
4516
4716
|
throw new Error("ContentProjection.grid.source must equal ContentProjection.text");
|
|
4517
4717
|
}
|
|
4518
|
-
const
|
|
4718
|
+
const gridWindow = projectionGridLineWindow(grid, projection.lines, lineBand);
|
|
4719
|
+
const signature = gridWindow.gated ? `${grid.revision}:${gridWindow.start}-${gridWindow.end}` : `${grid.revision}`;
|
|
4519
4720
|
if (el.dataset.vectoContentGrid !== signature) {
|
|
4520
4721
|
const materializeStart = typeof performance !== "undefined" ? performance.now() : 0;
|
|
4521
4722
|
this.clearContentGridState(node.id, el, false);
|
|
@@ -4523,7 +4724,8 @@ var Scene = class _Scene {
|
|
|
4523
4724
|
const selectionLine = this.contentGridSelectionLine(el);
|
|
4524
4725
|
let rebuiltSelectionLine = false;
|
|
4525
4726
|
const existingLines = el.children;
|
|
4526
|
-
for (let lineIndex =
|
|
4727
|
+
for (let lineIndex = gridWindow.start; lineIndex < gridWindow.end; lineIndex++) {
|
|
4728
|
+
const domIndex = lineIndex - gridWindow.start;
|
|
4527
4729
|
const gridLine = grid.lines[lineIndex];
|
|
4528
4730
|
const projectedLine = projectionLines[lineIndex];
|
|
4529
4731
|
const lineHeight = projectedLine?.lineHeight ?? grid.lineHeight;
|
|
@@ -4538,7 +4740,7 @@ var Scene = class _Scene {
|
|
|
4538
4740
|
lineFont,
|
|
4539
4741
|
lineIndex === 0
|
|
4540
4742
|
);
|
|
4541
|
-
const reusable = existingLines[
|
|
4743
|
+
const reusable = existingLines[domIndex];
|
|
4542
4744
|
if (reusable !== void 0 && reusable.dataset.vectoGridLineSig === lineSignature && reusable.dataset.vectoGridLine === `${lineIndex}`) {
|
|
4543
4745
|
continue;
|
|
4544
4746
|
}
|
|
@@ -4611,12 +4813,13 @@ var Scene = class _Scene {
|
|
|
4611
4813
|
lineElement.appendChild(marker);
|
|
4612
4814
|
}
|
|
4613
4815
|
}
|
|
4614
|
-
const occupant = el.children[
|
|
4816
|
+
const occupant = el.children[domIndex];
|
|
4615
4817
|
if (occupant) el.replaceChild(lineElement, occupant);
|
|
4616
4818
|
else el.appendChild(lineElement);
|
|
4617
4819
|
}
|
|
4618
|
-
|
|
4619
|
-
|
|
4820
|
+
const windowLength = gridWindow.end - gridWindow.start;
|
|
4821
|
+
while (el.children.length > windowLength) {
|
|
4822
|
+
if (selectionLine !== null && selectionLine >= gridWindow.end) {
|
|
4620
4823
|
rebuiltSelectionLine = true;
|
|
4621
4824
|
}
|
|
4622
4825
|
el.lastElementChild?.remove();
|
|
@@ -4624,6 +4827,11 @@ var Scene = class _Scene {
|
|
|
4624
4827
|
if (rebuiltSelectionLine) this.releaseContentSelectionForRebuild(el);
|
|
4625
4828
|
el.dataset.vectoProjectionLines = signature;
|
|
4626
4829
|
el.dataset.vectoContentGrid = signature;
|
|
4830
|
+
if (gridWindow.gated) {
|
|
4831
|
+
el.dataset.vectoProjectionWindow = `${gridWindow.start}-${gridWindow.end}/${grid.lines.length}`;
|
|
4832
|
+
} else {
|
|
4833
|
+
delete el.dataset.vectoProjectionWindow;
|
|
4834
|
+
}
|
|
4627
4835
|
el.dataset.vectoGridCarriers = `${el.querySelectorAll("[data-vecto-grid-cell]").length}`;
|
|
4628
4836
|
if (typeof performance !== "undefined") {
|
|
4629
4837
|
const materializeMs = performance.now() - materializeStart;
|
|
@@ -6670,6 +6878,7 @@ export {
|
|
|
6670
6878
|
VectoJSEvent,
|
|
6671
6879
|
WebGPUParticleSystemManager,
|
|
6672
6880
|
beginVectoUserTiming,
|
|
6881
|
+
contentLineInHint,
|
|
6673
6882
|
createWebGLPointRenderer,
|
|
6674
6883
|
endVectoUserTiming,
|
|
6675
6884
|
installRendererDevTraps,
|
|
@@ -20,6 +20,12 @@ export declare class CanvasRenderer implements IRenderer {
|
|
|
20
20
|
* change at runtime (e.g. a window dragged between displays).
|
|
21
21
|
*/
|
|
22
22
|
maxDPR?: number;
|
|
23
|
+
/**
|
|
24
|
+
* The ratio the context is actually scaled by, i.e. the last value the
|
|
25
|
+
* constructor or {@link resize} applied. Backs {@link pixelRatio}; see there
|
|
26
|
+
* for why this is recorded rather than recomputed on read.
|
|
27
|
+
*/
|
|
28
|
+
private appliedDPR;
|
|
23
29
|
/**
|
|
24
30
|
* Max circles per batched `fill()`. A single Canvas 2D `fill()` over a path is
|
|
25
31
|
* superlinear in sub-path count, so an unbounded batch is *slower* than many
|
|
@@ -77,6 +83,23 @@ export declare class CanvasRenderer implements IRenderer {
|
|
|
77
83
|
getContext(): CanvasRenderingContext2D;
|
|
78
84
|
/** Real `devicePixelRatio`, clamped to {@link maxDPR} when set. */
|
|
79
85
|
private effectiveDPR;
|
|
86
|
+
/**
|
|
87
|
+
* @inheritdoc
|
|
88
|
+
*
|
|
89
|
+
* The ratio the context is **currently scaled by**, recorded by the constructor
|
|
90
|
+
* and {@link resize} — deliberately not a live `effectiveDPR()` call.
|
|
91
|
+
*
|
|
92
|
+
* The distinction is load-bearing rather than pedantic. `devicePixelRatio`
|
|
93
|
+
* changes the instant a zoom lands, but the backing store is only reallocated
|
|
94
|
+
* when something calls {@link resize} (in a `Scene`, the `(resolution: Ndppx)`
|
|
95
|
+
* media query). A live getter therefore reports the *future* ratio during that
|
|
96
|
+
* window, and a caller rasterizing pixels from it produces a texture that the
|
|
97
|
+
* still-old context scale resamples — the same defect this property exists to
|
|
98
|
+
* let callers avoid, merely inverted. Reporting the applied ratio means a
|
|
99
|
+
* cache keyed on it is always consistent with the pixels it is blitted into,
|
|
100
|
+
* and it simply re-keys on the next `resize`.
|
|
101
|
+
*/
|
|
102
|
+
get pixelRatio(): number;
|
|
80
103
|
/**
|
|
81
104
|
* Resize the backing canvas buffer and re-apply DPR scaling.
|
|
82
105
|
*
|
|
@@ -143,6 +143,18 @@ export declare class GlyphRasterAtlas {
|
|
|
143
143
|
private _misses;
|
|
144
144
|
private _resets;
|
|
145
145
|
constructor(options?: GlyphRasterAtlasOptions);
|
|
146
|
+
/**
|
|
147
|
+
* The device-pixel-ratio these slots were rasterized at.
|
|
148
|
+
*
|
|
149
|
+
* Immutable by design. Slot `sx`/`sy`/`sw`/`sh` are device pixels at *this*
|
|
150
|
+
* ratio while `w`/`h` are CSS pixels, so changing it would invalidate every
|
|
151
|
+
* resident slot — an atlas is therefore keyed by DPR and replaced on a change,
|
|
152
|
+
* not mutated (see `Markdown`'s code atlas pool). Exposed so a caller can
|
|
153
|
+
* compare it against {@link IRenderer.pixelRatio} and assert the blit is
|
|
154
|
+
* 1:1 rather than resampled: `blitScale = renderer.pixelRatio / atlas.dpr`,
|
|
155
|
+
* which must be 1 for the pixels to land crisp.
|
|
156
|
+
*/
|
|
157
|
+
get pixelRatio(): number;
|
|
146
158
|
/** Live instrumentation snapshot. */
|
|
147
159
|
get stats(): GlyphRasterAtlasStats;
|
|
148
160
|
/**
|
|
@@ -67,6 +67,23 @@ export interface IRenderer {
|
|
|
67
67
|
* backend in the build where it matters is not much of a debug tool.
|
|
68
68
|
*/
|
|
69
69
|
readonly kind?: 'canvas2d' | 'svg' | 'three' | string;
|
|
70
|
+
/**
|
|
71
|
+
* Device pixels per CSS pixel of this renderer's backing store, i.e. the scale
|
|
72
|
+
* already applied to the drawing context.
|
|
73
|
+
*
|
|
74
|
+
* Read this rather than `window.devicePixelRatio` when rasterizing pixels that
|
|
75
|
+
* will be blitted into the renderer: the two differ whenever a backend clamps
|
|
76
|
+
* (`CanvasRenderer.maxDPR`, `SceneOptions.maxDPR`), and rasterizing at the
|
|
77
|
+
* window's ratio while the context is scaled to a clamped one blits a texture
|
|
78
|
+
* the destination then resamples. A cache keyed on this value also stays
|
|
79
|
+
* correct across a zoom or a monitor move, which a value captured once at
|
|
80
|
+
* module scope cannot — that is the defect this exists to make fixable
|
|
81
|
+
* (`GlyphRasterAtlas` consumers; see `Markdown`'s code atlas pool).
|
|
82
|
+
*
|
|
83
|
+
* Optional and a *live* read, not a snapshot: a backend that has no backing
|
|
84
|
+
* store of its own omits it, and a caller treats the absence as `1`.
|
|
85
|
+
*/
|
|
86
|
+
readonly pixelRatio?: number;
|
|
70
87
|
/**
|
|
71
88
|
* Enable or disable draw counting. Optional; a backend that cannot count omits
|
|
72
89
|
* it and a reader treats the absence as "not available".
|
package/dist/renderer.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
var
|
|
13
|
+
var _chunkM73XB4ZKjs = require('./chunk-M73XB4ZK.js');
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
|
|
@@ -22,4 +22,4 @@ var _chunkK2UGEIT6js = require('./chunk-K2UGEIT6.js');
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
exports.CanvasRenderer =
|
|
25
|
+
exports.CanvasRenderer = _chunkM73XB4ZKjs.CanvasRenderer; exports.GlyphRasterAtlas = _chunkM73XB4ZKjs.GlyphRasterAtlas; exports.SVGRenderer = _chunkM73XB4ZKjs.SVGRenderer; exports.TextRasterCache = _chunkM73XB4ZKjs.TextRasterCache; exports.WebGPUParticleSystemManager = _chunkM73XB4ZKjs.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkM73XB4ZKjs.createWebGLPointRenderer; exports.installRendererDevTraps = _chunkM73XB4ZKjs.installRendererDevTraps; exports.isRendererDevMode = _chunkM73XB4ZKjs.isRendererDevMode; exports.parseColorToRGBA = _chunkM73XB4ZKjs.parseColorToRGBA; exports.setRendererDevMode = _chunkM73XB4ZKjs.setRendererDevMode;
|
package/dist/renderer.mjs
CHANGED
package/dist/text.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
var
|
|
5
|
+
var _chunk7PI7LVWWjs = require('./chunk-7PI7LVWW.js');
|
|
6
6
|
|
|
7
7
|
// src/text/index.ts
|
|
8
8
|
var _text = require('@vectojs/text'); _createStarExport(_text);
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
exports.MSDFTextEntity =
|
|
12
|
+
exports.MSDFTextEntity = _chunk7PI7LVWWjs.MSDFTextEntity; exports.SVGEntity = _chunk7PI7LVWWjs.SVGEntity;
|
package/dist/text.mjs
CHANGED
package/dist/tree/Entity.d.ts
CHANGED
|
@@ -103,6 +103,43 @@ export interface ContentProjectionLine {
|
|
|
103
103
|
/** Styled text runs in visual order. */
|
|
104
104
|
runs?: ContentProjectionRun[];
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Advice from the {@link Scene} about which part of an entity is worth
|
|
108
|
+
* describing in {@link Entity.getContentProjection}.
|
|
109
|
+
*
|
|
110
|
+
* **Purely an optimization, and ignoring it is always correct.** The Scene
|
|
111
|
+
* windows the DOM itself, so an entity that returns its whole document still
|
|
112
|
+
* behaves correctly — it just pays to build lines that get discarded. An entity
|
|
113
|
+
* whose projection is O(glyphs) can use this to make that build O(visible)
|
|
114
|
+
* instead, which is the difference between per-frame cost that scales with the
|
|
115
|
+
* document and cost that scales with the viewport.
|
|
116
|
+
*
|
|
117
|
+
* Why a hint rather than a strict window: the entity owns the mapping from its
|
|
118
|
+
* own text to visual lines, and only it knows things like where a wrapped
|
|
119
|
+
* paragraph begins. Handing it a band and letting it round outward keeps that
|
|
120
|
+
* knowledge in one place. An entity may return more than asked — never less
|
|
121
|
+
* than it can, because text absent from the projection is invisible to
|
|
122
|
+
* find-in-page, copy and, for static text, the screen reader.
|
|
123
|
+
*/
|
|
124
|
+
export interface ContentProjectionHint {
|
|
125
|
+
/**
|
|
126
|
+
* Inclusive band of entity-local y worth projecting, already expanded by the
|
|
127
|
+
* scene's `contentProjectionMargin` and intersected with every clipping
|
|
128
|
+
* ancestor. Absent when no useful bound exists (a rotated or skewed
|
|
129
|
+
* transform, a boundless entity), in which case project everything.
|
|
130
|
+
*/
|
|
131
|
+
minY?: number;
|
|
132
|
+
maxY?: number;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Whether a line at `y` of height `height` is worth projecting under `hint`.
|
|
136
|
+
*
|
|
137
|
+
* Shared so every consumer rounds the same way: a line is kept when its box
|
|
138
|
+
* overlaps the band at all, which retains a line straddling the edge whole
|
|
139
|
+
* rather than clipping it mid-glyph. Returns `true` when the hint carries no
|
|
140
|
+
* band, so the default is always "project it".
|
|
141
|
+
*/
|
|
142
|
+
export declare function contentLineInHint(hint: ContentProjectionHint | undefined, y: number, height: number): boolean;
|
|
106
143
|
export interface ContentProjection {
|
|
107
144
|
/** The logical source text exposed to find, selection, copy, and assistive technology. */
|
|
108
145
|
text: string;
|
|
@@ -540,6 +577,42 @@ export declare abstract class Entity {
|
|
|
540
577
|
* nodes, so on-top components stay clickable.
|
|
541
578
|
*/
|
|
542
579
|
a11yFullViewport: boolean;
|
|
580
|
+
/**
|
|
581
|
+
* When this entity's a11y shadow node is materialized.
|
|
582
|
+
*
|
|
583
|
+
* `'eager'` (the default) keeps today's behaviour: a shadow node exists for as
|
|
584
|
+
* long as the entity is `interactive` with a box. That is right for a button or
|
|
585
|
+
* a link, and wrong for thousands of ephemeral, individually-meaningless
|
|
586
|
+
* entities — particles, danmaku, graph nodes — where it produces one DOM node
|
|
587
|
+
* per entity every frame.
|
|
588
|
+
*
|
|
589
|
+
* Measured on 5,000 moving interactive entities (`benchmarks/lazy-a11y/`):
|
|
590
|
+
* eager costs **72.2 ms/frame on Chrome and 114.3 ms on Firefox**, missing even
|
|
591
|
+
* 60 Hz, against **1.55/1.63 ms** for the same scene with one node projected —
|
|
592
|
+
* within noise of the 1.26/1.65 ms floor of projecting nothing at all.
|
|
593
|
+
*
|
|
594
|
+
* `'onDemand'` projects a node only while {@link Scene} considers the entity
|
|
595
|
+
* *engaged*: it is focused, it is the current pointer target, or it has been
|
|
596
|
+
* explicitly requested via {@link Scene.requestA11yProjection}. Crucially the
|
|
597
|
+
* trigger is not hover alone — a keyboard or assistive-technology user
|
|
598
|
+
* generates no hover, so a hover-only gate would remove exactly those users'
|
|
599
|
+
* access. Engagement therefore includes focus and an explicit request, and the
|
|
600
|
+
* entity stays hit-testable on canvas throughout, so a click still reaches it
|
|
601
|
+
* and promotes it.
|
|
602
|
+
*
|
|
603
|
+
* `'never'` suppresses the node entirely. Prefer `interactive = false` unless
|
|
604
|
+
* the entity genuinely needs pointer events without any semantic presence;
|
|
605
|
+
* this exists so a purely decorative interactive surface can opt out without
|
|
606
|
+
* losing canvas hit-testing.
|
|
607
|
+
*
|
|
608
|
+
* **This does not replace an aggregate description.** A thousand `'onDemand'`
|
|
609
|
+
* danmaku are individually reachable but say nothing collectively. The proven
|
|
610
|
+
* pattern is one aggregate live region (`role: 'status'`, `a11yFullViewport`)
|
|
611
|
+
* plus a small pool of persistent hotspots for the current selection — see
|
|
612
|
+
* `vectojs-native/danmaku`. Use `'onDemand'` to stop paying per entity, not as
|
|
613
|
+
* the whole accessibility story.
|
|
614
|
+
*/
|
|
615
|
+
a11yProjection: 'eager' | 'onDemand' | 'never';
|
|
543
616
|
/**
|
|
544
617
|
* Hide this entity AND its whole subtree from the accessibility/automation
|
|
545
618
|
* projection, regardless of each node's own `interactive` flag.
|
|
@@ -920,9 +993,13 @@ export declare abstract class Entity {
|
|
|
920
993
|
* `selectable` is set — natively selectable. Returns `null` by default.
|
|
921
994
|
* Read on the a11y sync cadence, so text changes propagate automatically.
|
|
922
995
|
*
|
|
996
|
+
* @param hint - Optional advice about which part of the entity is worth
|
|
997
|
+
* describing. Purely an optimization: ignoring it is always correct, which
|
|
998
|
+
* is why it is a parameter rather than a required contract change. See
|
|
999
|
+
* {@link ContentProjectionHint}.
|
|
923
1000
|
* @returns The projection descriptor, or `null` to project nothing.
|
|
924
1001
|
*/
|
|
925
|
-
getContentProjection(): ContentProjection | null;
|
|
1002
|
+
getContentProjection(hint?: ContentProjectionHint): ContentProjection | null;
|
|
926
1003
|
/**
|
|
927
1004
|
* Whether this entity still has a queued/running tween animation, or an
|
|
928
1005
|
* active {@link setTransition}/{@link animateTo}/{@link springTo} property
|