w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.5k stars 661 forks source link

'effective time range' for timeRange: auto has some undesirable behaviors #4346

Closed stephenmcgruer closed 2 years ago

stephenmcgruer commented 5 years ago

For effective time range, the spec currently says:

If the timeRange has the value "auto", 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.

There are a few questions we can ask about this.

If there are no associated animations

console.log(new ScrollTimeline({scrollSource: scroller}).currentTime);

What is the value of the above? We have to multiply the result by effective time range, but that isn't specified when there are no associated animations.

This could probably be fixed by just making it either 1 (so its a scroll fraction by default) or making it the scroll range (so it maps to scroll offset), but there's another issue...

When a new animation is associated with the same timeline

const timeline = new ScrollTimeline({scrollSource: scroller});
new Animation(new KeyframeEffect(target, keyframes, { duration: 1000; }), timeline).play();
// Some time later...
new Animation(new KeyframeEffect(other_target, other_keyframes, { duration: 2000; }), timeline).play();

When the second animation is created, the ScrollTimeline's currentTime suddenly jumps and as such so would the visual output of the first animation.

The problem with timeRange: auto

The idea behind timeRange: auto makes a lot of sense - most of the time, you want the output of the ScrollTimeline to be linearly mapped to the possible values for whatever animation is using it. But the way it is specified falls apart in practice - under the case of multiple animations in particular.

Arguably what a web developer really wants is to say "I'm using a finite timeline so I don't actually care about the duration of my effect; just make it match the timeline range". But we implemented that in a backwards manner; mutating the timeline to fit the effect duration rather than the effect duration to fit the timeline.

I don't have a specific proposal for fixing this yet; a naive approach would be to allow a special value for iteration duration which causes it to be taken from the timeline range, but this likely falls apart in the face of iteration count, etc.

birtles commented 5 years ago

If there are no associated animations, wouldn't zero be a better default? (To match the Infinity case, and my intuition at least).

I'm not sure the jumping behavior is a problem? There are plenty of other ways you can manipulate animations so that they jump? (e.g. setting the timing function, direction etc. If we could have, we would have made setting playbackRate jump too.)

Following your suggestion, I guess we can draw a parallel with scroll anchoring. That is, if scroll position was described as a percentage and content was added to the page, the content would "jump". Indeed, this used to happen in some circumstances before browsers added scroll anchoring. So, another way to approach this would be to add time anchoring (which is what setting the playback rate currently does). You'd have to decide what circumstances trigger it, however (e.g. presumably adding an animation to the timeline would, but what about altering the duration or iteration count after adding it?).

stephenmcgruer commented 5 years ago

I'm not sure the jumping behavior is a problem? There are plenty of other ways you can manipulate animations so that they jump? (e.g. setting the timing function, direction etc. If we could have, we would have made setting playbackRate jump too.)

But changing timing properties requires modifying animation1 directly. The problem here is that you can alter animation1's output visuals without actually touching the animation1 object (or the target it is animating, etc). All you have to do is create a new animation, animation2 which has the same timeline but an effect with a larger duration.

stephenmcgruer commented 5 years ago

This was discussed at the Chrome/Firefox/Safari animation sync yesterday. (From memory, so excuse mistakes).

We reached a general agreement that this is 'weird', but there was less consensus on whether it is bad. Brian argued that it is reasonable to expect web developers to match the duration of animations which are reusing the same ScrollTimeline. Stephen noted that with an infinite timeline if you want your animations to run for the same time, you have to set the same duration (duh) - this is arguably just the same thing for finite timelines.

Brian proposed looking at how CSS might work, and that might help. The CSS syntax as written is definitely not final, but we (re)learned that you cannot share ScrollTimelines using the CSS syntax; each animation has its own. We discussed how ScrollTimelines are cheap and we should probably encourage developers to create one per Animation by providing examples to that end.

There were two action items, both on smcgruer@:

Action: Make the wording more accurate as to what an 'associated' animation is for a timeline. Action: document examples where timeRange: auto would not work, and propose potential solutions which could resolve these (e.g. resolve timeRange once, bind the ScrollTimeline to an animation, your crazy idea here).

stephenmcgruer commented 5 years ago

Action: Make the wording more accurate as to what an 'associated' animation is for a timeline.

Investigating this. It turns out that the Web Animations spec already defines 'associated with a timeline': https://drafts.csswg.org/web-animations/#associated-with-a-timeline . Strangely, this is defined only for an Animation Effect (and refers to the undefined version for Animation) but is used for Animation! (https://drafts.csswg.org/web-animations/#ref-for-associated-with-a-timeline)

So I think the steps are:

  1. Fix the Web Animations spec to have a section for 'associated with a timeline' for Animation
  2. Make the Scroll-linked Animations spec link to that
birtles commented 5 years ago

For Web animations, the association between an animation and a timeline is defined in prose, it's just not linkified as a definition:

An animation connects a single animation effect, called its target effect, to a timeline and provides playback control. Both of these associations are optional and configurable such that an animation may have no associated target effect or timeline at a given moment.

However, that's the association going from animation to timeline. For the purposes of this issue it might be more clear to add a definition going the other direction or add some other definition.

For example, if I have an animation associated with timeline X, but which is idle, should it be considered when calculating the time range? I suspect not and that suggests we want some other definition about the set of animations we consider. Just ones whose target effect is current or in effect for example?

stephenmcgruer commented 5 years ago

For example, if I have an animation associated with timeline X, but which is idle, should it be considered when calculating the time range? I suspect not and that suggests we want some other definition about the set of animations we consider. Just ones whose target effect is current or in effect for example?

This sounds reasonable. To make sure I understand, this primarily changes the behavior to require a resolved startTime. Without an 'is current or in effect' wording, the ScrollTimeline would still consider an Animation for timeRange: auto resolution even if the Animation hasn't started, or has been paused/finished/cancelled.

Assumption for the above: w3c/scroll-animations#31 is fixed, i.e. a ScrollTimeline can have an unresolved time value by scrolling outside the startScrollOffset/endScrollOffset bounds but not be considered newly inactive (and thus currentTime doesn't get held in place).

There is a side-effect with using 'is current or is in effect' (or similar). When you scroll outside the range of startScrollOffset/endScrollOffset, the timeline produces unresolved time values (assuming no fill mode on the timeline). This will make all Animations which use that timeline idle, which means there will be no valid choice for timeRange: auto resolution. This is not actually observable I believe, because the current-time algorithm will early-exit anyway before using timeRange.

Finally, I think we will need to declare a default timeRange value of (e.g.) 1 when there are no 'active associated' Animations (or whatever term we come up with). JS API calls to the ScrollTimeline will be quite weird, but the actual Animation will operate as expected I think.

let scrollTimeline = new ScrollTimeline({
  timeRange: 'auto',
  startScrollOffset: '10px',
  endScrollOffset: '90px',
});
let effect1 = new KeyframeEffect(target, keyframes, { duration: 1000 });
let anim1 = new Animation(effect1, scrollTimeline);
scroller.scrollTop = 50px;
console.log(scrollTimeline.currentTime);  // 500
anim1.pause(); 
console.log(scrollTimeline.currentTime);  // 0.5 (assuming default of 1)

Action: document examples where timeRange: auto would not work, and propose potential solutions which could resolve these (e.g. resolve timeRange once, bind the ScrollTimeline to an animation, your crazy idea here).

Returning to this, given our previous conversation I am actually partial to just leaving the behavior as is, with a spec note that reusing a ScrollTimeline is discouraged and making sure all our examples don't do that. This will cause issues if web authors do:

let scrollTimeline = new ScrollTimeline({ timeRange: 'auto', ... });
let effect1 = new KeyframeEffect(target, keyframes, { duration: 1000 });
let anim1 = new Animation(effect1, scrollTimeline);
anim1.play();
setTimeout(function() {
  let effect2 = new KeyfrmaeEffect(target2, keyframes2, { duration: 2000 });
  let anim2 = new Animation(effect2, scrollTimeline);
  anim2.play();  // anim1 will suddenly jump forward, without user input
}, 500);
birtles commented 5 years ago

For example, if I have an animation associated with timeline X, but which is idle, should it be considered when calculating the time range? I suspect not and that suggests we want some other definition about the set of animations we consider. Just ones whose target effect is current or in effect for example?

This sounds reasonable. To make sure I understand, this primarily changes the behavior to require a resolved startTime. Without an 'is current or in effect' wording, the ScrollTimeline would still consider an Animation for timeRange: auto resolution even if the Animation hasn't started, or has been paused/finished/cancelled.

Yes, that sounds right.

There is a side-effect with using 'is current or is in effect' (or similar). When you scroll outside the range of startScrollOffset/endScrollOffset, the timeline produces unresolved time values (assuming no fill mode on the timeline). This will make all Animations which use that timeline idle, which means there will be no valid choice for timeRange: auto resolution. This is not actually observable I believe, because the current-time algorithm will early-exit anyway before using timeRange.

The alternative, as you mention, might be to define the association as depending on having a resolved startTime or not.

Finally, I think we will need to declare a default timeRange value of (e.g.) 1 when there are no 'active associated' Animations (or whatever term we come up with). JS API calls to the ScrollTimeline will be quite weird, but the actual Animation will operate as expected I think.

That's an interesting test case. Yes, it's weird, but I can't think of any better option at this point.

Returning to this, given our previous conversation I am actually partial to just leaving the behavior as is, with a spec note that reusing a ScrollTimeline is discouraged and making sure all our examples don't do that. This will cause issues if web authors do:

In that example, I'm actually more concerned about what will currently happen if the author does not call play(), i.e. they create anim2 and associate it with scrollTimeline but don't play it. I think it might be surprising if that ends up affecting anim1 (which I believe, as the spec currently stands, it would?).

stephenmcgruer commented 5 years ago

In that example, I'm actually more concerned about what will currently happen if the author does not call play(), i.e. they create anim2 and associate it with scrollTimeline but don't play it. I think it might be surprising if that ends up affecting anim1 (which I believe, as the spec currently stands, it would?).

As the spec currently stands, yes, but if we make it a requirement that the animation is current/in effect/has a resolved start time then just creating anim2 wouldn't be sufficient unless I'm overlooking something.

stephenmcgruer commented 5 years ago

I started working on a PR for this today, and realized that it creates a circular dependency. Both being current and in effect rely (ultimately) on timeline time (via https://drafts.csswg.org/web-animations/#current-time), and so we can't make ScrollTimeline's output time rely on them.

I don't immediately see a way around that problem (time is meant to flow from the timeline outwards, so we have to be careful about inverse dependencies). It might be the case that we have to accept that idle animations would still affect timeRange: auto calculation.

birtles commented 5 years ago

I thought the suggestion was to key on whether or not the startTime was resolved?

stephenmcgruer commented 5 years ago

Ah, I misunderstood you then. I will think on the implications of keying on startTime being resolved tomorrow, thanks.

stephenmcgruer commented 5 years ago

I don't think keying off startTime will work either; that also depends on the timeline's currentTime:

https://drafts.csswg.org/web-animations-1/#playing-an-animation-section, step 8.2::

Let ready time be the time value of the timeline associated with animation at the moment when animation became ready.

This ready time is what is used to set startTime, so it is a circular dependency again.

majido commented 4 years ago

I think if we manage to introduce progress-based animations as proposed in #4862 this issue will be moot.

flackr commented 2 years ago

Progress based animations are now the course of action. The primary issue brought up here no longer applies as each animation converts its own timing into progress based timing following https://drafts.csswg.org/web-animations-2/#time-based-animation-to-a-proportional-animation