Web Animations/Issues/Grouping of Animations

From Effects Task Force

Grouping of Animations / the Merge Compositor

Summary

Currently, animation effects are controlled by an AnimFunc, and the timing of these effects is controlled by a Timing object. Each Timing object defines a boundary of time within which an AnimFunc executes, and AnimFunc details don't leak across this boundary. However, for some effects it is desirable for an AnimFunc to be "shared" across multiple Timing object boundaries, so that smooth animations can occur.

The Problem

Example 1

Smoothly interpolating from one animated path to another. Say we want an object to move around a path "A" for 5 seconds, then smoothly transition over to path "B" (taking 2 seconds to do so), then continue along path "B" indefinitely. We can't just do this:

 <g>   // this will be animated
   <animation path=A duration="4s" iterationCount="Infinity">
   <animation path=B startDelay="5s" duration="6s" iterationCount="Infinity" operation="merge">
 </g>

because the "merge" operation is defined to operate over the duration of the time container it operates within (i.e. forever in this case), we'll never even start to embark on path B.

Instead, we need to do something like this:

 <g>
   <seq>
     <par duration="5s"> // this par is needed to clip the duration of just moving along path "A"
       <animation path=A duration="4s" iterationCount="Infinity">
     </par>
     <par duration="2s"> // this par defines the timing for the merge operation below
       <animation path=A duration="4s" iterationCount="Infinity" iterationStart="0.25"> // we need an iterationStart to ensure continuity of path "A"
       <animation path=B duration="6s" iterationCount="Infinity" operation="merge">
     </par>
    <animation path=B duration="6s" iterationCount="Infinity" operation="merge" iterationStart="0.333"> // we need an iterationStart to ensure continuity of path "B"
   </seq>
  </g>

This is horrendous :) Some obvious problems:

  • we need to split "one operation" into 3 pieces (before merge, during merge, after merge)
  • once we've made that split, we then need to tweak iterationStart values to ensure continuity across our artificially introduced barriers
  • we need to introduce extra "par" containers to control the timing of the first two pieces.

Things are slightly nicer in script because we can use GroupedAnimFunc objects to combine the two animations during the merge, and override the duration of each Anim object directly rather than wrapping in a <par>. However, even in script we still need to perform the split; and we still need to manually calculate iterationStart values to ensure continuity over this split.

Clearly, this problem would be solved if merge were parameterized by start and end time:

 <g>   // this will be animated
   <animation path=A duration="4s" iterationCount="Infinity">
   <animation path=B duration="6s" iterationCount="Infinity" operation="merge(5,7)">
 </g>

However, the same problem also crops up in another context.

Example 2

Let's say we still want our object to animate along path A then smoothly merge over to path B. However, path A is rotating at 0.1 Hz, and path B is rotating at 0.5 Hz. Naively we'd try something like this:

 <g>
   <par>
     <animation property="transform" from="rotate(0deg)" to="rotate(360deg)" duration="10s" iterationCount="Infinity">
     <animation path=A duration="4s" iterationCount="Infinity">
   </par>
   <par>
     <animation property="transform" from="rotate(0deg)" to="rotate(360deg)" duration="2s" iterationCount="Infinity">
     <animation path=B duration="6s" iterationCount="Infinity">
   </par operation="merge(5,7)">
 </g>

However, this doesn't work because merge operations are a property of AnimFuncs, not Timing containers, and hence can't apply to <par> groups. If the merge operation were placed on path B's animation, however, then the merge would only apply between B and the accumulated animation stack below B (rotation 1, path A, rotation 2) - the net effect would be that after 5 seconds path A suddenly starts rotating faster, and the object then merges across to a stationary path B over 2 seconds.

Instead, to make this example work, we need to resort to GroupedAnimFuncs, which don't even have an element syntax yet. Let's dream one up:

 <g>
   <animation>
     <animGroup>
       <animFunc property="transform" from="rotate(0deg)" to="rotate(360deg)">
       <animFunc path=A>
     </animGroup>
   </animation>
   <animation operation="merge(5,7)">
     <animGroup>
       <animFunc property="transform" from="rotate(0deg)" to="rotate(360deg)">
       <animFunc path=A>
     </animGroup>
   </animation>
 </g>

