@vectojs/core 1.11.3 → 1.12.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.
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3;// src/renderer/CanvasRenderer.ts
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4;// src/renderer/CanvasRenderer.ts
2
2
  var TWO_PI = Math.PI * 2;
3
3
  function getDevicePixelRatio() {
4
4
  return typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
@@ -29,6 +29,8 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
29
29
  __init2() {this.batchColor = ""}
30
30
  __init3() {this.batchAlpha = 1}
31
31
  __init4() {this.batchCount = 0}
32
+ __init5() {this._cachedFont = ""}
33
+ __init6() {this._cachedFill = ""}
32
34
  /**
33
35
  * @param canvas - The target canvas. Its backing store is resized to the
34
36
  * logical size × devicePixelRatio.
@@ -38,7 +40,7 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
38
40
  * set) so the canvas's own dimensions aren't clobbered by the window's.
39
41
  * @param maxDPR - See {@link maxDPR}.
40
42
  */
41
- constructor(canvas, size, maxDPR) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);
43
+ constructor(canvas, size, maxDPR) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);
42
44
  this.maxDPR = maxDPR;
43
45
  const dpr = this.effectiveDPR();
44
46
  this.width = _nullishCoalesce(_optionalChain([size, 'optionalAccess', _ => _.width]), () => ( (typeof window !== "undefined" ? window.innerWidth : canvas.width || 0)));
@@ -99,6 +101,8 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
99
101
  restore() {
100
102
  this.flush();
101
103
  this.ctx.restore();
104
+ this._cachedFont = "";
105
+ this._cachedFill = "";
102
106
  }
103
107
  /** @inheritdoc */
104
108
  translate(x, y) {
@@ -186,7 +190,10 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
186
190
  /** @inheritdoc */
187
191
  fill(color) {
188
192
  this.flush();
189
- this.ctx.fillStyle = color;
193
+ if (this._cachedFill !== color) {
194
+ this.ctx.fillStyle = color;
195
+ this._cachedFill = color;
196
+ }
190
197
  this.ctx.fill();
191
198
  }
192
199
  /** @inheritdoc */
@@ -201,8 +208,14 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
201
208
  /** @inheritdoc */
202
209
  fillText(text, x, y, font, color) {
203
210
  this.flush();
204
- this.ctx.font = font;
205
- this.ctx.fillStyle = color;
211
+ if (this._cachedFont !== font) {
212
+ this.ctx.font = font;
213
+ this._cachedFont = font;
214
+ }
215
+ if (this._cachedFill !== color) {
216
+ this.ctx.fillStyle = color;
217
+ this._cachedFill = color;
218
+ }
206
219
  this.ctx.fillText(text, x, y);
207
220
  }
208
221
  /** @inheritdoc */
@@ -223,6 +236,8 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
223
236
  this.batchColor = "";
224
237
  this.batchAlpha = 1;
225
238
  this.batchActive = false;
239
+ this._cachedFont = "";
240
+ this._cachedFill = "";
226
241
  }
227
242
  }, _class.__initStatic(), _class);
228
243
 
@@ -310,39 +325,39 @@ function fontFamilyFromShorthand(font, sizeToken) {
310
325
  var SVGRenderer = (_class2 = class {
311
326
 
312
327
 
313
- __init5() {this.buffer = []}
314
- __init6() {this.defsBuffer = []}
315
- __init7() {this.currentPath = []}
328
+ __init7() {this.buffer = []}
329
+ __init8() {this.defsBuffer = []}
330
+ __init9() {this.currentPath = []}
316
331
  // Inline matrix states (3x3 representation)
317
332
  // [a, b, c, d, e, f] corresponding to:
318
333
  // | a c e |
319
334
  // | b d f |
320
335
  // | 0 0 1 |
321
- __init8() {this.mStack = []}
322
- __init9() {this.ma = 1}
323
- __init10() {this.mb = 0}
324
- __init11() {this.mc = 0}
325
- __init12() {this.md = 1}
326
- __init13() {this.me = 0}
327
- __init14() {this.mf = 0}
336
+ __init10() {this.mStack = []}
337
+ __init11() {this.ma = 1}
338
+ __init12() {this.mb = 0}
339
+ __init13() {this.mc = 0}
340
+ __init14() {this.md = 1}
341
+ __init15() {this.me = 0}
342
+ __init16() {this.mf = 0}
328
343
  // Alpha stack
329
- __init15() {this.alphaStack = []}
330
- __init16() {this.globalAlpha = 1}
344
+ __init17() {this.alphaStack = []}
345
+ __init18() {this.globalAlpha = 1}
331
346
  // Clipping stack
332
- __init17() {this.clipDepthStack = []}
333
- __init18() {this.clipDepth = 0}
334
- __init19() {this.clipCounter = 0}
347
+ __init19() {this.clipDepthStack = []}
348
+ __init20() {this.clipDepth = 0}
349
+ __init21() {this.clipCounter = 0}
335
350
  // uniquely identifies clipPaths
336
351
  // Batch circles states (local coordinates)
337
- __init20() {this.batchCircles = []}
338
- __init21() {this.batchMatrix = [1, 0, 0, 1, 0, 0]}
339
- __init22() {this.batchColor = ""}
340
- __init23() {this.batchAlpha = 1}
341
- __init24() {this.batchActive = false}
352
+ __init22() {this.batchCircles = []}
353
+ __init23() {this.batchMatrix = [1, 0, 0, 1, 0, 0]}
354
+ __init24() {this.batchColor = ""}
355
+ __init25() {this.batchAlpha = 1}
356
+ __init26() {this.batchActive = false}
342
357
  // Cache for generated gradient defs
343
- __init25() {this.gradientCounter = 0}
344
- __init26() {this.gradientCache = /* @__PURE__ */ new Map()}
345
- constructor(width, height) {;_class2.prototype.__init5.call(this);_class2.prototype.__init6.call(this);_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);_class2.prototype.__init11.call(this);_class2.prototype.__init12.call(this);_class2.prototype.__init13.call(this);_class2.prototype.__init14.call(this);_class2.prototype.__init15.call(this);_class2.prototype.__init16.call(this);_class2.prototype.__init17.call(this);_class2.prototype.__init18.call(this);_class2.prototype.__init19.call(this);_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);
358
+ __init27() {this.gradientCounter = 0}
359
+ __init28() {this.gradientCache = /* @__PURE__ */ new Map()}
360
+ constructor(width, height) {;_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);_class2.prototype.__init11.call(this);_class2.prototype.__init12.call(this);_class2.prototype.__init13.call(this);_class2.prototype.__init14.call(this);_class2.prototype.__init15.call(this);_class2.prototype.__init16.call(this);_class2.prototype.__init17.call(this);_class2.prototype.__init18.call(this);_class2.prototype.__init19.call(this);_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);_class2.prototype.__init27.call(this);_class2.prototype.__init28.call(this);
346
361
  this.width = width;
347
362
  this.height = height;
348
363
  }
