Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[web-animations-1] [scroll-animations-1] Support automatic duration scroll animations #4862 #4890

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 10 additions & 58 deletions scroll-animations-1/Overview.bs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ Using the CSS markup:
<pre class='lang-css'>
@media (prefers-reduced-motion: no-preference) {
div.circle {
animation-duration: 1s;
animation-timing-function: linear;
animation-timeline: collision-timeline;
}
Expand Down Expand Up @@ -182,13 +181,13 @@ if (window.matchMedia('(prefers-reduced-motion: no-preference)').matches) {
end: '300px'
});

const left = leftCircle.animate({ transform: 'translate(300px)' }, 1000);
const left = leftCircle.animate({ transform: 'translate(300px)' });
left.timeline = collisionTimeline;

const right = leftCircle.animate({ transform: 'translate(350px)' }, 1000);
const right = leftCircle.animate({ transform: 'translate(350px)' });
right.timeline = collisionTimeline;

const union = unionCircle.animate({ opacity: 1 }, { duration: 1000, fill: "forwards" });
const union = unionCircle.animate({ opacity: 1 }, { fill: "forwards" });
union.timeline = new ScrollTimeline({
source: scrollableElement,
start: '250px',
Expand Down Expand Up @@ -244,7 +243,7 @@ If we use this API for this case, the example code will be as follow:

<pre class='lang-javascript'>
if (window.matchMedia('(prefers-reduced-motion: no-preference)').matches) {
var animation = div.animate({ width: '100%' }, { duration: 1000, fill: "forwards" });
var animation = div.animate({ width: '100%' }, { fill: "forwards" });
animation.timeline = new ScrollTimeline(
{ start: '0px' }
);
Expand Down Expand Up @@ -348,7 +347,6 @@ dictionary ScrollTimelineOptions {
ScrollDirection orientation = "block";
(DOMString or ElementBasedOffset) start = "auto";
(DOMString or ElementBasedOffset) end = "auto";
(double or ScrollTimelineAutoKeyword) timeRange = "auto";
};

[Exposed=Window]
Expand All @@ -358,13 +356,13 @@ interface ScrollTimeline : AnimationTimeline {
readonly attribute ScrollDirection orientation;
readonly attribute (DOMString or ElementBasedOffset) start;
readonly attribute (DOMString or ElementBasedOffset) end;
readonly attribute (double or ScrollTimelineAutoKeyword) timeRange;
};
</pre>

A <dfn>scroll timeline</dfn> is an {{AnimationTimeline}} whose time values are
determined not by wall-clock time, but by the progress of scrolling in a
[=scroll container=].
[=scroll container=]. The {{AnimationTimeline/duration}} of a <a>scroll timeline</a> is
100%.

<div link-for-hint="ScrollTimeline">

Expand All @@ -387,7 +385,7 @@ determined not by wall-clock time, but by the progress of scrolling in a

1. Set the {{ScrollTimeline/source}} of |timeline| to |source|.

1. Assign the {{ScrollTimeline/orientation}}, {{ScrollTimeline/start}}, {{ScrollTimeline/end}}, and {{ScrollTimeline/timeRange}} properties of |timeline| to the corresponding value from |options|.
1. Assign the {{ScrollTimeline/orientation}}, {{ScrollTimeline/start}}, and {{ScrollTimeline/end}} properties of |timeline| to the corresponding value from |options|.

Issue(5202): The above steps need clarification, particularly with regards to
handling of null values for |source|.
Expand All @@ -414,25 +412,6 @@ handling of null values for |source|.
offset=] in the direction specified by {{orientation}} that constitutes the
end of the range in which the timeline is active.

: <dfn attribute for=ScrollTimeline>timeRange</dfn>
:: A time duration that allows mapping between a distance scrolled, and
quantities specified in time units, such as an animation's [=duration=] and
[=start delay=].

Conceptually, {{timeRange}} represents the number of milliseconds to map to
the scroll range defined by {{start}} and {{end}}. As a result, this value
does not have a correspondence to wall-clock time.

This value is used to compute the timeline's [=effective time range=], and
the mapping is then defined by mapping the scroll distance from
{{start}} to {{end}}, to the [=effective time range=].

Issue(4862): We are working to remove the need for {{timeRange}} to be declared.
The most recent work on this involved introduction of the concept of
"progress-based animations" to web animations.

</div>

### Scroll Timeline Offset ### {#scroll-timeline-offset-section}

An <dfn>effective scroll offset</dfn> is a scroll position for a given [=scroll
Expand Down Expand Up @@ -647,38 +626,13 @@ if (window.matchMedia('(prefers-reduced-motion: no-preference)').matches) {
transform: ['translateX(0)',q 'translateX(50vw)'],
opacity: [0, 1]
}, {
timeline:timeline,
duration: 1000
timeline: timeline
}
);
}
</pre>


</div>

### The effective time range of a {{ScrollTimeline}} ### {#effective-time-range-algorithm}

The <dfn>effective time range</dfn> of a {{ScrollTimeline}} is calculated as
follows:

<div class="switch">

: If the {{timeRange}} has the value <code>"auto"</code>,
:: The [=effective time range=] is the maximum value of the
[=target effect end=] of all animations
directly associated with this timeline.

If any animation directly associated with the timeline has a
[=target effect end=] of infinity, the [=effective time range=]
is zero.

: Otherwise,
:: The [=effective time range=] is the {{ScrollTimeline}}'s
{{timeRange}}.

</div>

### The effective scroll range of a {{ScrollTimeline}} ### {#effective-scroll-range-algorithm}

The procedure to calculate <dfn>effective scroll range</dfn> of a
Expand Down Expand Up @@ -721,12 +675,12 @@ The [=current time=] of a {{ScrollTimeline}} is calculated as follows:
offset.

1. If <var>current scroll offset</var> is greater than or equal to [=effective
end offset=], return [=effective time range=].
end offset=], return [=duration=].

1. Return the result of evaluating the following expression:

<blockquote>
<code>(<var>current scroll offset</var> - [=effective start offset=]) / [=effective scroll range=] &times; [=effective time range=]</code>
<code>(<var>current scroll offset</var> - [=effective start offset=]) / [=effective scroll range=] &times; [=duration=]</code>
</blockquote>


Expand Down Expand Up @@ -894,7 +848,6 @@ interface CSSScrollTimelineRule : CSSRule {
{ width: "100vw" }
],
{
duration: 1000,
easing: "linear",
fill: "forwards"
});
Expand Down Expand Up @@ -937,7 +890,6 @@ interface CSSScrollTimelineRule : CSSRule {
/* This name is used to select both the keyframes and the
scroll-timeline at-rules. */
animation-name: progress;
animation-duration: 1s;
animation-fill-mode: forwards;
animation-timing-function: linear;
}
Expand Down
40 changes: 34 additions & 6 deletions web-animations-1/Overview.bs
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,12 @@ purpose of synchronization.
At any given moment, a [=timeline=] has a single current [=time value=] known
simply as the timeline's <dfn lt="timeline current time">current time</dfn>.

The <dfn lt="timeline duration">duration</a> of a timeline gives the
maximum value a timeline may generate for
[=timeline current time|current time=]. This is used to calculate the
[=iteration duration=] when the string <code>auto</code> is specified such that
the animation fills the available time.

A <a>timeline</a> is <dfn export lt="monotonically increasing timeline"
local-lt="monotonically increasing">monotonically increasing</dfn> if its
reported [=timeline current time|current time=] is always greater than or equal
Expand Down Expand Up @@ -727,6 +733,9 @@ is calculated as a fixed offset from the |now| timestamp provided each time the
This fixed offset is referred to as the document timeline's <dfn>origin
time</dfn>.

The <a lt="timeline duration">duration</a> of a [=document timeline=] is
null.

Issue(2079): There must be a better term than &ldquo;origin time&rdquo;&mdash;
it's too similar to &ldquo;time origin&rdquo;.

Expand Down Expand Up @@ -3974,6 +3983,7 @@ The <code>AnimationTimeline</code> interface {#the-animationtimeline-interface}
[Exposed=Window]
interface AnimationTimeline {
readonly attribute double? currentTime;
readonly attribute double? duration;
readonly attribute TimelinePhase phase;
};
</pre>
Expand All @@ -3987,6 +3997,9 @@ interface AnimationTimeline {
: <dfn attribute for=AnimationTimeline>phase</dfn>
:: Returns the <a lt="timeline phase">phase</a> for this timeline.

: <dfn attribute for=AnimationTimeline>duration</dfn>
:: Returns the <a lt="timeline duration">duration</a> for this timeline.

</div>

The <code>DocumentTimeline</code> interface {#the-documenttimeline-interface}
Expand Down Expand Up @@ -4412,10 +4425,8 @@ apart from the timing model.
{{AnimationEffect/getComputedTiming()}} must return a number
corresponding to the calculated value of the <a>iteration duration</a>
as defined in the description of the {{EffectTiming/duration}} member of
the {{EffectTiming}} interface.

In this level of the specification, that simply means that an
<code>auto</code> value is replaced by zero.
the {{EffectTiming}} interface. A percentage value is represented as its
double value (i.e. 100% returns 1.0).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this lose information? i.e. it doesn't roundtrip and it's not possible to tell whether 1.0 represents a percent or a millisecond time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's expected that getComputedTiming can lose information though, right? I thought we had settled on the progress being a double in the [0-1] range in our previous discussions.


* {{EffectTiming/fill}} &ndash; likewise, while
{{AnimationEffect/getTiming()}} may return the string <code>auto</code>,
Expand Down Expand Up @@ -4505,12 +4516,20 @@ dictionary OptionalEffectTiming {
the number of milliseconds from the [=start time=] of the associated
<a>animation</a> to the start of the <a>active interval</a>.

If the <a>iteration duration</a> is treated as a percentage, the start
delay will be treated as 0. Future levels of this specification are
expected to introduce support for percentage start delays.

: <dfn dict-member for=EffectTiming>endDelay</dfn><dfn dict-member
for=OptionalEffectTiming lt=endDelay></dfn>
:: The <a>end delay</a> which represents the number of milliseconds
from the end of an <a>animation effect</a>'s <a>active interval</a>
until its <a>end time</a>.

If the <a>iteration duration</a> is treated as a percentage, the end
delay will be treated as 0. Future levels of this specification are
expected to introduce support for percentage start delays.

: <dfn dict-member for=EffectTiming>fill</dfn><dfn dict-member
for=OptionalEffectTiming lt=fill></dfn>
:: The <a>fill mode</a> which defines the behavior of the <a>animation
Expand Down Expand Up @@ -4580,8 +4599,17 @@ dictionary OptionalEffectTiming {
time taken to complete a single iteration of the <a>animation
effect</a>.

In this level of this specification, the string value <code>auto</code>
is treated as the value zero for the purpose of timing model calculations
If the <a lt="timeline duration">duration</dfn> of the Animation's
<a>timeline</a> is a percentage, a non <code>auto</code> duration will
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you just required the duration to be a percentage, do you need to mention non-auto?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just required the timeline duration to be a percentage, however the non-auto duration refers to the animation duration.

be treated as 0. Future levels of this specification are expected to
introduce support for scaling duration, iterations and start/end delays
to fill a percentage timeline duration.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably should update the paragraph before this, and then describe the syntax for percentages (and how they are encoded as strings).

Also, it seems like the duration of a timeline can't be a percentage?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, my thinking was that internally we would recognize that the timeline duration is a percentage even though the value by inspection would be 1.0 (consistent with getComputedStyle). However, I'm guessing the issue is that if it's not represented in the WebIDL return value we can't recognize this.

Some options:

  • Define and use a subclass of AnimationTimeline (i.e. ProgressTimeline) and check the instance type to determine which behavior to apply.
  • Use a string value for duration

Any other ideas? Any recommendations?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good point. I think it's probably helpful to ask what we want getTiming() and getComputedTiming() to return when we later introduce effects that are a percentage of their parent group's time (or ancestor timeline if we introduced fixed duration document timelines for example).

I think in that case we might want something like:

  • getTiming().duration to return CSSNumberish, i.e. a double in the case of a fixed time (for compatibility) or a CSSNumberValue to represent a percentage.
  • getComputedTiming().duration to return a number which is either the resolved duration in ms (in part for compatibility, and in part so we can determine the actual duration for complex group effects) or a value between [0, 1] or [0, 100] to represent a percentage of an auto duration timeline.

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this, if we do the same for the timeline duration then we can know when a defined duration on the timeline is a time (where time-based values in the animations don't need to be dropped) or a percentage (where we convert time based animations to their respective proportions). I'll work on making this change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually we already specify that the return value from getTiming() must be auto if the duration is specified as auto so we should be returning the string 'auto' from getTiming which will round trip. I still like the idea of using CSSNumberish for the timeline duration though so that we clearly know whether the duration is time-based or progress (percentage) based.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could consider returning CSSNumberish from computed timing - that would sidestep the issue of converting the percentages to a double value. It will mean changing a lot of values to be CSSNumberish, but this might be cleaner in the long run. WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure. Once we are able to use percentages that resolve to times (e.g. duration is 50% of my parent group) I wonder if it would be useful to have getComputedTiming() return the actual number of seconds it resolves to? In which case, should percentages that resolve against scroll timelines resolve to pixels?

Unfortunately getComputedStyle doesn't give an entirely clear precedent here since it sometimes resolves percentages (e.g. for width and height) but otherwise doesn't. I guess the intention is that it should always return percentages. So maybe we should return CSSNumberish instead?

It would be interesting to know if anyone else has an opinion on this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure. Once we are able to use percentages that resolve to times (e.g. duration is 50% of my parent group) I wonder if it would be useful to have getComputedTiming() return the actual number of seconds it resolves to?

I could see this being useful, and might be worth doing.

In which case, should percentages that resolve against scroll timelines resolve to pixels?

This depends on what you consider to be the underlying value of the ScrollTimeline right? It could be pixels but it could also only operate in percentage of scroll or percentage of duration?

Unfortunately getComputedStyle doesn't give an entirely clear precedent here since it sometimes resolves percentages (e.g. for width and height) but otherwise doesn't. I guess the intention is that it should always return percentages. So maybe we should return CSSNumberish instead?

I think returning CSSNumberish and keeping percentages in the computed timing makes sense. One thing that I believe supports this is this note from css-cascade (emphasis mine):

Note: In general, the computed value resolves the specified value as far as possible without laying out the document or performing other expensive or hard-to-parallelize operations, such as resolving network requests or retrieving values other than from the element and its parent.

cc/ @andruud

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving percentages intact sounds entirely reasonable.

Although depending on layout in this case is probably less painful than normal, given the snapshotting described in Avoiding cycles with layout, which means we can resolve the percentages against whatever is known from the previous snapshot?

returning CSSNumberish

Note that (from brief inspection of css-typed-om & Blink): there seems to be an "understanding" that a return value of CSSNumberish means you always get a CSSNumericValue. So readonly CSSNumberish attributes would probably ideally be CSSNumericValue attributes. So since this would be the first deviation from this (I think), we should probably specify under which circumstances we can expect a raw double.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that (from brief inspection of css-typed-om & Blink): there seems to be an "understanding" that a return value of CSSNumberish means you always get a CSSNumericValue. So readonly CSSNumberish attributes would probably ideally be CSSNumericValue attributes. So since this would be the first deviation from this (I think), we should probably specify under which circumstances we can expect a raw double.

That sounds reasonable. If we were designing this from scratch I think we would always return a CSSNumericValue and it is only compatibility constraints that mean we sometimes need to return an actual number.


The string value <code>auto</code> is treated as the
<a lt="timeline duration">duration</dfn> of the Animation's <a>timeline</a>
(zero if the timeline is null or the timeline's duration is null)
divided by the <a>iteration count</a>
for the purpose of timing model calculations
and for the result of the {{EffectTiming/duration}} member returned
from {{AnimationEffect/getComputedTiming()}}.
If the author specifies the <code>auto</code> value user agents must,
Expand Down