Web Animations/Shared Timing
Summary
This page contain some thoughts about the proposal for TimedItems
to have a separate Timing
object that contains timing parameters and which can be shared.
The proposal
Based on the notes in [1], the proposed API would look something like the following:
anim.startTime // READ ONLY - don't even provide and beef up AnimationController? anim.currentTime // READ ONLY - don't even provide and beef up AnimationController? anim.currentIteration anim.filteredTime anim.endTime // READ ONLY - don't even provide and beef up AnimationController? anim.timing.startDelay anim.timing.iterationStart anim.timing.iterationCount anim.timing.iterationDuration anim.timing.fillMode anim.timing.playbackRate anim.timing.direction anim.timing.timingFunction anim.getActiveDuration() // getActiveDuration anim.getIterationDuration() // for groups, maybe even put on TimingGroup interface.
The proposal also expects that in the future Timing
would be extended to allow a cascade where Timing
items would be chained together such that undefined items in one Timing
object would defer to the equivalent item on the next Timing
object in the chain.
For the first version it is proposed that Timing
objects could be shared between TimedItem
s but not chained.
Concerns
Local changes have wide-ranging side effects
If Timing
objects are shared, it is not possible to know if a change made to a given Timing
object has side effects. This is particularly hazardous for new users and libraries and lends itself to surprising bugs.
For example, consider the use case of a library that implements timing based on a fixed rate of change, e.g. "2px/s". This has been proposed for SVG 2 and is a likely candidate for a script library to implement. The library takes an Animation
, reads the animation values and adjusts the keyframe offsets and iteration duration to achieve the desired rate of change.
If such a library were passed an animation with timing shared in the proposed approach, it would possibly cause undesired results for the other connected animations. Although it is possible that the author intended all linked animations to match the passed-in animation's duration even after the scaling it is more likely that their intention was either that (a) only the given animation change, or (b) all linked animations have a "2px/s" rate of change. Furthermore, the library is unable to do anything to detect or prevent this misuse. Generally speaking, libraries are unable to safely make local changes.
Furthermore, this property of seemingly local changes having possibly wide-ranging side effects, easily lends itself to misunderstanding by new users. For example,
animA = document.createAnimation(...); animB = document.createAnimation(...); // I just want animB to have the same timing as animA animB.timing = animA.timing; // But I also want animB to have a duration of 5 animB.duration = 5; // Hey, what happened to animA ??
While one might argue that this is idiomatic javascript and could be worked around by adding a clone()
method to Timing
, it is sure to catch authors out (especially when the change is subtle) and we would do best to avoid loading this footgun.
Sharing is very limited
The sharing offered by Timing
objects in the first version is very limited. It is all or nothing. If, for example, it is desired to share all timing properties but not the start delay, this is not possible. Perhaps start delay could be factored out into groups, but then what about fill mode, or duration etc. ?
As a result, the number of cases where sharing of Timing
is useful in version one is very limited.
Where you set the duration is different to where you get it
anim.timing.iterationDuration = 5; anim.getIterationDuration();
A similar inconsistency is seen in startTime
and startDelay
.
var actualStart = anim.startTime + anim.timing.startDelay;
In short, it's difficult for authors to remember where each property/method lives. Even assuming we establish some sensible rules that dictate where each item lives, it's still another concept the author must learn.
Unclear migration path
While the API for a version 2 that supports cascading of Timing
items is yet to be proposed, an initial consideration of options suggests that simply adding Timing
objects to version 1 does not necessarily pave a clear path for adding chaining in version 2.
For example, in a chain of Timing
objects a means is required for indicating which values fall-through and which stick. So far it has been suggested that null
or undefined
values would fall-through. Using null
would imply all members of the Timing
interface are null-able from version 1 (to avoid breaking content that assumes otherwise). Also, changes would be required to the way Timing
objects are created so that not all members are set.
Also, assuming that members of Timing objects are allowed to be unspecified (be it null
or undefined
), this would imply there is no single place where final values are available. We would need to either (a) add getFinalTiming()
—which has the same problem of introducing a gap between where values are set and where they are queries, or (b) rely on client code for filling in default values when performing calculations. (Other options? I thought maybe we could say that the Timing
attached to a TimedItem
would have to be fully specified but that doesn't work.)
Similarly, if timing.iterationDuraton = null
is used, in version 1, to mean, "Use the intrinsic iteration duration", then an alternative mechanism would be required to mean, "Use the inherited iteration duration" in version 2. Most likely a sentinel value (undefined
would not pass from a usability point of view) or resetIterationDuration()
would be needed. In any case, this would need to be present from version 1.
Complexity
As well as these specific issues, my concern is that there are a raft of minor complexities introduced by this approach which outweigh its benefit in version 1.
Adding the Timing
object to version one introduces an additional step in the chain when interacting with timing properties (literally every single other API reviewed puts the duration etc. directly on the Animation), it is another interface to look up when coding, it differs from every other animation API including HTML—not such a big deal by itself but makes the API a little less guessable, it is another concept (albeit a simple one, but if we have guidelines for when items appear on TimedItem
vs Timing
it is still another concept to learn), it is another moving piece for implementations to track (a minor concern but not insignificant since sharing means either you fragment the memory by allocating lots of smaller objects, or you do plumbing to keep track of them and split them off as needed), and it lends itself to misuse (covered above).
All of these costs may be justifiable if this feature provided significant value that addressed common use cases faced by the majority of authors, but, certainly in version one, it serves at best a very limited range of use cases (due to the limited sharing) of interest only to advanced developers many of which could probably be achieved by cloning or iterating over a list. Meanwhile the cost of the complexity is paid by all authors.
Proposed solution
In light of the above concerns I think a preferable approach would:
- Provide a single place where all final values can be queried regardless of any chaining that may be added in future
- Provide a place where local changes can be safely made by libraries etc. without having side effects
- Make the two above places one and the same
This is a fairly easy set of requirements to satisfy. The question, however, is, "Could such an approach be extended to support the kind of sharing of timing information proposed above?" I believe it could.
Firstly, the version 1 API:
anim.startTime anim.currentTime anim.currentIteration anim.filteredTime anim.endTime anim.startDelay anim.iterationStart anim.iterationCount anim.iterationDuration anim.activeDuration anim.fillMode anim.playbackRate anim.direction anim.timingFunction anim.resetIterationDuraton()
Notes:
- Local changes have no side effects
- All final values are in one place
- Setting and getting the iteration duration are in the same place (the same attribute)
startTime
andstartDelay
are in the same place- Consistent with other APIs
Also, when reading code:
anim.resetIterationDuration();
is easier to understand than:
anim.timing.iterationDuration = null;
What if we want to share timing information in a future version?
One (straw-man) possibility:
partial interface TimedItem { attribute Timing? shared; };
Setting shared
to a Timing
object makes all the properties on the TimedItem
take their values from the Timing
object.
Subsequently setting any properties directly on the TimedItem
(e.g. anim.startDelay
) is effectively a local override. Timed items keep track internally of which properties are specified locally and which are derived from shared
.
shared
is initially null
.
partial interface TimedItem { Timing shareTiming(); };
Returns a Timing
object filled with the current values of the timing properties on the TimedItem
and makes all the timing properties on the TimedItem
take their values from the newly created Timing
object.
(iterationDuration
probably requires special handling)
partial interface TimedItem { Timing useShared(Timing timing, DOMString properties...); };
Sets shared
to timing
. If properties
is provided it limits which properties are read from shared
and which ones are specified locally.
(I'm not really sure how to represent the last property yet, but it probably should take either (a) nothing, (b) a DOMString, or (c) a sequence of DOMStrings. The important thing for now is that it's optional).
Putting this API into practice:
var animA = document.createAnimation(...); var animB = document.createAnimation(...); // Make animA and animB share all timing animB.shared = animA.shareTiming(); // Or: animB.useShared(animA.shareTiming()); // animC wants to share just the duration with A and B animC.useShared(animA.shared, 'duration'); // Locally override startDelay on B animB.startDelay = 5; // Reset the local override animB.useShared(animB.shared, 'duration');
The key feature of this is that sharing behaviour is explicit so users don't accidentally share properties and get surprising bugs.
Consider the following code snippet:
anim.shared.duration = 3;
An author who has never used this API before can make a pretty good guess about what is going on here, and they will certainly be aware of the possibility of side effects from this action.
At the same time, there is still a single place where all final values can be queried (and changed). Furthermore, even before adding cascading it is possible to share just some timing and not all. (In fact, I'm not quite sure how cascading would work or if it's even necessary—one level of sharing is probably enough for most uses?)
The point is not that we should do this. The API needs work, for sure. The point is to try and establish, if we leave out sharing in version 1, whether we can add it later. I hope this proposal suggests it is possible, but we can follow this path a bit further and try to write out something more concrete if others are concerned it won't work.