This commit is contained in:
Iliyan Angelov
2025-09-14 23:24:25 +03:00
commit c67067a2a4
71311 changed files with 6800714 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
import { observeTimeline } from '../render/dom/scroll/observe.mjs';
import { supportsScrollTimeline } from '../render/dom/scroll/supports.mjs';
class GroupPlaybackControls {
constructor(animations) {
this.animations = animations.filter(Boolean);
}
then(onResolve, onReject) {
return Promise.all(this.animations).then(onResolve).catch(onReject);
}
/**
* TODO: Filter out cancelled or stopped animations before returning
*/
getAll(propName) {
return this.animations[0][propName];
}
setAll(propName, newValue) {
for (let i = 0; i < this.animations.length; i++) {
this.animations[i][propName] = newValue;
}
}
attachTimeline(timeline) {
const cancelAll = this.animations.map((animation) => {
if (supportsScrollTimeline() && animation.attachTimeline) {
animation.attachTimeline(timeline);
}
else {
animation.pause();
return observeTimeline((progress) => {
animation.time = animation.duration * progress;
}, timeline);
}
});
return () => {
cancelAll.forEach((cancelTimeline, i) => {
if (cancelTimeline)
cancelTimeline();
this.animations[i].stop();
});
};
}
get time() {
return this.getAll("time");
}
set time(time) {
this.setAll("time", time);
}
get speed() {
return this.getAll("speed");
}
set speed(speed) {
this.setAll("speed", speed);
}
get duration() {
let max = 0;
for (let i = 0; i < this.animations.length; i++) {
max = Math.max(max, this.animations[i].duration);
}
return max;
}
runAll(methodName) {
this.animations.forEach((controls) => controls[methodName]());
}
play() {
this.runAll("play");
}
pause() {
this.runAll("pause");
}
stop() {
this.runAll("stop");
}
cancel() {
this.runAll("cancel");
}
complete() {
this.runAll("complete");
}
}
export { GroupPlaybackControls };

View File

@@ -0,0 +1,83 @@
import { resolveElements } from '../render/dom/utils/resolve-element.mjs';
import { visualElementStore } from '../render/store.mjs';
import { invariant } from '../utils/errors.mjs';
import { GroupPlaybackControls } from './GroupPlaybackControls.mjs';
import { isDOMKeyframes } from './utils/is-dom-keyframes.mjs';
import { animateTarget } from './interfaces/visual-element-target.mjs';
import { createVisualElement } from './utils/create-visual-element.mjs';
import { animateSingleValue } from './interfaces/single-value.mjs';
import { createAnimationsFromSequence } from './sequence/create.mjs';
import { isMotionValue } from '../value/utils/is-motion-value.mjs';
function animateElements(elementOrSelector, keyframes, options, scope) {
const elements = resolveElements(elementOrSelector, scope);
const numElements = elements.length;
invariant(Boolean(numElements), "No valid element provided.");
const animations = [];
for (let i = 0; i < numElements; i++) {
const element = elements[i];
/**
* Check each element for an associated VisualElement. If none exists,
* we need to create one.
*/
if (!visualElementStore.has(element)) {
/**
* TODO: We only need render-specific parts of the VisualElement.
* With some additional work the size of the animate() function
* could be reduced significantly.
*/
createVisualElement(element);
}
const visualElement = visualElementStore.get(element);
const transition = { ...options };
/**
* Resolve stagger function if provided.
*/
if (typeof transition.delay === "function") {
transition.delay = transition.delay(i, numElements);
}
animations.push(...animateTarget(visualElement, { ...keyframes, transition }, {}));
}
return new GroupPlaybackControls(animations);
}
const isSequence = (value) => Array.isArray(value) && Array.isArray(value[0]);
function animateSequence(sequence, options, scope) {
const animations = [];
const animationDefinitions = createAnimationsFromSequence(sequence, options, scope);
animationDefinitions.forEach(({ keyframes, transition }, subject) => {
let animation;
if (isMotionValue(subject)) {
animation = animateSingleValue(subject, keyframes.default, transition.default);
}
else {
animation = animateElements(subject, keyframes, transition);
}
animations.push(animation);
});
return new GroupPlaybackControls(animations);
}
const createScopedAnimate = (scope) => {
/**
* Implementation
*/
function scopedAnimate(valueOrElementOrSequence, keyframes, options) {
let animation;
if (isSequence(valueOrElementOrSequence)) {
animation = animateSequence(valueOrElementOrSequence, keyframes, scope);
}
else if (isDOMKeyframes(keyframes)) {
animation = animateElements(valueOrElementOrSequence, keyframes, options, scope);
}
else {
animation = animateSingleValue(valueOrElementOrSequence, keyframes, options);
}
if (scope) {
scope.animations.push(animation);
}
return animation;
}
return scopedAnimate;
};
const animate = createScopedAnimate();
export { animate, createScopedAnimate };

View File

@@ -0,0 +1,40 @@
import { animateValue } from './js/index.mjs';
import { noop } from '../../utils/noop.mjs';
function createInstantAnimation({ keyframes, delay, onUpdate, onComplete, }) {
const setValue = () => {
onUpdate && onUpdate(keyframes[keyframes.length - 1]);
onComplete && onComplete();
/**
* TODO: As this API grows it could make sense to always return
* animateValue. This will be a bigger project as animateValue
* is frame-locked whereas this function resolves instantly.
* This is a behavioural change and also has ramifications regarding
* assumptions within tests.
*/
return {
time: 0,
speed: 1,
duration: 0,
play: (noop),
pause: (noop),
stop: (noop),
then: (resolve) => {
resolve();
return Promise.resolve();
},
cancel: (noop),
complete: (noop),
};
};
return delay
? animateValue({
keyframes: [0, 1],
duration: 0,
delay,
onComplete: setValue,
})
: setValue();
}
export { createInstantAnimation };

View File

@@ -0,0 +1,16 @@
import { frame, cancelFrame, frameData } from '../../../frameloop/frame.mjs';
const frameloopDriver = (update) => {
const passTimestamp = ({ timestamp }) => update(timestamp);
return {
start: () => frame.update(passTimestamp, true),
stop: () => cancelFrame(passTimestamp),
/**
* If we're processing this frame we can use the
* framelocked timestamp to keep things in sync.
*/
now: () => frameData.isProcessing ? frameData.timestamp : performance.now(),
};
};
export { frameloopDriver };

View File

