Asked  7 Months ago    Answers:  5   Viewed   42 times

I'm trying to draw with the mouse over a HTML5 canvas, but the only way that it seems to work well is if the canvas is in the position 0,0 (upper left corner) if I change the canvas position, for some reason it doesn't draw like it should. Here is my code.

 function createImageOnCanvas(imageId){
    document.getElementById("imgCanvas").style.display = "block";
    document.getElementById("images").style.overflowY= "hidden";
    var canvas = document.getElementById("imgCanvas");
    var context = canvas.getContext("2d");
    var img = new Image(300,300);
    img.src = document.getElementById(imageId).src;
    context.drawImage(img, (0),(0));
}

function draw(e){
    var canvas = document.getElementById("imgCanvas");
    var context = canvas.getContext("2d");
    posx = e.clientX;
    posy = e.clientY;
    context.fillStyle = "#000000";
    context.fillRect (posx, posy, 4, 4);
}

The HTML part

 <body>
 <div id="images">
 </div>
 <canvas onmousemove="draw(event)" style="margin:0;padding:0;" id="imgCanvas"
          class="canvasView" width="250" height="250"></canvas> 

I have read there's a way of creating a simple function in JavaScript to get the right position, but I have no idea about how to do it.

 Answers

77

The Simple 1:1 Scenario

For situations where the canvas element is 1:1 compared to the bitmap size, you can get the mouse positions by using this snippet:

function getMousePos(canvas, evt) {
    var rect = canvas.getBoundingClientRect();
    return {
      x: evt.clientX - rect.left,
      y: evt.clientY - rect.top
    };
}

Just call it from your event with the event and canvas as arguments. It returns an object with x and y for the mouse positions.

As the mouse position you are getting is relative to the client window you'll have to subtract the position of the canvas element to convert it relative to the element itself.

Example of integration in your code:

//put this outside the event loop..
var canvas = document.getElementById("imgCanvas");
var context = canvas.getContext("2d");

function draw(evt) {
    var pos = getMousePos(canvas, evt);

    context.fillStyle = "#000000";
    context.fillRect (pos.x, pos.y, 4, 4);
}

Note: borders and padding will affect position if applied directly to the canvas element so these needs to be considered via getComputedStyle() - or apply those styles to a parent div instead.

When Element and Bitmap are of different sizes

When there is the situation of having the element at a different size than the bitmap itself, for example, the element is scaled using CSS or there is pixel-aspect ratio etc. you will have to address this.

Example:

function  getMousePos(canvas, evt) {
  var rect = canvas.getBoundingClientRect(), // abs. size of element
      scaleX = canvas.width / rect.width,    // relationship bitmap vs. element for X
      scaleY = canvas.height / rect.height;  // relationship bitmap vs. element for Y

  return {
    x: (evt.clientX - rect.left) * scaleX,   // scale mouse coordinates after they have
    y: (evt.clientY - rect.top) * scaleY     // been adjusted to be relative to element
  }
}

With transformations applied to context (scale, rotation etc.)

Then there is the more complicated case where you have applied transformation to the context such as rotation, skew/shear, scale, translate etc. To deal with this you can calculate the inverse matrix of the current matrix.

Newer browsers let you read the current matrix via the currentTransform property and Firefox (current alpha) even provide a inverted matrix through the mozCurrentTransformInverted. Firefox however, via mozCurrentTransform, will return an Array and not DOMMatrix as it should. Neither Chrome, when enabled via experimental flags, will return a DOMMatrix but a SVGMatrix.

In most cases however you will have to implement a custom matrix solution of your own (such as my own solution here - free/MIT project) until this get full support.

When you eventually have obtained the matrix regardless of path you take to obtain one, you'll need to invert it and apply it to your mouse coordinates. The coordinates are then passed to the canvas which will use its matrix to convert it to back wherever it is at the moment.

This way the point will be in the correct position relative to the mouse. Also here you need to adjust the coordinates (before applying the inverse matrix to them) to be relative to the element.

An example just showing the matrix steps

function draw(evt) {
    var pos = getMousePos(canvas, evt);        // get adjusted coordinates as above
    var imatrix = matrix.inverse();            // get inverted matrix somehow
    pos = imatrix.applyToPoint(pos.x, pos.y);  // apply to adjusted coordinate

    context.fillStyle = "#000000";
    context.fillRect(pos.x-1, pos.y-1, 2, 2);
}

