@ -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}; |