Skip to content

Instantly share code, notes, and snippets.

@MegaLoler
Created September 3, 2017 00:21
Show Gist options
  • Save MegaLoler/7bb899fae99c2c225a2af75fc889c41e to your computer and use it in GitHub Desktop.
Save MegaLoler/7bb899fae99c2c225a2af75fc889c41e to your computer and use it in GitHub Desktop.
Ball physics with a tile map!
<html>
<head>
<title>Marb Maze Yo</title>
<style>
#id
{
padding: 0px;
margin: 0px;
border: 0px;
}
body
{
padding: 0px;
margin: 0px;
border: 0px;
}
</style>
</head>
<script>
var freeMouseDebug = false;
var debugGraphics = true;
var simulation;
function arraySafeGet(array, index)
{
if(array[index] == undefined)
{
return 0;
}
else
{
return array[index];
}
}
function normalizeVector(vector)
{
var length = getVectorLength(vector);
return vectorDivideConstant(vector, length);
}
function rotateVectorClockwise(vector)
{
return [vector[1], -vector[0]];
}
function rotateVectorCounterclockwise(vector)
{
return [-vector[1], vector[0]];
}
function dotProduct(a, b)
{
var sum = 0;
for(var i = 0; i < a.length; i++)
{
sum += a[i] * b[i];
}
return sum;
}
function vectorAdd(a, b)
{
var result = [];
var longest;
if(a.length > b.length)
{
longest = a;
}
else
{
longest = b;
}
for(var i in longest)
{
var j = arraySafeGet(a, i) + arraySafeGet(b, i);
result.push(j);
}
return result;
}
function vectorSubtract(a, b)
{
var result = [];
var longest;
if(a.length > b.length)
{
longest = a;
}
else
{
longest = b;
}
for(var i in longest)
{
var j = arraySafeGet(a, i) - arraySafeGet(b, i);
result.push(j);
}
return result;
}
function vectorMultiply(a, b)
{
var result = [];
var longest;
if(a.length > b.length)
{
longest = a;
}
else
{
longest = b;
}
for(var i in longest)
{
var j = arraySafeGet(a, i) * arraySafeGet(b, i);
result.push(j);
}
return result;
}
function vectorDivide(a, b)
{
var result = [];
var longest;
if(a.length > b.length)
{
longest = a;
}
else
{
longest = b;
}
for(var i in longest)
{
var j = arraySafeGet(a, i) / arraySafeGet(b, i);
result.push(j);
}
return result;
}
function vectorMultiplyConstant(vector, coefficient)
{
var result = [];
for(var value of vector)
{
result.push(value * coefficient);
}
return result;
}
function vectorDivideConstant(vector, coefficient)
{
var result = [];
for(var value of vector)
{
result.push(value / coefficient);
}
return result;
}
function getDistance(a, b)
{
var difference = vectorSubtract(a, b);
return getVectorLength(difference);
}
function getVectorLength(vector)
{
var sum = 0;
for(var value of vector)
{
sum += Math.pow(value, 2);
}
return Math.sqrt(sum);
}
function Tile_upLeftIncline()
{
// global pixel space
this.draw = function(context, tileSize, position)
{
context.beginPath();
context.moveTo((position[0]) * tileSize, (position[1] + 1) * tileSize);
context.lineTo((position[0] + 1) * tileSize, (position[1]) * tileSize);
context.lineTo((position[0] + 1) * tileSize, (position[1] + 1) * tileSize);
context.closePath();
context.fill();
};
// within tile bounds, local tile space
// vector to surface
this.getSurfaceVector = function(position)
{
var x = position[0];
var y = position[1];
var left = 0;
var right = 1;
var top = 0;
var bottom = 1;
var topDirection = top - y;
var bottomDirection = bottom - y;
var leftDirection = left - x;
var rightDirection = right - x;
if(x > left && x < right && y > bottom)
{
return [0, bottomDirection];
}
else if(y > top && y < bottom && x > right)
{
return [rightDirection, 0];
}
else if(x >= right && y >= bottom)
{
var cornerX = right;
var cornerY = bottom;
var dx = cornerX - x;
var dy = cornerY - y;
return [dx, dy];
}
else
{
var surfaceDirection = normalizeVector([1, -1]);
var normal = rotateVectorClockwise(surfaceDirection)
var surfaceTranslation = vectorMultiplyConstant(normal, Math.sqrt(2) / 2);
var dp = dotProduct(surfaceDirection, position);
var surfacePosition = vectorMultiplyConstant(surfaceDirection, dp);
surfacePosition[0] = Math.min(surfacePosition[0], 0.5);
surfacePosition[0] = Math.max(surfacePosition[0], -0.5);
surfacePosition[1] = Math.min(surfacePosition[1], 0.5);
surfacePosition[1] = Math.max(surfacePosition[1], -0.5);
var translatedPosition = vectorAdd(position, surfaceTranslation);
var surfaceVector = vectorSubtract(surfacePosition, translatedPosition);
return surfaceVector;
}
};
this.detectCollision = function(position)
{
return true;
};
}
function Tile_upRightIncline()
{
// global pixel space
this.draw = function(context, tileSize, position)
{
context.beginPath();
context.moveTo((position[0]) * tileSize, (position[1]) * tileSize);
context.lineTo((position[0] + 1) * tileSize, (position[1] + 1) * tileSize);
context.lineTo((position[0]) * tileSize, (position[1] + 1) * tileSize);
context.closePath();
context.fill();
};
// within tile bounds, local tile space
// vector to surface
this.getSurfaceVector = function(position)
{
var x = position[0];
var y = position[1];
var left = 0;
var right = 1;
var top = 0;
var bottom = 1;
var topDirection = top - y;
var bottomDirection = bottom - y;
var leftDirection = left - x;
var rightDirection = right - x;
if(x > left && x < right && y > bottom)
{
return [0, bottomDirection];
}
else if(y > top && y < bottom && x < left)
{
return [leftDirection, 0];
}
else if(x <= left && y >= bottom)
{
var cornerX = left;
var cornerY = bottom;
var dx = cornerX - x;
var dy = cornerY - y;
return [dx, dy];
}
else
{
var surfaceDirection = normalizeVector([1, 1]);
var dp = dotProduct(surfaceDirection, position);
var surfacePosition = vectorMultiplyConstant(surfaceDirection, dp);
surfacePosition[0] = Math.min(surfacePosition[0], 1);
surfacePosition[0] = Math.max(surfacePosition[0], 0);
surfacePosition[1] = Math.min(surfacePosition[1], 1);
surfacePosition[1] = Math.max(surfacePosition[1], 0);
var surfaceVector = vectorSubtract(surfacePosition, position);
return surfaceVector;
}
};
this.detectCollision = function(position)
{
return true;
};
}
function Tile_downLeftIncline()
{
// global pixel space
this.draw = function(context, tileSize, position)
{
context.beginPath();
context.moveTo((position[0]) * tileSize, (position[1]) * tileSize);
context.lineTo((position[0] + 1) * tileSize, (position[1]) * tileSize);
context.lineTo((position[0] + 1) * tileSize, (position[1] + 1) * tileSize);
context.closePath();
context.fill();
};
// within tile bounds, local tile space
// vector to surface
this.getSurfaceVector = function(position)
{
var x = position[0];
var y = position[1];
var left = 0;
var right = 1;
var top = 0;
var bottom = 1;
var topDirection = top - y;
var bottomDirection = bottom - y;
var leftDirection = left - x;
var rightDirection = right - x;
if(x > left && x < right && y < top)
{
return [0, topDirection];
}
else if(y > top && y < bottom && x > right)
{
return [rightDirection, 0];
}
else if(x >= right && y <= top)
{
var cornerX = right;
var cornerY = top;
var dx = cornerX - x;
var dy = cornerY - y;
return [dx, dy];
}
else
{
var surfaceDirection = normalizeVector([1, 1]);
var normal = rotateVectorClockwise(surfaceDirection)
var surfaceTranslation = vectorMultiplyConstant(normal, Math.sqrt(2) / 2);
var dp = dotProduct(surfaceDirection, position);
var surfacePosition = vectorMultiplyConstant(surfaceDirection, dp);
surfacePosition[0] = Math.min(surfacePosition[0], 1);
surfacePosition[0] = Math.max(surfacePosition[0], 0);
surfacePosition[1] = Math.min(surfacePosition[1], 1);
surfacePosition[1] = Math.max(surfacePosition[1], 0);
var translatedPosition = vectorAdd(position, surfaceTranslation);
var surfaceVector = vectorSubtract(surfacePosition, position);
return surfaceVector;
}
};
this.detectCollision = function(position)
{
return true;
};
}
function Tile_downRightIncline()
{
// global pixel space
this.draw = function(context, tileSize, position)
{
context.beginPath();
context.moveTo((position[0]) * tileSize, (position[1]) * tileSize);
context.lineTo((position[0] + 1) * tileSize, (position[1]) * tileSize);
context.lineTo((position[0]) * tileSize, (position[1] + 1) * tileSize);
context.closePath();
context.fill();
};
// within tile bounds, local tile space
// vector to surface
this.getSurfaceVector = function(position)
{
var x = position[0];
var y = position[1];
var left = 0;
var right = 1;
var top = 0;
var bottom = 1;
var topDirection = top - y;
var bottomDirection = bottom - y;
var leftDirection = left - x;
var rightDirection = right - x;
if(x > left && x < right && y < top)
{
return [0, topDirection];
}
else if(y > top && y < bottom && x < left)
{
return [leftDirection, 0];
}
else if(x <= left && y <= top)
{
var cornerX = left;
var cornerY = top;
var dx = cornerX - x;
var dy = cornerY - y;
return [dx, dy];
}
else
{
var surfaceDirection = normalizeVector([1, -1]);
var normal = rotateVectorClockwise(surfaceDirection)
var surfaceTranslation = vectorMultiplyConstant(normal, Math.sqrt(2) / 2);
var dp = dotProduct(surfaceDirection, position);
var surfacePosition = vectorMultiplyConstant(surfaceDirection, dp);
surfacePosition[0] = Math.min(surfacePosition[0], 0.5);
surfacePosition[0] = Math.max(surfacePosition[0], -0.5);
surfacePosition[1] = Math.min(surfacePosition[1], 0.5);
surfacePosition[1] = Math.max(surfacePosition[1], -0.5);
var translatedPosition = vectorAdd(position, surfaceTranslation);
var surfaceVector = vectorSubtract(surfacePosition, translatedPosition);
return surfaceVector;
}
};
this.detectCollision = function(position)
{
return true;
};
}
function Tile_block()
{
// global pixel space
this.draw = function(context, tileSize, position)
{
context.fillRect(position[0] * tileSize, position[1] * tileSize, tileSize, tileSize);
};
// within tile bounds, local tile space
// vector to surface
this.getSurfaceVector = function(position)
{
var x = position[0];
var y = position[1];
var left = 0;
var right = 1;
var top = 0;
var bottom = 1;
var topDirection = top - y;
var bottomDirection = bottom - y;
var leftDirection = left - x;
var rightDirection = right - x;
var topDistance = Math.abs(topDirection);
var bottomDistance = Math.abs(bottomDirection);
var leftDistance = Math.abs(leftDirection);
var rightDistance = Math.abs(rightDirection);
if(x > left && x < right)
{
var direction = topDistance < bottomDistance ? topDirection : bottomDirection;
return [0, direction];
}
else if(y > top && y < bottom)
{
var direction = leftDistance < rightDistance ? leftDirection : rightDirection;
return [direction, 0];
}
else
{
var cornerX, cornerY;
if(x <= left && y <= top)
{
cornerX = left;
cornerY = top;
}
else if(x >= right && y <= top)
{
cornerX = right;
cornerY = top;
}
else if(x <= left && y >= bottom)
{
cornerX = left;
cornerY = bottom;
}
else if(x >= right && y >= bottom)
{
cornerX = right;
cornerY = bottom;
}
var dx = cornerX - x;
var dy = cornerY - y;
return [dx, dy];
}
};
this.detectCollision = function(position)
{
var x = position[0];
var y = position[1];
var xCollide = x >= 0 && x <= 1;
var yCollide = y >= 0 && y <= 1;
return xCollide && yCollide;
};
}
function Tile_air()
{
// global pixel space
this.draw = function(context, tileSize, position)
{
};
// within tile bounds, local tile space
// vector to surface
this.getSurfaceVector = function(position)
{
return [0, 0];
};
this.detectCollision = function(position)
{
return false;
};
}
var tileSet = [new Tile_air(), new Tile_block(), new Tile_upLeftIncline(), new Tile_upRightIncline(), new Tile_downLeftIncline(), new Tile_downRightIncline()];
function Marble(position)
{
this.position = position;
this.previousPosition = position;
this.radius = 0.75;
this.draw = function(context, simulation)
{
var x = (this.position[0] - simulation.cameraX) * simulation.tileSize;
var y = (this.position[1] - simulation.cameraY) * simulation.tileSize;
context.beginPath();
context.arc(x, y, this.radius * simulation.tileSize, 0, Math.PI * 2);
context.fill();
}
this.update = function(context, simulation)
{
if(!freeMouseDebug)
{
if(mouseDown)
{
var mousePosition = [mouseX / simulation.tileSize + simulation.cameraX, mouseY / simulation.tileSize + simulation.cameraY];
var delta = vectorSubtract(mousePosition, this.position);
var distance = getVectorLength(delta);
var direction = vectorDivideConstant(delta, distance);
var mouseForce = vectorMultiplyConstant(direction, simulation.acceleration);
this.position = vectorAdd(this.position, mouseForce);
}
this.position = vectorAdd(this.position, [0, simulation.gravity]);
}
this.collisions(context, simulation);
this.integrate();
}
this.collisions = function(context, simulation)
{
var left = this.position[0] - this.radius;
var right = this.position[0] + this.radius;
var top = this.position[1] - this.radius;
var bottom = this.position[1] + this.radius;
for(var tileX = Math.floor(left); tileX <= right; tileX++)
{
for(var tileY = Math.floor(top); tileY <= bottom; tileY++)
{
var tile = simulation.getTile(tileX, tileY);
var localX = this.position[0] - tileX;
var localY = this.position[1] - tileY;
var localPosition = [localX, localY];
if(debugGraphics)
{
context.strokeStyle = "red";
var pixelX = (tileX - simulation.cameraX) * simulation.tileSize;
var pixelY = (tileY - simulation.cameraY) * simulation.tileSize;
context.strokeRect(pixelX, pixelY, simulation.tileSize, simulation.tileSize);
}
var surfaceVector = tile.getSurfaceVector(localPosition);
var length = getVectorLength(surfaceVector);
if(debugGraphics)
{
context.strokeStyle = "blue";
var pixelX = (this.position[0] - simulation.cameraX) * simulation.tileSize;
var pixelY = (this.position[1] - simulation.cameraY) * simulation.tileSize;
var pixelX2 = (this.position[0] + surfaceVector[0] - simulation.cameraX) * simulation.tileSize;
var pixelY2 = (this.position[1] + surfaceVector[1] - simulation.cameraY) * simulation.tileSize;
context.beginPath();
context.moveTo(pixelX, pixelY);
context.lineTo(pixelX2, pixelY2);
context.stroke();
}
if(length < this.radius)
{
var penetrationAmount = this.radius - length;
var direction = vectorDivideConstant(surfaceVector, length);
var surfaceVector = vectorAdd(localPosition, vectorMultiplyConstant(direction, this.radius));
if(debugGraphics)
{
context.strokeStyle = "yellow";
var pixelX = (tileX - simulation.cameraX) * simulation.tileSize;
var pixelY = (tileY - simulation.cameraY) * simulation.tileSize;
var pixelX2 = (tileX + surfaceVector[0] - simulation.cameraX) * simulation.tileSize;
var pixelY2 = (tileY + surfaceVector[1] - simulation.cameraY) * simulation.tileSize;
context.beginPath();
context.moveTo(pixelX, pixelY);
context.lineTo(pixelX2, pixelY2);
context.stroke();
}
if(tile.detectCollision(surfaceVector))
{
var penetrationVector = vectorMultiplyConstant(direction, -penetrationAmount);
if(debugGraphics)
{
context.strokeStyle = "green";
var pixelX = (this.position[0] - simulation.cameraX) * simulation.tileSize;
var pixelY = (this.position[1] - simulation.cameraY) * simulation.tileSize;
var pixelX2 = (this.position[0] + penetrationVector[0] - simulation.cameraX) * simulation.tileSize;
var pixelY2 = (this.position[1] + penetrationVector[1] - simulation.cameraY) * simulation.tileSize;
context.beginPath();
context.moveTo(pixelX, pixelY);
context.lineTo(pixelX2, pixelY2);
context.stroke();
}
penetrationVector = vectorDivideConstant(penetrationVector, simulation.impulseDivision);
if(!freeMouseDebug) this.position = vectorAdd(this.position, penetrationVector);
}
}
}
}
}
this.getVelocity = function()
{
return vectorSubtract(this.position, this.previousPosition)
}
this.integrate = function()
{
if(freeMouseDebug)
{
this.position = [mouseX / simulation.tileSize + simulation.cameraX, mouseY / simulation.tileSize + simulation.cameraY];
}
else
{
var currentPosition = this.position;
this.position = vectorAdd(this.position, this.getVelocity());
this.previousPosition = currentPosition;
}
}
}
function Simulation()
{
this.map = [];
this.mapWrap = true;
this.mapWidth = 16;
this.mapHeight = 16;
this.ether = tileSet[1];
this.cameraX = 0;
this.cameraY = 0;
// coordinate system is origin = top left, unit = tile size
this.tileSize = 32;
this.marbles = [];
this.gravity = 0.0001;
this.sweeps = 5;
this.impulseDivision = 5;
this.acceleration = 0.0005;
this.getTile = function(x, y)
{
if(this.mapWrap)
{
while(x < 0) x += this.mapWidth;
while(y < 0) y += this.mapHeight;
x %= this.mapWidth;
y %= this.mapHeight;
}
else
{
if(x < 0 || x >= this.mapWidth) return this.ether;
if(y < 0 || y >= this.mapHeight) return this.ether;
}
var tile = this.map[x + y * this.mapWidth];
if(tile == undefined) return this.ether;
return tile;
}
this.drawMap = function(context)
{
var width = context.canvas.width / this.tileSize;
var height = context.canvas.height / this.tileSize;
for(var x = this.cameraX; x < this.cameraX + 1 + width; x += 1)
{
for(var y = this.cameraY; y < this.cameraY + 1 + height; y += 1)
{
var indexX = Math.floor(x);
var indexY = Math.floor(y);
var tile = this.getTile(indexX, indexY);
var position = [indexX - this.cameraX, indexY - this.cameraY];
tile.draw(context, this.tileSize, position);
}
}
}
this.draw = function(context)
{
context.fillStyle = "gray";
this.drawMap(context);
for(var marble of this.marbles)
{
marble.draw(context, this);
}
}
this.update = function(context)
{
for(var i = 0; i < this.sweeps; i++)
{
for(var marble of this.marbles)
{
marble.update(context, this);
}
}
}
}
function draw()
{
context.clearRect(0, 0, canvas.width, canvas.height);
simulation.draw(context);
}
function physics()
{
simulation.update(context);
}
function loop()
{
draw();
physics();
enterLoop();
}
function enterLoop()
{
window.requestAnimationFrame(loop);
}
function mapFromIndices(indices)
{
var map = [];
for(var index of indices)
{
map.push(tileSet[index]);
}
return map;
}
function setup()
{
canvas = document.getElementById("canvas");
context = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
simulation = new Simulation();
simulation.map = mapFromIndices(
[0,0,0,0,0,0,0,0,4,5,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,4,5,0,
0,0,0,2,1,3,0,0,0,0,0,0,0,0,0,0,
0,0,2,1,1,1,3,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,4,1,3,0,0,2,1,3,0,0,0,
0,0,0,0,0,0,1,1,1,1,1,1,1,3,0,0,
0,0,0,0,0,0,4,1,1,5,0,0,0,4,3,0,
0,0,0,0,0,0,0,4,5,0,0,0,0,0,1,3,
3,0,0,0,0,0,0,0,0,0,0,0,0,0,4,1,
1,3,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,4,
4,1,0,0,0,0,0,0,0,0,2,1,3,0,0,0,
0,5,0,0,0,0,0,0,2,1,1,1,5,0,0,0,
0,0,0,2,1,1,1,1,1,1,1,5,0,0,0,0,
0,0,2,1,5,0,0,4,1,1,5,0,0,0,0,0,]
);
simulation.marbles.push(new Marble([6,0]));
}
function init()
{
setup();
enterLoop();
}
function mouseDrag()
{
var dx = mouseX - mouseClickX;
var dy = mouseY - mouseClickY;
simulation.cameraX = clickCameraX - dx / simulation.tileSize;
simulation.cameraY = clickCameraY - dy / simulation.tileSize;
}
window.onmousedown = function(event)
{
mouseClickX = mouseX;
mouseClickY = mouseY;
clickCameraX = simulation.cameraX;
clickCameraY = simulation.cameraY;
mouseDown = true;
}
window.onmouseup = function(event)
{
mouseDown = false;
}
window.onload = init;
var mouseX, mouseY;
var mouseClickX, mouseClickY;
var clickCameraX, clickCameraY;
var mouseDown = false;
// STOLEN MOUSE TRACKING CODE BC LAZY ><
document.onmousemove = function(event)
{
var dot, eventDoc, doc, body, pageX, pageY;
event = event || window.event; // IE-ism
// If pageX/Y aren't available and clientX/Y are,
// calculate pageX/Y - logic taken from jQuery.
// (This is to support old IE)
if (event.pageX == null && event.clientX != null) {
eventDoc = (event.target && event.target.ownerDocument) || document;
doc = eventDoc.documentElement;
body = eventDoc.body;
event.pageX = event.clientX +
(doc && doc.scrollLeft || body && body.scrollLeft || 0) -
(doc && doc.clientLeft || body && body.clientLeft || 0);
event.pageY = event.clientY +
(doc && doc.scrollTop || body && body.scrollTop || 0) -
(doc && doc.clientTop || body && body.clientTop || 0 );
}
// Use event.pageX / event.pageY here
mouseX = event.pageX;
mouseY = event.pageY;
if(mouseDown) mouseDrag();
}
// OK THATS ALL TE STOLEN CODE
</script>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment