8. Looping an animation

8.1. Problem

You want to loop an animation so it plays multiple times.

8.2. Solutions

Each animation approach can be used to create a looping animation, as described in the following sections.

The animation implemented in each case is a simple repeated movement of a rectangle from the right (x = 150.0) to the left (x = 50.0) of the stage, and back again, looped; like this (just a few iterations):

8.2.1. Solution 1: looping an implicit animation

Implicit animations, started using clutter_actor_animate(), can be looped via their associated ClutterTimeline.

Create a ClutterTimeline which is set to loop:

ClutterTimeline *timeline = clutter_timeline_new (1000);
clutter_timeline_set_repeat_count (timeline, -1);

Use this timeline when starting an implicit animation on an actor; in this case, to animate the actor's x coordinate from its initial value to 50.0:

/* assume actor is a ClutterActor instance */

/* actor's initial x value is 150.0 */
clutter_actor_set_x (actor, 150.0);

/* animate the actor (starting the timeline is implicit) */
clutter_actor_animate_with_timeline (actor,
                                     CLUTTER_LINEAR,
                                     timeline,
                                     "x", 50.0,
                                     NULL);

One further technique is to repeatedly reverse the timeline's direction to create a "closed loop" animation (one which returns to its origin at the end of each iteration). See this section for details.

The full code example shows how to run an implicit animation on a loop.

8.2.2. Solution 2: looping with ClutterAnimator

A ClutterAnimator animation can also be looped via its ClutterTimeline. However, as ClutterAnimator enables more complex animations, you don't have to manually invert the timeline at the end of each iteration. Instead, you can animate an actor's properties back to their initial values at the end of each iteration of the loop.

Creating the timeline and setting it to loop is the same as for implicit animations:

ClutterTimeline *timeline = clutter_timeline_new (2000);
clutter_timeline_set_repeat_count (timeline, -1);

Note that the timeline is twice the length of the one for the implicit animation: this is because, unlike the implicit animation, the movement from right to left and back again is a single animation. By contrast, in the implicit animation, the timeline runs forward, for the right to left movement; and then backwards, for the left to right movement. So rather than a 1000ms timeline running twice (once forward, once backward for the implicit animation), we have a 2000ms timeline running once (for ClutterAnimator).

Next, create a ClutterAnimator which animates the actor from right to left, then left to right:

/* assume actor is a ClutterActor instance */
ClutterAnimator *animator = clutter_animator_new ();

/* use the looping timeline as the timeline for the animator */
clutter_animator_set_timeline (animator, timeline);

/* set positions for the actor at various points through the animation:
 * at progress 0.0, x = 150.0 (right of the stage)
 * at progress 0.5, x = 50.0 (left of the stage)
 * at progress 1.0, x = 150.0 again (back to the right)
 */
clutter_animator_set (animator,
                      actor, "x", CLUTTER_LINEAR, 0.0, 150.0,
                      actor, "x", CLUTTER_LINEAR, 0.5, 50.0,
                      actor, "x", CLUTTER_LINEAR, 1.0, 150.0,
                      NULL);

Finally, start the animation:

clutter_animator_start (animator);

See the full example for more details.

8.2.3. Solution 3: looping with ClutterState

You can loop ClutterState animations by creating a cycle of states which "swallows its own tail": i.e. goes from a start state, through intermediate state(s), back to the start state, then again through the intermediate states(s), back to the start state, etc., ad infinitum.

For the animation we're implementing, there are two states the actor transitions between:

  1. The actor's x value is 150.0 (the start/end state, on the right of the stage).

  2. The actor's x value is 50.0 (the intermediate state, on the left of the stage).

Here is how to add those states to a ClutterState instance:

ClutterState *transitions = clutter_state_new ();

/* the duration for a transition from any state to any other is 1 second */
clutter_state_set_duration (transitions, NULL, NULL, 1000);

clutter_state_set (transitions, NULL, "right",
                   actor, "x", CLUTTER_LINEAR, 150.0,
                   NULL);

clutter_state_set (transitions, NULL, "left",
                   actor, "x", CLUTTER_LINEAR, 50.0,
                   NULL);

You also need a handler to move the ClutterState to its next state, called each time a state transition is completed:

/* handler to move the ClutterState to its next state */
static void
next_state (ClutterState *transitions,
            gpointer      user_data)
{
  const gchar *state = clutter_state_get_state (transitions);

  if (g_strcmp0 (state, "right") == 0)
    clutter_state_set_state (transitions, "left");
  else
    clutter_state_set_state (transitions, "right");
}

Then connect the ClutterState's completed signal to the handler, so that each time a state is reached, the transition to the next state begins:

/* connect the ClutterState completed signal to the handler */
g_signal_connect (transitions,
                  "completed",
                  G_CALLBACK (next_state),
                  NULL);

Finally, put the ClutterState into the start state to begin the animation:

