// modules/canvas-animations/InteractiveEffects.js import { CanvasManager } from './CanvasManager.js'; import { useEvent } from '../../core/useEvent.js'; import { Logger } from '../../core/logger.js'; /** * Interactive Canvas Effects - Mouse/touch interactions, hover effects */ export const InteractiveEffects = { /** * Initialize interactive canvas */ init(canvas, config) { const manager = new CanvasManager(canvas); const effectType = config.effect || 'ripple'; const state = { mouse: { x: 0, y: 0, isOver: false }, effects: [], lastTime: 0 }; this.setupInteractionEvents(canvas, manager, state, config); this.startAnimationLoop(manager, state, effectType, config); Logger.info('[InteractiveEffects] Initialized with effect:', effectType); }, /** * Setup mouse/touch interaction events */ setupInteractionEvents(canvas, manager, state, config) { // Mouse events useEvent(canvas, 'mousemove', (e) => { const pos = manager.getMousePosition(e); state.mouse.x = pos.x; state.mouse.y = pos.y; if (config.effect === 'trail') { this.addTrailPoint(state, pos.x, pos.y); } }, 'interactive-effects'); useEvent(canvas, 'mouseenter', () => { state.mouse.isOver = true; }, 'interactive-effects'); useEvent(canvas, 'mouseleave', () => { state.mouse.isOver = false; }, 'interactive-effects'); useEvent(canvas, 'click', (e) => { const pos = manager.getMousePosition(e); this.addClickEffect(state, pos.x, pos.y, config); }, 'interactive-effects'); // Touch events for mobile useEvent(canvas, 'touchstart', (e) => { e.preventDefault(); const touch = e.touches[0]; const pos = manager.getMousePosition(touch); this.addClickEffect(state, pos.x, pos.y, config); }, 'interactive-effects'); useEvent(canvas, 'touchmove', (e) => { e.preventDefault(); const touch = e.touches[0]; const pos = manager.getMousePosition(touch); state.mouse.x = pos.x; state.mouse.y = pos.y; state.mouse.isOver = true; if (config.effect === 'trail') { this.addTrailPoint(state, pos.x, pos.y); } }, 'interactive-effects'); useEvent(canvas, 'touchend', () => { state.mouse.isOver = false; }, 'interactive-effects'); }, /** * Start animation loop */ startAnimationLoop(manager, state, effectType, config) { const animate = (timestamp) => { const deltaTime = timestamp - state.lastTime; state.lastTime = timestamp; manager.clear(); switch (effectType) { case 'ripple': this.renderRippleEffect(manager, state, config); break; case 'trail': this.renderTrailEffect(manager, state, config); break; case 'particles': this.renderParticleEffect(manager, state, config, deltaTime); break; case 'magnetic': this.renderMagneticEffect(manager, state, config); break; default: this.renderRippleEffect(manager, state, config); } // Update effects this.updateEffects(state.effects, deltaTime); requestAnimationFrame(animate); }; requestAnimationFrame(animate); }, /** * Add click effect (ripple, explosion, etc.) */ addClickEffect(state, x, y, config) { const effect = { x, y, age: 0, maxAge: config.duration || 1000, type: 'click', intensity: config.intensity || 1 }; state.effects.push(effect); }, /** * Add trail point for mouse trail effect */ addTrailPoint(state, x, y) { const point = { x, y, age: 0, maxAge: 500, type: 'trail' }; state.effects.push(point); // Limit trail length const trailLength = 20; const trailPoints = state.effects.filter(e => e.type === 'trail'); if (trailPoints.length > trailLength) { const oldestIndex = state.effects.indexOf(trailPoints[0]); state.effects.splice(oldestIndex, 1); } }, /** * Render ripple effect */ renderRippleEffect(manager, state, config) { const { ctx } = manager; // Draw active ripples state.effects.forEach(effect => { if (effect.type === 'click') { const progress = effect.age / effect.maxAge; const radius = progress * (config.maxRadius || 100); const opacity = (1 - progress) * 0.8; ctx.save(); ctx.globalAlpha = opacity; ctx.strokeStyle = config.color || 'rgba(100, 150, 255, 1)'; ctx.lineWidth = config.lineWidth || 3; ctx.beginPath(); ctx.arc(effect.x / manager.options.pixelRatio, effect.y / manager.options.pixelRatio, radius, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); } }); // Draw hover effect if (state.mouse.isOver) { ctx.save(); ctx.globalAlpha = 0.3; ctx.fillStyle = config.hoverColor || 'rgba(100, 150, 255, 0.3)'; ctx.beginPath(); ctx.arc( state.mouse.x / manager.options.pixelRatio, state.mouse.y / manager.options.pixelRatio, config.hoverRadius || 30, 0, Math.PI * 2 ); ctx.fill(); ctx.restore(); } }, /** * Render trail effect */ renderTrailEffect(manager, state, config) { const { ctx } = manager; const trailPoints = state.effects.filter(e => e.type === 'trail'); if (trailPoints.length < 2) return; ctx.save(); ctx.strokeStyle = config.color || 'rgba(100, 150, 255, 0.8)'; ctx.lineWidth = config.lineWidth || 5; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; // Draw trail path ctx.beginPath(); trailPoints.forEach((point, index) => { const progress = 1 - (point.age / point.maxAge); const x = point.x / manager.options.pixelRatio; const y = point.y / manager.options.pixelRatio; ctx.globalAlpha = progress * 0.8; if (index === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } }); ctx.stroke(); ctx.restore(); }, /** * Render particle effect */ renderParticleEffect(manager, state, config, deltaTime) { const { ctx } = manager; // Spawn particles on mouse move if (state.mouse.isOver && Math.random() < 0.1) { const particle = { x: state.mouse.x, y: state.mouse.y, vx: (Math.random() - 0.5) * 4, vy: (Math.random() - 0.5) * 4, age: 0, maxAge: 1000, size: Math.random() * 5 + 2, type: 'particle' }; state.effects.push(particle); } // Update and draw particles state.effects.forEach(effect => { if (effect.type === 'particle') { // Update position effect.x += effect.vx; effect.y += effect.vy; effect.vy += 0.1; // Gravity const progress = effect.age / effect.maxAge; const opacity = (1 - progress) * 0.8; ctx.save(); ctx.globalAlpha = opacity; ctx.fillStyle = config.color || 'rgba(100, 150, 255, 1)'; ctx.beginPath(); ctx.arc( effect.x / manager.options.pixelRatio, effect.y / manager.options.pixelRatio, effect.size * (1 - progress * 0.5), 0, Math.PI * 2 ); ctx.fill(); ctx.restore(); } }); }, /** * Render magnetic effect */ renderMagneticEffect(manager, state, config) { const { ctx } = manager; const { width, height } = manager.getSize(); if (!state.mouse.isOver) return; // Draw magnetic field lines const centerX = width / 2; const centerY = height / 2; const mouseX = state.mouse.x / manager.options.pixelRatio; const mouseY = state.mouse.y / manager.options.pixelRatio; ctx.save(); ctx.strokeStyle = config.color || 'rgba(100, 150, 255, 0.6)'; ctx.lineWidth = 2; const lines = 8; for (let i = 0; i < lines; i++) { const angle = (i / lines) * Math.PI * 2; const startX = centerX + Math.cos(angle) * 50; const startY = centerY + Math.sin(angle) * 50; // Curve towards mouse const controlX = (startX + mouseX) / 2 + Math.sin(angle) * 30; const controlY = (startY + mouseY) / 2 + Math.cos(angle) * 30; ctx.beginPath(); ctx.moveTo(startX, startY); ctx.quadraticCurveTo(controlX, controlY, mouseX, mouseY); ctx.stroke(); } ctx.restore(); }, /** * Update all effects (age and cleanup) */ updateEffects(effects, deltaTime) { for (let i = effects.length - 1; i >= 0; i--) { effects[i].age += deltaTime; if (effects[i].age >= effects[i].maxAge) { effects.splice(i, 1); } } } };