UNPKG

framer-motion

Version:

An open source and production-ready motion library for React on the web

157 lines (132 loc) • 4.65 kB
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] } }