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 };