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

Mixing scroll & time-based delay & durations (scroll-triggered animations) #48

Open
AmeliaBR opened this issue Apr 19, 2019 · 5 comments

Comments

Projects
None yet
5 participants
@AmeliaBR
Copy link

commented Apr 19, 2019

Regarding the section on scroll-triggered animations, which says:

The design space for triggering animations is still open. We welcome input on this subject.

I think this is a very important use case & it shouldn't be left behind, as the overall design of the spec will be different if you're considering scroll-triggered and scroll-driven animations together.

In #39, @stephenmcgruer reports feedback complaining about the missing use case of "scroll into view" or "scroll out of view" animations, which are one type of scroll-triggered animations. From Stephen's perspective:

The common problem shared between these use cases is that they don't want to animate based on the absolute scroll position, but instead based on the location of an element within the scroller.

Stephen goes on to propose modifications to the scroll timeline that would make it possible to define a target scroll position or duration relative to the position of an element within the scroller. I think those are important suggestions, and discussion of them should continue in #39.

But, there is another aspect of these use cases: the designer often wants to combine a scroll-based start time with a fixed-time animation or transition, and maybe also a scroll-based end to a repeating animation. That's what I'm discussing here.

For a scripted animation, the use case is somewhat addressed by using IntersectionObserver to define the trigger, and then initiating (or cancelling) a regular time-based web animation call. But it would be preferable to be able to define the trigger & animation in the same API, without callbacks. And a solution is still needed for declarative animations.

The concern raised in the current spec draft is that:

it’s not possible to trigger CSS transitions from the compositor thread (because triggering a transition requires style resolution…)

I assume this perspective is based on how scroll-triggered are currently implementing, by adding a class to the element that triggers a style recalc which triggers a transition.

But another way of thinking about a scroll-trigger is to look at it as a delay. The transition or animation has already been calculated and queued up, but it is waiting for the correct time to start running.

CSS animations and web animations already have built-in separation of delay times versus duration times. Why not define the scroll timelines in a way that you can make one of these times scroll-based but not the other?

Requirements

(Using the CSS/declarative API for discussion, because that's what I'm most familiar with and it's the more difficult use case currently. And anything that can be defined in CSS can be represented in JS APIs, while the reverse isn't always true.)

Using a single animation-timeline property doesn't make sense if different aspects of the animation can be tied to different timelines.

Instead, we need values that can be used directly in the animation and transition -delay and -duration properties. These values would represent times that cannot be resolved until runtime.

However, we don't want to have to repeat all the scroll timeline parameters in each property; you should be able to reuse the same timeline for multiple animation effects.

For delays, you should be able to combine these unresolved time functions with literal times in a calc() expression. There's precedent for this in the all-encompasing web animations conceptual model: event-based timings in SMIL, which allow the start and end times of an animation to be defined as a certain time after (or before) an event. The ideal model should be extensible to support these types of unresolved triggers, too. (E.g., to delay a transition on an image until its load event fires).

For animation/transition durations, combining times and scroll positions is more complicated: scroll position is a representation of progress, not just a trigger event that occurs at a single point in time. If you wanted to combine the scroll progress with a time duration, you would need a conversion factor like the timeRange that's in the current spec. But I think it might turn out to be unneccessary: durations should logically be either scroll based or time based, but not some combination of the two. Position within duration can be defined as a percentage, rather than by mapping to a time value.

This I think would also cover some of the usability suggestions @bradkemper makes in #44, although in a different way.

Proposal

A new @timeline rule allows scroll timelines to be defined independent of any element. The rule would take an identifier and then a list of descriptors that replace the parameters to the scroll() function in the current spec. One of the descriptors would be a timeline-type: scroll, to allow other types of timelines to be added later.

Define a new timepoint() function (name to be debated) for use in the -delay properties, which takes a timeline identifier and an offset position, and a keyword that indicates if the timepoint should be triggered when tranversing the timeline forward, backward, or both (default to be debated). A timepoint function is treated as an unresolved time value and can be combined with literal times in calc() expressions.

Define a timeline() function for use in the -duration properties which takes a timeline identifier and optionally a percentage (default 100%) or some keyword that indicates "however much is left of the timeline after the delay".

