10. Animating an actor along a curved path

10.1. Problem

You want to animate an actor along a curved path: for example, to move an actor in a circle or spiral.

10.2. Solution

Create a ClutterPath to describe the path the actor should move along; then create a ClutterPathConstraint based on that path:

ClutterPath *path;
ClutterConstraint *constraint;

/* create the path */
path = clutter_path_new ();

/* first node is at 30,60 */
clutter_path_add_move_to (path, 30, 60);

/* add a curve to the top-right of the stage, with control
 * points relative to the start point at 30,60
 */
clutter_path_add_rel_curve_to (path,
                               120, 180,
                               180, 120,
                               240, 0);

/* create a constraint based on the path */
constraint = clutter_path_constraint_new (path, 0.0);

Note

For more on the types of curve and line segment available, see the ClutterPath API documentation.

Next, add the constraint to an actor; in this case, the actor is a red rectangle:

ClutterActor *rectangle;
ClutterActor *stage = clutter_stage_new ();

/* ...set size stage, color, etc... */

const ClutterColor *red_color = clutter_color_new (255, 0, 0, 255);

rectangle = clutter_rectangle_new_with_color (red_color);
clutter_actor_set_size (rectangle, 60, 60);

/* add the constraint to the rectangle; note that this
 * puts the rectangle at the start of the path, i.e. at position 30,60;
 * we also give the constraint a name, so we can use it from an implicit
 * animation
 */
clutter_actor_add_constraint_with_name (rectangle, "path", constraint);

/* add the rectangle to the stage */
clutter_actor_add_child (stage, rectangle);

Note how the constraint has to be assigned a name (here, "path") to make it accessible via implicit animations.

Finally, animate the constraint's offset property; which in turn moves the actor along the path:

ClutterTimeline *timeline;

/* create a timeline with 1000 milliseconds duration, which loops
 * indefinitely and reverses its direction each time it completes
 */
timeline = clutter_timeline_new (1000);
clutter_timeline_set_repeat_count (timeline, -1);
clutter_timeline_set_auto_reverse (timeline, TRUE);

/* animate the offset property on the constraint from 0.0 to 1.0;
 * note the syntax used to refer to the constraints metadata for the
 * rectangle actor:
 *
 *   "@constraints.<constraint name>.<property>"
 */
clutter_actor_animate_with_timeline (rectangle, CLUTTER_LINEAR, timeline,
                                     "@constraints.path.offset", 1.0,
                                     NULL);

/* ...show the stage, run the mainloop, free memory on exit... */

The full example shows how these fragments fit together. The animation produced by this example looks like this:

The second full example animates an actor around a simulated circle using a more complex ClutterPath.

10.3. Discussion

Animating an actor using ClutterPathConstraint is the recommended way to animate actors along curved paths. It replaces the older ClutterBehaviourPath.

A ClutterPathConstraint constrains an actor's x and y properties to a position along such a ClutterPath: a path through 2D space. The ClutterPath itself is composed of nodes (x,y positions in 2D space), connected by straight lines or (cubic) Bézier curves.

Note

ClutterPath doesn't have to be used in animations: it can also be used in drawing (see the non-rectangular actor recipe).

The actor's position along the path is determined by the constraint's offset property, which has a value between 0.0 and 1.0. When the offset is 0.0, the actor is at the beginning of the path; when the actor is at 1.0, the actor is at the end of the path. Between 0.0 and 1.0, the actor is some fraction of the way along the path.

If you immediately set the offset for the constraint (e.g. to 0.5), the actor is instantly placed at that position along the path: for offset = 0.5, at the halfway point.

By contrast, to animate an actor along a path, you animate the offset property of a ClutterPathConstraint. The actor's position along the path is dependent on the progress of the animation: when the animation starts, the actor is at the beginning of the path; by the end of the animation, it will have reached its end.

If you animate the constraint using a linear easing mode, the progress of the animation matches progress along the path: at half-way through the animation, the actor will be half-way along the path.

However, if you are using a non-linear easing mode (e.g. a quintic or cubic mode), the offset along the path and progress through the animation may differ. This is because the offset along the path is computed from the alpha value at that point in the animation; this in turn depends on the alpha function applied by the animation. (See the animations introduction for more details about alphas.)