@@ -1486,11 +1501,11 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
1486
1501
  `;
1487
1502
  var WebGPUParticleSystemManager = (_class3 = class {
1488
1503
 
1489
- __init27() {this.computePipeline = null}
1490
- __init28() {this.renderPipeline = null}
1491
- __init29() {this.computeBindGroupLayout = null}
1492
- __init30() {this.renderBindGroupLayout = null}
1493
- constructor(device) {;_class3.prototype.__init27.call(this);_class3.prototype.__init28.call(this);_class3.prototype.__init29.call(this);_class3.prototype.__init30.call(this);
1504
+ __init29() {this.computePipeline = null}
1505
+ __init30() {this.renderPipeline = null}
1506
+ __init31() {this.computeBindGroupLayout = null}
1507
+ __init32() {this.renderBindGroupLayout = null}
1508
+ constructor(device) {;_class3.prototype.__init29.call(this);_class3.prototype.__init30.call(this);_class3.prototype.__init31.call(this);_class3.prototype.__init32.call(this);
1494
1509
  this.device = device;
1495
1510
  }
1496
1511
  initPipelines(format) {
@@ -1631,6 +1646,92 @@ var WebGPUParticleSystemManager = (_class3 = class {
1631
1646
  }
1632
1647
  }, _class3);
1633
1648
 
1649
+ // src/renderer/TextRasterCache.ts
1650
+ var TextRasterCache = (_class4 = class {
1651
+ __init33() {this.cache = /* @__PURE__ */ new Map()}
1652
+
1653
+
1654
+ __init34() {this.scratchCtx = null}
1655
+ __init35() {this._hits = 0}
1656
+ __init36() {this._misses = 0}
1657
+ constructor(options = {}) {;_class4.prototype.__init33.call(this);_class4.prototype.__init34.call(this);_class4.prototype.__init35.call(this);_class4.prototype.__init36.call(this);
1658
+ this.maxEntries = Math.max(1, _nullishCoalesce(options.maxEntries, () => ( 4096)));
1659
+ this.dpr = Math.max(1, _nullishCoalesce(options.dpr, () => ( 1)));
1660
+ }
1661
+ /** Live instrumentation snapshot. */
1662
+ get stats() {
1663
+ return { hits: this._hits, misses: this._misses, size: this.cache.size };
1664
+ }
1665
+ measureCtx() {
1666
+ if (!this.scratchCtx) {
1667
+ if (typeof document === "undefined") return null;
1668
+ this.scratchCtx = document.createElement("canvas").getContext("2d");
1669
+ }
1670
+ return this.scratchCtx;
1671
+ }
1672
+ /**
1673
+ * Return the cached raster for a text run, rasterizing it on first request.
1674
+ *
1675
+ * @param font - Full CSS `font` shorthand, used for both measuring and painting.
1676
+ * @param color - CSS color baked into the raster.
1677
+ * @param text - The run to rasterize (may contain CJK / emoji).
1678
+ * @returns The raster, or `null` in a non-DOM/headless context (caller should
1679
+ * fall back to {@link IRenderer.fillText}).
1680
+ */
1681
+ get(font, color, text) {
1682
+ const key = font + "\0" + color + "\0" + text;
1683
+ const hit = this.cache.get(key);
1684
+ if (hit) {
1685
+ this._hits++;
1686
+ return hit;
1687
+ }
1688
+ this._misses++;
1689
+ const m = this.measureCtx();
1690
+ if (!m) return null;
1691
+ m.font = font;
1692
+ const metrics = m.measureText(text);
1693
+ const approx = metrics.width || text.length * 8;
1694
+ const ascent = metrics.actualBoundingBoxAscent || metrics.fontBoundingBoxAscent || approx * 0.8;
1695
+ const descent = metrics.actualBoundingBoxDescent || metrics.fontBoundingBoxDescent || approx * 0.2;
1696
+ const left = metrics.actualBoundingBoxLeft || 0;
1697
+ const right = metrics.actualBoundingBoxRight || metrics.width;
1698
+ const pad = 2;
1699
+ const offsetX = Math.ceil(left) + pad;
1700
+ const offsetY = Math.ceil(ascent) + pad;
1701
+ const width = offsetX + Math.ceil(right) + pad;
1702
+ const height = offsetY + Math.ceil(descent) + pad;
1703
+ if (typeof document === "undefined") return null;
1704
+ const canvas = document.createElement("canvas");
1705
+ const dpr = this.dpr;
1706
+ canvas.width = Math.max(1, Math.ceil(width * dpr));
1707
+ canvas.height = Math.max(1, Math.ceil(height * dpr));
1708
+ const ctx = canvas.getContext("2d");
1709
+ if (!ctx) return null;
1710
+ if (dpr !== 1) ctx.scale(dpr, dpr);
1711
+ ctx.font = font;
1712
+ ctx.textBaseline = "alphabetic";
1713
+ ctx.fillStyle = color;
1714
+ ctx.fillText(text, offsetX, offsetY);
1715
+ const raster = { canvas, width, height, offsetX, offsetY };
1716
+ if (this.cache.size >= this.maxEntries) {
1717
+ let toDrop = Math.ceil(this.maxEntries * 0.1);
1718
+ for (const k of this.cache.keys()) {
1719
+ this.cache.delete(k);
1720
+ if (--toDrop <= 0) break;
1721
+ }
1722
+ }
1723
+ this.cache.set(key, raster);
1724
+ return raster;
1725
+ }
1726
+ /** Drop all cached rasters and reset instrumentation. */
1727
+ clear() {
1728
+ this.cache.clear();
1729
+ this._hits = 0;
1730
+ this._misses = 0;
1731
+ }
1732
+ }, _class4);
1733
+
1734
+
1634
1735
 
1635
1736
 
1636
1737
 
@@ -1639,4 +1740,4 @@ var WebGPUParticleSystemManager = (_class3 = class {
1639
1740
 
1640
1741
 
1641
1742
 
1642
- exports.CanvasRenderer = CanvasRenderer; exports.sanitizeUrl = sanitizeUrl; exports.isSafeUrl = isSafeUrl; exports.SVGRenderer = SVGRenderer; exports.parseColorToRGBA = parseColorToRGBA; exports.createWebGLPointRenderer = createWebGLPointRenderer; exports.WebGPUParticleSystemManager = WebGPUParticleSystemManager;
1743
+ exports.CanvasRenderer = CanvasRenderer; exports.sanitizeUrl = sanitizeUrl; exports.isSafeUrl = isSafeUrl; exports.SVGRenderer = SVGRenderer; exports.parseColorToRGBA = parseColorToRGBA; exports.createWebGLPointRenderer = createWebGLPointRenderer; exports.WebGPUParticleSystemManager = WebGPUParticleSystemManager; exports.TextRasterCache = TextRasterCache;
@@ -29,6 +29,8 @@ var CanvasRenderer = class _CanvasRenderer {
29
29
  batchColor = "";
30
30
  batchAlpha = 1;
31
31
  batchCount = 0;
32
+ _cachedFont = "";
33
+ _cachedFill = "";
32
34
  /**
33
35
  * @param canvas - The target canvas. Its backing store is resized to the
34
36
  * logical size × devicePixelRatio.
@@ -99,6 +101,8 @@ var CanvasRenderer = class _CanvasRenderer {
99
101
  restore() {
100
102
  this.flush();
101
103
  this.ctx.restore();
104
+ this._cachedFont = "";
105
+ this._cachedFill = "";
102
106
  }
103
107
  /** @inheritdoc */
104
108
  translate(x, y) {
@@ -186,7 +190,10 @@ var CanvasRenderer = class _CanvasRenderer {
186
190
  /** @inheritdoc */
187
191
  fill(color) {
188
192
  this.flush();
189
- this.ctx.fillStyle = color;
193
+ if (this._cachedFill !== color) {
194
+ this.ctx.fillStyle = color;
195
+ this._cachedFill = color;
196
+ }
190
197
  this.ctx.fill();
191
198
  }
192
199
  /** @inheritdoc */
@@ -201,8 +208,14 @@ var CanvasRenderer = class _CanvasRenderer {
201
208
  /** @inheritdoc */
202
209
  fillText(text, x, y, font, color) {
203
210
  this.flush();
204
- this.ctx.font = font;
205
- this.ctx.fillStyle = color;
211
+ if (this._cachedFont !== font) {
212
+ this.ctx.font = font;
213
+ this._cachedFont = font;
214
+ }
215
+ if (this._cachedFill !== color) {
216
+ this.ctx.fillStyle = color;
217
+ this._cachedFill = color;
218
+ }
206
219
  this.ctx.fillText(text, x, y);
207
220
  }
208
221
  /** @inheritdoc */
@@ -223,6 +236,8 @@ var CanvasRenderer = class _CanvasRenderer {
223
236
  this.batchColor = "";
224
237
  this.batchAlpha = 1;
225
238
  this.batchActive = false;
239
+ this._cachedFont = "";
240
+ this._cachedFill = "";
226
241
  }
227
242
  };
228
243
 
@@ -1631,6 +1646,91 @@ var WebGPUParticleSystemManager = class {
1631
1646
  }
1632
1647
  };
1633
1648
 
1649
+ // src/renderer/TextRasterCache.ts
1650
+ var TextRasterCache = class {
1651
+ cache = /* @__PURE__ */ new Map();
1652
+ maxEntries;
1653
+ dpr;
1654
+ scratchCtx = null;
1655
+ _hits = 0;
1656
+ _misses = 0;
1657
+ constructor(options = {}) {
1658
+ this.maxEntries = Math.max(1, options.maxEntries ?? 4096);
1659
+ this.dpr = Math.max(1, options.dpr ?? 1);
1660
+ }
1661
+ /** Live instrumentation snapshot. */
1662
+ get stats() {
1663
+ return { hits: this._hits, misses: this._misses, size: this.cache.size };
1664
+ }
1665
+ measureCtx() {
1666
+ if (!this.scratchCtx) {
1667
+ if (typeof document === "undefined") return null;
1668
+ this.scratchCtx = document.createElement("canvas").getContext("2d");
1669
+ }
1670
+ return this.scratchCtx;
1671
+ }
1672
+ /**
1673
+ * Return the cached raster for a text run, rasterizing it on first request.
1674
+ *
1675
+ * @param font - Full CSS `font` shorthand, used for both measuring and painting.
1676
+ * @param color - CSS color baked into the raster.
1677
+ * @param text - The run to rasterize (may contain CJK / emoji).
1678
+ * @returns The raster, or `null` in a non-DOM/headless context (caller should
1679
+ * fall back to {@link IRenderer.fillText}).
1680
+ */
1681
+ get(font, color, text) {
1682
+ const key = font + "\0" + color + "\0" + text;
1683
+ const hit = this.cache.get(key);
1684
+ if (hit) {
1685
+ this._hits++;
1686
+ return hit;
1687
+ }
1688
+ this._misses++;
1689
+ const m = this.measureCtx();
1690
+ if (!m) return null;
1691
+ m.font = font;
1692
+ const metrics = m.measureText(text);
1693
+ const approx = metrics.width || text.length * 8;
1694
+ const ascent = metrics.actualBoundingBoxAscent || metrics.fontBoundingBoxAscent || approx * 0.8;
1695
+ const descent = metrics.actualBoundingBoxDescent || metrics.fontBoundingBoxDescent || approx * 0.2;
1696
+ const left = metrics.actualBoundingBoxLeft || 0;
1697
+ const right = metrics.actualBoundingBoxRight || metrics.width;
1698
+ const pad = 2;
1699
+ const offsetX = Math.ceil(left) + pad;
1700
+ const offsetY = Math.ceil(ascent) + pad;
1701
+ const width = offsetX + Math.ceil(right) + pad;
1702
+ const height = offsetY + Math.ceil(descent) + pad;
1703
+ if (typeof document === "undefined") return null;
1704
+ const canvas = document.createElement("canvas");
1705
+ const dpr = this.dpr;
1706
+ canvas.width = Math.max(1, Math.ceil(width * dpr));
1707
+ canvas.height = Math.max(1, Math.ceil(height * dpr));
1708
+ const ctx = canvas.getContext("2d");
1709
+ if (!ctx) return null;
1710
+ if (dpr !== 1) ctx.scale(dpr, dpr);
1711
+ ctx.font = font;
1712
+ ctx.textBaseline = "alphabetic";
1713
+ ctx.fillStyle = color;
1714
+ ctx.fillText(text, offsetX, offsetY);
1715
+ const raster = { canvas, width, height, offsetX, offsetY };
1716
+ if (this.cache.size >= this.maxEntries) {
1717
+ let toDrop = Math.ceil(this.maxEntries * 0.1);
1718
+ for (const k of this.cache.keys()) {
1719
+ this.cache.delete(k);
1720
+ if (--toDrop <= 0) break;
1721
+ }
1722
+ }
1723
+ this.cache.set(key, raster);
1724
+ return raster;
1725
+ }
1726
+ /** Drop all cached rasters and reset instrumentation. */
1727
+ clear() {
1728
+ this.cache.clear();
1729
+ this._hits = 0;
1730
+ this._misses = 0;
1731
+ }
1732
+ };
1733
+
1634
1734
  export {
1635
1735
  CanvasRenderer,
1636
1736
  sanitizeUrl,
@@ -1638,5 +1738,6 @@ export {
1638
1738
  SVGRenderer,
1639
1739
  parseColorToRGBA,
1640
1740
  createWebGLPointRenderer,
1641
- WebGPUParticleSystemManager
1741
+ WebGPUParticleSystemManager,
1742
+ TextRasterCache
1642
1743
  };
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export * from './renderer/WebGLPointRenderer';
5
5
  export * from './renderer/WebGPUParticleSystemManager';
6
6
  export * from './renderer/colorParse';
7
7
  export * from './renderer/url';
8
+ export * from './renderer/TextRasterCache';
8
9
  export * from './tree/Entity';
9
10
  export * from './tree/Scene';
10
11
  export * from './components/TextEntity';
package/dist/index.js CHANGED
@@ -12,7 +12,8 @@ var _chunkBA5HUUDFjs = require('./chunk-BA5HUUDF.js');
12
12
 
13
13
 
14
14
 
15
- var _chunkORCPCIJ7js = require('./chunk-ORCPCIJ7.js');
15
+
16
+ var _chunkBEUIB3U7js = require('./chunk-BEUIB3U7.js');
16
17
 
17
18
 
18
19
 
@@ -878,7 +879,7 @@ var Scene = (_class2 = class _Scene {
878
879
  if (options.renderer) {
879
880
  this.renderer = options.renderer;
880
881
  } else {
881
- this.renderer = new (0, _chunkORCPCIJ7js.CanvasRenderer)(
882
+ this.renderer = new (0, _chunkBEUIB3U7js.CanvasRenderer)(
882
883
  canvas,
883
884
  this.disableWindowResize ? { width: this.width, height: this.height } : void 0,
884
885
  this.maxDPR
@@ -1521,7 +1522,7 @@ var Scene = (_class2 = class _Scene {
1521
1522
  this.syncOptionalAttribute(
1522
1523
  el,
1523
1524
  "href",
1524
- attrs.href === void 0 ? void 0 : _chunkORCPCIJ7js.sanitizeUrl.call(void 0, attrs.href)
1525
+ attrs.href === void 0 ? void 0 : _chunkBEUIB3U7js.sanitizeUrl.call(void 0, attrs.href)
1525
1526
  );
1526
1527
  this.syncOptionalAttribute(el, "target", attrs.target);
1527
1528
  }
@@ -2550,7 +2551,7 @@ var Scene = (_class2 = class _Scene {
2550
2551
  * Export the current scene state to a lightweight, flat SVG XML string.
2551
2552
  */
2552
2553
  toSVG() {
2553
- const renderer = new (0, _chunkORCPCIJ7js.SVGRenderer)(this.width, this.height);
2554
+ const renderer = new (0, _chunkBEUIB3U7js.SVGRenderer)(this.width, this.height);
2554
2555
  this.render(renderer, 0, 0);
2555
2556
  return renderer.toXMLString();
2556
2557
  }
@@ -3567,8 +3568,9 @@ var DOMPortalEntity = (_class7 = class extends _chunk2Z23LTH3js.Entity {
3567
3568
  }, _class7);
3568
3569
 
3569
3570
  // src/index.ts
3570
- Scene.registerWebGLPointRendererCreator(_chunkORCPCIJ7js.createWebGLPointRenderer);
3571
- Scene.registerWebGPUParticleSystemManager(_chunkORCPCIJ7js.WebGPUParticleSystemManager);
3571
+ Scene.registerWebGLPointRendererCreator(_chunkBEUIB3U7js.createWebGLPointRenderer);
3572
+ Scene.registerWebGPUParticleSystemManager(_chunkBEUIB3U7js.WebGPUParticleSystemManager);
3573
+
3572
3574
 
3573
3575
 
3574
3576
 
@@ -3619,4 +3621,4 @@ Scene.registerWebGPUParticleSystemManager(_chunkORCPCIJ7js.WebGPUParticleSystemM
3619
3621
 
3620
3622
 
3621
3623
 
3622
- exports.ArabicShaper = _chunk4AR425ARjs.ArabicShaper; exports.BidiResolver = _chunk4AR425ARjs.BidiResolver; exports.CanvasRenderer = _chunkORCPCIJ7js.CanvasRenderer; exports.Circle = Circle; exports.ComputeParticleEntity = ComputeParticleEntity; exports.DOMPortalEntity = DOMPortalEntity; exports.Easing = _chunk2Z23LTH3js.Easing; exports.Entity = _chunk2Z23LTH3js.Entity; exports.GridTextEntity = GridTextEntity; exports.Group = Group; exports.LayoutEngine = _chunkBA5HUUDFjs.LayoutEngine; exports.LayoutResultBuffer = _chunkBA5HUUDFjs.LayoutResultBuffer; exports.LayoutWorkerManager = _chunk4AR425ARjs.LayoutWorkerManager; exports.MSDFFont = _chunk2Z23LTH3js.MSDFFont; exports.MSDFTextEntity = _chunk2Z23LTH3js.MSDFTextEntity; exports.PARTICLE_OFFSET_LIFE = PARTICLE_OFFSET_LIFE; exports.PARTICLE_OFFSET_ORIGIN_X = PARTICLE_OFFSET_ORIGIN_X; exports.PARTICLE_OFFSET_ORIGIN_Y = PARTICLE_OFFSET_ORIGIN_Y; exports.PARTICLE_OFFSET_POSITION_X = PARTICLE_OFFSET_POSITION_X; exports.PARTICLE_OFFSET_POSITION_Y = PARTICLE_OFFSET_POSITION_Y; exports.PARTICLE_OFFSET_SIZE = PARTICLE_OFFSET_SIZE; exports.PARTICLE_OFFSET_VELOCITY_X = PARTICLE_OFFSET_VELOCITY_X; exports.PARTICLE_OFFSET_VELOCITY_Y = PARTICLE_OFFSET_VELOCITY_Y; exports.PARTICLE_STRIDE_FLOATS = PARTICLE_STRIDE_FLOATS; exports.REDUCED_MOTION_FPS = REDUCED_MOTION_FPS; exports.Rect = Rect; exports.SVGEntity = _chunk2Z23LTH3js.SVGEntity; exports.SVGRenderer = _chunkORCPCIJ7js.SVGRenderer; exports.Scene = Scene; exports.SpatialHashGrid = SpatialHashGrid; exports.SplineEntity = SplineEntity; exports.SpringDriver = _chunk2Z23LTH3js.SpringDriver; exports.SpringPhysics = _chunk2Z23LTH3js.SpringPhysics; exports.TextEntity = TextEntity; exports.TweenDriver = _chunk2Z23LTH3js.TweenDriver; exports.VectoJSEvent = _chunk2Z23LTH3js.VectoJSEvent; exports.WebGPUParticleSystemManager = _chunkORCPCIJ7js.WebGPUParticleSystemManager; exports.clearCssLineBoxMetrics = _chunk2Z23LTH3js.clearCssLineBoxMetrics; exports.computeLineSegments = _chunkBA5HUUDFjs.computeLineSegments; exports.createCanvasMeasurer = _chunkBA5HUUDFjs.createCanvasMeasurer; exports.createWebGLPointRenderer = _chunkORCPCIJ7js.createWebGLPointRenderer; exports.cssLineBoxBaseline = _chunk2Z23LTH3js.cssLineBoxBaseline; exports.isSafeUrl = _chunkORCPCIJ7js.isSafeUrl; exports.isTweenConfig = _chunk2Z23LTH3js.isTweenConfig; exports.loadSpline = loadSpline; exports.parseColorToRGBA = _chunkORCPCIJ7js.parseColorToRGBA; exports.polySegmentToBezier = polySegmentToBezier; exports.prepareContentGrid = _chunk2Z23LTH3js.prepareContentGrid; exports.sanitizeUrl = _chunkORCPCIJ7js.sanitizeUrl;
3624
+ exports.ArabicShaper = _chunk4AR425ARjs.ArabicShaper; exports.BidiResolver = _chunk4AR425ARjs.BidiResolver; exports.CanvasRenderer = _chunkBEUIB3U7js.CanvasRenderer; exports.Circle = Circle; exports.ComputeParticleEntity = ComputeParticleEntity; exports.DOMPortalEntity = DOMPortalEntity; exports.Easing = _chunk2Z23LTH3js.Easing; exports.Entity = _chunk2Z23LTH3js.Entity; exports.GridTextEntity = GridTextEntity; exports.Group = Group; exports.LayoutEngine = _chunkBA5HUUDFjs.LayoutEngine; exports.LayoutResultBuffer = _chunkBA5HUUDFjs.LayoutResultBuffer; exports.LayoutWorkerManager = _chunk4AR425ARjs.LayoutWorkerManager; exports.MSDFFont = _chunk2Z23LTH3js.MSDFFont; exports.MSDFTextEntity = _chunk2Z23LTH3js.MSDFTextEntity; exports.PARTICLE_OFFSET_LIFE = PARTICLE_OFFSET_LIFE; exports.PARTICLE_OFFSET_ORIGIN_X = PARTICLE_OFFSET_ORIGIN_X; exports.PARTICLE_OFFSET_ORIGIN_Y = PARTICLE_OFFSET_ORIGIN_Y; exports.PARTICLE_OFFSET_POSITION_X = PARTICLE_OFFSET_POSITION_X; exports.PARTICLE_OFFSET_POSITION_Y = PARTICLE_OFFSET_POSITION_Y; exports.PARTICLE_OFFSET_SIZE = PARTICLE_OFFSET_SIZE; exports.PARTICLE_OFFSET_VELOCITY_X = PARTICLE_OFFSET_VELOCITY_X; exports.PARTICLE_OFFSET_VELOCITY_Y = PARTICLE_OFFSET_VELOCITY_Y; exports.PARTICLE_STRIDE_FLOATS = PARTICLE_STRIDE_FLOATS; exports.REDUCED_MOTION_FPS = REDUCED_MOTION_FPS; exports.Rect = Rect; exports.SVGEntity = _chunk2Z23LTH3js.SVGEntity; exports.SVGRenderer = _chunkBEUIB3U7js.SVGRenderer; exports.Scene = Scene; exports.SpatialHashGrid = SpatialHashGrid; exports.SplineEntity = SplineEntity; exports.SpringDriver = _chunk2Z23LTH3js.SpringDriver; exports.SpringPhysics = _chunk2Z23LTH3js.SpringPhysics; exports.TextEntity = TextEntity; exports.TextRasterCache = _chunkBEUIB3U7js.TextRasterCache; exports.TweenDriver = _chunk2Z23LTH3js.TweenDriver; exports.VectoJSEvent = _chunk2Z23LTH3js.VectoJSEvent; exports.WebGPUParticleSystemManager = _chunkBEUIB3U7js.WebGPUParticleSystemManager; exports.clearCssLineBoxMetrics = _chunk2Z23LTH3js.clearCssLineBoxMetrics; exports.computeLineSegments = _chunkBA5HUUDFjs.computeLineSegments; exports.createCanvasMeasurer = _chunkBA5HUUDFjs.createCanvasMeasurer; exports.createWebGLPointRenderer = _chunkBEUIB3U7js.createWebGLPointRenderer; exports.cssLineBoxBaseline = _chunk2Z23LTH3js.cssLineBoxBaseline; exports.isSafeUrl = _chunkBEUIB3U7js.isSafeUrl; exports.isTweenConfig = _chunk2Z23LTH3js.isTweenConfig; exports.loadSpline = loadSpline; exports.parseColorToRGBA = _chunkBEUIB3U7js.parseColorToRGBA; exports.polySegmentToBezier = polySegmentToBezier; exports.prepareContentGrid = _chunk2Z23LTH3js.prepareContentGrid; exports.sanitizeUrl = _chunkBEUIB3U7js.sanitizeUrl;
package/dist/index.mjs CHANGED
@@ -7,12 +7,13 @@ import {
7
7
  import {
8
8
  CanvasRenderer,
9
9
  SVGRenderer,
10
+ TextRasterCache,
10
11
  WebGPUParticleSystemManager,
11
12
  createWebGLPointRenderer,
12
13
  isSafeUrl,
13
14
  parseColorToRGBA,
14
15
  sanitizeUrl
15
- } from "./chunk-XZEPHCDZ.mjs";
16
+ } from "./chunk-L5BCKFQE.mjs";
16
17
  import {
17
18
  Easing,
18
19
  Entity,
@@ -3604,6 +3605,7 @@ export {
3604
3605
  SpringDriver,
3605
3606
  SpringPhysics,
3606
3607
  TextEntity,
3608
+ TextRasterCache,
3607
3609
  TweenDriver,
3608
3610
  VectoJSEvent,
3609
3611
  WebGPUParticleSystemManager,
@@ -23,6 +23,8 @@ export declare class CanvasRenderer implements IRenderer {
23
23
  private batchColor;
24
24
  private batchAlpha;
25
25
  private batchCount;
26
+ private _cachedFont;
27
+ private _cachedFill;
26
28
  /**
27
29
  * @param canvas - The target canvas. Its backing store is resized to the
28
30
  * logical size × devicePixelRatio.
@@ -0,0 +1,105 @@
1
+ /**
2
+ * A cache of pre-rasterized text runs, so a scene that draws the *same short
3
+ * strings* thousands of times per frame pays the Canvas2D text cost once per
4
+ * unique run instead of once per draw.
5
+ *
6
+ * ## Why
7
+ *
8
+ * `ctx.fillText()` is deceptively expensive at scale: each call re-shapes the
9
+ * string, re-parses the CSS `fillStyle` color, and rasterizes glyphs on the CPU
10
+ * main thread. When a view draws thousands of independently-positioned text
11
+ * runs per frame (danmaku/barrage, chat/log tails, data-grid cells, particle
12
+ * labels), that per-call shaping dominates the frame — a CPU profile shows the
13
+ * main thread pegged in native (`(program)`) code while the GPU sits idle,
14
+ * because the GPU can't be fed fast enough.
15
+ *
16
+ * This cache rasterizes each distinct `(font, color, text)` run to a small
17
+ * offscreen canvas exactly once. Every subsequent frame the caller blits it
18
+ * with a single {@link IRenderer.drawImage}, turning CPU text shaping into a
19
+ * GPU-friendly bitmap copy. When the set of distinct runs is bounded (a fixed
20
+ * phrase library, a small palette, a handful of font sizes) the steady-state
21
+ * hit rate approaches 100%; an insertion-order eviction cap bounds memory
22
+ * against unbounded (e.g. user-typed) content.
23
+ *
24
+ * It does **not** replace `fillText` for large or highly-varied text — the win
25
+ * comes from *reuse*. Emoji and color glyphs rasterize fine (they're baked into
26
+ * the bitmap), but a run that's only ever drawn once is pure overhead.
27
+ *
28
+ * ## Use
29
+ *
30
+ * ```ts
31
+ * const cache = new TextRasterCache();
32
+ * // in the frame loop, per text run:
33
+ * const r = cache.get('600 24px system-ui', '#38bdf8', label);
34
+ * if (r) renderer.drawImage(r.canvas, x - r.offsetX, baselineY - r.offsetY, r.width, r.height);
35
+ * else renderer.fillText(label, x, baselineY, '600 24px system-ui', '#38bdf8'); // headless fallback
36
+ * ```
37
+ *
38
+ * The blit position mirrors the `fillText` baseline: pass the glyph origin `x`
39
+ * and the text baseline `y`, then subtract the raster's `offsetX`/`offsetY`.
40
+ */
41
+ /** A rasterized text run plus the offsets needed to blit it at a baseline. */
42
+ export interface TextRaster {
43
+ /** The offscreen canvas holding the rasterized run. */
44
+ canvas: HTMLCanvasElement;
45
+ /** Blit destination width in CSS pixels. */
46
+ width: number;
47
+ /** Blit destination height in CSS pixels. */
48
+ height: number;
49
+ /** Left inset (CSS px) of the glyph origin inside the canvas. */
50
+ offsetX: number;
51
+ /** Distance (CSS px) from the canvas top down to the text baseline. */
52
+ offsetY: number;
53
+ }
54
+ /** Instrumentation counters for the cache (e.g. to surface a HUD hit rate). */
55
+ export interface TextRasterStats {
56
+ /** Requests served from an existing raster. */
57
+ hits: number;
58
+ /** Requests that had to rasterize a new run. */
59
+ misses: number;
60
+ /** Current number of cached rasters. */
61
+ size: number;
62
+ }
63
+ /** Options for {@link TextRasterCache}. */
64
+ export interface TextRasterCacheOptions {
65
+ /**
66
+ * Hard cap on cached rasters. On overflow the oldest ~10% (insertion order,
67
+ * which `Map` preserves) are evicted; evicted runs are re-rasterized cheaply
68
+ * on their next miss. Default `4096`.
69
+ */
70
+ maxEntries?: number;
71
+ /**
72
+ * Device-pixel-ratio the rasters are rendered at. `> 1` keeps text crisp on
73
+ * HiDPI displays at the cost of larger bitmaps; the returned `width`/`height`
74
+ * stay in CSS pixels so the blit is unchanged. Default `1`.
75
+ */
76
+ dpr?: number;
77
+ }
78
+ /**
79
+ * An isolated text-raster cache. Create one per renderer/scene (instances don't
80
+ * share state, so multiple scenes or an SSR pass never collide).
81
+ */
82
+ export declare class TextRasterCache {
83
+ private readonly cache;
84
+ private readonly maxEntries;
85
+ private readonly dpr;
86
+ private scratchCtx;
87
+ private _hits;
88
+ private _misses;
89
+ constructor(options?: TextRasterCacheOptions);
90
+ /** Live instrumentation snapshot. */
91
+ get stats(): TextRasterStats;
92
+ private measureCtx;
93
+ /**
94
+ * Return the cached raster for a text run, rasterizing it on first request.
95
+ *
96
+ * @param font - Full CSS `font` shorthand, used for both measuring and painting.
97
+ * @param color - CSS color baked into the raster.
98
+ * @param text - The run to rasterize (may contain CJK / emoji).
99
+ * @returns The raster, or `null` in a non-DOM/headless context (caller should
100
+ * fall back to {@link IRenderer.fillText}).
101
+ */
102
+ get(font: string, color: string, text: string): TextRaster | null;
103
+ /** Drop all cached rasters and reset instrumentation. */
104
+ clear(): void;
105
+ }
@@ -4,3 +4,4 @@ export * from './WebGLPointRenderer';
4
4
  export * from './WebGPUParticleSystemManager';
5
5
  export * from './IRenderer';
6
6
  export * from './colorParse';
7
+ export * from './TextRasterCache';
package/dist/renderer.js CHANGED
@@ -4,11 +4,13 @@
4
4
 
5
5
 
6
6
 
7
- var _chunkORCPCIJ7js = require('./chunk-ORCPCIJ7.js');
8
7
 
8
+ var _chunkBEUIB3U7js = require('./chunk-BEUIB3U7.js');
9
9
 
10
10
 
11
11
 
12
12
 
13
13
 
14
- exports.CanvasRenderer = _chunkORCPCIJ7js.CanvasRenderer; exports.SVGRenderer = _chunkORCPCIJ7js.SVGRenderer; exports.WebGPUParticleSystemManager = _chunkORCPCIJ7js.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkORCPCIJ7js.createWebGLPointRenderer; exports.parseColorToRGBA = _chunkORCPCIJ7js.parseColorToRGBA;
14
+
15
+
16
+ exports.CanvasRenderer = _chunkBEUIB3U7js.CanvasRenderer; exports.SVGRenderer = _chunkBEUIB3U7js.SVGRenderer; exports.TextRasterCache = _chunkBEUIB3U7js.TextRasterCache; exports.WebGPUParticleSystemManager = _chunkBEUIB3U7js.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkBEUIB3U7js.createWebGLPointRenderer; exports.parseColorToRGBA = _chunkBEUIB3U7js.parseColorToRGBA;
package/dist/renderer.mjs CHANGED
@@ -1,13 +1,15 @@
1
1
  import {
2
2
  CanvasRenderer,
3
3
  SVGRenderer,
4
+ TextRasterCache,
4
5
  WebGPUParticleSystemManager,
5
6
  createWebGLPointRenderer,
6
7
  parseColorToRGBA
7
- } from "./chunk-XZEPHCDZ.mjs";
8
+ } from "./chunk-L5BCKFQE.mjs";
8
9
  export {
9
10
  CanvasRenderer,
10
11
  SVGRenderer,
12
+ TextRasterCache,
11
13
  WebGPUParticleSystemManager,
12
14
  createWebGLPointRenderer,
13
15
  parseColorToRGBA
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectojs/core",
3
- "version": "1.11.3",
3
+ "version": "1.12.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },