[gnome-todo/gbsneto/animations: 10/10] Introduce an animation framework




commit 53698b4d0ed3a3daa0498bca6c1d8327856d5b32
Author: Georges Basile Stavracas Neto <georges stavracas gmail com>
Date:   Mon Aug 10 17:19:57 2020 -0300

    Introduce an animation framework
    
    This is pretty much a direct port of Clutter's animation framework,
    except it uses GdkFrameClock internally. The idea is to make all
    widgetry of GNOME To Do use it.
    
    It supports both implicit and explicit easing; has property- and
    keyframe-based transitions; and supports setting custom markers
    at timelines.
    
    Also add a new interactive test to showcase it.

 src/animation/gtd-animatable.c          |  215 +++
 src/animation/gtd-animatable.h          |   85 +
 src/animation/gtd-animation-enums.h     |  220 +++
 src/animation/gtd-animation-utils.c     |  169 ++
 src/animation/gtd-animation-utils.h     |   65 +
 src/animation/gtd-easing.c              |  573 +++++++
 src/animation/gtd-easing.h              |  157 ++
 src/animation/gtd-interval.c            | 1184 ++++++++++++++
 src/animation/gtd-interval.h            |  116 ++
 src/animation/gtd-keyframe-transition.c |  729 +++++++++
 src/animation/gtd-keyframe-transition.h |   85 +
 src/animation/gtd-property-transition.c |  369 +++++
 src/animation/gtd-property-transition.h |   55 +
 src/animation/gtd-timeline-private.h    |   31 +
 src/animation/gtd-timeline.c            | 2589 +++++++++++++++++++++++++++++++
 src/animation/gtd-timeline.h            |  200 +++
 src/animation/gtd-transition.c          |  689 ++++++++
 src/animation/gtd-transition.h          |   85 +
 src/gnome-todo.h                        |    4 +
 src/gtd-enum-types.c.template           |   39 +
 src/gtd-enum-types.h.template           |   24 +
 src/gtd-types.h                         |    3 +
 src/gui/gtd-widget.c                    | 1120 ++++++++++++-
 src/gui/gtd-widget.h                    |   34 +
 src/meson.build                         |   35 +
 tests/interactive/test-animation.c      |  272 ++++
 tests/meson.build                       |    1 +
 27 files changed, 9099 insertions(+), 49 deletions(-)
