Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaScript - Wrong mouse position when drawing on canvas

Here's a fiddle of the problem:

https://jsfiddle.net/y5cu0pxf/

I've searched and tried so much but can't find the problem. I just want the pen to draw exactly where the mouse is clicked, but it's offset for some reason.

Any ideas?

Here's the code:

var imageLoader = document.getElementById('imageLoader');
imageLoader.addEventListener('change', handleImage, false);

var canvas = document.getElementById('imageCanvas');
var ctx = canvas.getContext('2d');

function handleImage(e){

  var reader = new FileReader();

  reader.onload = function(event){

    var img = new Image();

    img.onload = function(){

      canvas.width = window.innerWidth * 0.5;
      canvas.height = window.innerHeight;

      var hRatio = canvas.width / img.width;
      var vRatio =  canvas.height / img.height;
      var ratio = Math.min (hRatio, vRatio);
      var centerShift_x = (canvas.width - (img.width * ratio)) / 2;
      var centerShift_y = (canvas.height - (img.height * ratio)) / 2;
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.drawImage(img, 0, 0, img.width, img.height,
                    centerShift_x, centerShift_y, img.width * ratio, img.height * ratio);
    }

    img.src = event.target.result;
  }
  reader.readAsDataURL(e.target.files[0]);
}

var isDrawing;
var rect = canvas.getBoundingClientRect();
var offsetX = rect.left;
var offsetY = rect.top;

canvas.onmousedown = function(e) {
  isDrawing = true;
  ctx.moveTo(e.clientX - offsetX, e.clientY - offsetY);
};
canvas.onmousemove = function(e) {
  if (isDrawing) {
    ctx.lineTo(e.clientX - offsetX, e.clientY - offsetY);
    ctx.stroke();
  }
};
canvas.onmouseup = function() {
  isDrawing = false;
};
like image 665
numberjak Avatar asked May 08 '17 16:05

numberjak


1 Answers

Canvas and the mouse

The canvas and size

The canvas has two size properties, one denotes the resolution in pixels, and the other specifise the display size in CSS units. The two are independent of each other.

// HTML <canvas id = "myCan"><canvas>
// To set the resolution use the canvas width and height properties
myCan.width = 1024;
myCan.height = 1024;
// To set the display size use the style width and height
myCan.style.width = "100%"; // Note you must post fix the unit type %,px,em
myCan.style.height = "100%";

By default the canvas resolution is set to 300 by 150 pixels. The canvas display size will depend on the layout and CSS rules.

When rendering to the canvas 2D context you render in pixel coordinates not style coordinates.

To get the position of the canvas

var canvasBounds = myCan.getBoundingClientRect();

The mouse

Mouse coordinates are in pixels.

Use one event handler to handle all mouse IO

const mouse = {
    x: 0, y: 0,                        // coordinates
    lastX: 0, lastY: 0,                // last frames mouse position 
    b1: false, b2: false, b3: false,   // buttons
    buttonNames: ["b1", "b2", "b3"],   // named buttons
}
function mouseEvent(event) {
    var bounds = myCan.getBoundingClientRect();
    // get the mouse coordinates, subtract the canvas top left and any scrolling
    mouse.x = event.pageX - bounds.left - scrollX;
    mouse.y = event.pageY - bounds.top - scrollY;

To get the correct canvas coordinate you need to scale the mouse coordinates to match the canvas resolution coordinates.

// first normalize the mouse coordinates from 0 to 1 (0,0) top left
// off canvas and (1,1) bottom right by dividing by the bounds width and height
mouse.x /= bounds.width; 
mouse.y /= bounds.height; 

// then scale to canvas coordinates by multiplying the normalized coords with the canvas resolution

mouse.x *= myCan.width;
mouse.y *= myCan.height;

Then just get the other info you are interested in.

    if (event.type === "mousedown") {
         mouse[mouse.buttonNames[event.which - 1]] = true;  // set the button as down
    } else if (event.type === "mouseup") {
         mouse[mouse.buttonNames[event.which - 1]] = false; // set the button up
    }
}

Capturing the mouse while dragging (button down)

When handling the mouse for something like a drawing app that uses the canvas you can not add the event listeners to the canvas directly. If you do you lose the mouse when the user moves off the canvas. If while off the canvas the user releases the mouse you will not know the button is up . The result is the button getting stuck on down. (As in your fiddle)

To capture the mouse so that you get all events that happen while the button is down, event if the user has moved of the canvas, or off the page, or off the screen you need to listen to the document's mouse events.

So to add the above mouse event listener

document.addEventListener("mousemove", mouseEvent);
document.addEventListener("mousedown", mouseEvent);
document.addEventListener("mouseup",   mouseEvent);

Now the mouseEvent handles all page clicks and captures the mouse exclusively to your page while the button is down.

You can check is a mouse event started on the canvas by just checking the event.target

// only start mouse down events if the users started on the canvas
if (event.type === "mousedown" && event.target.id === "myCan") {
     mouse[mouse.buttonNames[event.which - 1]] = true; 
}

Events should not render

Mouse events can fire very rapidly, with some settups firing mouse moves at over 600 events per second. If you use the mouse event to render to the canvas you will be wasting a lot of CPU time, and you will also be rending outside the DOMs synced compositing and layout engines.

Use a animation loop via requestAnimationFrame to draw.

function mainLoop(time) {
   if (mouse.b1) {  // is button 1 down?
       ctx.beginPath();
       ctx.moveTo(mouse.lastX,mouse.lastY);
       ctx.lineTo(mouse.x,mouse.y);
       ctx.stroke();
   }


   // save the last known mouse coordinate here not in the mouse event
   mouse.lastX = mouse.x;
   mouse.lastY = mouse.y;
   requestAnimationFrame(mainLoop); // get next frame
}
// start the app
requestAnimationFrame(mainLoop);

That should get your drawing app working as you want.

like image 71
Blindman67 Avatar answered Sep 20 '22 09:09

Blindman67