diff options
Diffstat (limited to 'src/animation')
| -rw-r--r-- | src/animation/gtd-animatable.c | 204 | ||||
| -rw-r--r-- | src/animation/gtd-animatable.h | 85 | ||||
| -rw-r--r-- | src/animation/gtd-animation-enums.h | 173 | ||||
| -rw-r--r-- | src/animation/gtd-animation-utils.c | 168 | ||||
| -rw-r--r-- | src/animation/gtd-animation-utils.h | 65 | ||||
| -rw-r--r-- | src/animation/gtd-easing.c | 474 | ||||
| -rw-r--r-- | src/animation/gtd-easing.h | 141 | ||||
| -rw-r--r-- | src/animation/gtd-interval.c | 1134 | ||||
| -rw-r--r-- | src/animation/gtd-interval.h | 116 | ||||
| -rw-r--r-- | src/animation/gtd-keyframe-transition.c | 716 | ||||
| -rw-r--r-- | src/animation/gtd-keyframe-transition.h | 83 | ||||
| -rw-r--r-- | src/animation/gtd-property-transition.c | 359 | ||||
| -rw-r--r-- | src/animation/gtd-property-transition.h | 55 | ||||
| -rw-r--r-- | src/animation/gtd-timeline.c | 1547 | ||||
| -rw-r--r-- | src/animation/gtd-timeline.h | 150 | ||||
| -rw-r--r-- | src/animation/gtd-transition.c | 655 | ||||
| -rw-r--r-- | src/animation/gtd-transition.h | 85 |
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, /* number of key frames */ + * 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 |