@@ -0,0 +1,302 @@
import { keyframes } from '../../generators/keyframes.mjs';
import { spring } from '../../generators/spring/index.mjs';
import { inertia } from '../../generators/inertia.mjs';
import { frameloopDriver } from './driver-frameloop.mjs';
import { interpolate } from '../../../utils/interpolate.mjs';
import { clamp } from '../../../utils/clamp.mjs';
import { millisecondsToSeconds, secondsToMilliseconds } from '../../../utils/time-conversion.mjs';
import { calcGeneratorDuration } from '../../generators/utils/calc-duration.mjs';
import { invariant } from '../../../utils/errors.mjs';
const types = {
decay: inertia,
inertia,
tween: keyframes,
keyframes: keyframes,
spring,
};
/**
* Animate a single value on the main thread.
*
* This function is written, where functionality overlaps,
* to be largely spec-compliant with WAAPI to allow fungibility
* between the two.
*/
function animateValue({ autoplay = true, delay = 0, driver = frameloopDriver, keyframes: keyframes$1, type = "keyframes", repeat = 0, repeatDelay = 0, repeatType = "loop", onPlay, onStop, onComplete, onUpdate, ...options }) {
let speed = 1;
let hasStopped = false;
let resolveFinishedPromise;
let currentFinishedPromise;
/**
* Resolve the current Promise every time we enter the
* finished state. This is WAAPI-compatible behaviour.
*/
const updateFinishedPromise = () => {
currentFinishedPromise = new Promise((resolve) => {
resolveFinishedPromise = resolve;
});
};
// Create the first finished promise
updateFinishedPromise();
let animationDriver;
const generatorFactory = types[type] || keyframes;
/**
* If this isn't the keyframes generator and we've been provided
* strings as keyframes, we need to interpolate these.
*/
let mapNumbersToKeyframes;
if (generatorFactory !== keyframes &&
typeof keyframes$1[0] !== "number") {
if (process.env.NODE_ENV !== "production") {
invariant(keyframes$1.length === 2, `Only two keyframes currently supported with spring and inertia animations. Trying to animate ${keyframes$1}`);
}
mapNumbersToKeyframes = interpolate([0, 100], keyframes$1, {
clamp: false,
});
keyframes$1 = [0, 100];
}
const generator = generatorFactory({ ...options, keyframes: keyframes$1 });
let mirroredGenerator;
if (repeatType === "mirror") {
mirroredGenerator = generatorFactory({
...options,
keyframes: [...keyframes$1].reverse(),
velocity: -(options.velocity || 0),
});
}
let playState = "idle";
let holdTime = null;
let startTime = null;
let cancelTime = null;
/**
* 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 && repeat) {
generator.calculatedDuration = calcGeneratorDuration(generator);
}
const { calculatedDuration } = generator;
let resolvedDuration = Infinity;
let totalDuration = Infinity;
if (calculatedDuration !== null) {
resolvedDuration = calculatedDuration + repeatDelay;
totalDuration = resolvedDuration * (repeat + 1) - repeatDelay;
}
let currentTime = 0;
const tick = (timestamp) => {
if (startTime === null)
return;
/**
* 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 (speed > 0)
startTime = Math.min(startTime, timestamp);
if (speed < 0)
startTime = Math.min(timestamp - totalDuration / speed, startTime);
if (holdTime !== null) {
currentTime = 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.
currentTime = Math.round(timestamp - startTime) * speed;
}
// Rebase on delay
const timeWithoutDelay = currentTime - delay * (speed >= 0 ? 1 : -1);
const isInDelayPhase = speed >= 0 ? timeWithoutDelay < 0 : timeWithoutDelay > totalDuration;
currentTime = Math.max(timeWithoutDelay, 0);
/**
* If this animation has finished, set the current time
* to the total duration.
*/
if (playState === "finished" && holdTime === null) {
currentTime = totalDuration;
}
let elapsed = 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(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$1[0] }
: frameGenerator.next(elapsed);
if (mapNumbersToKeyframes) {
state.value = mapNumbersToKeyframes(state.value);
}
let { done } = state;
if (!isInDelayPhase && calculatedDuration !== null) {
done = speed >= 0 ? currentTime >= totalDuration : currentTime <= 0;
}
const isAnimationFinished = holdTime === null &&
(playState === "finished" || (playState === "running" && done));
if (onUpdate) {
onUpdate(state.value);
}
if (isAnimationFinished) {
finish();
}
return state;
};
const stopAnimationDriver = () => {
animationDriver && animationDriver.stop();
animationDriver = undefined;
};
const cancel = () => {
playState = "idle";
stopAnimationDriver();
resolveFinishedPromise();
updateFinishedPromise();
startTime = cancelTime = null;
};
const finish = () => {
playState = "finished";
onComplete && onComplete();
stopAnimationDriver();
resolveFinishedPromise();
};
const play = () => {
if (hasStopped)
return;
if (!animationDriver)
animationDriver = driver(tick);
const now = animationDriver.now();
onPlay && onPlay();
if (holdTime !== null) {
startTime = now - holdTime;
}
else if (!startTime || playState === "finished") {
startTime = now;
}
if (playState === "finished") {
updateFinishedPromise();
}
cancelTime = startTime;
holdTime = null;
/**
* Set playState to running only after we've used it in
* the previous logic.
*/
playState = "running";
animationDriver.start();
};
if (autoplay) {
play();
}
const controls = {
then(resolve, reject) {
return currentFinishedPromise.then(resolve, reject);
},
get time() {
return millisecondsToSeconds(currentTime);
},
set time(newTime) {
newTime = secondsToMilliseconds(newTime);
currentTime = newTime;
if (holdTime !== null || !animationDriver || speed === 0) {
holdTime = newTime;
}
else {
startTime = animationDriver.now() - newTime / speed;
}
},
get duration() {
const duration = generator.calculatedDuration === null
? calcGeneratorDuration(generator)
: generator.calculatedDuration;
return millisecondsToSeconds(duration);
},
get speed() {
return speed;
},
set speed(newSpeed) {
if (newSpeed === speed || !animationDriver)
return;
speed = newSpeed;
controls.time = millisecondsToSeconds(currentTime);
},
get state() {
return playState;
},
play,
pause: () => {
playState = "paused";
holdTime = currentTime;
},
stop: () => {
hasStopped = true;
if (playState === "idle")
return;
playState = "idle";
onStop && onStop();
cancel();
},
cancel: () => {
if (cancelTime !== null)
tick(cancelTime);
cancel();
},
complete: () => {
playState = "finished";
},
sample: (elapsed) => {
startTime = 0;
return tick(elapsed);
},
};
return controls;
}
export { animateValue };

View File

@@ -0,0 +1,202 @@
import { animateStyle } from './index.mjs';
import { isWaapiSupportedEasing } from './easing.mjs';
import { getFinalKeyframe } from './utils/get-final-keyframe.mjs';
import { animateValue } from '../js/index.mjs';
import { millisecondsToSeconds, secondsToMilliseconds } from '../../../utils/time-conversion.mjs';
import { memo } from '../../../utils/memo.mjs';
import { noop } from '../../../utils/noop.mjs';
import { frame, cancelFrame } from '../../../frameloop/frame.mjs';
const supportsWaapi = memo(() => Object.hasOwnProperty.call(Element.prototype, "animate"));
/**
* A list of values that can be hardware-accelerated.
*/
const acceleratedValues = new Set([
"opacity",
"clipPath",
"filter",
"transform",
"backgroundColor",
]);
/**
* 10ms is chosen here as it strikes a balance between smooth
* results (more than one keyframe per frame at 60fps) and
* keyframe quantity.
*/
const sampleDelta = 10; //ms
/**
* Implement a practical max duration for keyframe generation
* to prevent infinite loops
*/
const maxDuration = 20000;
const requiresPregeneratedKeyframes = (valueName, options) => options.type === "spring" ||
valueName === "backgroundColor" ||
!isWaapiSupportedEasing(options.ease);
function createAcceleratedAnimation(value, valueName, { onUpdate, onComplete, ...options }) {
const canAccelerateAnimation = supportsWaapi() &&
acceleratedValues.has(valueName) &&
!options.repeatDelay &&
options.repeatType !== "mirror" &&
options.damping !== 0 &&
options.type !== "inertia";
if (!canAccelerateAnimation)
return false;
/**
* TODO: Unify with js/index
*/
let hasStopped = false;
let resolveFinishedPromise;
let currentFinishedPromise;
/**
* Cancelling an animation will write to the DOM. For safety we want to defer
* this until the next `update` frame lifecycle. This flag tracks whether we
* have a pending cancel, if so we shouldn't allow animations to finish.
*/
let pendingCancel = false;
/**
* Resolve the current Promise every time we enter the
* finished state. This is WAAPI-compatible behaviour.
*/
const updateFinishedPromise = () => {
currentFinishedPromise = new Promise((resolve) => {
resolveFinishedPromise = resolve;
});
};
// Create the first finished promise
updateFinishedPromise();
let { keyframes, duration = 300, ease, times } = options;
/**
* If this animation needs pre-generated keyframes then generate.
*/
if (requiresPregeneratedKeyframes(valueName, options)) {
const sampleAnimation = animateValue({
...options,
repeat: 0,
delay: 0,
});
let state = { done: false, value: keyframes[0] };
const pregeneratedKeyframes = [];
/**
* Bail after 20 seconds of pre-generated keyframes as it's likely
* we're heading for an infinite loop.
*/
let t = 0;
while (!state.done && t < maxDuration) {
state = sampleAnimation.sample(t);
pregeneratedKeyframes.push(state.value);
t += sampleDelta;
}
times = undefined;
keyframes = pregeneratedKeyframes;
duration = t - sampleDelta;
ease = "linear";
}
const animation = animateStyle(value.owner.current, valueName, keyframes, {
...options,
duration,
/**
* This function is currently not called if ease is provided
* as a function so the cast is safe.
*
* However it would be possible for a future refinement to port
* in easing pregeneration from Motion One for browsers that
* support the upcoming `linear()` easing function.
*/
ease: ease,
times,
});
const cancelAnimation = () => {
pendingCancel = false;
animation.cancel();
};
const safeCancel = () => {
pendingCancel = true;
frame.update(cancelAnimation);
resolveFinishedPromise();
updateFinishedPromise();
};
/**
* Prefer the `onfinish` prop as it's more widely supported than
* the `finished` promise.
*
* Here, we synchronously set the provided MotionValue to the end
* keyframe. If we didn't, when the WAAPI animation is finished it would
* be removed from the element which would then revert to its old styles.
*/
animation.onfinish = () => {
if (pendingCancel)
return;
value.set(getFinalKeyframe(keyframes, options));
onComplete && onComplete();
safeCancel();
};
/**
* Animation interrupt callback.
*/
const controls = {
then(resolve, reject) {
return currentFinishedPromise.then(resolve, reject);
},
attachTimeline(timeline) {
animation.timeline = timeline;
animation.onfinish = null;
return noop;
},
get time() {
return millisecondsToSeconds(animation.currentTime || 0);
},
set time(newTime) {
animation.currentTime = secondsToMilliseconds(newTime);
},
get speed() {
return animation.playbackRate;
},
set speed(newSpeed) {
animation.playbackRate = newSpeed;
},
get duration() {
return millisecondsToSeconds(duration);
},
play: () => {
if (hasStopped)
return;
animation.play();
/**
* Cancel any pending cancel tasks
*/
cancelFrame(cancelAnimation);
},
pause: () => animation.pause(),
stop: () => {
hasStopped = true;
if (animation.playState === "idle")
return;
/**
* WAAPI doesn't natively have any interruption capabilities.
*
* Rather than read commited styles back out of the DOM, we can
* create a renderless JS animation and sample it twice to calculate
* its current value, "previous" value, and therefore allow
* Motion to calculate velocity for any subsequent animation.
*/
const { currentTime } = animation;
if (currentTime) {
const sampleAnimation = animateValue({
...options,
autoplay: false,
});
value.setWithVelocity(sampleAnimation.sample(currentTime - sampleDelta).value, sampleAnimation.sample(currentTime).value, sampleDelta);
}
safeCancel();
},
complete: () => {
if (pendingCancel)
return;
animation.finish();
},
cancel: safeCancel,
};
return controls;
}
export { createAcceleratedAnimation };

View File

@@ -0,0 +1,31 @@
import { isBezierDefinition } from '../../../easing/utils/is-bezier-definition.mjs';
function isWaapiSupportedEasing(easing) {
return Boolean(!easing ||
(typeof easing === "string" && supportedWaapiEasing[easing]) ||
isBezierDefinition(easing) ||
(Array.isArray(easing) && easing.every(isWaapiSupportedEasing)));
}
const cubicBezierAsString = ([a, b, c, d]) => `cubic-bezier(${a}, ${b}, ${c}, ${d})`;
const supportedWaapiEasing = {
linear: "linear",
ease: "ease",
easeIn: "ease-in",
easeOut: "ease-out",
easeInOut: "ease-in-out",
circIn: cubicBezierAsString([0, 0.65, 0.55, 1]),
circOut: cubicBezierAsString([0.55, 0, 1, 0.45]),
backIn: cubicBezierAsString([0.31, 0.01, 0.66, -0.59]),
backOut: cubicBezierAsString([0.33, 1.53, 0.69, 0.99]),
};
function mapEasingToNativeEasing(easing) {
if (!easing)
return undefined;
return isBezierDefinition(easing)
? cubicBezierAsString(easing)
: Array.isArray(easing)
? easing.map(mapEasingToNativeEasing)
: supportedWaapiEasing[easing];
}
export { cubicBezierAsString, isWaapiSupportedEasing, mapEasingToNativeEasing, supportedWaapiEasing };

View File

@@ -0,0 +1,23 @@
import { mapEasingToNativeEasing } from './easing.mjs';
function animateStyle(element, valueName, keyframes, { delay = 0, duration, repeat = 0, repeatType = "loop", ease, times, } = {}) {
const keyframeOptions = { [valueName]: keyframes };
if (times)
keyframeOptions.offset = times;
const easing = mapEasingToNativeEasing(ease);
/**
* If this is an easing array, apply to keyframes, not animation as a whole
*/
if (Array.isArray(easing))
keyframeOptions.easing = easing;
return element.animate(keyframeOptions, {
delay,
duration,
easing: !Array.isArray(easing) ? easing : "linear",
fill: "both",
iterations: repeat + 1,
direction: repeatType === "reverse" ? "alternate" : "normal",
});
}
export { animateStyle };

View File

@@ -0,0 +1,8 @@
function getFinalKeyframe(keyframes, { repeat, repeatType = "loop" }) {
const index = repeat && repeatType !== "loop" && repeat % 2 === 1
? 0
: keyframes.length - 1;
return keyframes[index];
}
export { getFinalKeyframe };

View File

@@ -0,0 +1,87 @@
import { spring } from './spring/index.mjs';
import { calcGeneratorVelocity } from './utils/velocity.mjs';
function inertia({ keyframes, velocity = 0.0, power = 0.8, timeConstant = 325, bounceDamping = 10, bounceStiffness = 500, modifyTarget, min, max, restDelta = 0.5, restSpeed, }) {
const origin = keyframes[0];
const state = {
done: false,
value: origin,
};
const isOutOfBounds = (v) => (min !== undefined && v < min) || (max !== undefined && v > max);
const nearestBoundary = (v) => {
if (min === undefined)
return max;
if (max === undefined)
return min;
return Math.abs(min - v) < Math.abs(max - v) ? min : max;
};
let amplitude = power * velocity;
const ideal = origin + amplitude;
const target = modifyTarget === undefined ? ideal : modifyTarget(ideal);
/**
* If the target has changed we need to re-calculate the amplitude, otherwise
* the animation will start from the wrong position.
*/
if (target !== ideal)
amplitude = target - origin;
const calcDelta = (t) => -amplitude * Math.exp(-t / timeConstant);
const calcLatest = (t) => target + calcDelta(t);
const applyFriction = (t) => {
const delta = calcDelta(t);
const latest = calcLatest(t);
state.done = Math.abs(delta) <= restDelta;
state.value = state.done ? target : latest;
};
/**
* Ideally this would resolve for t in a stateless way, we could
* do that by always precalculating the animation but as we know
* this will be done anyway we can assume that spring will
* be discovered during that.
*/
let timeReachedBoundary;
let spring$1;
const checkCatchBoundary = (t) => {
if (!isOutOfBounds(state.value))
return;
timeReachedBoundary = t;
spring$1 = spring({
keyframes: [state.value, nearestBoundary(state.value)],
velocity: calcGeneratorVelocity(calcLatest, t, state.value),
damping: bounceDamping,
stiffness: bounceStiffness,
restDelta,
restSpeed,
});
};
checkCatchBoundary(0);
return {
calculatedDuration: null,
next: (t) => {
/**
* We need to resolve the friction to figure out if we need a
* spring but we don't want to do this twice per frame. So here
* we flag if we updated for this frame and later if we did
* we can skip doing it again.
*/
let hasUpdatedFrame = false;
if (!spring$1 && timeReachedBoundary === undefined) {
hasUpdatedFrame = true;
applyFriction(t);
checkCatchBoundary(t);
}
/**
* If we have a spring and the provided t is beyond the moment the friction
* animation crossed the min/max boundary, use the spring.
*/
if (timeReachedBoundary !== undefined && t > timeReachedBoundary) {
return spring$1.next(t - timeReachedBoundary);
}
else {
!hasUpdatedFrame && applyFriction(t);
return state;
}
},
};
}
export { inertia };

View File

@@ -0,0 +1,51 @@
import { easeInOut } from '../../easing/ease.mjs';
import { isEasingArray } from '../../easing/utils/is-easing-array.mjs';
import { easingDefinitionToFunction } from '../../easing/utils/map.mjs';
import { interpolate } from '../../utils/interpolate.mjs';
import { defaultOffset } from '../../utils/offsets/default.mjs';
import { convertOffsetToTimes } from '../../utils/offsets/time.mjs';
function defaultEasing(values, easing) {
return values.map(() => easing || easeInOut).splice(0, values.length - 1);
}
function keyframes({ duration = 300, keyframes: keyframeValues, times, ease = "easeInOut", }) {
/**
* Easing functions can be externally defined as strings. Here we convert them
* into actual functions.
*/
const easingFunctions = isEasingArray(ease)
? ease.map(easingDefinitionToFunction)
: easingDefinitionToFunction(ease);
/**
* This is the Iterator-spec return value. We ensure it's mutable rather than using a generator
* to reduce GC during animation.
*/
const state = {
done: false,
value: keyframeValues[0],
};
/**
* Create a times array based on the provided 0-1 offsets
*/
const absoluteTimes = convertOffsetToTimes(
// Only use the provided offsets if they're the correct length
// TODO Maybe we should warn here if there's a length mismatch
times && times.length === keyframeValues.length
? times
: defaultOffset(keyframeValues), duration);
const mapTimeToKeyframe = interpolate(absoluteTimes, keyframeValues, {
ease: Array.isArray(easingFunctions)
? easingFunctions
: defaultEasing(keyframeValues, easingFunctions),
});
return {
calculatedDuration: duration,
next: (t) => {
state.value = mapTimeToKeyframe(t);
state.done = t >= duration;
return state;
},
};
}
export { defaultEasing, keyframes };

View File

