Programming for Interactive Digital Arts
CS 2, Fall 2023

11. Pseudo-physics

To make objects move, we define rules as to how to update their positions each frame. We've seen a random rule -- take a random step in the x direction and a random step in the y direction. We've also seen rules that move in predefined directions -- take a fixed-sized step in both x and y or in angle. We put "bounce" conditions in those rules so that when the motion reached the wall or the full spiral, the direction of the step was reversed.

Today we'll look at a number of other different ways to define rules of motion. We'll stick with our same basic approach for moving objects -- each frame, update the position by some amount in the x direction (x += vx;) and some amount in the y direction (y += vy;). Here we use vx and vy as the variable names for the step sizes, representing the components of the velocity in the x and y directions. We'll develop rules based on the types of equations from physics class, although there's no attempt here to be physically accurate -- we don't worry about masses, exact constants of proportionality, etc.

Gravity

First let's consider gravity. Gravity accelerates an object towards the ground. So at each time step, the velocity in the y direction increases (vy += gravity;). Suppose we use 0.1 as the value for gravity. Then if we start off with vy as 0, after the first frame the object will be moving with a vy of 0.1, after the second frame 0.2, and so forth. It moves faster and faster toward the ground (increasing y) each frame. In the following sketch you can experiement with gravity by changing its value ('g','G' keys) and seeing its effect when the ball starts with random velocity ('r' key).

[sketch1]
PhysBall ball;

function setup()
{
  createCanvas(400,400);
  smooth();
  noStroke();
  background(0);

  ball = new PhysBall(width/2,height/2,color(255)); 
}

function draw()
{
  fill(0,25); noStroke();
  rect(0,0,width,height);
  ball.draw();
  ball.update();
}

function keyPressed()
{
  if (key=='r') { // randomize the ball
    ball.x = width/2;
    ball.y = height/2;
    ball.vx = random(-5,5);
    ball.vy = random(-5,5);
  }
  else if (key=='g') { // half as much gravity
    ball.gravity *= 0.5;
    println("gravity:"+ball.gravity);
  }
  else if (key=='G') {
    ball.gravity *= 2;
    println("gravity:"+ball.gravity);
  }
}
[PhysBall]
class PhysBall {
  var x, y;          // position
  var vx=0, vy=0;    // velocity in the two directions   
  var r=10;          // radius
  color c=color(255);  // color
  var gravity=0.1;   // the amount of acceleration

  // Initialize a Ball at position (x0,y0)
  PhysBall(var x0, var y0, color c0)
  {
    x = x0; 
    y = y0;
    c = c0;
  }

  function draw()
  {
    fill(c);
    stroke(red(c)/2,green(c)/2,blue(c)/2);
    ellipse(x,y,r*2,r*2);
  }
  
  function updateVelocity() {
    // Accelerate according to gravity
    vy += gravity;
  }
  
  function bounce() {
    if (x > width-r || x < r) { 
      x = constrain(x,r,width-r); 
      vx = -vx; 
    }
    if (y > height-r || y < r) { 
      y = constrain(y,r,height-r); 
      vy = -vy; 
    }
  }

  function update()
  {
    // update the velocity
    updateVelocity();
    
    // Move in the appropriate direction by the step size
    x += vx;
    y += vy;
    
    // bounce
    bounce();
  }
}

Friction

You've probably never seen a ball bounce forever like in this last sketch. In the real world, Tthe ball gets slowed down by various frictional forces -- the air itself slows it down, as does the collision with the ground (i.e., it transfers some energy to the air and to the ground). We can extend our gravity sketch to include a term I call "drag", which slows the ball down every frame, along with a term I call "frict", which slows it down only when it bounces inside the bounce conditional). These terms are multiplicative, as opposed to gravity, which is additive. Gravity constantly accelerates an object, whereas friction just "damps" the velocity proportional to its current magnitude. The faster something is going, the more effect a damping term has. Thus it causes the ball to gradually come to rest, in kind of an "easing"-like fashion. In the follwing sketch we add these two terms to our ball example (you can control their magnitude using the 'd','D','f','F' keys for frag and friction repsectively). Note that a value of 1 means there is no damping; since the damping term keeps getting multiplied each frame, it has to be fairly close to 1 to afunction immediately halting the object.

[sketch2]
PhysBall ball;

function setup()
{
  createCanvas(400,400);
  smooth();
  noStroke();
  background(0);

  ball = new PhysBall(width/2,height/2,color(255)); 
}

function draw()
{
  fill(0,25); noStroke();
  rect(0,0,width,height);
  ball.draw();
  ball.update();
}

function keyPressed()
{
  if (key=='r') { // randomize the ball
    ball.x = width/2;
    ball.y = height/2;
    ball.vx = random(-5,5);
    ball.vy = random(-5,5);
  }
  else if (key=='g') { // half as much gravity
    ball.gravity *= 0.5;
    println("gravity:"+ball.gravity);
  }
  else if (key=='G') {
    ball.gravity *= 2;
    println("gravity:"+ball.gravity);
  }
  else if (key=='d') { // half as much drag (measured as 1-d)
    ball.drag = 1 - (1-ball.drag)*0.5;
    println("drag:"+ball.drag);
  }
  else if (key=='D') {
    ball.drag = 1 - (1-ball.drag)*2;
    println("drag:"+ball.drag);
  }
  else if (key=='f') { // half as much damping
    ball.frict = 1 - (1-ball.frict)*0.5;
    println("y friction:"+ball.frict);
  }
  else if (key=='F') {
    ball.frict = 1 - (1-ball.frict)*2;
    println("y friction:"+ball.frict);
  }
}
[PhysBall]
class PhysBall {
  var x, y;          // position
  var vx=0, vy=0;    // velocity in the two directions   
  var r=10;          // radius
  color c=color(255);  // color
  var gravity=0.1;   // the amount of acceleration
  var drag=0.99;     // multiplicative factor for velocity
  var frict=0.75;   // multiplicative factor, only when bounce
  
  // Initialize a Ball at position (x0,y0)
  PhysBall(var x0, var y0, color c0)
  {
    x = x0; 
    y = y0;
    c = c0;
  }

  function draw()
  {
    fill(c);
    stroke(red(c)/2,green(c)/2,blue(c)/2);
    ellipse(x,y,r*2,r*2);
  }

  function updateVelocity() {
    // Accelerate according to gravity
    vy += gravity;
    // Now damp according to drag
    vx *= drag;
    vy *= drag;
  }
  
  function bounce() {
    if (x > width-r || x < r) { 
      x = constrain(x,r,width-r); 
      vx = -vx; 
      vy *= frict;   // damp
    }
    if (y > height-r || y < r) { 
      y = constrain(y,r,height-r); 
      vy = -vy; 
      vy *= frict;   // damp
    }
  }

  function update()
  {
    // update the velocity
    updateVelocity();
    
    // Move in the appropriate direction by the step size
    x += vx;
    y += vy;
    
    // bounce
    bounce();
  }
}

Interaction

We can also interact with the ball by grabbing it and tossing it around. If you click with the mouse on the object, we simply disable physics updates (since the ball his grabbed) and let it follow the mouse till the mouse button is released. While we do this, we can use the last mouse position to compute the direction of mouse movement and set the velocity accordingly. This makes it feel like we are tossing the ball in the direction of the mouse (try it). This is similar to how the iPhone gestures work.

[sketch3]
PhysBall ball;

function setup()
{
  createCanvas(400,400);
  smooth();
  noStroke();
  background(0);

  ball = new PhysBall(width/2,height/2,color(255)); 
}

function draw()
{
  fill(0,25); noStroke();
  rect(0,0,width,height);
  ball.draw();
  ball.update();
}

function keyPressed()
{
  if (key=='r') { // randomize the ball
    ball.x = width/2;
    ball.y = height/2;
    ball.vx = random(-5,5);
    ball.vy = random(-5,5);
  }
  else if (key=='g') { // half as much gravity
    ball.gravity *= 0.5;
    println("gravity:"+ball.gravity);
  }
  else if (key=='G') {
    ball.gravity *= 2;
    println("gravity:"+ball.gravity);
  }
  else if (key=='d') { // half as much drag (measured as 1-d)
    ball.drag = 1 - (1-ball.drag)*0.5;
    println("drag:"+ball.drag);
  }
  else if (key=='D') {
    ball.drag = 1 - (1-ball.drag)*2;
    println("drag:"+ball.drag);
  }
  else if (key=='f') { // half as much damping
    ball.frict = 1 - (1-ball.frict)*0.5;
    println("y friction:"+ball.frict);
  }
  else if (key=='F') {
    ball.frict = 1 - (1-ball.frict)*2;
    println("y friction:"+ball.frict);
  }
}
[PhysBall]
class PhysBall {
  var x, y;          // position
  var vx=0, vy=0;    // velocity in the two directions   
  var r=10;          // radius
  color c=color(255);  // color
  var gravity=0.1;   // the amount of acceleration
  var drag=0.99;     // multiplicative factor for velocity
  var frict=0.75;   // multiplicative factor, only when bounce
  
  var grabbed=false; // whether I have been grabbed by the mouse
  
  // Initialize a Ball at position (x0,y0)
  PhysBall(var x0, var y0, color c0)
  {
    x = x0; 
    y = y0;
    c = c0;
  }

  function draw()
  {
    fill(c);
    stroke(red(c)/2,green(c)/2,blue(c)/2);
    ellipse(x,y,r*2,r*2);
    if(grabbed) {
      fill(red(c)/2,green(c)/2,blue(c)/2);
      ellipse(x,y,r,r);
    }
  }