AnimFuncs can't have timing properties by definition. What timing properties should our animGroup objects have? Because each animFunc requires a different animationDuration, there is *nothing that will work* without performing animation surgery:

 <g>
   <seq>
     <animation duration="4s"> // Once around path A
       <animGroup>
         <animFunc property="transform" from="rotate(0deg)" to="rotate(144deg)"> // updated end of rotation.
         <animFunc path=A>
       </animGroup>
     </animation>
     <par>
       <animation duration="4s"> // Twice around path A.
         <animGroup>
           <animFunc property="transform" from="rotate(144deg)" to="rotate(288deg)"> // updated end of rotation.
           <animFunc path=A>
         </animGroup>
       </animation>
       <animation operation="merge(1,3)" duration="6s"> // path B is exactly 3 rotations long.
         <animGroup>
           <animFunc property="transform" from="rotate(0deg)" to="rotate(1080deg)"> // updated end of rotation.
           <animFunc path=B>
         </animGroup>
       </animation>
     </par>
     <animation operation=duration="6s" iterationCount="Infinite">
       <animGroup>
         <animFunc property="transform" from="rotate(0deg)" to="rotate(1080deg)"> // updated end of rotation.
         <animFunc path=B>
       </animGroup>
     </animation>
   </seq>
 </g>

Note that we can't even combine the merging stage with the post-merge stage because merge is a property of the AnimFunc (and hence would be duplicated by iterating). Things are even more dire without parametric merge:

 <g>
   <seq>
     <animation duration="4s"> // Once around path A
       <animGroup>
         <animFunc property="transform" from="rotate(0deg)" to="rotate(144deg)"> // updated end of rotation.
         <animFunc path=A>
       </animGroup>
     </animation>
     <animation duration="4s" iterationCount="0.25"> // Once around path A
       <animGroup>
         <animFunc property="transform" from="rotate(144deg)" to="rotate(288deg)">
         <animFunc path=A>
       </animGroup>
     </animation>
     <par duration="2s">
       <animation duration="4s" iterationStart="0.25">
         <animGroup>
           <animFunc property="transform" from="rotate(144deg)" to="rotate(288deg)">
           <animFunc path=A>
         </animGroup>
       </animation>
       <animation operation="merge" duration="6s">
         <animGroup>
           <animFunc property="transform" from="rotate(0deg)" to="rotate(1080deg)"> // updated end of rotation.
           <animFunc path=B>
         </animGroup>
       </animation>
     </par>
     <animation operation=duration="6s" iterationCount="Infinite">
       <animGroup>
         <animFunc property="transform" from="rotate(0deg)" to="rotate(1080deg)"> // updated end of rotation.
         <animFunc path=B>
       </animGroup>
     </animation>
   </seq>
 </g>

Hopefully it's clear by this point that timing properties and Animation Stack abstractions need to be orthogonal to each other rather than tied together.

Straw People

Allow AnimFunc objects to be linked or shared

One of the major issues here is that we need to perform timing surgery in order to ensure our AnimFunc start- and end-points line up. What if we could somehow share AnimFuncs, or at least link them, so that the current iteration time at the end of one func can be used as the start of the next func when they're meant to be "the same animation".

This doesn't solve the issue with two animations with different periods that need to be placed into a GroupedAnimFunc container for stack grouping purposes. Under some circumstances an accumulateOperation of "add" would mitigate this somewhat.

