UNPKG

framer-motion

Version:

A simple and powerful React animation library

402 lines (399 loc) • 19.1 kB
import { __assign, __spreadArray, __read } from 'tslib'; import sync, { cancelSync } from 'framesync'; import { motionValue } from '../value/index.mjs'; import { isMotionValue } from '../value/utils/is-motion-value.mjs'; import { variantPriorityOrder } from './utils/animation-state.mjs'; import { createLifecycles } from './utils/lifecycles.mjs'; import { updateMotionValuesFromProps } from './utils/motion-values.mjs'; import { checkIfControllingVariants, checkIfVariantNode, isVariantLabel } from './utils/variants.mjs'; var visualElement = function (_a) { var _b = _a.treeType, treeType = _b === void 0 ? "" : _b, build = _a.build, getBaseTarget = _a.getBaseTarget, makeTargetAnimatable = _a.makeTargetAnimatable, measureViewportBox = _a.measureViewportBox, renderInstance = _a.render, readValueFromInstance = _a.readValueFromInstance, removeValueFromRenderState = _a.removeValueFromRenderState, sortNodePosition = _a.sortNodePosition, scrapeMotionValuesFromProps = _a.scrapeMotionValuesFromProps; return function (_a, options) { var parent = _a.parent, props = _a.props, presenceId = _a.presenceId, blockInitialAnimation = _a.blockInitialAnimation, visualState = _a.visualState, shouldReduceMotion = _a.shouldReduceMotion; if (options === void 0) { options = {}; } var isMounted = false; var latestValues = visualState.latestValues, renderState = visualState.renderState; /** * 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; /** * Manages the subscriptions for a visual element's lifecycle, for instance * onRender */ var lifecycles = createLifecycles(); /** * 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 = {}; /** * 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 = __assign({}, latestValues); // Internal methods ======================== /** * On mount, this will be hydrated with a callback to disconnect * this visual element from its parent on unmount. */ var removeFromVariantTree; /** * Render the element with the latest styles outside of the React * render lifecycle */ function render() { if (!instance || !isMounted) return; triggerBuild(); renderInstance(instance, renderState, props.style, element.projection); } function triggerBuild() { build(element, renderState, latestValues, options, props); } function update() { lifecycles.notifyUpdate(latestValues); } /** * */ function bindToMotionValue(key, value) { var removeOnChange = value.onChange(function (latestValue) { latestValues[key] = latestValue; props.onUpdate && sync.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 isVariantNode = checkIfVariantNode(props); var element = __assign(__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, parent: parent, children: new Set(), /** * */ presenceId: presenceId, shouldReduceMotion: shouldReduceMotion, /** * 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 the projection tree, 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, /** * 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); }, mount: function (newInstance) { isMounted = true; instance = element.current = newInstance; if (element.projection) { element.projection.mount(newInstance); } if (isVariantNode && parent && !isControllingVariants) { removeFromVariantTree = parent === null || parent === void 0 ? void 0 : parent.addVariantChild(element); } values.forEach(function (value, key) { return bindToMotionValue(key, value); }); parent === null || parent === void 0 ? void 0 : parent.children.add(element); element.setProps(props); }, /** * */ unmount: function () { var _a; (_a = element.projection) === null || _a === void 0 ? void 0 : _a.unmount(); cancelSync.update(update); cancelSync.render(render); valueSubscriptions.forEach(function (remove) { return remove(); }); removeFromVariantTree === null || removeFromVariantTree === void 0 ? void 0 : removeFromVariantTree(); parent === null || parent === void 0 ? void 0 : parent.children.delete(element); lifecycles.clearAllListeners(); instance = undefined; isMounted = false; }, /** * 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(); }, /** * 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; }, /** * 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); }, /** * 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 () { return measureViewportBox(instance, props); }, // 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]; removeValueFromRenderState(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), { /** * 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.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) { if (newProps.transformTemplate || props.transformTemplate) { element.scheduleRender(); } 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; }, getTransformPagePoint: function () { return props.transformPagePoint; }, /** * 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; } }); return element; }; }; var variantProps = __spreadArray(["initial"], __read(variantPriorityOrder), false); var numVariantProps = variantProps.length; export { visualElement };