  function updateVelocity() {
    // Accelerate according to gravity
    vy += gravity;
    // Now damp according to drag
    vx *= drag;
    vy *= drag;
  }
  
  function bounce() {
    if (x > width-r || x < r) { 
      x = constrain(x,r,width-r); 
      vx = -vx; 
      vy *= frict;   // damp
    }
    if (y > height-r || y < r) { 
      y = constrain(y,r,height-r); 
      vy = -vy; 
      vy *= frict;   // damp
    }
  }
  
  function updateGrabbed() {
    // if I am not grabbed yet, grab me if the mouse is clicked on me 
    if(!grabbed) grabbed = mousePressed && dist(mouseX,mouseY,x,y) < r;
    // otherwise stop grabbing me when I release the mouse
    else grabbed = mousePressed;
  }

  function update()
  {
    // check if grabbed
    updateGrabbed();
    
    // follow the mouse if grabbed
    if(grabbed) {
      x = mouseX;
      y = mouseY;
      vx = mouseX - pmouseX;
      vy = mouseY-pmouseY;
    } else { // obey physics otherwise
      // update the velocity
      updateVelocity();
    
      // Move in the appropriate direction by the step size
      x += vx;
      y += vy;
    
      // bounce
      bounce();
    }
  }
}

Springs

The force applied by a spring is proportional to how far it has been stretched or compressed. Thus for a vertical spring, we take the difference between the current y and some center (rest position) y. If the difference positive, the spring is stretched and the force is negative, to make it shorter. If it's negative, the spring is compressed and the force is positive, to make it longer. Thus we negate the difference, and scale it by some amount, called the spring constant, to indicate how hard to try to restore the length (the "easing" type of concept again). We can also include a damping term, for the friction in the spring.

[sketch4]
Spring spring;

function setup()
{
  createCanvas(400,400);
  smooth();
  noStroke();
  background(0);

  spring = new Spring(width/2,height/2,color(255)); 
}

function draw()
{
  fill(0,25); noStroke();
  rect(0,0,width,height);
  spring.draw();
  spring.update();
}

function keyPressed()
{
  if (key=='k') { // half as much spring constant
    spring.k *= 0.5;
    println("spring:"+spring.k);
  }
  else if (key=='K') {
    if (spring.k < 0.5) {
      spring.k *= 2;
      println("spring:"+spring.k);
    }
  }
  else if (key=='d') { // half as much damping (measured as 1-d)
    spring.d = 1 - (1-spring.d)*0.5;
    println("damping:"+spring.d);
  }
  else if (key=='D') {
    spring.d = 1 - (1-spring.d)*2;
    println("damping:"+spring.d);
  }
}
[Spring]
class Spring {
  var cx, cy;        // rest position
  var x, y;          // current position
  var vx=0, vy=0;    // velocity in the two directions   
  var r = 10;        // radius
  color c=color(255);  // color
  var k = 0.1;       // spring constant
  var d = 0.95;      // damping

  var grabbed=false; // whether I have been grabbed by the mouse
  
  // Initialize a Spring at rest position (x0,y0)
  Spring(var x0, var y0, color c0)
  {
    cx = x0; cy = x0;
    x = x0; y = y0;
  }

  function draw()
  {
    fill(c);
    stroke(red(c)/2,green(c)/2,blue(c)/2);
    line(cx,cy,x,y);
    ellipse(cx,cy,r,r);
    ellipse(x,y,r*2,r*2);
    if(grabbed) {
      fill(red(c)/2,green(c)/2,blue(c)/2);
      ellipse(x,y,r,r);
    }
  }

  function updateVelocity() {
    // Apply spring force
    vx -= k * (x-cx);
    vy -= k * (y-cy);
    // Damp
    vx *= d;
    vy *= d;
  }
  
  function updateGrabbed() {
    // if I am not grabbed yet, grab me if the mouse is clicked on me 
    if(!grabbed) grabbed = mousePressed && dist(mouseX,mouseY,x,y) < r;
    // otherwise stop grabbing me when I release the mouse
    else grabbed = mousePressed;
  }

  function update()
  {
    // check if grabbed
    updateGrabbed();
    
    // follow the mouse if grabbed
    if(grabbed) {
      x = mouseX;
      y = mouseY;
      vx = mouseX - pmouseX;
      vy = mouseY-pmouseY;
    } else {  // obey physics otherwise
      // update the velocity
      updateVelocity();

      // Move in the appropriate direction by the step size
      x += vx;
      y += vy;      
    }
  }
}

We can have a two-dimensional spring that is restored in both x and y directions. This sketch is inspired by an example in the code for chapter 4 of Greenberg.