UNPKG

motion-dom

Version:
2,059 lines • 94.3 kB
import { SubscriptionManager, clamp } from 'motion-utils';
import { animateSingleValue } from '../../animation/animate/single-value.mjs';
import { spring } from '../../animation/generators/spring.mjs';
import { calcGeneratorDuration } from '../../animation/generators/utils/calc-duration.mjs';
import { notifyLayoutAnimationStart } from '../../animation/utils/notify-inspector.mjs';
import { getOptimisedAppearId } from '../../animation/optimized-appear/get-appear-id.mjs';
import { getValueTransition } from '../../animation/utils/get-value-transition.mjs';
import { microtask } from '../../frameloop/microtask.mjs';
import { time } from '../../frameloop/sync-time.mjs';
import { scaleCorrectors } from '../styles/scale-correction.mjs';
import { statsBuffer } from '../../stats/buffer.mjs';
import { delay } from '../../utils/delay.mjs';
import { isRefObject } from '../../utils/is-ref-object.mjs';
import { isSVGElement } from '../../utils/is-svg-element.mjs';
import { isSVGSVGElement } from '../../utils/is-svg-svg-element.mjs';
import { mixNumber } from '../../utils/mix/number.mjs';
import { motionValue } from '../../value/index.mjs';
import { resolveMotionValue } from '../../value/utils/resolve-motion-value.mjs';
import { mixValues } from '../animation/mix-values.mjs';
import { pointToProgressVelocity, resolveAutoMass } from '../animation/layout-velocity.mjs';
import { transformGroups } from '../../animation/waapi/transforms/groups.mjs';
import { copyBoxInto, copyAxisDeltaInto, copyAxisInto } from '../geometry/copy.mjs';
import { translateAxis, transformBox, applyBoxDelta, applyTreeDeltas } from '../geometry/delta-apply.mjs';
import { calcLength, calcRelativeBox, calcRelativePosition, calcBoxDelta, isNear } from '../geometry/delta-calc.mjs';
import { removeBoxTransforms } from '../geometry/delta-remove.mjs';
import { roundBox } from '../geometry/round.mjs';
import { createBox, createDelta } from '../geometry/models.mjs';
import { boxEqualsRounded, isDeltaZero, axisDeltaEquals, boxEquals, aspectRatio } from '../geometry/utils.mjs';
import { NodeStack } from '../shared/stack.mjs';
import { defaultScaleCorrectors } from '../styles/default-scale-correctors.mjs';
import { buildProjectionTransform } from '../styles/transform.mjs';
import { eachAxis } from '../utils/each-axis.mjs';
import { FlatTree } from '../utils/flat-tree.mjs';
import { hasTransform, hasScale, has2DTranslate, hasDistortingTransform, angleTransforms } from '../utils/has-transform.mjs';
import { globalProjectionState } from './state.mjs';
import { frame, cancelFrame, frameSteps } from '../../frameloop/frame.mjs';
import { frameData } from '../../frameloop/frame-data.mjs';

/**
 * Transforms that distort an element's layout, so must be reset before measuring.
 */
const distortingTransforms = ["z", "skew", "skewZ"];
/**
 * 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
 */
const animationTarget = 1000;
let id = 0;
function resetDistortingTransform(key, visualElement, values, sharedAnimationValues) {
    const { latestValues } = visualElement;
    // Record the distorting transform and then temporarily set it to 0
    if (latestValues[key]) {
        values[key] = latestValues[key];
        visualElement.setStaticValue(key, 0);
        if (sharedAnimationValues) {
            sharedAnimationValues[key] = 0;
        }
    }
}
/**
 * Move accelerated transforms back to the main thread. The WAAPI animation
 * hid startUpdate()'s reset of rotations, skews and z, and moving off it
 * writes the whole transform back, so reset them again before this node
 * is measured.
 */
const demoteTransforms = (node) => node.instance &&
    transformGroups.get(node.instance)?.demote() &&
    node.resetSkewAndRotation();
