GTK4 Animations, a strawman proposal

Animations

There are multiple ways to implement an animation framework for the toolkit and
applications to programmatically apply to a widget. During the past 10 years
we’ve iterated over most of them, and they all have different pros and cons
that we can summarise here.

Tweener

This approach is modelled on the Tweener API available in Flash, and ported to
JavaScript. It is based on a single entry point that takes the object to
animate; the duration of the animation; the initial delay in the tween; the
easing curve to compute the linear interpolations; and a list of key/value
pairs that represent the final state of each object property, e.g.

tweener_tween (widget_to_animate, duration, delay, easing_mode,
  "width", 200,
  "height", 200,
  "opacity", 1.0,
  NULL);

It’s also possible to attach callback to the tweening operation in order to be
notified of various state changes:

  • the start of the animation
  • each animation update, which typically means a notification on every frame
  • the end of the animation

Internally, every tween is added to a global list of tweens, and every frame
advances the tweening using the delta between the current and previous frame
timings, removing the tween from the list if it reaches its intended duration.
Each tween is also unaware of the state of each element of the scene graph,
and could be used to tween generic objects that do not contribute to the
rendering or layout; as long as there is a tween in flight, Tweener will
request a new frame.

The main advantage of this API is that it’s fairly simple and non-obtrusive.

The main disadvantage is that it relies heavily on exposing GObject properties
to widget and application developers; on the C side of things, the use of
variadic arguments is somewhat painful, as it trades off type safety for
convenience, and it introduces GValue boxing and unboxing all over the place,
which can be problematic in a performance critical path.

Another additional disadvantage is that Tweener is heavily based on single
widget animations; grouping, chaining, and looping animations are complicated,
and even rolling back an animation mid-flight requires various contorsions; it’s
definitely easier if the language has closures, but on a verbose language like
C it requires a lot of state to be created and shuffled along the various
callbacks.

Explicit transitions

The explicit transition API is modelled on the CoreAnimation API, and it’s
partially based on Tweener, but with a more specialised set of entry points.
This API is also the prototype for the equivalent CSS transitions.

The base class is a Transition object with the following properties:

Transition
  :widget — the widget that requested a transition
  :duration — the duration of the transition
  :delay — initial delay of the transition
  :direction — whether the transition goes forward or backward
  :repeat-count — the amount of repeats, or -1 for a looping transition
  :auto-reverse — whether the transition should reverse itself at the end
  :easing-curve — the easing curve for the linear interpolation

Additionally, a Transition can be started and stopped explicitly—hence the name
of the API. A Transition has callbacks to know when it started and when it
stopped, but it typically does not have a callback for per-frame updates. If
a transition needs to be stopped or updated in response to an event, it’s always
possible to explicitly manipulate it via its instance.

The Transition base class has an “update” virtual function that is invoked
every frame, with the delta between the current frame and the previous one
as an argument. Another argument is the current progress value, computed
after applying the various properties to the time delta.

Internally, every time a Transition is created, it’s added to a list of
transitions tied to the top level, and tied to the UI element that uses it:

gtk_widget_add_transition (widget, "transition-name", transition);

If a widget does not contribute to the scene graph—e.g. it’s not mapped— then
the transition will not start. This, for instance, allows adding transitions
to unparented widgets without having them start until the widget is mapped;
or have the transitions be automatically stopped when a widget is removed from
a parent and added to another parent, or simply destroyed. This also allows
us to avoid interpolating states like the allocation of a widget until the
widget has an allocation.

Using the Transition as the base class, we can implement a PropertyTransition,
which takes a GObject property and its current value as the initial state,
and a value as the final state.

We can also create various types of transitions, for instance:

  • TransitionGroup, which takes a list of Transition instances and advances
    them together
  • KeyframeTansition, which uses key frames to determine complex animations
    with multiple changes and easing curves
  • StateTransition, which uses named states to group multiple transitions

The downside of this API is that it’s much more verbose than Tweener, and it
makes writing simple, “fire and forget” transitions more complicated. Like
Tweener, it also exposes GObject properties to the widget and application
developers.

Implicit transitions

The implicit transition API is a compromise between Tweener and the Explicit
transition API, and it’s generally implemented on top of the latter.

Using the explicit Transition API it’s possible to have widgets create their
own implicit transitions for a subset of animatable properties. Whenever a
widget method to set a property is called, the widget implementation creates
a transition and fires it instantaneously.

Widgets are augmented by an “easing state” that behaves like a stack; for
instance:

gtk_widget_push_easing_state (widget, duration, delay, easing_curve);

…

gtk_widget_pop_easing_state (widget);

Will ensure that every animatable property of @widget that is set between the
push and pop operations will automatically create a transition between its
current state and the desired final state, using @duration, @delay, and
@easing_curve as the parameters of the transition.

Easing states can stack up, so it’s possible to create more complex implicit
transitions, for instance:

// go to fully opaque in 250 msecs
gtk_widget_push_easing_state (widget, 250, 0, ease_out);
gtk_widget_set_opacity (widget, 1.0);

// halfway through, scale up by 20%
gtk_widget_push_easing_state (widget, 250, 125, ease_out);
gtk_widget_set_scale (widget, 1.2, 1.2);
gtk_widget_pop_easing_state (widget);

gtk_widget_pop_easing_state (widget);

Of course, this being a simpler, almost automagical API than the explicit
transitions API, it’s more limited in what it can do out of the box. Only
animatable properties can have implicit transitions. Additionally, unless
the easing state is made accessible to out of tree widget authors, only
GTK can implement implicit transitions.

Implementing transitions

Case study 1: GtkStack, the compatible way

GtkStack already has various named transitions; in this case, we want to
maintain the same API, instead of allowing a free-for-all “lets move this
stack page to Mars” scenario.

Each named transition would be responsible for creating a Transition class
that would take the progress on each Transition.update() and apply it to the
current/previous/next pages involved in the animation itself.

Given the explosion of transition types, it’s also possible to create a single
Transition subclass that has the Stack transition type as a property, and
can subsume the existing GtkStack code that currently uses the ProgressTracker
helper object.

Case study 2: GtkStack, the “Bring Your Own” way

If we want to allow application developers to implement new transitions out
of tree on GtkStack widgets then we can turn the stack “inside out”; it should
be possible to set the Stack to clip to its allocation, and then create a
PropertyTransition instance on the transformation property of each page, in
order to re-implement transitions like the slide, over, and under ones.
Using the opacity property on the pages would allow the implementation of
the cross-fade transition.

9 Likes

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.