hy-masonry
Version:
Animated Masonry Layout as Web Component - Living organisms that morph and breathe
154 lines (132 loc) • 4.27 kB
text/typescript
import { AnimationFunction, AnimationOptions } from '../types';
export class AnimationEngine {
private animations: Map<string, AnimationFunction> = new Map();
private animationFrame: number | null = null;
private isRunning: boolean = false;
private lastTime: number = 0;
constructor() {
this.start();
}
/**
* Add animation to an item
*/
addAnimation(itemId: string, animationType: string, options: AnimationOptions = {}): void {
const animationFunction = this.createAnimation(animationType, options);
this.animations.set(itemId, animationFunction);
}
/**
* Remove animation from an item
*/
removeAnimation(itemId: string): void {
this.animations.delete(itemId);
}
/**
* Create animation function based on type
*/
private createAnimation(type: string, options: AnimationOptions): AnimationFunction {
switch (type) {
case 'breathing':
const breathing = new BreathingAnimation(options);
return (element: HTMLElement, deltaTime: number) => breathing.update(element, deltaTime);
case 'pulse':
const pulse = new PulseAnimation(options);
return (element: HTMLElement, deltaTime: number) => pulse.update(element, deltaTime);
case 'float':
const float = new FloatAnimation(options);
return (element: HTMLElement, deltaTime: number) => float.update(element, deltaTime);
default:
return (_element: HTMLElement, _deltaTime: number) => {}; // No animation
}
}
/**
* Start animation loop
*/
start(): void {
if (this.isRunning) return;
this.isRunning = true;
this.lastTime = performance.now();
this.animate();
}
/**
* Stop animation loop
*/
stop(): void {
this.isRunning = false;
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame);
this.animationFrame = null;
}
}
/**
* Main animation loop
*/
private animate(): void {
if (!this.isRunning) return;
const currentTime = performance.now();
const deltaTime = currentTime - this.lastTime;
this.lastTime = currentTime;
// Update all animations
this.animations.forEach((animation, itemId) => {
const element = document.querySelector(`[data-item-id="${itemId}"]`) as HTMLElement;
if (element) {
animation(element, deltaTime);
}
});
this.animationFrame = requestAnimationFrame(() => this.animate());
}
/**
* Get animation count
*/
getAnimationCount(): number {
return this.animations.size;
}
/**
* Clear all animations
*/
clear(): void {
this.animations.clear();
}
}
class BreathingAnimation {
private speed: number;
private intensity: number;
private phase: number;
constructor(options: AnimationOptions = {}) {
this.speed = options.speed || 2000;
this.intensity = options.intensity || 0.05;
this.phase = options.phase || 0;
}
update(element: HTMLElement, deltaTime: number): void {
const scale = 1 + Math.sin(this.phase) * this.intensity;
element.style.transform = `scale(${scale})`;
this.phase += (deltaTime / this.speed) * Math.PI * 2;
}
}
class PulseAnimation {
private speed: number;
private phase: number;
constructor(options: AnimationOptions = {}) {
this.speed = options.speed || 1500;
this.phase = options.phase || 0;
}
update(element: HTMLElement, deltaTime: number): void {
const opacity = 0.8 + Math.sin(this.phase) * 0.2;
element.style.opacity = opacity.toString();
this.phase += (deltaTime / this.speed) * Math.PI * 2;
}
}
class FloatAnimation {
private speed: number;
private intensity: number;
private phase: number;
constructor(options: AnimationOptions = {}) {
this.speed = options.speed || 3000;
this.intensity = options.intensity || 5;
this.phase = options.phase || 0;
}
update(element: HTMLElement, deltaTime: number): void {
const translateY = Math.sin(this.phase) * this.intensity;
element.style.transform = `translateY(${translateY}px)`;
this.phase += (deltaTime / this.speed) * Math.PI * 2;
}
}