type Viewport = { minX: number; minY: number; maxX: number; maxY: number; }; type Transform = { a: number; // scale X b: number; // skew Y c: number; // skew X d: number; // scale Y e: number; // translate X f: number; // translate Y }; export class ZoomPanController { private canvas: HTMLCanvasElement; private scale: number = 1; private translateX: number = 0; private translateY: number = 0; private isDragging: boolean = false; private dragStartX: number = 0; private dragStartY: number = 0; private dragStartTranslateX: number = 0; private dragStartTranslateY: number = 0; private minScale: number = 0.001; private maxScale: number = 1000; constructor(canvas: HTMLCanvasElement) { this.canvas = canvas; this.setupEventListeners(); } private setupEventListeners(): void { this.canvas.addEventListener('wheel', this.handleWheel.bind(this)); this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this)); this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this)); this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this)); this.canvas.addEventListener('mouseleave', this.handleMouseUp.bind(this)); } private handleWheel(event: WheelEvent): void { event.preventDefault(); const zoomIntensity = 0.1; const delta = event.deltaY > 0 ? -1 : 1; const zoomFactor = Math.exp(delta * zoomIntensity); // Calculate mouse position in world coordinates const rect = this.canvas.getBoundingClientRect(); const mouseX = event.clientX - rect.left; const mouseY = event.clientY - rect.top; const worldX = (mouseX - this.translateX) / this.scale; const worldY = (mouseY - this.translateY) / this.scale; // Apply zoom const newScale = this.scale * zoomFactor; this.scale = Math.max(this.minScale, Math.min(newScale, this.maxScale)); // Adjust translate to keep mouse position constant this.translateX = mouseX - worldX * this.scale; this.translateY = mouseY - worldY * this.scale; } private handleMouseDown(event: MouseEvent): void { if (event.button === 0) { // Left mouse button this.isDragging = true; this.dragStartX = event.clientX; this.dragStartY = event.clientY; this.dragStartTranslateX = this.translateX; this.dragStartTranslateY = this.translateY; this.canvas.style.cursor = 'grabbing'; } } private handleMouseMove(event: MouseEvent): void { if (this.isDragging) { const deltaX = event.clientX - this.dragStartX; const deltaY = event.clientY - this.dragStartY; this.translateX = this.dragStartTranslateX + deltaX; this.translateY = this.dragStartTranslateY + deltaY; } } private handleMouseUp(): void { if (this.isDragging) { this.isDragging = false; this.canvas.style.cursor = 'default'; } } public getTransform(): Transform { return { a: this.scale, b: 0, c: 0, d: this.scale, e: this.translateX, f: this.translateY }; } public getViewport(): Viewport { const rect = this.canvas.getBoundingClientRect(); const minX = (0 - this.translateX) / this.scale; const minY = (0 - this.translateY) / this.scale; const maxX = (rect.width - this.translateX) / this.scale; const maxY = (rect.height - this.translateY) / this.scale; return { minX, minY, maxX, maxY }; } public setScale(scale: number): void { this.scale = Math.max(this.minScale, Math.min(scale, this.maxScale)); } public setTranslate(x: number, y: number): void { this.translateX = x; this.translateY = y; } public getScale(): number { return this.scale; } public getTranslate(): { x: number; y: number } { return { x: this.translateX, y: this.translateY }; } public reset(): void { this.scale = 1; this.translateX = 0; this.translateY = 0; } }