/* * Gtd. * * An OpenGL based 'interactive canvas' library. * * Authored By Matthew Allum * * 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 . */ /** * 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; }