Converting Processing CircleCollision sketch to P5JS | Doesn't bounce, just dances

edited December 2014 in p5.js

Hey, I'm converting this sketch: https://processing.org/examples/circlecollision.html I've stripped back what I can, but still can't work out why it's flipping out. I can get it working in Processing just fine.

Using P5JS in the default IDE on OS X.

var ps;

function setup() {
  createCanvas(640, 360);
  ps = new ParticleSystem();
  while(ps.particles.length < 2) {
    var x = getRandomInt(0, width);
    var y = getRandomInt(0, height);
    ps.addParticle(x, y);
  }
}

function draw() {
  background(44, 44, 45);
  ps.run();
  ps.particles[0].checkCollision(ps.particles[1]);
}

function ParticleSystem(location) {
    this.origin = createVector(width/2, height/2);
    this.particles = [];
}

ParticleSystem.prototype.addParticle = function(x, y) {
    this.particles.push(new Particle(x, y));
}

ParticleSystem.prototype.run = function() {
    var p;
    for (var i = this.particles.length - 1; i >= 0; i--) {
        p = this.particles[i];
        p.run();
    }
}

function Particle(x, y) {
  this.location = createVector(x, y);

  var r = getRandomInt(40, 100);
  this.radius = r;
  this.m = r*0.01;

  this.velocity = p5.Vector.random2D();
  this.velocity.mult(3);
}

Particle.prototype.run = function() {
    this.update();
    this.checkBoundaryCollision();
    this.display();
}

Particle.prototype.update = function() {
    this.location.add(this.velocity);
}

Particle.prototype.display = function() {
    fill(44, 44, 45);
    stroke(255, 184, 12);
    strokeWeight(5);
    ellipse(this.location.x, this.location.y, this.radius*2, this.radius*2);
}


Particle.prototype.checkBoundaryCollision = function() {
    if (this.location.x > width - this.radius) {
      this.location.x = width - this.radius;
      this.velocity.x *= -1;
    } 
    else if (this.location.x < this.radius) {
      this.location.x = this.radius;
      this.velocity.x *= -1;
    } 
    else if (this.location.y > height - this.radius) {
      this.location.y = height-this.radius;
      this.velocity.y *= -1;
    } 
    else if (this.location.y < this.radius) {
      this.location.y = this.radius;
      this.velocity.y *= -1;
    }
}

Particle.prototype.checkCollision = function(other) {
    var bVect = p5.Vector.sub(this.location, other.location);
    var bVectMag = sqrt(bVect.x * bVect.x + bVect.y * bVect.y);

    if (bVectMag < this.radius + other.radius) {

      // get angle of bVect
      var theta  = atan2(bVect.x, bVect.y);
      // precalculate trig values
      var sine = sin(theta);
      var cosine = cos(theta);

      /* bTemp will hold rotated ball positions. You 
       just need to worry about bTemp[1] position*/
      var bTemp = [
        createVector(),
        createVector()
        ];

        /* this ball's position is relative to the other
         so you can use the vector between them (bVect) as the 
         reference point in the rotation expressions.
         bTemp[0].position.x and bTemp[0].position.y will initialize
         automatically to 0.0, which is what you want
         since b[1] will rotate around b[0] */
      bTemp[1].x  = cosine * bVect.x + sine * bVect.y;
      bTemp[1].y  = cosine * bVect.y - sine * bVect.x;

      // rotate Temporary velocities
      var vTemp = [
        createVector(),
        createVector()
        ];

      vTemp[0].x  = cosine * this.velocity.x + sine * this.velocity.y;
      vTemp[0].y  = cosine * this.velocity.y - sine * this.velocity.x;
      vTemp[1].x  = cosine * other.velocity.x + sine * other.velocity.y;
      vTemp[1].y  = cosine * other.velocity.y - sine * other.velocity.x;

      /* Now that velocities are rotated, you can use 1D
       conservation of momentum equations to calculate 
       the final velocity along the x-axis. */
      var vFinal = [
        createVector(),
        createVector()
        ];

      // final rotated velocity for b[0]
      vFinal[0].x = ((this.m - other.m) * vTemp[0].x + 2 * other.m * vTemp[1].x) / (this.m + other.m);
      vFinal[0].y = vTemp[0].y;

      // final rotated velocity for b[0]
      vFinal[1].x = ((other.m - this.m) * vTemp[1].x + 2 * this.m * vTemp[0].x) / (this.m + other.m);
      vFinal[1].y = vTemp[1].y;

      // hack to avoid clumping
      bTemp[0].x += vFinal[0].x;
      bTemp[1].x += vFinal[1].x;

      /* Rotate ball positions and velocities back
       Reverse signs in trig expressions to rotate 
       in the opposite direction */
      // rotate balls
      var bFinal = [
        createVector(),
        createVector()
        ];

      bFinal[0].x = cosine * bTemp[0].x - sine * bTemp[0].y;
      bFinal[0].y = cosine * bTemp[0].y + sine * bTemp[0].x;
      bFinal[1].x = cosine * bTemp[1].x - sine * bTemp[1].y;
      bFinal[1].y = cosine * bTemp[1].y + sine * bTemp[1].x;

      // update balls to screen position
      other.location.x = this.location.x + bFinal[1].x;
      other.location.y = this.location.y + bFinal[1].y;

      this.location.add(bFinal[0]);

      // update velocities
      this.velocity.x = cosine * vFinal[0].x - sine * vFinal[0].y;
      this.velocity.y = cosine * vFinal[0].y + sine * vFinal[0].x;
      other.velocity.x = cosine * vFinal[1].x - sine * vFinal[1].y;
      other.velocity.y = cosine * vFinal[1].y + sine * vFinal[1].x;
    }
}

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min)) + min;
}

Cheers, Ben

Answers

  • edited December 2014 Answer ✓

    I've tried to make 1 myself from the original: https://processing.org/examples/circlecollision.html
    Unfortunately I've stumbled in some bug too. Although diff. from yours! :-<
    Gonna try debugging it some other time: :-\"

    P.S.: Fixed w/ v2.2 and on! B-)

    /**
     * Circle Collision (v2.41)
     * by  Ira Greenberg
     * mod GoToLoop (2014/Dec/16)
     *
     * forum.processing.org/two/discussion/8683/
     * converting-processing-circlecollision-sketch-to-p5js-doesn039t-bounce-just-dances
     *
     * processing.org/examples/circlecollision.html
     */
    
    const BG = 0100, QTY = 3, balls = Array(QTY);
    
    function setup() {
      createCanvas(800, 600);
      frameRate(60).smooth().ellipseMode(CENTER);
      noStroke(), fill(Ball.COLOR);
    
      instantiateBalls();
    }
    
    function draw() {
      background(BG);
    
      for (var j, i = 0; i != QTY; ++i) {
        balls[j = i].script();
        while (++j < QTY)  if (balls[i].checkCollision(balls[j]))  break;
      }
    }
    
    function mouseClicked() {
      mouseButton == LEFT || instantiateBalls();
    }
    
    function instantiateBalls() {
      balls[0] = new Ball(random(75, width-75), random(75, height-75), 75);
      balls[1] = new Ball(random(25, width-25), random(25, height-25), 25);
      balls[2] = new Ball(random(50, width-50), random(50, height-50), 50);
    }
    
    // class Ball:
    function Ball(x, y, r) { // class Ball's constructor
      this.pos = createVector(x, y);
      this.vel = p5.Vector.random2D().mult(Ball.SPEED);
      this.rad = r, this.diam = 2*r, this.m = r/10;
    
      return Object.freeze(this);
    }
    
    Ball.SPEED = 8, Ball.COLOR = 0320; // class Ball's constants
    
    // class Ball's methods:
    Ball.prototype.script = function() {
      this.update(), this.checkBoundary(), this.display();
    };
    
    Ball.prototype.display = function() {
      ellipse(this.pos.x, this.pos.y, this.diam, this.diam);
    };
    
    Ball.prototype.update = function() {
      this.pos.add(this.vel);
    };
    
    Ball.prototype.checkBoundary = function() {
      if (this.pos.x > width-this.rad  | this.pos.x < this.rad)
        this.vel.x *= -1, this.update();
    
      if (this.pos.y > height-this.rad | this.pos.y < this.rad)
        this.vel.y *= -1, this.update();
    };
    
    Ball.prototype.checkCollision = (function() {
      const bTmp = Object.freeze([createVector(), createVector()]), bEnd = bTmp;
      const vTmp = Object.freeze([createVector(), createVector()]), vEnd = vTmp;
    
      var θ, sine, cosine;
    
      function rotX(vec, sig) {
        return cosine*vec.x + sig*sine*vec.y;
      }
    
      function rotY(vec, sig) {
        return cosine*vec.y - sig*sine*vec.x;
      }
    
      return function(other) {
        const bVec = bTmp[1].set(other.pos).sub(this.pos);
        if (bVec.mag() >= other.rad + this.rad)  return false;
    
        θ = bVec.heading(), sine = sin(θ), cosine = cos(θ);
        bVec.set(rotX(bVec, 1), rotY(bVec, 1), 0);
    
        vTmp[0].set(rotX(this. vel, 1), rotY(this. vel, 1), 0);
        vTmp[1].set(rotX(other.vel, 1), rotY(other.vel, 1), 0);
    
        const x0 = vTmp[0].x, x1 = vTmp[1].x, mt = this.m + other.m;
        bTmp[0].set( vEnd[0].x = (x0*(this.m - other.m) + 2*x1*other.m) / mt, 0, 0);
        bTmp[1].x += vEnd[1].x = (x1*(other.m - this.m) + 2*x0*this. m) / mt;
    
        bEnd[0].set(rotX(bTmp[0], -1), rotY(bTmp[0], -1), 0);
        bEnd[1].set(rotX(bTmp[1], -1), rotY(bTmp[1], -1), 0);
    
        other.pos.set(this.pos).add(bEnd[1]), this.pos.add(bEnd[0]);
    
        this. vel.set(rotX(vEnd[0], -1), rotY(vEnd[0], -1), 0);
        other.vel.set(rotX(vEnd[1], -1), rotY(vEnd[1], -1), 0);
    
        return true;
      };
    })();
    
    Object.freeze(Ball.prototype), Object.freeze(Ball);
    
  • Thanks for taking a look! Unfortunately when I run your script only one ball is created and I get the error:

    "27: Uncaught TypeError: Object # has no method 'checkCollision'"

  • edited December 2014
    • I've tested the code inside http://p5js.org/reference/ itself! /:)
    • Let's pick blue() function's reference page: http://p5js.org/reference/#/p5/blue
    • Click at "Edit" and paste my code there. Then click at "Run".
    • Make sure browser used is modern enough. Avoid old IE versions and Android browser! [-X
    • In my tests, even the old Opera 12.16 worked, although slowly. (:|
  • When you say just 'dances' do you mean it jiggles along the display boundary?

  • edited December 2014

    @quark, why don't you also follow my trick about running p5.js sketches in its own website?
    Just paste his code (or mine) there and you'll see what he means. O:-)

  • Answer ✓

    @GoToLoop thanks for the trick about running p5.js sketches in its own website - works a treat.

    I am not going to attempt to fix the code since I don't program using JavaScript but I do have a suggestion as to the cause.

    If you consider what happens when two particles collide, you first calculate the new velocity vectors which are then are applied to the particles position on the next update to get their new positions. I believe the problem is that the 2 particles are still colliding after the update so new velocity vectors are calculated but these will be in the opposite direction to the last ones. If this continues then the particles will appear to dance.

    The same thing can happen at the boundaries in that the particle remains outside the boundary jiggling back and forth because the velocity is multiplied by minus 1 on each update.

    To avoid the risk of jiggling on the boundaries I suggest you changes lines 65-82 to

    Particle.prototype.checkBoundaryCollision = function() {
        if (this.location.x > width - this.radius) {
          this.location.x = width - this.radius;
          this.velocity.x = -abs(this.velocity.x);
        }
        else if (this.location.x < this.radius) {
          this.location.x = this.radius;
          this.velocity.x = abs(this.velocity.x);
        }
        else if (this.location.y > height - this.radius) {
          this.location.y = height-this.radius;
          this.velocity.y = -abs(this.velocity.y);
        }
        else if (this.location.y < this.radius) {
          this.location.y = this.radius;
          this.velocity.y = abs(this.velocity.y);
        }
    }
    

    With regard to particle collisions you have two options

    1) When the collision is detected calculate the 'rebound' velocity vectors. Then move the two particles apart along the line joining their centres so the distance between them is > than the sum of their radii i.e. no longer colliding

    2) Use a boolean variable which is set to false when the two particles are not colliding and true when a collision has been detected. If true then still check for collision detection but do nothing if the boolean is true. It will eventually be changed back to false as they move apart. It means that the new velocity vectors are calculated just once per collision.

    Both of these solutions are based on the assumption that your code correctly calculates the rebound velocities and I am not convinced on that.

  • @quark Thanks for your input, much appreciated! Very good to know, will definitely help me avoid some of the unwanted movements.

    @GoToLoop Thanks for getting this working, I'll noodle through it and figure out where I was going wrong. Strange that it wasn't working in the P5JS IDE, so I'll investigate that also and submit a github issue if solve the problem.

  • Strange that it wasn't working in the P5JS IDE, ...

    p5.js' IDE is Mac OS X only! Seems like it's alpha and currently is at a very old version 0.1.8 from Sep 18th:
    https://github.com/antiboredom/jside/releases/

    While p5.js' complete library is v0.3.14 from Dec 10th:
    https://github.com/lmccart/p5.js/releases/

Sign In or Register to comment.