Upstream motiondivision/motion state of the touched files before 46bc6e6fee
This commit is contained in:
commit
1ba1c99cc1
6 files changed
+6391
No files matched your search
+5971
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,100 @@
|
|||||||
|
import { mixValues } from "../mix-values"
|
||||||
|
|
||||||
|
describe("mixValues", () => {
|
||||||
|
test("mixes borderRadius numbers", () => {
|
||||||
|
const output = {}
|
||||||
|
|
||||||
|
mixValues(
|
||||||
|
output,
|
||||||
|
{ borderTopLeftRadius: 10 },
|
||||||
|
{ borderTopLeftRadius: 20 },
|
||||||
|
0.5,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(output).toEqual({ borderTopLeftRadius: 15 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("mixes borderRadius px", () => {
|
||||||
|
const output = {}
|
||||||
|
|
||||||
|
mixValues(
|
||||||
|
output,
|
||||||
|
{ borderTopLeftRadius: "10px" },
|
||||||
|
{ borderTopLeftRadius: "20px" },
|
||||||
|
0.5,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(output).toEqual({ borderTopLeftRadius: 15 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("mixes borderRadius percentage", () => {
|
||||||
|
const output = {}
|
||||||
|
|
||||||
|
mixValues(
|
||||||
|
output,
|
||||||
|
{ borderTopLeftRadius: "10%" },
|
||||||
|
{ borderTopLeftRadius: "20%" },
|
||||||
|
0.5,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(output).toEqual({ borderTopLeftRadius: "15%" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("mixes borderRadius percentage with 0", () => {
|
||||||
|
const output = {}
|
||||||
|
|
||||||
|
mixValues(
|
||||||
|
output,
|
||||||
|
{ borderTopLeftRadius: 0 },
|
||||||
|
{ borderTopLeftRadius: "20%" },
|
||||||
|
0.5,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(output).toEqual({ borderTopLeftRadius: "10%" })
|
||||||
|
|
||||||
|
mixValues(
|
||||||
|
output,
|
||||||
|
{ borderTopLeftRadius: "20%" },
|
||||||
|
{ borderTopLeftRadius: 0 },
|
||||||
|
0.5,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(output).toEqual({ borderTopLeftRadius: "10%" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("doesn't mix % with px", () => {
|
||||||
|
const output = {}
|
||||||
|
|
||||||
|
mixValues(
|
||||||
|
output,
|
||||||
|
{ borderTopLeftRadius: "10px" },
|
||||||
|
{ borderTopLeftRadius: "20%" },
|
||||||
|
0.5,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(output).toEqual({ borderTopLeftRadius: "20%" })
|
||||||
|
|
||||||
|
mixValues(
|
||||||
|
output,
|
||||||
|
{ borderTopLeftRadius: "20%" },
|
||||||
|
{ borderTopLeftRadius: "10px" },
|
||||||
|
0.5,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(output).toEqual({ borderTopLeftRadius: "10px" })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { mixNumber } from "../../utils/mix/number"
|
||||||
|
import { percent, px } from "../../value/types/numbers/units"
|
||||||
|
import type { AnyResolvedKeyframe } from "../../animation/types"
|
||||||
|
import {
|
||||||
|
progress as calcProgress,
|
||||||
|
circOut,
|
||||||
|
EasingFunction,
|
||||||
|
noop,
|
||||||
|
} from "motion-utils"
|
||||||
|
import type { ResolvedValues } from "../../node/types"
|
||||||
|
import { cornerRadiusProps } from "../../utils/border-radius"
|
||||||
|
|
||||||
|
const numBorders = cornerRadiusProps.length
|
||||||
|
|
||||||
|
const asNumber = (value: AnyResolvedKeyframe) =>
|
||||||
|
typeof value === "string" ? parseFloat(value) : value
|
||||||
|
|
||||||
|
const isPx = (value: AnyResolvedKeyframe) =>
|
||||||
|
typeof value === "number" || px.test(value)
|
||||||
|
|
||||||
|
export function mixValues(
|
||||||
|
target: ResolvedValues,
|
||||||
|
follow: ResolvedValues,
|
||||||
|
lead: ResolvedValues,
|
||||||
|
progress: number,
|
||||||
|
shouldCrossfadeOpacity: boolean,
|
||||||
|
isOnlyMember: boolean
|
||||||
|
) {
|
||||||
|
if (shouldCrossfadeOpacity) {
|
||||||
|
target.opacity = mixNumber(
|
||||||
|
0,
|
||||||
|
(lead.opacity as number) ?? 1,
|
||||||
|
easeCrossfadeIn(progress)
|
||||||
|
)
|
||||||
|
target.opacityExit = mixNumber(
|
||||||
|
(follow.opacity as number) ?? 1,
|
||||||
|
0,
|
||||||
|
easeCrossfadeOut(progress)
|
||||||
|
)
|
||||||
|
} else if (isOnlyMember) {
|
||||||
|
target.opacity = mixNumber(
|
||||||
|
(follow.opacity as number) ?? 1,
|
||||||
|
(lead.opacity as number) ?? 1,
|
||||||
|
progress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mix border radius
|
||||||
|
*/
|
||||||
|
for (let i = 0; i < numBorders; i++) {
|
||||||
|
const borderLabel = cornerRadiusProps[i]
|
||||||
|
let followRadius = getRadius(follow, borderLabel)
|
||||||
|
let leadRadius = getRadius(lead, borderLabel)
|
||||||
|
|
||||||
|
if (followRadius === undefined && leadRadius === undefined) continue
|
||||||
|
|
||||||
|
followRadius ||= 0
|
||||||
|
leadRadius ||= 0
|
||||||
|
|
||||||
|
const canMix =
|
||||||
|
followRadius === 0 ||
|
||||||
|
leadRadius === 0 ||
|
||||||
|
isPx(followRadius) === isPx(leadRadius)
|
||||||
|
|
||||||
|
if (canMix) {
|
||||||
|
target[borderLabel] = Math.max(
|
||||||
|
mixNumber(
|
||||||
|
asNumber(followRadius),
|
||||||
|
asNumber(leadRadius),
|
||||||
|
progress
|
||||||
|
),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
|
if (percent.test(leadRadius) || percent.test(followRadius)) {
|
||||||
|
target[borderLabel] += "%"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
target[borderLabel] = leadRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mix rotation
|
||||||
|
*/
|
||||||
|
if (follow.rotate || lead.rotate) {
|
||||||
|
target.rotate = mixNumber(
|
||||||
|
(follow.rotate as number) || 0,
|
||||||
|
(lead.rotate as number) || 0,
|
||||||
|
progress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRadius(values: ResolvedValues, radiusName: string) {
|
||||||
|
return values[radiusName] !== undefined
|
||||||
|
? values[radiusName]
|
||||||
|
: values.borderRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
const easeCrossfadeIn = /*@__PURE__*/ compress(0, 0.5, circOut)
|
||||||
|
const easeCrossfadeOut = /*@__PURE__*/ compress(0.5, 0.95, noop)
|
||||||
|
|
||||||
|
function compress(
|
||||||
|
min: number,
|
||||||
|
max: number,
|
||||||
|
easing: EasingFunction
|
||||||
|
): EasingFunction {
|
||||||
|
return (p: number) => {
|
||||||
|
// Could replace ifs with clamp
|
||||||
|
if (p < min) return 0
|
||||||
|
if (p > max) return 1
|
||||||
|
return easing(calcProgress(min, max, p))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { buildProjectionTransform } from "../transform"
|
||||||
|
import { createDelta } from "../../geometry/models"
|
||||||
|
|
||||||
|
describe("buildProjectionTransform", () => {
|
||||||
|
it("Returns 'none' when no transform required", () => {
|
||||||
|
expect(buildProjectionTransform(createDelta(), { x: 1, y: 1 })).toEqual(
|
||||||
|
"none"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Creates the expected transform for the provided arguments", () => {
|
||||||
|
const delta = {
|
||||||
|
x: {
|
||||||
|
translate: 100,
|
||||||
|
scale: 2,
|
||||||
|
origin: 0.5,
|
||||||
|
originPoint: 100,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
translate: 300,
|
||||||
|
scale: 4,
|
||||||
|
origin: 0.5,
|
||||||
|
originPoint: 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expect(buildProjectionTransform(delta, { x: 1, y: 1 })).toEqual(
|
||||||
|
"translate3d(100px, 300px, 0px) scale(2, 4)"
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(buildProjectionTransform(delta, { x: 2, y: 0.5 })).toEqual(
|
||||||
|
"translate3d(50px, 600px, 0px) scale(0.5, 2) scale(4, 2)"
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
buildProjectionTransform(delta, { x: 2, y: 0.5 }, { rotate: 45 })
|
||||||
|
).toEqual(
|
||||||
|
"translate3d(50px, 600px, 0px) scale(0.5, 2) rotate(45deg) scale(4, 2)"
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
buildProjectionTransform(
|
||||||
|
{
|
||||||
|
x: {
|
||||||
|
translate: 100,
|
||||||
|
scale: 0.5,
|
||||||
|
origin: 0.5,
|
||||||
|
originPoint: 100,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
translate: 300,
|
||||||
|
scale: 0.5,
|
||||||
|
origin: 0.5,
|
||||||
|
originPoint: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ x: 2, y: 2 },
|
||||||
|
{ rotate: 90 }
|
||||||
|
)
|
||||||
|
).toEqual(
|
||||||
|
"translate3d(50px, 150px, 0px) scale(0.5, 0.5) rotate(90deg) "
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Doesn't apply unneccessary translation", () => {
|
||||||
|
const delta = {
|
||||||
|
x: {
|
||||||
|
translate: 0,
|
||||||
|
scale: 2,
|
||||||
|
origin: 0.5,
|
||||||
|
originPoint: 100,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
translate: 0,
|
||||||
|
scale: 4,
|
||||||
|
origin: 0.5,
|
||||||
|
originPoint: 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
buildProjectionTransform(delta, { x: 2, y: 4 }, { rotate: 10 })
|
||||||
|
).toEqual("scale(0.5, 0.25) rotate(10deg) scale(4, 16)")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Doesn't apply unneccessary tree scale", () => {
|
||||||
|
const delta = {
|
||||||
|
x: {
|
||||||
|
translate: 100,
|
||||||
|
scale: 2,
|
||||||
|
origin: 0.5,
|
||||||
|
originPoint: 100,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
translate: 100,
|
||||||
|
scale: 4,
|
||||||
|
origin: 0.5,
|
||||||
|
originPoint: 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
buildProjectionTransform(delta, { x: 1, y: 1 }, { rotate: 10 })
|
||||||
|
).toEqual("translate3d(100px, 100px, 0px) rotate(10deg) scale(2, 4)")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import type { Delta, Point } from "motion-utils"
|
||||||
|
import type { ResolvedValues } from "../../node/types"
|
||||||
|
|
||||||
|
export function buildProjectionTransform(
|
||||||
|
delta: Delta,
|
||||||
|
treeScale: Point,
|
||||||
|
latestTransform?: ResolvedValues
|
||||||
|
): string {
|
||||||
|
let transform = ""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The translations we use to calculate are always relative to the viewport coordinate space.
|
||||||
|
* But when we apply scales, we also scale the coordinate space of an element and its children.
|
||||||
|
* For instance if we have a treeScale (the culmination of all parent scales) of 0.5 and we need
|
||||||
|
* to move an element 100 pixels, we actually need to move it 200 in within that scaled space.
|
||||||
|
*/
|
||||||
|
const xTranslate = delta.x.translate / treeScale.x
|
||||||
|
const yTranslate = delta.y.translate / treeScale.y
|
||||||
|
const zTranslate = latestTransform?.z || 0
|
||||||
|
if (xTranslate || yTranslate || zTranslate) {
|
||||||
|
transform = `translate3d(${xTranslate}px, ${yTranslate}px, ${zTranslate}px) `
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply scale correction for the tree transform.
|
||||||
|
* This will apply scale to the screen-orientated axes.
|
||||||
|
*/
|
||||||
|
if (treeScale.x !== 1 || treeScale.y !== 1) {
|
||||||
|
transform += `scale(${1 / treeScale.x}, ${1 / treeScale.y}) `
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latestTransform) {
|
||||||
|
const {
|
||||||
|
transformPerspective,
|
||||||
|
rotate,
|
||||||
|
pathRotation,
|
||||||
|
rotateX,
|
||||||
|
rotateY,
|
||||||
|
skewX,
|
||||||
|
skewY,
|
||||||
|
} = latestTransform
|
||||||
|
if (transformPerspective)
|
||||||
|
transform = `perspective(${transformPerspective}px) ${transform}`
|
||||||
|
if (rotate) transform += `rotate(${rotate}deg) `
|
||||||
|
// Additive `rotate()` so user `rotate` isn't clobbered.
|
||||||
|
if (pathRotation) transform += `rotate(${pathRotation}deg) `
|
||||||
|
if (rotateX) transform += `rotateX(${rotateX}deg) `
|
||||||
|
if (rotateY) transform += `rotateY(${rotateY}deg) `
|
||||||
|
if (skewX) transform += `skewX(${skewX}deg) `
|
||||||
|
if (skewY) transform += `skewY(${skewY}deg) `
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply scale to match the size of the element to the size we want it.
|
||||||
|
* This will apply scale to the element-orientated axes.
|
||||||
|
*/
|
||||||
|
const elementScaleX = delta.x.scale * treeScale.x
|
||||||
|
const elementScaleY = delta.y.scale * treeScale.y
|
||||||
|
if (elementScaleX !== 1 || elementScaleY !== 1) {
|
||||||
|
transform += `scale(${elementScaleX}, ${elementScaleY})`
|
||||||
|
}
|
||||||
|
|
||||||
|
return transform || "none"
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { type AnyResolvedKeyframe } from "../../animation/types"
|
||||||
|
import { ResolvedValues } from "../../render/types"
|
||||||
|
|
||||||
|
function isIdentityScale(scale: AnyResolvedKeyframe | undefined) {
|
||||||
|
return scale === undefined || scale === 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasScale({ scale, scaleX, scaleY }: ResolvedValues) {
|
||||||
|
return (
|
||||||
|
!isIdentityScale(scale) ||
|
||||||
|
!isIdentityScale(scaleX) ||
|
||||||
|
!isIdentityScale(scaleY)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasTransform(values: ResolvedValues) {
|
||||||
|
return (
|
||||||
|
hasScale(values) ||
|
||||||
|
has2DTranslate(values) ||
|
||||||
|
values.z ||
|
||||||
|
values.rotate ||
|
||||||
|
values.rotateX ||
|
||||||
|
values.rotateY ||
|
||||||
|
values.skewX ||
|
||||||
|
values.skewY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function has2DTranslate(values: ResolvedValues) {
|
||||||
|
return is2DTranslate(values.x) || is2DTranslate(values.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
function is2DTranslate(value: AnyResolvedKeyframe | undefined) {
|
||||||
|
return value && value !== "0%"
|
||||||
|
}
|
||||||
Reference in new issue
Block a user