diff --git a/clutter/Makefile.am b/clutter/Makefile.am index ecb3e777f..39994e4c2 100644 --- a/clutter/Makefile.am +++ b/clutter/Makefile.am @@ -46,6 +46,7 @@ BUILT_SOURCES = $(MARSHALFILES) $(ENUMFILES) # please, keep this sorted alphabetically source_h = \ $(srcdir)/clutter-actor.h \ + $(srcdir)/clutter-actor-clone.h \ $(srcdir)/clutter-alpha.h \ $(srcdir)/clutter-animatable.h \ $(srcdir)/clutter-animation.h \ @@ -136,6 +137,7 @@ CLEANFILES = $(STAMPFILES) # please, keep this sorted alphabetically source_c = \ clutter-actor.c \ + clutter-actor-clone.c \ clutter-alpha.c \ clutter-animatable.c \ clutter-animation.c \ diff --git a/clutter/clutter-actor-clone.c b/clutter/clutter-actor-clone.c new file mode 100644 index 000000000..0e5b2b881 --- /dev/null +++ b/clutter/clutter-actor-clone.c @@ -0,0 +1,307 @@ +/* + * Clutter. + * + * An OpenGL based 'interactive canvas' library. + * + * Copyright (C) 2008 Intel Corporation. + * + * Authored By: Robert Bragg + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +/** + * SECTION:clutter-actor-clone + * @short_description: An actor that displays a scaled clone of another + * actor. + * + * #ClutterActorClone is a #ClutterActor which draws with the paint + * function of another actor, scaled to fit its own allocation. + */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "clutter-color.h" +#include "clutter-debug.h" +#include "clutter-main.h" +#include "clutter-private.h" +#include "clutter-actor-clone.h" + +#include "cogl/cogl.h" + +G_DEFINE_TYPE (ClutterActorClone, clutter_actor_clone, CLUTTER_TYPE_ACTOR); + +enum +{ + PROP_0, + + PROP_CLONE_SOURCE +}; + +#define CLUTTER_ACTOR_CLONE_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE ((obj), \ + CLUTTER_TYPE_ACTOR_CLONE, \ + ClutterActorClonePrivate)) + +struct _ClutterActorClonePrivate +{ + ClutterActor *clone_source; +}; + +static void +clutter_actor_clone_get_preferred_width (ClutterActor *self, + ClutterUnit for_height, + ClutterUnit *min_width_p, + ClutterUnit *natural_width_p) +{ + ClutterActorClonePrivate *priv = CLUTTER_ACTOR_CLONE (self)->priv; + ClutterActor *clone_source = priv->clone_source; + + clutter_actor_get_preferred_width (clone_source, + for_height, + min_width_p, + natural_width_p); +} + +static void +clutter_actor_clone_get_preferred_height (ClutterActor *self, + ClutterUnit for_width, + ClutterUnit *min_height_p, + ClutterUnit *natural_height_p) +{ + ClutterActorClonePrivate *priv = CLUTTER_ACTOR_CLONE (self)->priv; + ClutterActor *clone_source = priv->clone_source; + + clutter_actor_get_preferred_height (clone_source, + for_width, + min_height_p, + natural_height_p); +} + +static void +clutter_actor_clone_paint (ClutterActor *self) +{ + ClutterActorClone *clone = CLUTTER_ACTOR_CLONE (self); + ClutterActorClonePrivate *priv = clone->priv; + ClutterGeometry geom; + ClutterGeometry clone_source_geom; + float x_scale; + float y_scale; + ClutterActor *stage; + ClutterPerspective perspective; + guint stage_width; + guint stage_height; + + CLUTTER_NOTE (PAINT, + "painting clone actor '%s'", + clutter_actor_get_name (self) ? clutter_actor_get_name (self) + : "unknown"); + + clutter_actor_get_allocation_geometry (self, &geom); + clutter_actor_get_allocation_geometry (priv->clone_source, + &clone_source_geom); + + /* We need to scale what the clone-source actor paints to fill our own + * allocation... */ + x_scale = geom.width / clone_source_geom.width; + y_scale = geom.height / clone_source_geom.height; + + /* Once we have calculated the scale factors it's a case of pushing + * the scale factors to the bottom of the current model view stack... + */ + if (x_scale != 1.0 || y_scale != 1.0) + { + /* + * FIXME - this is quite expensive, it would be good it clutter kept + * the model view matrix changes made by cogl_setup_viewport seperate + * from those made while painting actors so we could simply replace the + * later by multiplying it with the scale avoiding the call to + * _clutter_actor_apply_modelview_transform_recursive. + */ + + stage = clutter_actor_get_stage (self); + if (!stage) + return; + + clutter_actor_get_size (stage, &stage_width, &stage_height); + clutter_stage_get_perspectivex (CLUTTER_STAGE (stage), &perspective); + + cogl_setup_viewport (stage_width, stage_height, + perspective.fovy, + perspective.aspect, + perspective.z_near, + perspective.z_far); + + cogl_scale (COGL_FIXED_FROM_FLOAT (x_scale), + COGL_FIXED_FROM_FLOAT (y_scale)); + + _clutter_actor_apply_modelview_transform_recursive (self, NULL); + } + + /* The final bits of magic: + * - We need to make sure that when the clone-source actor's paint method + * calls clutter_actor_get_paint_opacity, it traverses our parent not it's + * real parent. + * - We need to stop clutter_actor_paint applying the model view matrix of + * the clone source actor. + */ + _clutter_actor_set_opacity_parent (priv->clone_source, + clutter_actor_get_parent (self)); + _clutter_actor_set_enable_model_view_transform (priv->clone_source, FALSE); + + clutter_actor_paint (priv->clone_source); + + _clutter_actor_set_enable_model_view_transform (priv->clone_source, TRUE); + _clutter_actor_set_opacity_parent (priv->clone_source, NULL); +} + +static void +clutter_actor_clone_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ClutterActorClone *clone = CLUTTER_ACTOR_CLONE(object); + + switch (prop_id) + { + case PROP_CLONE_SOURCE: + clone->priv->clone_source = g_value_get_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clutter_actor_clone_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ClutterActorClonePrivate *priv = CLUTTER_ACTOR_CLONE(object)->priv; + + switch (prop_id) + { + case PROP_CLONE_SOURCE: + g_value_set_object (value, priv->clone_source); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clutter_actor_clone_finalize (GObject *object) +{ + G_OBJECT_CLASS (clutter_actor_clone_parent_class)->finalize (object); +} + +static void +clutter_actor_clone_dispose (GObject *object) +{ + G_OBJECT_CLASS (clutter_actor_clone_parent_class)->dispose (object); +} + +static void +clutter_actor_clone_class_init (ClutterActorCloneClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + + actor_class->paint = clutter_actor_clone_paint; + actor_class->get_preferred_width = + clutter_actor_clone_get_preferred_width; + actor_class->get_preferred_height = + clutter_actor_clone_get_preferred_height; + + gobject_class->finalize = clutter_actor_clone_finalize; + gobject_class->dispose = clutter_actor_clone_dispose; + gobject_class->set_property = clutter_actor_clone_set_property; + gobject_class->get_property = clutter_actor_clone_get_property; + + /** + * ClutterActorClone:clone-source + * + * This specifies the source actor being cloned + * + * Since: 1.0 + */ + g_object_class_install_property + (gobject_class, + PROP_CLONE_SOURCE, + g_param_spec_object ("clone-source", + "Clone Source", + "Specifies the actor to be cloned", + CLUTTER_TYPE_ACTOR, + G_PARAM_CONSTRUCT_ONLY + |G_PARAM_WRITABLE + |G_PARAM_READABLE)); + + g_type_class_add_private (gobject_class, sizeof (ClutterActorClonePrivate)); +} + +static void +clutter_actor_clone_init (ClutterActorClone *self) +{ + ClutterActorClonePrivate *priv; + + self->priv = priv = CLUTTER_ACTOR_CLONE_GET_PRIVATE (self); + + priv->clone_source = NULL; +} + +/** + * clutter_actor_clone_new: + * + * Creates a new #ClutterActor which clones clone_source. + * + * Return value: a new #ClutterActor + * + * Since: 1.0 + */ +ClutterActor * +clutter_actor_clone_new (ClutterActor *clone_source) +{ + return g_object_new (CLUTTER_TYPE_ACTOR_CLONE, + "clone-source", clone_source, + NULL); +} + +/** + * clutter_actor_clone_get_clone_source: + * + * @clone: a #ClutterActorClone + * + * Retrieves the source #ClutterActor being clone by @clone + * + * Return value: the actor source for the clone + * + * Since: 1.0 + */ +ClutterActor * +clutter_actor_clone_get_clone_source (ClutterActorClone *clone) +{ + ClutterActorClonePrivate *priv; + + g_return_val_if_fail (CLUTTER_IS_ACTOR_CLONE (clone), NULL); + + priv = clone->priv; + + return priv->clone_source; +} + diff --git a/clutter/clutter-actor-clone.h b/clutter/clutter-actor-clone.h new file mode 100644 index 000000000..c74a45d76 --- /dev/null +++ b/clutter/clutter-actor-clone.h @@ -0,0 +1,76 @@ +/* + * Clutter. + * + * An OpenGL based 'interactive canvas' library. + * + * Copyright (C) 2008 Intel Corporation. + * + * Authored By: Robert Bragg + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#if !defined(__CLUTTER_H_INSIDE__) && !defined(CLUTTER_COMPILATION) +#error "Only can be included directly." +#endif + +#ifndef __CLUTTER_ACTOR_CLONE_H__ +#define __CLUTTER_ACTOR_CLONE_H__ + +#include +#include +#include + +G_BEGIN_DECLS + +#define CLUTTER_TYPE_ACTOR_CLONE (clutter_actor_clone_get_type()) +#define CLUTTER_ACTOR_CLONE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CLUTTER_TYPE_ACTOR_CLONE, ClutterActorClone)) +#define CLUTTER_ACTOR_CLONE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), CLUTTER_TYPE_ACTOR_CLONE, ClutterActorCloneClass)) +#define CLUTTER_IS_ACTOR_CLONE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CLUTTER_TYPE_ACTOR_CLONE)) +#define CLUTTER_IS_ACTOR_CLONE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CLUTTER_TYPE_ACTOR_CLONE)) +#define CLUTTER_ACTOR_CLONE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), CLUTTER_TYPE_ACTOR_CLONE, ClutterActorCloneClass)) + +typedef struct _ClutterActorClone ClutterActorClone; +typedef struct _ClutterActorCloneClass ClutterActorCloneClass; +typedef struct _ClutterActorClonePrivate ClutterActorClonePrivate; + +struct _ClutterActorClone +{ + /*< private >*/ + ClutterActor parent; + + ClutterActorClonePrivate *priv; +}; + +struct _ClutterActorCloneClass +{ + /*< private >*/ + ClutterActorClass parent_class; + + /* padding for future expansion */ + void (*_clutter_actor_clone1) (void); + void (*_clutter_actor_clone2) (void); + void (*_clutter_actor_clone3) (void); + void (*_clutter_actor_clone4) (void); +}; + +GType clutter_actor_clone_get_type (void) G_GNUC_CONST; + +ClutterActor *clutter_actor_clone_new (ClutterActor *clone_source); + +ClutterActor *clutter_actor_clone_get_clone_source (ClutterActorClone *clone); + +G_END_DECLS + +#endif /* __CLUTTER_ACTOR_CLONE_H__ */ diff --git a/clutter/clutter-actor.c b/clutter/clutter-actor.c index 2f39c4f0c..53ffac0eb 100644 --- a/clutter/clutter-actor.c +++ b/clutter/clutter-actor.c @@ -263,6 +263,9 @@ struct _ClutterActorPrivate ShaderData *shader_data; PangoContext *pango_context; + + ClutterActor *opacity_parent; + gboolean enable_model_view_transform; }; enum @@ -1482,7 +1485,8 @@ clutter_actor_paint (ClutterActor *self) cogl_push_matrix(); - _clutter_actor_apply_modelview_transform (self); + if (priv->enable_model_view_transform) + _clutter_actor_apply_modelview_transform (self); if (priv->has_clip) { @@ -3029,6 +3033,9 @@ clutter_actor_init (ClutterActor *self) priv->needs_height_request = TRUE; priv->needs_allocation = TRUE; + priv->opacity_parent = NULL; + priv->enable_model_view_transform = TRUE; + memset (priv->clip, 0, sizeof (ClutterUnit) * 4); } @@ -5016,7 +5023,10 @@ clutter_actor_get_paint_opacity (ClutterActor *self) priv = self->priv; - parent = priv->parent_actor; + if (priv->opacity_parent) + parent = priv->opacity_parent; + else + parent = priv->parent_actor; /* Factor in the actual actors opacity with parents */ if (G_LIKELY (parent)) @@ -7703,3 +7713,26 @@ clutter_actor_create_pango_context (ClutterActor *self) return retval; } + +/* Allows overriding the parent traversed when querying an actors paint + * opacity. Used by ClutterActorClone. */ +void +_clutter_actor_set_opacity_parent (ClutterActor *self, + ClutterActor *parent) +{ + g_return_if_fail (CLUTTER_IS_ACTOR (self)); + + self->priv->opacity_parent = parent; +} + +/* Allows you to disable applying the actors model view transform during + * a paint. Used by ClutterActorClone. */ +void +_clutter_actor_set_enable_model_view_transform (ClutterActor *self, + gboolean enable) +{ + g_return_if_fail (CLUTTER_IS_ACTOR (self)); + + self->priv->enable_model_view_transform = enable; +} + diff --git a/clutter/clutter-private.h b/clutter/clutter-private.h index c65bfb066..a443ed85f 100644 --- a/clutter/clutter-private.h +++ b/clutter/clutter-private.h @@ -219,6 +219,12 @@ gboolean _clutter_boolean_handled_accumulator (GSignalInvocationHint *ihint, void _clutter_actor_apply_modelview_transform_recursive (ClutterActor *self, ClutterActor *ancestor); +void _clutter_actor_set_opacity_parent (ClutterActor *self, + ClutterActor *parent); + +void _clutter_actor_set_enable_model_view_transform (ClutterActor *self, + gboolean enable); + G_END_DECLS #endif /* _HAVE_CLUTTER_PRIVATE_H */ diff --git a/clutter/clutter.h b/clutter/clutter.h index 478d7b19f..61dae61bb 100644 --- a/clutter/clutter.h +++ b/clutter/clutter.h @@ -29,6 +29,7 @@ #define __CLUTTER_H_INSIDE__ #include "clutter-actor.h" +#include "clutter-actor-clone.h" #include "clutter-alpha.h" #include "clutter-animatable.h" #include "clutter-animation.h" diff --git a/tests/interactive/Makefile.am b/tests/interactive/Makefile.am index 8c20d7c9e..54b6c4849 100644 --- a/tests/interactive/Makefile.am +++ b/tests/interactive/Makefile.am @@ -5,6 +5,7 @@ UNIT_TESTS = \ test-offscreen.c \ test-scale.c \ test-actors.c \ + test-actors2.c \ test-behave.c \ test-project.c \ test-perspective.c \ diff --git a/tests/interactive/test-actors2.c b/tests/interactive/test-actors2.c new file mode 100644 index 000000000..ed06ff1ca --- /dev/null +++ b/tests/interactive/test-actors2.c @@ -0,0 +1,269 @@ +#include + +#if defined (_MSC_VER) && !defined (_USE_MATH_DEFINES) +#define _USE_MATH_DEFINES +#endif + +#include +#include +#include +#include +#include + +#define TRAILS 0 +#define NHANDS 6 +#define RADIUS ((CLUTTER_STAGE_WIDTH()+CLUTTER_STAGE_HEIGHT())/NHANDS) + +typedef struct SuperOH +{ + ClutterActor **hand, *bgtex; + ClutterActor *group; + +} SuperOH; + +static gint n_hands = NHANDS; + +static GOptionEntry super_oh_entries[] = { + { + "num-hands", 'n', + 0, + G_OPTION_ARG_INT, &n_hands, + "Number of hands", "HANDS" + }, + { NULL } +}; + +/* input handler */ +static gboolean +input_cb (ClutterActor *stage, + ClutterEvent *event, + gpointer data) +{ + SuperOH *oh = data; + + if (event->type == CLUTTER_BUTTON_PRESS) + { + ClutterButtonEvent *button_event; + ClutterActor *e; + gint x, y; + + clutter_event_get_coords (event, &x, &y); + + button_event = (ClutterButtonEvent *) event; + g_print ("*** button press event (button:%d) ***\n", + button_event->button); + + e = clutter_stage_get_actor_at_pos (CLUTTER_STAGE (stage), x, y); + + if (e && (CLUTTER_IS_TEXTURE (e) || CLUTTER_IS_CLONE_TEXTURE (e))) + { + clutter_actor_hide (e); + return TRUE; + } + } + else if (event->type == CLUTTER_KEY_RELEASE) + { + ClutterKeyEvent *kev = (ClutterKeyEvent *) event; + + g_print ("*** key press event (key:%c) ***\n", + clutter_key_event_symbol (kev)); + + if (clutter_key_event_symbol (kev) == CLUTTER_q) + { + clutter_main_quit (); + return TRUE; + } + else if (clutter_key_event_symbol (kev) == CLUTTER_r) + { + gint i; + + for (i = 0; i < n_hands; i++) + clutter_actor_show (oh->hand[i]); + + return TRUE; + } + } + + return FALSE; +} + + +/* Timeline handler */ +static void +frame_cb (ClutterTimeline *timeline, + gint frame_num, + gpointer data) +{ + SuperOH *oh = data; + gint i; + + /* Rotate everything clockwise about stage center*/ + + clutter_actor_set_rotation (CLUTTER_ACTOR (oh->group), + CLUTTER_Z_AXIS, + frame_num, + CLUTTER_STAGE_WIDTH () / 2, + CLUTTER_STAGE_HEIGHT () / 2, + 0); + + for (i = 0; i < n_hands; i++) + { + gdouble scale_x, scale_y; + + clutter_actor_get_scale (oh->hand[i], &scale_x, &scale_y); + + /* Rotate each hand around there centers - to get this we need + * to take into account any scaling. + * + * FIXME: scaling causes drift so disabled for now. Need rotation + * unit based functions to fix. + */ + clutter_actor_set_rotation (oh->hand[i], CLUTTER_Z_AXIS, + - 6.0 * frame_num, 0, 0, 0); + } +} + +G_MODULE_EXPORT int +test_actors2_main (int argc, char *argv[]) +{ + ClutterTimeline *timeline; + ClutterAlpha *alpha; + ClutterBehaviour *scaler_1, *scaler_2; + ClutterActor *stage; + ClutterColor stage_color = { 0x61, 0x64, 0x8c, 0xff }; + SuperOH *oh; + gint i; + GError *error; + ClutterActor *real_hand, *tmp; + ClutterColor clr= {0xff, 0xff, 0x00, 0xff}; + + error = NULL; + + clutter_init_with_args (&argc, &argv, + NULL, + super_oh_entries, + NULL, + &error); + if (error) + { + g_warning ("Unable to initialise Clutter:\n%s", + error->message); + g_error_free (error); + + exit (1); + } + + stage = clutter_stage_get_default (); + clutter_actor_set_size (stage, 800, 600); + + clutter_stage_set_title (CLUTTER_STAGE (stage), "Actors Test"); + clutter_stage_set_color (CLUTTER_STAGE (stage), + &stage_color); + + oh = g_new(SuperOH, 1); + + /* Create a timeline to manage animation */ + timeline = clutter_timeline_new (360, 60); /* num frames, fps */ + g_object_set (timeline, "loop", TRUE, NULL); /* have it loop */ + + /* fire a callback for frame change */ + g_signal_connect (timeline, "new-frame", G_CALLBACK (frame_cb), oh); + + /* Set up some behaviours to handle scaling */ + alpha = clutter_alpha_new_with_func (timeline, clutter_sine_func, + NULL, NULL); + + scaler_1 = clutter_behaviour_scale_new (alpha, + 0.5, 0.5, + 1.0, 1.0); + + scaler_2 = clutter_behaviour_scale_new (alpha, + 1.0, 1.0, + 0.5, 0.5); + + tmp = clutter_texture_new_from_file ("redhand.png", &error); + if (tmp == NULL) + { + g_error ("image load failed: %s", error->message); + exit (1); + } + real_hand = clutter_group_new (); + clutter_container_add_actor (CLUTTER_CONTAINER (real_hand), tmp); + tmp = clutter_rectangle_new_with_color (&clr); + clutter_actor_set_size (tmp, 100, 100); + clutter_container_add_actor (CLUTTER_CONTAINER (real_hand), tmp); + + /* Now stick the group we want to clone into another group with a custom + * opacity to verify that the clones don't traverse this parent when + * calculating their opacity. */ + tmp = clutter_group_new (); + clutter_actor_set_opacity (tmp, 0x80); + clutter_container_add_actor (CLUTTER_CONTAINER (tmp), real_hand); + clutter_container_add_actor (CLUTTER_CONTAINER (stage), tmp); + + /* create a new group to hold multiple actors in a group */ + oh->group = clutter_group_new(); + + oh->hand = g_new (ClutterActor*, n_hands); + for (i = 0; i < n_hands; i++) + { + gint x, y, w, h; + + /* Create a texture from file, then clone in to same resources */ + oh->hand[i] = clutter_actor_clone_new (real_hand); + + /* Place around a circle */ + w = clutter_actor_get_width (oh->hand[0]); + h = clutter_actor_get_height (oh->hand[0]); + + x = CLUTTER_STAGE_WIDTH () / 2 + + RADIUS + * cos (i * M_PI / (n_hands / 2)) + - w / 2; + + y = CLUTTER_STAGE_HEIGHT () / 2 + + RADIUS + * sin (i * M_PI / (n_hands / 2)) + - h / 2; + + clutter_actor_set_position (oh->hand[i], x, y); + + clutter_actor_move_anchor_point_from_gravity (oh->hand[i], + CLUTTER_GRAVITY_CENTER); + + /* Add to our group group */ + clutter_container_add_actor (CLUTTER_CONTAINER (oh->group), oh->hand[i]); + +#if 1 /* FIXME: disabled as causes drift? - see comment above */ + if (i % 2) + clutter_behaviour_apply (scaler_1, oh->hand[i]); + else + clutter_behaviour_apply (scaler_2, oh->hand[i]); +#endif + } + + /* Add the group to the stage */ + clutter_container_add_actor (CLUTTER_CONTAINER (stage), + CLUTTER_ACTOR (oh->group)); + + /* Show everying ( and map window ) */ + clutter_actor_show (stage); + + + g_signal_connect (stage, "button-press-event", + G_CALLBACK (input_cb), + oh); + g_signal_connect (stage, "key-release-event", + G_CALLBACK (input_cb), + oh); + + /* and start it */ + clutter_timeline_start (timeline); + + clutter_main (); + + g_free (oh->hand); + g_free (oh); + + return 0; +}