Special Effect: Moving to a Specified Point

In these notes you will learn:

  • How to make an object move to clicked point.
  • How to implement easing.

Introduction

In these notes we will see how to make an object move to a point on the screen where the user clicks.

Planning the Program

Lets write a demo program that makes a ball move smoothly to a point the user clicks. To keep things simple, we won’t worry about dragging or collisions with the screen edges.

One subtle point to consider is what to do when the user clicks on a new point while the ball is moving. Should the ball start moving to the new point, or should it ignore the click and continue uninterrupted? There is no right or wrong answer: it depends on what you want your program to do. We’ll assume the program should ignore clicks while the ball is moving.

Moving to a Clicked Point

Here’s is a first program that lets the user move a ball by clicking on points on the screen:

float x, y;
float diam;
float dx, dy;

// (goalX, goalY) is the new point the ball is moving to
float goalX;
float goalY;

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

  x = 250;
  y = 250;
  dx = 0;
  dy = 0;
  diam = 60;
}

void draw() {
  background(255);
  noStroke();

  // draw the circle
  fill(255, 0, 0);
  ellipse(x, y, diam, diam);

  // draw the goal point
  fill(0, 255, 0);
  ellipse(goalX, goalY, 10, 10);

  // move the circle
  x += dx;
  y += dy;

  // stop moving when the center of the ball is
  // less than two pixels away from the goal point
  if (dist(x, y, goalX, goalY) <= 2) {
    dx = 0;
    dy = 0;
  }
}

void mousePressed() {
  // the ball is not moving if both dx and dy are 0
  if (dx == 0 && dy == 0) {
    goalX = mouseX;
    goalY = mouseY;

    // d is the distance the ball must travel to reach the goal point.
    // To move 1 pixel in the direction of (goalX, goalY), move
    // (goalX - x) / d pixels along the x-axis, and (goalY - y) / d
    // pixels along the y-axis.
    float d = dist(x, y, goalX, goalY);
    dx = 2 * (goalX - x) / d;
    dy = 2 * (goalY - y) / d;

    println("mousePressed() called:");
    println("   dx = " + dx + ", dy = " + dy);
    println("   goalX = " + goalX + ", goalY = " + goalY);
  }
}

Notice a couple of things:

  • In the draw() function we use an if-statement to check if the ball is less than 2 pixels away from the goal point. If it is, then we stop it by setting its velocity to 0.

  • In the mousePressed() function we use an if-statement to check if the ball is not moving, i.e. if both dx and dy are 0. If the ball is not moving, then a new goal point is set and dx and dy are initialized.

  • Also in mousePressed, we set dx and dy like this:

    float d = dist(x, y, goalX, goalY);
    dx = 2 * (goalX - x) / d;
    dy = 2 * (goalY - y) / d;
    

    d is the distance the ball must travel to get to the goal point. It turns out that if you move (goalX - x) / d in the x-direction and (goalY - y) / d in the y-direction, then you are moving towards (goalX, goalY) at a rate of 1 pixel per call to draw() (the justification for this fact relies on the geometry of similar triangles). To move at a rate of 2 pixels per call to draw(), multiply both (goalX - x) / d and (goalY - y) / d by 2 as we’ve done in the code.

Easing

You may have noticed in the above program that the ball always moves at the same constant velocity. While this might be what you want, real-life objects rarely start and stop instantly. Instead, there is a short period of acceleration when they start, and a short period of deceleration when they stop.

Lets add this behavior to our program. As the ball gets closer and closer to the goal point, it should move slower and slower, eventually stopping completely.

So instead of moving at a constant speed, the ball decelerates as it gets closer to its goal.

This trick of using the distance from an object to control an object’s speed is known as easing.

Implementing Easing

Here’s a version of the above program that implements easing when the ball moves towards the target (there is no easing when the ball starts!):

float x, y;    /// center of the ball
float diam;
float dx, dy;  // ball's velocity

// (goalX, goalY) is the new point the ball is moving to
float goalX, goalY;

float maxSpeed;    // fasted speed the ball can go
float easingLimit; // distance from goal when easing starts

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

  // the ball starts in the center of the screen
  x = 250;
  y = 250;
  diam = 60;

  dx = 0;
  dy = 0;

  maxSpeed = 4;
  easingLimit = 100;
}

void draw() {
  background(255);
  noStroke();

  //
  // draw the circle
  //
  fill(255, 0, 0);
  ellipse(x, y, diam, diam);

  //
  //  dx != 0 || dy != 0 is true just when the ball is moving
  //
  //  ||  means "or" and != means "not equal to"
  //
  if (dx != 0 || dy != 0) {
    // draw the goal point the ball is moving towards;
    // the gray "halo" shows the easing limit where the
    // ball's velocity starts to decrease
    fill(0, 255, 0);
    ellipse(goalX, goalY, 5, 5);
    noFill();
    stroke(200);
    ellipse(goalX, goalY, easingLimit, easingLimit);
  }

  // move the ball
  x += dx;
  y += dy;

  // if the distance from the ball's center to the goal is less
  // than one pixel, stop the ball; or, if the ball is within
  // the easing limit of the goal, then set its speed to be
  // proportional to the distance from the goal
  float d = dist(x, y, goalX, goalY);
  if (d <= 2) {
    dx = 0;
    dy = 0;
    x = goalX;
    y = goalY;
  } else if (d < easingLimit) {
    float rate = (d / easingLimit) * maxSpeed;
    dx = rate * (goalX - x) / d;
    dy = rate * (goalY - y) / d;
  }
}

