UNPKG

@teaui/core

Version:

A high-level terminal UI library for Node

204 lines 6.56 kB
import { View } from '../View.js'; import { Style } from '../Style.js'; import { Point, Size } from '../geometry.js'; import { define } from '../util.js'; const WARM = [240, 223, 175]; const CYAN = [140, 208, 211]; const GRAY = [128, 128, 128]; const BODY_LINES = [ [ { text: ' ┌────────┐', fg: WARM }, { text: '──╮', fg: GRAY }, ], [ { text: ' │', fg: WARM }, { text: '╼╼╼╼╼╼╼╼', fg: CYAN }, { text: '│', fg: WARM }, { text: ' │', fg: GRAY }, ], [ { text: ' │', fg: WARM }, { text: ' TeaUI ', fg: CYAN }, { text: '│', fg: WARM }, { text: ' │', fg: GRAY }, ], [ { text: ' │', fg: WARM }, { text: '╼╼╼╼╼╼╼╼', fg: CYAN }, { text: '│', fg: WARM }, { text: '──╯', fg: GRAY }, ], [{ text: ' ╰▄▄▄▄▄▄▄▄╯ ', fg: WARM }], ]; const WIDTH = 16; const HEIGHT = 7; // Steam simulation constants const STEAM_ROWS = 2; // character rows for steam const STEAM_DOT_ROWS = STEAM_ROWS * 4; // braille rows const STEAM_DOT_COLS = WIDTH * 2; // braille columns const TICK_INTERVAL = 80; // ms per simulation step const MAX_PARTICLES = 24; const PARTICLE_MAX_LIFE = 10; // Spawn zone (dot-columns) — above the mug opening const SPAWN_MIN_X = 6; const SPAWN_MAX_X = 20; // Braille dot offsets: [row][col] // Each braille char is 2 dots wide × 4 dots tall const BRAILLE_DOT = [ [0x01, 0x08], [0x02, 0x10], [0x04, 0x20], [0x40, 0x80], ]; function steamToString(particles) { // Build a dot grid const grid = []; for (let r = 0; r < STEAM_DOT_ROWS; r++) { grid[r] = Array.from({ length: STEAM_DOT_COLS }, () => false); } for (const p of particles) { if (p.x >= 0 && p.x < STEAM_DOT_COLS && p.y >= 0 && p.y < STEAM_DOT_ROWS) { grid[p.y][p.x] = true; } } // Convert to braille characters const lines = []; for (let cy = 0; cy < STEAM_ROWS; cy++) { let line = ''; for (let cx = 0; cx < WIDTH; cx++) { let code = 0x2800; for (let dr = 0; dr < 4; dr++) { for (let dc = 0; dc < 2; dc++) { const dotRow = cy * 4 + dr; const dotCol = cx * 2 + dc; if (dotRow < STEAM_DOT_ROWS && dotCol < STEAM_DOT_COLS && grid[dotRow][dotCol]) { code += BRAILLE_DOT[dr][dc]; } } } line += String.fromCharCode(code); } lines.push(line); } return lines.join('\n'); } // Simple seedable RNG (xorshift32) so tests are deterministic when seeded function makeRng(seed) { let state = seed | 0 || 1; return () => { state ^= state << 13; state ^= state >> 17; state ^= state << 5; return (state >>> 0) / 0x100000000; }; } export class Logo extends View { #isAnimating = false; #particles = []; #elapsed = 0; #rng; constructor(props = {}) { super(props); this.#rng = makeRng(props.seed ?? Date.now() ^ (Math.random() * 0x7fffffff)); this.#update(props); define(this, 'isAnimating', { enumerable: true }); } get isAnimating() { return this.#isAnimating; } set isAnimating(value) { if (value === this.#isAnimating) return; this.#isAnimating = value; this.invalidateRender(); } update(props) { this.#update(props); super.update(props); } #update({ isAnimating }) { this.#isAnimating = isAnimating ?? false; } naturalSize(_available) { return new Size(WIDTH, HEIGHT); } receiveTick(dt) { if (!this.#isAnimating) return false; this.#elapsed += dt; let stepped = false; while (this.#elapsed >= TICK_INTERVAL) { this.#elapsed -= TICK_INTERVAL; this.#step(); stepped = true; } return stepped; } #step() { const rng = this.#rng; // Move existing particles upward with random drift for (const p of this.#particles) { const r = rng(); if (r < 0.15) { // drift left p.x -= 1; p.y -= 1; } else if (r < 0.3) { // drift right p.x += 1; p.y -= 1; } else if (r < 0.85) { // straight up p.y -= 1; } // else: stay (pause for a tick) p.life -= 1; } // Remove dead or out-of-bounds particles this.#particles = this.#particles.filter(p => p.life > 0 && p.y >= 0 && p.x >= 0 && p.x < STEAM_DOT_COLS); // Spawn new particles at the bottom of the steam area if (this.#particles.length < MAX_PARTICLES) { const count = 1 + Math.floor(rng() * 2); for (let i = 0; i < count; i++) { this.#particles.push({ x: SPAWN_MIN_X + Math.floor(rng() * (SPAWN_MAX_X - SPAWN_MIN_X)), y: STEAM_DOT_ROWS - 1, life: 3 + Math.floor(rng() * (PARTICLE_MAX_LIFE - 3)), }); } } } render(viewport) { if (this.#isAnimating) { viewport.registerTick(); } // Render steam const steamStyle = new Style({ foreground: GRAY }); if (this.#isAnimating) { const steam = steamToString(this.#particles); const steamLines = steam.split('\n'); for (let y = 0; y < steamLines.length; y++) { viewport.write(steamLines[y], new Point(0, y), steamStyle); } } else { // Static steam when not animating viewport.write(' ) ) ) ', new Point(0, 0), steamStyle); viewport.write(' ( ( ( ', new Point(0, 1), steamStyle); } // Render body for (let y = 0; y < BODY_LINES.length; y++) { let x = 0; for (const span of BODY_LINES[y]) { const style = new Style({ foreground: span.fg }); viewport.write(span.text, new Point(x, y + STEAM_ROWS), style); x += span.text.length; } } } } //# sourceMappingURL=Logo.js.map