main repo

This commit is contained in:
Basilosaurusrex
2025-11-24 18:09:40 +01:00
parent b636ee5e70
commit f027651f9b
34146 changed files with 4436636 additions and 0 deletions

4807
node_modules/motion-dom/dist/cjs/index.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,178 @@
import { MotionGlobalConfig, noop } from 'motion-utils';
import { time } from '../frameloop/sync-time.mjs';
import { JSAnimation } from './JSAnimation.mjs';
import { getFinalKeyframe } from './keyframes/get-final.mjs';
import { KeyframeResolver, flushKeyframeResolvers } from './keyframes/KeyframesResolver.mjs';
import { NativeAnimationExtended } from './NativeAnimationExtended.mjs';
import { canAnimate } from './utils/can-animate.mjs';
import { makeAnimationInstant } from './utils/make-animation-instant.mjs';
import { WithPromise } from './utils/WithPromise.mjs';
import { supportsBrowserAnimation } from './waapi/supports/waapi.mjs';
/**
* Maximum time allowed between an animation being created and it being
* resolved for us to use the latter as the start time.
*
* This is to ensure that while we prefer to "start" an animation as soon
* as it's triggered, we also want to avoid a visual jump if there's a big delay
* between these two moments.
*/
const MAX_RESOLVE_DELAY = 40;
class AsyncMotionValueAnimation extends WithPromise {
constructor({ autoplay = true, delay = 0, type = "keyframes", repeat = 0, repeatDelay = 0, repeatType = "loop", keyframes, name, motionValue, element, ...options }) {
super();
/**
* Bound to support return animation.stop pattern
*/
this.stop = () => {
if (this._animation) {
this._animation.stop();
this.stopTimeline?.();
}
this.keyframeResolver?.cancel();
};
this.createdAt = time.now();
const optionsWithDefaults = {
autoplay,
delay,
type,
repeat,
repeatDelay,
repeatType,
name,
motionValue,
element,
...options,
};
const KeyframeResolver$1 = element?.KeyframeResolver || KeyframeResolver;
this.keyframeResolver = new KeyframeResolver$1(keyframes, (resolvedKeyframes, finalKeyframe, forced) => this.onKeyframesResolved(resolvedKeyframes, finalKeyframe, optionsWithDefaults, !forced), name, motionValue, element);
this.keyframeResolver?.scheduleResolve();
}
onKeyframesResolved(keyframes, finalKeyframe, options, sync) {
this.keyframeResolver = undefined;
const { name, type, velocity, delay, isHandoff, onUpdate } = options;
this.resolvedAt = time.now();
/**
* If we can't animate this value with the resolved keyframes
* then we should complete it immediately.
*/
if (!canAnimate(keyframes, name, type, velocity)) {
if (MotionGlobalConfig.instantAnimations || !delay) {
onUpdate?.(getFinalKeyframe(keyframes, options, finalKeyframe));
}
keyframes[0] = keyframes[keyframes.length - 1];
makeAnimationInstant(options);
options.repeat = 0;
}
/**
* Resolve startTime for the animation.
*
* This method uses the createdAt and resolvedAt to calculate the
* animation startTime. *Ideally*, we would use the createdAt time as t=0
* as the following frame would then be the first frame of the animation in
* progress, which would feel snappier.
*
* However, if there's a delay (main thread work) between the creation of
* the animation and the first commited frame, we prefer to use resolvedAt
* to avoid a sudden jump into the animation.
*/
const startTime = sync
? !this.resolvedAt
? this.createdAt
: this.resolvedAt - this.createdAt > MAX_RESOLVE_DELAY
? this.resolvedAt
: this.createdAt
: undefined;
const resolvedOptions = {
startTime,
finalKeyframe,
...options,
keyframes,
};
/**
* Animate via WAAPI if possible. If this is a handoff animation, the optimised animation will be running via
* WAAPI. Therefore, this animation must be JS to ensure it runs "under" the
* optimised animation.
*/
const animation = !isHandoff && supportsBrowserAnimation(resolvedOptions)
? new NativeAnimationExtended({
...resolvedOptions,
element: resolvedOptions.motionValue.owner.current,
})
: new JSAnimation(resolvedOptions);
animation.finished.then(() => this.notifyFinished()).catch(noop);
if (this.pendingTimeline) {
this.stopTimeline = animation.attachTimeline(this.pendingTimeline);
this.pendingTimeline = undefined;
}
this._animation = animation;
}
get finished() {
if (!this._animation) {
return this._finished;
}
else {
return this.animation.finished;
}
}
then(onResolve, _onReject) {
return this.finished.finally(onResolve).then(() => { });
}
get animation() {
if (!this._animation) {
this.keyframeResolver?.resume();
flushKeyframeResolvers();
}
return this._animation;
}
get duration() {
return this.animation.duration;
}
get iterationDuration() {
return this.animation.iterationDuration;
}
get time() {
return this.animation.time;
}
set time(newTime) {
this.animation.time = newTime;
}
get speed() {
return this.animation.speed;
}
get state() {
return this.animation.state;
}
set speed(newSpeed) {
this.animation.speed = newSpeed;
}
get startTime() {
return this.animation.startTime;
}
attachTimeline(timeline) {
if (this._animation) {
this.stopTimeline = this.animation.attachTimeline(timeline);
}
else {
this.pendingTimeline = timeline;
}
return () => this.stop();
}
play() {
this.animation.play();
}
pause() {
this.animation.pause();
}
complete() {
this.animation.complete();
}
cancel() {
if (this._animation) {
this.animation.cancel();
}
this.keyframeResolver?.cancel();
}
}
export { AsyncMotionValueAnimation };

View File

@@ -0,0 +1,81 @@
class GroupAnimation {
constructor(animations) {
// Bound to accomadate common `return animation.stop` pattern
this.stop = () => this.runAll("stop");
this.animations = animations.filter(Boolean);
}
get finished() {
return Promise.all(this.animations.map((animation) => animation.finished));
}
/**
* 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 subscriptions = this.animations.map((animation) => animation.attachTimeline(timeline));
return () => {
subscriptions.forEach((cancel, i) => {
cancel && cancel();
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 state() {
return this.getAll("state");
}
get startTime() {
return this.getAll("startTime");
}
get duration() {
return getMax(this.animations, "duration");
}
get iterationDuration() {
return getMax(this.animations, "iterationDuration");
}
runAll(methodName) {
this.animations.forEach((controls) => controls[methodName]());
}
play() {
this.runAll("play");
}
pause() {
this.runAll("pause");
}
cancel() {
this.runAll("cancel");
}
complete() {
this.runAll("complete");
}
}
function getMax(animations, propName) {
let max = 0;
for (let i = 0; i < animations.length; i++) {
const value = animations[i][propName];
if (value !== null && value > max) {
max = value;
}
}
return max;
}
export { GroupAnimation };

View File

@@ -0,0 +1,9 @@
import { GroupAnimation } from './GroupAnimation.mjs';
class GroupAnimationWithThen extends GroupAnimation {
then(onResolve, _onReject) {
return this.finished.finally(onResolve).then(() => { });
}
}
export { GroupAnimationWithThen };

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

View File

@@ -0,0 +1,160 @@
import { invariant, millisecondsToSeconds, secondsToMilliseconds, noop } from 'motion-utils';
import { setStyle } from '../render/dom/style-set.mjs';
import { supportsScrollTimeline } from '../utils/supports/scroll-timeline.mjs';
import { getFinalKeyframe } from './keyframes/get-final.mjs';
import { WithPromise } from './utils/WithPromise.mjs';
import { startWaapiAnimation } from './waapi/start-waapi-animation.mjs';
import { applyGeneratorOptions } from './waapi/utils/apply-generator.mjs';
/**
* NativeAnimation implements AnimationPlaybackControls for the browser's Web Animations API.
*/
class NativeAnimation extends WithPromise {
constructor(options) {
super();
this.finishedTime = null;
this.isStopped = false;
if (!options)
return;
const { element, name, keyframes, pseudoElement, allowFlatten = false, finalKeyframe, onComplete, } = options;
this.isPseudoElement = Boolean(pseudoElement);
this.allowFlatten = allowFlatten;
this.options = options;
invariant(typeof options.type !== "string", `Mini animate() doesn't support "type" as a string.`, "mini-spring");
const transition = applyGeneratorOptions(options);
this.animation = startWaapiAnimation(element, name, keyframes, transition, pseudoElement);
if (transition.autoplay === false) {
this.animation.pause();
}
this.animation.onfinish = () => {
this.finishedTime = this.time;
if (!pseudoElement) {
const keyframe = getFinalKeyframe(keyframes, this.options, finalKeyframe, this.speed);
if (this.updateMotionValue) {
this.updateMotionValue(keyframe);
}
else {
/**
* If we can, we want to commit the final style as set by the user,
* rather than the computed keyframe value supplied by the animation.
*/
setStyle(element, name, keyframe);
}
this.animation.cancel();
}
onComplete?.();
this.notifyFinished();
};
}
play() {
if (this.isStopped)
return;
this.animation.play();
if (this.state === "finished") {
this.updateFinished();
}
}
pause() {
this.animation.pause();
}
complete() {
this.animation.finish?.();
}
cancel() {
try {
this.animation.cancel();
}
catch (e) { }
}
stop() {
if (this.isStopped)
return;
this.isStopped = true;
const { state } = this;
if (state === "idle" || state === "finished") {
return;
}
if (this.updateMotionValue) {
this.updateMotionValue();
}
else {
this.commitStyles();
}
if (!this.isPseudoElement)
this.cancel();
}
/**
* WAAPI doesn't natively have any interruption capabilities.
*
* In this method, we commit styles back to the DOM before cancelling
* the animation.
*
* This is designed to be overridden by NativeAnimationExtended, which
* will create a renderless JS animation and sample it twice to calculate
* its current value, "previous" value, and therefore allow
* Motion to also correctly calculate velocity for any subsequent animation
* while deferring the commit until the next animation frame.
*/
commitStyles() {
if (!this.isPseudoElement) {
this.animation.commitStyles?.();
}
}
get duration() {
const duration = this.animation.effect?.getComputedTiming?.().duration || 0;
return millisecondsToSeconds(Number(duration));
}
get iterationDuration() {
const { delay = 0 } = this.options || {};
return this.duration + millisecondsToSeconds(delay);
}
get time() {
return millisecondsToSeconds(Number(this.animation.currentTime) || 0);
}
set time(newTime) {
this.finishedTime = null;
this.animation.currentTime = secondsToMilliseconds(newTime);
}
/**
* The playback speed of the animation.
* 1 = normal speed, 2 = double speed, 0.5 = half speed.
*/
get speed() {
return this.animation.playbackRate;
}
set speed(newSpeed) {
// Allow backwards playback after finishing
if (newSpeed < 0)
this.finishedTime = null;
this.animation.playbackRate = newSpeed;
}
get state() {
return this.finishedTime !== null
? "finished"
: this.animation.playState;
}
get startTime() {
return Number(this.animation.startTime);
}
set startTime(newStartTime) {
this.animation.startTime = newStartTime;
}
/**
* Attaches a timeline to the animation, for instance the `ScrollTimeline`.
*/
attachTimeline({ timeline, observe }) {
if (this.allowFlatten) {
this.animation.effect?.updateTiming({ easing: "linear" });
}
this.animation.onfinish = null;
if (timeline && supportsScrollTimeline()) {
this.animation.timeline = timeline;
return noop;
}
else {
return observe(this);
}
}
}
export { NativeAnimation };

View File

@@ -0,0 +1,65 @@
import { secondsToMilliseconds } from 'motion-utils';
import { JSAnimation } from './JSAnimation.mjs';
import { NativeAnimation } from './NativeAnimation.mjs';
import { replaceTransitionType } from './utils/replace-transition-type.mjs';
import { replaceStringEasing } from './waapi/utils/unsupported-easing.mjs';
/**
* 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
class NativeAnimationExtended extends NativeAnimation {
constructor(options) {
/**
* The base NativeAnimation function only supports a subset
* of Motion easings, and WAAPI also only supports some
* easing functions via string/cubic-bezier definitions.
*
* This function replaces those unsupported easing functions
* with a JS easing function. This will later get compiled
* to a linear() easing function.
*/
replaceStringEasing(options);
/**
* Ensure we replace the transition type with a generator function
* before passing to WAAPI.
*
* TODO: Does this have a better home? It could be shared with
* JSAnimation.
*/
replaceTransitionType(options);
super(options);
if (options.startTime) {
this.startTime = options.startTime;
}
this.options = options;
}
/**
* 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.
*/
updateMotionValue(value) {
const { motionValue, onUpdate, onComplete, element, ...options } = this.options;
if (!motionValue)
return;
if (value !== undefined) {
motionValue.set(value);
return;
}
const sampleAnimation = new JSAnimation({
...options,
autoplay: false,
});
const sampleTime = secondsToMilliseconds(this.finishedTime ?? this.time);
motionValue.setWithVelocity(sampleAnimation.sample(sampleTime - sampleDelta).value, sampleAnimation.sample(sampleTime).value, sampleDelta);
sampleAnimation.stop();
}
}
export { NativeAnimationExtended };

