2
\$\begingroup\$

I've just rewritten this code (a simple gravitational restricted n-body simulation) to display using canvas, rather than SVG. The reason being that I'd like to be able to run it with many thousands of particles.

To my surprise I didn't see any sizable performance improvement after making the conversion. I'm wondering if I've missed some simple step in the implementation, or if I'm misidentifying the bottleneck?

    var width=0.5*window.innerWidth || document.body.clientWidth;
var height=width;
//var svg=d3.select("#sim").append("svg");
//svg.attr('width',width)
//  .attr('height',height);

var canvas = d3.select('#sim').append('canvas').attr('width', width).attr('height', height);
var context = canvas.node().getContext('2d');

// instead of using svg and storing data in DOM we create a pseudo-DOM (maybe - https://www.freecodecamp.org/news/d3-and-canvas-in-3-steps-8505c8b27444/)
//var customBase = document.createElement('custom');
//var custom = d3.select(customBase); // This is your SVG replacement and the parent of all other elements

var custom = d3.select('#sim').append('custom');

function databind(data) {
  var circles = custom.selectAll('custom.circle')
    .data(data);
  var enterSel = circles.enter()
    .append('custom').attr('class', 'circle')
    .attr('id',function(d) { return "particle" + d.id; })
    .attr('r',function(d) { return d.r; })
    .attr('cx', function(d) {return width*(d.x[0]+boxSize)/(2*boxSize);})
    .attr('cy', function(d) {return width*(d.x[1]+boxSize)/(2*boxSize);})
    .attr('fillStyle', function(d) {return d.c});
  circles.merge(enterSel)
    .transition()
    .attr('cx', function(d) {return width*(d.x[0]+boxSize)/(2*boxSize);})
    .attr('cy', function(d) {return width*(d.x[1]+boxSize)/(2*boxSize);})
    .attr('fillStyle', function(d) {return d.c});
}
//var join = custom.selectAll('custom.circle')  .data(data);
function draw(){
  context.clearRect(0, 0, width, height);
  var elements = custom.selectAll('custom.circle');
  elements.each(function(d,i) {
    var node = d3.select(this);
    //console.log(node.attr('fillStyle'))
    context.beginPath();
    context.arc(node.attr('cx'), node.attr('cy'), node.attr('r'), 0, 2*Math.PI);
    context.fillStyle = node.attr('fillStyle');
    context.fill();
    //context.lineWidth = 1;
    //context.stroke();
  })
}

var info = d3.select('body').append('div')
  .style('position', 'absolute')
  .style('top', '0')
  .style('left', '0')
  .style('padding', '5px')
  .style('background-color', '#aaa');
var start = d3.now(), t0 = 0, sum = 0, cnt = 0;
var fmt = d3.format(",d");

var G=6.67e-11; // in SI
var mSun=2e30;
var kpc=3e19;
var Gyr=3.15e16;
var Galt=G*(mSun*(Gyr**2))/(kpc**3);

var rHat=[0,0,0];
var a1=[0,0,0];
var a2=[0,0,0];

