@vectojs/core 1.24.0 → 1.26.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
@@ -8,13 +8,13 @@ import {
8
8
  isSafeUrl,
9
9
  parseColorToRGBA,
10
10
  sanitizeUrl
11
- } from "./chunk-ME4LB2HB.mjs";
11
+ } from "./chunk-I4WQY7CJ.mjs";
12
12
  import {
13
13
  Entity,
14
14
  MSDFTextEntity,
15
15
  SVGEntity,
16
16
  VectoJSEvent
17
- } from "./chunk-FRMLD4PP.mjs";
17
+ } from "./chunk-MVFPN4Y5.mjs";
18
18
 
19
19
  // src/tree/Scene.ts
20
20
  import { SpringDriver, TweenDriver } from "@vectojs/animation";
@@ -1370,6 +1370,57 @@ function rebaseChildBox(parent, parentOriginX, parentOriginY, child, childOrigin
1370
1370
  REBASED_BOX.matrix = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
1371
1371
  return REBASED_BOX;
1372
1372
  }
1373
+ var SCENE_OPTION_KEYS = [
1374
+ "a11ySyncInterval",
1375
+ "autoThrottle",
1376
+ "contentProjection",
1377
+ "contentProjectionMargin",
1378
+ "debugA11y",
1379
+ "disableWindowResize",
1380
+ "maxDPR",
1381
+ "maxFPS",
1382
+ "particleBackend",
1383
+ "pointBackend",
1384
+ "readingDirection",
1385
+ "renderer",
1386
+ "renderMode",
1387
+ "respectReducedMotion",
1388
+ "userTiming"
1389
+ ];
1390
+ var SCENE_FIELD_NOT_OPTION = {
1391
+ devMode: "set the static `Scene.devMode = true` before constructing"
1392
+ };
1393
+ function editDistance(a, b) {
1394
+ const m = a.length;
1395
+ const n = b.length;
1396
+ if (m === 0 || n === 0) return Math.max(m, n);
1397
+ let prev = Array.from({ length: n + 1 }, (_, i) => i);
1398
+ const curr = Array.from({ length: n + 1 });
1399
+ for (let i = 1; i <= m; i++) {
1400
+ curr[0] = i;
1401
+ for (let j = 1; j <= n; j++) {
1402
+ const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
1403
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
1404
+ }
1405
+ prev = curr.slice();
1406
+ }
1407
+ return prev[n];
1408
+ }
1409
+ function closestOptionKey(key) {
1410
+ const lower = key.toLowerCase();
1411
+ let best;
1412
+ let bestScore = Infinity;
1413
+ for (const candidate of SCENE_OPTION_KEYS) {
1414
+ if (candidate.toLowerCase() === lower) return candidate;
1415
+ const d = editDistance(lower, candidate.toLowerCase());
1416
+ if (d < bestScore) {
1417
+ bestScore = d;
1418
+ best = candidate;
1419
+ }
1420
+ }
1421
+ const limit = Math.max(2, Math.floor(key.length / 3));
1422
+ return bestScore <= limit ? best : void 0;
1423
+ }
1373
1424
  var REDUCED_MOTION_FPS = 30;