View File

@@ -0,0 +1,14 @@
import { NativeAnimation } from './NativeAnimation.mjs';
class NativeAnimationWrapper extends NativeAnimation {
constructor(animation) {
super();
this.animation = animation;
animation.onfinish = () => {
this.finishedTime = this.time;
this.notifyFinished();
};
}
}
export { NativeAnimationWrapper };

View File

@@ -0,0 +1,17 @@
import { time } from '../../frameloop/sync-time.mjs';
import { frame, cancelFrame, frameData } from '../../frameloop/frame.mjs';
const frameloopDriver = (update) => {
const passTimestamp = ({ timestamp }) => update(timestamp);
return {
start: (keepAlive = true) => frame.update(passTimestamp, keepAlive),
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 : time.now()),
};
};
export { frameloopDriver };

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), // TODO: This should be passing * 1000
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,49 @@
import { easeInOut, isEasingArray, easingDefinitionToFunction } from 'motion-utils';
import { interpolate } from '../../utils/interpolate.mjs';
import { defaultOffset } from '../keyframes/offsets/default.mjs';
import { convertOffsetToTimes } from '../keyframes/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,27 @@
const springDefaults = {
// Default spring physics
stiffness: 100,
damping: 10,
mass: 1.0,
velocity: 0.0,
// Default duration/bounce-based options
duration: 800, // in ms
bounce: 0.3,
visualDuration: 0.3, // in seconds
// Rest thresholds
restSpeed: {
granular: 0.01,
default: 2,
},
restDelta: {
granular: 0.005,
default: 0.5,
},
// Limits
minDuration: 0.01, // in seconds
maxDuration: 10.0, // in seconds
minDamping: 0.05,
maxDamping: 1,
};
export { springDefaults };

View File

@@ -0,0 +1,84 @@
import { warning, secondsToMilliseconds, clamp, millisecondsToSeconds } from 'motion-utils';
import { springDefaults } from './defaults.mjs';
const safeMin = 0.001;
function findSpring({ duration = springDefaults.duration, bounce = springDefaults.bounce, velocity = springDefaults.velocity, mass = springDefaults.mass, }) {
let envelope;
let derivative;
warning(duration <= secondsToMilliseconds(springDefaults.maxDuration), "Spring duration must be 10 seconds or less", "spring-duration-limit");
let dampingRatio = 1 - bounce;
/**
* Restrict dampingRatio and duration to within acceptable ranges.
*/
dampingRatio = clamp(springDefaults.minDamping, springDefaults.maxDamping, dampingRatio);
duration = clamp(springDefaults.minDuration, springDefaults.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: springDefaults.stiffness,
damping: springDefaults.damping,
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 };

View File

@@ -0,0 +1,175 @@
import { millisecondsToSeconds, secondsToMilliseconds, clamp } from 'motion-utils';
import { generateLinearEasing } from '../../waapi/utils/linear.mjs';
import { calcGeneratorDuration, maxGeneratorDuration } from '../utils/calc-duration.mjs';
import { createGeneratorEasing } from '../utils/create-generator-easing.mjs';
import { calcGeneratorVelocity } from '../utils/velocity.mjs';
import { springDefaults } from './defaults.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: springDefaults.velocity,
stiffness: springDefaults.stiffness,
damping: springDefaults.damping,
mass: springDefaults.mass,
isResolvedFromDuration: false,
...options,
};
// stiffness/damping/mass overrides duration/bounce
if (!isSpringType(options, physicsKeys) &&
isSpringType(options, durationKeys)) {
if (options.visualDuration) {
const visualDuration = options.visualDuration;
const root = (2 * Math.PI) / (visualDuration * 1.2);
const stiffness = root * root;
const damping = 2 *
clamp(0.05, 1, 1 - (options.bounce || 0)) *
Math.sqrt(stiffness);
springOptions = {
...springOptions,
mass: springDefaults.mass,
stiffness,
damping,
};
}
else {
const derived = findSpring(options);
springOptions = {
...springOptions,
...derived,
mass: springDefaults.mass,
};
springOptions.isResolvedFromDuration = true;
}
}
return springOptions;
}
function spring(optionsOrVisualDuration = springDefaults.visualDuration, bounce = springDefaults.bounce) {
const options = typeof optionsOrVisualDuration !== "object"
? {
visualDuration: optionsOrVisualDuration,
keyframes: [0, 1],
bounce,
}
: optionsOrVisualDuration;
let { restSpeed, restDelta } = options;
const origin = options.keyframes[0];
const target = options.keyframes[options.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
? springDefaults.restSpeed.granular
: springDefaults.restSpeed.default);
restDelta || (restDelta = isGranularScale
? springDefaults.restDelta.granular
: springDefaults.restDelta.default);
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);
};
}
const generator = {
calculatedDuration: isResolvedFromDuration ? duration || null : null,
next: (t) => {
const current = resolveSpring(t);
if (!isResolvedFromDuration) {
let currentVelocity = t === 0 ? initialVelocity : 0.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 =
t === 0
? secondsToMilliseconds(initialVelocity)
: calcGeneratorVelocity(resolveSpring, t, current);
}
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;
},
toString: () => {
const calculatedDuration = Math.min(calcGeneratorDuration(generator), maxGeneratorDuration);
const easing = generateLinearEasing((progress) => generator.next(calculatedDuration * progress).value, calculatedDuration, 30);
return calculatedDuration + "ms " + easing;
},
toTransition: () => { },
};
return generator;
}
spring.applyToOptions = (options) => {
const generatorOptions = createGeneratorEasing(options, 100, spring);
options.ease = generatorOptions.ease;
options.duration = secondsToMilliseconds(generatorOptions.duration);
options.type = "keyframes";
return options;
};
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,19 @@
import { millisecondsToSeconds } from 'motion-utils';
import { calcGeneratorDuration, maxGeneratorDuration } from './calc-duration.mjs';
/**
* Create a progress => progress easing function from a generator.
*/
function createGeneratorEasing(options, scale = 100, createGenerator) {
const generator = createGenerator({ ...options, keyframes: [0, scale] });
const duration = Math.min(calcGeneratorDuration(generator), maxGeneratorDuration);
return {
type: "keyframes",
ease: (progress) => {
return generator.next(duration * progress).value / scale;
},
duration: millisecondsToSeconds(duration),
};
}
export { createGeneratorEasing };

View File

@@ -0,0 +1,5 @@
function isGenerator(type) {
return typeof type === "function" && "applyToOptions" in type;
}
export { isGenerator };

View File

@@ -0,0 +1,9 @@
import { velocityPerSecond } from 'motion-utils';
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,131 @@
import { positionalKeys } from '../../render/utils/keys-position.mjs';
import { findDimensionValueType } from '../../value/types/dimensions.mjs';
import { getVariableValue } from '../utils/css-variables-conversion.mjs';
import { isCSSVariableToken } from '../utils/is-css-variable.mjs';
import { KeyframeResolver } from './KeyframesResolver.mjs';
import { isNone } from './utils/is-none.mjs';
import { makeNoneKeyframesAnimatable } from './utils/make-none-animatable.mjs';
import { isNumOrPxType, positionalValues } from './utils/unit-conversion.mjs';
class DOMKeyframesResolver extends KeyframeResolver {
constructor(unresolvedKeyframes, onComplete, name, motionValue, element) {
super(unresolvedKeyframes, onComplete, name, motionValue, element, true);
}
readKeyframes() {
const { unresolvedKeyframes, element, name } = this;
if (!element || !element.current)
return;
super.readKeyframes();
/**
* If any keyframe is a CSS variable, we need to find its value by sampling the element
*/
for (let i = 0; i < unresolvedKeyframes.length; i++) {
let keyframe = unresolvedKeyframes[i];
if (typeof keyframe === "string") {
keyframe = keyframe.trim();
if (isCSSVariableToken(keyframe)) {
const resolved = getVariableValue(keyframe, element.current);
if (resolved !== undefined) {
unresolvedKeyframes[i] = resolved;
}
if (i === unresolvedKeyframes.length - 1) {
this.finalKeyframe = keyframe;
}
}
}
}
/**
* Resolve "none" values. We do this potentially twice - once before and once after measuring keyframes.
* This could be seen as inefficient but it's a trade-off to avoid measurements in more situations, which
* have a far bigger performance impact.
*/
this.resolveNoneKeyframes();
/**
* Check to see if unit type has changed. If so schedule jobs that will
* temporarily set styles to the destination keyframes.
* Skip if we have more than two keyframes or this isn't a positional value.
* TODO: We can throw if there are multiple keyframes and the value type changes.
*/
if (!positionalKeys.has(name) || unresolvedKeyframes.length !== 2) {
return;
}
const [origin, target] = unresolvedKeyframes;
const originType = findDimensionValueType(origin);
const targetType = findDimensionValueType(target);
/**
* Either we don't recognise these value types or we can animate between them.
*/
if (originType === targetType)
return;
/**
* If both values are numbers or pixels, we can animate between them by
* converting them to numbers.
*/
if (isNumOrPxType(originType) && isNumOrPxType(targetType)) {
for (let i = 0; i < unresolvedKeyframes.length; i++) {
const value = unresolvedKeyframes[i];
if (typeof value === "string") {
unresolvedKeyframes[i] = parseFloat(value);
}
}
}
else if (positionalValues[name]) {
/**
* Else, the only way to resolve this is by measuring the element.
*/
this.needsMeasurement = true;
}
}
resolveNoneKeyframes() {
const { unresolvedKeyframes, name } = this;
const noneKeyframeIndexes = [];
for (let i = 0; i < unresolvedKeyframes.length; i++) {
if (unresolvedKeyframes[i] === null ||
isNone(unresolvedKeyframes[i])) {
noneKeyframeIndexes.push(i);
}
}
if (noneKeyframeIndexes.length) {
makeNoneKeyframesAnimatable(unresolvedKeyframes, noneKeyframeIndexes, name);
}
}
measureInitialState() {
const { element, unresolvedKeyframes, name } = this;
if (!element || !element.current)
return;
if (name === "height") {
this.suspendedScrollY = window.pageYOffset;
}
this.measuredOrigin = positionalValues[name](element.measureViewportBox(), window.getComputedStyle(element.current));
unresolvedKeyframes[0] = this.measuredOrigin;
// Set final key frame to measure after next render
const measureKeyframe = unresolvedKeyframes[unresolvedKeyframes.length - 1];
if (measureKeyframe !== undefined) {
element.getValue(name, measureKeyframe).jump(measureKeyframe, false);
}
}
measureEndState() {
const { element, name, unresolvedKeyframes } = this;
if (!element || !element.current)
return;
const value = element.getValue(name);
value && value.jump(this.measuredOrigin, false);
const finalKeyframeIndex = unresolvedKeyframes.length - 1;
const finalKeyframe = unresolvedKeyframes[finalKeyframeIndex];
unresolvedKeyframes[finalKeyframeIndex] = positionalValues[name](element.measureViewportBox(), window.getComputedStyle(element.current));
if (finalKeyframe !== null && this.finalKeyframe === undefined) {
this.finalKeyframe = finalKeyframe;
}
// If we removed transform values, reapply them before the next render
if (this.removedTransforms?.length) {
this.removedTransforms.forEach(([unsetTransformName, unsetTransformValue]) => {
element
.getValue(unsetTransformName)
.set(unsetTransformValue);
});
}
this.resolveNoneKeyframes();
}
}
export { DOMKeyframesResolver };

View File

