/* ========================================================================== SYSTEM AWAKENING - Progress Circles Module Canvas-based animated progress rings for system metrics ========================================================================== */ import { COLORS, METRICS_CONFIG, ANIMATION, generateRandomTargets, getRandomValue } from './config.js'; /** * Progress Circles Class * Manages the animated progress rings for the data core section */ export class ProgressCircles { /** * Create a new progress circles manager * @param {Array} canvases - Array of canvas elements */ constructor(canvases) { this.canvases = canvases; this.ctxs = canvases.map(canvas => canvas.getContext('2d')); // Animation state this.currentValues = [0, 0, 0]; this.targetValues = generateRandomTargets(); this.animationStartTime = null; this.isAnimating = false; this.animationId = null; // Performance tracking this.lastFrameTime = 0; this.fps = 60; // Initialize this.init(); } /** * Initialize the progress circles */ init() { // Set up canvas dimensions this.resize(); // Draw initial state this.drawAll(); // Start animation after a short delay setTimeout(() => { this.animateToTargets(); }, 1000); // Bind resize event this.bindResize(); console.log('Progress circles initialized'); } /** * Resize all canvases to match display size */ resize() { this.canvases.forEach((canvas, index) => { const displayWidth = canvas.clientWidth; const displayHeight = canvas.clientHeight; // Check if canvas needs resizing if (canvas.width !== displayWidth || canvas.height !== displayHeight) { canvas.width = displayWidth; canvas.height = displayHeight; // Redraw this circle this.drawRing(this.ctxs[index], this.currentValues[index], index); } }); } /** * Bind resize event listener */ bindResize() { let resizeTimeout; const handleResize = () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { this.resize(); }, 250); }; window.addEventListener('resize', handleResize); } /** * Animate all circles to their target values * @param {number} duration - Animation duration in milliseconds */ animateToTargets(duration = ANIMATION.PROGRESS_DURATION) { if (this.isAnimating) { return; } this.isAnimating = true; this.animationStartTime = Date.now(); const animate = (currentTime) => { if (!this.isAnimating) return; // Calculate progress (0 to 1) const elapsed = currentTime - this.animationStartTime; let progress = Math.min(elapsed / duration, 1); // Apply easing function (easeOutCubic) progress = 1 - Math.pow(1 - progress, 3); // Update current values this.currentValues = this.currentValues.map((current, index) => { const target = this.targetValues[index]; return current + (target - current) * progress; }); // Draw all circles this.drawAll(); // Update metric displays this.updateMetricDisplays(); if (progress < 1) { // Continue animation this.animationId = requestAnimationFrame(() => { animate(Date.now()); }); } else { // Animation complete this.animationComplete(); } }; // Start animation this.animationId = requestAnimationFrame(() => { animate(Date.now()); }); } /** * Handle animation completion */ animationComplete() { this.isAnimating = false; this.animationId = null; // Update status to "complete" this.updateStatusDisplays(METRICS_CONFIG.COMPLETE_STATUSES); // Generate new random targets after delay setTimeout(() => { this.targetValues = generateRandomTargets(); // Reset status to "initializing" this.updateStatusDisplays(METRICS_CONFIG.STATUSES); // Start new animation this.animateToTargets(); }, ANIMATION.PROGRESS_UPDATE_DELAY); } /** * Draw all progress rings */ drawAll() { this.ctxs.forEach((ctx, index) => { this.drawRing(ctx, this.currentValues[index], index); }); } /** * Draw a single progress ring * @param {CanvasRenderingContext2D} ctx - Canvas context * @param {number} value - Current value (0-100) * @param {number} index - Circle index (0=compute, 1=storage, 2=sync) */ drawRing(ctx, value, index) { const canvas = ctx.canvas; const width = canvas.width; const height = canvas.height; const centerX = width / 2; const centerY = height / 2; // Calculate radius based on canvas size const radius = Math.min(width, height) * 0.35; const lineWidth = Math.min(width, height) * 0.05; // Clear canvas with transparency ctx.clearRect(0, 0, width, height); // Draw background circle ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; ctx.lineWidth = lineWidth; ctx.stroke(); // Draw progress arc const endAngle = (value / 100) * Math.PI * 2; ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, endAngle); // Create gradient based on metric type const gradientColors = COLORS.RING_GRADIENTS[index]; const gradient = ctx.createLinearGradient( centerX - radius, centerY, centerX + radius, centerY ); gradient.addColorStop(0, gradientColors[0]); gradient.addColorStop(1, gradientColors[1]); ctx.strokeStyle = gradient; ctx.lineWidth = lineWidth; ctx.lineCap = 'round'; ctx.stroke(); // Add glow effect with shadow ctx.shadowBlur = 20; ctx.shadowColor = gradientColors[0]; ctx.stroke(); ctx.shadowBlur = 0; // Draw inner circle for depth effect ctx.beginPath(); ctx.arc(centerX, centerY, radius * 0.7, 0, Math.PI * 2); ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; ctx.fill(); // Draw pulse effect when near completion if (value > 95 && this.isAnimating) { this.drawPulseEffect(ctx, centerX, centerY, radius, gradientColors[0]); } } /** * Draw a pulse effect for completed rings * @param {CanvasRenderingContext2D} ctx - Canvas context * @param {number} centerX - Center X coordinate * @param {number} centerY - Center Y coordinate * @param {number} radius - Base radius * @param {string} color - Pulse color */ drawPulseEffect(ctx, centerX, centerY, radius, color) { const pulseTime = Date.now() % 2000 / 2000; // 2 second cycle // Draw multiple concentric circles with varying opacity for (let i = 0; i < 3; i++) { const pulsePhase = (pulseTime + i * 0.2) % 1; const pulseRadius = radius + pulsePhase * 30; const pulseOpacity = (1 - pulsePhase) * 0.3; ctx.beginPath(); ctx.arc(centerX, centerY, pulseRadius, 0, Math.PI * 2); ctx.strokeStyle = color.replace(')', `, ${pulseOpacity})`).replace('rgb', 'rgba'); ctx.lineWidth = 2; ctx.stroke(); } } /** * Update the displayed metric values */ updateMetricDisplays() { // Find metric display elements const metricValues = document.querySelectorAll('.metric-value'); if (metricValues.length === 3) { metricValues.forEach((element, index) => { const value = Math.round(this.currentValues[index]); // Update the displayed value const numberSpan = element.querySelector('span:first-child') || element; const unitSpan = element.querySelector('.unit'); // If it's just a text node, update it if (element.childNodes.length === 1 && element.firstChild.nodeType === 3) { element.textContent = value + (unitSpan ? unitSpan.textContent : ''); } else { // Update the numeric part const numericText = element.childNodes[0]; if (numericText.nodeType === 3) { numericText.textContent = value; } } }); } } /** * Update the status text displays * @param {Array} statuses - Array of status texts */ updateStatusDisplays(statuses) { const statusElements = document.querySelectorAll('.metric-status'); if (statusElements.length === 3) { statusElements.forEach((element, index) => { if (statuses[index]) { element.textContent = statuses[index]; // Add completion styling for final status if (statuses === METRICS_CONFIG.COMPLETE_STATUSES) { element.style.color = COLORS.PRIMARY; element.style.textShadow = COLORS.PRIMARY + ' 0 0 10px'; } else { element.style.color = COLORS.ACCENT; element.style.textShadow = 'none'; } } }); } } /** * Generate new random target values and update displays */ generateNewTargets() { // Generate new random values this.targetValues = generateRandomTargets(); // Reset current values to 0 this.currentValues = [0, 0, 0]; // Update target displays this.updateTargetDisplays(); // Redraw circles at 0 this.drawAll(); // Start new animation after short delay setTimeout(() => { this.animateToTargets(); }, 500); } /** * Update the displayed target values (for debugging or advanced UI) */ updateTargetDisplays() { // This could be used to show target values alongside current values // For now, we'll just log them for debugging console.log('New target values:', this.targetValues); } /** * Manually set target values (for testing or specific scenarios) * @param {Array} targets - Array of target values [compute, storage, sync] */ setTargetValues(targets) { if (targets.length === 3) { this.targetValues = targets.slice(); // Stop current animation if (this.isAnimating) { cancelAnimationFrame(this.animationId); this.isAnimating = false; } // Start new animation this.animateToTargets(); } } /** * Get the current progress values * @returns {Array} Current values [compute, storage, sync] */ getCurrentValues() { return this.currentValues.slice(); } /** * Get the target progress values * @returns {Array} Target values [compute, storage, sync] */ getTargetValues() { return this.targetValues.slice(); } /** * Check if animation is currently running * @returns {boolean} True if animating */ isAnimationRunning() { return this.isAnimating; } /** * Clean up resources */ dispose() { // Stop animation if (this.isAnimating) { cancelAnimationFrame(this.animationId); this.isAnimating = false; } // Clear canvases this.ctxs.forEach(ctx => { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); }); console.log('Progress circles disposed'); } }