@vectojs/core 1.14.0 → 1.16.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.
Files changed (52) hide show
  1. package/dist/{chunk-XIEQHSBB.mjs → chunk-AQTO7OSU.mjs} +253 -496
  2. package/dist/{chunk-2Z23LTH3.js → chunk-DUYB4GX4.js} +310 -551
  3. package/dist/{chunk-BEUIB3U7.js → chunk-IYPLG4Z4.js} +93 -43
  4. package/dist/{chunk-L5BCKFQE.mjs → chunk-JFU56BX4.mjs} +48 -0
  5. package/dist/index.d.ts +4 -12
  6. package/dist/index.js +2155 -456
  7. package/dist/index.mjs +1977 -279
  8. package/dist/layout/index.d.ts +1 -3
  9. package/dist/layout.js +3 -15
  10. package/dist/layout.mjs +2 -16
  11. package/dist/renderer/CanvasRenderer.d.ts +22 -0
  12. package/dist/renderer/IRenderer.d.ts +13 -0
  13. package/dist/renderer.js +4 -3
  14. package/dist/renderer.mjs +1 -1
  15. package/dist/text/MSDFTextEntity.d.ts +30 -1
  16. package/dist/text/index.d.ts +1 -5
  17. package/dist/text.js +5 -15
  18. package/dist/text.mjs +6 -17
  19. package/dist/tree/ComputeParticleEntity.d.ts +18 -0
  20. package/dist/tree/DOMPortalEntity.d.ts +18 -0
  21. package/dist/tree/Entity.d.ts +122 -6
  22. package/dist/tree/Scene.d.ts +380 -0
  23. package/dist/wasm/anim-backend.d.ts +86 -0
  24. package/dist/wasm/asset.d.ts +25 -0
  25. package/dist/wasm/asset.js +7 -0
  26. package/dist/wasm/asset.mjs +5 -0
  27. package/dist/wasm/backend.d.ts +154 -0
  28. package/dist/wasm/hit-backend.d.ts +92 -0
  29. package/dist/wasm/hit-store.d.ts +48 -0
  30. package/dist/wasm/particle-backend.d.ts +104 -0
  31. package/dist/wasm/scene-store.d.ts +28 -0
  32. package/dist/wasm/soa.d.ts +146 -0
  33. package/dist/wasm/vectojs_core.wasm +0 -0
  34. package/package.json +17 -8
  35. package/dist/animation/drivers.d.ts +0 -48
  36. package/dist/animation/easing.d.ts +0 -16
  37. package/dist/chunk-4AR425AR.js +0 -1121
  38. package/dist/chunk-BA5HUUDF.js +0 -760
  39. package/dist/chunk-IESDTEJ4.mjs +0 -1121
  40. package/dist/chunk-X7I465AQ.mjs +0 -760
  41. package/dist/layout/LayoutEngine.d.ts +0 -289
  42. package/dist/layout/LayoutWorker.d.ts +0 -23
  43. package/dist/layout/LayoutWorkerManager.d.ts +0 -26
  44. package/dist/layout/LayoutWorkerSource.d.ts +0 -1
  45. package/dist/layout/measure.d.ts +0 -20
  46. package/dist/math/SpatialHashGrid.d.ts +0 -53
  47. package/dist/math/SpringPhysics.d.ts +0 -13
  48. package/dist/text/ArabicShaper.d.ts +0 -10
  49. package/dist/text/BidiResolver.d.ts +0 -5
  50. package/dist/text/MSDFFont.d.ts +0 -129
  51. package/dist/text/PreparedContentGrid.d.ts +0 -60
  52. package/dist/text/Typography.d.ts +0 -11
@@ -12,6 +12,10 @@ export interface IWebGPUParticleSystemManager {
12
12
  }
13
13
  import { Entity } from './Entity';
14
14
  import { IRenderer } from '../renderer/IRenderer';
15
+ import { type WasmModuleSource, type WasmTransformBackend } from '../wasm/backend';
16
+ import { type HitModuleSource, type HitTestBackend } from '../wasm/hit-backend';
17
+ import { type AnimModuleSource, type AnimBackend } from '../wasm/anim-backend';
18
+ import { type ParticleModuleSource, type ParticleBackend } from '../wasm/particle-backend';
15
19
  /**
16
20
  * Options for {@link Scene}.
17
21
  */
@@ -110,6 +114,15 @@ export interface SceneOptions {
110
114
  * height (`undefined` → resolved to `Scene.height` at sync time).
111
115
  */
112
116
  contentProjectionMargin?: number;
117
+ /**
118
+ * Reading direction used to order the accessibility/automation shadow tree so
119
+ * keyboard **tab order** and screen-reader traversal follow the *visual*
120
+ * reading order (top-to-bottom, then inline) rather than scene-graph
121
+ * insertion order — two entities added in any order but drawn left/right of
122
+ * each other should Tab left→right (`'ltr'`, default) or right→left
123
+ * (`'rtl'`). Also settable later via {@link Scene.readingDirection}.
124
+ */
125
+ readingDirection?: 'ltr' | 'rtl';
113
126
  }
