I am a newbie with HTML5 Canvas and JavaScript, but is there a simple way to have Isometric projection in HTML5 Canvas element?
开发者_如何学编程I am mean the true isometric projection - http://en.wikipedia.org/wiki/Isometric_projection
Thanks all for reply's.
Axonometric rendering
The best way to handle axonometric (commonly called isometric) rendering is via a projection matrix.
A projection object as follows can describe all you need to do any form of axonometric projection
The object has 3 transforms for the x,y and z axis with each describing the scale and direction in the 2D projection for the x,y,z coordinates. A transform for the depth calculation and a origin that is in canvas pixels (if setTransform(1,0,0,1,0,0) or whatever the current transform for the canvas is)
To project a point call the function axoProjMat({x=10,y=10,z=10})
and it will return a 3D point with x,y being 2D coordinates of the vertex and z being the depth (with depth values positive approaching the view (opposite to 3D perspective projection));
// 3d 2d points
const P3 = (x=0, y=0, z=0) => ({x,y,z});
const P2 = (x=0, y=0) => ({x, y});
// projection object
const axoProjMat = {
xAxis : P2(1 , 0.5) ,
yAxis : P2(-1 , 0.5) ,
zAxis : P2(0 , -1) ,
depth : P3(0.5,0.5,1) , // projections have z as depth
origin : P2(), // (0,0) default 2D point
setProjection(name){
if(projTypes[name]){
Object.keys(projTypes[name]).forEach(key => {
this[key]=projTypes[name][key];
})
if(!projTypes[name].depth){
this.depth = P3(
this.xAxis.y,
this.yAxis.y,
-this.zAxis.y
);
}
}
},
project (p, retP = P3()) {
retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x + this.origin.x;
retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y + this.origin.y;
retP.z = p.x * this.depth.x + p.y * this.depth.y + p.z * this.depth.z;
return retP;
}
}
With the above object you can use the function axoProjMat.setProjection(name)
to select the projection type.
Below is the associated projection types as outlined on the wiki Axonometric projections plus two modifications commonly used in pixel art and games (prefixed with Pixel). Use axoProjMat.setProjection(name)
where name is one of the projTypes
property names.
const D2R = (ang) => (ang-90) * (Math.PI/180 );
const Ang2Vec = (ang,len = 1) => P2(Math.cos(D2R(ang)) * len,Math.sin(D2R(ang)) * len);
const projTypes = {
PixelBimetric : {
xAxis : P2(1 , 0.5) ,
yAxis : P2(-1 , 0.5) ,
zAxis : P2(0 , -1) ,
depth : P3(0.5,0.5,1) , // projections have z as depth
},
PixelTrimetric : {
xAxis : P2(1 , 0.5) ,
yAxis : P2(-0.5 , 1) ,
zAxis : P2(0 , -1) ,
depth : P3(0.5,1,1) ,
},
Isometric : {
xAxis : Ang2Vec(120) ,
yAxis : Ang2Vec(-120) ,
zAxis : Ang2Vec(0) ,
},
Bimetric : {
xAxis : Ang2Vec(116.57) ,
yAxis : Ang2Vec(-116.57) ,
zAxis : Ang2Vec(0) ,
},
Trimetric : {
xAxis : Ang2Vec(126.87,2/3) ,
yAxis : Ang2Vec(-104.04) ,
zAxis : Ang2Vec(0) ,
},
Military : {
xAxis : Ang2Vec(135) ,
yAxis : Ang2Vec(-135) ,
zAxis : Ang2Vec(0) ,
},
Cavalier : {
xAxis : Ang2Vec(135) ,
yAxis : Ang2Vec(-90) ,
zAxis : Ang2Vec(0) ,
},
TopDown : {
xAxis : Ang2Vec(180) ,
yAxis : Ang2Vec(-90) ,
zAxis : Ang2Vec(0) ,
}
}
Example of True Isometric Projection.
The snippet is an simple example with the projection set to Isometric
as detailed on the wiki link in the OP's question and using the above functions and objects.
const ctx = canvas.getContext("2d");
// function creates a 3D point (vertex)
function vertex(x, y, z) { return { x, y, z}};
// an array of vertices
const vertices = []; // an array of vertices
// create the 8 vertices that make up a box
const boxSize = 20; // size of the box
const hs = boxSize / 2; // half size shorthand for easier typing
vertices.push(vertex(-hs, -hs, -hs)); // lower top left index 0
vertices.push(vertex(hs, -hs, -hs)); // lower top right
vertices.push(vertex(hs, hs, -hs)); // lower bottom right
vertices.push(vertex(-hs, hs, -hs)); // lower bottom left
vertices.push(vertex(-hs, -hs, hs)); // upper top left index 4
vertices.push(vertex(hs, -hs, hs)); // upper top right
vertices.push(vertex(hs, hs, hs)); // upper bottom right
vertices.push(vertex(-hs, hs, hs)); // upper bottom left index 7
const colours = {
dark: "#040",
shade: "#360",
light: "#ad0",
bright: "#ee0",
}
function createPoly(indexes, colour) {
return {
indexes,
colour
}
}
const polygons = [];
polygons.push(createPoly([1, 2, 6, 5], colours.shade)); // right face
polygons.push(createPoly([2, 3, 7, 6], colours.light)); // front face
polygons.push(createPoly([4, 5, 6, 7], colours.bright)); // top face
// From here in I use P2,P3 to create 2D and 3D points
const P3 = (x = 0, y = 0, z = 0) => ({x,y,z});
const P2 = (x = 0, y = 0) => ({ x, y});
const D2R = (ang) => (ang-90) * (Math.PI/180 );
const Ang2Vec = (ang,len = 1) => P2(Math.cos(D2R(ang)) * len,Math.sin(D2R(ang)) * len);
const projTypes = {
PixelBimetric : {
xAxis : P2(1 , 0.5) ,
yAxis : P2(-1 , 0.5) ,
zAxis : P2(0 , -1) ,
depth : P3(0.5,0.5,1) , // projections have z as depth
},
PixelTrimetric : {
xAxis : P2(1 , 0.5) ,
yAxis : P2(-0.5 , 1) ,
zAxis : P2(0 , -1) ,
depth : P3(0.5,1,1) ,
},
Isometric : {
xAxis : Ang2Vec(120) ,
yAxis : Ang2Vec(-120) ,
zAxis : Ang2Vec(0) ,
},
Bimetric : {
xAxis : Ang2Vec(116.57) ,
yAxis : Ang2Vec(-116.57) ,
zAxis : Ang2Vec(0) ,
},
Trimetric : {
xAxis : Ang2Vec(126.87,2/3) ,
yAxis : Ang2Vec(-104.04) ,
zAxis : Ang2Vec(0) ,
},
Military : {
xAxis : Ang2Vec(135) ,
yAxis : Ang2Vec(-135) ,
zAxis : Ang2Vec(0) ,
},
Cavalier : {
xAxis : Ang2Vec(135) ,
yAxis : Ang2Vec(-90) ,
zAxis : Ang2Vec(0) ,
},
TopDown : {
xAxis : Ang2Vec(180) ,
yAxis : Ang2Vec(-90) ,
zAxis : Ang2Vec(0) ,
}
}
const axoProjMat = {
xAxis : P2(1 , 0.5) ,
yAxis : P2(-1 , 0.5) ,
zAxis : P2(0 , -1) ,
depth : P3(0.5,0.5,1) , // projections have z as depth
origin : P2(150,65), // (0,0) default 2D point
setProjection(name){
if(projTypes[name]){
Object.keys(projTypes[name]).forEach(key => {
this[key]=projTypes[name][key];
})
if(!projTypes[name].depth){
this.depth = P3(
this.xAxis.y,
this.yAxis.y,
-this.zAxis.y
);
}
}
},
project (p, retP = P3()) {
retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x + this.origin.x;
retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y + this.origin.y;
retP.z = p.x * this.depth.x + p.y * this.depth.y + p.z * this.depth.z;
return retP;
}
}
axoProjMat.setProjection("Isometric");
var x,y,z;
for(z = 0; z < 4; z++){
const hz = z/2;
for(y = hz; y < 4-hz; y++){
for(x = hz; x < 4-hz; x++){
// move the box
const translated = vertices.map(vert => {
return P3(
vert.x + x * boxSize,
vert.y + y * boxSize,
vert.z + z * boxSize,
);
});
// create a new array of 2D projected verts
const projVerts = translated.map(vert => axoProjMat.project(vert));
// and render
polygons.forEach(poly => {
ctx.fillStyle = poly.colour;
ctx.strokeStyle = poly.colour;
ctx.lineWidth = 1;
ctx.beginPath();
poly.indexes.forEach(index => ctx.lineTo(projVerts[index].x , projVerts[index].y));
ctx.stroke();
ctx.fill();
});
}
}
}
canvas {
border: 2px solid black;
}
body { font-family: arial; }
True Isometric projection. With x at 120deg, and y at -120deg from up.<br>
<canvas id="canvas"></canvas>
First, I would recommend thinking of the game world as a regular X by Y grid of square tiles. This makes everything from collision detection, pathfinding, and even rendering much easier.
To render the map in an isometric projection simply modify the projection matrix:
var ctx = canvas.getContext('2d');
function render(ctx) {
var dx = 0, dy = 0;
ctx.save();
// change projection to isometric view
ctx.translate(view.x, view.y);
ctx.scale(1, 0.5);
ctx.rotate(45 * Math.PI /180);
for (var y = 0; i < 10; y++) {
for (var x = 0; x < 10; x++) {
ctx.strokeRect(dx, dy, 40, 40);
dx += 40;
}
dx = 0;
dy += 40;
}
ctx.restore(); // back to orthogonal projection
// Now, figure out which tile is under the mouse cursor... :)
}
This is exciting the first time you get it work, but you'll quickly realize that it's not that useful for drawing actual isometric maps... you can't just rotate your tile images and see what's around the corner. The transformations are not so much for drawing, as they are for converting between screen space and world space.
Bonus: figuring out which tile the mouse is over
What you want to do is convert from "view coordinates" (pixel offsets from the canvas origin) to "world coordinates" (pixel offsets from tile 0,0 along the diagonal axes). Then simply divide the world coordinates by the tile width and height to get the "map coordinates".
In theory, all you need to do is project the "view position" vector by the inverse of the projection matrix above to get the "world position". I say in theory, because for some reason the canvas doesn't provide a way of returning the current projection matrix. There is a setTransform()
method, but no getTransform()
, so this is where you'd have to roll your own 3x3 transformation matrix.
It's not actually that hard, and you will need this for converting between world and view coordinates when drawing objects.
Hope this helps.
I create something this for my isometric app
class IsoProjection {
constructor() {
this.matP = [1, 0, 0, 1, 0, 0];
this.matI = [1, 0, 0, 1, 0, 0];
this.mapRatio = 1;
this.mapRatioI = 1;
}
isoToTilePos(a, ao) {
let m = this.matI,
b = ao || [],
i = 0,
j = 1;
do {
j = i + 1;
b[i] = a[i] * m[0] + a[j] * m[2] + m[4];
b[j] = a[i] * m[1] + a[j] * m[3] + m[5];
i += 2;
} while (i < a.length);
return b;
}
tileToIsoPos(a, ao) {
let m = this.matP,
b = ao || [],
i = 0,
j = 1;
do {
j = i + 1;
b[i] = a[i] * m[0] + a[j] * m[2] + m[4];
b[j] = a[i] * m[1] + a[j] * m[3] + m[5];
i += 2;
} while (i < a.length);
return b;
}
reset(numC, numR, cellW, cellH) {
/*
Math.sqrt(2 * isoW * isoW) = cellW
isoW = Math.sqrt(cellW * cellW / 2);
while map's tileW = 1
*/
let isoW = Math.sqrt(cellW * cellW / 2);
this.mapRatio = isoW;
this.mapRatioI = 1 / isoW;
// translation
let ctr = Math.max(numC, numR) / 2;
//rotation
let rot = -Math.PI / 4;
let cos = Math.cos(rot);
let sin = Math.sin(rot);
// scale
let sx = isoW;
let sy = cellH / cellW * isoW;
// the matrix
this.matP[0] = sx * cos;
this.matP[1] = sy * sin;
this.matP[2] = sx * -sin;
this.matP[3] = sy * cos;
this.matP[4] = 0;
this.matP[5] = 0;
// the inverted matrix;
let a = this.matP[0],
b = this.matP[1],
c = this.matP[2],
d = this.matP[3],
e = this.matP[4],
f = this.matP[5];
let det = a * d - b * c;
if (det !== 0) {
det = 1 / det;
this.matI[0] = d * det;
this.matI[1] = - b * det;
this.matI[2] = - c * det;
this.matI[3] = a * det;
this.matI[4] = (c * f - e * d) * det;
this.matI[5] = (e * b - a * f) * det;
} else {
this.matI[0] = a;
this.matI[1] = b;
this.matI[2] = c;
this.matI[3] = d;
this.matI[4] = e;
this.matI[5] = f;
}
return this;
}
}
精彩评论