UNPKG

babylon-msdf-text

Version:

**babylon-msdf-text** is a library for rendering high-quality, scalable, and anti-aliased text in Babylon.js using the Multi-channel Signed Distance Field (MSDF) technique. It offers an efficient and straightforward solution for superior text rendering in

626 lines (608 loc) 18.2 kB
var Q = Object.defineProperty; var Z = (e, t, n) => t in e ? Q(e, t, { enumerable: !0, configurable: !0, writable: !0, value: n }) : e[t] = n; var T = (e, t, n) => (Z(e, typeof t != "symbol" ? t + "" : t, n), n); import * as _ from "@babylonjs/core"; const J = /\n/, O = ` `, j = /\s/; function tt(e = "", t = {}) { const n = typeof t.width == "number" ? t.width : Number.MAX_VALUE, r = Math.max(0, t.start || 0), c = typeof t.end == "number" ? t.end : e.length, s = t.mode, f = t.measure || it; return t.width === 0 && t.mode !== "nowrap" ? [] : s === "pre" ? nt(f, e, r, c, n) : rt(f, e, r, c, n, s); } function et(e, t, n, r) { const c = e.indexOf(t, n); return c === -1 || c > r ? r : c; } function P(e) { return j.test(e); } function nt(e, t, n, r, c) { const s = []; let f = n; for (let o = n; o < r && o < t.length; o++) { const u = t.charAt(o), h = J.test(u); if (h || o === r - 1) { const i = h ? o : o + 1, d = e(t, f, i, c); s.push(d), f = o + 1; } } return s; } function rt(e, t, n, r, c, s) { const f = []; let o = c; for (s === "nowrap" && (o = Number.MAX_VALUE); n < r && n < t.length; ) { const u = et(t, O, n, r); for (; n < u && P(t.charAt(n)); ) n++; const h = e(t, n, u, o); let i = n + (h.end - h.start), d = i + O.length; if (i < u) { for (; i > n && !P(t.charAt(i)); ) i--; if (i === n) d > n + O.length && d--, i = d; else for (d = i; i > n && P(t.charAt(i - O.length)); ) i--; } if (i >= n) { const w = e(t, n, i, o); f.push(w); } n = d; } return f; } function it(e, t, n, r) { const c = Math.min(r, n - t); return { start: t, end: t + c, width: r }; } const ot = [ "x", "e", "a", "o", "n", "s", "r", "c", "u", "m", "v", "w", "z" ], st = ["m", "w"], lt = [ "H", "I", "N", "E", "F", "K", "L", "T", "U", "V", "W", "X", "Y", "Z" ], z = " ".charCodeAt(0), E = " ".charCodeAt(0), at = 0, Y = 1, $ = 2; class ct { constructor(t) { T(this, "glyphs", []); T(this, "_width", 0); T(this, "_height", 0); T(this, "_descender", 0); T(this, "_ascender", 0); T(this, "_xHeight", 0); T(this, "_baseline", 0); T(this, "_capHeight", 0); T(this, "_lineHeight", 0); T(this, "_linesTotal", 0); T(this, "_lettersTotal", 0); T(this, "_wordsTotal", 0); T(this, "_options"); T(this, "_fallbackSpaceGlyph", null); T(this, "_fallbackTabGlyph", null); this.update(t); } get width() { return this._width; } get height() { return this._height; } get descender() { return this._descender; } get ascender() { return this._ascender; } get xHeight() { return this._xHeight; } get baseline() { return this._baseline; } get capHeight() { return this._capHeight; } get lineHeight() { return this._lineHeight; } get linesTotal() { return this._linesTotal; } get lettersTotal() { return this._lettersTotal; } get wordsTotal() { return this._wordsTotal; } get glyphsArray() { return this.glyphs; } /** * Updates the text layout with new options * @param options - Text layout configuration */ update(t) { if (t = Object.assign( { measure: this.computeMetrics.bind(this) }, t ), this._options = t, this._options.tabSize = vt(t.tabSize, 4), !t.font) throw new Error("Must provide a valid bitmap font"); const n = this.glyphs, r = t.text || "", c = t.font; this._setupSpaceGlyphs(c); const s = tt(r, t), f = t.width || 0, o = r.split(" ").filter((l) => l !== ` `).length, u = r.split("").filter((l) => l !== ` ` && l !== " ").length; n.length = 0; const h = s.reduce( (l, A) => Math.max(l, A.width, f), 0 ); let i = 0, d = 0; const w = t.lineHeight ?? 1, a = c.common.lineHeight * Math.max(w, 1), p = c.common.base, m = a - p, g = t.letterSpacing || 0, v = a * s.length - m, I = gt(t.align); d -= v, this._width = h, this._height = v, this._descender = a - p, this._baseline = p, this._xHeight = ut(c), this._capHeight = ft(c), this._lineHeight = a, this._ascender = a - m - this._xHeight; let x = 0, b = 0; s.forEach((l, A) => { const L = l.start, y = l.end, W = l.width, D = r.slice(L, y).split(" ").filter((k) => k !== "").length, M = r.slice(L, y).split(" ").join("").length; let H = 0, G = 0, S; for (let k = L; k < y; k++) { const K = r.charCodeAt(k), C = this.getGlyph(c, K); if (C) { S && (i += R(c, S.id, C.id)); let N = i; I === Y ? N += (h - W) / 2 : I === $ && (N += h - W), n.push({ position: [N, d], data: C, index: k, linesTotal: s.length, lineIndex: A, lineLettersTotal: M, lineLetterIndex: H, lineWordsTotal: D, lineWordIndex: G, wordsTotal: o, wordIndex: x, lettersTotal: u, letterIndex: b }), C.id === E && (S == null ? void 0 : S.id) !== E && (G++, x++), C.id !== E && (H++, b++), i += C.xadvance + g, S = C; } } d += a, i = 0; }), this._lettersTotal = u, this._wordsTotal = o, this._linesTotal = s.length; } /** * Gets a glyph by its character ID * @param font - Font data * @param id - Character ID * @returns Glyph data or null */ getGlyph(t, n) { const r = X(t, n); return r || (n === z ? this._fallbackTabGlyph : n === E ? this._fallbackSpaceGlyph : null); } /** * Computes metrics for text layout */ computeMetrics(t, n, r, c) { const s = this._options.letterSpacing || 0, f = this._options.font; let o = 0, u = 0, h = 0, i; if (!f.chars || f.chars.length === 0) return { start: n, end: n, width: 0 }; r = Math.min(t.length, r); for (let d = n; d < r; d++) { const w = t.charCodeAt(d), a = this.getGlyph(f, w); if (a) { a.char = t[d]; const p = i ? R(f, i.id, a.id) : 0; o += p; const m = o + a.xadvance + s, g = o + a.width; if (g >= c || m >= c) break; o = m, u = g, i = a; } h++; } return i && (u += i.xoffset), { start: n, end: n + h, width: u }; } /** * Sets up fallback glyphs for space and tab */ _setupSpaceGlyphs(t) { if (this._fallbackSpaceGlyph = null, this._fallbackTabGlyph = null, !t.chars || t.chars.length === 0) return; const n = X(t, E) || dt(t) || t.chars[0], r = this._options.tabSize || 0 * n.xadvance; this._fallbackSpaceGlyph = n, this._fallbackTabGlyph = { ...n, x: 0, y: 0, xadvance: r, id: z, xoffset: 0, yoffset: 0, width: 0, height: 0 }; } } function ht(e) { return new ct(e); } function X(e, t) { if (!e.chars || e.chars.length === 0) return null; const n = V(e.chars, t); return n >= 0 ? e.chars[n] : null; } function ut(e) { for (const t of ot) { const n = t.charCodeAt(0), r = V(e.chars, n); if (r >= 0) return e.chars[r].height; } return 0; } function dt(e) { for (const t of st) { const n = t.charCodeAt(0), r = V(e.chars, n); if (r >= 0) return e.chars[r]; } return null; } function ft(e) { for (const t of lt) { const n = t.charCodeAt(0), r = V(e.chars, n); if (r >= 0) return e.chars[r].height; } return 0; } function R(e, t, n) { if (!e.kernings || e.kernings.length === 0) return 0; for (const r of e.kernings) if (r.first === t && r.second === n) return r.amount; return 0; } function gt(e) { return e === "center" ? Y : e === "right" ? $ : at; } function V(e, t, n = 0) { for (let r = n; r < e.length; r++) if (e[r].id === t) return r; return -1; } function vt(e, t) { return typeof e == "number" ? e : typeof t == "number" ? t : 0; } var B; ((e) => { function t(r, c, s, f, o) { const u = new Float32Array(r.length * 4 * 2), h = new Float32Array(r.length * 4 * 2), i = new Float32Array(r.length * 4 * 3), d = new Float32Array(r.length * 4 * 2); let w = 0, a = 0, p = 0, m = 0; return r.forEach((g) => { const v = g.data, I = v.x + v.width, x = v.y + v.height, b = v.x / c; let l = v.y / s; const A = I / c; let L = x / s; f && (l = (s - v.y) / s, L = (s - x) / s), u[w++] = b, u[w++] = l, u[w++] = b, u[w++] = L, u[w++] = A, u[w++] = L, u[w++] = A, u[w++] = l, h[m++] = g.position[0] / o.width, h[m++] = (g.position[1] + o.height) / o.height, h[m++] = g.position[0] / o.width, h[m++] = (g.position[1] + o.height + v.height) / o.height, h[m++] = (g.position[0] + v.width) / o.width, h[m++] = (g.position[1] + o.height + v.height) / o.height, h[m++] = (g.position[0] + v.width) / o.width, h[m++] = (g.position[1] + o.height) / o.height; const y = g.position[0] + v.xoffset, W = g.position[1] + v.yoffset, F = 0, D = v.width, M = v.height; i[a++] = y, i[a++] = -W, i[a++] = F, i[a++] = y, i[a++] = -(W + M), i[a++] = F, i[a++] = y + D, i[a++] = -(W + M), i[a++] = F, i[a++] = y + D, i[a++] = -W, i[a++] = F; const H = y + D / 2, G = W + M / 2; for (let S = 0; S < 4; S++) d[p++] = H, d[p++] = G; }), { uvs: u, layoutUvs: h, positions: i, centers: d }; } e.create = t; function n(r, c, s) { const f = [ [s.centers, "center", 2], [s.layoutUvs, "layoutUv", 2] ]; for (const [o, u, h] of f) { const i = new _.VertexBuffer( r, o, u, !0, !1, h ); c.setVerticesBuffer(i); } } e.setCustomAttributes = n; })(B || (B = {})); var U; ((e) => { function t(r) { const c = new Float32Array(r.length * 4), s = new Float32Array(r.length * 4), f = new Float32Array(r.length * 4), o = new Float32Array(r.length * 4), u = new Float32Array(r.length * 4), h = new Float32Array(r.length * 4), i = new Float32Array(r.length * 4); let d = 0, w = 0, a = 0, p = 0, m = 0, g = 0, v = 0, I = 0, x = 0, b = 0; for (const l of r) { for (let A = 0; A < 4; A++) c[d++] = l.lineIndex, s[w++] = l.lineLettersTotal, f[a++] = l.lineLetterIndex, o[p++] = l.lineWordsTotal, u[m++] = l.lineWordIndex, h[g++] = l.wordIndex, i[v++] = l.letterIndex; I = l.wordsTotal, x = l.linesTotal, b = l.lettersTotal; } return { linesTotal: x, lineIndex: c, lineLettersTotal: s, lineLetterIndex: f, lineWordsTotal: o, lineWordIndex: u, wordsTotal: I, wordIndex: h, lettersTotal: b, letterIndex: i }; } e.create = t; function n(r, c, s) { const f = [ [s.lineIndex, "lineIndex", 1], [s.lineLettersTotal, "lineLettersTotal", 1], [s.lineLetterIndex, "lineLetterIndex", 1], [s.lineWordsTotal, "lineWordsTotal", 1], [s.lineWordIndex, "lineWordIndex", 1], [s.wordIndex, "wordIndex", 1], [s.letterIndex, "letterIndex", 1] ]; for (const [o, u, h] of f) { const i = new _.VertexBuffer( r, o, u, !0, !1, h ); c.setVerticesBuffer(i); } } e.setCustomAttributes = n; })(U || (U = {})); function wt(e) { return e && e.__esModule && Object.prototype.hasOwnProperty.call(e, "default") ? e.default : e; } var pt = function(e) { switch (e) { case "int8": return Int8Array; case "int16": return Int16Array; case "int32": return Int32Array; case "uint8": return Uint8Array; case "uint16": return Uint16Array; case "uint32": return Uint32Array; case "float32": return Float32Array; case "float64": return Float64Array; case "array": return Array; case "uint8_clamped": return Uint8ClampedArray; } }, mt = Object.prototype.toString, Tt = xt; function xt(e) { return e.BYTES_PER_ELEMENT && mt.call(e.buffer) === "[object ArrayBuffer]" || Array.isArray(e); } /*! * Determine if an object is a Buffer * * @author Feross Aboukhadijeh <https://feross.org> * @license MIT */ var It = function(e) { return e != null && (q(e) || yt(e) || !!e._isBuffer); }; function q(e) { return !!e.constructor && typeof e.constructor.isBuffer == "function" && e.constructor.isBuffer(e); } function yt(e) { return typeof e.readFloatLE == "function" && typeof e.slice == "function" && q(e.slice(0, 0)); } var _t = pt, bt = Tt, At = It, Lt = [0, 2, 3], St = [2, 1, 3], Wt = function(t, n) { (!t || !(bt(t) || At(t))) && (n = t || {}, t = null), typeof n == "number" ? n = { count: n } : n = n || {}; for (var r = typeof n.type == "string" ? n.type : "uint16", c = typeof n.count == "number" ? n.count : 1, s = n.start || 0, f = n.clockwise !== !1 ? Lt : St, o = f[0], u = f[1], h = f[2], i = c * 6, d = t || new (_t(r))(i), w = 0, a = 0; w < i; w += 6, a += 4) { var p = w + s; d[p + 0] = a + 0, d[p + 1] = a + 1, d[p + 2] = a + 2, d[p + 3] = a + o, d[p + 4] = a + u, d[p + 5] = a + h; } return d; }; const Ct = /* @__PURE__ */ wt(Wt); function Mt(e, t, n) { const { color: r = new _.Color3(0, 0, 0), strokeColor: c = new _.Color3(0, 0, 0), opacity: s = 1, strokeWidth: f = 0.5, atlas: o, ...u } = t, h = n.getEngine(), i = ht(u), d = t.font, w = d.common.scaleW, a = d.common.scaleH, p = i.glyphs.filter((L) => { const y = L.data; return y.width * y.height > 0; }), m = B.create( p, w, a, !0, i ), g = U.create(p), v = Ct([], { clockwise: !0, type: "uint16", count: p.length }), I = new _.Mesh(e, n), x = new _.VertexData(); x.positions = m.positions, x.indices = v, x.uvs = m.uvs; const b = []; _.VertexData.ComputeNormals(m.positions, v, b), x.normals = b, B.setCustomAttributes(h, I, m), U.setCustomAttributes(h, I, g), x.applyToMesh(I), _.Effect.ShadersStore.MSDFVertexShader = kt, _.Effect.ShadersStore.MSDFFragmentShader = Ft; const l = new _.ShaderMaterial( "msdfShader", n, { vertex: "MSDF", fragment: "MSDF" }, { attributes: [ "position", "normal", "uv", "center", "layoutUv", "lineIndex", "lineLettersTotal", "lineLetterIndex", "lineWordsTotal", "lineWordIndex", "wordIndex", "letterIndex" ], uniforms: [ "world", "worldView", "worldViewProjection", "view", "viewProjection", "projection", "uColor", "uThreshold", "uStrokeOutsetWidth", "uStrokeInsetWidth", "uOpacity", "uAlphaTest", "uStrokeColor", "uLinesTotal", "uWordsTotal", "uLettersTotal" ], needAlphaBlending: !0 } ), A = o instanceof _.Texture ? o : new _.Texture(o, n); return l.setTexture("uFontAtlas", A), l.setColor3("uColor", r), l.setColor3("uStrokeColor", c), l.setFloat("uThreshold", 0.05), l.setFloat("uStrokeOutsetWidth", 0), l.setFloat("uStrokeInsetWidth", f), l.setFloat("uOpacity", s), l.setFloat("uAlphaTest", 0.01), l.setInt("uLinesTotal", g.linesTotal), l.setInt("uWordsTotal", g.wordsTotal), l.setInt("uLettersTotal", g.lettersTotal), l.backFaceCulling = !1, I.material = l, I; } const kt = ` precision highp float; attribute vec3 position; attribute vec2 uv; attribute vec2 center; attribute float lineIndex; attribute float lineLettersTotal; attribute float lineLetterIndex; attribute float lineWordsTotal; attribute float lineWordIndex; attribute float wordIndex; attribute float letterIndex; uniform mat4 viewProjection; varying vec2 vUv; varying vec2 vCenter; varying float vLineIndex; varying float vLineLettersTotal; varying float vLineLetterIndex; varying float vLineWordsTotal; varying float vLineWordIndex; varying float vWordIndex; varying float vLetterIndex; #include<instancesDeclaration> void main(void) { #include<instancesVertex> gl_Position = viewProjection * finalWorld * vec4(position, 1.0); vUv = uv; vCenter = center; vLineIndex = lineIndex; vLineLettersTotal = lineLettersTotal; vLineLetterIndex = lineLetterIndex; vLineWordsTotal = lineWordsTotal; vLineWordIndex = lineWordIndex; vWordIndex = wordIndex; vLetterIndex = letterIndex; } `, Ft = ` precision highp float; varying vec2 vUv; varying vec2 vCenter; varying float vLineIndex; varying float vLineLettersTotal; varying float vLineLetterIndex; varying float vLineWordsTotal; varying float vLineWordIndex; varying float vWordIndex; varying float vLetterIndex; uniform sampler2D uFontAtlas; uniform vec3 uStrokeColor; uniform vec3 uColor; uniform float uThreshold; uniform float uStrokeOutsetWidth; uniform float uStrokeInsetWidth; uniform float uOpacity; uniform float uAlphaTest; uniform int uLinesTotal; uniform int uWordsTotal; uniform int uLettersTotal; float median(float r, float g, float b) { return max(min(r, g), min(max(r, g), b)); } void main(void) { float thickness = 0.5; float softness = 0.5; vec3 s = texture2D(uFontAtlas, vUv).rgb; float sigDist = median(s.r, s.g, s.b) - 0.5; float afwidth = 1.4142135623730951 / 2.0; #ifdef IS_SMALL float alpha = smoothstep(uThreshold - afwidth, uThreshold + afwidth, sigDist); #else float alpha = clamp(sigDist / fwidth(sigDist) + 0.5, 0.0, 1.0); #endif float sigDistOutset = sigDist + uStrokeOutsetWidth * 0.5; float sigDistInset = sigDist - uStrokeInsetWidth * 0.5; #ifdef IS_SMALL float outset = smoothstep(uThreshold - afwidth, uThreshold + afwidth, sigDistOutset); float inset = 1.0 - smoothstep(uThreshold - afwidth, uThreshold + afwidth, sigDistInset); #else float outset = clamp(sigDistOutset / fwidth(sigDistOutset) + 0.5, 0.0, 1.0); float inset = 1.0 - clamp(sigDistInset / fwidth(sigDistInset) + 0.5, 0.0, 1.0); #endif float border = outset * inset; if (alpha < uAlphaTest) discard; vec4 filledFragColor = vec4(uColor, uOpacity * alpha); vec4 strokedFragColor = vec4(uStrokeColor, uOpacity * border); gl_FragColor = mix(filledFragColor, strokedFragColor, border); } `; export { Mt as createTextMesh };