hy-masonry
Version:
Animated Masonry Layout as Web Component - Living organisms that morph and breathe
167 lines (142 loc) • 4.41 kB
text/typescript
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);
}
}