@@ -0,0 +1,89 @@
import { warning } from '../../../utils/errors.mjs';
import { clamp } from '../../../utils/clamp.mjs';
import { secondsToMilliseconds, millisecondsToSeconds } from '../../../utils/time-conversion.mjs';
const safeMin = 0.001;
const minDuration = 0.01;
const maxDuration = 10.0;
const minDamping = 0.05;
const maxDamping = 1;
function findSpring({ duration = 800, bounce = 0.25, velocity = 0, mass = 1, }) {
let envelope;
let derivative;
warning(duration <= secondsToMilliseconds(maxDuration), "Spring duration must be 10 seconds or less");
let dampingRatio = 1 - bounce;
/**
* Restrict dampingRatio and duration to within acceptable ranges.
*/
dampingRatio = clamp(minDamping, maxDamping, dampingRatio);
duration = clamp(minDuration, maxDuration, millisecondsToSeconds(duration));
if (dampingRatio < 1) {
/**
* Underdamped spring
*/
envelope = (undampedFreq) => {
const exponentialDecay = undampedFreq * dampingRatio;
const delta = exponentialDecay * duration;
const a = exponentialDecay - velocity;
const b = calcAngularFreq(undampedFreq, dampingRatio);
const c = Math.exp(-delta);
return safeMin - (a / b) * c;
};
derivative = (undampedFreq) => {
const exponentialDecay = undampedFreq * dampingRatio;
const delta = exponentialDecay * duration;
const d = delta * velocity + velocity;
const e = Math.pow(dampingRatio, 2) * Math.pow(undampedFreq, 2) * duration;
const f = Math.exp(-delta);
const g = calcAngularFreq(Math.pow(undampedFreq, 2), dampingRatio);
const factor = -envelope(undampedFreq) + safeMin > 0 ? -1 : 1;
return (factor * ((d - e) * f)) / g;
};
}
else {
/**
* Critically-damped spring
*/
envelope = (undampedFreq) => {
const a = Math.exp(-undampedFreq * duration);
const b = (undampedFreq - velocity) * duration + 1;
return -safeMin + a * b;
};
derivative = (undampedFreq) => {
const a = Math.exp(-undampedFreq * duration);
const b = (velocity - undampedFreq) * (duration * duration);
return a * b;
};
}
const initialGuess = 5 / duration;
const undampedFreq = approximateRoot(envelope, derivative, initialGuess);
duration = secondsToMilliseconds(duration);
if (isNaN(undampedFreq)) {
return {
stiffness: 100,
damping: 10,
duration,
};
}
else {
const stiffness = Math.pow(undampedFreq, 2) * mass;
return {
stiffness,
damping: dampingRatio * 2 * Math.sqrt(mass * stiffness),
duration,
};
}
}
const rootIterations = 12;
function approximateRoot(envelope, derivative, initialGuess) {
let result = initialGuess;
for (let i = 1; i < rootIterations; i++) {
result = result - envelope(result) / derivative(result);
}
return result;
}
function calcAngularFreq(undampedFreq, dampingRatio) {
return undampedFreq * Math.sqrt(1 - dampingRatio * dampingRatio);
}
export { calcAngularFreq, findSpring, maxDamping, maxDuration, minDamping, minDuration };

View File

@@ -0,0 +1,131 @@
import { millisecondsToSeconds } from '../../../utils/time-conversion.mjs';
import { calcGeneratorVelocity } from '../utils/velocity.mjs';
import { findSpring, calcAngularFreq } from './find.mjs';
const durationKeys = ["duration", "bounce"];
const physicsKeys = ["stiffness", "damping", "mass"];
function isSpringType(options, keys) {
return keys.some((key) => options[key] !== undefined);
}
function getSpringOptions(options) {
let springOptions = {
velocity: 0.0,
stiffness: 100,
damping: 10,
mass: 1.0,
isResolvedFromDuration: false,
...options,
};
// stiffness/damping/mass overrides duration/bounce
if (!isSpringType(options, physicsKeys) &&
isSpringType(options, durationKeys)) {
const derived = findSpring(options);
springOptions = {
...springOptions,
...derived,
mass: 1.0,
};
springOptions.isResolvedFromDuration = true;
}
return springOptions;
}
function spring({ keyframes, restDelta, restSpeed, ...options }) {
const origin = keyframes[0];
const target = keyframes[keyframes.length - 1];
/**
* This is the Iterator-spec return value. We ensure it's mutable rather than using a generator
* to reduce GC during animation.
*/
const state = { done: false, value: origin };
const { stiffness, damping, mass, duration, velocity, isResolvedFromDuration, } = getSpringOptions({
...options,
velocity: -millisecondsToSeconds(options.velocity || 0),
});
const initialVelocity = velocity || 0.0;
const dampingRatio = damping / (2 * Math.sqrt(stiffness * mass));
const initialDelta = target - origin;
const undampedAngularFreq = millisecondsToSeconds(Math.sqrt(stiffness / mass));
/**
* If we're working on a granular scale, use smaller defaults for determining
* when the spring is finished.
*
* These defaults have been selected emprically based on what strikes a good
* ratio between feeling good and finishing as soon as changes are imperceptible.
*/
const isGranularScale = Math.abs(initialDelta) < 5;
restSpeed || (restSpeed = isGranularScale ? 0.01 : 2);
restDelta || (restDelta = isGranularScale ? 0.005 : 0.5);
let resolveSpring;
if (dampingRatio < 1) {
const angularFreq = calcAngularFreq(undampedAngularFreq, dampingRatio);
// Underdamped spring
resolveSpring = (t) => {
const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t);
return (target -
envelope *
(((initialVelocity +
dampingRatio * undampedAngularFreq * initialDelta) /
angularFreq) *
Math.sin(angularFreq * t) +
initialDelta * Math.cos(angularFreq * t)));
};
}
else if (dampingRatio === 1) {
// Critically damped spring
resolveSpring = (t) => target -
Math.exp(-undampedAngularFreq * t) *
(initialDelta +
(initialVelocity + undampedAngularFreq * initialDelta) * t);
}
else {
// Overdamped spring
const dampedAngularFreq = undampedAngularFreq * Math.sqrt(dampingRatio * dampingRatio - 1);
resolveSpring = (t) => {
const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t);
// When performing sinh or cosh values can hit Infinity so we cap them here
const freqForT = Math.min(dampedAngularFreq * t, 300);
return (target -
(envelope *
((initialVelocity +
dampingRatio * undampedAngularFreq * initialDelta) *
Math.sinh(freqForT) +
dampedAngularFreq *
initialDelta *
Math.cosh(freqForT))) /
dampedAngularFreq);
};
}
return {
calculatedDuration: isResolvedFromDuration ? duration || null : null,
next: (t) => {
const current = resolveSpring(t);
if (!isResolvedFromDuration) {
let currentVelocity = initialVelocity;
if (t !== 0) {
/**
* We only need to calculate velocity for under-damped springs
* as over- and critically-damped springs can't overshoot, so
* checking only for displacement is enough.
*/
if (dampingRatio < 1) {
currentVelocity = calcGeneratorVelocity(resolveSpring, t, current);
}
else {
currentVelocity = 0;
}
}
const isBelowVelocityThreshold = Math.abs(currentVelocity) <= restSpeed;
const isBelowDisplacementThreshold = Math.abs(target - current) <= restDelta;
state.done =
isBelowVelocityThreshold && isBelowDisplacementThreshold;
}
else {
state.done = t >= duration;
}
state.value = state.done ? target : current;
return state;
},
};
}
export { spring };

View File

@@ -0,0 +1,17 @@
/**
* Implement a practical max duration for keyframe generation
* to prevent infinite loops
*/
const maxGeneratorDuration = 20000;
function calcGeneratorDuration(generator) {
let duration = 0;
const timeStep = 50;
let state = generator.next(duration);
while (!state.done && duration < maxGeneratorDuration) {
duration += timeStep;
state = generator.next(duration);
}
return duration >= maxGeneratorDuration ? Infinity : duration;
}
export { calcGeneratorDuration, maxGeneratorDuration };

View File

@@ -0,0 +1,9 @@
import { velocityPerSecond } from '../../../utils/velocity-per-second.mjs';
const velocitySampleDuration = 5; // ms
function calcGeneratorVelocity(resolveValue, t, current) {
const prevT = Math.max(t - velocitySampleDuration, 0);
return velocityPerSecond(current - resolveValue(prevT), t - prevT);
}
export { calcGeneratorVelocity };

View File

@@ -0,0 +1,57 @@
import { invariant } from '../../utils/errors.mjs';
import { setValues } from '../../render/utils/setters.mjs';
import { animateVisualElement } from '../interfaces/visual-element.mjs';
function stopAnimation(visualElement) {
visualElement.values.forEach((value) => value.stop());
}
/**
* @public
*/
function animationControls() {
/**
* Track whether the host component has mounted.
*/
let hasMounted = false;
/**
* A collection of linked component animation controls.
*/
const subscribers = new Set();
const controls = {
subscribe(visualElement) {
subscribers.add(visualElement);
return () => void subscribers.delete(visualElement);
},
start(definition, transitionOverride) {
invariant(hasMounted, "controls.start() should only be called after a component has mounted. Consider calling within a useEffect hook.");
const animations = [];
subscribers.forEach((visualElement) => {
animations.push(animateVisualElement(visualElement, definition, {
transitionOverride,
}));
});
return Promise.all(animations);
},
set(definition) {
invariant(hasMounted, "controls.set() should only be called after a component has mounted. Consider calling within a useEffect hook.");
return subscribers.forEach((visualElement) => {
setValues(visualElement, definition);
});
},
stop() {
subscribers.forEach((visualElement) => {
stopAnimation(visualElement);
});
},
mount() {
hasMounted = true;
return () => {
hasMounted = false;
controls.stop();
};
},
};
return controls;
}
export { animationControls };

View File