114
127
  /** Frame-rate the loop is capped to when the OS requests reduced motion. */
115
128
  export declare const REDUCED_MOTION_FPS = 30;
@@ -162,12 +175,23 @@ export interface A11yTreeNode {
162
175
  export declare class Scene {
163
176
  private static webglCreator;
164
177
  private static webgpuManagerClass;
178
+ /** Upper bound (ms) on a single frame's `dt`. Caps the giant elapsed gap a
179
+ * backgrounded/refocused tab produces so physics advances at most one slow
180
+ * frame instead of the whole idle duration (~100ms ≈ 6 frames at 60fps). */
181
+ private static readonly MAX_FRAME_DT;
165
182
  static registerWebGLPointRendererCreator(creator: WebGLPointRendererCreator): void;
166
183
  static registerWebGPUParticleSystemManager(managerClass: any): void;
167
184
  private root;
168
185
  overlayRoot: Entity;
169
186
  private renderer;
170
187
  private isRunning;
188
+ /** Whether the canvas is at least partially in the viewport. When it scrolls
189
+ * fully off-screen the rAF loop pauses (stops rescheduling) instead of
190
+ * burning frames on a scene nobody can see; an IntersectionObserver resumes
191
+ * it on re-entry. Defaults true (and stays true where IntersectionObserver
192
+ * is unavailable, e.g. SSR/jsdom, so behavior is unchanged there). */
193
+ private _canvasOnScreen;
194
+ private _canvasObserver;
171
195
  private lastTime;
172
196
  canvas: HTMLCanvasElement;
173
197
  /**
@@ -201,10 +225,34 @@ export declare class Scene {
201
225
  maxFPS: number;
202
226
  /** Whether the OS prefers-reduced-motion setting auto-caps the loop. */
203
227
  respectReducedMotion: boolean;
228
+ /**
229
+ * Reading direction for accessibility tab/traversal order (`'ltr'` default,
230
+ * `'rtl'`). Controls the inline sort within a visual row in
231
+ * {@link enforceA11yDomOrder}. Set at runtime to re-flow tab order on the
232
+ * next sync (also trips a reorder).
233
+ */
234
+ get readingDirection(): 'ltr' | 'rtl';
235
+ set readingDirection(dir: 'ltr' | 'rtl');
236
+ private _readingDirection;
204
237
  /** Cached media-query list; `.matches` is read live each frame. */
205
238
  private reducedMotionQuery;
239
+ /** Cached `(forced-colors: active)` query (Windows High Contrast etc.). A
240
+ * canvas gets NO automatic forced-colors treatment from the browser (it's
241
+ * opaque pixels), so components must read {@link forcedColors} and repaint
242
+ * with system colors themselves; a change listener repaints idle scenes. */
243
+ private forcedColorsQuery;
244
+ private forcedColorsChangeHandler;
206
245
  /** True when the OS asks for reduced motion and we respect it. Read by the animation drivers. */
207
246
  get prefersReducedMotion(): boolean;
247
+ /**
248
+ * True when the OS is in a forced-colors mode (Windows High Contrast, and the
249
+ * `forced-colors: active` media feature generally). Canvas pixels are exempt
250
+ * from the browser's forced-colors remapping, so accessible components should
251
+ * read this and draw with CSS system colors (`CanvasText`, `Canvas`,
252
+ * `Highlight`, …) instead of their themed palette. Re-rendered automatically
253
+ * when the setting toggles.
254
+ */
255
+ get forcedColors(): boolean;
208
256
  /**
209
257
  * Throttle interval (ms) for the a11y/automation shadow sync. `0` = every
210
258
  * frame. See {@link SceneOptions.a11ySyncInterval}.
@@ -241,7 +289,32 @@ export declare class Scene {
241
289
  private frameHadAnimation;
242
290
  private frameHadInteractive;
243
291
  private resizeHandler;
292
+ /** Active `(resolution: Ndppx)` media query watching for a runtime DPR change
293
+ * (window moved between monitors, browser zoom) so the canvas backing store
294
+ * can be re-scaled — otherwise it stays rasterized at the old DPR and blurs.
295
+ * A resolution media query only fires when leaving its exact value, so the
296
+ * handler re-arms a fresh query for the new DPR each time. */
297
+ private dprMediaQuery;
298
+ /** For embedded (`disableWindowResize`) scenes: observes the canvas element so
299
+ * a CSS/layout-driven size change re-runs `resize()`. A window `resize`
300
+ * listener never fires for these (the window isn't what changed), so without
301
+ * this an embedded canvas stayed at its initial size forever. */
302
+ private canvasResizeObserver;
303
+ private dprChangeHandler;
244
304
  private focusedA11yElement;
305
+ /** Last geometry `syncOverlayGeometry` wrote, so an unchanged frame can skip the
306
+ * style writes entirely. Reset to `null` to force the next sync (a new overlay
307
+ * layer was created and has never been positioned). */
308
+ private _overlayGeometry;
309
+ /** Shadow elements the pointer is currently inside. Lets a removal that happens
310
+ * mid-hover synthesize the `pointerleave` the browser never sends for a
311
+ * detached element, so the entity doesn't keep its hover state. */
312
+ private readonly hoveredA11yElements;
313
+ /** Persistent tabindex=-1 element in a11yRoot. When the focused a11y mirror is
314
+ * pruned (virtualization/streaming/removal) while it holds focus, we move
315
+ * focus here instead of letting the browser drop it to <body> — keeping the
316
+ * screen-reader virtual cursor inside the scene's a11y region. */
317
+ private focusSentinel;
245
318
  private caretBlinkTimer;
246
319
  a11yNeedsReorder: boolean;
247
320
  private portalRoot;
@@ -252,6 +325,223 @@ export declare class Scene {
252
325
  private activePortalsPrevFrame;
253
326
  private portalEntities;
254
327
  private renderOrderCounter;
328
+ /**
329
+ * Monotonic render-frame counter, bumped once per authoritative `render()`
330
+ * pass. Entities stamp their per-frame world-matrix cache with this value and
331
+ * {@link Entity.getWorldTransform} trusts that cache only while it still
332
+ * matches, so a query outside the frame that produced it transparently falls
333
+ * back to the ancestor walk. Public for the same reason `Entity._getTrig`/
334
+ * `_setWorldCache` are: it is a cross-class render-internal contract.
335
+ */
336
+ currentFrame: number;
337
+ private _wasm;
338
+ private _transformBackend;
339
+ private _treeStore;
340
+ private _slotEntity;
341
+ private _wasmInputs;
342
+ private _wasmWorld;
343
+ private _structureVersion;
344
+ private _storeStructureVersion;
345
+ private _computeEntities;
346
+ private _computeEntitiesVersion;
347
+ /** Invalidate the resident WASM store layout; the next wasm-mode frame rebuilds
348
+ * it. Called by `Entity.add`/`remove` (topology changes only). */
349
+ markStructureChanged(): void;
350
+ /** The tree's ComputeParticleEntity instances, cached per structure version so
351
+ * a compute-free scene doesn't re-walk the whole tree every frame. */
352
+ private _computeEntitiesFor;
353
+ /** Which backend composes world matrices for the main render walk. */
354
+ get transformBackend(): 'js' | 'wasm';
355
+ /**
356
+ * Install (or clear) a WASM transform backend. Passing a backend switches the
357
+ * main render walk onto it; passing `null` reverts to the JS path. Synchronous
358
+ * and safe to call between frames — the next `render()` picks it up. Prefer
359
+ * {@link enableWasmTransforms} for the normal async hot-swap.
360
+ */
361
+ setTransformBackend(backend: WasmTransformBackend | null): void;
362
+ /**
363
+ * Asynchronously instantiate the WASM transform core and, on success, hot-swap
364
+ * the render walk onto it. Accepts whatever is convenient at the call site:
365
+ *
366
+ * ```ts
367
+ * // The common case — a bundler-emitted, co-located asset URL:
368
+ * await scene.enableWasmTransforms(new URL('./vectojs_core.wasm', import.meta.url));
369
+ * // …or a path string, a Response, or raw bytes you already have:
370
+ * await scene.enableWasmTransforms('/assets/vectojs_core.wasm');
371
+ * await scene.enableWasmTransforms(await fetch(url));
372
+ * await scene.enableWasmTransforms(myUint8Array);
373
+ * ```
374
+ *
375
+ * A URL/Response streams (compiles while it downloads, with a buffered
376
+ * fallback for a wrong MIME type); raw bytes instantiate directly. The Scene
377
+ * keeps rendering on the JS path until this resolves, and stays on JS if
378
+ * instantiation fails (CSP `wasm-unsafe-eval`, unsupported SIMD, corrupt or
379
+ * missing bytes, a 404) — failure is the default state, not an error path.
380
+ * Resolves `true` if WASM is now active, `false` if the JS path remains.
381
+ */
382
+ enableWasmTransforms(source: WasmModuleSource): Promise<boolean>;
383
+ private _hitWasm;
384
+ private _hitGridFrame;
385
+ private _hitGridOk;
386
+ private _hitSlotEntity;
387
+ private _hitBoundless;
388
+ /** Which backend answers `findEntityAt` for the main tree. */
389
+ get hitTestBackend(): 'js' | 'wasm';
390
+ /** Install (or clear) a WASM hit-test backend directly. Prefer
391
+ * {@link enableWasmHitTest} for the normal async hot-swap. */
392
+ setHitTestBackend(backend: HitTestBackend | null): void;
393
+ /**
394
+ * Asynchronously instantiate the WASM hit-test core and, on success, hot-swap
395
+ * `findEntityAt` onto it. Accepts the same source shapes as
396
+ * {@link enableWasmTransforms} (URL, path string, Response, or raw bytes).
397
+ * Stays on the JS walk if instantiation fails — failure is the default
398
+ * state, not an error path. Resolves `true` if WASM is now active.
399
+ */
400
+ enableWasmHitTest(source: HitModuleSource): Promise<boolean>;
401
+ /**
402
+ * Refresh the hit-test grid for the CURRENT tree state if it is stale (a
403
+ * structural or transform change may have happened since the last build —
404
+ * there is no cheap "nothing moved" shortcut for a spatial index the way
405
+ * there is for the transform store's topology-only run table, since ANY
406
+ * entity moving invalidates its AABB, not just add/remove/reparent; the
407
+ * measured build cost is cheap enough to redo per call). Returns `false`
408
+ * (grid untrustworthy — caller must use the JS walk) when there is no
409
+ * backend or the build overflowed its item budget.
410
+ */
411
+ private _ensureHitGrid;
412
+ /**
413
+ * `findEntityAt`'s WASM-accelerated path for the main tree. Scans only the
414
+ * queried cell's candidates (confirming each against its own AABB and precise
415
+ * `isPointInside`) merged against the (typically empty or tiny) list of
416
+ * entities with no `getBounds()`, taking whichever confirmed match has the
417
+ * higher pre-order index — see hit-store.ts for why that is exactly
418
+ * equivalent to findHitRecursively's topmost-hit priority. Always
419
+ * conclusive: returns the correct entity or `null`, never "inconclusive".
420
+ */
421
+ private _findEntityAtWasm;
422
+ private _animWasm;
423
+ private _activeDriverEntities;
424
+ private _springEntities;
425
+ private _springProps;
426
+ private _springDrivers;
427
+ private _tweenEntities;
428
+ private _tweenProps;
429
+ private _tweenDrivers;
430
+ /**
431
+ * Minimum number of batchable (spring, or named-easing tween) active drivers
432
+ * before a frame engages the WASM batch path at all; below it, every driver
433
+ * ticks on the normal JS per-entity path, unmodified.
434
+ *
435
+ * Re-measured on the INTEGRATED path (benchmarks/anim-wasm-scene, real
436
+ * Chrome 150 / Firefox 153, 2026-07-24 — correctness verified 0 mismatches
437
+ * across all three kinds before any of these numbers were trusted): the
438
+ * isolated kernel spike's "<100 drivers, wins everywhere" verdict did NOT
439
+ * survive integration, and neither did the first integrated pass's single
440
+ * gate-count verdict once broken out by driver kind. On Chrome, spring and
441
+ * mixed drivers are a real ~1.4–2.3× win from n=128 up through the tested
442
+ * ceiling of 16384, but pure-tween drivers are a LOSS at n=128 (0.71×,
443
+ * i.e. ~40% slower than the JS path) and only turn net-positive around
444
+ * n≈256 (1.52×). A single scalar gate can't be tight for spring/mixed
445
+ * without occasionally opening early on a tween-heavy scene and making it
446
+ * slower — 256 is chosen to keep the gate net-positive across all three
447
+ * kinds rather than optimal for any one of them; a kind-aware gate (see
448
+ * `_tickBatchedDrivers`'s per-kind arrays, which already separate spring
449
+ * from tween) would recover the 128–255 spring/mixed win without the
450
+ * tween regression, but that's a larger change than this measurement pass
451
+ * covers. On Firefox it is a net loss at every driver count measured, up
452
+ * to 16384 — not an allocation artifact (confirmed after removing all
453
+ * per-frame allocation from the gather/scatter path); SpiderMonkey's
454
+ * wasm-boundary/property-dispatch cost for this shape of call appears to
455
+ * structurally exceed the saving, at least at the scales tested here.
456
+ *
457
+ * 256 is set as a Chrome-oriented default so an app that opts in (this
458
+ * path is never engaged without an explicit {@link enableWasmAnimBatching}
459
+ * call) sees the gate open only where it reliably helps on Chromium,
460
+ * regardless of whether the scene's active drivers are spring, tween, or
461
+ * a mix of both. Unlike G1 (safe to default on everywhere) and G3 (opt-in,
462
+ * but a reliable win once its own gate condition holds), G2 has no
463
+ * threshold that is safe on every engine — raise or lower this per your
464
+ * own target browser mix and driver-kind distribution, or leave WASM
465
+ * animation batching disabled entirely on a Firefox-heavy audience.
466
+ */
467
+ animDriverGateCount: number;
468
+ /** Which backend advances active property drivers on the current gate
469
+ * decision. Reflects only whether a backend is installed — the per-frame
470
+ * gate can still choose the JS path even when this reads `'wasm'`. */
471
+ get animBackend(): 'js' | 'wasm';
472
+ /** Install (or clear) a WASM batched-animation backend directly. Prefer
473
+ * {@link enableWasmAnimBatching} for the normal async hot-swap. */
474
+ setAnimBackend(backend: AnimBackend | null): void;
475
+ /**
476
+ * Asynchronously instantiate the WASM batched-animation core and, on
477
+ * success, make it available to the per-frame gate (see
478
+ * {@link animDriverGateCount}). Accepts the same source shapes as
479
+ * {@link enableWasmTransforms}. Stays on the JS tick loop if instantiation
480
+ * fails — failure is the default state, not an error path. Resolves `true`
481
+ * if WASM is now available (not necessarily active every frame).
482
+ */
483
+ enableWasmAnimBatching(source: AnimModuleSource): Promise<boolean>;
484
+ private _particleWasm;
485
+ /** Which backend runs the CPU particle simulation. Reflects only whether a
486
+ * backend is installed (the WebGPU compute path, when active, is used first
487
+ * regardless). */
488
+ get particleSimBackend(): 'js' | 'wasm';
489
+ /** Install (or clear) a WASM particle backend directly. Prefer
490
+ * {@link enableWasmParticles} for the normal async hot-swap. */
491
+ setParticleBackend(backend: ParticleBackend | null): void;
492
+ /**
493
+ * Asynchronously instantiate the WASM particle core and, on success, use it
494
+ * for the CPU particle fallback. Accepts the same source shapes as
495
+ * {@link enableWasmTransforms}. Stays on the JS `updateCPU` path if
496
+ * instantiation fails — failure is the default state, not an error path.
497
+ * Resolves `true` if WASM is now active.
498
+ */
499
+ enableWasmParticles(source: ParticleModuleSource): Promise<boolean>;
500
+ /** Internal: called by `Entity._spawnDriver` when a new property driver
501
+ * starts. See {@link _activeDriverEntities}. */
502
+ _registerActiveDriverEntity(entity: Entity): void;
503
+ /**
504
+ * Drop `entity` and its whole subtree from the batched-driver candidate set.
505
+ * Called by {@link remove}/{@link hideOverlay} on detach: without this a
506
+ * removed-but-still-animating entity stays pinned in the Set (a leak) and its
507
+ * drivers keep ticking every frame even though it is off-tree. If it is later
508
+ * re-added, {@link registerActiveDriverSubtree} re-registers any node that
509
+ * still has live drivers, so the motion resumes.
510
+ */
511
+ private unregisterActiveDriverSubtree;
512
+ /**
513
+ * Re-register every node in `entity`'s subtree that still has live property
514
+ * drivers. Called by {@link add}/{@link showOverlay} so re-attaching a subtree
515
+ * that was removed mid-animation resumes its batched drivers (they were
516
+ * dropped from the candidate set on removal, but the driver state still lives
517
+ * on each entity).
518
+ */
519
+ private registerActiveDriverSubtree;
520
+ /**
521
+ * Advance every registered entity's active drivers for this frame, batching
522
+ * whichever are batchable (`SpringDriver`; `TweenDriver` with a named
523
+ * easing) through one WASM call each when the driver-count gate is open, and
524
+ * ticking the rest (a `TweenDriver` using a custom `EasingFn`) directly in
525
+ * JS regardless of the gate. A "claimed" entity must have ALL its drivers
526
+ * advanced here so it can be safely stamped `_driversTickedFrame` — leaving
527
+ * one unclaimed would silently stall it, since `tickDrivers()` skips the
528
+ * whole entity once stamped.
529
+ *
530
+ * Must run before ANY entity's `update()`/`tickDrivers()` this frame (see
531
+ * the call site in {@link render}) — the same ordering constraint G1 Stage 4
532
+ * discovered: a value this pass writes must be final before anything reads
533
+ * it, including the JS-mode interleaved walk and the WASM-mode transform
534
+ * pre-pass.
535
+ */
536
+ private _tickBatchedDrivers;
537
+ /**
538
+ * Compose the whole main tree's world matrices through the resident WASM store
539
+ * and return the world-matrix views for the render walk to read. Rebuilds the
540
+ * store layout (slots + runs) only when the tree structure changed since the
541
+ * last rebuild; otherwise it just gathers current transforms into the resident
542
+ * input view and runs the kernel. Returns `null` if there is no backend.
543
+ */
544
+ private _syncWasmStore;
255
545
  /**
256
546
  * Authoritative paint order for semantic nodes discovered during the main
257
547
  * render. A node may not have a DOM projection until the following a11y
@@ -261,6 +551,8 @@ export declare class Scene {
261
551
  private a11yRenderOrders;
262
552
  private pointRenderer;
263
553
  private glCanvas;
554
+ private glContextLostHandler;
555
+ private glContextRestoredHandler;
264
556
  private debugA11y;
265
557
  width: number;
266
558
  height: number;
@@ -285,6 +577,9 @@ export declare class Scene {
285
577
  private mouseY;
286
578
  private pointerMoveListener;
287
579
  private pointerLeaveListener;
580
+ /** Element the pointer listeners are bound to (parent container if present,
581
+ * else the canvas). Stored so `destroy()` detaches from the same element. */
582
+ private pointerEventTarget;
288
583
  private hasWarnedZeroSize;
289
584
  private fontLoadHandler;
290
585
  /** Toggle development-mode runtime warnings globally. */
@@ -296,8 +591,46 @@ export declare class Scene {
296
591
  /** @internal Periodic dev checks — called once per frame in dev mode. */
297
592
  private _devRunChecks;
298
593
  constructor(canvas: HTMLCanvasElement, options?: SceneOptions);
594
+ /**
595
+ * Arm a `(resolution: Ndppx)` media query for the current devicePixelRatio and
596
+ * re-apply the canvas scale when it changes. Such a query only fires when the
597
+ * DPR leaves its exact value, so on each change the old query is detached and
598
+ * a fresh one is armed for the new DPR. Re-runs `resize(width, height)` (which
599
+ * re-scales the backing store via the renderer) so text/vectors stay crisp
600
+ * after a monitor move or zoom. No-op without `matchMedia`.
601
+ */
602
+ private watchDevicePixelRatio;
603
+ /**
604
+ * Recover the WebGL point layer from a GPU context loss (driver TDR reset,
605
+ * tab backgrounded on mobile, GPU switch). Two things are required:
606
+ *
607
+ * 1. The `webglcontextlost` handler MUST call `preventDefault()`, or the
608
+ * browser never fires `webglcontextrestored` and the layer is blank
609
+ * forever. While lost, the old renderer's GL calls are silently ignored,
610
+ * so we drop it and the render loop simply skips the point layer.
611
+ * 2. On `webglcontextrestored`, all GL objects (programs, buffers, textures)
612
+ * are gone, so we rebuild the renderer from scratch via `Scene.webglCreator`
613
+ * on the same canvas, restore DPR/size, and repaint.
614
+ */
615
+ private setupGLContextRecovery;
299
616
  private endContentSelectionDrag;
300
617
  private releaseContentSelectionForRebuild;
618
+ /**
619
+ * Rebuild a content-projection element's DOM (`rebuild`) while preserving a
620
+ * text selection the user made inside it. A streaming message replaces its
621
+ * projection children on every appended chunk; without this, a selection in
622
+ * the UNCHANGED prefix is wiped on each frame ("can't select text in a
623
+ * message still receiving tokens"). We snapshot the selection's anchor/focus
624
+ * as linear character offsets within `el` before the rebuild and re-resolve
625
+ * them against the new DOM after, clamped to the new text length.
626
+ *
627
+ * Only fires when `el` owns the current selection and there is no active drag
628
+ * (mid-drag the browser is authoritative). The virtualization case — where
629
+ * `el` itself is removed from the DOM — is out of scope here (the node is
630
+ * genuinely freed; the browser clears the selection and there is nothing to
631
+ * restore against).
632
+ */
633
+ private preserveContentSelectionAcrossRebuild;
301
634
  /**
302
635
  * Expose the underlying {@link IRenderer} for advanced direct-draw operations.
303
636
  *
@@ -319,6 +652,14 @@ export declare class Scene {
319
652
  add(entity: Entity): this;
320
653
  private clearContentGridState;
321
654
  private removeA11yRecursively;
655
+ /**
656
+ * If `el` is about to be removed from the DOM while it holds browser focus,
657
+ * move focus to the a11y focus sentinel first. Removing the active element
658
+ * otherwise drops focus to `<body>`, which pulls a screen reader out of the
659
+ * scene's a11y region and back to the top of the page — the classic
660
+ * "lost my place on scroll/stream" bug for virtualized/recycled controls.
661
+ */
662
+ private preserveFocusOnRemoval;
322
663
  /**
323
664
  * Remove a top-level entity from the scene graph and clean up its
324
665
  * accessibility shadow elements recursively.
@@ -359,6 +700,14 @@ export declare class Scene {
359
700
  start(): void;
360
701
  /** Schedule the next frame, or no-op where `requestAnimationFrame` is absent (SSR). */
361
702
  private scheduleFrame;
703
+ /**
704
+ * Observe whether the canvas is on-screen so the rAF loop can pause when it
705
+ * scrolls fully out of view (a dashboard tab, a chart below the fold) and
706
+ * resume when it returns — instead of running the full update/render every
707
+ * frame for a scene nobody can see. No-op (stays "on screen") where
708
+ * `IntersectionObserver` is unavailable, so SSR/jsdom behavior is unchanged.
709
+ */
710
+ private watchCanvasVisibility;
362
711
  /**
363
712
  * Halt the render loop after the current frame completes.
364
713
  *
@@ -434,6 +783,16 @@ export declare class Scene {
434
783
  private getContentMetricScaleX;
435
784
  private scheduleContentGridCalibration;
436
785
  private enforceA11yDomOrder;
786
+ /**
787
+ * Reorder `normalElements` (in place) into visual reading order using the
788
+ * world positions `syncA11y` already wrote to each element's inline style
789
+ * (`top`/`left`/`height`). Elements are grouped into rows top-to-bottom (an
790
+ * element belongs to the current row while its top is above the row's
791
+ * running bottom edge), then sorted within a row by `left` — ascending for
792
+ * `'ltr'`, descending for `'rtl'`. The sort is stable, so entities at the
793
+ * same position keep their scene-graph (collection) order as a tiebreak.
794
+ */
795
+ private sortNormalElementsVisually;
437
796
  /** Keep DOM/WebGL overlay layers aligned with the canvas's CSS box. */
438
797
  private syncOverlayGeometry;
439
798
  getA11yTree(): A11yTreeNode[];
@@ -462,6 +821,13 @@ export declare class Scene {
462
821
  * Manually resize the Scene's viewport.
463
822
  */
464
823
  resize(width: number, height: number): void;
824
+ /** Effective device pixel ratio, matching CanvasRenderer: real DPR clamped to
825
+ * `maxDPR` when set. */
826
+ private effectiveDPR;
827
+ /** Size the WebGPU particle canvas: backing store at logical × DPR, CSS box at
828
+ * the logical size. Sizing the backing store in logical px (the old
829
+ * behavior) left it rasterized at 1× and CSS-stretched — blurry on HiDPI. */
830
+ private sizeGpuCanvas;
465
831
  /**
466
832
  * Gets the accessibility DOM element projected for the given entity ID.
467
833
  */
@@ -483,4 +849,18 @@ export declare class Scene {
483
849
  private recreateWebGPUDeviceWithRetry;
484
850
  private renderCPUParticles;
485
851
  private findHitRecursively;
852
+ /** Whether `node` opts out of being a pointer hit target: a disabled control
853
+ * or an explicit `pointerEvents: 'none'` in its a11y attributes. Its children
854
+ * are still walked (a transparent container can hold hittable descendants). */
855
+ private isPointerTransparent;
856
+ /**
857
+ * Whether a confirmed geometric hit on `node` at world `(x, y)` is a REAL hit,
858
+ * applying the same visibility/input gating as {@link findHitRecursively} but
859
+ * from a flat candidate (the WASM grid has no recursion clip-stack): the node
860
+ * and all ancestors are visible (`opacity > 0`), the point lies inside every
861
+ * `clipChildren` ancestor's world box, and the node isn't pointer-transparent
862
+ * (disabled / `pointerEvents: 'none'`). Keeps the WASM and JS hit paths in
863
+ * lockstep so they return the same entity.
864
+ */
865
+ private isHitEligible;
486
866
  }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * WASM batched-animation backend: advances every currently-active `SpringDriver`/
3
+ * `TweenDriver` in one call each (`spring_step`/`tween_step`), instead of the JS
4
+ * per-driver `driver.tick()` loop. This is an invisible accelerator — the JS tick
5
+ * loop ({@link Entity.tickDrivers}) is the permanent fallback, so a caller that
6
+ * cannot instantiate WASM, or whose active-driver count never crosses the gate
7
+ * (see `Scene._tickBatchedDrivers`), simply keeps using it.
8
+ *
9
+ * The kernel (`crates/vectojs-core-rs/src/anim.rs`) is bit-identical to
10
+ * `SpringPhysics.update` for springs; tweens match to ~1e-9 (not bit-exact — a
11
+ * `Math.pow`-vs-`powi` ULP difference recorded as a spike finding), which is why
12
+ * a WASM-batched tween needs a `retarget`-style resync, not raw byte reuse.
13
+ *
14
+ * Unlike the transform/hit-test stores, this backend holds no cross-frame
15
+ * residency: every qualifying frame re-gathers ALL currently-active batchable
16
+ * drivers into a fresh dense pack (see {@link ensure} + the spring/tween input
17
+ * views), runs the kernel once, and scatters results straight back out. This
18
+ * keeps the design robust to drivers joining/leaving between frames and to the
19
+ * gate itself flipping the JS/WASM path frame-to-frame — there is no persistent
20
+ * wasm-side state to invalidate.
21
+ */
22
+ export interface SpringView {
23
+ val: Float64Array;
24
+ target: Float64Array;
25
+ vel: Float64Array;
26
+ stiff: Float64Array;
27
+ damp: Float64Array;
28
+ mass: Float64Array;
29
+ }
30
+ export interface TweenView {
31
+ from: Float64Array;
32
+ to: Float64Array;
33
+ elapsed: Float64Array;
34
+ dur: Float64Array;
35
+ delay: Float64Array;
36
+ ease: Float64Array;
37
+ val: Float64Array;
38
+ }
39
+ export declare class AnimBackend {
40
+ private readonly ex;
41
+ private springCap;
42
+ private tweenCap;
43
+ private sv;
44
+ private tv;
45
+ constructor(instance: WebAssembly.Instance);
46
+ /** The resident spring SoA input/output views, valid until the next capacity
47
+ * growth. Write gathered driver state here before calling {@link stepSprings}. */
48
+ springView(): SpringView;
49
+ /** The resident tween SoA input/output views, valid until the next capacity
50
+ * growth. Write gathered driver state here before calling {@link stepTweens}. */
51
+ tweenView(): TweenView;
52
+ /**
53
+ * Size (and grow, if needed) capacity for `springCount` springs and
54
+ * `tweenCount` tweens. Call this BEFORE writing into {@link springView}/
55
+ * {@link tweenView} — a capacity growth detaches the previous views, so
56
+ * writing first and sizing after would write into a stale buffer.
57
+ */
58
+ ensure(springCount: number, tweenCount: number): void;
59
+ /** Advance `count` springs (from index 0) by `dtMs` milliseconds, in place. */
60
+ stepSprings(dtMs: number, count: number): void;
61
+ /** Advance `count` tweens (from index 0) by `dtMs` milliseconds, writing `val`. */
62
+ stepTweens(dtMs: number, count: number): void;
63
+ private refreshViews;
64
+ }
65
+ /**
66
+ * Instantiate synchronously (Node/tests, or a worker). Rejected on the browser
67
+ * main thread for modules >4 KB — use {@link instantiateAsync} there. Returns
68
+ * `null` if compilation/instantiation throws, so callers fall back to JS.
69
+ */
70
+ export declare function instantiateSync(bytes: BufferSource): AnimBackend | null;
71
+ /**
72
+ * Instantiate asynchronously (browser main thread). Returns `null` on any
73
+ * failure — CSP `wasm-unsafe-eval`, unsupported, corrupt/missing bytes — so the
74
+ * caller keeps using the JS path.
75
+ */
76
+ export declare function instantiateAsync(bytes: BufferSource): Promise<AnimBackend | null>;
77
+ /** Anything the anim core can be loaded from, matching the transform/hit-test
78
+ * cores' loading ergonomics. */
79
+ export type AnimModuleSource = BufferSource | string | URL | Response | Promise<Response>;
80
+ /**
81
+ * Instantiate from a URL/Response using streaming compilation when the
82
+ * platform supports it, falling back to fetch → arrayBuffer → instantiate when
83
+ * unavailable or the response's MIME type is rejected. Returns `null` on any
84
+ * failure so the caller keeps the JS path.
85
+ */
86
+ export declare function instantiateStreaming(source: string | URL | Response | Promise<Response>): Promise<AnimBackend | null>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Resolved URL of the prebuilt WebAssembly core, co-located with this module in
3
+ * the published package (`dist/wasm/vectojs_core.wasm`). Import it and hand it
4
+ * straight to any of the Scene WASM opt-ins:
5
+ *
6
+ * ```ts
7
+ * import { coreWasmUrl } from '@vectojs/core/wasm';
8
+ * await scene.enableWasmTransforms(coreWasmUrl);
9
+ * await scene.enableWasmAnimBatching(coreWasmUrl);
10
+ * await scene.enableWasmHitTest(coreWasmUrl);
11
+ * ```
12
+ *
13
+ * Why a helper module rather than a bare specifier: `new URL('@vectojs/core/…',
14
+ * import.meta.url)` does NOT work — `new URL` only resolves *relative* refs
15
+ * against a base, and a bare package specifier is not relative, so it never
16
+ * goes through package `exports`. Resolving `./vectojs_core.wasm` from *inside*
17
+ * the package (where this file sits next to the binary) is the only form that
18
+ * both native ESM and bundlers resolve correctly.
19
+ *
20
+ * The WASM is an optional accelerator: if a bundler tree-shakes this URL away or
21
+ * a runtime can't fetch it, every `enableWasm*` call simply returns `false` and
22
+ * the scene stays on the identical-output JS path. Nothing here is required for
23
+ * `@vectojs/core` to work.
24
+ */
25
+ export declare const coreWasmUrl: URL;
@@ -0,0 +1,7 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});const __vecto_cjs_url=require("url").pathToFileURL(__filename).href;
2
+
3
+ // src/wasm/asset.ts
4
+ var coreWasmUrl = new URL("./vectojs_core.wasm", __vecto_cjs_url);
5
+
6
+
7
+ exports.coreWasmUrl = coreWasmUrl;
@@ -0,0 +1,5 @@
1
+ // src/wasm/asset.ts
2
+ var coreWasmUrl = new URL("./vectojs_core.wasm", import.meta.url);
3
+ export {
4
+ coreWasmUrl
5
+ };