function gravParticle(){
  this.id=0;
  this.m=1e6;
  this.x=[0,0,0];
  this.v=[0,0,0];
  this.a=[0,0,0];
  this.flag=0;
  this.c='black';
  this.r=2;
  this.smooth=0.1;
}
function gravity(p1,p2){ // calculating and updating the acceleration of a pair of particles
  var r=Math.sqrt((p2.x[0]-p1.x[0])**2 + (p2.x[1]-p1.x[1])**2 + (p2.x[2]-p1.x[2])**2 + p1.smooth**2 + p2.smooth**2);

  rHat[0]=(p2.x[0]-p1.x[0])/r;
  rHat[1]=(p2.x[1]-p1.x[1])/r;
  rHat[2]=(p2.x[2]-p1.x[2])/r;

  a1[0]=rHat[0]*Galt*p2.m/(r**2);
  a1[1]=rHat[1]*Galt*p2.m/(r**2);
  a1[2]=rHat[2]*Galt*p2.m/(r**2);

  a2[0]=-rHat[0]*Galt*p1.m/(r**2);
  a2[1]=-rHat[1]*Galt*p1.m/(r**2);
  a2[2]=-rHat[2]*Galt*p1.m/(r**2);

  p1.a[0]=p1.a[0]+a1[0];
  p1.a[1]=p1.a[1]+a1[1];
  p1.a[2]=p1.a[2]+a1[2];

  p2.a[0]=p2.a[0]+a2[0];
  p2.a[1]=p2.a[1]+a2[1];
  p2.a[2]=p2.a[2]+a2[2];
}
function update(ps){
  clearAcc(ps)
  for (var i=0; i<nParticles; i++){
    if (ps[i].flag == 1){
      var index=ps[i].id;
      for (var j=i+1; j<nParticles; j++){
        gravity(ps[i],ps[j]);
      }
    }
  }
}
var boxSize=30;
var ps=[];
var host = new gravParticle();
host.m=1e11;
host.flag=1;
host.c='red';
host.r=20;
host.smooth=1;
var sat = new gravParticle();
sat.m=1e10;
sat.id=1;
sat.flag=1;
sat.x=[20,0,0];
sat.v=[0,-200,0];
sat.r=10;
sat.c='blue';
sat.smooth=0.5;
host.v[1]=-(sat.m/host.m)*sat.v[1];
ps.push(host);
ps.push(sat);
rMin=1;
rMax=20;
var nPassive=100;
for (var i=0;i<nPassive;i++){
  var pNew = new gravParticle();
  pNew.m=1e11/nPassive;
  pNew.id=i+2;
  pNew.flag=0;
  var theta=Math.PI*Math.random();
  var phi=2*Math.PI*Math.random();
  var r=rMin+Math.random()*(rMax - rMin);
  pNew.x=[r*Math.cos(phi)*Math.sin(theta),r*Math.sin(phi)*Math.sin(theta),r*Math.cos(theta)];

  var vTheta=Math.PI*Math.random();
  var vPhi=2*Math.PI*Math.random();
  var v=Math.sqrt(Galt*1e11/r);
  pNew.v=[v*Math.cos(vPhi)*Math.sin(vTheta),v*Math.sin(vPhi)*Math.sin(vTheta),v*Math.cos(vTheta)];
  //pNew.v=[150*randn_bm(),150*randn_bm(),150*randn_bm()];
  pNew.smooth=0.1;
  ps.push(pNew);
}

var nParticles=ps.length;

//var circles=custom.selectAll(".node")
//  .data(ps)
//  .enter()
//  .append('circle')
//  .attr('id',function(d) { return "particle" + d.id; })
//  .attr('r',function(d) { return d.r; })
//  .style("fill", function(d) {return d.c})
//  .attr('cx', function(d) {return width*(d.x[0]+boxSize)/(2*boxSize);})
//  .attr('cy', function(d) {return width*(d.x[1]+boxSize)/(2*boxSize);});


var dt=0.001;

update(ps);
databind(ps);
draw();

var t=0;

function evolve() {
  for (var i=0; i< nParticles; i++){
    ps[i].v[0]=ps[i].v[0]+ps[i].a[0]*dt/2;
    ps[i].v[1]=ps[i].v[1]+ps[i].a[1]*dt/2;
    ps[i].v[2]=ps[i].v[2]+ps[i].a[2]*dt/2;

    ps[i].x[0]=ps[i].x[0]+ps[i].v[0]*dt;
    ps[i].x[1]=ps[i].x[1]+ps[i].v[1]*dt;
    ps[i].x[2]=ps[i].x[2]+ps[i].v[2]*dt;
  }
  update(ps); // finds new acceleration
  for (var i=0; i < nParticles; i++){
    ps[i].v[0]=ps[i].v[0]+ps[i].a[0]*dt/2;
    ps[i].v[1]=ps[i].v[1]+ps[i].a[1]*dt/2;
    ps[i].v[2]=ps[i].v[2]+ps[i].a[2]*dt/2;
    //if (i>1000) {continue;}

    var thisCircle=d3.select('#particle'+ps[i].id);
    thisCircle
      .attr('cx', function(d) {return width*(d.x[0]+boxSize)/(2*boxSize);})
      .attr('cy',function(d) {return width*(d.x[1]+boxSize)/(2*boxSize);})
      .style("fill", function(d) {
        if (d.flag==0){ return d3.interpolateSpectral((d.x[2]+boxSize)/(2*boxSize))}
        return d.c
      })
  }
  draw();
  t = d3.now();
  var fps = 1000 / (t - t0);
  info.html(fmt(fps) + " fps = " + fmt(fps) + ' frames/sec');
  t0=t;
}

