456 lines
12 KiB
JavaScript
456 lines
12 KiB
JavaScript
|
|
/* ==========================================================================
|
||
|
|
SYSTEM AWAKENING - Animation Coordinator Module
|
||
|
|
Manages and coordinates all animations across the system
|
||
|
|
========================================================================== */
|
||
|
|
|
||
|
|
import {
|
||
|
|
ANIMATION,
|
||
|
|
NAV_CONFIG
|
||
|
|
} from './config.js';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Animation Coordinator Class
|
||
|
|
* Manages all animations and their synchronization
|
||
|
|
*/
|
||
|
|
export class AnimationCoordinator {
|
||
|
|
/**
|
||
|
|
* Create a new animation coordinator
|
||
|
|
* @param {Object} components - All animation components
|
||
|
|
*/
|
||
|
|
constructor(components = {}) {
|
||
|
|
// Component references
|
||
|
|
this.particleSystem = components.particleSystem;
|
||
|
|
this.progressCircles = components.progressCircles;
|
||
|
|
this.terminal = components.terminal;
|
||
|
|
this.typewriter = components.typewriter;
|
||
|
|
|
||
|
|
// Animation state
|
||
|
|
this.lastFrameTime = 0;
|
||
|
|
this.animationId = null;
|
||
|
|
this.isRunning = false;
|
||
|
|
|
||
|
|
// Scroll tracking
|
||
|
|
this.currentSection = 'hero';
|
||
|
|
this.scrollObserver = null;
|
||
|
|
|
||
|
|
// Performance monitoring
|
||
|
|
this.frameCount = 0;
|
||
|
|
this.lastFpsUpdate = 0;
|
||
|
|
this.currentFps = 60;
|
||
|
|
|
||
|
|
// Initialize
|
||
|
|
this.init();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize the animation coordinator
|
||
|
|
*/
|
||
|
|
init() {
|
||
|
|
// Start animation loop
|
||
|
|
this.start();
|
||
|
|
|
||
|
|
// Set up scroll animations
|
||
|
|
this.initScrollAnimations();
|
||
|
|
|
||
|
|
// Set up navigation tracking
|
||
|
|
this.initNavigationTracking();
|
||
|
|
|
||
|
|
// Start performance monitoring
|
||
|
|
this.startPerformanceMonitoring();
|
||
|
|
|
||
|
|
console.log('Animation coordinator initialized');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Start the main animation loop
|
||
|
|
*/
|
||
|
|
start() {
|
||
|
|
if (this.isRunning) return;
|
||
|
|
|
||
|
|
this.isRunning = true;
|
||
|
|
this.lastFrameTime = performance.now();
|
||
|
|
|
||
|
|
const animate = (timestamp) => {
|
||
|
|
if (!this.isRunning) return;
|
||
|
|
|
||
|
|
// Calculate delta time
|
||
|
|
const deltaTime = (timestamp - this.lastFrameTime) / 1000; // Convert to seconds
|
||
|
|
this.lastFrameTime = timestamp;
|
||
|
|
|
||
|
|
// Update frame count for FPS calculation
|
||
|
|
this.frameCount++;
|
||
|
|
|
||
|
|
// Update all animations
|
||
|
|
this.updateAnimations(deltaTime);
|
||
|
|
|
||
|
|
// Render all components
|
||
|
|
this.renderAll();
|
||
|
|
|
||
|
|
// Continue animation loop
|
||
|
|
this.animationId = requestAnimationFrame(animate);
|
||
|
|
};
|
||
|
|
|
||
|
|
// Start animation loop
|
||
|
|
this.animationId = requestAnimationFrame(animate);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Stop the animation loop
|
||
|
|
*/
|
||
|
|
stop() {
|
||
|
|
this.isRunning = false;
|
||
|
|
if (this.animationId) {
|
||
|
|
cancelAnimationFrame(this.animationId);
|
||
|
|
this.animationId = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update all animations
|
||
|
|
* @param {number} deltaTime - Time since last frame in seconds
|
||
|
|
*/
|
||
|
|
updateAnimations(deltaTime) {
|
||
|
|
// Update particle system
|
||
|
|
if (this.particleSystem && this.particleSystem.update) {
|
||
|
|
this.particleSystem.update(deltaTime);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update typewriter animation is handled internally by its class
|
||
|
|
// Update terminal animation is handled internally by its class
|
||
|
|
// Update progress circles animation is handled internally by its class
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Render all components
|
||
|
|
*/
|
||
|
|
renderAll() {
|
||
|
|
// Render particle system
|
||
|
|
if (this.particleSystem && this.particleSystem.render) {
|
||
|
|
this.particleSystem.render();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Other components handle their own rendering internally
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize scroll-triggered animations
|
||
|
|
*/
|
||
|
|
initScrollAnimations() {
|
||
|
|
// Set up Intersection Observer for card animations
|
||
|
|
this.setupCardAnimations();
|
||
|
|
|
||
|
|
// Set up parallax effects
|
||
|
|
this.setupParallaxEffects();
|
||
|
|
|
||
|
|
// Update navigation on scroll
|
||
|
|
this.setupScrollTracking();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set up card reveal animations using Intersection Observer
|
||
|
|
*/
|
||
|
|
setupCardAnimations() {
|
||
|
|
const cards = document.querySelectorAll('.card');
|
||
|
|
|
||
|
|
if (!cards.length) return;
|
||
|
|
|
||
|
|
// Create observer with options
|
||
|
|
const observerOptions = {
|
||
|
|
root: null, // Use viewport as root
|
||
|
|
rootMargin: '50px', // Trigger slightly before entering viewport
|
||
|
|
threshold: 0.1 // Trigger when 10% of element is visible
|
||
|
|
};
|
||
|
|
|
||
|
|
const observer = new IntersectionObserver((entries) => {
|
||
|
|
entries.forEach(entry => {
|
||
|
|
if (entry.isIntersecting) {
|
||
|
|
// Get delay from data attribute or calculate
|
||
|
|
const delay = entry.target.dataset.delay || 0;
|
||
|
|
|
||
|
|
// Animate card in after delay
|
||
|
|
setTimeout(() => {
|
||
|
|
entry.target.classList.add('visible');
|
||
|
|
|
||
|
|
// Add slight stagger for visual interest
|
||
|
|
const index = Array.from(cards).indexOf(entry.target);
|
||
|
|
const staggerDelay = index * 0.05; // 50ms per card
|
||
|
|
|
||
|
|
setTimeout(() => {
|
||
|
|
// Add hover-ready class after animation completes
|
||
|
|
entry.target.classList.add('animation-complete');
|
||
|
|
}, staggerDelay * 1000);
|
||
|
|
}, delay * 1000);
|
||
|
|
|
||
|
|
// Unobserve after animation starts
|
||
|
|
observer.unobserve(entry.target);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}, observerOptions);
|
||
|
|
|
||
|
|
// Observe all cards
|
||
|
|
cards.forEach(card => observer.observe(card));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set up parallax effects for depth
|
||
|
|
*/
|
||
|
|
setupParallaxEffects() {
|
||
|
|
// Simple parallax for hero section
|
||
|
|
const heroSection = document.getElementById('hero');
|
||
|
|
|
||
|
|
if (!heroSection) return;
|
||
|
|
|
||
|
|
// Use scroll event with throttling
|
||
|
|
let lastScrollY = window.scrollY;
|
||
|
|
let ticking = false;
|
||
|
|
|
||
|
|
const updateParallax = () => {
|
||
|
|
const scrollY = window.scrollY;
|
||
|
|
|
||
|
|
// Calculate parallax offset (subtle effect)
|
||
|
|
const parallaxOffset = scrollY * ANIMATION.PARALLAX_SPEED;
|
||
|
|
|
||
|
|
// Apply transform to hero section
|
||
|
|
heroSection.style.transform = `translateY(${parallaxOffset}px)`;
|
||
|
|
|
||
|
|
// Update last scroll position
|
||
|
|
lastScrollY = scrollY;
|
||
|
|
ticking = false;
|
||
|
|
};
|
||
|
|
|
||
|
|
const onScroll = () => {
|
||
|
|
if (!ticking) {
|
||
|
|
requestAnimationFrame(updateParallax);
|
||
|
|
ticking = true;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Add scroll listener
|
||
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set up scroll tracking for section navigation
|
||
|
|
*/
|
||
|
|
setupScrollTracking() {
|
||
|
|
// Throttle scroll events for performance
|
||
|
|
let ticking = false;
|
||
|
|
|
||
|
|
const updateCurrentSection = () => {
|
||
|
|
// Get all sections
|
||
|
|
const sections = NAV_CONFIG.SECTIONS.map(s => s.id);
|
||
|
|
const scrollPosition = window.scrollY + 100; // Offset for nav
|
||
|
|
|
||
|
|
// Find current section
|
||
|
|
for (const sectionId of sections) {
|
||
|
|
const section = document.getElementById(sectionId);
|
||
|
|
if (section) {
|
||
|
|
const sectionTop = section.offsetTop;
|
||
|
|
const sectionHeight = section.offsetHeight;
|
||
|
|
|
||
|
|
if (scrollPosition >= sectionTop &&
|
||
|
|
scrollPosition < sectionTop + sectionHeight) {
|
||
|
|
|
||
|
|
if (this.currentSection !== sectionId) {
|
||
|
|
this.currentSection = sectionId;
|
||
|
|
this.updateNavigation();
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
ticking = false;
|
||
|
|
};
|
||
|
|
|
||
|
|
const onScroll = () => {
|
||
|
|
if (!ticking) {
|
||
|
|
requestAnimationFrame(updateCurrentSection);
|
||
|
|
ticking = true;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Add scroll listener
|
||
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize navigation section tracking
|
||
|
|
*/
|
||
|
|
initNavigationTracking() {
|
||
|
|
// Update navigation on page load
|
||
|
|
this.updateNavigation();
|
||
|
|
|
||
|
|
// Set up click handlers for navigation
|
||
|
|
this.setupNavigationClickHandlers();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update navigation UI based on current section
|
||
|
|
*/
|
||
|
|
updateNavigation() {
|
||
|
|
const navLinks = document.querySelectorAll('.nav-link');
|
||
|
|
|
||
|
|
navLinks.forEach(link => {
|
||
|
|
const sectionId = link.getAttribute('href').substring(1);
|
||
|
|
|
||
|
|
if (sectionId === this.currentSection) {
|
||
|
|
link.classList.add('active');
|
||
|
|
} else {
|
||
|
|
link.classList.remove('active');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set up click handlers for navigation links
|
||
|
|
*/
|
||
|
|
setupNavigationClickHandlers() {
|
||
|
|
const navLinks = document.querySelectorAll('.nav-link');
|
||
|
|
|
||
|
|
navLinks.forEach(link => {
|
||
|
|
link.addEventListener('click', (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
|
||
|
|
const targetId = link.getAttribute('href').substring(1);
|
||
|
|
const targetSection = document.getElementById(targetId);
|
||
|
|
|
||
|
|
if (targetSection) {
|
||
|
|
// Smooth scroll to section
|
||
|
|
targetSection.scrollIntoView({
|
||
|
|
behavior: 'smooth',
|
||
|
|
block: 'start'
|
||
|
|
});
|
||
|
|
|
||
|
|
// Update current section
|
||
|
|
this.currentSection = targetId;
|
||
|
|
this.updateNavigation();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Start performance monitoring
|
||
|
|
*/
|
||
|
|
startPerformanceMonitoring() {
|
||
|
|
// Update FPS every second
|
||
|
|
setInterval(() => {
|
||
|
|
this.updateFps();
|
||
|
|
}, 1000);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update FPS calculation
|
||
|
|
*/
|
||
|
|
updateFps() {
|
||
|
|
const now = performance.now();
|
||
|
|
const elapsed = now - this.lastFpsUpdate;
|
||
|
|
|
||
|
|
if (elapsed >= 1000) {
|
||
|
|
this.currentFps = Math.round((this.frameCount * 1000) / elapsed);
|
||
|
|
this.frameCount = 0;
|
||
|
|
this.lastFpsUpdate = now;
|
||
|
|
|
||
|
|
// Adaptive quality based on FPS
|
||
|
|
this.adjustQualityForPerformance();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Adjust animation quality based on performance
|
||
|
|
*/
|
||
|
|
adjustQualityForPerformance() {
|
||
|
|
// Only adjust if adaptive quality is enabled
|
||
|
|
// This is a placeholder for more sophisticated quality adjustment
|
||
|
|
if (this.currentFps < 30) {
|
||
|
|
console.warn(`Low FPS detected: ${this.currentFps}. Consider reducing particle count.`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get current FPS
|
||
|
|
* @returns {number} Current frames per second
|
||
|
|
*/
|
||
|
|
getFps() {
|
||
|
|
return this.currentFps;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get current section ID
|
||
|
|
* @returns {string} Current section ID
|
||
|
|
*/
|
||
|
|
getCurrentSection() {
|
||
|
|
return this.currentSection;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if animation loop is running
|
||
|
|
* @returns {boolean} True if running
|
||
|
|
*/
|
||
|
|
isAnimationRunning() {
|
||
|
|
return this.isRunning;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Pause all animations
|
||
|
|
*/
|
||
|
|
pause() {
|
||
|
|
this.stop();
|
||
|
|
|
||
|
|
// Pause individual components
|
||
|
|
if (this.typewriter && this.typewriter.pause) {
|
||
|
|
this.typewriter.pause();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.terminal) {
|
||
|
|
// Terminal auto-logging is handled by interval
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('Animations paused');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Resume all animations
|
||
|
|
*/
|
||
|
|
resume() {
|
||
|
|
this.start();
|
||
|
|
|
||
|
|
// Resume individual components
|
||
|
|
if (this.typewriter && this.typewriter.resume) {
|
||
|
|
this.typewriter.resume();
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('Animations resumed');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Clean up all resources
|
||
|
|
*/
|
||
|
|
dispose() {
|
||
|
|
// Stop animation loop
|
||
|
|
this.stop();
|
||
|
|
|
||
|
|
// Dispose individual components
|
||
|
|
if (this.particleSystem && this.particleSystem.dispose) {
|
||
|
|
this.particleSystem.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.progressCircles && this.progressCircles.dispose) {
|
||
|
|
this.progressCircles.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.terminal && this.terminal.dispose) {
|
||
|
|
this.terminal.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.typewriter && this.typewriter.dispose) {
|
||
|
|
this.typewriter.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Remove event listeners
|
||
|
|
window.removeEventListener('scroll', this.handleScroll);
|
||
|
|
|
||
|
|
console.log('Animation coordinator disposed');
|
||
|
|
}
|
||
|
|
}
|