Creating a trail of moving object in Processing

From DftWiki

Jump to: navigation, search

--D. Thiebaut 08:41, 17 June 2012 (EDT)


This tutorial page explores various techniques for marking the path of animated objects in Processing. Many of the ideas and techniques presented here are taken from the Processing.org site, which remains one of the best resource for Processing. Don't hesitate to browse its pages for good ideas!


Contents



Unlimited Trail

Here we just start with the basic idea which we already played with in [Processing Skeleton Project and Simple Exercises | the previous tutorial in this section].


package tutorial1;
 
import processing.core.*;
 
public class Exercise1 extends PApplet {
 
        public void setup() {
                // define the window size, make graphics softer, and make
                // the background white
                size(600, 600);
                smooth();
                background( 0xeeeeff );
        }
 
        public void draw() {
 
                // change color of circle paint depending on mouse button
                if (mousePressed)  {
                        stroke( 255 );
                        fill(0);
                }
                else {
                        stroke( 0 );
                        fill(255);
                }
 
                // draw a circle where the mouse is located
                ellipse(mouseX, mouseY, 80, 80);
        }
}


  • Try it out!
  • Let's play some with this simple example.

Growing and shrinking circles

  • One way to make the circle grow and shrink as the animation goes on is to control its radii with a sine function whose angle is simply the number of frames since the beginning of the animation.
  • The trick here relies on knowing that Processing counts the number of frames it displays and keeps the count in a variable named frameCount. The counter starts with 0, and every time draw() is called by Processing, frameCount is incremented by 1.
  • Modify the program above, and add a new variable inside the draw() function:


                 float radius = 50 + 30 * sin( frameCount * 0.05f );


  • Because the sin() function oscillates between -1 and 1 as the angle increases, radius will oscillate between 50 - 30, and 50 + 30, or 20 and 80.
  • Change the call to ellipse() and replace the constant 80 by the variable radius.


                // draw a circle where the mouse is located
                ellipse(mouseX, mouseY, radius, radius);


  • Try your new code!

Exercise #1

QuestionMark1.jpg


  • Make the color change with the growing and shrinking of the circle. You may find the information in the processing page on color very useful!
  • Keep one of the radius values constant, and the other one equal to the variable radius.
  • Use two different variables, radius1 and radius2 which are initialized the same way radius is, but use the sin() function for one, and the cos() function for the other one. Use radius1 and radius2 as the two radii value in the ellipse() function...



Hints & Solutions


Fading Trail

One way to make the trail disappear is to make whatever is drawn in the applet become more and more transparent as it ages. In this case, objects that have been drawn in the current frame are fully opaque, while objects that have been drawn in earlier frames become more and more transparent, to the point where they disappear.

This is accomplished by painting a transparent rectangle over the whole applet before we draw new objects in the window. The objects already on the applet get more transparent as the process is repeated. All we need to do is add these three lines at the beginning of draw():

                noStroke();
                fill( 0xee, 0xee, 0xff, 50);
                rect(0, 0, width, height);


The magic number here is 50 which is the amount of transparency we give the whole applet before we draw a new circle.


By the way, if you are surprised by the expression 0xee' or 0xff startles you, don't be! It's just a different system for representing integer numbers. This format is call hexadecimal. The 0x part means that what follows are digits or letters that represent hexadecimal numbers. 0x00 is 0. 0x01 is 1. 0x09 is 9. 0x0a is 10 decimal. 0x0b is 11, 0x0c is 12, 0x0d 13, 0x0e 14, 0x0f 15, 0x10 16, ... 0xff is 255. In hexadecimal we can represent any integer between 0 and 255 with only two hexadecimal digits, between 0x00 and 0xff.


The new draw() function looks something like this:


        public void draw() {
                //Fade everything which is drawn
                noStroke();
                fill( 0xee, 0xee, 0xff, 50);
                rect(0, 0, width, height);     

                float radius = 50 + 30 * sin( frameCount * 0.05f );
                // change color of circle paint depending on mouse button
                if (mousePressed)  {
                        stroke( 255, 255, 255 );
                        fill( 0,0,0 );
                }
                else {
                        stroke( 0, 0, 0 );
                        fill( 255, 255, 255 );
                }
 
                // draw a circle where the mouse is located
                ellipse(mouseX, mouseY, radius, radius);
        }


  • Try it!


Exercise #2

QuestionMark2.jpg
  • Change the value of the transparency (50) and try different values to see how they affect the applet and the trail. The lowest acceptable is 0, the highest is 255.
  • Try adding a transparency value to the circles, to see how they are affected. Example of a transparency of 100:


                if (mousePressed)  {
                        stroke( 255, 255, 255, 100 );
                        fill( 0,0,0, 100 );
                }