function clearAcc(ps){ // clearing old accelerations at each step
  for (var i = 0; i < ps.length; i++) {
    ps[i].a=[0,0,0];
  }
}

var startStop=1
document.body.onkeyup = function(e){
  if(e.keyCode == 32){
      startStop=(startStop+1)%2
      if (startStop==0) {timer.stop();}
      if (startStop==1) {timer.restart(evolve,1000/60);}
  }
}
var timer=d3.interval(evolve,1000/60);

// Standard Normal variate using Box-Muller transform.
function randn_bm() {
  var u = 0, v = 0;
  while(u === 0) u = Math.random(); //Converting [0,1) to (0,1)
  while(v === 0) v = Math.random();
  return Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v );
}
\$\endgroup\$
0

1 Answer 1

5
\$\begingroup\$

Before anything else, the tutorial you're referencing is this: https://www.freecodecamp.org/news/d3-and-canvas-in-3-steps-8505c8b27444/

By reading the tutorial you'll see that the elements we'll create for manipulating the <canvas>...

... don’t live in the DOM but only in memory (in a ‘virtual’ DOM) and describe the life-cycle of these elements in a known D3 way.

However, this is what you did in your code:

var custom = d3.select('#sim').append('custom');

By doing that, you're actually appending a meaningless element called <custom> to the DOM! If you inspect your page you'll see thousands of them:

<div class="sim" id="sim">
    <canvas width="615" height="615"></canvas>
    <custom>
        <custom class="circle" id="particle0" r="20" cx="307.5" cy="307.5" fillstyle="red"></custom>
        <custom class="circle" id="particle1" r="10" cx="512.5" cy="307.5" fillstyle="blue"></custom>
        <custom class="circle" id="particle2" r="2" cx="313.23860812263854" cy="267.5420080575814" fillstyle="black"></custom>
        <custom class="circle" id="particle3" r="2" cx="210.56926409724912" cy="223.62781812619318" fillstyle="black"></custom>
        etc...
    </custom>
</div>

Because of that you're missing the whole point of using HTML canvas instead of SVG, and losing all its performance. In other words: you changed SVG for canvas but you're still appending and manipulating DOM elements, which is the very drawback of SVGs.

Using D3 with HTML canvas

Just like the tutorial you mentioned in your question, create an element without appending it:

var customBase = document.createElement('custom');

var custom = d3.select(customBase);

That's, by far, the most important change here. But there are other needed changes as well:

You don't need to draw anything in your enter/update selection, therefore you don't need to set attributes to them. The important thing here is entering/updating the elements and binding data to them.

That being said, it can be just:

function databind(data) {
  var circles = custom.selectAll('custom')
    .data(data);
  var enterSel = circles.enter()
    .append('custom');
  circles = circles.merge(enterSel);
}

By the way, you're missing an exit selection here. I don't know if missed it on purpose or not.

Then, after simplifying your selections, use the datum in the draw function itself:

function draw() {
  context.clearRect(0, 0, width, height);
  custom.selectAll('custom').each(function(d, i) {
    context.beginPath();
    context.arc(width * (d.x[0] + boxSize) / (2 * boxSize), width * (d.x[1] + boxSize) / (2 * boxSize), d.r, 0, 2 * Math.PI);
    context.fillStyle = d.c;
    context.fill();
  })
}

With these changes we get a solid 30-35 fps, way better than your 13-14fps:

var width = window.innerWidth || document.body.clientWidth;
var height = width;

var canvas = d3.select('#sim').append('canvas').attr('width', width).attr('height', height);
var context = canvas.node().getContext('2d');

var customBase = document.createElement('custom');

var custom = d3.select(customBase);

