0064: Cairo VIII – Animation on a DrawingArea

When animating on a DrawingArea, the drawing is done more or less the same way, but a bit at a time over a number of frames. Also, a signal is hooked up to a callback in the normal way, and then in the callback, a Timeout object is created. It’s the Timeout object, as you may guess, that’s used to determine how often the Surface gets refreshed and that’s what sets the frame rate.

A Simple Animated Timer

Results of this example:
Current example output
Current example output
Current example terminal output
Current example terminal output (click for enlarged view)

In this first example, we’ll slap down the numbers 1 through 24 in sequence and refresh the DrawingArea’s Surface every 24th of a second.

Here’s the initialization section of the MyDrawingArea object:

Timeout _timeout;
int number = 1;
int fps = 1000 / 24; // 24 frames per second

The Timeout class is part of glib, so it’s imported with:

import glib.Timeout;

at the top of the file.

And the fps variable is an easy way to set the frames per second. Timing is in milliseconds, so 1000 (one second’s worth of milliseconds) divided by the desired frame rate gives you exactly what you expect:

  • 1000 / 24 = 24 fps,
  • 1000 / 30 = 30 fps,
  • 1000 / 6 = 6 fps, and
  • so on.

Setting up the Timeout is the first thing you do in the callback and it’s done like this:

if(_timeout is null)
{
	_timeout = new Timeout(fps, &onFrameElapsed, false);
}

The Timeout object acts very much like a signal hook up. In our example, once the Timeout is instantiated:

  • onFrameElasped() function acts like a callback,
  • fps tells the Timeout how often to call onFrameElapsed(), and
  • false tells the Timeout not to fire right away, but to wait for the first interval to pass first.

And here’s what Timeout’s “callback” looks like:

bool onFrameElapsed()
{
	GtkAllocation size;
	getAllocation(size);
		
	queueDrawArea(size.x, size.y, size.width, size.height);
		
	return(true);
	
} // onFrameElapsed()

We grab a GtkAllocation like we have before so we can get the dimensions of the DrawingArea, then use those dimensions to redraw. We could, if we wanted, refresh only a small portion of the DrawingArea, but we’ll talk more about that another time.

As for the actual drawing itself, we do this:

if(number > 24) // number range: 1 - 24
{
	number = 1;
}

context.showText(number.to!string());
number++;

And that’s in the onDraw callback… which is the real callback attached to the DrawingArea as opposed to the sort-of callback attached to the Timeout.

We don’t have to set up a for() loop because the Timeout repeats 24 times per second and ends up doing the job a loop would normally do.

Animating the Drawing of a Circle

Results of this example:
Current example output
Current example output
Current example terminal output
Current example terminal output (click for enlarged view)

This example is very similar to redrawing text. In the initialization section we have:

Timeout _timeout;
float arcLength = PI / 12;
int fps = 1000 / 12; // 12 frames per second

This time, we’re running at 12 frames per second. The length of arc we’ll draw each frame is PI / 12 and because we’re working in radians and a full circle is 2PI, that means our circle will be redrawn once every two seconds.

The onFrameElapsed() Timeout callback is the same as before, so let’s have a gander at the onDraw callback:

bool onDraw(Scoped!Context context, Widget w)
{
	if(_timeout is null)
	{
		_timeout = new Timeout(fps, &onFrameElapsed, false);
		
	}
	
	if(arcLength > (PI * 2))
	{
		arcLength = PI / 12;
	}

	arcLength += (PI / 12);

	context.setLineWidth(3);
	context.arc(320, 180, 40, 0, arcLength);
	context.stroke(); // and draw
	
	return(true);
	
} // onDraw()

The action starts with the if() statement when we measure out a length of arc to draw, then add it to the length of arc we already have. Then we set up the line width, set up the arc() function and do the stroke.

Pretty simple. And, of course, you could do any other drawing in there as well. And this not being the 80’s or 90’s, you’d have to pack in a very long list of drawing commands before you slow down the refresh rate.

Flipbook Animation

Results of this example:
Current example output
Current example output
Current example terminal output
Current example terminal output (click for enlarged view)

And now for the pièce de resistance, loading a bunch of frames and flipping through them at 12 fps… which simulates shooting on twos. That’s animator parlance meaning that each image is shot twice and played back at 24 fps. Anyway, here’s the initialization section:

int currentFrame = 0;
int fps = 1000 / 12; // 6 frames per second
Timeout _timeout;
Pixbuf[] pixbufs;
int numberOfFrames = 75;

This time around, we’re going to keep track of our current frame. And there’s also an array of Pixbufs to store all the individual images that will be our frames.

The constructor plays a bigger part in things this time:

this()
{
	foreach(int i; 0..numberOfFrames)
	{
		if(i < 10)
		{
			pixbufs ~= new Pixbuf("./images/sequence/one00" ~ i.to!string() ~ ".tif");
		}
		else
		{
			pixbufs ~= new Pixbuf("./images/sequence/one0" ~ i.to!string() ~ ".tif");
		}

	} // for()
		
	addOnDraw(&onDraw);
		
} // this()

The foreach() loop loads all the frames and inside that, we build the file names through string concatenation (which is less trouble than copying and pasting a whole big long list of file names into an array).

Once the files are all loaded snug into their Pixbufs, we hook up the signal and move on.

Again, the Timeout’s callback is the same, so here’s the onDraw callback:

bool onDraw(Scoped!Context context, Widget w)
{
	if(_timeout is null)
	{
		_timeout = new Timeout(fps, &onFrameElapsed, false);
			
	}
		
	context.setSourcePixbuf(pixbufs[currentFrame], 0, 0);
	context.paint();
		
	currentFrame += 1;
		
	if(currentFrame >= numberOfFrames)
	{
		currentFrame = 0;
	}
		
	return(true);
		
} // onDraw()

So here we:

  • instantiate and hook up the Timeout,
  • grab a frame from our array and stuff it into the Context, and
  • do some frame number math.

Nothing to it.

Conclusion

Next time we’ll… dive back into the MVC series and look at the TreeStore to see how it differs from the ListStore. Don’t miss it, eh.

Comments? Questions? Observations?

Did we miss a tidbit of information that would make this post even more informative? Let's talk about it in the comments.

You can also subscribe via RSS so you won't miss anything. Thank you very much for dropping by.

© Copyright 2023 Ron Tarrant