Last active
May 28, 2016 15:46
-
-
Save arctwelve/83fe55f8dcae64033c71 to your computer and use it in GitHub Desktop.
N-body Strategy Simulator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: gpl-3.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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"; | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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