function databind(data) {
  var circles = custom.selectAll('custom')
    .data(data);
  var enterSel = circles.enter()
    .append('custom');
  circles = circles.merge(enterSel);
}
//var join = custom.selectAll('custom.circle')  .data(data);
function draw() {
  context.clearRect(0, 0, width, height);
  custom.selectAll('custom').each(function(d, i) {
    context.beginPath();
    context.arc(width * (d.x[0] + boxSize) / (2 * boxSize), width * (d.x[1] + boxSize) / (2 * boxSize), d.r, 0, 2 * Math.PI);
    context.fillStyle = d.c;
    context.fill();
  })
}

var info = d3.select('body').append('div')
  .style('position', 'absolute')
  .style('top', '0')
  .style('left', '0')
  .style('padding', '5px')
  .style('background-color', '#aaa');
var start = d3.now(),
  t0 = 0,
  sum = 0,
  cnt = 0;
var fmt = d3.format(",d");

var G = 6.67e-11; // in SI
var mSun = 2e30;
var kpc = 3e19;
var Gyr = 3.15e16;
var Galt = G * (mSun * (Gyr ** 2)) / (kpc ** 3);

var rHat = [0, 0, 0];
var a1 = [0, 0, 0];
var a2 = [0, 0, 0];

function gravParticle() {
  this.id = 0;
  this.m = 1e6;
  this.x = [0, 0, 0];
  this.v = [0, 0, 0];
  this.a = [0, 0, 0];
  this.flag = 0;
  this.c = 'black';
  this.r = 2;
  this.smooth = 0.1;
}

function gravity(p1, p2) { // calculating and updating the acceleration of a pair of particles
  var r = Math.sqrt((p2.x[0] - p1.x[0]) ** 2 + (p2.x[1] - p1.x[1]) ** 2 + (p2.x[2] - p1.x[2]) ** 2 + p1.smooth ** 2 + p2.smooth ** 2);

  rHat[0] = (p2.x[0] - p1.x[0]) / r;
  rHat[1] = (p2.x[1] - p1.x[1]) / r;
  rHat[2] = (p2.x[2] - p1.x[2]) / r;

  a1[0] = rHat[0] * Galt * p2.m / (r ** 2);
  a1[1] = rHat[1] * Galt * p2.m / (r ** 2);
  a1[2] = rHat[2] * Galt * p2.m / (r ** 2);

  a2[0] = -rHat[0] * Galt * p1.m / (r ** 2);
  a2[1] = -rHat[1] * Galt * p1.m / (r ** 2);
  a2[2] = -rHat[2] * Galt * p1.m / (r ** 2);

  p1.a[0] = p1.a[0] + a1[0];
  p1.a[1] = p1.a[1] + a1[1];
  p1.a[2] = p1.a[2] + a1[2];

  p2.a[0] = p2.a[0] + a2[0];
  p2.a[1] = p2.a[1] + a2[1];
  p2.a[2] = p2.a[2] + a2[2];
}

function update(ps) {
  clearAcc(ps)
  for (var i = 0; i < nParticles; i++) {
    if (ps[i].flag == 1) {
      var index = ps[i].id;
      for (var j = i + 1; j < nParticles; j++) {
        gravity(ps[i], ps[j]);
      }
    }
  }
}
var boxSize = 30;
var ps = [];
var host = new gravParticle();
host.m = 1e11;
host.flag = 1;
host.c = 'red';
host.r = 20;
host.smooth = 1;
var sat = new gravParticle();
sat.m = 1e10;
sat.id = 1;
sat.flag = 1;
sat.x = [20, 0, 0];
sat.v = [0, -200, 0];
sat.r = 10;
sat.c = 'blue';
sat.smooth = 0.5;
host.v[1] = -(sat.m / host.m) * sat.v[1];
ps.push(host);
ps.push(sat);
rMin = 1;
rMax = 20;
var nPassive = 5000;
for (var i = 0; i < nPassive; i++) {
  var pNew = new gravParticle();
  pNew.m = 1e11 / nPassive;
  pNew.id = i + 2;
  pNew.flag = 0;
  var theta = Math.PI * Math.random();
  var phi = 2 * Math.PI * Math.random();
  var r = rMin + Math.random() * (rMax - rMin);
  pNew.x = [r * Math.cos(phi) * Math.sin(theta), r * Math.sin(phi) * Math.sin(theta), r * Math.cos(theta)];

  var vTheta = Math.PI * Math.random();
  var vPhi = 2 * Math.PI * Math.random();
  var v = Math.sqrt(Galt * 1e11 / r);
  pNew.v = [v * Math.cos(vPhi) * Math.sin(vTheta), v * Math.sin(vPhi) * Math.sin(vTheta), v * Math.cos(vTheta)];
  //pNew.v=[150*randn_bm(),150*randn_bm(),150*randn_bm()];
  pNew.smooth = 0.1;
  ps.push(pNew);
}

