How to detect which element is hovered on a CanvasRenderingContext2D with transformations?

56 views Asked by At

I have a canvas on which I applied a few transformations (mostly translation to place the origin in the middle and a scale to zoom). I now want to find out if the cursor hovers an element drawn on the canvas.

The detection is quite simple: "is the cursor less than n screen pixels away from the center of the object".

How can I obtain the screen coordinates of an element that has had a transformation applied to it?

Here is what I've got so far: I do it the long way and it's not suitable when I apply other transformations, such as scale.

In the following example, the yellow square is centered at (0;0) but displayed in the middle of the screen due to ctx.translate().

const ctx = canv.getContext('2d')
ctx.fillRect(0, 0, canv.width, canv.height)

ctx.translate(canv.width / 2, canv.height / 2)

ctx.fillStyle = 'yellow'
ctx.fillRect(-4, -4, 8, 8) // centered at (0,0)

canv.addEventListener('mousemove', event => {
  const dX = canv.width / 2 - event.offsetX
  const dY = canv.height / 2 - event.offsetY
  const distance = Math.sqrt(dX * dX + dY * dY)
  coord.innerText = `Distance to square: ${distance}`
})
<canvas id="canv"></canvas>
<div id="coord"></div>

Is there a cleaner way to achieve this?

2

There are 2 answers

2
Kaiido On BEST ANSWER

Your 2D context has a getTransform() method, that does return a DOMMatrix object. From that matrix you can perform all your transformations. In your case, you'll need the invert matrix, and then use transformPoint() to apply that transformation over the point relative to your canvas element.

const ctx = canv.getContext('2d')
ctx.fillRect(0, 0, canv.width, canv.height)

ctx.translate(canv.width / 2, canv.height / 2)
ctx.scale(1.5, 0.5); // even with scale

ctx.fillStyle = 'yellow'
ctx.fillRect(-4, -4, 8, 8) // centered at (0,0)

canv.addEventListener('mousemove', event => {
  // `transformPoint()` takes a DOMPointInit, so we create one here
  const mouse = { x: event.offsetX, y: event.offsetY };
  const mat = ctx.getTransform().invertSelf();
  const rel = mat.transformPoint(mouse);
  const distance = Math.hypot(rel.x, rel.y);
  coord.innerText = `Distance to square: ${distance}`
})
<canvas id="canv"></canvas>
<div id="coord"></div>

3
SIDHANT PRIYADARSHAN BISWAL On
const elements = []

// Draw elements 
const rect = {
  x: 100, 
  y: 50, 
  width: 50,
  height: 80
}

ctx.translate(50, 100)
ctx.rotate(Math.PI / 4)
ctx.fillRect(rect.x, rect.y, rect.width, rect.height)

elements.push({
  ...rect,
  transform: ctx.getTransform()  
})

function checkHover(mouseX, mouseY) {

  // Inverse transform mouse pos
  const invertedMousePos = {
    x: mouseX,
    y: mouseY
  }
  invertTransform(invertedMousePos, ctx.getTransform().invertSelf())

  // Check if mouse pos intersects any element 
  for (let el of elements) {
    if (invertedMousePos.x > el.x && 
        invertedMousePos.y > el.y &&
        invertedMousePos.x < el.x + el.width &&
        invertedMousePos.y < el.y + el.height) {
      return el
    }
  }

  return null
}

function invertTransform(point, invertedMatrix) {

    const x = point.x * invertedMatrix.a + point.y * invertedMatrix.c + invertedMatrix.e const y = point.x * invertedMatrix.b + point.y * invertedMatrix.d + invertedMatrix.f

    point.x = x point.y = y

    return point
}