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