Chapter 1

Deterministic Motion


The aim of this chapter is to introduce the basics of 2D animation provided by the p5.js library, through examples of simple deterministic motion. This will be the foundation of motion simulation that I will be discussing for the rest of the project.

1.1 Phenomena

One of the most important and elementary features of life is the ability to move. Therefore it is essential for us to be able to simulate entities moving across the screen (our environment). But before simulating complex life-like motion, let us first simplify the problem to simulating simple motion - like an object moving along a straight line. This allows us to understand the modelling and implementation of simulating motion in our p5 sketches (these are explained further below) and extend the ideas to any kind of motion simulation.

We will build our simulations in this chapter around one of the key ideas of classical mechanics - Forces. We also ask the question: how does the existence of external forces such as gravity, air resistance, drag and friction affect the motion of inanimate objects? Further, how can we define such characteristics in our virtual environment such that the resulting simulations are akin to that seen in the real physical world? These are two sides of the same coin that we will continue to explore - break down phenomena into simple pieces by coding and picking out important parameters of the phenomena from the code.

Viewing motion simulation in such a manner allows us to move smoothly from simple animations, i.e. motion of an object in a straight line or a stationary object, to simple but naturally occurring physical phenomena, such as a ball being dropped to the ground from some height under the influence of gravity, parabolic motion of a thrown object, etc.

And finally the one shown below may not be a naturally occurring phenomenon, but to justify its implementation in code, I state the following: have we all not played around with a bouncing ball - throw it at a particular angle and see what reaction we get from the ball and its interactions with the wall, floor and household objects. Therefore, it's a simple yet an informative experiment to see the influence of gravity, collision and friction on a moving object.

1.2 Modeling

First, in order to simulate the effect of forces on an object, we need to represent the object in our virtual environment - the screen - or the p5 canvas (or just canvas). To simplify the physics - hence shortening code lengths, and to simplify the visualisation of our object, we will use a circle to represent our object and will be considered to be a point-mass object (where all the mass is concentrated at the centre)1

Now that we have the canvas (environment) and the circle (entity/object), we also need to represent time. This is done through redrawing on our canvas every frame.
For example, let’s say we initially draw the circle at the centre of canvas, then we would erase that drawing and draw the circle again a little bit to the right, repeat this a few times moving the circle a little bit to the right each time. Now show each of these drawings one by one at a particular speed (i.e. frame rate). If the speed is fast enough and the change in position is close enough, then voilà! You have your circle moving from left to right seemingly continuous to the eye.
This is precisely how animated motion pictures are made, and one can think of this as the digital analog. Note that this method of simulating motion is not something that has magically appeared, but from our intuitive understanding of motion in terms of displacement.

Displacement is just the change in position over time. Now, of course, in order to move to the continuous world, we would have to take limits of the change (in position) and derive the velocity at every instant, but that is impossible since the continuum is uncountable (i.e., you can never complete calculating velocity at ‘every instant’). But such instantaneous calculations are unnecessary for all practical purposes, since our brain cannot tell the difference visually between a continuous motion over 2 seconds versus many discrete steps of motion over 2 seconds2. Now, since it is in every frame that we change the position of the object, a convenient and natural unit of time is the frame rate. Similarly, since we also change the position of our object in terms of pixels3, we will consider the unit of distance to be pixels (or px). Thus any mention of value without attached units must be interpreted respectively. E.g. speed = 55 , means that we are setting the variable ‘speed’ to be 55 px/f

1.2.2 Modelling of physics:

The topic of physics we are engaging with - dynamics in 2 dimensions - uses both magnitude and direction to describe motion phenomena. Therefore, following suit with the usual textbook method of modelling these dual-parameter notions, we shall use vectors, since we are also already using a Cartesian plane to describe the position of entities.

Vectors are not only a useful tool to quantify relative position, through vector subtraction, but also a compact way of describing change. There are two ways in which we represent these vectors - Cartesian and Polar coordinates - either might be used depending on the situation and perspective required.

For example, if I have to move from point A to B, we first embed the positions of A and B, as vectors relative to the origin - which is already given to us in the usual Cartesian representation.
Now all we have to do is find out the units along the xx-axis (let’s call it xx) and the no. of units along the y-axis is B from A (this can also be done through vector subtraction of the position vectors of A and B). Now the vector (x,y)(x,y) precisely holds the information about how far and in what direction is B from A.

(Intend to add a diagram, that shows how two vectors are added and subtracted)

Therefore, a simple vector addition of (x,y)(x,y) to the position vector A is how we will linearly move an object from A to B in one frame.
But, suppose the two points A and B are much further apart, and we want to only reach B in 200 frames then we can scale down the magnitude of (x,y)(x,y) by 200 - using scalar multiplication to get (x,y)(x’, y’), then (x,y)(x’,y’) is precisely the velocity vector, therefore simply by adding (x,y)(x’,y’) to the current position vector at each frame - and setting the resultant vector as the new position vector for the next frame - we can produce a simulation / animation of an object moving from A to B along a straight line.

Similarly, any other quantity in motion phenomena that has both magnitude and direction will also be represented through vectors in our simulations.
This also allows manipulation and interaction of these different quantities - force (Gravitation, friction), acceleration, velocity, position through the means of vector addition, subtraction and scalar multiplication as follows:

In one frame (1 unit of time) given that our object is at current position, current velocity

  1. Suppose we have external forces f1,f2f_1, f_2, then by the second law of motion, we have:
    acceleration = (f1+f2)/mf_1 + f_2) / m = sum of external forces / mass

  2. (New Velocity - Current velocity) / time = acceleration, which implies
    New velocity = Current velocity + Acceleration

  3. Similarly, New position = Current position + new velocity

  4. Draw our next frame with the object in New position, and repeat from 1. unless explicitly stopped

The above method is commonly known as Euler Integration, although it is not the most accurate method that is used for describing continuous motion, it is good enough for the motion phenomena that were simulated for the project. This is mostly because our major requirements are smoothness - visually, and easy implementation - computationally.
It is likely that the above conditions are met because the change in quantities over each frame is relatively small, the errors - and hence the departure from real-life behaviour is visually minimal.

(Intend to add a picture that shows difference between curve that results from Euler-integration and the actual integral curve - and see what happens when step size is small)

Therefore, the above sequence works as a model of simulating dynamics of physical objects - with an explicit usage of the 2nd law, and an implicit use of the 1st law of motion - where an object’s velocity will not change unless an external force acts on it.

The choice of using forces active in every frame to compute a new position for the next frame, allows us to model multiple kinds of forces, within the same structure (the loop). Eg: constant forces in the environment - gravity, changing forces in simulation - wind force, activated forces under condition - friction, buoyancy.

1.3 Implementation

1.3.1: Moving object along a straight line

To draw a circle at the position (x,y)(x,y) with diameter d on our canvas, we use the built-in p5 function circle(x, y, d)

function setup(){  
    createCanvas( windowWidth, windowHeight );  
    background( 225 );  
    circle(10, 250, 50);  
}

Now to move the circle from the centre of the screen (width/2 height/2) to say 400 px to the right then we write the following code

let x_coord
let y_coord 
let timeTaken
let speed

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(255);
  fill(100);

  x_coord = width/2;
  y_coord = height/2;

  // Change the time taken here (in #frames), to see how it affects the animation
  // Note that this implementation is not used in our simulation below
  // This is just to show other ways of input that is possible
  timeTaken = prompt("Enter the time taken (in px) for the circle to move", 400);
  speed = 400/timeTaken

  circle(x_coord, y_coord, 20);
}

function draw() {
  // If condition used here to stop the draw loop() after we have arrived at our destination
  if (x_coord <= width/2 + 400){
    background(255);

    // Since we are only moving along x-axis, we can ignore the y-coordinate
    x_coord += speed;
    // If we want to move to a location that is not along either axis relative to initial position
    // Then we would have to calculate the slope of the line joining the locations and
    // use that to manipulate the y-coord i.e., 
    // y_coord += slope * speed;   [where slope = change in y / change in x]

    circle(x_coord, y_coord, 20);
  }
  // `noLoop()` will stop the draw loop
  else{ noLoop(); }
}

1.3.2 Vector implementation:

To create a of vector object (which is called p5.Vector internally in p5) we use:
let vec = createVector(30, 200)
Thus vec represents the position vector pointing from the origin to (30,200)(30, 200)

Basic Vector operations:
Where v1v_1, v2v_2 are vectors and a is a real number

Note that in v1.function() , function() is called as a method of the class. Any name that appears as v1.name is a property of the class that v1v_1 represents, the ‘ . ’ allows us to access those properties - thus we are able to call methods like add() since they belong to the p5.Vector class. To view all the methods please check the p5 reference.

1.3.3 Physical entities and classes:

We will implement physical entities (i.e., laws of physics obeying objects) with the help of a Mover class - Classes are how we define a new type of object in js and give it structure - with its own properties and functions.

Below is an example of how the class is implemented, where we are trying to simulate a ball, bouncing around in an enclosed space. Parameters and characteristics of the simulation are:

  1. Canvas size - 500×400500 \times 400 - chose so that the ball may hit the edges more often
  2. Initial velocity = (3,2)(3, -2) px/f -roughly simulates just tossing a ball towards a wall
  3. Collision with walls, floor - inelastic in nature

The constants - friction coefficient, gravitational acceleration, percentage of energy lost in after wall-collision are all chosen after experimentation with the visualisation produced by each of these quantities.
The mass of the object is an arbitrary quantity - only there to determine the size of the object and scaling of gravitational force. Note that we increase the gravitational force for heavier objects, in order to simulate what we see in usual environments - heavier objects fall faster relative to lighter ones (although this isn’t because of gravitation but rather other external forces like air resistance).

This is the basic structure of our class:

// Program to simulate a bouncing ball under the influence of gravity and frictional forces
class Mover {
  /* This is the syntax for creating a class, now within this block 
  we can define and attach properties and functions (methods) to the class
  by using the 'this' keyword */

  // The constructor sets the initial values that are required after instantiation
  // for the Mover class we require, x and y coordinates, and mass of the object

  constructor(x, y, m) {
    // The velocity chosen such that it showcases all the features of the program

    this.pos = createVector(x, y);
    this.vel = createVector(2, -3);
    this.acc = createVector();    // Creates a vector of default values (0,0)
    this.mass = m;
    this.r = m * 0.875;   // Object radius ∝ mass - i.e., assuming bigger ⇒ heavier
    this.D = this.r * 2;    // Diameter - used as argument for drawing circle
  }

  // Inside classes, function/methods don't need the 'function' keyword,
  update() {
    // The below three lines implement the physical laws as sequenced in the modelling section
    // This implementation will be used throughout all simulations as part of the update() method

    this.vel.add(this.acc);
    this.pos.add(this.vel);
    this.acc.mult(0);       //Acceleration is reset - only affected by force(s) at that frame

    // bounceEdges() is called after the reset, since the effect of friction
    // applied in bounceEdges() is only used in the next frame 
    this.bounceEdges();
  }

  show() {
    circle(this.pos.x, this.pos.y, this.D);
  }

  applyForce(force) {
    // Since gravitational acceleration is constant, we have to create a copy
    // And apply second law to the copied vector
    let f = p5.Vector.div(force, this.mass);
    this.acc.add(f);
  }

  bounceEdges(fric_Force = true) {
    if (this.pos.y + this.r > height) {
      this.vel.y *= -0.85;
      this.pos.y += height - (this.pos.y + this.r);

      if (fric_Force) {
        // Formula used is fric = µN (-û) where 
        // µ - frictional coeffecient, N- Normal force, û - unit vector along current velocity 
        // Basically friction is a force acting along the opposite direction of the velocity

        let fric = p5.Vector.setMag(this.vel, fric_coeff); 
        fric.mult(-1);
        this.applyForce(fric);
      }
    } else if (this.pos.y - this.r < 0) {
      this.vel.y *= -0.85;
      this.pos.y -= this.pos.y - this.r
    }

    if (this.pos.x + this.r > width) {
      this.vel.x *= -0.95;
      this.pos.x += width - (this.pos.x + this.r);
    } else if (this.pos.x - this.r < 0) {
      this.vel.x *= -0.95;
      this.pos.x -= this.pos.x - this.r;
    }
  }
}

let mv;   // The mover instance decleration
let gravity;
const fric_coeff = 0.075;

function setup() {
  createCanvas(400, 400);
  fill(100);
  gravity = createVector(0, 0.1);
  mv = new Mover(200, 80, 20);

}

function draw() {
  background(220);
  mv.show();

  // Applying any universal forces in the simulation - in this case just gravity
  // But since grativational force actually scales according to mass, we pass a new vector at each frame
  // i.e., our constant gravity vector multiplied by the mass of the mover
  mv.applyForce(p5.Vector.mult(gravity, mv.mass));


  mv.update();

}

