@vectojs/core 1.29.0 → 1.31.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-RWN3ITBD.mjs} +81 -1
- package/dist/{chunk-Z3HFY75R.js → chunk-TRT2TRCW.js} +158 -78
- package/dist/components/TextEntity.d.ts +3 -0
- package/dist/index.js +513 -244
- package/dist/index.mjs +293 -24
- package/dist/text/MSDFTextEntity.d.ts +3 -0
- package/dist/text.js +2 -2
- package/dist/text.mjs +1 -1
- package/dist/tree/Entity.d.ts +104 -1
- package/dist/tree/Scene.d.ts +127 -1
- package/package.json +1 -1
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,39 @@ 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;
|
|
1003
|
+
/**
|
|
1004
|
+
* A cheap, monotonically-increasing stamp of this entity's projected content.
|
|
1005
|
+
*
|
|
1006
|
+
* Purely an optimization, and opt-in: returning `null` (the default) means
|
|
1007
|
+
* "I cannot cheaply tell whether my content changed", and the {@link Scene}
|
|
1008
|
+
* then rebuilds the projection every synced frame exactly as before. An
|
|
1009
|
+
* implementation must bump the value whenever anything
|
|
1010
|
+
* {@link getContentProjection} would report changes — text, fonts, line
|
|
1011
|
+
* geometry, `selectable`, grid revision.
|
|
1012
|
+
*
|
|
1013
|
+
* When two consecutive syncs report the same epoch AND the entity's geometry
|
|
1014
|
+
* is unchanged, `Scene` skips the block *before* calling
|
|
1015
|
+
* {@link getContentProjection}. That matters because the projection call is
|
|
1016
|
+
* O(glyphs-in-block) and the DOM diff around it costs about the same again:
|
|
1017
|
+
* measured on a 1500-resident-block document, a sync in which the projected
|
|
1018
|
+
* text was byte-identical before and after still cost 17.875 ms, and skipping
|
|
1019
|
+
* unchanged blocks took that to 0.475 ms (carryctx CTX-0199, vectojs#343).
|
|
1020
|
+
*
|
|
1021
|
+
* Correctness is entirely on the implementer: a stale epoch means stale DOM,
|
|
1022
|
+
* so bump it in the same place the content is invalidated rather than trying
|
|
1023
|
+
* to enumerate mutation sites afterwards. Any monotonic counter works; the
|
|
1024
|
+
* value is only ever compared for equality with the previous sync's.
|
|
1025
|
+
*
|
|
1026
|
+
* @returns The current content epoch, or `null` to disable skipping.
|
|
1027
|
+
*/
|
|
1028
|
+
getContentEpoch(): number | null;
|
|
926
1029
|
/**
|
|
927
1030
|
* Whether this entity still has a queued/running tween animation, or an
|
|
928
1031
|
* active {@link setTransition}/{@link animateTo}/{@link springTo} property
|
package/dist/tree/Scene.d.ts
CHANGED
|
@@ -201,6 +201,34 @@ export interface SceneOptions {
|
|
|
201
201
|
* height (`undefined` → resolved to `Scene.height` at sync time).
|
|
202
202
|
*/
|
|
203
203
|
contentProjectionMargin?: number;
|
|
204
|
+
/**
|
|
205
|
+
* Virtualization margin (px) for the *semantic* tier of content projection —
|
|
206
|
+
* whether a block has **any** projected DOM at all, as opposed to
|
|
207
|
+
* {@link SceneOptions.contentProjectionMargin}, which decides whether that
|
|
208
|
+
* block's per-line **carriers** are windowed.
|
|
209
|
+
*
|
|
210
|
+
* Splitting the two makes a coarse resident tier expressible: with
|
|
211
|
+
* `contentSemanticMargin: Infinity` and a finite `contentProjectionMargin`,
|
|
212
|
+
* every block in the document keeps an element holding its full text — so
|
|
213
|
+
* find-in-page and screen-reader read-ahead see the whole document — while
|
|
214
|
+
* only blocks near the viewport pay for per-line carriers. One scalar could
|
|
215
|
+
* not express that, because a finite value freed off-band blocks entirely and
|
|
216
|
+
* `Infinity` also unwindowed every carrier, which is O(total document glyphs).
|
|
217
|
+
*
|
|
218
|
+
* `Infinity` is safe **here** and remains unsupported for
|
|
219
|
+
* `contentProjectionMargin`: the cost that made it unsupported comes from an
|
|
220
|
+
* unwindowed carrier band, not from resident text.
|
|
221
|
+
*
|
|
222
|
+
* Note the one-time cost. A resident tier materializes one element per block
|
|
223
|
+
* on the first sync — measured ~13µs per node created, so ~20ms at 1000 blocks
|
|
224
|
+
* and ~146ms at 10000 — as one synchronous block. Steady state is cheap
|
|
225
|
+
* (unchanged blocks skip via {@link Entity.getContentEpoch}), so this is a
|
|
226
|
+
* document-open stall, not a per-frame cost.
|
|
227
|
+
*
|
|
228
|
+
* Default: whatever `contentProjectionMargin` resolves to, so omitting this
|
|
229
|
+
* leaves behaviour unchanged.
|
|
230
|
+
*/
|
|
231
|
+
contentSemanticMargin?: number;
|
|
204
232
|
/**
|
|
205
233
|
* Reading direction used to order the accessibility/automation shadow tree so
|
|
206
234
|
* keyboard **tab order** and screen-reader traversal follow the *visual*
|
|
@@ -240,7 +268,7 @@ export interface SceneOptions {
|
|
|
240
268
|
* against. A new option must be added here too — the test suite asserts the two
|
|
241
269
|
* stay in sync.
|
|
242
270
|
*/
|
|
243
|
-
export declare const SCENE_OPTION_KEYS: readonly ['a11ySyncInterval', 'autoThrottle', 'contentProjection', 'contentProjectionMargin', 'debugA11y', 'disableWindowResize', 'maxDPR', 'maxFPS', 'particleBackend', 'pointBackend', 'readingDirection', 'renderer', 'renderMode', 'respectReducedMotion', 'userTiming'];
|
|
271
|
+
export declare const SCENE_OPTION_KEYS: readonly ['a11ySyncInterval', 'autoThrottle', 'contentProjection', 'contentProjectionMargin', 'contentSemanticMargin', 'debugA11y', 'disableWindowResize', 'maxDPR', 'maxFPS', 'particleBackend', 'pointBackend', 'readingDirection', 'renderer', 'renderMode', 'respectReducedMotion', 'userTiming'];
|
|
244
272
|
/** Frame-rate the loop is capped to when the OS requests reduced motion. */
|
|
245
273
|
export declare const REDUCED_MOTION_FPS = 30;
|
|
246
274
|
/**
|
|
@@ -488,6 +516,15 @@ export declare class Scene {
|
|
|
488
516
|
private a11yElements;
|
|
489
517
|
/** DOM nodes mirroring static text content, keyed by entity id. */
|
|
490
518
|
private contentElements;
|
|
519
|
+
/**
|
|
520
|
+
* What the last completed content-projection sync was built from, per entity.
|
|
521
|
+
*
|
|
522
|
+
* Compared at the top of {@link syncContentProjection} to skip a block whose
|
|
523
|
+
* content AND geometry are both unchanged, before the O(glyphs) projection
|
|
524
|
+
* build. Only populated for entities that opt in via
|
|
525
|
+
* {@link Entity.getContentEpoch}. (carryctx CTX-0199)
|
|
526
|
+
*/
|
|
527
|
+
private contentSyncState;
|
|
491
528
|
/** Pending cold font-calibration frame per projected grid entity. */
|
|
492
529
|
private contentGridCalibrationFrames;
|
|
493
530
|
/** Detached, untransformed font probes used by the cold calibration pass. */
|
|
@@ -522,6 +559,7 @@ export declare class Scene {
|
|
|
522
559
|
private contentMetricScaleX;
|
|
523
560
|
private contentProjectionEnabled;
|
|
524
561
|
private contentProjectionMargin;
|
|
562
|
+
private contentSemanticMargin;
|
|
525
563
|
/**
|
|
526
564
|
* True while a text-selection drag that started on a projection's blank
|
|
527
565
|
* region (no text node under the press) is being driven manually — the
|
|
@@ -555,6 +593,14 @@ export declare class Scene {
|
|
|
555
593
|
* mid-hover synthesize the `pointerleave` the browser never sends for a
|
|
556
594
|
* detached element, so the entity doesn't keep its hover state. */
|
|
557
595
|
private readonly hoveredA11yElements;
|
|
596
|
+
/**
|
|
597
|
+
* Entity ids the application has pinned via {@link requestA11yProjection}.
|
|
598
|
+
*
|
|
599
|
+
* Ids rather than entities so a removed entity cannot be retained by this set;
|
|
600
|
+
* a stale id simply never matches. Cleared per-entity by
|
|
601
|
+
* {@link releaseA11yProjection}.
|
|
602
|
+
*/
|
|
603
|
+
private readonly a11yProjectionRequests;
|
|
558
604
|
/** Persistent tabindex=-1 element in a11yRoot. When the focused a11y mirror is
|
|
559
605
|
* pruned (virtualization/streaming/removal) while it holds focus, we move
|
|
560
606
|
* focus here instead of letting the browser drop it to <body> — keeping the
|
|
@@ -1324,6 +1370,58 @@ export declare class Scene {
|
|
|
1324
1370
|
* predicate, which is only tractable while it has one home.
|
|
1325
1371
|
*/
|
|
1326
1372
|
private shouldProjectA11y;
|
|
1373
|
+
/**
|
|
1374
|
+
* Whether an `a11yProjection: 'onDemand'` entity is currently engaged enough to
|
|
1375
|
+
* deserve a shadow node.
|
|
1376
|
+
*
|
|
1377
|
+
* Deliberately **not** hover alone. A keyboard or assistive-technology user
|
|
1378
|
+
* generates no pointer events, so a hover-only trigger would withhold the
|
|
1379
|
+
* semantic node from precisely the users it exists for. Three signals, any of
|
|
1380
|
+
* which counts:
|
|
1381
|
+
*
|
|
1382
|
+
* - **Focus.** Covers keyboard traversal and AT-driven focus. Checked against
|
|
1383
|
+
* the live element so a node keeps its own focus rather than being pruned out
|
|
1384
|
+
* from under the user mid-interaction.
|
|
1385
|
+
* - **Pointer target.** The entity under the pointer, so a mouse user gets the
|
|
1386
|
+
* same node a hover-gated design would have given them.
|
|
1387
|
+
* - **Explicit request.** {@link Scene.requestA11yProjection}, for anything the
|
|
1388
|
+
* app knows is significant — the selected item, a search hit, a
|
|
1389
|
+
* just-announced element. This is the escape hatch that keeps the mode usable
|
|
1390
|
+
* when neither focus nor pointer applies.
|
|
1391
|
+
*
|
|
1392
|
+
* The entity stays hit-testable on canvas regardless, so a click always reaches
|
|
1393
|
+
* it and promotes it on the next sync.
|
|
1394
|
+
*/
|
|
1395
|
+
private a11yEngaged;
|
|
1396
|
+
/**
|
|
1397
|
+
* Whether `node` mirrors selectable text of its own.
|
|
1398
|
+
*
|
|
1399
|
+
* Such an entity must not be promoted by the pointer: its interactive a11y node
|
|
1400
|
+
* would sit above the text mirror and eat the mousedown that starts a native
|
|
1401
|
+
* selection.
|
|
1402
|
+
*/
|
|
1403
|
+
private projectsSelectableText;
|
|
1404
|
+
/**
|
|
1405
|
+
* Keep `entity`'s a11y shadow node projected while it has
|
|
1406
|
+
* `a11yProjection: 'onDemand'`.
|
|
1407
|
+
*
|
|
1408
|
+
* For anything the application knows matters but the engine cannot infer — the
|
|
1409
|
+
* selected danmaku, a search hit, a node just announced in a live region.
|
|
1410
|
+
* Without this, `'onDemand'` would be reachable only by focus or pointer, and
|
|
1411
|
+
* an app-driven selection change would leave the selected entity semantically
|
|
1412
|
+
* invisible.
|
|
1413
|
+
*
|
|
1414
|
+
* Idempotent. Has no effect on an `'eager'` entity, which is always projected.
|
|
1415
|
+
*/
|
|
1416
|
+
requestA11yProjection(entity: Entity | string): void;
|
|
1417
|
+
/**
|
|
1418
|
+
* Drop a projection request made by {@link requestA11yProjection}.
|
|
1419
|
+
*
|
|
1420
|
+
* The node is not removed immediately: it survives while it is focused or under
|
|
1421
|
+
* the pointer, and is pruned on the next sync that finds it unengaged. Releasing
|
|
1422
|
+
* a request the scene does not hold is a no-op.
|
|
1423
|
+
*/
|
|
1424
|
+
releaseA11yProjection(entity: Entity | string): void;
|
|
1327
1425
|
private syncA11y;
|
|
1328
1426
|
/**
|
|
1329
1427
|
* Mirror one entity's static text ({@link Entity.getContentProjection}) as a
|
|
@@ -1341,6 +1439,34 @@ export declare class Scene {
|
|
|
1341
1439
|
* culling and always count as visible, matching the legacy behavior.
|
|
1342
1440
|
*/
|
|
1343
1441
|
private projectionBoxVisible;
|
|
1442
|
+
/**
|
|
1443
|
+
* The band of an entity's own y coordinates that is worth projecting, or
|
|
1444
|
+
* `null` to project everything.
|
|
1445
|
+
*
|
|
1446
|
+
* {@link projectionBoxVisible} answers "is this entity near the viewport",
|
|
1447
|
+
* which frees whole blocks that scroll away. It cannot help a single entity
|
|
1448
|
+
* *taller* than the viewport: that entity's box always intersects, so every
|
|
1449
|
+
* one of its visual lines was materialized — a `<span>` per line and, on the
|
|
1450
|
+
* grid path, a `<span>` per glyph cluster. That is where "14.8k elements for a
|
|
1451
|
+
* 346KB Markdown doc" comes from, and it is O(document) rather than
|
|
1452
|
+
* O(viewport) in both element count and per-frame walk cost.
|
|
1453
|
+
*
|
|
1454
|
+
* Measured on one entity scrolled to its middle, real headed browsers
|
|
1455
|
+
* (`benchmarks/projection-per-line/`): at 4000 lines, materializing every line
|
|
1456
|
+
* costs 6.28 ms/frame on Chrome and 6.51 ms on Firefox with 36,000 child
|
|
1457
|
+
* elements, against 0.28/0.16 ms and 963 elements when only the visible band
|
|
1458
|
+
* is emitted. The gated cost is *flat* across a 20x document-size range, so
|
|
1459
|
+
* this converts an asymptote rather than shaving a constant.
|
|
1460
|
+
*
|
|
1461
|
+
* Returns local-y bounds in the entity's own coordinate space, already
|
|
1462
|
+
* expanded by `margin` and intersected with every `clipChildren` ancestor, so
|
|
1463
|
+
* a line inside a scrolled container is measured against the container rather
|
|
1464
|
+
* than the window. `null` means "no useful bound" — a degenerate transform, a
|
|
1465
|
+
* rotation/skew that makes a y-band meaningless, or a boundless entity — and
|
|
1466
|
+
* the caller must then project every line, because emitting nothing would
|
|
1467
|
+
* silently drop text from selection, find-in-page and screen readers.
|
|
1468
|
+
*/
|
|
1469
|
+
private projectionVisibleLocalYBand;
|
|
1344
1470
|
private syncContentProjection;
|
|
1345
1471
|
/**
|
|
1346
1472
|
* Materialize a prepared grid in logical source order while positioning each
|