UNPKG

aahook

Version:

A CLI tool that displays ASCII art when commands succeed or fail

346 lines 12.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AnimationEngine = void 0; const renderer_1 = require("./renderer"); const timing_controller_1 = require("./timing-controller"); /** * Main animation engine for rendering various animation types */ class AnimationEngine { constructor(options = {}) { this.options = options; this.interrupted = false; this.renderer = new renderer_1.Renderer(); this.timingController = new timing_controller_1.TimingController(options.timing); this.setupInterruptHandler(); } /** * Animate content with specified animation type */ async animate(content, type = 'typing') { // Check if terminal supports animation if (!this.renderer.supportsAnimation() && !this.options.preview) { // Fallback to static display this.renderer.write(Array.isArray(content) ? content.join('\n') : content); return; } this.interrupted = false; switch (type) { case 'typing': await this.animateTyping(content); break; case 'fade': await this.animateFade(content); break; case 'frames': await this.animateFrames(content); break; case 'blink': await this.animateBlink(content); break; case 'slide': await this.animateSlide(content); break; default: this.renderer.write(Array.isArray(content) ? content.join('\n') : content); } } /** * Typing animation - display characters one by one */ async animateTyping(content) { this.renderer.hideCursor(); const chars = content.split(''); const delay = Math.max(10, this.timingController.getCharDelay()); // Minimum 10ms delay for (let i = 0; i < chars.length && !this.interrupted; i++) { this.renderer.write(chars[i]); if (chars[i] !== '\n') { await this.timingController.wait(delay); } } // Ensure content ends with a newline if (!content.endsWith('\n')) { this.renderer.write('\n'); } this.renderer.showCursor(); } /** * Fade animation - display lines progressively */ async animateFade(content) { const lines = content.split('\n'); const delay = Math.max(30, this.timingController.getLineDelay()); const direction = this.options.effects?.direction || 'top'; // Hide cursor during animation this.renderer.hideCursor(); // Clear the area first for (let j = 0; j < lines.length; j++) { this.renderer.writeLine(''); } // Move back to start position for (let j = 0; j < lines.length; j++) { this.renderer.moveCursorUp(1); } // Save initial position this.renderer.savePosition(); // Determine fade order based on direction let linesToShow = []; if (direction === 'top' || direction === 'up') { // Fade from top to bottom for (let i = 0; i < lines.length && !this.interrupted; i++) { linesToShow.push(i); await this.renderFadeFrame(lines, linesToShow); await this.timingController.wait(delay); } } else if (direction === 'bottom' || direction === 'down') { // Fade from bottom to top for (let i = lines.length - 1; i >= 0 && !this.interrupted; i--) { linesToShow.unshift(i); await this.renderFadeFrame(lines, linesToShow); await this.timingController.wait(delay); } } else if (direction === 'left') { // Fade all lines character by character from left const maxLength = Math.max(...lines.map(l => l.length)); for (let charIndex = 1; charIndex <= maxLength && !this.interrupted; charIndex++) { this.renderer.restorePosition(); for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { this.renderer.clearLine(); const visiblePart = lines[lineIndex].substring(0, charIndex); this.renderer.writeLine(visiblePart); } await this.timingController.wait(delay / 2); // Faster for character fade } this.renderer.showCursor(); return; } else if (direction === 'right') { // Fade all lines character by character from right const maxLength = Math.max(...lines.map(l => l.length)); for (let charIndex = 1; charIndex <= maxLength && !this.interrupted; charIndex++) { this.renderer.restorePosition(); for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { this.renderer.clearLine(); const line = lines[lineIndex]; const visiblePart = line.substring(Math.max(0, line.length - charIndex)); const padding = ' '.repeat(Math.max(0, line.length - visiblePart.length)); this.renderer.writeLine(padding + visiblePart); } await this.timingController.wait(delay / 2); // Faster for character fade } this.renderer.showCursor(); return; } this.renderer.showCursor(); } /** * Render a single frame of fade animation */ async renderFadeFrame(lines, linesToShow) { this.renderer.restorePosition(); for (let i = 0; i < lines.length; i++) { this.renderer.clearLine(); if (linesToShow.includes(i)) { this.renderer.writeLine(lines[i]); } else { this.renderer.writeLine(''); } } } /** * Frame animation - cycle through multiple frames */ async animateFrames(frames) { if (!frames || frames.length === 0) return; const frameDelay = this.timingController.getFrameDelay(); const loops = this.options.timing?.loop || 1; this.renderer.hideCursor(); this.renderer.savePosition(); let currentLoop = 0; while (this.timingController.shouldContinueLoop(currentLoop) && !this.interrupted) { for (const frame of frames) { if (this.interrupted) break; this.renderer.restorePosition(); this.clearFrame(frame); this.renderer.write(frame); await this.timingController.wait(frameDelay); } currentLoop++; } this.renderer.showCursor(); } /** * Blink animation - make content blink */ async animateBlink(content) { const pattern = this.options.effects?.pattern; const blinkDelay = 500; // Default blink delay const loops = this.options.timing?.loop || 3; let processedContent = content; if (pattern) { // Apply blink only to matched patterns const regex = new RegExp(pattern, 'g'); processedContent = content.replace(regex, '\x1b[5m$&\x1b[25m'); this.renderer.write(processedContent); } else { // Blink entire content let currentLoop = 0; while (currentLoop < loops && !this.interrupted) { this.renderer.write(content); await this.timingController.wait(blinkDelay); this.renderer.clearLine(); this.renderer.write('\r'); await this.timingController.wait(blinkDelay); currentLoop++; } this.renderer.write(content); } } /** * Slide animation - slide content from direction */ async animateSlide(content) { const direction = this.options.effects?.direction || 'left'; const lines = content.split('\n'); const terminalSize = this.renderer.getTerminalSize(); const delay = 20; // Faster slide speed for smoother animation switch (direction) { case 'left': case 'right': await this.slideHorizontal(lines, direction, terminalSize.columns, delay); break; case 'top': case 'up': case 'bottom': case 'down': await this.slideVertical(lines, direction, delay); break; } } /** * Slide content horizontally */ async slideHorizontal(lines, direction, maxWidth, delay) { const maxLineLength = Math.max(...lines.map(l => l.length)); const steps = Math.min(maxLineLength + 10, maxWidth); // Add some padding for smooth exit // Hide cursor and save initial position this.renderer.hideCursor(); // Clear area first for (let j = 0; j < lines.length; j++) { this.renderer.clearLine(); if (j < lines.length - 1) { this.renderer.writeLine(''); } } // Move back to start position for (let j = 0; j < lines.length; j++) { this.renderer.moveCursorUp(1); } for (let i = 0; i <= steps && !this.interrupted; i++) { // Save cursor position this.renderer.savePosition(); for (const line of lines) { this.renderer.clearLine(); if (direction === 'left') { // Slide from right to left const offset = steps - i; const padding = ' '.repeat(Math.max(0, offset)); this.renderer.write(padding + line); } else { // Slide from left to right const padding = ' '.repeat(Math.max(0, i)); this.renderer.write(padding + line); } if (lines.indexOf(line) < lines.length - 1) { this.renderer.writeLine(''); } } await this.timingController.wait(delay); // Restore position for next frame this.renderer.restorePosition(); } // Final render at target position this.renderer.savePosition(); for (const line of lines) { this.renderer.clearLine(); if (direction === 'left') { this.renderer.writeLine(line); } else { const padding = ' '.repeat(Math.min(steps, maxWidth - line.length)); this.renderer.writeLine(padding + line); } } this.renderer.showCursor(); } /** * Slide content vertically */ async slideVertical(lines, direction, delay) { const isTopDirection = direction === 'top' || direction === 'up'; const orderedLines = isTopDirection ? lines : [...lines].reverse(); for (let i = 0; i < orderedLines.length && !this.interrupted; i++) { this.renderer.writeLine(orderedLines[i]); await this.timingController.wait(delay); } } /** * Get ordered lines based on direction */ getOrderedLines(lines, direction) { switch (direction) { case 'bottom': case 'down': return [...lines].reverse(); case 'left': case 'right': // For horizontal fade, still show line by line return lines; default: return lines; } } /** * Clear frame area */ clearFrame(frame) { const lines = frame.split('\n'); for (let i = 0; i < lines.length; i++) { this.renderer.clearLine(); if (i < lines.length - 1) { this.renderer.moveCursorDown(1); } } } /** * Setup interrupt handler for Ctrl+C */ setupInterruptHandler() { const handler = () => { this.interrupted = true; this.cleanup(); }; process.on('SIGINT', handler); // Store handler for cleanup this.interruptHandler = handler; } /** * Clean up resources */ cleanup() { this.renderer.cleanup(); // Remove interrupt handler if (this.interruptHandler) { process.removeListener('SIGINT', this.interruptHandler); } } } exports.AnimationEngine = AnimationEngine; //# sourceMappingURL=animation-engine.js.map