UNPKG

my-animation-lib

Version:

A powerful animation library combining Three.js, GSAP, custom scroll triggers, and advanced effects with MathUtils integration

670 lines (566 loc) 18.1 kB
import { gsap } from 'gsap'; import { ScrollTrigger } from './ScrollTrigger.js'; import { ThreeJSManager } from './ThreeJSManager.js'; import { MathUtils } from '../utils/MathUtils.js'; import { Easing } from '../utils/Easing.js'; /** * Main Animation Engine that coordinates all animations and effects */ export class AnimationEngine { constructor(options = {}) { this.options = { enableThreeJS: true, enableScrollTrigger: true, enableParallax: true, enableImageEffects: true, ...options }; this.scrollTrigger = null; this.threeJSManager = null; this.animations = new Map(); this.isInitialized = false; this.init(); } /** * Initialize the animation engine */ async init() { try { if (this.options.enableScrollTrigger) { this.scrollTrigger = new ScrollTrigger(); await this.scrollTrigger.init(); } if (this.options.enableThreeJS) { this.threeJSManager = new ThreeJSManager(); await this.threeJSManager.init(); } this.isInitialized = true; this.emit('ready'); } catch (error) { console.error('AnimationEngine initialization failed:', error); this.emit('error', error); } } /** * Create a new animation */ createAnimation(id, config) { if (this.animations.has(id)) { console.warn(`Animation with id '${id}' already exists. Overwriting...`); } const animation = { id, config, timeline: gsap.timeline(), isActive: false, startTime: 0 }; this.animations.set(id, animation); return animation; } /** * Play an animation */ playAnimation(id) { const animation = this.animations.get(id); if (!animation) { console.warn(`Animation '${id}' not found`); return false; } animation.timeline.play(); animation.isActive = true; animation.startTime = Date.now(); return true; } /** * Pause an animation */ pauseAnimation(id) { const animation = this.animations.get(id); if (animation) { animation.timeline.pause(); animation.isActive = false; } } /** * Stop an animation */ stopAnimation(id) { const animation = this.animations.get(id); if (animation) { animation.timeline.kill(); animation.isActive = false; } } /** * Get animation status */ getAnimationStatus(id) { const animation = this.animations.get(id); if (!animation) return null; return { id: animation.id, isActive: animation.isActive, progress: animation.timeline.progress(), duration: animation.timeline.duration(), isPlaying: animation.timeline.isActive() }; } /** * Create parallax effect */ createParallax(element, options = {}) { if (!this.options.enableParallax) { console.warn('Parallax effects are disabled'); return null; } const defaultOptions = { speed: 0.5, direction: 'vertical', easing: 'power2.out', ...options }; return this.scrollTrigger?.createParallax(element, defaultOptions); } /** * Create image effect */ createImageEffect(element, effectType, options = {}) { if (!this.options.enableImageEffects) { console.warn('Image effects are disabled'); return null; } const effects = { 'morph': this.createMorphEffect.bind(this), 'distortion': this.createDistortionEffect.bind(this), 'glitch': this.createGlitchEffect.bind(this), 'wave': this.createWaveEffect.bind(this) }; const effectFunction = effects[effectType]; if (!effectFunction) { console.warn(`Unknown effect type: ${effectType}`); return null; } return effectFunction(element, options); } /** * Create morph effect */ createMorphEffect(element, options = {}) { // Implementation for morph effect const timeline = gsap.timeline(); timeline.to(element, { duration: options.duration || 2, morphSVG: options.target || element, ease: options.easing || "power2.inOut" }); return timeline; } /** * Create distortion effect */ createDistortionEffect(element, options = {}) { // Implementation for distortion effect const timeline = gsap.timeline(); timeline.to(element, { duration: options.duration || 1.5, filter: "hue-rotate(180deg) saturate(2)", ease: options.easing || "power2.out" }); return timeline; } /** * Create glitch effect */ createGlitchEffect(element, options = {}) { // Implementation for glitch effect const timeline = gsap.timeline({ repeat: -1 }); timeline.to(element, { duration: 0.1, x: options.intensity || 10, ease: "none" }).to(element, { duration: 0.1, x: -(options.intensity || 10), ease: "none" }).to(element, { duration: 0.1, x: 0, ease: "none" }); return timeline; } /** * Create wave effect */ createWaveEffect(element, options = {}) { // Implementation for wave effect const timeline = gsap.timeline({ repeat: -1 }); timeline.to(element, { duration: options.duration || 2, y: options.amplitude || 20, ease: "sine.inOut" }).to(element, { duration: options.duration || 2, y: -(options.amplitude || 20), ease: "sine.inOut" }); return timeline; } /** * Create advanced wave effect using MathUtils */ createAdvancedWaveEffect(element, options = {}) { const { duration = 2, amplitude = 20, frequency = 1, phase = 0, easing = 'sine' } = options; const timeline = gsap.timeline({ repeat: -1 }); // Store reference to this instance for use in onUpdate const engine = this; // Use MathUtils for more sophisticated wave calculations const waveFunction = (t) => { const easedT = engine.getEasingFunction(easing)(t); return MathUtils.lerp(-amplitude, amplitude, easedT); }; timeline.to(element, { duration, y: amplitude, ease: "none", onUpdate: function() { const progress = this.progress(); const waveValue = waveFunction(progress); element.style.transform = `translateY(${waveValue}px)`; } }); return timeline; } /** * Create noise-based animation */ createNoiseAnimation(element, options = {}) { const { duration = 3, intensity = 10, noiseScale = 0.1 } = options; const timeline = gsap.timeline({ repeat: -1 }); timeline.to(element, { duration, ease: "none", onUpdate: function() { const progress = this.progress(); const time = progress * duration; // Use MathUtils.perlinNoise for smooth random movement const noiseX = MathUtils.perlinNoise(time * noiseScale, 0) * intensity; const noiseY = MathUtils.perlinNoise(0, time * noiseScale) * intensity; const currentTransform = element.style.transform || ''; const translateMatch = currentTransform.match(/translate\(([^)]+)\)/); if (translateMatch) { // Replace existing translate element.style.transform = currentTransform.replace( /translate\([^)]+\)/, `translate(${noiseX}px, ${noiseY}px)` ); } else { // Add new translate element.style.transform = `${currentTransform} translate(${noiseX}px, ${noiseY}px)`.trim(); } } }); return timeline; } /** * Create spring animation using MathUtils */ createSpringAnimation(element, options = {}) { const { targetValue = 100, stiffness = 0.1, damping = 0.8, duration = 2 } = options; const timeline = gsap.timeline(); timeline.to(element, { duration, ease: "none", onUpdate: function() { const progress = this.progress(); // Use MathUtils.elastic for spring-like motion const springValue = MathUtils.elastic(progress) * targetValue; const currentTransform = element.style.transform || ''; const translateMatch = currentTransform.match(/translateX\([^)]+\)/); if (translateMatch) { // Replace existing translateX element.style.transform = currentTransform.replace( /translateX\([^)]+\)/, `translateX(${springValue}px)` ); } else { // Add new translateX element.style.transform = `${currentTransform} translateX(${springValue}px)`.trim(); } } }); return timeline; } /** * Create morphing path animation */ createMorphingPath(element, pathPoints, options = {}) { const { duration = 3, easing = 'sine' } = options; const timeline = gsap.timeline(); timeline.to(element, { duration, ease: "none", onUpdate: function() { const progress = this.progress(); const easedProgress = this.getEasingFunction(easing)(progress); // Use MathUtils.catmullRom for smooth path interpolation const currentPoint = this.interpolatePath(pathPoints, easedProgress); element.style.transform = `translate(${currentPoint.x}px, ${currentPoint.y}px)`; } }); return timeline; } /** * Get easing function by name */ getEasingFunction(easingName) { const easingMap = { 'linear': Easing.linear, 'sine': Easing.easeInOutSine, 'bounce': Easing.easeOutBounce, 'elastic': Easing.easeOutElastic, 'back': Easing.easeOutBack, 'circular': Easing.easeInOutCirc, 'exponential': Easing.easeInOutExpo, 'cubic': Easing.easeInOutCubic, 'quartic': Easing.easeInOutQuart, 'quintic': Easing.easeInOutQuart, // MathUtils specific easing functions 'math-bounce': MathUtils.bounce, 'math-elastic': MathUtils.elastic, 'math-back': MathUtils.back, 'math-circular': MathUtils.circular, 'math-exponential': MathUtils.exponential, 'math-sine': MathUtils.sine, 'math-cubic': MathUtils.cubic, 'math-quartic': MathUtils.quartic, 'math-quintic': MathUtils.quintic, 'math-bounceOut': MathUtils.bounceOut, 'math-elasticOut': MathUtils.elasticOut, 'math-backOut': MathUtils.backOut }; return easingMap[easingName] || Easing.linear; } /** * Interpolate between path points using Catmull-Rom splines */ interpolatePath(points, t) { if (points.length < 4) { // Fallback to linear interpolation for insufficient points const index = t * (points.length - 1); const lowerIndex = Math.floor(index); const upperIndex = Math.ceil(index); const weight = index - lowerIndex; if (lowerIndex === upperIndex) return points[lowerIndex]; return { x: MathUtils.lerp(points[lowerIndex].x, points[upperIndex].x, weight), y: MathUtils.lerp(points[lowerIndex].y, points[upperIndex].y, weight) }; } // Use Catmull-Rom spline for smooth path interpolation const segmentIndex = Math.floor(t * (points.length - 3)); const segmentT = (t * (points.length - 3)) - segmentIndex; const p0 = points[Math.max(0, segmentIndex - 1)] || points[0]; const p1 = points[segmentIndex] || points[0]; const p2 = points[segmentIndex + 1] || points[points.length - 1]; const p3 = points[Math.min(points.length - 1, segmentIndex + 2)] || points[points.length - 1]; return { x: MathUtils.catmullRom(segmentT, p0.x, p1.x, p2.x, p3.x), y: MathUtils.catmullRom(segmentT, p0.y, p1.y, p2.y, p3.y) }; } /** * Create a smooth color transition using MathUtils */ createColorTransition(element, startColor, endColor, options = {}) { const { duration = 2, easing = 'sine' } = options; const timeline = gsap.timeline(); timeline.to(element, { duration, ease: "none", onUpdate: function() { const progress = this.progress(); const easedProgress = this.getEasingFunction(easing)(progress); // Use MathUtils.lerp for smooth color interpolation const currentColor = this.interpolateColor(startColor, endColor, easedProgress); element.style.backgroundColor = currentColor; } }); return timeline; } /** * Interpolate between colors */ interpolateColor(color1, color2, t) { // Parse hex colors to RGB const rgb1 = this.hexToRgb(color1); const rgb2 = this.hexToRgb(color2); if (!rgb1 || !rgb2) return color1; // Use MathUtils.lerp for each color channel const r = Math.round(MathUtils.lerp(rgb1.r, rgb2.r, t)); const g = Math.round(MathUtils.lerp(rgb1.g, rgb2.g, t)); const b = Math.round(MathUtils.lerp(rgb1.b, rgb2.b, t)); return `rgb(${r}, ${g}, ${b})`; } /** * Convert hex color to RGB */ hexToRgb(hex) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; } /** * Create a particle system animation using MathUtils */ createParticleSystem(container, options = {}) { const { particleCount = 50, duration = 5, spread = 100, speed = 1 } = options; const particles = []; const timeline = gsap.timeline({ repeat: -1 }); // Create particles for (let i = 0; i < particleCount; i++) { const particle = document.createElement('div'); particle.className = 'particle'; particle.style.cssText = ` position: absolute; width: 4px; height: 4px; background: #fff; border-radius: 50%; pointer-events: none; `; container.appendChild(particle); particles.push(particle); } // Animate particles using MathUtils timeline.to(particles, { duration, ease: "none", onUpdate: function() { const progress = this.progress(); const time = progress * duration * speed; particles.forEach((particle, index) => { // Use MathUtils.perlinNoise for organic movement const noiseX = MathUtils.perlinNoise(time * 0.1, index * 0.1) * spread; const noiseY = MathUtils.perlinNoise(index * 0.1, time * 0.1) * spread; // Use MathUtils.smoothstep for smooth boundaries const smoothX = MathUtils.smoothstep(-spread, spread, noiseX); const smoothY = MathUtils.smoothstep(-spread, spread, noiseY); particle.style.transform = `translate(${noiseX}px, ${noiseY}px)`; particle.style.opacity = MathUtils.lerp(1, 0, progress); }); } }); return timeline; } /** * Create a morphing shape animation */ createMorphingShape(element, shapes, options = {}) { const { duration = 3, easing = 'sine' } = options; const timeline = gsap.timeline(); timeline.to(element, { duration, ease: "none", onUpdate: function() { const progress = this.progress(); const easedProgress = this.getEasingFunction(easing)(progress); // Use MathUtils for smooth shape interpolation const currentShape = this.interpolateShapes(shapes, easedProgress); element.style.clipPath = currentShape; } }); return timeline; } /** * Interpolate between shape definitions */ interpolateShapes(shapes, t) { if (shapes.length < 2) return shapes[0] || 'none'; const index = t * (shapes.length - 1); const lowerIndex = Math.floor(index); const upperIndex = Math.ceil(index); const weight = index - lowerIndex; if (lowerIndex === upperIndex) return shapes[lowerIndex]; // Simple interpolation between shape strings // For more complex morphing, you'd need to parse and interpolate individual points return shapes[lowerIndex]; } /** * Get Three.js manager instance */ getThreeJSManager() { return this.threeJSManager; } /** * Get scroll trigger instance */ getScrollTrigger() { return this.scrollTrigger; } /** * Destroy the animation engine */ destroy() { this.animations.forEach(animation => { animation.timeline.kill(); }); this.animations.clear(); if (this.scrollTrigger) { this.scrollTrigger.destroy(); } if (this.threeJSManager) { this.threeJSManager.destroy(); } this.isInitialized = false; } emit(event, data) { // Simple event emitter implementation if (this.eventListeners && this.eventListeners[event]) { this.eventListeners[event].forEach(callback => callback(data)); } } on(event, callback) { if (!this.eventListeners) this.eventListeners = {}; if (!this.eventListeners[event]) this.eventListeners[event] = []; this.eventListeners[event].push(callback); } off(event, callback) { if (this.eventListeners && this.eventListeners[event]) { this.eventListeners[event] = this.eventListeners[event].filter(cb => cb !== callback); } } }