asciiground
Version:
Library for creating animated ASCII canvas backgrounds supporting generative patterns and extensive configuration options.
839 lines (838 loc) • 37 kB
JavaScript
//#region \0@oxc-project+runtime@0.153.0/helpers/esm/typeof.js
function e(t) {
"@babel/helpers - typeof";
return e = typeof Symbol == "function" && typeof Symbol.iterator == "symbol" ? function(e) {
return typeof e;
} : function(e) {
return e && typeof Symbol == "function" && e.constructor === Symbol && e !== Symbol.prototype ? "symbol" : typeof e;
}, e(t);
}
//#endregion
//#region \0@oxc-project+runtime@0.153.0/helpers/esm/toPrimitive.js
function t(t, n) {
if (e(t) != "object" || !t) return t;
var r = t[Symbol.toPrimitive];
if (r !== void 0) {
var i = r.call(t, n || "default");
if (e(i) != "object") return i;
throw TypeError("@@toPrimitive must return a primitive value.");
}
return (n === "string" ? String : Number)(t);
}
//#endregion
//#region \0@oxc-project+runtime@0.153.0/helpers/esm/toPropertyKey.js
function n(n) {
var r = t(n, "string");
return e(r) == "symbol" ? r : r + "";
}
//#endregion
//#region \0@oxc-project+runtime@0.153.0/helpers/esm/defineProperty.js
function r(e, t, r) {
return (t = n(t)) in e ? Object.defineProperty(e, t, {
value: r,
enumerable: !0,
configurable: !0,
writable: !0
}) : e[t] = r, e;
}
//#endregion
//#region src/patterns/pattern.ts
var i = { characters: [
"█",
"▓",
"▒",
"░",
" "
] }, a = class {
get id() {
return this.constructor.ID;
}
get options() {
return this._options;
}
get isDirty() {
return this._isDirty;
}
set isDirty(e) {
this._isDirty = e;
}
constructor(e = {}) {
r(this, "_options", void 0), r(this, "_isDirty", !1), this._options = {
...i,
...e
};
}
setOptions(e) {
this._isDirty = this._hasOptionsChanged(e), this._options = {
...this._options,
...e
};
}
initialize(e) {}
destroy() {}
onMouseInteraction(e, t, n) {}
_hasOptionsChanged(e) {
return Object.keys(e).some((t) => this._options[t] !== e[t]);
}
};
r(a, "ID", void 0);
//#endregion
//#region src/patterns/dummy-pattern.ts
var o = class extends a {
constructor(e = {}) {
super(e);
}
update(e) {
return this;
}
generate(e) {
return [];
}
};
r(o, "ID", "dummy");
//#endregion
//#region src/rendering/webgl/glyph-atlas.ts
var s = class {
constructor(e, t, n) {
r(this, "size", void 0), r(this, "_gl", void 0), r(this, "_canvas", void 0), r(this, "_context", void 0), r(this, "_pages", []), r(this, "_glyphs", /* @__PURE__ */ new Map()), this._gl = e, this._canvas = t, this._context = n, this.size = Math.min(1024, e.getParameter(e.MAX_TEXTURE_SIZE));
}
getFirstPage() {
return this._pages[0] ?? this._createPage();
}
destroy() {
this.reset(), this._canvas.width = this._canvas.height = 1;
}
reset() {
this._pages.forEach((e) => this._gl.deleteTexture(e.texture)), this._pages = [], this._glyphs.clear();
}
getGlyph(e, t, n) {
let r = this._glyphs.get(e);
if (r) return r;
let i = this._context;
i.font = `${t}px ${n}`, i.textBaseline = "top";
let a = i.measureText(e), o = Math.floor(-(a.actualBoundingBoxLeft ?? 0)) - 1, s = Math.floor(-(a.actualBoundingBoxAscent ?? 0)) - 1, c = Math.max(1, Math.ceil(a.actualBoundingBoxRight ?? a.width) - o + 1), l = Math.max(1, Math.ceil(a.actualBoundingBoxDescent ?? t) - s + 1);
if (c > this.size || l > this.size) throw Error("Character exceeds the WebGL glyph atlas size.");
let u = this._pages.at(-1) ?? this._createPage();
u.x + c > this.size && (u.x = 0, u.y += u.rowHeight, u.rowHeight = 0), u.y + l > this.size && (u = this._createPage());
let d = {
page: u,
x: u.x,
y: u.y,
width: c,
height: l,
left: o,
top: s
};
u.x += c, u.rowHeight = Math.max(u.rowHeight, l), this._canvas.width = c, this._canvas.height = l, i.font = `${t}px ${n}`, i.textBaseline = "top", i.fillStyle = "#ffffff", i.fillText(e, -o, -s);
let f = this._gl;
return f.bindTexture(f.TEXTURE_2D, u.texture), f.texSubImage2D(f.TEXTURE_2D, 0, d.x, d.y, f.RGBA, f.UNSIGNED_BYTE, this._canvas), this._glyphs.set(e, d), d;
}
_createPage() {
let e = this._gl, t = e.createTexture();
if (!t) throw Error("Could not create WebGL glyph atlas.");
let n = {
texture: t,
x: 0,
y: 0,
rowHeight: 0
};
return this._pages.push(n), e.bindTexture(e.TEXTURE_2D, t), e.texImage2D(e.TEXTURE_2D, 0, e.RGBA, this.size, this.size, 0, e.RGBA, e.UNSIGNED_BYTE, null), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_MIN_FILTER, e.LINEAR), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_MAG_FILTER, e.LINEAR), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_WRAP_S, e.CLAMP_TO_EDGE), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_WRAP_T, e.CLAMP_TO_EDGE), n;
}
}, c = class {
constructor(e, t) {
r(this, "_canvas", void 0), r(this, "_context", void 0), r(this, "_colors", /* @__PURE__ */ new Map()), this._canvas = e, this._context = t;
}
destroy() {
this._colors.clear(), this._canvas.width = this._canvas.height = 1;
}
parse(e) {
let t = this._colors.get(e);
if (t) return t;
let n = this._context;
n.clearRect(0, 0, 1, 1), n.fillStyle = "#000000", n.fillStyle = e, n.fillRect(0, 0, 1, 1);
let r = n.getImageData(0, 0, 1, 1).data, i = [
r[0] / 255,
r[1] / 255,
r[2] / 255,
r[3] / 255
];
return this._colors.size >= 1024 && this._colors.clear(), this._colors.set(e, i), i;
}
}, l = "#version 300 es\n\nprecision highp float;\nin vec4 a_rect;\nin vec4 a_uv;\nin vec4 a_color;\nin vec4 a_transform;\nuniform vec2 u_resolution;\nout vec2 v_uv;\nout vec4 v_color;\n\nconst vec2 corners[6] = vec2[6](\n vec2(0, 0), vec2(1, 0), vec2(0, 1),\n vec2(0, 1), vec2(1, 0), vec2(1, 1)\n);\n\nvoid main() {\n vec2 corner = corners[gl_VertexID];\n vec2 local = a_rect.xy + corner * a_rect.zw - a_transform.xy;\n\n vec2 position = a_transform.xy + vec2(\n local.x * a_transform.z - local.y * a_transform.w,\n local.x * a_transform.w + local.y * a_transform.z\n );\n\n gl_Position = vec4((position / u_resolution * 2.0 - 1.0) * vec2(1, -1), 0, 1);\n v_uv = a_uv.xy + corner * a_uv.zw;\n v_color = a_color;\n}\n", u = "#version 300 es\n\nprecision highp float;\nin vec2 v_uv;\nin vec4 v_color;\nuniform sampler2D u_texture;\nuniform bool u_solid;\nuniform vec4 u_clip;\nout vec4 fragColor;\n\nvoid main() {\n float coverage = u_solid ? 1.0 : texture(u_texture, v_uv).a;\n\n vec2 clippedSize = clamp(\n min(gl_FragCoord.xy + 0.5, u_clip.zw) - max(gl_FragCoord.xy - 0.5, u_clip.xy),\n 0.0, 1.0\n );\n\n fragColor = vec4(v_color.rgb, v_color.a * coverage * clippedSize.x * clippedSize.y);\n}\n", d = 4096, f = class {
constructor(e, t) {
r(this, "instances", new Float32Array(d * 16)), r(this, "_gl", void 0), r(this, "_canvas", void 0), r(this, "_program", null), r(this, "_buffer", null), r(this, "_vertexArray", null), r(this, "_shaders", []), r(this, "_resolution", null), r(this, "_solid", null), r(this, "_clip", null), this._gl = e, this._canvas = t;
}
destroy() {
let e = this._gl;
this._shaders.forEach((t) => e.deleteShader(t)), e.deleteBuffer(this._buffer), e.deleteVertexArray(this._vertexArray), e.deleteProgram(this._program), this._buffer = this._vertexArray = this._program = null, this._resolution = this._solid = this._clip = null, this._shaders = [];
}
setClip(e, t, n, r) {
this._gl.uniform4f(this._clip, e, t, n, r);
}
initialize() {
let e = this._gl, t = this._createShader(e.VERTEX_SHADER, l), n = this._createShader(e.FRAGMENT_SHADER, u);
if (this._program = e.createProgram(), !this._program) throw Error("Could not create WebGL program.");
if (e.attachShader(this._program, t), e.attachShader(this._program, n), e.linkProgram(this._program), !e.getProgramParameter(this._program, e.LINK_STATUS)) throw Error("Failed to link WebGL program: " + e.getProgramInfoLog(this._program));
if (this._shaders.forEach((t) => e.deleteShader(t)), this._shaders = [], this._buffer = e.createBuffer(), this._vertexArray = e.createVertexArray(), !this._buffer || !this._vertexArray) throw Error("Could not create WebGL character buffers.");
e.bindVertexArray(this._vertexArray), e.bindBuffer(e.ARRAY_BUFFER, this._buffer), e.bufferData(e.ARRAY_BUFFER, this.instances.byteLength, e.DYNAMIC_DRAW);
for (let [t, n] of [
"a_rect",
"a_uv",
"a_color",
"a_transform"
].entries()) {
let r = e.getAttribLocation(this._program, n);
e.enableVertexAttribArray(r), e.vertexAttribPointer(r, 4, e.FLOAT, !1, 64, t * 16), e.vertexAttribDivisor(r, 1);
}
this._resolution = e.getUniformLocation(this._program, "u_resolution"), this._solid = e.getUniformLocation(this._program, "u_solid"), this._clip = e.getUniformLocation(this._program, "u_clip"), e.useProgram(this._program), e.uniform1i(e.getUniformLocation(this._program, "u_texture"), 0), e.enable(e.BLEND), e.blendFuncSeparate(e.SRC_ALPHA, e.ONE_MINUS_SRC_ALPHA, e.ONE, e.ONE_MINUS_SRC_ALPHA), e.pixelStorei(e.UNPACK_PREMULTIPLY_ALPHA_WEBGL, !1), e.pixelStorei(e.UNPACK_FLIP_Y_WEBGL, !1), e.activeTexture(e.TEXTURE0), e.viewport(0, 0, this._canvas.width, this._canvas.height);
}
prepareDraw(e) {
let t = this._gl;
t.useProgram(this._program), t.bindVertexArray(this._vertexArray), t.bindBuffer(t.ARRAY_BUFFER, this._buffer), t.uniform2f(this._resolution, this._canvas.width, this._canvas.height), t.uniform1i(this._solid, +!!e), t.uniform4f(this._clip, 0, 0, this._canvas.width, this._canvas.height);
}
drawBatch(e, t) {
if (t === 0) return;
let n = this._gl;
n.bindTexture(n.TEXTURE_2D, e.texture), n.bufferSubData(n.ARRAY_BUFFER, 0, this.instances.subarray(0, t * 16)), n.drawArraysInstanced(n.TRIANGLES, 0, 6, t);
}
_createShader(e, t) {
let n = this._gl, r = n.createShader(e);
if (!r) throw Error("Could not create WebGL shader.");
if (this._shaders.push(r), n.shaderSource(r, t), n.compileShader(r), !n.getShaderParameter(r, n.COMPILE_STATUS)) throw Error("Failed to compile WebGL shader: " + n.getShaderInfoLog(r));
return r;
}
}, p = class {
constructor() {
r(this, "_gl", null), r(this, "_canvas", void 0), r(this, "_options", void 0), r(this, "_atlas", null), r(this, "_colors", null), r(this, "_pipeline", null), r(this, "_initialized", !1);
}
get options() {
return this._options;
}
set options(e) {
let t = e.fontSize !== this._options.fontSize || e.fontFamily !== this._options.fontFamily;
this._options = e, t && this._atlas?.reset();
}
initialize(e, t) {
this.destroy(), this._canvas = e, this._options = t;
let n = e.getContext("webgl2", {
alpha: !0,
preserveDrawingBuffer: !0
});
if (!n) throw Error("Could not get WebGL2 context from canvas.");
this._gl = n;
try {
let t = document.createElement("canvas"), r = document.createElement("canvas");
r.width = r.height = 1;
let i = t.getContext("2d"), a = r.getContext("2d", { willReadFrequently: !0 });
if (!i || !a) throw Error("Could not get 2D context for WebGL glyphs and colors.");
this._atlas = new s(n, t, i), this._colors = new c(r, a), this._pipeline = new f(n, e), this._pipeline.initialize(), this._atlas.getFirstPage(), this._initialized = !0;
} catch (e) {
throw this.destroy(), e;
}
}
clear(e) {
if (!this._initialized || !this._gl) return;
let t = this._gl;
if (t.isContextLost()) throw Error("WebGL context was lost.");
let n = this._colors.parse(e);
t.disable(t.SCISSOR_TEST), n[3] === 1 ? (t.clearColor(...n), t.clear(t.COLOR_BUFFER_BIT)) : n[3] > 0 && (this._pipeline.prepareDraw(!0), this._pipeline.instances.set([
0,
0,
this._canvas.width,
this._canvas.height,
0,
0,
0,
0,
...n,
0,
0,
1,
0
]), this._pipeline.drawBatch(this._atlas.getFirstPage(), 1));
}
render(e, t) {
if (!this._initialized || !this._gl || e.length === 0) return;
let n = this._gl;
if (this._pipeline.prepareDraw(!1), t.startColumn !== 0 || t.startRow !== 0 || t.endColumn !== t.columns || t.endRow !== t.rows) {
let e = Math.max(0, t.startColumn * t.charSpacingX), r = Math.max(0, t.startRow * t.charSpacingY), i = Math.min(this._canvas.width, t.endColumn * t.charSpacingX), a = Math.min(this._canvas.height, t.endRow * t.charSpacingY);
n.enable(n.SCISSOR_TEST), n.scissor(Math.floor(e), this._canvas.height - Math.ceil(a), Math.max(0, Math.ceil(i) - Math.floor(e)), Math.max(0, Math.ceil(a) - Math.floor(r))), this._pipeline.setClip(e, this._canvas.height - a, i, this._canvas.height - r);
}
let r = 0, i = this._atlas.getFirstPage();
try {
for (let n of e) {
if (n.x < 0 || n.x >= t.canvasWidth || n.y < 0 || n.y >= t.canvasHeight) continue;
let e = this._atlas.getGlyph(n.char, this._options.fontSize, this._options.fontFamily);
(e.page !== i || r === 4096) && (this._pipeline.drawBatch(i, r), i = e.page, r = 0);
let a = this._colors.parse(n.color || this._options.colorMap[n.char] || this._options.color), o = this._options.enableOpacity === !1 ? 1 : n.opacity ?? 1, s = n.scale ?? 1, c = n.rotation ?? 0;
this._pipeline.instances.set([
n.x + e.left,
n.y + e.top,
e.width,
e.height,
e.x / this._atlas.size,
e.y / this._atlas.size,
e.width / this._atlas.size,
e.height / this._atlas.size,
a[0],
a[1],
a[2],
a[3] * o,
n.x + t.charWidth / 2,
n.y + t.charHeight / 2,
Math.cos(c) * s,
Math.sin(c) * s
], r * 16), r++;
}
this._pipeline.drawBatch(i, r);
} finally {
n.disable(n.SCISSOR_TEST);
}
}
resize(e, t) {
this._initialized && this._gl && (this._canvas.width = e, this._canvas.height = t, this._gl.viewport(0, 0, e, t), this._gl.disable(this._gl.SCISSOR_TEST), this._gl.clearColor(0, 0, 0, 0), this._gl.clear(this._gl.COLOR_BUFFER_BIT));
}
destroy() {
this._atlas?.destroy(), this._pipeline?.destroy(), this._colors?.destroy(), this._gl?.getExtension("WEBGL_lose_context")?.loseContext(), this._atlas = this._pipeline = this._colors = null, this._gl = null, this._initialized = !1;
}
}, m = class {
constructor() {
r(this, "_canvas", void 0), r(this, "_context", void 0), r(this, "_options", void 0);
}
get options() {
return this._options;
}
set options(e) {
this._options = e, this._setupContext();
}
initialize(e, t) {
this._canvas = e, this._options = t;
let n = e.getContext("2d");
if (!n) throw Error("Could not get 2D context from canvas");
this._context = n, this._setupContext();
}
clear(e) {
this._context.fillStyle = e, this._context.fillRect(0, 0, this._canvas.width, this._canvas.height), this._context.fillStyle = this._options.color;
}
render(e, t) {
let n = t.startColumn !== 0 || t.startRow !== 0 || t.endColumn !== t.columns || t.endRow !== t.rows;
n && (this._context.save(), this._context.beginPath(), this._context.rect(t.startColumn * t.charSpacingX, t.startRow * t.charSpacingY, (t.endColumn - t.startColumn) * t.charSpacingX, (t.endRow - t.startRow) * t.charSpacingY), this._context.clip());
let r = this._options.color, i = this._options.colorMap, a = r, o = 1;
this._context.fillStyle = r;
for (let n of e) {
if (n.x < 0 || n.x >= t.canvasWidth || n.y < 0 || n.y >= t.canvasHeight) continue;
let e = this._options.enableOpacity === !1 ? 1 : n.opacity ?? 1;
e !== o && (this._context.globalAlpha = e, o = e);
let s = n.color || i[n.char] || r;
s !== a && (this._context.fillStyle = s, a = s), n.scale !== void 0 || n.rotation !== void 0 ? (this._context.save(), this._context.translate(n.x + t.charWidth / 2, n.y + t.charHeight / 2), n.rotation !== void 0 && this._context.rotate(n.rotation), n.scale !== void 0 && this._context.scale(n.scale, n.scale), this._context.fillText(n.char, -t.charWidth / 2, -t.charHeight / 2), this._context.restore()) : this._context.fillText(n.char, n.x, n.y);
}
o !== 1 && (this._context.globalAlpha = 1), a !== r && (this._context.fillStyle = r), n && this._context.restore();
}
resize(e, t) {
this._canvas.width = e, this._canvas.height = t, this._setupContext();
}
destroy() {}
_setupContext() {
this._context.font = `${this._options.fontSize}px ${this._options.fontFamily}`, this._context.textBaseline = "top", this._context.fillStyle = this._options.color;
}
}, h = (e) => {
switch (e) {
case "WebGL": return new p();
case "2D": return new m();
default: throw Error("Unknown renderer type given!");
}
}, g = {
color: "#3e3e80ff",
colorMap: {},
enableOpacity: !0,
fontSize: 32,
characterDensity: 1,
fontFamily: "monospace",
backgroundColor: "#181818ff",
padding: 0,
rendererType: "2D",
enableMouseInteraction: !1,
animated: !1,
animationSpeed: 1,
charSpacingX: void 0,
charSpacingY: void 0
}, _ = () => ({
canvas: null,
renderer: null,
pattern: null,
region: null,
options: {
...g,
resizeTo: window
},
lastTime: 0,
animationId: null,
animationTime: 0,
mouseX: 0,
mouseY: 0,
mouseClicked: !1,
tempCanvas: null,
tempContext: null,
lastHash: 0,
isDirty: !0,
resizeObserver: null,
frameCount: 0,
lastFpsTime: 0,
currentFps: 0,
fpsUpdateInterval: 1e3
}), v = class {
get options() {
return this._state.options;
}
get renderInfo() {
return {
fps: this._state.currentFps,
rows: this._state.region?.rows || 0,
columns: this._state.region?.columns || 0,
frameCount: this._state.frameCount
};
}
get mouseInfo() {
return {
x: this._state.mouseX,
y: this._state.mouseY,
clicked: this._state.mouseClicked
};
}
get region() {
if (!this._state.region) throw Error("Renderer is not initialized! Make sure to call the constructor first.");
return this._state.region;
}
get pattern() {
if (!this._state.pattern) throw Error("Pattern not initialized");
return this._state.pattern;
}
set pattern(e) {
this._state.pattern && this._state.pattern.destroy(), this._state.pattern = e, this._state.region && this._state.pattern.initialize(this._state.region), this._resetAnimationTime();
}
get isAnimating() {
return this._state.options.animated;
}
get canvas() {
if (!this._state.canvas) throw Error("Canvas not initialized.");
return this._state.canvas;
}
get realCanvasSize() {
let e = this._state.options.resizeTo;
if (e instanceof HTMLElement) {
let t = window.getComputedStyle(e), n = parseFloat(t.paddingLeft) + parseFloat(t.paddingRight), r = parseFloat(t.paddingTop) + parseFloat(t.paddingBottom);
return [e.clientWidth - n, e.clientHeight - r];
}
return [e.innerWidth, e.innerHeight];
}
get renderer() {
if (!this._state.renderer) throw Error("Renderer not initialized.");
return this._state.renderer;
}
get tempContext() {
if (!this._state.tempCanvas && (this._state.tempCanvas = document.createElement("canvas"), this._state.tempContext = this._state.tempCanvas.getContext("2d", { alpha: !1 }), !this._state.tempContext)) throw Error("Failed to create 2D context for temp canvas");
if (!this._state.tempContext) throw Error("Temp context not initialized.");
return this._state.tempContext;
}
constructor({ canvas: e, pattern: t, options: n }) {
r(this, "_state", _()), r(this, "_handleResize", void 0), r(this, "_contextLostHandler", () => {
this._state.isDirty = !0, this.render();
}), r(this, "_mouseMoveHandler", (e) => {
let t = this.canvas.getBoundingClientRect(), n = this.canvas.width / this.region.columns, r = this.canvas.height / this.region.rows;
this._state.mouseX = Math.floor((e.clientX - t.left) / n), this._state.mouseY = Math.floor((e.clientY - t.top) / r);
}), r(this, "_mouseClickHandler", () => {
this._state.mouseClicked = !0;
}), n || (n = {}), this._state.canvas = e, this._state.pattern = t || new o(), this._handleResize = this.resize.bind(this), this._state.options = {
...g,
resizeTo: window,
...n
}, this._setupRenderer(), this._state.options.enableMouseInteraction && this._setupMouseEvents();
}
render(e = performance.now()) {
let t = e - this._state.lastTime;
this._state.lastTime = e, this._state.options.animated && (this._state.animationTime += t / 1e3 * this._state.options.animationSpeed);
let n = {
time: this._state.animationTime,
deltaTime: t / 1e3,
animationTime: this._state.animationTime,
region: this.region,
mouseX: this._state.mouseX,
mouseY: this._state.mouseY,
clicked: this._state.mouseClicked,
isAnimating: this._state.options.animated,
animationSpeed: this._state.options.animationSpeed
};
this._state.mouseClicked = !1;
let r = this.pattern.update(n).generate(n), i = this._hash(r);
if (i !== this._state.lastHash || this._state.isDirty || this.pattern.isDirty) {
this._updateFps(e), this.pattern.isDirty = !1, this._state.isDirty = !1, this._state.lastHash = i;
try {
this.renderer.clear(this._state.options.backgroundColor), this.renderer.render(r, this.region);
} catch (e) {
if (this._state.options.rendererType !== "WebGL") throw e;
this._fallbackToCanvas(e), this.renderer.clear(this._state.options.backgroundColor), this.renderer.render(r, this.region);
}
}
}
startAnimation() {
if (this._state.animationId) throw Error("Animation is already running!");
this._state.options.animated = !0, this._state.lastTime = performance.now(), this._state.frameCount = 0, this._state.lastFpsTime = 0, this._state.currentFps = 0;
let e = (t) => {
this.isAnimating && (this.render(t), this._state.animationId = requestAnimationFrame(e));
};
this._state.animationId = requestAnimationFrame(e);
}
stopAnimation() {
this._state.options.animated = !1, this._state.animationId !== null && (cancelAnimationFrame(this._state.animationId), this._state.animationId = null);
}
setOptions(e) {
let t = this._state.options;
this._state.options = {
...t,
...e
}, this._state.isDirty = this._state.isDirty || this._hasOptionsChanged(t), t.rendererType !== this._state.options.rendererType && (this._removeMouseEvents(), this.renderer.destroy(), this._replaceCanvas(), this._initializeBackend()), this._state.region = this._calculateRegion(), this.pattern.initialize(this._state.region), this.renderer.options = this._state.options, this._syncAnimationState(), this._state.options.enableMouseInteraction ? (this._removeMouseEvents(), this._setupMouseEvents()) : this._removeMouseEvents();
}
resize() {
[this.canvas.width, this.canvas.height] = this.realCanvasSize, this._state.region = this._calculateRegion(), this.renderer.resize(this.canvas.width, this.canvas.height), this.pattern.initialize(this._state.region), this._state.isDirty = !0, this.isAnimating || this.render();
}
destroy() {
this.stopAnimation(), this._state.pattern?.destroy(), this._state.canvas?.removeEventListener("webglcontextlost", this._contextLostHandler), this._state.renderer?.destroy(), this._state.canvas?.removeEventListener("mousemove", this._mouseMoveHandler), this._state.canvas?.removeEventListener("click", this._mouseClickHandler), this._state.options.resizeTo.removeEventListener("resize", this._handleResize), this._state.resizeObserver?.disconnect(), this._state = _();
}
_calculateSpacing() {
let e = 0;
for (let t of this.pattern.options.characters) {
let n = this.tempContext.measureText(t);
e = Math.max(e, n.width);
}
let t = e, n = this.tempContext.measureText(this.pattern.options.characters.join("")), r = n.actualBoundingBoxAscent + n.actualBoundingBoxDescent, i = Math.max(r, this._state.options.fontSize), a = this._state.options.charSpacingX && this._state.options.charSpacingX > 0 ? this._state.options.charSpacingX : t, o = this._state.options.charSpacingY && this._state.options.charSpacingY > 0 ? this._state.options.charSpacingY : Math.max(i, this._state.options.fontSize * 1.2), s = this._state.options.characterDensity ?? 1, c = Number.isFinite(s) && s > 0 ? Math.sqrt(s) : 1;
return [
t,
i,
a / c,
o / c
];
}
_calculateRegion() {
this.tempContext.font = `${this._state.options.fontSize}px ${this._state.options.fontFamily}`;
let [e, t, n, r] = this._calculateSpacing(), i = Math.floor(this.canvas.width / n), a = Math.floor(this.canvas.height / r);
return {
startColumn: this._state.options.padding,
endColumn: i - this._state.options.padding,
startRow: this._state.options.padding,
endRow: a - this._state.options.padding,
columns: i,
rows: a,
charWidth: e,
charHeight: t,
charSpacingX: n,
charSpacingY: r,
canvasWidth: this.canvas.width,
canvasHeight: this.canvas.height
};
}
_setupRenderer() {
[this.canvas.width, this.canvas.height] = this.realCanvasSize, this._initializeBackend(), this._state.region = this._calculateRegion(), this.pattern.initialize(this.region), this._state.options.resizeTo instanceof HTMLElement ? (this._state.resizeObserver = new ResizeObserver(this._handleResize), this._state.resizeObserver.observe(this._state.options.resizeTo)) : this._state.options.resizeTo.addEventListener("resize", this._handleResize);
}
_replaceCanvas() {
let e = this.canvas;
e.removeEventListener("webglcontextlost", this._contextLostHandler);
let t = e.cloneNode(!0);
e.replaceWith(t), this._state.canvas = t, this._state.options.resizeTo === e && (this._state.options = {
...this._state.options,
resizeTo: t
}, this._state.resizeObserver?.disconnect(), this._state.resizeObserver?.observe(t));
}
_initializeBackend() {
this._state.renderer = h(this._state.options.rendererType);
try {
this.renderer.initialize(this.canvas, this._state.options);
} catch (e) {
if (this._state.options.rendererType !== "WebGL") throw this.renderer.destroy(), e;
this._fallbackToCanvas(e);
}
this._state.options.rendererType === "WebGL" && this.canvas.addEventListener("webglcontextlost", this._contextLostHandler);
}
_fallbackToCanvas(e) {
console.warn("WebGL failed, falling back to Canvas 2D:", e), this._removeMouseEvents(), this.renderer.destroy(), this._replaceCanvas(), this._state.options = {
...this._state.options,
rendererType: "2D"
}, this._state.renderer = h("2D"), this.renderer.initialize(this.canvas, this._state.options), this._state.options.enableMouseInteraction && this._setupMouseEvents();
}
_setupMouseEvents() {
this.canvas.addEventListener("mousemove", this._mouseMoveHandler), this.canvas.addEventListener("click", this._mouseClickHandler);
}
_removeMouseEvents() {
this.canvas.removeEventListener("mousemove", this._mouseMoveHandler), this.canvas.removeEventListener("click", this._mouseClickHandler);
}
_resetAnimationTime() {
this._state.animationTime = 0;
}
_syncAnimationState() {
this._state.options.animated && this._state.animationId === null ? this.startAnimation() : !this._state.options.animated && this._state.animationId !== null && this.stopAnimation();
}
_hash(e) {
let t = 0;
for (let { x: n, y: r, char: i, color: a = "", opacity: o = 1, scale: s = 1, rotation: c = 0 } of e) t ^= (n * 31 + r * 17 ^ i.charCodeAt(0)) + (a.charCodeAt(0) || 0) * 13 + Math.floor(o * 100) * 7 + Math.floor(s * 100) * 5 + Math.floor(c * 100) * 3;
return t;
}
_hasOptionsChanged(e) {
return Object.keys(e).some((t) => this._state.options[t] !== e[t]);
}
_updateFps(e) {
if (this._state.frameCount++, this._state.lastFpsTime === 0) {
this._state.lastFpsTime = e;
return;
}
let t = e - this._state.lastFpsTime;
t >= this._state.fpsUpdateInterval && (this._state.currentFps = Math.round(this._state.frameCount * 1e3 / t), this._state.frameCount = 0, this._state.lastFpsTime = e);
}
}, y = (e) => {
let t = Math.floor(e) % 2147483647;
return t <= 0 && (t += 2147483646), () => (t = t * 16807 % 2147483647, t / 2147483647);
}, b = {
frequency: .01,
octaves: 4,
persistence: .5,
lacunarity: 2,
seed: 0
}, x = class extends a {
get _octaves() {
return this._options.octaves;
}
get _persistence() {
return this._options.persistence;
}
get _lacunarity() {
return this._options.lacunarity;
}
constructor(e = {}) {
super({
...b,
...e
}), r(this, "_permutations", void 0), this._permutations = this._generatePermutations(this._options.seed);
}
setOptions(e) {
let t = this._options.seed;
super.setOptions(e), e.seed !== void 0 && e.seed !== t && (this._permutations = this._generatePermutations(this._options.seed));
}
update(e) {
return this;
}
generate({ animationTime: e, region: t }) {
if (this.options.characters.length === 0) return [];
let n = [];
for (let r = t.startRow; r <= t.endRow; r++) for (let i = t.startColumn; i <= t.endColumn; i++) {
let a = this._fractalNoise(i * this._options.frequency, r * this._options.frequency, e * .001), o = Math.max(0, Math.min(1, (a + 1) / 2)), s = Math.floor(o * this._options.characters.length), c = Math.max(0, Math.min(s, this._options.characters.length - 1));
n.push({
char: this._options.characters[c],
x: i * t.charSpacingX,
y: r * t.charSpacingY,
opacity: o
});
}
return n;
}
_generatePermutations(e) {
let t = y(e), n = Array.from({ length: 256 }, (e, t) => t);
for (let e = 255; e > 0; e--) {
let r = Math.floor(t() * (e + 1)), i = n[e];
n[e] = n[r], n[r] = i;
}
return n.concat(n);
}
_fade(e) {
return e * e * e * (e * (e * 6 - 15) + 10);
}
_lerp(e, t, n) {
return e + n * (t - e);
}
_gradient3D(e, t, n, r) {
let i = e & 15, a = i < 8 ? t : n, o = i < 4 ? n : i === 12 || i === 14 ? t : r;
return (i & 1 ? -a : a) + (i & 2 ? -o : o);
}
_noise3D(e, t, n) {
let r = Math.floor(e) & 255, i = Math.floor(t) & 255, a = Math.floor(n) & 255;
e -= Math.floor(e), t -= Math.floor(t), n -= Math.floor(n);
let o = this._fade(e), s = this._fade(t), c = this._fade(n), l = this._permutations, u = l[r] + i, d = l[u] + a, f = l[u + 1] + a, p = l[r + 1] + i, m = l[p] + a, h = l[p + 1] + a;
return this._lerp(this._lerp(this._lerp(this._gradient3D(l[d], e, t, n), this._gradient3D(l[m], e - 1, t, n), o), this._lerp(this._gradient3D(l[f], e, t - 1, n), this._gradient3D(l[h], e - 1, t - 1, n), o), s), this._lerp(this._lerp(this._gradient3D(l[d + 1], e, t, n - 1), this._gradient3D(l[m + 1], e - 1, t, n - 1), o), this._lerp(this._gradient3D(l[f + 1], e, t - 1, n - 1), this._gradient3D(l[h + 1], e - 1, t - 1, n - 1), o), s), c);
}
_fractalNoise(e, t, n = 0) {
let r = 0, i = 0, a = 1, o = 1;
for (let s = 0; s < this._octaves; s++) r += this._noise3D(e * o, t * o, n * o) * a, i += a, a *= this._persistence, o *= this._lacunarity;
return r / i;
}
};
r(x, "ID", "perlin-noise");
//#endregion
//#region src/patterns/rain-pattern.ts
var S = {
rainDensity: .8,
minDropLength: 8,
maxDropLength: 25,
minSpeed: .5,
maxSpeed: 1.5,
mutationRate: .04,
fadeOpacity: .2,
headColor: "#FFFFFF"
}, C = class extends a {
constructor(e = {}) {
super({
...S,
...e
}), r(this, "_rainDrops", []), r(this, "_region", null), r(this, "_lastFrameCharacters", []);
}
initialize(e) {
let t = this._rainDrops.length > 0, n = this._region;
this._region = e, !t || !n || Math.abs(n.columns - e.columns) > 2 || Math.abs(n.rows - e.rows) > 2 ? (this._rainDrops = [], this._lastFrameCharacters = [], this._initializeRainDrops()) : (this._adjustDropsToNewRegion(n, e), this._maintainRainDensity());
}
update(e) {
return this._region && (this._updateRainDrops(e), this._maintainRainDensity()), this;
}
generate(e) {
if ((!this._region || this._rainDrops.length === 0) && (this._region && this._rainDrops.length === 0 && this._initializeRainDrops(), this._rainDrops.length === 0)) return [];
let t = [];
if (this._options.fadeOpacity > 0) for (let e of this._lastFrameCharacters) t.push({
...e,
opacity: this._options.fadeOpacity
});
for (let n of this._rainDrops) this._renderRainDrop(n, t, e);
return this._lastFrameCharacters = t.filter((e) => e.opacity !== this._options.fadeOpacity), t;
}
_initializeRainDrops() {
if (!this._region) return;
let e = Math.floor(this._region.columns * this._options.rainDensity), t = Math.max(1, e);
for (let e = 0; e < t; e++) {
let n = Math.floor(Math.random() * this._region.columns) + this._region.startColumn, r = this._createRainDrop(n, 0);
e < Math.max(1, Math.floor(t * .3)) && (r.y = Math.random() * this._region.rows), this._rainDrops.push(r);
}
}
_createRainDrop(e, t = 0) {
let n = Math.floor(Math.random() * (this._options.maxDropLength - this._options.minDropLength)) + this._options.minDropLength, r = Array.from({ length: n }, () => this._options.characters[Math.floor(Math.random() * this._options.characters.length)]);
return {
y: -Math.floor(Math.random() * n) - Math.random() * 10,
column: e,
length: n,
characters: r,
lastMutationTime: t,
speed: Math.random() * (this._options.maxSpeed - this._options.minSpeed) + this._options.minSpeed
};
}
_updateRainDrops(e) {
if (this._region) for (let t of this._rainDrops) {
if (e.isAnimating && (t.y += t.speed * e.animationSpeed * e.deltaTime), e.animationTime - t.lastMutationTime > 1 / this._options.mutationRate && Math.random() < this._options.mutationRate) {
let n = Math.floor(Math.random() * t.length);
t.characters[n] = this._options.characters[Math.floor(Math.random() * this._options.characters.length)], t.lastMutationTime = e.animationTime;
}
t.y - t.length > this._region.endRow && this._resetRainDrop(t, e.animationTime);
}
}
_resetRainDrop(e, t) {
this._region && (e.lastMutationTime = t, e.y = -Math.floor(Math.random() * 8) - e.length, e.length = Math.floor(Math.random() * (this._options.maxDropLength - this._options.minDropLength)) + this._options.minDropLength, e.characters = Array.from({ length: e.length }, () => this._options.characters[Math.floor(Math.random() * this._options.characters.length)]), e.speed = Math.random() * (this._options.maxSpeed - this._options.minSpeed) + this._options.minSpeed, e.column = Math.floor(Math.random() * this._region.columns) + this._region.startColumn);
}
_maintainRainDensity() {
if (!this._region) return;
let e = Math.floor(this._region.columns * this._options.rainDensity);
for (; this._rainDrops.length < e;) {
let e = this._rainDrops.length % this._region.columns + this._region.startColumn, t = this._createRainDrop(e, 0);
Math.random() < .4 && (t.y = Math.random() * this._region.rows + this._region.startRow), this._rainDrops.push(t);
}
this._rainDrops.length > e && (this._rainDrops.length = e);
}
_adjustDropsToNewRegion(e, t) {
for (let e of this._rainDrops) e.column >= t.startColumn + t.columns ? e.column = e.column % t.columns + t.startColumn : e.column < t.startColumn && (e.column = t.startColumn);
}
_renderRainDrop(e, t, n) {
if (this._region) for (let n = 0; n < e.length; n++) {
let r = Math.floor(e.y) - n;
if (r < this._region.startRow || r > this._region.endRow || e.column < this._region.startColumn || e.column > this._region.endColumn) continue;
let i = e.column * this._region.charSpacingX, a = r * this._region.charSpacingY, o, s = 1;
n === 0 ? o = this._options.headColor : n >= 3 && (s = 1 - n / e.length), t.push({
x: i,
y: a,
color: o,
opacity: s,
char: e.characters[n]
});
}
}
destroy() {
this._rainDrops = [], this._lastFrameCharacters = [], this._region = null;
}
setOptions(e) {
if (super.setOptions(e), e.rainDensity !== void 0 && this._region && this._maintainRainDensity(), e.characters !== void 0) for (let e of this._rainDrops) e.characters = Array.from({ length: e.length }, () => this._options.characters[Math.floor(Math.random() * this._options.characters.length)]);
}
};
r(C, "ID", "rain");
//#endregion
//#region src/patterns/static-noise-pattern.ts
var w = { seed: 0 }, T = class extends a {
constructor(e = {}) {
super({
...w,
...e
});
}
update(e) {
return this;
}
generate({ region: e, animationTime: t }) {
let n = [], r = y(this._options.seed + Math.floor(t * 10));
for (let t = e.startRow; t <= e.endRow; t++) for (let i = e.startColumn; i <= e.endColumn; i++) {
let a = Math.floor(r() * this._options.characters.length), o = this._options.characters[a] || " ";
n.push({
char: o,
x: i * e.charSpacingX,
y: t * e.charSpacingY
});
}
return n;
}
};
r(T, "ID", "static");
//#endregion
//#region src/index.ts
var E = class {
constructor() {
r(this, "_renderer", null);
}
get pattern() {
return this.renderer.pattern;
}
get options() {
return Object.freeze({ ...this.renderer.options });
}
get canvas() {
return this.renderer.canvas;
}
get renderer() {
if (!this._renderer) throw Error("Renderer is not initialized - call init() first.");
return this._renderer;
}
set renderer(e) {
this._renderer = e;
}
init(e, t, n) {
return this.renderer = new v({
canvas: e,
pattern: t,
options: n
}), this;
}
startAnimation() {
return this.renderer.startAnimation(), this;
}
stopAnimation() {
return this.renderer.stopAnimation(), this;
}
setPattern(e) {
return this.renderer.pattern = e, this;
}
setOptions(e) {
return this.renderer.setOptions(e), this;
}
destroy() {
this.renderer.destroy(), this.renderer = null;
}
};
//#endregion
export { E as ASCIIGround, E as default, v as ASCIIRenderer, m as Canvas2DRenderer, o as DummyPattern, a as Pattern, x as PerlinNoisePattern, C as RainPattern, T as StaticNoisePattern, p as WebGLRenderer, h as createRenderer, y as createSeededRandom };