@vectojs/core 0.2.5 → 0.2.7

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
@@ -3,7 +3,7 @@ import {
3
3
  LayoutResultBuffer,
4
4
  computeLineSegments,
5
5
  createCanvasMeasurer
6
- } from "./chunk-SB3SCWLT.mjs";
6
+ } from "./chunk-SSOP3F5F.mjs";
7
7
  import {
8
8
  CanvasRenderer,
9
9
  SVGRenderer,
@@ -12,7 +12,7 @@ import {
12
12
  isSafeUrl,
13
13
  parseColorToRGBA,
14
14
  sanitizeUrl
15
- } from "./chunk-WEQRBXT2.mjs";
15
+ } from "./chunk-HVJHXIKW.mjs";
16
16
  import {
17
17
  Easing,
18
18
  Entity,
@@ -24,12 +24,12 @@ import {
24
24
  TweenDriver,
25
25
  VectoJSEvent,
26
26
  isTweenConfig
27
- } from "./chunk-P6MDWIEX.mjs";
27
+ } from "./chunk-YVN3PJRC.mjs";
28
28
  import {
29
29
  ArabicShaper,
30
30
  BidiResolver,
31
31
  LayoutWorkerManager
32
- } from "./chunk-B3Z3JEJH.mjs";
32
+ } from "./chunk-STWPTWO4.mjs";
33
33
 
34
34
  // src/tree/ComputeParticleEntity.ts
35
35
  var PARTICLE_STRIDE_FLOATS = 8;
