framer-motion
Version:
A simple and powerful React animation library
1,024 lines (1,022 loc) • 57 kB
JavaScript
import { __spreadArray, __read, __assign } from 'tslib';
import sync, { cancelSync, flushSync } from 'framesync';
import { mix } from 'popmotion';
import { animate } from '../../animation/animate.mjs';
import { SubscriptionManager } from '../../utils/subscription-manager.mjs';
import { mixValues } from '../animation/mix-values.mjs';
import { copyBoxInto } from '../geometry/copy.mjs';
import { translateAxis, transformBox, applyBoxDelta, applyTreeDeltas } from '../geometry/delta-apply.mjs';
import { calcRelativePosition, calcRelativeBox, calcBoxDelta, calcLength } from '../geometry/delta-calc.mjs';
import { removeBoxTransforms } from '../geometry/delta-remove.mjs';
import { createBox, createDelta } from '../geometry/models.mjs';
import { getValueTransition } from '../../animation/utils/transitions.mjs';
import { boxEquals, isDeltaZero } from '../geometry/utils.mjs';
import { NodeStack } from '../shared/stack.mjs';
import { scaleCorrectors } from '../styles/scale-correction.mjs';
import { buildProjectionTransform } from '../styles/transform.mjs';
import { eachAxis } from '../utils/each-axis.mjs';
import { hasTransform, hasScale } from '../utils/has-transform.mjs';
import { transformAxes } from '../../render/html/utils/transform.mjs';
import { FlatTree } from '../../render/utils/flat-tree.mjs';
import { resolveMotionValue } from '../../value/utils/resolve-motion-value.mjs';
/**
* We use 1000 as the animation target as 0-1000 maps better to pixels than 0-1
* which has a noticeable difference in spring animations
*/
var animationTarget = 1000;
/**
* 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.
*/
var 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,
};
function createProjectionNode(_a) {
var attachResizeListener = _a.attachResizeListener, defaultParent = _a.defaultParent, measureScroll = _a.measureScroll, resetTransform = _a.resetTransform;
return /** @class */ (function () {
function ProjectionNode(id, latestValues, parent) {
var _this = this;
if (latestValues === void 0) { latestValues = {}; }
if (parent === void 0) { parent = defaultParent === null || defaultParent === void 0 ? void 0 : defaultParent(); }
/**
* A Set containing all this component's children. This is used to iterate
* through the children.
*
* TODO: This could be faster to iterate as a flat array stored on the root node.
*/
this.children = new Set();
/**
* Options for the node. We use this to configure what kind of layout animations
* we should perform (if any).
*/
this.options = {};
/**
* We use this to detect when its safe to shut down part of a projection tree.
* We have to keep projecting children for scale correction and relative projection
* until all their parents stop performing layout animations.
*/
this.isTreeAnimating = false;
this.isAnimationBlocked = false;
/**
* Flag to true if we think this layout has been changed. We can't always know this,
* currently we set it to true every time a component renders, or if it has a layoutDependency
* if that has changed between renders. Additionally, components can be grouped by LayoutGroup
* and if one node is dirtied, they all are.
*/
this.isLayoutDirty = false;
/**
* Block layout updates for instant layout transitions throughout the tree.
*/
this.updateManuallyBlocked = false;
this.updateBlockedByResize = false;
/**
* Set to true between the start of the first `willUpdate` call and the end of the `didUpdate`
* call.
*/
this.isUpdating = false;
/**
* If this is an SVG element we currently disable projection transforms
*/
this.isSVG = false;
/**
* Flag to true (during promotion) if a node doing an instant layout transition needs to reset
* its projection styles.
*/
this.needsReset = false;
/**
* Flags whether this node should have its transform reset prior to measuring.
*/
this.shouldResetTransform = false;
/**
* An object representing the calculated contextual/accumulated/tree scale.
* This will be used to scale calculcated projection transforms, as these are
* calculated in screen-space but need to be scaled for elements to actually
* make it to their calculated destinations.
*
* TODO: Lazy-init
*/
this.treeScale = { x: 1, y: 1 };
/**
*
*/
this.eventHandlers = new Map();
// Note: Currently only running on root node
this.potentialNodes = new Map();
this.checkUpdateFailed = function () {
if (_this.isUpdating) {
_this.isUpdating = false;
_this.clearAllSnapshots();
}
};
this.updateProjection = function () {
_this.nodes.forEach(resolveTargetDelta);
_this.nodes.forEach(calcProjection);
};
this.hasProjected = false;
this.isVisible = true;
this.animationProgress = 0;
/**
* Shared layout
*/
// TODO Only running on root node
this.sharedNodes = new Map();
this.id = id;
this.latestValues = latestValues;
this.root = parent ? parent.root || parent : this;
this.path = parent ? __spreadArray(__spreadArray([], __read(parent.path), false), [parent], false) : [];
this.parent = parent;
this.depth = parent ? parent.depth + 1 : 0;
id && this.root.registerPotentialNode(id, this);
for (var i = 0; i < this.path.length; i++) {
this.path[i].shouldResetTransform = true;
}
if (this.root === this)
this.nodes = new FlatTree();
}
ProjectionNode.prototype.addEventListener = function (name, handler) {
if (!this.eventHandlers.has(name)) {
this.eventHandlers.set(name, new SubscriptionManager());
}
return this.eventHandlers.get(name).add(handler);
};
ProjectionNode.prototype.notifyListeners = function (name) {
var args = [];
for (var _i = 1; _i < arguments.length; _i++) {
args[_i - 1] = arguments[_i];
}
var subscriptionManager = this.eventHandlers.get(name);
subscriptionManager === null || subscriptionManager === void 0 ? void 0 : subscriptionManager.notify.apply(subscriptionManager, __spreadArray([], __read(args), false));
};
ProjectionNode.prototype.hasListeners = function (name) {
return this.eventHandlers.has(name);
};
ProjectionNode.prototype.registerPotentialNode = function (id, node) {
this.potentialNodes.set(id, node);
};
/**
* Lifecycles
*/
ProjectionNode.prototype.mount = function (instance, isLayoutDirty) {
var _this = this;
var _a;
if (isLayoutDirty === void 0) { isLayoutDirty = false; }
if (this.instance)
return;
this.isSVG =
instance instanceof SVGElement && instance.tagName !== "svg";
this.instance = instance;
var _b = this.options, layoutId = _b.layoutId, layout = _b.layout, visualElement = _b.visualElement;
if (visualElement && !visualElement.getInstance()) {
visualElement.mount(instance);
}
this.root.nodes.add(this);
(_a = this.parent) === null || _a === void 0 ? void 0 : _a.children.add(this);
this.id && this.root.potentialNodes.delete(this.id);
if (isLayoutDirty && (layout || layoutId)) {
this.isLayoutDirty = true;
}
if (attachResizeListener) {
var unblockTimeout_1;
var resizeUnblockUpdate_1 = function () {
return (_this.root.updateBlockedByResize = false);
};
attachResizeListener(instance, function () {
_this.root.updateBlockedByResize = true;
clearTimeout(unblockTimeout_1);
unblockTimeout_1 = window.setTimeout(resizeUnblockUpdate_1, 250);
if (globalProjectionState.hasAnimatedSinceResize) {
globalProjectionState.hasAnimatedSinceResize = false;
_this.nodes.forEach(finishAnimation);
}
});
}
if (layoutId) {
this.root.registerSharedNode(layoutId, this);
}
// Only register the handler if it requires layout animation
if (this.options.animate !== false &&
visualElement &&
(layoutId || layout)) {
this.addEventListener("didUpdate", function (_a) {
var _b, _c, _d, _e, _f;
var delta = _a.delta, hasLayoutChanged = _a.hasLayoutChanged, hasRelativeTargetChanged = _a.hasRelativeTargetChanged, newLayout = _a.layout;
if (_this.isTreeAnimationBlocked()) {
_this.target = undefined;
_this.relativeTarget = undefined;
return;
}
// TODO: Check here if an animation exists
var layoutTransition = (_c = (_b = _this.options.transition) !== null && _b !== void 0 ? _b : visualElement.getDefaultTransition()) !== null && _c !== void 0 ? _c : defaultLayoutTransition;
var _g = visualElement.getProps(), onLayoutAnimationStart = _g.onLayoutAnimationStart, onLayoutAnimationComplete = _g.onLayoutAnimationComplete;
/**
* The target layout of the element might stay the same,
* but its position relative to its parent has changed.
*/
var targetChanged = !_this.targetLayout ||
!boxEquals(_this.targetLayout, newLayout) ||
hasRelativeTargetChanged;
/**
* If the layout hasn't seemed to have changed, it might be that the
* element is visually in the same place in the document but its position
* relative to its parent has indeed changed. So here we check for that.
*/
var hasOnlyRelativeTargetChanged = !hasLayoutChanged && hasRelativeTargetChanged;
if (((_d = _this.resumeFrom) === null || _d === void 0 ? void 0 : _d.instance) ||
hasOnlyRelativeTargetChanged ||
(hasLayoutChanged &&
(targetChanged || !_this.currentAnimation))) {
if (_this.resumeFrom) {
_this.resumingFrom = _this.resumeFrom;
_this.resumingFrom.resumingFrom = undefined;
}
_this.setAnimationOrigin(delta, hasOnlyRelativeTargetChanged);
var animationOptions = __assign(__assign({}, getValueTransition(layoutTransition, "layout")), { onPlay: onLayoutAnimationStart, onComplete: onLayoutAnimationComplete });
if (visualElement.shouldReduceMotion) {
animationOptions.delay = 0;
animationOptions.type = false;
}
_this.startAnimation(animationOptions);
}
else {
/**
* If the layout hasn't changed and we have an animation that hasn't started yet,
* finish it immediately. Otherwise it will be animating from a location
* that was probably never commited to screen and look like a jumpy box.
*/
if (!hasLayoutChanged &&
_this.animationProgress === 0) {
_this.finishAnimation();
}
_this.isLead() && ((_f = (_e = _this.options).onExitComplete) === null || _f === void 0 ? void 0 : _f.call(_e));
}
_this.targetLayout = newLayout;
});
}
};
ProjectionNode.prototype.unmount = function () {
var _a, _b;
this.options.layoutId && this.willUpdate();
this.root.nodes.remove(this);
(_a = this.getStack()) === null || _a === void 0 ? void 0 : _a.remove(this);
(_b = this.parent) === null || _b === void 0 ? void 0 : _b.children.delete(this);
this.instance = undefined;
cancelSync.preRender(this.updateProjection);
};
// only on the root
ProjectionNode.prototype.blockUpdate = function () {
this.updateManuallyBlocked = true;
};
ProjectionNode.prototype.unblockUpdate = function () {
this.updateManuallyBlocked = false;
};
ProjectionNode.prototype.isUpdateBlocked = function () {
return this.updateManuallyBlocked || this.updateBlockedByResize;
};
ProjectionNode.prototype.isTreeAnimationBlocked = function () {
var _a;
return (this.isAnimationBlocked ||
((_a = this.parent) === null || _a === void 0 ? void 0 : _a.isTreeAnimationBlocked()) ||
false);
};
// Note: currently only running on root node
ProjectionNode.prototype.startUpdate = function () {
var _a;
if (this.isUpdateBlocked())
return;
this.isUpdating = true;
(_a = this.nodes) === null || _a === void 0 ? void 0 : _a.forEach(resetRotation);
};
ProjectionNode.prototype.willUpdate = function (shouldNotifyListeners) {
var _a, _b, _c;
if (shouldNotifyListeners === void 0) { shouldNotifyListeners = true; }
if (this.root.isUpdateBlocked()) {
(_b = (_a = this.options).onExitComplete) === null || _b === void 0 ? void 0 : _b.call(_a);
return;
}
!this.root.isUpdating && this.root.startUpdate();
if (this.isLayoutDirty)
return;
this.isLayoutDirty = true;
for (var i = 0; i < this.path.length; i++) {
var node = this.path[i];
node.shouldResetTransform = true;
/**
* TODO: Check we haven't updated the scroll
* since the last didUpdate
*/
node.updateScroll();
}
var _d = this.options, layoutId = _d.layoutId, layout = _d.layout;
if (layoutId === undefined && !layout)
return;
var transformTemplate = (_c = this.options.visualElement) === null || _c === void 0 ? void 0 : _c.getProps().transformTemplate;
this.prevTransformTemplateValue = transformTemplate === null || transformTemplate === void 0 ? void 0 : transformTemplate(this.latestValues, "");
this.updateSnapshot();
shouldNotifyListeners && this.notifyListeners("willUpdate");
};
// Note: Currently only running on root node
ProjectionNode.prototype.didUpdate = function () {
var updateWasBlocked = this.isUpdateBlocked();
// When doing an instant transition, we skip the layout update,
// but should still clean up the measurements so that the next
// snapshot could be taken correctly.
if (updateWasBlocked) {
this.unblockUpdate();
this.clearAllSnapshots();
this.nodes.forEach(clearMeasurements);
return;
}
if (!this.isUpdating)
return;
this.isUpdating = false;
/**
* Search for and mount newly-added projection elements.
*
* TODO: Every time a new component is rendered we could search up the tree for
* the closest mounted node and query from there rather than document.
*/
if (this.potentialNodes.size) {
this.potentialNodes.forEach(mountNodeEarly);
this.potentialNodes.clear();
}
/**
* Write
*/
this.nodes.forEach(resetTransformStyle);
/**
* Read ==================
*/
// Update layout measurements of updated children
this.nodes.forEach(updateLayout);
/**
* Write
*/
// Notify listeners that the layout is updated
this.nodes.forEach(notifyLayoutUpdate);
this.clearAllSnapshots();
// Flush any scheduled updates
flushSync.update();
flushSync.preRender();
flushSync.render();
};
ProjectionNode.prototype.clearAllSnapshots = function () {
this.nodes.forEach(clearSnapshot);
this.sharedNodes.forEach(removeLeadSnapshots);
};
ProjectionNode.prototype.scheduleUpdateProjection = function () {
sync.preRender(this.updateProjection, false, true);
};
ProjectionNode.prototype.scheduleCheckAfterUnmount = function () {
var _this = this;
/**
* If the unmounting node is in a layoutGroup and did trigger a willUpdate,
* we manually call didUpdate to give a chance to the siblings to animate.
* Otherwise, cleanup all snapshots to prevents future nodes from reusing them.
*/
sync.postRender(function () {
if (_this.isLayoutDirty) {
_this.root.didUpdate();
}
else {
_this.root.checkUpdateFailed();
}
});
};
/**
* Update measurements
*/
ProjectionNode.prototype.updateSnapshot = function () {
if (this.snapshot || !this.instance)
return;
var measured = this.measure();
var layout = this.removeTransform(this.removeElementScroll(measured));
roundBox(layout);
this.snapshot = {
measured: measured,
layout: layout,
latestValues: {},
};
};
ProjectionNode.prototype.updateLayout = function () {
var _a;
if (!this.instance)
return;
// TODO: Incorporate into a forwarded scroll offset
this.updateScroll();
if (!(this.options.alwaysMeasureLayout && this.isLead()) &&
!this.isLayoutDirty) {
return;
}
/**
* When a node is mounted, it simply resumes from the prevLead's
* snapshot instead of taking a new one, but the ancestors scroll
* might have updated while the prevLead is unmounted. We need to
* update the scroll again to make sure the layout we measure is
* up to date.
*/
if (this.resumeFrom && !this.resumeFrom.instance) {
for (var i = 0; i < this.path.length; i++) {
var node = this.path[i];
node.updateScroll();
}
}
var measured = this.measure();
roundBox(measured);
var prevLayout = this.layout;
this.layout = {
measured: measured,
actual: this.removeElementScroll(measured),
};
this.layoutCorrected = createBox();
this.isLayoutDirty = false;
this.projectionDelta = undefined;
this.notifyListeners("measure", this.layout.actual);
(_a = this.options.visualElement) === null || _a === void 0 ? void 0 : _a.notifyLayoutMeasure(this.layout.actual, prevLayout === null || prevLayout === void 0 ? void 0 : prevLayout.actual);
};
ProjectionNode.prototype.updateScroll = function () {
if (this.options.layoutScroll && this.instance) {
this.scroll = measureScroll(this.instance);
}
};
ProjectionNode.prototype.resetTransform = function () {
var _a;
if (!resetTransform)
return;
var isResetRequested = this.isLayoutDirty || this.shouldResetTransform;
var hasProjection = this.projectionDelta && !isDeltaZero(this.projectionDelta);
var transformTemplate = (_a = this.options.visualElement) === null || _a === void 0 ? void 0 : _a.getProps().transformTemplate;
var transformTemplateValue = transformTemplate === null || transformTemplate === void 0 ? void 0 : transformTemplate(this.latestValues, "");
var transformTemplateHasChanged = transformTemplateValue !== this.prevTransformTemplateValue;
if (isResetRequested &&
(hasProjection ||
hasTransform(this.latestValues) ||
transformTemplateHasChanged)) {
resetTransform(this.instance, transformTemplateValue);
this.shouldResetTransform = false;
this.scheduleRender();
}
};
ProjectionNode.prototype.measure = function () {
var visualElement = this.options.visualElement;
if (!visualElement)
return createBox();
var box = visualElement.measureViewportBox();
// Remove viewport scroll to give page-relative coordinates
var scroll = this.root.scroll;
if (scroll) {
translateAxis(box.x, scroll.x);
translateAxis(box.y, scroll.y);
}
return box;
};
ProjectionNode.prototype.removeElementScroll = function (box) {
var boxWithoutScroll = createBox();
copyBoxInto(boxWithoutScroll, box);
/**
* Performance TODO: Keep a cumulative scroll offset down the tree
* rather than loop back up the path.
*/
for (var i = 0; i < this.path.length; i++) {
var node = this.path[i];
var scroll_1 = node.scroll, options = node.options;
if (node !== this.root && scroll_1 && options.layoutScroll) {
translateAxis(boxWithoutScroll.x, scroll_1.x);
translateAxis(boxWithoutScroll.y, scroll_1.y);
}
}
return boxWithoutScroll;
};
ProjectionNode.prototype.applyTransform = function (box, transformOnly) {
if (transformOnly === void 0) { transformOnly = false; }
var withTransforms = createBox();
copyBoxInto(withTransforms, box);
for (var i = 0; i < this.path.length; i++) {
var node = this.path[i];
if (!transformOnly &&
node.options.layoutScroll &&
node.scroll &&
node !== node.root) {
transformBox(withTransforms, {
x: -node.scroll.x,
y: -node.scroll.y,
});
}
if (!hasTransform(node.latestValues))
continue;
transformBox(withTransforms, node.latestValues);
}
if (hasTransform(this.latestValues)) {
transformBox(withTransforms, this.latestValues);
}
return withTransforms;
};
ProjectionNode.prototype.removeTransform = function (box) {
var _a;
var boxWithoutTransform = createBox();
copyBoxInto(boxWithoutTransform, box);
for (var i = 0; i < this.path.length; i++) {
var node = this.path[i];
if (!node.instance)
continue;
if (!hasTransform(node.latestValues))
continue;
hasScale(node.latestValues) && node.updateSnapshot();
var sourceBox = createBox();
var nodeBox = node.measure();
copyBoxInto(sourceBox, nodeBox);
removeBoxTransforms(boxWithoutTransform, node.latestValues, (_a = node.snapshot) === null || _a === void 0 ? void 0 : _a.layout, sourceBox);
}
if (hasTransform(this.latestValues)) {
removeBoxTransforms(boxWithoutTransform, this.latestValues);
}
return boxWithoutTransform;
};
/**
*
*/
ProjectionNode.prototype.setTargetDelta = function (delta) {
this.targetDelta = delta;
this.root.scheduleUpdateProjection();
};
ProjectionNode.prototype.setOptions = function (options) {
var _a;
this.options = __assign(__assign(__assign({}, this.options), options), { crossfade: (_a = options.crossfade) !== null && _a !== void 0 ? _a : true });
};
ProjectionNode.prototype.clearMeasurements = function () {
this.scroll = undefined;
this.layout = undefined;
this.snapshot = undefined;
this.prevTransformTemplateValue = undefined;
this.targetDelta = undefined;
this.target = undefined;
this.isLayoutDirty = false;
};
/**
* Frame calculations
*/
ProjectionNode.prototype.resolveTargetDelta = function () {
var _a;
var _b = this.options, layout = _b.layout, layoutId = _b.layoutId;
/**
* If we have no layout, we can't perform projection, so early return
*/
if (!this.layout || !(layout || layoutId))
return;
/**
* If we don't have a targetDelta but do have a layout, we can attempt to resolve
* a relativeParent. This will allow a component to perform scale correction
* even if no animation has started.
*/
// TODO If this is unsuccessful this currently happens every frame
if (!this.targetDelta && !this.relativeTarget) {
// TODO: This is a semi-repetition of further down this function, make DRY
this.relativeParent = this.getClosestProjectingParent();
if (this.relativeParent && this.relativeParent.layout) {
this.relativeTarget = createBox();
this.relativeTargetOrigin = createBox();
calcRelativePosition(this.relativeTargetOrigin, this.layout.actual, this.relativeParent.layout.actual);
copyBoxInto(this.relativeTarget, this.relativeTargetOrigin);
}
}
/**
* If we have no relative target or no target delta our target isn't valid
* for this frame.
*/
if (!this.relativeTarget && !this.targetDelta)
return;
/**
* Lazy-init target data structure
*/
if (!this.target) {
this.target = createBox();
this.targetWithTransforms = createBox();
}
/**
* If we've got a relative box for this component, resolve it into a target relative to the parent.
*/
if (this.relativeTarget &&
this.relativeTargetOrigin &&
((_a = this.relativeParent) === null || _a === void 0 ? void 0 : _a.target)) {
calcRelativeBox(this.target, this.relativeTarget, this.relativeParent.target);
/**
* If we've only got a targetDelta, resolve it into a target
*/
}
else if (this.targetDelta) {
if (Boolean(this.resumingFrom)) {
// TODO: This is creating a new object every frame
this.target = this.applyTransform(this.layout.actual);
}
else {
copyBoxInto(this.target, this.layout.actual);
}
applyBoxDelta(this.target, this.targetDelta);
}
else {
/**
* If no target, use own layout as target
*/
copyBoxInto(this.target, this.layout.actual);
}
/**
* If we've been told to attempt to resolve a relative target, do so.
*/
if (this.attemptToResolveRelativeTarget) {
this.attemptToResolveRelativeTarget = false;
this.relativeParent = this.getClosestProjectingParent();
if (this.relativeParent &&
Boolean(this.relativeParent.resumingFrom) ===
Boolean(this.resumingFrom) &&
!this.relativeParent.options.layoutScroll &&
this.relativeParent.target) {
this.relativeTarget = createBox();
this.relativeTargetOrigin = createBox();
calcRelativePosition(this.relativeTargetOrigin, this.target, this.relativeParent.target);
copyBoxInto(this.relativeTarget, this.relativeTargetOrigin);
}
}
};
ProjectionNode.prototype.getClosestProjectingParent = function () {
if (!this.parent || hasTransform(this.parent.latestValues))
return undefined;
if ((this.parent.relativeTarget || this.parent.targetDelta) &&
this.parent.layout) {
return this.parent;
}
else {
return this.parent.getClosestProjectingParent();
}
};
ProjectionNode.prototype.calcProjection = function () {
var _a;
var _b = this.options, layout = _b.layout, layoutId = _b.layoutId;
/**
* If this section of the tree isn't animating we can
* delete our target sources for the following frame.
*/
this.isTreeAnimating = Boolean(((_a = this.parent) === null || _a === void 0 ? void 0 : _a.isTreeAnimating) ||
this.currentAnimation ||
this.pendingAnimation);
if (!this.isTreeAnimating) {
this.targetDelta = this.relativeTarget = undefined;
}
if (!this.layout || !(layout || layoutId))
return;
var lead = this.getLead();
/**
* Reset the corrected box with the latest values from box, as we're then going
* to perform mutative operations on it.
*/
copyBoxInto(this.layoutCorrected, this.layout.actual);
/**
* 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(this.layoutCorrected, this.treeScale, this.path, Boolean(this.resumingFrom) || this !== lead);
var target = lead.target;
if (!target)
return;
if (!this.projectionDelta) {
this.projectionDelta = createDelta();
this.projectionDeltaWithTransform = createDelta();
}
var prevTreeScaleX = this.treeScale.x;
var prevTreeScaleY = this.treeScale.y;
var prevProjectionTransform = this.projectionTransform;
/**
* 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 scale 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.
*/
calcBoxDelta(this.projectionDelta, this.layoutCorrected, target, this.latestValues);
this.projectionTransform = buildProjectionTransform(this.projectionDelta, this.treeScale);
if (this.projectionTransform !== prevProjectionTransform ||
this.treeScale.x !== prevTreeScaleX ||
this.treeScale.y !== prevTreeScaleY) {
this.hasProjected = true;
this.scheduleRender();
this.notifyListeners("projectionUpdate", target);
}
};
ProjectionNode.prototype.hide = function () {
this.isVisible = false;
// TODO: Schedule render
};
ProjectionNode.prototype.show = function () {
this.isVisible = true;
// TODO: Schedule render
};
ProjectionNode.prototype.scheduleRender = function (notifyAll) {
var _a, _b, _c;
if (notifyAll === void 0) { notifyAll = true; }
(_b = (_a = this.options).scheduleRender) === null || _b === void 0 ? void 0 : _b.call(_a);
notifyAll && ((_c = this.getStack()) === null || _c === void 0 ? void 0 : _c.scheduleRender());
if (this.resumingFrom && !this.resumingFrom.instance) {
this.resumingFrom = undefined;
}
};
ProjectionNode.prototype.setAnimationOrigin = function (delta, hasOnlyRelativeTargetChanged) {
var _this = this;
var _a;
if (hasOnlyRelativeTargetChanged === void 0) { hasOnlyRelativeTargetChanged = false; }
var snapshot = this.snapshot;
var snapshotLatestValues = (snapshot === null || snapshot === void 0 ? void 0 : snapshot.latestValues) || {};
var mixedValues = __assign({}, this.latestValues);
var targetDelta = createDelta();
this.relativeTarget = this.relativeTargetOrigin = undefined;
this.attemptToResolveRelativeTarget = !hasOnlyRelativeTargetChanged;
var relativeLayout = createBox();
var isSharedLayoutAnimation = snapshot === null || snapshot === void 0 ? void 0 : snapshot.isShared;
var isOnlyMember = (((_a = this.getStack()) === null || _a === void 0 ? void 0 : _a.members.length) || 0) <= 1;
var shouldCrossfadeOpacity = Boolean(isSharedLayoutAnimation &&
!isOnlyMember &&
this.options.crossfade === true &&
!this.path.some(hasOpacityCrossfade));
this.animationProgress = 0;
this.mixTargetDelta = function (latest) {
var _a;
var progress = latest / 1000;
mixAxisDelta(targetDelta.x, delta.x, progress);
mixAxisDelta(targetDelta.y, delta.y, progress);
_this.setTargetDelta(targetDelta);
if (_this.relativeTarget &&
_this.relativeTargetOrigin &&
_this.layout &&
((_a = _this.relativeParent) === null || _a === void 0 ? void 0 : _a.layout)) {
calcRelativePosition(relativeLayout, _this.layout.actual, _this.relativeParent.layout.actual);
mixBox(_this.relativeTarget, _this.relativeTargetOrigin, relativeLayout, progress);
}
if (isSharedLayoutAnimation) {
_this.animationValues = mixedValues;
mixValues(mixedValues, snapshotLatestValues, _this.latestValues, progress, shouldCrossfadeOpacity, isOnlyMember);
}
_this.root.scheduleUpdateProjection();
_this.scheduleRender();
_this.animationProgress = progress;
};
this.mixTargetDelta(0);
};
ProjectionNode.prototype.startAnimation = function (options) {
var _this = this;
var _a, _b;
this.notifyListeners("animationStart");
(_a = this.currentAnimation) === null || _a === void 0 ? void 0 : _a.stop();
if (this.resumingFrom) {
(_b = this.resumingFrom.currentAnimation) === null || _b === void 0 ? void 0 : _b.stop();
}
if (this.pendingAnimation) {
cancelSync.update(this.pendingAnimation);
this.pendingAnimation = undefined;
}
/**
* Start the animation in the next frame to have a frame with progress 0,
* where the target is the same as when the animation started, so we can
* calculate the relative positions correctly for instant transitions.
*/
this.pendingAnimation = sync.update(function () {
globalProjectionState.hasAnimatedSinceResize = true;
_this.currentAnimation = animate(0, animationTarget, __assign(__assign({}, options), { onUpdate: function (latest) {
var _a;
_this.mixTargetDelta(latest);
(_a = options.onUpdate) === null || _a === void 0 ? void 0 : _a.call(options, latest);
}, onComplete: function () {
var _a;
(_a = options.onComplete) === null || _a === void 0 ? void 0 : _a.call(options);
_this.completeAnimation();
} }));
if (_this.resumingFrom) {
_this.resumingFrom.currentAnimation = _this.currentAnimation;
}
_this.pendingAnimation = undefined;
});
};
ProjectionNode.prototype.completeAnimation = function () {
var _a;
if (this.resumingFrom) {
this.resumingFrom.currentAnimation = undefined;
this.resumingFrom.preserveOpacity = undefined;
}
(_a = this.getStack()) === null || _a === void 0 ? void 0 : _a.exitAnimationComplete();
this.resumingFrom =
this.currentAnimation =
this.animationValues =
undefined;
this.notifyListeners("animationComplete");
};
ProjectionNode.prototype.finishAnimation = function () {
var _a;
if (this.currentAnimation) {
(_a = this.mixTargetDelta) === null || _a === void 0 ? void 0 : _a.call(this, animationTarget);
this.currentAnimation.stop();
}
this.completeAnimation();
};
ProjectionNode.prototype.applyTransformsToTarget = function () {
var _a = this.getLead(), targetWithTransforms = _a.targetWithTransforms, target = _a.target, layout = _a.layout, latestValues = _a.latestValues;
if (!targetWithTransforms || !target || !layout)
return;
copyBoxInto(targetWithTransforms, target);
/**
* 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.
*/
transformBox(targetWithTransforms, latestValues);
/**
* 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.
*/
calcBoxDelta(this.projectionDeltaWithTransform, this.layoutCorrected, targetWithTransforms, latestValues);
};
ProjectionNode.prototype.registerSharedNode = function (layoutId, node) {
var _a, _b, _c;
if (!this.sharedNodes.has(layoutId)) {
this.sharedNodes.set(layoutId, new NodeStack());
}
var stack = this.sharedNodes.get(layoutId);
stack.add(node);
node.promote({
transition: (_a = node.options.initialPromotionConfig) === null || _a === void 0 ? void 0 : _a.transition,
preserveFollowOpacity: (_c = (_b = node.options.initialPromotionConfig) === null || _b === void 0 ? void 0 : _b.shouldPreserveFollowOpacity) === null || _c === void 0 ? void 0 : _c.call(_b, node),
});
};
ProjectionNode.prototype.isLead = function () {
var stack = this.getStack();
return stack ? stack.lead === this : true;
};
ProjectionNode.prototype.getLead = function () {
var _a;
var layoutId = this.options.layoutId;
return layoutId ? ((_a = this.getStack()) === null || _a === void 0 ? void 0 : _a.lead) || this : this;
};
ProjectionNode.prototype.getPrevLead = function () {
var _a;
var layoutId = this.options.layoutId;
return layoutId ? (_a = this.getStack()) === null || _a === void 0 ? void 0 : _a.prevLead : undefined;
};
ProjectionNode.prototype.getStack = function () {
var layoutId = this.options.layoutId;
if (layoutId)
return this.root.sharedNodes.get(layoutId);
};
ProjectionNode.prototype.promote = function (_a) {
var _b = _a === void 0 ? {} : _a, needsReset = _b.needsReset, transition = _b.transition, preserveFollowOpacity = _b.preserveFollowOpacity;
var stack = this.getStack();
if (stack)
stack.promote(this, preserveFollowOpacity);
if (needsReset) {
this.projectionDelta = undefined;
this.needsReset = true;
}
if (transition)
this.setOptions({ transition: transition });
};
ProjectionNode.prototype.relegate = function () {
var stack = this.getStack();
if (stack) {
return stack.relegate(this);
}
else {
return false;
}
};
ProjectionNode.prototype.resetRotation = function () {
var visualElement = this.options.visualElement;
if (!visualElement)
return;
// 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 (!visualElement.getStaticValue(key)) {
continue;
}
hasRotate = true;
// Record the rotation and then temporarily set it to 0
resetValues[key] = visualElement.getStaticValue(key);
visualElement.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.
visualElement === null || visualElement === void 0 ? void 0 : visualElement.syncRender();
// Put back all the values we reset
for (var key in resetValues) {
visualElement.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.
visualElement.scheduleRender();
};
ProjectionNode.prototype.getProjectionStyles = function (styleProp) {
var _a, _b, _c, _d, _e, _f;
if (styleProp === void 0) { styleProp = {}; }
// TODO: Return lifecycle-persistent object
var styles = {};
if (!this.instance || this.isSVG)
return styles;
if (!this.isVisible) {
return { visibility: "hidden" };
}
else {
styles.visibility = "";
}
var transformTemplate = (_a = this.options.visualElement) === null || _a === void 0 ? void 0 : _a.getProps().transformTemplate;
if (this.needsReset) {
this.needsReset = false;
styles.opacity = "";
styles.pointerEvents =
resolveMotionValue(styleProp.pointerEvents) || "";
styles.transform = transformTemplate
? transformTemplate(this.latestValues, "")
: "none";
return styles;
}
var lead = this.getLead();
if (!this.projectionDelta || !this.layout || !lead.target) {
var emptyStyles = {};
if (this.options.layoutId) {
emptyStyles.opacity = (_b = this.latestValues.opacity) !== null && _b !== void 0 ? _b : 1;
emptyStyles.pointerEvents =
resolveMotionValue(styleProp.pointerEvents) || "";
}
if (this.hasProjected && !hasTransform(this.latestValues)) {
emptyStyles.transform = transformTemplate
? transformTemplate({}, "")
: "none";
this.hasProjected = false;
}
return emptyStyles;
}
var valuesToRender = lead.animationValues || lead.latestValues;
this.applyTransformsToTarget();
styles.transform = buildProjectionTransform(this.projectionDeltaWithTransform, this.treeScale, valuesToRender);
if (transformTemplate) {
styles.transform = transformTemplate(valuesToRender, styles.transform);
}
var _g = this.projectionDelta, x = _g.x, y = _g.y;
styles.transformOrigin = "".concat(x.origin * 100, "% ").concat(y.origin * 100, "% 0");
if (lead.animationValues) {
/**
* If the lead component is animating, assign this either the entering/leaving
* opacity
*/
styles.opacity =
lead === this
? (_d = (_c = valuesToRender.opacity) !== null && _c !== void 0 ? _c : this.latestValues.opacity) !== null && _d !== void 0 ? _d : 1
: this.preserveOpacity
? this.latestValues.opacity
: valuesToRender.opacityExit;
}
else {
/**
* Or we're not animating at all, set the lead component to its actual
* opacity and other components to hidden.
*/
styles.opacity =
lead === this
? (_e = valuesToRender.opacity) !== null && _e !== void 0 ? _e : ""
: (_f = valuesToRender.opacityExit) !== null && _f !== void 0 ? _f : 0;
}
/**
* Apply scale correction
*/
for (var key in scaleCorrectors) {
if (valuesToRender[key] === undefined)
continue;
var _h = scaleCorrectors[key], correct = _h.correct, applyTo = _h.applyTo;
var corrected = correct(valuesToRender[key], lead);
if (applyTo) {
var num = applyTo.length;
for (var i = 0; i < num; i++) {
styles[applyTo[i]] = corrected;
}
}
else {
styles[key] = corrected;
}
}