w3c / csswg-drafts

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

[scroll-animations] Broader scope of scroll timelines #7759

Closed fantasai closed 1 year ago

fantasai commented 2 years ago

From #7047, it might be worth looking into the ability to split the declaration of a timeline (together with its scoping) from its actual attachment to a scroll container.

This would allow authors, for example, to declare a name on a subtree and make it available to all descendants of that subtree, and attach it to a scroll container that is a descendant within the subtree. (At the top level, declaring the name on the root element would make it global.)

Maybe something like scroll-timeline-attachment: local | defer | ancestor | closest where:

tabatkins commented 1 year ago

In that syntax, what happens if you set -root: foo; -name: bar; -axis: block;? Or is -root not list-coordinated with the other ones; it just, totally independently, declares a list of timelines to create on the element?

Assuming the latter, that sounds just fine to me.

If we need the "behave as local if no ancestor is found"-behavior, then we could do scroll-timeline-root:auto instead of none.

Hm, this comment implies that -root is list-coordinated, tho. I'm confused, then.

(I don't think this is a necessary ability, tho.)

andruud commented 1 year ago

totally independently, declares a list of timelines to create on the element?

Yes, I suppose. Your example would declare a named timeline foo (presumably for later definition), and would define a named timeline bar (presumably to attach to a prior declaration).

I'm confused, then.

Or I'm the confused one. Didn't think this part through. Yeah, disregard auto, I don't think that works well here ...

bramus commented 1 year ago

With -root defaulting to none, wouldn't that leave -name: --tl; -axis: x; in a limbo state because it has no -root to attach to?

andruud commented 1 year ago

Yes, correct. Too reliant on the shorthand?

bramus commented 1 year ago

Would be weird, no?

What if ancestor in your proposal was part of the scroll-timeline-name instead of the shorthand? I.e. animation-timeline-name: <custom-ident># ancestor?;

bramus commented 1 year ago

Or maybe you just made the case for the longhands being impractical, and we should just have scroll-timeline and scroll-timeline-root properties?

flackr commented 1 year ago

I was proposing earlier that a timeline is always defined for the name on the element setting scroll-timeline-name. The existence of root on an ancestor effectively extends the range (though technically / impl-wise is more complicated than this - see instance discussion) of that name but that name would be accessible to descendants regardless. I also think that scroll-timeline-root would be completely separate from the other scroll-timeline-* properties, not linked. There's no need to specify -root and -name on the same element unless you explicitly don't want the -name to be propagated up to a higher root.

andruud commented 1 year ago

@bramus / @flackr Yeah, I was trying to make each part of the "ground truth" (i.e. each longhand) more simple and give each thing as few jobs as possible. Then make the shorthand responsible for useful abstractions on top of that. (In theory that should be reasonable). So making -name behave "kind of like -root, sometimes" hurts. :-)

Would be weird, no?

Yes. No. Maybe. I can't tell.

Or maybe you just made the case for the longhands being impractical, and we should just have scroll-timeline and scroll-timeline-root properties?

Not making that case, this idea was discarded already? If longhands are too long, use the shorthand. :P

tabatkins commented 1 year ago

Your example would declare a named timeline foo (presumably for later definition), and would define a named timeline bar (presumably to attach to a prior declaration).

Good, that's what I assumed the meaning would be.

With -root defaulting to none, wouldn't that leave -name: --tl; -axis: x; in a limbo state because it has no -root to attach to?

Not in limbo, it would just mean you're searching for a -root: --tl on an ancestor. If there is none, then there's nothing to attach to, and the properties have no effect. (Or it implicitly creates a root on the element; either behavior works.)

There's no need to specify -root and -name on the same element unless you explicitly don't want the -name to be propagated up to a higher root.

I think you are, then, just describing the "search for an ancestor establishing a root of the same name, and if that fails, create a root on yourself" behavior, yeah?

andruud commented 1 year ago

There's no need to specify -root and -name on the same element unless you explicitly don't want the -name to be propagated up to a higher root.

I think you are, then, just describing the "search for an ancestor establishing a root of the same name, and if that fails, create a root on yourself" behavior, yeah?

That was my read too. I edited that into the proposal, how does it look now @bramus @flackr @tabatkins?

bramus commented 1 year ago

What’s the default value for scroll-timeline-root? If it’s none then it doesn’t really rhyme with the shorthand. Compare these two that both set the same values but have a different resulting scroll-timeline-root:

If it does the "search for an ancestor establishing a root of the same name, and if that fails, create a root on yourself" then feels more like that would be something that’s better represented by an auto keyword?

Iterating on that, scroll-timeline-root could be [ auto | none | <custom-ident> ]#:

Put differently: A scroll-timeline-name always wants to attach to a scroll-timeline-root with the same ident. If the -root is auto, the magic of the engine will make sure the -root is set to the -name’s ident. In case of -root set to none, the defined STL via -name will be forced to look up the ancestor tree to attach to a -root with that ident (so not auto and not none).

(<custom-ident> to be replaced by <dashed-ident> if other issue is resolved)

Shorthand-wise, it would then be:

(*) Or instead of ancestor it could simply be replaced by any of the scroll-timeline-root values.

I think that would make sense, as the short and longhands don’t have different effects.

(Haven’t re-checked the entire thread, but I feel this might have been proposed at a certain point in time and/or we might be saying the same thing and I didn’t grasp it entirely before 😅)

andruud commented 1 year ago

What’s the default value for scroll-timeline-root? If it’s none then it doesn’t really rhyme with the shorthand. Compare these two that both set the same values but have a different resulting scroll-timeline-root

But they don't set the same values if you neglect to set the root when using the longhands. The shorthand intentionally also sets -root to still have a way to keep the simple case simple:

scroll-timeline: foo => A "locally attached" block-axis timeline.

auto: Declare a STL with same ident as the -name property has set.

This requires list-coordination between -root/-name, which means that -root is actually just something that modifies the behavior of -name (and the <custom-ident> part of -root becomes pointless), which IMO brings us more or less back to the current world with -attachment.

flackr commented 1 year ago

I think you are, then, just describing the "search for an ancestor establishing a root of the same name, and if that fails, create a root on yourself" behavior, yeah?

Without -root specified, it wouldn't create a root on itself as it wouldn't allow additional descendant attachments. This makes it so that descendants with the same name can't invalidate the timeline in that scope. To go back to my earlier example you could have this:

<style>
unhappy-root {
  view-timeline-root: foo;
}
happy-subtree {
  view-timeline: foo block; 
}
.observer {
  animation: frames;
  animation-timeline: foo;
}
</style>
<unhappy-root id="A"> <!-- attached to by B and C -->
  <happy-subtree id="B"> <!-- establishes itself as foo timeline for descendants AND attaches to foo timeline on A -->
    <div class="observer"></div> <!-- animates to timeline on "B" -->
    <happy-subtree id="C"> <!-- establishes itself as foo timeline for descendants AND attaches to foo timeline on A -->
      <div class="observer"></div> <!-- animates to timeline on "C" -->
    </div>
  </happy-subtree>
  <div class="observer"></div> <!-- unhappy - has two timelines associated with the A timeline -->
</unhappy-root>

If B established an attachable root, the observer within B wouldn't run because there would be two attachments there, B and C.

flackr commented 1 year ago

@bramus / @flackr Yeah, I was trying to make each part of the "ground truth" (i.e. each longhand) more simple and give each thing as few jobs as possible. Then make the shorthand responsible for useful abstractions on top of that. (In theory that should be reasonable). So making -name behave "kind of like -root, sometimes" hurts. :-)

My intention was that -name establishes the current timeline as the one providing that name for the current subtree but does not establish a root that descendants can connect to. It does this regardless of whether or not there is a root timeline it also attaches to.

flackr commented 1 year ago

Thinking about this more, I think we should call this timeline-root and let both view and scroll timelines attach to it. The animation-timeline property doesn't care whether it gets an attaching view or scroll timeline, just looks up the name. This also makes it more obviously disconnected from the *-timeline shorthands.

Sidenote: I'm not sure which timeline we pick if an element defines a view-timeline-name: foo and a scroll-timeline-name: foo and itself/descendants try to use animation-timeline: foo.

tabatkins commented 1 year ago

Okay, after chatting with flackr privately I finally understand what they're suggesting. The model they're asserting was flying over my head. ^_^

Okay, so flackr's idea is this:

This model means we don't need to do any shenanigans with the shorthand. scroll-timeline just sets -name and -axis, creating local timelines. (view-timeline also sets -inset, or I guess could do so if we allow it in the shorthand.) To set up a deferred timeline you use scroll-timeline-root instead, which is not part of the property group.