@@ -0,0 +1,17 @@
import { useConstant } from '../../utils/use-constant.mjs';
import { useUnmountEffect } from '../../utils/use-unmount-effect.mjs';
import { createScopedAnimate } from '../animate.mjs';
function useAnimate() {
const scope = useConstant(() => ({
current: null,
animations: [],
}));
const animate = useConstant(() => createScopedAnimate(scope));
useUnmountEffect(() => {
scope.animations.forEach((animation) => animation.stop());
});
return [scope, animate];
}
export { useAnimate };

View File

@@ -0,0 +1,68 @@
import { useState, useEffect } from 'react';
import { useConstant } from '../../utils/use-constant.mjs';
import { getOrigin, checkTargetForNewValues } from '../../render/utils/setters.mjs';
import { makeUseVisualState } from '../../motion/utils/use-visual-state.mjs';
import { createBox } from '../../projection/geometry/models.mjs';
import { VisualElement } from '../../render/VisualElement.mjs';
import { animateVisualElement } from '../interfaces/visual-element.mjs';
const createObject = () => ({});
class StateVisualElement extends VisualElement {
build() { }
measureInstanceViewportBox() {
return createBox();
}
resetTransform() { }
restoreTransform() { }
removeValueFromRenderState() { }
renderInstance() { }
scrapeMotionValuesFromProps() {
return createObject();
}
getBaseTargetFromProps() {
return undefined;
}
readValueFromInstance(_state, key, options) {
return options.initialState[key] || 0;
}
sortInstanceNodePosition() {
return 0;
}
makeTargetAnimatableFromInstance({ transition, transitionEnd, ...target }) {
const origin = getOrigin(target, transition || {}, this);
checkTargetForNewValues(this, target, origin);
return { transition, transitionEnd, ...target };
}
}
const useVisualState = makeUseVisualState({
scrapeMotionValuesFromProps: createObject,
createRenderState: createObject,
});
/**
* This is not an officially supported API and may be removed
* on any version.
*/
function useAnimatedState(initialState) {
const [animationState, setAnimationState] = useState(initialState);
const visualState = useVisualState({}, false);
const element = useConstant(() => {
return new StateVisualElement({ props: {}, visualState, presenceContext: null }, { initialState });
});
useEffect(() => {
element.mount({});
return () => element.unmount();
}, [element]);
useEffect(() => {
element.update({
onUpdate: (v) => {
setAnimationState({ ...v });
},
}, null);
}, [setAnimationState, element]);
const startAnimation = useConstant(() => (animationDefinition) => {
return animateVisualElement(element, animationDefinition);
});
return [animationState, startAnimation];
}
export { useAnimatedState };

View File

@@ -0,0 +1,41 @@
import { animationControls } from './animation-controls.mjs';
import { useConstant } from '../../utils/use-constant.mjs';
import { useIsomorphicLayoutEffect } from '../../utils/use-isomorphic-effect.mjs';
/**
* Creates `AnimationControls`, which can be used to manually start, stop
* and sequence animations on one or more components.
*
* The returned `AnimationControls` should be passed to the `animate` property
* of the components you want to animate.
*
* These components can then be animated with the `start` method.
*
* ```jsx
* import * as React from 'react'
* import { motion, useAnimation } from 'framer-motion'
*
* export function MyComponent(props) {
* const controls = useAnimation()
*
* controls.start({
* x: 100,
* transition: { duration: 0.5 },
* })
*
* return <motion.div animate={controls} />
* }
* ```
*
* @returns Animation controller with `start` and `stop` methods
*
* @public
*/
function useAnimationControls() {
const controls = useConstant(animationControls);
useIsomorphicLayoutEffect(controls.mount, []);
return controls;
}
const useAnimation = useAnimationControls;
export { useAnimation, useAnimationControls };

View File

@@ -0,0 +1,116 @@
import { warning } from '../../utils/errors.mjs';
import { secondsToMilliseconds } from '../../utils/time-conversion.mjs';
import { instantAnimationState } from '../../utils/use-instant-transition-state.mjs';
import { createAcceleratedAnimation } from '../animators/waapi/create-accelerated-animation.mjs';
import { createInstantAnimation } from '../animators/instant.mjs';
import { getDefaultTransition } from '../utils/default-transitions.mjs';
import { isAnimatable } from '../utils/is-animatable.mjs';
import { getKeyframes } from '../utils/keyframes.mjs';
import { getValueTransition, isTransitionDefined } from '../utils/transitions.mjs';
import { animateValue } from '../animators/js/index.mjs';
import { MotionGlobalConfig } from '../../utils/GlobalConfig.mjs';
const animateMotionValue = (valueName, value, target, transition = {}) => {
return (onComplete) => {
const valueTransition = getValueTransition(transition, valueName) || {};
/**
* Most transition values are currently completely overwritten by value-specific
* transitions. In the future it'd be nicer to blend these transitions. But for now
* delay actually does inherit from the root transition if not value-specific.
*/
const delay = valueTransition.delay || transition.delay || 0;
/**
* Elapsed isn't a public transition option but can be passed through from
* optimized appear effects in milliseconds.
*/
let { elapsed = 0 } = transition;
elapsed = elapsed - secondsToMilliseconds(delay);
const keyframes = getKeyframes(value, valueName, target, valueTransition);
/**
* Check if we're able to animate between the start and end keyframes,
* and throw a warning if we're attempting to animate between one that's
* animatable and another that isn't.
*/
const originKeyframe = keyframes[0];
const targetKeyframe = keyframes[keyframes.length - 1];
const isOriginAnimatable = isAnimatable(valueName, originKeyframe);
const isTargetAnimatable = isAnimatable(valueName, targetKeyframe);
warning(isOriginAnimatable === isTargetAnimatable, `You are trying to animate ${valueName} from "${originKeyframe}" to "${targetKeyframe}". ${originKeyframe} is not an animatable value - to enable this animation set ${originKeyframe} to a value animatable to ${targetKeyframe} via the \`style\` property.`);
let options = {
keyframes,
velocity: value.getVelocity(),
ease: "easeOut",
...valueTransition,
delay: -elapsed,
onUpdate: (v) => {
value.set(v);
valueTransition.onUpdate && valueTransition.onUpdate(v);
},
onComplete: () => {
onComplete();
valueTransition.onComplete && valueTransition.onComplete();
},
};
/**
* If there's no transition defined for this value, we can generate
* unqiue transition settings for this value.
*/
if (!isTransitionDefined(valueTransition)) {
options = {
...options,
...getDefaultTransition(valueName, options),
};
}
/**
* Both WAAPI and our internal animation functions use durations
* as defined by milliseconds, while our external API defines them
* as seconds.
*/
if (options.duration) {
options.duration = secondsToMilliseconds(options.duration);
}
if (options.repeatDelay) {
options.repeatDelay = secondsToMilliseconds(options.repeatDelay);
}
if (!isOriginAnimatable ||
!isTargetAnimatable ||
instantAnimationState.current ||
valueTransition.type === false ||
MotionGlobalConfig.skipAnimations) {
/**
* If we can't animate this value, or the global instant animation flag is set,
* or this is simply defined as an instant transition, return an instant transition.
*/
return createInstantAnimation(instantAnimationState.current
? { ...options, delay: 0 }
: options);
}
/**
* Animate via WAAPI if possible.
*/
if (
/**
* 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.
*/
!transition.isHandoff &&
value.owner &&
value.owner.current instanceof HTMLElement &&
/**
* If we're outputting values to onUpdate then we can't use WAAPI as there's
* no way to read the value from WAAPI every frame.
*/
!value.owner.getProps().onUpdate) {
const acceleratedAnimation = createAcceleratedAnimation(value, valueName, options);
if (acceleratedAnimation)
return acceleratedAnimation;
}
/**
* If we didn't create an accelerated animation, create a JS animation
*/
return animateValue(options);
};
};
export { animateMotionValue };

View File

@@ -0,0 +1,11 @@
import { animateMotionValue } from './motion-value.mjs';
import { motionValue } from '../../value/index.mjs';
import { isMotionValue } from '../../value/utils/is-motion-value.mjs';
function animateSingleValue(value, keyframes, options) {
const motionValue$1 = isMotionValue(value) ? value : motionValue(value);
motionValue$1.start(animateMotionValue("", motionValue$1, keyframes, options));
return motionValue$1.animation;
}
export { animateSingleValue };

View File

@@ -0,0 +1,103 @@
import { transformProps } from '../../render/html/utils/transform.mjs';
import { optimizedAppearDataAttribute } from '../optimized-appear/data-id.mjs';
import { animateMotionValue } from './motion-value.mjs';
import { isWillChangeMotionValue } from '../../value/use-will-change/is.mjs';
import { setTarget } from '../../render/utils/setters.mjs';
import { getValueTransition } from '../utils/transitions.mjs';
import { frame } from '../../frameloop/frame.mjs';
/**
* Decide whether we should block this animation. Previously, we achieved this
* just by checking whether the key was listed in protectedKeys, but this
* posed problems if an animation was triggered by afterChildren and protectedKeys
* had been set to true in the meantime.
*/
function shouldBlockAnimation({ protectedKeys, needsAnimating }, key) {
const shouldBlock = protectedKeys.hasOwnProperty(key) && needsAnimating[key] !== true;
needsAnimating[key] = false;
return shouldBlock;
}
function hasKeyframesChanged(value, target) {
const current = value.get();
if (Array.isArray(target)) {
for (let i = 0; i < target.length; i++) {
if (target[i] !== current)
return true;
}
}
else {
return current !== target;
}
}
function animateTarget(visualElement, definition, { delay = 0, transitionOverride, type } = {}) {
let { transition = visualElement.getDefaultTransition(), transitionEnd, ...target } = visualElement.makeTargetAnimatable(definition);
const willChange = visualElement.getValue("willChange");
if (transitionOverride)
transition = transitionOverride;
const animations = [];
const animationTypeState = type &&
visualElement.animationState &&
visualElement.animationState.getState()[type];
for (const key in target) {
const value = visualElement.getValue(key);
const valueTarget = target[key];
if (!value ||
valueTarget === undefined ||
(animationTypeState &&
shouldBlockAnimation(animationTypeState, key))) {
continue;
}
const valueTransition = {
delay,
elapsed: 0,
...getValueTransition(transition || {}, key),
};
/**
* If this is the first time a value is being animated, check
* to see if we're handling off from an existing animation.
*/
if (window.HandoffAppearAnimations) {
const appearId = visualElement.getProps()[optimizedAppearDataAttribute];
if (appearId) {
const elapsed = window.HandoffAppearAnimations(appearId, key, value, frame);
if (elapsed !== null) {
valueTransition.elapsed = elapsed;
valueTransition.isHandoff = true;
}
}
}
let canSkip = !valueTransition.isHandoff &&
!hasKeyframesChanged(value, valueTarget);
if (valueTransition.type === "spring" &&
(value.getVelocity() || valueTransition.velocity)) {
canSkip = false;
}
/**
* Temporarily disable skipping animations if there's an animation in
* progress. Better would be to track the current target of a value
* and compare that against valueTarget.
*/
if (value.animation) {
canSkip = false;
}
if (canSkip)
continue;
value.start(animateMotionValue(key, value, valueTarget, visualElement.shouldReduceMotion && transformProps.has(key)
? { type: false }
: valueTransition));
const animation = value.animation;
if (isWillChangeMotionValue(willChange)) {
willChange.add(key);
animation.then(() => willChange.remove(key));
}
animations.push(animation);
}
if (transitionEnd) {
Promise.all(animations).then(() => {
transitionEnd && setTarget(visualElement, transitionEnd);
});
}
return animations;
}
export { animateTarget };

View File

@@ -0,0 +1,63 @@
import { resolveVariant } from '../../render/utils/resolve-dynamic-variants.mjs';
import { animateTarget } from './visual-element-target.mjs';
function animateVariant(visualElement, variant, options = {}) {
const resolved = resolveVariant(visualElement, variant, options.custom);
let { transition = visualElement.getDefaultTransition() || {} } = resolved || {};
if (options.transitionOverride) {
transition = options.transitionOverride;
}
/**
* If we have a variant, create a callback that runs it as an animation.
* Otherwise, we resolve a Promise immediately for a composable no-op.
*/
const getAnimation = resolved
? () => Promise.all(animateTarget(visualElement, resolved, options))
: () => Promise.resolve();
/**
* If we have children, create a callback that runs all their animations.
* Otherwise, we resolve a Promise immediately for a composable no-op.
*/
const getChildAnimations = visualElement.variantChildren && visualElement.variantChildren.size
? (forwardDelay = 0) => {
const { delayChildren = 0, staggerChildren, staggerDirection, } = transition;
return animateChildren(visualElement, variant, delayChildren + forwardDelay, staggerChildren, staggerDirection, options);
}
: () => Promise.resolve();
/**
* If the transition explicitly defines a "when" option, we need to resolve either
* this animation or all children animations before playing the other.
*/
const { when } = transition;
if (when) {
const [first, last] = when === "beforeChildren"
? [getAnimation, getChildAnimations]
: [getChildAnimations, getAnimation];
return first().then(() => last());
}
else {
return Promise.all([getAnimation(), getChildAnimations(options.delay)]);
}
}
function animateChildren(visualElement, variant, delayChildren = 0, staggerChildren = 0, staggerDirection = 1, options) {
const animations = [];
const maxStaggerDuration = (visualElement.variantChildren.size - 1) * staggerChildren;
const generateStaggerDuration = staggerDirection === 1
? (i = 0) => i * staggerChildren
: (i = 0) => maxStaggerDuration - i * staggerChildren;
Array.from(visualElement.variantChildren)
.sort(sortByTreeOrder)
.forEach((child, i) => {
child.notify("AnimationStart", variant);
animations.push(animateVariant(child, variant, {
...options,
delay: delayChildren + generateStaggerDuration(i),
}).then(() => child.notify("AnimationComplete", variant)));
});
return Promise.all(animations);
}
function sortByTreeOrder(a, b) {
return a.sortNodePosition(b);
}
export { animateVariant, sortByTreeOrder };

View File

@@ -0,0 +1,24 @@
import { resolveVariant } from '../../render/utils/resolve-dynamic-variants.mjs';
import { animateTarget } from './visual-element-target.mjs';
import { animateVariant } from './visual-element-variant.mjs';
function animateVisualElement(visualElement, definition, options = {}) {
visualElement.notify("AnimationStart", definition);
let animation;
if (Array.isArray(definition)) {
const animations = definition.map((variant) => animateVariant(visualElement, variant, options));
animation = Promise.all(animations);
}
else if (typeof definition === "string") {
animation = animateVariant(visualElement, definition, options);
}
else {
const resolvedDefinition = typeof definition === "function"
? resolveVariant(visualElement, definition, options.custom)
: definition;
animation = Promise.all(animateTarget(visualElement, resolvedDefinition, options));
}
return animation.then(() => visualElement.notify("AnimationComplete", definition));
}
export { animateVisualElement };

View File

@@ -0,0 +1,6 @@
import { camelToDash } from '../../render/dom/utils/camel-to-dash.mjs';
const optimizedAppearDataId = "framerAppearId";
const optimizedAppearDataAttribute = "data-" + camelToDash(optimizedAppearDataId);
export { optimizedAppearDataAttribute, optimizedAppearDataId };

View File

@@ -0,0 +1,62 @@
import { transformProps } from '../../render/html/utils/transform.mjs';
import { appearAnimationStore } from './store.mjs';
import { appearStoreId } from './store-id.mjs';
let handoffFrameTime;
function handoffOptimizedAppearAnimation(elementId, valueName,
/**
* Legacy arguments. This function is inlined as part of SSG so it can be there's
* a version mismatch between the main included Motion and the inlined script.
*
* Remove in early 2024.
*/
_value, _frame) {
const optimisedValueName = transformProps.has(valueName)
? "transform"
: valueName;
const storeId = appearStoreId(elementId, optimisedValueName);
const optimisedAnimation = appearAnimationStore.get(storeId);
if (!optimisedAnimation) {
return null;
}
const { animation, startTime } = optimisedAnimation;
const cancelAnimation = () => {
appearAnimationStore.delete(storeId);
try {
animation.cancel();
}
catch (error) { }
};
/**
* If the startTime is null, this animation is the Paint Ready detection animation
* and we can cancel it immediately without handoff.
*
* Or if we've already handed off the animation then we're now interrupting it.
* In which case we need to cancel it.
*/
if (startTime === null || window.HandoffComplete) {
cancelAnimation();
return null;
}
else {
/**
* Otherwise we're handing off this animation to the main thread.
*
* Record the time of the first handoff. We call performance.now() once
* here and once in startOptimisedAnimation to ensure we're getting
* close to a frame-locked time. This keeps all animations in sync.
*/
if (handoffFrameTime === undefined) {
handoffFrameTime = performance.now();
}
/**
* We use main thread timings vs those returned by Animation.currentTime as it
* can be the case, particularly in Firefox, that currentTime doesn't return
* an updated value for several frames, even as the animation plays smoothly via
* the GPU.
*/
return handoffFrameTime - startTime || 0;
}
}
export { handoffOptimizedAppearAnimation };

View File

@@ -0,0 +1,71 @@
import { appearStoreId } from './store-id.mjs';
import { animateStyle } from '../animators/waapi/index.mjs';
import { optimizedAppearDataId } from './data-id.mjs';
import { handoffOptimizedAppearAnimation } from './handoff.mjs';
import { appearAnimationStore } from './store.mjs';
import { noop } from '../../utils/noop.mjs';
/**
* A single time to use across all animations to manually set startTime
* and ensure they're all in sync.
*/
let startFrameTime;
/**
* A dummy animation to detect when Chrome is ready to start
* painting the page and hold off from triggering the real animation
* until then. We only need one animation to detect paint ready.
*
* https://bugs.chromium.org/p/chromium/issues/detail?id=1406850
*/
let readyAnimation;
function startOptimizedAppearAnimation(element, name, keyframes, options, onReady) {
// Prevent optimised appear animations if Motion has already started animating.
if (window.HandoffComplete) {
window.HandoffAppearAnimations = undefined;
return;
}
const id = element.dataset[optimizedAppearDataId];
if (!id)
return;
window.HandoffAppearAnimations = handoffOptimizedAppearAnimation;
const storeId = appearStoreId(id, name);
if (!readyAnimation) {
readyAnimation = animateStyle(element, name, [keyframes[0], keyframes[0]],
/**
* 10 secs is basically just a super-safe duration to give Chrome
* long enough to get the animation ready.
*/
{ duration: 10000, ease: "linear" });
appearAnimationStore.set(storeId, {
animation: readyAnimation,
startTime: null,
});
}
const startAnimation = () => {
readyAnimation.cancel();
const appearAnimation = animateStyle(element, name, keyframes, options);
/**
* Record the time of the first started animation. We call performance.now() once
* here and once in handoff to ensure we're getting
* close to a frame-locked time. This keeps all animations in sync.
*/
if (startFrameTime === undefined) {
startFrameTime = performance.now();
}
appearAnimation.startTime = startFrameTime;
appearAnimationStore.set(storeId, {
animation: appearAnimation,
startTime: startFrameTime,
});
if (onReady)
onReady(appearAnimation);
};
if (readyAnimation.ready) {
readyAnimation.ready.then(startAnimation).catch(noop);
}
else {
startAnimation();
}
}
export { startOptimizedAppearAnimation };

View File

@@ -0,0 +1,3 @@
const appearStoreId = (id, value) => `${id}: ${value}`;
export { appearStoreId };

View File

@@ -0,0 +1,3 @@
const appearAnimationStore = new Map();
export { appearAnimationStore };

View File

@@ -0,0 +1,227 @@
import { createGeneratorEasing } from '../../easing/utils/create-generator-easing.mjs';
import { resolveElements } from '../../render/dom/utils/resolve-element.mjs';
import { defaultOffset } from '../../utils/offsets/default.mjs';
import { fillOffset } from '../../utils/offsets/fill.mjs';
import { progress } from '../../utils/progress.mjs';
import { secondsToMilliseconds } from '../../utils/time-conversion.mjs';
import { isMotionValue } from '../../value/utils/is-motion-value.mjs';
import { calcNextTime } from './utils/calc-time.mjs';
import { addKeyframes } from './utils/edit.mjs';
import { compareByTime } from './utils/sort.mjs';
const defaultSegmentEasing = "easeInOut";
function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...sequenceTransition } = {}, scope) {
const defaultDuration = defaultTransition.duration || 0.3;
const animationDefinitions = new Map();
const sequences = new Map();
const elementCache = {};
const timeLabels = new Map();
let prevTime = 0;
let currentTime = 0;
let totalDuration = 0;
/**
* Build the timeline by mapping over the sequence array and converting
* the definitions into keyframes and offsets with absolute time values.
* These will later get converted into relative offsets in a second pass.
*/
for (let i = 0; i < sequence.length; i++) {
const segment = sequence[i];
/**
* If this is a timeline label, mark it and skip the rest of this iteration.
*/
if (typeof segment === "string") {
timeLabels.set(segment, currentTime);
continue;
}
else if (!Array.isArray(segment)) {
timeLabels.set(segment.name, calcNextTime(currentTime, segment.at, prevTime, timeLabels));
continue;
}
let [subject, keyframes, transition = {}] = segment;
/**
* If a relative or absolute time value has been specified we need to resolve
* it in relation to the currentTime.
*/
if (transition.at !== undefined) {
currentTime = calcNextTime(currentTime, transition.at, prevTime, timeLabels);
}
/**
* Keep track of the maximum duration in this definition. This will be
* applied to currentTime once the definition has been parsed.
*/
let maxDuration = 0;
const resolveValueSequence = (valueKeyframes, valueTransition, valueSequence, elementIndex = 0, numElements = 0) => {
const valueKeyframesAsList = keyframesAsList(valueKeyframes);
const { delay = 0, times = defaultOffset(valueKeyframesAsList), type = "keyframes", ...remainingTransition } = valueTransition;
let { ease = defaultTransition.ease || "easeOut", duration } = valueTransition;
/**
* Resolve stagger() if defined.
*/
const calculatedDelay = typeof delay === "function"
? delay(elementIndex, numElements)
: delay;
/**
* If this animation should and can use a spring, generate a spring easing function.
*/
const numKeyframes = valueKeyframesAsList.length;
if (numKeyframes <= 2 && type === "spring") {
/**
* As we're creating an easing function from a spring,
* ideally we want to generate it using the real distance
* between the two keyframes. However this isn't always
* possible - in these situations we use 0-100.
*/
let absoluteDelta = 100;
if (numKeyframes === 2 &&
isNumberKeyframesArray(valueKeyframesAsList)) {
const delta = valueKeyframesAsList[1] - valueKeyframesAsList[0];
absoluteDelta = Math.abs(delta);
}
const springTransition = { ...remainingTransition };
if (duration !== undefined) {
springTransition.duration = secondsToMilliseconds(duration);
}
const springEasing = createGeneratorEasing(springTransition, absoluteDelta);
ease = springEasing.ease;
duration = springEasing.duration;
}
duration !== null && duration !== void 0 ? duration : (duration = defaultDuration);
const startTime = currentTime + calculatedDelay;
const targetTime = startTime + duration;
/**
* If there's only one time offset of 0, fill in a second with length 1
*/
if (times.length === 1 && times[0] === 0) {
times[1] = 1;
}
/**
* Fill out if offset if fewer offsets than keyframes
*/
const remainder = times.length - valueKeyframesAsList.length;
remainder > 0 && fillOffset(times, remainder);
/**
* If only one value has been set, ie [1], push a null to the start of
* the keyframe array. This will let us mark a keyframe at this point
* that will later be hydrated with the previous value.
*/
valueKeyframesAsList.length === 1 &&
valueKeyframesAsList.unshift(null);
/**
* Add keyframes, mapping offsets to absolute time.
*/
addKeyframes(valueSequence, valueKeyframesAsList, ease, times, startTime, targetTime);
maxDuration = Math.max(calculatedDelay + duration, maxDuration);
totalDuration = Math.max(targetTime, totalDuration);
};
if (isMotionValue(subject)) {
const subjectSequence = getSubjectSequence(subject, sequences);
resolveValueSequence(keyframes, transition, getValueSequence("default", subjectSequence));
}
else {
/**
* Find all the elements specified in the definition and parse value
* keyframes from their timeline definitions.
*/
const elements = resolveElements(subject, scope, elementCache);
const numElements = elements.length;
/**
* For every element in this segment, process the defined values.
*/
for (let elementIndex = 0; elementIndex < numElements; elementIndex++) {
/**
* Cast necessary, but we know these are of this type
*/
keyframes = keyframes;
transition = transition;
const element = elements[elementIndex];
const subjectSequence = getSubjectSequence(element, sequences);
for (const key in keyframes) {
resolveValueSequence(keyframes[key], getValueTransition(transition, key), getValueSequence(key, subjectSequence), elementIndex, numElements);
}
}
}
prevTime = currentTime;
currentTime += maxDuration;
}
/**
* For every element and value combination create a new animation.
*/
sequences.forEach((valueSequences, element) => {
for (const key in valueSequences) {
const valueSequence = valueSequences[key];
/**
* Arrange all the keyframes in ascending time order.
*/
valueSequence.sort(compareByTime);
const keyframes = [];
const valueOffset = [];
const valueEasing = [];
/**
* For each keyframe, translate absolute times into
* relative offsets based on the total duration of the timeline.
*/
for (let i = 0; i < valueSequence.length; i++) {
const { at, value, easing } = valueSequence[i];
keyframes.push(value);
valueOffset.push(progress(0, totalDuration, at));
valueEasing.push(easing || "easeOut");
}
/**
* If the first keyframe doesn't land on offset: 0
* provide one by duplicating the initial keyframe. This ensures
* it snaps to the first keyframe when the animation starts.
*/
if (valueOffset[0] !== 0) {
valueOffset.unshift(0);
keyframes.unshift(keyframes[0]);
valueEasing.unshift(defaultSegmentEasing);
}
/**
* If the last keyframe doesn't land on offset: 1
* provide one with a null wildcard value. This will ensure it
* stays static until the end of the animation.
*/
if (valueOffset[valueOffset.length - 1] !== 1) {
valueOffset.push(1);
keyframes.push(null);
}
if (!animationDefinitions.has(element)) {
animationDefinitions.set(element, {
keyframes: {},
transition: {},
});
}
const definition = animationDefinitions.get(element);
definition.keyframes[key] = keyframes;
definition.transition[key] = {
...defaultTransition,
duration: totalDuration,
ease: valueEasing,
times: valueOffset,
...sequenceTransition,
};
}
});
return animationDefinitions;
}
function getSubjectSequence(subject, sequences) {
!sequences.has(subject) && sequences.set(subject, {});
return sequences.get(subject);
}
function getValueSequence(name, sequences) {
if (!sequences[name])
sequences[name] = [];
return sequences[name];
}
function keyframesAsList(keyframes) {
return Array.isArray(keyframes) ? keyframes : [keyframes];
}
function getValueTransition(transition, key) {
return transition[key]
? { ...transition, ...transition[key] }
: { ...transition };
}
const isNumber = (keyframe) => typeof keyframe === "number";
const isNumberKeyframesArray = (keyframes) => keyframes.every(isNumber);
export { createAnimationsFromSequence, getValueTransition };