var nParticles = ps.length;

var dt = 0.001;

update(ps);
databind(ps);
draw();

var t = 0;

function evolve() {
  for (var i = 0; i < nParticles; i++) {
    ps[i].v[0] = ps[i].v[0] + ps[i].a[0] * dt / 2;
    ps[i].v[1] = ps[i].v[1] + ps[i].a[1] * dt / 2;
    ps[i].v[2] = ps[i].v[2] + ps[i].a[2] * dt / 2;

    ps[i].x[0] = ps[i].x[0] + ps[i].v[0] * dt;
    ps[i].x[1] = ps[i].x[1] + ps[i].v[1] * dt;
    ps[i].x[2] = ps[i].x[2] + ps[i].v[2] * dt;
  }
  update(ps); // finds new acceleration
  for (var i = 0; i < nParticles; i++) {
    ps[i].v[0] = ps[i].v[0] + ps[i].a[0] * dt / 2;
    ps[i].v[1] = ps[i].v[1] + ps[i].a[1] * dt / 2;
    ps[i].v[2] = ps[i].v[2] + ps[i].a[2] * dt / 2;
    //if (i>1000) {continue;}

    var thisCircle = d3.select('#particle' + ps[i].id);
    thisCircle
      .attr('cx', function(d) {
        return width * (d.x[0] + boxSize) / (2 * boxSize);
      })
      .attr('cy', function(d) {
        return width * (d.x[1] + boxSize) / (2 * boxSize);
      })
      .style("fill", function(d) {
        return d.c
      })
  }
  draw();
  t = d3.now();
  var fps = 1000 / (t - t0);
  info.html(fmt(fps) + " fps = " + fmt(fps) + ' frames/sec');
  t0 = t;
}

function clearAcc(ps) { // clearing old accelerations at each step
  for (var i = 0; i < ps.length; i++) {
    ps[i].a = [0, 0, 0];
  }
}

var startStop = 1
document.body.onkeyup = function(e) {
  if (e.keyCode == 32) {
    startStop = (startStop + 1) % 2
    if (startStop == 0) {
      timer.stop();
    }
    if (startStop == 1) {
      timer.restart(evolve, 1000 / 60);
    }
  }
}
var timer = d3.interval(evolve, 1000 / 60);

// Standard Normal variate using Box-Muller transform.
function randn_bm() {
  var u = 0,
    v = 0;
  while (u === 0) u = Math.random(); //Converting [0,1) to (0,1)
  while (v === 0) v = Math.random();
  return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
}
div.container {
  display: flex;
  /* Magic begins */
  flex-direction: row;
  width: 100vw;
  height: 50vw;
}

div.sim {
  flex-grow: 1;
  width: 50%;
}

div .link {
  stroke: #bbb;
}

.node circle {
  stroke-width: 2px;
  r: 2px
}

* {
  margin: 0;
  padding: 0;
}

div.text {
  flex-grow: 1;
  width: 50%;
  padding-left: 5%;
  padding-right: 1%;
  align-self: flex-end;
  text-align: right;
  font-family: Futura, "Trebuchet MS", Arial, sans-serif;
  font-weight: bold;
  color: black;
}

h1 {
  margin: 3%;
  font-size: 32px;
}

h2 {
  margin: 3%;
  font-size: 14px;
}

