179 lines
6.2 KiB
JavaScript
179 lines
6.2 KiB
JavaScript
import { MotionGlobalConfig, noop } from 'motion-utils';
|
|
import { time } from '../frameloop/sync-time.mjs';
|
|
import { JSAnimation } from './JSAnimation.mjs';
|
|
import { getFinalKeyframe } from './keyframes/get-final.mjs';
|
|
import { KeyframeResolver, flushKeyframeResolvers } from './keyframes/KeyframesResolver.mjs';
|
|
import { NativeAnimationExtended } from './NativeAnimationExtended.mjs';
|
|
import { canAnimate } from './utils/can-animate.mjs';
|
|
import { makeAnimationInstant } from './utils/make-animation-instant.mjs';
|
|
import { WithPromise } from './utils/WithPromise.mjs';
|
|
import { supportsBrowserAnimation } from './waapi/supports/waapi.mjs';
|
|
|
|
/**
|
|
* Maximum time allowed between an animation being created and it being
|
|
* resolved for us to use the latter as the start time.
|
|
*
|
|
* This is to ensure that while we prefer to "start" an animation as soon
|
|
* as it's triggered, we also want to avoid a visual jump if there's a big delay
|
|
* between these two moments.
|
|
*/
|
|
const MAX_RESOLVE_DELAY = 40;
|
|
class AsyncMotionValueAnimation extends WithPromise {
|
|
constructor({ autoplay = true, delay = 0, type = "keyframes", repeat = 0, repeatDelay = 0, repeatType = "loop", keyframes, name, motionValue, element, ...options }) {
|
|
super();
|
|
/**
|
|
* Bound to support return animation.stop pattern
|
|
*/
|
|
this.stop = () => {
|
|
if (this._animation) {
|
|
this._animation.stop();
|
|
this.stopTimeline?.();
|
|
}
|
|
this.keyframeResolver?.cancel();
|
|
};
|
|
this.createdAt = time.now();
|
|
const optionsWithDefaults = {
|
|
autoplay,
|
|
delay,
|
|
type,
|
|
repeat,
|
|
repeatDelay,
|
|
repeatType,
|
|
name,
|
|
motionValue,
|
|
element,
|
|
...options,
|
|
};
|
|
const KeyframeResolver$1 = element?.KeyframeResolver || KeyframeResolver;
|
|
this.keyframeResolver = new KeyframeResolver$1(keyframes, (resolvedKeyframes, finalKeyframe, forced) => this.onKeyframesResolved(resolvedKeyframes, finalKeyframe, optionsWithDefaults, !forced), name, motionValue, element);
|
|
this.keyframeResolver?.scheduleResolve();
|
|
}
|
|
onKeyframesResolved(keyframes, finalKeyframe, options, sync) {
|
|
this.keyframeResolver = undefined;
|
|
const { name, type, velocity, delay, isHandoff, onUpdate } = options;
|
|
this.resolvedAt = time.now();
|
|
/**
|
|
* If we can't animate this value with the resolved keyframes
|
|
* then we should complete it immediately.
|
|
*/
|
|
if (!canAnimate(keyframes, name, type, velocity)) {
|
|
if (MotionGlobalConfig.instantAnimations || !delay) {
|
|
onUpdate?.(getFinalKeyframe(keyframes, options, finalKeyframe));
|
|
}
|
|
keyframes[0] = keyframes[keyframes.length - 1];
|
|
makeAnimationInstant(options);
|
|
options.repeat = 0;
|
|
}
|
|
/**
|
|
* Resolve startTime for the animation.
|
|
*
|
|
* This method uses the createdAt and resolvedAt to calculate the
|
|
* animation startTime. *Ideally*, we would use the createdAt time as t=0
|
|
* as the following frame would then be the first frame of the animation in
|
|
* progress, which would feel snappier.
|
|
*
|
|
* However, if there's a delay (main thread work) between the creation of
|
|
* the animation and the first commited frame, we prefer to use resolvedAt
|
|
* to avoid a sudden jump into the animation.
|
|
*/
|
|
const startTime = sync
|
|
? !this.resolvedAt
|
|
? this.createdAt
|
|
: this.resolvedAt - this.createdAt > MAX_RESOLVE_DELAY
|
|
? this.resolvedAt
|
|
: this.createdAt
|
|
: undefined;
|
|
const resolvedOptions = {
|
|
startTime,
|
|
finalKeyframe,
|
|
...options,
|
|
keyframes,
|
|
};
|
|
/**
|
|
* Animate via WAAPI if possible. If this is a handoff animation, the optimised animation will be running via
|
|
* WAAPI. Therefore, this animation must be JS to ensure it runs "under" the
|
|
* optimised animation.
|
|
*/
|
|
const animation = !isHandoff && supportsBrowserAnimation(resolvedOptions)
|
|
? new NativeAnimationExtended({
|
|
...resolvedOptions,
|
|
element: resolvedOptions.motionValue.owner.current,
|
|
})
|
|
: new JSAnimation(resolvedOptions);
|
|
animation.finished.then(() => this.notifyFinished()).catch(noop);
|
|
if (this.pendingTimeline) {
|
|
this.stopTimeline = animation.attachTimeline(this.pendingTimeline);
|
|
this.pendingTimeline = undefined;
|
|
}
|
|
this._animation = animation;
|
|
}
|
|
get finished() {
|
|
if (!this._animation) {
|
|
return this._finished;
|
|
}
|
|
else {
|
|
return this.animation.finished;
|
|
}
|
|
}
|
|
then(onResolve, _onReject) {
|
|
return this.finished.finally(onResolve).then(() => { });
|
|
}
|
|
get animation() {
|
|
if (!this._animation) {
|
|
this.keyframeResolver?.resume();
|
|
flushKeyframeResolvers();
|
|
}
|
|
return this._animation;
|
|
}
|
|
get duration() {
|
|
return this.animation.duration;
|
|
}
|
|
get iterationDuration() {
|
|
return this.animation.iterationDuration;
|
|
}
|
|
get time() {
|
|
return this.animation.time;
|
|
}
|
|
set time(newTime) {
|
|
this.animation.time = newTime;
|
|
}
|
|
get speed() {
|
|
return this.animation.speed;
|
|
}
|
|
get state() {
|
|
return this.animation.state;
|
|
}
|
|
set speed(newSpeed) {
|
|
this.animation.speed = newSpeed;
|
|
}
|
|
get startTime() {
|
|
return this.animation.startTime;
|
|
}
|
|
attachTimeline(timeline) {
|
|
if (this._animation) {
|
|
this.stopTimeline = this.animation.attachTimeline(timeline);
|
|
}
|
|
else {
|
|
this.pendingTimeline = timeline;
|
|
}
|
|
return () => this.stop();
|
|
}
|
|
play() {
|
|
this.animation.play();
|
|
}
|
|
pause() {
|
|
this.animation.pause();
|
|
}
|
|
complete() {
|
|
this.animation.complete();
|
|
}
|
|
cancel() {
|
|
if (this._animation) {
|
|
this.animation.cancel();
|
|
}
|
|
this.keyframeResolver?.cancel();
|
|
}
|
|
}
|
|
|
|
export { AsyncMotionValueAnimation };
|