├── .travis.yml ├── lib ├── createBody.js ├── spring.js ├── dragForce.js ├── eulerIntegrator.js ├── springForce.js └── bounds.js ├── .gitignore ├── package.json ├── test ├── dragForce.js ├── eulerIntegrator.js ├── springForce.js └── simulator.js ├── LICENSE ├── README.md └── index.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | -------------------------------------------------------------------------------- /lib/createBody.js: -------------------------------------------------------------------------------- 1 | var physics = require('ngraph.physics.primitives'); 2 | 3 | module.exports = function(pos) { 4 | return new physics.Body(pos); 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | reports 14 | .nyc_output 15 | 16 | npm-debug.log 17 | node_modules 18 | -------------------------------------------------------------------------------- /lib/spring.js: -------------------------------------------------------------------------------- 1 | module.exports = Spring; 2 | 3 | /** 4 | * Represents a physical spring. Spring connects two bodies, has rest length 5 | * stiffness coefficient and optional weight 6 | */ 7 | function Spring(fromBody, toBody, length, coeff) { 8 | this.from = fromBody; 9 | this.to = toBody; 10 | this.length = length; 11 | this.coeff = coeff; 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngraph.physics.simulator", 3 | "version": "2.0.0", 4 | "description": "Physics library for ngraph", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tap test/*.js" 8 | }, 9 | "keywords": [ 10 | "ngraph" 11 | ], 12 | "author": "Andrei Kashcha", 13 | "license": "MIT", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/anvaka/ngraph.physics.simulator" 17 | }, 18 | "dependencies": { 19 | "ngraph.events": "^1.0.0", 20 | "ngraph.expose": "^1.0.0", 21 | "ngraph.merge": "^1.0.0", 22 | "ngraph.physics.primitives": "^1.0.0", 23 | "ngraph.quadtreebh": "^1.0.0", 24 | "ngraph.random": "^1.0.0" 25 | }, 26 | "devDependencies": { 27 | "tap": "^14.2.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/dragForce.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents drag force, which reduces force value on each step by given 3 | * coefficient. 4 | * 5 | * @param {Object} options for the drag force 6 | * @param {Number=} options.dragCoeff drag force coefficient. 0.1 by default 7 | */ 8 | module.exports = function (options) { 9 | var merge = require('ngraph.merge'), 10 | expose = require('ngraph.expose'); 11 | 12 | options = merge(options, { 13 | dragCoeff: 0.02 14 | }); 15 | 16 | var api = { 17 | update : function (body) { 18 | body.force.x -= options.dragCoeff * body.velocity.x; 19 | body.force.y -= options.dragCoeff * body.velocity.y; 20 | } 21 | }; 22 | 23 | // let easy access to dragCoeff: 24 | expose(options, api, ['dragCoeff']); 25 | 26 | return api; 27 | }; 28 | -------------------------------------------------------------------------------- /test/dragForce.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test, 2 | createDragForce = require('../lib/dragForce'), 3 | physics = require('ngraph.physics.primitives'); 4 | 5 | test('reduces force value', function (t) { 6 | var body = new physics.Body(); 7 | body.force.x = 1; body.force.y = 1; 8 | body.velocity.x = 1; body.velocity.y = 1; 9 | 10 | var dragForce = createDragForce({ dragCoeff: 0.1 }); 11 | dragForce.update(body); 12 | 13 | t.ok(body.force.x < 1 && body.force.y < 1, 'Force value is reduced'); 14 | t.end(); 15 | }); 16 | 17 | test('Initialized with default value', function (t) { 18 | var dragForce = createDragForce(); 19 | var dragCoeff = dragForce.dragCoeff(); 20 | t.ok(typeof dragCoeff === 'number', 'Default value is present'); 21 | 22 | t.end(); 23 | }); 24 | 25 | test('Can update default value', function (t) { 26 | var dragForce = createDragForce(); 27 | var returnedForce = dragForce.dragCoeff(0.0); 28 | 29 | t.ok(dragForce === returnedForce, 'Allows chaining'); 30 | 31 | var dragCoeff = dragForce.dragCoeff(); 32 | t.ok(dragCoeff === 0.0, 'Default value is updated'); 33 | t.end(); 34 | }); 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2019 Andrei Kashcha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/eulerIntegrator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Performs forces integration, using given time step. Uses Euler method to solve 3 | * differential equation (http://en.wikipedia.org/wiki/Euler_method ). 4 | * 5 | * @returns {Number} squared distance of total position updates. 6 | */ 7 | 8 | module.exports = integrate; 9 | 10 | function integrate(bodies, timeStep) { 11 | var dx = 0, tx = 0, 12 | dy = 0, ty = 0, 13 | i, 14 | max = bodies.length; 15 | 16 | if (max === 0) { 17 | return 0; 18 | } 19 | 20 | for (i = 0; i < max; ++i) { 21 | var body = bodies[i], 22 | coeff = timeStep / body.mass; 23 | 24 | body.velocity.x += coeff * body.force.x; 25 | body.velocity.y += coeff * body.force.y; 26 | var vx = body.velocity.x, 27 | vy = body.velocity.y, 28 | v = Math.sqrt(vx * vx + vy * vy); 29 | 30 | if (v > 1) { 31 | // We normalize it so that we move within timeStep range. 32 | // for the case when v <= 1 - we let velocity to fade out. 33 | body.velocity.x = vx / v; 34 | body.velocity.y = vy / v; 35 | } 36 | 37 | dx = timeStep * body.velocity.x; 38 | dy = timeStep * body.velocity.y; 39 | 40 | body.pos.x += dx; 41 | body.pos.y += dy; 42 | 43 | tx += Math.abs(dx); ty += Math.abs(dy); 44 | } 45 | 46 | return (tx * tx + ty * ty)/max; 47 | } 48 | -------------------------------------------------------------------------------- /lib/springForce.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents spring force, which updates forces acting on two bodies, connected 3 | * by a spring. 4 | * 5 | * @param {Object} options for the spring force 6 | * @param {Number=} options.springCoeff spring force coefficient. 7 | * @param {Number=} options.springLength desired length of a spring at rest. 8 | */ 9 | module.exports = function (options) { 10 | var merge = require('ngraph.merge'); 11 | var random = require('ngraph.random').random(42); 12 | var expose = require('ngraph.expose'); 13 | 14 | options = merge(options, { 15 | springCoeff: 0.0002, 16 | springLength: 80 17 | }); 18 | 19 | var api = { 20 | /** 21 | * Upsates forces acting on a spring 22 | */ 23 | update : function (spring) { 24 | var body1 = spring.from, 25 | body2 = spring.to, 26 | length = spring.length < 0 ? options.springLength : spring.length, 27 | dx = body2.pos.x - body1.pos.x, 28 | dy = body2.pos.y - body1.pos.y, 29 | r = Math.sqrt(dx * dx + dy * dy); 30 | 31 | if (r === 0) { 32 | dx = (random.nextDouble() - 0.5) / 50; 33 | dy = (random.nextDouble() - 0.5) / 50; 34 | r = Math.sqrt(dx * dx + dy * dy); 35 | } 36 | 37 | var d = r - length; 38 | var coeff = ((!spring.coeff || spring.coeff < 0) ? options.springCoeff : spring.coeff) * d / r; 39 | 40 | body1.force.x += coeff * dx; 41 | body1.force.y += coeff * dy; 42 | 43 | body2.force.x -= coeff * dx; 44 | body2.force.y -= coeff * dy; 45 | } 46 | }; 47 | 48 | expose(options, api, ['springCoeff', 'springLength']); 49 | return api; 50 | } 51 | -------------------------------------------------------------------------------- /test/eulerIntegrator.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test, 2 | integrate = require('../lib/eulerIntegrator'), 3 | physics = require('ngraph.physics.primitives'); 4 | 5 | test('Body preserves velocity without forces', function (t) { 6 | var body = new physics.Body(); 7 | var timeStep = 1; 8 | body.mass = 1; body.velocity.x = 1; 9 | 10 | integrate([body], timeStep); 11 | t.equal(body.pos.x, 1, 'Should move by 1 pixel on first iteration'); 12 | 13 | timeStep = 2; // let's increase time step: 14 | integrate([body], timeStep); 15 | t.equal(body.pos.x, 3, 'Should move by 2 pixel on second iteration'); 16 | t.end(); 17 | }); 18 | 19 | test('Body gains velocity under force', function (t) { 20 | var body = new physics.Body(); 21 | var timeStep = 1; 22 | body.mass = 1; body.force.x = 0.1; 23 | 24 | // F = m * a; 25 | // since mass = 1 => F = a = y'; 26 | integrate([body], timeStep); 27 | t.equal(body.velocity.x, 0.1, 'Should increase velocity'); 28 | 29 | integrate([body], timeStep); 30 | t.equal(body.velocity.x, 0.2, 'Should increase velocity'); 31 | // floating point math: 32 | t.ok(0.29 < body.pos.x && body.pos.x < 0.31, 'Position should be at 0.3 now'); 33 | 34 | t.end(); 35 | }); 36 | 37 | test('No bodies yield 0 movement', function (t) { 38 | var movement = integrate([], 2); 39 | t.equals(movement, 0, 'Nothing has moved'); 40 | t.end(); 41 | }); 42 | 43 | test('Body does not move faster than 1px', function (t) { 44 | var body = new physics.Body(); 45 | var timeStep = 1; 46 | body.mass = 1; body.force.x = 2; 47 | 48 | integrate([body], timeStep); 49 | t.ok(body.velocity.x <= 1, 'Velocity should be withint speed limit'); 50 | 51 | integrate([body], timeStep); 52 | t.ok(body.velocity.x <= 1, 'Velocity should be withint speed limit'); 53 | 54 | t.end(); 55 | }); 56 | 57 | test('Can get total system movement', function (t) { 58 | var body = new physics.Body(); 59 | var timeStep = 1; 60 | body.mass = 1; body.velocity.x = 0.2; 61 | 62 | var movement = integrate([body], timeStep); 63 | // to improve performance, integrator does not take square root, thus 64 | // total movement is .2 * .2 = 0.04; 65 | t.ok(0.04 <= movement && movement <= 0.041, 'System should travel by 0.2 pixels'); 66 | t.end(); 67 | }); 68 | -------------------------------------------------------------------------------- /lib/bounds.js: -------------------------------------------------------------------------------- 1 | module.exports = function (bodies, settings) { 2 | var random = require('ngraph.random').random(42); 3 | var boundingBox = { x1: 0, y1: 0, x2: 0, y2: 0 }; 4 | 5 | return { 6 | box: boundingBox, 7 | 8 | update: updateBoundingBox, 9 | 10 | reset : function () { 11 | boundingBox.x1 = boundingBox.y1 = 0; 12 | boundingBox.x2 = boundingBox.y2 = 0; 13 | }, 14 | 15 | getBestNewPosition: function (neighbors) { 16 | var graphRect = boundingBox; 17 | 18 | var baseX = 0, baseY = 0; 19 | 20 | if (neighbors.length) { 21 | for (var i = 0; i < neighbors.length; ++i) { 22 | baseX += neighbors[i].pos.x; 23 | baseY += neighbors[i].pos.y; 24 | } 25 | 26 | baseX /= neighbors.length; 27 | baseY /= neighbors.length; 28 | } else { 29 | baseX = (graphRect.x1 + graphRect.x2) / 2; 30 | baseY = (graphRect.y1 + graphRect.y2) / 2; 31 | } 32 | 33 | var springLength = settings.springLength; 34 | return { 35 | x: baseX + random.next(springLength) - springLength / 2, 36 | y: baseY + random.next(springLength) - springLength / 2 37 | }; 38 | } 39 | }; 40 | 41 | function updateBoundingBox() { 42 | var i = bodies.length; 43 | if (i === 0) { return; } // don't have to wory here. 44 | 45 | var x1 = Number.MAX_VALUE, 46 | y1 = Number.MAX_VALUE, 47 | x2 = Number.MIN_VALUE, 48 | y2 = Number.MIN_VALUE; 49 | 50 | while(i--) { 51 | // this is O(n), could it be done faster with quadtree? 52 | // how about pinned nodes? 53 | var body = bodies[i]; 54 | if (body.isPinned) { 55 | body.pos.x = body.prevPos.x; 56 | body.pos.y = body.prevPos.y; 57 | } else { 58 | body.prevPos.x = body.pos.x; 59 | body.prevPos.y = body.pos.y; 60 | } 61 | if (body.pos.x < x1) { 62 | x1 = body.pos.x; 63 | } 64 | if (body.pos.x > x2) { 65 | x2 = body.pos.x; 66 | } 67 | if (body.pos.y < y1) { 68 | y1 = body.pos.y; 69 | } 70 | if (body.pos.y > y2) { 71 | y2 = body.pos.y; 72 | } 73 | } 74 | 75 | boundingBox.x1 = x1; 76 | boundingBox.x2 = x2; 77 | boundingBox.y1 = y1; 78 | boundingBox.y2 = y2; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Physics for ngraph 2 | 3 | This was a physics module for [ngraph.forcelayout](https://github.com/anvaka/ngraph.forcelayout). 4 | 5 | With time this module was merged into `ngraph.forcelayout` and this package is no 6 | longer used. 7 | 8 | Below is the rest of the readme file, kept for historical reasons. 9 | 10 | ## Old readme 11 | 12 | The module's primary focus is to serve force based graph layout, thus it manages a naïve system of bodies and springs. 13 | 14 | Simulator calculates forces acting on each body and then deduces their position via Newton's law. There are three major forces in the system: 15 | 16 | 1. Spring force keeps connected nodes together via [Hooke's law](http://en.wikipedia.org/wiki/Hooke's_law) 17 | 2. Each body repels each other via [Coulomb's law](http://en.wikipedia.org/wiki/Coulomb's_law) 18 | 3. To guarantee we get to "stable" state the system has a drag force which slows 19 | entire simulation down. 20 | 21 | Body forces are calculated in `n*lg(n)` time with help of Barnes-Hut algorithm implemented in [quadtree module](https://github.com/anvaka/ngraph.quadtreebh). [Euler method](http://en.wikipedia.org/wiki/Euler_method) is then used to solve ordinary differential equation of Newton's law and get position of bodies. 22 | 23 | [![build status](https://secure.travis-ci.org/anvaka/ngraph.physics.simulator.png)](http://travis-ci.org/anvaka/ngraph.physics.simulator) 24 | 25 | # quickstart 26 | 27 | ``` js 28 | var physics = require('ngraph.physics.primitives'); 29 | var body1 = new physics.Body(0, 0); 30 | var body2 = new physics.Body(1, 0); 31 | 32 | var createSimulator = require('ngraph.physics.simulator'); 33 | var simulator = createSimulator(); 34 | simulator.addBody(body1); 35 | simulator.addBody(body2); 36 | 37 | simulator.step(); 38 | ``` 39 | 40 | This will move apart two bodies. 41 | 42 | For more advanced use cases, please look inside `index.js`, which includes documentation for public API and describes engine configuration properties. 43 | 44 | # install 45 | 46 | With [npm](https://npmjs.org) do: 47 | 48 | ``` 49 | npm install ngraph.physics.simulator 50 | ``` 51 | 52 | # todo 53 | 54 | I spent countless hours trying to optimize performance of this module but it's not perfect. Ideally I'd love to use native arrays to simulate physics. Eventually this will allow to calculate forces on video card or via webworkers. 55 | 56 | # license 57 | 58 | MIT 59 | -------------------------------------------------------------------------------- /test/springForce.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test, 2 | createSpringForce = require('../lib/springForce'), 3 | Body = require('ngraph.physics.primitives').Body, 4 | Spring = require('../lib/spring'); 5 | 6 | 7 | test('Initialized with default value', function (t) { 8 | var springForce = createSpringForce(); 9 | var springCoeff = springForce.springCoeff(); 10 | var springLength = springForce.springLength(); 11 | 12 | t.ok(typeof springCoeff === 'number' && typeof springLength === 'number', 'Default values are present'); 13 | 14 | t.end(); 15 | }); 16 | 17 | 18 | test('Should bump bodies at same position', function (t) { 19 | var body1 = new Body(0, 0); 20 | var body2 = new Body(0, 0); 21 | // length between two bodies is 2, while ideal length is 1. Each body 22 | // should start moving towards each other after force update 23 | var idealLength = 1; 24 | var spring = new Spring(body1, body2, idealLength); 25 | var springForce = createSpringForce(); 26 | springForce.update(spring); 27 | 28 | t.ok(body1.force.x > 0, 'Body 1 should go right'); 29 | t.ok(body2.force.x < 0, 'Body 2 should go left'); 30 | t.end(); 31 | }); 32 | 33 | test('Check spring force direction', function (t) { 34 | var springForce = createSpringForce(); 35 | 36 | t.test('Should contract two bodies when ideal length is smaler than actual', function (t) { 37 | var body1 = new Body(-1, 0); 38 | var body2 = new Body(+1, 0); 39 | // length between two bodies is 2, while ideal length is 1. Each body 40 | // should start moving towards each other after force update 41 | var idealLength = 1; 42 | var spring = new Spring(body1, body2, idealLength); 43 | springForce.update(spring); 44 | 45 | t.ok(body1.force.x > 0, 'Body 1 should go right'); 46 | t.ok(body2.force.x < 0, 'Body 2 should go left'); 47 | t.end(); 48 | }); 49 | 50 | t.test('Should repel two bodies when ideal length is larger than actual', function (t) { 51 | var body1 = new Body(-1, 0); 52 | var body2 = new Body(+1, 0); 53 | // length between two bodies is 2, while ideal length is 1. Each body 54 | // should start moving towards each other after force update 55 | var idealLength = 3; 56 | var spring = new Spring(body1, body2, idealLength); 57 | springForce.update(spring); 58 | 59 | t.ok(body1.force.x < 0, 'Body 1 should go left'); 60 | t.ok(body2.force.x > 0, 'Body 2 should go right'); 61 | t.end(); 62 | }); 63 | 64 | t.end(); 65 | }); 66 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Manages a simulation of physical forces acting on bodies and springs. 3 | */ 4 | module.exports = physicsSimulator; 5 | 6 | function physicsSimulator(settings) { 7 | var Spring = require('./lib/spring'); 8 | var expose = require('ngraph.expose'); 9 | var merge = require('ngraph.merge'); 10 | var eventify = require('ngraph.events'); 11 | 12 | settings = merge(settings, { 13 | /** 14 | * Ideal length for links (springs in physical model). 15 | */ 16 | springLength: 30, 17 | 18 | /** 19 | * Hook's law coefficient. 1 - solid spring. 20 | */ 21 | springCoeff: 0.0008, 22 | 23 | /** 24 | * Coulomb's law coefficient. It's used to repel nodes thus should be negative 25 | * if you make it positive nodes start attract each other :). 26 | */ 27 | gravity: -1.2, 28 | 29 | /** 30 | * Theta coefficient from Barnes Hut simulation. Ranged between (0, 1). 31 | * The closer it's to 1 the more nodes algorithm will have to go through. 32 | * Setting it to one makes Barnes Hut simulation no different from 33 | * brute-force forces calculation (each node is considered). 34 | */ 35 | theta: 0.8, 36 | 37 | /** 38 | * Drag force coefficient. Used to slow down system, thus should be less than 1. 39 | * The closer it is to 0 the less tight system will be. 40 | */ 41 | dragCoeff: 0.02, 42 | 43 | /** 44 | * Default time step (dt) for forces integration 45 | */ 46 | timeStep : 20, 47 | }); 48 | 49 | // We allow clients to override basic factory methods: 50 | var createQuadTree = settings.createQuadTree || require('ngraph.quadtreebh'); 51 | var createBounds = settings.createBounds || require('./lib/bounds'); 52 | var createDragForce = settings.createDragForce || require('./lib/dragForce'); 53 | var createSpringForce = settings.createSpringForce || require('./lib/springForce'); 54 | var integrate = settings.integrator || require('./lib/eulerIntegrator'); 55 | var createBody = settings.createBody || require('./lib/createBody'); 56 | 57 | var bodies = [], // Bodies in this simulation. 58 | springs = [], // Springs in this simulation. 59 | quadTree = createQuadTree(settings), 60 | bounds = createBounds(bodies, settings), 61 | springForce = createSpringForce(settings), 62 | dragForce = createDragForce(settings); 63 | 64 | var bboxNeedsUpdate = true; 65 | var totalMovement = 0; // how much movement we made on last step 66 | 67 | var publicApi = { 68 | /** 69 | * Array of bodies, registered with current simulator 70 | * 71 | * Note: To add new body, use addBody() method. This property is only 72 | * exposed for testing/performance purposes. 73 | */ 74 | bodies: bodies, 75 | 76 | quadTree: quadTree, 77 | 78 | /** 79 | * Array of springs, registered with current simulator 80 | * 81 | * Note: To add new spring, use addSpring() method. This property is only 82 | * exposed for testing/performance purposes. 83 | */ 84 | springs: springs, 85 | 86 | /** 87 | * Returns settings with which current simulator was initialized 88 | */ 89 | settings: settings, 90 | 91 | /** 92 | * Performs one step of force simulation. 93 | * 94 | * @returns {boolean} true if system is considered stable; False otherwise. 95 | */ 96 | step: function () { 97 | accumulateForces(); 98 | 99 | var movement = integrate(bodies, settings.timeStep); 100 | bounds.update(); 101 | 102 | return movement; 103 | }, 104 | 105 | /** 106 | * Adds body to the system 107 | * 108 | * @param {ngraph.physics.primitives.Body} body physical body 109 | * 110 | * @returns {ngraph.physics.primitives.Body} added body 111 | */ 112 | addBody: function (body) { 113 | if (!body) { 114 | throw new Error('Body is required'); 115 | } 116 | bodies.push(body); 117 | 118 | return body; 119 | }, 120 | 121 | /** 122 | * Adds body to the system at given position 123 | * 124 | * @param {Object} pos position of a body 125 | * 126 | * @returns {ngraph.physics.primitives.Body} added body 127 | */ 128 | addBodyAt: function (pos) { 129 | if (!pos) { 130 | throw new Error('Body position is required'); 131 | } 132 | var body = createBody(pos); 133 | bodies.push(body); 134 | 135 | return body; 136 | }, 137 | 138 | /** 139 | * Removes body from the system 140 | * 141 | * @param {ngraph.physics.primitives.Body} body to remove 142 | * 143 | * @returns {Boolean} true if body found and removed. falsy otherwise; 144 | */ 145 | removeBody: function (body) { 146 | if (!body) { return; } 147 | 148 | var idx = bodies.indexOf(body); 149 | if (idx < 0) { return; } 150 | 151 | bodies.splice(idx, 1); 152 | if (bodies.length === 0) { 153 | bounds.reset(); 154 | } 155 | return true; 156 | }, 157 | 158 | /** 159 | * Adds a spring to this simulation. 160 | * 161 | * @returns {Object} - a handle for a spring. If you want to later remove 162 | * spring pass it to removeSpring() method. 163 | */ 164 | addSpring: function (body1, body2, springLength, springCoefficient) { 165 | if (!body1 || !body2) { 166 | throw new Error('Cannot add null spring to force simulator'); 167 | } 168 | 169 | if (typeof springLength !== 'number') { 170 | springLength = -1; // assume global configuration 171 | } 172 | 173 | var spring = new Spring(body1, body2, springLength, springCoefficient >= 0 ? springCoefficient : -1); 174 | springs.push(spring); 175 | 176 | // TODO: could mark simulator as dirty. 177 | return spring; 178 | }, 179 | 180 | /** 181 | * Returns amount of movement performed on last step() call 182 | */ 183 | getTotalMovement: function () { 184 | return totalMovement; 185 | }, 186 | 187 | /** 188 | * Removes spring from the system 189 | * 190 | * @param {Object} spring to remove. Spring is an object returned by addSpring 191 | * 192 | * @returns {Boolean} true if spring found and removed. falsy otherwise; 193 | */ 194 | removeSpring: function (spring) { 195 | if (!spring) { return; } 196 | var idx = springs.indexOf(spring); 197 | if (idx > -1) { 198 | springs.splice(idx, 1); 199 | return true; 200 | } 201 | }, 202 | 203 | getBestNewBodyPosition: function (neighbors) { 204 | return bounds.getBestNewPosition(neighbors); 205 | }, 206 | 207 | /** 208 | * Returns bounding box which covers all bodies 209 | */ 210 | getBBox: function () { 211 | if (bboxNeedsUpdate) { 212 | bounds.update(); 213 | bboxNeedsUpdate = false; 214 | } 215 | return bounds.box; 216 | }, 217 | 218 | invalidateBBox: function () { 219 | bboxNeedsUpdate = true; 220 | }, 221 | 222 | gravity: function (value) { 223 | if (value !== undefined) { 224 | settings.gravity = value; 225 | quadTree.options({gravity: value}); 226 | return this; 227 | } else { 228 | return settings.gravity; 229 | } 230 | }, 231 | 232 | theta: function (value) { 233 | if (value !== undefined) { 234 | settings.theta = value; 235 | quadTree.options({theta: value}); 236 | return this; 237 | } else { 238 | return settings.theta; 239 | } 240 | } 241 | }; 242 | 243 | // allow settings modification via public API: 244 | expose(settings, publicApi); 245 | 246 | eventify(publicApi); 247 | 248 | return publicApi; 249 | 250 | function accumulateForces() { 251 | // Accumulate forces acting on bodies. 252 | var body, 253 | i = bodies.length; 254 | 255 | if (i) { 256 | // only add bodies if there the array is not empty: 257 | quadTree.insertBodies(bodies); // performance: O(n * log n) 258 | while (i--) { 259 | body = bodies[i]; 260 | // If body is pinned there is no point updating its forces - it should 261 | // never move: 262 | if (!body.isPinned) { 263 | body.force.reset(); 264 | 265 | quadTree.updateBodyForce(body); 266 | dragForce.update(body); 267 | } 268 | } 269 | } 270 | 271 | i = springs.length; 272 | while(i--) { 273 | springForce.update(springs[i]); 274 | } 275 | } 276 | }; 277 | -------------------------------------------------------------------------------- /test/simulator.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test, 2 | createSimulator = require('..'), 3 | physics = require('ngraph.physics.primitives'); 4 | 5 | test('Can step without bodies', function (t) { 6 | var simulator = createSimulator(); 7 | t.equals(simulator.bodies.length, 0, 'There should be no bodies'); 8 | t.equals(simulator.springs.length, 0, 'There should be no springs'); 9 | simulator.step(); 10 | t.end(); 11 | }); 12 | 13 | test('it has settings exposed', function(t) { 14 | var mySettings = { }; 15 | var simulator = createSimulator(mySettings); 16 | t.ok(mySettings === simulator.settings, 'settings are exposed'); 17 | t.end(); 18 | }); 19 | 20 | test('it gives amount of total movement', function(t) { 21 | var simulator = createSimulator(); 22 | var body1 = new physics.Body(-10, 0); 23 | var body2 = new physics.Body(10, 0); 24 | simulator.addBody(body1); 25 | simulator.addBody(body2); 26 | simulator.step(); 27 | 28 | var totalMoved = simulator.getTotalMovement(); 29 | t.ok(!isNaN(totalMoved), 'Amount of total movement is returned'); 30 | t.end(); 31 | }); 32 | 33 | test('it can add a body at given position', function(t) { 34 | var simulator = createSimulator(); 35 | var pos1 = {x: -10, y: 0}; 36 | var pos2 = {x: 10, y: 0}; 37 | simulator.addBodyAt(pos1); 38 | simulator.addBodyAt(pos2); 39 | 40 | t.equals(simulator.bodies.length, 2, 'All bodies are added'); 41 | var body1 = simulator.bodies[0]; 42 | 43 | t.equals(body1.pos.x, -10, 'X is there'); 44 | t.equals(body1.pos.y, 0, 'Y is there'); 45 | 46 | var body2 = simulator.bodies[1]; 47 | t.equals(body2.pos.x, 10, 'X is there'); 48 | t.equals(body2.pos.y, 0, 'Y is there'); 49 | t.end(); 50 | }); 51 | 52 | test('Does not update position of one body', function (t) { 53 | var simulator = createSimulator(); 54 | var body = new physics.Body(0, 0); 55 | simulator.addBody(body); 56 | 57 | simulator.step(1); 58 | t.equals(simulator.bodies.length, 1, 'Number of bodies is 1'); 59 | t.equals(simulator.springs.length, 0, 'Number of springs is 0'); 60 | t.equals(simulator.bodies[0], body, 'Body points to actual object'); 61 | t.equals(body.pos.x, 0, 'X is not changed'); 62 | t.equals(body.pos.y, 0, 'Y is not changed'); 63 | t.end(); 64 | }); 65 | 66 | test('Can configure forces', function (t) { 67 | t.test('Gravity', function (t) { 68 | var simulator = createSimulator(); 69 | var body1 = new physics.Body(0, 0); 70 | var body2 = new physics.Body(1, 0); 71 | 72 | simulator.addBody(body1); 73 | simulator.addBody(body2); 74 | 75 | simulator.step(); 76 | // by default gravity is negative, bodies should repel each other: 77 | var x1 = body1.pos.x; 78 | var x2 = body2.pos.x; 79 | t.ok(x1 < 0, 'Body 1 moves away from body 2'); 80 | t.ok(x2 > 1, 'Body 2 moves away from body 1'); 81 | 82 | // now reverse gravity, and bodies should attract each other: 83 | simulator.gravity(100); 84 | simulator.step(); 85 | t.ok(body1.pos.x > x1, 'Body 1 moved towards body 2'); 86 | t.ok(body2.pos.x < x2, 'Body 2 moved towards body 1'); 87 | 88 | t.end(); 89 | }); 90 | 91 | t.test('Drag', function (t) { 92 | var simulator = createSimulator(); 93 | var body1 = new physics.Body(0, 0); 94 | body1.velocity.x = -1; // give it small impulse 95 | simulator.addBody(body1); 96 | 97 | simulator.step(); 98 | 99 | var x1 = body1.velocity.x; 100 | // by default drag force will slow down entire system: 101 | t.ok(x1 > -1, 'Body 1 moves at reduced speed'); 102 | 103 | // Restore original velocity, but now set drag force to 0 104 | body1.velocity.x = -1; 105 | simulator.dragCoeff(0); 106 | simulator.step(); 107 | t.ok(body1.velocity.x === -1, 'Velocity should remain unchanged'); 108 | t.end(); 109 | }); 110 | t.end(); 111 | }); 112 | 113 | test('Can remove bodies', function (t) { 114 | var simulator = createSimulator(); 115 | var body = new physics.Body(0, 0); 116 | simulator.addBody(body); 117 | t.equals(simulator.bodies.length, 1, 'Number of bodies is 1'); 118 | var result = simulator.removeBody(body); 119 | t.equals(result, true, 'body successfully removed'); 120 | t.equals(simulator.bodies.length, 0, 'Number of bodies is 0'); 121 | t.end(); 122 | }); 123 | 124 | test('Updates position for two bodies', function (t) { 125 | var simulator = createSimulator(); 126 | var body1 = new physics.Body(-1, 0); 127 | var body2 = new physics.Body(1, 0); 128 | simulator.addBody(body1); 129 | simulator.addBody(body2); 130 | 131 | simulator.step(); 132 | t.equals(simulator.bodies.length, 2, 'Number of bodies is 2'); 133 | t.ok(body1.pos.x !== 0, 'Body1.X has changed'); 134 | t.ok(body2.pos.x !== 0, 'Body2.X has changed'); 135 | 136 | t.equals(body1.pos.y, 0, 'Body1.Y has not changed'); 137 | t.equals(body2.pos.y, 0, 'Body2.Y has not changed'); 138 | t.end(); 139 | }); 140 | 141 | test('add spring should not add bodies', function (t) { 142 | var simulator = createSimulator(); 143 | var body1 = new physics.Body(-1, 0); 144 | var body2 = new physics.Body(1, 0); 145 | 146 | simulator.addSpring(body1, body2, 10); 147 | 148 | t.equals(simulator.bodies.length, 0, 'Should not add two bodies'); 149 | t.equals(simulator.bodies.length, 0, 'Should not add two bodies'); 150 | t.equals(simulator.springs.length, 1, 'Should have a spring'); 151 | t.end(); 152 | }); 153 | 154 | test('Spring affects bodies positions', function (t) { 155 | var simulator = createSimulator(); 156 | var body1 = new physics.Body(-10, 0); 157 | var body2 = new physics.Body(10, 0); 158 | simulator.addBody(body1); 159 | simulator.addBody(body2); 160 | // If you take this out, bodies will repel each other: 161 | simulator.addSpring(body1, body2, 1); 162 | 163 | simulator.step(); 164 | 165 | t.ok(body1.pos.x > -10, 'Body 1 should move towards body 2'); 166 | t.ok(body2.pos.x < 10, 'Body 2 should move towards body 1'); 167 | 168 | t.end(); 169 | }); 170 | 171 | test('Can remove springs', function (t) { 172 | var simulator = createSimulator(); 173 | var body1 = new physics.Body(-10, 0); 174 | var body2 = new physics.Body(10, 0); 175 | simulator.addBody(body1); 176 | simulator.addBody(body2); 177 | var spring = simulator.addSpring(body1, body2, 1); 178 | simulator.removeSpring(spring); 179 | 180 | simulator.step(); 181 | 182 | t.ok(body1.pos.x < -10, 'Body 1 should move away from body 2'); 183 | t.ok(body2.pos.x > 10, 'Body 2 should move away from body 1'); 184 | 185 | t.end(); 186 | }); 187 | 188 | test('Get bounding box', function (t) { 189 | var simulator = createSimulator(); 190 | var body1 = new physics.Body(0, 0); 191 | var body2 = new physics.Body(10, 10); 192 | simulator.addBody(body1); 193 | simulator.addBody(body2); 194 | simulator.step(); // this will move bodies farther away 195 | var bbox = simulator.getBBox(); 196 | t.ok(bbox.x1 <= 0, 'Left is 0'); 197 | t.ok(bbox.y1 <= 0, 'Top is 0'); 198 | t.ok(bbox.x2 >= 10, 'right is 10'); 199 | t.ok(bbox.y2 >= 10, 'bottom is 10'); 200 | t.end(); 201 | }); 202 | 203 | test('it updates bounding box', function (t) { 204 | var simulator = createSimulator(); 205 | var body1 = new physics.Body(0, 0); 206 | var body2 = new physics.Body(10, 10); 207 | simulator.addBody(body1); 208 | simulator.addBody(body2); 209 | var bbox = simulator.getBBox(); 210 | 211 | t.ok(bbox.x1 === 0, 'Left is 0'); 212 | t.ok(bbox.y1 === 0, 'Top is 0'); 213 | t.ok(bbox.x2 === 10, 'right is 10'); 214 | t.ok(bbox.y2 === 10, 'bottom is 10'); 215 | 216 | body1.setPosition(15, 15); 217 | simulator.invalidateBBox(); 218 | bbox = simulator.getBBox(); 219 | 220 | t.ok(bbox.x1 === 10, 'Left is 10'); 221 | t.ok(bbox.y1 === 10, 'Top is 10'); 222 | t.ok(bbox.x2 === 15, 'right is 15'); 223 | t.ok(bbox.y2 === 15, 'bottom is 15'); 224 | t.end(); 225 | }); 226 | 227 | test('Get best position', function (t) { 228 | t.test('can get with empty simulator', function (t) { 229 | var simulator = createSimulator(); 230 | var empty = simulator.getBestNewBodyPosition([]); 231 | t.ok(typeof empty.x === 'number', 'Has X'); 232 | t.ok(typeof empty.y === 'number', 'Has Y'); 233 | 234 | t.end(); 235 | }); 236 | 237 | t.end(); 238 | }); 239 | 240 | test('it can change settings', function(t) { 241 | var simulator = createSimulator(); 242 | 243 | var currentTheta = simulator.theta(); 244 | t.ok(typeof currentTheta === 'number', 'theta is here'); 245 | simulator.theta(1.2); 246 | t.equals(simulator.theta(), 1.2, 'theta is changed'); 247 | 248 | var currentSpringCoeff = simulator.springCoeff(); 249 | t.ok(typeof currentSpringCoeff === 'number', 'springCoeff is here'); 250 | simulator.springCoeff(0.8); 251 | t.equals(simulator.springCoeff(), 0.8, 'springCoeff is changed'); 252 | 253 | var gravity = simulator.gravity(); 254 | t.ok(typeof gravity === 'number', 'gravity is here'); 255 | simulator.gravity(-0.8); 256 | t.equals(simulator.gravity(), -0.8, 'gravity is changed'); 257 | 258 | var springLength = simulator.springLength(); 259 | t.ok(typeof springLength === 'number', 'springLength is here'); 260 | simulator.springLength(80); 261 | t.equals(simulator.springLength(), 80, 'springLength is changed'); 262 | 263 | var dragCoeff = simulator.dragCoeff(); 264 | t.ok(typeof dragCoeff === 'number', 'dragCoeff is here'); 265 | simulator.dragCoeff(0.8); 266 | t.equals(simulator.dragCoeff(), 0.8, 'dragCoeff is changed'); 267 | 268 | var timeStep = simulator.timeStep(); 269 | t.ok(typeof timeStep === 'number', 'timeStep is here'); 270 | simulator.timeStep(8); 271 | t.equals(simulator.timeStep(), 8, 'timeStep is changed'); 272 | 273 | t.end(); 274 | }); 275 | --------------------------------------------------------------------------------