---
diff --git a/src/animation/gtd-animatable.c b/src/animation/gtd-animatable.c
new file mode 100644
index 0000000..f2e3396
--- /dev/null
+++ b/src/animation/gtd-animatable.c
@@ -0,0 +1,215 @@
+/* gtd-animatable.c
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+
+/**
+ * SECTION:gtd-animatable
+ * @short_description: Interface for animatable classes
+ *
+ * #GtdAnimatable is an interface that allows a #GObject class
+ * to control how an widget will animate a property.
+ *
+ * Each #GtdAnimatable should implement the
+ * #GtdAnimatableInterface.interpolate_property() virtual function of the
+ * interface to compute the animation state between two values of an interval
+ * depending on a progress fwidget, expressed as a floating point value.
+ *
+ * #GtdAnimatable is available since Gtd 1.0
+ */
+
+#include "gtd-animatable.h"
+
+#include "gtd-debug.h"
+#include "gtd-interval.h"
+
+G_DEFINE_INTERFACE (GtdAnimatable, gtd_animatable, G_TYPE_OBJECT);
+
+static void
+gtd_animatable_default_init (GtdAnimatableInterface *iface)
+{
+}
+
+/**
+ * gtd_animatable_find_property:
+ * @animatable: a #GtdAnimatable
+ * @property_name: the name of the animatable property to find
+ *
+ * Finds the #GParamSpec for @property_name
+ *
+ * Return value: (transfer none): The #GParamSpec for the given property
+ *   or %NULL
+ *
+ * Since: 1.4
+ */
+GParamSpec *
+gtd_animatable_find_property (GtdAnimatable *animatable,
+                                  const gchar       *property_name)
+{
+  GtdAnimatableInterface *iface;
+
+  g_return_val_if_fail (GTD_IS_ANIMATABLE (animatable), NULL);
+  g_return_val_if_fail (property_name != NULL, NULL);
+
+  GTD_TRACE_MSG ("[animation] Looking for property '%s'", property_name);
+
+  iface = GTD_ANIMATABLE_GET_IFACE (animatable);
+  if (iface->find_property != NULL)
+    return iface->find_property (animatable, property_name);
+
+  return g_object_class_find_property (G_OBJECT_GET_CLASS (animatable),
+                                       property_name);
+}
+
+/**
+ * gtd_animatable_get_initial_state:
+ * @animatable: a #GtdAnimatable
+ * @property_name: the name of the animatable property to retrieve
+ * @value: a #GValue initialized to the type of the property to retrieve
+ *
+ * Retrieves the current state of @property_name and sets @value with it
+ *
+ * Since: 1.4
+ */
+void
+gtd_animatable_get_initial_state (GtdAnimatable *animatable,
+                                      const gchar       *property_name,
+                                      GValue            *value)
+{
+  GtdAnimatableInterface *iface;
+
+  g_return_if_fail (GTD_IS_ANIMATABLE (animatable));
+  g_return_if_fail (property_name != NULL);
+
+  GTD_TRACE_MSG ("[animation] Getting initial state of '%s'", property_name);
+
+  iface = GTD_ANIMATABLE_GET_IFACE (animatable);
+  if (iface->get_initial_state != NULL)
+    iface->get_initial_state (animatable, property_name, value);
+  else
+    g_object_get_property (G_OBJECT (animatable), property_name, value);
+}
+
+/**
+ * gtd_animatable_set_final_state:
+ * @animatable: a #GtdAnimatable
+ * @property_name: the name of the animatable property to set
+ * @value: the value of the animatable property to set
+ *
+ * Sets the current state of @property_name to @value
+ *
+ * Since: 1.4
+ */
+void
+gtd_animatable_set_final_state (GtdAnimatable *animatable,
+                                const gchar   *property_name,
+                                const GValue  *value)
+{
+  GtdAnimatableInterface *iface;
+
+  g_return_if_fail (GTD_IS_ANIMATABLE (animatable));
+  g_return_if_fail (property_name != NULL);
+
+  GTD_TRACE_MSG ("[animation] Setting state of property '%s'", property_name);
+
+  iface = GTD_ANIMATABLE_GET_IFACE (animatable);
+  if (iface->set_final_state != NULL)
+    iface->set_final_state (animatable, property_name, value);
+  else
+    g_object_set_property (G_OBJECT (animatable), property_name, value);
+}
+
+/**
+ * gtd_animatable_interpolate_value:
+ * @animatable: a #GtdAnimatable
+ * @property_name: the name of the property to interpolate
+ * @interval: a #GtdInterval with the animation range
+ * @progress: the progress to use to interpolate between the
+ *   initial and final values of the @interval
+ * @value: (out): return location for an initialized #GValue
+ *   using the same type of the @interval
+ *
+ * Asks a #GtdAnimatable implementation to interpolate a
+ * a named property between the initial and final values of
+ * a #GtdInterval, using @progress as the interpolation
+ * value, and store the result inside @value.
+ *
+ * This function should be used for every property animation
+ * involving #GtdAnimatable<!-- -->s.
+ *
+ * This function replaces gtd_animatable_animate_property().
+ *
+ * Return value: %TRUE if the interpolation was successful,
+ *   and %FALSE otherwise
+ *
+ * Since: 1.8
+ */
+gboolean
+gtd_animatable_interpolate_value (GtdAnimatable *animatable,
+                                  const gchar   *property_name,
+                                  GtdInterval   *interval,
+                                  gdouble        progress,
+                                  GValue        *value)
+{
+  GtdAnimatableInterface *iface;
+
+  g_return_val_if_fail (GTD_IS_ANIMATABLE (animatable), FALSE);
+  g_return_val_if_fail (property_name != NULL, FALSE);
+  g_return_val_if_fail (GTD_IS_INTERVAL (interval), FALSE);
+  g_return_val_if_fail (value != NULL, FALSE);
+
+  GTD_TRACE_MSG ("[animation] Interpolating '%s' (progress: %.3f)",
+                property_name,
+                progress);
+
+  iface = GTD_ANIMATABLE_GET_IFACE (animatable);
+  if (iface->interpolate_value != NULL)
+    {
+      return iface->interpolate_value (animatable, property_name,
+                                       interval,
+                                       progress,
+                                       value);
+    }
+  else
+    {
+      return gtd_interval_compute_value (interval, progress, value);
+    }
+}
+
+/**
+ * gtd_animatable_get_widget:
+ * @animatable: a #GtdAnimatable
+ *
+ * Get animated widget.
+ *
+ * Return value: (transfer none): a #GtdWidget
+ */
+GtdWidget *
+gtd_animatable_get_widget (GtdAnimatable *animatable)
+{
+  GtdAnimatableInterface *iface;
+
+  g_return_val_if_fail (GTD_IS_ANIMATABLE (animatable), NULL);
+
+  iface = GTD_ANIMATABLE_GET_IFACE (animatable);
+
+  g_return_val_if_fail (iface->get_widget, NULL);
+
+  return iface->get_widget (animatable);
+}
diff --git a/src/animation/gtd-animatable.h b/src/animation/gtd-animatable.h
new file mode 100644
index 0000000..3edb07d
--- /dev/null
+++ b/src/animation/gtd-animatable.h
@@ -0,0 +1,85 @@
+/* gtd-animatable.h
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "gtd-types.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_ANIMATABLE (gtd_animatable_get_type ())
+G_DECLARE_INTERFACE (GtdAnimatable, gtd_animatable, GTD, ANIMATABLE, GObject)
+
+/**
+ * GtdAnimatableInterface:
+ * @find_property: virtual function for retrieving the #GParamSpec of
+ *   an animatable property
+ * @get_initial_state: virtual function for retrieving the initial
+ *   state of an animatable property
+ * @set_final_state: virtual function for setting the state of an
+ *   animatable property
+ * @interpolate_value: virtual function for interpolating the progress
+ *   of a property
+ * @get_widget: virtual function for getting associated #GtdWidget
+ *
+ * Since: 1.0
+ */
+struct _GtdAnimatableInterface
+{
+  /*< private >*/
+  GTypeInterface parent_iface;
+
+  /*< public >*/
+  GParamSpec *(* find_property)     (GtdAnimatable *animatable,
+                                     const gchar       *property_name);
+  void        (* get_initial_state) (GtdAnimatable *animatable,
+                                     const gchar       *property_name,
+                                     GValue            *value);
+  void        (* set_final_state)   (GtdAnimatable *animatable,
+                                     const gchar       *property_name,
+                                     const GValue      *value);
+  gboolean    (* interpolate_value) (GtdAnimatable *animatable,
+                                     const gchar       *property_name,
+                                     GtdInterval   *interval,
+                                     gdouble            progress,
+                                     GValue            *value);
+  GtdWidget * (* get_widget)      (GtdAnimatable *animatable);
+};
+
+GParamSpec *gtd_animatable_find_property     (GtdAnimatable *animatable,
+                                              const gchar   *property_name);
+
+void        gtd_animatable_get_initial_state (GtdAnimatable *animatable,
+                                              const gchar   *property_name,
+                                              GValue        *value);
+
+void        gtd_animatable_set_final_state   (GtdAnimatable *animatable,
+                                              const gchar   *property_name,
+                                              const GValue  *value);
+
+gboolean    gtd_animatable_interpolate_value (GtdAnimatable *animatable,
+                                              const gchar   *property_name,
+                                              GtdInterval   *interval,
+                                              gdouble        progress,
+                                              GValue        *value);
+
+GtdWidget * gtd_animatable_get_widget      (GtdAnimatable *animatable);
+
+G_END_DECLS
diff --git a/src/animation/gtd-animation-enums.h b/src/animation/gtd-animation-enums.h
new file mode 100644
index 0000000..4c57998
--- /dev/null
+++ b/src/animation/gtd-animation-enums.h
@@ -0,0 +1,220 @@
+/* gtd-animation-enums.h
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+
+/**
+ * GtdEaseMode:
+ * @GTD_CUSTOM_MODE: custom progress function
+ * @GTD_EASE_LINEAR: linear tweening
+ * @GTD_EASE_IN_QUAD: quadratic tweening
+ * @GTD_EASE_OUT_QUAD: quadratic tweening, inverse of
+ *    %GTD_EASE_IN_QUAD
+ * @GTD_EASE_IN_OUT_QUAD: quadratic tweening, combininig
+ *    %GTD_EASE_IN_QUAD and %GTD_EASE_OUT_QUAD
+ * @GTD_EASE_IN_CUBIC: cubic tweening
+ * @GTD_EASE_OUT_CUBIC: cubic tweening, invers of
+ *    %GTD_EASE_IN_CUBIC
+ * @GTD_EASE_IN_OUT_CUBIC: cubic tweening, combining
+ *    %GTD_EASE_IN_CUBIC and %GTD_EASE_OUT_CUBIC
+ * @GTD_EASE_IN_QUART: quartic tweening
+ * @GTD_EASE_OUT_QUART: quartic tweening, inverse of
+ *    %GTD_EASE_IN_QUART
+ * @GTD_EASE_IN_OUT_QUART: quartic tweening, combining
+ *    %GTD_EASE_IN_QUART and %GTD_EASE_OUT_QUART
+ * @GTD_EASE_IN_QUINT: quintic tweening
+ * @GTD_EASE_OUT_QUINT: quintic tweening, inverse of
+ *    %GTD_EASE_IN_QUINT
+ * @GTD_EASE_IN_OUT_QUINT: fifth power tweening, combining
+ *    %GTD_EASE_IN_QUINT and %GTD_EASE_OUT_QUINT
+ * @GTD_EASE_IN_SINE: sinusoidal tweening
+ * @GTD_EASE_OUT_SINE: sinusoidal tweening, inverse of
+ *    %GTD_EASE_IN_SINE
+ * @GTD_EASE_IN_OUT_SINE: sine wave tweening, combining
+ *    %GTD_EASE_IN_SINE and %GTD_EASE_OUT_SINE
+ * @GTD_EASE_IN_EXPO: exponential tweening
+ * @GTD_EASE_OUT_EXPO: exponential tweening, inverse of
+ *    %GTD_EASE_IN_EXPO
+ * @GTD_EASE_IN_OUT_EXPO: exponential tweening, combining
+ *    %GTD_EASE_IN_EXPO and %GTD_EASE_OUT_EXPO
+ * @GTD_EASE_IN_CIRC: circular tweening
+ * @GTD_EASE_OUT_CIRC: circular tweening, inverse of
+ *    %GTD_EASE_IN_CIRC
+ * @GTD_EASE_IN_OUT_CIRC: circular tweening, combining
+ *    %GTD_EASE_IN_CIRC and %GTD_EASE_OUT_CIRC
+ * @GTD_EASE_IN_ELASTIC: elastic tweening, with offshoot on start
+ * @GTD_EASE_OUT_ELASTIC: elastic tweening, with offshoot on end
+ * @GTD_EASE_IN_OUT_ELASTIC: elastic tweening with offshoot on both ends
+ * @GTD_EASE_IN_BACK: overshooting cubic tweening, with
+ *   backtracking on start
+ * @GTD_EASE_OUT_BACK: overshooting cubic tweening, with
+ *   backtracking on end
+ * @GTD_EASE_IN_OUT_BACK: overshooting cubic tweening, with
+ *   backtracking on both ends
+ * @GTD_EASE_IN_BOUNCE: exponentially decaying parabolic (bounce)
+ *   tweening, with bounce on start
+ * @GTD_EASE_OUT_BOUNCE: exponentially decaying parabolic (bounce)
+ *   tweening, with bounce on end
+ * @GTD_EASE_IN_OUT_BOUNCE: exponentially decaying parabolic (bounce)
+ *   tweening, with bounce on both ends
+ * @GTD_STEPS: parametrized step function; see clutter_timeline_set_step_progress()
+ *   for further details. (Since 1.12)
+ * @GTD_STEP_START: equivalent to %GTD_STEPS with a number of steps
+ *   equal to 1, and a step mode of %GTD_STEP_MODE_START. (Since 1.12)
+ * @GTD_STEP_END: equivalent to %GTD_STEPS with a number of steps
+ *   equal to 1, and a step mode of %GTD_STEP_MODE_END. (Since 1.12)
+ * @GTD_CUBIC_BEZIER: cubic bezier between (0, 0) and (1, 1) with two
+ *   control points; see clutter_timeline_set_cubic_bezier_progress(). (Since 1.12)
+ * @GTD_EASE: equivalent to %GTD_CUBIC_BEZIER with control points
+ *   in (0.25, 0.1) and (0.25, 1.0). (Since 1.12)
+ * @GTD_EASE_IN: equivalent to %GTD_CUBIC_BEZIER with control points
+ *   in (0.42, 0) and (1.0, 1.0). (Since 1.12)
+ * @GTD_EASE_OUT: equivalent to %GTD_CUBIC_BEZIER with control points
+ *   in (0, 0) and (0.58, 1.0). (Since 1.12)
+ * @GTD_EASE_IN_OUT: equivalent to %GTD_CUBIC_BEZIER with control points
+ *   in (0.42, 0) and (0.58, 1.0). (Since 1.12)
+ * @GTD_ANIMATION_LAST: last animation mode, used as a guard for
+ *   registered global alpha functions
+ *
+ * The animation modes used by #ClutterAnimatable. This
+ * enumeration can be expanded in later versions of Clutter.
+ *
+ * <figure id="easing-modes">
+ *   <title>Easing modes provided by Clutter</title>
+ *   <graphic fileref="easing-modes.png" format="PNG"/>
+ * </figure>
+ *
+ * Every global alpha function registered using clutter_alpha_register_func()
+ * or clutter_alpha_register_closure() will have a logical id greater than
+ * %GTD_ANIMATION_LAST.
+ *
+ * Since: 1.0
+ */
+typedef enum
+{
+  GTD_CUSTOM_MODE = 0,
+
+  /* linear */
+  GTD_EASE_LINEAR,
+
+  /* quadratic */
+  GTD_EASE_IN_QUAD,
+  GTD_EASE_OUT_QUAD,
+  GTD_EASE_IN_OUT_QUAD,
+
+  /* cubic */
+  GTD_EASE_IN_CUBIC,
+  GTD_EASE_OUT_CUBIC,
+  GTD_EASE_IN_OUT_CUBIC,
+
+  /* quartic */
+  GTD_EASE_IN_QUART,
+  GTD_EASE_OUT_QUART,
+  GTD_EASE_IN_OUT_QUART,
+
+  /* quintic */
+  GTD_EASE_IN_QUINT,
+  GTD_EASE_OUT_QUINT,
+  GTD_EASE_IN_OUT_QUINT,
+
+  /* sinusoidal */
+  GTD_EASE_IN_SINE,
+  GTD_EASE_OUT_SINE,
+  GTD_EASE_IN_OUT_SINE,
+
+  /* exponential */
+  GTD_EASE_IN_EXPO,
+  GTD_EASE_OUT_EXPO,
+  GTD_EASE_IN_OUT_EXPO,
+
+  /* circular */
+  GTD_EASE_IN_CIRC,
+  GTD_EASE_OUT_CIRC,
+  GTD_EASE_IN_OUT_CIRC,
+
+  /* elastic */
+  GTD_EASE_IN_ELASTIC,
+  GTD_EASE_OUT_ELASTIC,
+  GTD_EASE_IN_OUT_ELASTIC,
+
+  /* overshooting cubic */
+  GTD_EASE_IN_BACK,
+  GTD_EASE_OUT_BACK,
+  GTD_EASE_IN_OUT_BACK,
+
+  /* exponentially decaying parabolic */
+  GTD_EASE_IN_BOUNCE,
+  GTD_EASE_OUT_BOUNCE,
+  GTD_EASE_IN_OUT_BOUNCE,
+
+  /* step functions (see css3-transitions) */
+  GTD_STEPS,
+  GTD_STEP_START, /* steps(1, start) */
+  GTD_STEP_END, /* steps(1, end) */
+
+  /* cubic bezier (see css3-transitions) */
+  GTD_EASE_CUBIC_BEZIER,
+  GTD_EASE,
+  GTD_EASE_IN,
+  GTD_EASE_OUT,
+  GTD_EASE_IN_OUT,
+
+  /* guard, before registered alpha functions */
+  GTD_EASE_LAST
+} GtdEaseMode;
+
+/**
+ * GtdTimelineDirection:
+ * @GTD_TIMELINE_FORWARD: forward direction for a timeline
+ * @GTD_TIMELINE_BACKWARD: backward direction for a timeline
+ *
+ * The direction of a #GtdTimeline
+ */
+typedef enum
+{
+  GTD_TIMELINE_FORWARD,
+  GTD_TIMELINE_BACKWARD
+} GtdTimelineDirection;
+
+/**
+ * GtdStepMode:
+ * @GTD_STEP_MODE_START: The change in the value of a
+ *   %GTD_STEP progress mode should occur at the start of
+ *   the transition
+ * @GTD_STEP_MODE_END: The change in the value of a
+ *   %GTD_STEP progress mode should occur at the end of
+ *   the transition
+ *
+ * Change the value transition of a step function.
+ *
+ * See gtd_timeline_set_step_progress().
+ */
+typedef enum
+{
+  GTD_STEP_MODE_START,
+  GTD_STEP_MODE_END
+} GtdStepMode;
+
+G_END_DECLS
diff --git a/src/animation/gtd-animation-utils.c b/src/animation/gtd-animation-utils.c
new file mode 100644
index 0000000..0abc2ca
--- /dev/null
+++ b/src/animation/gtd-animation-utils.c
@@ -0,0 +1,169 @@
+/* gtd-animation-utils.c
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "gtd-animation-utils.h"
+
+
+typedef struct
+{
+  GType value_type;
+  GtdProgressFunc func;
+} ProgressData;
+
+G_LOCK_DEFINE_STATIC (progress_funcs);
+static GHashTable *progress_funcs = NULL;
+
+gboolean
+gtd_has_progress_function (GType gtype)
+{
+  const char *type_name = g_type_name (gtype);
+
+  if (progress_funcs == NULL)
+    return FALSE;
+
+  return g_hash_table_lookup (progress_funcs, type_name) != NULL;
+}
+
+gboolean
+gtd_run_progress_function (GType gtype,
+                                const GValue *initial,
+                                const GValue *final,
+                                gdouble progress,
+                                GValue *retval)
+{
+  ProgressData *pdata;
+  gboolean res;
+
+  G_LOCK (progress_funcs);
+
+  if (G_UNLIKELY (progress_funcs == NULL))
+    {
+      res = FALSE;
+      goto out;
+    }
+
+  pdata = g_hash_table_lookup (progress_funcs, g_type_name (gtype));
+  if (G_UNLIKELY (pdata == NULL))
+    {
+      res = FALSE;
+      goto out;
+    }
+
+  res = pdata->func (initial, final, progress, retval);
+
+out:
+  G_UNLOCK (progress_funcs);
+
+  return res;
+}
+
+static void
+progress_data_destroy (gpointer data_)
+{
+  g_slice_free (ProgressData, data_);
+}
+
+/**
+ * gtd_interval_register_progress_func: (skip)
+ * @value_type: a #GType
+ * @func: a #GtdProgressFunc, or %NULL to unset a previously
+ *   set progress function
+ *
+ * Sets the progress function for a given @value_type, like:
+ *
+ * |[
+ *   gtd_interval_register_progress_func (MY_TYPE_FOO,
+ *                                            my_foo_progress);
+ * ]|
+ *
+ * Whenever a #GtdInterval instance using the default
+ * #GtdInterval::compute_value implementation is set as an
+ * interval between two #GValue of type @value_type, it will call
+ * @func to establish the value depending on the given progress,
+ * for instance:
+ *
+ * |[
+ *   static gboolean
+ *   my_int_progress (const GValue *a,
+ *                    const GValue *b,
+ *                    gdouble       progress,
+ *                    GValue       *retval)
+ *   {
+ *     gint ia = g_value_get_int (a);
+ *     gint ib = g_value_get_int (b);
+ *     gint res = factor * (ib - ia) + ia;
+ *
+ *     g_value_set_int (retval, res);
+ *
+ *     return TRUE;
+ *   }
+ *
+ *   gtd_interval_register_progress_func (G_TYPE_INT, my_int_progress);
+ * ]|
+ *
+ * To unset a previously set progress function of a #GType, pass %NULL
+ * for @func.
+ *
+ * Since: 1.0
+ */
+void
+gtd_interval_register_progress_func (GType               value_type,
+                                         GtdProgressFunc func)
+{
+  ProgressData *progress_func;
+  const char *type_name;
+
+  g_return_if_fail (value_type != G_TYPE_INVALID);
+
+  type_name = g_type_name (value_type);
+
+  G_LOCK (progress_funcs);
+
+  if (G_UNLIKELY (progress_funcs == NULL))
+    progress_funcs = g_hash_table_new_full (NULL, NULL,
+                                            NULL,
+                                            progress_data_destroy);
+
+  progress_func =
+    g_hash_table_lookup (progress_funcs, type_name);
+
+  if (G_UNLIKELY (progress_func))
+    {
+      if (func == NULL)
+        {
+          g_hash_table_remove (progress_funcs, type_name);
+          g_slice_free (ProgressData, progress_func);
+        }
+      else
+        progress_func->func = func;
+    }
+  else
+    {
+      progress_func = g_slice_new (ProgressData);
+      progress_func->value_type = value_type;
+      progress_func->func = func;
+
+      g_hash_table_replace (progress_funcs,
+                            (gpointer) type_name,
+                            progress_func);
+    }
+
+  G_UNLOCK (progress_funcs);
+}
diff --git a/src/animation/gtd-animation-utils.h b/src/animation/gtd-animation-utils.h
new file mode 100644
index 0000000..b41bc12
--- /dev/null
+++ b/src/animation/gtd-animation-utils.h
@@ -0,0 +1,65 @@
+/* gtd-animation-utils.h
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+
+/**
+ * GtdProgressFunc:
+ * @a: the initial value of an interval
+ * @b: the final value of an interval
+ * @progress: the progress factor, between 0 and 1
+ * @retval: the value used to store the progress
+ *
+ * Prototype of the progress function used to compute the value
+ * between the two ends @a and @b of an interval depending on
+ * the value of @progress.
+ *
+ * The #GValue in @retval is already initialized with the same
+ * type as @a and @b.
+ *
+ * This function will be called by #GtdInterval if the
+ * type of the values of the interval was registered using
+ * gtd_interval_register_progress_func().
+ *
+ * Return value: %TRUE if the function successfully computed
+ *   the value and stored it inside @retval
+ */
+typedef gboolean (* GtdProgressFunc) (const GValue *a,
+                                      const GValue *b,
+                                      gdouble       progress,
+                                      GValue       *retval);
+
+void            gtd_interval_register_progress_func (GType           value_type,
+                                                     GtdProgressFunc func);
+
+
+gboolean        gtd_has_progress_function  (GType gtype);
+gboolean        gtd_run_progress_function  (GType         gtype,
+                                            const GValue *initial,
+                                            const GValue *final,
+                                            gdouble       progress,
+                                            GValue       *retval);
+
+G_END_DECLS
diff --git a/src/animation/gtd-easing.c b/src/animation/gtd-easing.c
new file mode 100644
index 0000000..ef9a1c7
--- /dev/null
+++ b/src/animation/gtd-easing.c
@@ -0,0 +1,573 @@
+/* gtd-easing.c
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "gtd-easing.h"
+
+#include <math.h>
+
+gdouble
+gtd_linear (gdouble t,
+            gdouble d)
+{
+  return t / d;
+}
+
+gdouble
+gtd_ease_in_quad (gdouble t,
+                  gdouble d)
+{
+  gdouble p = t / d;
+
+  return p * p;
+}
+
+gdouble
+gtd_ease_out_quad (gdouble t,
+                   gdouble d)
+{
+  gdouble p = t / d;
+
+  return -1.0 * p * (p - 2);
+}
+
+gdouble
+gtd_ease_in_out_quad (gdouble t,
+                      gdouble d)
+{
+  gdouble p = t / (d / 2);
+
+  if (p < 1)
+    return 0.5 * p * p;
+
+  p -= 1;
+
+  return -0.5 * (p * (p - 2) - 1);
+}
+
+gdouble
+gtd_ease_in_cubic (gdouble t,
+                   gdouble d)
+{
+  gdouble p = t / d;
+
+  return p * p * p;
+}
+
+gdouble
+gtd_ease_out_cubic (gdouble t,
+                    gdouble d)
+{
+  gdouble p = t / d - 1;
+
+  return p * p * p + 1;
+}
+
+gdouble
+gtd_ease_in_out_cubic (gdouble t,
+                       gdouble d)
+{
+  gdouble p = t / (d / 2);
+
+  if (p < 1)
+    return 0.5 * p * p * p;
+
+  p -= 2;
+
+  return 0.5 * (p * p * p + 2);
+}
+
+gdouble
+gtd_ease_in_quart (gdouble t,
+                   gdouble d)
+{
+  gdouble p = t / d;
+
+  return p * p * p * p;
+}
+
+gdouble
+gtd_ease_out_quart (gdouble t,
+                    gdouble d)
+{
+  gdouble p = t / d - 1;
+
+  return -1.0 * (p * p * p * p - 1);
+}
+
+gdouble
+gtd_ease_in_out_quart (gdouble t,
+                       gdouble d)
+{
+  gdouble p = t / (d / 2);
+
+  if (p < 1)
+    return 0.5 * p * p * p * p;
+
+  p -= 2;
+
+  return -0.5 * (p * p * p * p - 2);
+}
+
+gdouble
+gtd_ease_in_quint (gdouble t,
+                   gdouble d)
+ {
+  gdouble p = t / d;
+
+  return p * p * p * p * p;
+}
+
+gdouble
+gtd_ease_out_quint (gdouble t,
+                    gdouble d)
+{
+  gdouble p = t / d - 1;
+
+  return p * p * p * p * p + 1;
+}
+
+gdouble
+gtd_ease_in_out_quint (gdouble t,
+                       gdouble d)
+{
+  gdouble p = t / (d / 2);
+
+  if (p < 1)
+    return 0.5 * p * p * p * p * p;
+
+  p -= 2;
+
+  return 0.5 * (p * p * p * p * p + 2);
+}
+
+gdouble
+gtd_ease_in_sine (gdouble t,
+                  gdouble d)
+{
+  return -1.0 * cos (t / d * G_PI_2) + 1.0;
+}
+
+gdouble
+gtd_ease_out_sine (gdouble t,
+                   gdouble d)
+{
+  return sin (t / d * G_PI_2);
+}
+
+gdouble
+gtd_ease_in_out_sine (gdouble t,
+                      gdouble d)
+{
+  return -0.5 * (cos (G_PI * t / d) - 1);
+}
+
+gdouble
+gtd_ease_in_expo (gdouble t,
+                  gdouble d)
+{
+  return (t == 0) ? 0.0 : pow (2, 10 * (t / d - 1));
+}
+
+gdouble
+gtd_ease_out_expo (gdouble t,
+                   gdouble d)
+{
+  return (t == d) ? 1.0 : -pow (2, -10 * t / d) + 1;
+}
+
+gdouble
+gtd_ease_in_out_expo (gdouble t,
+                      gdouble d)
+{
+  gdouble p;
+
+  if (t == 0)
+    return 0.0;
+
+  if (t == d)
+    return 1.0;
+
+  p = t / (d / 2);
+
+  if (p < 1)
+    return 0.5 * pow (2, 10 * (p - 1));
+
+  p -= 1;
+
+  return 0.5 * (-pow (2, -10 * p) + 2);
+}
+
+gdouble
+gtd_ease_in_circ (gdouble t,
+                  gdouble d)
+{
+  gdouble p = t / d;
+
+  return -1.0 * (sqrt (1 - p * p) - 1);
+}
+
+gdouble
+gtd_ease_out_circ (gdouble t,
+                   gdouble d)
+{
+  gdouble p = t / d - 1;
+
+  return sqrt (1 - p * p);
+}
+
+gdouble
+gtd_ease_in_out_circ (gdouble t,
+                      gdouble d)
+{
+  gdouble p = t / (d / 2);
+
+  if (p < 1)
+    return -0.5 * (sqrt (1 - p * p) - 1);
+
+  p -= 2;
+
+  return 0.5 * (sqrt (1 - p * p) + 1);
+}
+
+gdouble
+gtd_ease_in_elastic (gdouble t,
+                     gdouble d)
+{
+  gdouble p = d * .3;
+  gdouble s = p / 4;
+  gdouble q = t / d;
+
+  if (q == 1)
+    return 1.0;
+
+  q -= 1;
+
+  return -(pow (2, 10 * q) * sin ((q * d - s) * (2 * G_PI) / p));
+}
+
+gdouble
+gtd_ease_out_elastic (gdouble t,
+                      gdouble d)
+{
+  gdouble p = d * .3;
+  gdouble s = p / 4;
+  gdouble q = t / d;
+
+  if (q == 1)
+    return 1.0;
+
+  return pow (2, -10 * q) * sin ((q * d - s) * (2 * G_PI) / p) + 1.0;
+}
+
+gdouble
+gtd_ease_in_out_elastic (gdouble t,
+                         gdouble d)
+{
+  gdouble p = d * (.3 * 1.5);
+  gdouble s = p / 4;
+  gdouble q = t / (d / 2);
+
+  if (q == 2)
+    return 1.0;
+
+  if (q < 1)
+    {
+      q -= 1;
+
+      return -.5 * (pow (2, 10 * q) * sin ((q * d - s) * (2 * G_PI) / p));
+    }
+  else
+    {
+      q -= 1;
+
+      return pow (2, -10 * q)
+           * sin ((q * d - s) * (2 * G_PI) / p)
+           * .5 + 1.0;
+    }
+}
+
+gdouble
+gtd_ease_in_back (gdouble t,
+                  gdouble d)
+{
+  gdouble p = t / d;
+
+  return p * p * ((1.70158 + 1) * p - 1.70158);
+}
+
+gdouble
+gtd_ease_out_back (gdouble t,
+                   gdouble d)
+{
+  gdouble p = t / d - 1;
+
+  return p * p * ((1.70158 + 1) * p + 1.70158) + 1;
+}
+
+gdouble
+gtd_ease_in_out_back (gdouble t,
+                      gdouble d)
+{
+  gdouble p = t / (d / 2);
+  gdouble s = 1.70158 * 1.525;
+
+  if (p < 1)
+    return 0.5 * (p * p * ((s + 1) * p - s));
+
+  p -= 2;
+
+  return 0.5 * (p * p * ((s + 1) * p + s) + 2);
+}
+
+static inline gdouble
+ease_out_bounce_internal (gdouble t,
+                          gdouble d)
+{
+  gdouble p = t / d;
+
+  if (p < (1 / 2.75))
+    {
+      return 7.5625 * p * p;
+    }
+  else if (p < (2 / 2.75))
+    {
+      p -= (1.5 / 2.75);
+
+      return 7.5625 * p * p + .75;
+    }
+  else if (p < (2.5 / 2.75))
+    {
+      p -= (2.25 / 2.75);
+
+      return 7.5625 * p * p + .9375;
+    }
+  else
+    {
+      p -= (2.625 / 2.75);
+
+      return 7.5625 * p * p + .984375;
+    }
+}
+
+static inline gdouble
+ease_in_bounce_internal (gdouble t,
+                         gdouble d)
+{
+  return 1.0 - ease_out_bounce_internal (d - t, d);
+}
+
+gdouble
+gtd_ease_in_bounce (gdouble t,
+                    gdouble d)
+{
+  return ease_in_bounce_internal (t, d);
+}
+
+gdouble
+gtd_ease_out_bounce (gdouble t,
+                     gdouble d)
+{
+  return ease_out_bounce_internal (t, d);
+}
+
+gdouble
+gtd_ease_in_out_bounce (gdouble t,
+                        gdouble d)
+{
+  if (t < d / 2)
+    return ease_in_bounce_internal (t * 2, d) * 0.5;
+  else
+    return ease_out_bounce_internal (t * 2 - d, d) * 0.5 + 1.0 * 0.5;
+}
+
+static inline gdouble
+ease_steps_end (gdouble p,
+                int    n_steps)
+{
+  return floor (p * (gdouble) n_steps) / (gdouble) n_steps;
+}
+
+gdouble
+gtd_ease_steps_start (gdouble t,
+                      gdouble d,
+                      int     n_steps)
+{
+  return 1.0 - ease_steps_end (1.0 - (t / d), n_steps);
+}
+
+gdouble
+gtd_ease_steps_end (gdouble t,
+                    gdouble d,
+                    int     n_steps)
+{
+  return ease_steps_end ((t / d), n_steps);
+}
+
+static inline gdouble
+x_for_t (gdouble t,
+         gdouble x_1,
+         gdouble x_2)
+{
+  gdouble omt = 1.0 - t;
+
+  return 3.0 * omt * omt * t * x_1
+       + 3.0 * omt * t * t * x_2
+       + t * t * t;
+}
+
+static inline gdouble
+y_for_t (gdouble t,
+         gdouble y_1,
+         gdouble y_2)
+{
+  gdouble omt = 1.0 - t;
+
+  return 3.0 * omt * omt * t * y_1
+       + 3.0 * omt * t * t * y_2
+       + t * t * t;
+}
+
+static inline gdouble
+t_for_x (gdouble x,
+         gdouble x_1,
+         gdouble x_2)
+{
+  gdouble min_t = 0, max_t = 1;
+  int i;
+
+  for (i = 0; i < 30; ++i)
+    {
+      gdouble guess_t = (min_t + max_t) / 2.0;
+      gdouble guess_x = x_for_t (guess_t, x_1, x_2);
+
+      if (x < guess_x)
+        max_t = guess_t;
+      else
+        min_t = guess_t;
+    }
+
+  return (min_t + max_t) / 2.0;
+}
+
+gdouble
+gtd_ease_cubic_bezier (gdouble t,
+                       gdouble d,
+                       gdouble x_1,
+                       gdouble y_1,
+                       gdouble x_2,
+                       gdouble y_2)
+{
+  gdouble p = t / d;
+
+  if (p == 0.0)
+    return 0.0;
+
+  if (p == 1.0)
+    return 1.0;
+
+  return y_for_t (t_for_x (p, x_1, x_2), y_1, y_2);
+}
+
+/*< private >
+ * _gtd_animation_modes:
+ *
+ * A mapping of animation modes and easing functions.
+ */
+static const struct {
+  GtdEaseMode mode;
+  GtdEaseFunc func;
+  const char *name;
+} _gtd_animation_modes[] = {
+  { GTD_CUSTOM_MODE,         NULL, "custom" },
+
+  { GTD_EASE_LINEAR,         gtd_linear, "linear" },
+  { GTD_EASE_IN_QUAD,        gtd_ease_in_quad, "easeInQuad" },
+  { GTD_EASE_OUT_QUAD,       gtd_ease_out_quad, "easeOutQuad" },
+  { GTD_EASE_IN_OUT_QUAD,    gtd_ease_in_out_quad, "easeInOutQuad" },
+  { GTD_EASE_IN_CUBIC,       gtd_ease_in_cubic, "easeInCubic" },
+  { GTD_EASE_OUT_CUBIC,      gtd_ease_out_cubic, "easeOutCubic" },
+  { GTD_EASE_IN_OUT_CUBIC,   gtd_ease_in_out_cubic, "easeInOutCubic" },
+  { GTD_EASE_IN_QUART,       gtd_ease_in_quart, "easeInQuart" },
+  { GTD_EASE_OUT_QUART,      gtd_ease_out_quart, "easeOutQuart" },
+  { GTD_EASE_IN_OUT_QUART,   gtd_ease_in_out_quart, "easeInOutQuart" },
+  { GTD_EASE_IN_QUINT,       gtd_ease_in_quint, "easeInQuint" },
+  { GTD_EASE_OUT_QUINT,      gtd_ease_out_quint, "easeOutQuint" },
+  { GTD_EASE_IN_OUT_QUINT,   gtd_ease_in_out_quint, "easeInOutQuint" },
+  { GTD_EASE_IN_SINE,        gtd_ease_in_sine, "easeInSine" },
+  { GTD_EASE_OUT_SINE,       gtd_ease_out_sine, "easeOutSine" },
+  { GTD_EASE_IN_OUT_SINE,    gtd_ease_in_out_sine, "easeInOutSine" },
+  { GTD_EASE_IN_EXPO,        gtd_ease_in_expo, "easeInExpo" },
+  { GTD_EASE_OUT_EXPO,       gtd_ease_out_expo, "easeOutExpo" },
+  { GTD_EASE_IN_OUT_EXPO,    gtd_ease_in_out_expo, "easeInOutExpo" },
+  { GTD_EASE_IN_CIRC,        gtd_ease_in_circ, "easeInCirc" },
+  { GTD_EASE_OUT_CIRC,       gtd_ease_out_circ, "easeOutCirc" },
+  { GTD_EASE_IN_OUT_CIRC,    gtd_ease_in_out_circ, "easeInOutCirc" },
+  { GTD_EASE_IN_ELASTIC,     gtd_ease_in_elastic, "easeInElastic" },
+  { GTD_EASE_OUT_ELASTIC,    gtd_ease_out_elastic, "easeOutElastic" },
+  { GTD_EASE_IN_OUT_ELASTIC, gtd_ease_in_out_elastic, "easeInOutElastic" },
+  { GTD_EASE_IN_BACK,        gtd_ease_in_back, "easeInBack" },
+  { GTD_EASE_OUT_BACK,       gtd_ease_out_back, "easeOutBack" },
+  { GTD_EASE_IN_OUT_BACK,    gtd_ease_in_out_back, "easeInOutBack" },
+  { GTD_EASE_IN_BOUNCE,      gtd_ease_in_bounce, "easeInBounce" },
+  { GTD_EASE_OUT_BOUNCE,     gtd_ease_out_bounce, "easeOutBounce" },
+  { GTD_EASE_IN_OUT_BOUNCE,  gtd_ease_in_out_bounce, "easeInOutBounce" },
+
+  /* the parametrized functions need a cast */
+  { GTD_STEPS,               (GtdEaseFunc) gtd_ease_steps_end, "steps" },
+  { GTD_STEP_START,          (GtdEaseFunc) gtd_ease_steps_start, "stepStart" },
+  { GTD_STEP_END,            (GtdEaseFunc) gtd_ease_steps_end, "stepEnd" },
+
+  { GTD_EASE_CUBIC_BEZIER,   (GtdEaseFunc) gtd_ease_cubic_bezier, "cubicBezier" },
+  { GTD_EASE,                (GtdEaseFunc) gtd_ease_cubic_bezier, "ease" },
+  { GTD_EASE_IN,             (GtdEaseFunc) gtd_ease_cubic_bezier, "easeIn" },
+  { GTD_EASE_OUT,            (GtdEaseFunc) gtd_ease_cubic_bezier, "easeOut" },
+  { GTD_EASE_IN_OUT,         (GtdEaseFunc) gtd_ease_cubic_bezier, "easeInOut" },
+
+  { GTD_EASE_LAST,           NULL, "sentinel" },
+};
+
+GtdEaseFunc
+gtd_get_easing_func_for_mode (GtdEaseMode mode)
+{
+  g_assert (_gtd_animation_modes[mode].mode == mode);
+  g_assert (_gtd_animation_modes[mode].func != NULL);
+
+  return _gtd_animation_modes[mode].func;
+}
+
+const char *
+gtd_get_easing_name_for_mode (GtdEaseMode mode)
+{
+  g_assert (_gtd_animation_modes[mode].mode == mode);
+  g_assert (_gtd_animation_modes[mode].func != NULL);
+
+  return _gtd_animation_modes[mode].name;
+}
+
+gdouble
+gtd_easing_for_mode (GtdEaseMode mode,
+                     gdouble     t,
+                     gdouble     d)
+{
+  g_assert (_gtd_animation_modes[mode].mode == mode);
+  g_assert (_gtd_animation_modes[mode].func != NULL);
+
+  return _gtd_animation_modes[mode].func (t, d);
+}
diff --git a/src/animation/gtd-easing.h b/src/animation/gtd-easing.h
new file mode 100644
index 0000000..71800bd
--- /dev/null
+++ b/src/animation/gtd-easing.h
@@ -0,0 +1,157 @@
+/* gtd-easing.h
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <glib.h>
+
+#include "gtd-animation-enums.h"
+
+G_BEGIN_DECLS
+
+/**
+ * GtdEaseFunc:
+ * @t: elapsed time
+ * @d: total duration
+ *
+ * Internal type for the easing functions used by Gtd.
+ *
+ * Return value: the interpolated value, between -1.0 and 2.0
+ */
+typedef gdouble (* GtdEaseFunc) (gdouble t, gdouble d);
+
+GtdEaseFunc         gtd_get_easing_func_for_mode                 (GtdEaseMode        mode);
+
+const gchar*        gtd_get_easing_name_for_mode                 (GtdEaseMode        mode);
+
+gdouble             gtd_easing_for_mode                          (GtdEaseMode        mode,
+                                                                  gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_linear                             (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_quad                            (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_out_quad                           (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_out_quad                        (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_cubic                           (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_out_cubic                          (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_out_cubic                       (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_quart                           (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_out_quart                          (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_out_quart                       (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_quint                           (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_out_quint                          (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_out_quint                       (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_sine                            (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_out_sine                           (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_out_sine                        (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_expo                            (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_out_expo                           (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_out_expo                        (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_circ                            (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_out_circ                           (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_out_circ                        (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_elastic                         (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_out_elastic                        (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_out_elastic                     (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_back                            (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_out_back                           (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_out_back                        (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_bounce                          (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_out_bounce                         (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_in_out_bounce                      (gdouble            t,
+                                                                  gdouble            d);
+
+gdouble              gtd_ease_steps_start                        (gdouble            t,
+                                                                  gdouble            d,
+                                                                  int                steps);
+
+gdouble              gtd_ease_steps_end                          (gdouble            t,
+                                                                  gdouble            d,
+                                                                  int                steps);
+
+gdouble              gtd_ease_cubic_bezier                       (gdouble            t,
+                                                                  gdouble            d,
+                                                                  gdouble            x_1,
+                                                                  gdouble            y_1,
+                                                                  gdouble            x_2,
+                                                                  gdouble            y_2);
+
+
+G_END_DECLS
diff --git a/src/animation/gtd-interval.c b/src/animation/gtd-interval.c
new file mode 100644
index 0000000..2eb5583
--- /dev/null
+++ b/src/animation/gtd-interval.c
@@ -0,0 +1,1184 @@
+/* gtd-interval.c
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+
+/**
+ * SECTION:clutter-interval
+ * @short_description: An object holding an interval of two values
+ *
+ * #GtdInterval is a simple object that can hold two values
+ * defining an interval. #GtdInterval can hold any value that
+ * can be enclosed inside a #GValue.
+ *
+ * Once a #GtdInterval for a specific #GType has been instantiated
+ * the #GtdInterval:value-type property cannot be changed anymore.
+ *
+ * #GtdInterval starts with a floating reference; this means that
+ * any object taking a reference on a #GtdInterval instance should
+ * also take ownership of the interval by using g_object_ref_sink().
+ *
+ * #GtdInterval can be subclassed to override the validation
+ * and value computation.
+ *
+ * #GtdInterval is available since Gtd 1.0
+ */
+
+#include "gtd-interval.h"
+
+#include "gtd-animation-utils.h"
+#include "gtd-easing.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <glib.h>
+#include <glib-object.h>
+#include <gobject/gvaluecollector.h>
+
+enum
+{
+  PROP_0,
+  PROP_VALUE_TYPE,
+  PROP_INITIAL,
+  PROP_FINAL,
+  PROP_LAST,
+};
+
+static GParamSpec *obj_props[PROP_LAST];
+
+enum
+{
+  INITIAL,
+  FINAL,
+  RESULT,
+  N_VALUES,
+};
+
+typedef struct
+{
+  GType value_type;
+
+  GValue *values;
+} GtdIntervalPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GtdInterval, gtd_interval, G_TYPE_INITIALLY_UNOWNED);
+
+
+static gboolean
+gtd_interval_real_validate (GtdInterval *self,
+                            GParamSpec  *pspec)
+{
+  GType pspec_gtype = G_PARAM_SPEC_VALUE_TYPE (pspec);
+
+  /* then check the fundamental types */
+  switch (G_TYPE_FUNDAMENTAL (pspec_gtype))
+    {
+    case G_TYPE_INT:
+      {
+        GParamSpecInt *pspec_int = G_PARAM_SPEC_INT (pspec);
+        gint a, b;
+
+        a = b = 0;
+        gtd_interval_get_interval (self, &a, &b);
+        if ((a >= pspec_int->minimum && a <= pspec_int->maximum) &&
+            (b >= pspec_int->minimum && b <= pspec_int->maximum))
+          return TRUE;
+        else
+          return FALSE;
+      }
+      break;
+
+    case G_TYPE_INT64:
+      {
+        GParamSpecInt64 *pspec_int = G_PARAM_SPEC_INT64 (pspec);
+        gint64 a, b;
+
+        a = b = 0;
+        gtd_interval_get_interval (self, &a, &b);
+        if ((a >= pspec_int->minimum && a <= pspec_int->maximum) &&
+            (b >= pspec_int->minimum && b <= pspec_int->maximum))
+          return TRUE;
+        else
+          return FALSE;
+      }
+      break;
+
+    case G_TYPE_UINT:
+      {
+        GParamSpecUInt *pspec_uint = G_PARAM_SPEC_UINT (pspec);
+        guint a, b;
+
+        a = b = 0;
+        gtd_interval_get_interval (self, &a, &b);
+        if ((a >= pspec_uint->minimum && a <= pspec_uint->maximum) &&
+            (b >= pspec_uint->minimum && b <= pspec_uint->maximum))
+          return TRUE;
+        else
+          return FALSE;
+      }
+      break;
+
+    case G_TYPE_UINT64:
+      {
+        GParamSpecUInt64 *pspec_int = G_PARAM_SPEC_UINT64 (pspec);
+        guint64 a, b;
+
+        a = b = 0;
+        gtd_interval_get_interval (self, &a, &b);
+        if ((a >= pspec_int->minimum && a <= pspec_int->maximum) &&
+            (b >= pspec_int->minimum && b <= pspec_int->maximum))
+          return TRUE;
+        else
+          return FALSE;
+      }
+      break;
+
+    case G_TYPE_CHAR:
+      {
+        GParamSpecChar *pspec_char = G_PARAM_SPEC_CHAR (pspec);
+        guchar a, b;
+
+        a = b = 0;
+        gtd_interval_get_interval (self, &a, &b);
+        if ((a >= pspec_char->minimum && a <= pspec_char->maximum) &&
+            (b >= pspec_char->minimum && b <= pspec_char->maximum))
+          return TRUE;
+        else
+          return FALSE;
+      }
+      break;
+
+    case G_TYPE_UCHAR:
+      {
+        GParamSpecUChar *pspec_uchar = G_PARAM_SPEC_UCHAR (pspec);
+        guchar a, b;
+
+        a = b = 0;
+        gtd_interval_get_interval (self, &a, &b);
+        if ((a >= pspec_uchar->minimum && a <= pspec_uchar->maximum) &&
+            (b >= pspec_uchar->minimum && b <= pspec_uchar->maximum))
+          return TRUE;
+        else
+          return FALSE;
+      }
+      break;
+
+    case G_TYPE_FLOAT:
+      {
+        GParamSpecFloat *pspec_flt = G_PARAM_SPEC_FLOAT (pspec);
+        float a, b;
+
+        a = b = 0.f;
+        gtd_interval_get_interval (self, &a, &b);
+        if ((a >= pspec_flt->minimum && a <= pspec_flt->maximum) &&
+            (b >= pspec_flt->minimum && b <= pspec_flt->maximum))
+          return TRUE;
+        else
+          return FALSE;
+      }
+      break;
+
+    case G_TYPE_DOUBLE:
+      {
+        GParamSpecDouble *pspec_flt = G_PARAM_SPEC_DOUBLE (pspec);
+        double a, b;
+
+        a = b = 0;
+        gtd_interval_get_interval (self, &a, &b);
+        if ((a >= pspec_flt->minimum && a <= pspec_flt->maximum) &&
+            (b >= pspec_flt->minimum && b <= pspec_flt->maximum))
+          return TRUE;
+        else
+          return FALSE;
+      }
+      break;
+
+    case G_TYPE_BOOLEAN:
+      return TRUE;
+
+    default:
+      break;
+    }
+
+  return TRUE;
+}
+
+static gboolean
+gtd_interval_real_compute_value (GtdInterval *self,
+                                 gdouble      factor,
+                                 GValue      *value)
+{
+  GValue *initial, *final;
+  GType value_type;
+  gboolean retval = FALSE;
+
+  initial = gtd_interval_peek_initial_value (self);
+  final = gtd_interval_peek_final_value (self);
+
+  value_type = gtd_interval_get_value_type (self);
+
+  if (gtd_has_progress_function (value_type))
+    {
+      retval = gtd_run_progress_function (value_type,
+                                          initial,
+                                          final,
+                                          factor,
+                                          value);
+      if (retval)
+        return TRUE;
+    }
+
+  switch (G_TYPE_FUNDAMENTAL (value_type))
+    {
+    case G_TYPE_INT:
+      {
+        gint ia, ib, res;
+
+        ia = g_value_get_int (initial);
+        ib = g_value_get_int (final);
+
+        res = (factor * (ib - ia)) + ia;
+
+        g_value_set_int (value, res);
+
+        retval = TRUE;
+      }
+      break;
+
+    case G_TYPE_CHAR:
+      {
+        gchar ia, ib, res;
+
+        ia = g_value_get_schar (initial);
+        ib = g_value_get_schar (final);
+
+        res = (factor * (ib - (gdouble) ia)) + ia;
+
+        g_value_set_schar (value, res);
+
+        retval = TRUE;
+      }
+      break;
+
+    case G_TYPE_UINT:
+      {
+        guint ia, ib, res;
+
+        ia = g_value_get_uint (initial);
+        ib = g_value_get_uint (final);
+
+        res = (factor * (ib - (gdouble) ia)) + ia;
+
+        g_value_set_uint (value, res);
+
+        retval = TRUE;
+      }
+      break;
+
+    case G_TYPE_UCHAR:
+      {
+        guchar ia, ib, res;
+
+        ia = g_value_get_uchar (initial);
+        ib = g_value_get_uchar (final);
+
+        res = (factor * (ib - (gdouble) ia)) + ia;
+
+        g_value_set_uchar (value, res);
+
+        retval = TRUE;
+      }
+      break;
+
+    case G_TYPE_FLOAT:
+    case G_TYPE_DOUBLE:
+      {
+        gdouble ia, ib, res;
+
+        if (value_type == G_TYPE_DOUBLE)
+          {
+            ia = g_value_get_double (initial);
+            ib = g_value_get_double (final);
+          }
+        else
+          {
+            ia = g_value_get_float (initial);
+            ib = g_value_get_float (final);
+          }
+
+        res = (factor * (ib - ia)) + ia;
+
+        if (value_type == G_TYPE_DOUBLE)
+          g_value_set_double (value, res);
+        else
+          g_value_set_float (value, res);
+
+        retval = TRUE;
+      }
+      break;
+
+    case G_TYPE_BOOLEAN:
+      if (factor > 0.5)
+        g_value_set_boolean (value, TRUE);
+      else
+        g_value_set_boolean (value, FALSE);
+
+      retval = TRUE;
+      break;
+
+    case G_TYPE_BOXED:
+      break;
+
+    default:
+      break;
+    }
+
+  /* We're trying to animate a property without knowing how to do that. Issue
+   * a warning with a hint to what could be done to fix that */
+  if (G_UNLIKELY (retval == FALSE))
+    {
+      g_warning ("%s: Could not compute progress between two %s. You can "
+                 "register a progress function to instruct GtdInterval "
+                 "how to deal with this GType",
+                 G_STRLOC,
+                 g_type_name (value_type));
+    }
+
+  return retval;
+}
+
+static void
+gtd_interval_finalize (GObject *object)
+{
+  GtdInterval *self = GTD_INTERVAL (object);
+  GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+
+  if (G_IS_VALUE (&priv->values[INITIAL]))
+    g_value_unset (&priv->values[INITIAL]);
+
+  if (G_IS_VALUE (&priv->values[FINAL]))
+    g_value_unset (&priv->values[FINAL]);
+
+  if (G_IS_VALUE (&priv->values[RESULT]))
+    g_value_unset (&priv->values[RESULT]);
+
+  g_free (priv->values);
+
+  G_OBJECT_CLASS (gtd_interval_parent_class)->finalize (object);
+}
+
+static void
+gtd_interval_set_property (GObject      *gobject,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  GtdInterval *self = GTD_INTERVAL (gobject);
+  GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_VALUE_TYPE:
+      priv->value_type = g_value_get_gtype (value);
+      break;
+
+    case PROP_INITIAL:
+      if (g_value_get_boxed (value) != NULL)
+        gtd_interval_set_initial_value (self, g_value_get_boxed (value));
+      else if (G_IS_VALUE (&priv->values[INITIAL]))
+        g_value_unset (&priv->values[INITIAL]);
+      break;
+
+    case PROP_FINAL:
+      if (g_value_get_boxed (value) != NULL)
+        gtd_interval_set_final_value (self, g_value_get_boxed (value));
+      else if (G_IS_VALUE (&priv->values[FINAL]))
+        g_value_unset (&priv->values[FINAL]);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+gtd_interval_get_property (GObject    *gobject,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  GtdIntervalPrivate *priv;
+
+  priv = gtd_interval_get_instance_private (GTD_INTERVAL (gobject));
+
+  switch (prop_id)
+    {
+    case PROP_VALUE_TYPE:
+      g_value_set_gtype (value, priv->value_type);
+      break;
+
+    case PROP_INITIAL:
+      if (G_IS_VALUE (&priv->values[INITIAL]))
+        g_value_set_boxed (value, &priv->values[INITIAL]);
+      break;
+
+    case PROP_FINAL:
+      if (G_IS_VALUE (&priv->values[FINAL]))
+        g_value_set_boxed (value, &priv->values[FINAL]);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+gtd_interval_class_init (GtdIntervalClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  klass->validate = gtd_interval_real_validate;
+  klass->compute_value = gtd_interval_real_compute_value;
+
+  gobject_class->set_property = gtd_interval_set_property,
+  gobject_class->get_property = gtd_interval_get_property;
+  gobject_class->finalize = gtd_interval_finalize;
+
+  /**
+   * GtdInterval:value-type:
+   *
+   * The type of the values in the interval.
+   *
+   * Since: 1.0
+   */
+  obj_props[PROP_VALUE_TYPE] =
+    g_param_spec_gtype ("value-type",
+                        "Value Type",
+                        "The type of the values in the interval",
+                        G_TYPE_NONE,
+                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * GtdInterval:initial:
+   *
+   * The initial value of the interval.
+   *
+   * Since: 1.12
+   */
+  obj_props[PROP_INITIAL] =
+    g_param_spec_boxed ("initial",
+                        "Initial Value",
+                        "Initial value of the interval",
+                        G_TYPE_VALUE,
+                        G_PARAM_READWRITE |
+                        G_PARAM_STATIC_STRINGS);
+
+  /**
+   * GtdInterval:final:
+   *
+   * The final value of the interval.
+   *
+   * Since: 1.12
+   */
+  obj_props[PROP_FINAL] =
+    g_param_spec_boxed ("final",
+                        "Final Value",
+                        "Final value of the interval",
+                        G_TYPE_VALUE,
+                        G_PARAM_READWRITE |
+                        G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (gobject_class, PROP_LAST, obj_props);
+}
+
+static void
+gtd_interval_init (GtdInterval *self)
+{
+  GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+
+  priv->value_type = G_TYPE_INVALID;
+  priv->values = g_malloc0 (sizeof (GValue) * N_VALUES);
+}
+
+static inline void
+gtd_interval_set_value_internal (GtdInterval  *self,
+                                 gint          index_,
+                                 const GValue *value)
+{
+  GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+  GType value_type;
+
+  g_assert (index_ >= INITIAL && index_ <= RESULT);
+
+  if (G_IS_VALUE (&priv->values[index_]))
+    g_value_unset (&priv->values[index_]);
+
+  g_value_init (&priv->values[index_], priv->value_type);
+
+  value_type = G_VALUE_TYPE (value);
+  if (value_type != priv->value_type ||
+      !g_type_is_a (value_type, priv->value_type))
+    {
+      if (g_value_type_compatible (value_type, priv->value_type))
+        {
+          g_value_copy (value, &priv->values[index_]);
+          return;
+        }
+
+      if (g_value_type_transformable (value_type, priv->value_type))
+        {
+          GValue transform = G_VALUE_INIT;
+
+          g_value_init (&transform, priv->value_type);
+
+          if (g_value_transform (value, &transform))
+            g_value_copy (&transform, &priv->values[index_]);
+          else
+            {
+              g_warning ("%s: Unable to convert a value of type '%s' into "
+                         "the value type '%s' of the interval.",
+                         G_STRLOC,
+                         g_type_name (value_type),
+                         g_type_name (priv->value_type));
+            }
+
+          g_value_unset (&transform);
+        }
+    }
+  else
+    g_value_copy (value, &priv->values[index_]);
+}
+
+static inline void
+gtd_interval_get_value_internal (GtdInterval *self,
+                                 gint         index_,
+                                 GValue      *value)
+{
+  GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+
+  g_assert (index_ >= INITIAL && index_ <= RESULT);
+
+  g_value_copy (&priv->values[index_], value);
+}
+
+static gboolean
+gtd_interval_set_initial_internal (GtdInterval *self,
+                                   va_list     *args)
+{;
+  GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+  GType gtype = priv->value_type;
+  GValue value = G_VALUE_INIT;
+  gchar *error;
+
+  /* initial value */
+  G_VALUE_COLLECT_INIT (&value, gtype, *args, 0, &error);
+
+  if (error)
+    {
+      g_warning ("%s: %s", G_STRLOC, error);
+
+      /* we leak the value here as it might not be in a valid state
+       * given the error and calling g_value_unset() might lead to
+       * undefined behaviour
+       */
+      g_free (error);
+      return FALSE;
+    }
+
+  gtd_interval_set_value_internal (self, INITIAL, &value);
+  g_value_unset (&value);
+
+  return TRUE;
+}
+
+static gboolean
+gtd_interval_set_final_internal (GtdInterval *self,
+                                 va_list     *args)
+{
+  GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+  GType gtype = priv->value_type;
+  GValue value = G_VALUE_INIT;
+  gchar *error;
+
+  /* initial value */
+  G_VALUE_COLLECT_INIT (&value, gtype, *args, 0, &error);
+
+  if (error)
+    {
+      g_warning ("%s: %s", G_STRLOC, error);
+
+      /* we leak the value here as it might not be in a valid state
+       * given the error and calling g_value_unset() might lead to
+       * undefined behaviour
+       */
+      g_free (error);
+      return FALSE;
+    }
+
+  gtd_interval_set_value_internal (self, FINAL, &value);
+  g_value_unset (&value);
+
+  return TRUE;
+}
+
+static void
+gtd_interval_get_interval_valist (GtdInterval *self,
+                                  va_list      var_args)
+{
+  GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+  GType gtype = priv->value_type;
+  GValue value = G_VALUE_INIT;
+  gchar *error;
+
+  /* initial value */
+  g_value_init (&value, gtype);
+  gtd_interval_get_initial_value (self, &value);
+  G_VALUE_LCOPY (&value, var_args, 0, &error);
+  if (error)
+    {
+      g_warning ("%s: %s", G_STRLOC, error);
+      g_free (error);
+      g_value_unset (&value);
+      return;
+    }
+
+  g_value_unset (&value);
+
+  /* final value */
+  g_value_init (&value, gtype);
+  gtd_interval_get_final_value (self, &value);
+  G_VALUE_LCOPY (&value, var_args, 0, &error);
+  if (error)
+    {
+      g_warning ("%s: %s", G_STRLOC, error);
+      g_free (error);
+      g_value_unset (&value);
+      return;
+    }
+
+  g_value_unset (&value);
+}
+
+/**
+ * gtd_interval_new:
+ * @gtype: the type of the values in the interval
+ * @...: the initial value and the final value of the interval
+ *
+ * Creates a new #GtdInterval holding values of type @gtype.
+ *
+ * This function avoids using a #GValue for the initial and final values
+ * of the interval:
+ *
+ * |[
+ *   interval = gtd_interval_new (G_TYPE_FLOAT, 0.0, 1.0);
+ *   interval = gtd_interval_new (G_TYPE_BOOLEAN, FALSE, TRUE);
+ *   interval = gtd_interval_new (G_TYPE_INT, 0, 360);
+ * ]|
+ *
+ * Return value: the newly created #GtdInterval
+ *
+ * Since: 1.0
+ */
+GtdInterval *
+gtd_interval_new (GType gtype,
+                      ...)
+{
+  GtdInterval *retval;
+  va_list args;
+
+  g_return_val_if_fail (gtype != G_TYPE_INVALID, NULL);
+
+  retval = g_object_new (GTD_TYPE_INTERVAL, "value-type", gtype, NULL);
+
+  va_start (args, gtype);
+
+  if (!gtd_interval_set_initial_internal (retval, &args))
+    goto out;
+
+  gtd_interval_set_final_internal (retval, &args);
+
+out:
+  va_end (args);
+
+  return retval;
+}
+
+/**
+ * gtd_interval_new_with_values:
+ * @gtype: the type of the values in the interval
+ * @initial: (allow-none): a #GValue holding the initial value of the interval
+ * @final: (allow-none): a #GValue holding the final value of the interval
+ *
+ * Creates a new #GtdInterval of type @gtype, between @initial
+ * and @final.
+ *
+ * This function is useful for language bindings.
+ *
+ * Return value: the newly created #GtdInterval
+ *
+ * Since: 1.0
+ */
+GtdInterval *
+gtd_interval_new_with_values (GType         gtype,
+                                  const GValue *initial,
+                                  const GValue *final)
+{
+  g_return_val_if_fail (gtype != G_TYPE_INVALID, NULL);
+  g_return_val_if_fail (initial == NULL || G_VALUE_TYPE (initial) == gtype, NULL);
+  g_return_val_if_fail (final == NULL || G_VALUE_TYPE (final) == gtype, NULL);
+
+  return g_object_new (GTD_TYPE_INTERVAL,
+                       "value-type", gtype,
+                       "initial", initial,
+                       "final", final,
+                       NULL);
+}
+
+/**
+ * gtd_interval_clone:
+ * @interval: a #GtdInterval
+ *
+ * Creates a copy of @interval.
+ *
+ * Return value: (transfer full): the newly created #GtdInterval
+ *
+ * Since: 1.0
+ */
+GtdInterval *
+gtd_interval_clone (GtdInterval *self)
+{
+  GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+  GtdInterval *retval;
+  GType gtype;
+  GValue *tmp;
+
+  g_return_val_if_fail (GTD_IS_INTERVAL (self), NULL);
+  g_return_val_if_fail (priv->value_type != G_TYPE_INVALID, NULL);
+
+  gtype = priv->value_type;
+  retval = g_object_new (GTD_TYPE_INTERVAL, "value-type", gtype, NULL);
+
+  tmp = gtd_interval_peek_initial_value (self);
+  gtd_interval_set_initial_value (retval, tmp);
+
+  tmp = gtd_interval_peek_final_value (self);
+  gtd_interval_set_final_value (retval, tmp);
+
+  return retval;
+}
+
+/**
+ * gtd_interval_get_value_type:
+ * @interval: a #GtdInterval
+ *
+ * Retrieves the #GType of the values inside @interval.
+ *
+ * Return value: the type of the value, or G_TYPE_INVALID
+ *
+ * Since: 1.0
+ */
+GType
+gtd_interval_get_value_type (GtdInterval *self)
+{
+  GtdIntervalPrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_INTERVAL (self), G_TYPE_INVALID);
+
+  priv = gtd_interval_get_instance_private (self);
+  return priv->value_type;
+}
+
+/**
+ * gtd_interval_set_initial_value: (rename-to gtd_interval_set_initial)
+ * @interval: a #GtdInterval
+ * @value: a #GValue
+ *
+ * Sets the initial value of @interval to @value. The value is copied
+ * inside the #GtdInterval.
+ *
+ * Since: 1.0
+ */
+void
+gtd_interval_set_initial_value (GtdInterval  *self,
+                                const GValue *value)
+{
+  g_return_if_fail (GTD_IS_INTERVAL (self));
+  g_return_if_fail (value != NULL);
+
+  gtd_interval_set_value_internal (self, INITIAL, value);
+}
+
+/**
+ * gtd_interval_set_initial: (skip)
+ * @interval: a #GtdInterval
+ * @...: the initial value of the interval.
+ *
+ * Variadic arguments version of gtd_interval_set_initial_value().
+ *
+ * This function is meant as a convenience for the C API.
+ *
+ * Language bindings should use gtd_interval_set_initial_value()
+ * instead.
+ *
+ * Since: 1.10
+ */
+void
+gtd_interval_set_initial (GtdInterval *self,
+                              ...)
+{
+  va_list args;
+
+  g_return_if_fail (GTD_IS_INTERVAL (self));
+
+  va_start (args, self);
+  gtd_interval_set_initial_internal (self, &args);
+  va_end (args);
+}
+
+/**
+ * gtd_interval_get_initial_value:
+ * @interval: a #GtdInterval
+ * @value: (out caller-allocates): a #GValue
+ *
+ * Retrieves the initial value of @interval and copies
+ * it into @value.
+ *
+ * The passed #GValue must be initialized to the value held by
+ * the #GtdInterval.
+ *
+ * Since: 1.0
+ */
+void
+gtd_interval_get_initial_value (GtdInterval *self,
+                                    GValue          *value)
+{
+  g_return_if_fail (GTD_IS_INTERVAL (self));
+  g_return_if_fail (value != NULL);
+
+  gtd_interval_get_value_internal (self, INITIAL, value);
+}
+
+/**
+ * gtd_interval_peek_initial_value:
+ * @interval: a #GtdInterval
+ *
+ * Gets the pointer to the initial value of @interval
+ *
+ * Return value: (transfer none): the initial value of the interval.
+ *   The value is owned by the #GtdInterval and it should not be
+ *   modified or freed
+ *
+ * Since: 1.0
+ */
+GValue *
+gtd_interval_peek_initial_value (GtdInterval *self)
+{
+  GtdIntervalPrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_INTERVAL (self), NULL);
+
+  priv = gtd_interval_get_instance_private (self);
+  return priv->values + INITIAL;
+}
+
+/**
+ * gtd_interval_set_final_value: (rename-to gtd_interval_set_final)
+ * @interval: a #GtdInterval
+ * @value: a #GValue
+ *
+ * Sets the final value of @interval to @value. The value is
+ * copied inside the #GtdInterval.
+ *
+ * Since: 1.0
+ */
+void
+gtd_interval_set_final_value (GtdInterval *self,
+                                  const GValue    *value)
+{
+  g_return_if_fail (GTD_IS_INTERVAL (self));
+  g_return_if_fail (value != NULL);
+
+  gtd_interval_set_value_internal (self, FINAL, value);
+}
+
+/**
+ * gtd_interval_get_final_value:
+ * @interval: a #GtdInterval
+ * @value: (out caller-allocates): a #GValue
+ *
+ * Retrieves the final value of @interval and copies
+ * it into @value.
+ *
+ * The passed #GValue must be initialized to the value held by
+ * the #GtdInterval.
+ *
+ * Since: 1.0
+ */
+void
+gtd_interval_get_final_value (GtdInterval *self,
+                                  GValue          *value)
+{
+  g_return_if_fail (GTD_IS_INTERVAL (self));
+  g_return_if_fail (value != NULL);
+
+  gtd_interval_get_value_internal (self, FINAL, value);
+}
+
+/**
+ * gtd_interval_set_final: (skip)
+ * @interval: a #GtdInterval
+ * @...: the final value of the interval
+ *
+ * Variadic arguments version of gtd_interval_set_final_value().
+ *
+ * This function is meant as a convenience for the C API.
+ *
+ * Language bindings should use gtd_interval_set_final_value() instead.
+ *
+ * Since: 1.10
+ */
+void
+gtd_interval_set_final (GtdInterval *self,
+                            ...)
+{
+  va_list args;
+
+  g_return_if_fail (GTD_IS_INTERVAL (self));
+
+  va_start (args, self);
+  gtd_interval_set_final_internal (self, &args);
+  va_end (args);
+}
+
+/**
+ * gtd_interval_peek_final_value:
+ * @interval: a #GtdInterval
+ *
+ * Gets the pointer to the final value of @interval
+ *
+ * Return value: (transfer none): the final value of the interval.
+ *   The value is owned by the #GtdInterval and it should not be
+ *   modified or freed
+ *
+ * Since: 1.0
+ */
+GValue *
+gtd_interval_peek_final_value (GtdInterval *self)
+{
+  GtdIntervalPrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_INTERVAL (self), NULL);
+
+  priv = gtd_interval_get_instance_private (self);
+  return priv->values + FINAL;
+}
+
+/**
+ * gtd_interval_set_interval:
+ * @interval: a #GtdInterval
+ * @...: the initial and final values of the interval
+ *
+ * Variable arguments wrapper for gtd_interval_set_initial_value()
+ * and gtd_interval_set_final_value() that avoids using the
+ * #GValue arguments:
+ *
+ * |[
+ *   gtd_interval_set_interval (self, 0, 50);
+ *   gtd_interval_set_interval (self, 1.0, 0.0);
+ *   gtd_interval_set_interval (self, FALSE, TRUE);
+ * ]|
+ *
+ * This function is meant for the convenience of the C API; bindings
+ * should reimplement this function using the #GValue-based API.
+ *
+ * Since: 1.0
+ */
+void
+gtd_interval_set_interval (GtdInterval *self,
+                           ...)
+{
+  GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+  va_list args;
+
+  g_return_if_fail (GTD_IS_INTERVAL (self));
+  g_return_if_fail (priv->value_type != G_TYPE_INVALID);
+
+  va_start (args, self);
+
+  if (!gtd_interval_set_initial_internal (self, &args))
+    goto out;
+
+  gtd_interval_set_final_internal (self, &args);
+
+out:
+  va_end (args);
+}
+
+/**
+ * gtd_interval_get_interval:
+ * @interval: a #GtdInterval
+ * @...: return locations for the initial and final values of
+ *   the interval
+ *
+ * Variable arguments wrapper for gtd_interval_get_initial_value()
+ * and gtd_interval_get_final_value() that avoids using the
+ * #GValue arguments:
+ *
+ * |[
+ *   gint a = 0, b = 0;
+ *   gtd_interval_get_interval (self, &a, &b);
+ * ]|
+ *
+ * This function is meant for the convenience of the C API; bindings
+ * should reimplement this function using the #GValue-based API.
+ *
+ * Since: 1.0
+ */
+void
+gtd_interval_get_interval (GtdInterval *self,
+                               ...)
+{
+  GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+  va_list args;
+
+  g_return_if_fail (GTD_IS_INTERVAL (self));
+  g_return_if_fail (priv->value_type != G_TYPE_INVALID);
+
+  va_start (args, self);
+  gtd_interval_get_interval_valist (self, args);
+  va_end (args);
+}
+
+/**
+ * gtd_interval_validate:
+ * @interval: a #GtdInterval
+ * @pspec: a #GParamSpec
+ *
+ * Validates the initial and final values of @interval against
+ * a #GParamSpec.
+ *
+ * Return value: %TRUE if the #GtdInterval is valid, %FALSE otherwise
+ *
+ * Since: 1.0
+ */
+gboolean
+gtd_interval_validate (GtdInterval *self,
+                           GParamSpec      *pspec)
+{
+  g_return_val_if_fail (GTD_IS_INTERVAL (self), FALSE);
+  g_return_val_if_fail (G_IS_PARAM_SPEC (pspec), FALSE);
+
+  return GTD_INTERVAL_GET_CLASS (self)->validate (self, pspec);
+}
+
+/**
+ * gtd_interval_compute_value:
+ * @interval: a #GtdInterval
+ * @factor: the progress factor, between 0 and 1
+ * @value: (out caller-allocates): return location for an initialized #GValue
+ *
+ * Computes the value between the @interval boundaries given the
+ * progress @factor and copies it into @value.
+ *
+ * Return value: %TRUE if the operation was successful
+ *
+ * Since: 1.0
+ */
+gboolean
+gtd_interval_compute_value (GtdInterval *self,
+                            gdouble      factor,
+                            GValue      *value)
+{
+  g_return_val_if_fail (GTD_IS_INTERVAL (self), FALSE);
+  g_return_val_if_fail (value != NULL, FALSE);
+
+  return GTD_INTERVAL_GET_CLASS (self)->compute_value (self, factor, value);
+}
+
+/**
+ * gtd_interval_compute:
+ * @interval: a #GtdInterval
+ * @factor: the progress factor, between 0 and 1
+ *
+ * Computes the value between the @interval boundaries given the
+ * progress @factor
+ *
+ * Unlike gtd_interval_compute_value(), this function will
+ * return a const pointer to the computed value
+ *
+ * You should use this function if you immediately pass the computed
+ * value to another function that makes a copy of it, like
+ * g_object_set_property()
+ *
+ * Return value: (transfer none): a pointer to the computed value,
+ *   or %NULL if the computation was not successfull
+ *
+ * Since: 1.4
+ */
+const GValue *
+gtd_interval_compute (GtdInterval *self,
+                      gdouble      factor)
+{
+  GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+  GValue *value;
+  gboolean res;
+
+  g_return_val_if_fail (GTD_IS_INTERVAL (self), NULL);
+
+  value = &(priv->values[RESULT]);
+
+  if (G_VALUE_TYPE (value) == G_TYPE_INVALID)
+    g_value_init (value, priv->value_type);
+
+  res = GTD_INTERVAL_GET_CLASS (self)->compute_value (self,
+                                                              factor,
+                                                              value);
+
+  if (res)
+    return priv->values + RESULT;
+
+  return NULL;
+}
+
+/**
+ * gtd_interval_is_valid:
+ * @interval: a #GtdInterval
+ *
+ * Checks if the @interval has a valid initial and final values.
+ *
+ * Return value: %TRUE if the #GtdInterval has an initial and
+ *   final values, and %FALSE otherwise
+ *
+ * Since: 1.12
+ */
+gboolean
+gtd_interval_is_valid (GtdInterval *self)
+{
+  GtdIntervalPrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_INTERVAL (self), FALSE);
+
+  priv = gtd_interval_get_instance_private (self);
+
+  return G_IS_VALUE (&priv->values[INITIAL]) &&
+         G_IS_VALUE (&priv->values[FINAL]);
+}
diff --git a/src/animation/gtd-interval.h b/src/animation/gtd-interval.h
new file mode 100644
index 0000000..efcfb03
--- /dev/null
+++ b/src/animation/gtd-interval.h
@@ -0,0 +1,116 @@
+/* gtd-interval.h
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_INTERVAL (gtd_interval_get_type())
+G_DECLARE_DERIVABLE_TYPE (GtdInterval, gtd_interval, GTD, INTERVAL, GInitiallyUnowned)
+
+/**
+ * GtdIntervalClass:
+ * @validate: virtual function for validating an interval
+ *   using a #GParamSpec
+ * @compute_value: virtual function for computing the value
+ *   inside an interval using an adimensional factor between 0 and 1
+ *
+ * The #GtdIntervalClass contains only private data.
+ *
+ * Since: 1.0
+ */
+struct _GtdIntervalClass
+{
+  /*< private >*/
+  GInitiallyUnownedClass parent_class;
+
+  /*< public >*/
+  gboolean (* validate)      (GtdInterval *self,
+                              GParamSpec  *pspec);
+  gboolean (* compute_value) (GtdInterval *self,
+                              gdouble      factor,
+                              GValue      *value);
+
+  /*< private >*/
+  /* padding for future expansion */
+  void (*_gtd_reserved1) (void);
+  void (*_gtd_reserved2) (void);
+  void (*_gtd_reserved3) (void);
+  void (*_gtd_reserved4) (void);
+  void (*_gtd_reserved5) (void);
+  void (*_gtd_reserved6) (void);
+};
+
+GtdInterval*    gtd_interval_new                (GType gtype,
+                                                 ...);
+
+GtdInterval*    gtd_interval_new_with_values    (GType         gtype,
+                                                 const GValue *initial,
+                                                 const GValue *final);
+
+
+GtdInterval*     gtd_interval_clone              (GtdInterval *self);
+
+GType            gtd_interval_get_value_type     (GtdInterval *self);
+
+void             gtd_interval_set_initial        (GtdInterval *self,
+                                                      ...);
+
+void             gtd_interval_set_initial_value  (GtdInterval  *self,
+                                                  const GValue *value);
+
+void             gtd_interval_get_initial_value  (GtdInterval *self,
+                                                  GValue      *value);
+
+GValue*          gtd_interval_peek_initial_value (GtdInterval *self);
+
+void             gtd_interval_set_final          (GtdInterval *self,
+                                                  ...);
+
+void             gtd_interval_set_final_value    (GtdInterval  *self,
+                                                  const GValue *value);
+
+void             gtd_interval_get_final_value    (GtdInterval *self,
+                                                  GValue      *value);
+
+GValue*          gtd_interval_peek_final_value   (GtdInterval *self);
+
+void             gtd_interval_set_interval       (GtdInterval *self,
+                                                  ...);
+
+void             gtd_interval_get_interval       (GtdInterval *self,
+                                                  ...);
+
+gboolean         gtd_interval_validate           (GtdInterval *self,
+                                                  GParamSpec  *pspec);
+
+gboolean         gtd_interval_compute_value      (GtdInterval *self,
+                                                  gdouble      factor,
+                                                  GValue      *value);
+
+const GValue*    gtd_interval_compute            (GtdInterval *self,
+                                                  gdouble      factor);
+
+gboolean         gtd_interval_is_valid           (GtdInterval *self);
+
+
+G_END_DECLS
diff --git a/src/animation/gtd-keyframe-transition.c b/src/animation/gtd-keyframe-transition.c
new file mode 100644
index 0000000..424e27b
--- /dev/null
+++ b/src/animation/gtd-keyframe-transition.c
@@ -0,0 +1,729 @@
+/* gtd-keyframe-transition.c
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * SECTION:gtd-keyframe-transition
+ * @Title: GtdKeyframeTransition
+ * @Short_Description: Keyframe property transition
+ *
+ * #GtdKeyframeTransition allows animating a property by defining
+ * "key frames": values at a normalized position on the transition
+ * duration.
+ *
+ * The #GtdKeyframeTransition interpolates the value of the property
+ * to which it's bound across these key values.
+ *
+ * Setting up a #GtdKeyframeTransition means providing the times,
+ * values, and easing modes between these key frames, for instance:
+ *
+ * |[
+ *   GtdTransition *keyframe;
+ *
+ *   keyframe = gtd_keyframe_transition_new ("opacity");
+ *   gtd_transition_set_from (keyframe, G_TYPE_UINT, 255);
+ *   gtd_transition_set_to (keyframe, G_TYPE_UINT, 0);
+ *   gtd_keyframe_transition_set (GTD_KEYFRAME_TRANSITION (keyframe),
+ *                                    G_TYPE_UINT,
+ *                                    1, /&ast; number of key frames &ast;/
+ *                                    0.5, 128, GTD_EASE_IN_OUT_CUBIC);
+ * ]|
+ *
+ * The example above sets up a keyframe transition for the #GtdActor:opacity
+ * property of a #GtdActor; the transition starts and sets the value of the
+ * property to fully transparent; between the start of the transition and its mid
+ * point, it will animate the property to half opacity, using an easy in/easy out
+ * progress. Once the transition reaches the mid point, it will linearly fade the
+ * actor out until it reaches the end of the transition.
+ *
+ * The #GtdKeyframeTransition will add an implicit key frame between the last
+ * and the 1.0 value, to interpolate to the final value of the transition's
+ * interval.
+ *
+ * #GtdKeyframeTransition is available since Gtd 1.12.
+ */
+
+#include "gtd-keyframe-transition.h"
+
+#include "gtd-debug.h"
+#include "gtd-easing.h"
+#include "gtd-interval.h"
+#include "gtd-timeline.h"
+
+#include <math.h>
+#include <gobject/gvaluecollector.h>
+
+typedef struct _KeyFrame
+{
+  double key;
+
+  double start;
+  double end;
+
+  GtdEaseMode mode;
+
+  GtdInterval *interval;
+} KeyFrame;
+
+typedef struct
+{
+  GArray *frames;
+
+  gint current_frame;
+} GtdKeyframeTransitionPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GtdKeyframeTransition, gtd_keyframe_transition, GTD_TYPE_PROPERTY_TRANSITION)
+
+static void
+key_frame_free (gpointer data)
+{
+  if (data != NULL)
+    {
+      KeyFrame *key = data;
+
+      g_object_unref (key->interval);
+    }
+}
+
+static int
+sort_by_key (gconstpointer a,
+             gconstpointer b)
+{
+  const KeyFrame *k_a = a;
+  const KeyFrame *k_b = b;
+
+  if (fabs (k_a->key - k_b->key) < 0.0001)
+    return 0;
+
+  if (k_a->key > k_b->key)
+    return 1;
+
+  return -1;
+}
+
+static inline void
+gtd_keyframe_transition_sort_frames (GtdKeyframeTransition *self)
+{
+  GtdKeyframeTransitionPrivate *priv = gtd_keyframe_transition_get_instance_private (self);
+
+  if (priv->frames != NULL)
+    g_array_sort (priv->frames, sort_by_key);
+}
+
+static inline void
+gtd_keyframe_transition_init_frames (GtdKeyframeTransition *self,
+                                     gssize                 n_key_frames)
+{
+  GtdKeyframeTransitionPrivate *priv = gtd_keyframe_transition_get_instance_private (self);
+  guint i;
+
+  priv->frames = g_array_sized_new (FALSE, FALSE,
+                                    sizeof (KeyFrame),
+                                    n_key_frames);
+  g_array_set_clear_func (priv->frames, key_frame_free);
+
+  /* we add an implicit key frame that goes to 1.0, so that the
+   * user doesn't have to do that an can simply add key frames
+   * in between 0.0 and 1.0
+   */
+  for (i = 0; i < n_key_frames + 1; i++)
+    {
+      KeyFrame frame;
+
+      if (i == n_key_frames)
+        frame.key = 1.0;
+      else
+        frame.key = 0.0;
+
+      frame.mode = GTD_EASE_LINEAR;
+      frame.interval = NULL;
+
+      g_array_insert_val (priv->frames, i, frame);
+    }
+}
+
+static inline void
+gtd_keyframe_transition_update_frames (GtdKeyframeTransition *self)
+{
+  GtdKeyframeTransitionPrivate *priv = gtd_keyframe_transition_get_instance_private (self);
+  guint i;
+
+  if (priv->frames == NULL)
+    return;
+
+  for (i = 0; i < priv->frames->len; i++)
+    {
+      KeyFrame *cur_frame = &g_array_index (priv->frames, KeyFrame, i);
+      KeyFrame *prev_frame;
+
+      if (i > 0)
+        prev_frame = &g_array_index (priv->frames, KeyFrame, i - 1);
+      else
+        prev_frame = NULL;
+
+      if (prev_frame != NULL)
+        {
+          cur_frame->start = prev_frame->key;
+
+          if (prev_frame->interval != NULL)
+            {
+              const GValue *value;
+
+              value = gtd_interval_peek_final_value (prev_frame->interval);
+
+              if (cur_frame->interval != NULL)
+                gtd_interval_set_initial_value (cur_frame->interval, value);
+              else
+                {
+                  cur_frame->interval =
+                    gtd_interval_new_with_values (G_VALUE_TYPE (value), value, NULL);
+                }
+            }
+        }
+      else
+        cur_frame->start = 0.0;
+
+      cur_frame->end = cur_frame->key;
+    }
+}
+
+static void
+gtd_keyframe_transition_compute_value (GtdTransition *transition,
+                                       GtdAnimatable *animatable,
+                                       GtdInterval   *interval,
+                                       gdouble        progress)
+{
+  GtdKeyframeTransition *self = GTD_KEYFRAME_TRANSITION (transition);
+  GtdKeyframeTransitionPrivate *priv = gtd_keyframe_transition_get_instance_private (self);
+  GtdTimeline *timeline = GTD_TIMELINE (self);
+  GtdTransitionClass *parent_class;
+  GtdTimelineDirection direction;
+  GtdInterval *real_interval;
+  gdouble real_progress;
+  double t, d, p;
+  KeyFrame *cur_frame = NULL;
+
+  real_interval = interval;
+  real_progress = progress;
+
+  /* if we don't have any keyframe, we behave like our parent class */
+  if (priv->frames == NULL)
+    goto out;
+
+  direction = gtd_timeline_get_direction (timeline);
+
+  /* we need a normalized linear value */
+  t = gtd_timeline_get_elapsed_time (timeline);
+  d = gtd_timeline_get_duration (timeline);
+  p = t / d;
+
+  if (priv->current_frame < 0)
+    {
+      if (direction == GTD_TIMELINE_FORWARD)
+        priv->current_frame = 0;
+      else
+        priv->current_frame = priv->frames->len - 1;
+    }
+
+  cur_frame = &g_array_index (priv->frames, KeyFrame, priv->current_frame);
+
+  /* skip to the next key frame, depending on the direction of the timeline */
+  if (direction == GTD_TIMELINE_FORWARD)
+    {
+      if (p > cur_frame->end)
+        {
+          priv->current_frame = MIN (priv->current_frame + 1, priv->frames->len - 1);
+          cur_frame = &g_array_index (priv->frames, KeyFrame, priv->current_frame);
+       }
+    }
+  else
+    {
+      if (p < cur_frame->start)
+        {
+          priv->current_frame = MAX (priv->current_frame - 1, 0);
+
+          cur_frame = &g_array_index (priv->frames, KeyFrame, priv->current_frame);
+        }
+    }
+
+  /* if we are at the boundaries of the transition, use the from and to
+   * value from the transition
+   */
+  if (priv->current_frame == 0)
+    {
+      const GValue *value;
+
+      value = gtd_interval_peek_initial_value (interval);
+      gtd_interval_set_initial_value (cur_frame->interval, value);
+    }
+  else if (priv->current_frame == priv->frames->len - 1)
+    {
+      const GValue *value;
+
+      cur_frame->mode = gtd_timeline_get_progress_mode (timeline);
+
+      value = gtd_interval_peek_final_value (interval);
+      gtd_interval_set_final_value (cur_frame->interval, value);
+    }
+
+  /* update the interval to be used to interpolate the property */
+  real_interval = cur_frame->interval;
+
+  /* normalize the progress and apply the easing mode */
+  real_progress = gtd_easing_for_mode (cur_frame->mode,
+                                       (p - cur_frame->start),
+                                       (cur_frame->end - cur_frame->start));
+
+#ifdef GTD_ENABLE_DEBUG
+  if (GTD_HAS_DEBUG (ANIMATION))
+    {
+      char *from, *to;
+      const GValue *value;
+
+      value = gtd_interval_peek_initial_value (cur_frame->interval);
+      from = g_strdup_value_contents (value);
+
+      value = gtd_interval_peek_final_value (cur_frame->interval);
+      to = g_strdup_value_contents (value);
+
+      GTD_TRACE_MSG ("[animation] cur_frame [%d] => { %g, %s, %s %s %s } - "
+                    "progress: %g, sub-progress: %g\n",
+                    priv->current_frame,
+                    cur_frame->key,
+                    gtd_get_easing_name_for_mode (cur_frame->mode),
+                    from,
+                    direction == GTD_TIMELINE_FORWARD ? "->" : "<-",
+                    to,
+                    p,
+                     real_progress);
+
+      g_free (from);
+      g_free (to);
+    }
+#endif /* GTD_ENABLE_DEBUG */
+
+out:
+  parent_class =
+    GTD_TRANSITION_CLASS (gtd_keyframe_transition_parent_class);
+  parent_class->compute_value (transition, animatable, real_interval, real_progress);
+}
+
+static void
+gtd_keyframe_transition_started (GtdTimeline *timeline)
+{
+  GtdKeyframeTransition *self;
+  GtdKeyframeTransitionPrivate *priv;
+
+  self = GTD_KEYFRAME_TRANSITION (timeline);
+  priv = gtd_keyframe_transition_get_instance_private (self);
+
+  priv->current_frame = -1;
+
+  gtd_keyframe_transition_sort_frames (self);
+  gtd_keyframe_transition_update_frames (self);
+}
+
+static void
+gtd_keyframe_transition_completed (GtdTimeline *timeline)
+{
+  GtdKeyframeTransition *self = GTD_KEYFRAME_TRANSITION (timeline);
+  GtdKeyframeTransitionPrivate *priv = gtd_keyframe_transition_get_instance_private (self);
+
+  priv->current_frame = -1;
+}
+
+static void
+gtd_keyframe_transition_finalize (GObject *gobject)
+{
+  GtdKeyframeTransition *self = GTD_KEYFRAME_TRANSITION (gobject);
+  GtdKeyframeTransitionPrivate *priv = gtd_keyframe_transition_get_instance_private (self);
+
+  if (priv->frames != NULL)
+    g_array_unref (priv->frames);
+
+  G_OBJECT_CLASS (gtd_keyframe_transition_parent_class)->finalize (gobject);
+}
+
+static void
+gtd_keyframe_transition_class_init (GtdKeyframeTransitionClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  GtdTimelineClass *timeline_class = GTD_TIMELINE_CLASS (klass);
+  GtdTransitionClass *transition_class = GTD_TRANSITION_CLASS (klass);
+
+  gobject_class->finalize = gtd_keyframe_transition_finalize;
+
+  timeline_class->started = gtd_keyframe_transition_started;
+  timeline_class->completed = gtd_keyframe_transition_completed;
+
+  transition_class->compute_value = gtd_keyframe_transition_compute_value;
+}
+
+static void
+gtd_keyframe_transition_init (GtdKeyframeTransition *self)
+{
+}
+
+/**
+ * gtd_keyframe_transition_new:
+ * @property_name: the property to animate
+ *
+ * Creates a new #GtdKeyframeTransition for @property_name.
+ *
+ * Return value: (transfer full): the newly allocated
+ *   #GtdKeyframeTransition instance. Use g_object_unref() when
+ *   done to free its resources.
+ *
+ * Since: 1.12
+ */
+GtdTransition *
+gtd_keyframe_transition_new (const gchar *property_name)
+{
+  return g_object_new (GTD_TYPE_KEYFRAME_TRANSITION,
+                       "property-name", property_name,
+                       NULL);
+}
+
+/**
+ * gtd_keyframe_transition_set_key_frames:
+ * @transition: a #GtdKeyframeTransition
+ * @n_key_frames: the number of values
+ * @key_frames: (array length=n_key_frames): an array of keys between 0.0
+ *   and 1.0, one for each key frame
+ *
+ * Sets the keys for each key frame inside @transition.
+ *
+ * If @transition does not hold any key frame, @n_key_frames key frames
+ * will be created; if @transition already has key frames, @key_frames must
+ * have at least as many elements as the number of key frames.
+ *
+ * Since: 1.12
+ */
+void
+gtd_keyframe_transition_set_key_frames (GtdKeyframeTransition *self,
+                                        guint                  n_key_frames,
+                                        const gdouble         *key_frames)
+{
+  GtdKeyframeTransitionPrivate *priv;
+  guint i;
+
+  g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self));
+  g_return_if_fail (n_key_frames > 0);
+  g_return_if_fail (key_frames != NULL);
+
+  priv = gtd_keyframe_transition_get_instance_private (self);
+
+  if (priv->frames == NULL)
+    gtd_keyframe_transition_init_frames (self, n_key_frames);
+  else
+    g_return_if_fail (n_key_frames == priv->frames->len - 1);
+
+  for (i = 0; i < n_key_frames; i++)
+    {
+      KeyFrame *frame = &g_array_index (priv->frames, KeyFrame, i);
+
+      frame->key = key_frames[i];
+    }
+}
+
+/**
+ * gtd_keyframe_transition_set_values:
+ * @transition: a #GtdKeyframeTransition
+ * @n_values: the number of values
+ * @values: (array length=n_values): an array of values, one for each
+ *   key frame
+ *
+ * Sets the values for each key frame inside @transition.
+ *
+ * If @transition does not hold any key frame, @n_values key frames will
+ * be created; if @transition already has key frames, @values must have
+ * at least as many elements as the number of key frames.
+ *
+ * Since: 1.12
+ */
+void
+gtd_keyframe_transition_set_values (GtdKeyframeTransition *self,
+                                    guint                  n_values,
+                                    const GValue          *values)
+{
+  GtdKeyframeTransitionPrivate *priv;
+  guint i;
+
+  g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self));
+  g_return_if_fail (n_values > 0);
+  g_return_if_fail (values != NULL);
+
+  priv = gtd_keyframe_transition_get_instance_private (self);
+
+  if (priv->frames == NULL)
+    gtd_keyframe_transition_init_frames (self, n_values);
+  else
+    g_return_if_fail (n_values == priv->frames->len - 1);
+
+  for (i = 0; i < n_values; i++)
+    {
+      KeyFrame *frame = &g_array_index (priv->frames, KeyFrame, i);
+
+      if (frame->interval)
+        gtd_interval_set_final_value (frame->interval, &values[i]);
+      else
+        frame->interval =
+          gtd_interval_new_with_values (G_VALUE_TYPE (&values[i]), NULL,
+                                            &values[i]);
+    }
+}
+
+/**
+ * gtd_keyframe_transition_set_modes:
+ * @transition: a #GtdKeyframeTransition
+ * @n_modes: the number of easing modes
+ * @modes: (array length=n_modes): an array of easing modes, one for
+ *   each key frame
+ *
+ * Sets the easing modes for each key frame inside @transition.
+ *
+ * If @transition does not hold any key frame, @n_modes key frames will
+ * be created; if @transition already has key frames, @modes must have
+ * at least as many elements as the number of key frames.
+ *
+ * Since: 1.12
+ */
+void
+gtd_keyframe_transition_set_modes (GtdKeyframeTransition *self,
+                                   guint                  n_modes,
+                                   const GtdEaseMode     *modes)
+{
+  GtdKeyframeTransitionPrivate *priv;
+  guint i;
+
+  g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self));
+  g_return_if_fail (n_modes > 0);
+  g_return_if_fail (modes != NULL);
+
+  priv = gtd_keyframe_transition_get_instance_private (self);
+
+  if (priv->frames == NULL)
+    gtd_keyframe_transition_init_frames (self, n_modes);
+  else
+    g_return_if_fail (n_modes == priv->frames->len - 1);
+
+  for (i = 0; i < n_modes; i++)
+    {
+      KeyFrame *frame = &g_array_index (priv->frames, KeyFrame, i);
+
+      frame->mode = modes[i];
+    }
+}
+
+/**
+ * gtd_keyframe_transition_set: (skip)
+ * @transition: a #GtdKeyframeTransition
+ * @gtype: the type of the values to use for the key frames
+ * @n_key_frames: the number of key frames between the initial
+ *   and final values
+ * @...: a list of tuples, containing the key frame index, the value
+ *   at the key frame, and the animation mode
+ *
+ * Sets the key frames of the @transition.
+ *
+ * This variadic arguments function is a convenience for C developers;
+ * language bindings should use gtd_keyframe_transition_set_key_frames(),
+ * gtd_keyframe_transition_set_modes(), and
+ * gtd_keyframe_transition_set_values() instead.
+ *
+ * Since: 1.12
+ */
+void
+gtd_keyframe_transition_set (GtdKeyframeTransition *self,
+                             GType                  gtype,
+                             guint                  n_key_frames,
+                             ...)
+{
+  GtdKeyframeTransitionPrivate *priv;
+  va_list args;
+  guint i;
+
+  g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self));
+  g_return_if_fail (gtype != G_TYPE_INVALID);
+  g_return_if_fail (n_key_frames > 0);
+
+  priv = gtd_keyframe_transition_get_instance_private (self);
+
+  if (priv->frames == NULL)
+    gtd_keyframe_transition_init_frames (self, n_key_frames);
+  else
+    g_return_if_fail (n_key_frames == priv->frames->len - 1);
+
+  va_start (args, n_key_frames);
+
+  for (i = 0; i < n_key_frames; i++)
+    {
+      KeyFrame *frame = &g_array_index (priv->frames, KeyFrame, i);
+      GValue value = G_VALUE_INIT;
+      char *error = NULL;
+
+      frame->key = va_arg (args, gdouble);
+
+      G_VALUE_COLLECT_INIT (&value, gtype, args, 0, &error);
+      if (error != NULL)
+        {
+          g_warning ("%s: %s", G_STRLOC, error);
+          g_free (error);
+          break;
+        }
+
+      frame->mode = va_arg (args, GtdEaseMode);
+
+      g_clear_object (&frame->interval);
+      frame->interval = gtd_interval_new_with_values (gtype, NULL, &value);
+
+      g_value_unset (&value);
+    }
+
+  va_end (args);
+}
+
+/**
+ * gtd_keyframe_transition_clear:
+ * @transition: a #GtdKeyframeTransition
+ *
+ * Removes all key frames from @transition.
+ *
+ * Since: 1.12
+ */
+void
+gtd_keyframe_transition_clear (GtdKeyframeTransition *self)
+{
+  GtdKeyframeTransitionPrivate *priv;
+
+  g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self));
+
+  priv = gtd_keyframe_transition_get_instance_private (self);
+  if (priv->frames != NULL)
+    {
+      g_array_unref (priv->frames);
+      priv->frames = NULL;
+    }
+}
+
+/**
+ * gtd_keyframe_transition_get_n_key_frames:
+ * @transition: a #GtdKeyframeTransition
+ *
+ * Retrieves the number of key frames inside @transition.
+ *
+ * Return value: the number of key frames
+ *
+ * Since: 1.12
+ */
+guint
+gtd_keyframe_transition_get_n_key_frames (GtdKeyframeTransition *self)
+{
+  GtdKeyframeTransitionPrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_KEYFRAME_TRANSITION (self), 0);
+
+  priv = gtd_keyframe_transition_get_instance_private (self);
+  if (priv->frames == NULL)
+    return 0;
+
+  return priv->frames->len - 1;
+}
+
+/**
+ * gtd_keyframe_transition_set_key_frame:
+ * @transition: a #GtdKeyframeTransition
+ * @index_: the index of the key frame
+ * @key: the key of the key frame
+ * @mode: the easing mode of the key frame
+ * @value: a #GValue containing the value of the key frame
+ *
+ * Sets the details of the key frame at @index_ inside @transition.
+ *
+ * The @transition must already have a key frame at @index_, and @index_
+ * must be smaller than the number of key frames inside @transition.
+ *
+ * Since: 1.12
+ */
+void
+gtd_keyframe_transition_set_key_frame (GtdKeyframeTransition *self,
+                                           guint                      index_,
+                                           double                     key,
+                                           GtdEaseMode       mode,
+                                           const GValue              *value)
+{
+  GtdKeyframeTransitionPrivate *priv;
+  KeyFrame *frame;
+
+  g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self));
+
+  priv = gtd_keyframe_transition_get_instance_private (self);
+
+  g_return_if_fail (priv->frames != NULL);
+  g_return_if_fail (index_ < priv->frames->len - 1);
+
+  frame = &g_array_index (priv->frames, KeyFrame, index_);
+  frame->key = key;
+  frame->mode = mode;
+  gtd_interval_set_final_value (frame->interval, value);
+}
+
+/**
+ * gtd_keyframe_transition_get_key_frame:
+ * @transition: a #GtdKeyframeTransition
+ * @index_: the index of the key frame
+ * @key: (out) (allow-none): return location for the key, or %NULL
+ * @mode: (out) (allow-none): return location for the easing mode, or %NULL
+ * @value: (out caller-allocates): a #GValue initialized with the type of
+ *   the values
+ *
+ * Retrieves the details of the key frame at @index_ inside @transition.
+ *
+ * The @transition must already have key frames set, and @index_ must be
+ * smaller than the number of key frames.
+ *
+ * Since: 1.12
+ */
+void
+gtd_keyframe_transition_get_key_frame (GtdKeyframeTransition *self,
+                                       guint                  index_,
+                                       double                *key,
+                                       GtdEaseMode           *mode,
+                                       GValue                *value)
+{
+  GtdKeyframeTransitionPrivate *priv;
+  const KeyFrame *frame;
+
+  g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self));
+
+  priv = gtd_keyframe_transition_get_instance_private (self);
+  g_return_if_fail (priv->frames != NULL);
+  g_return_if_fail (index_ < priv->frames->len - 1);
+
+  frame = &g_array_index (priv->frames, KeyFrame, index_);
+
+  if (key != NULL)
+    *key = frame->key;
+
+  if (mode != NULL)
+    *mode = frame->mode;
+
+  if (value != NULL)
+    gtd_interval_get_final_value (frame->interval, value);
+}
diff --git a/src/animation/gtd-keyframe-transition.h b/src/animation/gtd-keyframe-transition.h
new file mode 100644
index 0000000..f302be7
--- /dev/null
+++ b/src/animation/gtd-keyframe-transition.h
@@ -0,0 +1,85 @@
+/* gtd-keyframe-transition.h
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "gtd-property-transition.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_KEYFRAME_TRANSITION (gtd_keyframe_transition_get_type())
+G_DECLARE_DERIVABLE_TYPE (GtdKeyframeTransition, gtd_keyframe_transition, GTD, KEYFRAME_TRANSITION, 
GtdPropertyTransition)
+
+/**
+ * GtdKeyframeTransitionClass:
+ *
+ * The `GtdKeyframeTransitionClass` structure contains only
+ * private data.
+ *
+ * Since: 1.12
+ */
+struct _GtdKeyframeTransitionClass
+{
+  /*< private >*/
+  GtdPropertyTransitionClass parent_class;
+
+  gpointer _padding[8];
+};
+
+
+GtdTransition*       gtd_keyframe_transition_new                 (const gchar *property_name);
+
+
+void                 gtd_keyframe_transition_set_key_frames      (GtdKeyframeTransition *transition,
+                                                                  guint                  n_key_frames,
+                                                                  const gdouble         *key_frames);
+
+void                 gtd_keyframe_transition_set_values          (GtdKeyframeTransition *transition,
+                                                                  guint                  n_values,
+                                                                  const GValue          *values);
+
+void                 gtd_keyframe_transition_set_modes           (GtdKeyframeTransition *transition,
+                                                                  guint                  n_modes,
+                                                                  const GtdEaseMode     *modes);
+
+void                 gtd_keyframe_transition_set                 (GtdKeyframeTransition *transition,
+                                                                  GType                  gtype,
+                                                                  guint                  n_key_frames,
+                                                                  ...);
+
+
+void                 gtd_keyframe_transition_set_key_frame       (GtdKeyframeTransition *transition,
+                                                                  guint                  index_,
+                                                                  double                 key,
+                                                                  GtdEaseMode            mode,
+                                                                  const GValue          *value);
+
+void                 gtd_keyframe_transition_get_key_frame       (GtdKeyframeTransition *transition,
+                                                                  guint                  index_,
+                                                                  double                *key,
+                                                                  GtdEaseMode           *mode,
+                                                                  GValue                *value);
+
+guint                gtd_keyframe_transition_get_n_key_frames    (GtdKeyframeTransition  *transition);
+
+
+void                 gtd_keyframe_transition_clear               (GtdKeyframeTransition  *transition);
+
+G_END_DECLS
diff --git a/src/animation/gtd-property-transition.c b/src/animation/gtd-property-transition.c
new file mode 100644
index 0000000..db364ae
--- /dev/null
+++ b/src/animation/gtd-property-transition.c
@@ -0,0 +1,369 @@
+/* gtd-property-transition.c
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+
+/**
+ * SECTION:gtd-property-transition
+ * @Title: GtdPropertyTransition
+ * @Short_Description: Property transitions
+ *
+ * #GtdPropertyTransition is a specialized #GtdTransition that
+ * can be used to tween a property of a #GtdAnimatable instance.
+ *
+ * #GtdPropertyTransition is available since Gtd 1.10
+ */
+
+#include "gtd-property-transition.h"
+
+#include "gtd-animatable.h"
+#include "gtd-debug.h"
+#include "gtd-interval.h"
+#include "gtd-transition.h"
+
+typedef struct
+{
+  gchar              *property_name;
+
+  GParamSpec         *pspec;
+} GtdPropertyTransitionPrivate;
+
+enum
+{
+  PROP_0,
+  PROP_PROPERTY_NAME,
+  PROP_LAST,
+};
+
+static GParamSpec *obj_props[PROP_LAST] = { NULL, };
+
+G_DEFINE_TYPE_WITH_PRIVATE (GtdPropertyTransition, gtd_property_transition, GTD_TYPE_TRANSITION)
+
+static inline void
+gtd_property_transition_ensure_interval (GtdPropertyTransition *self,
+                                         GtdAnimatable         *animatable,
+                                         GtdInterval           *interval)
+{
+  GtdPropertyTransitionPrivate *priv = gtd_property_transition_get_instance_private (self);
+  GValue *value_p;
+
+  if (gtd_interval_is_valid (interval))
+    return;
+
+  /* if no initial value has been set, use the current value */
+  value_p = gtd_interval_peek_initial_value (interval);
+  if (!G_IS_VALUE (value_p))
+    {
+      g_value_init (value_p, gtd_interval_get_value_type (interval));
+      gtd_animatable_get_initial_state (animatable,
+                                            priv->property_name,
+                                            value_p);
+    }
+
+  /* if no final value has been set, use the current value */
+  value_p = gtd_interval_peek_final_value (interval);
+  if (!G_IS_VALUE (value_p))
+    {
+      g_value_init (value_p, gtd_interval_get_value_type (interval));
+      gtd_animatable_get_initial_state (animatable,
+                                            priv->property_name,
+                                            value_p);
+    }
+}
+
+static void
+gtd_property_transition_attached (GtdTransition *transition,
+                                  GtdAnimatable *animatable)
+{
+  GtdPropertyTransition *self = GTD_PROPERTY_TRANSITION (transition);
+  GtdPropertyTransitionPrivate *priv = gtd_property_transition_get_instance_private (self);
+  GtdInterval *interval;
+
+  if (priv->property_name == NULL)
+    return;
+
+  priv->pspec =
+    gtd_animatable_find_property (animatable, priv->property_name);
+
+  if (priv->pspec == NULL)
+    return;
+
+  interval = gtd_transition_get_interval (transition);
+  if (interval == NULL)
+    return;
+
+  gtd_property_transition_ensure_interval (self, animatable, interval);
+}
+
+static void
+gtd_property_transition_detached (GtdTransition *transition,
+                                  GtdAnimatable *animatable)
+{
+  GtdPropertyTransition *self = GTD_PROPERTY_TRANSITION (transition);
+  GtdPropertyTransitionPrivate *priv = gtd_property_transition_get_instance_private (self);
+
+  priv->pspec = NULL;
+}
+
+static void
+gtd_property_transition_compute_value (GtdTransition *transition,
+                                       GtdAnimatable *animatable,
+                                       GtdInterval   *interval,
+                                       gdouble        progress)
+{
+  GtdPropertyTransition *self = GTD_PROPERTY_TRANSITION (transition);
+  GtdPropertyTransitionPrivate *priv = gtd_property_transition_get_instance_private (self);
+  GValue value = G_VALUE_INIT;
+  GType p_type, i_type;
+  gboolean res;
+
+  /* if we have a GParamSpec we also have an animatable instance */
+  if (priv->pspec == NULL)
+    return;
+
+  gtd_property_transition_ensure_interval (self, animatable, interval);
+
+  p_type = G_PARAM_SPEC_VALUE_TYPE (priv->pspec);
+  i_type = gtd_interval_get_value_type (interval);
+
+  g_value_init (&value, i_type);
+
+  res = gtd_animatable_interpolate_value (animatable,
+                                              priv->property_name,
+                                              interval,
+                                              progress,
+                                              &value);
+
+  if (res)
+    {
+      if (i_type != p_type || g_type_is_a (i_type, p_type))
+        {
+          if (g_value_type_transformable (i_type, p_type))
+            {
+              GValue transform = G_VALUE_INIT;
+
+              g_value_init (&transform, p_type);
+
+              if (g_value_transform (&value, &transform))
+                {
+                  gtd_animatable_set_final_state (animatable,
+                                                      priv->property_name,
+                                                      &transform);
+                }
+              else
+                g_warning ("%s: Unable to convert a value of type '%s' from "
+                           "the value type '%s' of the interval.",
+                           G_STRLOC,
+                           g_type_name (p_type),
+                           g_type_name (i_type));
+
+              g_value_unset (&transform);
+            }
+        }
+      else
+        gtd_animatable_set_final_state (animatable,
+                                            priv->property_name,
+                                            &value);
+    }
+
+  g_value_unset (&value);
+}
+
+static void
+gtd_property_transition_set_property (GObject      *gobject,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  GtdPropertyTransition *self = GTD_PROPERTY_TRANSITION (gobject);
+
+  switch (prop_id)
+    {
+    case PROP_PROPERTY_NAME:
+      gtd_property_transition_set_property_name (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+    }
+}
+
+static void
+gtd_property_transition_get_property (GObject    *gobject,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  GtdPropertyTransition *self = GTD_PROPERTY_TRANSITION (gobject);
+  GtdPropertyTransitionPrivate *priv = gtd_property_transition_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_PROPERTY_NAME:
+      g_value_set_string (value, priv->property_name);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+    }
+}
+
+static void
+gtd_property_transition_finalize (GObject *gobject)
+{
+  GtdPropertyTransition *self = GTD_PROPERTY_TRANSITION (gobject);
+  GtdPropertyTransitionPrivate *priv = gtd_property_transition_get_instance_private (self);
+
+  g_free (priv->property_name);
+
+  G_OBJECT_CLASS (gtd_property_transition_parent_class)->finalize (gobject);
+}
+
+static void
+gtd_property_transition_class_init (GtdPropertyTransitionClass *klass)
+{
+  GtdTransitionClass *transition_class = GTD_TRANSITION_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  transition_class->attached = gtd_property_transition_attached;
+  transition_class->detached = gtd_property_transition_detached;
+  transition_class->compute_value = gtd_property_transition_compute_value;
+
+  gobject_class->set_property = gtd_property_transition_set_property;
+  gobject_class->get_property = gtd_property_transition_get_property;
+  gobject_class->finalize = gtd_property_transition_finalize;
+
+  /**
+   * GtdPropertyTransition:property-name:
+   *
+   * The name of the property of a #GtdAnimatable to animate.
+   *
+   * Since: 1.10
+   */
+  obj_props[PROP_PROPERTY_NAME] =
+    g_param_spec_string ("property-name",
+                         "Property Name",
+                         "The name of the property to animate",
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (gobject_class, PROP_LAST, obj_props);
+}
+
+static void
+gtd_property_transition_init (GtdPropertyTransition *self)
+{
+}
+
+/**
+ * gtd_property_transition_new_for_actor:
+ * @actor: a #GtdActor
+ * @property_name: (allow-none): a property of @animatable, or %NULL
+ *
+ * Creates a new #GtdPropertyTransition.
+ *
+ * Return value: (transfer full): the newly created #GtdPropertyTransition.
+ *   Use g_object_unref() when done
+ */
+GtdTransition *
+gtd_property_transition_new_for_actor (GtdWidget  *widget,
+                                       const char *property_name)
+{
+  return g_object_new (GTD_TYPE_PROPERTY_TRANSITION,
+                       "widget", widget,
+                       "property-name", property_name,
+                       NULL);
+}
+
+/**
+ * gtd_property_transition_new:
+ * @property_name: (allow-none): a property of @animatable, or %NULL
+ *
+ * Creates a new #GtdPropertyTransition.
+ *
+ * Return value: (transfer full): the newly created #GtdPropertyTransition.
+ *   Use g_object_unref() when done
+ *
+ * Since: 1.10
+ */
+GtdTransition *
+gtd_property_transition_new (const char *property_name)
+{
+  return g_object_new (GTD_TYPE_PROPERTY_TRANSITION,
+                       "property-name", property_name,
+                       NULL);
+}
+
+/**
+ * gtd_property_transition_set_property_name:
+ * @transition: a #GtdPropertyTransition
+ * @property_name: (allow-none): a property name
+ *
+ * Sets the #GtdPropertyTransition:property-name property of @transition.
+ *
+ * Since: 1.10
+ */
+void
+gtd_property_transition_set_property_name (GtdPropertyTransition *self,
+                                           const gchar           *property_name)
+{
+  GtdPropertyTransitionPrivate *priv;
+  GtdAnimatable *animatable;
+
+  g_return_if_fail (GTD_IS_PROPERTY_TRANSITION (self));
+
+  priv = gtd_property_transition_get_instance_private (self);
+
+  if (g_strcmp0 (priv->property_name, property_name) == 0)
+    return;
+
+  g_free (priv->property_name);
+  priv->property_name = g_strdup (property_name);
+  priv->pspec = NULL;
+
+  animatable = gtd_transition_get_animatable (GTD_TRANSITION (self));
+  if (animatable)
+    priv->pspec = gtd_animatable_find_property (animatable, priv->property_name);
+
+  g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_PROPERTY_NAME]);
+}
+
+/**
+ * gtd_property_transition_get_property_name:
+ * @transition: a #GtdPropertyTransition
+ *
+ * Retrieves the value of the #GtdPropertyTransition:property-name
+ * property.
+ *
+ * Return value: the name of the property being animated, or %NULL if
+ *   none is set. The returned string is owned by the @transition and
+ *   it should not be freed.
+ *
+ * Since: 1.10
+ */
+const char *
+gtd_property_transition_get_property_name (GtdPropertyTransition *self)
+{
+  GtdPropertyTransitionPrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_PROPERTY_TRANSITION (self), NULL);
+
+  priv = gtd_property_transition_get_instance_private (self);
+  return priv->property_name;
+}
diff --git a/src/animation/gtd-property-transition.h b/src/animation/gtd-property-transition.h
new file mode 100644
index 0000000..3b38dda
--- /dev/null
+++ b/src/animation/gtd-property-transition.h
@@ -0,0 +1,55 @@
+/* gtd-property-transition.h
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "gtd-transition.h"
+#include "gtd-types.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_PROPERTY_TRANSITION (gtd_property_transition_get_type())
+G_DECLARE_DERIVABLE_TYPE (GtdPropertyTransition, gtd_property_transition, GTD, PROPERTY_TRANSITION, 
GtdTransition)
+
+/**
+ * GtdPropertyTransitionClass:
+ *
+ * The #GtdPropertyTransitionClass structure
+ * contains private data.
+ */
+struct _GtdPropertyTransitionClass
+{
+  /*< private >*/
+  GtdTransitionClass parent_class;
+
+  gpointer _padding[8];
+};
+
+GtdTransition *         gtd_property_transition_new_for_widget      (GtdWidget  *widget,
+                                                                     const char *property_name);
+
+GtdTransition *         gtd_property_transition_new                 (const char *property_name);
+
+void                    gtd_property_transition_set_property_name   (GtdPropertyTransition *transition,
+                                                                     const char            *property_name);
+
+const char *            gtd_property_transition_get_property_name   (GtdPropertyTransition *transition);
+
+G_END_DECLS
diff --git a/src/animation/gtd-timeline-private.h b/src/animation/gtd-timeline-private.h
new file mode 100644
index 0000000..ddf7360
--- /dev/null
+++ b/src/animation/gtd-timeline-private.h
@@ -0,0 +1,31 @@
+/* gtd-timeline-private.h
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+
+
+#pragma once
+
+#include "gtd-timeline.h"
+
+G_BEGIN_DECLS
+
+void gtd_timeline_cancel_delay (GtdTimeline *self);
+
+G_END_DECLS
diff --git a/src/animation/gtd-timeline.c b/src/animation/gtd-timeline.c
new file mode 100644
index 0000000..75735f5
--- /dev/null
+++ b/src/animation/gtd-timeline.c
@@ -0,0 +1,2589 @@
+/*
+ * Gtd.
+ *
+ * An OpenGL based 'interactive canvas' library.
+ *
+ * Authored By Matthew Allum  <mallum openedhand com>
+ *
+ * Copyright (C) 2006 OpenedHand
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION:gtd-timeline
+ * @short_description: A class for time-based events
+ *
+ * #GtdTimeline is a base class for managing time-based event that cause
+ * GTK to redraw, such as animations.
+ *
+ * Each #GtdTimeline instance has a duration: once a timeline has been
+ * started, using gtd_timeline_start(), it will emit a signal that can
+ * be used to update the state of the widgets.
+ *
+ * It is important to note that #GtdTimeline is not a generic API for
+ * calling closures after an interval; each Timeline is tied into the master
+ * clock used to drive the frame cycle. If you need to schedule a closure
+ * after an interval, see gtd_threads_add_timeout() instead.
+ *
+ * Users of #GtdTimeline should connect to the #GtdTimeline::new-frame
+ * signal, which is emitted each time a timeline is advanced during the maste
+ * clock iteration. The #GtdTimeline::new-frame signal provides the time
+ * elapsed since the beginning of the timeline, in milliseconds. A normalized
+ * progress value can be obtained by calling gtd_timeline_get_progress().
+ * By using gtd_timeline_get_delta() it is possible to obtain the wallclock
+ * time elapsed since the last emission of the #GtdTimeline::new-frame
+ * signal.
+ *
+ * Initial state can be set up by using the #GtdTimeline::started signal,
+ * while final state can be set up by using the #GtdTimeline::stopped
+ * signal. The #GtdTimeline guarantees the emission of at least a single
+ * #GtdTimeline::new-frame signal, as well as the emission of the
+ * #GtdTimeline::completed signal every time the #GtdTimeline reaches
+ * its #GtdTimeline:duration.
+ *
+ * It is possible to connect to specific points in the timeline progress by
+ * adding markers using gtd_timeline_add_marker_at_time() and connecting
+ * to the #GtdTimeline::marker-reached signal.
+ *
+ * Timelines can be made to loop once they reach the end of their duration, by
+ * using gtd_timeline_set_repeat_count(); a looping timeline will still
+ * emit the #GtdTimeline::completed signal once it reaches the end of its
+ * duration at each repeat. If you want to be notified of the end of the last
+ * repeat, use the #GtdTimeline::stopped signal.
+ *
+ * Timelines have a #GtdTimeline:direction: the default direction is
+ * %GTD_TIMELINE_FORWARD, and goes from 0 to the duration; it is possible
+ * to change the direction to %GTD_TIMELINE_BACKWARD, and have the timeline
+ * go from the duration to 0. The direction can be automatically reversed
+ * when reaching completion by using the #GtdTimeline:auto-reverse property.
+ *
+ * Timelines are used in the Gtd animation framework by classes like
+ * #GtdTransition.
+ */
+
+#define G_LOG_DOMAIN "GtdTimeline"
+
+#include "gtd-timeline.h"
+
+#include "gtd-debug.h"
+#include "gnome-todo.h"
+#include "gtd-timeline-private.h"
+
+typedef struct
+{
+  GtdTimelineDirection direction;
+
+  GdkFrameClock *custom_frame_clock;
+  GdkFrameClock *frame_clock;
+
+  GtdWidget *widget;
+  gulong widget_destroy_handler_id;
+  gulong widget_map_handler_id;
+  gulong widget_unmap_handler_id;
+
+  guint delay_id;
+
+  /* The total length in milliseconds of this timeline */
+  guint duration;
+  guint delay;
+
+  /* The current amount of elapsed time */
+  gint64 elapsed_time;
+
+  /* The elapsed time since the last frame was fired */
+  gint64 msecs_delta;
+
+  GHashTable *markers_by_name;
+
+  /* Time we last advanced the elapsed time and showed a frame */
+  gint64 last_frame_time;
+
+  /* How many times the timeline should repeat */
+  gint repeat_count;
+
+  /* The number of times the timeline has repeated */
+  gint current_repeat;
+
+  GtdTimelineProgressFunc progress_func;
+  gpointer progress_data;
+  GDestroyNotify progress_notify;
+  GtdEaseMode progress_mode;
+
+  /* step() parameters */
+  gint n_steps;
+  GtdStepMode step_mode;
+
+  /* cubic-bezier() parameters */
+  graphene_point_t cb_1;
+  graphene_point_t cb_2;
+
+  guint is_playing         : 1;
+
+  /* If we've just started playing and haven't yet gotten
+   * a tick from the master clock
+   */
+  guint waiting_first_tick : 1;
+  guint auto_reverse       : 1;
+} GtdTimelinePrivate;
+
+typedef struct
+{
+  gchar *name;
+  GQuark quark;
+
+  union {
+    guint msecs;
+    gdouble progress;
+  } data;
+
+  guint is_relative : 1;
+} TimelineMarker;
+
+enum
+{
+  PROP_0,
+
+  PROP_AUTO_REVERSE,
+  PROP_DELAY,
+  PROP_DURATION,
+  PROP_DIRECTION,
+  PROP_REPEAT_COUNT,
+  PROP_PROGRESS_MODE,
+  PROP_FRAME_CLOCK,
+  PROP_WIDGET,
+
+  PROP_LAST
+};
+
+static GParamSpec *obj_props[PROP_LAST] = { NULL, };
+
+enum
+{
+  NEW_FRAME,
+  STARTED,
+  PAUSED,
+  COMPLETED,
+  MARKER_REACHED,
+  STOPPED,
+
+  LAST_SIGNAL
+};
+
+static guint timeline_signals[LAST_SIGNAL] = { 0, };
+
+static void update_frame_clock (GtdTimeline *self);
+static void maybe_add_timeline (GtdTimeline *self);
+static void maybe_remove_timeline (GtdTimeline *self);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GtdTimeline, gtd_timeline, G_TYPE_OBJECT)
+
+
+static TimelineMarker *
+timeline_marker_new_time (const gchar *name,
+                          guint        msecs)
+{
+  TimelineMarker *marker = g_slice_new (TimelineMarker);
+
+  marker->name = g_strdup (name);
+  marker->quark = g_quark_from_string (marker->name);
+  marker->is_relative = FALSE;
+  marker->data.msecs = msecs;
+
+  return marker;
+}
+
+static TimelineMarker *
+timeline_marker_new_progress (const gchar *name,
+                              gdouble      progress)
+{
+  TimelineMarker *marker = g_slice_new (TimelineMarker);
+
+  marker->name = g_strdup (name);
+  marker->quark = g_quark_from_string (marker->name);
+  marker->is_relative = TRUE;
+  marker->data.progress = CLAMP (progress, 0.0, 1.0);
+
+  return marker;
+}
+
+static void
+timeline_marker_free (gpointer data)
+{
+  if (G_LIKELY (data))
+    {
+      TimelineMarker *marker = data;
+
+      g_free (marker->name);
+      g_slice_free (TimelineMarker, marker);
+    }
+}
+
+/*< private >
+ * gtd_timeline_add_marker_internal:
+ * @timeline: a #GtdTimeline
+ * @marker: a TimelineMarker
+ *
+ * Adds @marker into the hash table of markers for @timeline.
+ *
+ * The TimelineMarker will either be added or, in case of collisions
+ * with another existing marker, freed. In any case, this function
+ * assumes the ownership of the passed @marker.
+ */
+static inline void
+gtd_timeline_add_marker_internal (GtdTimeline    *self,
+                                  TimelineMarker *marker)
+{
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+  TimelineMarker *old_marker;
+
+  /* create the hash table that will hold the markers */
+  if (G_UNLIKELY (priv->markers_by_name == NULL))
+    priv->markers_by_name = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                                   NULL,
+                                                   timeline_marker_free);
+
+  old_marker = g_hash_table_lookup (priv->markers_by_name, marker->name);
+  if (old_marker != NULL)
+    {
+      guint msecs;
+
+      if (old_marker->is_relative)
+        msecs = old_marker->data.progress * priv->duration;
+      else
+        msecs = old_marker->data.msecs;
+
+      g_warning ("A marker named '%s' already exists at time %d",
+                 old_marker->name,
+                 msecs);
+      timeline_marker_free (marker);
+      return;
+    }
+
+  g_hash_table_insert (priv->markers_by_name, marker->name, marker);
+}
+
+static void
+on_widget_destroyed (GtdWidget    *widget,
+                    GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  priv->widget = NULL;
+}
+
+struct CheckIfMarkerHitClosure
+{
+  GtdTimeline *timeline;
+  GtdTimelineDirection direction;
+  gint new_time;
+  gint duration;
+  gint delta;
+};
+
+static gboolean
+have_passed_time (const struct CheckIfMarkerHitClosure *data,
+                  gint                                  msecs)
+{
+  /* Ignore markers that are outside the duration of the timeline */
+  if (msecs < 0 || msecs > data->duration)
+    return FALSE;
+
+  if (data->direction == GTD_TIMELINE_FORWARD)
+    {
+      /* We need to special case when a marker is added at the
+         beginning of the timeline */
+      if (msecs == 0 &&
+          data->delta > 0 &&
+          data->new_time - data->delta <= 0)
+        return TRUE;
+
+      /* Otherwise it's just a simple test if the time is in range of
+         the previous time and the new time */
+      return (msecs > data->new_time - data->delta &&
+              msecs <= data->new_time);
+    }
+  else
+    {
+      /* We need to special case when a marker is added at the
+         end of the timeline */
+      if (msecs == data->duration &&
+          data->delta > 0 &&
+          data->new_time + data->delta >= data->duration)
+        return TRUE;
+
+      /* Otherwise it's just a simple test if the time is in range of
+         the previous time and the new time */
+      return (msecs >= data->new_time &&
+              msecs < data->new_time + data->delta);
+    }
+}
+
+static void
+check_if_marker_hit (const gchar *name,
+                     TimelineMarker *marker,
+                     struct CheckIfMarkerHitClosure *data)
+{
+  gint msecs;
+
+  if (marker->is_relative)
+    msecs = (gdouble) data->duration * marker->data.progress;
+  else
+    msecs = marker->data.msecs;
+
+  if (have_passed_time (data, msecs))
+    {
+      GTD_TRACE_MSG ("Marker '%s' reached", name);
+
+      g_signal_emit (data->timeline, timeline_signals[MARKER_REACHED],
+                     marker->quark,
+                     name,
+                     msecs);
+    }
+}
+
+static void
+check_markers (GtdTimeline *self,
+               gint         delta)
+{
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+  struct CheckIfMarkerHitClosure data;
+
+  /* shortcircuit here if we don't have any marker installed */
+  if (priv->markers_by_name == NULL)
+    return;
+
+  /* store the details of the timeline so that changing them in a
+     marker signal handler won't affect which markers are hit */
+  data.timeline = self;
+  data.direction = priv->direction;
+  data.new_time = priv->elapsed_time;
+  data.duration = priv->duration;
+  data.delta = delta;
+
+  g_hash_table_foreach (priv->markers_by_name,
+                        (GHFunc) check_if_marker_hit,
+                        &data);
+}
+
+static void
+emit_frame_signal (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  /* see bug https://bugzilla.gnome.org/show_bug.cgi?id=654066 */
+  gint elapsed = (gint) priv->elapsed_time;
+
+  GTD_TRACE_MSG ("Emitting ::new-frame signal on timeline[%p]", self);
+
+  g_signal_emit (self, timeline_signals[NEW_FRAME], 0, elapsed);
+}
+
+static gboolean
+is_complete (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  return (priv->direction == GTD_TIMELINE_FORWARD
+          ? priv->elapsed_time >= priv->duration
+          : priv->elapsed_time <= 0);
+}
+
+static void
+set_is_playing (GtdTimeline *self,
+                gboolean         is_playing)
+{
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  is_playing = !!is_playing;
+
+  if (is_playing == priv->is_playing)
+    return;
+
+  priv->is_playing = is_playing;
+
+  if (priv->is_playing)
+    {
+      priv->waiting_first_tick = TRUE;
+      priv->current_repeat = 0;
+
+      maybe_add_timeline (self);
+    }
+  else
+    {
+      maybe_remove_timeline (self);
+    }
+}
+
+static gboolean
+gtd_timeline_do_frame (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  g_object_ref (self);
+
+  GTD_TRACE_MSG ("Timeline [%p] activated (elapsed time: %ld, "
+                 "duration: %ld, msecs_delta: %ld)",
+                 self,
+                 (long) priv->elapsed_time,
+                 (long) priv->duration,
+                 (long) priv->msecs_delta);
+
+  /* Advance time */
+  if (priv->direction == GTD_TIMELINE_FORWARD)
+    priv->elapsed_time += priv->msecs_delta;
+  else
+    priv->elapsed_time -= priv->msecs_delta;
+
+  /* If we have not reached the end of the timeline: */
+  if (!is_complete (self))
+    {
+      /* Emit the signal */
+      emit_frame_signal (self);
+      check_markers (self, priv->msecs_delta);
+
+      g_object_unref (self);
+
+      return priv->is_playing;
+    }
+  else
+    {
+      /* Handle loop or stop */
+      GtdTimelineDirection saved_direction = priv->direction;
+      gint elapsed_time_delta = priv->msecs_delta;
+      guint overflow_msecs = priv->elapsed_time;
+      gint end_msecs;
+
+      /* Update the current elapsed time in case the signal handlers
+       * want to take a peek. If we clamp elapsed time, then we need
+       * to correpondingly reduce elapsed_time_delta to reflect the correct
+       * range of times */
+      if (priv->direction == GTD_TIMELINE_FORWARD)
+        {
+          elapsed_time_delta -= (priv->elapsed_time - priv->duration);
+          priv->elapsed_time = priv->duration;
+        }
+      else if (priv->direction == GTD_TIMELINE_BACKWARD)
+        {
+          elapsed_time_delta -= - priv->elapsed_time;
+          priv->elapsed_time = 0;
+        }
+
+      end_msecs = priv->elapsed_time;
+
+      /* Emit the signal */
+      emit_frame_signal (self);
+      check_markers (self, elapsed_time_delta);
+
+      /* Did the signal handler modify the elapsed time? */
+      if (priv->elapsed_time != end_msecs)
+        {
+          g_object_unref (self);
+          return TRUE;
+        }
+
+      /* Note: If the new-frame signal handler paused the timeline
+       * on the last frame we will still go ahead and send the
+       * completed signal */
+      GTD_TRACE_MSG ("Timeline [%p] completed (cur: %ld, tot: %ld)",
+                    self,
+                    (long) priv->elapsed_time,
+                    (long) priv->msecs_delta);
+
+      if (priv->is_playing &&
+          (priv->repeat_count == 0 ||
+           priv->repeat_count == priv->current_repeat))
+        {
+          /* We stop the timeline now, so that the completed signal handler
+           * may choose to re-start the timeline
+           *
+           * XXX Perhaps we should do this earlier, and regardless of
+           * priv->repeat_count. Are we limiting the things that could be
+           * done in the above new-frame signal handler?
+           */
+          set_is_playing (self, FALSE);
+
+          g_signal_emit (self, timeline_signals[COMPLETED], 0);
+          g_signal_emit (self, timeline_signals[STOPPED], 0, TRUE);
+        }
+      else
+        g_signal_emit (self, timeline_signals[COMPLETED], 0);
+
+      priv->current_repeat += 1;
+
+      if (priv->auto_reverse)
+        {
+          /* :auto-reverse changes the direction of the timeline */
+          if (priv->direction == GTD_TIMELINE_FORWARD)
+            priv->direction = GTD_TIMELINE_BACKWARD;
+          else
+            priv->direction = GTD_TIMELINE_FORWARD;
+
+          g_object_notify_by_pspec (G_OBJECT (self),
+                                    obj_props[PROP_DIRECTION]);
+        }
+
+      /* Again check to see if the user has manually played with
+       * the elapsed time, before we finally stop or loop the timeline */
+
+      if (priv->elapsed_time != end_msecs &&
+          !(/* Except allow changing time from 0 -> duration (or vice-versa)
+               since these are considered equivalent */
+            (priv->elapsed_time == 0 && end_msecs == priv->duration) ||
+            (priv->elapsed_time == priv->duration && end_msecs == 0)
+          ))
+        {
+          g_object_unref (self);
+          return TRUE;
+        }
+
+      if (priv->repeat_count != 0)
+        {
+          /* We try and interpolate smoothly around a loop */
+          if (saved_direction == GTD_TIMELINE_FORWARD)
+            priv->elapsed_time = overflow_msecs - priv->duration;
+          else
+            priv->elapsed_time = priv->duration + overflow_msecs;
+
+          /* Or if the direction changed, we try and bounce */
+          if (priv->direction != saved_direction)
+            priv->elapsed_time = priv->duration - priv->elapsed_time;
+
+          /* If we have overflowed then we are changing the elapsed
+             time without emitting the new frame signal so we need to
+             check for markers again */
+          check_markers (self,
+                         priv->direction == GTD_TIMELINE_FORWARD
+                           ? priv->elapsed_time
+                           : priv->duration - priv->elapsed_time);
+
+          g_object_unref (self);
+          return TRUE;
+        }
+      else
+        {
+          gtd_timeline_rewind (self);
+
+          g_object_unref (self);
+          return FALSE;
+        }
+    }
+}
+
+static void
+tick_timeline (GtdTimeline *self,
+               gint64       tick_time)
+{
+  GtdTimelinePrivate *priv;
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  GTD_TRACE_MSG ("Timeline [%p] ticked (elapsed_time: %ld, msecs_delta: %ld, "
+                 "last_frame_time: %ld, tick_time: %ld)",
+                 self,
+                 (long) priv->elapsed_time,
+                 (long) priv->msecs_delta,
+                 (long) priv->last_frame_time,
+                 (long) tick_time);
+
+  /* Check the is_playing variable before performing the timeline tick.
+   * This is necessary, as if a timeline is stopped in response to a
+   * frame clock generated signal of a different timeline, this code can
+   * still be reached.
+   */
+  if (!priv->is_playing)
+    return;
+
+  if (priv->waiting_first_tick)
+    {
+      priv->last_frame_time = tick_time;
+      priv->msecs_delta = 0;
+      priv->waiting_first_tick = FALSE;
+      gtd_timeline_do_frame (self);
+    }
+  else
+    {
+      gint64 msecs;
+
+      msecs = tick_time - priv->last_frame_time;
+
+      /* if the clock rolled back between ticks we need to
+       * account for it; the best course of action, since the
+       * clock roll back can happen by any arbitrary amount
+       * of milliseconds, is to drop a frame here
+       */
+      if (msecs < 0)
+        {
+          priv->last_frame_time = tick_time;
+          return;
+        }
+
+      if (msecs != 0)
+        {
+          /* Avoid accumulating error */
+          priv->last_frame_time += msecs;
+          priv->msecs_delta = msecs;
+          gtd_timeline_do_frame (self);
+        }
+    }
+}
+
+#if 0
+static void
+advance_timeline (GtdTimeline *self,
+                  gint64       tick_time)
+{
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  g_object_ref (self);
+
+  GTD_TRACE_MSG ("Timeline [%p] advancing (cur: %ld, tot: %ld, tick_time: %lu)",
+                 self,
+                 (long) priv->elapsed_time,
+                 (long) priv->msecs_delta,
+                 (long) tick_time);
+
+  priv->msecs_delta = tick_time;
+  priv->is_playing = TRUE;
+
+  gtd_timeline_do_frame (self);
+
+  priv->is_playing = FALSE;
+
+  g_object_unref (self);
+}
+#endif
+
+static void
+on_frame_clock_after_paint_cb (GdkFrameClock *frame_clock,
+                               GtdTimeline   *self)
+{
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  tick_timeline (self, gdk_frame_clock_get_frame_time (frame_clock) / 1000);
+
+  if (priv->widget)
+    gtk_widget_queue_allocate (GTK_WIDGET (priv->widget));
+  else
+    gdk_frame_clock_request_phase (priv->frame_clock, GDK_FRAME_CLOCK_PHASE_LAYOUT);
+}
+
+static void
+maybe_add_timeline (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  if (!priv->frame_clock)
+    return;
+
+  g_signal_connect (priv->frame_clock, "after-paint", G_CALLBACK (on_frame_clock_after_paint_cb), self);
+
+  if (priv->widget)
+    gtk_widget_queue_allocate (GTK_WIDGET (priv->widget));
+  else
+    gdk_frame_clock_request_phase (priv->frame_clock, GDK_FRAME_CLOCK_PHASE_LAYOUT);
+}
+
+static void
+maybe_remove_timeline (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  if (!priv->frame_clock)
+    return;
+
+  g_signal_handlers_disconnect_by_func (priv->frame_clock, on_frame_clock_after_paint_cb, self);
+}
+
+static void
+set_frame_clock_internal (GtdTimeline   *self,
+                          GdkFrameClock *frame_clock)
+{
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  if (priv->frame_clock == frame_clock)
+    return;
+
+  if (priv->frame_clock && priv->is_playing)
+    maybe_remove_timeline (self);
+
+  g_set_object (&priv->frame_clock, frame_clock);
+
+  g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_FRAME_CLOCK]);
+
+  if (priv->is_playing)
+    maybe_add_timeline (self);
+}
+
+static void
+update_frame_clock (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+  GdkFrameClock *frame_clock = NULL;
+
+  if (priv->widget)
+    frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (priv->widget));
+
+  set_frame_clock_internal (self, frame_clock);
+}
+
+static void
+on_widget_map_changed_cb (GtdWidget   *widget,
+                          GtdTimeline *self)
+{
+  update_frame_clock (self);
+}
+
+/**
+ * gtd_timeline_set_widget:
+ * @timeline: a #GtdTimeline
+ * @widget: (nullable): a #GtdWidget
+ *
+ * Set the widget the timeline is associated with.
+ */
+void
+gtd_timeline_set_widget (GtdTimeline *self,
+                         GtdWidget   *widget)
+{
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  g_return_if_fail (!widget || (widget && !priv->custom_frame_clock));
+
+  if (priv->widget)
+    {
+      g_clear_signal_handler (&priv->widget_destroy_handler_id, priv->widget);
+      g_clear_signal_handler (&priv->widget_map_handler_id, priv->widget);
+      g_clear_signal_handler (&priv->widget_unmap_handler_id, priv->widget);
+      priv->widget = NULL;
+
+      if (priv->is_playing)
+        maybe_remove_timeline (self);
+
+      priv->frame_clock = NULL;
+    }
+
+  priv->widget = widget;
+
+  if (priv->widget)
+    {
+      priv->widget_destroy_handler_id =
+        g_signal_connect (priv->widget, "destroy",
+                          G_CALLBACK (on_widget_destroyed),
+                          self);
+      priv->widget_map_handler_id =
+        g_signal_connect (priv->widget, "map",
+                          G_CALLBACK (on_widget_map_changed_cb),
+                          self);
+      priv->widget_unmap_handler_id =
+        g_signal_connect (priv->widget, "unmap",
+                          G_CALLBACK (on_widget_map_changed_cb),
+                          self);
+    }
+
+  update_frame_clock (self);
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_timeline_set_property (GObject      *object,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+{
+  GtdTimeline *self = GTD_TIMELINE (object);
+
+  switch (prop_id)
+    {
+    case PROP_DELAY:
+      gtd_timeline_set_delay (self, g_value_get_uint (value));
+      break;
+
+    case PROP_DURATION:
+      gtd_timeline_set_duration (self, g_value_get_uint (value));
+      break;
+
+    case PROP_DIRECTION:
+      gtd_timeline_set_direction (self, g_value_get_enum (value));
+      break;
+
+    case PROP_AUTO_REVERSE:
+      gtd_timeline_set_auto_reverse (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_REPEAT_COUNT:
+      gtd_timeline_set_repeat_count (self, g_value_get_int (value));
+      break;
+
+    case PROP_PROGRESS_MODE:
+      gtd_timeline_set_progress_mode (self, g_value_get_enum (value));
+      break;
+
+    case PROP_FRAME_CLOCK:
+      gtd_timeline_set_frame_clock (self, g_value_get_object (value));
+      break;
+
+    case PROP_WIDGET:
+      gtd_timeline_set_widget (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+gtd_timeline_get_property (GObject    *object,
+                           guint       prop_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
+{
+  GtdTimeline *self = GTD_TIMELINE (object);
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_DELAY:
+      g_value_set_uint (value, priv->delay);
+      break;
+
+    case PROP_DURATION:
+      g_value_set_uint (value, gtd_timeline_get_duration (self));
+      break;
+
+    case PROP_DIRECTION:
+      g_value_set_enum (value, priv->direction);
+      break;
+
+    case PROP_AUTO_REVERSE:
+      g_value_set_boolean (value, priv->auto_reverse);
+      break;
+
+    case PROP_REPEAT_COUNT:
+      g_value_set_int (value, priv->repeat_count);
+      break;
+
+    case PROP_PROGRESS_MODE:
+      g_value_set_enum (value, priv->progress_mode);
+      break;
+
+    case PROP_FRAME_CLOCK:
+      g_value_set_object (value, priv->frame_clock);
+      break;
+
+    case PROP_WIDGET:
+      g_value_set_object (value, priv->widget);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+gtd_timeline_finalize (GObject *object)
+{
+  GtdTimeline *self = GTD_TIMELINE (object);
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  if (priv->markers_by_name)
+    g_hash_table_destroy (priv->markers_by_name);
+
+  if (priv->is_playing)
+    maybe_remove_timeline (self);
+
+  g_clear_object (&priv->frame_clock);
+
+  G_OBJECT_CLASS (gtd_timeline_parent_class)->finalize (object);
+}
+
+static void
+gtd_timeline_dispose (GObject *object)
+{
+  GtdTimeline *self = GTD_TIMELINE (object);
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  gtd_timeline_cancel_delay (self);
+
+  if (priv->widget)
+    {
+      g_clear_signal_handler (&priv->widget_destroy_handler_id, priv->widget);
+      g_clear_signal_handler (&priv->widget_map_handler_id, priv->widget);
+      g_clear_signal_handler (&priv->widget_unmap_handler_id, priv->widget);
+      priv->widget = NULL;
+    }
+
+  if (priv->progress_notify != NULL)
+    {
+      priv->progress_notify (priv->progress_data);
+      priv->progress_func = NULL;
+      priv->progress_data = NULL;
+      priv->progress_notify = NULL;
+    }
+
+  G_OBJECT_CLASS (gtd_timeline_parent_class)->dispose (object);
+}
+
+static void
+gtd_timeline_class_init (GtdTimelineClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  /**
+   * GtdTimeline::widget:
+   *
+   * The widget the timeline is associated with. This will determine what frame
+   * clock will drive it.
+   */
+  obj_props[PROP_WIDGET] =
+    g_param_spec_object ("widget",
+                         "Widget",
+                         "Associated GtdWidget",
+                         GTD_TYPE_WIDGET,
+                         G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  /**
+   * GtdTimeline:delay:
+   *
+   * A delay, in milliseconds, that should be observed by the
+   * timeline before actually starting.
+   *
+   * Since: 0.4
+   */
+  obj_props[PROP_DELAY] =
+    g_param_spec_uint ("delay",
+                       "Delay",
+                       "Delay before start",
+                       0, G_MAXUINT,
+                       0,
+                       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * GtdTimeline:duration:
+   *
+   * Duration of the timeline in milliseconds, depending on the
+   * GtdTimeline:fps value.
+   *
+   * Since: 0.6
+   */
+  obj_props[PROP_DURATION] =
+    g_param_spec_uint ("duration",
+                       "Duration",
+                       "Duration of the timeline in milliseconds",
+                       0, G_MAXUINT,
+                       1000,
+                       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * GtdTimeline:direction:GIT
+   *
+   * The direction of the timeline, either %GTD_TIMELINE_FORWARD or
+   * %GTD_TIMELINE_BACKWARD.
+   *
+   * Since: 0.6
+   */
+  obj_props[PROP_DIRECTION] =
+    g_param_spec_enum ("direction",
+                       "Direction",
+                       "Direction of the timeline",
+                       GTD_TYPE_TIMELINE_DIRECTION,
+                       GTD_TIMELINE_FORWARD,
+                       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * GtdTimeline:auto-reverse:
+   *
+   * If the direction of the timeline should be automatically reversed
+   * when reaching the end.
+   *
+   * Since: 1.6
+   */
+  obj_props[PROP_AUTO_REVERSE] =
+    g_param_spec_boolean ("auto-reverse",
+                          "Auto Reverse",
+                          "Whether the direction should be reversed when reaching the end",
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * GtdTimeline:repeat-count:
+   *
+   * Defines how many times the timeline should repeat.
+   *
+   * If the repeat count is 0, the timeline does not repeat.
+   *
+   * If the repeat count is set to -1, the timeline will repeat until it is
+   * stopped.
+   *
+   * Since: 1.10
+   */
+  obj_props[PROP_REPEAT_COUNT] =
+    g_param_spec_int ("repeat-count",
+                      "Repeat Count",
+                      "How many times the timeline should repeat",
+                      -1, G_MAXINT,
+                      0,
+                      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * GtdTimeline:progress-mode:
+   *
+   * Controls the way a #GtdTimeline computes the normalized progress.
+   *
+   * Since: 1.10
+   */
+  obj_props[PROP_PROGRESS_MODE] =
+    g_param_spec_enum ("progress-mode",
+                       "Progress Mode",
+                       "How the timeline should compute the progress",
+                       GTD_TYPE_EASE_MODE,
+                       GTD_EASE_LINEAR,
+                       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * GtdTimeline:frame-clock:
+   *
+   * The frame clock driving the timeline.
+   */
+  obj_props[PROP_FRAME_CLOCK] =
+    g_param_spec_object ("frame-clock",
+                         "Frame clock",
+                         "Frame clock driving the timeline",
+                         GDK_TYPE_FRAME_CLOCK,
+                         G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  object_class->dispose = gtd_timeline_dispose;
+  object_class->finalize = gtd_timeline_finalize;
+  object_class->set_property = gtd_timeline_set_property;
+  object_class->get_property = gtd_timeline_get_property;
+  g_object_class_install_properties (object_class, PROP_LAST, obj_props);
+
+  /**
+   * GtdTimeline::new-frame:
+   * @timeline: the timeline which received the signal
+   * @msecs: the elapsed time between 0 and duration
+   *
+   * The ::new-frame signal is emitted for each timeline running
+   * timeline before a new frame is drawn to give animations a chance
+   * to update the scene.
+   */
+  timeline_signals[NEW_FRAME] =
+    g_signal_new ("new-frame",
+                  G_TYPE_FROM_CLASS (object_class),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (GtdTimelineClass, new_frame),
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  1, G_TYPE_INT);
+  /**
+   * GtdTimeline::completed:
+   * @timeline: the #GtdTimeline which received the signal
+   *
+   * The #GtdTimeline::completed signal is emitted when the timeline's
+   * elapsed time reaches the value of the #GtdTimeline:duration
+   * property.
+   *
+   * This signal will be emitted even if the #GtdTimeline is set to be
+   * repeating.
+   *
+   * If you want to get notification on whether the #GtdTimeline has
+   * been stopped or has finished its run, including its eventual repeats,
+   * you should use the #GtdTimeline::stopped signal instead.
+   */
+  timeline_signals[COMPLETED] =
+    g_signal_new ("completed",
+                  G_TYPE_FROM_CLASS (object_class),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (GtdTimelineClass, completed),
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 0);
+  /**
+   * GtdTimeline::started:
+   * @timeline: the #GtdTimeline which received the signal
+   *
+   * The ::started signal is emitted when the timeline starts its run.
+   * This might be as soon as gtd_timeline_start() is invoked or
+   * after the delay set in the GtdTimeline:delay property has
+   * expired.
+   */
+  timeline_signals[STARTED] =
+    g_signal_new ("started",
+                  G_TYPE_FROM_CLASS (object_class),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (GtdTimelineClass, started),
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 0);
+  /**
+   * GtdTimeline::paused:
+   * @timeline: the #GtdTimeline which received the signal
+   *
+   * The ::paused signal is emitted when gtd_timeline_pause() is invoked.
+   */
+  timeline_signals[PAUSED] =
+    g_signal_new ("paused",
+                  G_TYPE_FROM_CLASS (object_class),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (GtdTimelineClass, paused),
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 0);
+  /**
+   * GtdTimeline::marker-reached:
+   * @timeline: the #GtdTimeline which received the signal
+   * @marker_name: the name of the marker reached
+   * @msecs: the elapsed time
+   *
+   * The ::marker-reached signal is emitted each time a timeline
+   * reaches a marker set with
+   * gtd_timeline_add_marker_at_time(). This signal is detailed
+   * with the name of the marker as well, so it is possible to connect
+   * a callback to the ::marker-reached signal for a specific marker
+   * with:
+   *
+   * <informalexample><programlisting>
+   *   gtd_timeline_add_marker_at_time (self, "foo", 500);
+   *   gtd_timeline_add_marker_at_time (self, "bar", 750);
+   *
+   *   g_signal_connect (self, "marker-reached",
+   *                     G_CALLBACK (each_marker_reached), NULL);
+   *   g_signal_connect (self, "marker-reached::foo",
+   *                     G_CALLBACK (foo_marker_reached), NULL);
+   *   g_signal_connect (self, "marker-reached::bar",
+   *                     G_CALLBACK (bar_marker_reached), NULL);
+   * </programlisting></informalexample>
+   *
+   * In the example, the first callback will be invoked for both
+   * the "foo" and "bar" marker, while the second and third callbacks
+   * will be invoked for the "foo" or "bar" markers, respectively.
+   *
+   * Since: 0.8
+   */
+  timeline_signals[MARKER_REACHED] =
+    g_signal_new ("marker-reached",
+                  G_TYPE_FROM_CLASS (object_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE |
+                  G_SIGNAL_DETAILED | G_SIGNAL_NO_HOOKS,
+                  G_STRUCT_OFFSET (GtdTimelineClass, marker_reached),
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  2,
+                  G_TYPE_STRING,
+                  G_TYPE_INT);
+  /**
+   * GtdTimeline::stopped:
+   * @timeline: the #GtdTimeline that emitted the signal
+   * @is_finished: %TRUE if the signal was emitted at the end of the
+   *   timeline.
+   *
+   * The #GtdTimeline::stopped signal is emitted when the timeline
+   * has been stopped, either because gtd_timeline_stop() has been
+   * called, or because it has been exhausted.
+   *
+   * This is different from the #GtdTimeline::completed signal,
+   * which gets emitted after every repeat finishes.
+   *
+   * If the #GtdTimeline has is marked as infinitely repeating,
+   * this signal will never be emitted.
+   *
+   * Since: 1.12
+   */
+  timeline_signals[STOPPED] =
+    g_signal_new ("stopped",
+                  G_TYPE_FROM_CLASS (object_class),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (GtdTimelineClass, stopped),
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  1,
+                  G_TYPE_BOOLEAN);
+}
+
+static void
+gtd_timeline_init (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  priv->progress_mode = GTD_EASE_LINEAR;
+
+  /* default steps() parameters are 1, end */
+  priv->n_steps = 1;
+  priv->step_mode = GTD_STEP_MODE_END;
+
+  /* default cubic-bezier() paramereters are (0, 0, 1, 1) */
+  graphene_point_init (&priv->cb_1, 0, 0);
+  graphene_point_init (&priv->cb_2, 1, 1);
+}
+
+static gboolean
+delay_timeout_func (gpointer data)
+{
+  GtdTimeline *self = data;
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  priv->delay_id = 0;
+  priv->msecs_delta = 0;
+  set_is_playing (self, TRUE);
+
+  g_signal_emit (self, timeline_signals[STARTED], 0);
+
+  return FALSE;
+}
+
+/**
+ * gtd_timeline_start:
+ * @timeline: A #GtdTimeline
+ *
+ * Starts the #GtdTimeline playing.
+ **/
+void
+gtd_timeline_start (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  if (priv->delay_id || priv->is_playing)
+    return;
+
+  if (priv->duration == 0)
+    return;
+
+  g_warn_if_fail ((priv->widget &&
+                   gtk_widget_get_mapped (GTK_WIDGET (priv->widget))) ||
+                  priv->frame_clock);
+
+  if (priv->delay)
+    {
+      priv->delay_id = g_timeout_add (priv->delay, delay_timeout_func, self);
+    }
+  else
+    {
+      priv->msecs_delta = 0;
+      set_is_playing (self, TRUE);
+
+      g_signal_emit (self, timeline_signals[STARTED], 0);
+    }
+}
+
+/**
+ * gtd_timeline_pause:
+ * @timeline: A #GtdTimeline
+ *
+ * Pauses the #GtdTimeline on current frame
+ **/
+void
+gtd_timeline_pause (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  gtd_timeline_cancel_delay (self);
+
+  if (!priv->is_playing)
+    return;
+
+  priv->msecs_delta = 0;
+  set_is_playing (self, FALSE);
+
+  g_signal_emit (self, timeline_signals[PAUSED], 0);
+}
+
+/**
+ * gtd_timeline_stop:
+ * @timeline: A #GtdTimeline
+ *
+ * Stops the #GtdTimeline and moves to frame 0
+ **/
+void
+gtd_timeline_stop (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv;
+  gboolean was_playing;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  /* we check the is_playing here because pause() will return immediately
+   * if the timeline wasn't playing, so we don't know if it was actually
+   * stopped, and yet we still don't want to emit a ::stopped signal if
+   * the timeline was not playing in the first place.
+   */
+  was_playing = priv->is_playing;
+
+  gtd_timeline_pause (self);
+  gtd_timeline_rewind (self);
+
+  if (was_playing)
+    g_signal_emit (self, timeline_signals[STOPPED], 0, FALSE);
+}
+
+/**
+ * gtd_timeline_rewind:
+ * @timeline: A #GtdTimeline
+ *
+ * Rewinds #GtdTimeline to the first frame if its direction is
+ * %GTD_TIMELINE_FORWARD and the last frame if it is
+ * %GTD_TIMELINE_BACKWARD.
+ */
+void
+gtd_timeline_rewind (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  if (priv->direction == GTD_TIMELINE_FORWARD)
+    gtd_timeline_advance (self, 0);
+  else if (priv->direction == GTD_TIMELINE_BACKWARD)
+    gtd_timeline_advance (self, priv->duration);
+}
+
+/**
+ * gtd_timeline_skip:
+ * @timeline: A #GtdTimeline
+ * @msecs: Amount of time to skip
+ *
+ * Advance timeline by the requested time in milliseconds
+ */
+void
+gtd_timeline_skip (GtdTimeline *self,
+                   guint        msecs)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  if (priv->direction == GTD_TIMELINE_FORWARD)
+    {
+      priv->elapsed_time += msecs;
+
+      if (priv->elapsed_time > priv->duration)
+        priv->elapsed_time = 1;
+    }
+  else if (priv->direction == GTD_TIMELINE_BACKWARD)
+    {
+      priv->elapsed_time -= msecs;
+
+      if (priv->elapsed_time < 1)
+        priv->elapsed_time = priv->duration - 1;
+    }
+
+  priv->msecs_delta = 0;
+}
+
+/**
+ * gtd_timeline_advance:
+ * @timeline: A #GtdTimeline
+ * @msecs: Time to advance to
+ *
+ * Advance timeline to the requested point. The point is given as a
+ * time in milliseconds since the timeline started.
+ *
+ * The @timeline will not emit the #GtdTimeline::new-frame
+ * signal for the given time. The first ::new-frame signal after the call to
+ * gtd_timeline_advance() will be emit the skipped markers.
+ */
+void
+gtd_timeline_advance (GtdTimeline *self,
+                      guint        msecs)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  priv->elapsed_time = CLAMP (msecs, 0, priv->duration);
+}
+
+/**
+ * gtd_timeline_get_elapsed_time:
+ * @timeline: A #GtdTimeline
+ *
+ * Request the current time position of the timeline.
+ *
+ * Return value: current elapsed time in milliseconds.
+ */
+guint
+gtd_timeline_get_elapsed_time (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+  priv = gtd_timeline_get_instance_private (self);
+  return priv->elapsed_time;
+}
+
+/**
+ * gtd_timeline_is_playing:
+ * @timeline: A #GtdTimeline
+ *
+ * Queries state of a #GtdTimeline.
+ *
+ * Return value: %TRUE if timeline is currently playing
+ */
+gboolean
+gtd_timeline_is_playing (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_TIMELINE (self), FALSE);
+
+  priv = gtd_timeline_get_instance_private (self);
+  return priv->is_playing;
+}
+
+/**
+ * gtd_timeline_new:
+ * @duration_ms: Duration of the timeline in milliseconds
+ *
+ * Creates a new #GtdTimeline with a duration of @duration_ms milli seconds.
+ *
+ * Return value: the newly created #GtdTimeline instance. Use
+ *   g_object_unref() when done using it
+ *
+ * Since: 0.6
+ */
+GtdTimeline *
+gtd_timeline_new (guint duration_ms)
+{
+  return g_object_new (GTD_TYPE_TIMELINE,
+                       "duration", duration_ms,
+                       NULL);
+}
+
+/**
+ * gtd_timeline_new_for_widget:
+ * @widget: The #GtdWidget the timeline is associated with
+ * @duration_ms: Duration of the timeline in milliseconds
+ *
+ * Creates a new #GtdTimeline with a duration of @duration milli seconds.
+ *
+ * Return value: the newly created #GtdTimeline instance. Use
+ *   g_object_unref() when done using it
+ */
+GtdTimeline *
+gtd_timeline_new_for_widget (GtdWidget *widget,
+                             guint      duration_ms)
+{
+  return g_object_new (GTD_TYPE_TIMELINE,
+                       "duration", duration_ms,
+                       "widget", widget,
+                       NULL);
+}
+
+/**
+ * gtd_timeline_new_for_frame_clock:
+ * @frame_clock: The #GdkFrameClock the timeline is driven by
+ * @duration_ms: Duration of the timeline in milliseconds
+ *
+ * Creates a new #GtdTimeline with a duration of @duration_ms milli seconds.
+ *
+ * Return value: the newly created #GtdTimeline instance. Use
+ *   g_object_unref() when done using it
+ */
+GtdTimeline *
+gtd_timeline_new_for_frame_clock (GdkFrameClock *frame_clock,
+                                  guint          duration_ms)
+{
+  return g_object_new (GTD_TYPE_TIMELINE,
+                       "duration", duration_ms,
+                       "frame-clock", frame_clock,
+                       NULL);
+}
+
+/**
+ * gtd_timeline_get_delay:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the delay set using gtd_timeline_set_delay().
+ *
+ * Return value: the delay in milliseconds.
+ *
+ * Since: 0.4
+ */
+guint
+gtd_timeline_get_delay (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+  priv = gtd_timeline_get_instance_private (self);
+  return priv->delay;
+}
+
+/**
+ * gtd_timeline_set_delay:
+ * @timeline: a #GtdTimeline
+ * @msecs: delay in milliseconds
+ *
+ * Sets the delay, in milliseconds, before @timeline should start.
+ *
+ * Since: 0.4
+ */
+void
+gtd_timeline_set_delay (GtdTimeline *self,
+                        guint        msecs)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  if (priv->delay != msecs)
+    {
+      priv->delay = msecs;
+      g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_DELAY]);
+    }
+}
+
+/**
+ * gtd_timeline_get_duration:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the duration of a #GtdTimeline in milliseconds.
+ * See gtd_timeline_set_duration().
+ *
+ * Return value: the duration of the timeline, in milliseconds.
+ *
+ * Since: 0.6
+ */
+guint
+gtd_timeline_get_duration (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  return priv->duration;
+}
+
+/**
+ * gtd_timeline_set_duration:
+ * @timeline: a #GtdTimeline
+ * @msecs: duration of the timeline in milliseconds
+ *
+ * Sets the duration of the timeline, in milliseconds. The speed
+ * of the timeline depends on the GtdTimeline:fps setting.
+ *
+ * Since: 0.6
+ */
+void
+gtd_timeline_set_duration (GtdTimeline *self,
+                           guint        msecs)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+  g_return_if_fail (msecs > 0);
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  if (priv->duration != msecs)
+    {
+      priv->duration = msecs;
+
+      g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_DURATION]);
+    }
+}
+
+/**
+ * gtd_timeline_get_progress:
+ * @timeline: a #GtdTimeline
+ *
+ * The position of the timeline in a normalized [-1, 2] interval.
+ *
+ * The return value of this function is determined by the progress
+ * mode set using gtd_timeline_set_progress_mode(), or by the
+ * progress function set using gtd_timeline_set_progress_func().
+ *
+ * Return value: the normalized current position in the timeline.
+ *
+ * Since: 0.6
+ */
+gdouble
+gtd_timeline_get_progress (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_TIMELINE (self), 0.0);
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  /* short-circuit linear progress */
+  if (priv->progress_func == NULL)
+    return (gdouble) priv->elapsed_time / (gdouble) priv->duration;
+  else
+    return priv->progress_func (self,
+                                (gdouble) priv->elapsed_time,
+                                (gdouble) priv->duration,
+                                priv->progress_data);
+}
+
+/**
+ * gtd_timeline_get_direction:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the direction of the timeline set with
+ * gtd_timeline_set_direction().
+ *
+ * Return value: the direction of the timeline
+ *
+ * Since: 0.6
+ */
+GtdTimelineDirection
+gtd_timeline_get_direction (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_TIMELINE (self), GTD_TIMELINE_FORWARD);
+
+  priv = gtd_timeline_get_instance_private (self);
+  return priv->direction;
+}
+
+/**
+ * gtd_timeline_set_direction:
+ * @timeline: a #GtdTimeline
+ * @direction: the direction of the timeline
+ *
+ * Sets the direction of @timeline, either %GTD_TIMELINE_FORWARD or
+ * %GTD_TIMELINE_BACKWARD.
+ *
+ * Since: 0.6
+ */
+void
+gtd_timeline_set_direction (GtdTimeline          *self,
+                            GtdTimelineDirection  direction)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  if (priv->direction != direction)
+    {
+      priv->direction = direction;
+
+      if (priv->elapsed_time == 0)
+        priv->elapsed_time = priv->duration;
+
+      g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_DIRECTION]);
+    }
+}
+
+/**
+ * gtd_timeline_get_delta:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the amount of time elapsed since the last
+ * GtdTimeline::new-frame signal.
+ *
+ * This function is only useful inside handlers for the ::new-frame
+ * signal, and its behaviour is undefined if the timeline is not
+ * playing.
+ *
+ * Return value: the amount of time in milliseconds elapsed since the
+ * last frame
+ *
+ * Since: 0.6
+ */
+guint
+gtd_timeline_get_delta (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+  if (!gtd_timeline_is_playing (self))
+    return 0;
+
+  priv = gtd_timeline_get_instance_private (self);
+  return priv->msecs_delta;
+}
+
+/**
+ * gtd_timeline_add_marker:
+ * @timeline: a #GtdTimeline
+ * @marker_name: the unique name for this marker
+ * @progress: the normalized value of the position of the martke
+ *
+ * Adds a named marker that will be hit when the timeline has reached
+ * the specified @progress.
+ *
+ * Markers are unique string identifiers for a given position on the
+ * timeline. Once @timeline reaches the given @progress of its duration,
+ * if will emit a ::marker-reached signal for each marker attached to
+ * that particular point.
+ *
+ * A marker can be removed with gtd_timeline_remove_marker(). The
+ * timeline can be advanced to a marker using
+ * gtd_timeline_advance_to_marker().
+ *
+ * See also: gtd_timeline_add_marker_at_time()
+ *
+ * Since: 1.14
+ */
+void
+gtd_timeline_add_marker (GtdTimeline *self,
+                         const gchar *marker_name,
+                         gdouble      progress)
+{
+  TimelineMarker *marker;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+  g_return_if_fail (marker_name != NULL);
+
+  marker = timeline_marker_new_progress (marker_name, progress);
+  gtd_timeline_add_marker_internal (self, marker);
+}
+
+/**
+ * gtd_timeline_add_marker_at_time:
+ * @timeline: a #GtdTimeline
+ * @marker_name: the unique name for this marker
+ * @msecs: position of the marker in milliseconds
+ *
+ * Adds a named marker that will be hit when the timeline has been
+ * running for @msecs milliseconds.
+ *
+ * Markers are unique string identifiers for a given position on the
+ * timeline. Once @timeline reaches the given @msecs, it will emit
+ * a ::marker-reached signal for each marker attached to that position.
+ *
+ * A marker can be removed with gtd_timeline_remove_marker(). The
+ * timeline can be advanced to a marker using
+ * gtd_timeline_advance_to_marker().
+ *
+ * See also: gtd_timeline_add_marker()
+ *
+ * Since: 0.8
+ */
+void
+gtd_timeline_add_marker_at_time (GtdTimeline *self,
+                                 const gchar *marker_name,
+                                 guint        msecs)
+{
+  TimelineMarker *marker;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+  g_return_if_fail (marker_name != NULL);
+  g_return_if_fail (msecs <= gtd_timeline_get_duration (self));
+
+  marker = timeline_marker_new_time (marker_name, msecs);
+  gtd_timeline_add_marker_internal (self, marker);
+}
+
+struct CollectMarkersClosure
+{
+  guint duration;
+  guint msecs;
+  GArray *markers;
+};
+
+static void
+collect_markers (const gchar *key,
+                 TimelineMarker *marker,
+                 struct CollectMarkersClosure *data)
+{
+  guint msecs;
+
+  if (marker->is_relative)
+    msecs = marker->data.progress * data->duration;
+  else
+    msecs = marker->data.msecs;
+
+  if (msecs == data->msecs)
+    {
+      gchar *name_copy = g_strdup (key);
+      g_array_append_val (data->markers, name_copy);
+    }
+}
+
+/**
+ * gtd_timeline_list_markers:
+ * @timeline: a #GtdTimeline
+ * @msecs: the time to check, or -1
+ * @n_markers: the number of markers returned
+ *
+ * Retrieves the list of markers at time @msecs. If @msecs is a
+ * negative integer, all the markers attached to @timeline will be
+ * returned.
+ *
+ * Return value: (transfer full) (array zero-terminated=1 length=n_markers):
+ *   a newly allocated, %NULL terminated string array containing the names
+ *   of the markers. Use g_strfreev() when done.
+ *
+ * Since: 0.8
+ */
+gchar **
+gtd_timeline_list_markers (GtdTimeline *self,
+                           gint         msecs,
+                           gsize       *n_markers)
+{
+  GtdTimelinePrivate *priv;
+  gchar **retval = NULL;
+  gsize i;
+
+  g_return_val_if_fail (GTD_IS_TIMELINE (self), NULL);
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  if (G_UNLIKELY (priv->markers_by_name == NULL))
+    {
+      if (n_markers)
+        *n_markers = 0;
+
+      return NULL;
+    }
+
+  if (msecs < 0)
+    {
+      GList *markers, *l;
+
+      markers = g_hash_table_get_keys (priv->markers_by_name);
+      retval = g_new0 (gchar*, g_list_length (markers) + 1);
+
+      for (i = 0, l = markers; l != NULL; i++, l = l->next)
+        retval[i] = g_strdup (l->data);
+
+      g_list_free (markers);
+    }
+  else
+    {
+      struct CollectMarkersClosure data;
+
+      data.duration = priv->duration;
+      data.msecs = msecs;
+      data.markers = g_array_new (TRUE, FALSE, sizeof (gchar *));
+
+      g_hash_table_foreach (priv->markers_by_name,
+                            (GHFunc) collect_markers,
+                            &data);
+
+      i = data.markers->len;
+      retval = (gchar **) (void *) g_array_free (data.markers, FALSE);
+    }
+
+  if (n_markers)
+    *n_markers = i;
+
+  return retval;
+}
+
+/**
+ * gtd_timeline_advance_to_marker:
+ * @timeline: a #GtdTimeline
+ * @marker_name: the name of the marker
+ *
+ * Advances @timeline to the time of the given @marker_name.
+ *
+ * Like gtd_timeline_advance(), this function will not
+ * emit the #GtdTimeline::new-frame for the time where @marker_name
+ * is set, nor it will emit #GtdTimeline::marker-reached for
+ * @marker_name.
+ *
+ * Since: 0.8
+ */
+void
+gtd_timeline_advance_to_marker (GtdTimeline *self,
+                                const gchar *marker_name)
+{
+  GtdTimelinePrivate *priv;
+  TimelineMarker *marker;
+  guint msecs;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+  g_return_if_fail (marker_name != NULL);
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  if (G_UNLIKELY (priv->markers_by_name == NULL))
+    {
+      g_warning ("No marker named '%s' found.", marker_name);
+      return;
+    }
+
+  marker = g_hash_table_lookup (priv->markers_by_name, marker_name);
+  if (marker == NULL)
+    {
+      g_warning ("No marker named '%s' found.", marker_name);
+      return;
+    }
+
+  if (marker->is_relative)
+    msecs = marker->data.progress * priv->duration;
+  else
+    msecs = marker->data.msecs;
+
+  gtd_timeline_advance (self, msecs);
+}
+
+/**
+ * gtd_timeline_remove_marker:
+ * @timeline: a #GtdTimeline
+ * @marker_name: the name of the marker to remove
+ *
+ * Removes @marker_name, if found, from @timeline.
+ *
+ * Since: 0.8
+ */
+void
+gtd_timeline_remove_marker (GtdTimeline *self,
+                            const gchar *marker_name)
+{
+  GtdTimelinePrivate *priv;
+  TimelineMarker *marker;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+  g_return_if_fail (marker_name != NULL);
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  if (G_UNLIKELY (priv->markers_by_name == NULL))
+    {
+      g_warning ("No marker named '%s' found.", marker_name);
+      return;
+    }
+
+  marker = g_hash_table_lookup (priv->markers_by_name, marker_name);
+  if (!marker)
+    {
+      g_warning ("No marker named '%s' found.", marker_name);
+      return;
+    }
+
+  /* this will take care of freeing the marker as well */
+  g_hash_table_remove (priv->markers_by_name, marker_name);
+}
+
+/**
+ * gtd_timeline_has_marker:
+ * @timeline: a #GtdTimeline
+ * @marker_name: the name of the marker
+ *
+ * Checks whether @timeline has a marker set with the given name.
+ *
+ * Return value: %TRUE if the marker was found
+ *
+ * Since: 0.8
+ */
+gboolean
+gtd_timeline_has_marker (GtdTimeline *self,
+                         const gchar *marker_name)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_TIMELINE (self), FALSE);
+  g_return_val_if_fail (marker_name != NULL, FALSE);
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  if (G_UNLIKELY (priv->markers_by_name == NULL))
+    return FALSE;
+
+  return g_hash_table_lookup (priv->markers_by_name, marker_name) != NULL;
+}
+
+/**
+ * gtd_timeline_set_auto_reverse:
+ * @timeline: a #GtdTimeline
+ * @reverse: %TRUE if the @timeline should reverse the direction
+ *
+ * Sets whether @timeline should reverse the direction after the
+ * emission of the #GtdTimeline::completed signal.
+ *
+ * Setting the #GtdTimeline:auto-reverse property to %TRUE is the
+ * equivalent of connecting a callback to the #GtdTimeline::completed
+ * signal and changing the direction of the timeline from that callback;
+ * for instance, this code:
+ *
+ * |[
+ * static void
+ * reverse_timeline (GtdTimeline *self)
+ * {
+ *   GtdTimelineDirection dir = gtd_timeline_get_direction (self);
+ *
+ *   if (dir == GTD_TIMELINE_FORWARD)
+ *     dir = GTD_TIMELINE_BACKWARD;
+ *   else
+ *     dir = GTD_TIMELINE_FORWARD;
+ *
+ *   gtd_timeline_set_direction (self, dir);
+ * }
+ * ...
+ *   timeline = gtd_timeline_new (1000);
+ *   gtd_timeline_set_repeat_count (self, -1);
+ *   g_signal_connect (self, "completed",
+ *                     G_CALLBACK (reverse_timeline),
+ *                     NULL);
+ * ]|
+ *
+ * can be effectively replaced by:
+ *
+ * |[
+ *   timeline = gtd_timeline_new (1000);
+ *   gtd_timeline_set_repeat_count (self, -1);
+ *   gtd_timeline_set_auto_reverse (self);
+ * ]|
+ *
+ * Since: 1.6
+ */
+void
+gtd_timeline_set_auto_reverse (GtdTimeline *self,
+                               gboolean     reverse)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+
+  reverse = !!reverse;
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  if (priv->auto_reverse != reverse)
+    {
+      priv->auto_reverse = reverse;
+      g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_AUTO_REVERSE]);
+    }
+}
+
+/**
+ * gtd_timeline_get_auto_reverse:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the value set by gtd_timeline_set_auto_reverse().
+ *
+ * Return value: %TRUE if the timeline should automatically reverse, and
+ *   %FALSE otherwise
+ *
+ * Since: 1.6
+ */
+gboolean
+gtd_timeline_get_auto_reverse (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_TIMELINE (self), FALSE);
+
+  priv = gtd_timeline_get_instance_private (self);
+  return priv->auto_reverse;
+}
+
+/**
+ * gtd_timeline_set_repeat_count:
+ * @timeline: a #GtdTimeline
+ * @count: the number of times the timeline should repeat
+ *
+ * Sets the number of times the @timeline should repeat.
+ *
+ * If @count is 0, the timeline never repeats.
+ *
+ * If @count is -1, the timeline will always repeat until
+ * it's stopped.
+ *
+ * Since: 1.10
+ */
+void
+gtd_timeline_set_repeat_count (GtdTimeline *self,
+                               gint         count)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+  g_return_if_fail (count >= -1);
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  if (priv->repeat_count != count)
+    {
+      priv->repeat_count = count;
+      g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_REPEAT_COUNT]);
+    }
+}
+
+/**
+ * gtd_timeline_get_repeat_count:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the number set using gtd_timeline_set_repeat_count().
+ *
+ * Return value: the number of repeats
+ *
+ * Since: 1.10
+ */
+gint
+gtd_timeline_get_repeat_count (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+  priv = gtd_timeline_get_instance_private (self);
+  return priv->repeat_count;
+}
+
+/**
+ * gtd_timeline_set_progress_func:
+ * @timeline: a #GtdTimeline
+ * @func: (scope notified) (allow-none): a progress function, or %NULL
+ * @data: (closure): data to pass to @func
+ * @notify: a function to be called when the progress function is removed
+ *    or the timeline is disposed
+ *
+ * Sets a custom progress function for @timeline. The progress function will
+ * be called by gtd_timeline_get_progress() and will be used to compute
+ * the progress value based on the elapsed time and the total duration of the
+ * timeline.
+ *
+ * If @func is not %NULL, the #GtdTimeline:progress-mode property will
+ * be set to %GTD_CUSTOM_MODE.
+ *
+ * If @func is %NULL, any previously set progress function will be unset, and
+ * the #GtdTimeline:progress-mode property will be set to %GTD_EASE_LINEAR.
+ *
+ * Since: 1.10
+ */
+void
+gtd_timeline_set_progress_func (GtdTimeline             *self,
+                                GtdTimelineProgressFunc  func,
+                                gpointer                 data,
+                                GDestroyNotify           notify)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  if (priv->progress_notify != NULL)
+    priv->progress_notify (priv->progress_data);
+
+  priv->progress_func = func;
+  priv->progress_data = data;
+  priv->progress_notify = notify;
+
+  if (priv->progress_func != NULL)
+    priv->progress_mode = GTD_CUSTOM_MODE;
+  else
+    priv->progress_mode = GTD_EASE_LINEAR;
+
+  g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_PROGRESS_MODE]);
+}
+
+static gdouble
+gtd_timeline_progress_func (GtdTimeline *self,
+                            gdouble      elapsed,
+                            gdouble      duration,
+                            gpointer     user_data)
+{
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  /* parametrized easing functions need to be handled separately */
+  switch (priv->progress_mode)
+    {
+    case GTD_STEPS:
+      if (priv->step_mode == GTD_STEP_MODE_START)
+        return gtd_ease_steps_start (elapsed, duration, priv->n_steps);
+      else if (priv->step_mode == GTD_STEP_MODE_END)
+        return gtd_ease_steps_end (elapsed, duration, priv->n_steps);
+      else
+        g_assert_not_reached ();
+      break;
+
+    case GTD_STEP_START:
+      return gtd_ease_steps_start (elapsed, duration, 1);
+
+    case GTD_STEP_END:
+      return gtd_ease_steps_end (elapsed, duration, 1);
+
+    case GTD_EASE_CUBIC_BEZIER:
+      return gtd_ease_cubic_bezier (elapsed, duration,
+                                    priv->cb_1.x, priv->cb_1.y,
+                                    priv->cb_2.x, priv->cb_2.y);
+
+    case GTD_EASE:
+      return gtd_ease_cubic_bezier (elapsed, duration,
+                                    0.25, 0.1, 0.25, 1.0);
+
+    case GTD_EASE_IN:
+      return gtd_ease_cubic_bezier (elapsed,
+                                    duration,
+                                    0.42, 0.0, 1.0, 1.0);
+
+    case GTD_EASE_OUT:
+      return gtd_ease_cubic_bezier (elapsed, duration,
+                                    0.0, 0.0, 0.58, 1.0);
+
+    case GTD_EASE_IN_OUT:
+      return gtd_ease_cubic_bezier (elapsed, duration,
+                                    0.42, 0.0, 0.58, 1.0);
+
+    default:
+      break;
+    }
+
+  return gtd_easing_for_mode (priv->progress_mode, elapsed, duration);
+}
+
+/**
+ * gtd_timeline_set_progress_mode:
+ * @timeline: a #GtdTimeline
+ * @mode: the progress mode, as a #GtdEaseMode
+ *
+ * Sets the progress function using a value from the #GtdEaseMode
+ * enumeration. The @mode cannot be %GTD_CUSTOM_MODE or bigger than
+ * %GTD_ANIMATION_LAST.
+ *
+ * Since: 1.10
+ */
+void
+gtd_timeline_set_progress_mode (GtdTimeline *self,
+                                GtdEaseMode  mode)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+  g_return_if_fail (mode < GTD_EASE_LAST);
+  g_return_if_fail (mode != GTD_CUSTOM_MODE);
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  if (priv->progress_mode == mode)
+    return;
+
+  if (priv->progress_notify != NULL)
+    priv->progress_notify (priv->progress_data);
+
+  priv->progress_mode = mode;
+
+  /* short-circuit linear progress */
+  if (priv->progress_mode != GTD_EASE_LINEAR)
+    priv->progress_func = gtd_timeline_progress_func;
+  else
+    priv->progress_func = NULL;
+
+  priv->progress_data = NULL;
+  priv->progress_notify = NULL;
+
+  g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_PROGRESS_MODE]);
+}
+
+/**
+ * gtd_timeline_get_progress_mode:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the progress mode set using gtd_timeline_set_progress_mode()
+ * or gtd_timeline_set_progress_func().
+ *
+ * Return value: a #GtdEaseMode
+ *
+ * Since: 1.10
+ */
+GtdEaseMode
+gtd_timeline_get_progress_mode (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_TIMELINE (self), GTD_EASE_LINEAR);
+
+  priv = gtd_timeline_get_instance_private (self);
+  return priv->progress_mode;
+}
+
+/**
+ * gtd_timeline_get_duration_hint:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the full duration of the @timeline, taking into account the
+ * current value of the #GtdTimeline:repeat-count property.
+ *
+ * If the #GtdTimeline:repeat-count property is set to -1, this function
+ * will return %G_MAXINT64.
+ *
+ * The returned value is to be considered a hint, and it's only valid
+ * as long as the @timeline hasn't been changed.
+ *
+ * Return value: the full duration of the #GtdTimeline
+ *
+ * Since: 1.10
+ */
+gint64
+gtd_timeline_get_duration_hint (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  if (priv->repeat_count == 0)
+    return priv->duration;
+  else if (priv->repeat_count < 0)
+    return G_MAXINT64;
+  else
+    return priv->repeat_count * priv->duration;
+}
+
+/**
+ * gtd_timeline_get_current_repeat:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the current repeat for a timeline.
+ *
+ * Repeats start at 0.
+ *
+ * Return value: the current repeat
+ *
+ * Since: 1.10
+ */
+gint
+gtd_timeline_get_current_repeat (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+  priv = gtd_timeline_get_instance_private (self);
+  return priv->current_repeat;
+}
+
+/**
+ * gtd_timeline_set_step_progress:
+ * @timeline: a #GtdTimeline
+ * @n_steps: the number of steps
+ * @step_mode: whether the change should happen at the start
+ *   or at the end of the step
+ *
+ * Sets the #GtdTimeline:progress-mode of the @timeline to %GTD_STEPS
+ * and provides the parameters of the step function.
+ *
+ * Since: 1.12
+ */
+void
+gtd_timeline_set_step_progress (GtdTimeline *self,
+                                gint         n_steps,
+                                GtdStepMode  step_mode)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+  g_return_if_fail (n_steps > 0);
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  if (priv->progress_mode == GTD_STEPS &&
+      priv->n_steps == n_steps &&
+      priv->step_mode == step_mode)
+    return;
+
+  priv->n_steps = n_steps;
+  priv->step_mode = step_mode;
+  gtd_timeline_set_progress_mode (self, GTD_STEPS);
+}
+
+/**
+ * gtd_timeline_get_step_progress:
+ * @timeline: a #GtdTimeline
+ * @n_steps: (out): return location for the number of steps, or %NULL
+ * @step_mode: (out): return location for the value change policy,
+ *   or %NULL
+ *
+ * Retrieves the parameters of the step progress mode used by @timeline.
+ *
+ * Return value: %TRUE if the @timeline is using a step progress
+ *   mode, and %FALSE otherwise
+ *
+ * Since: 1.12
+ */
+gboolean
+gtd_timeline_get_step_progress (GtdTimeline *self,
+                                gint        *n_steps,
+                                GtdStepMode *step_mode)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_TIMELINE (self), FALSE);
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  if (!(priv->progress_mode == GTD_STEPS ||
+        priv->progress_mode == GTD_STEP_START ||
+        priv->progress_mode == GTD_STEP_END))
+    return FALSE;
+
+  if (n_steps != NULL)
+    *n_steps = priv->n_steps;
+
+  if (step_mode != NULL)
+    *step_mode = priv->step_mode;
+
+  return TRUE;
+}
+
+/**
+ * gtd_timeline_set_cubic_bezier_progress:
+ * @timeline: a #GtdTimeline
+ * @c_1: the first control point for the cubic bezier
+ * @c_2: the second control point for the cubic bezier
+ *
+ * Sets the #GtdTimeline:progress-mode of @timeline
+ * to %GTD_CUBIC_BEZIER, and sets the two control
+ * points for the cubic bezier.
+ *
+ * The cubic bezier curve is between (0, 0) and (1, 1). The X coordinate
+ * of the two control points must be in the [ 0, 1 ] range, while the
+ * Y coordinate of the two control points can exceed this range.
+ *
+ * Since: 1.12
+ */
+void
+gtd_timeline_set_cubic_bezier_progress (GtdTimeline            *self,
+                                        const graphene_point_t *c_1,
+                                        const graphene_point_t *c_2)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+  g_return_if_fail (c_1 != NULL && c_2 != NULL);
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  priv->cb_1 = *c_1;
+  priv->cb_2 = *c_2;
+
+  /* ensure the range on the X coordinate */
+  priv->cb_1.x = CLAMP (priv->cb_1.x, 0.f, 1.f);
+  priv->cb_2.x = CLAMP (priv->cb_2.x, 0.f, 1.f);
+
+  gtd_timeline_set_progress_mode (self, GTD_EASE_CUBIC_BEZIER);
+}
+
+/**
+ * gtd_timeline_get_cubic_bezier_progress:
+ * @timeline: a #GtdTimeline
+ * @c_1: (out caller-allocates): return location for the first control
+ *   point of the cubic bezier, or %NULL
+ * @c_2: (out caller-allocates): return location for the second control
+ *   point of the cubic bezier, or %NULL
+ *
+ * Retrieves the control points for the cubic bezier progress mode.
+ *
+ * Return value: %TRUE if the @timeline is using a cubic bezier progress
+ *   more, and %FALSE otherwise
+ *
+ * Since: 1.12
+ */
+gboolean
+gtd_timeline_get_cubic_bezier_progress (GtdTimeline      *self,
+                                        graphene_point_t *c_1,
+                                        graphene_point_t *c_2)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_TIMELINE (self), FALSE);
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  if (!(priv->progress_mode == GTD_EASE_CUBIC_BEZIER ||
+        priv->progress_mode == GTD_EASE ||
+        priv->progress_mode == GTD_EASE_IN ||
+        priv->progress_mode == GTD_EASE_OUT ||
+        priv->progress_mode == GTD_EASE_IN_OUT))
+    return FALSE;
+
+  if (c_1 != NULL)
+    *c_1 = priv->cb_1;
+
+  if (c_2 != NULL)
+    *c_2 = priv->cb_2;
+
+  return TRUE;
+}
+
+void
+gtd_timeline_cancel_delay (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  g_clear_handle_id (&priv->delay_id, g_source_remove);
+}
+
+
+/**
+ * gtd_timeline_get_frame_clock: (skip)
+ */
+GdkFrameClock *
+gtd_timeline_get_frame_clock (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_TIMELINE (self), NULL);
+
+  priv = gtd_timeline_get_instance_private (self);
+  return priv->frame_clock;
+}
+
+void
+gtd_timeline_set_frame_clock (GtdTimeline   *self,
+                              GdkFrameClock *frame_clock)
+{
+  GtdTimelinePrivate *priv;
+
+  g_return_if_fail (GTD_IS_TIMELINE (self));
+
+  priv = gtd_timeline_get_instance_private (self);
+
+  g_assert (!frame_clock || (frame_clock && !priv->widget));
+  g_return_if_fail (!frame_clock || (frame_clock && !priv->widget));
+
+  priv->custom_frame_clock = frame_clock;
+  if (!priv->widget)
+    set_frame_clock_internal (self, frame_clock);
+}
+
+/**
+ * gtd_timeline_get_widget:
+ * @timeline: a #GtdTimeline
+ *
+ * Get the widget the timeline is associated with.
+ *
+ * Returns: (transfer none): the associated #GtdWidget
+ */
+GtdWidget *
+gtd_timeline_get_widget (GtdTimeline *self)
+{
+  GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+  return priv->widget;
+}
diff --git a/src/animation/gtd-timeline.h b/src/animation/gtd-timeline.h
new file mode 100644
index 0000000..2199f7a
--- /dev/null
+++ b/src/animation/gtd-timeline.h
@@ -0,0 +1,200 @@
+/* gtd-timeline.h
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * Heavily inspired by Clutter, authored By Matthew Allum  <mallum openedhand com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+#include "gtd-easing.h"
+#include "gtd-types.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_TIMELINE (gtd_timeline_get_type())
+G_DECLARE_DERIVABLE_TYPE (GtdTimeline, gtd_timeline, GTD, TIMELINE, GObject)
+
+/**
+ * GtdTimelineProgressFunc:
+ * @timeline: a #GtdTimeline
+ * @elapsed: the elapsed time, in milliseconds
+ * @total: the total duration of the timeline, in milliseconds,
+ * @user_data: data passed to the function
+ *
+ * A function for defining a custom progress.
+ *
+ * Return value: the progress, as a floating point value between -1.0 and 2.0.
+ */
+typedef gdouble (* GtdTimelineProgressFunc)     (GtdTimeline *timeline,
+                                                 gdouble      elapsed,
+                                                 gdouble      total,
+                                                 gpointer     user_data);
+
+/**
+ * GtdTimelineClass:
+ * @started: class handler for the #GtdTimeline::started signal
+ * @completed: class handler for the #GtdTimeline::completed signal
+ * @paused: class handler for the #GtdTimeline::paused signal
+ * @new_frame: class handler for the #GtdTimeline::new-frame signal
+ * @marker_reached: class handler for the #GtdTimeline::marker-reached signal
+ * @stopped: class handler for the #GtdTimeline::stopped signal
+ *
+ * The #GtdTimelineClass structure contains only private data
+ */
+struct _GtdTimelineClass
+{
+  /*< private >*/
+  GObjectClass        parent_class;
+
+  /*< public >*/
+  void               (*started)        (GtdTimeline *timeline);
+  void               (*completed)      (GtdTimeline *timeline);
+  void               (*paused)         (GtdTimeline *timeline);
+
+  void               (*new_frame)      (GtdTimeline *timeline,
+                                        gint             msecs);
+
+  void               (*marker_reached) (GtdTimeline *timeline,
+                                        const gchar     *marker_name,
+                                        gint             msecs);
+
+  void               (*stopped)        (GtdTimeline *timeline,
+                                        gboolean         is_finished);
+};
+
+GtdTimeline*         gtd_timeline_new_for_widget                 (GtdWidget            *widget,
+                                                                  guint                 duration_ms);
+
+GtdTimeline*         gtd_timeline_new_for_frame_clock            (GdkFrameClock        *frame_clock,
+                                                                  guint                 duration_ms);
+
+GtdWidget*           gtd_timeline_get_widget                     (GtdTimeline          *timeline);
+
+void                 gtd_timeline_set_widget                     (GtdTimeline          *timeline,
+                                                                  GtdWidget             *widget);
+
+guint                gtd_timeline_get_duration                   (GtdTimeline          *timeline);
+
+void                 gtd_timeline_set_duration                   (GtdTimeline          *timeline,
+                                                                  guint                 msecs);
+
+GtdTimelineDirection gtd_timeline_get_direction                  (GtdTimeline          *timeline);
+
+void                 gtd_timeline_set_direction                  (GtdTimeline          *timeline,
+                                                                  GtdTimelineDirection  direction);
+
+void                 gtd_timeline_start                          (GtdTimeline          *timeline);
+
+void                 gtd_timeline_pause                          (GtdTimeline          *timeline);
+
+void                 gtd_timeline_stop                           (GtdTimeline          *timeline);
+
+void                 gtd_timeline_set_auto_reverse               (GtdTimeline          *timeline,
+                                                                  gboolean              reverse);
+
+gboolean             gtd_timeline_get_auto_reverse               (GtdTimeline          *timeline);
+
+void                 gtd_timeline_set_repeat_count               (GtdTimeline          *timeline,
+                                                                  gint                  count);
+
+gint                 gtd_timeline_get_repeat_count               (GtdTimeline          *timeline);
+
+void                 gtd_timeline_rewind                         (GtdTimeline          *timeline);
+
+void                 gtd_timeline_skip                           (GtdTimeline          *timeline,
+                                                                  guint                 msecs);
+
+void                 gtd_timeline_advance                        (GtdTimeline          *timeline,
+                                                                  guint                 msecs);
+
+guint                gtd_timeline_get_elapsed_time               (GtdTimeline          *timeline);
+
+gdouble              gtd_timeline_get_progress                   (GtdTimeline          *timeline);
+
+gboolean             gtd_timeline_is_playing                     (GtdTimeline          *timeline);
+
+void                 gtd_timeline_set_delay                      (GtdTimeline          *timeline,
+                                                                  guint                 msecs);
+
+guint                gtd_timeline_get_delay                      (GtdTimeline          *timeline);
+
+guint                gtd_timeline_get_delta                      (GtdTimeline          *timeline);
+
+void                 gtd_timeline_add_marker                     (GtdTimeline          *timeline,
+                                                                  const gchar           *marker_name,
+                                                                  gdouble                progress);
+
+void                 gtd_timeline_add_marker_at_time             (GtdTimeline          *timeline,
+                                                                  const gchar          *marker_name,
+                                                                  guint                 msecs);
+
+void                 gtd_timeline_remove_marker                  (GtdTimeline          *timeline,
+                                                                  const gchar          *marker_name);
+
+gchar **             gtd_timeline_list_markers                   (GtdTimeline          *timeline,
+                                                                  gint                  msecs,
+                                                                  gsize                *n_markers);
+
+gboolean             gtd_timeline_has_marker                     (GtdTimeline          *timeline,
+                                                                  const gchar          *marker_name);
+
+void                 gtd_timeline_advance_to_marker              (GtdTimeline          *timeline,
+                                                                  const gchar          *marker_name);
+
+void                 gtd_timeline_set_progress_func              (GtdTimeline          *timeline,
+                                                                  GtdTimelineProgressFunc func,
+                                                                  gpointer                data,
+                                                                  GDestroyNotify          notify);
+
+void                 gtd_timeline_set_progress_mode              (GtdTimeline          *timeline,
+                                                                  GtdEaseMode           mode);
+
+GtdEaseMode          gtd_timeline_get_progress_mode              (GtdTimeline          *timeline);
+
+void                 gtd_timeline_set_step_progress              (GtdTimeline          *timeline,
+                                                                  gint                  n_steps,
+                                                                  GtdStepMode           step_mode);
+
+gboolean             gtd_timeline_get_step_progress              (GtdTimeline          *timeline,
+                                                                  gint                 *n_steps,
+                                                                  GtdStepMode          *step_mode);
+
+void                 gtd_timeline_set_cubic_bezier_progress      (GtdTimeline            *timeline,
+                                                                  const graphene_point_t *c_1,
+                                                                  const graphene_point_t *c_2);
+
+gboolean             gtd_timeline_get_cubic_bezier_progress      (GtdTimeline      *timeline,
+                                                                  graphene_point_t *c_1,
+                                                                  graphene_point_t *c_2);
+
+gint64               gtd_timeline_get_duration_hint              (GtdTimeline          *timeline);
+gint                 gtd_timeline_get_current_repeat             (GtdTimeline          *timeline);
+
+GdkFrameClock*       gtd_timeline_get_frame_clock                (GtdTimeline           *timeline);
+
+void                 gtd_timeline_set_frame_clock                (GtdTimeline           *timeline,
+                                                                  GdkFrameClock         *frame_clock);
+
+G_END_DECLS
+
+
+G_END_DECLS
diff --git a/src/animation/gtd-transition.c b/src/animation/gtd-transition.c
new file mode 100644
index 0000000..0d71355
--- /dev/null
+++ b/src/animation/gtd-transition.c
@@ -0,0 +1,689 @@
+/* gtd-transition.c
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+
+/**
+ * SECTION:gtd-transition
+ * @Title: GtdTransition
+ * @Short_Description: Transition between two values
+ *
+ * #GtdTransition is an abstract subclass of #GtdTimeline that
+ * computes the interpolation between two values, stored by a #GtdInterval.
+ */
+
+#include "gtd-transition.h"
+
+#include "gtd-animatable.h"
+#include "gtd-debug.h"
+#include "gtd-interval.h"
+#include "gtd-timeline.h"
+
+#include <gobject/gvaluecollector.h>
+
+typedef struct
+{
+  GtdInterval *interval;
+  GtdAnimatable *animatable;
+
+  guint remove_on_complete : 1;
+} GtdTransitionPrivate;
+
+enum
+{
+  PROP_0,
+  PROP_INTERVAL,
+  PROP_ANIMATABLE,
+  PROP_REMOVE_ON_COMPLETE,
+  PROP_LAST,
+};
+
+static GParamSpec *obj_props[PROP_LAST] = { NULL, };
+
+static GQuark quark_animatable_set = 0;
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GtdTransition, gtd_transition, GTD_TYPE_TIMELINE)
+
+static void
+gtd_transition_attach (GtdTransition *self,
+                       GtdAnimatable *animatable)
+{
+  GTD_TRANSITION_GET_CLASS (self)->attached (self, animatable);
+}
+
+static void
+gtd_transition_detach (GtdTransition *self,
+                       GtdAnimatable *animatable)
+{
+  GTD_TRANSITION_GET_CLASS (self)->detached (self, animatable);
+}
+
+static void
+gtd_transition_real_compute_value (GtdTransition *self,
+                                   GtdAnimatable *animatable,
+                                   GtdInterval   *interval,
+                                   gdouble        progress)
+{
+}
+
+static void
+gtd_transition_real_attached (GtdTransition *self,
+                              GtdAnimatable *animatable)
+{
+}
+
+static void
+gtd_transition_real_detached (GtdTransition *self,
+                              GtdAnimatable *animatable)
+{
+}
+
+static void
+gtd_transition_new_frame (GtdTimeline *timeline,
+                          gint         elapsed)
+{
+  GtdTransition *self = GTD_TRANSITION (timeline);
+  GtdTransitionPrivate *priv = gtd_transition_get_instance_private (self);
+  gdouble progress;
+
+  if (priv->interval == NULL ||
+      priv->animatable == NULL)
+    return;
+
+  progress = gtd_timeline_get_progress (timeline);
+
+  GTD_TRANSITION_GET_CLASS (timeline)->compute_value (self,
+                                                          priv->animatable,
+                                                          priv->interval,
+                                                          progress);
+}
+
+static void
+gtd_transition_stopped (GtdTimeline *timeline,
+                        gboolean     is_finished)
+{
+  GtdTransition *self = GTD_TRANSITION (timeline);
+  GtdTransitionPrivate *priv = gtd_transition_get_instance_private (self);
+
+  if (is_finished &&
+      priv->animatable != NULL &&
+      priv->remove_on_complete)
+    {
+      gtd_transition_detach (GTD_TRANSITION (timeline),
+                                 priv->animatable);
+      g_clear_object (&priv->animatable);
+    }
+}
+
+static void
+gtd_transition_set_property (GObject      *gobject,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  GtdTransition *self = GTD_TRANSITION (gobject);
+
+  switch (prop_id)
+    {
+    case PROP_INTERVAL:
+      gtd_transition_set_interval (self, g_value_get_object (value));
+      break;
+
+    case PROP_ANIMATABLE:
+      gtd_transition_set_animatable (self, g_value_get_object (value));
+      break;
+
+    case PROP_REMOVE_ON_COMPLETE:
+      gtd_transition_set_remove_on_complete (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+gtd_transition_get_property (GObject    *gobject,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  GtdTransition *self = GTD_TRANSITION (gobject);
+  GtdTransitionPrivate *priv = gtd_transition_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_INTERVAL:
+      g_value_set_object (value, priv->interval);
+      break;
+
+    case PROP_ANIMATABLE:
+      g_value_set_object (value, priv->animatable);
+      break;
+
+    case PROP_REMOVE_ON_COMPLETE:
+      g_value_set_boolean (value, priv->remove_on_complete);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+gtd_transition_dispose (GObject *gobject)
+{
+  GtdTransition *self = GTD_TRANSITION (gobject);
+  GtdTransitionPrivate *priv = gtd_transition_get_instance_private (self);
+
+  if (priv->animatable != NULL)
+    gtd_transition_detach (GTD_TRANSITION (gobject),
+                               priv->animatable);
+
+  g_clear_object (&priv->interval);
+  g_clear_object (&priv->animatable);
+
+  G_OBJECT_CLASS (gtd_transition_parent_class)->dispose (gobject);
+}
+
+static void
+gtd_transition_class_init (GtdTransitionClass *klass)
+{
+  GtdTimelineClass *timeline_class = GTD_TIMELINE_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  quark_animatable_set =
+    g_quark_from_static_string ("-gtd-transition-animatable-set");
+
+  klass->compute_value = gtd_transition_real_compute_value;
+  klass->attached = gtd_transition_real_attached;
+  klass->detached = gtd_transition_real_detached;
+
+  timeline_class->new_frame = gtd_transition_new_frame;
+  timeline_class->stopped = gtd_transition_stopped;
+
+  gobject_class->set_property = gtd_transition_set_property;
+  gobject_class->get_property = gtd_transition_get_property;
+  gobject_class->dispose = gtd_transition_dispose;
+
+  /**
+   * GtdTransition:interval:
+   *
+   * The #GtdInterval used to describe the initial and final states
+   * of the transition.
+   *
+   * Since: 1.10
+   */
+  obj_props[PROP_INTERVAL] =
+    g_param_spec_object ("interval",
+                         "Interval",
+                         "The interval of values to transition",
+                         GTD_TYPE_INTERVAL,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * GtdTransition:animatable:
+   *
+   * The #GtdAnimatable instance currently being animated.
+   *
+   * Since: 1.10
+   */
+  obj_props[PROP_ANIMATABLE] =
+    g_param_spec_object ("animatable",
+                         "Animatable",
+                         "The animatable object",
+                         GTD_TYPE_ANIMATABLE,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * GtdTransition:remove-on-complete:
+   *
+   * Whether the #GtdTransition should be automatically detached
+   * from the #GtdTransition:animatable instance whenever the
+   * #GtdTimeline::stopped signal is emitted.
+   *
+   * The #GtdTransition:remove-on-complete property takes into
+   * account the value of the #GtdTimeline:repeat-count property,
+   * and it only detaches the transition if the transition is not
+   * repeating.
+   *
+   * Since: 1.10
+   */
+  obj_props[PROP_REMOVE_ON_COMPLETE] =
+    g_param_spec_boolean ("remove-on-complete",
+                          "Remove on Complete",
+                          "Detach the transition when completed",
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (gobject_class, PROP_LAST, obj_props);
+}
+
+static void
+gtd_transition_init (GtdTransition *self)
+{
+}
+
+/**
+ * gtd_transition_set_interval:
+ * @transition: a #GtdTransition
+ * @interval: (allow-none): a #GtdInterval, or %NULL
+ *
+ * Sets the #GtdTransition:interval property using @interval.
+ *
+ * The @transition will acquire a reference on the @interval, sinking
+ * the floating flag on it if necessary.
+ *
+ * Since: 1.10
+ */
+void
+gtd_transition_set_interval (GtdTransition *self,
+                                 GtdInterval   *interval)
+{
+  GtdTransitionPrivate *priv;
+
+  g_return_if_fail (GTD_IS_TRANSITION (self));
+  g_return_if_fail (interval == NULL || GTD_IS_INTERVAL (interval));
+
+  priv = gtd_transition_get_instance_private (self);
+
+  if (priv->interval == interval)
+    return;
+
+  g_clear_object (&priv->interval);
+
+  if (interval != NULL)
+    priv->interval = g_object_ref_sink (interval);
+
+  g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_INTERVAL]);
+}
+
+/**
+ * gtd_transition_get_interval:
+ * @transition: a #GtdTransition
+ *
+ * Retrieves the interval set using gtd_transition_set_interval()
+ *
+ * Return value: (transfer none): a #GtdInterval, or %NULL; the returned
+ *   interval is owned by the #GtdTransition and it should not be freed
+ *   directly
+ *
+ * Since: 1.10
+ */
+GtdInterval *
+gtd_transition_get_interval (GtdTransition *self)
+{
+  GtdTransitionPrivate *priv = gtd_transition_get_instance_private (self);
+
+  g_return_val_if_fail (GTD_IS_TRANSITION (self), NULL);
+
+  priv = gtd_transition_get_instance_private (self);
+  return priv->interval;
+}
+
+/**
+ * gtd_transition_set_animatable:
+ * @transition: a #GtdTransition
+ * @animatable: (allow-none): a #GtdAnimatable, or %NULL
+ *
+ * Sets the #GtdTransition:animatable property.
+ *
+ * The @transition will acquire a reference to the @animatable instance,
+ * and will call the #GtdTransitionClass.attached() virtual function.
+ *
+ * If an existing #GtdAnimatable is attached to @self, the
+ * reference will be released, and the #GtdTransitionClass.detached()
+ * virtual function will be called.
+ *
+ * Since: 1.10
+ */
+void
+gtd_transition_set_animatable (GtdTransition *self,
+                                   GtdAnimatable *animatable)
+{
+  GtdTransitionPrivate *priv;
+  GtdWidget *widget;
+
+  g_return_if_fail (GTD_IS_TRANSITION (self));
+  g_return_if_fail (animatable == NULL || GTD_IS_ANIMATABLE (animatable));
+
+  priv = gtd_transition_get_instance_private (self);
+
+  if (priv->animatable == animatable)
+    return;
+
+  if (priv->animatable != NULL)
+    gtd_transition_detach (self, priv->animatable);
+
+  g_clear_object (&priv->animatable);
+
+  if (animatable != NULL)
+    {
+      priv->animatable = g_object_ref (animatable);
+      gtd_transition_attach (self, priv->animatable);
+    }
+
+  widget = gtd_animatable_get_widget (animatable);
+  gtd_timeline_set_widget (GTD_TIMELINE (self), widget);
+}
+
+/**
+ * gtd_transition_get_animatable:
+ * @transition: a #GtdTransition
+ *
+ * Retrieves the #GtdAnimatable set using gtd_transition_set_animatable().
+ *
+ * Return value: (transfer none): a #GtdAnimatable, or %NULL; the returned
+ *   animatable is owned by the #GtdTransition, and it should not be freed
+ *   directly.
+ *
+ * Since: 1.10
+ */
+GtdAnimatable *
+gtd_transition_get_animatable (GtdTransition *self)
+{
+  GtdTransitionPrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_TRANSITION (self), NULL);
+
+  priv = gtd_transition_get_instance_private (self);
+  return priv->animatable;
+}
+
+/**
+ * gtd_transition_set_remove_on_complete:
+ * @transition: a #GtdTransition
+ * @remove_complete: whether to detach @transition when complete
+ *
+ * Sets whether @transition should be detached from the #GtdAnimatable
+ * set using gtd_transition_set_animatable() when the
+ * #GtdTimeline::completed signal is emitted.
+ *
+ * Since: 1.10
+ */
+void
+gtd_transition_set_remove_on_complete (GtdTransition *self,
+                                       gboolean       remove_complete)
+{
+  GtdTransitionPrivate *priv;
+
+  g_return_if_fail (GTD_IS_TRANSITION (self));
+
+  priv = gtd_transition_get_instance_private (self);
+  remove_complete = !!remove_complete;
+
+  if (priv->remove_on_complete == remove_complete)
+    return;
+
+  priv->remove_on_complete = remove_complete;
+
+  g_object_notify_by_pspec (G_OBJECT (self),
+                            obj_props[PROP_REMOVE_ON_COMPLETE]);
+}
+
+/**
+ * gtd_transition_get_remove_on_complete:
+ * @transition: a #GtdTransition
+ *
+ * Retrieves the value of the #GtdTransition:remove-on-complete property.
+ *
+ * Return value: %TRUE if the @transition should be detached when complete,
+ *   and %FALSE otherwise
+ *
+ * Since: 1.10
+ */
+gboolean
+gtd_transition_get_remove_on_complete (GtdTransition *self)
+{
+  GtdTransitionPrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_TRANSITION (self), FALSE);
+
+  priv = gtd_transition_get_instance_private (self);
+  return priv->remove_on_complete;
+}
+
+typedef void (* IntervalSetFunc) (GtdInterval *interval,
+                                  const GValue    *value);
+
+static inline void
+gtd_transition_set_value (GtdTransition   *self,
+                          IntervalSetFunc  interval_set_func,
+                          const GValue    *value)
+{
+  GtdTransitionPrivate *priv = gtd_transition_get_instance_private (self);
+  GType interval_type;
+
+  if (priv->interval == NULL)
+    {
+      priv->interval = gtd_interval_new_with_values (G_VALUE_TYPE (value),
+                                                         NULL,
+                                                         NULL);
+      g_object_ref_sink (priv->interval);
+    }
+
+  interval_type = gtd_interval_get_value_type (priv->interval);
+
+  if (!g_type_is_a (G_VALUE_TYPE (value), interval_type))
+    {
+      if (g_value_type_compatible (G_VALUE_TYPE (value), interval_type))
+        {
+          interval_set_func (priv->interval, value);
+          return;
+        }
+
+      if (g_value_type_transformable (G_VALUE_TYPE (value), interval_type))
+        {
+          GValue transform = G_VALUE_INIT;
+
+          g_value_init (&transform, interval_type);
+          if (g_value_transform (value, &transform))
+            interval_set_func (priv->interval, &transform);
+          else
+            {
+              g_warning ("%s: Unable to convert a value of type '%s' into "
+                         "the value type '%s' of the interval used by the "
+                         "transition.",
+                         G_STRLOC,
+                         g_type_name (G_VALUE_TYPE (value)),
+                         g_type_name (interval_type));
+            }
+
+          g_value_unset (&transform);
+        }
+    }
+  else
+    interval_set_func (priv->interval, value);
+}
+
+/**
+ * gtd_transition_set_from_value: (rename-to gtd_transition_set_from)
+ * @transition: a #GtdTransition
+ * @value: a #GValue with the initial value of the transition
+ *
+ * Sets the initial value of the transition.
+ *
+ * This is a convenience function that will either create the
+ * #GtdInterval used by @self, or will update it if
+ * the #GtdTransition:interval is already set.
+ *
+ * This function will copy the contents of @value, so it is
+ * safe to call g_value_unset() after it returns.
+ *
+ * If @transition already has a #GtdTransition:interval set,
+ * then @value must hold the same type, or a transformable type,
+ * as the interval's #GtdInterval:value-type property.
+ *
+ * This function is meant to be used by language bindings.
+ *
+ * Since: 1.12
+ */
+void
+gtd_transition_set_from_value (GtdTransition *self,
+                                   const GValue      *value)
+{
+  g_return_if_fail (GTD_IS_TRANSITION (self));
+  g_return_if_fail (G_IS_VALUE (value));
+
+  gtd_transition_set_value (self,
+                                gtd_interval_set_initial_value,
+                                value);
+}
+
+/**
+ * gtd_transition_set_to_value: (rename-to gtd_transition_set_to)
+ * @transition: a #GtdTransition
+ * @value: a #GValue with the final value of the transition
+ *
+ * Sets the final value of the transition.
+ *
+ * This is a convenience function that will either create the
+ * #GtdInterval used by @self, or will update it if
+ * the #GtdTransition:interval is already set.
+ *
+ * This function will copy the contents of @value, so it is
+ * safe to call g_value_unset() after it returns.
+ *
+ * If @transition already has a #GtdTransition:interval set,
+ * then @value must hold the same type, or a transformable type,
+ * as the interval's #GtdInterval:value-type property.
+ *
+ * This function is meant to be used by language bindings.
+ *
+ * Since: 1.12
+ */
+void
+gtd_transition_set_to_value (GtdTransition *self,
+                                 const GValue      *value)
+{
+  g_return_if_fail (GTD_IS_TRANSITION (self));
+  g_return_if_fail (G_IS_VALUE (value));
+
+  gtd_transition_set_value (self,
+                                gtd_interval_set_final_value,
+                                value);
+}
+
+/**
+ * gtd_transition_set_from: (skip)
+ * @transition: a #GtdTransition
+ * @value_type: the type of the value to set
+ * @...: the initial value
+ *
+ * Sets the initial value of the transition.
+ *
+ * This is a convenience function that will either create the
+ * #GtdInterval used by @self, or will update it if
+ * the #GtdTransition:interval is already set.
+ *
+ * If @transition already has a #GtdTransition:interval set,
+ * then @value must hold the same type, or a transformable type,
+ * as the interval's #GtdInterval:value-type property.
+ *
+ * This is a convenience function for the C API; language bindings
+ * should use gtd_transition_set_from_value() instead.
+ *
+ * Since: 1.12
+ */
+void
+gtd_transition_set_from (GtdTransition *self,
+                             GType              value_type,
+                             ...)
+{
+  GValue value = G_VALUE_INIT;
+  gchar *error = NULL;
+  va_list args;
+
+  g_return_if_fail (GTD_IS_TRANSITION (self));
+  g_return_if_fail (value_type != G_TYPE_INVALID);
+
+  va_start (args, value_type);
+
+  G_VALUE_COLLECT_INIT (&value, value_type, args, 0, &error);
+
+  va_end (args);
+
+  if (error != NULL)
+    {
+      g_warning ("%s: %s", G_STRLOC, error);
+      g_free (error);
+      return;
+    }
+
+  gtd_transition_set_value (self,
+                                gtd_interval_set_initial_value,
+                                &value);
+
+  g_value_unset (&value);
+}
+
+/**
+ * gtd_transition_set_to: (skip)
+ * @transition: a #GtdTransition
+ * @value_type: the type of the value to set
+ * @...: the final value
+ *
+ * Sets the final value of the transition.
+ *
+ * This is a convenience function that will either create the
+ * #GtdInterval used by @self, or will update it if
+ * the #GtdTransition:interval is already set.
+ *
+ * If @transition already has a #GtdTransition:interval set,
+ * then @value must hold the same type, or a transformable type,
+ * as the interval's #GtdInterval:value-type property.
+ *
+ * This is a convenience function for the C API; language bindings
+ * should use gtd_transition_set_to_value() instead.
+ *
+ * Since: 1.12
+ */
+void
+gtd_transition_set_to (GtdTransition *self,
+                       GType          value_type,
+                       ...)
+{
+  GValue value = G_VALUE_INIT;
+  gchar *error = NULL;
+  va_list args;
+
+  g_return_if_fail (GTD_IS_TRANSITION (self));
+  g_return_if_fail (value_type != G_TYPE_INVALID);
+
+  va_start (args, value_type);
+
+  G_VALUE_COLLECT_INIT (&value, value_type, args, 0, &error);
+
+  va_end (args);
+
+  if (error != NULL)
+    {
+      g_warning ("%s: %s", G_STRLOC, error);
+      g_free (error);
+      return;
+    }
+
+  gtd_transition_set_value (self,
+                                gtd_interval_set_final_value,
+                                &value);
+
+  g_value_unset (&value);
+}
diff --git a/src/animation/gtd-transition.h b/src/animation/gtd-transition.h
new file mode 100644
index 0000000..3c8a918
--- /dev/null
+++ b/src/animation/gtd-transition.h
@@ -0,0 +1,85 @@
+/* gtd-transition.h
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "gtd-timeline.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_TRANSITION (gtd_transition_get_type())
+G_DECLARE_DERIVABLE_TYPE (GtdTransition, gtd_transition, GTD, TRANSITION, GtdTimeline)
+
+/**
+ * GtdTransitionClass:
+ * @attached: virtual function; called when a transition is attached to
+ *   a #GtdAnimatable instance
+ * @detached: virtual function; called when a transition is detached from
+ *   a #GtdAnimatable instance
+ * @compute_value: virtual function; called each frame to compute and apply
+ *   the interpolation of the interval
+ *
+ * The #GtdTransitionClass structure contains
+ * private data.
+ *
+ * Since: 1.10
+ */
+struct _GtdTransitionClass
+{
+  /*< private >*/
+  GtdTimelineClass parent_class;
+
+  /*< public >*/
+  void (* attached) (GtdTransition *transition,
+                     GtdAnimatable *animatable);
+  void (* detached) (GtdTransition *transition,
+                     GtdAnimatable *animatable);
+
+  void (* compute_value) (GtdTransition *transition,
+                          GtdAnimatable *animatable,
+                          GtdInterval   *interval,
+                          gdouble            progress);
+
+  /*< private >*/
+  gpointer _padding[8];
+};
+
+void                    gtd_transition_set_interval                 (GtdTransition *transition,
+                                                                     GtdInterval   *interval);
+GtdInterval *           gtd_transition_get_interval                 (GtdTransition *transition);
+void                    gtd_transition_set_from_value               (GtdTransition *transition,
+                                                                     const GValue  *value);
+void                    gtd_transition_set_to_value                 (GtdTransition *transition,
+                                                                     const GValue  *value);
+void                    gtd_transition_set_from                     (GtdTransition *transition,
+                                                                     GType          value_type,
+                                                                     ...);
+void                    gtd_transition_set_to                       (GtdTransition *transition,
+                                                                     GType          value_type,
+                                                                     ...);
+
+void                    gtd_transition_set_animatable               (GtdTransition *transition,
+                                                                     GtdAnimatable *animatable);
+GtdAnimatable *         gtd_transition_get_animatable               (GtdTransition *transition);
+void                    gtd_transition_set_remove_on_complete       (GtdTransition *transition,
+                                                                     gboolean       remove_complete);
+gboolean                gtd_transition_get_remove_on_complete       (GtdTransition *transition);
+
+G_END_DECLS
diff --git a/src/gnome-todo.h b/src/gnome-todo.h
index de306c4..7439200 100644
--- a/src/gnome-todo.h
+++ b/src/gnome-todo.h
@@ -21,8 +21,11 @@
 
 #include <libpeas/peas.h>
 
+#include "gtd-enum-types.h"
+
 #include "gtd-activatable.h"
 #include "gtd-bin-layout.h"
+#include "gtd-easing.h"
 #include "gtd-list-model-filter.h"
 #include "gtd-list-model-sort.h"
 #include "gtd-list-store.h"
@@ -40,6 +43,7 @@
 #include "gtd-task.h"
 #include "gtd-task-list.h"
 #include "gtd-task-list-view.h"
+#include "gtd-transition.h"
 #include "gtd-types.h"
 #include "gtd-utils.h"
 #include "gtd-widget.h"
diff --git a/src/gtd-enum-types.c.template b/src/gtd-enum-types.c.template
new file mode 100644
index 0000000..ed83a71
--- /dev/null
+++ b/src/gtd-enum-types.c.template
@@ -0,0 +1,39 @@
+/*** BEGIN file-header ***/
+#include "gtd-enum-types.h"
+
+/*** END file-header ***/
+
+/*** BEGIN file-production ***/
+/* enumerations from "@filename@" */
+#include "@filename@"
+
+/*** END file-production ***/
+
+/*** BEGIN value-header ***/
+GType
+@enum_name@_get_type (void)
+{
+       static GType the_type = 0;
+
+       if (the_type == 0)
+       {
+               static const G@Type@Value values[] = {
+/*** END value-header ***/
+
+/*** BEGIN value-production ***/
+                       { @VALUENAME@,
+                         "@VALUENAME@",
+                         "@valuenick@" },
+/*** END value-production ***/
+
+/*** BEGIN value-tail ***/
+                       { 0, NULL, NULL }
+               };
+               the_type = g_@type@_register_static (
+                               g_intern_static_string ("@EnumName@"),
+                               values);
+       }
+       return the_type;
+}
+
+/*** END value-tail ***/
diff --git a/src/gtd-enum-types.h.template b/src/gtd-enum-types.h.template
new file mode 100644
index 0000000..f3d5156
--- /dev/null
+++ b/src/gtd-enum-types.h.template
@@ -0,0 +1,24 @@
+/*** BEGIN file-header ***/
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+/*** END file-header ***/
+
+/*** BEGIN file-production ***/
+/* Enumerations from "@filename@" */
+
+/*** END file-production ***/
+
+/*** BEGIN enumeration-production ***/
+#define GTD_TYPE_@ENUMSHORT@   (@enum_name@_get_type())
+GType @enum_name@_get_type     (void) G_GNUC_CONST;
+
+/*** END enumeration-production ***/
+
+/*** BEGIN file-tail ***/
+G_END_DECLS
+
+/*** END file-tail ***/
diff --git a/src/gtd-types.h b/src/gtd-types.h
index 099396b..dc166ca 100644
--- a/src/gtd-types.h
+++ b/src/gtd-types.h
@@ -24,9 +24,11 @@
 G_BEGIN_DECLS
 
 typedef struct _GtdActivatable          GtdActivatable;
+typedef struct _GtdAnimatable           GtdAnimatable;
 typedef struct _GtdApplication          GtdApplication;
 typedef struct _GtdClock                GtdClock;
 typedef struct _GtdDoneButton           GtdDoneButton;
+typedef struct _GtdInterval             GtdInterval;
 typedef struct _GtdInitialSetupWindow   GtdInitialSetupWindow;
 typedef struct _GtdListView             GtdListView;
 typedef struct _GtdManager              GtdManager;
@@ -46,6 +48,7 @@ typedef struct _GtdTask                 GtdTask;
 typedef struct _GtdTaskList             GtdTaskList;
 typedef struct _GtdTaskListItem         GtdTaskListItem;
 typedef struct _GtdTaskRow              GtdTaskRow;
+typedef struct _GtdTransition           GtdTransition;
 typedef struct _GtdWidget               GtdWidget;
 typedef struct _GtdWindow               GtdWindow;
 typedef struct _GtdWorkspace            GtdWorkspace;
diff --git a/src/gui/gtd-widget.c b/src/gui/gtd-widget.c
index bdcf324..6b3334b 100644
--- a/src/gui/gtd-widget.c
+++ b/src/gui/gtd-widget.c
@@ -18,11 +18,17 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
+#include "gtd-animatable.h"
 #include "gtd-bin-layout.h"
 #include "gtd-debug.h"
+#include "gtd-animation-enums.h"
+#include "gtd-interval.h"
+#include "gtd-timeline-private.h"
+#include "gtd-property-transition.h"
 #include "gtd-widget-private.h"
 
 #include <graphene-gobject.h>
+#include <gobject/gvaluecollector.h>
 
 enum
 {
@@ -33,6 +39,19 @@ enum
 
 typedef struct
 {
+  guint easing_duration;
+  guint easing_delay;
+  GtdEaseMode easing_mode;
+} AnimationState;
+
+typedef struct
+{
+  struct {
+    GHashTable *transitions;
+    GArray *states;
+    AnimationState *current_state;
+  } animation;
+
   graphene_point3d_t  pivot_point;
   gfloat              rotation[3];
   gfloat              scale[3];
@@ -42,7 +61,16 @@ typedef struct
   GskTransform       *cached_transform;
 } GtdWidgetPrivate;
 
-G_DEFINE_TYPE_WITH_PRIVATE (GtdWidget, gtd_widget, GTK_TYPE_WIDGET)
+static void set_animatable_property (GtdWidget    *self,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec);
+
+static void gtd_animatable_iface_init (GtdAnimatableInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GtdWidget, gtd_widget, GTK_TYPE_WIDGET,
+                         G_ADD_PRIVATE (GtdWidget)
+                         G_IMPLEMENT_INTERFACE (GTD_TYPE_ANIMATABLE, gtd_animatable_iface_init))
 
 enum
 {
@@ -60,6 +88,16 @@ enum
   N_PROPS
 };
 
+enum
+{
+  TRANSITION_STOPPED,
+  TRANSITIONS_COMPLETED,
+  NUM_SIGNALS
+};
+
+
+static guint signals[NUM_SIGNALS] = { 0, };
+
 static GParamSpec *properties [N_PROPS] = { NULL, };
 
 
@@ -67,6 +105,307 @@ static GParamSpec *properties [N_PROPS] = { NULL, };
  * Auxiliary methods
  */
 
+typedef struct
+{
+  GtdWidget *widget;
+  GtdTransition *transition;
+  gchar *name;
+  gulong completed_id;
+} TransitionClosure;
+
+static void
+transition_closure_free (gpointer data)
+{
+  if (G_LIKELY (data != NULL))
+    {
+      TransitionClosure *closure = data;
+      GtdTimeline *timeline;
+
+      timeline = GTD_TIMELINE (closure->transition);
+
+      /* we disconnect the signal handler before stopping the timeline,
+       * so that we don't end up inside on_transition_stopped() from
+       * a call to g_hash_table_remove().
+       */
+      g_clear_signal_handler (&closure->completed_id, closure->transition);
+
+      if (gtd_timeline_is_playing (timeline))
+        gtd_timeline_stop (timeline);
+      else if (gtd_timeline_get_delay (timeline) > 0)
+        gtd_timeline_cancel_delay (timeline);
+
+      g_object_unref (closure->transition);
+
+      g_free (closure->name);
+
+      g_slice_free (TransitionClosure, closure);
+    }
+}
+
+static void
+on_transition_stopped_cb (GtdTransition     *transition,
+                          gboolean           is_finished,
+                          TransitionClosure *closure)
+{
+  GtdWidget *self = closure->widget;
+  GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self);
+  GQuark t_quark;
+  gchar *t_name;
+
+  if (closure->name == NULL)
+    return;
+
+  /* we need copies because we emit the signal after the
+   * TransitionClosure data structure has been freed
+   */
+  t_quark = g_quark_from_string (closure->name);
+  t_name = g_strdup (closure->name);
+
+  if (gtd_transition_get_remove_on_complete (transition))
+    {
+      /* this is safe, because the timeline has now stopped,
+       * so we won't recurse; the reference on the Animatable
+       * will be dropped by the ::stopped signal closure in
+       * GtdTransition, which is RUN_LAST, and thus will
+       * be called after this handler
+       */
+      g_hash_table_remove (priv->animation.transitions, closure->name);
+    }
+
+  /* we emit the ::transition-stopped after removing the
+   * transition, so that we can chain up new transitions
+   * without interfering with the one that just finished
+   */
+  g_signal_emit (self, signals[TRANSITION_STOPPED], t_quark, t_name, is_finished);
+
+  g_free (t_name);
+
+  /* if it's the last transition then we clean up */
+  if (g_hash_table_size (priv->animation.transitions) == 0)
+    {
+      g_hash_table_unref (priv->animation.transitions);
+      priv->animation.transitions = NULL;
+
+      GTD_TRACE_MSG ("[animation] Transitions for '%p' completed", self);
+
+      g_signal_emit (self, signals[TRANSITIONS_COMPLETED], 0);
+    }
+}
+
+static void
+add_transition_to_widget (GtdWidget     *self,
+                          const gchar   *name,
+                          GtdTransition *transition)
+{
+  GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self);
+  TransitionClosure *closure;
+  GtdTimeline *timeline;
+
+  GTD_ENTRY;
+
+  if (!priv->animation.transitions)
+    {
+      priv->animation.transitions = g_hash_table_new_full (g_str_hash,
+                                                           g_str_equal,
+                                                           NULL,
+                                                           transition_closure_free);
+    }
+
+  if (g_hash_table_lookup (priv->animation.transitions, name) != NULL)
+    {
+      g_warning ("A transition with name '%s' already exists for the widget '%p'",
+                 name,
+                 self);
+      GTD_RETURN ();
+    }
+
+  gtd_transition_set_animatable (transition, GTD_ANIMATABLE (self));
+
+  timeline = GTD_TIMELINE (transition);
+
+  closure = g_slice_new (TransitionClosure);
+  closure->widget = self;
+  closure->transition = g_object_ref (transition);
+  closure->name = g_strdup (name);
+  closure->completed_id = g_signal_connect (timeline,
+                                            "stopped",
+                                            G_CALLBACK (on_transition_stopped_cb),
+                                            closure);
+
+  GTD_TRACE_MSG ("[animation] Adding transition '%s' [%p] to widget %p",
+                closure->name,
+                closure->transition,
+                self);
+
+  g_hash_table_insert (priv->animation.transitions, closure->name, closure);
+  gtd_timeline_start (timeline);
+
+  GTD_EXIT;
+}
+
+static gboolean
+should_skip_implicit_transition (GtdWidget  *self,
+                                 GParamSpec *pspec)
+{
+  GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self);
+
+  /* if the easing state has a non-zero duration we always want an
+   * implicit transition to occur
+   */
+  if (priv->animation.current_state->easing_duration == 0)
+    return TRUE;
+
+  /* if the widget is not mapped and is not part of a branch of the scene
+   * graph that is being cloned, then we always skip implicit transitions
+   * on the account of the fact that the widget is not going to be visible
+   * when those transitions happen
+   */
+  if (!gtk_widget_get_mapped (GTK_WIDGET (self)))
+    return TRUE;
+
+  return FALSE;
+}
+
+static GtdTransition*
+create_transition (GtdWidget  *self,
+                   GParamSpec *pspec,
+                   ...)
+{
+  GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self);
+  g_autofree gchar *error = NULL;
+  g_auto (GValue) initial = G_VALUE_INIT;
+  g_auto (GValue) final = G_VALUE_INIT;
+  TransitionClosure *closure;
+  GtdTimeline *timeline;
+  GtdInterval *interval;
+  GtdTransition *res = NULL;
+  va_list var_args;
+  GType ptype;
+
+  g_assert (pspec != NULL);
+
+  if (!priv->animation.transitions)
+    {
+      priv->animation.transitions = g_hash_table_new_full (g_str_hash,
+                                                           g_str_equal,
+                                                           NULL,
+                                                           transition_closure_free);
+    }
+
+  va_start (var_args, pspec);
+
+  ptype = G_PARAM_SPEC_VALUE_TYPE (pspec);
+
+  G_VALUE_COLLECT_INIT (&initial, ptype, var_args, 0, &error);
+  if (error != NULL)
+    {
+      g_critical ("%s: %s", G_STRLOC, error);
+      goto out;
+    }
+
+  G_VALUE_COLLECT_INIT (&final, ptype, var_args, 0, &error);
+  if (error != NULL)
+    {
+      g_critical ("%s: %s", G_STRLOC, error);
+      goto out;
+    }
+
+  if (should_skip_implicit_transition (self, pspec))
+    {
+      GTD_TRACE_MSG ("[animation] Skipping implicit transition for '%p::%s'",
+                    self,
+                    pspec->name);
+
+      /* remove a transition, if one exists */
+      gtd_widget_remove_transition (self, pspec->name);
+
+      /* we don't go through the Animatable interface because we
+       * already know we got here through an animatable property.
+       */
+      set_animatable_property (self, pspec->param_id, &final, pspec);
+
+      goto out;
+    }
+
+  closure = g_hash_table_lookup (priv->animation.transitions, pspec->name);
+  if (closure == NULL)
+    {
+      res = gtd_property_transition_new (pspec->name);
+
+      gtd_transition_set_remove_on_complete (res, TRUE);
+
+      interval = gtd_interval_new_with_values (ptype, &initial, &final);
+      gtd_transition_set_interval (res, interval);
+
+      timeline = GTD_TIMELINE (res);
+      gtd_timeline_set_delay (timeline, priv->animation.current_state->easing_delay);
+      gtd_timeline_set_duration (timeline, priv->animation.current_state->easing_duration);
+      gtd_timeline_set_progress_mode (timeline, priv->animation.current_state->easing_mode);
+
+#ifdef GTD_ENABLE_DEBUG
+      if (GTD_HAS_DEBUG (ANIMATION))
+        {
+          gchar *initial_v, *final_v;
+
+          initial_v = g_strdup_value_contents (&initial);
+          final_v = g_strdup_value_contents (&final);
+
+          GTD_TRACE_MSG ("[animation] "
+                         "Created transition for %p:%s "
+                         "(len:%u, mode:%s, delay:%u) "
+                         "initial:%s, final:%s",
+                         self,
+                         pspec->name,
+                         priv->animation.current_state->easing_duration,
+                         gtd_get_easing_name_for_mode (priv->animation.current_state->easing_mode),
+                         priv->animation.current_state->easing_delay,
+                         initial_v, final_v);
+
+          g_free (initial_v);
+          g_free (final_v);
+        }
+#endif /* GTD_ENABLE_DEBUG */
+
+      /* this will start the transition as well */
+      add_transition_to_widget (self, pspec->name, res);
+
+      /* the widget now owns the transition */
+      g_object_unref (res);
+    }
+  else
+    {
+      GtdEaseMode cur_mode;
+      guint cur_duration;
+
+      GTD_TRACE_MSG ("[animation] Existing transition for %p:%s",
+                    self,
+                    pspec->name);
+
+      timeline = GTD_TIMELINE (closure->transition);
+
+      cur_duration = gtd_timeline_get_duration (timeline);
+      if (cur_duration != priv->animation.current_state->easing_duration)
+        gtd_timeline_set_duration (timeline, priv->animation.current_state->easing_duration);
+
+      cur_mode = gtd_timeline_get_progress_mode (timeline);
+      if (cur_mode != priv->animation.current_state->easing_mode)
+        gtd_timeline_set_progress_mode (timeline, priv->animation.current_state->easing_mode);
+
+      gtd_timeline_rewind (timeline);
+
+      interval = gtd_transition_get_interval (closure->transition);
+      gtd_interval_set_initial_value (interval, &initial);
+      gtd_interval_set_final_value (interval, &final);
+
+      res = closure->transition;
+    }
+
+out:
+  va_end (var_args);
+
+  return res;
+}
+
 static void
 invalidate_cached_transform (GtdWidget *self)
 {
@@ -97,17 +436,6 @@ calculate_transform (GtdWidget *self)
   transform = gsk_transform_perspective (transform,
                                          2 * MAX (priv->geometry.width, priv->geometry.height));
 
-  /* Rotation */
-  transform = gsk_transform_rotate_3d (transform, priv->rotation[X], graphene_vec3_x_axis ());
-  transform = gsk_transform_rotate_3d (transform, priv->rotation[Y], graphene_vec3_y_axis ());
-  transform = gsk_transform_rotate_3d (transform, priv->rotation[Z], graphene_vec3_z_axis ());
-
-  /* Scale */
-  if (G_APPROX_VALUE (priv->scale[Z], 1.f, FLT_EPSILON))
-    transform = gsk_transform_scale (transform, priv->scale[X], priv->scale[Y]);
-  else
-    transform = gsk_transform_scale_3d (transform, priv->scale[X], priv->scale[Y], priv->scale[Z]);
-
   /* Translation */
   if (G_APPROX_VALUE (priv->translation.z, 0.f, FLT_EPSILON))
     {
@@ -120,6 +448,17 @@ calculate_transform (GtdWidget *self)
       transform = gsk_transform_translate_3d (transform, &priv->translation);
     }
 
+  /* Scale */
+  if (G_APPROX_VALUE (priv->scale[Z], 1.f, FLT_EPSILON))
+    transform = gsk_transform_scale (transform, priv->scale[X], priv->scale[Y]);
+  else
+    transform = gsk_transform_scale_3d (transform, priv->scale[X], priv->scale[Y], priv->scale[Z]);
+
+  /* Rotation */
+  transform = gsk_transform_rotate_3d (transform, priv->rotation[X], graphene_vec3_x_axis ());
+  transform = gsk_transform_rotate_3d (transform, priv->rotation[Y], graphene_vec3_y_axis ());
+  transform = gsk_transform_rotate_3d (transform, priv->rotation[Z], graphene_vec3_z_axis ());
+
   /* Rollback pivot point */
   if (!pivot_is_zero)
     transform = gsk_transform_translate_3d (transform,
@@ -127,7 +466,234 @@ calculate_transform (GtdWidget *self)
                                                                     -pivot.y,
                                                                     -pivot.z));
 
