@vectojs/core 1.11.3 → 1.13.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/{chunk-ORCPCIJ7.js → chunk-BEUIB3U7.js} +135 -34
- package/dist/{chunk-XZEPHCDZ.mjs → chunk-L5BCKFQE.mjs} +105 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +151 -93
- package/dist/index.mjs +59 -1
- package/dist/renderer/CanvasRenderer.d.ts +2 -0
- package/dist/renderer/TextRasterCache.d.ts +105 -0
- package/dist/renderer/index.d.ts +1 -0
- package/dist/renderer.js +4 -2
- package/dist/renderer.mjs +3 -1
- package/dist/tree/Scene.d.ts +52 -0
- package/package.json +1 -1
|
@@ -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.
|
|
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.
|
|
205
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
330
|
-
|
|
344
|
+
__init17() {this.alphaStack = []}
|
|
345
|
+
__init18() {this.globalAlpha = 1}
|
|
331
346
|
// Clipping stack
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
constructor(width, height) {;_class2.prototype.
|
|
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
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
constructor(device) {;_class3.prototype.
|
|
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.
|
|
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.
|
|
205
|
-
|
|
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
|
-
|
|
15
|
+
|
|
16
|
+
var _chunkBEUIB3U7js = require('./chunk-BEUIB3U7.js');
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
|
|
@@ -666,16 +667,29 @@ var Scene = (_class2 = class _Scene {
|
|
|
666
667
|
__init10() {this.dirty = true}
|
|
667
668
|
/** Whether to throttle rendering to 2 FPS when the scene is static to save power. */
|
|
668
669
|
__init11() {this.autoThrottle = true}
|
|
670
|
+
// --- Frame telemetry (read via `frameStats`) ---------------------------
|
|
671
|
+
/** Wall-clock ms spent inside the last `render()` call. */
|
|
672
|
+
__init12() {this._lastFrameMs = 0}
|
|
673
|
+
/** Rolling exponential average of rendered-frame intervals, in ms. */
|
|
674
|
+
__init13() {this._avgFrameIntervalMs = 0}
|
|
675
|
+
/** dt (ms) handed to the last rendered frame. */
|
|
676
|
+
__init14() {this._lastDt = 0}
|
|
677
|
+
/** Count of frames actually rendered since the loop started. */
|
|
678
|
+
__init15() {this._renderedFrames = 0}
|
|
679
|
+
/** Count of rAF ticks skipped (idle / capped) since the loop started. */
|
|
680
|
+
__init16() {this._skippedFrames = 0}
|
|
681
|
+
/** `time` of the previous *rendered* frame, for interval measurement. */
|
|
682
|
+
__init17() {this._lastRenderTick = 0}
|
|
669
683
|
/**
|
|
670
684
|
* Frame-rate cap (power saving). `0` = uncapped (native refresh). When set,
|
|
671
685
|
* the loop renders at most `maxFPS` times per second; animations still run,
|
|
672
686
|
* just less often. See {@link SceneOptions.maxFPS}.
|
|
673
687
|
*/
|
|
674
|
-
|
|
688
|
+
__init18() {this.maxFPS = 60}
|
|
675
689
|
/** Whether the OS prefers-reduced-motion setting auto-caps the loop. */
|
|
676
|
-
|
|
690
|
+
__init19() {this.respectReducedMotion = true}
|
|
677
691
|
/** Cached media-query list; `.matches` is read live each frame. */
|
|
678
|
-
|
|
692
|
+
__init20() {this.reducedMotionQuery = null}
|
|
679
693
|
/** True when the OS asks for reduced motion and we respect it. Read by the animation drivers. */
|
|
680
694
|
get prefersReducedMotion() {
|
|
681
695
|
return this.respectReducedMotion && !!_optionalChain([this, 'access', _21 => _21.reducedMotionQuery, 'optionalAccess', _22 => _22.matches]);
|
|
@@ -684,95 +698,95 @@ var Scene = (_class2 = class _Scene {
|
|
|
684
698
|
* Throttle interval (ms) for the a11y/automation shadow sync. `0` = every
|
|
685
699
|
* frame. See {@link SceneOptions.a11ySyncInterval}.
|
|
686
700
|
*/
|
|
687
|
-
|
|
701
|
+
__init21() {this.a11ySyncInterval = 0}
|
|
688
702
|
/** Timestamp of the last a11y sync, for throttling. */
|
|
689
|
-
|
|
703
|
+
__init22() {this.lastA11ySync = -Infinity}
|
|
690
704
|
/** True if we skipped an a11y sync during animation and need to sync when at rest. */
|
|
691
|
-
|
|
705
|
+
__init23() {this.a11yPendingSyncAfterAnimation = false}
|
|
692
706
|
// A11y / Automation Layer. `null` in non-DOM (SSR/Node) environments — the
|
|
693
707
|
// whole projection degrades to a no-op so the engine's logic stays usable
|
|
694
708
|
// server-side (e.g. headless layout / vector export) without jsdom.
|
|
695
709
|
|
|
696
|
-
|
|
710
|
+
__init24() {this.a11yElements = /* @__PURE__ */ new Map()}
|
|
697
711
|
/** DOM nodes mirroring static text content, keyed by entity id. */
|
|
698
|
-
|
|
712
|
+
__init25() {this.contentElements = /* @__PURE__ */ new Map()}
|
|
699
713
|
/** Pending cold font-calibration frame per projected grid entity. */
|
|
700
|
-
|
|
714
|
+
__init26() {this.contentGridCalibrationFrames = /* @__PURE__ */ new Map()}
|
|
701
715
|
/** Detached, untransformed font probes used by the cold calibration pass. */
|
|
702
|
-
|
|
716
|
+
__init27() {this.contentGridCalibrationProbes = /* @__PURE__ */ new Map()}
|
|
703
717
|
/** Invalidates grid font calibration after browser font availability changes. */
|
|
704
|
-
|
|
718
|
+
__init28() {this.contentFontEpoch = 0}
|
|
705
719
|
/** Cached Canvas-to-client scale for the current font/viewport epoch. */
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
720
|
+
__init29() {this.contentMetricScaleEpoch = -1}
|
|
721
|
+
__init30() {this.contentMetricScaleX = 1}
|
|
722
|
+
__init31() {this.contentProjectionEnabled = true}
|
|
709
723
|
/**
|
|
710
724
|
* True while a text-selection drag that started on a projection's blank
|
|
711
725
|
* region (no text node under the press) is being driven manually — the
|
|
712
726
|
* browser has no native anchor for it, so mousemove extends the Selection
|
|
713
727
|
* from the position we resolved ourselves.
|
|
714
728
|
*/
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
729
|
+
__init32() {this.blankRegionSelectionDrag = false}
|
|
730
|
+
__init33() {this.contentSelectionAnchor = null}
|
|
731
|
+
__init34() {this.contentSelectionEndListener = null}
|
|
718
732
|
// Animation/interactive flags collected during the render walk (tree-walk
|
|
719
733
|
// fusion): the loop reads last frame's answers instead of re-walking the
|
|
720
734
|
// tree up to 4× per tick. Start true so the first tick stays conservative.
|
|
721
|
-
|
|
722
|
-
|
|
735
|
+
__init35() {this.frameHadAnimation = true}
|
|
736
|
+
__init36() {this.frameHadInteractive = true}
|
|
723
737
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
738
|
+
__init37() {this.focusedA11yElement = null}
|
|
739
|
+
__init38() {this.caretBlinkTimer = null}
|
|
740
|
+
__init39() {this.a11yNeedsReorder = true}
|
|
741
|
+
__init40() {this.portalRoot = null}
|
|
742
|
+
__init41() {this.fullViewportElements = []}
|
|
743
|
+
__init42() {this.normalElements = []}
|
|
744
|
+
__init43() {this.activeIds = /* @__PURE__ */ new Set()}
|
|
745
|
+
__init44() {this.activePortalsThisFrame = /* @__PURE__ */ new Set()}
|
|
746
|
+
__init45() {this.activePortalsPrevFrame = /* @__PURE__ */ new Set()}
|
|
747
|
+
__init46() {this.portalEntities = /* @__PURE__ */ new Map()}
|
|
748
|
+
__init47() {this.renderOrderCounter = 0}
|
|
735
749
|
/**
|
|
736
750
|
* Authoritative paint order for semantic nodes discovered during the main
|
|
737
751
|
* render. A node may not have a DOM projection until the following a11y
|
|
738
752
|
* sync, so retaining the order prevents a newly opened overlay from spending
|
|
739
753
|
* its first frame below previously projected controls.
|
|
740
754
|
*/
|
|
741
|
-
|
|
755
|
+
__init48() {this.a11yRenderOrders = /* @__PURE__ */ new Map()}
|
|
742
756
|
// Optional WebGL point-cloud layer (see SceneOptions.pointBackend).
|
|
743
|
-
|
|
744
|
-
|
|
757
|
+
__init49() {this.pointRenderer = null}
|
|
758
|
+
__init50() {this.glCanvas = null}
|
|
745
759
|
|
|
746
760
|
|
|
747
761
|
|
|
748
|
-
|
|
762
|
+
__init51() {this.disableWindowResize = false}
|
|
749
763
|
/** See {@link SceneOptions.maxDPR}. `undefined` = uncapped (real DPR). */
|
|
750
764
|
|
|
751
765
|
// WebGPU properties
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
766
|
+
__init52() {this.destroyed = false}
|
|
767
|
+
__init53() {this.device = null}
|
|
768
|
+
__init54() {this.deviceLost = false}
|
|
769
|
+
__init55() {this.particleBackend = "auto"}
|
|
770
|
+
__init56() {this._webgpuDisabled = false}
|
|
757
771
|
get webgpuDisabled() {
|
|
758
772
|
return this._webgpuDisabled || this.particleBackend === "cpu";
|
|
759
773
|
}
|
|
760
774
|
set webgpuDisabled(value) {
|
|
761
775
|
this._webgpuDisabled = value;
|
|
762
776
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
777
|
+
__init57() {this.recoveryTimerId = null}
|
|
778
|
+
__init58() {this.manager = null}
|
|
779
|
+
__init59() {this.initializingWebGPU = false}
|
|
780
|
+
__init60() {this.gpuCanvas = null}
|
|
781
|
+
__init61() {this.gpuContext = null}
|
|
768
782
|
/** True while the GPU canvas holds a presented particle frame (needs clearing when they leave). */
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
783
|
+
__init62() {this.gpuHasContent = false}
|
|
784
|
+
__init63() {this.mouseX = -9999}
|
|
785
|
+
__init64() {this.mouseY = -9999}
|
|
786
|
+
__init65() {this.pointerMoveListener = null}
|
|
787
|
+
__init66() {this.pointerLeaveListener = null}
|
|
788
|
+
__init67() {this.hasWarnedZeroSize = false}
|
|
789
|
+
__init68() {this.fontLoadHandler = null}
|
|
776
790
|
// ── Dev-mode warning infrastructure ──────────────────────────────
|
|
777
791
|
//
|
|
778
792
|
// Enable with `Scene.devMode = true` or by setting `globalThis.__DEV__`.
|
|
@@ -790,7 +804,7 @@ var Scene = (_class2 = class _Scene {
|
|
|
790
804
|
return false;
|
|
791
805
|
}
|
|
792
806
|
|
|
793
|
-
|
|
807
|
+
__init69() {this._devFrameCount = 0}
|
|
794
808
|
_devWarn(message) {
|
|
795
809
|
if (!this._devActive) return;
|
|
796
810
|
console.warn(`[vectojs/dev] ${message}`);
|
|
@@ -834,7 +848,7 @@ var Scene = (_class2 = class _Scene {
|
|
|
834
848
|
};
|
|
835
849
|
walkProjections(this.root);
|
|
836
850
|
}
|
|
837
|
-
constructor(canvas, options = {}) {;_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);_class2.prototype.__init29.call(this);_class2.prototype.__init30.call(this);_class2.prototype.__init31.call(this);_class2.prototype.__init32.call(this);_class2.prototype.__init33.call(this);_class2.prototype.__init34.call(this);_class2.prototype.__init35.call(this);_class2.prototype.__init36.call(this);_class2.prototype.__init37.call(this);_class2.prototype.__init38.call(this);_class2.prototype.__init39.call(this);_class2.prototype.__init40.call(this);_class2.prototype.__init41.call(this);_class2.prototype.__init42.call(this);_class2.prototype.__init43.call(this);_class2.prototype.__init44.call(this);_class2.prototype.__init45.call(this);_class2.prototype.__init46.call(this);_class2.prototype.__init47.call(this);_class2.prototype.__init48.call(this);_class2.prototype.__init49.call(this);_class2.prototype.__init50.call(this);_class2.prototype.__init51.call(this);_class2.prototype.__init52.call(this);_class2.prototype.__init53.call(this);_class2.prototype.__init54.call(this);_class2.prototype.__init55.call(this);_class2.prototype.__init56.call(this);_class2.prototype.__init57.call(this);_class2.prototype.__init58.call(this);_class2.prototype.__init59.call(this);_class2.prototype.__init60.call(this);_class2.prototype.__init61.call(this);_class2.prototype.__init62.call(this);_class2.prototype.__init63.call(this);
|
|
851
|
+
constructor(canvas, options = {}) {;_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);_class2.prototype.__init29.call(this);_class2.prototype.__init30.call(this);_class2.prototype.__init31.call(this);_class2.prototype.__init32.call(this);_class2.prototype.__init33.call(this);_class2.prototype.__init34.call(this);_class2.prototype.__init35.call(this);_class2.prototype.__init36.call(this);_class2.prototype.__init37.call(this);_class2.prototype.__init38.call(this);_class2.prototype.__init39.call(this);_class2.prototype.__init40.call(this);_class2.prototype.__init41.call(this);_class2.prototype.__init42.call(this);_class2.prototype.__init43.call(this);_class2.prototype.__init44.call(this);_class2.prototype.__init45.call(this);_class2.prototype.__init46.call(this);_class2.prototype.__init47.call(this);_class2.prototype.__init48.call(this);_class2.prototype.__init49.call(this);_class2.prototype.__init50.call(this);_class2.prototype.__init51.call(this);_class2.prototype.__init52.call(this);_class2.prototype.__init53.call(this);_class2.prototype.__init54.call(this);_class2.prototype.__init55.call(this);_class2.prototype.__init56.call(this);_class2.prototype.__init57.call(this);_class2.prototype.__init58.call(this);_class2.prototype.__init59.call(this);_class2.prototype.__init60.call(this);_class2.prototype.__init61.call(this);_class2.prototype.__init62.call(this);_class2.prototype.__init63.call(this);_class2.prototype.__init64.call(this);_class2.prototype.__init65.call(this);_class2.prototype.__init66.call(this);_class2.prototype.__init67.call(this);_class2.prototype.__init68.call(this);_class2.prototype.__init69.call(this);
|
|
838
852
|
this.canvas = canvas;
|
|
839
853
|
this.debugA11y = _nullishCoalesce(options.debugA11y, () => ( false));
|
|
840
854
|
this.disableWindowResize = _nullishCoalesce(options.disableWindowResize, () => ( false));
|
|
@@ -878,7 +892,7 @@ var Scene = (_class2 = class _Scene {
|
|
|
878
892
|
if (options.renderer) {
|
|
879
893
|
this.renderer = options.renderer;
|
|
880
894
|
} else {
|
|
881
|
-
this.renderer = new (0,
|
|
895
|
+
this.renderer = new (0, _chunkBEUIB3U7js.CanvasRenderer)(
|
|
882
896
|
canvas,
|
|
883
897
|
this.disableWindowResize ? { width: this.width, height: this.height } : void 0,
|
|
884
898
|
this.maxDPR
|
|
@@ -1317,6 +1331,36 @@ var Scene = (_class2 = class _Scene {
|
|
|
1317
1331
|
markDirty() {
|
|
1318
1332
|
this.dirty = true;
|
|
1319
1333
|
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Live frame telemetry for profilers and devtools overlays. All timings are
|
|
1336
|
+
* measured on the `requestAnimationFrame` loop; a scene driven only by
|
|
1337
|
+
* {@link step} (e.g. deterministic video export) leaves these at their zero
|
|
1338
|
+
* defaults.
|
|
1339
|
+
*
|
|
1340
|
+
* `fps` is derived from the interval between *rendered* frames, so idle
|
|
1341
|
+
* `onDemand` scenes and frames skipped by the {@link maxFPS} cap or the
|
|
1342
|
+
* static auto-throttle do not deflate it — it reports the cadence of actual
|
|
1343
|
+
* redraws, not the raw rAF rate. `frameTimeMs` is the wall-clock cost of the
|
|
1344
|
+
* last `render()` pass alone (excludes a11y/content-projection sync).
|
|
1345
|
+
*
|
|
1346
|
+
* The renderer always repaints the full canvas, so there is no partial
|
|
1347
|
+
* dirty-rectangle to expose; `dirty` is the boolean redraw-pending flag and
|
|
1348
|
+
* `pendingRedraw` reflects whether the next `onDemand` tick will actually
|
|
1349
|
+
* render.
|
|
1350
|
+
*/
|
|
1351
|
+
get frameStats() {
|
|
1352
|
+
const interval = this._avgFrameIntervalMs;
|
|
1353
|
+
return {
|
|
1354
|
+
fps: interval > 0 ? Math.min(1e3 / interval, this.maxFPS > 0 ? this.maxFPS : 1e3 / interval) : 0,
|
|
1355
|
+
frameTimeMs: this._lastFrameMs,
|
|
1356
|
+
frameIntervalMs: interval,
|
|
1357
|
+
dt: this._lastDt,
|
|
1358
|
+
renderedFrames: this._renderedFrames,
|
|
1359
|
+
skippedFrames: this._skippedFrames,
|
|
1360
|
+
renderMode: this.renderMode,
|
|
1361
|
+
dirty: this.dirty
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1320
1364
|
/** True when any node in the subtree has a pending animation. */
|
|
1321
1365
|
/** True when any node in the subtree is interactive (drives a11y sync). */
|
|
1322
1366
|
syncOptionalAttribute(element, name, value) {
|
|
@@ -1521,7 +1565,7 @@ var Scene = (_class2 = class _Scene {
|
|
|
1521
1565
|
this.syncOptionalAttribute(
|
|
1522
1566
|
el,
|
|
1523
1567
|
"href",
|
|
1524
|
-
attrs.href === void 0 ? void 0 :
|
|
1568
|
+
attrs.href === void 0 ? void 0 : _chunkBEUIB3U7js.sanitizeUrl.call(void 0, attrs.href)
|
|
1525
1569
|
);
|
|
1526
1570
|
this.syncOptionalAttribute(el, "target", attrs.target);
|
|
1527
1571
|
}
|
|
@@ -2220,6 +2264,7 @@ var Scene = (_class2 = class _Scene {
|
|
|
2220
2264
|
cap = Math.min(cap, 2);
|
|
2221
2265
|
}
|
|
2222
2266
|
if (cap > 0 && time - this.lastTime < 1e3 / cap - 1) {
|
|
2267
|
+
this._skippedFrames++;
|
|
2223
2268
|
this.scheduleFrame();
|
|
2224
2269
|
return;
|
|
2225
2270
|
}
|
|
@@ -2230,11 +2275,23 @@ var Scene = (_class2 = class _Scene {
|
|
|
2230
2275
|
}
|
|
2231
2276
|
this.lastTime = time;
|
|
2232
2277
|
if (this.renderMode === "onDemand" && isIdle) {
|
|
2278
|
+
this._skippedFrames++;
|
|
2233
2279
|
this.scheduleFrame();
|
|
2234
2280
|
return;
|
|
2235
2281
|
}
|
|
2236
2282
|
this.dirty = false;
|
|
2283
|
+
const now = typeof performance !== "undefined" ? performance.now() : time;
|
|
2284
|
+
if (this._lastRenderTick > 0) {
|
|
2285
|
+
const interval = time - this._lastRenderTick;
|
|
2286
|
+
if (interval > 0) {
|
|
2287
|
+
this._avgFrameIntervalMs = this._avgFrameIntervalMs === 0 ? interval : this._avgFrameIntervalMs * 0.9 + interval * 0.1;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
this._lastRenderTick = time;
|
|
2291
|
+
this._lastDt = dt;
|
|
2237
2292
|
this.render(this.renderer, dt, time);
|
|
2293
|
+
this._lastFrameMs = (typeof performance !== "undefined" ? performance.now() : time) - now;
|
|
2294
|
+
this._renderedFrames++;
|
|
2238
2295
|
const hasActiveAnimation = this.frameHadAnimation;
|
|
2239
2296
|
const hasInteractive = this.frameHadInteractive;
|
|
2240
2297
|
const wantsContentSync = this.contentProjectionEnabled;
|
|
@@ -2550,7 +2607,7 @@ var Scene = (_class2 = class _Scene {
|
|
|
2550
2607
|
* Export the current scene state to a lightweight, flat SVG XML string.
|
|
2551
2608
|
*/
|
|
2552
2609
|
toSVG() {
|
|
2553
|
-
const renderer = new (0,
|
|
2610
|
+
const renderer = new (0, _chunkBEUIB3U7js.SVGRenderer)(this.width, this.height);
|
|
2554
2611
|
this.render(renderer, 0, 0);
|
|
2555
2612
|
return renderer.toXMLString();
|
|
2556
2613
|
}
|
|
@@ -2761,15 +2818,15 @@ var TextEntity = (_class3 = class extends _chunk2Z23LTH3js.Entity {
|
|
|
2761
2818
|
|
|
2762
2819
|
|
|
2763
2820
|
|
|
2764
|
-
|
|
2821
|
+
__init70() {this.nodes = []}
|
|
2765
2822
|
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2823
|
+
__init71() {this.fillStyle = "#94a3b8"}
|
|
2824
|
+
__init72() {this.strokeStyle = null}
|
|
2825
|
+
__init73() {this.hoveredFillStyle = "#ffffff"}
|
|
2826
|
+
__init74() {this.lineWidth = 1}
|
|
2827
|
+
__init75() {this.isHovered = false}
|
|
2771
2828
|
constructor(text, atlas, maxWidth, fontSize = 24) {
|
|
2772
|
-
super();_class3.prototype.
|
|
2829
|
+
super();_class3.prototype.__init70.call(this);_class3.prototype.__init71.call(this);_class3.prototype.__init72.call(this);_class3.prototype.__init73.call(this);_class3.prototype.__init74.call(this);_class3.prototype.__init75.call(this);;
|
|
2773
2830
|
this.text = text;
|
|
2774
2831
|
this.atlas = atlas;
|
|
2775
2832
|
this.fontSize = fontSize;
|
|
@@ -2884,15 +2941,15 @@ var TextEntity = (_class3 = class extends _chunk2Z23LTH3js.Entity {
|
|
|
2884
2941
|
// src/components/GridTextEntity.ts
|
|
2885
2942
|
var GridTextEntity = (_class4 = class extends _chunk2Z23LTH3js.Entity {
|
|
2886
2943
|
|
|
2887
|
-
|
|
2888
|
-
|
|
2944
|
+
__init76() {this.fillStyle = "#ffffff"}
|
|
2945
|
+
__init77() {this.grid = []}
|
|
2889
2946
|
// Array of rows
|
|
2890
|
-
|
|
2891
|
-
|
|
2947
|
+
__init78() {this.cols = 0}
|
|
2948
|
+
__init79() {this.rows = 0}
|
|
2892
2949
|
|
|
2893
2950
|
|
|
2894
2951
|
constructor(_atlas, fontSize = 10) {
|
|
2895
|
-
super();_class4.prototype.
|
|
2952
|
+
super();_class4.prototype.__init76.call(this);_class4.prototype.__init77.call(this);_class4.prototype.__init78.call(this);_class4.prototype.__init79.call(this);;
|
|
2896
2953
|
this.fontSize = fontSize;
|
|
2897
2954
|
this.charWidth = fontSize * 1;
|
|
2898
2955
|
this.charHeight = fontSize * 1.1;
|
|
@@ -2984,23 +3041,23 @@ var SplineEntity = (_class5 = class extends _chunk2Z23LTH3js.Entity {
|
|
|
2984
3041
|
|
|
2985
3042
|
|
|
2986
3043
|
|
|
2987
|
-
|
|
2988
|
-
|
|
3044
|
+
__init80() {this.offscreen = null}
|
|
3045
|
+
__init81() {this.baked = false}
|
|
2989
3046
|
/** Logical (CSS-pixel) size of the baked bitmap — the blit destination size. */
|
|
2990
|
-
|
|
2991
|
-
|
|
3047
|
+
__init82() {this.bakedWidth = 0}
|
|
3048
|
+
__init83() {this.bakedHeight = 0}
|
|
2992
3049
|
/** Gradient strokes can't be baked to a solid-color bitmap; they render per-frame. */
|
|
2993
3050
|
|
|
2994
3051
|
/** Lazily-flattened polylines (one Float32Array of [x,y,...] per segment) for hit-testing. */
|
|
2995
|
-
|
|
3052
|
+
__init84() {this.polylines = null}
|
|
2996
3053
|
/**
|
|
2997
3054
|
* When `true`, the renderer draws a rounded-rect outline of the entity's
|
|
2998
3055
|
* local bounds after painting the curves. Useful for drag feedback and
|
|
2999
3056
|
* debugging hit areas. Defaults to `false`.
|
|
3000
3057
|
*/
|
|
3001
|
-
|
|
3058
|
+
__init85() {this.showBounds = false}
|
|
3002
3059
|
constructor(doc, opts = {}) {
|
|
3003
|
-
super();_class5.prototype.
|
|
3060
|
+
super();_class5.prototype.__init80.call(this);_class5.prototype.__init81.call(this);_class5.prototype.__init82.call(this);_class5.prototype.__init83.call(this);_class5.prototype.__init84.call(this);_class5.prototype.__init85.call(this);;
|
|
3004
3061
|
this.doc = doc;
|
|
3005
3062
|
this.lineWidth = _nullishCoalesce(opts.lineWidth, () => ( 2));
|
|
3006
3063
|
this.cache = _nullishCoalesce(opts.cache, () => ( true));
|
|
@@ -3375,9 +3432,9 @@ var Group = class extends _chunk2Z23LTH3js.Entity {
|
|
|
3375
3432
|
// src/math/SpatialHashGrid.ts
|
|
3376
3433
|
var SpatialHashGrid = (_class6 = class {
|
|
3377
3434
|
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
constructor(cellSize = 64) {;_class6.prototype.
|
|
3435
|
+
__init86() {this.grid = /* @__PURE__ */ new Map()}
|
|
3436
|
+
__init87() {this.entityCells = /* @__PURE__ */ new Map()}
|
|
3437
|
+
constructor(cellSize = 64) {;_class6.prototype.__init86.call(this);_class6.prototype.__init87.call(this);
|
|
3381
3438
|
this.cellSize = cellSize;
|
|
3382
3439
|
}
|
|
3383
3440
|
hash(cx, cy) {
|
|
@@ -3469,18 +3526,18 @@ var SpatialHashGrid = (_class6 = class {
|
|
|
3469
3526
|
// src/tree/DOMPortalEntity.ts
|
|
3470
3527
|
var DOMPortalEntity = (_class7 = class extends _chunk2Z23LTH3js.Entity {
|
|
3471
3528
|
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3529
|
+
__init88() {this.isDOMPortal = true}
|
|
3530
|
+
__init89() {this.domListeners = []}
|
|
3531
|
+
__init90() {this.resizeObserver = null}
|
|
3532
|
+
__init91() {this.cachedWidth = 100}
|
|
3533
|
+
__init92() {this.cachedHeight = 100}
|
|
3534
|
+
__init93() {this.lastWidth = ""}
|
|
3535
|
+
__init94() {this.lastHeight = ""}
|
|
3536
|
+
__init95() {this.lastTransform = ""}
|
|
3537
|
+
__init96() {this.lastZIndex = ""}
|
|
3538
|
+
__init97() {this.lastOpacity = ""}
|
|
3482
3539
|
constructor(domElement, width, height, id) {
|
|
3483
|
-
super(id);_class7.prototype.
|
|
3540
|
+
super(id);_class7.prototype.__init88.call(this);_class7.prototype.__init89.call(this);_class7.prototype.__init90.call(this);_class7.prototype.__init91.call(this);_class7.prototype.__init92.call(this);_class7.prototype.__init93.call(this);_class7.prototype.__init94.call(this);_class7.prototype.__init95.call(this);_class7.prototype.__init96.call(this);_class7.prototype.__init97.call(this);;
|
|
3484
3541
|
this.domElement = domElement;
|
|
3485
3542
|
this.width = _nullishCoalesce(width, () => ( 0));
|
|
3486
3543
|
this.height = _nullishCoalesce(height, () => ( 0));
|
|
@@ -3567,8 +3624,9 @@ var DOMPortalEntity = (_class7 = class extends _chunk2Z23LTH3js.Entity {
|
|
|
3567
3624
|
}, _class7);
|
|
3568
3625
|
|
|
3569
3626
|
// src/index.ts
|
|
3570
|
-
Scene.registerWebGLPointRendererCreator(
|
|
3571
|
-
Scene.registerWebGPUParticleSystemManager(
|
|
3627
|
+
Scene.registerWebGLPointRendererCreator(_chunkBEUIB3U7js.createWebGLPointRenderer);
|
|
3628
|
+
Scene.registerWebGPUParticleSystemManager(_chunkBEUIB3U7js.WebGPUParticleSystemManager);
|
|
3629
|
+
|
|
3572
3630
|
|
|
3573
3631
|
|
|
3574
3632
|
|
|
@@ -3619,4 +3677,4 @@ Scene.registerWebGPUParticleSystemManager(_chunkORCPCIJ7js.WebGPUParticleSystemM
|
|
|
3619
3677
|
|
|
3620
3678
|
|
|
3621
3679
|
|
|
3622
|
-
exports.ArabicShaper = _chunk4AR425ARjs.ArabicShaper; exports.BidiResolver = _chunk4AR425ARjs.BidiResolver; exports.CanvasRenderer =
|
|
3680
|
+
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-
|
|
16
|
+
} from "./chunk-L5BCKFQE.mjs";
|
|
16
17
|
import {
|
|
17
18
|
Easing,
|
|
18
19
|
Entity,
|
|
@@ -666,6 +667,19 @@ var Scene = class _Scene {
|
|
|
666
667
|
dirty = true;
|
|
667
668
|
/** Whether to throttle rendering to 2 FPS when the scene is static to save power. */
|
|
668
669
|
autoThrottle = true;
|
|
670
|
+
// --- Frame telemetry (read via `frameStats`) ---------------------------
|
|
671
|
+
/** Wall-clock ms spent inside the last `render()` call. */
|
|
672
|
+
_lastFrameMs = 0;
|
|
673
|
+
/** Rolling exponential average of rendered-frame intervals, in ms. */
|
|
674
|
+
_avgFrameIntervalMs = 0;
|
|
675
|
+
/** dt (ms) handed to the last rendered frame. */
|
|
676
|
+
_lastDt = 0;
|
|
677
|
+
/** Count of frames actually rendered since the loop started. */
|
|
678
|
+
_renderedFrames = 0;
|
|
679
|
+
/** Count of rAF ticks skipped (idle / capped) since the loop started. */
|
|
680
|
+
_skippedFrames = 0;
|
|
681
|
+
/** `time` of the previous *rendered* frame, for interval measurement. */
|
|
682
|
+
_lastRenderTick = 0;
|
|
669
683
|
/**
|
|
670
684
|
* Frame-rate cap (power saving). `0` = uncapped (native refresh). When set,
|
|
671
685
|
* the loop renders at most `maxFPS` times per second; animations still run,
|
|
@@ -1317,6 +1331,36 @@ var Scene = class _Scene {
|
|
|
1317
1331
|
markDirty() {
|
|
1318
1332
|
this.dirty = true;
|
|
1319
1333
|
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Live frame telemetry for profilers and devtools overlays. All timings are
|
|
1336
|
+
* measured on the `requestAnimationFrame` loop; a scene driven only by
|
|
1337
|
+
* {@link step} (e.g. deterministic video export) leaves these at their zero
|
|
1338
|
+
* defaults.
|
|
1339
|
+
*
|
|
1340
|
+
* `fps` is derived from the interval between *rendered* frames, so idle
|
|
1341
|
+
* `onDemand` scenes and frames skipped by the {@link maxFPS} cap or the
|
|
1342
|
+
* static auto-throttle do not deflate it — it reports the cadence of actual
|
|
1343
|
+
* redraws, not the raw rAF rate. `frameTimeMs` is the wall-clock cost of the
|
|
1344
|
+
* last `render()` pass alone (excludes a11y/content-projection sync).
|
|
1345
|
+
*
|
|
1346
|
+
* The renderer always repaints the full canvas, so there is no partial
|
|
1347
|
+
* dirty-rectangle to expose; `dirty` is the boolean redraw-pending flag and
|
|
1348
|
+
* `pendingRedraw` reflects whether the next `onDemand` tick will actually
|
|
1349
|
+
* render.
|
|
1350
|
+
*/
|
|
1351
|
+
get frameStats() {
|
|
1352
|
+
const interval = this._avgFrameIntervalMs;
|
|
1353
|
+
return {
|
|
1354
|
+
fps: interval > 0 ? Math.min(1e3 / interval, this.maxFPS > 0 ? this.maxFPS : 1e3 / interval) : 0,
|
|
1355
|
+
frameTimeMs: this._lastFrameMs,
|
|
1356
|
+
frameIntervalMs: interval,
|
|
1357
|
+
dt: this._lastDt,
|
|
1358
|
+
renderedFrames: this._renderedFrames,
|
|
1359
|
+
skippedFrames: this._skippedFrames,
|
|
1360
|
+
renderMode: this.renderMode,
|
|
1361
|
+
dirty: this.dirty
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1320
1364
|
/** True when any node in the subtree has a pending animation. */
|
|
1321
1365
|
/** True when any node in the subtree is interactive (drives a11y sync). */
|
|
1322
1366
|
syncOptionalAttribute(element, name, value) {
|
|
@@ -2220,6 +2264,7 @@ var Scene = class _Scene {
|
|
|
2220
2264
|
cap = Math.min(cap, 2);
|
|
2221
2265
|
}
|
|
2222
2266
|
if (cap > 0 && time - this.lastTime < 1e3 / cap - 1) {
|
|
2267
|
+
this._skippedFrames++;
|
|
2223
2268
|
this.scheduleFrame();
|
|
2224
2269
|
return;
|
|
2225
2270
|
}
|
|
@@ -2230,11 +2275,23 @@ var Scene = class _Scene {
|
|
|
2230
2275
|
}
|
|
2231
2276
|
this.lastTime = time;
|
|
2232
2277
|
if (this.renderMode === "onDemand" && isIdle) {
|
|
2278
|
+
this._skippedFrames++;
|
|
2233
2279
|
this.scheduleFrame();
|
|
2234
2280
|
return;
|
|
2235
2281
|
}
|
|
2236
2282
|
this.dirty = false;
|
|
2283
|
+
const now = typeof performance !== "undefined" ? performance.now() : time;
|
|
2284
|
+
if (this._lastRenderTick > 0) {
|
|
2285
|
+
const interval = time - this._lastRenderTick;
|
|
2286
|
+
if (interval > 0) {
|
|
2287
|
+
this._avgFrameIntervalMs = this._avgFrameIntervalMs === 0 ? interval : this._avgFrameIntervalMs * 0.9 + interval * 0.1;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
this._lastRenderTick = time;
|
|
2291
|
+
this._lastDt = dt;
|
|
2237
2292
|
this.render(this.renderer, dt, time);
|
|
2293
|
+
this._lastFrameMs = (typeof performance !== "undefined" ? performance.now() : time) - now;
|
|
2294
|
+
this._renderedFrames++;
|
|
2238
2295
|
const hasActiveAnimation = this.frameHadAnimation;
|
|
2239
2296
|
const hasInteractive = this.frameHadInteractive;
|
|
2240
2297
|
const wantsContentSync = this.contentProjectionEnabled;
|
|
@@ -3604,6 +3661,7 @@ export {
|
|
|
3604
3661
|
SpringDriver,
|
|
3605
3662
|
SpringPhysics,
|
|
3606
3663
|
TextEntity,
|
|
3664
|
+
TextRasterCache,
|
|
3607
3665
|
TweenDriver,
|
|
3608
3666
|
VectoJSEvent,
|
|
3609
3667
|
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
|
+
}
|
package/dist/renderer/index.d.ts
CHANGED
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
|
-
|
|
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-
|
|
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/dist/tree/Scene.d.ts
CHANGED
|
@@ -101,6 +101,28 @@ export interface SceneOptions {
|
|
|
101
101
|
}
|
|
102
102
|
/** Frame-rate the loop is capped to when the OS requests reduced motion. */
|
|
103
103
|
export declare const REDUCED_MOTION_FPS = 30;
|
|
104
|
+
/**
|
|
105
|
+
* Live render-loop telemetry, read from {@link Scene.frameStats}. See that
|
|
106
|
+
* getter for how each field is measured.
|
|
107
|
+
*/
|
|
108
|
+
export interface FrameStats {
|
|
109
|
+
/** Rendered-frame cadence (Hz), clamped to `maxFPS`. `0` before the first pair of rendered frames. */
|
|
110
|
+
fps: number;
|
|
111
|
+
/** Wall-clock ms of the last `render()` pass (excludes a11y/content sync). */
|
|
112
|
+
frameTimeMs: number;
|
|
113
|
+
/** Smoothed interval between rendered frames, in ms (EMA). */
|
|
114
|
+
frameIntervalMs: number;
|
|
115
|
+
/** dt (ms) handed to the last rendered frame. */
|
|
116
|
+
dt: number;
|
|
117
|
+
/** Total frames rendered since `start()`. */
|
|
118
|
+
renderedFrames: number;
|
|
119
|
+
/** Total rAF ticks skipped (idle/onDemand/capped) since `start()`. */
|
|
120
|
+
skippedFrames: number;
|
|
121
|
+
/** The scene's current render mode. */
|
|
122
|
+
renderMode: 'always' | 'onDemand';
|
|
123
|
+
/** Whether a redraw is currently pending (the boolean dirty flag). */
|
|
124
|
+
dirty: boolean;
|
|
125
|
+
}
|
|
104
126
|
export interface A11yTreeNode {
|
|
105
127
|
id: string;
|
|
106
128
|
tag: string;
|
|
@@ -147,6 +169,18 @@ export declare class Scene {
|
|
|
147
169
|
private dirty;
|
|
148
170
|
/** Whether to throttle rendering to 2 FPS when the scene is static to save power. */
|
|
149
171
|
autoThrottle: boolean;
|
|
172
|
+
/** Wall-clock ms spent inside the last `render()` call. */
|
|
173
|
+
private _lastFrameMs;
|
|
174
|
+
/** Rolling exponential average of rendered-frame intervals, in ms. */
|
|
175
|
+
private _avgFrameIntervalMs;
|
|
176
|
+
/** dt (ms) handed to the last rendered frame. */
|
|
177
|
+
private _lastDt;
|
|
178
|
+
/** Count of frames actually rendered since the loop started. */
|
|
179
|
+
private _renderedFrames;
|
|
180
|
+
/** Count of rAF ticks skipped (idle / capped) since the loop started. */
|
|
181
|
+
private _skippedFrames;
|
|
182
|
+
/** `time` of the previous *rendered* frame, for interval measurement. */
|
|
183
|
+
private _lastRenderTick;
|
|
150
184
|
/**
|
|
151
185
|
* Frame-rate cap (power saving). `0` = uncapped (native refresh). When set,
|
|
152
186
|
* the loop renders at most `maxFPS` times per second; animations still run,
|
|
@@ -339,6 +373,24 @@ export declare class Scene {
|
|
|
339
373
|
* entity state outside of {@link Entity.animate} so the change is rendered.
|
|
340
374
|
*/
|
|
341
375
|
markDirty(): void;
|
|
376
|
+
/**
|
|
377
|
+
* Live frame telemetry for profilers and devtools overlays. All timings are
|
|
378
|
+
* measured on the `requestAnimationFrame` loop; a scene driven only by
|
|
379
|
+
* {@link step} (e.g. deterministic video export) leaves these at their zero
|
|
380
|
+
* defaults.
|
|
381
|
+
*
|
|
382
|
+
* `fps` is derived from the interval between *rendered* frames, so idle
|
|
383
|
+
* `onDemand` scenes and frames skipped by the {@link maxFPS} cap or the
|
|
384
|
+
* static auto-throttle do not deflate it — it reports the cadence of actual
|
|
385
|
+
* redraws, not the raw rAF rate. `frameTimeMs` is the wall-clock cost of the
|
|
386
|
+
* last `render()` pass alone (excludes a11y/content-projection sync).
|
|
387
|
+
*
|
|
388
|
+
* The renderer always repaints the full canvas, so there is no partial
|
|
389
|
+
* dirty-rectangle to expose; `dirty` is the boolean redraw-pending flag and
|
|
390
|
+
* `pendingRedraw` reflects whether the next `onDemand` tick will actually
|
|
391
|
+
* render.
|
|
392
|
+
*/
|
|
393
|
+
get frameStats(): FrameStats;
|
|
342
394
|
/** True when any node in the subtree has a pending animation. */
|
|
343
395
|
/** True when any node in the subtree is interactive (drives a11y sync). */
|
|
344
396
|
private syncOptionalAttribute;
|