Skip to content

Instantly share code, notes, and snippets.

@arctwelve
Last active May 28, 2016 15:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arctwelve/83fe55f8dcae64033c71 to your computer and use it in GitHub Desktop.
Save arctwelve/83fe55f8dcae64033c71 to your computer and use it in GitHub Desktop.
N-body Strategy Simulator
license: gpl-3.0
"use strict";
/*
* The abstract base class for all strategy classes.
*/
var AbstractStrategy = function (args) {
if (!(this instanceof AbstractStrategy)) {
throw new TypeError('Cannot call a class as a function');
}
if (this.constructor === AbstractStrategy) {
throw new TypeError('Abstract class "AbstractStrategy" cannot be instantiated directly.');
}
this.gravity = args["gravity"];
this.damping = args["damping"];
this.timeStep = args["timeStep"];
this.dt2 = Math.pow(this.timeStep, 2);
this.bodies = [];
this.numBodies = 0;
}
AbstractStrategy.prototype.addBody = function (b) {
this.bodies.push(b);
this.numBodies = this.bodies.length;
}
AbstractStrategy.prototype.getBodies = function () {
return this.bodies;
}
AbstractStrategy.prototype.getNumBodies = function () {
return this.numBodies;
}
AbstractStrategy.prototype.simulate = function () {
this.accumulateForces();
this.integrate();
}
AbstractStrategy.prototype.integrate = function () {
for (var i = 0; i < this.numBodies; i++) {
this.bodies[i].integrate(this.dt2, this.damping);
}
}
/*
* By default, classes that extend AbstractStrategy inherit this
* method. It's a simplified accumulator in that the distance of
* the bodies isn't used in the force equation -- just gravity and
* the mass of the bodies.
*
* The DistanceForceStrategy.js class shows how you can optionally
* override this method and change its behavior. In
* DistanceForceStrategy.js the method does use the full law of gravity,
* where the distance of the bodies is taken into account, along with
* body mass and gravity.
*
* Another concrete Strategy class could override this method and use a
* completely different technique for accumulating forces. You could
* implement an n-body strategy that used Barnes-Hut to get better
* performance. Or if you needed to toggle the strategies to an 'off'
* state you could override this method and leave the body of it empty.
*/
AbstractStrategy.prototype.accumulateForces = function () {
var force = new Point();
for (var i = 0; i < this.numBodies; i++) {
var pa = this.bodies[i];
for (var j = i + 1; j < this.numBodies; j++) {
var pb = this.bodies[j];
var vect = pb.curr.subtract(pa.curr);
force.angle = vect.angle;
force.length = this.gravity * pa.mass * pb.mass;
pa.addForce(force)
force = force.multiply(-1);
pb.addForce(force);
}
}
}
"use strict";
/*
* Strategy with an arrangment of particles in a carpet-like grid.
*/
var CarpetStrategy = function () {
AbstractStrategy.call(this, {timeStep: 1/100, gravity: 100, damping: 0.999});
var count = 99;
var colWidth = 60;
var rowHeight = 70;
var newRowAtCol = 11;
var origin = this.getCenter(rowHeight, colWidth, newRowAtCol, count);
var rad = 2;
var mass = 1;
var colorA = "red";
var colorB = "orange";
var colCount = 0;
var p = new Point(origin);
for (var i = 0; i < count; i++) {
var color = (i < count / 2) ? colorA : colorB;
this.addBody(new CircleBody(p.x, p.y, rad, mass, color));
p.x += colWidth;
if (colCount++ >= newRowAtCol - 1) {
p.x = origin.x;
p.y += rowHeight;
colCount = 0;
}
}
}
CarpetStrategy.prototype = Object.create(AbstractStrategy.prototype);
CarpetStrategy.prototype.constructor = CarpetStrategy;
/*
* Returns the centerpoint of the carpet grid
*/
CarpetStrategy.prototype.getCenter = function (rowH, colW, newRowAt, numBodies) {
var c = view.center;
var halfW = ((newRowAt - 1) * colW) / 2;
var numRows = Math.ceil(numBodies / newRowAt);
var halfH = ((numRows - 1) * rowH) / 2;
var cx = c.x - halfW;
var cy = c.y - halfH;
return new Point(cx, cy);
}
"use strict";
/*
* Circle shaped body used in the simulator.
*
* Each CircleBody handles its physical simulation through
* a verlet integrator in its integrate(...) method.
*
* The strategy classes decide the initial location, mass and
* color of each CircleBody -- and apply forces on the bodies.
*/
var CircleBody = function (x, y, radius, mass, color) {
this.radius = radius;
this.setMass(mass);
this.curr = new Point(x, y);
this.prev = new Point(x, y);
this.forces = new Point();
this.g = new Path.Circle(this.curr, this.radius);
this.g.fillColor = color;
this.g.strokeColor = color;
this.g.strokeWidth = 1;
}
CircleBody.prototype = {
get velocity() {
return this.curr.subtract(this.prev);
},
set velocity(v) {
this.prev = this.curr.subtract(v);
},
set position(p) {
this.prev = p;
this.curr = p;
}
}
CircleBody.prototype.integrate = function (dt2, damping) {
var temp = this.curr.clone();
var nv = this.velocity.add(this.forces.multiply(dt2));
this.curr = this.curr.add(nv.multiply(damping))
this.prev = temp.clone();
this.forces = new Point();
}
CircleBody.prototype.addForce = function (f) {
this.forces = this.forces.add(f.multiply(this.invMass));
}
CircleBody.prototype.draw = function () {
this.g.position = this.curr;
}
CircleBody.prototype.setMass = function (m) {
if (m === 0) m = 0.0001;
this.mass = m;
this.invMass = 1 / m;
}
"use strict";
/*
* Strategy class that uses the distance of the bodies along with mass
* and a gravity constant in the accumulator
*/
var DistanceForceStrategy = function () {
AbstractStrategy.call(this, {timeStep: 1/5, gravity: 50, damping: 0.998});
var count = 99;
var c = view.center;
this.addBody(new CircleBody(c.x, c.y, 50, 5000, '#0033dd'));
for (var i = 2; i < count + 2; i++) {
var px = c.x + (i * 50) + 100;
var py = c.y - 150;
var body = new CircleBody(px, py, 3, 5, '#00CCFF');
body.addForce(new Point(-900, -200));
this.addBody(body);
}
}
DistanceForceStrategy.prototype = Object.create(AbstractStrategy.prototype);
DistanceForceStrategy.prototype.constructor = DistanceForceStrategy;
/*
* Override the accumulateForces method from AbstractStrategy and use
* the distance of the bodies in the force equation.
*/
DistanceForceStrategy.prototype.accumulateForces = function () {
var force = new Point();
for (var i = 0; i < this.numBodies; i++) {
var pa = this.bodies[i];
for (var j = i + 1; j < this.numBodies; j++) {
var pb = this.bodies[j];
var vect = pb.curr.subtract(pa.curr);
// only apply force if the bodies aren't touching
if (vect.length < pb.radius + pa.radius) continue;
force.angle = vect.angle;
force.length = (this.gravity * pa.mass * pb.mass) / (vect.length * vect.length);
pa.addForce(force)
force = force.multiply(-1);
pb.addForce(force);
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>NBody Simulation with Strategy Pattern</title>
<style>
html,
body {
margin: 0;
overflow: hidden;
height: 100%;
background-color: #333333;
}
canvas {
width: 100%;
height: 100%;
background-color: #222222;
}
.buttonParent {
padding: 10px 0 10px 0;
position: fixed;
left: 0;
bottom: 0;
height: 30px;
width: 100%;
background: #ddd;
text-align: center;
}
.uiButton {
box-shadow: inset 0 1px 0 0 #ffffff;
background: linear-gradient(to bottom, #f9f9f9 5%, #e9e9e9 100%);
background-color: #f9f9f9;
border-radius: 6px;
border: 1px solid #aaa;
display: inline-block;
cursor: pointer;
color: #666666;
font: bold 15px Arial;
padding: 6px 60px;
text-decoration: none;
text-shadow: 0 1px 0 #ffffff;
margin-right: 10px;
}
.uiButton:hover {
background: linear-gradient(to bottom, #e9e9e9 5%, #f9f9f9 100%);
background-color: #e9e9e9;
}
.uiButton:active {
position: relative;
top: 1px;
}
.uiButton:last-child {
margin-right: 0;
}
</style>
</head>
<body onload="new NBodyContext()">
<canvas id="myCanvas"></canvas>
<div class="buttonParent">
<a id="sprBtn" class="uiButton">Spiral</a>
<a id="obtBtn" class="uiButton">Orbit</a>
<a id="cptBtn" class="uiButton">Carpet</a>
<a id="dstBtn" class="uiButton">Distance</a>
<a id="mouBtn" class="uiButton">Mouse</a>
</div>
<!--
For production, you'd want these all as modules, especially so you wouldn't have to list/load
all the strategy.js files here - in order to experiment switching between them in NBodyContext.js
-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.9.25/paper-core.min.js"></script>
<script src="CircleBody.js"></script>
<script src="NBodyContext.js"></script>
<script src="AbstractStrategy.js"></script>
<script src="OrbitStrategy.js"></script>
<script src="CarpetStrategy.js"></script>
<script src="SpiralStrategy.js"></script>
<script src="MouseEventStrategy.js"></script>
<script src="DistanceForceStrategy.js"></script>
<script>
window.onresize = function () {
location.reload();
}
</script>
</body>
</html>
"use strict";
/*
* Strategy that uses mouse events to affect the location and mass of an
* invisible particle.
*/
var MouseEventStrategy = function () {
AbstractStrategy.call(this, {timeStep: 1/25, gravity: 0.1, damping: 0.97});
var count = 99;
var c = view.center;
var dispersal = 300;
for (var i = 0; i < count; i++) {
var px = c.x + (Math.random() - 0.5) * dispersal;
var py = c.y + (Math.random() - 0.5) * dispersal;
this.addBody(new CircleBody(px, py, 3, 5, "yellow"));
}
this.mouseMass = 10000;
this.mouseBody = new CircleBody(c.x, c.y, 0, this.mouseMass, "black");
this.addBody(this.mouseBody);
this.mousePoint = new Point();
this.createMouseEvents();
}
MouseEventStrategy.prototype = Object.create(AbstractStrategy.prototype);
MouseEventStrategy.prototype.constructor = MouseEventStrategy;
/*
* Override the accumulateForces method from AbstractStrategy and track the mouse location.
* Notice that, in contrast to the DistanceForceStategy, we're calling the default base method
* first and just adding a little onto it.
*/
MouseEventStrategy.prototype.accumulateForces = function () {
AbstractStrategy.prototype.accumulateForces.call(this);
this.mouseBody.position = this.mousePoint;
}
/*
* Add mouse events. Note that the context is responsible for cleaning up events each
* time a new strategy is loaded.
*/
MouseEventStrategy.prototype.createMouseEvents = function () {
var $this = this;
view.on('mousedown', function (event) {
$this.mouseBody.setMass($this.mouseMass * -0.5);
document.body.style.cursor = "crosshair";
});
view.on('mousemove', function (event) {
$this.mousePoint = event.point;
});
view.on('mouseup', function (event) {
$this.mouseBody.setMass($this.mouseMass);
document.body.style.cursor = "default";
});
}
"use strict";
/*
* In the Strategy design pattern, the context class holds or manages
* the Strategy classes. The NBodyContext class also acts as the top
* level 'Engine' class for the demo: initializing, applying the selected
* strategy and running the main loop.
*/
var NBodyContext = function () {
this.addStrategy("obtBtn", OrbitStrategy);
this.addStrategy("cptBtn", CarpetStrategy);
this.addStrategy("sprBtn", SpiralStrategy);
this.addStrategy("dstBtn", DistanceForceStrategy);
this.addStrategy("mouBtn", MouseEventStrategy);
this.initCanvas();
this.simStrategy = new SpiralStrategy();
this.run();
}
NBodyContext.prototype.initCanvas = function () {
var canvas = document.getElementById('myCanvas');
paper.setup(canvas);
paper.install(window);
}
NBodyContext.prototype.run = function () {
var $this = this;
view.onFrame = function (event) {
$this.simStrategy.simulate();
$this.draw();
}
}
NBodyContext.prototype.draw = function () {
var s = this.simStrategy;
for (var i = 0; i < s.getNumBodies(); i++) {
s.getBodies()[i].draw();
}
}
/*
* Adds a strategy to the context and attaches it to the passed button element.
*/
NBodyContext.prototype.addStrategy = function (buttonID, strategyClass) {
var $this = this;
var b = document.getElementById(buttonID);
b.onclick = function () {
project.clear();
view.off({mousedown: '', mousemove: '', mouseup: ''});
$this.simStrategy = new strategyClass();
$this.run();
}
}
"use strict";
/*
* Simple strategy of a few 'planet' bodies orbiting a central one
*/
var OrbitStrategy = function () {
AbstractStrategy.call(this, {timeStep: 1/10, gravity: 0.5, damping: 1.0});
var c = view.center
var star = new CircleBody(c.x, c.y, 100, 300, 'orange');
var planetA = new CircleBody(c.x, c.y + 350, 7, 0.1, 'blue');
var planetB = new CircleBody(c.x, c.y - 250, 4, 0.09, 'red');
var planetC = new CircleBody(c.x, c.y - 450, 10, 0.5, 'green');
this.addBody(star);
this.addBody(planetA);
this.addBody(planetB);
this.addBody(planetC);
planetA.addForce(new Point(-200, 0));
planetB.addForce(new Point(-100, 0));
planetC.addForce(new Point(-800, 0));
}
OrbitStrategy.prototype = Object.create(AbstractStrategy.prototype);
OrbitStrategy.prototype.constructor = OrbitStrategy;
"use strict";
/*
* A strategy that creates a spiral configuration of bodies, of increasing
* mass and size.
*/
var SpiralStrategy = function () {
AbstractStrategy.call(this, {timeStep:1/20, gravity:0.1, damping:0.999});
var scale = 20;
var count = 99;
var radCoef = 0.4;
var mssCoef = 0.01;
var colorA = new Color(1.0, 0.0, 1.0, 0.9);
var colorB = new Color(1.0, 0.5, 0.0, 0.9);
var c = view.center.clone();
c.x -= 200;
for (var i = 1; i <= count; i++) {
c.x += Math.sin(i * 0.1) * scale;
c.y += Math.cos(i * 0.1) * (scale += 1);
var rad = i * radCoef + 1;
var mss = i * mssCoef + 1;
var color = (i % 2 == 0) ? colorA : colorB;
this.addBody(new CircleBody(c.x, c.y, rad, mss, color));
}
}
SpiralStrategy.prototype = Object.create(AbstractStrategy.prototype);
SpiralStrategy.prototype.constructor = SpiralStrategy;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment