Interconnected Spheres Javascript Demo

Posted on September 7th, 2020
Previous Article :: Next Article

Recently I came across the following tweet:

This made me think: “this would make a for a neat Javascript example”. So I quickly coded up a variant, which likely is solving a much simpler set of equations than Prof. Bertolotti had in mind. In the demo below, the spheres are connected with the spring equation, \(\vec{F}=k\left(\vec{x}_1-\vec{x}_2\right)\). A friction term is also applied to the velocity term. I don’t have much background in atomic science but I am sure this is far from a reasonable approximation of a crystal lattice. But still, this system is fun to play with. Click the mouse to drag any of the internal spheres. So far, this doesn’t work on iPhones, future work.


You can also view the code at this link: spheres.html. The videos below show the system with two different “atom” configurations. I find it interesting that in the second one, you can see the original displaced sphere return to its initial position before the disturbance finishes propagating through the system.

Figure 1. Two examples of system dynamics given different number of spheres

Implementation

Coding up this example took only about 2 hours. The main purpose of this post is to describe how the code works in case you encounter the need to implement something similar for your own research. As you know, I am a huge proponent of using HTML and Javascript for scientific computing. Running in a web browser, you get interactivity “for free”. You can see the entire source by opening spheres.html in a new tab, then right clicking, and selecting “View Page Source”.

HTML+CSS

The code starts off with an embedded CSS style sheet used to apply some formatting. Specifically, we add a border around a <canvas> elements, make <div> inline-block so we can stack divs side by side, and we give <input> legacy terminal look with monospace characters and green background.