One way to think about this is to imagine the actor making a journey along the path. The alpha function governs the actor's speed, including how it speeds up and slows down during its journey. The actor's speed may be constant (as in a linear easing mode). Alternatively, the actor's speed may not be constant: it might start out fast then slow down (ease out); or start slow and speed up (ease in); or start and end fast, but slow down in the middle (ease in and ease out); or some other more complex arrangement (as in the bounce and elastic easing modes). So where the actor is on the path at a particular time doesn't directly relate to how long it's been travelling: the position is determined both by how long it's been travelling, and changes in its speed throughout the journey.

10.3.1. Other ways to animate along a path

ClutterPathConstraint is the only decent way of animating along curves in a predictable and manageable fashion. It can also be used to animate along paths composed of straight lines, though this isn't essential: you can do straight line animations directly with ClutterAnimator, ClutterState or implicit animations. But if you need to animate between more than a half a dozen sets of points joined by straight lines, ClutterPathConstraint makes sense then too.

It is also possible to animate actors over very simple, non-Bézier curves without using ClutterPathConstraint. This can be done by animating the actor's position properties using a non-linear easing mode (see the ClutterAlpha documentation for available modes, or write your own custom alpha function). This example shows how to animate two actors on curved paths around each other without ClutterPathConstraint.

However, it is difficult to precisely calculate paths with this approach. It is also only practical where you have a very simple curve: if you want to chain together several curved motions (as in the circle example), this quickly becomes unwieldy.

Tip

If you want physics-based animation, look at clutter-box2d.

10.4. Full examples

Example 5.16. Using a ClutterPathConstraint with implicit animations to move an actor along a curved path

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

int
main (int   argc,
      char *argv[])
{
  ClutterActor *stage;
  ClutterPath *path;
  ClutterConstraint *constraint;
  ClutterActor *rectangle;
  ClutterTimeline *timeline;

  const ClutterColor *stage_color = clutter_color_new (51, 51, 85, 255);
  const ClutterColor *red_color = clutter_color_new (255, 0, 0, 255);

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

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

  /* create the path */
  path = clutter_path_new ();
  clutter_path_add_move_to (path, 30, 60);

  /* add a curve round to the top-right of the stage */
  clutter_path_add_rel_curve_to (path,
                                 120, 180,
                                 180, 120,
                                 240, 0);

  /* create a constraint based on the path */
  constraint = clutter_path_constraint_new (path, 0.0);

  /* put a rectangle at the start of the path */
  rectangle = clutter_rectangle_new_with_color (red_color);
  clutter_actor_set_size (rectangle, 60, 60);

  /* add the constraint to the rectangle */
  clutter_actor_add_constraint_with_name (rectangle, "path", constraint);

  /* add the rectangle to the stage */
  clutter_container_add_actor (CLUTTER_CONTAINER (stage), rectangle);

  /* set up the timeline */
  timeline = clutter_timeline_new (1000);
  clutter_timeline_set_repeat_count (timeline, -1);
  clutter_timeline_set_auto_reverse (timeline, TRUE);

  clutter_actor_animate_with_timeline (rectangle, CLUTTER_LINEAR, timeline,
                                       "@constraints.path.offset", 1.0,
                                       NULL);

  clutter_actor_show (stage);

  clutter_main ();

  return EXIT_SUCCESS;
}


Example 5.17. Using a ClutterPathConstraint with ClutterAnimator to animate an actor on a simulated circular path

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

#define STAGE_SIDE 400.0

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

/* Build a "circular" path out of 4 Bezier curves
 *
 * code modified from
 * http://git.clutter-project.org/dax/tree/dax/dax-traverser-clutter.c#n328
 *
 * see http://www.whizkidtech.redprince.net/bezier/circle/
 * for further explanation
 */
