@ -1,409 +0,0 @@ | |||
/** | |||
* Created by Alex on 2/23/2015. | |||
*/ | |||
function BarnesHutSolver(body, options) { | |||
this.body = body; | |||
this.options = options; | |||
} | |||
/** | |||
* This function calculates the forces the nodes apply on eachother based on a gravitational model. | |||
* The Barnes Hut method is used to speed up this N-body simulation. | |||
* | |||
* @private | |||
*/ | |||
BarnesHutSolver.prototype.solve = function() { | |||
if (this.options.gravitationalConstant != 0) { | |||
var node; | |||
var nodes = this.body.calculationNodes; | |||
var nodeIndices = this.body.calculationNodeIndices; | |||
var nodeCount = nodeIndices.length; | |||
var barnesHutTree = this._formBarnesHutTree(nodes,nodeIndices); | |||
// place the nodes one by one recursively | |||
for (var i = 0; i < nodeCount; i++) { | |||
node = nodes[nodeIndices[i]]; | |||
if (node.options.mass > 0) { | |||
// starting with root is irrelevant, it never passes the BarnesHutSolver condition | |||
this._getForceContribution(barnesHutTree.root.children.NW,node); | |||
this._getForceContribution(barnesHutTree.root.children.NE,node); | |||
this._getForceContribution(barnesHutTree.root.children.SW,node); | |||
this._getForceContribution(barnesHutTree.root.children.SE,node); | |||
} | |||
} | |||
} | |||
}; | |||
/** | |||
* This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. | |||
* If a region contains a single node, we check if it is not itself, then we apply the force. | |||
* | |||
* @param parentBranch | |||
* @param node | |||
* @private | |||
*/ | |||
BarnesHutSolver.prototype._getForceContribution = function(parentBranch,node) { | |||
// we get no force contribution from an empty region | |||
if (parentBranch.childrenCount > 0) { | |||
var dx,dy,distance; | |||
// get the distance from the center of mass to the node. | |||
dx = parentBranch.centerOfMass.x - node.x; | |||
dy = parentBranch.centerOfMass.y - node.y; | |||
distance = Math.sqrt(dx * dx + dy * dy); | |||
// BarnesHutSolver condition | |||
// original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed | |||
// calcSize = 1/s --> d * 1/s > 1/theta = passed | |||
if (distance * parentBranch.calcSize > this.options.thetaInverted) { | |||
// duplicate code to reduce function calls to speed up program | |||
if (distance == 0) { | |||
distance = 0.1*Math.random(); | |||
dx = distance; | |||
} | |||
var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); | |||
var fx = dx * gravityForce; | |||
var fy = dy * gravityForce; | |||
node.fx += fx; | |||
node.fy += fy; | |||
} | |||
else { | |||
// Did not pass the condition, go into children if available | |||
if (parentBranch.childrenCount == 4) { | |||
this._getForceContribution(parentBranch.children.NW,node); | |||
this._getForceContribution(parentBranch.children.NE,node); | |||
this._getForceContribution(parentBranch.children.SW,node); | |||
this._getForceContribution(parentBranch.children.SE,node); | |||
} | |||
else { // parentBranch must have only one node, if it was empty we wouldnt be here | |||
if (parentBranch.children.data.id != node.id) { // if it is not self | |||
// duplicate code to reduce function calls to speed up program | |||
if (distance == 0) { | |||
distance = 0.5*Math.random(); | |||
dx = distance; | |||
} | |||
var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); | |||
var fx = dx * gravityForce; | |||
var fy = dy * gravityForce; | |||
node.fx += fx; | |||
node.fy += fy; | |||
} | |||
} | |||
} | |||
} | |||
}; | |||
/** | |||
* This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. | |||
* | |||
* @param nodes | |||
* @param nodeIndices | |||
* @private | |||
*/ | |||
BarnesHutSolver.prototype._formBarnesHutTree = function(nodes,nodeIndices) { | |||
var node; | |||
var nodeCount = nodeIndices.length; | |||
var minX = Number.MAX_VALUE, | |||
minY = Number.MAX_VALUE, | |||
maxX =-Number.MAX_VALUE, | |||
maxY =-Number.MAX_VALUE; | |||
// get the range of the nodes | |||
for (var i = 0; i < nodeCount; i++) { | |||
var x = nodes[nodeIndices[i]].x; | |||
var y = nodes[nodeIndices[i]].y; | |||
if (nodes[nodeIndices[i]].options.mass > 0) { | |||
if (x < minX) { minX = x; } | |||
if (x > maxX) { maxX = x; } | |||
if (y < minY) { minY = y; } | |||
if (y > maxY) { maxY = y; } | |||
} | |||
} | |||
// make the range a square | |||
var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y | |||
if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize | |||
else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize | |||
var minimumTreeSize = 1e-5; | |||
var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX)); | |||
var halfRootSize = 0.5 * rootSize; | |||
var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY); | |||
// construct the barnesHutTree | |||
var barnesHutTree = { | |||
root:{ | |||
centerOfMass: {x:0, y:0}, | |||
mass:0, | |||
range: { | |||
minX: centerX-halfRootSize,maxX:centerX+halfRootSize, | |||
minY: centerY-halfRootSize,maxY:centerY+halfRootSize | |||
}, | |||
size: rootSize, | |||
calcSize: 1 / rootSize, | |||
children: { data:null}, | |||
maxWidth: 0, | |||
level: 0, | |||
childrenCount: 4 | |||
} | |||
}; | |||
this._splitBranch(barnesHutTree.root); | |||
// place the nodes one by one recursively | |||
for (i = 0; i < nodeCount; i++) { | |||
node = nodes[nodeIndices[i]]; | |||
if (node.options.mass > 0) { | |||
this._placeInTree(barnesHutTree.root,node); | |||
} | |||
} | |||
// make global | |||
return barnesHutTree | |||
}; | |||
/** | |||
* this updates the mass of a branch. this is increased by adding a node. | |||
* | |||
* @param parentBranch | |||
* @param node | |||
* @private | |||
*/ | |||
BarnesHutSolver.prototype._updateBranchMass = function(parentBranch, node) { | |||
var totalMass = parentBranch.mass + node.options.mass; | |||
var totalMassInv = 1/totalMass; | |||
parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; | |||
parentBranch.centerOfMass.x *= totalMassInv; | |||
parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; | |||
parentBranch.centerOfMass.y *= totalMassInv; | |||
parentBranch.mass = totalMass; | |||
var biggestSize = Math.max(Math.max(node.height,node.radius),node.width); | |||
parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth; | |||
}; | |||
/** | |||
* determine in which branch the node will be placed. | |||
* | |||
* @param parentBranch | |||
* @param node | |||
* @param skipMassUpdate | |||
* @private | |||
*/ | |||
BarnesHutSolver.prototype._placeInTree = function(parentBranch,node,skipMassUpdate) { | |||
if (skipMassUpdate != true || skipMassUpdate === undefined) { | |||
// update the mass of the branch. | |||
this._updateBranchMass(parentBranch,node); | |||
} | |||
if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW | |||
if (parentBranch.children.NW.range.maxY > node.y) { // in NW | |||
this._placeInRegion(parentBranch,node,"NW"); | |||
} | |||
else { // in SW | |||
this._placeInRegion(parentBranch,node,"SW"); | |||
} | |||
} | |||
else { // in NE or SE | |||
if (parentBranch.children.NW.range.maxY > node.y) { // in NE | |||
this._placeInRegion(parentBranch,node,"NE"); | |||
} | |||
else { // in SE | |||
this._placeInRegion(parentBranch,node,"SE"); | |||
} | |||
} | |||
}; | |||
/** | |||
* actually place the node in a region (or branch) | |||
* | |||
* @param parentBranch | |||
* @param node | |||
* @param region | |||
* @private | |||
*/ | |||
BarnesHutSolver.prototype._placeInRegion = function(parentBranch,node,region) { | |||
switch (parentBranch.children[region].childrenCount) { | |||
case 0: // place node here | |||
parentBranch.children[region].children.data = node; | |||
parentBranch.children[region].childrenCount = 1; | |||
this._updateBranchMass(parentBranch.children[region],node); | |||
break; | |||
case 1: // convert into children | |||
// if there are two nodes exactly overlapping (on init, on opening of cluster etc.) | |||
// we move one node a pixel and we do not put it in the tree. | |||
if (parentBranch.children[region].children.data.x == node.x && | |||
parentBranch.children[region].children.data.y == node.y) { | |||
node.x += Math.random(); | |||
node.y += Math.random(); | |||
} | |||
else { | |||
this._splitBranch(parentBranch.children[region]); | |||
this._placeInTree(parentBranch.children[region],node); | |||
} | |||
break; | |||
case 4: // place in branch | |||
this._placeInTree(parentBranch.children[region],node); | |||
break; | |||
} | |||
}; | |||
/** | |||
* this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch | |||
* after the split is complete. | |||
* | |||
* @param parentBranch | |||
* @private | |||
*/ | |||
BarnesHutSolver.prototype._splitBranch = function(parentBranch) { | |||
// if the branch is shaded with a node, replace the node in the new subset. | |||
var containedNode = null; | |||
if (parentBranch.childrenCount == 1) { | |||
containedNode = parentBranch.children.data; | |||
parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0; | |||
} | |||
parentBranch.childrenCount = 4; | |||
parentBranch.children.data = null; | |||
this._insertRegion(parentBranch,"NW"); | |||
this._insertRegion(parentBranch,"NE"); | |||
this._insertRegion(parentBranch,"SW"); | |||
this._insertRegion(parentBranch,"SE"); | |||
if (containedNode != null) { | |||
this._placeInTree(parentBranch,containedNode); | |||
} | |||
}; | |||
/** | |||
* This function subdivides the region into four new segments. | |||
* Specifically, this inserts a single new segment. | |||
* It fills the children section of the parentBranch | |||
* | |||
* @param parentBranch | |||
* @param region | |||
* @param parentRange | |||
* @private | |||
*/ | |||
BarnesHutSolver.prototype._insertRegion = function(parentBranch, region) { | |||
var minX,maxX,minY,maxY; | |||
var childSize = 0.5 * parentBranch.size; | |||
switch (region) { | |||
case "NW": | |||
minX = parentBranch.range.minX; | |||
maxX = parentBranch.range.minX + childSize; | |||
minY = parentBranch.range.minY; | |||
maxY = parentBranch.range.minY + childSize; | |||
break; | |||
case "NE": | |||
minX = parentBranch.range.minX + childSize; | |||
maxX = parentBranch.range.maxX; | |||
minY = parentBranch.range.minY; | |||
maxY = parentBranch.range.minY + childSize; | |||
break; | |||
case "SW": | |||
minX = parentBranch.range.minX; | |||
maxX = parentBranch.range.minX + childSize; | |||
minY = parentBranch.range.minY + childSize; | |||
maxY = parentBranch.range.maxY; | |||
break; | |||
case "SE": | |||
minX = parentBranch.range.minX + childSize; | |||
maxX = parentBranch.range.maxX; | |||
minY = parentBranch.range.minY + childSize; | |||
maxY = parentBranch.range.maxY; | |||
break; | |||
} | |||
parentBranch.children[region] = { | |||
centerOfMass:{x:0,y:0}, | |||
mass:0, | |||
range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY}, | |||
size: 0.5 * parentBranch.size, | |||
calcSize: 2 * parentBranch.calcSize, | |||
children: {data:null}, | |||
maxWidth: 0, | |||
level: parentBranch.level+1, | |||
childrenCount: 0 | |||
}; | |||
}; | |||
/** | |||
* This function is for debugging purposed, it draws the tree. | |||
* | |||
* @param ctx | |||
* @param color | |||
* @private | |||
*/ | |||
BarnesHutSolver.prototype._drawTree = function(ctx,color) { | |||
if (this.barnesHutTree !== undefined) { | |||
ctx.lineWidth = 1; | |||
this._drawBranch(this.barnesHutTree.root,ctx,color); | |||
} | |||
}; | |||
/** | |||
* This function is for debugging purposes. It draws the branches recursively. | |||
* | |||
* @param branch | |||
* @param ctx | |||
* @param color | |||
* @private | |||
*/ | |||
BarnesHutSolver.prototype._drawBranch = function(branch,ctx,color) { | |||
if (color === undefined) { | |||
color = "#FF0000"; | |||
} | |||
if (branch.childrenCount == 4) { | |||
this._drawBranch(branch.children.NW,ctx); | |||
this._drawBranch(branch.children.NE,ctx); | |||
this._drawBranch(branch.children.SE,ctx); | |||
this._drawBranch(branch.children.SW,ctx); | |||
} | |||
ctx.strokeStyle = color; | |||
ctx.beginPath(); | |||
ctx.moveTo(branch.range.minX,branch.range.minY); | |||
ctx.lineTo(branch.range.maxX,branch.range.minY); | |||
ctx.stroke(); | |||
ctx.beginPath(); | |||
ctx.moveTo(branch.range.maxX,branch.range.minY); | |||
ctx.lineTo(branch.range.maxX,branch.range.maxY); | |||
ctx.stroke(); | |||
ctx.beginPath(); | |||
ctx.moveTo(branch.range.maxX,branch.range.maxY); | |||
ctx.lineTo(branch.range.minX,branch.range.maxY); | |||
ctx.stroke(); | |||
ctx.beginPath(); | |||
ctx.moveTo(branch.range.minX,branch.range.maxY); | |||
ctx.lineTo(branch.range.minX,branch.range.minY); | |||
ctx.stroke(); | |||
/* | |||
if (branch.mass > 0) { | |||
ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); | |||
ctx.stroke(); | |||
} | |||
*/ | |||
}; | |||
module.exports = BarnesHutSolver; |
@ -1,32 +0,0 @@ | |||
/** | |||
* Created by Alex on 2/23/2015. | |||
*/ | |||
function CentralGravitySolver(body, options) { | |||
this.body = body; | |||
this.options = options; | |||
} | |||
CentralGravitySolver.prototype.solve = function () { | |||
var dx, dy, distance, node, i; | |||
var nodes = this.body.calculationNodes; | |||
var gravity = this.options.centralGravity; | |||
var gravityForce = 0; | |||
for (i = 0; i < this.body.calculationNodeIndices.length; i++) { | |||
node = nodes[this.body.calculationNodeIndices[i]]; | |||
node.damping = this.options.damping; // possibly add function to alter damping properties of clusters. | |||
dx = -node.x; | |||
dy = -node.y; | |||
distance = Math.sqrt(dx * dx + dy * dy); | |||
gravityForce = (distance == 0) ? 0 : (gravity / distance); | |||
node.fx = dx * gravityForce; | |||
node.fy = dy * gravityForce; | |||
} | |||
}; | |||
module.exports = CentralGravitySolver; |
@ -1,101 +0,0 @@ | |||
/** | |||
* Created by Alex on 2/23/2015. | |||
*/ | |||
function SpringSolver(body, options) { | |||
this.body = body; | |||
this.options = options; | |||
} | |||
/** | |||
* this function calculates the effects of the springs in the case of unsmooth curves. | |||
* | |||
* @private | |||
*/ | |||
SpringSolver.prototype._calculateSpringForces = function () { | |||
var edgeLength, edge, edgeId; | |||
var edges = this.edges; | |||
// forces caused by the edges, modelled as springs | |||
for (edgeId in edges) { | |||
if (edges.hasOwnProperty(edgeId)) { | |||
edge = edges[edgeId]; | |||
if (edge.connected === true) { | |||
// only calculate forces if nodes are in the same sector | |||
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { | |||
edgeLength = edge.physics.springLength; | |||
this._calculateSpringForce(edge.from, edge.to, edgeLength); | |||
} | |||
} | |||
} | |||
} | |||
}; | |||
/** | |||
* This function calculates the springforces on the nodes, accounting for the support nodes. | |||
* | |||
* @private | |||
*/ | |||
SpringSolver.prototype._calculateSpringForcesWithSupport = function () { | |||
var edgeLength, edge, edgeId; | |||
var edges = this.edges; | |||
// forces caused by the edges, modelled as springs | |||
for (edgeId in edges) { | |||
if (edges.hasOwnProperty(edgeId)) { | |||
edge = edges[edgeId]; | |||
if (edge.connected === true) { | |||
// only calculate forces if nodes are in the same sector | |||
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { | |||
if (edge.via != null) { | |||
var node1 = edge.to; | |||
var node2 = edge.via; | |||
var node3 = edge.from; | |||
edgeLength = edge.physics.springLength; | |||
this._calculateSpringForce(node1, node2, 0.5 * edgeLength); | |||
this._calculateSpringForce(node2, node3, 0.5 * edgeLength); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
}; | |||
/** | |||
* This is the code actually performing the calculation for the function above. It is split out to avoid repetition. | |||
* | |||
* @param node1 | |||
* @param node2 | |||
* @param edgeLength | |||
* @private | |||
*/ | |||
SpringSolver.prototype._calculateSpringForce = function (node1, node2, edgeLength) { | |||
var dx, dy, fx, fy, springForce, distance; | |||
dx = (node1.x - node2.x); | |||
dy = (node1.y - node2.y); | |||
distance = Math.sqrt(dx * dx + dy * dy); | |||
distance = distance == 0 ? 0.01 : distance; | |||
// the 1/distance is so the fx and fy can be calculated without sine or cosine. | |||
springForce = this.options.springConstant * (edgeLength - distance) / distance; | |||
fx = dx * springForce; | |||
fy = dy * springForce; | |||
node1.fx += fx; | |||
node1.fy += fy; | |||
node2.fx -= fx; | |||
node2.fy -= fy; | |||
}; | |||
module.exports = SpringSolver; |
@ -0,0 +1,434 @@ | |||
/** | |||
* Created by Alex on 2/23/2015. | |||
*/ | |||
class BarnesHutSolver { | |||
constructor(body, options) { | |||
this.body = body; | |||
this.options = options; | |||
} | |||
/** | |||
* This function calculates the forces the nodes apply on eachother based on a gravitational model. | |||
* The Barnes Hut method is used to speed up this N-body simulation. | |||
* | |||
* @private | |||
*/ | |||
solve() { | |||
if (this.options.gravitationalConstant != 0) { | |||
var node; | |||
var nodes = this.body.calculationNodes; | |||
var nodeIndices = this.body.calculationNodeIndices; | |||
var nodeCount = nodeIndices.length; | |||
var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); | |||
// place the nodes one by one recursively | |||
for (var i = 0; i < nodeCount; i++) { | |||
node = nodes[nodeIndices[i]]; | |||
if (node.options.mass > 0) { | |||
// starting with root is irrelevant, it never passes the BarnesHutSolver condition | |||
this._getForceContribution(barnesHutTree.root.children.NW, node); | |||
this._getForceContribution(barnesHutTree.root.children.NE, node); | |||
this._getForceContribution(barnesHutTree.root.children.SW, node); | |||
this._getForceContribution(barnesHutTree.root.children.SE, node); | |||
} | |||
} | |||
} | |||
} | |||
/** | |||
* This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. | |||
* If a region contains a single node, we check if it is not itself, then we apply the force. | |||
* | |||
* @param parentBranch | |||
* @param node | |||
* @private | |||
*/ | |||
_getForceContribution(parentBranch, node) { | |||
// we get no force contribution from an empty region | |||
if (parentBranch.childrenCount > 0) { | |||
var dx, dy, distance; | |||
// get the distance from the center of mass to the node. | |||
dx = parentBranch.centerOfMass.x - node.x; | |||
dy = parentBranch.centerOfMass.y - node.y; | |||
distance = Math.sqrt(dx * dx + dy * dy); | |||
// BarnesHutSolver condition | |||
// original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed | |||
// calcSize = 1/s --> d * 1/s > 1/theta = passed | |||
if (distance * parentBranch.calcSize > this.options.thetaInverted) { | |||
// duplicate code to reduce function calls to speed up program | |||
if (distance == 0) { | |||
distance = 0.1 * Math.random(); | |||
dx = distance; | |||
} | |||
var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); | |||
var fx = dx * gravityForce; | |||
var fy = dy * gravityForce; | |||
node.fx += fx; | |||
node.fy += fy; | |||
} | |||
else { | |||
// Did not pass the condition, go into children if available | |||
if (parentBranch.childrenCount == 4) { | |||
this._getForceContribution(parentBranch.children.NW, node); | |||
this._getForceContribution(parentBranch.children.NE, node); | |||
this._getForceContribution(parentBranch.children.SW, node); | |||
this._getForceContribution(parentBranch.children.SE, node); | |||
} | |||
else { // parentBranch must have only one node, if it was empty we wouldnt be here | |||
if (parentBranch.children.data.id != node.id) { // if it is not self | |||
// duplicate code to reduce function calls to speed up program | |||
if (distance == 0) { | |||
distance = 0.5 * Math.random(); | |||
dx = distance; | |||
} | |||
var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); | |||
var fx = dx * gravityForce; | |||
var fy = dy * gravityForce; | |||
node.fx += fx; | |||
node.fy += fy; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
/** | |||
* This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. | |||
* | |||
* @param nodes | |||
* @param nodeIndices | |||
* @private | |||
*/ | |||
_formBarnesHutTree(nodes, nodeIndices) { | |||
var node; | |||
var nodeCount = nodeIndices.length; | |||
var minX = Number.MAX_VALUE, | |||
minY = Number.MAX_VALUE, | |||
maxX = -Number.MAX_VALUE, | |||
maxY = -Number.MAX_VALUE; | |||
// get the range of the nodes | |||
for (var i = 0; i < nodeCount; i++) { | |||
var x = nodes[nodeIndices[i]].x; | |||
var y = nodes[nodeIndices[i]].y; | |||
if (nodes[nodeIndices[i]].options.mass > 0) { | |||
if (x < minX) { | |||
minX = x; | |||
} | |||
if (x > maxX) { | |||
maxX = x; | |||
} | |||
if (y < minY) { | |||
minY = y; | |||
} | |||
if (y > maxY) { | |||
maxY = y; | |||
} | |||
} | |||
} | |||
// make the range a square | |||
var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y | |||
if (sizeDiff > 0) { | |||
minY -= 0.5 * sizeDiff; | |||
maxY += 0.5 * sizeDiff; | |||
} // xSize > ySize | |||
else { | |||
minX += 0.5 * sizeDiff; | |||
maxX -= 0.5 * sizeDiff; | |||
} // xSize < ySize | |||
var minimumTreeSize = 1e-5; | |||
var rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX)); | |||
var halfRootSize = 0.5 * rootSize; | |||
var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY); | |||
// construct the barnesHutTree | |||
var barnesHutTree = { | |||
root: { | |||
centerOfMass: {x: 0, y: 0}, | |||
mass: 0, | |||
range: { | |||
minX: centerX - halfRootSize, maxX: centerX + halfRootSize, | |||
minY: centerY - halfRootSize, maxY: centerY + halfRootSize | |||
}, | |||
size: rootSize, | |||
calcSize: 1 / rootSize, | |||
children: {data: null}, | |||
maxWidth: 0, | |||
level: 0, | |||
childrenCount: 4 | |||
} | |||
}; | |||
this._splitBranch(barnesHutTree.root); | |||
// place the nodes one by one recursively | |||
for (i = 0; i < nodeCount; i++) { | |||
node = nodes[nodeIndices[i]]; | |||
if (node.options.mass > 0) { | |||
this._placeInTree(barnesHutTree.root, node); | |||
} | |||
} | |||
// make global | |||
return barnesHutTree | |||
} | |||
/** | |||
* this updates the mass of a branch. this is increased by adding a node. | |||
* | |||
* @param parentBranch | |||
* @param node | |||
* @private | |||
*/ | |||
_updateBranchMass(parentBranch, node) { | |||
var totalMass = parentBranch.mass + node.options.mass; | |||
var totalMassInv = 1 / totalMass; | |||
parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; | |||
parentBranch.centerOfMass.x *= totalMassInv; | |||
parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; | |||
parentBranch.centerOfMass.y *= totalMassInv; | |||
parentBranch.mass = totalMass; | |||
var biggestSize = Math.max(Math.max(node.height, node.radius), node.width); | |||
parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth; | |||
} | |||
/** | |||
* determine in which branch the node will be placed. | |||
* | |||
* @param parentBranch | |||
* @param node | |||
* @param skipMassUpdate | |||
* @private | |||
*/ | |||
_placeInTree(parentBranch, node, skipMassUpdate) { | |||
if (skipMassUpdate != true || skipMassUpdate === undefined) { | |||
// update the mass of the branch. | |||
this._updateBranchMass(parentBranch, node); | |||
} | |||
if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW | |||
if (parentBranch.children.NW.range.maxY > node.y) { // in NW | |||
this._placeInRegion(parentBranch, node, "NW"); | |||
} | |||
else { // in SW | |||
this._placeInRegion(parentBranch, node, "SW"); | |||
} | |||
} | |||
else { // in NE or SE | |||
if (parentBranch.children.NW.range.maxY > node.y) { // in NE | |||
this._placeInRegion(parentBranch, node, "NE"); | |||
} | |||
else { // in SE | |||
this._placeInRegion(parentBranch, node, "SE"); | |||
} | |||
} | |||
} | |||
/** | |||
* actually place the node in a region (or branch) | |||
* | |||
* @param parentBranch | |||
* @param node | |||
* @param region | |||
* @private | |||
*/ | |||
_placeInRegion(parentBranch, node, region) { | |||
switch (parentBranch.children[region].childrenCount) { | |||
case 0: // place node here | |||
parentBranch.children[region].children.data = node; | |||
parentBranch.children[region].childrenCount = 1; | |||
this._updateBranchMass(parentBranch.children[region], node); | |||
break; | |||
case 1: // convert into children | |||
// if there are two nodes exactly overlapping (on init, on opening of cluster etc.) | |||
// we move one node a pixel and we do not put it in the tree. | |||
if (parentBranch.children[region].children.data.x == node.x && | |||
parentBranch.children[region].children.data.y == node.y) { | |||
node.x += Math.random(); | |||
node.y += Math.random(); | |||
} | |||
else { | |||
this._splitBranch(parentBranch.children[region]); | |||
this._placeInTree(parentBranch.children[region], node); | |||
} | |||
break; | |||
case 4: // place in branch | |||
this._placeInTree(parentBranch.children[region], node); | |||
break; | |||
} | |||
} | |||
/** | |||
* this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch | |||
* after the split is complete. | |||
* | |||
* @param parentBranch | |||
* @private | |||
*/ | |||
_splitBranch(parentBranch) { | |||
// if the branch is shaded with a node, replace the node in the new subset. | |||
var containedNode = null; | |||
if (parentBranch.childrenCount == 1) { | |||
containedNode = parentBranch.children.data; | |||
parentBranch.mass = 0; | |||
parentBranch.centerOfMass.x = 0; | |||
parentBranch.centerOfMass.y = 0; | |||
} | |||
parentBranch.childrenCount = 4; | |||
parentBranch.children.data = null; | |||
this._insertRegion(parentBranch, "NW"); | |||
this._insertRegion(parentBranch, "NE"); | |||
this._insertRegion(parentBranch, "SW"); | |||
this._insertRegion(parentBranch, "SE"); | |||
if (containedNode != null) { | |||
this._placeInTree(parentBranch, containedNode); | |||
} | |||
} | |||
/** | |||
* This function subdivides the region into four new segments. | |||
* Specifically, this inserts a single new segment. | |||
* It fills the children section of the parentBranch | |||
* | |||
* @param parentBranch | |||
* @param region | |||
* @param parentRange | |||
* @private | |||
*/ | |||
_insertRegion(parentBranch, region) { | |||
var minX, maxX, minY, maxY; | |||
var childSize = 0.5 * parentBranch.size; | |||
switch (region) { | |||
case "NW": | |||
minX = parentBranch.range.minX; | |||
maxX = parentBranch.range.minX + childSize; | |||
minY = parentBranch.range.minY; | |||
maxY = parentBranch.range.minY + childSize; | |||
break; | |||
case "NE": | |||
minX = parentBranch.range.minX + childSize; | |||
maxX = parentBranch.range.maxX; | |||
minY = parentBranch.range.minY; | |||
maxY = parentBranch.range.minY + childSize; | |||
break; | |||
case "SW": | |||
minX = parentBranch.range.minX; | |||
maxX = parentBranch.range.minX + childSize; | |||
minY = parentBranch.range.minY + childSize; | |||
maxY = parentBranch.range.maxY; | |||
break; | |||
case "SE": | |||
minX = parentBranch.range.minX + childSize; | |||
maxX = parentBranch.range.maxX; | |||
minY = parentBranch.range.minY + childSize; | |||
maxY = parentBranch.range.maxY; | |||
break; | |||
} | |||
parentBranch.children[region] = { | |||
centerOfMass: {x: 0, y: 0}, | |||
mass: 0, | |||
range: {minX: minX, maxX: maxX, minY: minY, maxY: maxY}, | |||
size: 0.5 * parentBranch.size, | |||
calcSize: 2 * parentBranch.calcSize, | |||
children: {data: null}, | |||
maxWidth: 0, | |||
level: parentBranch.level + 1, | |||
childrenCount: 0 | |||
}; | |||
} | |||
//--------------------------- DEBUGGING BELOW ---------------------------// | |||
/** | |||
* This function is for debugging purposed, it draws the tree. | |||
* | |||
* @param ctx | |||
* @param color | |||
* @private | |||
*/ | |||
_drawTree(ctx, color) { | |||
if (this.barnesHutTree !== undefined) { | |||
ctx.lineWidth = 1; | |||
this._drawBranch(this.barnesHutTree.root, ctx, color); | |||
} | |||
} | |||
/** | |||
* This function is for debugging purposes. It draws the branches recursively. | |||
* | |||
* @param branch | |||
* @param ctx | |||
* @param color | |||
* @private | |||
*/ | |||
_drawBranch(branch, ctx, color) { | |||
if (color === undefined) { | |||
color = "#FF0000"; | |||
} | |||
if (branch.childrenCount == 4) { | |||
this._drawBranch(branch.children.NW, ctx); | |||
this._drawBranch(branch.children.NE, ctx); | |||
this._drawBranch(branch.children.SE, ctx); | |||
this._drawBranch(branch.children.SW, ctx); | |||
} | |||
ctx.strokeStyle = color; | |||
ctx.beginPath(); | |||
ctx.moveTo(branch.range.minX, branch.range.minY); | |||
ctx.lineTo(branch.range.maxX, branch.range.minY); | |||
ctx.stroke(); | |||
ctx.beginPath(); | |||
ctx.moveTo(branch.range.maxX, branch.range.minY); | |||
ctx.lineTo(branch.range.maxX, branch.range.maxY); | |||
ctx.stroke(); | |||
ctx.beginPath(); | |||
ctx.moveTo(branch.range.maxX, branch.range.maxY); | |||
ctx.lineTo(branch.range.minX, branch.range.maxY); | |||
ctx.stroke(); | |||
ctx.beginPath(); | |||
ctx.moveTo(branch.range.minX, branch.range.maxY); | |||
ctx.lineTo(branch.range.minX, branch.range.minY); | |||
ctx.stroke(); | |||
/* | |||
if (branch.mass > 0) { | |||
ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); | |||
ctx.stroke(); | |||
} | |||
*/ | |||
} | |||
} | |||
export {BarnesHutSolver}; |
@ -0,0 +1,32 @@ | |||
/** | |||
* Created by Alex on 2/23/2015. | |||
*/ | |||
class CentralGravitySolver { | |||
constructor(body, options) { | |||
this.body = body; | |||
this.options = options; | |||
} | |||
solve() { | |||
var dx, dy, distance, node, i; | |||
var nodes = this.body.calculationNodes; | |||
var gravity = this.options.centralGravity; | |||
var gravityForce = 0; | |||
var calculationNodeIndices = this.body.calculationNodeIndices; | |||
for (i = 0; i < calculationNodeIndices.length; i++) { | |||
node = nodes[calculationNodeIndices[i]]; | |||
dx = -node.x; | |||
dy = -node.y; | |||
distance = Math.sqrt(dx * dx + dy * dy); | |||
gravityForce = (distance == 0) ? 0 : (gravity / distance); | |||
node.fx = dx * gravityForce; | |||
node.fy = dy * gravityForce; | |||
} | |||
} | |||
} | |||
export {CentralGravitySolver}; |
@ -0,0 +1,109 @@ | |||
/** | |||
* Created by Alex on 2/23/2015. | |||
*/ | |||
class SpringSolver { | |||
constructor(body, options) { | |||
this.body = body; | |||
this.options = options; | |||
} | |||
solve() { | |||
if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) { | |||
this._calculateSpringForcesWithSupport(); | |||
} | |||
else { | |||
this._calculateSpringForces(); | |||
} | |||
} | |||
/** | |||
* this function calculates the effects of the springs in the case of unsmooth curves. | |||
* | |||
* @private | |||
*/ | |||
_calculateSpringForces() { | |||
var edgeLength, edge, edgeId; | |||
var edges = this.edges; | |||
// forces caused by the edges, modelled as springs | |||
for (edgeId in edges) { | |||
if (edges.hasOwnProperty(edgeId)) { | |||
edge = edges[edgeId]; | |||
if (edge.connected === true) { | |||
// only calculate forces if nodes are in the same sector | |||
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { | |||
edgeLength = edge.physics.springLength; | |||
this._calculateSpringForce(edge.from, edge.to, edgeLength); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
/** | |||
* This function calculates the springforces on the nodes, accounting for the support nodes. | |||
* | |||
* @private | |||
*/ | |||
_calculateSpringForcesWithSupport() { | |||
var edgeLength, edge, edgeId; | |||
var edges = this.edges; | |||
// forces caused by the edges, modelled as springs | |||
for (edgeId in edges) { | |||
if (edges.hasOwnProperty(edgeId)) { | |||
edge = edges[edgeId]; | |||
if (edge.connected === true) { | |||
// only calculate forces if nodes are in the same sector | |||
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { | |||
if (edge.via != null) { | |||
var node1 = edge.to; | |||
var node2 = edge.via; | |||
var node3 = edge.from; | |||
edgeLength = edge.physics.springLength; | |||
this._calculateSpringForce(node1, node2, 0.5 * edgeLength); | |||
this._calculateSpringForce(node2, node3, 0.5 * edgeLength); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
/** | |||
* This is the code actually performing the calculation for the function above. It is split out to avoid repetition. | |||
* | |||
* @param node1 | |||
* @param node2 | |||
* @param edgeLength | |||
* @private | |||
*/ | |||
_calculateSpringForce(node1, node2, edgeLength) { | |||
var dx, dy, fx, fy, springForce, distance; | |||
dx = (node1.x - node2.x); | |||
dy = (node1.y - node2.y); | |||
distance = Math.sqrt(dx * dx + dy * dy); | |||
distance = distance == 0 ? 0.01 : distance; | |||
// the 1/distance is so the fx and fy can be calculated without sine or cosine. | |||
springForce = this.options.springConstant * (edgeLength - distance) / distance; | |||
fx = dx * springForce; | |||
fy = dy * springForce; | |||
node1.fx += fx; | |||
node1.fy += fy; | |||
node2.fx -= fx; | |||
node2.fy -= fy; | |||
} | |||
} | |||
export {SpringSolver}; |