6. Reusing a complex animation on different actors

6.1. Problem

You want to apply the same complex animation to several different actors.

6.2. Solution

Instead of animating each actor separately, create a rig: an empty container with an associated animation, which will be animated in lieu of animating the actor directly. Do this as follows:

  1. Initialise the stage and actors, including those to be animated.

  2. Define a ClutterContainer and a ClutterAnimator animation to animate it.

  3. When you need to animate an actor:

    1. Create an instance of the rig and its animator.

    2. Reparent the actor to the rig.

    3. Run the rig's animation.

For this solution, we're using JSON to define the animation and the user interface elements. For more details about this approach, see the chapter on ClutterScript.

Here's an extract of the JSON definition for the stage and one of five rectangles placed at its left edge (the full definition is in the appendix):

[
  {
    "type" : "ClutterStage",
    "id" : "stage",

    ... stage properties, signal handlers etc. ...

    "children" : [
      {
        "type" : "ClutterRectangle",
        "id" : "rect1",
        "color" : "white",
        "width" : 50,
        "height" : 50,
        "y" : 50,
        "reactive" : true,
        "signals" : [
          { "name" : "button-press-event", "handler" : "foo_button_pressed_cb" }
        ]
      },

      ... more children defined here ...
    ]
  }
]

The key point to note is how a signal handler is defined for the button-press-event, so that the foo_button_pressed_cb() function will trigger the animation when a (mouse) button is pressed on each rectangle.

The second JSON definition includes the rig (an empty ClutterGroup) and a ClutterAnimator to animate it. The animation moves the container across the stage and scales it to twice its original size. (This is the same code as in the appendix):

[
  {
    "type" : "ClutterGroup",
    "id" : "rig"
  },

  {
    "type" : "ClutterAnimator",
    "id" : "animator",
    "duration" : 2000,

    "properties" : [
      {
        "object" : "rig",
        "name" : "x",
        "ease-in" : true,
        "keys" : [
          [ 0.0, "linear", 0.0 ],
          [ 1.0, "easeOutCubic", 150.0 ]
        ]
      },
      {
        "object" : "rig",
        "name" : "scale-x",
        "ease-in" : true,
        "keys" : [
          [ 0.5, "linear", 1.0 ],
          [ 1.0, "easeOutBack", 2.0 ]
        ]
      },
      {
        "object" : "rig",
        "name" : "scale-y",
        "ease-in" : true,
        "keys" : [
          [ 0.5, "linear", 1.0 ],
          [ 1.0, "easeOutBack", 2.0 ]
        ]
      }
    ]
  }
]

The remaining parts of the application code load the user interface definition, setting up the stage and rectangles; and define the callback. The full code is in the appendix, but below is the most important part, the callback function:

gboolean
foo_button_pressed_cb (ClutterActor *actor,
                       ClutterEvent *event,
                       gpointer      user_data)
{
  ClutterScript *ui = CLUTTER_SCRIPT (user_data);
  ClutterStage *stage = CLUTTER_STAGE (clutter_script_get_object (ui, "stage"));

  ClutterScript *script;
  ClutterActor *rig;
  ClutterAnimator *animator;

  /* load the rig and its animator from a JSON file */
  script = clutter_script_new ();

  /* use a function defined statically in this source file to load the JSON */
  load_script_from_file (script, ANIMATION_FILE);

  clutter_script_get_objects (script,
                              "rig", &rig,
                              "animator", &animator,
                              NULL);

  /* remove the button press handler from the rectangle */
  g_signal_handlers_disconnect_by_func (actor,
                                        G_CALLBACK (foo_button_pressed_cb),
                                        NULL);

  /* add a callback to clean up the script when the rig is destroyed */
  g_object_set_data_full (G_OBJECT (rig), "script", script, g_object_unref);

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

  /* place the rig at the same coordinates on the stage as the rectangle */
  clutter_actor_set_position (rig,
                              clutter_actor_get_x (actor),
                              clutter_actor_get_y (actor));

  /* put the rectangle into the top-left corner of the rig */
  g_object_ref (actor);
  clutter_actor_remove_child (clutter_actor_get_parent (actor), actor);
  clutter_actor_add_child (rig, actor);

  clutter_actor_set_position (actor, 0, 0);

  /* animate the rig */
  clutter_animator_start (animator);

  return TRUE;
}

The code creates a new rig and associated animation at the point when the rectangle is clicked. It then positions the rig at the same coordinates as the rectangle, reparents the rectangle to the rig, and starts the rig's animation.

Note