(For reference, a transparency of 255 is fully transparent, while 0 is fully opaque.)

  • Change the background color to white, or black, and, again, play with different values of transparencies to better understand how the applet is affected.

Blurring the Trail

Another approach that is similar to the transparency approach (making past drawn objects more and more transparent as time progresses) is to continuously blur the past objects more and more until they merge with the background. This operation is carried out by the Processing filter() function. You can read some background information here. There is some good background material as well here. Read this first, then continue with this section!

Let's play! Here some code you should paste into a new class in your Eclipse project:

package tutorial1;
 
import processing.core.*;
 
public class Main4 extends PApplet {
       
        public void setup() {
                // define the window size, make graphics softer, and make
                // the background white
                size(600, 600);
                smooth();
                background( 255 );
        }
 
        public void draw() {
                // keep everything that is currently drawn, but blur everything.
                filter( BLUR,1 );
               
                float radius = 50; // + 30 * sin( frameCount * 0.05f );
               
                // change color of circle paint depending on mouse button
                if (mousePressed)  {
                        stroke( 255, 255, 255 );
                        fill( 0,0,0 );
                }
                else {
                        stroke( 0, 0, 0 );
                        fill( 255, 255, 255 );
                }
 
                // draw a circle where the mouse is located
                ellipse(mouseX, mouseY, radius, radius);
        }
}


  • See how the circles get blurred little by little, and that the last one drawn, the one that follows the mouse is always sharp?
  • You will notice that little by little the screen gets darker and darker and will likely finish all black, except for the white circle. That is because we are constantly adding new black pixels in the border of the new circles, and if we do not move the mouse much, the black of the pixels, as it gets blurred, creates darker and darker pixels around, and this gray increases until the window becomes black. We'll see in one of the exercises how we can keep the window white.




Exercise #3

QuestionMark3.jpg
  • Run the code above and move the mouse in big circles inside the applet. Keep a constant speed. See how much a delay there in Processing keeping up with you. As you move the mouse pointer around, Processing draws circles 1/2 an inch to an inch away from it, as it tries to keep up. See that?
  • Now, change the line
              filter( BLUR, 1 );
and replace 1 by 6. This number controls how aggressive the blurring operation is.
  • Repeat the same experiment, moving the mouse pointer in circles. You should observe that this time the applet is not keeping up with you as well as before. This is because a BLUR with parameter 1 involves modifying fewer pixels around each pixel blurred, than a BLUR of 6.
  • Play with different values of BLUR to better get a sense of the amount of blurring, and how this algorithm works.
  • What if we still wanted to use a BLUR of 6 but make the applet more responsive? We can simply decide to blur every other frame!
         if ( frameCount % 2 == 0 )
              filter( BLUR, 6 );
  • Try this in your code and see if the applet gets a bit more responsive.





Exercise #4

QuestionMark4.jpg
  • To keep the window white and prevent it from getting gray or black, just use the transparency trick we saw earlier, and add these 3 lines at the beginning of draw():


                noStroke();
                fill( 255, 255, 255, 100);
                rect(0, 0, width, height);
  • Play with the transparency of 100. Make it larger, smaller. See what you like best as a result. Play as well with the BLUR parameter and adjust these two numbers until you get something you like.




Exercise #5

QuestionMark5.jpg
  • Replace
        filter( BLUR, 1 );
by
        filter( DILATE );
and see what happens...
  • DILATE is another algorithm that the filter() function can apply to the whole applet. Just another option when creating your animation and special effects!







Limited Trail

In the limited trail case, we only draw a finite number of circles in the trail. Say 10. So, when we draw a new circle, we also draw the last 9 circles we drew last, and none of the others. So we need a data structure that will hold circles in a First In, First Out way. Processing supports ArrayList (as does Java). So we need to figure out what we need to remember for each circles. In our case, it's the x and y of the center of the ellipse, as well as its radii. The best way to do this is to create a circle class that will hold these values.

First, an Applet with A Circle Class

Study the code below and see how the new class encapsulates the circle information.

package tutorial1;

import processing.core.*;

/**
 *  CircleClass: a simple container to hold the location of
 *  the center of the circle, and the frameCount when it
 *  appears on the applet, as the frameCount is used to
 *  define the rate at which the circles "breathes".
 */

class CircleClass {
        public float x;
        public float y;
        public int   frameCount;
       
        public void CircleClass( float xx, float yy, int fc ) {
                x = xx;
                y = yy;
                frameCount = fc;
        }
       