clutter_state_warp_to_state (transitions, "right");

See the full example for more details.

8.3. Discussion

We use two different approaches to looping in the solutions:

  1. Setting the ClutterTimeline to loop (via clutter_timeline_set_repeat_count()). This is the best approach where the timeline is explicit (for ClutterAnimator and implicit animations).

  2. Cycling through states in a ClutterState. In this case, the timeline is implicit and we don't need to manually control it: the loop is a consequence of cycling repeatedly through a series of states.

The following sections cover some other aspects of looping animations.

8.3.1. Looping a fixed number of times

ClutterTimeline doesn't have any built-in functionality to support looping a certain number of times. But it is reasonably easy to count the number of iterations completed and stop the animation when some limit is reached.

For example, you could use a static counter to keep track of the iteration count:

static guint counter = 0;

Implement the looping behaviour as in the above solutions, but use a callback function to set/reset the counter each time the timeline completes. For example, for the ClutterAnimator solution, you would connect the completed signal of the timeline to a callback function:

g_signal_connect (timeline,
                  "completed",
                  G_CALLBACK (timeline_completed_cb),
                  NULL);

And implement a callback function which resets the counter and stops the timeline if more than two iterations have been counted:

static void
timeline_completed_cb (ClutterTimeline *timeline,
                       gpointer         user_data)
{
  counter++;

  if (counter > 2)
    {
      counter = 0;
      clutter_timeline_stop (timeline);
    }
}

Note that it's simple to count iterations and control the timeline using ClutterAnimator or ClutterState, as the whole animation (right to left and back) is a discrete unit. Doing the same with implicit animations is possible (one forward + one backward run along the timeline is one iteration). But you will be really stretching the implicit animation API beyond its intended use cases.

8.3.2. Creating a "closed loop" with an implicit animation

When using implicit animations, at the end of the timeline (before the next iteration of the loop), an actor's properties "jump" back to their initial values (as they were when the timeline started). For example, in the earlier solution, the actor's initial x value was 150.0; so the default behaviour on each iteration of the loop would be to animate the actor to x = 50.0 then jump it immediately back to x = 150.0, before continuing the loop.

To prevent this happening, you can create a "closed" loop: animate the actor's properties away from their initial values, then back again.

This could be done manually, by creating two separate animations, one the inverse of the other, and chaining them together.

However, a simpler solution is to run forward through the timeline once, and have the timeline invert itself when its end is reached. The animation then continues, but in reverse. Once the backward iteration completes, the timeline sets itself to run forward again, etc.

To make a timeline reverse its direction each time it completes, use the clutter_timeline_set_auto_reverse() function:

clutter_timeline_set_auto_reverse (timeline, TRUE);

This is the approach used in the example, which results in a smooth, repeated right to left, left to right motion.

See this recipe for more details about inverting a timeline.

8.4. Full examples

Example 5.11. Looping an implicit animation

#include <stdlib.h>
#include <clutter/clutter.h>

static const ClutterColor stage_color = { 0x33, 0x33, 0x55, 0xff };
static const ClutterColor red_color = { 0xff, 0x00, 0x00, 0xff };

typedef struct
{
  ClutterActor    *actor;
  ClutterTimeline *timeline;
} State;

static gboolean
key_pressed_cb (ClutterActor *actor,
                ClutterEvent *event,
                gpointer      user_data)
{
  State *state = (State *) user_data;

  /* only start animating if actor isn't animating already */
  if (clutter_actor_get_animation (state->actor) == NULL)
    clutter_actor_animate_with_timeline (state->actor,
                                         CLUTTER_LINEAR,
                                         state->timeline,
                                         "x", 50.0,
                                         NULL);

  return TRUE;
}

int
main (int   argc,
      char *argv[])
{
  State *state = g_new0 (State, 1);

  ClutterActor *stage;

  if (clutter_init (&argc, &argv) != CLUTTER_INIT_SUCCESS)
    return 1;

  stage = clutter_stage_new ();
  clutter_actor_set_size (stage, 300, 200);
  clutter_stage_set_color (CLUTTER_STAGE (stage), &stage_color);
  g_signal_connect (stage, "destroy", G_CALLBACK (clutter_main_quit), NULL);

  state->actor = clutter_rectangle_new_with_color (&red_color);
  clutter_actor_set_size (state->actor, 100, 100);
  clutter_actor_set_position (state->actor, 150, 50);

  state->timeline = clutter_timeline_new (1000);
  clutter_timeline_set_repeat_count (state->timeline, -1);
  clutter_timeline_set_auto_reverse (state->timeline, TRUE);

  g_signal_connect (stage,
                    "key-press-event",
                    G_CALLBACK (key_pressed_cb),
                    state);

  clutter_container_add_actor (CLUTTER_CONTAINER (stage), state->actor);

  clutter_actor_show (stage);

  clutter_main ();

  g_object_unref (state->timeline);
  g_free (state);

  return EXIT_SUCCESS;
}