-  priv->cached_transform = transform;
+  priv->cached_transform = transform;
+}
+
+static void
+set_rotation_internal (GtdWidget *self,
+                       gfloat     rotation_x,
+                       gfloat     rotation_y,
+                       gfloat     rotation_z)
+{
+  GtdWidgetPrivate *priv;
+  gboolean changed[3];
+
+  GTD_ENTRY;
+
+  g_return_if_fail (GTD_IS_WIDGET (self));
+
+  priv = gtd_widget_get_instance_private (self);
+
+  changed[X] = !G_APPROX_VALUE (priv->rotation[X], rotation_x, FLT_EPSILON);
+  changed[Y] = !G_APPROX_VALUE (priv->rotation[Y], rotation_y, FLT_EPSILON);
+  changed[Z] = !G_APPROX_VALUE (priv->rotation[Z], rotation_z, FLT_EPSILON);
+
+  if (!changed[X] && !changed[Y] && !changed[Z])
+    GTD_RETURN ();
+
+  invalidate_cached_transform (self);
+
+  priv->rotation[X] = rotation_x;
+  priv->rotation[Y] = rotation_y;
+  priv->rotation[Z] = rotation_z;
+
+  if (changed[X])
+    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ROTATION_X]);
+
+  if (changed[Y])
+    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ROTATION_Y]);
+
+  if (changed[Z])
+    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ROTATION_Z]);
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+
+  GTD_EXIT;
+}
+
+static void
+set_scale_internal (GtdWidget *self,
+                    gfloat     scale_x,
+                    gfloat     scale_y,
+                    gfloat     scale_z)
+{
+  GtdWidgetPrivate *priv;
+  gboolean changed[3];
+
+  GTD_ENTRY;
+
+  g_return_if_fail (GTD_IS_WIDGET (self));
+
+  priv = gtd_widget_get_instance_private (self);
+
+  changed[X] = !G_APPROX_VALUE (priv->scale[X], scale_x, FLT_EPSILON);
+  changed[Y] = !G_APPROX_VALUE (priv->scale[Y], scale_y, FLT_EPSILON);
+  changed[Z] = !G_APPROX_VALUE (priv->scale[Z], scale_z, FLT_EPSILON);
+
+  if (!changed[X] && !changed[Y] && !changed[Z])
+    GTD_RETURN ();
+
+  invalidate_cached_transform (self);
+
+  priv->scale[X] = scale_x;
+  priv->scale[Y] = scale_y;
+  priv->scale[Z] = scale_z;
+
+  if (changed[X])
+    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SCALE_X]);
+
+  if (changed[Y])
+    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SCALE_Y]);
+
+  if (changed[Z])
+    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SCALE_Z]);
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+
+  GTD_EXIT;
+}
+
+static void
+set_translation_internal (GtdWidget *self,
+                          gfloat     translation_x,
+                          gfloat     translation_y,
+                          gfloat     translation_z)
+{
+  graphene_point3d_t old_translation, translation;
+  GtdWidgetPrivate *priv;
+
+  GTD_ENTRY;
+
+  g_return_if_fail (GTD_IS_WIDGET (self));
+
+  priv = gtd_widget_get_instance_private (self);
+  translation = GRAPHENE_POINT3D_INIT (translation_x, translation_y, translation_z);
+
+  if (graphene_point3d_equal (&priv->translation, &translation))
+    GTD_RETURN ();
+
+  old_translation = priv->translation;
+
+  invalidate_cached_transform (self);
+  priv->translation = translation;
+
+  if (!G_APPROX_VALUE (old_translation.x, translation.x, FLT_EPSILON))
+    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TRANSLATION_X]);
+
+  if (!G_APPROX_VALUE (old_translation.y, translation.y, FLT_EPSILON))
+    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TRANSLATION_Y]);
+
+  if (!G_APPROX_VALUE (old_translation.y, translation.y, FLT_EPSILON))
+    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TRANSLATION_Z]);
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+
+  GTD_EXIT;
+}
+
+static void
+set_animatable_property (GtdWidget    *self,
+                         guint         prop_id,
+                         const GValue *value,
+                         GParamSpec   *pspec)
+{
+  GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self);
+  GObject *object = G_OBJECT (self);
+
+  g_object_freeze_notify (object);
+
+  switch (prop_id)
+    {
+    case PROP_ROTATION_X:
+      set_rotation_internal (self, g_value_get_float (value), priv->rotation[Y], priv->rotation[Z]);
+      break;
+
+    case PROP_ROTATION_Y:
+      set_rotation_internal (self, priv->rotation[X], g_value_get_float (value), priv->rotation[Z]);
+      break;
+
+    case PROP_ROTATION_Z:
+      set_rotation_internal (self, priv->rotation[X], priv->rotation[Y], g_value_get_float (value));
+      break;
+
+    case PROP_SCALE_X:
+      set_scale_internal (self, g_value_get_float (value), priv->scale[Y], priv->scale[Z]);
+      break;
+
+    case PROP_SCALE_Y:
+      set_scale_internal (self, priv->scale[X], g_value_get_float (value), priv->scale[Z]);
+      break;
+
+    case PROP_SCALE_Z:
+      set_scale_internal (self, priv->scale[X], priv->scale[Y], g_value_get_float (value));
+      break;
+
+    case PROP_TRANSLATION_X:
+      set_translation_internal (self, g_value_get_float (value), priv->translation.y, priv->translation.z);
+      break;
+
+    case PROP_TRANSLATION_Y:
+      set_translation_internal (self, priv->translation.x, g_value_get_float (value), priv->translation.z);
+      break;
+
+    case PROP_TRANSLATION_Z:
+      set_translation_internal (self, priv->translation.x, priv->translation.y, g_value_get_float (value));
+      break;
+
+    default:
+      g_object_set_property (object, pspec->name, value);
+      break;
+    }
+
+  g_object_thaw_notify (object);
+}
+
+/*
+ * GtdAnimatable interface
+ */
+
+static GParamSpec *
+gtd_widget_find_property (GtdAnimatable *animatable,
+                          const gchar   *property_name)
+{
+  return g_object_class_find_property (G_OBJECT_GET_CLASS (animatable), property_name);
+}
+
+static void
+gtd_widget_get_initial_state (GtdAnimatable *animatable,
+                              const gchar   *property_name,
+                              GValue        *initial)
+{
+  g_object_get_property (G_OBJECT (animatable), property_name, initial);
+}
+
+static void
+gtd_widget_set_final_state (GtdAnimatable *animatable,
+                            const gchar   *property_name,
+                            const GValue  *final)
+{
+  GObjectClass *obj_class = G_OBJECT_GET_CLASS (animatable);
+  GParamSpec *pspec;
+
+  pspec = g_object_class_find_property (obj_class, property_name);
+
+  if (pspec)
+    set_animatable_property (GTD_WIDGET (animatable), pspec->param_id, final, pspec);
+}
+
+static GtdWidget*
+gtd_widget_get_widget (GtdAnimatable *animatable)
+{
+  return GTD_WIDGET (animatable);
+}
+
+static void
+gtd_animatable_iface_init (GtdAnimatableInterface *iface)
+{
+  iface->find_property = gtd_widget_find_property;
+  iface->get_initial_state = gtd_widget_get_initial_state;
+  iface->set_final_state = gtd_widget_set_final_state;
+  iface->get_widget = gtd_widget_get_widget;
 }
 
 
