Skip to content

Commit 507bc0f

Browse files
mattgperryclaude
andcommitted
Require originX/originY motion values for scaled layout parents
Remove resolveComputedOrigin getComputedStyle approach. Layout animations in scaled parents work correctly when originX/originY are set as motion values (not CSS transformOrigin). Update test to use originX: 0, originY: 0. Fixes #3356 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6ee0c50 commit 507bc0f

File tree

2 files changed

+8
-43
lines changed

2 files changed

+8
-43
lines changed

dev/react/src/tests/layout-scaled-parent.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import { useState } from "react"
88
* A child motion.div with layout toggles its CSS top position.
99
* The layout animation should smoothly interpolate between the two visual positions.
1010
*
11-
* With the bug, the snapshot measurement in removeTransform uses the wrong
12-
* transform-origin when removing the parent's scale, causing incorrect deltas.
11+
* With the bug, the projection system doesn't account for the parent's scale
12+
* when computing layout animation deltas, causing misaligned animations.
13+
* Using originX/originY as motion values (not CSS transformOrigin) ensures
14+
* removeTransform uses the correct origin.
1315
*/
1416
export const App = () => {
1517
const [toggled, setToggled] = useState(false)
@@ -20,7 +22,8 @@ export const App = () => {
2022
layoutRoot
2123
style={{
2224
scale: 2,
23-
transformOrigin: "0 0",
25+
originX: 0,
26+
originY: 0,
2427
position: "absolute",
2528
top: 0,
2629
left: 0,

packages/motion-dom/src/projection/node/create-projection-node.ts

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -86,35 +86,6 @@ const animationTarget = 1000
8686

8787
let id = 0
8888

89-
/**
90-
* When an element has scale in latestValues but no originX/originY motion
91-
* values, read the actual transform-origin from the computed style so
92-
* removeBoxTransforms uses the correct origin instead of defaulting to 0.5.
93-
*/
94-
function resolveComputedOrigin(
95-
latestValues: ResolvedValues,
96-
instance: unknown
97-
): ResolvedValues {
98-
if (
99-
!hasScale(latestValues) ||
100-
latestValues.originX !== undefined ||
101-
latestValues.originY !== undefined ||
102-
!instance
103-
) {
104-
return latestValues
105-
}
106-
const computed = window.getComputedStyle(instance as Element)
107-
const originStr = computed.transformOrigin
108-
if (!originStr) return latestValues
109-
const parts = originStr.split(" ")
110-
const el = instance as unknown as HTMLElement
111-
return {
112-
...latestValues,
113-
originX: parseFloat(parts[0]) / (el.offsetWidth || 1),
114-
originY: parseFloat(parts[1]) / (el.offsetHeight || 1),
115-
}
116-
}
117-
11889
function resetDistortingTransform(
11990
key: string,
12091
visualElement: VisualElement,
@@ -1133,23 +1104,14 @@ export function createProjectionNode<I>({
11331104

11341105
removeBoxTransforms(
11351106
boxWithoutTransform,
1136-
resolveComputedOrigin(
1137-
node.latestValues,
1138-
node.instance
1139-
),
1107+
node.latestValues,
11401108
node.snapshot?.layoutBox,
11411109
sourceBox
11421110
)
11431111
}
11441112

11451113
if (hasTransform(this.latestValues)) {
1146-
removeBoxTransforms(
1147-
boxWithoutTransform,
1148-
resolveComputedOrigin(
1149-
this.latestValues,
1150-
this.instance
1151-
)
1152-
)
1114+
removeBoxTransforms(boxWithoutTransform, this.latestValues)
11531115
}
11541116

11551117
return boxWithoutTransform

0 commit comments

Comments
 (0)