@kya-os/cli
Version:
CLI for KYA-OS MCP-I setup and management
278 lines • 10.9 kB
JavaScript
/**
* Waves Effect
* Creates wave patterns that travel across the text
*/
import { BaseEffect, } from "../types.js";
import { ANSI } from "../utils.js";
import { Gradient, GradientDirection, EASING } from "../gradient.js";
/**
* Waves effect implementation
*/
export class WavesEffect extends BaseEffect {
constructor(options = {}) {
super();
this.name = "Waves";
this.description = "Creates wave patterns that travel across the text";
this.characterStates = new Map();
this.animationFrameCount = 0;
this.totalFrames = 0;
this.wavePosition = 0;
this._isEffectComplete = false;
this.options = {
duration: 4000,
waveColors: ["0080ff", "00a0ff", "00c0ff", "00e0ff", "00ffff", "80ffff", "ffffff"],
finalColor: "ffffff",
waveSpeed: 0.15,
waveFrequency: 2,
waveAmplitude: 3,
direction: "horizontal",
useGradient: true,
...options,
};
}
/**
* Initialize the effect
*/
onInitialize() {
this.totalFrames = Math.floor((this.options.duration / 1000) * this.config.frameRate);
// Create gradient if enabled
if (this.options.useGradient) {
this.gradient = new Gradient({
stops: this.options.waveColors,
steps: 10,
direction: this.options.direction === "vertical"
? GradientDirection.HORIZONTAL
: GradientDirection.VERTICAL,
easingFn: EASING.inOutSine,
});
}
// Initialize character states
this.characterStates.clear();
const dimensions = this.getCanvasDimensions();
for (const char of this.characters) {
const key = `${char.originalPosition.x},${char.originalPosition.y}`;
// Calculate wave offset based on position and direction
let waveOffset = 0;
switch (this.options.direction) {
case "horizontal":
waveOffset = char.originalPosition.x / dimensions.width;
break;
case "vertical":
waveOffset = char.originalPosition.y / dimensions.height;
break;
case "diagonal":
waveOffset = (char.originalPosition.x + char.originalPosition.y) /
(dimensions.width + dimensions.height);
break;
}
this.characterStates.set(key, {
phase: "waiting",
waveOffset,
waveIntensity: 0,
originalY: char.originalPosition.y,
currentWaveY: char.originalPosition.y,
colorIndex: 0,
settleProgress: 0,
});
}
this.wavePosition = -0.5; // Start wave off-screen
this.animationFrameCount = 0;
this._isEffectComplete = false;
}
/**
* 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;
// Use wave-displaced position for horizontal/diagonal waves
let x = char.originalPosition.x;
let y = char.originalPosition.y;
if (this.options.direction !== "vertical" && state.phase === "waving") {
y = Math.round(state.currentWaveY);
}
else if (this.options.direction === "vertical" && state.phase === "waving") {
// For vertical waves, displace horizontally
x = Math.round(char.originalPosition.x +
Math.sin(state.currentWaveY * Math.PI * this.options.waveFrequency) *
this.options.waveAmplitude);
}
if (y >= 0 && y < dimensions.height && x >= 0 && x < dimensions.width) {
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 wave position
this.wavePosition += this.options.waveSpeed;
// Update character states
for (const [key, state] of this.characterStates) {
const [_x, _y] = key.split(",").map(Number);
// Calculate wave influence
const waveDistance = Math.abs(this.wavePosition - state.waveOffset);
const waveInfluence = Math.max(0, 1 - waveDistance * 2);
if (waveInfluence > 0 && state.phase !== "complete") {
state.phase = "waving";
state.waveIntensity = waveInfluence;
// Calculate wave displacement
const wavePhase = (state.waveOffset + this.wavePosition) *
Math.PI * 2 * this.options.waveFrequency;
const waveHeight = Math.sin(wavePhase) * this.options.waveAmplitude * waveInfluence;
if (this.options.direction !== "vertical") {
state.currentWaveY = state.originalY + waveHeight;
}
else {
// For vertical waves, we'll use this for horizontal displacement
state.currentWaveY = wavePhase;
}
// Update color based on wave position
if (this.gradient) {
const spectrum = this.gradient.getSpectrum();
state.colorIndex = Math.floor(waveInfluence * (spectrum.length - 1));
}
else {
state.colorIndex = Math.floor(waveInfluence * (this.options.waveColors.length - 1));
}
}
else if (state.phase === "waving" && waveInfluence === 0) {
// Start settling
state.phase = "settling";
}
// Handle settling phase
if (state.phase === "settling") {
state.settleProgress += 0.05;
state.currentWaveY = state.originalY +
(state.currentWaveY - state.originalY) * (1 - state.settleProgress);
if (state.settleProgress >= 1) {
state.phase = "complete";
state.currentWaveY = state.originalY;
}
}
}
// Check if effect is complete
this._isEffectComplete = progress >= 1 ||
Array.from(this.characterStates.values()).every(s => s.phase === "complete");
}
/**
* Create visual representation of a character
*/
createCharacterVisual(char, state) {
let color = this.options.finalColor;
switch (state.phase) {
case "waiting":
// Use dimmed color while waiting
color = "404040";
break;
case "waving":
// Use wave color based on intensity
if (this.gradient) {
const spectrum = this.gradient.getSpectrum();
color = spectrum[state.colorIndex] || spectrum[spectrum.length - 1];
}
else {
color = this.options.waveColors[state.colorIndex] ||
this.options.waveColors[this.options.waveColors.length - 1];
}
break;
case "settling":
// Interpolate from wave color to final color
const waveColor = this.gradient
? this.gradient.getSpectrum()[0]
: this.options.waveColors[0];
color = this.interpolateColor(waveColor, this.options.finalColor, state.settleProgress);
break;
case "complete":
color = this.options.finalColor;
break;
}
return {
symbol: char.originalSymbol,
colors: {
fg: color,
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.wavePosition = -0.5;
this.animationFrameCount = 0;
this._isEffectComplete = false;
this.onInitialize();
}
}
//# sourceMappingURL=waves.js.map