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