motion-dom
Version:
204 lines (201 loc) • 8.01 kB
JavaScript
import { MotionGlobalConfig } 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 { resolveStartTime } from './utils/resolve-start-time.mjs';
import { WithPromise } from './utils/WithPromise.mjs';
import { supportsBrowserAnimation } from './waapi/supports/waapi.mjs';
import { canGroupTransform, TransformAnimation } from './waapi/transforms/TransformAnimation.mjs';
class AsyncMotionValueAnimation extends WithPromise {
constructor(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 { keyframes, name, motionValue, element } = options;
/**
* animateMotionValue builds a fresh options object per value, so
* it's completed in place as keyframes resolve rather than copied.
*/
const optionsWithDefaults = options;
optionsWithDefaults.autoplay ?? (optionsWithDefaults.autoplay = true);
optionsWithDefaults.delay ?? (optionsWithDefaults.delay = 0);
optionsWithDefaults.type ?? (optionsWithDefaults.type = "keyframes");
optionsWithDefaults.repeat ?? (optionsWithDefaults.repeat = 0);
optionsWithDefaults.repeatDelay ?? (optionsWithDefaults.repeatDelay = 0);
optionsWithDefaults.repeatType ?? (optionsWithDefaults.repeatType = "loop");
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.
*/
const canAnimateValue = canAnimate(keyframes, name, type, velocity);
if (!canAnimateValue) {
if (MotionGlobalConfig.instantAnimations || !delay) {
onUpdate?.(getFinalKeyframe(keyframes, options, finalKeyframe));
}
keyframes[0] = keyframes[keyframes.length - 1];
makeAnimationInstant(options);
options.repeat = 0;
}
const { onComplete } = options;
options.finalKeyframe = finalKeyframe;
options.keyframes = keyframes;
/**
* JSAnimation and NativeAnimation call onComplete exactly when
* their own `finished` resolves, so this replaces a promise chain
* per value with a callback.
*/
options.onComplete = () => {
onComplete?.();
this.notifyFinished();
};
/**
* 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.
*
* Also skip WAAPI when keyframes aren't animatable, as the resolved
* values may not be valid CSS and would trigger browser warnings.
*/
const useWaapi = canAnimateValue && !isHandoff && supportsBrowserAnimation(options);
/**
* Independent transforms (x, scale etc) on an HTML element are
* composed into one WAAPI transform animation per element.
*/
const isGroupedTransform = !useWaapi && canGroupTransform(options);
/**
* Resolve startTime for the animation. A startTime passed in options
* (an optimised appear handoff syncing to its WAAPI animation) takes
* precedence.
*
* *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. If keyframes resolved on a later frame,
* and long after creation, we start from then instead.
*
* If they resolved immediately, a JSAnimation picks its own start
* time and resolves it the same way on its first frame, so every
* animation started in that moment stays in sync. WAAPI has no frame
* of ours to do that on.
*/
if (sync &&
(useWaapi ||
isGroupedTransform ||
this.resolvedAt !== this.createdAt)) {
options.startTime ?? (options.startTime = resolveStartTime(this.createdAt, this.resolvedAt));
}
let animation;
if (useWaapi) {
/**
* The resolver needed the VisualElement, WAAPI needs the DOM
* element. JSAnimation reads neither, so this is safe to
* leave in place if we fall back to it.
*/
options.element = options.motionValue?.owner?.current;
try {
animation = new NativeAnimationExtended(options);
}
catch {
animation = new JSAnimation(options);
}
}
else if (isGroupedTransform) {
animation = new TransformAnimation(options);
}
else {
animation = new JSAnimation(options);
}
if (this.pendingTimeline) {
this.stopTimeline = animation.attachTimeline(this.pendingTimeline);
this.pendingTimeline = undefined;
}
this._animation = animation;
}
get finished() {
return this._animation ? this._animation.finished : super.finished;
}
then(onResolve, _onReject) {
return this.finished.finally(onResolve).then(() => { });
}
/**
* The value now, if the animation runs on the compositor.
*/
liveValue() {
return this._animation?.liveValue?.();
}
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() {
this._animation?.cancel();
this.keyframeResolver?.cancel();
}
}
export { AsyncMotionValueAnimation };
//# sourceMappingURL=AsyncMotionValueAnimation.mjs.map