Special Effect: Dragging and Dropping

In these notes you will learn:

  • How to implement drag-and-drop using the mouseDragged function.
  • How to drag an object while keeping the mouse pointer fixed on the drag point.

Introduction

A standard user interface effect is “dragging” an object around the screen by clicking and holding it. For example, in a window interface you can delete a file by dragging its icon into a trash bin. In this note we will see how to implement basic drag-and-drop in Processing.

Planning the Program

Let’s write a program that lets the user drag a ball around the screen. To be clear, let’s say exactly what we mean by dragging and dropping. Dragging means that the user clicks the mouse pointer somewhere on the ball and then, while still holding the mouse button down, moves the mouse. As the mouse moves, the circle moves. The “dropping” part occurs when the user releases the mouse button, and the circle detaches from the pointer and stays where it is.

Dragging and dropping is such a common operation that Processing provides the special mouseDragged() function to handle it. This function is automatically called when the user “clicks and holds” the mouse, i.e. when they do a drag-and-drop action.

The Program

Here is our first drag-and-drop program:

class Sprite {
  float x;
  float y;
  float dx;
  float dy;
}

Sprite ball = new Sprite();
float diam;

void setup() {
  size(500, 500);
  smooth();
  ball.x = 250;
  ball.y = 250;
  diam = 125;
}

void draw() {
  background(255);
  noStroke();
  fill(255, 0, 0);
  ellipse(ball.x, ball.y, diam, diam);
}

void mouseDragged() {
  if (pointInCircle(mouseX, mouseY, ball.x, ball.y, diam / 2)) {
    ball.x = mouseX;
    ball.y = mouseY;
  }
}

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

A Small Problem

You may have noticed when playing with the previous program that if you click near the edge of the circle, then its center immediately jumps to the mouse pointer location. It seems unnatural and is the sort of small blemish that makes a program look unpolished.

Suppose we don’t want that initial jump, and instead want the dragging to happen smoothly for the entire time it’s being dragged. How can we make this work? Let’s first try to understand why that jump is happening. Look at the mouseDragged() function

void mouseDragged() {
  if (pointInCircle(mouseX, mouseY, ball.x, ball.y, diam / 2)) {
    ball.x = mouseX;
    ball.y = mouseY;
  }
}

When we drag the mouse on the ball, this function is called and it sets the the ball’s center to (mouseX, mouseY). The first time this is done is when the jump may occur: if the mouse pointer happens to start dragging near the ball’s edge, then, in the next frame of animation, the ball’s center jumps to the mouse location.

How can we fix this? Imagine we’ve just started dragging the ball with the mouse pointer near its edge. Suppose, in one frame, the mouse moves 3 pixels down, and 2 pixels to the right. What we want is for the center of the ball to move 3 pixels down, and 2 pixels right. In other words, instead of setting the center to be the location of the mouse pointer, we want the center to also move down and to the right, mimicking the mouse movement. We can do this by adding 3 pixels to y and add 2 pixels to x.

To figure out the distance to move the ball’s center, we’ll use the special Processing variables pmouseX and pmouseY. The point (pmouseX, pmouseY) is the location of the mouse pointer in the previous call to draw(). Since the mouse pointer is at (mouseX, mouseY) in the current call to draw(), the distance the mouse pointer moved is mouseX - pmouseX in the direction of the x-axis, and mouseY - pmouseY in the direction of the y-axis.

In code we get this:

void mouseDragged() {
  if (pointInCircle(mouseX, mouseY, ball.x, ball.y, diam / 2)) {
    ball.x += mouseX - pmouseX;
    ball.y += mouseY - pmouseY;
  }
}

Here is the full program:

class Sprite {
  float x;
  float y;
  float dx;
  float dy;
}

Sprite ball = new Sprite();
float diam;

void setup() {
  size(500, 500);
  smooth();
  ball.x = 250;
  ball.y = 250;
  diam = 125;
}

void draw() {
  background(255);
  noStroke();
  fill(255, 0, 0);
  ellipse(ball.x, ball.y, diam, diam);
}

void mouseDragged() {
  if (pointInCircle(mouseX, mouseY, ball.x, ball.y, diam / 2)) {
    ball.x += mouseX - pmouseX;
    ball.y += mouseY - pmouseY;
  }
}

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

Simulating pmouseX and pmouseY Ourselves

If Processing didn’t already provide us with pmouseX and pmouseY, then we could create our own versions of those variables ourselves like this:

// ... Sprite class as before ...

Sprite ball = new Sprite();
float diam;

float prevMouseX;  // these are both automatically initialized
float prevMouseY;  // to 0.0

// ...

void draw() {
  // ... code as before

  prevMouseX = mouseX;  // remember the current values of
  prevMouseY = mouseY;  // mouseX and mouseY
}

void mouseDragged() {
  if (pointInCircle(mouseX, mouseY, ball.x, ball.y, diam / 2)) {
    ball.x += mouseX - prevMouseX;
    ball.y += mouseY - prevMouseY;
  }
}

