User Input and Animation





This tutorial will build on the previous tutorial, adding animation and user control via the keyboard.
  1. Rotating Our Cube
  2. Key Presses in FLTK
  3. Build and Run the Program
  4. Idle Callback for Animation
  5. Build and Run the Program Again
Lets get started.



Step 1:  Rotating Our Cube

We're going to need to keep track of the orientation of our cube so we'll need to add some member variables to the MyWindow class.  Add these lines to MyWindow.h.

      float rotation,
            rotationIncrement;


rotation will keep track of our cubes horizontal rotation and rotationIncrement is the rate that we will change that rotation.  This will make it easy for us to change the rotation speed later if we want.  We need to initialize these variables so our cube starts in the same orientation each time we run our program.  We also need to set the amount we're going to rotate with each key press.  The constructor for MyWindow is the obvious place to add this code.  Our modified constructor should look like this:

MyWindow::MyWindow(int width, int height, char* title) : Fl_Gl_Window(width, height, title)
{
   mode(FL_RGB | FL_ALPHA | FL_DEPTH | FL_DOUBLE);

   rotation = 0.f;
   rotationIncrement = 10.f;
}


We now need to make an OpenGL call to use our rotation value.  We'll place this after the gluLookAt call in draw() but before we make the call to DrawCube().

   . . .
   gluLookAt(0, 0, 3, 0, 0, 0, 0, 1, 0);
   glRotatef(rotation, 0, 1, 0);

   // draw something
   DrawCube();
   . . .


glRotatef() takes the angle to rotate (in degrees) and the axis to rotate around (the positive y axis in our case).  Our cube is all set to rotate.  We just need to modify the horizontal and vertical angles while our program is running.  We'll let the user do this with the arrow keys.


Step 2:  Key Presses in FLTK

FLTK uses an event callback scheme to let us process user input and system events.  To get a chance to react to these events we have to define a callback method.  For the window classes in FLTK this method is called handle.  handle takes and integer that is a constant for the type of event that has occurred and should return a non-zero value if you handled the event.  If you didn't take care of the event you should call the handle method of the base class (Fl_Gl_Window in this case) to give it a chance to react to the event.  We first need to add a method declaration for handle to the MyWindow class.  We need to add this to MyWindow.h right after the declaration for DrawCube().

      . . .
      virtual void draw();
      void DrawCube();
      virtual int handle(int event);

      float rotation,
            rotationIncrement;
      . . .

The definition for handle should be added to bottom of MyWindow.cpp.

int MyWindow::handle(int event)
{
   switch (event)
   {
      case FL_FOCUS:
      case FL_UNFOCUS:
         return 1;

      case FL_KEYBOARD:
         int key = Fl::event_key();
         switch (key)
         {
            case FL_Left:
               rotation -= rotationIncrement;
               redraw();
               return 1;

            case FL_Right:
               rotation += rotationIncrement;
               redraw();
               return 1;
         }
   }

   return Fl_Gl_Window::handle(event);
}


Our handle method first sees if we're interested in the type of event that has occurred.  You'll notice we respond to the FL_FOCUS and FL_UNFOCUS events but don't actually do anything.  We need to do this to make sure FLTK sends all the keyboard events to our window.  If the event was a keyboard event we then see if the key was the left or right arrow keys.  If it was we modify the rotation of our cube.  We then call redraw().  This tells FLTK that our window needs to be updated which will call are draw() method to get called.  If the event was not a key press or was not a key we were interested in we call the handle() method of our base class so it can react to the event if it wants.

We need to add one more included file to the top of MyWindow.cpp.  We need the FLTK header with the definition of Fl::event_key().

#include <Fl/Fl.h>

Ok we should be set to rotate our cube with the keyboard.


Step 3:  Build and Run the Program




Choose Build -> Build Solution to compile and link the program and Debug -> Start Without Debugging to run it.

You should see an OpenGL window just like this.  By pressing the left or right arrow keys the cube should now rotate and you should see the other sides of the cube rendered in different colors.


Step 4:  Idle Callback for Animation

Now that we can rotate the cube from the keyboard lets see about getting the cube to rotate on its own.  Basically we need to continuously update our rotation angle wait for the window to refresh and then update it again.  Luckily FLTK provide a means for us to do such a thing.  FLTK provides what it calls idle callbacks.  Idle callbacks work much like the handle() callback we already used except this callback will only be called when our window isn't busy doing something else (like handling a key press or redrawing the contents of the window).

First we're going to add a boolean flag to our window class that will tell us whether we want to rotate the cube with the idle callback or not.  We'll later type this boolean to the space bar so we can turn the animation on and off as we wish.  Add the boolean declaration to MyWindow.h after the other member declarations.

      . . .
      float rotation,
            rotationIncrement;
      bool  animating;
      . . .

The next thing to do is to create a function that FLTK will call when it is idle.  This function needs to be a global function and not a member of our class because FLTK will want to get a pointer to the function.  Here's the definition of our idle callback that we'll add to MyWindow.cpp just before the constructor.

void IdleCallback(void* pData)
{
   if (pData != NULL)
   {
      MyWindow* pWindow = reinterpret_cast<MyWindow*>(pData);
      if (pWindow->animating)
      {
         pWindow->rotation += pWindow->rotationIncrement / 100;
         pWindow->redraw();
      }
   }
}


This function takes a void pointer which will actually be a pointer to our window.  We check if the pointer is not NULL and then cast it to be a MyWindow pointer.  We check to see if our cube is animating and update its rotation if it is.  Notice that I've scaled down the rotation increment.  Idle callbacks will happen much quicker than the arrow keys can be pressed so we need to slow down the animating rotation speed.

We now need to tell FLTK to call our idle callback when the window is not otherwise busy.  We'll add this call to the MyWindow constructor and why were there we can initialize the animating boolean we added to our class.  Our revised constructor should look like this.

MyWindow::MyWindow(int width, int height, char* title) : Fl_Gl_Window(width, height, title)
{
   mode(FL_RGB | FL_ALPHA | FL_DEPTH | FL_DOUBLE);

   rotation = 0.f;
   rotationIncrement = 10.f;
   animating = false;

   Fl::add_idle(IdleCallback, this);
}



add_idle takes a pointer to the function to call when the window is idle and a void pointer which will be passed to the idle callback.  We've placed a pointer to our window in the void pointer so we can update the rotation from the idle callback.

The only thing left to do is tie our animating boolean to the space bar.  We'll need to modify our handle() method to do this.


int MyWindow::handle(int event)
{
   switch (event)
   {
      case FL_FOCUS:
      case FL_UNFOCUS:
         return 1;

      case FL_KEYBOARD:
         int key = Fl::event_key();
         switch (key)
         {
            case FL_Left:
               rotation -= rotationIncrement;
               redraw();
               return 1;

            case FL_Right:
               rotation += rotationIncrement;
               redraw();
               return 1;

            case ' ':
               animating = !animating;
               return 1;
         }
   }

   return Fl_Gl_Window::handle(event);
}



As you can see we just need to add a case for the space character and flop the animating flag if the key pressed was the space bar.



Step 5:  Build and Run the Program




To test the new version of out program we choose  Build -> Build Solution to compile and link the program and Debug -> Start Without Debugging to run it.




Source code for this tutorial.

Go to the previous tutorial.
Go to the next tutorial.