It's pretty clear how this would work in script:

 var firstPath = new PathAnimFunc(A);
 var firstRotate = new KeyframeAnimFunc("transform", ["rotate(0deg)", "rotate(144deg)"], {accumulateOperation:"add"});
 var firstGroup = new GroupedAnimFunc([firstPath, firstRotate]);
 
 var secondPath = new PathAnimFunc(B);
 var secondRotate = new KeyframeAnimFunc("transform", ["rotate(0deg), "rotate(1080deg)"]);
 var secondGroup = new GroupedAnimFunc([secondPath, secondRotate], {operation: "merge"});
 var thirdGroup = new GroupedAnimFunc([secondPath, secondRotate]);
 
 var preMerge = new Anim(object, firstGroup, {duration: "4s", iterationCount: "1.25"});
 var merge = new ParAnimationGoup(
   [
     new Animation(object, firstGroup, {duration: "4s"}),
     new Animation(object, secondGroup, {duration: "6s"})
   ], { duration: "2s" }
 var postMerge = new Anim(object, thirdGroup, {duration: "6s", iterationCount: "Infinity"});
 
 var container = new SeqAnimationGroup([preMerge, merge, postMerge]);

However, this is far from intuitive. Why do we need two groups for the second path? (it's because merge is a property of the group, and we don't want it set after the merge has occurred). What would happen if we shared an AnimFunc with two animations that ran partially or completely in parallel? I think it's completely non-obvious that the firstGroup "continues" into the merge, and that the children of the secondGroup "continue" into the thirdGroup.

It's also hard to see how this would work declaratively.

Allow parametric merge

I think this is a good idea, as for very little increase in complexity it makes a bunch of stuff much easier to specify. Even though it doesn't completely solve the problems, it makes a lot of things easier and I think we should combine it with whatever else we end up choosing.

Push grouping to the level of Animations

The grouping parts of this problem would be solved if grouping and timing were distinct concepts. If Animations could be grouped instead of AnimFuncs, then groups could exist independently of time ranges, and artificial time ranges would not need to be introduced to control grouping. Because we already call sequence and parallel containers AnimGroups, I'm going to use the name "Collection" for compositing grouping below. In reality, though, I think it would be better to switch SeqAnimGroup and ParAnimGroup to SeqAnimContainer and ParAnimContainer, and use the name "Group" for the construct described here.

A few options are explored below.

Explicit <collection> tags / AnimCollection constructors.

Taking our second example and assuming parametric merge is a thing:

<g>
  <collection>
    <animation property="transform" from="rotate(0deg)" to="rotate(360deg)" duration="10s" iterationCount="Infinity" >
    <animation path=A duration="4s" iterationCount="Infinity">
  </collection>
  <collection operation="merge(5,7)">
    <animation property="transform" from="rotate(0deg)" to="rotate(360deg)" duration="2s" iterationCount="Infinity">
    <animation path=B duration="6s" iterationCount="Infinity">
  </collection>
</g>

And in script:

var firstPath = new Anim(object, {path: A}, {duration: "4s", iterationCount: "Infinity"});
var firstRotate = new Anim(object, {property: "transform", from: "rotate(0deg)", to: "rotate(360deg)"}, {duration: "10s", iterationCount: "Infinity"});

var secondPath = new Anim(object, {path: B}, {duration: "6s", iterationCount: "Infinity"});
var secondRotate = new Anim(object, {property: "transform", from: "rotate(0deg)", to: "rotate(360deg)"}, {duration: "2s", iterationCount: "Infinity"});

var firstGroup = new AnimCollection([firstPath, firstRotate]);
var secondGroup = new AnimCollection([secondPath, secondRotate], "merge(5, 7)");

The script "just works" as groups don't influence timing, only resolution order. So all of the animations start immediately (they're all newly defined), then the grouping takes care of keeping the first rotation and path separate from the second rotation and path, with a merge from one to the other.

This is fairly simple, but some things to note:

  1. groups don't imply timing, but (especially in the declarative case) kinda look like they do / should
  2. if using a declarative syntax, groups could not span multiple different parts of the animation tree. This isn't a problem in script. I'm not yet sure if there are use cases where this could be useful.

Groups *are* Collections

In this approach, a <par> or <seq> container implicitly groups the contained animations for compositing. This allows us to do:

 <g>
  <par>
    <animation property="transform" from="rotate(0deg)" to="rotate(360deg)" duration="10s" iterationCount="Infinity" >
    <animation path=A duration="4s" iterationCount="Infinity">
  </par>
  <par operation="merge(5,7)">
    <animation property="transform" from="rotate(0deg)" to="rotate(360deg)" duration="2s" iterationCount="Infinity">
    <animation path=B duration="6s" iterationCount="Infinity">
  </par>
</g>

which is very neat. This approach introduces some subtle changes to the outcome of compositing under some circumstances, but I actually think the resulting behavior is slightly cleaner than what we have currently specified. With this approach, visually grouped animations in web page text will be more related to each other during compositing than visually separate animations. Furthermore, this becomes possible:

<g id="foo">
  <!-- a complicated description of multi-element and multi-property animation behavior goes here -->
</g>


<def>
  <par operation="merge(0, 2)" id="newFoo">
  <!-- a complicated description of a new multi-element and multi-property animation behavior goes here. This will be triggered by an event. -->
  </par>
</def>

<script>
foo.onclick = function(event) {  newFoo.animate(foo); }
</script>

This will push an instance of the newFoo template onto the animation stack, above foo. The merge on newFoo means that the new behavior will gradually take effect over 2 seconds.

Groups *generate* Collections

With this approach, a <par> or <seq> group isn't a collection, but does generate an implicit collection to which all children of the group are added by default. However, a property / method call on Anims and AnimGroups could change membership to a different group or remove an Animation from a group altogether. This is an extension of the "Groups are Collections" idea that allow multiple different animations without timing relationships to be grouped together for the purpose of compositing. I think it's very powerful but I'm not yet sure if there are use cases that support it.

As an example:

 <par id="parGroup">
   <animation ... id="A"/>
   <animation ... id="B"/>
   <seq>
     <animation ... id="C"/>
     <animation ... group="parGroup" id="D"/>
     <animation ... id="E"/>
   </seq>
   <animation ... id="F" group=""/>
 </par>

Would produce the following stack (items at the bottom are overwritten by items at the top):

F
parGroup

the parGroup stack would contain:

D
seq
B
A

the seq stack would contain

E
C

Recommendations

I'd really like to see some discussion on this, but at the moment I'm leaning towards specifying either "Groups are collections" or "Groups generate collections". Does anyone have any other approaches that might work, or reasons why these approaches are undesirable?