void mousePressed() {
  if (dx == 0 && dy == 0) {
    goalX = mouseX;
    goalY = mouseY;
    float d = dist(x, y, mouseX, mouseY);
    dx = maxSpeed * (goalX - x) / d;
    dy = maxSpeed * (goalY - y) / d;
  }
}

Note the following:

  • Two new variables have been added:

    float maxSpeed;
    float easingLimit;
    

    maxSpeed is the fastest the ball can move when going to a new point. When the distance between the ball and goal point is less than easingLimit, we set the ball’s velocity to be proportional to its distance from the goal.

    Note that you can get a wide variety of behaviors just by changing the values of maxSpeed and easingLimit.

  • In draw(), we now need to check if the ball is within the target’s easing limit:

    float d = dist(x, y, goalX, goalY);
    
    if (d <= 2) {
      dx = 0;
      dy = 0;
      x = goalX;
      y = goalY;
    } else if (d < easingLimit) {
      float rate = (d / easingLimit) * maxSpeed;
      dx = rate * (goalX - x) / d;
      dy = rate * (goalY - y) / d;
    }
    

    First, d, which is the distance between the ball and the goal point, is calculated. We do this because we use the distance many times in the lines that follow, and so it is more efficient — and easier to read — if we calculate it once at the start.

    Next comes an if-else-if statement:

    if (d <= 2) {
      dx = 0;
      dy = 0;
      x = goalX;
      y = goalY;
    } else if (d < easingLimit) {
      float rate = (d / easingLimit) * maxSpeed;
      dx = rate * (goalX - x) / d;
      dy = rate * (goalY - y) / d;
    }
    

    First we check if the value of d is less than 2. If so, we stop the ball by setting dx and dy to 0. Since it’s possible that the ball might not end up centered exactly on the goal point, we then explicitly center at (goalX, goalY).

    Next comes the else if part where we check to see if the ball is within the easing limit. If it is, then we set the speed to be proportional to the ball’s distance from the goal. We need to do a bit of arithmetic here to get the correct speed:

    float rate = (d / easingLimit) * maxSpeed;
    

    Since d < easingLimit when this statement is executed, the expression d / easingLimit is less than 1. When we multiply that by maxSpeed, the result is a speed less than maxSpeed that is proportional to d. In other words, speed decreases as the ball gets closer to the goal.

    Note

    We are playing fast and loose with the equations for speed and acceleration because all that matters to us is that the resulting motion looks good. We are not worried about a precise simulation of reality. If we were, we would need to be much more careful to use the proper physical equations.

  • Finally, we add a small target and “halo” to help visualize the goal point and the easing limit:

    if (dx != 0 && dy != 0) {
      fill(0, 255, 0);
      ellipse(goalX, goalY, 5, 5);
      noFill();
      stroke(0, 255, 0);
      ellipse(goalX, goalY, easingLimit, easingLimit);
      // ...
    }
    

    This provides an easy way to visually check if the program is working as expected.

Easing to a goal point with an easing halo.

Questions

  1. Explaining the basic technique of easing.

  2. Assuming easingLimit has the same value as in the program in the notes, do the following two code fragments do the same thing?

    //
    // fragment 1 (else if)
    //
    if (d < 1) {
      dx = 0;
      dy = 0;
    } else if (d < easingLimit) {
      speed = d * maxSpeed / easingLimit;
    }
    
    //
    // fragment 2 (if)
    //
    if (d < 1) {
      dx = 0;
      dy = 0;
    }
    if (d < easingLimit) {
      speed = d * maxSpeed / easingLimit;
    }
    

    Explain your answer carefully!

  3. What values of easingLimit make the two code fragments in the previous question behave differently?

  4. Assuming easingLimit has the same value as in the program in the notes, do the following two code fragments do the same thing?

    //
    // fragment 1
    //
    if (d < 1) {
       dx = 0;
       dy = 0;
    } else if (d < easingLimit) {
      speed = d * maxSpeed / easingLimit;
    }
    
    //
    // fragment 2
    //
    if (d < easingLimit) {
      speed = d * maxSpeed / easingLimit;
    } else if (d < 1) {
       dx = 0;
       dy = 0;
    }
    

    Explain your answer carefully!

  5. Explain in English what the boolean expression dx != 0 && dy != 0 tests for. Is it equivalent to the expression !(dx == 0 && dy == 0)?

Programming Questions

  1. There’s a bug hiding in the first program.

    You can see it if you change the speed of the ball to be 3 in mousePressed:

    dx = 3 * (goalX - x) / d;
    dy = 3 * (goalY - y) / d;
    

    When you try the program you should see that sometimes the ball flies past the green dot without stopping.

    Figure out what the problem is and fix it.

  2. Modify the easing program to add easing to the beginning of the ball’s journey, i.e. make the ball’s velocity start out slow and then gradually increase until it is far enough away from where it started.

  3. Modify the easing program so that if the user clicks on a new point while the ball is moving, then it will immediately start moving towards this new point.