        public void draw( PApplet p ) {
                float radius1 = 50 + 30 * p.sin( frameCount * 0.05f );
                float radius2 = 50 + 30 * p.sin( frameCount * 0.05f );
                p.ellipse( x, y, radius1, radius2 );
        }
}

/**
 * Main3: the main Processing applet.
 *
 */

public class Main3 extends PApplet {

        CircleClass c;
       
        /**
         * setup().  Sets the applet up.
         */

        public void setup(){
          size(800,600);           // Size of Background/panel
          background( 0xeeeeff );   //Color of Background, Black
          frameRate(30);    //Frame rate set at 30
       
          c = new CircleClass( );
        }

       
        /**
         * draw(): called to draw every frame.
         * draws a circle where ever the mouse is.  
         */

        public void draw() {        

          //Draw ellipses
          stroke( 0 );
          fill( 255 );
          c.x = mouseX;
          c.y = mouseY;
          c.frameCount = frameCount;
          c.draw( this );
                 
        }
}
  • Try out the code, and play with it!

A Trail of the Last n circles

All we need to do now is to remember the last n (say 10) circles drawn, and every call to draw we erase the applet, and draw these n circles. To remember the circles, we store them in a queue (FIFO) implemented for right now with an ArrayList.

Paste the code below in a class called Main. Study the code. See the new import statements that introduce the ArrayList and the Iterator in the applet.


package tutorial1;

import java.util.ArrayList;  // needed to
import java.util.Iterator;

import processing.core.*;

/**
 *  CircleClass: a simple container to hold the location of
 *  the center of the circle, and the frameCount when it
 *  appears on the applet, as the frameCount is used to
 *  define the rate at which the circles "breathes".
 */

class CircleClass {
        public float x;
        public float y;
        public int   frameCount;
       
        CircleClass( float xx, float yy, int fc ) {
                x = xx;
                y = yy;
                frameCount = fc;
        }
       
        public void draw( PApplet p ) {
                float radius1 = 50 + 30 * p.sin( frameCount * 0.05f );
                float radius2 = 50 + 30 * p.sin( frameCount * 0.05f );
                p.ellipse( x, y, radius1, radius2 );
        }
}

/**
 * Main3: the main Processing applet.
 *
 */

public class Main extends PApplet {

        ArrayList circles;
       
        /**
         * setup().  Sets the applet up.
         */

        public void setup(){
          size(800,600);           // Size of Background/panel
          background( 0xeeeeff );   //Color of Background, Black
          frameRate(30);    //Frame rate set at 30

          circles = new ArrayList();
        }

       
        /**
         * draw(): called to draw every frame.
         * draws a circle where ever the mouse is.  
         */

        public void draw() {        
          // erase the applet
          background( 0xeeeeff );
         
          // add a new circle to the end of the list
          circles.add( new CircleClass( mouseX, mouseY, frameCount ) );
                 
          // remove oldest (first) circle if more than 10
          if ( circles.size() > 10 )
                  circles.remove( 0 );
         
          // display the circles
          stroke( 0 );   // black outline
          fill( 255 );   // white interior
          for ( Iterator< CircleClass > it = circles.iterator(); it.hasNext(); ) {
                  CircleClass c = it.next();
                  c.draw( this );
          }
        }
}


  • Run the code and see how we now have a trail of 10 circles.



Exercise #6

QuestionMark6.jpg
  • Change the number of circles kept in the trail. Try large numbers, such as 100, or 500.
  • Modify the program so that a transparency value is added to the color of each circle (fill and stroke), and make the transparency of the most recent (last) circle fully opaque (0), and the transparency of the oldest one (at Index 0), 255. You may find that looping through the circles in reverse order makes this simpler:


       for ( int i = circles.size()-1; i >= 0; i = i - 1 ) {
            CircleClass c = circles.get( i );
            ...
       }






Exercise #7

QuestionMark7.png


  • Instead of having the leading circle follow the mouse, make it move randomly, in a manner similar to what we explored in the previous tutorial.





Exercise #8

QuestionMark8.jpg


  • Are you ready for a bit of challenge? Try keeping track of 2 circles, each with a trail of 10 circles...
  • When you're done, try keeping track of 20 circles, each with a trail of 10 circles...





A More Efficient Trail

The previous example is fine for short trail, but ArrayList are not efficient data structures when we remove their 0th element: they shift all the remaining elements by one, which is an O(N) operation. If the trail is long, say 10,000 element, then removing the 0th one every call to the draw() function will take 10,000 steps.

The queue, which is an interface to the LinkedList collection in Java is a better suited data-structure, with an O(1) add() and remove() operation (remove() is called poll() for queues).

