Search code examples
javascriptcanvasmouseeventobject-fit

How to convert mouse position to canvas coordinate using object-fit: contain


Using object-fit: contain allow to resize a canvas to fit inside a container.

This snippet is drawing a circle using mouse event position :

canvas.width=1920;
canvas.height=1200;
const ctx=canvas.getContext("2d");
ctx.lineWidth = 10;
ctx.strokeRect(0,0,canvas.width,canvas.height);
canvas.onmousemove = (e) => {
  ctx.clearRect(0,0,canvas.width,canvas.height);
  ctx.strokeRect(0,0,canvas.width,canvas.height);
  ctx.beginPath();
  const x = parseInt(e.offsetX*canvas.width/canvas.offsetWidth);
  const y = parseInt(e.offsetY*canvas.height/canvas.offsetHeight);
  ctx.arc(x,y, 10, 0, 2 * Math.PI);
  ctx.stroke();
  pt.innerText=`${x}x${y}`;
}
#container {
  height: 150px;
}
#canvas {
  object-fit: contain;
  width: 100%;
  height: 100%;
  border: 1px solid red;
}
<div id="container">
  <canvas id="canvas"></canvas>
</div>
<div id="pt"></div>

The circle and mouse pointer are matching only on the middle of the canvas. On other position we got something like enter image description here

It seems event coordinate cover the entire area including the letterboxing. Removing object-fit: contain; draw circle on mouse position (but doesnot fit it).
How to convert mouse event position to canvas coordinate ? It will need to get letterboxing height & width ?


Solution

  • This one's rather tricky. The problem is that the canvas element takes up 100% of the width and height of it's parent - so in this case the entire width of the viewport. However due to limiting the height to a fixed pixel size and using the CSS object-fit: contain property the actual size of the canvas on screen is a lot smaller.

    So the first step is to calculate the actual area:

        let onScreenHeight = e.target.parentElement.getBoundingClientRect().height;
        let scaleWidth = onScreenHeight / canvas.height;
        let scaleHeight = e.target.parentElement.getBoundingClientRect().width / canvas.width;
        let onScreenWidth = canvas.width * scaleWidth;
    

    Which will give use 150 pixels height - as specified cia CSS - and 240 pixels width.

    Next step is calculating the 'virtual' width & height of that area, taking into account the actual dimensions of the viewport.

        let virtualWidth = onScreenWidth / scaleHeight;
        let virtualHeight = onScreenHeight / scaleWidth;
    

    As we know the virtual dimensions now, everything that's left is checking if we hover the mouse over it! This is done by translating the mouse coordinates to the virtual coordinate space and if the resulting number is >0 and < virtualWidth we know that we're inside the canvas - horizontally.

    If we take this number and divide it by virtualWidth, we get a factor we can use to multiply with the canvas actual width to finally get the real on screen x coordinate.

        let x = ((mouseX - canvas.width / 2 + virtualWidth / 2) / virtualWidth) * canvas.width;
        let y = ((mouseY - canvas.height / 2 + virtualHeight / 2) / virtualHeight) * canvas.height;
    

    Everything put together:

    canvas.width = 1920;
    canvas.height = 1200;
    const ctx = canvas.getContext("2d");
    ctx.lineWidth = 10;
    ctx.strokeRect(0, 0, canvas.width, canvas.height);
    canvas.onmousemove = (e) => {
        let onScreenHeight = e.target.parentElement.getBoundingClientRect().height;
        let scaleWidth = onScreenHeight / canvas.height;
        let scaleHeight = e.target.parentElement.getBoundingClientRect().width / canvas.width;
        let onScreenWidth = canvas.width * scaleWidth;
        let virtualWidth = onScreenWidth / scaleHeight;
        let virtualHeight = onScreenHeight / scaleWidth;
        
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.strokeRect(0, 0, canvas.width, canvas.height);
        ctx.beginPath();
        let mouseX = parseInt(e.offsetX * canvas.width / canvas.offsetWidth);
        let mouseY = parseInt(e.offsetY * canvas.height / canvas.offsetHeight);
    
        let x = ((mouseX - canvas.width / 2 + virtualWidth / 2) / virtualWidth) * canvas.width;
        let y = ((mouseY - canvas.height / 2 + virtualHeight / 2) / virtualHeight) * canvas.height
        ctx.arc(x, y, 10, 0, 2 * Math.PI);
        ctx.stroke();
    }
    #container {
      height: 150px;
    }
    #canvas {
      object-fit: contain;
      width: 100%;
      height: 100%;
      border: 1px solid red;
    }
    <div id="container">
      <canvas id="canvas"></canvas>
    </div>