UNPKG

framer-motion

Version:

A simple and powerful React animation library

1,024 lines (1,022 loc) • 57 kB
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; } }