// modules/canvas-animations/ScrollEffects.js import { CanvasManager } from './CanvasManager.js'; import { useEvent } from '../../core/useEvent.js'; import { Logger } from '../../core/logger.js'; /** * Scroll-based Canvas Effects - Parallax and scroll animations */ export const ScrollEffects = { /** * Initialize parallax canvas effect */ initParallax(canvas, config) { const manager = new CanvasManager(canvas); const elements = this.createParallaxElements(canvas, config); let ticking = false; const updateParallax = () => { if (!ticking) { requestAnimationFrame(() => { this.renderParallax(manager, elements, config); ticking = false; }); ticking = true; } }; // Listen to scroll events useEvent(window, 'scroll', updateParallax, 'scroll-parallax'); useEvent(window, 'resize', updateParallax, 'scroll-parallax'); // Initial render updateParallax(); Logger.info('[ScrollEffects] Parallax initialized with', elements.length, 'elements'); }, /** * Create parallax elements based on config */ createParallaxElements(canvas, config) { const elements = []; const layerCount = config.layers || 3; const elementCount = config.elements || 20; for (let i = 0; i < elementCount; i++) { elements.push({ x: Math.random() * canvas.clientWidth, y: Math.random() * canvas.clientHeight * 2, // Allow elements outside viewport size: Math.random() * 20 + 5, layer: Math.floor(Math.random() * layerCount), speed: 0.1 + (Math.random() * 0.5), // Different parallax speeds opacity: Math.random() * 0.7 + 0.3, color: this.getLayerColor(Math.floor(Math.random() * layerCount), config) }); } return elements; }, /** * Get color for parallax layer */ getLayerColor(layer, config) { const colors = config.colors || [ 'rgba(100, 150, 255, 0.6)', // Front layer - more opaque 'rgba(150, 100, 255, 0.4)', // Middle layer 'rgba(200, 100, 150, 0.2)' // Back layer - more transparent ]; return colors[layer] || colors[0]; }, /** * Render parallax effect */ renderParallax(manager, elements, config) { manager.clear(); const scrollY = window.pageYOffset; const canvasRect = manager.canvas.getBoundingClientRect(); const canvasTop = canvasRect.top + scrollY; // Calculate relative scroll position const relativeScroll = scrollY - canvasTop; const scrollProgress = relativeScroll / window.innerHeight; elements.forEach(element => { // Apply parallax offset based on layer and scroll const parallaxOffset = scrollProgress * element.speed * 100; const y = element.y - parallaxOffset; // Only render elements that are potentially visible if (y > -element.size && y < manager.canvas.clientHeight + element.size) { manager.ctx.save(); manager.ctx.globalAlpha = element.opacity; manager.ctx.fillStyle = element.color; manager.ctx.beginPath(); manager.ctx.arc(element.x, y, element.size, 0, Math.PI * 2); manager.ctx.fill(); manager.ctx.restore(); } }); }, /** * Initialize scroll-based animations */ initScrollAnimation(canvas, config) { const manager = new CanvasManager(canvas); const animationType = config.animation || 'wave'; let ticking = false; const updateAnimation = () => { if (!ticking) { requestAnimationFrame(() => { this.renderScrollAnimation(manager, animationType, config); ticking = false; }); ticking = true; } }; useEvent(window, 'scroll', updateAnimation, 'scroll-animation'); useEvent(window, 'resize', updateAnimation, 'scroll-animation'); // Initial render updateAnimation(); Logger.info('[ScrollEffects] Scroll animation initialized:', animationType); }, /** * Render scroll-based animations */ renderScrollAnimation(manager, animationType, config) { manager.clear(); const scrollY = window.pageYOffset; const canvasRect = manager.canvas.getBoundingClientRect(); const canvasTop = canvasRect.top + scrollY; const relativeScroll = scrollY - canvasTop; const scrollProgress = Math.max(0, Math.min(1, relativeScroll / window.innerHeight)); switch (animationType) { case 'wave': this.renderWaveAnimation(manager, scrollProgress, config); break; case 'progress': this.renderProgressAnimation(manager, scrollProgress, config); break; case 'morph': this.renderMorphAnimation(manager, scrollProgress, config); break; default: this.renderWaveAnimation(manager, scrollProgress, config); } }, /** * Render wave animation based on scroll */ renderWaveAnimation(manager, progress, config) { const { ctx } = manager; const { width, height } = manager.getSize(); ctx.strokeStyle = config.color || 'rgba(100, 150, 255, 0.8)'; ctx.lineWidth = config.lineWidth || 3; ctx.beginPath(); const amplitude = (config.amplitude || 50) * progress; const frequency = config.frequency || 0.02; const phase = progress * Math.PI * 2; for (let x = 0; x <= width; x += 2) { const y = height / 2 + Math.sin(x * frequency + phase) * amplitude; if (x === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } } ctx.stroke(); }, /** * Render progress bar animation */ renderProgressAnimation(manager, progress, config) { const { ctx } = manager; const { width, height } = manager.getSize(); const barHeight = config.barHeight || 10; const y = height / 2 - barHeight / 2; // Background ctx.fillStyle = config.backgroundColor || 'rgba(255, 255, 255, 0.2)'; ctx.fillRect(0, y, width, barHeight); // Progress ctx.fillStyle = config.color || 'rgba(100, 150, 255, 0.8)'; ctx.fillRect(0, y, width * progress, barHeight); }, /** * Render morphing shapes */ renderMorphAnimation(manager, progress, config) { const { ctx } = manager; const { width, height } = manager.getSize(); ctx.fillStyle = config.color || 'rgba(100, 150, 255, 0.6)'; const centerX = width / 2; const centerY = height / 2; const maxRadius = Math.min(width, height) / 3; ctx.beginPath(); const points = config.points || 6; for (let i = 0; i <= points; i++) { const angle = (i / points) * Math.PI * 2; const radiusVariation = Math.sin(progress * Math.PI * 4 + angle * 3) * 0.3 + 1; const radius = maxRadius * progress * radiusVariation; const x = centerX + Math.cos(angle) * radius; const y = centerY + Math.sin(angle) * radius; if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } } ctx.closePath(); ctx.fill(); } };