Simple collision sim with surprisingly mesmerizing visual results. Be sure to check out the options.
Note: Runs at max speed.
Controls:
s
to play/paused
to toggle orange dotsl
to toggle orange linesBuilt with blockbuilder.org
xxxxxxxxxx
<html>
<head></head>
<body>
<div id="app"></div>
<script>
(function() {
var options = {
width: 960,
height: 500,
points: 100,
delayedReflection: false,
force: 7, // initial force
startX: .5, // relative horizontal offset
startY: .5, // relative vertical offset
fade: .015,
debugDots: true,
debugLines: true
};
function Simulation(bounds) {
this.points = [];
this.bounds = bounds;
this.addPoint = function(x, y, vx, vy) {
this.points.push({
x: x,
y: y,
vx: vx,
vy: vy
});
};
this.step = function(speed, delayedChange) {
var i = this.points.length, p, m = -1, bx, by;
speed = speed ? speed : 1;
while(i--) {
bx = null; by = null;
p = this.points[i];
p.x += p.vx * speed;
p.y += p.vy * speed;
if(p.x < this.bounds[0]) { bx = this.bounds[0]; }
if(p.x > this.bounds[2]) { bx = this.bounds[2]; }
if(p.y < this.bounds[1]) { by = this.bounds[1]; }
if(p.y > this.bounds[3]) { by = this.bounds[3]; }
if(bx !== null) {
p.vx *= m;
if(!delayedChange) {
p.x -= 2 * (p.x - bx);
}
}
if(by !== null) {
p.vy *= m;
if(!delayedChange) {
p.y -= 2 * (p.y - by);
}
}
}
}
}
function initRadialPoints(sim, count, initialForce, width, height, relOffsetX, relOffsetY) {
var i = -1,
f = Math.PI * 2 / count,
sx = width * relOffsetX,
sy = height * relOffsetY;
while(++i < options.points) {
sim.addPoint(
sx,
sy,
Math.sin(f * i) * initialForce,
Math.cos(f * i) * initialForce
);
}
}
var status = true;
var debugDots = options.debugDots;
var debugLines = options.debugLines;
var app = document.getElementById('app');
var ctx = {
live: document.createElement('canvas').getContext('2d'),
lines: document.createElement('canvas').getContext('2d'),
debug: document.createElement('canvas').getContext('2d'),
trigger: document.createElement('canvas').getContext('2d')
};
ctx.live.canvas.width = ctx.lines.canvas.width = ctx.debug.canvas.width = options.width;
ctx.live.canvas.height = ctx.lines.canvas.height = ctx.debug.canvas.height = options.height;
app.appendChild(ctx.live.canvas);
ctx.lines.strokeStyle = 'rgba(0,0,0,.2)';
ctx.lines.fillStyle = 'rgba(255,255,255,' + options.fade + ')';
ctx.lines.lineWidth = 1;
ctx.debug.fillStyle = ctx.debug.strokeStyle = '#f50';
ctx.debug.lineWidth = 2;
var sim = new Simulation([0, 0, options.width - 1, options.height - 1]);
initRadialPoints(sim, options.points, options.force, options.width, options.height, options.startX, options.startY);
function clear(ctx, clearColor) {
if(!clearColor) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
return;
}
var fs = clearColor !== ctx.fillStyle ? ctx.fillStyle : null;
fs && (ctx.fillStyle = clearColor);
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
fs && (ctx.fillStyle = fs);
}
function drawLines(ctx, points, scale) {
ctx.beginPath();
scale = scale || 1;
ctx.moveTo(points[0].x * scale, points[0].y * scale);
for(var i = 1; i < points.length; i++) {
ctx.lineTo(points[i].x * scale, points[i].y * scale);
}
ctx.closePath();
ctx.stroke();
}
function drawDots(ctx, points, radius) {
for(var i = 0; i < points.length; i++) {
ctx.beginPath();
ctx.arc(points[i].x, points[i].y, radius, 0, Math.PI * 2);
ctx.fill();
}
}
function play() {
if(status) {
sim.step(null, options.delayedReflection);
clear(ctx.live);
clear(ctx.lines, ctx.lines.fillStyle);
drawLines(ctx.lines, sim.points, true);
ctx.live.drawImage(ctx.lines.canvas, 0, 0);
(debugDots || debugLines) && clear(ctx.debug);
debugLines && drawLines(ctx.debug, sim.points);
debugDots && drawDots(ctx.debug, sim.points, 3);
(debugDots || debugLines) && ctx.live.drawImage(ctx.debug.canvas, 0, 0);
}
requestAnimationFrame(play);
}
document.addEventListener('keydown', function(e) {
if(e.ctrlKey || e.metaKey || e.altKey) {
return;
}
switch(e.key) {
case 's': status = !status; break;
case 'd': debugDots = !debugDots; break;
case 'l': debugLines = !debugLines; break;
}
});
play();
}());
</script>
</body>
</html>