@@ -150,6 +716,7 @@ gtd_widget_dispose (GObject *object)
     }
 
   invalidate_cached_transform (self);
+  gtd_widget_remove_all_transitions (self);
 
   G_OBJECT_CLASS (gtd_widget_parent_class)->dispose (object);
 }
@@ -386,6 +953,50 @@ gtd_widget_class_init (GtdWidgetClass *klass)
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
+  /**
+   * GtdWidget::transitions-completed:
+   * @actor: a #GtdWidget
+   *
+   * The ::transitions-completed signal is emitted once all transitions
+   * involving @actor are complete.
+   *
+   * Since: 1.10
+   */
+  signals[TRANSITIONS_COMPLETED] =
+    g_signal_new ("transitions-completed",
+                  G_TYPE_FROM_CLASS (object_class),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  0);
+
+  /**
+   * GtdWidget::transition-stopped:
+   * @actor: a #GtdWidget
+   * @name: the name of the transition
+   * @is_finished: whether the transition was finished, or stopped
+   *
+   * The ::transition-stopped signal is emitted once a transition
+   * is stopped; a transition is stopped once it reached its total
+   * duration (including eventual repeats), it has been stopped
+   * using gtd_timeline_stop(), or it has been removed from the
+   * transitions applied on @actor, using gtd_actor_remove_transition().
+   *
+   * Since: 1.12
+   */
+  signals[TRANSITION_STOPPED] =
+    g_signal_new ("transition-stopped",
+                  G_TYPE_FROM_CLASS (object_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE |
+                  G_SIGNAL_NO_HOOKS | G_SIGNAL_DETAILED,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  2,
+                  G_TYPE_STRING,
+                  G_TYPE_BOOLEAN);
+
   gtk_widget_class_set_layout_manager_type (widget_class, GTD_TYPE_BIN_LAYOUT);
 }
 
