first commit
This commit is contained in:
349
node_modules/motion-dom/dist/es/animation/JSAnimation.mjs
generated
vendored
Normal file
349
node_modules/motion-dom/dist/es/animation/JSAnimation.mjs
generated
vendored
Normal file
@@ -0,0 +1,349 @@
|
||||
import { invariant, pipe, clamp, millisecondsToSeconds, secondsToMilliseconds } from 'motion-utils';
|
||||
import { time } from '../frameloop/sync-time.mjs';
|
||||
import { activeAnimations } from '../stats/animation-count.mjs';
|
||||
import { mix } from '../utils/mix/index.mjs';
|
||||
import { frameloopDriver } from './drivers/frame.mjs';
|
||||
import { inertia } from './generators/inertia.mjs';
|
||||
import { keyframes } from './generators/keyframes.mjs';
|
||||
import { calcGeneratorDuration } from './generators/utils/calc-duration.mjs';
|
||||
import { getFinalKeyframe } from './keyframes/get-final.mjs';
|
||||
import { replaceTransitionType } from './utils/replace-transition-type.mjs';
|
||||
import { WithPromise } from './utils/WithPromise.mjs';
|
||||
|
||||
const percentToProgress = (percent) => percent / 100;
|
||||
class JSAnimation extends WithPromise {
|
||||
constructor(options) {
|
||||
super();
|
||||
this.state = "idle";
|
||||
this.startTime = null;
|
||||
this.isStopped = false;
|
||||
/**
|
||||
* The current time of the animation.
|
||||
*/
|
||||
this.currentTime = 0;
|
||||
/**
|
||||
* The time at which the animation was paused.
|
||||
*/
|
||||
this.holdTime = null;
|
||||
/**
|
||||
* Playback speed as a factor. 0 would be stopped, -1 reverse and 2 double speed.
|
||||
*/
|
||||
this.playbackSpeed = 1;
|
||||
/**
|
||||
* This method is bound to the instance to fix a pattern where
|
||||
* animation.stop is returned as a reference from a useEffect.
|
||||
*/
|
||||
this.stop = () => {
|
||||
const { motionValue } = this.options;
|
||||
if (motionValue && motionValue.updatedAt !== time.now()) {
|
||||
this.tick(time.now());
|
||||
}
|
||||
this.isStopped = true;
|
||||
if (this.state === "idle")
|
||||
return;
|
||||
this.teardown();
|
||||
this.options.onStop?.();
|
||||
};
|
||||
activeAnimations.mainThread++;
|
||||
this.options = options;
|
||||
this.initAnimation();
|
||||
this.play();
|
||||
if (options.autoplay === false)
|
||||
this.pause();
|
||||
}
|
||||
initAnimation() {
|
||||
const { options } = this;
|
||||
replaceTransitionType(options);
|
||||
const { type = keyframes, repeat = 0, repeatDelay = 0, repeatType, velocity = 0, } = options;
|
||||
let { keyframes: keyframes$1 } = options;
|
||||
const generatorFactory = type || keyframes;
|
||||
if (process.env.NODE_ENV !== "production" &&
|
||||
generatorFactory !== keyframes) {
|
||||
invariant(keyframes$1.length <= 2, `Only two keyframes currently supported with spring and inertia animations. Trying to animate ${keyframes$1}`, "spring-two-frames");
|
||||
}
|
||||
if (generatorFactory !== keyframes &&
|
||||
typeof keyframes$1[0] !== "number") {
|
||||
this.mixKeyframes = pipe(percentToProgress, mix(keyframes$1[0], keyframes$1[1]));
|
||||
keyframes$1 = [0, 100];
|
||||
}
|
||||
const generator = generatorFactory({ ...options, keyframes: keyframes$1 });
|
||||
/**
|
||||
* If we have a mirror repeat type we need to create a second generator that outputs the
|
||||
* mirrored (not reversed) animation and later ping pong between the two generators.
|
||||
*/
|
||||
if (repeatType === "mirror") {
|
||||
this.mirroredGenerator = generatorFactory({
|
||||
...options,
|
||||
keyframes: [...keyframes$1].reverse(),
|
||||
velocity: -velocity,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* If duration is undefined and we have repeat options,
|
||||
* we need to calculate a duration from the generator.
|
||||
*
|
||||
* We set it to the generator itself to cache the duration.
|
||||
* Any timeline resolver will need to have already precalculated
|
||||
* the duration by this step.
|
||||
*/
|
||||
if (generator.calculatedDuration === null) {
|
||||
generator.calculatedDuration = calcGeneratorDuration(generator);
|
||||
}
|
||||
const { calculatedDuration } = generator;
|
||||
this.calculatedDuration = calculatedDuration;
|
||||
this.resolvedDuration = calculatedDuration + repeatDelay;
|
||||
this.totalDuration = this.resolvedDuration * (repeat + 1) - repeatDelay;
|
||||
this.generator = generator;
|
||||
}
|
||||
updateTime(timestamp) {
|
||||
const animationTime = Math.round(timestamp - this.startTime) * this.playbackSpeed;
|
||||
// Update currentTime
|
||||
if (this.holdTime !== null) {
|
||||
this.currentTime = this.holdTime;
|
||||
}
|
||||
else {
|
||||
// Rounding the time because floating point arithmetic is not always accurate, e.g. 3000.367 - 1000.367 =
|
||||
// 2000.0000000000002. This is a problem when we are comparing the currentTime with the duration, for
|
||||
// example.
|
||||
this.currentTime = animationTime;
|
||||
}
|
||||
}
|
||||
tick(timestamp, sample = false) {
|
||||
const { generator, totalDuration, mixKeyframes, mirroredGenerator, resolvedDuration, calculatedDuration, } = this;
|
||||
if (this.startTime === null)
|
||||
return generator.next(0);
|
||||
const { delay = 0, keyframes, repeat, repeatType, repeatDelay, type, onUpdate, finalKeyframe, } = this.options;
|
||||
/**
|
||||
* requestAnimationFrame timestamps can come through as lower than
|
||||
* the startTime as set by performance.now(). Here we prevent this,
|
||||
* though in the future it could be possible to make setting startTime
|
||||
* a pending operation that gets resolved here.
|
||||
*/
|
||||
if (this.speed > 0) {
|
||||
this.startTime = Math.min(this.startTime, timestamp);
|
||||
}
|
||||
else if (this.speed < 0) {
|
||||
this.startTime = Math.min(timestamp - totalDuration / this.speed, this.startTime);
|
||||
}
|
||||
if (sample) {
|
||||
this.currentTime = timestamp;
|
||||
}
|
||||
else {
|
||||
this.updateTime(timestamp);
|
||||
}
|
||||
// Rebase on delay
|
||||
const timeWithoutDelay = this.currentTime - delay * (this.playbackSpeed >= 0 ? 1 : -1);
|
||||
const isInDelayPhase = this.playbackSpeed >= 0
|
||||
? timeWithoutDelay < 0
|
||||
: timeWithoutDelay > totalDuration;
|
||||
this.currentTime = Math.max(timeWithoutDelay, 0);
|
||||
// If this animation has finished, set the current time to the total duration.
|
||||
if (this.state === "finished" && this.holdTime === null) {
|
||||
this.currentTime = totalDuration;
|
||||
}
|
||||
let elapsed = this.currentTime;
|
||||
let frameGenerator = generator;
|
||||
if (repeat) {
|
||||
/**
|
||||
* Get the current progress (0-1) of the animation. If t is >
|
||||
* than duration we'll get values like 2.5 (midway through the
|
||||
* third iteration)
|
||||
*/
|
||||
const progress = Math.min(this.currentTime, totalDuration) / resolvedDuration;
|
||||
/**
|
||||
* Get the current iteration (0 indexed). For instance the floor of
|
||||
* 2.5 is 2.
|
||||
*/
|
||||
let currentIteration = Math.floor(progress);
|
||||
/**
|
||||
* Get the current progress of the iteration by taking the remainder
|
||||
* so 2.5 is 0.5 through iteration 2
|
||||
*/
|
||||
let iterationProgress = progress % 1.0;
|
||||
/**
|
||||
* If iteration progress is 1 we count that as the end
|
||||
* of the previous iteration.
|
||||
*/
|
||||
if (!iterationProgress && progress >= 1) {
|
||||
iterationProgress = 1;
|
||||
}
|
||||
iterationProgress === 1 && currentIteration--;
|
||||
currentIteration = Math.min(currentIteration, repeat + 1);
|
||||
/**
|
||||
* Reverse progress if we're not running in "normal" direction
|
||||
*/
|
||||
const isOddIteration = Boolean(currentIteration % 2);
|
||||
if (isOddIteration) {
|
||||
if (repeatType === "reverse") {
|
||||
iterationProgress = 1 - iterationProgress;
|
||||
if (repeatDelay) {
|
||||
iterationProgress -= repeatDelay / resolvedDuration;
|
||||
}
|
||||
}
|
||||
else if (repeatType === "mirror") {
|
||||
frameGenerator = mirroredGenerator;
|
||||
}
|
||||
}
|
||||
elapsed = clamp(0, 1, iterationProgress) * resolvedDuration;
|
||||
}
|
||||
/**
|
||||
* If we're in negative time, set state as the initial keyframe.
|
||||
* This prevents delay: x, duration: 0 animations from finishing
|
||||
* instantly.
|
||||
*/
|
||||
const state = isInDelayPhase
|
||||
? { done: false, value: keyframes[0] }
|
||||
: frameGenerator.next(elapsed);
|
||||
if (mixKeyframes) {
|
||||
state.value = mixKeyframes(state.value);
|
||||
}
|
||||
let { done } = state;
|
||||
if (!isInDelayPhase && calculatedDuration !== null) {
|
||||
done =
|
||||
this.playbackSpeed >= 0
|
||||
? this.currentTime >= totalDuration
|
||||
: this.currentTime <= 0;
|
||||
}
|
||||
const isAnimationFinished = this.holdTime === null &&
|
||||
(this.state === "finished" || (this.state === "running" && done));
|
||||
// TODO: The exception for inertia could be cleaner here
|
||||
if (isAnimationFinished && type !== inertia) {
|
||||
state.value = getFinalKeyframe(keyframes, this.options, finalKeyframe, this.speed);
|
||||
}
|
||||
if (onUpdate) {
|
||||
onUpdate(state.value);
|
||||
}
|
||||
if (isAnimationFinished) {
|
||||
this.finish();
|
||||
}
|
||||
return state;
|
||||
}
|
||||
/**
|
||||
* Allows the returned animation to be awaited or promise-chained. Currently
|
||||
* resolves when the animation finishes at all but in a future update could/should
|
||||
* reject if its cancels.
|
||||
*/
|
||||
then(resolve, reject) {
|
||||
return this.finished.then(resolve, reject);
|
||||
}
|
||||
get duration() {
|
||||
return millisecondsToSeconds(this.calculatedDuration);
|
||||
}
|
||||
get iterationDuration() {
|
||||
const { delay = 0 } = this.options || {};
|
||||
return this.duration + millisecondsToSeconds(delay);
|
||||
}
|
||||
get time() {
|
||||
return millisecondsToSeconds(this.currentTime);
|
||||
}
|
||||
set time(newTime) {
|
||||
newTime = secondsToMilliseconds(newTime);
|
||||
this.currentTime = newTime;
|
||||
if (this.startTime === null ||
|
||||
this.holdTime !== null ||
|
||||
this.playbackSpeed === 0) {
|
||||
this.holdTime = newTime;
|
||||
}
|
||||
else if (this.driver) {
|
||||
this.startTime = this.driver.now() - newTime / this.playbackSpeed;
|
||||
}
|
||||
this.driver?.start(false);
|
||||
}
|
||||
get speed() {
|
||||
return this.playbackSpeed;
|
||||
}
|
||||
set speed(newSpeed) {
|
||||
this.updateTime(time.now());
|
||||
const hasChanged = this.playbackSpeed !== newSpeed;
|
||||
this.playbackSpeed = newSpeed;
|
||||
if (hasChanged) {
|
||||
this.time = millisecondsToSeconds(this.currentTime);
|
||||
}
|
||||
}
|
||||
play() {
|
||||
if (this.isStopped)
|
||||
return;
|
||||
const { driver = frameloopDriver, startTime } = this.options;
|
||||
if (!this.driver) {
|
||||
this.driver = driver((timestamp) => this.tick(timestamp));
|
||||
}
|
||||
this.options.onPlay?.();
|
||||
const now = this.driver.now();
|
||||
if (this.state === "finished") {
|
||||
this.updateFinished();
|
||||
this.startTime = now;
|
||||
}
|
||||
else if (this.holdTime !== null) {
|
||||
this.startTime = now - this.holdTime;
|
||||
}
|
||||
else if (!this.startTime) {
|
||||
this.startTime = startTime ?? now;
|
||||
}
|
||||
if (this.state === "finished" && this.speed < 0) {
|
||||
this.startTime += this.calculatedDuration;
|
||||
}
|
||||
this.holdTime = null;
|
||||
/**
|
||||
* Set playState to running only after we've used it in
|
||||
* the previous logic.
|
||||
*/
|
||||
this.state = "running";
|
||||
this.driver.start();
|
||||
}
|
||||
pause() {
|
||||
this.state = "paused";
|
||||
this.updateTime(time.now());
|
||||
this.holdTime = this.currentTime;
|
||||
}
|
||||
complete() {
|
||||
if (this.state !== "running") {
|
||||
this.play();
|
||||
}
|
||||
this.state = "finished";
|
||||
this.holdTime = null;
|
||||
}
|
||||
finish() {
|
||||
this.notifyFinished();
|
||||
this.teardown();
|
||||
this.state = "finished";
|
||||
this.options.onComplete?.();
|
||||
}
|
||||
cancel() {
|
||||
this.holdTime = null;
|
||||
this.startTime = 0;
|
||||
this.tick(0);
|
||||
this.teardown();
|
||||
this.options.onCancel?.();
|
||||
}
|
||||
teardown() {
|
||||
this.state = "idle";
|
||||
this.stopDriver();
|
||||
this.startTime = this.holdTime = null;
|
||||
activeAnimations.mainThread--;
|
||||
}
|
||||
stopDriver() {
|
||||
if (!this.driver)
|
||||
return;
|
||||
this.driver.stop();
|
||||
this.driver = undefined;
|
||||
}
|
||||
sample(sampleTime) {
|
||||
this.startTime = 0;
|
||||
return this.tick(sampleTime, true);
|
||||
}
|
||||
attachTimeline(timeline) {
|
||||
if (this.options.allowFlatten) {
|
||||
this.options.type = "keyframes";
|
||||
this.options.ease = "linear";
|
||||
this.initAnimation();
|
||||
}
|
||||
this.driver?.stop();
|
||||
return timeline.observe(this);
|
||||
}
|
||||
}
|
||||
// Legacy function support
|
||||
function animateValue(options) {
|
||||
return new JSAnimation(options);
|
||||
}
|
||||
|
||||
export { JSAnimation, animateValue };
|
||||
Reference in New Issue
Block a user