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 thatpmouseX
andpmouseY
have different values when referenced inside ofdraw()
and inside of mouse events likemousePressed()
and mouseMoved(). Insidedraw()
,mouseX
andpmouseY
update only once per frame (once per trip through thedraw()
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 likeline(pmouseX, pmouseY, mouseX, mouseY)
insidedraw()
would have lots of gaps, becausepmouseX
may have changed several times in between the calls toline()
.
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¶
- Write an explanation of dragging-and-dropping that a smart but computer illiterate grandpa could understand.
- When is the
mouseDragged()
function called? - Explain the variables
pmouseX
andpmouseY
.
Programming Questions¶
- 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.
- 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.
- 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);
}