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

21
frontend/node_modules/framer-motion/LICENSE.md generated vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Framer B.V.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

105
frontend/node_modules/framer-motion/README.md generated vendored Normal file
View File

@@ -0,0 +1,105 @@
<p align="center">
<img src="https://framerusercontent.com/images/48ha9ZR9oZQGQ6gZ8YUfElP3T0A.png" width="50" height="50" alt="Framer Motion Icon" />
</p>
<h1 align="center">Framer Motion</h1>
<h3 align="center">
An open source motion library for React, made by Framer.
</h3>
<h3 align="center">
Motion powers Framer, the web builder for creative pros. Design and ship your dream site. Zero code, maximum speed.
</h3>
<br/>
<p align="center">
<a href="https://www.framer.com?utm_source=motion-readme">
<img src="https://framerusercontent.com/images/atXqxn4JhKm4LXVncdNjkKV7yCU.png" width="140" alt="Start for free" />
</a>
</p>
<br/>
<p align="center">
<a href="https://www.framer.com?utm_source=motion-readme">
<img src="https://framerusercontent.com/images/pMSOmGP2V8sSaZRV2D7i4HTBTe4.png" width="1000" alt="Framer Banner" />
</a>
</p>
<br>
<p align="center">
<a href="https://www.npmjs.com/package/framer-motion" target="_blank">
<img src="https://img.shields.io/npm/v/framer-motion.svg?style=flat-square" />
</a>
<a href="https://www.npmjs.com/package/framer-motion" target="_blank">
<img src="https://img.shields.io/npm/dm/framer-motion.svg?style=flat-square" />
</a>
<a href="https://twitter.com/framer" target="_blank">
<img src="https://img.shields.io/twitter/follow/framer.svg?style=social&label=Follow" />
</a>
<a href="https://discord.gg/DfkSpYe" target="_blank">
<img src="https://img.shields.io/discord/308323056592486420.svg?logo=discord&logoColor=white" alt="Chat on Discord">
</a>
</p>
<br>
<hr>
<br>
Framer Motion is an open source, production-ready library thats designed for all creative developers.
It looks like this:
```jsx
<motion.div animate={{ x: 0 }} />
```
It does all this:
- [Springs](https://www.framer.com/docs/transition/#spring?utm_source=motion-readme-docs)
- [Keyframes](https://www.framer.com/docs/animation/##keyframes?utm_source=motion-readme-docs)
- [Layout animations](https://www.framer.com/docs/layout-animations/?utm_source=motion-readme-docs)
- [Shared layout animations](https://www.framer.com/docs/layout-animations/#shared-layout-animations?utm_source=motion-readme-docs)
- [Gestures (drag/tap/hover)](https://www.framer.com/docs/gestures/?utm_source=motion-readme-docs)
- [Scroll animations](https://www.framer.com/docs/scroll-animations?utm_source=motion-readme-docs)
- [SVG paths](https://www.framer.com/docs/component/###svg-line-drawing?utm_source=motion-readme-docs)
- [Exit animations](https://www.framer.com/docs/animate-presence/?utm_source=motion-readme-docs)
- Server-side rendering
- [Hardware-accelerated animations](https://www.framer.com/docs/animation/#hardware-accelerated-animations?utm_source=motion-readme-docs)
- [Orchestrate animations across components](https://www.framer.com/docs/animation/##orchestration?utm_source=motion-readme-docs)
- [CSS variables](https://www.framer.com/docs/component/##css-variables?utm_source=motion-readme-docs)
...and a whole lot more.
## Get started
### 🐇 Quick start
Install `framer-motion` with via your package manager:
```
npm install framer-motion
```
Then import the `motion` component:
```jsx
import { motion } from "framer-motion"
export const MyComponent = ({ isVisible }) => (
<motion.div animate={{ opacity: isVisible ? 1 : 0 }} />
)
```
### 📚 Docs
- Check out [our documentation](https://www.framer.com/docs/?utm_source=motion-readme-docs) for guides and a full API reference.
- Or see [our examples](https://www.framer.com/docs/examples/?utm_source=motion-readme-docs) for inspiration.
### 💎 Contribute
- Want to contribute to Framer Motion? Our [contributing guide](https://github.com/framer/motion/blob/master/CONTRIBUTING.md) has you covered.
### 👩🏻‍⚖️ License
- Framer Motion is MIT licensed.
### ✨ Framer
- Design and publish sites that inspire. [Try Framer for free](https://www.framer.com/?utm_source=motion-readme).

View File

@@ -0,0 +1,53 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var indexLegacy = require('./index-legacy-87714a68.js');
exports.MotionValue = indexLegacy.MotionValue;
exports.animate = indexLegacy.animate;
exports.anticipate = indexLegacy.anticipate;
exports.backIn = indexLegacy.backIn;
exports.backInOut = indexLegacy.backInOut;
exports.backOut = indexLegacy.backOut;
exports.cancelFrame = indexLegacy.cancelFrame;
exports.cancelSync = indexLegacy.cancelSync;
exports.circIn = indexLegacy.circIn;
exports.circInOut = indexLegacy.circInOut;
exports.circOut = indexLegacy.circOut;
exports.clamp = indexLegacy.clamp;
exports.createScopedAnimate = indexLegacy.createScopedAnimate;
exports.cubicBezier = indexLegacy.cubicBezier;
exports.delay = indexLegacy.delay;
exports.distance = indexLegacy.distance;
exports.distance2D = indexLegacy.distance2D;
exports.easeIn = indexLegacy.easeIn;
exports.easeInOut = indexLegacy.easeInOut;
exports.easeOut = indexLegacy.easeOut;
exports.frame = indexLegacy.frame;
exports.frameData = indexLegacy.frameData;
exports.inView = indexLegacy.inView;
exports.interpolate = indexLegacy.interpolate;
Object.defineProperty(exports, 'invariant', {
enumerable: true,
get: function () { return indexLegacy.invariant; }
});
exports.mirrorEasing = indexLegacy.mirrorEasing;
exports.mix = indexLegacy.mix;
exports.motionValue = indexLegacy.motionValue;
exports.pipe = indexLegacy.pipe;
exports.progress = indexLegacy.progress;
exports.reverseEasing = indexLegacy.reverseEasing;
exports.scroll = indexLegacy.scroll;
exports.scrollInfo = indexLegacy.scrollInfo;
exports.stagger = indexLegacy.stagger;
exports.steps = indexLegacy.steps;
exports.sync = indexLegacy.sync;
exports.transform = indexLegacy.transform;
Object.defineProperty(exports, 'warning', {
enumerable: true,
get: function () { return indexLegacy.warning; }
});
exports.wrap = indexLegacy.wrap;

File diff suppressed because it is too large Load Diff

6313
frontend/node_modules/framer-motion/dist/cjs/index.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

929
frontend/node_modules/framer-motion/dist/dom-entry.d.ts generated vendored Normal file
View File

@@ -0,0 +1,929 @@
declare type EasingFunction = (v: number) => number;
declare type EasingModifier = (easing: EasingFunction) => EasingFunction;
declare type BezierDefinition = [number, number, number, number];
declare type EasingDefinition = BezierDefinition | "linear" | "easeIn" | "easeOut" | "easeInOut" | "circIn" | "circOut" | "circInOut" | "backIn" | "backOut" | "backInOut" | "anticipate";
/**
* The easing function to use. Set as one of:
*
* - The name of an in-built easing function.
* - An array of four numbers to define a cubic bezier curve.
* - An easing function, that accepts and returns a progress value between `0` and `1`.
*
* @public
*/
declare type Easing = EasingDefinition | EasingFunction;
interface Point {
x: number;
y: number;
}
declare type Process = (data: FrameData) => void;
declare type Schedule = (process: Process, keepAlive?: boolean, immediate?: boolean) => Process;
interface Step {
schedule: Schedule;
cancel: (process: Process) => void;
process: (data: FrameData) => void;
}
declare type StepId = "prepare" | "read" | "update" | "preRender" | "render" | "postRender";
declare type Batcher = {
[key in StepId]: Schedule;
};
declare type Steps = {
[key in StepId]: Step;
};
interface FrameData {
delta: number;
timestamp: number;
isProcessing: boolean;
}
/**
* @public
*/
interface SVGPathProperties {
pathLength?: number;
pathOffset?: number;
pathSpacing?: number;
}
declare type GenericKeyframesTarget<V> = [null, ...V[]] | V[];
/**
* An update function. It accepts a timestamp used to advance the animation.
*/
declare type Update = (timestamp: number) => void;
/**
* Drivers accept a update function and call it at an interval. This interval
* could be a synchronous loop, a setInterval, or tied to the device's framerate.
*/
interface DriverControls {
start: () => void;
stop: () => void;
now: () => number;
}
declare type Driver = (update: Update) => DriverControls;
interface SVGAttributes {
accentHeight?: number | string | undefined;
accumulate?: "none" | "sum" | undefined;
additive?: "replace" | "sum" | undefined;
alignmentBaseline?: "auto" | "baseline" | "before-edge" | "text-before-edge" | "middle" | "central" | "after-edge" | "text-after-edge" | "ideographic" | "alphabetic" | "hanging" | "mathematical" | "inherit" | undefined;
allowReorder?: "no" | "yes" | undefined;
alphabetic?: number | string | undefined;
amplitude?: number | string | undefined;
arabicForm?: "initial" | "medial" | "terminal" | "isolated" | undefined;
ascent?: number | string | undefined;
attributeName?: string | undefined;
attributeType?: string | undefined;
autoReverse?: boolean | undefined;
azimuth?: number | string | undefined;
baseFrequency?: number | string | undefined;
baselineShift?: number | string | undefined;
baseProfile?: number | string | undefined;
bbox?: number | string | undefined;
begin?: number | string | undefined;
bias?: number | string | undefined;
by?: number | string | undefined;
calcMode?: number | string | undefined;
capHeight?: number | string | undefined;
clip?: number | string | undefined;
clipPath?: string | undefined;
clipPathUnits?: number | string | undefined;
clipRule?: number | string | undefined;
colorInterpolation?: number | string | undefined;
colorInterpolationFilters?: "auto" | "sRGB" | "linearRGB" | "inherit" | undefined;
colorProfile?: number | string | undefined;
colorRendering?: number | string | undefined;
contentScriptType?: number | string | undefined;
contentStyleType?: number | string | undefined;
cursor?: number | string | undefined;
cx?: number | string | undefined;
cy?: number | string | undefined;
d?: string | undefined;
decelerate?: number | string | undefined;
descent?: number | string | undefined;
diffuseConstant?: number | string | undefined;
direction?: number | string | undefined;
display?: number | string | undefined;
divisor?: number | string | undefined;
dominantBaseline?: number | string | undefined;
dur?: number | string | undefined;
dx?: number | string | undefined;
dy?: number | string | undefined;
edgeMode?: number | string | undefined;
elevation?: number | string | undefined;
enableBackground?: number | string | undefined;
end?: number | string | undefined;
exponent?: number | string | undefined;
externalResourcesRequired?: boolean | undefined;
fill?: string | undefined;
fillOpacity?: number | string | undefined;
fillRule?: "nonzero" | "evenodd" | "inherit" | undefined;
filter?: string | undefined;
filterRes?: number | string | undefined;
filterUnits?: number | string | undefined;
floodColor?: number | string | undefined;
floodOpacity?: number | string | undefined;
focusable?: boolean | "auto" | undefined;
fontFamily?: string | undefined;
fontSize?: number | string | undefined;
fontSizeAdjust?: number | string | undefined;
fontStretch?: number | string | undefined;
fontStyle?: number | string | undefined;
fontVariant?: number | string | undefined;
fontWeight?: number | string | undefined;
format?: number | string | undefined;
fr?: number | string | undefined;
from?: number | string | undefined;
fx?: number | string | undefined;
fy?: number | string | undefined;
g1?: number | string | undefined;
g2?: number | string | undefined;
glyphName?: number | string | undefined;
glyphOrientationHorizontal?: number | string | undefined;
glyphOrientationVertical?: number | string | undefined;
glyphRef?: number | string | undefined;
gradientTransform?: string | undefined;
gradientUnits?: string | undefined;
hanging?: number | string | undefined;
horizAdvX?: number | string | undefined;
horizOriginX?: number | string | undefined;
href?: string | undefined;
ideographic?: number | string | undefined;
imageRendering?: number | string | undefined;
in2?: number | string | undefined;
in?: string | undefined;
intercept?: number | string | undefined;
k1?: number | string | undefined;
k2?: number | string | undefined;
k3?: number | string | undefined;
k4?: number | string | undefined;
k?: number | string | undefined;
kernelMatrix?: number | string | undefined;
kernelUnitLength?: number | string | undefined;
kerning?: number | string | undefined;
keyPoints?: number | string | undefined;
keySplines?: number | string | undefined;
keyTimes?: number | string | undefined;
lengthAdjust?: number | string | undefined;
letterSpacing?: number | string | undefined;
lightingColor?: number | string | undefined;
limitingConeAngle?: number | string | undefined;
local?: number | string | undefined;
markerEnd?: string | undefined;
markerHeight?: number | string | undefined;
markerMid?: string | undefined;
markerStart?: string | undefined;
markerUnits?: number | string | undefined;
markerWidth?: number | string | undefined;
mask?: string | undefined;
maskContentUnits?: number | string | undefined;
maskUnits?: number | string | undefined;
mathematical?: number | string | undefined;
mode?: number | string | undefined;
numOctaves?: number | string | undefined;
offset?: number | string | undefined;
opacity?: number | string | undefined;
operator?: number | string | undefined;
order?: number | string | undefined;
orient?: number | string | undefined;
orientation?: number | string | undefined;
origin?: number | string | undefined;
overflow?: number | string | undefined;
overlinePosition?: number | string | undefined;
overlineThickness?: number | string | undefined;
paintOrder?: number | string | undefined;
panose1?: number | string | undefined;
path?: string | undefined;
pathLength?: number | string | undefined;
patternContentUnits?: string | undefined;
patternTransform?: number | string | undefined;
patternUnits?: string | undefined;
pointerEvents?: number | string | undefined;
points?: string | undefined;
pointsAtX?: number | string | undefined;
pointsAtY?: number | string | undefined;
pointsAtZ?: number | string | undefined;
preserveAlpha?: boolean | undefined;
preserveAspectRatio?: string | undefined;
primitiveUnits?: number | string | undefined;
r?: number | string | undefined;
radius?: number | string | undefined;
refX?: number | string | undefined;
refY?: number | string | undefined;
renderingIntent?: number | string | undefined;
repeatCount?: number | string | undefined;
repeatDur?: number | string | undefined;
requiredExtensions?: number | string | undefined;
requiredFeatures?: number | string | undefined;
restart?: number | string | undefined;
result?: string | undefined;
rotate?: number | string | undefined;
rx?: number | string | undefined;
ry?: number | string | undefined;
scale?: number | string | undefined;
seed?: number | string | undefined;
shapeRendering?: number | string | undefined;
slope?: number | string | undefined;
spacing?: number | string | undefined;
specularConstant?: number | string | undefined;
specularExponent?: number | string | undefined;
speed?: number | string | undefined;
spreadMethod?: string | undefined;
startOffset?: number | string | undefined;
stdDeviation?: number | string | undefined;
stemh?: number | string | undefined;
stemv?: number | string | undefined;
stitchTiles?: number | string | undefined;
stopColor?: string | undefined;
stopOpacity?: number | string | undefined;
strikethroughPosition?: number | string | undefined;
strikethroughThickness?: number | string | undefined;
string?: number | string | undefined;
stroke?: string | undefined;
strokeDasharray?: string | number | undefined;
strokeDashoffset?: string | number | undefined;
strokeLinecap?: "butt" | "round" | "square" | "inherit" | undefined;
strokeLinejoin?: "miter" | "round" | "bevel" | "inherit" | undefined;
strokeMiterlimit?: number | string | undefined;
strokeOpacity?: number | string | undefined;
strokeWidth?: number | string | undefined;
surfaceScale?: number | string | undefined;
systemLanguage?: number | string | undefined;
tableValues?: number | string | undefined;
targetX?: number | string | undefined;
targetY?: number | string | undefined;
textAnchor?: string | undefined;
textDecoration?: number | string | undefined;
textLength?: number | string | undefined;
textRendering?: number | string | undefined;
to?: number | string | undefined;
transform?: string | undefined;
u1?: number | string | undefined;
u2?: number | string | undefined;
underlinePosition?: number | string | undefined;
underlineThickness?: number | string | undefined;
unicode?: number | string | undefined;
unicodeBidi?: number | string | undefined;
unicodeRange?: number | string | undefined;
unitsPerEm?: number | string | undefined;
vAlphabetic?: number | string | undefined;
values?: string | undefined;
vectorEffect?: number | string | undefined;
version?: string | undefined;
vertAdvY?: number | string | undefined;
vertOriginX?: number | string | undefined;
vertOriginY?: number | string | undefined;
vHanging?: number | string | undefined;
vIdeographic?: number | string | undefined;
viewBox?: string | undefined;
viewTarget?: number | string | undefined;
visibility?: number | string | undefined;
vMathematical?: number | string | undefined;
widths?: number | string | undefined;
wordSpacing?: number | string | undefined;
writingMode?: number | string | undefined;
x1?: number | string | undefined;
x2?: number | string | undefined;
x?: number | string | undefined;
xChannelSelector?: string | undefined;
xHeight?: number | string | undefined;
xlinkActuate?: string | undefined;
xlinkArcrole?: string | undefined;
xlinkHref?: string | undefined;
xlinkRole?: string | undefined;
xlinkShow?: string | undefined;
xlinkTitle?: string | undefined;
xlinkType?: string | undefined;
xmlBase?: string | undefined;
xmlLang?: string | undefined;
xmlns?: string | undefined;
xmlnsXlink?: string | undefined;
xmlSpace?: string | undefined;
y1?: number | string | undefined;
y2?: number | string | undefined;
y?: number | string | undefined;
yChannelSelector?: string | undefined;
z?: number | string | undefined;
zoomAndPan?: string | undefined;
}
interface ProgressTimeline {
currentTime: null | {
value: number;
};
cancel?: VoidFunction;
}
interface AnimationPlaybackLifecycles<V> {
onUpdate?: (latest: V) => void;
onPlay?: () => void;
onComplete?: () => void;
onRepeat?: () => void;
onStop?: () => void;
}
interface Transition extends AnimationPlaybackOptions, Omit<SpringOptions, "keyframes">, Omit<InertiaOptions, "keyframes">, KeyframeOptions {
delay?: number;
elapsed?: number;
driver?: Driver;
type?: "decay" | "spring" | "keyframes" | "tween" | "inertia";
duration?: number;
autoplay?: boolean;
}
interface ValueAnimationTransition<V = any> extends Transition, AnimationPlaybackLifecycles<V> {
isHandoff?: boolean;
}
interface AnimationScope<T = any> {
readonly current: T;
animations: AnimationPlaybackControls[];
}
declare type StyleTransitions = {
[K in keyof CSSStyleDeclarationWithTransform]?: Transition;
};
declare type SVGPathTransitions = {
[K in keyof SVGPathProperties]: Transition;
};
declare type SVGTransitions = {
[K in keyof SVGAttributes]: Transition;
};
declare type VariableTransitions = {
[key: `--${string}`]: Transition;
};
declare type AnimationOptionsWithValueOverrides<V = any> = StyleTransitions & SVGPathTransitions & SVGTransitions & VariableTransitions & ValueAnimationTransition<V>;
interface DynamicAnimationOptions extends Omit<AnimationOptionsWithValueOverrides, "delay"> {
delay?: number | DynamicOption<number>;
}
declare type ElementOrSelector = Element | Element[] | NodeListOf<Element> | string;
/**
* @public
*/
interface AnimationPlaybackControls {
time: number;
speed: number;
state?: AnimationPlayState;
duration: number;
stop: () => void;
play: () => void;
pause: () => void;
complete: () => void;
cancel: () => void;
then: (onResolve: VoidFunction, onReject?: VoidFunction) => Promise<void>;
attachTimeline?: (timeline: ProgressTimeline) => VoidFunction;
}
declare type DynamicOption<T> = (i: number, total: number) => T;
interface CSSStyleDeclarationWithTransform extends Omit<CSSStyleDeclaration, "direction" | "transition"> {
x: number | string;
y: number | string;
z: number | string;
rotateX: number | string;
rotateY: number | string;
rotateZ: number | string;
scaleX: number;
scaleY: number;
scaleZ: number;
skewX: number | string;
skewY: number | string;
}
declare type ValueKeyframe = string | number;
declare type UnresolvedValueKeyframe = ValueKeyframe | null;
declare type ValueKeyframesDefinition = ValueKeyframe | ValueKeyframe[] | UnresolvedValueKeyframe[];
declare type StyleKeyframesDefinition = {
[K in keyof CSSStyleDeclarationWithTransform]?: ValueKeyframesDefinition;
};
declare type SVGKeyframesDefinition = {
[K in keyof SVGAttributes]?: ValueKeyframesDefinition;
};
declare type VariableKeyframesDefinition = {
[key: `--${string}`]: ValueKeyframesDefinition;
};
declare type SVGPathKeyframesDefinition = {
[K in keyof SVGPathProperties]?: ValueKeyframesDefinition;
};
declare type DOMKeyframesDefinition = StyleKeyframesDefinition & SVGKeyframesDefinition & SVGPathKeyframesDefinition & VariableKeyframesDefinition;
interface VelocityOptions {
velocity?: number;
restSpeed?: number;
restDelta?: number;
}
interface AnimationPlaybackOptions {
repeat?: number;
repeatType?: "loop" | "reverse" | "mirror";
repeatDelay?: number;
}
interface DurationSpringOptions {
duration?: number;
bounce?: number;
}
interface SpringOptions extends DurationSpringOptions, VelocityOptions {
stiffness?: number;
damping?: number;
mass?: number;
}
interface DecayOptions extends VelocityOptions {
keyframes?: number[];
power?: number;
timeConstant?: number;
modifyTarget?: (v: number) => number;
}
interface InertiaOptions extends DecayOptions {
bounceStiffness?: number;
bounceDamping?: number;
min?: number;
max?: number;
}
interface KeyframeOptions {
ease?: Easing | Easing[];
times?: number[];
}
/**
* @public
*/
declare type Subscriber<T> = (v: T) => void;
/**
* @public
*/
declare type PassiveEffect<T> = (v: T, safeSetter: (v: T) => void) => void;
interface MotionValueEventCallbacks<V> {
animationStart: () => void;
animationComplete: () => void;
animationCancel: () => void;
change: (latestValue: V) => void;
renderRequest: () => void;
velocityChange: (latestVelocity: number) => void;
}
interface ResolvedValues {
[key: string]: string | number;
}
interface Owner {
current: HTMLElement | unknown;
getProps: () => {
onUpdate?: (latest: ResolvedValues) => void;
};
}
interface MotionValueOptions {
owner?: Owner;
}
/**
* `MotionValue` is used to track the state and velocity of motion values.
*
* @public
*/
declare class MotionValue<V = any> {
/**
* This will be replaced by the build step with the latest version number.
* When MotionValues are provided to motion components, warn if versions are mixed.
*/
version: string;
/**
* If a MotionValue has an owner, it was created internally within Framer Motion
* and therefore has no external listeners. It is therefore safe to animate via WAAPI.
*/
owner?: Owner;
private stopPassiveEffect?;
/**
* A reference to the currently-controlling Popmotion animation
*
*
*/
animation?: AnimationPlaybackControls;
/**
* Adds a function that will be notified when the `MotionValue` is updated.
*
* It returns a function that, when called, will cancel the subscription.
*
* When calling `onChange` inside a React component, it should be wrapped with the
* `useEffect` hook. As it returns an unsubscribe function, this should be returned
* from the `useEffect` function to ensure you don't add duplicate subscribers..
*
* ```jsx
* export const MyComponent = () => {
* const x = useMotionValue(0)
* const y = useMotionValue(0)
* const opacity = useMotionValue(1)
*
* useEffect(() => {
* function updateOpacity() {
* const maxXY = Math.max(x.get(), y.get())
* const newOpacity = transform(maxXY, [0, 100], [1, 0])
* opacity.set(newOpacity)
* }
*
* const unsubscribeX = x.on("change", updateOpacity)
* const unsubscribeY = y.on("change", updateOpacity)
*
* return () => {
* unsubscribeX()
* unsubscribeY()
* }
* }, [])
*
* return <motion.div style={{ x }} />
* }
* ```
*
* @param subscriber - A function that receives the latest value.
* @returns A function that, when called, will cancel this subscription.
*
* @deprecated
*/
onChange(subscription: Subscriber<V>): () => void;
/**
* An object containing a SubscriptionManager for each active event.
*/
private events;
on<EventName extends keyof MotionValueEventCallbacks<V>>(eventName: EventName, callback: MotionValueEventCallbacks<V>[EventName]): VoidFunction;
clearListeners(): void;
/**
* Sets the state of the `MotionValue`.
*
* @remarks
*
* ```jsx
* const x = useMotionValue(0)
* x.set(10)
* ```
*
* @param latest - Latest value to set.
* @param render - Whether to notify render subscribers. Defaults to `true`
*
* @public
*/
set(v: V, render?: boolean): void;
setWithVelocity(prev: V, current: V, delta: number): void;
/**
* Set the state of the `MotionValue`, stopping any active animations,
* effects, and resets velocity to `0`.
*/
jump(v: V): void;
updateAndNotify: (v: V, render?: boolean) => void;
/**
* Returns the latest state of `MotionValue`
*
* @returns - The latest state of `MotionValue`
*
* @public
*/
get(): V;
/**
* @public
*/
getPrevious(): V;
/**
* Returns the latest velocity of `MotionValue`
*
* @returns - The latest velocity of `MotionValue`. Returns `0` if the state is non-numerical.
*
* @public
*/
getVelocity(): number;
hasAnimated: boolean;
/**
* Stop the currently active animation.
*
* @public
*/
stop(): void;
/**
* Returns `true` if this value is currently animating.
*
* @public
*/
isAnimating(): boolean;
private clearAnimation;
/**
* Destroy and clean up subscribers to this `MotionValue`.
*
* The `MotionValue` hooks like `useMotionValue` and `useTransform` automatically
* handle the lifecycle of the returned `MotionValue`, so this method is only necessary if you've manually
* created a `MotionValue` via the `motionValue` function.
*
* @public
*/
destroy(): void;
}
declare function motionValue<V>(init: V, options?: MotionValueOptions): MotionValue<V>;
declare type SequenceTime = number | "<" | `+${number}` | `-${number}` | `${string}`;
declare type SequenceLabel = string;
interface SequenceLabelWithTime {
name: SequenceLabel;
at: SequenceTime;
}
interface At {
at?: SequenceTime;
}
declare type MotionValueSegment = [
MotionValue,
UnresolvedValueKeyframe | UnresolvedValueKeyframe[]
];
declare type MotionValueSegmentWithTransition = [
MotionValue,
UnresolvedValueKeyframe | UnresolvedValueKeyframe[],
Transition & At
];
declare type DOMSegment = [ElementOrSelector, DOMKeyframesDefinition];
declare type DOMSegmentWithTransition = [
ElementOrSelector,
DOMKeyframesDefinition,
DynamicAnimationOptions & At
];
declare type Segment = MotionValueSegment | MotionValueSegmentWithTransition | DOMSegment | DOMSegmentWithTransition | SequenceLabel | SequenceLabelWithTime;
declare type AnimationSequence = Segment[];
interface SequenceOptions extends AnimationPlaybackOptions {
delay?: number;
duration?: number;
defaultTransition?: Transition;
}
declare const createScopedAnimate: (scope?: AnimationScope) => {
<V>(from: V, to: V | GenericKeyframesTarget<V>, options?: ValueAnimationTransition<V> | undefined): AnimationPlaybackControls;
<V_1>(value: MotionValue<V_1>, keyframes: V_1 | GenericKeyframesTarget<V_1>, options?: ValueAnimationTransition<V_1> | undefined): AnimationPlaybackControls;
(value: ElementOrSelector, keyframes: DOMKeyframesDefinition, options?: DynamicAnimationOptions): AnimationPlaybackControls;
(sequence: AnimationSequence, options?: SequenceOptions): AnimationPlaybackControls;
};
declare const animate: {
<V>(from: V, to: V | GenericKeyframesTarget<V>, options?: ValueAnimationTransition<V> | undefined): AnimationPlaybackControls;
<V_1>(value: MotionValue<V_1>, keyframes: V_1 | GenericKeyframesTarget<V_1>, options?: ValueAnimationTransition<V_1> | undefined): AnimationPlaybackControls;
(value: ElementOrSelector, keyframes: DOMKeyframesDefinition, options?: DynamicAnimationOptions): AnimationPlaybackControls;
(sequence: AnimationSequence, options?: SequenceOptions): AnimationPlaybackControls;
};
interface ScrollOptions {
source?: Element;
axis?: "x" | "y";
}
declare type OnScroll = (progress: number) => void;
interface AxisScrollInfo {
current: number;
offset: number[];
progress: number;
scrollLength: number;
velocity: number;
targetOffset: number;
targetLength: number;
containerLength: number;
interpolatorOffsets?: number[];
interpolate?: EasingFunction;
}
interface ScrollInfo {
time: number;
x: AxisScrollInfo;
y: AxisScrollInfo;
}
declare type OnScrollInfo = (info: ScrollInfo) => void;
declare type SupportedEdgeUnit = "px" | "vw" | "vh" | "%";
declare type EdgeUnit = `${number}${SupportedEdgeUnit}`;
declare type NamedEdges = "start" | "end" | "center";
declare type EdgeString = NamedEdges | EdgeUnit | `${number}`;
declare type Edge = EdgeString | number;
declare type ProgressIntersection = [number, number];
declare type Intersection = `${Edge} ${Edge}`;
declare type ScrollOffset = Array<Edge | Intersection | ProgressIntersection>;
interface ScrollInfoOptions {
container?: HTMLElement;
target?: Element;
axis?: "x" | "y";
offset?: ScrollOffset;
smooth?: number;
}
declare class GroupPlaybackControls implements AnimationPlaybackControls {
animations: AnimationPlaybackControls[];
constructor(animations: Array<AnimationPlaybackControls | undefined>);
then(onResolve: VoidFunction, onReject?: VoidFunction): Promise<void>;
/**
* TODO: Filter out cancelled or stopped animations before returning
*/
private getAll;
private setAll;
attachTimeline(timeline: any): () => void;
get time(): number;
set time(time: number);
get speed(): number;
set speed(speed: number);
get duration(): number;
private runAll;
play(): void;
pause(): void;
stop(): void;
cancel(): void;
complete(): void;
}
declare class ScrollTimeline implements ProgressTimeline {
constructor(options: ScrollOptions);
currentTime: null | {
value: number;
};
cancel?: VoidFunction;
}
declare global {
interface Window {
ScrollTimeline: ScrollTimeline;
}
}
declare function scroll(onScroll: OnScroll | GroupPlaybackControls, options?: ScrollOptions): VoidFunction;
declare function scrollInfo(onScroll: OnScrollInfo, { container, ...options }?: ScrollInfoOptions): () => void;
declare type ViewChangeHandler = (entry: IntersectionObserverEntry) => void;
interface InViewOptions {
root?: Element | Document;
margin?: string;
amount?: "some" | "all" | number;
}
declare function inView(elementOrSelector: ElementOrSelector, onStart: (entry: IntersectionObserverEntry) => void | ViewChangeHandler, { root, margin: rootMargin, amount }?: InViewOptions): VoidFunction;
declare const anticipate: (p: number) => number;
declare const backOut: (t: number) => number;
declare const backIn: EasingFunction;
declare const backInOut: EasingFunction;
declare const circIn: EasingFunction;
declare const circOut: EasingFunction;
declare const circInOut: EasingFunction;
declare const easeIn: (t: number) => number;
declare const easeOut: (t: number) => number;
declare const easeInOut: (t: number) => number;
declare function cubicBezier(mX1: number, mY1: number, mX2: number, mY2: number): (t: number) => number;
declare const mirrorEasing: EasingModifier;
declare const reverseEasing: EasingModifier;
declare type StaggerOrigin = "first" | "last" | "center" | number;
declare type StaggerOptions = {
startDelay?: number;
from?: StaggerOrigin;
ease?: Easing;
};
declare function stagger(duration?: number, { startDelay, from, ease }?: StaggerOptions): DynamicOption<number>;
/**
* @public
*/
interface TransformOptions<T> {
/**
* Clamp values to within the given range. Defaults to `true`
*
* @public
*/
clamp?: boolean;
/**
* Easing functions to use on the interpolations between each value in the input and output ranges.
*
* If provided as an array, the array must be one item shorter than the input and output ranges, as the easings apply to the transition **between** each.
*
* @public
*/
ease?: EasingFunction | EasingFunction[];
/**
* Provide a function that can interpolate between any two values in the provided range.
*
* @public
*/
mixer?: (from: T, to: T) => (v: number) => any;
}
/**
* Transforms numbers into other values by mapping them from an input range to an output range.
* Returns the type of the input provided.
*
* @remarks
*
* Given an input range of `[0, 200]` and an output range of
* `[0, 1]`, this function will return a value between `0` and `1`.
* The input range must be a linear series of numbers. The output range
* can be any supported value type, such as numbers, colors, shadows, arrays, objects and more.
* Every value in the output range must be of the same type and in the same format.
*
* ```jsx
* import * as React from "react"
* import { transform } from "framer-motion"
*
* export function MyComponent() {
* const inputRange = [0, 200]
* const outputRange = [0, 1]
* const output = transform(100, inputRange, outputRange)
*
* // Returns 0.5
* return <div>{output}</div>
* }
* ```
*
* @param inputValue - A number to transform between the input and output ranges.
* @param inputRange - A linear series of numbers (either all increasing or decreasing).
* @param outputRange - A series of numbers, colors, strings, or arrays/objects of those. Must be the same length as `inputRange`.
* @param options - Clamp: Clamp values to within the given range. Defaults to `true`.
*
* @public
*/
declare function transform<T>(inputValue: number, inputRange: number[], outputRange: T[], options?: TransformOptions<T>): T;
/**
*
* Transforms numbers into other values by mapping them from an input range to an output range.
*
* Given an input range of `[0, 200]` and an output range of
* `[0, 1]`, this function will return a value between `0` and `1`.
* The input range must be a linear series of numbers. The output range
* can be any supported value type, such as numbers, colors, shadows, arrays, objects and more.
* Every value in the output range must be of the same type and in the same format.
*
* ```jsx
* import * as React from "react"
* import { Frame, transform } from "framer"
*
* export function MyComponent() {
* const inputRange = [-200, -100, 100, 200]
* const outputRange = [0, 1, 1, 0]
* const convertRange = transform(inputRange, outputRange)
* const output = convertRange(-150)
*
* // Returns 0.5
* return <div>{output}</div>
* }
*
* ```
*
* @param inputRange - A linear series of numbers (either all increasing or decreasing).
* @param outputRange - A series of numbers, colors or strings. Must be the same length as `inputRange`.
* @param options - Clamp: clamp values to within the given range. Defaults to `true`.
*
* @public
*/
declare function transform<T>(inputRange: number[], outputRange: T[], options?: TransformOptions<T>): (inputValue: number) => T;
declare const clamp: (min: number, max: number, v: number) => number;
declare type DelayedFunction = (overshoot: number) => void;
/**
* Timeout defined in ms
*/
declare function delay(callback: DelayedFunction, timeout: number): () => void;
declare const distance: (a: number, b: number) => number;
declare function distance2D(a: Point, b: Point): number;
declare type DevMessage = (check: boolean, message: string) => void;
declare let warning: DevMessage;
declare let invariant: DevMessage;
declare type Mix<T> = (v: number) => T;
declare type MixerFactory<T> = (from: T, to: T) => Mix<T>;
interface InterpolateOptions<T> {
clamp?: boolean;
ease?: EasingFunction | EasingFunction[];
mixer?: MixerFactory<T>;
}
/**
* Create a function that maps from a numerical input array to a generic output array.
*
* Accepts:
* - Numbers
* - Colors (hex, hsl, hsla, rgb, rgba)
* - Complex (combinations of one or more numbers or strings)
*
* ```jsx
* const mixColor = interpolate([0, 1], ['#fff', '#000'])
*
* mixColor(0.5) // 'rgba(128, 128, 128, 1)'
* ```
*
* TODO Revist this approach once we've moved to data models for values,
* probably not needed to pregenerate mixer functions.
*
* @public
*/
declare function interpolate<T>(input: number[], output: T[], { clamp: isClamp, ease, mixer }?: InterpolateOptions<T>): (v: number) => T;
declare const mix: (from: number, to: number, progress: number) => number;
declare const pipe: (...transformers: Function[]) => Function;
declare const progress: (from: number, to: number, value: number) => number;
declare const wrap: (min: number, max: number, v: number) => number;
declare const frame: Batcher;
declare const cancelFrame: (process: Process) => void;
declare const frameData: FrameData;
declare const steps: Steps;
/**
* @deprecated
*
* Import as `frame` instead.
*/
declare const sync: Batcher;
/**
* @deprecated
*
* Use cancelFrame(callback) instead.
*/
declare const cancelSync: {};
export { DelayedFunction, DevMessage, InterpolateOptions, MixerFactory, MotionValue, PassiveEffect, Subscriber, animate, anticipate, backIn, backInOut, backOut, cancelFrame, cancelSync, circIn, circInOut, circOut, clamp, createScopedAnimate, cubicBezier, delay, distance, distance2D, easeIn, easeInOut, easeOut, frame, frameData, inView, interpolate, invariant, mirrorEasing, mix, motionValue, pipe, progress, reverseEasing, scroll, scrollInfo, stagger, steps, sync, transform, warning, wrap };

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,302 @@
import { keyframes } from '../../generators/keyframes.mjs';
import { spring } from '../../generators/spring/index.mjs';
import { inertia } from '../../generators/inertia.mjs';
import { frameloopDriver } from './driver-frameloop.mjs';
import { interpolate } from '../../../utils/interpolate.mjs';
import { clamp } from '../../../utils/clamp.mjs';
import { millisecondsToSeconds, secondsToMilliseconds } from '../../../utils/time-conversion.mjs';
import { calcGeneratorDuration } from '../../generators/utils/calc-duration.mjs';
import { invariant } from '../../../utils/errors.mjs';
const types = {
decay: inertia,
inertia,
tween: keyframes,
keyframes: keyframes,
spring,
};
/**
* Animate a single value on the main thread.
*
* This function is written, where functionality overlaps,
* to be largely spec-compliant with WAAPI to allow fungibility
* between the two.
*/
function animateValue({ autoplay = true, delay = 0, driver = frameloopDriver, keyframes: keyframes$1, type = "keyframes", repeat = 0, repeatDelay = 0, repeatType = "loop", onPlay, onStop, onComplete, onUpdate, ...options }) {
let speed = 1;
let hasStopped = false;
let resolveFinishedPromise;
let currentFinishedPromise;
/**
* Resolve the current Promise every time we enter the
* finished state. This is WAAPI-compatible behaviour.
*/
const updateFinishedPromise = () => {
currentFinishedPromise = new Promise((resolve) => {
resolveFinishedPromise = resolve;
});
};
// Create the first finished promise
updateFinishedPromise();
let animationDriver;
const generatorFactory = types[type] || keyframes;
/**
* If this isn't the keyframes generator and we've been provided
* strings as keyframes, we need to interpolate these.
*/
let mapNumbersToKeyframes;
if (generatorFactory !== keyframes &&
typeof keyframes$1[0] !== "number") {
if (process.env.NODE_ENV !== "production") {
invariant(keyframes$1.length === 2, `Only two keyframes currently supported with spring and inertia animations. Trying to animate ${keyframes$1}`);
}
mapNumbersToKeyframes = interpolate([0, 100], keyframes$1, {
clamp: false,
});
keyframes$1 = [0, 100];
}
const generator = generatorFactory({ ...options, keyframes: keyframes$1 });
let mirroredGenerator;
if (repeatType === "mirror") {
mirroredGenerator = generatorFactory({
...options,
keyframes: [...keyframes$1].reverse(),
velocity: -(options.velocity || 0),
});
}
let playState = "idle";
let holdTime = null;
let startTime = null;
let cancelTime = null;
/**
* If duration is undefined and we have repeat options,
* we need to calculate a duration from the generator.
*
* We set it to the generator itself to cache the duration.
* Any timeline resolver will need to have already precalculated
* the duration by this step.
*/
if (generator.calculatedDuration === null && repeat) {
generator.calculatedDuration = calcGeneratorDuration(generator);
}
const { calculatedDuration } = generator;
let resolvedDuration = Infinity;
let totalDuration = Infinity;
if (calculatedDuration !== null) {
resolvedDuration = calculatedDuration + repeatDelay;
totalDuration = resolvedDuration * (repeat + 1) - repeatDelay;
}
let currentTime = 0;
const tick = (timestamp) => {
if (startTime === null)
return;
/**
* requestAnimationFrame timestamps can come through as lower than
* the startTime as set by performance.now(). Here we prevent this,
* though in the future it could be possible to make setting startTime
* a pending operation that gets resolved here.
*/
if (speed > 0)
startTime = Math.min(startTime, timestamp);
if (speed < 0)
startTime = Math.min(timestamp - totalDuration / speed, startTime);
if (holdTime !== null) {
currentTime = holdTime;
}
else {
// Rounding the time because floating point arithmetic is not always accurate, e.g. 3000.367 - 1000.367 =
// 2000.0000000000002. This is a problem when we are comparing the currentTime with the duration, for
// example.
currentTime = Math.round(timestamp - startTime) * speed;
}
// Rebase on delay
const timeWithoutDelay = currentTime - delay * (speed >= 0 ? 1 : -1);
const isInDelayPhase = speed >= 0 ? timeWithoutDelay < 0 : timeWithoutDelay > totalDuration;
currentTime = Math.max(timeWithoutDelay, 0);
/**
* If this animation has finished, set the current time
* to the total duration.
*/
if (playState === "finished" && holdTime === null) {
currentTime = totalDuration;
}
let elapsed = currentTime;
let frameGenerator = generator;
if (repeat) {
/**
* Get the current progress (0-1) of the animation. If t is >
* than duration we'll get values like 2.5 (midway through the
* third iteration)
*/
const progress = Math.min(currentTime, totalDuration) / resolvedDuration;
/**
* Get the current iteration (0 indexed). For instance the floor of
* 2.5 is 2.
*/
let currentIteration = Math.floor(progress);
/**
* Get the current progress of the iteration by taking the remainder
* so 2.5 is 0.5 through iteration 2
*/
let iterationProgress = progress % 1.0;
/**
* If iteration progress is 1 we count that as the end
* of the previous iteration.
*/
if (!iterationProgress && progress >= 1) {
iterationProgress = 1;
}
iterationProgress === 1 && currentIteration--;
currentIteration = Math.min(currentIteration, repeat + 1);
/**
* Reverse progress if we're not running in "normal" direction
*/
const isOddIteration = Boolean(currentIteration % 2);
if (isOddIteration) {
if (repeatType === "reverse") {
iterationProgress = 1 - iterationProgress;
if (repeatDelay) {
iterationProgress -= repeatDelay / resolvedDuration;
}
}
else if (repeatType === "mirror") {
frameGenerator = mirroredGenerator;
}
}
elapsed = clamp(0, 1, iterationProgress) * resolvedDuration;
}
/**
* If we're in negative time, set state as the initial keyframe.
* This prevents delay: x, duration: 0 animations from finishing
* instantly.
*/
const state = isInDelayPhase
? { done: false, value: keyframes$1[0] }
: frameGenerator.next(elapsed);
if (mapNumbersToKeyframes) {
state.value = mapNumbersToKeyframes(state.value);
}
let { done } = state;
if (!isInDelayPhase && calculatedDuration !== null) {
done = speed >= 0 ? currentTime >= totalDuration : currentTime <= 0;
}
const isAnimationFinished = holdTime === null &&
(playState === "finished" || (playState === "running" && done));
if (onUpdate) {
onUpdate(state.value);
}
if (isAnimationFinished) {
finish();
}
return state;
};
const stopAnimationDriver = () => {
animationDriver && animationDriver.stop();
animationDriver = undefined;
};
const cancel = () => {
playState = "idle";
stopAnimationDriver();
resolveFinishedPromise();
updateFinishedPromise();
startTime = cancelTime = null;
};
const finish = () => {
playState = "finished";
onComplete && onComplete();
stopAnimationDriver();
resolveFinishedPromise();
};
const play = () => {
if (hasStopped)
return;
if (!animationDriver)
animationDriver = driver(tick);
const now = animationDriver.now();
onPlay && onPlay();
if (holdTime !== null) {
startTime = now - holdTime;
}
else if (!startTime || playState === "finished") {
startTime = now;
}
if (playState === "finished") {
updateFinishedPromise();
}
cancelTime = startTime;
holdTime = null;
/**
* Set playState to running only after we've used it in
* the previous logic.
*/
playState = "running";
animationDriver.start();
};
if (autoplay) {
play();
}
const controls = {
then(resolve, reject) {
return currentFinishedPromise.then(resolve, reject);
},
get time() {
return millisecondsToSeconds(currentTime);
},
set time(newTime) {
newTime = secondsToMilliseconds(newTime);
currentTime = newTime;
if (holdTime !== null || !animationDriver || speed === 0) {
holdTime = newTime;
}
else {
startTime = animationDriver.now() - newTime / speed;
}
},
get duration() {
const duration = generator.calculatedDuration === null
? calcGeneratorDuration(generator)
: generator.calculatedDuration;
return millisecondsToSeconds(duration);
},
get speed() {
return speed;
},
set speed(newSpeed) {
if (newSpeed === speed || !animationDriver)
return;
speed = newSpeed;
controls.time = millisecondsToSeconds(currentTime);
},
get state() {
return playState;
},
play,
pause: () => {
playState = "paused";
holdTime = currentTime;
},
stop: () => {
hasStopped = true;
if (playState === "idle")
return;
playState = "idle";
onStop && onStop();
cancel();
},
cancel: () => {
if (cancelTime !== null)
tick(cancelTime);
cancel();
},
complete: () => {
playState = "finished";
},
sample: (elapsed) => {
startTime = 0;
return tick(elapsed);
},
};
return controls;
}
export { animateValue };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,71 @@
import * as React from 'react';
import { useId, useRef, useInsertionEffect } from 'react';
/**
* Measurement functionality has to be within a separate component
* to leverage snapshot lifecycle.
*/
class PopChildMeasure extends React.Component {
getSnapshotBeforeUpdate(prevProps) {
const element = this.props.childRef.current;
if (element && prevProps.isPresent && !this.props.isPresent) {
const size = this.props.sizeRef.current;
size.height = element.offsetHeight || 0;
size.width = element.offsetWidth || 0;
size.top = element.offsetTop;
size.left = element.offsetLeft;
}
return null;
}
/**
* Required with getSnapshotBeforeUpdate to stop React complaining.
*/
componentDidUpdate() { }
render() {
return this.props.children;
}
}
function PopChild({ children, isPresent }) {
const id = useId();
const ref = useRef(null);
const size = useRef({
width: 0,
height: 0,
top: 0,
left: 0,
});
/**
* We create and inject a style block so we can apply this explicit
* sizing in a non-destructive manner by just deleting the style block.
*
* We can't apply size via render as the measurement happens
* in getSnapshotBeforeUpdate (post-render), likewise if we apply the
* styles directly on the DOM node, we might be overwriting
* styles set via the style prop.
*/
useInsertionEffect(() => {
const { width, height, top, left } = size.current;
if (isPresent || !ref.current || !width || !height)
return;
ref.current.dataset.motionPopId = id;
const style = document.createElement("style");
document.head.appendChild(style);
if (style.sheet) {
style.sheet.insertRule(`
[data-motion-pop-id="${id}"] {
position: absolute !important;
width: ${width}px !important;
height: ${height}px !important;
top: ${top}px !important;
left: ${left}px !important;
}
`);
}
return () => {
document.head.removeChild(style);
};
}, [isPresent]);
return (React.createElement(PopChildMeasure, { isPresent: isPresent, childRef: ref, sizeRef: size }, React.cloneElement(children, { ref })));
}
export { PopChild };

View File

@@ -0,0 +1,56 @@
import * as React from 'react';
import { useId, useMemo } from 'react';
import { PresenceContext } from '../../context/PresenceContext.mjs';
import { useConstant } from '../../utils/use-constant.mjs';
import { PopChild } from './PopChild.mjs';
const PresenceChild = ({ children, initial, isPresent, onExitComplete, custom, presenceAffectsLayout, mode, }) => {
const presenceChildren = useConstant(newChildrenMap);
const id = useId();
const context = useMemo(() => ({
id,
initial,
isPresent,
custom,
onExitComplete: (childId) => {
presenceChildren.set(childId, true);
for (const isComplete of presenceChildren.values()) {
if (!isComplete)
return; // can stop searching when any is incomplete
}
onExitComplete && onExitComplete();
},
register: (childId) => {
presenceChildren.set(childId, false);
return () => presenceChildren.delete(childId);
},
}),
/**
* If the presence of a child affects the layout of the components around it,
* we want to make a new context value to ensure they get re-rendered
* so they can detect that layout change.
*/
presenceAffectsLayout ? undefined : [isPresent]);
useMemo(() => {
presenceChildren.forEach((_, key) => presenceChildren.set(key, false));
}, [isPresent]);
/**
* If there's no `motion` components to fire exit animations, we want to remove this
* component immediately.
*/
React.useEffect(() => {
!isPresent &&
!presenceChildren.size &&
onExitComplete &&
onExitComplete();
}, [isPresent]);
if (mode === "popLayout") {
children = React.createElement(PopChild, { isPresent: isPresent }, children);
}
return (React.createElement(PresenceContext.Provider, { value: context }, children));
};
function newChildrenMap() {
return new Map();
}
export { PresenceChild };

View File

@@ -0,0 +1,169 @@
import * as React from 'react';
import { useContext, useRef, cloneElement, Children, isValidElement } from 'react';
import { useForceUpdate } from '../../utils/use-force-update.mjs';
import { useIsMounted } from '../../utils/use-is-mounted.mjs';
import { PresenceChild } from './PresenceChild.mjs';
import { LayoutGroupContext } from '../../context/LayoutGroupContext.mjs';
import { useIsomorphicLayoutEffect } from '../../utils/use-isomorphic-effect.mjs';
import { useUnmountEffect } from '../../utils/use-unmount-effect.mjs';
import { invariant } from '../../utils/errors.mjs';
const getChildKey = (child) => child.key || "";
function updateChildLookup(children, allChildren) {
children.forEach((child) => {
const key = getChildKey(child);
allChildren.set(key, child);
});
}
function onlyElements(children) {
const filtered = [];
// We use forEach here instead of map as map mutates the component key by preprending `.$`
Children.forEach(children, (child) => {
if (isValidElement(child))
filtered.push(child);
});
return filtered;
}
/**
* `AnimatePresence` enables the animation of components that have been removed from the tree.
*
* When adding/removing more than a single child, every child **must** be given a unique `key` prop.
*
* Any `motion` components that have an `exit` property defined will animate out when removed from
* the tree.
*
* ```jsx
* import { motion, AnimatePresence } from 'framer-motion'
*
* export const Items = ({ items }) => (
* <AnimatePresence>
* {items.map(item => (
* <motion.div
* key={item.id}
* initial={{ opacity: 0 }}
* animate={{ opacity: 1 }}
* exit={{ opacity: 0 }}
* />
* ))}
* </AnimatePresence>
* )
* ```
*
* You can sequence exit animations throughout a tree using variants.
*
* If a child contains multiple `motion` components with `exit` props, it will only unmount the child
* once all `motion` components have finished animating out. Likewise, any components using
* `usePresence` all need to call `safeToRemove`.
*
* @public
*/
const AnimatePresence = ({ children, custom, initial = true, onExitComplete, exitBeforeEnter, presenceAffectsLayout = true, mode = "sync", }) => {
invariant(!exitBeforeEnter, "Replace exitBeforeEnter with mode='wait'");
// We want to force a re-render once all exiting animations have finished. We
// either use a local forceRender function, or one from a parent context if it exists.
const forceRender = useContext(LayoutGroupContext).forceRender || useForceUpdate()[0];
const isMounted = useIsMounted();
// Filter out any children that aren't ReactElements. We can only track ReactElements with a props.key
const filteredChildren = onlyElements(children);
let childrenToRender = filteredChildren;
const exitingChildren = useRef(new Map()).current;
// Keep a living record of the children we're actually rendering so we
// can diff to figure out which are entering and exiting
const presentChildren = useRef(childrenToRender);
// A lookup table to quickly reference components by key
const allChildren = useRef(new Map()).current;
// If this is the initial component render, just deal with logic surrounding whether
// we play onMount animations or not.
const isInitialRender = useRef(true);
useIsomorphicLayoutEffect(() => {
isInitialRender.current = false;
updateChildLookup(filteredChildren, allChildren);
presentChildren.current = childrenToRender;
});
useUnmountEffect(() => {
isInitialRender.current = true;
allChildren.clear();
exitingChildren.clear();
});
if (isInitialRender.current) {
return (React.createElement(React.Fragment, null, childrenToRender.map((child) => (React.createElement(PresenceChild, { key: getChildKey(child), isPresent: true, initial: initial ? undefined : false, presenceAffectsLayout: presenceAffectsLayout, mode: mode }, child)))));
}
// If this is a subsequent render, deal with entering and exiting children
childrenToRender = [...childrenToRender];
// Diff the keys of the currently-present and target children to update our
// exiting list.
const presentKeys = presentChildren.current.map(getChildKey);
const targetKeys = filteredChildren.map(getChildKey);
// Diff the present children with our target children and mark those that are exiting
const numPresent = presentKeys.length;
for (let i = 0; i < numPresent; i++) {
const key = presentKeys[i];
if (targetKeys.indexOf(key) === -1 && !exitingChildren.has(key)) {
exitingChildren.set(key, undefined);
}
}
// If we currently have exiting children, and we're deferring rendering incoming children
// until after all current children have exiting, empty the childrenToRender array
if (mode === "wait" && exitingChildren.size) {
childrenToRender = [];
}
// Loop through all currently exiting components and clone them to overwrite `animate`
// with any `exit` prop they might have defined.
exitingChildren.forEach((component, key) => {
// If this component is actually entering again, early return
if (targetKeys.indexOf(key) !== -1)
return;
const child = allChildren.get(key);
if (!child)
return;
const insertionIndex = presentKeys.indexOf(key);
let exitingComponent = component;
if (!exitingComponent) {
const onExit = () => {
// clean up the exiting children map
exitingChildren.delete(key);
// compute the keys of children that were rendered once but are no longer present
// this could happen in case of too many fast consequent renderings
// @link https://github.com/framer/motion/issues/2023
const leftOverKeys = Array.from(allChildren.keys()).filter((childKey) => !targetKeys.includes(childKey));
// clean up the all children map
leftOverKeys.forEach((leftOverKey) => allChildren.delete(leftOverKey));
// make sure to render only the children that are actually visible
presentChildren.current = filteredChildren.filter((presentChild) => {
const presentChildKey = getChildKey(presentChild);
return (
// filter out the node exiting
presentChildKey === key ||
// filter out the leftover children
leftOverKeys.includes(presentChildKey));
});
// Defer re-rendering until all exiting children have indeed left
if (!exitingChildren.size) {
if (isMounted.current === false)
return;
forceRender();
onExitComplete && onExitComplete();
}
};
exitingComponent = (React.createElement(PresenceChild, { key: getChildKey(child), isPresent: false, onExitComplete: onExit, custom: custom, presenceAffectsLayout: presenceAffectsLayout, mode: mode }, child));
exitingChildren.set(key, exitingComponent);
}
childrenToRender.splice(insertionIndex, 0, exitingComponent);
});
// Add `MotionContext` even to children that don't need it to ensure we're rendering
// the same tree between renders
childrenToRender = childrenToRender.map((child) => {
const key = child.key;
return exitingChildren.has(key) ? (child) : (React.createElement(PresenceChild, { key: getChildKey(child), isPresent: true, presenceAffectsLayout: presenceAffectsLayout, mode: mode }, child));
});
if (process.env.NODE_ENV !== "production" &&
mode === "wait" &&
childrenToRender.length > 1) {
console.warn(`You're attempting to animate multiple children within AnimatePresence, but its mode is set to "wait". This will lead to odd visual behaviour.`);
}
return (React.createElement(React.Fragment, null, exitingChildren.size
? childrenToRender
: childrenToRender.map((child) => cloneElement(child))));
};
export { AnimatePresence };

View File

@@ -0,0 +1,66 @@
import { useContext, useId, useEffect } from 'react';
import { PresenceContext } from '../../context/PresenceContext.mjs';
/**
* When a component is the child of `AnimatePresence`, it can use `usePresence`
* to access information about whether it's still present in the React tree.
*
* ```jsx
* import { usePresence } from "framer-motion"
*
* export const Component = () => {
* const [isPresent, safeToRemove] = usePresence()
*
* useEffect(() => {
* !isPresent && setTimeout(safeToRemove, 1000)
* }, [isPresent])
*
* return <div />
* }
* ```
*
* If `isPresent` is `false`, it means that a component has been removed the tree, but
* `AnimatePresence` won't really remove it until `safeToRemove` has been called.
*
* @public
*/
function usePresence() {
const context = useContext(PresenceContext);
if (context === null)
return [true, null];
const { isPresent, onExitComplete, register } = context;
// It's safe to call the following hooks conditionally (after an early return) because the context will always
// either be null or non-null for the lifespan of the component.
const id = useId();
useEffect(() => register(id), []);
const safeToRemove = () => onExitComplete && onExitComplete(id);
return !isPresent && onExitComplete ? [false, safeToRemove] : [true];
}
/**
* Similar to `usePresence`, except `useIsPresent` simply returns whether or not the component is present.
* There is no `safeToRemove` function.
*
* ```jsx
* import { useIsPresent } from "framer-motion"
*
* export const Component = () => {
* const isPresent = useIsPresent()
*
* useEffect(() => {
* !isPresent && console.log("I've been removed!")
* }, [isPresent])
*
* return <div />
* }
* ```
*
* @public
*/
function useIsPresent() {
return isPresent(useContext(PresenceContext));
}
function isPresent(context) {
return context === null ? true : context.isPresent;
}
export { isPresent, useIsPresent, usePresence };

View File

@@ -0,0 +1,14 @@
import { invariant } from '../utils/errors.mjs';
import * as React from 'react';
import { useConstant } from '../utils/use-constant.mjs';
import { LayoutGroup } from './LayoutGroup/index.mjs';
let id = 0;
const AnimateSharedLayout = ({ children }) => {
React.useEffect(() => {
invariant(false, "AnimateSharedLayout is deprecated: https://www.framer.com/docs/guide-upgrade/##shared-layout-animations");
}, []);
return (React.createElement(LayoutGroup, { id: useConstant(() => `asl-${id++}`) }, children));
};
export { AnimateSharedLayout };

View File

@@ -0,0 +1,31 @@
import * as React from 'react';
import { useContext, useRef, useMemo } from 'react';
import { LayoutGroupContext } from '../../context/LayoutGroupContext.mjs';
import { DeprecatedLayoutGroupContext } from '../../context/DeprecatedLayoutGroupContext.mjs';
import { useForceUpdate } from '../../utils/use-force-update.mjs';
import { nodeGroup } from '../../projection/node/group.mjs';
const shouldInheritGroup = (inherit) => inherit === true;
const shouldInheritId = (inherit) => shouldInheritGroup(inherit === true) || inherit === "id";
const LayoutGroup = ({ children, id, inherit = true }) => {
const layoutGroupContext = useContext(LayoutGroupContext);
const deprecatedLayoutGroupContext = useContext(DeprecatedLayoutGroupContext);
const [forceRender, key] = useForceUpdate();
const context = useRef(null);
const upstreamId = layoutGroupContext.id || deprecatedLayoutGroupContext;
if (context.current === null) {
if (shouldInheritId(inherit) && upstreamId) {
id = id ? upstreamId + "-" + id : upstreamId;
}
context.current = {
id,
group: shouldInheritGroup(inherit)
? layoutGroupContext.group || nodeGroup()
: nodeGroup(),
};
}
const memoizedContext = useMemo(() => ({ ...context.current, forceRender }), [key]);
return (React.createElement(LayoutGroupContext.Provider, { value: memoizedContext }, children));
};
export { LayoutGroup };

View File

@@ -0,0 +1,67 @@
import * as React from 'react';
import { useState, useRef, useEffect } from 'react';
import { LazyContext } from '../../context/LazyContext.mjs';
import { loadFeatures } from '../../motion/features/load-features.mjs';
/**
* Used in conjunction with the `m` component to reduce bundle size.
*
* `m` is a version of the `motion` component that only loads functionality
* critical for the initial render.
*
* `LazyMotion` can then be used to either synchronously or asynchronously
* load animation and gesture support.
*
* ```jsx
* // Synchronous loading
* import { LazyMotion, m, domAnimation } from "framer-motion"
*
* function App() {
* return (
* <LazyMotion features={domAnimation}>
* <m.div animate={{ scale: 2 }} />
* </LazyMotion>
* )
* }
*
* // Asynchronous loading
* import { LazyMotion, m } from "framer-motion"
*
* function App() {
* return (
* <LazyMotion features={() => import('./path/to/domAnimation')}>
* <m.div animate={{ scale: 2 }} />
* </LazyMotion>
* )
* }
* ```
*
* @public
*/
function LazyMotion({ children, features, strict = false }) {
const [, setIsLoaded] = useState(!isLazyBundle(features));
const loadedRenderer = useRef(undefined);
/**
* If this is a synchronous load, load features immediately
*/
if (!isLazyBundle(features)) {
const { renderer, ...loadedFeatures } = features;
loadedRenderer.current = renderer;
loadFeatures(loadedFeatures);
}
useEffect(() => {
if (isLazyBundle(features)) {
features().then(({ renderer, ...loadedFeatures }) => {
loadFeatures(loadedFeatures);
loadedRenderer.current = renderer;
setIsLoaded(true);
});
}
}, []);
return (React.createElement(LazyContext.Provider, { value: { renderer: loadedRenderer.current, strict } }, children));
}
function isLazyBundle(features) {
return typeof features === "function";
}
export { LazyMotion };

View File

@@ -0,0 +1,43 @@
import * as React from 'react';
import { useContext, useMemo } from 'react';
import { MotionConfigContext } from '../../context/MotionConfigContext.mjs';
import { loadExternalIsValidProp } from '../../render/dom/utils/filter-props.mjs';
import { useConstant } from '../../utils/use-constant.mjs';
/**
* `MotionConfig` is used to set configuration options for all children `motion` components.
*
* ```jsx
* import { motion, MotionConfig } from "framer-motion"
*
* export function App() {
* return (
* <MotionConfig transition={{ type: "spring" }}>
* <motion.div animate={{ x: 100 }} />
* </MotionConfig>
* )
* }
* ```
*
* @public
*/
function MotionConfig({ children, isValidProp, ...config }) {
isValidProp && loadExternalIsValidProp(isValidProp);
/**
* Inherit props from any parent MotionConfig components
*/
config = { ...useContext(MotionConfigContext), ...config };
/**
* Don't allow isStatic to change between renders as it affects how many hooks
* motion components fire.
*/
config.isStatic = useConstant(() => config.isStatic);
/**
* Creating a new config context object will re-render every `motion` component
* every time it renders. So we only want to create a new one sparingly.
*/
const context = useMemo(() => config, [JSON.stringify(config.transition), config.transformPagePoint, config.reducedMotion]);
return (React.createElement(MotionConfigContext.Provider, { value: context }, children));
}
export { MotionConfig };

View File

@@ -0,0 +1,53 @@
import { invariant } from '../../utils/errors.mjs';
import * as React from 'react';
import { forwardRef, useRef, useEffect } from 'react';
import { ReorderContext } from '../../context/ReorderContext.mjs';
import { motion } from '../../render/dom/motion.mjs';
import { useConstant } from '../../utils/use-constant.mjs';
import { checkReorder } from './utils/check-reorder.mjs';
function ReorderGroup({ children, as = "ul", axis = "y", onReorder, values, ...props }, externalRef) {
const Component = useConstant(() => motion(as));
const order = [];
const isReordering = useRef(false);
invariant(Boolean(values), "Reorder.Group must be provided a values prop");
const context = {
axis,
registerItem: (value, layout) => {
// If the entry was already added, update it rather than adding it again
const idx = order.findIndex((entry) => value === entry.value);
if (idx !== -1) {
order[idx].layout = layout[axis];
}
else {
order.push({ value: value, layout: layout[axis] });
}
order.sort(compareMin);
},
updateOrder: (item, offset, velocity) => {
if (isReordering.current)
return;
const newOrder = checkReorder(order, item, offset, velocity);
if (order !== newOrder) {
isReordering.current = true;
onReorder(newOrder
.map(getValue)
.filter((value) => values.indexOf(value) !== -1));
}
},
};
useEffect(() => {
isReordering.current = false;
});
return (React.createElement(Component, { ...props, ref: externalRef, ignoreStrict: true },
React.createElement(ReorderContext.Provider, { value: context }, children)));
}
const Group = forwardRef(ReorderGroup);
function getValue(item) {
return item.value;
}
function compareMin(a, b) {
return a.layout.min - b.layout.min;
}
export { Group, ReorderGroup };

View File

@@ -0,0 +1,33 @@
import { invariant } from '../../utils/errors.mjs';
import * as React from 'react';
import { forwardRef, useContext } from 'react';
import { ReorderContext } from '../../context/ReorderContext.mjs';
import { motion } from '../../render/dom/motion.mjs';
import { useConstant } from '../../utils/use-constant.mjs';
import { useMotionValue } from '../../value/use-motion-value.mjs';
import { useTransform } from '../../value/use-transform.mjs';
import { isMotionValue } from '../../value/utils/is-motion-value.mjs';
function useDefaultMotionValue(value, defaultValue = 0) {
return isMotionValue(value) ? value : useMotionValue(defaultValue);
}
function ReorderItem({ children, style = {}, value, as = "li", onDrag, layout = true, ...props }, externalRef) {
const Component = useConstant(() => motion(as));
const context = useContext(ReorderContext);
const point = {
x: useDefaultMotionValue(style.x),
y: useDefaultMotionValue(style.y),
};
const zIndex = useTransform([point.x, point.y], ([latestX, latestY]) => latestX || latestY ? 1 : "unset");
invariant(Boolean(context), "Reorder.Item must be a child of Reorder.Group");
const { axis, registerItem, updateOrder } = context;
return (React.createElement(Component, { drag: axis, ...props, dragSnapToOrigin: true, style: { ...style, x: point.x, y: point.y, zIndex }, layout: layout, onDrag: (event, gesturePoint) => {
const { velocity } = gesturePoint;
velocity[axis] &&
updateOrder(value, point[axis].get(), velocity[axis]);
onDrag && onDrag(event, gesturePoint);
}, onLayoutMeasure: (measured) => registerItem(value, measured), ref: externalRef, ignoreStrict: true }, children));
}
const Item = forwardRef(ReorderItem);
export { Item, ReorderItem };

View File

@@ -0,0 +1,9 @@
import { Group } from './Group.mjs';
import { Item } from './Item.mjs';
const Reorder = {
Group,
Item,
};
export { Reorder };

View File

@@ -0,0 +1,24 @@
import { moveItem } from '../../../utils/array.mjs';
import { mix } from '../../../utils/mix.mjs';
function checkReorder(order, value, offset, velocity) {
if (!velocity)
return order;
const index = order.findIndex((item) => item.value === value);
if (index === -1)
return order;
const nextOffset = velocity > 0 ? 1 : -1;
const nextItem = order[index + nextOffset];
if (!nextItem)
return order;
const item = order[index];
const nextLayout = nextItem.layout;
const nextItemCenter = mix(nextLayout.min, nextLayout.max, 0.5);
if ((nextOffset === 1 && item.layout.max + offset > nextItemCenter) ||
(nextOffset === -1 && item.layout.min + offset < nextItemCenter)) {
return moveItem(order, index, index + nextOffset);
}
return order;
}
export { checkReorder };

View File

@@ -0,0 +1,10 @@
import { createContext } from 'react';
/**
* Note: Still used by components generated by old versions of Framer
*
* @deprecated
*/
const DeprecatedLayoutGroupContext = createContext(null);
export { DeprecatedLayoutGroupContext };

View File

@@ -0,0 +1,5 @@
import { createContext } from 'react';
const LayoutGroupContext = createContext({});
export { LayoutGroupContext };

View File

@@ -0,0 +1,5 @@
import { createContext } from 'react';
const LazyContext = createContext({ strict: false });
export { LazyContext };

View File

@@ -0,0 +1,12 @@
import { createContext } from 'react';
/**
* @public
*/
const MotionConfigContext = createContext({
transformPagePoint: (p) => p,
isStatic: false,
reducedMotion: "never",
});
export { MotionConfigContext };

View File

@@ -0,0 +1,13 @@
import { useContext, useMemo } from 'react';
import { MotionContext } from './index.mjs';
import { getCurrentTreeVariants } from './utils.mjs';
function useCreateMotionContext(props) {
const { initial, animate } = getCurrentTreeVariants(props, useContext(MotionContext));
return useMemo(() => ({ initial, animate }), [variantLabelsAsDependency(initial), variantLabelsAsDependency(animate)]);
}
function variantLabelsAsDependency(prop) {
return Array.isArray(prop) ? prop.join(" ") : prop;
}
export { useCreateMotionContext };

View File

@@ -0,0 +1,5 @@
import { createContext } from 'react';
const MotionContext = createContext({});
export { MotionContext };

View File

@@ -0,0 +1,17 @@
import { isVariantLabel } from '../../render/utils/is-variant-label.mjs';
import { isControllingVariants } from '../../render/utils/is-controlling-variants.mjs';
function getCurrentTreeVariants(props, context) {
if (isControllingVariants(props)) {
const { initial, animate } = props;
return {
initial: initial === false || isVariantLabel(initial)
? initial
: undefined,
animate: isVariantLabel(animate) ? animate : undefined,
};
}
return props.inherit !== false ? context : {};
}
export { getCurrentTreeVariants };

View File

@@ -0,0 +1,8 @@
import { createContext } from 'react';
/**
* @public
*/
const PresenceContext = createContext(null);
export { PresenceContext };

View File

@@ -0,0 +1,5 @@
import { createContext } from 'react';
const ReorderContext = createContext(null);
export { ReorderContext };

View File

@@ -0,0 +1,8 @@
import { createContext } from 'react';
/**
* Internal, exported only for usage in Framer
*/
const SwitchLayoutGroupContext = createContext({});
export { SwitchLayoutGroupContext };

View File

@@ -0,0 +1,7 @@
function record(data) {
if (window.MotionDebug) {
window.MotionDebug.record(data);
}
}
export { record };

View File

@@ -0,0 +1,25 @@
export { MotionValue, motionValue } from './value/index.mjs';
export { animate, createScopedAnimate } from './animation/animate.mjs';
export { scroll } from './render/dom/scroll/index.mjs';
export { scrollInfo } from './render/dom/scroll/track.mjs';
export { inView } from './render/dom/viewport/index.mjs';
export { anticipate } from './easing/anticipate.mjs';
export { backIn, backInOut, backOut } from './easing/back.mjs';
export { circIn, circInOut, circOut } from './easing/circ.mjs';
export { easeIn, easeInOut, easeOut } from './easing/ease.mjs';
export { cubicBezier } from './easing/cubic-bezier.mjs';
export { mirrorEasing } from './easing/modifiers/mirror.mjs';
export { reverseEasing } from './easing/modifiers/reverse.mjs';
export { stagger } from './animation/utils/stagger.mjs';
export { transform } from './utils/transform.mjs';
export { clamp } from './utils/clamp.mjs';
export { delay } from './utils/delay.mjs';
export { distance, distance2D } from './utils/distance.mjs';
export { invariant, warning } from './utils/errors.mjs';
export { interpolate } from './utils/interpolate.mjs';
export { mix } from './utils/mix.mjs';
export { pipe } from './utils/pipe.mjs';
export { progress } from './utils/progress.mjs';
export { wrap } from './utils/wrap.mjs';
export { cancelSync, sync } from './frameloop/index-legacy.mjs';
export { cancelFrame, frame, frameData, steps } from './frameloop/frame.mjs';

View File

@@ -0,0 +1,5 @@
import { backIn } from './back.mjs';
const anticipate = (p) => (p *= 2) < 1 ? 0.5 * backIn(p) : 0.5 * (2 - Math.pow(2, -10 * (p - 1)));
export { anticipate };

View File

@@ -0,0 +1,9 @@
import { cubicBezier } from './cubic-bezier.mjs';
import { mirrorEasing } from './modifiers/mirror.mjs';
import { reverseEasing } from './modifiers/reverse.mjs';
const backOut = cubicBezier(0.33, 1.53, 0.69, 0.99);
const backIn = reverseEasing(backOut);
const backInOut = mirrorEasing(backIn);
export { backIn, backInOut, backOut };

View File

@@ -0,0 +1,8 @@
import { mirrorEasing } from './modifiers/mirror.mjs';
import { reverseEasing } from './modifiers/reverse.mjs';
const circIn = (p) => 1 - Math.sin(Math.acos(p));
const circOut = reverseEasing(circIn);
const circInOut = mirrorEasing(circIn);
export { circIn, circInOut, circOut };

View File

@@ -0,0 +1,51 @@
import { noop } from '../utils/noop.mjs';
/*
Bezier function generator
This has been modified from Gaëtan Renaudeau's BezierEasing
https://github.com/gre/bezier-easing/blob/master/src/index.js
https://github.com/gre/bezier-easing/blob/master/LICENSE
I've removed the newtonRaphsonIterate algo because in benchmarking it
wasn't noticiably faster than binarySubdivision, indeed removing it
usually improved times, depending on the curve.
I also removed the lookup table, as for the added bundle size and loop we're
only cutting ~4 or so subdivision iterations. I bumped the max iterations up
to 12 to compensate and this still tended to be faster for no perceivable
loss in accuracy.
Usage
const easeOut = cubicBezier(.17,.67,.83,.67);
const x = easeOut(0.5); // returns 0.627...
*/
// Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
const calcBezier = (t, a1, a2) => (((1.0 - 3.0 * a2 + 3.0 * a1) * t + (3.0 * a2 - 6.0 * a1)) * t + 3.0 * a1) *
t;
const subdivisionPrecision = 0.0000001;
const subdivisionMaxIterations = 12;
function binarySubdivide(x, lowerBound, upperBound, mX1, mX2) {
let currentX;
let currentT;
let i = 0;
do {
currentT = lowerBound + (upperBound - lowerBound) / 2.0;
currentX = calcBezier(currentT, mX1, mX2) - x;
if (currentX > 0.0) {
upperBound = currentT;
}
else {
lowerBound = currentT;
}
} while (Math.abs(currentX) > subdivisionPrecision &&
++i < subdivisionMaxIterations);
return currentT;
}
function cubicBezier(mX1, mY1, mX2, mY2) {
// If this is a linear gradient, return linear easing
if (mX1 === mY1 && mX2 === mY2)
return noop;
const getTForX = (aX) => binarySubdivide(aX, 0, 1, mX1, mX2);
// If animation is at start/end, return t without easing
return (t) => t === 0 || t === 1 ? t : calcBezier(getTForX(t), mY1, mY2);
}
export { cubicBezier };

View File

@@ -0,0 +1,7 @@
import { cubicBezier } from './cubic-bezier.mjs';
const easeIn = cubicBezier(0.42, 0, 1, 1);
const easeOut = cubicBezier(0, 0, 0.58, 1);
const easeInOut = cubicBezier(0.42, 0, 0.58, 1);
export { easeIn, easeInOut, easeOut };

View File

@@ -0,0 +1,5 @@
// Accepts an easing function and returns a new one that outputs mirrored values for
// the second half of the animation. Turns easeIn into easeInOut.
const mirrorEasing = (easing) => (p) => p <= 0.5 ? easing(2 * p) / 2 : (2 - easing(2 * (1 - p))) / 2;
export { mirrorEasing };

View File

@@ -0,0 +1,5 @@
// Accepts an easing function and returns a new one that outputs reversed values.
// Turns easeIn into easeOut.
const reverseEasing = (easing) => (p) => 1 - easing(1 - p);
export { reverseEasing };

View File

@@ -0,0 +1,18 @@
import { spring } from '../../animation/generators/spring/index.mjs';
import { calcGeneratorDuration, maxGeneratorDuration } from '../../animation/generators/utils/calc-duration.mjs';
import { millisecondsToSeconds } from '../../utils/time-conversion.mjs';
/**
* Create a progress => progress easing function from a generator.
*/
function createGeneratorEasing(options, scale = 100) {
const generator = spring({ keyframes: [0, scale], ...options });
const duration = Math.min(calcGeneratorDuration(generator), maxGeneratorDuration);
return {
type: "keyframes",
ease: (progress) => generator.next(duration * progress).value / scale,
duration: millisecondsToSeconds(duration),
};
}
export { createGeneratorEasing };

View File

@@ -0,0 +1,8 @@
import { wrap } from '../../utils/wrap.mjs';
import { isEasingArray } from './is-easing-array.mjs';
function getEasingForSegment(easing, i) {
return isEasingArray(easing) ? easing[wrap(0, easing.length, i)] : easing;
}
export { getEasingForSegment };

View File

@@ -0,0 +1,3 @@
const isBezierDefinition = (easing) => Array.isArray(easing) && typeof easing[0] === "number";
export { isBezierDefinition };

View File

@@ -0,0 +1,5 @@
const isEasingArray = (ease) => {
return Array.isArray(ease) && typeof ease[0] !== "number";
};
export { isEasingArray };

View File

@@ -0,0 +1,37 @@
import { invariant } from '../../utils/errors.mjs';
import { cubicBezier } from '../cubic-bezier.mjs';
import { noop } from '../../utils/noop.mjs';
import { easeIn, easeInOut, easeOut } from '../ease.mjs';
import { circIn, circInOut, circOut } from '../circ.mjs';
import { backIn, backInOut, backOut } from '../back.mjs';
import { anticipate } from '../anticipate.mjs';
const easingLookup = {
linear: noop,
easeIn,
easeInOut,
easeOut,
circIn,
circInOut,
circOut,
backIn,
backInOut,
backOut,
anticipate,
};
const easingDefinitionToFunction = (definition) => {
if (Array.isArray(definition)) {
// If cubic bezier definition, create bezier curve
invariant(definition.length === 4, `Cubic bezier arrays must contain four numerical values.`);
const [x1, y1, x2, y2] = definition;
return cubicBezier(x1, y1, x2, y2);
}
else if (typeof definition === "string") {
// Else lookup from table
invariant(easingLookup[definition] !== undefined, `Invalid easing type '${definition}'`);
return easingLookup[definition];
}
return definition;
};
export { easingDefinitionToFunction };

View File

@@ -0,0 +1,6 @@
function addDomEvent(target, eventName, handler, options = { passive: true }) {
target.addEventListener(eventName, handler, options);
return () => target.removeEventListener(eventName, handler);
}
export { addDomEvent };

View File

@@ -0,0 +1,8 @@
import { addDomEvent } from './add-dom-event.mjs';
import { addPointerInfo } from './event-info.mjs';
function addPointerEvent(target, eventName, handler, options) {
return addDomEvent(target, eventName, addPointerInfo(handler), options);
}
export { addPointerEvent };

View File

@@ -0,0 +1,15 @@
import { isPrimaryPointer } from './utils/is-primary-pointer.mjs';
function extractEventInfo(event, pointType = "page") {
return {
point: {
x: event[pointType + "X"],
y: event[pointType + "Y"],
},
};
}
const addPointerInfo = (handler) => {
return (event) => isPrimaryPointer(event) && handler(event, extractEventInfo(event));
};
export { addPointerInfo, extractEventInfo };

View File

@@ -0,0 +1,34 @@
import { useEffect } from 'react';
import { addDomEvent } from './add-dom-event.mjs';
/**
* Attaches an event listener directly to the provided DOM element.
*
* Bypassing React's event system can be desirable, for instance when attaching non-passive
* event handlers.
*
* ```jsx
* const ref = useRef(null)
*
* useDomEvent(ref, 'wheel', onWheel, { passive: false })
*
* return <div ref={ref} />
* ```
*
* @param ref - React.RefObject that's been provided to the element you want to bind the listener to.
* @param eventName - Name of the event you want listen for.
* @param handler - Function to fire when receiving the event.
* @param options - Options to pass to `Event.addEventListener`.
*
* @public
*/
function useDomEvent(ref, eventName, handler, options) {
useEffect(() => {
const element = ref.current;
if (handler && element) {
return addDomEvent(element, eventName, handler, options);
}
}, [ref, eventName, handler, options]);
}
export { useDomEvent };

View File

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

View File

@@ -0,0 +1,60 @@
import { createRenderStep } from './render-step.mjs';
const stepsOrder = [
"prepare",
"read",
"update",
"preRender",
"render",
"postRender",
];
const maxElapsed = 40;
function createRenderBatcher(scheduleNextBatch, allowKeepAlive) {
let runNextFrame = false;
let useDefaultElapsed = true;
const state = {
delta: 0,
timestamp: 0,
isProcessing: false,
};
const steps = stepsOrder.reduce((acc, key) => {
acc[key] = createRenderStep(() => (runNextFrame = true));
return acc;
}, {});
const processStep = (stepId) => steps[stepId].process(state);
const processBatch = () => {
const timestamp = performance.now();
runNextFrame = false;
state.delta = useDefaultElapsed
? 1000 / 60
: Math.max(Math.min(timestamp - state.timestamp, maxElapsed), 1);
state.timestamp = timestamp;
state.isProcessing = true;
stepsOrder.forEach(processStep);
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) => stepsOrder.forEach((key) => steps[key].cancel(process));
return { schedule, cancel, state, steps };
}
export { createRenderBatcher, stepsOrder };

View File

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

View File

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

View File

@@ -0,0 +1,104 @@
class Queue {
constructor() {
this.order = [];
this.scheduled = new Set();
}
add(process) {
if (!this.scheduled.has(process)) {
this.scheduled.add(process);
this.order.push(process);
return true;
}
}
remove(process) {
const index = this.order.indexOf(process);
if (index !== -1) {
this.order.splice(index, 1);
this.scheduled.delete(process);
}
}
clear() {
this.order.length = 0;
this.scheduled.clear();
}
}
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 Queue();
let nextFrame = new Queue();
let numToRun = 0;
/**
* Track whether we're currently processing jobs in this step. This way
* we can decide whether to schedule new jobs for this frame or next.
*/
let isProcessing = false;
let flushNextFrame = false;
/**
* A set of processes which were marked keepAlive when scheduled.
*/
const toKeepAlive = new WeakSet();
const step = {
/**
* Schedule a process to run on the next frame.
*/
schedule: (callback, keepAlive = false, immediate = false) => {
const addToCurrentFrame = immediate && isProcessing;
const queue = addToCurrentFrame ? thisFrame : nextFrame;
if (keepAlive)
toKeepAlive.add(callback);
if (queue.add(callback) && addToCurrentFrame && isProcessing) {
// If we're adding it to the currently running queue, update its measured size
numToRun = thisFrame.order.length;
}
return callback;
},
/**
* Cancel the provided callback from running on the next frame.
*/
cancel: (callback) => {
nextFrame.remove(callback);
toKeepAlive.delete(callback);
},
/**
* Execute all schedule callbacks.
*/
process: (frameData) => {
/**
* If we're already processing we've probably been triggered by a flushSync
* inside an existing process. Instead of executing, mark flushNextFrame
* as true and ensure we flush the following frame at the end of this one.
*/
if (isProcessing) {
flushNextFrame = true;
return;
}
isProcessing = true;
[thisFrame, nextFrame] = [nextFrame, thisFrame];
// Clear the next frame queue
nextFrame.clear();
// Execute this frame
numToRun = thisFrame.order.length;
if (numToRun) {
for (let i = 0; i < numToRun; i++) {
const callback = thisFrame.order[i];
callback(frameData);
if (toKeepAlive.has(callback)) {
step.schedule(callback);
runNextFrame();
}
}
}
isProcessing = false;
if (flushNextFrame) {
flushNextFrame = false;
step.process(frameData);
}
},
};
return step;
}
export { createRenderStep };

View File

@@ -0,0 +1,481 @@
import { invariant } from '../../utils/errors.mjs';
import { PanSession } from '../pan/PanSession.mjs';
import { getGlobalLock } from './utils/lock.mjs';
import { isRefObject } from '../../utils/is-ref-object.mjs';
import { addPointerEvent } from '../../events/add-pointer-event.mjs';
import { applyConstraints, calcRelativeConstraints, resolveDragElastic, calcViewportConstraints, defaultElastic, rebaseAxisConstraints, calcOrigin } from './utils/constraints.mjs';
import { createBox } from '../../projection/geometry/models.mjs';
import { eachAxis } from '../../projection/utils/each-axis.mjs';
import { measurePageBox } from '../../projection/utils/measure.mjs';
import { extractEventInfo } from '../../events/event-info.mjs';
import { convertBoxToBoundingBox, convertBoundingBoxToBox } from '../../projection/geometry/conversion.mjs';
import { addDomEvent } from '../../events/add-dom-event.mjs';
import { calcLength } from '../../projection/geometry/delta-calc.mjs';
import { mix } from '../../utils/mix.mjs';
import { percent } from '../../value/types/numbers/units.mjs';
import { animateMotionValue } from '../../animation/interfaces/motion-value.mjs';
import { getContextWindow } from '../../utils/get-context-window.mjs';
import { frame } from '../../frameloop/frame.mjs';
const elementDragControls = new WeakMap();
/**
*
*/
// let latestPointerEvent: PointerEvent
class VisualElementDragControls {
constructor(visualElement) {
// This is a reference to the global drag gesture lock, ensuring only one component
// can "capture" the drag of one or both axes.
// TODO: Look into moving this into pansession?
this.openGlobalLock = null;
this.isDragging = false;
this.currentDirection = null;
this.originPoint = { x: 0, y: 0 };
/**
* The permitted boundaries of travel, in pixels.
*/
this.constraints = false;
this.hasMutatedConstraints = false;
/**
* The per-axis resolved elastic values.
*/
this.elastic = createBox();
this.visualElement = visualElement;
}
start(originEvent, { snapToCursor = false } = {}) {
/**
* Don't start dragging if this component is exiting
*/
const { presenceContext } = this.visualElement;
if (presenceContext && presenceContext.isPresent === false)
return;
const onSessionStart = (event) => {
const { dragSnapToOrigin } = this.getProps();
// Stop or pause any animations on both axis values immediately. This allows the user to throw and catch
// the component.
dragSnapToOrigin ? this.pauseAnimation() : this.stopAnimation();
if (snapToCursor) {
this.snapToCursor(extractEventInfo(event, "page").point);
}
};
const onStart = (event, info) => {
// Attempt to grab the global drag gesture lock - maybe make this part of PanSession
const { drag, dragPropagation, onDragStart } = this.getProps();
if (drag && !dragPropagation) {
if (this.openGlobalLock)
this.openGlobalLock();
this.openGlobalLock = getGlobalLock(drag);
// If we don 't have the lock, don't start dragging
if (!this.openGlobalLock)
return;
}
this.isDragging = true;
this.currentDirection = null;
this.resolveConstraints();
if (this.visualElement.projection) {
this.visualElement.projection.isAnimationBlocked = true;
this.visualElement.projection.target = undefined;
}
/**
* Record gesture origin
*/
eachAxis((axis) => {
let current = this.getAxisMotionValue(axis).get() || 0;
/**
* If the MotionValue is a percentage value convert to px
*/
if (percent.test(current)) {
const { projection } = this.visualElement;
if (projection && projection.layout) {
const measuredAxis = projection.layout.layoutBox[axis];
if (measuredAxis) {
const length = calcLength(measuredAxis);
current = length * (parseFloat(current) / 100);
}
}
}
this.originPoint[axis] = current;
});
// Fire onDragStart event
if (onDragStart) {
frame.update(() => onDragStart(event, info), false, true);
}
const { animationState } = this.visualElement;
animationState && animationState.setActive("whileDrag", true);
};
const onMove = (event, info) => {
// latestPointerEvent = event
const { dragPropagation, dragDirectionLock, onDirectionLock, onDrag, } = this.getProps();
// If we didn't successfully receive the gesture lock, early return.
if (!dragPropagation && !this.openGlobalLock)
return;
const { offset } = info;
// Attempt to detect drag direction if directionLock is true
if (dragDirectionLock && this.currentDirection === null) {
this.currentDirection = getCurrentDirection(offset);
// If we've successfully set a direction, notify listener
if (this.currentDirection !== null) {
onDirectionLock && onDirectionLock(this.currentDirection);
}
return;
}
// Update each point with the latest position
this.updateAxis("x", info.point, offset);
this.updateAxis("y", info.point, offset);
/**
* Ideally we would leave the renderer to fire naturally at the end of
* this frame but if the element is about to change layout as the result
* of a re-render we want to ensure the browser can read the latest
* bounding box to ensure the pointer and element don't fall out of sync.
*/
this.visualElement.render();
/**
* This must fire after the render call as it might trigger a state
* change which itself might trigger a layout update.
*/
onDrag && onDrag(event, info);
};
const onSessionEnd = (event, info) => this.stop(event, info);
const resumeAnimation = () => eachAxis((axis) => {
var _a;
return this.getAnimationState(axis) === "paused" &&
((_a = this.getAxisMotionValue(axis).animation) === null || _a === void 0 ? void 0 : _a.play());
});
const { dragSnapToOrigin } = this.getProps();
this.panSession = new PanSession(originEvent, {
onSessionStart,
onStart,
onMove,
onSessionEnd,
resumeAnimation,
}, {
transformPagePoint: this.visualElement.getTransformPagePoint(),
dragSnapToOrigin,
contextWindow: getContextWindow(this.visualElement),
});
}
stop(event, info) {
const isDragging = this.isDragging;
this.cancel();
if (!isDragging)
return;
const { velocity } = info;
this.startAnimation(velocity);
const { onDragEnd } = this.getProps();
if (onDragEnd) {
frame.update(() => onDragEnd(event, info));
}
}
cancel() {
this.isDragging = false;
const { projection, animationState } = this.visualElement;
if (projection) {
projection.isAnimationBlocked = false;
}
this.panSession && this.panSession.end();
this.panSession = undefined;
const { dragPropagation } = this.getProps();
if (!dragPropagation && this.openGlobalLock) {
this.openGlobalLock();
this.openGlobalLock = null;
}
animationState && animationState.setActive("whileDrag", false);
}
updateAxis(axis, _point, offset) {
const { drag } = this.getProps();
// If we're not dragging this axis, do an early return.
if (!offset || !shouldDrag(axis, drag, this.currentDirection))
return;
const axisValue = this.getAxisMotionValue(axis);
let next = this.originPoint[axis] + offset[axis];
// Apply constraints
if (this.constraints && this.constraints[axis]) {
next = applyConstraints(next, this.constraints[axis], this.elastic[axis]);
}
axisValue.set(next);
}
resolveConstraints() {
var _a;
const { dragConstraints, dragElastic } = this.getProps();
const layout = this.visualElement.projection &&
!this.visualElement.projection.layout
? this.visualElement.projection.measure(false)
: (_a = this.visualElement.projection) === null || _a === void 0 ? void 0 : _a.layout;
const prevConstraints = this.constraints;
if (dragConstraints && isRefObject(dragConstraints)) {
if (!this.constraints) {
this.constraints = this.resolveRefConstraints();
}
}
else {
if (dragConstraints && layout) {
this.constraints = calcRelativeConstraints(layout.layoutBox, dragConstraints);
}
else {
this.constraints = false;
}
}
this.elastic = resolveDragElastic(dragElastic);
/**
* If we're outputting to external MotionValues, we want to rebase the measured constraints
* from viewport-relative to component-relative.
*/
if (prevConstraints !== this.constraints &&
layout &&
this.constraints &&
!this.hasMutatedConstraints) {
eachAxis((axis) => {
if (this.getAxisMotionValue(axis)) {
this.constraints[axis] = rebaseAxisConstraints(layout.layoutBox[axis], this.constraints[axis]);
}
});
}
}
resolveRefConstraints() {
const { dragConstraints: constraints, onMeasureDragConstraints } = this.getProps();
if (!constraints || !isRefObject(constraints))
return false;
const constraintsElement = constraints.current;
invariant(constraintsElement !== null, "If `dragConstraints` is set as a React ref, that ref must be passed to another component's `ref` prop.");
const { projection } = this.visualElement;
// TODO
if (!projection || !projection.layout)
return false;
const constraintsBox = measurePageBox(constraintsElement, projection.root, this.visualElement.getTransformPagePoint());
let measuredConstraints = calcViewportConstraints(projection.layout.layoutBox, constraintsBox);
/**
* If there's an onMeasureDragConstraints listener we call it and
* if different constraints are returned, set constraints to that
*/
if (onMeasureDragConstraints) {
const userConstraints = onMeasureDragConstraints(convertBoxToBoundingBox(measuredConstraints));
this.hasMutatedConstraints = !!userConstraints;
if (userConstraints) {
measuredConstraints = convertBoundingBoxToBox(userConstraints);
}
}
return measuredConstraints;
}
startAnimation(velocity) {
const { drag, dragMomentum, dragElastic, dragTransition, dragSnapToOrigin, onDragTransitionEnd, } = this.getProps();
const constraints = this.constraints || {};
const momentumAnimations = eachAxis((axis) => {
if (!shouldDrag(axis, drag, this.currentDirection)) {
return;
}
let transition = (constraints && constraints[axis]) || {};
if (dragSnapToOrigin)
transition = { min: 0, max: 0 };
/**
* Overdamp the boundary spring if `dragElastic` is disabled. There's still a frame
* of spring animations so we should look into adding a disable spring option to `inertia`.
* We could do something here where we affect the `bounceStiffness` and `bounceDamping`
* using the value of `dragElastic`.
*/
const bounceStiffness = dragElastic ? 200 : 1000000;
const bounceDamping = dragElastic ? 40 : 10000000;
const inertia = {
type: "inertia",
velocity: dragMomentum ? velocity[axis] : 0,
bounceStiffness,
bounceDamping,
timeConstant: 750,
restDelta: 1,
restSpeed: 10,
...dragTransition,
...transition,
};
// If we're not animating on an externally-provided `MotionValue` we can use the
// component's animation controls which will handle interactions with whileHover (etc),
// otherwise we just have to animate the `MotionValue` itself.
return this.startAxisValueAnimation(axis, inertia);
});
// Run all animations and then resolve the new drag constraints.
return Promise.all(momentumAnimations).then(onDragTransitionEnd);
}
startAxisValueAnimation(axis, transition) {
const axisValue = this.getAxisMotionValue(axis);
return axisValue.start(animateMotionValue(axis, axisValue, 0, transition));
}
stopAnimation() {
eachAxis((axis) => this.getAxisMotionValue(axis).stop());
}
pauseAnimation() {
eachAxis((axis) => { var _a; return (_a = this.getAxisMotionValue(axis).animation) === null || _a === void 0 ? void 0 : _a.pause(); });
}
getAnimationState(axis) {
var _a;
return (_a = this.getAxisMotionValue(axis).animation) === null || _a === void 0 ? void 0 : _a.state;
}
/**
* Drag works differently depending on which props are provided.
*
* - If _dragX and _dragY are provided, we output the gesture delta directly to those motion values.
* - Otherwise, we apply the delta to the x/y motion values.
*/
getAxisMotionValue(axis) {
const dragKey = "_drag" + axis.toUpperCase();
const props = this.visualElement.getProps();
const externalMotionValue = props[dragKey];
return externalMotionValue
? externalMotionValue
: this.visualElement.getValue(axis, (props.initial ? props.initial[axis] : undefined) || 0);
}
snapToCursor(point) {
eachAxis((axis) => {
const { drag } = this.getProps();
// If we're not dragging this axis, do an early return.
if (!shouldDrag(axis, drag, this.currentDirection))
return;
const { projection } = this.visualElement;
const axisValue = this.getAxisMotionValue(axis);
if (projection && projection.layout) {
const { min, max } = projection.layout.layoutBox[axis];
axisValue.set(point[axis] - mix(min, max, 0.5));
}
});
}
/**
* When the viewport resizes we want to check if the measured constraints
* have changed and, if so, reposition the element within those new constraints
* relative to where it was before the resize.
*/
scalePositionWithinConstraints() {
if (!this.visualElement.current)
return;
const { drag, dragConstraints } = this.getProps();
const { projection } = this.visualElement;
if (!isRefObject(dragConstraints) || !projection || !this.constraints)
return;
/**
* Stop current animations as there can be visual glitching if we try to do
* this mid-animation
*/
this.stopAnimation();
/**
* Record the relative position of the dragged element relative to the
* constraints box and save as a progress value.
*/
const boxProgress = { x: 0, y: 0 };
eachAxis((axis) => {
const axisValue = this.getAxisMotionValue(axis);
if (axisValue) {
const latest = axisValue.get();
boxProgress[axis] = calcOrigin({ min: latest, max: latest }, this.constraints[axis]);
}
});
/**
* Update the layout of this element and resolve the latest drag constraints
*/
const { transformTemplate } = this.visualElement.getProps();
this.visualElement.current.style.transform = transformTemplate
? transformTemplate({}, "")
: "none";
projection.root && projection.root.updateScroll();
projection.updateLayout();
this.resolveConstraints();
/**
* For each axis, calculate the current progress of the layout axis
* within the new constraints.
*/
eachAxis((axis) => {
if (!shouldDrag(axis, drag, null))
return;
/**
* Calculate a new transform based on the previous box progress
*/
const axisValue = this.getAxisMotionValue(axis);
const { min, max } = this.constraints[axis];
axisValue.set(mix(min, max, boxProgress[axis]));
});
}
addListeners() {
if (!this.visualElement.current)
return;
elementDragControls.set(this.visualElement, this);
const element = this.visualElement.current;
/**
* Attach a pointerdown event listener on this DOM element to initiate drag tracking.
*/
const stopPointerListener = addPointerEvent(element, "pointerdown", (event) => {
const { drag, dragListener = true } = this.getProps();
drag && dragListener && this.start(event);
});
const measureDragConstraints = () => {
const { dragConstraints } = this.getProps();
if (isRefObject(dragConstraints)) {
this.constraints = this.resolveRefConstraints();
}
};
const { projection } = this.visualElement;
const stopMeasureLayoutListener = projection.addEventListener("measure", measureDragConstraints);
if (projection && !projection.layout) {
projection.root && projection.root.updateScroll();
projection.updateLayout();
}
measureDragConstraints();
/**
* Attach a window resize listener to scale the draggable target within its defined
* constraints as the window resizes.
*/
const stopResizeListener = addDomEvent(window, "resize", () => this.scalePositionWithinConstraints());
/**
* If the element's layout changes, calculate the delta and apply that to
* the drag gesture's origin point.
*/
const stopLayoutUpdateListener = projection.addEventListener("didUpdate", (({ delta, hasLayoutChanged }) => {
if (this.isDragging && hasLayoutChanged) {
eachAxis((axis) => {
const motionValue = this.getAxisMotionValue(axis);
if (!motionValue)
return;
this.originPoint[axis] += delta[axis].translate;
motionValue.set(motionValue.get() + delta[axis].translate);
});
this.visualElement.render();
}
}));
return () => {
stopResizeListener();
stopPointerListener();
stopMeasureLayoutListener();
stopLayoutUpdateListener && stopLayoutUpdateListener();
};
}
getProps() {
const props = this.visualElement.getProps();
const { drag = false, dragDirectionLock = false, dragPropagation = false, dragConstraints = false, dragElastic = defaultElastic, dragMomentum = true, } = props;
return {
...props,
drag,
dragDirectionLock,
dragPropagation,
dragConstraints,
dragElastic,
dragMomentum,
};
}
}
function shouldDrag(direction, drag, currentDirection) {
return ((drag === true || drag === direction) &&
(currentDirection === null || currentDirection === direction));
}
/**
* Based on an x/y offset determine the current drag direction. If both axis' offsets are lower
* than the provided threshold, return `null`.
*
* @param offset - The x/y offset from origin.
* @param lockThreshold - (Optional) - the minimum absolute offset before we can determine a drag direction.
*/
function getCurrentDirection(offset, lockThreshold = 10) {
let direction = null;
if (Math.abs(offset.y) > lockThreshold) {
direction = "y";
}
else if (Math.abs(offset.x) > lockThreshold) {
direction = "x";
}
return direction;
}
export { VisualElementDragControls, elementDragControls };

View File

@@ -0,0 +1,27 @@
import { Feature } from '../../motion/features/Feature.mjs';
import { noop } from '../../utils/noop.mjs';
import { VisualElementDragControls } from './VisualElementDragControls.mjs';
class DragGesture extends Feature {
constructor(node) {
super(node);
this.removeGroupControls = noop;
this.removeListeners = noop;
this.controls = new VisualElementDragControls(node);
}
mount() {
// If we've been provided a DragControls for manual control over the drag gesture,
// subscribe this component to it on mount.
const { dragControls } = this.node.getProps();
if (dragControls) {
this.removeGroupControls = dragControls.subscribe(this.controls);
}
this.removeListeners = this.controls.addListeners() || noop;
}
unmount() {
this.removeGroupControls();
this.removeListeners();
}
}
export { DragGesture };

View File

@@ -0,0 +1,88 @@
import { useConstant } from '../../utils/use-constant.mjs';
/**
* Can manually trigger a drag gesture on one or more `drag`-enabled `motion` components.
*
* ```jsx
* const dragControls = useDragControls()
*
* function startDrag(event) {
* dragControls.start(event, { snapToCursor: true })
* }
*
* return (
* <>
* <div onPointerDown={startDrag} />
* <motion.div drag="x" dragControls={dragControls} />
* </>
* )
* ```
*
* @public
*/
class DragControls {
constructor() {
this.componentControls = new Set();
}
/**
* Subscribe a component's internal `VisualElementDragControls` to the user-facing API.
*
* @internal
*/
subscribe(controls) {
this.componentControls.add(controls);
return () => this.componentControls.delete(controls);
}
/**
* Start a drag gesture on every `motion` component that has this set of drag controls
* passed into it via the `dragControls` prop.
*
* ```jsx
* dragControls.start(e, {
* snapToCursor: true
* })
* ```
*
* @param event - PointerEvent
* @param options - Options
*
* @public
*/
start(event, options) {
this.componentControls.forEach((controls) => {
controls.start(event.nativeEvent || event, options);
});
}
}
const createDragControls = () => new DragControls();
/**
* Usually, dragging is initiated by pressing down on a `motion` component with a `drag` prop
* and moving it. For some use-cases, for instance clicking at an arbitrary point on a video scrubber, we
* might want to initiate that dragging from a different component than the draggable one.
*
* By creating a `dragControls` using the `useDragControls` hook, we can pass this into
* the draggable component's `dragControls` prop. It exposes a `start` method
* that can start dragging from pointer events on other components.
*
* ```jsx
* const dragControls = useDragControls()
*
* function startDrag(event) {
* dragControls.start(event, { snapToCursor: true })
* }
*
* return (
* <>
* <div onPointerDown={startDrag} />
* <motion.div drag="x" dragControls={dragControls} />
* </>
* )
* ```
*
* @public
*/
function useDragControls() {
return useConstant(createDragControls);
}
export { DragControls, useDragControls };

View File

@@ -0,0 +1,125 @@
import { progress } from '../../../utils/progress.mjs';
import { calcLength } from '../../../projection/geometry/delta-calc.mjs';
import { clamp } from '../../../utils/clamp.mjs';
import { mix } from '../../../utils/mix.mjs';
/**
* Apply constraints to a point. These constraints are both physical along an
* axis, and an elastic factor that determines how much to constrain the point
* by if it does lie outside the defined parameters.
*/
function applyConstraints(point, { min, max }, elastic) {
if (min !== undefined && point < min) {
// If we have a min point defined, and this is outside of that, constrain
point = elastic ? mix(min, point, elastic.min) : Math.max(point, min);
}
else if (max !== undefined && point > max) {
// If we have a max point defined, and this is outside of that, constrain
point = elastic ? mix(max, point, elastic.max) : Math.min(point, max);
}
return point;
}
/**
* Calculate constraints in terms of the viewport when defined relatively to the
* measured axis. This is measured from the nearest edge, so a max constraint of 200
* on an axis with a max value of 300 would return a constraint of 500 - axis length
*/
function calcRelativeAxisConstraints(axis, min, max) {
return {
min: min !== undefined ? axis.min + min : undefined,
max: max !== undefined
? axis.max + max - (axis.max - axis.min)
: undefined,
};
}
/**
* Calculate constraints in terms of the viewport when
* defined relatively to the measured bounding box.
*/
function calcRelativeConstraints(layoutBox, { top, left, bottom, right }) {
return {
x: calcRelativeAxisConstraints(layoutBox.x, left, right),
y: calcRelativeAxisConstraints(layoutBox.y, top, bottom),
};
}
/**
* Calculate viewport constraints when defined as another viewport-relative axis
*/
function calcViewportAxisConstraints(layoutAxis, constraintsAxis) {
let min = constraintsAxis.min - layoutAxis.min;
let max = constraintsAxis.max - layoutAxis.max;
// If the constraints axis is actually smaller than the layout axis then we can
// flip the constraints
if (constraintsAxis.max - constraintsAxis.min <
layoutAxis.max - layoutAxis.min) {
[min, max] = [max, min];
}
return { min, max };
}
/**
* Calculate viewport constraints when defined as another viewport-relative box
*/
function calcViewportConstraints(layoutBox, constraintsBox) {
return {
x: calcViewportAxisConstraints(layoutBox.x, constraintsBox.x),
y: calcViewportAxisConstraints(layoutBox.y, constraintsBox.y),
};
}
/**
* Calculate a transform origin relative to the source axis, between 0-1, that results
* in an asthetically pleasing scale/transform needed to project from source to target.
*/
function calcOrigin(source, target) {
let origin = 0.5;
const sourceLength = calcLength(source);
const targetLength = calcLength(target);
if (targetLength > sourceLength) {
origin = progress(target.min, target.max - sourceLength, source.min);
}
else if (sourceLength > targetLength) {
origin = progress(source.min, source.max - targetLength, target.min);
}
return clamp(0, 1, origin);
}
/**
* Rebase the calculated viewport constraints relative to the layout.min point.
*/
function rebaseAxisConstraints(layout, constraints) {
const relativeConstraints = {};
if (constraints.min !== undefined) {
relativeConstraints.min = constraints.min - layout.min;
}
if (constraints.max !== undefined) {
relativeConstraints.max = constraints.max - layout.min;
}
return relativeConstraints;
}
const defaultElastic = 0.35;
/**
* Accepts a dragElastic prop and returns resolved elastic values for each axis.
*/
function resolveDragElastic(dragElastic = defaultElastic) {
if (dragElastic === false) {
dragElastic = 0;
}
else if (dragElastic === true) {
dragElastic = defaultElastic;
}
return {
x: resolveAxisElastic(dragElastic, "left", "right"),
y: resolveAxisElastic(dragElastic, "top", "bottom"),
};
}
function resolveAxisElastic(dragElastic, minLabel, maxLabel) {
return {
min: resolvePointElastic(dragElastic, minLabel),
max: resolvePointElastic(dragElastic, maxLabel),
};
}
function resolvePointElastic(dragElastic, label) {
return typeof dragElastic === "number"
? dragElastic
: dragElastic[label] || 0;
}
export { applyConstraints, calcOrigin, calcRelativeAxisConstraints, calcRelativeConstraints, calcViewportAxisConstraints, calcViewportConstraints, defaultElastic, rebaseAxisConstraints, resolveAxisElastic, resolveDragElastic, resolvePointElastic };

View File

@@ -0,0 +1,53 @@
function createLock(name) {
let lock = null;
return () => {
const openLock = () => {
lock = null;
};
if (lock === null) {
lock = name;
return openLock;
}
return false;
};
}
const globalHorizontalLock = createLock("dragHorizontal");
const globalVerticalLock = createLock("dragVertical");
function getGlobalLock(drag) {
let lock = false;
if (drag === "y") {
lock = globalVerticalLock();
}
else if (drag === "x") {
lock = globalHorizontalLock();
}
else {
const openHorizontal = globalHorizontalLock();
const openVertical = globalVerticalLock();
if (openHorizontal && openVertical) {
lock = () => {
openHorizontal();
openVertical();
};
}
else {
// Release the locks because we don't use them
if (openHorizontal)
openHorizontal();
if (openVertical)
openVertical();
}
}
return lock;
}
function isDragActive() {
// Check the gesture lock - if we get it, it means no drag gesture is active
// and we can safely fire the tap gesture.
const openGestureLock = getGlobalLock(true);
if (!openGestureLock)
return true;
openGestureLock();
return false;
}
export { createLock, getGlobalLock, isDragActive };

View File

@@ -0,0 +1,41 @@
import { addDomEvent } from '../events/add-dom-event.mjs';
import { Feature } from '../motion/features/Feature.mjs';
import { pipe } from '../utils/pipe.mjs';
class FocusGesture extends Feature {
constructor() {
super(...arguments);
this.isActive = false;
}
onFocus() {
let isFocusVisible = false;
/**
* If this element doesn't match focus-visible then don't
* apply whileHover. But, if matches throws that focus-visible
* is not a valid selector then in that browser outline styles will be applied
* to the element by default and we want to match that behaviour with whileFocus.
*/
try {
isFocusVisible = this.node.current.matches(":focus-visible");
}
catch (e) {
isFocusVisible = true;
}
if (!isFocusVisible || !this.node.animationState)
return;
this.node.animationState.setActive("whileFocus", true);
this.isActive = true;
}
onBlur() {
if (!this.isActive || !this.node.animationState)
return;
this.node.animationState.setActive("whileFocus", false);
this.isActive = false;
}
mount() {
this.unmount = pipe(addDomEvent(this.node.current, "focus", () => this.onFocus()), addDomEvent(this.node.current, "blur", () => this.onBlur()));
}
unmount() { }
}
export { FocusGesture };

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