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 am using the following function to get mouse coordinates on canvas after performing rotations.
function getWindowToCanvas(canvas, x, y) {
const ctx = canvas.getContext("2d");
var transform = ctx.getTransform();
var rect = canvas.getBoundingClientRect();
var screenX = (x - rect.left) * (canvas.width / rect.width);
var screenY = (y - rect.top) * (canvas.height / rect.height);
if (transform.isIdentity) {
return {
x: screenX,
y: screenY
};
} else {
console.log(transform.invertSelf());
const invMat = transform.invertSelf();
return {
x: Math.round(screenX * invMat.a + screenY * invMat.c + invMat.e),
y: Math.round(screenX * invMat.b + screenY * invMat.d + invMat.f)
};
}
}
I used the inverted transform matrix after reading html5-canvas-transformation-algorithm and best-way-to-transform-mouse-coordinates-to-html5-canvass-transformed-context
I am letting the user draw rectangles with the mouse, and I need to get the mouse x,y coordinates after transformations, but once the canvas is rotated (say by 90 deg) then the rectangles no longer follow the mouse pointer.
Does anyone know what I'm doing wrong?
Thanks to #MarkusJarderot and jsFiddle getting mouse coordinates from un-rotated canvas I was able to get a solution that is close to perfect. I don't quite understand it, but it works much better.
function getWindowToCanvas(canvas, e) {
//first calculate normal mouse coordinates
e = e || window.event;
var target = e.target || e.srcElement,
style = target.currentStyle || window.getComputedStyle(target, null),
borderLeftWidth = parseInt(style["borderLeftWidth"], 10),
borderTopWidth = parseInt(style["borderTopWidth"], 10),
rect = target.getBoundingClientRect(),
offsetX = e.clientX - borderLeftWidth - rect.left,
offsetY = e.clientY - borderTopWidth - rect.top;
let x = (offsetX * target.width) / target.clientWidth;
let y = (offsetY * target.height) / target.clientHeight;
//then adjust coordinates for the context's transformations
const ctx = canvas.getContext("2d");
var transform = ctx.getTransform();
const invMat = transform.invertSelf();
return {
x: x * invMat.a + y * invMat.c + invMat.e,
y: x * invMat.b + y * invMat.d + invMat.f
};
}
The only issue remaining is that, when rotated say 45deg, drawing a rectangle with ctx.rect() draws a rectangle that parallels with respect to the canvas, not to the window, so the rectangle is slanted even though it is finally in the right place. I want to draw rectangles with respect to the window, not the canvas. However, this may just be how ctx.rect() works, and I'll need to update later. For now, this could help others.
UPDATE
Figured out original bug.
Since I didn't understand why my original function was not working, used the above solution to start trouble-shooting it. It turns out that the reason the above code did not work is because I was calling console.log(transform.invertSelf()) to see the transform while I was debugging. This mutated the transform. So, when I called var invMat = transform.invertSelf() right after, I inverted it yet again! I should have paid attention to the 'self' in 'invertSelf'.
This function now works
function getWindowToCanvas(canvas, x, y) {
var rect = canvas.getBoundingClientRect();
var screenX = (x - rect.left) * (canvas.width / rect.width);
var screenY = (y - rect.top) * (canvas.height / rect.height);
const ctx = canvas.getContext("2d");
var transform = ctx.getTransform();
if (transform.isIdentity) {
return {
x: screenX,
y: screenY
};
} else {
// console.log(transform.invertSelf()); //don't invert twice!!
const invMat = transform.invertSelf();
return {
x: Math.round(screenX * invMat.a + screenY * invMat.c + invMat.e),
y: Math.round(screenX * invMat.b + screenY * invMat.d + invMat.f)
};
}
}
I'm having some issues calculating the corners of a rotated rectangle within a rotated container with both having offset x/y co-ords.
The pivot is off but I'm not sure of the solution. The following scenarios work:
(x, y, rotation)
image = 0, 0, 45
container = 100, 100, 45
image = 200, 0, 45
container = 100, 100, 0
however setting the rotation of the container, and the image co-ords messes up the pivot e.g.
image = 200, 0, 45
container = 100, 100, 45
Below is the code for calculating the corners of the image in global co-ordinate space:
public get corners() {
const worldData = this.worldData;
//Get angle of object in radians;
const radAngle = worldData.rotation * Math.PI / 180;
const pivotX = worldData.pivotX;
const pivotY = worldData.pivotY;
const width = this.sourceWidth * worldData.scaleX;
const height = this.sourceHeight * worldData.scaleY;
const x = worldData.x;//this.x;
const y = worldData.y;//this.y;
//Get the corners
const c1 = this.getCorner(pivotX, pivotY, x, y, radAngle);
const c2 = this.getCorner(pivotX, pivotY, x + width, y, radAngle);
const c3 = this.getCorner(pivotX, pivotY, x + width, y + height, radAngle);
const c4 = this.getCorner(pivotX, pivotY, x, y + height, radAngle);
return {c1, c2, c3, c4};
}
public get worldData() {
let x = this.x;
let y = this.y;
let pivotX = this.x;
let pivotY = this.y;
let rotation = this.rotation;
let scaleX = this.scaleX;
let scaleY = this.scaleY;
let parent = this.parent;
while(parent) {
x += parent.x;
y += parent.y;
pivotX += parent.x;
pivotY += parent.y;
rotation += parent.rotation;
scaleX *= parent.scaleX;
scaleY *= parent.scaleY;
parent = parent.parent;
}
return {x, y, scaleX, scaleY, rotation, pivotX, pivotY}
}
protected getCorner(pivotX:number, pivotY:number, cornerX:number, cornerY:number, angle:number) {
let x, y, distance, diffX, diffY;
/// get distance from center to point
diffX = cornerX - pivotX;
diffY = cornerY - pivotY;
distance = Math.sqrt(diffX * diffX + diffY * diffY);
/// find angle from pivot to corner
angle += Math.atan2(diffY, diffX);
/// get new x and y and round it off to integer
x = pivotX + distance * Math.cos(angle);
y = pivotY + distance * Math.sin(angle);
return {x, y};
}
Let's suppose that the scenario is as follows:
where the lower left corner of the image (solid line) has coordinates (x_i, y_i) and the lower left corner of the container (dashed line) has coordinates (X_c, Y_c). Moreover, the image (of width w and height h) is rotated counter-clockwise by angle beta with respect to the laboratory frame, while the container itself is rotated (also counter-clockwise) by angle alpha.
Now, let's focus for example on the upper-right corner P. With respect to the laboratory frame (global canvas), its coordinates can be expressed as:
R(beta) . ( w, h ) + ( x_i, y_i )
where . denotes matrix multiplication, and R is a counter-clockwise rotation matrix
R(beta) = [ cos(beta) -sin(beta) ]
[ sin(beta) cos(beta) ]
Now, we need to transform this into a coordinate frame with respect to the container. Formally, this means that we need first to subtract the offset and then to rotate by -alpha (or alpha clock-wise). Thus with everything together:
R(-alpha).( R(beta) . (w, h) + (x_i, y_i) - (X_c, Y_c) )
The other corners can be handled similarly, just by replacing (w, h) with the proper coordinates...
In terms of code, one might implement these formulae as:
//counter-clock-wise rotation by given angle in degrees
function rotateCCWBy(angle, {x, y}) {
const angle_rad = angle * Math.PI / 180;
const cos_a = Math.cos(angle_rad),
sin_a = Math.sin(angle_rad);
return {
x: cos_a * x - sin_a * y,
y: sin_a * x + cos_a * y
};
}
//shift by a multiple fac of an offset {xref, yref}
function offsetBy(fac, {x:xref, y:yref}, {x, y}) {
return {
x: fac*xref + x,
y: fac*yref + y
};
}
const image = {
coords: {x: 200, y: 0}, //lab-frame coordinates
angle: 45, //lab-frame rotation angle
width: 50,
height: 10
};
const container = {
coords: {x: 100, y: 100}, //lab-frame coordinates
angle: 45 //lab-frame rotation angle
};
//calculate the coordinates of the image's top-right corner
//with respect to the container
const corner = rotateCCWBy(-container.angle,
offsetBy(
-1, container.coords,
offsetBy(
+1, image.coords,
rotateCCWBy(image.angle,
{x: image.width, y: image.height}
)
)
)
);
console.log(corner);
EDIT:
In case the y-axis is supposed to point "downwards", the formulas above work as well, one just needs to interpret the angles as clock-wise instead of counter-clockwise (so in principle the function rotateCCWBy should be renamed to rotateCWBy). As an example, let's consider this scenario:
Here, the top-left corner of the container is located at position (2,1) and the container itself is rotated by 15 degrees. The image (black rectangle) of width 4 and height 2 is rotated by 30 degrees and its top-left corner is located at position (3, 3). Now, we want to calculate the coordinates (x, y) of point P with respect to the container.
Using:
const image = {
coords: {x: 3, y: 3}, //lab-frame coordinates
angle: 30, //lab-frame rotation angle
width: 4,
height: 2
};
const container = {
coords: {x: 2, y: 1}, //lab-frame coordinates
angle: 15 //lab-frame rotation angle
};
//calculate the coordinates of the image's top-left corner
//with respect to the container
const corner = rotateCCWBy(-container.angle,
offsetBy(
-1, container.coords,
offsetBy(
+1, image.coords,
rotateCCWBy(image.angle,
{x: image.width, y: image.height}
)
)
)
);
console.log(corner);
yields
{ x: 4.8296291314453415, y: 4.640160440463835 }
which can be (approximately) visually verified from the attached figure.
EDIT2:
After additional clarification, the coordinates of the image are not supposed to be "lab-frame" (i.e., with respect to the canvas), but with respect to the already rotated container. Thus the transformation needs to be adapted as:
const corner =
offsetBy(
+1, container.coords,
rotateCCWBy(container.angle,
offsetBy(
+1, image.coords,
rotateCCWBy(image.angle,
{x: image.width, y: image.height}
)
)
)
);
function rotateCCWBy(angle, {x, y}) {
const angle_rad = angle * Math.PI / 180;
const cos_a = Math.cos(angle_rad),
sin_a = Math.sin(angle_rad);
return {
x: cos_a * x - sin_a * y,
y: sin_a * x + cos_a * y
};
}
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.
in the jsfiddle below im trying to get the yellow and red circles to initially appear in the center, as opposed to moving to the cursor's initial assumed position - which is (0,0) as the cursor is not yet in the actual screen of the page (if I'm guessing correctly).
http://jsfiddle.net/fhmkf/220/
Is there a way I can define the initial placement of the cursor - that is when calculating (window).mousemove(function(e){... - particularly the e.pageX and e.pageY?
The js looks like so:
var Circle = function(container, follower, r){
var center = {
x: $(container).width()/2 - r,
y: $(container).height()/2 - r
};
var distanceThreshold = $(container).width()/2 - r;
var mouseX = 0, mouseY = 0;
$(window).mousemove(function(e){
var d = {
x: e.pageX - center.x,
y: e.pageY - center.y
};
var distance = Math.sqrt(d.x*d.x + d.y*d.y);
if (distance < distanceThreshold) {
mouseX = e.pageX;
mouseY = e.pageY;
} else {
mouseX = d.x / distance * distanceThreshold + center.x;
mouseY = d.y / distance * distanceThreshold + center.y;
}
});
// cache the selector
var follower = $(follower);
var xp = 0, yp = 0;
var loop = setInterval(function(){
// change 12 to alter damping higher is slower
xp += (mouseX - xp) / 2;
yp += (mouseY - yp) / 2;
follower.css({left:xp, top:yp});
}, 30);
};
var c1 = new Circle(".container", "#follower", 15);
var c2 = new Circle(".container2", "#follower2", 25);