8

enter image description hereenter image description here

BACKGROUND: I have an HTML5 canvas and I have an image drawn on it. Now when the image is first loaded, it is loaded at a scale of 100%. The image is 5000 x 5000. And the canvas size is 600 x 600. So onload, I only see the first 600 x-pixels and 600 y-pixels. I have the option of scaling and translating the image on the canvas.

MY ISSUE: I am trying to figure out an algorithm that return the pixel coordinates of a mouse click relative to the image, not the canvas while taking into account scaling and translating. I know there are a lot of topics already on this, but nothing I've seen has worked. My issue is when I have multiple translations and scaling. I can zoom once and get the correct coordinates, and I can then scale and get the right coordinates again, but once I zoom or scale more than once, the coordinates are off.

Here is what I have so far.

//get pixel coordinates from canvas mousePos.x, mousePos.y
(mousePos.x - x_translation)/scale //same for mousePos.y

annotationCanvas.addEventListener('mouseup',function(evt){
                     dragStart = null;
                     if (!dragged) {
                       var mousePos = getMousePos(canvas, evt);
                       var message1 =  " mouse  x: " + (mousePos.x) + '  ' + "mouse y: " + (mousePos.y);
                       var message =  "  x: " + ((mousePos.x + accX)/currentZoom*currentZoom) + '  ' + "y: " + ((mousePos.y + accY)/currentZoom);
                       console.log(message);
                       console.log(message1);
                       console.log("zoomAcc = " + zoomAcc);
                       console.log("currentZoom = " + currentZoom);
                       ctx.fillStyle="#FF0000";
                       ctx.fillRect((mousePos.x + accX)/currentZoom, (mousePos.y + accY)/currentZoom, -5, -5);

                     }
             },true);
//accX and accY are the cumulative shift for x and y respectively, and xShift and xShift yShift are the incremental shifts of x and y respectively

where current zoom is the accumulative zoom. and zoomAcc is the single iteration of zoom at that point. So in this case, when I zoom in, zoomAcc is always 1.1, and currentZoom = currentZoom*zoomAcc.

Why is this wrong? if someone can please show me how to track these transformations and then apply them to mousePos.x and mousePos.y I would be grateful.

thanks

UPDATE:

In the image, the green dot is where I clicked, the red dot is where my calculation of that point is calculated, using markE's method. The m values are the matrix values in your markE's method.

flash
  • 265
  • 4
  • 15

2 Answers2

16

When you command the context to translate and scale, these are known as canvas transformations.

Canvas transformations are based on a matrix that can be represented by 6 array elements:

// an array representing the canvas affine transformation matrix 
var matrix=[1,0,0,1,0,0];

If you do context.translate or context.scale and also simultaneously update the matrix, then you can use the matrix to convert untransformed X/Y coordinates (like mouse events) into transformed image coordinates.

context.translate:

You can simultaneously do context.translate(x,y) and track that translation in the matrix like this:

// do the translate
// but also save the translate in the matrix
function translate(x,y){
    matrix[4] += matrix[0] * x + matrix[2] * y;
    matrix[5] += matrix[1] * x + matrix[3] * y;
    ctx.translate(x,y);
}

context.scale:

You can simultaneously do context.scale(x,y) and track that scaling the matrix like this:

// do the scale
// but also save the scale in the matrix
function scale(x,y){
    matrix[0] *= x;
    matrix[1] *= x;
    matrix[2] *= y;
    matrix[3] *= y;    
    ctx.scale(x,y);
}

Converting mouse coordinates to transformed image coordinates

The problem is the browser is unaware that you have transformed your canvas coordinate system and the browser will return mouse coordinates relative to the browser window--not relative to the transformed canvas.

Fortunately the transformation matrix has been tracking all your accumulated translations and scalings.

You can convert the browser’s window coordinates to transformed coordinates like this:

// convert mouseX/mouseY coordinates
// into transformed coordinates

function getXY(mouseX,mouseY){
    newX = mouseX * matrix[0] + mouseY * matrix[2] + matrix[4];
    newY = mouseX * matrix[1] + mouseY * matrix[3] + matrix[5];
    return({x:newX,y:newY});
}
markE
  • 102,905
  • 11
  • 164
  • 176
  • markE, thank you for providing a detailed answer. I have followed your instructions, and again, The first time I zoom I get the right coordinates, but when I shift after that zoom, it's off. I have added a picture to my question – flash Feb 12 '14 at 06:33
  • You are missing the automatic scaling due to the canvas' `width` and `height` attributes. I guess that should be fixed by adding the ratio of actual width and height to canvas width and height. – Domi Apr 19 '14 at 07:19
  • Also, please note that certain things will reset the canvas' internal transformation matrix. E.g. when setting `canvas.width` or `height`. Make sure to reset your matrix afterwards. – Domi Apr 19 '14 at 08:03
  • where is the context.rotate() and context.transform() ? – cuixiping Sep 03 '15 at 13:03
  • @cuixiping. This question is about scaling & translating. Here's another post that deals with rotation using the transformation matrix: http://stackoverflow.com/questions/18437039/canvashow-to-complete-translate-skew-rotate-in-just-one-transform-statement/18437802#18437802. BTW, `context.transform` is used to modify the existing context matrix. You can also use `context.setTransform` to overwrite the existing context matrix. ;-) – markE Sep 03 '15 at 15:53
1

There's a DOMMatrix object that will apply transformations to coordinates. I calculated coordinates for translated and rotated shapes as follows by putting my x and y coordinates into a DOMPoint and using a method of the DOMMatrix returned by CanvasRenderingContext2D.getTransform. This allowed a click handler to figure out which shape on the canvas was being clicked. This code apparently performs the calculation in markE's answer:

const oldX = 1, oldY = 1; // your values here
const transform = context.getTransform();
// Destructure to get the x and y values out of the transformed DOMPoint.
const { x, y } = transform.transformPoint(new DOMPoint(oldX, oldY));

DOMMatrix also has methods for translating and scaling and other operations, so you don't need to manually write those out anymore. MDN doesn't fully document them but does link to a page with the specification of non-mutating and mutating methods.

cyclaminist
  • 1,697
  • 1
  • 6
  • 12