framer-motion 12.23.24 → 12.23.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -108,21 +108,27 @@ Motion is sustainable thanks to the kind support of its sponsors.
108
108
 
109
109
  [Become a sponsor](https://motion.dev/sponsor)
110
110
 
111
- ### Partner
111
+ ### Partners
112
112
 
113
- Motion powers Framer animations, the web builder for creative pros. Design and ship your dream site. Zero code, maximum speed.
113
+ Motion powers the animations for all websites built with Framer, the web builder for creative pros. The Motion website itself is built on Framer, for its delightful canvas-based editing and powerful CMS features.
114
114
 
115
115
  <a href="https://framer.link/FlnUbQY">
116
116
  <img alt="Framer" src="https://github.com/user-attachments/assets/22a79be7-672e-4336-bfb7-5d55d1deb917" width="250px" height="150px">
117
117
  </a>
118
118
 
119
+ Motion drives the animations on the Cursor homepage, and is working with Cursor to bring powerful AI workflows to the Motion examples and docs.
120
+
121
+ <a href="https://cursor.com">
122
+ <img alt="Cursor" src="https://github.com/user-attachments/assets/81c482d3-c2c2-4b35-bbcf-933b28d5b448" width="250px" height="150px" />
123
+ </a>
124
+
119
125
  ### Platinum
120
126
 
121
- <a href="https://linear.app"><img alt="Linear" src="https://github.com/user-attachments/assets/f9ce44b4-af28-4770-bb6e-9515b474bfb2" width="250px" height="150px"></a> <a href="https://figma.com"><img alt="Figma" src="https://github.com/user-attachments/assets/1077d0ab-4305-4a1f-81c8-d5be8c4c6717" width="250px" height="150px"></a> <a href="https://sanity.io"><img alt="Sanity" src="https://github.com/user-attachments/assets/80134088-f456-483f-8edd-940593c120ce" width="250px" height="150px"></a>
127
+ <a href="https://linear.app"><img alt="Linear" src="https://github.com/user-attachments/assets/f9ce44b4-af28-4770-bb6e-9515b474bfb2" width="250px" height="150px"></a> <a href="https://figma.com"><img alt="Figma" src="https://github.com/user-attachments/assets/1077d0ab-4305-4a1f-81c8-d5be8c4c6717" width="250px" height="150px"></a> <a href="https://sanity.io"><img alt="Sanity" src="https://github.com/user-attachments/assets/80134088-f456-483f-8edd-940593c120ce" width="250px" height="150px"></a> <a href="https://animations.dev"><img alt="Sanity" src="https://github.com/user-attachments/assets/7c5ab87d-c7d9-44b4-9c7e-f9e6a9f3ba3b" width="250px" height="150px"></a>
122
128
 
123
129
  ### Gold
124
130
 
125
- <a href="https://tailwindcss.com"><img alt="Tailwind" src="https://github.com/user-attachments/assets/1d5f2571-8bc3-4367-9fec-14d291168ff0" width="200px" height="120px"></a> <a href="https://emilkowal.ski"><img alt="Emil Kowalski" src="https://github.com/user-attachments/assets/33d1cb98-238a-4eed-a0df-9c7ab097d65b" width="200px" height="120px"></a> <a href="https://liveblocks.io"><img alt="Liveblocks" src="https://github.com/user-attachments/assets/28eddbe5-1617-4e74-969d-2eb6fcd481af" width="200px" height="120px"></a> <a href="https://lu.ma"><img alt="Luma" src="https://github.com/user-attachments/assets/ac282433-6adb-4ad2-9fd2-5c6ee513c14b" width="200px" height="120px"></a> <a href="https://notion.com"><img alt="Notion" src="https://github.com/user-attachments/assets/a27a6033-3cb0-4232-a6bb-625e1824517b" width="200px" height="120px"></a> <a href="https://lottiefiles.com"><img alt="LottieFiles" src="https://github.com/user-attachments/assets/4e99d8c7-4cba-43ee-93c5-93861ae708ec" width="200px" height="120px"></a>
131
+ <a href="https://liveblocks.io"><img alt="Liveblocks" src="https://github.com/user-attachments/assets/28eddbe5-1617-4e74-969d-2eb6fcd481af" width="200px" height="120px"></a> <a href="https://lu.ma"><img alt="Luma" src="https://github.com/user-attachments/assets/ac282433-6adb-4ad2-9fd2-5c6ee513c14b" width="200px" height="120px"></a> <a href="https://notion.com"><img alt="Notion" src="https://github.com/user-attachments/assets/a27a6033-3cb0-4232-a6bb-625e1824517b" width="200px" height="120px"></a> <a href="https://lottiefiles.com"><img alt="LottieFiles" src="https://github.com/user-attachments/assets/4e99d8c7-4cba-43ee-93c5-93861ae708ec" width="200px" height="120px"></a>
126
132
 
127
133
  ### Silver
128
134
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var featureBundle = require('./feature-bundle-v2Gb94eA.js');
5
+ var featureBundle = require('./feature-bundle-kvRbMDEA.js');
6
6
  require('react');
7
7
  require('motion-dom');
8
8
  require('motion-utils');
package/dist/cjs/dom.js CHANGED
@@ -1468,7 +1468,91 @@ function renderHTML(element, { style, vars }, styleProp, projection) {
1468
1468
  }
1469
1469
  }
1470
1470
 
1471
- const scaleCorrectors = {};
1471
+ function pixelsToPercent(pixels, axis) {
1472
+ if (axis.max === axis.min)
1473
+ return 0;
1474
+ return (pixels / (axis.max - axis.min)) * 100;
1475
+ }
1476
+ /**
1477
+ * We always correct borderRadius as a percentage rather than pixels to reduce paints.
1478
+ * For example, if you are projecting a box that is 100px wide with a 10px borderRadius
1479
+ * into a box that is 200px wide with a 20px borderRadius, that is actually a 10%
1480
+ * borderRadius in both states. If we animate between the two in pixels that will trigger
1481
+ * a paint each time. If we animate between the two in percentage we'll avoid a paint.
1482
+ */
1483
+ const correctBorderRadius = {
1484
+ correct: (latest, node) => {
1485
+ if (!node.target)
1486
+ return latest;
1487
+ /**
1488
+ * If latest is a string, if it's a percentage we can return immediately as it's
1489
+ * going to be stretched appropriately. Otherwise, if it's a pixel, convert it to a number.
1490
+ */
1491
+ if (typeof latest === "string") {
1492
+ if (motionDom.px.test(latest)) {
1493
+ latest = parseFloat(latest);
1494
+ }
1495
+ else {
1496
+ return latest;
1497
+ }
1498
+ }
1499
+ /**
1500
+ * If latest is a number, it's a pixel value. We use the current viewportBox to calculate that
1501
+ * pixel value as a percentage of each axis
1502
+ */
1503
+ const x = pixelsToPercent(latest, node.target.x);
1504
+ const y = pixelsToPercent(latest, node.target.y);
1505
+ return `${x}% ${y}%`;
1506
+ },
1507
+ };
1508
+
1509
+ const correctBoxShadow = {
1510
+ correct: (latest, { treeScale, projectionDelta }) => {
1511
+ const original = latest;
1512
+ const shadow = motionDom.complex.parse(latest);
1513
+ // TODO: Doesn't support multiple shadows
1514
+ if (shadow.length > 5)
1515
+ return original;
1516
+ const template = motionDom.complex.createTransformer(latest);
1517
+ const offset = typeof shadow[0] !== "number" ? 1 : 0;
1518
+ // Calculate the overall context scale
1519
+ const xScale = projectionDelta.x.scale * treeScale.x;
1520
+ const yScale = projectionDelta.y.scale * treeScale.y;
1521
+ shadow[0 + offset] /= xScale;
1522
+ shadow[1 + offset] /= yScale;
1523
+ /**
1524
+ * Ideally we'd correct x and y scales individually, but because blur and
1525
+ * spread apply to both we have to take a scale average and apply that instead.
1526
+ * We could potentially improve the outcome of this by incorporating the ratio between
1527
+ * the two scales.
1528
+ */
1529
+ const averageScale = motionDom.mixNumber(xScale, yScale, 0.5);
1530
+ // Blur
1531
+ if (typeof shadow[2 + offset] === "number")
1532
+ shadow[2 + offset] /= averageScale;
1533
+ // Spread
1534
+ if (typeof shadow[3 + offset] === "number")
1535
+ shadow[3 + offset] /= averageScale;
1536
+ return template(shadow);
1537
+ },
1538
+ };
1539
+
1540
+ const scaleCorrectors = {
1541
+ borderRadius: {
1542
+ ...correctBorderRadius,
1543
+ applyTo: [
1544
+ "borderTopLeftRadius",
1545
+ "borderTopRightRadius",
1546
+ "borderBottomLeftRadius",
1547
+ "borderBottomRightRadius",
1548
+ ],
1549
+ },
1550
+ borderTopLeftRadius: correctBorderRadius,
1551
+ borderTopRightRadius: correctBorderRadius,
1552
+ borderBottomLeftRadius: correctBorderRadius,
1553
+ borderBottomRightRadius: correctBorderRadius,
1554
+ boxShadow: correctBoxShadow,
1555
+ };
1472
1556
 
