Allow canvas shapes to be dragged and zoomed

by Rajesh Danabal

HTML

<canvas id="canvas" width="800" height="600"></canvas>

CSS

body {
  margin: 0;
  overflow: hidden;
}
canvas {
  border: 1px solid #ccc;
  display: block;
}

TypeScript

type Shape = {
  id: string;
  x: number;
  y: number;
  width: number;
  height: number;
  color: string;
};

type InteractionEventHandlers = {
  onShapeClick?: (shape: Shape, e: MouseEvent) => void;
  onShapeContextMenu?: (shape: Shape, e: MouseEvent) => void;
  onShapeDragStart?: (shape: Shape, e: MouseEvent) => void;
  onShapeDrag?: (shape: Shape, e: MouseEvent) => void;
  onShapeDragEnd?: (shape: Shape, e: MouseEvent) => void;

  onCanvasClick?: (e: MouseEvent) => void;
  onCanvasContextMenu?: (e: MouseEvent) => void;
  onCanvasDragStart?: (e: MouseEvent) => void;
  onCanvasDrag?: (e: MouseEvent) => void;
  onCanvasDragEnd?: (e: MouseEvent) => void;

  onCanvasZoom?: (e: WheelEvent, scale: number) => void;
};

type InteractionState = {
  draggingShape: Shape | null;
  draggingCanvas: boolean;
  scale: number;
  offsetX: number;
  offsetY: number;
  lastX: number;
  lastY: number;
};

class InteractionManager {
  private canvas: HTMLCanvasElement;
  private context: CanvasRenderingContext2D;
  private shapes: Shape[];
  public state: InteractionState;
  private handlers: InteractionEventHandlers;

  constructor(
    canvas: HTMLCanvasElement,
    context: CanvasRenderingContext2D,
    shapes: Shape[],
    handlers: InteractionEventHandlers = {}
  ) {
    this.canvas = canvas;
    this.context = context;
    this.shapes = shapes;
    this.handlers = handlers;

    this.state = {
      draggingShape: null,
      draggingCanvas: false,
      scale: 1,
      offsetX: 0,
      offsetY: 0,
      lastX: 0,
      lastY: 0,
    };

    this.bindEvents();
  }

  private toWorld(x: number, y: number) {
    return {
      x: (x - this.state.offsetX) / this.state.scale,
      y: (y - this.state.offsetY) / this.state.scale,
    };
  }

  private getShapeAt(x: number, y: number): Shape | null {
    for (let i = this.shapes.length - 1; i >= 0; i--) {
      const s = this.shapes[i];
      if (x >= s.x && x <= s.x + s.width && y >= s.y && y <= s.y + s.height) {
        return...