@vectojs/core 1.20.0 → 1.21.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-YLH7F4ZV.js → chunk-AGP4VLF4.js} +32 -0
- package/dist/{chunk-TJCXB2F6.mjs → chunk-FRMLD4PP.mjs} +32 -0
- package/dist/{chunk-L4SWVP2H.js → chunk-GKSCJ6AF.js} +201 -9
- package/dist/{chunk-QS3CUV7H.mjs → chunk-RTENOAYT.mjs} +192 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +416 -204
- package/dist/index.mjs +232 -20
- package/dist/renderer/CanvasRenderer.d.ts +2 -0
- package/dist/renderer/GlyphRasterAtlas.d.ts +186 -0
- package/dist/renderer/IRenderer.d.ts +31 -0
- package/dist/renderer/index.d.ts +1 -0
- package/dist/renderer.js +4 -2
- package/dist/renderer.mjs +3 -1
- package/dist/text.js +2 -2
- package/dist/text.mjs +1 -1
- package/dist/tree/Entity.d.ts +126 -0
- package/dist/tree/Scene.d.ts +114 -1
- package/package.json +1 -1
|
@@ -964,6 +964,38 @@ var Entity = (_class2 = class {
|
|
|
964
964
|
*
|
|
965
965
|
* @returns The {@link A11yAttributes} for this entity's shadow node.
|
|
966
966
|
*/
|
|
967
|
+
/**
|
|
968
|
+
* Describe this entity's own debug surface for DevTools.
|
|
969
|
+
*
|
|
970
|
+
* Returns `null` by default, meaning "nothing beyond the generic `Entity`
|
|
971
|
+
* fields the inspector already shows". Override in a component to expose the
|
|
972
|
+
* state that makes it inspectable — see {@link DevtoolsDescriptor}.
|
|
973
|
+
*
|
|
974
|
+
* Called only while a panel is inspecting this entity, never per frame, so it
|
|
975
|
+
* may compute values that would be too expensive to track continuously.
|
|
976
|
+
*
|
|
977
|
+
* @returns A descriptor, or `null` to opt out.
|
|
978
|
+
*/
|
|
979
|
+
getDevtoolsDescriptor() {
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Which of a child's properties this entity computes during layout.
|
|
984
|
+
*
|
|
985
|
+
* Returns an empty array by default, meaning "this entity does not position its
|
|
986
|
+
* children". A container that lays out children — `Stack`, `Table`, `Tabs` —
|
|
987
|
+
* overrides it so tooling can mark those values as parent-owned: editing `x` on
|
|
988
|
+
* a `Stack` child is reverted by the next layout, and knowing that in advance is
|
|
989
|
+
* the difference between a confusing tool and a correct one.
|
|
990
|
+
*
|
|
991
|
+
* @param child - The child being asked about. Containers whose control depends
|
|
992
|
+
* on the child (a `Table` cell versus its header) can answer per child.
|
|
993
|
+
* @returns Property names this entity overwrites on that child.
|
|
994
|
+
*/
|
|
995
|
+
getLayoutControlledProperties(child) {
|
|
996
|
+
void child;
|
|
997
|
+
return [];
|
|
998
|
+
}
|
|
967
999
|
getA11yAttributes() {
|
|
968
1000
|
return {};
|
|
969
1001
|
}
|
|
@@ -962,6 +962,38 @@ var Entity = class {
|
|
|
962
962
|
*
|
|
963
963
|
* @returns The {@link A11yAttributes} for this entity's shadow node.
|
|
964
964
|
*/
|
|
965
|
+
/**
|
|
966
|
+
* Describe this entity's own debug surface for DevTools.
|
|
967
|
+
*
|
|
968
|
+
* Returns `null` by default, meaning "nothing beyond the generic `Entity`
|
|
969
|
+
* fields the inspector already shows". Override in a component to expose the
|
|
970
|
+
* state that makes it inspectable — see {@link DevtoolsDescriptor}.
|
|
971
|
+
*
|
|
972
|
+
* Called only while a panel is inspecting this entity, never per frame, so it
|
|
973
|
+
* may compute values that would be too expensive to track continuously.
|
|
974
|
+
*
|
|
975
|
+
* @returns A descriptor, or `null` to opt out.
|
|
976
|
+
*/
|
|
977
|
+
getDevtoolsDescriptor() {
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Which of a child's properties this entity computes during layout.
|
|
982
|
+
*
|
|
983
|
+
* Returns an empty array by default, meaning "this entity does not position its
|
|
984
|
+
* children". A container that lays out children — `Stack`, `Table`, `Tabs` —
|
|
985
|
+
* overrides it so tooling can mark those values as parent-owned: editing `x` on
|
|
986
|
+
* a `Stack` child is reverted by the next layout, and knowing that in advance is
|
|
987
|
+
* the difference between a confusing tool and a correct one.
|
|
988
|
+
*
|
|
989
|
+
* @param child - The child being asked about. Containers whose control depends
|
|
990
|
+
* on the child (a `Table` cell versus its header) can answer per child.
|
|
991
|
+
* @returns Property names this entity overwrites on that child.
|
|
992
|
+
*/
|
|
993
|
+
getLayoutControlledProperties(child) {
|
|
994
|
+
void child;
|
|
995
|
+
return [];
|
|
996
|
+
}
|
|
965
997
|
getA11yAttributes() {
|
|
966
998
|
return {};
|
|
967
999
|
}
|
|
@@ -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; var _class4;const __vecto_cjs_url=require("url").pathToFileURL(__filename).href;
|
|
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; var _class5;const __vecto_cjs_url=require("url").pathToFileURL(__filename).href;
|
|
2
2
|
|
|
3
3
|
// src/renderer/CanvasRenderer.ts
|
|
4
4
|
var TWO_PI = Math.PI * 2;
|
|
@@ -212,6 +212,11 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
|
|
|
212
212
|
this.ctx.drawImage(source, dx, dy, dw, dh);
|
|
213
213
|
}
|
|
214
214
|
/** @inheritdoc */
|
|
215
|
+
drawImageRect(source, sx, sy, sw, sh, dx, dy, dw, dh) {
|
|
216
|
+
this.flush();
|
|
217
|
+
this.ctx.drawImage(source, sx, sy, sw, sh, dx, dy, dw, dh);
|
|
218
|
+
}
|
|
219
|
+
/** @inheritdoc */
|
|
215
220
|
fillCircle(cx, cy, radius, color, alpha = 1) {
|
|
216
221
|
if (this.batchActive && (color !== this.batchColor || alpha !== this.batchAlpha)) {
|
|
217
222
|
this.flush();
|
|
@@ -1809,15 +1814,201 @@ var WebGPUParticleSystemManager = (_class3 = class {
|
|
|
1809
1814
|
}
|
|
1810
1815
|
}, _class3);
|
|
1811
1816
|
|
|
1817
|
+
// src/renderer/GlyphRasterAtlas.ts
|
|
1818
|
+
var HARD_MAX_SIZE = 8192;
|
|
1819
|
+
var PAD = 2;
|
|
1820
|
+
var GlyphRasterAtlas = (_class4 = class {
|
|
1821
|
+
__init35() {this.slots = /* @__PURE__ */ new Map()}
|
|
1822
|
+
|
|
1823
|
+
|
|
1824
|
+
__init36() {this.canvas = null}
|
|
1825
|
+
__init37() {this.ctx = null}
|
|
1826
|
+
/**
|
|
1827
|
+
* Shelf packing: glyphs land left-to-right on a row, then a new row starts.
|
|
1828
|
+
* A monospace grid produces near-uniform widths, so shelves waste very little
|
|
1829
|
+
* and cost one comparison per insert — a real 2D packer would buy nothing here.
|
|
1830
|
+
*/
|
|
1831
|
+
__init38() {this.penX = PAD}
|
|
1832
|
+
__init39() {this.penY = PAD}
|
|
1833
|
+
__init40() {this.rowHeight = 0}
|
|
1834
|
+
__init41() {this._hits = 0}
|
|
1835
|
+
__init42() {this._misses = 0}
|
|
1836
|
+
__init43() {this._resets = 0}
|
|
1837
|
+
constructor(options = {}) {;_class4.prototype.__init35.call(this);_class4.prototype.__init36.call(this);_class4.prototype.__init37.call(this);_class4.prototype.__init38.call(this);_class4.prototype.__init39.call(this);_class4.prototype.__init40.call(this);_class4.prototype.__init41.call(this);_class4.prototype.__init42.call(this);_class4.prototype.__init43.call(this);
|
|
1838
|
+
this.dpr = Math.max(1, _nullishCoalesce(options.dpr, () => ( 1)));
|
|
1839
|
+
this.maxSize = Math.min(HARD_MAX_SIZE, Math.max(256, _nullishCoalesce(options.maxSize, () => ( 2048))));
|
|
1840
|
+
}
|
|
1841
|
+
/** Live instrumentation snapshot. */
|
|
1842
|
+
get stats() {
|
|
1843
|
+
return {
|
|
1844
|
+
hits: this._hits,
|
|
1845
|
+
misses: this._misses,
|
|
1846
|
+
size: this.slots.size,
|
|
1847
|
+
resets: this._resets
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* The atlas canvas, to pass as the blit source.
|
|
1852
|
+
*
|
|
1853
|
+
* `null` until the first successful {@link get}, and in any non-DOM context.
|
|
1854
|
+
*/
|
|
1855
|
+
get source() {
|
|
1856
|
+
return this.canvas;
|
|
1857
|
+
}
|
|
1858
|
+
ensureCanvas() {
|
|
1859
|
+
if (this.ctx) return true;
|
|
1860
|
+
if (typeof document === "undefined") return false;
|
|
1861
|
+
const canvas = document.createElement("canvas");
|
|
1862
|
+
canvas.width = this.maxSize;
|
|
1863
|
+
canvas.height = this.maxSize;
|
|
1864
|
+
const ctx = canvas.getContext("2d");
|
|
1865
|
+
if (!ctx) return false;
|
|
1866
|
+
this.canvas = canvas;
|
|
1867
|
+
this.ctx = ctx;
|
|
1868
|
+
return true;
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Look up a glyph, rasterizing it into the atlas on first request.
|
|
1872
|
+
*
|
|
1873
|
+
* @param font - Full CSS `font` shorthand, used for measuring and painting.
|
|
1874
|
+
* @param color - CSS color baked into the pixels.
|
|
1875
|
+
* @param glyph - A single grapheme cluster. Long strings are rejected
|
|
1876
|
+
* (`null`): they defeat the atlas's fixed-slot packing and belong in
|
|
1877
|
+
* `fillText` or {@link TextRasterCache}.
|
|
1878
|
+
* @returns The slot, or `null` when the caller must fall back to `fillText`
|
|
1879
|
+
* (headless, unrasterizable, or too large to pack).
|
|
1880
|
+
*/
|
|
1881
|
+
get(font, color, glyph) {
|
|
1882
|
+
const key = font + "\0" + color + "\0" + glyph;
|
|
1883
|
+
const existing = this.slots.get(key);
|
|
1884
|
+
if (existing !== void 0) {
|
|
1885
|
+
if (existing) this._hits++;
|
|
1886
|
+
return existing;
|
|
1887
|
+
}
|
|
1888
|
+
this._misses++;
|
|
1889
|
+
if (glyph.length === 0 || glyph.length > 8) {
|
|
1890
|
+
this.slots.set(key, null);
|
|
1891
|
+
return null;
|
|
1892
|
+
}
|
|
1893
|
+
if (!this.ensureCanvas()) return null;
|
|
1894
|
+
const ctx = this.ctx;
|
|
1895
|
+
ctx.font = font;
|
|
1896
|
+
const metrics = ctx.measureText(glyph);
|
|
1897
|
+
const advance = metrics.width;
|
|
1898
|
+
const ascent = metrics.actualBoundingBoxAscent || metrics.fontBoundingBoxAscent || advance * 0.9;
|
|
1899
|
+
const descent = metrics.actualBoundingBoxDescent || metrics.fontBoundingBoxDescent || advance * 0.25;
|
|
1900
|
+
const left = metrics.actualBoundingBoxLeft || 0;
|
|
1901
|
+
const right = metrics.actualBoundingBoxRight || advance;
|
|
1902
|
+
const offsetX = Math.ceil(left) + PAD;
|
|
1903
|
+
const offsetY = Math.ceil(ascent) + PAD;
|
|
1904
|
+
const w = offsetX + Math.ceil(right) + PAD;
|
|
1905
|
+
const h = offsetY + Math.ceil(descent) + PAD;
|
|
1906
|
+
if (!(w > 0) || !(h > 0)) {
|
|
1907
|
+
this.slots.set(key, null);
|
|
1908
|
+
return null;
|
|
1909
|
+
}
|
|
1910
|
+
const dpr = this.dpr;
|
|
1911
|
+
const dw = Math.ceil(w * dpr);
|
|
1912
|
+
const dh = Math.ceil(h * dpr);
|
|
1913
|
+
if (dw + PAD * 2 > this.maxSize || dh + PAD * 2 > this.maxSize) {
|
|
1914
|
+
this.slots.set(key, null);
|
|
1915
|
+
return null;
|
|
1916
|
+
}
|
|
1917
|
+
if (this.penX + dw + PAD > this.maxSize) {
|
|
1918
|
+
this.penX = PAD;
|
|
1919
|
+
this.penY += this.rowHeight + PAD;
|
|
1920
|
+
this.rowHeight = 0;
|
|
1921
|
+
}
|
|
1922
|
+
if (this.penY + dh + PAD > this.maxSize) {
|
|
1923
|
+
this.reset();
|
|
1924
|
+
if (!this.ensureCanvas()) return null;
|
|
1925
|
+
this.ctx.font = font;
|
|
1926
|
+
}
|
|
1927
|
+
const sx = this.penX;
|
|
1928
|
+
const sy = this.penY;
|
|
1929
|
+
const c = this.ctx;
|
|
1930
|
+
c.save();
|
|
1931
|
+
c.translate(sx, sy);
|
|
1932
|
+
if (dpr !== 1) c.scale(dpr, dpr);
|
|
1933
|
+
c.font = font;
|
|
1934
|
+
c.textBaseline = "alphabetic";
|
|
1935
|
+
c.fillStyle = color;
|
|
1936
|
+
c.fillText(glyph, offsetX, offsetY);
|
|
1937
|
+
c.restore();
|
|
1938
|
+
this.penX += dw + PAD;
|
|
1939
|
+
if (dh > this.rowHeight) this.rowHeight = dh;
|
|
1940
|
+
const slot = {
|
|
1941
|
+
sx,
|
|
1942
|
+
sy,
|
|
1943
|
+
sw: dw,
|
|
1944
|
+
sh: dh,
|
|
1945
|
+
w,
|
|
1946
|
+
h,
|
|
1947
|
+
offsetX,
|
|
1948
|
+
offsetY,
|
|
1949
|
+
glyph,
|
|
1950
|
+
font,
|
|
1951
|
+
advance,
|
|
1952
|
+
left,
|
|
1953
|
+
right
|
|
1954
|
+
};
|
|
1955
|
+
this.slots.set(key, slot);
|
|
1956
|
+
return slot;
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* Find the slot occupying a source position, or `null`.
|
|
1960
|
+
*
|
|
1961
|
+
* The inverse of {@link get}: it maps a blit back to the glyph it drew. Exists
|
|
1962
|
+
* for instrumentation — a test or devtool that traces `drawImage` calls sees
|
|
1963
|
+
* only a source rect, and needs this to recover which cluster was painted and
|
|
1964
|
+
* with what metrics. Linear over resident slots, so it is a diagnostic, not a
|
|
1965
|
+
* per-frame call.
|
|
1966
|
+
*/
|
|
1967
|
+
slotAt(sx, sy) {
|
|
1968
|
+
for (const slot of this.slots.values()) {
|
|
1969
|
+
if (slot && slot.sx === sx && slot.sy === sy) return slot;
|
|
1970
|
+
}
|
|
1971
|
+
return null;
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Drop every glyph and reuse the canvas.
|
|
1975
|
+
*
|
|
1976
|
+
* Call after a font or theme change: slots are keyed by `(font, color, glyph)`
|
|
1977
|
+
* so stale entries are never *returned* wrongly, but they do occupy space.
|
|
1978
|
+
*/
|
|
1979
|
+
reset() {
|
|
1980
|
+
this.slots.clear();
|
|
1981
|
+
this.penX = PAD;
|
|
1982
|
+
this.penY = PAD;
|
|
1983
|
+
this.rowHeight = 0;
|
|
1984
|
+
this._resets++;
|
|
1985
|
+
if (this.ctx && this.canvas) {
|
|
1986
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
/** Release the backing canvas and all slots. */
|
|
1990
|
+
destroy() {
|
|
1991
|
+
this.slots.clear();
|
|
1992
|
+
this.canvas = null;
|
|
1993
|
+
this.ctx = null;
|
|
1994
|
+
this.penX = PAD;
|
|
1995
|
+
this.penY = PAD;
|
|
1996
|
+
this.rowHeight = 0;
|
|
1997
|
+
this._hits = 0;
|
|
1998
|
+
this._misses = 0;
|
|
1999
|
+
this._resets = 0;
|
|
2000
|
+
}
|
|
2001
|
+
}, _class4);
|
|
2002
|
+
|
|
1812
2003
|
// src/renderer/TextRasterCache.ts
|
|
1813
|
-
var TextRasterCache = (
|
|
1814
|
-
|
|
2004
|
+
var TextRasterCache = (_class5 = class {
|
|
2005
|
+
__init44() {this.cache = /* @__PURE__ */ new Map()}
|
|
1815
2006
|
|
|
1816
2007
|
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
constructor(options = {}) {;
|
|
2008
|
+
__init45() {this.scratchCtx = null}
|
|
2009
|
+
__init46() {this._hits = 0}
|
|
2010
|
+
__init47() {this._misses = 0}
|
|
2011
|
+
constructor(options = {}) {;_class5.prototype.__init44.call(this);_class5.prototype.__init45.call(this);_class5.prototype.__init46.call(this);_class5.prototype.__init47.call(this);
|
|
1821
2012
|
this.maxEntries = Math.max(1, _nullishCoalesce(options.maxEntries, () => ( 4096)));
|
|
1822
2013
|
this.dpr = Math.max(1, _nullishCoalesce(options.dpr, () => ( 1)));
|
|
1823
2014
|
}
|
|
@@ -1892,7 +2083,8 @@ var TextRasterCache = (_class4 = class {
|
|
|
1892
2083
|
this._hits = 0;
|
|
1893
2084
|
this._misses = 0;
|
|
1894
2085
|
}
|
|
1895
|
-
},
|
|
2086
|
+
}, _class5);
|
|
2087
|
+
|
|
1896
2088
|
|
|
1897
2089
|
|
|
1898
2090
|
|
|
@@ -1903,4 +2095,4 @@ var TextRasterCache = (_class4 = class {
|
|
|
1903
2095
|
|
|
1904
2096
|
|
|
1905
2097
|
|
|
1906
|
-
exports.CanvasRenderer = CanvasRenderer; exports.sanitizeUrl = sanitizeUrl; exports.isSafeUrl = isSafeUrl; exports.SVGRenderer = SVGRenderer; exports.parseColorToRGBA = parseColorToRGBA; exports.createWebGLPointRenderer = createWebGLPointRenderer; exports.WebGPUParticleSystemManager = WebGPUParticleSystemManager; exports.TextRasterCache = TextRasterCache;
|
|
2098
|
+
exports.CanvasRenderer = CanvasRenderer; exports.sanitizeUrl = sanitizeUrl; exports.isSafeUrl = isSafeUrl; exports.SVGRenderer = SVGRenderer; exports.parseColorToRGBA = parseColorToRGBA; exports.createWebGLPointRenderer = createWebGLPointRenderer; exports.WebGPUParticleSystemManager = WebGPUParticleSystemManager; exports.GlyphRasterAtlas = GlyphRasterAtlas; exports.TextRasterCache = TextRasterCache;
|
|
@@ -210,6 +210,11 @@ var CanvasRenderer = class _CanvasRenderer {
|
|
|
210
210
|
this.ctx.drawImage(source, dx, dy, dw, dh);
|
|
211
211
|
}
|
|
212
212
|
/** @inheritdoc */
|
|
213
|
+
drawImageRect(source, sx, sy, sw, sh, dx, dy, dw, dh) {
|
|
214
|
+
this.flush();
|
|
215
|
+
this.ctx.drawImage(source, sx, sy, sw, sh, dx, dy, dw, dh);
|
|
216
|
+
}
|
|
217
|
+
/** @inheritdoc */
|
|
213
218
|
fillCircle(cx, cy, radius, color, alpha = 1) {
|
|
214
219
|
if (this.batchActive && (color !== this.batchColor || alpha !== this.batchAlpha)) {
|
|
215
220
|
this.flush();
|
|
@@ -1807,6 +1812,192 @@ var WebGPUParticleSystemManager = class {
|
|
|
1807
1812
|
}
|
|
1808
1813
|
};
|
|
1809
1814
|
|
|
1815
|
+
// src/renderer/GlyphRasterAtlas.ts
|
|
1816
|
+
var HARD_MAX_SIZE = 8192;
|
|
1817
|
+
var PAD = 2;
|
|
1818
|
+
var GlyphRasterAtlas = class {
|
|
1819
|
+
slots = /* @__PURE__ */ new Map();
|
|
1820
|
+
dpr;
|
|
1821
|
+
maxSize;
|
|
1822
|
+
canvas = null;
|
|
1823
|
+
ctx = null;
|
|
1824
|
+
/**
|
|
1825
|
+
* Shelf packing: glyphs land left-to-right on a row, then a new row starts.
|
|
1826
|
+
* A monospace grid produces near-uniform widths, so shelves waste very little
|
|
1827
|
+
* and cost one comparison per insert — a real 2D packer would buy nothing here.
|
|
1828
|
+
*/
|
|
1829
|
+
penX = PAD;
|
|
1830
|
+
penY = PAD;
|
|
1831
|
+
rowHeight = 0;
|
|
1832
|
+
_hits = 0;
|
|
1833
|
+
_misses = 0;
|
|
1834
|
+
_resets = 0;
|
|
1835
|
+
constructor(options = {}) {
|
|
1836
|
+
this.dpr = Math.max(1, options.dpr ?? 1);
|
|
1837
|
+
this.maxSize = Math.min(HARD_MAX_SIZE, Math.max(256, options.maxSize ?? 2048));
|
|
1838
|
+
}
|
|
1839
|
+
/** Live instrumentation snapshot. */
|
|
1840
|
+
get stats() {
|
|
1841
|
+
return {
|
|
1842
|
+
hits: this._hits,
|
|
1843
|
+
misses: this._misses,
|
|
1844
|
+
size: this.slots.size,
|
|
1845
|
+
resets: this._resets
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* The atlas canvas, to pass as the blit source.
|
|
1850
|
+
*
|
|
1851
|
+
* `null` until the first successful {@link get}, and in any non-DOM context.
|
|
1852
|
+
*/
|
|
1853
|
+
get source() {
|
|
1854
|
+
return this.canvas;
|
|
1855
|
+
}
|
|
1856
|
+
ensureCanvas() {
|
|
1857
|
+
if (this.ctx) return true;
|
|
1858
|
+
if (typeof document === "undefined") return false;
|
|
1859
|
+
const canvas = document.createElement("canvas");
|
|
1860
|
+
canvas.width = this.maxSize;
|
|
1861
|
+
canvas.height = this.maxSize;
|
|
1862
|
+
const ctx = canvas.getContext("2d");
|
|
1863
|
+
if (!ctx) return false;
|
|
1864
|
+
this.canvas = canvas;
|
|
1865
|
+
this.ctx = ctx;
|
|
1866
|
+
return true;
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* Look up a glyph, rasterizing it into the atlas on first request.
|
|
1870
|
+
*
|
|
1871
|
+
* @param font - Full CSS `font` shorthand, used for measuring and painting.
|
|
1872
|
+
* @param color - CSS color baked into the pixels.
|
|
1873
|
+
* @param glyph - A single grapheme cluster. Long strings are rejected
|
|
1874
|
+
* (`null`): they defeat the atlas's fixed-slot packing and belong in
|
|
1875
|
+
* `fillText` or {@link TextRasterCache}.
|
|
1876
|
+
* @returns The slot, or `null` when the caller must fall back to `fillText`
|
|
1877
|
+
* (headless, unrasterizable, or too large to pack).
|
|
1878
|
+
*/
|
|
1879
|
+
get(font, color, glyph) {
|
|
1880
|
+
const key = font + "\0" + color + "\0" + glyph;
|
|
1881
|
+
const existing = this.slots.get(key);
|
|
1882
|
+
if (existing !== void 0) {
|
|
1883
|
+
if (existing) this._hits++;
|
|
1884
|
+
return existing;
|
|
1885
|
+
}
|
|
1886
|
+
this._misses++;
|
|
1887
|
+
if (glyph.length === 0 || glyph.length > 8) {
|
|
1888
|
+
this.slots.set(key, null);
|
|
1889
|
+
return null;
|
|
1890
|
+
}
|
|
1891
|
+
if (!this.ensureCanvas()) return null;
|
|
1892
|
+
const ctx = this.ctx;
|
|
1893
|
+
ctx.font = font;
|
|
1894
|
+
const metrics = ctx.measureText(glyph);
|
|
1895
|
+
const advance = metrics.width;
|
|
1896
|
+
const ascent = metrics.actualBoundingBoxAscent || metrics.fontBoundingBoxAscent || advance * 0.9;
|
|
1897
|
+
const descent = metrics.actualBoundingBoxDescent || metrics.fontBoundingBoxDescent || advance * 0.25;
|
|
1898
|
+
const left = metrics.actualBoundingBoxLeft || 0;
|
|
1899
|
+
const right = metrics.actualBoundingBoxRight || advance;
|
|
1900
|
+
const offsetX = Math.ceil(left) + PAD;
|
|
1901
|
+
const offsetY = Math.ceil(ascent) + PAD;
|
|
1902
|
+
const w = offsetX + Math.ceil(right) + PAD;
|
|
1903
|
+
const h = offsetY + Math.ceil(descent) + PAD;
|
|
1904
|
+
if (!(w > 0) || !(h > 0)) {
|
|
1905
|
+
this.slots.set(key, null);
|
|
1906
|
+
return null;
|
|
1907
|
+
}
|
|
1908
|
+
const dpr = this.dpr;
|
|
1909
|
+
const dw = Math.ceil(w * dpr);
|
|
1910
|
+
const dh = Math.ceil(h * dpr);
|
|
1911
|
+
if (dw + PAD * 2 > this.maxSize || dh + PAD * 2 > this.maxSize) {
|
|
1912
|
+
this.slots.set(key, null);
|
|
1913
|
+
return null;
|
|
1914
|
+
}
|
|
1915
|
+
if (this.penX + dw + PAD > this.maxSize) {
|
|
1916
|
+
this.penX = PAD;
|
|
1917
|
+
this.penY += this.rowHeight + PAD;
|
|
1918
|
+
this.rowHeight = 0;
|
|
1919
|
+
}
|
|
1920
|
+
if (this.penY + dh + PAD > this.maxSize) {
|
|
1921
|
+
this.reset();
|
|
1922
|
+
if (!this.ensureCanvas()) return null;
|
|
1923
|
+
this.ctx.font = font;
|
|
1924
|
+
}
|
|
1925
|
+
const sx = this.penX;
|
|
1926
|
+
const sy = this.penY;
|
|
1927
|
+
const c = this.ctx;
|
|
1928
|
+
c.save();
|
|
1929
|
+
c.translate(sx, sy);
|
|
1930
|
+
if (dpr !== 1) c.scale(dpr, dpr);
|
|
1931
|
+
c.font = font;
|
|
1932
|
+
c.textBaseline = "alphabetic";
|
|
1933
|
+
c.fillStyle = color;
|
|
1934
|
+
c.fillText(glyph, offsetX, offsetY);
|
|
1935
|
+
c.restore();
|
|
1936
|
+
this.penX += dw + PAD;
|
|
1937
|
+
if (dh > this.rowHeight) this.rowHeight = dh;
|
|
1938
|
+
const slot = {
|
|
1939
|
+
sx,
|
|
1940
|
+
sy,
|
|
1941
|
+
sw: dw,
|
|
1942
|
+
sh: dh,
|
|
1943
|
+
w,
|
|
1944
|
+
h,
|
|
1945
|
+
offsetX,
|
|
1946
|
+
offsetY,
|
|
1947
|
+
glyph,
|
|
1948
|
+
font,
|
|
1949
|
+
advance,
|
|
1950
|
+
left,
|
|
1951
|
+
right
|
|
1952
|
+
};
|
|
1953
|
+
this.slots.set(key, slot);
|
|
1954
|
+
return slot;
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* Find the slot occupying a source position, or `null`.
|
|
1958
|
+
*
|
|
1959
|
+
* The inverse of {@link get}: it maps a blit back to the glyph it drew. Exists
|
|
1960
|
+
* for instrumentation — a test or devtool that traces `drawImage` calls sees
|
|
1961
|
+
* only a source rect, and needs this to recover which cluster was painted and
|
|
1962
|
+
* with what metrics. Linear over resident slots, so it is a diagnostic, not a
|
|
1963
|
+
* per-frame call.
|
|
1964
|
+
*/
|
|
1965
|
+
slotAt(sx, sy) {
|
|
1966
|
+
for (const slot of this.slots.values()) {
|
|
1967
|
+
if (slot && slot.sx === sx && slot.sy === sy) return slot;
|
|
1968
|
+
}
|
|
1969
|
+
return null;
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Drop every glyph and reuse the canvas.
|
|
1973
|
+
*
|
|
1974
|
+
* Call after a font or theme change: slots are keyed by `(font, color, glyph)`
|
|
1975
|
+
* so stale entries are never *returned* wrongly, but they do occupy space.
|
|
1976
|
+
*/
|
|
1977
|
+
reset() {
|
|
1978
|
+
this.slots.clear();
|
|
1979
|
+
this.penX = PAD;
|
|
1980
|
+
this.penY = PAD;
|
|
1981
|
+
this.rowHeight = 0;
|
|
1982
|
+
this._resets++;
|
|
1983
|
+
if (this.ctx && this.canvas) {
|
|
1984
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
/** Release the backing canvas and all slots. */
|
|
1988
|
+
destroy() {
|
|
1989
|
+
this.slots.clear();
|
|
1990
|
+
this.canvas = null;
|
|
1991
|
+
this.ctx = null;
|
|
1992
|
+
this.penX = PAD;
|
|
1993
|
+
this.penY = PAD;
|
|
1994
|
+
this.rowHeight = 0;
|
|
1995
|
+
this._hits = 0;
|
|
1996
|
+
this._misses = 0;
|
|
1997
|
+
this._resets = 0;
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
|
|
1810
2001
|
// src/renderer/TextRasterCache.ts
|
|
1811
2002
|
var TextRasterCache = class {
|
|
1812
2003
|
cache = /* @__PURE__ */ new Map();
|
|
@@ -1900,5 +2091,6 @@ export {
|
|
|
1900
2091
|
parseColorToRGBA,
|
|
1901
2092
|
createWebGLPointRenderer,
|
|
1902
2093
|
WebGPUParticleSystemManager,
|
|
2094
|
+
GlyphRasterAtlas,
|
|
1903
2095
|
TextRasterCache
|
|
1904
2096
|
};
|
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/GlyphRasterAtlas';
|
|
8
9
|
export * from './renderer/TextRasterCache';
|
|
9
10
|
export * from './tree/Entity';
|
|
10
11
|
export * from './tree/Scene';
|