feat(canvas): Create ZoomPanController for mouse wheel zoom, drag pan, coordinate transforms, zoom limits

This commit is contained in:
2026-06-22 07:18:55 +00:00
parent ed96766ea8
commit 6f0ec40998
+138
View File
@@ -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;
}
}