Unfortunately, this doesn’t work! At a glance, it seems that everything makes sense. Both prevMouseX and preMouseY are defined properly, they get assigned mouseX and mouseY in draw(), and they are used properly in mouseDragged. But try running the program with these changes, and you’ll soon see the circle and mouse pointer are moving differently.

So what’s the problem? It is not at all obvious why it’s different from when we used pmouseX and pmouseY. One thing we could do is to read the documentation for pmouseX to see if it says anything relevant. And indeed it does: the second paragraph of the description of pmouseX is this:

You may find that pmouseX and pmouseY have different values when referenced inside of draw() and inside of mouse events like mousePressed() and mouseMoved(). Inside draw(), mouseX and pmouseY update only once per frame (once per trip through the draw() loop). But inside mouse events, they update each time the event is called. If these values weren’t updated immediately during mouse events, then the mouse position would be read only once per frame, resulting in slight delays and choppy interaction. If the mouse variables were always updated multiple times per frame, then something like line(pmouseX, pmouseY, mouseX, mouseY) inside draw() would have lots of gaps, because pmouseX may have changed several times in between the calls to line().

What this tells is that we also need to update prevMouseX and prevMouseY and the end of mouseDragged like this:

void mouseDragged() {
  if (pointInCircle(mouseX, mouseY, ball.x, ball.y, diam / 2)) {
    ball.x += mouseX - prevMouseX;
    ball.y += mouseY - prevMouseY;
  }
  prevMouseX = mouseX;  // remember the current values of
  prevMouseY = mouseY;  // mouseX and mouseY
}

With this addition, the program works properly.

Notice that the fix is simple: we just added two statements to the end of mouseDragged(). What was hard was figuring out what to do. In general, there’s no easy recipe to follow for debugging a program. Every bug is different, and with more experience and knowledge of Processing you will get better at it.

Questions

  1. Write an explanation of dragging-and-dropping that a smart but computer illiterate grandpa could understand.
  2. When is the mouseDragged() function called?
  3. Explain the variables pmouseX and pmouseY.

Programming Questions

  1. Modify the drag-and-drop program so that two differently-colored circles can be dragged and dropped around the screen. Set the transparency of the circles so that it is impossible for one of the circles to completely hide the other.
  2. Modify the drag-and-drop program so that it is impossible for any portion of the circle to go off the screen. However, edges of the circle should be able to touch the edges of the screen. Hint: Read about the Processing constrain function.
  3. Write a program that lets a user drag-and-drop a rectangle around the screen. Make sure that the mouse pointer stays on the point that is first clicked while the rectangle is being dragged.

Sample Program

The following program combines a number of the programming features we’ve learned so far. It draws a red and a green ball, and a red and a green rectangle. When the red ball is moved onto the red rectangle, and the red rectangle is moved onto the green rectangle, the rectangles “fly” away.

class Sprite {
  float x;
  float y;
  float dx;
  float dy;
}

Sprite redBall = new Sprite();
Sprite greenBall = new Sprite();

Sprite redRect = new Sprite();
Sprite greenRect = new Sprite();

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

  // randomly place the red ball on the left half of the screen
  redBall.x = random(50, 200);
  redBall.y = random(50, 450);

  // randomly place the green ball on the right half of the screen
  greenBall.x = random(300, 450);
  greenBall.y = random(50, 450);

  // right half of the screen is red
  redRect.x = 250;
  redRect.y = 0;

  // left half of the screen is green
  greenRect.x = 0;
  greenRect.y = 0;
}

void draw() {
  background(255);

  // red rectangle
  noStroke();
  fill(255, 0, 0, 50);
  rect(redRect.x, redRect.y, 250, 500);

  // green rectangle
  noStroke();
  fill(0, 255, 0, 50);
  rect(greenRect.x, greenRect.y, 250, 500);

  noStroke();
  fill(255, 0, 0);
  ellipse(redBall.x, redBall.y, 50, 50);

  noStroke();
  fill(0, 255, 0);
  ellipse(greenBall.x, greenBall.y, 50, 50);

  checkGameOver();

  redBall.x += redBall.dx;
  greenBall.x += greenBall.dx;

  redRect.x += redRect.dx;
  greenRect.y += greenRect.dy;
}

// you win when the red ball is on the right half
// of the screen, and the green ball is on the left half
void checkGameOver() {
  if ( pointInRect(redBall.x, redBall.y, 250, 0, 250, 500)
    && pointInRect(greenBall.x, greenBall.y, 0, 0, 250, 500)) {
      redRect.dx = -3;
      greenRect.dy = 3;
  }
}

void mouseDragged() {
  if (pointInCircle(mouseX, mouseY, redBall.x, redBall.y, 25)) {
    redBall.x += mouseX - pmouseX;
    redBall.y += mouseY - pmouseY;
  }
  if (pointInCircle(mouseX, mouseY, greenBall.x, greenBall.y, 25)) {
    greenBall.x += mouseX - pmouseX;
    greenBall.y += mouseY - pmouseY;
  }
}

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

boolean pointInRect(float a, float b, float x, float y, float w, float h)
{
  return (a >= x && a <= x + w) && (b >= y && b <= y + h);
}