@kya-os/cli
Version:
CLI for MCP-I setup and management
394 lines • 14.9 kB
JavaScript
/**
* Decrypt Effect
* Movie-style text decryption effect with typing animation
*/
import { BaseEffect, } from "../types.js";
import { ANSI } from "../utils.js";
/**
* Decrypt effect implementation
*/
export class DecryptEffect extends BaseEffect {
constructor(options = {}) {
super();
this.name = "Decrypt";
this.description = "Movie-style text decryption effect with typing animation";
this.characterStates = new Map();
this.animationFrameCount = 0;
this.totalFrames = 0;
this.typingQueue = [];
this.activeTypingChars = new Set();
this.decryptingChars = new Set();
this._isEffectComplete = false;
// Character sets for encryption
this.encryptedSymbols = [];
this.typingSymbols = ["▉", "▓", "▒", "░"];
this.options = {
duration: 5000,
typingSpeed: 1,
ciphertextColors: ["008000", "00cb00", "00ff00"],
finalColor: "eda000",
useGradient: true,
gradientDirection: "vertical",
typingProbability: 0.75,
fastReveal: false,
revealCompletionRatio: 0.3,
...options,
};
this.initializeEncryptedSymbols();
}
/**
* Initialize the pool of encrypted symbols
*/
initializeEncryptedSymbols() {
// Keyboard characters
for (let i = 33; i < 127; i++) {
this.encryptedSymbols.push(String.fromCharCode(i));
}
// Box drawing characters
for (let i = 9472; i < 9599; i++) {
this.encryptedSymbols.push(String.fromCharCode(i));
}
// Block characters
for (let i = 9608; i < 9632; i++) {
this.encryptedSymbols.push(String.fromCharCode(i));
}
// Misc symbols
for (let i = 174; i < 452; i++) {
this.encryptedSymbols.push(String.fromCharCode(i));
}
}
/**
* Initialize the effect
*/
onInitialize() {
this.totalFrames = Math.floor((this.options.duration / 1000) * this.config.frameRate);
// Initialize character states
this.characterStates.clear();
this.typingQueue = [...this.characters];
this.activeTypingChars.clear();
this.decryptingChars.clear();
// Shuffle typing queue for random order
this.shuffleArray(this.typingQueue);
// Calculate final colors for gradient effect
const dimensions = this.getCanvasDimensions();
const gradientColors = this.calculateGradientColors(dimensions);
for (const char of this.characters) {
const key = `${char.originalPosition.x},${char.originalPosition.y}`;
let finalColor = this.options.finalColor;
if (this.options.useGradient) {
const gradientKey = this.options.gradientDirection === "vertical"
? char.originalPosition.y
: char.originalPosition.x;
finalColor = gradientColors[gradientKey] || this.options.finalColor;
}
this.characterStates.set(key, {
phase: "hidden",
typingFrame: 0,
decryptStartTime: -1,
currentSymbol: char.originalSymbol,
currentColor: this.getRandomCiphertextColor(),
finalColor,
decryptSpeed: Math.floor(Math.random() * 3) + 2,
decryptChangesRemaining: Math.floor(Math.random() * 15) + 80,
discoveryFrame: 0,
});
}
this.animationFrameCount = 0;
this._isEffectComplete = false;
}
/**
* Calculate gradient colors based on dimensions
*/
calculateGradientColors(dimensions) {
const gradientMap = {};
const size = this.options.gradientDirection === "vertical"
? dimensions.height
: dimensions.width;
// Simple linear gradient from ciphertext color to final color
const startColor = this.options.ciphertextColors[this.options.ciphertextColors.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;
}
/**
* 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 && state.phase !== "hidden") {
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 animation state for all characters
*/
updateAnimationState() {
// Phase 1: Typing animation
if (this.typingQueue.length > 0 || this.activeTypingChars.size > 0) {
this.updateTypingPhase();
}
else if (this.decryptingChars.size === 0) {
// Transition to decryption phase
for (const char of this.characters) {
const key = `${char.originalPosition.x},${char.originalPosition.y}`;
const state = this.characterStates.get(key);
if (state) {
state.phase = "fast_decrypt";
state.decryptStartTime = this.animationFrameCount;
this.decryptingChars.add(key);
}
}
}
// Phase 2: Decryption animation
if (this.decryptingChars.size > 0) {
this.updateDecryptionPhase();
}
// Check if effect is complete
this._isEffectComplete = this.checkCompletion();
}
/**
* Update typing phase animation
*/
updateTypingPhase() {
// Start typing new characters
if (this.typingQueue.length > 0 && Math.random() < this.options.typingProbability) {
for (let i = 0; i < this.options.typingSpeed && this.typingQueue.length > 0; i++) {
const char = this.typingQueue.shift();
const key = `${char.originalPosition.x},${char.originalPosition.y}`;
const state = this.characterStates.get(key);
if (state) {
state.phase = "typing";
state.typingFrame = 0;
this.activeTypingChars.add(key);
}
}
}
// Update active typing characters
for (const key of this.activeTypingChars) {
const state = this.characterStates.get(key);
if (state) {
state.typingFrame++;
// Complete typing animation after showing all typing symbols
if (state.typingFrame > this.typingSymbols.length * 2 + 4) {
state.phase = "fast_decrypt";
this.activeTypingChars.delete(key);
}
}
}
}
/**
* Update decryption phase animation
*/
updateDecryptionPhase() {
const completedKeys = [];
for (const key of this.decryptingChars) {
const state = this.characterStates.get(key);
if (!state)
continue;
const framesSinceStart = this.animationFrameCount - state.decryptStartTime;
if (state.phase === "fast_decrypt") {
// Fast decrypt phase - rapid symbol changes
if (framesSinceStart % 3 === 0) {
state.currentSymbol = this.getRandomEncryptedSymbol();
state.decryptChangesRemaining--;
}
// Transition to slow decrypt after initial burst
if (state.decryptChangesRemaining <= 15) {
state.phase = "slow_decrypt";
state.decryptSpeed = Math.floor(Math.random() * 50) + 25;
}
}
else if (state.phase === "slow_decrypt") {
// Slow decrypt phase - less frequent changes
if (framesSinceStart % state.decryptSpeed === 0) {
state.currentSymbol = this.getRandomEncryptedSymbol();
state.decryptChangesRemaining--;
// 30% chance of extra long duration
if (Math.random() < 0.3) {
state.decryptSpeed = Math.floor(Math.random() * 75) + 50;
}
else {
state.decryptSpeed = Math.floor(Math.random() * 5) + 5;
}
}
// Transition to discovered
if (state.decryptChangesRemaining <= 0) {
state.phase = "discovered";
state.discoveryFrame = 0;
const [x, y] = key.split(",").map(Number);
const char = this.characters.find(c => c.originalPosition.x === x && c.originalPosition.y === y);
if (char) {
state.currentSymbol = char.originalSymbol;
}
}
}
else if (state.phase === "discovered") {
// Discovery fade-in effect
state.discoveryFrame++;
if (state.discoveryFrame > 10) {
completedKeys.push(key);
}
}
}
// Remove completed characters from decrypting set
for (const key of completedKeys) {
this.decryptingChars.delete(key);
}
}
/**
* Create visual representation of a character
*/
createCharacterVisual(char, state) {
let symbol = char.originalSymbol;
let color = state.currentColor;
switch (state.phase) {
case "typing":
// Typing animation with block characters
const typingIndex = Math.floor(state.typingFrame / 2) % this.typingSymbols.length;
symbol = state.typingFrame < this.typingSymbols.length * 2
? this.typingSymbols[typingIndex]
: state.currentSymbol;
color = this.getRandomCiphertextColor();
break;
case "fast_decrypt":
case "slow_decrypt":
symbol = state.currentSymbol;
color = state.currentColor;
break;
case "discovered":
// Fade from white to final color
const fadeFactor = Math.min(state.discoveryFrame / 10, 1);
color = this.interpolateColor("ffffff", state.finalColor, fadeFactor);
symbol = char.originalSymbol;
break;
}
return {
symbol,
colors: {
fg: color,
bg: null,
},
};
}
/**
* Get a random ciphertext color
*/
getRandomCiphertextColor() {
return this.options.ciphertextColors[Math.floor(Math.random() * this.options.ciphertextColors.length)];
}
/**
* Get a random encrypted symbol
*/
getRandomEncryptedSymbol() {
return this.encryptedSymbols[Math.floor(Math.random() * this.encryptedSymbols.length)];
}
/**
* Check if the effect is complete
*/
checkCompletion() {
if (this.animationFrameCount >= this.totalFrames) {
return true;
}
for (const state of this.characterStates.values()) {
if (state.phase !== "discovered" || state.discoveryFrame < 10) {
return false;
}
}
return true;
}
/**
* Shuffle array in place
*/
shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
/**
* 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.typingQueue = [];
this.activeTypingChars.clear();
this.decryptingChars.clear();
this.animationFrameCount = 0;
this._isEffectComplete = false;
this.onInitialize();
}
}
//# sourceMappingURL=decrypt.js.map