console
var Point = function(x, y) {
this.current = {
x: x,
y: y
};
this.previous = {
x: x,
y: y
};
this.__defineGetter__('x', function() {
return this.current.x;
});
this.__defineGetter__('y', function() {
return this.current.y;
});
};
Object.assign(Point.prototype, {
integrate: function() {
if (this.isStatic){ return };
var current = this.current,
previous = this.previous,
x = current.x,
y = current.y;
current.x += x - previous.x;
current.y += y - previous.y + .28;
previous.x = x;
previous.y = y;
},
getDisplacement: function(centerOfMass) {
var x = this.current.x - centerOfMass.x,
y = this.current.y - centerOfMass.y;
return {
x: x,
y: y
};
}
});
var Body = function(points) {
this.points = points;
this.original = [];
this.centerOfMass = this.getCenterOfMass();
this.angle = 0;
this.stiffness = 1;
this.configure();
this.min = {};
this.max = {};
this.type = Body.POLY;
this.zindex = Body.zindex++;
};
Body.POLY = 0;
Body.LINE = 1;
Body.zindex = 0;
Body.prototype = Object.assign(Body.prototype, {
configure: function() {
var points = this.points || [],
l = points.length,
centerOfMass = this.getCenterOfMass();
while (l--) {
var point = points[l];
this.original[l] = this.original[l] || {};
this.original[l].x = point.current.x - centerOfMass.x;
this.original[l].y = point.current.y - centerOfMass.y;
}
},
getCenterOfMass: function(original) {
var points = this.points,
current = original ? "original" : "current",
l = points.length,
x = 0,
y = 0;
while (l--) {
x += points[l][current].x;
y += points[l][current].y;
}
return {
x: x / points.length,
y: y / points.length
};
},
shapeMatch: function() {
var centerOfMass = this.getCenterOfMass(),
angleDelta = 0;
var points = this.points,
l = points.length;
while (l--) {
var point = points[l],
displacement = point.getDisplacement(centerOfMass);
var cos = displacement.x * this.original[l].x + displacement.y * this.original[l].y,
sin = displacement.y * this.original[l].x - displacement.x * this.original[l].y;
angleDelta += Math.atan2(sin, cos);
}
angleDelta /= points.length;
this.angleDelta = angleDelta;
this.angle += angleDelta;
this.angle %= Math.PI * 2;
},
updatePositions: function() {
var points = this.points,
centerOfMass = this.getCenterOfMass(),
cos = Math.cos(this.angleDelta),
sin = Math.sin(this.angleDelta);
var l = points.length;
while (l--) {
var point = points[l],
goalX = cos * this.original[l].x - sin * this.original[l].y,
goalY = sin * this.original[l].x + cos * this.original[l].y;
this.original[l].x = goalX;
this.original[l].y = goalY;
goalX += centerOfMass.x;
goalY += centerOfMass.y;
point.current.x += (goalX - point.current.x) * this.stiffness;
point.current.y += (goalY - point.current.y) * this.stiffness;
}
this.centerOfMass.x = centerOfMass.x;
this.centerOfMass.y = centerOfMass.y;
},
updateAABB: function() {
var minX = this.points[0].current.x,
minY = this.points[0].current.y,
maxX = this.points[0].current.x,
maxY = this.points[0].current.y;
for (var i = 1, l = this.points.length; i < l; i++) {
var point = this.points[i];
minX = Math.min(point.current.x, minX);
minY = Math.min(point.current.y, minY);
maxX = Math.max(point.current.x, maxX);
maxY = Math.max(point.current.y, maxY);
}
this.max.x = maxX;
this.max.y = maxY;
this.min.x = minX;
this.min.y = minY;
}
})
var World = function(gravity, width, height) {
this.points = [];
this.bodies = [];
this.constraints = [];
this.width = width;
this.height = height;
this.gravity = gravity;
this.isStarted = true;
}
Object.assign(World.prototype, {
addPoint: function(point) {
if (Array.isArray(point)) {
for (var i = 0, l = point.length; i < l; i++){
this.addPoint(point[i]);
}
} else {
!~this.points.indexOf(point) && this.points.push(point);
}
return this;
},
addBody: function(body) {
return body && !~this.bodies.indexOf(body) && this.bodies.push(body) && this.addPoint(body.points);
},
shapeMatch: function() {
var l = this.bodies.length;
while (l--) {
this.bodies[l].shapeMatch();
this.bodies[l].updateAABB();
this.bodies[l].updatePositions();
}
return this;
},
updatePositions: function() {
var l = this.bodies.length;
while (l--) {
this.bodies[l].updatePositions();
}
return this;
},
bounds: function() {
var points = this.points,
i = points.length;
while (i--) {
var point = points[i],
x = point.current.x,
y = point.current.y;
point.current.x = Math.max(0, Math.min(this.width, x));
point.current.y = Math.max(0, Math.min(this.height, y));
if (y >= this.height) {
point.current.x -= (x - point.previous.x) * .1;
}
}
return this;
},
integrate: function() {
var points = this.points,
i = points.length;
while (i--) {
points[i].integrate();
}
return this;
},
getProjection: function(axis, points) {
var dot = points[0].x * axis.x + points[0].y * axis.y,
min = max = dot;
for (var i = 1; i < points.length; i++) {
var point = points[i];
dot = point.x * axis.x + point.y * axis.y;
if (dot < min) {
min = dot;
} else if (dot > max){
max = dot
};
}
return {
min: min,
max: max
};
},
distance: function(A, B) {
return A.min < B.min ? B.min - A.max : A.min - B.max;
},
overlap: function(A, B) {
return A.max.x > B.min.x && A.min.x < B.max.x && A.max.y > B.min.y && A.min.y < B.max.y;
},
detectCollisions: function() {
this.bodies.sort(function(a, b) {
return a.min.x > b.min.x ? 1 : -1;
});
for (var i = 0, l = this.bodies.length; i < l; i++) {
var A = this.bodies[i];
for (var j = i + 1; j < l && A.max.x > this.bodies[j].min.x; j++) {
var B = this.bodies[j];
if (A.type !== Body.POLY || B.type !== Body.POLY) {
continue;
}
if (this.overlap(A, B)) {
var data = this.getCollisionData(A, B);
if (data) {
this.resolveCollisions(A, B, data);
}
}
}
}
return this;
},
getCollisionData: function(A, B) {
var aLength = A.points.length,
bLength = B.points.length;
var depth = Number.POSITIVE_INFINITY,
edge = [],
bBody = B,
aBody = A,
axis, normal, contact;
for (var i = 0, l = aLength + bLength; i < l; i++) {
var body = i < aLength ? A : B,
j = i < aLength ? i : i - aLength,
n = (j + 1) === body.points.length ? 0 : j + 1;
var a = body.points[j],
b = body.points[n];
axis = {
x: a.y - b.y,
y: b.x - a.x
};
var invLength = 1 / Math.sqrt(axis.x * axis.x + axis.y * axis.y);
axis.x *= invLength;
axis.y *= invLength;
var projectionA = this.getProjection(axis, A.points),
projectionB = this.getProjection(axis, B.points),
distance = this.distance(projectionA, projectionB);
if (distance > 0) {
return false
} else if (Math.abs(distance) < depth) {
depth = Math.abs(distance);
normal = axis;
edge[0] = body.points[j];
edge[1] = body.points[n];
bBody = body;
}
}
if (bBody !== B) {
bBody = A;
aBody = B;
}
var centerX = aBody.centerOfMass.x - bBody.centerOfMass.x,
centerY = aBody.centerOfMass.y - bBody.centerOfMass.y;
if ((normal.x * centerX + normal.y * centerY) < 0) {
normal.x = -normal.x;
normal.y = -normal.y
};
var collisionVector = {
x: normal.x * depth,
y: normal.y * depth
};
var smallest = Number.POSITIVE_INFINITY;
for (var i = 0, l = aBody.points.length; i < l; i++) {
var point = aBody.points[i],
x = point.current.x - aBody.centerOfMass.x,
y = point.current.y - aBody.centerOfMass.y,
distance = normal.x * x + normal.y * y;
if (distance < smallest) {
smallest = distance;
contact = point;
}
}
return {
depth: depth,
edge: edge,
normal: normal,
vector: collisionVector,
contact: contact
};
},
resolveCollisions: function(A, B, collision) {
var a = collision.edge[0],
b = collision.edge[1];
var T;
if (Math.abs(a.x - b.x) > Math.abs(a.y - b.y)){
T = (collision.contact.x - collision.vector.x - a.x) / (b.x - a.x);
} else {
T = (collision.contact.x - collision.vector.y - a.y) / (b.y - a.y);
}
var lambda = 1.0 / (T * T + (1 - T) * (1 - T));
a.current.x -= collision.vector.x * (1 - T) * 0.5 * lambda;
a.current.y -= collision.vector.y * (1 - T) * 0.5 * lambda;
b.current.x -= collision.vector.x * T * 0.5 * lambda;
b.current.y -= collision.vector.y * T * 0.5 * lambda;
collision.contact.current.x += collision.vector.x * 0.5;
collision.contact.current.y += collision.vector.y * 0.5;
},
pause: function() {
this.started = false;
},
resume: function() {
this.started = true;
update();
},
findClosestPoint: function(input, radius) {
var closest,
points = this.points,
radius = radius || 5;
for (var i = 0, l = points.length; i < l; i++) {
var current = points[i].current;
if (
(current.x >= input.x - radius) &&
(current.x <= input.x + radius) &&
(current.y >= input.y - radius) &&
(current.y <= input.y + radius)
){
closest = points[i];
};
}
return closest;
}
})
var CanvasRenderer = function(canvas) {
this.canvas = canvas;
this.width = canvas.width;
this.height = canvas.height;
var g = this.g = canvas.getContext('2d');
this.render = function(world) {
var bodies = world.bodies.slice().sort(function(a, b) {
return a.zindex < b.zindex ? 1 : -1
})
for (var i = 0, l = bodies.length; i < l; i++) {
var body = bodies[i];
switch (body.type) {
case Body.LINE:
this.drawLine(body.points[0], body.points[1]);
break;
default:
this.drawPoly(body.points, body.color || 0x0);
}
}
};
this.clear = function() {
g.clearRect(0, 0, this.width, this.height);
};
this.drawCircle = function(position, color, size) {
g.save();
g.fillStyle = color || 0x0;
g.beginPath();
g.arc(position.x, position.y, size || 5, 0, Math.PI * 2, false);
g.fill();
g.restore();
};
this.drawLine = function(a, b, color, width) {
g.save();
g.strokeStyle = color || 0x0;
g.lineWidth = width || 1;
g.beginPath();
g.moveTo(a.x, a.y);
g.lineTo(b.x, b.y);
g.stroke();
g.restore();
};
this.drawPoly = function(points, color) {
var l = points.length,
firstPoint = points[l - 1].current;
g.save();
g.fillStyle = color || 0x0;
g.beginPath();
g.moveTo(firstPoint.x, firstPoint.y);
while (l--) {
var point = points[l].current;
g.lineTo(point.x, point.y);
}
g.fill();
g.restore();
};
this.drawRect = function(min, max, rotation, color) {
g.save();
g.fillStyle = color || 0x0;
g.beginPath();
g.rect(min.x, min.y, max.x - min.x, max.y - min.y);
g.fill();
g.restore();
}
};
var InputController = function(canvas, world) {
this.x = 0;
this.y = 0;
this.canvas = canvas;
this.world = world;
this.isPressed = false;
var self = this;
this.up = function(event) {
event.preventDefault();
self.isPressed = false;
}
this.down = function(event) {
event.preventDefault();
self.isPressed = true;
var event = event.changedTouches ? event.changedTouches[0] : event;
self.x = event.offsetX;
self.y = event.offsetY;
self.closestPoint = self.world.findClosestPoint(self, 50);
}
this.move = function(event) {
event.preventDefault();
var event = event.changedTouches ? event.changedTouches[0] : event;
self.touches = event.changedTouches;
self.x = event.offsetX;
self.y = event.offsetY;
console.log(event)
}
canvas.addEventListener('mouseup', this.up, false);
canvas.addEventListener('mousedown', this.down, false);
document.addEventListener('mousemove', this.move, false);
};
function createBox(x, y, width, height){
var box = new Body([
new Point(x, y), new Point(x, y + height),
new Point(x + width, y + height), new Point(x + width, y)
]);
return box;
}
var renderer = new CanvasRenderer(document.getElementById('canvas')),
gravity = {
x: 0,
y: 9.8
},
world = new World(gravity, renderer.width, renderer.height),
input = new InputController(renderer.canvas, world);
var x = world.width / 2,
y = 100;
var box = createBox(0, 0, 50, 50);
var box2 = createBox(50, 50, 50, 50);
var triangle = new Body([
new Point(x, y),
new Point(x - 50, y + 100), new Point(x + 50, y + 100)
]);
box2.stiffness = .20 / 3;
box2.color = "yellow";
box.color = "black";
triangle.color = "blue";
var yello2 = createBox(50, 50, 50, 50);
yello2.stiffness = .20 / 3;
yello2.color = "green";
world
.addBody(box)
.addBody(box2)
.addBody(yello2)
.addBody(triangle)
for(var i = 0; i < 20; i++){
var randomBox = createBox(i * 2, i * 5, Math.random()*30,Math.random()*50);
randomBox.stiffness = 1;
world.addBody(randomBox)
}
var point1 = new Point(world.width / 2, 0);
var point2 = new Point(world.width / 2, 50);
var point3 = triangle.points[0]
var chunk1 = new Body([point1, point2]);
var chunk2 = new Body([point2, point3]);
point1.isStatic = true;
chunk1.type = Body.LINE;
chunk2.type = Body.LINE;
world.addBody(chunk1);
world.addBody(chunk2);
(function update() {
renderer.clear();
box.color = "black";
box2.color = "yellow";
triangle.color = "blue";
point1.current.x = world.width / 2;
point1.current.y = 0;
if (input.isPressed && input.closestPoint) {
input.closestPoint.current.x += (input.x - input.closestPoint.current.x) * .1;
input.closestPoint.current.y += (input.y - input.closestPoint.current.y) * .1;
renderer.drawLine(input, input.closestPoint.current);
renderer.drawCircle(input.closestPoint.current, 'red');
}
world.shapeMatch()
var iterations = 5;
while (iterations--) {
world.detectCollisions().shapeMatch().bounds()
}
world.integrate()
world.isStarted && requestAnimationFrame(update);
renderer.render(world);
})()
<canvas id="canvas" width="630" height="490"></canvas>
* { margin: 0; padding:0 }
body {
background: #333;
}
canvas { display: block; border:none; background-color: #888; margin: 0 auto; }