1473
1557
  function isForcedMotionValue(key, { layout, layoutId }) {
1474
1558
  return (motionDom.transformProps.has(key) ||
@@ -825,7 +825,91 @@ class NodeStack {
825
825
  }
826
826
  }
827
827
 
828
- const scaleCorrectors = {};
828
+ function pixelsToPercent(pixels, axis) {
829
+ if (axis.max === axis.min)
830
+ return 0;
831
+ return (pixels / (axis.max - axis.min)) * 100;
832
+ }
833
+ /**
834
+ * We always correct borderRadius as a percentage rather than pixels to reduce paints.
835
+ * For example, if you are projecting a box that is 100px wide with a 10px borderRadius
836
+ * into a box that is 200px wide with a 20px borderRadius, that is actually a 10%
837
+ * borderRadius in both states. If we animate between the two in pixels that will trigger
838
+ * a paint each time. If we animate between the two in percentage we'll avoid a paint.
839
+ */
840
+ const correctBorderRadius = {
841
+ correct: (latest, node) => {
842
+ if (!node.target)
843
+ return latest;
844
+ /**
845
+ * If latest is a string, if it's a percentage we can return immediately as it's
846
+ * going to be stretched appropriately. Otherwise, if it's a pixel, convert it to a number.
847
+ */
848
+ if (typeof latest === "string") {
849
+ if (motionDom.px.test(latest)) {
850
+ latest = parseFloat(latest);
851
+ }
852
+ else {
853
+ return latest;
854
+ }
855
+ }
856
+ /**
857
+ * If latest is a number, it's a pixel value. We use the current viewportBox to calculate that
858
+ * pixel value as a percentage of each axis
859
+ */
860
+ const x = pixelsToPercent(latest, node.target.x);
861
+ const y = pixelsToPercent(latest, node.target.y);
862
+ return `${x}% ${y}%`;
863
+ },
864
+ };
865
+
866
+ const correctBoxShadow = {
867
+ correct: (latest, { treeScale, projectionDelta }) => {
868
+ const original = latest;
869
+ const shadow = motionDom.complex.parse(latest);
870
+ // TODO: Doesn't support multiple shadows
871
+ if (shadow.length > 5)
872
+ return original;
873
+ const template = motionDom.complex.createTransformer(latest);
874
+ const offset = typeof shadow[0] !== "number" ? 1 : 0;
875
+ // Calculate the overall context scale
876
+ const xScale = projectionDelta.x.scale * treeScale.x;
877
+ const yScale = projectionDelta.y.scale * treeScale.y;
878
+ shadow[0 + offset] /= xScale;
879
+ shadow[1 + offset] /= yScale;
880
+ /**
881
+ * Ideally we'd correct x and y scales individually, but because blur and
882
+ * spread apply to both we have to take a scale average and apply that instead.
883
+ * We could potentially improve the outcome of this by incorporating the ratio between
884
+ * the two scales.
885
+ */
886
+ const averageScale = motionDom.mixNumber(xScale, yScale, 0.5);
887
+ // Blur
888
+ if (typeof shadow[2 + offset] === "number")
889
+ shadow[2 + offset] /= averageScale;
890
+ // Spread
891
+ if (typeof shadow[3 + offset] === "number")
892
+ shadow[3 + offset] /= averageScale;
893
+ return template(shadow);
894
+ },
895
+ };
896
+
897
+ const scaleCorrectors = {
898
+ borderRadius: {
899
+ ...correctBorderRadius,
900
+ applyTo: [
901
+ "borderTopLeftRadius",
902
+ "borderTopRightRadius",
903
+ "borderBottomLeftRadius",
904
+ "borderBottomRightRadius",
905
+ ],
906
+ },
907
+ borderTopLeftRadius: correctBorderRadius,
908
+ borderTopRightRadius: correctBorderRadius,
909
+ borderBottomLeftRadius: correctBorderRadius,
910
+ borderBottomRightRadius: correctBorderRadius,
911
+ boxShadow: correctBoxShadow,
912
+ };
829
913
  function addScaleCorrector(correctors) {
830
914
  for (const key in correctors) {
831
915
  scaleCorrectors[key] = correctors[key];
@@ -1042,6 +1126,7 @@ function createProjectionNode$1({ attachResizeListener, defaultParent, measureSc
1042
1126
  */
1043
1127
  this.eventHandlers = new Map();
1044
1128
  this.hasTreeAnimated = false;
1129
+ this.layoutVersion = 0;
1045
1130
  // Note: Currently only running on root node
1046
1131
  this.updateScheduled = false;
1047
1132
  this.scheduleUpdate = () => this.update();
@@ -1081,6 +1166,7 @@ function createProjectionNode$1({ attachResizeListener, defaultParent, measureSc
1081
1166
  * Frame calculations
1082
1167
  */
1083
1168
  this.resolvedRelativeTargetAt = 0.0;
1169
+ this.linkedParentVersion = 0;
1084
1170
  this.hasProjected = false;
1085
1171
  this.isVisible = true;
1086
1172
  this.animationProgress = 0;
@@ -1434,6 +1520,7 @@ function createProjectionNode$1({ attachResizeListener, defaultParent, measureSc
1434
1520
  }
1435
1521
  const prevLayout = this.layout;
1436
1522
  this.layout = this.measure(false);
1523
+ this.layoutVersion++;
1437
1524
  this.layoutCorrected = createBox();
1438
1525
  this.isLayoutDirty = false;
1439
1526
  this.projectionDelta = undefined;
@@ -1653,25 +1740,23 @@ function createProjectionNode$1({ attachResizeListener, defaultParent, measureSc
1653
1740
  if (!this.layout || !(layout || layoutId))
1654
1741
  return;
1655
1742
  this.resolvedRelativeTargetAt = motionDom.frameData.timestamp;
1743
+ const relativeParent = this.getClosestProjectingParent();
1744
+ if (relativeParent &&
1745
+ this.linkedParentVersion !== relativeParent.layoutVersion &&
1746
+ !relativeParent.options.layoutRoot) {
1747
+ this.removeRelativeTarget();
1748
+ }
1656
1749
  /**
1657
1750
  * If we don't have a targetDelta but do have a layout, we can attempt to resolve
1658
1751
  * a relativeParent. This will allow a component to perform scale correction
1659
1752
  * even if no animation has started.
1660
1753
  */
1661
1754
  if (!this.targetDelta && !this.relativeTarget) {
1662
- const relativeParent = this.getClosestProjectingParent();
1663
- if (relativeParent &&
1664
- relativeParent.layout &&
1665
- this.animationProgress !== 1) {
1666
- this.relativeParent = relativeParent;
1667
- this.forceRelativeParentToResolveTarget();
1668
- this.relativeTarget = createBox();
1669
- this.relativeTargetOrigin = createBox();
1670
- calcRelativePosition(this.relativeTargetOrigin, this.layout.layoutBox, relativeParent.layout.layoutBox);
1671
- copyBoxInto(this.relativeTarget, this.relativeTargetOrigin);
1755
+ if (relativeParent && relativeParent.layout) {
1756
+ this.createRelativeTarget(relativeParent, this.layout.layoutBox, relativeParent.layout.layoutBox);
1672
1757
  }
1673
1758
  else {
1674
- this.relativeParent = this.relativeTarget = undefined;
1759
+ this.removeRelativeTarget();
1675
1760
  }
1676
1761
  }
1677
1762
  /**
@@ -1721,19 +1806,13 @@ function createProjectionNode$1({ attachResizeListener, defaultParent, measureSc
1721
1806
  */
1722
1807
  if (this.attemptToResolveRelativeTarget) {
1723
1808
  this.attemptToResolveRelativeTarget = false;
1724
- const relativeParent = this.getClosestProjectingParent();
1725
1809
  if (relativeParent &&
1726
1810
  Boolean(relativeParent.resumingFrom) ===
1727
1811
  Boolean(this.resumingFrom) &&
1728
1812
  !relativeParent.options.layoutScroll &&
1729
1813
  relativeParent.target &&
1730
1814
  this.animationProgress !== 1) {
1731
- this.relativeParent = relativeParent;
1732
- this.forceRelativeParentToResolveTarget();
1733
- this.relativeTarget = createBox();
1734
- this.relativeTargetOrigin = createBox();
1735
- calcRelativePosition(this.relativeTargetOrigin, this.target, relativeParent.target);
1736
- copyBoxInto(this.relativeTarget, this.relativeTargetOrigin);
1815
+ this.createRelativeTarget(relativeParent, this.target, relativeParent.target);
1737
1816
  }
1738
1817
  else {
1739
1818
  this.relativeParent = this.relativeTarget = undefined;
@@ -1765,6 +1844,18 @@ function createProjectionNode$1({ attachResizeListener, defaultParent, measureSc
1765
1844
  this.options.layoutRoot) &&
1766
1845
  this.layout);
1767
1846
  }
1847
+ createRelativeTarget(relativeParent, layout, parentLayout) {
1848
+ this.relativeParent = relativeParent;
1849
+ this.linkedParentVersion = relativeParent.layoutVersion;
1850
+ this.forceRelativeParentToResolveTarget();
1851
+ this.relativeTarget = createBox();
1852
+ this.relativeTargetOrigin = createBox();
1853
+ calcRelativePosition(this.relativeTargetOrigin, layout, parentLayout);
1854
+ copyBoxInto(this.relativeTarget, this.relativeTargetOrigin);
1855
+ }
1856
+ removeRelativeTarget() {
1857
+ this.relativeParent = this.relativeTarget = undefined;
1858
+ }
1768
1859
  calcProjection() {
1769
1860
  const lead = this.getLead();
1770
1861
  const isShared = Boolean(this.resumingFrom) || this !== lead;
@@ -2532,75 +2623,6 @@ const HTMLProjectionNode = createProjectionNode$1({
2532
2623
  checkIsScrollRoot: (instance) => Boolean(window.getComputedStyle(instance).position === "fixed"),
2533
2624
  });
2534
2625
 
2535
- function pixelsToPercent(pixels, axis) {
2536
- if (axis.max === axis.min)
2537
- return 0;
2538
- return (pixels / (axis.max - axis.min)) * 100;
2539
- }
2540
- /**
2541
- * We always correct borderRadius as a percentage rather than pixels to reduce paints.
2542
- * For example, if you are projecting a box that is 100px wide with a 10px borderRadius
2543
- * into a box that is 200px wide with a 20px borderRadius, that is actually a 10%
2544
- * borderRadius in both states. If we animate between the two in pixels that will trigger
2545
- * a paint each time. If we animate between the two in percentage we'll avoid a paint.
2546
- */
2547
- const correctBorderRadius = {
2548
- correct: (latest, node) => {
2549
- if (!node.target)
2550
- return latest;
2551
- /**
2552
- * If latest is a string, if it's a percentage we can return immediately as it's
2553
- * going to be stretched appropriately. Otherwise, if it's a pixel, convert it to a number.
2554
- */
2555
- if (typeof latest === "string") {
2556
- if (motionDom.px.test(latest)) {
2557
- latest = parseFloat(latest);
2558
- }
2559
- else {
2560
- return latest;
2561
- }
2562
- }
2563
- /**
2564
- * If latest is a number, it's a pixel value. We use the current viewportBox to calculate that
2565
- * pixel value as a percentage of each axis
2566
- */
2567
- const x = pixelsToPercent(latest, node.target.x);
2568
- const y = pixelsToPercent(latest, node.target.y);
2569
- return `${x}% ${y}%`;
2570
- },
2571
- };
2572
-
2573
- const correctBoxShadow = {
2574
- correct: (latest, { treeScale, projectionDelta }) => {
2575
- const original = latest;
2576
- const shadow = motionDom.complex.parse(latest);
2577
- // TODO: Doesn't support multiple shadows
2578
- if (shadow.length > 5)
2579
- return original;
2580
- const template = motionDom.complex.createTransformer(latest);
2581
- const offset = typeof shadow[0] !== "number" ? 1 : 0;
2582
- // Calculate the overall context scale
2583
- const xScale = projectionDelta.x.scale * treeScale.x;
2584
- const yScale = projectionDelta.y.scale * treeScale.y;
2585
- shadow[0 + offset] /= xScale;
2586
- shadow[1 + offset] /= yScale;
2587
- /**
2588
- * Ideally we'd correct x and y scales individually, but because blur and
2589
- * spread apply to both we have to take a scale average and apply that instead.
2590
- * We could potentially improve the outcome of this by incorporating the ratio between
2591
- * the two scales.
2592
- */
2593
- const averageScale = motionDom.mixNumber(xScale, yScale, 0.5);
2594
- // Blur
2595
- if (typeof shadow[2 + offset] === "number")
2596
- shadow[2 + offset] /= averageScale;
2597
- // Spread
2598
- if (typeof shadow[3 + offset] === "number")
2599
- shadow[3 + offset] /= averageScale;
2600
- return template(shadow);
2601
- },
2602
- };
2603
-
2604
2626
  /**
2605
2627
  * Bounding boxes tend to be defined as top, left, right, bottom. For various operations
2606
2628
  * it's easier to consider each axis individually. This function returns a bounding box
@@ -5872,7 +5894,6 @@ class MeasureLayoutWithContext extends React.Component {
5872
5894
  componentDidMount() {
5873
5895
  const { visualElement, layoutGroup, switchLayoutGroup, layoutId } = this.props;
5874
5896
  const { projection } = visualElement;
5875
- addScaleCorrector(defaultScaleCorrectors);
5876
5897
  if (projection) {
5877
5898
  if (layoutGroup.group)
5878
5899
  layoutGroup.group.add(projection);
@@ -5971,22 +5992,6 @@ function MeasureLayout(props) {
5971
5992
  const layoutGroup = React.useContext(LayoutGroupContext);
5972
5993
  return (jsxRuntime.jsx(MeasureLayoutWithContext, { ...props, layoutGroup: layoutGroup, switchLayoutGroup: React.useContext(SwitchLayoutGroupContext), isPresent: isPresent, safeToRemove: safeToRemove }));
5973
5994
  }
5974
- const defaultScaleCorrectors = {
5975
- borderRadius: {
5976
- ...correctBorderRadius,
5977
- applyTo: [
5978
- "borderTopLeftRadius",
5979
- "borderTopRightRadius",
5980
- "borderBottomLeftRadius",
5981
- "borderBottomRightRadius",
5982
- ],
5983
- },
5984
- borderTopLeftRadius: correctBorderRadius,
5985
- borderTopRightRadius: correctBorderRadius,
5986
- borderBottomLeftRadius: correctBorderRadius,
5987
- borderBottomRightRadius: correctBorderRadius,
5988
- boxShadow: correctBoxShadow,
5989
- };
5990
5995
 
5991
5996
  const drag = {
5992
5997
  pan: {
package/dist/cjs/index.js CHANGED
@@ -4,7 +4,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
  var React = require('react');
7
- var featureBundle = require('./feature-bundle-v2Gb94eA.js');
7
+ var featureBundle = require('./feature-bundle-kvRbMDEA.js');
8
8
  var motionDom = require('motion-dom');
9
9
  var motionUtils = require('motion-utils');
10
10
 
package/dist/cjs/m.js CHANGED
@@ -75,7 +75,91 @@ function variantLabelsAsDependency(prop) {
75
75
  return Array.isArray(prop) ? prop.join(" ") : prop;
76
76
  }
77
77
 
78
- const scaleCorrectors = {};
78
+ function pixelsToPercent(pixels, axis) {
79
+ if (axis.max === axis.min)
80
+ return 0;
81
+ return (pixels / (axis.max - axis.min)) * 100;
82
+ }
83
+ /**
84
+ * We always correct borderRadius as a percentage rather than pixels to reduce paints.
85
+ * For example, if you are projecting a box that is 100px wide with a 10px borderRadius
86
+ * into a box that is 200px wide with a 20px borderRadius, that is actually a 10%
87
+ * borderRadius in both states. If we animate between the two in pixels that will trigger
88
+ * a paint each time. If we animate between the two in percentage we'll avoid a paint.
89
+ */
90
+ const correctBorderRadius = {
91
+ correct: (latest, node) => {
92
+ if (!node.target)
93
+ return latest;
94
+ /**
95
+ * If latest is a string, if it's a percentage we can return immediately as it's
96
+ * going to be stretched appropriately. Otherwise, if it's a pixel, convert it to a number.
97
+ */
98
+ if (typeof latest === "string") {
99
+ if (motionDom.px.test(latest)) {
100
+ latest = parseFloat(latest);
101
+ }
102
+ else {
103
+ return latest;
104
+ }
105
+ }
106
+ /**
107
+ * If latest is a number, it's a pixel value. We use the current viewportBox to calculate that
108
+ * pixel value as a percentage of each axis
109
+ */
110
+ const x = pixelsToPercent(latest, node.target.x);
111
+ const y = pixelsToPercent(latest, node.target.y);
112
+ return `${x}% ${y}%`;
113
+ },
114
+ };
115
+
116
+ const correctBoxShadow = {
117
+ correct: (latest, { treeScale, projectionDelta }) => {
118
+ const original = latest;
119
+ const shadow = motionDom.complex.parse(latest);
120
+ // TODO: Doesn't support multiple shadows
121
+ if (shadow.length > 5)
122
+ return original;
123
+ const template = motionDom.complex.createTransformer(latest);
124
+ const offset = typeof shadow[0] !== "number" ? 1 : 0;
125
+ // Calculate the overall context scale
126
+ const xScale = projectionDelta.x.scale * treeScale.x;
127
+ const yScale = projectionDelta.y.scale * treeScale.y;
128
+ shadow[0 + offset] /= xScale;
129
+ shadow[1 + offset] /= yScale;
130
+ /**
131
+ * Ideally we'd correct x and y scales individually, but because blur and
132
+ * spread apply to both we have to take a scale average and apply that instead.
133
+ * We could potentially improve the outcome of this by incorporating the ratio between
134
+ * the two scales.
135
+ */
136
+ const averageScale = motionDom.mixNumber(xScale, yScale, 0.5);
137
+ // Blur
138
+ if (typeof shadow[2 + offset] === "number")
139
+ shadow[2 + offset] /= averageScale;
140
+ // Spread
141
+ if (typeof shadow[3 + offset] === "number")
142
+ shadow[3 + offset] /= averageScale;
143
+ return template(shadow);
144
+ },
145
+ };
146
+
147
+ const scaleCorrectors = {
148
+ borderRadius: {
149
+ ...correctBorderRadius,
150
+ applyTo: [
151
+ "borderTopLeftRadius",
152
+ "borderTopRightRadius",
153
+ "borderBottomLeftRadius",
154
+ "borderBottomRightRadius",
155
+ ],
156
+ },
157
+ borderTopLeftRadius: correctBorderRadius,
158
+ borderTopRightRadius: correctBorderRadius,
159
+ borderBottomLeftRadius: correctBorderRadius,
160
+ borderBottomRightRadius: correctBorderRadius,
161
+ boxShadow: correctBoxShadow,
162
+ };
79
163
 
80
164
  function isForcedMotionValue(key, { layout, layoutId }) {
81
165
  return (motionDom.transformProps.has(key) ||