@vectojs/core 1.9.2 → 1.11.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/index.mjs CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  isSafeUrl,
13
13
  parseColorToRGBA,
14
14
  sanitizeUrl
15
- } from "./chunk-WT2XZHJQ.mjs";
15
+ } from "./chunk-XZEPHCDZ.mjs";
16
16
  import {
17
17
  Easing,
18
18
  Entity,
@@ -27,7 +27,7 @@ import {
27
27
  cssLineBoxBaseline,
28
28
  isTweenConfig,
29
29
  prepareContentGrid
30
- } from "./chunk-J2P6QZJV.mjs";
30
+ } from "./chunk-XIEQHSBB.mjs";
31
31
  import {
32
32
  ArabicShaper,
33
33
  BidiResolver,
@@ -708,6 +708,8 @@ var Scene = class _Scene {
708
708
  width;
709
709
  height;
710
710
  disableWindowResize = false;
711
+ /** See {@link SceneOptions.maxDPR}. `undefined` = uncapped (real DPR). */
712
+ maxDPR;
711
713
  // WebGPU properties
712
714
  destroyed = false;
713
715
  device = null;
@@ -733,10 +735,72 @@ var Scene = class _Scene {
733
735
  pointerLeaveListener = null;
734
736
  hasWarnedZeroSize = false;
735
737
  fontLoadHandler = null;
738
+ // ── Dev-mode warning infrastructure ──────────────────────────────
739
+ //
740
+ // Enable with `Scene.devMode = true` or by setting `globalThis.__DEV__`.
741
+ // Auto-detected when `NODE_ENV === 'development'`.
742
+ //
743
+ // Checks run once every ~120 frames (~2s at 60fps) to keep overhead
744
+ // negligible even when dev mode is on.
745
+ /** Toggle development-mode runtime warnings globally. */
746
+ static devMode = false;
747
+ static _devModeDetected() {
748
+ if (_Scene.devMode) return true;
749
+ const gp = typeof globalThis !== "undefined" ? globalThis : void 0;
750
+ if (gp?.__DEV__) return true;
751
+ if (gp?.process?.env?.NODE_ENV === "development") return true;
752
+ return false;
753
+ }
754
+ _devActive;
755
+ _devFrameCount = 0;
756
+ _devWarn(message) {
757
+ if (!this._devActive) return;
758
+ console.warn(`[vectojs/dev] ${message}`);
759
+ }
760
+ /** @internal Periodic dev checks — called once per frame in dev mode. */
761
+ _devRunChecks() {
762
+ this._devFrameCount++;
763
+ if (this._devFrameCount % 120 !== 0) return;
764
+ if (this.a11yElements) {
765
+ let interactiveCount = 0;
766
+ const walk = (node) => {
767
+ if (node.interactive && node.width > 0) interactiveCount++;
768
+ for (const c of node.children) walk(c);
769
+ };
770
+ walk(this.root);
771
+ for (const c of this.overlayRoot.children) walk(c);
772
+ const shadowCount = this.a11yElements.size;
773
+ if (shadowCount > interactiveCount + 2) {
774
+ this._devWarn(
775
+ `a11yElements (${shadowCount}) exceeds interactive entities (${interactiveCount}). Call scene.detachA11y(entity) before removing interactive children from the tree, or their shadow nodes leak.`
776
+ );
777
+ }
778
+ }
779
+ let checked = 0;
780
+ const walkProjections = (node) => {
781
+ if (checked > 10) return;
782
+ const proj = node.getContentProjection?.();
783
+ if (proj?.text && proj.selectable !== false) {
784
+ const el = this.contentElements?.get(node.id);
785
+ if (el) {
786
+ const projectedText = el.textContent || "";
787
+ if (projectedText !== "" && projectedText !== proj.text) {
788
+ this._devWarn(
789
+ `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.`
790
+ );
791
+ }
792
+ }
793
+ }
794
+ checked++;
795
+ for (const c of node.children) walkProjections(c);
796
+ };
797
+ walkProjections(this.root);
798
+ }
736
799
  constructor(canvas, options = {}) {
737
800
  this.canvas = canvas;
738
801
  this.debugA11y = options.debugA11y ?? false;
739
802
  this.disableWindowResize = options.disableWindowResize ?? false;
803
+ this.maxDPR = options.maxDPR;
740
804
  if (this.disableWindowResize) {
741
805
  const styleWidth = parseInlinePx(canvas.style?.width);
742
806
  const styleHeight = parseInlinePx(canvas.style?.height);
@@ -754,6 +818,7 @@ var Scene = class _Scene {
754
818
  this.particleBackend = options.particleBackend ?? "auto";
755
819
  this.a11ySyncInterval = options.a11ySyncInterval ?? 0;
756
820
  this.contentProjectionEnabled = options.contentProjection ?? true;
821
+ this._devActive = _Scene._devModeDetected();
757
822
  this.reducedMotionQuery = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
758
823
  this.root = new class RootEntity extends Entity {
759
824
  isPointInside() {
@@ -777,7 +842,8 @@ var Scene = class _Scene {
777
842
  } else {
778
843
  this.renderer = new CanvasRenderer(
779
844
  canvas,
780
- this.disableWindowResize ? { width: this.width, height: this.height } : void 0
845
+ this.disableWindowResize ? { width: this.width, height: this.height } : void 0,
846
+ this.maxDPR
781
847
  );
782
848
  }
783
849
  if (typeof document !== "undefined") {
@@ -903,6 +969,7 @@ var Scene = class _Scene {
903
969
  if (canvas.parentElement) canvas.parentElement.appendChild(gl);
904
970
  const pr = _Scene.webglCreator ? _Scene.webglCreator(gl) : null;
905
971
  if (pr) {
972
+ pr.maxDPR = this.maxDPR;
906
973
  pr.resize(this.width, this.height);
907
974
  this.glCanvas = gl;
908
975
  this.pointRenderer = pr;
@@ -1268,6 +1335,9 @@ var Scene = class _Scene {
1268
1335
  el.addEventListener("click", (e) => {
1269
1336
  node.dispatchEvent(new VectoJSEvent("click", node, e));
1270
1337
  });
1338
+ el.addEventListener("dblclick", (e) => {
1339
+ node.dispatchEvent(new VectoJSEvent("dblclick", node, e));
1340
+ });
1271
1341
  el.addEventListener("mouseenter", (e) => {
1272
1342
  if (this.debugA11y) el.style.backgroundColor = "rgba(56, 189, 248, 0.2)";
1273
1343
  node.dispatchEvent(new VectoJSEvent("hover", node, e, false));
@@ -2284,6 +2354,13 @@ var Scene = class _Scene {
2284
2354
  node.update(dt, time);
2285
2355
  if (!walkHadAnimation && node.hasPendingAnimations()) walkHadAnimation = true;
2286
2356
  if (!walkHadInteractive && node.interactive) walkHadInteractive = true;
2357
+ if (this._devActive && this._devFrameCount % 120 === 0) {
2358
+ if (node.update !== Entity.prototype.update && node.hasPendingAnimations === Entity.prototype.hasPendingAnimations) {
2359
+ this._devWarn(
2360
+ `Entity "${node.id}" overrides update() but not hasPendingAnimations(). Custom motion in update() without overriding hasPendingAnimations() causes the idle throttle to drop the animation to ~2fps. Override hasPendingAnimations() to return true while motion is in flight.`
2361
+ );
2362
+ }
2363
+ }
2287
2364
  }
2288
2365
  const cos = Math.cos(node.rotation);
2289
2366
  const sin = Math.sin(node.rotation);
@@ -2414,6 +2491,10 @@ var Scene = class _Scene {
2414
2491
  this.pointRenderer?.flush();
2415
2492
  }
2416
2493
  renderer.present?.();
2494
+ if (this._devActive) {
2495
+ this._devFrameCount++;
2496
+ this._devRunChecks();
2497
+ }
2417
2498
  }
2418
2499
  /**
2419
2500
  * Export the current scene state to a lightweight, flat SVG XML string.
@@ -2431,9 +2512,13 @@ var Scene = class _Scene {
2431
2512
  this.height = height;
2432
2513
  this.contentFontEpoch++;
2433
2514
  if (typeof this.renderer.resize === "function") {
2515
+ if ("maxDPR" in this.renderer) this.renderer.maxDPR = this.maxDPR;
2434
2516
  this.renderer.resize(width, height);
2435
2517
  }
2436
- this.pointRenderer?.resize(width, height);
2518
+ if (this.pointRenderer) {
2519
+ this.pointRenderer.maxDPR = this.maxDPR;
2520
+ this.pointRenderer.resize(width, height);
2521
+ }
2437
2522
  if (this.gpuCanvas) {
2438
2523
  this.gpuCanvas.width = width;
2439
2524
  this.gpuCanvas.height = height;
@@ -3,6 +3,15 @@ export declare class CanvasRenderer implements IRenderer {
3
3
  private ctx;
4
4
  private width;
5
5
  private height;
6
+ /**
7
+ * Cap on the effective device pixel ratio applied by the constructor and
8
+ * {@link resize}. `undefined` (default) uses the real, uncapped
9
+ * `devicePixelRatio` — unchanged from prior versions. Set directly, or via
10
+ * the constructor's third argument; `Scene` (see `SceneOptions.maxDPR`)
11
+ * keeps this in sync on every {@link resize} call, since the real DPR can
12
+ * change at runtime (e.g. a window dragged between displays).
13
+ */
14
+ maxDPR?: number;
6
15
  /**
7
16
  * Max circles per batched `fill()`. A single Canvas 2D `fill()` over a path is
8
17
  * superlinear in sub-path count, so an unbounded batch is *slower* than many
@@ -21,11 +30,12 @@ export declare class CanvasRenderer implements IRenderer {
21
30
  * fullscreen canvas and sizes to the window — pass this for embedded /
22
31
  * custom-container canvases (the Scene does when `disableWindowResize` is
23
32
  * set) so the canvas's own dimensions aren't clobbered by the window's.
33
+ * @param maxDPR - See {@link maxDPR}.
24
34
  */
25
35
  constructor(canvas: HTMLCanvasElement, size?: {
26
36
  width: number;
27
37
  height: number;
28
- });
38
+ }, maxDPR?: number);
29
39
  /**
30
40
  * Expose the underlying `CanvasRenderingContext2D` for operations not
31
41
  * covered by the {@link IRenderer} interface.
@@ -33,6 +43,8 @@ export declare class CanvasRenderer implements IRenderer {
33
43
  * @returns The raw 2D rendering context.
34
44
  */
35
45
  getContext(): CanvasRenderingContext2D;
46
+ /** Real `devicePixelRatio`, clamped to {@link maxDPR} when set. */
47
+ private effectiveDPR;
36
48
  /**
37
49
  * Resize the backing canvas buffer and re-apply DPR scaling.
38
50
  *
@@ -7,6 +7,18 @@
7
7
  export interface PointRenderer {
8
8
  /** Resize the backing buffer + GL viewport to a logical `w × h` (DPR applied). */
9
9
  resize(width: number, height: number): void;
10
+ /**
11
+ * Cap on the effective device pixel ratio applied by {@link resize}.
12
+ * `undefined` (default) uses the real, uncapped `devicePixelRatio`. Set
13
+ * before calling `resize()` for it to take effect on that call (matches
14
+ * {@link import('../tree/Scene').SceneOptions.maxDPR} — `Scene` sets this
15
+ * once at construction and again before every `resize()` call, since a
16
+ * factory function has no other way to receive the option: the WebGL point
17
+ * layer's creator is a plain `(canvas) => PointRenderer` registered once by
18
+ * `@vectojs/core`'s module init, with no room for a per-Scene constructor
19
+ * argument).
20
+ */
21
+ maxDPR?: number;
10
22
  /** Begin a frame: reset the accumulated primitive buffers. */
11
23
  begin(): void;
12
24
  /** Add one circle in world (CSS-pixel) coordinates; `alpha` multiplies the color's. */
package/dist/renderer.js CHANGED
@@ -4,11 +4,11 @@
4
4
 
5
5
 
6
6
 
7
- var _chunkBPMNCGU7js = require('./chunk-BPMNCGU7.js');
7
+ var _chunkORCPCIJ7js = require('./chunk-ORCPCIJ7.js');
8
8
 
9
9
 
10
10
 
11
11
 
12
12
 
13
13
 
14
- exports.CanvasRenderer = _chunkBPMNCGU7js.CanvasRenderer; exports.SVGRenderer = _chunkBPMNCGU7js.SVGRenderer; exports.WebGPUParticleSystemManager = _chunkBPMNCGU7js.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkBPMNCGU7js.createWebGLPointRenderer; exports.parseColorToRGBA = _chunkBPMNCGU7js.parseColorToRGBA;
14
+ exports.CanvasRenderer = _chunkORCPCIJ7js.CanvasRenderer; exports.SVGRenderer = _chunkORCPCIJ7js.SVGRenderer; exports.WebGPUParticleSystemManager = _chunkORCPCIJ7js.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkORCPCIJ7js.createWebGLPointRenderer; exports.parseColorToRGBA = _chunkORCPCIJ7js.parseColorToRGBA;
package/dist/renderer.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  WebGPUParticleSystemManager,
5
5
  createWebGLPointRenderer,
6
6
  parseColorToRGBA
7
- } from "./chunk-WT2XZHJQ.mjs";
7
+ } from "./chunk-XZEPHCDZ.mjs";
8
8
  export {
9
9
  CanvasRenderer,
10
10
  SVGRenderer,
package/dist/text.js CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
 
7
7
 
8
- var _chunkGKLTTGBRjs = require('./chunk-GKLTTGBR.js');
8
+ var _chunk2Z23LTH3js = require('./chunk-2Z23LTH3.js');
9
9
 
10
10
 
11
11
 
@@ -19,4 +19,4 @@ var _chunk4AR425ARjs = require('./chunk-4AR425AR.js');
19
19
 
20
20
 
21
21
 
22
- exports.ArabicShaper = _chunk4AR425ARjs.ArabicShaper; exports.BidiResolver = _chunk4AR425ARjs.BidiResolver; exports.MSDFFont = _chunkGKLTTGBRjs.MSDFFont; exports.MSDFTextEntity = _chunkGKLTTGBRjs.MSDFTextEntity; exports.SVGEntity = _chunkGKLTTGBRjs.SVGEntity; exports.clearCssLineBoxMetrics = _chunkGKLTTGBRjs.clearCssLineBoxMetrics; exports.cssLineBoxBaseline = _chunkGKLTTGBRjs.cssLineBoxBaseline; exports.prepareContentGrid = _chunkGKLTTGBRjs.prepareContentGrid;
22
+ exports.ArabicShaper = _chunk4AR425ARjs.ArabicShaper; exports.BidiResolver = _chunk4AR425ARjs.BidiResolver; exports.MSDFFont = _chunk2Z23LTH3js.MSDFFont; exports.MSDFTextEntity = _chunk2Z23LTH3js.MSDFTextEntity; exports.SVGEntity = _chunk2Z23LTH3js.SVGEntity; exports.clearCssLineBoxMetrics = _chunk2Z23LTH3js.clearCssLineBoxMetrics; exports.cssLineBoxBaseline = _chunk2Z23LTH3js.cssLineBoxBaseline; exports.prepareContentGrid = _chunk2Z23LTH3js.prepareContentGrid;
package/dist/text.mjs CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  clearCssLineBoxMetrics,
6
6
  cssLineBoxBaseline,
7
7
  prepareContentGrid
8
- } from "./chunk-J2P6QZJV.mjs";
8
+ } from "./chunk-XIEQHSBB.mjs";
9
9
  import {
10
10
  ArabicShaper,
11
11
  BidiResolver
@@ -192,7 +192,7 @@ export interface A11yAttributes {
192
192
  /**
193
193
  * Union of all pointer/interaction events that can be emitted by an {@link Entity}.
194
194
  */
195
- export type VectoEvent = 'click' | 'hover' | 'pointerdown' | 'pointerup' | 'pointercancel' | 'pointermove' | 'pointerleave' | 'change' | 'focus' | 'blur' | 'wheel' | 'keydown' | 'keyup';
195
+ export type VectoEvent = 'click' | 'dblclick' | 'hover' | 'pointerdown' | 'pointerup' | 'pointercancel' | 'pointermove' | 'pointerleave' | 'change' | 'focus' | 'blur' | 'wheel' | 'keydown' | 'keyup';
196
196
  /** Options for {@link Entity.on} / {@link Entity.off}. */
197
197
  export interface ListenerOptions {
198
198
  /** Register the listener for the capture phase (root→target) instead of bubble. */
@@ -470,6 +470,15 @@ export declare abstract class Entity {
470
470
  * @param payload - Arbitrary data forwarded to each listener.
471
471
  */
472
472
  emit(event: VectoEvent, payload: any): void;
473
+ /**
474
+ * Programmatically focus the entity's projected a11y shadow element, if one
475
+ * exists. After `scene.add(entity)` the shadow element is typically created
476
+ * on the next a11y sync (within one animation frame), so this method retries
477
+ * once on the next rAF if the element isn't immediately available — matching
478
+ * the timing pattern the prior workaround described in findings.md achieved
479
+ * with `requestAnimationFrame(() => document.getElementById(id)?.focus())`.
480
+ */
481
+ focus(): void;
473
482
  /** Run one node's listeners for the event, honoring stopImmediatePropagation. */
474
483
  private fireListeners;
475
484
  /**
@@ -70,6 +70,21 @@ export interface SceneOptions {
70
70
  * Useful when Vecto is running inside a custom layout container or offscreen canvas.
71
71
  */
72
72
  disableWindowResize?: boolean;
73
+ /**
74
+ * Cap the effective device pixel ratio used to size the Canvas2D and WebGL
75
+ * point-layer backing stores. `undefined` (default) reads the real,
76
+ * uncapped `window.devicePixelRatio` — unchanged from prior versions.
77
+ * Backing-store render cost scales with `logical size × dpr²`, so a
78
+ * full-screen HiDPI scene (`pointBackend: 'webgl'` in particular) can
79
+ * overrun its frame budget on a DPR-3 display while running fine on the
80
+ * DPR-1 dev machine it was tuned on (findings.md, 2026-07-16). `maxDPR: 2`
81
+ * keeps the display retina-crisp (2x already exceeds what most eyes
82
+ * resolve) while roughly halving the backing-store pixel count at DPR 3.
83
+ * Applied at construction and re-applied on every {@link resize} call
84
+ * (including the automatic window-resize listener), since the real DPR
85
+ * can change at runtime (a window dragged between displays).
86
+ */
87
+ maxDPR?: number;
73
88
  /**
74
89
  * Enable automatic throttling to 2 FPS when the scene is static (no active transitions
75
90
  * and not marked dirty) to save power/CPU. Default is `true`.
@@ -196,6 +211,8 @@ export declare class Scene {
196
211
  width: number;
197
212
  height: number;
198
213
  private disableWindowResize;
214
+ /** See {@link SceneOptions.maxDPR}. `undefined` = uncapped (real DPR). */
215
+ maxDPR?: number;
199
216
  private destroyed;
200
217
  private device;
201
218
  private deviceLost;
@@ -216,6 +233,14 @@ export declare class Scene {
216
233
  private pointerLeaveListener;
217
234
  private hasWarnedZeroSize;
218
235
  private fontLoadHandler;
236
+ /** Toggle development-mode runtime warnings globally. */
237
+ static devMode: boolean;
238
+ private static _devModeDetected;
239
+ private _devActive;
240
+ private _devFrameCount;
241
+ private _devWarn;
242
+ /** @internal Periodic dev checks — called once per frame in dev mode. */
243
+ private _devRunChecks;
219
244
  constructor(canvas: HTMLCanvasElement, options?: SceneOptions);
220
245
  private endContentSelectionDrag;
221
246
  private releaseContentSelectionForRebuild;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectojs/core",
3
- "version": "1.9.2",
3
+ "version": "1.11.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },