Re: [csswg-drafts] [scroll-animations] Support progress-based animations on finite timelines (#4862)

I did some thinking about this proposal and time-keyed Keyframe effect(#4907) one and their implications. 

Here I will try to share some of my understanding so far in form of a proposal and explore some more interesting edge cases. Hopefully this will be useful in our F2F discussion to help tease out some
key questions and decisions that need to be made.


<figure>

<table>
  <tr>
   <td>
<img src="https://user-images.githubusercontent.com/944639/81129787-87941180-8f13-11ea-8058-e1cb4474e89c.png" width="" alt="alt_text" title="image_tooltip">
   </td>
   <td>

<img src="https://user-images.githubusercontent.com/944639/81129811-97135a80-8f13-11ea-8717-85e4c1a3554d.png" width="" alt="alt_text" title="image_tooltip">


   </td>
   <td>

<img src="https://user-images.githubusercontent.com/944639/81129830-a4304980-8f13-11ea-8b30-3bcc4063ebc4.png" width="" alt="alt_text" title="image_tooltip">

   </td>
  </tr>
</table>

<figcaption>
<center>
<strong>Figure1.</strong> Visual summary of the changes proposed to the model in each proposal.
</center>
</ficaption>
</figure>

<details>
    <summary>Graphviz Dot file</summary>
digraph D {
  label = "Current model";
  labelloc = "t"; 



  TL [
    shape=invhouse 
    label=<Timeline <br/><font point-size='10'> <i>t: 0 to infinity</i></font> >
  ]
  
  AN [
      shape=diamond
      label=<Animation <br/><font point-size='10'> <i>t: 0 to duration</i></font> > 
     ]

  KFE [
      shape=box
      label=<Keyframes <br/><font point-size='10'> <i>p: 0 to 1</i></font> > 
      ]

  TL -> AN [label="  time"  fontcolor=grey]
  AN -> KFE [label="  progress" fontcolor=grey]

}


</details>



## TL;DR

Below are some changes to the web animation model and API that I think will achieves both these usecases. In the new model the animation continues to produce a progress value while it can receive either progress or time from its timeline. Also the keyframes may be keyed by time values but they continue to consume only progress. Later I discuss some of the edge cases associated with these changed. 

Summary of proposed changes:

*   Allow progress as input/output where time is allowed and vice versa.
    *   `Animation.{duration, delay, endDelay, currentTime}`   (currently time)
    *   `EffectTiming.{duration, delay, endDelay, currentTime}` (currently time)
    *   `ComputedEffectTiming.{localTime, activeDuration}` (currently a mix of both)
    *   Keyframe offsets  (currently progress)
*   Timelines are either progress-based (which implies finite) or time-based:
    *   `ScrollTimeline` is a progress-based timeline. In other words, it would produce a progress value but not a time value.
    *   `DocumentTimeline` would be considered a time-based timeline. In other words, it would produce a current time but not a progress value.
*   Animations can be configured as either progress-based or time-based but not a mix of both.
    *   In other words, both duration and delays should be in the same unit. (later we can explore how this restriction can be relaxed in future)
    *   Current time will match this unit.
*   Animation would continue converting their current time into simple progress values to be fed into keyframe effect.
*   Keyframe effects can be keyed by either progress or time values. When time values are used with the largest value being MAX, and iteration duration is D then we can have the following:
    *   **D is unspecified** => D is assigned to be MAX.
    *   **D  <= MAX**   =>  In this case keyframes would be played for a smaller number of iterations as specified since animation only runs for (iterations \* D) seconds.
    *   **D  > MAX**  => In this case once once time is outside of the valid range (iterations \* MAX) we clip it.
*   In terms of API:
    *   Accept timing input as unit less percentages e.g., ‘ duration: 80%’ or time e.g., duration: 800. This allows us to differentiate between them.
    *   Computed timing values could be in progress or in time depending on the animation/timeline mode. This is consistent with existing API that has both.


## Additional Detail 

*Sidenote*: I use the term progress (e.g., a value in [0, 1]) when referring to progress of animation/timeline and percentage (value in 0% 100%) when referring to a fraction of a whole (which could be time or progress).


### Dealing with conflicting modes

When we allow both progress-based and time-based timelines and animations then some combinations of these may not be valid. See the table below:

<figure>

<table>
  <tr>
   <td>Timeline
   </td>
   <td>Animation
   </td>
   <td>Keyframe
   </td>
   <td>
   </td>
  </tr>
  <tr>
   <td rowspan="4" >T
   </td>
   <td rowspan="2" >T
   </td>
   <td>T
   </td>
   <td>Valid
<p>
<em>Note: duration may be left unspecified, if it is specified then it possible that progress > 1 in which case we cap it to 1</em>
   </td>
  </tr>
  <tr>
   <td>P
   </td>
   <td>Valid <-- **Current Model**
   </td>
  </tr>
  <tr>
   <td style="background-color:red">P
   </td>
   <td style="background-color:red">P
   </td>
   <td>**Invalid**
<p>
<em>Note: Cannot turn <code>t:0->infinity</code> into a progress.</em>
   </td>
  </tr>
  <tr>
   <td>P
   </td>
   <td>T
   </td>
   <td>Valid
   </td>
  </tr>
  <tr>
   <td rowspan="4" >P
   </td>
   <td rowspan="2" >T
   </td>
   <td>T
   </td>
   <td>Valid with caveat
<p>
<em>Note: Each animation converts <code>p:0->1</code> into <code>t:0->duration </code>with a caveat that duration cannot be infinity.</em>
   </td>
  </tr>
  <tr>
   <td>P
   </td>
   <td>Valid with the same caveat.
   </td>
  </tr>
  <tr>
   <td rowspan="2" >P
   </td>
   <td>T
   </td>
   <td>Valid
   </td>
  </tr>
  <tr>
   <td>P
   </td>
   <td>Valid
<p>
Note: This is the fully progress-based case.
   </td>
  </tr>
</table>


<figcaption>
<center><strong>Table1.</strong> Valid combination of time-based (<strong>T</strong>) and progress-based (<strong>P</strong>) for timeline, animation, keyframe. </center>
</figcaption>

</figure>

So here are the invalid situation in this model with some suggested reasonable behavior for them:



*   Assigning a progress-based animation to a document timeline.
    *   There is no intrinsic duration for this animation. Set the animation duration to zero (similar to how [currently](https://drafts.csswg.org/web-animations-1/#dom-effecttiming-duration) duration ‘auto’ is handled).
*   Assigning a time-based animation with infinite duration (iterations: Infinity) to a progress based timeline.
    *   It is not meaningful to map a [0, 1] interval into an infinite duration. A safe behavior may be to have animation current time set to 0 and log a warning. (This is already the case for scroll timeline with timeRange: auto.)

    *   See below for a discussion on the finite case of this.



Open Questions:

*   Is there a meaningful case for changing an animation from a time-based timeline to a progress-based timeline? And does our current behavior support this?


### Using progress/percentage in the API

Once we have a progress-based timeline, it obviates the need for specifying animation timings in terms of time but we can simply have them be declared in terms of progress as well. Another alternative is to accept percentages which can be either mapped to time or progress making animation timing relative which is especially nice once we have group effects. 

I think here are the main questions:


1.  Should we reuse existing attributes e.g., Animation.currentTime and overload them to represent both <percentage> and <time> values or should we introduce new properties e.g., Animation.currentProgress?
1.  What is the type/format that should be used to represent progress? The answer to this may depend on the answer to the above but here are some options to consider:
    1.  Progress represented as a double between 0 - 100.
    1.  Progress represented as a double between 0 - 1 (e.g., 0.2).
    1.  Progress represented as a percentage string (e.g., ‘20%’)
1.  Do we have a different style for input timing properties vs computed ones?

The introduction of a new set of properties seems to increase the API surface and make writing generic animation code less ergonomic as devs need to check the animation type and use the right property. 

Another consideration is that if we overload the same properties then for input properties we need to have different JS types (e.g., string and double) for percentage/progress and time values so that parsing logic can differentiate them. Also for some output properties reading them from Animation API and using them again with the same API to create a new animation should ideally preserve the semantic. The latter case does not matter for computed output attributes (e.g., Animation.currentTime)

Note that there is some precedent to consider here:

1.  Keyframe offset syntax: for these we use a (0-1) progress values for both input and computed offsets in Javascript. Interestingly we use percentages in CSS. 
1.  ComputedEffectTiming.progress: for these we use a (0-1) progress value.

Having said these I suggest this approach:

Accept timing input (duration, delay,...) as unit less percentages ( duration: '80%') and time (duration: 800). This allows us to differentiate between them based on type. The fact that percentages are unit less means that in future we can resolve them against other time values and they are not limited to representing progress. (If we go with this idea, perhaps we should allow use of percentage for specifying offsets.)

Output computed timing values as double which may be progress or time depending on the animation/timeline mode. This is consistent with existing API that has both except that we now overload the same property. I think this is simple and lends itself to doing math and computation using computed values.

**TODO**: look into CSSOM Types to see if we should use those instead?


Below is an example of how the above proposal would look like in practice.

```js

// Lets create a progress-based animation with a progress-based timeline (scroll timeline)
Const t = new ScrollTimeline()
const a = $div.animate({opacity: 0}, {duration: '80%', delay: '20%', timeline: t};

// The conversion scheme allows the same to work if I use time values for duration 

const b = $div.animate({opacity: 0}, {duration: 800 , delay: 200, timeline: t};

document.scrollingElement.scrollTop = 100; // assume this scrolls the page half-way.

// Scroll timeline is progress-based
a.timeline.currentTime;  // 0.5

// A is a progress based animation so we return <progress>
a.currentTime;  // 0.5
a.effect.getComputedTiming().localTime; // 0.5

// Progress is one case where we have already use progress
a.effect.getComputedTiming().progress; // 0.375


// B is a time-based animation so we match that and output accordingly
b.currentTime;  // 500
b.effect.getComputedTiming().localTime; // 500
b.effect.getComputedTiming().progress; // 0.375

// What about input values? We maintain the format for them which means they can be reused for another animation without losing their semantic.
a.effect.getTiming().duration; // "80%"
b.effect.getTiming().duration; // 800

```



### Assigning time-based animations to a progress-based timeline

This is an interesting edge case. What happens when we associate a time-based animation with a finite duration to a progress based timeline. 

It will be reasonable if this case works as expected. To enable this we need a mapping between progress values to time values. Here is a simple proposal that archives this:



1.  Compute animation end time T = `max(start delay + active duration + end delay, 0)` which will be in time unit.
1.  Compute _TimeToProgressRatio_ as 1/T 
1.  With this ratio all time values can be converted into progress.

<figure>
<img src="https://user-images.githubusercontent.com/944639/81130025-45b79b00-8f14-11ea-90e8-d1c781f99aa7.png">

<figcaption>
<strong>Figure2.</strong> In this example the _TimeToProgressRatio_ is 1/200 which means 50ms delay gets converted into ‘0.25’ progress value.
</figcaption>

</figure>

This process ensures that the relationship between delay/duration/endDelay is preserved and they have the intended effect. Note that each animation has its own specific _TimeToProgressRatio _so associating another animation to the same timeline does not affect any other animations.

So let’s consider a bit more complex case where there is a time-keyed keyframes involved. Here we first resolve auto/unspecified values against children. In this case we resolve the auto value to 100ms. So the animation end is 150ms which gives us a 0.66 time to percent ratio. 


<figure>
<img src="https://user-images.githubusercontent.com/944639/81130046-56681100-8f14-11ea-8402-dfbdd0bc3dd9.png">

<figcaption>
<strong>Figure 3.</strong> A more complicated scenario involving auto duration and time to progress mapping.</figcaption>

</figure>

Putting it all together:

1.  Resolve any unspecified/auto value based on child effect’s intrinsic values.
1.  Compute the animation end time which should be in time or progress units (and not mixed).
1.  Convert animation timing units to match timeline:
    1.  animation is time-based &&  timeline is progress-based => if animation end is infinite set time 0 otherwise compute _TimeToProgressRatio_ and convert time to progress per above.
    1.  animation is progress-based &&  timeline is time-based => if DocumentTimeline, set time to 0.
    1.  Otherwise no work is necessary.
1.  At the end of this step the computed timing values would be either in <progress> or in time units and consistent with the timeline units.


### What about GroupEffects (TODO/Defer)

There was a suggestion that once we support progress/percentages for timings, we can perhaps use the same for group effects.

I think this can work nicely if we use percentages for input timing properties. For example consider having a child effect duration/delay be specified in percentages.
These get resolved agains the parent group effect duration.


*   GroupEffects timing may be unspecified in which case they use intrinsic durations from their children.
*   The children effect can use percentages which get resolved against group effect duration.

TODO: Things get more complicated if we mix both of these and create inter-dependencies. This needs more consideration.
 

### Mixing time/progress values in a single Animation (TODO/Defer)

It is not clear to me at this point how or if at all this should work.
I think it is reasonable to not allow this for v1 but relax the restriction if we come up with a good model for it.

-- 
GitHub Notification of comment by majido
Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/4862#issuecomment-624394015 using your GitHub account

Received on Wednesday, 6 May 2020 01:14:50 UTC