diff --git a/frontend/src/canvas/ZoomPanController.ts b/frontend/src/canvas/ZoomPanController.ts new file mode 100644 index 0000000..fdf0074 --- /dev/null +++ b/frontend/src/canvas/ZoomPanController.ts @@ -0,0 +1,138 @@ +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; + } +}