static ClutterPath *
build_circular_path (gfloat cx,
                     gfloat cy,
                     gfloat r)
{
  ClutterPath *path;
  static gfloat kappa = 4 * (G_SQRT2 - 1) / 3;

  path = clutter_path_new ();

  clutter_path_add_move_to (path, cx + r, cy);
  clutter_path_add_curve_to (path,
                             cx + r, cy + r * kappa,
                             cx + r * kappa, cy + r,
                             cx, cy + r);
  clutter_path_add_curve_to (path,
                             cx - r * kappa, cy + r,
                             cx - r, cy + r * kappa,
                             cx - r, cy);
  clutter_path_add_curve_to (path,
                             cx - r, cy - r * kappa,
                             cx - r * kappa, cy - r,
                             cx, cy - r);
  clutter_path_add_curve_to (path,
                             cx + r * kappa, cy - r,
                             cx + r, cy - r * kappa,
                             cx + r, cy);
  clutter_path_add_close (path);

  return path;
}

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[])
{
  ClutterPath *path;
  ClutterConstraint *constraint;
  ClutterAnimator *animator;
  ClutterTimeline *timeline;
  ClutterActor *stage;
  ClutterActor *rectangle;

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

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

  rectangle = clutter_rectangle_new_with_color (&red_color);
  clutter_actor_set_size (rectangle, STAGE_SIDE / 8, STAGE_SIDE / 8);
  clutter_actor_set_position (rectangle,
                              STAGE_SIDE / 2,
                              STAGE_SIDE / 2);

  clutter_container_add_actor (CLUTTER_CONTAINER (stage),
                               rectangle);

  /* set up a path and make a constraint with it */
  path = build_circular_path (STAGE_SIDE / 2,
                              STAGE_SIDE / 2,
                              STAGE_SIDE / 4);
  constraint = clutter_path_constraint_new (path, 0.0);

  /* apply the constraint to the rectangle; note that there
   * is no need to name the constraint, as we will be animating
   * the constraint's offset property directly using ClutterAnimator
   */
  clutter_actor_add_constraint (rectangle, constraint);

  /* animation to animate the path offset */
  animator = clutter_animator_new ();
  clutter_animator_set_duration (animator, 5000);

  /* use ClutterAnimator to animate the constraint directly */
  clutter_animator_set (animator,
                        constraint, "offset", CLUTTER_LINEAR, 0.0, 0.0,
                        constraint, "offset", CLUTTER_LINEAR, 1.0, 1.0,
                        NULL);

  timeline = clutter_animator_get_timeline (animator);
  clutter_timeline_set_repeat_count (timeline, -1);
  clutter_timeline_set_auto_reverse (timeline, TRUE);

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

  clutter_actor_show (stage);

  clutter_main ();

  /* clean up */
  g_object_unref (animator);

  return EXIT_SUCCESS;
}


Example 5.18. Animating actors on curved paths using easing modes

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

typedef struct {
  ClutterActor *red;
  ClutterActor *green;
  ClutterTimeline *timeline;
} State;

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

static void
reverse_timeline (ClutterTimeline *timeline)
{
  ClutterTimelineDirection dir = clutter_timeline_get_direction (timeline);

  if (dir == CLUTTER_TIMELINE_FORWARD)
    dir = CLUTTER_TIMELINE_BACKWARD;
  else
    dir = CLUTTER_TIMELINE_FORWARD;

  clutter_timeline_set_direction (timeline, dir);
}

/* a key press either starts the timeline or reverses it */
static gboolean
key_pressed_cb (ClutterActor *actor,
                ClutterEvent *event,
                gpointer      user_data)
{
  State *state = (State *) user_data;

  if (clutter_timeline_is_playing (state->timeline))
    reverse_timeline (state->timeline);
  else
    clutter_timeline_start (state->timeline);

  return TRUE;
}

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

  ClutterActor *stage;
  ClutterAnimator *animator;

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

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

  state->red = clutter_rectangle_new_with_color (&red_color);
  clutter_actor_set_size (state->red, 100, 100);
  clutter_actor_set_position (state->red, 300, 300);

  state->green = clutter_rectangle_new_with_color (&green_color);
  clutter_actor_set_size (state->green, 100, 100);
  clutter_actor_set_position (state->green, 0, 0);

  animator = clutter_animator_new ();
  clutter_animator_set_duration (animator, 1000);

  clutter_animator_set (animator,
                        state->red, "x", CLUTTER_LINEAR, 0.0, 300.0,
                        state->red, "y", CLUTTER_LINEAR, 0.0, 300.0,
                        state->red, "x", CLUTTER_LINEAR, 1.0, 0.0,
                        state->red, "y", CLUTTER_EASE_IN_QUINT, 1.0, 0.0,
                        NULL);

  clutter_animator_set (animator,
                        state->green, "x", CLUTTER_LINEAR, 0.0, 0.0,
                        state->green, "y", CLUTTER_LINEAR, 0.0, 0.0,
                        state->green, "x", CLUTTER_LINEAR, 1.0, 300.0,
                        state->green, "y", CLUTTER_EASE_IN_QUINT, 1.0, 300.0,
                        NULL);

  state->timeline = clutter_animator_get_timeline (animator);

  clutter_timeline_set_auto_reverse (state->timeline, TRUE);

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

  clutter_container_add (CLUTTER_CONTAINER (stage), state->red, state->green, NULL);

  clutter_actor_show (stage);

  clutter_main ();

  g_object_unref (animator);

  g_free (state);

  return EXIT_SUCCESS;
}