Isometric tilemap using canvas (with click detection) - javascript

I am currently developing a game, which requires a map consisting of various tile images. I managed to make them display correctly (see second image) but I am now unsure of how to calculate the clicked tile from the mouse position.
Are there any existing libraries for this purpose?
Please also note, that the tile images aren't drawn perfectly "corner-facing-camera", they are slightly rotated clockwise.

Isometric Transformations
Define a projection
Isometric display is the same as standard display, the only thing that has changed is the direction of the x and y axis. Normally the x axis is defined as (1,0) one unit across and zero down and the y axis is (0,1) zero units across and one down. For isometric (strictly speaking your image is a dimetric projection) you will have something like x axis (0.5,1) and y axis (-1,0.5)
The Matrix
From this you can create a rendering matrix with 6 values Two each for both axes and two for the origin, which I will ignore for now (the origin) and just use the 4 for the axis and assume that the origin is always at 0,0
var dimetricMatrix = [0.5,1.0,-1,0.5]; // x and y axis
Matrix transformation
From that you can get a point on the display that matches a given isometric coordinate. Lets say the blocks are 200 by 200 pixels and that you address each block by the block x and y. Thus the block in the bottom of your image is at x = 2 and y = 1 (the first top block is x = 0, y = 0)
Using the matrix we can get the pixel location of the block
var blockW = 200;
var blockH = 200;
var locX = 2;
var locY = 1;
function getLoc(x,y){
var xx,yy; // intermediate results
var m = dimetricMatrix; // short cut to make code readable
x *= blockW; // scale up
y *= blockH;
// now move along the projection x axis
xx = x * m[0];
yy = x * m[1];
// then add the distance along the y axis
xx += y * m[2];
yy += y * m[3];
return {x : xx, y : yy};
}
Befoer I move on you can see that I have scaled the x and y by the block size. We can simplify the above code and include the scale 200,200 in the matrix
var xAxis = [0.5, 1.0];
var yAxis = [-1, 0.5];
var blockW = 200;
var blockH = 200;
// now create the matrix and scale the x and y axis
var dimetricMatrix = [
xAxis[0] * blockW,
xAxis[1] * blockW,
yAxis[0] * blockH,
yAxis[1] * blockH,
]; // x and y axis
The matrix holds the scale in the x and y axis so that the two numbers for x axis tell us the direction and length of a transformed unit.
Simplify function
And redo the getLoc function for speed and efficiency
function transformPoint(point,matrix,result){
if(result === undefined){
result = {};
}
// now move along the projection x axis
result.x = point.x * matrix[0] + point.y * matrix[2];
result.y = point.x * matrix[1] + point.y * matrix[3];
return result;
}
So pass a point and get a transformed point back. The result argument allows you to pass an existing point and that saves having to allocate a new point if you are doing it often.
var point = {x : 2, y : 1};
var screen = transformPoint(point,dimetricMatrix);
// result is the screen location of the block
// next time
screen = transformPoint(point,dimetricMatrix,screen); // pass the screen obj
// to avoid those too
// GC hits that kill
// game frame rates
Inverting the Matrix
All that is handy but you need the reverse of what we just did. Luckily the way matrices work allows us to reverse the process by inverting the matrix.
function invertMatrix(matrix){
var m = matrix; // shortcut to make code readable
var rm = [0,0,0,0]; // resulting matrix
// get the cross product of the x and y axis. It is the area of the rectangle made by the
// two axis
var cross = m[0] * m[3] - m[1] * m[2]; // I call it the cross but most will call
// it the determinate (I think that cross
// product is more suited to geometry while
// determinate is for maths geeks)
rm[0] = m[3] / cross; // invert both axis and unscale (if cross is 1 then nothing)
rm[1] = -m[1] / cross;
rm[2] = -m[2] / cross;
rm[3] = m[0] / cross;
return rm;
}
Now we can invert our matrix
var dimetricMatrixInv = invertMatrix(dimetricMatrix); // get the invers
And now that we have the inverse matrix we can use the transform function to convert from a screen location to a block location
var screen = {x : 100, y : 200};
var blockLoc = transformPoint(screen, dimetricMatrixInv );
// result is the location of the block
The Matrix for rendering
For a bit of magic the transformation matrix dimetricMatrix can also be used by the 2D canvas, but you need to add the origin.
var m = dimetricMatrix;
ctx.setTransform(m[0], m[1], m[2], m[3], 0, 0); // assume origin at 0,0
Now you can draw a box around the block with
ctx.strokeRect(2,1,1,1); // 3rd by 2nd block 1 by 1 block wide.
The origin
I have left out the origin in all the above, I will leave that up to you to find as there is a trillion pages online about matrices as all 2D and 3D rendering use them and getting a good deep knowledge of them is important if you wish to get into computer visualization.

