framer-motion
Version:
An open source and production-ready motion library for React on the web
7,450 lines • 291 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var tslib = require('tslib');
var sync = require('framesync');
var popmotion = require('popmotion');
var heyListen = require('hey-listen');
var styleValueTypes = require('style-value-types');
var React = require('react');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var sync__default = /*#__PURE__*/_interopDefaultLegacy(sync);
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
var Presence;
(function (Presence) {
Presence[Presence["Entering"] = 0] = "Entering";
Presence[Presence["Present"] = 1] = "Present";
Presence[Presence["Exiting"] = 2] = "Exiting";
})(Presence || (Presence = {}));
(function (VisibilityAction) {
VisibilityAction[VisibilityAction["Hide"] = 0] = "Hide";
VisibilityAction[VisibilityAction["Show"] = 1] = "Show";
})(exports.VisibilityAction || (exports.VisibilityAction = {}));
// Call a handler once for each axis
function eachAxis(handler) {
return [handler("x"), handler("y")];
}
function noop(any) {
return any;
}
/**
* Bounding boxes tend to be defined as top, left, right, bottom. For various operations
* it's easier to consider each axis individually. This function returns a bounding box
* as a map of single-axis min/max values.
*/
function convertBoundingBoxToAxisBox(_a) {
var top = _a.top, left = _a.left, right = _a.right, bottom = _a.bottom;
return {
x: { min: left, max: right },
y: { min: top, max: bottom },
};
}
function convertAxisBoxToBoundingBox(_a) {
var x = _a.x, y = _a.y;
return {
top: y.min,
bottom: y.max,
left: x.min,
right: x.max,
};
}
/**
* Applies a TransformPoint function to a bounding box. TransformPoint is usually a function
* provided by Framer to allow measured points to be corrected for device scaling. This is used
* when measuring DOM elements and DOM event points.
*/
function transformBoundingBox(_a, transformPoint) {
var top = _a.top, left = _a.left, bottom = _a.bottom, right = _a.right;
if (transformPoint === void 0) { transformPoint = noop; }
var topLeft = transformPoint({ x: left, y: top });
var bottomRight = transformPoint({ x: right, y: bottom });
return {
top: topLeft.y,
left: topLeft.x,
bottom: bottomRight.y,
right: bottomRight.x,
};
}
/**
* Create an empty axis box of zero size
*/
function axisBox() {
return { x: { min: 0, max: 1 }, y: { min: 0, max: 1 } };
}
function copyAxisBox(box) {
return {
x: tslib.__assign({}, box.x),
y: tslib.__assign({}, box.y),
};
}
/**
* Create an empty box delta
*/
var zeroDelta = {
translate: 0,
scale: 1,
origin: 0,
originPoint: 0,
};
function delta() {
return {
x: tslib.__assign({}, zeroDelta),
y: tslib.__assign({}, zeroDelta),
};
}
/**
* Reset an axis to the provided origin box.
*
* This is a mutative operation.
*/
function resetAxis(axis, originAxis) {
axis.min = originAxis.min;
axis.max = originAxis.max;
}
/**
* Reset a box to the provided origin box.
*
* This is a mutative operation.
*/
function resetBox(box, originBox) {
resetAxis(box.x, originBox.x);
resetAxis(box.y, originBox.y);
}
/**
* Scales a point based on a factor and an originPoint
*/
function scalePoint(point, scale, originPoint) {
var distanceFromOrigin = point - originPoint;
var scaled = scale * distanceFromOrigin;
return originPoint + scaled;
}
/**
* Applies a translate/scale delta to a point
*/
function applyPointDelta(point, translate, scale, originPoint, boxScale) {
if (boxScale !== undefined) {
point = scalePoint(point, boxScale, originPoint);
}
return scalePoint(point, scale, originPoint) + translate;
}
/**
* Applies a translate/scale delta to an axis
*/
function applyAxisDelta(axis, translate, scale, originPoint, boxScale) {
if (translate === void 0) { translate = 0; }
if (scale === void 0) { scale = 1; }
axis.min = applyPointDelta(axis.min, translate, scale, originPoint, boxScale);
axis.max = applyPointDelta(axis.max, translate, scale, originPoint, boxScale);
}
/**
* Applies a translate/scale delta to a box
*/
function applyBoxDelta(box, _a) {
var x = _a.x, y = _a.y;
applyAxisDelta(box.x, x.translate, x.scale, x.originPoint);
applyAxisDelta(box.y, y.translate, y.scale, y.originPoint);
}
/**
* Apply a transform to an axis from the latest resolved motion values.
* This function basically acts as a bridge between a flat motion value map
* and applyAxisDelta
*/
function applyAxisTransforms(final, axis, transforms, _a) {
var _b = tslib.__read(_a, 3), key = _b[0], scaleKey = _b[1], originKey = _b[2];
// Copy the current axis to the final axis before mutation
final.min = axis.min;
final.max = axis.max;
var axisOrigin = transforms[originKey] !== undefined ? transforms[originKey] : 0.5;
var originPoint = popmotion.mix(axis.min, axis.max, axisOrigin);
// Apply the axis delta to the final axis
applyAxisDelta(final, transforms[key], transforms[scaleKey], originPoint, transforms.scale);
}
/**
* The names of the motion values we want to apply as translation, scale and origin.
*/
var xKeys = ["x", "scaleX", "originX"];
var yKeys = ["y", "scaleY", "originY"];
/**
* Apply a transform to a box from the latest resolved motion values.
*/
function applyBoxTransforms(finalBox, box, transforms) {
applyAxisTransforms(finalBox.x, box.x, transforms, xKeys);
applyAxisTransforms(finalBox.y, box.y, transforms, yKeys);
}
/**
* Remove a delta from a point. This is essentially the steps of applyPointDelta in reverse
*/
function removePointDelta(point, translate, scale, originPoint, boxScale) {
point -= translate;
point = scalePoint(point, 1 / scale, originPoint);
if (boxScale !== undefined) {
point = scalePoint(point, 1 / boxScale, originPoint);
}
return point;
}
/**
* Remove a delta from an axis. This is essentially the steps of applyAxisDelta in reverse
*/
function removeAxisDelta(axis, translate, scale, origin, boxScale) {
if (translate === void 0) { translate = 0; }
if (scale === void 0) { scale = 1; }
if (origin === void 0) { origin = 0.5; }
var originPoint = popmotion.mix(axis.min, axis.max, origin) - translate;
axis.min = removePointDelta(axis.min, translate, scale, originPoint, boxScale);
axis.max = removePointDelta(axis.max, translate, scale, originPoint, boxScale);
}
/**
* Remove a transforms from an axis. This is essentially the steps of applyAxisTransforms in reverse
* and acts as a bridge between motion values and removeAxisDelta
*/
function removeAxisTransforms(axis, transforms, _a) {
var _b = tslib.__read(_a, 3), key = _b[0], scaleKey = _b[1], originKey = _b[2];
removeAxisDelta(axis, transforms[key], transforms[scaleKey], transforms[originKey], transforms.scale);
}
/**
* Remove a transforms from an box. This is essentially the steps of applyAxisBox in reverse
* and acts as a bridge between motion values and removeAxisDelta
*/
function removeBoxTransforms(box, transforms) {
removeAxisTransforms(box.x, transforms, xKeys);
removeAxisTransforms(box.y, transforms, yKeys);
}
/**
* Apply a tree of deltas to a box. We do this to calculate the effect of all the transforms
* in a tree upon our box before then calculating how to project it into our desired viewport-relative box
*
* This is the final nested loop within updateLayoutDelta for future refactoring
*/
function applyTreeDeltas(box, treeScale, treePath) {
var treeLength = treePath.length;
if (!treeLength)
return;
// Reset the treeScale
treeScale.x = treeScale.y = 1;
for (var i = 0; i < treeLength; i++) {
var delta = treePath[i].getLayoutState().delta;
// Incoporate each ancestor's scale into a culmulative treeScale for this component
treeScale.x *= delta.x.scale;
treeScale.y *= delta.y.scale;
// Apply each ancestor's calculated delta into this component's recorded layout box
applyBoxDelta(box, delta);
}
}
var clampProgress = function (v) { return popmotion.clamp(0, 1, v); };
/**
* Returns true if the provided value is within maxDistance of the provided target
*/
function isNear(value, target, maxDistance) {
if (target === void 0) { target = 0; }
if (maxDistance === void 0) { maxDistance = 0.01; }
return popmotion.distance(value, target) < maxDistance;
}
function calcLength(axis) {
return axis.max - axis.min;
}
/**
* Calculate a transform origin relative to the source axis, between 0-1, that results
* in an asthetically pleasing scale/transform needed to project from source to target.
*/
function calcOrigin(source, target) {
var origin = 0.5;
var sourceLength = calcLength(source);
var targetLength = calcLength(target);
if (targetLength > sourceLength) {
origin = popmotion.progress(target.min, target.max - sourceLength, source.min);
}
else if (sourceLength > targetLength) {
origin = popmotion.progress(source.min, source.max - targetLength, target.min);
}
return clampProgress(origin);
}
/**
* Update the AxisDelta with a transform that projects source into target.
*
* The transform `origin` is optional. If not provided, it'll be automatically
* calculated based on the relative positions of the two bounding boxes.
*/
function updateAxisDelta(delta, source, target, origin) {
if (origin === void 0) { origin = 0.5; }
delta.origin = origin;
delta.originPoint = popmotion.mix(source.min, source.max, delta.origin);
delta.scale = calcLength(target) / calcLength(source);
if (isNear(delta.scale, 1, 0.0001))
delta.scale = 1;
delta.translate =
popmotion.mix(target.min, target.max, delta.origin) - delta.originPoint;
if (isNear(delta.translate))
delta.translate = 0;
}
/**
* Update the BoxDelta with a transform that projects the source into the target.
*
* The transform `origin` is optional. If not provided, it'll be automatically
* calculated based on the relative positions of the two bounding boxes.
*/
function updateBoxDelta(delta, source, target, origin) {
updateAxisDelta(delta.x, source.x, target.x, defaultOrigin(origin.originX));
updateAxisDelta(delta.y, source.y, target.y, defaultOrigin(origin.originY));
}
/**
* Currently this only accepts numerical origins, measured as 0-1, but could
* accept pixel values by comparing to the target axis.
*/
function defaultOrigin(origin) {
return typeof origin === "number" ? origin : 0.5;
}
function isRefObject(ref) {
return (typeof ref === "object" &&
Object.prototype.hasOwnProperty.call(ref, "current"));
}
function addUniqueItem(arr, item) {
arr.indexOf(item) === -1 && arr.push(item);
}
function removeItem(arr, item) {
var index = arr.indexOf(item);
index > -1 && arr.splice(index, 1);
}
function subscriptionManager() {
var subscriptions = [];
return {
add: function (handler) {
addUniqueItem(subscriptions, handler);
return function () { return removeItem(subscriptions, handler); };
},
notify: function (a, b, c) {
var numSubscriptions = subscriptions.length;
if (!numSubscriptions)
return;
if (numSubscriptions === 1) {
/**
* If there's only a single handler we can just call it without invoking a loop.
*/
subscriptions[0](a, b, c);
}
else {
for (var i = 0; i < numSubscriptions; i++) {
/**
* Check whether the handler exists before firing as it's possible
* the subscriptions were modified during this loop running.
*/
var handler = subscriptions[i];
handler && handler(a, b, c);
}
}
},
getSize: function () { return subscriptions.length; },
clear: function () {
subscriptions.length = 0;
},
};
}
var isFloat = function (value) {
return !isNaN(parseFloat(value));
};
/**
* `MotionValue` is used to track the state and velocity of motion values.
*
* @public
*/
var MotionValue = /** @class */ (function () {
/**
* @param init - The initiating value
* @param config - Optional configuration options
*
* - `transformer`: A function to transform incoming values with.
*
* @internal
*/
function MotionValue(init) {
var _this = this;
/**
* Duration, in milliseconds, since last updating frame.
*
* @internal
*/
this.timeDelta = 0;
/**
* Timestamp of the last time this `MotionValue` was updated.
*
* @internal
*/
this.lastUpdated = 0;
/**
* Functions to notify when the `MotionValue` updates.
*
* @internal
*/
this.updateSubscribers = subscriptionManager();
/**
* Functions to notify when the `MotionValue` updates and `render` is set to `true`.
*
* @internal
*/
this.renderSubscribers = subscriptionManager();
/**
* Tracks whether this value can output a velocity. Currently this is only true
* if the value is numerical, but we might be able to widen the scope here and support
* other value types.
*
* @internal
*/
this.canTrackVelocity = false;
this.updateAndNotify = function (v, render) {
if (render === void 0) { render = true; }
_this.prev = _this.current;
_this.current = v;
if (_this.prev !== _this.current) {
_this.updateSubscribers.notify(_this.current);
}
if (render) {
_this.renderSubscribers.notify(_this.current);
}
// Update timestamp
var _a = sync.getFrameData(), delta = _a.delta, timestamp = _a.timestamp;
if (_this.lastUpdated !== timestamp) {
_this.timeDelta = delta;
_this.lastUpdated = timestamp;
sync__default['default'].postRender(_this.scheduleVelocityCheck);
}
};
/**
* Schedule a velocity check for the next frame.
*
* This is an instanced and bound function to prevent generating a new
* function once per frame.
*
* @internal
*/
this.scheduleVelocityCheck = function () { return sync__default['default'].postRender(_this.velocityCheck); };
/**
* Updates `prev` with `current` if the value hasn't been updated this frame.
* This ensures velocity calculations return `0`.
*
* This is an instanced and bound function to prevent generating a new
* function once per frame.
*
* @internal
*/
this.velocityCheck = function (_a) {
var timestamp = _a.timestamp;
if (timestamp !== _this.lastUpdated) {
_this.prev = _this.current;
}
};
this.hasAnimated = false;
this.current = init;
this.canTrackVelocity = isFloat(this.current);
}
/**
* 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..
*
* @library
*
* ```jsx
* function 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.onChange(updateOpacity)
* const unsubscribeY = y.onChange(updateOpacity)
*
* return () => {
* unsubscribeX()
* unsubscribeY()
* }
* }, [])
*
* return <Frame x={x} />
* }
* ```
*
* @motion
*
* ```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.onChange(updateOpacity)
* const unsubscribeY = y.onChange(updateOpacity)
*
* return () => {
* unsubscribeX()
* unsubscribeY()
* }
* }, [])
*
* return <motion.div style={{ x }} />
* }
* ```
*
* @internalremarks
*
* We could look into a `useOnChange` hook if the above lifecycle management proves confusing.
*
* ```jsx
* useOnChange(x, () => {})
* ```
*
* @param subscriber - A function that receives the latest value.
* @returns A function that, when called, will cancel this subscription.
*
* @public
*/
MotionValue.prototype.onChange = function (subscription) {
return this.updateSubscribers.add(subscription);
};
MotionValue.prototype.clearListeners = function () {
this.updateSubscribers.clear();
};
/**
* Adds a function that will be notified when the `MotionValue` requests a render.
*
* @param subscriber - A function that's provided the latest value.
* @returns A function that, when called, will cancel this subscription.
*
* @internal
*/
MotionValue.prototype.onRenderRequest = function (subscription) {
// Render immediately
subscription(this.get());
return this.renderSubscribers.add(subscription);
};
/**
* Attaches a passive effect to the `MotionValue`.
*
* @internal
*/
MotionValue.prototype.attach = function (passiveEffect) {
this.passiveEffect = passiveEffect;
};
/**
* 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
*/
MotionValue.prototype.set = function (v, render) {
if (render === void 0) { render = true; }
if (!render || !this.passiveEffect) {
this.updateAndNotify(v, render);
}
else {
this.passiveEffect(v, this.updateAndNotify);
}
};
/**
* Returns the latest state of `MotionValue`
*
* @returns - The latest state of `MotionValue`
*
* @public
*/
MotionValue.prototype.get = function () {
return this.current;
};
/**
* @public
*/
MotionValue.prototype.getPrevious = function () {
return this.prev;
};
/**
* Returns the latest velocity of `MotionValue`
*
* @returns - The latest velocity of `MotionValue`. Returns `0` if the state is non-numerical.
*
* @public
*/
MotionValue.prototype.getVelocity = function () {
// This could be isFloat(this.prev) && isFloat(this.current), but that would be wasteful
return this.canTrackVelocity
? // These casts could be avoided if parseFloat would be typed better
popmotion.velocityPerSecond(parseFloat(this.current) -
parseFloat(this.prev), this.timeDelta)
: 0;
};
/**
* Registers a new animation to control this `MotionValue`. Only one
* animation can drive a `MotionValue` at one time.
*
* ```jsx
* value.start()
* ```
*
* @param animation - A function that starts the provided animation
*
* @internal
*/
MotionValue.prototype.start = function (animation) {
var _this = this;
this.stop();
return new Promise(function (resolve) {
_this.hasAnimated = true;
_this.stopAnimation = animation(resolve);
}).then(function () { return _this.clearAnimation(); });
};
/**
* Stop the currently active animation.
*
* @public
*/
MotionValue.prototype.stop = function () {
if (this.stopAnimation)
this.stopAnimation();
this.clearAnimation();
};
/**
* Returns `true` if this value is currently animating.
*
* @public
*/
MotionValue.prototype.isAnimating = function () {
return !!this.stopAnimation;
};
MotionValue.prototype.clearAnimation = function () {
this.stopAnimation = null;
};
/**
* 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
*/
MotionValue.prototype.destroy = function () {
this.updateSubscribers.clear();
this.renderSubscribers.clear();
this.stop();
};
return MotionValue;
}());
/**
* @internal
*/
function motionValue(init) {
return new MotionValue(init);
}
var isMotionValue = function (value) {
return value instanceof MotionValue;
};
/**
* A list of all transformable axes. We'll use this list to generated a version
* of each axes for each transform.
*/
var transformAxes = ["", "X", "Y", "Z"];
/**
* An ordered array of each transformable value. By default, transform values
* will be sorted to this order.
*/
var order = ["perspective", "translate", "scale", "rotate", "skew"];
/**
* Generate a list of every possible transform key.
*/
var transformProps = ["transformPerspective", "x", "y", "z"];
order.forEach(function (operationKey) {
transformAxes.forEach(function (axesKey) {
var key = operationKey + axesKey;
transformProps.push(key);
});
});
/**
* A function to use with Array.sort to sort transform keys by their default order.
*/
function sortTransformProps(a, b) {
return transformProps.indexOf(a) - transformProps.indexOf(b);
}
/**
* A quick lookup for transform props.
*/
var transformPropSet = new Set(transformProps);
function isTransformProp(key) {
return transformPropSet.has(key);
}
/**
* A quick lookup for transform origin props
*/
var transformOriginProps = new Set(["originX", "originY", "originZ"]);
function isTransformOriginProp(key) {
return transformOriginProps.has(key);
}
/**
* Converts seconds to milliseconds
*
* @param seconds - Time in seconds.
* @return milliseconds - Converted time in milliseconds.
*/
var secondsToMilliseconds = function (seconds) { return seconds * 1000; };
var easingLookup = {
linear: popmotion.linear,
easeIn: popmotion.easeIn,
easeInOut: popmotion.easeInOut,
easeOut: popmotion.easeOut,
circIn: popmotion.circIn,
circInOut: popmotion.circInOut,
circOut: popmotion.circOut,
backIn: popmotion.backIn,
backInOut: popmotion.backInOut,
backOut: popmotion.backOut,
anticipate: popmotion.anticipate,
bounceIn: popmotion.bounceIn,
bounceInOut: popmotion.bounceInOut,
bounceOut: popmotion.bounceOut,
};
var easingDefinitionToFunction = function (definition) {
if (Array.isArray(definition)) {
// If cubic bezier definition, create bezier curve
heyListen.invariant(definition.length === 4, "Cubic bezier arrays must contain four numerical values.");
var _a = tslib.__read(definition, 4), x1 = _a[0], y1 = _a[1], x2 = _a[2], y2 = _a[3];
return popmotion.cubicBezier(x1, y1, x2, y2);
}
else if (typeof definition === "string") {
// Else lookup from table
heyListen.invariant(easingLookup[definition] !== undefined, "Invalid easing type '" + definition + "'");
return easingLookup[definition];
}
return definition;
};
var isEasingArray = function (ease) {
return Array.isArray(ease) && typeof ease[0] !== "number";
};
/**
* Check if a value is animatable. Examples:
*
* ✅: 100, "100px", "#fff"
* ❌: "block", "url(2.jpg)"
* @param value
*
* @internal
*/
var isAnimatable = function (key, value) {
// If the list of keys tat might be non-animatable grows, replace with Set
if (key === "zIndex")
return false;
// If it's a number or a keyframes array, we can animate it. We might at some point
// need to do a deep isAnimatable check of keyframes, or let Popmotion handle this,
// but for now lets leave it like this for performance reasons
if (typeof value === "number" || Array.isArray(value))
return true;
if (typeof value === "string" && // It's animatable if we have a string
styleValueTypes.complex.test(value) && // And it contains numbers and/or colors
!value.startsWith("url(") // Unless it starts with "url("
) {
return true;
}
return false;
};
var isKeyframesTarget = function (v) {
return Array.isArray(v);
};
var underDampedSpring = function () { return ({
type: "spring",
stiffness: 500,
damping: 25,
restDelta: 0.5,
restSpeed: 10,
}); };
var criticallyDampedSpring = function (to) { return ({
type: "spring",
stiffness: 550,
damping: to === 0 ? 2 * Math.sqrt(550) : 30,
restDelta: 0.01,
restSpeed: 10,
}); };
var linearTween = function () { return ({
type: "keyframes",
ease: "linear",
duration: 0.3,
}); };
var keyframes = function (values) { return ({
type: "keyframes",
duration: 0.8,
values: values,
}); };
var defaultTransitions = {
x: underDampedSpring,
y: underDampedSpring,
z: underDampedSpring,
rotate: underDampedSpring,
rotateX: underDampedSpring,
rotateY: underDampedSpring,
rotateZ: underDampedSpring,
scaleX: criticallyDampedSpring,
scaleY: criticallyDampedSpring,
scale: criticallyDampedSpring,
opacity: linearTween,
backgroundColor: linearTween,
color: linearTween,
default: criticallyDampedSpring,
};
var getDefaultTransition = function (valueKey, to) {
var transitionFactory;
if (isKeyframesTarget(to)) {
transitionFactory = keyframes;
}
else {
transitionFactory =
defaultTransitions[valueKey] || defaultTransitions.default;
}
return tslib.__assign({ to: to }, transitionFactory(to));
};
/**
* ValueType for "auto"
*/
var auto = {
test: function (v) { return v === "auto"; },
parse: function (v) { return v; },
};
/**
* ValueType for ints
*/
var int = tslib.__assign(tslib.__assign({}, styleValueTypes.number), { transform: Math.round });
/**
* A map of default value types for common values
*/
var defaultValueTypes = {
// Color props
color: styleValueTypes.color,
backgroundColor: styleValueTypes.color,
outlineColor: styleValueTypes.color,
fill: styleValueTypes.color,
stroke: styleValueTypes.color,
// Border props
borderColor: styleValueTypes.color,
borderTopColor: styleValueTypes.color,
borderRightColor: styleValueTypes.color,
borderBottomColor: styleValueTypes.color,
borderLeftColor: styleValueTypes.color,
borderWidth: styleValueTypes.px,
borderTopWidth: styleValueTypes.px,
borderRightWidth: styleValueTypes.px,
borderBottomWidth: styleValueTypes.px,
borderLeftWidth: styleValueTypes.px,
borderRadius: styleValueTypes.px,
radius: styleValueTypes.px,
borderTopLeftRadius: styleValueTypes.px,
borderTopRightRadius: styleValueTypes.px,
borderBottomRightRadius: styleValueTypes.px,
borderBottomLeftRadius: styleValueTypes.px,
// Positioning props
width: styleValueTypes.px,
maxWidth: styleValueTypes.px,
height: styleValueTypes.px,
maxHeight: styleValueTypes.px,
size: styleValueTypes.px,
top: styleValueTypes.px,
right: styleValueTypes.px,
bottom: styleValueTypes.px,
left: styleValueTypes.px,
// Spacing props
padding: styleValueTypes.px,
paddingTop: styleValueTypes.px,
paddingRight: styleValueTypes.px,
paddingBottom: styleValueTypes.px,
paddingLeft: styleValueTypes.px,
margin: styleValueTypes.px,
marginTop: styleValueTypes.px,
marginRight: styleValueTypes.px,
marginBottom: styleValueTypes.px,
marginLeft: styleValueTypes.px,
// Transform props
rotate: styleValueTypes.degrees,
rotateX: styleValueTypes.degrees,
rotateY: styleValueTypes.degrees,
rotateZ: styleValueTypes.degrees,
scale: styleValueTypes.scale,
scaleX: styleValueTypes.scale,
scaleY: styleValueTypes.scale,
scaleZ: styleValueTypes.scale,
skew: styleValueTypes.degrees,
skewX: styleValueTypes.degrees,
skewY: styleValueTypes.degrees,
distance: styleValueTypes.px,
translateX: styleValueTypes.px,
translateY: styleValueTypes.px,
translateZ: styleValueTypes.px,
x: styleValueTypes.px,
y: styleValueTypes.px,
z: styleValueTypes.px,
perspective: styleValueTypes.px,
transformPerspective: styleValueTypes.px,
opacity: styleValueTypes.alpha,
originX: styleValueTypes.progressPercentage,
originY: styleValueTypes.progressPercentage,
originZ: styleValueTypes.px,
// Misc
zIndex: int,
filter: styleValueTypes.filter,
WebkitFilter: styleValueTypes.filter,
// SVG
fillOpacity: styleValueTypes.alpha,
strokeOpacity: styleValueTypes.alpha,
numOctaves: int,
};
/**
* A list of value types commonly used for dimensions
*/
var dimensionValueTypes = [styleValueTypes.number, styleValueTypes.px, styleValueTypes.percent, styleValueTypes.degrees, styleValueTypes.vw, styleValueTypes.vh, auto];
/**
* Tests a provided value against a ValueType
*/
var testValueType = function (v) { return function (type) { return type.test(v); }; };
/**
* Tests a dimensional value against the list of dimension ValueTypes
*/
var findDimensionValueType = function (v) {
return dimensionValueTypes.find(testValueType(v));
};
/**
* A list of all ValueTypes
*/
var valueTypes = tslib.__spread(dimensionValueTypes, [styleValueTypes.color, styleValueTypes.complex]);
/**
* Tests a value against the list of ValueTypes
*/
var findValueType = function (v) { return valueTypes.find(testValueType(v)); };
/**
* Gets the default ValueType for the provided value key
*/
var getDefaultValueType = function (key) { return defaultValueTypes[key]; };
/**
* Provided a value and a ValueType, returns the value as that value type.
*/
var getValueAsType = function (value, type) {
return type && typeof value === "number"
? type.transform(value)
: value;
};
function getAnimatableNone(key, value) {
var _a;
var defaultValueType = getDefaultValueType(key);
if (defaultValueType !== styleValueTypes.filter)
defaultValueType = styleValueTypes.complex;
// If value is not recognised as animatable, ie "none", create an animatable version origin based on the target
return (_a = defaultValueType.getAnimatableNone) === null || _a === void 0 ? void 0 : _a.call(defaultValueType, value);
}
/**
* 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(_a) {
var when = _a.when, delay = _a.delay, delayChildren = _a.delayChildren, staggerChildren = _a.staggerChildren, staggerDirection = _a.staggerDirection, repeat = _a.repeat, repeatType = _a.repeatType, repeatDelay = _a.repeatDelay, from = _a.from, transition = tslib.__rest(_a, ["when", "delay", "delayChildren", "staggerChildren", "staggerDirection", "repeat", "repeatType", "repeatDelay", "from"]);
return !!Object.keys(transition).length;
}
var legacyRepeatWarning = false;
/**
* Convert Framer Motion's Transition type into Popmotion-compatible options.
*/
function convertTransitionToAnimationOptions(_a) {
var ease = _a.ease, times = _a.times, yoyo = _a.yoyo, flip = _a.flip, loop = _a.loop, transition = tslib.__rest(_a, ["ease", "times", "yoyo", "flip", "loop"]);
var options = tslib.__assign({}, transition);
if (times)
options["offset"] = times;
/**
* Convert any existing durations from seconds to milliseconds
*/
if (transition.duration)
options["duration"] = secondsToMilliseconds(transition.duration);
if (transition.repeatDelay)
options.repeatDelay = secondsToMilliseconds(transition.repeatDelay);
/**
* Map easing names to Popmotion's easing functions
*/
if (ease) {
options["ease"] = isEasingArray(ease)
? ease.map(easingDefinitionToFunction)
: easingDefinitionToFunction(ease);
}
/**
* Support legacy transition API
*/
if (transition.type === "tween")
options.type = "keyframes";
/**
* TODO: These options are officially removed from the API.
*/
if (yoyo || loop || flip) {
heyListen.warning(!legacyRepeatWarning, "yoyo, loop and flip have been removed from the API. Replace with repeat and repeatType options.");
legacyRepeatWarning = true;
if (yoyo) {
options.repeatType = "reverse";
}
else if (loop) {
options.repeatType = "loop";
}
else if (flip) {
options.repeatType = "mirror";
}
options.repeat = loop || yoyo || flip || transition.repeat;
}
/**
* TODO: Popmotion 9 has the ability to automatically detect whether to use
* a keyframes or spring animation, but does so by detecting velocity and other spring options.
* It'd be good to introduce a similar thing here.
*/
if (transition.type !== "spring")
options.type = "keyframes";
return options;
}
/**
* Get the delay for a value by checking Transition with decreasing specificity.
*/
function getDelayFromTransition(transition, key) {
var _a, _b, _c, _d, _e;
return ((_e = (_d = (_b = (_a = transition[key]) === null || _a === void 0 ? void 0 : _a.delay) !== null && _b !== void 0 ? _b : (_c = transition["default"]) === null || _c === void 0 ? void 0 : _c.delay) !== null && _d !== void 0 ? _d : transition.delay) !== null && _e !== void 0 ? _e : 0);
}
function hydrateKeyframes(options) {
if (Array.isArray(options.to) && options.to[0] === null) {
options.to = tslib.__spread(options.to);
options.to[0] = options.from;
}
return options;
}
function getPopmotionAnimationOptions(transition, options, key) {
var _a;
if (Array.isArray(options.to)) {
(_a = transition.duration) !== null && _a !== void 0 ? _a : (transition.duration = 0.8);
}
hydrateKeyframes(options);
/**
* Get a default transition if none is determined to be defined.
*/
if (!isTransitionDefined(transition)) {
transition = tslib.__assign(tslib.__assign({}, transition), getDefaultTransition(key, options.to));
}
return tslib.__assign(tslib.__assign({}, options), convertTransitionToAnimationOptions(transition));
}
/**
*
*/
function getAnimation(key, value, target, transition, onComplete) {
var _a;
var valueTransition = getValueTransition(transition, key);
var origin = (_a = valueTransition.from) !== null && _a !== void 0 ? _a : value.get();
var isTargetAnimatable = isAnimatable(key, target);
/**
* If we're trying to animate from "none", try and get an animatable version
* of the target. This could be improved to work both ways.
*/
if (origin === "none" && isTargetAnimatable && typeof target === "string") {
origin = getAnimatableNone(key, target);
}
var isOriginAnimatable = isAnimatable(key, origin);
heyListen.warning(isOriginAnimatable === isTargetAnimatable, "You are trying to animate " + key + " from \"" + origin + "\" to \"" + target + "\". " + origin + " is not an animatable value - to enable this animation set " + origin + " to a value animatable to " + target + " via the `style` property.");
function start() {
var options = {
from: origin,
to: target,
velocity: value.getVelocity(),
onComplete: onComplete,
onUpdate: function (v) { return value.set(v); },
};
return valueTransition.type === "inertia" ||
valueTransition.type === "decay"
? popmotion.inertia(tslib.__assign(tslib.__assign({}, options), valueTransition))
: popmotion.animate(tslib.__assign(tslib.__assign({}, getPopmotionAnimationOptions(valueTransition, options, key)), { onUpdate: function (v) {
var _a;
options.onUpdate(v);
(_a = valueTransition.onUpdate) === null || _a === void 0 ? void 0 : _a.call(valueTransition, v);
}, onComplete: function () {
var _a;
options.onComplete();
(_a = valueTransition.onComplete) === null || _a === void 0 ? void 0 : _a.call(valueTransition);
} }));
}
function set() {
var _a;
value.set(target);
onComplete();
(_a = valueTransition === null || valueTransition === void 0 ? void 0 : valueTransition.onComplete) === null || _a === void 0 ? void 0 : _a.call(valueTransition);
return { stop: function () { } };
}
return !isOriginAnimatable ||
!isTargetAnimatable ||
valueTransition.type === false
? set
: start;
}
function getValueTransition(transition, key) {
return transition[key] || transition["default"] || transition;
}
/**
* Start animation on a MotionValue. This function is an interface between
* Framer Motion and Popmotion
*
* @internal
*/
function startAnimation(key, value, target, transition) {
if (transition === void 0) { transition = {}; }
return value.start(function (onComplete) {
var delayTimer;
var controls;
var animation = getAnimation(key, value, target, transition, onComplete);
var delay = getDelayFromTransition(transition, key);
var start = function () { return (controls = animation()); };
if (delay) {
delayTimer = setTimeout(start, secondsToMilliseconds(delay));
}
else {
start();
}
return function () {
clearTimeout(delayTimer);
controls === null || controls === void 0 ? void 0 : controls.stop();
};
});
}
/**
* Check if value is a numerical string, ie a string that is purely a number eg "100" or "-100.1"
*/
var isNumericalString = function (v) { return /^\-?\d*\.?\d+$/.test(v); };
var isCustomValue = function (v) {
return Boolean(v && typeof v === "object" && v.mix && v.toValue);
};
var resolveFinalValueInKeyframes = function (v) {
// TODO maybe throw if v.length - 1 is placeholder token?
return isKeyframesTarget(v) ? v[v.length - 1] || 0 : v;
};
/**
* Decides if the supplied variable is an array of variant labels
*/
function isVariantLabels(v) {
return Array.isArray(v);
}
/**
* Decides if the supplied variable is variant label
*/
function isVariantLabel(v) {
return typeof v === "string" || isVariantLabels(v);
}
/**
* Creates an object containing the latest state of every MotionValue on a VisualElement
*/
function getCurrent(visualElement) {
var current = {};
visualElement.forEachValue(function (value, key) { return (current[key] = value.get()); });
return current;
}
/**
* Creates an object containing the latest velocity of every MotionValue on a VisualElement
*/
function getVelocity(visualElement) {
var velocity = {};
visualElement.forEachValue(function (value, key) { return (velocity[key] = value.getVelocity()); });
return velocity;
}
function resolveVariantFromProps(props, definition, custom, currentValues, currentVelocity) {
var _a;
if (currentValues === void 0) { currentValues = {}; }
if (currentVelocity === void 0) { currentVelocity = {}; }
if (typeof definition === "string") {
definition = (_a = props.variants) === null || _a === void 0 ? void 0 : _a[definition];
}
return typeof definition === "function"
? definition(custom !== null && custom !== void 0 ? custom : props.custom, currentValues, currentVelocity)
: definition;
}
/**
* Resovles a variant if it's a variant resolver
*/
function resolveVariant(visualElement, definition, custom) {
var props = visualElement.getProps();
return resolveVariantFromProps(props, definition, custom !== null && custom !== void 0 ? custom : props.custom, getCurrent(visualElement), getVelocity(visualElement));
}
function checkIfControllingVariants(props) {
var _a;
return (typeof ((_a = props.animate) === null || _a === void 0 ? void 0 : _a.start) === "function" ||
isVariantLabel(props.animate) ||
isVariantLabel(props.whileHover) ||
isVariantLabel(props.whileDrag) ||
isVariantLabel(props.whileTap) ||
isVariantLabel(props.whileFocus) ||
isVariantLabel(props.exit));
}
/**
* Set VisualElement's MotionValue, creating a new MotionValue for it if
* it doesn't exist.
*/
function setMotionValue(visualElement, key, value) {
if (visualElement.hasValue(key)) {
visualElement.getValue(key).set(value);
}
else {
visualElement.addValue(key, motionValue(value));
}
}
function setTarget(visualElement, definition) {
var resolved = resolveVariant(visualElement, definition);
var _a = resolved
? visualElement.makeTargetAnimatable(resolved, false)
: {}, _b = _a.transitionEnd, transitionEnd = _b === void 0 ? {} : _b, _c = _a.transition, target = tslib.__rest(_a, ["transitionEnd", "transition"]);
target = tslib.__assign(tslib.__assign({}, target), transitionEnd);
for (var key in target) {
var value = resolveFinalValueInKeyframes(target[key]);
setMotionValue(visualElement, key, value);
}
}
function setVariants(visualElement, variantLabels) {
var reversedLabels = tslib.__spread(variantLabels).reverse();
reversedLabels.forEach(function (key) {
var _a;
var variant = visualElement.getVariant(key);
variant && setTarget(visualElement, variant);
(_a = visualElement.variantChildren) === null || _a === void 0 ? void 0 : _a.forEach(function (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);
}
}
function checkTargetForNewValues(visualElement, target, origin) {
var _a, _b;
var _c;
var newValueKeys = Object.keys(target).filter(function (key) { return !visualElement.hasValue(key); });
var numNewValues = newValueKeys.length;
if (!numNewValues)
return;
for (var i = 0; i < numNewValues; i++) {
var key = newValueKeys[i];
var targetValue = target[key];
var value = null;
// If this is a keyframes value, we can attempt to use the first value in the
// array as that's going to be the first value of the animation anyway
if (Array.isArray(targetValue)) {
value = targetValue[0];
}
// If it isn't a keyframes or the first keyframes value was set as `null`, read the
// value from the DOM. It might be worth investigating whether to check props (for SVG)
// or props.style (for HTML) if the value exists there before attempting to read.
if (value === null) {
var readValue = (_a = origin[key]) !== null && _a !== void 0 ? _a : visualElement.readValue(key);
value = readValue !== undefined ? readValue : target[key];
heyListen.invariant(value !== null, "No initial value for \"" + key + "\" can be inferred. Ensure an initial value for \"" + key + "\" is defined on the component.");
}
if (typeof value === "string" && isNumericalString(value)) {
// If this is a number read as a string, ie "0" or "200", convert it to a number
value = parseFloat(value);
}
else if (!findValueType(value) && styleValueTypes.complex.test(targetValue)) {
value = getAnimatableNone(key, targetValue);
}
visualElement.addValue(key, motionValue(value));
(_b = (_c = origin)[key]) !== null && _b !== void 0 ? _b : (_c[key] = value);
visualElement.setBaseTarget(key, value);
}
}
function getOriginFromTransition(key, transition) {
if (!transition)
return;
var valueTransition = transition[key] || transition["default"] || transition;
return valueTransition.from;
}
function getOrigin(target, transition, visualElement) {
var _a, _b;
var origin = {};
for (var key in target) {
origin[key] = (_a = getOriginFromTransition(key, transition)) !== null && _a !== void 0 ? _a : (_b = visualElement.getValue(key)) === null || _b === void 0 ? void 0 : _b.get();
}
return origin;
}
/**
* @internal
*/
function animateVisualElement(visualElement, definition, options) {
visualElement.notifyAnimationStart();
var animation;
if (Array.isArray(definition)) {
var animations = definition.map(function (variant) {
return animateVariant(visualElement, variant, options);
});
animation = Promise.all(animations);
}
else if (typeof definition === "string") {
animation = animateVariant(visualElement, definition, options);
}
else {
// TODO: Remove any and handle TargetResolver
animation = animateTarget(visualElement, definition, options);
}
return animation.then(function () { return visualElement.notifyAnimationComplete(); });
}
function animateVariant(visualElement, variant, options) {
var _a;
if (options === void 0) { options = {}; }
var resolved = resolveVariant(visualElement, variant, options.custom);
var _b = (resolved || {}).transition, transition = _b === void 0 ? visualElement.getDefaultTransition() || {} : _b;
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.
*/
var getAnimation = resolved
? function () { return animateTarget(visualElement, resolved, options); }
: function () { return 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.
*/
var getChildAnimations = ((_a = visualElement.variantChildren) === null || _a === void 0 ? void 0 : _a.size) ? function (forwardDelay) {
if (forwardDelay === void 0) { forwardDelay = 0; }
var _a = transition.delayChildren, delayChildren = _a === void 0 ? 0 : _a, staggerChildren = transition.staggerChildren, staggerDirection = transition.staggerDirection;
return animateChildren(visualElement, variant, delayChildren + forwardDelay, staggerChildren, staggerDirection, options);
}
: function () { return 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.
*/
var when = transition.when;
if (when) {
var _c = tslib.__read(when === "beforeChildren"
? [getAnimation, getChildAnimations]
: [getChildAnimations, getAnimation], 2), first = _c[0], last = _c[1];
return first().then(last);
}
else {
return Promise.all([getAnimation(), getChildAnimations(options.delay)]);
}
}
/**
* @internal
*/
function animateTarget(visualElement, definition, _a) {
var _b;
var _c = _a === void 0 ? {} : _a, _d = _c.delay, delay = _d === void 0 ? 0 : _d, transitionOverride = _c.transitionOverride, type = _c.type;
var _e = visualElement.makeTargetAnimatable(definition), _f = _e.transition, transition = _f === void 0 ? visualElement.getDefaultTransition() : _f, transitionEnd = _e.transitionEnd, target = tslib.__rest(_e, ["transition", "transitionEnd"]);
if (transitionOverride)
transition = transitionOverride;
var animations = [];
var animationTypeState = type && ((_b = visualElement.animationState) === null || _b === void 0 ? void 0 : _b.getState()[type]);
for (var key in target) {
var value = visualElement.getValue(key);
var valueTarget = target[key];
if (!value ||
valueTarget === undefined ||
(animationTypeState &&
shouldBlockAnimation(animationTypeState, key))) {
continue;
}
var animation = startAnimation(key, value, valueTarget, tslib.__assign({ delay: delay }, transition));
animations.push(animation);
}
return Promise.all(animations).then(function () {
transitionEnd && setTarget(visualElement, transitionEnd);
});
}
function animateChildren(visualElement, variant, delayChildren, staggerChildren, staggerDirection, options) {
if (delayChildren === void 0) { delayChildren = 0; }
if (staggerChildren === void 0) { staggerChildren = 0; }
if (staggerDirection === void 0) { staggerDirection = 1; }
var animations = [];
var maxStaggerDuration = (visualElement.variantChildren.size - 1) * staggerChildren;
var generateStaggerDuration = staggerDirection === 1
? function (i) {
if (i === void 0) { i = 0; }
return i * staggerChildren;
}
: function (i) {
if (i === void 0) { i = 0; }
return maxStaggerDuration - i * staggerChildren;
};
Array.from(visualElement.variantChildren)
.sort(sortByTreeOrder)
.forEach(function (child, i) {
animations.push(animateVariant(child, variant, tslib.__assign(tslib.__assign({}, options), { delay: delayChildren + generateStaggerDuration(i) })));
});
return Promise.all(animations);
}
function stopAnimation(visualElement) {
visualElement.forEachValue(function (value) { return value.stop(); });
}
function sortByTreeOrder(a, b) {
return a.sortNodePosition(b);
}
/**
* 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(_a, key) {
var protectedKeys = _a.protectedKeys, needsAnimating = _a.needsAnimating;
var shouldBlock = protectedKeys.hasOwnProperty(key) && needsAnimating[key] !== true;
needsAnimating[key] = false;
return shouldBlock;
}
function animationControls() {
/**
* Track whether the host component has mounted.
*/
var hasMounted = false;
/**
* Pending animations that are started before a component is mounted.
* TODO: Remove this as animations should only run in effects
*/
var pendingAnimations = [];
/**
* A collection of linked component animation controls.
*/
var subscribers = new Set();
var controls = {
subscribe: function (visualElement) {
subscribers.add(visualElement);
return function () { return void subscribers.delete(visualElement); };
},
start: function (definition, transitionOverride) {
/**
* TODO: We only perform this hasMounted check because in Framer we used to
* encourage the ability to start an animation within the render phase. This
* isn't behaviour concurrent-safe so when we make Framer concurrent-safe
* we can ditch this.
*/
if (hasMounted) {
var animations_1 = [];
subscribers.forEach(function (visualElement) {
animations_1.push(animateVisualElement(visualElement, definition, {
transitionOverride: transitionOverride,
}));
});
return Promise.all(animations_1);
}
else {
return new Promise(function (resolve) {
pendingAnimations.push({
animation: [definition, transitionOverride],
resolve: resolve,
});
});
}
},
set: function (definition) {
heyListen.invariant(hasMounted, "controls.set() should only be called after a component has mounted. Consider calling within a useEffect hook.");
return subscribers.forEach(function (visualElement) {
setValues(visualElement, definition);
});
},
stop: function () {
subscribers.forEach(function (visualElement) {
stopAnimation(visualElement);
});
},
mount: function () {
hasMounted = true;
pendingAnimations.forEach(function (_a) {
var animation = _a.animation, resolve = _a.resolve;
controls.start.apply(controls, tslib.__spread(animation)).then(resolve);
});
return function () {
hasMounted = false;
controls.stop();
};
},
};
return controls;
}
function isAnimationControls(v) {
return typeof v === "object" && typeof v.start === "function";
}
function isCSSVariable(value) {
return typeof value === "string" && value.startsWith("var(--");
}
/**
* Parse Framer's special CSS variable format into a CSS token and a fallback.
*
* ```
* `var(--foo, #fff)` => [`--foo`, '#fff']
* ```
*
* @param current
*/
var cssVariableRegex = /var\((--[a-zA-Z0-9-_]+),? ?([a-zA-Z0-9 ()%#.,-]+)?\)/;
function parseCSSVariable(current) {
var match = cssVariableRegex.exec(current);
if (!match)
return [,];
var _a = tslib.__read(match, 3), token = _a[1], fallback = _a[2];
return [token, fallback];
}
var maxDepth = 4;
function getVariableValue(current, element, depth) {
if (depth === void 0) { depth = 1; }
heyListen.invariant(depth <= maxDepth, "Max CSS variable fallback depth detected in property \"" + current + "\". This may indicate a circular fallback dependency.");
var _a = tslib.__read(parseCSSVariable(current), 2), token = _a[0], fallback = _a[1];
// No CSS variable detected
if (!token)
return;
// Attempt to read this CSS variable off the element
var resolved = window.getComputedStyle(element).getPropertyValue(token);
if (resolved) {
return resolved.trim();
}
else if (isCSSVariable(fallback)) {
// The fallback might itself be a CSS variable, in which case we attempt to resolve it too.
return getVariableValue(fallback, element, depth + 1);
}
else {
return fallback;
}
}
/**
* Resolve CSS variables from
*
* @internal
*/
function resolveCSSVariables(visualElement, _a, transitionEnd) {
var _b;
var target = tslib.__rest(_a, []);
var element = visualElement.getInstance();
if (!(element instanceof HTMLElement))
return { target: target, transitionEnd: transitionEnd };
// If `transitionEnd` isn't `undefined`, clone it. We could clone `target` and `transitionEnd`
// only if they change but I think this reads clearer and this isn't a performance-critical path.
if (transitionEnd) {
transitionEnd = tslib.__assign({}, transitionEnd);
}
// Go through existing `MotionValue`s and ensure any existing CSS variables are resolved
visualElement.forEachValue(function (value) {
var current = value.get();
if (!isCSSVariable(current))
return;
var resolved = getVariableValue(current, element);
if (resolved)
value.set(resolved);
});
// Cycle through every target property and resolve CSS variables. Currently
// we only read single-var properties like `var(--foo)`, not `calc(var(--foo) + 20px)`
for (var key in target) {
var current = target[key];
if (!isCSSVariable(current))
continue;
var resolved = getVariableValue(current, element);
if (!resolved)
continue;
// Clone target if it hasn't already been
target[key] = resolved;
// If the user hasn't already set this key on `transitionEnd`, set it to the unresolved
// CSS variable. This will ensure that after the animation the component will reflect
// changes in the value of the CSS variable.
if (transitionEnd)
(_b = transitionEnd[key]) !== null && _b !== void 0 ? _b : (transitionEnd[key] = current);
}
return { target: target, transitionEnd: transitionEnd };
}
function pixelsToPercent(pixels, axis) {
return (pixels / (axis.max - axis.min)) * 100;
}
/**
* We always correct borderRadius as a percentage rather than pixels to reduce paints.
* For example, if you are projecting a box that is 100px wide with a 10px borderRadius
* into a box that is 200px wide with a 20px borderRadius, that is actually a 10%
* borderRadius in both states. If we animate between the two in pixels that will trigger
* a paint each time. If we animate between the two in percentage we'll avoid a paint.
*/
function correctBorderRadius(latest, _layoutState, _a) {
var target = _a.target;
/**
* If latest is a string, if it's a percentage we can return immediately as it's
* going to be stretched appropriately. Otherwise, if it's a pixel, convert it to a number.
*/
if (typeof latest === "string") {
if (styleValueTypes.px.test(latest)) {
latest = parseFloat(latest);
}
else {
return latest;
}
}
/**
* If latest is a number, it's a pixel value. We use the current viewportBox to calculate that
* pixel value as a percentage of each axis
*/
var x = pixelsToPercent(latest, target.x);
var y = pixelsToPercent(latest, target.y);
return x + "% " + y + "%";
}
var varToken = "_$css";
function correctBoxShadow(latest, _a) {
var delta = _a.delta, treeScale = _a.treeScale;
var original = latest;
/**
* We need to first strip and store CSS variables from the string.
*/
var containsCSSVariables = latest.includes("var(");
var cssVariables = [];
if (containsCSSVariables) {
latest = latest.replace(cssVariableRegex, function (match) {
cssVariables.push(match);
return varToken;
});
}
var shadow = styleValueTypes.complex.parse(latest);
// TODO: Doesn't support multiple shadows
if (shadow.length > 5)
return original;
var template = styleValueTypes.complex.createTransformer(latest);
var offset = typeof shadow[0] !== "number" ? 1 : 0;
// Calculate the overall context scale
var xScale = delta.x.scale * treeScale.x;
var yScale = delta.y.scale * treeScale.y;
shadow[0 + offset] /= xScale;
shadow[1 + offset] /= yScale;
/**
* Ideally we'd correct x and y scales individually, but because blur and
* spread apply to both we have to take a scale average and apply that instead.
* We could potentially improve the outcome of this by incorporating the ratio between
* the two scales.
*/
var averageScale = popmotion.mix(xScale, yScale, 0.5);
// Blur
if (typeof shadow[2 + offset] === "number")
shadow[2 + offset] /= averageScale;
// Spread
if (typeof shadow[3 + offset] === "number")
shadow[3 + offset] /= averageScale;
var output = template(shadow);
if (containsCSSVariables) {
var i_1 = 0;
output = output.replace(varToken, function () {
var cssVariable = cssVariables[i_1];
i_1++;
return cssVariable;
});
}
return output;
}
var borderCorrectionDefinition = {
process: correctBorderRadius,
};
var valueScaleCorrection = {
borderRadius: tslib.__assign(tslib.__assign({}, borderCorrectionDefinition), { applyTo: [
"borderTopLeftRadius",
"borderTopRightRadius",
"borderBottomLeftRadius",
"borderBottomRightRadius",
] }),
borderTopLeftRadius: borderCorrectionDefinition,
borderTopRightRadius: borderCorrectionDefinition,
borderBottomLeftRadius: borderCorrectionDefinition,
borderBottomRightRadius: borderCorrectionDefinition,
boxShadow: {
process: correctBoxShadow,
},
};
/**
* @internal
*/
function addScaleCorrection(correctors) {
for (var key in correctors) {
valueScaleCorrection[key] = correctors[key];
}
}
function isForcedMotionValue(key, _a) {
var layout = _a.layout, layoutId = _a.layoutId;
return (isTransformProp(key) ||
isTransformOriginProp(key) ||
((layout || layoutId !== undefined) && !!valueScaleCorrection[key]));
}
var createProjectionState = function () { return ({
isEnabled: false,
isTargetLocked: false,
target: axisBox(),
targetFinal: axisBox(),
}); };
function createVisualState(props, parent, blockInitialAnimation) {
var style = props.style;
var values = {};
for (var key in style) {
if (isMotionValue(style[key])) {
values[key] = style[key].get();
}
else if (isForcedMotionValue(key, props)) {
values[key] = style[key];
}
}
var initial = props.initial, animate = props.animate;
var isControllingVariants = checkIfControllingVariants(props);
var isVariantNode = isControllingVariants || props.variants;
if (isVariantNode && !isControllingVariants && props.inherit !== false) {
var context = parent === null || parent === void 0 ? void 0 : parent.getVariantContext();
if (context) {
initial !== null && initial !== void 0 ? initial : (initial = context.initial);
animate !== null && animate !== void 0 ? animate : (animate = context.animate);
}
}
var initialToSet = blockInitialAnimation || initial === false ? animate : initial;
if (initialToSet &&
typeof initialToSet !== "boolean" &&
!isAnimationControls(initialToSet)) {
var list = Array.isArray(initialToSet) ? initialToSet : [initialToSet];
list.forEach(function (definition) {
var resolved = resolveVariantFromProps(props, definition);
if (!resolved)
return;
var transitionEnd = resolved.transitionEnd, transition = resolved.transition, target = tslib.__rest(resolved, ["transitionEnd", "transition"]);
for (var key in target)
values[key] = target[key];
for (var key in transitionEnd)
values[key] = transitionEnd[key];
});
}
return values;
}
function createLayoutState() {
return {
isHydrated: false,
layout: axisBox(),
layoutCorrected: axisBox(),
treeScale: { x: 1, y: 1 },
delta: delta(),
deltaFinal: delta(),
deltaTransform: "",
};
}
var zeroLayout = createLayoutState();
var translateAlias = {
x: "translateX",
y: "translateY",
z: "translateZ",
transformPerspective: "perspective",
};
/**
* Build a CSS transform style from individual x/y/scale etc properties.
*
* This outputs with a default order of transforms/scales/rotations, this can be customised by
* providing a transformTemplate function.
*/
function buildTransform(_a, _b, transformIsDefault, transformTemplate) {
var transform = _a.transform, transformKeys = _a.transformKeys;
var _c = _b.enableHardwareAcceleration, enableHardwareAcceleration = _c === void 0 ? true : _c, _d = _b.allowTransformNone, allowTransformNone = _d === void 0 ? true : _d;
// The transform string we're going to build into.
var transformString = "";
// Transform keys into their default order - this will determine the output order.
transformKeys.sort(sortTransformProps);
// Track whether the defined transform has a defined z so we don't add a
// second to enable hardware acceleration
var transformHasZ = false;
// Loop over each transform and build them into transformString
var numTransformKeys = transformKeys.length;
for (var i = 0; i < numTransformKeys; i++) {
var key = transformKeys[i];
transformString += (translateAlias[key] || key) + "(" + transform[key] + ") ";
if (key === "z")
transformHasZ = true;
}
if (!transformHasZ && enableHardwareAcceleration) {
transformString += "translateZ(0)";
}
else {
transformString = transformString.trim();
}
// If we have a custom `transform` template, pass our transform values and
// generated transformString to that before returning
if (transformTemplate) {
transformString = transformTemplate(transform, transformIsDefault ? "" : transformString);
}
else if (allowTransformNone && transformIsDefault) {
transformString = "none";
}
return transformString;
}
/**
* Build a transformOrigin style. Uses the same defaults as the browser for
* undefined origins.
*/
function buildTransformOrigin(_a) {
var _b = _a.originX, originX = _b === void 0 ? "50%" : _b, _c = _a.originY, originY = _c === void 0 ? "50%" : _c, _d = _a.originZ, originZ = _d === void 0 ? 0 : _d;
return originX + " " + originY + " " + originZ;
}
/**
* Build a transform style that takes a calculated delta between the element's current
* space on screen and projects it into the desired space.
*/
function buildLayoutProjectionTransform(_a, treeScale, latestTransform) {
var x = _a.x, y = _a.y;
/**
* The translations we use to calculate are always relative to the viewport coordinate space.
* But when we apply scales, we also scale the coordinate space of an element and its children.
* For instance if we have a treeScale (the culmination of all parent scales) of 0.5 and we need
* to move an element 100 pixels, we actually need to move it 200 in within that scaled space.
*/
var xTranslate = x.translate / treeScale.x;
var yTranslate = y.translate / treeScale.y;
var transform = "translate3d(" + xTranslate + "px, " + yTranslate + "px, 0) ";
if (latestTransform) {
var rotate = latestTransform.rotate, rotateX = latestTransform.rotateX, rotateY = latestTransform.rotateY;
if (rotate)
transform += "rotate(" + rotate + ") ";
if (rotateX)
transform += "rotateX(" + rotateX + ") ";
if (rotateY)
transform += "rotateY(" + rotateY + ") ";
}
transform += "scale(" + x.scale + ", " + y.scale + ")";
return !latestTransform && transform === identityProjection ? "" : transform;
}
/**
* Take the calculated delta origin and apply it as a transform string.
*/
function buildLayoutProjectionTransformOrigin(_a) {
var deltaFinal = _a.deltaFinal;
return deltaFinal.x.origin * 100 + "% " + deltaFinal.y.origin * 100 + "% 0";
}
var identityProjection = buildLayoutProjectionTransform(zeroLayout.delta, zeroLayout.treeScale, { x: 1, y: 1 });
function shallowCompare(next, prev) {
if (!Array.isArray(prev))
return false;
var prevLength = prev.length;
if (prevLength !== next.length)
return false;
for (var i = 0; i < prevLength; i++) {
if (prev[i] !== next[i])
return false;
}
return true;
}
var AnimationType;
(function (AnimationType) {
AnimationType["Animate"] = "animate";
AnimationType["Hover"] = "whileHover";
AnimationType["Tap"] = "whileTap";
AnimationType["Drag"] = "whileDrag";
AnimationType["Focus"] = "whileFocus";
AnimationType["Exit"] = "exit";
})(AnimationType || (AnimationType = {}));
var variantPriorityOrder = [
AnimationType.Animate,
AnimationType.Hover,
AnimationType.Tap,
AnimationType.Drag,
AnimationType.Focus,
AnimationType.Exit,
];
var reversePriorityOrder = tslib.__spread(variantPriorityOrder).reverse();
var numAnimationTypes = variantPriorityOrder.length;
function animateList(visualElement) {
return function (animations) {
return Promise.all(animations.map(function (_a) {
var animation = _a.animation, options = _a.options;
return animateVisualElement(visualElement, animation, options);
}));
};
}
function createAnimationState(visualElement) {
var animate = animateList(visualElement);
var state = createState();
var allAnimatedKeys = {};
var isInitialRender = true;
/**
* This function will be used to reduce the animation definitions for
* each active animation type into an object of resolved values for it.
*/
var buildResolvedTypeValues = function (acc, definition) {
var resolved = resolveVariant(visualElement, definition);
if (resolved) {
var transition = resolved.transition, transitionEnd = resolved.transitionEnd, target = tslib.__rest(resolved, ["transition", "transitionEnd"]);
acc = tslib.__assign(tslib.__assign(tslib.__assign({}, acc), target), transitionEnd);
}
return acc;
};
function isAnimated(key) {
return allAnimatedKeys[key] !== undefined;
}
/**
* This just allows us to inject mocked animation functions
* @internal
*/
function setAnimateFunction(makeAnimator) {
animate = makeAnimator(visualElement);
}
/**
* When we receive new props, we need to:
* 1. Create a list of protected keys for each type. This is a directory of
* value keys that are currently being "handled" by types of a higher priority
* so that whenever an animation is played of a given type, these values are
* protected from being animated.
* 2. Determine if an animation type needs animating.
* 3. Determine if any values have been removed from a type and figure out
* what to animate those to.
*/
function animateChanges(options, changedActiveType) {
var props = visualElement.getProps();
var context = visualElement.getVariantContext(true) || {};
/**
* A list of animations that we'll build into as we iterate through the animation
* types. This will get executed at the end of the function.
*/
var animations = [];
/**
* Keep track of which values have been removed. Then, as we hit lower priority
* animation types, we can check if they contain removed values and animate to that.
*/
var removedKeys = new Set();
/**
* A dictionary of all encountered keys. This is an object to let us build into and
* copy it without iteration. Each time we hit an animation type we set its protected
* keys - the keys its not allowed to animate - to the latest version of this object.
*/
var encounteredKeys = {};
/**
* If a variant has been removed at a given index, and this component is controlling
* variant animations, we want to ensure lower-priority variants are forced to animate.
*/
var removedVariantIndex = Infinity;
var _loop_1 = function (i) {
var type = reversePriorityOrder[i];
var typeState = state[type];
var prop = (_a = props[type]) !== null && _a !== void 0 ? _a : context[type];
var propIsVariant = isVariantLabel(prop);
/**
* If this type has *just* changed isActive status, set activeDelta
* to that status. Otherwise set to null.
*/
var activeDelta = type === changedActiveType ? typeState.isActive : null;
if (activeDelta === false)
removedVariantIndex = i;
/**
* If this prop is an inherited variant, rather than been set directly on the
* component itself, we want to make sure we allow the parent to trigger animations.
*
* TODO: Can probably change this to a !isControllingVariants check
*/
var isInherited = prop === context[type] && prop !== props[type] && propIsVariant;
/**
*
*/
if (isInherited &&
isInitialRender &&
visualElement.manuallyAnimateOnMount) {
isInherited = false;
}
/**
* Set all encountered keys so far as the protected keys for this type. This will
* be any key that has been animated or otherwise handled by active, higher-priortiy types.
*/
typeState.protectedKeys = tslib.__assign({}, encounteredKeys);
// Check if we can skip analysing this prop early
if (
// If it isn't active and hasn't *just* been set as inactive
(!typeState.isActive && activeDelta === null) ||
// If we didn't and don't have any defined prop for this animation type
(!prop && !typeState.prevProp) ||
// Or if the prop doesn't define an animation
isAnimationControls(prop) ||
typeof prop === "boolean") {
return "continue";
}
/**
* As we go look through the values defined on this type, if we detect
* a changed value or a value that was removed in a higher priority, we set
* this to true and add this prop to the animation list.
*/
var shouldAnimateType = variantsHaveChanged(typeState.prevProp, prop) ||
// If we're making this variant active, we want to always make it active
(type === changedActiveType &&
typeState.isActive &&
!isInherited &&
propIsVariant) ||
// If we removed a higher-priority variant (i is in reverse order)
(i > removedVariantIndex && propIsVariant);
/**
* As animations can be set as variant lists, variants or target objects, we
* coerce everything to an array if it isn't one already
*/
var definitionList = Array.isArray(prop) ? prop : [prop];
/**
* Build an object of all the resolved values. We'll use this in the subsequent
* animateChanges calls to determine whether a value has changed.
*/
var resolvedValues = definitionList.reduce(buildResolvedTypeValues, {});
if (activeDelta === false)
resolvedValues = {};
/**
* Now we need to loop through all the keys in the prev prop and this prop,
* and decide:
* 1. If the value has changed, and needs animating
* 2. If it has been removed, and needs adding to the removedKeys set
* 3. If it has been removed in a higher priority type and needs animating
* 4. If it hasn't been removed in a higher priority but hasn't changed, and
* needs adding to the type's protectedKeys list.
*/
var _a = typeState.prevResolvedValues, prevResolvedValues = _a === void 0 ? {} : _a;
var allKeys = tslib.__assign(tslib.__assign({}, prevResolvedValues), resolvedValues);
var markToAnimate = function (key) {
shouldAnimateType = true;
removedKeys.delete(key);
typeState.needsAnimating[key] = true;
};
for (var key in allKeys) {
var next = resolvedValues[key];
var prev = prevResolvedValues[key];
// If we've already handled this we can just skip ahead
if (encounteredKeys.hasOwnProperty(key))
continue;
/**
* If the value has changed, we probably want to animate it.
*/
if (next !== prev) {
/**
* If both values are keyframes, we need to shallow compare them to
* detect whether any value has changed. If it has, we animate it.
*/
if (isKeyframesTarget(next) && isKeyframesTarget(prev)) {
if (!shallowCompare(next, prev)) {
markToAnimate(key);
}
else {
/**
* If it hasn't changed, we want to ensure it doesn't animate by
* adding it to the list of protected keys.
*/
typeState.protectedKeys[key] = true;
}
}
else if (next !== undefined) {
// If next is defined and doesn't equal prev, it needs animating
markToAnimate(key);
}
else {
// If it's undefined, it's been removed.
removedKeys.add(key);
}
}
else if (next !== undefined && removedKeys.has(key)) {
/**
* If next hasn't changed and it isn't undefined, we want to check if it's
* been removed by a higher priority
*/
markToAnimate(key);
}
else {
/**
* If it hasn't changed, we add it to the list of protected values
* to ensure it doesn't get animated.
*/
typeState.protectedKeys[key] = true;
}
}
/**
* Update the typeState so next time animateChanges is called we can compare the
* latest prop and resolvedValues to these.
*/
typeState.prevProp = prop;
typeState.prevResolvedValues = resolvedValues;
/**
*
*/
if (typeState.isActive) {
encounteredKeys = tslib.__assign(tslib.__assign({}, encounteredKeys), resolvedValues);
}
if (isInitialRender && visualElement.blockInitialAnimation) {
shouldAnimateType = false;
}
/**
* If this is an inherited prop we want to hard-block animations
* TODO: Test as this should probably still handle animations triggered
* by removed values?
*/
if (shouldAnimateType && !isInherited) {
animations.push.apply(animations, tslib.__spread(definitionList.map(function (animation) { return ({
animation: animation,
options: tslib.__assign({ type: type }, options),
}); })));
}
};
/**
* Iterate through all animation types in reverse priority order. For each, we want to
* detect which values it's handling and whether or not they've changed (and therefore
* need to be animated). If any values have been removed, we want to detect those in
* lower priority props and flag for animation.
*/
for (var i = 0; i < numAnimationTypes; i++) {
_loop_1(i);
}
allAnimatedKeys = tslib.__assign({}, encounteredKeys);
/**
* If there are some removed value that haven't been dealt with,
* we need to create a new animation that falls back either to the value
* defined in the style prop, or the last read value.
*/
if (removedKeys.size) {
var fallbackAnimation_1 = {};
removedKeys.forEach(function (key) {
var fallbackTarget = visualElement.getBaseTarget(key);
if (fallbackTarget !== undefined) {
fallbackAnimation_1[key] = fallbackTarget;
}
});
animations.push({ animation: fallbackAnimation_1 });
}
var shouldAnimate = Boolean(animations.length);
if (isInitialRender &&
props.initial === false &&
!visualElement.manuallyAnimateOnMount) {
shouldAnimate = false;
}
isInitialRender = false;
return shouldAnimate ? animate(animations) : Promise.resolve();
}
/**
* Change whether a certain animation type is active.
*/
function setActive(type, isActive, options) {
var _a;
// If the active state hasn't changed, we can safely do nothing here
if (state[type].isActive === isActive)
return Promise.resolve();
// Propagate active change to children
(_a = visualElement.variantChildren) === null || _a === void 0 ? void 0 : _a.forEach(function (child) { var _a; return (_a = child.animationState) === null || _a === void 0 ? void 0 : _a.setActive(type, isActive); });
state[type].isActive = isActive;
return animateChanges(options, type);
}
return {
isAnimated: isAnimated,
animateChanges: animateChanges,
setActive: setActive,
setAnimateFunction: setAnimateFunction,
getState: function () { return state; },
};
}
function variantsHaveChanged(prev, next) {
if (typeof next === "string") {
return next !== prev;
}
else if (isVariantLabels(next)) {
return !shallowCompare(next, prev);
}
return false;
}
function createTypeState(isActive) {
if (isActive === void 0) { isActive = false; }
return {
isActive: isActive,
protectedKeys: {},
needsAnimating: {},
prevResolvedValues: {},
};
}
function createState() {
var _a;
return _a = {},
_a[AnimationType.Animate] = createTypeState(true),
_a[AnimationType.Hover] = createTypeState(),
_a[AnimationType.Tap] = createTypeState(),
_a[AnimationType.Drag] = createTypeState(),
_a[AnimationType.Focus] = createTypeState(),
_a[AnimationType.Exit] = createTypeState(),
_a;
}
var names = [
"LayoutMeasure",
"BeforeLayoutMeasure",
"LayoutUpdate",
"ViewportBoxUpdate",
"Update",
"Render",
"AnimationComplete",
"LayoutAnimationComplete",
"AnimationStart",
"SetAxisTarget",
];
function createLifecycles() {
var managers = names.map(subscriptionManager);
var propSubscriptions = {};
var lifecycles = {
clearAllListeners: function () { return managers.forEach(function (manager) { return manager.clear(); }); },
updatePropListeners: function (props) {
return names.forEach(function (name) {
var _a;
(_a = propSubscriptions[name]) === null || _a === void 0 ? void 0 : _a.call(propSubscriptions);
var on = "on" + name;
var propListener = props[on];
if (propListener) {
propSubscriptions[name] = lifecycles[on](propListener);
}
});
},
};
managers.forEach(function (manager, i) {
lifecycles["on" + names[i]] = manager.add;
lifecycles["notify" + names[i]] = manager.notify;
});
return lifecycles;
}
function updateMotionValuesFromProps(element, next, prev) {
var _a;
for (var key in next) {
var nextValue = next[key];
var prevValue = prev[key];
if (isMotionValue(nextValue)) {
/**
* If this is a motion value found in props or style, we want to add it
* to our visual element's motion value map.
*/
element.addValue(key, nextValue);
}
else if (isMotionValue(prevValue)) {
/**
* If we're swapping to a new motion value, create a new motion value
* from that
*/
element.addValue(key, motionValue(nextValue));
}
else if (prevValue !== nextValue) {
/**
* If this is a flat value that has changed, update the motion value
* or create one if it doesn't exist. We only want to do this if we're
* not handling the value with our animation state.
*/
if (element.hasValue(key)) {
var existingValue = element.getValue(key);
// TODO: Only update values that aren't being animated or even looked at
!existingValue.hasAnimated && existingValue.set(nextValue);
}
else {
element.addValue(key, motionValue((_a = element.getStaticValue(key)) !== null && _a !== void 0 ? _a : nextValue));
}
}
}
// Handle removed values
for (var key in prev) {
if (next[key] === undefined)
element.removeValue(key);
}
return next;
}
function updateLayoutDeltas(_a, _b, treePath, transformOrigin) {
var delta = _a.delta, layout = _a.layout, layoutCorrected = _a.layoutCorrected, treeScale = _a.treeScale;
var target = _b.target;
/**
* Reset the corrected box with the latest values from box, as we're then going
* to perform mutative operations on it.
*/
resetBox(layoutCorrected, layout);
/**
* Apply all the parent deltas to this box to produce the corrected box. This
* is the layout box, as it will appear on screen as a result of the transforms of its parents.
*/
applyTreeDeltas(layoutCorrected, treeScale, treePath);
/**
* Update the delta between the corrected box and the target box before user-set transforms were applied.
* This will allow us to calculate the corrected borderRadius and boxShadow to compensate
* for our layout reprojection, but still allow them to be scaled correctly by the user.
* It might be that to simplify this we may want to accept that user-set scale1 is also corrected
* and we wouldn't have to keep and calc both deltas, OR we could support a user setting
* to allow people to choose whether these styles are corrected based on just the
* layout reprojection or the final bounding box.
*/
updateBoxDelta(delta, layoutCorrected, target, transformOrigin);
}
var visualElement = function (_a) {
var _b = _a.treeType, treeType = _b === void 0 ? "" : _b, createRenderState = _a.createRenderState, build = _a.build, getBaseTarget = _a.getBaseTarget, makeTargetAnimatable = _a.makeTargetAnimatable, measureViewportBox = _a.measureViewportBox, onMount = _a.onMount, renderInstance = _a.render, readValueFromInstance = _a.readValueFromInstance, resetTransform = _a.resetTransform, restoreTransform = _a.restoreTransform, removeValueFromMutableState = _a.removeValueFromMutableState, sortNodePosition = _a.sortNodePosition, scrapeMotionValuesFromProps = _a.scrapeMotionValuesFromProps;
return function (_a, options) {
var parent = _a.parent, externalRef = _a.ref, props = _a.props, isStatic = _a.isStatic, presenceId = _a.presenceId, blockInitialAnimation = _a.blockInitialAnimation;
if (options === void 0) { options = {}; }
/**
* The instance of the render-specific node that will be hydrated by the
* exposed React ref. So for example, this visual element can host a
* HTMLElement, plain object, or Three.js object. The functions provided
* in VisualElementConfig allow us to interface with this instance.
*/
var instance;
/**
* A set of all children of this visual element. We use this to traverse
* the tree when updating layout projections.
*/
var children = new Set();
/**
* Manages the subscriptions for a visual element's lifecycle, for instance
* onRender and onViewportBoxUpdate.
*/
var lifecycles = createLifecycles();
/**
*
*/
var projection = createProjectionState();
/**
* The latest resolved motion values.
*/
var latestValues = createVisualState(props, parent, blockInitialAnimation);
/**
* This is a reference to the visual state of the "lead" visual element.
* Usually, this will be this visual element. But if it shares a layoutId
* with other visual elements, only one of them will be designated lead by
* AnimateSharedLayout. All the other visual elements will take on the visual
* appearance of the lead while they crossfade to it.
*/
var leadProjection = projection;
var leadLatestValues = latestValues;
var unsubscribeFromLeadVisualElement;
/**
* The latest layout measurements and calculated projections. This
* is seperate from the target projection data in visualState as
* many visual elements might point to the same piece of visualState as
* a target, whereas they might each have different layouts and thus
* projection calculations needed to project into the same viewport box.
*/
var layoutState = createLayoutState();
/**
* Each visual element creates a pool of renderer-specific mutable state
* which allows renderer-specific calculations to occur while reducing GC.
*/
var renderState = createRenderState();
/**
*
*/
var crossfader;
/**
* Keep track of whether the viewport box has been updated since the
* last time the layout projection was re-calculated.
*/
var hasViewportBoxUpdated = false;
/**
* A map of all motion values attached to this visual element. Motion
* values are source of truth for any given animated value. A motion
* value might be provided externally by the component via props.
*/
var values = new Map();
/**
* A map of every subscription that binds the provided or generated
* motion values onChange listeners to this visual element.
*/
var valueSubscriptions = new Map();
/**
* A reference to the previously-provided motion values as returned
* from scrapeMotionValuesFromProps. We use the keys in here to determine
* if any motion values need to be removed after props are updated.
*/
var prevMotionValues = {};
/**
* x/y motion values that track the progress of initiated layout
* animations.
*
* TODO: Target for removal
*/
var projectionTargetProgress;
/**
* When values are removed from all animation props we need to search
* for a fallback value to animate to. These values are tracked in baseTarget.
*/
var baseTarget = tslib.__assign({}, latestValues);
// Internal methods ========================
/**
* On mount, this will be hydrated with a callback to disconnect
* this visual element from its parent on unmount.
*/
var removeFromMotionTree;
var removeFromVariantTree;
/**
*
*/
function mount() {
element.pointTo(element);
removeFromMotionTree = parent === null || parent === void 0 ? void 0 : parent.addChild(element);
if (isVariantNode && parent && !isControllingVariants) {
removeFromVariantTree = parent === null || parent === void 0 ? void 0 : parent.addVariantChild(element);
}
onMount === null || onMount === void 0 ? void 0 : onMount(element, instance, renderState);
}
/**
*
*/
function unmount() {
sync.cancelSync.update(update);
sync.cancelSync.render(render);
sync.cancelSync.preRender(element.updateLayoutProjection);
valueSubscriptions.forEach(function (remove) { return remove(); });
element.stopLayoutAnimation();
removeFromMotionTree === null || removeFromMotionTree === void 0 ? void 0 : removeFromMotionTree();
removeFromVariantTree === null || removeFromVariantTree === void 0 ? void 0 : removeFromVariantTree();
unsubscribeFromLeadVisualElement === null || unsubscribeFromLeadVisualElement === void 0 ? void 0 : unsubscribeFromLeadVisualElement();
lifecycles.clearAllListeners();
}
/**
*
*/
function isProjecting() {
return projection.isEnabled && layoutState.isHydrated;
}
/**
*
*/
function render() {
if (!instance)
return;
if (isProjecting()) {
/**
* Apply the latest user-set transforms to the targetBox to produce the targetBoxFinal.
* This is the final box that we will then project into by calculating a transform delta and
* applying it to the corrected box.
*/
applyBoxTransforms(leadProjection.targetFinal, leadProjection.target, leadLatestValues);
/**
* Update the delta between the corrected box and the final target box, after
* user-set transforms are applied to it. This will be used by the renderer to
* create a transform style that will reproject the element from its actual layout
* into the desired bounding box.
*/
updateBoxDelta(layoutState.deltaFinal, layoutState.layoutCorrected, leadProjection.targetFinal, latestValues);
}
triggerBuild();
renderInstance(instance, renderState);
}
function triggerBuild() {
var valuesToRender = latestValues;
if (crossfader && crossfader.isActive()) {
valuesToRender = crossfader.getCrossfadeState(element);
}
build(element, renderState, valuesToRender, leadProjection, layoutState, options, props);
}
function update() {
lifecycles.notifyUpdate(latestValues);
}
function updateLayoutProjection() {
var delta = layoutState.delta, treeScale = layoutState.treeScale;
var prevTreeScaleX = treeScale.x;
var prevTreeScaleY = treeScale.x;
var prevDeltaTransform = layoutState.deltaTransform;
updateLayoutDeltas(layoutState, leadProjection, element.path, latestValues);
hasViewportBoxUpdated &&
element.notifyViewportBoxUpdate(leadProjection.target, delta);
hasViewportBoxUpdated = false;
var deltaTransform = buildLayoutProjectionTransform(delta, treeScale);
if (deltaTransform !== prevDeltaTransform ||
// Also compare calculated treeScale, for values that rely on this only for scale correction
prevTreeScaleX !== treeScale.x ||
prevTreeScaleY !== treeScale.y) {
element.scheduleRender();
}
layoutState.deltaTransform = deltaTransform;
}
/**
*
*/
function bindToMotionValue(key, value) {
var removeOnChange = value.onChange(function (latestValue) {
latestValues[key] = latestValue;
props.onUpdate && sync__default['default'].update(update, false, true);
});
var removeOnRenderRequest = value.onRenderRequest(element.scheduleRender);
valueSubscriptions.set(key, function () {
removeOnChange();
removeOnRenderRequest();
});
}
/**
* Any motion values that are provided to the element when created
* aren't yet bound to the element, as this would technically be impure.
* However, we iterate through the motion values and set them to the
* initial values for this component.
*
* TODO: This is impure and we should look at changing this to run on mount.
* Doing so will break some tests but this isn't neccessarily a breaking change,
* more a reflection of the test.
*/
var initialMotionValues = scrapeMotionValuesFromProps(props);
for (var key in initialMotionValues) {
var value = initialMotionValues[key];
if (latestValues[key] !== undefined && isMotionValue(value)) {
value.set(latestValues[key], false);
}
}
/**
* Determine what role this visual element should take in the variant tree.
*/
var isControllingVariants = checkIfControllingVariants(props);
var definesInitialVariant = isVariantLabel(props.initial);
var isVariantNode = Boolean(definesInitialVariant || isControllingVariants || props.variants);
var element = tslib.__assign(tslib.__assign({ treeType: treeType,
/**
* This is a mirror of the internal instance prop, which keeps
* VisualElement type-compatible with React's RefObject.
*/
current: null,
/**
* The depth of this visual element within the visual element tree.
*/
depth: parent ? parent.depth + 1 : 0,
/**
* An ancestor path back to the root visual element. This is used
* by layout projection to quickly recurse back up the tree.
*/
path: parent ? tslib.__spread(parent.path, [parent]) : [],
/**
*
*/
presenceId: presenceId,
projection: projection,
/**
* If this component is part of the variant tree, it should track
* any children that are also part of the tree. This is essentially
* a shadow tree to simplify logic around how to stagger over children.
*/
variantChildren: isVariantNode ? new Set() : undefined,
/**
* Whether this instance is visible. This can be changed imperatively
* by AnimateSharedLayout, is analogous to CSS's visibility in that
* hidden elements should take up layout, and needs enacting by the configured
* render function.
*/
isVisible: undefined,
/**
* Normally, if a component is controlled by a parent's variants, it can
* rely on that ancestor to trigger animations further down the tree.
* However, if a component is created after its parent is mounted, the parent
* won't trigger that mount animation so the child needs to.
*
* TODO: This might be better replaced with a method isParentMounted
*/
manuallyAnimateOnMount: Boolean(parent === null || parent === void 0 ? void 0 : parent.isMounted()),
/**
* This can be set by AnimatePresence to force components that mount
* at the same time as it to mount as if they have initial={false} set.
*/
blockInitialAnimation: blockInitialAnimation,
/**
* If a visual element is static, it's essentially in "pure" mode with
* no additional functionality like animations or gestures loaded in.
* This can be considered Framer canvas mode.
*/
isStatic: isStatic,
/**
* A boolean that can be used to determine whether to respect hover events.
* For layout measurements we often have to reposition the instance by
* removing its transform. This can trigger hover events, which is
* undesired.
*/
isHoverEventsEnabled: true,
/**
* Determine whether this component has mounted yet. This is mostly used
* by variant children to determine whether they need to trigger their
* own animations on mount.
*/
isMounted: function () { return Boolean(instance); },
/**
* Add a child visual element to our set of children.
*/
addChild: function (child) {
children.add(child);
return function () { return children.delete(child); };
},
/**
* Add a child visual element to our set of children.
*/
addVariantChild: function (child) {
var _a;
var closestVariantNode = element.getClosestVariantNode();
if (closestVariantNode) {
(_a = closestVariantNode.variantChildren) === null || _a === void 0 ? void 0 : _a.add(child);
return function () { return closestVariantNode.variantChildren.delete(child); };
}
},
sortNodePosition: function (other) {
/**
* If these nodes aren't even of the same type we can't compare their depth.
*/
if (!sortNodePosition || treeType !== other.treeType)
return 0;
return sortNodePosition(element.getInstance(), other.getInstance());
},
/**
* Returns the closest variant node in the tree starting from
* this visual element.
*/
getClosestVariantNode: function () {
return isVariantNode ? element : parent === null || parent === void 0 ? void 0 : parent.getClosestVariantNode();
},
/**
* A method that schedules an update to layout projections throughout
* the tree. We inherit from the parent so there's only ever one
* job scheduled on the next frame - that of the root visual element.
*/
scheduleUpdateLayoutProjection: parent
? parent.scheduleUpdateLayoutProjection
: function () { return sync__default['default'].preRender(element.updateLayoutProjection, false, true); },
/**
* Expose the latest layoutId prop.
*/
getLayoutId: function () { return props.layoutId; },
/**
* Returns the current instance.
*/
getInstance: function () { return instance; },
/**
* Get/set the latest static values.
*/
getStaticValue: function (key) { return latestValues[key]; }, setStaticValue: function (key, value) { return (latestValues[key] = value); },
/**
* Returns the latest motion value state. Currently only used to take
* a snapshot of the visual element - perhaps this can return the whole
* visual state
*/
getLatestValues: function () { return latestValues; },
/**
* Replaces the current mutable states with fresh ones. This is used
* in static mode where rather than creating a new visual element every
* render we can just make fresh state.
*/
clearState: function (newProps) {
values.clear();
props = newProps;
leadLatestValues = latestValues = createVisualState(props, parent, blockInitialAnimation);
renderState = createRenderState();
},
/**
* Set the visiblity of the visual element. If it's changed, schedule
* a render to reflect these changes.
*/
setVisibility: function (visibility) {
if (element.isVisible === visibility)
return;
element.isVisible = visibility;
element.scheduleRender();
},
/**
* Make a target animatable by Popmotion. For instance, if we're
* trying to animate width from 100px to 100vw we need to measure 100vw
* in pixels to determine what we really need to animate to. This is also
* pluggable to support Framer's custom value types like Color,
* and CSS variables.
*/
makeTargetAnimatable: function (target, canMutate) {
if (canMutate === void 0) { canMutate = true; }
return makeTargetAnimatable(element, target, props, canMutate);
},
/**
* Temporarily suspend hover events while we remove transforms in order to measure the layout.
*
* This seems like an odd bit of scheduling but what we're doing is saying after
* the next render, wait 10 milliseconds before reenabling hover events. Waiting until
* the next frame results in missed, valid hover events. But triggering on the postRender
* frame is too soon to avoid triggering events with layout measurements.
*
* Note: If we figure out a way of measuring layout while transforms remain applied, this can be removed.
*/
suspendHoverEvents: function () {
element.isHoverEventsEnabled = false;
sync__default['default'].postRender(function () {
return setTimeout(function () { return (element.isHoverEventsEnabled = true); }, 10);
});
},
// Motion values ========================
/**
* Add a motion value and bind it to this visual element.
*/
addValue: function (key, value) {
// Remove existing value if it exists
if (element.hasValue(key))
element.removeValue(key);
values.set(key, value);
latestValues[key] = value.get();
bindToMotionValue(key, value);
},
/**
* Remove a motion value and unbind any active subscriptions.
*/
removeValue: function (key) {
var _a;
values.delete(key);
(_a = valueSubscriptions.get(key)) === null || _a === void 0 ? void 0 : _a();
valueSubscriptions.delete(key);
delete latestValues[key];
removeValueFromMutableState(key, renderState);
},
/**
* Check whether we have a motion value for this key
*/
hasValue: function (key) { return values.has(key); },
/**
* Get a motion value for this key. If called with a default
* value, we'll create one if none exists.
*/
getValue: function (key, defaultValue) {
var value = values.get(key);
if (value === undefined && defaultValue !== undefined) {
value = motionValue(defaultValue);
element.addValue(key, value);
}
return value;
},
/**
* Iterate over our motion values.
*/
forEachValue: function (callback) { return values.forEach(callback); },
/**
* If we're trying to animate to a previously unencountered value,
* we need to check for it in our state and as a last resort read it
* directly from the instance (which might have performance implications).
*/
readValue: function (key) { var _a; return (_a = latestValues[key]) !== null && _a !== void 0 ? _a : readValueFromInstance(instance, key, options); },
/**
* Set the base target to later animate back to. This is currently
* only hydrated on creation and when we first read a value.
*/
setBaseTarget: function (key, value) {
baseTarget[key] = value;
},
/**
* Find the base target for a value thats been removed from all animation
* props.
*/
getBaseTarget: function (key) {
if (getBaseTarget) {
var target = getBaseTarget(props, key);
if (target !== undefined && !isMotionValue(target))
return target;
}
return baseTarget[key];
} }, lifecycles), {
/**
* A ref function to be provided to the mounting React component.
* This is used to hydrated the instance and run mount/unmount lifecycles.
*/
ref: function (mountingElement) {
instance = element.current = mountingElement;
mountingElement ? mount() : unmount();
if (!externalRef)
return;
if (typeof externalRef === "function") {
externalRef(mountingElement);
}
else if (isRefObject(externalRef)) {
externalRef.current = mountingElement;
}
},
/**
* Build the renderer state based on the latest visual state.
*/
build: function () {
triggerBuild();
return renderState;
},
/**
* Schedule a render on the next animation frame.
*/
scheduleRender: function () {
sync__default['default'].render(render, false, true);
},
/**
* Synchronously fire render. It's prefered that we batch renders but
* in many circumstances, like layout measurement, we need to run this
* synchronously. However in those instances other measures should be taken
* to batch reads/writes.
*/
syncRender: render,
/**
* Update the provided props. Ensure any newly-added motion values are
* added to our map, old ones removed, and listeners updated.
*/
setProps: function (newProps) {
props = newProps;
lifecycles.updatePropListeners(newProps);
prevMotionValues = updateMotionValuesFromProps(element, scrapeMotionValuesFromProps(props), prevMotionValues);
}, getProps: function () { return props; },
// Variants ==============================
/**
* Returns the variant definition with a given name.
*/
getVariant: function (name) { var _a; return (_a = props.variants) === null || _a === void 0 ? void 0 : _a[name]; },
/**
* Returns the defined default transition on this component.
*/
getDefaultTransition: function () { return props.transition; },
/**
* Used by child variant nodes to get the closest ancestor variant props.
*/
getVariantContext: function (startAtParent) {
if (startAtParent === void 0) { startAtParent = false; }
if (startAtParent)
return parent === null || parent === void 0 ? void 0 : parent.getVariantContext();
if (!isControllingVariants) {
var context_1 = (parent === null || parent === void 0 ? void 0 : parent.getVariantContext()) || {};
if (props.initial !== undefined) {
context_1.initial = props.initial;
}
return context_1;
}
var context = {};
for (var i = 0; i < numVariantProps; i++) {
var name_1 = variantProps[i];
var prop = props[name_1];
if (isVariantLabel(prop) || prop === false) {
context[name_1] = prop;
}
}
return context;
},
// Layout projection ==============================
/**
* Enable layout projection for this visual element. Won't actually
* occur until we also have hydrated layout measurements.
*/
enableLayoutProjection: function () {
projection.isEnabled = true;
},
/**
* Lock the projection target, for instance when dragging, so
* nothing else can try and animate it.
*/
lockProjectionTarget: function () {
projection.isTargetLocked = true;
},
unlockProjectionTarget: function () {
element.stopLayoutAnimation();
projection.isTargetLocked = false;
},
/**
* Record the viewport box as it was before an expected mutation/re-render
*/
snapshotViewportBox: function () {
// TODO: Store this snapshot in LayoutState
element.prevViewportBox = element.measureViewportBox(false);
/**
* Update targetBox to match the prevViewportBox. This is just to ensure
* that targetBox is affected by scroll in the same way as the measured box
*/
element.rebaseProjectionTarget(false, element.prevViewportBox);
}, getLayoutState: function () { return layoutState; }, setCrossfader: function (newCrossfader) {
crossfader = newCrossfader;
},
/**
* Start a layout animation on a given axis.
* TODO: This could be better.
*/
startLayoutAnimation: function (axis, transition) {
var progress = element.getProjectionAnimationProgress()[axis];
var _a = projection.target[axis], min = _a.min, max = _a.max;
var length = max - min;
progress.clearListeners();
progress.set(min);
progress.set(min); // Set twice to hard-reset velocity
progress.onChange(function (v) {
return element.setProjectionTargetAxis(axis, v, v + length);
});
return element.animateMotionValue(axis, progress, 0, transition);
},
/**
* Stop layout animations.
*/
stopLayoutAnimation: function () {
eachAxis(function (axis) {
return element.getProjectionAnimationProgress()[axis].stop();
});
},
/**
* Measure the current viewport box with or without transforms.
* Only measures axis-aligned boxes, rotate and skew must be manually
* removed with a re-render to work.
*/
measureViewportBox: function (withTransform) {
if (withTransform === void 0) { withTransform = true; }
var viewportBox = measureViewportBox(instance, options);
if (!withTransform)
removeBoxTransforms(viewportBox, latestValues);
return viewportBox;
},
/**
* Update the layoutState by measuring the DOM layout. This
* should be called after resetting any layout-affecting transforms.
*/
updateLayoutMeasurement: function () {
element.notifyBeforeLayoutMeasure(layoutState.layout);
layoutState.isHydrated = true;
layoutState.layout = element.measureViewportBox();
layoutState.layoutCorrected = copyAxisBox(layoutState.layout);
element.notifyLayoutMeasure(layoutState.layout, element.prevViewportBox || layoutState.layout);
sync__default['default'].update(function () { return element.rebaseProjectionTarget(); });
},
/**
* Get the motion values tracking the layout animations on each
* axis. Lazy init if not already created.
*/
getProjectionAnimationProgress: function () {
projectionTargetProgress || (projectionTargetProgress = {
x: motionValue(0),
y: motionValue(0),
});
return projectionTargetProgress;
},
/**
* Update the projection of a single axis. Schedule an update to
* the tree layout projection.
*/
setProjectionTargetAxis: function (axis, min, max) {
var target = projection.target[axis];
target.min = min;
target.max = max;
// Flag that we want to fire the onViewportBoxUpdate event handler
hasViewportBoxUpdated = true;
lifecycles.notifySetAxisTarget();
},
/**
* Rebase the projection target on top of the provided viewport box
* or the measured layout. This ensures that non-animating elements
* don't fall out of sync differences in measurements vs projections
* after a page scroll or other relayout.
*/
rebaseProjectionTarget: function (force, box) {
if (box === void 0) { box = layoutState.layout; }
var _a = element.getProjectionAnimationProgress(), x = _a.x, y = _a.y;
var shouldRebase = !projection.isTargetLocked &&
!x.isAnimating() &&
!y.isAnimating();
if (force || shouldRebase) {
eachAxis(function (axis) {
var _a = box[axis], min = _a.min, max = _a.max;
element.setProjectionTargetAxis(axis, min, max);
});
}
},
/**
* Notify the visual element that its layout is up-to-date.
* Currently Animate.tsx uses this to check whether a layout animation
* needs to be performed.
*/
notifyLayoutReady: function (config) {
element.notifyLayoutUpdate(layoutState.layout, element.prevViewportBox || layoutState.layout, config);
},
/**
* Temporarily reset the transform of the instance.
*/
resetTransform: function () { return resetTransform(element, instance, props); },
/**
* Perform the callback after temporarily unapplying the transform
* upwards through the tree.
*/
withoutTransform: function (callback) {
var isEnabled = projection.isEnabled;
isEnabled && element.resetTransform();
parent ? parent.withoutTransform(callback) : callback();
isEnabled && restoreTransform(instance, renderState);
},
updateLayoutProjection: function () {
isProjecting() && updateLayoutProjection();
children.forEach(fireUpdateLayoutProjection);
},
/**
*
*/
pointTo: function (newLead) {
leadProjection = newLead.projection;
leadLatestValues = newLead.getLatestValues();
/**
* Subscribe to lead component's layout animations
*/
unsubscribeFromLeadVisualElement === null || unsubscribeFromLeadVisualElement === void 0 ? void 0 : unsubscribeFromLeadVisualElement();
unsubscribeFromLeadVisualElement = popmotion.pipe(newLead.onSetAxisTarget(element.scheduleUpdateLayoutProjection), newLead.onLayoutAnimationComplete(function () {
var _a;
if (element.isPresent) {
element.presence = Presence.Present;
}
else {
(_a = element.layoutSafeToRemove) === null || _a === void 0 ? void 0 : _a.call(element);
}
}));
},
// TODO: Clean this up
isPresent: true, presence: Presence.Entering });
return element;
};
};
function fireUpdateLayoutProjection(child) {
child.updateLayoutProjection();
}
var variantProps = tslib.__spread(["initial"], variantPriorityOrder);
var numVariantProps = variantProps.length;
/**
* Measure and return the element bounding box.
*
* We convert the box into an AxisBox2D to make it easier to work with each axis
* individually and programmatically.
*
* This function optionally accepts a transformPagePoint function which allows us to compensate
* for, for instance, measuring the element within a scaled plane like a Framer devivce preview component.
*/
function getBoundingBox(element, transformPagePoint) {
var box = element.getBoundingClientRect();
return convertBoundingBoxToAxisBox(transformBoundingBox(box, transformPagePoint));
}
/**
* Returns true if the provided key is a CSS variable
*/
function isCSSVariable$1(key) {
return key.startsWith("--");
}
function buildHTMLStyles(state, latestValues, projection, layoutState, options, transformTemplate) {
var _a;
var style = state.style, vars = state.vars, transform = state.transform, transformKeys = state.transformKeys, transformOrigin = state.transformOrigin;
// Empty the transformKeys array. As we're throwing out refs to its items
// this might not be as cheap as suspected. Maybe using the array as a buffer
// with a manual incrementation would be better.
transformKeys.length = 0;
// Track whether we encounter any transform or transformOrigin values.
var hasTransform = false;
var hasTransformOrigin = false;
// Does the calculated transform essentially equal "none"?
var transformIsNone = true;
/**
* Loop over all our latest animated values and decide whether to handle them
* as a style or CSS variable.
*
* Transforms and transform origins are kept seperately for further processing.
*/
for (var key in latestValues) {
var value = latestValues[key];
/**
* If this is a CSS variable we don't do any further processing.
*/
if (isCSSVariable$1(key)) {
vars[key] = value;
continue;
}
// Convert the value to its default value type, ie 0 -> "0px"
var valueType = getDefaultValueType(key);
var valueAsType = getValueAsType(value, valueType);
if (isTransformProp(key)) {
// If this is a transform, flag to enable further transform processing
hasTransform = true;
transform[key] = valueAsType;
transformKeys.push(key);
// If we already know we have a non-default transform, early return
if (!transformIsNone)
continue;
// Otherwise check to see if this is a default transform
if (value !== ((_a = valueType.default) !== null && _a !== void 0 ? _a : 0))
transformIsNone = false;
}
else if (isTransformOriginProp(key)) {
transformOrigin[key] = valueAsType;
// If this is a transform origin, flag and enable further transform-origin processing
hasTransformOrigin = true;
}
else {
/**
* If layout projection is on, and we need to perform scale correction for this
* value type, perform it.
*/
if (layoutState.isHydrated && valueScaleCorrection[key]) {
var correctedValue = valueScaleCorrection[key].process(value, layoutState, projection);
/**
* Scale-correctable values can define a number of other values to break
* down into. For instance borderRadius needs applying to borderBottomLeftRadius etc
*/
var applyTo = valueScaleCorrection[key].applyTo;
if (applyTo) {
var num = applyTo.length;
for (var i = 0; i < num; i++) {
style[applyTo[i]] = correctedValue;
}
}
else {
style[key] = correctedValue;
}
}
else {
style[key] = valueAsType;
}
}
}
if (projection.isEnabled && layoutState.isHydrated) {
style.transform = buildLayoutProjectionTransform(layoutState.deltaFinal, layoutState.treeScale, hasTransform ? transform : undefined);
if (transformTemplate) {
style.transform = transformTemplate(transform, style.transform);
}
style.transformOrigin = buildLayoutProjectionTransformOrigin(layoutState);
}
else {
if (hasTransform) {
style.transform = buildTransform(state, options, transformIsNone, transformTemplate);
}
if (hasTransformOrigin) {
style.transformOrigin = buildTransformOrigin(transformOrigin);
}
}
}
var positionalKeys = new Set([
"width",
"height",
"top",
"left",
"right",
"bottom",
"x",
"y",
]);
var isPositionalKey = function (key) { return positionalKeys.has(key); };
var hasPositionalKey = function (target) {
return Object.keys(target).some(isPositionalKey);
};
var setAndResetVelocity = function (value, to) {
// Looks odd but setting it twice doesn't render, it'll just
// set both prev and current to the latest value
value.set(to, false);
value.set(to);
};
var isNumOrPxType = function (v) {
return v === styleValueTypes.number || v === styleValueTypes.px;
};
var BoundingBoxDimension;
(function (BoundingBoxDimension) {
BoundingBoxDimension["width"] = "width";
BoundingBoxDimension["height"] = "height";
BoundingBoxDimension["left"] = "left";
BoundingBoxDimension["right"] = "right";
BoundingBoxDimension["top"] = "top";
BoundingBoxDimension["bottom"] = "bottom";
})(BoundingBoxDimension || (BoundingBoxDimension = {}));
var getPosFromMatrix = function (matrix, pos) {
return parseFloat(matrix.split(", ")[pos]);
};
var getTranslateFromMatrix = function (pos2, pos3) { return function (_bbox, _a) {
var transform = _a.transform;
if (transform === "none" || !transform)
return 0;
var matrix3d = transform.match(/^matrix3d\((.+)\)$/);
if (matrix3d) {
return getPosFromMatrix(matrix3d[1], pos3);
}
else {
var matrix = transform.match(/^matrix\((.+)\)$/);
if (matrix) {
return getPosFromMatrix(matrix[1], pos2);
}
else {
return 0;
}
}
}; };
var transformKeys = new Set(["x", "y", "z"]);
var nonTranslationalTransformKeys = transformProps.filter(function (key) { return !transformKeys.has(key); });
function removeNonTranslationalTransform(visualElement) {
var removedTransforms = [];
nonTranslationalTransformKeys.forEach(function (key) {
var value = visualElement.getValue(key);
if (value !== undefined) {
removedTransforms.push([key, value.get()]);
value.set(key.startsWith("scale") ? 1 : 0);
}
});
// Apply changes to element before measurement
if (removedTransforms.length)
visualElement.syncRender();
return removedTransforms;
}
var positionalValues = {
// Dimensions
width: function (_a) {
var x = _a.x;
return x.max - x.min;
},
height: function (_a) {
var y = _a.y;
return y.max - y.min;
},
top: function (_bbox, _a) {
var top = _a.top;
return parseFloat(top);
},
left: function (_bbox, _a) {
var left = _a.left;
return parseFloat(left);
},
bottom: function (_a, _b) {
var y = _a.y;
var top = _b.top;
return parseFloat(top) + (y.max - y.min);
},
right: function (_a, _b) {
var x = _a.x;
var left = _b.left;
return parseFloat(left) + (x.max - x.min);
},
// Transform
x: getTranslateFromMatrix(4, 13),
y: getTranslateFromMatrix(5, 14),
};
var convertChangedValueTypes = function (target, visualElement, changedKeys) {
var originBbox = visualElement.measureViewportBox();
var element = visualElement.getInstance();
var elementComputedStyle = getComputedStyle(element);
var display = elementComputedStyle.display, top = elementComputedStyle.top, left = elementComputedStyle.left, bottom = elementComputedStyle.bottom, right = elementComputedStyle.right, transform = elementComputedStyle.transform;
var originComputedStyle = { top: top, left: left, bottom: bottom, right: right, transform: transform };
// If the element is currently set to display: "none", make it visible before
// measuring the target bounding box
if (display === "none") {
visualElement.setStaticValue("display", target.display || "block");
}
// Apply the latest values (as set in checkAndConvertChangedValueTypes)
visualElement.syncRender();
var targetBbox = visualElement.measureViewportBox();
changedKeys.forEach(function (key) {
// Restore styles to their **calculated computed style**, not their actual
// originally set style. This allows us to animate between equivalent pixel units.
var value = visualElement.getValue(key);
setAndResetVelocity(value, positionalValues[key](originBbox, originComputedStyle));
target[key] = positionalValues[key](targetBbox, elementComputedStyle);
});
return target;
};
var checkAndConvertChangedValueTypes = function (visualElement, target, origin, transitionEnd) {
if (origin === void 0) { origin = {}; }
if (transitionEnd === void 0) { transitionEnd = {}; }
target = tslib.__assign({}, target);
transitionEnd = tslib.__assign({}, transitionEnd);
var targetPositionalKeys = Object.keys(target).filter(isPositionalKey);
// We want to remove any transform values that could affect the element's bounding box before
// it's measured. We'll reapply these later.
var removedTransformValues = [];
var hasAttemptedToRemoveTransformValues = false;
var changedValueTypeKeys = [];
targetPositionalKeys.forEach(function (key) {
var value = visualElement.getValue(key);
if (!visualElement.hasValue(key))
return;
var from = origin[key];
var to = target[key];
var fromType = findDimensionValueType(from);
var toType;
// TODO: The current implementation of this basically throws an error
// if you try and do value conversion via keyframes. There's probably
// a way of doing this but the performance implications would need greater scrutiny,
// as it'd be doing multiple resize-remeasure operations.
if (isKeyframesTarget(to)) {
var numKeyframes = to.length;
for (var i = to[0] === null ? 1 : 0; i < numKeyframes; i++) {
if (!toType) {
toType = findDimensionValueType(to[i]);
heyListen.invariant(toType === fromType ||
(isNumOrPxType(fromType) && isNumOrPxType(toType)), "Keyframes must be of the same dimension as the current value");
}
else {
heyListen.invariant(findDimensionValueType(to[i]) === toType, "All keyframes must be of the same type");
}
}
}
else {
toType = findDimensionValueType(to);
}
if (fromType !== toType) {
// If they're both just number or px, convert them both to numbers rather than
// relying on resize/remeasure to convert (which is wasteful in this situation)
if (isNumOrPxType(fromType) && isNumOrPxType(toType)) {
var current = value.get();
if (typeof current === "string") {
value.set(parseFloat(current));
}
if (typeof to === "string") {
target[key] = parseFloat(to);
}
else if (Array.isArray(to) && toType === styleValueTypes.px) {
target[key] = to.map(parseFloat);
}
}
else if ((fromType === null || fromType === void 0 ? void 0 : fromType.transform) && (toType === null || toType === void 0 ? void 0 : toType.transform) &&
(from === 0 || to === 0)) {
// If one or the other value is 0, it's safe to coerce it to the
// type of the other without measurement
if (from === 0) {
value.set(toType.transform(from));
}
else {
target[key] = fromType.transform(to);
}
}
else {
// If we're going to do value conversion via DOM measurements, we first
// need to remove non-positional transform values that could affect the bbox measurements.
if (!hasAttemptedToRemoveTransformValues) {
removedTransformValues = removeNonTranslationalTransform(visualElement);
hasAttemptedToRemoveTransformValues = true;
}
changedValueTypeKeys.push(key);
transitionEnd[key] =
transitionEnd[key] !== undefined
? transitionEnd[key]
: target[key];
setAndResetVelocity(value, to);
}
}
});
if (changedValueTypeKeys.length) {
var convertedTarget = convertChangedValueTypes(target, visualElement, changedValueTypeKeys);
// If we removed transform values, reapply them before the next render
if (removedTransformValues.length) {
removedTransformValues.forEach(function (_a) {
var _b = tslib.__read(_a, 2), key = _b[0], value = _b[1];
visualElement.getValue(key).set(value);
});
}
// Reapply original values
visualElement.syncRender();
return { target: convertedTarget, transitionEnd: transitionEnd };
}
else {
return { target: target, transitionEnd: transitionEnd };
}
};
/**
* Convert value types for x/y/width/height/top/left/bottom/right
*
* Allows animation between `'auto'` -> `'100%'` or `0` -> `'calc(50% - 10vw)'`
*
* @internal
*/
function unitConversion(visualElement, target, origin, transitionEnd) {
return hasPositionalKey(target)
? checkAndConvertChangedValueTypes(visualElement, target, origin, transitionEnd)
: { target: target, transitionEnd: transitionEnd };
}
/**
* Parse a DOM variant to make it animatable. This involves resolving CSS variables
* and ensuring animations like "20%" => "calc(50vw)" are performed in pixels.
*/
var parseDomVariant = function (visualElement, target, origin, transitionEnd) {
var resolved = resolveCSSVariables(visualElement, target, transitionEnd);
target = resolved.target;
transitionEnd = resolved.transitionEnd;
return unitConversion(visualElement, target, origin, transitionEnd);
};
function getComputedStyle$1(element) {
return window.getComputedStyle(element);
}
var htmlConfig = {
treeType: "dom",
readValueFromInstance: function (domElement, key) {
if (isTransformProp(key)) {
var defaultType = getDefaultValueType(key);
return defaultType ? defaultType.default || 0 : 0;
}
else {
var computedStyle = getComputedStyle$1(domElement);
return ((isCSSVariable$1(key)
? computedStyle.getPropertyValue(key)
: computedStyle[key]) || 0);
}
},
createRenderState: function () { return ({
style: {},
transform: {},
transformKeys: [],
transformOrigin: {},
vars: {},
}); },
sortNodePosition: function (a, b) {
/**
* compareDocumentPosition returns a bitmask, by using the bitwise &
* we're returning true if 2 in that bitmask is set to true. 2 is set
* to true if b preceeds a.
*/
return a.compareDocumentPosition(b) & 2 ? 1 : -1;
},
getBaseTarget: function (props, key) {
var _a;
return (_a = props.style) === null || _a === void 0 ? void 0 : _a[key];
},
measureViewportBox: function (element, _a) {
var transformPagePoint = _a.transformPagePoint;
return getBoundingBox(element, transformPagePoint);
},
/**
* Reset the transform on the current Element. This is called as part
* of a batched process across the entire layout tree. To remove this write
* cycle it'd be interesting to see if it's possible to "undo" all the current
* layout transforms up the tree in the same way this.getBoundingBoxWithoutTransforms
* works
*/
resetTransform: function (element, domElement, props) {
/**
* When we reset the transform of an element, there's a fair possibility that
* the element will visually move from underneath the pointer, triggering attached
* pointerenter/leave events. We temporarily suspend these while measurement takes place.
*/
element.suspendHoverEvents();
var transformTemplate = props.transformTemplate;
domElement.style.transform = transformTemplate
? transformTemplate({}, "")
: "none";
// Ensure that whatever happens next, we restore our transform on the next frame
element.scheduleRender();
},
restoreTransform: function (instance, mutableState) {
instance.style.transform = mutableState.style.transform;
},
removeValueFromMutableState: function (key, _a) {
var vars = _a.vars, style = _a.style;
delete vars[key];
delete style[key];
},
/**
* Ensure that HTML and Framer-specific value types like `px`->`%` and `Color`
* can be animated by Motion.
*/
makeTargetAnimatable: function (element, _a, _b, isMounted) {
var transformValues = _b.transformValues;
if (isMounted === void 0) { isMounted = true; }
var transition = _a.transition, transitionEnd = _a.transitionEnd, target = tslib.__rest(_a, ["transition", "transitionEnd"]);
var origin = getOrigin(target, transition || {}, element);
/**
* If Framer has provided a function to convert `Color` etc value types, convert them
*/
if (transformValues) {
if (transitionEnd)
transitionEnd = transformValues(transitionEnd);
if (target)
target = transformValues(target);
if (origin)
origin = transformValues(origin);
}
if (isMounted) {
checkTargetForNewValues(element, target, origin);
var parsed = parseDomVariant(element, target, origin, transitionEnd);
transitionEnd = parsed.transitionEnd;
target = parsed.target;
}
return tslib.__assign({ transition: transition,
transitionEnd: transitionEnd }, target);
},
scrapeMotionValuesFromProps: function (props) {
var style = props.style;
var newValues = {};
for (var key in style) {
if (isMotionValue(style[key]) || isForcedMotionValue(key, props)) {
newValues[key] = style[key];
}
}
return newValues;
},
build: function (element, renderState, latestValues, projection, layoutState, options, props) {
if (element.isVisible !== undefined) {
renderState.style.visibility = element.isVisible
? "visible"
: "hidden";
}
buildHTMLStyles(renderState, latestValues, projection, layoutState, options, props.transformTemplate);
},
render: function (element, _a) {
var style = _a.style, vars = _a.vars;
// Directly assign style into the Element's style prop. In tests Object.assign is the
// fastest way to assign styles.
Object.assign(element.style, style);
// Loop over any CSS variables and assign those.
for (var key in vars) {
element.style.setProperty(key, vars[key]);
}
},
};
var htmlVisualElement = visualElement(htmlConfig);
function calcOrigin$1(origin, offset, size) {
return typeof origin === "string"
? origin
: styleValueTypes.px.transform(offset + size * origin);
}
/**
* The SVG transform origin defaults are different to CSS and is less intuitive,
* so we use the measured dimensions of the SVG to reconcile these.
*/
function calcSVGTransformOrigin(dimensions, originX, originY) {
var pxOriginX = calcOrigin$1(originX, dimensions.x, dimensions.width);
var pxOriginY = calcOrigin$1(originY, dimensions.y, dimensions.height);
return pxOriginX + " " + pxOriginY;
}
// Convert a progress 0-1 to a pixels value based on the provided length
var progressToPixels = function (progress, length) {
return styleValueTypes.px.transform(progress * length);
};
var dashKeys = {
offset: "stroke-dashoffset",
array: "stroke-dasharray",
};
var camelKeys = {
offset: "strokeDashoffset",
array: "strokeDasharray",
};
/**
* Build SVG path properties. Uses the path's measured length to convert
* our custom pathLength, pathSpacing and pathOffset into stroke-dashoffset
* and stroke-dasharray attributes.
*
* This function is mutative to reduce per-frame GC.
*/
function buildSVGPath(attrs, totalLength, length, spacing, offset, useDashCase) {
if (spacing === void 0) { spacing = 1; }
if (offset === void 0) { offset = 0; }
if (useDashCase === void 0) { useDashCase = true; }
// We use dash case when setting attributes directly to the DOM node and camel case
// when defining props on a React component.
var keys = useDashCase ? dashKeys : camelKeys;
// Build the dash offset
attrs[keys.offset] = progressToPixels(-offset, totalLength);
// Build the dash array
var pathLength = progressToPixels(length, totalLength);
var pathSpacing = progressToPixels(spacing, totalLength);
attrs[keys.array] = pathLength + " " + pathSpacing;
}
/**
* Build SVG visual attrbutes, like cx and style.transform
*/
function buildSVGAttrs(state, _a, projection, layoutState, options, transformTemplate) {
var attrX = _a.attrX, attrY = _a.attrY, originX = _a.originX, originY = _a.originY, pathLength = _a.pathLength, _b = _a.pathSpacing, pathSpacing = _b === void 0 ? 1 : _b, _c = _a.pathOffset, pathOffset = _c === void 0 ? 0 : _c,
// This is object creation, which we try to avoid per-frame.
latest = tslib.__rest(_a, ["attrX", "attrY", "originX", "originY", "pathLength", "pathSpacing", "pathOffset"]);
buildHTMLStyles(state, latest, projection, layoutState, options, transformTemplate);
state.attrs = state.style;
state.style = {};
var attrs = state.attrs, style = state.style, dimensions = state.dimensions, totalPathLength = state.totalPathLength;
/**
* However, we apply transforms as CSS transforms. So if we detect a transform we take it from attrs
* and copy it into style.
*/
if (attrs.transform) {
style.transform = attrs.transform;
delete attrs.transform;
}
// Parse transformOrigin
if (originX !== undefined || originY !== undefined || style.transform) {
style.transformOrigin = calcSVGTransformOrigin(dimensions, originX !== undefined ? originX : 0.5, originY !== undefined ? originY : 0.5);
}
// Treat x/y not as shortcuts but as actual attributes
if (attrX !== undefined)
attrs.x = attrX;
if (attrY !== undefined)
attrs.y = attrY;
// Build SVG path if one has been measured
if (totalPathLength !== undefined && pathLength !== undefined) {
buildSVGPath(attrs, totalPathLength, pathLength, pathSpacing, pathOffset, false);
}
}
var CAMEL_CASE_PATTERN = /([a-z])([A-Z])/g;
var REPLACE_TEMPLATE = "$1-$2";
/**
* Convert camelCase to dash-case properties.
*/
var camelToDash = function (str) {
return str.replace(CAMEL_CASE_PATTERN, REPLACE_TEMPLATE).toLowerCase();
};
/**
* A set of attribute names that are always read/written as camel case.
*/
var camelCaseAttributes = new Set([
"baseFrequency",
"diffuseConstant",
"kernelMatrix",
"kernelUnitLength",
"keySplines",
"keyTimes",
"limitingConeAngle",
"markerHeight",
"markerWidth",
"numOctaves",
"targetX",
"targetY",
"surfaceScale",
"specularConstant",
"specularExponent",
"stdDeviation",
"tableValues",
"viewBox",
]);
var zeroDimensions = {
x: 0,
y: 0,
width: 0,
height: 0,
};
var svgMutableState = function () { return (tslib.__assign(tslib.__assign({}, htmlConfig.createRenderState()), { attrs: {}, dimensions: zeroDimensions })); };
var svgVisualElement = visualElement(tslib.__assign(tslib.__assign({}, htmlConfig), { createRenderState: svgMutableState, onMount: function (element, instance, mutableState) {
try {
mutableState.dimensions =
typeof instance.getBBox === "function"
? instance.getBBox()
: instance.getBoundingClientRect();
}
catch (e) {
// Most likely trying to measure an unrendered element under Firefox
mutableState.dimensions = zeroDimensions;
}
if (isPath(instance)) {
mutableState.totalPathLength = instance.getTotalLength();
}
/**
* Ensure we render the element as soon as possible to reflect the measured dimensions.
* Preferably this would happen synchronously but we put it in rAF to prevent layout thrashing.
*/
element.scheduleRender();
},
getBaseTarget: function (props, key) {
return props[key];
},
readValueFromInstance: function (domElement, key) {
var _a;
if (isTransformProp(key)) {
return ((_a = getDefaultValueType(key)) === null || _a === void 0 ? void 0 : _a.default) || 0;
}
key = !camelCaseAttributes.has(key) ? camelToDash(key) : key;
return domElement.getAttribute(key);
},
scrapeMotionValuesFromProps: function (props) {
var newValues = htmlConfig.scrapeMotionValuesFromProps(props);
for (var key in props) {
if (isMotionValue(props[key])) {
if (key === "x" || key === "y") {
key = "attr" + key.toUpperCase();
}
newValues[key] = props[key];
}
}
return newValues;
},
build: function (_element, renderState, latestValues, projection, layoutState, options, props) {
buildSVGAttrs(renderState, latestValues, projection, layoutState, options, props.transformTemplate);
},
render: function (element, mutableState) {
htmlConfig.render(element, mutableState);
for (var key in mutableState.attrs) {
element.setAttribute(!camelCaseAttributes.has(key) ? camelToDash(key) : key, mutableState.attrs[key]);
}
} }));
function isPath(element) {
return element.tagName === "path";
}
/**
* @internal
*/
/**
* We keep these listed seperately as we use the lowercase tag names as part
* of the runtime bundle to detect SVG components
*/
var lowercaseSVGElements = [
"animate",
"circle",
"defs",
"desc",
"ellipse",
"g",
"image",
"line",
"filter",
"marker",
"mask",
"metadata",
"path",
"pattern",
"polygon",
"polyline",
"rect",
"stop",
"svg",
"switch",
"symbol",
"text",
"tspan",
"use",
"view",
];
function isSVGComponent(Component) {
/**
* If it's not a string, it's a custom React component. Currently we only support
* HTML custom React components.
*/
if (typeof Component !== "string")
return false;
/**
* If it contains a dash, the element is a custom HTML webcomponent.
*/
if (Component.includes("-"))
return false;
/**
* If it's in our list of lowercase SVG tags, it's an SVG component
*/
if (lowercaseSVGElements.indexOf(Component) > -1)
return true;
/**
* If it contains a capital letter, it's an SVG component
*/
if (/[A-Z]/.test(Component))
return true;
}
function createDomVisualElement(Component, isStatic, options) {
var isSVG = isSVGComponent(Component);
var factory = isSVG ? svgVisualElement : htmlVisualElement;
return factory(options, {
enableHardwareAcceleration: !isStatic && !isSVG,
});
}
/**
* 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) {
var ref = React.useRef(null);
if (ref.current === null) {
ref.current = init();
}
return ref.current;
}
function useInitialMotionValues(visualElement) {
var createStyle = function () {
var _a = visualElement.build(), vars = _a.vars, style = _a.style;
return tslib.__assign(tslib.__assign({}, vars), style);
};
return visualElement.isStatic ? createStyle() : useConstant(createStyle);
}
function useStyle(visualElement, props) {
var styleProp = props.style || {};
var style = {};
/**
* Copy non-Motion Values straight into style
*/
for (var key in styleProp) {
// TODO We might want this to be a hasValue check? Although this could be impure
if (!isMotionValue(styleProp[key]) &&
!isForcedMotionValue(key, props)) {
style[key] = styleProp[key];
}
}
style = tslib.__assign(tslib.__assign({}, style), useInitialMotionValues(visualElement));
if (props.transformValues) {
style = props.transformValues(style);
}
return style;
}
function useHTMLProps(visualElement, props) {
// The `any` isn't ideal but it is the type of createElement props argument
var htmlProps = {};
var style = useStyle(visualElement, props);
if (Boolean(props.drag)) {
// Disable the ghost element when a user drags
htmlProps.draggable = false;
// Disable text selection
style.userSelect = style.WebkitUserSelect = style.WebkitTouchCallout =
"none";
// Disable scrolling on the draggable direction
style.touchAction =
props.drag === true
? "none"
: "pan-" + (props.drag === "x" ? "y" : "x");
}
htmlProps.style = style;
return htmlProps;
}
/**
* @public
*/
var MotionConfigContext = React.createContext({
transformPagePoint: function (p) { return p; },
features: [],
isStatic: false,
});
/**
* MotionConfig can be used in combination with the `m` component to cut bundle size
* and dynamically load only the features you use.
*
* ```jsx
* import {
* m as motion,
* AnimationFeature,
* MotionConfig
* } from "framer-motion"
*
* export function App() {
* return (
* <MotionConfig features={[AnimationFeature]}>
* <motion.div animate={{ x: 100 }} />
* </MotionConfig>
* )
* }
* ```
*
* @public
*/
function MotionConfig(_a) {
var children = _a.children, _b = _a.features, features = _b === void 0 ? [] : _b, transition = _a.transition, props = tslib.__rest(_a, ["children", "features", "transition"]);
var pluginContext = React.useContext(MotionConfigContext);
var loadedFeatures = tslib.__spread(new Set(tslib.__spread(pluginContext.features, features)));
// We do want to rerender children when the number of loaded features changes
var value = React.useMemo(function () { return ({
features: loadedFeatures,
transition: transition || pluginContext.transition,
}); }, [loadedFeatures.length, transition]);
// Mutative to prevent triggering rerenders in all listening
// components every time this component renders
for (var key in props) {
value[key] = props[key];
}
return (React.createElement(MotionConfigContext.Provider, { value: value }, children));
}
function useUnmountEffect(callback) {
return React.useEffect(function () { return function () { return callback(); }; }, []);
}
function addDomEvent(target, eventName, handler, options) {
target.addEventListener(eventName, handler, options);
return function () { return target.removeEventListener(eventName, handler, options); };
}
/**
* Attaches an event listener directly to the provided DOM element.
*
* Bypassing React's event system can be desirable, for instance when attaching non-passive
* event handlers.
*
* ```jsx
* const ref = useRef(null)
*
* useDomEvent(ref, 'wheel', onWheel, { passive: false })
*
* return <div ref={ref} />
* ```
*
* @param ref - React.RefObject that's been provided to the element you want to bind the listener to.
* @param eventName - Name of the event you want listen for.
* @param handler - Function to fire when receiving the event.
* @param options - Options to pass to `Event.addEventListener`.
*
* @public
*/
function useDomEvent(ref, eventName, handler, options) {
React.useEffect(function () {
var element = ref.current;
if (handler && element) {
return addDomEvent(element, eventName, handler, options);
}
}, [ref, eventName, handler, options]);
}
function isMouseEvent(event) {
// PointerEvent inherits from MouseEvent so we can't use a straight instanceof check.
if (typeof PointerEvent !== "undefined" && event instanceof PointerEvent) {
return !!(event.pointerType === "mouse");
}
return event instanceof MouseEvent;
}
function isTouchEvent(event) {
var hasTouches = !!event.touches;
return hasTouches;
}
/**
* Filters out events not attached to the primary pointer (currently left mouse button)
* @param eventHandler
*/
function filterPrimaryPointer(eventHandler) {
return function (event) {
var isMouseEvent = event instanceof MouseEvent;
var isPrimaryPointer = !isMouseEvent ||
(isMouseEvent && event.button === 0);
if (isPrimaryPointer) {
eventHandler(event);
}
};
}
var defaultPagePoint = { pageX: 0, pageY: 0 };
function pointFromTouch(e, pointType) {
if (pointType === void 0) { pointType = "page"; }
var primaryTouch = e.touches[0] || e.changedTouches[0];
var point = primaryTouch || defaultPagePoint;
return {
x: point[pointType + "X"],
y: point[pointType + "Y"],
};
}
function pointFromMouse(point, pointType) {
if (pointType === void 0) { pointType = "page"; }
return {
x: point[pointType + "X"],
y: point[pointType + "Y"],
};
}
function extractEventInfo(event, pointType) {
if (pointType === void 0) { pointType = "page"; }
return {
point: isTouchEvent(event)
? pointFromTouch(event, pointType)
: pointFromMouse(event, pointType),
};
}
function getViewportPointFromEvent(event) {
return extractEventInfo(event, "client");
}
var wrapHandler = function (handler, shouldFilterPrimaryPointer) {
if (shouldFilterPrimaryPointer === void 0) { shouldFilterPrimaryPointer = false; }
var listener = function (event) {
return handler(event, extractEventInfo(event));
};
return shouldFilterPrimaryPointer
? filterPrimaryPointer(listener)
: listener;
};
var isBrowser = typeof window !== "undefined";
// We check for event support via functions in case they've been mocked by a testing suite.
var supportsPointerEvents = function () {
return isBrowser && window.onpointerdown === null;
};
var supportsTouchEvents = function () {
return isBrowser && window.ontouchstart === null;
};
var supportsMouseEvents = function () {
return isBrowser && window.onmousedown === null;
};
var mouseEventNames = {
pointerdown: "mousedown",
pointermove: "mousemove",
pointerup: "mouseup",
pointercancel: "mousecancel",
pointerover: "mouseover",
pointerout: "mouseout",
pointerenter: "mouseenter",
pointerleave: "mouseleave",
};
var touchEventNames = {
pointerdown: "touchstart",
pointermove: "touchmove",
pointerup: "touchend",
pointercancel: "touchcancel",
};
function getPointerEventName(name) {
if (supportsPointerEvents()) {
return name;
}
else if (supportsTouchEvents()) {
return touchEventNames[name];
}
else if (supportsMouseEvents()) {
return mouseEventNames[name];
}
return name;
}
function addPointerEvent(target, eventName, handler, options) {
return addDomEvent(target, getPointerEventName(eventName), wrapHandler(handler, eventName === "pointerdown"), options);
}
function usePointerEvent(ref, eventName, handler, options) {
return useDomEvent(ref, getPointerEventName(eventName), handler && wrapHandler(handler, eventName === "pointerdown"), options);
}
/**
* @internal
*/
var PanSession = /** @class */ (function () {
function PanSession(event, handlers, _a) {
var _this = this;
var transformPagePoint = (_a === void 0 ? {} : _a).transformPagePoint;
/**
* @internal
*/
this.startEvent = null;
/**
* @internal
*/
this.lastMoveEvent = null;
/**
* @internal
*/
this.lastMoveEventInfo = null;
/**
* @internal
*/
this.handlers = {};
this.updatePoint = function () {
if (!(_this.lastMoveEvent && _this.lastMoveEventInfo))
return;
var info = getPanInfo(_this.lastMoveEventInfo, _this.history);
var isPanStarted = _this.startEvent !== null;
// Only start panning if the offset is larger than 3 pixels. If we make it
// any larger than this we'll want to reset the pointer history
// on the first update to avoid visual snapping to the cursoe.
var isDistancePastThreshold = popmotion.distance(info.offset, { x: 0, y: 0 }) >= 3;
if (!isPanStarted && !isDistancePastThreshold)
return;
var point = info.point;
var timestamp = sync.getFrameData().timestamp;
_this.history.push(tslib.__assign(tslib.__assign({}, point), { timestamp: timestamp }));
var _a = _this.handlers, onStart = _a.onStart, onMove = _a.onMove;
if (!isPanStarted) {
onStart && onStart(_this.lastMoveEvent, info);
_this.startEvent = _this.lastMoveEvent;
}
onMove && onMove(_this.lastMoveEvent, info);
};
this.handlePointerMove = function (event, info) {
_this.lastMoveEvent = event;
_this.lastMoveEventInfo = transformPoint(info, _this.transformPagePoint);
// Because Safari doesn't trigger mouseup events when it's above a `<select>`
if (isMouseEvent(event) && event.buttons === 0) {
_this.handlePointerUp(event, info);
return;
}
// Throttle mouse move event to once per frame
sync__default['default'].update(_this.updatePoint, true);
};
this.handlePointerUp = function (event, info) {
_this.end();
var onEnd = _this.handlers.onEnd;
if (!onEnd)
return;
var panInfo = getPanInfo(transformPoint(info, _this.transformPagePoint), _this.history);
onEnd && onEnd(event, panInfo);
};
// If we have more than one touch, don't start detecting this gesture
if (isTouchEvent(event) && event.touches.length > 1)
return;
this.handlers = handlers;
this.transformPagePoint = transformPagePoint;
var info = extractEventInfo(event);
var initialInfo = transformPoint(info, this.transformPagePoint);
var point = initialInfo.point;
var timestamp = sync.getFrameData().timestamp;
this.history = [tslib.__assign(tslib.__assign({}, point), { timestamp: timestamp })];
var onSessionStart = handlers.onSessionStart;
onSessionStart &&
onSessionStart(event, getPanInfo(initialInfo, this.history));
this.removeListeners = popmotion.pipe(addPointerEvent(window, "pointermove", this.handlePointerMove), addPointerEvent(window, "pointerup", this.handlePointerUp), addPointerEvent(window, "pointercancel", this.handlePointerUp));
}
PanSession.prototype.updateHandlers = function (handlers) {
this.handlers = handlers;
};
PanSession.prototype.end = function () {
this.removeListeners && this.removeListeners();
sync.cancelSync.update(this.updatePoint);
};
return PanSession;
}());
function transformPoint(info, transformPagePoint) {
return transformPagePoint ? { point: transformPagePoint(info.point) } : info;
}
function subtractPoint(a, b) {
return { x: a.x - b.x, y: a.y - b.y };
}
function getPanInfo(_a, history) {
var point = _a.point;
return {
point: point,
delta: subtractPoint(point, lastDevicePoint(history)),
offset: subtractPoint(point, startDevicePoint(history)),
velocity: getVelocity$1(history, 0.1),
};
}
function startDevicePoint(history) {
return history[0];
}
function lastDevicePoint(history) {
return history[history.length - 1];
}
function getVelocity$1(history, timeDelta) {
if (history.length < 2) {
return { x: 0, y: 0 };
}
var i = history.length - 1;
var timestampedPoint = null;
var lastPoint = lastDevicePoint(history);
while (i >= 0) {
timestampedPoint = history[i];
if (lastPoint.timestamp - timestampedPoint.timestamp >
secondsToMilliseconds(timeDelta)) {
break;
}
i--;
}
if (!timestampedPoint) {
return { x: 0, y: 0 };
}
var time = (lastPoint.timestamp - timestampedPoint.timestamp) / 1000;
if (time === 0) {
return { x: 0, y: 0 };
}
var currentVelocity = {
x: (lastPoint.x - timestampedPoint.x) / time,
y: (lastPoint.y - timestampedPoint.y) / time,
};
if (currentVelocity.x === Infinity) {
currentVelocity.x = 0;
}
if (currentVelocity.y === Infinity) {
currentVelocity.y = 0;
}
return currentVelocity;
}
/**
*
* @param handlers -
* @param ref -
*
* @internalremarks
* Currently this sets new pan gesture functions every render. The memo route has been explored
* in the past but ultimately we're still creating new functions every render. An optimisation
* to explore is creating the pan gestures and loading them into a `ref`.
*
* @internal
*/
function usePanGesture(_a, ref) {
var onPan = _a.onPan, onPanStart = _a.onPanStart, onPanEnd = _a.onPanEnd, onPanSessionStart = _a.onPanSessionStart;
var hasPanEvents = onPan || onPanStart || onPanEnd || onPanSessionStart;
var panSession = React.useRef(null);
var transformPagePoint = React.useContext(MotionConfigContext).transformPagePoint;
var handlers = {
onSessionStart: onPanSessionStart,
onStart: onPanStart,
onMove: onPan,
onEnd: function (event, info) {
panSession.current = null;
onPanEnd && onPanEnd(event, info);
},
};
React.useEffect(function () {
if (panSession.current !== null) {
panSession.current.updateHandlers(handlers);
}
});
function onPointerDown(event) {
panSession.current = new PanSession(event, handlers, {
transformPagePoint: transformPagePoint,
});
}
usePointerEvent(ref, "pointerdown", hasPanEvents && onPointerDown);
useUnmountEffect(function () { return panSession.current && panSession.current.end(); });
}
/**
* Recursively traverse up the tree to check whether the provided child node
* is the parent or a descendant of it.
*
* @param parent - Element to find
* @param child - Element to test against parent
*/
var isNodeOrChild = function (parent, child) {
if (!child) {
return false;
}
else if (parent === child) {
return true;
}
else {
return isNodeOrChild(parent, child.parentElement);
}
};
function createLock(name) {
var lock = null;
return function () {
var openLock = function () {
lock = null;
};
if (lock === null) {
lock = name;
return openLock;
}
return false;
};
}
var globalHorizontalLock = createLock("dragHorizontal");
var globalVerticalLock = createLock("dragVertical");
function getGlobalLock(drag) {
var lock = false;
if (drag === "y") {
lock = globalVerticalLock();
}
else if (drag === "x") {
lock = globalHorizontalLock();
}
else {
var openHorizontal_1 = globalHorizontalLock();
var openVertical_1 = globalVerticalLock();
if (openHorizontal_1 && openVertical_1) {
lock = function () {
openHorizontal_1();
openVertical_1();
};
}
else {
// Release the locks because we don't use them
if (openHorizontal_1)
openHorizontal_1();
if (openVertical_1)
openVertical_1();
}
}
return lock;
}
function isDragActive() {
// Check the gesture lock - if we get it, it means no drag gesture is active
// and we can safely fire the tap gesture.
var openGestureLock = getGlobalLock(true);
if (!openGestureLock)
return true;
openGestureLock();
return false;
}
/**
* @param handlers -
* @internal
*/
function useTapGesture(_a, visualElement) {
var onTap = _a.onTap, onTapStart = _a.onTapStart, onTapCancel = _a.onTapCancel, whileTap = _a.whileTap;
var hasPressListeners = onTap || onTapStart || onTapCancel || whileTap;
var isPressing = React.useRef(false);
var cancelPointerEndListeners = React.useRef(null);
function removePointerEndListener() {
var _a;
(_a = cancelPointerEndListeners.current) === null || _a === void 0 ? void 0 : _a.call(cancelPointerEndListeners);
cancelPointerEndListeners.current = null;
}
function checkPointerEnd() {
var _a;
removePointerEndListener();
isPressing.current = false;
(_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive(AnimationType.Tap, false);
return !isDragActive();
}
function onPointerUp(event, info) {
if (!checkPointerEnd())
return;
/**
* We only count this as a tap gesture if the event.target is the same
* as, or a child of, this component's element
*/
!isNodeOrChild(visualElement.getInstance(), event.target)
? onTapCancel === null || onTapCancel === void 0 ? void 0 : onTapCancel(event, info) : onTap === null || onTap === void 0 ? void 0 : onTap(event, info);
}
function onPointerCancel(event, info) {
if (!checkPointerEnd())
return;
onTapCancel === null || onTapCancel === void 0 ? void 0 : onTapCancel(event, info);
}
function onPointerDown(event, info) {
var _a;
removePointerEndListener();
if (isPressing.current)
return;
isPressing.current = true;
cancelPointerEndListeners.current = popmotion.pipe(addPointerEvent(window, "pointerup", onPointerUp), addPointerEvent(window, "pointercancel", onPointerCancel));
onTapStart === null || onTapStart === void 0 ? void 0 : onTapStart(event, info);
(_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive(AnimationType.Tap, true);
}
usePointerEvent(visualElement, "pointerdown", hasPressListeners ? onPointerDown : undefined);
useUnmountEffect(removePointerEndListener);
}
function createHoverEvent(visualElement, isActive, callback) {
return function (event, info) {
var _a;
if (!isMouseEvent(event) || !visualElement.isHoverEventsEnabled)
return;
callback === null || callback === void 0 ? void 0 : callback(event, info);
(_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive(AnimationType.Hover, isActive);
};
}
function useHoverGesture(_a, visualElement) {
var onHoverStart = _a.onHoverStart, onHoverEnd = _a.onHoverEnd, whileHover = _a.whileHover;
usePointerEvent(visualElement, "pointerenter", onHoverStart || whileHover
? createHoverEvent(visualElement, true, onHoverStart)
: undefined);
usePointerEvent(visualElement, "pointerleave", onHoverEnd || whileHover
? createHoverEvent(visualElement, false, onHoverEnd)
: undefined);
}
/**
*
* @param props
* @param ref
* @internal
*/
function useFocusGesture(_a, visualElement) {
var whileFocus = _a.whileFocus;
var onFocus = function () {
var _a;
(_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive(AnimationType.Focus, true);
};
var onBlur = function () {
var _a;
(_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive(AnimationType.Focus, false);
};
useDomEvent(visualElement, "focus", whileFocus ? onFocus : undefined);
useDomEvent(visualElement, "blur", whileFocus ? onBlur : undefined);
}
/**
* Add pan and tap gesture recognition to an element.
*
* @param props - Gesture event handlers
* @param ref - React `ref` containing a DOM `Element`
* @public
*/
function useGestures(props, visualElement) {
usePanGesture(props, visualElement);
useTapGesture(props, visualElement);
useHoverGesture(props, visualElement);
useFocusGesture(props, visualElement);
}
var makeRenderlessComponent = function (hook) { return function (props) {
hook(props);
return null;
}; };
var gestureProps = [
"onPan",
"onPanStart",
"onPanEnd",
"onPanSessionStart",
"onTap",
"onTapStart",
"onTapCancel",
"onHoverStart",
"onHoverEnd",
"whileFocus",
"whileTap",
"whileHover",
];
var GestureComponent = makeRenderlessComponent(function (_a) {
var visualElement = _a.visualElement, props = tslib.__rest(_a, ["visualElement"]);
useGestures(props, visualElement);
});
/**
* @public
*/
var Gestures = {
key: "gestures",
shouldRender: function (props) {
return gestureProps.some(function (key) { return props.hasOwnProperty(key); });
},
getComponent: function () { return GestureComponent; },
};
/**
* A list of all valid MotionProps.
*
* @internalremarks
* This doesn't throw if a `MotionProp` name is missing - it should.
*/
var validMotionProps = new Set(tslib.__spread([
"initial",
"animate",
"exit",
"style",
"variants",
"transition",
"transformTemplate",
"transformValues",
"custom",
"inherit",
"layout",
"layoutId",
"onLayoutAnimationComplete",
"onViewportBoxUpdate",
"onLayoutMeasure",
"onBeforeLayoutMeasure",
"onAnimationStart",
"onAnimationComplete",
"onUpdate",
"onDragStart",
"onDrag",
"onDragEnd",
"onMeasureDragConstraints",
"onDirectionLock",
"onDragTransitionEnd",
"drag",
"dragControls",
"dragListener",
"dragConstraints",
"dragDirectionLock",
"_dragX",
"_dragY",
"dragElastic",
"dragMomentum",
"dragPropagation",
"dragTransition",
"whileDrag"
], gestureProps));
/**
* Check whether a prop name is a valid `MotionProp` key.
*
* @param key - Name of the property to check
* @returns `true` is key is a valid `MotionProp`.
*
* @public
*/
function isValidMotionProp(key) {
return validMotionProps.has(key);
}
var isPropValid = function (key) { return !isValidMotionProp(key); };
/**
* Emotion and Styled Components both allow users to pass through arbitrary props to their components
* to dynamically generate CSS. They both use the `@emotion/is-prop-valid` package to determine which
* of these should be passed to the underlying DOM node.
*
* However, when styling a Motion component `styled(motion.div)`, both packages pass through *all* props
* as it's seen as an arbitrary component rather than a DOM node. Motion only allows arbitrary props
* passed through the `custom` prop so it doesn't *need* the payload or computational overhead of
* `@emotion/is-prop-valid`, however to fix this problem we need to use it.
*
* By making it an optionalDependency we can offer this functionality only in the situations where it's
* actually required.
*/
try {
var emotionIsPropValid_1 = require("@emotion/is-prop-valid").default;
isPropValid = function (key) {
// Handle events explicitly as Emotion validates them all as true
if (key.startsWith("on")) {
return !isValidMotionProp(key);
}
else {
return emotionIsPropValid_1(key);
}
};
}
catch (_a) {
// We don't need to actually do anything here - the fallback is the existing `isPropValid`.
}
function filterProps(props) {
var domProps = {};
for (var key in props) {
if (isPropValid(key))
domProps[key] = props[key];
}
return domProps;
}
function useInitialMotionProps(visualElement, props) {
var createAttrs = function () {
var attrs = visualElement.build().attrs;
var resolvedMotionValueProps = {};
for (var key in props) {
if (isMotionValue(props[key])) {
resolvedMotionValueProps[key] = props[key].get();
}
}
return tslib.__assign(tslib.__assign({}, attrs), resolvedMotionValueProps);
};
return visualElement.isStatic ? createAttrs() : useConstant(createAttrs);
}
function useSVGProps(visualElement, props) {
var svgProps = useInitialMotionProps(visualElement, props);
var style = useStyle(visualElement, props);
// TODO: Figure out why these aren't being removed
delete style.transform;
delete style.transformOrigin;
svgProps.style = style;
return svgProps;
}
function useRender(Component, props, visualElement) {
// Only filter props from components we control, ie `motion.div`. If this
// is a custom component pass along everything provided to it.
var forwardedProps = typeof Component === "string" ? filterProps(props) : props;
// Generate props to visually render this component
var useProps = isSVGComponent(Component) ? useSVGProps : useHTMLProps;
var visualProps = useProps(visualElement, props);
return React.createElement(Component, tslib.__assign(tslib.__assign(tslib.__assign({}, forwardedProps), { ref: visualElement.ref }), visualProps));
}
/**
* Load features via renderless components based on the provided MotionProps.
* TODO: Look into porting this to a component-less appraoch.
*/
function useFeatures(defaultFeatures, isStatic, visualElement, props) {
var plugins = React.useContext(MotionConfigContext);
// If this is a static component, or we're rendering on the server, we don't load
// any feature components
if (isStatic || typeof window === "undefined")
return null;
var allFeatures = tslib.__spread(defaultFeatures, plugins.features);
var numFeatures = allFeatures.length;
var features = [];
// Decide which features we should render and add them to the returned array
for (var i = 0; i < numFeatures; i++) {
var _a = allFeatures[i], shouldRender = _a.shouldRender, key = _a.key, getComponent = _a.getComponent;
if (shouldRender(props)) {
var Component = getComponent(props);
Component &&
features.push(React.createElement(Component, tslib.__assign({ key: key }, props, { visualElement: visualElement })));
}
}
return features;
}
var MotionContext = React.createContext(undefined);
function useVisualElementContext() {
return React.useContext(MotionContext);
}
/**
* @public
*/
var PresenceContext = React.createContext(null);
/**
* 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() {
var context = React.useContext(PresenceContext);
if (context === null)
return [true, null];
var isPresent = context.isPresent, onExitComplete = context.onExitComplete, register = context.register;
// 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.
// Replace with useOpaqueId when released in React
var id = useUniqueId();
React.useEffect(function () { return register(id); }, []);
var safeToRemove = function () { return onExitComplete === null || onExitComplete === void 0 ? void 0 : onExitComplete(id); };
return !isPresent && onExitComplete ? [false, safeToRemove] : [true];
}
/**
* Similar to `usePresence`, except `useIsPresent` simply returns whether or not the component is present.
* There is no `safeToRemove` function.
*
* ```jsx
* import { useIsPresent } from "framer-motion"
*
* export const Component = () => {
* const isPresent = useIsPresent()
*
* useEffect(() => {
* !isPresent && console.log("I've been removed!")
* }, [isPresent])
*
* return <div />
* }
* ```
*
* @public
*/
function useIsPresent() {
return isPresent(React.useContext(PresenceContext));
}
function isPresent(context) {
return context === null ? true : context.isPresent;
}
var counter = 0;
var incrementId = function () { return counter++; };
var useUniqueId = function () { return useConstant(incrementId); };
/**
* @internal
*/
var LayoutGroupContext = React.createContext(null);
/**
* Default handlers for batching VisualElements
*/
var defaultHandler = {
measureLayout: function (child) { return child.updateLayoutMeasurement(); },
layoutReady: function (child) { return child.notifyLayoutReady(); },
};
/**
* Create a batcher to process VisualElements
*/
function createBatcher() {
var queue = new Set();
return {
add: function (child) { return queue.add(child); },
flush: function (_a) {
var _b = _a === void 0 ? defaultHandler : _a, measureLayout = _b.measureLayout, layoutReady = _b.layoutReady, parent = _b.parent;
var order = Array.from(queue).sort(function (a, b) { return a.depth - b.depth; });
var resetAndMeasure = function () {
/**
* Write: Reset any transforms on children elements so we can read their actual layout
*/
order.forEach(function (child) { return child.resetTransform(); });
/**
* Read: Measure the actual layout
*/
order.forEach(measureLayout);
};
parent
? parent.withoutTransform(resetAndMeasure)
: resetAndMeasure();
/**
* Write: Notify the VisualElements they're ready for further write operations.
*/
order.forEach(layoutReady);
/**
* After all children have started animating, ensure any Entering components are set to Present.
* If we add deferred animations (set up all animations and then start them in two loops) this
* could be moved to the start loop. But it needs to happen after all the animations configs
* are generated in AnimateSharedLayout as this relies on presence data
*/
order.forEach(function (child) {
if (child.isPresent)
child.presence = Presence.Present;
});
queue.clear();
},
};
}
function isSharedLayout(context) {
return !!context.forceUpdate;
}
var SharedLayoutContext = React.createContext(createBatcher());
/**
* @internal
*/
var FramerTreeLayoutContext = React.createContext(createBatcher());
var isBrowser$1 = typeof window !== "undefined";
var useIsomorphicLayoutEffect = isBrowser$1 ? React.useLayoutEffect : React.useEffect;
function useSnapshotOnUnmount(visualElement) {
var syncLayout = React.useContext(SharedLayoutContext);
var framerSyncLayout = React.useContext(FramerTreeLayoutContext);
useIsomorphicLayoutEffect(function () { return function () {
if (isSharedLayout(syncLayout)) {
syncLayout.remove(visualElement);
}
if (isSharedLayout(framerSyncLayout)) {
framerSyncLayout.remove(visualElement);
}
}; }, []);
}
function useLayoutId(_a) {
var layoutId = _a.layoutId;
var layoutGroupId = React.useContext(LayoutGroupContext);
return layoutGroupId && layoutId !== undefined
? layoutGroupId + "-" + layoutId
: layoutId;
}
function useVisualElement(createVisualElement, Component, props, isStatic, ref) {
var config = React.useContext(MotionConfigContext);
var parent = useVisualElementContext();
var presenceContext = React.useContext(PresenceContext);
var layoutId = useLayoutId(props);
var visualElementRef = React.useRef(null);
if (isStatic && visualElementRef.current) {
/**
* Clear the VisualElement state in static mode after the initial render.
* This will allow the VisualElement to render every render as if its the first,
* with no history. This is basically a cheaper way of reinstantiating the VisualElement
* every render.
*/
visualElementRef.current.clearState(props);
}
else if (!visualElementRef.current) {
visualElementRef.current = createVisualElement(Component, isStatic, {
parent: parent,
ref: ref,
isStatic: isStatic,
props: tslib.__assign(tslib.__assign({}, props), { layoutId: layoutId }),
presenceId: presenceContext === null || presenceContext === void 0 ? void 0 : presenceContext.id,
blockInitialAnimation: (presenceContext === null || presenceContext === void 0 ? void 0 : presenceContext.initial) === false,
});
}
var visualElement = visualElementRef.current;
useIsomorphicLayoutEffect(function () {
visualElement.setProps(tslib.__assign(tslib.__assign(tslib.__assign({}, config), props), { layoutId: layoutId }));
visualElement.isPresent = isPresent(presenceContext);
visualElement.isPresenceRoot =
!parent || parent.presenceId !== (presenceContext === null || presenceContext === void 0 ? void 0 : presenceContext.id);
/**
* Fire a render to ensure the latest state is reflected on-screen.
*/
if (!visualElement.isStatic)
visualElement.syncRender();
});
/**
* Don't fire unnecessary effects if this is a static component.
*/
if (isStatic)
return visualElement;
React.useEffect(function () {
var _a;
/**
* In a future refactor we can replace the features-as-components and
* have this loop through them all firing "effect" listeners
*/
(_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.animateChanges();
});
/**
* If this component is a child of AnimateSharedLayout, we need to snapshot the component
* before it's unmounted. This lives here rather than in features/layout/Measure because
* as a child component its unmount effect runs after this component has been unmounted.
*/
useSnapshotOnUnmount(visualElement);
return visualElement;
}
/**
* Create a `motion` component.
*
* This function accepts a Component argument, which can be either a string (ie "div"
* for `motion.div`), or an actual React component.
*
* Alongside this is a config option which provides a way of rendering the provided
* component "offline", or outside the React render cycle.
*
* @internal
*/
function createMotionComponent(Component, _a) {
var defaultFeatures = _a.defaultFeatures, createVisualElement = _a.createVisualElement, useRender = _a.useRender;
function MotionComponent(props, externalRef) {
/**
* If a component is static, we only visually update it as a
* result of a React re-render, rather than any interactions or animations.
* If this component or any ancestor is static, we disable hardware acceleration
* and don't load any additional functionality.
*/
var isStatic = React.useContext(MotionConfigContext).isStatic;
/**
* Create a VisualElement for this component. A VisualElement provides a common
* interface to renderer-specific APIs (ie DOM/Three.js etc) as well as
* providing a way of rendering to these APIs outside of the React render loop
* for more performant animations and interactions
*/
var visualElement = useVisualElement(createVisualElement, Component, props, isStatic, externalRef);
/**
* Load features as renderless components unless the component isStatic
*/
var features = useFeatures(defaultFeatures, isStatic, visualElement, props);
var component = useRender(Component, props, visualElement);
// The mount order and hierarchy is specific to ensure our element ref is hydrated by the time
// all plugins and features has to execute.
return (React.createElement(React.Fragment, null,
React.createElement(MotionContext.Provider, { value: visualElement }, component),
features));
}
return React.forwardRef(MotionComponent);
}
/**
* Apply constraints to a point. These constraints are both physical along an
* axis, and an elastic factor that determines how much to constrain the point
* by if it does lie outside the defined parameters.
*/
function applyConstraints(point, _a, elastic) {
var min = _a.min, max = _a.max;
if (min !== undefined && point < min) {
// If we have a min point defined, and this is outside of that, constrain
point = elastic ? popmotion.mix(min, point, elastic) : Math.max(point, min);
}
else if (max !== undefined && point > max) {
// If we have a max point defined, and this is outside of that, constrain
point = elastic ? popmotion.mix(max, point, elastic) : Math.min(point, max);
}
return point;
}
/**
* Calculates a min projection point based on a pointer, pointer progress
* within the drag target, and constraints.
*
* For instance if an element was 100px width, we were dragging from 0.25
* along this axis, the pointer is at 200px, and there were no constraints,
* we would calculate a min projection point of 175px.
*/
function calcConstrainedMinPoint(point, length, progress, constraints, elastic) {
// Calculate a min point for this axis and apply it to the current pointer
var min = point - length * progress;
return constraints ? applyConstraints(min, constraints, elastic) : min;
}
/**
* Calculate constraints in terms of the viewport when defined relatively to the
* measured axis. This is measured from the nearest edge, so a max constraint of 200
* on an axis with a max value of 300 would return a constraint of 500 - axis length
*/
function calcRelativeAxisConstraints(axis, min, max) {
return {
min: min !== undefined ? axis.min + min : undefined,
max: max !== undefined
? axis.max + max - (axis.max - axis.min)
: undefined,
};
}
/**
* Calculate constraints in terms of the viewport when
* defined relatively to the measured bounding box.
*/
function calcRelativeConstraints(layoutBox, _a) {
var top = _a.top, left = _a.left, bottom = _a.bottom, right = _a.right;
return {
x: calcRelativeAxisConstraints(layoutBox.x, left, right),
y: calcRelativeAxisConstraints(layoutBox.y, top, bottom),
};
}
/**
* Calculate viewport constraints when defined as another viewport-relative axis
*/
function calcViewportAxisConstraints(layoutAxis, constraintsAxis) {
var _a;
var min = constraintsAxis.min - layoutAxis.min;
var max = constraintsAxis.max - layoutAxis.max;
// If the constraints axis is actually smaller than the layout axis then we can
// flip the constraints
if (constraintsAxis.max - constraintsAxis.min <
layoutAxis.max - layoutAxis.min) {
_a = tslib.__read([max, min], 2), min = _a[0], max = _a[1];
}
return {
min: layoutAxis.min + min,
max: layoutAxis.min + max,
};
}
/**
* Calculate viewport constraints when defined as another viewport-relative box
*/
function calcViewportConstraints(layoutBox, constraintsBox) {
return {
x: calcViewportAxisConstraints(layoutBox.x, constraintsBox.x),
y: calcViewportAxisConstraints(layoutBox.y, constraintsBox.y),
};
}
/**
* Calculate the an axis position based on two axes and a progress value.
*/
function calcPositionFromProgress(axis, constraints, progress) {
var axisLength = axis.max - axis.min;
var min = popmotion.mix(constraints.min, constraints.max - axisLength, progress);
return { min: min, max: min + axisLength };
}
/**
* Rebase the calculated viewport constraints relative to the layout.min point.
*/
function rebaseAxisConstraints(layout, constraints) {
var relativeConstraints = {};
if (constraints.min !== undefined) {
relativeConstraints.min = constraints.min - layout.min;
}
if (constraints.max !== undefined) {
relativeConstraints.max = constraints.max - layout.min;
}
return relativeConstraints;
}
var elementDragControls = new WeakMap();
/**
*
*/
var lastPointerEvent;
var VisualElementDragControls = /** @class */ (function () {
function VisualElementDragControls(_a) {
var visualElement = _a.visualElement;
/**
* Track whether we're currently dragging.
*
* @internal
*/
this.isDragging = false;
/**
* The current direction of drag, or `null` if both.
*
* @internal
*/
this.currentDirection = null;
/**
* The permitted boundaries of travel, in pixels.
*
* @internal
*/
this.constraints = false;
/**
* A reference to the host component's latest props.
*
* @internal
*/
this.props = {};
/**
* @internal
*/
this.hasMutatedConstraints = false;
/**
* Track the initial position of the cursor relative to the dragging element
* when dragging starts as a value of 0-1 on each axis. We then use this to calculate
* an ideal bounding box for the VisualElement renderer to project into every frame.
*
* @internal
*/
this.cursorProgress = {
x: 0.5,
y: 0.5,
};
// When updating _dragX, or _dragY instead of the VisualElement,
// persist their values between drag gestures.
this.originPoint = {};
// This is a reference to the global drag gesture lock, ensuring only one component
// can "capture" the drag of one or both axes.
// TODO: Look into moving this into pansession?
this.openGlobalLock = null;
/**
* @internal
*/
this.panSession = null;
this.visualElement = visualElement;
this.visualElement.enableLayoutProjection();
elementDragControls.set(visualElement, this);
}
/**
* Instantiate a PanSession for the drag gesture
*
* @public
*/
VisualElementDragControls.prototype.start = function (originEvent, _a) {
var _this = this;
var _b = _a === void 0 ? {} : _a, _c = _b.snapToCursor, snapToCursor = _c === void 0 ? false : _c, cursorProgress = _b.cursorProgress;
/**
* If this drag session has been manually triggered by the user, it might be from an event
* outside the draggable element. If snapToCursor is set to true, we need to measure the position
* of the element and snap it to the cursor.
*/
snapToCursor && this.snapToCursor(originEvent);
var onSessionStart = function () {
// Stop any animations on both axis values immediately. This allows the user to throw and catch
// the component.
_this.stopMotion();
};
var onStart = function (event, info) {
var _a, _b, _c;
// Attempt to grab the global drag gesture lock - maybe make this part of PanSession
var _d = _this.props, drag = _d.drag, dragPropagation = _d.dragPropagation;
if (drag && !dragPropagation) {
if (_this.openGlobalLock)
_this.openGlobalLock();
_this.openGlobalLock = getGlobalLock(drag);
// If we don 't have the lock, don't start dragging
if (!_this.openGlobalLock)
return;
}
/**
* Record the progress of the mouse within the draggable element on each axis.
* onPan, we're going to use this to calculate a new bounding box for the element to
* project into. This will ensure that even if the DOM element moves via a relayout, it'll
* stick to the correct place under the pointer.
*/
_this.prepareBoundingBox();
_this.visualElement.lockProjectionTarget();
/**
* Resolve the drag constraints. These are either set as top/right/bottom/left constraints
* relative to the element's layout, or a ref to another element. Both need converting to
* viewport coordinates.
*/
_this.resolveDragConstraints();
/**
* When dragging starts, we want to find where the cursor is relative to the bounding box
* of the element. Every frame, we calculate a new bounding box using this relative position
* and let the visualElement renderer figure out how to reproject the element into this bounding
* box.
*
* By doing it this way, rather than applying an x/y transform directly to the element,
* we can ensure the component always visually sticks to the cursor as we'd expect, even
* if the DOM element itself changes layout as a result of React updates the user might
* make based on the drag position.
*/
var point = getViewportPointFromEvent(event).point;
eachAxis(function (axis) {
var _a = _this.visualElement.projection.target[axis], min = _a.min, max = _a.max;
_this.cursorProgress[axis] = cursorProgress
? cursorProgress[axis]
: popmotion.progress(min, max, point[axis]);
/**
* If we have external drag MotionValues, record their origin point. On pointermove
* we'll apply the pan gesture offset directly to this value.
*/
var axisValue = _this.getAxisMotionValue(axis);
if (axisValue) {
_this.originPoint[axis] = axisValue.get();
}
});
// Set current drag status
_this.isDragging = true;
_this.currentDirection = null;
// Fire onDragStart event
(_b = (_a = _this.props).onDragStart) === null || _b === void 0 ? void 0 : _b.call(_a, event, info);
(_c = _this.visualElement.animationState) === null || _c === void 0 ? void 0 : _c.setActive(AnimationType.Drag, true);
};
var onMove = function (event, info) {
var _a, _b, _c, _d;
var _e = _this.props, dragPropagation = _e.dragPropagation, dragDirectionLock = _e.dragDirectionLock;
// If we didn't successfully receive the gesture lock, early return.
if (!dragPropagation && !_this.openGlobalLock)
return;
var offset = info.offset;
// Attempt to detect drag direction if directionLock is true
if (dragDirectionLock && _this.currentDirection === null) {
_this.currentDirection = getCurrentDirection(offset);
// If we've successfully set a direction, notify listener
if (_this.currentDirection !== null) {
(_b = (_a = _this.props).onDirectionLock) === null || _b === void 0 ? void 0 : _b.call(_a, _this.currentDirection);
}
return;
}
// Update each point with the latest position
_this.updateAxis("x", event, offset);
_this.updateAxis("y", event, offset);
// Fire onDrag event
(_d = (_c = _this.props).onDrag) === null || _d === void 0 ? void 0 : _d.call(_c, event, info);
// Update the last pointer event
lastPointerEvent = event;
};
var onEnd = function (event, info) { return _this.stop(event, info); };
var transformPagePoint = this.props.transformPagePoint;
this.panSession = new PanSession(originEvent, {
onSessionStart: onSessionStart,
onStart: onStart,
onMove: onMove,
onEnd: onEnd,
}, { transformPagePoint: transformPagePoint });
};
/**
* Ensure the component's layout and target bounding boxes are up-to-date.
*/
VisualElementDragControls.prototype.prepareBoundingBox = function () {
var visualElement = this.visualElement;
visualElement.withoutTransform(function () {
visualElement.updateLayoutMeasurement();
});
visualElement.rebaseProjectionTarget(true, visualElement.measureViewportBox(false));
};
VisualElementDragControls.prototype.resolveDragConstraints = function () {
var _this = this;
var dragConstraints = this.props.dragConstraints;
if (dragConstraints) {
this.constraints = isRefObject(dragConstraints)
? this.resolveRefConstraints(this.visualElement.getLayoutState().layout, dragConstraints)
: calcRelativeConstraints(this.visualElement.getLayoutState().layout, dragConstraints);
}
else {
this.constraints = false;
}
/**
* If we're outputting to external MotionValues, we want to rebase the measured constraints
* from viewport-relative to component-relative.
*/
if (this.constraints && !this.hasMutatedConstraints) {
eachAxis(function (axis) {
if (_this.getAxisMotionValue(axis)) {
_this.constraints[axis] = rebaseAxisConstraints(_this.visualElement.getLayoutState().layout[axis], _this.constraints[axis]);
}
});
}
};
VisualElementDragControls.prototype.resolveRefConstraints = function (layoutBox, constraints) {
var _a = this.props, onMeasureDragConstraints = _a.onMeasureDragConstraints, transformPagePoint = _a.transformPagePoint;
var constraintsElement = constraints.current;
heyListen.invariant(constraintsElement !== null, "If `dragConstraints` is set as a React ref, that ref must be passed to another component's `ref` prop.");
this.constraintsBox = getBoundingBox(constraintsElement, transformPagePoint);
var measuredConstraints = calcViewportConstraints(layoutBox, this.constraintsBox);
/**
* If there's an onMeasureDragConstraints listener we call it and
* if different constraints are returned, set constraints to that
*/
if (onMeasureDragConstraints) {
var userConstraints = onMeasureDragConstraints(convertAxisBoxToBoundingBox(measuredConstraints));
this.hasMutatedConstraints = !!userConstraints;
if (userConstraints) {
measuredConstraints = convertBoundingBoxToAxisBox(userConstraints);
}
}
return measuredConstraints;
};
VisualElementDragControls.prototype.cancelDrag = function () {
var _a;
this.isDragging = false;
this.panSession && this.panSession.end();
this.panSession = null;
if (!this.props.dragPropagation && this.openGlobalLock) {
this.openGlobalLock();
this.openGlobalLock = null;
}
(_a = this.visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive(AnimationType.Drag, false);
};
VisualElementDragControls.prototype.stop = function (event, info) {
var _a;
this.visualElement.unlockProjectionTarget();
(_a = this.panSession) === null || _a === void 0 ? void 0 : _a.end();
this.panSession = null;
var isDragging = this.isDragging;
this.cancelDrag();
if (!isDragging)
return;
var _b = this.props, dragMomentum = _b.dragMomentum, dragElastic = _b.dragElastic, onDragEnd = _b.onDragEnd;
if (dragMomentum || dragElastic) {
var velocity = info.velocity;
this.animateDragEnd(velocity);
}
onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd(event, info);
};
VisualElementDragControls.prototype.snapToCursor = function (event) {
var _this = this;
this.prepareBoundingBox();
eachAxis(function (axis) {
var drag = _this.props.drag;
// If we're not dragging this axis, do an early return.
if (!shouldDrag(axis, drag, _this.currentDirection))
return;
var axisValue = _this.getAxisMotionValue(axis);
if (axisValue) {
var point = getViewportPointFromEvent(event).point;
var box = _this.visualElement.getLayoutState().layout;
var length_1 = box[axis].max - box[axis].min;
var center = box[axis].min + length_1 / 2;
var offset = point[axis] - center;
_this.originPoint[axis] = point[axis];
axisValue.set(offset);
}
else {
_this.cursorProgress[axis] = 0.5;
_this.updateVisualElementAxis(axis, event);
}
});
};
/**
* Update the specified axis with the latest pointer information.
*/
VisualElementDragControls.prototype.updateAxis = function (axis, event, offset) {
var drag = this.props.drag;
// If we're not dragging this axis, do an early return.
if (!shouldDrag(axis, drag, this.currentDirection))
return;
return this.getAxisMotionValue(axis)
? this.updateAxisMotionValue(axis, offset)
: this.updateVisualElementAxis(axis, event);
};
VisualElementDragControls.prototype.updateAxisMotionValue = function (axis, offset) {
var axisValue = this.getAxisMotionValue(axis);
if (!offset || !axisValue)
return;
var dragElastic = this.props.dragElastic;
var nextValue = this.originPoint[axis] + offset[axis];
var update = this.constraints
? applyConstraints(nextValue, this.constraints[axis], dragElastic)
: nextValue;
axisValue.set(update);
};
VisualElementDragControls.prototype.updateVisualElementAxis = function (axis, event) {
var _a;
var dragElastic = this.props.dragElastic;
// Get the actual layout bounding box of the element
var axisLayout = this.visualElement.getLayoutState().layout[axis];
// Calculate its current length. In the future we might want to lerp this to animate
// between lengths if the layout changes as we change the DOM
var axisLength = axisLayout.max - axisLayout.min;
// Get the initial progress that the pointer sat on this axis on gesture start.
var axisProgress = this.cursorProgress[axis];
var point = getViewportPointFromEvent(event).point;
// Calculate a new min point based on the latest pointer position, constraints and elastic
var min = calcConstrainedMinPoint(point[axis], axisLength, axisProgress, (_a = this.constraints) === null || _a === void 0 ? void 0 : _a[axis], dragElastic);
// Update the axis viewport target with this new min and the length
this.visualElement.setProjectionTargetAxis(axis, min, min + axisLength);
};
VisualElementDragControls.prototype.setProps = function (_a) {
var _b = _a.drag, drag = _b === void 0 ? false : _b, _c = _a.dragDirectionLock, dragDirectionLock = _c === void 0 ? false : _c, _d = _a.dragPropagation, dragPropagation = _d === void 0 ? false : _d, _e = _a.dragConstraints, dragConstraints = _e === void 0 ? false : _e, _f = _a.dragElastic, dragElastic = _f === void 0 ? 0.35 : _f, _g = _a.dragMomentum, dragMomentum = _g === void 0 ? true : _g, remainingProps = tslib.__rest(_a, ["drag", "dragDirectionLock", "dragPropagation", "dragConstraints", "dragElastic", "dragMomentum"]);
this.props = tslib.__assign({ drag: drag,
dragDirectionLock: dragDirectionLock,
dragPropagation: dragPropagation,
dragConstraints: dragConstraints,
dragElastic: dragElastic,
dragMomentum: dragMomentum }, remainingProps);
};
/**
* Drag works differently depending on which props are provided.
*
* - If _dragX and _dragY are provided, we output the gesture delta directly to those motion values.
* - If the component will perform layout animations, we output the gesture to the component's
* visual bounding box
* - Otherwise, we apply the delta to the x/y motion values.
*/
VisualElementDragControls.prototype.getAxisMotionValue = function (axis) {
var _a = this.props, layout = _a.layout, layoutId = _a.layoutId;
var dragKey = "_drag" + axis.toUpperCase();
if (this.props[dragKey]) {
return this.props[dragKey];
}
else if (!layout && layoutId === undefined) {
return this.visualElement.getValue(axis, 0);
}
};
VisualElementDragControls.prototype.animateDragEnd = function (velocity) {
var _this = this;
var _a = this.props, drag = _a.drag, dragMomentum = _a.dragMomentum, dragElastic = _a.dragElastic, dragTransition = _a.dragTransition;
var momentumAnimations = eachAxis(function (axis) {
if (!shouldDrag(axis, drag, _this.currentDirection)) {
return;
}
var transition = _this.constraints ? _this.constraints[axis] : {};
/**
* Overdamp the boundary spring if `dragElastic` is disabled. There's still a frame
* of spring animations so we should look into adding a disable spring option to `inertia`.
* We could do something here where we affect the `bounceStiffness` and `bounceDamping`
* using the value of `dragElastic`.
*/
var bounceStiffness = dragElastic ? 200 : 1000000;
var bounceDamping = dragElastic ? 40 : 10000000;
var inertia = tslib.__assign(tslib.__assign({ type: "inertia", velocity: dragMomentum ? velocity[axis] : 0, bounceStiffness: bounceStiffness,
bounceDamping: bounceDamping, timeConstant: 750, restDelta: 1, restSpeed: 10 }, dragTransition), transition);
// If we're not animating on an externally-provided `MotionValue` we can use the
// component's animation controls which will handle interactions with whileHover (etc),
// otherwise we just have to animate the `MotionValue` itself.
return _this.getAxisMotionValue(axis)
? _this.startAxisValueAnimation(axis, inertia)
: _this.visualElement.startLayoutAnimation(axis, inertia);
});
// Run all animations and then resolve the new drag constraints.
return Promise.all(momentumAnimations).then(function () {
var _a, _b;
(_b = (_a = _this.props).onDragTransitionEnd) === null || _b === void 0 ? void 0 : _b.call(_a);
});
};
VisualElementDragControls.prototype.stopMotion = function () {
var _this = this;
eachAxis(function (axis) {
var axisValue = _this.getAxisMotionValue(axis);
axisValue
? axisValue.stop()
: _this.visualElement.stopLayoutAnimation();
});
};
VisualElementDragControls.prototype.startAxisValueAnimation = function (axis, transition) {
var axisValue = this.getAxisMotionValue(axis);
if (!axisValue)
return;
var currentValue = axisValue.get();
axisValue.set(currentValue);
axisValue.set(currentValue); // Set twice to hard-reset velocity
return startAnimation(axis, axisValue, 0, transition);
};
VisualElementDragControls.prototype.scalePoint = function () {
var _this = this;
var _a = this.props, drag = _a.drag, dragConstraints = _a.dragConstraints;
if (!isRefObject(dragConstraints) || !this.constraintsBox)
return;
// Stop any current animations as there can be some visual glitching if we resize mid animation
this.stopMotion();
// Record the relative progress of the targetBox relative to the constraintsBox
var boxProgress = { x: 0, y: 0 };
eachAxis(function (axis) {
boxProgress[axis] = calcOrigin(_this.visualElement.projection.target[axis], _this.constraintsBox[axis]);
});
/**
* For each axis, calculate the current progress of the layout axis within the constraints.
* Then, using the latest layout and constraints measurements, reposition the new layout axis
* proportionally within the constraints.
*/
this.prepareBoundingBox();
this.resolveDragConstraints();
eachAxis(function (axis) {
if (!shouldDrag(axis, drag, null))
return;
// Calculate the position of the targetBox relative to the constraintsBox using the
// previously calculated progress
var _a = calcPositionFromProgress(_this.visualElement.projection.target[axis], _this.constraintsBox[axis], boxProgress[axis]), min = _a.min, max = _a.max;
_this.visualElement.setProjectionTargetAxis(axis, min, max);
});
};
VisualElementDragControls.prototype.mount = function (visualElement) {
var _this = this;
var element = visualElement.getInstance();
/**
* Attach a pointerdown event listener on this DOM element to initiate drag tracking.
*/
var stopPointerListener = addPointerEvent(element, "pointerdown", function (event) {
var _a = _this.props, drag = _a.drag, _b = _a.dragListener, dragListener = _b === void 0 ? true : _b;
drag && dragListener && _this.start(event);
});
/**
* Attach a window resize listener to scale the draggable target within its defined
* constraints as the window resizes.
*/
var stopResizeListener = addDomEvent(window, "resize", function () {
_this.scalePoint();
});
/**
* Ensure drag constraints are resolved correctly relative to the dragging element
* whenever its layout changes.
*/
var stopLayoutUpdateListener = visualElement.onLayoutUpdate(function () {
if (_this.isDragging)
_this.resolveDragConstraints();
});
/**
* If the previous component with this same layoutId was dragging at the time
* it was unmounted, we want to continue the same gesture on this component.
*/
var prevDragCursor = visualElement.prevDragCursor;
if (prevDragCursor) {
this.start(lastPointerEvent, { cursorProgress: prevDragCursor });
}
/**
* Return a function that will teardown the drag gesture
*/
return function () {
stopPointerListener === null || stopPointerListener === void 0 ? void 0 : stopPointerListener();
stopResizeListener === null || stopResizeListener === void 0 ? void 0 : stopResizeListener();
stopLayoutUpdateListener === null || stopLayoutUpdateListener === void 0 ? void 0 : stopLayoutUpdateListener();
_this.cancelDrag();
};
};
return VisualElementDragControls;
}());
function shouldDrag(direction, drag, currentDirection) {
return ((drag === true || drag === direction) &&
(currentDirection === null || currentDirection === direction));
}
/**
* Based on an x/y offset determine the current drag direction. If both axis' offsets are lower
* than the provided threshold, return `null`.
*
* @param offset - The x/y offset from origin.
* @param lockThreshold - (Optional) - the minimum absolute offset before we can determine a drag direction.
*/
function getCurrentDirection(offset, lockThreshold) {
if (lockThreshold === void 0) { lockThreshold = 10; }
var direction = null;
if (Math.abs(offset.y) > lockThreshold) {
direction = "y";
}
else if (Math.abs(offset.x) > lockThreshold) {
direction = "x";
}
return direction;
}
/**
* A hook that allows an element to be dragged.
*
* @internal
*/
function useDrag(props, visualElement) {
var groupDragControls = props.dragControls;
var transformPagePoint = React.useContext(MotionConfigContext).transformPagePoint;
var dragControls = useConstant(function () {
return new VisualElementDragControls({
visualElement: visualElement,
});
});
dragControls.setProps(tslib.__assign(tslib.__assign({}, props), { transformPagePoint: transformPagePoint }));
// If we've been provided a DragControls for manual control over the drag gesture,
// subscribe this component to it on mount.
React.useEffect(function () { return groupDragControls && groupDragControls.subscribe(dragControls); }, [dragControls]);
// Mount the drag controls with the visualElement
React.useEffect(function () { return dragControls.mount(visualElement); }, []);
}
var Component = makeRenderlessComponent(function (_a) {
var visualElement = _a.visualElement, props = tslib.__rest(_a, ["visualElement"]);
return useDrag(props, visualElement);
});
/**
* @public
*/
var Drag = {
key: "drag",
shouldRender: function (props) { return !!props.drag || !!props.dragControls; },
getComponent: function () { return Component; },
};
function checkShouldInheritVariant(_a) {
var animate = _a.animate, variants = _a.variants, inherit = _a.inherit;
return inherit !== null && inherit !== void 0 ? inherit : (!!variants && !animate);
}
/**
* TODO: This component is quite small and no longer directly imports animation code.
* It could be a candidate for folding back into the main `motion` component.
*/
var ExitComponent = makeRenderlessComponent(function (props) {
var custom = props.custom, visualElement = props.visualElement;
var _a = tslib.__read(usePresence(), 2), isPresent = _a[0], onExitComplete = _a[1];
var presenceContext = React.useContext(PresenceContext);
React.useEffect(function () {
var _a, _b;
var animation = (_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive(AnimationType.Exit, !isPresent, { custom: (_b = presenceContext === null || presenceContext === void 0 ? void 0 : presenceContext.custom) !== null && _b !== void 0 ? _b : custom });
!isPresent && (animation === null || animation === void 0 ? void 0 : animation.then(onExitComplete));
}, [isPresent]);
});
/**
* @public
*/
var Exit = {
key: "exit",
shouldRender: function (props) { return !!props.exit && !checkShouldInheritVariant(props); },
getComponent: function () { return ExitComponent; },
};
var AnimationState = makeRenderlessComponent(function (props) {
var visualElement = props.visualElement, animate = props.animate;
/**
* We dynamically generate the AnimationState manager as it contains a reference
* to the underlying animation library. We only want to load that if we load this,
* so people can optionally code split it out using the `m` component.
*/
visualElement.animationState || (visualElement.animationState = createAnimationState(visualElement));
/**
* Subscribe any provided AnimationControls to the component's VisualElement
*/
if (isAnimationControls(animate)) {
React.useEffect(function () { return animate.subscribe(visualElement); }, [animate]);
}
});
/**
* @public
*/
var Animation = {
key: "animation",
shouldRender: function () { return true; },
getComponent: function (_a) {
var animate = _a.animate, whileHover = _a.whileHover, whileFocus = _a.whileFocus, whileTap = _a.whileTap, whileDrag = _a.whileDrag, exit = _a.exit, variants = _a.variants;
return animate ||
whileHover ||
whileFocus ||
whileTap ||
whileDrag ||
exit ||
variants
? AnimationState
: undefined;
},
};
function tweenAxis(target, prev, next, p) {
target.min = popmotion.mix(prev.min, next.min, p);
target.max = popmotion.mix(prev.max, next.max, p);
}
var progressTarget = 1000;
var Animate = /** @class */ (function (_super) {
tslib.__extends(Animate, _super);
function Animate() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.frameTarget = {
x: { min: 0, max: 0 },
y: { min: 0, max: 0 },
};
_this.stopAxisAnimation = {
x: undefined,
y: undefined,
};
_this.isAnimatingTree = false;
_this.animate = function (target, origin, _a) {
if (_a === void 0) { _a = {}; }
var originBox = _a.originBox, targetBox = _a.targetBox, visibilityAction = _a.visibilityAction, shouldStackAnimate = _a.shouldStackAnimate, onComplete = _a.onComplete, config = tslib.__rest(_a, ["originBox", "targetBox", "visibilityAction", "shouldStackAnimate", "onComplete"]);
var _b = _this.props, visualElement = _b.visualElement, layout = _b.layout;
/**
* Early return if we've been instructed not to animate this render.
*/
if (shouldStackAnimate === false) {
_this.isAnimatingTree = false;
return _this.safeToRemove();
}
/**
* Prioritise tree animations
*/
if (_this.isAnimatingTree && shouldStackAnimate !== true) {
return;
}
else if (shouldStackAnimate) {
_this.isAnimatingTree = true;
}
/**
* Allow the measured origin (prev bounding box) and target (actual layout) to be
* overridden by the provided config.
*/
origin = originBox || origin;
target = targetBox || target;
var boxHasMoved = hasMoved(origin, target);
var animations = eachAxis(function (axis) {
/**
* If layout is set to "position", we can resize the origin box based on the target
* box and only animate its position.
*/
if (layout === "position") {
var targetLength = target[axis].max - target[axis].min;
origin[axis].max = origin[axis].min + targetLength;
}
if (visualElement.projection.isTargetLocked) {
return;
}
else if (visibilityAction !== undefined) {
visualElement.setVisibility(visibilityAction === exports.VisibilityAction.Show);
}
else if (boxHasMoved) {
// If the box has moved, animate between it's current visual state and its
// final state
return _this.animateAxis(axis, target[axis], origin[axis], config);
}
else {
// If the box has remained in the same place, immediately set the axis target
// to the final desired state
return visualElement.setProjectionTargetAxis(axis, target[axis].min, target[axis].max);
}
});
// Force a render to ensure there's no flash of uncorrected bounding box.
visualElement.syncRender();
/**
* If this visualElement isn't present (ie it's been removed from the tree by the user but
* kept in by the tree by AnimatePresence) then call safeToRemove when all axis animations
* have successfully finished.
*/
return Promise.all(animations).then(function () {
_this.isAnimatingTree = false;
onComplete && onComplete();
visualElement.notifyLayoutAnimationComplete();
});
};
return _this;
}
Animate.prototype.componentDidMount = function () {
var _this = this;
var visualElement = this.props.visualElement;
visualElement.animateMotionValue = startAnimation;
visualElement.enableLayoutProjection();
this.unsubLayoutReady = visualElement.onLayoutUpdate(this.animate);
visualElement.layoutSafeToRemove = function () { return _this.safeToRemove(); };
};
Animate.prototype.componentWillUnmount = function () {
var _this = this;
this.unsubLayoutReady();
eachAxis(function (axis) { var _a, _b; return (_b = (_a = _this.stopAxisAnimation)[axis]) === null || _b === void 0 ? void 0 : _b.call(_a); });
};
/**
* TODO: This manually performs animations on the visualElement's layout progress
* values. It'd be preferable to amend the startLayoutAxisAnimation
* API to accept more custom animations like this.
*/
Animate.prototype.animateAxis = function (axis, target, origin, _a) {
var _b, _c;
var transition = (_a === void 0 ? {} : _a).transition;
(_c = (_b = this.stopAxisAnimation)[axis]) === null || _c === void 0 ? void 0 : _c.call(_b);
var visualElement = this.props.visualElement;
var frameTarget = this.frameTarget[axis];
var layoutProgress = visualElement.getProjectionAnimationProgress()[axis];
/**
* Set layout progress back to 0. We set it twice to hard-reset any velocity that might
* be re-incoporated into a subsequent spring animation.
*/
layoutProgress.clearListeners();
layoutProgress.set(0);
layoutProgress.set(0);
/**
* Create an animation function to run once per frame. This will tween the visual bounding box from
* origin to target using the latest progress value.
*/
var frame = function () {
// Convert the latest layoutProgress, which is a value from 0-1000, into a 0-1 progress
var p = layoutProgress.get() / progressTarget;
// Tween the axis and update the visualElement with the latest values
tweenAxis(frameTarget, origin, target, p);
visualElement.setProjectionTargetAxis(axis, frameTarget.min, frameTarget.max);
};
// Synchronously run a frame to ensure there's no flash of the uncorrected bounding box.
frame();
// Ensure that the layout delta is updated for this frame.
visualElement.updateLayoutProjection();
// Create a function to stop animation on this specific axis
var unsubscribeProgress = layoutProgress.onChange(frame);
// Start the animation on this axis
var animation = startAnimation(axis === "x" ? "layoutX" : "layoutY", layoutProgress, progressTarget, transition || this.props.transition || defaultTransition).then(unsubscribeProgress);
this.stopAxisAnimation[axis] = function () {
layoutProgress.stop();
unsubscribeProgress();
};
return animation;
};
Animate.prototype.safeToRemove = function () {
var _a, _b;
(_b = (_a = this.props).safeToRemove) === null || _b === void 0 ? void 0 : _b.call(_a);
};
Animate.prototype.render = function () {
return null;
};
return Animate;
}(React.Component));
function AnimateLayoutContextProvider(props) {
var _a = tslib.__read(usePresence(), 2), safeToRemove = _a[1];
return React.createElement(Animate, tslib.__assign({}, props, { safeToRemove: safeToRemove }));
}
function hasMoved(a, b) {
return (!isZeroBox(a) &&
!isZeroBox(b) &&
(!axisIsEqual(a.x, b.x) || !axisIsEqual(a.y, b.y)));
}
var zeroAxis = { min: 0, max: 0 };
function isZeroBox(a) {
return axisIsEqual(a.x, zeroAxis) && axisIsEqual(a.y, zeroAxis);
}
function axisIsEqual(a, b) {
return a.min === b.min && a.max === b.max;
}
var defaultTransition = {
duration: 0.45,
ease: [0.4, 0, 0.1, 1],
};
/**
* @public
*/
var AnimateLayout = {
key: "animate-layout",
shouldRender: function (props) {
return !!props.layout || props.layoutId !== undefined;
},
getComponent: function () { return AnimateLayoutContextProvider; },
};
/**
* This component is responsible for scheduling the measuring of the motion component
*/
var Measure = /** @class */ (function (_super) {
tslib.__extends(Measure, _super);
function Measure() {
return _super !== null && _super.apply(this, arguments) || this;
}
/**
* If this is a child of a SyncContext, register the VisualElement with it on mount.
*/
Measure.prototype.componentDidMount = function () {
var _a = this.props, syncLayout = _a.syncLayout, framerSyncLayout = _a.framerSyncLayout, visualElement = _a.visualElement;
isSharedLayout(syncLayout) && syncLayout.register(visualElement);
isSharedLayout(framerSyncLayout) &&
framerSyncLayout.register(visualElement);
};
/**
* If this is a child of a SyncContext, notify it that it needs to re-render. It will then
* handle the snapshotting.
*
* If it is stand-alone component, add it to the batcher.
*/
Measure.prototype.getSnapshotBeforeUpdate = function () {
var _a = this.props, syncLayout = _a.syncLayout, visualElement = _a.visualElement;
if (isSharedLayout(syncLayout)) {
syncLayout.syncUpdate();
}
else {
visualElement.snapshotViewportBox();
syncLayout.add(visualElement);
}
return null;
};
Measure.prototype.componentDidUpdate = function () {
var _a = this.props, syncLayout = _a.syncLayout, visualElement = _a.visualElement;
if (!isSharedLayout(syncLayout))
syncLayout.flush();
/**
* If this axis isn't animating as a result of this render we want to reset the targetBox
* to the measured box
*/
visualElement.rebaseProjectionTarget();
};
Measure.prototype.render = function () {
return null;
};
return Measure;
}(React__default['default'].Component));
function MeasureContextProvider(props) {
var syncLayout = React.useContext(SharedLayoutContext);
var framerSyncLayout = React.useContext(FramerTreeLayoutContext);
return (React__default['default'].createElement(Measure, tslib.__assign({}, props, { syncLayout: syncLayout, framerSyncLayout: framerSyncLayout })));
}
var MeasureLayout = {
key: "measure-layout",
shouldRender: function (props) {
return !!props.drag || !!props.layout || props.layoutId !== undefined;
},
getComponent: function () { return MeasureContextProvider; },
};
var allMotionFeatures = [
MeasureLayout,
Animation,
Drag,
Gestures,
Exit,
AnimateLayout,
];
var domBaseConfig = {
createVisualElement: createDomVisualElement,
useRender: useRender,
};
/**
* Convert any React component into a `motion` component. The provided component
* **must** use `React.forwardRef` to the underlying DOM component you want to animate.
*
* ```jsx
* const Component = React.forwardRef((props, ref) => {
* return <div ref={ref} />
* })
*
* const MotionComponent = motion.custom(Component)
* ```
*
* @public
*/
function createMotionProxy(defaultFeatures) {
var config = tslib.__assign(tslib.__assign({}, domBaseConfig), { defaultFeatures: defaultFeatures });
function custom(Component) {
return createMotionComponent(Component, config);
}
var componentCache = new Map();
function get(target, key) {
if (key === "custom")
return target.custom;
if (!componentCache.has(key)) {
componentCache.set(key, createMotionComponent(key, config));
}
return componentCache.get(key);
}
return new Proxy({ custom: custom }, { get: get });
}
/**
* HTML & SVG components, optimised for use with gestures and animation. These can be used as
* drop-in replacements for any HTML & SVG component, all CSS & SVG properties are supported.
*
* @public
*/
var motion = /*@__PURE__*/ createMotionProxy(allMotionFeatures);
/**
* Create a DOM `motion` component with the provided string. This is primarily intended
* as a full alternative to `motion` for consumers who have to support environments that don't
* support `Proxy`.
*
* ```javascript
* import { createDomMotionComponent } from "framer-motion"
*
* const motion = {
* div: createDomMotionComponent('div')
* }
* ```
*
* @public
*/
function createDomMotionComponent(key) {
var config = tslib.__assign(tslib.__assign({}, domBaseConfig), { defaultFeatures: allMotionFeatures });
return createMotionComponent(key, config);
}
/**
* @public
*/
var m = /*@__PURE__*/ createMotionProxy([MeasureLayout]);
function useForceUpdate() {
var unloadingRef = React.useRef(false);
var _a = tslib.__read(React.useState(0), 2), forcedRenderCount = _a[0], setForcedRenderCount = _a[1];
useUnmountEffect(function () { return (unloadingRef.current = true); });
return React.useCallback(function () {
!unloadingRef.current && setForcedRenderCount(forcedRenderCount + 1);
}, [forcedRenderCount]);
}
var presenceId = 0;
function getPresenceId() {
var id = presenceId;
presenceId++;
return id;
}
var PresenceChild = function (_a) {
var children = _a.children, initial = _a.initial, isPresent = _a.isPresent, onExitComplete = _a.onExitComplete, custom = _a.custom, presenceAffectsLayout = _a.presenceAffectsLayout;
var presenceChildren = useConstant(newChildrenMap);
var id = useConstant(getPresenceId);
var context = React.useMemo(function () { return ({
id: id,
initial: initial,
isPresent: isPresent,
custom: custom,
onExitComplete: function (childId) {
presenceChildren.set(childId, true);
var allComplete = true;
presenceChildren.forEach(function (isComplete) {
if (!isComplete)
allComplete = false;
});
allComplete && (onExitComplete === null || onExitComplete === void 0 ? void 0 : onExitComplete());
},
register: function (childId) {
presenceChildren.set(childId, false);
return function () { return presenceChildren.delete(childId); };
},
}); },
/**
* If the presence of a child affects the layout of the components around it,
* we want to make a new context value to ensure they get re-rendered
* so they can detect that layout change.
*/
presenceAffectsLayout ? undefined : [isPresent]);
React.useMemo(function () {
presenceChildren.forEach(function (_, key) { return presenceChildren.set(key, false); });
}, [isPresent]);
return (React.createElement(PresenceContext.Provider, { value: context }, children));
};
function newChildrenMap() {
return new Map();
}
function getChildKey(child) {
return child.key || "";
}
function updateChildLookup(children, allChildren) {
var seenChildren = process.env.NODE_ENV !== "production" ? new Set() : null;
children.forEach(function (child) {
var key = getChildKey(child);
if (process.env.NODE_ENV !== "production" && seenChildren) {
if (seenChildren.has(key)) {
console.warn("Children of AnimatePresence require unique keys. \"" + key + "\" is a duplicate.");
}
seenChildren.add(key);
}
allChildren.set(key, child);
});
}
function onlyElements(children) {
var filtered = [];
// We use forEach here instead of map as map mutates the component key by preprending `.$`
React.Children.forEach(children, function (child) {
if (React.isValidElement(child))
filtered.push(child);
});
return filtered;
}
/**
* `AnimatePresence` enables the animation of components that have been removed from the tree.
*
* When adding/removing more than a single child, every child **must** be given a unique `key` prop.
*
* @library
*
* Any `Frame` components that have an `exit` property defined will animate out when removed from
* the tree.
*
* ```jsx
* import { Frame, AnimatePresence } from 'framer'
*
* // As items are added and removed from `items`
* export function Items({ items }) {
* return (
* <AnimatePresence>
* {items.map(item => (
* <Frame
* key={item.id}
* initial={{ opacity: 0 }}
* animate={{ opacity: 1 }}
* exit={{ opacity: 0 }}
* />
* ))}
* </AnimatePresence>
* )
* }
* ```
*
* You can sequence exit animations throughout a tree using variants.
*
* @motion
*
* 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
*/
var AnimatePresence = function (_a) {
var children = _a.children, custom = _a.custom, _b = _a.initial, initial = _b === void 0 ? true : _b, onExitComplete = _a.onExitComplete, exitBeforeEnter = _a.exitBeforeEnter, _c = _a.presenceAffectsLayout, presenceAffectsLayout = _c === void 0 ? true : _c;
// We want to force a re-render once all exiting animations have finished. We
// either use a local forceRender function, or one from a parent context if it exists.
var forceRender = useForceUpdate();
var layoutContext = React.useContext(SharedLayoutContext);
if (isSharedLayout(layoutContext)) {
forceRender = layoutContext.forceUpdate;
}
var isInitialRender = React.useRef(true);
// Filter out any children that aren't ReactElements. We can only track ReactElements with a props.key
var filteredChildren = onlyElements(children);
// Keep a living record of the children we're actually rendering so we
// can diff to figure out which are entering and exiting
var presentChildren = React.useRef(filteredChildren);
// A lookup table to quickly reference components by key
var allChildren = React.useRef(new Map())
.current;
// A living record of all currently exiting components.
var exiting = React.useRef(new Set()).current;
updateChildLookup(filteredChildren, allChildren);
// If this is the initial component render, just deal with logic surrounding whether
// we play onMount animations or not.
if (isInitialRender.current) {
isInitialRender.current = false;
return (React.createElement(React.Fragment, null, filteredChildren.map(function (child) { return (React.createElement(PresenceChild, { key: getChildKey(child), isPresent: true, initial: initial ? undefined : false, presenceAffectsLayout: presenceAffectsLayout }, child)); })));
}
// If this is a subsequent render, deal with entering and exiting children
var childrenToRender = tslib.__spread(filteredChildren);
// Diff the keys of the currently-present and target children to update our
// exiting list.
var presentKeys = presentChildren.current.map(getChildKey);
var targetKeys = filteredChildren.map(getChildKey);
// Diff the present children with our target children and mark those that are exiting
var numPresent = presentKeys.length;
for (var i = 0; i < numPresent; i++) {
var key = presentKeys[i];
if (targetKeys.indexOf(key) === -1) {
exiting.add(key);
}
else {
// In case this key has re-entered, remove from the exiting list
exiting.delete(key);
}
}
// If we currently have exiting children, and we're deferring rendering incoming children
// until after all current children have exiting, empty the childrenToRender array
if (exitBeforeEnter && exiting.size) {
childrenToRender = [];
}
// Loop through all currently exiting components and clone them to overwrite `animate`
// with any `exit` prop they might have defined.
exiting.forEach(function (key) {
// If this component is actually entering again, early return
if (targetKeys.indexOf(key) !== -1)
return;
var child = allChildren.get(key);
if (!child)
return;
var insertionIndex = presentKeys.indexOf(key);
var onExit = function () {
allChildren.delete(key);
exiting.delete(key);
// Remove this child from the present children
var removeIndex = presentChildren.current.findIndex(function (presentChild) { return presentChild.key === key; });
presentChildren.current.splice(removeIndex, 1);
// Defer re-rendering until all exiting children have indeed left
if (!exiting.size) {
presentChildren.current = filteredChildren;
forceRender();
onExitComplete && onExitComplete();
}
};
childrenToRender.splice(insertionIndex, 0, React.createElement(PresenceChild, { key: getChildKey(child), isPresent: false, onExitComplete: onExit, custom: custom, presenceAffectsLayout: presenceAffectsLayout }, child));
});
// Add `MotionContext` even to children that don't need it to ensure we're rendering
// the same tree between renders
childrenToRender = childrenToRender.map(function (child) {
var key = child.key;
return exiting.has(key) ? (child) : (React.createElement(PresenceChild, { key: getChildKey(child), isPresent: true, presenceAffectsLayout: presenceAffectsLayout }, child));
});
presentChildren.current = childrenToRender;
if (process.env.NODE_ENV !== "production" &&
exitBeforeEnter &&
childrenToRender.length > 1) {
console.warn("You're attempting to animate multiple children within AnimatePresence, but its exitBeforeEnter prop is set to true. This will lead to odd visual behaviour.");
}
return (React.createElement(React.Fragment, null, exiting.size
? childrenToRender
: childrenToRender.map(function (child) { return React.cloneElement(child); })));
};
/**
* Animate a single value or a `MotionValue`.
*
* The first argument is either a `MotionValue` to animate, or an initial animation value.
*
* The second is either a value to animate to, or an array of keyframes to animate through.
*
* The third argument can be either tween or spring options, and optional lifecycle methods: `onUpdate`, `onPlay`, `onComplete`, `onRepeat` and `onStop`.
*
* Returns `PlaybackControls`, currently just a `stop` method.
*
* ```javascript
* const x = useMotionValue(0)
*
* useEffect(() => {
* const controls = animate(x, 100, {
* type: "spring",
* stiffness: 2000,
* onComplete: v => {}
* })
*
* return controls.stop
* })
* ```
*
* @public
*/
function animate(from, to, transition) {
if (transition === void 0) { transition = {}; }
var value = isMotionValue(from) ? from : motionValue(from);
startAnimation("", value, to, transition);
return {
stop: function () { return value.stop(); },
};
}
function createCrossfader() {
/**
* The current state of the crossfade as a value between 0 and 1
*/
var progress = motionValue(1);
var options = {
lead: undefined,
follow: undefined,
crossfadeOpacity: false,
preserveFollowOpacity: false,
};
var leadState = {};
var followState = {};
/**
*
*/
var isActive = false;
/**
*
*/
var finalCrossfadeFrame = null;
/**
* Framestamp of the last frame we updated values at.
*/
var prevUpdate = 0;
function startCrossfadeAnimation(target, transition) {
var lead = options.lead, follow = options.follow;
isActive = true;
finalCrossfadeFrame = null;
return animate(progress, target, tslib.__assign(tslib.__assign({}, transition), { onUpdate: function () {
lead && lead.scheduleRender();
follow && follow.scheduleRender();
}, onComplete: function () {
isActive = false;
/**
* If the crossfade animation is no longer active, flag a frame
* that we're still allowed to crossfade
*/
finalCrossfadeFrame = sync.getFrameData().timestamp;
} }));
}
function updateCrossfade() {
var _a, _b;
/**
* We only want to compute the crossfade once per frame, so we
* compare the previous update framestamp with the current frame
* and early return if they're the same.
*/
var timestamp = sync.getFrameData().timestamp;
var lead = options.lead, follow = options.follow;
if (timestamp === prevUpdate || !lead)
return;
prevUpdate = timestamp;
/**
* Merge each component's latest values into our crossfaded state
* before crossfading.
*/
var latestLeadValues = lead.getLatestValues();
Object.assign(leadState, latestLeadValues);
var latestFollowValues = follow
? follow.getLatestValues()
: options.prevValues;
Object.assign(followState, latestFollowValues);
var p = progress.get();
/**
* Crossfade the opacity between the two components. This will result
* in a different opacity for each component.
*/
var leadTargetOpacity = (_a = latestLeadValues.opacity) !== null && _a !== void 0 ? _a : 1;
var followTargetOpacity = (_b = latestFollowValues === null || latestFollowValues === void 0 ? void 0 : latestFollowValues.opacity) !== null && _b !== void 0 ? _b : 1;
if (options.crossfadeOpacity && follow) {
leadState.opacity = popmotion.mix(0, leadTargetOpacity, easeCrossfadeIn(p));
followState.opacity = options.preserveFollowOpacity
? followTargetOpacity
: popmotion.mix(followTargetOpacity, 0, easeCrossfadeOut(p));
}
else if (!follow) {
leadState.opacity = popmotion.mix(followTargetOpacity, leadTargetOpacity, p);
}
mixValues(leadState, followState, latestLeadValues, latestFollowValues || {}, Boolean(follow), p);
}
return {
isActive: function () {
return leadState &&
(isActive || sync.getFrameData().timestamp === finalCrossfadeFrame);
},
fromLead: function (transition) {
return startCrossfadeAnimation(0, transition);
},
toLead: function (transition) {
progress.set(options.follow ? 1 - progress.get() : 0);
return startCrossfadeAnimation(1, transition);
},
reset: function () { return progress.set(1); },
stop: function () { return progress.stop(); },
getCrossfadeState: function (element) {
updateCrossfade();
return element === options.lead ? leadState : followState;
},
setOptions: function (newOptions) {
options = newOptions;
leadState = {};
followState = {};
},
getLatestValues: function () {
return leadState;
},
};
}
var easeCrossfadeIn = compress(0, 0.5, popmotion.circOut);
var easeCrossfadeOut = compress(0.5, 0.95, popmotion.linear);
function compress(min, max, easing) {
return function (p) {
// Could replace ifs with clamp
if (p < min)
return 0;
if (p > max)
return 1;
return easing(popmotion.progress(min, max, p));
};
}
var borders = ["TopLeft", "TopRight", "BottomLeft", "BottomRight"];
var numBorders = borders.length;
function mixValues(leadState, followState, latestLeadValues, latestFollowValues, hasFollowElement, p) {
/**
* Mix border radius
*/
for (var i = 0; i < numBorders; i++) {
var borderLabel = "border" + borders[i] + "Radius";
var followRadius = getRadius(latestFollowValues, borderLabel);
var leadRadius = getRadius(latestLeadValues, borderLabel);
/**
* Currently we're only crossfading between numerical border radius.
* It would be possible to crossfade between percentages for a little
* extra bundle size.
*/
if (typeof followRadius === "number" &&
typeof leadRadius === "number") {
var radius = popmotion.mix(followRadius, leadRadius, p);
leadState[borderLabel] = followState[borderLabel] = radius;
}
}
/**
* Mix rotation
*/
if (latestFollowValues.rotate || latestLeadValues.rotate) {
var rotate = popmotion.mix(latestFollowValues.rotate || 0, latestLeadValues.rotate || 0, p);
leadState.rotate = followState.rotate = rotate;
}
/**
* We only want to mix the background color if there's a follow element
* that we're not crossfading opacity between. For instance with switch
* AnimateSharedLayout animations, this helps the illusion of a continuous
* element being animated but also cuts down on the number of paints triggered
* for elements where opacity is doing that work for us.
*/
if (!hasFollowElement &&
latestLeadValues.backgroundColor &&
latestFollowValues.backgroundColor) {
/**
* This isn't ideal performance-wise as mixColor is creating a new function every frame.
* We could probably create a mixer that runs at the start of the animation but
* the idea behind the crossfader is that it runs dynamically between two potentially
* changing targets (ie opacity or borderRadius may be animating independently via variants)
*/
leadState.backgroundColor = followState.backgroundColor = popmotion.mixColor(latestFollowValues.backgroundColor, latestLeadValues.backgroundColor)(p);
}
}
function getRadius(values, radiusName) {
var _a, _b;
return (_b = (_a = values[radiusName]) !== null && _a !== void 0 ? _a : values.borderRadius) !== null && _b !== void 0 ? _b : 0;
}
function layoutStack() {
var stack = new Set();
var state = { leadIsExiting: false };
var prevState = tslib.__assign({}, state);
var prevValues;
var prevViewportBox;
var prevDragCursor;
var crossfader = createCrossfader();
var needsCrossfadeAnimation = false;
function getFollowViewportBox() {
return state.follow ? state.follow.prevViewportBox : prevViewportBox;
}
function getFollowLayout() {
var _a;
return (_a = state.follow) === null || _a === void 0 ? void 0 : _a.getLayoutState().layout;
}
return {
add: function (element) {
element.setCrossfader(crossfader);
stack.add(element);
/**
* Hydrate new element with previous drag position if we have one
*/
if (prevDragCursor)
element.prevDragCursor = prevDragCursor;
if (!state.lead)
state.lead = element;
},
remove: function (element) {
stack.delete(element);
},
getLead: function () { return state.lead; },
updateSnapshot: function () {
if (!state.lead)
return;
prevValues = crossfader.isActive()
? crossfader.getLatestValues()
: state.lead.getLatestValues();
prevViewportBox = state.lead.prevViewportBox;
var dragControls = elementDragControls.get(state.lead);
if (dragControls && dragControls.isDragging) {
prevDragCursor = dragControls.cursorProgress;
}
},
clearSnapshot: function () {
prevDragCursor = prevViewportBox = undefined;
},
updateLeadAndFollow: function () {
var _a;
prevState = tslib.__assign({}, state);
var lead;
var follow;
var order = Array.from(stack);
for (var i = order.length; i--; i >= 0) {
var element = order[i];
if (lead)
follow !== null && follow !== void 0 ? follow : (follow = element);
lead !== null && lead !== void 0 ? lead : (lead = element);
if (lead && follow)
break;
}
state.lead = lead;
state.follow = follow;
state.leadIsExiting = ((_a = state.lead) === null || _a === void 0 ? void 0 : _a.presence) === Presence.Exiting;
crossfader.setOptions({
lead: lead,
follow: follow,
prevValues: prevValues,
crossfadeOpacity: (follow === null || follow === void 0 ? void 0 : follow.isPresenceRoot) || (lead === null || lead === void 0 ? void 0 : lead.isPresenceRoot),
});
if (prevState.lead !== state.lead ||
prevState.leadIsExiting !== state.leadIsExiting) {
needsCrossfadeAnimation = true;
}
},
animate: function (child, shouldCrossfade) {
if (shouldCrossfade === void 0) { shouldCrossfade = false; }
if (child === state.lead) {
if (shouldCrossfade) {
/**
* Point a lead to itself in case it was previously pointing
* to a different visual element
*/
child.pointTo(state.lead);
}
else {
child.setVisibility(true);
}
var config = {};
if (child.presence === Presence.Entering) {
config.originBox = getFollowViewportBox();
}
else if (child.presence === Presence.Exiting) {
config.targetBox = getFollowLayout();
}
if (needsCrossfadeAnimation) {
needsCrossfadeAnimation = false;
var transition = child.getDefaultTransition();
child.presence === Presence.Entering
? crossfader.toLead(transition)
: crossfader.fromLead(transition);
}
child.notifyLayoutReady(config);
}
else {
if (shouldCrossfade) {
state.lead && child.pointTo(state.lead);
}
else {
child.setVisibility(false);
}
}
},
};
}
function resetRotate(child) {
// If there's no detected rotation values, we can early return without a forced render.
var hasRotate = false;
// Keep a record of all the values we've reset
var resetValues = {};
// Check the rotate value of all axes and reset to 0
for (var i = 0; i < transformAxes.length; i++) {
var axis = transformAxes[i];
var key = "rotate" + axis;
// If this rotation doesn't exist as a motion value, then we don't
// need to reset it
if (!child.hasValue(key) || child.getStaticValue(key) === 0)
continue;
hasRotate = true;
// Record the rotation and then temporarily set it to 0
resetValues[key] = child.getStaticValue(key);
child.setStaticValue(key, 0);
}
// If there's no rotation values, we don't need to do any more.
if (!hasRotate)
return;
// Force a render of this element to apply the transform with all rotations
// set to 0.
child.syncRender();
// Put back all the values we reset
for (var key in resetValues) {
child.setStaticValue(key, resetValues[key]);
}
// Schedule a render for the next frame. This ensures we won't visually
// see the element with the reset rotate value applied.
child.scheduleRender();
}
/**
* @public
*/
var AnimateSharedLayout = /** @class */ (function (_super) {
tslib.__extends(AnimateSharedLayout, _super);
function AnimateSharedLayout() {
var _this = _super !== null && _super.apply(this, arguments) || this;
/**
* A list of all the children in the shared layout
*/
_this.children = new Set();
/**
* As animate components with a defined `layoutId` are added/removed to the tree,
* we store them in order. When one is added, it will animate out from the
* previous one, and when it's removed, it'll animate to the previous one.
*/
_this.stacks = new Map();
/**
* Track whether the component has mounted. If it hasn't, the presence of added children
* are set to Present, whereas if it has they're considered Entering
*/
_this.hasMounted = false;
/**
* Track whether we already have an update scheduled. If we don't, we'll run snapshots
* and schedule one.
*/
_this.updateScheduled = false;
/**
* Tracks whether we already have a render scheduled. If we don't, we'll force one with this.forceRender
*/
_this.renderScheduled = false;
/**
* The methods provided to all children in the shared layout tree.
*/
_this.syncContext = tslib.__assign(tslib.__assign({}, createBatcher()), { syncUpdate: function (force) { return _this.scheduleUpdate(force); }, forceUpdate: function () {
// By copying syncContext to itself, when this component re-renders it'll also re-render
// all children subscribed to the SharedLayout context.
_this.syncContext = tslib.__assign({}, _this.syncContext);
_this.scheduleUpdate(true);
}, register: function (child) { return _this.addChild(child); }, remove: function (child) { return _this.removeChild(child); } });
return _this;
}
AnimateSharedLayout.prototype.componentDidMount = function () {
this.hasMounted = true;
};
AnimateSharedLayout.prototype.componentDidUpdate = function () {
this.startLayoutAnimation();
};
AnimateSharedLayout.prototype.shouldComponentUpdate = function () {
this.renderScheduled = true;
return true;
};
AnimateSharedLayout.prototype.startLayoutAnimation = function () {
var _this = this;
/**
* Reset update and render scheduled status
*/
this.renderScheduled = this.updateScheduled = false;
var type = this.props.type;
/**
* Update presence metadata based on the latest AnimatePresence status.
* This is a kind of goofy way of dealing with this, perhaps there's a better model to find.
*/
this.children.forEach(function (child) {
if (!child.isPresent) {
child.presence = Presence.Exiting;
}
else if (child.presence !== Presence.Entering) {
child.presence =
child.presence === Presence.Exiting
? Presence.Entering
: Presence.Present;
}
});
this.updateStacks();
/**
* Create a handler which we can use to flush the children animations
*/
var handler = {
measureLayout: function (child) { return child.updateLayoutMeasurement(); },
layoutReady: function (child) {
if (child.getLayoutId() !== undefined) {
var stack = _this.getStack(child);
stack.animate(child, type === "crossfade");
}
else {
child.notifyLayoutReady();
}
},
parent: this.context,
};
/**
* Shared layout animations can be used without the AnimateSharedLayout wrapping component.
* This requires some co-ordination across components to stop layout thrashing
* and ensure measurements are taken at the correct time.
*
* Here we use that same mechanism of schedule/flush.
*/
this.children.forEach(function (child) { return _this.syncContext.add(child); });
this.syncContext.flush(handler);
/**
* Clear snapshots so subsequent rerenders don't retain memory of outgoing components
*/
this.stacks.forEach(function (stack) { return stack.clearSnapshot(); });
};
AnimateSharedLayout.prototype.updateStacks = function () {
this.stacks.forEach(function (stack) { return stack.updateLeadAndFollow(); });
};
AnimateSharedLayout.prototype.scheduleUpdate = function (force) {
if (force === void 0) { force = false; }
if (!(force || !this.updateScheduled))
return;
/**
* Flag we've scheduled an update
*/
this.updateScheduled = true;
/**
* Write: Reset rotation transforms so bounding boxes can be accurately measured.
*/
this.children.forEach(function (child) { return resetRotate(child); });
/**
* Read: Snapshot children
*/
this.children.forEach(function (child) { return child.snapshotViewportBox(); });
/**
* Every child keeps a local snapshot, but we also want to record
* snapshots of the visible children as, if they're are being removed
* in this render, we can still access them.
*
* TODO: What would be better here is doing a single loop where we
* only snapshotViewportBoxes of undefined layoutIds and then one for each stack
*/
this.stacks.forEach(function (stack) { return stack.updateSnapshot(); });
/**
* Force a rerender by setting state if we aren't already going to render.
*/
if (force || !this.renderScheduled) {
this.renderScheduled = true;
this.forceUpdate();
}
};
AnimateSharedLayout.prototype.addChild = function (child) {
this.children.add(child);
this.addToStack(child);
child.presence = this.hasMounted ? Presence.Entering : Presence.Present;
};
AnimateSharedLayout.prototype.removeChild = function (child) {
this.scheduleUpdate();
this.children.delete(child);
this.removeFromStack(child);
};
AnimateSharedLayout.prototype.addToStack = function (child) {
var stack = this.getStack(child);
stack === null || stack === void 0 ? void 0 : stack.add(child);
};
AnimateSharedLayout.prototype.removeFromStack = function (child) {
var stack = this.getStack(child);
stack === null || stack === void 0 ? void 0 : stack.remove(child);
};
/**
* Return a stack of animate children based on the provided layoutId.
* Will create a stack if none currently exists with that layoutId.
*/
AnimateSharedLayout.prototype.getStack = function (child) {
var id = child.getLayoutId();
if (id === undefined)
return;
// Create stack if it doesn't already exist
!this.stacks.has(id) && this.stacks.set(id, layoutStack());
return this.stacks.get(id);
};
AnimateSharedLayout.prototype.render = function () {
return (React.createElement(SharedLayoutContext.Provider, { value: this.syncContext }, this.props.children));
};
AnimateSharedLayout.contextType = MotionContext;
return AnimateSharedLayout;
}(React.Component));
/**
* Creates a `MotionValue` to track the state and velocity of a value.
*
* Usually, these are created automatically. For advanced use-cases, like use with `useTransform`, you can create `MotionValue`s externally and pass them into the animated component via the `style` prop.
*
* @library
*
* ```jsx
* export function MyComponent() {
* const scale = useMotionValue(1)
*
* return <Frame scale={scale} />
* }
* ```
*
* @motion
*
* ```jsx
* export const MyComponent = () => {
* const scale = useMotionValue(1)
*
* return <motion.div style={{ scale }} />
* }
* ```
*
* @param initial - The initial state.
*
* @public
*/
function useMotionValue(initial) {
return useConstant(function () { return motionValue(initial); });
}
function useOnChange(value, callback) {
React.useEffect(function () {
return isMotionValue(value) ? value.onChange(callback) : undefined;
});
}
function useMultiOnChange(values, handler) {
React.useEffect(function () {
var subscriptions = values.map(function (value) { return value.onChange(handler); });
return function () { return subscriptions.forEach(function (unsubscribe) { return unsubscribe(); }); };
});
}
function useCombineMotionValues(values, combineValues) {
/**
* Initialise the returned motion value. This remains the same between renders.
*/
var value = useMotionValue(combineValues());
/**
* Create a function that will update the template motion value with the latest values.
* This is pre-bound so whenever a motion value updates it can schedule its
* execution in Framesync. If it's already been scheduled it won't be fired twice
* in a single frame.
*/
var updateValue = function () { return value.set(combineValues()); };
/**
* Synchronously update the motion value with the latest values during the render.
* This ensures that within a React render, the styles applied to the DOM are up-to-date.
*/
updateValue();
/**
* Subscribe to all motion values found within the template. Whenever any of them change,
* schedule an update.
*/
useMultiOnChange(values, function () { return sync__default['default'].update(updateValue, false, true); });
return value;
}
/**
* Combine multiple motion values into a new one using a string template literal.
*
* ```jsx
* import {
* motion,
* useSpring,
* useMotionValue,
* useMotionTemplate
* } from "framer-motion"
*
* function Component() {
* const shadowX = useSpring(0)
* const shadowY = useMotionValue(0)
* const shadow = useMotionTemplate`drop-shadow(${shadowX}px ${shadowY}px 20px rgba(0,0,0,0.3))`
*
* return <motion.div style={{ filter: shadow }} />
* }
* ```
*
* @public
*/
function useMotionTemplate(fragments) {
var values = [];
for (var _i = 1; _i < arguments.length; _i++) {
values[_i - 1] = arguments[_i];
}
/**
* Create a function that will build a string from the latest motion values.
*/
var numFragments = fragments.length;
function buildValue() {
var output = "";
for (var i = 0; i < numFragments; i++) {
output += fragments[i];
var value = values[i];
if (value)
output += values[i].get();
}
return output;
}
return useCombineMotionValues(values, buildValue);
}
/**
* If the provided value is a MotionValue, this returns the actual value, otherwise just the value itself
*
* TODO: Remove and move to library
*
* @internal
*/
function resolveMotionValue(value) {
var unwrappedValue = value instanceof MotionValue ? value.get() : value;
return isCustomValue(unwrappedValue)
? unwrappedValue.toValue()
: unwrappedValue;
}
var isCustomValueType = function (v) {
return typeof v === "object" && v.mix;
};
var getMixer = function (v) { return (isCustomValueType(v) ? v.mix : undefined); };
function transform() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
var useImmediate = !Array.isArray(args[0]);
var argOffset = useImmediate ? 0 : -1;
var inputValue = args[0 + argOffset];
var inputRange = args[1 + argOffset];
var outputRange = args[2 + argOffset];
var options = args[3 + argOffset];
var interpolator = popmotion.interpolate(inputRange, outputRange, tslib.__assign({ mixer: getMixer(outputRange[0]) }, options));
return useImmediate ? interpolator(inputValue) : interpolator;
}
function useTransform(input, inputRangeOrTransformer, outputRange, options) {
var transformer = typeof inputRangeOrTransformer === "function"
? inputRangeOrTransformer
: transform(inputRangeOrTransformer, outputRange, options);
return Array.isArray(input)
? useListTransform(input, transformer)
: useListTransform([input], function (_a) {
var _b = tslib.__read(_a, 1), latest = _b[0];
return transformer(latest);
});
}
function useListTransform(values, transformer) {
var latest = useConstant(function () { return []; });
return useCombineMotionValues(values, function () {
latest.length = 0;
var numValues = values.length;
for (var i = 0; i < numValues; i++) {
latest[i] = values[i].get();
}
return transformer(latest);
});
}
/**
* Creates a `MotionValue` that, when `set`, will use a spring animation to animate to its new state.
*
* It can either work as a stand-alone `MotionValue` by initialising it with a value, or as a subscriber
* to another `MotionValue`.
*
* @remarks
*
* ```jsx
* const x = useSpring(0, { stiffness: 300 })
* const y = useSpring(x, { damping: 10 })
* ```
*
* @param inputValue - `MotionValue` or number. If provided a `MotionValue`, when the input `MotionValue` changes, the created `MotionValue` will spring towards that value.
* @param springConfig - Configuration options for the spring.
* @returns `MotionValue`
*
* @public
*/
function useSpring(source, config) {
if (config === void 0) { config = {}; }
var isStatic = React.useContext(MotionConfigContext).isStatic;
var activeSpringAnimation = React.useRef(null);
var value = useMotionValue(isMotionValue(source) ? source.get() : source);
React.useMemo(function () {
return value.attach(function (v, set) {
/**
* A more hollistic approach to this might be to use isStatic to fix VisualElement animations
* at that level, but this will work for now
*/
if (isStatic)
return set(v);
if (activeSpringAnimation.current) {
activeSpringAnimation.current.stop();
}
activeSpringAnimation.current = popmotion.animate(tslib.__assign(tslib.__assign({ from: value.get(), to: v, velocity: value.getVelocity() }, config), { onUpdate: set }));
return value.get();
});
}, Object.values(config));
useOnChange(source, function (v) { return value.set(parseFloat(v)); });
return value;
}
function createScrollMotionValues() {
return {
scrollX: motionValue(0),
scrollY: motionValue(0),
scrollXProgress: motionValue(0),
scrollYProgress: motionValue(0),
};
}
function setProgress(offset, maxOffset, value) {
value.set(!offset || !maxOffset ? 0 : offset / maxOffset);
}
function createScrollUpdater(values, getOffsets) {
var update = function () {
var _a = getOffsets(), xOffset = _a.xOffset, yOffset = _a.yOffset, xMaxOffset = _a.xMaxOffset, yMaxOffset = _a.yMaxOffset;
// Set absolute positions
values.scrollX.set(xOffset);
values.scrollY.set(yOffset);
// Set 0-1 progress
setProgress(xOffset, xMaxOffset, values.scrollXProgress);
setProgress(yOffset, yMaxOffset, values.scrollYProgress);
};
update();
return update;
}
var getElementScrollOffsets = function (element) { return function () {
return {
xOffset: element.scrollLeft,
yOffset: element.scrollTop,
xMaxOffset: element.scrollWidth - element.offsetWidth,
yMaxOffset: element.scrollHeight - element.offsetHeight,
};
}; };
/**
* Returns MotionValues that update when the provided element scrolls:
*
* - `scrollX` — Horizontal scroll distance in pixels.
* - `scrollY` — Vertical scroll distance in pixels.
* - `scrollXProgress` — Horizontal scroll progress between `0` and `1`.
* - `scrollYProgress` — Vertical scroll progress between `0` and `1`.
*
* This element must be set to `overflow: scroll` on either or both axes to report scroll offset.
*
* @library
*
* ```jsx
* import * as React from "react"
* import {
* Frame,
* useElementScroll,
* useTransform
* } from "framer"
*
* export function MyComponent() {
* const ref = React.useRef()
* const { scrollYProgress } = useElementScroll(ref)
*
* return (
* <Frame ref={ref}>
* <Frame scaleX={scrollYProgress} />
* </Frame>
* )
* }
* ```
*
* @motion
*
* ```jsx
* export const MyComponent = () => {
* const ref = useRef()
* const { scrollYProgress } = useElementScroll(ref)
*
* return (
* <div ref={ref}>
* <motion.div style={{ scaleX: scrollYProgress }} />
* </div>
* )
* }
* ```
*
* @public
*/
function useElementScroll(ref) {
var values = useConstant(createScrollMotionValues);
useIsomorphicLayoutEffect(function () {
var element = ref.current;
heyListen.invariant(!!element, "ref provided to useScroll must be passed into a HTML element.");
if (!element)
return;
var updateScrollValues = createScrollUpdater(values, getElementScrollOffsets(element));
var scrollListener = addDomEvent(element, "scroll", updateScrollValues, { passive: true });
var resizeListener = addDomEvent(element, "resize", updateScrollValues);
return function () {
scrollListener && scrollListener();
resizeListener && resizeListener();
};
}, []);
return values;
}
var viewportScrollValues;
function getViewportScrollOffsets() {
return {
xOffset: window.pageXOffset,
yOffset: window.pageYOffset,
xMaxOffset: document.body.clientWidth - window.innerWidth,
yMaxOffset: document.body.clientHeight - window.innerHeight,
};
}
var hasListeners = false;
function addEventListeners() {
hasListeners = true;
if (typeof window === "undefined")
return;
var updateScrollValues = createScrollUpdater(viewportScrollValues, getViewportScrollOffsets);
addDomEvent(window, "scroll", updateScrollValues, { passive: true });
addDomEvent(window, "resize", updateScrollValues);
}
/**
* Returns MotionValues that update when the viewport scrolls:
*
* - `scrollX` — Horizontal scroll distance in pixels.
* - `scrollY` — Vertical scroll distance in pixels.
* - `scrollXProgress` — Horizontal scroll progress between `0` and `1`.
* - `scrollYProgress` — Vertical scroll progress between `0` and `1`.
*
* **Warning:** Setting `body` or `html` to `height: 100%` or similar will break the `Progress`
* values as this breaks the browser's capability to accurately measure the page length.
*
* @library
*
* ```jsx
* import * as React from "react"
* import {
* Frame,
* useViewportScroll,
* useTransform
* } from "framer"
*
* export function MyComponent() {
* const { scrollYProgress } = useViewportScroll()
* return <Frame scaleX={scrollYProgress} />
* }
* ```
*
* @motion
*
* ```jsx
* export const MyComponent = () => {
* const { scrollYProgress } = useViewportScroll()
* return <motion.div style={{ scaleX: scrollYProgress }} />
* }
* ```
*
* @public
*/
function useViewportScroll() {
/**
* Lazy-initialise the viewport scroll values
*/
if (!viewportScrollValues) {
viewportScrollValues = createScrollMotionValues();
}
useIsomorphicLayoutEffect(function () {
!hasListeners && addEventListeners();
}, []);
return viewportScrollValues;
}
// Does this device prefer reduced motion? Returns `null` server-side.
var prefersReducedMotion;
function initPrefersReducedMotion() {
prefersReducedMotion = motionValue(null);
if (typeof window === "undefined")
return;
if (window.matchMedia) {
var motionMediaQuery_1 = window.matchMedia("(prefers-reduced-motion)");
var setReducedMotionPreferences = function () {
return prefersReducedMotion.set(motionMediaQuery_1.matches);
};
motionMediaQuery_1.addListener(setReducedMotionPreferences);
setReducedMotionPreferences();
}
else {
prefersReducedMotion.set(false);
}
}
/**
* A hook that returns `true` if we should be using reduced motion based on the current device's Reduced Motion setting.
*
* This can be used to implement changes to your UI based on Reduced Motion. For instance, replacing motion-sickness inducing
* `x`/`y` animations with `opacity`, disabling the autoplay of background videos, or turning off parallax motion.
*
* It will actively respond to changes and re-render your components with the latest setting.
*
* ```jsx
* export function Sidebar({ isOpen }) {
* const shouldReduceMotion = useReducedMotion()
* const closedX = shouldReduceMotion ? 0 : "-100%"
*
* return (
* <motion.div animate={{
* opacity: isOpen ? 1 : 0,
* x: isOpen ? 0 : closedX
* }} />
* )
* }
* ```
*
* @return boolean
*
* @public
*/
function useReducedMotion() {
/**
* Lazy initialisation of prefersReducedMotion
*/
!prefersReducedMotion && initPrefersReducedMotion();
var _a = tslib.__read(React.useState(prefersReducedMotion.get()), 2), shouldReduceMotion = _a[0], setShouldReduceMotion = _a[1];
useOnChange(prefersReducedMotion, setShouldReduceMotion);
return shouldReduceMotion;
}
/**
* 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.
*
* @library
*
* ```jsx
* import * as React from 'react'
* import { Frame, useAnimation } from 'framer'
*
* export function MyComponent(props) {
* const controls = useAnimation()
*
* controls.start({
* x: 100,
* transition: { duration: 0.5 },
* })
*
* return <Frame animate={controls} />
* }
* ```
*
* @motion
*
* ```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 useAnimation() {
var controls = useConstant(animationControls);
React.useEffect(controls.mount, []);
return controls;
}
/**
* Cycles through a series of visual properties. Can be used to toggle between or cycle through animations. It works similar to `useState` in React. It is provided an initial array of possible states, and returns an array of two arguments.
*
* @library
*
* ```jsx
* import * as React from "react"
* import { Frame, useCycle } from "framer"
*
* export function MyComponent() {
* const [x, cycleX] = useCycle(0, 50, 100)
*
* return (
* <Frame
* animate={{ x: x }}
* onTap={() => cycleX()}
* />
* )
* }
* ```
*
* @motion
*
* An index value can be passed to the returned `cycle` function to cycle to a specific index.
*
* ```jsx
* import * as React from "react"
* import { motion, useCycle } from "framer-motion"
*
* export const MyComponent = () => {
* const [x, cycleX] = useCycle(0, 50, 100)
*
* return (
* <motion.div
* animate={{ x: x }}
* onTap={() => cycleX()}
* />
* )
* }
* ```
*
* @param items - items to cycle through
* @returns [currentState, cycleState]
*
* @public
*/
function useCycle() {
var items = [];
for (var _i = 0; _i < arguments.length; _i++) {
items[_i] = arguments[_i];
}
var index = React.useRef(0);
var _a = tslib.__read(React.useState(items[index.current]), 2), item = _a[0], setItem = _a[1];
return [
item,
function (next) {
index.current =
typeof next !== "number"
? popmotion.wrap(0, items.length, index.current + 1)
: next;
setItem(items[index.current]);
},
];
}
/**
* Can manually trigger a drag gesture on one or more `drag`-enabled `motion` components.
*
* @library
*
* ```jsx
* const dragControls = useDragControls()
*
* function startDrag(event) {
* dragControls.start(event, { snapToCursor: true })
* }
*
* return (
* <>
* <Frame onTapStart={startDrag} />
* <Frame drag="x" dragControls={dragControls} />
* </>
* )
* ```
*
* @motion
*
* ```jsx
* const dragControls = useDragControls()
*
* function startDrag(event) {
* dragControls.start(event, { snapToCursor: true })
* }
*
* return (
* <>
* <div onPointerDown={startDrag} />
* <motion.div drag="x" dragControls={dragControls} />
* </>
* )
* ```
*
* @public
*/
var DragControls = /** @class */ (function () {
function DragControls() {
this.componentControls = new Set();
}
/**
* Subscribe a component's internal `VisualElementDragControls` to the user-facing API.
*
* @internal
*/
DragControls.prototype.subscribe = function (controls) {
var _this = this;
this.componentControls.add(controls);
return function () { return _this.componentControls.delete(controls); };
};
/**
* Start a drag gesture on every `motion` component that has this set of drag controls
* passed into it via the `dragControls` prop.
*
* ```jsx
* dragControls.start(e, {
* snapToCursor: true
* })
* ```
*
* @param event - PointerEvent
* @param options - Options
*
* @public
*/
DragControls.prototype.start = function (event, options) {
this.componentControls.forEach(function (controls) {
controls.start(event.nativeEvent || event, options);
});
};
DragControls.prototype.updateConstraints = function () {
this.componentControls.forEach(function (controls) {
controls.prepareBoundingBox();
controls.resolveDragConstraints();
});
};
return DragControls;
}());
var createDragControls = function () { return new DragControls(); };
/**
* Usually, dragging is initiated by pressing down on a `motion` component with a `drag` prop
* and moving it. For some use-cases, for instance clicking at an arbitrary point on a video scrubber, we
* might want to initiate that dragging from a different component than the draggable one.
*
* By creating a `dragControls` using the `useDragControls` hook, we can pass this into
* the draggable component's `dragControls` prop. It exposes a `start` method
* that can start dragging from pointer events on other components.
*
* @library
*
* ```jsx
* const dragControls = useDragControls()
*
* function startDrag(event) {
* dragControls.start(event, { snapToCursor: true })
* }
*
* return (
* <>
* <Frame onTapStart={startDrag} />
* <Frame drag="x" dragControls={dragControls} />
* </>
* )
* ```
*
* @motion
*
* ```jsx
* const dragControls = useDragControls()
*
* function startDrag(event) {
* dragControls.start(event, { snapToCursor: true })
* }
*
* return (
* <>
* <div onPointerDown={startDrag} />
* <motion.div drag="x" dragControls={dragControls} />
* </>
* )
* ```
*
* @public
*/
function useDragControls() {
return useConstant(createDragControls);
}
var stateVisualElement = visualElement({
createRenderState: function () { return ({}); },
build: function () { },
measureViewportBox: axisBox,
resetTransform: function () { },
restoreTransform: function () { },
removeValueFromMutableState: function () { },
render: function () { },
scrapeMotionValuesFromProps: function () {
return {};
},
readValueFromInstance: function (_state, key, options) {
return options.initialState[key] || 0;
},
makeTargetAnimatable: function (element, _a) {
var transition = _a.transition, transitionEnd = _a.transitionEnd, target = tslib.__rest(_a, ["transition", "transitionEnd"]);
var origin = getOrigin(target, transition || {}, element);
checkTargetForNewValues(element, target, origin);
return tslib.__assign({ transition: transition, transitionEnd: transitionEnd }, target);
},
});
/**
* This is not an officially supported API and may be removed
* on any version.
* @internal
*/
function useAnimatedState(initialState) {
var _a = tslib.__read(React.useState(initialState), 2), animationState = _a[0], setAnimationState = _a[1];
var element = useConstant(function () {
return stateVisualElement({ props: {} }, { initialState: initialState });
});
React.useEffect(function () {
element.ref({});
return function () { return element.ref(null); };
}, []);
React.useEffect(function () {
element.setProps({
onUpdate: function (v) { return setAnimationState(tslib.__assign({}, v)); },
});
});
var startAnimation = useConstant(function () { return function (animationDefinition) {
return animateVisualElement(element, animationDefinition);
}; });
return [animationState, startAnimation];
}
// Keep things reasonable and avoid scale: Infinity. In practise we might need
// to add another value, opacity, that could interpolate scaleX/Y [0,0.01] => [0,1]
// to simply hide content at unreasonable scales.
var maxScale = 100000;
var invertScale = function (scale) {
return scale > 0.001 ? 1 / scale : maxScale;
};
var hasWarned = false;
/**
* Returns a `MotionValue` each for `scaleX` and `scaleY` that update with the inverse
* of their respective parent scales.
*
* This is useful for undoing the distortion of content when scaling a parent component.
*
* By default, `useInvertedScale` will automatically fetch `scaleX` and `scaleY` from the nearest parent.
* By passing other `MotionValue`s in as `useInvertedScale({ scaleX, scaleY })`, it will invert the output
* of those instead.
*
* @motion
*
* ```jsx
* const MyComponent = () => {
* const { scaleX, scaleY } = useInvertedScale()
* return <motion.div style={{ scaleX, scaleY }} />
* }
* ```
*
* @library
*
* ```jsx
* function MyComponent() {
* const { scaleX, scaleY } = useInvertedScale()
* return <Frame scaleX={scaleX} scaleY={scaleY} />
* }
* ```
*
* @deprecated
*/
function useInvertedScale(scale) {
var parentScaleX = useMotionValue(1);
var parentScaleY = useMotionValue(1);
var visualElement = useVisualElementContext();
heyListen.invariant(!!(scale || visualElement), "If no scale values are provided, useInvertedScale must be used within a child of another motion component.");
heyListen.warning(hasWarned, "useInvertedScale is deprecated and will be removed in 3.0. Use the layout prop instead.");
hasWarned = true;
if (scale) {
parentScaleX = scale.scaleX || parentScaleX;
parentScaleY = scale.scaleY || parentScaleY;
}
else if (visualElement) {
parentScaleX = visualElement.getValue("scaleX", 1);
parentScaleY = visualElement.getValue("scaleY", 1);
}
var scaleX = useTransform(parentScaleX, invertScale);
var scaleY = useTransform(parentScaleY, invertScale);
return { scaleX: scaleX, scaleY: scaleY };
}
exports.AnimateLayoutFeature = AnimateLayout;
exports.AnimatePresence = AnimatePresence;
exports.AnimateSharedLayout = AnimateSharedLayout;
exports.AnimationFeature = Animation;
exports.DragControls = DragControls;
exports.DragFeature = Drag;
exports.ExitFeature = Exit;
exports.FramerTreeLayoutContext = FramerTreeLayoutContext;
exports.GesturesFeature = Gestures;
exports.LayoutGroupContext = LayoutGroupContext;
exports.MotionConfig = MotionConfig;
exports.MotionConfigContext = MotionConfigContext;
exports.MotionValue = MotionValue;
exports.PresenceContext = PresenceContext;
exports.SharedLayoutContext = SharedLayoutContext;
exports.addScaleCorrection = addScaleCorrection;
exports.animate = animate;
exports.animateVisualElement = animateVisualElement;
exports.animationControls = animationControls;
exports.createBatcher = createBatcher;
exports.createCrossfader = createCrossfader;
exports.createDomMotionComponent = createDomMotionComponent;
exports.createMotionComponent = createMotionComponent;
exports.isValidMotionProp = isValidMotionProp;
exports.m = m;
exports.motion = motion;
exports.motionValue = motionValue;
exports.resolveMotionValue = resolveMotionValue;
exports.transform = transform;
exports.useAnimation = useAnimation;
exports.useCycle = useCycle;
exports.useDeprecatedAnimatedState = useAnimatedState;
exports.useDeprecatedInvertedScale = useInvertedScale;
exports.useDomEvent = useDomEvent;
exports.useDragControls = useDragControls;
exports.useElementScroll = useElementScroll;
exports.useGestures = useGestures;
exports.useIsPresent = useIsPresent;
exports.useMotionTemplate = useMotionTemplate;
exports.useMotionValue = useMotionValue;
exports.usePanGesture = usePanGesture;
exports.usePresence = usePresence;
exports.useReducedMotion = useReducedMotion;
exports.useSpring = useSpring;
exports.useTapGesture = useTapGesture;
exports.useTransform = useTransform;
exports.useViewportScroll = useViewportScroll;
exports.visualElement = visualElement;