@@ -399,6 +1010,9 @@ gtd_widget_init (GtdWidget *self)
   priv->scale[Z] = 1.f;
 
   priv->pivot_point = GRAPHENE_POINT3D_INIT (0.5, 0.5, 0.f);
+
+  gtd_widget_save_easing_state (self);
+  gtd_widget_set_easing_duration (self, 0);
 }
 
 GtkWidget*
@@ -498,22 +1112,14 @@ gtd_widget_set_rotation (GtdWidget *self,
   if (!changed[X] && !changed[Y] && !changed[Z])
     GTD_RETURN ();
 
-  invalidate_cached_transform (self);
-
-  priv->rotation[X] = rotation_x;
-  priv->rotation[Y] = rotation_y;
-  priv->rotation[Z] = rotation_z;
-
   if (changed[X])
-    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ROTATION_X]);
+    create_transition (self, properties[PROP_ROTATION_X], priv->rotation[X], rotation_x);
 
   if (changed[Y])
-    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ROTATION_Y]);
+    create_transition (self, properties[PROP_ROTATION_Y], priv->rotation[Y], rotation_y);
 
   if (changed[Z])
-    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ROTATION_Z]);
-
-  gtk_widget_queue_resize (GTK_WIDGET (self));
+    create_transition (self, properties[PROP_ROTATION_Z], priv->rotation[Z], rotation_z);
 
   GTD_EXIT;
 }
