summaryrefslogtreecommitdiff
path: root/src/animation
diff options
context:
space:
mode:
Diffstat (limited to 'src/animation')
-rw-r--r--src/animation/gtd-animatable.c204
-rw-r--r--src/animation/gtd-animatable.h85
-rw-r--r--src/animation/gtd-animation-enums.h173
-rw-r--r--src/animation/gtd-animation-utils.c168
-rw-r--r--src/animation/gtd-animation-utils.h65
-rw-r--r--src/animation/gtd-easing.c474
-rw-r--r--src/animation/gtd-easing.h141
-rw-r--r--src/animation/gtd-interval.c1134
-rw-r--r--src/animation/gtd-interval.h116
-rw-r--r--src/animation/gtd-keyframe-transition.c716
-rw-r--r--src/animation/gtd-keyframe-transition.h83
-rw-r--r--src/animation/gtd-property-transition.c359
-rw-r--r--src/animation/gtd-property-transition.h55
-rw-r--r--src/animation/gtd-timeline.c1547
-rw-r--r--src/animation/gtd-timeline.h150
-rw-r--r--src/animation/gtd-transition.c655
-rw-r--r--src/animation/gtd-transition.h85
17 files changed, 6210 insertions, 0 deletions
diff --git a/src/animation/gtd-animatable.c b/src/animation/gtd-animatable.c
new file mode 100644
index 0000000..b068b31
--- /dev/null
+++ b/src/animation/gtd-animatable.c
@@ -0,0 +1,204 @@
+/* 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.
+ */
+
+#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) (nullable): The #GParamSpec for the given property
+ */
+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
+ */
+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
+ */
+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
+ */
+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..ed26f8e
--- /dev/null
+++ b/src/animation/gtd-animation-enums.h
@@ -0,0 +1,173 @@
+/* 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_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,
+
+ /* 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;
+
+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..7ab2198
--- /dev/null
+++ b/src/animation/gtd-animation-utils.c
@@ -0,0 +1,168 @@
+/* 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))
+ {
+ res = FALSE;
+ goto out;
+ }
+
+ pdata = g_hash_table_lookup (progress_funcs, g_type_name (gtype));
+ if (G_UNLIKELY (!pdata))
+ {
+ 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_free (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))
+ 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_free (progress_func);
+ }
+ else
+ {
+ progress_func->func = func;
+ }
+ }
+ else
+ {
+ progress_func = g_new0 (ProgressData, 1);
+ 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..c674f91
--- /dev/null
+++ b/src/animation/gtd-easing.c
@@ -0,0 +1,474 @@
+/* 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;
+}
+
+/*< 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" },
+
+ { 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..b83bb7f
--- /dev/null
+++ b/src/animation/gtd-easing.h
@@ -0,0 +1,141 @@
+/* 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);
+
+G_END_DECLS
diff --git a/src/animation/gtd-interval.c b/src/animation/gtd-interval.c
new file mode 100644
index 0000000..20efd82
--- /dev/null
+++ b/src/animation/gtd-interval.c
@@ -0,0 +1,1134 @@
+/* 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.
+ */
+ 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.
+ */
+ 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.
+ */
+ 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
+ */
+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
+ */
+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
+ */
+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
+ */
+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.
+ */
+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.
+ */
+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.
+ */
+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
+ */
+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.
+ */
+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.
+ */
+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.
+ */
+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
+ */
+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.
+ */
+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.
+ */
+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
+ */
+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
+ */
+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
+ */
+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
+ */
+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..a77fea2
--- /dev/null
+++ b/src/animation/gtd-keyframe-transition.c
@@ -0,0 +1,716 @@
+/* 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.
+ */
+
+#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,
+ (gint) 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 == (gint) 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.
+ */
+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.
+ */
+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.
+ */
+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
+ */
+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.
+ */
+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.
+ */
+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..d3d5024
--- /dev/null
+++ b/src/animation/gtd-keyframe-transition.h
@@ -0,0 +1,83 @@
+/* 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..f0ceb88
--- /dev/null
+++ b/src/animation/gtd-property-transition.c
@@ -0,0 +1,359 @@
+/* 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.
+ */
+
+#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.
+ */
+ 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
+ */
+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.
+ */
+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.
+ */
+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.c b/src/animation/gtd-timeline.c
new file mode 100644
index 0000000..da16fd9
--- /dev/null
+++ b/src/animation/gtd-timeline.c
@@ -0,0 +1,1547 @@
+/*
+ * 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 "endeavour.h"
+
+typedef struct
+{
+ GtdTimelineDirection direction;
+
+ GtdWidget *widget;
+ guint frame_tick_id;
+
+ gint64 duration_us;
+ gint64 delay_us;
+
+ /* The current amount of elapsed time */
+ gint64 elapsed_time_us;
+
+ /* The elapsed time since the last frame was fired */
+ gint64 delta_us;
+
+ /* Time we last advanced the elapsed time and showed a frame */
+ gint64 last_frame_time_us;
+
+ gint64 start_us;
+
+ /* 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;
+
+ guint is_playing : 1;
+
+ /* If we've just started playing and haven't yet gotten
+ * a tick from the master clock
+ */
+ guint auto_reverse : 1;
+} GtdTimelinePrivate;
+
+enum
+{
+ PROP_0,
+
+ PROP_AUTO_REVERSE,
+ PROP_DELAY,
+ PROP_DURATION,
+ PROP_DIRECTION,
+ PROP_REPEAT_COUNT,
+ PROP_PROGRESS_MODE,
+ PROP_WIDGET,
+
+ PROP_LAST
+};
+
+static GParamSpec *obj_props[PROP_LAST] = { NULL, };
+
+enum
+{
+ NEW_FRAME,
+ STARTED,
+ PAUSED,
+ COMPLETED,
+ STOPPED,
+
+ LAST_SIGNAL
+};
+
+static guint timeline_signals[LAST_SIGNAL] = { 0, };
+
+static void set_is_playing (GtdTimeline *self,
+ gboolean is_playing);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GtdTimeline, gtd_timeline, G_TYPE_OBJECT)
+
+static inline gint64
+us_to_ms (gint64 ms)
+{
+ return ms / 1000;
+}
+
+static inline gint64
+ms_to_us (gint64 us)
+{
+ return us * 1000;
+}
+
+static inline gboolean
+is_waiting_for_delay (GtdTimeline *self,
+ gint64 frame_time_us)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+ return priv->start_us + priv->delay_us > frame_time_us;
+}
+
+static void
+emit_frame_signal (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+ gint64 elapsed_time_ms = us_to_ms (priv->elapsed_time_us);
+
+ GTD_TRACE_MSG ("Emitting ::new-frame signal on timeline[%p]", self);
+
+ g_signal_emit (self, timeline_signals[NEW_FRAME], 0, elapsed_time_ms);
+}
+
+static gboolean
+is_complete (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ return (priv->direction == GTD_TIMELINE_FORWARD
+ ? priv->elapsed_time_us >= priv->duration_us
+ : priv->elapsed_time_us <= 0);
+}
+
+static gboolean
+maybe_loop_timeline (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+ GtdTimelineDirection saved_direction = priv->direction;
+ gint64 overflow_us = priv->elapsed_time_us;
+ gint64 end_us;
+
+ /* 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)
+ priv->elapsed_time_us = priv->duration_us;
+ else if (priv->direction == GTD_TIMELINE_BACKWARD)
+ priv->elapsed_time_us = 0;
+
+ end_us = priv->elapsed_time_us;
+
+ /* Emit the signal */
+ emit_frame_signal (self);
+
+ /* Did the signal handler modify the elapsed time? */
+ if (priv->elapsed_time_us != end_us)
+ 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µs, tot: %ldµs)",
+ self,
+ priv->elapsed_time_us,
+ priv->delta_us);
+
+ 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
+ */
+ 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,
+ * except changing time from 0 -> duration (or vice-versa)
+ * since these are considered equivalent
+ */
+ if (priv->elapsed_time_us != end_us &&
+ !((priv->elapsed_time_us == 0 && end_us == priv->duration_us) ||
+ (priv->elapsed_time_us == priv->duration_us && end_us == 0)))
+ {
+ return TRUE;
+ }
+
+ if (priv->repeat_count == 0)
+ {
+ gtd_timeline_rewind (self);
+ return FALSE;
+ }
+
+ /* Try and interpolate smoothly around a loop */
+ if (saved_direction == GTD_TIMELINE_FORWARD)
+ priv->elapsed_time_us = overflow_us - priv->duration_us;
+ else
+ priv->elapsed_time_us = priv->duration_us + overflow_us;
+
+ /* Or if the direction changed, we try and bounce */
+ if (priv->direction != saved_direction)
+ priv->elapsed_time_us = priv->duration_us - priv->elapsed_time_us;
+
+ return TRUE;
+}
+
+static gboolean
+tick_timeline (GtdTimeline *self,
+ gint64 tick_time_us)
+{
+ GtdTimelinePrivate *priv;
+ gboolean should_continue;
+ gboolean complete;
+ gint64 elapsed_us;
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ GTD_TRACE_MSG ("Timeline [%p] ticked (elapsed_time: %ldµs, delta_us: %ldµs, "
+ "last_frame_time: %ldµs, tick_time: %ldµs)",
+ self,
+ priv->elapsed_time_us,
+ priv->delta_us,
+ priv->last_frame_time_us,
+ tick_time_us);
+
+ /* 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 FALSE;
+
+ elapsed_us = tick_time_us - priv->last_frame_time_us;
+ priv->last_frame_time_us = tick_time_us;
+
+ if (is_waiting_for_delay (self, tick_time_us))
+ {
+ GTD_TRACE_MSG ("- waiting for delay");
+ return G_SOURCE_CONTINUE;
+ }
+
+ /* 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 (elapsed_us <= 0)
+ return TRUE;
+
+ priv->delta_us = elapsed_us;
+
+ GTD_TRACE_MSG ("Timeline [%p] activated (elapsed time: %ldµs, "
+ "duration: %ldµs, delta_us: %ldµs)",
+ self,
+ priv->elapsed_time_us,
+ priv->duration_us,
+ priv->delta_us);
+
+ g_object_ref (self);
+
+ /* Advance time */
+ if (priv->direction == GTD_TIMELINE_FORWARD)
+ priv->elapsed_time_us += priv->delta_us;
+ else
+ priv->elapsed_time_us -= priv->delta_us;
+
+ complete = is_complete (self);
+ should_continue = !complete ? priv->is_playing : maybe_loop_timeline (self);
+
+ /* If we have not reached the end of the timeline */
+ if (!complete)
+ emit_frame_signal (self);
+
+ g_object_unref (self);
+
+ return should_continue;
+}
+
+static gboolean
+frame_tick_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ gpointer user_data)
+{
+ GtdTimeline *self = GTD_TIMELINE (user_data);
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+ gint64 frame_time_us;
+
+ frame_time_us = gdk_frame_clock_get_frame_time (frame_clock);
+
+ if (tick_timeline (self, frame_time_us))
+ return G_SOURCE_CONTINUE;
+
+ priv->frame_tick_id = 0;
+ return G_SOURCE_REMOVE;
+}
+
+static void
+add_tick_callback (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ g_assert (!(priv->frame_tick_id > 0 && !priv->widget));
+
+ if (priv->frame_tick_id == 0)
+ {
+ priv->frame_tick_id = gtk_widget_add_tick_callback (GTK_WIDGET (priv->widget),
+ frame_tick_cb,
+ self,
+ NULL);
+ }
+}
+
+static void
+remove_tick_callback (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ g_assert (!(priv->frame_tick_id > 0 && !priv->widget));
+
+ if (priv->frame_tick_id > 0)
+ {
+ gtk_widget_remove_tick_callback (GTK_WIDGET (priv->widget), priv->frame_tick_id);
+ priv->frame_tick_id = 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->start_us = g_get_monotonic_time ();
+ priv->last_frame_time_us = priv->start_us;
+ priv->current_repeat = 0;
+
+ add_tick_callback (self);
+ }
+ else
+ {
+ remove_tick_callback (self);
+ }
+}
+
+static gdouble
+timeline_progress_func (GtdTimeline *self,
+ gdouble elapsed,
+ gdouble duration,
+ gpointer user_data)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ return gtd_easing_for_mode (priv->progress_mode, elapsed, duration);
+}
+
+
+/*
+ * 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_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, us_to_ms (priv->delay_us));
+ 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_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_dispose (GObject *object)
+{
+ GtdTimeline *self = GTD_TIMELINE (object);
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ 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.
+ */
+ 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.
+ */
+ 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.
+ */
+ 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.
+ */
+ 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.
+ */
+ 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.
+ */
+ 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);
+
+ object_class->dispose = gtd_timeline_dispose;
+ 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::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.
+ */
+ 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;
+ priv->progress_func = timeline_progress_func;
+}
+
+/**
+ * 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
+ */
+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_set_widget:
+ * @timeline: a #GtdTimeline
+ * @widget: 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 (GTD_IS_TIMELINE (self));
+
+ if (priv->widget)
+ {
+ remove_tick_callback (self);
+ priv->widget = NULL;
+ }
+
+ priv->widget = widget;
+
+ if (priv->is_playing)
+ add_tick_callback (self);
+}
+
+/**
+ * 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->is_playing)
+ return;
+
+ if (priv->duration_us == 0)
+ return;
+
+ priv->delta_us = 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);
+
+ if (!priv->is_playing)
+ return;
+
+ priv->delta_us = 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, us_to_ms (priv->duration_us));
+}
+
+/**
+ * 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;
+ gint64 us;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+ us = ms_to_us (msecs);
+
+ if (priv->direction == GTD_TIMELINE_FORWARD)
+ {
+ priv->elapsed_time_us += us;
+
+ if (priv->elapsed_time_us > priv->duration_us)
+ priv->elapsed_time_us = 1;
+ }
+ else if (priv->direction == GTD_TIMELINE_BACKWARD)
+ {
+ priv->elapsed_time_us -= us;
+
+ if (priv->elapsed_time_us < 1)
+ priv->elapsed_time_us = priv->duration_us - 1;
+ }
+
+ priv->delta_us = 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;
+ gint64 us;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+ us = ms_to_us (msecs);
+
+ priv->elapsed_time_us = MIN (us, priv->duration_us);
+}
+
+/**
+ * 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 us_to_ms (priv->elapsed_time_us);
+}
+
+/**
+ * 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_get_delay:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the delay set using gtd_timeline_set_delay().
+ *
+ * Return value: the delay in milliseconds.
+ */
+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 us_to_ms (priv->delay_us);
+}
+
+/**
+ * gtd_timeline_set_delay:
+ * @timeline: a #GtdTimeline
+ * @msecs: delay in milliseconds
+ *
+ * Sets the delay, in milliseconds, before @timeline should start.
+ */
+void
+gtd_timeline_set_delay (GtdTimeline *self,
+ guint msecs)
+{
+ GtdTimelinePrivate *priv;
+ gint64 us;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+ us = ms_to_us (msecs);
+
+ if (priv->delay_us != us)
+ {
+ priv->delay_us = us;
+ 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.
+ */
+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 us_to_ms (priv->duration_us);
+}
+
+/**
+ * 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.
+ */
+void
+gtd_timeline_set_duration (GtdTimeline *self,
+ guint msecs)
+{
+ GtdTimelinePrivate *priv;
+ gint64 us;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+ g_return_if_fail (msecs > 0);
+
+ priv = gtd_timeline_get_instance_private (self);
+ us = ms_to_us (msecs);
+
+ if (priv->duration_us != us)
+ {
+ priv->duration_us = us;
+
+ 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.
+ */
+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);
+
+ return priv->progress_func (self,
+ (gdouble) priv->elapsed_time_us,
+ (gdouble) priv->duration_us,
+ 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
+ */
+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.
+ */
+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_us == 0)
+ priv->elapsed_time_us = priv->duration_us;
+
+ 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
+ */
+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 us_to_ms (priv->delta_us);
+}
+
+/**
+ * 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);
+ * ]|
+ */
+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
+ */
+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.
+ */
+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
+ */
+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.
+ */
+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]);
+}
+
+/**
+ * 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.
+ */
+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;
+ priv->progress_func = timeline_progress_func;
+ 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
+ */
+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
+ */
+gint64
+gtd_timeline_get_duration_hint (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+ gint64 duration_ms;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+ priv = gtd_timeline_get_instance_private (self);
+ duration_ms = us_to_ms (priv->duration_us);
+
+ if (priv->repeat_count == 0)
+ return duration_ms;
+ else if (priv->repeat_count < 0)
+ return G_MAXINT64;
+ else
+ return priv->repeat_count * duration_ms;
+}
+
+/**
+ * gtd_timeline_get_current_repeat:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the current repeat for a timeline.
+ *
+ * Repeats start at 0.
+ *
+ * Return value: the current repeat
+ */
+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_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..a8d6023
--- /dev/null
+++ b/src/animation/gtd-timeline.h
@@ -0,0 +1,150 @@
+/* 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
+ * @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 (*stopped) (GtdTimeline *timeline,
+ gboolean is_finished);
+};
+
+GtdTimeline* gtd_timeline_new_for_widget (GtdWidget *widget,
+ 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_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);
+
+gint64 gtd_timeline_get_duration_hint (GtdTimeline *timeline);
+gint gtd_timeline_get_current_repeat (GtdTimeline *timeline);
+
+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..0ca1d3f
--- /dev/null
+++ b/src/animation/gtd-transition.c
@@ -0,0 +1,655 @@
+/* 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 || !priv->animatable)
+ 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.
+ */
+ 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.
+ */
+ 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.
+ */
+ 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.
+ */
+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
+ */
+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.
+ */
+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.
+ */
+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.
+ */
+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
+ */
+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.
+ */
+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.
+ */
+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.
+ */
+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.
+ */
+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..32e20fa
--- /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