Skip to content

[scroll-animations] Underserved use cases from Origin Trial #4345

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
stephenmcgruer opened this issue Mar 14, 2019 · 3 comments
Closed

[scroll-animations] Underserved use cases from Origin Trial #4345

stephenmcgruer opened this issue Mar 14, 2019 · 3 comments
Labels

Comments

@stephenmcgruer
Copy link
Contributor

Background

In developing the ScrollTimeline spec over the last few months, and after discussions with developers on their experience trying out ScrollTimeline1, we have found the current spec leaves a number of scroll-related animations either hard or impossible to implement. This issue attempts to list those and suggest a possible alteration to the spec to enable the unsupported use cases.

Some (but not all) of these issues were discovered when exploring AMP's amp-position-observer component. Additionally some of the issues were previously mentioned when scroll-animations was first proposed in WICG.

Examined Usecases

  1. Non-full viewport parallax.
  2. Animate an element as it becomes visible in the viewport, and as it stops being visible again (aka the 'reveal'/'unreveal' case).
    • Example: Fade an element in as it becomes visible in the viewport.
    • This effect could be symmetrical or asymmetrical (e.g. different animations for entering at the bottom and exiting the top).
    • One may only want to animate one side of this (e.g. as it becomes visible but not when it leaves, such as in the above example).
  3. Animate an element as long as it is visible.
    • Example: spin an element for as long as it is visible.
    • Note that this is different than use-case 2, as it implies continuing to animate whilst the element is in the viewport after it has been revealed.
    • Note that here we are assuming that the animation is still scroll-bound. One could consider a time-bound animation, which then implies the need for 'scroll triggers'.

Underlying Problem

The common problem shared between these usecases 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. Most commonly this is 'when the element is visible in the scroller viewport' and the element in question is usually the one which is being animated and the scroller is usually the document viewport.

As an example, see this gif. The animation only starts when the entirety of the clock is inside the viewport, and it finishes the animation as it reaches the point where any part of the clock leaves the viewport.

Why can't the developer calculate startScrollOffset manually?

It may seem like a developer can just do something like the following:

var startScrollOffset = target.getBoundingClientRect().y - scroller.getBoundingClientRect().y;

However, this has a number of problems:

  • It puts the onus on devs which make it more likely that they won’t do the right thing2. Also requiring getBoundingClientRect usage can cause layout thrashing if not careful.
  • Zooming. This changes layout and might change where the element is, and the web developer cannot detect it.
  • Anything else that affects layout, e.g. new content added to the page. Again, the web developer may not be able to detect it - and even if they can the browser is often better placed to do the right thing.
    • Also, even if the developer can detect it, startScrollOffset is immutable so they need to reconstruct an entire timeline!

1: Via the AnimationWorklet origin trial; however their thoughts on ScrollTimeline were agnostic to using AnimationWorklet.

2: For example, scroll snap points moved away from an "offset model" to a "box alignment model" because of similar issues.

@stephenmcgruer
Copy link
Contributor Author

stephenmcgruer commented Mar 14, 2019

Proposal: Dynamic startScrollOffset/endScrollOffset

One possible solution would be to add some way to reference an element for the startScrollOffset and endScrollOffset, for example:

var animationTarget = document.getElementById("animationTarget");
var timeline = new ScrollTimeline({,
   startScrollOffset: { target: animationTarget },
});

The default behavior would be that the effective startScrollOffset would be the position at which the target element would start becoming visible in the scrollSource, and the effective endScrollOffset would be the position at which the target element would stop being visible in the scrollSource.

In addition, there could be:

  • A 'start/end' marker for the scrollSource to determine which edge you use (enables the reveal/unreveal cases)
    • The names here could definitely be better; it's unclear which side of the scroller the 'start' is (for an orientation: block, default writing-mode ScrollTimeline, I had 'start' as the bottom of the scroller which might not be what the reader imagines!)
  • A percentage 'threshold' for the target to determine how much must be becoming visible/leaving being visible at the calculated point.
    • 0: the first pixel of the target.
    • 0.5: the middle pixel of the target.
    • 1: the last pixel of the target.
  • A non-negative 'margin' around the target element, such that the calculation could be offset by the margin.
    • Note that we chose non-negative here because IntersectionObserver's margins are non-negative, but that may not be necessary.
    • TODO: Use-cases for the margin.

