| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592 |
- import { statsBuffer, getValueTransition, cancelFrame, time, frameData, frameSteps, microtask, frame, activeAnimations } from 'motion-dom';
- import { SubscriptionManager, noop } from 'motion-utils';
- import { animateSingleValue } from '../../animation/animate/single-value.mjs';
- import { getOptimisedAppearId } from '../../animation/optimized-appear/get-appear-id.mjs';
- import { isSVGElement } from '../../render/dom/utils/is-svg-element.mjs';
- import { FlatTree } from '../../render/utils/flat-tree.mjs';
- import { clamp } from '../../utils/clamp.mjs';
- import { delay } from '../../utils/delay.mjs';
- import { mixNumber } from '../../utils/mix/number.mjs';
- import { resolveMotionValue } from '../../value/utils/resolve-motion-value.mjs';
- import { mixValues } from '../animation/mix-values.mjs';
- import { copyBoxInto, copyAxisDeltaInto } from '../geometry/copy.mjs';
- import { translateAxis, transformBox, applyBoxDelta, applyTreeDeltas } from '../geometry/delta-apply.mjs';
- import { calcLength, calcRelativePosition, calcRelativeBox, 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, aspectRatio, boxEquals } from '../geometry/utils.mjs';
- import { NodeStack } from '../shared/stack.mjs';
- import { scaleCorrectors } from '../styles/scale-correction.mjs';
- import { buildProjectionTransform } from '../styles/transform.mjs';
- import { eachAxis } from '../utils/each-axis.mjs';
- import { hasTransform, hasScale, has2DTranslate } from '../utils/has-transform.mjs';
- import { globalProjectionState } from './state.mjs';
- const metrics = {
- nodes: 0,
- calculatedTargetDeltas: 0,
- calculatedProjections: 0,
- };
- const transformAxes = ["", "X", "Y", "Z"];
- const hiddenVisibility = { visibility: "hidden" };
- /**
- * 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;
- /**
- * 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;
- // 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.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, isLayoutDirty = this.root.hasTreeAnimated) {
- if (this.instance)
- return;
- this.isSVG = isSVGElement(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 (isLayoutDirty && (layout || layoutId)) {
- this.isLayoutDirty = true;
- }
- if (attachResizeListener) {
- let cancelDelay;
- const resizeUnblockUpdate = () => (this.root.updateBlockedByResize = false);
- attachResizeListener(instance, () => {
- 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;
- }
- this.setAnimationOrigin(delta, hasOnlyRelativeTargetChanged);
- const animationOptions = {
- ...getValueTransition(layoutTransition, "layout"),
- onPlay: onLayoutAnimationStart,
- onComplete: onLayoutAnimationComplete,
- };
- if (visualElement.shouldReduceMotion ||
- this.options.layoutRoot) {
- animationOptions.delay = 0;
- animationOptions.type = false;
- }
- this.startAnimation(animationOptions);
- }
- else {
- /**
- * If the layout hasn't changed and we have an animation that hasn't started yet,
- * finish it immediately. Otherwise it will be animating from a location
- * that was probably never commited to screen and look like a jumpy box.
- */
- if (!hasLayoutChanged) {
- 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;
- 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;
- 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) {
- this.unblockUpdate();
- this.clearAllSnapshots();
- this.nodes.forEach(clearMeasurements);
- return;
- }
- if (!this.isUpdating) {
- this.nodes.forEach(clearIsLayoutDirty);
- }
- this.isUpdating = false;
- /**
- * 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;
- // TODO: Incorporate into a forwarded scroll offset
- this.updateScroll();
- if (!(this.options.alwaysMeasureLayout && this.isLead()) &&
- !this.isLayoutDirty) {
- return;
- }
- /**
- * When a node is mounted, it simply resumes from the prevLead's
- * snapshot instead of taking a new one, but the ancestors scroll
- * might have updated while the prevLead is unmounted. We need to
- * update the scroll again to make sure the layout we measure is
- * up to date.
- */
- if (this.resumeFrom && !this.resumeFrom.instance) {
- for (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.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) {
- 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 &&
- (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) {
- const withTransforms = 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) {
- transformBox(withTransforms, {
- x: -node.scroll.offset.x,
- y: -node.scroll.offset.y,
- });
- }
- if (!hasTransform(node.latestValues))
- continue;
- transformBox(withTransforms, node.latestValues);
- }
- if (hasTransform(this.latestValues)) {
- transformBox(withTransforms, this.latestValues);
- }
- 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 (!node.instance)
- continue;
- if (!hasTransform(node.latestValues))
- continue;
- 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;
- }
- 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;
- /**
- * 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();
- if (relativeParent &&
- relativeParent.layout &&
- 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);
- }
- else {
- this.relativeParent = this.relativeTarget = undefined;
- }
- }
- /**
- * 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);
- /**
- * 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);
- }
- 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;
- const relativeParent = this.getClosestProjectingParent();
- if (relativeParent &&
- Boolean(relativeParent.resumingFrom) ===
- Boolean(this.resumingFrom) &&
- !relativeParent.options.layoutScroll &&
- relativeParent.target &&
- 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);
- }
- 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);
- }
- 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) {
- 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;
- this.mixTargetDelta = (latest) => {
- const progress = latest / 1000;
- mixAxisDelta(targetDelta.x, delta.x, progress);
- mixAxisDelta(targetDelta.y, delta.y, progress);
- this.setTargetDelta(targetDelta);
- if (this.relativeTarget &&
- this.relativeTargetOrigin &&
- this.layout &&
- this.relativeParent &&
- this.relativeParent.layout) {
- calcRelativePosition(relativeLayout, this.layout.layoutBox, this.relativeParent.layout.layoutBox);
- 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);
- }
- this.root.scheduleUpdateProjection();
- this.scheduleRender();
- this.animationProgress = progress;
- };
- this.mixTargetDelta(this.options.layoutRoot ? 1000 : 0);
- }
- startAnimation(options) {
- this.notifyListeners("animationStart");
- this.currentAnimation && this.currentAnimation.stop();
- if (this.resumingFrom && this.resumingFrom.currentAnimation) {
- 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;
- activeAnimations.layout++;
- this.currentAnimation = animateSingleValue(0, animationTarget, {
- ...options,
- onUpdate: (latest) => {
- this.mixTargetDelta(latest);
- options.onUpdate && options.onUpdate(latest);
- },
- onStop: () => {
- activeAnimations.layout--;
- },
- onComplete: () => {
- activeAnimations.layout--;
- 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();
- }
- getProjectionStyles(styleProp) {
- if (!this.instance || this.isSVG)
- return undefined;
- if (!this.isVisible) {
- return hiddenVisibility;
- }
- const styles = {
- visibility: "",
- };
- const transformTemplate = this.getTransformTemplate();
- if (this.needsReset) {
- this.needsReset = false;
- styles.opacity = "";
- styles.pointerEvents =
- resolveMotionValue(styleProp?.pointerEvents) || "";
- styles.transform = transformTemplate
- ? transformTemplate(this.latestValues, "")
- : "none";
- return styles;
- }
- const lead = this.getLead();
- if (!this.projectionDelta || !this.layout || !lead.target) {
- const emptyStyles = {};
- if (this.options.layoutId) {
- emptyStyles.opacity =
- this.latestValues.opacity !== undefined
- ? this.latestValues.opacity
- : 1;
- emptyStyles.pointerEvents =
- resolveMotionValue(styleProp?.pointerEvents) || "";
- }
- if (this.hasProjected && !hasTransform(this.latestValues)) {
- emptyStyles.transform = transformTemplate
- ? transformTemplate({}, "")
- : "none";
- this.hasProjected = false;
- }
- return emptyStyles;
- }
- const valuesToRender = lead.animationValues || lead.latestValues;
- this.applyTransformsToTarget();
- styles.transform = buildProjectionTransform(this.projectionDeltaWithTransform, this.treeScale, valuesToRender);
- if (transformTemplate) {
- styles.transform = transformTemplate(valuesToRender, styles.transform);
- }
- const { x, y } = this.projectionDelta;
- styles.transformOrigin = `${x.origin * 100}% ${y.origin * 100}% 0`;
- if (lead.animationValues) {
- /**
- * If the lead component is animating, assign this either the entering/leaving
- * opacity
- */
- styles.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.
- */
- styles.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 = styles.transform === "none"
- ? valuesToRender[key]
- : correct(valuesToRender[key], lead);
- if (applyTo) {
- const num = applyTo.length;
- for (let i = 0; i < num; i++) {
- styles[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 {
- styles[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) {
- styles.pointerEvents =
- lead === this
- ? resolveMotionValue(styleProp?.pointerEvents) || ""
- : "none";
- }
- return styles;
- }
- 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 (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 relativeSnapshot = createBox();
- calcRelativePosition(relativeSnapshot, snapshot.layoutBox, parentSnapshot.layoutBox);
- const relativeLayout = createBox();
- calcRelativePosition(relativeLayout, layout, parentLayout.layoutBox);
- 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 clearIsLayoutDirty(node) {
- node.isLayoutDirty = false;
- }
- 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 mixAxisDelta(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, mixAxisDelta, mixBox, propagateDirtyNodes };
|