Replace js by ts
This commit is contained in:
231
src-tauri/assets/viewer/zoom-controller-enhanced.ts
Normal file
231
src-tauri/assets/viewer/zoom-controller-enhanced.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* 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<number>, bufferSet: Set<number>) => void;
|
||||
private getVisibilityState: () => { visibleSet: Set<number>, bufferSet: Set<number> };
|
||||
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<number>, bufferSet: Set<number>) => void, getVisibilityState: () => { visibleSet: Set<number>, bufferSet: Set<number> }, 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 };
|
||||
Reference in New Issue
Block a user