Related

How to get the intersection points of an Arc and a line?

I need to place several number of line segments inside an arc, but to do that i need to have intersection points so that i can place the lines inside the arc perfectly;
I thought about a way to calculate the distance and check if it is less than radius, but the thing is i need to know the C,D & E points so that i can place the line segments, so i'm lost here, any one can help please?
EDIT
The radius is specified
Number of line segments may vary, but there are 2 lines at least
Lines start at starting border, end at the ending border; e.g: Start is C, end point is D
EDIT
In order to be clear about what i'm trying to do, i'm uploading another illustration;
I need to get the coordinates of [CD],[EI],[JK] lines,
Ok... Here we go. The following snippet should work for any arc (defined with an angle and a radius) and for any number of equally spaced segments you want.
Currently, it assumes that the arc is perfectly placed horizontally (like in your example), but it can be "easily" extended to allow translated/rotated arcs.
The getLinesCoords() function will return an object whose x and y properties contains arrays with the corresponding coordinates for each segment.
"y" coordinates are the "height" of the segment from the center (G in your image) and "x" are the start/end position, always from center (left/right depends on sign +/-).
If you have any question, please ask.
// *** ARC ***
const R = 100; // RADIUS
const PHI = 180; // ANGLE (DEG)
// *** LINES ***
const LINES = 10; // NUMBER OF LINES TO BE PLACED
// *** CALCULATIONS ***
const coords = getLinesCoords(R, PHI, LINES);
console.log(coords);
function getLinesCoords(radius, angle, linesNum) {
let i = 0;
let arcAvailHeight = 0;
let linesSep = 0;
let linesYCoords = [];
let linesXCoords = [];
let angleRad = angle * Math.PI / 180;
// GET AVAILABLE HEIGHT FOR PLACING LINES
arcAvailHeight = radius * (1 - Math.cos(angleRad / 2));
// GET LINES SEPARATION
linesSep = arcAvailHeight / (linesNum + 1);
// GET Y COORDINATES FOR LINES
for (i = 0; i < linesNum; i++) {
linesYCoords[i] = linesSep * (i + 1);
}
// GET CORRESPONDING X COORDINATES FOR LINES
linesYCoords.forEach((y) => {
linesXCoords.push(Math.sqrt(radius**2 - (radius * Math.cos(angleRad / 2) + y)**2));
});
return ({x: linesXCoords, y: linesYCoords});
}

Trigonometry Issue causing distortion when drawing floor textures in raycaster