@@ -0,0 +1,147 @@
import { fillWildcards } from './utils/fill-wildcards.mjs';
import { removeNonTranslationalTransform } from './utils/unit-conversion.mjs';
import { frame } from '../../frameloop/frame.mjs';
const toResolve = new Set();
let isScheduled = false;
let anyNeedsMeasurement = false;
let isForced = false;
function measureAllKeyframes() {
if (anyNeedsMeasurement) {
const resolversToMeasure = Array.from(toResolve).filter((resolver) => resolver.needsMeasurement);
const elementsToMeasure = new Set(resolversToMeasure.map((resolver) => resolver.element));
const transformsToRestore = new Map();
/**
* Write pass
* If we're measuring elements we want to remove bounding box-changing transforms.
*/
elementsToMeasure.forEach((element) => {
const removedTransforms = removeNonTranslationalTransform(element);
if (!removedTransforms.length)
return;
transformsToRestore.set(element, removedTransforms);
element.render();
});
// Read
resolversToMeasure.forEach((resolver) => resolver.measureInitialState());
// Write
elementsToMeasure.forEach((element) => {
element.render();
const restore = transformsToRestore.get(element);
if (restore) {
restore.forEach(([key, value]) => {
element.getValue(key)?.set(value);
});
}
});
// Read
resolversToMeasure.forEach((resolver) => resolver.measureEndState());
// Write
resolversToMeasure.forEach((resolver) => {
if (resolver.suspendedScrollY !== undefined) {
window.scrollTo(0, resolver.suspendedScrollY);
}
});
}
anyNeedsMeasurement = false;
isScheduled = false;
toResolve.forEach((resolver) => resolver.complete(isForced));
toResolve.clear();
}
function readAllKeyframes() {
toResolve.forEach((resolver) => {
resolver.readKeyframes();
if (resolver.needsMeasurement) {
anyNeedsMeasurement = true;
}
});
}
function flushKeyframeResolvers() {
isForced = true;
readAllKeyframes();
measureAllKeyframes();
isForced = false;
}
class KeyframeResolver {
constructor(unresolvedKeyframes, onComplete, name, motionValue, element, isAsync = false) {
this.state = "pending";
/**
* Track whether this resolver is async. If it is, it'll be added to the
* resolver queue and flushed in the next frame. Resolvers that aren't going
* to trigger read/write thrashing don't need to be async.
*/
this.isAsync = false;
/**
* Track whether this resolver needs to perform a measurement
* to resolve its keyframes.
*/
this.needsMeasurement = false;
this.unresolvedKeyframes = [...unresolvedKeyframes];
this.onComplete = onComplete;
this.name = name;
this.motionValue = motionValue;
this.element = element;
this.isAsync = isAsync;
}
scheduleResolve() {
this.state = "scheduled";
if (this.isAsync) {
toResolve.add(this);
if (!isScheduled) {
isScheduled = true;
frame.read(readAllKeyframes);
frame.resolveKeyframes(measureAllKeyframes);
}
}
else {
this.readKeyframes();
this.complete();
}
}
readKeyframes() {
const { unresolvedKeyframes, name, element, motionValue } = this;
// If initial keyframe is null we need to read it from the DOM
if (unresolvedKeyframes[0] === null) {
const currentValue = motionValue?.get();
// TODO: This doesn't work if the final keyframe is a wildcard
const finalKeyframe = unresolvedKeyframes[unresolvedKeyframes.length - 1];
if (currentValue !== undefined) {
unresolvedKeyframes[0] = currentValue;
}
else if (element && name) {
const valueAsRead = element.readValue(name, finalKeyframe);
if (valueAsRead !== undefined && valueAsRead !== null) {
unresolvedKeyframes[0] = valueAsRead;
}
}
if (unresolvedKeyframes[0] === undefined) {
unresolvedKeyframes[0] = finalKeyframe;
}
if (motionValue && currentValue === undefined) {
motionValue.set(unresolvedKeyframes[0]);
}
}
fillWildcards(unresolvedKeyframes);
}
setFinalKeyframe() { }
measureInitialState() { }
renderEndStyles() { }
measureEndState() { }
complete(isForcedComplete = false) {
this.state = "complete";
this.onComplete(this.unresolvedKeyframes, this.finalKeyframe, isForcedComplete);
toResolve.delete(this);
}
cancel() {
if (this.state === "scheduled") {
toResolve.delete(this);
this.state = "pending";
}
}
resume() {
if (this.state === "pending")
this.scheduleResolve();
}
}
export { KeyframeResolver, flushKeyframeResolvers };

View File

@@ -0,0 +1,11 @@
const isNotNull = (value) => value !== null;
function getFinalKeyframe(keyframes, { repeat, repeatType = "loop" }, finalKeyframe, speed = 1) {
const resolvedKeyframes = keyframes.filter(isNotNull);
const useFirstKeyframe = speed < 0 || (repeat && repeatType !== "loop" && repeat % 2 === 1);
const index = useFirstKeyframe ? 0 : resolvedKeyframes.length - 1;
return !index || finalKeyframe === undefined
? resolvedKeyframes[index]
: finalKeyframe;
}
export { getFinalKeyframe };

View File

@@ -0,0 +1,9 @@
import { fillOffset } from './fill.mjs';
function defaultOffset(arr) {
const offset = [0];
fillOffset(offset, arr.length - 1);
return offset;
}
export { defaultOffset };

View File

@@ -0,0 +1,12 @@
import { progress } from 'motion-utils';
import { mixNumber } from '../../../utils/mix/number.mjs';
function fillOffset(offset, remaining) {
const min = offset[offset.length - 1];
for (let i = 1; i <= remaining; i++) {
const offsetProgress = progress(0, remaining, i);
offset.push(mixNumber(min, 1, offsetProgress));
}
}
export { fillOffset };

View File

@@ -0,0 +1,5 @@
function convertOffsetToTimes(offset, duration) {
return offset.map((o) => o * duration);
}
export { convertOffsetToTimes };

View File

@@ -0,0 +1,11 @@
import { pxValues } from '../../waapi/utils/px-values.mjs';
function applyPxDefaults(keyframes, name) {
for (let i = 0; i < keyframes.length; i++) {
if (typeof keyframes[i] === "number" && pxValues.has(name)) {
keyframes[i] = keyframes[i] + "px";
}
}
}
export { applyPxDefaults };

View File

@@ -0,0 +1,7 @@
function fillWildcards(keyframes) {
for (let i = 1; i < keyframes.length; i++) {
keyframes[i] ?? (keyframes[i] = keyframes[i - 1]);
}
}
export { fillWildcards };

View File

@@ -0,0 +1,15 @@
import { isZeroValueString } from 'motion-utils';
function isNone(value) {
if (typeof value === "number") {
return value === 0;
}
else if (value !== null) {
return value === "none" || value === "0" || isZeroValueString(value);
}
else {
return true;
}
}
export { isNone };

View File

@@ -0,0 +1,30 @@
import { analyseComplexValue } from '../../../value/types/complex/index.mjs';
import { getAnimatableNone } from '../../../value/types/utils/animatable-none.mjs';
/**
* If we encounter keyframes like "none" or "0" and we also have keyframes like
* "#fff" or "200px 200px" we want to find a keyframe to serve as a template for
* the "none" keyframes. In this case "#fff" or "200px 200px" - then these get turned into
* zero equivalents, i.e. "#fff0" or "0px 0px".
*/
const invalidTemplates = new Set(["auto", "none", "0"]);
function makeNoneKeyframesAnimatable(unresolvedKeyframes, noneKeyframeIndexes, name) {
let i = 0;
let animatableTemplate = undefined;
while (i < unresolvedKeyframes.length && !animatableTemplate) {
const keyframe = unresolvedKeyframes[i];
if (typeof keyframe === "string" &&
!invalidTemplates.has(keyframe) &&
analyseComplexValue(keyframe).values.length) {
animatableTemplate = unresolvedKeyframes[i];
}
i++;
}
if (animatableTemplate && name) {
for (const noneIndex of noneKeyframeIndexes) {
unresolvedKeyframes[noneIndex] = getAnimatableNone(name, animatableTemplate);
}
}
}
export { makeNoneKeyframesAnimatable };

View File

@@ -0,0 +1,36 @@
import { parseValueFromTransform } from '../../../render/dom/parse-transform.mjs';
import { transformPropOrder } from '../../../render/utils/keys-transform.mjs';
import { number } from '../../../value/types/numbers/index.mjs';
import { px } from '../../../value/types/numbers/units.mjs';
const isNumOrPxType = (v) => v === number || v === px;
const transformKeys = new Set(["x", "y", "z"]);
const nonTranslationalTransformKeys = transformPropOrder.filter((key) => !transformKeys.has(key));
function removeNonTranslationalTransform(visualElement) {
const removedTransforms = [];
nonTranslationalTransformKeys.forEach((key) => {
const value = visualElement.getValue(key);
if (value !== undefined) {
removedTransforms.push([key, value.get()]);
value.set(key.startsWith("scale") ? 1 : 0);
}
});
return removedTransforms;
}
const positionalValues = {
// Dimensions
width: ({ x }, { paddingLeft = "0", paddingRight = "0" }) => x.max - x.min - parseFloat(paddingLeft) - parseFloat(paddingRight),
height: ({ y }, { paddingTop = "0", paddingBottom = "0" }) => y.max - y.min - parseFloat(paddingTop) - parseFloat(paddingBottom),
top: (_bbox, { top }) => parseFloat(top),
left: (_bbox, { left }) => parseFloat(left),
bottom: ({ y }, { top }) => parseFloat(top) + (y.max - y.min),
right: ({ x }, { left }) => parseFloat(left) + (x.max - x.min),
// Transform
x: (_bbox, { transform }) => parseValueFromTransform(transform, "x"),
y: (_bbox, { transform }) => parseValueFromTransform(transform, "y"),
};
// Alias translate longform names
positionalValues.translateX = positionalValues.x;
positionalValues.translateY = positionalValues.y;
export { isNumOrPxType, positionalValues, removeNonTranslationalTransform };

View File

@@ -0,0 +1,26 @@
class WithPromise {
constructor() {
this.updateFinished();
}
get finished() {
return this._finished;
}
updateFinished() {
this._finished = new Promise((resolve) => {
this.resolve = resolve;
});
}
notifyFinished() {
this.resolve();
}
/**
* Allows the animation to be awaited.
*
* @deprecated Use `finished` instead.
*/
then(onResolve, onReject) {
return this.finished.then(onResolve, onReject);
}
}
export { WithPromise };

View File

@@ -0,0 +1,9 @@
const animationMaps = new WeakMap();
const animationMapKey = (name, pseudoElement = "") => `${name}:${pseudoElement}`;
function getAnimationMap(element) {
const map = animationMaps.get(element) || new Map();
animationMaps.set(element, map);
return map;
}
export { animationMapKey, getAnimationMap };

View File

@@ -0,0 +1,42 @@
import { warning } from 'motion-utils';
import { isGenerator } from '../generators/utils/is-generator.mjs';
import { isAnimatable } from './is-animatable.mjs';
function hasKeyframesChanged(keyframes) {
const current = keyframes[0];
if (keyframes.length === 1)
return true;
for (let i = 0; i < keyframes.length; i++) {
if (keyframes[i] !== current)
return true;
}
}
function canAnimate(keyframes, name, type, velocity) {
/**
* 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];
if (originKeyframe === null)
return false;
/**
* These aren't traditionally animatable but we do support them.
* In future we could look into making this more generic or replacing
* this function with mix() === mixImmediate
*/
if (name === "display" || name === "visibility")
return true;
const targetKeyframe = keyframes[keyframes.length - 1];
const isOriginAnimatable = isAnimatable(originKeyframe, name);
const isTargetAnimatable = isAnimatable(targetKeyframe, name);
warning(isOriginAnimatable === isTargetAnimatable, `You are trying to animate ${name} from "${originKeyframe}" to "${targetKeyframe}". "${isOriginAnimatable ? targetKeyframe : originKeyframe}" is not an animatable value.`, "value-not-animatable");
// Always skip if any of these are true
if (!isOriginAnimatable || !isTargetAnimatable) {
return false;
}
return (hasKeyframesChanged(keyframes) ||
((type === "spring" || isGenerator(type)) && velocity));
}
export { canAnimate };

View File

