reveal-on-scroll-react
Version:
Add scroll animation to your React application super fast!
1,536 lines (1,456 loc) • 337 kB
JavaScript
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var React = require('react');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
const htmlElements = [
"p",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"strong",
"em",
"blockquote",
"cite",
"q",
"div",
"ul",
"li",
// "a",
// "img",
];
/**
* @public
*/
const MotionConfigContext = React.createContext({
transformPagePoint: (p) => p,
isStatic: false,
reducedMotion: "never",
});
const MotionContext = React.createContext({});
/**
* @public
*/
const PresenceContext = React.createContext(null);
const isBrowser = typeof document !== "undefined";
const useIsomorphicLayoutEffect = isBrowser ? React.useLayoutEffect : React.useEffect;
const LazyContext = React.createContext({ strict: false });
function useVisualElement(Component, visualState, props, createVisualElement) {
const { visualElement: parent } = React.useContext(MotionContext);
const lazyContext = React.useContext(LazyContext);
const presenceContext = React.useContext(PresenceContext);
const reducedMotionConfig = React.useContext(MotionConfigContext).reducedMotion;
const visualElementRef = React.useRef();
/**
* If we haven't preloaded a renderer, check to see if we have one lazy-loaded
*/
createVisualElement = createVisualElement || lazyContext.renderer;
if (!visualElementRef.current && createVisualElement) {
visualElementRef.current = createVisualElement(Component, {
visualState,
parent,
props,
presenceContext,
blockInitialAnimation: presenceContext
? presenceContext.initial === false
: false,
reducedMotionConfig,
});
}
const visualElement = visualElementRef.current;
React.useInsertionEffect(() => {
visualElement && visualElement.update(props, presenceContext);
});
useIsomorphicLayoutEffect(() => {
visualElement && visualElement.render();
});
React.useEffect(() => {
visualElement && visualElement.updateFeatures();
});
/**
* Ideally this function would always run in a useEffect.
*
* However, if we have optimised appear animations to handoff from,
* it needs to happen synchronously to ensure there's no flash of
* incorrect styles in the event of a hydration error.
*
* So if we detect a situtation where optimised appear animations
* are running, we use useLayoutEffect to trigger animations.
*/
const useAnimateChangesEffect = window.HandoffAppearAnimations
? useIsomorphicLayoutEffect
: React.useEffect;
useAnimateChangesEffect(() => {
if (visualElement && visualElement.animationState) {
visualElement.animationState.animateChanges();
}
});
return visualElement;
}
function isRefObject(ref) {
return (typeof ref === "object" &&
Object.prototype.hasOwnProperty.call(ref, "current"));
}
/**
* Creates a ref function that, when called, hydrates the provided
* external ref and VisualElement.
*/
function useMotionRef(visualState, visualElement, externalRef) {
return React.useCallback((instance) => {
instance && visualState.mount && visualState.mount(instance);
if (visualElement) {
instance
? visualElement.mount(instance)
: visualElement.unmount();
}
if (externalRef) {
if (typeof externalRef === "function") {
externalRef(instance);
}
else if (isRefObject(externalRef)) {
externalRef.current = instance;
}
}
},
/**
* Only pass a new ref callback to React if we've received a visual element
* factory. Otherwise we'll be mounting/remounting every time externalRef
* or other dependencies change.
*/
[visualElement]);
}
/**
* Decides if the supplied variable is variant label
*/
function isVariantLabel(v) {
return typeof v === "string" || Array.isArray(v);
}
function isAnimationControls(v) {
return typeof v === "object" && typeof v.start === "function";
}
const variantPriorityOrder = [
"animate",
"whileInView",
"whileFocus",
"whileHover",
"whileTap",
"whileDrag",
"exit",
];
const variantProps = ["initial", ...variantPriorityOrder];
function isControllingVariants(props) {
return (isAnimationControls(props.animate) ||
variantProps.some((name) => isVariantLabel(props[name])));
}
function isVariantNode(props) {
return Boolean(isControllingVariants(props) || props.variants);
}
function getCurrentTreeVariants(props, context) {
if (isControllingVariants(props)) {
const { initial, animate } = props;
return {
initial: initial === false || isVariantLabel(initial)
? initial
: undefined,
animate: isVariantLabel(animate) ? animate : undefined,
};
}
return props.inherit !== false ? context : {};
}
function useCreateMotionContext(props) {
const { initial, animate } = getCurrentTreeVariants(props, React.useContext(MotionContext));
return React.useMemo(() => ({ initial, animate }), [variantLabelsAsDependency(initial), variantLabelsAsDependency(animate)]);
}
function variantLabelsAsDependency(prop) {
return Array.isArray(prop) ? prop.join(" ") : prop;
}
const featureProps = {
animation: [
"animate",
"variants",
"whileHover",
"whileTap",
"exit",
"whileInView",
"whileFocus",
"whileDrag",
],
exit: ["exit"],
drag: ["drag", "dragControls"],
focus: ["whileFocus"],
hover: ["whileHover", "onHoverStart", "onHoverEnd"],
tap: ["whileTap", "onTap", "onTapStart", "onTapCancel"],
pan: ["onPan", "onPanStart", "onPanSessionStart", "onPanEnd"],
inView: ["whileInView", "onViewportEnter", "onViewportLeave"],
layout: ["layout", "layoutId"],
};
const featureDefinitions = {};
for (const key in featureProps) {
featureDefinitions[key] = {
isEnabled: (props) => featureProps[key].some((name) => !!props[name]),
};
}
function loadFeatures(features) {
for (const key in features) {
featureDefinitions[key] = {
...featureDefinitions[key],
...features[key],
};
}
}
/**
* Creates a constant value over the lifecycle of a component.
*
* Even if `useMemo` is provided an empty array as its final argument, it doesn't offer
* a guarantee that it won't re-run for performance reasons later on. By using `useConstant`
* you can ensure that initialisers don't execute twice or more.
*/
function useConstant(init) {
const ref = React.useRef(null);
if (ref.current === null) {
ref.current = init();
}
return ref.current;
}
/**
* This should only ever be modified on the client otherwise it'll
* persist through server requests. If we need instanced states we
* could lazy-init via root.
*/
const globalProjectionState = {
/**
* Global flag as to whether the tree has animated since the last time
* we resized the window
*/
hasAnimatedSinceResize: true,
/**
* We set this to true once, on the first update. Any nodes added to the tree beyond that
* update will be given a `data-projection-id` attribute.
*/
hasEverUpdated: false,
};
let id$2 = 1;
function useProjectionId() {
return useConstant(() => {
if (globalProjectionState.hasEverUpdated) {
return id$2++;
}
});
}
const LayoutGroupContext = React.createContext({});
/**
* Internal, exported only for usage in Framer
*/
const SwitchLayoutGroupContext = React.createContext({});
const motionComponentSymbol = Symbol.for("motionComponentSymbol");
/**
* 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.
*/
function createMotionComponent({ preloadedFeatures, createVisualElement, useRender, useVisualState, Component, }) {
preloadedFeatures && loadFeatures(preloadedFeatures);
function MotionComponent(props, externalRef) {
/**
* If we need to measure the element we load this functionality in a
* separate class component in order to gain access to getSnapshotBeforeUpdate.
*/
let MeasureLayout;
const configAndProps = {
...React.useContext(MotionConfigContext),
...props,
layoutId: useLayoutId(props),
};
const { isStatic } = configAndProps;
const context = useCreateMotionContext(props);
/**
* Create a unique projection ID for this component. If a new component is added
* during a layout animation we'll use this to query the DOM and hydrate its ref early, allowing
* us to measure it as soon as any layout effect flushes pending layout animations.
*
* Performance note: It'd be better not to have to search the DOM for these elements.
* For newly-entering components it could be enough to only correct treeScale, in which
* case we could mount in a scale-correction mode. This wouldn't be enough for
* shared element transitions however. Perhaps for those we could revert to a root node
* that gets forceRendered and layout animations are triggered on its layout effect.
*/
const projectionId = isStatic ? undefined : useProjectionId();
const visualState = useVisualState(props, isStatic);
if (!isStatic && isBrowser) {
/**
* 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
*/
context.visualElement = useVisualElement(Component, visualState, configAndProps, createVisualElement);
/**
* Load Motion gesture and animation features. These are rendered as renderless
* components so each feature can optionally make use of React lifecycle methods.
*/
const initialLayoutGroupConfig = React.useContext(SwitchLayoutGroupContext);
const isStrict = React.useContext(LazyContext).strict;
if (context.visualElement) {
MeasureLayout = context.visualElement.loadFeatures(
// Note: Pass the full new combined props to correctly re-render dynamic feature components.
configAndProps, isStrict, preloadedFeatures, projectionId, initialLayoutGroupConfig);
}
}
/**
* The mount order and hierarchy is specific to ensure our element ref
* is hydrated by the time features fire their effects.
*/
return (React__namespace.createElement(MotionContext.Provider, { value: context },
MeasureLayout && context.visualElement ? (React__namespace.createElement(MeasureLayout, { visualElement: context.visualElement, ...configAndProps })) : null,
useRender(Component, props, projectionId, useMotionRef(visualState, context.visualElement, externalRef), visualState, isStatic, context.visualElement)));
}
const ForwardRefComponent = React.forwardRef(MotionComponent);
ForwardRefComponent[motionComponentSymbol] = Component;
return ForwardRefComponent;
}
function useLayoutId({ layoutId }) {
const layoutGroupId = React.useContext(LayoutGroupContext).id;
return layoutGroupId && layoutId !== undefined
? layoutGroupId + "-" + layoutId
: layoutId;
}
/**
* 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(Component)
* ```
*
* @public
*/
function createMotionProxy(createConfig) {
function custom(Component, customMotionComponentConfig = {}) {
return createMotionComponent(createConfig(Component, customMotionComponentConfig));
}
if (typeof Proxy === "undefined") {
return custom;
}
/**
* A cache of generated `motion` components, e.g `motion.div`, `motion.input` etc.
* Rather than generating them anew every render.
*/
const componentCache = new Map();
return new Proxy(custom, {
/**
* Called when `motion` is referenced with a prop: `motion.div`, `motion.input` etc.
* The prop name is passed through as `key` and we can use that to generate a `motion`
* DOM component with that name.
*/
get: (_target, key) => {
/**
* If this element doesn't exist in the component cache, create it and cache.
*/
if (!componentCache.has(key)) {
componentCache.set(key, custom(key));
}
return componentCache.get(key);
},
});
}
/**
* We keep these listed seperately as we use the lowercase tag names as part
* of the runtime bundle to detect SVG components
*/
const lowercaseSVGElements = [
"animate",
"circle",
"defs",
"desc",
"ellipse",
"g",
"image",
"line",
"filter",
"marker",
"mask",
"metadata",
"path",
"pattern",
"polygon",
"polyline",
"rect",
"stop",
"switch",
"symbol",
"svg",
"text",
"tspan",
"use",
"view",
];
function isSVGComponent(Component) {
if (
/**
* If it's not a string, it's a custom React component. Currently we only support
* HTML custom React components.
*/
typeof Component !== "string" ||
/**
* If it contains a dash, the element is a custom HTML webcomponent.
*/
Component.includes("-")) {
return false;
}
else if (
/**
* If it's in our list of lowercase SVG tags, it's an SVG component
*/
lowercaseSVGElements.indexOf(Component) > -1 ||
/**
* If it contains a capital letter, it's an SVG component
*/
/[A-Z]/.test(Component)) {
return true;
}
return false;
}
const scaleCorrectors = {};
function addScaleCorrector(correctors) {
Object.assign(scaleCorrectors, correctors);
}
/**
* Generate a list of every possible transform key.
*/
const transformPropOrder = [
"transformPerspective",
"x",
"y",
"z",
"translateX",
"translateY",
"translateZ",
"scale",
"scaleX",
"scaleY",
"rotate",
"rotateX",
"rotateY",
"rotateZ",
"skew",
"skewX",
"skewY",
];
/**
* A quick lookup for transform props.
*/
const transformProps = new Set(transformPropOrder);
function isForcedMotionValue(key, { layout, layoutId }) {
return (transformProps.has(key) ||
key.startsWith("origin") ||
((layout || layoutId !== undefined) &&
(!!scaleCorrectors[key] || key === "opacity")));
}
const isMotionValue = (value) => Boolean(value && value.getVelocity);
const translateAlias = {
x: "translateX",
y: "translateY",
z: "translateZ",
transformPerspective: "perspective",
};
const numTransforms = transformPropOrder.length;
/**
* 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(transform, { enableHardwareAcceleration = true, allowTransformNone = true, }, transformIsDefault, transformTemplate) {
// The transform string we're going to build into.
let transformString = "";
/**
* Loop over all possible transforms in order, adding the ones that
* are present to the transform string.
*/
for (let i = 0; i < numTransforms; i++) {
const key = transformPropOrder[i];
if (transform[key] !== undefined) {
const transformName = translateAlias[key] || key;
transformString += `${transformName}(${transform[key]}) `;
}
}
if (enableHardwareAcceleration && !transform.z) {
transformString += "translateZ(0)";
}
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;
}
const checkStringStartsWith = (token) => (key) => typeof key === "string" && key.startsWith(token);
const isCSSVariableName = checkStringStartsWith("--");
const isCSSVariableToken = checkStringStartsWith("var(--");
/**
* Provided a value and a ValueType, returns the value as that value type.
*/
const getValueAsType = (value, type) => {
return type && typeof value === "number"
? type.transform(value)
: value;
};
const clamp = (min, max, v) => Math.min(Math.max(v, min), max);
const number = {
test: (v) => typeof v === "number",
parse: parseFloat,
transform: (v) => v,
};
const alpha = {
...number,
transform: (v) => clamp(0, 1, v),
};
const scale = {
...number,
default: 1,
};
/**
* TODO: When we move from string as a source of truth to data models
* everything in this folder should probably be referred to as models vs types
*/
// If this number is a decimal, make it just five decimal places
// to avoid exponents
const sanitize = (v) => Math.round(v * 100000) / 100000;
const floatRegex = /(-)?([\d]*\.?[\d])+/g;
const colorRegex = /(#[0-9a-f]{3,8}|(rgb|hsl)a?\((-?[\d\.]+%?[,\s]+){2}(-?[\d\.]+%?)\s*[\,\/]?\s*[\d\.]*%?\))/gi;
const singleColorRegex = /^(#[0-9a-f]{3,8}|(rgb|hsl)a?\((-?[\d\.]+%?[,\s]+){2}(-?[\d\.]+%?)\s*[\,\/]?\s*[\d\.]*%?\))$/i;
function isString(v) {
return typeof v === "string";
}
const createUnitType = (unit) => ({
test: (v) => isString(v) && v.endsWith(unit) && v.split(" ").length === 1,
parse: parseFloat,
transform: (v) => `${v}${unit}`,
});
const degrees = createUnitType("deg");
const percent = createUnitType("%");
const px = createUnitType("px");
const vh = createUnitType("vh");
const vw = createUnitType("vw");
const progressPercentage = {
...percent,
parse: (v) => percent.parse(v) / 100,
transform: (v) => percent.transform(v * 100),
};
const int = {
...number,
transform: Math.round,
};
const numberValueTypes = {
// Border props
borderWidth: px,
borderTopWidth: px,
borderRightWidth: px,
borderBottomWidth: px,
borderLeftWidth: px,
borderRadius: px,
radius: px,
borderTopLeftRadius: px,
borderTopRightRadius: px,
borderBottomRightRadius: px,
borderBottomLeftRadius: px,
// Positioning props
width: px,
maxWidth: px,
height: px,
maxHeight: px,
size: px,
top: px,
right: px,
bottom: px,
left: px,
// Spacing props
padding: px,
paddingTop: px,
paddingRight: px,
paddingBottom: px,
paddingLeft: px,
margin: px,
marginTop: px,
marginRight: px,
marginBottom: px,
marginLeft: px,
// Transform props
rotate: degrees,
rotateX: degrees,
rotateY: degrees,
rotateZ: degrees,
scale,
scaleX: scale,
scaleY: scale,
scaleZ: scale,
skew: degrees,
skewX: degrees,
skewY: degrees,
distance: px,
translateX: px,
translateY: px,
translateZ: px,
x: px,
y: px,
z: px,
perspective: px,
transformPerspective: px,
opacity: alpha,
originX: progressPercentage,
originY: progressPercentage,
originZ: px,
// Misc
zIndex: int,
// SVG
fillOpacity: alpha,
strokeOpacity: alpha,
numOctaves: int,
};
function buildHTMLStyles(state, latestValues, options, transformTemplate) {
const { style, vars, transform, transformOrigin } = state;
// Track whether we encounter any transform or transformOrigin values.
let hasTransform = false;
let hasTransformOrigin = false;
// Does the calculated transform essentially equal "none"?
let 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 (const key in latestValues) {
const value = latestValues[key];
/**
* If this is a CSS variable we don't do any further processing.
*/
if (isCSSVariableName(key)) {
vars[key] = value;
continue;
}
// Convert the value to its default value type, ie 0 -> "0px"
const valueType = numberValueTypes[key];
const valueAsType = getValueAsType(value, valueType);
if (transformProps.has(key)) {
// If this is a transform, flag to enable further transform processing
hasTransform = true;
transform[key] = valueAsType;
// 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 !== (valueType.default || 0))
transformIsNone = false;
}
else if (key.startsWith("origin")) {
// If this is a transform origin, flag and enable further transform-origin processing
hasTransformOrigin = true;
transformOrigin[key] = valueAsType;
}
else {
style[key] = valueAsType;
}
}
if (!latestValues.transform) {
if (hasTransform || transformTemplate) {
style.transform = buildTransform(state.transform, options, transformIsNone, transformTemplate);
}
else if (style.transform) {
/**
* If we have previously created a transform but currently don't have any,
* reset transform style to none.
*/
style.transform = "none";
}
}
/**
* Build a transformOrigin style. Uses the same defaults as the browser for
* undefined origins.
*/
if (hasTransformOrigin) {
const { originX = "50%", originY = "50%", originZ = 0, } = transformOrigin;
style.transformOrigin = `${originX} ${originY} ${originZ}`;
}
}
const createHtmlRenderState = () => ({
style: {},
transform: {},
transformOrigin: {},
vars: {},
});
function copyRawValuesOnly(target, source, props) {
for (const key in source) {
if (!isMotionValue(source[key]) && !isForcedMotionValue(key, props)) {
target[key] = source[key];
}
}
}
function useInitialMotionValues({ transformTemplate }, visualState, isStatic) {
return React.useMemo(() => {
const state = createHtmlRenderState();
buildHTMLStyles(state, visualState, { enableHardwareAcceleration: !isStatic }, transformTemplate);
return Object.assign({}, state.vars, state.style);
}, [visualState]);
}
function useStyle(props, visualState, isStatic) {
const styleProp = props.style || {};
const style = {};
/**
* Copy non-Motion Values straight into style
*/
copyRawValuesOnly(style, styleProp, props);
Object.assign(style, useInitialMotionValues(props, visualState, isStatic));
return props.transformValues ? props.transformValues(style) : style;
}
function useHTMLProps(props, visualState, isStatic) {
// The `any` isn't ideal but it is the type of createElement props argument
const htmlProps = {};
const style = useStyle(props, visualState, isStatic);
if (props.drag && props.dragListener !== false) {
// 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"}`;
}
if (props.tabIndex === undefined &&
(props.onTap || props.onTapStart || props.whileTap)) {
htmlProps.tabIndex = 0;
}
htmlProps.style = style;
return htmlProps;
}
/**
* A list of all valid MotionProps.
*
* @privateRemarks
* This doesn't throw if a `MotionProp` name is missing - it should.
*/
const validMotionProps = new Set([
"animate",
"exit",
"variants",
"initial",
"style",
"values",
"variants",
"transition",
"transformTemplate",
"transformValues",
"custom",
"inherit",
"onLayoutAnimationStart",
"onLayoutAnimationComplete",
"onLayoutMeasure",
"onBeforeLayoutMeasure",
"onAnimationStart",
"onAnimationComplete",
"onUpdate",
"onDragStart",
"onDrag",
"onDragEnd",
"onMeasureDragConstraints",
"onDirectionLock",
"onDragTransitionEnd",
"_dragX",
"_dragY",
"onHoverStart",
"onHoverEnd",
"onViewportEnter",
"onViewportLeave",
"ignoreStrict",
"viewport",
]);
/**
* 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 (key.startsWith("while") ||
(key.startsWith("drag") && key !== "draggable") ||
key.startsWith("layout") ||
key.startsWith("onTap") ||
key.startsWith("onPan") ||
validMotionProps.has(key));
}
let shouldForward = (key) => !isValidMotionProp(key);
function loadExternalIsValidProp(isValidProp) {
if (!isValidProp)
return;
// Explicitly filter our events
shouldForward = (key) => key.startsWith("on") ? !isValidMotionProp(key) : isValidProp(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 {
/**
* We attempt to import this package but require won't be defined in esm environments, in that case
* isPropValid will have to be provided via `MotionContext`. In a 6.0.0 this should probably be removed
* in favour of explicit injection.
*/
loadExternalIsValidProp(require("@emotion/is-prop-valid").default);
}
catch (_a) {
// We don't need to actually do anything here - the fallback is the existing `isPropValid`.
}
function filterProps(props, isDom, forwardMotionProps) {
const filteredProps = {};
for (const key in props) {
/**
* values is considered a valid prop by Emotion, so if it's present
* this will be rendered out to the DOM unless explicitly filtered.
*
* We check the type as it could be used with the `feColorMatrix`
* element, which we support.
*/
if (key === "values" && typeof props.values === "object")
continue;
if (shouldForward(key) ||
(forwardMotionProps === true && isValidMotionProp(key)) ||
(!isDom && !isValidMotionProp(key)) ||
// If trying to use native HTML drag events, forward drag listeners
(props["draggable"] && key.startsWith("onDrag"))) {
filteredProps[key] = props[key];
}
}
return filteredProps;
}
function calcOrigin$1(origin, offset, size) {
return typeof origin === "string"
? origin
: 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) {
const pxOriginX = calcOrigin$1(originX, dimensions.x, dimensions.width);
const pxOriginY = calcOrigin$1(originY, dimensions.y, dimensions.height);
return `${pxOriginX} ${pxOriginY}`;
}
const dashKeys = {
offset: "stroke-dashoffset",
array: "stroke-dasharray",
};
const 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, length, spacing = 1, offset = 0, useDashCase = true) {
// Normalise path length by setting SVG attribute pathLength to 1
attrs.pathLength = 1;
// We use dash case when setting attributes directly to the DOM node and camel case
// when defining props on a React component.
const keys = useDashCase ? dashKeys : camelKeys;
// Build the dash offset
attrs[keys.offset] = px.transform(-offset);
// Build the dash array
const pathLength = px.transform(length);
const pathSpacing = px.transform(spacing);
attrs[keys.array] = `${pathLength} ${pathSpacing}`;
}
/**
* Build SVG visual attrbutes, like cx and style.transform
*/
function buildSVGAttrs(state, { attrX, attrY, originX, originY, pathLength, pathSpacing = 1, pathOffset = 0,
// This is object creation, which we try to avoid per-frame.
...latest }, options, isSVGTag, transformTemplate) {
buildHTMLStyles(state, latest, options, transformTemplate);
/**
* For svg tags we just want to make sure viewBox is animatable and treat all the styles
* as normal HTML tags.
*/
if (isSVGTag) {
if (state.style.viewBox) {
state.attrs.viewBox = state.style.viewBox;
}
return;
}
state.attrs = state.style;
state.style = {};
const { attrs, style, dimensions } = state;
/**
* 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) {
if (dimensions)
style.transform = attrs.transform;
delete attrs.transform;
}
// Parse transformOrigin
if (dimensions &&
(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 defined
if (pathLength !== undefined) {
buildSVGPath(attrs, pathLength, pathSpacing, pathOffset, false);
}
}
const createSvgRenderState = () => ({
...createHtmlRenderState(),
attrs: {},
});
const isSVGTag = (tag) => typeof tag === "string" && tag.toLowerCase() === "svg";
function useSVGProps(props, visualState, _isStatic, Component) {
const visualProps = React.useMemo(() => {
const state = createSvgRenderState();
buildSVGAttrs(state, visualState, { enableHardwareAcceleration: false }, isSVGTag(Component), props.transformTemplate);
return {
...state.attrs,
style: { ...state.style },
};
}, [visualState]);
if (props.style) {
const rawStyles = {};
copyRawValuesOnly(rawStyles, props.style, props);
visualProps.style = { ...rawStyles, ...visualProps.style };
}
return visualProps;
}
function createUseRender(forwardMotionProps = false) {
const useRender = (Component, props, projectionId, ref, { latestValues }, isStatic) => {
const useVisualProps = isSVGComponent(Component)
? useSVGProps
: useHTMLProps;
const visualProps = useVisualProps(props, latestValues, isStatic, Component);
const filteredProps = filterProps(props, typeof Component === "string", forwardMotionProps);
const elementProps = {
...filteredProps,
...visualProps,
ref,
};
/**
* If component has been handed a motion value as its child,
* memoise its initial value and render that. Subsequent updates
* will be handled by the onChange handler
*/
const { children } = props;
const renderedChildren = React.useMemo(() => (isMotionValue(children) ? children.get() : children), [children]);
if (projectionId) {
elementProps["data-projection-id"] = projectionId;
}
return React.createElement(Component, {
...elementProps,
children: renderedChildren,
});
};
return useRender;
}
/**
* Convert camelCase to dash-case properties.
*/
const camelToDash = (str) => str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
function renderHTML(element, { style, vars }, styleProp, projection) {
Object.assign(element.style, style, projection && projection.getProjectionStyles(styleProp));
// Loop over any CSS variables and assign those.
for (const key in vars) {
element.style.setProperty(key, vars[key]);
}
}
/**
* A set of attribute names that are always read/written as camel case.
*/
const camelCaseAttributes = new Set([
"baseFrequency",
"diffuseConstant",
"kernelMatrix",
"kernelUnitLength",
"keySplines",
"keyTimes",
"limitingConeAngle",
"markerHeight",
"markerWidth",
"numOctaves",
"targetX",
"targetY",
"surfaceScale",
"specularConstant",
"specularExponent",
"stdDeviation",
"tableValues",
"viewBox",
"gradientTransform",
"pathLength",
"startOffset",
"textLength",
"lengthAdjust",
]);
function renderSVG(element, renderState, _styleProp, projection) {
renderHTML(element, renderState, undefined, projection);
for (const key in renderState.attrs) {
element.setAttribute(!camelCaseAttributes.has(key) ? camelToDash(key) : key, renderState.attrs[key]);
}
}
function scrapeMotionValuesFromProps$1(props, prevProps) {
const { style } = props;
const newValues = {};
for (const key in style) {
if (isMotionValue(style[key]) ||
(prevProps.style && isMotionValue(prevProps.style[key])) ||
isForcedMotionValue(key, props)) {
newValues[key] = style[key];
}
}
return newValues;
}
function scrapeMotionValuesFromProps(props, prevProps) {
const newValues = scrapeMotionValuesFromProps$1(props, prevProps);
for (const key in props) {
if (isMotionValue(props[key]) || isMotionValue(prevProps[key])) {
const targetKey = key === "x" || key === "y" ? "attr" + key.toUpperCase() : key;
newValues[targetKey] = props[key];
}
}
return newValues;
}
function resolveVariantFromProps(props, definition, custom, currentValues = {}, currentVelocity = {}) {
/**
* If the variant definition is a function, resolve.
*/
if (typeof definition === "function") {
definition = definition(custom !== undefined ? custom : props.custom, currentValues, currentVelocity);
}
/**
* If the variant definition is a variant label, or
* the function returned a variant label, resolve.
*/
if (typeof definition === "string") {
definition = props.variants && props.variants[definition];
}
/**
* At this point we've resolved both functions and variant labels,
* but the resolved variant label might itself have been a function.
* If so, resolve. This can only have returned a valid target object.
*/
if (typeof definition === "function") {
definition = definition(custom !== undefined ? custom : props.custom, currentValues, currentVelocity);
}
return definition;
}
const isKeyframesTarget = (v) => {
return Array.isArray(v);
};
const isCustomValue = (v) => {
return Boolean(v && typeof v === "object" && v.mix && v.toValue);
};
const resolveFinalValueInKeyframes = (v) => {
// TODO maybe throw if v.length - 1 is placeholder token?
return isKeyframesTarget(v) ? v[v.length - 1] || 0 : v;
};
/**
* If the provided value is a MotionValue, this returns the actual value, otherwise just the value itself
*
* TODO: Remove and move to library
*/
function resolveMotionValue(value) {
const unwrappedValue = isMotionValue(value) ? value.get() : value;
return isCustomValue(unwrappedValue)
? unwrappedValue.toValue()
: unwrappedValue;
}
function makeState({ scrapeMotionValuesFromProps, createRenderState, onMount, }, props, context, presenceContext) {
const state = {
latestValues: makeLatestValues(props, context, presenceContext, scrapeMotionValuesFromProps),
renderState: createRenderState(),
};
if (onMount) {
state.mount = (instance) => onMount(props, instance, state);
}
return state;
}
const makeUseVisualState = (config) => (props, isStatic) => {
const context = React.useContext(MotionContext);
const presenceContext = React.useContext(PresenceContext);
const make = () => makeState(config, props, context, presenceContext);
return isStatic ? make() : useConstant(make);
};
function makeLatestValues(props, context, presenceContext, scrapeMotionValues) {
const values = {};
const motionValues = scrapeMotionValues(props, {});
for (const key in motionValues) {
values[key] = resolveMotionValue(motionValues[key]);
}
let { initial, animate } = props;
const isControllingVariants$1 = isControllingVariants(props);
const isVariantNode$1 = isVariantNode(props);
if (context &&
isVariantNode$1 &&
!isControllingVariants$1 &&
props.inherit !== false) {
if (initial === undefined)
initial = context.initial;
if (animate === undefined)
animate = context.animate;
}
let isInitialAnimationBlocked = presenceContext
? presenceContext.initial === false
: false;
isInitialAnimationBlocked = isInitialAnimationBlocked || initial === false;
const variantToSet = isInitialAnimationBlocked ? animate : initial;
if (variantToSet &&
typeof variantToSet !== "boolean" &&
!isAnimationControls(variantToSet)) {
const list = Array.isArray(variantToSet) ? variantToSet : [variantToSet];
list.forEach((definition) => {
const resolved = resolveVariantFromProps(props, definition);
if (!resolved)
return;
const { transitionEnd, transition, ...target } = resolved;
for (const key in target) {
let valueTarget = target[key];
if (Array.isArray(valueTarget)) {
/**
* Take final keyframe if the initial animation is blocked because
* we want to initialise at the end of that blocked animation.
*/
const index = isInitialAnimationBlocked
? valueTarget.length - 1
: 0;
valueTarget = valueTarget[index];
}
if (valueTarget !== null) {
values[key] = valueTarget;
}
}
for (const key in transitionEnd)
values[key] = transitionEnd[key];
});
}
return values;
}
const svgMotionConfig = {
useVisualState: makeUseVisualState({
scrapeMotionValuesFromProps: scrapeMotionValuesFromProps,
createRenderState: createSvgRenderState,
onMount: (props, instance, { renderState, latestValues }) => {
try {
renderState.dimensions =
typeof instance.getBBox ===
"function"
? instance.getBBox()
: instance.getBoundingClientRect();
}
catch (e) {
// Most likely trying to measure an unrendered element under Firefox
renderState.dimensions = {
x: 0,
y: 0,
width: 0,
height: 0,
};
}
buildSVGAttrs(renderState, latestValues, { enableHardwareAcceleration: false }, isSVGTag(instance.tagName), props.transformTemplate);
renderSVG(instance, renderState);
},
}),
};
const htmlMotionConfig = {
useVisualState: makeUseVisualState({
scrapeMotionValuesFromProps: scrapeMotionValuesFromProps$1,
createRenderState: createHtmlRenderState,
}),
};
function createDomMotionConfig(Component, { forwardMotionProps = false }, preloadedFeatures, createVisualElement) {
const baseConfig = isSVGComponent(Component)
? svgMotionConfig
: htmlMotionConfig;
return {
...baseConfig,
preloadedFeatures,
useRender: createUseRender(forwardMotionProps),
createVisualElement,
Component,
};
}
function addDomEvent(target, eventName, handler, options = { passive: true }) {
target.addEventListener(eventName, handler, options);
return () => target.removeEventListener(eventName, handler);
}
const isPrimaryPointer = (event) => {
if (event.pointerType === "mouse") {
return typeof event.button !== "number" || event.button <= 0;
}
else {
/**
* isPrimary is true for all mice buttons, whereas every touch point
* is regarded as its own input. So subsequent concurrent touch points
* will be false.
*
* Specifically match against false here as incomplete versions of
* PointerEvents in very old browser might have it set as undefined.
*/
return event.isPrimary !== false;
}
};
function extractEventInfo(event, pointType = "page") {
return {
point: {
x: event[pointType + "X"],
y: event[pointType + "Y"],
},
};
}
const addPointerInfo = (handler) => {
return (event) => isPrimaryPointer(event) && handler(event, extractEventInfo(event));
};
function addPointerEvent(target, eventName, handler, options) {
return addDomEvent(target, eventName, addPointerInfo(handler), options);
}
/**
* Pipe
* Compose other transformers to run linearily
* pipe(min(20), max(40))
* @param {...functions} transformers
* @return {function}
*/
const combineFunctions = (a, b) => (v) => b(a(v));
const pipe = (...transformers) => transformers.reduce(combineFunctions);
function createLock(name) {
let lock = null;
return () => {
const openLock = () => {
lock = null;
};
if (lock === null) {
lock = name;
return openLock;
}
return false;
};
}
const globalHorizontalLock = createLock("dragHorizontal");
const globalVerticalLock = createLock("dragVertical");
function getGlobalLock(drag) {
let lock = false;
if (drag === "y") {
lock = globalVerticalLock();
}
else if (drag === "x") {
lock = globalHorizontalLock();
}
else {
const openHorizontal = globalHorizontalLock();
const openVertical = globalVerticalLock();
if (openHorizontal && openVertical) {
lock = () => {
openHorizontal();
openVertical();
};
}
else {
// Release the locks because we don't use them
if (openHorizontal)
openHorizontal();
if (openVertical)
openVertical();
}
}
return lock;
}
function isDragActive() {
// Check the gesture lock - if we get it, it means no drag gesture is active
// and we can safely fire the tap gesture.
const openGestureLock = getGlobalLock(true);
if (!openGestureLock)
return true;
openGestureLock();
return false;
}
class Feature {
constructor(node) {
this.isMounted = false;
this.node = node;
}
update() { }
}
function createRenderStep(runNextFrame) {
/**
* We create and reuse two arrays, one to queue jobs for the current frame
* and one for the next. We reuse to avoid triggering GC after x frames.
*/
let toRun = [];
let toRunNextFrame = [];
/**
*
*/
let numToRun = 0;
/**
* Track whether we're currently processing jobs in this step. This way
* we can decide whether to schedule new jobs for this frame or next.
*/
let isProcessing = false;
let flushNextFrame = false;
/**
* A set of processes which were marked keepAlive when scheduled.
*/
const toKeepAlive = new WeakSet();
const step = {
/**
* Schedule a process to run on the next frame.
*/
schedule: (callback, keepAlive = false, immediate = false) => {
const addToCurrentFrame = immediate && isProcessing;
const buffer = addToCurrentFrame ? toRun : toRunNextFrame;
if (keepAlive)
toKeepAlive.add(callback);
// If the buffer doesn't already contain this callback, add it
if (buffer.indexOf(callback) === -1) {
buffer.push(callback);
// If we're adding it to the currently running buffer, update its measured size
if (addToCurrentFrame && isProcessing)
numToRun = toRun.length;
}
return callback;
},
/**
* Cancel the provided callback from running on the next frame.
*/
cancel: (callback) => {
const index = toRunNextFrame.indexOf(callback);
if (index !== -1)
toRunNextFrame.splice(index, 1);
toKeepAlive.delete(callback);
},
/**
* Execute all schedule callbacks.
*/
process: (frameData) => {
/**
* If we're already processing we've probably been triggered by a flushSync
* inside an existing process. Instead of executing, mark flushNextFrame
* as true and ensure we flush the following frame at the end of this one.
*/
if (isProcessing) {
flushNextFrame = true;
return;
}
isProcessing = true;
[toRun, toRunNextFrame] = [toRunNextFrame, toRun];
// Clear the next frame list
toRunNextFrame.length = 0;
// Execute this frame
numToRun = toRun.length;
if (numToRun) {
for (let i = 0; i < numToRun; i++) {
const callback = toRun[i];
callback(frameData);
if (toKeepAlive.has(callback)) {
step.schedule(callback);
runNextFrame();
}
}
}
isProcessing = false;
if (flushNextFrame) {
flushNextFrame = false;
step.process(frameData);
}
},
};
return step;
}
const frameData = {
delta: 0,
timestamp: 0,
};
const maxElapsed = 40;
let useDefaultElapsed = true;
let runNextFrame = false;
let isProcessing = false;
const stepsOrder = [
"read",
"update",
"preRender",
"render",
"postRender",
];
const steps = stepsOrder.reduce((acc, key) => {
acc[key] = createRenderStep(() => (runNextFrame = true));
return acc;
}, {});
const sync = stepsOrder.reduce((acc, key) => {
const step = steps[key];
acc[key] = (process, keepAlive = false, immediate = false) => {
if (!runNextFrame)
startLoop();
return step.schedule(process, keepAlive, immediate);
};
return acc;
}, {});
const cancelSync = stepsOrder.reduce((acc, key) => {
acc[key] = steps[key].cancel;
return acc;
}, {});
const flushSync = stepsOrder.reduce((acc, key) => {
acc[key] = () => steps[key].process(frameData);
return acc;
}, {});
const processStep = (stepId) => steps[stepId