Skip to content

Element-based start and end offsets #4337

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

Closed
majido opened this issue Sep 12, 2019 · 16 comments · Fixed by #5264
Closed

Element-based start and end offsets #4337

majido opened this issue Sep 12, 2019 · 16 comments · Fixed by #5264

Comments

@majido
Copy link
Contributor

majido commented Sep 12, 2019

A very common usage pattern for scroll timeline is animating items as they enter or exit the scrollport (or viewport). This was identified as a shortcoming of the current API and it is explained in details here

This issue tries to propose an extension in the ScrollTimeline API to help address this shortcoming. This builds on top of our earlier proposal here.

Proposed Design

Allow scroll timeline's start and end offsets to be declared in terms of elements on the page. More accurately as a single intersection threshold (with a similar semantic as IntersectionObserver) between scroll timeline's scroll source and another element. Note that while the target element is often the animation target itself but this is not necessary.

Why Intersection Semantics and IntersectionObserver

We believe most common use cases can map easily to an intersection point which is simple to express and understand. There are several examples below that demonstrate this more concretely. Assuming intersection semantic is the right one then it is natural to use Intersection Observer which is a primitive that was designed for this very exact use case. By using Intersection Observer semantics which are well understood, specified and documented, we keep the platform consistent, make the feature potentially easier to implement, and also make it easier to polyfilling as well.

Additions to the IntersectionObserver model

To make this work for ScrollTimeline we need a few additions to the Intersection Observer model but they seem to be reasonable and small.

One Dimensional Intersection: Intersection observer calculates intersection (and thus thresholds) on 2d plane but for scroll we want one dimensional intersection. I believe this is easy to define and introduce to intersection observer model.

Edge Dependency: Intersection observer does not differentiate between start and

end edges and produces intersection entries regardless of where the intersection occurs. The actual observer callback can then detect this based on the info that is available in the entry. For scroll timeline use cases we want to differentiate when something intersect at the start or end edge. Again this seems likely to be a simple addition to Intersection Observer model.

Proposed API

interface ScrollTimeline : AnimationTimeline {
  readonly attribute (DOMString or IntersectionBasedOffset) startScrollOffset;
  readonly attribute (DOMString or IntersectionBasedOffset) endScrollOffset;

  // No change here
  readonly attribute Element scrollSource;
  readonly attribute ScrollDirection orientation;
  readonly attribute (double or ScrollTimelineAutoKeyword) timeRange;
  readonly attribute FillMode fill;
};

dictionary IntersectionBasedOffset {
  Element target;
  Edge edge = "start";
  double threshold = 0.0;
  DOMString rootMargin;
}

enum Edge {"start", "end"}

Semantic for intersection based offsets:

  • When an intersection based offset is used then the implementation must behave as if there exists this corresponding underlying intersection observer
     const startObserver = new IntersectionObserver({
        root: timeline.scrollSource,
        rootMargin: timeline.startScrollOffset.rootMargin,
        thresholds: [timeline.startScrollOffset.threshold],
        edge: timeline.startScrollOffset.edge,
        mode: '1d'
      }).observe(timeline.startScrollOffset.target);
    
     // Similarly one exists for end offset.
    Note: edge and mode here represent the new addition to the Intersection Observer model.
  • When the corresponding start intersection observer would have notified its clients that an intersection at the threshold is reached then the scroll timeline should start ticking and thus its associated animations become active. The scroll offset at that moment is considered the concrete start offset.
  • Similarly when the end intersection observer would have notified its clients that an intersection at the threshold is reached then the scroll timeline should stops ticking and thus its associated animations become inactive. The scroll offset at that moment is considered the concrete end offset.
  • The concrete start and end offsets will be used when calculating scroll timeline's current time.

IMPORTANT: It is actually not required for implementations to create an instance of intersection observer. In fact they can and should precompute the exact scroll offsets that would result in the given thresholds and use those as concrete offsets. Though such pre-computed offsets gets invalidated and need to be recomputed whenever intersection observer would have been invalidated.

Note that “0” threshold in IntersectionObserver signals signal transition from not-intersecting to intersecting if the target and root become edge-adjacent, even if the actual overlap area is zero pixels. This matches what we want as well. It may however be necessary to differentiate between the case when zero is “intersecting” or “non-intersecting” which is exposed by Intersection observer as isIntersecting attribute.