I'm creating a game with raycasted 3D graphics like Wolfenstein 3D but using line segments instead of a grid of blocks for walls. Everything is fine when drawing the floors until rotating the player view.
the floor should be aligned against the walls
Here is the view in 2D, with each pixel on the floor on the screen rendered as a blue point:
In the top image is when the player's rotation is Math.PI. In the bottom image it is rotated slightly.
A significant feature of this is the beginning of the cone of points is aligned along the y axis. It should look like a frustrum.
Here is the code I am using to find the x and y coordinates of each point where a texture is drawn on the floor. This code is run for each x value on the screen.
The variable "projPlane" is the projection plane, which is the size of the screen.
projDistance is the distance from the player to the projection plane so that it fits within the field of view, or (projPlane.width/2)/Math.tan(VectorMath.toRadians(fov/2))
pHeight is the players height.
The variable "x" is the x value of the row being rendered on the screen.
//FLOOR TEXTURE
var floorSize = Math.floor((projPlane.height-wallSize)/2); //draw the floor from under the wall
var floorTextureIndex = 1;
//for texture y
if(floorSize > 0){ // values need to be positive
//find the point on the floor
var textureWidth = textures[floorTextureIndex].textureImage.width;
var textureHeight = textures[floorTextureIndex].textureImage.height;
//console.log(coordX);
for (var ty = 0; ty < floorSize; ty++){
//angle is tan
var yAngle = projPlane.distance / (ty + wallSize/2); //ty + wallSize/2 is the point on the projection plane
var yDistance = yAngle * pHeight; //pHeight is player height
var worldY = player.y + Math.sin(player.vector)*yDistance;
var coordY = Math.floor(worldY % (textureHeight));
var xAngle = Math.atan((projPlane.width/2 - x)/projPlane.distance);
/*if(x < projPlane.width/2){//tangent of the angle in the projectionPlane
xAngle = (x) / projPlane.distance;
}
else{
xAngle = (x-projPlane.width) / projPlane.distance;
}*/
var xDistance = yDistance/Math.cos(xAngle);
var worldX = player.x + Math.cos(player.vector - xAngle)*xDistance;
//console.log(xDistance);
var coordX = Math.floor(worldX % (textureWidth));//disable until I can get y
floorPoints.push(new Point(worldX,worldY));
var tempTexture = textures[floorTextureIndex];
if(tempTexture.textureData[coordX] != undefined){
// a different function drawns these to the screen in descending order
floorTextureColors.push(tempTexture.textureData[coordX][coordY]);
}
};
}
It doesn't seem to be an issue with the y value since the y coordinates of the floor texture seem to appear where they should.(EDIT: it actually was to do with the y value. Adding the xAngle to the player.vector when finding the y position returns a correct y position but there is still a "curved" distortion. I hope one of you can propose a more concrete solution.)
What I do to find the X coordinate is form a triangle with the distance from the player to the floor point as the opposite side the angle that the point makes with the player. The hypotenuse should be the magnitude of the distance to the point's x coordinate.
Then I multiply the cosine of the angle by the magnitude to get the x value.
It works whenever the character isn't pointing west and east. What is causing all the first points to have the same y value? I think that's the biggest clue on the distortion occurring here.

Canvas Graph plotting data incorrectly

I have made a simple graph in a canvas but am having difficulty with two issues.
The first issue is setting the vertical axis with an appropriate scale automatically with enough room for each data value in an array. Ideally i'd like the numbers to be more rounded to the nearest million or thousand etc depending on it's actual value ranges rather than a value like 33145 as the first scale line.
Currently one value is too high for the scale and is not being drawn on the canvas because it is out of bounds.
The second issue, is the points don't seem to be plotting in their correct location, which I am unsure where my mistake was.
I made a JSFiddle as for the most part it might be a bit confusing without seeing it in action:
http://jsfiddle.net/ezttywzr/
This is how i plot my data and draw my vertical axis:
Vertical Axis:
var x = 0,
y,
range = data.max() - data.min(),
valueStep = range / 10,
// get width of largest number
margin = 3 + ctx.measureText(data.min() + (valueStep*10)).width,
pixelStep = (graph.height-40) / 10,
verticalP = pixelStep,
output;
// draw left hand values
for(var i = 0; i < 11; i++){
output = data.min() + (valueStep*i);
y = graph.height-20 - (verticalP + i*pixelStep);
ctx.fillText(output,x,y+6);
ctx.beginPath();
ctx.moveTo(margin, y);
ctx.lineTo(x2,y);
ctx.stroke();
}
Data Plotting:
var y = graph.height,
x = margin,
pos,
valueStep = (graph.width-(margin*2)) / data.length,
pixelRange = graph.height-20,
pp = range / pixelRange;
for(var i = 0; i < data.length; i++){
x += valueStep;
pos = x - (valueStep/2);
ctx.beginPath();
ctx.moveTo(x, graph.height-20);
ctx.lineTo(x, graph.height);
ctx.stroke();
ctx.fillText('Week '+(i+1),pos-(ctx.measureText('Week '+(i+1)).width/2),y);
ctx.beginPath();
ctx.arc(pos,(graph.height-20)-(verticalP+(data[i]/pp)),2,0,2*Math.PI);
ctx.stroke();
ctx.fill();
}
Nice job so far.
I made a few changes: http://jsfiddle.net/ezttywzr/2/
To get the scale I used
STEP = data.max() / NUM_HORIZONTAL_LINES
Where NUM_HORIZONTAL_LINES is the number of horizontal lines you want above the x-axis. In this case I used 10.
This means the first line will be 1 * STEP, the second will be 2 * STEP, the third will be 3 * STEP and so on..
This scale is convenient because it guarantees that the max value fits on the graph. In fact, the max value is on the top line because of the way we defined the scale.
Once we have our scale it's easy to calculate the position of the points relative to the x-axis. It's simply:
(PIXELS_PER_STEP / STEP) * VALUE
To go a step further you can do some math to round the top point of the graph up and pick a scale with that has nice round numbers.

