Closed fantasai closed 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.)
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 ...
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?
Yes, correct. Too reliant on the shorthand?
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?;
ancestor
: declare+define localancestor
: attach to -root with same ident declared somewhere up the tree.Or maybe you just made the case for the longhands being impractical, and we should just have scroll-timeline
and scroll-timeline-root
properties?
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.
@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
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?
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?
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
:
scroll-timeline: foo block;
=> scroll-timeline-root: foo; scroll-timeline-name: foo; scroll-timeline-axis: block;
scroll-timeline-name: foo; scroll-timeline-axis: block;
=> scroll-timeline-root: none; scroll-timeline-name: foo; scroll-timeline-axis: block;
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> ]#
:
auto
: Declare a STL with same ident as the -name
property has set. The resulting STL will be local + subtree only. In case no -name
is set on the element, this declaration has no effect.none
: Don’t declare. A STL defined with -name
will try and attach to a parent that has -root
set to the same ident.<custom-ident>
: Declare STL with given ident. Children with the same ident set as their -name
and -root
set to auto
will attach to this.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:
scroll-timeline: foo block;
=> scroll-timeline-root: auto; scroll-timeline-name: foo; scroll-timeline-axis: block;
~> scroll-timeline-root: foo; scroll-timeline-name: foo; scroll-timeline-axis: block;
scroll-timeline-name: foo; scroll-timeline-axis: block;
=> scroll-timeline-root: auto; scroll-timeline-name: foo; scroll-timeline-axis: block;
~> scroll-timeline-root: foo; scroll-timeline-name: foo; scroll-timeline-axis: block;
scroll-timeline: foo block ancestor;
(*) => scroll-timeline-root: none; scroll-timeline-name: foo; scroll-timeline-axis: block;
(*) 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 😅)
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
.
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.
@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.
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
.
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:
-name
and -axis
work together to create and attach a local timeline, always. No exceptions, no behavior changes, you just set them both (or rely on defaults, whatever) and you'll get a local timeline drawing from your scroller, which is visible to yourself and your descendants. (If you're not scrollable, this is just an inactive timeline.)
-root
creates a different kind of object, a deferred timeline, which by definition doesn't have an axis attached directly. Instead, it tries to match up with a single descendant local timeline with the same name. If it finds one (not 0 or >1), then it uses that local timeline's scroller. (If it can't find exactly one match, it's an inactive timeline, too.)
whenever something is trying to find a timeline of a given name, it just looks at itself and its ancestors and uses the first timeline it finds with that name. This might be a local timeline, or a deferred timeline; the difference is meaningless.
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.
I'm also in favor of the -root
proposal, with a few notes:
defer
value isn't required, and I think the single handshake should be fine.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. (:
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?
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.
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
.
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
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?
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.
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.
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.
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.
+1 for using the attached timeline and not the deferred, much more intuitive
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.
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.
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.
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
The CSS Working Group just discussed [scroll-animations] Broader scope of scroll timelines
, and agreed to the following:
RESOLVED: remove scroll/view-timeline-attachment, add timeline-scope, which accepts a list of timeline names and raises their scope
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
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:local
has the current behavior of binding the name to this element’s scroll containerdefer
declares and scopes the name, but does not bind it to a scroll containerancestor
looks up the ancestor chain for a matching timeline name and attaches to that instance; failing a match, declares it locallyclosest
looks back up the tree including previous siblings for a matching timeline name (same lookup as animation-timeline), and attaches to the first matching instance; failing a match, declares it locally