@@ -0,0 +1,41 @@
import { invariant, isNumericalString } from 'motion-utils';
import { isCSSVariableToken } from './is-css-variable.mjs';
/**
* Parse Framer's special CSS variable format into a CSS token and a fallback.
*
* ```
* `var(--foo, #fff)` => [`--foo`, '#fff']
* ```
*
* @param current
*/
const splitCSSVariableRegex =
// eslint-disable-next-line redos-detector/no-unsafe-regex -- false positive, as it can match a lot of words
/^var\(--(?:([\w-]+)|([\w-]+), ?([a-zA-Z\d ()%#.,-]+))\)/u;
function parseCSSVariable(current) {
const match = splitCSSVariableRegex.exec(current);
if (!match)
return [,];
const [, token1, token2, fallback] = match;
return [`--${token1 ?? token2}`, fallback];
}
const maxDepth = 4;
function getVariableValue(current, element, depth = 1) {
invariant(depth <= maxDepth, `Max CSS variable fallback depth detected in property "${current}". This may indicate a circular fallback dependency.`, "max-css-var-depth");
const [token, fallback] = parseCSSVariable(current);
// No CSS variable detected
if (!token)
return;
// Attempt to read this CSS variable off the element
const resolved = window.getComputedStyle(element).getPropertyValue(token);
if (resolved) {
const trimmed = resolved.trim();
return isNumericalString(trimmed) ? parseFloat(trimmed) : trimmed;
}
return isCSSVariableToken(fallback)
? getVariableValue(fallback, element, depth + 1)
: fallback;
}
export { getVariableValue, parseCSSVariable };

View File

@@ -0,0 +1,7 @@
function getValueTransition(transition, key) {
return (transition?.[key] ??
transition?.["default"] ??
transition);
}
export { getValueTransition };

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 = (value, name) => {
// If the list of keys that might be non-animatable grows, replace with Set
if (name === "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,15 @@
const checkStringStartsWith = (token) => (key) => typeof key === "string" && key.startsWith(token);
const isCSSVariableName =
/*@__PURE__*/ checkStringStartsWith("--");
const startsAsVariableToken =
/*@__PURE__*/ checkStringStartsWith("var(--");
const isCSSVariableToken = (value) => {
const startsWithToken = startsAsVariableToken(value);
if (!startsWithToken)
return false;
// Ensure any comments are stripped from the value as this can harm performance of the regex.
return singleCssVariableRegex.test(value.split("/*")[0].trim());
};
const singleCssVariableRegex = /var\(--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)$/iu;
export { isCSSVariableName, isCSSVariableToken };

View File

@@ -0,0 +1,6 @@
function makeAnimationInstant(options) {
options.duration = 0;
options.type = "keyframes";
}
export { makeAnimationInstant };

View File

@@ -0,0 +1,18 @@
import { inertia } from '../generators/inertia.mjs';
import { keyframes } from '../generators/keyframes.mjs';
import { spring } from '../generators/spring/index.mjs';
const transitionTypeMap = {
decay: inertia,
inertia,
tween: keyframes,
keyframes: keyframes,
spring,
};
function replaceTransitionType(transition) {
if (typeof transition.type === "string") {
transition.type = transitionTypeMap[transition.type];
}
}
export { replaceTransitionType };

View File

@@ -0,0 +1,3 @@
const cubicBezierAsString = ([a, b, c, d]) => `cubic-bezier(${a}, ${b}, ${c}, ${d})`;
export { cubicBezierAsString };

View File

@@ -0,0 +1,14 @@
import { isBezierDefinition } from 'motion-utils';
import { supportsLinearEasing } from '../../../utils/supports/linear-easing.mjs';
import { supportedWaapiEasing } from './supported.mjs';
function isWaapiSupportedEasing(easing) {
return Boolean((typeof easing === "function" && supportsLinearEasing()) ||
!easing ||
(typeof easing === "string" &&
(easing in supportedWaapiEasing || supportsLinearEasing())) ||
isBezierDefinition(easing) ||
(Array.isArray(easing) && easing.every(isWaapiSupportedEasing)));
}
export { isWaapiSupportedEasing };

View File

@@ -0,0 +1,28 @@
import { isBezierDefinition } from 'motion-utils';
import { supportsLinearEasing } from '../../../utils/supports/linear-easing.mjs';
import { generateLinearEasing } from '../utils/linear.mjs';
import { cubicBezierAsString } from './cubic-bezier.mjs';
import { supportedWaapiEasing } from './supported.mjs';
function mapEasingToNativeEasing(easing, duration) {
if (!easing) {
return undefined;
}
else if (typeof easing === "function") {
return supportsLinearEasing()
? generateLinearEasing(easing, duration)
: "ease-out";
}
else if (isBezierDefinition(easing)) {
return cubicBezierAsString(easing);
}
else if (Array.isArray(easing)) {
return easing.map((segmentEasing) => mapEasingToNativeEasing(segmentEasing, duration) ||
supportedWaapiEasing.easeOut);
}
else {
return supportedWaapiEasing[easing];
}
}
export { mapEasingToNativeEasing };

View File

@@ -0,0 +1,15 @@
import { cubicBezierAsString } from './cubic-bezier.mjs';
const supportedWaapiEasing = {
linear: "linear",
ease: "ease",
easeIn: "ease-in",
easeOut: "ease-out",
easeInOut: "ease-in-out",
circIn: /*@__PURE__*/ cubicBezierAsString([0, 0.65, 0.55, 1]),
circOut: /*@__PURE__*/ cubicBezierAsString([0.55, 0, 1, 0.45]),
backIn: /*@__PURE__*/ cubicBezierAsString([0.31, 0.01, 0.66, -0.59]),
backOut: /*@__PURE__*/ cubicBezierAsString([0.33, 1.53, 0.69, 0.99]),
};
export { supportedWaapiEasing };

View File

@@ -0,0 +1,39 @@
import { activeAnimations } from '../../stats/animation-count.mjs';
import { statsBuffer } from '../../stats/buffer.mjs';
import { mapEasingToNativeEasing } from './easing/map-easing.mjs';
function startWaapiAnimation(element, valueName, keyframes, { delay = 0, duration = 300, repeat = 0, repeatType = "loop", ease = "easeOut", times, } = {}, pseudoElement = undefined) {
const keyframeOptions = {
[valueName]: keyframes,
};
if (times)
keyframeOptions.offset = times;
const easing = mapEasingToNativeEasing(ease, duration);
/**
* If this is an easing array, apply to keyframes, not animation as a whole
*/
if (Array.isArray(easing))
keyframeOptions.easing = easing;
if (statsBuffer.value) {
activeAnimations.waapi++;
}
const options = {
delay,
duration,
easing: !Array.isArray(easing) ? easing : "linear",
fill: "both",
iterations: repeat + 1,
direction: repeatType === "reverse" ? "alternate" : "normal",
};
if (pseudoElement)
options.pseudoElement = pseudoElement;
const animation = element.animate(keyframeOptions, options);
if (statsBuffer.value) {
animation.finished.finally(() => {
activeAnimations.waapi--;
});
}
return animation;
}
export { startWaapiAnimation };

View File

@@ -0,0 +1,13 @@
import { memo } from 'motion-utils';
const supportsPartialKeyframes = /*@__PURE__*/ memo(() => {
try {
document.createElement("div").animate({ opacity: [1] });
}
catch (e) {
return false;
}
return true;
});
export { supportsPartialKeyframes };

View File

@@ -0,0 +1,43 @@
import { memo } from 'motion-utils';
/**
* A list of values that can be hardware-accelerated.
*/
const acceleratedValues = new Set([
"opacity",
"clipPath",
"filter",
"transform",
// TODO: Could be re-enabled now we have support for linear() easing
// "background-color"
]);
const supportsWaapi = /*@__PURE__*/ memo(() => Object.hasOwnProperty.call(Element.prototype, "animate"));
function supportsBrowserAnimation(options) {
const { motionValue, name, repeatDelay, repeatType, damping, type } = options;
const subject = motionValue?.owner?.current;
/**
* We use this check instead of isHTMLElement() because we explicitly
* **don't** want elements in different timing contexts (i.e. popups)
* to be accelerated, as it's not possible to sync these animations
* properly with those driven from the main window frameloop.
*/
if (!(subject instanceof HTMLElement)) {
return false;
}
const { onUpdate, transformTemplate } = motionValue.owner.getProps();
return (supportsWaapi() &&
name &&
acceleratedValues.has(name) &&
(name !== "transform" || !transformTemplate) &&
/**
* 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.
*/
!onUpdate &&
!repeatDelay &&
repeatType !== "mirror" &&
damping !== 0 &&
type !== "inertia");
}
export { supportsBrowserAnimation };

View File

@@ -0,0 +1,14 @@
/**
* A list of values that can be hardware-accelerated.
*/
const acceleratedValues = new Set([
"opacity",
"clipPath",
"filter",
"transform",
// TODO: Can be accelerated but currently disabled until https://issues.chromium.org/issues/41491098 is resolved
// or until we implement support for linear() easing.
// "background-color"
]);
export { acceleratedValues };

View File

@@ -0,0 +1,15 @@
import { supportsLinearEasing } from '../../../utils/supports/linear-easing.mjs';
import { isGenerator } from '../../generators/utils/is-generator.mjs';
function applyGeneratorOptions({ type, ...options }) {
if (isGenerator(type) && supportsLinearEasing()) {
return type.applyToOptions(options);
}
else {
options.duration ?? (options.duration = 300);
options.ease ?? (options.ease = "easeOut");
}
return options;
}
export { applyGeneratorOptions };

View File

@@ -0,0 +1,12 @@
const generateLinearEasing = (easing, duration, // as milliseconds
resolution = 10 // as milliseconds
) => {
let points = "";
const numPoints = Math.max(Math.round(duration / resolution), 2);
for (let i = 0; i < numPoints; i++) {
points += Math.round(easing(i / (numPoints - 1)) * 10000) / 10000 + ", ";
}
return `linear(${points.substring(0, points.length - 2)})`;
};
export { generateLinearEasing };

View File

@@ -0,0 +1,39 @@
const pxValues = new Set([
// Border props
"borderWidth",
"borderTopWidth",
"borderRightWidth",
"borderBottomWidth",
"borderLeftWidth",
"borderRadius",
"radius",
"borderTopLeftRadius",
"borderTopRightRadius",
"borderBottomRightRadius",
"borderBottomLeftRadius",
// Positioning props
"width",
"maxWidth",
"height",
"maxHeight",
"top",
"right",
"bottom",
"left",
// Spacing props
"padding",
"paddingTop",
"paddingRight",
"paddingBottom",
"paddingLeft",
"margin",
"marginTop",
"marginRight",
"marginBottom",
"marginLeft",
// Misc
"backgroundPositionX",
"backgroundPositionY",
]);
export { pxValues };

View File

@@ -0,0 +1,18 @@
import { anticipate, backInOut, circInOut } from 'motion-utils';
const unsupportedEasingFunctions = {
anticipate,
backInOut,
circInOut,
};
function isUnsupportedEase(key) {
return key in unsupportedEasingFunctions;
}
function replaceStringEasing(transition) {
if (typeof transition.ease === "string" &&
isUnsupportedEase(transition.ease)) {
transition.ease = unsupportedEasingFunctions[transition.ease];
}
}
export { replaceStringEasing };

View File

@@ -0,0 +1,47 @@
import { frame, cancelFrame } from '../frameloop/frame.mjs';
import { numberValueTypes } from '../value/types/maps/number.mjs';
import { getValueAsType } from '../value/types/utils/get-as-type.mjs';
class MotionValueState {
constructor() {
this.latest = {};
this.values = new Map();
}
set(name, value, render, computed, useDefaultValueType = true) {
const existingValue = this.values.get(name);
if (existingValue) {
existingValue.onRemove();
}
const onChange = () => {
const v = value.get();
if (useDefaultValueType) {
this.latest[name] = getValueAsType(v, numberValueTypes[name]);
}
else {
this.latest[name] = v;
}
render && frame.render(render);
};
onChange();
const cancelOnChange = value.on("change", onChange);
computed && value.addDependent(computed);
const remove = () => {
cancelOnChange();
render && cancelFrame(render);
this.values.delete(name);
computed && value.removeDependent(computed);
};
this.values.set(name, { value, onRemove: remove });
return remove;
}
get(name) {
return this.values.get(name)?.value;
}
destroy() {
for (const value of this.values.values()) {
value.onRemove();
}
}
}
export { MotionValueState };

41
node_modules/motion-dom/dist/es/effects/attr/index.mjs generated vendored Normal file
View File

@@ -0,0 +1,41 @@
import { camelToDash } from '../../render/dom/utils/camel-to-dash.mjs';
import { createSelectorEffect } from '../utils/create-dom-effect.mjs';
import { createEffect } from '../utils/create-effect.mjs';
function canSetAsProperty(element, name) {
if (!(name in element))
return false;
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), name) ||
Object.getOwnPropertyDescriptor(element, name);
// Check if it has a setter
return descriptor && typeof descriptor.set === "function";
}
const addAttrValue = (element, state, key, value) => {
const isProp = canSetAsProperty(element, key);
const name = isProp
? key
: key.startsWith("data") || key.startsWith("aria")
? camelToDash(key)
: key;
/**
* Set attribute directly via property if available
*/
const render = isProp
? () => {
element[name] = state.latest[key];
}
: () => {
const v = state.latest[key];
if (v === null || v === undefined) {
element.removeAttribute(name);
}
else {
element.setAttribute(name, String(v));
}
};
return state.set(key, value, render);
};
const attrEffect = /*@__PURE__*/ createSelectorEffect(
/*@__PURE__*/ createEffect(addAttrValue));
export { addAttrValue, attrEffect };

View File

@@ -0,0 +1,9 @@
import { createEffect } from '../utils/create-effect.mjs';
const propEffect = /*@__PURE__*/ createEffect((subject, state, key, value) => {
return state.set(key, value, () => {
subject[key] = state.latest[key];
}, undefined, false);
});
export { propEffect };

View File

@@ -0,0 +1,52 @@
import { isCSSVar } from '../../render/dom/is-css-var.mjs';
import { transformProps } from '../../render/utils/keys-transform.mjs';
import { isHTMLElement } from '../../utils/is-html-element.mjs';
import { MotionValue } from '../../value/index.mjs';
import { createSelectorEffect } from '../utils/create-dom-effect.mjs';
import { createEffect } from '../utils/create-effect.mjs';
import { buildTransform } from './transform.mjs';
const originProps = new Set(["originX", "originY", "originZ"]);
const addStyleValue = (element, state, key, value) => {
let render = undefined;
let computed = undefined;
if (transformProps.has(key)) {
if (!state.get("transform")) {
// If this is an HTML element, we need to set the transform-box to fill-box
// to normalise the transform relative to the element's bounding box
if (!isHTMLElement(element) && !state.get("transformBox")) {
addStyleValue(element, state, "transformBox", new MotionValue("fill-box"));
}
state.set("transform", new MotionValue("none"), () => {
element.style.transform = buildTransform(state);
});
}
computed = state.get("transform");
}
else if (originProps.has(key)) {
if (!state.get("transformOrigin")) {
state.set("transformOrigin", new MotionValue(""), () => {
const originX = state.latest.originX ?? "50%";
const originY = state.latest.originY ?? "50%";
const originZ = state.latest.originZ ?? 0;
element.style.transformOrigin = `${originX} ${originY} ${originZ}`;
});
}
computed = state.get("transformOrigin");
}
else if (isCSSVar(key)) {
render = () => {
element.style.setProperty(key, state.latest[key]);
};
}
else {
render = () => {
element.style[key] = state.latest[key];
};
}
return state.set(key, value, render, computed);
};
const styleEffect = /*@__PURE__*/ createSelectorEffect(
/*@__PURE__*/ createEffect(addStyleValue));
export { addStyleValue, styleEffect };

View File

@@ -0,0 +1,38 @@
import { transformPropOrder } from '../../render/utils/keys-transform.mjs';
const translateAlias = {
x: "translateX",
y: "translateY",
z: "translateZ",
transformPerspective: "perspective",
};
function buildTransform(state) {
let transform = "";
let transformIsDefault = true;
/**
* Loop over all possible transforms in order, adding the ones that
* are present to the transform string.
*/
for (let i = 0; i < transformPropOrder.length; i++) {
const key = transformPropOrder[i];
const value = state.latest[key];
if (value === undefined)
continue;
let valueIsDefault = true;
if (typeof value === "number") {
valueIsDefault = value === (key.startsWith("scale") ? 1 : 0);
}
else {
valueIsDefault = parseFloat(value) === 0;
}
if (!valueIsDefault) {
transformIsDefault = false;
const transformName = translateAlias[key] || key;
const valueToRender = state.latest[key];
transform += `${transformName}(${valueToRender}) `;
}
}
return transformIsDefault ? "none" : transform.trim();
}
export { buildTransform };

41
node_modules/motion-dom/dist/es/effects/svg/index.mjs generated vendored Normal file
View File

@@ -0,0 +1,41 @@
import { MotionValue } from '../../value/index.mjs';
import { px } from '../../value/types/numbers/units.mjs';
import { addAttrValue } from '../attr/index.mjs';
import { addStyleValue } from '../style/index.mjs';
import { createSelectorEffect } from '../utils/create-dom-effect.mjs';
import { createEffect } from '../utils/create-effect.mjs';
import { frame } from '../../frameloop/frame.mjs';
const toPx = px.transform;
function addSVGPathValue(element, state, key, value) {
frame.render(() => element.setAttribute("pathLength", "1"));
if (key === "pathOffset") {
return state.set(key, value, () => element.setAttribute("stroke-dashoffset", toPx(-state.latest[key])));
}
else {
if (!state.get("stroke-dasharray")) {
state.set("stroke-dasharray", new MotionValue("1 1"), () => {
const { pathLength = 1, pathSpacing } = state.latest;
element.setAttribute("stroke-dasharray", `${toPx(pathLength)} ${toPx(pathSpacing ?? 1 - Number(pathLength))}`);
});
}
return state.set(key, value, undefined, state.get("stroke-dasharray"));
}
}
const addSVGValue = (element, state, key, value) => {
if (key.startsWith("path")) {
return addSVGPathValue(element, state, key, value);
}
else if (key.startsWith("attr")) {
return addAttrValue(element, state, convertAttrKey(key), value);
}
const handler = key in element.style ? addStyleValue : addAttrValue;
return handler(element, state, key, value);
};
const svgEffect = /*@__PURE__*/ createSelectorEffect(
/*@__PURE__*/ createEffect(addSVGValue));
function convertAttrKey(key) {
return key.replace(/^attr([A-Z])/, (_, firstChar) => firstChar.toLowerCase());
}
export { svgEffect };

View File

@@ -0,0 +1,18 @@
import { resolveElements } from '../../utils/resolve-elements.mjs';
function createSelectorEffect(subjectEffect) {
return (subject, values) => {
const elements = resolveElements(subject);
const subscriptions = [];
for (const element of elements) {
const remove = subjectEffect(element, values);
subscriptions.push(remove);
}
return () => {
for (const remove of subscriptions)
remove();
};
};
}
export { createSelectorEffect };

View File

@@ -0,0 +1,21 @@
import { MotionValueState } from '../MotionValueState.mjs';
function createEffect(addValue) {
const stateCache = new WeakMap();
const subscriptions = [];
return (subject, values) => {
const state = stateCache.get(subject) ?? new MotionValueState();
stateCache.set(subject, state);
for (const key in values) {
const value = values[key];
const remove = addValue(subject, state, key, value);
subscriptions.push(remove);
}
return () => {
for (const cancel of subscriptions)
cancel();
};
};
}
export { createEffect };

71
node_modules/motion-dom/dist/es/frameloop/batcher.mjs generated vendored Normal file
View File

@@ -0,0 +1,71 @@
import { MotionGlobalConfig } from 'motion-utils';
import { stepsOrder } from './order.mjs';
import { createRenderStep } from './render-step.mjs';
const maxElapsed = 40;
function createRenderBatcher(scheduleNextBatch, allowKeepAlive) {
let runNextFrame = false;
let useDefaultElapsed = true;
const state = {
delta: 0.0,
timestamp: 0.0,
isProcessing: false,
};
const flagRunNextFrame = () => (runNextFrame = true);
const steps = stepsOrder.reduce((acc, key) => {
acc[key] = createRenderStep(flagRunNextFrame, allowKeepAlive ? key : undefined);
return acc;
}, {});
const { setup, read, resolveKeyframes, preUpdate, update, preRender, render, postRender, } = steps;
const processBatch = () => {
const timestamp = MotionGlobalConfig.useManualTiming
? state.timestamp
: performance.now();
runNextFrame = false;
if (!MotionGlobalConfig.useManualTiming) {
state.delta = useDefaultElapsed
? 1000 / 60
: Math.max(Math.min(timestamp - state.timestamp, maxElapsed), 1);
}
state.timestamp = timestamp;
state.isProcessing = true;
// Unrolled render loop for better per-frame performance
setup.process(state);
read.process(state);
resolveKeyframes.process(state);
preUpdate.process(state);
update.process(state);
preRender.process(state);
render.process(state);
postRender.process(state);
state.isProcessing = false;
if (runNextFrame && allowKeepAlive) {
useDefaultElapsed = false;
scheduleNextBatch(processBatch);
}
};
const wake = () => {
runNextFrame = true;
useDefaultElapsed = true;
if (!state.isProcessing) {
scheduleNextBatch(processBatch);
}
};
const schedule = stepsOrder.reduce((acc, key) => {
const step = steps[key];
acc[key] = (process, keepAlive = false, immediate = false) => {
if (!runNextFrame)
wake();
return step.schedule(process, keepAlive, immediate);
};
return acc;
}, {});
const cancel = (process) => {
for (let i = 0; i < stepsOrder.length; i++) {
steps[stepsOrder[i]].cancel(process);
}
};
return { schedule, cancel, state, steps };
}
export { createRenderBatcher };

6
node_modules/motion-dom/dist/es/frameloop/frame.mjs generated vendored Normal file
View File

@@ -0,0 +1,6 @@
import { noop } from 'motion-utils';
import { createRenderBatcher } from './batcher.mjs';
const { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop, true);
export { cancelFrame, frame, frameData, frameSteps };

View File

@@ -0,0 +1,20 @@
import { stepsOrder } from './order.mjs';
import { frame, cancelFrame } from './frame.mjs';
/**
* @deprecated
*
* Import as `frame` instead.
*/
const sync = frame;
/**
* @deprecated
*
* Use cancelFrame(callback) instead.
*/
const cancelSync = stepsOrder.reduce((acc, key) => {
acc[key] = (process) => cancelFrame(process);
return acc;
}, {});
export { cancelSync, sync };

View File

@@ -0,0 +1,6 @@
import { createRenderBatcher } from './batcher.mjs';
const { schedule: microtask, cancel: cancelMicrotask } =
/* @__PURE__ */ createRenderBatcher(queueMicrotask, false);
export { cancelMicrotask, microtask };

12
node_modules/motion-dom/dist/es/frameloop/order.mjs generated vendored Normal file
View File

@@ -0,0 +1,12 @@
const stepsOrder = [
"setup", // Compute
"read", // Read
"resolveKeyframes", // Write/Read/Write/Read
"preUpdate", // Compute
"update", // Compute
"preRender", // Compute
"render", // Write
"postRender", // Compute
];
export { stepsOrder };

View File

@@ -0,0 +1,92 @@
import { statsBuffer } from '../stats/buffer.mjs';
function createRenderStep(runNextFrame, stepName) {
/**
* We create and reuse two queues, one to queue jobs for the current frame
* and one for the next. We reuse to avoid triggering GC after x frames.
*/
let thisFrame = new Set();
let nextFrame = new Set();
/**
* Track whether we're currently processing jobs in this step. This way
* we can decide whether to schedule new jobs for this frame or next.
*/
let isProcessing = false;
let flushNextFrame = false;
/**
* A set of processes which were marked keepAlive when scheduled.
*/
const toKeepAlive = new WeakSet();
let latestFrameData = {
delta: 0.0,
timestamp: 0.0,
isProcessing: false,
};
let numCalls = 0;
function triggerCallback(callback) {
if (toKeepAlive.has(callback)) {
step.schedule(callback);
runNextFrame();
}
numCalls++;
callback(latestFrameData);
}
const step = {
/**
* Schedule a process to run on the next frame.
*/
schedule: (callback, keepAlive = false, immediate = false) => {
const addToCurrentFrame = immediate && isProcessing;
const queue = addToCurrentFrame ? thisFrame : nextFrame;
if (keepAlive)
toKeepAlive.add(callback);
if (!queue.has(callback))
queue.add(callback);
return callback;
},
/**
* Cancel the provided callback from running on the next frame.
*/
cancel: (callback) => {
nextFrame.delete(callback);
toKeepAlive.delete(callback);
},
/**
* Execute all schedule callbacks.
*/
process: (frameData) => {
latestFrameData = frameData;
/**
* If we're already processing we've probably been triggered by a flushSync
* inside an existing process. Instead of executing, mark flushNextFrame
* as true and ensure we flush the following frame at the end of this one.
*/
if (isProcessing) {
flushNextFrame = true;
return;
}
isProcessing = true;
[thisFrame, nextFrame] = [nextFrame, thisFrame];
// Execute this frame
thisFrame.forEach(triggerCallback);
/**
* If we're recording stats then
*/
if (stepName && statsBuffer.value) {
statsBuffer.value.frameloop[stepName].push(numCalls);
}
numCalls = 0;
// Clear the frame so no callbacks remain. This is to avoid
// memory leaks should this render step not run for a while.
thisFrame.clear();
isProcessing = false;
if (flushNextFrame) {
flushNextFrame = false;
step.process(frameData);
}
},
};
return step;
}
export { createRenderStep };

View File

@@ -0,0 +1,31 @@
import { MotionGlobalConfig } from 'motion-utils';
import { frameData } from './frame.mjs';
let now;
function clearTime() {
now = undefined;
}
/**
* An eventloop-synchronous alternative to performance.now().
*
* Ensures that time measurements remain consistent within a synchronous context.
* Usually calling performance.now() twice within the same synchronous context
* will return different values which isn't useful for animations when we're usually
* trying to sync animations to the same frame.
*/
const time = {
now: () => {
if (now === undefined) {
time.set(frameData.isProcessing || MotionGlobalConfig.useManualTiming
? frameData.timestamp
: performance.now());
}
return now;
},
set: (newTime) => {
now = newTime;
queueMicrotask(clearTime);
},
};
export { time };

View File

@@ -0,0 +1,9 @@
const isDragging = {
x: false,
y: false,
};
function isDragActive() {
return isDragging.x || isDragging.y;
}
export { isDragActive, isDragging };

View File

@@ -0,0 +1,28 @@
import { isDragging } from './is-active.mjs';
function setDragLock(axis) {
if (axis === "x" || axis === "y") {
if (isDragging[axis]) {
return null;
}
else {
isDragging[axis] = true;
return () => {
isDragging[axis] = false;
};
}
}
else {
if (isDragging.x || isDragging.y) {
return null;
}
else {
isDragging.x = isDragging.y = true;
return () => {
isDragging.x = isDragging.y = false;
};
}
}
}
export { setDragLock };

37
node_modules/motion-dom/dist/es/gestures/hover.mjs generated vendored Normal file
View File

@@ -0,0 +1,37 @@
import { isDragActive } from './drag/state/is-active.mjs';
import { setupGesture } from './utils/setup.mjs';
function isValidHover(event) {
return !(event.pointerType === "touch" || isDragActive());
}
/**
* Create a hover gesture. hover() is different to .addEventListener("pointerenter")
* in that it has an easier syntax, filters out polyfilled touch events, interoperates
* with drag gestures, and automatically removes the "pointerennd" event listener when the hover ends.
*
* @public
*/
function hover(elementOrSelector, onHoverStart, options = {}) {
const [elements, eventOptions, cancel] = setupGesture(elementOrSelector, options);
const onPointerEnter = (enterEvent) => {
if (!isValidHover(enterEvent))
return;
const { target } = enterEvent;
const onHoverEnd = onHoverStart(target, enterEvent);
if (typeof onHoverEnd !== "function" || !target)
return;
const onPointerLeave = (leaveEvent) => {
if (!isValidHover(leaveEvent))
return;
onHoverEnd(leaveEvent);
target.removeEventListener("pointerleave", onPointerLeave);
};
target.addEventListener("pointerleave", onPointerLeave, eventOptions);
};
elements.forEach((element) => {
element.addEventListener("pointerenter", onPointerEnter, eventOptions);
});
return cancel;
}
export { hover };

View File

@@ -0,0 +1,83 @@
import { isHTMLElement } from '../../utils/is-html-element.mjs';
import { isDragActive } from '../drag/state/is-active.mjs';
import { isNodeOrChild } from '../utils/is-node-or-child.mjs';
import { isPrimaryPointer } from '../utils/is-primary-pointer.mjs';
import { setupGesture } from '../utils/setup.mjs';
import { isElementKeyboardAccessible } from './utils/is-keyboard-accessible.mjs';
import { enableKeyboardPress } from './utils/keyboard.mjs';
import { isPressing } from './utils/state.mjs';
/**
* Filter out events that are not primary pointer events, or are triggering
* while a Motion gesture is active.
*/
function isValidPressEvent(event) {
return isPrimaryPointer(event) && !isDragActive();
}
/**
* Create a press gesture.
*
* Press is different to `"pointerdown"`, `"pointerup"` in that it
* automatically filters out secondary pointer events like right
* click and multitouch.
*
* It also adds accessibility support for keyboards, where
* an element with a press gesture will receive focus and
* trigger on Enter `"keydown"` and `"keyup"` events.
*
* This is different to a browser's `"click"` event, which does
* respond to keyboards but only for the `"click"` itself, rather
* than the press start and end/cancel. The element also needs
* to be focusable for this to work, whereas a press gesture will
* make an element focusable by default.
*
* @public
*/
function press(targetOrSelector, onPressStart, options = {}) {
const [targets, eventOptions, cancelEvents] = setupGesture(targetOrSelector, options);
const startPress = (startEvent) => {
const target = startEvent.currentTarget;
if (!isValidPressEvent(startEvent))
return;
isPressing.add(target);
const onPressEnd = onPressStart(target, startEvent);
const onPointerEnd = (endEvent, success) => {
window.removeEventListener("pointerup", onPointerUp);
window.removeEventListener("pointercancel", onPointerCancel);
if (isPressing.has(target)) {
isPressing.delete(target);
}
if (!isValidPressEvent(endEvent)) {
return;
}
if (typeof onPressEnd === "function") {
onPressEnd(endEvent, { success });
}
};
const onPointerUp = (upEvent) => {
onPointerEnd(upEvent, target === window ||
target === document ||
options.useGlobalTarget ||
isNodeOrChild(target, upEvent.target));
};
const onPointerCancel = (cancelEvent) => {
onPointerEnd(cancelEvent, false);
};
window.addEventListener("pointerup", onPointerUp, eventOptions);
window.addEventListener("pointercancel", onPointerCancel, eventOptions);
};
targets.forEach((target) => {
const pointerDownTarget = options.useGlobalTarget ? window : target;
pointerDownTarget.addEventListener("pointerdown", startPress, eventOptions);
if (isHTMLElement(target)) {
target.addEventListener("focus", (event) => enableKeyboardPress(event, eventOptions));
if (!isElementKeyboardAccessible(target) &&
!target.hasAttribute("tabindex")) {
target.tabIndex = 0;
}
}
});
return cancelEvents;
}
export { press };

View File

@@ -0,0 +1,13 @@
const focusableElements = new Set([
"BUTTON",
"INPUT",
"SELECT",
"TEXTAREA",
"A",
]);
function isElementKeyboardAccessible(element) {
return (focusableElements.has(element.tagName) ||
element.tabIndex !== -1);
}
export { isElementKeyboardAccessible };

View File

@@ -0,0 +1,38 @@
import { isPressing } from './state.mjs';
/**
* Filter out events that are not "Enter" keys.
*/
function filterEvents(callback) {
return (event) => {
if (event.key !== "Enter")
return;
callback(event);
};
}
function firePointerEvent(target, type) {
target.dispatchEvent(new PointerEvent("pointer" + type, { isPrimary: true, bubbles: true }));
}
const enableKeyboardPress = (focusEvent, eventOptions) => {
const element = focusEvent.currentTarget;
if (!element)
return;
const handleKeydown = filterEvents(() => {
if (isPressing.has(element))
return;
firePointerEvent(element, "down");
const handleKeyup = filterEvents(() => {
firePointerEvent(element, "up");
});
const handleBlur = () => firePointerEvent(element, "cancel");
element.addEventListener("keyup", handleKeyup, eventOptions);
element.addEventListener("blur", handleBlur, eventOptions);
});
element.addEventListener("keydown", handleKeydown, eventOptions);
/**
* Add an event listener that fires on blur to remove the keydown events.
*/
element.addEventListener("blur", () => element.removeEventListener("keydown", handleKeydown), eventOptions);
};
export { enableKeyboardPress };

View File

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

View File

@@ -0,0 +1,20 @@
/**
* Recursively traverse up the tree to check whether the provided child node
* is the parent or a descendant of it.
*
* @param parent - Element to find
* @param child - Element to test against parent
*/
const isNodeOrChild = (parent, child) => {
if (!child) {
return false;
}
else if (parent === child) {
return true;
}
else {
return isNodeOrChild(parent, child.parentElement);
}
};
export { isNodeOrChild };

View File

@@ -0,0 +1,18 @@
const isPrimaryPointer = (event) => {
if (event.pointerType === "mouse") {
return typeof event.button !== "number" || event.button <= 0;
}
else {
/**
* isPrimary is true for all mice buttons, whereas every touch point
* is regarded as its own input. So subsequent concurrent touch points
* will be false.
*
* Specifically match against false here as incomplete versions of
* PointerEvents in very old browser might have it set as undefined.
*/
return event.isPrimary !== false;
}
};
export { isPrimaryPointer };

View File

@@ -0,0 +1,15 @@
import { resolveElements } from '../../utils/resolve-elements.mjs';
function setupGesture(elementOrSelector, options) {
const elements = resolveElements(elementOrSelector);
const gestureAbortController = new AbortController();
const eventOptions = {
passive: true,
...options,
signal: gestureAbortController.signal,
};
const cancel = () => gestureAbortController.abort();
return [elements, eventOptions, cancel];
}
export { setupGesture };

100
node_modules/motion-dom/dist/es/index.mjs generated vendored Normal file
View File

@@ -0,0 +1,100 @@
export { AsyncMotionValueAnimation } from './animation/AsyncMotionValueAnimation.mjs';
export { GroupAnimation } from './animation/GroupAnimation.mjs';
export { GroupAnimationWithThen } from './animation/GroupAnimationWithThen.mjs';
export { JSAnimation, animateValue } from './animation/JSAnimation.mjs';
export { NativeAnimation } from './animation/NativeAnimation.mjs';
export { NativeAnimationExtended } from './animation/NativeAnimationExtended.mjs';
export { NativeAnimationWrapper } from './animation/NativeAnimationWrapper.mjs';
export { animationMapKey, getAnimationMap } from './animation/utils/active-animations.mjs';
export { getVariableValue, parseCSSVariable } from './animation/utils/css-variables-conversion.mjs';
export { getValueTransition } from './animation/utils/get-value-transition.mjs';
export { isCSSVariableName, isCSSVariableToken } from './animation/utils/is-css-variable.mjs';
export { makeAnimationInstant } from './animation/utils/make-animation-instant.mjs';
export { inertia } from './animation/generators/inertia.mjs';
export { defaultEasing, keyframes } from './animation/generators/keyframes.mjs';
export { spring } from './animation/generators/spring/index.mjs';
export { calcGeneratorDuration, maxGeneratorDuration } from './animation/generators/utils/calc-duration.mjs';
export { createGeneratorEasing } from './animation/generators/utils/create-generator-easing.mjs';
export { isGenerator } from './animation/generators/utils/is-generator.mjs';
export { DOMKeyframesResolver } from './animation/keyframes/DOMKeyframesResolver.mjs';
export { KeyframeResolver, flushKeyframeResolvers } from './animation/keyframes/KeyframesResolver.mjs';
export { defaultOffset } from './animation/keyframes/offsets/default.mjs';
export { fillOffset } from './animation/keyframes/offsets/fill.mjs';
export { convertOffsetToTimes } from './animation/keyframes/offsets/time.mjs';
export { applyPxDefaults } from './animation/keyframes/utils/apply-px-defaults.mjs';
export { fillWildcards } from './animation/keyframes/utils/fill-wildcards.mjs';
export { cubicBezierAsString } from './animation/waapi/easing/cubic-bezier.mjs';
export { isWaapiSupportedEasing } from './animation/waapi/easing/is-supported.mjs';
export { mapEasingToNativeEasing } from './animation/waapi/easing/map-easing.mjs';
export { supportedWaapiEasing } from './animation/waapi/easing/supported.mjs';
export { startWaapiAnimation } from './animation/waapi/start-waapi-animation.mjs';
export { supportsPartialKeyframes } from './animation/waapi/supports/partial-keyframes.mjs';
export { supportsBrowserAnimation } from './animation/waapi/supports/waapi.mjs';
export { acceleratedValues } from './animation/waapi/utils/accelerated-values.mjs';
export { applyGeneratorOptions } from './animation/waapi/utils/apply-generator.mjs';
export { generateLinearEasing } from './animation/waapi/utils/linear.mjs';
export { addAttrValue, attrEffect } from './effects/attr/index.mjs';
export { propEffect } from './effects/prop/index.mjs';
export { addStyleValue, styleEffect } from './effects/style/index.mjs';
export { svgEffect } from './effects/svg/index.mjs';
export { createRenderBatcher } from './frameloop/batcher.mjs';
export { cancelMicrotask, microtask } from './frameloop/microtask.mjs';
export { time } from './frameloop/sync-time.mjs';
export { isDragActive, isDragging } from './gestures/drag/state/is-active.mjs';
export { setDragLock } from './gestures/drag/state/set-active.mjs';
export { hover } from './gestures/hover.mjs';
export { press } from './gestures/press/index.mjs';
export { isNodeOrChild } from './gestures/utils/is-node-or-child.mjs';
export { isPrimaryPointer } from './gestures/utils/is-primary-pointer.mjs';
export { defaultTransformValue, parseValueFromTransform, readTransformValue } from './render/dom/parse-transform.mjs';
export { getComputedStyle } from './render/dom/style-computed.mjs';
export { setStyle } from './render/dom/style-set.mjs';
export { positionalKeys } from './render/utils/keys-position.mjs';
export { transformPropOrder, transformProps } from './render/utils/keys-transform.mjs';
export { resize } from './resize/index.mjs';
export { observeTimeline } from './scroll/observe.mjs';
export { recordStats } from './stats/index.mjs';
export { activeAnimations } from './stats/animation-count.mjs';
export { statsBuffer } from './stats/buffer.mjs';
export { interpolate } from './utils/interpolate.mjs';
export { isHTMLElement } from './utils/is-html-element.mjs';
export { isSVGElement } from './utils/is-svg-element.mjs';
export { isSVGSVGElement } from './utils/is-svg-svg-element.mjs';
export { mix } from './utils/mix/index.mjs';
export { mixColor, mixLinearColor } from './utils/mix/color.mjs';
export { getMixer, mixArray, mixComplex, mixObject } from './utils/mix/complex.mjs';
export { mixImmediate } from './utils/mix/immediate.mjs';
export { mixNumber } from './utils/mix/number.mjs';
export { invisibleValues, mixVisibility } from './utils/mix/visibility.mjs';
export { resolveElements } from './utils/resolve-elements.mjs';
export { getOriginIndex, stagger } from './utils/stagger.mjs';
export { supportsFlags } from './utils/supports/flags.mjs';
export { supportsLinearEasing } from './utils/supports/linear-easing.mjs';
export { supportsScrollTimeline } from './utils/supports/scroll-timeline.mjs';
export { transform } from './utils/transform.mjs';
export { MotionValue, collectMotionValues, motionValue } from './value/index.mjs';
export { mapValue } from './value/map-value.mjs';
export { attachSpring, springValue } from './value/spring-value.mjs';
export { transformValue } from './value/transform-value.mjs';
export { color } from './value/types/color/index.mjs';
export { hex } from './value/types/color/hex.mjs';
export { hsla } from './value/types/color/hsla.mjs';
export { hslaToRgba } from './value/types/color/hsla-to-rgba.mjs';
export { rgbUnit, rgba } from './value/types/color/rgba.mjs';
export { analyseComplexValue, complex } from './value/types/complex/index.mjs';
export { dimensionValueTypes, findDimensionValueType } from './value/types/dimensions.mjs';
export { defaultValueTypes, getDefaultValueType } from './value/types/maps/defaults.mjs';
export { numberValueTypes } from './value/types/maps/number.mjs';
export { transformValueTypes } from './value/types/maps/transform.mjs';
export { alpha, number, scale } from './value/types/numbers/index.mjs';
export { degrees, percent, progressPercentage, px, vh, vw } from './value/types/numbers/units.mjs';
export { testValueType } from './value/types/test.mjs';
export { getAnimatableNone } from './value/types/utils/animatable-none.mjs';
export { findValueType } from './value/types/utils/find.mjs';
export { getValueAsType } from './value/types/utils/get-as-type.mjs';
export { isMotionValue } from './value/utils/is-motion-value.mjs';
export { ViewTransitionBuilder, animateView } from './view/index.mjs';
export { getViewAnimationLayerInfo } from './view/utils/get-layer-info.mjs';
export { getViewAnimations } from './view/utils/get-view-animations.mjs';
export { cancelSync, sync } from './frameloop/index-legacy.mjs';
export { cancelFrame, frame, frameData, frameSteps } from './frameloop/frame.mjs';

View File

@@ -0,0 +1,3 @@
const isCSSVar = (name) => name.startsWith("--");
export { isCSSVar };

View File

@@ -0,0 +1,83 @@
const radToDeg = (rad) => (rad * 180) / Math.PI;
const rotate = (v) => {
const angle = radToDeg(Math.atan2(v[1], v[0]));
return rebaseAngle(angle);
};
const matrix2dParsers = {
x: 4,
y: 5,
translateX: 4,
translateY: 5,
scaleX: 0,
scaleY: 3,
scale: (v) => (Math.abs(v[0]) + Math.abs(v[3])) / 2,
rotate,
rotateZ: rotate,
skewX: (v) => radToDeg(Math.atan(v[1])),
skewY: (v) => radToDeg(Math.atan(v[2])),
skew: (v) => (Math.abs(v[1]) + Math.abs(v[2])) / 2,
};
const rebaseAngle = (angle) => {
angle = angle % 360;
if (angle < 0)
angle += 360;
return angle;
};
const rotateZ = rotate;
const scaleX = (v) => Math.sqrt(v[0] * v[0] + v[1] * v[1]);
const scaleY = (v) => Math.sqrt(v[4] * v[4] + v[5] * v[5]);
const matrix3dParsers = {
x: 12,
y: 13,
z: 14,
translateX: 12,
translateY: 13,
translateZ: 14,
scaleX,
scaleY,
scale: (v) => (scaleX(v) + scaleY(v)) / 2,
rotateX: (v) => rebaseAngle(radToDeg(Math.atan2(v[6], v[5]))),
rotateY: (v) => rebaseAngle(radToDeg(Math.atan2(-v[2], v[0]))),
rotateZ,
rotate: rotateZ,
skewX: (v) => radToDeg(Math.atan(v[4])),
skewY: (v) => radToDeg(Math.atan(v[1])),
skew: (v) => (Math.abs(v[1]) + Math.abs(v[4])) / 2,
};
function defaultTransformValue(name) {
return name.includes("scale") ? 1 : 0;
}
function parseValueFromTransform(transform, name) {
if (!transform || transform === "none") {
return defaultTransformValue(name);
}
const matrix3dMatch = transform.match(/^matrix3d\(([-\d.e\s,]+)\)$/u);
let parsers;
let match;
if (matrix3dMatch) {
parsers = matrix3dParsers;
match = matrix3dMatch;
}
else {
const matrix2dMatch = transform.match(/^matrix\(([-\d.e\s,]+)\)$/u);
parsers = matrix2dParsers;
match = matrix2dMatch;
}
if (!match) {
return defaultTransformValue(name);
}
const valueParser = parsers[name];
const values = match[1].split(",").map(convertTransformToNumber);
return typeof valueParser === "function"
? valueParser(values)
: values[valueParser];
}
const readTransformValue = (instance, name) => {
const { transform = "none" } = getComputedStyle(instance);
return parseValueFromTransform(transform, name);
};
function convertTransformToNumber(value) {
return parseFloat(value.trim());
}
export { defaultTransformValue, parseValueFromTransform, readTransformValue };

View File

@@ -0,0 +1,10 @@
import { isCSSVar } from './is-css-var.mjs';
function getComputedStyle(element, name) {
const computedStyle = window.getComputedStyle(element);
return isCSSVar(name)
? computedStyle.getPropertyValue(name)
: computedStyle[name];
}
export { getComputedStyle };

View File

@@ -0,0 +1,9 @@
import { isCSSVar } from './is-css-var.mjs';
function setStyle(element, name, value) {
isCSSVar(name)
? element.style.setProperty(name, value)
: (element.style[name] = value);
}
export { setStyle };

View File

@@ -0,0 +1,5 @@
function camelToDash(str) {
return str.replace(/([A-Z])/g, (match) => `-${match.toLowerCase()}`);
}
export { camelToDash };

View File

@@ -0,0 +1,13 @@
import { transformPropOrder } from './keys-transform.mjs';
const positionalKeys = new Set([
"width",
"height",
"top",
"left",
"right",
"bottom",
...transformPropOrder,
]);
export { positionalKeys };

View File

@@ -0,0 +1,28 @@
/**
* Generate a list of every possible transform key.
*/
const transformPropOrder = [
"transformPerspective",
"x",
"y",
"z",
"translateX",
"translateY",
"translateZ",
"scale",
"scaleX",
"scaleY",
"rotate",
"rotateX",
"rotateY",
"rotateZ",
"skew",
"skewX",
"skewY",
];
/**
* A quick lookup for transform props.
*/
const transformProps = /*@__PURE__*/ (() => new Set(transformPropOrder))();
export { transformPropOrder, transformProps };

View File

@@ -0,0 +1,63 @@
import { isSVGElement } from '../utils/is-svg-element.mjs';
import { resolveElements } from '../utils/resolve-elements.mjs';
const resizeHandlers = new WeakMap();
let observer;
const getSize = (borderBoxAxis, svgAxis, htmlAxis) => (target, borderBoxSize) => {
if (borderBoxSize && borderBoxSize[0]) {
return borderBoxSize[0][(borderBoxAxis + "Size")];
}
else if (isSVGElement(target) && "getBBox" in target) {
return target.getBBox()[svgAxis];
}
else {
return target[htmlAxis];
}
};
const getWidth = /*@__PURE__*/ getSize("inline", "width", "offsetWidth");
const getHeight = /*@__PURE__*/ getSize("block", "height", "offsetHeight");
function notifyTarget({ target, borderBoxSize }) {
resizeHandlers.get(target)?.forEach((handler) => {
handler(target, {
get width() {
return getWidth(target, borderBoxSize);
},
get height() {
return getHeight(target, borderBoxSize);
},
});
});
}
function notifyAll(entries) {
entries.forEach(notifyTarget);
}
function createResizeObserver() {
if (typeof ResizeObserver === "undefined")
return;
observer = new ResizeObserver(notifyAll);
}
function resizeElement(target, handler) {
if (!observer)
createResizeObserver();
const elements = resolveElements(target);
elements.forEach((element) => {
let elementHandlers = resizeHandlers.get(element);
if (!elementHandlers) {
elementHandlers = new Set();
resizeHandlers.set(element, elementHandlers);
}
elementHandlers.add(handler);
observer?.observe(element);
});
return () => {
elements.forEach((element) => {
const elementHandlers = resizeHandlers.get(element);
elementHandlers?.delete(handler);
if (!elementHandlers?.size) {
observer?.unobserve(element);
}
});
};
}
export { resizeElement };

View File

@@ -0,0 +1,31 @@
const windowCallbacks = new Set();
let windowResizeHandler;
function createWindowResizeHandler() {
windowResizeHandler = () => {
const info = {
get width() {
return window.innerWidth;
},
get height() {
return window.innerHeight;
},
};
windowCallbacks.forEach((callback) => callback(info));
};
window.addEventListener("resize", windowResizeHandler);
}
function resizeWindow(callback) {
windowCallbacks.add(callback);
if (!windowResizeHandler)
createWindowResizeHandler();
return () => {
windowCallbacks.delete(callback);
if (!windowCallbacks.size &&
typeof windowResizeHandler === "function") {
window.removeEventListener("resize", windowResizeHandler);
windowResizeHandler = undefined;
}
};
}
export { resizeWindow };

8
node_modules/motion-dom/dist/es/resize/index.mjs generated vendored Normal file
View File

@@ -0,0 +1,8 @@
import { resizeElement } from './handle-element.mjs';
import { resizeWindow } from './handle-window.mjs';
function resize(a, b) {
return typeof a === "function" ? resizeWindow(a) : resizeElement(a, b);
}
export { resize };

18
node_modules/motion-dom/dist/es/scroll/observe.mjs generated vendored Normal file
View File

@@ -0,0 +1,18 @@
import { frame, cancelFrame } from '../frameloop/frame.mjs';
function observeTimeline(update, timeline) {
let prevProgress;
const onFrame = () => {
const { currentTime } = timeline;
const percentage = currentTime === null ? 0 : currentTime.value;
const progress = percentage / 100;
if (prevProgress !== progress) {
update(progress);
}
prevProgress = progress;
};
frame.preUpdate(onFrame, true);
return () => cancelFrame(onFrame);
}
export { observeTimeline };

View File

@@ -0,0 +1,7 @@
const activeAnimations = {
layout: 0,
mainThread: 0,
waapi: 0,
};
export { activeAnimations };

6
node_modules/motion-dom/dist/es/stats/buffer.mjs generated vendored Normal file
View File

@@ -0,0 +1,6 @@
const statsBuffer = {
value: null,
addProjectionMetrics: null,
};
export { statsBuffer };

117
node_modules/motion-dom/dist/es/stats/index.mjs generated vendored Normal file
View File

@@ -0,0 +1,117 @@
import { activeAnimations } from './animation-count.mjs';
import { statsBuffer } from './buffer.mjs';
import { frame, cancelFrame, frameData } from '../frameloop/frame.mjs';
function record() {
const { value } = statsBuffer;
if (value === null) {
cancelFrame(record);
return;
}
value.frameloop.rate.push(frameData.delta);
value.animations.mainThread.push(activeAnimations.mainThread);
value.animations.waapi.push(activeAnimations.waapi);
value.animations.layout.push(activeAnimations.layout);
}
function mean(values) {
return values.reduce((acc, value) => acc + value, 0) / values.length;
}
function summarise(values, calcAverage = mean) {
if (values.length === 0) {
return {
min: 0,
max: 0,
avg: 0,
};
}
return {
min: Math.min(...values),
max: Math.max(...values),
avg: calcAverage(values),
};
}
const msToFps = (ms) => Math.round(1000 / ms);
function clearStatsBuffer() {
statsBuffer.value = null;
statsBuffer.addProjectionMetrics = null;
}
function reportStats() {
const { value } = statsBuffer;
if (!value) {
throw new Error("Stats are not being measured");
}
clearStatsBuffer();
cancelFrame(record);
const summary = {
frameloop: {
setup: summarise(value.frameloop.setup),
rate: summarise(value.frameloop.rate),
read: summarise(value.frameloop.read),
resolveKeyframes: summarise(value.frameloop.resolveKeyframes),
preUpdate: summarise(value.frameloop.preUpdate),
update: summarise(value.frameloop.update),
preRender: summarise(value.frameloop.preRender),
render: summarise(value.frameloop.render),
postRender: summarise(value.frameloop.postRender),
},
animations: {
mainThread: summarise(value.animations.mainThread),
waapi: summarise(value.animations.waapi),
layout: summarise(value.animations.layout),
},
layoutProjection: {
nodes: summarise(value.layoutProjection.nodes),
calculatedTargetDeltas: summarise(value.layoutProjection.calculatedTargetDeltas),
calculatedProjections: summarise(value.layoutProjection.calculatedProjections),
},
};
/**
* Convert the rate to FPS
*/
const { rate } = summary.frameloop;
rate.min = msToFps(rate.min);
rate.max = msToFps(rate.max);
rate.avg = msToFps(rate.avg);
[rate.min, rate.max] = [rate.max, rate.min];
return summary;
}
function recordStats() {
if (statsBuffer.value) {
clearStatsBuffer();
throw new Error("Stats are already being measured");
}
const newStatsBuffer = statsBuffer;
newStatsBuffer.value = {
frameloop: {
setup: [],
rate: [],
read: [],
resolveKeyframes: [],
preUpdate: [],
update: [],
preRender: [],
render: [],
postRender: [],
},
animations: {
mainThread: [],
waapi: [],
layout: [],
},
layoutProjection: {
nodes: [],
calculatedTargetDeltas: [],
calculatedProjections: [],
},
};
newStatsBuffer.addProjectionMetrics = (metrics) => {
const { layoutProjection } = newStatsBuffer.value;
layoutProjection.nodes.push(metrics.nodes);
layoutProjection.calculatedTargetDeltas.push(metrics.calculatedTargetDeltas);
layoutProjection.calculatedProjections.push(metrics.calculatedProjections);
};
frame.postRender(record, true);
return reportStats;
}
export { recordStats };

74
node_modules/motion-dom/dist/es/utils/interpolate.mjs generated vendored Normal file
View File

@@ -0,0 +1,74 @@
import { invariant, clamp, MotionGlobalConfig, noop, pipe, progress } from 'motion-utils';
import { mix } from './mix/index.mjs';
function createMixers(output, ease, customMixer) {
const mixers = [];
const mixerFactory = customMixer || MotionGlobalConfig.mix || mix;
const numMixers = output.length - 1;
for (let i = 0; i < numMixers; i++) {
let mixer = mixerFactory(output[i], output[i + 1]);
if (ease) {
const easingFunction = Array.isArray(ease) ? ease[i] || noop : ease;
mixer = pipe(easingFunction, mixer);
}
mixers.push(mixer);
}
return mixers;
}
/**
* Create a function that maps from a numerical input array to a generic output array.
*
* Accepts:
* - Numbers
* - Colors (hex, hsl, hsla, rgb, rgba)
* - Complex (combinations of one or more numbers or strings)
*
* ```jsx
* const mixColor = interpolate([0, 1], ['#fff', '#000'])
*
* mixColor(0.5) // 'rgba(128, 128, 128, 1)'
* ```
*
* TODO Revisit this approach once we've moved to data models for values,
* probably not needed to pregenerate mixer functions.
*
* @public
*/
function interpolate(input, output, { clamp: isClamp = true, ease, mixer } = {}) {
const inputLength = input.length;
invariant(inputLength === output.length, "Both input and output ranges must be the same length", "range-length");
/**
* If we're only provided a single input, we can just make a function
* that returns the output.
*/
if (inputLength === 1)
return () => output[0];
if (inputLength === 2 && output[0] === output[1])
return () => output[1];
const isZeroDeltaRange = input[0] === input[1];
// If input runs highest -> lowest, reverse both arrays
if (input[0] > input[inputLength - 1]) {
input = [...input].reverse();
output = [...output].reverse();
}
const mixers = createMixers(output, ease, mixer);
const numMixers = mixers.length;
const interpolator = (v) => {
if (isZeroDeltaRange && v < input[0])
return output[0];
let i = 0;
if (numMixers > 1) {
for (; i < input.length - 2; i++) {
if (v < input[i + 1])
break;
}
}
const progressInRange = progress(input[i], input[i + 1], v);
return mixers[i](progressInRange);
};
return isClamp
? (v) => interpolator(clamp(input[0], input[inputLength - 1], v))
: interpolator;
}
export { interpolate };

View File

@@ -0,0 +1,11 @@
import { isObject } from 'motion-utils';
/**
* Checks if an element is an HTML element in a way
* that works across iframes
*/
function isHTMLElement(element) {
return isObject(element) && "offsetHeight" in element;
}
export { isHTMLElement };

View File

@@ -0,0 +1,11 @@
import { isObject } from 'motion-utils';
/**
* Checks if an element is an SVG element in a way
* that works across iframes
*/
function isSVGElement(element) {
return isObject(element) && "ownerSVGElement" in element;
}
export { isSVGElement };

View File

@@ -0,0 +1,11 @@
import { isSVGElement } from './is-svg-element.mjs';
/**
* Checks if an element is specifically an SVGSVGElement (the root SVG element)
* in a way that works across iframes
*/
function isSVGSVGElement(element) {
return isSVGElement(element) && element.tagName === "svg";
}
export { isSVGSVGElement };

47
node_modules/motion-dom/dist/es/utils/mix/color.mjs generated vendored Normal file
View File

@@ -0,0 +1,47 @@
import { warning } from 'motion-utils';
import { hex } from '../../value/types/color/hex.mjs';
import { hsla } from '../../value/types/color/hsla.mjs';
import { hslaToRgba } from '../../value/types/color/hsla-to-rgba.mjs';
import { rgba } from '../../value/types/color/rgba.mjs';
import { mixImmediate } from './immediate.mjs';
import { mixNumber } from './number.mjs';
// Linear color space blending
// Explained https://www.youtube.com/watch?v=LKnqECcg6Gw
// Demonstrated http://codepen.io/osublake/pen/xGVVaN
const mixLinearColor = (from, to, v) => {
const fromExpo = from * from;
const expo = v * (to * to - fromExpo) + fromExpo;
return expo < 0 ? 0 : Math.sqrt(expo);
};
const colorTypes = [hex, rgba, hsla];
const getColorType = (v) => colorTypes.find((type) => type.test(v));
function asRGBA(color) {
const type = getColorType(color);
warning(Boolean(type), `'${color}' is not an animatable color. Use the equivalent color code instead.`, "color-not-animatable");
if (!Boolean(type))
return false;
let model = type.parse(color);
if (type === hsla) {
// TODO Remove this cast - needed since Motion's stricter typing
model = hslaToRgba(model);
}
return model;
}
const mixColor = (from, to) => {
const fromRGBA = asRGBA(from);
const toRGBA = asRGBA(to);
if (!fromRGBA || !toRGBA) {
return mixImmediate(from, to);
}
const blended = { ...fromRGBA };
return (v) => {
blended.red = mixLinearColor(fromRGBA.red, toRGBA.red, v);
blended.green = mixLinearColor(fromRGBA.green, toRGBA.green, v);
blended.blue = mixLinearColor(fromRGBA.blue, toRGBA.blue, v);
blended.alpha = mixNumber(fromRGBA.alpha, toRGBA.alpha, v);
return rgba.transform(blended);
};
};
export { mixColor, mixLinearColor };

92
node_modules/motion-dom/dist/es/utils/mix/complex.mjs generated vendored Normal file
View File

@@ -0,0 +1,92 @@
import { pipe, warning } from 'motion-utils';
import { isCSSVariableToken } from '../../animation/utils/is-css-variable.mjs';
import { color } from '../../value/types/color/index.mjs';
import { complex, analyseComplexValue } from '../../value/types/complex/index.mjs';
import { mixColor } from './color.mjs';
import { mixImmediate } from './immediate.mjs';
import { mixNumber as mixNumber$1 } from './number.mjs';
import { invisibleValues, mixVisibility } from './visibility.mjs';
function mixNumber(a, b) {
return (p) => mixNumber$1(a, b, p);
}
function getMixer(a) {
if (typeof a === "number") {
return mixNumber;
}
else if (typeof a === "string") {
return isCSSVariableToken(a)
? mixImmediate
: color.test(a)
? mixColor
: mixComplex;
}
else if (Array.isArray(a)) {
return mixArray;
}
else if (typeof a === "object") {
return color.test(a) ? mixColor : mixObject;
}
return mixImmediate;
}
function mixArray(a, b) {
const output = [...a];
const numValues = output.length;
const blendValue = a.map((v, i) => getMixer(v)(v, b[i]));
return (p) => {
for (let i = 0; i < numValues; i++) {
output[i] = blendValue[i](p);
}
return output;
};
}
function mixObject(a, b) {
const output = { ...a, ...b };
const blendValue = {};
for (const key in output) {
if (a[key] !== undefined && b[key] !== undefined) {
blendValue[key] = getMixer(a[key])(a[key], b[key]);
}
}
return (v) => {
for (const key in blendValue) {
output[key] = blendValue[key](v);
}
return output;
};
}
function matchOrder(origin, target) {
const orderedOrigin = [];
const pointers = { color: 0, var: 0, number: 0 };
for (let i = 0; i < target.values.length; i++) {
const type = target.types[i];
const originIndex = origin.indexes[type][pointers[type]];
const originValue = origin.values[originIndex] ?? 0;
orderedOrigin[i] = originValue;
pointers[type]++;
}
return orderedOrigin;
}
const mixComplex = (origin, target) => {
const template = complex.createTransformer(target);
const originStats = analyseComplexValue(origin);
const targetStats = analyseComplexValue(target);
const canInterpolate = originStats.indexes.var.length === targetStats.indexes.var.length &&
originStats.indexes.color.length === targetStats.indexes.color.length &&
originStats.indexes.number.length >= targetStats.indexes.number.length;
if (canInterpolate) {
if ((invisibleValues.has(origin) &&
!targetStats.values.length) ||
(invisibleValues.has(target) &&
!originStats.values.length)) {
return mixVisibility(origin, target);
}
return pipe(mixArray(matchOrder(originStats, targetStats), targetStats.values), template);
}
else {
warning(true, `Complex values '${origin}' and '${target}' too different to mix. Ensure all colors are of the same type, and that each contains the same quantity of number and color values. Falling back to instant transition.`, "complex-values-different");
return mixImmediate(origin, target);
}
};
export { getMixer, mixArray, mixComplex, mixObject };

View File

@@ -0,0 +1,5 @@
function mixImmediate(a, b) {
return (p) => (p > 0 ? b : a);
}
export { mixImmediate };

14
node_modules/motion-dom/dist/es/utils/mix/index.mjs generated vendored Normal file
View File

@@ -0,0 +1,14 @@
import { getMixer } from './complex.mjs';
import { mixNumber } from './number.mjs';
function mix(from, to, p) {
if (typeof from === "number" &&
typeof to === "number" &&
typeof p === "number") {
return mixNumber(from, to, p);
}
const mixer = getMixer(from);
return mixer(from, to);
}
export { mix };

26
node_modules/motion-dom/dist/es/utils/mix/number.mjs generated vendored Normal file
View File

@@ -0,0 +1,26 @@
/*
Value in range from progress
Given a lower limit and an upper limit, we return the value within
that range as expressed by progress (usually a number from 0 to 1)
So progress = 0.5 would change
from -------- to
to
from ---- to
E.g. from = 10, to = 20, progress = 0.5 => 15
@param [number]: Lower limit of range
@param [number]: Upper limit of range
@param [number]: The progress between lower and upper limits expressed 0-1
@return [number]: Value as calculated from progress within range (not limited within range)
*/
const mixNumber = (from, to, progress) => {
return from + (to - from) * progress;
};
export { mixNumber };

View File

@@ -0,0 +1,16 @@
const invisibleValues = new Set(["none", "hidden"]);
/**
* Returns a function that, when provided a progress value between 0 and 1,
* will return the "none" or "hidden" string only when the progress is that of
* the origin or target.
*/
function mixVisibility(origin, target) {
if (invisibleValues.has(origin)) {
return (p) => (p <= 0 ? origin : target);
}
else {
return (p) => (p >= 1 ? target : origin);
}
}
export { invisibleValues, mixVisibility };

Some files were not shown because too many files have changed in this diff Show More