motion-dom
Version:
112 lines (109 loc) • 4.01 kB
JavaScript
import { FollowAnimation } from '../../animation/FollowAnimation.mjs';
import { asNumber } from './as-number.mjs';
import { isMotionValue } from './is-motion-value.mjs';
/**
* Animate `value` to its latest value with the generator in
* `options.type`, tracking `source` if it's a `MotionValue`. Takes a
* generator rather than a type name, so spring-only callers don't
* bundle every generator.
*/
function follow(value, source, options) {
const initialValue = value.get();
let activeAnimation = null;
let set;
const unit = typeof initialValue === "string"
? initialValue.replace(/[\d.-]/g, "")
: undefined;
const onUpdate = (v) => set((unit ? v + unit : v));
const onPlay = () => value["events"].animationStart?.notify();
const stopAnimation = () => {
if (activeAnimation) {
activeAnimation.stop();
activeAnimation = null;
}
value.animation = undefined;
};
value.attach((v, safeSet) => {
set = safeSet;
const target = asNumber(v);
if (activeAnimation?.state === "running") {
/**
* Steer the running animation rather than replacing it. This
* keeps its completion promise and uses its analytical velocity
* for accuracy, preventing systematic velocity loss at high
* frame rates (240hz+).
*/
activeAnimation.setTarget(target, options.velocity);
return;
}
const current = asNumber(value.get());
const velocity = activeAnimation
? activeAnimation.getGeneratorVelocity()
: value.getVelocity();
stopAnimation();
// Don't animate if we're already at the target
if (current === target)
return;
const animationOptions = {
keyframes: [current, target],
velocity,
restDelta: 0.001,
restSpeed: 0.01,
...options,
onUpdate,
};
const animation = (activeAnimation = new FollowAnimation({
...animationOptions,
onPlay,
}));
value.animation = animation;
animation.then(() => {
// Ignore if this animation has since been replaced
if (activeAnimation !== animation)
return;
activeAnimation = null;
value.animation = undefined;
value["events"].animationComplete?.notify();
});
}, stopAnimation);
if (isMotionValue(source)) {
let skipNextAnimation = options.skipInitialAnimation === true;
const onSourceChange = (v) => {
if (skipNextAnimation) {
skipNextAnimation = false;
value.jump(parseValue(v, unit), false);
}
else {
value.set(parseValue(v, unit));
}
};
const removeSourceOnChange = source.on("change", onSourceChange);
/**
* Catch up on a change made before we subscribed, for instance
* between a React render reading the source and the effect
* attaching to it. Subtracting is falsy for equal or non-numeric
* (NaN) values.
*/
const latest = source.get();
if (asNumber(latest) - asNumber(value.get()))
onSourceChange(latest);
const removeValueOnDestroy = value.on("destroy", removeSourceOnChange);
/**
* Stop our animation on detach, otherwise a re-attach (new
* options, Strict Mode) starts a second animation that fights
* this orphaned one. The catch-up above resumes from the
* current value and velocity.
*/
return () => {
removeSourceOnChange();
removeValueOnDestroy();
stopAnimation();
};
}
return stopAnimation;
}
function parseValue(v, unit) {
return unit ? v + unit : v;
}
export { follow };
//# sourceMappingURL=follow.mjs.map