@@ -566,22 +1172,14 @@ gtd_widget_set_scale (GtdWidget *self,
   if (!changed[X] && !changed[Y] && !changed[Z])
     GTD_RETURN ();
 
-  invalidate_cached_transform (self);
-
-  priv->scale[X] = scale_x;
-  priv->scale[Y] = scale_y;
-  priv->scale[Z] = scale_z;
-
   if (changed[X])
-    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SCALE_X]);
+    create_transition (self, properties[PROP_SCALE_X], priv->scale[X], scale_x);
 
   if (changed[Y])
-    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SCALE_Y]);
+    create_transition (self, properties[PROP_SCALE_Y], priv->scale[Y], scale_y);
 
   if (changed[Z])
-    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SCALE_Z]);
-
-  gtk_widget_queue_resize (GTK_WIDGET (self));
+    create_transition (self, properties[PROP_SCALE_Z], priv->scale[Z], scale_z);
 
   GTD_EXIT;
 }
@@ -618,7 +1216,7 @@ gtd_widget_set_translation (GtdWidget *self,
                             gfloat     translation_y,
                             gfloat     translation_z)
 {
-  graphene_point3d_t old_translation, translation;
+  graphene_point3d_t translation;
   GtdWidgetPrivate *priv;
 
   GTD_ENTRY;
@@ -631,21 +1229,14 @@ gtd_widget_set_translation (GtdWidget *self,
   if (graphene_point3d_equal (&priv->translation, &translation))
     GTD_RETURN ();
 
-  old_translation = priv->translation;
-
-  invalidate_cached_transform (self);
-  priv->translation = translation;
-
-  if (!G_APPROX_VALUE (old_translation.x, translation.x, FLT_EPSILON))
-    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TRANSLATION_X]);
-
-  if (!G_APPROX_VALUE (old_translation.y, translation.y, FLT_EPSILON))
-    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TRANSLATION_Y]);
+  if (!G_APPROX_VALUE (priv->translation.x, translation.x, FLT_EPSILON))
+    create_transition (self, properties[PROP_TRANSLATION_X], priv->translation.x, translation_x);
 
