UNPKG

asciiground

Version:

Library for creating animated ASCII canvas backgrounds supporting generative patterns and extensive configuration options.

938 lines (929 loc) 33 kB
var M = Object.defineProperty; var w = (c, e, t) => e in c ? M(c, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : c[e] = t; var h = (c, e, t) => w(c, typeof e != "symbol" ? e + "" : e, t); const R = { characters: ["█", "▓", "▒", "░", " "] }; class p { constructor(e = {}) { /** * Options for the pattern, initialized with default values. */ h(this, "_options"); /** * Flag indicating if the pattern needs to be re-rendered. * This is set to true when pattern options change in a way that required re-render (e.g. color change). */ h(this, "_isDirty", !1); this._options = { ...R, ...e }; } get id() { return this.constructor.ID; } get options() { return this._options; } get isDirty() { return this._isDirty; } set isDirty(e) { this._isDirty = e; } /** * Update pattern options without recreating the pattern instance. * Override this method if your pattern has expensive initialization that should be preserved. * @param newOptions - partial options to update */ setOptions(e) { this._isDirty = this._hasOptionsChanged(e), this._options = { ...this._options, ...e }; } /** * Called when the pattern is initialized or resized. * Use this to set up any internal state or precompute values. * @param _region - the rendering region including visible area and padding. */ initialize(e) { } /** * Called when the pattern is destroyed. * Use this to clean up resources, cancel timers, etc. */ destroy() { } /** * Handle mouse interactions with the pattern. * Override to implement custom mouse effects. * @param _x - mouse X position relative to canvas. * @param _y - mouse Y position relative to canvas. * @param _clicked - Whether mouse was clicked this frame. */ onMouseInteraction(e, t, i) { } /** * Check if the pattern options have changed. * @param options - new options to compare against current options. * @returns True if any options have changed, false otherwise. */ _hasOptionsChanged(e) { return Object.keys(e).some((t) => { const i = this._options[t], s = e[t]; return i !== s; }); } } /** * Unique identifier for the pattern, that should be overridden in subclasses. */ h(p, "ID"); class D extends p { constructor(e = {}) { super(e); } update(e) { return this; } generate(e) { return []; } } h(D, "ID", "dummy"); class d { constructor() { h(this, "_canvas"); h(this, "_context"); h(this, "_options"); } get options() { return this._options; } set options(e) { this._options = e, this._setupContext(); } initialize(e, t) { this._canvas = e, this._options = t; const i = e.getContext("2d"); if (!i) throw new Error("Could not get 2D context from canvas"); this._context = i, 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) { const i = t.startColumn !== 0 || t.startRow !== 0 || t.endColumn !== t.columns || t.endRow !== t.rows; i && (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()); for (const s of e) s.x < 0 || s.x >= t.canvasWidth || s.y < 0 || s.y >= t.canvasHeight || (s.opacity !== void 0 && (this._context.globalAlpha = s.opacity), s.color && (this._context.fillStyle = s.color), s.scale !== void 0 || s.rotation !== void 0 ? (this._context.save(), this._context.translate(s.x + t.charWidth / 2, s.y + t.charHeight / 2), s.rotation !== void 0 && this._context.rotate(s.rotation), s.scale !== void 0 && this._context.scale(s.scale, s.scale), this._context.fillText(s.char, -t.charWidth / 2, -t.charHeight / 2), this._context.restore()) : this._context.fillText(s.char, s.x, s.y), s.opacity !== void 0 && (this._context.globalAlpha = 1), s.color && (this._context.fillStyle = this._options.color)); i && 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; } } class T { constructor() { h(this, "_gl"); h(this, "_canvas"); h(this, "_program"); h(this, "_options"); h(this, "_isInitialized", !1); } get options() { return this._options; } set options(e) { this._options = e; } initialize(e) { this._canvas = e; const t = e.getContext("webgl2"); if (!t) throw new Error("Could not get WebGL2 context from canvas."); this._gl = t, this._setupWebGL(), this._isInitialized = !0; } clear(e) { if (!this._isInitialized) return; const t = this._gl, i = e.replace("#", ""), s = parseInt(i.substring(0, 2), 16) / 255, n = parseInt(i.substring(2, 4), 16) / 255, a = parseInt(i.substring(4, 6), 16) / 255; t.clearColor(s, n, a, 1), t.clear(t.COLOR_BUFFER_BIT); } render(e, t) { this._isInitialized; } resize(e, t) { this._isInitialized && (this._canvas.width = e, this._canvas.height = t, this._gl.viewport(0, 0, e, t)); } destroy() { if (!this._isInitialized) return; const e = this._gl; this._program && e.deleteProgram(this._program), this._isInitialized = !1; } _setupWebGL() { const e = this._gl, t = `#version 300 es precision mediump float; in vec2 a_position; in vec2 a_texCoord; in float a_opacity; uniform vec2 u_resolution; uniform mat3 u_transform; out vec2 v_texCoord; out float v_opacity; void main() { vec3 position = u_transform * vec3(a_position, 1.0); vec2 clipSpace = ((position.xy / u_resolution) * 2.0 - 1.0) * vec2(1, -1); gl_Position = vec4(clipSpace, 0, 1); v_texCoord = a_texCoord; v_opacity = a_opacity; } `, i = `#version 300 es precision mediump float; in vec2 v_texCoord; in float v_opacity; uniform sampler2D u_texture; uniform vec3 u_color; out vec4 fragColor; void main() { float alpha = texture(u_texture, v_texCoord).r; fragColor = vec4(u_color, alpha * v_opacity); } `, s = this._createShader(e.VERTEX_SHADER, t), n = this._createShader(e.FRAGMENT_SHADER, i); if (this._program = e.createProgram(), e.attachShader(this._program, s), e.attachShader(this._program, n), e.linkProgram(this._program), !e.getProgramParameter(this._program, e.LINK_STATUS)) throw new Error("Failed to link WebGL program: " + e.getProgramInfoLog(this._program)); e.enable(e.BLEND), e.blendFunc(e.SRC_ALPHA, e.ONE_MINUS_SRC_ALPHA); } _createShader(e, t) { const i = this._gl, s = i.createShader(e); if (i.shaderSource(s, t), i.compileShader(s), !i.getShaderParameter(s, i.COMPILE_STATUS)) { const n = i.getShaderInfoLog(s); throw i.deleteShader(s), new Error(`Failed to compile shader: ${n}`); } return s; } } const I = (c) => { switch (c) { case "WebGL": if (typeof WebGL2RenderingContext < "u") try { return new T(); } catch { return new d(); } else return new d(); case "2D": return new d(); default: throw new Error("Unknown renderer type given!"); } }, x = { color: "#3e3e80ff", fontSize: 32, fontFamily: "monospace", backgroundColor: "#181818ff", padding: 0, rendererType: "2D", enableMouseInteraction: !1, animated: !1, animationSpeed: 1, charSpacingX: void 0, charSpacingY: void 0, resizeTo: window }, C = () => ({ canvas: null, renderer: null, pattern: null, region: null, options: x, lastTime: 0, animationId: null, animationTime: 0, mouseX: 0, mouseY: 0, mouseClicked: !1, tempCanvas: null, tempContext: null, lastHash: 0, isDirty: !0, resizeObserver: null }); class A { /** * Create a new ASCIIRenderer instance. * @param options - the renderer constructor parameters. */ constructor({ canvas: e, pattern: t, options: i }) { h(this, "_state", C()); h(this, "_handleResize"); /** * Handle mouse move events to update mouse position. * @param event Mouse event to handle. */ h(this, "_mouseMoveHandler", (e) => { const t = this.canvas.getBoundingClientRect(); this._state.mouseX = e.clientX - t.left, this._state.mouseY = e.clientY - t.top; }); /** * Handle mouse click events to update clicked state. * This can be used by patterns to respond to user input. */ h(this, "_mouseClickHandler", () => { this._state.mouseClicked = !0; }); i || (i = {}), this._state.canvas = e, this._state.pattern = t || new D(), this._handleResize = this.resize.bind(this), this._state.options = { ...x, ...i }, this._state.renderer = I(this._state.options.rendererType || "2D"), this._state.region = this._calculateRegion(), this._setupRenderer(), this._state.options.enableMouseInteraction && this._setupMouseEvents(); } /** * Get the current rendering options. * @returns The current ASCIIRendererOptions. */ get options() { return this._state.options; } /** Get the current pattern generator. */ get pattern() { if (!this._state.pattern) throw new Error("Pattern not initialized"); return this._state.pattern; } /** Set a new pattern generator for the renderer. */ 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(); } /** Whether the renderer is currently animating. */ get isAnimating() { return this._state.options.animated; } /** * Get the canvas element, throwing an error if not initialized. */ get canvas() { if (!this._state.canvas) throw new Error("Canvas not initialized."); return this._state.canvas; } /** * Get the real canvas size, accounting for padding and `resizeTo` target size. * This is used to ensure the canvas is sized correctly for rendering. */ get realCanvasSize() { const e = this._state.options.resizeTo; if (e instanceof HTMLElement) { const t = window.getComputedStyle(e), i = parseFloat(t.paddingLeft) + parseFloat(t.paddingRight), s = parseFloat(t.paddingTop) + parseFloat(t.paddingBottom); return [e.clientWidth - i, e.clientHeight - s]; } return [e.innerWidth, e.innerHeight]; } /** * Get the renderer, throwing an error if not initialized. */ get renderer() { if (!this._state.renderer) throw new Error("Renderer not initialized."); return this._state.renderer; } /** * Get the region, throwing an error if not calculated. */ get region() { if (!this._state.region) throw new Error("Region not calculated."); return this._state.region; } /** * Get the temp context, ensuring it's initialized. */ get tempContext() { if (!this._state.tempCanvas && (this._state.tempCanvas = document.createElement("canvas"), this._state.tempContext = this._state.tempCanvas.getContext("2d"), !this._state.tempContext)) throw new Error("Failed to create 2D context for temp canvas"); if (!this._state.tempContext) throw new Error("Temp context not initialized."); return this._state.tempContext; } /** * Render a single frame. This method updates the pattern state and renders characters to the canvas. * @param time - optional timestamp for the frame, defaults to `performance.now()`. */ render(e = performance.now()) { const t = e - this._state.lastTime; this._state.lastTime = e, this._state.options.animated && (this._state.animationTime += t / 1e3 * this._state.options.animationSpeed); const i = { 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; const s = this.pattern.update(i).generate(i); !this._hasOutputChanged(s) && !this._state.isDirty && !this.pattern.isDirty || (this.pattern.isDirty = !1, this._state.isDirty = !1, this._state.lastHash = this._hash(s), this.renderer.clear(this._state.options.backgroundColor), this.renderer.render(s, this.region)); } /** * Start animation loop. */ startAnimation() { if (this._state.animationId) throw new Error("Animation is already running!"); this._state.options.animated = !0, this._state.lastTime = performance.now(); const e = (t) => { this.isAnimating && (this.render(t), this._state.animationId = requestAnimationFrame(e)); }; this._state.animationId = requestAnimationFrame(e); } /** * Stop animation loop. */ stopAnimation() { this._state.options.animated = !1, this._state.animationId !== null && (cancelAnimationFrame(this._state.animationId), this._state.animationId = null); } /** * Update rendering options. */ setOptions(e) { const t = this._state.options; this._state.options = { ...t, ...e }, this._state.isDirty = this._hasOptionsChanged(t), this._state.region = this._calculateRegion(), this.pattern.initialize(this._state.region), this.renderer.options = this._state.options, this._syncAnimationState(); } /** * Resize the canvas and recalculate layout. */ 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.isAnimating || (this._state.isDirty = !0, this.render()); } /** * Cleanup resources and stop animation. */ destroy() { this.stopAnimation(), this._state.pattern?.destroy(), 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 = C(); } /** * Calculate character spacing based on font metrics. * @returns A tuple containing the character width, height, and spacing sizes. */ _calculateSpacing() { let e = 0; for (const r of this.pattern.options.characters) { const _ = this.tempContext.measureText(r); e = Math.max(e, _.width); } const t = e, i = this.tempContext.measureText(this.pattern.options.characters.join("")), s = i.actualBoundingBoxAscent + i.actualBoundingBoxDescent, n = Math.max(s, 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(n, this._state.options.fontSize * 1.2); return [t, n, a, o]; } /** * Calculate the rendering region based on canvas size and options. * @returns The calculated RenderRegion object. */ _calculateRegion() { this.tempContext.font = `${this._state.options.fontSize}px ${this._state.options.fontFamily}`; const [e, t, i, s] = this._calculateSpacing(), n = Math.floor(this.canvas.width / i), a = Math.floor(this.canvas.height / s); return { startColumn: this._state.options.padding, endColumn: n - this._state.options.padding, startRow: this._state.options.padding, endRow: a - this._state.options.padding, columns: n, rows: a, charWidth: e, charHeight: t, charSpacingX: i, charSpacingY: s, canvasWidth: this.canvas.width, canvasHeight: this.canvas.height }; } /** * Initialize the renderer with the canvas and options. * This sets up the rendering context and prepares for rendering. */ _setupRenderer() { [this.canvas.width, this.canvas.height] = this.realCanvasSize, this.renderer.initialize(this.canvas, this._state.options), 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); } /** * Setup mouse event listeners for interaction. * This allows patterns to respond to mouse movements and clicks. */ _setupMouseEvents() { this.canvas.addEventListener("mousemove", this._mouseMoveHandler), this.canvas.addEventListener("click", this._mouseClickHandler); } /** * Reset the animation time to zero. * Useful when restarting animations or switching patterns. */ _resetAnimationTime() { this._state.animationTime = 0; } /** * Synchronize animation state with the current options. * This ensures that the renderer reflects the current animation settings. */ _syncAnimationState() { this._state.options.animated && this._state.animationId === null ? this.startAnimation() : !this._state.options.animated && this._state.animationId !== null && this.stopAnimation(); } /** * Generate a hash for the current character data list. * @param list - the character data list to hash. * @returns A numeric hash value. */ _hash(e) { let t = 0; for (const { x: i, y: s, char: n, color: a = "", opacity: o = 1, scale: r = 1, rotation: _ = 0 } of e) t ^= (i * 31 + s * 17 ^ n.charCodeAt(0)) + (a.charCodeAt(0) || 0) * 13 + Math.floor(o * 100) * 7 + Math.floor(r * 100) * 5 + Math.floor(_ * 100) * 3; return t; } /** * Check if the output has changed since the last render. * This is used to avoid unnecessary rendering when nothing has changed. * @param list - the current character data list. * @returns True if the output has changed, false otherwise. */ _hasOutputChanged(e) { return this._hash(e) !== this._state.lastHash; } /** * Check if the new options differ from the current ones. * @param options - the options to compare against current options. * @returns True if any option has changed, false otherwise. */ _hasOptionsChanged(e) { return Object.keys(e).some((t) => { const i = this._state.options[t], s = e[t]; return i !== s; }); } } const y = (c) => { let e = c === 0 ? 1 : c; return () => (e = e * 16807 % 2147483647, e / 2147483647); }, z = { frequency: 0.01, octaves: 4, persistence: 0.5, lacunarity: 2, seed: 0 }; class b extends p { constructor(t = {}) { super({ ...z, ...t }); /** * Stores a permutation table used for generating Perlin noise. * This array contains a shuffled sequence of numbers and is used to * determine gradient directions and hashing in algorithm. */ h(this, "_permutations"); this._permutations = this._generatePermutations(this._options.seed); } get _frequency() { return this._options.frequency; } get _octaves() { return this._options.octaves; } get _persistence() { return this._options.persistence; } get _lacunarity() { return this._options.lacunarity; } /** * Update options while preserving expensive permutation table when possible. */ setOptions(t) { const i = this._options.seed; super.setOptions(t), t.seed !== void 0 && t.seed !== i && (this._permutations = this._generatePermutations(this._options.seed)); } update(t) { return this; } /** * Generate characters for the current frame using Perlin noise. * @param context - the current rendering context with time and region info * @returns Array of character data for rendering */ generate({ animationTime: t, region: i }) { if (this.options.characters.length === 0) return []; const s = []; for (let n = i.startRow; n <= i.endRow; n++) for (let a = i.startColumn; a <= i.endColumn; a++) { const o = this._fractalNoise( a * this._options.frequency, n * this._options.frequency, t * 1e-3 ), r = Math.max(0, Math.min(1, (o + 1) / 2)), _ = Math.floor(r * this._options.characters.length), l = Math.max(0, Math.min(_, this._options.characters.length - 1)); s.push({ char: this._options.characters[l], x: a * i.charSpacingX, y: n * i.charSpacingY, opacity: r }); } return s; } /** * Generate a proper permutation table for Perlin noise. */ _generatePermutations(t) { const i = y(t), s = Array.from({ length: 256 }, (n, a) => a); for (let n = 255; n > 0; n--) { const a = Math.floor(i() * (n + 1)), o = s[n]; s[n] = s[a], s[a] = o; } return s.concat(s); } /** * Fade function for smooth interpolation. */ _fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); } /** * Linear interpolation. */ _lerp(t, i, s) { return t + s * (i - t); } /** * 3D gradient function. */ _gradient3D(t, i, s, n) { const a = t & 15, o = a < 8 ? i : s, r = a < 4 ? s : a === 12 || a === 14 ? i : n; return ((a & 1) === 0 ? o : -o) + ((a & 2) === 0 ? r : -r); } /** * Generate 3D Perlin noise at given coordinates. */ _noise3D(t, i, s) { const n = Math.floor(t) & 255, a = Math.floor(i) & 255, o = Math.floor(s) & 255; t -= Math.floor(t), i -= Math.floor(i), s -= Math.floor(s); const r = this._fade(t), _ = this._fade(i), l = this._fade(s), u = this._permutations[n] + a & 255, m = this._permutations[u] + o & 255, g = this._permutations[u + 1 & 255] + o & 255, f = this._permutations[n + 1 & 255] + a & 255, v = this._permutations[f] + o & 255, S = this._permutations[f + 1 & 255] + o & 255; return this._lerp( this._lerp( this._lerp( this._gradient3D(this._permutations[m], t, i, s), this._gradient3D(this._permutations[v], t - 1, i, s), r ), this._lerp( this._gradient3D(this._permutations[g], t, i - 1, s), this._gradient3D(this._permutations[S], t - 1, i - 1, s), r ), _ ), this._lerp( this._lerp( this._gradient3D(this._permutations[m + 1 & 255], t, i, s - 1), this._gradient3D(this._permutations[v + 1 & 255], t - 1, i, s - 1), r ), this._lerp( this._gradient3D(this._permutations[g + 1 & 255], t, i - 1, s - 1), this._gradient3D(this._permutations[S + 1 & 255], t - 1, i - 1, s - 1), r ), _ ), l ); } /** * Generate fractal noise using multiple octaves. * This creates more natural-looking, organic patterns. */ _fractalNoise(t, i, s = 0) { let n = 0, a = 0, o = 1, r = 1; for (let _ = 0; _ < this._octaves; _++) n += this._noise3D( t * r, i * r, s * r ) * o, a += o, o *= this._persistence, r *= this._lacunarity; return n / a; } /** * Generate animated noise that changes over time. * This creates flowing, organic motion patterns. */ _animatedNoise(t, i, s) { const a = this._frequency, o = this._fractalNoise( t * a, i * a, s * 0.01 ), r = this._fractalNoise( (t + 1e3) * a, (i + 1e3) * a, s * 0.01 * 0.8 ); return (o + r * 0.5) / 1.5; } /** * Generate a noise function suitable for ASCII pattern generation. */ _getNoiseFunction(t = "down") { return (i, s, n) => { let a = i, o = s; const r = n; switch (t) { case "left": a = i + n * 0.5; break; case "right": a = i - n * 0.5; break; case "up": o = s + n * 0.5; break; case "down": o = s - n * 0.5; break; } return this._animatedNoise(a, o, r); }; } } h(b, "ID", "perlin-noise"); const E = { rainDensity: 0.8, minDropLength: 8, maxDropLength: 25, minSpeed: 0.5, maxSpeed: 1.5, mutationRate: 0.04, fadeOpacity: 0.2, headColor: "#FFFFFF" }; class L extends p { constructor(t = {}) { super({ ...E, ...t }); h(this, "_rainDrops", []); h(this, "_region", null); h(this, "_lastFrameCharacters", []); } initialize(t) { const i = this._rainDrops.length > 0, s = this._region; this._region = t, !i || !s || Math.abs(s.columns - t.columns) > 2 || Math.abs(s.rows - t.rows) > 2 ? (this._rainDrops = [], this._lastFrameCharacters = [], this._initializeRainDrops()) : (this._adjustDropsToNewRegion(s, t), this._maintainRainDensity()); } update(t) { return this._region ? (this._updateRainDrops(t), this._maintainRainDensity(), this) : this; } generate(t) { if ((!this._region || this._rainDrops.length === 0) && (this._region && this._rainDrops.length === 0 && this._initializeRainDrops(), this._rainDrops.length === 0)) return []; const i = []; if (this._options.fadeOpacity > 0) for (const s of this._lastFrameCharacters) i.push({ ...s, opacity: this._options.fadeOpacity }); for (const s of this._rainDrops) this._renderRainDrop(s, i, t); return this._lastFrameCharacters = i.filter((s) => s.opacity !== this._options.fadeOpacity), i; } _initializeRainDrops() { if (!this._region) return; const t = Math.floor(this._region.columns * this._options.rainDensity), i = Math.max(1, t); for (let s = 0; s < i; s++) { const n = Math.floor(Math.random() * this._region.columns) + this._region.startColumn, a = this._createRainDrop(n, 0); s < Math.max(1, Math.floor(i * 0.3)) && (a.y = Math.random() * this._region.rows), this._rainDrops.push(a); } } _createRainDrop(t, i = 0) { const s = Math.floor( Math.random() * (this._options.maxDropLength - this._options.minDropLength) ) + this._options.minDropLength, n = Array.from( { length: s }, () => this._options.characters[Math.floor(Math.random() * this._options.characters.length)] ); return { y: -Math.floor(Math.random() * s) - Math.random() * 10, column: t, length: s, characters: n, lastMutationTime: i, speed: Math.random() * (this._options.maxSpeed - this._options.minSpeed) + this._options.minSpeed }; } _updateRainDrops(t) { if (this._region) for (const i of this._rainDrops) { if (t.isAnimating && (i.y += i.speed * t.animationSpeed * t.deltaTime), t.animationTime - i.lastMutationTime > 1 / this._options.mutationRate && Math.random() < this._options.mutationRate) { const s = Math.floor(Math.random() * i.length); i.characters[s] = this._options.characters[Math.floor(Math.random() * this._options.characters.length)], i.lastMutationTime = t.animationTime; } i.y - i.length > this._region.endRow && this._resetRainDrop(i, t.animationTime); } } _resetRainDrop(t, i) { this._region && (t.lastMutationTime = i, t.y = -Math.floor(Math.random() * 8) - t.length, t.length = Math.floor( Math.random() * (this._options.maxDropLength - this._options.minDropLength) ) + this._options.minDropLength, t.characters = Array.from( { length: t.length }, () => this._options.characters[Math.floor(Math.random() * this._options.characters.length)] ), t.speed = Math.random() * (this._options.maxSpeed - this._options.minSpeed) + this._options.minSpeed, t.column = Math.floor(Math.random() * this._region.columns) + this._region.startColumn); } _maintainRainDensity() { if (!this._region) return; const t = Math.floor(this._region.columns * this._options.rainDensity); for (; this._rainDrops.length < t; ) { const i = this._rainDrops.length % this._region.columns + this._region.startColumn, s = this._createRainDrop(i, 0); Math.random() < 0.4 && (s.y = Math.random() * this._region.rows + this._region.startRow), this._rainDrops.push(s); } this._rainDrops.length > t && (this._rainDrops.length = t); } /** * Adjust existing drops to fit a new region without losing current state. */ _adjustDropsToNewRegion(t, i) { for (const s of this._rainDrops) s.column >= i.startColumn + i.columns ? s.column = s.column % i.columns + i.startColumn : s.column < i.startColumn && (s.column = i.startColumn); } _renderRainDrop(t, i, s) { if (this._region) for (let n = 0; n < t.length; n++) { const a = Math.floor(t.y) - n; if (a < this._region.startRow || a > this._region.endRow || t.column < this._region.startColumn || t.column > this._region.endColumn) continue; const o = t.column * this._region.charSpacingX, r = a * this._region.charSpacingY; let _, l = 1; n === 0 ? _ = this._options.headColor : n >= 3 && (l = 1 - n / t.length), i.push({ x: o, y: r, color: _, opacity: l, char: t.characters[n] }); } } destroy() { this._rainDrops = [], this._lastFrameCharacters = [], this._region = null; } setOptions(t) { if (super.setOptions(t), t.rainDensity !== void 0 && this._region && this._maintainRainDensity(), t.characters !== void 0) for (const i of this._rainDrops) i.characters = Array.from( { length: i.length }, () => this._options.characters[Math.floor(Math.random() * this._options.characters.length)] ); } } h(L, "ID", "rain"); const O = { seed: 0 }; class F extends p { constructor(e = {}) { super({ ...O, ...e }); } update(e) { return this; } generate({ region: e, animationTime: t }) { const i = [], s = y(this._options.seed + Math.floor(t * 10)); for (let n = e.startRow; n <= e.endRow; n++) for (let a = e.startColumn; a <= e.endColumn; a++) { const o = Math.floor(s() * this._options.characters.length), r = this._options.characters[o] || " "; i.push({ char: r, x: a * e.charSpacingX, y: n * e.charSpacingY }); } return i; } } h(F, "ID", "static"); class P { constructor() { h(this, "_renderer", null); } get pattern() { return this.renderer.pattern; } get options() { return Object.freeze(this.renderer.options); } get renderer() { if (!this._renderer) throw new Error("Renderer is not initialized - call init() first."); return this._renderer; } set renderer(e) { this._renderer = e; } /** * Initialize the ASCIIGround instance with a canvas, a pattern and renderer options. * @param canvas - The HTML canvas element to render on. * @param pattern - The pattern to use for rendering. * @param options - Optional renderer options. */ init(e, t, i) { return this.renderer = new A({ canvas: e, pattern: t, options: i }), this; } /** * Start the animation. * @returns The current ASCIIGround instance. */ startAnimation() { return this.renderer.startAnimation(), this; } /** * Stop the animation. * @returns The current ASCIIGround instance. */ stopAnimation() { return this.renderer.stopAnimation(), this; } /** * Set a new pattern generator for the renderer. * @param pattern - The new pattern to set. * @returns The current ASCIIGround instance. */ setPattern(e) { return this.renderer.pattern = e, this; } /** * Set new options for the renderer. * @param options - The new options to set. * @returns The current ASCIIGround instance. */ setOptions(e) { return this.renderer.setOptions(e), this; } /** * Destroy the ASCIIGround instance, cleaning up resources. * This will stop the animation and nullify the renderer. */ destroy() { this.renderer.destroy(), this.renderer = null; } } export { P as ASCIIGround, A as ASCIIRenderer, d as Canvas2DRenderer, D as DummyPattern, p as Pattern, b as PerlinNoisePattern, L as RainPattern, F as StaticNoisePattern, T as WebGLRenderer, I as createRenderer, y as createSeededRandom, P as default };