An example of using currentTransform when implemented would be:

    var pos = getMousePos(canvas, e);          // get adjusted coordinates as above
    var matrix = ctx.currentTransform;         // W3C (future)
    var imatrix = matrix.invertSelf();         // invert

    // apply to point:
    var x = pos.x * imatrix.a + pos.y * imatrix.c + imatrix.e;
    var y = pos.x * imatrix.b + pos.y * imatrix.d + imatrix.f;

Update I made a free solution (MIT) to embed all these steps into a single easy-to-use object that can be found here and also takes care of a few other nitty-gritty things most ignore.

Tuesday, June 1, 2021
 
CAMason
answered 7 Months ago
81

You have to wait that your image has loaded before you can paint it on the canvas.

To do so, simply use the load event handler of your <img> element :

// create a new image
var img = new Image();
// declare a function to call once the image has loaded
img.onload = function(){
  var canvas = document.createElement('canvas');
  canvas.width = img.width;
  canvas.height = img.height;
  var context = canvas.getContext('2d');
  context.drawImage(img, 0,0);
  var dataURL = canvas.toDataURL();
  // now you can do something with the dataURL
  doSomething(dataURL);
}
// now set the image's src
img.src = "http://somerandomWebsite/picture.png";

Also, for the canvas' context.toDataURL() and context.getImageData to work properly, you have to get your image resource in a cross-origin compliant way, otherwise the canvas is "tainted" which means that any method to get pixels data will be blocked.

  • If your image comes from the same server, no problem.
  • If your image is served from an external server, be sure that it allows yours in its cross-origin headers and set the img.crossOrigin to "use-credentials".
  • If the server does allow anonymous requests, you can set the img.crossOrigin to "anonymous".

Nota Bene : CORS header is sent by the server, the cross-origin attribute will only let it know that you want to use CORS to get the image data, in no way you can circumvent it if the server is not set properly.
Also some UserAgents (IE & Safari) still haven't implemented this attribute.

Edge Case : If some of your images are from your server and some are from a CORS compliant one, then you may want to use the onerror event handler which should fire if you set the cross-origin attribute to "anonymous" on a non CORS server.

function corsError(){
  this.crossOrigin='';
  this.src='';
  this.removeEventListener('error', corsError, false);
} 
img.addEventListener('error', corsError, false);
Tuesday, June 1, 2021
 
Saxophlutist
answered 7 Months ago
39
context.drawImage(imageObj, 0, 0, 100, 100 * imageObj.height / imageObj.width)
Sunday, August 8, 2021
 
DHranger
answered 4 Months ago
90

You're setting your canvas width and height in CSS. That just stretches the canvas the same as it would an image.

The effect is drawing in the wrong place.

Instead you need to set your canvas dimensions on the tag itself:

<canvas width="400" height="400"></canvas>
Monday, November 1, 2021
 
Jean
answered 1 Month ago
39

In my mind, the way zoom works is that based on the position of the point of interest (in your case mouse), the zoom has a weighted impact on the (x, y) position of the canvas. If the mouse is at the top left, this weight should be (0,0), if it's at the bottom right, it should be (1,1). And the centre should be (0.5,0.5).

So, assuming we have the weights, whenever the zoom changes, the weights show us how far the top left corner of the canvas is from the point of interest, and we should move it away by the weight*dimension*(delta zoom).

Note that in order to calculate the weights, we need to consider the zoomed in/out value of the dimensions. So if width is 100 and zoom is 0.5, we should assume the width is 50. So all the values would be absolute (since the mouse (x,y) are absolute).

so, it's like:

function worldZoom(e) {
    const {x, y, deltaY} = e;
    const direction = deltaY > 0 ? -1 : 1;
    const factor = 0.01;
    const zoom = 1 * direction * factor;

    // compute the weights for x and y
    const wx = (x-controls.view.x)/(width*controls.view.zoom);
    const wy = (y-controls.view.y)/(height*controls.view.zoom);

    // apply the change in x,y and zoom.
    controls.view.x -= wx*width*zoom;
    controls.view.y -= wy*height*zoom;
    controls.view.zoom += zoom;
}

You can try it in this codepen.

Or a bit simpler, compute the weights inline:

controls.view.x -= (x-controls.view.x)/(controls.view.zoom)*zoom;
controls.view.y -= (y-controls.view.y)/(controls.view.zoom)*zoom;
controls.view.zoom += zoom;
Tuesday, November 23, 2021
 
magix
answered 1 Week ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :  
Share