Home > Software engineering >  Mouse event position on a canvas rescaled by a CSS rule
Mouse event position on a canvas rescaled by a CSS rule

Time:01-18

When a HTML canvas is in "standard" 1:1 scale (neither enlarged nor shrinked by a CSS rule), it is simple to get the mouse position in the canvas coordinates:

c2.onpointermove = (e) => {
    var mousemove_coords = { x: e.offsetX, y: e.offsetY };

Here is an example where the canvas' size gets modified by flex max-width CSS rules. The cursor should change only on top of the green square, which is not the case here, the cursor changes on bottom right of the square.

How to get the coordinates of a mouse event over a canvas when it is resized by a CSS rule?

var c1 = document.getElementById("canvas1"), ctx1 = c1.getContext("2d");
var c2 = document.getElementById("canvas2"), ctx2 = c2.getContext("2d");
var w = 2000, h = 1000;
var x0 = 50, y0 = 75, x1 = 100, y1 = 200;
ctx1.canvas.width = w;
ctx1.canvas.height = h; 
ctx1.rect(0, 0, w, h);
ctx1.fill();
ctx2.canvas.width = w;
ctx2.canvas.height = h; 
ctx2.rect(x0, y0, x1, y1);
ctx2.fillStyle = "green";
ctx2.fill();
c2.onpointermove = (e) => {
    var mousemove_coords = { x: e.offsetX, y: e.offsetY };
    var hovered =
        mousemove_coords.x >= x0 &&
        mousemove_coords.x <= x1 &&
        mousemove_coords.y >= y0 &&
        mousemove_coords.y <= y1;
    c2.style.cursor = hovered ? "move" : "crosshair";
};
body, .container { height: 100%; width: 100%; margin: 0; padding: 0; }
.container { display: flex; align-items: center; justify-content: center; background-color: yellow; }
.left-column { margin: 1rem; flex: 1; display: flex; align-items: center; justify-content: center; }
.canvas-wrapper { position: relative; }
#canvas1 { max-width: 100%; max-height: 100%; }
#canvas2 { position: absolute; top: 0; left: 0; max-width: 100%; max-height: 100%; }
.right-column { width: 300px; }
<div >
    <div >
        <div >
            <canvas id="canvas1"></canvas>
            <canvas id="canvas2"></canvas>
        </div>
    </div>
    <div >
        Hello world
    </div>
</div>

Edit: in my real code, I'm drawing on the canvas from frames received by HTTP requests with:

update_frame_task = setInterval(() => {
        fetch("get_frame")
            .then((r) => r.arrayBuffer())
            .then((arr) => {
                if (size === null) {
                    size = { w: 2000, h: 1000 };
                    ctx.canvas.width = size.w;
                    ctx.canvas.height = size.h;
                    ctx2.canvas.width = size.w;
                    ctx2.canvas.height = size.h;
                }
                var byteArray = new Uint8ClampedArray(arr);
                var imgData = new ImageData(byteArray, size.w, size.h);
                ctx.putImageData(imgData, 0, 0);
            });
    }, 200);

CodePudding user response:

You can scale the mouse co-ordinates to the CSS resize using getComputedStyle.

Also, canvas rect uses width and height.

var c1 = document.getElementById("canvas1"), ctx1 = c1.getContext("2d");
var c2 = document.getElementById("canvas2"), ctx2 = c2.getContext("2d");
var w = 2000, h = 1000;
var x0 = 50, y0 = 75, x1 = 100, y1 = 200;
ctx1.canvas.width = w;
ctx1.canvas.height = h; 
ctx1.rect(0, 0, w, h);
ctx1.fill();
ctx2.canvas.width = w;
ctx2.canvas.height = h; 
ctx2.rect(x0, y0, x1, y1);
ctx2.fillStyle = "green";
ctx2.fill();
ratio=w/parseFloat(window.getComputedStyle(c2).width);
c2.onpointermove = (e) => {

    var mousemove_coords = { x: e.offsetX*ratio, y: e.offsetY*ratio };

    k.innerHTML=mousemove_coords.x '<br>' mousemove_coords.y; // scaled coords

    var hovered =
        mousemove_coords.x >= x0 &&
        mousemove_coords.x <= x0 x1 &&
        mousemove_coords.y >= y0 &&
        mousemove_coords.y <= y0 y1;
    c2.style.cursor = hovered ? "move" : "crosshair";
};
body, .container { height: 100%; width: 100%; margin: 0; padding: 0; }
.container { display: flex; align-items: center; justify-content: center; background-color: yellow; }
.left-column { margin: 1rem; flex: 1; display: flex; align-items: center; justify-content: center; }
.canvas-wrapper { position: relative; }
#canvas1 { max-width: 100%; max-height: 100%; }
#canvas2 { position: absolute; top: 0; left: 0; max-width: 100%; max-height: 100%; }
.right-column { width: 300px; }
<div >
    <div >
        <div >
            <canvas id="canvas1"></canvas>
            <canvas id="canvas2"></canvas>
        </div>
    </div>
    <div id='k' >
        Hello world
    </div>
</div>

CodePudding user response:

Don't resize the CSS size of the canvas, the image in it only gets blurry. Instead, scale the image. As putImageData doesn't support scaling, use drawImage to draw the image into the canvas, it has the ability to scale the image. Since ImageData object can't be passed to drawImage, you've to convert it to ImageBitmap using createImageBitmap. With these changes your fetch should look something like this:

fetch("get_frame")
  .then((r) => r.arrayBuffer())
  .then(async (arr) => {
    if (size === null) {
      size = {
        w: 2000,
        h: 1000
      };
     }
    var byteArray = new Uint8ClampedArray(arr);
    var imgData = new ImageData(byteArray, size.w, size.h);
    var image = await createImageBitmap(imageData);
    ctx.drawImage(image, 0, 0, 800, 400);
  });

This might not fix all the issues, as you've already might have changed the size of the canvas with CSS. To fix that you could remove all the CSS of the canvas, and apply JMP's answer, and set the dimensions with width and height attributes of the canvas when the layout of the page is created/changes. Responsive canvases are a bit tricky to design.

  • Related