p {
  margin: 3%;
  font-size: 16px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div class="container">
  <div class="sim" id="sim"> </div>
</div>

Bear in mind that this is just the beginning, there is room for lots of improvements (both D3 related and general JavaScript good practices, like using let and const). For instance, if you remove that ID selection inside evolve (whose purpose is not clear for me), we get something around 50 fps:

var width = window.innerWidth || document.body.clientWidth;
var height = width;

var canvas = d3.select('#sim').append('canvas').attr('width', width).attr('height', height);
var context = canvas.node().getContext('2d');

var customBase = document.createElement('custom');

var custom = d3.select(customBase);

//var join = custom.selectAll('custom.circle')  .data(data);
function draw(data) {
  var circles = custom.selectAll('custom')
    .data(data);
  var enterSel = circles.enter()
    .append('custom');
  circles = circles.merge(enterSel);
  context.clearRect(0, 0, width, height);
  circles.each(function(d, i) {
    context.beginPath();
    context.arc(width * (d.x[0] + boxSize) / (2 * boxSize), width * (d.x[1] + boxSize) / (2 * boxSize), d.r, 0, 2 * Math.PI);
    context.fillStyle = d.c;
    context.fill();
  })
}

var info = d3.select('body').append('div')
  .style('position', 'absolute')
  .style('top', '0')
  .style('left', '0')
  .style('padding', '5px')
  .style('background-color', '#aaa');
var start = d3.now(),
  t0 = 0,
  sum = 0,
  cnt = 0;
var fmt = d3.format(",d");

var G = 6.67e-11; // in SI
var mSun = 2e30;
var kpc = 3e19;
var Gyr = 3.15e16;
var Galt = G * (mSun * (Gyr ** 2)) / (kpc ** 3);

var rHat = [0, 0, 0];
var a1 = [0, 0, 0];
var a2 = [0, 0, 0];

function gravParticle() {
  this.id = 0;
  this.m = 1e6;
  this.x = [0, 0, 0];
  this.v = [0, 0, 0];
  this.a = [0, 0, 0];
  this.flag = 0;
  this.c = 'black';
  this.r = 2;
  this.smooth = 0.1;
}

function gravity(p1, p2) { // calculating and updating the acceleration of a pair of particles
  var r = Math.sqrt((p2.x[0] - p1.x[0]) ** 2 + (p2.x[1] - p1.x[1]) ** 2 + (p2.x[2] - p1.x[2]) ** 2 + p1.smooth ** 2 + p2.smooth ** 2);

  rHat[0] = (p2.x[0] - p1.x[0]) / r;
  rHat[1] = (p2.x[1] - p1.x[1]) / r;
  rHat[2] = (p2.x[2] - p1.x[2]) / r;

  a1[0] = rHat[0] * Galt * p2.m / (r ** 2);
  a1[1] = rHat[1] * Galt * p2.m / (r ** 2);
  a1[2] = rHat[2] * Galt * p2.m / (r ** 2);

  a2[0] = -rHat[0] * Galt * p1.m / (r ** 2);
  a2[1] = -rHat[1] * Galt * p1.m / (r ** 2);
  a2[2] = -rHat[2] * Galt * p1.m / (r ** 2);

  p1.a[0] = p1.a[0] + a1[0];
  p1.a[1] = p1.a[1] + a1[1];
  p1.a[2] = p1.a[2] + a1[2];

  p2.a[0] = p2.a[0] + a2[0];
  p2.a[1] = p2.a[1] + a2[1];
  p2.a[2] = p2.a[2] + a2[2];
}

function update(ps) {
  clearAcc(ps)
  for (var i = 0; i < nParticles; i++) {
    if (ps[i].flag == 1) {
      var index = ps[i].id;
      for (var j = i + 1; j < nParticles; j++) {
        gravity(ps[i], ps[j]);
      }
    }
  }
}
var boxSize = 30;
var ps = [];
var host = new gravParticle();
host.m = 1e11;
host.flag = 1;
host.c = 'red';
host.r = 20;
host.smooth = 1;
var sat = new gravParticle();
sat.m = 1e10;
sat.id = 1;
sat.flag = 1;
sat.x = [20, 0, 0];
sat.v = [0, -200, 0];
sat.r = 10;
sat.c = 'blue';
sat.smooth = 0.5;
host.v[1] = -(sat.m / host.m) * sat.v[1];
ps.push(host);
ps.push(sat);
rMin = 1;
rMax = 20;
var nPassive = 5000;
for (var i = 0; i < nPassive; i++) {
  var pNew = new gravParticle();
  pNew.m = 1e11 / nPassive;
  pNew.id = i + 2;
  pNew.flag = 0;
  var theta = Math.PI * Math.random();
  var phi = 2 * Math.PI * Math.random();
  var r = rMin + Math.random() * (rMax - rMin);
  pNew.x = [r * Math.cos(phi) * Math.sin(theta), r * Math.sin(phi) * Math.sin(theta), r * Math.cos(theta)];

  var vTheta = Math.PI * Math.random();
  var vPhi = 2 * Math.PI * Math.random();
  var v = Math.sqrt(Galt * 1e11 / r);
  pNew.v = [v * Math.cos(vPhi) * Math.sin(vTheta), v * Math.sin(vPhi) * Math.sin(vTheta), v * Math.cos(vTheta)];
  //pNew.v=[150*randn_bm(),150*randn_bm(),150*randn_bm()];
  pNew.smooth = 0.1;
  ps.push(pNew);
}

var nParticles = ps.length;

var dt = 0.001;

update(ps);
draw(ps);

var t = 0;

function evolve() {
  for (var i = 0; i < nParticles; i++) {
    ps[i].v[0] = ps[i].v[0] + ps[i].a[0] * dt / 2;
    ps[i].v[1] = ps[i].v[1] + ps[i].a[1] * dt / 2;
    ps[i].v[2] = ps[i].v[2] + ps[i].a[2] * dt / 2;

    ps[i].x[0] = ps[i].x[0] + ps[i].v[0] * dt;
    ps[i].x[1] = ps[i].x[1] + ps[i].v[1] * dt;
    ps[i].x[2] = ps[i].x[2] + ps[i].v[2] * dt;
  }
  update(ps); // finds new acceleration
  for (var i = 0; i < nParticles; i++) {
    ps[i].v[0] = ps[i].v[0] + ps[i].a[0] * dt / 2;
    ps[i].v[1] = ps[i].v[1] + ps[i].a[1] * dt / 2;
    ps[i].v[2] = ps[i].v[2] + ps[i].a[2] * dt / 2;
    //if (i>1000) {continue;}
  }
  draw(ps);
  t = d3.now();
  var fps = 1000 / (t - t0);
  info.html(fmt(fps) + " fps = " + fmt(fps) + ' frames/sec');
  t0 = t;
}

function clearAcc(ps) { // clearing old accelerations at each step
  for (var i = 0; i < ps.length; i++) {
    ps[i].a = [0, 0, 0];
  }
}

var startStop = 1
document.body.onkeyup = function(e) {
  if (e.keyCode == 32) {
    startStop = (startStop + 1) % 2
    if (startStop == 0) {
      timer.stop();
    }
    if (startStop == 1) {
      timer.restart(evolve, 1000 / 60);
    }
  }
}
var timer = d3.interval(evolve, 1000 / 60);

// Standard Normal variate using Box-Muller transform.
function randn_bm() {
  var u = 0,
    v = 0;
  while (u === 0) u = Math.random(); //Converting [0,1) to (0,1)
  while (v === 0) v = Math.random();
  return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
}
div.container {
  display: flex;
  /* Magic begins */
  flex-direction: row;
  width: 100vw;
  height: 50vw;
}

div.sim {
  flex-grow: 1;
  width: 50%;
}

div .link {
  stroke: #bbb;
}

.node circle {
  stroke-width: 2px;
  r: 2px
}

* {
  margin: 0;
  padding: 0;
}

div.text {
  flex-grow: 1;
  width: 50%;
  padding-left: 5%;
  padding-right: 1%;
  align-self: flex-end;
  text-align: right;
  font-family: Futura, "Trebuchet MS", Arial, sans-serif;
  font-weight: bold;
  color: black;
}

h1 {
  margin: 3%;
  font-size: 32px;
}

h2 {
  margin: 3%;
  font-size: 14px;
}

p {
  margin: 3%;
  font-size: 16px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div class="container">
  <div class="sim" id="sim"> </div>
</div>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.