UNPKG

framer-motion

Version:

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

306 lines (265 loc) • 10.2 kB
import { number, px, ValueType } from "style-value-types" import { Target, TargetWithKeyframes } from "../../../types" import { isKeyframesTarget } from "../../../animation/utils/is-keyframes-target" import { invariant } from "hey-listen" import { MotionValue } from "../../../value" import { transformProps } from "./transform" import { AxisBox2D } from "../../../types/geometry" import { findDimensionValueType } from "./value-types" import { VisualElement } from "../../types" const positionalKeys = new Set([ "width", "height", "top", "left", "right", "bottom", "x", "y", ]) const isPositionalKey = (key: string) => positionalKeys.has(key) const hasPositionalKey = (target: TargetWithKeyframes) => { return Object.keys(target).some(isPositionalKey) } const setAndResetVelocity = (value: MotionValue, to: string | number) => { // Looks odd but setting it twice doesn't render, it'll just // set both prev and current to the latest value value.set(to, false) value.set(to) } const isNumOrPxType = (v?: ValueType): v is ValueType => v === number || v === px export enum BoundingBoxDimension { width = "width", height = "height", left = "left", right = "right", top = "top", bottom = "bottom", } type GetActualMeasurementInPixels = ( bbox: AxisBox2D, computedStyle: Partial<CSSStyleDeclaration> ) => number const getPosFromMatrix = (matrix: string, pos: number) => parseFloat(matrix.split(", ")[pos]) const getTranslateFromMatrix = ( pos2: number, pos3: number ): GetActualMeasurementInPixels => (_bbox, { transform }) => { if (transform === "none" || !transform) return 0 const matrix3d = transform.match(/^matrix3d\((.+)\)$/) if (matrix3d) { return getPosFromMatrix(matrix3d[1], pos3) } else { const matrix = transform.match(/^matrix\((.+)\)$/) as string[] if (matrix) { return getPosFromMatrix(matrix[1], pos2) } else { return 0 } } } const transformKeys = new Set(["x", "y", "z"]) const nonTranslationalTransformKeys = transformProps.filter( (key) => !transformKeys.has(key) ) type RemovedTransforms = [string, string | number][] function removeNonTranslationalTransform(visualElement: VisualElement) { const removedTransforms: RemovedTransforms = [] nonTranslationalTransformKeys.forEach((key) => { const value: | MotionValue<string | number> | undefined = visualElement.getValue(key) if (value !== undefined) { removedTransforms.push([key, value.get()]) value.set(key.startsWith("scale") ? 1 : 0) } }) // Apply changes to element before measurement if (removedTransforms.length) visualElement.syncRender() return removedTransforms } const positionalValues: { [key: string]: GetActualMeasurementInPixels } = { // Dimensions width: ({ x }) => x.max - x.min, height: ({ y }) => y.max - y.min, top: (_bbox, { top }) => parseFloat(top as string), left: (_bbox, { left }) => parseFloat(left as string), bottom: ({ y }, { top }) => parseFloat(top as string) + (y.max - y.min), right: ({ x }, { left }) => parseFloat(left as string) + (x.max - x.min), // Transform x: getTranslateFromMatrix(4, 13), y: getTranslateFromMatrix(5, 14), } const convertChangedValueTypes = ( target: TargetWithKeyframes, visualElement: VisualElement, changedKeys: string[] ) => { const originBbox = visualElement.measureViewportBox() const element = visualElement.getInstance() const elementComputedStyle = getComputedStyle(element) const { display, top, left, bottom, right, transform, } = elementComputedStyle const originComputedStyle = { top, left, bottom, right, transform } // If the element is currently set to display: "none", make it visible before // measuring the target bounding box if (display === "none") { visualElement.setStaticValue( "display", (target.display as string) || "block" ) } // Apply the latest values (as set in checkAndConvertChangedValueTypes) visualElement.syncRender() const targetBbox = visualElement.measureViewportBox() changedKeys.forEach((key) => { // Restore styles to their **calculated computed style**, not their actual // originally set style. This allows us to animate between equivalent pixel units. const value = visualElement.getValue(key) as MotionValue setAndResetVelocity( value, positionalValues[key](originBbox, originComputedStyle) ) target[key] = positionalValues[key](targetBbox, elementComputedStyle) }) return target } const checkAndConvertChangedValueTypes = ( visualElement: VisualElement, target: TargetWithKeyframes, origin: Target = {}, transitionEnd: Target = {} ): { target: TargetWithKeyframes; transitionEnd: Target } => { target = { ...target } transitionEnd = { ...transitionEnd } const targetPositionalKeys = Object.keys(target).filter(isPositionalKey) // We want to remove any transform values that could affect the element's bounding box before // it's measured. We'll reapply these later. let removedTransformValues: RemovedTransforms = [] let hasAttemptedToRemoveTransformValues = false const changedValueTypeKeys: string[] = [] targetPositionalKeys.forEach((key) => { const value = visualElement.getValue(key) as MotionValue< number | string > if (!visualElement.hasValue(key)) return const from = origin[key] const to = target[key] const fromType = findDimensionValueType(from) let toType // TODO: The current implementation of this basically throws an error // if you try and do value conversion via keyframes. There's probably // a way of doing this but the performance implications would need greater scrutiny, // as it'd be doing multiple resize-remeasure operations. if (isKeyframesTarget(to)) { const numKeyframes = to.length for (let i = to[0] === null ? 1 : 0; i < numKeyframes; i++) { if (!toType) { toType = findDimensionValueType(to[i]) invariant( toType === fromType || (isNumOrPxType(fromType) && isNumOrPxType(toType)), "Keyframes must be of the same dimension as the current value" ) } else { invariant( findDimensionValueType(to[i]) === toType, "All keyframes must be of the same type" ) } } } else { toType = findDimensionValueType(to) } if (fromType !== toType) { // If they're both just number or px, convert them both to numbers rather than // relying on resize/remeasure to convert (which is wasteful in this situation) if (isNumOrPxType(fromType) && isNumOrPxType(toType)) { const current = value.get() if (typeof current === "string") { value.set(parseFloat(current)) } if (typeof to === "string") { target[key] = parseFloat(to) } else if (Array.isArray(to) && toType === px) { target[key] = to.map(parseFloat) } } else if ( fromType?.transform && toType?.transform && (from === 0 || to === 0) ) { // If one or the other value is 0, it's safe to coerce it to the // type of the other without measurement if (from === 0) { value.set((toType as any).transform(from)) } else { target[key] = (fromType as any).transform(to) } } else { // If we're going to do value conversion via DOM measurements, we first // need to remove non-positional transform values that could affect the bbox measurements. if (!hasAttemptedToRemoveTransformValues) { removedTransformValues = removeNonTranslationalTransform( visualElement ) hasAttemptedToRemoveTransformValues = true } changedValueTypeKeys.push(key) transitionEnd[key] = transitionEnd[key] !== undefined ? transitionEnd[key] : target[key] setAndResetVelocity(value, to) } } }) if (changedValueTypeKeys.length) { const convertedTarget = convertChangedValueTypes( target, visualElement, changedValueTypeKeys ) // If we removed transform values, reapply them before the next render if (removedTransformValues.length) { removedTransformValues.forEach(([key, value]) => { visualElement.getValue(key)!.set(value) }) } // Reapply original values visualElement.syncRender() return { target: convertedTarget, transitionEnd } } else { return { target, transitionEnd } } } /** * Convert value types for x/y/width/height/top/left/bottom/right * * Allows animation between `'auto'` -> `'100%'` or `0` -> `'calc(50% - 10vw)'` * * @internal */ export function unitConversion( visualElement: VisualElement, target: TargetWithKeyframes, origin?: Target, transitionEnd?: Target ): { target: TargetWithKeyframes; transitionEnd?: Target } { return hasPositionalKey(target) ? checkAndConvertChangedValueTypes( visualElement, target, origin, transitionEnd ) : { target, transitionEnd } }