diff --git a/package.json b/package.json index 4c940da3..9f3cebe5 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "dev": "run-all \"serve\" \" microbundle watch -f iife \"", "deploy": "npm run build", "test-setup": "node test/setup/checkout-wpt.mjs", - "test:wpt": "npm run test-setup && cd test && cd wpt && (python wpt run --headless -y --log-wptreport ../report/data.json --log-wptscreenshot=../report/screenshots.txt --log-html=../report/index.html --inject-script ../../dist/scroll-timeline.js firefox scroll-animations || true)", - "test:simple": "npm run test-setup && cd test && cd wpt && python wpt serve --inject-script ../../dist/scroll-timeline.js", + "test:wpt": "cd test && cd wpt && (python wpt run --headless -y --processes=12 --log-wptreport ../report/data.json --log-wptscreenshot=../report/screenshots.txt --log-html=../report/index.html --inject-script ../../dist/scroll-timeline.js firefox scroll-animations || true)", + "test:simple": "cd test && cd wpt && python wpt serve --inject-script ../../dist/scroll-timeline.js", "test:compare": "node test/summarize-json.mjs test/report/data.json > test/report/summary.txt && echo 'Comparing test results. If different and expected, patch the following diff to test/expected.txt:' && diff test/expected.txt test/report/summary.txt" }, "repository": { diff --git a/src/proxy-animation.js b/src/proxy-animation.js index bd279a2a..5adc0a2a 100644 --- a/src/proxy-animation.js +++ b/src/proxy-animation.js @@ -35,8 +35,14 @@ function createReadyPromise(details) { details.readyPromise = new PromiseWrapper(); // Trigger the pending task on the next animation frame. requestAnimationFrame(() => { - const timelineTime = details.timeline.currentTime; - if (timelineTime !== null) + const timelineTime = details.timeline?.currentTime ?? null; + let isReady = false + if (details.pendingTask === 'play') { + isReady = timelineTime !== null && (details.startTime !== null || details.holdTime !== null) + } else { + isReady = timelineTime !== null + } + if (isReady) notifyReady(details); }); } @@ -59,8 +65,9 @@ function toCssNumberish(details, value) { "InvalidStateError"); } + const rangeDuration = details.rangeDuration ?? 100; const limit = effectEnd(details); - const percent = limit ? 100 * value / limit : 0; + const percent = limit ? rangeDuration * value / limit : 0; return CSS.percent(percent); } @@ -75,7 +82,7 @@ function fromCssNumberish(details, value) { return value; const convertedTime = value.to('ms'); - if (convertTime) + if (convertedTime) return convertedTime.value; throw new DOMException( @@ -88,8 +95,9 @@ function fromCssNumberish(details, value) { return value; if (value.unit === 'percent') { + const rangeDuration = details.rangeDuration ?? 100; const duration = effectEnd(details); - return value.value * duration / 100; + return value.value * duration / rangeDuration; } throw new DOMException( @@ -255,6 +263,61 @@ function applyPendingPlaybackRate(details) { } } +/** + * Procedure to silently set the current time of an animation to seek time + * https://drafts.csswg.org/web-animations-2/#silently-set-the-current-time + * @param details + * @param {CSSUnitValue} seekTime + */ +function silentlySetTheCurrentTime(details, seekTime) { + // The procedure to silently set the current time of an animation, animation, to seek time is as follows: + // 1. If seek time is an unresolved time value, then perform the following steps. + // 1. If the current time is resolved, then throw a TypeError. + // 2. Abort these steps. + if (seekTime == null) { + if (details.currentTime !== null) { + throw new TypeError(); + } + } + // 2. Let valid seek time be the result of running the validate a CSSNumberish time procedure with seek time as the input. + // 3. If valid seek time is false, abort this procedure. + seekTime = fromCssNumberish(details, seekTime); + + // 4. Set auto align start time to false. + details.autoAlignStartTime = false; + + // 5. Update either animation’s hold time or start time as follows: + // + // 5a If any of the following conditions are true: + // - animation’s hold time is resolved, or + // - animation’s start time is unresolved, or + // - animation has no associated timeline or the associated timeline is inactive, or + // - animation’s playback rate is 0, + // 1. Set animation’s hold time to seek time. + // + // 5b Otherwise, + // Set animation’s start time to the result of evaluating timeline time - (seek time / playback rate) where + // timeline time is the current time value of timeline associated with animation. + if (details.holdTime !== null || details.startTime === null || + details.timeline.phase === 'inactive' || details.animation.playbackRate === 0) { + details.holdTime = seekTime; + } else { + details.startTime = + fromCssNumberish(details, details.timeline.currentTime) - seekTime / details.animation.playbackRate; + } + + // 6. If animation has no associated timeline or the associated timeline is inactive, make animation’s start time + // unresolved. + // This preserves the invariant that when we don’t have an active timeline it is only possible to set either the + // start time or the animation’s current time. + if (details.timeline.phase === 'inactive') { + details.startTime = null; + } + + // 7. Make animation’s previous current time unresolved. + details.previousCurrentTime = null +} + function calculateCurrentTime(details) { if (!details.timeline) return null; @@ -451,88 +514,75 @@ function playInternal(details, autoRewind) { // false. let hasPendingReadyPromise = false; - // 3. Let seek time be a time value that is initially unresolved. - let seekTime = null; - - // 4. Let has finite timeline be true if animation has an associated + // 3. Let has finite timeline be true if animation has an associated // timeline that is not monotonically increasing. // Note: this value will always true at this point in the polyfill. // Following steps are pruned based on the procedure for scroll // timelines. - - // 5. Perform the steps corresponding to the first matching condition from + // + // 4. Let previous current time be the animation’s current time + // + // 5. Let enable seek be true if the auto-rewind flag is true and has finite timeline is false. + // Otherwise, initialize to false. + // + // 6. Perform the steps corresponding to the first matching condition from // the following, if any: // - // 5a If animation’s effective playback rate > 0, the auto-rewind flag is + // 6a If animation’s effective playback rate > 0, enable seek is // true and either animation’s: - // current time is unresolved, or - // current time < zero, or - // current time >= target effect end, - // 5a1. Set seek time to zero. + // previous current time is unresolved, or + // previous current time < zero, or + // previous current time >= associated effect end, + // 6a1. Set the animation’s hold time to zero. // - // 5b If animation’s effective playback rate < 0, the auto-rewind flag is + // 6b If animation’s effective playback rate < 0, enable seek is // true and either animation’s: - // current time is unresolved, or - // current time ≤ zero, or - // current time > target effect end, - // 5b1. If associated effect end is positive infinity, + // previous current time is unresolved, or + // previous current time is ≤ zero, or + // previous current time is > associated effect end, + // 6b1. If associated effect end is positive infinity, // throw an "InvalidStateError" DOMException and abort these steps. - // 5b2. Otherwise, - // 5b2a Set seek time to animation's associated effect end. + // 6b2. Otherwise, + // 5b2a Set the animation’s hold time to the animation’s associated effect end. // - // 5c If animation’s effective playback rate = 0 and animation’s current time + // 6c If animation’s effective playback rate = 0 and animation’s current time // is unresolved, - // 5c1. Set seek time to zero. + // 6c1. Set the animation’s hold time to zero. let previousCurrentTime = fromCssNumberish(details, details.proxy.currentTime); - // Resume of a paused animation after a timeline change snaps to the scroll - // position. - if (details.resetCurrentTimeOnResume) { - previousCurrentTime = null; - details.resetCurrentTimeOnResume = false; - } - const playbackRate = effectivePlaybackRate(details); - const upperBound = effectEnd(details); - if (playbackRate > 0 && autoRewind && (previousCurrentTime == null || - previousCurrentTime < 0 || - previousCurrentTime >= upperBound)) { - seekTime = 0; - } else if (playbackRate < 0 && autoRewind && - (previousCurrentTime == null || previousCurrentTime <= 0 || - previousCurrentTime > upperBound)) { - if (upperBound == Infinity) { - // Defer to native implementation to handle throwing the exception. - details.animation.play(); - return; - } - seekTime = upperBound; - } else if (playbackRate == 0 && previousCurrentTime == null) { - seekTime = 0; + if (playbackRate == 0 && previousCurrentTime == null) { + details.holdTime = 0; } - - // 6. If seek time is resolved, - // 6a1. Set animation's start time to seek time. - // 6a2. Let animation's hold time be unresolved. - // 6a3. Apply any pending playback rate on animation. - if (seekTime != null) { - details.startTime = seekTime; - details.holdTime = null; - applyPendingPlaybackRate(details); + // 12. If has finite timeline and previous current time is unresolved: + // Set the flag auto align start time to true. + // NOTE: If play is called for a CSS animation during style update, the animation’s start time cannot be reliably + // calculated until post layout since the start time is to align with the start or end of the animation range + // (depending on the playback rate). In this case, the animation is said to have an auto-aligned start time, + // whereby the start time is automatically adjusted as needed to align the animation’s progress to the + // animation range. + if (previousCurrentTime == null) { + details.autoAlignStartTime = true; } - // Additional step for the polyfill. - addAnimation(details.timeline, details.animation, - tickAnimation.bind(details.proxy), renormalizeTiming.bind(details.proxy)); + // Not by spec, but required by tests in play-animation.html: + // - Playing a finished animation restarts the animation aligned at the start + // - Playing a pause-pending but previously finished animation realigns with the scroll position + // - Playing a finished animation clears the start time + if (details.proxy.playState === 'finished' || abortedPause) { + details.holdTime = null + details.startTime = null + details.autoAlignStartTime = true; + } - // 7. If animation's hold time is resolved, let its start time be - // unresolved. + // 13. If animation's hold time is resolved, let its start time be + // unresolved. if (details.holdTime) { details.startTime = null; } - // 8. If animation has a pending play task or a pending pause task, + // 14. If animation has a pending play task or a pending pause task, // 8.1 Cancel that task. // 8.2 Set has pending ready promise to true. if (details.pendingTask) { @@ -540,17 +590,19 @@ function playInternal(details, autoRewind) { hasPendingReadyPromise = true; } - // 9. If the following three conditions are all satisfied: + // 15. If the following three conditions are all satisfied: // animation’s hold time is unresolved, and - // seek time is unresolved, and // aborted pause is false, and // animation does not have a pending playback rate, // abort this procedure. - if (details.holdTime === null && seekTime === null && + // Additonal check for polyfill: Does not have the auto align start time flag set. + // If we return when this flag is set, a play task will not be scheduled, leaving the animation in the + // idle state. If the animation is in the idle state, the auto align procedure will bail. + if (details.holdTime === null && !details.autoAlignStartTime && !abortedPause && details.pendingPlaybackRate === null) - return; + return; - // 10. If has pending ready promise is false, let animation’s current ready + // 16. If has pending ready promise is false, let animation’s current ready // promise be a new promise in the relevant Realm of animation. if (details.readyPromise && !hasPendingReadyPromise) details.readyPromise = null; @@ -559,12 +611,18 @@ function playInternal(details, autoRewind) { // correct value for current time. syncCurrentTime(details); - // 11. Schedule a task to run as soon as animation is ready. + // 17. Schedule a task to run as soon as animation is ready. if (!details.readyPromise) createReadyPromise(details); details.pendingTask = 'play'; - // 12. Run the procedure to update an animation’s finished state for animation + // Additional step for the polyfill. + // This must run after setting up the ready promise, otherwise we will run + // the procedure for calculating auto aligned start time before play state is running + addAnimation(details.timeline, details.animation, + tickAnimation.bind(details.proxy)); + + // 18. Run the procedure to update an animation’s finished state for animation // with the did seek flag set to false, and the synchronously notify flag // set to false. updateFinishedState(details, /* seek */ false, /* synchronous */ false); @@ -575,13 +633,24 @@ function tickAnimation(timelineTime) { if (timelineTime == null) { // While the timeline is inactive, it's effect should not be applied. // To polyfill this behavior, we cancel the underlying animation. - if (details.animation.playState != 'idle') + if (details.proxy.playState !== 'paused' && details.animation.playState != 'idle') details.animation.cancel(); return; } + // When updating timeline current time, the start time of any attached animation is conditionally updated. For each + // attached animation, run the procedure for calculating an auto-aligned start time. + autoAlignStartTime(details); + if (details.pendingTask) { - notifyReady(details); + // Commit pending tasks asynchronously if they are ready after aligning start time + requestAnimationFrame(() => { + if (details.pendingTask === 'play' && (details.startTime !== null || details.holdTime !== null)) { + commitPendingPlay(details); + } else if (details.pendingTask === 'pause') { + commitPendingPause(details); + } + }); } const playState = this.playState; @@ -601,12 +670,9 @@ function tickAnimation(timelineTime) { } } -function renormalizeTiming() { - const details = proxyAnimations.get(this); - if (details) { - // Force renormalization. - details.specifiedTiming = null; - } +function renormalizeTiming(details) { + // Force renormalization. + details.specifiedTiming = null; } function notifyReady(details) { @@ -646,7 +712,7 @@ function createProxyEffect(details) { const timing = target.apply(effect); if (details.timeline) { - const preConvertLocalTime = timing.localTime; + const rangeDuration = details.duration ?? 100; timing.localTime = toCssNumberish(details, timing.localTime); timing.endTime = toCssNumberish(details, timing.endTime); timing.activeDuration = @@ -655,7 +721,7 @@ function createProxyEffect(details) { const iteration_duration = timing.iterations ? (limit - timing.delay - timing.endDelay) / timing.iterations : 0; timing.duration = limit ? - CSS.percent(100 * iteration_duration / limit) : + CSS.percent(rangeDuration * iteration_duration / limit) : CSS.percent(0); // Correct for inactive timeline. @@ -667,7 +733,7 @@ function createProxyEffect(details) { } }; // Override getTiming to normalize the timing. EffectEnd for the animation - // align with the timeline duration. + // align with the range duration. const getTimingHandler = { apply: function(target, thisArg) { // Arbitrary conversion of 100% to ms. @@ -679,35 +745,17 @@ function createProxyEffect(details) { details.specifiedTiming = target.apply(effect); let timing = Object.assign({}, details.specifiedTiming); - const timeline = details.timeline; - // TODO: These delays most likely need to be rewritten to rangeStart/rangeEnd - let computedDelays = false; - let startDelay; - let endDelay; - if (timeline instanceof ViewTimeline) { - // Compute start and end delay to align with start and end times. - // If times not specified use cover 0% to cover 100%. - startDelay = fractionalStartDelay(details); - endDelay = fractionalEndDelay(details); - computedDelays = true; - } - let totalDuration; // Duration 'auto' case. - if (timing.duration === null || timing.duration === 'auto' || - computedDelays) { + if (timing.duration === null || timing.duration === 'auto' || details.autoDurationEffect) { if (details.timeline) { - if (computedDelays) { - timing.delay = startDelay * INTERNAL_DURATION_MS; - timing.endDelay = endDelay * INTERNAL_DURATION_MS; - } else { - // TODO: start and end delay are specced as doubles and currently - // ignored for a progress based animation. Support delay and endDelay - // once CSSNumberish. - timing.delay = 0; - timing.endDelay = 0; - } + details.autoDurationEffect = true + // TODO: start and end delay are specced as doubles and currently + // ignored for a progress based animation. Support delay and endDelay + // once CSSNumberish. + timing.delay = 0; + timing.endDelay = 0; totalDuration = timing.iterations ? INTERNAL_DURATION_MS : 0; timing.duration = timing.iterations ? (totalDuration - timing.delay - timing.endDelay) / @@ -748,6 +796,10 @@ function createProxyEffect(details) { "Effect iterations cannot be Infinity when used with Scroll " + "Timelines"); } + + if (typeof duration !== 'undefined' && duration !== 'auto') { + details.autoDurationEffect = null + } } // Apply updates on top of the original specified timing. @@ -755,7 +807,7 @@ function createProxyEffect(details) { target.apply(effect, [details.specifiedTiming]); } target.apply(effect, argumentsList); - renormalizeTiming() + renormalizeTiming(details); } }; const proxy = new Proxy(effect, handler); @@ -784,6 +836,60 @@ function fractionalEndDelay(details) { return 1 - relativePosition(details.timeline, endTime.rangeName, endTime.offset); } +/** + * Procedure for calculating an auto-aligned start time. + * https://drafts.csswg.org/web-animations-2/#animation-calculating-an-auto-aligned-start-time + * @param details + */ +function autoAlignStartTime(details) { + // When attached to a non-monotonic timeline, the start time of the animation may be layout dependent. In this case, + // we defer calculation of the start time until the timeline has been updated post layout. When updating timeline + // current time, the start time of any attached animation is conditionally updated. The procedure for calculating an + // auto-aligned start time is as follows: + + // 1. If the auto-align start time flag is false, abort this procedure. + if (!details.autoAlignStartTime) { + return; + } + + // 2. If the timeline is inactive, abort this procedure. + if (!details.timeline || !details.timeline.currentTime) { + return; + } + + // 3. If play state is idle, abort this procedure. + // 4. If play state is paused, and hold time is resolved, abort this procedure. + if (details.proxy.playState === 'idle' || + (details.proxy.playState === 'paused' && details.holdTime !== null)) { + return; + } + + const previousRangeDuration = details.rangeDuration + + // 5. Let start offset be the resolved timeline time corresponding to the start of the animation attachment range. + // In the case of view timelines, it requires a calculation based on the proportion of the cover range. + const startOffset = CSS.percent(fractionalStartDelay(details) * 100) + + // 6. Let end offset be the resolved timeline time corresponding to the end of the animation attachment range. + // In the case of view timelines, it requires a calculation based on the proportion of the cover range. + const endOffset = CSS.percent((1 - fractionalEndDelay(details)) * 100) + + // Store the range duration, until we can find a spec aligned method to calculate iteration duration + // TODO: Clarify how range duration should be resolved + details.rangeDuration = endOffset.value - startOffset.value + // 7. Set start time to start offset if effective playback rate ≥ 0, and end offset otherwise. + const playbackRate = effectivePlaybackRate(details); + details.startTime = fromCssNumberish(details,playbackRate >= 0 ? startOffset : endOffset) + + // 8. Clear hold time. + details.holdTime = null + + // Additional polyfill step needed to renormalize timing when range has changed + if (details.rangeDuration !== previousRangeDuration) { + renormalizeTiming(details) + } +} + // Create an alternate Animation class which proxies API requests. // TODO: Create a full-fledged proxy so missing methods are automatically // fetched from Animation. @@ -811,10 +917,9 @@ export class ProxyAnimation { // numbers in milliseconds. startTime: null, holdTime: null, + rangeDuration: null, previousCurrentTime: null, - // When changing the timeline on a paused animation, we defer updating the - // start time until the animation resumes playing. - resetCurrentTimeOnResume: false, + autoAlignStartTime: false, // Calls to reverse and updatePlaybackRate set a pending rate that does // not immediately take effect. The value of this property is // inaccessible via the web animations API and therefore explicitly @@ -861,6 +966,7 @@ export class ProxyAnimation { details.animation.effect = newEffect; // Reset proxy to force re-initialization the next time it is accessed. details.effect = null; + details.autoDurationEffec = null; } get timeline() { @@ -871,6 +977,7 @@ export class ProxyAnimation { } set timeline(newTimeline) { // https://drafts4.csswg.org/web-animations-2/#setting-the-timeline + const details = proxyAnimations.get(this); // 1. Let old timeline be the current timeline of animation, if any. // 2. If new timeline is the same object as old timeline, abort this @@ -885,27 +992,36 @@ export class ProxyAnimation { // 4. Let previous current time be the animation’s current time. const previousCurrentTime = this.currentTime; - const details = proxyAnimations.get(this); - const end = effectEnd(details); - const progress = - end > 0 ? fromCssNumberish(details, previousCurrentTime) / end : 0; + // 5. Set previous progress based in the first condition that applies: + // If previous current time is unresolved: + // Set previous progress to unresolved. + // If endTime time is zero: + // Set previous progress to zero. + // Otherwise + // Set previous progress = previous current time / endTime time + let end = effectEnd(details); + let previousProgress; + if (previousCurrentTime === null) { + previousProgress = null + } else if (end === 0) { + previousProgress = 0; + } else { + previousProgress = fromCssNumberish(details, previousCurrentTime) / end; + } - // 5. Let from finite timeline be true if old timeline is not null and not + // 9. Let from finite timeline be true if old timeline is not null and not // monotonically increasing. const fromScrollTimeline = (oldTimeline instanceof ScrollTimeline); - // 6. Let to finite timeline be true if timeline is not null and not + // 10. Let to finite timeline be true if timeline is not null and not // monotonically increasing. const toScrollTimeline = (newTimeline instanceof ScrollTimeline); - // 7. Let the timeline of animation be new timeline. + // 11. Let the timeline of animation be new timeline. // Cannot assume that the native implementation has mutable timeline // support. Deferring this step until we know that we are either // polyfilling, supporting natively, or throwing an error. - // 8. Set the flag reset current time on resume to false. - details.resetCurrentTimeOnResume = false; - // Additional step required to track whether the animation was pending in // order to set up a new ready promise if needed. const pending = this.pending; @@ -914,53 +1030,41 @@ export class ProxyAnimation { removeAnimation(details.timeline, details.animation); } - // 9. Perform the steps corresponding to the first matching condition from + // 12. Perform the steps corresponding to the first matching condition from // the following, if any: // If to finite timeline, if (toScrollTimeline) { - // Deferred step 7. + // Deferred step 11. details.timeline = newTimeline; // 1. Apply any pending playback rate on animation applyPendingPlaybackRate(details); - // 2. Let seek time be zero if playback rate >= 0, and animation’s - // associated effect end otherwise. - const seekTime = - details.animation.playbackRate >= 0 ? 0 : effectEnd(details); - - // 3. Update the animation based on the first matching condition if any: - switch (previousPlayState) { - // If either of the following conditions are true: - // * previous play state is running or, - // * previous play state is finished - // Set animation’s start time to seek time. - case 'running': - case 'finished': - details.startTime = seekTime; - // Additional polyfill step needed to associate the animation with - // the scroll timeline. - addAnimation(details.timeline, details.animation, - tickAnimation.bind(this), renormalizeTiming.bind(this)); - break; - - // If previous play state is paused: - // If previous current time is resolved: - // * Set the flag reset current time on resume to true. - // * Set start time to unresolved. - // * Set hold time to previous current time. - case 'paused': - details.resetCurrentTimeOnResume = true; - details.startTime = null; - details.holdTime = - fromCssNumberish(details, CSS.percent(100 * progress)); - break; - - // Oterwise - default: - details.holdTime = null; - details.startTime = null; + // 2. Set auto align start time to true. + details.autoAlignStartTime = true; + // 3. Set start time to unresolved. + details.startTime = null; + // 4. Set hold time to unresolved. + details.holdTime = null; + + // 5. If previous play state is "finished" or "running" + if (previousPlayState === 'running' || previousPlayState === 'finished') { + // 1. Schedule a pending play task + if (!details.readyPromise || details.readyPromise.state === 'resolved') { + createReadyPromise(details); + } + details.pendingTask = 'play'; + // Additional polyfill step needed to associate the animation with + // the scroll timeline. + addAnimation(details.timeline, details.animation, + tickAnimation.bind(this)); + } + // 6. If previous play state is "paused" and previous progress is resolved: + if (previousPlayState === 'paused' && previousProgress !== null) { + // 1. Set hold time to previous progress * endTime time. This step ensures that previous progress is preserved + // even in the case of a pause-pending animation with a resolved start time. + details.holdTime = previousProgress * end; } // Additional steps required if the animation is pending as we need to @@ -982,14 +1086,14 @@ export class ProxyAnimation { // a monotonic timeline as well; however, we do not have a direct means // of applying the steps to the native animation. - // 10. If the start time of animation is resolved, make animation’s hold + // 15. If the start time of animation is resolved, make animation’s hold // time unresolved. This step ensures that the finished play state of // animation is not “sticky” but is re-evaluated based on its updated // current time. if (details.startTime !== null) details.holdTime = null; - // 11. Run the procedure to update an animation’s finished state for + // 16. Run the procedure to update an animation’s finished state for // animation with the did seek flag set to false, and the // synchronously notify flag set to false. updateFinishedState(details, false, false); @@ -998,7 +1102,7 @@ export class ProxyAnimation { // To monotonic timeline. if (details.animation.timeline == newTimeline) { - // Deferred step 7 from above. Clearing the proxy's timeline will + // Deferred step 11 from above. Clearing the proxy's timeline will // re-associate the proxy with the native animation. removeAnimation(details.timeline, details.animation); details.timeline = null; @@ -1007,7 +1111,7 @@ export class ProxyAnimation { // Run the procedure to set the current time to previous current time. if (fromScrollTimeline) { if (previousCurrentTime !== null) - details.animation.currentTime = progress * effectEnd(details); + details.animation.currentTime = previousProgress * effectEnd(details); switch (previousPlayState) { case 'paused': @@ -1034,20 +1138,26 @@ export class ProxyAnimation { set startTime(value) { // https://drafts.csswg.org/web-animations/#setting-the-start-time-of-an-animation const details = proxyAnimations.get(this); + // 1. Let valid start time be the result of running the validate a CSSNumberish time procedure with new start time + // as the input. + // 2. If valid start time is false, abort this procedure. value = fromCssNumberish(details, value); if (!details.timeline) { details.animation.startTime = value; return; } - // 1. Let timeline time be the current time value of the timeline that + // 3. Set auto align start time to false. + details.autoAlignStartTime = false; + + // 4. Let timeline time be the current time value of the timeline that // animation is associated with. If there is no timeline associated with // animation or the associated timeline is inactive, let the timeline // time be unresolved. const timelineTime = fromCssNumberish(details, details.timeline.currentTime); - // 2. If timeline time is unresolved and new start time is resolved, make + // 5. If timeline time is unresolved and new start time is resolved, make // animation’s hold time unresolved. if (timelineTime == null && details.startTime != null) { details.holdTime = null; @@ -1056,21 +1166,18 @@ export class ProxyAnimation { syncCurrentTime(details); } - // 3. Let previous current time be animation’s current time. + // 6. Let previous current time be animation’s current time. // Note: This is the current time after applying the changes from the // previous step which may cause the current time to become unresolved. const previousCurrentTime = fromCssNumberish(details, this.currentTime); - // 4. Apply any pending playback rate on animation. + // 7. Apply any pending playback rate on animation. applyPendingPlaybackRate(details); - // 5. Set animation’s start time to new start time. + // 8. Set animation’s start time to new start time. details.startTime = value; - // 6. Set the reset current time on resume flag to false. - details.resetCurrentTimeOnResume = false; - - // 7. Update animation’s hold time based on the first matching condition + // 9. Update animation’s hold time based on the first matching condition // from the following, // If new start time is resolved, @@ -1086,17 +1193,17 @@ export class ProxyAnimation { else details.holdTime = previousCurrentTime; - // 7. If animation has a pending play task or a pending pause task, cancel - // that task and resolve animation’s current ready promise with - // animation. + // 12. If animation has a pending play task or a pending pause task, cancel + // that task and resolve animation’s current ready promise with + // animation. if (details.pendingTask) { details.pendingTask = null; details.readyPromise.resolve(this); } - // 8. Run the procedure to update an animation’s finished state for animation - // with the did seek flag set to true, and the synchronously notify flag - // set to false. + // 13. Run the procedure to update an animation’s finished state for animation + // with the did seek flag set to true, and the synchronously notify flag + // set to false. updateFinishedState(details, true, false); // Ensure that currentTime is updated for the native animation. @@ -1115,45 +1222,30 @@ export class ProxyAnimation { } set currentTime(value) { const details = proxyAnimations.get(this); - value = fromCssNumberish(details, value); - if (!details.timeline || value == null) { + if (!details.timeline) { details.animation.currentTime = value; return; } - - // https://drafts.csswg.org/web-animations/#setting-the-current-time-of-an-animation - const previouStartTime = details.startTime; - const previousHoldTime = details.holdTime; - const timelinePhase = details.timeline.phase; - - // Update either the hold time or the start time. - if (details.holdTime !== null || details.startTime === null || - timelinePhase == 'inactive' || details.animation.playbackRate == 0) { - // TODO: Support hold phase. - details.holdTime = value; - } else { - details.startTime = calculateStartTime(details, value); - } - details.resetCurrentTimeOnResume = false; - - // Preserve invariant that we can only set a start time or a hold time in - // the absence of an active timeline. - if (timelinePhase == 'inactive') - details.startTime = null; - - // Reset the previous current time. - details.previousCurrentTime = null; - - // Synchronously resolve pending pause task. + // https://drafts.csswg.org/web-animations-2/#setting-the-current-time-of-an-animation + // 1. Run the steps to silently set the current time of animation to seek time. + silentlySetTheCurrentTime(details, value); + + // 2. If animation has a pending pause task, synchronously complete the pause operation by performing the following steps: + // 1. Set animation’s hold time to seek time. + // 2. Apply any pending playback rate to animation. + // 3. Make animation’s start time unresolved. + // 4. Cancel the pending pause task. + // 5. Resolve animation’s current ready promise with animation. if (details.pendingTask == 'pause') { - details.holdTime = value; + details.holdTime = fromCssNumberish(details, value); applyPendingPlaybackRate(details); details.startTime = null; details.pendingTask = null; details.readyPromise.resolve(this); } - // Update the finished state. + // 3. Run the procedure to update an animation’s finished state for animation with the did seek flag set to true, + // and the synchronously notify flag set to false. updateFinishedState(details, true, false); } @@ -1334,51 +1426,34 @@ export class ProxyAnimation { } // https://www.w3.org/TR/web-animations-1/#pausing-an-animation-section + // and https://drafts.csswg.org/web-animations-2/#pausing-an-animation-section // 1. If animation has a pending pause task, abort these steps. // 2. If the play state of animation is paused, abort these steps. if (this.playState == "paused") return; - // 3. Let seek time be a time value that is initially unresolved. - // 4. Let has finite timeline be true if animation has an associated - // timeline that is not monotonically increasing. + // Replaced steps from https://drafts.csswg.org/web-animations-2/#pausing-an-animation-section + // + // 3. Let has finite timeline be true if animation has an associated timeline that is not monotonically increasing. // Note: always true if we have reached this point in the polyfill. // Pruning following steps to be specific to scroll timelines. - let seekTime = null; - - // 5. If the animation’s current time is unresolved, perform the steps - // according to the first matching condition from below: - // 5a. If animation’s playback rate is ≥ 0, - // Set seek time to zero. - // 5b. Otherwise, - // If associated effect end for animation is positive infinity, - // throw an "InvalidStateError" DOMException and abort these - // steps. - // Otherwise, - // Set seek time to animation's associated effect end. - - const playbackRate = details.animation.playbackRate; - const duration = effectEnd(details); - + // 4. If the animation’s current time is unresolved and has finite timeline is false, perform the steps according + // to the first matching condition below: + // + // 4a If animation’s playback rate is ≥ 0, + // Set hold time to zero. + // 4b Otherwise, + // 4b1 If associated effect end for animation is positive infinity, + // throw an "InvalidStateError" DOMException and abort these steps. + // 4b2 Otherwise, + // Set hold time to animation’s associated effect end. + // If has finite timeline is true, and the animation’s current time is unresolved + // Set the auto align start time flag to true. if (details.animation.currentTime === null) { - if (playbackRate >= 0) { - seekTime = 0; - } else if (duration == Infinity) { - // Let native implementation take care of throwing the exception. - details.animation.pause(); - return; - } else { - seekTime = duration; - } + details.autoAlignStartTime = true; } - // 6. If seek time is resolved, - // If has finite timeline is true, - // Set animation's start time to seek time. - if (seekTime !== null) - details.startTime = seekTime; - // 7. Let has pending ready promise be a boolean flag that is initially // false. // 8. If animation has a pending play task, cancel that task and let has @@ -1390,20 +1465,26 @@ export class ProxyAnimation { else details.readyPromise = null; - // 10. Schedule a task to be executed at the first possible moment after the - // user agent has performed any processing necessary to suspend the - // playback of animation’s target effect, if any. + // 10. Schedule a task to be executed at the first possible moment where all of the following conditions are true: + // + // the user agent has performed any processing necessary to suspend the playback of animation’s associated + // effect, if any. + // the animation is associated with a timeline that is not inactive. + // the animation has a resolved hold time or start time. if (!details.readyPromise) createReadyPromise(details); details.pendingTask ='pause'; + + // Additional step for the polyfill. + // This must run after setting up the ready promise, otherwise we will run + // the procedure for calculating auto aligned start time before play state is running + addAnimation(details.timeline, details.animation, tickAnimation.bind(details.proxy)); } reverse() { const details = proxyAnimations.get(this); const playbackRate = effectivePlaybackRate(details); - const previousCurrentTime = - details.resetCurrentTimeOnResume ? - null : fromCssNumberish(details, this.currentTime); + const previousCurrentTime = fromCssNumberish(details, this.currentTime); const inifiniteDuration = effectEnd(details) == Infinity; // Let the native implementation handle throwing the exception in cases diff --git a/src/scroll-timeline-base.js b/src/scroll-timeline-base.js index f37a23d2..fdf3b85d 100644 --- a/src/scroll-timeline-base.js +++ b/src/scroll-timeline-base.js @@ -18,6 +18,7 @@ import {simplifyCalculation} from "./simplify-calculation"; installCSSOM(); const DEFAULT_TIMELINE_AXIS = 'block'; +const NAMED_TIMELINE_RANGES = ["contain", "cover", "entry", "exit", "entry-crossing", "exit-crossing"]; let scrollTimelineOptions = new WeakMap(); let sourceDetails = new WeakMap(); @@ -27,6 +28,14 @@ function scrollEventSource(source) { return source; } +function updateRanges(scrollTimelineInstance) { + const details = scrollTimelineOptions.get(scrollTimelineInstance); + // Store ranges + for (const rangeName of NAMED_TIMELINE_RANGES) { + details.ranges[rangeName] = range(scrollTimelineInstance, rangeName); + } +} + /** * Updates the currentTime for all Web Animation instanced attached to a ScrollTimeline instance * @param scrollTimelineInstance {ScrollTimeline} @@ -52,14 +61,14 @@ function updateInternal(scrollTimelineInstance) { function directionAwareScrollOffset(source, axis) { if (!source) return null; - const scrollPos = sourceDetails.get(source).scrollPos; + const measurements = sourceDetails.get(source).measurements; const style = getComputedStyle(source); // All writing modes are vertical except for horizontal-tb. // TODO: sideways-lr should flow bottom to top, but is currently unsupported // in Chrome. // http://drafts.csswg.org/css-writing-modes-4/#block-flow const horizontalWritingMode = style.writingMode == 'horizontal-tb'; - let currentScrollOffset = scrollPos.scrollTop; + let currentScrollOffset = measurements.scrollTop; if (axis == 'x' || (axis == 'inline' && horizontalWritingMode) || (axis == 'block' && !horizontalWritingMode)) { @@ -68,7 +77,7 @@ function directionAwareScrollOffset(source, axis) { // block flow. This is a consequence of shifting the scroll origin due to // changes in the overflow direction. // http://drafts.csswg.org/cssom-view/#overflow-directions. - currentScrollOffset = Math.abs(scrollPos.scrollLeft); + currentScrollOffset = Math.abs(measurements.scrollLeft); } return currentScrollOffset; } @@ -90,6 +99,7 @@ export function calculateTargetEffectEnd(animation) { * @returns {number} */ export function calculateMaxScrollOffset(source, axis) { + const measurements = sourceDetails.get(source).measurements; // Only one horizontal writing mode: horizontal-tb. All other writing modes // flow vertically. const horizontalWritingMode = @@ -99,9 +109,9 @@ export function calculateMaxScrollOffset(source, axis) { else if (axis === "inline") axis = horizontalWritingMode ? "x" : "y"; if (axis === "y") - return source.scrollHeight - source.clientHeight; + return measurements.scrollHeight - measurements.clientHeight; else if (axis === "x") - return source.scrollWidth - source.clientWidth; + return measurements.scrollWidth - measurements.clientWidth; } function resolvePx(cssValue, resolvedLength) { @@ -158,6 +168,42 @@ function validateAnonymousSource(timeline) { updateSource(timeline, source); } +/** + * Read measurements of source element + * @param {HTMLElement} source + * @return {{clientWidth: *, scrollHeight: *, scrollLeft, clientHeight: *, scrollTop, scrollWidth: *}} + */ +function measureSource (source) { + return { + scrollLeft: source.scrollLeft, + scrollTop: source.scrollTop, + scrollWidth: source.scrollWidth, + scrollHeight: source.scrollHeight, + clientWidth: source.clientWidth, + clientHeight: source.clientHeight + }; +} + +/** + * Update measurements of source, and update timelines + * @param {HTMLElement} source + */ +function updateSourceMeasurements(source) { + let details = sourceDetails.get(source); + details.measurements = measureSource(source); + // Update dimensions of ranges before ticking the timelines + for (const timeline of details.timelines) { + updateRanges(timeline); + } + requestAnimationFrame(() => { + // Defer ticking timeline to animation frame to prevent + // "ResizeObserver loop completed with undelivered notifications" + for (const timeline of details.timelines) { + updateInternal(timeline); + } + }); +} + function updateSource(timeline, source) { const oldSource = scrollTimelineOptions.get(timeline).source; if (oldSource == source) @@ -181,50 +227,39 @@ function updateSource(timeline, source) { let details = sourceDetails.get(source); if (!details) { // This is the first timeline for this source - // Store a list of connected timelines and current scroll position - details = { - timelines: [], - scrollPos: { - scrollLeft: source.scrollLeft, - scrollTop: source.scrollTop - } - }; + // Store a list of connected timelines and current measurements + details = {timelines: [], measurements: measureSource(source)}; sourceDetails.set(source, details); - const resizeObserver = new ResizeObserver(() => { - // Sample and store scroll pos - details.scrollPos = { - scrollLeft: source.scrollLeft, - scrollTop: source.scrollTop - }; - requestAnimationFrame(() => { - // Defer ticking timeline to animation frame to prevent - // "ResizeObserver loop completed with undelivered notifications" - for (const timeline of details.timelines) { - renormalizeAnimationTimings(timeline); - } - }); - }); + // Use resize observer to detect changes to source size + const resizeObserver = new ResizeObserver(() => updateSourceMeasurements(source)); resizeObserver.observe(source); + for (const child of source.children) { + resizeObserver.observe(child); + } + + // Use mutation observer to detect updated style and class attributes on source element + const mutationObserver = new MutationObserver(() => updateSourceMeasurements(source)); + mutationObserver.observe(source, {attributes: true, attributeFilter: ['style', 'class']}); const scrollListener = () => { // Sample and store scroll pos - details.scrollPos = { - scrollLeft: source.scrollLeft, - scrollTop: source.scrollTop - }; + details.measurements.scrollLeft = source.scrollLeft; + details.measurements.scrollTop = source.scrollTop; + for (const timeline of details.timelines) { updateInternal(timeline); } }; scrollEventSource(source).addEventListener("scroll", scrollListener); details.disconnect = () => { - resizeObserver.unobserve(source); resizeObserver.disconnect(); + mutationObserver.disconnect(); scrollEventSource(source).removeEventListener("scroll", scrollListener); }; } details.timelines.push(timeline); + updateRanges(timeline); } } @@ -248,9 +283,8 @@ export function removeAnimation(scrollTimeline, animation) { * @param scrollTimeline {ScrollTimeline} * @param animation {Animation} * @param tickAnimation {function(number)} - * @param renormalizeTiming {function()} */ -export function addAnimation(scrollTimeline, animation, tickAnimation, renormalizeTiming) { +export function addAnimation(scrollTimeline, animation, tickAnimation) { let animations = scrollTimelineOptions.get(scrollTimeline).animations; for (let i = 0; i < animations.length; i++) { // @TODO: This early return causes issues when a page with the polyfill @@ -264,20 +298,11 @@ export function addAnimation(scrollTimeline, animation, tickAnimation, renormali animations.push({ animation: animation, - tickAnimation: tickAnimation, - renormalizeTiming: renormalizeTiming + tickAnimation: tickAnimation }); updateInternal(scrollTimeline); } -function renormalizeAnimationTimings(scrollTimeline) { - let animations = scrollTimelineOptions.get(scrollTimeline).animations; - for (const animation of animations) { - animation.renormalizeTiming(); - } - updateInternal(scrollTimeline); -} - // TODO: this is a private function used for unit testing add function export function _getStlOptions(scrollTimeline) { return scrollTimelineOptions.get(scrollTimeline); @@ -298,6 +323,9 @@ export class ScrollTimeline { // Internal members animations: [], scrollListener: null, + + // Stored ranges + ranges: {} }); const source = options && options.source !== undefined ? options.source @@ -662,8 +690,9 @@ function parseInset(value, containerSize) { // Calculate the fractional offset of a (phase, percent) pair relative to the // full cover range. export function relativePosition(timeline, phase, offset) { - const phaseRange = range(timeline, phase); - const coverRange = range(timeline, 'cover'); + const details = scrollTimelineOptions.get(timeline); + const phaseRange = details.ranges[phase]; + const coverRange = details.ranges['cover']; return calculateRelativePosition(phaseRange, offset, coverRange); } @@ -698,7 +727,17 @@ export class ViewTimeline extends ScrollTimeline { const details = scrollTimelineOptions.get(this); details.subject = options && options.subject ? options.subject : undefined; // TODO: Handle insets. + if (details.subject) { + const resizeObserver = new ResizeObserver(() => { + updateRanges(this); + }); + resizeObserver.observe(details.subject); + const mutationObserver = new MutationObserver(() => { + updateRanges(this); + }); + mutationObserver.observe(details.subject, {attributes: true, attributeFilter: ['class', 'style']}); + } validateSource(this); updateInternal(this); } diff --git a/test/expected.txt b/test/expected.txt index ee21004b..80f694b9 100644 --- a/test/expected.txt +++ b/test/expected.txt @@ -575,7 +575,7 @@ PASS /scroll-animations/scroll-timelines/current-time-root-scroller.html current PASS /scroll-animations/scroll-timelines/current-time-writing-modes.html currentTime handles direction: rtl correctly PASS /scroll-animations/scroll-timelines/current-time-writing-modes.html currentTime handles writing-mode: vertical-rl correctly PASS /scroll-animations/scroll-timelines/current-time-writing-modes.html currentTime handles writing-mode: vertical-lr correctly -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the delay to a positive number +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the delay to a positive number PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the delay to a negative number PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the delay of an animation in progress: positive delay that causes the animation to be no longer in-effect PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the delay of an animation in progress: negative delay that seeks into the active interval @@ -583,14 +583,14 @@ PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Throws when setting invalid delay value: NaN PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Throws when setting invalid delay value: Infinity PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Throws when setting invalid delay value: -Infinity -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the endDelay to a positive number -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the endDelay to a negative number +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the endDelay to a positive number +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the endDelay to a negative number PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Throws when setting the endDelay to infinity PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Throws when setting the endDelay to negative infinity -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the fill to 'none' -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the fill to 'forwards' -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the fill to 'backwards' -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the fill to 'both' +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the fill to 'none' +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the fill to 'forwards' +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the fill to 'backwards' +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the fill to 'both' PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the iterationStart of an animation in progress: backwards-filling PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the iterationStart of an animation in progress: active phase PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the iterationStart of an animation in progress: forwards-filling @@ -598,12 +598,12 @@ PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Throws when se PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Throws when setting invalid iterationStart value: NaN PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Throws when setting invalid iterationStart value: Infinity PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Throws when setting invalid iterationStart value: -Infinity -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting iterations to a double value +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting iterations to a double value PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Throws when setting iterations to Infinity FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the iterations of an animation in progress PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the iterations of an animation in progress with duration "auto" -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the duration to 123.45 -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the duration to auto +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the duration to 123.45 +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the duration to auto PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Throws when setting invalid duration: -1 PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Throws when setting invalid duration: NaN PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Throws when setting invalid duration: Infinity @@ -612,34 +612,34 @@ PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Throws when se PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Throws when setting invalid duration: "100" FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the duration of an animation in progress PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the duration of an animation in progress such that the the start and current time do not change -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the direction to each of the possible keywords +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the direction to each of the possible keywords PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the direction of an animation in progress from 'normal' to 'reverse' PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the direction of an animation in progress from 'normal' to 'reverse' while at start of active interval PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the direction of an animation in progress from 'normal' to 'reverse' while filling backwards PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the direction of an animation in progress from 'normal' to 'alternate' PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the direction of an animation in progress from 'alternate' to 'alternate-reverse' -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a step-start function -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a steps(1, start) function -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a steps(2, start) function +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a step-start function +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a steps(1, start) function +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a steps(2, start) function FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a step-end function FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a steps(1) function FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a steps(1, end) function FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a steps(2, end) function PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a linear function -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a ease function -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a ease-in function -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a ease-in-out function -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a ease-out function -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a easing function which produces values greater than 1 -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a easing function which produces values less than 1 -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Updates the specified value when setting the easing to 'ease' +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a ease function +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a ease-in function +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a ease-in-out function +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a ease-out function +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a easing function which produces values greater than 1 +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing to a easing function which produces values less than 1 +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Updates the specified value when setting the easing to 'ease' PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Updates the specified value when setting the easing to 'linear' -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Updates the specified value when setting the easing to 'ease-in' -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Updates the specified value when setting the easing to 'ease-out' -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Updates the specified value when setting the easing to 'ease-in-out' -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Updates the specified value when setting the easing to 'cubic-bezier(0.1, 5, 0.23, 0)' -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Updates the specified value when setting the easing to 'steps(3, start)' -FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Updates the specified value when setting the easing to 'steps(3)' +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Updates the specified value when setting the easing to 'ease-in' +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Updates the specified value when setting the easing to 'ease-out' +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Updates the specified value when setting the easing to 'ease-in-out' +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Updates the specified value when setting the easing to 'cubic-bezier(0.1, 5, 0.23, 0)' +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Updates the specified value when setting the easing to 'steps(3, start)' +PASS /scroll-animations/scroll-timelines/effect-updateTiming.html Updates the specified value when setting the easing to 'steps(3)' FAIL /scroll-animations/scroll-timelines/effect-updateTiming.html Allows setting the easing of an animation in progress PASS /scroll-animations/scroll-timelines/finish-animation.html Finishing an animation with a zero playback rate throws PASS /scroll-animations/scroll-timelines/finish-animation.html Finishing an animation seeks to the end time @@ -702,7 +702,7 @@ PASS /scroll-animations/scroll-timelines/pause-animation.html A pending ready pr FAIL /scroll-animations/scroll-timelines/pause-animation.html A pause-pending animation maintains the current time when applying a pending playback rate PASS /scroll-animations/scroll-timelines/pause-animation.html The animation's current time remains fixed after pausing PASS /scroll-animations/scroll-timelines/pause-animation.html Pausing a canceled animation sets the current time -FAIL /scroll-animations/scroll-timelines/pause-animation.html Pause pending task doesn't run when the timeline is inactive. +PASS /scroll-animations/scroll-timelines/pause-animation.html Pause pending task doesn't run when the timeline is inactive. PASS /scroll-animations/scroll-timelines/pause-animation.html Animation start and current times are correct if scroll timeline is activated after animation.pause call. FAIL /scroll-animations/scroll-timelines/play-animation.html Playing an animations aligns the start time with the start of the active range PASS /scroll-animations/scroll-timelines/play-animation.html Playing an animations with a negative playback rate aligns the start time with the end of the active range @@ -712,7 +712,7 @@ FAIL /scroll-animations/scroll-timelines/play-animation.html Playing a running a PASS /scroll-animations/scroll-timelines/play-animation.html Playing a finished animation restarts the animation aligned at the start FAIL /scroll-animations/scroll-timelines/play-animation.html Playing a finished and reversed animation restarts the animation aligned at the end PASS /scroll-animations/scroll-timelines/play-animation.html Playing a pause-pending but previously finished animation realigns with the scroll position -FAIL /scroll-animations/scroll-timelines/play-animation.html Playing a finished animation clears the start time +PASS /scroll-animations/scroll-timelines/play-animation.html Playing a finished animation clears the start time PASS /scroll-animations/scroll-timelines/play-animation.html The ready promise should be replaced if the animation is not already pending PASS /scroll-animations/scroll-timelines/play-animation.html A pending ready promise should be resolved and not replaced when the animation enters the running state FAIL /scroll-animations/scroll-timelines/play-animation.html Resuming an animation from paused realigns with scroll position. @@ -787,7 +787,7 @@ PASS /scroll-animations/scroll-timelines/scroll-animation-effect-phases.tentativ PASS /scroll-animations/scroll-timelines/scroll-animation-effect-phases.tentative.html Make scroller inactive, then set current time to an in range time PASS /scroll-animations/scroll-timelines/scroll-animation-effect-phases.tentative.html Animation effect is still applied after pausing and making timeline inactive. PASS /scroll-animations/scroll-timelines/scroll-animation-effect-phases.tentative.html Make timeline inactive, force style update then pause the animation. No crashing indicates test success. -FAIL /scroll-animations/scroll-timelines/scroll-animation-inactive-timeline.html Play pending task doesn't run when the timeline is inactive. +PASS /scroll-animations/scroll-timelines/scroll-animation-inactive-timeline.html Play pending task doesn't run when the timeline is inactive. PASS /scroll-animations/scroll-timelines/scroll-animation-inactive-timeline.html Animation start and current times are correct if scroll timeline is activated after animation.play call. FAIL /scroll-animations/scroll-timelines/scroll-animation-inactive-timeline.html Animation start and current times are correct if scroll timeline is activated after setting start time. FAIL /scroll-animations/scroll-timelines/scroll-animation-inactive-timeline.html Animation current time is correct when the timeline becomes newly inactive and then active again. @@ -849,7 +849,7 @@ FAIL /scroll-animations/scroll-timelines/setting-start-time.html Setting the sta PASS /scroll-animations/scroll-timelines/setting-timeline.tentative.html Setting a scroll timeline on a play-pending animation synchronizes currentTime of the animation with the scroll position. PASS /scroll-animations/scroll-timelines/setting-timeline.tentative.html Setting a scroll timeline on a pause-pending animation fixes the currentTime of the animation based on the scroll position once resumed PASS /scroll-animations/scroll-timelines/setting-timeline.tentative.html Setting a scroll timeline on a reversed play-pending animation synchronizes the currentTime of the animation with the scroll position. -FAIL /scroll-animations/scroll-timelines/setting-timeline.tentative.html Setting a scroll timeline on a running animation synchronizes the currentTime of the animation with the scroll position. +PASS /scroll-animations/scroll-timelines/setting-timeline.tentative.html Setting a scroll timeline on a running animation synchronizes the currentTime of the animation with the scroll position. PASS /scroll-animations/scroll-timelines/setting-timeline.tentative.html Setting a scroll timeline on a paused animation fixes the currentTime of the animation based on the scroll position when resumed PASS /scroll-animations/scroll-timelines/setting-timeline.tentative.html Setting a scroll timeline on a reversed paused animation fixes the currentTime of the animation based on the scroll position when resumed PASS /scroll-animations/scroll-timelines/setting-timeline.tentative.html Transitioning from a scroll timeline to a document timeline on a running animation preserves currentTime @@ -918,8 +918,8 @@ FAIL /scroll-animations/view-timelines/inline-view-timeline-current-time.tentati FAIL /scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-1.html View timeline top-sticky during entry. FAIL /scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-2.html View timeline bottom-sticky during entry and top-sticky during exit. FAIL /scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-3.html View timeline top-sticky and bottom-sticky during entry. -FAIL /scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-4.html View timeline top-sticky before entry. -FAIL /scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-5.html View timeline bottom-sticky before entry and top-sticky after exit. +PASS /scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-4.html View timeline top-sticky before entry. +PASS /scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-5.html View timeline bottom-sticky before entry and top-sticky after exit. FAIL /scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-6.html View timeline target > viewport, bottom-sticky during entry and top-sticky during exit. FAIL /scroll-animations/view-timelines/sticky/view-timeline-sticky-offscreen-7.html View timeline target > viewport, bottom-sticky and top-sticky during contain. FAIL /scroll-animations/view-timelines/svg-graphics-element-001.html View timeline attached to SVG graphics element @@ -939,14 +939,14 @@ FAIL /scroll-animations/view-timelines/view-timeline-inset.html view timeline wi FAIL /scroll-animations/view-timelines/view-timeline-inset.html view timeline with font relative inset. FAIL /scroll-animations/view-timelines/view-timeline-inset.html view timeline with viewport relative insets. FAIL /scroll-animations/view-timelines/view-timeline-inset.html view timeline inset as string -FAIL /scroll-animations/view-timelines/view-timeline-inset.html view timeline with invalid inset +PASS /scroll-animations/view-timelines/view-timeline-inset.html view timeline with invalid inset PASS /scroll-animations/view-timelines/view-timeline-missing-subject.html ViewTimeline with missing subject PASS /scroll-animations/view-timelines/view-timeline-on-display-none-element.html element with display: none should have inactive viewtimeline -FAIL /scroll-animations/view-timelines/view-timeline-range-large-subject.html View timeline with range set via delays. +PASS /scroll-animations/view-timelines/view-timeline-range-large-subject.html View timeline with range set via delays. FAIL /scroll-animations/view-timelines/view-timeline-range.html View timeline with range as pair. -FAIL /scroll-animations/view-timelines/view-timeline-range.html View timeline with range and inferred name or offset. -FAIL /scroll-animations/view-timelines/view-timeline-range.html View timeline with range as pair. -FAIL /scroll-animations/view-timelines/view-timeline-range.html View timeline with range as pair. +PASS /scroll-animations/view-timelines/view-timeline-range.html View timeline with range and inferred name or offset. +PASS /scroll-animations/view-timelines/view-timeline-range.html View timeline with range as pair. +PASS /scroll-animations/view-timelines/view-timeline-range.html View timeline with range as pair. FAIL /scroll-animations/view-timelines/view-timeline-range.html View timeline with range as strings. PASS /scroll-animations/view-timelines/view-timeline-root-source.html Test view-timeline with document scrolling element. PASS /scroll-animations/view-timelines/view-timeline-snapport.html Default ViewTimeline is not affected by scroll-padding @@ -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 356 of 959 tests. +Passed 394 of 959 tests.