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:
co-authored by
Claude Haiku 5.5
parent
ee8258556d
commit
10e453e4be
Generated
Vendored
+206
@@ -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
|
||||
Generated
Vendored
+154
@@ -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
|
||||
Generated
Vendored
+82
@@ -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
|
||||
Generated
Vendored
+10
@@ -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
|
||||
Generated
Vendored
+418
@@ -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
|
||||
Generated
Vendored
+178
@@ -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
|
||||
Generated
Vendored
+115
@@ -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
|
||||
Generated
Vendored
+22
@@ -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
|
||||
Generated
Vendored
+113
@@ -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
|
||||
Generated
Vendored
+80
@@ -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
|
||||
Generated
Vendored
+12
@@ -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
|
||||
Generated
Vendored
+19
@@ -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
|
||||
Generated
Vendored
+90
@@ -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
|
||||
Generated
Vendored
+74
@@ -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
|
||||
Generated
Vendored
+399
@@ -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
|
||||
apps/web/.output/server/node_modules/motion-dom/dist/es/animation/generators/utils/calc-duration.mjs
Generated
Vendored
+19
@@ -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
|
||||
Generated
Vendored
+33
@@ -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
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
function isGenerator(type) {
|
||||
return typeof type === "function" && "applyToOptions" in type;
|
||||
}
|
||||
|
||||
export { isGenerator };
|
||||
//# sourceMappingURL=is-generator.mjs.map
|
||||
Generated
Vendored
+22
@@ -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
|
||||
Generated
Vendored
+106
@@ -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
|
||||
Generated
Vendored
+109
@@ -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
|
||||
Generated
Vendored
+40
@@ -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
|
||||
Generated
Vendored
+18
@@ -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
|
||||
apps/web/.output/server/node_modules/motion-dom/dist/es/animation/keyframes/DOMKeyframesResolver.mjs
Generated
Vendored
+172
@@ -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
|
||||
Generated
Vendored
+179
@@ -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
|
||||
Generated
Vendored
+12
@@ -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
|
||||
Generated
Vendored
+10
@@ -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
|
||||
Generated
Vendored
+13
@@ -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
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
function convertOffsetToTimes(offset, duration) {
|
||||
return offset.map((o) => o * duration);
|
||||
}
|
||||
|
||||
export { convertOffsetToTimes };
|
||||
//# sourceMappingURL=time.mjs.map
|
||||
Generated
Vendored
+12
@@ -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
|
||||
apps/web/.output/server/node_modules/motion-dom/dist/es/animation/keyframes/utils/fill-wildcards.mjs
Generated
Vendored
+8
@@ -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
|
||||
Generated
Vendored
+16
@@ -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
|
||||
Generated
Vendored
+37
@@ -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
|
||||
Generated
Vendored
+73
@@ -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
|
||||
Generated
Vendored
+7
@@ -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
|
||||
apps/web/.output/server/node_modules/motion-dom/dist/es/animation/optimized-appear/get-appear-id.mjs
Generated
Vendored
+8
@@ -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
|
||||
Generated
Vendored
+34
@@ -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
|
||||
Generated
Vendored
+13
@@ -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
|
||||
+189
@@ -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
|
||||
Generated
Vendored
+16
@@ -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
|
||||
Generated
Vendored
+46
@@ -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
|
||||
apps/web/.output/server/node_modules/motion-dom/dist/es/animation/utils/css-variables-conversion.mjs
Generated
Vendored
+42
@@ -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
|
||||
Generated
Vendored
+41
@@ -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
|
||||
Generated
Vendored
+14
@@ -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
|
||||
Generated
Vendored
+31
@@ -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
|
||||
Generated
Vendored
+27
@@ -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
|
||||
Generated
Vendored
+27
@@ -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
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
function makeAnimationInstant(options) {
|
||||
options.duration = 0;
|
||||
options.type = "keyframes";
|
||||
}
|
||||
|
||||
export { makeAnimationInstant };
|
||||
//# sourceMappingURL=make-animation-instant.mjs.map
|
||||
Generated
Vendored
+56
@@ -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
|
||||
Generated
Vendored
+19
@@ -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
|
||||
Generated
Vendored
+17
@@ -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
|
||||
Generated
Vendored
+15
@@ -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
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
const cubicBezierAsString = ([a, b, c, d]) => `cubic-bezier(${a}, ${b}, ${c}, ${d})`;
|
||||
|
||||
export { cubicBezierAsString };
|
||||
//# sourceMappingURL=cubic-bezier.mjs.map
|
||||
Generated
Vendored
+15
@@ -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
|
||||
Generated
Vendored
+29
@@ -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
|
||||
Generated
Vendored
+16
@@ -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
|
||||
Generated
Vendored
+29
@@ -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
|
||||
Generated
Vendored
+14
@@ -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
|
||||
Generated
Vendored
+62
@@ -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
|
||||
Generated
Vendored
+902
@@ -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
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
const transformGroups = new WeakMap();
|
||||
|
||||
export { transformGroups };
|
||||
//# sourceMappingURL=groups.mjs.map
|
||||
apps/web/.output/server/node_modules/motion-dom/dist/es/animation/waapi/utils/accelerated-values.mjs
Generated
Vendored
+13
@@ -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
|
||||
Generated
Vendored
+16
@@ -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
|
||||
Generated
Vendored
+13
@@ -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
|
||||
Generated
Vendored
+13
@@ -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
|
||||
Generated
Vendored
+59
@@ -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
|
||||
apps/web/.output/server/node_modules/motion-dom/dist/es/animation/waapi/utils/unsupported-easing.mjs
Generated
Vendored
+19
@@ -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
|
||||
Generated
Vendored
+112
@@ -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
|
||||
+48
@@ -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
|
||||
+23
@@ -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
|
||||
+99
@@ -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
|
||||
Generated
Vendored
+46
@@ -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
|
||||
+91
@@ -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
|
||||
Generated
Vendored
+27
@@ -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
|
||||
Generated
Vendored
+27
@@ -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
|
||||
+7
@@ -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
|
||||
+72
@@ -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
|
||||
+12
@@ -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
|
||||
+8
@@ -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
|
||||
Generated
Vendored
+21
@@ -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
|
||||
+7
@@ -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
|
||||
+13
@@ -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
|
||||
Generated
Vendored
+87
@@ -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
|
||||
+32
@@ -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
|
||||
Generated
Vendored
+10
@@ -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
|
||||
Generated
Vendored
+29
@@ -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
|
||||
+70
@@ -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
|
||||
+97
@@ -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
|
||||
Generated
Vendored
+34
@@ -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
|
||||
Generated
Vendored
+39
@@ -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
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
const isPressing = new WeakSet();
|
||||
|
||||
export { isPressing };
|
||||
//# sourceMappingURL=state.mjs.map
|
||||
Generated
Vendored
+21
@@ -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
|
||||
Generated
Vendored
+19
@@ -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
|
||||
+16
@@ -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
@@ -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
|
||||
Generated
Vendored
+363
@@ -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
|
||||
Generated
Vendored
+19
@@ -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
|
||||
Generated
Vendored
+80
@@ -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
|
||||
Generated
Vendored
+34
@@ -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
|
||||
Generated
Vendored
+32
@@ -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
Reference in New Issue
Block a user