interface AnimationOptions {
element: HTMLElement;
loadingElement: HTMLElement;
baseUrl: string;
totalFrames: number;
fps?: number;
fileFormat?: string;
onProgress?: (progress: number) => void;
}
class NetworkSpriteAnimation {
private element: HTMLElement;
private loadingElement: HTMLElement;
private frames: string[] = [];
private loadedImages: HTMLImageElement[] = [];
private currentFrame: number = 0;
private fps: number;
private isPlaying: boolean = false;
private baseUrl: string;
private totalFrames: number;
private animationFrameId?: number;
private onProgress?: (progress: number) => void;
private fileFormat: string;
constructor(options: AnimationOptions) {
this.element = options.element;
this.loadingElement = options.loadingElement;
this.baseUrl = options.baseUrl;
this.totalFrames = options.totalFrames;
this.fps = options.fps || 30;
this.fileFormat = options.fileFormat || 'jpg';
this.onProgress = options.onProgress;
}
private generateFrameUrl(frameNumber: number): string {
return `${this.baseUrl}/frame-${String(frameNumber).padStart(3, '0')}.${this.fileFormat}`;
}
public async preloadImages(): Promise<boolean> {
const loadPromises: Promise<HTMLImageElement>[] = [];
for (let i = 1; i <= this.totalFrames; i++) {
const imageUrl = this.generateFrameUrl(i);
this.frames.push(imageUrl);
const promise = new Promise<HTMLImageElement>((resolve, reject) => {
const img = new Image();
img.onload = () => {
this.loadedImages[i-1] = img;
if (this.onProgress) {
const progress = (this.loadedImages.filter(Boolean).length / this.totalFrames) * 100;
this.onProgress(progress);
}
resolve(img);
};
img.onerror = reject;
img.src = imageUrl;
});
loadPromises.push(promise);
}
try {
await Promise.all(loadPromises);
this.loadingElement.style.display = 'none';
this.element.style.opacity = '1';
return true;
} catch (error) {
console.error('Failed to load images:', error);
return false;
}
}
public play(): void {
if (this.isPlaying || this.loadedImages.length !== this.totalFrames) return;
this.isPlaying = true;
let lastTime = 0;
const frameTime = 1000 / this.fps;
const animate = (currentTime: number): void => {
if (!this.isPlaying) return;
if (currentTime - lastTime >= frameTime) {
this.currentFrame = (this.currentFrame + 1) % this.totalFrames;
this.element.style.backgroundImage = `url(${this.frames[this.currentFrame]})`;
lastTime = currentTime;
}
this.animationFrameId = requestAnimationFrame(animate);
};
this.animationFrameId = requestAnimationFrame(animate);
}
public pause(): void {
this.isPlaying = false;
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
}
public setFps(newFps: number): void {
this.fps = newFps;
}
public getCurrentFrame(): number {
return this.currentFrame;
}
public getTotalFrames(): number {
return this.totalFrames;
}
public isAnimationPlaying(): boolean {
return this.isPlaying;
}
public destroy(): void {
this.pause();
this.loadedImages = [];
this.frames = [];
this.removeEventListeners();
}
private removeEventListeners(): void {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
}
}
private resizeObserver?: ResizeObserver;
private intersectionObserver?: IntersectionObserver;
public enableResponsive(): void {
this.resizeObserver = new ResizeObserver(() => {
this.updateImageSize();
});
this.resizeObserver.observe(this.element);
}
public enableVisibilityControl(): void {
this.intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.play();
} else {
this.pause();
}
});
}, {
threshold: 0.1
});
this.intersectionObserver.observe(this.element);
}
private updateImageSize(): void {
const containerWidth = this.element.clientWidth;
const containerHeight = this.element.clientHeight;
this.element.style.backgroundSize = 'cover';
}
}
export default NetworkSpriteAnimation;