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>