Once you start animating anything more than simple fade and slide effects, pretty soon you start wanting to synchronize things. The penguins should start dancing together, the watermelon should explode the moment the blind-folded person hits it, the credits should roll after the movie finishes and so on.
In this post I want to outline the approach taken to these problems in Web Animations and the biggest new feature in the spec: timing groups. But first, what can we already do?
Synchronization in CSS
In CSS you can do a few things:
- By toggling a style on a common ancestor you can get two animations to start at the same time. For example, see this example from Smashing Magazine.
- Using animation-delay you can stagger the start times of animations and by calculating the length of A, you can use this to make B start after A.
There are a few limitations though:
- It’s not particularly scalable—if A changes duration, you need to update B as well. That’s particularly problematic if, for example, A is a video of unknown length.
- You’re not declaring the temporal relationships between different animations—their synchronization is just a by-product of how you triggered them—and as a result there’s no easy way to pause or seek a particular set of animations.
- It requires you to structure your document so that you’ve got a common
ancestor to toggle a class on.
Suddenly your document structure is doing triple duty: semantics, layout
(all those empty
<div>
s to persuade CSS to do what you want), and timing. - You can’t create animations on the fly that line up with already running animations.
The problem with SVG and syncbase timing
SVG has a neat feature which seems to meet these needs: syncbase timing. That lets you create arrangements like:
<animate id="a" begin="b.end" ... />
<animate id="b" begin="0s" end="c.end" ... />
<animate id="c" begin="a.begin+2s" ... />
That would seem to do everything we could ever want, except it has a few problems:
-
It’s complex. The above example is just the tip of the iceberg. You can have any number of conditions on both the begin and end of an animation pointing to any other animation including the same animation. You can create cyclic dependencies and some are allowed, some are not, and the rules for breaking cycles are vague (and thus, inconsistently implemented).
What’s more, all this feeds in to the most complex part of SVG animation: deciding what intervals to create. As a result if you want to determine what animations are active at a given time, you have to step through all the significant moments up to that time and work out what intervals to create. This makes seeking costly.
-
Another problem is that you can’t cancel just part of such a sequence.
For example, imagine you have a sequence of actions A, B, and C representing “fade the background of the button from grey to blue”, “pulse it”, then “make it glow”. You trigger A when the mouse moves over the button but the mouse suddenly leaves while A is still running. You cancel A (e.g. using
end="mouseout"
orendElement
) but then B still runs since it sees that A has ended. And consequently C also runs. There’s no way to cancel the chain and nor is there any way to seek/pause such a chain independently of other animations that might be running.
Enter timing groups
These use cases can be addressed using the concept of timing groups. This is a well-established concept in animation APIs. For example,
- QML has
ParallelAnimation
s andSequenceAnimation
s. - SMIL has
<par>
,<seq>
, and<excl>
time containers. - Android
has
AnimatorSet
s that play animations together or in sequence.
Common to all these APIs is the concept of two types of groups: groups that run animations together and groups that run animations in sequence.
Furthermore, in each of these APIs the two types of groups can be nested so you can easily build complex arrangements up from simple parts in a way that is easy to reason about.
For example, the following arrangement:
<animate id="curtainOpens" ... />
<animate id="penguinADances" begin="curtainOpens.end" ... />
<animate id="penguinBDances" begin="curtainOpens.end" ... />
<animate id="curtainCloses" begin="penguinBDances.end" ... />
could be represented with timing groups as:
<seq>
<animate id="curtainOpens" ... />
<par>
<animate id="penguinADances" ... />
<animate id="penguinBDances" ... />
</par>
<animate id="curtainCloses" ... />
</seq>
Graphically, the arrangement is as follows:
You can cancel a chain of animations in this case by simply cancelling the group. The hierarchical nature of the groups also means seeking is constant-time with regards to the seek interval.
Timing groups in Web Animations
In Web Animations we have adopted this same approach of using timing groups with the following features:
- Two types of timing groups: parallel, sequence.
- Children of a group can have a delay which can also be negative. This allows children of a sequence group to overlap so you can, for example, start a fade animation 2s before a video ends. (SMIL does not allow negative delays on children of a sequence group.)
- Any timing property you can apply to an animation, you can also apply to a group which means you can repeat, ease, reverse, delay, or speed up a group.
The last point is interesting since most other APIs don’t do that. It can be quite useful as we showed in a demo of this in our preview video last October where we combined a door closing animation and a creaking sound “animation” in a group then eased the group.
However, it introduces some complexity. For example, since easing functions can be non-invertible, it means you can’t always convert from child time to group time which complicates event handling.
Also, the interaction between fill modes specified on a group and on a child are not always obvious and we need to revise the spec to be more intuitive in this area.
Building up the example above using the API in Web Animations look like the following.
document.timeline.play(
new SeqGroup([
new Animation(...), // Curtains open
new ParGroup([
new Animation(...), // Penguin A
new Animation(...) // Penguin B
]),
new Animation(...) // Curtains close
])
);
On a side-note, we’ve had some feedback about ParGroup
and SeqGroup
being
a bit cryptic, especially for anyone not familiar with SMIL (which is pretty
much everyone).
ParallelGroup
would be more readable but it’s a lot to type for something we
expect authors to use a lot.
I wonder if just Parallel
would make sense?
Perhaps this could be a named constructor for ParallelGroup
?
If you have any suggestions we’d love to hear them.
Either respond here, or, better yet, send a mail to
public-fx@w3.org with subject starting
[web-animations]
.
The mapping of timing groups to SVG and CSS has yet to be worked out.
Future extensions to timing groups
One common request that arises in regards to timing groups is the ability to set the duration of the group and specify the duration of the children as fractions of that parent. That sort of adaptive duration is something that’s quite useful but also can easily become complex—like CSS flexbox in the temporal domain. It’s something we’ll look at in a future version but for now we’re comfortable it can be added to the model and API fairly naturally.
If you want to read up on the detail, have a look at the
spec.
As always, your comments are welcome at
public-fx@w3.org with subject starting
[web-animations]
.
Your two examples are not quite equivalent, as the latter will close the curtain when both penguins have finished dancing, but the former is only watching penguin B. Is there a way to change the syncbase syntax to look at both?
e.g.