Convert co-ordinates to isometric and vice versa not working

I have two functions which recieve co-ordinates that are not returning the correct output.
One receives the position of the mouse relative to the element and returns a grid co ordinate for isometric tiles.
The other function essentially reverses this process from iso tile back to pixel position on the screen.
When i send in a co-ordinate for my mouse position and convert it to isometric, then convert it back to pixel position i get a different result from what i started with by a large margin rather than the rounding of the tile size - suggesting I got the maths wrong some where, but am not sure where.
My two functions are:
function isoToScreen(isoX,isoY){ //recieves simple grid co-ordinate (int,int)
var x = (isoX - isoY) * (grid.getWidth()/2),
y = (isoX + isoY) * (grid.getHeight()/2);
//need to remove the camera offset to get the relative position
x = camera.removeOffsetX(x);
y = camera.removeOffsetY(y);
return {'x':x,'y':y};
}
function screenToIso(x,y){ //receives mouse position relative to canvas
//add camera offset to get the correct isometric grid
x = camera.addOffsetX(x);
y = camera.addOffsetY(y);
var isoX = x / (grid.getWidth()/2) + y / (grid.getHeight()/2),
isoY = y / (grid.getHeight()/2) - x / (grid.getWidth()/2);
return {'x':Math.floor(isoX),'y':Math.floor(isoY)}
}
Just some extra info, grid height == 46 and grid width == 92.
Can any one see where i am going wrong in the my maths logic?
In screenToIso you are multiplying the vector [x;y] by the matrix:
[ 2 / grid.getWidth(), 2 / grid.getHeight()]
[ -2 / grid.getWidth(), 2 / grid.getHeight()]
Its inverse is:
[grid.getWidth() / 4, -grid.getWidth() / 4]
[grid.getHeight() / 4, grid.getHeight() / 4]
Hence the first two lines of isoToScreen should be:
var x = (grid.getWidth() / 4) * isoX - (grid.getWidth() / 4) * isoY;
var y = (grid.getHeight() / 4) * isox + (grid.getHeight() / 4) * isoY;

Convert 3D world space coordinates to SVG viewport coordinates

I've been attempting to display the positions of players in a 3D game, on a web page, from an overhead view perspective (like a minimap). I'm simply superimposing markers (via SVG) on a 1024x1024 image of the game's level (an overhead view, taken from the game itself). I'm only concerned with the x, y coordinates, since I'm not using z in an overhead view.
The game's world has equal min/max coordinates for both x and y: -4096 to 4096. The 0, 0 coordinate is the center of the world.
To make things even more interesting, the initial position of a game level, within the game world, is arbitrary. So, for example, the upper-left world coordinate for the particular level I've been testing with is -2440, 3383.
My initial confusion comes from the fact that the 0,0 coordinate in a web page is the top-left, versus center in the world space.
How do I correctly convert the 3D world space coordinates to properly display in a web page viewport of any dimension?
Here's what I've tried (I've been attempting to use the viewbox attribute in svg to handle the upper left world coordinate offset)
scalePosition: function (targetWidth, targetHeight) {
// in-game positions
var gamePosition = this.get('pos');
var MAX_X = 8192,
MAX_Y = 8192;
// Flip y
gamePosition.y = -gamePosition.y;
// Ensure game coordinates are only positive values
// In game = -4096 < x|y < 4096 (0,0 = center)
// Browser = 0 < x|y < 8192 (0,0 = top-left)
gamePosition.x += 4096;
gamePosition.y += 4096;
// Target dimenions
targetWidth = (targetWidth || 1024),
targetHeight = (targetHeight || 1024);
// Find scale between game dimensions and target dimensions
var xScale = MAX_X / targetWidth,
yScale = MAX_Y / targetHeight;
// Convert in-game coords to target coords
var targetX = gamePosition.x / xScale,
targetY = gamePosition.y / yScale;
return {
x: targetX,
y: targetY
};
},

Categories

Resources