The signal handler has to be declared non-static and you must use -export-dynamic as an option to the compiler, otherwise the function isn't visible to ClutterScript (as outlined in this recipe).

This is what the animation looks like:

6.3. Discussion

The above solution reparents an actor to be animated into a rig (an empty placeholder). The rig is a container which acts as a temporary parent for the actor we really want to animate. By animating the rig, it appears as though the actor inside it is being animated (but see these caveats). This means the same animation can be easily applied to different actors: create an instance of the rig, reparent an actor to it, then run the rig's animation. This is simpler than creating a separate animation for each actor individually, or reusing a single ClutterAnimator on different actors (see this section).

Using JSON enhances the animation's reusability (it's even potentially reusable in another application), makes the code simpler (an animation can be loaded directly from the script), and makes refactoring easier (the animation can be modified without recompiling the application code). However, it also puts some minor limitations on the animation's reusability; namely, you can only set absolute property values in a JSON animation definition. This makes JSON less useful in cases where you need to animate properties relative to their starting values: for example, "move 50 pixels along the x axis" or "rotate by 10 degrees more on the z axis". (This type of animation is probably less portable anyway.) In such cases, the programmable API may be a better option: see the ClutterAnimator documentation for examples.

6.3.1. One animation vs. many

In the sample code, a new instance of the rig and its animation are created for each actor. One side effect of this is that all of the actors can animate simultaneously with the "same" animation. If you don't want this behaviour, but still want to use a rig approach, you could create a single instance of the rig and its animation. Then, you could reparent each actor to it in turn.

