first commit

This commit is contained in:
2026-06-24 09:48:54 +02:00
commit 41e62ddcad
33739 changed files with 4266226 additions and 0 deletions
+21
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.
+111
View File
@@ -0,0 +1,111 @@
<p align="center">
<img width="100" height="100" alt="Motion logo" src="https://user-images.githubusercontent.com/7850794/164965523-3eced4c4-6020-467e-acde-f11b7900ad62.png" />
</p>
<h1 align="center">Motion for React</h1>
<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/motiondotdev" target="_blank">
<img src="https://img.shields.io/twitter/follow/framer.svg?style=social&label=Follow" />
</a>
</p>
<br>
<hr>
<br>
Motion for React is an open source, production-ready library thats designed for all creative developers.
It's the only animation library with a hybrid engine, combining the power of JavaScript animations with the performance of native browser APIs.
It looks like this:
```jsx
<motion.div animate={{ x: 0 }} />
```
It does all this:
- [Springs](https://motion.dev/docs/react-transitions#spring)
- [Keyframes](https://motion.dev/docs/react-animation#keyframes)
- [Layout animations](https://motion.dev/docs/react-layout-animations)
- [Shared layout animations](https://motion.dev/docs/react-layout-animations#shared-layout-animations)
- [Gestures (drag/tap/hover)](https://motion.dev/docs/react-gestures)
- [Scroll animations](https://motion.dev/docs/react-scroll-animations)
- [SVG paths](https://motion.dev/docs/react-animation#svg-line-drawing)
- [Exit animations](https://motion.dev/docs/react-animation#exit-animations)
- [Server-side rendering](https://motion.dev/docs/react-motion-component#server-side-rendering)
- [Independent transforms](https://motion.dev/docs/react-motion-component#style)
- [Orchestrate animations across components](https://motion.dev/docs/react-animation#orchestration)
- [CSS variables](https://motion.dev/docs/react-animation#css-variables)
...and a whole lot more.
## Get started
### 🐇 Quick start
Install `motion` via your package manager:
```
npm install motion
```
Then import the `motion` component:
```jsx
import { motion } from "motion/react"
export function Component({ isVisible }) {
return <motion.div animate={{ opacity: isVisible ? 1 : 0 }} />
}
```
### 💎 Contribute
- Want to contribute to Motion? Our [contributing guide](https://github.com/motiondivision/motion/blob/master/CONTRIBUTING.md) has you covered.
### 👩🏻‍⚖️ License
- Motion for React is MIT licensed.
## ✨ Sponsors
Motion is sustainable thanks to the kind support of its sponsors.
### Partners
#### Framer
Motion powers Framer animations, the web builder for creative pros. Design and ship your dream site. Zero code, maximum speed.
<a href="https://www.framer.com?utm_source=motion-readme">
<img alt="Framer" src="https://github.com/user-attachments/assets/0404c7a1-c29d-4785-89ae-aae315f3c759" width="300px" height="200px">
</a>
### Platinum
<a href="https://syntax.fm"><img alt="Syntax.fm" src="https://github.com/user-attachments/assets/ab852bfe-b36c-490b-b98d-4061158c4863" width="300px" height="200px"></a> <a href="https://tailwindcss.com"><img alt="Tailwind" src="https://github.com/user-attachments/assets/c0496f09-b8ee-4bc4-85ab-83a071bbbdec" width="300px" height="200px"></a> <a href="https://emilkowal.ski"><img alt="Emil Kowalski" src="https://github.com/user-attachments/assets/29f56b1a-37fb-4695-a6a6-151f6c24864f" width="300px" height="200px"></a> <a href="https://linear.app"><img alt="Linear" src="https://github.com/user-attachments/assets/a93710bb-d8ed-40e3-b0fb-1c5b3e2b16bb" width="300px" height="200px"></a>
### Gold
<a href="https://vercel.com"><img alt="Vercel" src="https://github.com/user-attachments/assets/23cb1e37-fa67-49ad-8f77-7f4b8eaba325" width="225px" height="150px"></a> <a href="https://liveblocks.io"><img alt="Liveblocks" src="https://github.com/user-attachments/assets/31436a47-951e-4eab-9a68-bdd54ccf9444" width="225px" height="150px"></a> <a href="https://lu.ma"><img alt="Luma" src="https://github.com/user-attachments/assets/4fae0c9d-de0f-4042-9cd6-e07885d028a9" width="225px" height="150px"></a>
### Silver
<a href="https://www.frontend.fyi/?utm_source=motion"><img alt="Frontend.fyi" src="https://github.com/user-attachments/assets/07d23aa5-69db-44a0-849d-90177e6fc817" width="150px" height="100px"></a> <a href="https://statamic.com"><img alt="Statamic" src="https://github.com/user-attachments/assets/5d28f090-bdd9-4b31-b134-fb2b94ca636f" width="150px" height="100px"></a> <a href="https://firecrawl.dev"><img alt="Firecrawl" src="https://github.com/user-attachments/assets/cba90e54-1329-4353-8fba-85beef4d2ee9" width="150px" height="100px"></a> <a href="https://puzzmo.com"><img alt="Puzzmo" src="https://github.com/user-attachments/assets/aa2d5586-e5e2-43b9-8446-db456e4b0758" width="150px" height="100px"></a> <a href="https://buildui.com"><img alt="Build UI" src="https://github.com/user-attachments/assets/024bfcd5-50e8-4b3d-a115-d5c6d6030d1c" width="150px" height="100px"></a> <a href="https://hover.dev"><img alt="Hover" src="https://github.com/user-attachments/assets/4715b555-d2ac-4cb7-9f35-d36d708827b3" width="150px" height="100px"></a>
### Personal
- [Nusu](https://x.com/nusualabuga)
- [OlegWock](https://sinja.io)
- [Lambert Weller](https://github.com/l-mbert)
- [Jake LeBoeuf](https://jklb.wf)
- [Han Lee](https://github.com/hahnlee)
+1
View File
@@ -0,0 +1 @@
This directory is a fallback for `exports["./client"]` in the root `framer-motion` `package.json`.
+6
View File
@@ -0,0 +1,6 @@
{
"private": true,
"types": "../dist/client.d.ts",
"main": "../dist/cjs/client.js",
"module": "../dist/es/client.mjs"
}
+9700
View File
File diff suppressed because it is too large Load Diff
+644
View File
@@ -0,0 +1,644 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var motionDom = require('motion-dom');
var motionUtils = require('motion-utils');
const wrap = (min, max, v) => {
const rangeSize = max - min;
return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min;
};
const isEasingArray = (ease) => {
return Array.isArray(ease) && typeof ease[0] !== "number";
};
function getEasingForSegment(easing, i) {
return isEasingArray(easing) ? easing[wrap(0, easing.length, i)] : easing;
}
/*
Value in range from progress
Given a lower limit and an upper limit, we return the value within
that range as expressed by progress (usually a number from 0 to 1)
So progress = 0.5 would change
from -------- to
to
from ---- to
E.g. from = 10, to = 20, progress = 0.5 => 15
@param [number]: Lower limit of range
@param [number]: Upper limit of range
@param [number]: The progress between lower and upper limits expressed 0-1
@return [number]: Value as calculated from progress within range (not limited within range)
*/
const mixNumber = (from, to, progress) => {
return from + (to - from) * progress;
};
function fillOffset(offset, remaining) {
const min = offset[offset.length - 1];
for (let i = 1; i <= remaining; i++) {
const offsetProgress = motionUtils.progress(0, remaining, i);
offset.push(mixNumber(min, 1, offsetProgress));
}
}
function defaultOffset(arr) {
const offset = [0];
fillOffset(offset, arr.length - 1);
return offset;
}
const isMotionValue = (value) => Boolean(value && value.getVelocity);
function isDOMKeyframes(keyframes) {
return typeof keyframes === "object" && !Array.isArray(keyframes);
}
function resolveSubjects(subject, keyframes, scope, selectorCache) {
if (typeof subject === "string" && isDOMKeyframes(keyframes)) {
return motionDom.resolveElements(subject, scope, selectorCache);
}
else if (subject instanceof NodeList) {
return Array.from(subject);
}
else if (Array.isArray(subject)) {
return subject;
}
else {
return [subject];
}
}
function calculateRepeatDuration(duration, repeat, _repeatDelay) {
return duration * (repeat + 1);
}
/**
* 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;
}
}
function removeItem(arr, item) {
const index = arr.indexOf(item);
if (index > -1)
arr.splice(index, 1);
}
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: mixNumber(startTime, endTime, offset[i]),
easing: getEasingForSegment(easing, i),
});
}
}
/**
* Take an array of times that represent repeated keyframes. For instance
* if we have original times of [0, 0.5, 1] then our repeated times will
* be [0, 0.5, 1, 1, 1.5, 2]. Loop over the times and scale them back
* down to a 0-1 scale.
*/
function normalizeTimes(times, repeat) {
for (let i = 0; i < times.length; i++) {
times[i] = times[i] / (repeat + 1);
}
}
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;
}
}
const defaultSegmentEasing = "easeInOut";
const MAX_REPEAT = 20;
function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...sequenceTransition } = {}, scope, generators) {
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, numSubjects = 0) => {
const valueKeyframesAsList = keyframesAsList(valueKeyframes);
const { delay = 0, times = defaultOffset(valueKeyframesAsList), type = "keyframes", repeat, repeatType, repeatDelay = 0, ...remainingTransition } = valueTransition;
let { ease = defaultTransition.ease || "easeOut", duration } = valueTransition;
/**
* Resolve stagger() if defined.
*/
const calculatedDelay = typeof delay === "function"
? delay(elementIndex, numSubjects)
: delay;
/**
* If this animation should and can use a spring, generate a spring easing function.
*/
const numKeyframes = valueKeyframesAsList.length;
const createGenerator = motionDom.isGenerator(type)
? type
: generators === null || generators === void 0 ? void 0 : generators[type];
if (numKeyframes <= 2 && createGenerator) {
/**
* 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 = motionUtils.secondsToMilliseconds(duration);
}
const springEasing = motionDom.createGeneratorEasing(springTransition, absoluteDelta, createGenerator);
ease = springEasing.ease;
duration = springEasing.duration;
}
duration !== null && duration !== void 0 ? duration : (duration = defaultDuration);
const startTime = currentTime + calculatedDelay;
/**
* 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);
/**
* Handle repeat options
*/
if (repeat) {
motionUtils.invariant(repeat < MAX_REPEAT, "Repeat count too high, must be less than 20");
duration = calculateRepeatDuration(duration, repeat);
const originalKeyframes = [...valueKeyframesAsList];
const originalTimes = [...times];
ease = Array.isArray(ease) ? [...ease] : [ease];
const originalEase = [...ease];
for (let repeatIndex = 0; repeatIndex < repeat; repeatIndex++) {
valueKeyframesAsList.push(...originalKeyframes);
for (let keyframeIndex = 0; keyframeIndex < originalKeyframes.length; keyframeIndex++) {
times.push(originalTimes[keyframeIndex] + (repeatIndex + 1));
ease.push(keyframeIndex === 0
? "linear"
: getEasingForSegment(originalEase, keyframeIndex - 1));
}
}
normalizeTimes(times, repeat);
}
const targetTime = startTime + duration;
/**
* 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 {
const subjects = resolveSubjects(subject, keyframes, scope, elementCache);
const numSubjects = subjects.length;
/**
* For every element in this segment, process the defined values.
*/
for (let subjectIndex = 0; subjectIndex < numSubjects; subjectIndex++) {
/**
* Cast necessary, but we know these are of this type
*/
keyframes = keyframes;
transition = transition;
const thisSubject = subjects[subjectIndex];
const subjectSequence = getSubjectSequence(thisSubject, sequences);
for (const key in keyframes) {
resolveValueSequence(keyframes[key], getValueTransition(transition, key), getValueSequence(key, subjectSequence), subjectIndex, numSubjects);
}
}
}
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(motionUtils.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 && transition[key]
? {
...transition,
...transition[key],
}
: { ...transition };
}
const isNumber = (keyframe) => typeof keyframe === "number";
const isNumberKeyframesArray = (keyframes) => keyframes.every(isNumber);
function startWaapiAnimation(element, valueName, keyframes, { delay = 0, duration = 300, repeat = 0, repeatType = "loop", ease = "easeInOut", times, } = {}) {
const keyframeOptions = { [valueName]: keyframes };
if (times)
keyframeOptions.offset = times;
const easing = motionDom.mapEasingToNativeEasing(ease, duration);
/**
* 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",
});
}
const createUnitType = (unit) => ({
test: (v) => typeof v === "string" && v.endsWith(unit) && v.split(" ").length === 1,
parse: parseFloat,
transform: (v) => `${v}${unit}`,
});
const px = /*@__PURE__*/ createUnitType("px");
const browserNumberValueTypes = {
// Border props
borderWidth: px,
borderTopWidth: px,
borderRightWidth: px,
borderBottomWidth: px,
borderLeftWidth: px,
borderRadius: px,
radius: px,
borderTopLeftRadius: px,
borderTopRightRadius: px,
borderBottomRightRadius: px,
borderBottomLeftRadius: px,
// Positioning props
width: px,
maxWidth: px,
height: px,
maxHeight: px,
top: px,
right: px,
bottom: px,
left: px,
// Spacing props
padding: px,
paddingTop: px,
paddingRight: px,
paddingBottom: px,
paddingLeft: px,
margin: px,
marginTop: px,
marginRight: px,
marginBottom: px,
marginLeft: px,
// Misc
backgroundPositionX: px,
backgroundPositionY: px,
};
const isNotNull = (value) => value !== null;
function getFinalKeyframe(keyframes, { repeat, repeatType = "loop" }, finalKeyframe) {
const resolvedKeyframes = keyframes.filter(isNotNull);
const index = repeat && repeatType !== "loop" && repeat % 2 === 1
? 0
: resolvedKeyframes.length - 1;
return !index || finalKeyframe === undefined
? resolvedKeyframes[index]
: finalKeyframe;
}
function setCSSVar(element, name, value) {
element.style.setProperty(`--${name}`, value);
}
function setStyle(element, name, value) {
element.style[name] = value;
}
const supportsPartialKeyframes = /*@__PURE__*/ motionUtils.memo(() => {
try {
document.createElement("div").animate({ opacity: [1] });
}
catch (e) {
return false;
}
return true;
});
const supportsWaapi = /*@__PURE__*/ motionUtils.memo(() => Object.hasOwnProperty.call(Element.prototype, "animate"));
const state = new WeakMap();
function hydrateKeyframes(valueName, keyframes, read) {
for (let i = 0; i < keyframes.length; i++) {
if (keyframes[i] === null) {
keyframes[i] = i === 0 ? read() : keyframes[i - 1];
}
if (typeof keyframes[i] === "number" &&
browserNumberValueTypes[valueName]) {
keyframes[i] = browserNumberValueTypes[valueName].transform(keyframes[i]);
}
}
if (!supportsPartialKeyframes() && keyframes.length < 2) {
keyframes.unshift(read());
}
}
const defaultEasing = "easeOut";
function getElementAnimationState(element) {
const animationState = state.get(element) || new Map();
state.set(element, animationState);
return state.get(element);
}
class NativeAnimation extends motionDom.NativeAnimationControls {
constructor(element, valueName, valueKeyframes, options) {
const isCSSVar = valueName.startsWith("--");
motionUtils.invariant(typeof options.type !== "string", `animateMini doesn't support "type" as a string. Did you mean to import { spring } from "framer-motion"?`);
const existingAnimation = getElementAnimationState(element).get(valueName);
existingAnimation && existingAnimation.stop();
const readInitialKeyframe = () => {
return valueName.startsWith("--")
? element.style.getPropertyValue(valueName)
: window.getComputedStyle(element)[valueName];
};
if (!Array.isArray(valueKeyframes)) {
valueKeyframes = [valueKeyframes];
}
hydrateKeyframes(valueName, valueKeyframes, readInitialKeyframe);
// TODO: Replace this with toString()?
if (motionDom.isGenerator(options.type)) {
const generatorOptions = motionDom.createGeneratorEasing(options, 100, options.type);
options.ease = motionDom.supportsLinearEasing()
? generatorOptions.ease
: defaultEasing;
options.duration = motionUtils.secondsToMilliseconds(generatorOptions.duration);
options.type = "keyframes";
}
else {
options.ease = options.ease || defaultEasing;
}
const onFinish = () => {
this.setValue(element, valueName, getFinalKeyframe(valueKeyframes, options));
this.cancel();
this.resolveFinishedPromise();
};
const init = () => {
this.setValue = isCSSVar ? setCSSVar : setStyle;
this.options = options;
this.updateFinishedPromise();
this.removeAnimation = () => {
const elementState = state.get(element);
elementState && elementState.delete(valueName);
};
};
if (!supportsWaapi()) {
super();
init();
onFinish();
}
else {
super(startWaapiAnimation(element, valueName, valueKeyframes, options));
init();
if (options.autoplay === false) {
this.animation.pause();
}
this.animation.onfinish = onFinish;
getElementAnimationState(element).set(valueName, this);
}
}
/**
* Allows the returned animation to be awaited or promise-chained. Currently
* resolves when the animation finishes at all but in a future update could/should
* reject if its cancels.
*/
then(resolve, reject) {
return this.currentFinishedPromise.then(resolve, reject);
}
updateFinishedPromise() {
this.currentFinishedPromise = new Promise((resolve) => {
this.resolveFinishedPromise = resolve;
});
}
play() {
if (this.state === "finished") {
this.updateFinishedPromise();
}
super.play();
}
cancel() {
this.removeAnimation();
super.cancel();
}
}
function animateElements(elementOrSelector, keyframes, options, scope) {
const elements = motionDom.resolveElements(elementOrSelector, scope);
const numElements = elements.length;
motionUtils.invariant(Boolean(numElements), "No valid element provided.");
const animations = [];
for (let i = 0; i < numElements; i++) {
const element = elements[i];
const elementTransition = { ...options };
/**
* Resolve stagger function if provided.
*/
if (typeof elementTransition.delay === "function") {
elementTransition.delay = elementTransition.delay(i, numElements);
}
for (const valueName in keyframes) {
const valueKeyframes = keyframes[valueName];
const valueOptions = {
...motionDom.getValueTransition(elementTransition, valueName),
};
valueOptions.duration = valueOptions.duration
? motionUtils.secondsToMilliseconds(valueOptions.duration)
: valueOptions.duration;
valueOptions.delay = motionUtils.secondsToMilliseconds(valueOptions.delay || 0);
animations.push(new NativeAnimation(element, valueName, valueKeyframes, valueOptions));
}
}
return animations;
}
function animateSequence(definition, options) {
const animations = [];
createAnimationsFromSequence(definition, options).forEach(({ keyframes, transition }, element) => {
animations.push(...animateElements(element, keyframes, transition));
});
return new motionDom.GroupPlaybackControls(animations);
}
const createScopedWaapiAnimate = (scope) => {
function scopedAnimate(elementOrSelector, keyframes, options) {
return new motionDom.GroupPlaybackControls(animateElements(elementOrSelector, keyframes, options, scope));
}
return scopedAnimate;
};
const animateMini = /*@__PURE__*/ createScopedWaapiAnimate();
exports.animate = animateMini;
exports.animateSequence = animateSequence;
+5926
View File
File diff suppressed because it is too large Load Diff
+12631
View File
File diff suppressed because it is too large Load Diff
+1881
View File
File diff suppressed because it is too large Load Diff
+273
View File
@@ -0,0 +1,273 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var react = require('react');
var motionDom = require('motion-dom');
var motionUtils = require('motion-utils');
/**
* Creates a constant value over the lifecycle of a component.
*
* Even if `useMemo` is provided an empty array as its final argument, it doesn't offer
* a guarantee that it won't re-run for performance reasons later on. By using `useConstant`
* you can ensure that initialisers don't execute twice or more.
*/
function useConstant(init) {
const ref = react.useRef(null);
if (ref.current === null) {
ref.current = init();
}
return ref.current;
}
function useUnmountEffect(callback) {
return react.useEffect(() => () => callback(), []);
}
function startWaapiAnimation(element, valueName, keyframes, { delay = 0, duration = 300, repeat = 0, repeatType = "loop", ease = "easeInOut", times, } = {}) {
const keyframeOptions = { [valueName]: keyframes };
if (times)
keyframeOptions.offset = times;
const easing = motionDom.mapEasingToNativeEasing(ease, duration);
/**
* 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",
});
}
const createUnitType = (unit) => ({
test: (v) => typeof v === "string" && v.endsWith(unit) && v.split(" ").length === 1,
parse: parseFloat,
transform: (v) => `${v}${unit}`,
});
const px = /*@__PURE__*/ createUnitType("px");
const browserNumberValueTypes = {
// Border props
borderWidth: px,
borderTopWidth: px,
borderRightWidth: px,
borderBottomWidth: px,
borderLeftWidth: px,
borderRadius: px,
radius: px,
borderTopLeftRadius: px,
borderTopRightRadius: px,
borderBottomRightRadius: px,
borderBottomLeftRadius: px,
// Positioning props
width: px,
maxWidth: px,
height: px,
maxHeight: px,
top: px,
right: px,
bottom: px,
left: px,
// Spacing props
padding: px,
paddingTop: px,
paddingRight: px,
paddingBottom: px,
paddingLeft: px,
margin: px,
marginTop: px,
marginRight: px,
marginBottom: px,
marginLeft: px,
// Misc
backgroundPositionX: px,
backgroundPositionY: px,
};
const isNotNull = (value) => value !== null;
function getFinalKeyframe(keyframes, { repeat, repeatType = "loop" }, finalKeyframe) {
const resolvedKeyframes = keyframes.filter(isNotNull);
const index = repeat && repeatType !== "loop" && repeat % 2 === 1
? 0
: resolvedKeyframes.length - 1;
return !index || finalKeyframe === undefined
? resolvedKeyframes[index]
: finalKeyframe;
}
function setCSSVar(element, name, value) {
element.style.setProperty(`--${name}`, value);
}
function setStyle(element, name, value) {
element.style[name] = value;
}
const supportsPartialKeyframes = /*@__PURE__*/ motionUtils.memo(() => {
try {
document.createElement("div").animate({ opacity: [1] });
}
catch (e) {
return false;
}
return true;
});
const supportsWaapi = /*@__PURE__*/ motionUtils.memo(() => Object.hasOwnProperty.call(Element.prototype, "animate"));
const state = new WeakMap();
function hydrateKeyframes(valueName, keyframes, read) {
for (let i = 0; i < keyframes.length; i++) {
if (keyframes[i] === null) {
keyframes[i] = i === 0 ? read() : keyframes[i - 1];
}
if (typeof keyframes[i] === "number" &&
browserNumberValueTypes[valueName]) {
keyframes[i] = browserNumberValueTypes[valueName].transform(keyframes[i]);
}
}
if (!supportsPartialKeyframes() && keyframes.length < 2) {
keyframes.unshift(read());
}
}
const defaultEasing = "easeOut";
function getElementAnimationState(element) {
const animationState = state.get(element) || new Map();
state.set(element, animationState);
return state.get(element);
}
class NativeAnimation extends motionDom.NativeAnimationControls {
constructor(element, valueName, valueKeyframes, options) {
const isCSSVar = valueName.startsWith("--");
motionUtils.invariant(typeof options.type !== "string", `animateMini doesn't support "type" as a string. Did you mean to import { spring } from "framer-motion"?`);
const existingAnimation = getElementAnimationState(element).get(valueName);
existingAnimation && existingAnimation.stop();
const readInitialKeyframe = () => {
return valueName.startsWith("--")
? element.style.getPropertyValue(valueName)
: window.getComputedStyle(element)[valueName];
};
if (!Array.isArray(valueKeyframes)) {
valueKeyframes = [valueKeyframes];
}
hydrateKeyframes(valueName, valueKeyframes, readInitialKeyframe);
// TODO: Replace this with toString()?
if (motionDom.isGenerator(options.type)) {
const generatorOptions = motionDom.createGeneratorEasing(options, 100, options.type);
options.ease = motionDom.supportsLinearEasing()
? generatorOptions.ease
: defaultEasing;
options.duration = motionUtils.secondsToMilliseconds(generatorOptions.duration);
options.type = "keyframes";
}
else {
options.ease = options.ease || defaultEasing;
}
const onFinish = () => {
this.setValue(element, valueName, getFinalKeyframe(valueKeyframes, options));
this.cancel();
this.resolveFinishedPromise();
};
const init = () => {
this.setValue = isCSSVar ? setCSSVar : setStyle;
this.options = options;
this.updateFinishedPromise();
this.removeAnimation = () => {
const elementState = state.get(element);
elementState && elementState.delete(valueName);
};
};
if (!supportsWaapi()) {
super();
init();
onFinish();
}
else {
super(startWaapiAnimation(element, valueName, valueKeyframes, options));
init();
if (options.autoplay === false) {
this.animation.pause();
}
this.animation.onfinish = onFinish;
getElementAnimationState(element).set(valueName, this);
}
}
/**
* Allows the returned animation to be awaited or promise-chained. Currently
* resolves when the animation finishes at all but in a future update could/should
* reject if its cancels.
*/
then(resolve, reject) {
return this.currentFinishedPromise.then(resolve, reject);
}
updateFinishedPromise() {
this.currentFinishedPromise = new Promise((resolve) => {
this.resolveFinishedPromise = resolve;
});
}
play() {
if (this.state === "finished") {
this.updateFinishedPromise();
}
super.play();
}
cancel() {
this.removeAnimation();
super.cancel();
}
}
function animateElements(elementOrSelector, keyframes, options, scope) {
const elements = motionDom.resolveElements(elementOrSelector, scope);
const numElements = elements.length;
motionUtils.invariant(Boolean(numElements), "No valid element provided.");
const animations = [];
for (let i = 0; i < numElements; i++) {
const element = elements[i];
const elementTransition = { ...options };
/**
* Resolve stagger function if provided.
*/
if (typeof elementTransition.delay === "function") {
elementTransition.delay = elementTransition.delay(i, numElements);
}
for (const valueName in keyframes) {
const valueKeyframes = keyframes[valueName];
const valueOptions = {
...motionDom.getValueTransition(elementTransition, valueName),
};
valueOptions.duration = valueOptions.duration
? motionUtils.secondsToMilliseconds(valueOptions.duration)
: valueOptions.duration;
valueOptions.delay = motionUtils.secondsToMilliseconds(valueOptions.delay || 0);
animations.push(new NativeAnimation(element, valueName, valueKeyframes, valueOptions));
}
}
return animations;
}
const createScopedWaapiAnimate = (scope) => {
function scopedAnimate(elementOrSelector, keyframes, options) {
return new motionDom.GroupPlaybackControls(animateElements(elementOrSelector, keyframes, options, scope));
}
return scopedAnimate;
};
function useAnimateMini() {
const scope = useConstant(() => ({
current: null, // Will be hydrated by React
animations: [],
}));
const animate = useConstant(() => createScopedWaapiAnimate(scope));
useUnmountEffect(() => {
scope.animations.forEach((animation) => animation.stop());
});
return [scope, animate];
}
exports.useAnimate = useAnimateMini;
+2554
View File
File diff suppressed because it is too large Load Diff
+230
View File
@@ -0,0 +1,230 @@
import { AnimationPlaybackControls, TransformProperties, AnimationPlaybackOptions, Transition, UnresolvedValueKeyframe, ElementOrSelector, DOMKeyframesDefinition, AnimationOptions, GroupPlaybackControls } from 'motion-dom';
/**
* @public
*/
type Subscriber<T> = (v: T) => void;
interface MotionValueEventCallbacks<V> {
animationStart: () => void;
animationComplete: () => void;
animationCancel: () => void;
change: (latestValue: V) => void;
renderRequest: () => void;
}
interface ResolvedValues {
[key: string]: string | number;
}
interface Owner {
current: HTMLElement | unknown;
getProps: () => {
onUpdate?: (latest: ResolvedValues) => void;
transformTemplate?: (transform: TransformProperties, generatedTransform: string) => string;
};
}
/**
* `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 Motion
* and therefore has no external listeners. It is therefore safe to animate via WAAPI.
*/
owner?: Owner;
/**
* The current state of the `MotionValue`.
*/
private current;
/**
* The previous state of the `MotionValue`.
*/
private prev;
/**
* The previous state of the `MotionValue` at the end of the previous frame.
*/
private prevFrameValue;
/**
* The last time the `MotionValue` was updated.
*/
updatedAt: number;
/**
* The time `prevFrameValue` was updated.
*/
prevUpdatedAt: number | undefined;
private stopPassiveEffect?;
/**
* A reference to the currently-controlling animation.
*/
animation?: AnimationPlaybackControls;
setCurrent(current: V): void;
setPrevFrameValue(prevFrameValue?: V | undefined): void;
/**
* 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, endAnimation?: boolean): void;
updateAndNotify: (v: V, render?: boolean) => void;
/**
* Returns the latest state of `MotionValue`
*
* @returns - The latest state of `MotionValue`
*
* @public
*/
get(): NonNullable<V>;
/**
* @public
*/
getPrevious(): V | undefined;
/**
* 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;
}
type GenericKeyframesTarget<V> = V[] | Array<null | V>;
type ObjectTarget<O> = {
[K in keyof O]?: O[K] | GenericKeyframesTarget<O[K]>;
};
type SequenceTime = number | "<" | `+${number}` | `-${number}` | `${string}`;
type SequenceLabel = string;
interface SequenceLabelWithTime {
name: SequenceLabel;
at: SequenceTime;
}
interface At {
at?: SequenceTime;
}
type MotionValueSegment = [
MotionValue,
UnresolvedValueKeyframe | UnresolvedValueKeyframe[]
];
type MotionValueSegmentWithTransition = [
MotionValue,
UnresolvedValueKeyframe | UnresolvedValueKeyframe[],
Transition & At
];
type DOMSegment = [ElementOrSelector, DOMKeyframesDefinition];
type DOMSegmentWithTransition = [
ElementOrSelector,
DOMKeyframesDefinition,
AnimationOptions & At
];
type ObjectSegment<O extends {} = {}> = [O, ObjectTarget<O>];
type ObjectSegmentWithTransition<O extends {} = {}> = [
O,
ObjectTarget<O>,
AnimationOptions & At
];
type Segment = ObjectSegment | ObjectSegmentWithTransition | SequenceLabel | SequenceLabelWithTime | MotionValueSegment | MotionValueSegmentWithTransition | DOMSegment | DOMSegmentWithTransition;
type AnimationSequence = Segment[];
interface SequenceOptions extends AnimationPlaybackOptions {
delay?: number;
duration?: number;
defaultTransition?: Transition;
}
declare function animateSequence(definition: AnimationSequence, options?: SequenceOptions): GroupPlaybackControls;
declare const animateMini: (elementOrSelector: ElementOrSelector, keyframes: DOMKeyframesDefinition, options?: AnimationOptions) => AnimationPlaybackControls;
export { animateMini as animate, animateSequence };
+1
View File
File diff suppressed because one or more lines are too long
+579
View File
@@ -0,0 +1,579 @@
import * as motion_dom from 'motion-dom';
import { AnimationPlaybackControls, TransformProperties, UnresolvedValueKeyframe, Transition, ElementOrSelector, DOMKeyframesDefinition, AnimationOptions, AnimationPlaybackOptions, Easing, AnimationScope, ValueAnimationTransition, EasingFunction, ProgressTimeline, EasingModifier, ValueAnimationOptions, KeyframeGenerator, DynamicOption } from 'motion-dom';
export * from 'motion-dom';
export { BezierDefinition, Easing, EasingDefinition, EasingFunction, EasingModifier, isDragActive } from 'motion-dom';
export { invariant, noop, progress } from 'motion-utils';
/**
* @public
*/
type Subscriber<T> = (v: T) => void;
/**
* @public
*/
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;
}
interface ResolvedValues {
[key: string]: string | number;
}
interface Owner {
current: HTMLElement | unknown;
getProps: () => {
onUpdate?: (latest: ResolvedValues) => void;
transformTemplate?: (transform: TransformProperties, generatedTransform: string) => string;
};
}
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 Motion
* and therefore has no external listeners. It is therefore safe to animate via WAAPI.
*/
owner?: Owner;
/**
* The current state of the `MotionValue`.
*/
private current;
/**
* The previous state of the `MotionValue`.
*/
private prev;
/**
* The previous state of the `MotionValue` at the end of the previous frame.
*/
private prevFrameValue;
/**
* The last time the `MotionValue` was updated.
*/
updatedAt: number;
/**
* The time `prevFrameValue` was updated.
*/
prevUpdatedAt: number | undefined;
private stopPassiveEffect?;
/**
* A reference to the currently-controlling animation.
*/
animation?: AnimationPlaybackControls;
setCurrent(current: V): void;
setPrevFrameValue(prevFrameValue?: V | undefined): void;
/**
* 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, endAnimation?: boolean): void;
updateAndNotify: (v: V, render?: boolean) => void;
/**
* Returns the latest state of `MotionValue`
*
* @returns - The latest state of `MotionValue`
*
* @public
*/
get(): NonNullable<V>;
/**
* @public
*/
getPrevious(): V | undefined;
/**
* 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>;
interface Point {
x: number;
y: number;
}
type GenericKeyframesTarget<V> = V[] | Array<null | V>;
type ObjectTarget<O> = {
[K in keyof O]?: O[K] | GenericKeyframesTarget<O[K]>;
};
type SequenceTime = number | "<" | `+${number}` | `-${number}` | `${string}`;
type SequenceLabel = string;
interface SequenceLabelWithTime {
name: SequenceLabel;
at: SequenceTime;
}
interface At {
at?: SequenceTime;
}
type MotionValueSegment = [
MotionValue,
UnresolvedValueKeyframe | UnresolvedValueKeyframe[]
];
type MotionValueSegmentWithTransition = [
MotionValue,
UnresolvedValueKeyframe | UnresolvedValueKeyframe[],
Transition & At
];
type DOMSegment = [ElementOrSelector, DOMKeyframesDefinition];
type DOMSegmentWithTransition = [
ElementOrSelector,
DOMKeyframesDefinition,
AnimationOptions & At
];
type ObjectSegment<O extends {} = {}> = [O, ObjectTarget<O>];
type ObjectSegmentWithTransition<O extends {} = {}> = [
O,
ObjectTarget<O>,
AnimationOptions & At
];
type Segment = ObjectSegment | ObjectSegmentWithTransition | SequenceLabel | SequenceLabelWithTime | MotionValueSegment | MotionValueSegmentWithTransition | DOMSegment | DOMSegmentWithTransition;
type AnimationSequence = Segment[];
interface SequenceOptions extends AnimationPlaybackOptions {
delay?: number;
duration?: number;
defaultTransition?: Transition;
}
interface AbsoluteKeyframe {
value: string | number | null;
at: number;
easing?: Easing;
}
type ValueSequence = AbsoluteKeyframe[];
interface SequenceMap {
[key: string]: ValueSequence;
}
type ResolvedAnimationDefinition = {
keyframes: {
[key: string]: UnresolvedValueKeyframe[];
};
transition: {
[key: string]: Transition;
};
};
type ResolvedAnimationDefinitions = Map<Element | MotionValue, ResolvedAnimationDefinition>;
/**
* Creates an animation function that is optionally scoped
* to a specific element.
*/
declare function createScopedAnimate(scope?: AnimationScope): {
(sequence: AnimationSequence, options?: SequenceOptions): AnimationPlaybackControls;
(value: string | MotionValue<string>, keyframes: string | GenericKeyframesTarget<string>, options?: ValueAnimationTransition<string>): AnimationPlaybackControls;
(value: number | MotionValue<number>, keyframes: number | GenericKeyframesTarget<number>, options?: ValueAnimationTransition<number>): AnimationPlaybackControls;
<V>(value: V | MotionValue<V>, keyframes: V | GenericKeyframesTarget<V>, options?: ValueAnimationTransition<V>): AnimationPlaybackControls;
(element: ElementOrSelector, keyframes: DOMKeyframesDefinition, options?: AnimationOptions): AnimationPlaybackControls;
<O extends {}>(object: O | O[], keyframes: ObjectTarget<O>, options?: AnimationOptions): AnimationPlaybackControls;
};
declare const animate: {
(sequence: AnimationSequence, options?: SequenceOptions): AnimationPlaybackControls;
(value: string | MotionValue<string>, keyframes: string | GenericKeyframesTarget<string>, options?: ValueAnimationTransition<string>): AnimationPlaybackControls;
(value: number | MotionValue<number>, keyframes: number | GenericKeyframesTarget<number>, options?: ValueAnimationTransition<number>): AnimationPlaybackControls;
<V>(value: V | MotionValue<V>, keyframes: V | GenericKeyframesTarget<V>, options?: ValueAnimationTransition<V>): AnimationPlaybackControls;
(element: ElementOrSelector, keyframes: DOMKeyframesDefinition, options?: AnimationOptions): AnimationPlaybackControls;
<O extends {}>(object: O | O[], keyframes: ObjectTarget<O>, options?: AnimationOptions): AnimationPlaybackControls;
};
declare const animateMini: (elementOrSelector: ElementOrSelector, keyframes: DOMKeyframesDefinition, options?: AnimationOptions) => AnimationPlaybackControls;
interface ScrollOptions {
source?: HTMLElement;
container?: HTMLElement;
target?: Element;
axis?: "x" | "y";
offset?: ScrollOffset;
}
type OnScrollProgress = (progress: number) => void;
type OnScrollWithInfo = (progress: number, info: ScrollInfo) => void;
type OnScroll = OnScrollProgress | OnScrollWithInfo;
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;
}
type OnScrollInfo = (info: ScrollInfo) => void;
type SupportedEdgeUnit = "px" | "vw" | "vh" | "%";
type EdgeUnit = `${number}${SupportedEdgeUnit}`;
type NamedEdges = "start" | "end" | "center";
type EdgeString = NamedEdges | EdgeUnit | `${number}`;
type Edge = EdgeString | number;
type ProgressIntersection = [number, number];
type Intersection = `${Edge} ${Edge}`;
type ScrollOffset = Array<Edge | Intersection | ProgressIntersection>;
interface ScrollInfoOptions {
container?: HTMLElement;
target?: Element;
axis?: "x" | "y";
offset?: ScrollOffset;
}
declare global {
interface Window {
ScrollTimeline: ScrollTimeline;
}
}
declare class ScrollTimeline implements ProgressTimeline {
constructor(options: ScrollOptions);
currentTime: null | {
value: number;
};
cancel?: VoidFunction;
}
declare function scroll(onScroll: OnScroll | AnimationPlaybackControls, { axis, ...options }?: ScrollOptions): VoidFunction;
declare function scrollInfo(onScroll: OnScrollInfo, { container, ...options }?: ScrollInfoOptions): () => void;
type ViewChangeHandler = (entry: IntersectionObserverEntry) => void;
type MarginValue = `${number}${"px" | "%"}`;
type MarginType = MarginValue | `${MarginValue} ${MarginValue}` | `${MarginValue} ${MarginValue} ${MarginValue}` | `${MarginValue} ${MarginValue} ${MarginValue} ${MarginValue}`;
interface InViewOptions {
root?: Element | Document;
margin?: MarginType;
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: motion_dom.EasingFunction;
declare const backInOut: motion_dom.EasingFunction;
declare const circIn: EasingFunction;
declare const circOut: EasingFunction;
declare const circInOut: EasingFunction;
declare function cubicBezier(mX1: number, mY1: number, mX2: number, mY2: number): (t: number) => number;
declare const easeIn: (t: number) => number;
declare const easeOut: (t: number) => number;
declare const easeInOut: (t: number) => number;
declare const mirrorEasing: EasingModifier;
declare const reverseEasing: EasingModifier;
type Direction = "start" | "end";
declare function steps(numSteps: number, direction?: Direction): EasingFunction;
declare function inertia({ keyframes, velocity, power, timeConstant, bounceDamping, bounceStiffness, modifyTarget, min, max, restDelta, restSpeed, }: ValueAnimationOptions<number>): KeyframeGenerator<number>;
declare function keyframes<T extends string | number>({ duration, keyframes: keyframeValues, times, ease, }: ValueAnimationOptions<T>): KeyframeGenerator<T>;
declare function spring(optionsOrVisualDuration?: ValueAnimationOptions<number> | number, bounce?: number): KeyframeGenerator<number>;
type StaggerOrigin = "first" | "last" | "center" | number;
type StaggerOptions = {
startDelay?: number;
from?: StaggerOrigin;
ease?: Easing;
};
declare function stagger(duration?: number, { startDelay, from, ease }?: StaggerOptions): DynamicOption<number>;
type Process = (data: FrameData) => void;
type Schedule = (process: Process, keepAlive?: boolean, immediate?: boolean) => Process;
interface Step {
schedule: Schedule;
cancel: (process: Process) => void;
process: (data: FrameData) => void;
}
type StepId = "read" | "resolveKeyframes" | "update" | "preRender" | "render" | "postRender";
type Batcher = {
[key in StepId]: Schedule;
};
type Steps = {
[key in StepId]: Step;
};
interface FrameData {
delta: number;
timestamp: number;
isProcessing: boolean;
}
declare const frame: Batcher;
declare const cancelFrame: (process: Process) => void;
declare const frameData: FrameData;
declare const frameSteps: Steps;
/**
* An eventloop-synchronous alternative to performance.now().
*
* Ensures that time measurements remain consistent within a synchronous context.
* Usually calling performance.now() twice within the same synchronous context
* will return different values which isn't useful for animations when we're usually
* trying to sync animations to the same frame.
*/
declare const time: {
now: () => number;
set: (newTime: number) => void;
};
declare const clamp: (min: number, max: number, v: number) => number;
type DelayedFunction = (overshoot: number) => void;
declare function delayInSeconds(callback: DelayedFunction, timeout: number): () => void;
declare const distance: (a: number, b: number) => number;
declare function distance2D(a: Point, b: Point): number;
type Mix<T> = (v: number) => T;
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;
type Mixer<T> = (p: number) => T;
declare function mix<T>(from: T, to: T): Mixer<T>;
declare function mix(from: number, to: number, p: number): number;
declare const pipe: (...transformers: Function[]) => Function;
/**
* @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 wrap: (min: number, max: number, v: number) => number;
/**
* @deprecated
*
* Import as `frame` instead.
*/
declare const sync: Batcher;
/**
* @deprecated
*
* Use cancelFrame(callback) instead.
*/
declare const cancelSync: Record<string, (process: Process) => void>;
export { type AbsoluteKeyframe, type AnimationSequence, type At, type DOMSegment, type DOMSegmentWithTransition, type DelayedFunction, type Direction, type InterpolateOptions, type MixerFactory, MotionValue, type MotionValueSegment, type MotionValueSegmentWithTransition, type ObjectSegment, type ObjectSegmentWithTransition, type ObjectTarget, type PassiveEffect, type ResolvedAnimationDefinition, type ResolvedAnimationDefinitions, type Segment, type SequenceLabel, type SequenceLabelWithTime, type SequenceMap, type SequenceOptions, type SequenceTime, type Subscriber, type ValueSequence, animate, animateMini, anticipate, backIn, backInOut, backOut, cancelFrame, cancelSync, circIn, circInOut, circOut, clamp, createScopedAnimate, cubicBezier, delayInSeconds as delay, distance, distance2D, easeIn, easeInOut, easeOut, frame, frameData, frameSteps, inView, inertia, interpolate, keyframes, mirrorEasing, mix, motionValue, pipe, reverseEasing, scroll, scrollInfo, spring, stagger, steps, sync, time, transform, wrap };
+1
View File
File diff suppressed because one or more lines are too long
+34
View File
@@ -0,0 +1,34 @@
import { GroupPlaybackControls } from 'motion-dom';
import { animateSequence } from './sequence.mjs';
import { animateSubject } from './subject.mjs';
function isSequence(value) {
return Array.isArray(value) && value.some(Array.isArray);
}
/**
* Creates an animation function that is optionally scoped
* to a specific element.
*/
function createScopedAnimate(scope) {
/**
* Implementation
*/
function scopedAnimate(subjectOrSequence, optionsOrKeyframes, options) {
let animations = [];
if (isSequence(subjectOrSequence)) {
animations = animateSequence(subjectOrSequence, optionsOrKeyframes, scope);
}
else {
animations = animateSubject(subjectOrSequence, optionsOrKeyframes, options, scope);
}
const animation = new GroupPlaybackControls(animations);
if (scope) {
scope.animations.push(animation);
}
return animation;
}
return scopedAnimate;
}
const animate = createScopedAnimate();
export { animate, createScopedAnimate };
@@ -0,0 +1,19 @@
import { resolveElements } from 'motion-dom';
import { isDOMKeyframes } from '../utils/is-dom-keyframes.mjs';
function resolveSubjects(subject, keyframes, scope, selectorCache) {
if (typeof subject === "string" && isDOMKeyframes(keyframes)) {
return resolveElements(subject, scope, selectorCache);
}
else if (subject instanceof NodeList) {
return Array.from(subject);
}
else if (Array.isArray(subject)) {
return subject;
}
else {
return [subject];
}
}
export { resolveSubjects };
+14
View File
@@ -0,0 +1,14 @@
import { spring } from '../generators/spring/index.mjs';
import { createAnimationsFromSequence } from '../sequence/create.mjs';
import { animateSubject } from './subject.mjs';
function animateSequence(sequence, options, scope) {
const animations = [];
const animationDefinitions = createAnimationsFromSequence(sequence, options, scope, { spring });
animationDefinitions.forEach(({ keyframes, transition }, subject) => {
animations.push(...animateSubject(subject, keyframes, transition));
});
return animations;
}
export { animateSequence };
+11
View File
@@ -0,0 +1,11 @@
import { motionValue } from '../../value/index.mjs';
import { isMotionValue } from '../../value/utils/is-motion-value.mjs';
import { animateMotionValue } from '../interfaces/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 };
+52
View File
@@ -0,0 +1,52 @@
import { invariant } from 'motion-utils';
import { visualElementStore } from '../../render/store.mjs';
import { isMotionValue } from '../../value/utils/is-motion-value.mjs';
import { animateTarget } from '../interfaces/visual-element-target.mjs';
import { createDOMVisualElement, createObjectVisualElement } from '../utils/create-visual-element.mjs';
import { isDOMKeyframes } from '../utils/is-dom-keyframes.mjs';
import { resolveSubjects } from './resolve-subjects.mjs';
import { animateSingleValue } from './single-value.mjs';
function isSingleValue(subject, keyframes) {
return (isMotionValue(subject) ||
typeof subject === "number" ||
(typeof subject === "string" && !isDOMKeyframes(keyframes)));
}
/**
* Implementation
*/
function animateSubject(subject, keyframes, options, scope) {
const animations = [];
if (isSingleValue(subject, keyframes)) {
animations.push(animateSingleValue(subject, isDOMKeyframes(keyframes)
? keyframes.default || keyframes
: keyframes, options ? options.default || options : options));
}
else {
const subjects = resolveSubjects(subject, keyframes, scope);
const numSubjects = subjects.length;
invariant(Boolean(numSubjects), "No valid elements provided.");
for (let i = 0; i < numSubjects; i++) {
const thisSubject = subjects[i];
const createVisualElement = thisSubject instanceof Element
? createDOMVisualElement
: createObjectVisualElement;
if (!visualElementStore.has(thisSubject)) {
createVisualElement(thisSubject);
}
const visualElement = visualElementStore.get(thisSubject);
const transition = { ...options };
/**
* Resolve stagger function if provided.
*/
if ("delay" in transition &&
typeof transition.delay === "function") {
transition.delay = transition.delay(i, numSubjects);
}
animations.push(...animateTarget(visualElement, { ...keyframes, transition }, {}));
}
}
return animations;
}
export { animateSubject };
@@ -0,0 +1,317 @@
import { supportsLinearEasing, attachTimeline, isGenerator, isWaapiSupportedEasing } from 'motion-dom';
import { millisecondsToSeconds, secondsToMilliseconds, noop } from 'motion-utils';
import { anticipate } from '../../easing/anticipate.mjs';
import { backInOut } from '../../easing/back.mjs';
import { circInOut } from '../../easing/circ.mjs';
import { DOMKeyframesResolver } from '../../render/dom/DOMKeyframesResolver.mjs';
import { BaseAnimation } from './BaseAnimation.mjs';
import { MainThreadAnimation } from './MainThreadAnimation.mjs';
import { acceleratedValues } from './utils/accelerated-values.mjs';
import { startWaapiAnimation } from './waapi/index.mjs';
import { getFinalKeyframe } from './waapi/utils/get-final-keyframe.mjs';
import { supportsWaapi } from './waapi/utils/supports-waapi.mjs';
/**
* 10ms is chosen here as it strikes a balance between smooth
* results (more than one keyframe per frame at 60fps) and
* keyframe quantity.
*/
const sampleDelta = 10; //ms
/**
* Implement a practical max duration for keyframe generation
* to prevent infinite loops
*/
const maxDuration = 20000;
/**
* Check if an animation can run natively via WAAPI or requires pregenerated keyframes.
* WAAPI doesn't support spring or function easings so we run these as JS animation before
* handing off.
*/
function requiresPregeneratedKeyframes(options) {
return (isGenerator(options.type) ||
options.type === "spring" ||
!isWaapiSupportedEasing(options.ease));
}
function pregenerateKeyframes(keyframes, options) {
/**
* Create a main-thread animation to pregenerate keyframes.
* We sample this at regular intervals to generate keyframes that we then
* linearly interpolate between.
*/
const sampleAnimation = new MainThreadAnimation({
...options,
keyframes,
repeat: 0,
delay: 0,
isGenerator: true,
});
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;
}
return {
times: undefined,
keyframes: pregeneratedKeyframes,
duration: t - sampleDelta,
ease: "linear",
};
}
const unsupportedEasingFunctions = {
anticipate,
backInOut,
circInOut,
};
function isUnsupportedEase(key) {
return key in unsupportedEasingFunctions;
}
class AcceleratedAnimation extends BaseAnimation {
constructor(options) {
super(options);
const { name, motionValue, element, keyframes } = this.options;
this.resolver = new DOMKeyframesResolver(keyframes, (resolvedKeyframes, finalKeyframe) => this.onKeyframesResolved(resolvedKeyframes, finalKeyframe), name, motionValue, element);
this.resolver.scheduleResolve();
}
initPlayback(keyframes, finalKeyframe) {
let { duration = 300, times, ease, type, motionValue, name, startTime, } = this.options;
/**
* If element has since been unmounted, return false to indicate
* the animation failed to initialised.
*/
if (!motionValue.owner || !motionValue.owner.current) {
return false;
}
/**
* If the user has provided an easing function name that isn't supported
* by WAAPI (like "anticipate"), we need to provide the corressponding
* function. This will later get converted to a linear() easing function.
*/
if (typeof ease === "string" &&
supportsLinearEasing() &&
isUnsupportedEase(ease)) {
ease = unsupportedEasingFunctions[ease];
}
/**
* If this animation needs pre-generated keyframes then generate.
*/
if (requiresPregeneratedKeyframes(this.options)) {
const { onComplete, onUpdate, motionValue, element, ...options } = this.options;
const pregeneratedAnimation = pregenerateKeyframes(keyframes, options);
keyframes = pregeneratedAnimation.keyframes;
// If this is a very short animation, ensure we have
// at least two keyframes to animate between as older browsers
// can't animate between a single keyframe.
if (keyframes.length === 1) {
keyframes[1] = keyframes[0];
}
duration = pregeneratedAnimation.duration;
times = pregeneratedAnimation.times;
ease = pregeneratedAnimation.ease;
type = "keyframes";
}
const animation = startWaapiAnimation(motionValue.owner.current, name, keyframes, { ...this.options, duration, times, ease });
// Override the browser calculated startTime with one synchronised to other JS
// and WAAPI animations starting this event loop.
animation.startTime = startTime !== null && startTime !== void 0 ? startTime : this.calcStartTime();
if (this.pendingTimeline) {
attachTimeline(animation, this.pendingTimeline);
this.pendingTimeline = undefined;
}
else {
/**
* 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 = () => {
const { onComplete } = this.options;
motionValue.set(getFinalKeyframe(keyframes, this.options, finalKeyframe));
onComplete && onComplete();
this.cancel();
this.resolveFinishedPromise();
};
}
return {
animation,
duration,
times,
type,
ease,
keyframes: keyframes,
};
}
get duration() {
const { resolved } = this;
if (!resolved)
return 0;
const { duration } = resolved;
return millisecondsToSeconds(duration);
}
get time() {
const { resolved } = this;
if (!resolved)
return 0;
const { animation } = resolved;
return millisecondsToSeconds(animation.currentTime || 0);
}
set time(newTime) {
const { resolved } = this;
if (!resolved)
return;
const { animation } = resolved;
animation.currentTime = secondsToMilliseconds(newTime);
}
get speed() {
const { resolved } = this;
if (!resolved)
return 1;
const { animation } = resolved;
return animation.playbackRate;
}
set speed(newSpeed) {
const { resolved } = this;
if (!resolved)
return;
const { animation } = resolved;
animation.playbackRate = newSpeed;
}
get state() {
const { resolved } = this;
if (!resolved)
return "idle";
const { animation } = resolved;
return animation.playState;
}
get startTime() {
const { resolved } = this;
if (!resolved)
return null;
const { animation } = resolved;
// Coerce to number as TypeScript incorrectly types this
// as CSSNumberish
return animation.startTime;
}
/**
* Replace the default DocumentTimeline with another AnimationTimeline.
* Currently used for scroll animations.
*/
attachTimeline(timeline) {
if (!this._resolved) {
this.pendingTimeline = timeline;
}
else {
const { resolved } = this;
if (!resolved)
return noop;
const { animation } = resolved;
attachTimeline(animation, timeline);
}
return noop;
}
play() {
if (this.isStopped)
return;
const { resolved } = this;
if (!resolved)
return;
const { animation } = resolved;
if (animation.playState === "finished") {
this.updateFinishedPromise();
}
animation.play();
}
pause() {
const { resolved } = this;
if (!resolved)
return;
const { animation } = resolved;
animation.pause();
}
stop() {
this.resolver.cancel();
this.isStopped = true;
if (this.state === "idle")
return;
this.resolveFinishedPromise();
this.updateFinishedPromise();
const { resolved } = this;
if (!resolved)
return;
const { animation, keyframes, duration, type, ease, times } = resolved;
if (animation.playState === "idle" ||
animation.playState === "finished") {
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.
*/
if (this.time) {
const { motionValue, onUpdate, onComplete, element, ...options } = this.options;
const sampleAnimation = new MainThreadAnimation({
...options,
keyframes,
duration,
type,
ease,
times,
isGenerator: true,
});
const sampleTime = secondsToMilliseconds(this.time);
motionValue.setWithVelocity(sampleAnimation.sample(sampleTime - sampleDelta).value, sampleAnimation.sample(sampleTime).value, sampleDelta);
}
const { onStop } = this.options;
onStop && onStop();
this.cancel();
}
complete() {
const { resolved } = this;
if (!resolved)
return;
resolved.animation.finish();
}
cancel() {
const { resolved } = this;
if (!resolved)
return;
resolved.animation.cancel();
}
static supports(options) {
const { motionValue, name, repeatDelay, repeatType, damping, type } = options;
if (!motionValue ||
!motionValue.owner ||
!(motionValue.owner.current instanceof HTMLElement)) {
return false;
}
const { onUpdate, transformTemplate } = motionValue.owner.getProps();
return (supportsWaapi() &&
name &&
acceleratedValues.has(name) &&
/**
* If we're outputting values to onUpdate then we can't use WAAPI as there's
* no way to read the value from WAAPI every frame.
*/
!onUpdate &&
!transformTemplate &&
!repeatDelay &&
repeatType !== "mirror" &&
damping !== 0 &&
type !== "inertia");
}
}
export { AcceleratedAnimation };
@@ -0,0 +1,118 @@
import { time } from '../../frameloop/sync-time.mjs';
import { flushKeyframeResolvers } from '../../render/utils/KeyframesResolver.mjs';
import { instantAnimationState } from '../../utils/use-instant-transition-state.mjs';
import { canAnimate } from './utils/can-animate.mjs';
import { getFinalKeyframe } from './waapi/utils/get-final-keyframe.mjs';
/**
* Maximum time allowed between an animation being created and it being
* resolved for us to use the latter as the start time.
*
* This is to ensure that while we prefer to "start" an animation as soon
* as it's triggered, we also want to avoid a visual jump if there's a big delay
* between these two moments.
*/
const MAX_RESOLVE_DELAY = 40;
class BaseAnimation {
constructor({ autoplay = true, delay = 0, type = "keyframes", repeat = 0, repeatDelay = 0, repeatType = "loop", ...options }) {
// Track whether the animation has been stopped. Stopped animations won't restart.
this.isStopped = false;
this.hasAttemptedResolve = false;
this.createdAt = time.now();
this.options = {
autoplay,
delay,
type,
repeat,
repeatDelay,
repeatType,
...options,
};
this.updateFinishedPromise();
}
/**
* This method uses the createdAt and resolvedAt to calculate the
* animation startTime. *Ideally*, we would use the createdAt time as t=0
* as the following frame would then be the first frame of the animation in
* progress, which would feel snappier.
*
* However, if there's a delay (main thread work) between the creation of
* the animation and the first commited frame, we prefer to use resolvedAt
* to avoid a sudden jump into the animation.
*/
calcStartTime() {
if (!this.resolvedAt)
return this.createdAt;
return this.resolvedAt - this.createdAt > MAX_RESOLVE_DELAY
? this.resolvedAt
: this.createdAt;
}
/**
* A getter for resolved data. If keyframes are not yet resolved, accessing
* this.resolved will synchronously flush all pending keyframe resolvers.
* This is a deoptimisation, but at its worst still batches read/writes.
*/
get resolved() {
if (!this._resolved && !this.hasAttemptedResolve) {
flushKeyframeResolvers();
}
return this._resolved;
}
/**
* A method to be called when the keyframes resolver completes. This method
* will check if its possible to run the animation and, if not, skip it.
* Otherwise, it will call initPlayback on the implementing class.
*/
onKeyframesResolved(keyframes, finalKeyframe) {
this.resolvedAt = time.now();
this.hasAttemptedResolve = true;
const { name, type, velocity, delay, onComplete, onUpdate, isGenerator, } = this.options;
/**
* If we can't animate this value with the resolved keyframes
* then we should complete it immediately.
*/
if (!isGenerator && !canAnimate(keyframes, name, type, velocity)) {
// Finish immediately
if (instantAnimationState.current || !delay) {
onUpdate &&
onUpdate(getFinalKeyframe(keyframes, this.options, finalKeyframe));
onComplete && onComplete();
this.resolveFinishedPromise();
return;
}
// Finish after a delay
else {
this.options.duration = 0;
}
}
const resolvedAnimation = this.initPlayback(keyframes, finalKeyframe);
if (resolvedAnimation === false)
return;
this._resolved = {
keyframes,
finalKeyframe,
...resolvedAnimation,
};
this.onPostResolved();
}
onPostResolved() { }
/**
* Allows the returned animation to be awaited or promise-chained. Currently
* resolves when the animation finishes at all but in a future update could/should
* reject if its cancels.
*/
then(resolve, reject) {
return this.currentFinishedPromise.then(resolve, reject);
}
flatten() {
this.options.type = "keyframes";
this.options.ease = "linear";
}
updateFinishedPromise() {
this.currentFinishedPromise = new Promise((resolve) => {
this.resolveFinishedPromise = resolve;
});
}
}
export { BaseAnimation };
@@ -0,0 +1,389 @@
import { isGenerator, calcGeneratorDuration } from 'motion-dom';
import { invariant, millisecondsToSeconds, secondsToMilliseconds } from 'motion-utils';
import { KeyframeResolver } from '../../render/utils/KeyframesResolver.mjs';
import { clamp } from '../../utils/clamp.mjs';
import { mix } from '../../utils/mix/index.mjs';
import { pipe } from '../../utils/pipe.mjs';
import { inertia } from '../generators/inertia.mjs';
import { keyframes } from '../generators/keyframes.mjs';
import { spring } from '../generators/spring/index.mjs';
import { BaseAnimation } from './BaseAnimation.mjs';
import { frameloopDriver } from './drivers/driver-frameloop.mjs';
import { getFinalKeyframe } from './waapi/utils/get-final-keyframe.mjs';
const generators = {
decay: inertia,
inertia,
tween: keyframes,
keyframes: keyframes,
spring,
};
const percentToProgress = (percent) => percent / 100;
/**
* Animation that runs on the main thread. Designed to be WAAPI-spec in the subset of
* features we expose publically. Mostly the compatibility is to ensure visual identity
* between both WAAPI and main thread animations.
*/
class MainThreadAnimation extends BaseAnimation {
constructor(options) {
super(options);
/**
* The time at which the animation was paused.
*/
this.holdTime = null;
/**
* The time at which the animation was cancelled.
*/
this.cancelTime = null;
/**
* The current time of the animation.
*/
this.currentTime = 0;
/**
* Playback speed as a factor. 0 would be stopped, -1 reverse and 2 double speed.
*/
this.playbackSpeed = 1;
/**
* The state of the animation to apply when the animation is resolved. This
* allows calls to the public API to control the animation before it is resolved,
* without us having to resolve it first.
*/
this.pendingPlayState = "running";
/**
* The time at which the animation was started.
*/
this.startTime = null;
this.state = "idle";
/**
* This method is bound to the instance to fix a pattern where
* animation.stop is returned as a reference from a useEffect.
*/
this.stop = () => {
this.resolver.cancel();
this.isStopped = true;
if (this.state === "idle")
return;
this.teardown();
const { onStop } = this.options;
onStop && onStop();
};
const { name, motionValue, element, keyframes } = this.options;
const KeyframeResolver$1 = (element === null || element === void 0 ? void 0 : element.KeyframeResolver) || KeyframeResolver;
const onResolved = (resolvedKeyframes, finalKeyframe) => this.onKeyframesResolved(resolvedKeyframes, finalKeyframe);
this.resolver = new KeyframeResolver$1(keyframes, onResolved, name, motionValue, element);
this.resolver.scheduleResolve();
}
flatten() {
super.flatten();
// If we've already resolved the animation, re-initialise it
if (this._resolved) {
Object.assign(this._resolved, this.initPlayback(this._resolved.keyframes));
}
}
initPlayback(keyframes$1) {
const { type = "keyframes", repeat = 0, repeatDelay = 0, repeatType, velocity = 0, } = this.options;
const generatorFactory = isGenerator(type)
? type
: generators[type] || keyframes;
/**
* If our generator doesn't support mixing numbers, we need to replace keyframes with
* [0, 100] and then make a function that maps that to the actual keyframes.
*
* 100 is chosen instead of 1 as it works nicer with spring animations.
*/
let mapPercentToKeyframes;
let mirroredGenerator;
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}`);
}
mapPercentToKeyframes = pipe(percentToProgress, mix(keyframes$1[0], keyframes$1[1]));
keyframes$1 = [0, 100];
}
const generator = generatorFactory({ ...this.options, keyframes: keyframes$1 });
/**
* If we have a mirror repeat type we need to create a second generator that outputs the
* mirrored (not reversed) animation and later ping pong between the two generators.
*/
if (repeatType === "mirror") {
mirroredGenerator = generatorFactory({
...this.options,
keyframes: [...keyframes$1].reverse(),
velocity: -velocity,
});
}
/**
* If duration is undefined and we have repeat options,
* we need to calculate a duration from the generator.
*
* We set it to the generator itself to cache the duration.
* Any timeline resolver will need to have already precalculated
* the duration by this step.
*/
if (generator.calculatedDuration === null) {
generator.calculatedDuration = calcGeneratorDuration(generator);
}
const { calculatedDuration } = generator;
const resolvedDuration = calculatedDuration + repeatDelay;
const totalDuration = resolvedDuration * (repeat + 1) - repeatDelay;
return {
generator,
mirroredGenerator,
mapPercentToKeyframes,
calculatedDuration,
resolvedDuration,
totalDuration,
};
}
onPostResolved() {
const { autoplay = true } = this.options;
this.play();
if (this.pendingPlayState === "paused" || !autoplay) {
this.pause();
}
else {
this.state = this.pendingPlayState;
}
}
tick(timestamp, sample = false) {
const { resolved } = this;
// If the animations has failed to resolve, return the final keyframe.
if (!resolved) {
const { keyframes } = this.options;
return { done: true, value: keyframes[keyframes.length - 1] };
}
const { finalKeyframe, generator, mirroredGenerator, mapPercentToKeyframes, keyframes, calculatedDuration, totalDuration, resolvedDuration, } = resolved;
if (this.startTime === null)
return generator.next(0);
const { delay, repeat, repeatType, repeatDelay, onUpdate } = this.options;
/**
* requestAnimationFrame timestamps can come through as lower than
* the startTime as set by performance.now(). Here we prevent this,
* though in the future it could be possible to make setting startTime
* a pending operation that gets resolved here.
*/
if (this.speed > 0) {
this.startTime = Math.min(this.startTime, timestamp);
}
else if (this.speed < 0) {
this.startTime = Math.min(timestamp - totalDuration / this.speed, this.startTime);
}
// Update currentTime
if (sample) {
this.currentTime = timestamp;
}
else if (this.holdTime !== null) {
this.currentTime = this.holdTime;
}
else {
// Rounding the time because floating point arithmetic is not always accurate, e.g. 3000.367 - 1000.367 =
// 2000.0000000000002. This is a problem when we are comparing the currentTime with the duration, for
// example.
this.currentTime =
Math.round(timestamp - this.startTime) * this.speed;
}
// Rebase on delay
const timeWithoutDelay = this.currentTime - delay * (this.speed >= 0 ? 1 : -1);
const isInDelayPhase = this.speed >= 0
? timeWithoutDelay < 0
: timeWithoutDelay > totalDuration;
this.currentTime = Math.max(timeWithoutDelay, 0);
// If this animation has finished, set the current time to the total duration.
if (this.state === "finished" && this.holdTime === null) {
this.currentTime = totalDuration;
}
let elapsed = this.currentTime;
let frameGenerator = generator;
if (repeat) {
/**
* Get the current progress (0-1) of the animation. If t is >
* than duration we'll get values like 2.5 (midway through the
* third iteration)
*/
const progress = Math.min(this.currentTime, totalDuration) / resolvedDuration;
/**
* Get the current iteration (0 indexed). For instance the floor of
* 2.5 is 2.
*/
let currentIteration = Math.floor(progress);
/**
* Get the current progress of the iteration by taking the remainder
* so 2.5 is 0.5 through iteration 2
*/
let iterationProgress = progress % 1.0;
/**
* If iteration progress is 1 we count that as the end
* of the previous iteration.
*/
if (!iterationProgress && progress >= 1) {
iterationProgress = 1;
}
iterationProgress === 1 && currentIteration--;
currentIteration = Math.min(currentIteration, repeat + 1);
/**
* Reverse progress if we're not running in "normal" direction
*/
const isOddIteration = Boolean(currentIteration % 2);
if (isOddIteration) {
if (repeatType === "reverse") {
iterationProgress = 1 - iterationProgress;
if (repeatDelay) {
iterationProgress -= repeatDelay / resolvedDuration;
}
}
else if (repeatType === "mirror") {
frameGenerator = mirroredGenerator;
}
}
elapsed = clamp(0, 1, iterationProgress) * resolvedDuration;
}
/**
* If we're in negative time, set state as the initial keyframe.
* This prevents delay: x, duration: 0 animations from finishing
* instantly.
*/
const state = isInDelayPhase
? { done: false, value: keyframes[0] }
: frameGenerator.next(elapsed);
if (mapPercentToKeyframes) {
state.value = mapPercentToKeyframes(state.value);
}
let { done } = state;
if (!isInDelayPhase && calculatedDuration !== null) {
done =
this.speed >= 0
? this.currentTime >= totalDuration
: this.currentTime <= 0;
}
const isAnimationFinished = this.holdTime === null &&
(this.state === "finished" || (this.state === "running" && done));
if (isAnimationFinished && finalKeyframe !== undefined) {
state.value = getFinalKeyframe(keyframes, this.options, finalKeyframe);
}
if (onUpdate) {
onUpdate(state.value);
}
if (isAnimationFinished) {
this.finish();
}
return state;
}
get duration() {
const { resolved } = this;
return resolved ? millisecondsToSeconds(resolved.calculatedDuration) : 0;
}
get time() {
return millisecondsToSeconds(this.currentTime);
}
set time(newTime) {
newTime = secondsToMilliseconds(newTime);
this.currentTime = newTime;
if (this.holdTime !== null || this.speed === 0) {
this.holdTime = newTime;
}
else if (this.driver) {
this.startTime = this.driver.now() - newTime / this.speed;
}
}
get speed() {
return this.playbackSpeed;
}
set speed(newSpeed) {
const hasChanged = this.playbackSpeed !== newSpeed;
this.playbackSpeed = newSpeed;
if (hasChanged) {
this.time = millisecondsToSeconds(this.currentTime);
}
}
play() {
if (!this.resolver.isScheduled) {
this.resolver.resume();
}
if (!this._resolved) {
this.pendingPlayState = "running";
return;
}
if (this.isStopped)
return;
const { driver = frameloopDriver, onPlay, startTime } = this.options;
if (!this.driver) {
this.driver = driver((timestamp) => this.tick(timestamp));
}
onPlay && onPlay();
const now = this.driver.now();
if (this.holdTime !== null) {
this.startTime = now - this.holdTime;
}
else if (!this.startTime) {
this.startTime = startTime !== null && startTime !== void 0 ? startTime : this.calcStartTime();
}
else if (this.state === "finished") {
this.startTime = now;
}
if (this.state === "finished") {
this.updateFinishedPromise();
}
this.cancelTime = this.startTime;
this.holdTime = null;
/**
* Set playState to running only after we've used it in
* the previous logic.
*/
this.state = "running";
this.driver.start();
}
pause() {
var _a;
if (!this._resolved) {
this.pendingPlayState = "paused";
return;
}
this.state = "paused";
this.holdTime = (_a = this.currentTime) !== null && _a !== void 0 ? _a : 0;
}
complete() {
if (this.state !== "running") {
this.play();
}
this.pendingPlayState = this.state = "finished";
this.holdTime = null;
}
finish() {
this.teardown();
this.state = "finished";
const { onComplete } = this.options;
onComplete && onComplete();
}
cancel() {
if (this.cancelTime !== null) {
this.tick(this.cancelTime);
}
this.teardown();
this.updateFinishedPromise();
}
teardown() {
this.state = "idle";
this.stopDriver();
this.resolveFinishedPromise();
this.updateFinishedPromise();
this.startTime = this.cancelTime = null;
this.resolver.cancel();
}
stopDriver() {
if (!this.driver)
return;
this.driver.stop();
this.driver = undefined;
}
sample(time) {
this.startTime = 0;
return this.tick(time, true);
}
}
// Legacy interface
function animateValue(options) {
return new MainThreadAnimation(options);
}
export { MainThreadAnimation, animateValue };
@@ -0,0 +1,17 @@
import { time } from '../../../frameloop/sync-time.mjs';
import { frame, cancelFrame, frameData } from '../../../frameloop/frame.mjs';
const frameloopDriver = (update) => {
const passTimestamp = ({ timestamp }) => update(timestamp);
return {
start: () => 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 : time.now()),
};
};
export { frameloopDriver };
@@ -0,0 +1,14 @@
/**
* A list of values that can be hardware-accelerated.
*/
const acceleratedValues = new Set([
"opacity",
"clipPath",
"filter",
"transform",
// TODO: Can be accelerated but currently disabled until https://issues.chromium.org/issues/41491098 is resolved
// or until we implement support for linear() easing.
// "background-color"
]);
export { acceleratedValues };
@@ -0,0 +1,42 @@
import { isGenerator } from 'motion-dom';
import { warning } from 'motion-utils';
import { isAnimatable } from '../../utils/is-animatable.mjs';
function hasKeyframesChanged(keyframes) {
const current = keyframes[0];
if (keyframes.length === 1)
return true;
for (let i = 0; i < keyframes.length; i++) {
if (keyframes[i] !== current)
return true;
}
}
function canAnimate(keyframes, name, type, velocity) {
/**
* Check if we're able to animate between the start and end keyframes,
* and throw a warning if we're attempting to animate between one that's
* animatable and another that isn't.
*/
const originKeyframe = keyframes[0];
if (originKeyframe === null)
return false;
/**
* These aren't traditionally animatable but we do support them.
* In future we could look into making this more generic or replacing
* this function with mix() === mixImmediate
*/
if (name === "display" || name === "visibility")
return true;
const targetKeyframe = keyframes[keyframes.length - 1];
const isOriginAnimatable = isAnimatable(originKeyframe, name);
const isTargetAnimatable = isAnimatable(targetKeyframe, name);
warning(isOriginAnimatable === isTargetAnimatable, `You are trying to animate ${name} from "${originKeyframe}" to "${targetKeyframe}". ${originKeyframe} is not an animatable value - to enable this animation set ${originKeyframe} to a value animatable to ${targetKeyframe} via the \`style\` property.`);
// Always skip if any of these are true
if (!isOriginAnimatable || !isTargetAnimatable) {
return false;
}
return (hasKeyframesChanged(keyframes) ||
((type === "spring" || isGenerator(type)) && velocity));
}
export { canAnimate };
@@ -0,0 +1,112 @@
import { NativeAnimationControls, isGenerator, createGeneratorEasing, supportsLinearEasing } from 'motion-dom';
import { invariant, secondsToMilliseconds } from 'motion-utils';
import { startWaapiAnimation } from './index.mjs';
import { browserNumberValueTypes } from '../../../render/dom/value-types/number-browser.mjs';
import { getFinalKeyframe } from './utils/get-final-keyframe.mjs';
import { setCSSVar, setStyle } from './utils/style.mjs';
import { supportsPartialKeyframes } from './utils/supports-partial-keyframes.mjs';
import { supportsWaapi } from './utils/supports-waapi.mjs';
const state = new WeakMap();
function hydrateKeyframes(valueName, keyframes, read) {
for (let i = 0; i < keyframes.length; i++) {
if (keyframes[i] === null) {
keyframes[i] = i === 0 ? read() : keyframes[i - 1];
}
if (typeof keyframes[i] === "number" &&
browserNumberValueTypes[valueName]) {
keyframes[i] = browserNumberValueTypes[valueName].transform(keyframes[i]);
}
}
if (!supportsPartialKeyframes() && keyframes.length < 2) {
keyframes.unshift(read());
}
}
const defaultEasing = "easeOut";
function getElementAnimationState(element) {
const animationState = state.get(element) || new Map();
state.set(element, animationState);
return state.get(element);
}
class NativeAnimation extends NativeAnimationControls {
constructor(element, valueName, valueKeyframes, options) {
const isCSSVar = valueName.startsWith("--");
invariant(typeof options.type !== "string", `animateMini doesn't support "type" as a string. Did you mean to import { spring } from "framer-motion"?`);
const existingAnimation = getElementAnimationState(element).get(valueName);
existingAnimation && existingAnimation.stop();
const readInitialKeyframe = () => {
return valueName.startsWith("--")
? element.style.getPropertyValue(valueName)
: window.getComputedStyle(element)[valueName];
};
if (!Array.isArray(valueKeyframes)) {
valueKeyframes = [valueKeyframes];
}
hydrateKeyframes(valueName, valueKeyframes, readInitialKeyframe);
// TODO: Replace this with toString()?
if (isGenerator(options.type)) {
const generatorOptions = createGeneratorEasing(options, 100, options.type);
options.ease = supportsLinearEasing()
? generatorOptions.ease
: defaultEasing;
options.duration = secondsToMilliseconds(generatorOptions.duration);
options.type = "keyframes";
}
else {
options.ease = options.ease || defaultEasing;
}
const onFinish = () => {
this.setValue(element, valueName, getFinalKeyframe(valueKeyframes, options));
this.cancel();
this.resolveFinishedPromise();
};
const init = () => {
this.setValue = isCSSVar ? setCSSVar : setStyle;
this.options = options;
this.updateFinishedPromise();
this.removeAnimation = () => {
const elementState = state.get(element);
elementState && elementState.delete(valueName);
};
};
if (!supportsWaapi()) {
super();
init();
onFinish();
}
else {
super(startWaapiAnimation(element, valueName, valueKeyframes, options));
init();
if (options.autoplay === false) {
this.animation.pause();
}
this.animation.onfinish = onFinish;
getElementAnimationState(element).set(valueName, this);
}
}
/**
* Allows the returned animation to be awaited or promise-chained. Currently
* resolves when the animation finishes at all but in a future update could/should
* reject if its cancels.
*/
then(resolve, reject) {
return this.currentFinishedPromise.then(resolve, reject);
}
updateFinishedPromise() {
this.currentFinishedPromise = new Promise((resolve) => {
this.resolveFinishedPromise = resolve;
});
}
play() {
if (this.state === "finished") {
this.updateFinishedPromise();
}
super.play();
}
cancel() {
this.removeAnimation();
super.cancel();
}
}
export { NativeAnimation };
@@ -0,0 +1,34 @@
import { resolveElements, getValueTransition } from 'motion-dom';
import { invariant, secondsToMilliseconds } from 'motion-utils';
import { NativeAnimation } from './NativeAnimation.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];
const elementTransition = { ...options };
/**
* Resolve stagger function if provided.
*/
if (typeof elementTransition.delay === "function") {
elementTransition.delay = elementTransition.delay(i, numElements);
}
for (const valueName in keyframes) {
const valueKeyframes = keyframes[valueName];
const valueOptions = {
...getValueTransition(elementTransition, valueName),
};
valueOptions.duration = valueOptions.duration
? secondsToMilliseconds(valueOptions.duration)
: valueOptions.duration;
valueOptions.delay = secondsToMilliseconds(valueOptions.delay || 0);
animations.push(new NativeAnimation(element, valueName, valueKeyframes, valueOptions));
}
}
return animations;
}
export { animateElements };
@@ -0,0 +1,13 @@
import { GroupPlaybackControls } from 'motion-dom';
import { createAnimationsFromSequence } from '../../sequence/create.mjs';
import { animateElements } from './animate-elements.mjs';
function animateSequence(definition, options) {
const animations = [];
createAnimationsFromSequence(definition, options).forEach(({ keyframes, transition }, element) => {
animations.push(...animateElements(element, keyframes, transition));
});
return new GroupPlaybackControls(animations);
}
export { animateSequence };
@@ -0,0 +1,12 @@
import { GroupPlaybackControls } from 'motion-dom';
import { animateElements } from './animate-elements.mjs';
const createScopedWaapiAnimate = (scope) => {
function scopedAnimate(elementOrSelector, keyframes, options) {
return new GroupPlaybackControls(animateElements(elementOrSelector, keyframes, options, scope));
}
return scopedAnimate;
};
const animateMini = /*@__PURE__*/ createScopedWaapiAnimate();
export { animateMini, createScopedWaapiAnimate };
+23
View File
@@ -0,0 +1,23 @@
import { mapEasingToNativeEasing } from 'motion-dom';
function startWaapiAnimation(element, valueName, keyframes, { delay = 0, duration = 300, repeat = 0, repeatType = "loop", ease = "easeInOut", times, } = {}) {
const keyframeOptions = { [valueName]: keyframes };
if (times)
keyframeOptions.offset = times;
const easing = mapEasingToNativeEasing(ease, duration);
/**
* If this is an easing array, apply to keyframes, not animation as a whole
*/
if (Array.isArray(easing))
keyframeOptions.easing = easing;
return element.animate(keyframeOptions, {
delay,
duration,
easing: !Array.isArray(easing) ? easing : "linear",
fill: "both",
iterations: repeat + 1,
direction: repeatType === "reverse" ? "alternate" : "normal",
});
}
export { startWaapiAnimation };
@@ -0,0 +1,12 @@
const isNotNull = (value) => value !== null;
function getFinalKeyframe(keyframes, { repeat, repeatType = "loop" }, finalKeyframe) {
const resolvedKeyframes = keyframes.filter(isNotNull);
const index = repeat && repeatType !== "loop" && repeat % 2 === 1
? 0
: resolvedKeyframes.length - 1;
return !index || finalKeyframe === undefined
? resolvedKeyframes[index]
: finalKeyframe;
}
export { getFinalKeyframe };
@@ -0,0 +1,8 @@
function setCSSVar(element, name, value) {
element.style.setProperty(`--${name}`, value);
}
function setStyle(element, name, value) {
element.style[name] = value;
}
export { setCSSVar, setStyle };
@@ -0,0 +1,13 @@
import { memo } from 'motion-utils';
const supportsPartialKeyframes = /*@__PURE__*/ memo(() => {
try {
document.createElement("div").animate({ opacity: [1] });
}
catch (e) {
return false;
}
return true;
});
export { supportsPartialKeyframes };
@@ -0,0 +1,5 @@
import { memo } from 'motion-utils';
const supportsWaapi = /*@__PURE__*/ memo(() => Object.hasOwnProperty.call(Element.prototype, "animate"));
export { supportsWaapi };
+87
View File
@@ -0,0 +1,87 @@
import { spring } from './spring/index.mjs';
import { calcGeneratorVelocity } from './utils/velocity.mjs';
function inertia({ keyframes, velocity = 0.0, power = 0.8, timeConstant = 325, bounceDamping = 10, bounceStiffness = 500, modifyTarget, min, max, restDelta = 0.5, restSpeed, }) {
const origin = keyframes[0];
const state = {
done: false,
value: origin,
};
const isOutOfBounds = (v) => (min !== undefined && v < min) || (max !== undefined && v > max);
const nearestBoundary = (v) => {
if (min === undefined)
return max;
if (max === undefined)
return min;
return Math.abs(min - v) < Math.abs(max - v) ? min : max;
};
let amplitude = power * velocity;
const ideal = origin + amplitude;
const target = modifyTarget === undefined ? ideal : modifyTarget(ideal);
/**
* If the target has changed we need to re-calculate the amplitude, otherwise
* the animation will start from the wrong position.
*/
if (target !== ideal)
amplitude = target - origin;
const calcDelta = (t) => -amplitude * Math.exp(-t / timeConstant);
const calcLatest = (t) => target + calcDelta(t);
const applyFriction = (t) => {
const delta = calcDelta(t);
const latest = calcLatest(t);
state.done = Math.abs(delta) <= restDelta;
state.value = state.done ? target : latest;
};
/**
* Ideally this would resolve for t in a stateless way, we could
* do that by always precalculating the animation but as we know
* this will be done anyway we can assume that spring will
* be discovered during that.
*/
let timeReachedBoundary;
let spring$1;
const checkCatchBoundary = (t) => {
if (!isOutOfBounds(state.value))
return;
timeReachedBoundary = t;
spring$1 = spring({
keyframes: [state.value, nearestBoundary(state.value)],
velocity: calcGeneratorVelocity(calcLatest, t, state.value), // TODO: This should be passing * 1000
damping: bounceDamping,
stiffness: bounceStiffness,
restDelta,
restSpeed,
});
};
checkCatchBoundary(0);
return {
calculatedDuration: null,
next: (t) => {
/**
* We need to resolve the friction to figure out if we need a
* spring but we don't want to do this twice per frame. So here
* we flag if we updated for this frame and later if we did
* we can skip doing it again.
*/
let hasUpdatedFrame = false;
if (!spring$1 && timeReachedBoundary === undefined) {
hasUpdatedFrame = true;
applyFriction(t);
checkCatchBoundary(t);
}
/**
* If we have a spring and the provided t is beyond the moment the friction
* animation crossed the min/max boundary, use the spring.
*/
if (timeReachedBoundary !== undefined && t >= timeReachedBoundary) {
return spring$1.next(t - timeReachedBoundary);
}
else {
!hasUpdatedFrame && applyFriction(t);
return state;
}
},
};
}
export { inertia };
+51
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 };
@@ -0,0 +1,27 @@
const springDefaults = {
// Default spring physics
stiffness: 100,
damping: 10,
mass: 1.0,
velocity: 0.0,
// Default duration/bounce-based options
duration: 800, // in ms
bounce: 0.3,
visualDuration: 0.3, // in seconds
// Rest thresholds
restSpeed: {
granular: 0.01,
default: 2,
},
restDelta: {
granular: 0.005,
default: 0.5,
},
// Limits
minDuration: 0.01, // in seconds
maxDuration: 10.0, // in seconds
minDamping: 0.05,
maxDamping: 1,
};
export { springDefaults };
@@ -0,0 +1,85 @@
import { warning, secondsToMilliseconds, millisecondsToSeconds } from 'motion-utils';
import { clamp } from '../../../utils/clamp.mjs';
import { springDefaults } from './defaults.mjs';
const safeMin = 0.001;
function findSpring({ duration = springDefaults.duration, bounce = springDefaults.bounce, velocity = springDefaults.velocity, mass = springDefaults.mass, }) {
let envelope;
let derivative;
warning(duration <= secondsToMilliseconds(springDefaults.maxDuration), "Spring duration must be 10 seconds or less");
let dampingRatio = 1 - bounce;
/**
* Restrict dampingRatio and duration to within acceptable ranges.
*/
dampingRatio = clamp(springDefaults.minDamping, springDefaults.maxDamping, dampingRatio);
duration = clamp(springDefaults.minDuration, springDefaults.maxDuration, millisecondsToSeconds(duration));
if (dampingRatio < 1) {
/**
* Underdamped spring
*/
envelope = (undampedFreq) => {
const exponentialDecay = undampedFreq * dampingRatio;
const delta = exponentialDecay * duration;
const a = exponentialDecay - velocity;
const b = calcAngularFreq(undampedFreq, dampingRatio);
const c = Math.exp(-delta);
return safeMin - (a / b) * c;
};
derivative = (undampedFreq) => {
const exponentialDecay = undampedFreq * dampingRatio;
const delta = exponentialDecay * duration;
const d = delta * velocity + velocity;
const e = Math.pow(dampingRatio, 2) * Math.pow(undampedFreq, 2) * duration;
const f = Math.exp(-delta);
const g = calcAngularFreq(Math.pow(undampedFreq, 2), dampingRatio);
const factor = -envelope(undampedFreq) + safeMin > 0 ? -1 : 1;
return (factor * ((d - e) * f)) / g;
};
}
else {
/**
* Critically-damped spring
*/
envelope = (undampedFreq) => {
const a = Math.exp(-undampedFreq * duration);
const b = (undampedFreq - velocity) * duration + 1;
return -safeMin + a * b;
};
derivative = (undampedFreq) => {
const a = Math.exp(-undampedFreq * duration);
const b = (velocity - undampedFreq) * (duration * duration);
return a * b;
};
}
const initialGuess = 5 / duration;
const undampedFreq = approximateRoot(envelope, derivative, initialGuess);
duration = secondsToMilliseconds(duration);
if (isNaN(undampedFreq)) {
return {
stiffness: springDefaults.stiffness,
damping: springDefaults.damping,
duration,
};
}
else {
const stiffness = Math.pow(undampedFreq, 2) * mass;
return {
stiffness,
damping: dampingRatio * 2 * Math.sqrt(mass * stiffness),
duration,
};
}
}
const rootIterations = 12;
function approximateRoot(envelope, derivative, initialGuess) {
let result = initialGuess;
for (let i = 1; i < rootIterations; i++) {
result = result - envelope(result) / derivative(result);
}
return result;
}
function calcAngularFreq(undampedFreq, dampingRatio) {
return undampedFreq * Math.sqrt(1 - dampingRatio * dampingRatio);
}
export { calcAngularFreq, findSpring };
@@ -0,0 +1,166 @@
import { calcGeneratorDuration, maxGeneratorDuration, generateLinearEasing } from 'motion-dom';
import { millisecondsToSeconds, secondsToMilliseconds } from 'motion-utils';
import { clamp } from '../../../utils/clamp.mjs';
import { calcGeneratorVelocity } from '../utils/velocity.mjs';
import { springDefaults } from './defaults.mjs';
import { findSpring, calcAngularFreq } from './find.mjs';
const durationKeys = ["duration", "bounce"];
const physicsKeys = ["stiffness", "damping", "mass"];
function isSpringType(options, keys) {
return keys.some((key) => options[key] !== undefined);
}
function getSpringOptions(options) {
let springOptions = {
velocity: springDefaults.velocity,
stiffness: springDefaults.stiffness,
damping: springDefaults.damping,
mass: springDefaults.mass,
isResolvedFromDuration: false,
...options,
};
// stiffness/damping/mass overrides duration/bounce
if (!isSpringType(options, physicsKeys) &&
isSpringType(options, durationKeys)) {
if (options.visualDuration) {
const visualDuration = options.visualDuration;
const root = (2 * Math.PI) / (visualDuration * 1.2);
const stiffness = root * root;
const damping = 2 *
clamp(0.05, 1, 1 - (options.bounce || 0)) *
Math.sqrt(stiffness);
springOptions = {
...springOptions,
mass: springDefaults.mass,
stiffness,
damping,
};
}
else {
const derived = findSpring(options);
springOptions = {
...springOptions,
...derived,
mass: springDefaults.mass,
};
springOptions.isResolvedFromDuration = true;
}
}
return springOptions;
}
function spring(optionsOrVisualDuration = springDefaults.visualDuration, bounce = springDefaults.bounce) {
const options = typeof optionsOrVisualDuration !== "object"
? {
visualDuration: optionsOrVisualDuration,
keyframes: [0, 1],
bounce,
}
: optionsOrVisualDuration;
let { restSpeed, restDelta } = options;
const origin = options.keyframes[0];
const target = options.keyframes[options.keyframes.length - 1];
/**
* This is the Iterator-spec return value. We ensure it's mutable rather than using a generator
* to reduce GC during animation.
*/
const state = { done: false, value: origin };
const { stiffness, damping, mass, duration, velocity, isResolvedFromDuration, } = getSpringOptions({
...options,
velocity: -millisecondsToSeconds(options.velocity || 0),
});
const initialVelocity = velocity || 0.0;
const dampingRatio = damping / (2 * Math.sqrt(stiffness * mass));
const initialDelta = target - origin;
const undampedAngularFreq = millisecondsToSeconds(Math.sqrt(stiffness / mass));
/**
* If we're working on a granular scale, use smaller defaults for determining
* when the spring is finished.
*
* These defaults have been selected emprically based on what strikes a good
* ratio between feeling good and finishing as soon as changes are imperceptible.
*/
const isGranularScale = Math.abs(initialDelta) < 5;
restSpeed || (restSpeed = isGranularScale
? springDefaults.restSpeed.granular
: springDefaults.restSpeed.default);
restDelta || (restDelta = isGranularScale
? springDefaults.restDelta.granular
: springDefaults.restDelta.default);
let resolveSpring;
if (dampingRatio < 1) {
const angularFreq = calcAngularFreq(undampedAngularFreq, dampingRatio);
// Underdamped spring
resolveSpring = (t) => {
const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t);
return (target -
envelope *
(((initialVelocity +
dampingRatio * undampedAngularFreq * initialDelta) /
angularFreq) *
Math.sin(angularFreq * t) +
initialDelta * Math.cos(angularFreq * t)));
};
}
else if (dampingRatio === 1) {
// Critically damped spring
resolveSpring = (t) => target -
Math.exp(-undampedAngularFreq * t) *
(initialDelta +
(initialVelocity + undampedAngularFreq * initialDelta) * t);
}
else {
// Overdamped spring
const dampedAngularFreq = undampedAngularFreq * Math.sqrt(dampingRatio * dampingRatio - 1);
resolveSpring = (t) => {
const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t);
// When performing sinh or cosh values can hit Infinity so we cap them here
const freqForT = Math.min(dampedAngularFreq * t, 300);
return (target -
(envelope *
((initialVelocity +
dampingRatio * undampedAngularFreq * initialDelta) *
Math.sinh(freqForT) +
dampedAngularFreq *
initialDelta *
Math.cosh(freqForT))) /
dampedAngularFreq);
};
}
const generator = {
calculatedDuration: isResolvedFromDuration ? duration || null : null,
next: (t) => {
const current = resolveSpring(t);
if (!isResolvedFromDuration) {
let currentVelocity = 0.0;
/**
* We only need to calculate velocity for under-damped springs
* as over- and critically-damped springs can't overshoot, so
* checking only for displacement is enough.
*/
if (dampingRatio < 1) {
currentVelocity =
t === 0
? secondsToMilliseconds(initialVelocity)
: calcGeneratorVelocity(resolveSpring, t, current);
}
const isBelowVelocityThreshold = Math.abs(currentVelocity) <= restSpeed;
const isBelowDisplacementThreshold = Math.abs(target - current) <= restDelta;
state.done =
isBelowVelocityThreshold && isBelowDisplacementThreshold;
}
else {
state.done = t >= duration;
}
state.value = state.done ? target : current;
return state;
},
toString: () => {
const calculatedDuration = Math.min(calcGeneratorDuration(generator), maxGeneratorDuration);
const easing = generateLinearEasing((progress) => generator.next(calculatedDuration * progress).value, calculatedDuration, 30);
return calculatedDuration + "ms " + easing;
},
};
return generator;
}
export { spring };
@@ -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 };
@@ -0,0 +1,80 @@
import { invariant } from 'motion-utils';
import { setTarget } from '../../render/utils/setters.mjs';
import { animateVisualElement } from '../interfaces/visual-element.mjs';
function stopAnimation(visualElement) {
visualElement.values.forEach((value) => value.stop());
}
function setVariants(visualElement, variantLabels) {
const reversedLabels = [...variantLabels].reverse();
reversedLabels.forEach((key) => {
const variant = visualElement.getVariant(key);
variant && setTarget(visualElement, variant);
if (visualElement.variantChildren) {
visualElement.variantChildren.forEach((child) => {
setVariants(child, variantLabels);
});
}
});
}
function setValues(visualElement, definition) {
if (Array.isArray(definition)) {
return setVariants(visualElement, definition);
}
else if (typeof definition === "string") {
return setVariants(visualElement, [definition]);
}
else {
setTarget(visualElement, definition);
}
}
/**
* @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, setValues };
@@ -0,0 +1,17 @@
import { useConstant } from '../../utils/use-constant.mjs';
import { useUnmountEffect } from '../../utils/use-unmount-effect.mjs';
import { createScopedWaapiAnimate } from '../animators/waapi/animate-style.mjs';
function useAnimateMini() {
const scope = useConstant(() => ({
current: null, // Will be hydrated by React
animations: [],
}));
const animate = useConstant(() => createScopedWaapiAnimate(scope));
useUnmountEffect(() => {
scope.animations.forEach((animation) => animation.stop());
});
return [scope, animate];
}
export { useAnimateMini };
+17
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/index.mjs';
function useAnimate() {
const scope = useConstant(() => ({
current: null, // Will be hydrated by React
animations: [],
}));
const animate = useConstant(() => createScopedAnimate(scope));
useUnmountEffect(() => {
scope.animations.forEach((animation) => animation.stop());
});
return [scope, animate];
}
export { useAnimate };
@@ -0,0 +1,64 @@
import { useState, useLayoutEffect } from 'react';
import { useConstant } from '../../utils/use-constant.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 {
constructor() {
super(...arguments);
this.measureInstanceViewportBox = createBox;
}
build() { }
resetTransform() { }
restoreTransform() { }
removeValueFromRenderState() { }
renderInstance() { }
scrapeMotionValuesFromProps() {
return createObject();
}
getBaseTargetFromProps() {
return undefined;
}
readValueFromInstance(_state, key, options) {
return options.initialState[key] || 0;
}
sortInstanceNodePosition() {
return 0;
}
}
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: {
onUpdate: (v) => {
setAnimationState({ ...v });
},
},
visualState,
presenceContext: null,
}, { initialState });
});
useLayoutEffect(() => {
element.mount({});
return () => element.unmount();
}, [element]);
const startAnimation = useConstant(() => (animationDefinition) => {
return animateVisualElement(element, animationDefinition);
});
return [animationState, startAnimation];
}
export { useAnimatedState };
+41
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 };
@@ -0,0 +1,112 @@
import { getValueTransition, GroupPlaybackControls } from 'motion-dom';
import { secondsToMilliseconds } from 'motion-utils';
import { frame } from '../../frameloop/frame.mjs';
import { MotionGlobalConfig } from '../../utils/GlobalConfig.mjs';
import { instantAnimationState } from '../../utils/use-instant-transition-state.mjs';
import { AcceleratedAnimation } from '../animators/AcceleratedAnimation.mjs';
import { MainThreadAnimation } from '../animators/MainThreadAnimation.mjs';
import { getFinalKeyframe } from '../animators/waapi/utils/get-final-keyframe.mjs';
import { getDefaultTransition } from '../utils/default-transitions.mjs';
import { isTransitionDefined } from '../utils/is-transition-defined.mjs';
const animateMotionValue = (name, value, target, transition = {}, element, isHandoff) => (onComplete) => {
const valueTransition = getValueTransition(transition, name) || {};
/**
* Most transition values are currently completely overwritten by value-specific
* transitions. In the future it'd be nicer to blend these transitions. But for now
* delay actually does inherit from the root transition if not value-specific.
*/
const delay = valueTransition.delay || transition.delay || 0;
/**
* Elapsed isn't a public transition option but can be passed through from
* optimized appear effects in milliseconds.
*/
let { elapsed = 0 } = transition;
elapsed = elapsed - secondsToMilliseconds(delay);
let options = {
keyframes: Array.isArray(target) ? target : [null, target],
ease: "easeOut",
velocity: value.getVelocity(),
...valueTransition,
delay: -elapsed,
onUpdate: (v) => {
value.set(v);
valueTransition.onUpdate && valueTransition.onUpdate(v);
},
onComplete: () => {
onComplete();
valueTransition.onComplete && valueTransition.onComplete();
},
name,
motionValue: value,
element: isHandoff ? undefined : element,
};
/**
* If there's no transition defined for this value, we can generate
* unqiue transition settings for this value.
*/
if (!isTransitionDefined(valueTransition)) {
options = {
...options,
...getDefaultTransition(name, options),
};
}
/**
* Both WAAPI and our internal animation functions use durations
* as defined by milliseconds, while our external API defines them
* as seconds.
*/
if (options.duration) {
options.duration = secondsToMilliseconds(options.duration);
}
if (options.repeatDelay) {
options.repeatDelay = secondsToMilliseconds(options.repeatDelay);
}
if (options.from !== undefined) {
options.keyframes[0] = options.from;
}
let shouldSkip = false;
if (options.type === false ||
(options.duration === 0 && !options.repeatDelay)) {
options.duration = 0;
if (options.delay === 0) {
shouldSkip = true;
}
}
if (instantAnimationState.current ||
MotionGlobalConfig.skipAnimations) {
shouldSkip = true;
options.duration = 0;
options.delay = 0;
}
/**
* If we can or must skip creating the animation, and apply only
* the final keyframe, do so. We also check once keyframes are resolved but
* this early check prevents the need to create an animation at all.
*/
if (shouldSkip && !isHandoff && value.get() !== undefined) {
const finalKeyframe = getFinalKeyframe(options.keyframes, valueTransition);
if (finalKeyframe !== undefined) {
frame.update(() => {
options.onUpdate(finalKeyframe);
options.onComplete();
});
// We still want to return some animation controls here rather
// than returning undefined
return new GroupPlaybackControls([]);
}
}
/**
* Animate via WAAPI if possible. If this is a handoff animation, the optimised animation will be running via
* WAAPI. Therefore, this animation must be JS to ensure it runs "under" the
* optimised animation.
*/
if (!isHandoff && AcceleratedAnimation.supports(options)) {
return new AcceleratedAnimation(options);
}
else {
return new MainThreadAnimation(options);
}
};
export { animateMotionValue };
@@ -0,0 +1,75 @@
import { getValueTransition } from 'motion-dom';
import { positionalKeys } from '../../render/html/utils/keys-position.mjs';
import { setTarget } from '../../render/utils/setters.mjs';
import { addValueToWillChange } from '../../value/use-will-change/add-will-change.mjs';
import { getOptimisedAppearId } from '../optimized-appear/get-appear-id.mjs';
import { animateMotionValue } from './motion-value.mjs';
import { frame } from '../../frameloop/frame.mjs';
/**
* 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 animateTarget(visualElement, targetAndTransition, { delay = 0, transitionOverride, type } = {}) {
var _a;
let { transition = visualElement.getDefaultTransition(), transitionEnd, ...target } = targetAndTransition;
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, (_a = visualElement.latestValues[key]) !== null && _a !== void 0 ? _a : null);
const valueTarget = target[key];
if (valueTarget === undefined ||
(animationTypeState &&
shouldBlockAnimation(animationTypeState, key))) {
continue;
}
const valueTransition = {
delay,
...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.
*/
let isHandoff = false;
if (window.MotionHandoffAnimation) {
const appearId = getOptimisedAppearId(visualElement);
if (appearId) {
const startTime = window.MotionHandoffAnimation(appearId, key, frame);
if (startTime !== null) {
valueTransition.startTime = startTime;
isHandoff = true;
}
}
}
addValueToWillChange(visualElement, key);
value.start(animateMotionValue(key, value, valueTarget, visualElement.shouldReduceMotion && positionalKeys.has(key)
? { type: false }
: valueTransition, visualElement, isHandoff));
const animation = value.animation;
if (animation) {
animations.push(animation);
}
}
if (transitionEnd) {
Promise.all(animations).then(() => {
frame.update(() => {
transitionEnd && setTarget(visualElement, transitionEnd);
});
});
}
return animations;
}
export { animateTarget };
@@ -0,0 +1,66 @@
import { resolveVariant } from '../../render/utils/resolve-dynamic-variants.mjs';
import { animateTarget } from './visual-element-target.mjs';
function animateVariant(visualElement, variant, options = {}) {
var _a;
const resolved = resolveVariant(visualElement, variant, options.type === "exit"
? (_a = visualElement.presenceContext) === null || _a === void 0 ? void 0 : _a.custom
: undefined);
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 };
@@ -0,0 +1,26 @@
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 };
@@ -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 };
@@ -0,0 +1,7 @@
import { optimizedAppearDataAttribute } from './data-id.mjs';
function getOptimisedAppearId(visualElement) {
return visualElement.props[optimizedAppearDataAttribute];
}
export { getOptimisedAppearId };
@@ -0,0 +1,40 @@
import { appearAnimationStore } from './store.mjs';
import { appearStoreId } from './store-id.mjs';
function handoffOptimizedAppearAnimation(elementId, valueName, frame) {
var _a;
const storeId = appearStoreId(elementId, valueName);
const optimisedAnimation = appearAnimationStore.get(storeId);
if (!optimisedAnimation) {
return null;
}
const { animation, startTime } = optimisedAnimation;
function cancelAnimation() {
var _a;
(_a = window.MotionCancelOptimisedAnimation) === null || _a === void 0 ? void 0 : _a.call(window, elementId, valueName, frame);
}
/**
* We can cancel the animation once it's finished now that we've synced
* with Motion.
*
* Prefer onfinish over finished as onfinish is backwards compatible with
* older browsers.
*/
animation.onfinish = cancelAnimation;
if (startTime === null || ((_a = window.MotionHandoffIsComplete) === null || _a === void 0 ? void 0 : _a.call(window, elementId))) {
/**
* 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.
*/
cancelAnimation();
return null;
}
else {
return startTime;
}
}
export { handoffOptimizedAppearAnimation };
+173
View File
@@ -0,0 +1,173 @@
import { appearStoreId } from './store-id.mjs';
import { startWaapiAnimation } from '../animators/waapi/index.mjs';
import { optimizedAppearDataId } from './data-id.mjs';
import { handoffOptimizedAppearAnimation } from './handoff.mjs';
import { appearAnimationStore, appearComplete } from './store.mjs';
import { noop } from 'motion-utils';
import { getOptimisedAppearId } from './get-appear-id.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;
/**
* Keep track of animations that were suspended vs cancelled so we
* can easily resume them when we're done measuring layout.
*/
const suspendedAnimations = new Set();
function resumeSuspendedAnimations() {
suspendedAnimations.forEach((data) => {
data.animation.play();
data.animation.startTime = data.startTime;
});
suspendedAnimations.clear();
}
function startOptimizedAppearAnimation(element, name, keyframes, options, onReady) {
// Prevent optimised appear animations if Motion has already started animating.
if (window.MotionIsMounted) {
return;
}
const id = element.dataset[optimizedAppearDataId];
if (!id)
return;
window.MotionHandoffAnimation = handoffOptimizedAppearAnimation;
const storeId = appearStoreId(id, name);
if (!readyAnimation) {
readyAnimation = startWaapiAnimation(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,
});
/**
* If there's no readyAnimation then there's been no instantiation
* of handoff animations.
*/
window.MotionHandoffAnimation = handoffOptimizedAppearAnimation;
window.MotionHasOptimisedAnimation = (elementId, valueName) => {
if (!elementId)
return false;
/**
* Keep a map of elementIds that have started animating. We check
* via ID instead of Element because of hydration errors and
* pre-hydration checks. We also actively record IDs as they start
* animating rather than simply checking for data-appear-id as
* this attrbute might be present but not lead to an animation, for
* instance if the element's appear animation is on a different
* breakpoint.
*/
if (!valueName) {
return appearComplete.has(elementId);
}
const animationId = appearStoreId(elementId, valueName);
return Boolean(appearAnimationStore.get(animationId));
};
window.MotionHandoffMarkAsComplete = (elementId) => {
if (appearComplete.has(elementId)) {
appearComplete.set(elementId, true);
}
};
window.MotionHandoffIsComplete = (elementId) => {
return appearComplete.get(elementId) === true;
};
/**
* We only need to cancel transform animations as
* they're the ones that will interfere with the
* layout animation measurements.
*/
window.MotionCancelOptimisedAnimation = (elementId, valueName, frame, canResume) => {
const animationId = appearStoreId(elementId, valueName);
const data = appearAnimationStore.get(animationId);
if (!data)
return;
if (frame && canResume === undefined) {
/**
* Wait until the end of the subsequent frame to cancel the animation
* to ensure we don't remove the animation before the main thread has
* had a chance to resolve keyframes and render.
*/
frame.postRender(() => {
frame.postRender(() => {
data.animation.cancel();
});
});
}
else {
data.animation.cancel();
}
if (frame && canResume) {
suspendedAnimations.add(data);
frame.render(resumeSuspendedAnimations);
}
else {
appearAnimationStore.delete(animationId);
/**
* If there are no more animations left, we can remove the cancel function.
* This will let us know when we can stop checking for conflicting layout animations.
*/
if (!appearAnimationStore.size) {
window.MotionCancelOptimisedAnimation = undefined;
}
}
};
window.MotionCheckAppearSync = (visualElement, valueName, value) => {
var _a, _b;
const appearId = getOptimisedAppearId(visualElement);
if (!appearId)
return;
const valueIsOptimised = (_a = window.MotionHasOptimisedAnimation) === null || _a === void 0 ? void 0 : _a.call(window, appearId, valueName);
const externalAnimationValue = (_b = visualElement.props.values) === null || _b === void 0 ? void 0 : _b[valueName];
if (!valueIsOptimised || !externalAnimationValue)
return;
const removeSyncCheck = value.on("change", (latestValue) => {
var _a;
if (externalAnimationValue.get() !== latestValue) {
(_a = window.MotionCancelOptimisedAnimation) === null || _a === void 0 ? void 0 : _a.call(window, appearId, valueName);
removeSyncCheck();
}
});
return removeSyncCheck;
};
}
const startAnimation = () => {
readyAnimation.cancel();
const appearAnimation = startWaapiAnimation(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);
};
appearComplete.set(id, false);
if (readyAnimation.ready) {
readyAnimation.ready.then(startAnimation).catch(noop);
}
else {
startAnimation();
}
}
export { startOptimizedAppearAnimation };
@@ -0,0 +1,8 @@
import { transformProps } from '../../render/html/utils/keys-transform.mjs';
const appearStoreId = (elementId, valueName) => {
const key = transformProps.has(valueName) ? "transform" : valueName;
return `${elementId}: ${key}`;
};
export { appearStoreId };
@@ -0,0 +1,4 @@
const appearAnimationStore = new Map();
const appearComplete = new Map();
export { appearAnimationStore, appearComplete };
+253
View File
@@ -0,0 +1,253 @@
import { isGenerator, createGeneratorEasing } from 'motion-dom';
import { progress, secondsToMilliseconds, invariant } from 'motion-utils';
import { getEasingForSegment } from '../../easing/utils/get-easing-for-segment.mjs';
import { defaultOffset } from '../../utils/offsets/default.mjs';
import { fillOffset } from '../../utils/offsets/fill.mjs';
import { isMotionValue } from '../../value/utils/is-motion-value.mjs';
import { resolveSubjects } from '../animate/resolve-subjects.mjs';
import { calculateRepeatDuration } from './utils/calc-repeat-duration.mjs';
import { calcNextTime } from './utils/calc-time.mjs';
import { addKeyframes } from './utils/edit.mjs';
import { normalizeTimes } from './utils/normalize-times.mjs';
import { compareByTime } from './utils/sort.mjs';
const defaultSegmentEasing = "easeInOut";
const MAX_REPEAT = 20;
function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...sequenceTransition } = {}, scope, generators) {
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, numSubjects = 0) => {
const valueKeyframesAsList = keyframesAsList(valueKeyframes);
const { delay = 0, times = defaultOffset(valueKeyframesAsList), type = "keyframes", repeat, repeatType, repeatDelay = 0, ...remainingTransition } = valueTransition;
let { ease = defaultTransition.ease || "easeOut", duration } = valueTransition;
/**
* Resolve stagger() if defined.
*/
const calculatedDelay = typeof delay === "function"
? delay(elementIndex, numSubjects)
: delay;
/**
* If this animation should and can use a spring, generate a spring easing function.
*/
const numKeyframes = valueKeyframesAsList.length;
const createGenerator = isGenerator(type)
? type
: generators === null || generators === void 0 ? void 0 : generators[type];
if (numKeyframes <= 2 && createGenerator) {
/**
* 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, createGenerator);
ease = springEasing.ease;
duration = springEasing.duration;
}
duration !== null && duration !== void 0 ? duration : (duration = defaultDuration);
const startTime = currentTime + calculatedDelay;
/**
* 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);
/**
* Handle repeat options
*/
if (repeat) {
invariant(repeat < MAX_REPEAT, "Repeat count too high, must be less than 20");
duration = calculateRepeatDuration(duration, repeat);
const originalKeyframes = [...valueKeyframesAsList];
const originalTimes = [...times];
ease = Array.isArray(ease) ? [...ease] : [ease];
const originalEase = [...ease];
for (let repeatIndex = 0; repeatIndex < repeat; repeatIndex++) {
valueKeyframesAsList.push(...originalKeyframes);
for (let keyframeIndex = 0; keyframeIndex < originalKeyframes.length; keyframeIndex++) {
times.push(originalTimes[keyframeIndex] + (repeatIndex + 1));
ease.push(keyframeIndex === 0
? "linear"
: getEasingForSegment(originalEase, keyframeIndex - 1));
}
}
normalizeTimes(times, repeat);
}
const targetTime = startTime + duration;
/**
* 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 {
const subjects = resolveSubjects(subject, keyframes, scope, elementCache);
const numSubjects = subjects.length;
/**
* For every element in this segment, process the defined values.
*/
for (let subjectIndex = 0; subjectIndex < numSubjects; subjectIndex++) {
/**
* Cast necessary, but we know these are of this type
*/
keyframes = keyframes;
transition = transition;
const thisSubject = subjects[subjectIndex];
const subjectSequence = getSubjectSequence(thisSubject, sequences);
for (const key in keyframes) {
resolveValueSequence(keyframes[key], getValueTransition(transition, key), getValueSequence(key, subjectSequence), subjectIndex, numSubjects);
}
}
}
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 && transition[key]
? {
...transition,
...transition[key],
}
: { ...transition };
}
const isNumber = (keyframe) => typeof keyframe === "number";
const isNumberKeyframesArray = (keyframes) => keyframes.every(isNumber);
export { createAnimationsFromSequence, getValueTransition };
@@ -0,0 +1,5 @@
function calculateRepeatDuration(duration, repeat, _repeatDelay) {
return duration * (repeat + 1);
}
export { calculateRepeatDuration };
@@ -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 };
+31
View File
@@ -0,0 +1,31 @@
import { getEasingForSegment } from '../../../easing/utils/get-easing-for-segment.mjs';
import { removeItem } from '../../../utils/array.mjs';
import { mixNumber } from '../../../utils/mix/number.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: mixNumber(startTime, endTime, offset[i]),
easing: getEasingForSegment(easing, i),
});
}
}
export { addKeyframes, eraseKeyframes };
@@ -0,0 +1,13 @@
/**
* Take an array of times that represent repeated keyframes. For instance
* if we have original times of [0, 0.5, 1] then our repeated times will
* be [0, 0.5, 1, 1, 1.5, 2]. Loop over the times and scale them back
* down to a 0-1 scale.
*/
function normalizeTimes(times, repeat) {
for (let i = 0; i < times.length; i++) {
times[i] = times[i] / (repeat + 1);
}
}
export { normalizeTimes };
+14
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 };
@@ -0,0 +1,44 @@
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';
import { ObjectVisualElement } from '../../render/object/ObjectVisualElement.mjs';
function createDOMVisualElement(element) {
const options = {
presenceContext: null,
props: {},
visualState: {
renderState: {
transform: {},
transformOrigin: {},
style: {},
vars: {},
attrs: {},
},
latestValues: {},
},
};
const node = isSVGElement(element)
? new SVGVisualElement(options)
: new HTMLVisualElement(options);
node.mount(element);
visualElementStore.set(element, node);
}
function createObjectVisualElement(subject) {
const options = {
presenceContext: null,
props: {},
visualState: {
renderState: {
output: {},
},
latestValues: {},
},
};
const node = new ObjectVisualElement(options);
node.mount(subject);
visualElementStore.set(subject, node);
}
export { createDOMVisualElement, createObjectVisualElement };
@@ -0,0 +1,40 @@
import { transformProps } from '../../render/html/utils/keys-transform.mjs';
const underDampedSpring = {
type: "spring",
stiffness: 500,
damping: 25,
restSpeed: 10,
};
const criticallyDampedSpring = (target) => ({
type: "spring",
stiffness: 550,
damping: target === 0 ? 2 * Math.sqrt(550) : 30,
restSpeed: 10,
});
const keyframesTransition = {
type: "keyframes",
duration: 0.8,
};
/**
* Default easing curve is a slightly shallower version of
* the default browser easing curve.
*/
const ease = {
type: "keyframes",
ease: [0.25, 0.1, 0.35, 1],
duration: 0.3,
};
const getDefaultTransition = (valueKey, { keyframes }) => {
if (keyframes.length > 2) {
return keyframesTransition;
}
else if (transformProps.has(valueKey)) {
return valueKey.startsWith("scale")
? criticallyDampedSpring(keyframes[1])
: underDampedSpring;
}
return ease;
};
export { getDefaultTransition };
+30
View File
@@ -0,0 +1,30 @@
import { complex } from '../../value/types/complex/index.mjs';
/**
* Check if a value is animatable. Examples:
*
* ✅: 100, "100px", "#fff"
* ❌: "block", "url(2.jpg)"
* @param value
*
* @internal
*/
const isAnimatable = (value, name) => {
// If the list of keys tat might be non-animatable grows, replace with Set
if (name === "zIndex")
return false;
// If it's a number or a keyframes array, we can animate it. We might at some point
// need to do a deep isAnimatable check of keyframes, or let Popmotion handle this,
// but for now lets leave it like this for performance reasons
if (typeof value === "number" || Array.isArray(value))
return true;
if (typeof value === "string" && // It's animatable if we have a string
(complex.test(value) || value === "0") && // And it contains numbers and/or colors
!value.startsWith("url(") // Unless it starts with "url("
) {
return true;
}
return false;
};
export { isAnimatable };
@@ -0,0 +1,7 @@
function isAnimationControls(v) {
return (v !== null &&
typeof v === "object" &&
typeof v.start === "function");
}
export { isAnimationControls };
@@ -0,0 +1,5 @@
function isDOMKeyframes(keyframes) {
return typeof keyframes === "object" && !Array.isArray(keyframes);
}
export { isDOMKeyframes };
@@ -0,0 +1,5 @@
const isKeyframesTarget = (v) => {
return Array.isArray(v);
};
export { isKeyframesTarget };
+15
View File
@@ -0,0 +1,15 @@
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);
}
else {
return true;
}
}
export { isNone };
@@ -0,0 +1,10 @@
/**
* 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;
}
export { isTransitionDefined };
+26
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 };
+3
View File
@@ -0,0 +1,3 @@
"use client";
export { createMotionComponent as create } from './render/components/motion/create.mjs';
export { MotionA as a, MotionAbbr as abbr, MotionAddress as address, MotionAnimate as animate, MotionArea as area, MotionArticle as article, MotionAside as aside, MotionAudio as audio, MotionB as b, MotionBase as base, MotionBdi as bdi, MotionBdo as bdo, MotionBig as big, MotionBlockquote as blockquote, MotionBody as body, MotionButton as button, MotionCanvas as canvas, MotionCaption as caption, MotionCircle as circle, MotionCite as cite, MotionClipPath as clipPath, MotionCode as code, MotionCol as col, MotionColgroup as colgroup, MotionData as data, MotionDatalist as datalist, MotionDd as dd, MotionDefs as defs, MotionDel as del, MotionDesc as desc, MotionDetails as details, MotionDfn as dfn, MotionDialog as dialog, MotionDiv as div, MotionDl as dl, MotionDt as dt, MotionEllipse as ellipse, MotionEm as em, MotionEmbed as embed, MotionFeBlend as feBlend, MotionFeColorMatrix as feColorMatrix, MotionFeComponentTransfer as feComponentTransfer, MotionFeComposite as feComposite, MotionFeConvolveMatrix as feConvolveMatrix, MotionFeDiffuseLighting as feDiffuseLighting, MotionFeDisplacementMap as feDisplacementMap, MotionFeDistantLight as feDistantLight, MotionFeDropShadow as feDropShadow, MotionFeFlood as feFlood, MotionFeFuncA as feFuncA, MotionFeFuncB as feFuncB, MotionFeFuncG as feFuncG, MotionFeFuncR as feFuncR, MotionFeGaussianBlur as feGaussianBlur, MotionFeImage as feImage, MotionFeMerge as feMerge, MotionFeMergeNode as feMergeNode, MotionFeMorphology as feMorphology, MotionFeOffset as feOffset, MotionFePointLight as fePointLight, MotionFeSpecularLighting as feSpecularLighting, MotionFeSpotLight as feSpotLight, MotionFeTile as feTile, MotionFeTurbulence as feTurbulence, MotionFieldset as fieldset, MotionFigcaption as figcaption, MotionFigure as figure, MotionFilter as filter, MotionFooter as footer, MotionForeignObject as foreignObject, MotionForm as form, MotionG as g, MotionH1 as h1, MotionH2 as h2, MotionH3 as h3, MotionH4 as h4, MotionH5 as h5, MotionH6 as h6, MotionHead as head, MotionHeader as header, MotionHgroup as hgroup, MotionHr as hr, MotionHtml as html, MotionI as i, MotionIframe as iframe, MotionImage as image, MotionImg as img, MotionInput as input, MotionIns as ins, MotionKbd as kbd, MotionKeygen as keygen, MotionLabel as label, MotionLegend as legend, MotionLi as li, MotionLine as line, MotionLinearGradient as linearGradient, MotionLink as link, MotionMain as main, MotionMap as map, MotionMark as mark, MotionMarker as marker, MotionMask as mask, MotionMenu as menu, MotionMenuitem as menuitem, MotionMetadata as metadata, MotionMeter as meter, MotionNav as nav, MotionObject as object, MotionOl as ol, MotionOptgroup as optgroup, MotionOption as option, MotionOutput as output, MotionP as p, MotionParam as param, MotionPath as path, MotionPattern as pattern, MotionPicture as picture, MotionPolygon as polygon, MotionPolyline as polyline, MotionPre as pre, MotionProgress as progress, MotionQ as q, MotionRadialGradient as radialGradient, MotionRect as rect, MotionRp as rp, MotionRt as rt, MotionRuby as ruby, MotionS as s, MotionSamp as samp, MotionScript as script, MotionSection as section, MotionSelect as select, MotionSmall as small, MotionSource as source, MotionSpan as span, MotionStop as stop, MotionStrong as strong, MotionStyle as style, MotionSub as sub, MotionSummary as summary, MotionSup as sup, MotionSvg as svg, MotionSymbol as symbol, MotionTable as table, MotionTbody as tbody, MotionTd as td, MotionText as text, MotionTextPath as textPath, MotionTextarea as textarea, MotionTfoot as tfoot, MotionTh as th, MotionThead as thead, MotionTime as time, MotionTitle as title, MotionTr as tr, MotionTrack as track, MotionTspan as tspan, MotionU as u, MotionUl as ul, MotionUse as use, MotionVideo as video, MotionView as view, MotionWbr as wbr, MotionWebview as webview } from './render/components/motion/elements.mjs';
@@ -0,0 +1,77 @@
"use client";
import { jsx } from 'react/jsx-runtime';
import * as React from 'react';
import { useId, useRef, useContext, useInsertionEffect } from 'react';
import { MotionConfigContext } from '../../context/MotionConfigContext.mjs';
/**
* 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,
});
const { nonce } = useContext(MotionConfigContext);
/**
* 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");
if (nonce)
style.nonce = nonce;
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 (jsx(PopChildMeasure, { isPresent: isPresent, childRef: ref, sizeRef: size, children: React.cloneElement(children, { ref }) }));
}
export { PopChild };
@@ -0,0 +1,61 @@
"use client";
import { jsx } from 'react/jsx-runtime';
import * as React from 'react';
import { useId, useCallback, 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 memoizedOnExitComplete = useCallback((childId) => {
presenceChildren.set(childId, true);
for (const isComplete of presenceChildren.values()) {
if (!isComplete)
return; // can stop searching when any is incomplete
}
onExitComplete && onExitComplete();
}, [presenceChildren, onExitComplete]);
const context = useMemo(() => ({
id,
initial,
isPresent,
custom,
onExitComplete: memoizedOnExitComplete,
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
? [Math.random(), memoizedOnExitComplete]
: [isPresent, memoizedOnExitComplete]);
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 = jsx(PopChild, { isPresent: isPresent, children: children });
}
return (jsx(PresenceContext.Provider, { value: context, children: children }));
};
function newChildrenMap() {
return new Map();
}
export { PresenceChild };
+166
View File
@@ -0,0 +1,166 @@
"use client";
import { jsx, Fragment } from 'react/jsx-runtime';
import { useMemo, useRef, useState, useContext } from 'react';
import { LayoutGroupContext } from '../../context/LayoutGroupContext.mjs';
import { useConstant } from '../../utils/use-constant.mjs';
import { PresenceChild } from './PresenceChild.mjs';
import { usePresence } from './use-presence.mjs';
import { onlyElements, getChildKey } from './utils.mjs';
import { useIsomorphicLayoutEffect } from '../../utils/use-isomorphic-effect.mjs';
/**
* `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, presenceAffectsLayout = true, mode = "sync", propagate = false, }) => {
const [isParentPresent, safeToRemove] = usePresence(propagate);
/**
* Filter any children that aren't ReactElements. We can only track components
* between renders with a props.key.
*/
const presentChildren = useMemo(() => onlyElements(children), [children]);
/**
* Track the keys of the currently rendered children. This is used to
* determine which children are exiting.
*/
const presentKeys = propagate && !isParentPresent ? [] : presentChildren.map(getChildKey);
/**
* If `initial={false}` we only want to pass this to components in the first render.
*/
const isInitialRender = useRef(true);
/**
* A ref containing the currently present children. When all exit animations
* are complete, we use this to re-render the component with the latest children
* *committed* rather than the latest children *rendered*.
*/
const pendingPresentChildren = useRef(presentChildren);
/**
* Track which exiting children have finished animating out.
*/
const exitComplete = useConstant(() => new Map());
/**
* Save children to render as React state. To ensure this component is concurrent-safe,
* we check for exiting children via an effect.
*/
const [diffedChildren, setDiffedChildren] = useState(presentChildren);
const [renderedChildren, setRenderedChildren] = useState(presentChildren);
useIsomorphicLayoutEffect(() => {
isInitialRender.current = false;
pendingPresentChildren.current = presentChildren;
/**
* Update complete status of exiting children.
*/
for (let i = 0; i < renderedChildren.length; i++) {
const key = getChildKey(renderedChildren[i]);
if (!presentKeys.includes(key)) {
if (exitComplete.get(key) !== true) {
exitComplete.set(key, false);
}
}
else {
exitComplete.delete(key);
}
}
}, [renderedChildren, presentKeys.length, presentKeys.join("-")]);
const exitingChildren = [];
if (presentChildren !== diffedChildren) {
let nextChildren = [...presentChildren];
/**
* Loop through all the currently rendered components and decide which
* are exiting.
*/
for (let i = 0; i < renderedChildren.length; i++) {
const child = renderedChildren[i];
const key = getChildKey(child);
if (!presentKeys.includes(key)) {
nextChildren.splice(i, 0, child);
exitingChildren.push(child);
}
}
/**
* If we're in "wait" mode, and we have exiting children, we want to
* only render these until they've all exited.
*/
if (mode === "wait" && exitingChildren.length) {
nextChildren = exitingChildren;
}
setRenderedChildren(onlyElements(nextChildren));
setDiffedChildren(presentChildren);
/**
* Early return to ensure once we've set state with the latest diffed
* children, we can immediately re-render.
*/
return;
}
if (process.env.NODE_ENV !== "production" &&
mode === "wait" &&
renderedChildren.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.`);
}
/**
* If we've been provided a forceRender function by the LayoutGroupContext,
* we can use it to force a re-render amongst all surrounding components once
* all components have finished animating out.
*/
const { forceRender } = useContext(LayoutGroupContext);
return (jsx(Fragment, { children: renderedChildren.map((child) => {
const key = getChildKey(child);
const isPresent = propagate && !isParentPresent
? false
: presentChildren === renderedChildren ||
presentKeys.includes(key);
const onExit = () => {
if (exitComplete.has(key)) {
exitComplete.set(key, true);
}
else {
return;
}
let isEveryExitComplete = true;
exitComplete.forEach((isExitComplete) => {
if (!isExitComplete)
isEveryExitComplete = false;
});
if (isEveryExitComplete) {
forceRender === null || forceRender === void 0 ? void 0 : forceRender();
setRenderedChildren(pendingPresentChildren.current);
propagate && (safeToRemove === null || safeToRemove === void 0 ? void 0 : safeToRemove());
onExitComplete && onExitComplete();
}
};
return (jsx(PresenceChild, { isPresent: isPresent, initial: !isInitialRender.current || initial
? undefined
: false, custom: isPresent ? undefined : custom, presenceAffectsLayout: presenceAffectsLayout, mode: mode, onExitComplete: isPresent ? undefined : onExit, children: child }, key));
}) }));
};
export { AnimatePresence };
@@ -0,0 +1,69 @@
import { useContext, useId, useEffect, useCallback } 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(subscribe = true) {
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(() => {
if (subscribe)
register(id);
}, [subscribe]);
const safeToRemove = useCallback(() => subscribe && onExitComplete && onExitComplete(id), [id, onExitComplete, subscribe]);
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 };
@@ -0,0 +1,14 @@
import { Children, isValidElement } from 'react';
const getChildKey = (child) => child.key || "";
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;
}
export { getChildKey, onlyElements };
+15
View File
@@ -0,0 +1,15 @@
import { jsx } from 'react/jsx-runtime';
import { invariant } from 'motion-utils';
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 (jsx(LayoutGroup, { id: useConstant(() => `asl-${id++}`), children: children }));
};
export { AnimateSharedLayout };
+32
View File
@@ -0,0 +1,32 @@
"use client";
import { jsx } from 'react/jsx-runtime';
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 (jsx(LayoutGroupContext.Provider, { value: memoizedContext, children: children }));
};
export { LayoutGroup };
+68
View File
@@ -0,0 +1,68 @@
"use client";
import { jsx } from 'react/jsx-runtime';
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 (jsx(LazyContext.Provider, { value: { renderer: loadedRenderer.current, strict }, children: children }));
}
function isLazyBundle(features) {
return typeof features === "function";
}
export { LazyMotion };
+48
View File
@@ -0,0 +1,48 @@
"use client";
import { jsx } from 'react/jsx-runtime';
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 (jsx(MotionConfigContext.Provider, { value: context, children: children }));
}
export { MotionConfig };
+53
View File
@@ -0,0 +1,53 @@
"use client";
import { jsx } from 'react/jsx-runtime';
import { invariant } from 'motion-utils';
import { forwardRef, useRef, useEffect } from 'react';
import { ReorderContext } from '../../context/ReorderContext.mjs';
import { motion } from '../../render/components/motion/proxy.mjs';
import { useConstant } from '../../utils/use-constant.mjs';
import { checkReorder } from './utils/check-reorder.mjs';
function ReorderGroupComponent({ 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 (jsx(Component, { ...props, ref: externalRef, ignoreStrict: true, children: jsx(ReorderContext.Provider, { value: context, children: children }) }));
}
const ReorderGroup = /*@__PURE__*/ forwardRef(ReorderGroupComponent);
function getValue(item) {
return item.value;
}
function compareMin(a, b) {
return a.layout.min - b.layout.min;
}
export { ReorderGroup, ReorderGroupComponent };
+34
View File
@@ -0,0 +1,34 @@
"use client";
import { jsx } from 'react/jsx-runtime';
import { invariant } from 'motion-utils';
import { forwardRef, useContext } from 'react';
import { ReorderContext } from '../../context/ReorderContext.mjs';
import { motion } from '../../render/components/motion/proxy.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 ReorderItemComponent({ 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 (jsx(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: children }));
}
const ReorderItem = /*@__PURE__*/ forwardRef(ReorderItemComponent);
export { ReorderItem, ReorderItemComponent };
+2
View File
@@ -0,0 +1,2 @@
export { ReorderGroup as Group } from './Group.mjs';
export { ReorderItem as Item } from './Item.mjs';
@@ -0,0 +1,24 @@
import { moveItem } from '../../../utils/array.mjs';
import { mixNumber } from '../../../utils/mix/number.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 = mixNumber(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 };
@@ -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 };
+6
View File
@@ -0,0 +1,6 @@
"use client";
import { createContext } from 'react';
const LayoutGroupContext = createContext({});
export { LayoutGroupContext };
+6
View File
@@ -0,0 +1,6 @@
"use client";
import { createContext } from 'react';
const LazyContext = createContext({ strict: false });
export { LazyContext };
+13
View File
@@ -0,0 +1,13 @@
"use client";
import { createContext } from 'react';
/**
* @public
*/
const MotionConfigContext = createContext({
transformPagePoint: (p) => p,
isStatic: false,
reducedMotion: "never",
});
export { MotionConfigContext };
+13
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 };
+6
View File
@@ -0,0 +1,6 @@
"use client";
import { createContext } from 'react';
const MotionContext = createContext({});
export { MotionContext };
+17
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 };
+9
View File
@@ -0,0 +1,9 @@
"use client";
import { createContext } from 'react';
/**
* @public
*/
const PresenceContext = createContext(null);
export { PresenceContext };
+6
View File
@@ -0,0 +1,6 @@
"use client";
import { createContext } from 'react';
const ReorderContext = createContext(null);
export { ReorderContext };
@@ -0,0 +1,9 @@
"use client";
import { createContext } from 'react';
/**
* Internal, exported only for usage in Framer
*/
const SwitchLayoutGroupContext = createContext({});
export { SwitchLayoutGroupContext };
+2
View File
@@ -0,0 +1,2 @@
export { animateSequence } from './animation/animators/waapi/animate-sequence.mjs';
export { animateMini as animate } from './animation/animators/waapi/animate-style.mjs';
+31
View File
@@ -0,0 +1,31 @@
export { isDragActive } from 'motion-dom';
export { invariant, noop, progress } from 'motion-utils';
export { animate, createScopedAnimate } from './animation/animate/index.mjs';
export { animateMini } from './animation/animators/waapi/animate-style.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 { MotionValue, motionValue } from './value/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 { cubicBezier } from './easing/cubic-bezier.mjs';
export { easeIn, easeInOut, easeOut } from './easing/ease.mjs';
export { mirrorEasing } from './easing/modifiers/mirror.mjs';
export { reverseEasing } from './easing/modifiers/reverse.mjs';
export { steps } from './easing/steps.mjs';
export { inertia } from './animation/generators/inertia.mjs';
export { keyframes } from './animation/generators/keyframes.mjs';
export { spring } from './animation/generators/spring/index.mjs';
export { stagger } from './animation/utils/stagger.mjs';
export { time } from './frameloop/sync-time.mjs';
export { clamp } from './utils/clamp.mjs';
export { delayInSeconds as delay } from './utils/delay.mjs';
export { distance, distance2D } from './utils/distance.mjs';
export { interpolate } from './utils/interpolate.mjs';
export { mix } from './utils/mix/index.mjs';
export { pipe } from './utils/pipe.mjs';
export { transform } from './utils/transform.mjs';
export { wrap } from './utils/wrap.mjs';
export { cancelSync, sync } from './frameloop/index-legacy.mjs';
export { cancelFrame, frame, frameData, frameSteps } from './frameloop/frame.mjs';
+5
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 };
+9
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 = /*@__PURE__*/ cubicBezier(0.33, 1.53, 0.69, 0.99);
const backIn = /*@__PURE__*/ reverseEasing(backOut);
const backInOut = /*@__PURE__*/ mirrorEasing(backIn);
export { backIn, backInOut, backOut };
+8
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 };

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