JavaScript EditorFree JavaScript Editor     Perl Manuals 

Main Page

17.10. tkneko—Animating the Neko on a Canvas

A more interesting task is emulating Masayuki Koba's xneko game, where a neko chases after the cursor, running up, down, left, right, and in circles, stopping only when the cursor stops. If the cursor stays motionless long enough, the neko falls into a deep sleep. When the cursor moves again, the neko awakens and resumes chasing the cursor. The neko is confined to the MainWindow, but if the cursor leads him to a window edge, he scratches to get free and eventually either falls asleep or resumes running.

To simulate motion, we display images of the neko at various positions on the Canvas at tenth of a second intervals (anything slower presents unacceptable flicker). The images we have to work with are shown in Figure 17-14. To make the neko run left, we repeatedly display the left1.ppm and left2.ppm images. (If the images display at the same Canvas coordinate, the neko runs in place. We might as well have used Tk::Animation if we wanted that effect.)

The neko's actions are state driven.[44]

[44] The "togi" states are from the original Japanese code. We don't pretend to know what they mean, although they are the "scratching the wall" states.

There are in fact five distinct states: the neko is either waking, moving, stopping, scratching, or sleeping. The tkneko states are encoded in a Perl hash with compiled (for efficiency) regular expressions as keys and code references (the state processors) as values:

%states = (
    qr/AWAKE/                   => \&do_awake,
    qr/STOP/                    => \&do_stop,
    qr/UTOGI|RTOGI|DTOGI|LTOGI/ => \&do_togi,
    qr/SLEEP/                   => \&do_sleep,
);           # neko state table

The states that are dependent on the neko's direction (but otherwise equivalent) are further divided into substates, described by a regular expression with alternatives.

go_neko, the animation main loop, is activated by a repeating 100 millisecond timer event. The subroutine's job is simply to call a subroutine based on the animation's current state, $state. The subroutine in turn selects an appropriate PPM image and displays it on the canvas.

As long as the neko stays in a constant state, running left for example, the variable $state_count keeps incrementing, and the state processing subroutine do_move can use this to alternately select the left1.ppm or left2.ppm image. The debug -textvariable $where shows this state information as well as the neko's current Canvas coordinates, $nx and $ny. Figure 17-17 shows the neko in its sleep state.

$mw->repeat(100 => \&go_neko);

sub go_neko {

    $state_count++;              # current state's cycle count
    $where = sprintf("state=%-7s state_count=%05d, nx=%04d, ny=%04d",
                     $state, $state_count, $nx, $ny);

    foreach my $regex (keys %states) {
        next STATES unless $state =~ /^($regex)$/;

} # end go_neko

We create all the PPM images during initialization, make Canvas image items of them, and store the item IDs in the %pixmaps hash, indexed by filename. But we don't want all these individual animation frames visible unless they're needed, so we position them off-Canvas at the invisible coordinates (-1000, -1000).

foreach my $pfn ( <$image_base/*.ppm> ) {
    my $bpfn = basename $pfn;
    $pixmaps{$bpfn} = $canvas->createImage(-1000, -1000,
        -image => $canvas->Photo(-file => $pfn));
Figure 17-17

Figure 17-17. The neko has spent 79 cycles in the SLEEP state

Hidden Canvas Items

After this chapter was written, Tk 800.018 introduced the -state option for individual Canvas items, whose value can be normal, disabled, or hidden. We can take advantage of this and instead of moving an image offscreen, simply mark it as hidden:

$canvas->itemconfigure($pixmaps{$pxid}, -state => 'hidden');

This is the preferred solution, because to be sure that the image we move is offscreen, we need to factor in the current -scrollregion:

@scrollregion = @{$canvas->cget(-scrollregion) };
$scrollregion[0] - 1000, $scrollregion[1] - 1000);

The -scrollregion option is a reference to an array of two canvas coordinates (four items): the top-left corner and bottom-right corner of a bounding box describing the maximum extents that one may scroll the canvas. For instance:

$canvas->configure(-scrollregion => [-1100, -1100, 400, 400]);

defines a square canvas 1500 pixels per side that in theory can be scrolled up-and-left so that our "hidden" Canvas items become visible. To be really sure the image is hidden, we should substitute the image's width and height for the constants 1000.

When a state processing subroutine selects an image (animation frame) for display, it calls the frame subroutine with the new pixmap name. After hiding the old image, frame moves the new image to the neko's current Canvas position.[45]

[45] Once again, for Tk Version 800.018 and newer it's preferable to set an image's state to hidden to make it disappear, and normal to make it visible.

sub frame {
    $canvas->coords($pixmaps{$pix}, -1000, -1000);
    $pix = "$_[0].ppm";
    $canvas->coords($pixmaps{$pix}, $nx, $ny);

So do_move might make a call such as this to make the neko run left:

frame 'left' . (($state_count % 2) + 1);

Of course, in the actual program, the direction isn't a hardcoded string but the back-reference $1 from the state table's regular expression match.

To make the neko follow the cursor, we use Tk's pointerxy command to get the cursor's coordinates, compute a heading from the neko to the cursor, and then map that value to a new state. $r2d is the radian-to-degree conversion factor, and $h is the new heading, in degrees.

($x, $y) = $canvas->pointerxy;

my $h = int( $r2d * atan2( ($y - $ny), ($x - $nx) ) ) % 360;
my($degrees, $dir);

foreach (
         [[ 22.5,  67.5], 'DWRIGHT'],
         [[ 67.5, 112.5], 'DOWN'],
         [[112.5, 157.5], 'DWLEFT'],
         [[157.5, 202.5], 'LEFT'],
         [[202.5, 247.5], 'UPLEFT'],
         [[247.5, 292.5], 'UP'],
         [[292.5, 337.5], 'UPRIGHT'],
         [[337.5,  22.5], 'RIGHT'],
         ) {
    ($degrees, $dir) = ($_->[0], $_->[1]);
    last if $h >= $degrees->[0] and $h < $degrees->[1];
} # forend

set_state $dir;

And that's really all there is to it. As you'd expect, there are many tiny details we've ignored, so the entire program is available at the O'Reilly web site.

JavaScript EditorJavaScript Formatter     Perl Manuals