UNPKG

hy-masonry

Version:

Animated Masonry Layout as Web Component - Living organisms that morph and breathe

167 lines (142 loc) 4.41 kB
import { MorphingState } from '../types'; import { SizeCalculator } from './size-calculator'; export class MorphingManager { private morphingItems: Map<string, MorphingState> = new Map(); private originalSizes: Map<string, string> = new Map(); private morphAnimationFrame: number | null = null; private isRunning: boolean = false; constructor() { this.start(); } /** * Morph item to new size */ morphItem(itemId: string, targetSize: string, duration: number = 500): void { const element = document.querySelector(`[data-item-id="${itemId}"]`) as HTMLElement; if (!element) return; const currentSize = element.getAttribute('data-size') || '1x1'; const originalSize = this.originalSizes.get(itemId) || currentSize; const morphingState: MorphingState = { originalSize, targetSize, duration, startTime: Date.now(), progress: 0, update: function(currentTime: number): number { this.progress = Math.min((currentTime - this.startTime) / this.duration, 1); return this.progress; }, isComplete: function(): boolean { return this.progress >= 1; } }; this.morphingItems.set(itemId, morphingState); element.classList.add('morphing'); // Dispatch morph start event this.dispatchEvent('item:morph', { itemId, targetSize, duration }); } /** * Reset item to original size */ resetItem(itemId: string, duration: number = 500): void { const originalSize = this.originalSizes.get(itemId); if (originalSize) { this.morphItem(itemId, originalSize, duration); } } /** * Check if item is currently morphing */ isMorphing(itemId: string): boolean { return this.morphingItems.has(itemId); } /** * Store original size for an item */ storeOriginalSize(itemId: string, size: string): void { this.originalSizes.set(itemId, size); } /** * Start morphing animation loop */ start(): void { if (this.isRunning) return; this.isRunning = true; this.animate(); } /** * Stop morphing animation loop */ stop(): void { this.isRunning = false; if (this.morphAnimationFrame) { cancelAnimationFrame(this.morphAnimationFrame); this.morphAnimationFrame = null; } } /** * Main morphing animation loop */ private animate(): void { if (!this.isRunning) return; const currentTime = Date.now(); const completedItems: string[] = []; this.morphingItems.forEach((state, itemId) => { const element = document.querySelector(`[data-item-id="${itemId}"]`) as HTMLElement; if (!element) { completedItems.push(itemId); return; } const progress = state.update(currentTime); const morphedSize = SizeCalculator.morphSize( state.originalSize, state.targetSize, progress ); // Update element size element.setAttribute('data-size', morphedSize); element.style.gridColumn = `span ${SizeCalculator.parseSize(morphedSize).cols}`; element.style.gridRow = `span ${SizeCalculator.parseSize(morphedSize).rows}`; if (state.isComplete()) { completedItems.push(itemId); element.classList.remove('morphing'); // Dispatch morph end event this.dispatchEvent('item:morph-end', { itemId, finalSize: state.targetSize }); } }); // Remove completed items completedItems.forEach(itemId => { this.morphingItems.delete(itemId); }); this.morphAnimationFrame = requestAnimationFrame(() => this.animate()); } /** * Dispatch custom event */ private dispatchEvent(eventName: string, detail: any): void { const event = new CustomEvent(eventName, { detail, bubbles: true, cancelable: true }); document.dispatchEvent(event); } /** * Get morphing items count */ getMorphingCount(): number { return this.morphingItems.size; } /** * Clear all morphing states */ clear(): void { this.morphingItems.clear(); } /** * Get morphing state for an item */ getMorphingState(itemId: string): MorphingState | undefined { return this.morphingItems.get(itemId); } }