Optional scrollRange

We can also introduce scrollRange that could be used to declare one offset in terms of the other using, start + range = end. This can be handy in some cases.

CSS Syntax

We are assuming that we use and @rule based css syntax (See this issue for further details). In that case the intersection based offset can be implemented in the form of a css function:

<intersection()> = intersection(<target-selector>, <edge>, <threshold>, <root-margin>)
<target-selector> = <selector()>
<edge> = 'start' | 'end'
<threshold> = <number>
<root-margin> = [ <length> | <percentage> | auto ]{1,4}


<selector()> = element(<target-id>)
<target-id> = ':animation-target' | ':root' | '#'  <custom-ident>

Initially, it may be enough for <element()> to only support #ID selector and also a special syntax element(:animaton-target) that selects the animation target itself. Later this can be expanded to support more complex selectors.

Here is an example of how it can be used:

@timeline first-scroll-timeline {
  timeline-type: scroll; 
  timeline-source: element(:root) ;
  scroll-direction: block;
  /* start when target has entered scrollport */
  scroll-offset-start: intersection(element(#myid), start);
  /* end when target has left scrollport */
  scroll-offset-end: intersection(element(#myid), end); 
}

@timeline second-scroll-timeline {
  timeline-type: scroll; 
  timeline-source: element(:root) ;
  scroll-direction: block;
  /* start when half the target is within the scrollport */
  scroll-offset-start: intersection(element(:animation-target), start, 50);
  /* end after 10rem of scrolling */
  scroll-range: 10rem; 
}

@keyframes reveal {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

#target {
  animation-name: reveal;
  animation-duration: 1s;
  animation-timeline: first-scroll-timeline;
}

Examples

The following examples are meant to help demonstrate the ergonomics of the API is some common scenarios.

Example 1. Reveal / Unreveal

An image that goes from transparent to opaque when it enters scrollport and in reverse when it leaves scrollport.

html structure:

<scroller>
  <image>

reveal animation:

  • animation target: image
  • animation effect: opacity 0 -> 1
  • start offset: { target: $image, edge: 'start', threshold: 0} // when element starts to enter scrollport
  • end offset: { target: $image, edge: 'start', threshold: 100} // when element is fully within scrollport

Note: If we had scrollRanger we could specify scrollRange: 100% of image height instead.

unreveal animation:

  • animation target: image
  • animation effect: opacity 1 -> 0
  • start offset: { target: $image, edge: 'end', threshold: 100} // when element starts to leave scrollport
  • end offset: { target: $image, edge: 'end', threshold: 0 }

Note that if element is larger than scrollport the two animations may overlap. Animation composite can be used to determine how this works.

Here is these effect expressed using web animation API in Javascript:

const scroller = document.getElementById("scroller");
const image = document.getElementById("image");

const revealTimeline = new ScrollTimeline({
  startScrollOffset: { target: image, edge: 'start', threshold: 0 },
  endScrollOffset: { target: image, edge: 'start', threshold: 100 },
});

const revealEffect = new KeyframeEffect(
  image,
  { opacity: [0, 1]},
  { duration: 1000, fill: forwards }
);


const unrevealTimeline = new ScrollTimeline({
  // Finish the moment we completely leave the scrollport
  // end offset assumes intersections that involve the scroller "end" edge
  startScrollOffset:{ target: image, edge: 'end', threshold: 100}
  endScrollOffset:{ target: image, edge: 'end', threshold:0}
});


const unrevealEffect = new KeyframeEffect(
  image,
  { opacity: [1, 0]},
  { duration: 1000, fill: backwards }
);

let reveal = new Animation(revealEffect, revealTimeline);
let unreveal = new Animation(unrevealEffect, unrevealTimeline);

reveal.play();
unreveal.play();

Here is how the same thing expressed in CSS:

@timeline reveal-scroll-timeline {
  timeline-type: scroll; 
  timeline-source: element(scroller) ;
  scroll-direction: block;
  /* start when target has entered scrollport */
  scroll-offset-start: intersection(element(:animation-target), start, 0);
  /* end when target is fully within scrollport */
  scroll-offset-end: intersection(element(:animation-target), start, 100); 
}

@timeline unreveal-scroll-timeline {
  timeline-type: scroll; 
  timeline-source: element(scroller) ;
  scroll-direction: block;
  scroll-offset-start: intersection(element(:animation-target), end, 100);
  scroll-offset-end: intersection(element(:animation-target), end, 0); 
}

@keyframes reveal {
 from { opacity: 0;}
 to { opacity: 1;}
}

@keyframes unreveal {
 from { opacity: 1;}
 to { opacity: 0;}
}


.image {
  animation-name: reveal, unreveal;
  animation-duration: 1000;
  animation-timeline: reveal-scroll-timeline, unreveal-scroll-timeline;
  animation-fill-mode: both;
}

Here is a diagram that demonstrates how the two timelines and their associated animations start and end.

reveal-scroll-example

Example 2. Progress bar left to right as we scroll a single page

Consider a document that consists of several sections and we want to show a simple progress bar that goes from 0 -> 100% when user scrolls through each section. This examples shows how intersection target may be different from animation target.

html structure:

<html>
  <page>
    <progressbar> - positioned sticky
  <page>
    <progressbar>
  <page>
    <progressbar>

Progress animation:

  • animation target: progress bar
  • animation effect: width 0 -> 100vw
  • start offset: when page enters the scrollport
  • end offset: when page leaves the scrollport
const scroller = document.scrollingElement;

for (page of scroller.querySelectorAll('.page')) {
  const progressbar = page.querySelector('.progressbar');

  const progressEffect = new ScrollTimeline({
    // just as we enter
    startScrollOffset: { target: page, edge: 'start', threshold: 0 },
    // just as we leave
    endScrollOffset:   { target: page, edge: 'end',   threshold: 100 },  
  });

  const progressEffect = new KeyframeEffect(
    progressbar,
    { width: ['0vw', '100vw']},
    { duration: 1000 }
  );

  const progressAnimation = new Animation(progressEffect, progressEffect);
  progressAnimation.play();
}

Here is how the same thing expressed in CSS:

@timeline root-scroll-timeline {
  timeline-type: scroll; 
  timeline-source: element(:root) ;
  scroll-direction: block;
  scroll-offset-start: intersection(element(:animation-target), start, 0);
  scroll-offset-end: intersection(element(:animation-target), end, 100); 
}

@keyframes progressbar-effect {
 from { --pb-width: 0vw;} // --pb-width is a custom var with length type
 to { --pb-width: 100vw;}
}

.page {
  animation-name: progressbar-effect;
  animation-duration: 1000;
  animation-timeline: root-scroll-timeline;
}
.page > .progressbar {
  width: var(--pb-width);
  position: sticky;
}

Example 3. Image scales up to be fully centered

An image that starts growing and reaches its maximum size once it is fully centered in the viewport. For example see iphone 11 page
html structure:

<scroller>
  <image>

Scale animation:

  • animation target: image
  • animation effect: transform: scale(0.5) -> transform: scale(1)
  • start offset: when element becomes 50% visible
  • end offset: start offset + 50% of scroller height
const scroller = document.getElementById("scroller");
const image = document.getElementById("image");

const timeline = new ScrollTimeline({
  scrollSource: scroller,
  startScrollOffset: { target: image, edge: 'start', threshold: 50 },
  scrollRange: getBCR(scroller).height / 2
});

const effect = new KeyframeEffect(
  image,
  { transform: ['scale(0.5)', 'scale(1)']},
  { duration: 1000, fill: both }
);

let scaleAnimation = new Animation(effect, timeline);
scaleAnimation.play();

Note: this is a case where scrollRange is useful. Here we want end offset to be relative to start offset but it is not easily specified as an intersection. With a simple addition of scrollRange we can define an intersection based start and then specify the scroll range for which the animation should remain for. It is perhaps possible to do a similar thing with an extra element that is positioned to be in the center of the page but that is not as ergonomic.

Open Questions

Circularity

What happens if the animation moves the elements used in the target that could cause the scroll bounds to change which then alters the animation. This can lead to circular dependency between animation and its triggers.

One way to avoid this circularity is to freeze start/end offset when timeline is active/inactive (and thus animating). Here is the change that can potentially achieve this:

  • When a timeline is active has an active animation it ignores any start observer notifications and uses its existing concrete start offset.

  • When a timeline is inactive it ignores any end observer notifications and uses its existing concrete end offset.

TODO: Do we also need to assume that we calculate intersections based on last frame’s layout? Otherwise in the same frame we may have a situation where: 1) compute intersection -> 2)trigger animation which invalidates layout -> 3) in new layout we no longer intersect.

Intersections are calculated based on previous frame

The operation of IntersectionObserver requires the document lifecycle is complete (clean layout/paint (?)). So its callbacks are invoked in the next frame after the fact. Is this sufficient for start/end offset for scroll-driven effects?

Note that to avoid circularity we already ignore the animation output itself. This puts additional restriction that we ignore anything that has occurred since the last frame (e.g., event handlers dirtying the layout, etc.).

I believe this a common problem for any element-based approach regardless of how the concept of intersection is declared.

Dealing with offsets outside scroll range

Sometimes the start and end offsets are outside the scroll range. For example consider the case when the animation target is already visible when page loads at initial scroll offset (or perhaps its content partially outside the scroller when it is fully scroller).

Here are two ways to handle this situation:

  1. animation starts mid-way. In this case, the concrete intersection offset is computed as "negative" value so zero scroll offset provides a non-zero time value.

  2. adjust duration so animation plays faster. In this case the concrete intersection offset is clamped to (0, scroll max).

Seems like options 1 should be default but in future we can have extensions to Allow the clamp behavior.

Prior art and alternatives considered

Scroll timebase proposal considered an element based trigger points as an important feature.

  NOTE2: there are so many ways to define the syntax here
  that we expect it to change a lot. The important features
  are:

  - starting an animation at a point in the page
  - a point is able to be specified in relation to an element's position
  - it's possible to specify an end point, which would stop the animation
    (although this could be split into two properties, so you
    could have on/off animations as you scroll).
@majido
Copy link
Contributor Author

majido commented Sep 12, 2019

@stephenmcgruer @birtles I will be at TPAC and will be more than happy to discuss this F2F.

flackr referenced this issue in flackr/scroll-timeline Sep 16, 2019
Adds an intersection based offset polyfill based on
https://github.com/WICG/scroll-animations/issues/51
which allows the start and end offsets to be calculated based on the
position of a target element.

Also adds a demo indicating the utility of these target based effects.
@flackr
Copy link
Contributor

flackr commented Sep 16, 2019

I put together a rough polyfill / demo of most of the JS API - notably missing rootMargin:
https://flackr.github.io/scroll-timeline/demo/parallax/

One other deviation in that demo is that it uses clamped start and end offsets. If I follow the proposed unclamped behavior outside the scroll range then many of the effects become harder to calculate how to correctly apply:
https://flackr.github.io/scroll-timeline/demo/parallax/unclamped.html

I initially found the edge parameter confusing but in retrospect the ability to make animations that are based on an element's introduction into one edge of the screen make sense.

@flackr
Copy link
Contributor

flackr commented Sep 16, 2019

Interestingly though, clamping makes the header fly-in animations difficult as the headers which start on screen have a start and end scroll offset of effectively 0, making the current time undefined. The polyfill currently special cases that to time 0 to avoid the division by 0. What should the behavior be when they work out to the same value?

@birtles
Copy link
Contributor

birtles commented Sep 17, 2019

I went through this proposal today. It looks really good. I'm especially glad to see the CSS syntax too. My initial reactions are:

  1. How do we handle scroll-triggered time-based animations?
  2. If these timelines are uni-directional (I might have misunderstood this), then when we load a page with scroll anchoring, which timeline gets triggered?
  3. I'm not sure about how to handle the circularity / layout thrashing case. Maybe the proposal here is fine.

@majido
Copy link
Contributor Author

majido commented Sep 17, 2019

@flackr the polyfill and demos are awesome specially that it is started to identify some good insights for clamped/unclamp behavior. On the edge case where both start == end == current scroll offsets then per current spect we will be in otherwise clause of step 4 here which should return effective time range and not zero (I am papering over the fill-mode issue). This may be more reasonable than zero but this needs more thinking.

@majido
Copy link
Contributor Author

majido commented Sep 17, 2019

I went through this proposal today. It looks really good. I'm especially glad to see the CSS syntax too.

Thanks for the feedback @birtles. Here are some initial thoughts on the issues you mentioned:

How do we handle scroll-triggered time-based animations?

In Lyon face-to-face there was a discussion on this topic. And we resolved to keep ScrollTimeline focused on scroll-linked effect and have a separate/simpler concepts to trigger transitions/time-based animations that are simpler. Notes from that discussion are here

For scroll-triggered time-based animations one proposal was to maybe do an :ever-been-visible pseudo-classes or such and deal with more complex case with IntersectionObserver. I think it we land on a good css syntax for intersection for scroll timeline we may in future be able to use it with a trigger syntax if it was needed. At the moment, this proposal is focused on scroll-linked effect using scroll timeline.

If these timelines are uni-directional (I might have misunderstood this), then when we load a page with scroll anchoring, which timeline gets triggered?

These are not uni-directional. It is the same model as current ScrollTimeline which is bi-directional. Rob's demo page can show this. Just load the page, scroll half way and refresh which should load the page in the previous scroll position. The animation will be at the state that it was before which is what I would have expected 👍.

I'm not sure about how to handle the circularity / layout thrashing case. Maybe the proposal here is fine.

I hope the current proposed solution is enough but this needs more explorations. Ideas welcome! BTW that the current spec has some wording on avoiding layout circularity but it is focused on the "current time". I think we may need a slightly different approach for start and end bounds.

@birtles
Copy link
Contributor

birtles commented Sep 17, 2019

If these timelines are uni-directional (I might have misunderstood this), then when we load a page with scroll anchoring, which timeline gets triggered?

These are not uni-directional. It is the same model as current ScrollTimeline which is bi-directional. Rob's demo page can show this. Just load the page, scroll half way and refresh which should load the page in the previous scroll position. The animation will be at the state that it was before which is what I would have expected 👍.

Ok, I need to re-read it I guess. I don't understand why two timelines are needed for the first example if this is bidirectional. Why is one not enough?

How do we handle scroll-triggered time-based animations?

In Lyon face-to-face there was a discussion on this topic. And we resolved to keep ScrollTimeline focused on scroll-linked effect and have a separate/simpler concepts to trigger transitions/time-based animations that are simpler. Notes from that discussion are here

For scroll-triggered time-based animations one proposal was to maybe do an :ever-been-visible pseudo-classes or such and deal with more complex case with IntersectionObserver. I think it we land on a good css syntax for intersection for scroll timeline we may in future be able to use it with a trigger syntax if it was needed. At the moment, this proposal is focused on scroll-linked effect using scroll timeline.

I think if we're adding this much API surface area, we should be able to handle scroll-triggered time-based animations too. Let's work out how to do that.

@majido
Copy link
Contributor Author

majido commented Sep 17, 2019

Here is a diagram that may help with understanding the first example better. Basically there are two timelines involved because the effects involve two different ranges:

  1. reveal range: starts when element starts coming into viewport, and ends until it is completely visible.
  2. unreveal range: starts when element starts leaving the viewport, and ends until it is completely invisible.

reveal-scroll-example

@birtles
Copy link
Contributor

birtles commented Sep 17, 2019

Thank you, that helps a lot!

@birtles
Copy link
Contributor

birtles commented Sep 17, 2019

I think if we're adding this much API surface area, we should be able to handle scroll-triggered time-based animations too. Let's work out how to do that.

Spoke to @heycam about this and realized we could use vanilla IntersectionObserver to do this, particularly with the 1D mode @majido is proposing to make there. @majido also mentioned adding a CSS syntax for this. It would be good to see JS and CSS examples of this in the spec.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed ScrollTimeline, and agreed to the following:

  • RESOLVED: moved scroll-timeline into csswg-drafts
The full IRC log of that discussion <emilio> Topic: ScrollTimeline
<emilio> majidvp: Last f2f I explained the 2 big issues that remained, the css syntax and the problem that the spec only accepts concrete scroll offsets and such and most use cases rely on viewport offsets and such
<emilio> ... so we got lots of feedback from devs that it's hard to compute the right offsets
<fantasai> basically, same problem as scroll-snap had in the beginning...
<emilio> ... so we want to propose some changes to scroll timeline to make the scroll offsets not specified but match intersection of boxes or such
<emilio> majidvp: flackr has done a polyfill for that and the api
<emilio> ... what we're proposing here is specifying offsets in terms of intersection observer semantics
<heycam> q+
<emilio> ... which is start and end of animation as intersection observer offsets
<emilio> ... just one-dimensional rather than two-dimensional
<emilio> majidvp: [goes through the proposal in https://github.com/WICG/scroll-animations/issues/51]
<emilio> github: https://github.com/WICG/scroll-animations/issues/51
<emilio> majidvp: we're also proposing a function-like syntax
<emilio> ... but let me show demos
<emilio> majidvp: [goes through https://flackr.github.io/scroll-timeline/demo/parallax/]
<emilio> majidvp: [goes back to the proposed css syntax]
<emilio> ... there are some open questions like how to fix the circularity in the case layout moves the element while animation
<emilio> ... proposal is to freeze the offset when the animation starts
<heycam> q-
<emilio> ... also how intersections are computed and such
<emilio> ... these are open questions that we're trying to work through
<emilio> ... not proposing concrete solution
<emilio> ... happy to answer questions / feedback / concerns
<emilio> smfr: I like the way it generally looks, and I like the IntersectionObserver thing
<emilio> ... seems much more natural
<emilio> ... can you do something like a spinner that stops as soon as soon as you scroll away?
<emilio> majidvp: ScrollTimeline should not solve that, you need a trigger for that...
<emilio> ... I don't wanted to fix that use case here, but maybe a `:visible` pseudo-class or a CSS intersection observer like syntax
<emilio> iank_: you can polyfill that already with intersection observer, I think it's nice to keep it focused
<emilio> dino: I think this would be a simple addition now that we have the range to address this use case
<emilio> smfr: there's also the case where you stop the animation but let it run to complete a cycle
<emilio> ... so that it comes back into the viewport in a good position
<emilio> majidvp: may be addressable with the range
<emilio> smfr: another piece of feedback is that it seems that the css api is getting a bit out of control
<emilio> ... I'd be fine with just a JS api
<emilio> majidvp: that's the opposite of the last F2F discussion, but it's fine for me...
<heycam> heycam: the small additions to the Intersection Observer model, they sohuld just be added to Intersection Observer itself
<emilio> majidvp: I think they should be added to the spec even if they're not web-exposed.
<emilio> heycam: it'd be nice specially if you don't solve the time-based viewport-triggered animation
<emilio> majidvp: I _think_ you can compute that with the current IntersectionObserver given it provides the intersection area
<emilio> Rossen_: Looks awesome, what are you asking from us?
<emilio> majidvp: confirmation of general direction would be great
<emilio> ... may be nice to bring into csswg-drafts, though may not be that important
<emilio> Rossen_: I think we could do that
<emilio> smfr: where does web-animations live?
<emilio> birtles: CSS
<emilio> RESOLVED: moved scroll-timeline into csswg-drafts

@flackr
Copy link
Contributor

flackr commented Sep 17, 2019

@flackr the polyfill and demos are awesome specially that it is started to identify some good insights for clamped/unclamp behavior. On the edge case where both start == end == current scroll offsets then per current spect we will be in otherwise clause of step 4 here which should return effective time range and not zero (I am papering over the fill-mode issue). This may be more reasonable than zero but this needs more thinking.

Thanks, I've updated the polyfill to properly implement fill modes, and identified a potential issue from the web animations api #4323. As for fill modes, it almost seems like the timeline should always fill, except that it should fill with a value which doesn't make the animation current so that we use the effect's fill behavior. Unfortunately, such a value doesn't exist for the before state. I filed #4325 to discuss this further. Cheers!

@flackr
Copy link
Contributor

flackr commented Sep 18, 2019

Note that your examples (and indeed my polyfill since it followed the examples) use thresholds in the range [0, 100] but the IntersectionObserver spec says that thresholds should be in the range [0, 1.0]. I've updated the polyfill and demos.

@dontcallmedom dontcallmedom transferred this issue from WICG/scroll-animations Sep 19, 2019
@majido majido added the scroll-animations-1 Current Work label Sep 19, 2019
@majido majido added this to the scroll-animations-1 FPWD milestone Mar 31, 2020
@majido
Copy link
Contributor Author

majido commented Apr 8, 2020

As I was doing a prototype implementation of this idea in Chromium, I realized that I have not made this clear that the target for element-based offset need to be a descendant of the scroll timeline source. Otherwise finding an scroll offset that corresponds to intersection does not make much sense. We just have to make this clear when specifying this proposal.

Note that a similar restriction exists for Intersection Observer as well:

If the intersection root is an Element, and target is not a descendant of the intersection root in the containing block chain, skip further processing for target.

If author provides a non-descendant targets we can throw on construction, or simply never resolve these to a concrete scroll offset. The IntersectionObserver takes that latter approach and simply never delivers an intersection record for such cases.

@majido
Copy link
Contributor Author

majido commented Apr 8, 2020

Another interesting edge case is when target element (or scroll source?) does not have a layout box. We should handle this case gracefully as well.

Looking at InteresectionObserver, it invokes getBoundingClientRect, which invokes getClientRects that returns an empty rect when there is no layout box.

If the element on which it was invoked does not have an associated layout box return an empty DOMRectList object and stop this algorithm.

For IntersectionObserve, this means an empty intersection which probably safe. For ScrollTimeline have to check if a similar solution works or perhaps we should be more explicitly handling this case as opposed to relying on empty rects.

majido added a commit that referenced this issue Apr 24, 2020
Per CSSWG resolution [1], we graduated this specification from WICG to CSSWG
a while back. So this is an official ED spec for CSSWG. The status and level
are updated to reflect this.

[1] #4337 (comment)
majido added a commit that referenced this issue Jun 1, 2020
Add basic definition for Element-based offsets

Major changes:
- Introduce concept of "scroll timeline offset" that can be container-based (existing concept) and element-based (new concept).
- Add IDL for the new offset type and use it.
- Define the process for each offset type to be resolved into an effective scroll offset.
- Update current time calculation to resolve offsets and use the effective values.
- Add basic diagram to show the behavior visually for a simple example

Minor changes:
 - Rewrap lines to fit in 80 chars
 - Trim end-of-line whitespace
 - Clarify some definitions

TODO (as follow ups):
 - Define threshold for element-based offset.
 - Add css syntax for element-based offsets.
 - Add more examples.
@majido majido self-assigned this Jun 1, 2020
majido added a commit that referenced this issue Jun 30, 2020
Add css syntax to for element-based offsets. Fixes #4337.

The element-based syntax is simply applied when the value starts with  `selector(#id)` with the following characteristics: 
 - `selector( <<id-selector>> )` is required and is expected to be the first value.
 - both edge and threshold are optional can can be provided in any order.

I followed some of the ideas mentioned by @tabatkins in #4348 to to get to a more ergonomic  css function syntax.  In particular there is no comma and the optional params can be in any order. Note that unlike  #4348 we are not adding a function syntax.
@bramus
Copy link
Contributor

bramus commented Jan 26, 2021

EDIT: The following below is an implementation bug on Chrome's end, as per this tweet.

In the OP I see an example that uses two @​scroll-timeline blocks.

@timeline reveal-scroll-timeline {
  timeline-type: scroll; 
  timeline-source: element(scroller) ;
  scroll-direction: block;
  /* start when target has entered scrollport */
  scroll-offset-start: intersection(element(:animation-target), start, 0);
  /* end when target is fully within scrollport */
  scroll-offset-end: intersection(element(:animation-target), start, 100); 
}

@timeline unreveal-scroll-timeline {
  timeline-type: scroll; 
  timeline-source: element(scroller) ;
  scroll-direction: block;
  scroll-offset-start: intersection(element(:animation-target), end, 100);
  scroll-offset-end: intersection(element(:animation-target), end, 0); 
}

@keyframes reveal {
 from { opacity: 0;}
 to { opacity: 1;}
}

@keyframes unreveal {
 from { opacity: 1;}
 to { opacity: 0;}
}


.image {
  animation-name: reveal, unreveal;
  animation-duration: 1000;
  animation-timeline: reveal-scroll-timeline, unreveal-scroll-timeline;
  animation-fill-mode: both;
}

Is that still allowed (albeit with an updated syntax)? It's that I've been fiddling with it in this CodePen demo (Chrome Canary with “Experimental Web Platform Features” enabled required), and cannot seem to get that to work.

Don't know if this is because the implementation is still in development, or if the spec disallows it. If it's the latter then a note on this in the spec would be needed (or I overlooked it).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants