BOC v1.0: RS256 auth, Ledger integration, Prometheus metrics, CI/CD, backup
This commit is contained in:
Generated
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
import { addDomEvent } from '../../events/add-dom-event.mjs';
|
||||
import { createProjectionNode } from './create-projection-node.mjs';
|
||||
|
||||
const DocumentProjectionNode = createProjectionNode({
|
||||
attachResizeListener: (ref, notify) => addDomEvent(ref, "resize", notify),
|
||||
measureScroll: () => ({
|
||||
x: document.documentElement.scrollLeft || document.body?.scrollLeft || 0,
|
||||
y: document.documentElement.scrollTop || document.body?.scrollTop || 0,
|
||||
}),
|
||||
checkIsScrollRoot: () => true,
|
||||
});
|
||||
|
||||
export { DocumentProjectionNode };
|
||||
//# sourceMappingURL=DocumentProjectionNode.mjs.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"DocumentProjectionNode.mjs","sources":["../../../../src/projection/node/DocumentProjectionNode.ts"],"sourcesContent":["import { addDomEvent } from \"../../events/add-dom-event\"\nimport { createProjectionNode } from \"./create-projection-node\"\n\nexport const DocumentProjectionNode = createProjectionNode<Window>({\n attachResizeListener: (\n ref: Window | Element,\n notify: VoidFunction\n ): VoidFunction => addDomEvent(ref, \"resize\", notify),\n measureScroll: () => ({\n x: document.documentElement.scrollLeft || document.body?.scrollLeft || 0,\n y: document.documentElement.scrollTop || document.body?.scrollTop || 0,\n }),\n checkIsScrollRoot: () => true,\n})\n"],"names":[],"mappings":";;;AAGO,MAAM,sBAAsB,GAAG,oBAAoB,CAAS;AAC/D,IAAA,oBAAoB,EAAE,CAClB,GAAqB,EACrB,MAAoB,KACL,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAC;AACrD,IAAA,aAAa,EAAE,OAAO;AAClB,QAAA,CAAC,EAAE,QAAQ,CAAC,eAAe,CAAC,UAAU,IAAI,QAAQ,CAAC,IAAI,EAAE,UAAU,IAAI,CAAC;AACxE,QAAA,CAAC,EAAE,QAAQ,CAAC,eAAe,CAAC,SAAS,IAAI,QAAQ,CAAC,IAAI,EAAE,SAAS,IAAI,CAAC;KACzE,CAAC;AACF,IAAA,iBAAiB,EAAE,MAAM,IAAI;AAChC,CAAA;;;;"}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { createProjectionNode } from './create-projection-node.mjs';
|
||||
import { DocumentProjectionNode } from './DocumentProjectionNode.mjs';
|
||||
|
||||
const rootProjectionNode = {
|
||||
current: undefined,
|
||||
};
|
||||
const HTMLProjectionNode = createProjectionNode({
|
||||
measureScroll: (instance) => ({
|
||||
x: instance.scrollLeft,
|
||||
y: instance.scrollTop,
|
||||
}),
|
||||
defaultParent: () => {
|
||||
if (!rootProjectionNode.current) {
|
||||
const documentNode = new DocumentProjectionNode({});
|
||||
documentNode.mount(window);
|
||||
documentNode.setOptions({ layoutScroll: true });
|
||||
rootProjectionNode.current = documentNode;
|
||||
}
|
||||
return rootProjectionNode.current;
|
||||
},
|
||||
resetTransform: (instance, value) => {
|
||||
instance.style.transform = value !== undefined ? value : "none";
|
||||
},
|
||||
checkIsScrollRoot: (instance) => Boolean(window.getComputedStyle(instance).position === "fixed"),
|
||||
});
|
||||
|
||||
export { HTMLProjectionNode, rootProjectionNode };
|
||||
//# sourceMappingURL=HTMLProjectionNode.mjs.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"HTMLProjectionNode.mjs","sources":["../../../../src/projection/node/HTMLProjectionNode.ts"],"sourcesContent":["import { createProjectionNode } from \"./create-projection-node\"\nimport { DocumentProjectionNode } from \"./DocumentProjectionNode\"\nimport { IProjectionNode } from \"./types\"\n\nexport const rootProjectionNode: { current: IProjectionNode | undefined } = {\n current: undefined,\n}\n\nexport const HTMLProjectionNode = createProjectionNode<HTMLElement>({\n measureScroll: (instance) => ({\n x: instance.scrollLeft,\n y: instance.scrollTop,\n }),\n defaultParent: () => {\n if (!rootProjectionNode.current) {\n const documentNode = new DocumentProjectionNode({})\n documentNode.mount(window)\n documentNode.setOptions({ layoutScroll: true })\n rootProjectionNode.current = documentNode\n }\n return rootProjectionNode.current\n },\n resetTransform: (instance, value) => {\n instance.style.transform = value !== undefined ? value : \"none\"\n },\n checkIsScrollRoot: (instance) =>\n Boolean(window.getComputedStyle(instance).position === \"fixed\"),\n})\n"],"names":[],"mappings":";;;AAIO,MAAM,kBAAkB,GAA6C;AACxE,IAAA,OAAO,EAAE,SAAS;;AAGf,MAAM,kBAAkB,GAAG,oBAAoB,CAAc;AAChE,IAAA,aAAa,EAAE,CAAC,QAAQ,MAAM;QAC1B,CAAC,EAAE,QAAQ,CAAC,UAAU;QACtB,CAAC,EAAE,QAAQ,CAAC,SAAS;KACxB,CAAC;IACF,aAAa,EAAE,MAAK;AAChB,QAAA,IAAI,CAAC,kBAAkB,CAAC,OAAO,EAAE;AAC7B,YAAA,MAAM,YAAY,GAAG,IAAI,sBAAsB,CAAC,EAAE,CAAC;AACnD,YAAA,YAAY,CAAC,KAAK,CAAC,MAAM,CAAC;YAC1B,YAAY,CAAC,UAAU,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;AAC/C,YAAA,kBAAkB,CAAC,OAAO,GAAG,YAAY;QAC7C;QACA,OAAO,kBAAkB,CAAC,OAAO;IACrC,CAAC;AACD,IAAA,cAAc,EAAE,CAAC,QAAQ,EAAE,KAAK,KAAI;AAChC,QAAA,QAAQ,CAAC,KAAK,CAAC,SAAS,GAAG,KAAK,KAAK,SAAS,GAAG,KAAK,GAAG,MAAM;IACnE,CAAC;AACD,IAAA,iBAAiB,EAAE,CAAC,QAAQ,KACxB,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC;AACtE,CAAA;;;;"}
|
||||
Generated
Vendored
+1695
@@ -0,0 +1,1695 @@
|
||||
import { SubscriptionManager, clamp, noop } from 'motion-utils';
|
||||
import { animateSingleValue } from '../../animation/animate/single-value.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 { 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 { 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 { createBox, createDelta } from '../geometry/models.mjs';
|
||||
import { boxEqualsRounded, isDeltaZero, axisDeltaEquals, boxEquals, aspectRatio } from '../geometry/utils.mjs';
|
||||
import { NodeStack } from '../shared/stack.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 } from '../utils/has-transform.mjs';
|
||||
import { globalProjectionState } from './state.mjs';
|
||||
import { frame, cancelFrame, frameData, frameSteps } from '../../frameloop/frame.mjs';
|
||||
|
||||
const metrics = {
|
||||
nodes: 0,
|
||||
calculatedTargetDeltas: 0,
|
||||
calculatedProjections: 0,
|
||||
};
|
||||
const transformAxes = ["", "X", "Y", "Z"];
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 createProjectionNode({ attachResizeListener, defaultParent, measureScroll, checkIsScrollRoot, resetTransform, }) {
|
||||
return class ProjectionNode {
|
||||
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 calculcated projection transforms, as these are
|
||||
* calculated in screen-space but need to be scaled for elements to layoutly
|
||||
* make it to their calculated destinations.
|
||||
*
|
||||
* TODO: Lazy-init
|
||||
*/
|
||||
this.treeScale = { x: 1, y: 1 };
|
||||
/**
|
||||
*
|
||||
*/
|
||||
this.eventHandlers = new Map();
|
||||
this.hasTreeAnimated = false;
|
||||
this.layoutVersion = 0;
|
||||
// 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;
|
||||
/**
|
||||
* Reset debug counts. Manually resetting rather than creating a new
|
||||
* object each frame.
|
||||
*/
|
||||
if (statsBuffer.value) {
|
||||
metrics.nodes =
|
||||
metrics.calculatedTargetDeltas =
|
||||
metrics.calculatedProjections =
|
||||
0;
|
||||
}
|
||||
this.nodes.forEach(propagateDirtyNodes);
|
||||
this.nodes.forEach(resolveTargetDelta);
|
||||
this.nodes.forEach(calcProjection);
|
||||
this.nodes.forEach(cleanDirtyNodes);
|
||||
if (statsBuffer.addProjectionMetrics) {
|
||||
statsBuffer.addProjectionMetrics(metrics);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Frame calculations
|
||||
*/
|
||||
this.resolvedRelativeTargetAt = 0.0;
|
||||
this.linkedParentVersion = 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) {
|
||||
const subscriptionManager = this.eventHandlers.get(name);
|
||||
subscriptionManager && subscriptionManager.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 && 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();
|
||||
cancelDelay = delay(resizeUnblockUpdate, 250);
|
||||
if (globalProjectionState.hasAnimatedSinceResize) {
|
||||
globalProjectionState.hasAnimatedSinceResize = false;
|
||||
this.nodes.forEach(finishAnimation);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (layoutId) {
|
||||
this.root.registerSharedNode(layoutId, this);
|
||||
}
|
||||
// Only register the handler if it requires layout animation
|
||||
if (this.options.animate !== false &&
|
||||
visualElement &&
|
||||
(layoutId || layout)) {
|
||||
this.addEventListener("didUpdate", ({ delta, hasLayoutChanged, hasRelativeLayoutChanged, layout: newLayout, }) => {
|
||||
if (this.isTreeAnimationBlocked()) {
|
||||
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);
|
||||
}
|
||||
if (this.isLead() && this.options.onExitComplete) {
|
||||
this.options.onExitComplete();
|
||||
}
|
||||
}
|
||||
this.targetLayout = newLayout;
|
||||
});
|
||||
}
|
||||
}
|
||||
unmount() {
|
||||
this.options.layoutId && this.willUpdate();
|
||||
this.root.nodes.remove(this);
|
||||
const stack = this.getStack();
|
||||
stack && stack.remove(this);
|
||||
this.parent && this.parent.children.delete(this);
|
||||
this.instance = undefined;
|
||||
this.eventHandlers.clear();
|
||||
cancelFrame(this.updateProjection);
|
||||
}
|
||||
// only on the root
|
||||
blockUpdate() {
|
||||
this.updateManuallyBlocked = true;
|
||||
}
|
||||
unblockUpdate() {
|
||||
this.updateManuallyBlocked = false;
|
||||
}
|
||||
isUpdateBlocked() {
|
||||
return this.updateManuallyBlocked || this.updateBlockedByResize;
|
||||
}
|
||||
isTreeAnimationBlocked() {
|
||||
return (this.isAnimationBlocked ||
|
||||
(this.parent && this.parent.isTreeAnimationBlocked()) ||
|
||||
false);
|
||||
}
|
||||
// Note: currently only running on root node
|
||||
startUpdate() {
|
||||
if (this.isUpdateBlocked())
|
||||
return;
|
||||
this.isUpdating = true;
|
||||
this.nodes && 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 && 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;
|
||||
this.isLayoutDirty = true;
|
||||
for (let i = 0; i < this.path.length; i++) {
|
||||
const node = this.path[i];
|
||||
node.shouldResetTransform = true;
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
/**
|
||||
* 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);
|
||||
this.sharedNodes.forEach(removeLeadSnapshots);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
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);
|
||||
this.layoutVersion++;
|
||||
if (!this.layoutCorrected)
|
||||
this.layoutCorrected = createBox();
|
||||
this.isLayoutDirty = false;
|
||||
this.projectionDelta = undefined;
|
||||
this.notifyListeners("measure", this.layout.layoutBox);
|
||||
const { visualElement } = this.options;
|
||||
visualElement &&
|
||||
visualElement.notify("LayoutMeasure", this.layout.layoutBox, prevLayout ? prevLayout.layoutBox : undefined);
|
||||
}
|
||||
updateScroll(phase = "measure") {
|
||||
let needsMeasurement = Boolean(this.options.layoutScroll && this.instance);
|
||||
if (this.scroll &&
|
||||
this.scroll.animationId === this.root.animationId &&
|
||||
this.scroll.phase === phase) {
|
||||
needsMeasurement = false;
|
||||
}
|
||||
if (needsMeasurement && this.instance) {
|
||||
const isRoot = checkIsScrollRoot(this.instance);
|
||||
this.scroll = {
|
||||
animationId: this.root.animationId,
|
||||
phase,
|
||||
isRoot,
|
||||
offset: measureScroll(this.instance),
|
||||
wasRoot: this.scroll ? this.scroll.isRoot : isRoot,
|
||||
};
|
||||
}
|
||||
}
|
||||
resetTransform() {
|
||||
if (!resetTransform)
|
||||
return;
|
||||
const isResetRequested = this.isLayoutDirty ||
|
||||
this.shouldResetTransform ||
|
||||
this.options.alwaysMeasureLayout;
|
||||
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) {
|
||||
const withTransforms = output || createBox();
|
||||
copyBoxInto(withTransforms, box);
|
||||
for (let i = 0; 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 (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,
|
||||
};
|
||||
}
|
||||
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 (relativeParent &&
|
||||
this.linkedParentVersion !== relativeParent.layoutVersion &&
|
||||
!relativeParent.options.layoutRoot) {
|
||||
this.removeRelativeTarget();
|
||||
}
|
||||
/**
|
||||
* 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)) {
|
||||
this.applyTransform(this.layout.layoutBox, false, this.target);
|
||||
}
|
||||
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 &&
|
||||
Boolean(relativeParent.resumingFrom) ===
|
||||
Boolean(this.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
|
||||
*/
|
||||
if (statsBuffer.value) {
|
||||
metrics.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.linkedParentVersion = relativeParent.layoutVersion;
|
||||
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 && 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.
|
||||
*/
|
||||
applyTreeDeltas(this.layoutCorrected, this.treeScale, this.path, isShared);
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
if (statsBuffer.value) {
|
||||
metrics.calculatedProjections++;
|
||||
}
|
||||
}
|
||||
hide() {
|
||||
this.isVisible = false;
|
||||
// TODO: Schedule render
|
||||
}
|
||||
show() {
|
||||
this.isVisible = true;
|
||||
// TODO: Schedule render
|
||||
}
|
||||
scheduleRender(notifyAll = true) {
|
||||
this.options.visualElement?.scheduleRender();
|
||||
if (notifyAll) {
|
||||
const stack = this.getStack();
|
||||
stack && stack.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 snapshotSource = snapshot ? snapshot.source : undefined;
|
||||
const layoutSource = this.layout ? this.layout.source : undefined;
|
||||
const isSharedLayoutAnimation = snapshotSource !== layoutSource;
|
||||
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);
|
||||
this.mixTargetDelta = (latest) => {
|
||||
const progress = latest / 1000;
|
||||
const point = interpolate?.(progress);
|
||||
if (point) {
|
||||
targetDelta.x.translate = point.x;
|
||||
targetDelta.x.scale = mixNumber(delta.x.scale, 1, progress);
|
||||
targetDelta.x.origin = delta.x.origin;
|
||||
targetDelta.x.originPoint = delta.x.originPoint;
|
||||
targetDelta.y.translate = point.y;
|
||||
targetDelta.y.scale = mixNumber(delta.y.scale, 1, progress);
|
||||
targetDelta.y.origin = delta.y.origin;
|
||||
targetDelta.y.originPoint = delta.y.originPoint;
|
||||
}
|
||||
else {
|
||||
mixAxisDeltaLinear(targetDelta.x, delta.x, progress);
|
||||
mixAxisDeltaLinear(targetDelta.y, delta.y, progress);
|
||||
}
|
||||
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);
|
||||
mixBox(this.relativeTarget, this.relativeTargetOrigin, relativeLayout, progress);
|
||||
/**
|
||||
* 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 && 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");
|
||||
this.currentAnimation?.stop();
|
||||
this.resumingFrom?.currentAnimation?.stop();
|
||||
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);
|
||||
this.currentAnimation = animateSingleValue(this.motionValue, [0, 1000], {
|
||||
...options,
|
||||
velocity: 0,
|
||||
isSync: true,
|
||||
onUpdate: (latest) => {
|
||||
this.mixTargetDelta(latest);
|
||||
options.onUpdate && options.onUpdate(latest);
|
||||
},
|
||||
onComplete: () => {
|
||||
options.onComplete && options.onComplete();
|
||||
this.completeAnimation();
|
||||
},
|
||||
});
|
||||
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 =
|
||||
undefined;
|
||||
this.notifyListeners("animationComplete");
|
||||
}
|
||||
finishAnimation() {
|
||||
if (this.currentAnimation) {
|
||||
this.mixTargetDelta && this.mixTargetDelta(animationTarget);
|
||||
this.currentAnimation.stop();
|
||||
}
|
||||
this.completeAnimation();
|
||||
}
|
||||
applyTransformsToTarget() {
|
||||
const lead = this.getLead();
|
||||
let { targetWithTransforms, target, layout, latestValues } = 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)) {
|
||||
target = this.target || createBox();
|
||||
const xLength = calcLength(this.layout.layoutBox.x);
|
||||
target.x.min = lead.target.x.min;
|
||||
target.x.max = target.x.min + xLength;
|
||||
const yLength = calcLength(this.layout.layoutBox.y);
|
||||
target.y.min = lead.target.y.min;
|
||||
target.y.max = target.y.min + yLength;
|
||||
}
|
||||
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 ? config.transition : undefined,
|
||||
preserveFollowOpacity: config && 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, } = {}) {
|
||||
const stack = this.getStack();
|
||||
if (stack)
|
||||
stack.promote(this, preserveFollowOpacity);
|
||||
if (needsReset) {
|
||||
this.projectionDelta = undefined;
|
||||
this.needsReset = true;
|
||||
}
|
||||
if (transition)
|
||||
this.setOptions({ transition });
|
||||
}
|
||||
relegate() {
|
||||
const stack = this.getStack();
|
||||
if (stack) {
|
||||
return stack.relegate(this);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
resetSkewAndRotation() {
|
||||
const { visualElement } = this.options;
|
||||
if (!visualElement)
|
||||
return;
|
||||
// If there's no detected skew or rotation values, we can early return without a forced render.
|
||||
let hasDistortingTransform = false;
|
||||
/**
|
||||
* An unrolled check for rotation values. Most elements don't have any rotation and
|
||||
* skipping the nested loop and new object creation is 50% faster.
|
||||
*/
|
||||
const { latestValues } = visualElement;
|
||||
if (latestValues.z ||
|
||||
latestValues.rotate ||
|
||||
latestValues.rotateX ||
|
||||
latestValues.rotateY ||
|
||||
latestValues.rotateZ ||
|
||||
latestValues.skewX ||
|
||||
latestValues.skewY) {
|
||||
hasDistortingTransform = true;
|
||||
}
|
||||
// If there's no distorting values, we don't need to do any more.
|
||||
if (!hasDistortingTransform)
|
||||
return;
|
||||
const resetValues = {};
|
||||
if (latestValues.z) {
|
||||
resetDistortingTransform("z", visualElement, resetValues, this.animationValues);
|
||||
}
|
||||
// Check the skew and rotate value of all axes and reset to 0
|
||||
for (let i = 0; i < transformAxes.length; i++) {
|
||||
resetDistortingTransform(`rotate${transformAxes[i]}`, visualElement, resetValues, this.animationValues);
|
||||
resetDistortingTransform(`skew${transformAxes[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;
|
||||
if (!this.isVisible) {
|
||||
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;
|
||||
}
|
||||
const lead = this.getLead();
|
||||
if (!this.projectionDelta || !this.layout || !lead.target) {
|
||||
if (this.options.layoutId) {
|
||||
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 = undefined;
|
||||
}
|
||||
// Only run on root
|
||||
resetTree() {
|
||||
this.root.nodes.forEach((node) => node.currentAnimation?.stop());
|
||||
this.root.nodes.forEach(clearMeasurements);
|
||||
this.root.sharedNodes.clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
function updateLayout(node) {
|
||||
node.updateLayout();
|
||||
}
|
||||
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
|
||||
if (animationType === "size") {
|
||||
eachAxis((axis) => {
|
||||
const axisSnapshot = isShared
|
||||
? snapshot.measuredBox[axis]
|
||||
: snapshot.layoutBox[axis];
|
||||
const length = calcLength(axisSnapshot);
|
||||
axisSnapshot.min = layout[axis].min;
|
||||
axisSnapshot.max = axisSnapshot.min + length;
|
||||
});
|
||||
}
|
||||
else if (animationType === "x" || animationType === "y") {
|
||||
const snapAxis = animationType === "x" ? "y" : "x";
|
||||
copyAxisInto(isShared
|
||||
? snapshot.measuredBox[snapAxis]
|
||||
: snapshot.layoutBox[snapAxis], layout[snapAxis]);
|
||||
}
|
||||
else if (shouldAnimatePositionOnly(animationType, snapshot.layoutBox, layout)) {
|
||||
eachAxis((axis) => {
|
||||
const axisSnapshot = isShared
|
||||
? snapshot.measuredBox[axis]
|
||||
: snapshot.layoutBox[axis];
|
||||
const length = calcLength(layout[axis]);
|
||||
axisSnapshot.max = axisSnapshot.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 layoutDelta = createDelta();
|
||||
calcBoxDelta(layoutDelta, layout, snapshot.layoutBox);
|
||||
const visualDelta = createDelta();
|
||||
if (isShared) {
|
||||
calcBoxDelta(visualDelta, node.applyTransform(measuredLayout, true), snapshot.measuredBox);
|
||||
}
|
||||
else {
|
||||
calcBoxDelta(visualDelta, layout, snapshot.layoutBox);
|
||||
}
|
||||
const hasLayoutChanged = !isDeltaZero(layoutDelta);
|
||||
let hasRelativeLayoutChanged = false;
|
||||
if (!node.resumeFrom) {
|
||||
const relativeParent = node.getClosestProjectingParent();
|
||||
/**
|
||||
* 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;
|
||||
const relativeSnapshot = createBox();
|
||||
calcRelativePosition(relativeSnapshot, snapshot.layoutBox, parentSnapshot.layoutBox, anchor);
|
||||
const relativeLayout = createBox();
|
||||
calcRelativePosition(relativeLayout, layout, parentLayout.layoutBox, anchor);
|
||||
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()) {
|
||||
const { onExitComplete } = node.options;
|
||||
onExitComplete && 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
|
||||
*/
|
||||
if (statsBuffer.value) {
|
||||
metrics.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;
|
||||
}
|
||||
/**
|
||||
* 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.layout;
|
||||
node.isLayoutDirty = true;
|
||||
}
|
||||
}
|
||||
function resetTransformStyle(node) {
|
||||
const { visualElement } = node.options;
|
||||
if (visualElement && 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();
|
||||
}
|
||||
function removeLeadSnapshots(stack) {
|
||||
stack.removeLeadSnapshot();
|
||||
}
|
||||
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 && node.animationValues.opacityExit !== undefined);
|
||||
}
|
||||
const defaultLayoutTransition = {
|
||||
duration: 0.45,
|
||||
ease: [0.4, 0, 0.1, 1],
|
||||
};
|
||||
const userAgentContains = (string) => typeof navigator !== "undefined" &&
|
||||
navigator.userAgent &&
|
||||
navigator.userAgent.toLowerCase().includes(string);
|
||||
/**
|
||||
* Measured bounding boxes must be rounded in Safari and
|
||||
* left untouched in Chrome, otherwise non-integer layouts within scaled-up elements
|
||||
* can appear to jump.
|
||||
*/
|
||||
const roundPoint = userAgentContains("applewebkit/") && !userAgentContains("chrome/")
|
||||
? Math.round
|
||||
: noop;
|
||||
function roundAxis(axis) {
|
||||
// Round to the nearest .5 pixels to support subpixel layouts
|
||||
axis.min = roundPoint(axis.min);
|
||||
axis.max = roundPoint(axis.max);
|
||||
}
|
||||
function roundBox(box) {
|
||||
roundAxis(box.x);
|
||||
roundAxis(box.y);
|
||||
}
|
||||
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
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
+25
@@ -0,0 +1,25 @@
|
||||
const notify = (node) => !node.isLayoutDirty && node.willUpdate(false);
|
||||
function nodeGroup() {
|
||||
const nodes = new Set();
|
||||
const subscriptions = new WeakMap();
|
||||
const dirtyAll = () => nodes.forEach(notify);
|
||||
return {
|
||||
add: (node) => {
|
||||
nodes.add(node);
|
||||
subscriptions.set(node, node.addEventListener("willUpdate", dirtyAll));
|
||||
},
|
||||
remove: (node) => {
|
||||
nodes.delete(node);
|
||||
const unsubscribe = subscriptions.get(node);
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
subscriptions.delete(node);
|
||||
}
|
||||
dirtyAll();
|
||||
},
|
||||
dirty: dirtyAll,
|
||||
};
|
||||
}
|
||||
|
||||
export { nodeGroup };
|
||||
//# sourceMappingURL=group.mjs.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"group.mjs","sources":["../../../../src/projection/node/group.ts"],"sourcesContent":["import { IProjectionNode } from \"./types\"\n\nconst notify = (node: IProjectionNode) =>\n !node.isLayoutDirty && node.willUpdate(false)\n\nexport interface NodeGroup {\n add: (node: IProjectionNode) => void\n remove: (node: IProjectionNode) => void\n dirty: VoidFunction\n}\n\nexport function nodeGroup(): NodeGroup {\n const nodes = new Set<IProjectionNode>()\n const subscriptions = new WeakMap<IProjectionNode, () => void>()\n\n const dirtyAll = () => nodes.forEach(notify)\n\n return {\n add: (node) => {\n nodes.add(node)\n subscriptions.set(\n node,\n node.addEventListener(\"willUpdate\", dirtyAll)\n )\n },\n remove: (node) => {\n nodes.delete(node)\n const unsubscribe = subscriptions.get(node)\n if (unsubscribe) {\n unsubscribe()\n subscriptions.delete(node)\n }\n dirtyAll()\n },\n dirty: dirtyAll,\n }\n}\n"],"names":[],"mappings":"AAEA,MAAM,MAAM,GAAG,CAAC,IAAqB,KACjC,CAAC,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;SAQjC,SAAS,GAAA;AACrB,IAAA,MAAM,KAAK,GAAG,IAAI,GAAG,EAAmB;AACxC,IAAA,MAAM,aAAa,GAAG,IAAI,OAAO,EAA+B;IAEhE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;IAE5C,OAAO;AACH,QAAA,GAAG,EAAE,CAAC,IAAI,KAAI;AACV,YAAA,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;AACf,YAAA,aAAa,CAAC,GAAG,CACb,IAAI,EACJ,IAAI,CAAC,gBAAgB,CAAC,YAAY,EAAE,QAAQ,CAAC,CAChD;QACL,CAAC;AACD,QAAA,MAAM,EAAE,CAAC,IAAI,KAAI;AACb,YAAA,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC;YAClB,MAAM,WAAW,GAAG,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC;YAC3C,IAAI,WAAW,EAAE;AACb,gBAAA,WAAW,EAAE;AACb,gBAAA,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC;YAC9B;AACA,YAAA,QAAQ,EAAE;QACd,CAAC;AACD,QAAA,KAAK,EAAE,QAAQ;KAClB;AACL;;;;"}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* This should only ever be modified on the client otherwise it'll
|
||||
* persist through server requests. If we need instanced states we
|
||||
* could lazy-init via root.
|
||||
*/
|
||||
const globalProjectionState = {
|
||||
/**
|
||||
* Global flag as to whether the tree has animated since the last time
|
||||
* we resized the window
|
||||
*/
|
||||
hasAnimatedSinceResize: true,
|
||||
/**
|
||||
* We set this to true once, on the first update. Any nodes added to the tree beyond that
|
||||
* update will be given a `data-projection-id` attribute.
|
||||
*/
|
||||
hasEverUpdated: false,
|
||||
};
|
||||
|
||||
export { globalProjectionState };
|
||||
//# sourceMappingURL=state.mjs.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"state.mjs","sources":["../../../../src/projection/node/state.ts"],"sourcesContent":["/**\n * This should only ever be modified on the client otherwise it'll\n * persist through server requests. If we need instanced states we\n * could lazy-init via root.\n */\nexport const globalProjectionState = {\n /**\n * Global flag as to whether the tree has animated since the last time\n * we resized the window\n */\n hasAnimatedSinceResize: true,\n\n /**\n * We set this to true once, on the first update. Any nodes added to the tree beyond that\n * update will be given a `data-projection-id` attribute.\n */\n hasEverUpdated: false,\n}\n"],"names":[],"mappings":"AAAA;;;;AAIG;AACI,MAAM,qBAAqB,GAAG;AACjC;;;AAGG;AACH,IAAA,sBAAsB,EAAE,IAAI;AAE5B;;;AAGG;AACH,IAAA,cAAc,EAAE,KAAK;;;;;"}
|
||||
Reference in New Issue
Block a user