function cancelTreeOptimisedTransformAnimations(projectionNode) {
    projectionNode.hasCheckedOptimisedAppear = true;
    if (projectionNode.root === projectionNode)
        return;
    const { visualElement } = projectionNode.options;
    if (!visualElement)
        return;
    const appearId = getOptimisedAppearId(visualElement);
    if (window.MotionHasOptimisedAnimation(appearId, "transform")) {
        const { layout, layoutId } = projectionNode.options;
        window.MotionCancelOptimisedAnimation(appearId, "transform", frame, !(layout || layoutId));
    }
    const { parent } = projectionNode;
    if (parent && !parent.hasCheckedOptimisedAppear) {
        cancelTreeOptimisedTransformAnimations(parent);
    }
}
function getClosestProjectingNode(visualElement) {
    if (!visualElement)
        return undefined;
    return visualElement.options.allowProjection !== false
        ? visualElement.projection
        : getClosestProjectingNode(visualElement.parent);
}
function createProjectionNode({ attachResizeListener, defaultParent, measureScroll, checkIsScrollRoot, resetTransform, }) {
    /**
     * Correctors registered via addScaleCorrector before projection loaded
     * (e.g. with an async LazyMotion bundle) take precedence over the defaults.
     * Applied here rather than at module scope so bundlers can drop projection
     * when it's unused.
     */
    Object.assign(scaleCorrectors, {
        ...defaultScaleCorrectors,
        ...scaleCorrectors,
    });
    return class ProjectionNode {
        /**
         * Create the projection node of a `motion` component's VisualElement,
         * from its props. This runs while the component renders, before its
         * children. It's here rather than in the component so React
         * bundles only include it with the features that supply a
         * ProjectionNode. Vanilla bundles that use the projection node
         * (drag(), animateLayout()) include it too, as a class static
         * can't be tree-shaken.
         */
        static create(visualElement, props, initialPromotionConfig) {
            const { layoutId, layout, drag, dragConstraints, layoutScroll, layoutRoot, layoutAnchor, layoutCrossfade, } = props;
            const projection = (visualElement.projection = new this(visualElement.latestValues, props["data-framer-portal-id"]
                ? undefined
                : getClosestProjectingNode(visualElement.parent)));
            projection.setOptions({
                layoutId,
                layout,
                alwaysMeasureLayout: Boolean(drag) ||
                    (dragConstraints && isRefObject(dragConstraints)),
                visualElement,
                /**
                 * TODO: Update options in an effect. This could be tricky as it'll be too late
                 * to update by the time layout animations run.
                 * We also need to fix this safeToRemove by linking it up to the one returned by usePresence,
                 * ensuring it gets called if there's no potential layout animations.
                 *
                 */
                animationType: typeof layout === "string" ? layout : "both",
                initialPromotionConfig,
                crossfade: layoutCrossfade,
                layoutScroll,
                layoutRoot,
                layoutAnchor,
            });
            /**
             * This node will take over from its layoutId's current lead, so snapshot
             * the lead now, while rendering. By the time the lead unmounts (or this
             * node mounts) React may already have committed DOM changes that come
             * before it in the tree, like a sibling changing height, and a snapshot
             * taken then starts the animation from a position never seen on screen.
             *
             * If React discards this render (e.g. it suspends), nothing else
             * completes the update, so check it after the next frame as an
             * unmounting node does.
             */
            const lead = projection.getStack()?.lead;
            if (lead && !projection.root.isUpdateBlocked()) {
                lead.willUpdate();
                lead.scheduleCheckAfterUnmount();
            }
        }
        constructor(latestValues = {}, parent = defaultParent?.()) {
            /**
             * A unique ID generated for every projection node.
             */
            this.id = id++;
            /**
             * An id that represents a unique session instigated by startUpdate.
             */
            this.animationId = 0;
            this.animationCommitId = 0;
            /**
             * 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;
            /**
             * Flag to true if we think the projection calculations for this node needs
             * recalculating as a result of an updated transform or layout animation.
             */
            this.isProjectionDirty = false;
            /**
             * Flag to true if the layout *or* transform has changed. This then gets propagated
             * throughout the projection tree, forcing any element below to recalculate on the next frame.
             */
            this.isSharedProjectionDirty = false;
            /**
             * Flag transform dirty. This gets propagated throughout the whole tree but is only
             * respected by shared nodes.
             */
            this.isTransformDirty = 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;
            /**
             * Store whether this node has been checked for optimised appear animations. As
             * effects fire bottom-up, and we want to look up the tree for appear animations,
             * this makes sure we only check each path once, stopping at nodes that
             * have already been checked.
             */
            this.hasCheckedOptimisedAppear = false;
            /**
             * An object representing the calculated contextual/accumulated/tree scale.
             * This will be used to scale calculated projection transforms, as these are
             * calculated in screen-space but need to be scaled for elements to visually
             * make it to their calculated destinations.
             *
             * TODO: Lazy-init
             */
            this.treeScale = { x: 1, y: 1 };
            /**
             *
             */
            this.eventHandlers = new Map();
            this.hasTreeAnimated = false;
            // Note: Currently only running on root node
            this.updateScheduled = false;
            this.scheduleUpdate = () => this.update();
            this.projectionUpdateScheduled = false;
            this.checkUpdateFailed = () => {
                if (this.isUpdating) {
                    this.isUpdating = false;
                    this.clearAllSnapshots();
                }
            };
            /**
             * This is a multi-step process as shared nodes might be of different depths. Nodes
             * are sorted by depth order, so we need to resolve the entire tree before moving to
             * the next step.
             */
            this.updateProjection = () => {
                this.projectionUpdateScheduled = false;
                this.nodes.forEach(propagateDirtyNodes);
                this.nodes.forEach(resolveTargetDelta);
                this.nodes.forEach(calcProjection);
                this.nodes.forEach(cleanDirtyNodes);
                /**
                 * Record this frame's debug counts, if recordStats() is running.
                 */
                statsBuffer.countProjection?.();
            };
            /**
             * Frame calculations
             */
            this.resolvedRelativeTargetAt = 0.0;
            this.hasProjected = false;
            this.isVisible = true;
            this.animationProgress = 0;
            /**
             * Shared layout
             */
            // TODO Only running on root node
            this.sharedNodes = new Map();
            this.latestValues = latestValues;
            this.root = parent ? parent.root || parent : this;
            this.path = parent ? [...parent.path, parent] : [];
            this.parent = parent;
            this.depth = parent ? parent.depth + 1 : 0;
            for (let i = 0; i < this.path.length; i++) {
                this.path[i].shouldResetTransform = true;
            }
            if (this.root === this)
                this.nodes = new FlatTree();
        }
        addEventListener(name, handler) {
            if (!this.eventHandlers.has(name)) {
                this.eventHandlers.set(name, new SubscriptionManager());
            }
            return this.eventHandlers.get(name).add(handler);
        }
        notifyListeners(name, ...args) {
            this.eventHandlers.get(name)?.notify(...args);
        }
        hasListeners(name) {
            return this.eventHandlers.has(name);
        }
        /**
         * Lifecycles
         */
        mount(instance) {
            if (this.instance)
                return;
            this.isSVG = isSVGElement(instance) && !isSVGSVGElement(instance);
            this.instance = instance;
            const { layoutId, layout, visualElement } = this.options;
            if (visualElement && !visualElement.current) {
                visualElement.mount(instance);
            }
            this.root.nodes.add(this);
            this.parent?.children.add(this);
            if (this.root.hasTreeAnimated && (layout || layoutId)) {
                this.isLayoutDirty = true;
            }
            if (attachResizeListener) {
                let cancelDelay;
                let innerWidth = 0;
                const resizeUnblockUpdate = () => (this.root.updateBlockedByResize = false);
                // Set initial innerWidth in a frame.read callback to batch the read
                frame.read(() => {
                    innerWidth = window.innerWidth;
                });
                attachResizeListener(instance, () => {
                    const newInnerWidth = window.innerWidth;
                    if (newInnerWidth === innerWidth)
                        return;
                    innerWidth = newInnerWidth;
                    this.root.updateBlockedByResize = true;
                    cancelDelay?.();
                    cancelDelay = delay(resizeUnblockUpdate, 250);
                    if (globalProjectionState.hasAnimatedSinceResize) {
                        globalProjectionState.hasAnimatedSinceResize = false;
                        this.nodes.forEach(finishAnimation);
                    }
                });
            }
            if (layoutId) {
                this.root.registerSharedNode(layoutId, this);
            }
            this.addLayoutListener();
        }
        /**
         * Only register the layout animation handler if this node requires
         * layout animation. This is called on mount, and again whenever options
         * change, as `layout` or `layoutId` may be enabled after mount.
         */
        addLayoutListener() {
            const { layoutId, layout, visualElement, animate } = this.options;
            if (!this.instance ||
                this.hasLayoutListener ||
                animate === false ||
                !visualElement ||
                !(layoutId || layout))
                return;
            this.hasLayoutListener = true;
            this.addEventListener("didUpdate", ({ delta, hasLayoutChanged, hasRelativeLayoutChanged, layout: newLayout, }) => {
                if (this.isAnimationBlocked) {
                    this.target = undefined;
                    this.relativeTarget = undefined;
                    return;
                }
                // TODO: Check here if an animation exists
                const layoutTransition = this.options.transition ||
                    visualElement.getDefaultTransition() ||
                    defaultLayoutTransition;
                const { onLayoutAnimationStart, onLayoutAnimationComplete, } = visualElement.getProps();
                /**
                 * The target layout of the element might stay the same,
                 * but its position relative to its parent has changed.
                 */
                const hasTargetChanged = !this.targetLayout ||
                    !boxEqualsRounded(this.targetLayout, newLayout);
                /*
                 * Note: Disabled to fix relative animations always triggering new
                 * layout animations. If this causes further issues, we can try
                 * a different approach to detecting relative target changes.
                 */
                // || hasRelativeLayoutChanged
                /**
                 * 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.
                 */
                const hasOnlyRelativeTargetChanged = !hasLayoutChanged && hasRelativeLayoutChanged;
                if (this.options.layoutRoot ||
                    this.resumeFrom ||
                    hasOnlyRelativeTargetChanged ||
                    (hasLayoutChanged &&
                        (hasTargetChanged || !this.currentAnimation))) {
                    if (this.resumeFrom) {
                        this.resumingFrom = this.resumeFrom;
                        this.resumingFrom.resumingFrom = undefined;
                    }
                    const animationOptions = {
                        ...getValueTransition(layoutTransition, "layout"),
                        onPlay: onLayoutAnimationStart,
                        onComplete: onLayoutAnimationComplete,
                    };
                    if (visualElement.shouldReduceMotion ||
                        this.options.layoutRoot) {
                        animationOptions.delay = 0;
                        animationOptions.type = false;
                    }
                    this.startAnimation(animationOptions);
                    /**
                     * Set animation origin after starting animation to avoid layout jump
                     * caused by stopping previous layout animation
                     */
                    this.setAnimationOrigin(delta, hasOnlyRelativeTargetChanged, animationOptions.path);
                }
                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 committed to screen and look like a jumpy box.
                     */
                    if (!hasLayoutChanged) {
                        finishAnimation(this);
                    }
                    this.isLead() && this.options.onExitComplete?.();
                }
                this.targetLayout = newLayout;
            });
        }
        unmount() {
            this.options.layoutId && this.willUpdate();
            this.root.nodes.remove(this);
            this.getStack()?.remove(this);
            this.parent?.children.delete(this);
            this.instance = undefined;
            this.eventHandlers.clear();
            this.hasLayoutListener = false;
            cancelFrame(this.updateProjection);
        }
        // only on the root
        blockUpdate() {
            this.updateManuallyBlocked = true;
        }
        unblockUpdate() {
            this.updateManuallyBlocked = false;
        }
        isUpdateBlocked() {
            return this.updateManuallyBlocked || this.updateBlockedByResize;
        }
        // Note: currently only running on root node
        startUpdate() {
            if (this.isUpdateBlocked())
                return;
            this.isUpdating = true;
            this.nodes?.forEach(resetSkewAndRotation);
            this.animationId++;
        }
        getTransformTemplate() {
            const { visualElement } = this.options;
            return visualElement && visualElement.getProps().transformTemplate;
        }
        willUpdate(shouldNotifyListeners = true) {
            this.root.hasTreeAnimated = true;
            if (this.root.isUpdateBlocked()) {
                this.options.onExitComplete?.();
                return;
            }
            /**
             * If we're running optimised appear animations then these must be
             * cancelled before measuring the DOM. This is so we can measure
             * the true layout of the element rather than the WAAPI animation
             * which will be unaffected by the resetSkewAndRotate step.
             *
             * Note: This is a DOM write. Worst case scenario is this is sandwiched
             * between other snapshot reads which will cause unnecessary style recalculations.
             * This has to happen here though, as we don't yet know which nodes will need
             * snapshots in startUpdate(), but we only want to cancel optimised animations
             * if a layout animation measurement is actually going to be affected by them.
             */
            if (window.MotionCancelOptimisedAnimation &&
                !this.hasCheckedOptimisedAppear) {
                cancelTreeOptimisedTransformAnimations(this);
            }
            !this.root.isUpdating && this.root.startUpdate();
            if (this.isLayoutDirty)
                return;
            /**
             * Accelerated transforms aren't reflected in latestValues or
             * removed by resetTransform, so move them back to the main
             * thread before this node, or its ancestors, are measured.
             */
            demoteTransforms(this);
            this.isLayoutDirty = true;
            for (let i = 0; i < this.path.length; i++) {
                const node = this.path[i];
                node.shouldResetTransform = true;
                demoteTransforms(node);
                /**
                 * Percentage translates resolve against layoutBox dimensions,
                 * so ancestors with them must be re-measured after transform reset.
                 */
                if (typeof node.latestValues.x === "string" ||
                    typeof node.latestValues.y === "string") {
                    node.isLayoutDirty = true;
                }
                node.updateScroll("snapshot");
                if (node.options.layoutRoot) {
                    node.willUpdate(false);
                }
                /**
                 * Measure the nearest dragged ancestor before the DOM
                 * changes, so this node can follow its layout change.
                 */
                if (node.isAnimationBlocked) {
                    node.dragSnapshot || (node.dragSnapshot = node.measure());
                    this.dragAncestor = node;
                }
            }
            const { layoutId, layout } = this.options;
            if (layoutId === undefined && !layout)
                return;
            const transformTemplate = this.getTransformTemplate();
            this.prevTransformTemplateValue = transformTemplate
                ? transformTemplate(this.latestValues, "")
                : undefined;
            this.updateSnapshot();
            shouldNotifyListeners && this.notifyListeners("willUpdate");
        }
        update() {
            this.updateScheduled = false;
            const 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) {
                const wasBlockedByResize = this.updateBlockedByResize;
                this.unblockUpdate();
                this.updateBlockedByResize = false;
                this.clearAllSnapshots();
                /**
                 * When blocked by resize, still measure layouts so
                 * callbacks like onLayoutMeasure fire (e.g. Reorder).
                 * Skip notifyLayoutUpdate to prevent animations.
                 */
                if (wasBlockedByResize) {
                    this.nodes.forEach(forceLayoutMeasure);
                }
                this.nodes.forEach(clearMeasurements);
                return;
            }
            /**
             * If this is a repeat of didUpdate then ignore the animation.
             */
            if (this.animationId <= this.animationCommitId) {
                this.nodes.forEach(clearIsLayoutDirty);
                return;
            }
            this.animationCommitId = this.animationId;
            if (!this.isUpdating) {
                this.nodes.forEach(clearIsLayoutDirty);
            }
            else {
                this.isUpdating = false;
                /**
                 * Ensure animation-blocked nodes (e.g. during drag)
                 * get measured even when memoized (willUpdate skipped).
                 */
                this.nodes.forEach(ensureDraggedNodesSnapshotted);
                this.nodes.forEach(syncRelativeLayout);
                /**
                 * 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();
            /**
             * Manually flush any pending updates. Ideally
             * we could leave this to the following requestAnimationFrame but this seems
             * to leave a flash of incorrectly styled content.
             */
            const now = time.now();
            frameData.delta = clamp(0, 1000 / 60, now - frameData.timestamp);
            frameData.timestamp = now;
            frameData.isProcessing = true;
            frameSteps.update.process(frameData);
            frameSteps.preRender.process(frameData);
            frameSteps.render.process(frameData);
            frameData.isProcessing = false;
        }
        didUpdate() {
            if (!this.updateScheduled) {
                this.updateScheduled = true;
                microtask.read(this.scheduleUpdate);
            }
        }
        clearAllSnapshots() {
            this.nodes.forEach(clearSnapshot);
            /**
             * A stack's last member can unmount while a replacement with the
             * same layoutId mounts in the same commit, so the stack (and its
             * lead) must outlive the unmount for the replacement to resume
             * from it. By now that handoff has happened, so stacks that are
             * still empty only retain unmounted nodes and can be released.
             */
            this.sharedNodes.forEach((stack, layoutId) => stack.members.length
                ? stack.removeLeadSnapshot()
                : this.sharedNodes.delete(layoutId));
        }
        scheduleUpdateProjection() {
            if (!this.projectionUpdateScheduled) {
                this.projectionUpdateScheduled = true;
                frame.preRender(this.updateProjection, false, true);
            }
        }
        scheduleCheckAfterUnmount() {
            /**
             * 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.
             */
            frame.postRender(() => {
                if (this.isLayoutDirty) {
                    this.root.didUpdate();
                }
                else {
                    this.root.checkUpdateFailed();
                }
            });
        }
        /**
         * Update measurements
         */
        updateSnapshot() {
            if (this.snapshot || !this.instance)
                return;
            this.snapshot = this.measure();
            if (this.snapshot &&
                !calcLength(this.snapshot.measuredBox.x) &&
                !calcLength(this.snapshot.measuredBox.y)) {
                this.snapshot = undefined;
            }
            /**
             * Record how fast the element is moving when it's measured, so
             * the next layout animation can start at the same speed.
             */
            if (this.snapshot) {
                const { visualElement } = this.options;
                /**
                 * A paused or stopped animation hands on no velocity.
                 */
                const isRunning = this.currentAnimation?.state === "running";
                const edges = isRunning && this.animationEdges;
                /**
                 * A child moving inside its parent hands on only its
                 * velocity relative to the parent.
                 */
                if (isRunning && this.relativeTarget) {
                    this.snapshot.relativeVelocity =
                        this.relativeVelocity?.slice() || [];
                }
                const progress = (edges && this.motionValue?.getVelocity()) || 0;
                const velocity = [0, 1, 2, 3, 4, 5].map((i) => (i < 4
                    ? edges &&
                        (this.edgeValues
                            ? this.edgeVelocity(i)
                            : (-edges[i] * progress) / 1000)
                    : visualElement
                        ?.getValue(i < 5 ? "x" : "y")
                        ?.getVelocity()) || 0);
                if (velocity.some(Boolean))
                    this.snapshot.velocity = velocity;
            }
        }
        updateLayout() {
            if (!this.instance)
                return;
            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 (let i = 0; i < this.path.length; i++) {
                    const node = this.path[i];
                    node.updateScroll();
                }
            }
            const prevLayout = this.layout;
            this.layout = this.measure(false);
            if (!this.layoutCorrected)
                this.layoutCorrected = createBox();
            this.isLayoutDirty = false;
            this.projectionDelta = undefined;
            this.notifyListeners("measure", this.layout.layoutBox);
            const { visualElement } = this.options;
            visualElement?.notify("LayoutMeasure", this.layout.layoutBox, prevLayout?.layoutBox);
        }
        updateScroll(phase = "measure") {
            const { scroll, instance } = this;
            const { animationId } = this.root;
            if (this.options.layoutScroll &&
                instance &&
                !(scroll &&
                    scroll.animationId === animationId &&
                    scroll.phase === phase)) {
                const isRoot = checkIsScrollRoot(instance);
                this.scroll = {
                    animationId,
                    phase,
                    isRoot,
                    offset: measureScroll(instance),
                    wasRoot: scroll ? scroll.isRoot : isRoot,
                };
            }
        }
        resetTransform() {
            if (!resetTransform)
                return;
            const isResetRequested = this.isLayoutDirty ||
                this.shouldResetTransform ||
                this.options.alwaysMeasureLayout;
            isResetRequested && demoteTransforms(this);
            const hasProjection = this.projectionDelta && !isDeltaZero(this.projectionDelta);
            const transformTemplate = this.getTransformTemplate();
            const transformTemplateValue = transformTemplate
                ? transformTemplate(this.latestValues, "")
                : undefined;
            const transformTemplateHasChanged = transformTemplateValue !== this.prevTransformTemplateValue;
            if (isResetRequested &&
                this.instance &&
                (hasProjection ||
                    hasTransform(this.latestValues) ||
                    transformTemplateHasChanged)) {
                resetTransform(this.instance, transformTemplateValue);
                this.shouldResetTransform = false;
                this.scheduleRender();
            }
        }
        measure(removeTransform = true) {
            const pageBox = this.measurePageBox();
            let layoutBox = this.removeElementScroll(pageBox);
            /**
             * Measurements taken during the pre-render stage
             * still have transforms applied so we remove them
             * via calculation.
             */
            if (removeTransform) {
                layoutBox = this.removeTransform(layoutBox);
            }
            roundBox(layoutBox);
            return {
                animationId: this.root.animationId,
                measuredBox: pageBox,
                layoutBox,
                latestValues: {},
                source: this.id,
            };
        }
        measurePageBox() {
            const { visualElement } = this.options;
            if (!visualElement)
                return createBox();
            const box = visualElement.measureViewportBox();
            const wasInScrollRoot = this.scroll?.wasRoot || this.path.some(checkNodeWasScrollRoot);
            if (!wasInScrollRoot) {
                // Remove viewport scroll to give page-relative coordinates
                const { scroll } = this.root;
                if (scroll) {
                    translateAxis(box.x, scroll.offset.x);
                    translateAxis(box.y, scroll.offset.y);
                }
            }
            return box;
        }
        removeElementScroll(box) {
            const boxWithoutScroll = createBox();
            copyBoxInto(boxWithoutScroll, box);
            if (this.scroll?.wasRoot) {
                return boxWithoutScroll;
            }
            /**
             * Performance TODO: Keep a cumulative scroll offset down the tree
             * rather than loop back up the path.
             */
            for (let i = 0; i < this.path.length; i++) {
                const node = this.path[i];
                const { scroll, options } = node;
                if (node !== this.root && scroll && options.layoutScroll) {
                    /**
                     * If this is a new scroll root, we want to remove all previous scrolls
                     * from the viewport box.
                     */
                    if (scroll.wasRoot) {
                        copyBoxInto(boxWithoutScroll, box);
                    }
                    translateAxis(boxWithoutScroll.x, scroll.offset.x);
                    translateAxis(boxWithoutScroll.y, scroll.offset.y);
                }
            }
            return boxWithoutScroll;
        }
        applyTransform(box, transformOnly = false, output, skipOwnTransform, start = 0) {
            const withTransforms = output || createBox();
            copyBoxInto(withTransforms, box);
            for (let i = start; i < this.path.length; i++) {
                const node = this.path[i];
                if (!transformOnly &&
                    node.options.layoutScroll &&
                    node.scroll &&
                    node !== node.root) {
                    translateAxis(withTransforms.x, -node.scroll.offset.x);
                    translateAxis(withTransforms.y, -node.scroll.offset.y);
                }
                if (!hasTransform(node.latestValues))
                    continue;
                transformBox(withTransforms, node.latestValues, node.layout?.layoutBox);
            }
            if (!skipOwnTransform && hasTransform(this.latestValues)) {
                transformBox(withTransforms, this.latestValues, this.layout?.layoutBox);
            }
            return withTransforms;
        }
        removeTransform(box) {
            const boxWithoutTransform = createBox();
            copyBoxInto(boxWithoutTransform, box);
            for (let i = 0; i < this.path.length; i++) {
                const node = this.path[i];
                if (!hasTransform(node.latestValues))
                    continue;
                let sourceBox;
                if (node.instance) {
                    hasScale(node.latestValues) && node.updateSnapshot();
                    sourceBox = createBox();
                    copyBoxInto(sourceBox, node.measurePageBox());
                }
                removeBoxTransforms(boxWithoutTransform, node.latestValues, node.snapshot?.layoutBox, sourceBox);
            }
            if (hasTransform(this.latestValues)) {
                removeBoxTransforms(boxWithoutTransform, this.latestValues);
            }
            return boxWithoutTransform;
        }
        setTargetDelta(delta) {
            this.targetDelta = delta;
            this.root.scheduleUpdateProjection();
            this.isProjectionDirty = true;
        }
        setOptions(options) {
            this.options = {
                ...this.options,
                ...options,
                crossfade: options.crossfade !== undefined ? options.crossfade : true,
            };
            this.addLayoutListener();
        }
        clearMeasurements() {
            this.scroll = undefined;
            this.layout = undefined;
            this.snapshot = undefined;
            this.prevTransformTemplateValue = undefined;
            this.targetDelta = undefined;
            this.target = undefined;
            this.isLayoutDirty = false;
        }
        forceRelativeParentToResolveTarget() {
            if (!this.relativeParent)
                return;
            /**
             * If the parent target isn't up-to-date, force it to update.
             * This is an unfortunate de-optimisation as it means any updating relative
             * projection will cause all the relative parents to recalculate back
             * up the tree.
             */
            if (this.relativeParent.resolvedRelativeTargetAt !==
                frameData.timestamp) {
                this.relativeParent.resolveTargetDelta(true);
            }
        }
        resolveTargetDelta(forceRecalculation = false) {
            /**
             * Once the dirty status of nodes has been spread through the tree, we also
             * need to check if we have a shared node of a different depth that has itself
             * been dirtied.
             */
            const lead = this.getLead();
            this.isProjectionDirty || (this.isProjectionDirty = lead.isProjectionDirty);
            this.isTransformDirty || (this.isTransformDirty = lead.isTransformDirty);
            this.isSharedProjectionDirty || (this.isSharedProjectionDirty = lead.isSharedProjectionDirty);
            const isShared = Boolean(this.resumingFrom) || this !== lead;
            /**
             * We don't use transform for this step of processing so we don't
             * need to check whether any nodes have changed transform.
             */
            const canSkip = !(forceRecalculation ||
                (isShared && this.isSharedProjectionDirty) ||
                this.isProjectionDirty ||
                this.parent?.isProjectionDirty ||
                this.attemptToResolveRelativeTarget ||
                this.root.updateBlockedByResize);
            if (canSkip)
                return;
            const { layout, layoutId } = this.options;
            /**
             * If we have no layout, we can't perform projection, so early return
             */
            if (!this.layout || !(layout || layoutId))
                return;
            this.resolvedRelativeTargetAt = frameData.timestamp;
            const relativeParent = this.getClosestProjectingParent();
            /**
             * 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.
             */
            if (!this.targetDelta && !this.relativeTarget) {
                if (this.options.layoutAnchor !== false &&
                    relativeParent &&
                    relativeParent.layout) {
                    this.createRelativeTarget(relativeParent, this.layout.layoutBox, relativeParent.layout.layoutBox);
                }
                else {
                    this.removeRelativeTarget();
                }
            }
            /**
             * 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 &&
                this.relativeParent &&
                this.relativeParent.target) {
                this.forceRelativeParentToResolveTarget();
                calcRelativeBox(this.target, this.relativeTarget, this.relativeParent.target, this.options.layoutAnchor || undefined);
                /**
                 * If we've only got a targetDelta, resolve it into a target
                 */
            }
            else if (this.targetDelta) {
                if (Boolean(this.resumingFrom)) {
                    /**
                     * Own transforms are applied later, in applyTransformsToTarget,
                     * so only apply ancestor transforms here or they'd be doubled.
                     */
                    this.applyTransform(this.layout.layoutBox, false, this.target, true);
                }
                else {
                    copyBoxInto(this.target, this.layout.layoutBox);
                }
                applyBoxDelta(this.target, this.targetDelta);
            }
            else {
                /**
                 * If no target, use own layout as target
                 */
                copyBoxInto(this.target, this.layout.layoutBox);
            }
            /**
             * If we've been told to attempt to resolve a relative target, do so.
             */
            if (this.attemptToResolveRelativeTarget) {
                this.attemptToResolveRelativeTarget = false;
                if (this.options.layoutAnchor !== false &&
                    relativeParent &&
                    (!relativeParent.resumingFrom === !this.resumingFrom ||
                        /**
                         * Shared layout animations within a layoutRoot
                         * animate relative to it.
                         */
                        (relativeParent.options.layoutRoot &&
                            !relativeParent.resumingFrom)) &&
                    !relativeParent.options.layoutScroll &&
                    relativeParent.target &&
                    this.animationProgress !== 1) {
                    this.createRelativeTarget(relativeParent, this.target, relativeParent.target);
                }
                else {
                    this.relativeParent = this.relativeTarget = undefined;
                }
            }
            /**
             * Increase debug counter for resolved target deltas
             */
            statsBuffer.countProjection?.(1); // calculatedTargetDeltas
        }
        getClosestProjectingParent() {
            if (!this.parent ||
                hasScale(this.parent.latestValues) ||
                has2DTranslate(this.parent.latestValues)) {
                return undefined;
            }
            if (this.parent.isProjecting()) {
                return this.parent;
            }
            else {
                return this.parent.getClosestProjectingParent();
            }
        }
        isProjecting() {
            return Boolean((this.relativeTarget ||
                this.targetDelta ||
                this.options.layoutRoot) &&
                this.layout);
        }
        createRelativeTarget(relativeParent, layout, parentLayout) {
            this.relativeParent = relativeParent;
            this.forceRelativeParentToResolveTarget();
            this.relativeTarget = createBox();
            this.relativeTargetOrigin = createBox();
            calcRelativePosition(this.relativeTargetOrigin, layout, parentLayout, this.options.layoutAnchor || undefined);
            copyBoxInto(this.relativeTarget, this.relativeTargetOrigin);
        }
        removeRelativeTarget() {
            this.relativeParent = this.relativeTarget = undefined;
        }
        calcProjection() {
            const lead = this.getLead();
            const isShared = Boolean(this.resumingFrom) || this !== lead;
            let canSkip = true;
            /**
             * If this is a normal layout animation and neither this node nor its nearest projecting
             * is dirty then we can't skip.
             */
            if (this.isProjectionDirty || this.parent?.isProjectionDirty) {
                canSkip = false;
            }
            /**
             * If this is a shared layout animation and this node's shared projection is dirty then
             * we can't skip.
             */
            if (isShared &&
                (this.isSharedProjectionDirty || this.isTransformDirty)) {
                canSkip = false;
            }
            /**
             * If we have resolved the target this frame we must recalculate the
             * projection to ensure it visually represents the internal calculations.
             */
            if (this.resolvedRelativeTargetAt === frameData.timestamp) {
                canSkip = false;
            }
            if (canSkip)
                return;
            const { layout, layoutId } = this.options;
            /**
             * If this section of the tree isn't animating we can
             * delete our target sources for the following frame.
             */
            this.isTreeAnimating = Boolean(this.parent?.isTreeAnimating ||
                this.currentAnimation ||
                this.pendingAnimation);
            if (!this.isTreeAnimating) {
                this.targetDelta = this.relativeTarget = undefined;
            }
            if (!this.layout || !(layout || layoutId))
                return;
            /**
             * 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.layoutBox);
            /**
             * Record previous tree scales before updating.
             */
            const prevTreeScaleX = this.treeScale.x;
            const prevTreeScaleY = this.treeScale.y;
            /**
             * 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.
             *
             * A shared target is usually in page space, so ancestor transforms are applied
             * too. But a target relative to a parent that isn't itself shared (like a
             * layoutRoot) is in that parent's layout space. So the transforms of the
             * parent and its ancestors, which move both elements alike, are skipped,
             * but those of other ancestors (between it and either element) apply.
             */
            applyTreeDeltas(this.layoutCorrected, this.treeScale, this.path, isShared &&
                (lead.relativeTarget && !lead.relativeParent.resumingFrom
                    ? lead.relativeParent
                    : true));
            /**
             * If this layer needs to perform scale correction but doesn't have a target,
             * use the layout as the target.
             */
            if (lead.layout &&
                !lead.target &&
                (this.treeScale.x !== 1 || this.treeScale.y !== 1)) {
                lead.target = lead.layout.layoutBox;
                lead.targetWithTransforms = createBox();
            }
            const { target } = lead;
            if (!target) {
                /**
                 * If we don't have a target to project into, but we were previously
                 * projecting, we want to remove the stored transform and schedule
                 * a render to ensure the elements reflect the removed transform.
                 */
                if (this.prevProjectionDelta) {
                    this.createProjectionDeltas();
                    this.scheduleRender();
                }
                return;
            }
            if (!this.projectionDelta || !this.prevProjectionDelta) {
                this.createProjectionDeltas();
            }
            else {
                copyAxisDeltaInto(this.prevProjectionDelta.x, this.projectionDelta.x);
                copyAxisDeltaInto(this.prevProjectionDelta.y, this.projectionDelta.y);
            }
            /**
             * 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);
            if (this.treeScale.x !== prevTreeScaleX ||
                this.treeScale.y !== prevTreeScaleY ||
                !axisDeltaEquals(this.projectionDelta.x, this.prevProjectionDelta.x) ||
                !axisDeltaEquals(this.projectionDelta.y, this.prevProjectionDelta.y)) {
                this.hasProjected = true;
                this.scheduleRender();
                this.notifyListeners("projectionUpdate", target);
            }
            /**
             * Increase debug counter for recalculated projections
             */
            statsBuffer.countProjection?.(2); // calculatedProjections
        }
        hide() {
            this.isVisible = false;
            // TODO: Schedule render
        }
        show() {
            this.isVisible = true;
            // TODO: Schedule render
        }
        scheduleRender(notifyAll = true) {
            this.options.visualElement?.scheduleRender();
            if (notifyAll) {
                this.getStack()?.scheduleRender();
            }
            if (this.resumingFrom && !this.resumingFrom.instance) {
                this.resumingFrom = undefined;
            }
        }
        createProjectionDeltas() {
            this.prevProjectionDelta = createDelta();
            this.projectionDelta = createDelta();
            this.projectionDeltaWithTransform = createDelta();
        }
        setAnimationOrigin(delta, hasOnlyRelativeTargetChanged = false, pathFn) {
            const snapshot = this.snapshot;
            const snapshotLatestValues = snapshot ? snapshot.latestValues : {};
            const mixedValues = { ...this.latestValues };
            const targetDelta = createDelta();
            if (!this.relativeParent ||
                !this.relativeParent.options.layoutRoot) {
                this.relativeTarget = this.relativeTargetOrigin = undefined;
            }
            this.attemptToResolveRelativeTarget = !hasOnlyRelativeTargetChanged;
            const relativeLayout = createBox();
            const isSharedLayoutAnimation = snapshot?.source !== this.layout?.source;
            const stack = this.getStack();
            const isOnlyMember = !stack || stack.members.length <= 1;
            const shouldCrossfadeOpacity = Boolean(isSharedLayoutAnimation &&
                !isOnlyMember &&
                this.options.crossfade === true &&
                !this.path.some(hasOpacityCrossfade));
            this.animationProgress = 0;
            let prevRelativeTarget;
            // The path decides whether the layout shift is worth curving
            // (distance floor) and resolves the interpolator from the delta.
            const interpolate = pathFn?.interpolateProjection(delta);
            const box = this.layout?.layoutBox;
            let edges = box &&
                [box.x.min, box.x.max, box.y.min, box.y.max].map((edge, i) => {
                    const { translate, scale, originPoint } = delta[i < 2 ? "x" : "y"];
                    return (edge - originPoint) * (scale - 1) + translate;
                });
            /**
             * A zero-length layout axis has no finite edge offsets.
             */
            if (!edges?.every(isFinite))
                edges = undefined;
            this.animationEdges = edges;
            this.edgeValues =
                this.startVelocity =
                    this.impulse =
                        this.relativeVelocity =
                            this.relativeStart =
                                undefined;
            /**
             * A shared snapshot is measured with the old element's own
             * transforms (like x/y from drag), so it hands their velocity on
             * too. Otherwise those values keep animating themselves.
             */
            const velocity = snapshot?.velocity;
            /**
             * A child that wasn't moving inside an animating parent before
             * moved in page space, so that is its relative velocity.
             */
            const relative = snapshot?.relativeVelocity || velocity;
            if (relative?.slice(0, 4).some(Boolean) &&
                this.getClosestProjectingParent()) {
                this.relativeStart = relative;
            }
            if (velocity && edges) {
                const v = velocity
                    .slice(0, 4)
                    .map((edge, i) => edge +
                    (isSharedLayoutAnimation
                        ? velocity[4 + (i >> 1)]
                        : 0));
                if (v.some(Boolean)) {
                    this.startVelocity = interpolate
                        ? pointToProgressVelocity(delta.x.translate, delta.y.translate, { x: (v[0] + v[1]) / 2, y: (v[2] + v[3]) / 2 })
                        : v;
                }
            }
            this.mixTargetDelta = (latest) => {
                const progress = latest / 1000;
                const point = interpolate?.(progress);
                const values = this.edgeValues;
                for (let i = 0; i < 2; i++) {
                    const axis = i ? "y" : "x";
                    values
                        ? edgesToDelta(targetDelta[axis], delta[axis], this.layout.layoutBox[axis], values[i * 2], values[i * 2 + 1])
                        : mixAxisDeltaLinear(targetDelta[axis], delta[axis], progress);
                }
                if (point) {
                    targetDelta.x.translate = point.x;
                    targetDelta.y.translate = point.y;
                }
                this.setTargetDelta(targetDelta);
                if (this.relativeTarget &&
                    this.relativeTargetOrigin &&
                    this.layout &&
                    this.relativeParent &&
                    this.relativeParent.layout) {
                    calcRelativePosition(relativeLayout, this.layout.layoutBox, this.relativeParent.layout.layoutBox, this.options.layoutAnchor || undefined);
                    const from = this.relativeTargetOrigin;
                    const to = relativeLayout;
                    /**
                     * A child mixes its place in its parent by progress,
                     * so a nested tree moves as one.
                     */
                    mixBox(this.relativeTarget, from, to, progress);
                    /**
                     * Springs are linear, so the child's own velocity adds
                     * the impulse response on top of the mix.
                     */
                    /**
                     * Only springs take velocity, so a tween skips this.
                     */
                    if (this.isSpringLayout) {
                        const box = this.relativeTarget;
                        const impulse = this.impulse || 0;
                        const start = this.relativeStart;
                        const now = time.now();
                        const dt = (now - this.relativeTime) / 1000;
                        this.relativeTime = now;
                        const speeds = (this.relativeVelocity || (this.relativeVelocity = [0, 0, 0, 0]));
                        for (let i = 0; i < 4; i++) {
                            const axis = i < 2 ? "x" : "y";
                            const edge = i & 1 ? "max" : "min";
                            if (start)
                                box[axis][edge] += start[i] * impulse;
                            if (prevRelativeTarget && dt > 0) {
                                speeds[i] =
                                    (box[axis][edge] -
                                        prevRelativeTarget[axis][edge]) /
                                        dt;
                            }
                        }
                    }
                    /**
                     * If this is an unchanged relative target we can consider the
                     * projection not dirty.
                     */
                    if (prevRelativeTarget &&
                        boxEquals(this.relativeTarget, prevRelativeTarget)) {
                        this.isProjectionDirty = false;
                    }
                    if (!prevRelativeTarget)
                        prevRelativeTarget = createBox();
                    copyBoxInto(prevRelativeTarget, this.relativeTarget);
                }
                if (isSharedLayoutAnimation) {
                    this.animationValues = mixedValues;
                    mixValues(mixedValues, snapshotLatestValues, this.latestValues, progress, shouldCrossfadeOpacity, isOnlyMember);
                }
                if (point?.rotate !== undefined) {
                    // Dedicated `pathRotation` channel, not `rotate`, so an
                    // animating `rotate` is composed with, never clobbered.
                    if (!this.animationValues)
                        this.animationValues = mixedValues;
                    this.animationValues.pathRotation = point.rotate;
                }
                this.root.scheduleUpdateProjection();
                this.scheduleRender();
                this.animationProgress = progress;
            };
            this.mixTargetDelta(this.options.layoutRoot ? 1000 : 0);
        }
        startAnimation(options) {
            this.notifyListeners("animationStart");
            const isSpring = (this.isSpringLayout = options.type === "spring");
            this.currentAnimation?.stop();
            this.resumingFrom?.currentAnimation?.stop();
            if (this.resumingFrom)
                this.resumingFrom.edgeValues = undefined;
            if (this.pendingAnimation) {
                cancelFrame(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 = frame.update(() => {
                globalProjectionState.hasAnimatedSinceResize = true;
                this.motionValue || (this.motionValue = motionValue(0));
                this.motionValue.jump(0, false);
                /**
                 * A duration spring ignores velocity, like in animate(), so
                 * it ends on time.
                 */
                const velocity = this.startVelocity;
                this.startVelocity = undefined;
                let transition = options;
                let mainVelocity = 0;
                if (isSpring) {
                    /**
                     * With mass: "auto", mass comes from the element's own
                     * size.
                     */
                    if (options.mass === "auto") {
                        const box = this.layout?.layoutBox;
                        transition = resolveAutoMass(options, box &&
                            Math.sqrt(calcLength(box.x) * calcLength(box.y)));
                    }
                    /**
                     * Only springs take a velocity. A tween keeps one
                     * driver, so its controls (pause, time) reach it all.
                     */
                    const relative = this.relativeStart;
                    if ((velocity || relative) && !options.repeat) {
                        if (typeof velocity === "number") {
                            mainVelocity = velocity;
                        }
                        else {
                            /**
                             * Each edge (x min, x max, y min, y max) gets its
                             * own spring in pixels, with its own start
                             * velocity. An edge that doesn't move keeps
                             * still, and a thrown element keeps its size.
                             * One generator steps them all with the main
                             * spring, so one animation drives them and its
                             * controls reach them all.
                             */
                            const edges = velocity && this.animationEdges;
                            transition = {
                                ...transition,
                                type: (o) => {
                                    const springs = [
                                        o.velocity,
                                        ...(velocity || []),
                                    ].map((v, i) => spring({
                                        ...o,
                                        keyframes: i
                                            ? [edges[i - 1], 0]
                                            : o.keyframes,
                                        velocity: v,
                                    }));
                                    const count = springs.length;
                                    /**
                                     * The response to 1000 px/s, for a
                                     * child's velocity inside its parent.
                                     * Coarse rest thresholds, because a
                                     * small offset of it is far below 1px.
                                     */
                                    relative &&
                                        springs.push(spring({
                                            ...o,
                                            keyframes: [0, 0],
                                            velocity: 1000,
                                            restDelta: 0.5,
                                            restSpeed: 5,
                                        }));
                                    let time = 0;
                                    const values = [];
                                    const generator = {
                                        calculatedDuration: null,
                                        next: (t) => {
                                            time = t;
                                            const state = springs[0].next(t);
                                            let done = state.done;
                                            for (let i = 1; i < springs.length; i++) {
                                                const { value, done: rest } = springs[i].next(t);
                                                if (i < count) {
                                                    values[i - 1] = value;
                                                }
                                                else {
                                                    this.impulse = value / 1000;
                                                }
                                                done = done && rest;
                                            }
                                            if (edges)
                                                this.edgeValues = values;
                                            state.done = done;
                                            return state;
                                        },
                                    };
                                    /**
                                     * Measure the duration now, then go back
                                     * to the start, so a delay shows the
                                     * start and not the end.
                                     */
                                    generator.calculatedDuration =
                                        calcGeneratorDuration(generator);
                                    generator.next(0);
                                    this.edgeVelocity = (i) => springs[i + 1].velocity(time);
                                    return generator;
                                },
                            };
                        }
                    }
                }
                const main = animateSingleValue(this.motionValue, [0, 1000], {
                    ...transition,
                    velocity: mainVelocity,
                    isSync: true,
                    onUpdate: (latest) => {
                        this.mixTargetDelta(latest);
                        options.onUpdate && options.onUpdate(latest);
                    },
                    onComplete: () => {
                        options.onComplete && options.onComplete();
                        this.completeAnimation();
                    },
                });
                this.currentAnimation = main;
                notifyLayoutAnimationStart(this.currentAnimation, this);
                if (this.resumingFrom) {
                    this.resumingFrom.currentAnimation = this.currentAnimation;
                }
                this.pendingAnimation = undefined;
            });
        }
        completeAnimation() {
            if (this.resumingFrom) {
                this.resumingFrom.currentAnimation = undefined;
                this.resumingFrom.preserveOpacity = undefined;
            }
            const stack = this.getStack();
            stack && stack.exitAnimationComplete();
            this.resumingFrom =
                this.currentAnimation =
                    this.animationValues =
                        this.edgeValues =
                            this.edgeVelocity =
                                this.impulse =
                                    undefined;
            this.notifyListeners("animationComplete");
        }
        finishAnimation() {
            if (this.currentAnimation) {
                this.edgeValues = this.impulse = undefined;
                this.mixTargetDelta?.(animationTarget);
                this.currentAnimation.stop();
            }
            this.completeAnimation();
        }
        applyTransformsToTarget() {
            const lead = this.getLead();
            const { targetWithTransforms, layout, latestValues } = lead;
            let { target } = lead;
            if (!targetWithTransforms || !target || !layout)
                return;
            /**
             * If we're only animating position, and this element isn't the lead element,
             * then instead of projecting into the lead box we instead want to calculate
             * a new target that aligns the two boxes but maintains the layout shape.
             */
            if (this !== lead &&
                this.layout &&
                layout &&
                shouldAnimatePositionOnly(this.options.animationType, this.layout.layoutBox, layout.layoutBox)) {
                const { layoutBox } = this.layout;
                const leadTarget = target;
                target = this.target || createBox();
                alignAxis(target.x, leadTarget.x, layoutBox.x);
                alignAxis(target.y, leadTarget.y, layoutBox.y);
            }
            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 layout layout
             * into the desired bounding box.
             */
            calcBoxDelta(this.projectionDeltaWithTransform, this.layoutCorrected, targetWithTransforms, latestValues);
        }
        registerSharedNode(layoutId, node) {
            if (!this.sharedNodes.has(layoutId)) {
                this.sharedNodes.set(layoutId, new NodeStack());
            }
            const stack = this.sharedNodes.get(layoutId);
            stack.add(node);
            const config = node.options.initialPromotionConfig;
            node.promote({
                transition: config?.transition,
                preserveFollowOpacity: config?.shouldPreserveFollowOpacity
                    ? config.shouldPreserveFollowOpacity(node)
                    : undefined,
            });
        }
        isLead() {
            const stack = this.getStack();
            return stack ? stack.lead === this : true;
        }
        getLead() {
            const { layoutId } = this.options;
            return layoutId ? this.getStack()?.lead || this : this;
        }
        getPrevLead() {
            const { layoutId } = this.options;
            return layoutId ? this.getStack()?.prevLead : undefined;
        }
        getStack() {
            const { layoutId } = this.options;
            if (layoutId)
                return this.root.sharedNodes.get(layoutId);
        }
        promote({ needsReset, transition, preserveFollowOpacity, } = {}) {
            this.getStack()?.promote(this, preserveFollowOpacity);
            if (needsReset) {
                this.projectionDelta = undefined;
                this.needsReset = true;
            }
            if (transition)
                this.setOptions({ transition });
        }
        relegate() {
            return this.getStack()?.relegate(this) || false;
        }
        resetSkewAndRotation() {
            const { visualElement } = this.options;
            if (!visualElement)
                return;
            /**
             * An unrolled check for rotation values. Most elements don't have any rotation and
             * skipping the loop and new object creation is 50% faster.
             * If there's no distorting values, we don't need to do any more.
             */
            const { latestValues } = visualElement;
            if (!hasDistortingTransform(latestValues))
                return;
            const resetValues = {};
            // Check the skew and rotate value of all axes and reset to 0
            for (let i = 0; i < distortingTransforms.length; i++) {
                resetDistortingTransform(distortingTransforms[i], visualElement, resetValues, this.animationValues);
            }
            for (let i = 0; i < angleTransforms.length; i++) {
                resetDistortingTransform(angleTransforms[i], visualElement, resetValues, this.animationValues);
            }
            // Force a render of this element to apply the transform with all skews and rotations
            // set to 0.
            visualElement.render();
            // Put back all the values we reset
            for (const key in resetValues) {
                visualElement.setStaticValue(key, resetValues[key]);
                if (this.animationValues) {
                    this.animationValues[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();
        }
        applyProjectionStyles(targetStyle, // CSSStyleDeclaration - doesn't allow numbers to be assigned to properties
        styleProp) {
            if (!this.instance || this.isSVG)
                return;
            const lead = this.getLead();
            /**
             * Without crossfade, a new lead hides the previous one, as it's
             * usually unmounting. One that gave its layoutId away keeps
             * rendering, so only hide nodes that follow another.
             */
            if (!this.isVisible && lead !== this) {
                targetStyle.visibility = "hidden";
                return;
            }
            const transformTemplate = this.getTransformTemplate();
            if (this.needsReset) {
                this.needsReset = false;
                targetStyle.visibility = "";
                targetStyle.opacity = "";
                targetStyle.pointerEvents =
                    resolveMotionValue(styleProp?.pointerEvents) || "";
                targetStyle.transform = transformTemplate
                    ? transformTemplate(this.latestValues, "")
                    : "none";
                return;
            }
            if (!this.projectionDelta || !this.layout || !lead.target) {
                if (this.options.layoutId || this.needsShow) {
                    this.needsShow = false;
                    targetStyle.visibility = "";
                    targetStyle.opacity =
                        this.latestValues.opacity !== undefined
                            ? this.latestValues.opacity
                            : 1;
                    targetStyle.pointerEvents =
                        resolveMotionValue(styleProp?.pointerEvents) || "";
                }
                if (this.hasProjected && !hasTransform(this.latestValues)) {
                    targetStyle.transform = transformTemplate
                        ? transformTemplate({}, "")
                        : "none";
                    this.hasProjected = false;
                }
                return;
            }
            targetStyle.visibility = "";
            const valuesToRender = lead.animationValues || lead.latestValues;
            this.applyTransformsToTarget();
            let transform = buildProjectionTransform(this.projectionDeltaWithTransform, this.treeScale, valuesToRender);
            if (transformTemplate) {
                transform = transformTemplate(valuesToRender, transform);
            }
            targetStyle.transform = transform;
            const { x, y } = this.projectionDelta;
            targetStyle.transformOrigin = `${x.origin * 100}% ${y.origin * 100}% 0`;
            if (lead.animationValues) {
                /**
                 * If the lead component is animating, assign this either the entering/leaving
                 * opacity
                 */
                targetStyle.opacity =
                    lead === this
                        ? valuesToRender.opacity ??
                            this.latestValues.opacity ??
                            1
                        : this.preserveOpacity
                            ? this.latestValues.opacity
                            : valuesToRender.opacityExit;
            }
            else {
                /**
                 * Or we're not animating at all, set the lead component to its layout
                 * opacity and other components to hidden.
                 */
                targetStyle.opacity =
                    lead === this
                        ? valuesToRender.opacity !== undefined
                            ? valuesToRender.opacity
                            : ""
                        : valuesToRender.opacityExit !== undefined
                            ? valuesToRender.opacityExit
                            : 0;
            }
            /**
             * Apply scale correction
             */
            for (const key in scaleCorrectors) {
                if (valuesToRender[key] === undefined)
                    continue;
                const { correct, applyTo, isCSSVariable } = scaleCorrectors[key];
                /**
                 * Only apply scale correction to the value if we have an
                 * active projection transform. Otherwise these values become
                 * vulnerable to distortion if the element changes size without
                 * a corresponding layout animation.
                 */
                const corrected = transform === "none"
                    ? valuesToRender[key]
                    : correct(valuesToRender[key], lead);
                if (applyTo) {
                    const num = applyTo.length;
                    for (let i = 0; i < num; i++) {
                        targetStyle[applyTo[i]] = corrected;
                    }
                }
                else {
                    // If this is a CSS variable, set it directly on the instance.
                    // Replacing this function from creating styles to setting them
                    // would be a good place to remove per frame object creation
                    if (isCSSVariable) {
                        this.options.visualElement.renderState.vars[key] = corrected;
                    }
                    else {
                        targetStyle[key] = corrected;
                    }
                }
            }
            /**
             * Disable pointer events on follow components. This is to ensure
             * that if a follow component covers a lead component it doesn't block
             * pointer events on the lead.
             */
            if (this.options.layoutId) {
                targetStyle.pointerEvents =
                    lead === this
                        ? resolveMotionValue(styleProp?.pointerEvents) || ""
                        : "none";
            }
        }
        clearSnapshot() {
            this.resumeFrom =
                this.snapshot =
                    this.dragSnapshot =
                        this.dragAncestor =
                            undefined;
        }
        // Only run on root
        resetTree() {
            this.root.nodes.forEach((node) => node.currentAnimation?.stop());
            this.root.nodes.forEach(clearMeasurements);
            this.root.sharedNodes.clear();
        }
    };
}
/**
 * Nodes are depth-ordered, so a dragged ancestor's layout is updated before
 * its descendants'.
 */
function updateLayout(node) {
    node.updateLayout();
    followDraggedAncestor(node);
}
function notifyLayoutUpdate(node) {
    const snapshot = node.resumeFrom?.snapshot || node.snapshot;
    if (node.isLead() &&
        node.layout &&
        snapshot &&
        node.hasListeners("didUpdate")) {
        const { layoutBox: layout, measuredBox: measuredLayout } = node.layout;
        const { animationType } = node.options;
        const isShared = snapshot.source !== node.layout.source;
        // TODO Maybe we want to also resize the layout snapshot so we don't trigger
        // animations for instance if layout="size" and an element has only changed position
        const resizeSnapshot = (box, to) => {
            if (animationType === "size") {
                eachAxis((axis) => {
                    const length = calcLength(box[axis]);
                    box[axis].min = to[axis].min;
                    box[axis].max = box[axis].min + length;
                });
            }
            else if (animationType === "x" || animationType === "y") {
                const snapAxis = animationType === "x" ? "y" : "x";
                copyAxisInto(box[snapAxis], to[snapAxis]);
            }
            else if (shouldAnimatePositionOnly(animationType, snapshot.layoutBox, layout)) {
                eachAxis((axis) => {
                    const length = calcLength(to[axis]);
                    box[axis].max = box[axis].min + length;
                    /**
                     * Ensure relative target gets resized and rerendererd
                     */
                    if (node.relativeTarget && !node.currentAnimation) {
                        node.isProjectionDirty = true;
                        node.relativeTarget[axis].max =
                            node.relativeTarget[axis].min + length;
                    }
                });
            }
        };
        const snapshotBox = isShared ? snapshot.measuredBox : snapshot.layoutBox;
        resizeSnapshot(snapshotBox, layout);
        const layoutDelta = createDelta();
        calcBoxDelta(layoutDelta, layout, snapshot.layoutBox);
        const visualDelta = createDelta();
        calcBoxDelta(visualDelta, isShared ? node.applyTransform(measuredLayout, true) : layout, snapshotBox);
        const hasLayoutChanged = !isDeltaZero(layoutDelta);
        let hasRelativeLayoutChanged = false;
        const relativeParent = node.getClosestProjectingParent();
        /**
         * A snapshot from another element is only relative to this node's
         * relativeParent if it's a layoutRoot that contains both. A node can
         * hold one without resumeFrom, when the element it came from changed
         * layoutId but stayed mounted (see switchLayoutId), or a remix
         * layout exit cleared it (see markLayoutExit).
         */
        if (!isShared ||
            (relativeParent?.options.layoutRoot &&
                node.resumeFrom?.path.includes(relativeParent))) {
            /**
             * If the relativeParent is itself resuming from a different element then
             * the relative snapshot is not relavent
             */
            if (relativeParent && !relativeParent.resumeFrom) {
                const { snapshot: parentSnapshot, layout: parentLayout } = relativeParent;
                if (parentSnapshot && parentLayout) {
                    const anchor = node.options.layoutAnchor || undefined;
                    /**
                     * A shared snapshot includes the transforms of the old
                     * element's ancestors below the layoutRoot, as they
                     * aren't shared with the new element.
                     */
                    const relativeSnapshot = createBox();
                    calcRelativePosition(relativeSnapshot, isShared
                        ? node.resumeFrom.applyTransform(snapshot.layoutBox, true, undefined, true, relativeParent.depth + 1)
                        : snapshot.layoutBox, parentSnapshot.layoutBox, anchor);
                    const relativeLayout = createBox();
                    calcRelativePosition(relativeLayout, layout, parentLayout.layoutBox, anchor);
                    /**
                     * A shared snapshot is resized in page space, above, so
                     * resize it relative to the layoutRoot too.
                     */
                    isShared && resizeSnapshot(relativeSnapshot, relativeLayout);
                    if (!boxEqualsRounded(relativeSnapshot, relativeLayout)) {
                        hasRelativeLayoutChanged = true;
                    }
                    if (relativeParent.options.layoutRoot) {
                        node.relativeTarget = relativeLayout;
                        node.relativeTargetOrigin = relativeSnapshot;
                        node.relativeParent = relativeParent;
                    }
                }
            }
        }
        node.notifyListeners("didUpdate", {
            layout,
            snapshot,
            delta: visualDelta,
            layoutDelta,
            hasLayoutChanged,
            hasRelativeLayoutChanged,
        });
    }
    else if (node.isLead()) {
        node.options.onExitComplete?.();
    }
    /**
     * Clearing transition
     * TODO: Investigate why this transition is being passed in as {type: false } from Framer
     * and why we need it at all
     */
    node.options.transition = undefined;
}
function propagateDirtyNodes(node) {
    /**
     * Increase debug counter for nodes encountered this frame
     */
    statsBuffer.countProjection?.(0); // nodes
    if (!node.parent)
        return;
    /**
     * If this node isn't projecting, propagate isProjectionDirty. It will have
     * no performance impact but it will allow the next child that *is* projecting
     * but *isn't* dirty to just check its parent to see if *any* ancestor needs
     * correcting.
     */
    if (!node.isProjecting()) {
        node.isProjectionDirty = node.parent.isProjectionDirty;
    }
    /**
     * Propagate isSharedProjectionDirty and isTransformDirty
     * throughout the whole tree. A future revision can take another look at
     * this but for safety we still recalcualte shared nodes.
     */
    node.isSharedProjectionDirty || (node.isSharedProjectionDirty = Boolean(node.isProjectionDirty ||
        node.parent.isProjectionDirty ||
        node.parent.isSharedProjectionDirty));
    node.isTransformDirty || (node.isTransformDirty = node.parent.isTransformDirty);
}
function cleanDirtyNodes(node) {
    node.isProjectionDirty =
        node.isSharedProjectionDirty =
            node.isTransformDirty =
                false;
}
function clearSnapshot(node) {
    node.clearSnapshot();
}
function clearMeasurements(node) {
    node.clearMeasurements();
}
function forceLayoutMeasure(node) {
    node.isLayoutDirty = true;
    node.updateLayout();
}
function clearIsLayoutDirty(node) {
    node.isLayoutDirty = false;
}
/**
 * A dragged node doesn't layout animate, so it jumps to its new layout. Shift
 * its descendants' snapshots by the same amount, so they only animate layout
 * changes relative to it, rather than lagging behind.
 *
 * If its drag ended since the snapshot (e.g. drag disabled from onDrag), it
 * layout animates again, so its descendants mustn't follow its jump.
 */
function followDraggedAncestor(node) {
    const { snapshot, dragAncestor } = node;
    const to = dragAncestor?.isAnimationBlocked && dragAncestor.layout;
    if (!snapshot || !to || node.resumeFrom || node.isAnimationBlocked)
        return;
    const from = dragAncestor.dragSnapshot;
    eachAxis((axis) => translateAxis(snapshot.layoutBox[axis], to.layoutBox[axis].min - from.layoutBox[axis].min));
}
/**
 * When a node is animation-blocked (e.g. during drag) and its component
 * didn't re-render (memoized), willUpdate() is never called so there's
 * no snapshot. Use the previous layout as a snapshot and mark dirty so
 * resetTransform/updateLayout/notifyLayoutUpdate process it normally.
 */
function ensureDraggedNodesSnapshotted(node) {
    if (node.isAnimationBlocked && node.layout && !node.isLayoutDirty) {
        node.snapshot = node.dragSnapshot || node.layout;
        node.isLayoutDirty = true;
    }
}
/**
 * If a node's relative parent is about to be re-measured but the node isn't
 * (e.g. it's in another LayoutGroup), its relative target is out of date.
 * If it's layout animating, re-measure it too, so its relative target keeps
 * resolving against current layouts. Otherwise its layout is still valid
 * relative to its parent, so drop the relative target, and any targetDelta
 * left over from a finished animation while an ancestor still animates, to be
 * rebuilt from its layout.
 */
function syncRelativeLayout(node) {
    if (node.relativeTarget && node.relativeParent?.isLayoutDirty) {
        if (node.currentAnimation) {
            node.isLayoutDirty = true;
        }
        else {
            node.targetDelta = undefined;
            node.removeRelativeTarget();
        }
    }
}
function resetTransformStyle(node) {
    const { visualElement } = node.options;
    if (visualElement?.getProps().onBeforeLayoutMeasure) {
        visualElement.notify("BeforeLayoutMeasure");
    }
    node.resetTransform();
}
function finishAnimation(node) {
    node.finishAnimation();
    node.targetDelta = node.relativeTarget = node.target = undefined;
    node.isProjectionDirty = true;
}
function resolveTargetDelta(node) {
    node.resolveTargetDelta();
}
function calcProjection(node) {
    node.calcProjection();
}
function resetSkewAndRotation(node) {
    node.resetSkewAndRotation();
}
/**
 * Build an axis delta that moves the layout's edges by these offsets (px).
 */
function edgesToDelta(output, delta, layout, min, max) {
    const length = layout.max - layout.min;
    const scale = length ? Math.max(0, (length + max - min) / length) : 1;
    const origin = (output.originPoint = delta.originPoint);
    output.origin = delta.origin;
    output.scale = scale;
    output.translate =
        layout.min + min - (origin + (layout.min - origin) * scale);
}
function mixAxisDeltaLinear(output, delta, p) {
    output.translate = mixNumber(delta.translate, 0, p);
    output.scale = mixNumber(delta.scale, 1, p);
    output.origin = delta.origin;
    output.originPoint = delta.originPoint;
}
function mixAxis(output, from, to, p) {
    output.min = mixNumber(from.min, to.min, p);
    output.max = mixNumber(from.max, to.max, p);
}
function mixBox(output, from, to, p) {
    mixAxis(output.x, from.x, to.x, p);
    mixAxis(output.y, from.y, to.y, p);
}
function hasOpacityCrossfade(node) {
    return node.animationValues?.opacityExit !== undefined;
}
const defaultLayoutTransition = {
    duration: 0.45,
    ease: [0.4, 0, 0.1, 1],
};
/**
 * Align an axis to the start of another, keeping the length of a layout axis.
 * The length is read first, as output can be the layout axis itself.
 */
function alignAxis(output, from, layout) {
    output.max = calcLength(layout) + (output.min = from.min);
}
function shouldAnimatePositionOnly(animationType, snapshot, layout) {
    return (animationType === "position" ||
        (animationType === "preserve-aspect" &&
            !isNear(aspectRatio(snapshot), aspectRatio(layout), 0.2)));
}
function checkNodeWasScrollRoot(node) {
    return node !== node.root && node.scroll?.wasRoot;
}

export { cleanDirtyNodes, createProjectionNode, mixAxis, mixBox, propagateDirtyNodes };
//# sourceMappingURL=create-projection-node.mjs.map