-  if (!G_APPROX_VALUE (old_translation.y, translation.y, FLT_EPSILON))
-    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TRANSLATION_Z]);
+  if (!G_APPROX_VALUE (priv->translation.y, translation.y, FLT_EPSILON))
+    create_transition (self, properties[PROP_TRANSLATION_Y], priv->translation.y, translation_y);
 
-  gtk_widget_queue_resize (GTK_WIDGET (self));
+  if (!G_APPROX_VALUE (priv->translation.y, translation.y, FLT_EPSILON))
+    create_transition (self, properties[PROP_TRANSLATION_Z], priv->translation.z, translation_z);
 
   GTD_EXIT;
 }
@@ -686,3 +1277,434 @@ gtd_widget_update_pivot_for_geometry (GtdWidget           *self,
       priv->geometry = *geometry;
     }
 }
+
+/**
+ * gtd_widget_add_transition:
+ * @self: a #GtdWidget
+ * @name: the name of the transition to add
+ * @transition: the #GtdTransition to add
+ *
+ * Adds a @transition to the #GtdWidget's list of animations.
+ *
+ * The @name string is a per-widget unique identifier of the @transition: only
+ * one #GtdTransition can be associated to the specified @name.
+ *
+ * The @transition will be started once added.
+ *
+ * This function will take a reference on the @transition.
+ *
+ * This function is usually called implicitly when modifying an animatable
+ * property.
+ *
+ * Since: 1.10
+ */
+void
+gtd_widget_add_transition (GtdWidget     *self,
+                           const gchar   *name,
+                           GtdTransition *transition)
+{
+  g_return_if_fail (GTD_IS_WIDGET (self));
+  g_return_if_fail (name != NULL);
+  g_return_if_fail (GTD_IS_TRANSITION (transition));
+
+  add_transition_to_widget (self, name, transition);
+}
+
+/**
+ * gtd_widget_remove_transition:
+ * @self: a #GtdWidget
+ * @name: the name of the transition to remove
+ *
+ * Removes the transition stored inside a #GtdWidget using @name
+ * identifier.
+ *
+ * If the transition is currently in progress, it will be stopped.
+ *
+ * This function releases the reference acquired when the transition
+ * was added to the #GtdWidget.
+ *
+ * Since: 1.10
+ */
+void
+gtd_widget_remove_transition (GtdWidget   *self,
+                              const gchar *name)
+{
+  GtdWidgetPrivate *priv;
+  TransitionClosure *closure;
+  gboolean was_playing;
+  GQuark t_quark;
+  gchar *t_name;
+
+  g_return_if_fail (GTD_IS_WIDGET (self));
+  g_return_if_fail (name != NULL);
+
+  priv = gtd_widget_get_instance_private (self);
+
+  if (priv->animation.transitions == NULL)
+    return;
+
+  closure = g_hash_table_lookup (priv->animation.transitions, name);
+  if (closure == NULL)
+    return;
+
+  was_playing =
+    gtd_timeline_is_playing (GTD_TIMELINE (closure->transition));
+  t_quark = g_quark_from_string (closure->name);
+  t_name = g_strdup (closure->name);
+
+  g_hash_table_remove (priv->animation.transitions, name);
+
+  /* we want to maintain the invariant that ::transition-stopped is
+   * emitted after the transition has been removed, to allow replacing
+   * or chaining; removing the transition from the hash table will
+   * stop it, but transition_closure_free() will disconnect the signal
+   * handler we install in add_transition_internal(), to avoid loops
+   * or segfaults.
+   *
+   * since we know already that a transition will stop once it's removed
+   * from an widget, we can simply emit the ::transition-stopped here
+   * ourselves, if the timeline was playing (if it wasn't, then the
+   * signal was already emitted at least once).
+   */
+  if (was_playing)
+    g_signal_emit (self, signals[TRANSITION_STOPPED], t_quark, t_name, FALSE);
+
+  g_free (t_name);
+}
+
+/**
+ * gtd_widget_remove_all_transitions:
+ * @self: a #GtdWidget
+ *
+ * Removes all transitions associated to @self.
+ *
+ * Since: 1.10
+ */
+void
+gtd_widget_remove_all_transitions (GtdWidget *self)
+{
+  GtdWidgetPrivate *priv;
+
+  g_return_if_fail (GTD_IS_WIDGET (self));
+
+  priv = gtd_widget_get_instance_private (self);
+  if (priv->animation.transitions == NULL)
+    return;
+
+  g_hash_table_remove_all (priv->animation.transitions);
+}
+
+/**
+ * gtd_widget_set_easing_duration:
+ * @self: a #GtdWidget
+ * @msecs: the duration of the easing, or %NULL
+ *
+ * Sets the duration of the tweening for animatable properties
+ * of @self for the current easing state.
+ *
+ * Since: 1.10
+ */
+void
+gtd_widget_set_easing_duration (GtdWidget *self,
+                                guint      msecs)
+{
+  GtdWidgetPrivate *priv;
+
+  g_return_if_fail (GTD_IS_WIDGET (self));
+
+  priv = gtd_widget_get_instance_private (self);
+
+  if (priv->animation.current_state == NULL)
+    {
+      g_warning ("You must call gtd_widget_save_easing_state() prior "
+                 "to calling gtd_widget_set_easing_duration().");
+      return;
+    }
+
+  if (priv->animation.current_state->easing_duration != msecs)
+    priv->animation.current_state->easing_duration = msecs;
+}
+
+/**
+ * gtd_widget_get_easing_duration:
+ * @self: a #GtdWidget
+ *
+ * Retrieves the duration of the tweening for animatable
+ * properties of @self for the current easing state.
+ *
+ * Return value: the duration of the tweening, in milliseconds
+ *
+ * Since: 1.10
+ */
+guint
+gtd_widget_get_easing_duration (GtdWidget *self)
+{
+  GtdWidgetPrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_WIDGET (self), 0);
+
+  priv = gtd_widget_get_instance_private (self);
+  if (priv->animation.current_state != NULL)
+    return priv->animation.current_state->easing_duration;
+
+  return 0;
+}
+
+/**
+ * gtd_widget_set_easing_mode:
+ * @self: a #GtdWidget
+ * @mode: an easing mode, excluding %GTD_CUSTOM_MODE
+ *
+ * Sets the easing mode for the tweening of animatable properties
+ * of @self.
+ *
+ * Since: 1.10
+ */
+void
+gtd_widget_set_easing_mode (GtdWidget   *self,
+                            GtdEaseMode  mode)
+{
+  GtdWidgetPrivate *priv;
+
+  g_return_if_fail (GTD_IS_WIDGET (self));
+  g_return_if_fail (mode != GTD_CUSTOM_MODE);
+  g_return_if_fail (mode < GTD_EASE_LAST);
+
+  priv = gtd_widget_get_instance_private (self);
+  if (priv->animation.current_state == NULL)
+    {
+      g_warning ("You must call gtd_widget_save_easing_state() prior "
+                 "to calling gtd_widget_set_easing_mode().");
+      return;
+    }
+
+  if (priv->animation.current_state->easing_mode != mode)
+    priv->animation.current_state->easing_mode = mode;
+}
+
+/**
+ * gtd_widget_get_easing_mode:
+ * @self: a #GtdWidget
+ *
+ * Retrieves the easing mode for the tweening of animatable properties
+ * of @self for the current easing state.
+ *
+ * Return value: an easing mode
+ *
+ * Since: 1.10
+ */
+GtdEaseMode
+gtd_widget_get_easing_mode (GtdWidget *self)
+{
+  GtdWidgetPrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_WIDGET (self), GTD_EASE_OUT_CUBIC);
+
+  priv = gtd_widget_get_instance_private (self);
+
+  if (priv->animation.current_state != NULL)
+    return priv->animation.current_state->easing_mode;
+
+  return GTD_EASE_OUT_CUBIC;
+}
+
+/**
+ * gtd_widget_set_easing_delay:
+ * @self: a #GtdWidget
+ * @msecs: the delay before the start of the tweening, in milliseconds
+ *
+ * Sets the delay that should be applied before tweening animatable
+ * properties.
+ *
+ * Since: 1.10
+ */
+void
+gtd_widget_set_easing_delay (GtdWidget *self,
+                             guint      msecs)
+{
+  GtdWidgetPrivate *priv;
+
+  g_return_if_fail (GTD_IS_WIDGET (self));
+
+  priv = gtd_widget_get_instance_private (self);
+
+  if (priv->animation.current_state == NULL)
+    {
+      g_warning ("You must call gtd_widget_save_easing_state() prior "
+                 "to calling gtd_widget_set_easing_delay().");
+      return;
+    }
+
+  if (priv->animation.current_state->easing_delay != msecs)
+    priv->animation.current_state->easing_delay = msecs;
+}
+
+/**
+ * gtd_widget_get_easing_delay:
+ * @self: a #GtdWidget
+ *
+ * Retrieves the delay that should be applied when tweening animatable
+ * properties.
+ *
+ * Return value: a delay, in milliseconds
+ *
+ * Since: 1.10
+ */
+guint
+gtd_widget_get_easing_delay (GtdWidget *self)
+{
+  GtdWidgetPrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_WIDGET (self), 0);
+
+  priv = gtd_widget_get_instance_private (self);
+
+  if (priv->animation.current_state != NULL)
+    return priv->animation.current_state->easing_delay;
+
+  return 0;
+}
+
+/**
+ * gtd_widget_get_transition:
+ * @self: a #GtdWidget
+ * @name: the name of the transition
+ *
+ * Retrieves the #GtdTransition of a #GtdWidget by using the
+ * transition @name.
+ *
+ * Transitions created for animatable properties use the name of the
+ * property itself, for instance the code below:
+ *
+ * |[<!-- language="C" -->
+ *   gtd_widget_set_easing_duration (widget, 1000);
+ *   gtd_widget_set_rotation_angle (widget, GTD_Y_AXIS, 360.0);
+ *
+ *   transition = gtd_widget_get_transition (widget, "rotation-angle-y");
+ *   g_signal_connect (transition, "stopped",
+ *                     G_CALLBACK (on_transition_stopped),
+ *                     widget);
+ * ]|
+ *
+ * will call the `on_transition_stopped` callback when the transition
+ * is finished.
+ *
+ * If you just want to get notifications of the completion of a transition,
+ * you should use the #GtdWidget::transition-stopped signal, using the
+ * transition name as the signal detail.
+ *
+ * Return value: (transfer none): a #GtdTransition, or %NULL is none
+ *   was found to match the passed name; the returned instance is owned
+ *   by Gtd and it should not be freed
+ */
+GtdTransition *
+gtd_widget_get_transition (GtdWidget   *self,
+                           const gchar *name)
+{
+  TransitionClosure *closure;
+  GtdWidgetPrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_WIDGET (self), NULL);
+  g_return_val_if_fail (name != NULL, NULL);
+
+  priv = gtd_widget_get_instance_private (self);
+  if (priv->animation.transitions == NULL)
+    return NULL;
+
+  closure = g_hash_table_lookup (priv->animation.transitions, name);
+  if (closure == NULL)
+    return NULL;
+
+  return closure->transition;
+}
+
+/**
+ * gtd_widget_has_transitions: (skip)
+ */
+gboolean
+gtd_widget_has_transitions (GtdWidget *self)
+{
+  GtdWidgetPrivate *priv;
+
+  g_return_val_if_fail (GTD_IS_WIDGET (self), FALSE);
+
+  priv = gtd_widget_get_instance_private (self);
+  if (priv->animation.transitions == NULL)
+    return FALSE;
+
+  return g_hash_table_size (priv->animation.transitions) > 0;
+}
+
+/**
+ * gtd_widget_save_easing_state:
+ * @self: a #GtdWidget
+ *
+ * Saves the current easing state for animatable properties, and creates
+ * a new state with the default values for easing mode and duration.
+ *
+ * New transitions created after calling this function will inherit the
+ * duration, easing mode, and delay of the new easing state; this also
+ * applies to transitions modified in flight.
+ */
+void
+gtd_widget_save_easing_state (GtdWidget *self)
+{
+  GtdWidgetPrivate *priv;
+  AnimationState new_state;
+
+  g_return_if_fail (GTD_IS_WIDGET (self));
+
+  priv = gtd_widget_get_instance_private (self);
+
+  if (priv->animation.states == NULL)
+    priv->animation.states = g_array_new (FALSE, FALSE, sizeof (AnimationState));
+
+  new_state.easing_mode = GTD_EASE_OUT_CUBIC;
+  new_state.easing_duration = 250;
+  new_state.easing_delay = 0;
+
+  g_array_append_val (priv->animation.states, new_state);
+
+  priv->animation.current_state = &g_array_index (priv->animation.states,
+                                    AnimationState,
+                                    priv->animation.states->len - 1);
+}
+
+/**
+ * gtd_widget_restore_easing_state:
+ * @self: a #GtdWidget
+ *
+ * Restores the easing state as it was prior to a call to
+ * gtd_widget_save_easing_state().
+ *
+ * Since: 1.10
+ */
+void
+gtd_widget_restore_easing_state (GtdWidget *self)
+{
+  GtdWidgetPrivate *priv;
+
+  g_return_if_fail (GTD_IS_WIDGET (self));
+
+  priv = gtd_widget_get_instance_private (self);
+
+  if (priv->animation.states == NULL)
+    {
+      g_critical ("The function gtd_widget_restore_easing_state() has "
+                  "been called without a previous call to "
+                  "gtd_widget_save_easing_state().");
+      return;
+    }
+
+  g_array_remove_index (priv->animation.states, priv->animation.states->len - 1);
+
+  if (priv->animation.states->len > 0)
+    priv->animation.current_state = &g_array_index (priv->animation.states, AnimationState, 
priv->animation.states->len - 1);
+  else
+    {
+      g_array_unref (priv->animation.states);
+      priv->animation.states = NULL;
+      priv->animation.current_state = NULL;
+    }
+}
+
diff --git a/src/gui/gtd-widget.h b/src/gui/gtd-widget.h
index 63d721e..c7b4ad2 100644
--- a/src/gui/gtd-widget.h
+++ b/src/gui/gtd-widget.h
@@ -22,6 +22,9 @@
 
 #include <gtk/gtk.h>
 
