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