Below is the same example as above, implemented with queues:


package tutorial1;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;

import processing.core.*;

/**
 * Main3: the main Processing applet.
 *
 */

public class Main5 extends PApplet {

        /**
         * CircleClass: a simple container to hold the location of the center of the
         * circle, and the frameCount when it appears on the applet, as the
         * frameCount is used to define the rate at which the circles "breathes".
         */

        class CircleClass {
                public float x;
                public float y;
                public int frameCount;

                CircleClass(float xx, float yy, int fc) {
                        x = xx;
                        y = yy;
                        frameCount = fc;
                }

                public void draw(PApplet p) {
                        float radius1 = 50 + 30 * p.sin(frameCount * 0.05f);
                        float radius2 = 50 + 30 * p.sin(frameCount * 0.05f);
                        p.ellipse(x, y, radius1, radius2);
                }
        }
        //----------------------------- end of CircleClass -----------------------------

        // member variables
        Queue circles;
        int maxTrail = 10;  // number of circles in the trail

        /**
         * setup(). Sets the applet up.
         */

        public void setup() {
                size(800, 600); // Size of Background/panel
                background(0xeeeeff); // Color of Background, Black
                frameRate(30); // Frame rate set at 30

                circles = new LinkedList();
        }

        /**
         * draw(): called to draw every frame. draws a circle where ever the mouse
         * is.
         */

        public void draw() {
                // erase the applet
                background(0xeeeeff);

                // add a new circle to the end of the list
                circles.add(new CircleClass(mouseX, mouseY, frameCount));

                // remove oldest (first) circle if more than 10
                if (circles.size() > maxTrail)
                        circles.poll();

                // display the circles
                stroke(0); // black outline
                fill(255); // white interior
                for (Iterator<CircleClass> it = circles.iterator(); it.hasNext();) {
                        CircleClass c = it.next();
                        c.draw(this);
                }
        }
}


  • Note that I have made the CircleClass local to the Main class. This way CircleClass does not conflict in Eclipse with other CircleClass definitions that may exist in different versions of my projects.





Exercise #9

QuestionMark9.jpg


  • Redo Exercise #8, but with a queue data-structure to keep track of the trail. You may still want to use an ArrayList to keep track of the several circles with trails...




Selective Transparency

Assume that you want to display two different kinds of objects that are moving around the applet, and you want one kind of object to vanish with the transparency trick, while the others do not. One possible way to accomplish this is to use a PGraphics buffer. The idea of a graphics buffer is to have another "surface" on which to draw pixels, but to make this surface not subject to the graphics commands that are called inside the draw() function. And at the end of draw() we "overlap" the buffer onto the applet.

An example will illustrate how this might work. Copy/paste the code below

package tutorial1;
 
import processing.core.*;
 
public class Main extends PApplet {
        // define a buffer to draw the rectangles on
        PGraphics buffer;
       
        public void setup() {
                // define the window size, make graphics softer, and make
                // the background white
                size(600, 600);

                // create the buffer to be the same size as the applet
                buffer = createGraphics(width, height, P2D);

                smooth();
                background( 0 );
                frameRate( 30 );
        }
 
       
        public void draw() {           
                // Fade everything on the applet
                noStroke();
                fill( 255, 255, 255, 100);
                rect(0, 0, width, height);

                // change color of circle paint depending on mouse button
                if (mousePressed)  {
                        stroke( 255, 255, 255 );
                        fill( 0,0,0 );
                }
                else {
                        stroke( 0, 0, 0 );
                        fill( 255, 255, 255 );
                }
 
                // draw a circle where the mouse is located
                ellipse( mouseX, mouseY, 50, 50 );

                // draw a rectangle on the buffer, offset by 100 pixels right and down
                // from the mouse cursor.              
                buffer.beginDraw();
                buffer.rectMode( CENTER );
                buffer.rect( mouseX + 100 , mouseY + 100, 50, 50 );
                buffer.endDraw();
                // overlay the buffer on the applet
                image(buffer, 0, 0);
        }
}


  • Play with the code. Notice how it the circles disappear while the rectangles don't. Neat, no? :-)





Exercise #10

QuestionMark10.jpg


  • You will notice that the circles appear below, or underneath the rectangles. Change that so that they appear above the rectangles.
  • Make the rectangles move randomly, rather than at a constant offset from the circles.
  • Switch the behaviors of the rectangles and the circles. The circles should not disappear while the rectangles should... :-)
  • Use the filter( BLUR ) method for the circles, and the filter( DILATE) method for the rectangles, simultaneously...  :-) :-)











Voila!