UNPKG

@kya-os/cli

Version:

CLI for MCP-I setup and management

421 lines 17.7 kB
/** * Blackhole Effect * Characters are consumed by a black hole and explode outwards */ import { BaseEffect, } from "../types.js"; import { ANSI } from "../utils.js"; /** * Blackhole effect implementation */ export class BlackholeEffect extends BaseEffect { constructor(options = {}) { super(); this.name = "Blackhole"; this.description = "Characters are consumed by a black hole and explode outwards"; this.characterStates = new Map(); this.animationFrameCount = 0; this.totalFrames = 0; this._isEffectComplete = false; // Animation phase tracking this.currentPhase = "forming"; // @ts-ignore - Reserved for phase transitions this.phaseProgress = 0; // Blackhole properties this.blackholeCenter = { x: 0, y: 0 }; this.blackholeRadius = 0; this.blackholeChars = []; // @ts-ignore - Reserved for queued consumption this.consumptionQueue = []; // Star symbols this.starSymbols = ["*", "'", "`", "¤", "•", "°", "·", "✦", "✧"]; this.starfieldColors = [ "4a4a4d", "808080", "a0a0a0", "c0c0c0", "e0e0e0", "ffffff", ]; this.options = { duration: 6000, blackholeColor: "ffffff", starColors: ["4a4a4d", "808080", "a0a0a0", "c0c0c0", "e0e0e0", "ffffff"], finalColor: "ffffff", useGradient: false, gradientDirection: "diagonal", blackholeSize: 0.3, ...options, }; } /** * Initialize the effect */ onInitialize() { this.totalFrames = Math.floor((this.options.duration / 1000) * this.config.frameRate); const dimensions = this.getCanvasDimensions(); this.blackholeCenter = { x: Math.floor(dimensions.width / 2), y: Math.floor(dimensions.height / 2), }; this.blackholeRadius = Math.max(3, Math.floor(Math.min(dimensions.width * this.options.blackholeSize, dimensions.height * this.options.blackholeSize))); // Initialize character states this.characterStates.clear(); this.blackholeChars = []; this.consumptionQueue = []; // Calculate final colors for gradient const gradientColors = this.calculateGradientColors(dimensions); // Select characters for blackhole ring const blackholeCharCount = Math.min(Math.floor(this.blackholeRadius * 3), Math.floor(this.characters.length * 0.3)); const shuffledChars = [...this.characters].sort(() => Math.random() - 0.5); this.blackholeChars = shuffledChars.slice(0, blackholeCharCount); this.consumptionQueue = shuffledChars.slice(blackholeCharCount); // Initialize all character states for (const char of this.characters) { const key = `${char.originalPosition.x},${char.originalPosition.y}`; const isBlackholeChar = this.blackholeChars.includes(char); let finalColor = this.options.finalColor; if (this.options.useGradient) { const gradientKey = this.getGradientKey(char.originalPosition, dimensions); finalColor = gradientColors[gradientKey] || this.options.finalColor; } // Random starfield position const starfieldPos = { x: Math.floor(Math.random() * dimensions.width), y: Math.floor(Math.random() * dimensions.height), }; this.characterStates.set(key, { phase: "starfield", isBlackholeRing: isBlackholeChar, starSymbol: this.starSymbols[Math.floor(Math.random() * this.starSymbols.length)], starColor: this.starfieldColors[Math.floor(Math.random() * this.starfieldColors.length)], position: { ...starfieldPos }, targetPosition: { ...char.originalPosition }, spiralAngle: Math.random() * Math.PI * 2, spiralRadius: this.calculateDistance(starfieldPos, this.blackholeCenter), explosionVector: { x: 0, y: 0 }, settlingProgress: 0, finalColor, }); } this.currentPhase = "forming"; this.phaseProgress = 0; this.animationFrameCount = 0; this._isEffectComplete = false; } /** * Calculate gradient colors */ calculateGradientColors(dimensions) { const gradientMap = {}; if (this.options.gradientDirection === "diagonal") { const maxDist = Math.sqrt(dimensions.width ** 2 + dimensions.height ** 2); for (let y = 0; y < dimensions.height; y++) { for (let x = 0; x < dimensions.width; x++) { const dist = Math.sqrt(x ** 2 + y ** 2); const factor = dist / maxDist; const key = `${x},${y}`; gradientMap[key] = this.interpolateColor(this.options.starColors[this.options.starColors.length - 1], this.options.finalColor, factor); } } } else { const size = this.options.gradientDirection === "vertical" ? dimensions.height : dimensions.width; const startColor = this.options.starColors[this.options.starColors.length - 1]; for (let i = 0; i < size; i++) { const factor = i / (size - 1); gradientMap[i] = this.interpolateColor(startColor, this.options.finalColor, factor); } } return gradientMap; } /** * Get gradient key based on direction */ getGradientKey(pos, _dimensions) { if (this.options.gradientDirection === "diagonal") { return `${pos.x},${pos.y}`; } else if (this.options.gradientDirection === "vertical") { return pos.y; } else { return pos.x; } } /** * Render the next frame */ async render() { if (!this.isInitialized) { throw new Error("Effect not initialized"); } // Update animation state this.updateAnimationState(); // 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 = Math.round(state.position.x); const y = Math.round(state.position.y); 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 animation state for all characters */ updateAnimationState() { const progress = this.animationFrameCount / this.totalFrames; // Update phase transitions if (this.currentPhase === "forming" && progress > 0.15) { this.currentPhase = "consuming"; } else if (this.currentPhase === "consuming" && progress > 0.5) { this.currentPhase = "collapsing"; } else if (this.currentPhase === "collapsing" && progress > 0.6) { this.currentPhase = "exploding"; } else if (this.currentPhase === "exploding" && progress > 0.7) { this.currentPhase = "settling"; } // Update character positions based on phase for (const [_key, state] of this.characterStates) { this.updateCharacterPosition(state); } // Check completion this._isEffectComplete = progress >= 1; } /** * Update individual character position */ updateCharacterPosition(state) { const frameProgress = this.animationFrameCount / this.totalFrames; switch (this.currentPhase) { case "forming": if (state.isBlackholeRing) { // Move to blackhole ring position const ringProgress = Math.min(frameProgress / 0.15, 1); const angle = (this.blackholeChars.indexOf(this.characters.find((c) => this.characterStates.get(`${c.originalPosition.x},${c.originalPosition.y}`) === state)) / this.blackholeChars.length) * Math.PI * 2; const targetX = this.blackholeCenter.x + Math.cos(angle) * this.blackholeRadius; const targetY = this.blackholeCenter.y + Math.sin(angle) * this.blackholeRadius; state.position.x = state.position.x + (targetX - state.position.x) * ringProgress * 0.1; state.position.y = state.position.y + (targetY - state.position.y) * ringProgress * 0.1; state.phase = "consuming"; } break; case "consuming": if (state.isBlackholeRing) { // Rotate blackhole ring const angle = (this.blackholeChars.indexOf(this.characters.find((c) => this.characterStates.get(`${c.originalPosition.x},${c.originalPosition.y}`) === state)) / this.blackholeChars.length) * Math.PI * 2 + frameProgress * Math.PI * 2; state.position.x = this.blackholeCenter.x + Math.cos(angle) * this.blackholeRadius; state.position.y = this.blackholeCenter.y + Math.sin(angle) * this.blackholeRadius; } else { // Spiral into blackhole const consumeProgress = (frameProgress - 0.15) / 0.35; if (consumeProgress > 0) { state.spiralAngle += 0.2; state.spiralRadius *= 0.95; state.position.x = this.blackholeCenter.x + Math.cos(state.spiralAngle) * state.spiralRadius; state.position.y = this.blackholeCenter.y + Math.sin(state.spiralAngle) * state.spiralRadius; if (state.spiralRadius < 2) { state.phase = "collapsed"; state.position = { ...this.blackholeCenter }; } } } break; case "collapsing": // All characters move to center const collapseProgress = (frameProgress - 0.5) / 0.1; if (collapseProgress > 0) { state.position.x += (this.blackholeCenter.x - state.position.x) * 0.2; state.position.y += (this.blackholeCenter.y - state.position.y) * 0.2; state.phase = "collapsed"; } break; case "exploding": // Calculate explosion vector if (state.explosionVector.x === 0 && state.explosionVector.y === 0) { const angle = Math.atan2(state.targetPosition.y - this.blackholeCenter.y, state.targetPosition.x - this.blackholeCenter.x); const distance = this.calculateDistance(state.targetPosition, this.blackholeCenter); const speed = Math.min(distance * 0.3, 5); state.explosionVector.x = Math.cos(angle) * speed; state.explosionVector.y = Math.sin(angle) * speed; state.phase = "exploding"; } // Apply explosion movement state.position.x += state.explosionVector.x; state.position.y += state.explosionVector.y; // Dampen explosion state.explosionVector.x *= 0.9; state.explosionVector.y *= 0.9; break; case "settling": // Settle to final position const settleProgress = (frameProgress - 0.7) / 0.3; if (settleProgress > 0) { state.position.x += (state.targetPosition.x - state.position.x) * 0.1; state.position.y += (state.targetPosition.y - state.position.y) * 0.1; state.settlingProgress = Math.min(settleProgress, 1); if (Math.abs(state.position.x - state.targetPosition.x) < 0.5 && Math.abs(state.position.y - state.targetPosition.y) < 0.5) { state.position = { ...state.targetPosition }; state.phase = "final"; } } break; } } /** * Create visual representation of a character */ createCharacterVisual(char, state) { let symbol = state.starSymbol; let color = state.starColor; switch (state.phase) { case "starfield": // Initial starfield state break; case "consuming": if (state.isBlackholeRing) { symbol = "*"; color = this.options.blackholeColor; } else { // Fading as consumed const fadeColors = ["808080", "606060", "404040", "202020", "000000"]; const fadeIndex = Math.min(Math.floor((1 - state.spiralRadius / 50) * fadeColors.length), fadeColors.length - 1); color = fadeColors[Math.max(0, fadeIndex)]; } break; case "collapsed": symbol = "●"; color = this.options.starColors[Math.floor(Math.random() * this.options.starColors.length)]; break; case "exploding": symbol = char.originalSymbol; color = this.options.starColors[Math.floor(this.animationFrameCount / 2) % this.options.starColors.length]; break; case "settling": case "final": symbol = char.originalSymbol; // Fade from star color to final color const starColor = this.options.starColors[0]; color = this.interpolateColor(starColor, state.finalColor, state.settlingProgress); break; } return { symbol, colors: { fg: color, bg: null, }, }; } /** * Calculate distance between two coordinates */ calculateDistance(a, b) { return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2); } /** * 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.blackholeChars = []; this.consumptionQueue = []; this.currentPhase = "forming"; this.phaseProgress = 0; this.animationFrameCount = 0; this._isEffectComplete = false; this.onInitialize(); } } //# sourceMappingURL=blackhole.js.map