Skip to content

Instantly share code, notes, and snippets.

@MegaLoler
Created September 3, 2017 00:19
Show Gist options
  • Save MegaLoler/01ec3d27d3d50da7cf5b40fae10014c2 to your computer and use it in GitHub Desktop.
Save MegaLoler/01ec3d27d3d50da7cf5b40fae10014c2 to your computer and use it in GitHub Desktop.
Bouncing point mass structures on springs in a 3d tunnel
<html>
<head>
<title>Bouncing Vubes are Godo</title>
<style>
#id
{
padding: 0px;
margin: 0px;
border: 0px;
}
body
{
padding: 0px;
margin: 0px;
border: 0px;
}
</style>
<script>
var canvas, context;
var simulation;
var pointRadius = 2;
var mouseZ = 0;
var mouseRadius = 32;
var touchingPoint = null;
var mouseConstraint = new Constraint(null, new Point([0, 0, 0]), 0);
var roundingTolerance = 1000000;
window.onmousedown = function()
{
mouseConstraint.a = touchingPoint;
}
window.onmouseup = function()
{
mouseConstraint.a = null;
}
function unit()
{
return context.canvas.height / 2.0;
}
function unitToPixel(value, context)
{
return value * unit();
}
function pixelToUnit(value, context)
{
return value / unit();
}
function pixelToVector(vector, context)
{
var converted = [];
for(var value of vector)
{
converted.push(pixelToUnit(value, context));
}
return converted;
}
function vectorToPixel(vector, context)
{
var converted = [];
for(var value of vector)
{
converted.push(unitToPixel(value, context));
}
return converted;
}
function centerVector(vector, context)
{
return [vector[0] + context.canvas.width / 2.0, vector[1] + context.canvas.height / 2.0].concat(vector.slice(2));
}
function decenterVector(vector, context)
{
return [vector[0] - context.canvas.width / 2.0, vector[1] - context.canvas.height / 2.0];
}
function projectVector(vector, fieldOfView)
{
var result = vector.slice(); // copy
while(result.length > 2)
{
var distance = result.pop();
var mul = Math.pow(fieldOfView, distance);
for(var i in result)
{
result[i] *= mul;
}
}
return result;
}
function vectorToPixelAndCenter(vector, context)
{
return centerVector(vectorToPixel(vector, context), context);
}
function vectorProjectToPixelAndCenter(vector, context, fieldOfView)
{
return centerVector(vectorToPixel(projectVector(vector, fieldOfView), context), context);
}
function arraySafeGet(array, index)
{
if(array[index] == undefined)
{
return 0;
}
else
{
return array[index];
}
}
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);
var sum = 0;
for(var value of difference)
{
sum += Math.pow(value, 2);
}
return Math.sqrt(sum);
}
function Point(position)
{
this.position = position;
this.previousPosition = position;
this.static = false;
this.mass = 1;
this.getVelocity = Point_getVelocity;
this.integrate = Point_integrate;
this.draw = Point_draw;
this.getPixelPosition = Point_getPixelPosition;
}
function Point_getVelocity()
{
return vectorSubtract(this.position, this.previousPosition)
}
function Point_getPixelPosition(fieldOfView)
{
return vectorProjectToPixelAndCenter(this.position, context, fieldOfView);
}
function Point_draw(context, fieldOfView)
{
var position = this.getPixelPosition(fieldOfView);
var x = position[0] - pointRadius;
var y = position[1] - pointRadius;
context.fillRect(x, y, pointRadius * 2 + 1, pointRadius * 2 + 1);
}
function Point_integrate()
{
if(this.static) return;
var currentPosition = this.position;
this.position = vectorAdd(this.position, this.getVelocity());
this.previousPosition = currentPosition;
}
function Constraint(a, b, distance)
{
this.a = a;
this.b = b;
this.distance = distance;
this.impulseDivision = 10;
this.apply = Constraint_apply;
this.draw = Constraint_draw;
}
function Constraint_draw(context, fieldOfView)
{
if(this.a == null || this.b == null) return;
var position1 = vectorProjectToPixelAndCenter(this.a.position, context, fieldOfView);
var position2 = vectorProjectToPixelAndCenter(this.b.position, context, fieldOfView);
context.beginPath();
context.moveTo(position1[0], position1[1]);
context.lineTo(position2[0], position2[1]);
context.stroke();
}
function Constraint_apply()
{
if(this.a == null || this.b == null) return;
var distance = getDistance(this.a.position, this.b.position);
if(distance == 0) return;
var penetration = distance - this.distance;
var direction = vectorSubtract(this.b.position, this.a.position);
direction = vectorDivideConstant(direction, distance);
var force = penetration / this.impulseDivision;
var totalMass = this.a.mass + this.b.mass;
var weightA = this.b.mass / totalMass;
var weightB = this.a.mass / totalMass;
var forceA = vectorMultiplyConstant(direction, force * weightA);
var forceB = vectorMultiplyConstant(direction, -force * weightB);
this.a.position = vectorAdd(this.a.position, forceA);
this.b.position = vectorAdd(this.b.position, forceB);
}
function Simulation()
{
this.sweeps = 5;
this.gravity = 0.00008;
this.airFriction = 0.005;
this.collisionImpulseDivision = 3;
this.borderDistance = 0.5;
this.fieldOfView = 2;
this.points = [];
this.constraints = [];
this.draw = Simulation_draw;
this.update = Simulation_update;
this.step = Simulation_step;
this.getPointReflection1 = Simulation_getPointReflection1;
this.getPointReflection2 = Simulation_getPointReflection2;
this.getPointReflection3 = Simulation_getPointReflection3;
this.getPointReflection4 = Simulation_getPointReflection4;
this.getConstraintReflection1 = Simulation_getConstraintReflection1;
this.getConstraintReflection2 = Simulation_getConstraintReflection2;
this.getConstraintReflection3 = Simulation_getConstraintReflection3;
this.getConstraintReflection4 = Simulation_getConstraintReflection4;
}
function Simulation_getPointReflection1(point)
{
var reflectionPosition = vectorAdd(point.position, [0, this.borderDistance]);
var multiplyMask = Array(reflectionPosition.length).fill(1);
multiplyMask[1] = -1;
reflectionPosition = vectorMultiply(reflectionPosition, multiplyMask);
reflectionPosition = vectorAdd(reflectionPosition, [0, -this.borderDistance]);
return new Point(reflectionPosition);
}
function Simulation_getPointReflection2(point)
{
var reflectionPosition = vectorAdd(point.position, [0, -this.borderDistance]);
var multiplyMask = Array(reflectionPosition.length).fill(1);
multiplyMask[1] = -1;
reflectionPosition = vectorMultiply(reflectionPosition, multiplyMask);
reflectionPosition = vectorAdd(reflectionPosition, [0, this.borderDistance]);
return new Point(reflectionPosition);
}
function Simulation_getPointReflection3(point)
{
var ratio = context.canvas.width / context.canvas.height;
var reflectionPosition = vectorAdd(point.position, [this.borderDistance * ratio]);
var multiplyMask = Array(reflectionPosition.length).fill(1);
multiplyMask[0] = -1;
reflectionPosition = vectorMultiply(reflectionPosition, multiplyMask);
reflectionPosition = vectorAdd(reflectionPosition, [-this.borderDistance * ratio]);
return new Point(reflectionPosition);
}
function Simulation_getPointReflection4(point)
{
var ratio = context.canvas.width / context.canvas.height;
var reflectionPosition = vectorAdd(point.position, [-this.borderDistance * ratio]);
var multiplyMask = Array(reflectionPosition.length).fill(1);
multiplyMask[0] = -1;
reflectionPosition = vectorMultiply(reflectionPosition, multiplyMask);
reflectionPosition = vectorAdd(reflectionPosition, [this.borderDistance * ratio]);
return new Point(reflectionPosition);
}
function Simulation_getConstraintReflection1(constraint)
{
return new Constraint(this.getPointReflection1(constraint.a), this.getPointReflection1(constraint.b), 0);
}
function Simulation_getConstraintReflection2(constraint)
{
return new Constraint(this.getPointReflection2(constraint.a), this.getPointReflection2(constraint.b), 0);
}
function Simulation_getConstraintReflection3(constraint)
{
return new Constraint(this.getPointReflection3(constraint.a), this.getPointReflection3(constraint.b), 0);
}
function Simulation_getConstraintReflection4(constraint)
{
return new Constraint(this.getPointReflection4(constraint.a), this.getPointReflection4(constraint.b), 0);
}
function Simulation_draw(context)
{
for(var constraint of this.constraints)
{
// reflection
context.strokeStyle = "#888888";
if(constraint.a != null && constraint.b != null)
{
this.getConstraintReflection1(constraint).draw(context, this.fieldOfView);
this.getConstraintReflection2(constraint).draw(context, this.fieldOfView);
this.getConstraintReflection3(constraint).draw(context, this.fieldOfView);
this.getConstraintReflection4(constraint).draw(context, this.fieldOfView);
}
}
touchingPoint = null;
for(var point of this.points)
{
// reflection
context.fillStyle = "#888888";
this.getPointReflection1(point).draw(context, this.fieldOfView);
this.getPointReflection2(point).draw(context, this.fieldOfView);
this.getPointReflection3(point).draw(context, this.fieldOfView);
this.getPointReflection4(point).draw(context, this.fieldOfView);
}
for(var i = 0; i < 24; i++)
{
var d = Math.pow(this.borderDistance, (i + 1) / 2);
context.strokeStyle = "rgba(255, 100, 0, 1)";
var ratio = context.canvas.width / context.canvas.height;
var x = (context.canvas.width - context.canvas.width * d) / 2;
var y = (context.canvas.height - context.canvas.height * d) / 2;
context.strokeRect(x, y, context.canvas.width * d, context.canvas.height * d);
context.strokeStyle = "rgba(255, 100, 0, " + (d * 0.5 + 0.01) + ")";
context.beginPath();
context.moveTo(x, 0);
context.lineTo(x, context.canvas.height);
context.stroke();
context.beginPath();
context.moveTo(context.canvas.width - x, 0);
context.lineTo(context.canvas.width - x, context.canvas.height);
context.stroke();
context.beginPath();
context.moveTo(0, y);
context.lineTo(context.canvas.width, y);
context.stroke();
context.beginPath();
context.moveTo(0, context.canvas.height - y);
context.lineTo(context.canvas.width, context.canvas.height - y);
context.stroke();
}
context.strokeStyle = "rgba(255, 100, 0, 1)";
context.beginPath();
context.moveTo(0, 0);
context.lineTo(context.canvas.width, context.canvas.height);
context.stroke();
context.beginPath();
context.moveTo(0, context.canvas.height);
context.lineTo(context.canvas.width, 0);
context.stroke();
for(var constraint of this.constraints)
{
// non reflection
context.strokeStyle = "#000000";
constraint.draw(context, this.fieldOfView);
}
touchingPoint = null;
for(var point of this.points)
{
// non reflection
var position = point.getPixelPosition(this.fieldOfView);
var distance = getDistance(position, [mouseX, mouseY]);
if(distance < mouseRadius)
{
context.fillStyle = "#FF0000";
touchingPoint = point;
}
else
{
context.fillStyle = "#000000";
}
point.draw(context, this.fieldOfView);
}
}
function Simulation_step(context)
{
for(var point of this.points)
{
point.position[1] += this.gravity;
}
for(var point of this.points)
{
var force = vectorMultiplyConstant(point.getVelocity(), -this.airFriction);
point.position = vectorAdd(point.position, force);
}
for(var constraint of this.constraints)
{
constraint.apply();
}
for(var point of this.points)
{
var ratio = context.canvas.width / context.canvas.height;
var top = -this.borderDistance;
var bottom = this.borderDistance;
var left = top * ratio;
var right = bottom * ratio;
var x = point.position[0];
var y = point.position[1];
var topPenetration = top - y;
var bottomPenetration = y - bottom;
var leftPenetration = left - x;
var rightPenetration = x - right;
if(topPenetration > 0)
{
point.position[1] += topPenetration / this.collisionImpulseDivision;
}
if(bottomPenetration > 0)
{
point.position[1] -= bottomPenetration / this.collisionImpulseDivision;
}
if(leftPenetration > 0)
{
point.position[0] += leftPenetration / this.collisionImpulseDivision;
}
if(rightPenetration > 0)
{
point.position[0] -= rightPenetration / this.collisionImpulseDivision;
}
}
for(var constraint of this.constraints)
{
constraint.apply();
}
for(var point of this.points)
{
point.integrate();
}
}
function Simulation_update(context)
{
for(var i = 0; i < this.sweeps; i++)
{
this.step(context);
}
}
function draw()
{
context.clearRect(0, 0, canvas.width, canvas.height);
simulation.draw(context);
}
function physics()
{
simulation.update(context);
}
function loop()
{
mouseConstraint.b.position = pixelToVector(decenterVector([mouseX, mouseY], context), context).concat([mouseZ]);
physics();
draw();
enterLoop();
}
function enterLoop()
{
window.requestAnimationFrame(loop);
}
function generateParallelConstraints(half1, half2, radius)
{
// non cross section
/*for(var i in half1)
{
var point1 = half1[i];
var point2 = half2[i];
var constraint = new Constraint(point1, point2, radius);
simulation.constraints.push(constraint);
}*/
// cross section
for(var point1 of half1)
{
for(var point2 of half2)
{
var constraint = new Constraint(point1, point2, getDistance(point1.position, point2.position));
simulation.constraints.push(constraint);
}
}
}
function generateNCube(n, radius, translation)
{
if(n == 0)
{
var point = new Point(translation);
simulation.points.push(point);
return [point];
}
var translationHalf1 = Array(n).fill(0);
var translationHalf2 = Array(n).fill(0);
translationHalf1[n - 1] = -radius;
translationHalf2[n - 1] = radius;
var half1 = generateNCube(n - 1, radius, vectorAdd(translation, translationHalf1));
var half2 = generateNCube(n - 1, radius, vectorAdd(translation, translationHalf2));
generateParallelConstraints(half1, half2, radius * 2);
return half1.concat(half2);
}
function round(value, place)
{
return Math.floor(value * place) / place;
}
function vectorIsEqualExceptIndex(a, b, exceptionIndex)
{
for(var i = 0; i < a.length; i++)
{
if(i == exceptionIndex) continue;
var value1 = round(a[i], roundingTolerance);
var value2 = round(b[i], roundingTolerance);
if(value1 != value2) return false;
}
return true;
}
function vectorIsEqual(a, b)
{
for(var i = 0; i < a.length; i++)
{
var value1 = round(a[i], roundingTolerance);
var value2 = round(b[i], roundingTolerance);
if(value1 != value2) return false;
}
return true;
}
function findPairPointAlongAxis(point, points, axis)
{
for(var pair of points)
{
if(vectorIsEqual(point.position, pair.position)) continue;
if(vectorIsEqualExceptIndex(point.position, pair.position, axis))
{
return pair;
}
}
// on of a kind
return point;
}
function reorderPointsForFlip(points, axis)
{
console.log(points);
var reordered = [];
for(var point of points)
{
var pair = findPairPointAlongAxis(point, points, axis);
reordered.push(pair);
}
return reordered;
}
// rotations =
// [0] = xy plane
// [1] = yz plane
// [2] = zw plane
// etc
function generateNSphere(n, radius, slices, translation, rotations)
{
if(n == 1)
{
var position1 = [radius];
var position2 = [-radius];
for(var i = 0; i < rotations.length; i++)
{
var angle = rotations[i];
var distance = position1[i];
var positionDimension1 = Math.cos(angle) * distance;
var positionDimension2 = Math.sin(angle) * distance;
var rotationPlaneDimension1 = i;
var rotationPlaneDimension2 = i + 1;
position1[rotationPlaneDimension1] = positionDimension1;
position1[rotationPlaneDimension2] = positionDimension2;
position2[rotationPlaneDimension1] = -positionDimension1;
position2[rotationPlaneDimension2] = -positionDimension2;
}
var point1 = new Point(vectorAdd(translation, position1));
var point2 = new Point(vectorAdd(translation, position2));
simulation.points.push(point1);
simulation.points.push(point2);
// braces
//var constraint = new Constraint(point1, point2, getDistance(point1.position, point2.position));
//simulation.constraints.push(constraint);
return [point1, point2];
}
var totalPoints = [];
var previousPoints = null;
var originalPoints = null;
for(var i = 0; i < slices + 2; i++)
{
if(i < slices + 1)
{
rotations[n - 2] = Math.PI * (i / (slices + 1));
var newPoints = generateNSphere(n - 1, radius, slices, translation, rotations);
}
else
{
var newPoints = reorderPointsForFlip(originalPoints, n - 2);
}
if(previousPoints != null)
{
// frame
/*for(var j = 0; j < newPoints.length; j++)
{
var point1 = newPoints[j];
var point2 = previousPoints[j];
var constraint = new Constraint(point1, point2, getDistance(point1.position, point2.position));
simulation.constraints.push(constraint);
}*/
}
else
{
originalPoints = newPoints;
}
if(i == slices + 1) break;
previousPoints = newPoints;
totalPoints = totalPoints.concat(newPoints);
}
return totalPoints;
}
function generateConstraintNet(points)
{
for(var a of points)
{
for(var b of points)
{
if(a != b)
{
var constraint = new Constraint(a, b, getDistance(a.position, b.position));
simulation.constraints.push(constraint);
}
}
}
}
function deleteDuplicatePoints(points)
{
for(var i = 0; i < points.length - 1; i++)
{
var a = points[i];
for(var j = i + 1; j < points.length; j++)
{
var b = points[j];
if(vectorIsEqual(a.position, b.position))
{
points.splice(j, 1);
j--;
}
}
}
}
function setup()
{
canvas = document.getElementById("canvas");
context = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
mouseConstraint.impulseDivision = 100;
//mouseConstraint.b.position.mass = 100;
simulation = new Simulation();
simulation.constraints.push(mouseConstraint);
simulation.points.push(mouseConstraint.b);
mouseConstraint.b.static = true;
generateNCube(3, 0.2, [-0.4, 0, -0.2])
var spherePoints = generateNSphere(3, 0.2, 3, [0.4, 0, -0.2], []);
deleteDuplicatePoints(simulation.points);
deleteDuplicatePoints(spherePoints);
generateConstraintNet(spherePoints);
/*var a = new Point([-0.2, 0]);
var b = new Point([0.2, 0]);
simulation.points.push(a);
simulation.points.push(b);
simulation.constraints.push(new Constraint(a, b, 0.4));*/
}
function init()
{
setup();
enterLoop();
}
window.onload = init;
var mouseX, mouseY;
// 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;
}
// OK THATS ALL TE STOLEN CODE
</script>
</head>
<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