var canvas = document.createElement("canvas");
canvas.width = 500;
canvas.height = 300;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
// block types
const types = {
quad : 1,
split1 : 2, // split from top left to bottom right
split2 : 3, // split from top right to bottom left
}
/*
// A block object example to define meaning of properties
var blockObject = {
x : 0, // top left base x pos
y : 0, // top left base y pos
z : 0, // top left base z pos
norm1, // normal of quad or top right or bottom right triangles
norm2, // normal of quad or top left or bottom left triangles
p1 : 0, // top left
p2 : 0, // top right
p3 : 0, // bottom right
p4 : 0, // bottom left
type : types.quad,
pNorm : null, // this is set when a height test is done. It is the normal at the point of the height test
}*/
// compute the surface normal from two vectors on the surface. (cross product of v1,v2)
function getSurfaceNorm(x1,y1,z1,x2,y2,z2){
// normalise vectors
var d1= Math.hypot(x1,y1,z1);
x1 /= d1;
y1 /= d1;
z1 /= d1;
var d2= Math.hypot(x2,y2,z2);
x2 /= d2;
y2 /= d2;
z2 /= d2;
var norm = {}
norm.x = y1 * z2 - z1 * y2;
norm.y = z1 * x2 - x1 * z2;
norm.z = x1 * y2 - y1 * x2;
return norm;
}
// This defines a block with p1-p2 the height of the corners
// starting top left and clockwise around to p4 bottom left
// If the block is split with 2 slopes then it will be
// of type.split1 or type.split2. If a single slope then it is a type.quad
// Also calculates the normals
function createBlock(x,y,z,h1,h2,h3,h4,type){
var norm1,norm2;
if(type === types.quad){
norm1 = norm2 = getSurfaceNorm(1, 0, h2 - h1, 0, 1, h4 - h1);
}else if(type === types.split1){
norm1 = getSurfaceNorm(1, 0, h2 - h1, 1, 1, h3 - h1);
norm2 = getSurfaceNorm(0, 1, h2 - h1, 1, 1, h3 - h1);
}else{
norm1 = getSurfaceNorm(0, 1, h2-h3, 1, 0, h4 - h3);
norm2 = getSurfaceNorm(1, 0, h2 - h1, 0, 1, h4 - h1);
}
return {
p1 : h1, // top left
p2 : h2, // top right
p3 : h3, // bottom right
p4 : h4, // bottom left
x,y,z,type,
norm1,norm2,
}
}
// get the height on the block at x,y
// also sets the surface block.pNorm to match the correct normal
function getHeight(block,x,y){
var b = block; // alias to make codes easier to read.
if(b.type === types.quad){
b.pNorm = b.norm1;
if(b.p1 === b.p2){
return (b.p3 - b.p1) * (y % 1) + b.p1 + b.z;
}
return (b.p2 - b.p1) * (x % 1) + b.p1 + b.z;
}else if(b.type === types.split1){
if(x % 1 > y % 1){ // on top right side
b.pNorm = b.norm1;
if(b.p1 === b.p2){
if(b.p1 === b.p3){
return b.p1 + b.z;
}
return (b.p3 - b.p1) * (y % 1) + b.p1 + b.z;
}
if(b.p2 === b.p3){
return (b.p2 - b.p1) * (x % 1) + b.p1 + b.z;
}
return (b.p3 - b.p2) * (y % 1) + (b.p2 - b.p1) * (x % 1) + b.p1 + b.z;
}
// on bottom left size
b.pNorm = b.norm2;
if(b.p3 === b.p4){
if(b.p1 === b.p3){
return b.p1 + b.z;
}
return (b.p3 - b.p1) * (y % 1) + b.p1 + b.z;
}
if(b.p1 === b.p4){
return (b.p3 - b.p1) * (x % 1) + b.p1 + b.z;
}
var h = (b.p4 - b.p1) * (y % 1);
var h1 = b.p3 - (b.p4 - b.p1) + h;
return (h1 - (h + b.p1)) * (x % 1) + (h + b.p1) + b.z;
}
if(1 - (x % 1) < y % 1){ // on bottom right side
b.pNorm = b.norm1;
if(b.p3 === b.p4){
if(b.p3 === b.p2){
return b.p2 + b.z;
}
return (b.p3 - b.p2) * (y % 1) + b.p4 + b.z;
}
if(b.p2 === b.p3){
return (b.p4 - b.p2) * (x % 1) + b.p2 + b.z;
}
var h = (b.p3 - b.p2) * (y % 1);
var h1 = b.p4 - (b.p3 - b.p2) + h;
return (h + b.p2 - h1) * (x % 1) + h1 + b.z;
}
// on top left size
b.pNorm = b.norm2;
if(b.p1 === b.p2){
if(b.p1 === b.p4){
return b.p1 + b.z;
}
return (b.p4 - b.p1) * (y % 1) + b.p1 + b.z;
}
if(b.p1 === b.p4){
return (b.p2 - b.p1) * (x % 1) + b.p1 + b.z;
}
var h = (b.p4 - b.p1) * (y % 1);
var h1 = b.p2 + h;
return (h1 - (h + b.p1)) * (x % 1) + (h + b.p1) + b.z;
}
const projection = {
width : 20,
depth : 20, // y axis
height : 8, // z axis
xSlope : 0.5,
ySlope : 0.5,
originX : canvas.width / 2,
originY : canvas.height / 4,
toScreen(x,y,z,point = [],pos = 0){
point[pos] = x * this.width - y * this.depth + this.originX;
point[pos + 1] = x * this.width * this.xSlope + y * this.depth * this.ySlope -z * this.height + this.originY;
return point;
}
}
// working arrays to avoid excessive GC hits
var pointArray = [0,0]
var workArray = [0,0,0,0,0,0,0,0,0,0,0,0,0,0];
function drawBlock(block,col,lWidth,edge){
var b = block;
ctx.strokeStyle = col;
ctx.lineWidth = lWidth;
ctx.beginPath();
projection.toScreen(b.x, b.y, b.z + b.p1, workArray, 0);
projection.toScreen(b.x + 1, b.y, b.z + b.p2, workArray, 2);
projection.toScreen(b.x + 1, b.y + 1, b.z + b.p3, workArray, 4);
projection.toScreen(b.x, b.y + 1, b.z + b.p4, workArray, 6);
if(b.type === types.quad){
ctx.moveTo(workArray[0],workArray[1]);
ctx.lineTo(workArray[2],workArray[3]);
ctx.lineTo(workArray[4],workArray[5]);
ctx.lineTo(workArray[6],workArray[7]);
ctx.closePath();
}else if(b.type === types.split1){
ctx.moveTo(workArray[0],workArray[1]);
ctx.lineTo(workArray[2],workArray[3]);
ctx.lineTo(workArray[4],workArray[5]);
ctx.closePath();
ctx.moveTo(workArray[0],workArray[1]);
ctx.lineTo(workArray[4],workArray[5]);
ctx.lineTo(workArray[6],workArray[7]);
ctx.closePath();
}else if(b.type === types.split2){
ctx.moveTo(workArray[0],workArray[1]);
ctx.lineTo(workArray[2],workArray[3]);
ctx.lineTo(workArray[6],workArray[7]);
ctx.closePath();
ctx.moveTo(workArray[2],workArray[3]);
ctx.lineTo(workArray[4],workArray[5]);
ctx.lineTo(workArray[6],workArray[7]);
ctx.closePath();
}
if(edge){
projection.toScreen(b.x + 1, b.y, b.z, workArray, 8);
projection.toScreen(b.x + 1, b.y + 1, b.z, workArray, 10);
projection.toScreen(b.x, b.y + 1, b.z, workArray, 12);
if(edge === 1){ // right edge
ctx.moveTo(workArray[2],workArray[3]);
ctx.lineTo(workArray[8],workArray[9]);
ctx.lineTo(workArray[10],workArray[11]);
ctx.lineTo(workArray[4],workArray[5]);
}
if(edge === 2){ // right edge
ctx.moveTo(workArray[4],workArray[5]);
ctx.lineTo(workArray[10],workArray[11]);
ctx.lineTo(workArray[12],workArray[13]);
ctx.lineTo(workArray[6],workArray[7]);
}
if(edge === 3){ // right edge
ctx.moveTo(workArray[2],workArray[3]);
ctx.lineTo(workArray[8],workArray[9]);
ctx.lineTo(workArray[10],workArray[11]);
ctx.lineTo(workArray[12],workArray[13]);
ctx.lineTo(workArray[6],workArray[7]);
ctx.moveTo(workArray[10],workArray[11]);
ctx.lineTo(workArray[4],workArray[5]);
}
}
ctx.stroke();
}
function createMap(){
var base = "0".charCodeAt(0);
for(var y = 0; y < mapSize.depth; y ++){
for(var x = 0; x < mapSize.width; x ++){
var index = y * (mapSize.width + 1) + x;
var b;
var p1= map.charCodeAt(index)-base;
var p2= map.charCodeAt(index+1)-base;
var p3= map.charCodeAt(index+1+mapSize.width + 1)-base;
var p4= map.charCodeAt(index+mapSize.width + 1)-base;
var type;
if((p1 === p2 && p3 === p4) || (p1 === p4 && p2 === p3)){
type = types.quad;
}else if(p1 === p3){
type = types.split1;
}else if(p4 === p2){
type = types.split2;
}else{
// throw new RangeError("Map has badly formed block")
type = types.split2;
}
blocks.push(
b = createBlock(
x,y,0,p1,p2,p3,p4,type
)
);
}
}
}
function drawMap(){
for(var i = 0; i < blocks.length; i ++){
var edge = 0;
if(i % mapSize.width === mapSize.width- 1){
edge = 1;
}
if(Math.floor(i / mapSize.width) === mapSize.width- 1){
edge |= 2;
}
drawBlock(blocks[i],"black",1,edge);
}
}
function drawBallShadow(ball){
var i;
var x,y,ix,iy;
ctx.globalAlpha = 0.5;
ctx.fillStyle = "black";
ctx.beginPath();
var first = 0;
for(var i = 0; i < 1; i += 1/8){
var ang = i * Math.PI * 2;
x = ball.x + (ball.rad / projection.width ) * Math.cos(ang) * 0.7;
y = ball.y + (ball.rad / projection.depth ) * Math.sin(ang) * 0.7;
if(x < mapSize.width && x >= 0 && y < mapSize.depth && y > 0){
ix = Math.floor(x + mapSize.width) % mapSize.width;
iy = Math.floor(y + mapSize.depth) % mapSize.depth;
var block = blocks[ix + iy * mapSize.width];
var z = getHeight(block,x,y);
projection.toScreen(x,y,z, pointArray);
if(first === 0){
first = 1;
ctx.moveTo(pointArray[0],pointArray[1]);
}else{
ctx.lineTo(pointArray[0],pointArray[1]);
}
}
}
ctx.fill();
ctx.globalAlpha = 1;
}
function drawBall(ball){
projection.toScreen(ball.x, ball.y, ball.z, pointArray);
ctx.fillStyle = ball.col;
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(pointArray[0],pointArray[1],ball.rad,0,Math.PI * 2);
ctx.stroke();
ctx.fill();
ctx.fillStyle = "white";
ctx.beginPath();
ctx.arc(pointArray[0]-ball.rad/2,pointArray[1]-ball.rad/2,ball.rad/4,0,Math.PI * 2);
ctx.fill();
}
function updateBall(ball){
// reset ball if out of bounds;
if(ball.x > mapSize.width || ball.y > mapSize.depth || ball.x < 0 || ball.y < 0){
ball.x += ball.dx;
ball.y += ball.dy;
ball.z += ball.dz;
ball.dz -= 0.1;
if(ball.z < -10){
ball.x = Math.random() * 3;
ball.y = Math.random() * 3;
ball.dz = 0;
// give random speed
ball.dx = Math.random() * 0.01;
ball.dy = Math.random() * 0.01;
}else{
ball.dist = Math.hypot(ball.x - 20,ball.y - 20, ball.z - 20);
return;
}
}
// get the block under the ball
var block = blocks[Math.floor(ball.x) + Math.floor(ball.y) * mapSize.width];
const lastZ = ball.z;
// get the height of the black at the balls position
ball.z = getHeight(block,ball.x,ball.y);
// use the face normal to add velocity in the direction of the normal
ball.dx += block.pNorm.x * 0.01;
ball.dy += block.pNorm.y * 0.01;
// move the ball up by the amount of its radius
ball.z += ball.rad / projection.height;
ball.dz =lastZ - ball.z;
// draw the shadow and ball
ball.x += ball.dx;
ball.y += ball.dy;
// get distance from camera;
ball.dist = Math.hypot(ball.x - 20,ball.y - 20, ball.z - 20);
}
function renderBall(ball){
drawBallShadow(ball);
drawBall(ball);
}
function copyCanvas(canvas){
var can = document.createElement("canvas");
can.width = canvas.width;
can.height = canvas.height;
can.ctx = can.getContext("2d");
can.ctx.drawImage(canvas,0,0);
return can;
}
var map = `
9988888789999
9887787678999
9877676567899
9876765678789
9876655567789
8766555456789
7655554456678
6654443456789
6543334566678
5432345677889
4321234567789
4321234567899
5412345678999
`.replace(/\n| |\t/g,"");
var mapSize = {width : 12, depth : 12}; // one less than map width and depth
var blocks = [];
ctx.clearRect(0,0,canvas.width,canvas.height)
createMap();
drawMap();
var background = copyCanvas(canvas);
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var globalTime; // global to this
var balls = [{
x : -10,
y : 0,
z : 100,
dx : 0,
dy : 0,
dz : 0,
col : "red",
rad : 10,
},{
x : -10,
y : 0,
z : 100,
dx : 0,
dy : 0,
dz : 0,
col : "Green",
rad : 10,
},{
x : -10,
y : 0,
z : 100,
dx : 0,
dy : 0,
dz : 0,
col : "Blue",
rad : 10,
},{
x : -10,
y : 0,
z : 100,
dx : 0,
dy : 0,
dz : 0,
col : "yellow",
rad : 10,
},{
x : -10,
y : 0,
z : 100,
dx : 0,
dy : 0,
dz : 0,
col : "cyan",
rad : 10,
},{
x : -10,
y : 0,
z : 100,
dx : 0,
dy : 0,
dz : 0,
col : "black",
rad : 10,
},{
x : -10,
y : 0,
z : 100,
dx : 0,
dy : 0,
dz : 0,
col : "white",
rad : 10,
},{
x : -10,
y : 0,
z : 100,
dx : 0,
dy : 0,
dz : 0,
col : "orange",
rad : 10,
}
];
// main update function
function update(timer){
globalTime = timer;
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,w,h);
ctx.drawImage(background,0,0);
// get the block under the ball
for(var i = 0; i < balls.length; i ++){
updateBall(balls[i]);
}
balls.sort((a,b)=>b.dist - a.dist);
for(var i = 0; i < balls.length; i ++){
renderBall(balls[i]);
}
requestAnimationFrame(update);
}
requestAnimationFrame(update);