This structure serves as a template for any kind of moving object - a method to display our mover, a method to update its position - using the 2nd law, and other methods that take care of detecting and processing forces.

This is how an instance of the object is made and used in our sketches:

let mv;   // The mover instance decleration
let gravity;
const fric_coeff = 0.075;

function setup() {
  createCanvas(400, 400);
  fill(100);
  gravity = createVector(0, 0.1);
  mv = new Mover(200, 80, 20);

}

function draw() {
  background(220);
  mv.show();

  // Applying any universal forces in the simulation - in this case just gravity
  // But since grativational force actually scales according to mass, we pass a new vector at each frame
  // i.e., our constant gravity vector multiplied by the mass of the mover
  mv.applyForce(p5.Vector.mult(gravity, mv.mass));


  mv.update();

}

Where mv = new Mover(200, 80, 20) is the syntax to create an object of type Mover and x=200x = 200, y=80y = 80 and mass =20= 20 is passed to the constructor, which then creates all the initial required values.

Now the main method, that takes care of processing forces - is applyForce()

  applyForce(force) {
    // Since gravitational acceleration is constant, we have to create a copy
    // And apply second law to the copied vector
    let f = p5.Vector.div(force, this.mass);
    this.acc.add(f);
  }

Here p5.Vector.div() is a static method. Static methods are used when we want to return a new instance of the Class using an already existing instance (here f is used) over manipulating property values of that particular instance ( then we will use f.div() ).

The secondary method of our Mover class is the bounceEdges() method. It checks if at the next frame or the updated position before displaying is beyond the edges of the screen. If so, it repositions the object right at the edge, and reduces the velocity to simulate a inelastic collision

  // Implementation of above logic
  if (this.pos.x + this.r > width) {
      this.vel.x *= -0.95;
      this.pos.x += width - (this.pos.x + this.r);
    } else if (this.pos.x - this.r < 0) {
      this.vel.x *= -0.95;
      this.pos.x -= this.pos.x - this.r;
    }

and if the edge is the lower one (floor) then, we also inform my Mover, that frictional force must also be added, along with the other forces in the next frame through this.applyForce(fric).4

bounceEdges(fric_Force = true) {
  /*
  code from prev block goes here
  ...
  */

  if (this.pos.y + this.r > height) {
    this.vel.y *= -0.85;
    this.pos.y += height - (this.pos.y + this.r);

    if (fric_Force) {
      // Formula used is fric = µN (-û) where 
      // µ - frictional coeffecient, N- Normal force, û - unit vector along current velocity 
      // Basically friction is a force acting along the opposite direction of the velocity

      let fric = p5.Vector.setMag(this.vel, fric_coeff); 
      fric.mult(-1);
      this.applyForce(fric);
    }
  }
}

In the above snippet, similar to p5.Vector.div(), here p5.Vector.setMag(this.vel, fric_coeff) - takes the the velocity of our object, and scales the magnitude to be fric_coeff.

Putting it together we get this :)

Notes:

  1. Using a circle would also simplify calculations to do with contact - enables us to ignore surface area in calculations we do.

  2. Although the scope of the nuances behind this claim is beyond this thesis, according to the current knowledge of visual processing, the information of shape, depth, colour and motion are handled separately and finally seen as an image to us. The speed at which this information can be processed therefore limits the ability to distinguish between two still images and one continuous motion. And this line seems to be drawn at around 10 to 12 images per second (i.e. 10-12 fps).
    But the kind of motion we see depends on intensity and frequency of sustained light and length of darkness

    This is one of the numerous tricks that simulation uses - where the fundamental limits of ourselves help in decreasing required precision and therefore reducing computational overheads whenever possible. One can think of this as also discovering the required parameters of our simulation so that we can simulate real life-like motion to the extent needed. One more technique that makes use of the limits of our eye is anti-aliasing - allows us to define and use real coordinates in a discrete world

  3. Note that pixels in various contexts mean different things, but CSS sets a standard definition of relative pixel dimensions, which is used in JS and hence p5.js as well. Relative means that - if object travels 5 px/frame - regardless of absolute pixel resolution of two different monitors - it will cross a canvas of width 400 px in 80 frames

  4. Multiple other implementation orders were tried, but this is the one that with simplest, small code and the one that makes sure friction is only applied once when the ball is close to the ground