/** * zoom-controller-enhanced.ts — Advanced zoom controller with smooth transitions * * Provides smooth zoom animations, adaptive quality during zoom, * and intelligent zoom level management. */ interface EnhancedZoomControllerOptions { animate?: boolean; qualityOverride?: number | null; } class EnhancedZoomController { private container: HTMLElement; private pageManager: any; // Replace with proper type when available private onScaleChange: (scale: number, visibleSet: Set, bufferSet: Set) => void; private getVisibilityState: () => { visibleSet: Set, bufferSet: Set }; private zoomLabel: HTMLElement | null; // Zoom state private scale: number; private targetScale: number; private minScale: number; private maxScale: number; private zoomAnimation: number | null; private isZooming: boolean; private zoomQuality: number; constructor(container: HTMLElement, pageManager: any, onScaleChange: (scale: number, visibleSet: Set, bufferSet: Set) => void, getVisibilityState: () => { visibleSet: Set, bufferSet: Set }, zoomLabel: HTMLElement | null) { this.container = container; this.pageManager = pageManager; this.onScaleChange = onScaleChange; this.getVisibilityState = getVisibilityState; this.zoomLabel = zoomLabel; // Zoom state this.scale = 1.0; this.targetScale = 1.0; this.minScale = 0.1; this.maxScale = 5.0; this.zoomAnimation = null; this.isZooming = false; this.zoomQuality = 1.0; // Setup event listeners this.setupEventListeners(); // Update zoom label this.updateZoomLabel(); } private setupEventListeners(): void { // Smooth zoom with animation this.container.addEventListener('wheel', (event) => { if (event.ctrlKey || event.metaKey) { this.handleZoomWheel(event); event.preventDefault(); } }, { passive: false }); // Double-click for zoom to fit this.container.addEventListener('dblclick', (event) => { this.zoomToFit(); }); } private handleZoomWheel(event: WheelEvent): void { // Calculate zoom factor based on wheel delta const zoomFactor = event.deltaY > 0 ? 0.9 : 1.1; const newScale = this.scale * zoomFactor; // Apply constraints const constrainedScale = Math.max(this.minScale, Math.min(this.maxScale, newScale)); if (Math.abs(constrainedScale - this.scale) > 0.01) { this.applyScale(constrainedScale); } } public applyScale(newScale: number, options: EnhancedZoomControllerOptions = {}): void { const { animate = true, qualityOverride = null } = options; // Constrain scale this.targetScale = Math.max(this.minScale, Math.min(this.maxScale, newScale)); if (Math.abs(this.targetScale - this.scale) < 0.01) return; if (animate) { this.startZoomAnimation(qualityOverride); } else { this.scale = this.targetScale; const { visibleSet, bufferSet } = this.getVisibilityState(); this.onScaleChange(this.scale, visibleSet, bufferSet); this.updateZoomLabel(); } } private startZoomAnimation(qualityOverride: number | null = null): void { if (this.zoomAnimation) { cancelAnimationFrame(this.zoomAnimation); } this.isZooming = true; const startScale = this.scale; const endScale = this.targetScale; const startTime = performance.now(); const duration = 150; // 150ms for smooth zoom // Set lower quality during zoom for performance this.zoomQuality = qualityOverride !== null ? qualityOverride : 0.8; const animate = (currentTime: number) => { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); // Ease-in-out animation for natural feel const easedProgress = this.easeInOutCubic(progress); this.scale = startScale + (endScale - startScale) * easedProgress; // Apply the scale this.applyScaleToContainer(); // Notify about scale change with current quality const { visibleSet, bufferSet } = this.getVisibilityState(); this.onScaleChange(this.scale, visibleSet, bufferSet); if (progress < 1) { this.zoomAnimation = requestAnimationFrame(animate); } else { this.isZooming = false; this.scale = endScale; this.zoomQuality = 1.0; // Restore full quality // Final high-quality render const { visibleSet, bufferSet } = this.getVisibilityState(); this.onScaleChange(this.scale, visibleSet, bufferSet); this.updateZoomLabel(); this.zoomAnimation = null; } }; this.zoomAnimation = requestAnimationFrame(animate); } private applyScaleToContainer(): void { // Apply CSS transform for smooth zooming this.container.style.transform = `scale(${this.scale})`; // Also update the page manager if (this.pageManager.setScale) { this.pageManager.setScale(this.scale); } } private easeInOutCubic(t: number): number { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } private zoomToFit(): void { if (!this.pageManager || !this.pageManager.viewports) return; const firstViewport = this.pageManager.viewports[0]; if (!firstViewport) return; const containerWidth = this.container.clientWidth; const containerHeight = this.container.clientHeight; // Calculate fit scale const scaleW = (containerWidth - 40) / firstViewport.width; const scaleH = (containerHeight - 40) / firstViewport.height; const fitScale = Math.min(scaleW, scaleH); this.applyScale(Math.max(this.minScale, Math.min(this.maxScale, fitScale))); } public zoomIn(): void { this.applyScale(this.scale * 1.25); } public zoomOut(): void { this.applyScale(this.scale * 0.8); } public zoomToActualSize(): void { this.applyScale(1.0); } public getCurrentScale(): number { return this.scale; } public getCurrentZoomQuality(): number { return this.zoomQuality; } public isZoomingInProgress(): boolean { return this.isZooming; } private updateZoomLabel(): void { if (this.zoomLabel) { this.zoomLabel.textContent = `${Math.round(this.scale * 100)}%`; } } public cleanup(): void { if (this.zoomAnimation) { cancelAnimationFrame(this.zoomAnimation); } } public getZoomStats(): { currentScale: number; targetScale: number; isZooming: boolean; zoomQuality: number; } { return { currentScale: this.scale, targetScale: this.targetScale, isZooming: this.isZooming, zoomQuality: this.zoomQuality }; } } // Export for ES6 modules export { EnhancedZoomController };