w3c / csswg-drafts

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

[scroll-animations-1] Allow `<length-percentage>` in `<keyframe-selector>` when combined with `<timeline-range-name>` #10000

Open bramus opened 8 months ago

bramus commented 8 months ago

The problem

With Scroll-Driven Animations, authors often want to run animations offsetted against one of the segment edges. For example, to run an animation up to "100px from the start of the range". For this, authors can do calculations in keyframe offsets, e.g. animation-range: entry 0% entry calc(0% + 100px);.

While this syntax works perfectly fine, it is not allowed everywhere. Depending on which keyframes format authors use, they can or can’t do calculations.

This leads to frustration, as recently expressed by Matthew Perry on Twitter:

It's stuff like this that leads people back to using animation libraries. And these will only be more bloated if they have to jump through hoops like converting one animation into multiple animations to support multistop

I think we should close this gap, and allow calculated keyframes across the board.

Performing calculations in the offsets

Web Animations API

If they want to – and they often do – authors can also do calculations in the offset value. When passing in keyframes as an object, they can adjust the values for rangeStart and/or rangeEnd:

document.querySelectorAll('#list-view li').forEach($li => {
    const timeline = new ViewTimeline({
        subject: $li,
        axis: 'block',
    });

    $li.animate({
        opacity: [ 0, 1 ],
        transform: [ 'translateY(100%)', 'translateY(0)'],
    }, {
        fill: 'forwards',
        timeline,
        rangeStart: 'entry 0%',
        rangeEnd: 'entry calc(0% + 100px)', // 👈 This line
    });

    $li.animate({
        opacity: [ 1, 0 ],
        transform: [ 'translateY(0)', 'translateY(-100%)'],
    }, {
        fill: 'forwards',
        timeline,
        rangeStart: 'exit calc(100% - 100px)', // 👈 This line
        rangeEnd: 'exit 100%',
    });
});

However, when passing in an array of objects as the keyframes, it is not accepted.

document.querySelectorAll('#list-view li').forEach($li => {
    const timeline = new ViewTimeline({
        subject: $li,
        axis: 'block',
    });

    $li.animate([
        { opacity: 0, offset: 'entry 0%' },
        { opacity: 1, offset: 'entry calc(0% + 100px)' }, // ❌ Does not work
        { opacity: 1, offset: 'exit calc(100% - 100px)' }, // ❌ Does not work
        { opacity: 0, offset: 'exit 100%' },
    ], {
        fill: 'both',
        timeline,
    });
});

In both snippets above, authors get back this error:

Uncaught TypeError: Failed to execute 'animate' on 'Element': timeline offset must be of the form [timeline-range-name]

CSS Animations

Looking at the CSS variant of SDA, it’s similar there: when using animation-range an author can do calculations, but when creating a set of keyframes with @keyframes they can not.

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

el {
  animation-name: f;
  animation-timeline: view();
  animation-range: entry 0% entry calc(0% + 100px); /* ✅ Allowed */
}
@keyframes f {
  entry 0% { opacity: 0; } /* ✅ Allowed */
  entry calc(0% + 100px) { opacity: 1; } /* ❌ Not allowed */
}

el {
  animation-name: f;
  animation-timeline: view();
}

The Cause

In both cases (CSS or WAAPI), this is because of the syntax of keyframe selectors. As per spec, the offsets are limited to <percentage>s when combined with a <timeline-range-name>:

<keyframe-selector> = from | to | <percentage [0,100]> | <timeline-range-name> <percentage>

This in contrast to animation-range-start and animation-range-end which do accept lengths (spec):

animation-range-start = [ normal | <length-percentage> | <timeline-range-name> <length-percentage>? ]#

Proposed Solution

Allow <length-percentage> – instead of only <percentage> – in <keyframe-selector> when combined with <timeline-range-name>.

By allowing <length-percentage>, authors can perform these calculations across the board, independent of which format they use to create their animation. The proposed new syntax is this:

<keyframe-selector> = from | to | <percentage [0,100]> | <timeline-range-name> <length-percentage>

As a result, the failing code snippets listed above will start to work fine.

nathanbabcock commented 6 months ago

Really sensible idea. Here's a supporting example that led me here:

image

https://codepen.io/nathanbabcock/pen/zYXmNwX

The goal is to add a gradient fade effect to the top/bottom of scrolling content as a subtle indication that there's more content in that direction. It should smoothly fade out when reaching the top or bottom of the scroll container (at an absolute pixel offset proportional to the length of the fade effect). Even with animation-range, there's no way to accomplish this effect without double-nesting the content in two separate divs: one to track offset from the top, and one to track offset from the bottom.

Bramus' proposal above would make it possible to express all the logic underneath a single @keyframes block.

bramus commented 1 month ago

After reading #10879, maybe we should allow <length-percentage> inside <keyframe-selector> in general, to open up the scroll-timeline case as well?

@keyframes f {
  0% { translate: 0% 0%; } /* ✅ Allowed */
  150px { translate: 0% -100%; } /* ❌ Not allowed */
}

el {
  animation-name: f;
  animation-timeline: scroll();
}

(These are the keyframes for something like this shrinking header that animates using the animation-range 0px 150px)

If we don’t want this extra addition, the current proposal can already work if we enable scroll to target the full range of the ScrollTimeline (see request here):

@keyframes f {
  scroll 0% { translate: 0% 0%; } /* ✅ Allowed */
  scroll 150px { translate: 0% -100%; } /* ❌ Not allowed */
}

el {
  animation-name: f;
  animation-timeline: scroll();
}
bramus commented 1 month ago

Feedback from @flackr, @fantasai, @tabatkins, @ydaniv, @birtles, etc would be appreciated here :)

ydaniv commented 1 month ago

Since we already allowed range names in there, might as well allow length-percentage too. The whole point of that was to allow specifying points on the timeline directly as selectors, so makes sense to allow anything that can select a point on the timeline.