Variations on Dots and Lines

In these notes you will learn:

  • Simple techniques for simulating gravity and elasticity with an array of points.
  • How to drag and drop points stored in an array.

Introduction

The dots and lines notes showed how to write a program that made a trail of dots and lines wherever the user clicked on the screen. Here we will see a couple of neat variations on that program.

Throughout these notes we will be modifying the OOP version of the dots and lines program.

Variation 1: Bouncing Points

Here’s what we would like to have happen: after the user has clicked as many points on the screen as they like, they press any key and instantly the points start falling downwards. When they hit the bottom of the screen they bounce up like they were rubber balls. The lines stay connected to the moving balls.

We’ve already seen how to do this with a single ball in the gravity notes. The program we will write here is essentially a combination of those notes and the dots and lines program

Here’s the code:

class Point {
  float x, y;
  float dx, dy;


  Point() {   // default constructor
    x = 0;
    y = 0;
    dx = 0;
    dy = 0;
  }

  Point(int a, int b) {  // constructor
    x = a;
    y = b;
    dx = 0;
    dy = 0;
  }
}

ArrayList<Point> points;

float gravity;
float elasticity;

void setup() {
  size(500, 500);
  smooth();

  points = new ArrayList<Point>();
  gravity = 0;
  elasticity = 0;
}

void draw() {
  background(255);

  int n = points.size(); // n is the number of points

  // draw the lines
  if (n > 1) {
    stroke(0);
    int i = 1;
    while (i < n) {
      Point p = points.get(i - 1);
      Point q = points.get(i);
      line(p.x, p.y, q.x, q.y);
      ++i;
    }
  }

  // draw the dots
  if (n > 0) {
    fill(255, 0, 0);
    noStroke();
    for(Point p : points) {
      ellipse(p.x, p.y, 10, 10);
    }
  }

  // update the dot positions
  for(Point p : points) {
    p.x += p.dx;
    p.y += p.dy;

    p.dy += gravity;

    if (p.y >= 500) {
      p.dy = -(p.dy * elasticity);
      p.y = 499;
    }
  }
} // draw

void mousePressed() {
  Point p = new Point(mouseX, mouseY);
  points.add(p);
}

void keyPressed() {
  gravity = 0.1;
  elasticity = 0.8;
}

Be sure to try this program: the collapsing effect is quite neat!

Note the following:

  • We’ve added dx and dy variables to the points to allow them move.
  • The gravity and elasticity variables are used in the same way as in the earlier notes on gravity. Gravity controls how fast the balls fall down, and elasticity controls how far they bounce up after hitting the ground.
  • Gravity only comes into effect after the keyPressed() function is called, i.e. after the user presses any key. Before that, gravity is 0 and so has no effect on the points.

Variation 2: Dragging and Dropping the Points (Optional)

In this variation, we want to allow the user to drag-and-drop any of the points they have placed on the screen. The idea is to combine the previous program with the ideas from the drag and drop notes from earlier in the course.

One of the problems we will face is handling overlapping points. When two, or more, points overlap, we must be careful to drag only one of them. The way we will handle this is to use a new variable called pointBeingDragged that records the point currently being dragged. If no point is being dragged, then we will set pointBeingDragged to null.

Using pointBeingDragged, we can then easily ensure we are always dragging the correct point.

Another detail we need to think about is what to do when the user clicks on a point (when no point is being dragged). In the previous program, a new point was drawn even if it was on top of an old one. However, in this program if the user clicks on an existing point, we will assume they want to drag it and so will not create a new point.

With those details in mind, here is the re-written code:

class Point {
  float x, y;
  float dx, dy;
  float diam;

  // default constructor
  Point() {
    x = 0;
    y = 0;
    diam = 20;
    dx = 0;
    dy = 0;
  }

  Point(int a, int b) {
    x = a;
    y = b;
    diam = 20;
    dx = 0;
    dy = 0;
  }
}

ArrayList<Point> points;
Point pointBeingDragged;  // initiall null
float gravity;
float elasticity;

void setup() {
  size(500, 500);
  smooth();

  points = new ArrayList<Point>();
  gravity = 0;
  elasticity = 0;
}

