JSFiddle - React, Tailwind, and code Playground

by Rajesh Danabal

HTML

<script src="https://cdn.jsdelivr.net/npm/d3-quadtree@3"></script>
<canvas id="canvas" width="600" height="400" style="border:1px solid #ccc;"></canvas>

TypeScript

// Assuming D3 is loaded from CDN

const { quadtree } = d3

class InteractionManager {
  constructor(canvas, context, shapes, handlers = {}) {
    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.quad = this.buildQuadTree()
    this.bindEvents()
  }

  buildQuadTree() {
    return quadtree()
      .x((d) => d.x + d.width / 2)
      .y((d) => d.y + d.height / 2)
      .addAll(this.shapes)
  }

  toWorldCoords(x, y) {
    return {
      x: (x - this.state.offsetX) / this.state.scale,
      y: (y - this.state.offsetY) / this.state.scale,
    }
  }

  getShapeAt(x, y) {
    let found = null
    this.quad.visit((node, x0, y0, x1, y1) => {
      if (!node.length) {
        for (const shape of node.data ? [node.data] : []) {
          if (
            x >= shape.x &&
            x <= shape.x + shape.width &&
            y >= shape.y &&
            y <= shape.y + shape.height
          ) {
            found = shape
            return true
          }
        }
      }
      return false
    })
    return found
  }

  bindEvents() {
    const getMousePos = (e) => ({
      x: e.offsetX,
      y: e.offsetY,
    })

    this.canvas.addEventListener("mousedown", (e) => {
      const { x, y } = getMousePos(e)
      const world = this.toWorldCoords(x, y)

      this.state.lastX = x
      this.state.lastY = y

      const target = this.getShapeAt(world.x, world.y)

      if (e.button === 0) {
        if (target) {
          this.state.draggingShape = target
          this.handlers.onShapeDragStart?.(target, e)
        } else {
          this.state.draggingCanvas = true
          this.handlers.onCanvasDragStart?.(e)
        }
      }
    })

    this.canvas.addEventListener("mousemove", (e) => {
      const { x, y } = getMousePos(e)
      const dx = x -...