After giving it some thought while writing this out, I think this is my favorite mental model so far. It doesn't require any syntax contortions, you don't have useless syntax combos, and while it creates more theoretical objects (both a local and a deferred timeline object, rather than just creating one timeline object that's lives higher up than its attached scroller) this multiplication gives a simpler model overall, I think.

ydaniv commented 1 year ago

I'm also in favor of the -root proposal, with a few notes:

I know I'm also repeating much of what's said above, also wanted to list it again in a single place, with a couple of my notes. (:

ydaniv commented 1 year ago

I think I didn't see the last 3 comments from Friday when writing the above, but I think we're pretty much aligned.

@flackr:

Thinking about this more, I think we should call this timeline-root and let both view and scroll timelines attach to it. The animation-timeline property doesn't care whether it gets an attaching view or scroll timeline, just looks up the name. This also makes it more obviously disconnected from the *-timeline shorthands.

If we do that and later want to introduce another timeline (say hover-timeline) we'll get into trouble there. While it does look a bit odd to separate between the two, they do solve quite different scenarios, so I think it won't be noticed in practice. @bramus you did tons of demos, WDYT?

flackr commented 1 year ago

If we do that and later want to introduce another timeline (say hover-timeline) we'll get into trouble there. While it does look a bit odd to separate between the two, they do solve quite different scenarios, so I think it won't be noticed in practice. @bramus you did tons of demos, WDYT?

Would this not be relevant for any named animation timeline? If we introduce other timelines that are visible to animation-timeline the intention would seem to be to make them visible to other elements at which point I feel like they should participate in the same mechanism. If hover-timeline (or some other specific timeline) for some reason should only be usable on the same element, we could make it so that it can only be defined anonymously (e.g. similar to scroll() or view() anonymous functions) and not named. Note that no matter what we do, the JS api would allow using any element's timeline on any other element.

ydaniv commented 1 year ago

Would this not be relevant for any named animation timeline? If we introduce other timelines that are visible to animation-timeline the intention would seem to be to make them visible to other elements at which point I feel like they should participate in the same mechanism.

Hmm, I see what you mean. If animation-timeline already converges all names to the same sink, we may as well duplicate that to the -root mechanism. And anyhow, we already have the name-based handshake. ok, so SGTM on timeline-root.

bramus commented 1 year ago

Also a fan of a unified property name.

Going further, should it be animation-timeline-root to hint at its intended use in animation-timeline? Or, asked differently: do timelines make sense beyond animations? If yes, then fine with just timeline-root

ydaniv commented 1 year ago

do timelines make sense beyond animations?

Beyond animations I'm thinking of effects that involve canvases and/or media. But not sure whether these are things we'll actually introduce...

For example: is there a chance we'll consider introducing attaching a scroll/view-timeline to a video element?

andruud commented 1 year ago

The animation-timeline property doesn't care whether it gets an attaching view or scroll timeline, just looks up the name.

Not quite, you can have both scroll and view timelines side-by-side, and animation-timeline prioritizes one over the other.

Also a fan of a unified property name.

OK, but then we'd have to define which kind of timeline timeline-root prefers to attach to, and what to prioritize if the same element defines a deferred timeline, a scroll timeline, and a view timeline all named "foo". IMO we can default to two -root properties, and discuss a hypothetical unification separately.

https://github.com/w3c/csswg-drafts/issues/7759#issuecomment-1527936739

SGTM, that mental model is actually quite close to the Blink implementation.

But what does @fantasai think about it? It doesn't nest well by default, since a (local) timeline is exposed whether it likes it or not. E.g. you have to explicitly block by specifying a -root to avoid your local timeline potentially messing up deferred timelines above you.

flackr commented 1 year ago

Not quite, you can have both scroll and view timelines side-by-side, and animation-timeline prioritizes one over the other.

It's still a single answer for the used timeline for a given name though.

Also a fan of a unified property name.

OK, but then we'd have to define which kind of timeline timeline-root prefers to attach to, and what to prioritize if the same element defines a deferred timeline, a scroll timeline, and a view timeline all named "foo". IMO we can default to two -root properties, and discuss a hypothetical unification separately.

But we've already decided that a deferred timeline is invalid if more than one timeline attaches to it, right? So you wouldn't get any timeline from the root, would you?

But what does @fantasai think about it? It doesn't nest well by default, since a (local) timeline is exposed whether it likes it or not. E.g. you have to explicitly block by specifying a -root to avoid your local timeline potentially messing up deferred timelines above you.

The subtree happily animates with its local timeline though, which I thought was the main concern, but happy to hear @fantasai's thoughts.

andruud commented 1 year ago

Re. "unified -root property", I now realize that this means that the type of timeline instance (as seen by JS) is dynamic depending on what the descendants has. This means that timeline lookup (from animation-timeline) doesn't know which timeline to connect an animation to at the time it needs to know that. (See related discussion https://github.com/w3c/csswg-drafts/pull/8680#discussion_r1157642033). The current spec takes care to avoid this problem, and we do need to maintain that in future iterations. So I think we should reject the "unified -root property" idea outright.

flackr commented 1 year ago

Re @andruud, this dynamic type issue can be avoided by either having the deferred timeline not have a specific type (just a generic animation timeline type), or by implementing the suggestion above that the developer does not ever see the "deferred" instance and instead the observed timeline in JS is the actual attached timeline.

ydaniv commented 1 year ago

+1 for using the attached timeline and not the deferred, much more intuitive

andruud commented 1 year ago

the observed timeline in JS is the actual attached timeline

I'd avoid this. It means multi-frame weirdness from Scroll/ViewTimeline is leaking more than it needs to. You would see the attached timeline as of the previous frame, which for newly created animations means getAnimations()[i].timeline would return no timeline.

A simple solution is to just use two -root properties.

flackr commented 1 year ago

You would see the attached timeline as of the previous frame

Can you explain this? If the timeline attachment changes it changes for the current frame doesn't it? Otherwise you could have a flash of content at a completely different position.

The previous frame current time in the spec is meant for cases where the source isn't changing so that we don't have to flush style and layout again if the scroll position changes after or as a result of animating.

which for newly created animations means getAnimations()[i].timeline would return no timeline.

getAnimations() forces a style and layout to ensure it picks up newly created animations. Wouldn't this pick up the currently resolved timeline?

A simple solution is to just use two -root properties.

It's putting a bit of extra awkwardness on developers to have two root properties gathering names for the same conceptual animation-timeline namespace which could hypothetically include future named timeline types as well.

andruud commented 1 year ago

If the timeline attachment changes it changes for the current frame doesn't it?

Actually yes. Disregard what I said. Got confused I guess. :-)

The deferred timeline's snapshot won't be updated to take into account the new attachment until the next frame, but getAnimations()[i].timeline would still return the new attachment.

Then the only issue I can think of is that e.g. .timeline = .timeline would not be no-op. E.g. calling that on an animation connected to a deferred timeline with no attached timeline would remove the timeline. But as you pointed out elsewhere, that might be similar (enough) to how poking the web animations API too hard generally disconnects the thing from being automatically updated by CSS.

fantasai commented 1 year ago

Finally read everything, my comments are that I think @andruud's proposal is really confusing from a user point of view, but @flackr's works for me.

My mental model is that we have the various properties working as they did before we defined -attachment, and we have a new property, timeline-root (or timeline-scope, which would be my suggestion), which changes the scope of a matching descendant timeline, making it visible to the timeline-scope element’s entire subtree.

Nested timelines with the same name would obscure ancestor timelines with that name, which allows for recursion. The one technical downside compared to -attachment model is that you can't jump over e.g. conflicting names in a shadow DOM wrapper because of the implicit matching. But I think the usability is better, so probably worth that cost.

Wrt side comments: whether view-timeline-inset is part of view-timeline is an open issue (we can discuss it), and wrt which type of timeline wins, this is already defined in https://drafts.csswg.org/scroll-animations-1/#timeline-scope

css-meeting-bot commented 1 year ago

The CSS Working Group just discussed [scroll-animations] Broader scope of scroll timelines, and agreed to the following:

The full IRC log of that discussion <TabAtkins> flackr: We previously resolved we wanted the scroll-anim spec to define a timeline that is provided by a scroller that wasn't discovered yet
<TabAtkins> flackr: This issue is about the specific syntax
<TabAtkins> flackr: Had discussions with a bunch of people and we have a proposed consensus
<TabAtkins> flackr: New property, timeline-root
<TabAtkins> flackr: Not directly related to the view-timeline or scroll-timeline property
<TabAtkins> flackr: timeline-root defines a new "deferred timeline" object, which will find a nested timeline to attach to.
<TabAtkins> flackr: Sorry, timeline-scope as the name.
<miriam> ack fantasai
<TabAtkins> fantasai: The way I'd describe is you create a timeline by naming an axis or whatever.
<TabAtkins> fantasai: timeline-scope lets you increase the scope; you put it on an ancestor and it "hoists" the timeline higher to make it visible to more elements
<TabAtkins> flackr: Conceptually this is true, but technically it does create its own timeline, for animation discovery purposes.
<TabAtkins> flackr: But it's fine to think of it that way, and the timeline you observe from JS will be the real timeline generated by the scroller or whatever.
<TabAtkins> miriam: What's the value of the property?
<TabAtkins> flackr: A dashed-ident
<flackr> probably a list of dashed-idents
<fantasai> TabAtkins: It will search for a timeline with the same name (of any kind of timeline) among its descendants
<fantasai> TabAtkins: if it finds one (not zero, not many) it attaches that timeline to the name for its entire scope
<TabAtkins> proposed resolution: remove scroll/view-timeline-attachment, add timeline-scope, which accepts a list of timeline names and raises their scope
<TabAtkins> miriam: Objections?
<bramus> +1
<fantasai> +1
<TabAtkins> RESOLVED: remove scroll/view-timeline-attachment, add timeline-scope, which accepts a list of timeline names and raises their scope
<ydaniv> +1
fantasai commented 1 year ago

Edits have been folded into scroll-animations-1 for timeline-scope for now.

See follow-up issue https://github.com/w3c/csswg-drafts/issues/8915