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

Replace proxied animations in getAnimations reusing proxied CSS animations #89

Merged
merged 5 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
} from "./scroll-timeline-base";
import {
animate,
elementGetAnimations,
documentGetAnimations,
ProxyAnimation
} from "./proxy-animation.js";

Expand Down Expand Up @@ -61,6 +63,16 @@ function initPolyfill() {
if (!Reflect.defineProperty(window, 'Animation', { value: ProxyAnimation })) {
throw Error('Error installing Animation constructor.');
}
if (!Reflect.defineProperty(Element.prototype, "getAnimations", { value: elementGetAnimations })) {
throw Error(
"Error installing ScrollTimeline polyfill: could not attach WAAPI's getAnimations to DOM Element"
);
}
if (!Reflect.defineProperty(document, "getAnimations", { value: documentGetAnimations })) {
throw Error(
"Error installing ScrollTimeline polyfill: could not attach WAAPI's getAnimations to document"
);
}
}

initPolyfill();
41 changes: 38 additions & 3 deletions src/proxy-animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
relativePosition
} from "./scroll-timeline-base";

const nativeDocumentGetAnimations = document.getAnimations;
const nativeElementGetAnimations = window.Element.prototype.getAnimations;
const nativeElementAnimate = window.Element.prototype.animate;
const nativeAnimation = window.Animation;

Expand Down Expand Up @@ -829,6 +831,20 @@ function fractionalEndDelay(details) {
return 1 - relativePosition(details.timeline, endTime.rangeName, endTime.offset);
}

// Map from an instance of ProxyAnimation to internal details about that animation.
// See ProxyAnimation constructor for details.
let proxyAnimations = new WeakMap();

// Clear cache containing the ProxyAnimation instances when leaving the page.
// See https://github.com/flackr/scroll-timeline/issues/146#issuecomment-1698159183
// for details.
window.addEventListener('pagehide', (e) => {
proxyAnimations = new WeakMap();
}, false);

// Map from the real underlying native animation to the ProxyAnimation proxy of it.
let proxiedAnimations = new WeakMap();