Example 5.12. Looping with ClutterAnimator

#include <stdlib.h>
#include <clutter/clutter.h>

static const ClutterColor stage_color = { 0x33, 0x33, 0x55, 0xff };
static const ClutterColor red_color = { 0xff, 0x00, 0x00, 0xff };

static gboolean
key_pressed_cb (ClutterActor *actor,
                ClutterEvent *event,
                gpointer      user_data)
{
  ClutterTimeline *timeline = CLUTTER_TIMELINE (user_data);

  if (!clutter_timeline_is_playing (timeline))
    clutter_timeline_start (timeline);

  return TRUE;
}

int
main (int   argc,
      char *argv[])
{
  ClutterActor *stage;
  ClutterActor *actor;
  ClutterTimeline *timeline;
  ClutterAnimator *animator;

  if (clutter_init (&argc, &argv) != CLUTTER_INIT_SUCCESS)
    return 1;

  stage = clutter_stage_new ();
  clutter_actor_set_size (stage, 300, 200);
  clutter_stage_set_color (CLUTTER_STAGE (stage), &stage_color);
  g_signal_connect (stage, "destroy", G_CALLBACK (clutter_main_quit), NULL);

  actor = clutter_rectangle_new_with_color (&red_color);
  clutter_actor_set_size (actor, 100, 100);
  clutter_actor_set_position (actor, 150, 50);

  timeline = clutter_timeline_new (2000);
  clutter_timeline_set_repeat_count (timeline, -1);

  animator = clutter_animator_new ();
  clutter_animator_set_timeline (animator, timeline);

  clutter_animator_set (animator,
                        actor, "x", CLUTTER_LINEAR, 0.0, 150.0,
                        actor, "x", CLUTTER_LINEAR, 0.5, 50.0,
                        actor, "x", CLUTTER_LINEAR, 1.0, 150.0,
                        NULL);

  clutter_container_add_actor (CLUTTER_CONTAINER (stage), actor);

  g_signal_connect (stage,
                    "key-press-event",
                    G_CALLBACK (key_pressed_cb),
                    timeline);

  clutter_actor_show (stage);

  clutter_main ();

  g_object_unref (animator);

  return EXIT_SUCCESS;
}


Example 5.13. Looping with ClutterState

#include <stdlib.h>
#include <clutter/clutter.h>

static const ClutterColor stage_color = { 0x33, 0x33, 0x55, 0xff };
static const ClutterColor red_color = { 0xff, 0x00, 0x00, 0xff };

static void
next_state (ClutterState *transitions,
            gpointer      user_data)
{
  const gchar *state = clutter_state_get_state (transitions);

  if (g_strcmp0 (state, "right") == 0)
    clutter_state_set_state (transitions, "left");
  else
    clutter_state_set_state (transitions, "right");
}

static gboolean
key_pressed_cb (ClutterActor *actor,
                ClutterEvent *event,
                gpointer      user_data)
{
  ClutterState *transitions = CLUTTER_STATE (user_data);

  if (!clutter_timeline_is_playing (clutter_state_get_timeline (transitions)))
    next_state (transitions, NULL);

  return TRUE;
}

int
main (int   argc,
      char *argv[])
{
  ClutterActor *stage;
  ClutterActor *actor;
  ClutterState *transitions;

  if (clutter_init (&argc, &argv) != CLUTTER_INIT_SUCCESS)
    return 1;

  stage = clutter_stage_new ();
  clutter_actor_set_size (stage, 300, 200);
  clutter_stage_set_color (CLUTTER_STAGE (stage), &stage_color);
  g_signal_connect (stage, "destroy", G_CALLBACK (clutter_main_quit), NULL);

  actor = clutter_rectangle_new_with_color (&red_color);
  clutter_actor_set_position (actor, 150, 50);
  clutter_actor_set_size (actor, 100, 100);

  transitions = clutter_state_new ();
  clutter_state_set_duration (transitions, NULL, NULL, 1000);

  clutter_state_set (transitions, NULL, "right",
                     actor, "x", CLUTTER_LINEAR, 150.0,
                     NULL);

  clutter_state_set (transitions, NULL, "left",
                     actor, "x", CLUTTER_LINEAR, 50.0,
                     NULL);

  clutter_state_warp_to_state (transitions, "right");

  g_signal_connect (stage,
                    "key-press-event",
                    G_CALLBACK (key_pressed_cb),
                    transitions);

  g_signal_connect (transitions,
                    "completed",
                    G_CALLBACK (next_state),
                    NULL);

  clutter_container_add_actor (CLUTTER_CONTAINER (stage), actor);

  clutter_actor_show (stage);

  clutter_main ();

  g_object_unref (transitions);

  return EXIT_SUCCESS;
}