void draw() {
  background(255);

  int n = points.size(); // n is the number of points

  // draw the lines
  if (n > 1) {
    stroke(0);
    int i = 1;
    while (i < n) {
      Point p = points.get(i - 1);
      Point q = points.get(i);
      line(p.x, p.y, q.x, q.y);
      ++i;
    }
  }

  // draw the dots
  if (n > 0) {
    fill(255, 0, 0);
    noStroke();
    for(Point p : points) {
      ellipse(p.x, p.y, p.diam, p.diam);
    }
  }

  // update the dot positions
  for(Point p : points) {
    p.x += p.dx;
    p.y += p.dy;

    p.dy += gravity;

    if (p.y >= 500) {
      p.dy = -(p.dy * elasticity);
      p.y = 499;
    }
  }
} // draw

void mousePressed() {
  // first check to see if the user has clicked inside an existing point;
  // if so, then we assume they want to drag it and not create a new point
  for(Point p : points) {
    if (pointInCircle(mouseX, mouseY, p.x, p.y, p.diam / 2)) {
      pointBeingDragged = p;
      return;  // ends the mousePressed() function
    }
  }

  // only reach here if the user has not clicked in an existing point
  Point p = new Point(mouseX, mouseY);
  points.add(p);
}

void keyPressed() {
  gravity = 0.1;
  elasticity = 0.8;
}

void mouseDragged() {
  if (pointBeingDragged != null) {
    Point p = pointBeingDragged;
    p.x = mouseX;
    p.y = mouseY;
  }
}

void mouseReleased() {
  pointBeingDragged = null;
}

boolean pointInCircle(float x, float y, float a, float b, float r) {
  if (dist(x, y, a, b) <= r) {
    return true;
  }
  else {
    return false;
  }
}

Notice the following:

  • When the user clicks the mouse, there are two possibilities: they either want to create a new point, or drag an existing point. We distinguish between these two choices by checking if the user clicks on an existing point. If they do, we assume they want to drag it:

    void mousePressed() {
      // first check to see if the user has clicked inside an existing point;
      // if so, then we assume they want to drag it and not create a new point
      for(Point p : points) {
        if (pointInCircle(mouseX, mouseY, p.x, p.y, p.diam / 2)) {
          pointBeingDragged = p;
          return;  // ends the mousePressed() function
        }
      }
    
      // only reach here if the user has not clicked in an existing point
      Point p = new Point(mouseX, mouseY);
      points.add(p);
    }
    

    This first step is to check to see if the click is inside an existing point. We do that using a for-each to call pointInCircle on all the objects in points. If the mouse is within a point, then we record that point using pointBeingDragged. The return statement on its own line causes the function to end immediately.

    If the click is not inside an existing point, then we assume the user wants to create a brand new point. The code for this is the same as in the previous program.

  • The mouseDragged function is called automatically when the mouse is being dragged (i.e. when the mouse is moving an a button is being pressed):

    void mouseDragged() {
      if (pointBeingDragged != null) {
        Point p = pointBeingDragged;
        p.x = mouseX;
        p.y = mouseY;
      }
    }
    

    We can only drag one point at a time, and pointBeingDragged stores the point currently being dragged.

    However, it is possible that the user is dragging the mouse without having first selected a point, e.g. they could start dragging on some place on the screen that doesn’t have a point. In that case pointBeingDragged will be null, which means there is nothing to move. So the if-statement in mouseDragged is needed to check for this possibility.

  • Dragging ends when the mouse is released:

    void mouseReleased() {
      pointBeingDragged = null;
    }
    

    mouseReleased is automatically called whenever a mouse button is released, and for us that means we want pointBeingDragged to be set -1.

Questions

  1. Describe what a return statement on its own line does:

    return;
    
  2. Is the if-statement in mouseDragged necessary? What goes wrong if it (and its matching }) is removed?

  3. The variable pointBeingDragged is quite a long name. Why is that a good thing in this program? Why would a short variable name, like d, not be as good?

Programming Questions

  1. Modify the final variation 2 program so that the point being dragged is green (and the rest of the points are red).

  2. When the balls drop in the sample programs above, they bounce lower and lower until they settle on the bottom edge. However, they do not lie still: they constantly jiggle around.

    Fix this: modify the second sample program (the one with drag-and-drop) so that after the balls drop they eventually become completely still at the bottom of the screen.