<style>
canvas {border: 1px solid gray; box-shadow: 3px 3px #eee;}
div {display:inline-block; margin-left:10px;vertical-align:top;
font-family:monospace;}
div input{background:green;color:gold;margin:0.25em;}
div input[type="button"] {background-color:#eee;font-size:1.2em;border-radius:6px;border:2px outset gray;color:green;}
div input[type="button"]:hover {background-color:#fff};
</style>

We next add a 500×500 pixel <canvas> element. This demo was developed for a desktop and support for responsive web design and mobile friendliness is still pending. We also create a <div> containing various input fields to specify parameters such as the spring constant, friction term, time step, and the mesh dimensions. We add a listener to handle the user clicking the “Restart” button.

<canvas id="c" width="500px" height="500px">
</canvas>
 
<div>
Spring k: <input id="k" size="3" value="0.3"> <br>
Friction: <input id="alpha" size="3" value="0.01"> [0..1] <br>
Time step: <input id="dt" size="3" value="0.1"> <br>
Balls: <input id="ni" size="1" value="10">&times;<input id="nj" size="1" value="10"> <br>
<input type="button" value="Restart" onclick="init()">
</div>

Javascript

The rest of the code is Javascript.

<script>
const c = document.getElementById("c");
const ctx = c.getContext('2d');
 
var ni = 10;
var nj = 10;
var balls = [];  // empty array
 
/* ... */
 
// create a grid of balls
function init() {
  console.log("init");
  ni = parseInt(document.getElementById("ni").value);
  if (isNaN(ni) || ni<3 || ni>50) {ni = 10;document.getElementById("ni").value=ni;}
 
  nj = parseInt(document.getElementById("nj").value);
  if (isNaN(nj) || nj<3 || nj>50) {nj = 10;document.getElementById("nj").value=nj;}
 
  // set spacing and origin
  let di = 0.8*c.width/(ni-1);
  let dj = 0.8*c.height/(nj-1);
  let x0 = 0.1*c.width;
  let y0 = 0.1*c.height;
 
  balls = [];
  for (var i=0;i<ni;i++) {
    var col = [];           // empty column
    for (var j=0;j<nj;j++) {
      const x = x0+i*di;
      const y = y0+j*dj;
      const u = 0;
      const v = 0; 
      const r = 10;
      col.push({x:x,y:y,u:u,v:v,r:r});
    }
    balls.push(col);	// add entire column
  }
} 
 
// generate spheres and start animation
init();   
window.requestAnimationFrame(advance);

In the above snippet, we start off by grabbing the canvas element and then creating a 2D “context” that is used to actually draw shapes. We also include an “init” function that creates the initial ni*nj grid of spheres. The grid dimensions are obtained by converting the “value” in the HTML element with ids “ni” and “nj” into a number. If this fails (the user typed in something that is not a number), we revert to the default 10×10 configuration. We then create the grid. The ball positions are stored in a two-dimension array balls[i][j]. Since there is not a direct way to create 2D arrays in Javascript, we first populate the [j] column of each “i” index. This entire column (a 1D array) is then appended to the balls array as entry balls[i]. Positions are set from the standard Cartesian mapping, \(x = x_0 + i\Delta x\). Spacing and origin are set such that the “ni” balls span the middle 80% of the canvas area. This init function is called on start (note the init() line) but also on every “Restart” button click. We also use the requestAnimationFrame window-object function to call advance the next time the window is to be redrawn. Unlike the legacy setTimeout, this function is called just once.

Integration

The advance function updates positions of the internal spheres by integrating the equations of motion. The simple explicit forward Euler method is used here. Technically, perhaps we can claim that this is the more accurate Leapfrog, since initial velocity acceleration is zero anyway. However, this is no longer true once a sphere is displaced by the user. The velocity rewind should be applied to the dragging function, but this is left for future work. Anyway, this function begins by grabbing values from the <input> fields. This is not the optimal approach as we may end up grabbing value that is currently being edited, and thus not parseable to a float. Perhaps a better approach would be to use event listeners and only update our coefficients then. That approach would however require the user to either press the Enter key or tab out of the current field. We try to parse the strings value to an integer. If we do not succeed, the default value is substituted. We next assign some arbitrary mass, this is merely used to scale the force.

We then loop through all balls. Being stored as a 2D grid makes it easy to retrieve the neighbors. We only loop through the internal points since the boundary spheres are assumed to be fixed. As part of future work, it would be nice to make this an user input. For each ball, we compute the total force
$$\vec{F}_i = k\sum_{j}^4 \left(\vec{x}_{i,j}-\vec{x}_i\right)$$
where \(\vec{x}_{i,j}\) are the positions of the four neighbors of sphere i. Velocity and position is then integrated per
$$
\frac{d\vec{v}}{dt} = \frac{\vec{F}}{m} \qquad; \frac{d\vec{x}}{dt} = \vec{v}
$$
however, a friction-like damping is added to the velocity term, \((1-\alpha) \vec{v} \to \vec{v}\) (this effect could also be added, more physically, by adding a friction term). This term makes it possible for the spheres to stop oscillating. Finally, once the positions are updated, we call the draw function to update the screen output.

function advance() {
  // spring constant
  let k = parseFloat(document.getElementById("k").value);
  if (isNaN(k)) {k = 0.3;document.getElementById("k").value=k;}
 
  // friction term
  let alpha = parseFloat(document.getElementById("alpha").value);
  if (isNaN(alpha) || alpha<0 || alpha>1) {alpha = 0.01;document.getElementById("alpha").value=alpha;}
 
  // time step
  let dt = parseFloat(document.getElementById("dt").value);
  if (isNaN(dt)) {dt = 0.1;document.getElementById("dt").value=dt;}
 
  let m = 1;	 // some mass
 
  // update velocity
  for (var i=1;i<ni-1;i++)
    for (var j=1;j<nj-1;j++) {    
      let x0 = balls[i][j].x;
      let y0 = balls[i][j].y;
      //compute total force using only the four nearby neighbors
      let Fx = 0;
      let Fy = 0;
      if (i>0) {
        let dx = balls[i-1][j].x-x0;
        let dy = balls[i-1][j].y-y0;
        Fx += k*dx;
        Fy += k*dy;
      }
      if (i<ni-1) {
        let dx = balls[i+1][j].x-x0;
        let dy = balls[i+1][j].y-y0;
        Fx += k*dx;
        Fy += k*dy;
      }
      if (j>0) {
        let dx = balls[i][j-1].x-x0;
        let dy = balls[i][j-1].y-y0;
        Fx += k*dx;
        Fy += k*dy;
      }
      if (j<nj-1) {
        let dx = balls[i][j+1].x-x0;
        let dy = balls[i][j+1].y-y0;
        Fx += k*dx;
        Fy += k*dy;
      }
 
      // update velocity
      balls[i][j].u+=(Fx/m*dt);  
      balls[i][j].v+=(Fy/m*dt);
 
      // friction
      balls[i][j].u*=(1-alpha);
      balls[i][j].v*=(1-alpha);
    }
 
  // update position
  for (var i =0;i<ni;i++)
   for (var j=0;j<nj;j++) {   
     balls[i][j].x+=balls[i][j].u*dt;
     balls[i][j].y+=balls[i][j].v*dt;
  }
 
  draw();
}

Screen Output

The screen output is handled by the draw function. It starts by clearing the canvas by filling the entire element with a white rectangle. Next we draw the “sticks” denoting the interconnections. These are drawn first to make the balls appear on top of them. For each sphere, we draw an “L” connecting to its neighbor on the right and above it, as long as we remain in bounds. We then draw the spheres. Again, we loop through the grid, and simply draw a circle at each ball’s position. We use a radial gradient to give it a slight 3D effect. Finally, we call requestAnimationFrame to have the browser call the advance function again. This is how we implement the “main loop”.

function draw() {
  // clear canvas;
  ctx.fillStyle = "white";
  ctx.fillRect(0,0,c.width,c.height);
 
  //draw sticks
  for (var i=0;i<ni;i++) 
    for (var j=0;j<nj;j++) {
      ctx.strokeStyle="#eee";
 
      if(i<ni-1) {
        ctx.beginPath;
	ctx.moveTo(balls[i][j].x,balls[i][j].y);
	ctx.lineTo(balls[i+1][j].x,balls[i+1][j].y);
	ctx.stroke();
      }
      if(j<nj-1) {
        ctx.beginPath;
        ctx.moveTo(balls[i][j].x,balls[i][j].y);
        ctx.lineTo(balls[i][j+1].x,balls[i][j+1].y);
        ctx.stroke();
      }
   }
 
  // draw spheres
  for (var i = 0;i<ni;i++)
    for (var j=0;j<nj;j++) {
      var ball = balls[i][j]; 
      var grd = ctx.createRadialGradient(ball.x, ball.y, 0, ball.x, ball.y, 1.1*ball.r);
      grd.addColorStop(0, "black");
      grd.addColorStop(1, "white");
      ctx.fillStyle = grd;
      ctx.beginPath();
      ctx.arc(ball.x, ball.y, ball.r, 0, 2 * Math.PI);
      ctx.fill(); 
    }
 
  // continue animation
  window.requestAnimationFrame(advance);
}

User Interface

All that remains is implementing the drag-and-drop functionality. To do so, we add “mousedown” (mouse button pressed down) and “mousemove” event listeners to the canvas element. Whenever the user clicks the mouse or moves the mouse within the canvas, these event handlers get called. We also add a “mouseup” (mouse button released) listener, but this one is added to the entire window. This makes it possible to capture the mouse button being released even if the user moves the mouse off the canvas element.

When the mouse is pressed down, we convert the mouse position, given relative to the window, to a coordinate within our canvas. We then loop through the balls, and for each, compute the distance from the sphere center to the mouse position. Distance no greater than the sphere radius yields a match: the user click the mouse while the mouse is on top of a sphere. We store the sphere i and j index in a dragging object. This object is checked by the drag function that is called on mouse move. If the index is positive (indicating that the user clicked the mouse on top of a sphere), we simply update that sphere’s position to where the mouse is. Again, we translate the window mouse coordinate to the canvas position. Finally, the dragStop function called on the button release simply invalidates the dragging object. It is here where we should do a velocity rewind for Leapfrog, but… future work.

//drag and drop support
var dragging = {i:-1,j:-1};
c.addEventListener("mousedown", dragStart);
c.addEventListener("mousemove", drag);
document.addEventListener("mouseup", dragStop);
 
// mouse down
function dragStart(event){
  const rect = c.getBoundingClientRect()
  const x = event.clientX - rect.left
  const y = event.clientY - rect.top
 
  // check if mouse on top of one the balls
  for (var i=1;i<ni-1;i++) 
    for (var j=1;j<nj-1;j++) {
      dx = balls[i][j].x-x;
      dy = balls[i][j].y-y;
	  if (Math.sqrt(dx*dx+dy*dy)<=balls[i][j].r) {
        dragging.i=i;
        dragging.j=j;     
        return;  // stop search
      }     
    }  
}
 
// mouse move
function drag(event) {
  if (dragging.i<0) return;
  const rect = c.getBoundingClientRect()
  const x = event.clientX - rect.left
  const y = event.clientY - rect.top
  balls[dragging.i][dragging.j].x=x;
  balls[dragging.i][dragging.j].y=y;
  balls[dragging.i][dragging.j].u=0;
  balls[dragging.i][dragging.j].v=0;
}
 
// mouse up
function dragStop(event) {
  dragging.i=-1; 
}

And that’s it – sort of. This code does not yet work on mobile devices. Interaction with a touch screen is handled via “touch” events. This also remains as future work.

Future Work

As was noted above, there are few items left for future work. For instance, I meant to add inputs to control which boundaries are fixed. This would make it possible to fix only, let’s say, the bottom wall, or only the 4 corners. The velocity rewind for Leapfrog is also missing. The code does not work on the iPhone (and possibly other touch-based devices). There is also no way to resize the canvas. I may get to these at some future time. Alternatively, if you end up making these changes and don’t mind sharing, let me know so I can updated the code.

Leave a Reply

You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre lang="" line="" escaped="" cssfile=""> In addition, you can use \( ...\) to include equations.