block by harrystevens e2f49170367bbc10644ecb81f0e6dc54

Bouncing Balls II

Full Screen

An update to a previous block, but with collision detection and Canvas. This block uses Geometric.js for geometric calculations.

TODO:

index.html

<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      margin: 0;
    }
  </style>
</head>
<body>
  <div id="simulation"></div>
  <script src="https://d3js.org/d3-random.v1.min.js"></script>
  <script src="https://unpkg.com/geometric@2/build/geometric.min.js"></script>
  <script>

  // The Simulation class
  class Simulation {
    init(opts){
      this.width = opts && opts.width ? opts.width : innerWidth;
      this.height = opts && opts.height ? opts.height : innerHeight;
      this.center = [this.width / 2, this.height / 2];
      this.data = [];
      
      return this;
    }
    
    add(datum){
      const d = datum || {};
      d.pos = d.pos || this.center;
      d.radius = d.radius || 5;
      d.angle = d.angle || 0;
      d.speed = d.speed || 1;
      
      this.data.push(d);
      
      return this;
    }
    
    tick(){
      // Loop through the data
      for (let i = 0; i < this.data.length; i++){
        const d = this.data[i];
        d.collided = false;

        // Detect collisions
        for (let i0 = 0; i0 < this.data.length; i0++){
          const d0 = this.data[i0];
          d0.collided = false;

          // Collision!
          if (i !== i0 && geometric.lineLength([d.pos, d0.pos]) < d.radius + d0.radius && !d.collided && !d0.collided){
            
            // To avoid having them stick to each other,
            // test if moving them in each other's angles will bring them closer or farther apart
            const keep = geometric.lineLength([
                    geometric.pointTranslate(d.pos, d.angle, d.speed),
                    geometric.pointTranslate(d0.pos, d0.angle, d0.speed)
                  ]),
                  swap = geometric.lineLength([
                    geometric.pointTranslate(d.pos, d0.angle, d0.speed),
                    geometric.pointTranslate(d0.pos, d.angle, d.speed)
                  ]);

            if (keep < swap) {
              const dc = JSON.parse(JSON.stringify(d));

              d.angle = d0.angle;
              d.speed = d0.speed;
              d0.angle = dc.angle;
              d0.speed = dc.speed;
              
              d.collided = true;
              d0.collided = true;
            }

            break;
          }
        }

        // Detect sides
        const wallVertical = d.pos[0] <= d.radius || d.pos[0] >= this.width - d.radius,
              wallHorizontal = d.pos[1] <= d.radius || d.pos[1] >= this.height - d.radius;

        if (wallVertical || wallHorizontal){

          // Is it moving more towards the middle or away from it?
          const t0 = geometric.pointTranslate(d.pos, d.angle, d.speed);
          const l0 = geometric.lineLength([this.center, t0]);

          const reflected = geometric.angleReflect(d.angle, wallVertical ? 90 : 0);
          const t1 = geometric.pointTranslate(d.pos, reflected, d.speed);
          const l1 = geometric.lineLength([this.center, t1]);

          if (l1 < l0) d.angle = reflected;
        }

        d.pos = geometric.pointTranslate(d.pos, d.angle, d.speed);
      }
    }
  }

  // Initiate a simulation
  const mySimulation = (_ => {
    const simulation = new Simulation;
    
    // Initialize this simulation with simulation.init
    // You can pass an optional configuration object to init with the properties:
    //   - width
    //   - height
    simulation.init();
    
    // We'll create 100 circles of random radii, moving in random directions at random speeds.
    for (let i = 0; i < 100; i++){
      const radius = d3.randomUniform(4, 10)();
      
      // Add a circle to your simulation with simulation.add
      simulation.add({
        speed: d3.randomUniform(1, 3)(),
        angle: d3.randomUniform(0, 360)(),
        pos: [
          d3.randomUniform(radius, simulation.width - radius)(),
          d3.randomUniform(radius, simulation.height - radius)()
        ],
        radius
      });
    }
    
    return simulation;
  })();

  // Draw the simulation
  const wrapper = document.getElementById("simulation");
  const canvas = document.createElement("canvas");
  canvas.width = mySimulation.width;
  canvas.height = mySimulation.height;
  canvas.style.background = "black"; 
  wrapper.appendChild(canvas);
  const ctx = canvas.getContext("2d");
  ctx.fillStyle = "steelblue";
  ctx.strokeStyle = "white";

  function tick(){
    requestAnimationFrame(tick);
    ctx.clearRect(0, 0, mySimulation.width, mySimulation.height);

    // The simulation.tick method advances the simulation one tick
    mySimulation.tick();
    for (let i = 0, l = mySimulation.data.length; i < l; i++){
      const d = mySimulation.data[i];
      ctx.beginPath();
      ctx.arc(...d.pos, d.radius, 0, 2 * Math.PI);
      ctx.fill();   
      ctx.stroke();
    }
  }
  tick();

  </script>
</body>
</html>