Negative startScrollOffset/endScrollOffset

One interesting aside is that this proposal would require negative startScrollOffsets/endScrollOffsets (at least the resolved versions) to be supported. This is required for the case where you want to (for example) base the startScrollOffset on an element entering the viewport, but the element is already in the viewport at scroll offset 0. In that case, to get the right currentTime you need to scroll backwards into 'negative space' (conceptually, not actually!) to get the right currentTime.

TODO: When is negative endScrollOffset required?

Target Descendancy and Nested Scrollers

One possible complication is where the target element is not a descendant of the scrollingSource or
the target element is inside another scroller inside the scrollingSource.

For the former, it seems reasonable to require that target is a descendant of scrollSource, with some reasonable failure value (0/max-offset?) if it is not.

For the latter, I think it may not be possible for the browser to calculate the scroll offset required to make the target visible, e.g. if the target is out of sight in a nested scroller. We could have a requirement that there aren't any nested scrollers (similar to descendancy) or there may be a better solution.

Examples

// reveal.js (implements https://output.jsbin.com/pucuyic)
let image = document.getElementById("image");
let timeline = new ScrollTimeline({
  scrollSource: null,  // This example uses the document scrollingElement.
  // When the first pixel appears.
  startScrollOffset: { target: image, scrollSourceEdge: 'start', threshold: 0 }, 
  // When the last pixel appears. 
  endScrollOffset: { target: image, scrollSourceEdge: 'start', threshold: 1 }, 
});
let effect = new KeyframeEffect(
  image,
  [ { "opacity": 0 }, { "opacity": 1 } ],
  { duration: 1000 }
);
let anim = new Animation(effect, timeline);
anim.play();
// animate-while-visible.js (implements https://output.jsbin.com/qirafih)
let clock = document.getElementById("clock-scene");
let timeline = new ScrollTimeline({
  scrollSource: null,  // This example uses the document scrollingElement.
  // When the last pixel appears.
  startScrollOffset: { target: clock, scrollSourceEdge: 'start', threshold: 1},
  // When the first pixel disappears.
  endScrollOffset: { target: clock, scrollSourceEdge: 'end', threshold: 0 },
});
let effect = new KeyframeEffect(
    clock.querySelector(".clock-hand"),
    [ { "transform": "rotate(-180deg)" }, { "transform": "rotate(180deg)" } ],
    { duration: 1000 }
);
let anim = new Animation(effect, timeline);
anim.play();

@stephenmcgruer
Copy link
Contributor Author

Alternative Proposal: A new type of Timeline (ViewportTimeline?)

If the above proposal seems too 'weird' for ScrollTimeline, we could also extract it into its own 'ViewportTimeline'. I don't personally think this is the correct way forward, but am open to feedback.

A ViewportTimeline would capture much of the previous post, but would have an API something like the following (credit to Ali Ghassemi for the API here, as well as many of the ideas that ended up in the proposal above):

// Note: maybe these don't all need to be readonly.
interface ViewportTimeline : AnimationTimeline {
  readonly attribute Element target;
  readonly attribute Element root;
  readonly attribute DOMString rootMargin;
  readonly attribute double startThreshold;
  readonly attribute double endThreshold;
  // Plus fill, timeRange, and orientation, same as ScrollTimeline.
};

It is important to note that this API as written here doesn't actually allow the non-symmetrical reveal/unreveal cases, but it's likely it could be adapted to do so.

@stephenmcgruer stephenmcgruer changed the title Underserved Usecases from Origin Trial Underserved use cases from Origin Trial Mar 25, 2019
@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
@stephenmcgruer stephenmcgruer changed the title Underserved use cases from Origin Trial [scroll-animations] Underserved use cases from Origin Trial Oct 10, 2019
@majido
Copy link
Contributor

majido commented Jun 9, 2020

This usecase is now addressed by element-based offsets (#4337).

The cross-origin iframe issue is tracked in #4344.

So closing this issue.

@majido majido closed this as completed Jun 9, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants