svelte-motion
Version:
Svelte animation library based on the React library framer-motion.
699 lines (696 loc) • 33.8 kB
JavaScript
/**
based on framer-motion@4.1.1,
Copyright (c) 2018 Framer B.V.
*/
import {fixed} from '../utils/fix-process-env';
import { __spreadArray, __read } from 'tslib';
import sync, { cancelSync } from 'framesync';
import { pipe } from 'popmotion';
import { Presence } from '../components/AnimateSharedLayout/types.js';
import { eachAxis } from '../utils/each-axis.js';
import { axisBox } from '../utils/geometry/index.js';
import { removeBoxTransforms, applyBoxTransforms } from '../utils/geometry/delta-apply.js';
import { calcRelativeBox, updateBoxDelta } from '../utils/geometry/delta-calc.js';
import { motionValue } from '../value/index.js';
import { isMotionValue } from '../value/utils/is-motion-value.js';
import { buildLayoutProjectionTransform } from './html/utils/build-projection-transform.js';
import { variantPriorityOrder } from './utils/animation-state.js';
import { createLifecycles } from './utils/lifecycles.js';
import { updateMotionValuesFromProps } from './utils/motion-values.js';
import { updateLayoutDeltas } from './utils/projection.js';
import { createLayoutState, createProjectionState } from './utils/state.js';
import { FlatTree } from './utils/flat-tree.js';
import { checkIfControllingVariants, checkIfVariantNode, isVariantLabel } from './utils/variants.js';
import { setCurrentViewportBox } from './dom/projection/relative-set.js';
import { isDraggable } from './utils/is-draggable.js';
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, resetTransform = _a.resetTransform, restoreTransform = _a.restoreTransform, 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;
if (options === void 0) { options = {}; }
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 and onViewportBoxUpdate.
*/
var lifecycles = createLifecycles();
/**
*
*/
var projection = createProjectionState();
/**
* A reference to the nearest projecting parent. This is either
* undefined if we haven't looked for the nearest projecting parent,
* false if there is no parent performing layout projection, or a reference
* to the projecting parent.
*/
var projectionParent;
/**
* 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();
/**
*
*/
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 = Object.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;
/**
*
*/
function render() {
if (!instance)
return;
if (element.isProjectionReady()) {
/**
* 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()) {
var crossfadedValues = crossfader.getCrossfadeState(element);
if (crossfadedValues)
valuesToRender = crossfadedValues;
}
build(element, renderState, valuesToRender, leadProjection, layoutState, options, props);
}
function update() {
lifecycles.notifyUpdate(latestValues);
}
function updateLayoutProjection() {
if (!element.isProjectionReady())
return;
var delta = layoutState.delta, treeScale = layoutState.treeScale;
var prevTreeScaleX = treeScale.x;
var prevTreeScaleY = treeScale.y;
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 updateTreeLayoutProjection() {
element.layoutTree.forEach(fireUpdateLayoutProjection);
}
/**
*
*/
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 = Object.assign(Object.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(),
/**
* An ancestor path back to the root visual element. This is used
* by layout projection to quickly recurse back up the tree.
*/
path: parent ? __spreadArray(__spreadArray([], __read(parent.path)), [parent]) : [], layoutTree: parent ? parent.layoutTree : new FlatTree(),
/**
*
*/
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,
/**
* 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) {
instance = element.current = newInstance;
element.pointTo(element);
if (isVariantNode && parent && !isControllingVariants) {
removeFromVariantTree = parent === null || parent === void 0 ? void 0 : parent.addVariantChild(element);
}
parent === null || parent === void 0 ? void 0 : parent.children.add(element);
},
/**
*
*/
unmount: function () {
cancelSync.update(update);
cancelSync.render(render);
cancelSync.preRender(element.updateLayoutProjection);
valueSubscriptions.forEach(function (remove) { return remove(); });
element.stopLayoutAnimation();
element.layoutTree.remove(element);
removeFromVariantTree === null || removeFromVariantTree === void 0 ? void 0 : removeFromVariantTree();
parent === null || parent === void 0 ? void 0 : parent.children.delete(element);
unsubscribeFromLeadVisualElement === null || unsubscribeFromLeadVisualElement === void 0 ? void 0 : unsubscribeFromLeadVisualElement();
lifecycles.clearAllListeners();
},
/**
* 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.preRender(element.updateTreeLayoutProjection, 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; },
/**
* 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);
},
// 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) {
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;
element.layoutTree.add(element);
},
/**
* 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;
}, getLayoutState: function () { return layoutState; }, setCrossfader: function (newCrossfader) {
crossfader = newCrossfader;
}, isProjectionReady: function () {
return projection.isEnabled &&
projection.isHydrated &&
layoutState.isHydrated;
},
/**
* Start a layout animation on a given axis.
*/
startLayoutAnimation: function (axis, transition, isRelative) {
if (isRelative === void 0) { isRelative = false; }
var progress = element.getProjectionAnimationProgress()[axis];
var _a = isRelative
? projection.relativeTarget[axis]
: 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) {
element.setProjectionTargetAxis(axis, v, v + length, isRelative);
});
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;
},
/**
* 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, isRelative) {
if (isRelative === void 0) { isRelative = false; }
var target;
if (isRelative) {
if (!projection.relativeTarget) {
projection.relativeTarget = axisBox();
}
target = projection.relativeTarget[axis];
}
else {
projection.relativeTarget = undefined;
target = projection.target[axis];
}
projection.isHydrated = true;
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.relativeTarget &&
!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) {
setCurrentViewportBox(element);
element.notifyLayoutUpdate(layoutState.layout, element.prevViewportBox || layoutState.layout, config);
},
/**
* Temporarily reset the transform of the instance.
*/
resetTransform: function () { return resetTransform(element, instance, props); }, restoreTransform: function () { return restoreTransform(instance, renderState); }, updateLayoutProjection: updateLayoutProjection,
updateTreeLayoutProjection: function () {
element.layoutTree.forEach(fireResolveRelativeTargetBox);
/**
* Schedule the projection updates at the end of the current preRender
* step. This will ensure that all layout trees will first resolve
* relative projection boxes into viewport boxes, and *then*
* update projections.
*/
sync.preRender(updateTreeLayoutProjection, false, true);
// sync.postRender(() => element.scheduleUpdateLayoutProjection())
},
getProjectionParent: function () {
if (projectionParent === undefined) {
var foundParent = false;
// Search backwards through the tree path
for (var i = element.path.length - 1; i >= 0; i--) {
var ancestor = element.path[i];
if (ancestor.projection.isEnabled) {
foundParent = ancestor;
break;
}
}
projectionParent = foundParent;
}
return projectionParent;
},
resolveRelativeTargetBox: function () {
var relativeParent = element.getProjectionParent();
if (!projection.relativeTarget || !relativeParent)
return;
calcRelativeBox(projection, relativeParent.projection);
if (isDraggable(relativeParent)) {
var target = projection.target;
applyBoxTransforms(target, target, relativeParent.getLatestValues());
}
},
shouldResetTransform: function () {
return Boolean(props._layoutResetTransform);
},
/**
*
*/
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 = 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 fireResolveRelativeTargetBox(child) {
child.resolveRelativeTargetBox();
}
function fireUpdateLayoutProjection(child) {
child.updateLayoutProjection();
}
var variantProps = __spreadArray(["initial"], __read(variantPriorityOrder));
var numVariantProps = variantProps.length;
export { visualElement };