To ensure that the rig only animates one actor (or group of actors) at a time, you could track whether the rig is currently animating (e.g. by examining the animation's timeline with clutter_animator_get_timeline()). Then, if the animation is running, prevent any other actor from being reparented to the rig.

Note that you would also need to "reset" the rig each time the animation completed (move it back to the right start values for its properties), ready to animate the next actor.

6.3.2. Caveats about animating a rig instead of an actor

There are a few issues to be aware of in cases where you animate a rig with contained actors, rather than animating the actor directly:

  • Animating a rig doesn't always produce the same visual effect as animating an actor directly. For example, compare the following cases:

    • You rotate an actor by 180 degrees in the y axis, then by 90 degrees in the z axis. The actor appears to rotate in a clockwise direction.

    • You rotate the parent container of an actor by 180 degrees in the y axis; then rotate the actor by 90 degrees in the z axis. The actor appears to rotate in an anti-clockwise direction. By rotating the container, the "back" of the actor faces the view point, so the actor's movement appears reversed. See this recipe for more details.

    There may be other situations where you get similar discrepancies.

  • Animating a rig doesn't change an actor's properties, but animating the actor does.

    When you animate a container rather than the actor directly, the reported properties of the actor may not reflect its visual appearance. For example, if you apply a scale animation to a container, the final scale of actors inside it (as returned by clutter_actor_get_scale()) will not reflect the scaling applied to their container; whereas directly animating the actors would cause their scale properties to change.

  • Reparenting an actor to a rig can cause the actor to "jump" to the rig's position, unless you align the actor to the rig first.

    Note that in the sample code, the position of the actor (x, y coordinates) is copied to the rig before the reparenting happens. The actor is then reparented to the rig, and positioned in the rig's top-left corner. So the actor appears to be in the same position, but is now actually inside a rig at the actor's old position.

    Why bother to do this? Because the rig has a default position of 0,0 (top-left of its container, the stage). If you reparent the actor to the rig, without first copying the actor's position to the rig, the actor appears to "jump" to the rig's position.

6.4. Full example

Note

The three separate code examples in this section constitute a single application which implements the above solution.

Example 5.5. ClutterScript JSON defining several rectangles with signal handlers

[
  {
    "type" : "ClutterStage",
    "id" : "stage",
    "width" : 300,
    "height" : 200,
    "color" : "#333355ff",

    "signals" : [
      { "name" : "destroy", "handler" : "clutter_main_quit" }
    ],

    "children" : [
      {
        "type" : "ClutterRectangle",
        "id" : "rect1",
        "color" : "white",
        "width" : 50,
        "height" : 50,
        "y" : 50,
        "reactive" : true,
        "signals" : [
          { "name" : "button-press-event", "handler" : "foo_button_pressed_cb" }
        ]
      },

      {
        "type" : "ClutterRectangle",
        "id" : "rect2",
        "color" : "blue",
        "width" : 50,
        "height" : 50,
        "y" : 50,
        "reactive" : true,
        "signals" : [
          { "name" : "button-press-event", "handler" : "foo_button_pressed_cb" }
        ]
      },

      {
        "type" : "ClutterRectangle",
        "id" : "rect3",
        "color" : "green",
        "width" : 50,
        "height" : 50,
        "y" : 50,
        "reactive" : true,
        "signals" : [
          { "name" : "button-press-event", "handler" : "foo_button_pressed_cb" }
        ]
      },

      {
        "type" : "ClutterRectangle",
        "id" : "rect4",
        "color" : "red",
        "width" : 50,
        "height" : 50,
        "y" : 50,
        "reactive" : true,
        "signals" : [
          { "name" : "button-press-event", "handler" : "foo_button_pressed_cb" }
        ]
      },

      {
        "type" : "ClutterRectangle",
        "id" : "rect5",
        "color" : "grey",
        "width" : 50,
        "height" : 50,
        "y" : 50,
        "reactive" : true,
        "signals" : [
          { "name" : "button-press-event", "handler" : "foo_button_pressed_cb" }
        ]
      }
    ]
  }
]


Example 5.6. ClutterScript JSON describing a "rig" and a ClutterAnimator animation

[
  {
    "type" : "ClutterGroup",
    "id" : "rig"
  },

  {
    "type" : "ClutterAnimator",
    "id" : "animator",
    "duration" : 2000,

    "properties" : [
      {
        "object" : "rig",
        "name" : "x",
        "ease-in" : true,
        "keys" : [
          [ 0.0, "linear", 0.0 ],
          [ 1.0, "easeOutCubic", 150.0 ]
        ]
      },
      {
        "object" : "rig",
        "name" : "scale-x",
        "ease-in" : true,
        "keys" : [
          [ 0.5, "linear", 1.0 ],
          [ 1.0, "easeOutBack", 2.0 ]
        ]
      },
      {
        "object" : "rig",
        "name" : "scale-y",
        "ease-in" : true,
        "keys" : [
          [ 0.5, "linear", 1.0 ],
          [ 1.0, "easeOutBack", 2.0 ]
        ]
      }
    ]
  }
]


Example 5.7. Loading ClutterScript from JSON files in response to events in a user interface

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

#define UI_FILE "animations-reuse-ui.json"
#define ANIMATION_FILE "animations-reuse-animation.json"

static gboolean
load_script_from_file (ClutterScript *script,
                       gchar         *filename)
{
  GError *error = NULL;

  clutter_script_load_from_file (script, filename, &error);

  if (error != NULL)
    {
      g_critical ("Error loading ClutterScript file %s\n%s", filename, error->message);
      g_error_free (error);
      exit (EXIT_FAILURE);
    }

  return TRUE;
}

gboolean
foo_button_pressed_cb (ClutterActor *actor,
                       ClutterEvent *event,
                       gpointer      user_data)
{
  ClutterScript *ui = CLUTTER_SCRIPT (user_data);
  ClutterStage *stage = CLUTTER_STAGE (clutter_script_get_object (ui, "stage"));

  ClutterScript *script;
  ClutterActor *rig;
  ClutterAnimator *animator;

  /* load the rig and its animator from a JSON file */
  script = clutter_script_new ();

  /* use a function defined statically in this source file to load the JSON */
  load_script_from_file (script, ANIMATION_FILE);

  clutter_script_get_objects (script,
                              "rig", &rig,
                              "animator", &animator,
                              NULL);

  /* remove the button press handler from the rectangle */
  g_signal_handlers_disconnect_by_func (actor,
                                        G_CALLBACK (foo_button_pressed_cb),
                                        NULL);

  /* add a callback to clean up the script when the rig is destroyed */
  g_object_set_data_full (G_OBJECT (rig), "script", script, g_object_unref);

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

  /* place the rig at the same coordinates on the stage as the rectangle */
  clutter_actor_set_position (rig,
                              clutter_actor_get_x (actor),
                              clutter_actor_get_y (actor));

  /* put the rectangle into the top-left corner of the rig */
  clutter_actor_reparent (actor, rig);

  clutter_actor_set_position (actor, 0, 0);

  /* animate the rig */
  clutter_animator_start (animator);

  return TRUE;
}

int
main (int argc, char *argv[])
{
  ClutterScript *script;
  ClutterActor *stage;

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

  script = clutter_script_new ();
  load_script_from_file (script, UI_FILE);

  clutter_script_connect_signals (script, script);

  clutter_script_get_objects (script,
                              "stage", &stage,
                              NULL);

  clutter_actor_show (stage);

  clutter_main ();

  g_object_unref (script);

  return EXIT_SUCCESS;
}