UNPKG

@kya-os/cli

Version:

CLI for MCP-I setup and management

234 lines 8.54 kB
/** * Burn Effect * Characters are ignited and burn up the screen */ import { BaseEffect, } from "../types.js"; import { ANSI } from "../utils.js"; /** * Burn effect implementation */ export class BurnEffect extends BaseEffect { constructor(options = {}) { super(); this.name = "Burn"; this.description = "Characters are ignited and burn up the screen"; this.characterStates = new Map(); this.burnFront = -1; // Current row being burned this.animationFrameCount = 0; this.totalFrames = 0; this.burnPhaseDuration = 10; this._isEffectComplete = false; this.options = { duration: 3000, startingColor: "837373", burnColors: ["ffffff", "fff75d", "fe650d", "8A003C", "510100"], burnSpeed: 2, burnDelay: 3, burnRandomness: 0.3, ...options, }; } /** * Initialize the effect */ onInitialize() { const dimensions = this.getCanvasDimensions(); this.totalFrames = Math.floor((this.options.duration / 1000) * this.config.frameRate); this.burnPhaseDuration = Math.floor(this.totalFrames / (dimensions.height + 10)); // Initialize character states this.characterStates.clear(); for (const char of this.characters) { const key = `${char.originalPosition.x},${char.originalPosition.y}`; this.characterStates.set(key, { burnStartTime: -1, burnPhase: 0, isBurning: false, fadeProgress: 0, }); } this.burnFront = -1; this.animationFrameCount = 0; this._isEffectComplete = false; } /** * Render the next frame */ async render() { if (!this.isInitialized) { throw new Error("Effect not initialized"); } // Update burn state this.updateBurnState(); // Create the frame const dimensions = this.getCanvasDimensions(); const frame = Array(dimensions.height) .fill("") .map(() => Array(dimensions.width).fill(" ")); // Render characters for (const char of this.characters) { const key = `${char.originalPosition.x},${char.originalPosition.y}`; const state = this.characterStates.get(key); if (state) { const visual = this.createCharacterVisual(char, state); char.visual = visual; const { x, y } = char.currentPosition; if (y >= 0 && y < dimensions.height && x >= 0 && x < dimensions.width) { // Apply ANSI color codes const colorCode = this.getColorCode(visual.colors.fg); const resetCode = ANSI.reset; frame[y][x] = colorCode + visual.symbol + resetCode; } } } this.animationFrameCount++; // Convert frame to strings return frame.map((row) => row.join("")); } /** * Update burn state for all characters */ updateBurnState() { const dimensions = this.getCanvasDimensions(); // Update burn front if (this.animationFrameCount % this.options.burnDelay === 0) { this.burnFront++; } // Update character states let allBurned = true; for (const char of this.characters) { const key = `${char.originalPosition.x},${char.originalPosition.y}`; const state = this.characterStates.get(key); if (!state) continue; // Check if character should start burning if (!state.isBurning && char.originalPosition.y >= dimensions.height - this.burnFront - 1) { const randomDelay = Math.random() * this.options.burnRandomness; if (randomDelay < 0.5) { state.isBurning = true; state.burnStartTime = this.animationFrameCount; } } // Update burning characters if (state.isBurning) { const burnTime = this.animationFrameCount - state.burnStartTime; const phase = Math.min(Math.floor(burnTime / this.burnPhaseDuration), 6); state.burnPhase = phase; if (phase >= 6) { state.fadeProgress = Math.min((burnTime - 6 * this.burnPhaseDuration) / (this.burnPhaseDuration * 2), 1); } } if (state.burnPhase < 6) { allBurned = false; } } this._isEffectComplete = allBurned || this.animationFrameCount >= this.totalFrames; } /** * Create visual representation of a character */ createCharacterVisual(char, state) { let color; if (!state.isBurning) { // Not burning yet - use starting color color = this.options.startingColor; } else if (state.burnPhase === 0) { // Just started burning - still starting color color = this.options.startingColor; } else if (state.burnPhase < 6) { // Burning - use burn colors const colorIndex = Math.min(state.burnPhase - 1, this.options.burnColors.length - 1); color = this.options.burnColors[colorIndex]; } else { // Burned out - fade from last burn color to transparent/final const lastBurnColor = this.options.burnColors[this.options.burnColors.length - 1]; if (state.fadeProgress < 0.5) { // Fade to dark color = this.interpolateColor(lastBurnColor, "000000", state.fadeProgress * 2); } else { // Character disappears color = "000000"; } } // Apply some flickering to burning characters let finalColor = color; if (state.burnPhase > 0 && state.burnPhase < 5 && Math.random() < 0.3) { // Flicker between current and next color const nextIndex = Math.min(state.burnPhase, this.options.burnColors.length - 1); const nextColor = this.options.burnColors[nextIndex]; finalColor = this.interpolateColor(color, nextColor, 0.5); } return { symbol: state.burnPhase >= 6 && state.fadeProgress > 0.7 ? " " : char.originalSymbol, colors: { fg: finalColor, bg: null, }, }; } /** * Get ANSI color code for a color */ getColorCode(color) { if (!color || this.config.noColor) { return ""; } // For RGB colors, convert to ANSI 24-bit color if (typeof color === "string") { const r = parseInt(color.substring(0, 2), 16); const g = parseInt(color.substring(2, 4), 16); const b = parseInt(color.substring(4, 6), 16); return `\x1b[38;2;${r};${g};${b}m`; } // For XTerm colors return `\x1b[38;5;${color}m`; } /** * Interpolate between two colors */ interpolateColor(color1, color2, factor) { const r1 = parseInt(color1.substring(0, 2), 16); const g1 = parseInt(color1.substring(2, 4), 16); const b1 = parseInt(color1.substring(4, 6), 16); const r2 = parseInt(color2.substring(0, 2), 16); const g2 = parseInt(color2.substring(2, 4), 16); const b2 = parseInt(color2.substring(4, 6), 16); const r = Math.round(r1 + (r2 - r1) * factor); const g = Math.round(g1 + (g2 - g1) * factor); const b = Math.round(b1 + (b2 - b1) * factor); return [r, g, b] .map((c) => Math.max(0, Math.min(255, c)).toString(16).padStart(2, "0")) .join(""); } /** * Render fallback for when effects are disabled */ renderFallback() { return this.text.split("\n"); } /** * Check if effect is complete */ isComplete() { return this._isEffectComplete; } /** * Reset the effect */ onReset() { this.characterStates.clear(); this.burnFront = -1; this.animationFrameCount = 0; this._isEffectComplete = false; this.onInitialize(); } } //# sourceMappingURL=burn.js.map