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
JavaScript
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);
}
}
}