@kya-os/cli
Version:
CLI for MCP-I setup and management
221 lines • 8.03 kB
JavaScript
/**
* Beams Effect
* Creates beams which travel over the canvas illuminating the characters
*/
import { BaseEffect } from "../types.js";
import { ANSI, easeInOutSine } from "../utils.js";
/**
* Beams effect implementation
*/
export class BeamsEffect extends BaseEffect {
constructor(options = {}) {
super();
this.name = "Beams";
this.description = "Creates beams which travel over the canvas illuminating the characters";
this.beams = [];
this.animationFrameCount = 0;
this.totalFrames = 0;
this.nextBeamId = 0;
this.lastBeamSpawn = 0;
this._isEffectComplete = false;
this.options = {
duration: 5000,
beamCount: 8,
beamWidth: 5,
beamSpeedRange: [1.0, 3.0],
beamColors: ["ffffff", "00D1FF", "8A008A"],
illuminationBrightness: 2.0,
baseColor: "202020",
...options,
};
}
/**
* Initialize the effect
*/
onInitialize() {
this.totalFrames = Math.floor((this.options.duration / 1000) * this.config.frameRate);
this.beams = [];
this.animationFrameCount = 0;
this.nextBeamId = 0;
this.lastBeamSpawn = 0;
this._isEffectComplete = false;
// Set base color for all characters
for (const char of this.characters) {
char.visual.colors.fg = this.options.baseColor;
}
}
/**
* Render the next frame
*/
async render() {
if (!this.isInitialized) {
throw new Error("Effect not initialized");
}
// Update beams
this.updateBeams();
// Render text with beam effects line by line
const lines = this.text.split("\n");
const renderedLines = [];
for (let y = 0; y < lines.length; y++) {
let line = "";
const chars = [...lines[y]];
for (let x = 0; x < chars.length; x++) {
const char = chars[x];
let color = this.options.baseColor;
// Check if character is illuminated by any beam
for (const beam of this.beams) {
if (!beam.active)
continue;
const distance = beam.direction === "horizontal"
? Math.abs(y - beam.position.y)
: Math.abs(x - beam.position.x);
if (distance <= beam.width) {
const intensity = 1 - distance / beam.width;
const easedIntensity = easeInOutSine(intensity);
const beamColor = this.getBeamColor(beam);
color = this.applyIllumination(color, beamColor, easedIntensity);
}
}
const colorCode = this.getColorCode(color);
const resetCode = ANSI.reset;
line += colorCode + char + resetCode;
}
renderedLines.push(line);
}
this.animationFrameCount++;
return renderedLines;
}
/**
* Update beam positions and states
*/
updateBeams() {
const dimensions = this.getCanvasDimensions();
const spawnInterval = Math.floor(this.totalFrames / (this.options.beamCount * 3));
// Spawn new beams more frequently
if (this.beams.filter(b => b.active).length < this.options.beamCount &&
this.animationFrameCount - this.lastBeamSpawn > spawnInterval &&
this.animationFrameCount < this.totalFrames * 0.8) {
this.spawnBeam();
this.lastBeamSpawn = this.animationFrameCount;
}
// Update existing beams
for (const beam of this.beams) {
if (!beam.active)
continue;
// Update position based on direction
if (beam.direction === "horizontal") {
beam.position.x += beam.speed;
// Deactivate if off screen
if (beam.position.x > dimensions.width + beam.width) {
beam.active = false;
}
}
else {
beam.position.y += beam.speed;
// Deactivate if off screen
if (beam.position.y > dimensions.height + beam.width) {
beam.active = false;
}
}
}
// Check if effect is complete
const activeBeams = this.beams.filter((b) => b.active).length;
this._isEffectComplete =
activeBeams === 0 && this.animationFrameCount >= this.totalFrames * 0.8;
}
/**
* Spawn a new beam
*/
spawnBeam() {
const dimensions = this.getCanvasDimensions();
const isHorizontal = Math.random() > 0.5;
const beam = {
id: this.nextBeamId++,
position: {
x: isHorizontal
? -this.options.beamWidth
: Math.random() * dimensions.width,
y: isHorizontal
? Math.random() * dimensions.height
: -this.options.beamWidth,
},
direction: isHorizontal ? "horizontal" : "vertical",
speed: this.options.beamSpeedRange[0] +
Math.random() *
(this.options.beamSpeedRange[1] - this.options.beamSpeedRange[0]),
width: this.options.beamWidth,
progress: 0,
active: true,
};
this.beams.push(beam);
}
/**
* Get beam color based on its properties
*/
getBeamColor(beam) {
// Cycle through colors based on beam ID
const colorIndex = beam.id % this.options.beamColors.length;
return this.options.beamColors[colorIndex];
}
/**
* Apply illumination to a base color
*/
applyIllumination(baseColor, beamColor, intensity) {
// Parse colors
const baseR = parseInt(baseColor.substring(0, 2), 16);
const baseG = parseInt(baseColor.substring(2, 4), 16);
const baseB = parseInt(baseColor.substring(4, 6), 16);
const beamR = parseInt(beamColor.substring(0, 2), 16);
const beamG = parseInt(beamColor.substring(2, 4), 16);
const beamB = parseInt(beamColor.substring(4, 6), 16);
// Apply illumination with brightness multiplier
const brightness = 1 + (this.options.illuminationBrightness - 1) * intensity;
const r = Math.min(255, Math.round(baseR + (beamR - baseR) * intensity * brightness));
const g = Math.min(255, Math.round(baseG + (beamG - baseG) * intensity * brightness));
const b = Math.min(255, Math.round(baseB + (beamB - baseB) * intensity * brightness));
return [r, g, b]
.map((c) => Math.max(0, Math.min(255, c)).toString(16).padStart(2, "0"))
.join("");
}
/**
* 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`;
}
/**
* 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.beams = [];
this.animationFrameCount = 0;
this.nextBeamId = 0;
this.lastBeamSpawn = 0;
this._isEffectComplete = false;
this.onInitialize();
}
}
//# sourceMappingURL=beams.js.map