Creating an animated display

Arjen Markus (10 October 2002) It occurred to me that the technique of producing an animated display has not been documented with so many words on the Wiki. Well, here is my view on the subject.

Suppose you have a canvas which needs to redrawn after some time. This will be our animated display, as the redrawn picture will look slightly different.

There are roughly two options:

  • Redrawing the individual pictures is fast enough, so anybody can run the script directly and watch the movie.
  • Redrawing them takes either too much time or we need a compact form in which to distribute the result (say as an animated GIF file).

The first part requires knowledge of the after command, such as in Keep a GUI alive during a long calculation. The second part requires additional tools, such as ImageMagick ([L1 ]) to complete the job.

Both approaches are relatively easy (see the script below), but they are certainly easiest on UNIX/Linux, where most tools are command-line driven.

The first approach is this:

  • In a procedure, draw the picture.
  • When it completes, change the "time" parameter, use the after command to wait a short interval and then restart the procedure.

Note: the after command is essential here, it will make sure that the next picture is drawn at the appropriate time. In the meantime the canvas can be drawn, events can be processed etc.

Further Note: there are two subtle problems with the naive use of after in doing animation. One arises when trying to get consistent behaviour across varying machines, the other arises when the time required to draw the scene varies. The former can often be seen when you try running an old game on a new fast computer--the game may zips along way too fast to play. The latter is commonly seen during the play of a game when a new animated object appears on the screen and the animation suddenly slows down. Fortunately, for simple animations both problems have the same easy solution: make the after interval non-constant. Specifically, when designing the animation decide how much time you want between successive scenes (often starting in terms of frames per second). Then, before drawing a scene, note the time; when done drawing, compute how much time is left before the next scene is needed and use that value for the interval for after. [For real high quality animation, that wait interval is not wasted but used to start drawing the next scene into a second buffer, but that's a whole other discussion.] KPV

The second approach just adds two steps:

  • When the picture is finished, save the picture in a bitmap file
  • When the animation is complete and we have all pictures stored on disk, use some tool to create the animation file.

On UNIX/Linux, the tool to save the canvas in a bitmap file that I use frequently is "xwd":

   exec xwd -id [winfo id $canvas] -out [format "anim%04d.xwd" $count]

where "canvas" holds the name of the canvas we draw in and "count" is a counter for the picture. By formatting the names of the bitmap files in four figures, anim0000.xwd, anim0001.xwd, ..., the conversion to an animated GIF file via ImageMagick's convert becomes a single instruction:

In the shell, type:

    > convert anim*.xwd my_anim.gif

(convert will convert all files to GIF files - this is based on the extensions - and as there is more than one input file, it is going to be an animated GIF file.)

The script below shows an animation of a bouncing ball. Nothing particularly interesting, but it illustrates the ideas.


 package require Tk

 # drawBall --
 #    Draw a red circle at a certain height above the green ground
 #
 # Arguments:
 #    time     Time parameter, used to calculate the actual height
 #
 # Result:
 #    None
 #
 # Note:
 #    Assume a perfectly elastic collision. The time parameter must
 #    be reduced to the time since the last collision.
 #
 proc drawBall {time} {
   global accel
   global velo0
   global cnv_height
   global cnv_width

   set period [expr {2.0*$velo0/$accel}]
   set time2  [expr {$time - $period * int($time/$period)}]

   set grass_height 20
   set radius        7
   set ball_height   [expr {$velo0*$time2-0.5*$accel*$time2*$time2}]
   set pix_height [expr {
                 $cnv_height-$grass_height - $radius - $ball_height}]

   set xl     [expr {0.5*$cnv_width-$radius}]
   set xr     [expr {0.5*$cnv_width+$radius}]
   set yb     [expr {int($pix_height)-$radius}]
   set yt     [expr {int($pix_height)+$radius}]

   .cnv delete all
   .cnv create rectangle 0 $cnv_height $cnv_width \
      [expr {$cnv_height-$grass_height}] -fill green -outline green
   .cnv create oval      $xl $yb $xr $yt -fill red -outline black
 }

 # nextPicture --
 #    Prepare to call the next picture, stop after some predefined
 #    number of steps.
 #
 # Arguments:
 #    step     Step number (converted to time)
 #
 # Result:
 #    None
 #
 proc nextPicture {step} {

   # Here you can insert the code to save the current picture
   #
   # exec xwd -id [winfo id .cnv] -out [format "anim%04d.xwd" $step]
   #

   #
   # Draw the picture
   #
   drawBall [expr {0.1*$step}]

   #
   # Set up the next picture via the [after] command
   #
   if { $step < 1000 } {
      incr step
      after 100 [list nextPicture $step]
   }
 }

 # main --
 #   Set up the canvas, start the loop
 #

 global cnv_width
 global cnv_height
 global velo0
 global accel

 set cnv_width   400
 set cnv_height  300

 set velo0        70.0  ;# m/s
 set accel        10.0  ;# m/s2
                       ;# pixels become m that way :)

 canvas .cnv -width $cnv_width -height $cnv_height -background white
 pack   .cnv -fill both

 drawBall 0
 #
 # Wait until the dust settles - important for saving the frames
 # on a slow system
 #
 tkwait visibility .
 nextPicture 0

See also: Creating an animated display, part 2