/**
* Procedure for calculating an auto-aligned start time.
* https://drafts.csswg.org/web-animations-2/#animation-calculating-an-auto-aligned-start-time
Expand Down Expand Up @@ -886,15 +902,14 @@ function autoAlignStartTime(details) {
// Create an alternate Animation class which proxies API requests.
// TODO: Create a full-fledged proxy so missing methods are automatically
// fetched from Animation.
let proxyAnimations = new WeakMap();

export class ProxyAnimation {
constructor(effect, timeline, animOptions={}) {
const animation =
(effect instanceof nativeAnimation) ?
effect : new nativeAnimation(effect, animationTimeline);
const isScrollAnimation = timeline instanceof ScrollTimeline;
const animationTimeline = isScrollAnimation ? undefined : timeline;
proxiedAnimations.set(animation, this);
proxyAnimations.set(this, {
animation: animation,
timeline: isScrollAnimation ? timeline : undefined,
Expand Down Expand Up @@ -1817,4 +1832,24 @@ export function animate(keyframes, options) {
}

return proxyAnimation;
};
}

function replaceProxiedAnimations(animationsList) {
for (let i = 0; i < animationsList.length; ++i) {
let proxyAnimation = proxiedAnimations.get(animationsList[i]);
if (proxyAnimation) {
animationsList[i] = proxyAnimation;
}
}
return animationsList;
}

export function elementGetAnimations(options) {
let animations = nativeElementGetAnimations.apply(this, [options]);
return replaceProxiedAnimations(animations);
}

export function documentGetAnimations(options) {
let animations = nativeDocumentGetAnimations.apply(this, [options]);
return replaceProxiedAnimations(animations);
}
34 changes: 10 additions & 24 deletions src/scroll-timeline-css.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,37 +140,23 @@ export function initCSSPolyfill() {

initMutationObserver();

// Cache all Proxy Animations
let proxyAnimations = new WeakMap();

// We are not wrapping capturing 'animationstart' by a 'load' event,
// because we may lose some of the 'animationstart' events by the time 'load' is completed.
window.addEventListener('animationstart', (evt) => {
evt.target.getAnimations().filter(anim => anim.animationName === evt.animationName).forEach(anim => {
// Create a per-element cache
if (!proxyAnimations.has(evt.target)) {
proxyAnimations.set(evt.target, new Map());
}
const elementProxyAnimations = proxyAnimations.get(evt.target);

// Store Proxy Animation in the cache
if (!elementProxyAnimations.has(anim.animationName)) {
const result = createScrollTimeline(anim, anim.animationName, evt.target);
if (result && result.timeline && anim.timeline != result.timeline) {
elementProxyAnimations.set(anim.animationName, new ProxyAnimation(anim, result.timeline, result.animOptions));
const result = createScrollTimeline(anim, anim.animationName, evt.target);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

result may be null.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Canceling and restarting a CSS animation will unconditionally create a new ScrollTimeline. Possibly not a big performance hit, but it looked like this was one of the things the previous cache PR was trying to avoid.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on result possibly being null

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

result may be null.

Thanks, done!

Canceling and restarting a CSS animation will unconditionally create a new ScrollTimeline. Possibly not a big performance hit, but it looked like this was one of the things the previous cache PR was trying to avoid.

I think createScrollTimeline is pretty inexpensive. Scroll driven animations themselves will generally start once and run forever. Non-scroll driven animations should quickly be determined not to match any of the rules in getAnimationTimeline (and we didn't used to cache misses anyways). There are other easy optimizations we could do here if it became a bottleneck.

if (result) {
// If the CSS Animation refers to a scroll or view timeline we need to proxy the animation instance.
if (result.timeline && !(anim instanceof ProxyAnimation)) {
const proxyAnimation = new ProxyAnimation(anim, result.timeline, result.animOptions);
anim.pause();
proxyAnimation.play();
} else {
elementProxyAnimations.set(anim.animationName, null);
// If the timeline was removed or the animation was already an instance of a proxy animation,
// invoke the set the timeline procedure on the existing animation.
anim.timeline = result.timeline;
}
}

// Get Proxy Animation from cache
const proxyAnimation = elementProxyAnimations.get(anim.animationName);

// Swap the original animation with the proxied one
if (proxyAnimation !== null) {
anim.pause();
proxyAnimation.play();
}
});
});

Expand Down
8 changes: 4 additions & 4 deletions test/expected.txt
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ FAIL /scroll-animations/css/animation-timeline-deferred.html Animation.timeline
FAIL /scroll-animations/css/animation-timeline-deferred.html Animation.timeline returns null for inactive deferred timeline
FAIL /scroll-animations/css/animation-timeline-deferred.html Animation.timeline returns null for inactive (overattached) deferred timeline
FAIL /scroll-animations/css/animation-timeline-ignored.tentative.html Changing animation-timeline changes the timeline (sanity check)
FAIL /scroll-animations/css/animation-timeline-ignored.tentative.html animation-timeline ignored after setting timeline with JS (ScrollTimeline from JS)
PASS /scroll-animations/css/animation-timeline-ignored.tentative.html animation-timeline ignored after setting timeline with JS (ScrollTimeline from JS)
FAIL /scroll-animations/css/animation-timeline-ignored.tentative.html animation-timeline ignored after setting timeline with JS (ScrollTimeline from CSS)
PASS /scroll-animations/css/animation-timeline-ignored.tentative.html animation-timeline ignored after setting timeline with JS (document timeline)
PASS /scroll-animations/css/animation-timeline-ignored.tentative.html animation-timeline ignored after setting timeline with JS (null)
FAIL /scroll-animations/css/animation-timeline-ignored.tentative.html animation-timeline ignored after setting timeline with JS (document timeline)
FAIL /scroll-animations/css/animation-timeline-ignored.tentative.html animation-timeline ignored after setting timeline with JS (null)
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked and these two new failures are really just exposing existing issues. The first is failing because the animated position is ever so slightly off from the position we had with the scroll timeline - perhaps we need to allow for some skew? The second is that we simply don't currently support setting timeline = null, but before this change we were setting timeline = null on the real animation rather than the proxy animation so it didn't exercise the code at all.

FAIL /scroll-animations/css/animation-timeline-in-keyframe.html The animation-timeline property may not be used in keyframes
PASS /scroll-animations/css/animation-timeline-multiple.html animation-timeline works with multiple timelines
FAIL /scroll-animations/css/animation-timeline-none.html Animation with animation-timeline:none holds current time at zero
Expand Down Expand Up @@ -957,4 +957,4 @@ FAIL /scroll-animations/view-timelines/view-timeline-sticky-block.html View time
FAIL /scroll-animations/view-timelines/view-timeline-sticky-inline.html View timeline with sticky target, block axis.
FAIL /scroll-animations/view-timelines/view-timeline-subject-size-changes.html View timeline with subject size change after the creation of the animation
FAIL /scroll-animations/view-timelines/zero-intrinsic-iteration-duration.tentative.html Intrinsic iteration duration is non-negative
Passed 392 of 959 tests.
Passed 391 of 959 tests.