feat(canvas): Create ZoomPanController for mouse wheel zoom, drag pan, coordinate transforms, zoom limits
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user