+#include "gtd-animation-enums.h"
+#include "gtd-types.h"
+
 G_BEGIN_DECLS
 
 #define GTD_TYPE_WIDGET (gtd_widget_get_type ())
@@ -73,4 +76,35 @@ void                 gtd_widget_set_translation                  (GtdWidget
 GskTransform*        gtd_widget_apply_transform                  (GtdWidget          *self,
                                                                   GskTransform       *transform);
 
+void                 gtd_widget_save_easing_state                 (GtdWidget         *self);
+
+void                 gtd_widget_restore_easing_state              (GtdWidget         *self);
+
+void                 gtd_widget_set_easing_mode                   (GtdWidget         *self,
+                                                                   GtdEaseMode        mode);
+
+GtdEaseMode          gtd_widget_get_easing_mode                   (GtdWidget         *self);
+
+void                 gtd_widget_set_easing_duration               (GtdWidget         *self,
+                                                                   guint              msecs);
+
+guint                gtd_widget_get_easing_duration               (GtdWidget         *self);
+
+void                 gtd_widget_set_easing_delay                  (GtdWidget         *self,
+                                                                   guint              msecs);
+
+guint                gtd_widget_get_easing_delay                  (GtdWidget         *self);
+
+GtdTransition*       gtd_widget_get_transition                    (GtdWidget         *self,
+                                                                   const gchar       *name);
+
+void                 gtd_widget_add_transition                    (GtdWidget         *self,
+                                                                   const gchar       *name,
+                                                                   GtdTransition     *transition);
+
+void                 gtd_widget_remove_transition                 (GtdWidget         *self,
+                                                                   const gchar       *name);
+
+void                 gtd_widget_remove_all_transitions            (GtdWidget         *self);
+
 G_END_DECLS
diff --git a/src/meson.build b/src/meson.build
index 309f471..bea40f5 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -22,6 +22,7 @@ gnome_todo_deps += vcs_identifier_h
 ###########
 
 incs = include_directories(
+  'animation',
   'core',
   'gui',
   'models',
@@ -43,11 +44,37 @@ gtd_deps = gnome_todo_deps + [
 ]
 
 
+#########
+# Enums #
+#########
+
+enum_headers = files(
+  join_paths('animation', 'gtd-animation-enums.h'),
+)
+
+enum_types = 'gtd-enum-types'
+
+sources += gnome.mkenums(
+  enum_types,
+     sources: enum_headers,
+  c_template: enum_types + '.c.template',
+  h_template: enum_types + '.h.template'
+)
+
+
 ################
 # Header files #
 ################
 
 headers = files(
+  'animation/gtd-animatable.h',
+  'animation/gtd-animation-utils.h',
+  'animation/gtd-easing.h',
+  'animation/gtd-interval.h',
+  'animation/gtd-keyframe-transition.h',
+  'animation/gtd-property-transition.h',
+  'animation/gtd-timeline.h',
+  'animation/gtd-transition.h',
   'core/gtd-activatable.h',
   'core/gtd-clock.h',
   'core/gtd-manager.h',
@@ -83,6 +110,14 @@ install_headers(headers, subdir: meson.project_name())
 ################
 
 sources += files(
+  'animation/gtd-animatable.c',
+  'animation/gtd-animation-utils.c',
+  'animation/gtd-easing.c',
+  'animation/gtd-interval.c',
+  'animation/gtd-keyframe-transition.c',
+  'animation/gtd-property-transition.c',
+  'animation/gtd-timeline.c',
+  'animation/gtd-transition.c',
   'core/gtd-activatable.c',
   'core/gtd-clock.c',
   'core/gtd-log.c',
diff --git a/tests/interactive/test-animation.c b/tests/interactive/test-animation.c
new file mode 100644
index 0000000..1fd2253
--- /dev/null
+++ b/tests/interactive/test-animation.c
@@ -0,0 +1,272 @@
+/* test-widget.c
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "gtd-keyframe-transition.h"
+#include "gtd-widget.h"
+
+static const char *css =
+"translated {"
+"  background-image: none;"
+"  background-color: red;"
+"}\n"
+"wiggler {"
+"  background-image: none;"
+"  background-color: green;"
+"}\n"
+"rotated {"
+"  background-image: none;"
+"  background-color: blue;"
+"}\n"
+"mover {"
+"  background-image: none;"
+"  background-color: pink;"
+"}\n"
+;
+
+static const char *ui =
+"<interface>"
+"  <object class='GtkWindow' id='window'>"
+"    <property name='default-width'>600</property>"
+"    <property name='default-height'>400</property>"
+"    <child>"
+"      <object class='GtkBox'>"
+"        <child>"
+"          <object class='GtdWidget'>"
+"            <property name='hexpand'>true</property>"
+"            <child>"
+"              <object class='GtdWidget' id='translated'>"
+"                <property name='css-name'>translated</property>"
+"                <property name='halign'>center</property>"
+"                <property name='valign'>center</property>"
+"                <property name='width-request'>30</property>"
+"                <property name='height-request'>30</property>"
+"              </object>"
+"            </child>"
+"            <child>"
+"              <object class='GtdWidget' id='wiggler'>"
+"                <property name='css-name'>wiggler</property>"
+"                <property name='translation-y'>40</property>"
+"                <property name='halign'>center</property>"
+"                <property name='valign'>start</property>"
+"                <property name='width-request'>300</property>"
+"                <property name='height-request'>40</property>"
+"              </object>"
+"            </child>"
+"            <child>"
+"              <object class='GtdWidget' id='rotated'>"
+"                <property name='css-name'>rotated</property>"
+"                <property name='translation-y'>-80</property>"
+"                <property name='halign'>center</property>"
+"                <property name='valign'>end</property>"
+"                <property name='width-request'>40</property>"
+"                <property name='height-request'>40</property>"
+"              </object>"
+"            </child>"
+"            <child>"
+"              <object class='GtdWidget' id='mover'>"
+"                <property name='css-name'>mover</property>"
+"                <property name='translation-x'>-200</property>"
+"                <property name='translation-y'>-40</property>"
+"                <property name='halign'>center</property>"
+"                <property name='valign'>end</property>"
+"                <property name='width-request'>50</property>"
+"                <property name='height-request'>50</property>"
+"              </object>"
+"            </child>"
+"          </object>"
+"        </child>"
+"        <child>"
+"          <object class='GtkButton' id='button'>"
+"            <property name='label'>Move</property>"
+"            <property name='valign'>start</property>"
+"            <property name='margin-top'>12</property>"
+"            <property name='margin-start'>12</property>"
+"            <property name='margin-end'>12</property>"
+"            <property name='margin-bottom'>12</property>"
+"          </object>"
+"        </child>"
+"      </object>"
+"    </child>"
+"  </object>"
+"</interface>";
+
+static gboolean pink_moved = FALSE;
+
+static void
+animate_rotation (GtdWidget *widget)
+{
+  GtdTransition *rotation_z;
+  GtdTransition *scale_x;
+  GtdTransition *scale_y;
+
+  rotation_z = gtd_property_transition_new ("rotation-z");
+  gtd_transition_set_from (rotation_z, G_TYPE_FLOAT, 0.f);
+  gtd_transition_set_to (rotation_z, G_TYPE_FLOAT, 360.f);
+  gtd_timeline_set_duration (GTD_TIMELINE (rotation_z), 750);
+  gtd_timeline_set_repeat_count (GTD_TIMELINE (rotation_z), -1);
+  gtd_timeline_set_auto_reverse (GTD_TIMELINE (rotation_z), TRUE);
+
+  scale_x = gtd_property_transition_new ("scale-x");
+  gtd_transition_set_from (scale_x, G_TYPE_FLOAT, 1.f);
+  gtd_transition_set_to (scale_x, G_TYPE_FLOAT, 2.f);
+  gtd_timeline_set_duration (GTD_TIMELINE (scale_x), 750);
+  gtd_timeline_set_repeat_count (GTD_TIMELINE (scale_x), -1);
+  gtd_timeline_set_auto_reverse (GTD_TIMELINE (scale_x), TRUE);
+
+  scale_y = gtd_property_transition_new ("scale-y");
+  gtd_transition_set_from (scale_y, G_TYPE_FLOAT, 1.f);
+  gtd_transition_set_to (scale_y, G_TYPE_FLOAT, 2.f);
+  gtd_timeline_set_duration (GTD_TIMELINE (scale_y), 750);
+  gtd_timeline_set_repeat_count (GTD_TIMELINE (scale_y), -1);
+  gtd_timeline_set_auto_reverse (GTD_TIMELINE (scale_y), TRUE);
+
+  gtd_widget_add_transition (widget, "loop-rotation-z", rotation_z);
+  gtd_widget_add_transition (widget, "loop-scale-x", scale_x);
+  gtd_widget_add_transition (widget, "loop-scale-y", scale_y);
+}
+
+static void
+animate_translation (GtdWidget *widget)
+{
+  GtdTransition *transition_x;
+
+  transition_x = gtd_property_transition_new ("translation-x");
+  gtd_transition_set_from (transition_x, G_TYPE_FLOAT, -200.f);
+  gtd_transition_set_to (transition_x, G_TYPE_FLOAT, 200.f);
+  gtd_timeline_set_duration (GTD_TIMELINE (transition_x), 2000);
+  gtd_timeline_set_repeat_count (GTD_TIMELINE (transition_x), -1);
+  gtd_timeline_set_auto_reverse (GTD_TIMELINE (transition_x), TRUE);
+
+  gtd_widget_add_transition (widget, "loop-translation-x", transition_x);
+}
+
+static void
+animate_wiggle (GtdWidget *widget)
+{
+  GtdTransition *transition_x;
+
+  g_message ("Adding wiggle");
+
+  gtd_widget_remove_all_transitions (widget);
+
+  transition_x = gtd_keyframe_transition_new ("translation-x");
+  gtd_transition_set_from (transition_x, G_TYPE_FLOAT, 0.f);
+  gtd_transition_set_to (transition_x, G_TYPE_FLOAT, 0.f);
+  gtd_timeline_set_duration (GTD_TIMELINE (transition_x), 350);
+  gtd_timeline_set_delay (GTD_TIMELINE (transition_x), 1000);
+  gtd_keyframe_transition_set (GTD_KEYFRAME_TRANSITION (transition_x),
+                               G_TYPE_FLOAT,
+                               5,
+                               0.20, -15.f, GTD_EASE_OUT_QUAD,
+                               0.40,  15.f, GTD_EASE_LINEAR,
+                               0.60, -15.f, GTD_EASE_LINEAR,
+                               0.80,  15.f, GTD_EASE_LINEAR,
+                               1.00,   0.f, GTD_EASE_IN_QUAD);
+
+  gtd_widget_add_transition (widget, "wiggle", transition_x);
+
+  g_signal_connect_swapped (transition_x,
+                            "completed",
+                            G_CALLBACK (animate_wiggle),
+                            widget);
+}
+
+static void
+move_pink_cb (GtkButton *button,
+              GtdWidget *widget)
+{
+  GtdTransition *rotation_y;
+
+  gtd_widget_remove_all_transitions (widget);
+
+  rotation_y = gtd_property_transition_new ("rotation-y");
+  gtd_transition_set_from (rotation_y, G_TYPE_FLOAT, 0.f);
+  gtd_transition_set_to (rotation_y, G_TYPE_FLOAT, 360.f);
+  gtd_timeline_set_duration (GTD_TIMELINE (rotation_y), 500);
+  gtd_timeline_set_repeat_count (GTD_TIMELINE (rotation_y), 3);
+
+  gtd_widget_save_easing_state (widget);
+  gtd_widget_set_easing_duration (widget, 2000);
+  gtd_widget_set_easing_mode (widget, GTD_EASE_LINEAR);
+  gtd_widget_set_translation (widget, pink_moved ? -200.f : 200.f, -40.f, 0.f);
+  gtd_widget_restore_easing_state (widget);
+
+  gtd_widget_add_transition (widget, "loop-rotation-y", rotation_y);
+
+  pink_moved = !pink_moved;
+}
+
+static GtkWidget *
+create_ui (void)
+{
+  g_autoptr (GtkBuilder) builder = NULL;
+  g_autoptr (GError) error = NULL;
+  GtkWidget *win;
+
+  g_type_ensure (GTD_TYPE_WIDGET);
+
+  builder = gtk_builder_new ();
+  if (!gtk_builder_add_from_string (builder, ui, -1, &error))
+    {
+      g_warning ("%s", error->message);
+      return NULL;
+    }
+
+  win = (GtkWidget *)gtk_builder_get_object (builder, "window");
+  g_object_ref (win);
+
+  animate_rotation ((GtdWidget *)gtk_builder_get_object (builder, "rotated"));
+  animate_translation ((GtdWidget *)gtk_builder_get_object (builder, "translated"));
+  animate_wiggle ((GtdWidget *)gtk_builder_get_object (builder, "wiggler"));
+
+  g_signal_connect (gtk_builder_get_object (builder, "button"),
+                    "clicked",
+                    G_CALLBACK (move_pink_cb),
+                    gtk_builder_get_object (builder, "mover"));
+
+  return win;
+}
+
+gint
+main (gint   argc,
+      gchar *argv[])
+{
+  g_autoptr (GtkCssProvider) css_provider = NULL;
+  GtkWindow *window;
+
+  g_set_prgname ("test-colorbutton");
+  g_set_application_name ("GNOME To Do | Widget Test");
+
+  gtk_init ();
+
+  css_provider = gtk_css_provider_new ();
+  gtk_css_provider_load_from_data (css_provider, css, -1);
+  gtk_style_context_add_provider_for_display (gdk_display_get_default (),
+                                              GTK_STYLE_PROVIDER (css_provider),
+                                              GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+  window = GTK_WINDOW (create_ui ());
+  gtk_window_present (window);
+
+  while (TRUE)
+    g_main_context_iteration (NULL, TRUE);
+
+  return 0;
+}
diff --git a/tests/meson.build b/tests/meson.build
index 1b6e03c..a6d1d72 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -75,6 +75,7 @@ endforeach
 #####################
 
 interactive_tests = [
+  'test-animation',
   'test-colorbutton',
   'test-filter-sort',
   'test-star-widget',



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]