1

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 Answers 2

1

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>

Sign up to request clarification or add additional context in comments.

2 Comments

This is the opposite of what I asked as I would like to change the element coordinates to check the distance in the screen space, not in the canvas space. Nontheless, your answer helped me figure out the solution (I use your technique on the element, not on the mouse, without invertSelf()). You may or may not edit your very good answer, I'm going to accept it anyway. Thanks!
I think I reproduced what your snippet was doing for the simple case of translate(centerX, centerY), but in a more agnostic way that works with any transform. And I'm not sure what you were actually after, so I'm not sure how to edit it properly. If you wish, you can edit this answer yourself, it's a community wiki anyway, maybe you can simply add your solution to it?
1
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
}

3 Comments

What is invertTransform? It doesn't seem to exist as a JS function. Did you create that and forget to add it to the answer?
My Apology! I forgot to add
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.