Extend the animation-iteration-count property to include a "repeat until <timepoint>" syntax.

Example:

@timeline root-scroller {
  timeline-type: scroll;
  timeline-node: selector(:root) ; /* the element that controls this timeline;
         the selector would match a single element using document.querySelector() rules */
  scroll-direction: block; /* descriptors that are specific to scroll timelines */
  scroll-offset-start: 5rem;
  scroll-offset-end: calc(100% - 5rem); /* scroll-offset-end instead of end-scroll-offset
          so that we could have a scroll-offset shorthand */ 
}
.progress-bar {
  animation-name: progress;
  animation-duration: timeline(root-scroller);
}
.footer-effect {
  animation-name: almost-done, finished;
  animation-delay: timepoint(root-scroller 95% forwards), calc(timepoint(root-scroller 100%) + 0.5s);
  animation-duration: 0.5s, 1s, 
}

Possible extensions to other types of timelines:

  • something like the "viewport" timeline proposed in #39 (comment) for defining the position of an element within the viewport or its scrolling container, if it turns out that it's easier to treat that as a separate timeline type instead of an extra descriptor/option on a scroll timeline.

  • a media playback timeline, where the timeline-node is an audio or video element that you want to sync the animations to.

  • an event-based timeline, like the SMIL model, where time runs as normal but the timeline start (and maybe end) time is sync'd to a specific event. timeline-node would need a keyword to represent "the same element as the animation" instead of a selected node. It would also need a keyword for events on the window — which would finally allow us to create CSS animations where all animations are sync'd to a master timeline (e.g., based on the window's DOMContentLoaded event), instead of being staggered by when each element gets added to the DOM.

Remaining issue:

To use this for transition delays, we still need to have a separate style calc for representing the "pre-transition" value. Which currently means that you need to have the element in the DOM & style calculated, and then trigger a state change that changes the styles, like adding a class to the root element after the first user interaction event. Which cancels out the benefit of doing this all declaratively.

This issue isn't specific to scroll effects: it's a general limitation for any transition that you want to trigger after loading a page or adding an element to it. Either you need to force a style calc in your script, or you need to use a keyframes animation instead of a transition. I wonder if we could define a "before first render" pseudoclass that allows the author to declare a set of styles that are used solely for calculating initial transitions?

@birtles

This comment has been minimized.

Copy link
Collaborator

commented Apr 26, 2019

But another way of thinking about a scroll-trigger is to look at it as a delay. The transition or animation has already been calculated and queued up, but it is waiting for the correct time to start running.

That's an interesting idea. I think we were really attached to the idea of transitions because they give you the auto-reversing behavior that you so often want for scroll-triggered time-based animations.

e.g.

div.menu {
  transition: transform 0.5s;
}

@trigger scroll(element(#body), vertical, 1px) {
  div.menu {
    transform: scale(0.5);
  }
}

When you scroll back over the threshold, you want the transition to run backwards, and furthermore, you want the interrupted transition behavior where the duration is shortened.

We looked at ways to make that happen on the compositor but couldn't find a good way (short of trying to pre-generate every possible style change produced by a @trigger rule--something which quickly becomes unwieldy).

If we're prepared to give up on transitions, or prepared to give up on them being triggered on the compositor (and hence possibly being a little laggy), then there's a lot we can do in this space and I too would prefer we thought about it up front.

(.... And anything that can be defined in CSS can be represented in JS APIs, while the reverse isn't always true.)

I'm keen to work out the declarative syntax up front, but we shouldn't dismiss the JS representation--for this feature we've found the JS side harder since it permits less magic and is sometimes constrained by the different types used to represent the values.

Using a single animation-timeline property doesn't make sense if different aspects of the animation can be tied to different timelines.

I'm not sure I follow this part. Do you have an example?

A new @timeline rule allows scroll timelines to be defined independent of any element.

I like this, but at the same time I'm concerned that @keyframes is already quite problematic when it comes to shadow DOM etc. due to the requirement to have a globally unique name. It also makes generating @keyframes from script a pain.

I wonder if we could find an alternative mechanism that allows names to be re-used somehow? Perhaps something along the lines of how counters work?

If we can solve that, then we might be able to use @timeline as the basis for a declarative version of GroupEffects which is a problem we've never quite managed to solve.

Define a timeline() function for use in the -duration properties which takes a timeline identifier and optionally a percentage (default 100%) or some keyword that indicates "however much is left of the timeline after the delay".

I like this. I would like to consider how it applies to the JS API too, however. It could potentially be useful for GroupEffects too as a means of saying, "This group effect takes 30% of its parent group's duration".

* a media playback timeline, where the `timeline-node` is an audio or video element that you want to sync the animations to.

I know a number of people are interested in this one.

@majido

This comment has been minimized.

Copy link
Member

commented Jun 5, 2019

I think this can be a good discussion topic for a breakout brainstorming in Toronto F2F.
Both @flackr and I will be there on Thursday and I think @birtles and @graouts are able to call in.

Also I filed #49 more specifically geared toward brainstorming the css syntax for the current scroll timeline model (focused on scroll-linked usecases) copying over the @Timeline rule idea but only as it related to timeline (i.e., no timepoint etc.)

@majido

This comment has been minimized.

Copy link
Member

commented Jun 6, 2019

I also like the @timeline rule idea as the syntax for the timeline. I have captured the idea in #49 to be evaluated.

Using a single animation-timeline property doesn't make sense if different aspects of the animation can be tied to different timelines.

But about the idea of using multiple timelines for animations, If I understand this correctly the suggestion is t be able to set delay and duration based on different timelines (e.g., delay is a scroll position while duration is time based which would give you a scroll-triggered but time-drive animations).

We have also previously discussed the idea of having two separate timelines (e.g., one that controls that animation (triggers start, finish events) , and one the drives progress of the effect (i.e., provide progress value).

Having thought about this some more, I feel the complexity of two timeline is not worth it. Here are a few points:

  • The fact that web-animation is time-based cannot be changed but generally using "time" for non-time concepts (scroll, visibility, etc.) is not ideal. For driving the animation we are forced to map values to time but I don't think we need to continue limiting ourselves to time when it comes to "triggers".
  • Unlike progress which is continuous, triggers are discrete which suggests to me that mapping them to time is not ideal.
  • Web Animation already has semantic for play/pause/cancel operations, I think a simpler approach will be to allow tying triggers to these.

We can debate the syntax but something like what @birtles suggests above can works. Imagine these triggers:

@trigger scroll(selector(#body), vertical, 1px) {
  div.menu {
    transform: scale(0.5);
  }
}

/* I think most people want visibility as opposed to having to manually compute 
scroll offsets. Another way to do this is to have intersection ration ala intersection observer. */
@trigger visible(selector(#target)) {
   #target {
    opacity: 1;
  }
}

/* For fun, imagine :hover and :checked as triggers  */
@trigger checked(selector(.hamburger)) {
  div.menu {
    transform: translateX(200px);
  }
}

If we're prepared to give up on transitions, or prepared to give up on them being triggered on the compositor (and hence possibly being a little laggy), then there's a lot we can do in this space and I too would prefer we thought about it up front.

I agree that will open up the possibilities. Most of the useful triggers are not very easily mapped to compositor thread and similarly as you pointed our transitions which are useful with triggers are not well mapped to compositor.

I personally don't think scroll-triggered effects need the same level of synchronicity with scroll offset as scroll-linked animations require. So having them lag behind (i.e., not run on compositor) is fine specially if it means we get a more ergonomic API.

@tabatkins

This comment has been minimized.

Copy link
Collaborator

commented Jun 6, 2019

What happens if you trigger the obvious cyclic behavior? That is, in a @trigger visible(...) {...} rule, you transform the element to be off-screen? Does it just start reversing at that point?

@css-meeting-bot

This comment has been minimized.

Copy link

commented Jun 6, 2019

The CSS Working Group just discussed Mixing scroll & time-based delay & durations (scroll-triggered animations).

The full IRC log of that discussion <emilio> topic: Mixing scroll & time-based delay & durations (scroll-triggered animations)
<astearns> github: https://github.com//issues/48
<emilio> majidvp: currently the scroll-timeline idea is solidly focus on scroll-triggered animations
<emilio> ... there's another kind of scroll-triggered animation
<emilio> ... so stuff starts animating when you hit a given scroll offset
<emilio> ... the original spec dropped those because they considered that it could be done with transitions and that it couldn't be done off-main-thread
<emilio> ... there's now a proposal from AmeliaBR where you'd have two animation timelines
<emilio> ... so you have a timeline with time, and the other one you can use it to move the animation forward
<emilio> ... so you could have two timelines, a scroll timeline and an animation timelin
<emilio> *timeline
<emilio> ... to me it feels that we're complicating the animation model
<emilio> ... so we probably want to mix these two features by complicating the scroll timeline
<emilio> ... triggers are mostly script-based
<emilio> ... and most use-cases care about visibility
<emilio> ... not scroll-position
<emilio> ... animation has the notion of start / stop / cancel / ...
<emilio> ... So I think triggers are not a good fit for timelines
<emilio> ... but I wanted to get a sense of the feeling for trying to solve both together or not
<heycam> q+
<emilio> AmeliaBR: so the reason I wrote this is that I was concerned that the current draft pushed away this idea
<emilio> ... because of the way they were using time values in properties as a proxy for scroll positions
<emilio> ... you say that an element is assoc. with a timeline, and any times are treated as scroll position changes
<emilio> ... that seemed to prevent the idea of mixing the two
<emilio> ... if we go with `@timeline` I think we can step back and talk about scroll / in-view triggered animations and still have them be possible
<emilio> ... for the specifics of the syntax there are some good criticisms of my initial proposal
<emilio> ... but the idea is to focus to make sure that as this spec gets adopted it's extensible to that idea that some parts of the animation can be fixed-time and some can be event or movement-based
<emilio> ... as how do we map that to web animations it may be better mapped for triggered events
<emilio> ... so the animation-delay is more of an event-based anyway, so it interacts in a different way
<emilio> ... they're not necessarily additive
<astearns> ack heycam
<emilio> heycam: I like the idea of having something declarative of having a way to trigger animations when something becomes visible
<emilio> ... since it's a common pattern
<emilio> ... but the proposal seems pretty complicated
<emilio> ... I think I'd try to make it work with animations / animation-play-state
<emilio> ... maybe with an `:ever-been-visible` pseudo-classes or such
<emilio> ... I think solving only the simple cases is fine, you can always fall back to script and `IntersectionObserver` or such
<emilio> AmeliaBR: that's a very good point, probably the scroll-into-view use-case is much more common use-case
<emilio> ... I previously had proposals for `:visible` pseudo-class or such
<emilio> ... probably with something with margins like scroll-snap and intersection-observer
<emilio> TabAtkins: Triggering off scroll-position is nice because it's not cyclic, but triggering off visibility is cyclic
<emilio> AmeliaBR: `:visible` should work with static pos
<emilio> ... that way it'd work with `:static`
<emilio> TabAtkins: but you can still animate top/left and the the static position changes
<emilio> florian: any css prop that depends on layout is ~impossible
<emilio> TabAtkins: very complicated at least
<TabAtkins> s/:static/:stuck/
<emilio> astearns: so close and rely on other means?
<florian> s/any css prop/any css pseudo-class/
<emilio> heycam: maybe `:ever-been-visible` wouldn't be cyclic
<emilio> TabAtkins: that's fine from a theoretical standpoint
<emilio> AmeliaBR: heycam, maybe wanna file an issue `:ever-been-visible`?
<emilio> fantasai: that's statefule
<emilio> florian: how does thiat interact with reloading in such
<emilio> *And such
<emilio> florian: we don't have any other stateful thing in selectors
<emilio> TabAtkins: yeah, animations are stateful, if you have an infinite animation
<emilio> ... if you remove it it resets
<emilio> astearns: so if we had `:ever-been-visible`, do we still want to follow-up on the same issue?
<emilio> astearns: I suggest we close the current issue, look at use-cases for `:ever-been-visible` and discuss if it's worth it
<emilio> (no objections)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.