framer-motion
Version:
An open source and production-ready motion library for React on the web
157 lines (132 loc) • 4.65 kB
text/typescript
import { Axis } from "../../../types/geometry"
import { complex, px } from "style-value-types"
import { mix } from "popmotion"
import { cssVariableRegex } from "../utils/css-variables-conversion"
import { LayoutState, TargetProjection } from "../../utils/state"
type ScaleCorrection = (
latest: string | number,
layoutState: LayoutState,
projection: TargetProjection
) => string | number
interface ScaleCorrectionDefinition {
process: ScaleCorrection
applyTo?: string[]
}
type ScaleCorrectionDefinitionMap = {
[key: string]: ScaleCorrectionDefinition
}
export function pixelsToPercent(pixels: number, axis: Axis): number {
return (pixels / (axis.max - axis.min)) * 100
}
/**
* We always correct borderRadius as a percentage rather than pixels to reduce paints.
* For example, if you are projecting a box that is 100px wide with a 10px borderRadius
* into a box that is 200px wide with a 20px borderRadius, that is actually a 10%
* borderRadius in both states. If we animate between the two in pixels that will trigger
* a paint each time. If we animate between the two in percentage we'll avoid a paint.
*/
export function correctBorderRadius(
latest: string | number,
_layoutState: LayoutState,
{ target }: TargetProjection
) {
/**
* If latest is a string, if it's a percentage we can return immediately as it's
* going to be stretched appropriately. Otherwise, if it's a pixel, convert it to a number.
*/
if (typeof latest === "string") {
if (px.test(latest)) {
latest = parseFloat(latest)
} else {
return latest
}
}
/**
* If latest is a number, it's a pixel value. We use the current viewportBox to calculate that
* pixel value as a percentage of each axis
*/
const x = pixelsToPercent(latest, target.x)
const y = pixelsToPercent(latest, target.y)
return `${x}% ${y}%`
}
const varToken = "_$css"
export function correctBoxShadow(
latest: string,
{ delta, treeScale }: LayoutState
) {
const original = latest
/**
* We need to first strip and store CSS variables from the string.
*/
const containsCSSVariables = latest.includes("var(")
const cssVariables: string[] = []
if (containsCSSVariables) {
latest = latest.replace(cssVariableRegex, (match) => {
cssVariables.push(match)
return varToken
})
}
const shadow = complex.parse(latest)
// TODO: Doesn't support multiple shadows
if (shadow.length > 5) return original
const template = complex.createTransformer(latest)
const offset = typeof shadow[0] !== "number" ? 1 : 0
// Calculate the overall context scale
const xScale = delta.x.scale * treeScale.x
const yScale = delta.y.scale * treeScale.y
// Scale x/y
;(shadow[0 + offset] as number) /= xScale
;(shadow[1 + offset] as number) /= yScale
/**
* Ideally we'd correct x and y scales individually, but because blur and
* spread apply to both we have to take a scale average and apply that instead.
* We could potentially improve the outcome of this by incorporating the ratio between
* the two scales.
*/
const averageScale = mix(xScale, yScale, 0.5)
// Blur
if (typeof shadow[2 + offset] === "number")
(shadow[2 + offset] as number) /= averageScale
// Spread
if (typeof shadow[3 + offset] === "number")
(shadow[3 + offset] as number) /= averageScale
let output = template(shadow)
if (containsCSSVariables) {
let i = 0
output = output.replace(varToken, () => {
const cssVariable = cssVariables[i]
i++
return cssVariable
})
}
return output
}
const borderCorrectionDefinition = {
process: correctBorderRadius,
}
export const valueScaleCorrection: ScaleCorrectionDefinitionMap = {
borderRadius: {
...borderCorrectionDefinition,
applyTo: [
"borderTopLeftRadius",
"borderTopRightRadius",
"borderBottomLeftRadius",
"borderBottomRightRadius",
],
},
borderTopLeftRadius: borderCorrectionDefinition,
borderTopRightRadius: borderCorrectionDefinition,
borderBottomLeftRadius: borderCorrectionDefinition,
borderBottomRightRadius: borderCorrectionDefinition,
boxShadow: {
process: correctBoxShadow,
},
}
/**
* @internal
*/
export function addScaleCorrection(correctors: ScaleCorrectionDefinitionMap) {
for (const key in correctors) {
valueScaleCorrection[key] = correctors[key]
}
}