build: ship pre-built API bundle; images copy output instead of installing

- API: `pnpm build` bundles dist/ into apps/api/.output (esbuild), with
  Linux musl native packages for sharp and argon2 kept alongside
- docker/api.Dockerfile copies that output; no pnpm install on the server
- add dist/migrate.js (prisma migrate deploy equivalent, same
  _prisma_migrations table and lock) so the Prisma CLI is not needed at runtime
- pnpm-workspace: supportedArchitectures for linux/x64/musl
- admin: apps/admin/.env.production sets VITE_SITE_URL for the production build
- drop NPM_REGISTRY / SITE_URL build args from compose and .env.example
- gitignore: anchor node_modules rules so pre-built node_modules are committed
- README: describe the pre-built deploy flow

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
S. Esmaeili
2026-10-24 14:41:31 +03:30
co-authored by Claude Haiku 5.5
parent ee8258556d
commit 10e453e4be
692 changed files with 303601 additions and 147 deletions
@@ -0,0 +1,206 @@
import { MotionGlobalConfig } from 'motion-utils';
import { time } from '../frameloop/sync-time.mjs';
import { JSAnimation } from './JSAnimation.mjs';
import { getFinalKeyframe } from './keyframes/get-final.mjs';
import { KeyframeResolver, flushKeyframeResolvers } from './keyframes/KeyframesResolver.mjs';
import { NativeAnimationExtended } from './NativeAnimationExtended.mjs';
import { canAnimate } from './utils/can-animate.mjs';
import { makeAnimationInstant } from './utils/make-animation-instant.mjs';
import { resolveStartTime } from './utils/resolve-start-time.mjs';
import { WithPromise } from './utils/WithPromise.mjs';
import { supportsBrowserAnimation } from './waapi/supports/waapi.mjs';
import { canGroupTransform, TransformAnimation } from './waapi/transforms/TransformAnimation.mjs';
class AsyncMotionValueAnimation extends WithPromise {
constructor(options) {
super();
/**
* Bound to support return animation.stop pattern
*/
this.stop = () => {
if (this._animation) {
this._animation.stop();
this.stopTimeline?.();
}
this.keyframeResolver?.cancel();
};
this.createdAt = time.now();
const { keyframes, name, motionValue, element } = options;
/**
* animateMotionValue builds a fresh options object per value, so
* it's completed in place as keyframes resolve rather than copied.
*/
const optionsWithDefaults = options;
optionsWithDefaults.autoplay ?? (optionsWithDefaults.autoplay = true);
optionsWithDefaults.delay ?? (optionsWithDefaults.delay = 0);
optionsWithDefaults.type ?? (optionsWithDefaults.type = "keyframes");
optionsWithDefaults.repeat ?? (optionsWithDefaults.repeat = 0);
optionsWithDefaults.repeatDelay ?? (optionsWithDefaults.repeatDelay = 0);
optionsWithDefaults.repeatType ?? (optionsWithDefaults.repeatType = "loop");
const KeyframeResolver$1 = element?.KeyframeResolver || KeyframeResolver;
this.keyframeResolver = new KeyframeResolver$1(keyframes, (resolvedKeyframes, finalKeyframe, forced) => this.onKeyframesResolved(resolvedKeyframes, finalKeyframe, optionsWithDefaults, !forced), name, motionValue, element);
this.keyframeResolver?.scheduleResolve();
}
onKeyframesResolved(keyframes, finalKeyframe, options, sync) {
this.keyframeResolver = undefined;
const { name, type, velocity, delay, isHandoff, onUpdate } = options;
this.resolvedAt = time.now();
/**
* If we can't animate this value with the resolved keyframes
* then we should complete it immediately.
*/
let canAnimateValue = true;
if (!canAnimate(keyframes, name, type, velocity)) {
canAnimateValue = false;
if (MotionGlobalConfig.instantAnimations || !delay) {
onUpdate?.(getFinalKeyframe(keyframes, options, finalKeyframe));
}
keyframes[0] = keyframes[keyframes.length - 1];
makeAnimationInstant(options);
options.repeat = 0;
}
const { onComplete } = options;
options.finalKeyframe = finalKeyframe;
options.keyframes = keyframes;
/**
* JSAnimation and NativeAnimation call onComplete exactly when
* their own `finished` resolves, so this replaces a promise chain
* per value with a callback.
*/
options.onComplete = () => {
onComplete?.();
this.notifyFinished();
};
/**
* Animate via WAAPI if possible. If this is a handoff animation, the optimised animation will be running via
* WAAPI. Therefore, this animation must be JS to ensure it runs "under" the
* optimised animation.
*
* Also skip WAAPI when keyframes aren't animatable, as the resolved
* values may not be valid CSS and would trigger browser warnings.
*/
const useWaapi = canAnimateValue && !isHandoff && supportsBrowserAnimation(options);
/**
* Independent transforms (x, scale etc) on an HTML element are
* composed into one WAAPI transform animation per element.
*/
const isGroupedTransform = !useWaapi && canGroupTransform(options);
/**
* Resolve startTime for the animation. A startTime passed in options
* (an optimised appear handoff syncing to its WAAPI animation) takes
* precedence.
*
* *Ideally*, we would use the createdAt time as t=0 as the following
* frame would then be the first frame of the animation in progress,
* which would feel snappier. If keyframes resolved on a later frame,
* and long after creation, we start from then instead.
*
* If they resolved immediately, a JSAnimation picks its own start
* time and resolves it the same way on its first frame, so every
* animation started in that moment stays in sync. WAAPI has no frame
* of ours to do that on.
*/
if (sync &&
(useWaapi ||
isGroupedTransform ||
this.resolvedAt !== this.createdAt)) {
options.startTime ?? (options.startTime = resolveStartTime(this.createdAt, this.resolvedAt));
}
let animation;
if (useWaapi) {
/**
* The resolver needed the VisualElement, WAAPI needs the DOM
* element. JSAnimation reads neither, so this is safe to
* leave in place if we fall back to it.
*/
options.element = options.motionValue?.owner?.current;
try {
animation = new NativeAnimationExtended(options);
}
catch {
animation = new JSAnimation(options);
}
}
else if (isGroupedTransform) {
animation = new TransformAnimation(options);
}
else {
animation = new JSAnimation(options);
}
if (this.pendingTimeline) {
this.stopTimeline = animation.attachTimeline(this.pendingTimeline);
this.pendingTimeline = undefined;
}
this._animation = animation;
}
get finished() {
return this._animation ? this._animation.finished : super.finished;
}
then(onResolve, _onReject) {
return this.finished.finally(onResolve).then(() => { });
}
/**
* The value now, if the animation runs on the compositor.
*/
liveValue() {
return this._animation?.liveValue?.();
}
get animation() {
if (!this._animation) {
this.keyframeResolver?.resume();
flushKeyframeResolvers();
}
return this._animation;
}
get duration() {
return this.animation.duration;
}
get iterationDuration() {
return this.animation.iterationDuration;
}
get time() {
return this.animation.time;
}
set time(newTime) {
this.animation.time = newTime;
}
get speed() {
return this.animation.speed;
}
get state() {
return this.animation.state;
}
set speed(newSpeed) {
this.animation.speed = newSpeed;
}
get startTime() {
return this.animation.startTime;
}
attachTimeline(timeline) {
if (this._animation) {
this.stopTimeline = this.animation.attachTimeline(timeline);
}
else {
this.pendingTimeline = timeline;
}
return () => this.stop();
}
play() {
this.animation.play();
}
pause() {
this.animation.pause();
}
complete() {
this.animation.complete();
}
cancel() {
if (this._animation) {
this.animation.cancel();
}
this.keyframeResolver?.cancel();
}
}
export { AsyncMotionValueAnimation };
//# sourceMappingURL=AsyncMotionValueAnimation.mjs.map
@@ -0,0 +1,154 @@
import { time } from '../frameloop/sync-time.mjs';
import { calcGeneratorVelocity } from './generators/utils/velocity.mjs';
import { resolveStartTime } from './utils/resolve-start-time.mjs';
import { WithPromise } from './utils/WithPromise.mjs';
import { frame, cancelFrame } from '../frameloop/frame.mjs';
/**
* Every running follower ticks from this one frame callback.
*/
const active = new Set();
const tickAll = ({ timestamp }) => {
active.forEach((animation) => animation.tick(timestamp));
};
/**
* A lean animation for values that follow a target (`followValue`,
* `springValue`): a single numeric generator, retargeted in place as
* the target moves and ticked from a frame callback shared by every
* follower. Only implements the controls `MotionValue.animation`
* exposes. Repeat options are ignored: a follower only ever heads for
* its latest target.
*/
class FollowAnimation extends WithPromise {
constructor(options) {
super();
this.state = "idle";
this.startTime = 0;
this.currentTime = 0;
/**
* Whether the first tick has fired `onPlay`.
*/
this.started = false;
/**
* A target set since the last tick, applied at the next one so any
* number of sets per frame cost one retarget.
*/
this.hasNextTarget = false;
this.nextTarget = 0;
/**
* Bound to the instance so `animation.stop` can be passed around.
*/
this.stop = () => {
if (this.state === "idle")
return;
this.teardown();
this.options.onStop?.();
};
this.options = options;
this.factory = options.type;
this.generator = this.factory(options);
const { driver } = options;
if (driver)
this.driver = driver((timestamp) => this.tick(timestamp));
this.startTime = this.now();
this.state = "running";
if (this.driver) {
this.driver.start();
}
else {
active.size || frame.update(tickAll, true);
active.add(this);
}
}
/**
* Aim at a new target from wherever the animation has reached by the
* next tick. `velocity` fixes the velocity to steer with, otherwise
* the generator's is used.
*/
setTarget(target, velocity) {
this.nextTarget = target;
this.nextVelocity = velocity;
this.hasNextTarget = true;
}
/**
* Steer towards new keyframes from the current position and velocity.
* Springs are updated in place; other generators are recreated.
*/
retarget(keyframes, velocity) {
const { options, generator } = this;
options.keyframes = keyframes;
options.velocity = velocity;
this.startTime = this.now();
this.currentTime = 0;
if (generator.retarget) {
generator.retarget(keyframes, velocity);
}
else {
this.generator = this.factory(options);
}
}
tick(timestamp) {
const { options, hasNextTarget: retargeted } = this;
const { delay = 0, onUpdate, onPlay } = options;
// Retargets happen on a tick, so only the first start can lag
if (!this.started) {
this.startTime = resolveStartTime(this.startTime, timestamp);
}
const elapsed = Math.round(timestamp - this.startTime) - delay;
const t = (this.currentTime = Math.max(0, elapsed));
const state = this.generator.next(t);
// Hold the origin through the delay, even if the generator is already at rest
const value = elapsed < 0 ? options.keyframes[0] : state.value;
if (retargeted) {
/**
* Steer from where the current trajectory has reached this
* frame, then render that position as the new origin.
*/
this.hasNextTarget = false;
const { keyframes } = options;
keyframes[0] = value;
keyframes[1] = this.nextTarget;
this.retarget(keyframes, this.nextVelocity ?? this.getGeneratorVelocity());
}
if (retargeted || !this.started) {
this.started = true;
onPlay?.();
}
/**
* Callbacks may stop this animation or set a new target, so check
* the live state rather than what was read at the top of the tick.
*/
if (this.state !== "running")
return;
onUpdate?.(value);
if (state.done &&
elapsed >= 0 &&
!retargeted &&
!this.hasNextTarget &&
this.state === "running") {
this.notifyFinished();
this.teardown();
this.state = "finished";
options.onComplete?.();
}
}
getGeneratorVelocity() {
return calcGeneratorVelocity(this.generator, this.currentTime, this.options.velocity);
}
now() {
return this.driver ? this.driver.now() : time.now();
}
teardown() {
this.state = "idle";
if (this.driver) {
this.driver.stop();
}
else {
active.delete(this);
active.size || cancelFrame(tickAll);
}
}
}
export { FollowAnimation };
//# sourceMappingURL=FollowAnimation.mjs.map
@@ -0,0 +1,82 @@
class GroupAnimation {
constructor(animations) {
// Bound to accommodate 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 };
//# sourceMappingURL=GroupAnimation.mjs.map
@@ -0,0 +1,10 @@
import { GroupAnimation } from './GroupAnimation.mjs';
class GroupAnimationWithThen extends GroupAnimation {
then(onResolve, _onReject) {
return this.finished.finally(onResolve).then(() => { });
}
}
export { GroupAnimationWithThen };
//# sourceMappingURL=GroupAnimationWithThen.mjs.map
@@ -0,0 +1,418 @@
import { invariant, pipe, clamp, millisecondsToSeconds, secondsToMilliseconds } from 'motion-utils';
import { time } from '../frameloop/sync-time.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 { calcGeneratorVelocity } from './generators/utils/velocity.mjs';
import { getFinalKeyframe } from './keyframes/get-final.mjs';
import { replaceTransitionType } from './utils/replace-transition-type.mjs';
import { resolveStartTime } from './utils/resolve-start-time.mjs';
import { notifyAnimationStart } from './utils/notify-inspector.mjs';
import { WithPromise } from './utils/WithPromise.mjs';
const percentToProgress = (percent) => percent / 100;
const round = (value) => Math.round(value * 10000) / 10000;
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;
/**
* Reusable state object for the delay phase to avoid
* allocating a new object every frame.
*/
this.delayState = {
done: false,
value: undefined,
};
/**
* 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?.();
};
this.options = options;
this.initAnimation();
this.play();
if (options.autoplay === false)
this.pause();
notifyAnimationStart(this, this.options);
}
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(keyframes$1 === options.keyframes
? options
: { ...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, calculatedDuration } = this;
if (this.startTime === null)
return generator.next(0);
const { delay = 0, keyframes, type, onUpdate, finalKeyframe, } = this.options;
if (this.startTime === this.pendingStartTime) {
this.startTime = resolveStartTime(this.startTime, timestamp);
}
this.pendingStartTime = undefined;
/**
* 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;
}
const state = this.generate(this.currentTime, isInDelayPhase);
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;
}
/**
* This animation's time, including delay, at a timestamp.
*/
at(timestamp) {
return (this.holdTime ??
round((timestamp - this.startTime) * this.playbackSpeed));
}
/**
* The value at a timestamp, without changing this animation's state.
* If resolved, it ends at the last resolved keyframe, which is in the
* same unit as the others, rather than at finalKeyframe.
*/
sampleAt(timestamp, resolved) {
const { options, totalDuration } = this;
const t = this.at(timestamp) - (options.delay || 0);
return t >= totalDuration && options.type !== inertia
? getFinalKeyframe(options.keyframes, options, resolved ? undefined : options.finalKeyframe)
: this.generate(Math.max(t, 0), t < 0).value;
}
/**
* The generator's state `currentTime` ms after the delay, without
* changing the animation's own state.
*/
generate(currentTime, isInDelayPhase) {
const { generator, totalDuration, mixKeyframes, mirroredGenerator, resolvedDuration, } = this;
const { keyframes, repeat, repeatType, repeatDelay } = this.options;
let elapsed = currentTime;
let frameGenerator = generator;
if (repeat) {
/**
* Get the current progress (0-1) of the animation. If t is >
* than duration we'll get values like 2.5 (midway through the
* third iteration)
*/
const progress = Math.min(currentTime, totalDuration) / resolvedDuration;
/**
* Get the current iteration (0 indexed). For instance the floor of
* 2.5 is 2.
*/
let currentIteration = Math.floor(progress);
/**
* Get the current progress of the iteration by taking the remainder
* so 2.5 is 0.5 through iteration 2
*/
let iterationProgress = progress % 1.0;
/**
* If iteration progress is 1 we count that as the end
* of the previous iteration.
*/
if (!iterationProgress && progress >= 1) {
iterationProgress = 1;
}
iterationProgress === 1 && currentIteration--;
currentIteration = Math.min(currentIteration, repeat + 1);
/**
* Reverse progress if we're not running in "normal" direction
*/
const isOddIteration = Boolean(currentIteration % 2);
if (isOddIteration) {
if (repeatType === "reverse") {
iterationProgress = 1 - iterationProgress;
if (repeatDelay) {
iterationProgress -= repeatDelay / resolvedDuration;
}
}
else if (repeatType === "mirror") {
frameGenerator = mirroredGenerator;
}
}
elapsed = clamp(0, 1, iterationProgress) * resolvedDuration;
}
/**
* If we're in negative time, set state as the initial keyframe.
* This prevents delay: x, duration: 0 animations from finishing
* instantly.
*/
let state;
if (isInDelayPhase) {
this.delayState.value = keyframes[0];
state = this.delayState;
}
else {
state = frameGenerator.next(elapsed);
}
if (mixKeyframes && !isInDelayPhase) {
state.value = mixKeyframes(state.value);
}
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;
}
if (this.driver) {
this.driver.start(false);
}
else {
this.startTime = 0;
this.state = "paused";
this.holdTime = newTime;
this.tick(newTime);
}
}
/**
* Returns the generator's velocity at the current time in units/second.
* Uses the analytical derivative when available (springs), avoiding
* the MotionValue's frame-dependent velocity estimation.
*/
getGeneratorVelocity() {
return calcGeneratorVelocity(this.generator, this.currentTime, this.options.velocity);
}
get speed() {
return this.playbackSpeed;
}
set speed(newSpeed) {
const hasChanged = this.playbackSpeed !== newSpeed;
if (hasChanged && this.driver) {
this.updateTime(time.now());
}
this.playbackSpeed = newSpeed;
if (hasChanged && this.driver) {
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 / (this.playbackSpeed || 1);
}
else if (!this.startTime) {
this.startTime = startTime ?? (this.pendingStartTime = 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;
}
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, round };
//# sourceMappingURL=JSAnimation.mjs.map
@@ -0,0 +1,178 @@
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 { notifyAnimationStart } from './utils/notify-inspector.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;
/**
* Tracks a manually-set start time that takes precedence over WAAPI's
* dynamic startTime. This is cleared when play() or time setter is called,
* allowing WAAPI to take over timing.
*/
this.manualStartTime = null;
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);
}
/**
* 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.
* We always do this, even when a motion value is present, to prevent
* a visual flash in Firefox where the WAAPI animation's fill is removed
* during cancel() before the scheduled render can apply the correct value.
*/
setStyle(element, name, keyframe);
this.animation.cancel();
}
onComplete?.();
this.notifyFinished();
};
notifyAnimationStart(this, options, transition);
}
play() {
if (this.isStopped)
return;
this.manualStartTime = null;
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() {
const element = this.options?.element;
if (!this.isPseudoElement && element?.isConnected) {
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) {
const wasFinished = this.finishedTime !== null;
this.manualStartTime = null;
this.finishedTime = null;
this.animation.currentTime = secondsToMilliseconds(newTime);
if (wasFinished) {
this.animation.pause();
}
}
/**
* 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 this.manualStartTime ?? Number(this.animation.startTime);
}
set startTime(newStartTime) {
this.manualStartTime = this.animation.startTime = newStartTime;
}
/**
* Attaches a timeline to the animation, for instance the `ScrollTimeline`.
*/
attachTimeline({ timeline, onAttach, observe, }) {
if (this.allowFlatten) {
this.animation.effect?.updateTiming({ easing: "linear" });
}
this.animation.onfinish = null;
if (timeline && supportsScrollTimeline()) {
this.animation.timeline = timeline;
onAttach?.(this.animation);
return noop;
}
else {
return observe(this);
}
}
}
export { NativeAnimation };
//# sourceMappingURL=NativeAnimation.mjs.map
@@ -0,0 +1,115 @@
import { clamp } from 'motion-utils';
import { time } from '../frameloop/sync-time.mjs';
import { setStyle } from '../render/dom/style-set.mjs';
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);
/**
* Only set startTime when the animation should autoplay.
* Setting startTime on a paused WAAPI animation unpauses it
* (per the WAAPI spec), which breaks autoplay: false.
*/
if (options.startTime !== undefined && options.autoplay !== false) {
this.startTime = options.startTime;
}
this.options = options;
}
/**
* The animation's time now, in ms. Wall-clock rather than WAAPI's
* currentTime while running, as under CPU load currentTime may not
* reflect the actual elapsed time, causing incorrect sampling and
* visual jumps.
*/
elapsed() {
return this.state === "running"
? (time.now() - this.startTime) * this.speed
: this.time * 1000;
}
/**
* The value at a time, from a renderless JS animation.
*/
sample(sampleTime) {
if (!this.sampler) {
const { motionValue, onUpdate, onComplete, onPlay, onStop, element, ...options } = this.options;
/**
* Stop it straight away so its driver doesn't run every frame.
* It can still be sampled.
*/
this.sampler = new JSAnimation({ ...options, autoplay: false });
this.sampler.stop();
}
return this.sampler.sample(Math.max(0, sampleTime)).value;
}
/**
* The value now, as WAAPI doesn't set the motion value while it runs.
*/
liveValue() {
const { state } = this;
if (state === "running" || state === "paused") {
return this.sample(this.elapsed());
}
}
/**
* WAAPI doesn't natively have any interruption capabilities.
*
* Rather than read committed styles back out of the DOM, we can
* sample a renderless JS animation twice to calculate its current
* value, "previous" value, and therefore allow Motion to calculate
* velocity for any subsequent animation.
*/
updateMotionValue(value) {
const { motionValue, element, name } = this.options;
if (!motionValue)
return;
if (value !== undefined) {
motionValue.set(value);
return;
}
const { speed } = this;
const sampleTime = this.elapsed();
const delta = this.state === "running" && speed
? clamp(0, sampleDelta, sampleTime)
: 0;
const current = this.sample(sampleTime);
/**
* Write the estimated value to inline style so it persists
* after cancel(), covering the async gap before the next
* animation starts.
*/
if (element && name)
setStyle(element, name, current);
motionValue.setWithVelocity(this.sample(sampleTime - delta * Math.sign(speed)), current, delta / Math.abs(speed || 1));
}
}
export { NativeAnimationExtended, sampleDelta };
//# sourceMappingURL=NativeAnimationExtended.mjs.map
@@ -0,0 +1,22 @@
import { NativeAnimation } from './NativeAnimation.mjs';
import { notifyAnimationStart } from './utils/notify-inspector.mjs';
class NativeAnimationWrapper extends NativeAnimation {
constructor(animation) {
super();
this.animation = animation;
animation.onfinish = () => {
this.finishedTime = this.time;
this.notifyFinished();
};
/**
* The optionless super() returns before NativeAnimation's own
* notify, so wrapped animations (view transition cross-fades)
* report here, after the wrapped WAAPI animation is attached.
*/
notifyAnimationStart(this, {});
}
}
export { NativeAnimationWrapper };
//# sourceMappingURL=NativeAnimationWrapper.mjs.map
@@ -0,0 +1,113 @@
import { invariant, removeItem } from 'motion-utils';
import { positionalKeys } from '../../render/utils/keys-position.mjs';
import { motionValue } from '../../value/index.mjs';
import { animateMotionValue } from '../interfaces/motion-value.mjs';
import { frame } from '../../frameloop/frame.mjs';
/**
* Effects registered via `animate.addEffect()`, most recent first.
*/
const effects = [];
function addEffect(effect) {
invariant(typeof effect.test === "function" && typeof effect.read === "function", "Effects passed to animate.addEffect() need test() and read().", "effect-missing-test");
removeEffect(effect);
effects.unshift(effect);
}
function removeEffect(effect) {
removeItem(effects, effect);
}
function findEffect(subject) {
return effects.find((effect) => effect.test(subject));
}
/**
* Animate each key in `keyframes` on the motion value `getValue` returns
* for it. `element` is what the keyframe resolver uses to read, render
* and measure the subject when the effect defers resolution to it.
*/
function animateValues(getValue, keyframes, transition = {}, element) {
const animations = [];
const { velocity, path } = transition;
const reduceMotion = transition.reduceMotion ?? element?.shouldReduceMotion;
/**
* Only elements have x/y to move along a path. path claims them by
* deleting them, so hand it a copy.
*/
if (path?.animateValues && element) {
keyframes = { ...keyframes };
path.animateValues(getValue, keyframes, transition, 0, animations);
}
for (const key in keyframes) {
if (key === "transition" || key === "transitionEnd")
continue;
const target = keyframes[key];
if (target === undefined)
continue;
const value = getValue(key);
/**
* If the value is already at the defined target, skip the animation.
* We still re-assert the value via frame.update to take precedence
* over any stale transitionEnd callbacks from previous animations.
*/
const current = value.get();
if (current !== undefined &&
!value.isAnimating() &&
!Array.isArray(target) &&
target === current &&
!velocity) {
frame.update(() => value.set(target));
continue;
}
value.start(animateMotionValue(key, value, target, reduceMotion && positionalKeys.has(key)
? { type: false }
: transition, element));
value.animation &&
animations.push(value.animation);
}
const { transitionEnd } = keyframes;
if (transitionEnd) {
const applyTransitionEnd = () => frame.update(() => {
for (const key in transitionEnd) {
getValue(key).set(transitionEnd[key]);
}
});
animations.length
? Promise.all(animations).then(applyTransitionEnd)
: applyTransitionEnd();
}
return animations;
}
/**
* Animate the keys of `subject` via `effect`. Motion values are created on
* first animation and bound to the subject through the effect for the
* rest of its life.
*
* Without `element`, a new value is seeded synchronously from the first
* keyframe or `effect.read()`. With it, the value starts undefined and the
* keyframe resolver reads the subject (batched, with unit conversion by
* measurement) via `element`.
*/
function animateEffectSubject(effect, subject, keyframes, transition, element) {
return animateValues((key) => {
let value = effect.get(subject, key);
if (!value) {
let initial;
if (!element) {
const target = keyframes[key];
initial =
firstKeyframe(target) ??
effect.read(subject, key, target);
invariant(initial !== undefined, `"${key}" can't be read from the animated subject. Provide [from, to] keyframes.`, "effect-unreadable-value");
}
value = motionValue(initial, { owner: element });
effect(subject, { [key]: value });
}
return value;
}, keyframes, transition, element);
}
function firstKeyframe(target) {
const first = Array.isArray(target) ? target[0] : undefined;
return first === null ? undefined : first;
}
export { addEffect, animateEffectSubject, animateValues, findEffect, removeEffect };
//# sourceMappingURL=effects.mjs.map
@@ -0,0 +1,80 @@
import { styleSubjectEffect } from '../../effects/style/index.mjs';
import { svgSubjectEffect } from '../../effects/svg/index.mjs';
import { measureViewportBox } from '../../projection/utils/measure.mjs';
import { isSVGElement } from '../../utils/is-svg-element.mjs';
import { DOMKeyframesResolver } from '../keyframes/DOMKeyframesResolver.mjs';
import { animateValues, animateEffectSubject } from './effects.mjs';
const noProps = {};
const getElementEffect = (element) => (isSVGElement(element)
? svgSubjectEffect
: styleSubjectEffect);
/**
* Presents an effect bound to a DOM element in the shape the DOM keyframe
* resolver and WAAPI expect, which is that of a VisualElement. Holds no
* state: everything delegates to the effect. Goes once the resolver takes
* an effect and subject directly.
*/
class EffectSubject {
constructor(effect, current) {
this.effect = effect;
this.current = current;
this.KeyframeResolver = DOMKeyframesResolver;
}
getValue(key) {
return this.effect.get(this.current, key);
}
readValue(key, target) {
return this.effect.read(this.current, key, target);
}
render() {
this.effect.flush(this.current);
}
measureViewportBox() {
return measureViewportBox(this.current);
}
getProps() {
return noProps;
}
}
/**
* Animate a DOM element's styles. HTML elements are bound through
* styleEffect, SVG through svgEffect, and both are resolved with the DOM
* keyframe resolver so reads are batched, units are converted by
* measurement and eligible values run on WAAPI.
*
* Pass the element's VisualElement (a <motion.*> component or an
* animateLayout() node) to animate its values instead, so the two keep
* sharing values and a renderer.
*/
function animateElement(element, keyframes, transition, visualElement) {
if (visualElement) {
return animateValues((key) => visualElement.getValue(key, null), keyframes, transition, visualElement);
}
const effect = getElementEffect(element);
return animateEffectSubject(effect, element, keyframes, transition, new EffectSubject(effect, element));
}
/**
* Hand the values styleEffect/svgEffect are rendering on `element`,
* whether bound by animate() or directly, to a VisualElement that now
* owns the element (e.g. created by animateLayout()), so a single
* renderer drives them alongside its own values.
*/
function handOffElementState(element, visualElement) {
const state = getElementEffect(element).state(element);
if (!state)
return;
const { transformKeys } = state;
state.release().forEach((value, key) => {
/**
* The style effect derives transform (from the bound transform
* keys) and transformBox itself; a VisualElement builds its own.
*/
const derived = key === "transformBox" ||
(key === "transform" && transformKeys?.length);
derived || visualElement.addValue(key, value);
});
}
export { animateElement, handOffElementState };
//# sourceMappingURL=element.mjs.map
@@ -0,0 +1,12 @@
import { animateMotionValue } from '../interfaces/motion-value.mjs';
import { motionValue } from '../../value/index.mjs';
import { isMotionValue } from '../../value/utils/is-motion-value.mjs';
function animateSingleValue(value, keyframes, options) {
const motionValue$1 = isMotionValue(value) ? value : motionValue(value);
motionValue$1.start(animateMotionValue("", motionValue$1, keyframes, options));
return motionValue$1.animation;
}
export { animateSingleValue };
//# sourceMappingURL=single-value.mjs.map
@@ -0,0 +1,19 @@
import { time } from '../../frameloop/sync-time.mjs';
import { frameData } from '../../frameloop/frame-data.mjs';
import { cancelFrame, frame } 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 };
//# sourceMappingURL=frame.mjs.map
@@ -0,0 +1,90 @@
import { spring } from './spring.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) => v < min || 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 applyFriction = (t) => {
const delta = calcDelta(t);
state.done = Math.abs(delta) <= restDelta;
state.value = state.done ? target : target + delta;
};
/**
* 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)],
/**
* The friction curve is target + calcDelta(t), so its exact
* derivative is -calcDelta(t) / timeConstant in units/ms,
* converted here to the units/second expected by spring.
*/
velocity: (-calcDelta(t) / timeConstant) * 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 };
//# sourceMappingURL=inertia.mjs.map
@@ -0,0 +1,74 @@
import { easeInOut, isEasingArray, easingDefinitionToFunction, MotionGlobalConfig, clamp } from 'motion-utils';
import { interpolate } from '../../utils/interpolate.mjs';
import { mix } from '../../utils/mix/index.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) || easeInOut;
/**
* 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],
};
/**
* Fast setup path for two keyframes one easing.
*/
if (keyframeValues.length === 2 &&
!Array.isArray(easingFunctions) &&
(!times ||
times.length !== 2 ||
(times[0] === 0 && times[1] === 1))) {
const [origin, target] = keyframeValues;
const mixer = origin === target
? undefined
: (MotionGlobalConfig.mix || mix)(origin, target);
return {
calculatedDuration: duration,
next: (t) => {
state.value = mixer
? mixer(easingFunctions(duration > 0 ? clamp(0, 1, t / duration) : 1))
: target;
state.done = t >= duration;
return state;
},
};
}
/**
* 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 };
//# sourceMappingURL=keyframes.mjs.map
@@ -0,0 +1,399 @@
import { millisecondsToSeconds, secondsToMilliseconds, warning, 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';
const springDefaults = {
// Default spring physics
stiffness: 100,
damping: 10,
mass: 1.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,
};
/**
* Maps bounce to a damping ratio as SwiftUI does: 0 is critically damped, and
* it approaches undamped towards 1 and infinitely overdamped towards -1. Both
* ends are limited so the spring still moves and settles.
*/
const bounceToDampingRatio = (bounce) => bounce < 0
? 1 / Math.max(1 + bounce, springDefaults.minDamping)
: Math.max(1 - bounce, springDefaults.minDamping);
/**
* Scales an overdamped spring's undamped frequency so that, at its defining
* time, it has as far left to go as the critically damped spring that's
* criticalFreqTime radians in by then. A negative bounce then changes the
* shape of the curve, not when it arrives. 1 at a damping ratio of 1, so
* there's no jump at a bounce of 0.
*
* Solved by Newton on the remaining distance (times fast - slow), which is
* convex from criticalFreqTime onwards. criticalFreqTime is always below the
* answer, so it converges from below without overshooting.
*/
function overdampedFreqScale(dampingRatio, criticalFreqTime) {
if (!(dampingRatio > 1))
return 1;
const root = Math.sqrt(dampingRatio * dampingRatio - 1);
const slow = dampingRatio - root;
const fast = dampingRatio + root;
const target = 2 * root * Math.exp(-criticalFreqTime) * (1 + criticalFreqTime);
return (approximateRoot((freqTime) => fast * Math.exp(-slow * freqTime) -
slow * Math.exp(-fast * freqTime) -
target, (freqTime) => Math.exp(-fast * freqTime) - Math.exp(-slow * freqTime), criticalFreqTime) / criticalFreqTime);
}
function calcAngularFreq(undampedFreq, dampingRatio) {
return undampedFreq * Math.sqrt(1 - dampingRatio * dampingRatio);
}
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;
}
/**
* This is ported from the Framer implementation of duration-based spring resolution.
*/
const safeMin = 0.001;
/**
* Assumes zero initial velocity and the default mass: time-defined springs
* ignore inherited velocity and only resolve without valid physics. Returns
* NaN physics when the root search doesn't converge.
*/
function findSpring({ duration = springDefaults.duration, bounce = springDefaults.bounce, }) {
let envelope;
let derivative;
warning(duration <= secondsToMilliseconds(springDefaults.maxDuration), "Spring duration must be 10 seconds or less", "spring-duration-limit");
const dampingRatio = bounceToDampingRatio(bounce);
duration = clamp(springDefaults.minDuration, springDefaults.maxDuration, millisecondsToSeconds(duration));
if (dampingRatio < 1) {
/**
* Underdamped spring
*/
envelope = (undampedFreq) => {
const exponentialDecay = undampedFreq * dampingRatio;
const delta = exponentialDecay * duration;
const b = calcAngularFreq(undampedFreq, dampingRatio);
const c = Math.exp(-delta);
return safeMin - (exponentialDecay / b) * c;
};
derivative = (undampedFreq) => {
const exponentialDecay = undampedFreq * dampingRatio;
const delta = exponentialDecay * duration;
const e = dampingRatio *
dampingRatio *
undampedFreq *
undampedFreq *
duration;
const f = Math.exp(-delta);
const g = calcAngularFreq(undampedFreq * undampedFreq, dampingRatio);
const factor = -envelope(undampedFreq) + safeMin > 0 ? -1 : 1;
return (factor * -e * f) / g;
};
}
else {
/**
* Critically-damped spring. Overdamped springs reuse this root,
* scaled below.
*/
envelope = (undampedFreq) => {
const a = Math.exp(-undampedFreq * duration);
const b = undampedFreq * duration + 1;
return -safeMin + a * b;
};
derivative = (undampedFreq) => {
const a = Math.exp(-undampedFreq * duration);
const b = -undampedFreq * (duration * duration);
return a * b;
};
}
const initialGuess = 5 / duration;
const criticalFreq = approximateRoot(envelope, derivative, initialGuess);
const undampedFreq = criticalFreq *
overdampedFreqScale(dampingRatio, criticalFreq * duration);
const stiffness = undampedFreq * undampedFreq;
return {
stiffness,
damping: dampingRatio * 2 * Math.sqrt(stiffness),
duration: secondsToMilliseconds(duration),
};
}
/**
* Spring physics must be finite. stiffness and mass are also divisors so must
* be positive, whereas a damping of 0 is a valid, perpetually oscillating
* spring. Relational rather than Number.isFinite so numeric strings still
* coerce.
*/
const isValidPhysics = (value, canBeZero) => (canBeZero ? value >= 0 : value > 0) && value < Infinity;
/**
* Returns value if it's usable spring physics, otherwise undefined so callers
* fall back to the default. An explicit `undefined`, e.g. from a forwarded
* optional prop, must fall back too. Invalid physics would otherwise resolve
* to NaN spring values that never report done.
*/
function resolvePhysics(value, canBeZero) {
if (isValidPhysics(value, canBeZero))
return value;
if (process.env.NODE_ENV !== "production") {
warning(value === undefined, "Spring stiffness and mass must be positive, damping 0 or greater", "spring-invalid-physics");
}
return undefined;
}
function getSpringOptions(options) {
/**
* Resolve physics before choosing between physics- and duration-based
* resolution, so an invalid stiffness doesn't also silently discard a
* valid duration/bounce.
*/
const validStiffness = resolvePhysics(options.stiffness);
const validDamping = resolvePhysics(options.damping, true);
const validMass = resolvePhysics(options.mass);
const springOptions = {
...options,
stiffness: validStiffness ?? springDefaults.stiffness,
damping: validDamping ?? springDefaults.damping,
mass: validMass ?? springDefaults.mass,
isResolvedFromDuration: false,
// stiffness/damping/mass overrides duration/bounce
isTimeDefined: (validStiffness ?? validDamping ?? validMass) === undefined &&
(options.duration !== undefined || options.bounce !== undefined),
};
if (springOptions.isTimeDefined) {
if (options.visualDuration) {
const dampingRatio = bounceToDampingRatio(options.bounce || 0);
const root = ((2 * Math.PI) / (options.visualDuration * 1.2)) *
overdampedFreqScale(dampingRatio, (2 * Math.PI) / 1.2);
springOptions.stiffness = root * root;
springOptions.damping =
2 * dampingRatio * Math.sqrt(springOptions.stiffness);
}
else {
Object.assign(springOptions, findSpring(springOptions));
springOptions.isResolvedFromDuration = true;
}
/**
* Time options can degenerate: a NaN bounce gives a NaN damping, an
* infinite visualDuration a 0 stiffness, and findSpring NaN for both
* when it doesn't converge. Replace the two together, so the
* relationship duration resolution establishes between them is never
* left half-overwritten.
*/
if (!isValidPhysics(springOptions.stiffness) ||
!isValidPhysics(springOptions.damping, true)) {
springOptions.stiffness = springDefaults.stiffness;
springOptions.damping = springDefaults.damping;
}
}
return springOptions;
}
function spring(optionsOrVisualDuration = springDefaults.visualDuration, bounce = springDefaults.bounce) {
const options = typeof optionsOrVisualDuration !== "object"
? {
visualDuration: optionsOrVisualDuration,
keyframes: [0, 1],
bounce,
}
: optionsOrVisualDuration;
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, isResolvedFromDuration, isTimeDefined, } = getSpringOptions({ ...options });
/**
* Time-defined springs ignore inherited velocity. Velocity from
* interrupted animations causes massive oscillation on small-range
* animations.
*/
const inheritVelocity = (velocity) => isTimeDefined ? 0 : -millisecondsToSeconds(velocity);
const dampingRatio = damping / (2 * Math.sqrt(stiffness * mass));
const undampedAngularFreq = millisecondsToSeconds(Math.sqrt(stiffness / mass));
const decay = dampingRatio * undampedAngularFreq;
/**
* Everything that changes when the spring is retargeted: written by
* retarget() and update(), read by the resolvers. Grouped on one object,
* like the coefficients (c) below. Writing doubles to object fields
* measured marginally faster than to captured let variables in optimised
* code; neither allocates, so this is a grouping choice, not a GC one.
*/
const s = {
target,
delta: target - origin,
velocity: inheritVelocity(options.velocity || 0) || 0,
restSpeed: 0,
restDelta: 0,
};
/**
* If we're working on a granular scale, use smaller defaults for determining
* when the spring is finished.
*
* These defaults have been selected empirically based on what strikes a good
* ratio between feeling good and finishing as soon as changes are imperceptible.
*/
const setRestThresholds = () => {
const isGranularScale = Math.abs(s.delta) < 5;
s.restSpeed =
options.restSpeed ||
(isGranularScale
? springDefaults.restSpeed.granular
: springDefaults.restSpeed.default);
s.restDelta =
options.restDelta ||
(isGranularScale
? springDefaults.restDelta.granular
: springDefaults.restDelta.default);
};
setRestThresholds();
let resolveSpring;
let resolveVelocity;
/**
* Derives the coefficients that depend on origin, target and initial
* velocity. Called once now and again whenever the spring is retargeted.
*/
let update;
if (dampingRatio < 1) {
const angularFreq = calcAngularFreq(undampedAngularFreq, dampingRatio);
/**
* A is the position coefficient, sinC/cosC the coefficients of the
* analytical derivative (px/ms). The exp/sin/cos terms depend only
* on t, so they're memoized by t independently of the target: a
* spring retargeted every frame samples the same t each frame and
* skips the transcendentals entirely.
*/
const c = { A: 0, sinC: 0, cosC: 0, t: -1, env: 0, sin: 0, cos: 0 };
update = () => {
c.A = (s.velocity + decay * s.delta) / angularFreq;
c.sinC = decay * c.A + s.delta * angularFreq;
c.cosC = decay * s.delta - c.A * angularFreq;
};
const sample = (t) => {
if (t !== c.t) {
c.t = t;
c.env = Math.exp(-decay * t);
c.sin = Math.sin(angularFreq * t);
c.cos = Math.cos(angularFreq * t);
}
};
// Underdamped spring
resolveSpring = (t) => {
sample(t);
return s.target - c.env * (c.A * c.sin + s.delta * c.cos);
};
resolveVelocity = (t) => {
sample(t);
return c.env * (c.sinC * c.sin + c.cosC * c.cos);
};
}
else if (dampingRatio === 1) {
// Critically damped spring
resolveSpring = (t) => s.target -
Math.exp(-undampedAngularFreq * t) *
(s.delta + (s.velocity + undampedAngularFreq * s.delta) * t);
// Analytical derivative of critically damped spring (px/ms)
const c = { C: 0 };
update = () => {
c.C = s.velocity + undampedAngularFreq * s.delta;
};
resolveVelocity = (t) => Math.exp(-undampedAngularFreq * t) *
(undampedAngularFreq * c.C * t - s.velocity);
}
else {
/**
* Overdamped spring: the sum of a slow and a fast decaying
* exponential, so no term can overflow however heavily it's damped.
*/
const dampedAngularFreq = undampedAngularFreq * Math.sqrt(dampingRatio * dampingRatio - 1);
const slow = decay - dampedAngularFreq;
const fast = decay + dampedAngularFreq;
/**
* Physics-defined springs keep a non-physical limit: once
* dampedAngularFreq * t passes 300, both terms decay at the damping
* rate. Heavily overdamped drag springs (dragElastic: 0,
* dragTransition) rely on this to settle almost at once rather than
* creep at stiffness / damping. Negative bounce needs the exact
* curve, so time-defined springs are exempt.
*/
const limit = isTimeDefined ? Infinity : 300 / dampedAngularFreq;
const decayAt = (rate, t) => Math.exp(t > limit ? -rate * limit - decay * (t - limit) : -rate * t);
const c = { S: 0, F: 0 };
update = () => {
const P = (s.velocity + decay * s.delta) / dampedAngularFreq;
c.S = (s.delta + P) / 2;
c.F = (s.delta - P) / 2;
};
resolveSpring = (t) => s.target - c.S * decayAt(slow, t) - c.F * decayAt(fast, t);
// Analytical derivative of overdamped spring (px/ms)
resolveVelocity = (t) => slow * c.S * decayAt(slow, t) + fast * c.F * decayAt(fast, t);
}
update();
const calculatedDuration = isResolvedFromDuration ? duration || null : null;
const generator = {
calculatedDuration,
/**
* Aim the spring at a new target from its current position and
* velocity, reusing the resolved physics and closures.
*/
retarget: (keyframes, newVelocity) => {
s.target = keyframes[keyframes.length - 1];
s.delta = s.target - keyframes[0];
s.velocity = inheritVelocity(newVelocity);
// Default thresholds depend on the scale of the new delta
if (!(options.restSpeed && options.restDelta))
setRestThresholds();
// Invalidate any duration lazily cached by JSAnimation
generator.calculatedDuration = calculatedDuration;
state.done = false;
update();
},
velocity: (t) => secondsToMilliseconds(resolveVelocity(t)),
next: (t) => {
const current = resolveSpring(t);
if (!isResolvedFromDuration) {
const currentVelocity = secondsToMilliseconds(resolveVelocity(t));
state.done =
Math.abs(currentVelocity) <= s.restSpeed &&
Math.abs(s.target - current) <= s.restDelta;
}
else {
state.done = t >= duration;
}
state.value = state.done ? s.target : current;
return state;
},
toString: () => {
const easingDuration = Math.min(calcGeneratorDuration(generator), maxGeneratorDuration);
const easing = generateLinearEasing((progress) => generator.next(easingDuration * progress).value, easingDuration, 30);
return easingDuration + "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 };
//# sourceMappingURL=spring.mjs.map
@@ -0,0 +1,19 @@
/**
* Implement a practical max duration for keyframe generation
* to prevent infinite loops
*/
const maxGeneratorDuration = 20000;
function calcGeneratorDuration(generator, timeStep = 50, maxDuration = maxGeneratorDuration, keyframes) {
let duration = 0;
let state = generator.next(duration);
keyframes?.push(state.value);
while (!state.done && duration < maxDuration) {
duration += timeStep;
state = generator.next(duration);
keyframes?.push(state.value);
}
return duration >= maxDuration ? Infinity : duration;
}
export { calcGeneratorDuration, maxGeneratorDuration };
//# sourceMappingURL=calc-duration.mjs.map
@@ -0,0 +1,33 @@
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) {
/**
* The easing is generated over 0 - scale, but velocity is in the
* value's own units, so scale it to match. Otherwise an interrupted
* spring hands off the wrong speed, or direction. Keyframes with
* units (colours, filters, mixed units) give NaN, which is falsy,
* so their velocity passes through unscaled.
*/
const { keyframes: k, velocity: v } = options;
const delta = k && k[k.length - 1] - k[0];
const generator = createGenerator({
...options,
velocity: v && delta ? (v * scale) / delta : v,
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 };
//# sourceMappingURL=create-generator-easing.mjs.map
@@ -0,0 +1,6 @@
function isGenerator(type) {
return typeof type === "function" && "applyToOptions" in type;
}
export { isGenerator };
//# sourceMappingURL=is-generator.mjs.map
@@ -0,0 +1,22 @@
import { velocityPerSecond } from 'motion-utils';
const velocitySampleDuration = 5; // ms
function getGeneratorVelocity(resolveValue, t, current) {
const prevT = Math.max(t - velocitySampleDuration, 0);
return velocityPerSecond(current - resolveValue(prevT), t - prevT);
}
/**
* A generator's velocity at time t in units/second: analytical where the
* generator provides it (springs), otherwise by finite difference. Before
* the animation has started this is its initial velocity.
*/
function calcGeneratorVelocity(generator, t, initialVelocity = 0) {
if (t <= 0)
return initialVelocity;
return generator.velocity
? generator.velocity(t)
: getGeneratorVelocity((s) => generator.next(s).value, t, generator.next(t).value);
}
export { calcGeneratorVelocity, getGeneratorVelocity };
//# sourceMappingURL=velocity.mjs.map
@@ -0,0 +1,106 @@
import { secondsToMilliseconds, MotionGlobalConfig } from 'motion-utils';
import { AsyncMotionValueAnimation } from '../AsyncMotionValueAnimation.mjs';
import { JSAnimation } from '../JSAnimation.mjs';
import { getValueTransition } from '../utils/get-value-transition.mjs';
import { makeAnimationInstant } from '../utils/make-animation-instant.mjs';
import { getDefaultTransition } from '../utils/default-transitions.mjs';
import { getFinalKeyframe } from '../keyframes/get-final.mjs';
import { isTransitionDefined } from '../utils/is-transition-defined.mjs';
import { frame } from '../../frameloop/frame.mjs';
const animateMotionValue = (name, value, target, transition = {}, element, isHandoff) => (onComplete) => {
const valueTransition = getValueTransition(transition, name) || {};
/**
* Most transition values are currently completely overwritten by value-specific
* transitions. In the future it'd be nicer to blend these transitions. But for now
* delay actually does inherit from the root transition if not value-specific.
*/
const delay = valueTransition.delay || transition.delay || 0;
/**
* Elapsed isn't a public transition option but can be passed through from
* optimized appear effects in milliseconds.
*/
let { elapsed = 0 } = transition;
elapsed = elapsed - secondsToMilliseconds(delay);
const options = {
keyframes: Array.isArray(target) ? target : [null, target],
ease: "easeOut",
velocity: value.getVelocity(),
...valueTransition,
delay: -elapsed,
onUpdate: (v) => {
value.set(v);
valueTransition.onUpdate && valueTransition.onUpdate(v);
},
onComplete: () => {
onComplete();
valueTransition.onComplete && valueTransition.onComplete();
},
name,
motionValue: value,
element: isHandoff ? undefined : element,
hasOnUpdate: !!valueTransition.onUpdate,
};
/**
* If there's no transition defined for this value, we can generate
* unique transition settings for this value.
*/
if (!isTransitionDefined(valueTransition)) {
Object.assign(options, getDefaultTransition(name, options));
}
/**
* Both WAAPI and our internal animation functions use durations
* as defined by milliseconds, while our external API defines them
* as seconds.
*/
options.duration && (options.duration = secondsToMilliseconds(options.duration));
options.repeatDelay && (options.repeatDelay = secondsToMilliseconds(options.repeatDelay));
/**
* Support deprecated way to set initial value. Prefer keyframe syntax.
*/
if (options.from !== undefined) {
options.keyframes[0] = options.from;
}
let shouldSkip = false;
if (options.type === false ||
(options.duration === 0 && !options.repeatDelay)) {
makeAnimationInstant(options);
if (options.delay === 0) {
shouldSkip = true;
}
}
if (MotionGlobalConfig.instantAnimations ||
MotionGlobalConfig.skipAnimations ||
element?.shouldSkipAnimations ||
valueTransition.skipAnimations) {
shouldSkip = true;
makeAnimationInstant(options);
options.delay = 0;
}
/**
* If the transition type or easing has been explicitly set by the user
* then we don't want to allow flattening the animation.
*/
options.allowFlatten = !valueTransition.type && !valueTransition.ease;
/**
* If we can or must skip creating the animation, and apply only
* the final keyframe, do so. We also check once keyframes are resolved but
* this early check prevents the need to create an animation at all.
*/
if (shouldSkip && !isHandoff && value.get() !== undefined) {
const finalKeyframe = getFinalKeyframe(options.keyframes, valueTransition);
if (finalKeyframe !== undefined) {
frame.update(() => {
options.onUpdate(finalKeyframe);
options.onComplete();
});
return;
}
}
return valueTransition.isSync
? new JSAnimation(options)
: new AsyncMotionValueAnimation(options);
};
export { animateMotionValue };
//# sourceMappingURL=motion-value.mjs.map
@@ -0,0 +1,109 @@
import { getValueTransition } from '../utils/get-value-transition.mjs';
import { resolveTransition } from '../utils/resolve-transition.mjs';
import { positionalKeys } from '../../render/utils/keys-position.mjs';
import { setTarget } from '../../render/utils/setters.mjs';
import { addValueToWillChange } from '../../value/will-change/add-will-change.mjs';
import { getOptimisedAppearId } from '../optimized-appear/get-appear-id.mjs';
import { animateMotionValue } from './motion-value.mjs';
import { frame } from '../../frameloop/frame.mjs';
const isBrowser = typeof window !== "undefined";
/**
* Decide whether we should block this animation. Previously, we achieved this
* just by checking whether the key was listed in protectedKeys, but this
* posed problems if an animation was triggered by afterChildren and protectedKeys
* had been set to true in the meantime.
*/
function shouldBlockAnimation({ protectedKeys, needsAnimating }, key) {
const shouldBlock = key in protectedKeys && !needsAnimating[key];
needsAnimating[key] = false;
return shouldBlock;
}
function animateTarget(visualElement, targetAndTransition, { delay = 0, transitionOverride, type } = {}) {
let { transition, transitionEnd, ...target } = targetAndTransition;
const defaultTransition = visualElement.getDefaultTransition();
transition = transition
? resolveTransition(transition, defaultTransition)
: defaultTransition;
const reduceMotion = transition?.reduceMotion;
const skipAnimations = transition?.skipAnimations;
if (transitionOverride)
transition = transitionOverride;
const animations = [];
const animationTypeState = type && visualElement.animationState?.getState()[type];
const path = transition?.path;
if (path) {
// path mutates `target` to claim x/y; loop below skips them.
path.animateValues
? path.animateValues((key) => visualElement.getValue(key, visualElement.latestValues[key] ?? 0), target, transition, delay, animations)
: path.animateVisualElement?.(visualElement, target, transition, delay, animations);
}
for (const key in target) {
const value = visualElement.getValue(key, visualElement.latestValues[key] ?? null);
const valueTarget = target[key];
if (valueTarget === undefined ||
(animationTypeState &&
shouldBlockAnimation(animationTypeState, key))) {
continue;
}
const valueTransition = {
delay,
...getValueTransition(transition || {}, key),
};
if (skipAnimations)
valueTransition.skipAnimations = true;
/**
* If the value is already at the defined target, skip the animation.
* We still re-assert the value via frame.update to take precedence
* over any stale transitionEnd callbacks from previous animations.
*/
const currentValue = value.get();
if (currentValue !== undefined &&
!value.isAnimating() &&
!Array.isArray(valueTarget) &&
valueTarget === currentValue &&
!valueTransition.velocity) {
frame.update(() => value.set(valueTarget));
continue;
}
/**
* If this is the first time a value is being animated, check
* to see if we're handling off from an existing animation.
*/
let isHandoff = false;
if (isBrowser && window.MotionHandoffAnimation) {
const appearId = getOptimisedAppearId(visualElement);
if (appearId) {
const startTime = window.MotionHandoffAnimation(appearId, key, frame);
if (startTime !== null) {
valueTransition.startTime = startTime;
isHandoff = true;
}
}
}
addValueToWillChange(visualElement, key);
const shouldReduceMotion = reduceMotion ?? visualElement.shouldReduceMotion;
value.start(animateMotionValue(key, value, valueTarget, shouldReduceMotion && positionalKeys.has(key)
? { type: false }
: valueTransition, visualElement, isHandoff));
const animation = value.animation;
if (animation) {
animations.push(animation);
}
}
if (transitionEnd) {
const applyTransitionEnd = () => frame.update(() => {
transitionEnd && setTarget(visualElement, transitionEnd);
});
if (animations.length) {
Promise.all(animations).then(applyTransitionEnd);
}
else {
applyTransitionEnd();
}
}
return animations;
}
export { animateTarget };
//# sourceMappingURL=visual-element-target.mjs.map
@@ -0,0 +1,40 @@
import { resolveVariant, getTypeCustom } from '../../render/utils/resolve-dynamic-variants.mjs';
import { calcChildStagger } from '../utils/calc-child-stagger.mjs';
import { animateTarget } from './visual-element-target.mjs';
function animateVariant(visualElement, variant, options = {}) {
const resolved = resolveVariant(visualElement, variant, getTypeCustom(visualElement, options.type));
const transition = options.transitionOverride ||
(resolved?.transition ?? (visualElement.getDefaultTransition() || {}));
const getAnimation = () => Promise.all(resolved ? animateTarget(visualElement, resolved, options) : []);
const getChildAnimations = (delay = 0) => {
const { variantChildren } = visualElement;
const { delayChildren = 0, staggerChildren, staggerDirection, } = transition;
const animations = [];
variantChildren?.forEach((child) => {
child.notify("AnimationStart", variant);
animations.push(animateVariant(child, variant, {
...options,
delay: delay +
(typeof delayChildren === "function"
? 0
: delayChildren) +
calcChildStagger(variantChildren, child, delayChildren, staggerChildren, staggerDirection),
}).then(() => child.notify("AnimationComplete", variant)));
});
return Promise.all(animations);
};
/**
* when: "beforeChildren" | "afterChildren" runs this element's animation
* and its children's in sequence.
*/
const { when } = transition;
return when
? when === "beforeChildren"
? getAnimation().then(() => getChildAnimations())
: getChildAnimations().then(getAnimation)
: Promise.all([getAnimation(), getChildAnimations(options.delay)]);
}
export { animateVariant };
//# sourceMappingURL=visual-element-variant.mjs.map
@@ -0,0 +1,18 @@
import { resolveVariant } from '../../render/utils/resolve-dynamic-variants.mjs';
import { animateTarget } from './visual-element-target.mjs';
import { animateVariant } from './visual-element-variant.mjs';
function animateVisualElement(visualElement, definition, options = {}) {
visualElement.notify("AnimationStart", definition);
const animation = Array.isArray(definition)
? Promise.all(definition.map((variant) => animateVariant(visualElement, variant, options)))
: typeof definition === "string"
? animateVariant(visualElement, definition, options)
: Promise.all(animateTarget(visualElement, resolveVariant(visualElement, definition, options.custom), options));
return animation.then(() => {
visualElement.notify("AnimationComplete", definition);
});
}
export { animateVisualElement };
//# sourceMappingURL=visual-element.mjs.map
@@ -0,0 +1,172 @@
import { positionalKeys } from '../../render/utils/keys-position.mjs';
import { findDimensionValueType } from '../../value/types/dimensions.mjs';
import { getVariableValue } from '../utils/css-variables-conversion.mjs';
import { containsCSSVariable, 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 { positionalValues, isNumOrPxType } from './utils/unit-conversion.mjs';
class DOMKeyframesResolver extends KeyframeResolver {
constructor(unresolvedKeyframes, onComplete, name, motionValue, element) {
super(unresolvedKeyframes, onComplete, name, motionValue, element, true);
}
/**
* Keyframes that don't need reading from the DOM, or measuring,
* resolve straight away rather than in the frame loop's batched read.
*/
scheduleResolve() {
if (this.canReadNow()) {
this.readKeyframes();
if (!this.needsMeasurement)
return this.complete();
}
super.scheduleResolve();
}
canReadNow() {
const { unresolvedKeyframes, element, motionValue } = this;
const origin = unresolvedKeyframes[0] ?? motionValue?.get();
if (!element?.current || origin === undefined)
return false;
for (let i = 0; i < unresolvedKeyframes.length; i++) {
if (containsCSSVariable(i ? unresolvedKeyframes[i] : origin)) {
return false;
}
}
return 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;
// Two numbers share a unit, so skip the value type search.
if (typeof origin === "number" && typeof target === "number")
return;
const originType = findDimensionValueType(origin);
const targetType = findDimensionValueType(target);
/**
* If one keyframe contains embedded CSS variables (e.g. in calc()) and the other
* doesn't, we need to measure to convert to pixels. This handles GitHub issue #3410.
*/
const originHasVar = containsCSSVariable(origin);
const targetHasVar = containsCSSVariable(target);
if (originHasVar !== targetHasVar && positionalValues[name]) {
this.needsMeasurement = true;
return;
}
/**
* 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);
}
}
measure() {
const { element, name } = this;
return positionalValues[name](window.getComputedStyle(element.current), () => element.measureViewportBox());
}
measureInitialState() {
const { element, unresolvedKeyframes, name } = this;
if (!element || !element.current)
return;
if (name === "height") {
this.suspendedScrollY = window.pageYOffset;
}
this.measuredOrigin = this.measure();
unresolvedKeyframes[0] = this.measuredOrigin;
// Set final key frame to measure after next render
const measureKeyframe = unresolvedKeyframes[unresolvedKeyframes.length - 1];
if (measureKeyframe !== undefined) {
this.motionValue?.jump(measureKeyframe, false);
}
}
measureEndState() {
const { element, unresolvedKeyframes } = this;
if (!element || !element.current)
return;
this.motionValue?.jump(this.measuredOrigin, false);
const finalKeyframeIndex = unresolvedKeyframes.length - 1;
const finalKeyframe = unresolvedKeyframes[finalKeyframeIndex];
unresolvedKeyframes[finalKeyframeIndex] = this.measure();
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 };
//# sourceMappingURL=DOMKeyframesResolver.mjs.map
@@ -0,0 +1,179 @@
import { isNumericalString, isZeroValueString } from 'motion-utils';
import { complex } from '../../value/types/complex/index.mjs';
import { getAnimatableNone } from '../../value/types/utils/animatable-none.mjs';
import { fillWildcards } from './utils/fill-wildcards.mjs';
import { boxDependentValues, 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 = [];
const elementsToMeasure = new Set();
const elementsToUntransform = new Set();
toResolve.forEach((resolver) => {
if (!resolver.needsMeasurement)
return;
resolversToMeasure.push(resolver);
elementsToMeasure.add(resolver.element);
if (boxDependentValues.has(resolver.name)) {
elementsToUntransform.add(resolver.element);
}
});
const transformsToRestore = new Map();
/**
* Write pass
* Values measured from the bounding box need bounding box-changing
* transforms removed first. Values read from computed style don't.
*/
elementsToUntransform.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;
}
/**
* Normalise a value read from the subject into an animation origin: a
* number read as a string ("0", "200") becomes a number, and a value
* that isn't animatable (e.g. "none") but whose target is becomes an
* animatable zero in the shape of the target.
*/
function readOrigin(value, name, target) {
if (typeof value === "string") {
if (isNumericalString(value) || isZeroValueString(value)) {
return parseFloat(value);
}
else if (!complex.test(value) && complex.test(target)) {
return getAnimatableNone(name, target);
}
}
return value ?? undefined;
}
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 = readOrigin(element.readValue(name, finalKeyframe), name, finalKeyframe);
if (valueAsRead !== undefined) {
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 };
//# sourceMappingURL=KeyframesResolver.mjs.map
@@ -0,0 +1,12 @@
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 };
//# sourceMappingURL=get-final.mjs.map
@@ -0,0 +1,10 @@
import { fillOffset } from './fill.mjs';
function defaultOffset(arr) {
const offset = [0];
fillOffset(offset, arr.length - 1);
return offset;
}
export { defaultOffset };
//# sourceMappingURL=default.mjs.map
@@ -0,0 +1,13 @@
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 };
//# sourceMappingURL=fill.mjs.map
@@ -0,0 +1,6 @@
function convertOffsetToTimes(offset, duration) {
return offset.map((o) => o * duration);
}
export { convertOffsetToTimes };
//# sourceMappingURL=time.mjs.map
@@ -0,0 +1,12 @@
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 };
//# sourceMappingURL=apply-px-defaults.mjs.map
@@ -0,0 +1,8 @@
function fillWildcards(keyframes) {
for (let i = 1; i < keyframes.length; i++) {
keyframes[i] ?? (keyframes[i] = keyframes[i - 1]);
}
}
export { fillWildcards };
//# sourceMappingURL=fill-wildcards.mjs.map
@@ -0,0 +1,16 @@
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 };
//# sourceMappingURL=is-none.mjs.map
@@ -0,0 +1,37 @@
import { hasComplexValues } 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) &&
hasComplexValues(keyframe)) {
animatableTemplate = unresolvedKeyframes[i];
}
i++;
}
if (animatableTemplate && name) {
for (const noneIndex of noneKeyframeIndexes) {
/**
* A zero-valued template like "0%" is already its own
* animatable none, so there's nothing to derive.
*/
if (unresolvedKeyframes[noneIndex] === animatableTemplate)
continue;
unresolvedKeyframes[noneIndex] = getAnimatableNone(name, animatableTemplate);
}
}
}
export { makeNoneKeyframesAnimatable };
//# sourceMappingURL=make-none-animatable.mjs.map
@@ -0,0 +1,73 @@
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));
/**
* Reset any bounding box-changing transforms so the element can be
* measured. Returns the values to restore. Values already at their
* default don't change the box, so they're left alone: an element with
* only `rotate: 0` doesn't need to be re-rendered before measuring.
*/
function removeNonTranslationalTransform(visualElement) {
const removedTransforms = [];
nonTranslationalTransformKeys.forEach((key) => {
const value = visualElement.getValue(key);
if (value !== undefined) {
const current = value.get();
const reset = key.startsWith("scale") ? 1 : 0;
if (current === reset)
return;
removedTransforms.push([key, current]);
value.set(reset);
}
});
return removedTransforms;
}
/**
* Values that can only be measured from the bounding box. Elements with
* these values need bounding box-changing transforms removed first.
*/
const boxDependentValues = new Set(["bottom", "right"]);
/**
* Computed width/height is already in pixels and unaffected by
* transforms. It's "auto" for elements without a layout box (e.g. inline),
* in which case we fall back to measuring the bounding box.
*/
function measureDimension(computed, measureBox, axis, paddingStart, paddingEnd, boxSizing) {
const px = parseFloat(computed);
if (!isNaN(px))
return px;
const { min, max } = measureBox()[axis];
const size = max - min;
return boxSizing === "border-box"
? size
: size - parseFloat(paddingStart) - parseFloat(paddingEnd);
}
const positionalValues = {
// Dimensions
width: ({ width, paddingLeft = "0", paddingRight = "0", boxSizing }, measureBox) => measureDimension(width, measureBox, "x", paddingLeft, paddingRight, boxSizing),
height: ({ height, paddingTop = "0", paddingBottom = "0", boxSizing }, measureBox) => measureDimension(height, measureBox, "y", paddingTop, paddingBottom, boxSizing),
top: ({ top }) => parseFloat(top),
left: ({ left }) => parseFloat(left),
bottom: ({ top }, measureBox) => {
const { y } = measureBox();
return parseFloat(top) + (y.max - y.min);
},
right: ({ left }, measureBox) => {
const { x } = measureBox();
return parseFloat(left) + (x.max - x.min);
},
// Transform
x: ({ transform }) => parseValueFromTransform(transform, "x"),
y: ({ transform }) => parseValueFromTransform(transform, "y"),
};
// Alias translate longform names
positionalValues.translateX = positionalValues.x;
positionalValues.translateY = positionalValues.y;
export { boxDependentValues, isNumOrPxType, positionalValues, removeNonTranslationalTransform };
//# sourceMappingURL=unit-conversion.mjs.map
@@ -0,0 +1,7 @@
import { camelToDash } from '../../render/dom/utils/camel-to-dash.mjs';
const optimizedAppearDataId = "framerAppearId";
const optimizedAppearDataAttribute = "data-" + camelToDash(optimizedAppearDataId);
export { optimizedAppearDataAttribute, optimizedAppearDataId };
//# sourceMappingURL=data-id.mjs.map
@@ -0,0 +1,8 @@
import { optimizedAppearDataAttribute } from './data-id.mjs';
function getOptimisedAppearId(visualElement) {
return visualElement.props[optimizedAppearDataAttribute];
}
export { getOptimisedAppearId };
//# sourceMappingURL=get-appear-id.mjs.map
@@ -0,0 +1,34 @@
class WithPromise {
constructor() {
this.isResolved = false;
}
get finished() {
if (!this._finished) {
this._finished = this.isResolved
? Promise.resolve()
: new Promise((resolve) => {
this._resolve = resolve;
});
}
return this._finished;
}
updateFinished() {
this._finished = this._resolve = undefined;
this.isResolved = false;
}
notifyFinished() {
this.isResolved = true;
this._resolve?.();
}
/**
* Allows the animation to be awaited.
*
* @deprecated Use `finished` instead.
*/
then(onResolve, onReject) {
return this.finished.then(onResolve, onReject);
}
}
export { WithPromise };
//# sourceMappingURL=WithPromise.mjs.map
@@ -0,0 +1,13 @@
const animationMaps = new WeakMap();
const animationMapKey = (name, pseudoElement = "") => `${name}:${pseudoElement}`;
function getAnimationMap(element) {
let map = animationMaps.get(element);
if (!map) {
map = new Map();
animationMaps.set(element, map);
}
return map;
}
export { animationMapKey, getAnimationMap };
//# sourceMappingURL=active-animations.mjs.map
@@ -0,0 +1,189 @@
import { wrap } from 'motion-utils';
import { motionValue } from '../../value/index.mjs';
import { animateMotionValue } from '../interfaces/motion-value.mjs';
import { getValueTransition } from './get-value-transition.mjs';
const MIN_LAYOUT_DISTANCE = 20;
function bezierPoint(t, origin, control, target) {
const inv = 1 - t;
return inv * inv * origin + 2 * inv * t * control + t * t * target;
}
function bezierTangentAngle(t, originX, controlX, targetX, originY, controlY, targetY) {
const dx = 2 * (1 - t) * (controlX - originX) + 2 * t * (targetX - controlX);
const dy = 2 * (1 - t) * (controlY - originY) + 2 * t * (targetY - controlY);
return Math.atan2(dy, dx) * (180 / Math.PI);
}
function computeArcControlPoint(fromX, fromY, toX, toY, strength, peak) {
const deltaX = toX - fromX;
const deltaY = toY - fromY;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance > 0) {
const normalPerpX = -deltaY / distance;
const normalPerpY = deltaX / distance;
const desiredHeight = strength * distance;
return {
x: fromX + deltaX * peak + normalPerpX * desiredHeight,
y: fromY + deltaY * peak + normalPerpY * desiredHeight,
};
}
return { x: fromX, y: fromY };
}
/**
* The pure sampling factory: `(from, to) => (t) => point`. Internal —
* used by {@link arc} and the unit tests. Not part of the public surface.
*/
function createArcPath({ strength = 0.5, peak = 0.5, direction, rotate = false, } = {}) {
const rotationScale = rotate === true ? 1 : typeof rotate === "number" ? rotate : 0;
// Auto-direction only: persists across calls to flip the bulge back
// onto the same screen side when the dominant axis changes between
// calls. Reuse the factory (module scope / useMemo) to keep this alive.
let prevBulgeSign;
const createInterpolator = (from, to) => {
const dx = to.x - from.x;
const dy = to.y - from.y;
let signed;
if (direction === "cw") {
signed = -strength;
}
else if (direction === "ccw") {
signed = strength;
}
else {
const dom = Math.abs(dx) >= Math.abs(dy) ? dx : dy;
signed = dom < 0 ? -strength : strength;
}
let control = computeArcControlPoint(from.x, from.y, to.x, to.y, signed, peak);
if (direction === undefined) {
const isVertical = Math.abs(dx) < Math.abs(dy);
const midX = from.x + dx * peak;
const midY = from.y + dy * peak;
const bulgeSign = isVertical
? Math.sign(control.x - midX)
: Math.sign(control.y - midY);
if (prevBulgeSign !== undefined &&
bulgeSign !== 0 &&
bulgeSign !== prevBulgeSign) {
signed = -signed;
control = computeArcControlPoint(from.x, from.y, to.x, to.y, signed, peak);
}
else if (bulgeSign !== 0) {
prevBulgeSign = bulgeSign;
}
}
const tangent0 = rotationScale
? bezierTangentAngle(0, from.x, control.x, to.x, from.y, control.y, to.y)
: 0;
const tangent1 = rotationScale
? bezierTangentAngle(1, from.x, control.x, to.x, from.y, control.y, to.y)
: 0;
const tangentDelta = rotationScale
? wrap(-180, 180, tangent1 - tangent0)
: 0;
return (t) => {
const out = {
x: bezierPoint(t, from.x, control.x, to.x),
y: bezierPoint(t, from.y, control.y, to.y),
};
if (rotationScale) {
const raw = bezierTangentAngle(t, from.x, control.x, to.x, from.y, control.y, to.y);
const baseline = tangent0 + tangentDelta * t;
out.rotate = wrap(-180, 180, raw - baseline) * rotationScale;
}
return out;
};
};
return createInterpolator;
}
/**
* Creates a curved path for `transition.path`:
*
* ```ts
* <motion.div animate={{ x: 200, y: 100 }} transition={{ path: arc() }} />
* ```
*
* Reuse the returned value (module scope / useMemo / useRef) so its
* continuity closure survives re-renders — a fresh `arc()` has no memory.
*/
function arc(options = {}) {
const sample = createArcPath(options);
const path = {
interpolateProjection(delta) {
// `from` is the current translate offset (carries any in-flight
// displacement when interrupted); `to` is the new layout origin.
// The distance floor avoids visible wobble on tiny shifts.
const tx = delta.x.translate;
const ty = delta.y.translate;
if (Math.sqrt(tx * tx + ty * ty) < MIN_LAYOUT_DISTANCE) {
return undefined;
}
return sample({ x: tx, y: ty }, { x: 0, y: 0 });
},
animateValues(getValue, target, transition, delay, animations) {
if (!("x" in target || "y" in target))
return;
const xValue = getValue("x");
const yValue = getValue("y");
const xRaw = target.x;
const yRaw = target.y;
const xFrom = (Array.isArray(xRaw) && xRaw[0] != null
? xRaw[0]
: xValue?.get()) ?? 0;
const yFrom = (Array.isArray(yRaw) && yRaw[0] != null
? yRaw[0]
: yValue?.get()) ?? 0;
const xTo = (Array.isArray(xRaw)
? xRaw[xRaw.length - 1]
: xRaw ?? xFrom);
const yTo = (Array.isArray(yRaw)
? yRaw[yRaw.length - 1]
: yRaw ?? yFrom);
// Interruption needs no flag: x/y already hold the displaced
// mid-arc position, so xFrom/yFrom carry the continuity geometry.
const interpolate = sample({ x: xFrom, y: yFrom }, { x: xTo, y: yTo });
// Drive a dedicated `pathRotation` value (composed onto `rotate`
// at the build sites) rather than `rotate` itself, so a
// concurrent rotate animation composes and nothing accumulates
// on interrupt.
const pathRotationValue = interpolate(0).rotate !== undefined
? getValue("pathRotation")
: undefined;
const pathTransition = {
delay,
...getValueTransition(transition || {}, "x"),
};
delete pathTransition.path;
const progress = motionValue(0);
progress.start(animateMotionValue("", progress, [0, 1000], {
...pathTransition,
isSync: true,
velocity: 0,
onUpdate: (latest) => {
const point = interpolate(latest / 1000);
xValue?.set(point.x);
yValue?.set(point.y);
if (pathRotationValue && point.rotate !== undefined) {
pathRotationValue.set(point.rotate);
}
},
onComplete: () => {
xValue?.set(xTo);
yValue?.set(yTo);
pathRotationValue?.set(0);
},
// Interrupt/cancel must clear our additive contribution
// so it can't linger on top of the user's `rotate`.
onStop: () => pathRotationValue?.set(0),
onCancel: () => pathRotationValue?.set(0),
}));
if (progress.animation) {
animations.push(progress.animation);
}
delete target.x;
delete target.y;
},
};
return path;
}
export { arc, createArcPath };
//# sourceMappingURL=arc.mjs.map
@@ -0,0 +1,16 @@
function calcChildStagger(children, child, delayChildren, staggerChildren = 0, staggerDirection = 1) {
const index = Array.from(children)
.sort((a, b) => a.sortNodePosition(b))
.indexOf(child);
const numChildren = children.size;
const maxStaggerDuration = (numChildren - 1) * staggerChildren;
const delayIsFunction = typeof delayChildren === "function";
return delayIsFunction
? delayChildren(index, numChildren)
: staggerDirection === 1
? index * staggerChildren
: maxStaggerDuration - index * staggerChildren;
}
export { calcChildStagger };
//# sourceMappingURL=calc-child-stagger.mjs.map
@@ -0,0 +1,46 @@
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);
// Always skip if any of these are true
if (!isOriginAnimatable || !isTargetAnimatable) {
// Only build the message when we're going to warn
isOriginAnimatable !== isTargetAnimatable &&
warning(false, `You are trying to animate ${name} from "${originKeyframe}" to "${targetKeyframe}". "${isOriginAnimatable ? targetKeyframe : originKeyframe}" is not an animatable value.`, "value-not-animatable");
return false;
}
return (hasKeyframesChanged(keyframes) ||
((type === "spring" || isGenerator(type)) && velocity));
}
export { canAnimate };
//# sourceMappingURL=can-animate.mjs.map
@@ -0,0 +1,42 @@
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 };
//# sourceMappingURL=css-variables-conversion.mjs.map
@@ -0,0 +1,41 @@
import { transformProps } from '../../render/utils/keys-transform.mjs';
const underDampedSpring = {
type: "spring",
stiffness: 500,
damping: 25,
restSpeed: 10,
};
const criticallyDampedSpring = (target) => ({
type: "spring",
stiffness: 550,
damping: target === 0 ? 2 * Math.sqrt(550) : 30,
restSpeed: 10,
});
const keyframesTransition = {
type: "keyframes",
duration: 0.8,
};
/**
* Default easing curve is a slightly shallower version of
* the default browser easing curve.
*/
const ease = {
type: "keyframes",
ease: [0.25, 0.1, 0.35, 1],
duration: 0.3,
};
const getDefaultTransition = (valueKey, { keyframes }) => {
if (keyframes.length > 2) {
return keyframesTransition;
}
else if (transformProps.has(valueKey)) {
return valueKey.startsWith("scale")
? criticallyDampedSpring(keyframes[1])
: underDampedSpring;
}
return ease;
};
export { getDefaultTransition };
//# sourceMappingURL=default-transitions.mjs.map
@@ -0,0 +1,14 @@
import { resolveTransition } from './resolve-transition.mjs';
function getValueTransition(transition, key) {
const valueTransition = transition?.[key] ??
transition?.["default"] ??
transition;
if (valueTransition !== transition) {
return resolveTransition(valueTransition, transition);
}
return valueTransition;
}
export { getValueTransition };
//# sourceMappingURL=get-value-transition.mjs.map
@@ -0,0 +1,31 @@
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 };
//# sourceMappingURL=is-animatable.mjs.map
@@ -0,0 +1,27 @@
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;
/**
* Check if a value contains a CSS variable anywhere (e.g. inside calc()).
* Unlike isCSSVariableToken which checks if the value IS a var() token,
* this checks if the value CONTAINS var() somewhere in the string.
*/
function containsCSSVariable(value) {
if (typeof value !== "string")
return false;
// Strip comments to avoid false positives
return value.split("/*")[0].includes("var(--");
}
export { containsCSSVariable, isCSSVariableName, isCSSVariableToken };
//# sourceMappingURL=is-css-variable.mjs.map
@@ -0,0 +1,27 @@
const orchestrationKeys = new Set([
"when",
"delay",
"delayChildren",
"staggerChildren",
"staggerDirection",
"repeat",
"repeatType",
"repeatDelay",
"from",
"elapsed",
]);
/**
* Decide whether a transition is defined on a given Transition.
* This filters out orchestration options and returns true
* if any options are left.
*/
function isTransitionDefined(transition) {
for (const key in transition) {
if (!orchestrationKeys.has(key))
return true;
}
return false;
}
export { isTransitionDefined };
//# sourceMappingURL=is-transition-defined.mjs.map
@@ -0,0 +1,7 @@
function makeAnimationInstant(options) {
options.duration = 0;
options.type = "keyframes";
}
export { makeAnimationInstant };
//# sourceMappingURL=make-animation-instant.mjs.map
@@ -0,0 +1,56 @@
import { frameData } from '../../frameloop/frame-data.mjs';
import { time } from '../../frameloop/sync-time.mjs';
function baseRecord(kind, animation) {
return {
kind,
animation,
timestamp: time.now(),
frameTimestamp: frameData.timestamp,
frameIsProcessing: frameData.isProcessing,
};
}
/**
* Notify an optional inspector without adding a public Motion API.
*
* The record is a raw firehose: the live animation handle, the concrete
* animation's internal options object untouched (timing fields in
* internal milliseconds, name/keyframes/element/motionValue still
* attached), a timestamp and the raw frameloop state at notify time.
* All interpretation belongs to the consumer behind
* globalThis.__MOTION_INSPECT__.
*/
function notifyAnimationStart(animation, options, transition) {
const notify = globalThis.__MOTION_INSPECT__;
if (!notify)
return;
try {
notify({
...baseRecord("animation-start", animation),
options: transition ? { ...options, ...transition } : options,
});
}
catch { }
}
/**
* Layout animations drive a projection node's progress rather than a
* property, so the record forwards the live node itself: it carries the
* DOM instance, the layout options (layoutId) and the progress handle.
* Fired inside the frame.update that starts the animation, so nodes
* animating in the same layout commit share frameTimestamp.
*/
function notifyLayoutAnimationStart(animation, node) {
const notify = globalThis.__MOTION_INSPECT__;
if (!notify)
return;
try {
notify({
...baseRecord("layout-animation-start", animation),
node,
});
}
catch { }
}
export { notifyAnimationStart, notifyLayoutAnimationStart };
//# sourceMappingURL=notify-inspector.mjs.map
@@ -0,0 +1,19 @@
import { inertia } from '../generators/inertia.mjs';
import { keyframes } from '../generators/keyframes.mjs';
import { spring } from '../generators/spring.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 };
//# sourceMappingURL=replace-transition-type.mjs.map
@@ -0,0 +1,17 @@
import { MotionGlobalConfig } from 'motion-utils';
/**
* If an animation's keyframes resolve, or its first frame renders, more than
* this long after its start time, it starts from then instead. While we prefer
* to "start" an animation as soon as it's triggered, this avoids a visual jump
* into it after main thread work.
*
* Manual timing steps frames by however long it likes, so never lags.
*/
const MAX_START_DELAY = 40;
const resolveStartTime = (startTime, now) => now - startTime > MAX_START_DELAY && !MotionGlobalConfig.useManualTiming
? now
: startTime;
export { resolveStartTime };
//# sourceMappingURL=resolve-start-time.mjs.map
@@ -0,0 +1,15 @@
/**
* If `transition` has `inherit: true`, shallow-merge it with
* `parentTransition` (child keys win) and strip the `inherit` key.
* Otherwise return `transition` unchanged.
*/
function resolveTransition(transition, parentTransition) {
if (transition?.inherit && parentTransition) {
const { inherit: _, ...rest } = transition;
return { ...parentTransition, ...rest };
}
return transition;
}
export { resolveTransition };
//# sourceMappingURL=resolve-transition.mjs.map
@@ -0,0 +1,4 @@
const cubicBezierAsString = ([a, b, c, d]) => `cubic-bezier(${a}, ${b}, ${c}, ${d})`;
export { cubicBezierAsString };
//# sourceMappingURL=cubic-bezier.mjs.map
@@ -0,0 +1,15 @@
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 };
//# sourceMappingURL=is-supported.mjs.map
@@ -0,0 +1,29 @@
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 };
//# sourceMappingURL=map-easing.mjs.map
@@ -0,0 +1,16 @@
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 };
//# sourceMappingURL=supported.mjs.map
@@ -0,0 +1,29 @@
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;
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;
return element.animate(keyframeOptions, options);
}
export { startWaapiAnimation };
//# sourceMappingURL=start-waapi-animation.mjs.map
@@ -0,0 +1,14 @@
import { memo } from 'motion-utils';
const supportsPartialKeyframes = /*@__PURE__*/ memo(() => {
try {
document.createElement("div").animate({ opacity: [1] });
}
catch (e) {
return false;
}
return true;
});
export { supportsPartialKeyframes };
//# sourceMappingURL=partial-keyframes.mjs.map
@@ -0,0 +1,62 @@
import { memo } from 'motion-utils';
import { acceleratedValues } from '../utils/accelerated-values.mjs';
import { hasBrowserOnlyColors } from '../utils/is-browser-color.mjs';
const colorProperties = new Set([
"color",
"backgroundColor",
"outlineColor",
"fill",
"stroke",
"borderColor",
"borderTopColor",
"borderRightColor",
"borderBottomColor",
"borderLeftColor",
]);
const supportsWaapi = /*@__PURE__*/ memo(() => Object.hasOwnProperty.call(Element.prototype, "animate"));
function supportsBrowserAnimation(options) {
const { motionValue, name, repeatDelay, repeatType, damping, type, keyframes, } = options;
/**
* Most values can't be accelerated at all, so check the name before
* looking at the element or its props.
*/
if (!name ||
!(acceleratedValues.has(name) || colorProperties.has(name))) {
return false;
}
const subject = motionValue?.owner?.current;
/**
* We use instanceof checks instead of isHTMLElement()/isSVGElement()
* 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) &&
!(subject instanceof SVGElement)) {
return false;
}
const { onUpdate, transformTemplate } = motionValue.owner.getProps();
return (supportsWaapi() &&
/**
* Force WAAPI for color properties with browser-only color formats
* (oklch, oklab, lab, lch, etc.) that the JS animation path can't parse.
*/
(acceleratedValues.has(name) ||
(colorProperties.has(name) &&
hasBrowserOnlyColors(keyframes))) &&
(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, supportsWaapi };
//# sourceMappingURL=waapi.mjs.map
@@ -0,0 +1,902 @@
import { isBezierDefinition, millisecondsToSeconds, clamp, progress } from 'motion-utils';
import { frame } from '../../../frameloop/frame.mjs';
import { frameData } from '../../../frameloop/frame-data.mjs';
import { time } from '../../../frameloop/sync-time.mjs';
import { buildTransform, translateAlias } from '../../../render/html/utils/build-transform.mjs';
import { transformPropOrder } from '../../../render/utils/keys-transform.mjs';
import { mixNumber } from '../../../utils/mix/number.mjs';
import { numberValueTypes } from '../../../value/types/maps/number.mjs';
import { getValueAsType } from '../../../value/types/utils/get-as-type.mjs';
import { frameloopDriver } from '../../drivers/frame.mjs';
import { keyframes } from '../../generators/keyframes.mjs';
import { defaultOffset } from '../../keyframes/offsets/default.mjs';
import { JSAnimation, round } from '../../JSAnimation.mjs';
import { sampleDelta } from '../../NativeAnimationExtended.mjs';
import { cubicBezierAsString } from '../easing/cubic-bezier.mjs';
import { supportedWaapiEasing } from '../easing/supported.mjs';
import { supportsWaapi } from '../supports/waapi.mjs';
import { supportsLinearEasing } from '../../../utils/supports/linear-easing.mjs';
import { transformGroups } from './groups.mjs';
/**
* Sample at the interruption sampling rate, up to a cap for long
* animations.
*/
const maxSamples = 200;
/**
* How far values may stray from the WAAPI animation: as a fraction of
* each value's change when they share an easing, otherwise in px or deg,
* or for scale.
*/
const easingTolerance = 0.001;
const sampleTolerance = 0.1;
const scaleTolerance = 0.001;
/**
* If an outside write, or a seek the WAAPI animation can't follow,
* happens again within this window, the group is being driven every
* frame (scrubbing, drag), which is cheaper on the main thread than
* rebuilding the WAAPI animation every frame.
*/
const churnWindow = 100;
/**
* Named easings that are the same cubic-bezier in JS and CSS.
*/
const isNativeEasing = (ease) => /^(linear|ease(In|Out|InOut))$/.test(ease);
const byTime = (a, b) => a - b;
/**
* n + 1 evenly spaced timestamps, from one timestamp to another.
*/
const spread = (from, to, n) => {
const result = [];
for (let i = 0; i <= n; i++)
result.push(mixNumber(from, to, i / n));
return result;
};
const getOwner = (options) => options.motionValue.owner;
const getGroup = (element) => transformGroups.get(element);
const read = (owner, key) => owner.latestValues ? owner.latestValues[key] : owner.getValue(key)?.get();
/**
* Whether this animation should join its element's transform group.
* Animations that can't be accelerated still join, so the group knows
* to stay on the main thread while they run. Browsers without linear()
* easing stay on the main thread.
*/
const canGroupTransform = ({ name, motionValue, }) => name &&
transformPropOrder.includes(name) &&
motionValue?.owner?.current instanceof HTMLElement &&
supportsWaapi() &&
supportsLinearEasing();
function canAccelerateTransform(options) {
const owner = getOwner(options);
const { onUpdate, transformTemplate } = owner.getProps();
return (
/**
* A handoff from an optimised appear animation is already running
* on its own WAAPI animation. onUpdate needs every value, every
* frame.
*/
!options.isHandoff &&
!onUpdate &&
!options.hasOnUpdate &&
/**
* transformTemplate could output anything.
*/
!transformTemplate &&
options.type !== "inertia" &&
/**
* A literal transform, or pathRotation, replaces or adds to the
* transform we'd build. styleEffect builds its own as "none".
*/
(read(owner, "transform") || "none") === "none" &&
!read(owner, "pathRotation") &&
/**
* Layout animations write transform every frame.
*/
!owner.projection?.isProjecting());
}
class TransformAnimation extends JSAnimation {
constructor(options) {
const element = getOwner(options).current;
const { driver = frameloopDriver } = options;
let animation;
/**
* Before JSAnimation replaces the type with its generator.
*/
const canAccelerate = canAccelerateTransform(options);
/**
* The driver ticks on the main thread only once the group has
* moved there. Until then, starting or stopping tells the group to
* update its WAAPI animation.
*/
options.driver = (update) => {
const js = driver(update);
return {
js,
now: js.now,
start: (keepAlive = true) => {
const group = getGroup(element);
const isJS = !group || group.isJS;
/**
* keepAlive is false when seeking. Seeking the WAAPI
* animation is free, but if a seek needs a rebuild and
* keeps happening, the group moves to the main thread.
*/
isJS
? js.start(keepAlive)
: group.schedule(false, !keepAlive);
},
stop: () => {
js.stop();
const group = getGroup(element);
group && !group.isFinishing && group.release(animation);
},
};
};
super(options);
animation = this;
this.canAccelerate = canAccelerate;
/**
* stop is bound in JSAnimation, so wrap it rather than override.
* Before stopping, set the motion value to where the WAAPI
* animation is now, with velocity, for the next animation to
* start from.
*/
const { stop } = this;
this.stop = () => {
this.isAccelerated() && this.sync();
stop();
};
}
get group() {
return getGroup(getOwner(this.options).current);
}
get js() {
return this.driver?.js;
}
get name() {
return this.options.name;
}
/**
* When this stops moving, or, if it repeats forever, when it starts
* repeating, and how long each repeat takes. Held (paused, or at
* speed 0) animations never stop moving.
*/
timing() {
const { startTime, speed, totalDuration } = this;
const { delay = 0, repeatType } = this.options;
return this.holdTime !== null
? [Infinity, 0]
: totalDuration === Infinity
? [
startTime + delay / speed,
(this.resolvedDuration * (repeatType === "loop" ? 1 : 2)) /
speed || 1,
]
: [startTime + (delay + totalDuration) / speed, 0];
}
/**
* Whether this is running, or paused, on the compositor.
*/
isAccelerated() {
const { group, state } = this;
return (group?.tracks.has(this) &&
!group.isJS &&
this.canAccelerate &&
(state === "running" || state === "paused"));
}
/**
* The value now, while this runs on the compositor and so doesn't
* set its motion value every frame.
*/
liveValue() {
return this.isAccelerated() ? this.sampleAt(time.now()) : undefined;
}
/**
* If this is a two-keyframe tween whose easing WAAPI runs exactly,
* when it starts and stops moving, and that easing.
*/
tween() {
const { keyframes: keyframes$1, type, ease, times, repeat, delay = 0 } = this.options;
const easing = isBezierDefinition(ease)
? cubicBezierAsString(ease)
: isNativeEasing(ease) && supportedWaapiEasing[ease];
if (easing &&
!repeat &&
!times &&
keyframes$1.length === 2 &&
type === keyframes &&
this.holdTime === null) {
const start = this.startTime + delay / this.speed;
return [start, start + this.resolvedDuration / this.speed, easing];
}
}
/**
* The timestamps between two timestamps where this reaches a
* keyframe or starts a repeat, where it can turn or jump.
*/
boundaries(from, to) {
const { startTime, speed, resolvedDuration, options } = this;
const { delay = 0, repeat = 0, repeatDelay = 0, repeatType, times, keyframes: keyframes$1, type, } = options;
const offsets = (type || keyframes) === keyframes
? times || defaultOffset(keyframes$1)
: [0, 1];
const start = startTime + delay / speed;
const result = [];
if (this.holdTime !== null || !(resolvedDuration > 0))
return result;
for (let i = Math.max(0, Math.floor(((from - start) * speed) / resolvedDuration)); i <= repeat; i++) {
const iterationStart = start + (i * resolvedDuration) / speed;
if (iterationStart > to)
break;
for (const offset of offsets) {
const t = iterationStart +
((i % 2 && repeatType && repeatType !== "loop"
? 1 - offset
: offset) *
(resolvedDuration - repeatDelay)) /
speed;
t > from && t < to && result.push(t);
}
}
return result;
}
/**
* The value at a timestamp, as a number.
*/
numberAt(timestamp) {
return parseFloat(this.sampleAt(timestamp, true));
}
/**
* Write the current value, and velocity, to the motion value.
*/
sync() {
const now = time.now();
this.options.motionValue.setWithVelocity(this.sampleAt(now - sampleDelta, true), this.sampleAt(now, true), sampleDelta);
}
play() {
/**
* A stopped animation (one that was interrupted) can't play again,
* so it mustn't join the group.
*/
if (this.isStopped)
return;
(this.group || createGroup(getOwner(this.options))).tracks.add(this);
super.play();
}
pause() {
super.pause();
if (this.isAccelerated()) {
this.sync();
this.group.schedule(false);
}
}
/**
* JSAnimation finishes on its next tick, which only happens on the
* main thread, so finish now.
*/
complete() {
super.complete();
const { group } = this;
group && !group.isJS && this.canAccelerate && this.tick(time.now());
}
/**
* Without a tick to rebase it, a held time still includes the delay,
* so take it off here, as a JS animation does on its next tick.
*/
get time() {
if (this.isAccelerated()) {
return millisecondsToSeconds(clamp(0, this.totalDuration, this.at(time.now()) - (this.options.delay || 0)));
}
return super.time;
}
/**
* A JS animation sets its motion value from a seek on its next tick.
* This doesn't tick, so set it now, rather than leave it to be set
* when this next stops.
*/
set time(newTime) {
super.time = newTime;
this.isAccelerated() && this.sync();
}
/**
* Scroll timelines set time every frame, so run these on the main
* thread.
*/
attachTimeline(timeline) {
this.canAccelerate = false;
this.group?.demote(true);
return super.attachTimeline(timeline);
}
}
function createGroup(owner) {
const element = owner.current;
const tracks = new Set();
const byKey = new Map();
/**
* Transforms that aren't animating, as built into the WAAPI animation.
*/
let statics = {};
/**
* The transforms in the WAAPI animation, in order.
*/
let keys = [];
let built = new Map();
let segments = [];
let needsBuild = false;
let isScheduled = false;
let isEnded = false;
let hasChurned = false;
let lastChurn = -Infinity;
const has = (key) => {
for (const track of tracks)
if (track.name === key)
return true;
};
/**
* The transform values at a timestamp or, without one, from the
* motion values. Animations that don't repeat forever are sampled no
* earlier than settled, so a loop that starts from an earlier cycle
* boundary holds them at their end.
*/
const valuesAt = (timestamp, settled) => {
const values = {};
for (const key of timestamp === undefined ? transformPropOrder : keys) {
const track = byKey.get(key);
const value = timestamp === undefined
? read(owner, key)
: track
? track.sampleAt(settled > timestamp && !track.timing()[1]
? settled
: timestamp)
: statics[key];
if (value !== undefined)
values[key] = value;
}
return values;
};
/**
* Build the transform at a timestamp. Unlike buildTransform, every key
* is written, even at its default, so each keyframe has the same list
* of functions and the browser interpolates them one by one.
*/
const compose = (timestamp, settled) => {
let transform = "";
const values = valuesAt(timestamp, settled);
for (const key in values) {
transform +=
(translateAlias[key] || key) +
"(" +
getValueAsType(values[key], numberValueTypes[key]) +
") ";
}
return transform || "none";
};
/**
* Write the transform as an inline style, as the renderer would.
*/
const commit = (values = valuesAt()) => {
element.style.transform = buildTransform(values, {});
};
/**
* Cancel the WAAPI animations, by default first committing the
* transform. The renderer writes its own version on its next render.
*/
const stop = (shouldCommit = true) => {
shouldCommit && commit();
segments.forEach(({ animation }) => animation.cancel());
segments = [];
};
const add = (animation, offset, end = Infinity) => {
const segment = { animation, offset, end };
animation.startTime = time.now() + offset;
/**
* Animations finish when the WAAPI animation reaches their end.
* Once a loop has taken over, the finished animation isn't needed.
*/
if (end < Infinity) {
animation.onfinish = () => {
segment.isDone = true;
if (segments.some((s) => s.end === Infinity)) {
animation.cancel();
segments = segments.filter((s) => s !== segment);
}
group.schedule(false);
};
}
/**
* Something else cancelled it, so carry on on the main thread.
*/
animation.oncancel = () => segments.includes(segment) && group.demote();
segments.push(segment);
};
/**
* Where a value jumps between two timestamps, as when a repeat starts
* again: the timestamps either side of the jump, or none if it moves
* there continuously.
*/
const jumpAt = (value, a, b) => {
const change = (x, y) => Math.abs(value(y) - value(x));
const size = change(a, b);
while (b - a > 0.01) {
const mid = (a + b) / 2;
change(a, mid) > change(mid, b) ? (b = mid) : (a = mid);
}
return change(a, b) > size / 2 ? [a, b] : [];
};
/**
* If every moving animation follows the same eased progress between
* two timestamps, the easing that describes it.
*/
const getEasing = (moving, from, to, bounds) => {
const samples = Math.min(maxSamples, Math.max(2, Math.ceil((to - from) / sampleDelta)));
/**
* Too many keyframes or repeats to follow with this many samples.
*/
if (bounds.length * 4 > samples)
return;
const at = spread(from, to, samples).concat(bounds).sort(byTime);
const position = (t) => progress(from, to, t);
let shared;
let progressAt;
for (const track of moving) {
const values = at.map((t) => track.numberAt(t));
const [first] = values;
const changes = values.map((value) => value - first);
const end = changes[changes.length - 1];
/**
* A value that ends only a tiny way from where it started, as
* a round trip does if it's built just after it starts, or
* from a float residue, ends where it started. Dividing by
* that tiny range would stretch the easing to values the
* compositor can't render.
*/
const range = changes.some((change) => Math.abs(change) * easingTolerance > Math.abs(end))
? 0
: end;
const progress = changes.map((change) => range ? change / range : change);
/**
* A value that doesn't change, like one still in its delay,
* fits any easing. One that changes but ends where it started
* fits none.
*/
if (!range) {
if (progress.some((p) => !(Math.abs(p) <= easingTolerance))) {
return;
}
}
else if (!shared) {
shared = progress;
progressAt = (t) => (track.numberAt(t) - first) / range;
}
else if (progress.some((p, i) => !(Math.abs(p - shared[i]) <= easingTolerance))) {
return;
}
}
if (!shared ||
shared.every((p, i) => Math.abs(p - position(at[i])) <= easingTolerance)) {
return "linear";
}
/**
* A repeat that starts again jumps back, which takes two points at
* the same position.
*/
const points = [];
const point = (t, p = progressAt(t)) => points.push(round(p) + " " + round(position(t) * 100) + "%");
shared.forEach((p, i) => {
i &&
Math.abs(p - shared[i - 1]) > 0.2 &&
jumpAt(progressAt, at[i - 1], at[i]).forEach((t) => point(t));
point(at[i], p);
});
return "linear(" + points + ")";
};
/**
* Sample timestamps between two timestamps, closer together where the
* moving animations curve.
*/
const sample = (moving, from, to, bounds) => {
const times = [from];
/**
* Values that aren't numbers, like calc(), are left to the
* browser to interpolate.
*/
const isStraight = (a, b) => moving.every((track) => !(Math.abs(track.numberAt((a + b) / 2) -
(track.numberAt(a) + track.numberAt(b)) / 2) >
(track.name.startsWith("scale")
? scaleTolerance
: sampleTolerance)));
const split = (a, b, depth) => {
if (b - a < 1 || isStraight(a, b)) {
times.push(b);
}
else if (depth < 4) {
split(a, (a + b) / 2, depth + 1);
split((a + b) / 2, b, depth + 1);
}
else {
/**
* Keyframes either side of a jump, like a repeat starting
* again, so it doesn't play as a quick sweep.
*/
for (const track of moving) {
times.push(...jumpAt((t) => track.numberAt(t), a, b));
}
times.push(b);
}
};
/**
* Split at least every 100ms, and at every keyframe and repeat,
* either side of where a value can jump.
*/
const ends = spread(from, to, Math.ceil((to - from) / 100));
for (const t of bounds)
ends.push(t, Math.min(t + 0.01, to));
let start = from;
for (const end of ends.sort(byTime)) {
end > start && split(start, end, 0);
start = end;
}
return times.sort(byTime);
};
/**
* If every moving animation is the same tween, started together, as
* happens for the values of one animate() call or transition, when
* they move and their native easing.
*/
const getTween = (moving) => {
let shared;
for (const track of moving) {
const tween = track.tween();
if (!tween ||
shared?.some((v, i) => i < 2
? Math.abs(tween[i] - v) > 1
: tween[i] !== v)) {
return;
}
shared = tween;
}
return shared;
};
/**
* A WAAPI animation of the composed transform between two timestamps.
*/
const run = (moving, from, to, options = { fill: "both" }, settled, nativeEasing) => {
const bounds = moving.flatMap((track) => track.boundaries(from, to));
const easing = nativeEasing || getEasing(moving, from, to, bounds);
const times = easing ? [from, to] : sample(moving, from, to, bounds);
const transform = times.map((t) => compose(t, settled));
let keyframes = { transform };
if (!easing) {
keyframes.offset = times.map((t) => progress(from, to, t));
}
/**
* If nothing moves, don't hold a transform, which, even at its
* default, would make the element a containing block. The empty
* animation still finishes the animations.
*/
if (transform.every((t) => t === transform[0])) {
commit(valuesAt(from, settled));
keyframes = null;
}
return element.animate(keyframes, {
...options,
duration: to - from,
easing: easing || "linear",
});
};
/**
* Replace the WAAPI animations with ones built from now. Returns
* undefined if the animations can't be composed.
*/
const animate = (now) => {
/**
* The finite part runs until the last finite animation (or the
* last delay) ends. Infinitely repeating animations then continue
* as one looping WAAPI animation, which needs them to share a
* cycle.
*/
let end = now;
let cycle = 0;
let cycleStart = 0;
const moving = [];
const looping = [];
const ends = new Set();
byKey.clear();
built = new Map();
for (const track of tracks) {
byKey.set(track.name, track);
const [trackEnd, trackCycle] = track.timing();
const isHeld = trackEnd === Infinity;
built.set(track, [track.at(now), track.speed, isHeld]);
/**
* Held animations don't move, so keep their motion values up
* to date, as a JSAnimation would on a seek.
*/
if (isHeld) {
track.sync();
continue;
}
if (track.speed < 0)
return;
if (trackCycle) {
if (cycle) {
const phase = (trackEnd - cycleStart) / trackCycle;
if (Math.abs(trackCycle - cycle) > 1 ||
Math.abs(phase - Math.round(phase)) * cycle > 1) {
return;
}
}
else {
cycle = trackCycle;
cycleStart = trackEnd;
}
looping.push(track);
}
else {
ends.add(trackEnd);
}
moving.push(track);
end = Math.max(end, trackEnd);
}
statics = {};
/**
* Transforms that aren't animating are left out at their default.
*/
keys = transformPropOrder.filter((key) => {
if (byKey.has(key))
return true;
const value = (statics[key] = read(owner, key));
return (value !== undefined &&
parseFloat(value) !==
(key.startsWith("scale") ? 1 : 0));
});
stop(false);
/**
* Values that share one tween are one WAAPI animation of it, with
* two keyframes and its own easing, from when they started.
*/
const tween = !cycle && getTween(moving);
try {
if (tween && tween[1] > now) {
const [from, to, easing] = tween;
add(run(moving, from, to, undefined, undefined, easing), from - now, to - from);
}
else if (end > now) {
add(run(moving, now, end), 0, end - now);
/**
* Animations that end sooner finish on the events of
* empty animations that end with them.
*/
ends.forEach((trackEnd) => trackEnd < end - 1 &&
add(element.animate(null, {
duration: trackEnd - now,
}), 0, trackEnd - now));
}
if (cycle) {
/**
* Build one cycle from a cycle boundary, so it repeats
* seamlessly, and start it part way through.
*/
const from = cycleStart + Math.floor((end - cycleStart) / cycle) * cycle;
add(run(looping, from, from + cycle, {
iterations: Infinity,
iterationStart: (end - from) / cycle,
/**
* It must not fill backwards over the finite part
* that runs before it.
*/
fill: "forwards",
}, end), end - now);
}
}
catch {
return;
}
/**
* Everything is held, so hold it with an inline style instead.
*/
segments.length || stop();
return true;
};
/**
* If every animation has been paused, played, seeked or sped up
* together since the WAAPI animation was built, do the same to it.
* Returns false if it needs rebuilding instead.
*/
const retime = (now) => {
let elapsed;
let rate = 0;
for (const track of tracks) {
const state = built.get(track);
if (!state)
return false;
const [builtTime, builtSpeed, wasHeld] = state;
const trackTime = track.at(now);
const isHeld = track.timing()[0] === Infinity;
if (wasHeld) {
if (isHeld && trackTime === builtTime)
continue;
return false;
}
const trackElapsed = (trackTime - builtTime) / builtSpeed;
const trackRate = isHeld ? 0 : track.speed / builtSpeed;
if (elapsed === undefined) {
elapsed = trackElapsed;
rate = trackRate;
}
else if (Math.abs(trackElapsed - elapsed) > 1 ||
trackRate !== rate) {
return false;
}
}
if (elapsed === undefined)
return true;
if (!segments.length)
return false;
for (const { offset, end, isDone } of segments) {
const local = elapsed - offset;
if (end < Infinity &&
(isDone ? local < end - 1 : local < 0 || local >= end)) {
return false;
}
}
for (const { animation, offset, isDone } of segments) {
if (isDone)
continue;
const local = elapsed - offset;
if (rate) {
animation.playbackRate = rate;
animation.startTime = now - local / rate;
}
else {
animation.pause();
animation.currentTime = local;
}
}
return true;
};
/**
* If no animations are left, write where they got to and cancel the
* WAAPI animations. Returns whether the group ended.
*/
const end = () => {
if (tracks.size)
return false;
isEnded = true;
transformGroups.delete(element);
group.isJS || stop();
return true;
};
const build = () => {
const now = time.now();
/**
* Finish the animations that have reached their end, which the
* WAAPI animation has already rendered.
*/
if (!group.isJS) {
group.isFinishing = true;
tracks.forEach((track) => {
const [trackEnd, trackCycle] = track.timing();
if (track.state === "running" &&
track.speed > 0 &&
!trackCycle &&
trackEnd <= now + 1) {
track.complete();
/**
* From now it's checked for outside writes like any
* other static transform. If the WAAPI animation never
* rendered it (it was instant), rebuild with its value.
*/
statics[track.name] = read(owner, track.name);
built.has(track) || (needsBuild = true);
}
});
group.isFinishing = false;
}
isScheduled = false;
if (isEnded)
return;
tracks.forEach(({ state }, track) => {
if (state === "idle" || state === "finished")
tracks.delete(track);
});
if (end())
return;
for (const track of tracks) {
if (!track.canAccelerate)
return group.demote(true);
}
/**
* Everything that kept the group on the main thread has finished,
* so move the remaining animations back to the compositor.
*/
if (group.isJS) {
group.isJS = false;
needsBuild = true;
tracks.forEach((track) => track.js?.stop());
}
const shouldBuild = needsBuild;
const churned = hasChurned;
needsBuild = hasChurned = false;
if (shouldBuild || !retime(now)) {
if (churned) {
if (now - lastChurn < churnWindow)
return group.demote();
lastChurn = now;
}
if (!animate(now))
return group.demote();
}
};
const group = {
tracks,
isJS: false,
isFinishing: false,
schedule(build_, isChurn) {
if (isEnded)
return;
needsBuild || (needsBuild = build_);
hasChurned || (hasChurned = !!isChurn);
/**
* Build in this frame's render step, so it's in place before
* anything reads the DOM in postRender, or straight after the
* current task outside of a frame.
*/
if (!isScheduled) {
isScheduled = true;
frameData.isProcessing
? frame.render(build, false, true)
: queueMicrotask(build);
}
},
/**
* End the group now, rather than when it next builds, so the
* element's transform is the caller's again as soon as its last
* animation is stopped.
*/
release(track) {
/**
* Only drop the stopped animation. Others can be idle because
* they're part way through play(), and are about to run.
*/
const { state } = track;
if (state === "idle" || state === "finished")
tracks.delete(track);
end() || group.schedule(true);
},
/**
* An outside write. Rebuild once, but if it keeps happening, move
* to the main thread.
*/
check() {
if (group.isJS)
return;
for (const key in statics) {
if (read(owner, key) !== statics[key] && !has(key)) {
return group.schedule(true, true);
}
}
},
demote(canReturn) {
if (group.isJS || isEnded)
return;
/**
* Unless only another animation is holding the group on the
* main thread, these animations stay there until they end.
*/
tracks.forEach((track) => {
track.isAccelerated() && track.sync();
canReturn || (track.canAccelerate = false);
});
group.isJS = true;
built = new Map();
stop();
tracks.forEach((track) => track.state === "running" && track.js?.start());
},
};
transformGroups.set(element, group);
return group;
}
export { TransformAnimation, canGroupTransform };
//# sourceMappingURL=TransformAnimation.mjs.map
@@ -0,0 +1,4 @@
const transformGroups = new WeakMap();
export { transformGroups };
//# sourceMappingURL=groups.mjs.map
@@ -0,0 +1,13 @@
/**
* A list of values that can be hardware-accelerated.
*/
const acceleratedValues = new Set([
"opacity",
"clipPath",
"filter",
"transform",
"backgroundColor",
]);
export { acceleratedValues };
//# sourceMappingURL=accelerated-values.mjs.map
@@ -0,0 +1,16 @@
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 };
//# sourceMappingURL=apply-generator.mjs.map
@@ -0,0 +1,13 @@
const browserColorFunctions = /^(?:oklch|oklab|lab|lch|color|color-mix|light-dark)\(/;
function hasBrowserOnlyColors(keyframes) {
for (let i = 0; i < keyframes.length; i++) {
if (typeof keyframes[i] === "string" &&
browserColorFunctions.test(keyframes[i])) {
return true;
}
}
return false;
}
export { hasBrowserOnlyColors };
//# sourceMappingURL=is-browser-color.mjs.map
@@ -0,0 +1,13 @@
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 };
//# sourceMappingURL=linear.mjs.map
@@ -0,0 +1,59 @@
import { cornerRadiusProps } from '../../../utils/border-radius.mjs';
const pxValues = new Set([
// Border props
"borderWidth",
"borderTopWidth",
"borderRightWidth",
"borderBottomWidth",
"borderLeftWidth",
"borderRadius",
...cornerRadiusProps,
// Positioning props
"width",
"maxWidth",
"height",
"maxHeight",
"top",
"right",
"bottom",
"left",
"inset",
"insetBlock",
"insetBlockStart",
"insetBlockEnd",
"insetInline",
"insetInlineStart",
"insetInlineEnd",
// Spacing props
"padding",
"paddingTop",
"paddingRight",
"paddingBottom",
"paddingLeft",
"paddingBlock",
"paddingBlockStart",
"paddingBlockEnd",
"paddingInline",
"paddingInlineStart",
"paddingInlineEnd",
"margin",
"marginTop",
"marginRight",
"marginBottom",
"marginLeft",
"marginBlock",
"marginBlockStart",
"marginBlockEnd",
"marginInline",
"marginInlineStart",
"marginInlineEnd",
// Typography
"fontSize",
// Misc
"backgroundPositionX",
"backgroundPositionY",
]);
export { pxValues };
//# sourceMappingURL=px-values.mjs.map
@@ -0,0 +1,19 @@
import { circInOut, backInOut, anticipate } 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 };
//# sourceMappingURL=unsupported-easing.mjs.map
@@ -0,0 +1,112 @@
import { frame } from '../frameloop/frame.mjs';
class MotionValueState {
/**
* @param step - The frameloop step renders are scheduled in. Defaults
* to `frame.render`. Effects that feed a render loop running in
* `frame.render` (GPU scenes) should write in `frame.preRender`.
*/
constructor(step = frame.render) {
this.step = step;
this.values = new Map();
/**
* Renders scheduled for the next flush, usually one or two (values
* bound to the same computed render, e.g. `x` and `y` to `transform`,
* share an entry). Tracked with a count rather than truncating the
* array so no backing store is reallocated every frame. Kept per
* state so pending writes can be flushed synchronously before the
* subject is measured.
*/
this.pending = [];
this.numPending = 0;
/**
* Run the pending renders. Resetting the count first means a render
* scheduled while flushing lands in the next frame, as with the
* frameloop itself.
*/
this.flush = () => {
const { pending, numPending } = this;
this.numPending = 0;
for (let i = 0; i < numPending; i++)
pending[i]();
};
}
/**
* @param render - Writes the value to the subject. Renders read the
* motion value directly rather than a cached copy, so there is one
* place a value lives.
* @param computed - A value already in this state (e.g. `transform`)
* whose render should run whenever `value` changes.
*/
set(name, value, render, computed) {
this.values.get(name)?.onRemove();
if (computed) {
for (const entry of this.values.values()) {
if (entry.value === computed)
render = entry.render;
}
}
const onChange = () => render && this.schedule(render);
/**
* Values created ahead of a DOM read start out undefined and
* have nothing to render until they're set.
*/
value.get() !== undefined && onChange();
const cancelOnChange = value.on("change", onChange);
const onRemove = () => {
cancelOnChange();
render && !computed && this.cancel(render);
/**
* A later set() of the same name has already removed this
* entry, so leave the one that replaced it.
*/
this.values.get(name)?.onRemove === onRemove &&
this.values.delete(name);
};
this.values.set(name, {
value,
render: computed ? undefined : render,
onRemove,
});
return onRemove;
}
get(name) {
return this.values.get(name)?.value;
}
/**
* Detach every value from this state and return them, so another
* renderer can take them over. The state stays cached by its effect,
* so the bound transforms are reset too rather than leaking into
* values bound later.
*/
release() {
const values = new Map();
this.values.forEach((entry, name) => {
values.set(name, entry.value);
entry.onRemove();
});
this.transformKeys = this.transformValues = undefined;
return values;
}
schedule(render) {
const { pending, numPending } = this;
for (let i = 0; i < numPending; i++) {
if (pending[i] === render)
return;
}
numPending || this.step(this.flush);
pending[this.numPending++] = render;
}
cancel(render) {
const { pending } = this;
for (let i = 0; i < this.numPending; i++) {
if (pending[i] === render) {
pending[i] = pending[--this.numPending];
return;
}
}
}
}
export { MotionValueState };
//# sourceMappingURL=MotionValueState.mjs.map
@@ -0,0 +1,48 @@
import { camelToDash } from '../../render/dom/utils/camel-to-dash.mjs';
import { numberValueTypes } from '../../value/types/maps/number.mjs';
import { getValueAsType } from '../../value/types/utils/get-as-type.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";
}
/**
* `name` is the attribute written when it differs from the key the value
* is stored under, e.g. `attrX` -> `x`. Numbers take the attribute's
* default unit (`x: 100` -> `"100px"`), as they do when key and name match.
*/
const addAttrValue = (element, state, key, value, name = key) => {
const isProp = canSetAsProperty(element, name);
if (!isProp && (name.startsWith("data") || name.startsWith("aria"))) {
name = camelToDash(name);
}
const type = numberValueTypes[key] || numberValueTypes[name];
/**
* Set attribute directly via property if available
*/
const render = isProp
? () => {
element[name] = getValueAsType(value.get(), numberValueTypes[key]);
}
: () => {
const v = getValueAsType(value.get(), type);
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 };
//# sourceMappingURL=index.mjs.map
@@ -0,0 +1,23 @@
import { isObject } from 'motion-utils';
import { createEffect } from '../utils/create-effect.mjs';
/**
* Writes motion values to the properties of any object. This is
* animate()'s fallback for subjects no registered effect claims.
*/
const propEffect = /*@__PURE__*/ createEffect((subject, state, key, value) => {
return state.set(key, value, () => {
subject[key] = value.get();
});
}, {
test: (subject) => isObject(subject),
read: (subject, key) => {
const value = subject[key];
return typeof value === "string" || typeof value === "number"
? value
: undefined;
},
});
export { propEffect };
//# sourceMappingURL=index.mjs.map
@@ -0,0 +1,99 @@
import { transformGroups } from '../../animation/waapi/transforms/groups.mjs';
import { isCSSVar } from '../../render/dom/is-css-var.mjs';
import { readTransformValue } from '../../render/dom/parse-transform.mjs';
import { transformProps, transformPropOrder } from '../../render/utils/keys-transform.mjs';
import { isHTMLElement } from '../../utils/is-html-element.mjs';
import { isSVGElement } from '../../utils/is-svg-element.mjs';
import { MotionValue } from '../../value/index.mjs';
import { numberValueTypes } from '../../value/types/maps/number.mjs';
import { getValueAsType } from '../../value/types/utils/get-as-type.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"]);
/**
* A bound value in its default unit, e.g. `originX: 50` -> `"50%"`.
*/
const styleValue = (state, key) => getValueAsType(state.get(key)?.get(), numberValueTypes[key]);
const addStyleValue = (element, state, key, value) => {
let render = undefined;
let computed = undefined;
if (transformProps.has(key)) {
if (key !== "pathRotation") {
const keys = (state.transformKeys ?? (state.transformKeys = []));
(state.transformValues ?? (state.transformValues = {}))[key] = value;
if (!keys.includes(key)) {
keys.push(key);
keys.sort((a, b) => transformPropOrder.indexOf(a) -
transformPropOrder.indexOf(b));
}
}
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"), () => {
transformGroups.get(element)?.check();
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 = styleValue(state, "originX") ?? "50%";
const originY = styleValue(state, "originY") ?? "50%";
const originZ = styleValue(state, "originZ") ?? 0;
element.style.transformOrigin = `${originX} ${originY} ${originZ}`;
});
}
computed = state.get("transformOrigin");
}
else if (isCSSVar(key)) {
render = () => {
element.style.setProperty(key, value.getCurrent());
};
}
else {
render = () => {
element.style[key] = getValueAsType(value.getCurrent(), numberValueTypes[key]);
};
}
return state.set(key, value, render, computed);
};
const isStyleSubject = (subject) => isHTMLElement(subject) || isSVGElement(subject);
/**
* Reads the current value of a style from the element, as the initial
* keyframe when `animate()` targets an element via this effect. Transforms
* are parsed out of the computed matrix; everything else is the computed
* style. Units aren't converted to match the target keyframes. A style
* the browser can't report is 0, as a VisualElement reports it.
*/
const readStyleValue = (element, key) => {
if (transformProps.has(key)) {
return readTransformValue(element, key);
}
const computedStyle = getComputedStyle(element);
const value = isCSSVar(key)
? computedStyle.getPropertyValue(key)
: computedStyle[key];
return (typeof value === "string" && value.trim()) || 0;
};
/**
* The per-element effect `animate()` binds through, so HTML values share
* state with a direct `styleEffect()` call. The exported `styleEffect`
* is this wrapped to also accept selectors.
*/
const styleSubjectEffect = /*@__PURE__*/ createEffect(addStyleValue, {
test: isStyleSubject,
read: readStyleValue,
});
const styleEffect =
/*@__PURE__*/ createSelectorEffect(styleSubjectEffect);
export { addStyleValue, originProps, readStyleValue, styleEffect, styleSubjectEffect };
//# sourceMappingURL=index.mjs.map
@@ -0,0 +1,46 @@
import { translateAlias } from '../../render/html/utils/build-transform.mjs';
import { transformValueTypes } from '../../value/types/maps/transform.mjs';
import { getValueAsType } from '../../value/types/utils/get-as-type.mjs';
/**
* Cache of `translateX(`-style openers so each render is a few string
* concatenations rather than a template per transform.
*/
const openers = {};
function buildTransform(state) {
let transform = "";
const { transformKeys: keys = [], transformValues: values = {} } = state;
/**
* Loop over the bound transforms in order, adding the ones that
* aren't at their default value to the transform string.
*/
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = values[key].getCurrent();
if (value === undefined)
continue;
const parsed = typeof value === "number" ? value : parseFloat(value);
if (parsed !== (key.startsWith("scale") ? 1 : 0)) {
transform +=
(transform && " ") +
(openers[key] ||
(openers[key] = (translateAlias[key] || key) + "(")) +
getValueAsType(value, transformValueTypes[key]) +
")";
}
}
// See build-transform.ts: additive `rotate()` so user `rotate` isn't
// clobbered. Not a `transformPropOrder` slot.
const pathRotation = state.get("pathRotation")?.get();
if (pathRotation) {
transform +=
(transform && " ") +
"rotate(" +
getValueAsType(pathRotation, transformValueTypes.pathRotation) +
")";
}
return transform || "none";
}
export { buildTransform };
//# sourceMappingURL=transform.mjs.map
@@ -0,0 +1,91 @@
import { isCSSVar } from '../../render/dom/is-css-var.mjs';
import { camelToDash } from '../../render/dom/utils/camel-to-dash.mjs';
import { cssStyleProperties } from '../../render/svg/utils/build-attrs.mjs';
import { transformProps } from '../../render/utils/keys-transform.mjs';
import { isSVGElement } from '../../utils/is-svg-element.mjs';
import { MotionValue } from '../../value/index.mjs';
import { numberValueTypes } from '../../value/types/maps/number.mjs';
import { addAttrValue } from '../attr/index.mjs';
import { originProps, addStyleValue, readStyleValue } 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';
function addSVGPathValue(element, state, key, value) {
frame.render(() => element.setAttribute("pathLength", "1"));
if (key === "pathOffset") {
return state.set(key, value, () => {
// Use unitless value to avoid Safari zoom bug
const offset = value.get();
element.setAttribute("stroke-dashoffset", `${-offset}`);
});
}
else {
if (!state.get("stroke-dasharray")) {
state.set("stroke-dasharray", new MotionValue("1 1"), () => {
const pathLength = state.get("pathLength")?.get() ?? 1;
const pathSpacing = state.get("pathSpacing")?.get();
// Use unitless values to avoid Safari zoom bug
element.setAttribute("stroke-dasharray", `${pathLength} ${pathSpacing ?? 1 - Number(pathLength)}`);
});
}
return state.set(key, value, undefined, state.get("stroke-dasharray"));
}
}
const addSVGValue = (element, state, key, value) => {
if (key.startsWith("path") && !transformProps.has(key)) {
return addSVGPathValue(element, state, key, value);
}
else if (key.startsWith("attr")) {
return addAttrValue(element, state, key, value, convertAttrKey(key));
}
/**
* Transforms and origins aren't CSS properties (bar `x`/`y` in some
* browsers), so check them by name rather than `key in element.style`.
*/
const handler = transformProps.has(key) ||
originProps.has(key) ||
isCSSVar(key) ||
key in element.style
? addStyleValue
: addAttrValue;
return handler(element, state, key, value);
};
/**
* Reads the current value of `key` from an SVG element as the origin of
* an animation, as the SVG VisualElement does: transforms start from
* their defaults, CSS variables and the few CSS-only properties come from
* computed style, falling back to the attribute when that's empty (e.g.
* the element isn't rendered), and everything else is read from the
* attribute, dash-cased (`strokeWidth` -> `stroke-width`) or, failing
* that, as written (`baseFrequency`).
*/
const readSVGValue = (element, key) => {
if (transformProps.has(key)) {
return numberValueTypes[key]?.default || 0;
}
if (isCSSVar(key) || cssStyleProperties.includes(key)) {
return (readStyleValue(element, key) ||
element.getAttribute(camelToDash(key)) ||
0);
}
key = convertAttrKey(key);
return (element.getAttribute(camelToDash(key)) ??
element.getAttribute(key) ??
undefined);
};
/**
* The per-element effect `animate()` binds through, so SVG values share
* state with a direct `svgEffect()` call.
*/
const svgSubjectEffect = /*@__PURE__*/ createEffect(addSVGValue, {
test: isSVGElement,
read: readSVGValue,
});
const svgEffect = /*@__PURE__*/ createSelectorEffect(svgSubjectEffect);
function convertAttrKey(key) {
return key.replace(/^attr([A-Z])/, (_, firstChar) => firstChar.toLowerCase());
}
export { addSVGValue, readSVGValue, svgEffect, svgSubjectEffect };
//# sourceMappingURL=index.mjs.map
@@ -0,0 +1,27 @@
import { resolveElements } from '../../utils/resolve-elements.mjs';
function createSelectorEffect(subjectEffect) {
const effect = (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();
};
};
const { test, read, get, flush, state } = subjectEffect;
return Object.assign(effect, {
test,
read,
get: get,
flush,
state,
});
}
export { createSelectorEffect };
//# sourceMappingURL=create-dom-effect.mjs.map
@@ -0,0 +1,27 @@
import { MotionValueState } from '../MotionValueState.mjs';
function createEffect(addValue, { step, ...options } = {}) {
const stateCache = new WeakMap();
const effect = (subject, values) => {
const state = stateCache.get(subject) ?? new MotionValueState(step);
stateCache.set(subject, state);
const subscriptions = [];
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();
};
};
return Object.assign(effect, options, {
get: (subject, key) => stateCache.get(subject)?.get(key),
flush: (subject) => stateCache.get(subject)?.flush(),
state: (subject) => stateCache.get(subject),
});
}
export { createEffect };
//# sourceMappingURL=create-effect.mjs.map
@@ -0,0 +1,7 @@
function addDomEvent(target, eventName, handler, options = { passive: true }) {
target.addEventListener(eventName, handler, options);
return () => target.removeEventListener(eventName, handler, options);
}
export { addDomEvent };
//# sourceMappingURL=add-dom-event.mjs.map
@@ -0,0 +1,72 @@
import { MotionGlobalConfig } from 'motion-utils';
import { stepsOrder } from './order.mjs';
import { createRenderStep } from './render-step.mjs';
const maxElapsed = 40;
function createRenderBatcher(scheduleNextBatch, allowKeepAlive, state = {
delta: 0.0,
timestamp: 0.0,
isProcessing: false,
}) {
let runNextFrame = false;
let useDefaultElapsed = true;
const flagRunNextFrame = () => (runNextFrame = true);
const steps = stepsOrder.reduce((acc, key) => {
acc[key] = createRenderStep(flagRunNextFrame);
return acc;
}, {});
const { setup, read, resolveKeyframes, preUpdate, update, preRender, render, postRender, } = steps;
const processBatch = () => {
const useManualTiming = MotionGlobalConfig.useManualTiming;
const timestamp = useManualTiming
? state.timestamp
: performance.now();
runNextFrame = false;
if (!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 };
//# sourceMappingURL=batcher.mjs.map
@@ -0,0 +1,12 @@
/**
* The frameloop's state, kept apart from the frameloop itself so that
* reading it doesn't bundle the scheduler.
*/
const frameData = {
delta: 0.0,
timestamp: 0.0,
isProcessing: false,
};
export { frameData };
//# sourceMappingURL=frame-data.mjs.map
@@ -0,0 +1,8 @@
import { noop } from 'motion-utils';
import { createRenderBatcher } from './batcher.mjs';
import { frameData } from './frame-data.mjs';
const { schedule: frame, cancel: cancelFrame, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop, true, frameData);
export { cancelFrame, frame, frameSteps };
//# sourceMappingURL=frame.mjs.map
@@ -0,0 +1,21 @@
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 };
//# sourceMappingURL=index-legacy.mjs.map
@@ -0,0 +1,7 @@
import { createRenderBatcher } from './batcher.mjs';
const { schedule: microtask, cancel: cancelMicrotask } =
/* @__PURE__ */ createRenderBatcher(queueMicrotask, false);
export { cancelMicrotask, microtask };
//# sourceMappingURL=microtask.mjs.map
@@ -0,0 +1,13 @@
const stepsOrder = [
"setup", // Compute
"read", // Read
"resolveKeyframes", // Write/Read/Write/Read
"preUpdate", // Compute
"update", // Compute
"preRender", // Compute
"render", // Write
"postRender", // Compute
];
export { stepsOrder };
//# sourceMappingURL=order.mjs.map
@@ -0,0 +1,87 @@
function createRenderStep(runNextFrame) {
/**
* 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.
* A keepAlive process is always also held by a frame queue until
* it's cancelled, so a Set has the same lifetime semantics as a
* WeakSet here while being considerably faster to query every frame.
*/
const toKeepAlive = new Set();
let latestFrameData = {
delta: 0.0,
timestamp: 0.0,
isProcessing: false,
};
function triggerCallback(callback) {
if (toKeepAlive.has(callback)) {
nextFrame.add(callback);
runNextFrame();
}
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);
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;
// Swap this frame and the next to avoid GC
const prevFrame = thisFrame;
thisFrame = nextFrame;
nextFrame = prevFrame;
// Execute this frame
thisFrame.forEach(triggerCallback);
// 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 };
//# sourceMappingURL=render-step.mjs.map
@@ -0,0 +1,32 @@
import { MotionGlobalConfig } from 'motion-utils';
import { frameData } from './frame-data.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 };
//# sourceMappingURL=sync-time.mjs.map
@@ -0,0 +1,10 @@
const isDragging = {
x: false,
y: false,
};
function isDragActive() {
return isDragging.x || isDragging.y;
}
export { isDragActive, isDragging };
//# sourceMappingURL=is-active.mjs.map
@@ -0,0 +1,29 @@
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 };
//# sourceMappingURL=set-active.mjs.map
@@ -0,0 +1,70 @@
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 "pointerleave" event listener when the hover ends.
*
* @public
*/
function hover(elementOrSelector, onHoverStart, options = {}) {
const [elements, eventOptions, cancel] = setupGesture(elementOrSelector, options);
elements.forEach((element) => {
let isPressed = false;
let deferredHoverEnd = false;
let hoverEndCallback;
const removePointerLeave = () => {
element.removeEventListener("pointerleave", onPointerLeave);
};
const endHover = (event) => {
if (hoverEndCallback) {
hoverEndCallback(event);
hoverEndCallback = undefined;
}
removePointerLeave();
};
const onPointerUp = (event) => {
isPressed = false;
window.removeEventListener("pointerup", onPointerUp);
window.removeEventListener("pointercancel", onPointerUp);
if (deferredHoverEnd) {
deferredHoverEnd = false;
endHover(event);
}
};
const onPointerDown = () => {
isPressed = true;
window.addEventListener("pointerup", onPointerUp, eventOptions);
window.addEventListener("pointercancel", onPointerUp, eventOptions);
};
const onPointerLeave = (leaveEvent) => {
if (leaveEvent.pointerType === "touch")
return;
if (isPressed) {
deferredHoverEnd = true;
return;
}
endHover(leaveEvent);
};
const onPointerEnter = (enterEvent) => {
if (!isValidHover(enterEvent))
return;
deferredHoverEnd = false;
const onHoverEnd = onHoverStart(element, enterEvent);
if (typeof onHoverEnd !== "function")
return;
hoverEndCallback = onHoverEnd;
element.addEventListener("pointerleave", onPointerLeave, eventOptions);
};
element.addEventListener("pointerenter", onPointerEnter, eventOptions);
element.addEventListener("pointerdown", onPointerDown, eventOptions);
});
return cancel;
}
export { hover };
//# sourceMappingURL=hover.mjs.map
@@ -0,0 +1,97 @@
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();
}
const claimedPointerDownEvents = new WeakSet();
/**
* 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;
if (claimedPointerDownEvents.has(startEvent))
return;
isPressing.add(target);
if (options.stopPropagation) {
claimedPointerDownEvents.add(startEvent);
}
const onPressEnd = onPressStart(target, startEvent);
/**
* End listeners run in the capture phase so a descendant calling
* stopPropagation() in its own pointerup handler can't prevent the
* press gesture from ending. This also keeps the gesture-end
* ordering consistent with the drag gesture. See #2794.
*/
const endEventOptions = { ...eventOptions, capture: true };
const onPointerEnd = (endEvent, success) => {
window.removeEventListener("pointerup", onPointerUp, endEventOptions);
window.removeEventListener("pointercancel", onPointerCancel, endEventOptions);
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, endEventOptions);
window.addEventListener("pointercancel", onPointerCancel, endEventOptions);
};
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 };
//# sourceMappingURL=index.mjs.map
@@ -0,0 +1,34 @@
const keyboardAccessibleElements = new Set([
"BUTTON",
"INPUT",
"SELECT",
"TEXTAREA",
"A",
]);
/**
* Checks if an element is natively keyboard accessible (focusable).
* Used by the press gesture to determine if we need to add tabIndex.
*/
function isElementKeyboardAccessible(element) {
return (keyboardAccessibleElements.has(element.tagName) ||
element.isContentEditable === true);
}
const textInputElements = new Set(["INPUT", "SELECT", "TEXTAREA"]);
/**
* Checks if an element has text selection or direct interaction behavior
* that should block drag gestures from starting.
*
* This specifically targets form controls where the user might want to select
* text or interact with the control (e.g., sliders, dropdowns).
*
* Buttons and links are NOT included because they don't have click-and-move
* actions of their own - they only respond to click events, so dragging
* should still work when initiated from these elements.
*/
function isElementTextInput(element) {
return (textInputElements.has(element.tagName) ||
element.isContentEditable === true);
}
export { isElementKeyboardAccessible, isElementTextInput };
//# sourceMappingURL=is-keyboard-accessible.mjs.map
@@ -0,0 +1,39 @@
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 };
//# sourceMappingURL=keyboard.mjs.map
@@ -0,0 +1,4 @@
const isPressing = new WeakSet();
export { isPressing };
//# sourceMappingURL=state.mjs.map
@@ -0,0 +1,21 @@
/**
* 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 };
//# sourceMappingURL=is-node-or-child.mjs.map
@@ -0,0 +1,19 @@
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 };
//# sourceMappingURL=is-primary-pointer.mjs.map
@@ -0,0 +1,16 @@
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 };
//# sourceMappingURL=setup.mjs.map
+181
View File
@@ -0,0 +1,181 @@
export { AsyncMotionValueAnimation } from './animation/AsyncMotionValueAnimation.mjs';
export { GroupAnimation } from './animation/GroupAnimation.mjs';
export { GroupAnimationWithThen } from './animation/GroupAnimationWithThen.mjs';
export { JSAnimation, animateValue, round } from './animation/JSAnimation.mjs';
export { NativeAnimation } from './animation/NativeAnimation.mjs';
export { NativeAnimationExtended, sampleDelta } from './animation/NativeAnimationExtended.mjs';
export { NativeAnimationWrapper } from './animation/NativeAnimationWrapper.mjs';
export { animationMapKey, getAnimationMap } from './animation/utils/active-animations.mjs';
export { calcChildStagger } from './animation/utils/calc-child-stagger.mjs';
export { arc } from './animation/utils/arc.mjs';
export { getVariableValue, parseCSSVariable } from './animation/utils/css-variables-conversion.mjs';
export { getDefaultTransition } from './animation/utils/default-transitions.mjs';
export { getFinalKeyframe } from './animation/keyframes/get-final.mjs';
export { getValueTransition } from './animation/utils/get-value-transition.mjs';
export { resolveTransition } from './animation/utils/resolve-transition.mjs';
export { containsCSSVariable, isCSSVariableName, isCSSVariableToken } from './animation/utils/is-css-variable.mjs';
export { isTransitionDefined } from './animation/utils/is-transition-defined.mjs';
export { makeAnimationInstant } from './animation/utils/make-animation-instant.mjs';
export { animateMotionValue } from './animation/interfaces/motion-value.mjs';
export { animateVisualElement } from './animation/interfaces/visual-element.mjs';
export { animateTarget } from './animation/interfaces/visual-element-target.mjs';
export { animateVariant } from './animation/interfaces/visual-element-variant.mjs';
export { optimizedAppearDataAttribute, optimizedAppearDataId } from './animation/optimized-appear/data-id.mjs';
export { getOptimisedAppearId } from './animation/optimized-appear/get-appear-id.mjs';
export { inertia } from './animation/generators/inertia.mjs';
export { defaultEasing, keyframes } from './animation/generators/keyframes.mjs';
export { spring } from './animation/generators/spring.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, supportsWaapi } 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 { addEffect, animateEffectSubject, animateValues, findEffect, removeEffect } from './animation/animate/effects.mjs';
export { animateElement, handOffElementState } from './animation/animate/element.mjs';
export { addAttrValue, attrEffect } from './effects/attr/index.mjs';
export { MotionValueState } from './effects/MotionValueState.mjs';
export { propEffect } from './effects/prop/index.mjs';
export { addStyleValue, originProps, readStyleValue, styleEffect, styleSubjectEffect } from './effects/style/index.mjs';
export { addSVGValue, readSVGValue, svgEffect, svgSubjectEffect } from './effects/svg/index.mjs';
export { createEffect } from './effects/utils/create-effect.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 { isElementKeyboardAccessible, isElementTextInput } from './gestures/press/utils/is-keyboard-accessible.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 { isKeyframesTarget } from './render/utils/is-keyframes-target.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 { recordStats } from './stats/index.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, supportsViewTimeline } from './utils/supports/scroll-timeline.mjs';
export { transform } from './utils/transform.mjs';
export { MotionValue, collectMotionValues, motionValue } from './value/index.mjs';
export { attachFollow, followValue } from './value/follow-value.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, hasComplexValues } 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 { addValueToWillChange } from './value/will-change/add-will-change.mjs';
export { isWillChangeMotionValue } from './value/will-change/is.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 { DOMVisualElement } from './render/dom/DOMVisualElement.mjs';
export { Feature } from './render/Feature.mjs';
export { HTMLVisualElement } from './render/html/HTMLVisualElement.mjs';
export { ObjectVisualElement } from './render/object/ObjectVisualElement.mjs';
export { visualElementStore } from './render/store.mjs';
export { SVGVisualElement } from './render/svg/SVGVisualElement.mjs';
export { VisualElement, getFeatureDefinitions, setFeatureDefinitions } from './render/VisualElement.mjs';
export { checkVariantsDidChange, createAnimationState } from './render/utils/animation-state.mjs';
export { getVariantContext } from './render/utils/get-variant-context.mjs';
export { isAnimationControls } from './render/utils/is-animation-controls.mjs';
export { isControllingVariants, isVariantNode } from './render/utils/is-controlling-variants.mjs';
export { isForcedMotionValue } from './render/utils/is-forced-motion-value.mjs';
export { isVariantLabel } from './render/utils/is-variant-label.mjs';
export { updateMotionValuesFromProps } from './render/utils/motion-values.mjs';
export { resolveVariant } from './render/utils/resolve-dynamic-variants.mjs';
export { resolveVariantFromProps } from './render/utils/resolve-variants.mjs';
export { setTarget } from './render/utils/setters.mjs';
export { variantPriorityOrder, variantProps } from './render/utils/variant-props.mjs';
export { initPrefersReducedMotion } from './render/utils/reduced-motion/index.mjs';
export { convertBoundingBoxToBox, convertBoxToBoundingBox, transformBoxPoints } from './projection/geometry/conversion.mjs';
export { copyAxisDeltaInto, copyAxisInto, copyBoxInto } from './projection/geometry/copy.mjs';
export { applyAxisDelta, applyBoxDelta, applyPointDelta, applyTreeDeltas, scalePoint, transformAxis, transformBox, translateAxis } from './projection/geometry/delta-apply.mjs';
export { calcAxisDelta, calcBoxDelta, calcLength, calcRelativeAxis, calcRelativeAxisPosition, calcRelativeBox, calcRelativePosition, isNear } from './projection/geometry/delta-calc.mjs';
export { removeAxisDelta, removeAxisTransforms, removeBoxTransforms, removePointDelta } from './projection/geometry/delta-remove.mjs';
export { createAxis, createAxisDelta, createBox, createDelta } from './projection/geometry/models.mjs';
export { aspectRatio, axisDeltaEquals, axisEquals, axisEqualsRounded, boxEquals, boxEqualsRounded, isDeltaZero } from './projection/geometry/utils.mjs';
export { eachAxis } from './projection/utils/each-axis.mjs';
export { has2DTranslate, hasScale, hasTransform } from './projection/utils/has-transform.mjs';
export { measurePageBox, measureViewportBox } from './projection/utils/measure.mjs';
export { correctBorderRadius, pixelsToPercent } from './projection/styles/scale-border-radius.mjs';
export { correctBoxShadow } from './projection/styles/scale-box-shadow.mjs';
export { buildProjectionTransform } from './projection/styles/transform.mjs';
export { mixValues } from './projection/animation/mix-values.mjs';
export { animateSingleValue } from './animation/animate/single-value.mjs';
export { addDomEvent } from './events/add-dom-event.mjs';
export { compareByDepth } from './projection/utils/compare-by-depth.mjs';
export { FlatTree } from './projection/utils/flat-tree.mjs';
export { delay, delayInSeconds } from './utils/delay.mjs';
export { resolveMotionValue } from './value/utils/resolve-motion-value.mjs';
export { cleanDirtyNodes, createProjectionNode, propagateDirtyNodes } from './projection/node/create-projection-node.mjs';
export { DocumentProjectionNode } from './projection/node/DocumentProjectionNode.mjs';
export { nodeGroup } from './projection/node/group.mjs';
export { HTMLProjectionNode, rootProjectionNode } from './projection/node/HTMLProjectionNode.mjs';
export { globalProjectionState } from './projection/node/state.mjs';
export { NodeStack } from './projection/shared/stack.mjs';
export { camelToDash } from './render/dom/utils/camel-to-dash.mjs';
export { buildHTMLStyles } from './render/html/utils/build-styles.mjs';
export { buildTransform } from './render/html/utils/build-transform.mjs';
export { renderHTML } from './render/html/utils/render.mjs';
export { scrapeMotionValuesFromProps as scrapeHTMLMotionValuesFromProps } from './render/html/utils/scrape-motion-values.mjs';
export { buildSVGAttrs } from './render/svg/utils/build-attrs.mjs';
export { camelCaseAttributes } from './render/svg/utils/camel-case-attrs.mjs';
export { isSVGTag } from './render/svg/utils/is-svg-tag.mjs';
export { buildSVGPath } from './render/svg/utils/path.mjs';
export { renderSVG } from './render/svg/utils/render.mjs';
export { scrapeMotionValuesFromProps as scrapeSVGMotionValuesFromProps } from './render/svg/utils/scrape-motion-values.mjs';
export { LayoutAnimationBuilder, parseAnimateLayoutArgs } from './layout/LayoutAnimationBuilder.mjs';
export { animateLayout } from './layout/animate-layout.mjs';
export { cancelSync, sync } from './frameloop/index-legacy.mjs';
export { addScaleCorrector, scaleCorrectors } from './projection/styles/scale-correction.mjs';
export { cancelFrame, frame, frameSteps } from './frameloop/frame.mjs';
export { frameData } from './frameloop/frame-data.mjs';
export { hasReducedMotionListener, prefersReducedMotion } from './render/utils/reduced-motion/state.mjs';
//# sourceMappingURL=index.mjs.map
@@ -0,0 +1,363 @@
import { clamp } from 'motion-utils';
import { handOffElementState } from '../animation/animate/element.mjs';
import { GroupAnimation } from '../animation/GroupAnimation.mjs';
import { microtask } from '../frameloop/microtask.mjs';
import { time } from '../frameloop/sync-time.mjs';
import { HTMLProjectionNode } from '../projection/node/HTMLProjectionNode.mjs';
import { HTMLVisualElement } from '../render/html/HTMLVisualElement.mjs';
import { visualElementStore } from '../render/store.mjs';
import { hasTransform } from '../projection/utils/has-transform.mjs';
import { resolveElements } from '../utils/resolve-elements.mjs';
import { frameData } from '../frameloop/frame-data.mjs';
import { frameSteps } from '../frameloop/frame.mjs';
const layoutSelector = "[data-layout],[data-layout-id]";
/**
* All imperatively-created projection nodes live in one persistent tree,
* shared across animateLayout() calls (and with any React-created nodes,
* via the singleton document root). Keyed by element for reuse.
*/
const layoutNodes = new WeakMap();
/**
* Builders created within the same synchronous tick are flushed together
* as a single "commit": every node is snapshotted before any updateDom
* runs, mirroring React batching renders from different parts of the tree.
*/
let pendingBuilders;
function collectLayoutElements(scope) {
const elements = [];
if (scope instanceof HTMLElement && scope.matches(layoutSelector)) {
elements.push(scope);
}
scope.querySelectorAll(layoutSelector).forEach((element) => {
if (element instanceof HTMLElement)
elements.push(element);
});
return elements;
}
/**
* Process any work scheduled on the frameloop now. A previous animation
* may have been seeked while paused (controls.time = x) without a frame
* having rendered it - we must materialise that state into the DOM
* before taking snapshots.
*/
function flushPendingFrame() {
if (frameData.isProcessing)
return;
const now = time.now();
frameData.delta = clamp(0, 1000 / 60, now - frameData.timestamp);
frameData.timestamp = now;
frameData.isProcessing = true;
frameSteps.update.process(frameData);
frameSteps.preRender.process(frameData);
frameSteps.render.process(frameData);
frameData.isProcessing = false;
}
function getProjectionParent(element) {
let ancestor = element.parentElement;
while (ancestor) {
const node = layoutNodes.get(ancestor);
if (node && node.instance)
return node;
ancestor = ancestor.parentElement;
}
return undefined;
}
function createVisualElement() {
return new HTMLVisualElement({
props: {},
presenceContext: null,
visualState: {
latestValues: {},
renderState: {
transform: {},
transformOrigin: {},
style: {},
vars: {},
},
},
}, { allowProjection: true });
}
function readNodeOptions(element, transition) {
const layoutAttr = element.getAttribute("data-layout");
const layoutId = element.getAttribute("data-layout-id") ?? undefined;
return {
layoutId,
layout: layoutAttr !== null ? true : undefined,
animationType: (!layoutAttr || layoutAttr === "true"
? "both"
: layoutAttr),
transition,
};
}
function prepareNode(element, transition) {
let node = layoutNodes.get(element);
if (!node) {
let visualElement = visualElementStore.get(element);
if (!visualElement) {
visualElement = createVisualElement();
/**
* Values animate() has been rendering on this element move to
* the VisualElement, so the layout animation composes with
* them instead of overwriting them.
*/
handOffElementState(element, visualElement);
}
/**
* A first-time element may carry a projection transform in its
* inline style (e.g. it was cloned from an element mid-animation).
* That transform isn't tracked in latestValues so the engine can't
* reset it before measuring - clear it now so the first layout
* measurement isn't inflated.
*/
if (element.style.transform &&
!hasTransform(visualElement.latestValues)) {
element.style.transform = "";
}
node = new HTMLProjectionNode(visualElement.latestValues, getProjectionParent(element));
visualElement.projection = node;
node.setOptions({
...readNodeOptions(element, transition),
visualElement,
});
node.mount(element);
layoutNodes.set(element, node);
}
else {
node.setOptions(readNodeOptions(element, transition));
}
node.isPresent = true;
if (node.options.onExitComplete) {
node.setOptions({ onExitComplete: undefined });
}
return node;
}
function sortDocumentOrder(elements) {
return [...elements].sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
}
function dropNode(element, node) {
node.setOptions({ onExitComplete: undefined });
/**
* Stop any lingering animation so it can't leak into future updates.
* A follow node can share its currentAnimation with a surviving lead
* (via resumingFrom), in which case it isn't ours to stop.
*/
const stack = node.getStack();
if (!stack || node.isLead())
node.currentAnimation?.stop();
node.unmount();
layoutNodes.delete(element);
}
function flushPendingBuilders() {
const builders = pendingBuilders;
pendingBuilders = undefined;
flushPendingFrame();
/**
* Discover and mount every node across all builders before snapshotting
* any of them. Mounting during an active update flags isLayoutDirty,
* which would make that node's own willUpdate skip its snapshot.
* Document order guarantees ancestors mount before descendants, even
* when they're discovered by different builders.
*/
const targets = new Map();
for (const builder of builders) {
for (const element of builder.collectTargets()) {
const owners = targets.get(element);
owners ? owners.push(builder) : targets.set(element, [builder]);
}
}
const union = new Map();
for (const element of sortDocumentOrder(targets.keys())) {
const owners = targets.get(element);
const node = prepareNode(element, owners[owners.length - 1].transitionFor(element));
for (const owner of owners)
owner.adopt(element, node);
union.set(element, node);
}
union.forEach((node) => {
node.isLayoutDirty = false;
node.willUpdate();
});
const updatePromises = [];
for (const builder of builders) {
const result = builder.runUpdate();
if (result)
updatePromises.push(result);
}
const commit = () => {
/**
* Process all additions before any removals so that, even across
* builders, a removed member knows whether a replacement with the
* same layoutId was added in this commit.
*/
const newMemberIds = new Set();
for (const builder of builders) {
builder.reconcileAdditions(newMemberIds);
}
for (const builder of builders) {
builder.reconcileRemovals(newMemberIds);
}
let root;
union.forEach((node) => (root || (root = node.root)));
for (const builder of builders)
root || (root = builder.getRoot());
root?.didUpdate();
/**
* The root flushes the update on a microtask, synchronously
* processing the frame that creates the layout animations. Collect
* them in a later microtask step of the same pass.
*/
microtask.render(() => {
for (const builder of builders)
builder.finalize();
});
};
updatePromises.length ? Promise.all(updatePromises).then(commit) : commit();
}
class LayoutAnimationBuilder {
constructor(scope, updateDom, defaultOptions) {
this.scope = scope;
this.updateDom = updateDom;
this.defaultOptions = defaultOptions;
this.sharedTransitions = new Map();
this.notifyReady = () => { };
this.rejectReady = () => { };
this.tracked = new Map();
this.restorePoints = new Map();
this.readyPromise = new Promise((resolve, reject) => {
this.notifyReady = resolve;
this.rejectReady = reject;
});
if (!pendingBuilders) {
pendingBuilders = [];
queueMicrotask(flushPendingBuilders);
}
pendingBuilders.push(this);
}
shared(id, transition) {
this.sharedTransitions.set(id, transition);
return this;
}
then(resolve, reject) {
return this.readyPromise.then(resolve, reject);
}
transitionFor(element) {
const layoutId = element.getAttribute("data-layout-id");
return ((layoutId && this.sharedTransitions.get(layoutId)) ||
this.defaultOptions);
}
adopt(element, node) {
this.tracked.set(element, node);
this.restorePoints.set(element, {
parent: element.parentElement,
next: element.nextSibling,
});
}
collectTargets() {
return collectLayoutElements(this.scope);
}
runUpdate() {
try {
const result = this.updateDom();
if (result && typeof result.then === "function") {
return result.then(undefined, (error) => {
this.updateError = error;
});
}
}
catch (error) {
this.updateError = error;
}
return undefined;
}
reconcileAdditions(newMemberIds) {
for (const element of collectLayoutElements(this.scope)) {
if (this.tracked.has(element))
continue;
const node = prepareNode(element, this.transitionFor(element));
this.adopt(element, node);
node.options.layoutId && newMemberIds.add(node.options.layoutId);
}
}
reconcileRemovals(newMemberIds) {
this.tracked.forEach((node, element) => {
if (element.isConnected)
return;
const restore = this.restorePoints.get(element);
this.restorePoints.delete(element);
const { layoutId } = node.options;
const stack = node.getStack();
const hasSurvivor = stack &&
stack.members.some((member) => member !== node &&
member.instance
?.isConnected);
/**
* A removed lead with a surviving stack member - and no
* replacement member added this commit - runs an exit
* crossfade: restore the element to its old position in the
* DOM, relegate it and let the survivor take over. It's
* removed again once the animation completes.
*/
if (layoutId &&
node.isLead() &&
hasSurvivor &&
!newMemberIds.has(layoutId)) {
if (restore && restore.parent.isConnected) {
restore.parent.insertBefore(element, restore.next && restore.next.parentNode === restore.parent
? restore.next
: null);
node.isPresent = false;
node.setOptions({
onExitComplete: () => {
element.remove();
dropNode(element, node);
},
});
if (node.relegate())
return;
element.remove();
}
}
dropNode(element, node);
this.tracked.delete(element);
});
}
getRoot() {
let root;
this.tracked.forEach((node) => (root || (root = node.root)));
return root;
}
finalize() {
if (this.updateError) {
this.rejectReady(this.updateError);
return;
}
const animations = new Set();
this.tracked.forEach((node) => {
if (node.instance && node.currentAnimation) {
animations.add(node.currentAnimation);
}
});
this.notifyReady(new GroupAnimation([...animations]));
}
}
function parseAnimateLayoutArgs(scopeOrUpdateDom, updateDomOrOptions, options) {
if (typeof scopeOrUpdateDom === "function") {
return {
scope: document,
updateDom: scopeOrUpdateDom,
defaultOptions: updateDomOrOptions,
};
}
const scope = scopeOrUpdateDom instanceof Document
? scopeOrUpdateDom
: resolveElements(scopeOrUpdateDom)[0] ?? document;
return {
scope,
updateDom: updateDomOrOptions,
defaultOptions: options,
};
}
export { LayoutAnimationBuilder, parseAnimateLayoutArgs };
//# sourceMappingURL=LayoutAnimationBuilder.mjs.map
@@ -0,0 +1,19 @@
import { parseAnimateLayoutArgs, LayoutAnimationBuilder } from './LayoutAnimationBuilder.mjs';
/**
* Animate layout changes made within `updateDom` on elements tagged
* with `data-layout` or `data-layout-id`.
*
* ```
* animateLayout(() => {
* element.style.justifyContent = "flex-end"
* })
* ```
*/
function animateLayout(scopeOrUpdateDom, updateDomOrOptions, options) {
const { scope, updateDom, defaultOptions } = parseAnimateLayoutArgs(scopeOrUpdateDom, updateDomOrOptions, options);
return new LayoutAnimationBuilder(scope, updateDom, defaultOptions);
}
export { animateLayout };
//# sourceMappingURL=animate-layout.mjs.map
@@ -0,0 +1,80 @@
import { mixNumber } from '../../utils/mix/number.mjs';
import { percent, px } from '../../value/types/numbers/units.mjs';
import { progress, circOut, noop } from 'motion-utils';
import { cornerRadiusProps } from '../../utils/border-radius.mjs';
import { asNumber } from '../../value/utils/as-number.mjs';
const numBorders = cornerRadiusProps.length;
const mixedTransforms = [
"rotate",
"rotateX",
"rotateY",
"rotateZ",
"skewX",
"skewY",
];
const numMixedTransforms = mixedTransforms.length;
const isPx = (value) => typeof value === "number" || px.test(value);
function mixValues(target, follow, lead, progress, shouldCrossfadeOpacity, isOnlyMember) {
if (shouldCrossfadeOpacity) {
target.opacity = mixNumber(0, lead.opacity ?? 1, easeCrossfadeIn(progress));
target.opacityExit = mixNumber(follow.opacity ?? 1, 0, easeCrossfadeOut(progress));
}
else if (isOnlyMember) {
target.opacity = mixNumber(follow.opacity ?? 1, lead.opacity ?? 1, progress);
}
/**
* Mix border radius
*/
for (let i = 0; i < numBorders; i++) {
const borderLabel = cornerRadiusProps[i];
let followRadius = getRadius(follow, borderLabel);
let leadRadius = getRadius(lead, borderLabel);
if (followRadius === undefined && leadRadius === undefined)
continue;
followRadius || (followRadius = 0);
leadRadius || (leadRadius = 0);
const canMix = followRadius === 0 ||
leadRadius === 0 ||
isPx(followRadius) === isPx(leadRadius);
if (canMix) {
target[borderLabel] = Math.max(mixNumber(asNumber(followRadius), asNumber(leadRadius), progress), 0);
if (percent.test(leadRadius) || percent.test(followRadius)) {
target[borderLabel] += "%";
}
}
else {
target[borderLabel] = leadRadius;
}
}
/**
* Mix rotation and skew. These can't be projected, so mix them every
* frame rather than leaving them frozen at the animation-start snapshot.
*/
for (let i = 0; i < numMixedTransforms; i++) {
const key = mixedTransforms[i];
if (follow[key] || lead[key]) {
target[key] = mixNumber(follow[key] || 0, lead[key] || 0, progress);
}
}
}
function getRadius(values, radiusName) {
return values[radiusName] !== undefined
? values[radiusName]
: values.borderRadius;
}
const easeCrossfadeIn = /*@__PURE__*/ compress(0, 0.5, circOut);
const easeCrossfadeOut = /*@__PURE__*/ compress(0.5, 0.95, noop);
function compress(min, max, easing) {
return (p) => {
// Could replace ifs with clamp
if (p < min)
return 0;
if (p > max)
return 1;
return easing(progress(min, max, p));
};
}
export { mixValues };
//# sourceMappingURL=mix-values.mjs.map
@@ -0,0 +1,34 @@
/**
* Bounding boxes tend to be defined as top, left, right, bottom. For various operations
* it's easier to consider each axis individually. This function returns a bounding box
* as a map of single-axis min/max values.
*/
function convertBoundingBoxToBox({ top, left, right, bottom, }) {
return {
x: { min: left, max: right },
y: { min: top, max: bottom },
};
}
function convertBoxToBoundingBox({ x, y }) {
return { top: y.min, right: x.max, bottom: y.max, left: x.min };
}
/**
* Applies a TransformPoint function to a bounding box. TransformPoint is usually a function
* provided by Framer to allow measured points to be corrected for device scaling. This is used
* when measuring DOM elements and DOM event points.
*/
function transformBoxPoints(point, transformPoint) {
if (!transformPoint)
return point;
const topLeft = transformPoint({ x: point.left, y: point.top });
const bottomRight = transformPoint({ x: point.right, y: point.bottom });
return {
top: topLeft.y,
left: topLeft.x,
bottom: bottomRight.y,
right: bottomRight.x,
};
}
export { convertBoundingBoxToBox, convertBoxToBoundingBox, transformBoxPoints };
//# sourceMappingURL=conversion.mjs.map
@@ -0,0 +1,32 @@
/**
* Reset an axis to the provided origin box.
*
* This is a mutative operation.
*/
function copyAxisInto(axis, originAxis) {
axis.min = originAxis.min;
axis.max = originAxis.max;
}
/**
* Reset a box to the provided origin box.
*
* This is a mutative operation.
*/
function copyBoxInto(box, originBox) {
copyAxisInto(box.x, originBox.x);
copyAxisInto(box.y, originBox.y);
}
/**
* Reset a delta to the provided origin box.
*
* This is a mutative operation.
*/
function copyAxisDeltaInto(delta, originDelta) {
delta.translate = originDelta.translate;
delta.scale = originDelta.scale;
delta.originPoint = originDelta.originPoint;
delta.origin = originDelta.origin;
}
export { copyAxisDeltaInto, copyAxisInto, copyBoxInto };
//# sourceMappingURL=copy.mjs.map

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