@@ -296,6 +296,11 @@ var ComputeParticleEntity = class extends Entity {
296
296
 
297
297
  // src/tree/Scene.ts
298
298
  var REDUCED_MOTION_FPS = 30;
299
+ function parseInlinePx(value) {
300
+ if (!value || !value.endsWith("px")) return null;
301
+ const n = parseFloat(value);
302
+ return Number.isFinite(n) && n > 0 ? n : null;
303
+ }
299
304
  var Scene = class _Scene {
300
305
  static webglCreator = null;
301
306
  static webgpuManagerClass = null;
@@ -350,6 +355,9 @@ var Scene = class _Scene {
350
355
  // server-side (e.g. headless layout / vector export) without jsdom.
351
356
  a11yRoot;
352
357
  a11yElements = /* @__PURE__ */ new Map();
358
+ /** DOM nodes mirroring static text content, keyed by entity id. */
359
+ contentElements = /* @__PURE__ */ new Map();
360
+ contentProjectionEnabled = true;
353
361
  resizeHandler;
354
362
  focusedA11yElement = null;
355
363
  caretBlinkTimer = null;
@@ -386,6 +394,8 @@ var Scene = class _Scene {
386
394
  initializingWebGPU = false;
387
395
  gpuCanvas = null;
388
396
  gpuContext = null;
397
+ /** True while the GPU canvas holds a presented particle frame (needs clearing when they leave). */
398
+ gpuHasContent = false;
389
399
  mouseX = -9999;
390
400
  mouseY = -9999;
391
401
  pointerMoveListener = null;
@@ -396,8 +406,10 @@ var Scene = class _Scene {
396
406
  this.debugA11y = options.debugA11y ?? false;
397
407
  this.disableWindowResize = options.disableWindowResize ?? false;
398
408
  if (this.disableWindowResize) {
399
- this.width = canvas.width || canvas.clientWidth || 0;
400
- this.height = canvas.height || canvas.clientHeight || 0;
409
+ const styleWidth = parseInlinePx(canvas.style?.width);
410
+ const styleHeight = parseInlinePx(canvas.style?.height);
411
+ this.width = styleWidth ?? (canvas.width || canvas.clientWidth || 0);
412
+ this.height = styleHeight ?? (canvas.height || canvas.clientHeight || 0);
401
413
  } else {
402
414
  this.width = typeof window !== "undefined" ? window.innerWidth : canvas.clientWidth || canvas.width || 800;
403
415
  this.height = typeof window !== "undefined" ? window.innerHeight : canvas.clientHeight || canvas.height || 600;
@@ -409,6 +421,7 @@ var Scene = class _Scene {
409
421
  this.autoThrottle = options.autoThrottle ?? true;
410
422
  this.particleBackend = options.particleBackend ?? "auto";
411
423
  this.a11ySyncInterval = options.a11ySyncInterval ?? 0;
424
+ this.contentProjectionEnabled = options.contentProjection ?? true;
412
425
  this.reducedMotionQuery = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
413
426
  this.root = new class RootEntity extends Entity {
414
427
  isPointInside() {
@@ -430,7 +443,10 @@ var Scene = class _Scene {
430
443
  if (options.renderer) {
431
444
  this.renderer = options.renderer;
432
445
  } else {
433
- this.renderer = new CanvasRenderer(canvas);
446
+ this.renderer = new CanvasRenderer(
447
+ canvas,
448
+ this.disableWindowResize ? { width: this.width, height: this.height } : void 0
449
+ );
434
450
  }
435
451
  if (typeof document !== "undefined") {
436
452
  this.a11yRoot = document.createElement("div");
@@ -559,6 +575,11 @@ var Scene = class _Scene {
559
575
  * @param entity - The subtree whose shadow nodes should be removed.
560
576
  */
561
577
  detachA11y(entity) {
578
+ const contentEl = this.contentElements.get(entity.id);
579
+ if (contentEl) {
580
+ contentEl.remove();
581
+ this.contentElements.delete(entity.id);
582
+ }
562
583
  this.removeA11yRecursively(entity);
563
584
  }
564
585
  /**
@@ -605,6 +626,8 @@ var Scene = class _Scene {
605
626
  this.a11yRoot?.remove();
606
627
  this.portalRoot?.remove();
607
628
  this.a11yElements.clear();
629
+ for (const el of this.contentElements.values()) el.remove();
630
+ this.contentElements.clear();
608
631
  this.pointRenderer?.destroy();
609
632
  this.renderer.dispose?.();
610
633
  this.glCanvas?.remove();
@@ -619,6 +642,10 @@ var Scene = class _Scene {
619
642
  this.manager.destroy();
620
643
  this.manager = null;
621
644
  }
645
+ if (this.device) {
646
+ this.device.destroy?.();
647
+ this.device = null;
648
+ }
622
649
  }
623
650
  setupEvents() {
624
651
  if (typeof window !== "undefined" && !this.disableWindowResize) {
@@ -963,11 +990,96 @@ var Scene = class _Scene {
963
990
  el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
964
991
  }
965
992
  }
993
+ this.syncContentProjection(node);
966
994
  for (const child of node.children) this.syncA11y(child);
967
995
  if (node === this.root) {
968
996
  for (const overlay of this.overlayRoot.children) this.syncA11y(overlay);
969
997
  }
970
998
  }
999
+ /**
1000
+ * Mirror one entity's static text ({@link Entity.getContentProjection}) as a
1001
+ * transparent DOM node positioned over the drawn glyphs. Runs on the a11y
1002
+ * sync cadence; all writes are dirty-checked. Off-viewport projections are
1003
+ * hidden (`display: none`) so text-heavy scenes only materialize what is
1004
+ * visible to the browser's text machinery anyway.
1005
+ */
1006
+ syncContentProjection(node) {
1007
+ if (!this.contentProjectionEnabled || !this.a11yRoot) return;
1008
+ const projection = node.getContentProjection();
1009
+ let el = this.contentElements.get(node.id);
1010
+ if (!projection || !projection.text) {
1011
+ if (el) {
1012
+ el.remove();
1013
+ this.contentElements.delete(node.id);
1014
+ }
1015
+ return;
1016
+ }
1017
+ if (!el) {
1018
+ el = document.createElement("div");
1019
+ el.setAttribute("data-vecto-content", node.id);
1020
+ const s = el.style;
1021
+ s.position = "absolute";
1022
+ s.transformOrigin = "0 0";
1023
+ s.margin = "0";
1024
+ s.padding = "0";
1025
+ s.color = "transparent";
1026
+ s.whiteSpace = "pre-wrap";
1027
+ s.overflow = "hidden";
1028
+ s.zIndex = "0";
1029
+ el.addEventListener(
1030
+ "wheel",
1031
+ (e2) => {
1032
+ node.dispatchEvent(new VectoJSEvent("wheel", node, e2));
1033
+ },
1034
+ { passive: false }
1035
+ );
1036
+ this.a11yRoot.appendChild(el);
1037
+ this.contentElements.set(node.id, el);
1038
+ }
1039
+ if (el.textContent !== projection.text) el.textContent = projection.text;
1040
+ const font = projection.font ?? "";
1041
+ if (el.style.font !== font) el.style.font = font;
1042
+ const lineHeight = projection.lineHeight !== void 0 ? `${projection.lineHeight}px` : "";
1043
+ if (el.style.lineHeight !== lineHeight) el.style.lineHeight = lineHeight;
1044
+ const hidden = node.interactive ? "true" : null;
1045
+ if (el.getAttribute("aria-hidden") !== hidden) {
1046
+ if (hidden) el.setAttribute("aria-hidden", hidden);
1047
+ else el.removeAttribute("aria-hidden");
1048
+ }
1049
+ const selectable = projection.selectable === true;
1050
+ const pointerEvents = selectable ? "auto" : "none";
1051
+ if (el.style.pointerEvents !== pointerEvents) {
1052
+ el.style.pointerEvents = pointerEvents;
1053
+ el.style.userSelect = selectable ? "text" : "none";
1054
+ el.style.cursor = selectable ? "text" : "";
1055
+ }
1056
+ const { a, b, c, d, e, f } = node.getWorldTransform();
1057
+ el.style.left = `${e}px`;
1058
+ el.style.top = `${f}px`;
1059
+ if (node.width > 0) el.style.width = `${node.width}px`;
1060
+ if (node.height > 0) el.style.height = `${node.height}px`;
1061
+ el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
1062
+ let visible = true;
1063
+ if (node.width > 0 && node.height > 0) {
1064
+ let minX = Infinity;
1065
+ let minY = Infinity;
1066
+ let maxX = -Infinity;
1067
+ let maxY = -Infinity;
1068
+ for (let i = 0; i < 4; i++) {
1069
+ const lx = i & 1 ? node.width : 0;
1070
+ const ly = i & 2 ? node.height : 0;
1071
+ const wx = a * lx + c * ly + e;
1072
+ const wy = b * lx + d * ly + f;
1073
+ if (wx < minX) minX = wx;
1074
+ if (wx > maxX) maxX = wx;
1075
+ if (wy < minY) minY = wy;
1076
+ if (wy > maxY) maxY = wy;
1077
+ }
1078
+ visible = maxX >= 0 && minX <= this.width && maxY >= 0 && minY <= this.height;
1079
+ }
1080
+ const display = visible ? "" : "none";
1081
+ if (el.style.display !== display) el.style.display = display;
1082
+ }
971
1083
  enforceA11yDomOrder() {
972
1084
  if (!this.a11yRoot) return;
973
1085
  this.fullViewportElements.length = 0;
@@ -1164,8 +1276,8 @@ var Scene = class _Scene {
1164
1276
  loop(time) {
1165
1277
  if (!this.isRunning) return;
1166
1278
  let cap = this.effectiveMaxFPS();
1167
- const isStatic = this.autoThrottle && !this.dirty && !this.hasAnyPendingAnimation(this.root) && !this.hasAnyPendingAnimation(this.overlayRoot);
1168
- if (isStatic && this.renderMode === "always" && this.maxFPS > 0) {
1279
+ const isIdle = !this.dirty && !this.hasAnyPendingAnimation(this.root) && !this.hasAnyPendingAnimation(this.overlayRoot);
1280
+ if (isIdle && this.autoThrottle && this.renderMode === "always" && this.maxFPS > 0) {
1169
1281
  cap = Math.min(cap, 2);
1170
1282
  }
1171
1283
  if (cap > 0 && time - this.lastTime < 1e3 / cap - 1) {
@@ -1174,17 +1286,19 @@ var Scene = class _Scene {
1174
1286
  }
1175
1287
  const dt = time - this.lastTime;
1176
1288
  this.lastTime = time;
1177
- if (this.renderMode === "onDemand" && isStatic) {
1289
+ if (this.renderMode === "onDemand" && isIdle) {
1178
1290
  this.scheduleFrame();
1179
1291
  return;
1180
1292
  }
1293
+ this.dirty = false;
1181
1294
  this.render(this.renderer, dt, time);
1182
1295
  const hasActiveAnimation = this.hasAnyPendingAnimation(this.root) || this.hasAnyPendingAnimation(this.overlayRoot);
1183
1296
  const hasInteractive = this.hasAnyInteractive(this.root) || this.hasAnyInteractive(this.overlayRoot);
1297
+ const wantsContentSync = this.contentProjectionEnabled;
1184
1298
  const shouldSyncInterval = this.a11ySyncInterval <= 0 || time - this.lastA11ySync >= this.a11ySyncInterval;
1185
- if ((hasInteractive || this.a11yElements.size > 0) && (shouldSyncInterval || this.a11yPendingSyncAfterAnimation)) {
1299
+ if ((hasInteractive || this.a11yElements.size > 0 || wantsContentSync) && (shouldSyncInterval || this.a11yPendingSyncAfterAnimation)) {
1186
1300
  this.lastA11ySync = time;
1187
- if (hasInteractive) {
1301
+ if (hasInteractive || wantsContentSync) {
1188
1302
  this.syncA11y(this.root);
1189
1303
  }
1190
1304
  this.enforceA11yDomOrder();
@@ -1192,7 +1306,6 @@ var Scene = class _Scene {
1192
1306
  } else if (hasActiveAnimation) {
1193
1307
  this.a11yPendingSyncAfterAnimation = true;
1194
1308
  }
1195
- this.dirty = false;
1196
1309
  this.scheduleFrame();
1197
1310
  }
1198
1311
  /**
@@ -1304,6 +1417,7 @@ var Scene = class _Scene {
1304
1417
  renderPass.end();
1305
1418
  }
1306
1419
  this.device.queue.submit([commandEncoder.finish()]);
1420
+ if (this.gpuContext) this.gpuHasContent = true;
1307
1421
  } catch (e) {
1308
1422
  console.error("WebGPU frame execution failed. Falling back.", e);
1309
1423
  this.deviceLost = true;
@@ -1312,9 +1426,23 @@ var Scene = class _Scene {
1312
1426
  }
1313
1427
  } else if (isMainRenderPath) {
1314
1428
  for (const entity of computeEntities) {
1315
- entity.updateCPU(dt / 1e3, this.mouseX, this.mouseY, this.width, this.height);
1429
+ let mx = this.mouseX;
1430
+ let my = this.mouseY;
1431
+ if (mx > -9e3 && my > -9e3) {
1432
+ const local = entity.worldToLocal(mx, my);
1433
+ if (local) {
1434
+ mx = local.x;
1435
+ my = local.y;
1436
+ } else {
1437
+ mx = -9999;
1438
+ my = -9999;
1439
+ }
1440
+ }
1441
+ entity.updateCPU(dt / 1e3, mx, my, this.width, this.height);
1316
1442
  }
1317
1443
  }
1444
+ } else if (isMainRenderer) {
1445
+ this.clearGPUCanvasIfStale();
1318
1446
  }
1319
1447
  renderer.clear();
1320
1448
  if (isMainRenderer) {
@@ -1412,7 +1540,19 @@ var Scene = class _Scene {
1412
1540
  if (visible) {
1413
1541
  if (node instanceof ComputeParticleEntity) {
1414
1542
  if (this.deviceLost || this.webgpuDisabled || !this.device || !this.manager) {
1415
- this.renderCPUParticles(renderer, node, worldOpacity);
1543
+ this.renderCPUParticles(
1544
+ renderer,
1545
+ node,
1546
+ worldOpacity,
1547
+ a,
1548
+ b,
1549
+ c,
1550
+ d,
1551
+ te,
1552
+ tf,
1553
+ worldScaleX,
1554
+ isSimilarityTransform
1555
+ );
1416
1556
  }
1417
1557
  } else {
1418
1558
  node.render(renderer);
@@ -1436,6 +1576,7 @@ var Scene = class _Scene {
1436
1576
  if (isMainRenderer) {
1437
1577
  this.pointRenderer?.flush();
1438
1578
  }
1579
+ renderer.present?.();
1439
1580
  }
1440
1581
  /**
1441
1582
  * Export the current scene state to a lightweight, flat SVG XML string.
@@ -1455,6 +1596,10 @@ var Scene = class _Scene {
1455
1596
  this.renderer.resize(width, height);
1456
1597
  }
1457
1598
  this.pointRenderer?.resize(width, height);
1599
+ if (this.gpuCanvas) {
1600
+ this.gpuCanvas.width = width;
1601
+ this.gpuCanvas.height = height;
1602
+ }
1458
1603
  this.markDirty();
1459
1604
  }
1460
1605
  /**
@@ -1477,6 +1622,27 @@ var Scene = class _Scene {
1477
1622
  if (overlayHit) return overlayHit;
1478
1623
  return this.findHitRecursively(this.root, x, y);
1479
1624
  }
1625
+ /** Submit one transparent clear pass when particle content lingers on the GPU canvas. */
1626
+ clearGPUCanvasIfStale() {
1627
+ if (!this.gpuHasContent || !this.device || !this.gpuContext || this.deviceLost) return;
1628
+ try {
1629
+ const encoder = this.device.createCommandEncoder();
1630
+ const pass = encoder.beginRenderPass({
1631
+ colorAttachments: [
1632
+ {
1633
+ view: this.gpuContext.getCurrentTexture().createView(),
1634
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
1635
+ loadOp: "clear",
1636
+ storeOp: "store"
1637
+ }
1638
+ ]
1639
+ });
1640
+ pass.end();
1641
+ this.device.queue.submit([encoder.finish()]);
1642
+ } catch {
1643
+ }
1644
+ this.gpuHasContent = false;
1645
+ }
1480
1646
  async initWebGPUContext(entities) {
1481
1647
  if (!navigator.gpu) {
1482
1648
  throw new Error("WebGPU not supported on this platform.");
@@ -1569,10 +1735,10 @@ var Scene = class _Scene {
1569
1735
  }).catch(() => this.recreateWebGPUDeviceWithRetry(entities, attempt + 1));
1570
1736
  }, backoff);
1571
1737
  }
1572
- renderCPUParticles(renderer, entity, worldOpacity) {
1738
+ renderCPUParticles(renderer, entity, worldOpacity, a, b, c, d, e, f, worldScale, isSimilarityTransform) {
1573
1739
  const data = entity.particleData;
1574
1740
  const size = entity.maxParticles;
1575
- const isMain = renderer === this.renderer;
1741
+ const useGL = renderer === this.renderer && !!this.pointRenderer && isSimilarityTransform;
1576
1742
  for (let i = 0; i < size; i++) {
1577
1743
  const idx = i * 8;
1578
1744
  const x = data[idx];
@@ -1582,8 +1748,14 @@ var Scene = class _Scene {
1582
1748
  if (life === 0) continue;
1583
1749
  const opacity = life < 0 ? worldOpacity : worldOpacity * Math.min(1, life);
1584
1750
  const scale = life >= 0 ? Math.min(1, life) : 1;
1585
- if (isMain && this.pointRenderer) {
1586
- this.pointRenderer.addCircle(x, y, pSize * scale, entity.baseColor, opacity);
1751
+ if (useGL) {
1752
+ this.pointRenderer.addCircle(
1753
+ a * x + c * y + e,
1754
+ b * x + d * y + f,
1755
+ pSize * scale * worldScale,
1756
+ entity.baseColor,
1757
+ opacity
1758
+ );
1587
1759
  } else {
1588
1760
  renderer.fillCircle(x, y, pSize * scale, entity.baseColor, opacity);
1589
1761
  }
@@ -1631,6 +1803,14 @@ var TextEntity = class extends Entity {
1631
1803
  this.on("hover", () => this.isHovered = true);
1632
1804
  this.on("pointerleave", () => this.isHovered = false);
1633
1805
  }
1806
+ /**
1807
+ * Mirror the rendered text into the DOM content layer: find-in-page, screen
1808
+ * readers, crawlers, and translation see the same string the canvas draws.
1809
+ */
1810
+ getContentProjection() {
1811
+ if (!this.text) return null;
1812
+ return { text: this.text, font: `${this.fontSize}px sans-serif` };
1813
+ }
1634
1814
  /**
1635
1815
  * Replace the text content. Runs the **cold** measurement pass (re-segment +
1636
1816
  * re-measure) since the glyphs changed, then re-lays out.
@@ -1809,6 +1989,11 @@ var SplineEntity = class extends Entity {
1809
1989
  bounds;
1810
1990
  offscreen = null;
1811
1991
  baked = false;
1992
+ /** Logical (CSS-pixel) size of the baked bitmap — the blit destination size. */
1993
+ bakedWidth = 0;
1994
+ bakedHeight = 0;
1995
+ /** Gradient strokes can't be baked to a solid-color bitmap; they render per-frame. */
1996
+ containsGradient;
1812
1997
  /** Lazily-flattened polylines (one Float32Array of [x,y,...] per segment) for hit-testing. */
1813
1998
  polylines = null;
1814
1999
  /**
@@ -1828,6 +2013,8 @@ var SplineEntity = class extends Entity {
1828
2013
  this.bounds = this.computeBounds();
1829
2014
  this.width = this.bounds.width;
1830
2015
  this.height = this.bounds.height;
2016
+ const isGradient = (c) => c !== null && !Array.isArray(c);
2017
+ this.containsGradient = (this.doc.equations?.some((eq) => isGradient(eq.color_rgb)) ?? false) || (this.doc.paths?.some((p) => isGradient(p.color_rgb)) ?? false);
1831
2018
  this.interactive = true;
1832
2019
  }
1833
2020
  computeBounds() {
@@ -1987,18 +2174,22 @@ var SplineEntity = class extends Entity {
1987
2174
  const pad = this.lineWidth + 2;
1988
2175
  const w = Math.max(1, Math.ceil(this.bounds.width) + pad * 2);
1989
2176
  const h = Math.max(1, Math.ceil(this.bounds.height) + pad * 2);
2177
+ this.bakedWidth = w;
2178
+ this.bakedHeight = h;
2179
+ const dpr = typeof window !== "undefined" && typeof window.devicePixelRatio === "number" ? window.devicePixelRatio || 1 : 1;
1990
2180
  let canvas;
1991
2181
  if (typeof OffscreenCanvas !== "undefined") {
1992
- canvas = new OffscreenCanvas(w, h);
2182
+ canvas = new OffscreenCanvas(w * dpr, h * dpr);
1993
2183
  } else if (typeof document !== "undefined") {
1994
2184
  canvas = document.createElement("canvas");
1995
- canvas.width = w;
1996
- canvas.height = h;
2185
+ canvas.width = w * dpr;
2186
+ canvas.height = h * dpr;
1997
2187
  } else {
1998
2188
  return;
1999
2189
  }
2000
2190
  const ctx = canvas.getContext("2d");
2001
2191
  if (!ctx) return;
2192
+ ctx.scale(dpr, dpr);
2002
2193
  ctx.translate(pad - this.bounds.x, pad - this.bounds.y);
2003
2194
  ctx.lineWidth = this.lineWidth;
2004
2195
  ctx.lineCap = "round";
@@ -2032,7 +2223,7 @@ var SplineEntity = class extends Entity {
2032
2223
  }
2033
2224
  render(r) {
2034
2225
  let rendered = false;
2035
- if (this.cache) {
2226
+ if (this.cache && !this.containsGradient) {
2036
2227
  if (!this.baked) this.bake();
2037
2228
  if (this.offscreen) {
2038
2229
  const pad = this.lineWidth + 2;
@@ -2040,8 +2231,9 @@ var SplineEntity = class extends Entity {
2040
2231
  this.offscreen,
2041
2232
  this.bounds.x - pad,
2042
2233
  this.bounds.y - pad,
2043
- this.offscreen.width,
2044
- this.offscreen.height
2234
+ // Logical size, not the (DPR-scaled) bitmap size.
2235
+ this.bakedWidth,
2236
+ this.bakedHeight
2045
2237
  );
2046
2238
  rendered = true;
2047
2239
  }
@@ -1 +1 @@
1
- export declare const WORKER_SOURCE_STRING = "\"use strict\";(()=>{var k=new Map;function n(t){return typeof t==\"number\"&&Number.isFinite(t)}function H(t){return t.origin?t.origin===self.location.origin:!0}function M(t){if(!t||typeof t!=\"object\")return!1;let e=t;return typeof e.id==\"string\"&&n(e.seqId)&&typeof e.text==\"string\"&&typeof e.fontId==\"string\"&&(e.fontData===void 0||typeof e.fontData==\"object\")&&n(e.maxWidth)&&n(e.maxHeight)&&n(e.fontSize)&&(e.lineHeight===void 0||n(e.lineHeight))&&(e.letterSpacing===void 0||n(e.letterSpacing))}self.onmessage=t=>{if(!H(t)||!M(t.data))return;let{id:e,seqId:F,text:q,fontId:d,fontData:f,maxWidth:y,maxHeight:I,fontSize:i,lineHeight:A,letterSpacing:D}=t.data;f&&k.set(d,f);let s=k.get(d);if(!s)return;let g=[],l=[],p=[],m=[],o=0,a=0,h=s.metrics?.ascender??.8,W=s.metrics?.descender??-.2,b=A??i*(h-W),x=Array.from(q);for(let c=0;c<x.length;c++){let u=x[c].codePointAt(0),S=(s.glyphs?.find(C=>C.unicode===u)?.advance??1)*i;o+S>y&&u===32&&(o=0,a++),g.push(u),l.push(o);let w=a*b+h*i;p.push(w),m.push(-256),o+=S+(D??0)}let r={id:e,seqId:F,width:Math.min(o,y),height:(a+1)*b,codePoints:new Uint32Array(g),xCoords:new Float32Array(l),yCoords:new Float32Array(p),packedStyles:new Uint32Array(m)};self.postMessage(r,[r.codePoints.buffer,r.xCoords.buffer,r.yCoords.buffer,r.packedStyles.buffer])};})();\n";
1
+ export declare const WORKER_SOURCE_STRING = "\"use strict\";(()=>{var C=new Map;function f(t){return typeof t==\"number\"&&Number.isFinite(t)}function U(t){return t.origin?t.origin===self.location.origin:!0}function j(t){if(!t||typeof t!=\"object\")return!1;let e=t;return typeof e.id==\"string\"&&f(e.seqId)&&typeof e.text==\"string\"&&typeof e.fontId==\"string\"&&(e.fontData===void 0||typeof e.fontData==\"object\")&&f(e.maxWidth)&&f(e.maxHeight)&&f(e.fontSize)&&(e.lineHeight===void 0||f(e.lineHeight))&&(e.letterSpacing===void 0||f(e.letterSpacing))}self.onmessage=t=>{if(!U(t)||!j(t.data))return;let{id:e,seqId:D,text:L,fontId:k,fontData:S,maxWidth:H,maxHeight:z,fontSize:d,lineHeight:I,letterSpacing:M}=t.data;S&&C.set(k,S);let g=C.get(k);if(!g)return;let m=[],i=[],h=[],w=[],n=0,c=0,r=0,o=-1,p=g.metrics?.ascender??.8,R=g.metrics?.descender??-.2,b=I??d*(p-R),P=M??0,F=new Map;for(let s of g.glyphs??[])F.set(s.unicode,s.advance);let x=()=>{n>r&&(r=n),n=0,c++,o=-1},A=Array.from(L);for(let s=0;s<A.length;s++){let a=A[s].codePointAt(0);if(a===10){x();continue}let W=(F.get(a)??1)*d,q=a>=11904;if(q&&(o=-1),n+W>H&&n>0){if(a===32){x();continue}if(o>=0&&i[o]>0){let l=i[o];l>r&&(r=l),c++;let v=c*b+p*d;for(let y=o;y<i.length;y++)i[y]-=l,h[y]=v;n-=l}else x()}a===32?o=-1:o===-1&&!q&&(o=m.length),m.push(a),i.push(n),h.push(c*b+p*d),w.push(-256),n+=W+P}n>r&&(r=n);let u={id:e,seqId:D,width:r,height:(c+1)*b,codePoints:new Uint32Array(m),xCoords:new Float32Array(i),yCoords:new Float32Array(h),packedStyles:new Uint32Array(w)};self.postMessage(u,[u.codePoints.buffer,u.xCoords.buffer,u.yCoords.buffer,u.packedStyles.buffer])};})();\n";
package/dist/layout.js CHANGED
@@ -3,14 +3,14 @@
3
3
 
4
4
 
5
5
 
6
- var _chunk5KLB6BEZjs = require('./chunk-5KLB6BEZ.js');
6
+ var _chunkBWLNEJJWjs = require('./chunk-BWLNEJJW.js');
7
7
 
8
8
 
9
- var _chunk76NMTLYLjs = require('./chunk-76NMTLYL.js');
9
+ var _chunkCTZQOM5Zjs = require('./chunk-CTZQOM5Z.js');
10
10
 
11
11
 
12
12
 
13
13
 
14
14
 
15
15
 
16
- exports.LayoutEngine = _chunk5KLB6BEZjs.LayoutEngine; exports.LayoutResultBuffer = _chunk5KLB6BEZjs.LayoutResultBuffer; exports.LayoutWorkerManager = _chunk76NMTLYLjs.LayoutWorkerManager; exports.computeLineSegments = _chunk5KLB6BEZjs.computeLineSegments; exports.createCanvasMeasurer = _chunk5KLB6BEZjs.createCanvasMeasurer;
16
+ exports.LayoutEngine = _chunkBWLNEJJWjs.LayoutEngine; exports.LayoutResultBuffer = _chunkBWLNEJJWjs.LayoutResultBuffer; exports.LayoutWorkerManager = _chunkCTZQOM5Zjs.LayoutWorkerManager; exports.computeLineSegments = _chunkBWLNEJJWjs.computeLineSegments; exports.createCanvasMeasurer = _chunkBWLNEJJWjs.createCanvasMeasurer;
package/dist/layout.mjs CHANGED
@@ -3,10 +3,10 @@ import {
3
3
  LayoutResultBuffer,
4
4
  computeLineSegments,
5
5
  createCanvasMeasurer
6
- } from "./chunk-SB3SCWLT.mjs";
6
+ } from "./chunk-SSOP3F5F.mjs";
7
7
  import {
8
8
  LayoutWorkerManager
9
- } from "./chunk-B3Z3JEJH.mjs";
9
+ } from "./chunk-STWPTWO4.mjs";
10
10
  export {
11
11
  LayoutEngine,
12
12
  LayoutResultBuffer,
@@ -14,7 +14,18 @@ export declare class CanvasRenderer implements IRenderer {
14
14
  private batchColor;
15
15
  private batchAlpha;
16
16
  private batchCount;
17
- constructor(canvas: HTMLCanvasElement);
17
+ /**
18
+ * @param canvas - The target canvas. Its backing store is resized to the
19
+ * logical size × devicePixelRatio.
20
+ * @param size - Explicit logical size. Without it the renderer assumes a
21
+ * fullscreen canvas and sizes to the window — pass this for embedded /
22
+ * custom-container canvases (the Scene does when `disableWindowResize` is
23
+ * set) so the canvas's own dimensions aren't clobbered by the window's.
24
+ */
25
+ constructor(canvas: HTMLCanvasElement, size?: {
26
+ width: number;
27
+ height: number;
28
+ });
18
29
  /**
19
30
  * Expose the underlying `CanvasRenderingContext2D` for operations not
20
31
  * covered by the {@link IRenderer} interface.
@@ -175,6 +175,13 @@ export interface IRenderer {
175
175
  stop: number;
176
176
  color: string;
177
177
  }[]): any;
178
+ /**
179
+ * Present the completed frame, called by {@link Scene} exactly once at the
180
+ * end of each render pass (after the final {@link flush}). Retained-scene
181
+ * backends (e.g. `@vectojs/three`) do their single real GL render here;
182
+ * immediate-mode backends (Canvas2D, SVG) don't need it. Optional.
183
+ */
184
+ present?(): void;
178
185
  /**
179
186
  * Release any backend-owned GPU textures / GL contexts / caches.
180
187
  *
@@ -18,7 +18,7 @@
18
18
  * returns it verbatim — relative navigation is never script-injectable.
19
19
  * 4. If the URL parses with a scheme NOT in {@link SAFE_SCHEMES}, returns `'#'`
20
20
  * to keep the link non-empty but inert.
21
- * 5. Otherwise returns the canonical `URL.toString()` form.
21
+ * 5. Otherwise returns the trimmed input unchanged (no canonicalization).
22
22
  *
23
23
  * The function never throws; malformed input falls back to `'#'`.
24
24
  */
package/dist/renderer.js CHANGED
@@ -4,11 +4,11 @@
4
4
 
5
5
 
6
6
 
7
- var _chunkTPNADFNNjs = require('./chunk-TPNADFNN.js');
7
+ var _chunkGTQRCY6Wjs = require('./chunk-GTQRCY6W.js');
8
8
 
9
9
 
10
10
 
11
11
 
12
12
 
13
13
 
14
- exports.CanvasRenderer = _chunkTPNADFNNjs.CanvasRenderer; exports.SVGRenderer = _chunkTPNADFNNjs.SVGRenderer; exports.WebGPUParticleSystemManager = _chunkTPNADFNNjs.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkTPNADFNNjs.createWebGLPointRenderer; exports.parseColorToRGBA = _chunkTPNADFNNjs.parseColorToRGBA;
14
+ exports.CanvasRenderer = _chunkGTQRCY6Wjs.CanvasRenderer; exports.SVGRenderer = _chunkGTQRCY6Wjs.SVGRenderer; exports.WebGPUParticleSystemManager = _chunkGTQRCY6Wjs.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkGTQRCY6Wjs.createWebGLPointRenderer; exports.parseColorToRGBA = _chunkGTQRCY6Wjs.parseColorToRGBA;
package/dist/renderer.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  WebGPUParticleSystemManager,
5
5
  createWebGLPointRenderer,
6
6
  parseColorToRGBA
7
- } from "./chunk-WEQRBXT2.mjs";
7
+ } from "./chunk-HVJHXIKW.mjs";
8
8
  export {
9
9
  CanvasRenderer,
10
10
  SVGRenderer,
@@ -1,4 +1,4 @@
1
- import { Entity } from '../tree/Entity';
1
+ import { Entity, type ContentProjection } from '../tree/Entity';
2
2
  import { MSDFFont } from './MSDFFont';
3
3
  export interface MSDFTextEntityOptions {
4
4
  font: MSDFFont;
@@ -8,6 +8,10 @@ export interface MSDFTextEntityOptions {
8
8
  color?: string;
9
9
  lineHeight?: number;
10
10
  letterSpacing?: number;
11
+ /** Wrap boundary in logical pixels. Defaults to 1000. */
12
+ maxWidth?: number;
13
+ /** Layout height limit in logical pixels. Defaults to 1000. */
14
+ maxHeight?: number;
11
15
  }
12
16
  export declare class MSDFTextEntity extends Entity {
13
17
  private font;
@@ -17,13 +21,23 @@ export declare class MSDFTextEntity extends Entity {
17
21
  color: string;
18
22
  private letterSpacing;
19
23
  private lineHeight?;
24
+ private maxWidth;
25
+ private maxHeight;
20
26
  private text;
21
27
  private lastRenderedSeqId;
22
28
  private rgbColorCache;
23
29
  private fontStringCache;
24
30
  private layoutResult;
25
31
  constructor(text: string, options: MSDFTextEntityOptions);
32
+ /** Change the wrap boundary and re-run layout for the current text. */
33
+ setMaxWidth(maxWidth: number): void;
26
34
  setText(text: string): void;
35
+ private queueLayout;
36
+ /**
37
+ * Mirror the rendered text into the DOM content layer: find-in-page, screen
38
+ * readers, crawlers, and translation see the same string the canvas draws.
39
+ */
40
+ getContentProjection(): ContentProjection | null;
27
41
  isPointInside(globalX: number, globalY: number): boolean;
28
42
  render(renderer: any): void;
29
43
  destroy(): void;
package/dist/text.js CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkTQ4H357Qjs = require('./chunk-TQ4H357Q.js');
5
+ var _chunkDWMQYHNCjs = require('./chunk-DWMQYHNC.js');
6
6
 
7
7
 
8
8
 
9
- var _chunk76NMTLYLjs = require('./chunk-76NMTLYL.js');
9
+ var _chunkCTZQOM5Zjs = require('./chunk-CTZQOM5Z.js');
10
10
 
11
11
 
12
12
 
13
13
 
14
14
 
15
15
 
16
- exports.ArabicShaper = _chunk76NMTLYLjs.ArabicShaper; exports.BidiResolver = _chunk76NMTLYLjs.BidiResolver; exports.MSDFFont = _chunkTQ4H357Qjs.MSDFFont; exports.MSDFTextEntity = _chunkTQ4H357Qjs.MSDFTextEntity; exports.SVGEntity = _chunkTQ4H357Qjs.SVGEntity;
16
+ exports.ArabicShaper = _chunkCTZQOM5Zjs.ArabicShaper; exports.BidiResolver = _chunkCTZQOM5Zjs.BidiResolver; exports.MSDFFont = _chunkDWMQYHNCjs.MSDFFont; exports.MSDFTextEntity = _chunkDWMQYHNCjs.MSDFTextEntity; exports.SVGEntity = _chunkDWMQYHNCjs.SVGEntity;
package/dist/text.mjs CHANGED
@@ -2,11 +2,11 @@ import {
2
2
  MSDFFont,
3
3
  MSDFTextEntity,
4
4
  SVGEntity
5
- } from "./chunk-P6MDWIEX.mjs";
5
+ } from "./chunk-YVN3PJRC.mjs";
6
6
  import {
7
7
  ArabicShaper,
8
8
  BidiResolver
9
- } from "./chunk-B3Z3JEJH.mjs";
9
+ } from "./chunk-STWPTWO4.mjs";
10
10
  export {
11
11
  ArabicShaper,
12
12
  BidiResolver,