diff options
| author | Matthew Fennell <matthew@fennell.dev> | 2025-12-27 12:40:20 +0000 |
|---|---|---|
| committer | Matthew Fennell <matthew@fennell.dev> | 2025-12-27 12:40:20 +0000 |
| commit | 5d8e439bc597159e3c9f0a8b65c0ae869dead3a8 (patch) | |
| tree | ed28aefed8add0da1c55c08fdf80b23c4346e0dc /src/animation/gtd-timeline.c | |
Import Upstream version 43.0upstream/latest
Diffstat (limited to 'src/animation/gtd-timeline.c')
| -rw-r--r-- | src/animation/gtd-timeline.c | 1547 |
1 files changed, 1547 insertions, 0 deletions
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; +} |
