| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319 |
- import { supportsLinearEasing, startWaapiAnimation, attachTimeline, isGenerator, isWaapiSupportedEasing } from 'motion-dom';
- import { millisecondsToSeconds, secondsToMilliseconds, noop } from 'motion-utils';
- import { anticipate } from '../../easing/anticipate.mjs';
- import { backInOut } from '../../easing/back.mjs';
- import { circInOut } from '../../easing/circ.mjs';
- import { DOMKeyframesResolver } from '../../render/dom/DOMKeyframesResolver.mjs';
- import { BaseAnimation } from './BaseAnimation.mjs';
- import { MainThreadAnimation } from './MainThreadAnimation.mjs';
- import { acceleratedValues } from './utils/accelerated-values.mjs';
- import { getFinalKeyframe } from './waapi/utils/get-final-keyframe.mjs';
- import { supportsWaapi } from './waapi/utils/supports-waapi.mjs';
- /**
- * 10ms is chosen here as it strikes a balance between smooth
- * results (more than one keyframe per frame at 60fps) and
- * keyframe quantity.
- */
- const sampleDelta = 10; //ms
- /**
- * Implement a practical max duration for keyframe generation
- * to prevent infinite loops
- */
- const maxDuration = 20000;
- /**
- * Check if an animation can run natively via WAAPI or requires pregenerated keyframes.
- * WAAPI doesn't support spring or function easings so we run these as JS animation before
- * handing off.
- */
- function requiresPregeneratedKeyframes(options) {
- return (isGenerator(options.type) ||
- options.type === "spring" ||
- !isWaapiSupportedEasing(options.ease));
- }
- function pregenerateKeyframes(keyframes, options) {
- /**
- * Create a main-thread animation to pregenerate keyframes.
- * We sample this at regular intervals to generate keyframes that we then
- * linearly interpolate between.
- */
- const sampleAnimation = new MainThreadAnimation({
- ...options,
- keyframes,
- repeat: 0,
- delay: 0,
- isGenerator: true,
- });
- let state = { done: false, value: keyframes[0] };
- const pregeneratedKeyframes = [];
- /**
- * Bail after 20 seconds of pre-generated keyframes as it's likely
- * we're heading for an infinite loop.
- */
- let t = 0;
- while (!state.done && t < maxDuration) {
- state = sampleAnimation.sample(t);
- pregeneratedKeyframes.push(state.value);
- t += sampleDelta;
- }
- return {
- times: undefined,
- keyframes: pregeneratedKeyframes,
- duration: t - sampleDelta,
- ease: "linear",
- };
- }
- const unsupportedEasingFunctions = {
- anticipate,
- backInOut,
- circInOut,
- };
- function isUnsupportedEase(key) {
- return key in unsupportedEasingFunctions;
- }
- class AcceleratedAnimation extends BaseAnimation {
- constructor(options) {
- super(options);
- const { name, motionValue, element, keyframes } = this.options;
- this.resolver = new DOMKeyframesResolver(keyframes, (resolvedKeyframes, finalKeyframe) => this.onKeyframesResolved(resolvedKeyframes, finalKeyframe), name, motionValue, element);
- this.resolver.scheduleResolve();
- }
- initPlayback(keyframes, finalKeyframe) {
- let { duration = 300, times, ease, type, motionValue, name, startTime, } = this.options;
- /**
- * If element has since been unmounted, return false to indicate
- * the animation failed to initialised.
- */
- if (!motionValue.owner || !motionValue.owner.current) {
- return false;
- }
- /**
- * If the user has provided an easing function name that isn't supported
- * by WAAPI (like "anticipate"), we need to provide the corressponding
- * function. This will later get converted to a linear() easing function.
- */
- if (typeof ease === "string" &&
- supportsLinearEasing() &&
- isUnsupportedEase(ease)) {
- ease = unsupportedEasingFunctions[ease];
- }
- /**
- * If this animation needs pre-generated keyframes then generate.
- */
- if (requiresPregeneratedKeyframes(this.options)) {
- const { onComplete, onUpdate, motionValue, element, ...options } = this.options;
- const pregeneratedAnimation = pregenerateKeyframes(keyframes, options);
- keyframes = pregeneratedAnimation.keyframes;
- // If this is a very short animation, ensure we have
- // at least two keyframes to animate between as older browsers
- // can't animate between a single keyframe.
- if (keyframes.length === 1) {
- keyframes[1] = keyframes[0];
- }
- duration = pregeneratedAnimation.duration;
- times = pregeneratedAnimation.times;
- ease = pregeneratedAnimation.ease;
- type = "keyframes";
- }
- const animation = startWaapiAnimation(motionValue.owner.current, name, keyframes, { ...this.options, duration, times, ease });
- // Override the browser calculated startTime with one synchronised to other JS
- // and WAAPI animations starting this event loop.
- animation.startTime = startTime ?? this.calcStartTime();
- if (this.pendingTimeline) {
- attachTimeline(animation, this.pendingTimeline);
- this.pendingTimeline = undefined;
- }
- else {
- /**
- * Prefer the `onfinish` prop as it's more widely supported than
- * the `finished` promise.
- *
- * Here, we synchronously set the provided MotionValue to the end
- * keyframe. If we didn't, when the WAAPI animation is finished it would
- * be removed from the element which would then revert to its old styles.
- */
- animation.onfinish = () => {
- const { onComplete } = this.options;
- motionValue.set(getFinalKeyframe(keyframes, this.options, finalKeyframe));
- onComplete && onComplete();
- this.cancel();
- this.resolveFinishedPromise();
- };
- }
- return {
- animation,
- duration,
- times,
- type,
- ease,
- keyframes: keyframes,
- };
- }
- get duration() {
- const { resolved } = this;
- if (!resolved)
- return 0;
- const { duration } = resolved;
- return millisecondsToSeconds(duration);
- }
- get time() {
- const { resolved } = this;
- if (!resolved)
- return 0;
- const { animation } = resolved;
- return millisecondsToSeconds(animation.currentTime || 0);
- }
- set time(newTime) {
- const { resolved } = this;
- if (!resolved)
- return;
- const { animation } = resolved;
- animation.currentTime = secondsToMilliseconds(newTime);
- }
- get speed() {
- const { resolved } = this;
- if (!resolved)
- return 1;
- const { animation } = resolved;
- return animation.playbackRate;
- }
- get finished() {
- return this.resolved.animation.finished;
- }
- set speed(newSpeed) {
- const { resolved } = this;
- if (!resolved)
- return;
- const { animation } = resolved;
- animation.playbackRate = newSpeed;
- }
- get state() {
- const { resolved } = this;
- if (!resolved)
- return "idle";
- const { animation } = resolved;
- return animation.playState;
- }
- get startTime() {
- const { resolved } = this;
- if (!resolved)
- return null;
- const { animation } = resolved;
- // Coerce to number as TypeScript incorrectly types this
- // as CSSNumberish
- return animation.startTime;
- }
- /**
- * Replace the default DocumentTimeline with another AnimationTimeline.
- * Currently used for scroll animations.
- */
- attachTimeline(timeline) {
- if (!this._resolved) {
- this.pendingTimeline = timeline;
- }
- else {
- const { resolved } = this;
- if (!resolved)
- return noop;
- const { animation } = resolved;
- attachTimeline(animation, timeline);
- }
- return noop;
- }
- play() {
- if (this.isStopped)
- return;
- const { resolved } = this;
- if (!resolved)
- return;
- const { animation } = resolved;
- if (animation.playState === "finished") {
- this.updateFinishedPromise();
- }
- animation.play();
- }
- pause() {
- const { resolved } = this;
- if (!resolved)
- return;
- const { animation } = resolved;
- animation.pause();
- }
- stop() {
- this.resolver.cancel();
- this.isStopped = true;
- if (this.state === "idle")
- return;
- this.resolveFinishedPromise();
- this.updateFinishedPromise();
- const { resolved } = this;
- if (!resolved)
- return;
- const { animation, keyframes, duration, type, ease, times } = resolved;
- if (animation.playState === "idle" ||
- animation.playState === "finished") {
- return;
- }
- /**
- * WAAPI doesn't natively have any interruption capabilities.
- *
- * Rather than read commited styles back out of the DOM, we can
- * create a renderless JS animation and sample it twice to calculate
- * its current value, "previous" value, and therefore allow
- * Motion to calculate velocity for any subsequent animation.
- */
- if (this.time) {
- const { motionValue, onUpdate, onComplete, element, ...options } = this.options;
- const sampleAnimation = new MainThreadAnimation({
- ...options,
- keyframes,
- duration,
- type,
- ease,
- times,
- isGenerator: true,
- });
- const sampleTime = secondsToMilliseconds(this.time);
- motionValue.setWithVelocity(sampleAnimation.sample(sampleTime - sampleDelta).value, sampleAnimation.sample(sampleTime).value, sampleDelta);
- }
- const { onStop } = this.options;
- onStop && onStop();
- this.cancel();
- }
- complete() {
- const { resolved } = this;
- if (!resolved)
- return;
- resolved.animation.finish();
- }
- cancel() {
- const { resolved } = this;
- if (!resolved)
- return;
- resolved.animation.cancel();
- }
- static supports(options) {
- const { motionValue, name, repeatDelay, repeatType, damping, type } = options;
- if (!motionValue ||
- !motionValue.owner ||
- !(motionValue.owner.current instanceof HTMLElement)) {
- return false;
- }
- const { onUpdate, transformTemplate } = motionValue.owner.getProps();
- return (supportsWaapi() &&
- name &&
- acceleratedValues.has(name) &&
- (name !== "transform" || !transformTemplate) &&
- /**
- * If we're outputting values to onUpdate then we can't use WAAPI as there's
- * no way to read the value from WAAPI every frame.
- */
- !onUpdate &&
- !repeatDelay &&
- repeatType !== "mirror" &&
- damping !== 0 &&
- type !== "inertia");
- }
- }
- export { AcceleratedAnimation };
|