416 lines
12 KiB
JavaScript
416 lines
12 KiB
JavaScript
|
|
/* ==========================================================================
|
||
|
|
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');
|
||
|
|
}
|
||
|
|
}
|