@kya-os/cli
Version:
CLI for MCP-I setup and management
421 lines • 17.7 kB
JavaScript
/**
* 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