1374
1425
  function parseInlinePx(value) {
1375
1426
  if (!value || !value.endsWith("px")) return null;
@@ -2803,6 +2854,37 @@ var Scene = class _Scene {
2803
2854
  if (!this._devActive) return;
2804
2855
  console.warn(`[vectojs/dev] ${message}`);
2805
2856
  }
2857
+ /**
2858
+ * Warn (dev mode only) about `SceneOptions` keys this version does not read.
2859
+ *
2860
+ * A structural type makes an unrecognized key a silent no-op, and TypeScript
2861
+ * only rejects one when the object literal sits inline at the call site — not
2862
+ * when options are built dynamically, and never in plain JS. Since the
2863
+ * failure mode is "the option appears to work", a runtime check is the only
2864
+ * thing that surfaces it.
2865
+ *
2866
+ * Dev-mode only on purpose: the loop is O(keys × known keys) with an edit
2867
+ * distance per pair, which is nothing at construction but is still pure
2868
+ * overhead in production, where the value has already been shipped.
2869
+ */
2870
+ _warnUnknownOptions(options) {
2871
+ if (!this._devActive) return;
2872
+ const known = new Set(SCENE_OPTION_KEYS);
2873
+ for (const key of Object.keys(options)) {
2874
+ if (known.has(key)) continue;
2875
+ const fieldHint = SCENE_FIELD_NOT_OPTION[key];
2876
+ if (fieldHint) {
2877
+ this._devWarn(
2878
+ `SceneOptions: \`${key}\` is not a constructor option \u2014 ${fieldHint}. Passing it here has no effect.`
2879
+ );
2880
+ continue;
2881
+ }
2882
+ const suggestion = closestOptionKey(key);
2883
+ this._devWarn(
2884
+ `SceneOptions: unknown option \`${key}\` is ignored.` + (suggestion ? ` Did you mean \`${suggestion}\`?` : "")
2885
+ );
2886
+ }
2887
+ }
2806
2888
  /** @internal Periodic dev checks — called once per frame in dev mode. */
2807
2889
  _devRunChecks() {
2808
2890
  this._devFrameCount++;
@@ -2867,7 +2949,9 @@ var Scene = class _Scene {
2867
2949
  this.contentProjectionEnabled = options.contentProjection ?? true;
2868
2950
  this.contentProjectionMargin = options.contentProjectionMargin;
2869
2951
  this.readingDirection = options.readingDirection ?? "ltr";
2952
+ this.renderMode = options.renderMode ?? "always";
2870
2953
  this._devActive = _Scene._devModeDetected();
2954
+ this._warnUnknownOptions(options);
2871
2955
  this.reducedMotionQuery = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
2872
2956
  if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
2873
2957
  this.forcedColorsQuery = window.matchMedia("(forced-colors: active)");
@@ -5709,11 +5793,11 @@ function contentGridLineSignature(grid, line, projected, lineHeight, baseline, f
5709
5793
  // src/components/TextEntity.ts
5710
5794
  import {
5711
5795
  LayoutEngine,
5712
- createCanvasMeasurer
5796
+ resolveGlyphMeasurer
5713
5797
  } from "@vectojs/layout";
5714
- var sharedMeasurer;
5798
+ var sharedMeasurer = null;
5715
5799
  function defaultMeasurer() {
5716
- if (sharedMeasurer === void 0) sharedMeasurer = createCanvasMeasurer("sans-serif");
5800
+ sharedMeasurer ??= resolveGlyphMeasurer("sans-serif");
5717
5801
  return sharedMeasurer;
5718
5802
  }
5719
5803
  var TextEntity = class extends Entity {
@@ -6488,6 +6572,7 @@ export {
6488
6572
  PARTICLE_STRIDE_FLOATS,
6489
6573
  REDUCED_MOTION_FPS,
6490
6574
  Rect,
6575
+ SCENE_OPTION_KEYS,
6491
6576
  SVGEntity,
6492
6577
  SVGRenderer,
6493
6578
  Scene,
package/dist/renderer.js CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
 
9
9
 
10
- var _chunkKEBYJVD6js = require('./chunk-KEBYJVD6.js');
10
+ var _chunkKHGHP2J3js = require('./chunk-KHGHP2J3.js');
11
11
 
12
12
 
13
13
 
@@ -16,4 +16,4 @@ var _chunkKEBYJVD6js = require('./chunk-KEBYJVD6.js');
16
16
 
17
17
 
18
18
 
19
- exports.CanvasRenderer = _chunkKEBYJVD6js.CanvasRenderer; exports.GlyphRasterAtlas = _chunkKEBYJVD6js.GlyphRasterAtlas; exports.SVGRenderer = _chunkKEBYJVD6js.SVGRenderer; exports.TextRasterCache = _chunkKEBYJVD6js.TextRasterCache; exports.WebGPUParticleSystemManager = _chunkKEBYJVD6js.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkKEBYJVD6js.createWebGLPointRenderer; exports.parseColorToRGBA = _chunkKEBYJVD6js.parseColorToRGBA;
19
+ exports.CanvasRenderer = _chunkKHGHP2J3js.CanvasRenderer; exports.GlyphRasterAtlas = _chunkKHGHP2J3js.GlyphRasterAtlas; exports.SVGRenderer = _chunkKHGHP2J3js.SVGRenderer; exports.TextRasterCache = _chunkKHGHP2J3js.TextRasterCache; exports.WebGPUParticleSystemManager = _chunkKHGHP2J3js.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkKHGHP2J3js.createWebGLPointRenderer; exports.parseColorToRGBA = _chunkKHGHP2J3js.parseColorToRGBA;
package/dist/renderer.mjs CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  WebGPUParticleSystemManager,
7
7
  createWebGLPointRenderer,
8
8
  parseColorToRGBA
9
- } from "./chunk-ME4LB2HB.mjs";
9
+ } from "./chunk-I4WQY7CJ.mjs";
10
10
  export {
11
11
  CanvasRenderer,
12
12
  GlyphRasterAtlas,
@@ -34,10 +34,34 @@ export declare class MSDFTextEntity extends Entity {
34
34
  private layoutText;
35
35
  private text;
36
36
  private lastRenderedSeqId;
37
+ private atlasDecodeTarget;
38
+ private atlasDecodeHandler;
37
39
  private rgbColorCache;
38
40
  private fontStringCache;
39
41
  private layoutResult;
40
42
  constructor(text: string, options: MSDFTextEntityOptions);
43
+ /**
44
+ * Repaint once the atlas raster decodes.
45
+ *
46
+ * The WebGL backend refuses to upload a not-yet-decoded atlas (it would pin an
47
+ * empty texture in its identity cache forever), so the upload has to happen on
48
+ * a LATER frame — and nothing else schedules one. Layout marks the scene dirty
49
+ * when the worker replies, which for a network-served atlas is long before the
50
+ * image lands, so the scene is already idle by then.
51
+ *
52
+ * Measured on Chromium and Firefox (2026-07-31) with a 600 ms atlas: the
53
+ * scene's own rAF loop never uploaded a decoded atlas in EITHER render mode.
54
+ * `onDemand` skips idle frames outright; `always` throttles to 2 FPS when
55
+ * idle, so whether it recovers is down to whether a throttled tick happens to
56
+ * land after the decode — Chromium got one, Firefox did not. Neither is a
57
+ * mechanism, which is why this listener exists rather than relying on the
58
+ * frame loop to come back around.
59
+ *
60
+ * Only `HTMLImageElement`-shaped sources have a decode to wait for; a canvas,
61
+ * `ImageBitmap`, or `VideoFrame` atlas is ready on arrival.
62
+ */
63
+ private watchAtlasDecode;
64
+ private detachAtlasDecodeListener;
41
65
  /** Change the wrap boundary and re-run layout for the current text. */
42
66
  setMaxWidth(maxWidth: number): void;
43
67
  /**
@@ -1,12 +1,21 @@
1
1
  import { Entity } from '../tree/Entity';
2
2
  import { IRenderer } from '../renderer/IRenderer';
3
3
  export declare class SVGEntity extends Entity {
4
+ /**
5
+ * Stroke colour of the fallback marker drawn when the source cannot be
6
+ * rasterized. Set to `'transparent'` to opt out and keep the box empty.
7
+ * Default `'rgba(248,113,113,0.9)'`.
8
+ */
9
+ fallbackStroke: string;
10
+ /** Fill behind the fallback marker. Default `'rgba(248,113,113,0.12)'`. */
11
+ fallbackFill: string;
4
12
  private svgSource;
5
13
  private imageBitmap;
6
14
  private imageElement;
7
15
  private blobURL;
8
16
  private currentImg;
9
17
  private lodTimeout;
18
+ private rasterFailed;
10
19
  private cachedDoc;
11
20
  private baseWidth;
12
21
  private baseHeight;
@@ -16,7 +25,22 @@ export declare class SVGEntity extends Entity {
16
25
  setSVGSource(svgSource: string): void;
17
26
  private parseSVGDimensions;
18
27
  private triggerRasterization;
28
+ /**
29
+ * Whether the source genuinely rasterized to a bitmap.
30
+ *
31
+ * Distinguishes "drew the real artwork" from "drew the fallback marker",
32
+ * which pixel counts alone cannot tell apart — both are non-blank.
33
+ */
34
+ hasRasterBitmap(): boolean;
35
+ /**
36
+ * Whether rasterization failed, so {@link render} draws the fallback marker.
37
+ *
38
+ * `false` while a raster is still in flight; only a settled failure sets it.
39
+ */
40
+ hasRasterFailed(): boolean;
19
41
  isPointInside(globalX: number, globalY: number): boolean;
20
42
  render(r: IRenderer): void;
43
+ /** Box outline plus a diagonal cross — the conventional "broken image" mark. */
44
+ private drawFallback;
21
45
  destroy(): void;
22
46
  }
package/dist/text.js CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkAGP4VLF4js = require('./chunk-AGP4VLF4.js');
5
+ var _chunkZ3HFY75Rjs = require('./chunk-Z3HFY75R.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 = _chunkAGP4VLF4js.MSDFTextEntity; exports.SVGEntity = _chunkAGP4VLF4js.SVGEntity;
12
+ exports.MSDFTextEntity = _chunkZ3HFY75Rjs.MSDFTextEntity; exports.SVGEntity = _chunkZ3HFY75Rjs.SVGEntity;
package/dist/text.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MSDFTextEntity,
3
3
  SVGEntity
4
- } from "./chunk-FRMLD4PP.mjs";
4
+ } from "./chunk-MVFPN4Y5.mjs";
5
5
 
6
6
  // src/text/index.ts
7
7
  export * from "@vectojs/text";
@@ -210,7 +210,37 @@ export interface SceneOptions {
210
210
  * (`'rtl'`). Also settable later via {@link Scene.readingDirection}.
211
211
  */
212
212
  readingDirection?: 'ltr' | 'rtl';
213
+ /**
214
+ * When to repaint:
215
+ * - `'always'` (default): drive a continuous rAF loop, throttling to 2 FPS
216
+ * while the scene is idle if {@link SceneOptions.autoThrottle} is on.
217
+ * - `'onDemand'`: paint only after {@link Scene.markDirty} (or an active
218
+ * transition), so a genuinely static scene costs zero frames.
219
+ *
220
+ * Also settable later via {@link Scene.renderMode}. Prefer this option when
221
+ * the mode is known at construction: it applies before the first frame, so an
222
+ * `onDemand` scene never pays for the initial always-on frames.
223
+ */
224
+ renderMode?: 'always' | 'onDemand';
213
225
  }
226
+ /**
227
+ * Every recognized {@link SceneOptions} key, used only to warn about unknown
228
+ * ones in dev mode.
229
+ *
230
+ * This exists because `SceneOptions` is structural: passing a key it does not
231
+ * declare is a **silent** no-op, and TypeScript only catches it when the object
232
+ * is written inline at the call site. Code that builds options dynamically, or
233
+ * plain untranspiled JS, gets no diagnostic at all. `renderMode` was a public
234
+ * field with no matching option for several releases, and four `@vectojs` demos
235
+ * shipped `new Scene(canvas, { renderMode: 'onDemand' })` — reading correctly,
236
+ * doing nothing, and sitting on the 2 FPS idle floor.
237
+ *
238
+ * Kept as a literal rather than derived from a type: `keyof SceneOptions` does
239
+ * not survive to runtime, so this list is the only form a constructor can check
240
+ * against. A new option must be added here too — the test suite asserts the two
241
+ * stay in sync.
242
+ */
243
+ export declare const SCENE_OPTION_KEYS: readonly ['a11ySyncInterval', 'autoThrottle', 'contentProjection', 'contentProjectionMargin', 'debugA11y', 'disableWindowResize', 'maxDPR', 'maxFPS', 'particleBackend', 'pointBackend', 'readingDirection', 'renderer', 'renderMode', 'respectReducedMotion', 'userTiming'];
214
244
  /** Frame-rate the loop is capped to when the OS requests reduced motion. */
215
245
  export declare const REDUCED_MOTION_FPS = 30;
216
246
  /**
@@ -955,6 +985,20 @@ export declare class Scene {
955
985
  private _devActive;
956
986
  private _devFrameCount;
957
987
  private _devWarn;
988
+ /**
989
+ * Warn (dev mode only) about `SceneOptions` keys this version does not read.
990
+ *
991
+ * A structural type makes an unrecognized key a silent no-op, and TypeScript
992
+ * only rejects one when the object literal sits inline at the call site — not
993
+ * when options are built dynamically, and never in plain JS. Since the
994
+ * failure mode is "the option appears to work", a runtime check is the only
995
+ * thing that surfaces it.
996
+ *
997
+ * Dev-mode only on purpose: the loop is O(keys × known keys) with an edit
998
+ * distance per pair, which is nothing at construction but is still pure
999
+ * overhead in production, where the value has already been shipped.
1000
+ */
1001
+ private _warnUnknownOptions;
958
1002
  /** @internal Periodic dev checks — called once per frame in dev mode. */
959
1003
  private _devRunChecks;
960
1004
  constructor(canvas: HTMLCanvasElement, options?: SceneOptions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectojs/core",
3
- "version": "1.24.0",
3
+ "version": "1.26.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -61,20 +61,20 @@
61
61
  "scripts": {
62
62
  "build": "(cd ../math && bun run build) && (cd ../text && bun run build) && (cd ../layout && bun run build) && (cd ../animation && bun run build) && tsup && tsc -p tsconfig.build.json",
63
63
  "test": "vitest run",
64
- "test:e2e": "bun e2e/hidpi.e2e.ts && bun e2e/text-projection.e2e.ts"
64
+ "test:e2e": "bun e2e/hidpi.e2e.ts && bun e2e/text-projection.e2e.ts && bun e2e/svg-fallback.e2e.ts && bun e2e/msdf-atlas-decode.e2e.ts && bun e2e/layout-worker-fallback.e2e.ts"
65
65
  },
66
66
  "dependencies": {
67
67
  "@vectojs/animation": "^0.1.1",
68
- "@vectojs/layout": "^0.4.0",
68
+ "@vectojs/layout": "^0.5.0",
69
69
  "@vectojs/math": "^0.1.1",
70
- "@vectojs/text": "^0.2.0"
70
+ "@vectojs/text": "^0.3.0"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@guidepup/virtual-screen-reader": "0.32.1",
74
74
  "@vitest/coverage-v8": "^4.1.10",
75
75
  "esbuild": "^0.28.1",
76
- "jsdom": "^29.1.1",
77
- "puppeteer-core": "^25.3.0",
76
+ "jsdom": "^30.0.1",
77
+ "puppeteer-core": "^25.4.0",
78
78
  "tsup": "^8.3.5",
79
79
  "vitest": "^4.1.10"
80
80
  }