: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
if (!(this.options.alwaysMeasureLayout && this.isLead()) &&
* 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
if (this.resumeFrom && !this.resumeFrom.instance) {
for (let i = 0; i < this.path.length; i++) {
const node = this.path[i];
const prevLayout = this.layout;
this.layout = this.measure(false);
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 ? prevLayout.layoutBox : undefined);
updateScroll(phase = "measure") {
let needsMeasurement = Boolean(this.options.layoutScroll && this.instance);
this.scroll.animationId === this.root.animationId &&
this.scroll.phase === phase) {
needsMeasurement = false;
animationId: this.root.animationId,
isRoot: checkIsScrollRoot(this.instance),
offset: measureScroll(this.instance),
const isResetRequested = this.isLayoutDirty || this.shouldResetTransform;
const hasProjection = this.projectionDelta && !isDeltaZero(this.projectionDelta);
const transformTemplate = this.getTransformTemplate();
const transformTemplateValue = transformTemplate
? transformTemplate(this.latestValues, "")
const transformTemplateHasChanged = transformTemplateValue !== this.prevTransformTemplateValue;
hasTransform(this.latestValues) ||
transformTemplateHasChanged)) {
resetTransform(this.instance, transformTemplateValue);
this.shouldResetTransform = false;
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
layoutBox = this.removeTransform(layoutBox);
animationId: this.root.animationId,
const { visualElement } = this.options;
const box = visualElement.measureViewportBox();
// Remove viewport scroll to give page-relative coordinates
const { scroll } = this.root;
translateAxis(box.x, scroll.offset.x);
translateAxis(box.y, scroll.offset.y);
removeElementScroll(box) {
const boxWithoutScroll = createBox();
copyBoxInto(boxWithoutScroll, box);
* 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
copyBoxInto(boxWithoutScroll, box);
const { scroll: rootScroll } = this.root;
* Undo the application of page scroll that was originally added
* to the measured bounding box.
translateAxis(boxWithoutScroll.x, -rootScroll.offset.x);
translateAxis(boxWithoutScroll.y, -rootScroll.offset.y);
translateAxis(boxWithoutScroll.x, scroll.offset.x);
translateAxis(boxWithoutScroll.y, scroll.offset.y);
applyTransform(box, transformOnly = false) {
const withTransforms = createBox();
copyBoxInto(withTransforms, box);
for (let i = 0; i < this.path.length; i++) {
const node = this.path[i];
node.options.layoutScroll &&
transformBox(withTransforms, {
x: -node.scroll.offset.x,
y: -node.scroll.offset.y,
if (!hasTransform(node.latestValues))
transformBox(withTransforms, node.latestValues);
if (hasTransform(this.latestValues)) {
transformBox(withTransforms, this.latestValues);
const boxWithoutTransform = createBox();
copyBoxInto(boxWithoutTransform, box);
for (let i = 0; i < this.path.length; i++) {
const node = this.path[i];
if (!hasTransform(node.latestValues))
hasScale(node.latestValues) && node.updateSnapshot();
const sourceBox = createBox();
const nodeBox = node.measurePageBox();
copyBoxInto(sourceBox, nodeBox);
removeBoxTransforms(boxWithoutTransform, node.latestValues, node.snapshot ? node.snapshot.layoutBox : undefined, sourceBox);
if (hasTransform(this.latestValues)) {
removeBoxTransforms(boxWithoutTransform, this.latestValues);
return boxWithoutTransform;
this.targetDelta = delta;
this.root.scheduleUpdateProjection();
this.isProjectionDirty = true;
crossfade: options.crossfade !== undefined ? options.crossfade : true,
this.snapshot = undefined;
this.prevTransformTemplateValue = undefined;
this.targetDelta = undefined;
this.isLayoutDirty = false;
forceRelativeParentToResolveTarget() {
if (!this.relativeParent)
* 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
if (this.relativeParent.resolvedRelativeTargetAt !==
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
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 ||
((_a = this.parent) === null || _a === void 0 ? void 0 : _a.isProjectionDirty) ||
this.attemptToResolveRelativeTarget);
const { layout, layoutId } = this.options;
* If we have no layout, we can't perform projection, so early return
if (!this.layout || !(layout || layoutId))
this.resolvedRelativeTargetAt = frameData.timestamp;
* 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) {
const relativeParent = this.getClosestProjectingParent();
this.animationProgress !== 1) {
this.relativeParent = relativeParent;
this.forceRelativeParentToResolveTarget();
this.relativeTarget = createBox();
this.relativeTargetOrigin = createBox();
calcRelativePosition(this.relativeTargetOrigin, this.layout.layoutBox, relativeParent.layout.layoutBox);
copyBoxInto(this.relativeTarget, this.relativeTargetOrigin);
this.relativeParent = this.relativeTarget = undefined;
* If we have no relative target or no target delta our target isn't valid
if (!this.relativeTarget && !this.targetDelta)
* Lazy-init target data structure
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.target) {
this.forceRelativeParentToResolveTarget();
calcRelativeBox(this.target, this.relativeTarget, this.relativeParent.target);
* If we've only got a targetDelta, resolve it into a target
else if (this.targetDelta) {
if (Boolean(this.resumingFrom)) {
// TODO: This is creating a new object every frame
this.target = this.applyTransform(this.layout.layoutBox);
copyBoxInto(this.target, this.layout.layoutBox);
applyBoxDelta(this.target, this.targetDelta);
* 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;
const relativeParent = this.getClosestProjectingParent();
Boolean(relativeParent.resumingFrom) ===
Boolean(this.resumingFrom) &&
!relativeParent.options.layoutScroll &&
this.animationProgress !== 1) {
this.relativeParent = relativeParent;
this.forceRelativeParentToResolveTarget();
this.relativeTarget = createBox();
this.relativeTargetOrigin = createBox();
calcRelativePosition(this.relativeTargetOrigin, this.target, relativeParent.target);
copyBoxInto(this.relativeTarget, this.relativeTargetOrigin);
this.relativeParent = this.relativeTarget = undefined;
* Increase debug counter for resolved target deltas
projectionFrameData.resolvedTargetDeltas++;
getClosestProjectingParent() {
hasScale(this.parent.latestValues) ||
has2DTranslate(this.parent.latestValues)) {
if (this.parent.isProjecting()) {
return this.parent.getClosestProjectingParent();
return Boolean((this.relativeTarget ||
this.options.layoutRoot) &&
const lead = this.getLead();
const isShared = Boolean(this.resumingFrom) || this !== lead;
* 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 || ((_a = this.parent) === null || _a === void 0 ? void 0 : _a.isProjectionDirty)) {
* If this is a shared layout animation and this node's shared projection is dirty then
(this.isSharedProjectionDirty || this.isTransformDirty)) {
* 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) {
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) ||
if (!this.isTreeAnimating) {
this.targetDelta = this.relativeTarget = undefined;
if (!this.layout || !(layout || layoutId))
* 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.
(this.treeScale.x !== 1 || this.treeScale.y !== 1)) {
lead.target = lead.layout.layoutBox;
lead.targetWithTransforms = createBox();
* 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.projectionTransform) {
this.projectionDelta = createDelta();
this.projectionTransform = "none";
if (!this.projectionDelta) {
this.projectionDelta = createDelta();
this.projectionDeltaWithTransform = createDelta();
const prevProjectionTransform = this.projectionTransform;
* Update the delta between the corrected box and the target box before user-set transforms were applied.
* This will allow us to calculate the corrected borderRadius and boxShadow to compensate
* for our layout reprojection, but still allow them to be scaled correctly by the user.
* It might be that to simplify this we may want to accept that user-set scale is also corrected
* and we wouldn't have to keep and calc both deltas, OR we could support a user setting
* to allow people to choose whether these styles are corrected based on just the
* layout reprojection or the final bounding box.
calcBoxDelta(this.projectionDelta, this.layoutCorrected, target, this.latestValues);
this.projectionTransform = buildProjectionTransform(this.projectionDelta, this.treeScale);
if (this.projectionTransform !== prevProjectionTransform ||
this.treeScale.x !== prevTreeScaleX ||
this.treeScale.y !== prevTreeScaleY) {
this.hasProjected = true;
this.notifyListeners("projectionUpdate", target);
* Increase debug counter for recalculated projections
projectionFrameData.recalculatedProjection++;
scheduleRender(notifyAll = true) {
this.options.scheduleRender && this.options.scheduleRender();
const stack = this.getStack();
stack && stack.scheduleRender();
if (this.resumingFrom && !this.resumingFrom.instance) {
this.resumingFrom = undefined;
setAnimationOrigin(delta, hasOnlyRelativeTargetChanged = false) {
const snapshot = this.snapshot;
const snapshotLatestValues = snapshot
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 &&
this.options.crossfade === true &&
!this.path.some(hasOpacityCrossfade));
this.animationProgress = 0;
this.mixTargetDelta = (latest) => {
const progress = latest / 1000;
mixAxisDelta(targetDelta.x, delta.x, progress);