View File

@@ -0,0 +1,21 @@
/**
* Given a absolute or relative time definition and current/prev time state of the sequence,
* calculate an absolute time for the next keyframes.
*/
function calcNextTime(current, next, prev, labels) {
var _a;
if (typeof next === "number") {
return next;
}
else if (next.startsWith("-") || next.startsWith("+")) {
return Math.max(0, current + parseFloat(next));
}
else if (next === "<") {
return prev;
}
else {
return (_a = labels.get(next)) !== null && _a !== void 0 ? _a : current;
}
}
export { calcNextTime };

View File

@@ -0,0 +1,31 @@
import { getEasingForSegment } from '../../../easing/utils/get-easing-for-segment.mjs';
import { removeItem } from '../../../utils/array.mjs';
import { mix } from '../../../utils/mix.mjs';
function eraseKeyframes(sequence, startTime, endTime) {
for (let i = 0; i < sequence.length; i++) {
const keyframe = sequence[i];
if (keyframe.at > startTime && keyframe.at < endTime) {
removeItem(sequence, keyframe);
// If we remove this item we have to push the pointer back one
i--;
}
}
}
function addKeyframes(sequence, keyframes, easing, offset, startTime, endTime) {
/**
* Erase every existing value between currentTime and targetTime,
* this will essentially splice this timeline into any currently
* defined ones.
*/
eraseKeyframes(sequence, startTime, endTime);
for (let i = 0; i < keyframes.length; i++) {
sequence.push({
value: keyframes[i],
at: mix(startTime, endTime, offset[i]),
easing: getEasingForSegment(easing, i),
});
}
}
export { addKeyframes, eraseKeyframes };

View File

@@ -0,0 +1,14 @@
function compareByTime(a, b) {
if (a.at === b.at) {
if (a.value === null)
return 1;
if (b.value === null)
return -1;
return 0;
}
else {
return a.at - b.at;
}
}
export { compareByTime };

View File

@@ -0,0 +1,32 @@
import { isSVGElement } from '../../render/dom/utils/is-svg-element.mjs';
import { SVGVisualElement } from '../../render/svg/SVGVisualElement.mjs';
import { HTMLVisualElement } from '../../render/html/HTMLVisualElement.mjs';
import { visualElementStore } from '../../render/store.mjs';
function createVisualElement(element) {
const options = {
presenceContext: null,
props: {},
visualState: {
renderState: {
transform: {},
transformOrigin: {},
style: {},
vars: {},
attrs: {},
},
latestValues: {},
},
};
const node = isSVGElement(element)
? new SVGVisualElement(options, {
enableHardwareAcceleration: false,
})
: new HTMLVisualElement(options, {
enableHardwareAcceleration: true,
});
node.mount(element);
visualElementStore.set(element, node);
}
export { createVisualElement };

View File

@@ -0,0 +1,40 @@
import { transformProps } from '../../render/html/utils/transform.mjs';
const underDampedSpring = {
type: "spring",
stiffness: 500,
damping: 25,
restSpeed: 10,
};
const criticallyDampedSpring = (target) => ({
type: "spring",
stiffness: 550,
damping: target === 0 ? 2 * Math.sqrt(550) : 30,
restSpeed: 10,
});
const keyframesTransition = {
type: "keyframes",
duration: 0.8,
};
/**
* Default easing curve is a slightly shallower version of
* the default browser easing curve.
*/
const ease = {
type: "keyframes",
ease: [0.25, 0.1, 0.35, 1],
duration: 0.3,
};
const getDefaultTransition = (valueKey, { keyframes }) => {
if (keyframes.length > 2) {
return keyframesTransition;
}
else if (transformProps.has(valueKey)) {
return valueKey.startsWith("scale")
? criticallyDampedSpring(keyframes[1])
: underDampedSpring;
}
return ease;
};
export { getDefaultTransition };

View File

@@ -0,0 +1,30 @@
import { complex } from '../../value/types/complex/index.mjs';
/**
* Check if a value is animatable. Examples:
*
* ✅: 100, "100px", "#fff"
* ❌: "block", "url(2.jpg)"
* @param value
*
* @internal
*/
const isAnimatable = (key, value) => {
// If the list of keys tat might be non-animatable grows, replace with Set
if (key === "zIndex")
return false;
// If it's a number or a keyframes array, we can animate it. We might at some point
// need to do a deep isAnimatable check of keyframes, or let Popmotion handle this,
// but for now lets leave it like this for performance reasons
if (typeof value === "number" || Array.isArray(value))
return true;
if (typeof value === "string" && // It's animatable if we have a string
(complex.test(value) || value === "0") && // And it contains numbers and/or colors
!value.startsWith("url(") // Unless it starts with "url("
) {
return true;
}
return false;
};
export { isAnimatable };

View File

@@ -0,0 +1,7 @@
function isAnimationControls(v) {
return (v !== null &&
typeof v === "object" &&
typeof v.start === "function");
}
export { isAnimationControls };

View File

@@ -0,0 +1,5 @@
function isDOMKeyframes(keyframes) {
return typeof keyframes === "object" && !Array.isArray(keyframes);
}
export { isDOMKeyframes };

View File

@@ -0,0 +1,5 @@
const isKeyframesTarget = (v) => {
return Array.isArray(v);
};
export { isKeyframesTarget };

View File

@@ -0,0 +1,12 @@
import { isZeroValueString } from '../../utils/is-zero-value-string.mjs';
function isNone(value) {
if (typeof value === "number") {
return value === 0;
}
else if (value !== null) {
return value === "none" || value === "0" || isZeroValueString(value);
}
}
export { isNone };

View File

@@ -0,0 +1,45 @@
import { getAnimatableNone } from '../../render/dom/value-types/animatable-none.mjs';
import { isAnimatable } from './is-animatable.mjs';
import { isNone } from './is-none.mjs';
function getKeyframes(value, valueName, target, transition) {
const isTargetAnimatable = isAnimatable(valueName, target);
let keyframes;
if (Array.isArray(target)) {
keyframes = [...target];
}
else {
keyframes = [null, target];
}
const defaultOrigin = transition.from !== undefined ? transition.from : value.get();
let animatableTemplateValue = undefined;
const noneKeyframeIndexes = [];
for (let i = 0; i < keyframes.length; i++) {
/**
* Fill null/wildcard keyframes
*/
if (keyframes[i] === null) {
keyframes[i] = i === 0 ? defaultOrigin : keyframes[i - 1];
}
if (isNone(keyframes[i])) {
noneKeyframeIndexes.push(i);
}
// TODO: Clean this conditional, it works for now
if (typeof keyframes[i] === "string" &&
keyframes[i] !== "none" &&
keyframes[i] !== "0") {
animatableTemplateValue = keyframes[i];
}
}
if (isTargetAnimatable &&
noneKeyframeIndexes.length &&
animatableTemplateValue) {
for (let i = 0; i < noneKeyframeIndexes.length; i++) {
const index = noneKeyframeIndexes[i];
keyframes[index] = getAnimatableNone(valueName, animatableTemplateValue);
}
}
return keyframes;
}
export { getKeyframes };

View File

@@ -0,0 +1,26 @@
import { easingDefinitionToFunction } from '../../easing/utils/map.mjs';
function getOriginIndex(from, total) {
if (from === "first") {
return 0;
}
else {
const lastIndex = total - 1;
return from === "last" ? lastIndex : lastIndex / 2;
}
}
function stagger(duration = 0.1, { startDelay = 0, from = 0, ease } = {}) {
return (i, total) => {
const fromIndex = typeof from === "number" ? from : getOriginIndex(from, total);
const distance = Math.abs(fromIndex - i);
let delay = duration * distance;
if (ease) {
const maxDelay = total * duration;
const easingFunction = easingDefinitionToFunction(ease);
delay = easingFunction(delay / maxDelay) * maxDelay;
}
return startDelay + delay;
};
}
export { getOriginIndex, stagger };

View File

@@ -0,0 +1,13 @@
/**
* Decide whether a transition is defined on a given Transition.
* This filters out orchestration options and returns true
* if any options are left.
*/
function isTransitionDefined({ when, delay: _delay, delayChildren, staggerChildren, staggerDirection, repeat, repeatType, repeatDelay, from, elapsed, ...transition }) {
return !!Object.keys(transition).length;
}
function getValueTransition(transition, key) {
return transition[key] || transition["default"] || transition;
}
export { getValueTransition, isTransitionDefined };