Hi I am trying to create isometric graphic app with React, mostly base on the code here.
I achieved most of the functions (ie. zoom and scroll).
But hovering tiles after zooming gives me wrong mouse position (hover position).
You can see what I mean here.
You can zoom with scrolling vertically.
When it is not zoomed in or out, hovering tile works correctly (tile color changes where the mouse positions).
But after zooming out/in it is not working right.
Does anyone know how to get the mouse position or tile index correctly after zooming in/out?
Implemented code can be found on my Github repo here
Code snippet for getting target tile is below:
const handleHover = (x: number, y: number) => {
const { e: xPos, f: yPos } = ctx.getTransform()
const mouse_x = mouseRef.current.x - x - xPos
const mouse_y = mouseRef.current.y - y - yPos
const hoverTileX =
Math.floor(
mouse_y / Tile.TILE_HEIGHT + mouse_x / Tile.TILE_WIDTH
) - 1
const hoverTileY = Math.floor(
-mouse_x / Tile.TILE_WIDTH + mouse_y / Tile.TILE_HEIGHT
)
if (
hoverTileX >= 0 &&
hoverTileY >= 0 &&
hoverTileX < gridSize &&
hoverTileY < gridSize
) {
const renderX =
x + (hoverTileX - hoverTileY) * Tile.TILE_HALF_WIDTH
const renderY =
y + (hoverTileX + hoverTileY) * Tile.TILE_HALF_HEIGHT
renderTileHover(ctx)(renderX, renderY + Tile.TILE_HEIGHT)
}
}
I am not good at maths so I really need help...
Thank you.
I figured out how to achieve this.
I will leave it here so anyone who has the same issue wont waste a lot of time for this kind of issue.
My code is like this:
/**
* #param context canvas context 2d
* #param inputX mouse/touch input position x (ie. clientX)
* #param inputY mouse/touch input position y (ie. clientY)
* #returns {x, y} x and y position of inputX/Y which map scale and position are taken into account
*/
export const getTransformedPoint = (context: CanvasRenderingContext2D, inputX: number, inputY: number) => {
const transform = context.getTransform()
const invertedScaleX = DEFAULT_MAP_SCALE / transform.a
const invertedScaleY = DEFAULT_MAP_SCALE / transform.d
const transformedX = invertedScaleX * inputX - invertedScaleX * transform.e
const transformedY = invertedScaleY * inputY - invertedScaleY * transform.f
return { x: transformedX, y: transformedY }
}
/**
*
* #param startPosition position where map start rendered (Position2D has {x: number, y: number} type)
* #param inputX mouse/touch input position x (ie. clientX)
* #param inputY mouse/touch input position x (ie. clientY)
* #returns positionX, positionY: tile position x, y axis
*/
export const getTilePosition = (
startPosition: Position2D,
inputX: number,
inputY: number
): { positionX: number; positionY: number } => {
const positionX =
Math.floor((inputY - startPosition.y) / TILE_HEIGHT + (inputX - startPosition.x) / TILE_WIDTH) - 1
const positionY = Math.floor(
(inputY - startPosition.y) / TILE_HEIGHT - (inputX - startPosition.x) / TILE_WIDTH
)
return { positionX, positionY }
}
// usage
const onClick = (e: MouseEvent) => {
const { x: mouseX, y: mouseY } = getTransformedPoint(ctx, e.clientX, e.clientY)
const { positionX, positionY } = getTilePosition(startPositionRef.current, mouseX, mouseY)
// Do something with positionX and positionY...
// ie.
if (return positionX >= 0 && positionY >= 0 && positionX < GRID_SIZE && positionY < GRID_SIZE) {
// code when a user clicks a tile within the map
}
}
I referenced this for calculating the mouse position when the map is zoomed out/in.
Try below
const handleHover = (x: number, y: number) => {
// use the current scale of the canvas context
const { a: scale, e: xPos, f: yPos } = ctx.getTransform()
const mouse_x = (mouseRef.current.x - x - xPos) * scale
const mouse_y = (mouseRef.current.y - y - yPos) * scale
// rest of the code...
}
I'm building a basic game using plain javascript and I am trying to rotate my object to follow my mouse.
I've tried getting the client's mouse X and Y then subtracting the canvas width and height divided by two. Then taking those values and inputing it into Math.atan2(). However, I feel the issue may be in my transform and rotate. The code bellow is what I've tried.
WIDTH = c.height;
HEIGHT = c.width;
document.onmousemove = function(ve){
let cX = -c.width / 2;
let cY = -c.height / 2;
let x = ve.offsetX;
let y = ve.offsetY;
var rX = cX + x - 8;
var rY = cY + y - 8;
player.angle = Math.atan2(rX, rY) / Math.PI * 180;
}
function update(){
var now = Date.now();
dt = now - lastUpdate;
ctx.clearRect(0, 0, WIDTH, HEIGHT);
ctx.setTransform(1, 0, 0, 1, WIDTH / 2, HEIGHT / 2);
ctx.rotate(player.angle + 10);
drawCircle(player.x, player.y, 20, 0, 180, "red");
tx.setTransform(1, 0, 0, 1, 0, 0);
}
setInterval(update, dt/10000);
The player spins around my mouse in wide circles with no apparent pattern.
Here's a gif showing what's happening.
https://gyazo.com/006c99879ecf219791d059de14d98b74
In order to rotate the object to follow the mouse you need to get the angle between the previous position of the mouse and the actual position of the mouse and use this angle to rotate the object. Also the object is drawn with the tip in the origin of the canvas {x:0,y:0} so you'll need to translate the player to the position of the mouse.
I hope this is what you need.
const ctx = c.getContext("2d")
const HEIGHT = c.height = window.innerHeight;
const WIDTH = c.width = window.innerWidth;
let m = {x:0,y:0}
let prev = {x:0,y:0}
let angle = 0;
c.addEventListener("mousemove",(evt)=>{
ctx.clearRect(-WIDTH, -HEIGHT, 2*WIDTH, 2*HEIGHT);
// the previous position of the mouse
prev.x = m.x;
prev.y = m.y;
//the actual position of the mouse
m = oMousePos(c, evt);
// if the mpuse is moving get the angle between the previoue position and the actual position of the mouse
if(m.x != prev.x && m.y != prev.y){
angle = Math.atan2(m.y-prev.y, m.x-prev.x)
}
ctx.restore();
ctx.save();
ctx.translate(m.x, m.y);
ctx.rotate(angle);
drawPlayer();
})
function drawPlayer(){
ctx.fillStyle = "black";
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(-20,-5);
ctx.lineTo(-20,5);
ctx.lineTo(0,0);
ctx.closePath();
ctx.fill()
}
// a function to detect the mouse position
function oMousePos(canvas, evt) {
var ClientRect = canvas.getBoundingClientRect();
return { //objeto
x: Math.round(evt.clientX - ClientRect.left),
y: Math.round(evt.clientY - ClientRect.top)
}
}
<canvas id="c"></canvas>
As an observation: in your code you have Math.atan2(rX, rY) The first argument has to be y.
ive drawn a checker board on a canvas and i want to highlight the square which the mouse is over. I have given it a go but the furthest i can get is with it half a square out of sync.
Here is my code:
canvas.addEventListener('mousemove', function(evt)
{
const position = getGridPoint(evt);
drawBoard(); //Clears the last highlight
context.lineWidth='3'; //Draws the new highlight
context.strokeStyle = 'yellow';
context.rect(position.x * board.squareW, position.y * board.squareH, board.squareW, board.squareH);
context.stroke();
})
function getGridPoint(evt)
{
const rect = canvas.getBoundingClientRect();
//board.w = width of the board
//board.squareW = width of each tile on the board
const x = Math.round((evt.clientX - rect.left) / (rect.right - 2 - rect.left) * board.w);
const y = Math.round((evt.clientY - rect.top) / (rect.bottom - 2 - rect.top) * board.h);
const roundX = Math.round(x / board.squareW);
const roundY = Math.round(y / board.squareH);
return {
x: roundX,
y: roundY
};
}
Its something in the 2nd function where im using math.round
So it will work if i manually subtract half a tile's width from x and y but it seems a hacky way and id rather do it properly in the first place
JSfiddle: http://jsfiddle.net/5toudex0/3/
try this for getTile
function getTile(evt)
{
const rect = canvas.getBoundingClientRect();
//board.w = width of the board
//board.squareW = width of each tile on the board
const x = Math.floor((evt.clientX - rect.left) / board.squareW);
const y = Math.floor((evt.clientY - rect.top) / board.squareH);
return {
x: x,
y: y
};
}
With a little change made to the mousemove handler for the canvas, you can (0) directly get the client position of the mouse and then (1) compute the col/row indices for the tile to highlight.
Consider the following change to your code:
canvas.addEventListener('mousemove', function(evt)
{
const position = getTile(evt);
var xPos = evt.clientX, yPos = evt.clientY;
var xTileIndex = (xPos / board.squareW)>>0;
var yTileIndex = (yPos / board.squareH)>>0;
console.log(`x,y = ${xPos},${yPos}`);
console.log(`x,y = ${xTileIndex},${yTileIndex}`);
In the example fiddle: https://jsfiddle.net/krazyjakee/uazy86m4/ you can drag the mouse around underneath the vector point shown as a blue square. You will see the line draw a path from the vector, through the mouse and to the edge of the viewport where a green square is shown, indicating it has found the edge. However, above the vector, the green square disappears as it fails to detect the edge of the viewport.
Here is the current logic I am using to detect the edge.
const angle = Math.atan2(mouse.y - vectorCenter.y, mouse.x - vectorCenter.x);
const cosAngle = Math.abs(Math.cos(angle));
const sinAngle = Math.abs(Math.sin(angle));
const vx = (viewport.width - vectorCenter.x) * sinAngle;
const vy = (viewport.height - vectorCenter.y) * cosAngle;
const vpMagnitude = vx <= vy ?
(viewport.width - vectorCenter.x) / cosAngle :
(viewport.height - vectorCenter.y) / sinAngle;
const viewportX = vectorCenter.x + Math.cos(angle) * vpMagnitude;
const viewportY = vectorCenter.y + Math.sin(angle) * vpMagnitude;
const viewPortEdge = {
x: viewportX,
y: viewportY,
};
Please help me figure out how to correctly detect the position in the top edge of the viewport.
I didn't look into why exactly this fails for the top because there's an easier approach to this than dealing with angles. You can get the position by some simple vector calculations.
First, for the sake of explicitness and to prevent hardcoding any values into the computation I've extended your viewport
const viewport = {
x: 0,
y: 0,
width: window.innerWidth,
height: window.innerHeight,
get left(){ return this.x },
get right(){ return this.x + this.width },
get top(){ return this.y },
get bottom(){ return this.y + this.height },
};
now the calculation:
//prevent division by 0
const notZero = v => +v || Number.MIN_VALUE;
let vx = mouse.x - vectorCenter.x;
let vy = mouse.y - vectorCenter.y;
//that's why I've extended the viewport, so I don't have to hardcode any values here
//Math.min() to check wich border I hit first, X or Y
let t = Math.min(
((vx<0? viewport.left: viewport.right) - vectorCenter.x) / notZero(vx),
((vy<0? viewport.top: viewport.bottom) - vectorCenter.y) / notZero(vy)
);
const viewPortEdge = {
x: vectorCenter.x + vx * t,
y: vectorCenter.y + vy * t,
};
so t is the factor by wich I have to scale the vector between the mouse and the vectorCenter to hit the closest edge in that particular direction.
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();
Instead of keeping track of the various transformations, I inquired of the canvas for the current transform:
function mouseUp(canvas, event) {
const rect = canvas.getBoundingClientRect();
const transform = graphics.getTransform();
const canvasX = (event.clientX - rect.left - transform.e) / transform.a;
const canvasY = (event.clientY - rect.top - transform.f) / transform.d;
The doesn't deal with skew, but it gives a general idea of the approach I'm using.