import { invariant, millisecondsToSeconds, secondsToMilliseconds, noop } from 'motion-utils'; import { setStyle } from '../render/dom/style-set.mjs'; import { supportsScrollTimeline } from '../utils/supports/scroll-timeline.mjs'; import { getFinalKeyframe } from './keyframes/get-final.mjs'; import { WithPromise } from './utils/WithPromise.mjs'; import { startWaapiAnimation } from './waapi/start-waapi-animation.mjs'; import { applyGeneratorOptions } from './waapi/utils/apply-generator.mjs'; /** * NativeAnimation implements AnimationPlaybackControls for the browser's Web Animations API. */ class NativeAnimation extends WithPromise { constructor(options) { super(); this.finishedTime = null; this.isStopped = false; if (!options) return; const { element, name, keyframes, pseudoElement, allowFlatten = false, finalKeyframe, onComplete, } = options; this.isPseudoElement = Boolean(pseudoElement); this.allowFlatten = allowFlatten; this.options = options; invariant(typeof options.type !== "string", `Mini animate() doesn't support "type" as a string.`, "mini-spring"); const transition = applyGeneratorOptions(options); this.animation = startWaapiAnimation(element, name, keyframes, transition, pseudoElement); if (transition.autoplay === false) { this.animation.pause(); } this.animation.onfinish = () => { this.finishedTime = this.time; if (!pseudoElement) { const keyframe = getFinalKeyframe(keyframes, this.options, finalKeyframe, this.speed); if (this.updateMotionValue) { this.updateMotionValue(keyframe); } else { /** * If we can, we want to commit the final style as set by the user, * rather than the computed keyframe value supplied by the animation. */ setStyle(element, name, keyframe); } this.animation.cancel(); } onComplete?.(); this.notifyFinished(); }; } play() { if (this.isStopped) return; this.animation.play(); if (this.state === "finished") { this.updateFinished(); } } pause() { this.animation.pause(); } complete() { this.animation.finish?.(); } cancel() { try { this.animation.cancel(); } catch (e) { } } stop() { if (this.isStopped) return; this.isStopped = true; const { state } = this; if (state === "idle" || state === "finished") { return; } if (this.updateMotionValue) { this.updateMotionValue(); } else { this.commitStyles(); } if (!this.isPseudoElement) this.cancel(); } /** * WAAPI doesn't natively have any interruption capabilities. * * In this method, we commit styles back to the DOM before cancelling * the animation. * * This is designed to be overridden by NativeAnimationExtended, which * will create a renderless JS animation and sample it twice to calculate * its current value, "previous" value, and therefore allow * Motion to also correctly calculate velocity for any subsequent animation * while deferring the commit until the next animation frame. */ commitStyles() { if (!this.isPseudoElement) { this.animation.commitStyles?.(); } } get duration() { const duration = this.animation.effect?.getComputedTiming?.().duration || 0; return millisecondsToSeconds(Number(duration)); } get iterationDuration() { const { delay = 0 } = this.options || {}; return this.duration + millisecondsToSeconds(delay); } get time() { return millisecondsToSeconds(Number(this.animation.currentTime) || 0); } set time(newTime) { this.finishedTime = null; this.animation.currentTime = secondsToMilliseconds(newTime); } /** * The playback speed of the animation. * 1 = normal speed, 2 = double speed, 0.5 = half speed. */ get speed() { return this.animation.playbackRate; } set speed(newSpeed) { // Allow backwards playback after finishing if (newSpeed < 0) this.finishedTime = null; this.animation.playbackRate = newSpeed; } get state() { return this.finishedTime !== null ? "finished" : this.animation.playState; } get startTime() { return Number(this.animation.startTime); } set startTime(newStartTime) { this.animation.startTime = newStartTime; } /** * Attaches a timeline to the animation, for instance the `ScrollTimeline`. */ attachTimeline({ timeline, observe }) { if (this.allowFlatten) { this.animation.effect?.updateTiming({ easing: "linear" }); } this.animation.onfinish = null; if (timeline && supportsScrollTimeline()) { this.animation.timeline = timeline; return noop; } else { return observe(this); } } } export { NativeAnimation };