textmode.js
Version:
Apply real-time ASCII conversion to any HTML canvas.
1,365 lines (1,363 loc) • 151 kB
JavaScript
var se = Object.defineProperty;
var ae = (h, e, t) => e in h ? se(h, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : h[e] = t;
var o = (h, e, t) => ae(h, typeof e != "symbol" ? e + "" : e, t);
class F extends Error {
constructor(t, r, i = {}) {
const s = F.createFormattedMessage(t, i);
super(s);
o(this, "originalError");
o(this, "context");
this.name = "TextmodeError", this.originalError = r, this.context = i;
}
/**
* Create a formatted error message that includes context
*/
static createFormattedMessage(t, r) {
let i = t;
if (r && Object.keys(r).length > 0) {
i += `
📋 Context:`;
for (const [s, a] of Object.entries(r)) {
const n = F.formatValue(a);
i += `
- ${s}: ${n}`;
}
}
return i += `
`, i += "↓".repeat(24) + `
`, i;
}
/**
* Format values for better display in error messages
*/
static formatValue(t) {
if (t === null) return "null";
if (t === void 0) return "undefined";
if (typeof t == "string") return `"${t}"`;
if (typeof t == "number" || typeof t == "boolean") return String(t);
if (Array.isArray(t))
return t.length === 0 ? "[]" : t.length <= 5 ? `[${t.map((r) => F.formatValue(r)).join(", ")}]` : `[${t.slice(0, 3).map((r) => F.formatValue(r)).join(", ")}, ... +${t.length - 3} more]`;
if (typeof t == "object") {
const r = Object.keys(t);
return r.length === 0 ? "{}" : r.length <= 3 ? `{ ${r.map((a) => `${a}: ${F.formatValue(t[a])}`).join(", ")} }` : `{ ${r.slice(0, 2).map((s) => `${s}: ${F.formatValue(t[s])}`).join(", ")}, ... +${r.length - 2} more }`;
}
return String(t);
}
}
var ne = /* @__PURE__ */ ((h) => (h[h.SILENT = 0] = "SILENT", h[h.WARNING = 1] = "WARNING", h[h.ERROR = 2] = "ERROR", h[h.THROW = 3] = "THROW", h))(ne || {});
const E = class E {
constructor() {
o(this, "_options", {
globalLevel: 3
/* THROW */
});
}
static getInstance() {
return E._instance || (E._instance = new E()), E._instance;
}
/**
* Handle an error based on the configured settings
* @returns true if execution should continue, false if error was handled
*/
_handle(e, t, r) {
const i = "[textmode.js]";
switch (this._options.globalLevel) {
case 0:
return !1;
// Validation failed, handled silently
case 1:
return console.group(
`%c${i} Oops! (╯°□°)╯︵ Something went wrong in your code.`,
"color: #f44336; font-weight: bold; background: #ffebee; padding: 2px 6px; border-radius: 3px;"
), console.warn(F.createFormattedMessage(e, t)), console.groupEnd(), !1;
case 2:
return console.group(
`%c${i} Oops! (╯°□°)╯︵ Something went wrong in your code.`,
"color: #f44336; font-weight: bold; background: #ffebee; padding: 2px 6px; border-radius: 3px;"
), console.error(F.createFormattedMessage(e, t)), console.groupEnd(), !1;
case 3:
default:
const s = new F(e, r, t);
throw console.group(
`%c${i} Oops! (╯°□°)╯︵ Something went wrong in your code.`,
"color: #d32f2f; font-weight: bold; background: #ffcdd2; padding: 2px 6px; border-radius: 3px;"
), s;
}
}
/**
* Validate a condition and handle errors if validation fails
* @param condition The condition to validate
* @param message Error message if validation fails
* @param context Additional context for debugging
* @returns true if validation passed, false if validation failed and was handled
*/
validate(e, t, r) {
return e ? !0 : (this._handle(t, r), !1);
}
/**
* Set global error level
*/
setGlobalLevel(e) {
this._options.globalLevel = e;
}
};
o(E, "_instance", null);
let z = E;
const x = z.getInstance();
class oe {
constructor(e, t, r = t, i = {}) {
o(this, "gl");
o(this, "_framebuffer");
o(this, "_texture");
o(this, "_width");
o(this, "_height");
o(this, "options");
o(this, "previousState", null);
o(this, "_pixels", null);
this.gl = e, this._width = t, this._height = r, this.options = {
filter: "nearest",
wrap: "clamp",
format: "rgba",
type: "unsigned_byte",
...i
}, this._texture = this.createTexture(), this._framebuffer = e.createFramebuffer(), this.attachTexture();
}
createTexture() {
const { gl: e } = this, t = e.createTexture();
e.bindTexture(e.TEXTURE_2D, t);
const r = this.options.filter === "linear" ? e.LINEAR : e.NEAREST, i = this.options.wrap === "repeat" ? e.REPEAT : e.CLAMP_TO_EDGE;
return e.texParameteri(e.TEXTURE_2D, e.TEXTURE_MIN_FILTER, r), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_MAG_FILTER, r), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_WRAP_S, i), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_WRAP_T, i), this.updateTextureSize(), t;
}
updateTextureSize() {
const { gl: e } = this, t = e.RGBA, r = e.RGBA, i = this.options.type === "float" ? e.FLOAT : e.UNSIGNED_BYTE;
e.texImage2D(e.TEXTURE_2D, 0, t, this._width, this._height, 0, r, i, null);
}
attachTexture() {
const { gl: e } = this;
e.bindFramebuffer(e.FRAMEBUFFER, this._framebuffer), e.framebufferTexture2D(e.FRAMEBUFFER, e.COLOR_ATTACHMENT0, e.TEXTURE_2D, this._texture, 0), e.bindFramebuffer(e.FRAMEBUFFER, null);
}
/**
* Update the framebuffer texture with canvas or video content
*/
update(e) {
const { gl: t } = this;
e instanceof HTMLVideoElement && e.readyState < 2 || (t.bindTexture(t.TEXTURE_2D, this._texture), t.texImage2D(t.TEXTURE_2D, 0, t.RGBA, t.RGBA, t.UNSIGNED_BYTE, e), t.bindTexture(t.TEXTURE_2D, null));
}
/**
* Update the framebuffer texture with pixel data
*/
updatePixels(e, t, r) {
const { gl: i } = this;
i.bindTexture(i.TEXTURE_2D, this._texture), i.texImage2D(i.TEXTURE_2D, 0, i.RGBA, t, r, 0, i.RGBA, i.UNSIGNED_BYTE, e), i.bindTexture(i.TEXTURE_2D, null);
}
/**
* Resize the framebuffer
*/
resize(e, t) {
const { gl: r } = this;
this._width = e, this._height = t, r.bindTexture(r.TEXTURE_2D, this._texture), this.updateTextureSize(), r.bindTexture(r.TEXTURE_2D, null);
}
/**
* Begin rendering to this framebuffer
*/
begin() {
const { gl: e } = this;
this.previousState = {
framebuffer: e.getParameter(e.FRAMEBUFFER_BINDING),
viewport: e.getParameter(e.VIEWPORT)
}, e.bindFramebuffer(e.FRAMEBUFFER, this._framebuffer), e.viewport(0, 0, this._width, this._height);
}
/**
* End rendering to this framebuffer and restore previous state
*/
end() {
if (!this.previousState) return;
const { gl: e } = this;
e.bindFramebuffer(e.FRAMEBUFFER, this.previousState.framebuffer), e.viewport(...this.previousState.viewport), this.previousState = null;
}
/**
* Load pixel data from the framebuffer into the pixels array
*/
loadPixels() {
const { gl: e } = this;
this._pixels || (this._pixels = new Uint8Array(this._width * this._height * 4));
const t = e.getParameter(e.FRAMEBUFFER_BINDING);
e.bindFramebuffer(e.FRAMEBUFFER, this._framebuffer), e.readPixels(0, 0, this._width, this._height, e.RGBA, e.UNSIGNED_BYTE, this._pixels), e.bindFramebuffer(e.FRAMEBUFFER, t);
}
get(e, t, r, i) {
const { gl: s } = this;
if (e === void 0 && t === void 0) {
const a = new Uint8Array(this._width * this._height * 4), n = s.getParameter(s.FRAMEBUFFER_BINDING);
return s.bindFramebuffer(s.FRAMEBUFFER, this._framebuffer), s.readPixels(0, 0, this._width, this._height, s.RGBA, s.UNSIGNED_BYTE, a), s.bindFramebuffer(s.FRAMEBUFFER, n), a;
} else if (r === void 0 && i === void 0) {
(e < 0 || t < 0 || e >= this._width || t >= this._height) && (console.warn("The x and y values passed to Framebuffer.get are outside of its range and will be clamped."), e = Math.max(0, Math.min(e, this._width - 1)), t = Math.max(0, Math.min(t, this._height - 1)));
const a = new Uint8Array(4), n = s.getParameter(s.FRAMEBUFFER_BINDING);
return s.bindFramebuffer(s.FRAMEBUFFER, this._framebuffer), s.readPixels(e, t, 1, 1, s.RGBA, s.UNSIGNED_BYTE, a), s.bindFramebuffer(s.FRAMEBUFFER, n), [a[0], a[1], a[2], a[3]];
} else {
e = Math.max(0, Math.min(e, this._width - 1)), t = Math.max(0, Math.min(t, this._height - 1)), r = Math.max(1, Math.min(r, this._width - e)), i = Math.max(1, Math.min(i, this._height - t));
const a = new Uint8Array(r * i * 4), n = s.getParameter(s.FRAMEBUFFER_BINDING);
return s.bindFramebuffer(s.FRAMEBUFFER, this._framebuffer), s.readPixels(e, t, r, i, s.RGBA, s.UNSIGNED_BYTE, a), s.bindFramebuffer(s.FRAMEBUFFER, n), a;
}
}
get framebuffer() {
return this._framebuffer;
}
get texture() {
return this._texture;
}
get width() {
return this._width;
}
get height() {
return this._height;
}
get pixels() {
return this._pixels;
}
}
class Q {
constructor(e, t, r) {
o(this, "gl");
o(this, "x");
o(this, "y");
this.gl = e, this.x = t, this.y = r;
}
}
class M {
constructor(e, t, r, i, s) {
/** The WebGL rendering context */
o(this, "gl");
/** The vertex buffer containing position and texture coordinates */
o(this, "vertexBuffer");
/** The number of vertices in this geometry (always 6 for two triangles) */
o(this, "vertexCount", 6);
/** Bytes per vertex: depends on position format (vec2 vs vec3) */
o(this, "bytesPerVertex");
this.gl = e, this.bytesPerVertex = 16;
const a = e.getParameter(e.VIEWPORT), n = a[2], l = a[3], c = e.getParameter(e.FRAMEBUFFER_BINDING) !== null, u = t / n * 2 - 1, d = (t + i) / n * 2 - 1;
let f, g;
c ? (f = r / l * 2 - 1, g = (r + s) / l * 2 - 1) : (f = 1 - r / l * 2, g = 1 - (r + s) / l * 2);
let _, p, C, b;
_ = u, C = d, p = f, b = g;
const v = this.generateVertices(_, p, C, b);
this.vertexBuffer = e.createBuffer(), e.bindBuffer(e.ARRAY_BUFFER, this.vertexBuffer), e.bufferData(e.ARRAY_BUFFER, v, e.STATIC_DRAW);
}
/**
* Generate vertex data for the rectangle with texture coordinates
* @private
*/
generateVertices(e, t, r, i) {
return new Float32Array([
e,
i,
0,
1,
// bottom-left
r,
i,
1,
1,
// bottom-right
e,
t,
0,
0,
// top-left
e,
t,
0,
0,
// top-left
r,
i,
1,
1,
// bottom-right
r,
t,
1,
0
// top-right
]);
}
/**
* Render the rectangle using position and texture coordinate attributes
*/
render() {
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
const e = this.gl.getParameter(this.gl.CURRENT_PROGRAM);
let t = this.gl.getAttribLocation(e, "a_position"), r = this.gl.getAttribLocation(e, "a_texCoord");
this.gl.enableVertexAttribArray(t), this.gl.vertexAttribPointer(t, 2, this.gl.FLOAT, !1, this.bytesPerVertex, 0), this.gl.enableVertexAttribArray(r), this.gl.vertexAttribPointer(r, 2, this.gl.FLOAT, !1, this.bytesPerVertex, 8), this.gl.drawArrays(this.gl.TRIANGLES, 0, this.vertexCount), this.gl.disableVertexAttribArray(t), this.gl.disableVertexAttribArray(r);
}
}
class he extends Q {
constructor(t, r, i, s, a) {
super(t, r, i);
o(this, "width");
o(this, "height");
this.width = s, this.height = a;
}
/**
* Render the filled rectangle using the existing Rectangle geometry.
*/
renderFill() {
new M(this.gl, this.x, this.y, this.width, this.height).render();
}
/**
* Render the stroke rectangle as four separate Rectangle instances for each edge.
* This approach ensures clean corners with proper overlap and leverages existing geometry.
* @param weight The stroke thickness in pixels
*/
renderStroke(t) {
if (t <= 0) return;
const r = new M(this.gl, this.x, this.y, this.width, t), i = new M(this.gl, this.x + this.width - t, this.y, t, this.height), s = new M(this.gl, this.x, this.y + this.height - t, this.width, t), a = new M(this.gl, this.x, this.y, t, this.height);
r.render(), i.render(), s.render(), a.render();
}
}
class le {
constructor(e, t, r, i, s, a) {
/** The WebGL rendering context */
o(this, "gl");
/** The vertex buffer containing position and texture coordinates */
o(this, "vertexBuffer");
/** The number of vertices in this geometry (always 6 for two triangles) */
o(this, "vertexCount", 6);
/** Bytes per vertex: vec2+vec2 = 16 bytes */
o(this, "bytesPerVertex");
this.gl = e, this.bytesPerVertex = 16;
const n = e.getParameter(e.VIEWPORT), l = n[2], c = n[3], u = e.getParameter(e.FRAMEBUFFER_BINDING) !== null, d = i - t, f = s - r, g = Math.sqrt(d * d + f * f);
if (g === 0) {
const ie = this.generateVertices(0, 0, 0, 0);
this.vertexBuffer = e.createBuffer(), e.bindBuffer(e.ARRAY_BUFFER, this.vertexBuffer), e.bufferData(e.ARRAY_BUFFER, ie, e.STATIC_DRAW);
return;
}
const _ = d / g, C = -(f / g), b = _, v = a / 2, w = t + C * v, A = r + b * v, R = t - C * v, y = r - b * v, P = i + C * v, D = s + b * v, Z = i - C * v, W = s - b * v, J = w / l * 2 - 1, K = R / l * 2 - 1, ee = P / l * 2 - 1, te = Z / l * 2 - 1;
let B, G, V, k;
u ? (B = A / c * 2 - 1, G = y / c * 2 - 1, V = D / c * 2 - 1, k = W / c * 2 - 1) : (B = 1 - A / c * 2, G = 1 - y / c * 2, V = 1 - D / c * 2, k = 1 - W / c * 2);
const re = this.generateLineVertices(
J,
B,
K,
G,
ee,
V,
te,
k
);
this.vertexBuffer = e.createBuffer(), e.bindBuffer(e.ARRAY_BUFFER, this.vertexBuffer), e.bufferData(e.ARRAY_BUFFER, re, e.STATIC_DRAW);
}
/**
* Generate vertex data for a rectangle representing the line with texture coordinates
* @private
*/
generateVertices(e, t, r, i) {
return new Float32Array([
e,
i,
0,
1,
// bottom-left
r,
i,
1,
1,
// bottom-right
e,
t,
0,
0,
// top-left
e,
t,
0,
0,
// top-left
r,
i,
1,
1,
// bottom-right
r,
t,
1,
0
// top-right
]);
}
/**
* Generate vertex data for the line rectangle with texture coordinates
* Uses the four corners calculated based on line direction and thickness
* @private
*/
generateLineVertices(e, t, r, i, s, a, n, l) {
return new Float32Array([
e,
t,
0,
0,
// corner1 (start + perpendicular)
r,
i,
0,
1,
// corner2 (start - perpendicular)
s,
a,
1,
0,
// corner3 (end + perpendicular)
r,
i,
0,
1,
// corner2 (start - perpendicular)
n,
l,
1,
1,
// corner4 (end - perpendicular)
s,
a,
1,
0
// corner3 (end + perpendicular)
]);
}
/**
* Render the line using position and texture coordinate attributes
*/
render() {
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
const e = this.gl.getParameter(this.gl.CURRENT_PROGRAM);
let t = this.gl.getAttribLocation(e, "a_position"), r = this.gl.getAttribLocation(e, "a_texCoord");
this.gl.enableVertexAttribArray(t), this.gl.vertexAttribPointer(t, 2, this.gl.FLOAT, !1, this.bytesPerVertex, 0), this.gl.enableVertexAttribArray(r), this.gl.vertexAttribPointer(r, 2, this.gl.FLOAT, !1, this.bytesPerVertex, 8), this.gl.drawArrays(this.gl.TRIANGLES, 0, this.vertexCount), this.gl.disableVertexAttribArray(t), this.gl.disableVertexAttribArray(r);
}
}
class ce extends Q {
constructor(t, r, i, s, a) {
super(t, r, i);
o(this, "x2");
o(this, "y2");
this.x2 = s, this.y2 = a;
}
/**
* Lines don't support fill rendering - this method does nothing.
* Lines are rendered only with stroke properties.
*/
renderFill() {
}
/**
* Render the line with the specified stroke weight.
* @param weight The stroke thickness in pixels
*/
renderStroke(t) {
if (t <= 0) return;
new le(this.gl, this.x, this.y, this.x2, this.y2, t).render();
}
}
class T {
constructor(e, t, r) {
o(this, "gl");
o(this, "program");
o(this, "uniformLocations", /* @__PURE__ */ new Map());
o(this, "attributeLocations", /* @__PURE__ */ new Map());
o(this, "textureUnitCounter", 0);
this.gl = e, this.program = this.createProgram(t, r), this.cacheLocations();
}
createProgram(e, t) {
const r = this.createShader(this.gl.VERTEX_SHADER, e), i = this.createShader(this.gl.FRAGMENT_SHADER, t), s = this.gl.createProgram();
if (this.gl.attachShader(s, r), this.gl.attachShader(s, i), this.gl.linkProgram(s), !this.gl.getProgramParameter(s, this.gl.LINK_STATUS)) {
const a = this.gl.getProgramInfoLog(s);
throw new Error(`Shader program link error: ${a}`);
}
return this.gl.deleteShader(r), this.gl.deleteShader(i), s;
}
createShader(e, t) {
const r = this.gl.createShader(e);
if (this.gl.shaderSource(r, t), this.gl.compileShader(r), !this.gl.getShaderParameter(r, this.gl.COMPILE_STATUS)) {
const i = this.gl.getShaderInfoLog(r);
throw this.gl.deleteShader(r), new Error(`Shader compilation error: ${i}`);
}
return r;
}
cacheLocations() {
const e = this.gl.getProgramParameter(this.program, this.gl.ACTIVE_UNIFORMS);
for (let r = 0; r < e; r++) {
const i = this.gl.getActiveUniform(this.program, r);
if (i) {
const s = this.gl.getUniformLocation(this.program, i.name);
s && this.uniformLocations.set(i.name, s);
}
}
const t = this.gl.getProgramParameter(this.program, this.gl.ACTIVE_ATTRIBUTES);
for (let r = 0; r < t; r++) {
const i = this.gl.getActiveAttrib(this.program, r);
if (i) {
const s = this.gl.getAttribLocation(this.program, i.name);
this.attributeLocations.set(i.name, s);
}
}
}
/**
* Use this shader program
*/
use() {
this.gl.useProgram(this.program), this.resetTextureUnits();
}
/**
* Set a single uniform value with automatic texture unit management
*/
setUniform(e, t) {
const r = this.uniformLocations.get(e);
if (!r)
return;
const i = this.getUniformInfo(e);
if (typeof t == "number")
i && i.type === this.gl.INT ? this.gl.uniform1i(r, Math.floor(t)) : this.gl.uniform1f(r, t);
else if (typeof t == "boolean")
this.gl.uniform1i(r, t ? 1 : 0);
else if (Array.isArray(t))
if (i && (i.type === this.gl.INT_VEC2 || i.type === this.gl.INT_VEC3 || i.type === this.gl.INT_VEC4)) {
const s = t.map((a) => Math.floor(a));
switch (s.length) {
case 2:
this.gl.uniform2iv(r, s);
break;
case 3:
this.gl.uniform3iv(r, s);
break;
case 4:
this.gl.uniform4iv(r, s);
break;
default:
console.warn(`Unsupported array length ${s.length} for uniform '${e}'`);
}
} else
switch (t.length) {
case 2:
this.gl.uniform2f(r, t[0], t[1]);
break;
case 3:
this.gl.uniform3f(r, t[0], t[1], t[2]);
break;
case 4:
this.gl.uniform4f(r, t[0], t[1], t[2], t[3]);
break;
default:
console.warn(`Unsupported array length ${t.length} for uniform '${e}'`);
}
else if (t instanceof WebGLTexture) {
const s = this.getNextTextureUnit();
this.gl.uniform1i(r, s), this.gl.activeTexture(this.gl.TEXTURE0 + s), this.gl.bindTexture(this.gl.TEXTURE_2D, t);
} else if (t && typeof t == "object" && "texture" in t) {
const s = this.getNextTextureUnit();
this.gl.uniform1i(r, s), this.gl.activeTexture(this.gl.TEXTURE0 + s), this.gl.bindTexture(this.gl.TEXTURE_2D, t.texture);
} else
console.warn(`Unsupported uniform type for '${e}':`, typeof t);
}
/**
* Get uniform info to determine the correct WebGL type
*/
getUniformInfo(e) {
const t = this.gl.getProgramParameter(this.program, this.gl.ACTIVE_UNIFORMS);
for (let r = 0; r < t; r++) {
const i = this.gl.getActiveUniform(this.program, r);
if (i && i.name === e)
return i;
}
return null;
}
getNextTextureUnit() {
return this.textureUnitCounter++;
}
/**
* Check if this shader has a specific uniform
*/
hasUniform(e) {
return this.uniformLocations.has(e);
}
/**
* Check if this shader has a specific attribute
*/
hasAttribute(e) {
return this.attributeLocations.has(e);
}
/**
* Get the WebGL program
*/
get glProgram() {
return this.program;
}
/**
* Reset texture unit counter (useful when starting a new frame)
*/
resetTextureUnits() {
this.textureUnitCounter = 0;
}
}
var S = "attribute vec2 a_position;attribute vec2 a_texCoord;varying vec2 v_uv;uniform float u_rotation;uniform vec2 u_center;uniform float u_aspectRatio;mat2 rotate2D(float angle){float s=sin(angle);float c=cos(angle);return mat2(c,-s,s,c);}void main(){v_uv=a_texCoord;vec2 pos=a_position;pos-=u_center;pos.x*=u_aspectRatio;pos=rotate2D(-u_rotation)*pos;pos.x/=u_aspectRatio;pos+=u_center;gl_Position=vec4(pos,0.0,1.0);}", ue = "precision lowp float;uniform sampler2D u_texture;varying vec2 v_uv;void main(){gl_FragColor=texture2D(u_texture,v_uv);}", de = "precision lowp float;uniform vec4 u_color;void main(){gl_FragColor=u_color;}";
class fe {
constructor(e) {
o(this, "gl");
o(this, "imageShader");
o(this, "solidColorShader");
o(this, "currentShader", null);
// Fill state management - default: white fill enabled
o(this, "currentFillColor", [1, 1, 1, 1]);
o(this, "fillMode", !0);
// Stroke state management - default: black stroke enabled, weight 1
o(this, "currentStrokeColor", [0, 0, 0, 1]);
o(this, "currentStrokeWeight", 1);
o(this, "strokeMode", !0);
// Transformation state management
o(this, "currentRotation", 0);
// in degrees
// State stack for push/pop functionality
o(this, "stateStack", []);
this.gl = e, this.imageShader = new T(this.gl, S, ue), this.solidColorShader = new T(this.gl, S, de), this.gl.enable(this.gl.BLEND), this.gl.blendEquation(this.gl.FUNC_ADD), this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
}
/**
* Set the current shader
*/
shader(e) {
this.currentShader = e, e.use();
}
/**
* Sets the fill color for subsequent rendering operations
* @param r Red component *(0-255)*
* @param g Green component *(0-255, optional)*
* @param b Blue component *(0-255, optional)*
* @param a Alpha component *(0-255, optional)*
*/
fill(e, t, r, i) {
if (this.fillMode = !0, t === void 0 && r === void 0 && i === void 0) {
const s = e / 255;
this.currentFillColor = [s, s, s, 1];
} else if (r !== void 0 && i === void 0)
this.currentFillColor = [e / 255, t / 255, r / 255, 1];
else if (r !== void 0 && i !== void 0)
this.currentFillColor = [e / 255, t / 255, r / 255, i / 255];
else
throw new Error("Invalid fill parameters. Use fill(gray), fill(r,g,b), or fill(r,g,b,a)");
}
/**
* Sets the stroke color for subsequent rendering operations
* @param r Red component *(0-255)*
* @param g Green component *(0-255, optional)*
* @param b Blue component *(0-255, optional)*
* @param a Alpha component *(0-255, optional)*
*/
stroke(e, t, r, i) {
if (this.strokeMode = !0, t === void 0 && r === void 0 && i === void 0) {
const s = e / 255;
this.currentStrokeColor = [s, s, s, 1];
} else if (r !== void 0 && i === void 0)
this.currentStrokeColor = [e / 255, t / 255, r / 255, 1];
else if (r !== void 0 && i !== void 0)
this.currentStrokeColor = [e / 255, t / 255, r / 255, i / 255];
else
throw new Error("Invalid stroke parameters. Use stroke(gray), stroke(r,g,b), or stroke(r,g,b,a)");
}
/**
* Sets the stroke weight (thickness) for subsequent stroke operations
* @param weight The stroke thickness in pixels
*/
strokeWeight(e) {
if (e < 0)
throw new Error("Stroke weight must be non-negative");
this.currentStrokeWeight = e;
}
/**
* Disables stroke rendering for subsequent operations
*/
noStroke() {
this.strokeMode = !1;
}
/**
* Disables fill rendering for subsequent operations
*/
noFill() {
this.fillMode = !1;
}
/**
* Sets the rotation angle for subsequent rendering operations
* @param degrees The rotation angle in degrees
*/
rotate(e) {
this.currentRotation = e;
}
/**
* Save the current rendering state (fill, stroke, etc.) to the state stack
*/
push() {
this.stateStack.push({
fillColor: [...this.currentFillColor],
fillMode: this.fillMode,
strokeColor: [...this.currentStrokeColor],
strokeWeight: this.currentStrokeWeight,
strokeMode: this.strokeMode,
rotation: this.currentRotation
});
}
/**
* Restore the most recently saved rendering state from the state stack
*/
pop() {
const e = this.stateStack.pop();
e ? (this.currentFillColor = e.fillColor, this.fillMode = e.fillMode, this.currentStrokeColor = e.strokeColor, this.currentStrokeWeight = e.strokeWeight, this.strokeMode = e.strokeMode, this.currentRotation = e.rotation) : console.warn("pop() called without matching push()");
}
/**
* Reset frame-specific state - called automatically after each frame.
* Note: This does not reset fill/stroke state as that should persist
* across frames and be managed by push/pop or explicit calls.
*/
reset() {
this.currentShader = null, this.stateStack = [], this.currentRotation = 0, this.fillMode = !0, this.strokeMode = !0, this.currentFillColor = [1, 1, 1, 1], this.currentStrokeColor = [0, 0, 0, 1], this.currentStrokeWeight = 1;
}
createShader(e, t) {
return new T(this.gl, e, t);
}
/**
* Set a uniform value for the current shader
*/
setUniform(e, t) {
this.currentShader.setUniform(e, t);
}
/**
* Draw a rectangle with the current fill and/or stroke settings
*/
rect(e, t, r, i) {
const s = new he(this.gl, e, t, r, i);
if (this.currentShader !== null) {
if (this.currentRotation !== 0) {
const { centerX: d, centerY: f, radians: g, aspectRatio: _ } = this.calculateRotationParams(e, t, r, i);
this.setUniform("u_rotation", g), this.setUniform("u_center", [d, f]), this.setUniform("u_aspectRatio", _);
} else
this.setUniform("u_rotation", 0), this.setUniform("u_center", [0, 0]), this.setUniform("u_aspectRatio", 1);
s.renderFill(), this.currentShader = null;
return;
}
const a = this.solidColorShader, { centerX: n, centerY: l, radians: c, aspectRatio: u } = this.calculateRotationParams(e, t, r, i);
this.fillMode && (this.shader(a), this.setUniform("u_color", this.currentFillColor), this.setUniform("u_rotation", c), this.setUniform("u_center", [n, l]), this.setUniform("u_aspectRatio", u), s.renderFill()), this.strokeMode && (this.shader(a), this.setUniform("u_color", this.currentStrokeColor), this.setUniform("u_rotation", c), this.setUniform("u_center", [n, l]), this.setUniform("u_aspectRatio", u), s.renderStroke(this.currentStrokeWeight)), this.currentShader = null;
}
/**
* Draw a line from (x1, y1) to (x2, y2) with the current stroke settings.
* Lines only support stroke rendering - fill properties are ignored.
* @param x1 X-coordinate of the line start point
* @param y1 Y-coordinate of the line start point
* @param x2 X-coordinate of the line end point
* @param y2 Y-coordinate of the line end point
*/
line(e, t, r, i) {
if (!this.strokeMode) return;
const s = new ce(this.gl, e, t, r, i);
if (this.currentShader !== null) {
if (this.currentRotation !== 0) {
const p = (e + r) / 2, C = (t + i) / 2, b = Math.abs(r - e), v = Math.abs(i - t), { centerX: w, centerY: A, radians: R, aspectRatio: y } = this.calculateRotationParams(p - b / 2, C - v / 2, b, v);
this.setUniform("u_rotation", R), this.setUniform("u_center", [w, A]), this.setUniform("u_aspectRatio", y);
} else
this.setUniform("u_rotation", 0), this.setUniform("u_center", [0, 0]), this.setUniform("u_aspectRatio", 1);
s.renderStroke(this.currentStrokeWeight), this.currentShader = null;
return;
}
const a = this.solidColorShader, n = (e + r) / 2, l = (t + i) / 2, c = Math.abs(r - e), u = Math.abs(i - t), { centerX: d, centerY: f, radians: g, aspectRatio: _ } = this.calculateRotationParams(n - c / 2, l - u / 2, c, u);
this.shader(a), this.setUniform("u_color", this.currentStrokeColor), this.setUniform("u_rotation", g), this.setUniform("u_center", [d, f]), this.setUniform("u_aspectRatio", _), s.renderStroke(this.currentStrokeWeight), this.currentShader = null;
}
/**
* Calculate rotation parameters for built-in shaders (NDC coordinates)
*/
calculateRotationParams(e, t, r, i) {
const s = this.gl.getParameter(this.gl.VIEWPORT), a = s[2], n = s[3], l = a / n, c = this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING) !== null, u = e + r / 2, d = t + i / 2, f = u / a * 2 - 1;
let g;
c ? g = d / n * 2 - 1 : g = 1 - d / n * 2;
const _ = this.currentRotation * Math.PI / 180;
return { centerX: f, centerY: g, radians: _, aspectRatio: l };
}
/**
* Create a new framebuffer
*/
createFramebuffer(e, t, r = {}) {
return new oe(this.gl, e, t, r);
}
/**
* Fill the current framebuffer with a solid color
*/
background(e, t = e, r = e, i = 255) {
this.clear(e / 255, t / 255, r / 255, i / 255);
}
/**
* Clear the current framebuffer
*/
clear(e = 0, t = 0, r = 0, i = 0) {
this.gl.clearColor(e, t, r, i), this.gl.clear(this.gl.COLOR_BUFFER_BIT);
}
/**
* Ensure viewport matches canvas dimensions
*/
resetViewport() {
this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
}
/**
* Get the WebGL context
*/
get context() {
return this.gl;
}
/**
* Render a framebuffer at a specific position with optional scaling
*/
image(e, t, r, i, s) {
this.shader(this.imageShader), this.setUniform("u_texture", e.texture);
const { centerX: a, centerY: n, radians: l, aspectRatio: c } = this.calculateRotationParams(
t,
r,
i ?? e.width,
s ?? e.height
);
this.setUniform("u_rotation", l), this.setUniform("u_center", [a, n]), this.setUniform("u_aspectRatio", c), this.rect(t, r, i ?? e.width, s ?? e.height);
}
}
var m = {};
m.parse = function(h) {
var e = function(s, a, n, l) {
var c = m.T, u = {
cmap: c.cmap,
head: c.head,
hhea: c.hhea,
maxp: c.maxp,
hmtx: c.hmtx,
loca: c.loca,
glyf: c.glyf
}, d = { _data: s, _index: a, _offset: n };
for (var f in u) {
var g = m.findTable(s, f, n);
if (g) {
var _ = g[0], p = l[_];
p == null && (p = u[f].parseTab(s, _, g[1], d)), d[f] = l[_] = p;
}
}
return d;
}, t = new Uint8Array(h), r = {}, i = e(t, 0, 0, r);
return [i];
};
m.findTable = function(h, e, t) {
for (var r = m.B, i = r.readUshort(h, t + 4), s = t + 12, a = 0; a < i; a++) {
var n = r.readASCII(h, s, 4);
r.readUint(h, s + 4);
var l = r.readUint(h, s + 8), c = r.readUint(h, s + 12);
if (n == e) return [l, c];
s += 16;
}
return null;
};
m.T = {};
m.B = {
readShort: function(h, e) {
var t = m.B.t.uint16;
return t[0] = h[e] << 8 | h[e + 1], m.B.t.int16[0];
},
readUshort: function(h, e) {
return h[e] << 8 | h[e + 1];
},
readUshorts: function(h, e, t) {
for (var r = [], i = 0; i < t; i++)
r.push(m.B.readUshort(h, e + i * 2));
return r;
},
readUint: function(h, e) {
var t = m.B.t.uint8;
return t[3] = h[e], t[2] = h[e + 1], t[1] = h[e + 2], t[0] = h[e + 3], m.B.t.uint32[0];
},
readASCII: function(h, e, t) {
for (var r = "", i = 0; i < t; i++) r += String.fromCharCode(h[e + i]);
return r;
},
// Simplified typed array buffer - only what's needed
t: function() {
var h = new ArrayBuffer(8);
return {
uint8: new Uint8Array(h),
int16: new Int16Array(h),
uint16: new Uint16Array(h),
uint32: new Uint32Array(h)
};
}()
};
m.T.cmap = {
parseTab: function(h, e, t) {
var r = { tables: [], ids: {}, off: e };
h = new Uint8Array(h.buffer, e, t), e = 0;
var i = m.B, s = i.readUshort, a = m.T.cmap;
s(h, e), e += 2;
var n = s(h, e);
e += 2;
for (var l = [], c = 0; c < n; c++) {
var u = s(h, e);
e += 2;
var d = s(h, e);
e += 2;
var f = i.readUint(h, e);
e += 4;
var g = "p" + u + "e" + d, _ = l.indexOf(f);
if (_ == -1) {
_ = r.tables.length;
var p = {};
l.push(f);
var C = p.format = s(h, f);
C == 4 ? p = a.parse4(h, f, p) : C == 12 && (p = a.parse12(h, f, p)), r.tables.push(p);
}
r.ids[g] != null && console.log("multiple tables for one platform+encoding: " + g), r.ids[g] = _;
}
return r;
},
parse4: function(h, e, t) {
var r = m.B, i = r.readUshort, s = r.readUshorts, a = e;
e += 2;
var n = i(h, e);
e += 2, i(h, e), e += 2;
var l = i(h, e);
e += 2;
var c = l >>> 1;
t.searchRange = i(h, e), e += 2, t.entrySelector = i(h, e), e += 2, t.rangeShift = i(h, e), e += 2, t.endCount = s(h, e, c), e += c * 2, e += 2, t.startCount = s(h, e, c), e += c * 2, t.idDelta = [];
for (var u = 0; u < c; u++)
t.idDelta.push(r.readShort(h, e)), e += 2;
return t.idRangeOffset = s(h, e, c), e += c * 2, t.glyphIdArray = s(h, e, a + n - e >> 1), t;
},
parse12: function(h, e, t) {
var r = m.B, i = r.readUint;
e += 4, i(h, e), e += 4, i(h, e), e += 4;
var s = i(h, e) * 3;
e += 4;
for (var a = t.groups = new Uint32Array(s), n = 0; n < s; n += 3)
a[n] = i(h, e + (n << 2)), a[n + 1] = i(h, e + (n << 2) + 4), a[n + 2] = i(h, e + (n << 2) + 8);
return t;
}
};
m.T.head = {
parseTab: function(h, e, t) {
var r = m.B, i = {};
return e += 18, i.unitsPerEm = r.readUshort(h, e), e += 2, e += 16, i.xMin = r.readShort(h, e), e += 2, i.yMin = r.readShort(h, e), e += 2, i.xMax = r.readShort(h, e), e += 2, i.yMax = r.readShort(h, e), e += 2, e += 6, i.indexToLocFormat = r.readShort(h, e), i;
}
};
m.T.hhea = {
parseTab: function(h, e, t) {
var r = m.B, i = {};
e += 4;
for (var s = [
"ascender",
"descender",
"lineGap",
"advanceWidthMax",
"minLeftSideBearing",
"minRightSideBearing",
"xMaxExtent",
"caretSlopeRise",
"caretSlopeRun",
"caretOffset",
"res0",
"res1",
"res2",
"res3",
"metricDataFormat",
"numberOfHMetrics"
], a = 0; a < s.length; a++) {
var n = s[a], l = n == "advanceWidthMax" || n == "numberOfHMetrics" ? r.readUshort : r.readShort;
i[n] = l(h, e + a * 2);
}
return i;
}
};
m.T.hmtx = {
parseTab: function(h, e, t, r) {
for (var i = m.B, s = [], a = [], n = r.maxp.numGlyphs, l = r.hhea.numberOfHMetrics, c = 0, u = 0, d = 0; d < l; )
c = i.readUshort(h, e + (d << 2)), u = i.readShort(h, e + (d << 2) + 2), s.push(c), a.push(u), d++;
for (; d < n; )
s.push(c), a.push(u), d++;
return { aWidth: s, lsBearing: a };
}
};
m.T.maxp = {
parseTab: function(h, e, t) {
var r = m.B, i = r.readUshort, s = {};
return r.readUint(h, e), e += 4, s.numGlyphs = i(h, e), e += 2, s;
}
};
m.T.loca = {
parseTab: function(h, e, t, r) {
var i = m.B, s = [], a = r.head.indexToLocFormat, n = r.maxp.numGlyphs + 1;
if (a == 0) for (var l = 0; l < n; l++) s.push(i.readUshort(h, e + (l << 1)) << 1);
if (a == 1) for (var l = 0; l < n; l++) s.push(i.readUint(h, e + (l << 2)));
return s;
}
};
m.T.glyf = {
parseTab: function(h, e, t, r) {
for (var i = [], s = r.maxp.numGlyphs, a = 0; a < s; a++) i.push(null);
return i;
},
_parseGlyf: function(h, e) {
var t = m.B, r = h._data, i = h.loca;
if (i[e] == i[e + 1]) return null;
var s = m.findTable(r, "glyf", h._offset)[0] + i[e], a = {};
if (a.noc = t.readShort(r, s), s += 2, a.xMin = t.readShort(r, s), s += 2, a.yMin = t.readShort(r, s), s += 2, a.xMax = t.readShort(r, s), s += 2, a.yMax = t.readShort(r, s), s += 2, a.xMin >= a.xMax || a.yMin >= a.yMax) return null;
if (a.noc > 0) {
a.endPts = [];
for (var n = 0; n < a.noc; n++)
a.endPts.push(t.readUshort(r, s)), s += 2;
var l = t.readUshort(r, s);
if (s += 2, r.length - s < l) return null;
s += l;
var c = a.endPts[a.noc - 1] + 1;
a.flags = [];
for (var n = 0; n < c; n++) {
var u = r[s];
if (s++, a.flags.push(u), u & 8) {
var d = r[s];
s++;
for (var f = 0; f < d; f++)
a.flags.push(u), n++;
}
}
a.xs = [];
for (var n = 0; n < c; n++) {
var g = (a.flags[n] & 2) != 0, _ = (a.flags[n] & 16) != 0;
g ? (a.xs.push(_ ? r[s] : -r[s]), s++) : _ ? a.xs.push(0) : (a.xs.push(t.readShort(r, s)), s += 2);
}
a.ys = [];
for (var n = 0; n < c; n++) {
var g = (a.flags[n] & 4) != 0, _ = (a.flags[n] & 32) != 0;
g ? (a.ys.push(_ ? r[s] : -r[s]), s++) : _ ? a.ys.push(0) : (a.ys.push(t.readShort(r, s)), s += 2);
}
for (var p = 0, C = 0, n = 0; n < c; n++)
p += a.xs[n], C += a.ys[n], a.xs[n] = p, a.ys[n] = C;
} else
a.parts = [];
return a;
}
};
typeof module < "u" && module.exports ? module.exports = m : typeof window < "u" && (window.Typr = m);
class me {
/**
* Extracts all available characters from a font's cmap tables.
* @param font The parsed font object from Typr
* @returns Array of unique character strings
*/
extractCharacters(e) {
var r;
const t = [];
return (r = e == null ? void 0 : e.cmap) != null && r.tables ? (e.cmap.tables.forEach((i) => {
if (i.format === 4) {
const s = this._extractCharactersFromFormat4Table(i);
t.push(...s);
} else if (i.format === 12) {
const s = this._extractCharactersFromFormat12Table(i);
t.push(...s);
}
}), [...new Set(t)]) : [];
}
/**
* Extracts characters from a Format 4 cmap table (Basic Multilingual Plane).
* @param table The Format 4 cmap table
* @returns Array of character strings
*/
_extractCharactersFromFormat4Table(e) {
const t = [];
if (!e.startCount || !e.endCount || !e.idRangeOffset || !e.idDelta)
return t;
for (let r = 0; r < e.startCount.length; r++) {
const i = e.startCount[r], s = e.endCount[r];
if (!(i === 65535 && s === 65535)) {
for (let a = i; a <= s; a++)
if (this._calculateGlyphIndexFormat4(e, a, r) > 0) {
const l = String.fromCodePoint(a);
t.push(l);
}
}
}
return t;
}
/**
* Extracts characters from a Format 12 cmap table (Extended Unicode ranges).
* @param table The Format 12 cmap table
* @returns Array of character strings
*/
_extractCharactersFromFormat12Table(e) {
const t = [];
if (!e.groups)
return t;
for (let r = 0; r < e.groups.length; r += 3) {
const i = e.groups[r], s = e.groups[r + 1], a = e.groups[r + 2];
for (let n = i; n <= s; n++)
if (a + (n - i) > 0) {
const c = String.fromCodePoint(n);
t.push(c);
}
}
return t;
}
/**
* Calculates the glyph index for a character in a Format 4 cmap table.
* @param table The Format 4 cmap table
* @param codePoint The Unicode code point
* @param rangeIndex The index of the character range
* @returns The glyph index, or 0 if not found
*/
_calculateGlyphIndexFormat4(e, t, r) {
if (e.idRangeOffset[r] === 0)
return t + e.idDelta[r] & 65535;
{
const i = e.idRangeOffset[r] / 2 + (t - e.startCount[r]) - (e.startCount.length - r);
if (i >= 0 && e.glyphIdArray && i < e.glyphIdArray.length) {
const s = e.glyphIdArray[i];
if (s !== 0)
return s + e.idDelta[r] & 65535;
}
}
return 0;
}
/**
* Filters out problematic characters that might cause rendering issues.
* @param characters Array of character strings to filter
* @returns Filtered array of character strings
*/
filterProblematicCharacters(e) {
return e.filter((t) => this._isValidCharacter(t));
}
/**
* Checks if a character is valid for rendering.
* @param char The character to check
* @returns True if the character is valid, false otherwise
*/
_isValidCharacter(e) {
const t = e.codePointAt(0) || 0;
return !(t >= 0 && t <= 31 && t !== 9 && t !== 10 && t !== 13 || t >= 127 && t <= 159);
}
}
class ge {
/**
* Creates a new TextureAtlasCreation instance.
* @param renderer The WebGL renderer instance
*/
constructor(e) {
o(this, "_textureCanvas");
o(this, "_textureContext");
o(this, "_renderer");
this._renderer = e, this._textureCanvas = document.createElement("canvas"), this._textureContext = this._textureCanvas.getContext("2d", { willReadFrequently: !0, alpha: !1 });
}
/**
* Creates a texture atlas from the given characters.
* @param characters Array of TextmodeCharacter objects
* @param maxGlyphDimensions Maximum dimensions of glyphs
* @param fontSize Font size for rendering
* @param fontFamilyName Font family name to use
* @returns Object containing framebuffer, columns, and rows
*/
createTextureAtlas(e, t, r, i) {
const s = e.length, a = Math.ceil(Math.sqrt(s)), n = Math.ceil(s / a), l = t.width * a, c = t.height * n;
this._setupCanvas(l, c, r, i), this._renderCharactersToCanvas(e, t, a, r), this._applyBlackWhiteThreshold();
const u = this._renderer.createFramebuffer(l, c, { filter: "nearest" });
return u.update(this._textureCanvas), {
framebuffer: u,
columns: a,
rows: n
};
}
/**
* Sets up the canvas for rendering.
* @param width Canvas buffer width
* @param height Canvas buffer height
* @param fontSize Font size
* @param fontFamilyName Font family name
* @param logicalWidth Logical width for scaling context
* @param logicalHeight Logical height for scaling context
*/
_setupCanvas(e, t, r, i) {
this._textureCanvas.width = e, this._textureCanvas.height = t, this._textureCanvas.style.width = e + "px", this._textureCanvas.style.height = e + "px", this._textureContext.imageSmoothingEnabled = !1, this._textureCanvas.style.imageRendering = "pixelated", this._textureContext.fillStyle = "black", this._textureContext.fillRect(0, 0, e, t), this._textureContext.font = `${r}px ${i}`, this._textureContext.textBaseline = "top", this._textureContext.textAlign = "left", this._textureContext.fillStyle = "white";
}
/**
* Renders all characters to the canvas in a grid layout.
* @param characters Array of characters to render
* @param maxGlyphDimensions Maximum glyph dimensions
* @param textureColumns Number of columns in the texture
* @param fontSize Font size
*/
_renderCharactersToCanvas(e, t, r, i) {
for (let s = 0; s < e.length; s++) {
const a = s % r, n = Math.floor(s / r), l = a * t.width + t.width * 0.5, c = n * t.height + t.height * 0.5, u = Math.round(l - t.width * 0.5), d = Math.round(c - i * 0.5);
this._textureContext.fillText(e[s].character, u, d);
}
}
/**
* Applies a black and white threshold filter to the canvas.
* This converts antialiased grayscale pixels to pure black or white,
* ensuring crisp text rendering suitable for NEAREST texture filtering.
* @param threshold Threshold value (0-255) for black/white conversion
*/
_applyBlackWhiteThreshold(e = 128) {
const t = this._textureContext.getImageData(0, 0, this._textureCanvas.width, this._textureCanvas.height), r = t.data;
for (let i = 0; i < r.length; i += 4) {
const s = 0.299 * r[i] + 0.587 * r[i + 1] + 0.114 * r[i + 2], a = e + 32, n = s > a ? 255 : 0;
r[i] = n, r[i + 1] = n, r[i + 2] = n;
}
this._textureContext.putImageData(t, 0, 0);
}
}
class pe {
/**
* Creates a new MetricsCalculation instance.
*/
constructor() {
o(this, "_tempCanvas");
o(this, "_tempContext");
this._tempCanvas = document.createElement("canvas"), this._tempContext = this._tempCanvas.getContext("2d");
}
/**
* Calculates the maximum glyph dimensions for a given set of characters.
* @param characters Array of character strings
* @param fontSize Font size to use for measurement
* @param fontFamilyName Font family name
* @param fontFace FontFace object (optional, for validation)
* @returns Object containing width and height dimensions
*/
calculateMaxGlyphDimensions(e, t, r) {
this._tempContext.font = `${t}px ${r}`;
let i = 0, s = 0;
for (const a of e) {
const n = this._tempContext.measureText(a), l = n.width, c = n.actualBoundingBoxAscent + n.actualBoundingBoxDescent;
l > 0 && (i = Math.max(i, l), s = Math.max(s, c));
}
return {
width: Math.ceil(i),
height: Math.ceil(s)
};
}
}
class _e {
/**
* Creates TextmodeCharacter objects with unique color assignments.
* @param characters Array of character strings
* @param font The parsed font object from Typr
* @returns Array of TextmodeCharacter objects with colors
*/
createCharacterObjects(e, t) {
return e.map((r, i) => {
const s = r.codePointAt(0) || 0, a = this._generateCharacterColor(i);
let n = 0;
if (t.hmtx && t.hmtx.aWidth) {
const l = this._getGlyphIndex(t, s);
l > 0 && t.hmtx.aWidth[l] !== void 0 && (n = t.hmtx.aWidth[l]);
}
return {
character: r,
unicode: s,
color: a,
advanceWidth: n
};
});
}
/**
* Gets the glyph index for a given Unicode code point in a Typr.js font
* This is a simplified version for advance width lookup only
* @param fontData The Typr.js font data
* @param codePoint The Unicode code point to look up
* @returns The glyph index, or 0 if not found
*/
_getGlyphIndex(e, t) {
const r = e.cmap;
if (!r || !r.tables) return 0;
for (const i of r.tables)
if (i.format === 4) {
for (let s = 0; s < i.startCount.length; s++)
if (t >= i.startCount[s] && t <= i.endCount[s]) {
if (i.idRangeOffset[s] === 0)
return t + i.idDelta[s] & 65535;
{
const a = i.idRangeOffset[s] / 2 + (t - i.startCount[s]) - (i.startCount.length - s);
if (a >= 0 && a < i.glyphIdArray.length) {
const n = i.glyphIdArray[a];
if (n !== 0)
return n + i.idDelta[s] & 65535;
}
}
}
}
return 0;
}
/**
* Generates a unique RGB color for a character based on its index.
* @param index The index of the character
* @returns RGB color as a tuple [r, g, b]
*/
_generateCharacterColor(e) {
const t = e % 256, r = Math.floor(e / 256) % 256, i = Math.floor(e / 65536) % 256;
return [t, r, i];
}
/**
* Gets the color for a specific character.
* @param character The character to get the color for
* @param characters Array of TextmodeCharacter objects
* @returns RGB color as a tuple [r, g, b], or [0, 0, 0] if not found
*/
getCharacterColor(e, t) {
if (!x.validate(
typeof e == "string" && e.length === 1,
"Character must be a single character string.",
{ method: "getCharacterColor", providedValue: e }
))
return [0, 0, 0];
const r = t.find((i) => i.character === e);
return r ? r.color : [0, 0, 0];
}
/**
* Gets colors for multiple characters.
* @param characterString String of characters to get colors for
* @param characters Array of TextmodeCharacter objects
* @returns Array of RGB colors for each character
*/
getCharacterColors(e, t) {
return x.validate(
typeof e == "string" && e.length > 0,
"Characters must be a string with at least one character.",
{ method: "getCharacterColors", providedValue: e }
) ? e.split("").map((r) => this.getCharacterColor(r, t) || [0, 0, 0]) : [[0, 0, 0]];
}
}
class xe {
/**
* Creates a new TextmodeFont instance.
* @param renderer Renderer instance for texture creation
* @param fontSize Font size to use for the texture atlas
* @ignore
*/
constructor(e, t = 16) {
o(this, "_font");
o(this, "_characters", []);
o(this, "_fontFramebuffer");
o(this, "_fontSize", 16);
o(this, "_textureColumns", 0);
o(this, "_textureRows", 0);
o(this, "_maxGlyphDimensions", { width: 0, height: 0 });
o(this, "_fontFace");
o(this, "_fontFamilyName", "UrsaFont");
// Component classes
o(this, "_characterExtractor");
o(this, "_textureAtlas");
o(this, "_metricsCalculator");
o(this, "_characterColorMapper");
this._fontSize = t, this._characterExtractor = new me(), this._textureAtlas = new ge(e), this._metricsCalculator = new pe(), this._characterColorMapper = new _e();
}
/**
* Initializes the font manager by loading the font and creating the texture atlas.
* @param fontSource Optional URL to load a custom font. If not provided, uses embedded font (full builds only).
* @returns Promise that resolves when initialization is complete
* @ignore
*/
async initialize(e) {
let t;
if (e) {
const r = await fetch(e);
if (!r.ok)
throw new F(`Failed to load font file: ${r.status} ${r.statusText}`);
t = await r.arrayBuffer();
} else
throw new F("Embedded font not available. This appears to be a minified buil