skip to Main Content

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?

3

Answers


  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
    }
    
    Login or Signup to reply.
  2. 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
    }

    Login or Signup to reply.
  3. 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>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search