I implemented a zoom function in my canvas just like this one: Zoom in on a point (using scale and translate)
Now I need to calculate the position of the mouse in relation to the canvas, I first tried like this:
var rect = this._canvas.getBoundingClientRect();
var x = ((event.clientX - rect.left) / (rect.right - rect.left) * this._canvas.width);
var y = ((event.clientY - rect.top) / (rect.bottom - rect.top) * this._canvas.height);
This works excellent until I zoom... I tried to do it like this:
var x = ((event.clientX - rect.left) / (rect.right - rect.left) * this._canvas.width) - this._canvas.offsetLeft ;
var y = ((event.clientY - rect.top) / (rect.bottom - rect.top) * this._canvas.height) - offset.top this._canvas.offSetTop ;
Any hint ? Or should I better use a JS library to interact with the canvas element ? If so, do you have any experience ?
Inverse Matrix
This answer include rotation as well because the scale is part of the rotation in the matrix you can't really exclude one or the other. But you can ignore the rotation (set it as zero) and just set scale and translation and it does what you want.
The inverse transform. It basically does the reverse of the standard 2D transformations. It will require that you keep track of the transformations so you can create the inverse transform, this can prove problematic in complex transforms if you wish to use ctx.rotation
, ctx.scale
, ctx.translate
or ctx.transform
. As you requirements are simple I have created a simple function to do the minimum transformation.
The following creates both the transformation matrix and the inverse transform as two arrays called matrix and invMatrix. The arguments are translation x,y (in canvas coordinates), scale, and rotation.
var matrix = [1,0,0,1,0,0];
var invMatrix = [1,0,0,1];
function createMatrix(x, y, scale, rotate){
var m = matrix; // just to make it easier to type and read
var im = invMatrix; // just to make it easier to type and read
// create the rotation and scale parts of the matrix
m[3] = m[0] = Math.cos(rotate) * scale;
m[2] = -(m[1] = Math.sin(rotate) * scale);
// add the translation
m[4] = x;
m[5] = y;
// calculate the inverse transformation
// first get the cross product of x axis and y axis
cross = m[0] * m[3] - m[1] * m[2];
// now get the inverted axis
im[0] = m[3] / cross;
im[1] = -m[1] / cross;
im[2] = -m[2] / cross;
im[3] = m[0] / cross;
}
Using the function
To use the function is simple. Just call with the desired values for position, scale and rotation.
Applying the inverse
To get the world coordinates (the transformed coordinates) from a pixel space (screen x, y) you need to apply the inverse transform
function toWorld(x,y){
var xx, yy, m, result;
m = invMatrix;
xx = x - matrix[4]; // remove the translation
yy = y - matrix[5]; // by subtracting the origin
// return the point {x:?,y:?} by multiplying xx,yy by the inverse matrix
return {
x: xx * m[0] + yy * m[2],
y: xx * m[1] + yy * m[3]
}
}
So if you want the mouse position in world space
var mouseWorldSpace = toWorld(mouse.x,mouse.y); // get the world space coordinates of the mouse
The function will convert any coordinate that is in screen space to the correct coordinate in world space.
Setting the 2D context transform
To use the transform you can set the 2D context transformation directly with
var m = matrix;
ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
Demo
And a demo to show it in use. A lot of extra code but I am sure you can find the parts you need. The Demo animates the transformation by rotating, scaling, and translating using createMatrix
then uses toWorld
to convert the mouse coordinates to the world space.
// the demo function
var demo = function(){
/** fullScreenCanvas.js begin **/
// create a full document canvas on top
var canvas = (function(){
var canvas = document.getElementById("canv");
if(canvas !== null){
document.body.removeChild(canvas);
}
// creates a blank image with 2d context
canvas = document.createElement("canvas");
canvas.id = "canv";
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
canvas.style.zIndex = 1000;
canvas.ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
return canvas;
})();
var ctx = canvas.ctx;
/** fullScreenCanvas.js end **/
/** MouseFull.js begin **/
// get the mouse data . This is a generic mouse handler I use so a little over kill for this example
var canvasMouseCallBack = undefined; // if needed
var mouse = (function(){
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false,
interfaceId : 0, buttonLastRaw : 0, buttonRaw : 0,
over : false, // mouse is over the element
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
getInterfaceId : function () { return this.interfaceId++; }, // For UI functions
startMouse:undefined,
};
function mouseMove(e) {
var t = e.type, m = mouse;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
m.alt = e.altKey;m.shift = e.shiftKey;m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
} else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") { m.buttonRaw = 0; m.over = false;
} else if (t === "mouseover") { m.over = true;
} else if (t === "mousewheel") { m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") { m.w = -e.detail;}
if (canvasMouseCallBack) { canvasMouseCallBack(m.x, m.y); }
e.preventDefault();
}
function startMouse(element){
if(element === undefined){
element = document;
}
"mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",").forEach(
function(n){element.addEventListener(n, mouseMove);});
element.addEventListener("contextmenu", function (e) {e.preventDefault();}, false);
}
mouse.mouseStart = startMouse;
return mouse;
})();
if(typeof canvas === "undefined"){
mouse.mouseStart();
}else{
mouse.mouseStart(canvas);
}
/** MouseFull.js end **/
// some stuff to draw a grid
var gridStart= -(canvas.width/10)*4;
var gridEnd = (canvas.width/10)*14;
var gridStepMajor = canvas.width/10;
var gridStepMinor = canvas.width/20;
var minorCol = "#999";
var majorCol = "#000";
var minorWidth = 1;
var majorWidth = 3;
// some stuf to animate the transformation
var timer = 0;
var timerStep = 0.01;
//----------------------------------------------------------------------------
// the code from the answer
var matrix = [1, 0, 0, 1, 0, 0]; // normal matrix
var invMatrix = [1, 0, 0, 1]; // inverse matrix
function createMatrix(x, y, scale, rotate){
var m = matrix; // just to make it easier to type and read
var im = invMatrix; // just to make it easier to type and read
// create the scale and rotation part of the matrix
m[3] = m[0] = Math.cos(rotate) * scale;
m[2] = -(m[1] = Math.sin(rotate) * scale);
// translation
m[4] = x;
m[5] = y;
// calculate the inverse transformation
// first get the cross product of x axis and y axis
cross = m[0] * m[3] - m[1] * m[2];
// now get the inverted axies
im[0] = m[3] / cross;
im[1] = -m[1] / cross;
im[2] = -m[2] / cross;
im[3] = m[0] / cross;
}
// function to transform to world space
function toWorld(x,y){
var xx, yy, m;
m = invMatrix;
xx = x - matrix[4];
yy = y - matrix[5];
return {
x: xx * m[0] + yy * m[2] ,
y: xx * m[1] + yy * m[3]
}
}
//----------------------------------------------------------------------------
// center of canvas
var cw = canvas.width / 2;
var ch = canvas.height / 2;
// the main loop
function update(){
var i,x,y,s;
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset the transform so we can clear
ctx.clearRect(0, 0, canvas.width, canvas.height); // clear the canvas
// animate the transformation
timer += timerStep;
x = Math.cos(timer) * gridStepMajor * 5 + cw; // position
y = Math.sin(timer) * gridStepMajor * 5 + ch;
s = Math.sin(timer/1.2) + 1.5; // scale
//----------------------------------------------------------------------
// create the matrix at x,y scale = s and rotation time/3
createMatrix(x,y,s,timer/3);
// use the created matrix to set the transformation
var m = matrix;
ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
//----------------------------------------------------------------------------
//draw a grid
ctx.lineWidth = 2;
ctx.beginPath();
ctx.strokeStyle = majorCol ;
ctx.lineWidth = majorWidth;
for(i = gridStart; i <= gridEnd; i+= gridStepMajor){
ctx.moveTo(gridStart, i);
ctx.lineTo(gridEnd, i);
ctx.moveTo(i, gridStart);
ctx.lineTo(i, gridEnd);
}
ctx.stroke();
ctx.strokeStyle = minorCol ;
ctx.lineWidth = minorWidth;
for(i = gridStart+gridStepMinor; i < gridEnd; i+= gridStepMinor){
ctx.moveTo(gridStart, i);
ctx.lineTo(gridEnd, i);
ctx.moveTo(i, gridStart);
ctx.lineTo(i, gridEnd);
}
ctx.stroke();
//---------------------------------------------------------------------
// get the mouse world coordinates
var mouseWorldPos = toWorld(mouse.x, mouse.y);
//---------------------------------------------------------------------
// marke the location with a cross and a circle;
ctx.strokeStyle = "red";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(mouseWorldPos.x - gridStepMajor, mouseWorldPos.y)
ctx.lineTo(mouseWorldPos.x + gridStepMajor, mouseWorldPos.y)
ctx.moveTo(mouseWorldPos.x, mouseWorldPos.y - gridStepMajor)
ctx.lineTo(mouseWorldPos.x, mouseWorldPos.y + gridStepMajor)
ctx.stroke();
ctx.fillStyle = "red";
ctx.strokeStyle = "yellow";
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(mouseWorldPos.x, mouseWorldPos.y, 6, 0, Math.PI*2);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "Blue";
ctx.setTransform(1,0,0,1,0,0);
ctx.font = "18px Arial";
var str = "Mouse canvas X: "+ mouse.x + " Y: " + mouse.y;
ctx.fillText(str , 10 ,18);
var str = "Mouse world X: "+ mouseWorldPos.x.toFixed(2) + " Y: " + mouseWorldPos.y.toFixed(2);
ctx.fillText(str , 10 ,36);
// if not over request a new animtion frame
if(!endItAll){
requestAnimationFrame(update);
}else{
// if done remove the canvas
var can = document.getElementById("canv");
if(can !== null){
document.body.removeChild(can);
}
// flag that we are ready to start again
endItAll = false;
}
}
update(); // start the animation
}
// Flag to indicate that the current execution should shut down
var endItAll = false;
// resizes but waits for the current running animnation to shut down
function resizeIt(){
endItAll = true;
function waitForIt(){
if(!endItAll){
demo();
}else{
setTimeout(waitForIt, 100);
}
}
setTimeout(waitForIt, 100);
}
// starts the demo
demo();
// listen to resize events and resize canvas if needed
window.addEventListener("resize",resizeIt)
Go step by step :
Find the coordinates of the mouse on the canvas:
var rect = canvas.getBoundingClientRect();
var xMouse = event.clientX - rect.left;
var yMouse = event.clientY - rect.top;
Normalize those coordinates so they are in [0;1] :
var relX = xMouse / canvas.width;
var relY = yMouse / canvas.height;
now say you view is defined by a rect called... well... viewRect, the position of the mouse in the view is :
var viewX = viewRect.left + relX*(viewRect.right-viewRect.left);
var viewY = viewRect.top + relY*(viewRect.bottom-viewRect.top);
When you launch your app your rect is 0,0,canvasWidth, canvasHeight.
When you click, you have to adjust your rect.
If clicking means zooming by zFactor
at viewX, viewY, code will look like :
var newWidth = viewRect.width/zFactor;
var newHeight = viewRect.height/zFactor;
viewRect.left = viewX - newWidth/2;
viewRect.right = viewX + newWidth/2;
viewRect.top = viewY - newHeight/2;
viewRect.bottom = viewY + newHeight/2;
your draw method should look like :
context.save();
context.translate((viewRect.left+viewRect.right )/ 2, ...) ;
var scaleFactor = (viewRect.right+viewRect.left ) / canvasWidth;
context.scale(scaleFactor, scaleFactor);
... draw
context.restore();
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With