Skip to content

Commit

Permalink
Replace proxied animations in getAnimations reusing proxied CSS anima…
Browse files Browse the repository at this point in the history
…tions (#89)

This makes document.getAnimations and Element.getAnimations correctly
return proxied instances of the animation so as to behave as expected.
Also by getting the proxied animations we can intelligently reuse the
existing proxied CSS animation when only timing details have changed
which implicitly fixes the performance issue of #84.
  • Loading branch information
flackr authored Dec 21, 2023
1 parent 665687c commit 0db3973
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 31 deletions.
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);
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)
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.

0 comments on commit 0db3973

Please sign in to comment.