From 67365fb75ece7677e41b12e60286a0a8f5a969bf Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Mon, 12 Jan 2026 15:48:33 +0100 Subject: [PATCH 01/11] Move VisualElement class hierarchy to motion-dom This refactoring moves the core VisualElement classes from framer-motion to motion-dom, making them framework-agnostic: - VisualElement base class with injectable feature definitions - DOMVisualElement, HTMLVisualElement, SVGVisualElement - ObjectVisualElement for animating plain JS objects - Feature base class - visualElementStore - Animation state management utilities - Variant resolution utilities - Reduced motion utilities - Projection geometry, styles, and animation utilities React-specific code (features, contexts, hooks) remains in framer-motion. The projection node core is kept in framer-motion due to complex React deps. Co-Authored-By: Claude Opus 4.5 --- packages/framer-motion/src/index.ts | 11 +- packages/motion-dom/src/index.ts | 73 ++ .../src/projection/animation/mix-values.ts | 116 +++ .../src/projection/geometry/conversion.ts | 43 + .../src/projection/geometry/copy.ts | 33 + .../src/projection/geometry/delta-apply.ts | 193 ++++ .../src/projection/geometry/delta-calc.ts | 94 ++ .../src/projection/geometry/delta-remove.ts | 123 +++ .../src/projection/geometry/index.ts | 7 + .../src/projection/geometry/models.ts | 20 + .../src/projection/geometry/utils.ts | 41 + .../motion-dom/src/projection/styles/index.ts | 5 + .../projection/styles/scale-border-radius.ts | 42 + .../src/projection/styles/scale-box-shadow.ts | 42 + .../src/projection/styles/scale-correction.ts | 30 + .../src/projection/styles/transform.ts | 55 ++ .../motion-dom/src/projection/styles/types.ts | 27 + .../src/projection/utils/has-transform.ts | 35 + .../src/projection/utils/measure.ts | 31 + packages/motion-dom/src/render/Feature.ts | 24 + .../motion-dom/src/render/VisualElement.ts | 886 ++++++++++++++++++ .../src/render/dom/DOMVisualElement.ts | 58 ++ packages/motion-dom/src/render/dom/types.ts | 18 + .../src/render/html/HTMLVisualElement.ts | 74 ++ packages/motion-dom/src/render/html/types.ts | 33 + .../src/render/html/utils/build-styles.ts | 80 ++ .../src/render/html/utils/build-transform.ts | 78 ++ .../src/render/html/utils/render.ts | 26 + .../render/html/utils/scrape-motion-values.ts | 29 + .../src/render/object/ObjectVisualElement.ts | 56 ++ packages/motion-dom/src/render/store.ts | 3 + .../src/render/svg/SVGVisualElement.ts | 81 ++ packages/motion-dom/src/render/svg/types.ts | 10 + .../src/render/svg/utils/build-attrs.ts | 97 ++ .../src/render/svg/utils/camel-case-attrs.ts | 28 + .../src/render/svg/utils/is-svg-tag.ts | 2 + .../motion-dom/src/render/svg/utils/path.ts | 42 + .../motion-dom/src/render/svg/utils/render.ts | 21 + .../render/svg/utils/scrape-motion-values.ts | 33 + packages/motion-dom/src/render/types.ts | 158 ++++ .../src/render/utils/animation-state.ts | 503 ++++++++++ .../src/render/utils/calc-child-stagger.ts | 26 + .../src/render/utils/get-variant-context.ts | 46 + .../src/render/utils/is-animation-controls.ts | 9 + .../render/utils/is-controlling-variants.ts | 17 + .../render/utils/is-forced-motion-value.ts | 22 + .../src/render/utils/is-keyframes-target.ts | 7 + .../src/render/utils/is-variant-label.ts | 6 + .../src/render/utils/motion-values.ts | 64 ++ .../src/render/utils/reduced-motion/index.ts | 23 + .../src/render/utils/reduced-motion/state.ts | 8 + .../render/utils/resolve-dynamic-variants.ts | 34 + .../src/render/utils/resolve-variants.ts | 73 ++ .../src/render/utils/shallow-compare.ts | 13 + .../src/render/utils/variant-props.ts | 13 + 55 files changed, 3721 insertions(+), 1 deletion(-) create mode 100644 packages/motion-dom/src/projection/animation/mix-values.ts create mode 100644 packages/motion-dom/src/projection/geometry/conversion.ts create mode 100644 packages/motion-dom/src/projection/geometry/copy.ts create mode 100644 packages/motion-dom/src/projection/geometry/delta-apply.ts create mode 100644 packages/motion-dom/src/projection/geometry/delta-calc.ts create mode 100644 packages/motion-dom/src/projection/geometry/delta-remove.ts create mode 100644 packages/motion-dom/src/projection/geometry/index.ts create mode 100644 packages/motion-dom/src/projection/geometry/models.ts create mode 100644 packages/motion-dom/src/projection/geometry/utils.ts create mode 100644 packages/motion-dom/src/projection/styles/index.ts create mode 100644 packages/motion-dom/src/projection/styles/scale-border-radius.ts create mode 100644 packages/motion-dom/src/projection/styles/scale-box-shadow.ts create mode 100644 packages/motion-dom/src/projection/styles/scale-correction.ts create mode 100644 packages/motion-dom/src/projection/styles/transform.ts create mode 100644 packages/motion-dom/src/projection/styles/types.ts create mode 100644 packages/motion-dom/src/projection/utils/has-transform.ts create mode 100644 packages/motion-dom/src/projection/utils/measure.ts create mode 100644 packages/motion-dom/src/render/Feature.ts create mode 100644 packages/motion-dom/src/render/VisualElement.ts create mode 100644 packages/motion-dom/src/render/dom/DOMVisualElement.ts create mode 100644 packages/motion-dom/src/render/dom/types.ts create mode 100644 packages/motion-dom/src/render/html/HTMLVisualElement.ts create mode 100644 packages/motion-dom/src/render/html/types.ts create mode 100644 packages/motion-dom/src/render/html/utils/build-styles.ts create mode 100644 packages/motion-dom/src/render/html/utils/build-transform.ts create mode 100644 packages/motion-dom/src/render/html/utils/render.ts create mode 100644 packages/motion-dom/src/render/html/utils/scrape-motion-values.ts create mode 100644 packages/motion-dom/src/render/object/ObjectVisualElement.ts create mode 100644 packages/motion-dom/src/render/store.ts create mode 100644 packages/motion-dom/src/render/svg/SVGVisualElement.ts create mode 100644 packages/motion-dom/src/render/svg/utils/build-attrs.ts create mode 100644 packages/motion-dom/src/render/svg/utils/camel-case-attrs.ts create mode 100644 packages/motion-dom/src/render/svg/utils/is-svg-tag.ts create mode 100644 packages/motion-dom/src/render/svg/utils/path.ts create mode 100644 packages/motion-dom/src/render/svg/utils/render.ts create mode 100644 packages/motion-dom/src/render/svg/utils/scrape-motion-values.ts create mode 100644 packages/motion-dom/src/render/types.ts create mode 100644 packages/motion-dom/src/render/utils/animation-state.ts create mode 100644 packages/motion-dom/src/render/utils/calc-child-stagger.ts create mode 100644 packages/motion-dom/src/render/utils/get-variant-context.ts create mode 100644 packages/motion-dom/src/render/utils/is-animation-controls.ts create mode 100644 packages/motion-dom/src/render/utils/is-controlling-variants.ts create mode 100644 packages/motion-dom/src/render/utils/is-forced-motion-value.ts create mode 100644 packages/motion-dom/src/render/utils/is-keyframes-target.ts create mode 100644 packages/motion-dom/src/render/utils/is-variant-label.ts create mode 100644 packages/motion-dom/src/render/utils/motion-values.ts create mode 100644 packages/motion-dom/src/render/utils/reduced-motion/index.ts create mode 100644 packages/motion-dom/src/render/utils/reduced-motion/state.ts create mode 100644 packages/motion-dom/src/render/utils/resolve-dynamic-variants.ts create mode 100644 packages/motion-dom/src/render/utils/resolve-variants.ts create mode 100644 packages/motion-dom/src/render/utils/shallow-compare.ts create mode 100644 packages/motion-dom/src/render/utils/variant-props.ts diff --git a/packages/framer-motion/src/index.ts b/packages/framer-motion/src/index.ts index c0f9b5aa27..d0194e63a8 100644 --- a/packages/framer-motion/src/index.ts +++ b/packages/framer-motion/src/index.ts @@ -125,7 +125,16 @@ export { SwitchLayoutGroupContext } from "./context/SwitchLayoutGroupContext" export type { AnimatePresenceProps } from "./components/AnimatePresence/types" export type { LazyProps } from "./components/LazyMotion/types" export type { MotionConfigProps } from "./components/MotionConfig" -export type * from "./motion/features/types" +export type { + HydratedFeatureDefinition, + HydratedFeatureDefinitions, + FeatureDefinition, + FeatureDefinitions, + FeaturePackage, + FeaturePackages, + FeatureBundle, + LazyFeatureBundle, +} from "./motion/features/types" export type { MotionProps, MotionStyle, diff --git a/packages/motion-dom/src/index.ts b/packages/motion-dom/src/index.ts index 571e450a9b..3680a59fce 100644 --- a/packages/motion-dom/src/index.ts +++ b/packages/motion-dom/src/index.ts @@ -126,6 +126,79 @@ export * from "./view/types" export * from "./view/utils/get-layer-info" export * from "./view/utils/get-view-animations" +// Visual Element +export { VisualElement, setFeatureDefinitions, getFeatureDefinitions } from "./render/VisualElement" +export type { MotionStyle } from "./render/VisualElement" +export { Feature } from "./render/Feature" +export { DOMVisualElement } from "./render/dom/DOMVisualElement" +export { HTMLVisualElement } from "./render/html/HTMLVisualElement" +export { SVGVisualElement } from "./render/svg/SVGVisualElement" +export { ObjectVisualElement } from "./render/object/ObjectVisualElement" +export { visualElementStore } from "./render/store" +export type { + ResolvedValues, + PresenceContextProps, + ReducedMotionConfig, + MotionConfigContextProps, + VisualState, + VisualElementOptions, + VisualElementEventCallbacks, + LayoutLifecycles, + ScrapeMotionValuesFromProps, + UseRenderState, + AnimationType, + FeatureClass, +} from "./render/types" +export * from "./render/dom/types" +export * from "./render/html/types" + +// Animation State +export { createAnimationState, checkVariantsDidChange } from "./render/utils/animation-state" +export type { AnimationState, AnimationTypeState, AnimationList } from "./render/utils/animation-state" + +// Variant utilities +export { isVariantLabel } from "./render/utils/is-variant-label" +export { isControllingVariants, isVariantNode } from "./render/utils/is-controlling-variants" +export { getVariantContext } from "./render/utils/get-variant-context" +export { resolveVariantFromProps } from "./render/utils/resolve-variants" +export { resolveVariant } from "./render/utils/resolve-dynamic-variants" +export { updateMotionValuesFromProps } from "./render/utils/motion-values" +export { variantProps, variantPriorityOrder } from "./render/utils/variant-props" +export { isAnimationControls } from "./render/utils/is-animation-controls" +export { isForcedMotionValue, scaleCorrectors, addScaleCorrectors } from "./render/utils/is-forced-motion-value" + +// Reduced motion +export { + initPrefersReducedMotion, + hasReducedMotionListener, + prefersReducedMotion, +} from "./render/utils/reduced-motion" + +// Projection geometry +export * from "./projection/geometry" +export { hasTransform, hasScale, has2DTranslate } from "./projection/utils/has-transform" +export { measureViewportBox, measurePageBox } from "./projection/utils/measure" + +// Projection styles +export * from "./projection/styles/types" +export { pixelsToPercent, correctBorderRadius } from "./projection/styles/scale-border-radius" +export { correctBoxShadow } from "./projection/styles/scale-box-shadow" +export { buildProjectionTransform } from "./projection/styles/transform" + +// Projection animation +export { mixValues } from "./projection/animation/mix-values" + +// HTML/SVG utilities +export { buildHTMLStyles } from "./render/html/utils/build-styles" +export { buildTransform } from "./render/html/utils/build-transform" +export { renderHTML } from "./render/html/utils/render" +export { buildSVGAttrs } from "./render/svg/utils/build-attrs" +export { renderSVG } from "./render/svg/utils/render" +export { buildSVGPath } from "./render/svg/utils/path" +export { camelCaseAttributes } from "./render/svg/utils/camel-case-attrs" +export { isSVGTag } from "./render/svg/utils/is-svg-tag" +export { camelToDash } from "./render/dom/utils/camel-to-dash" + /** * Deprecated */ diff --git a/packages/motion-dom/src/projection/animation/mix-values.ts b/packages/motion-dom/src/projection/animation/mix-values.ts new file mode 100644 index 0000000000..e6a701939d --- /dev/null +++ b/packages/motion-dom/src/projection/animation/mix-values.ts @@ -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" + +const borders = ["TopLeft", "TopRight", "BottomLeft", "BottomRight"] +const numBorders = borders.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 = `border${borders[i]}Radius` + 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)) + } +} diff --git a/packages/motion-dom/src/projection/geometry/conversion.ts b/packages/motion-dom/src/projection/geometry/conversion.ts new file mode 100644 index 0000000000..ba7e950095 --- /dev/null +++ b/packages/motion-dom/src/projection/geometry/conversion.ts @@ -0,0 +1,43 @@ +import { BoundingBox, Box, TransformPoint } from "motion-utils" + +/** + * Bounding boxes tend to be defined as top, left, right, bottom. For various operations + * it's easier to consider each axis individually. This function returns a bounding box + * as a map of single-axis min/max values. + */ +export function convertBoundingBoxToBox({ + top, + left, + right, + bottom, +}: BoundingBox): Box { + return { + x: { min: left, max: right }, + y: { min: top, max: bottom }, + } +} + +export function convertBoxToBoundingBox({ x, y }: Box): BoundingBox { + return { top: y.min, right: x.max, bottom: y.max, left: x.min } +} + +/** + * Applies a TransformPoint function to a bounding box. TransformPoint is usually a function + * provided by Framer to allow measured points to be corrected for device scaling. This is used + * when measuring DOM elements and DOM event points. + */ +export function transformBoxPoints( + point: BoundingBox, + transformPoint?: TransformPoint +) { + if (!transformPoint) return point + const topLeft = transformPoint({ x: point.left, y: point.top }) + const bottomRight = transformPoint({ x: point.right, y: point.bottom }) + + return { + top: topLeft.y, + left: topLeft.x, + bottom: bottomRight.y, + right: bottomRight.x, + } +} diff --git a/packages/motion-dom/src/projection/geometry/copy.ts b/packages/motion-dom/src/projection/geometry/copy.ts new file mode 100644 index 0000000000..a642c61641 --- /dev/null +++ b/packages/motion-dom/src/projection/geometry/copy.ts @@ -0,0 +1,33 @@ +import { Axis, AxisDelta, Box } from "motion-utils" + +/** + * Reset an axis to the provided origin box. + * + * This is a mutative operation. + */ +export function copyAxisInto(axis: Axis, originAxis: Axis) { + axis.min = originAxis.min + axis.max = originAxis.max +} + +/** + * Reset a box to the provided origin box. + * + * This is a mutative operation. + */ +export function copyBoxInto(box: Box, originBox: Box) { + copyAxisInto(box.x, originBox.x) + copyAxisInto(box.y, originBox.y) +} + +/** + * Reset a delta to the provided origin box. + * + * This is a mutative operation. + */ +export function copyAxisDeltaInto(delta: AxisDelta, originDelta: AxisDelta) { + delta.translate = originDelta.translate + delta.scale = originDelta.scale + delta.originPoint = originDelta.originPoint + delta.origin = originDelta.origin +} diff --git a/packages/motion-dom/src/projection/geometry/delta-apply.ts b/packages/motion-dom/src/projection/geometry/delta-apply.ts new file mode 100644 index 0000000000..7e72e695d2 --- /dev/null +++ b/packages/motion-dom/src/projection/geometry/delta-apply.ts @@ -0,0 +1,193 @@ +import { Axis, Box, Delta, Point } from "motion-utils" +import { mixNumber } from "../../utils/mix/number" +import { ResolvedValues } from "../../render/types" +import { hasTransform } from "../utils/has-transform" + +/** + * Scales a point based on a factor and an originPoint + */ +export function scalePoint(point: number, scale: number, originPoint: number) { + const distanceFromOrigin = point - originPoint + const scaled = scale * distanceFromOrigin + return originPoint + scaled +} + +/** + * Applies a translate/scale delta to a point + */ +export function applyPointDelta( + point: number, + translate: number, + scale: number, + originPoint: number, + boxScale?: number +): number { + if (boxScale !== undefined) { + point = scalePoint(point, boxScale, originPoint) + } + + return scalePoint(point, scale, originPoint) + translate +} + +/** + * Applies a translate/scale delta to an axis + */ +export function applyAxisDelta( + axis: Axis, + translate: number = 0, + scale: number = 1, + originPoint: number, + boxScale?: number +): void { + axis.min = applyPointDelta( + axis.min, + translate, + scale, + originPoint, + boxScale + ) + + axis.max = applyPointDelta( + axis.max, + translate, + scale, + originPoint, + boxScale + ) +} + +/** + * Applies a translate/scale delta to a box + */ +export function applyBoxDelta(box: Box, { x, y }: Delta): void { + applyAxisDelta(box.x, x.translate, x.scale, x.originPoint) + applyAxisDelta(box.y, y.translate, y.scale, y.originPoint) +} + +const TREE_SCALE_SNAP_MIN = 0.999999999999 +const TREE_SCALE_SNAP_MAX = 1.0000000000001 + +/** + * Apply a tree of deltas to a box. We do this to calculate the effect of all the transforms + * in a tree upon our box before then calculating how to project it into our desired viewport-relative box + * + * This is the final nested loop within updateLayoutDelta for future refactoring + */ +export function applyTreeDeltas( + box: Box, + treeScale: Point, + treePath: any[], + isSharedTransition: boolean = false +) { + const treeLength = treePath.length + if (!treeLength) return + + // Reset the treeScale + treeScale.x = treeScale.y = 1 + + let node: any + let delta: Delta | undefined + + for (let i = 0; i < treeLength; i++) { + node = treePath[i] + delta = node.projectionDelta + + /** + * TODO: Prefer to remove this, but currently we have motion components with + * display: contents in Framer. + */ + const { visualElement } = node.options + if ( + visualElement && + visualElement.props.style && + visualElement.props.style.display === "contents" + ) { + continue + } + + if ( + isSharedTransition && + node.options.layoutScroll && + node.scroll && + node !== node.root + ) { + transformBox(box, { + x: -node.scroll.offset.x, + y: -node.scroll.offset.y, + }) + } + + if (delta) { + // Incoporate each ancestor's scale into a cumulative treeScale for this component + treeScale.x *= delta.x.scale + treeScale.y *= delta.y.scale + + // Apply each ancestor's calculated delta into this component's recorded layout box + applyBoxDelta(box, delta) + } + + if (isSharedTransition && hasTransform(node.latestValues)) { + transformBox(box, node.latestValues) + } + } + + /** + * Snap tree scale back to 1 if it's within a non-perceivable threshold. + * This will help reduce useless scales getting rendered. + */ + if ( + treeScale.x < TREE_SCALE_SNAP_MAX && + treeScale.x > TREE_SCALE_SNAP_MIN + ) { + treeScale.x = 1.0 + } + if ( + treeScale.y < TREE_SCALE_SNAP_MAX && + treeScale.y > TREE_SCALE_SNAP_MIN + ) { + treeScale.y = 1.0 + } +} + +export function translateAxis(axis: Axis, distance: number) { + axis.min = axis.min + distance + axis.max = axis.max + distance +} + +/** + * Apply a transform to an axis from the latest resolved motion values. + * This function basically acts as a bridge between a flat motion value map + * and applyAxisDelta + */ +export function transformAxis( + axis: Axis, + axisTranslate?: number, + axisScale?: number, + boxScale?: number, + axisOrigin: number = 0.5 +): void { + const originPoint = mixNumber(axis.min, axis.max, axisOrigin) + + // Apply the axis delta to the final axis + applyAxisDelta(axis, axisTranslate, axisScale, originPoint, boxScale) +} + +/** + * Apply a transform to a box from the latest resolved motion values. + */ +export function transformBox(box: Box, transform: ResolvedValues) { + transformAxis( + box.x, + transform.x as number, + transform.scaleX as number, + transform.scale as number, + transform.originX as number + ) + transformAxis( + box.y, + transform.y as number, + transform.scaleY as number, + transform.scale as number, + transform.originY as number + ) +} diff --git a/packages/motion-dom/src/projection/geometry/delta-calc.ts b/packages/motion-dom/src/projection/geometry/delta-calc.ts new file mode 100644 index 0000000000..6f405bc625 --- /dev/null +++ b/packages/motion-dom/src/projection/geometry/delta-calc.ts @@ -0,0 +1,94 @@ +import { Axis, AxisDelta, Box, Delta } from "motion-utils" +import { mixNumber } from "../../utils/mix/number" +import { ResolvedValues } from "../../render/types" + +const SCALE_PRECISION = 0.0001 +const SCALE_MIN = 1 - SCALE_PRECISION +const SCALE_MAX = 1 + SCALE_PRECISION +const TRANSLATE_PRECISION = 0.01 +const TRANSLATE_MIN = 0 - TRANSLATE_PRECISION +const TRANSLATE_MAX = 0 + TRANSLATE_PRECISION + +export function calcLength(axis: Axis) { + return axis.max - axis.min +} + +export function isNear( + value: number, + target: number, + maxDistance: number +): boolean { + return Math.abs(value - target) <= maxDistance +} + +export function calcAxisDelta( + delta: AxisDelta, + source: Axis, + target: Axis, + origin: number = 0.5 +) { + delta.origin = origin + delta.originPoint = mixNumber(source.min, source.max, delta.origin) + delta.scale = calcLength(target) / calcLength(source) + delta.translate = + mixNumber(target.min, target.max, delta.origin) - delta.originPoint + + if ( + (delta.scale >= SCALE_MIN && delta.scale <= SCALE_MAX) || + isNaN(delta.scale) + ) { + delta.scale = 1.0 + } + + if ( + (delta.translate >= TRANSLATE_MIN && + delta.translate <= TRANSLATE_MAX) || + isNaN(delta.translate) + ) { + delta.translate = 0.0 + } +} + +export function calcBoxDelta( + delta: Delta, + source: Box, + target: Box, + origin?: ResolvedValues +): void { + calcAxisDelta( + delta.x, + source.x, + target.x, + origin ? (origin.originX as number) : undefined + ) + calcAxisDelta( + delta.y, + source.y, + target.y, + origin ? (origin.originY as number) : undefined + ) +} + +export function calcRelativeAxis(target: Axis, relative: Axis, parent: Axis) { + target.min = parent.min + relative.min + target.max = target.min + calcLength(relative) +} + +export function calcRelativeBox(target: Box, relative: Box, parent: Box) { + calcRelativeAxis(target.x, relative.x, parent.x) + calcRelativeAxis(target.y, relative.y, parent.y) +} + +export function calcRelativeAxisPosition( + target: Axis, + layout: Axis, + parent: Axis +) { + target.min = layout.min - parent.min + target.max = target.min + calcLength(layout) +} + +export function calcRelativePosition(target: Box, layout: Box, parent: Box) { + calcRelativeAxisPosition(target.x, layout.x, parent.x) + calcRelativeAxisPosition(target.y, layout.y, parent.y) +} diff --git a/packages/motion-dom/src/projection/geometry/delta-remove.ts b/packages/motion-dom/src/projection/geometry/delta-remove.ts new file mode 100644 index 0000000000..0d196c4daf --- /dev/null +++ b/packages/motion-dom/src/projection/geometry/delta-remove.ts @@ -0,0 +1,123 @@ +import { Axis, Box } from "motion-utils" +import { mixNumber } from "../../utils/mix/number" +import { percent } from "../../value/types/numbers/units" +import { ResolvedValues } from "../../render/types" +import { scalePoint } from "./delta-apply" + +/** + * Remove a delta from a point. This is essentially the steps of applyPointDelta in reverse + */ +export function removePointDelta( + point: number, + translate: number, + scale: number, + originPoint: number, + boxScale?: number +): number { + point -= translate + point = scalePoint(point, 1 / scale, originPoint) + + if (boxScale !== undefined) { + point = scalePoint(point, 1 / boxScale, originPoint) + } + + return point +} + +/** + * Remove a delta from an axis. This is essentially the steps of applyAxisDelta in reverse + */ +export function removeAxisDelta( + axis: Axis, + translate: number | string = 0, + scale: number = 1, + origin: number = 0.5, + boxScale?: number, + originAxis: Axis = axis, + sourceAxis: Axis = axis +): void { + if (percent.test(translate)) { + translate = parseFloat(translate as string) + const relativeProgress = mixNumber( + sourceAxis.min, + sourceAxis.max, + translate / 100 + ) + translate = relativeProgress - sourceAxis.min + } + + if (typeof translate !== "number") return + + let originPoint = mixNumber(originAxis.min, originAxis.max, origin) + if (axis === originAxis) originPoint -= translate + + axis.min = removePointDelta( + axis.min, + translate, + scale, + originPoint, + boxScale + ) + + axis.max = removePointDelta( + axis.max, + translate, + scale, + originPoint, + boxScale + ) +} + +/** + * Remove a transforms from an axis. This is essentially the steps of applyAxisTransforms in reverse + * and acts as a bridge between motion values and removeAxisDelta + */ +export function removeAxisTransforms( + axis: Axis, + transforms: ResolvedValues, + [key, scaleKey, originKey]: string[], + origin?: Axis, + sourceAxis?: Axis +) { + removeAxisDelta( + axis, + transforms[key] as number, + transforms[scaleKey] as number, + transforms[originKey] as number, + transforms.scale as number, + origin, + sourceAxis + ) +} + +/** + * The names of the motion values we want to apply as translation, scale and origin. + */ +const xKeys = ["x", "scaleX", "originX"] +const yKeys = ["y", "scaleY", "originY"] + +/** + * Remove a transforms from an box. This is essentially the steps of applyAxisBox in reverse + * and acts as a bridge between motion values and removeAxisDelta + */ +export function removeBoxTransforms( + box: Box, + transforms: ResolvedValues, + originBox?: Box, + sourceBox?: Box +): void { + removeAxisTransforms( + box.x, + transforms, + xKeys, + originBox ? originBox.x : undefined, + sourceBox ? sourceBox.x : undefined + ) + removeAxisTransforms( + box.y, + transforms, + yKeys, + originBox ? originBox.y : undefined, + sourceBox ? sourceBox.y : undefined + ) +} diff --git a/packages/motion-dom/src/projection/geometry/index.ts b/packages/motion-dom/src/projection/geometry/index.ts new file mode 100644 index 0000000000..d0707d77ae --- /dev/null +++ b/packages/motion-dom/src/projection/geometry/index.ts @@ -0,0 +1,7 @@ +export * from "./models" +export * from "./delta-calc" +export * from "./delta-apply" +export * from "./delta-remove" +export * from "./copy" +export * from "./conversion" +export * from "./utils" diff --git a/packages/motion-dom/src/projection/geometry/models.ts b/packages/motion-dom/src/projection/geometry/models.ts new file mode 100644 index 0000000000..94366060da --- /dev/null +++ b/packages/motion-dom/src/projection/geometry/models.ts @@ -0,0 +1,20 @@ +import { Axis, AxisDelta, Box, Delta } from "motion-utils" + +export const createAxisDelta = (): AxisDelta => ({ + translate: 0, + scale: 1, + origin: 0, + originPoint: 0, +}) + +export const createDelta = (): Delta => ({ + x: createAxisDelta(), + y: createAxisDelta(), +}) + +export const createAxis = (): Axis => ({ min: 0, max: 0 }) + +export const createBox = (): Box => ({ + x: createAxis(), + y: createAxis(), +}) diff --git a/packages/motion-dom/src/projection/geometry/utils.ts b/packages/motion-dom/src/projection/geometry/utils.ts new file mode 100644 index 0000000000..ede3558d31 --- /dev/null +++ b/packages/motion-dom/src/projection/geometry/utils.ts @@ -0,0 +1,41 @@ +import { Axis, AxisDelta, Box, Delta } from "motion-utils" +import { calcLength } from "./delta-calc" + +function isAxisDeltaZero(delta: AxisDelta) { + return delta.translate === 0 && delta.scale === 1 +} + +export function isDeltaZero(delta: Delta) { + return isAxisDeltaZero(delta.x) && isAxisDeltaZero(delta.y) +} + +export function axisEquals(a: Axis, b: Axis) { + return a.min === b.min && a.max === b.max +} + +export function boxEquals(a: Box, b: Box) { + return axisEquals(a.x, b.x) && axisEquals(a.y, b.y) +} + +export function axisEqualsRounded(a: Axis, b: Axis) { + return ( + Math.round(a.min) === Math.round(b.min) && + Math.round(a.max) === Math.round(b.max) + ) +} + +export function boxEqualsRounded(a: Box, b: Box) { + return axisEqualsRounded(a.x, b.x) && axisEqualsRounded(a.y, b.y) +} + +export function aspectRatio(box: Box): number { + return calcLength(box.x) / calcLength(box.y) +} + +export function axisDeltaEquals(a: AxisDelta, b: AxisDelta) { + return ( + a.translate === b.translate && + a.scale === b.scale && + a.originPoint === b.originPoint + ) +} diff --git a/packages/motion-dom/src/projection/styles/index.ts b/packages/motion-dom/src/projection/styles/index.ts new file mode 100644 index 0000000000..de88d54414 --- /dev/null +++ b/packages/motion-dom/src/projection/styles/index.ts @@ -0,0 +1,5 @@ +export * from "./types" +export * from "./scale-border-radius" +export * from "./scale-box-shadow" +export * from "./scale-correction" +export * from "./transform" diff --git a/packages/motion-dom/src/projection/styles/scale-border-radius.ts b/packages/motion-dom/src/projection/styles/scale-border-radius.ts new file mode 100644 index 0000000000..21fd541a1c --- /dev/null +++ b/packages/motion-dom/src/projection/styles/scale-border-radius.ts @@ -0,0 +1,42 @@ +import { px } from "../../value/types/numbers/units" +import type { Axis } from "motion-utils" +import type { ScaleCorrectorDefinition } from "./types" + +export function pixelsToPercent(pixels: number, axis: Axis): number { + if (axis.max === axis.min) return 0 + 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 const correctBorderRadius: ScaleCorrectorDefinition = { + correct: (latest, node) => { + if (!node.target) return latest + + /** + * 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, node.target.x) + const y = pixelsToPercent(latest, node.target.y) + + return `${x}% ${y}%` + }, +} diff --git a/packages/motion-dom/src/projection/styles/scale-box-shadow.ts b/packages/motion-dom/src/projection/styles/scale-box-shadow.ts new file mode 100644 index 0000000000..c5f594e1d6 --- /dev/null +++ b/packages/motion-dom/src/projection/styles/scale-box-shadow.ts @@ -0,0 +1,42 @@ +import { complex } from "../../value/types/complex" +import { mixNumber } from "../../utils/mix/number" +import type { ScaleCorrectorDefinition } from "./types" + +export const correctBoxShadow: ScaleCorrectorDefinition = { + correct: (latest: string, { treeScale, projectionDelta }) => { + const original = latest + 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 = projectionDelta!.x.scale * treeScale!.x + const yScale = projectionDelta!.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 = mixNumber(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 + + return template(shadow) + }, +} diff --git a/packages/motion-dom/src/projection/styles/scale-correction.ts b/packages/motion-dom/src/projection/styles/scale-correction.ts new file mode 100644 index 0000000000..030445024b --- /dev/null +++ b/packages/motion-dom/src/projection/styles/scale-correction.ts @@ -0,0 +1,30 @@ +import { isCSSVariableName } from "../../animation/utils/is-css-variable" +import { correctBorderRadius } from "./scale-border-radius" +import { correctBoxShadow } from "./scale-box-shadow" +import type { ScaleCorrectorMap } from "./types" + +export const scaleCorrectors: ScaleCorrectorMap = { + borderRadius: { + ...correctBorderRadius, + applyTo: [ + "borderTopLeftRadius", + "borderTopRightRadius", + "borderBottomLeftRadius", + "borderBottomRightRadius", + ], + }, + borderTopLeftRadius: correctBorderRadius, + borderTopRightRadius: correctBorderRadius, + borderBottomLeftRadius: correctBorderRadius, + borderBottomRightRadius: correctBorderRadius, + boxShadow: correctBoxShadow, +} + +export function addScaleCorrector(correctors: ScaleCorrectorMap) { + for (const key in correctors) { + scaleCorrectors[key] = correctors[key] + if (isCSSVariableName(key)) { + scaleCorrectors[key].isCSSVariable = true + } + } +} diff --git a/packages/motion-dom/src/projection/styles/transform.ts b/packages/motion-dom/src/projection/styles/transform.ts new file mode 100644 index 0000000000..dde9c04d3e --- /dev/null +++ b/packages/motion-dom/src/projection/styles/transform.ts @@ -0,0 +1,55 @@ +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, rotateX, rotateY, skewX, skewY } = + latestTransform + if (transformPerspective) + transform = `perspective(${transformPerspective}px) ${transform}` + if (rotate) transform += `rotate(${rotate}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" +} diff --git a/packages/motion-dom/src/projection/styles/types.ts b/packages/motion-dom/src/projection/styles/types.ts new file mode 100644 index 0000000000..d1d89ec0ac --- /dev/null +++ b/packages/motion-dom/src/projection/styles/types.ts @@ -0,0 +1,27 @@ +import type { AnyResolvedKeyframe } from "../../animation/types" +import type { Box, Delta, Point } from "motion-utils" + +/** + * Minimal interface for projection node properties used by scale correctors. + * This avoids circular dependencies with the full IProjectionNode interface. + */ +export interface ScaleCorrectionNode { + target?: Box + treeScale?: Point + projectionDelta?: Delta +} + +export type ScaleCorrector = ( + latest: AnyResolvedKeyframe, + node: ScaleCorrectionNode +) => AnyResolvedKeyframe + +export interface ScaleCorrectorDefinition { + correct: ScaleCorrector + applyTo?: string[] + isCSSVariable?: boolean +} + +export interface ScaleCorrectorMap { + [key: string]: ScaleCorrectorDefinition +} diff --git a/packages/motion-dom/src/projection/utils/has-transform.ts b/packages/motion-dom/src/projection/utils/has-transform.ts new file mode 100644 index 0000000000..e76432bd3e --- /dev/null +++ b/packages/motion-dom/src/projection/utils/has-transform.ts @@ -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%" +} diff --git a/packages/motion-dom/src/projection/utils/measure.ts b/packages/motion-dom/src/projection/utils/measure.ts new file mode 100644 index 0000000000..4503738595 --- /dev/null +++ b/packages/motion-dom/src/projection/utils/measure.ts @@ -0,0 +1,31 @@ +import { TransformPoint } from "motion-utils" +import { + convertBoundingBoxToBox, + transformBoxPoints, +} from "../geometry/conversion" +import { translateAxis } from "../geometry/delta-apply" + +export function measureViewportBox( + instance: HTMLElement, + transformPoint?: TransformPoint +) { + return convertBoundingBoxToBox( + transformBoxPoints(instance.getBoundingClientRect(), transformPoint) + ) +} + +export function measurePageBox( + element: HTMLElement, + rootProjectionNode: any, + transformPagePoint?: TransformPoint +) { + const viewportBox = measureViewportBox(element, transformPagePoint) + const { scroll } = rootProjectionNode + + if (scroll) { + translateAxis(viewportBox.x, scroll.offset.x) + translateAxis(viewportBox.y, scroll.offset.y) + } + + return viewportBox +} diff --git a/packages/motion-dom/src/render/Feature.ts b/packages/motion-dom/src/render/Feature.ts new file mode 100644 index 0000000000..2a36ead46f --- /dev/null +++ b/packages/motion-dom/src/render/Feature.ts @@ -0,0 +1,24 @@ +/** + * Feature base class for extending VisualElement functionality. + * Features are plugins that can be mounted/unmounted to add behavior + * like gestures, animations, or layout tracking. + */ +export abstract class Feature<_T extends any = any> { + isMounted = false + + /** + * A reference to the VisualElement this feature is attached to. + * Typed as any to avoid circular dependencies - will be a VisualElement at runtime. + */ + node: any + + constructor(node: any) { + this.node = node + } + + abstract mount(): void + + abstract unmount(): void + + update(): void {} +} diff --git a/packages/motion-dom/src/render/VisualElement.ts b/packages/motion-dom/src/render/VisualElement.ts new file mode 100644 index 0000000000..2106ec7741 --- /dev/null +++ b/packages/motion-dom/src/render/VisualElement.ts @@ -0,0 +1,886 @@ +import { Box } from "motion-utils" +import { + isNumericalString, + isZeroValueString, + SubscriptionManager, + warnOnce, +} from "motion-utils" +import { cancelFrame, frame } from "../frameloop" +import { microtask } from "../frameloop/microtask" +import { time } from "../frameloop/sync-time" +import { motionValue, MotionValue } from "../value" +import { isMotionValue } from "../value/utils/is-motion-value" +import { KeyframeResolver } from "../animation/keyframes/KeyframesResolver" +import type { AnyResolvedKeyframe } from "../animation/types" +import { transformProps } from "./utils/keys-transform" +import { complex } from "../value/types/complex" +import { findValueType } from "../value/types/utils/find" +import { getAnimatableNone } from "../value/types/utils/animatable-none" +import type { MotionNodeOptions } from "../node/types" +import { createBox } from "../projection/geometry/models" +import { + initPrefersReducedMotion, + hasReducedMotionListener, + prefersReducedMotion, +} from "./utils/reduced-motion" +import { visualElementStore } from "./store" +import { + ResolvedValues, + VisualElementEventCallbacks, + VisualElementOptions, + PresenceContextProps, + ReducedMotionConfig, + FeatureDefinitions, + MotionConfigContextProps, +} from "./types" +import { AnimationState } from "./utils/animation-state" +import { + isControllingVariants as checkIsControllingVariants, + isVariantNode as checkIsVariantNode, +} from "./utils/is-controlling-variants" +import { updateMotionValuesFromProps } from "./utils/motion-values" +import { resolveVariantFromProps } from "./utils/resolve-variants" +import { Feature } from "./Feature" + +const propEventHandlers = [ + "AnimationStart", + "AnimationComplete", + "Update", + "BeforeLayoutMeasure", + "LayoutMeasure", + "LayoutAnimationStart", + "LayoutAnimationComplete", +] as const + +/** + * Static feature definitions - can be injected by framework layer + */ +let featureDefinitions: Partial = {} + +/** + * Set feature definitions for all VisualElements. + * This should be called by the framework layer (e.g., framer-motion) during initialization. + */ +export function setFeatureDefinitions(definitions: Partial) { + featureDefinitions = definitions +} + +/** + * Get the current feature definitions + */ +export function getFeatureDefinitions(): Partial { + return featureDefinitions +} + +/** + * Motion style type - a subset of CSS properties that can contain motion values + */ +export type MotionStyle = { + [K: string]: AnyResolvedKeyframe | MotionValue | undefined +} + +/** + * A VisualElement is an imperative abstraction around UI elements such as + * HTMLElement, SVGElement, Three.Object3D etc. + */ +export abstract class VisualElement< + Instance = unknown, + RenderState = unknown, + Options extends {} = {} +> { + /** + * VisualElements are arranged in trees mirroring that of the React tree. + * Each type of VisualElement has a unique name, to detect when we're crossing + * type boundaries within that tree. + */ + abstract type: string + + /** + * An `Array.sort` compatible function that will compare two Instances and + * compare their respective positions within the tree. + */ + abstract sortInstanceNodePosition(a: Instance, b: Instance): number + + /** + * Measure the viewport-relative bounding box of the Instance. + */ + abstract measureInstanceViewportBox( + instance: Instance, + props: MotionNodeOptions & Partial + ): Box + + /** + * When a value has been removed from all animation props we need to + * pick a target to animate back to. For instance, for HTMLElements + * we can look in the style prop. + */ + abstract getBaseTargetFromProps( + props: MotionNodeOptions, + key: string + ): AnyResolvedKeyframe | undefined | MotionValue + + /** + * When we first animate to a value we need to animate it *from* a value. + * Often this have been specified via the initial prop but it might be + * that the value needs to be read from the Instance. + */ + abstract readValueFromInstance( + instance: Instance, + key: string, + options: Options + ): AnyResolvedKeyframe | null | undefined + + /** + * When a value has been removed from the VisualElement we use this to remove + * it from the inherting class' unique render state. + */ + abstract removeValueFromRenderState( + key: string, + renderState: RenderState + ): void + + /** + * Run before a React or VisualElement render, builds the latest motion + * values into an Instance-specific format. For example, HTMLVisualElement + * will use this step to build `style` and `var` values. + */ + abstract build( + renderState: RenderState, + latestValues: ResolvedValues, + props: MotionNodeOptions + ): void + + /** + * Apply the built values to the Instance. For example, HTMLElements will have + * styles applied via `setProperty` and the style attribute, whereas SVGElements + * will have values applied to attributes. + */ + abstract renderInstance( + instance: Instance, + renderState: RenderState, + styleProp?: MotionStyle, + projection?: any + ): void + + /** + * This method is called when a transform property is bound to a motion value. + * It's currently used to measure SVG elements when a new transform property is bound. + */ + onBindTransform?(): void + + /** + * If the component child is provided as a motion value, handle subscriptions + * with the renderer-specific VisualElement. + */ + handleChildMotionValue?(): void + + /** + * This method takes React props and returns found MotionValues. For example, HTML + * MotionValues will be found within the style prop, whereas for Three.js within attribute arrays. + * + * This isn't an abstract method as it needs calling in the constructor, but it is + * intended to be one. + */ + scrapeMotionValuesFromProps( + _props: MotionNodeOptions, + _prevProps: MotionNodeOptions, + _visualElement: VisualElement + ): { + [key: string]: MotionValue | AnyResolvedKeyframe + } { + return {} + } + + /** + * A reference to the current underlying Instance, e.g. a HTMLElement + * or Three.Mesh etc. + */ + current: Instance | null = null + + /** + * A reference to the parent VisualElement (if exists). + */ + parent: VisualElement | undefined + + /** + * A set containing references to this VisualElement's children. + */ + children = new Set() + + /** + * A set containing the latest children of this VisualElement. This is flushed + * at the start of every commit. We use it to calculate the stagger delay + * for newly-added children. + */ + enteringChildren?: Set + + /** + * The depth of this VisualElement within the overall VisualElement tree. + */ + depth: number + + /** + * The current render state of this VisualElement. Defined by inherting VisualElements. + */ + renderState: RenderState + + /** + * An object containing the latest static values for each of this VisualElement's + * MotionValues. + */ + latestValues: ResolvedValues + + /** + * Determine what role this visual element should take in the variant tree. + */ + isVariantNode: boolean = false + isControllingVariants: boolean = false + + /** + * If this component is part of the variant tree, it should track + * any children that are also part of the tree. This is essentially + * a shadow tree to simplify logic around how to stagger over children. + */ + variantChildren?: Set + + /** + * Decides whether this VisualElement should animate in reduced motion + * mode. + * + * TODO: This is currently set on every individual VisualElement but feels + * like it could be set globally. + */ + shouldReduceMotion: boolean | null = null + + /** + * Normally, if a component is controlled by a parent's variants, it can + * rely on that ancestor to trigger animations further down the tree. + * However, if a component is created after its parent is mounted, the parent + * won't trigger that mount animation so the child needs to. + * + * TODO: This might be better replaced with a method isParentMounted + */ + manuallyAnimateOnMount: boolean + + /** + * This can be set by AnimatePresence to force components that mount + * at the same time as it to mount as if they have initial={false} set. + */ + blockInitialAnimation: boolean + + /** + * A reference to this VisualElement's projection node, used in layout animations. + */ + projection?: any + + /** + * A map of all motion values attached to this visual element. Motion + * values are source of truth for any given animated value. A motion + * value might be provided externally by the component via props. + */ + values = new Map() + + /** + * The AnimationState, this is hydrated by the animation Feature. + */ + animationState?: AnimationState + + KeyframeResolver = KeyframeResolver + + /** + * The options used to create this VisualElement. The Options type is defined + * by the inheriting VisualElement and is passed straight through to the render functions. + */ + readonly options: Options + + /** + * A reference to the latest props provided to the VisualElement's host React component. + */ + props: MotionNodeOptions + prevProps?: MotionNodeOptions + + presenceContext: PresenceContextProps | null + prevPresenceContext?: PresenceContextProps | null + + /** + * Cleanup functions for active features (hover/tap/exit etc) + */ + private features: { + [K in keyof FeatureDefinitions]?: Feature + } = {} + + /** + * A map of every subscription that binds the provided or generated + * motion values onChange listeners to this visual element. + */ + private valueSubscriptions = new Map() + + /** + * A reference to the ReducedMotionConfig passed to the VisualElement's host React component. + */ + private reducedMotionConfig: ReducedMotionConfig | undefined + + /** + * On mount, this will be hydrated with a callback to disconnect + * this visual element from its parent on unmount. + */ + private removeFromVariantTree: undefined | VoidFunction + + /** + * A reference to the previously-provided motion values as returned + * from scrapeMotionValuesFromProps. We use the keys in here to determine + * if any motion values need to be removed after props are updated. + */ + private prevMotionValues: MotionStyle = {} + + /** + * When values are removed from all animation props we need to search + * for a fallback value to animate to. These values are tracked in baseTarget. + */ + private baseTarget: ResolvedValues + + /** + * Create an object of the values we initially animated from (if initial prop present). + */ + private initialValues: ResolvedValues + + /** + * An object containing a SubscriptionManager for each active event. + */ + private events: { + [key: string]: SubscriptionManager + } = {} + + /** + * An object containing an unsubscribe function for each prop event subscription. + * For example, every "Update" event can have multiple subscribers via + * VisualElement.on(), but only one of those can be defined via the onUpdate prop. + */ + private propEventSubscriptions: { + [key: string]: VoidFunction + } = {} + + constructor( + { + parent, + props, + presenceContext, + reducedMotionConfig, + blockInitialAnimation, + visualState, + }: VisualElementOptions, + options: Options = {} as any + ) { + const { latestValues, renderState } = visualState + this.latestValues = latestValues + this.baseTarget = { ...latestValues } + this.initialValues = props.initial ? { ...latestValues } : {} + this.renderState = renderState + this.parent = parent + this.props = props + this.presenceContext = presenceContext + this.depth = parent ? parent.depth + 1 : 0 + this.reducedMotionConfig = reducedMotionConfig + this.options = options + this.blockInitialAnimation = Boolean(blockInitialAnimation) + + this.isControllingVariants = checkIsControllingVariants(props) + this.isVariantNode = checkIsVariantNode(props) + if (this.isVariantNode) { + this.variantChildren = new Set() + } + + this.manuallyAnimateOnMount = Boolean(parent && parent.current) + + /** + * Any motion values that are provided to the element when created + * aren't yet bound to the element, as this would technically be impure. + * However, we iterate through the motion values and set them to the + * initial values for this component. + * + * TODO: This is impure and we should look at changing this to run on mount. + * Doing so will break some tests but this isn't necessarily a breaking change, + * more a reflection of the test. + */ + const { willChange, ...initialMotionValues } = + this.scrapeMotionValuesFromProps(props, {}, this) + + for (const key in initialMotionValues) { + const value = initialMotionValues[key] + + if (latestValues[key] !== undefined && isMotionValue(value)) { + value.set(latestValues[key]) + } + } + } + + mount(instance: Instance) { + this.current = instance + + visualElementStore.set(instance, this) + + if (this.projection && !this.projection.instance) { + this.projection.mount(instance) + } + + if (this.parent && this.isVariantNode && !this.isControllingVariants) { + this.removeFromVariantTree = this.parent.addVariantChild(this) + } + + this.values.forEach((value, key) => this.bindToMotionValue(key, value)) + + /** + * Determine reduced motion preference. Only initialize the matchMedia + * listener if we actually need the dynamic value (i.e., when config + * is neither "never" nor "always"). + */ + if (this.reducedMotionConfig === "never") { + this.shouldReduceMotion = false + } else if (this.reducedMotionConfig === "always") { + this.shouldReduceMotion = true + } else { + if (!hasReducedMotionListener.current) { + initPrefersReducedMotion() + } + this.shouldReduceMotion = prefersReducedMotion.current + } + + if (process.env.NODE_ENV !== "production") { + warnOnce( + this.shouldReduceMotion !== true, + "You have Reduced Motion enabled on your device. Animations may not appear as expected.", + "reduced-motion-disabled" + ) + } + + this.parent?.addChild(this) + + this.update(this.props, this.presenceContext) + } + + unmount() { + this.projection && this.projection.unmount() + cancelFrame(this.notifyUpdate) + cancelFrame(this.render) + this.valueSubscriptions.forEach((remove) => remove()) + this.valueSubscriptions.clear() + this.removeFromVariantTree && this.removeFromVariantTree() + this.parent?.removeChild(this) + + for (const key in this.events) { + this.events[key].clear() + } + + for (const key in this.features) { + const feature = this.features[key as keyof typeof this.features] + if (feature) { + feature.unmount() + feature.isMounted = false + } + } + this.current = null + } + + addChild(child: VisualElement) { + this.children.add(child) + this.enteringChildren ??= new Set() + this.enteringChildren.add(child) + } + + removeChild(child: VisualElement) { + this.children.delete(child) + this.enteringChildren && this.enteringChildren.delete(child) + } + + private bindToMotionValue(key: string, value: MotionValue) { + if (this.valueSubscriptions.has(key)) { + this.valueSubscriptions.get(key)!() + } + + const valueIsTransform = transformProps.has(key) + + if (valueIsTransform && this.onBindTransform) { + this.onBindTransform() + } + + const removeOnChange = value.on( + "change", + (latestValue: AnyResolvedKeyframe) => { + this.latestValues[key] = latestValue + + this.props.onUpdate && frame.preRender(this.notifyUpdate) + + if (valueIsTransform && this.projection) { + this.projection.isTransformDirty = true + } + + this.scheduleRender() + } + ) + + let removeSyncCheck: VoidFunction | void + if (typeof window !== "undefined" && (window as any).MotionCheckAppearSync) { + removeSyncCheck = (window as any).MotionCheckAppearSync(this, key, value) + } + + this.valueSubscriptions.set(key, () => { + removeOnChange() + if (removeSyncCheck) removeSyncCheck() + if (value.owner) value.stop() + }) + } + + sortNodePosition(other: VisualElement) { + /** + * If these nodes aren't even of the same type we can't compare their depth. + */ + if ( + !this.current || + !this.sortInstanceNodePosition || + this.type !== other.type + ) { + return 0 + } + + return this.sortInstanceNodePosition( + this.current as Instance, + other.current as Instance + ) + } + + updateFeatures() { + let key: keyof typeof featureDefinitions = "animation" + + for (key in featureDefinitions) { + const featureDefinition = featureDefinitions[key] + + if (!featureDefinition) continue + + const { isEnabled, Feature: FeatureConstructor } = featureDefinition + + /** + * If this feature is enabled but not active, make a new instance. + */ + if ( + !this.features[key] && + FeatureConstructor && + isEnabled(this.props) + ) { + this.features[key] = new FeatureConstructor(this) as any + } + + /** + * If we have a feature, mount or update it. + */ + if (this.features[key]) { + const feature = this.features[key]! + if (feature.isMounted) { + feature.update() + } else { + feature.mount() + feature.isMounted = true + } + } + } + } + + notifyUpdate = () => this.notify("Update", this.latestValues) + + triggerBuild() { + this.build(this.renderState, this.latestValues, this.props) + } + + render = () => { + if (!this.current) return + this.triggerBuild() + this.renderInstance( + this.current, + this.renderState, + (this.props as any).style, + this.projection + ) + } + + private renderScheduledAt = 0.0 + scheduleRender = () => { + const now = time.now() + if (this.renderScheduledAt < now) { + this.renderScheduledAt = now + frame.render(this.render, false, true) + } + } + + /** + * Measure the current viewport box with or without transforms. + * Only measures axis-aligned boxes, rotate and skew must be manually + * removed with a re-render to work. + */ + measureViewportBox() { + return this.current + ? this.measureInstanceViewportBox(this.current, this.props) + : createBox() + } + + getStaticValue(key: string) { + return this.latestValues[key] + } + + setStaticValue(key: string, value: AnyResolvedKeyframe) { + this.latestValues[key] = value + } + + /** + * Update the provided props. Ensure any newly-added motion values are + * added to our map, old ones removed, and listeners updated. + */ + update(props: MotionNodeOptions, presenceContext: PresenceContextProps | null) { + if (props.transformTemplate || this.props.transformTemplate) { + this.scheduleRender() + } + + this.prevProps = this.props + this.props = props + + this.prevPresenceContext = this.presenceContext + this.presenceContext = presenceContext + + /** + * Update prop event handlers ie onAnimationStart, onAnimationComplete + */ + for (let i = 0; i < propEventHandlers.length; i++) { + const key = propEventHandlers[i] + if (this.propEventSubscriptions[key]) { + this.propEventSubscriptions[key]() + delete this.propEventSubscriptions[key] + } + + const listenerName = ("on" + key) as keyof typeof props + const listener = props[listenerName] + if (listener) { + this.propEventSubscriptions[key] = this.on(key as any, listener) + } + } + + this.prevMotionValues = updateMotionValuesFromProps( + this, + this.scrapeMotionValuesFromProps(props, this.prevProps || {}, this), + this.prevMotionValues + ) + + if (this.handleChildMotionValue) { + this.handleChildMotionValue() + } + } + + getProps() { + return this.props + } + + /** + * Returns the variant definition with a given name. + */ + getVariant(name: string) { + return this.props.variants ? this.props.variants[name] : undefined + } + + /** + * Returns the defined default transition on this component. + */ + getDefaultTransition() { + return this.props.transition + } + + getTransformPagePoint() { + return (this.props as any).transformPagePoint + } + + getClosestVariantNode(): VisualElement | undefined { + return this.isVariantNode + ? this + : this.parent + ? this.parent.getClosestVariantNode() + : undefined + } + + /** + * Add a child visual element to our set of children. + */ + addVariantChild(child: VisualElement) { + const closestVariantNode = this.getClosestVariantNode() + if (closestVariantNode) { + closestVariantNode.variantChildren && + closestVariantNode.variantChildren.add(child) + return () => closestVariantNode.variantChildren!.delete(child) + } + } + + /** + * Add a motion value and bind it to this visual element. + */ + addValue(key: string, value: MotionValue) { + // Remove existing value if it exists + const existingValue = this.values.get(key) + + if (value !== existingValue) { + if (existingValue) this.removeValue(key) + this.bindToMotionValue(key, value) + this.values.set(key, value) + this.latestValues[key] = value.get() + } + } + + /** + * Remove a motion value and unbind any active subscriptions. + */ + removeValue(key: string) { + this.values.delete(key) + const unsubscribe = this.valueSubscriptions.get(key) + if (unsubscribe) { + unsubscribe() + this.valueSubscriptions.delete(key) + } + delete this.latestValues[key] + this.removeValueFromRenderState(key, this.renderState) + } + + /** + * Check whether we have a motion value for this key + */ + hasValue(key: string) { + return this.values.has(key) + } + + /** + * Get a motion value for this key. If called with a default + * value, we'll create one if none exists. + */ + getValue(key: string): MotionValue | undefined + getValue(key: string, defaultValue: AnyResolvedKeyframe | null): MotionValue + getValue( + key: string, + defaultValue?: AnyResolvedKeyframe | null + ): MotionValue | undefined { + if (this.props.values && this.props.values[key]) { + return this.props.values[key] + } + + let value = this.values.get(key) + + if (value === undefined && defaultValue !== undefined) { + value = motionValue( + defaultValue === null ? undefined : defaultValue, + { owner: this } + ) + this.addValue(key, value) + } + + return value + } + + /** + * If we're trying to animate to a previously unencountered value, + * we need to check for it in our state and as a last resort read it + * directly from the instance (which might have performance implications). + */ + readValue(key: string, target?: AnyResolvedKeyframe | null) { + let value = + this.latestValues[key] !== undefined || !this.current + ? this.latestValues[key] + : this.getBaseTargetFromProps(this.props, key) ?? + this.readValueFromInstance(this.current, key, this.options) + + if (value !== undefined && value !== null) { + if ( + typeof value === "string" && + (isNumericalString(value) || isZeroValueString(value)) + ) { + // If this is a number read as a string, ie "0" or "200", convert it to a number + value = parseFloat(value) + } else if (!findValueType(value) && complex.test(target)) { + value = getAnimatableNone(key, target as string) + } + + this.setBaseTarget(key, isMotionValue(value) ? value.get() : value) + } + + return isMotionValue(value) ? value.get() : value + } + + /** + * Set the base target to later animate back to. This is currently + * only hydrated on creation and when we first read a value. + */ + setBaseTarget(key: string, value: AnyResolvedKeyframe) { + this.baseTarget[key] = value + } + + /** + * Find the base target for a value thats been removed from all animation + * props. + */ + getBaseTarget(key: string): ResolvedValues[string] | undefined | null { + const { initial } = this.props + + let valueFromInitial: ResolvedValues[string] | undefined | null + + if (typeof initial === "string" || typeof initial === "object") { + const variant = resolveVariantFromProps( + this.props, + initial as any, + this.presenceContext?.custom + ) + if (variant) { + valueFromInitial = variant[ + key as keyof typeof variant + ] as string + } + } + + /** + * If this value still exists in the current initial variant, read that. + */ + if (initial && valueFromInitial !== undefined) { + return valueFromInitial + } + + /** + * Alternatively, if this VisualElement config has defined a getBaseTarget + * so we can read the value from an alternative source, try that. + */ + const target = this.getBaseTargetFromProps(this.props, key) + if (target !== undefined && !isMotionValue(target)) return target + + /** + * If the value was initially defined on initial, but it doesn't any more, + * return undefined. Otherwise return the value as initially read from the DOM. + */ + return this.initialValues[key] !== undefined && + valueFromInitial === undefined + ? undefined + : this.baseTarget[key] + } + + on( + eventName: EventName, + callback: VisualElementEventCallbacks[EventName] + ) { + if (!this.events[eventName]) { + this.events[eventName] = new SubscriptionManager() + } + + return this.events[eventName].add(callback) + } + + notify( + eventName: EventName, + ...args: any + ) { + if (this.events[eventName]) { + this.events[eventName].notify(...args) + } + } + + scheduleRenderMicrotask() { + microtask.render(this.render) + } +} diff --git a/packages/motion-dom/src/render/dom/DOMVisualElement.ts b/packages/motion-dom/src/render/dom/DOMVisualElement.ts new file mode 100644 index 0000000000..6dbbfd675d --- /dev/null +++ b/packages/motion-dom/src/render/dom/DOMVisualElement.ts @@ -0,0 +1,58 @@ +import { isMotionValue } from "../../value/utils/is-motion-value" +import type { MotionValue } from "../../value" +import type { AnyResolvedKeyframe } from "../../animation/types" +import { DOMKeyframesResolver } from "../../animation/keyframes/DOMKeyframesResolver" +import type { MotionNodeOptions } from "../../node/types" +import type { DOMVisualElementOptions } from "./types" +import type { HTMLRenderState } from "../html/types" +import { VisualElement, MotionStyle } from "../VisualElement" + +export abstract class DOMVisualElement< + Instance extends HTMLElement | SVGElement = HTMLElement, + State extends HTMLRenderState = HTMLRenderState, + Options extends DOMVisualElementOptions = DOMVisualElementOptions +> extends VisualElement { + sortInstanceNodePosition(a: Instance, b: Instance): number { + /** + * compareDocumentPosition returns a bitmask, by using the bitwise & + * we're returning true if 2 in that bitmask is set to true. 2 is set + * to true if b preceeds a. + */ + return a.compareDocumentPosition(b) & 2 ? 1 : -1 + } + + getBaseTargetFromProps( + props: MotionNodeOptions, + key: string + ): AnyResolvedKeyframe | MotionValue | undefined { + const style = (props as MotionNodeOptions & { style?: MotionStyle }).style + return style ? (style[key] as string) : undefined + } + + removeValueFromRenderState( + key: string, + { vars, style }: HTMLRenderState + ): void { + delete vars[key] + delete style[key] + } + + KeyframeResolver = DOMKeyframesResolver + + childSubscription?: VoidFunction + handleChildMotionValue() { + if (this.childSubscription) { + this.childSubscription() + delete this.childSubscription + } + + const { children } = this.props as MotionNodeOptions & { children?: MotionValue | any } + if (isMotionValue(children)) { + this.childSubscription = children.on("change", (latest) => { + if (this.current) { + this.current.textContent = `${latest}` + } + }) + } + } +} diff --git a/packages/motion-dom/src/render/dom/types.ts b/packages/motion-dom/src/render/dom/types.ts new file mode 100644 index 0000000000..f24fdeac82 --- /dev/null +++ b/packages/motion-dom/src/render/dom/types.ts @@ -0,0 +1,18 @@ +export interface DOMVisualElementOptions { + /** + * If `true`, this element will be included in the projection tree. + * + * Default: `true` + * + * @public + */ + allowProjection?: boolean + + /** + * Allow this element to be GPU-accelerated. We currently enable this by + * adding a `translateZ(0)`. + * + * @public + */ + enableHardwareAcceleration?: boolean +} diff --git a/packages/motion-dom/src/render/html/HTMLVisualElement.ts b/packages/motion-dom/src/render/html/HTMLVisualElement.ts new file mode 100644 index 0000000000..f6e38ca293 --- /dev/null +++ b/packages/motion-dom/src/render/html/HTMLVisualElement.ts @@ -0,0 +1,74 @@ +import type { Box } from "motion-utils" +import type { AnyResolvedKeyframe } from "../../animation/types" +import { isCSSVariableName } from "../../animation/utils/is-css-variable" +import type { MotionNodeOptions } from "../../node/types" +import { transformProps } from "../utils/keys-transform" +import { + defaultTransformValue, + readTransformValue, +} from "../dom/parse-transform" +import { measureViewportBox } from "../../projection/utils/measure" +import { DOMVisualElement } from "../dom/DOMVisualElement" +import type { DOMVisualElementOptions } from "../dom/types" +import type { ResolvedValues, MotionConfigContextProps } from "../types" +import type { VisualElement } from "../VisualElement" +import { HTMLRenderState } from "./types" +import { buildHTMLStyles } from "./utils/build-styles" +import { renderHTML } from "./utils/render" +import { scrapeMotionValuesFromProps } from "./utils/scrape-motion-values" + +export function getComputedStyle(element: HTMLElement) { + return window.getComputedStyle(element) +} + +export class HTMLVisualElement extends DOMVisualElement< + HTMLElement, + HTMLRenderState, + DOMVisualElementOptions +> { + type = "html" + + readValueFromInstance( + instance: HTMLElement, + key: string + ): AnyResolvedKeyframe | null | undefined { + if (transformProps.has(key)) { + return this.projection?.isProjecting + ? defaultTransformValue(key) + : readTransformValue(instance, key) + } else { + const computedStyle = getComputedStyle(instance) + const value = + (isCSSVariableName(key) + ? computedStyle.getPropertyValue(key) + : computedStyle[key as keyof typeof computedStyle]) || 0 + + return typeof value === "string" ? value.trim() : (value as number) + } + } + + measureInstanceViewportBox( + instance: HTMLElement, + { transformPagePoint }: MotionNodeOptions & Partial + ): Box { + return measureViewportBox(instance, transformPagePoint) + } + + build( + renderState: HTMLRenderState, + latestValues: ResolvedValues, + props: MotionNodeOptions + ) { + buildHTMLStyles(renderState, latestValues, props.transformTemplate) + } + + scrapeMotionValuesFromProps( + props: MotionNodeOptions, + prevProps: MotionNodeOptions, + visualElement: VisualElement + ) { + return scrapeMotionValuesFromProps(props, prevProps, visualElement) + } + + renderInstance = renderHTML +} diff --git a/packages/motion-dom/src/render/html/types.ts b/packages/motion-dom/src/render/html/types.ts new file mode 100644 index 0000000000..1021fb8637 --- /dev/null +++ b/packages/motion-dom/src/render/html/types.ts @@ -0,0 +1,33 @@ +import { ResolvedValues } from "../types" + +export interface TransformOrigin { + originX?: number | string + originY?: number | string + originZ?: number | string +} + +export interface HTMLRenderState { + /** + * A mutable record of transforms we want to apply directly to the rendered Element + * every frame. We use a mutable data structure to reduce GC during animations. + */ + transform: ResolvedValues + + /** + * A mutable record of transform origins we want to apply directly to the rendered Element + * every frame. We use a mutable data structure to reduce GC during animations. + */ + transformOrigin: TransformOrigin + + /** + * A mutable record of styles we want to apply directly to the rendered Element + * every frame. We use a mutable data structure to reduce GC during animations. + */ + style: ResolvedValues + + /** + * A mutable record of CSS variables we want to apply directly to the rendered Element + * every frame. We use a mutable data structure to reduce GC during animations. + */ + vars: ResolvedValues +} diff --git a/packages/motion-dom/src/render/html/utils/build-styles.ts b/packages/motion-dom/src/render/html/utils/build-styles.ts new file mode 100644 index 0000000000..5914df7876 --- /dev/null +++ b/packages/motion-dom/src/render/html/utils/build-styles.ts @@ -0,0 +1,80 @@ +import { getValueAsType } from "../../../value/types/utils/get-as-type" +import { numberValueTypes } from "../../../value/types/maps/number" +import { transformProps } from "../../utils/keys-transform" +import { isCSSVariableName } from "../../../animation/utils/is-css-variable" +import { ResolvedValues } from "../../types" +import { HTMLRenderState } from "../types" +import { buildTransform } from "./build-transform" +import type { MotionNodeOptions } from "../../../node/types" + +export function buildHTMLStyles( + state: HTMLRenderState, + latestValues: ResolvedValues, + transformTemplate?: MotionNodeOptions["transformTemplate"] +) { + const { style, vars, transformOrigin } = state + + // Track whether we encounter any transform or transformOrigin values. + let hasTransform = false + let hasTransformOrigin = false + + /** + * Loop over all our latest animated values and decide whether to handle them + * as a style or CSS variable. + * + * Transforms and transform origins are kept separately for further processing. + */ + for (const key in latestValues) { + const value = latestValues[key] + + if (transformProps.has(key)) { + // If this is a transform, flag to enable further transform processing + hasTransform = true + continue + } else if (isCSSVariableName(key)) { + vars[key] = value + continue + } else { + // Convert the value to its default value type, ie 0 -> "0px" + const valueAsType = getValueAsType(value, numberValueTypes[key]) + + if (key.startsWith("origin")) { + // If this is a transform origin, flag and enable further transform-origin processing + hasTransformOrigin = true + transformOrigin[key as keyof typeof transformOrigin] = + valueAsType + } else { + style[key] = valueAsType + } + } + } + + if (!latestValues.transform) { + if (hasTransform || transformTemplate) { + style.transform = buildTransform( + latestValues, + state.transform, + transformTemplate + ) + } else if (style.transform) { + /** + * If we have previously created a transform but currently don't have any, + * reset transform style to none. + */ + style.transform = "none" + } + } + + /** + * Build a transformOrigin style. Uses the same defaults as the browser for + * undefined origins. + */ + if (hasTransformOrigin) { + const { + originX = "50%", + originY = "50%", + originZ = 0, + } = transformOrigin + style.transformOrigin = `${originX} ${originY} ${originZ}` + } +} diff --git a/packages/motion-dom/src/render/html/utils/build-transform.ts b/packages/motion-dom/src/render/html/utils/build-transform.ts new file mode 100644 index 0000000000..17e52bdc2c --- /dev/null +++ b/packages/motion-dom/src/render/html/utils/build-transform.ts @@ -0,0 +1,78 @@ +import { getValueAsType } from "../../../value/types/utils/get-as-type" +import { numberValueTypes } from "../../../value/types/maps/number" +import { transformPropOrder } from "../../utils/keys-transform" +import { ResolvedValues } from "../../types" +import { HTMLRenderState } from "../types" +import type { MotionNodeOptions } from "../../../node/types" + +const translateAlias = { + x: "translateX", + y: "translateY", + z: "translateZ", + transformPerspective: "perspective", +} + +const numTransforms = transformPropOrder.length + +/** + * Build a CSS transform style from individual x/y/scale etc properties. + * + * This outputs with a default order of transforms/scales/rotations, this can be customised by + * providing a transformTemplate function. + */ +export function buildTransform( + latestValues: ResolvedValues, + transform: HTMLRenderState["transform"], + transformTemplate?: MotionNodeOptions["transformTemplate"] +) { + // The transform string we're going to build into. + let transformString = "" + let transformIsDefault = true + + /** + * Loop over all possible transforms in order, adding the ones that + * are present to the transform string. + */ + for (let i = 0; i < numTransforms; i++) { + const key = transformPropOrder[i] as keyof typeof translateAlias + const value = latestValues[key] + + if (value === undefined) continue + + let valueIsDefault = true + if (typeof value === "number") { + valueIsDefault = value === (key.startsWith("scale") ? 1 : 0) + } else { + valueIsDefault = parseFloat(value) === 0 + } + + if (!valueIsDefault || transformTemplate) { + const valueAsType = getValueAsType(value, numberValueTypes[key]) + + if (!valueIsDefault) { + transformIsDefault = false + const transformName = translateAlias[key] || key + transformString += `${transformName}(${valueAsType}) ` + } + + if (transformTemplate) { + transform[key] = valueAsType + } + } + } + + transformString = transformString.trim() + + // If we have a custom `transform` template, pass our transform values and + // generated transformString to that before returning + if (transformTemplate) { + transformString = transformTemplate( + transform, + transformIsDefault ? "" : transformString + ) + } else if (transformIsDefault) { + transformString = "none" + } + + return transformString +} diff --git a/packages/motion-dom/src/render/html/utils/render.ts b/packages/motion-dom/src/render/html/utils/render.ts new file mode 100644 index 0000000000..dd20bda831 --- /dev/null +++ b/packages/motion-dom/src/render/html/utils/render.ts @@ -0,0 +1,26 @@ +import type { MotionStyle } from "../../VisualElement" +import { HTMLRenderState } from "../types" + +export function renderHTML( + element: HTMLElement, + { style, vars }: HTMLRenderState, + styleProp?: MotionStyle, + projection?: any +) { + const elementStyle = element.style + + let key: string + for (key in style) { + // CSSStyleDeclaration has [index: number]: string; in the types, so we use that as key type. + elementStyle[key as unknown as number] = style[key] as string + } + + // Write projection styles directly to element style + projection?.applyProjectionStyles(elementStyle, styleProp) + + for (key in vars) { + // Loop over any CSS variables and assign those. + // They can only be assigned using `setProperty`. + elementStyle.setProperty(key, vars[key] as string) + } +} diff --git a/packages/motion-dom/src/render/html/utils/scrape-motion-values.ts b/packages/motion-dom/src/render/html/utils/scrape-motion-values.ts new file mode 100644 index 0000000000..b125347706 --- /dev/null +++ b/packages/motion-dom/src/render/html/utils/scrape-motion-values.ts @@ -0,0 +1,29 @@ +import { isMotionValue } from "../../../value/utils/is-motion-value" +import type { MotionNodeOptions } from "../../../node/types" +import { isForcedMotionValue } from "../../utils/is-forced-motion-value" +import type { VisualElement } from "../../VisualElement" + +export function scrapeMotionValuesFromProps( + props: MotionNodeOptions, + prevProps: MotionNodeOptions, + visualElement?: VisualElement +) { + const style = (props as any).style + const prevStyle = (prevProps as any)?.style + const newValues: { [key: string]: any } = {} + + if (!style) return newValues + + for (const key in style) { + if ( + isMotionValue(style[key]) || + (prevStyle && isMotionValue(prevStyle[key])) || + isForcedMotionValue(key, props) || + visualElement?.getValue(key)?.liveStyle !== undefined + ) { + newValues[key] = style[key] + } + } + + return newValues +} diff --git a/packages/motion-dom/src/render/object/ObjectVisualElement.ts b/packages/motion-dom/src/render/object/ObjectVisualElement.ts new file mode 100644 index 0000000000..b9f545a5e3 --- /dev/null +++ b/packages/motion-dom/src/render/object/ObjectVisualElement.ts @@ -0,0 +1,56 @@ +import { createBox } from "../../projection/geometry/models" +import { ResolvedValues } from "../types" +import { VisualElement } from "../VisualElement" + +interface ObjectRenderState { + output: ResolvedValues +} + +function isObjectKey(key: string, object: Object): key is keyof Object { + return key in object +} + +export class ObjectVisualElement extends VisualElement< + Object, + ObjectRenderState +> { + type = "object" + + readValueFromInstance(instance: Object, key: string) { + if (isObjectKey(key, instance)) { + const value = instance[key] + if (typeof value === "string" || typeof value === "number") { + return value + } + } + + return undefined + } + + getBaseTargetFromProps() { + return undefined + } + + removeValueFromRenderState( + key: string, + renderState: ObjectRenderState + ): void { + delete renderState.output[key] + } + + measureInstanceViewportBox() { + return createBox() + } + + build(renderState: ObjectRenderState, latestValues: ResolvedValues) { + Object.assign(renderState.output, latestValues) + } + + renderInstance(instance: Object, { output }: ObjectRenderState) { + Object.assign(instance, output) + } + + sortInstanceNodePosition() { + return 0 + } +} diff --git a/packages/motion-dom/src/render/store.ts b/packages/motion-dom/src/render/store.ts new file mode 100644 index 0000000000..f94df0e77c --- /dev/null +++ b/packages/motion-dom/src/render/store.ts @@ -0,0 +1,3 @@ +import type { VisualElement } from "./VisualElement" + +export const visualElementStore = new WeakMap() diff --git a/packages/motion-dom/src/render/svg/SVGVisualElement.ts b/packages/motion-dom/src/render/svg/SVGVisualElement.ts new file mode 100644 index 0000000000..0c17249a3d --- /dev/null +++ b/packages/motion-dom/src/render/svg/SVGVisualElement.ts @@ -0,0 +1,81 @@ +import type { AnyResolvedKeyframe } from "../../animation/types" +import type { MotionValue } from "../../value" +import type { MotionNodeOptions } from "../../node/types" +import { transformProps } from "../utils/keys-transform" +import { getDefaultValueType } from "../../value/types/maps/defaults" +import { createBox } from "../../projection/geometry/models" +import { DOMVisualElement } from "../dom/DOMVisualElement" +import type { DOMVisualElementOptions } from "../dom/types" +import { camelToDash } from "../dom/utils/camel-to-dash" +import type { ResolvedValues } from "../types" +import type { VisualElement, MotionStyle } from "../VisualElement" +import { SVGRenderState } from "./types" +import { buildSVGAttrs } from "./utils/build-attrs" +import { camelCaseAttributes } from "./utils/camel-case-attrs" +import { isSVGTag } from "./utils/is-svg-tag" +import { renderSVG } from "./utils/render" +import { scrapeMotionValuesFromProps } from "./utils/scrape-motion-values" + +export class SVGVisualElement extends DOMVisualElement< + SVGElement, + SVGRenderState, + DOMVisualElementOptions +> { + type = "svg" + + isSVGTag = false + + getBaseTargetFromProps( + props: MotionNodeOptions, + key: string + ): AnyResolvedKeyframe | MotionValue | undefined { + return props[key as keyof MotionNodeOptions] + } + + readValueFromInstance(instance: SVGElement, key: string) { + if (transformProps.has(key)) { + const defaultType = getDefaultValueType(key) + return defaultType ? defaultType.default || 0 : 0 + } + key = !camelCaseAttributes.has(key) ? camelToDash(key) : key + return instance.getAttribute(key) + } + + measureInstanceViewportBox = createBox + + scrapeMotionValuesFromProps( + props: MotionNodeOptions, + prevProps: MotionNodeOptions, + visualElement: VisualElement + ) { + return scrapeMotionValuesFromProps(props, prevProps, visualElement) + } + + build( + renderState: SVGRenderState, + latestValues: ResolvedValues, + props: MotionNodeOptions + ) { + buildSVGAttrs( + renderState, + latestValues, + this.isSVGTag, + props.transformTemplate, + (props as any).style + ) + } + + renderInstance( + instance: SVGElement, + renderState: SVGRenderState, + styleProp?: MotionStyle | undefined, + projection?: any + ): void { + renderSVG(instance, renderState, styleProp, projection) + } + + mount(instance: SVGElement) { + this.isSVGTag = isSVGTag(instance.tagName) + super.mount(instance) + } +} diff --git a/packages/motion-dom/src/render/svg/types.ts b/packages/motion-dom/src/render/svg/types.ts index a3065d0864..5cdb90a757 100644 --- a/packages/motion-dom/src/render/svg/types.ts +++ b/packages/motion-dom/src/render/svg/types.ts @@ -1,4 +1,14 @@ import { AnyResolvedKeyframe } from "../../animation/types" +import { ResolvedValues } from "../types" +import { HTMLRenderState } from "../html/types" + +export interface SVGRenderState extends HTMLRenderState { + /** + * A mutable record of attributes we want to apply directly to the rendered Element + * every frame. We use a mutable data structure to reduce GC during animations. + */ + attrs: ResolvedValues +} export interface SVGAttributes { accentHeight?: AnyResolvedKeyframe | undefined diff --git a/packages/motion-dom/src/render/svg/utils/build-attrs.ts b/packages/motion-dom/src/render/svg/utils/build-attrs.ts new file mode 100644 index 0000000000..95e4a48aab --- /dev/null +++ b/packages/motion-dom/src/render/svg/utils/build-attrs.ts @@ -0,0 +1,97 @@ +import type { MotionNodeOptions } from "../../../node/types" +import { buildHTMLStyles } from "../../html/utils/build-styles" +import { ResolvedValues } from "../../types" +import { SVGRenderState } from "../types" +import { buildSVGPath } from "./path" + +/** + * CSS Motion Path properties that should remain as CSS styles on SVG elements. + */ +const cssMotionPathProperties = [ + "offsetDistance", + "offsetPath", + "offsetRotate", + "offsetAnchor", +] + +/** + * Build SVG visual attributes, like cx and style.transform + */ +export function buildSVGAttrs( + state: SVGRenderState, + { + attrX, + attrY, + attrScale, + pathLength, + pathSpacing = 1, + pathOffset = 0, + // This is object creation, which we try to avoid per-frame. + ...latest + }: ResolvedValues, + isSVGTag: boolean, + transformTemplate?: MotionNodeOptions["transformTemplate"], + styleProp?: Record +) { + buildHTMLStyles(state, latest, transformTemplate) + + /** + * For svg tags we just want to make sure viewBox is animatable and treat all the styles + * as normal HTML tags. + */ + if (isSVGTag) { + if (state.style.viewBox) { + state.attrs.viewBox = state.style.viewBox + } + return + } + + state.attrs = state.style + state.style = {} + const { attrs, style } = state + + /** + * However, we apply transforms as CSS transforms. + * So if we detect a transform, transformOrigin we take it from attrs and copy it into style. + */ + if (attrs.transform) { + style.transform = attrs.transform + delete attrs.transform + } + if (style.transform || attrs.transformOrigin) { + style.transformOrigin = attrs.transformOrigin ?? "50% 50%" + delete attrs.transformOrigin + } + + if (style.transform) { + /** + * SVG's element transform-origin uses its own median as a reference. + * Therefore, transformBox becomes a fill-box + */ + style.transformBox = (styleProp?.transformBox as string) ?? "fill-box" + delete attrs.transformBox + } + + for (const key of cssMotionPathProperties) { + if (attrs[key] !== undefined) { + style[key] = attrs[key] + delete attrs[key] + } + } + + // Render attrX/attrY/attrScale as attributes + if (attrX !== undefined) attrs.x = attrX + if (attrY !== undefined) attrs.y = attrY + if (attrScale !== undefined) attrs.scale = attrScale + + // Build SVG path if one has been defined + if (pathLength !== undefined) { + buildSVGPath( + attrs, + pathLength as number, + pathSpacing as number, + pathOffset as number, + false + ) + } +} diff --git a/packages/motion-dom/src/render/svg/utils/camel-case-attrs.ts b/packages/motion-dom/src/render/svg/utils/camel-case-attrs.ts new file mode 100644 index 0000000000..4947bfd2f9 --- /dev/null +++ b/packages/motion-dom/src/render/svg/utils/camel-case-attrs.ts @@ -0,0 +1,28 @@ +/** + * A set of attribute names that are always read/written as camel case. + */ +export const camelCaseAttributes = new Set([ + "baseFrequency", + "diffuseConstant", + "kernelMatrix", + "kernelUnitLength", + "keySplines", + "keyTimes", + "limitingConeAngle", + "markerHeight", + "markerWidth", + "numOctaves", + "targetX", + "targetY", + "surfaceScale", + "specularConstant", + "specularExponent", + "stdDeviation", + "tableValues", + "viewBox", + "gradientTransform", + "pathLength", + "startOffset", + "textLength", + "lengthAdjust", +]) diff --git a/packages/motion-dom/src/render/svg/utils/is-svg-tag.ts b/packages/motion-dom/src/render/svg/utils/is-svg-tag.ts new file mode 100644 index 0000000000..aa99a203b9 --- /dev/null +++ b/packages/motion-dom/src/render/svg/utils/is-svg-tag.ts @@ -0,0 +1,2 @@ +export const isSVGTag = (tag: unknown) => + typeof tag === "string" && tag.toLowerCase() === "svg" diff --git a/packages/motion-dom/src/render/svg/utils/path.ts b/packages/motion-dom/src/render/svg/utils/path.ts new file mode 100644 index 0000000000..aa76dcfcb6 --- /dev/null +++ b/packages/motion-dom/src/render/svg/utils/path.ts @@ -0,0 +1,42 @@ +import { px } from "../../../value/types/numbers/units" +import { ResolvedValues } from "../../types" + +const dashKeys = { + offset: "stroke-dashoffset", + array: "stroke-dasharray", +} + +const camelKeys = { + offset: "strokeDashoffset", + array: "strokeDasharray", +} + +/** + * Build SVG path properties. Uses the path's measured length to convert + * our custom pathLength, pathSpacing and pathOffset into stroke-dashoffset + * and stroke-dasharray attributes. + * + * This function is mutative to reduce per-frame GC. + */ +export function buildSVGPath( + attrs: ResolvedValues, + length: number, + spacing = 1, + offset = 0, + useDashCase: boolean = true +): void { + // Normalise path length by setting SVG attribute pathLength to 1 + attrs.pathLength = 1 + + // We use dash case when setting attributes directly to the DOM node and camel case + // when defining props on a React component. + const keys = useDashCase ? dashKeys : camelKeys + + // Build the dash offset + attrs[keys.offset] = px.transform!(-offset) + + // Build the dash array + const pathLength = px.transform!(length) + const pathSpacing = px.transform!(spacing) + attrs[keys.array] = `${pathLength} ${pathSpacing}` +} diff --git a/packages/motion-dom/src/render/svg/utils/render.ts b/packages/motion-dom/src/render/svg/utils/render.ts new file mode 100644 index 0000000000..5741dc03aa --- /dev/null +++ b/packages/motion-dom/src/render/svg/utils/render.ts @@ -0,0 +1,21 @@ +import type { MotionStyle } from "../../VisualElement" +import { camelToDash } from "../../dom/utils/camel-to-dash" +import { renderHTML } from "../../html/utils/render" +import { SVGRenderState } from "../types" +import { camelCaseAttributes } from "./camel-case-attrs" + +export function renderSVG( + element: SVGElement, + renderState: SVGRenderState, + _styleProp?: MotionStyle, + projection?: any +) { + renderHTML(element as any, renderState, undefined, projection) + + for (const key in renderState.attrs) { + element.setAttribute( + !camelCaseAttributes.has(key) ? camelToDash(key) : key, + renderState.attrs[key] as string + ) + } +} diff --git a/packages/motion-dom/src/render/svg/utils/scrape-motion-values.ts b/packages/motion-dom/src/render/svg/utils/scrape-motion-values.ts new file mode 100644 index 0000000000..c902de0873 --- /dev/null +++ b/packages/motion-dom/src/render/svg/utils/scrape-motion-values.ts @@ -0,0 +1,33 @@ +import { isMotionValue } from "../../../value/utils/is-motion-value" +import type { MotionNodeOptions } from "../../../node/types" +import { transformPropOrder } from "../../utils/keys-transform" +import { scrapeMotionValuesFromProps as scrapeHTMLMotionValuesFromProps } from "../../html/utils/scrape-motion-values" +import type { VisualElement } from "../../VisualElement" + +export function scrapeMotionValuesFromProps( + props: MotionNodeOptions, + prevProps: MotionNodeOptions, + visualElement?: VisualElement +) { + const newValues = scrapeHTMLMotionValuesFromProps( + props, + prevProps, + visualElement + ) + + for (const key in props) { + if ( + isMotionValue(props[key as keyof typeof props]) || + isMotionValue(prevProps[key as keyof typeof prevProps]) + ) { + const targetKey = + transformPropOrder.indexOf(key) !== -1 + ? "attr" + key.charAt(0).toUpperCase() + key.substring(1) + : key + + newValues[targetKey] = props[key as keyof typeof props] + } + } + + return newValues +} diff --git a/packages/motion-dom/src/render/types.ts b/packages/motion-dom/src/render/types.ts new file mode 100644 index 0000000000..1957cfcf86 --- /dev/null +++ b/packages/motion-dom/src/render/types.ts @@ -0,0 +1,158 @@ +import type { + AnimationDefinition, + MotionNodeOptions, + ResolvedValues, + VariantLabels, +} from "../node/types" +import type { AnyResolvedKeyframe, Transition } from "../animation/types" +import type { MotionValue } from "../value" +import type { Axis, Box, TransformPoint } from "motion-utils" + +// Re-export types for convenience +export type { ResolvedValues } + +/** + * @public + */ +export interface PresenceContextProps { + id: string + isPresent: boolean + register: (id: string | number) => () => void + onExitComplete?: (id: string | number) => void + initial?: false | VariantLabels + custom?: any +} + +/** + * @public + */ +export type ReducedMotionConfig = "always" | "never" | "user" + +/** + * @public + */ +export interface MotionConfigContextProps { + /** + * Internal, exported only for usage in Framer + */ + transformPagePoint: TransformPoint + + /** + * Internal. Determines whether this is a static context ie the Framer canvas. If so, + * it'll disable all dynamic functionality. + */ + isStatic: boolean + + /** + * Defines a new default transition for the entire tree. + * + * @public + */ + transition?: Transition + + /** + * If true, will respect the device prefersReducedMotion setting by switching + * transform animations off. + * + * @public + */ + reducedMotion?: ReducedMotionConfig + + /** + * A custom `nonce` attribute used when wanting to enforce a Content Security Policy (CSP). + * For more details see: + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src#unsafe_inline_styles + * + * @public + */ + nonce?: string +} + +export interface VisualState<_Instance, RenderState> { + latestValues: ResolvedValues + renderState: RenderState +} + +export interface VisualElementOptions { + visualState: VisualState + parent?: any // VisualElement - circular reference handled at runtime + variantParent?: any + presenceContext: PresenceContextProps | null + props: MotionNodeOptions + blockInitialAnimation?: boolean + reducedMotionConfig?: ReducedMotionConfig + /** + * Explicit override for SVG detection. When true, uses SVG rendering; + * when false, uses HTML rendering. If undefined, auto-detects. + */ + isSVG?: boolean +} + +export interface VisualElementEventCallbacks { + BeforeLayoutMeasure: () => void + LayoutMeasure: (layout: Box, prevLayout?: Box) => void + LayoutUpdate: (layout: Axis, prevLayout: Axis) => void + Update: (latest: ResolvedValues) => void + AnimationStart: (definition: AnimationDefinition) => void + AnimationComplete: (definition: AnimationDefinition) => void + LayoutAnimationStart: () => void + LayoutAnimationComplete: () => void + SetAxisTarget: () => void + Unmount: () => void +} + +export interface LayoutLifecycles { + onBeforeLayoutMeasure?(box: Box): void + onLayoutMeasure?(box: Box, prevBox: Box): void + /** + * @internal + */ + onLayoutAnimationStart?(): void + /** + * @internal + */ + onLayoutAnimationComplete?(): void +} + +export type ScrapeMotionValuesFromProps = ( + props: MotionNodeOptions, + prevProps: MotionNodeOptions, + visualElement?: any +) => { + [key: string]: MotionValue | AnyResolvedKeyframe +} + +export type UseRenderState = () => RenderState + +/** + * Animation type for variant state management + */ +export type AnimationType = + | "animate" + | "whileHover" + | "whileTap" + | "whileDrag" + | "whileFocus" + | "whileInView" + | "exit" + +export interface FeatureClass { + new (props: Props): any +} + +export interface FeatureDefinition { + isEnabled: (props: MotionNodeOptions) => boolean + Feature?: FeatureClass +} + +export type FeatureDefinitions = { + animation?: FeatureDefinition + exit?: FeatureDefinition + drag?: FeatureDefinition + tap?: FeatureDefinition + focus?: FeatureDefinition + hover?: FeatureDefinition + pan?: FeatureDefinition + inView?: FeatureDefinition + layout?: FeatureDefinition +} diff --git a/packages/motion-dom/src/render/utils/animation-state.ts b/packages/motion-dom/src/render/utils/animation-state.ts new file mode 100644 index 0000000000..08c46003e6 --- /dev/null +++ b/packages/motion-dom/src/render/utils/animation-state.ts @@ -0,0 +1,503 @@ +import type { + AnimationDefinition, + TargetAndTransition, + VariantLabels, +} from "../../node/types" +import type { Transition } from "../../animation/types" +import type { AnimationType } from "../types" +import { calcChildStagger } from "./calc-child-stagger" +import { getVariantContext } from "./get-variant-context" +import { isAnimationControls } from "./is-animation-controls" +import { isKeyframesTarget } from "./is-keyframes-target" +import { isVariantLabel } from "./is-variant-label" +import { resolveVariant } from "./resolve-dynamic-variants" +import { shallowCompare } from "./shallow-compare" +import { variantPriorityOrder } from "./variant-props" + +export interface VisualElementAnimationOptions { + delay?: number + transitionOverride?: Transition + custom?: any + type?: AnimationType +} + +export interface AnimationState { + animateChanges: (type?: AnimationType) => Promise + setActive: ( + type: AnimationType, + isActive: boolean, + options?: VisualElementAnimationOptions + ) => Promise + setAnimateFunction: (fn: any) => void + getState: () => { [key: string]: AnimationTypeState } + reset: () => void +} + +interface DefinitionAndOptions { + animation: AnimationDefinition + options?: VisualElementAnimationOptions +} + +export type AnimationList = string[] | TargetAndTransition[] + +const reversePriorityOrder = [...variantPriorityOrder].reverse() +const numAnimationTypes = variantPriorityOrder.length + +/** + * Type for the animate function that can be injected. + * This allows the animation implementation to be provided by the framework layer. + */ +export type AnimateFunction = (animations: DefinitionAndOptions[]) => Promise + +/** + * Type for the function that creates an animate function for a visual element. + */ +export type MakeAnimateFunction = (visualElement: any) => AnimateFunction + +function defaultAnimateList(_visualElement: any) { + return (_animations: DefinitionAndOptions[]) => Promise.resolve() +} + +export function createAnimationState( + visualElement: any, + makeAnimateFunction: MakeAnimateFunction = defaultAnimateList +): AnimationState { + let animate = makeAnimateFunction(visualElement) + let state = createState() + let isInitialRender = true + + /** + * This function will be used to reduce the animation definitions for + * each active animation type into an object of resolved values for it. + */ + const buildResolvedTypeValues = + (type: AnimationType) => + ( + acc: { [key: string]: any }, + definition: string | TargetAndTransition | undefined + ) => { + const resolved = resolveVariant( + visualElement, + definition, + type === "exit" + ? visualElement.presenceContext?.custom + : undefined + ) + + if (resolved) { + const { transition, transitionEnd, ...target } = resolved + acc = { ...acc, ...target, ...transitionEnd } + } + + return acc + } + + /** + * This just allows us to inject mocked animation functions + * @internal + */ + function setAnimateFunction(makeAnimator: MakeAnimateFunction) { + animate = makeAnimator(visualElement) + } + + /** + * When we receive new props, we need to: + * 1. Create a list of protected keys for each type. This is a directory of + * value keys that are currently being "handled" by types of a higher priority + * so that whenever an animation is played of a given type, these values are + * protected from being animated. + * 2. Determine if an animation type needs animating. + * 3. Determine if any values have been removed from a type and figure out + * what to animate those to. + */ + function animateChanges(changedActiveType?: AnimationType) { + const { props } = visualElement + const context = getVariantContext(visualElement.parent) || {} + + /** + * A list of animations that we'll build into as we iterate through the animation + * types. This will get executed at the end of the function. + */ + const animations: DefinitionAndOptions[] = [] + + /** + * Keep track of which values have been removed. Then, as we hit lower priority + * animation types, we can check if they contain removed values and animate to that. + */ + const removedKeys = new Set() + + /** + * A dictionary of all encountered keys. This is an object to let us build into and + * copy it without iteration. Each time we hit an animation type we set its protected + * keys - the keys its not allowed to animate - to the latest version of this object. + */ + let encounteredKeys: { [key: string]: any } = {} + + /** + * If a variant has been removed at a given index, and this component is controlling + * variant animations, we want to ensure lower-priority variants are forced to animate. + */ + let removedVariantIndex = Infinity + + /** + * Iterate through all animation types in reverse priority order. For each, we want to + * detect which values it's handling and whether or not they've changed (and therefore + * need to be animated). If any values have been removed, we want to detect those in + * lower priority props and flag for animation. + */ + for (let i = 0; i < numAnimationTypes; i++) { + const type = reversePriorityOrder[i] + const typeState = state[type] + const prop = + props[type] !== undefined + ? props[type] + : context[type as keyof typeof context] + const propIsVariant = isVariantLabel(prop) + + /** + * If this type has *just* changed isActive status, set activeDelta + * to that status. Otherwise set to null. + */ + const activeDelta = + type === changedActiveType ? typeState.isActive : null + + if (activeDelta === false) removedVariantIndex = i + + /** + * If this prop is an inherited variant, rather than been set directly on the + * component itself, we want to make sure we allow the parent to trigger animations. + * + * TODO: Can probably change this to a !isControllingVariants check + */ + let isInherited = + prop === context[type as keyof typeof context] && + prop !== props[type] && + propIsVariant + + if ( + isInherited && + isInitialRender && + visualElement.manuallyAnimateOnMount + ) { + isInherited = false + } + + /** + * Set all encountered keys so far as the protected keys for this type. This will + * be any key that has been animated or otherwise handled by active, higher-priortiy types. + */ + typeState.protectedKeys = { ...encounteredKeys } + + // Check if we can skip analysing this prop early + if ( + // If it isn't active and hasn't *just* been set as inactive + (!typeState.isActive && activeDelta === null) || + // If we didn't and don't have any defined prop for this animation type + (!prop && !typeState.prevProp) || + // Or if the prop doesn't define an animation + isAnimationControls(prop) || + typeof prop === "boolean" + ) { + continue + } + + /** + * As we go look through the values defined on this type, if we detect + * a changed value or a value that was removed in a higher priority, we set + * this to true and add this prop to the animation list. + */ + const variantDidChange = checkVariantsDidChange( + typeState.prevProp, + prop + ) + + let shouldAnimateType = + variantDidChange || + // If we're making this variant active, we want to always make it active + (type === changedActiveType && + typeState.isActive && + !isInherited && + propIsVariant) || + // If we removed a higher-priority variant (i is in reverse order) + (i > removedVariantIndex && propIsVariant) + + let handledRemovedValues = false + + /** + * As animations can be set as variant lists, variants or target objects, we + * coerce everything to an array if it isn't one already + */ + const definitionList = Array.isArray(prop) ? prop : [prop] + + /** + * Build an object of all the resolved values. We'll use this in the subsequent + * animateChanges calls to determine whether a value has changed. + */ + let resolvedValues = definitionList.reduce( + buildResolvedTypeValues(type), + {} + ) + + if (activeDelta === false) resolvedValues = {} + + /** + * Now we need to loop through all the keys in the prev prop and this prop, + * and decide: + * 1. If the value has changed, and needs animating + * 2. If it has been removed, and needs adding to the removedKeys set + * 3. If it has been removed in a higher priority type and needs animating + * 4. If it hasn't been removed in a higher priority but hasn't changed, and + * needs adding to the type's protectedKeys list. + */ + const { prevResolvedValues = {} } = typeState + + const allKeys = { + ...prevResolvedValues, + ...resolvedValues, + } + const markToAnimate = (key: string) => { + shouldAnimateType = true + if (removedKeys.has(key)) { + handledRemovedValues = true + removedKeys.delete(key) + } + typeState.needsAnimating[key] = true + + const motionValue = visualElement.getValue(key) + if (motionValue) motionValue.liveStyle = false + } + + for (const key in allKeys) { + const next = resolvedValues[key] + const prev = prevResolvedValues[key] + + // If we've already handled this we can just skip ahead + if (encounteredKeys.hasOwnProperty(key)) continue + + /** + * If the value has changed, we probably want to animate it. + */ + let valueHasChanged = false + if (isKeyframesTarget(next) && isKeyframesTarget(prev)) { + valueHasChanged = !shallowCompare(next, prev) + } else { + valueHasChanged = next !== prev + } + + if (valueHasChanged) { + if (next !== undefined && next !== null) { + // If next is defined and doesn't equal prev, it needs animating + markToAnimate(key) + } else { + // If it's undefined, it's been removed. + removedKeys.add(key) + } + } else if (next !== undefined && removedKeys.has(key)) { + /** + * If next hasn't changed and it isn't undefined, we want to check if it's + * been removed by a higher priority + */ + markToAnimate(key) + } else { + /** + * If it hasn't changed, we add it to the list of protected values + * to ensure it doesn't get animated. + */ + typeState.protectedKeys[key] = true + } + } + + /** + * Update the typeState so next time animateChanges is called we can compare the + * latest prop and resolvedValues to these. + */ + typeState.prevProp = prop + typeState.prevResolvedValues = resolvedValues + + if (typeState.isActive) { + encounteredKeys = { ...encounteredKeys, ...resolvedValues } + } + + if (isInitialRender && visualElement.blockInitialAnimation) { + shouldAnimateType = false + } + + /** + * If this is an inherited prop we want to skip this animation + * unless the inherited variants haven't changed on this render. + */ + const willAnimateViaParent = isInherited && variantDidChange + const needsAnimating = !willAnimateViaParent || handledRemovedValues + if (shouldAnimateType && needsAnimating) { + animations.push( + ...definitionList.map((animation) => { + const options: VisualElementAnimationOptions = { type } + + /** + * If we're performing the initial animation, but we're not + * rendering at the same time as the variant-controlling parent, + * we want to use the parent's transition to calculate the stagger. + */ + if ( + typeof animation === "string" && + isInitialRender && + !willAnimateViaParent && + visualElement.manuallyAnimateOnMount && + visualElement.parent + ) { + const { parent } = visualElement + const parentVariant = resolveVariant( + parent, + animation + ) + + if (parent.enteringChildren && parentVariant) { + const { delayChildren } = + parentVariant.transition || {} + options.delay = calcChildStagger( + parent.enteringChildren, + visualElement, + delayChildren + ) + } + } + + return { + animation: animation as AnimationDefinition, + options, + } + }) + ) + } + } + + /** + * If there are some removed value that haven't been dealt with, + * we need to create a new animation that falls back either to the value + * defined in the style prop, or the last read value. + */ + if (removedKeys.size) { + const fallbackAnimation: TargetAndTransition = {} + + /** + * If the initial prop contains a transition we can use that, otherwise + * allow the animation function to use the visual element's default. + */ + if (typeof props.initial !== "boolean") { + const initialTransition = resolveVariant( + visualElement, + Array.isArray(props.initial) + ? props.initial[0] + : props.initial + ) + + if (initialTransition && initialTransition.transition) { + fallbackAnimation.transition = initialTransition.transition + } + } + + removedKeys.forEach((key) => { + const fallbackTarget = visualElement.getBaseTarget(key) + + const motionValue = visualElement.getValue(key) + if (motionValue) motionValue.liveStyle = true + + // @ts-expect-error - @mattgperry to figure if we should do something here + fallbackAnimation[key] = fallbackTarget ?? null + }) + + animations.push({ animation: fallbackAnimation }) + } + + let shouldAnimate = Boolean(animations.length) + + if ( + isInitialRender && + (props.initial === false || props.initial === props.animate) && + !visualElement.manuallyAnimateOnMount + ) { + shouldAnimate = false + } + + isInitialRender = false + return shouldAnimate ? animate(animations) : Promise.resolve() + } + + /** + * Change whether a certain animation type is active. + */ + function setActive(type: AnimationType, isActive: boolean) { + // If the active state hasn't changed, we can safely do nothing here + if (state[type].isActive === isActive) return Promise.resolve() + + // Propagate active change to children + visualElement.variantChildren?.forEach((child: any) => + child.animationState?.setActive(type, isActive) + ) + + state[type].isActive = isActive + + const animations = animateChanges(type) + + for (const key in state) { + state[key as keyof typeof state].protectedKeys = {} + } + + return animations + } + + return { + animateChanges, + setActive, + setAnimateFunction, + getState: () => state, + reset: () => { + state = createState() + /** + * Temporarily disabling resetting this flag as it prevents components + * with initial={false} from animating after being remounted, for instance + * as the child of an Activity component. + */ + // isInitialRender = true + }, + } +} + +export function checkVariantsDidChange(prev: any, next: any) { + if (typeof next === "string") { + return next !== prev + } else if (Array.isArray(next)) { + return !shallowCompare(next, prev) + } + + return false +} + +export interface AnimationTypeState { + isActive: boolean + protectedKeys: { [key: string]: true } + needsAnimating: { [key: string]: boolean } + prevResolvedValues: { [key: string]: any } + prevProp?: VariantLabels | TargetAndTransition +} + +function createTypeState(isActive = false): AnimationTypeState { + return { + isActive, + protectedKeys: {}, + needsAnimating: {}, + prevResolvedValues: {}, + } +} + +function createState() { + return { + animate: createTypeState(true), + whileInView: createTypeState(), + whileHover: createTypeState(), + whileTap: createTypeState(), + whileDrag: createTypeState(), + whileFocus: createTypeState(), + exit: createTypeState(), + } +} diff --git a/packages/motion-dom/src/render/utils/calc-child-stagger.ts b/packages/motion-dom/src/render/utils/calc-child-stagger.ts new file mode 100644 index 0000000000..f5b4c3ac7f --- /dev/null +++ b/packages/motion-dom/src/render/utils/calc-child-stagger.ts @@ -0,0 +1,26 @@ +import type { DynamicOption } from "../../animation/types" + +/** + * Calculate the stagger delay for a child element. + * Uses `any` types for visual elements to avoid circular dependencies. + */ +export function calcChildStagger( + children: Set, + child: any, + delayChildren?: number | DynamicOption, + staggerChildren: number = 0, + staggerDirection: number = 1 +): number { + const index = Array.from(children) + .sort((a, b) => a.sortNodePosition(b)) + .indexOf(child) + const numChildren = children.size + const maxStaggerDuration = (numChildren - 1) * staggerChildren + const delayIsFunction = typeof delayChildren === "function" + + return delayIsFunction + ? delayChildren(index, numChildren) + : staggerDirection === 1 + ? index * staggerChildren + : maxStaggerDuration - index * staggerChildren +} diff --git a/packages/motion-dom/src/render/utils/get-variant-context.ts b/packages/motion-dom/src/render/utils/get-variant-context.ts new file mode 100644 index 0000000000..e8027001b6 --- /dev/null +++ b/packages/motion-dom/src/render/utils/get-variant-context.ts @@ -0,0 +1,46 @@ +import { isVariantLabel } from "./is-variant-label" +import { variantProps } from "./variant-props" + +const numVariantProps = variantProps.length + +type VariantStateContext = { + initial?: string | string[] + animate?: string | string[] + exit?: string | string[] + whileHover?: string | string[] + whileDrag?: string | string[] + whileFocus?: string | string[] + whileTap?: string | string[] +} + +/** + * Get variant context from a visual element's parent chain. + * Uses `any` type for visualElement to avoid circular dependencies. + */ +export function getVariantContext( + visualElement?: any +): undefined | VariantStateContext { + if (!visualElement) return undefined + + if (!visualElement.isControllingVariants) { + const context = visualElement.parent + ? getVariantContext(visualElement.parent) || {} + : {} + if (visualElement.props.initial !== undefined) { + context.initial = visualElement.props.initial as any + } + return context + } + + const context: VariantStateContext = {} + for (let i = 0; i < numVariantProps; i++) { + const name = variantProps[i] as keyof typeof context + const prop = visualElement.props[name] + + if (isVariantLabel(prop) || prop === false) { + ;(context as any)[name] = prop + } + } + + return context +} diff --git a/packages/motion-dom/src/render/utils/is-animation-controls.ts b/packages/motion-dom/src/render/utils/is-animation-controls.ts new file mode 100644 index 0000000000..11dca87215 --- /dev/null +++ b/packages/motion-dom/src/render/utils/is-animation-controls.ts @@ -0,0 +1,9 @@ +import type { LegacyAnimationControls } from "../../node/types" + +export function isAnimationControls(v?: unknown): v is LegacyAnimationControls { + return ( + v !== null && + typeof v === "object" && + typeof (v as LegacyAnimationControls).start === "function" + ) +} diff --git a/packages/motion-dom/src/render/utils/is-controlling-variants.ts b/packages/motion-dom/src/render/utils/is-controlling-variants.ts new file mode 100644 index 0000000000..eac8d8e4bd --- /dev/null +++ b/packages/motion-dom/src/render/utils/is-controlling-variants.ts @@ -0,0 +1,17 @@ +import type { MotionNodeOptions } from "../../node/types" +import { isAnimationControls } from "./is-animation-controls" +import { isVariantLabel } from "./is-variant-label" +import { variantProps } from "./variant-props" + +export function isControllingVariants(props: MotionNodeOptions) { + return ( + isAnimationControls(props.animate) || + variantProps.some((name) => + isVariantLabel(props[name as keyof typeof props]) + ) + ) +} + +export function isVariantNode(props: MotionNodeOptions) { + return Boolean(isControllingVariants(props) || props.variants) +} diff --git a/packages/motion-dom/src/render/utils/is-forced-motion-value.ts b/packages/motion-dom/src/render/utils/is-forced-motion-value.ts new file mode 100644 index 0000000000..3350dd6c5b --- /dev/null +++ b/packages/motion-dom/src/render/utils/is-forced-motion-value.ts @@ -0,0 +1,22 @@ +import { transformProps } from "./keys-transform" +import type { MotionNodeOptions } from "../../node/types" +import { + scaleCorrectors, + addScaleCorrector, +} from "../../projection/styles/scale-correction" + +// Re-export for backward compatibility +export { scaleCorrectors } +export { addScaleCorrector as addScaleCorrectors } + +export function isForcedMotionValue( + key: string, + { layout, layoutId }: MotionNodeOptions +) { + return ( + transformProps.has(key) || + key.startsWith("origin") || + ((layout || layoutId !== undefined) && + (!!scaleCorrectors[key] || key === "opacity")) + ) +} diff --git a/packages/motion-dom/src/render/utils/is-keyframes-target.ts b/packages/motion-dom/src/render/utils/is-keyframes-target.ts new file mode 100644 index 0000000000..074ad7d09b --- /dev/null +++ b/packages/motion-dom/src/render/utils/is-keyframes-target.ts @@ -0,0 +1,7 @@ +import type { UnresolvedValueKeyframe, ValueKeyframesDefinition } from "../../animation/types" + +export const isKeyframesTarget = ( + v: ValueKeyframesDefinition +): v is UnresolvedValueKeyframe[] => { + return Array.isArray(v) +} diff --git a/packages/motion-dom/src/render/utils/is-variant-label.ts b/packages/motion-dom/src/render/utils/is-variant-label.ts new file mode 100644 index 0000000000..7818a94af4 --- /dev/null +++ b/packages/motion-dom/src/render/utils/is-variant-label.ts @@ -0,0 +1,6 @@ +/** + * Decides if the supplied variable is variant label + */ +export function isVariantLabel(v: unknown): v is string | string[] { + return typeof v === "string" || Array.isArray(v) +} diff --git a/packages/motion-dom/src/render/utils/motion-values.ts b/packages/motion-dom/src/render/utils/motion-values.ts new file mode 100644 index 0000000000..6f6247e678 --- /dev/null +++ b/packages/motion-dom/src/render/utils/motion-values.ts @@ -0,0 +1,64 @@ +import { motionValue } from "../../value" +import { isMotionValue } from "../../value/utils/is-motion-value" + +type MotionStyleLike = Record + +/** + * Updates motion values from props changes. + * Uses `any` type for element to avoid circular dependencies with VisualElement. + */ +export function updateMotionValuesFromProps( + element: any, + next: MotionStyleLike, + prev: MotionStyleLike +) { + for (const key in next) { + const nextValue = next[key] + const prevValue = prev[key] + + if (isMotionValue(nextValue)) { + /** + * If this is a motion value found in props or style, we want to add it + * to our visual element's motion value map. + */ + element.addValue(key, nextValue) + } else if (isMotionValue(prevValue)) { + /** + * If we're swapping from a motion value to a static value, + * create a new motion value from that + */ + element.addValue(key, motionValue(nextValue, { owner: element })) + } else if (prevValue !== nextValue) { + /** + * If this is a flat value that has changed, update the motion value + * or create one if it doesn't exist. We only want to do this if we're + * not handling the value with our animation state. + */ + if (element.hasValue(key)) { + const existingValue = element.getValue(key)! + + if (existingValue.liveStyle === true) { + existingValue.jump(nextValue) + } else if (!existingValue.hasAnimated) { + existingValue.set(nextValue) + } + } else { + const latestValue = element.getStaticValue(key) + element.addValue( + key, + motionValue( + latestValue !== undefined ? latestValue : nextValue, + { owner: element } + ) + ) + } + } + } + + // Handle removed values + for (const key in prev) { + if (next[key] === undefined) element.removeValue(key) + } + + return next +} diff --git a/packages/motion-dom/src/render/utils/reduced-motion/index.ts b/packages/motion-dom/src/render/utils/reduced-motion/index.ts new file mode 100644 index 0000000000..6ee2128cd4 --- /dev/null +++ b/packages/motion-dom/src/render/utils/reduced-motion/index.ts @@ -0,0 +1,23 @@ +import { hasReducedMotionListener, prefersReducedMotion } from "./state" + +const isBrowser = typeof window !== "undefined" + +export function initPrefersReducedMotion() { + hasReducedMotionListener.current = true + if (!isBrowser) return + + if (window.matchMedia) { + const motionMediaQuery = window.matchMedia("(prefers-reduced-motion)") + + const setReducedMotionPreferences = () => + (prefersReducedMotion.current = motionMediaQuery.matches) + + motionMediaQuery.addEventListener("change", setReducedMotionPreferences) + + setReducedMotionPreferences() + } else { + prefersReducedMotion.current = false + } +} + +export { prefersReducedMotion, hasReducedMotionListener } diff --git a/packages/motion-dom/src/render/utils/reduced-motion/state.ts b/packages/motion-dom/src/render/utils/reduced-motion/state.ts new file mode 100644 index 0000000000..b0ceaf9823 --- /dev/null +++ b/packages/motion-dom/src/render/utils/reduced-motion/state.ts @@ -0,0 +1,8 @@ +interface ReducedMotionState { + current: boolean | null +} + +// Does this device prefer reduced motion? Returns `null` server-side. +export const prefersReducedMotion: ReducedMotionState = { current: null } + +export const hasReducedMotionListener = { current: false } diff --git a/packages/motion-dom/src/render/utils/resolve-dynamic-variants.ts b/packages/motion-dom/src/render/utils/resolve-dynamic-variants.ts new file mode 100644 index 0000000000..e7a111418e --- /dev/null +++ b/packages/motion-dom/src/render/utils/resolve-dynamic-variants.ts @@ -0,0 +1,34 @@ +import type { + AnimationDefinition, + TargetAndTransition, + TargetResolver, +} from "../../node/types" +import { resolveVariantFromProps } from "./resolve-variants" + +/** + * Resolves a variant if it's a variant resolver. + * Uses `any` type for visualElement to avoid circular dependencies. + */ +export function resolveVariant( + visualElement: any, + definition?: TargetAndTransition | TargetResolver, + custom?: any +): TargetAndTransition +export function resolveVariant( + visualElement: any, + definition?: AnimationDefinition, + custom?: any +): TargetAndTransition | undefined +export function resolveVariant( + visualElement: any, + definition?: AnimationDefinition, + custom?: any +) { + const props = visualElement.getProps() + return resolveVariantFromProps( + props, + definition, + custom !== undefined ? custom : props.custom, + visualElement + ) +} diff --git a/packages/motion-dom/src/render/utils/resolve-variants.ts b/packages/motion-dom/src/render/utils/resolve-variants.ts new file mode 100644 index 0000000000..08241498d7 --- /dev/null +++ b/packages/motion-dom/src/render/utils/resolve-variants.ts @@ -0,0 +1,73 @@ +import type { + AnimationDefinition, + MotionNodeOptions, + TargetAndTransition, + TargetResolver, +} from "../../node/types" +import type { ResolvedValues } from "../types" + +function getValueState(visualElement?: any): [ResolvedValues, ResolvedValues] { + const state: [ResolvedValues, ResolvedValues] = [{}, {}] + + visualElement?.values.forEach((value: any, key: string) => { + state[0][key] = value.get() + state[1][key] = value.getVelocity() + }) + + return state +} + +export function resolveVariantFromProps( + props: MotionNodeOptions, + definition: TargetAndTransition | TargetResolver, + custom?: any, + visualElement?: any +): TargetAndTransition +export function resolveVariantFromProps( + props: MotionNodeOptions, + definition?: AnimationDefinition, + custom?: any, + visualElement?: any +): undefined | TargetAndTransition +export function resolveVariantFromProps( + props: MotionNodeOptions, + definition?: AnimationDefinition, + custom?: any, + visualElement?: any +) { + /** + * If the variant definition is a function, resolve. + */ + if (typeof definition === "function") { + const [current, velocity] = getValueState(visualElement) + definition = definition( + custom !== undefined ? custom : props.custom, + current, + velocity + ) + } + + /** + * If the variant definition is a variant label, or + * the function returned a variant label, resolve. + */ + if (typeof definition === "string") { + definition = props.variants && props.variants[definition] + } + + /** + * At this point we've resolved both functions and variant labels, + * but the resolved variant label might itself have been a function. + * If so, resolve. This can only have returned a valid target object. + */ + if (typeof definition === "function") { + const [current, velocity] = getValueState(visualElement) + definition = definition( + custom !== undefined ? custom : props.custom, + current, + velocity + ) + } + + return definition +} diff --git a/packages/motion-dom/src/render/utils/shallow-compare.ts b/packages/motion-dom/src/render/utils/shallow-compare.ts new file mode 100644 index 0000000000..96a87ecade --- /dev/null +++ b/packages/motion-dom/src/render/utils/shallow-compare.ts @@ -0,0 +1,13 @@ +export function shallowCompare(next: any[], prev: any[] | null) { + if (!Array.isArray(prev)) return false + + const prevLength = prev.length + + if (prevLength !== next.length) return false + + for (let i = 0; i < prevLength; i++) { + if (prev[i] !== next[i]) return false + } + + return true +} diff --git a/packages/motion-dom/src/render/utils/variant-props.ts b/packages/motion-dom/src/render/utils/variant-props.ts new file mode 100644 index 0000000000..5dd6de0c7d --- /dev/null +++ b/packages/motion-dom/src/render/utils/variant-props.ts @@ -0,0 +1,13 @@ +import type { AnimationType } from "../types" + +export const variantPriorityOrder: AnimationType[] = [ + "animate", + "whileInView", + "whileFocus", + "whileHover", + "whileTap", + "whileDrag", + "exit", +] + +export const variantProps = ["initial", ...variantPriorityOrder] From 530398bc99729f9388ddedc31dfdebdc26fe84a1 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Mon, 12 Jan 2026 16:20:05 +0100 Subject: [PATCH 02/11] Remove duplicate projection/utils code from framer-motion - Delete barrel files that were just re-exporting from motion-dom - Update all imports to use motion-dom directly - Add eachAxis to motion-dom exports - Delete duplicate files: mix-values.ts, each-axis.ts, has-transform.ts, measure.ts - Delete projection geometry barrel files: models.ts, delta-calc.ts, delta-apply.ts, delta-remove.ts, copy.ts, conversion.ts, utils.ts - Delete projection styles barrel files: types.ts, scale-border-radius.ts, scale-box-shadow.ts, scale-correction.ts, transform.ts, transform-origin.ts - Delete render utils barrel files: variant-props.ts, is-variant-label.ts - Update test files to import from motion-dom instead of deleted barrel files Co-Authored-By: Claude Opus 4.5 --- .../src/animation/hooks/use-animated-state.ts | 3 +- .../src/context/MotionContext/utils.ts | 2 +- .../drag/VisualElementDragControls.ts | 22 +- .../src/gestures/drag/utils/constraints.ts | 4 +- packages/framer-motion/src/index.ts | 5 +- .../motion/utils/is-forced-motion-value.ts | 3 +- packages/framer-motion/src/projection.ts | 18 +- .../animation/__tests__/mix-values.test.ts | 2 +- .../src/projection/animation/mix-values.ts | 138 ------------- .../geometry/__tests__/conversion.test.ts | 2 +- .../geometry/__tests__/copy.test.ts | 3 +- .../geometry/__tests__/delta-apply.test.ts | 2 +- .../geometry/__tests__/delta-calc.test.ts | 7 +- .../geometry/__tests__/operations.test.ts | 3 +- .../src/projection/geometry/conversion.ts | 43 ---- .../src/projection/geometry/copy.ts | 33 --- .../src/projection/geometry/delta-apply.ts | 194 ------------------ .../src/projection/geometry/delta-calc.ts | 94 --------- .../src/projection/geometry/delta-remove.ts | 122 ----------- .../src/projection/geometry/models.ts | 20 -- .../src/projection/geometry/utils.ts | 41 ---- .../projection/node/create-projection-node.ts | 54 +++-- .../styles/__tests__/scale-correction.test.ts | 7 +- .../styles/__tests__/transform.test.ts | 3 +- .../projection/styles/scale-border-radius.ts | 42 ---- .../src/projection/styles/scale-box-shadow.ts | 41 ---- .../src/projection/styles/scale-correction.ts | 30 --- .../src/projection/styles/transform-origin.ts | 0 .../src/projection/styles/transform.ts | 55 ----- .../src/projection/styles/types.ts | 17 -- .../utils/__tests__/each-axis.test.ts | 2 +- .../src/projection/utils/has-transform.ts | 35 ---- .../src/projection/utils/measure.ts | 32 --- .../framer-motion/src/render/VisualElement.ts | 2 +- .../src/render/html/HTMLVisualElement.ts | 2 +- .../src/render/object/ObjectVisualElement.ts | 2 +- .../src/render/svg/SVGVisualElement.ts | 2 +- .../utils/__tests__/StateVisualElement.ts | 2 +- .../src/render/utils/animation-state.ts | 3 +- .../src/render/utils/get-variant-context.ts | 3 +- .../render/utils/is-controlling-variants.ts | 3 +- .../src/render/utils/is-variant-label.ts | 6 - .../src/render/utils/variant-props.ts | 13 -- packages/motion-dom/src/index.ts | 1 + .../src/projection/utils/each-axis.ts | 0 45 files changed, 78 insertions(+), 1040 deletions(-) delete mode 100644 packages/framer-motion/src/projection/animation/mix-values.ts delete mode 100644 packages/framer-motion/src/projection/geometry/conversion.ts delete mode 100644 packages/framer-motion/src/projection/geometry/copy.ts delete mode 100644 packages/framer-motion/src/projection/geometry/delta-apply.ts delete mode 100644 packages/framer-motion/src/projection/geometry/delta-calc.ts delete mode 100644 packages/framer-motion/src/projection/geometry/delta-remove.ts delete mode 100644 packages/framer-motion/src/projection/geometry/models.ts delete mode 100644 packages/framer-motion/src/projection/geometry/utils.ts delete mode 100644 packages/framer-motion/src/projection/styles/scale-border-radius.ts delete mode 100644 packages/framer-motion/src/projection/styles/scale-box-shadow.ts delete mode 100644 packages/framer-motion/src/projection/styles/scale-correction.ts delete mode 100644 packages/framer-motion/src/projection/styles/transform-origin.ts delete mode 100644 packages/framer-motion/src/projection/styles/transform.ts delete mode 100644 packages/framer-motion/src/projection/styles/types.ts delete mode 100644 packages/framer-motion/src/projection/utils/has-transform.ts delete mode 100644 packages/framer-motion/src/projection/utils/measure.ts delete mode 100644 packages/framer-motion/src/render/utils/is-variant-label.ts delete mode 100644 packages/framer-motion/src/render/utils/variant-props.ts rename packages/{framer-motion => motion-dom}/src/projection/utils/each-axis.ts (100%) diff --git a/packages/framer-motion/src/animation/hooks/use-animated-state.ts b/packages/framer-motion/src/animation/hooks/use-animated-state.ts index 642b49113f..c7d7ecbcf8 100644 --- a/packages/framer-motion/src/animation/hooks/use-animated-state.ts +++ b/packages/framer-motion/src/animation/hooks/use-animated-state.ts @@ -1,9 +1,8 @@ "use client" -import { TargetAndTransition } from "motion-dom" +import { createBox, TargetAndTransition } from "motion-dom" import { useLayoutEffect, useState } from "react" import { makeUseVisualState } from "../../motion/utils/use-visual-state" -import { createBox } from "../../projection/geometry/models" import { ResolvedValues } from "../../render/types" import { VisualElement } from "../../render/VisualElement" import { useConstant } from "../../utils/use-constant" diff --git a/packages/framer-motion/src/context/MotionContext/utils.ts b/packages/framer-motion/src/context/MotionContext/utils.ts index 22d3c42406..3fd3daee0f 100644 --- a/packages/framer-motion/src/context/MotionContext/utils.ts +++ b/packages/framer-motion/src/context/MotionContext/utils.ts @@ -1,7 +1,7 @@ +import { isVariantLabel } from "motion-dom" import type { MotionContextProps } from "." import { MotionProps } from "../../motion/types" import { isControllingVariants } from "../../render/utils/is-controlling-variants" -import { isVariantLabel } from "../../render/utils/is-variant-label" export function getCurrentTreeVariants( props: MotionProps, diff --git a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts index 3810088e67..28800324d9 100644 --- a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts +++ b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts @@ -1,12 +1,18 @@ import { - isElementKeyboardAccessible, - PanInfo, - ResolvedConstraints, - Transition, + calcLength, + convertBoundingBoxToBox, + convertBoxToBoundingBox, + createBox, + eachAxis, frame, + isElementKeyboardAccessible, + measurePageBox, mixNumber, + PanInfo, percent, + ResolvedConstraints, setDragLock, + Transition, } from "motion-dom" import { Axis, Point, invariant } from "motion-utils" import { animateMotionValue } from "../../animation/interfaces/motion-value" @@ -14,15 +20,7 @@ import { addDomEvent } from "../../events/add-dom-event" import { addPointerEvent } from "../../events/add-pointer-event" import { extractEventInfo } from "../../events/event-info" import { MotionProps } from "../../motion/types" -import { - convertBoundingBoxToBox, - convertBoxToBoundingBox, -} from "../../projection/geometry/conversion" -import { calcLength } from "../../projection/geometry/delta-calc" -import { createBox } from "../../projection/geometry/models" import type { LayoutUpdateData } from "../../projection/node/types" -import { eachAxis } from "../../projection/utils/each-axis" -import { measurePageBox } from "../../projection/utils/measure" import type { VisualElement } from "../../render/VisualElement" import { getContextWindow } from "../../utils/get-context-window" import { isRefObject } from "../../utils/is-ref-object" diff --git a/packages/framer-motion/src/gestures/drag/utils/constraints.ts b/packages/framer-motion/src/gestures/drag/utils/constraints.ts index a667945f2e..0d62fdf429 100644 --- a/packages/framer-motion/src/gestures/drag/utils/constraints.ts +++ b/packages/framer-motion/src/gestures/drag/utils/constraints.ts @@ -1,5 +1,4 @@ -import type { DragElastic, ResolvedConstraints } from "motion-dom" -import { mixNumber } from "motion-dom" +import { calcLength, mixNumber, type DragElastic, type ResolvedConstraints } from "motion-dom" import { Axis, BoundingBox, @@ -8,7 +7,6 @@ import { clamp, Point, } from "motion-utils" -import { calcLength } from "../../../projection/geometry/delta-calc" /** * Apply constraints to a point. These constraints are both physical along an diff --git a/packages/framer-motion/src/index.ts b/packages/framer-motion/src/index.ts index d0194e63a8..2f06db194f 100644 --- a/packages/framer-motion/src/index.ts +++ b/packages/framer-motion/src/index.ts @@ -24,8 +24,7 @@ export { makeUseVisualState, VisualState, } from "./motion/utils/use-visual-state" -export { calcLength } from "./projection/geometry/delta-calc" -export { createBox } from "./projection/geometry/models" +export { calcLength, createBox } from "motion-dom" export { filterProps } from "./render/dom/utils/filter-props" export { AnimationType } from "./render/utils/types" export { isBrowser } from "./utils/is-browser" @@ -89,7 +88,7 @@ export { export { isMotionComponent } from "./motion/utils/is-motion-component" export { unwrapMotionComponent } from "./motion/utils/unwrap-motion-component" export { isValidMotionProp } from "./motion/utils/valid-prop" -export { addScaleCorrector } from "./projection/styles/scale-correction" +export { addScaleCorrectors as addScaleCorrector } from "motion-dom" export { useInstantLayoutTransition } from "./projection/use-instant-layout-transition" export { useResetProjection } from "./projection/use-reset-projection" export { buildTransform } from "./render/html/utils/build-transform" diff --git a/packages/framer-motion/src/motion/utils/is-forced-motion-value.ts b/packages/framer-motion/src/motion/utils/is-forced-motion-value.ts index 1f85a2a98e..19abe767fc 100644 --- a/packages/framer-motion/src/motion/utils/is-forced-motion-value.ts +++ b/packages/framer-motion/src/motion/utils/is-forced-motion-value.ts @@ -1,6 +1,5 @@ -import { transformProps } from "motion-dom" +import { scaleCorrectors, transformProps } from "motion-dom" import { MotionProps } from "../.." -import { scaleCorrectors } from "../../projection/styles/scale-correction" export function isForcedMotionValue( key: string, diff --git a/packages/framer-motion/src/projection.ts b/packages/framer-motion/src/projection.ts index ace274532e..c9edc1c38d 100644 --- a/packages/framer-motion/src/projection.ts +++ b/packages/framer-motion/src/projection.ts @@ -1,11 +1,15 @@ -export { recordStats, statsBuffer } from "motion-dom" -export { calcBoxDelta } from "./projection/geometry/delta-calc" +export { + recordStats, + statsBuffer, + calcBoxDelta, + correctBorderRadius, + correctBoxShadow, + addScaleCorrectors as addScaleCorrector, + frame, + frameData, + mix, +} from "motion-dom" export { nodeGroup } from "./projection/node/group" export { HTMLProjectionNode } from "./projection/node/HTMLProjectionNode" -export { correctBorderRadius } from "./projection/styles/scale-border-radius" -export { correctBoxShadow } from "./projection/styles/scale-box-shadow" -export { addScaleCorrector } from "./projection/styles/scale-correction" export { HTMLVisualElement } from "./render/html/HTMLVisualElement" export { buildTransform } from "./render/html/utils/build-transform" -export { frame, frameData, mix } -import { frame, frameData, mix } from "motion-dom" diff --git a/packages/framer-motion/src/projection/animation/__tests__/mix-values.test.ts b/packages/framer-motion/src/projection/animation/__tests__/mix-values.test.ts index 7c98a2b7bd..b95a38d468 100644 --- a/packages/framer-motion/src/projection/animation/__tests__/mix-values.test.ts +++ b/packages/framer-motion/src/projection/animation/__tests__/mix-values.test.ts @@ -1,4 +1,4 @@ -import { mixValues } from "../mix-values" +import { mixValues } from "motion-dom" describe("mixValues", () => { test("mixes borderRadius numbers", () => { diff --git a/packages/framer-motion/src/projection/animation/mix-values.ts b/packages/framer-motion/src/projection/animation/mix-values.ts deleted file mode 100644 index 92a4108dad..0000000000 --- a/packages/framer-motion/src/projection/animation/mix-values.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { type AnyResolvedKeyframe, mixNumber, percent, px } from "motion-dom" -import { - progress as calcProgress, - circOut, - EasingFunction, - noop, -} from "motion-utils" -import { ResolvedValues } from "../../render/types" - -const borders = ["TopLeft", "TopRight", "BottomLeft", "BottomRight"] -const numBorders = borders.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 = `border${borders[i]}Radius` - 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 -} - -// /** -// * We only want to mix the background color if there's a follow element -// * that we're not crossfading opacity between. For instance with switch -// * AnimateSharedLayout animations, this helps the illusion of a continuous -// * element being animated but also cuts down on the number of paints triggered -// * for elements where opacity is doing that work for us. -// */ -// if ( -// !hasFollowElement && -// latestLeadValues.backgroundColor && -// latestFollowValues.backgroundColor -// ) { -// /** -// * This isn't ideal performance-wise as mixColor is creating a new function every frame. -// * We could probably create a mixer that runs at the start of the animation but -// * the idea behind the crossfader is that it runs dynamically between two potentially -// * changing targets (ie opacity or borderRadius may be animating independently via variants) -// */ -// leadState.backgroundColor = followState.backgroundColor = mixColor( -// latestFollowValues.backgroundColor as string, -// latestLeadValues.backgroundColor as string -// )(p) -// } - -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)) - } -} diff --git a/packages/framer-motion/src/projection/geometry/__tests__/conversion.test.ts b/packages/framer-motion/src/projection/geometry/__tests__/conversion.test.ts index 5ed2dbe800..a27d0c4a9b 100644 --- a/packages/framer-motion/src/projection/geometry/__tests__/conversion.test.ts +++ b/packages/framer-motion/src/projection/geometry/__tests__/conversion.test.ts @@ -1,4 +1,4 @@ -import { convertBoundingBoxToBox } from "../conversion" +import { convertBoundingBoxToBox } from "motion-dom" describe("convertBoundingBoxToBox", () => { it("Correctly converts a bounding box into a box", () => { diff --git a/packages/framer-motion/src/projection/geometry/__tests__/copy.test.ts b/packages/framer-motion/src/projection/geometry/__tests__/copy.test.ts index 6695ac73ff..f042708cf5 100644 --- a/packages/framer-motion/src/projection/geometry/__tests__/copy.test.ts +++ b/packages/framer-motion/src/projection/geometry/__tests__/copy.test.ts @@ -1,5 +1,4 @@ -import { copyBoxInto } from "../copy" -import { createBox } from "../models" +import { copyBoxInto, createBox } from "motion-dom" describe("copyBoxInto", () => { it("copies one box into an existing box", () => { diff --git a/packages/framer-motion/src/projection/geometry/__tests__/delta-apply.test.ts b/packages/framer-motion/src/projection/geometry/__tests__/delta-apply.test.ts index 56ce365b89..f87abee72c 100644 --- a/packages/framer-motion/src/projection/geometry/__tests__/delta-apply.test.ts +++ b/packages/framer-motion/src/projection/geometry/__tests__/delta-apply.test.ts @@ -1,4 +1,4 @@ -import { scalePoint, applyPointDelta, applyAxisDelta } from "../delta-apply" +import { scalePoint, applyPointDelta, applyAxisDelta } from "motion-dom" describe("scalePoint", () => { test("correctly scales a point based on a factor and an originPoint", () => { diff --git a/packages/framer-motion/src/projection/geometry/__tests__/delta-calc.test.ts b/packages/framer-motion/src/projection/geometry/__tests__/delta-calc.test.ts index 881ef50263..b2294367d7 100644 --- a/packages/framer-motion/src/projection/geometry/__tests__/delta-calc.test.ts +++ b/packages/framer-motion/src/projection/geometry/__tests__/delta-calc.test.ts @@ -3,9 +3,10 @@ import { calcAxisDelta, calcRelativeBox, calcRelativePosition, -} from "../delta-calc" -import { applyAxisDelta } from "../delta-apply" -import { createBox, createDelta } from "../models" + applyAxisDelta, + createBox, + createDelta, +} from "motion-dom" describe("isNear", () => { test("Correctly indicate when the provided value is within maxDistance of the provided target", () => { diff --git a/packages/framer-motion/src/projection/geometry/__tests__/operations.test.ts b/packages/framer-motion/src/projection/geometry/__tests__/operations.test.ts index ce5ef11cd8..68ff9501f6 100644 --- a/packages/framer-motion/src/projection/geometry/__tests__/operations.test.ts +++ b/packages/framer-motion/src/projection/geometry/__tests__/operations.test.ts @@ -1,5 +1,4 @@ -import { createAxis } from "../models" -import { translateAxis } from "../delta-apply" +import { createAxis, translateAxis } from "motion-dom" describe("translateAxis", () => { it("applies a translation to an Axis", () => { diff --git a/packages/framer-motion/src/projection/geometry/conversion.ts b/packages/framer-motion/src/projection/geometry/conversion.ts deleted file mode 100644 index ba7e950095..0000000000 --- a/packages/framer-motion/src/projection/geometry/conversion.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { BoundingBox, Box, TransformPoint } from "motion-utils" - -/** - * Bounding boxes tend to be defined as top, left, right, bottom. For various operations - * it's easier to consider each axis individually. This function returns a bounding box - * as a map of single-axis min/max values. - */ -export function convertBoundingBoxToBox({ - top, - left, - right, - bottom, -}: BoundingBox): Box { - return { - x: { min: left, max: right }, - y: { min: top, max: bottom }, - } -} - -export function convertBoxToBoundingBox({ x, y }: Box): BoundingBox { - return { top: y.min, right: x.max, bottom: y.max, left: x.min } -} - -/** - * Applies a TransformPoint function to a bounding box. TransformPoint is usually a function - * provided by Framer to allow measured points to be corrected for device scaling. This is used - * when measuring DOM elements and DOM event points. - */ -export function transformBoxPoints( - point: BoundingBox, - transformPoint?: TransformPoint -) { - if (!transformPoint) return point - const topLeft = transformPoint({ x: point.left, y: point.top }) - const bottomRight = transformPoint({ x: point.right, y: point.bottom }) - - return { - top: topLeft.y, - left: topLeft.x, - bottom: bottomRight.y, - right: bottomRight.x, - } -} diff --git a/packages/framer-motion/src/projection/geometry/copy.ts b/packages/framer-motion/src/projection/geometry/copy.ts deleted file mode 100644 index a642c61641..0000000000 --- a/packages/framer-motion/src/projection/geometry/copy.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Axis, AxisDelta, Box } from "motion-utils" - -/** - * Reset an axis to the provided origin box. - * - * This is a mutative operation. - */ -export function copyAxisInto(axis: Axis, originAxis: Axis) { - axis.min = originAxis.min - axis.max = originAxis.max -} - -/** - * Reset a box to the provided origin box. - * - * This is a mutative operation. - */ -export function copyBoxInto(box: Box, originBox: Box) { - copyAxisInto(box.x, originBox.x) - copyAxisInto(box.y, originBox.y) -} - -/** - * Reset a delta to the provided origin box. - * - * This is a mutative operation. - */ -export function copyAxisDeltaInto(delta: AxisDelta, originDelta: AxisDelta) { - delta.translate = originDelta.translate - delta.scale = originDelta.scale - delta.originPoint = originDelta.originPoint - delta.origin = originDelta.origin -} diff --git a/packages/framer-motion/src/projection/geometry/delta-apply.ts b/packages/framer-motion/src/projection/geometry/delta-apply.ts deleted file mode 100644 index c0404cdebb..0000000000 --- a/packages/framer-motion/src/projection/geometry/delta-apply.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { mixNumber } from "motion-dom" -import { Axis, Box, Delta, Point } from "motion-utils" -import { ResolvedValues } from "../../render/types" -import { IProjectionNode } from "../node/types" -import { hasTransform } from "../utils/has-transform" - -/** - * Scales a point based on a factor and an originPoint - */ -export function scalePoint(point: number, scale: number, originPoint: number) { - const distanceFromOrigin = point - originPoint - const scaled = scale * distanceFromOrigin - return originPoint + scaled -} - -/** - * Applies a translate/scale delta to a point - */ -export function applyPointDelta( - point: number, - translate: number, - scale: number, - originPoint: number, - boxScale?: number -): number { - if (boxScale !== undefined) { - point = scalePoint(point, boxScale, originPoint) - } - - return scalePoint(point, scale, originPoint) + translate -} - -/** - * Applies a translate/scale delta to an axis - */ -export function applyAxisDelta( - axis: Axis, - translate: number = 0, - scale: number = 1, - originPoint: number, - boxScale?: number -): void { - axis.min = applyPointDelta( - axis.min, - translate, - scale, - originPoint, - boxScale - ) - - axis.max = applyPointDelta( - axis.max, - translate, - scale, - originPoint, - boxScale - ) -} - -/** - * Applies a translate/scale delta to a box - */ -export function applyBoxDelta(box: Box, { x, y }: Delta): void { - applyAxisDelta(box.x, x.translate, x.scale, x.originPoint) - applyAxisDelta(box.y, y.translate, y.scale, y.originPoint) -} - -const TREE_SCALE_SNAP_MIN = 0.999999999999 -const TREE_SCALE_SNAP_MAX = 1.0000000000001 - -/** - * Apply a tree of deltas to a box. We do this to calculate the effect of all the transforms - * in a tree upon our box before then calculating how to project it into our desired viewport-relative box - * - * This is the final nested loop within updateLayoutDelta for future refactoring - */ -export function applyTreeDeltas( - box: Box, - treeScale: Point, - treePath: IProjectionNode[], - isSharedTransition: boolean = false -) { - const treeLength = treePath.length - if (!treeLength) return - - // Reset the treeScale - treeScale.x = treeScale.y = 1 - - let node: IProjectionNode - let delta: Delta | undefined - - for (let i = 0; i < treeLength; i++) { - node = treePath[i] - delta = node.projectionDelta - - /** - * TODO: Prefer to remove this, but currently we have motion components with - * display: contents in Framer. - */ - const { visualElement } = node.options - if ( - visualElement && - visualElement.props.style && - visualElement.props.style.display === "contents" - ) { - continue - } - - if ( - isSharedTransition && - node.options.layoutScroll && - node.scroll && - node !== node.root - ) { - transformBox(box, { - x: -node.scroll.offset.x, - y: -node.scroll.offset.y, - }) - } - - if (delta) { - // Incoporate each ancestor's scale into a cumulative treeScale for this component - treeScale.x *= delta.x.scale - treeScale.y *= delta.y.scale - - // Apply each ancestor's calculated delta into this component's recorded layout box - applyBoxDelta(box, delta) - } - - if (isSharedTransition && hasTransform(node.latestValues)) { - transformBox(box, node.latestValues) - } - } - - /** - * Snap tree scale back to 1 if it's within a non-perceivable threshold. - * This will help reduce useless scales getting rendered. - */ - if ( - treeScale.x < TREE_SCALE_SNAP_MAX && - treeScale.x > TREE_SCALE_SNAP_MIN - ) { - treeScale.x = 1.0 - } - if ( - treeScale.y < TREE_SCALE_SNAP_MAX && - treeScale.y > TREE_SCALE_SNAP_MIN - ) { - treeScale.y = 1.0 - } -} - -export function translateAxis(axis: Axis, distance: number) { - axis.min = axis.min + distance - axis.max = axis.max + distance -} - -/** - * Apply a transform to an axis from the latest resolved motion values. - * This function basically acts as a bridge between a flat motion value map - * and applyAxisDelta - */ -export function transformAxis( - axis: Axis, - axisTranslate?: number, - axisScale?: number, - boxScale?: number, - axisOrigin: number = 0.5 -): void { - const originPoint = mixNumber(axis.min, axis.max, axisOrigin) - - // Apply the axis delta to the final axis - applyAxisDelta(axis, axisTranslate, axisScale, originPoint, boxScale) -} - -/** - * Apply a transform to a box from the latest resolved motion values. - */ -export function transformBox(box: Box, transform: ResolvedValues) { - transformAxis( - box.x, - transform.x as number, - transform.scaleX as number, - transform.scale as number, - transform.originX as number - ) - transformAxis( - box.y, - transform.y as number, - transform.scaleY as number, - transform.scale as number, - transform.originY as number - ) -} diff --git a/packages/framer-motion/src/projection/geometry/delta-calc.ts b/packages/framer-motion/src/projection/geometry/delta-calc.ts deleted file mode 100644 index 088579d542..0000000000 --- a/packages/framer-motion/src/projection/geometry/delta-calc.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { mixNumber } from "motion-dom" -import { Axis, AxisDelta, Box, Delta } from "motion-utils" -import { ResolvedValues } from "../../render/types" - -const SCALE_PRECISION = 0.0001 -const SCALE_MIN = 1 - SCALE_PRECISION -const SCALE_MAX = 1 + SCALE_PRECISION -const TRANSLATE_PRECISION = 0.01 -const TRANSLATE_MIN = 0 - TRANSLATE_PRECISION -const TRANSLATE_MAX = 0 + TRANSLATE_PRECISION - -export function calcLength(axis: Axis) { - return axis.max - axis.min -} - -export function isNear( - value: number, - target: number, - maxDistance: number -): boolean { - return Math.abs(value - target) <= maxDistance -} - -export function calcAxisDelta( - delta: AxisDelta, - source: Axis, - target: Axis, - origin: number = 0.5 -) { - delta.origin = origin - delta.originPoint = mixNumber(source.min, source.max, delta.origin) - delta.scale = calcLength(target) / calcLength(source) - delta.translate = - mixNumber(target.min, target.max, delta.origin) - delta.originPoint - - if ( - (delta.scale >= SCALE_MIN && delta.scale <= SCALE_MAX) || - isNaN(delta.scale) - ) { - delta.scale = 1.0 - } - - if ( - (delta.translate >= TRANSLATE_MIN && - delta.translate <= TRANSLATE_MAX) || - isNaN(delta.translate) - ) { - delta.translate = 0.0 - } -} - -export function calcBoxDelta( - delta: Delta, - source: Box, - target: Box, - origin?: ResolvedValues -): void { - calcAxisDelta( - delta.x, - source.x, - target.x, - origin ? (origin.originX as number) : undefined - ) - calcAxisDelta( - delta.y, - source.y, - target.y, - origin ? (origin.originY as number) : undefined - ) -} - -export function calcRelativeAxis(target: Axis, relative: Axis, parent: Axis) { - target.min = parent.min + relative.min - target.max = target.min + calcLength(relative) -} - -export function calcRelativeBox(target: Box, relative: Box, parent: Box) { - calcRelativeAxis(target.x, relative.x, parent.x) - calcRelativeAxis(target.y, relative.y, parent.y) -} - -export function calcRelativeAxisPosition( - target: Axis, - layout: Axis, - parent: Axis -) { - target.min = layout.min - parent.min - target.max = target.min + calcLength(layout) -} - -export function calcRelativePosition(target: Box, layout: Box, parent: Box) { - calcRelativeAxisPosition(target.x, layout.x, parent.x) - calcRelativeAxisPosition(target.y, layout.y, parent.y) -} diff --git a/packages/framer-motion/src/projection/geometry/delta-remove.ts b/packages/framer-motion/src/projection/geometry/delta-remove.ts deleted file mode 100644 index 8426297880..0000000000 --- a/packages/framer-motion/src/projection/geometry/delta-remove.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { mixNumber, percent } from "motion-dom" -import { Axis, Box } from "motion-utils" -import { ResolvedValues } from "../../render/types" -import { scalePoint } from "./delta-apply" - -/** - * Remove a delta from a point. This is essentially the steps of applyPointDelta in reverse - */ -export function removePointDelta( - point: number, - translate: number, - scale: number, - originPoint: number, - boxScale?: number -): number { - point -= translate - point = scalePoint(point, 1 / scale, originPoint) - - if (boxScale !== undefined) { - point = scalePoint(point, 1 / boxScale, originPoint) - } - - return point -} - -/** - * Remove a delta from an axis. This is essentially the steps of applyAxisDelta in reverse - */ -export function removeAxisDelta( - axis: Axis, - translate: number | string = 0, - scale: number = 1, - origin: number = 0.5, - boxScale?: number, - originAxis: Axis = axis, - sourceAxis: Axis = axis -): void { - if (percent.test(translate)) { - translate = parseFloat(translate as string) - const relativeProgress = mixNumber( - sourceAxis.min, - sourceAxis.max, - translate / 100 - ) - translate = relativeProgress - sourceAxis.min - } - - if (typeof translate !== "number") return - - let originPoint = mixNumber(originAxis.min, originAxis.max, origin) - if (axis === originAxis) originPoint -= translate - - axis.min = removePointDelta( - axis.min, - translate, - scale, - originPoint, - boxScale - ) - - axis.max = removePointDelta( - axis.max, - translate, - scale, - originPoint, - boxScale - ) -} - -/** - * Remove a transforms from an axis. This is essentially the steps of applyAxisTransforms in reverse - * and acts as a bridge between motion values and removeAxisDelta - */ -export function removeAxisTransforms( - axis: Axis, - transforms: ResolvedValues, - [key, scaleKey, originKey]: string[], - origin?: Axis, - sourceAxis?: Axis -) { - removeAxisDelta( - axis, - transforms[key] as number, - transforms[scaleKey] as number, - transforms[originKey] as number, - transforms.scale as number, - origin, - sourceAxis - ) -} - -/** - * The names of the motion values we want to apply as translation, scale and origin. - */ -const xKeys = ["x", "scaleX", "originX"] -const yKeys = ["y", "scaleY", "originY"] - -/** - * Remove a transforms from an box. This is essentially the steps of applyAxisBox in reverse - * and acts as a bridge between motion values and removeAxisDelta - */ -export function removeBoxTransforms( - box: Box, - transforms: ResolvedValues, - originBox?: Box, - sourceBox?: Box -): void { - removeAxisTransforms( - box.x, - transforms, - xKeys, - originBox ? originBox.x : undefined, - sourceBox ? sourceBox.x : undefined - ) - removeAxisTransforms( - box.y, - transforms, - yKeys, - originBox ? originBox.y : undefined, - sourceBox ? sourceBox.y : undefined - ) -} diff --git a/packages/framer-motion/src/projection/geometry/models.ts b/packages/framer-motion/src/projection/geometry/models.ts deleted file mode 100644 index 94366060da..0000000000 --- a/packages/framer-motion/src/projection/geometry/models.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Axis, AxisDelta, Box, Delta } from "motion-utils" - -export const createAxisDelta = (): AxisDelta => ({ - translate: 0, - scale: 1, - origin: 0, - originPoint: 0, -}) - -export const createDelta = (): Delta => ({ - x: createAxisDelta(), - y: createAxisDelta(), -}) - -export const createAxis = (): Axis => ({ min: 0, max: 0 }) - -export const createBox = (): Box => ({ - x: createAxis(), - y: createAxis(), -}) diff --git a/packages/framer-motion/src/projection/geometry/utils.ts b/packages/framer-motion/src/projection/geometry/utils.ts deleted file mode 100644 index ede3558d31..0000000000 --- a/packages/framer-motion/src/projection/geometry/utils.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Axis, AxisDelta, Box, Delta } from "motion-utils" -import { calcLength } from "./delta-calc" - -function isAxisDeltaZero(delta: AxisDelta) { - return delta.translate === 0 && delta.scale === 1 -} - -export function isDeltaZero(delta: Delta) { - return isAxisDeltaZero(delta.x) && isAxisDeltaZero(delta.y) -} - -export function axisEquals(a: Axis, b: Axis) { - return a.min === b.min && a.max === b.max -} - -export function boxEquals(a: Box, b: Box) { - return axisEquals(a.x, b.x) && axisEquals(a.y, b.y) -} - -export function axisEqualsRounded(a: Axis, b: Axis) { - return ( - Math.round(a.min) === Math.round(b.min) && - Math.round(a.max) === Math.round(b.max) - ) -} - -export function boxEqualsRounded(a: Box, b: Box) { - return axisEqualsRounded(a.x, b.x) && axisEqualsRounded(a.y, b.y) -} - -export function aspectRatio(box: Box): number { - return calcLength(box.x) / calcLength(box.y) -} - -export function axisDeltaEquals(a: AxisDelta, b: AxisDelta) { - return ( - a.translate === b.translate && - a.scale === b.scale && - a.originPoint === b.originPoint - ) -} diff --git a/packages/framer-motion/src/projection/node/create-projection-node.ts b/packages/framer-motion/src/projection/node/create-projection-node.ts index 7ffac3b483..66b321d353 100644 --- a/packages/framer-motion/src/projection/node/create-projection-node.ts +++ b/packages/framer-motion/src/projection/node/create-projection-node.ts @@ -1,19 +1,45 @@ import { activeAnimations, + applyBoxDelta, + applyTreeDeltas, + aspectRatio, + axisDeltaEquals, + boxEquals, + boxEqualsRounded, + buildProjectionTransform, + calcBoxDelta, + calcLength, + calcRelativeBox, + calcRelativePosition, cancelFrame, + copyAxisDeltaInto, + copyBoxInto, + createBox, + createDelta, + eachAxis, frame, frameData, frameSteps, getValueTransition, + has2DTranslate, + hasScale, + hasTransform, + isDeltaZero, + isNear, isSVGElement, isSVGSVGElement, JSAnimation, microtask, mixNumber, + mixValues, MotionValue, motionValue, + removeBoxTransforms, + scaleCorrectors, statsBuffer, time, + transformBox, + translateAxis, Transition, ValueAnimationOptions, type Process, @@ -37,35 +63,7 @@ import { FlatTree } from "../../render/utils/flat-tree" import { VisualElement } from "../../render/VisualElement" import { delay } from "../../utils/delay" import { resolveMotionValue } from "../../value/utils/resolve-motion-value" -import { mixValues } from "../animation/mix-values" -import { copyAxisDeltaInto, copyBoxInto } from "../geometry/copy" -import { - applyBoxDelta, - applyTreeDeltas, - transformBox, - translateAxis, -} from "../geometry/delta-apply" -import { - calcBoxDelta, - calcLength, - calcRelativeBox, - calcRelativePosition, - isNear, -} from "../geometry/delta-calc" -import { removeBoxTransforms } from "../geometry/delta-remove" -import { createBox, createDelta } from "../geometry/models" -import { - aspectRatio, - axisDeltaEquals, - boxEquals, - boxEqualsRounded, - isDeltaZero, -} from "../geometry/utils" import { NodeStack } from "../shared/stack" -import { scaleCorrectors } from "../styles/scale-correction" -import { buildProjectionTransform } from "../styles/transform" -import { eachAxis } from "../utils/each-axis" -import { has2DTranslate, hasScale, hasTransform } from "../utils/has-transform" import { globalProjectionState } from "./state" import { IProjectionNode, diff --git a/packages/framer-motion/src/projection/styles/__tests__/scale-correction.test.ts b/packages/framer-motion/src/projection/styles/__tests__/scale-correction.test.ts index 834ef40a0b..39f108adec 100644 --- a/packages/framer-motion/src/projection/styles/__tests__/scale-correction.test.ts +++ b/packages/framer-motion/src/projection/styles/__tests__/scale-correction.test.ts @@ -1,7 +1,10 @@ +import { + correctBorderRadius, + pixelsToPercent, + correctBoxShadow, +} from "motion-dom" import { createTestNode } from "../../node/__tests__/TestProjectionNode" import { IProjectionNode } from "../../node/types" -import { correctBorderRadius, pixelsToPercent } from "../scale-border-radius" -import { correctBoxShadow } from "../scale-box-shadow" describe("pixelsToPercent", () => { test("Correctly converts pixels to percent", () => { diff --git a/packages/framer-motion/src/projection/styles/__tests__/transform.test.ts b/packages/framer-motion/src/projection/styles/__tests__/transform.test.ts index ca4b0f5ff5..7a19a827e2 100644 --- a/packages/framer-motion/src/projection/styles/__tests__/transform.test.ts +++ b/packages/framer-motion/src/projection/styles/__tests__/transform.test.ts @@ -1,5 +1,4 @@ -import { buildProjectionTransform } from "../transform" -import { createDelta } from "../../geometry/models" +import { buildProjectionTransform, createDelta } from "motion-dom" describe("buildProjectionTransform", () => { it("Returns 'none' when no transform required", () => { diff --git a/packages/framer-motion/src/projection/styles/scale-border-radius.ts b/packages/framer-motion/src/projection/styles/scale-border-radius.ts deleted file mode 100644 index 8d7e1cbfd9..0000000000 --- a/packages/framer-motion/src/projection/styles/scale-border-radius.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { px } from "motion-dom" -import { Axis } from "motion-utils" -import { ScaleCorrectorDefinition } from "./types" - -export function pixelsToPercent(pixels: number, axis: Axis): number { - if (axis.max === axis.min) return 0 - 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 const correctBorderRadius: ScaleCorrectorDefinition = { - correct: (latest, node) => { - if (!node.target) return latest - - /** - * 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, node.target.x) - const y = pixelsToPercent(latest, node.target.y) - - return `${x}% ${y}%` - }, -} diff --git a/packages/framer-motion/src/projection/styles/scale-box-shadow.ts b/packages/framer-motion/src/projection/styles/scale-box-shadow.ts deleted file mode 100644 index 20b627c5c9..0000000000 --- a/packages/framer-motion/src/projection/styles/scale-box-shadow.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { complex, mixNumber } from "motion-dom" -import { ScaleCorrectorDefinition } from "./types" - -export const correctBoxShadow: ScaleCorrectorDefinition = { - correct: (latest: string, { treeScale, projectionDelta }) => { - const original = latest - 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 = projectionDelta!.x.scale * treeScale!.x - const yScale = projectionDelta!.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 = mixNumber(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 - - return template(shadow) - }, -} diff --git a/packages/framer-motion/src/projection/styles/scale-correction.ts b/packages/framer-motion/src/projection/styles/scale-correction.ts deleted file mode 100644 index e03473694a..0000000000 --- a/packages/framer-motion/src/projection/styles/scale-correction.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { isCSSVariableName } from "motion-dom" -import { correctBorderRadius } from "./scale-border-radius" -import { correctBoxShadow } from "./scale-box-shadow" -import { ScaleCorrectorMap } from "./types" - -export const scaleCorrectors: ScaleCorrectorMap = { - borderRadius: { - ...correctBorderRadius, - applyTo: [ - "borderTopLeftRadius", - "borderTopRightRadius", - "borderBottomLeftRadius", - "borderBottomRightRadius", - ], - }, - borderTopLeftRadius: correctBorderRadius, - borderTopRightRadius: correctBorderRadius, - borderBottomLeftRadius: correctBorderRadius, - borderBottomRightRadius: correctBorderRadius, - boxShadow: correctBoxShadow, -} - -export function addScaleCorrector(correctors: ScaleCorrectorMap) { - for (const key in correctors) { - scaleCorrectors[key] = correctors[key] - if (isCSSVariableName(key)) { - scaleCorrectors[key].isCSSVariable = true - } - } -} diff --git a/packages/framer-motion/src/projection/styles/transform-origin.ts b/packages/framer-motion/src/projection/styles/transform-origin.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/framer-motion/src/projection/styles/transform.ts b/packages/framer-motion/src/projection/styles/transform.ts deleted file mode 100644 index b26bb700b4..0000000000 --- a/packages/framer-motion/src/projection/styles/transform.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { Delta, Point } from "motion-utils" -import { ResolvedValues } from "../../render/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, rotateX, rotateY, skewX, skewY } = - latestTransform - if (transformPerspective) - transform = `perspective(${transformPerspective}px) ${transform}` - if (rotate) transform += `rotate(${rotate}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" -} diff --git a/packages/framer-motion/src/projection/styles/types.ts b/packages/framer-motion/src/projection/styles/types.ts deleted file mode 100644 index 8d4f0b8ed0..0000000000 --- a/packages/framer-motion/src/projection/styles/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { type AnyResolvedKeyframe } from "motion-dom" -import { IProjectionNode } from "../node/types" - -export type ScaleCorrector = ( - latest: AnyResolvedKeyframe, - node: IProjectionNode -) => AnyResolvedKeyframe - -export interface ScaleCorrectorDefinition { - correct: ScaleCorrector - applyTo?: string[] - isCSSVariable?: boolean -} - -export interface ScaleCorrectorMap { - [key: string]: ScaleCorrectorDefinition -} diff --git a/packages/framer-motion/src/projection/utils/__tests__/each-axis.test.ts b/packages/framer-motion/src/projection/utils/__tests__/each-axis.test.ts index cf5f3290f0..bca5de770d 100644 --- a/packages/framer-motion/src/projection/utils/__tests__/each-axis.test.ts +++ b/packages/framer-motion/src/projection/utils/__tests__/each-axis.test.ts @@ -1,4 +1,4 @@ -import { eachAxis } from "../each-axis" +import { eachAxis } from "motion-dom" describe("eachAxis", () => { it("calls a function, once for each axis", () => { diff --git a/packages/framer-motion/src/projection/utils/has-transform.ts b/packages/framer-motion/src/projection/utils/has-transform.ts deleted file mode 100644 index 279e3c5b7b..0000000000 --- a/packages/framer-motion/src/projection/utils/has-transform.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { type AnyResolvedKeyframe } from "motion-dom" -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%" -} diff --git a/packages/framer-motion/src/projection/utils/measure.ts b/packages/framer-motion/src/projection/utils/measure.ts deleted file mode 100644 index 03fc64a653..0000000000 --- a/packages/framer-motion/src/projection/utils/measure.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { TransformPoint } from "motion-utils" -import { - convertBoundingBoxToBox, - transformBoxPoints, -} from "../geometry/conversion" -import { translateAxis } from "../geometry/delta-apply" -import { IProjectionNode } from "../node/types" - -export function measureViewportBox( - instance: HTMLElement, - transformPoint?: TransformPoint -) { - return convertBoundingBoxToBox( - transformBoxPoints(instance.getBoundingClientRect(), transformPoint) - ) -} - -export function measurePageBox( - element: HTMLElement, - rootProjectionNode: IProjectionNode, - transformPagePoint?: TransformPoint -) { - const viewportBox = measureViewportBox(element, transformPagePoint) - const { scroll } = rootProjectionNode - - if (scroll) { - translateAxis(viewportBox.x, scroll.offset.x) - translateAxis(viewportBox.y, scroll.offset.y) - } - - return viewportBox -} diff --git a/packages/framer-motion/src/render/VisualElement.ts b/packages/framer-motion/src/render/VisualElement.ts index 537a6a65d5..4908de9ebc 100644 --- a/packages/framer-motion/src/render/VisualElement.ts +++ b/packages/framer-motion/src/render/VisualElement.ts @@ -1,6 +1,7 @@ import { cancelFrame, complex, + createBox, findValueType, frame, getAnimatableNone, @@ -29,7 +30,6 @@ import { featureDefinitions } from "../motion/features/definitions" import { Feature } from "../motion/features/Feature" import { FeatureDefinitions } from "../motion/features/types" import { MotionProps, MotionStyle } from "../motion/types" -import { createBox } from "../projection/geometry/models" import { IProjectionNode } from "../projection/node/types" import { initPrefersReducedMotion } from "../utils/reduced-motion" import { diff --git a/packages/framer-motion/src/render/html/HTMLVisualElement.ts b/packages/framer-motion/src/render/html/HTMLVisualElement.ts index 9a955783cc..0a8ee7c154 100644 --- a/packages/framer-motion/src/render/html/HTMLVisualElement.ts +++ b/packages/framer-motion/src/render/html/HTMLVisualElement.ts @@ -2,13 +2,13 @@ import { AnyResolvedKeyframe, defaultTransformValue, isCSSVariableName, + measureViewportBox, readTransformValue, transformProps, } from "motion-dom" import type { Box } from "motion-utils" import { MotionConfigContext } from "../../context/MotionConfigContext" import { MotionProps } from "../../motion/types" -import { measureViewportBox } from "../../projection/utils/measure" import { DOMVisualElement } from "../dom/DOMVisualElement" import { DOMVisualElementOptions } from "../dom/types" import type { ResolvedValues } from "../types" diff --git a/packages/framer-motion/src/render/object/ObjectVisualElement.ts b/packages/framer-motion/src/render/object/ObjectVisualElement.ts index b9f545a5e3..fb47f6b48b 100644 --- a/packages/framer-motion/src/render/object/ObjectVisualElement.ts +++ b/packages/framer-motion/src/render/object/ObjectVisualElement.ts @@ -1,4 +1,4 @@ -import { createBox } from "../../projection/geometry/models" +import { createBox } from "motion-dom" import { ResolvedValues } from "../types" import { VisualElement } from "../VisualElement" diff --git a/packages/framer-motion/src/render/svg/SVGVisualElement.ts b/packages/framer-motion/src/render/svg/SVGVisualElement.ts index c4af3c13d9..e46ebf260a 100644 --- a/packages/framer-motion/src/render/svg/SVGVisualElement.ts +++ b/packages/framer-motion/src/render/svg/SVGVisualElement.ts @@ -1,11 +1,11 @@ import { AnyResolvedKeyframe, + createBox, getDefaultValueType, MotionValue, transformProps, } from "motion-dom" import { MotionProps, MotionStyle } from "../../motion/types" -import { createBox } from "../../projection/geometry/models" import { IProjectionNode } from "../../projection/node/types" import { DOMVisualElement } from "../dom/DOMVisualElement" import { DOMVisualElementOptions } from "../dom/types" diff --git a/packages/framer-motion/src/render/utils/__tests__/StateVisualElement.ts b/packages/framer-motion/src/render/utils/__tests__/StateVisualElement.ts index 8857a7c5d8..111f2eb2d6 100644 --- a/packages/framer-motion/src/render/utils/__tests__/StateVisualElement.ts +++ b/packages/framer-motion/src/render/utils/__tests__/StateVisualElement.ts @@ -1,6 +1,6 @@ +import { createBox } from "motion-dom" import { ResolvedValues } from "../../types" import { MotionProps, MotionStyle } from "../../../motion/types" -import { createBox } from "../../../projection/geometry/models" import { VisualElement } from "../../VisualElement" export class StateVisualElement extends VisualElement< diff --git a/packages/framer-motion/src/render/utils/animation-state.ts b/packages/framer-motion/src/render/utils/animation-state.ts index a6e1fce249..365dd68493 100644 --- a/packages/framer-motion/src/render/utils/animation-state.ts +++ b/packages/framer-motion/src/render/utils/animation-state.ts @@ -1,4 +1,5 @@ import type { AnimationDefinition, TargetAndTransition } from "motion-dom" +import { isVariantLabel, variantPriorityOrder } from "motion-dom" import { VisualElementAnimationOptions } from "../../animation/interfaces/types" import { animateVisualElement } from "../../animation/interfaces/visual-element" import { calcChildStagger } from "../../animation/utils/calc-child-stagger" @@ -8,10 +9,8 @@ import { VariantLabels } from "../../motion/types" import { shallowCompare } from "../../utils/shallow-compare" import type { VisualElement } from "../VisualElement" import { getVariantContext } from "./get-variant-context" -import { isVariantLabel } from "./is-variant-label" import { resolveVariant } from "./resolve-dynamic-variants" import { AnimationType } from "./types" -import { variantPriorityOrder } from "./variant-props" export interface AnimationState { animateChanges: (type?: AnimationType) => Promise diff --git a/packages/framer-motion/src/render/utils/get-variant-context.ts b/packages/framer-motion/src/render/utils/get-variant-context.ts index f2c9089f50..5dd7201703 100644 --- a/packages/framer-motion/src/render/utils/get-variant-context.ts +++ b/packages/framer-motion/src/render/utils/get-variant-context.ts @@ -1,6 +1,5 @@ +import { isVariantLabel, variantProps } from "motion-dom" import { VisualElement } from "../VisualElement" -import { isVariantLabel } from "./is-variant-label" -import { variantProps } from "./variant-props" const numVariantProps = variantProps.length diff --git a/packages/framer-motion/src/render/utils/is-controlling-variants.ts b/packages/framer-motion/src/render/utils/is-controlling-variants.ts index 2fef33d16e..04d6a0f4a5 100644 --- a/packages/framer-motion/src/render/utils/is-controlling-variants.ts +++ b/packages/framer-motion/src/render/utils/is-controlling-variants.ts @@ -1,7 +1,6 @@ +import { isVariantLabel, variantProps } from "motion-dom" import { isAnimationControls } from "../../animation/utils/is-animation-controls" import { MotionProps } from "../../motion/types" -import { isVariantLabel } from "./is-variant-label" -import { variantProps } from "./variant-props" export function isControllingVariants(props: MotionProps) { return ( diff --git a/packages/framer-motion/src/render/utils/is-variant-label.ts b/packages/framer-motion/src/render/utils/is-variant-label.ts deleted file mode 100644 index 7818a94af4..0000000000 --- a/packages/framer-motion/src/render/utils/is-variant-label.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Decides if the supplied variable is variant label - */ -export function isVariantLabel(v: unknown): v is string | string[] { - return typeof v === "string" || Array.isArray(v) -} diff --git a/packages/framer-motion/src/render/utils/variant-props.ts b/packages/framer-motion/src/render/utils/variant-props.ts deleted file mode 100644 index eedb5dba6d..0000000000 --- a/packages/framer-motion/src/render/utils/variant-props.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { AnimationType } from "./types" - -export const variantPriorityOrder: AnimationType[] = [ - "animate", - "whileInView", - "whileFocus", - "whileHover", - "whileTap", - "whileDrag", - "exit", -] - -export const variantProps = ["initial", ...variantPriorityOrder] diff --git a/packages/motion-dom/src/index.ts b/packages/motion-dom/src/index.ts index 3680a59fce..bd2ad27409 100644 --- a/packages/motion-dom/src/index.ts +++ b/packages/motion-dom/src/index.ts @@ -178,6 +178,7 @@ export { export * from "./projection/geometry" export { hasTransform, hasScale, has2DTranslate } from "./projection/utils/has-transform" export { measureViewportBox, measurePageBox } from "./projection/utils/measure" +export { eachAxis } from "./projection/utils/each-axis" // Projection styles export * from "./projection/styles/types" diff --git a/packages/framer-motion/src/projection/utils/each-axis.ts b/packages/motion-dom/src/projection/utils/each-axis.ts similarity index 100% rename from packages/framer-motion/src/projection/utils/each-axis.ts rename to packages/motion-dom/src/projection/utils/each-axis.ts From c8913c2af51a3c8e5282fdacf38bc8a53208c8b4 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Mon, 12 Jan 2026 19:20:42 +0100 Subject: [PATCH 03/11] Latest --- .../src/animation/animate/subject.ts | 4 +- .../src/animation/hooks/animation-controls.ts | 3 +- .../src/animation/hooks/use-animated-state.ts | 4 +- .../src/animation/interfaces/motion-value.ts | 16 +- .../src/animation/interfaces/types.ts | 3 +- .../interfaces/visual-element-target.ts | 6 +- .../interfaces/visual-element-variant.ts | 4 +- .../animation/interfaces/visual-element.ts | 4 +- .../src/animation/optimized-appear/data-id.ts | 2 +- .../src/animation/utils/calc-child-stagger.ts | 3 +- .../animation/utils/create-visual-element.ts | 13 +- .../src/context/MotionContext/index.ts | 2 +- .../src/context/MotionContext/utils.ts | 3 +- .../drag/VisualElementDragControls.ts | 2 +- .../framer-motion/src/gestures/drag/index.ts | 4 +- packages/framer-motion/src/gestures/hover.ts | 3 +- packages/framer-motion/src/gestures/press.ts | 3 +- packages/framer-motion/src/index.ts | 4 +- .../__tests__/transition-keyframes.test.tsx | 2 +- .../src/motion/features/Feature.ts | 2 +- .../src/motion/features/animation/index.ts | 20 +- .../src/motion/features/definitions.ts | 38 +- .../motion/features/layout/MeasureLayout.tsx | 3 +- .../src/motion/features/load-features.ts | 7 +- packages/framer-motion/src/motion/index.tsx | 3 +- .../src/motion/utils/use-motion-ref.ts | 2 +- .../src/motion/utils/use-visual-element.ts | 4 +- .../src/motion/utils/use-visual-state.ts | 15 +- packages/framer-motion/src/projection.ts | 4 +- .../projection/node/create-projection-node.ts | 4 +- .../src/projection/node/types.ts | 4 +- .../framer-motion/src/render/VisualElement.ts | 867 ------------------ .../src/render/dom/DOMVisualElement.ts | 61 -- .../src/render/dom/create-visual-element.ts | 3 +- .../src/render/dom/use-motion-value-child.ts | 3 +- .../dom/utils/__tests__/camel-to-dash.test.ts | 2 +- .../src/render/dom/utils/camel-to-dash.ts | 5 - .../src/render/html/HTMLVisualElement.ts | 75 -- .../src/render/html/use-props.ts | 3 +- .../html/utils/__tests__/build-styles.test.ts | 3 +- .../utils/__tests__/build-transform.test.ts | 3 +- .../src/render/html/utils/build-styles.ts | 82 -- .../src/render/html/utils/build-transform.ts | 80 -- .../src/render/html/utils/render.ts | 27 - .../render/html/utils/scrape-motion-values.ts | 3 +- .../src/render/object/ObjectVisualElement.ts | 56 -- packages/framer-motion/src/render/store.ts | 3 - .../src/render/svg/SVGVisualElement.ts | 84 -- .../framer-motion/src/render/svg/use-props.ts | 3 +- .../render/svg/utils/__tests__/path.test.ts | 2 +- .../src/render/svg/utils/build-attrs.ts | 97 -- .../src/render/svg/utils/camel-case-attrs.ts | 28 - .../src/render/svg/utils/is-svg-tag.ts | 2 - .../src/render/svg/utils/path.ts | 42 - .../src/render/svg/utils/render.ts | 22 - .../render/svg/utils/scrape-motion-values.ts | 3 +- packages/framer-motion/src/render/types.ts | 18 +- .../utils/__tests__/StateVisualElement.ts | 4 +- .../utils/__tests__/animation-state.test.ts | 3 +- .../render/utils/__tests__/variants.test.ts | 2 +- .../src/render/utils/animation-state.ts | 487 ---------- .../src/render/utils/compare-by-depth.ts | 2 +- .../src/render/utils/get-variant-context.ts | 42 - .../render/utils/is-controlling-variants.ts | 16 - .../src/render/utils/is-draggable.ts | 2 +- .../src/render/utils/motion-values.ts | 60 -- .../render/utils/resolve-dynamic-variants.ts | 34 - .../src/render/utils/resolve-variants.ts | 76 -- .../framer-motion/src/render/utils/setters.ts | 16 +- .../src/utils/get-context-window.ts | 2 +- .../reduced-motion/__tests__/index.test.tsx | 2 +- .../src/utils/reduced-motion/index.ts | 20 - .../src/utils/reduced-motion/state.ts | 8 - .../reduced-motion/use-reduced-motion.ts | 7 +- .../value/use-will-change/add-will-change.ts | 2 +- packages/motion-dom/src/index.ts | 8 +- .../src/projection/geometry/index.ts | 7 - .../motion-dom/src/projection/styles/index.ts | 5 - packages/motion-dom/src/render/types.ts | 2 + 79 files changed, 157 insertions(+), 2418 deletions(-) delete mode 100644 packages/framer-motion/src/render/VisualElement.ts delete mode 100644 packages/framer-motion/src/render/dom/DOMVisualElement.ts delete mode 100644 packages/framer-motion/src/render/dom/utils/camel-to-dash.ts delete mode 100644 packages/framer-motion/src/render/html/HTMLVisualElement.ts delete mode 100644 packages/framer-motion/src/render/html/utils/build-styles.ts delete mode 100644 packages/framer-motion/src/render/html/utils/build-transform.ts delete mode 100644 packages/framer-motion/src/render/html/utils/render.ts delete mode 100644 packages/framer-motion/src/render/object/ObjectVisualElement.ts delete mode 100644 packages/framer-motion/src/render/store.ts delete mode 100644 packages/framer-motion/src/render/svg/SVGVisualElement.ts delete mode 100644 packages/framer-motion/src/render/svg/utils/build-attrs.ts delete mode 100644 packages/framer-motion/src/render/svg/utils/camel-case-attrs.ts delete mode 100644 packages/framer-motion/src/render/svg/utils/is-svg-tag.ts delete mode 100644 packages/framer-motion/src/render/svg/utils/path.ts delete mode 100644 packages/framer-motion/src/render/svg/utils/render.ts delete mode 100644 packages/framer-motion/src/render/utils/animation-state.ts delete mode 100644 packages/framer-motion/src/render/utils/get-variant-context.ts delete mode 100644 packages/framer-motion/src/render/utils/is-controlling-variants.ts delete mode 100644 packages/framer-motion/src/render/utils/motion-values.ts delete mode 100644 packages/framer-motion/src/render/utils/resolve-dynamic-variants.ts delete mode 100644 packages/framer-motion/src/render/utils/resolve-variants.ts delete mode 100644 packages/framer-motion/src/utils/reduced-motion/index.ts delete mode 100644 packages/framer-motion/src/utils/reduced-motion/state.ts delete mode 100644 packages/motion-dom/src/projection/geometry/index.ts delete mode 100644 packages/motion-dom/src/projection/styles/index.ts diff --git a/packages/framer-motion/src/animation/animate/subject.ts b/packages/framer-motion/src/animation/animate/subject.ts index 43fd266bb1..967a1e02ab 100644 --- a/packages/framer-motion/src/animation/animate/subject.ts +++ b/packages/framer-motion/src/animation/animate/subject.ts @@ -5,14 +5,14 @@ import { DOMKeyframesDefinition, AnimationOptions as DynamicAnimationOptions, ElementOrSelector, + isMotionValue, MotionValue, TargetAndTransition, UnresolvedValueKeyframe, ValueAnimationTransition, - isMotionValue, + visualElementStore, } from "motion-dom" import { invariant } from "motion-utils" -import { visualElementStore } from "../../render/store" import { animateTarget } from "../interfaces/visual-element-target" import { ObjectTarget } from "../sequence/types" import { diff --git a/packages/framer-motion/src/animation/hooks/animation-controls.ts b/packages/framer-motion/src/animation/hooks/animation-controls.ts index 952aa61237..48e296d0a0 100644 --- a/packages/framer-motion/src/animation/hooks/animation-controls.ts +++ b/packages/framer-motion/src/animation/hooks/animation-controls.ts @@ -1,7 +1,6 @@ -import type { AnimationDefinition, LegacyAnimationControls } from "motion-dom" +import type { AnimationDefinition, LegacyAnimationControls, VisualElement } from "motion-dom" import { invariant } from "motion-utils" import { setTarget } from "../../render/utils/setters" -import type { VisualElement } from "../../render/VisualElement" import { animateVisualElement } from "../interfaces/visual-element" function stopAnimation(visualElement: VisualElement) { diff --git a/packages/framer-motion/src/animation/hooks/use-animated-state.ts b/packages/framer-motion/src/animation/hooks/use-animated-state.ts index c7d7ecbcf8..f300bb6ac2 100644 --- a/packages/framer-motion/src/animation/hooks/use-animated-state.ts +++ b/packages/framer-motion/src/animation/hooks/use-animated-state.ts @@ -1,10 +1,8 @@ "use client" -import { createBox, TargetAndTransition } from "motion-dom" +import { createBox, ResolvedValues, TargetAndTransition, VisualElement } from "motion-dom" import { useLayoutEffect, useState } from "react" import { makeUseVisualState } from "../../motion/utils/use-visual-state" -import { ResolvedValues } from "../../render/types" -import { VisualElement } from "../../render/VisualElement" import { useConstant } from "../../utils/use-constant" import { animateVisualElement } from "../interfaces/visual-element" diff --git a/packages/framer-motion/src/animation/interfaces/motion-value.ts b/packages/framer-motion/src/animation/interfaces/motion-value.ts index 28c4e6f6cb..3b6c01de9d 100644 --- a/packages/framer-motion/src/animation/interfaces/motion-value.ts +++ b/packages/framer-motion/src/animation/interfaces/motion-value.ts @@ -1,20 +1,18 @@ -import type { - AnyResolvedKeyframe, - MotionValue, - StartAnimation, - UnresolvedKeyframes, - ValueTransition, -} from "motion-dom" import { AsyncMotionValueAnimation, frame, getValueTransition, JSAnimation, makeAnimationInstant, - ValueAnimationOptions, + type AnyResolvedKeyframe, + type MotionValue, + type StartAnimation, + type UnresolvedKeyframes, + type ValueAnimationOptions, + type ValueTransition, + type VisualElement, } from "motion-dom" import { MotionGlobalConfig, secondsToMilliseconds } from "motion-utils" -import type { VisualElement } from "../../render/VisualElement" import { getFinalKeyframe } from "../animators/waapi/utils/get-final-keyframe" import { getDefaultTransition } from "../utils/default-transitions" import { isTransitionDefined } from "../utils/is-transition-defined" diff --git a/packages/framer-motion/src/animation/interfaces/types.ts b/packages/framer-motion/src/animation/interfaces/types.ts index 2644885b6c..159409c556 100644 --- a/packages/framer-motion/src/animation/interfaces/types.ts +++ b/packages/framer-motion/src/animation/interfaces/types.ts @@ -1,5 +1,4 @@ -import type { Transition } from "motion-dom" -import type { AnimationType } from "../../render/utils/types" +import type { AnimationType, Transition } from "motion-dom" export type VisualElementAnimationOptions = { delay?: number diff --git a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts index 8110c502c8..766e4c7094 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts @@ -1,13 +1,13 @@ -import type { TargetAndTransition } from "motion-dom" import { AnimationPlaybackControlsWithThen, frame, getValueTransition, positionalKeys, + type AnimationTypeState, + type TargetAndTransition, + type VisualElement, } from "motion-dom" -import type { AnimationTypeState } from "../../render/utils/animation-state" import { setTarget } from "../../render/utils/setters" -import type { VisualElement } from "../../render/VisualElement" import { addValueToWillChange } from "../../value/use-will-change/add-will-change" import { getOptimisedAppearId } from "../optimized-appear/get-appear-id" import { animateMotionValue } from "./motion-value" diff --git a/packages/framer-motion/src/animation/interfaces/visual-element-variant.ts b/packages/framer-motion/src/animation/interfaces/visual-element-variant.ts index e1e3121dee..8ab277ec2b 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-variant.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-variant.ts @@ -1,6 +1,4 @@ -import { DynamicOption } from "motion-dom" -import { resolveVariant } from "../../render/utils/resolve-dynamic-variants" -import { VisualElement } from "../../render/VisualElement" +import { DynamicOption, resolveVariant, type VisualElement } from "motion-dom" import { calcChildStagger } from "../utils/calc-child-stagger" import { VisualElementAnimationOptions } from "./types" import { animateTarget } from "./visual-element-target" diff --git a/packages/framer-motion/src/animation/interfaces/visual-element.ts b/packages/framer-motion/src/animation/interfaces/visual-element.ts index f97d9d2598..ba4edcdb45 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element.ts @@ -1,6 +1,4 @@ -import type { AnimationDefinition } from "motion-dom" -import { resolveVariant } from "../../render/utils/resolve-dynamic-variants" -import { VisualElement } from "../../render/VisualElement" +import { resolveVariant, type AnimationDefinition, type VisualElement } from "motion-dom" import { VisualElementAnimationOptions } from "./types" import { animateTarget } from "./visual-element-target" import { animateVariant } from "./visual-element-variant" diff --git a/packages/framer-motion/src/animation/optimized-appear/data-id.ts b/packages/framer-motion/src/animation/optimized-appear/data-id.ts index f9443d26b7..873535827a 100644 --- a/packages/framer-motion/src/animation/optimized-appear/data-id.ts +++ b/packages/framer-motion/src/animation/optimized-appear/data-id.ts @@ -1,4 +1,4 @@ -import { camelToDash } from "../../render/dom/utils/camel-to-dash" +import { camelToDash } from "motion-dom" export const optimizedAppearDataId = "framerAppearId" diff --git a/packages/framer-motion/src/animation/utils/calc-child-stagger.ts b/packages/framer-motion/src/animation/utils/calc-child-stagger.ts index 8ae31c363f..b7bbf48e3a 100644 --- a/packages/framer-motion/src/animation/utils/calc-child-stagger.ts +++ b/packages/framer-motion/src/animation/utils/calc-child-stagger.ts @@ -1,5 +1,4 @@ -import { DynamicOption } from "motion-dom" -import { VisualElement } from "../../render/VisualElement" +import { DynamicOption, type VisualElement } from "motion-dom" export function calcChildStagger( children: Set, diff --git a/packages/framer-motion/src/animation/utils/create-visual-element.ts b/packages/framer-motion/src/animation/utils/create-visual-element.ts index bb364c43dc..43c5b7dbd5 100644 --- a/packages/framer-motion/src/animation/utils/create-visual-element.ts +++ b/packages/framer-motion/src/animation/utils/create-visual-element.ts @@ -1,8 +1,11 @@ -import { isSVGElement, isSVGSVGElement } from "motion-dom" -import { HTMLVisualElement } from "../../render/html/HTMLVisualElement" -import { ObjectVisualElement } from "../../render/object/ObjectVisualElement" -import { visualElementStore } from "../../render/store" -import { SVGVisualElement } from "../../render/svg/SVGVisualElement" +import { + HTMLVisualElement, + isSVGElement, + isSVGSVGElement, + ObjectVisualElement, + SVGVisualElement, + visualElementStore, +} from "motion-dom" export function createDOMVisualElement(element: HTMLElement | SVGElement) { const options = { diff --git a/packages/framer-motion/src/context/MotionContext/index.ts b/packages/framer-motion/src/context/MotionContext/index.ts index 7c7710767a..14fea16efa 100644 --- a/packages/framer-motion/src/context/MotionContext/index.ts +++ b/packages/framer-motion/src/context/MotionContext/index.ts @@ -1,7 +1,7 @@ "use client" +import type { VisualElement } from "motion-dom" import { createContext } from "react" -import type { VisualElement } from "../../render/VisualElement" export interface MotionContextProps { visualElement?: VisualElement diff --git a/packages/framer-motion/src/context/MotionContext/utils.ts b/packages/framer-motion/src/context/MotionContext/utils.ts index 3fd3daee0f..398ec13363 100644 --- a/packages/framer-motion/src/context/MotionContext/utils.ts +++ b/packages/framer-motion/src/context/MotionContext/utils.ts @@ -1,7 +1,6 @@ -import { isVariantLabel } from "motion-dom" +import { isControllingVariants, isVariantLabel } from "motion-dom" import type { MotionContextProps } from "." import { MotionProps } from "../../motion/types" -import { isControllingVariants } from "../../render/utils/is-controlling-variants" export function getCurrentTreeVariants( props: MotionProps, diff --git a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts index 28800324d9..46d8a8b2fd 100644 --- a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts +++ b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts @@ -13,6 +13,7 @@ import { ResolvedConstraints, setDragLock, Transition, + type VisualElement, } from "motion-dom" import { Axis, Point, invariant } from "motion-utils" import { animateMotionValue } from "../../animation/interfaces/motion-value" @@ -21,7 +22,6 @@ import { addPointerEvent } from "../../events/add-pointer-event" import { extractEventInfo } from "../../events/event-info" import { MotionProps } from "../../motion/types" import type { LayoutUpdateData } from "../../projection/node/types" -import type { VisualElement } from "../../render/VisualElement" import { getContextWindow } from "../../utils/get-context-window" import { isRefObject } from "../../utils/is-ref-object" import { addValueToWillChange } from "../../value/use-will-change/add-will-change" diff --git a/packages/framer-motion/src/gestures/drag/index.ts b/packages/framer-motion/src/gestures/drag/index.ts index f6cfb9cc93..70df7bf50e 100644 --- a/packages/framer-motion/src/gestures/drag/index.ts +++ b/packages/framer-motion/src/gestures/drag/index.ts @@ -1,6 +1,6 @@ -import { Feature } from "../../motion/features/Feature" -import type { VisualElement } from "../../render/VisualElement" +import type { VisualElement } from "motion-dom" import { noop } from "motion-utils" +import { Feature } from "../../motion/features/Feature" import { VisualElementDragControls } from "./VisualElementDragControls" export class DragGesture extends Feature { diff --git a/packages/framer-motion/src/gestures/hover.ts b/packages/framer-motion/src/gestures/hover.ts index a8ddd4f06e..38c93d1fe4 100644 --- a/packages/framer-motion/src/gestures/hover.ts +++ b/packages/framer-motion/src/gestures/hover.ts @@ -1,7 +1,6 @@ -import { frame, hover } from "motion-dom" +import { frame, hover, type VisualElement } from "motion-dom" import { extractEventInfo } from "../events/event-info" import { Feature } from "../motion/features/Feature" -import type { VisualElement } from "../render/VisualElement" function handleHoverEvent( node: VisualElement, diff --git a/packages/framer-motion/src/gestures/press.ts b/packages/framer-motion/src/gestures/press.ts index dce7d1be19..729f6356de 100644 --- a/packages/framer-motion/src/gestures/press.ts +++ b/packages/framer-motion/src/gestures/press.ts @@ -1,7 +1,6 @@ -import { frame, press } from "motion-dom" +import { frame, press, type VisualElement } from "motion-dom" import { extractEventInfo } from "../events/event-info" import { Feature } from "../motion/features/Feature" -import { VisualElement } from "../render/VisualElement" function handlePressEvent( node: VisualElement, diff --git a/packages/framer-motion/src/index.ts b/packages/framer-motion/src/index.ts index 2f06db194f..cf74650801 100644 --- a/packages/framer-motion/src/index.ts +++ b/packages/framer-motion/src/index.ts @@ -91,9 +91,7 @@ export { isValidMotionProp } from "./motion/utils/valid-prop" export { addScaleCorrectors as addScaleCorrector } from "motion-dom" export { useInstantLayoutTransition } from "./projection/use-instant-layout-transition" export { useResetProjection } from "./projection/use-reset-projection" -export { buildTransform } from "./render/html/utils/build-transform" -export { visualElementStore } from "./render/store" -export { VisualElement } from "./render/VisualElement" +export { buildTransform, visualElementStore, VisualElement } from "motion-dom" export { useAnimationFrame } from "./utils/use-animation-frame" export { Cycle, CycleState, useCycle } from "./utils/use-cycle" export { useInView, UseInViewOptions } from "./utils/use-in-view" diff --git a/packages/framer-motion/src/motion/__tests__/transition-keyframes.test.tsx b/packages/framer-motion/src/motion/__tests__/transition-keyframes.test.tsx index ff5d6cc6f8..83b7e46910 100644 --- a/packages/framer-motion/src/motion/__tests__/transition-keyframes.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/transition-keyframes.test.tsx @@ -1,6 +1,6 @@ +import { checkVariantsDidChange } from "motion-dom" import { motion, motionValue } from "../.." import { render } from "../../jest.setup" -import { checkVariantsDidChange } from "../../render/utils/animation-state" describe("keyframes transition", () => { test("keyframes as target", async () => { diff --git a/packages/framer-motion/src/motion/features/Feature.ts b/packages/framer-motion/src/motion/features/Feature.ts index bf5b64222a..83a0152f81 100644 --- a/packages/framer-motion/src/motion/features/Feature.ts +++ b/packages/framer-motion/src/motion/features/Feature.ts @@ -1,4 +1,4 @@ -import type { VisualElement } from "../../render/VisualElement" +import type { VisualElement } from "motion-dom" export abstract class Feature { isMounted = false diff --git a/packages/framer-motion/src/motion/features/animation/index.ts b/packages/framer-motion/src/motion/features/animation/index.ts index bc05d57774..e5c5386446 100644 --- a/packages/framer-motion/src/motion/features/animation/index.ts +++ b/packages/framer-motion/src/motion/features/animation/index.ts @@ -1,8 +1,22 @@ +import { createAnimationState, type VisualElement } from "motion-dom" import { isAnimationControls } from "../../../animation/utils/is-animation-controls" -import { createAnimationState } from "../../../render/utils/animation-state" -import { VisualElement } from "../../../render/VisualElement" +import { animateVisualElement } from "../../../animation/interfaces/visual-element" import { Feature } from "../Feature" +/** + * Creates the animate function that will be used by the animation state + * to perform actual animations using framer-motion's animation system. + */ +function makeAnimateFunction(visualElement: VisualElement) { + return (animations: Array<{ animation: any; options?: any }>) => { + return Promise.all( + animations.map(({ animation, options }) => + animateVisualElement(visualElement, animation, options) + ) + ) + } +} + export class AnimationFeature extends Feature { unmountControls?: () => void @@ -13,7 +27,7 @@ export class AnimationFeature extends Feature { */ constructor(node: VisualElement) { super(node) - node.animationState ||= createAnimationState(node) + node.animationState ||= createAnimationState(node, makeAnimateFunction) } updateAnimationControlsSubscription() { diff --git a/packages/framer-motion/src/motion/features/definitions.ts b/packages/framer-motion/src/motion/features/definitions.ts index 50e587d3b7..61c122ce93 100644 --- a/packages/framer-motion/src/motion/features/definitions.ts +++ b/packages/framer-motion/src/motion/features/definitions.ts @@ -1,3 +1,4 @@ +import { getFeatureDefinitions, setFeatureDefinitions } from "motion-dom" import { MotionProps } from "../types" import { FeatureDefinitions } from "./types" @@ -22,13 +23,36 @@ const featureProps = { layout: ["layout", "layoutId"], } -export const featureDefinitions: Partial = {} +let isInitialized = false -for (const key in featureProps) { - featureDefinitions[key as keyof typeof featureDefinitions] = { - isEnabled: (props: MotionProps) => - featureProps[key as keyof typeof featureProps].some( - (name: string) => !!props[name as keyof typeof props] - ), +/** + * Initialize feature definitions with isEnabled checks. + * This must be called before any motion components are rendered. + */ +export function initFeatureDefinitions() { + if (isInitialized) return + + const initialFeatureDefinitions: Partial = {} + + for (const key in featureProps) { + initialFeatureDefinitions[ + key as keyof typeof initialFeatureDefinitions + ] = { + isEnabled: (props: MotionProps) => + featureProps[key as keyof typeof featureProps].some( + (name: string) => !!props[name as keyof typeof props] + ), + } } + + setFeatureDefinitions(initialFeatureDefinitions) + isInitialized = true +} + +/** + * Get the current feature definitions, initializing if needed. + */ +export function getInitializedFeatureDefinitions(): Partial { + initFeatureDefinitions() + return getFeatureDefinitions() } diff --git a/packages/framer-motion/src/motion/features/layout/MeasureLayout.tsx b/packages/framer-motion/src/motion/features/layout/MeasureLayout.tsx index 74426ddeda..eafe0f45ff 100644 --- a/packages/framer-motion/src/motion/features/layout/MeasureLayout.tsx +++ b/packages/framer-motion/src/motion/features/layout/MeasureLayout.tsx @@ -1,6 +1,6 @@ "use client" -import { frame, microtask } from "motion-dom" +import { frame, microtask, type VisualElement } from "motion-dom" import { Component, useContext } from "react" import { usePresence } from "../../../components/AnimatePresence/use-presence" import { @@ -9,7 +9,6 @@ import { } from "../../../context/LayoutGroupContext" import { SwitchLayoutGroupContext } from "../../../context/SwitchLayoutGroupContext" import { globalProjectionState } from "../../../projection/node/state" -import { VisualElement } from "../../../render/VisualElement" import { MotionProps } from "../../types" interface MeasureContextProps { diff --git a/packages/framer-motion/src/motion/features/load-features.ts b/packages/framer-motion/src/motion/features/load-features.ts index 4810a844cf..fd2147ad90 100644 --- a/packages/framer-motion/src/motion/features/load-features.ts +++ b/packages/framer-motion/src/motion/features/load-features.ts @@ -1,11 +1,16 @@ -import { featureDefinitions } from "./definitions" +import { setFeatureDefinitions } from "motion-dom" +import { getInitializedFeatureDefinitions } from "./definitions" import { FeaturePackages } from "./types" export function loadFeatures(features: FeaturePackages) { + const featureDefinitions = getInitializedFeatureDefinitions() + for (const key in features) { featureDefinitions[key as keyof typeof featureDefinitions] = { ...featureDefinitions[key as keyof typeof featureDefinitions], ...features[key as keyof typeof features], } as any } + + setFeatureDefinitions(featureDefinitions) } diff --git a/packages/framer-motion/src/motion/index.tsx b/packages/framer-motion/src/motion/index.tsx index b5737cc6b2..ebef5b2475 100644 --- a/packages/framer-motion/src/motion/index.tsx +++ b/packages/framer-motion/src/motion/index.tsx @@ -17,7 +17,7 @@ import { SVGRenderState } from "../render/svg/types" import { useSVGVisualState } from "../render/svg/use-svg-visual-state" import { CreateVisualElement } from "../render/types" import { isBrowser } from "../utils/is-browser" -import { featureDefinitions } from "./features/definitions" +import { getInitializedFeatureDefinitions } from "./features/definitions" import { loadFeatures } from "./features/load-features" import { FeatureBundle, FeaturePackages } from "./features/types" import { MotionProps } from "./types" @@ -203,6 +203,7 @@ function useStrictMode( } function getProjectionFunctionality(props: MotionProps) { + const featureDefinitions = getInitializedFeatureDefinitions() const { drag, layout } = featureDefinitions if (!drag && !layout) return {} diff --git a/packages/framer-motion/src/motion/utils/use-motion-ref.ts b/packages/framer-motion/src/motion/utils/use-motion-ref.ts index 8ed2b46601..5606047551 100644 --- a/packages/framer-motion/src/motion/utils/use-motion-ref.ts +++ b/packages/framer-motion/src/motion/utils/use-motion-ref.ts @@ -1,8 +1,8 @@ "use client" +import type { VisualElement } from "motion-dom" import * as React from "react" import { useCallback, useInsertionEffect, useRef } from "react" -import type { VisualElement } from "../../render/VisualElement" import { VisualState } from "./use-visual-state" /** diff --git a/packages/framer-motion/src/motion/utils/use-visual-element.ts b/packages/framer-motion/src/motion/utils/use-visual-element.ts index 7f009c1542..fa980eb2bf 100644 --- a/packages/framer-motion/src/motion/utils/use-visual-element.ts +++ b/packages/framer-motion/src/motion/utils/use-visual-element.ts @@ -1,5 +1,6 @@ "use client" +import type { HTMLRenderState, SVGRenderState, VisualElement } from "motion-dom" import * as React from "react" import { useContext, useEffect, useInsertionEffect, useRef } from "react" import { optimizedAppearDataAttribute } from "../../animation/optimized-appear/data-id" @@ -14,10 +15,7 @@ import { import { MotionProps } from "../../motion/types" import { IProjectionNode } from "../../projection/node/types" import { DOMMotionComponents } from "../../render/dom/types" -import { HTMLRenderState } from "../../render/html/types" -import { SVGRenderState } from "../../render/svg/types" import { CreateVisualElement } from "../../render/types" -import type { VisualElement } from "../../render/VisualElement" import { isRefObject } from "../../utils/is-ref-object" import { useIsomorphicLayoutEffect } from "../../utils/use-isomorphic-effect" import { VisualState } from "./use-visual-state" diff --git a/packages/framer-motion/src/motion/utils/use-visual-state.ts b/packages/framer-motion/src/motion/utils/use-visual-state.ts index 81a5d5fd77..7fd3356cab 100644 --- a/packages/framer-motion/src/motion/utils/use-visual-state.ts +++ b/packages/framer-motion/src/motion/utils/use-visual-state.ts @@ -1,6 +1,12 @@ "use client" -import { AnyResolvedKeyframe } from "motion-dom" +import { + AnyResolvedKeyframe, + isControllingVariants as checkIsControllingVariants, + isVariantNode as checkIsVariantNode, + ResolvedValues, + resolveVariantFromProps, +} from "motion-dom" import { useContext } from "react" import { isAnimationControls } from "../../animation/utils/is-animation-controls" import { MotionContext, MotionContextProps } from "../../context/MotionContext" @@ -8,12 +14,7 @@ import { PresenceContext, type PresenceContextProps, } from "../../context/PresenceContext" -import { ResolvedValues, ScrapeMotionValuesFromProps } from "../../render/types" -import { - isControllingVariants as checkIsControllingVariants, - isVariantNode as checkIsVariantNode, -} from "../../render/utils/is-controlling-variants" -import { resolveVariantFromProps } from "../../render/utils/resolve-variants" +import { ScrapeMotionValuesFromProps } from "../../render/types" import { useConstant } from "../../utils/use-constant" import { resolveMotionValue } from "../../value/utils/resolve-motion-value" import { MotionProps } from "../types" diff --git a/packages/framer-motion/src/projection.ts b/packages/framer-motion/src/projection.ts index c9edc1c38d..fd49f5ff2e 100644 --- a/packages/framer-motion/src/projection.ts +++ b/packages/framer-motion/src/projection.ts @@ -8,8 +8,8 @@ export { frame, frameData, mix, + HTMLVisualElement, + buildTransform, } from "motion-dom" export { nodeGroup } from "./projection/node/group" export { HTMLProjectionNode } from "./projection/node/HTMLProjectionNode" -export { HTMLVisualElement } from "./render/html/HTMLVisualElement" -export { buildTransform } from "./render/html/utils/build-transform" diff --git a/packages/framer-motion/src/projection/node/create-projection-node.ts b/packages/framer-motion/src/projection/node/create-projection-node.ts index 66b321d353..256dd515b2 100644 --- a/packages/framer-motion/src/projection/node/create-projection-node.ts +++ b/packages/framer-motion/src/projection/node/create-projection-node.ts @@ -57,10 +57,8 @@ import { import { animateSingleValue } from "../../animation/animate/single-value" import { getOptimisedAppearId } from "../../animation/optimized-appear/get-appear-id" import { MotionStyle } from "../../motion/types" -import { HTMLVisualElement } from "../../projection" -import { ResolvedValues } from "../../render/types" +import { HTMLVisualElement, ResolvedValues, VisualElement } from "motion-dom" import { FlatTree } from "../../render/utils/flat-tree" -import { VisualElement } from "../../render/VisualElement" import { delay } from "../../utils/delay" import { resolveMotionValue } from "../../value/utils/resolve-motion-value" import { NodeStack } from "../shared/stack" diff --git a/packages/framer-motion/src/projection/node/types.ts b/packages/framer-motion/src/projection/node/types.ts index b01bb2abf0..2e8e23de81 100644 --- a/packages/framer-motion/src/projection/node/types.ts +++ b/packages/framer-motion/src/projection/node/types.ts @@ -1,10 +1,8 @@ -import type { JSAnimation, Transition, ValueTransition } from "motion-dom" +import type { JSAnimation, ResolvedValues, Transition, ValueTransition, VisualElement } from "motion-dom" import { Box, Delta, Point } from "motion-utils" import { InitialPromotionConfig } from "../../context/SwitchLayoutGroupContext" import { MotionStyle } from "../../motion/types" -import { ResolvedValues } from "../../render/types" import { FlatTree } from "../../render/utils/flat-tree" -import type { VisualElement } from "../../render/VisualElement" import { NodeStack } from "../shared/stack" export interface Measurements { diff --git a/packages/framer-motion/src/render/VisualElement.ts b/packages/framer-motion/src/render/VisualElement.ts deleted file mode 100644 index 4908de9ebc..0000000000 --- a/packages/framer-motion/src/render/VisualElement.ts +++ /dev/null @@ -1,867 +0,0 @@ -import { - cancelFrame, - complex, - createBox, - findValueType, - frame, - getAnimatableNone, - isMotionValue, - KeyframeResolver, - microtask, - motionValue, - time, - transformProps, - type AnyResolvedKeyframe, - type MotionValue, -} from "motion-dom" -import type { Box } from "motion-utils" -import { - isNumericalString, - isZeroValueString, - SubscriptionManager, - warnOnce, -} from "motion-utils" -import { - MotionConfigContext, - ReducedMotionConfig, -} from "../context/MotionConfigContext" -import type { PresenceContextProps } from "../context/PresenceContext" -import { featureDefinitions } from "../motion/features/definitions" -import { Feature } from "../motion/features/Feature" -import { FeatureDefinitions } from "../motion/features/types" -import { MotionProps, MotionStyle } from "../motion/types" -import { IProjectionNode } from "../projection/node/types" -import { initPrefersReducedMotion } from "../utils/reduced-motion" -import { - hasReducedMotionListener, - prefersReducedMotion, -} from "../utils/reduced-motion/state" -import { visualElementStore } from "./store" -import { - ResolvedValues, - VisualElementEventCallbacks, - VisualElementOptions, -} from "./types" -import { AnimationState } from "./utils/animation-state" -import { - isControllingVariants as checkIsControllingVariants, - isVariantNode as checkIsVariantNode, -} from "./utils/is-controlling-variants" -import { updateMotionValuesFromProps } from "./utils/motion-values" -import { resolveVariantFromProps } from "./utils/resolve-variants" - -const propEventHandlers = [ - "AnimationStart", - "AnimationComplete", - "Update", - "BeforeLayoutMeasure", - "LayoutMeasure", - "LayoutAnimationStart", - "LayoutAnimationComplete", -] as const - -/** - * A VisualElement is an imperative abstraction around UI elements such as - * HTMLElement, SVGElement, Three.Object3D etc. - */ -export abstract class VisualElement< - Instance = unknown, - RenderState = unknown, - Options extends {} = {} -> { - /** - * VisualElements are arranged in trees mirroring that of the React tree. - * Each type of VisualElement has a unique name, to detect when we're crossing - * type boundaries within that tree. - */ - abstract type: string - - /** - * An `Array.sort` compatible function that will compare two Instances and - * compare their respective positions within the tree. - */ - abstract sortInstanceNodePosition(a: Instance, b: Instance): number - - /** - * Measure the viewport-relative bounding box of the Instance. - */ - abstract measureInstanceViewportBox( - instance: Instance, - props: MotionProps & Partial - ): Box - - /** - * When a value has been removed from all animation props we need to - * pick a target to animate back to. For instance, for HTMLElements - * we can look in the style prop. - */ - abstract getBaseTargetFromProps( - props: MotionProps, - key: string - ): AnyResolvedKeyframe | undefined | MotionValue - - /** - * When we first animate to a value we need to animate it *from* a value. - * Often this have been specified via the initial prop but it might be - * that the value needs to be read from the Instance. - */ - abstract readValueFromInstance( - instance: Instance, - key: string, - options: Options - ): AnyResolvedKeyframe | null | undefined - - /** - * When a value has been removed from the VisualElement we use this to remove - * it from the inherting class' unique render state. - */ - abstract removeValueFromRenderState( - key: string, - renderState: RenderState - ): void - - /** - * Run before a React or VisualElement render, builds the latest motion - * values into an Instance-specific format. For example, HTMLVisualElement - * will use this step to build `style` and `var` values. - */ - abstract build( - renderState: RenderState, - latestValues: ResolvedValues, - props: MotionProps - ): void - - /** - * Apply the built values to the Instance. For example, HTMLElements will have - * styles applied via `setProperty` and the style attribute, whereas SVGElements - * will have values applied to attributes. - */ - abstract renderInstance( - instance: Instance, - renderState: RenderState, - styleProp?: MotionStyle, - projection?: IProjectionNode - ): void - - /** - * This method is called when a transform property is bound to a motion value. - * It's currently used to measure SVG elements when a new transform property is bound. - */ - onBindTransform?(): void - - /** - * If the component child is provided as a motion value, handle subscriptions - * with the renderer-specific VisualElement. - */ - handleChildMotionValue?(): void - - /** - * This method takes React props and returns found MotionValues. For example, HTML - * MotionValues will be found within the style prop, whereas for Three.js within attribute arrays. - * - * This isn't an abstract method as it needs calling in the constructor, but it is - * intended to be one. - */ - scrapeMotionValuesFromProps( - _props: MotionProps, - _prevProps: MotionProps, - _visualElement: VisualElement - ): { - [key: string]: MotionValue | AnyResolvedKeyframe - } { - return {} - } - - /** - * A reference to the current underlying Instance, e.g. a HTMLElement - * or Three.Mesh etc. - */ - current: Instance | null = null - - /** - * A reference to the parent VisualElement (if exists). - */ - parent: VisualElement | undefined - - /** - * A set containing references to this VisualElement's children. - */ - children = new Set() - - /** - * A set containing the latest children of this VisualElement. This is flushed - * at the start of every commit. We use it to calculate the stagger delay - * for newly-added children. - */ - enteringChildren?: Set - - /** - * The depth of this VisualElement within the overall VisualElement tree. - */ - depth: number - - /** - * The current render state of this VisualElement. Defined by inherting VisualElements. - */ - renderState: RenderState - - /** - * An object containing the latest static values for each of this VisualElement's - * MotionValues. - */ - latestValues: ResolvedValues - - /** - * Determine what role this visual element should take in the variant tree. - */ - isVariantNode: boolean = false - isControllingVariants: boolean = false - - /** - * If this component is part of the variant tree, it should track - * any children that are also part of the tree. This is essentially - * a shadow tree to simplify logic around how to stagger over children. - */ - variantChildren?: Set - - /** - * Decides whether this VisualElement should animate in reduced motion - * mode. - * - * TODO: This is currently set on every individual VisualElement but feels - * like it could be set globally. - */ - shouldReduceMotion: boolean | null = null - - /** - * Normally, if a component is controlled by a parent's variants, it can - * rely on that ancestor to trigger animations further down the tree. - * However, if a component is created after its parent is mounted, the parent - * won't trigger that mount animation so the child needs to. - * - * TODO: This might be better replaced with a method isParentMounted - */ - manuallyAnimateOnMount: boolean - - /** - * This can be set by AnimatePresence to force components that mount - * at the same time as it to mount as if they have initial={false} set. - */ - blockInitialAnimation: boolean - - /** - * A reference to this VisualElement's projection node, used in layout animations. - */ - projection?: IProjectionNode - - /** - * A map of all motion values attached to this visual element. Motion - * values are source of truth for any given animated value. A motion - * value might be provided externally by the component via props. - */ - values = new Map() - - /** - * The AnimationState, this is hydrated by the animation Feature. - */ - animationState?: AnimationState - - KeyframeResolver = KeyframeResolver - - /** - * The options used to create this VisualElement. The Options type is defined - * by the inheriting VisualElement and is passed straight through to the render functions. - */ - readonly options: Options - - /** - * A reference to the latest props provided to the VisualElement's host React component. - */ - props: MotionProps - prevProps?: MotionProps - - presenceContext: PresenceContextProps | null - prevPresenceContext?: PresenceContextProps | null - - /** - * Cleanup functions for active features (hover/tap/exit etc) - */ - private features: { - [K in keyof FeatureDefinitions]?: Feature - } = {} - - /** - * A map of every subscription that binds the provided or generated - * motion values onChange listeners to this visual element. - */ - private valueSubscriptions = new Map() - - /** - * A reference to the ReducedMotionConfig passed to the VisualElement's host React component. - */ - private reducedMotionConfig: ReducedMotionConfig | undefined - - /** - * On mount, this will be hydrated with a callback to disconnect - * this visual element from its parent on unmount. - */ - private removeFromVariantTree: undefined | VoidFunction - - /** - * A reference to the previously-provided motion values as returned - * from scrapeMotionValuesFromProps. We use the keys in here to determine - * if any motion values need to be removed after props are updated. - */ - private prevMotionValues: MotionStyle = {} - - /** - * When values are removed from all animation props we need to search - * for a fallback value to animate to. These values are tracked in baseTarget. - */ - private baseTarget: ResolvedValues - - /** - * Create an object of the values we initially animated from (if initial prop present). - */ - private initialValues: ResolvedValues - - /** - * An object containing a SubscriptionManager for each active event. - */ - private events: { - [key: string]: SubscriptionManager - } = {} - - /** - * An object containing an unsubscribe function for each prop event subscription. - * For example, every "Update" event can have multiple subscribers via - * VisualElement.on(), but only one of those can be defined via the onUpdate prop. - */ - private propEventSubscriptions: { - [key: string]: VoidFunction - } = {} - - constructor( - { - parent, - props, - presenceContext, - reducedMotionConfig, - blockInitialAnimation, - visualState, - }: VisualElementOptions, - options: Options = {} as any - ) { - const { latestValues, renderState } = visualState - this.latestValues = latestValues - this.baseTarget = { ...latestValues } - this.initialValues = props.initial ? { ...latestValues } : {} - this.renderState = renderState - this.parent = parent - this.props = props - this.presenceContext = presenceContext - this.depth = parent ? parent.depth + 1 : 0 - this.reducedMotionConfig = reducedMotionConfig - this.options = options - this.blockInitialAnimation = Boolean(blockInitialAnimation) - - this.isControllingVariants = checkIsControllingVariants(props) - this.isVariantNode = checkIsVariantNode(props) - if (this.isVariantNode) { - this.variantChildren = new Set() - } - - this.manuallyAnimateOnMount = Boolean(parent && parent.current) - - /** - * Any motion values that are provided to the element when created - * aren't yet bound to the element, as this would technically be impure. - * However, we iterate through the motion values and set them to the - * initial values for this component. - * - * TODO: This is impure and we should look at changing this to run on mount. - * Doing so will break some tests but this isn't necessarily a breaking change, - * more a reflection of the test. - */ - const { willChange, ...initialMotionValues } = - this.scrapeMotionValuesFromProps(props, {}, this) - - for (const key in initialMotionValues) { - const value = initialMotionValues[key] - - if (latestValues[key] !== undefined && isMotionValue(value)) { - value.set(latestValues[key]) - } - } - } - - mount(instance: Instance) { - this.current = instance - - visualElementStore.set(instance, this) - - if (this.projection && !this.projection.instance) { - this.projection.mount(instance) - } - - if (this.parent && this.isVariantNode && !this.isControllingVariants) { - this.removeFromVariantTree = this.parent.addVariantChild(this) - } - - this.values.forEach((value, key) => this.bindToMotionValue(key, value)) - - /** - * Determine reduced motion preference. Only initialize the matchMedia - * listener if we actually need the dynamic value (i.e., when config - * is neither "never" nor "always"). - */ - if (this.reducedMotionConfig === "never") { - this.shouldReduceMotion = false - } else if (this.reducedMotionConfig === "always") { - this.shouldReduceMotion = true - } else { - if (!hasReducedMotionListener.current) { - initPrefersReducedMotion() - } - this.shouldReduceMotion = prefersReducedMotion.current - } - - if (process.env.NODE_ENV !== "production") { - warnOnce( - this.shouldReduceMotion !== true, - "You have Reduced Motion enabled on your device. Animations may not appear as expected.", - "reduced-motion-disabled" - ) - } - - this.parent?.addChild(this) - - this.update(this.props, this.presenceContext) - } - - unmount() { - this.projection && this.projection.unmount() - cancelFrame(this.notifyUpdate) - cancelFrame(this.render) - this.valueSubscriptions.forEach((remove) => remove()) - this.valueSubscriptions.clear() - this.removeFromVariantTree && this.removeFromVariantTree() - this.parent?.removeChild(this) - - for (const key in this.events) { - this.events[key].clear() - } - - for (const key in this.features) { - const feature = this.features[key as keyof typeof this.features] - if (feature) { - feature.unmount() - feature.isMounted = false - } - } - this.current = null - } - - addChild(child: VisualElement) { - this.children.add(child) - this.enteringChildren ??= new Set() - this.enteringChildren.add(child) - } - - removeChild(child: VisualElement) { - this.children.delete(child) - this.enteringChildren && this.enteringChildren.delete(child) - } - - private bindToMotionValue(key: string, value: MotionValue) { - if (this.valueSubscriptions.has(key)) { - this.valueSubscriptions.get(key)!() - } - - const valueIsTransform = transformProps.has(key) - - if (valueIsTransform && this.onBindTransform) { - this.onBindTransform() - } - - const removeOnChange = value.on( - "change", - (latestValue: AnyResolvedKeyframe) => { - this.latestValues[key] = latestValue - - this.props.onUpdate && frame.preRender(this.notifyUpdate) - - if (valueIsTransform && this.projection) { - this.projection.isTransformDirty = true - } - - this.scheduleRender() - } - ) - - let removeSyncCheck: VoidFunction | void - if (window.MotionCheckAppearSync) { - removeSyncCheck = window.MotionCheckAppearSync(this, key, value) - } - - this.valueSubscriptions.set(key, () => { - removeOnChange() - if (removeSyncCheck) removeSyncCheck() - if (value.owner) value.stop() - }) - } - - sortNodePosition(other: VisualElement) { - /** - * If these nodes aren't even of the same type we can't compare their depth. - */ - if ( - !this.current || - !this.sortInstanceNodePosition || - this.type !== other.type - ) { - return 0 - } - - return this.sortInstanceNodePosition( - this.current as Instance, - other.current as Instance - ) - } - - updateFeatures() { - let key: keyof typeof featureDefinitions = "animation" - - for (key in featureDefinitions) { - const featureDefinition = featureDefinitions[key] - - if (!featureDefinition) continue - - const { isEnabled, Feature: FeatureConstructor } = featureDefinition - - /** - * If this feature is enabled but not active, make a new instance. - */ - if ( - !this.features[key] && - FeatureConstructor && - isEnabled(this.props) - ) { - this.features[key] = new FeatureConstructor(this) as any - } - - /** - * If we have a feature, mount or update it. - */ - if (this.features[key]) { - const feature = this.features[key]! - if (feature.isMounted) { - feature.update() - } else { - feature.mount() - feature.isMounted = true - } - } - } - } - - notifyUpdate = () => this.notify("Update", this.latestValues) - - triggerBuild() { - this.build(this.renderState, this.latestValues, this.props) - } - - render = () => { - if (!this.current) return - this.triggerBuild() - this.renderInstance( - this.current, - this.renderState, - this.props.style, - this.projection - ) - } - - private renderScheduledAt = 0.0 - scheduleRender = () => { - const now = time.now() - if (this.renderScheduledAt < now) { - this.renderScheduledAt = now - frame.render(this.render, false, true) - } - } - - /** - * Measure the current viewport box with or without transforms. - * Only measures axis-aligned boxes, rotate and skew must be manually - * removed with a re-render to work. - */ - measureViewportBox() { - return this.current - ? this.measureInstanceViewportBox(this.current, this.props) - : createBox() - } - - getStaticValue(key: string) { - return this.latestValues[key] - } - - setStaticValue(key: string, value: AnyResolvedKeyframe) { - this.latestValues[key] = value - } - - /** - * Update the provided props. Ensure any newly-added motion values are - * added to our map, old ones removed, and listeners updated. - */ - update(props: MotionProps, presenceContext: PresenceContextProps | null) { - if (props.transformTemplate || this.props.transformTemplate) { - this.scheduleRender() - } - - this.prevProps = this.props - this.props = props - - this.prevPresenceContext = this.presenceContext - this.presenceContext = presenceContext - - /** - * Update prop event handlers ie onAnimationStart, onAnimationComplete - */ - for (let i = 0; i < propEventHandlers.length; i++) { - const key = propEventHandlers[i] - if (this.propEventSubscriptions[key]) { - this.propEventSubscriptions[key]() - delete this.propEventSubscriptions[key] - } - - const listenerName = ("on" + key) as keyof typeof props - const listener = props[listenerName] - if (listener) { - this.propEventSubscriptions[key] = this.on(key as any, listener) - } - } - - this.prevMotionValues = updateMotionValuesFromProps( - this, - this.scrapeMotionValuesFromProps(props, this.prevProps, this), - this.prevMotionValues - ) - - if (this.handleChildMotionValue) { - this.handleChildMotionValue() - } - } - - getProps() { - return this.props - } - - /** - * Returns the variant definition with a given name. - */ - getVariant(name: string) { - return this.props.variants ? this.props.variants[name] : undefined - } - - /** - * Returns the defined default transition on this component. - */ - getDefaultTransition() { - return this.props.transition - } - - getTransformPagePoint() { - return (this.props as any).transformPagePoint - } - - getClosestVariantNode(): VisualElement | undefined { - return this.isVariantNode - ? this - : this.parent - ? this.parent.getClosestVariantNode() - : undefined - } - - /** - * Add a child visual element to our set of children. - */ - addVariantChild(child: VisualElement) { - const closestVariantNode = this.getClosestVariantNode() - if (closestVariantNode) { - closestVariantNode.variantChildren && - closestVariantNode.variantChildren.add(child) - return () => closestVariantNode.variantChildren!.delete(child) - } - } - - /** - * Add a motion value and bind it to this visual element. - */ - addValue(key: string, value: MotionValue) { - // Remove existing value if it exists - const existingValue = this.values.get(key) - - if (value !== existingValue) { - if (existingValue) this.removeValue(key) - this.bindToMotionValue(key, value) - this.values.set(key, value) - this.latestValues[key] = value.get() - } - } - - /** - * Remove a motion value and unbind any active subscriptions. - */ - removeValue(key: string) { - this.values.delete(key) - const unsubscribe = this.valueSubscriptions.get(key) - if (unsubscribe) { - unsubscribe() - this.valueSubscriptions.delete(key) - } - delete this.latestValues[key] - this.removeValueFromRenderState(key, this.renderState) - } - - /** - * Check whether we have a motion value for this key - */ - hasValue(key: string) { - return this.values.has(key) - } - - /** - * Get a motion value for this key. If called with a default - * value, we'll create one if none exists. - */ - getValue(key: string): MotionValue | undefined - getValue(key: string, defaultValue: AnyResolvedKeyframe | null): MotionValue - getValue( - key: string, - defaultValue?: AnyResolvedKeyframe | null - ): MotionValue | undefined { - if (this.props.values && this.props.values[key]) { - return this.props.values[key] - } - - let value = this.values.get(key) - - if (value === undefined && defaultValue !== undefined) { - value = motionValue( - defaultValue === null ? undefined : defaultValue, - { owner: this } - ) - this.addValue(key, value) - } - - return value - } - - /** - * If we're trying to animate to a previously unencountered value, - * we need to check for it in our state and as a last resort read it - * directly from the instance (which might have performance implications). - */ - readValue(key: string, target?: AnyResolvedKeyframe | null) { - let value = - this.latestValues[key] !== undefined || !this.current - ? this.latestValues[key] - : this.getBaseTargetFromProps(this.props, key) ?? - this.readValueFromInstance(this.current, key, this.options) - - if (value !== undefined && value !== null) { - if ( - typeof value === "string" && - (isNumericalString(value) || isZeroValueString(value)) - ) { - // If this is a number read as a string, ie "0" or "200", convert it to a number - value = parseFloat(value) - } else if (!findValueType(value) && complex.test(target)) { - value = getAnimatableNone(key, target as string) - } - - this.setBaseTarget(key, isMotionValue(value) ? value.get() : value) - } - - return isMotionValue(value) ? value.get() : value - } - - /** - * Set the base target to later animate back to. This is currently - * only hydrated on creation and when we first read a value. - */ - setBaseTarget(key: string, value: AnyResolvedKeyframe) { - this.baseTarget[key] = value - } - - /** - * Find the base target for a value thats been removed from all animation - * props. - */ - getBaseTarget(key: string): ResolvedValues[string] | undefined | null { - const { initial } = this.props - - let valueFromInitial: ResolvedValues[string] | undefined | null - - if (typeof initial === "string" || typeof initial === "object") { - const variant = resolveVariantFromProps( - this.props, - initial as any, - this.presenceContext?.custom - ) - if (variant) { - valueFromInitial = variant[ - key as keyof typeof variant - ] as string - } - } - - /** - * If this value still exists in the current initial variant, read that. - */ - if (initial && valueFromInitial !== undefined) { - return valueFromInitial - } - - /** - * Alternatively, if this VisualElement config has defined a getBaseTarget - * so we can read the value from an alternative source, try that. - */ - const target = this.getBaseTargetFromProps(this.props, key) - if (target !== undefined && !isMotionValue(target)) return target - - /** - * If the value was initially defined on initial, but it doesn't any more, - * return undefined. Otherwise return the value as initially read from the DOM. - */ - return this.initialValues[key] !== undefined && - valueFromInitial === undefined - ? undefined - : this.baseTarget[key] - } - - on( - eventName: EventName, - callback: VisualElementEventCallbacks[EventName] - ) { - if (!this.events[eventName]) { - this.events[eventName] = new SubscriptionManager() - } - - return this.events[eventName].add(callback) - } - - notify( - eventName: EventName, - ...args: any - ) { - if (this.events[eventName]) { - this.events[eventName].notify(...args) - } - } - - scheduleRenderMicrotask() { - microtask.render(this.render) - } -} diff --git a/packages/framer-motion/src/render/dom/DOMVisualElement.ts b/packages/framer-motion/src/render/dom/DOMVisualElement.ts deleted file mode 100644 index 627dd07fcb..0000000000 --- a/packages/framer-motion/src/render/dom/DOMVisualElement.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - AnyResolvedKeyframe, - DOMKeyframesResolver, - isMotionValue, - MotionValue, -} from "motion-dom" -import { MotionProps, MotionStyle } from "../../motion/types" -import { DOMVisualElementOptions } from "../dom/types" -import { HTMLRenderState } from "../html/types" -import { VisualElement } from "../VisualElement" - -export abstract class DOMVisualElement< - Instance extends HTMLElement | SVGElement = HTMLElement, - State extends HTMLRenderState = HTMLRenderState, - Options extends DOMVisualElementOptions = DOMVisualElementOptions -> extends VisualElement { - sortInstanceNodePosition(a: Instance, b: Instance): number { - /** - * compareDocumentPosition returns a bitmask, by using the bitwise & - * we're returning true if 2 in that bitmask is set to true. 2 is set - * to true if b preceeds a. - */ - return a.compareDocumentPosition(b) & 2 ? 1 : -1 - } - - getBaseTargetFromProps( - props: MotionProps, - key: string - ): AnyResolvedKeyframe | MotionValue | undefined { - return props.style - ? (props.style[key as keyof MotionStyle] as string) - : undefined - } - - removeValueFromRenderState( - key: string, - { vars, style }: HTMLRenderState - ): void { - delete vars[key] - delete style[key] - } - - KeyframeResolver = DOMKeyframesResolver - - childSubscription?: VoidFunction - handleChildMotionValue() { - if (this.childSubscription) { - this.childSubscription() - delete this.childSubscription - } - - const { children } = this.props - if (isMotionValue(children)) { - this.childSubscription = children.on("change", (latest) => { - if (this.current) { - this.current.textContent = `${latest}` - } - }) - } - } -} diff --git a/packages/framer-motion/src/render/dom/create-visual-element.ts b/packages/framer-motion/src/render/dom/create-visual-element.ts index 2655ee361a..cc51621b9b 100644 --- a/packages/framer-motion/src/render/dom/create-visual-element.ts +++ b/packages/framer-motion/src/render/dom/create-visual-element.ts @@ -1,6 +1,5 @@ +import { HTMLVisualElement, SVGVisualElement } from "motion-dom" import { ComponentType, Fragment } from "react" -import { HTMLVisualElement } from "../html/HTMLVisualElement" -import { SVGVisualElement } from "../svg/SVGVisualElement" import { CreateVisualElement, VisualElementOptions } from "../types" import { isSVGComponent } from "./utils/is-svg-component" diff --git a/packages/framer-motion/src/render/dom/use-motion-value-child.ts b/packages/framer-motion/src/render/dom/use-motion-value-child.ts index c0fe9258fb..8ebe07c792 100644 --- a/packages/framer-motion/src/render/dom/use-motion-value-child.ts +++ b/packages/framer-motion/src/render/dom/use-motion-value-child.ts @@ -1,9 +1,8 @@ "use client" -import { MotionValue } from "motion-dom" +import { MotionValue, type VisualElement } from "motion-dom" import { useConstant } from "../../utils/use-constant" import { useMotionValueEvent } from "../../utils/use-motion-value-event" -import type { VisualElement } from "../VisualElement" export function useMotionValueChild( children: MotionValue, diff --git a/packages/framer-motion/src/render/dom/utils/__tests__/camel-to-dash.test.ts b/packages/framer-motion/src/render/dom/utils/__tests__/camel-to-dash.test.ts index 1722467a35..6779ef60ff 100644 --- a/packages/framer-motion/src/render/dom/utils/__tests__/camel-to-dash.test.ts +++ b/packages/framer-motion/src/render/dom/utils/__tests__/camel-to-dash.test.ts @@ -1,5 +1,5 @@ import "../../../../jest.setup" -import { camelToDash } from "../camel-to-dash" +import { camelToDash } from "motion-dom" describe("camelToDash", () => { it("Converts camel case to dash case", () => { diff --git a/packages/framer-motion/src/render/dom/utils/camel-to-dash.ts b/packages/framer-motion/src/render/dom/utils/camel-to-dash.ts deleted file mode 100644 index 8219a3f8f1..0000000000 --- a/packages/framer-motion/src/render/dom/utils/camel-to-dash.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Convert camelCase to dash-case properties. - */ -export const camelToDash = (str: string) => - str.replace(/([a-z])([A-Z])/gu, "$1-$2").toLowerCase() diff --git a/packages/framer-motion/src/render/html/HTMLVisualElement.ts b/packages/framer-motion/src/render/html/HTMLVisualElement.ts deleted file mode 100644 index 0a8ee7c154..0000000000 --- a/packages/framer-motion/src/render/html/HTMLVisualElement.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { - AnyResolvedKeyframe, - defaultTransformValue, - isCSSVariableName, - measureViewportBox, - readTransformValue, - transformProps, -} from "motion-dom" -import type { Box } from "motion-utils" -import { MotionConfigContext } from "../../context/MotionConfigContext" -import { MotionProps } from "../../motion/types" -import { DOMVisualElement } from "../dom/DOMVisualElement" -import { DOMVisualElementOptions } from "../dom/types" -import type { ResolvedValues } from "../types" -import { VisualElement } from "../VisualElement" -import { HTMLRenderState } from "./types" -import { buildHTMLStyles } from "./utils/build-styles" -import { renderHTML } from "./utils/render" -import { scrapeMotionValuesFromProps } from "./utils/scrape-motion-values" - -export function getComputedStyle(element: HTMLElement) { - return window.getComputedStyle(element) -} - -export class HTMLVisualElement extends DOMVisualElement< - HTMLElement, - HTMLRenderState, - DOMVisualElementOptions -> { - type = "html" - - readValueFromInstance( - instance: HTMLElement, - key: string - ): AnyResolvedKeyframe | null | undefined { - if (transformProps.has(key)) { - return this.projection?.isProjecting - ? defaultTransformValue(key) - : readTransformValue(instance, key) - } else { - const computedStyle = getComputedStyle(instance) - const value = - (isCSSVariableName(key) - ? computedStyle.getPropertyValue(key) - : computedStyle[key as keyof typeof computedStyle]) || 0 - - return typeof value === "string" ? value.trim() : (value as number) - } - } - - measureInstanceViewportBox( - instance: HTMLElement, - { transformPagePoint }: MotionProps & Partial - ): Box { - return measureViewportBox(instance, transformPagePoint) - } - - build( - renderState: HTMLRenderState, - latestValues: ResolvedValues, - props: MotionProps - ) { - buildHTMLStyles(renderState, latestValues, props.transformTemplate) - } - - scrapeMotionValuesFromProps( - props: MotionProps, - prevProps: MotionProps, - visualElement: VisualElement - ) { - return scrapeMotionValuesFromProps(props, prevProps, visualElement) - } - - renderInstance = renderHTML -} diff --git a/packages/framer-motion/src/render/html/use-props.ts b/packages/framer-motion/src/render/html/use-props.ts index 4c760ae26d..164f831181 100644 --- a/packages/framer-motion/src/render/html/use-props.ts +++ b/packages/framer-motion/src/render/html/use-props.ts @@ -1,11 +1,10 @@ "use client" -import { AnyResolvedKeyframe, isMotionValue, MotionValue } from "motion-dom" +import { AnyResolvedKeyframe, buildHTMLStyles, isMotionValue, MotionValue } from "motion-dom" import { HTMLProps, useMemo } from "react" import { MotionProps } from "../../motion/types" import { isForcedMotionValue } from "../../motion/utils/is-forced-motion-value" import { ResolvedValues } from "../types" -import { buildHTMLStyles } from "./utils/build-styles" import { createHtmlRenderState } from "./utils/create-render-state" export function copyRawValuesOnly( diff --git a/packages/framer-motion/src/render/html/utils/__tests__/build-styles.test.ts b/packages/framer-motion/src/render/html/utils/__tests__/build-styles.test.ts index ffa0cd9ce4..06de8441c1 100644 --- a/packages/framer-motion/src/render/html/utils/__tests__/build-styles.test.ts +++ b/packages/framer-motion/src/render/html/utils/__tests__/build-styles.test.ts @@ -1,8 +1,7 @@ +import { buildHTMLStyles, ResolvedValues } from "motion-dom" import "../../../../jest.setup" import { DOMVisualElementOptions } from "../../../dom/types" -import { ResolvedValues } from "../../../types" import { TransformOrigin } from "../../types" -import { buildHTMLStyles } from "../build-styles" describe("buildHTMLStyles", () => { test("Builds basic styles", () => { diff --git a/packages/framer-motion/src/render/html/utils/__tests__/build-transform.test.ts b/packages/framer-motion/src/render/html/utils/__tests__/build-transform.test.ts index ddcf2b1b6a..29f9d9b4a0 100644 --- a/packages/framer-motion/src/render/html/utils/__tests__/build-transform.test.ts +++ b/packages/framer-motion/src/render/html/utils/__tests__/build-transform.test.ts @@ -1,6 +1,5 @@ -import { transformProps } from "motion-dom" +import { buildTransform, transformProps } from "motion-dom" import "../../../../jest.setup" -import { buildTransform } from "../build-transform" describe("transformProps.has", () => { it("Correctly identifies only transformPerspective as a transform prop", () => { diff --git a/packages/framer-motion/src/render/html/utils/build-styles.ts b/packages/framer-motion/src/render/html/utils/build-styles.ts deleted file mode 100644 index 8cd2954717..0000000000 --- a/packages/framer-motion/src/render/html/utils/build-styles.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { - getValueAsType, - isCSSVariableName, - numberValueTypes, - transformProps, -} from "motion-dom" -import { MotionProps } from "../../../motion/types" -import { ResolvedValues } from "../../types" -import { HTMLRenderState } from "../types" -import { buildTransform } from "./build-transform" - -export function buildHTMLStyles( - state: HTMLRenderState, - latestValues: ResolvedValues, - transformTemplate?: MotionProps["transformTemplate"] -) { - const { style, vars, transformOrigin } = state - - // Track whether we encounter any transform or transformOrigin values. - let hasTransform = false - let hasTransformOrigin = false - - /** - * Loop over all our latest animated values and decide whether to handle them - * as a style or CSS variable. - * - * Transforms and transform origins are kept separately for further processing. - */ - for (const key in latestValues) { - const value = latestValues[key] - - if (transformProps.has(key)) { - // If this is a transform, flag to enable further transform processing - hasTransform = true - continue - } else if (isCSSVariableName(key)) { - vars[key] = value - continue - } else { - // Convert the value to its default value type, ie 0 -> "0px" - const valueAsType = getValueAsType(value, numberValueTypes[key]) - - if (key.startsWith("origin")) { - // If this is a transform origin, flag and enable further transform-origin processing - hasTransformOrigin = true - transformOrigin[key as keyof typeof transformOrigin] = - valueAsType - } else { - style[key] = valueAsType - } - } - } - - if (!latestValues.transform) { - if (hasTransform || transformTemplate) { - style.transform = buildTransform( - latestValues, - state.transform, - transformTemplate - ) - } else if (style.transform) { - /** - * If we have previously created a transform but currently don't have any, - * reset transform style to none. - */ - style.transform = "none" - } - } - - /** - * Build a transformOrigin style. Uses the same defaults as the browser for - * undefined origins. - */ - if (hasTransformOrigin) { - const { - originX = "50%", - originY = "50%", - originZ = 0, - } = transformOrigin - style.transformOrigin = `${originX} ${originY} ${originZ}` - } -} diff --git a/packages/framer-motion/src/render/html/utils/build-transform.ts b/packages/framer-motion/src/render/html/utils/build-transform.ts deleted file mode 100644 index 0b19ad52fd..0000000000 --- a/packages/framer-motion/src/render/html/utils/build-transform.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { - getValueAsType, - numberValueTypes, - transformPropOrder, -} from "motion-dom" -import { MotionProps } from "../../../motion/types" -import { ResolvedValues } from "../../types" -import { HTMLRenderState } from "../types" - -const translateAlias = { - x: "translateX", - y: "translateY", - z: "translateZ", - transformPerspective: "perspective", -} - -const numTransforms = transformPropOrder.length - -/** - * Build a CSS transform style from individual x/y/scale etc properties. - * - * This outputs with a default order of transforms/scales/rotations, this can be customised by - * providing a transformTemplate function. - */ -export function buildTransform( - latestValues: ResolvedValues, - transform: HTMLRenderState["transform"], - transformTemplate?: MotionProps["transformTemplate"] -) { - // The transform string we're going to build into. - let transformString = "" - let transformIsDefault = true - - /** - * Loop over all possible transforms in order, adding the ones that - * are present to the transform string. - */ - for (let i = 0; i < numTransforms; i++) { - const key = transformPropOrder[i] as keyof typeof translateAlias - const value = latestValues[key] - - if (value === undefined) continue - - let valueIsDefault = true - if (typeof value === "number") { - valueIsDefault = value === (key.startsWith("scale") ? 1 : 0) - } else { - valueIsDefault = parseFloat(value) === 0 - } - - if (!valueIsDefault || transformTemplate) { - const valueAsType = getValueAsType(value, numberValueTypes[key]) - - if (!valueIsDefault) { - transformIsDefault = false - const transformName = translateAlias[key] || key - transformString += `${transformName}(${valueAsType}) ` - } - - if (transformTemplate) { - transform[key] = valueAsType - } - } - } - - transformString = transformString.trim() - - // If we have a custom `transform` template, pass our transform values and - // generated transformString to that before returning - if (transformTemplate) { - transformString = transformTemplate( - transform, - transformIsDefault ? "" : transformString - ) - } else if (transformIsDefault) { - transformString = "none" - } - - return transformString -} diff --git a/packages/framer-motion/src/render/html/utils/render.ts b/packages/framer-motion/src/render/html/utils/render.ts deleted file mode 100644 index 6587e2d920..0000000000 --- a/packages/framer-motion/src/render/html/utils/render.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { MotionStyle } from "../../.." -import { IProjectionNode } from "../../../projection/node/types" -import { HTMLRenderState } from "../types" - -export function renderHTML( - element: HTMLElement, - { style, vars }: HTMLRenderState, - styleProp?: MotionStyle, - projection?: IProjectionNode -) { - const elementStyle = element.style - - let key: string - for (key in style) { - // CSSStyleDeclaration has [index: number]: string; in the types, so we use that as key type. - elementStyle[key as unknown as number] = style[key] as string - } - - // Write projection styles directly to element style - projection?.applyProjectionStyles(elementStyle, styleProp) - - for (key in vars) { - // Loop over any CSS variables and assign those. - // They can only be assigned using `setProperty`. - elementStyle.setProperty(key, vars[key] as string) - } -} diff --git a/packages/framer-motion/src/render/html/utils/scrape-motion-values.ts b/packages/framer-motion/src/render/html/utils/scrape-motion-values.ts index 872d1c00fc..c06bedacc3 100644 --- a/packages/framer-motion/src/render/html/utils/scrape-motion-values.ts +++ b/packages/framer-motion/src/render/html/utils/scrape-motion-values.ts @@ -1,7 +1,6 @@ -import { isMotionValue } from "motion-dom" +import { isMotionValue, type VisualElement } from "motion-dom" import { MotionProps, MotionStyle } from "../../../motion/types" import { isForcedMotionValue } from "../../../motion/utils/is-forced-motion-value" -import type { VisualElement } from "../../VisualElement" export function scrapeMotionValuesFromProps( props: MotionProps, diff --git a/packages/framer-motion/src/render/object/ObjectVisualElement.ts b/packages/framer-motion/src/render/object/ObjectVisualElement.ts deleted file mode 100644 index fb47f6b48b..0000000000 --- a/packages/framer-motion/src/render/object/ObjectVisualElement.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { createBox } from "motion-dom" -import { ResolvedValues } from "../types" -import { VisualElement } from "../VisualElement" - -interface ObjectRenderState { - output: ResolvedValues -} - -function isObjectKey(key: string, object: Object): key is keyof Object { - return key in object -} - -export class ObjectVisualElement extends VisualElement< - Object, - ObjectRenderState -> { - type = "object" - - readValueFromInstance(instance: Object, key: string) { - if (isObjectKey(key, instance)) { - const value = instance[key] - if (typeof value === "string" || typeof value === "number") { - return value - } - } - - return undefined - } - - getBaseTargetFromProps() { - return undefined - } - - removeValueFromRenderState( - key: string, - renderState: ObjectRenderState - ): void { - delete renderState.output[key] - } - - measureInstanceViewportBox() { - return createBox() - } - - build(renderState: ObjectRenderState, latestValues: ResolvedValues) { - Object.assign(renderState.output, latestValues) - } - - renderInstance(instance: Object, { output }: ObjectRenderState) { - Object.assign(instance, output) - } - - sortInstanceNodePosition() { - return 0 - } -} diff --git a/packages/framer-motion/src/render/store.ts b/packages/framer-motion/src/render/store.ts deleted file mode 100644 index f94df0e77c..0000000000 --- a/packages/framer-motion/src/render/store.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { VisualElement } from "./VisualElement" - -export const visualElementStore = new WeakMap() diff --git a/packages/framer-motion/src/render/svg/SVGVisualElement.ts b/packages/framer-motion/src/render/svg/SVGVisualElement.ts deleted file mode 100644 index e46ebf260a..0000000000 --- a/packages/framer-motion/src/render/svg/SVGVisualElement.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { - AnyResolvedKeyframe, - createBox, - getDefaultValueType, - MotionValue, - transformProps, -} from "motion-dom" -import { MotionProps, MotionStyle } from "../../motion/types" -import { IProjectionNode } from "../../projection/node/types" -import { DOMVisualElement } from "../dom/DOMVisualElement" -import { DOMVisualElementOptions } from "../dom/types" -import { camelToDash } from "../dom/utils/camel-to-dash" -import { ResolvedValues } from "../types" -import { VisualElement } from "../VisualElement" -import { SVGRenderState } from "./types" -import { buildSVGAttrs } from "./utils/build-attrs" -import { camelCaseAttributes } from "./utils/camel-case-attrs" -import { isSVGTag } from "./utils/is-svg-tag" -import { renderSVG } from "./utils/render" -import { scrapeMotionValuesFromProps } from "./utils/scrape-motion-values" - -export class SVGVisualElement extends DOMVisualElement< - SVGElement, - SVGRenderState, - DOMVisualElementOptions -> { - type = "svg" - - isSVGTag = false - - getBaseTargetFromProps( - props: MotionProps, - key: string - ): AnyResolvedKeyframe | MotionValue | undefined { - return props[key as keyof MotionProps] - } - - readValueFromInstance(instance: SVGElement, key: string) { - if (transformProps.has(key)) { - const defaultType = getDefaultValueType(key) - return defaultType ? defaultType.default || 0 : 0 - } - key = !camelCaseAttributes.has(key) ? camelToDash(key) : key - return instance.getAttribute(key) - } - - measureInstanceViewportBox = createBox - - scrapeMotionValuesFromProps( - props: MotionProps, - prevProps: MotionProps, - visualElement: VisualElement - ) { - return scrapeMotionValuesFromProps(props, prevProps, visualElement) - } - - build( - renderState: SVGRenderState, - latestValues: ResolvedValues, - props: MotionProps - ) { - buildSVGAttrs( - renderState, - latestValues, - this.isSVGTag, - props.transformTemplate, - props.style - ) - } - - renderInstance( - instance: SVGElement, - renderState: SVGRenderState, - styleProp?: MotionStyle | undefined, - projection?: IProjectionNode | undefined - ): void { - renderSVG(instance, renderState, styleProp, projection) - } - - mount(instance: SVGElement) { - this.isSVGTag = isSVGTag(instance.tagName) - super.mount(instance) - } -} diff --git a/packages/framer-motion/src/render/svg/use-props.ts b/packages/framer-motion/src/render/svg/use-props.ts index 46b324d112..885ee4c0de 100644 --- a/packages/framer-motion/src/render/svg/use-props.ts +++ b/packages/framer-motion/src/render/svg/use-props.ts @@ -1,12 +1,11 @@ "use client" +import { buildSVGAttrs, isSVGTag } from "motion-dom" import { useMemo } from "react" import { MotionProps } from "../../motion/types" import { copyRawValuesOnly } from "../html/use-props" import { ResolvedValues } from "../types" -import { buildSVGAttrs } from "./utils/build-attrs" import { createSvgRenderState } from "./utils/create-render-state" -import { isSVGTag } from "./utils/is-svg-tag" export function useSVGProps( props: MotionProps, diff --git a/packages/framer-motion/src/render/svg/utils/__tests__/path.test.ts b/packages/framer-motion/src/render/svg/utils/__tests__/path.test.ts index 84104d72cd..09ef5a8711 100644 --- a/packages/framer-motion/src/render/svg/utils/__tests__/path.test.ts +++ b/packages/framer-motion/src/render/svg/utils/__tests__/path.test.ts @@ -1,5 +1,5 @@ +import { buildSVGPath } from "motion-dom" import "../../../../jest.setup" -import { buildSVGPath } from "../path" describe("buildSVGPath", () => { it("correctly generates SVG path props", () => { diff --git a/packages/framer-motion/src/render/svg/utils/build-attrs.ts b/packages/framer-motion/src/render/svg/utils/build-attrs.ts deleted file mode 100644 index 0696171640..0000000000 --- a/packages/framer-motion/src/render/svg/utils/build-attrs.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { MotionProps } from "../../../motion/types" -import { buildHTMLStyles } from "../../html/utils/build-styles" -import { ResolvedValues } from "../../types" -import { SVGRenderState } from "../types" -import { buildSVGPath } from "./path" - -/** - * CSS Motion Path properties that should remain as CSS styles on SVG elements. - */ -const cssMotionPathProperties = [ - "offsetDistance", - "offsetPath", - "offsetRotate", - "offsetAnchor", -] - -/** - * Build SVG visual attributes, like cx and style.transform - */ -export function buildSVGAttrs( - state: SVGRenderState, - { - attrX, - attrY, - attrScale, - pathLength, - pathSpacing = 1, - pathOffset = 0, - // This is object creation, which we try to avoid per-frame. - ...latest - }: ResolvedValues, - isSVGTag: boolean, - transformTemplate?: MotionProps["transformTemplate"], - styleProp?: MotionProps["style"] -) { - buildHTMLStyles(state, latest, transformTemplate) - - /** - * For svg tags we just want to make sure viewBox is animatable and treat all the styles - * as normal HTML tags. - */ - if (isSVGTag) { - if (state.style.viewBox) { - state.attrs.viewBox = state.style.viewBox - } - return - } - - state.attrs = state.style - state.style = {} - const { attrs, style } = state - - /** - * However, we apply transforms as CSS transforms. - * So if we detect a transform, transformOrigin we take it from attrs and copy it into style. - */ - if (attrs.transform) { - style.transform = attrs.transform - delete attrs.transform - } - if (style.transform || attrs.transformOrigin) { - style.transformOrigin = attrs.transformOrigin ?? "50% 50%" - delete attrs.transformOrigin - } - - if (style.transform) { - /** - * SVG's element transform-origin uses its own median as a reference. - * Therefore, transformBox becomes a fill-box - */ - style.transformBox = (styleProp?.transformBox as string) ?? "fill-box" - delete attrs.transformBox - } - - for (const key of cssMotionPathProperties) { - if (attrs[key] !== undefined) { - style[key] = attrs[key] - delete attrs[key] - } - } - - // Render attrX/attrY/attrScale as attributes - if (attrX !== undefined) attrs.x = attrX - if (attrY !== undefined) attrs.y = attrY - if (attrScale !== undefined) attrs.scale = attrScale - - // Build SVG path if one has been defined - if (pathLength !== undefined) { - buildSVGPath( - attrs, - pathLength as number, - pathSpacing as number, - pathOffset as number, - false - ) - } -} diff --git a/packages/framer-motion/src/render/svg/utils/camel-case-attrs.ts b/packages/framer-motion/src/render/svg/utils/camel-case-attrs.ts deleted file mode 100644 index 4947bfd2f9..0000000000 --- a/packages/framer-motion/src/render/svg/utils/camel-case-attrs.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * A set of attribute names that are always read/written as camel case. - */ -export const camelCaseAttributes = new Set([ - "baseFrequency", - "diffuseConstant", - "kernelMatrix", - "kernelUnitLength", - "keySplines", - "keyTimes", - "limitingConeAngle", - "markerHeight", - "markerWidth", - "numOctaves", - "targetX", - "targetY", - "surfaceScale", - "specularConstant", - "specularExponent", - "stdDeviation", - "tableValues", - "viewBox", - "gradientTransform", - "pathLength", - "startOffset", - "textLength", - "lengthAdjust", -]) diff --git a/packages/framer-motion/src/render/svg/utils/is-svg-tag.ts b/packages/framer-motion/src/render/svg/utils/is-svg-tag.ts deleted file mode 100644 index aa99a203b9..0000000000 --- a/packages/framer-motion/src/render/svg/utils/is-svg-tag.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const isSVGTag = (tag: unknown) => - typeof tag === "string" && tag.toLowerCase() === "svg" diff --git a/packages/framer-motion/src/render/svg/utils/path.ts b/packages/framer-motion/src/render/svg/utils/path.ts deleted file mode 100644 index b8db94976a..0000000000 --- a/packages/framer-motion/src/render/svg/utils/path.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { px } from "motion-dom" -import { ResolvedValues } from "../../types" - -const dashKeys = { - offset: "stroke-dashoffset", - array: "stroke-dasharray", -} - -const camelKeys = { - offset: "strokeDashoffset", - array: "strokeDasharray", -} - -/** - * Build SVG path properties. Uses the path's measured length to convert - * our custom pathLength, pathSpacing and pathOffset into stroke-dashoffset - * and stroke-dasharray attributes. - * - * This function is mutative to reduce per-frame GC. - */ -export function buildSVGPath( - attrs: ResolvedValues, - length: number, - spacing = 1, - offset = 0, - useDashCase: boolean = true -): void { - // Normalise path length by setting SVG attribute pathLength to 1 - attrs.pathLength = 1 - - // We use dash case when setting attributes directly to the DOM node and camel case - // when defining props on a React component. - const keys = useDashCase ? dashKeys : camelKeys - - // Build the dash offset - attrs[keys.offset] = px.transform!(-offset) - - // Build the dash array - const pathLength = px.transform!(length) - const pathSpacing = px.transform!(spacing) - attrs[keys.array] = `${pathLength} ${pathSpacing}` -} diff --git a/packages/framer-motion/src/render/svg/utils/render.ts b/packages/framer-motion/src/render/svg/utils/render.ts deleted file mode 100644 index b643c874e3..0000000000 --- a/packages/framer-motion/src/render/svg/utils/render.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { MotionStyle } from "../../../motion/types" -import { IProjectionNode } from "../../../projection/node/types" -import { camelToDash } from "../../dom/utils/camel-to-dash" -import { renderHTML } from "../../html/utils/render" -import { SVGRenderState } from "../types" -import { camelCaseAttributes } from "./camel-case-attrs" - -export function renderSVG( - element: SVGElement, - renderState: SVGRenderState, - _styleProp?: MotionStyle, - projection?: IProjectionNode -) { - renderHTML(element as any, renderState, undefined, projection) - - for (const key in renderState.attrs) { - element.setAttribute( - !camelCaseAttributes.has(key) ? camelToDash(key) : key, - renderState.attrs[key] as string - ) - } -} diff --git a/packages/framer-motion/src/render/svg/utils/scrape-motion-values.ts b/packages/framer-motion/src/render/svg/utils/scrape-motion-values.ts index c0760d0094..631bc2af59 100644 --- a/packages/framer-motion/src/render/svg/utils/scrape-motion-values.ts +++ b/packages/framer-motion/src/render/svg/utils/scrape-motion-values.ts @@ -1,7 +1,6 @@ -import { isMotionValue, transformPropOrder } from "motion-dom" +import { isMotionValue, transformPropOrder, type VisualElement } from "motion-dom" import { MotionProps } from "../../../motion/types" import { scrapeMotionValuesFromProps as scrapeHTMLMotionValuesFromProps } from "../../html/utils/scrape-motion-values" -import type { VisualElement } from "../../VisualElement" export function scrapeMotionValuesFromProps( props: MotionProps, diff --git a/packages/framer-motion/src/render/types.ts b/packages/framer-motion/src/render/types.ts index 5427316d3b..e63bbe4031 100644 --- a/packages/framer-motion/src/render/types.ts +++ b/packages/framer-motion/src/render/types.ts @@ -1,12 +1,16 @@ -import type { AnimationDefinition } from "motion-dom" -import { AnyResolvedKeyframe, MotionValue } from "motion-dom" +import { + AnyResolvedKeyframe, + MotionValue, + ResolvedValues, + type AnimationDefinition, + type VisualElement, +} from "motion-dom" import type { Axis, Box } from "motion-utils" import { ReducedMotionConfig } from "../context/MotionConfigContext" import type { PresenceContextProps } from "../context/PresenceContext" import { MotionProps } from "../motion/types" import { VisualState } from "../motion/utils/use-visual-state" import { DOMMotionComponents } from "./dom/types" -import type { VisualElement } from "./VisualElement" export type ScrapeMotionValuesFromProps = ( props: MotionProps, @@ -33,12 +37,8 @@ export interface VisualElementOptions { isSVG?: boolean } -/** - * A generic set of string/number values - */ -export interface ResolvedValues { - [key: string]: AnyResolvedKeyframe -} +// Re-export ResolvedValues from motion-dom for backward compatibility +export type { ResolvedValues } export interface VisualElementEventCallbacks { BeforeLayoutMeasure: () => void diff --git a/packages/framer-motion/src/render/utils/__tests__/StateVisualElement.ts b/packages/framer-motion/src/render/utils/__tests__/StateVisualElement.ts index 111f2eb2d6..5dfe931bee 100644 --- a/packages/framer-motion/src/render/utils/__tests__/StateVisualElement.ts +++ b/packages/framer-motion/src/render/utils/__tests__/StateVisualElement.ts @@ -1,7 +1,5 @@ -import { createBox } from "motion-dom" -import { ResolvedValues } from "../../types" +import { createBox, ResolvedValues, VisualElement } from "motion-dom" import { MotionProps, MotionStyle } from "../../../motion/types" -import { VisualElement } from "../../VisualElement" export class StateVisualElement extends VisualElement< ResolvedValues, diff --git a/packages/framer-motion/src/render/utils/__tests__/animation-state.test.ts b/packages/framer-motion/src/render/utils/__tests__/animation-state.test.ts index 9848ac69c3..3c17527627 100644 --- a/packages/framer-motion/src/render/utils/__tests__/animation-state.test.ts +++ b/packages/framer-motion/src/render/utils/__tests__/animation-state.test.ts @@ -1,7 +1,6 @@ -import { AnimationState, createAnimationState } from "../animation-state" +import { createAnimationState, type AnimationState, type VisualElement } from "motion-dom" import { MotionProps } from "../../../motion/types" import { createHtmlRenderState } from "../../html/utils/create-render-state" -import { VisualElement } from "../../VisualElement" import { StateVisualElement } from "./StateVisualElement" function createTest( diff --git a/packages/framer-motion/src/render/utils/__tests__/variants.test.ts b/packages/framer-motion/src/render/utils/__tests__/variants.test.ts index 81e6985951..4f4d01d94d 100644 --- a/packages/framer-motion/src/render/utils/__tests__/variants.test.ts +++ b/packages/framer-motion/src/render/utils/__tests__/variants.test.ts @@ -1,4 +1,4 @@ -import { resolveVariantFromProps } from "../resolve-variants" +import { resolveVariantFromProps } from "motion-dom" describe("resolveVariantFromProps", () => { test("Resolves string", () => { diff --git a/packages/framer-motion/src/render/utils/animation-state.ts b/packages/framer-motion/src/render/utils/animation-state.ts deleted file mode 100644 index 365dd68493..0000000000 --- a/packages/framer-motion/src/render/utils/animation-state.ts +++ /dev/null @@ -1,487 +0,0 @@ -import type { AnimationDefinition, TargetAndTransition } from "motion-dom" -import { isVariantLabel, variantPriorityOrder } from "motion-dom" -import { VisualElementAnimationOptions } from "../../animation/interfaces/types" -import { animateVisualElement } from "../../animation/interfaces/visual-element" -import { calcChildStagger } from "../../animation/utils/calc-child-stagger" -import { isAnimationControls } from "../../animation/utils/is-animation-controls" -import { isKeyframesTarget } from "../../animation/utils/is-keyframes-target" -import { VariantLabels } from "../../motion/types" -import { shallowCompare } from "../../utils/shallow-compare" -import type { VisualElement } from "../VisualElement" -import { getVariantContext } from "./get-variant-context" -import { resolveVariant } from "./resolve-dynamic-variants" -import { AnimationType } from "./types" - -export interface AnimationState { - animateChanges: (type?: AnimationType) => Promise - setActive: ( - type: AnimationType, - isActive: boolean, - options?: VisualElementAnimationOptions - ) => Promise - setAnimateFunction: (fn: any) => void - getState: () => { [key: string]: AnimationTypeState } - reset: () => void -} - -interface DefinitionAndOptions { - animation: AnimationDefinition - options?: VisualElementAnimationOptions -} - -export type AnimationList = string[] | TargetAndTransition[] - -const reversePriorityOrder = [...variantPriorityOrder].reverse() -const numAnimationTypes = variantPriorityOrder.length - -function animateList(visualElement: VisualElement) { - return (animations: DefinitionAndOptions[]) => - Promise.all( - animations.map(({ animation, options }) => - animateVisualElement(visualElement, animation, options) - ) - ) -} - -export function createAnimationState( - visualElement: VisualElement -): AnimationState { - let animate = animateList(visualElement) - let state = createState() - let isInitialRender = true - - /** - * This function will be used to reduce the animation definitions for - * each active animation type into an object of resolved values for it. - */ - const buildResolvedTypeValues = - (type: AnimationType) => - ( - acc: { [key: string]: any }, - definition: string | TargetAndTransition | undefined - ) => { - const resolved = resolveVariant( - visualElement, - definition, - type === "exit" - ? visualElement.presenceContext?.custom - : undefined - ) - - if (resolved) { - const { transition, transitionEnd, ...target } = resolved - acc = { ...acc, ...target, ...transitionEnd } - } - - return acc - } - - /** - * This just allows us to inject mocked animation functions - * @internal - */ - function setAnimateFunction(makeAnimator: typeof animateList) { - animate = makeAnimator(visualElement) - } - - /** - * When we receive new props, we need to: - * 1. Create a list of protected keys for each type. This is a directory of - * value keys that are currently being "handled" by types of a higher priority - * so that whenever an animation is played of a given type, these values are - * protected from being animated. - * 2. Determine if an animation type needs animating. - * 3. Determine if any values have been removed from a type and figure out - * what to animate those to. - */ - function animateChanges(changedActiveType?: AnimationType) { - const { props } = visualElement - const context = getVariantContext(visualElement.parent) || {} - - /** - * A list of animations that we'll build into as we iterate through the animation - * types. This will get executed at the end of the function. - */ - const animations: DefinitionAndOptions[] = [] - - /** - * Keep track of which values have been removed. Then, as we hit lower priority - * animation types, we can check if they contain removed values and animate to that. - */ - const removedKeys = new Set() - - /** - * A dictionary of all encountered keys. This is an object to let us build into and - * copy it without iteration. Each time we hit an animation type we set its protected - * keys - the keys its not allowed to animate - to the latest version of this object. - */ - let encounteredKeys = {} - - /** - * If a variant has been removed at a given index, and this component is controlling - * variant animations, we want to ensure lower-priority variants are forced to animate. - */ - let removedVariantIndex = Infinity - - /** - * Iterate through all animation types in reverse priority order. For each, we want to - * detect which values it's handling and whether or not they've changed (and therefore - * need to be animated). If any values have been removed, we want to detect those in - * lower priority props and flag for animation. - */ - for (let i = 0; i < numAnimationTypes; i++) { - const type = reversePriorityOrder[i] - const typeState = state[type] - const prop = - props[type] !== undefined - ? props[type] - : context[type as keyof typeof context] - const propIsVariant = isVariantLabel(prop) - - /** - * If this type has *just* changed isActive status, set activeDelta - * to that status. Otherwise set to null. - */ - const activeDelta = - type === changedActiveType ? typeState.isActive : null - - if (activeDelta === false) removedVariantIndex = i - - /** - * If this prop is an inherited variant, rather than been set directly on the - * component itself, we want to make sure we allow the parent to trigger animations. - * - * TODO: Can probably change this to a !isControllingVariants check - */ - let isInherited = - prop === context[type as keyof typeof context] && - prop !== props[type] && - propIsVariant - - if ( - isInherited && - isInitialRender && - visualElement.manuallyAnimateOnMount - ) { - isInherited = false - } - - /** - * Set all encountered keys so far as the protected keys for this type. This will - * be any key that has been animated or otherwise handled by active, higher-priortiy types. - */ - typeState.protectedKeys = { ...encounteredKeys } - - // Check if we can skip analysing this prop early - if ( - // If it isn't active and hasn't *just* been set as inactive - (!typeState.isActive && activeDelta === null) || - // If we didn't and don't have any defined prop for this animation type - (!prop && !typeState.prevProp) || - // Or if the prop doesn't define an animation - isAnimationControls(prop) || - typeof prop === "boolean" - ) { - continue - } - - /** - * As we go look through the values defined on this type, if we detect - * a changed value or a value that was removed in a higher priority, we set - * this to true and add this prop to the animation list. - */ - const variantDidChange = checkVariantsDidChange( - typeState.prevProp, - prop - ) - - let shouldAnimateType = - variantDidChange || - // If we're making this variant active, we want to always make it active - (type === changedActiveType && - typeState.isActive && - !isInherited && - propIsVariant) || - // If we removed a higher-priority variant (i is in reverse order) - (i > removedVariantIndex && propIsVariant) - - let handledRemovedValues = false - - /** - * As animations can be set as variant lists, variants or target objects, we - * coerce everything to an array if it isn't one already - */ - const definitionList = Array.isArray(prop) ? prop : [prop] - - /** - * Build an object of all the resolved values. We'll use this in the subsequent - * animateChanges calls to determine whether a value has changed. - */ - let resolvedValues = definitionList.reduce( - buildResolvedTypeValues(type), - {} - ) - - if (activeDelta === false) resolvedValues = {} - - /** - * Now we need to loop through all the keys in the prev prop and this prop, - * and decide: - * 1. If the value has changed, and needs animating - * 2. If it has been removed, and needs adding to the removedKeys set - * 3. If it has been removed in a higher priority type and needs animating - * 4. If it hasn't been removed in a higher priority but hasn't changed, and - * needs adding to the type's protectedKeys list. - */ - const { prevResolvedValues = {} } = typeState - - const allKeys = { - ...prevResolvedValues, - ...resolvedValues, - } - const markToAnimate = (key: string) => { - shouldAnimateType = true - if (removedKeys.has(key)) { - handledRemovedValues = true - removedKeys.delete(key) - } - typeState.needsAnimating[key] = true - - const motionValue = visualElement.getValue(key) - if (motionValue) motionValue.liveStyle = false - } - - for (const key in allKeys) { - const next = resolvedValues[key] - const prev = prevResolvedValues[key] - - // If we've already handled this we can just skip ahead - if (encounteredKeys.hasOwnProperty(key)) continue - - /** - * If the value has changed, we probably want to animate it. - */ - let valueHasChanged = false - if (isKeyframesTarget(next) && isKeyframesTarget(prev)) { - valueHasChanged = !shallowCompare(next, prev) - } else { - valueHasChanged = next !== prev - } - - if (valueHasChanged) { - if (next !== undefined && next !== null) { - // If next is defined and doesn't equal prev, it needs animating - markToAnimate(key) - } else { - // If it's undefined, it's been removed. - removedKeys.add(key) - } - } else if (next !== undefined && removedKeys.has(key)) { - /** - * If next hasn't changed and it isn't undefined, we want to check if it's - * been removed by a higher priority - */ - markToAnimate(key) - } else { - /** - * If it hasn't changed, we add it to the list of protected values - * to ensure it doesn't get animated. - */ - typeState.protectedKeys[key] = true - } - } - - /** - * Update the typeState so next time animateChanges is called we can compare the - * latest prop and resolvedValues to these. - */ - typeState.prevProp = prop - typeState.prevResolvedValues = resolvedValues - - if (typeState.isActive) { - encounteredKeys = { ...encounteredKeys, ...resolvedValues } - } - - if (isInitialRender && visualElement.blockInitialAnimation) { - shouldAnimateType = false - } - - /** - * If this is an inherited prop we want to skip this animation - * unless the inherited variants haven't changed on this render. - */ - const willAnimateViaParent = isInherited && variantDidChange - const needsAnimating = !willAnimateViaParent || handledRemovedValues - if (shouldAnimateType && needsAnimating) { - animations.push( - ...definitionList.map((animation) => { - const options: VisualElementAnimationOptions = { type } - - /** - * If we're performing the initial animation, but we're not - * rendering at the same time as the variant-controlling parent, - * we want to use the parent's transition to calculate the stagger. - */ - if ( - typeof animation === "string" && - isInitialRender && - !willAnimateViaParent && - visualElement.manuallyAnimateOnMount && - visualElement.parent - ) { - const { parent } = visualElement - const parentVariant = resolveVariant( - parent, - animation - ) - - if (parent.enteringChildren && parentVariant) { - const { delayChildren } = - parentVariant.transition || {} - options.delay = calcChildStagger( - parent.enteringChildren, - visualElement, - delayChildren - ) - } - } - - return { - animation: animation as AnimationDefinition, - options, - } - }) - ) - } - } - - /** - * If there are some removed value that haven't been dealt with, - * we need to create a new animation that falls back either to the value - * defined in the style prop, or the last read value. - */ - if (removedKeys.size) { - const fallbackAnimation: TargetAndTransition = {} - - /** - * If the initial prop contains a transition we can use that, otherwise - * allow the animation function to use the visual element's default. - */ - if (typeof props.initial !== "boolean") { - const initialTransition = resolveVariant( - visualElement, - Array.isArray(props.initial) - ? props.initial[0] - : props.initial - ) - - if (initialTransition && initialTransition.transition) { - fallbackAnimation.transition = initialTransition.transition - } - } - - removedKeys.forEach((key) => { - const fallbackTarget = visualElement.getBaseTarget(key) - - const motionValue = visualElement.getValue(key) - if (motionValue) motionValue.liveStyle = true - - // @ts-expect-error - @mattgperry to figure if we should do something here - fallbackAnimation[key] = fallbackTarget ?? null - }) - - animations.push({ animation: fallbackAnimation }) - } - - let shouldAnimate = Boolean(animations.length) - - if ( - isInitialRender && - (props.initial === false || props.initial === props.animate) && - !visualElement.manuallyAnimateOnMount - ) { - shouldAnimate = false - } - - isInitialRender = false - return shouldAnimate ? animate(animations) : Promise.resolve() - } - - /** - * Change whether a certain animation type is active. - */ - function setActive(type: AnimationType, isActive: boolean) { - // If the active state hasn't changed, we can safely do nothing here - if (state[type].isActive === isActive) return Promise.resolve() - - // Propagate active change to children - visualElement.variantChildren?.forEach((child) => - child.animationState?.setActive(type, isActive) - ) - - state[type].isActive = isActive - - const animations = animateChanges(type) - - for (const key in state) { - state[key as keyof typeof state].protectedKeys = {} - } - - return animations - } - - return { - animateChanges, - setActive, - setAnimateFunction, - getState: () => state, - reset: () => { - state = createState() - /** - * Temporarily disabling resetting this flag as it prevents components - * with initial={false} from animating after being remounted, for instance - * as the child of an Activity component. - */ - // isInitialRender = true - }, - } -} - -export function checkVariantsDidChange(prev: any, next: any) { - if (typeof next === "string") { - return next !== prev - } else if (Array.isArray(next)) { - return !shallowCompare(next, prev) - } - - return false -} - -export interface AnimationTypeState { - isActive: boolean - protectedKeys: { [key: string]: true } - needsAnimating: { [key: string]: boolean } - prevResolvedValues: { [key: string]: any } - prevProp?: VariantLabels | TargetAndTransition -} - -function createTypeState(isActive = false): AnimationTypeState { - return { - isActive, - protectedKeys: {}, - needsAnimating: {}, - prevResolvedValues: {}, - } -} - -function createState() { - return { - animate: createTypeState(true), - whileInView: createTypeState(), - whileHover: createTypeState(), - whileTap: createTypeState(), - whileDrag: createTypeState(), - whileFocus: createTypeState(), - exit: createTypeState(), - } -} diff --git a/packages/framer-motion/src/render/utils/compare-by-depth.ts b/packages/framer-motion/src/render/utils/compare-by-depth.ts index d8eee636f7..dbdc13f66e 100644 --- a/packages/framer-motion/src/render/utils/compare-by-depth.ts +++ b/packages/framer-motion/src/render/utils/compare-by-depth.ts @@ -1,4 +1,4 @@ -import type { VisualElement } from "../VisualElement" +import type { VisualElement } from "motion-dom" export interface WithDepth { depth: number diff --git a/packages/framer-motion/src/render/utils/get-variant-context.ts b/packages/framer-motion/src/render/utils/get-variant-context.ts deleted file mode 100644 index 5dd7201703..0000000000 --- a/packages/framer-motion/src/render/utils/get-variant-context.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { isVariantLabel, variantProps } from "motion-dom" -import { VisualElement } from "../VisualElement" - -const numVariantProps = variantProps.length - -type VariantStateContext = { - initial?: string | string[] - animate?: string | string[] - exit?: string | string[] - whileHover?: string | string[] - whileDrag?: string | string[] - whileFocus?: string | string[] - whileTap?: string | string[] -} - -export function getVariantContext( - visualElement?: VisualElement -): undefined | VariantStateContext { - if (!visualElement) return undefined - - if (!visualElement.isControllingVariants) { - const context = visualElement.parent - ? getVariantContext(visualElement.parent) || {} - : {} - if (visualElement.props.initial !== undefined) { - context.initial = visualElement.props.initial as any - } - return context - } - - const context = {} - for (let i = 0; i < numVariantProps; i++) { - const name = variantProps[i] as keyof typeof context - const prop = visualElement.props[name] - - if (isVariantLabel(prop) || prop === false) { - context[name] = prop - } - } - - return context -} diff --git a/packages/framer-motion/src/render/utils/is-controlling-variants.ts b/packages/framer-motion/src/render/utils/is-controlling-variants.ts deleted file mode 100644 index 04d6a0f4a5..0000000000 --- a/packages/framer-motion/src/render/utils/is-controlling-variants.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { isVariantLabel, variantProps } from "motion-dom" -import { isAnimationControls } from "../../animation/utils/is-animation-controls" -import { MotionProps } from "../../motion/types" - -export function isControllingVariants(props: MotionProps) { - return ( - isAnimationControls(props.animate) || - variantProps.some((name) => - isVariantLabel(props[name as keyof typeof props]) - ) - ) -} - -export function isVariantNode(props: MotionProps) { - return Boolean(isControllingVariants(props) || props.variants) -} diff --git a/packages/framer-motion/src/render/utils/is-draggable.ts b/packages/framer-motion/src/render/utils/is-draggable.ts index 508dfaaaad..1ea56eb8b6 100644 --- a/packages/framer-motion/src/render/utils/is-draggable.ts +++ b/packages/framer-motion/src/render/utils/is-draggable.ts @@ -1,4 +1,4 @@ -import type { VisualElement } from "../VisualElement" +import type { VisualElement } from "motion-dom" export function isDraggable(visualElement: VisualElement) { const { drag, _dragX } = visualElement.getProps() diff --git a/packages/framer-motion/src/render/utils/motion-values.ts b/packages/framer-motion/src/render/utils/motion-values.ts deleted file mode 100644 index 0f7803647e..0000000000 --- a/packages/framer-motion/src/render/utils/motion-values.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { isMotionValue, motionValue } from "motion-dom" -import { MotionStyle } from "../../motion/types" -import type { VisualElement } from "../VisualElement" - -export function updateMotionValuesFromProps( - element: VisualElement, - next: MotionStyle, - prev: MotionStyle -) { - for (const key in next) { - const nextValue = next[key as keyof MotionStyle] - const prevValue = prev[key as keyof MotionStyle] - - if (isMotionValue(nextValue)) { - /** - * If this is a motion value found in props or style, we want to add it - * to our visual element's motion value map. - */ - element.addValue(key, nextValue) - } else if (isMotionValue(prevValue)) { - /** - * If we're swapping from a motion value to a static value, - * create a new motion value from that - */ - element.addValue(key, motionValue(nextValue, { owner: element })) - } else if (prevValue !== nextValue) { - /** - * If this is a flat value that has changed, update the motion value - * or create one if it doesn't exist. We only want to do this if we're - * not handling the value with our animation state. - */ - if (element.hasValue(key)) { - const existingValue = element.getValue(key)! - - if (existingValue.liveStyle === true) { - existingValue.jump(nextValue) - } else if (!existingValue.hasAnimated) { - existingValue.set(nextValue) - } - } else { - const latestValue = element.getStaticValue(key) - element.addValue( - key, - motionValue( - latestValue !== undefined ? latestValue : nextValue, - { owner: element } - ) - ) - } - } - } - - // Handle removed values - for (const key in prev) { - if (next[key as keyof MotionStyle] === undefined) - element.removeValue(key) - } - - return next -} diff --git a/packages/framer-motion/src/render/utils/resolve-dynamic-variants.ts b/packages/framer-motion/src/render/utils/resolve-dynamic-variants.ts deleted file mode 100644 index 7e15df1df2..0000000000 --- a/packages/framer-motion/src/render/utils/resolve-dynamic-variants.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { - AnimationDefinition, - TargetAndTransition, - TargetResolver, -} from "motion-dom" -import type { VisualElement } from "../VisualElement" -import { resolveVariantFromProps } from "./resolve-variants" - -/** - * Resovles a variant if it's a variant resolver - */ -export function resolveVariant( - visualElement: VisualElement, - definition?: TargetAndTransition | TargetResolver, - custom?: any -): TargetAndTransition -export function resolveVariant( - visualElement: VisualElement, - definition?: AnimationDefinition, - custom?: any -): TargetAndTransition | undefined -export function resolveVariant( - visualElement: VisualElement, - definition?: AnimationDefinition, - custom?: any -) { - const props = visualElement.getProps() - return resolveVariantFromProps( - props, - definition, - custom !== undefined ? custom : props.custom, - visualElement - ) -} diff --git a/packages/framer-motion/src/render/utils/resolve-variants.ts b/packages/framer-motion/src/render/utils/resolve-variants.ts deleted file mode 100644 index 71e45cc2b6..0000000000 --- a/packages/framer-motion/src/render/utils/resolve-variants.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { - AnimationDefinition, - TargetAndTransition, - TargetResolver, -} from "motion-dom" -import type { MotionProps } from "../../motion/types" -import { VisualElement } from "../VisualElement" -import type { ResolvedValues } from "../types" - -function getValueState( - visualElement?: VisualElement -): [ResolvedValues, ResolvedValues] { - const state: [ResolvedValues, ResolvedValues] = [{}, {}] - - visualElement?.values.forEach((value, key) => { - state[0][key] = value.get() - state[1][key] = value.getVelocity() - }) - - return state -} - -export function resolveVariantFromProps( - props: MotionProps, - definition: TargetAndTransition | TargetResolver, - custom?: any, - visualElement?: VisualElement -): TargetAndTransition -export function resolveVariantFromProps( - props: MotionProps, - definition?: AnimationDefinition, - custom?: any, - visualElement?: VisualElement -): undefined | TargetAndTransition -export function resolveVariantFromProps( - props: MotionProps, - definition?: AnimationDefinition, - custom?: any, - visualElement?: VisualElement -) { - /** - * If the variant definition is a function, resolve. - */ - if (typeof definition === "function") { - const [current, velocity] = getValueState(visualElement) - definition = definition( - custom !== undefined ? custom : props.custom, - current, - velocity - ) - } - - /** - * If the variant definition is a variant label, or - * the function returned a variant label, resolve. - */ - if (typeof definition === "string") { - definition = props.variants && props.variants[definition] - } - - /** - * At this point we've resolved both functions and variant labels, - * but the resolved variant label might itself have been a function. - * If so, resolve. This can only have returned a valid target object. - */ - if (typeof definition === "function") { - const [current, velocity] = getValueState(visualElement) - definition = definition( - custom !== undefined ? custom : props.custom, - current, - velocity - ) - } - - return definition -} diff --git a/packages/framer-motion/src/render/utils/setters.ts b/packages/framer-motion/src/render/utils/setters.ts index 196964355d..cb8992c742 100644 --- a/packages/framer-motion/src/render/utils/setters.ts +++ b/packages/framer-motion/src/render/utils/setters.ts @@ -1,13 +1,13 @@ -import type { - AnimationDefinition, - AnyResolvedKeyframe, - UnresolvedValueKeyframe, - ValueKeyframesDefinition, +import { + motionValue, + resolveVariant, + type AnimationDefinition, + type AnyResolvedKeyframe, + type UnresolvedValueKeyframe, + type ValueKeyframesDefinition, + type VisualElement, } from "motion-dom" -import { motionValue } from "motion-dom" import { isKeyframesTarget } from "../../animation/utils/is-keyframes-target" -import type { VisualElement } from "../VisualElement" -import { resolveVariant } from "./resolve-dynamic-variants" /** * Set VisualElement's MotionValue, creating a new MotionValue for it if diff --git a/packages/framer-motion/src/utils/get-context-window.ts b/packages/framer-motion/src/utils/get-context-window.ts index ac4fbb6cf0..5f0926c74a 100644 --- a/packages/framer-motion/src/utils/get-context-window.ts +++ b/packages/framer-motion/src/utils/get-context-window.ts @@ -1,4 +1,4 @@ -import { VisualElement } from "../render/VisualElement" +import type { VisualElement } from "motion-dom" // Fixes https://github.com/motiondivision/motion/issues/2270 export const getContextWindow = ({ current }: VisualElement) => { diff --git a/packages/framer-motion/src/utils/reduced-motion/__tests__/index.test.tsx b/packages/framer-motion/src/utils/reduced-motion/__tests__/index.test.tsx index 887f558c45..181ee059cb 100644 --- a/packages/framer-motion/src/utils/reduced-motion/__tests__/index.test.tsx +++ b/packages/framer-motion/src/utils/reduced-motion/__tests__/index.test.tsx @@ -1,7 +1,7 @@ +import { hasReducedMotionListener } from "motion-dom" import { render } from "../../../jest.setup" import { motion } from "../../../render/components/motion" import { MotionConfig } from "../../../components/MotionConfig" -import { hasReducedMotionListener } from "../state" describe("reduced motion listener initialization", () => { beforeEach(() => { diff --git a/packages/framer-motion/src/utils/reduced-motion/index.ts b/packages/framer-motion/src/utils/reduced-motion/index.ts deleted file mode 100644 index b6cbe47bff..0000000000 --- a/packages/framer-motion/src/utils/reduced-motion/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { isBrowser } from "../is-browser" -import { hasReducedMotionListener, prefersReducedMotion } from "./state" - -export function initPrefersReducedMotion() { - hasReducedMotionListener.current = true - if (!isBrowser) return - - if (window.matchMedia) { - const motionMediaQuery = window.matchMedia("(prefers-reduced-motion)") - - const setReducedMotionPreferences = () => - (prefersReducedMotion.current = motionMediaQuery.matches) - - motionMediaQuery.addEventListener("change", setReducedMotionPreferences) - - setReducedMotionPreferences() - } else { - prefersReducedMotion.current = false - } -} diff --git a/packages/framer-motion/src/utils/reduced-motion/state.ts b/packages/framer-motion/src/utils/reduced-motion/state.ts deleted file mode 100644 index b0ceaf9823..0000000000 --- a/packages/framer-motion/src/utils/reduced-motion/state.ts +++ /dev/null @@ -1,8 +0,0 @@ -interface ReducedMotionState { - current: boolean | null -} - -// Does this device prefer reduced motion? Returns `null` server-side. -export const prefersReducedMotion: ReducedMotionState = { current: null } - -export const hasReducedMotionListener = { current: false } diff --git a/packages/framer-motion/src/utils/reduced-motion/use-reduced-motion.ts b/packages/framer-motion/src/utils/reduced-motion/use-reduced-motion.ts index ec3f0d3501..765ddd0db1 100644 --- a/packages/framer-motion/src/utils/reduced-motion/use-reduced-motion.ts +++ b/packages/framer-motion/src/utils/reduced-motion/use-reduced-motion.ts @@ -1,9 +1,12 @@ "use client" +import { + hasReducedMotionListener, + initPrefersReducedMotion, + prefersReducedMotion, +} from "motion-dom" import { warnOnce } from "motion-utils" import { useState } from "react" -import { initPrefersReducedMotion } from "." -import { hasReducedMotionListener, prefersReducedMotion } from "./state" /** * A hook that returns `true` if we should be using reduced motion based on the current device's Reduced Motion setting. diff --git a/packages/framer-motion/src/value/use-will-change/add-will-change.ts b/packages/framer-motion/src/value/use-will-change/add-will-change.ts index 94c4658e06..75dad81ce0 100644 --- a/packages/framer-motion/src/value/use-will-change/add-will-change.ts +++ b/packages/framer-motion/src/value/use-will-change/add-will-change.ts @@ -1,5 +1,5 @@ +import type { VisualElement } from "motion-dom" import { MotionGlobalConfig } from "motion-utils" -import type { VisualElement } from "../../render/VisualElement" import { isWillChangeMotionValue } from "./is" export function addValueToWillChange( diff --git a/packages/motion-dom/src/index.ts b/packages/motion-dom/src/index.ts index bd2ad27409..e33aeeadd5 100644 --- a/packages/motion-dom/src/index.ts +++ b/packages/motion-dom/src/index.ts @@ -175,7 +175,13 @@ export { } from "./render/utils/reduced-motion" // Projection geometry -export * from "./projection/geometry" +export * from "./projection/geometry/models" +export * from "./projection/geometry/delta-calc" +export * from "./projection/geometry/delta-apply" +export * from "./projection/geometry/delta-remove" +export * from "./projection/geometry/copy" +export * from "./projection/geometry/conversion" +export * from "./projection/geometry/utils" export { hasTransform, hasScale, has2DTranslate } from "./projection/utils/has-transform" export { measureViewportBox, measurePageBox } from "./projection/utils/measure" export { eachAxis } from "./projection/utils/each-axis" diff --git a/packages/motion-dom/src/projection/geometry/index.ts b/packages/motion-dom/src/projection/geometry/index.ts deleted file mode 100644 index d0707d77ae..0000000000 --- a/packages/motion-dom/src/projection/geometry/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from "./models" -export * from "./delta-calc" -export * from "./delta-apply" -export * from "./delta-remove" -export * from "./copy" -export * from "./conversion" -export * from "./utils" diff --git a/packages/motion-dom/src/projection/styles/index.ts b/packages/motion-dom/src/projection/styles/index.ts deleted file mode 100644 index de88d54414..0000000000 --- a/packages/motion-dom/src/projection/styles/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./types" -export * from "./scale-border-radius" -export * from "./scale-box-shadow" -export * from "./scale-correction" -export * from "./transform" diff --git a/packages/motion-dom/src/render/types.ts b/packages/motion-dom/src/render/types.ts index 1957cfcf86..23e091391f 100644 --- a/packages/motion-dom/src/render/types.ts +++ b/packages/motion-dom/src/render/types.ts @@ -143,6 +143,8 @@ export interface FeatureClass { export interface FeatureDefinition { isEnabled: (props: MotionNodeOptions) => boolean Feature?: FeatureClass + ProjectionNode?: any + MeasureLayout?: any } export type FeatureDefinitions = { From ea266671d0b8b7b0311e848fc42d61d1fe6b245c Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Mon, 12 Jan 2026 20:27:39 +0100 Subject: [PATCH 04/11] Move animateVisualElement and dependencies to motion-dom - Move animation interfaces (animateMotionValue, animateTarget, animateVariant, animateVisualElement) to motion-dom - Move animation utilities (getDefaultTransition, isTransitionDefined, getFinalKeyframe, calcChildStagger) to motion-dom - Move optimized-appear utilities to motion-dom - Move will-change utilities to motion-dom - Move setTarget to motion-dom - Consolidate Feature class in motion-dom with proper VisualElement typing - Update all framer-motion imports to use motion-dom exports - Delete duplicate files from framer-motion Co-Authored-By: Claude Opus 4.5 --- .../src/animation/animate/single-value.ts | 2 +- .../src/animation/animate/subject.ts | 2 +- .../src/animation/hooks/animation-controls.ts | 10 +++++-- .../src/animation/hooks/use-animated-state.ts | 9 ++++-- .../src/animation/interfaces/types.ts | 8 ----- .../src/animation/optimized-appear/start.ts | 7 ++--- .../drag/VisualElementDragControls.ts | 4 +-- .../framer-motion/src/gestures/drag/index.ts | 3 +- packages/framer-motion/src/gestures/focus.ts | 2 +- packages/framer-motion/src/gestures/hover.ts | 3 +- .../framer-motion/src/gestures/pan/index.ts | 4 +-- packages/framer-motion/src/gestures/press.ts | 3 +- packages/framer-motion/src/index.ts | 6 ++-- .../src/motion/features/Feature.ts | 17 ----------- .../src/motion/features/animation/exit.ts | 2 +- .../src/motion/features/animation/index.ts | 11 ++++--- .../src/motion/features/types.ts | 2 +- .../src/motion/features/viewport/index.ts | 2 +- .../src/motion/utils/use-visual-element.ts | 8 +++-- packages/framer-motion/src/projection.ts | 2 +- .../projection/node/create-projection-node.ts | 2 +- .../use-will-change/WillChangeMotionValue.ts | 8 +++-- .../use-will-change/__tests__/is.test.ts | 3 +- .../src/value/use-will-change/index.ts | 2 +- .../src/animation/interfaces/motion-value.ts | 29 +++++++++---------- .../src/animation/interfaces/types.ts | 9 ++++++ .../interfaces/visual-element-target.ts | 18 +++++------- .../interfaces/visual-element-variant.ts | 6 ++-- .../animation/interfaces/visual-element.ts | 6 ++-- .../src/animation/optimized-appear/data-id.ts | 2 +- .../optimized-appear/get-appear-id.ts | 2 +- .../src/animation/optimized-appear/types.ts | 4 +-- .../src/animation/utils/calc-child-stagger.ts | 3 +- .../animation/utils/default-transitions.ts | 3 +- .../animation}/utils/get-final-keyframe.ts | 2 +- .../animation/utils/is-transition-defined.ts | 3 +- packages/motion-dom/src/index.ts | 23 ++++++++++++++- packages/motion-dom/src/render/Feature.ts | 12 ++++---- .../src/render/utils/animation-state.ts | 11 ++----- .../src/render/utils/calc-child-stagger.ts | 26 ----------------- .../render/utils/is-forced-motion-value.ts | 4 +-- .../src/render/utils/setters.ts | 20 ++++++------- .../src/value/will-change}/add-will-change.ts | 2 +- .../src/value/will-change}/is.ts | 4 +-- .../src/value/will-change}/types.ts | 2 +- 45 files changed, 150 insertions(+), 163 deletions(-) delete mode 100644 packages/framer-motion/src/animation/interfaces/types.ts delete mode 100644 packages/framer-motion/src/motion/features/Feature.ts rename packages/{framer-motion => motion-dom}/src/animation/interfaces/motion-value.ts (86%) create mode 100644 packages/motion-dom/src/animation/interfaces/types.ts rename packages/{framer-motion => motion-dom}/src/animation/interfaces/visual-element-target.ts (88%) rename packages/{framer-motion => motion-dom}/src/animation/interfaces/visual-element-variant.ts (93%) rename packages/{framer-motion => motion-dom}/src/animation/interfaces/visual-element.ts (80%) rename packages/{framer-motion => motion-dom}/src/animation/optimized-appear/data-id.ts (72%) rename packages/{framer-motion => motion-dom}/src/animation/optimized-appear/get-appear-id.ts (82%) rename packages/{framer-motion => motion-dom}/src/animation/optimized-appear/types.ts (93%) rename packages/{framer-motion => motion-dom}/src/animation/utils/calc-child-stagger.ts (86%) rename packages/{framer-motion => motion-dom}/src/animation/utils/default-transitions.ts (90%) rename packages/{framer-motion/src/animation/animators/waapi => motion-dom/src/animation}/utils/get-final-keyframe.ts (89%) rename packages/{framer-motion => motion-dom}/src/animation/utils/is-transition-defined.ts (83%) delete mode 100644 packages/motion-dom/src/render/utils/calc-child-stagger.ts rename packages/{framer-motion => motion-dom}/src/render/utils/setters.ts (73%) rename packages/{framer-motion/src/value/use-will-change => motion-dom/src/value/will-change}/add-will-change.ts (91%) rename packages/{framer-motion/src/value/use-will-change => motion-dom/src/value/will-change}/is.ts (59%) rename packages/{framer-motion/src/value/use-will-change => motion-dom/src/value/will-change}/types.ts (65%) diff --git a/packages/framer-motion/src/animation/animate/single-value.ts b/packages/framer-motion/src/animation/animate/single-value.ts index db34093deb..31d463c9e3 100644 --- a/packages/framer-motion/src/animation/animate/single-value.ts +++ b/packages/framer-motion/src/animation/animate/single-value.ts @@ -1,4 +1,5 @@ import { + animateMotionValue, AnimationPlaybackControlsWithThen, AnyResolvedKeyframe, motionValue as createMotionValue, @@ -7,7 +8,6 @@ import { UnresolvedValueKeyframe, ValueAnimationTransition, } from "motion-dom" -import { animateMotionValue } from "../interfaces/motion-value" export function animateSingleValue( value: MotionValue | V, diff --git a/packages/framer-motion/src/animation/animate/subject.ts b/packages/framer-motion/src/animation/animate/subject.ts index 967a1e02ab..605879385d 100644 --- a/packages/framer-motion/src/animation/animate/subject.ts +++ b/packages/framer-motion/src/animation/animate/subject.ts @@ -1,4 +1,5 @@ import { + animateTarget, AnimationPlaybackControlsWithThen, AnimationScope, AnyResolvedKeyframe, @@ -13,7 +14,6 @@ import { visualElementStore, } from "motion-dom" import { invariant } from "motion-utils" -import { animateTarget } from "../interfaces/visual-element-target" import { ObjectTarget } from "../sequence/types" import { createDOMVisualElement, diff --git a/packages/framer-motion/src/animation/hooks/animation-controls.ts b/packages/framer-motion/src/animation/hooks/animation-controls.ts index 48e296d0a0..0a40b11d65 100644 --- a/packages/framer-motion/src/animation/hooks/animation-controls.ts +++ b/packages/framer-motion/src/animation/hooks/animation-controls.ts @@ -1,7 +1,11 @@ -import type { AnimationDefinition, LegacyAnimationControls, VisualElement } from "motion-dom" +import { + animateVisualElement, + setTarget, + type AnimationDefinition, + type LegacyAnimationControls, + type VisualElement, +} from "motion-dom" import { invariant } from "motion-utils" -import { setTarget } from "../../render/utils/setters" -import { animateVisualElement } from "../interfaces/visual-element" function stopAnimation(visualElement: VisualElement) { visualElement.values.forEach((value) => value.stop()) diff --git a/packages/framer-motion/src/animation/hooks/use-animated-state.ts b/packages/framer-motion/src/animation/hooks/use-animated-state.ts index f300bb6ac2..ee19a370c8 100644 --- a/packages/framer-motion/src/animation/hooks/use-animated-state.ts +++ b/packages/framer-motion/src/animation/hooks/use-animated-state.ts @@ -1,10 +1,15 @@ "use client" -import { createBox, ResolvedValues, TargetAndTransition, VisualElement } from "motion-dom" +import { + animateVisualElement, + createBox, + ResolvedValues, + TargetAndTransition, + VisualElement, +} from "motion-dom" import { useLayoutEffect, useState } from "react" import { makeUseVisualState } from "../../motion/utils/use-visual-state" import { useConstant } from "../../utils/use-constant" -import { animateVisualElement } from "../interfaces/visual-element" interface AnimatedStateOptions { initialState: ResolvedValues diff --git a/packages/framer-motion/src/animation/interfaces/types.ts b/packages/framer-motion/src/animation/interfaces/types.ts deleted file mode 100644 index 159409c556..0000000000 --- a/packages/framer-motion/src/animation/interfaces/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { AnimationType, Transition } from "motion-dom" - -export type VisualElementAnimationOptions = { - delay?: number - transitionOverride?: Transition - custom?: any - type?: AnimationType -} diff --git a/packages/framer-motion/src/animation/optimized-appear/start.ts b/packages/framer-motion/src/animation/optimized-appear/start.ts index a35907f77e..82712dd357 100644 --- a/packages/framer-motion/src/animation/optimized-appear/start.ts +++ b/packages/framer-motion/src/animation/optimized-appear/start.ts @@ -1,18 +1,17 @@ import { AnyResolvedKeyframe, Batcher, + getOptimisedAppearId, MotionValue, + optimizedAppearDataId, startWaapiAnimation, ValueAnimationTransition, + type WithAppearProps, } from "motion-dom" import { noop } from "motion-utils" -import { optimizedAppearDataId } from "./data-id" -import { getOptimisedAppearId } from "./get-appear-id" import { handoffOptimizedAppearAnimation } from "./handoff" import { appearAnimationStore, appearComplete, AppearStoreEntry } from "./store" import { appearStoreId } from "./store-id" -import "./types" -import type { WithAppearProps } from "./types" /** * A single time to use across all animations to manually set startTime diff --git a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts index 46d8a8b2fd..9fab2ca9af 100644 --- a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts +++ b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts @@ -1,4 +1,6 @@ import { + addValueToWillChange, + animateMotionValue, calcLength, convertBoundingBoxToBox, convertBoxToBoundingBox, @@ -16,7 +18,6 @@ import { type VisualElement, } from "motion-dom" import { Axis, Point, invariant } from "motion-utils" -import { animateMotionValue } from "../../animation/interfaces/motion-value" import { addDomEvent } from "../../events/add-dom-event" import { addPointerEvent } from "../../events/add-pointer-event" import { extractEventInfo } from "../../events/event-info" @@ -24,7 +25,6 @@ import { MotionProps } from "../../motion/types" import type { LayoutUpdateData } from "../../projection/node/types" import { getContextWindow } from "../../utils/get-context-window" import { isRefObject } from "../../utils/is-ref-object" -import { addValueToWillChange } from "../../value/use-will-change/add-will-change" import { PanSession } from "../pan/PanSession" import { applyConstraints, diff --git a/packages/framer-motion/src/gestures/drag/index.ts b/packages/framer-motion/src/gestures/drag/index.ts index 70df7bf50e..be540e5d6c 100644 --- a/packages/framer-motion/src/gestures/drag/index.ts +++ b/packages/framer-motion/src/gestures/drag/index.ts @@ -1,6 +1,5 @@ -import type { VisualElement } from "motion-dom" +import { Feature, type VisualElement } from "motion-dom" import { noop } from "motion-utils" -import { Feature } from "../../motion/features/Feature" import { VisualElementDragControls } from "./VisualElementDragControls" export class DragGesture extends Feature { diff --git a/packages/framer-motion/src/gestures/focus.ts b/packages/framer-motion/src/gestures/focus.ts index 98a7a3dc2e..7c5fc1f087 100644 --- a/packages/framer-motion/src/gestures/focus.ts +++ b/packages/framer-motion/src/gestures/focus.ts @@ -1,6 +1,6 @@ +import { Feature } from "motion-dom" import { pipe } from "motion-utils" import { addDomEvent } from "../events/add-dom-event" -import { Feature } from "../motion/features/Feature" export class FocusGesture extends Feature { private isActive = false diff --git a/packages/framer-motion/src/gestures/hover.ts b/packages/framer-motion/src/gestures/hover.ts index 38c93d1fe4..1ac509e379 100644 --- a/packages/framer-motion/src/gestures/hover.ts +++ b/packages/framer-motion/src/gestures/hover.ts @@ -1,6 +1,5 @@ -import { frame, hover, type VisualElement } from "motion-dom" +import { Feature, frame, hover, type VisualElement } from "motion-dom" import { extractEventInfo } from "../events/event-info" -import { Feature } from "../motion/features/Feature" function handleHoverEvent( node: VisualElement, diff --git a/packages/framer-motion/src/gestures/pan/index.ts b/packages/framer-motion/src/gestures/pan/index.ts index c6229c5ecf..42de424e27 100644 --- a/packages/framer-motion/src/gestures/pan/index.ts +++ b/packages/framer-motion/src/gestures/pan/index.ts @@ -1,8 +1,6 @@ -import type { PanInfo } from "motion-dom" -import { frame } from "motion-dom" +import { Feature, frame, type PanInfo } from "motion-dom" import { noop } from "motion-utils" import { addPointerEvent } from "../../events/add-pointer-event" -import { Feature } from "../../motion/features/Feature" import { getContextWindow } from "../../utils/get-context-window" import { PanSession } from "./PanSession" diff --git a/packages/framer-motion/src/gestures/press.ts b/packages/framer-motion/src/gestures/press.ts index 729f6356de..e5d2650896 100644 --- a/packages/framer-motion/src/gestures/press.ts +++ b/packages/framer-motion/src/gestures/press.ts @@ -1,6 +1,5 @@ -import { frame, press, type VisualElement } from "motion-dom" +import { Feature, frame, press, type VisualElement } from "motion-dom" import { extractEventInfo } from "../events/event-info" -import { Feature } from "../motion/features/Feature" function handlePressEvent( node: VisualElement, diff --git a/packages/framer-motion/src/index.ts b/packages/framer-motion/src/index.ts index cf74650801..a9c3b388dc 100644 --- a/packages/framer-motion/src/index.ts +++ b/packages/framer-motion/src/index.ts @@ -74,7 +74,7 @@ export { useAnimation, useAnimationControls, } from "./animation/hooks/use-animation" -export { animateVisualElement } from "./animation/interfaces/visual-element" +export { animateVisualElement } from "motion-dom" export { useIsPresent, usePresence, @@ -88,7 +88,7 @@ export { export { isMotionComponent } from "./motion/utils/is-motion-component" export { unwrapMotionComponent } from "./motion/utils/unwrap-motion-component" export { isValidMotionProp } from "./motion/utils/valid-prop" -export { addScaleCorrectors as addScaleCorrector } from "motion-dom" +export { addScaleCorrector } from "motion-dom" export { useInstantLayoutTransition } from "./projection/use-instant-layout-transition" export { useResetProjection } from "./projection/use-reset-projection" export { buildTransform, visualElementStore, VisualElement } from "motion-dom" @@ -104,7 +104,7 @@ export { usePageInView } from "./utils/use-page-in-view" /** * Appear animations */ -export { optimizedAppearDataAttribute } from "./animation/optimized-appear/data-id" +export { optimizedAppearDataAttribute } from "motion-dom" export { startOptimizedAppearAnimation } from "./animation/optimized-appear/start" /** diff --git a/packages/framer-motion/src/motion/features/Feature.ts b/packages/framer-motion/src/motion/features/Feature.ts deleted file mode 100644 index 83a0152f81..0000000000 --- a/packages/framer-motion/src/motion/features/Feature.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { VisualElement } from "motion-dom" - -export abstract class Feature { - isMounted = false - - node: VisualElement - - constructor(node: VisualElement) { - this.node = node - } - - abstract mount(): void - - abstract unmount(): void - - update(): void {} -} diff --git a/packages/framer-motion/src/motion/features/animation/exit.ts b/packages/framer-motion/src/motion/features/animation/exit.ts index a284236fc6..505d4ee37e 100644 --- a/packages/framer-motion/src/motion/features/animation/exit.ts +++ b/packages/framer-motion/src/motion/features/animation/exit.ts @@ -1,4 +1,4 @@ -import { Feature } from "../Feature" +import { Feature } from "motion-dom" let id = 0 diff --git a/packages/framer-motion/src/motion/features/animation/index.ts b/packages/framer-motion/src/motion/features/animation/index.ts index e5c5386446..269ec7a90b 100644 --- a/packages/framer-motion/src/motion/features/animation/index.ts +++ b/packages/framer-motion/src/motion/features/animation/index.ts @@ -1,7 +1,10 @@ -import { createAnimationState, type VisualElement } from "motion-dom" -import { isAnimationControls } from "../../../animation/utils/is-animation-controls" -import { animateVisualElement } from "../../../animation/interfaces/visual-element" -import { Feature } from "../Feature" +import { + createAnimationState, + Feature, + isAnimationControls, + animateVisualElement, + type VisualElement, +} from "motion-dom" /** * Creates the animate function that will be used by the animation state diff --git a/packages/framer-motion/src/motion/features/types.ts b/packages/framer-motion/src/motion/features/types.ts index fe49e365af..f9ac3e8922 100644 --- a/packages/framer-motion/src/motion/features/types.ts +++ b/packages/framer-motion/src/motion/features/types.ts @@ -1,6 +1,6 @@ +import type { Feature } from "motion-dom" import { CreateVisualElement } from "../../render/types" import { MotionProps } from "../types" -import type { Feature } from "./Feature" import { MeasureLayout } from "./layout/MeasureLayout" interface FeatureClass { diff --git a/packages/framer-motion/src/motion/features/viewport/index.ts b/packages/framer-motion/src/motion/features/viewport/index.ts index ad61447577..c59b8e0c7f 100644 --- a/packages/framer-motion/src/motion/features/viewport/index.ts +++ b/packages/framer-motion/src/motion/features/viewport/index.ts @@ -1,5 +1,5 @@ +import { Feature } from "motion-dom" import { MotionProps } from "../../types" -import { Feature } from "../Feature" import { observeIntersection } from "./observers" const thresholdNames = { diff --git a/packages/framer-motion/src/motion/utils/use-visual-element.ts b/packages/framer-motion/src/motion/utils/use-visual-element.ts index fa980eb2bf..dfedd7a455 100644 --- a/packages/framer-motion/src/motion/utils/use-visual-element.ts +++ b/packages/framer-motion/src/motion/utils/use-visual-element.ts @@ -1,9 +1,13 @@ "use client" -import type { HTMLRenderState, SVGRenderState, VisualElement } from "motion-dom" +import { + optimizedAppearDataAttribute, + type HTMLRenderState, + type SVGRenderState, + type VisualElement, +} from "motion-dom" import * as React from "react" import { useContext, useEffect, useInsertionEffect, useRef } from "react" -import { optimizedAppearDataAttribute } from "../../animation/optimized-appear/data-id" import { LazyContext } from "../../context/LazyContext" import { MotionConfigContext } from "../../context/MotionConfigContext" import { MotionContext } from "../../context/MotionContext" diff --git a/packages/framer-motion/src/projection.ts b/packages/framer-motion/src/projection.ts index fd49f5ff2e..f62f21e2cd 100644 --- a/packages/framer-motion/src/projection.ts +++ b/packages/framer-motion/src/projection.ts @@ -4,7 +4,7 @@ export { calcBoxDelta, correctBorderRadius, correctBoxShadow, - addScaleCorrectors as addScaleCorrector, + addScaleCorrector, frame, frameData, mix, diff --git a/packages/framer-motion/src/projection/node/create-projection-node.ts b/packages/framer-motion/src/projection/node/create-projection-node.ts index 256dd515b2..3c30b8227c 100644 --- a/packages/framer-motion/src/projection/node/create-projection-node.ts +++ b/packages/framer-motion/src/projection/node/create-projection-node.ts @@ -55,7 +55,7 @@ import { SubscriptionManager, } from "motion-utils" import { animateSingleValue } from "../../animation/animate/single-value" -import { getOptimisedAppearId } from "../../animation/optimized-appear/get-appear-id" +import { getOptimisedAppearId } from "motion-dom" import { MotionStyle } from "../../motion/types" import { HTMLVisualElement, ResolvedValues, VisualElement } from "motion-dom" import { FlatTree } from "../../render/utils/flat-tree" diff --git a/packages/framer-motion/src/value/use-will-change/WillChangeMotionValue.ts b/packages/framer-motion/src/value/use-will-change/WillChangeMotionValue.ts index 470a15a3af..1dffa39120 100644 --- a/packages/framer-motion/src/value/use-will-change/WillChangeMotionValue.ts +++ b/packages/framer-motion/src/value/use-will-change/WillChangeMotionValue.ts @@ -1,5 +1,9 @@ -import { acceleratedValues, MotionValue, transformProps } from "motion-dom" -import { WillChange } from "./types" +import { + acceleratedValues, + MotionValue, + transformProps, + type WillChange, +} from "motion-dom" export class WillChangeMotionValue extends MotionValue diff --git a/packages/framer-motion/src/value/use-will-change/__tests__/is.test.ts b/packages/framer-motion/src/value/use-will-change/__tests__/is.test.ts index c568a9562b..e10d3a606c 100644 --- a/packages/framer-motion/src/value/use-will-change/__tests__/is.test.ts +++ b/packages/framer-motion/src/value/use-will-change/__tests__/is.test.ts @@ -1,5 +1,4 @@ -import { MotionValue } from "motion-dom" -import { isWillChangeMotionValue } from "../is" +import { isWillChangeMotionValue, MotionValue } from "motion-dom" import { WillChangeMotionValue } from "../WillChangeMotionValue" describe("isWillChangeMotionValue", () => { diff --git a/packages/framer-motion/src/value/use-will-change/index.ts b/packages/framer-motion/src/value/use-will-change/index.ts index f212e0a84f..eec008d7f9 100644 --- a/packages/framer-motion/src/value/use-will-change/index.ts +++ b/packages/framer-motion/src/value/use-will-change/index.ts @@ -1,8 +1,8 @@ "use client" +import type { WillChange } from "motion-dom" import { useConstant } from "../../utils/use-constant" import { WillChangeMotionValue } from "./WillChangeMotionValue" -import { WillChange } from "./types" export function useWillChange(): WillChange { return useConstant(() => new WillChangeMotionValue("auto")) diff --git a/packages/framer-motion/src/animation/interfaces/motion-value.ts b/packages/motion-dom/src/animation/interfaces/motion-value.ts similarity index 86% rename from packages/framer-motion/src/animation/interfaces/motion-value.ts rename to packages/motion-dom/src/animation/interfaces/motion-value.ts index 3b6c01de9d..319c2713cb 100644 --- a/packages/framer-motion/src/animation/interfaces/motion-value.ts +++ b/packages/motion-dom/src/animation/interfaces/motion-value.ts @@ -1,21 +1,20 @@ -import { - AsyncMotionValueAnimation, - frame, - getValueTransition, - JSAnimation, - makeAnimationInstant, - type AnyResolvedKeyframe, - type MotionValue, - type StartAnimation, - type UnresolvedKeyframes, - type ValueAnimationOptions, - type ValueTransition, - type VisualElement, -} from "motion-dom" import { MotionGlobalConfig, secondsToMilliseconds } from "motion-utils" -import { getFinalKeyframe } from "../animators/waapi/utils/get-final-keyframe" +import { AsyncMotionValueAnimation } from "../AsyncMotionValueAnimation" +import { JSAnimation } from "../JSAnimation" +import type { + AnyResolvedKeyframe, + ValueAnimationOptions, + ValueTransition, +} from "../types" +import type { UnresolvedKeyframes } from "../keyframes/KeyframesResolver" +import { getValueTransition } from "../utils/get-value-transition" +import { makeAnimationInstant } from "../utils/make-animation-instant" import { getDefaultTransition } from "../utils/default-transitions" +import { getFinalKeyframe } from "../utils/get-final-keyframe" import { isTransitionDefined } from "../utils/is-transition-defined" +import { frame } from "../../frameloop" +import type { MotionValue, StartAnimation } from "../../value" +import type { VisualElement } from "../../render/VisualElement" export const animateMotionValue = ( diff --git a/packages/motion-dom/src/animation/interfaces/types.ts b/packages/motion-dom/src/animation/interfaces/types.ts new file mode 100644 index 0000000000..76960b4583 --- /dev/null +++ b/packages/motion-dom/src/animation/interfaces/types.ts @@ -0,0 +1,9 @@ +import type { AnimationType } from "../../render/types" +import type { Transition } from "../types" + +export interface VisualElementAnimationOptions { + delay?: number + transitionOverride?: Transition + custom?: any + type?: AnimationType +} diff --git a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts b/packages/motion-dom/src/animation/interfaces/visual-element-target.ts similarity index 88% rename from packages/framer-motion/src/animation/interfaces/visual-element-target.ts rename to packages/motion-dom/src/animation/interfaces/visual-element-target.ts index 766e4c7094..62db8a3cdb 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts +++ b/packages/motion-dom/src/animation/interfaces/visual-element-target.ts @@ -1,17 +1,15 @@ -import { - AnimationPlaybackControlsWithThen, - frame, - getValueTransition, - positionalKeys, - type AnimationTypeState, - type TargetAndTransition, - type VisualElement, -} from "motion-dom" +import { frame } from "../../frameloop" +import { getValueTransition } from "../utils/get-value-transition" +import { positionalKeys } from "../../render/utils/keys-position" import { setTarget } from "../../render/utils/setters" -import { addValueToWillChange } from "../../value/use-will-change/add-will-change" +import { addValueToWillChange } from "../../value/will-change/add-will-change" import { getOptimisedAppearId } from "../optimized-appear/get-appear-id" import { animateMotionValue } from "./motion-value" import type { VisualElementAnimationOptions } from "./types" +import type { AnimationPlaybackControlsWithThen } from "../types" +import type { TargetAndTransition } from "../../node/types" +import type { AnimationTypeState } from "../../render/utils/animation-state" +import type { VisualElement } from "../../render/VisualElement" /** * Decide whether we should block this animation. Previously, we achieved this diff --git a/packages/framer-motion/src/animation/interfaces/visual-element-variant.ts b/packages/motion-dom/src/animation/interfaces/visual-element-variant.ts similarity index 93% rename from packages/framer-motion/src/animation/interfaces/visual-element-variant.ts rename to packages/motion-dom/src/animation/interfaces/visual-element-variant.ts index 8ab277ec2b..439562d897 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-variant.ts +++ b/packages/motion-dom/src/animation/interfaces/visual-element-variant.ts @@ -1,7 +1,9 @@ -import { DynamicOption, resolveVariant, type VisualElement } from "motion-dom" +import { resolveVariant } from "../../render/utils/resolve-dynamic-variants" import { calcChildStagger } from "../utils/calc-child-stagger" -import { VisualElementAnimationOptions } from "./types" +import type { VisualElementAnimationOptions } from "./types" import { animateTarget } from "./visual-element-target" +import type { DynamicOption } from "../types" +import type { VisualElement } from "../../render/VisualElement" export function animateVariant( visualElement: VisualElement, diff --git a/packages/framer-motion/src/animation/interfaces/visual-element.ts b/packages/motion-dom/src/animation/interfaces/visual-element.ts similarity index 80% rename from packages/framer-motion/src/animation/interfaces/visual-element.ts rename to packages/motion-dom/src/animation/interfaces/visual-element.ts index ba4edcdb45..5c47c10456 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element.ts +++ b/packages/motion-dom/src/animation/interfaces/visual-element.ts @@ -1,5 +1,7 @@ -import { resolveVariant, type AnimationDefinition, type VisualElement } from "motion-dom" -import { VisualElementAnimationOptions } from "./types" +import { resolveVariant } from "../../render/utils/resolve-dynamic-variants" +import type { AnimationDefinition } from "../../node/types" +import type { VisualElement } from "../../render/VisualElement" +import type { VisualElementAnimationOptions } from "./types" import { animateTarget } from "./visual-element-target" import { animateVariant } from "./visual-element-variant" diff --git a/packages/framer-motion/src/animation/optimized-appear/data-id.ts b/packages/motion-dom/src/animation/optimized-appear/data-id.ts similarity index 72% rename from packages/framer-motion/src/animation/optimized-appear/data-id.ts rename to packages/motion-dom/src/animation/optimized-appear/data-id.ts index 873535827a..f9443d26b7 100644 --- a/packages/framer-motion/src/animation/optimized-appear/data-id.ts +++ b/packages/motion-dom/src/animation/optimized-appear/data-id.ts @@ -1,4 +1,4 @@ -import { camelToDash } from "motion-dom" +import { camelToDash } from "../../render/dom/utils/camel-to-dash" export const optimizedAppearDataId = "framerAppearId" diff --git a/packages/framer-motion/src/animation/optimized-appear/get-appear-id.ts b/packages/motion-dom/src/animation/optimized-appear/get-appear-id.ts similarity index 82% rename from packages/framer-motion/src/animation/optimized-appear/get-appear-id.ts rename to packages/motion-dom/src/animation/optimized-appear/get-appear-id.ts index e2878f6deb..be40859350 100644 --- a/packages/framer-motion/src/animation/optimized-appear/get-appear-id.ts +++ b/packages/motion-dom/src/animation/optimized-appear/get-appear-id.ts @@ -1,5 +1,5 @@ import { optimizedAppearDataAttribute } from "./data-id" -import { WithAppearProps } from "./types" +import type { WithAppearProps } from "./types" export function getOptimisedAppearId( visualElement: WithAppearProps diff --git a/packages/framer-motion/src/animation/optimized-appear/types.ts b/packages/motion-dom/src/animation/optimized-appear/types.ts similarity index 93% rename from packages/framer-motion/src/animation/optimized-appear/types.ts rename to packages/motion-dom/src/animation/optimized-appear/types.ts index 654ffb6245..ff7f276558 100644 --- a/packages/framer-motion/src/animation/optimized-appear/types.ts +++ b/packages/motion-dom/src/animation/optimized-appear/types.ts @@ -1,5 +1,5 @@ -import type { Batcher } from "motion-dom" -import { MotionValue } from "motion-dom" +import type { Batcher } from "../../frameloop/types" +import type { MotionValue } from "../../value" import { optimizedAppearDataAttribute } from "./data-id" /** diff --git a/packages/framer-motion/src/animation/utils/calc-child-stagger.ts b/packages/motion-dom/src/animation/utils/calc-child-stagger.ts similarity index 86% rename from packages/framer-motion/src/animation/utils/calc-child-stagger.ts rename to packages/motion-dom/src/animation/utils/calc-child-stagger.ts index b7bbf48e3a..546b629e39 100644 --- a/packages/framer-motion/src/animation/utils/calc-child-stagger.ts +++ b/packages/motion-dom/src/animation/utils/calc-child-stagger.ts @@ -1,4 +1,5 @@ -import { DynamicOption, type VisualElement } from "motion-dom" +import type { DynamicOption } from "../types" +import type { VisualElement } from "../../render/VisualElement" export function calcChildStagger( children: Set, diff --git a/packages/framer-motion/src/animation/utils/default-transitions.ts b/packages/motion-dom/src/animation/utils/default-transitions.ts similarity index 90% rename from packages/framer-motion/src/animation/utils/default-transitions.ts rename to packages/motion-dom/src/animation/utils/default-transitions.ts index f73e4698bb..ad7d48eaed 100644 --- a/packages/framer-motion/src/animation/utils/default-transitions.ts +++ b/packages/motion-dom/src/animation/utils/default-transitions.ts @@ -1,4 +1,5 @@ -import { transformProps, ValueAnimationOptions } from "motion-dom" +import { transformProps } from "../../render/utils/keys-transform" +import type { ValueAnimationOptions } from "../types" const underDampedSpring: Partial = { type: "spring", diff --git a/packages/framer-motion/src/animation/animators/waapi/utils/get-final-keyframe.ts b/packages/motion-dom/src/animation/utils/get-final-keyframe.ts similarity index 89% rename from packages/framer-motion/src/animation/animators/waapi/utils/get-final-keyframe.ts rename to packages/motion-dom/src/animation/utils/get-final-keyframe.ts index bac2f647ad..8ca690f293 100644 --- a/packages/framer-motion/src/animation/animators/waapi/utils/get-final-keyframe.ts +++ b/packages/motion-dom/src/animation/utils/get-final-keyframe.ts @@ -1,4 +1,4 @@ -import { AnimationPlaybackOptions } from "motion-dom" +import type { AnimationPlaybackOptions } from "../types" const isNotNull = (value: unknown) => value !== null diff --git a/packages/framer-motion/src/animation/utils/is-transition-defined.ts b/packages/motion-dom/src/animation/utils/is-transition-defined.ts similarity index 83% rename from packages/framer-motion/src/animation/utils/is-transition-defined.ts rename to packages/motion-dom/src/animation/utils/is-transition-defined.ts index c59664c8bc..8e6e25259d 100644 --- a/packages/framer-motion/src/animation/utils/is-transition-defined.ts +++ b/packages/motion-dom/src/animation/utils/is-transition-defined.ts @@ -1,4 +1,5 @@ -import { type AnyResolvedKeyframe, type Transition } from "motion-dom" +import type { AnyResolvedKeyframe } from "../types" +import type { Transition } from "../types" /** * Decide whether a transition is defined on a given Transition. diff --git a/packages/motion-dom/src/index.ts b/packages/motion-dom/src/index.ts index e33aeeadd5..e92a0b21f9 100644 --- a/packages/motion-dom/src/index.ts +++ b/packages/motion-dom/src/index.ts @@ -11,6 +11,22 @@ export * from "./animation/utils/css-variables-conversion" export * from "./animation/utils/get-value-transition" export * from "./animation/utils/is-css-variable" export * from "./animation/utils/make-animation-instant" +export { getDefaultTransition } from "./animation/utils/default-transitions" +export { isTransitionDefined } from "./animation/utils/is-transition-defined" +export { getFinalKeyframe } from "./animation/utils/get-final-keyframe" +export { calcChildStagger } from "./animation/utils/calc-child-stagger" + +// Animation interfaces +export { animateMotionValue } from "./animation/interfaces/motion-value" +export { animateTarget } from "./animation/interfaces/visual-element-target" +export { animateVariant } from "./animation/interfaces/visual-element-variant" +export { animateVisualElement } from "./animation/interfaces/visual-element" +export type { VisualElementAnimationOptions } from "./animation/interfaces/types" + +// Optimized appear +export { optimizedAppearDataId, optimizedAppearDataAttribute } from "./animation/optimized-appear/data-id" +export { getOptimisedAppearId } from "./animation/optimized-appear/get-appear-id" +export type { WithAppearProps, HandoffFunction } from "./animation/optimized-appear/types" export * from "./animation/generators/inertia" export * from "./animation/generators/keyframes" @@ -69,6 +85,7 @@ export * from "./render/dom/style-set" export * from "./render/svg/types" export * from "./render/utils/keys-position" export * from "./render/utils/keys-transform" +export { isKeyframesTarget } from "./render/utils/is-keyframes-target" export * from "./resize" @@ -120,6 +137,9 @@ export * from "./value/types/utils/animatable-none" export * from "./value/types/utils/find" export * from "./value/types/utils/get-as-type" export * from "./value/utils/is-motion-value" +export type { WillChange } from "./value/will-change/types" +export { isWillChangeMotionValue } from "./value/will-change/is" +export { addValueToWillChange } from "./value/will-change/add-will-change" export * from "./view" export * from "./view/types" @@ -165,7 +185,8 @@ export { resolveVariant } from "./render/utils/resolve-dynamic-variants" export { updateMotionValuesFromProps } from "./render/utils/motion-values" export { variantProps, variantPriorityOrder } from "./render/utils/variant-props" export { isAnimationControls } from "./render/utils/is-animation-controls" -export { isForcedMotionValue, scaleCorrectors, addScaleCorrectors } from "./render/utils/is-forced-motion-value" +export { isForcedMotionValue, scaleCorrectors, addScaleCorrector } from "./render/utils/is-forced-motion-value" +export { setTarget } from "./render/utils/setters" // Reduced motion export { diff --git a/packages/motion-dom/src/render/Feature.ts b/packages/motion-dom/src/render/Feature.ts index 2a36ead46f..f5713f68d5 100644 --- a/packages/motion-dom/src/render/Feature.ts +++ b/packages/motion-dom/src/render/Feature.ts @@ -1,18 +1,16 @@ +import type { VisualElement } from "./VisualElement" + /** * Feature base class for extending VisualElement functionality. * Features are plugins that can be mounted/unmounted to add behavior * like gestures, animations, or layout tracking. */ -export abstract class Feature<_T extends any = any> { +export abstract class Feature { isMounted = false - /** - * A reference to the VisualElement this feature is attached to. - * Typed as any to avoid circular dependencies - will be a VisualElement at runtime. - */ - node: any + node: VisualElement - constructor(node: any) { + constructor(node: VisualElement) { this.node = node } diff --git a/packages/motion-dom/src/render/utils/animation-state.ts b/packages/motion-dom/src/render/utils/animation-state.ts index 08c46003e6..ddadb38cba 100644 --- a/packages/motion-dom/src/render/utils/animation-state.ts +++ b/packages/motion-dom/src/render/utils/animation-state.ts @@ -3,9 +3,9 @@ import type { TargetAndTransition, VariantLabels, } from "../../node/types" -import type { Transition } from "../../animation/types" import type { AnimationType } from "../types" -import { calcChildStagger } from "./calc-child-stagger" +import type { VisualElementAnimationOptions } from "../../animation/interfaces/types" +import { calcChildStagger } from "../../animation/utils/calc-child-stagger" import { getVariantContext } from "./get-variant-context" import { isAnimationControls } from "./is-animation-controls" import { isKeyframesTarget } from "./is-keyframes-target" @@ -14,12 +14,7 @@ import { resolveVariant } from "./resolve-dynamic-variants" import { shallowCompare } from "./shallow-compare" import { variantPriorityOrder } from "./variant-props" -export interface VisualElementAnimationOptions { - delay?: number - transitionOverride?: Transition - custom?: any - type?: AnimationType -} +export type { VisualElementAnimationOptions } export interface AnimationState { animateChanges: (type?: AnimationType) => Promise diff --git a/packages/motion-dom/src/render/utils/calc-child-stagger.ts b/packages/motion-dom/src/render/utils/calc-child-stagger.ts deleted file mode 100644 index f5b4c3ac7f..0000000000 --- a/packages/motion-dom/src/render/utils/calc-child-stagger.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { DynamicOption } from "../../animation/types" - -/** - * Calculate the stagger delay for a child element. - * Uses `any` types for visual elements to avoid circular dependencies. - */ -export function calcChildStagger( - children: Set, - child: any, - delayChildren?: number | DynamicOption, - staggerChildren: number = 0, - staggerDirection: number = 1 -): number { - const index = Array.from(children) - .sort((a, b) => a.sortNodePosition(b)) - .indexOf(child) - const numChildren = children.size - const maxStaggerDuration = (numChildren - 1) * staggerChildren - const delayIsFunction = typeof delayChildren === "function" - - return delayIsFunction - ? delayChildren(index, numChildren) - : staggerDirection === 1 - ? index * staggerChildren - : maxStaggerDuration - index * staggerChildren -} diff --git a/packages/motion-dom/src/render/utils/is-forced-motion-value.ts b/packages/motion-dom/src/render/utils/is-forced-motion-value.ts index 3350dd6c5b..ca9373fa7d 100644 --- a/packages/motion-dom/src/render/utils/is-forced-motion-value.ts +++ b/packages/motion-dom/src/render/utils/is-forced-motion-value.ts @@ -5,9 +5,7 @@ import { addScaleCorrector, } from "../../projection/styles/scale-correction" -// Re-export for backward compatibility -export { scaleCorrectors } -export { addScaleCorrector as addScaleCorrectors } +export { scaleCorrectors, addScaleCorrector } export function isForcedMotionValue( key: string, diff --git a/packages/framer-motion/src/render/utils/setters.ts b/packages/motion-dom/src/render/utils/setters.ts similarity index 73% rename from packages/framer-motion/src/render/utils/setters.ts rename to packages/motion-dom/src/render/utils/setters.ts index cb8992c742..cf6d06c0f7 100644 --- a/packages/framer-motion/src/render/utils/setters.ts +++ b/packages/motion-dom/src/render/utils/setters.ts @@ -1,13 +1,13 @@ -import { - motionValue, - resolveVariant, - type AnimationDefinition, - type AnyResolvedKeyframe, - type UnresolvedValueKeyframe, - type ValueKeyframesDefinition, - type VisualElement, -} from "motion-dom" -import { isKeyframesTarget } from "../../animation/utils/is-keyframes-target" +import { motionValue } from "../../value" +import { resolveVariant } from "./resolve-dynamic-variants" +import { isKeyframesTarget } from "./is-keyframes-target" +import type { AnimationDefinition } from "../../node/types" +import type { + AnyResolvedKeyframe, + UnresolvedValueKeyframe, + ValueKeyframesDefinition, +} from "../../animation/types" +import type { VisualElement } from "../VisualElement" /** * Set VisualElement's MotionValue, creating a new MotionValue for it if diff --git a/packages/framer-motion/src/value/use-will-change/add-will-change.ts b/packages/motion-dom/src/value/will-change/add-will-change.ts similarity index 91% rename from packages/framer-motion/src/value/use-will-change/add-will-change.ts rename to packages/motion-dom/src/value/will-change/add-will-change.ts index 75dad81ce0..94c4658e06 100644 --- a/packages/framer-motion/src/value/use-will-change/add-will-change.ts +++ b/packages/motion-dom/src/value/will-change/add-will-change.ts @@ -1,5 +1,5 @@ -import type { VisualElement } from "motion-dom" import { MotionGlobalConfig } from "motion-utils" +import type { VisualElement } from "../../render/VisualElement" import { isWillChangeMotionValue } from "./is" export function addValueToWillChange( diff --git a/packages/framer-motion/src/value/use-will-change/is.ts b/packages/motion-dom/src/value/will-change/is.ts similarity index 59% rename from packages/framer-motion/src/value/use-will-change/is.ts rename to packages/motion-dom/src/value/will-change/is.ts index 98eac48e40..e48f2d2bd2 100644 --- a/packages/framer-motion/src/value/use-will-change/is.ts +++ b/packages/motion-dom/src/value/will-change/is.ts @@ -1,5 +1,5 @@ -import { isMotionValue } from "motion-dom" -import { WillChange } from "./types" +import { isMotionValue } from "../utils/is-motion-value" +import type { WillChange } from "./types" export function isWillChangeMotionValue(value: any): value is WillChange { return Boolean(isMotionValue(value) && (value as WillChange).add) diff --git a/packages/framer-motion/src/value/use-will-change/types.ts b/packages/motion-dom/src/value/will-change/types.ts similarity index 65% rename from packages/framer-motion/src/value/use-will-change/types.ts rename to packages/motion-dom/src/value/will-change/types.ts index b58b9b7987..cafa43ec61 100644 --- a/packages/framer-motion/src/value/use-will-change/types.ts +++ b/packages/motion-dom/src/value/will-change/types.ts @@ -1,4 +1,4 @@ -import type { MotionValue } from "motion-dom" +import type { MotionValue } from "../index" export interface WillChange extends MotionValue { add(name: string): void From cb08012542229dc07d1188d5bfd26243ded6bf0e Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Mon, 12 Jan 2026 20:37:08 +0100 Subject: [PATCH 05/11] Latest --- .../__tests__/get-final-keyframes.test.ts | 27 ------------------- .../__tests__/is-transition-defined.test.ts} | 0 2 files changed, 27 deletions(-) delete mode 100644 packages/framer-motion/src/animation/animators/waapi/utils/__tests__/get-final-keyframes.test.ts rename packages/{framer-motion/src/animation/utils/__tests__/transitions.test.ts => motion-dom/src/animation/utils/__tests__/is-transition-defined.test.ts} (100%) diff --git a/packages/framer-motion/src/animation/animators/waapi/utils/__tests__/get-final-keyframes.test.ts b/packages/framer-motion/src/animation/animators/waapi/utils/__tests__/get-final-keyframes.test.ts deleted file mode 100644 index ec18ff108e..0000000000 --- a/packages/framer-motion/src/animation/animators/waapi/utils/__tests__/get-final-keyframes.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { getFinalKeyframe } from "../get-final-keyframe" - -describe("getFinalKeyframe", () => { - test("returns final keyframe", () => { - expect(getFinalKeyframe([0, 1], {})).toEqual(1) - expect(getFinalKeyframe([0, 1], { repeat: 1 })).toEqual(1) - expect(getFinalKeyframe([0, 1], { repeat: 2 })).toEqual(1) - expect( - getFinalKeyframe([0, 1], { repeat: 1, repeatType: "loop" }) - ).toEqual(1) - expect( - getFinalKeyframe([0, 1], { repeat: 2, repeatType: "loop" }) - ).toEqual(1) - expect( - getFinalKeyframe([0, 1], { repeat: 1, repeatType: "reverse" }) - ).toEqual(0) - expect( - getFinalKeyframe([0, 1], { repeat: 2, repeatType: "reverse" }) - ).toEqual(1) - expect( - getFinalKeyframe([0, 1], { repeat: 1, repeatType: "mirror" }) - ).toEqual(0) - expect( - getFinalKeyframe([0, 1], { repeat: 2, repeatType: "mirror" }) - ).toEqual(1) - }) -}) diff --git a/packages/framer-motion/src/animation/utils/__tests__/transitions.test.ts b/packages/motion-dom/src/animation/utils/__tests__/is-transition-defined.test.ts similarity index 100% rename from packages/framer-motion/src/animation/utils/__tests__/transitions.test.ts rename to packages/motion-dom/src/animation/utils/__tests__/is-transition-defined.test.ts From 8ea3e4c67494a9307741b214a52edeecf2b6fecc Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Mon, 12 Jan 2026 21:08:06 +0100 Subject: [PATCH 06/11] Latest --- .../src/context/PresenceContext.ts | 14 +------ .../src/motion/features/animation/index.ts | 17 +-------- .../motion/utils/is-forced-motion-value.ts | 14 ------- .../src/render/html/use-props.ts | 3 +- .../render/html/utils/scrape-motion-values.ts | 31 +++------------ .../render/svg/utils/scrape-motion-values.ts | 36 +++--------------- packages/framer-motion/src/render/types.ts | 38 +++---------------- packages/motion-dom/src/index.ts | 2 + .../animation/__tests__/mix-values.test.ts | 2 +- .../geometry/__tests__/conversion.test.ts | 2 +- .../geometry/__tests__/copy.test.ts | 3 +- .../geometry/__tests__/delta-apply.test.ts | 2 +- .../geometry/__tests__/delta-calc.test.ts | 12 ++---- .../geometry/__tests__/operations.test.ts | 3 +- .../styles/__tests__/transform.test.ts | 3 +- .../utils/__tests__/each-axis.test.ts | 2 +- .../src/render/utils/animation-state.ts | 27 ++++++------- 17 files changed, 50 insertions(+), 161 deletions(-) delete mode 100644 packages/framer-motion/src/motion/utils/is-forced-motion-value.ts rename packages/{framer-motion => motion-dom}/src/projection/animation/__tests__/mix-values.test.ts (98%) rename packages/{framer-motion => motion-dom}/src/projection/geometry/__tests__/conversion.test.ts (84%) rename packages/{framer-motion => motion-dom}/src/projection/geometry/__tests__/copy.test.ts (80%) rename packages/{framer-motion => motion-dom}/src/projection/geometry/__tests__/delta-apply.test.ts (96%) rename packages/{framer-motion => motion-dom}/src/projection/geometry/__tests__/delta-calc.test.ts (93%) rename packages/{framer-motion => motion-dom}/src/projection/geometry/__tests__/operations.test.ts (79%) rename packages/{framer-motion => motion-dom}/src/projection/styles/__tests__/transform.test.ts (96%) rename packages/{framer-motion => motion-dom}/src/projection/utils/__tests__/each-axis.test.ts (84%) diff --git a/packages/framer-motion/src/context/PresenceContext.ts b/packages/framer-motion/src/context/PresenceContext.ts index 082f6ce87f..6cbb46e472 100644 --- a/packages/framer-motion/src/context/PresenceContext.ts +++ b/packages/framer-motion/src/context/PresenceContext.ts @@ -1,19 +1,9 @@ "use client" import { createContext } from "react" -import { VariantLabels } from "../motion/types" +import type { PresenceContextProps } from "motion-dom" -/** - * @public - */ -export interface PresenceContextProps { - id: string - isPresent: boolean - register: (id: string | number) => () => void - onExitComplete?: (id: string | number) => void - initial?: false | VariantLabels - custom?: any -} +export type { PresenceContextProps } /** * @public diff --git a/packages/framer-motion/src/motion/features/animation/index.ts b/packages/framer-motion/src/motion/features/animation/index.ts index 269ec7a90b..3e657e0fb6 100644 --- a/packages/framer-motion/src/motion/features/animation/index.ts +++ b/packages/framer-motion/src/motion/features/animation/index.ts @@ -2,24 +2,9 @@ import { createAnimationState, Feature, isAnimationControls, - animateVisualElement, type VisualElement, } from "motion-dom" -/** - * Creates the animate function that will be used by the animation state - * to perform actual animations using framer-motion's animation system. - */ -function makeAnimateFunction(visualElement: VisualElement) { - return (animations: Array<{ animation: any; options?: any }>) => { - return Promise.all( - animations.map(({ animation, options }) => - animateVisualElement(visualElement, animation, options) - ) - ) - } -} - export class AnimationFeature extends Feature { unmountControls?: () => void @@ -30,7 +15,7 @@ export class AnimationFeature extends Feature { */ constructor(node: VisualElement) { super(node) - node.animationState ||= createAnimationState(node, makeAnimateFunction) + node.animationState ||= createAnimationState(node) } updateAnimationControlsSubscription() { diff --git a/packages/framer-motion/src/motion/utils/is-forced-motion-value.ts b/packages/framer-motion/src/motion/utils/is-forced-motion-value.ts deleted file mode 100644 index 19abe767fc..0000000000 --- a/packages/framer-motion/src/motion/utils/is-forced-motion-value.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { scaleCorrectors, transformProps } from "motion-dom" -import { MotionProps } from "../.." - -export function isForcedMotionValue( - key: string, - { layout, layoutId }: MotionProps -) { - return ( - transformProps.has(key) || - key.startsWith("origin") || - ((layout || layoutId !== undefined) && - (!!scaleCorrectors[key] || key === "opacity")) - ) -} diff --git a/packages/framer-motion/src/render/html/use-props.ts b/packages/framer-motion/src/render/html/use-props.ts index 164f831181..9d7c61b40f 100644 --- a/packages/framer-motion/src/render/html/use-props.ts +++ b/packages/framer-motion/src/render/html/use-props.ts @@ -1,9 +1,8 @@ "use client" -import { AnyResolvedKeyframe, buildHTMLStyles, isMotionValue, MotionValue } from "motion-dom" +import { AnyResolvedKeyframe, buildHTMLStyles, isForcedMotionValue, isMotionValue, MotionValue } from "motion-dom" import { HTMLProps, useMemo } from "react" import { MotionProps } from "../../motion/types" -import { isForcedMotionValue } from "../../motion/utils/is-forced-motion-value" import { ResolvedValues } from "../types" import { createHtmlRenderState } from "./utils/create-render-state" diff --git a/packages/framer-motion/src/render/html/utils/scrape-motion-values.ts b/packages/framer-motion/src/render/html/utils/scrape-motion-values.ts index c06bedacc3..d6605c01e3 100644 --- a/packages/framer-motion/src/render/html/utils/scrape-motion-values.ts +++ b/packages/framer-motion/src/render/html/utils/scrape-motion-values.ts @@ -1,26 +1,7 @@ -import { isMotionValue, type VisualElement } from "motion-dom" -import { MotionProps, MotionStyle } from "../../../motion/types" -import { isForcedMotionValue } from "../../../motion/utils/is-forced-motion-value" +import { + scrapeHTMLMotionValuesFromProps, + type ScrapeMotionValuesFromProps, +} from "motion-dom" -export function scrapeMotionValuesFromProps( - props: MotionProps, - prevProps: MotionProps, - visualElement?: VisualElement -) { - const { style } = props - const newValues: { [key: string]: any } = {} - - for (const key in style) { - if ( - isMotionValue(style[key as keyof MotionStyle]) || - (prevProps.style && - isMotionValue(prevProps.style[key as keyof MotionStyle])) || - isForcedMotionValue(key, props) || - visualElement?.getValue(key)?.liveStyle !== undefined - ) { - newValues[key] = style[key as keyof MotionStyle] - } - } - - return newValues -} +export const scrapeMotionValuesFromProps = + scrapeHTMLMotionValuesFromProps as ScrapeMotionValuesFromProps diff --git a/packages/framer-motion/src/render/svg/utils/scrape-motion-values.ts b/packages/framer-motion/src/render/svg/utils/scrape-motion-values.ts index 631bc2af59..4c9716f134 100644 --- a/packages/framer-motion/src/render/svg/utils/scrape-motion-values.ts +++ b/packages/framer-motion/src/render/svg/utils/scrape-motion-values.ts @@ -1,31 +1,7 @@ -import { isMotionValue, transformPropOrder, type VisualElement } from "motion-dom" -import { MotionProps } from "../../../motion/types" -import { scrapeMotionValuesFromProps as scrapeHTMLMotionValuesFromProps } from "../../html/utils/scrape-motion-values" +import { + scrapeSVGMotionValuesFromProps, + type ScrapeMotionValuesFromProps, +} from "motion-dom" -export function scrapeMotionValuesFromProps( - props: MotionProps, - prevProps: MotionProps, - visualElement?: VisualElement -) { - const newValues = scrapeHTMLMotionValuesFromProps( - props, - prevProps, - visualElement - ) - - for (const key in props) { - if ( - isMotionValue(props[key as keyof typeof props]) || - isMotionValue(prevProps[key as keyof typeof prevProps]) - ) { - const targetKey = - transformPropOrder.indexOf(key) !== -1 - ? "attr" + key.charAt(0).toUpperCase() + key.substring(1) - : key - - newValues[targetKey] = props[key as keyof typeof props] - } - } - - return newValues -} +export const scrapeMotionValuesFromProps = + scrapeSVGMotionValuesFromProps as ScrapeMotionValuesFromProps diff --git a/packages/framer-motion/src/render/types.ts b/packages/framer-motion/src/render/types.ts index e63bbe4031..9f08769805 100644 --- a/packages/framer-motion/src/render/types.ts +++ b/packages/framer-motion/src/render/types.ts @@ -2,16 +2,19 @@ import { AnyResolvedKeyframe, MotionValue, ResolvedValues, - type AnimationDefinition, type VisualElement, + type VisualElementEventCallbacks, + type LayoutLifecycles, + type UseRenderState, } from "motion-dom" -import type { Axis, Box } from "motion-utils" import { ReducedMotionConfig } from "../context/MotionConfigContext" import type { PresenceContextProps } from "../context/PresenceContext" import { MotionProps } from "../motion/types" import { VisualState } from "../motion/utils/use-visual-state" import { DOMMotionComponents } from "./dom/types" +export type { VisualElementEventCallbacks, LayoutLifecycles, UseRenderState } + export type ScrapeMotionValuesFromProps = ( props: MotionProps, prevProps: MotionProps, @@ -20,8 +23,6 @@ export type ScrapeMotionValuesFromProps = ( [key: string]: MotionValue | AnyResolvedKeyframe } -export type UseRenderState = () => RenderState - export interface VisualElementOptions { visualState: VisualState parent?: VisualElement @@ -40,35 +41,6 @@ export interface VisualElementOptions { // Re-export ResolvedValues from motion-dom for backward compatibility export type { ResolvedValues } -export interface VisualElementEventCallbacks { - BeforeLayoutMeasure: () => void - LayoutMeasure: (layout: Box, prevLayout?: Box) => void - LayoutUpdate: (layout: Axis, prevLayout: Axis) => void - Update: (latest: ResolvedValues) => void - AnimationStart: (definition: AnimationDefinition) => void - AnimationComplete: (definition: AnimationDefinition) => void - LayoutAnimationStart: () => void - LayoutAnimationComplete: () => void - SetAxisTarget: () => void - Unmount: () => void -} - -export interface LayoutLifecycles { - onBeforeLayoutMeasure?(box: Box): void - - onLayoutMeasure?(box: Box, prevBox: Box): void - - /** - * @internal - */ - onLayoutAnimationStart?(): void - - /** - * @internal - */ - onLayoutAnimationComplete?(): void -} - export type CreateVisualElement< Props = {}, TagName extends keyof DOMMotionComponents | string = "div" diff --git a/packages/motion-dom/src/index.ts b/packages/motion-dom/src/index.ts index e92a0b21f9..e71c30c30d 100644 --- a/packages/motion-dom/src/index.ts +++ b/packages/motion-dom/src/index.ts @@ -220,11 +220,13 @@ export { mixValues } from "./projection/animation/mix-values" export { buildHTMLStyles } from "./render/html/utils/build-styles" export { buildTransform } from "./render/html/utils/build-transform" export { renderHTML } from "./render/html/utils/render" +export { scrapeMotionValuesFromProps as scrapeHTMLMotionValuesFromProps } from "./render/html/utils/scrape-motion-values" export { buildSVGAttrs } from "./render/svg/utils/build-attrs" export { renderSVG } from "./render/svg/utils/render" export { buildSVGPath } from "./render/svg/utils/path" export { camelCaseAttributes } from "./render/svg/utils/camel-case-attrs" export { isSVGTag } from "./render/svg/utils/is-svg-tag" +export { scrapeMotionValuesFromProps as scrapeSVGMotionValuesFromProps } from "./render/svg/utils/scrape-motion-values" export { camelToDash } from "./render/dom/utils/camel-to-dash" /** diff --git a/packages/framer-motion/src/projection/animation/__tests__/mix-values.test.ts b/packages/motion-dom/src/projection/animation/__tests__/mix-values.test.ts similarity index 98% rename from packages/framer-motion/src/projection/animation/__tests__/mix-values.test.ts rename to packages/motion-dom/src/projection/animation/__tests__/mix-values.test.ts index b95a38d468..7c98a2b7bd 100644 --- a/packages/framer-motion/src/projection/animation/__tests__/mix-values.test.ts +++ b/packages/motion-dom/src/projection/animation/__tests__/mix-values.test.ts @@ -1,4 +1,4 @@ -import { mixValues } from "motion-dom" +import { mixValues } from "../mix-values" describe("mixValues", () => { test("mixes borderRadius numbers", () => { diff --git a/packages/framer-motion/src/projection/geometry/__tests__/conversion.test.ts b/packages/motion-dom/src/projection/geometry/__tests__/conversion.test.ts similarity index 84% rename from packages/framer-motion/src/projection/geometry/__tests__/conversion.test.ts rename to packages/motion-dom/src/projection/geometry/__tests__/conversion.test.ts index a27d0c4a9b..5ed2dbe800 100644 --- a/packages/framer-motion/src/projection/geometry/__tests__/conversion.test.ts +++ b/packages/motion-dom/src/projection/geometry/__tests__/conversion.test.ts @@ -1,4 +1,4 @@ -import { convertBoundingBoxToBox } from "motion-dom" +import { convertBoundingBoxToBox } from "../conversion" describe("convertBoundingBoxToBox", () => { it("Correctly converts a bounding box into a box", () => { diff --git a/packages/framer-motion/src/projection/geometry/__tests__/copy.test.ts b/packages/motion-dom/src/projection/geometry/__tests__/copy.test.ts similarity index 80% rename from packages/framer-motion/src/projection/geometry/__tests__/copy.test.ts rename to packages/motion-dom/src/projection/geometry/__tests__/copy.test.ts index f042708cf5..6695ac73ff 100644 --- a/packages/framer-motion/src/projection/geometry/__tests__/copy.test.ts +++ b/packages/motion-dom/src/projection/geometry/__tests__/copy.test.ts @@ -1,4 +1,5 @@ -import { copyBoxInto, createBox } from "motion-dom" +import { copyBoxInto } from "../copy" +import { createBox } from "../models" describe("copyBoxInto", () => { it("copies one box into an existing box", () => { diff --git a/packages/framer-motion/src/projection/geometry/__tests__/delta-apply.test.ts b/packages/motion-dom/src/projection/geometry/__tests__/delta-apply.test.ts similarity index 96% rename from packages/framer-motion/src/projection/geometry/__tests__/delta-apply.test.ts rename to packages/motion-dom/src/projection/geometry/__tests__/delta-apply.test.ts index f87abee72c..56ce365b89 100644 --- a/packages/framer-motion/src/projection/geometry/__tests__/delta-apply.test.ts +++ b/packages/motion-dom/src/projection/geometry/__tests__/delta-apply.test.ts @@ -1,4 +1,4 @@ -import { scalePoint, applyPointDelta, applyAxisDelta } from "motion-dom" +import { scalePoint, applyPointDelta, applyAxisDelta } from "../delta-apply" describe("scalePoint", () => { test("correctly scales a point based on a factor and an originPoint", () => { diff --git a/packages/framer-motion/src/projection/geometry/__tests__/delta-calc.test.ts b/packages/motion-dom/src/projection/geometry/__tests__/delta-calc.test.ts similarity index 93% rename from packages/framer-motion/src/projection/geometry/__tests__/delta-calc.test.ts rename to packages/motion-dom/src/projection/geometry/__tests__/delta-calc.test.ts index b2294367d7..3aaf66c102 100644 --- a/packages/framer-motion/src/projection/geometry/__tests__/delta-calc.test.ts +++ b/packages/motion-dom/src/projection/geometry/__tests__/delta-calc.test.ts @@ -1,12 +1,6 @@ -import { - isNear, - calcAxisDelta, - calcRelativeBox, - calcRelativePosition, - applyAxisDelta, - createBox, - createDelta, -} from "motion-dom" +import { isNear, calcAxisDelta, calcRelativeBox, calcRelativePosition } from "../delta-calc" +import { applyAxisDelta } from "../delta-apply" +import { createBox, createDelta } from "../models" describe("isNear", () => { test("Correctly indicate when the provided value is within maxDistance of the provided target", () => { diff --git a/packages/framer-motion/src/projection/geometry/__tests__/operations.test.ts b/packages/motion-dom/src/projection/geometry/__tests__/operations.test.ts similarity index 79% rename from packages/framer-motion/src/projection/geometry/__tests__/operations.test.ts rename to packages/motion-dom/src/projection/geometry/__tests__/operations.test.ts index 68ff9501f6..ce5ef11cd8 100644 --- a/packages/framer-motion/src/projection/geometry/__tests__/operations.test.ts +++ b/packages/motion-dom/src/projection/geometry/__tests__/operations.test.ts @@ -1,4 +1,5 @@ -import { createAxis, translateAxis } from "motion-dom" +import { createAxis } from "../models" +import { translateAxis } from "../delta-apply" describe("translateAxis", () => { it("applies a translation to an Axis", () => { diff --git a/packages/framer-motion/src/projection/styles/__tests__/transform.test.ts b/packages/motion-dom/src/projection/styles/__tests__/transform.test.ts similarity index 96% rename from packages/framer-motion/src/projection/styles/__tests__/transform.test.ts rename to packages/motion-dom/src/projection/styles/__tests__/transform.test.ts index 7a19a827e2..ca4b0f5ff5 100644 --- a/packages/framer-motion/src/projection/styles/__tests__/transform.test.ts +++ b/packages/motion-dom/src/projection/styles/__tests__/transform.test.ts @@ -1,4 +1,5 @@ -import { buildProjectionTransform, createDelta } from "motion-dom" +import { buildProjectionTransform } from "../transform" +import { createDelta } from "../../geometry/models" describe("buildProjectionTransform", () => { it("Returns 'none' when no transform required", () => { diff --git a/packages/framer-motion/src/projection/utils/__tests__/each-axis.test.ts b/packages/motion-dom/src/projection/utils/__tests__/each-axis.test.ts similarity index 84% rename from packages/framer-motion/src/projection/utils/__tests__/each-axis.test.ts rename to packages/motion-dom/src/projection/utils/__tests__/each-axis.test.ts index bca5de770d..cf5f3290f0 100644 --- a/packages/framer-motion/src/projection/utils/__tests__/each-axis.test.ts +++ b/packages/motion-dom/src/projection/utils/__tests__/each-axis.test.ts @@ -1,4 +1,4 @@ -import { eachAxis } from "motion-dom" +import { eachAxis } from "../each-axis" describe("eachAxis", () => { it("calls a function, once for each axis", () => { diff --git a/packages/motion-dom/src/render/utils/animation-state.ts b/packages/motion-dom/src/render/utils/animation-state.ts index ddadb38cba..b1cc01bc74 100644 --- a/packages/motion-dom/src/render/utils/animation-state.ts +++ b/packages/motion-dom/src/render/utils/animation-state.ts @@ -5,6 +5,7 @@ import type { } from "../../node/types" import type { AnimationType } from "../types" import type { VisualElementAnimationOptions } from "../../animation/interfaces/types" +import { animateVisualElement } from "../../animation/interfaces/visual-element" import { calcChildStagger } from "../../animation/utils/calc-child-stagger" import { getVariantContext } from "./get-variant-context" import { isAnimationControls } from "./is-animation-controls" @@ -44,20 +45,18 @@ const numAnimationTypes = variantPriorityOrder.length */ export type AnimateFunction = (animations: DefinitionAndOptions[]) => Promise -/** - * Type for the function that creates an animate function for a visual element. - */ -export type MakeAnimateFunction = (visualElement: any) => AnimateFunction - -function defaultAnimateList(_visualElement: any) { - return (_animations: DefinitionAndOptions[]) => Promise.resolve() +function createAnimateFunction(visualElement: any): AnimateFunction { + return (animations: DefinitionAndOptions[]) => { + return Promise.all( + animations.map(({ animation, options }) => + animateVisualElement(visualElement, animation, options) + ) + ) + } } -export function createAnimationState( - visualElement: any, - makeAnimateFunction: MakeAnimateFunction = defaultAnimateList -): AnimationState { - let animate = makeAnimateFunction(visualElement) +export function createAnimationState(visualElement: any): AnimationState { + let animate = createAnimateFunction(visualElement) let state = createState() let isInitialRender = true @@ -91,7 +90,9 @@ export function createAnimationState( * This just allows us to inject mocked animation functions * @internal */ - function setAnimateFunction(makeAnimator: MakeAnimateFunction) { + function setAnimateFunction( + makeAnimator: (visualElement: any) => AnimateFunction + ) { animate = makeAnimator(visualElement) } From 43d758873e339896a7cac4859a847b881c0ae74c Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Mon, 12 Jan 2026 21:13:13 +0100 Subject: [PATCH 07/11] Remove duplicate isAnimationControls from framer-motion Import from motion-dom instead of having a local copy. Co-Authored-By: Claude Opus 4.5 --- .../src/animation/utils/is-animation-controls.ts | 9 --------- .../framer-motion/src/motion/utils/use-visual-state.ts | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 packages/framer-motion/src/animation/utils/is-animation-controls.ts diff --git a/packages/framer-motion/src/animation/utils/is-animation-controls.ts b/packages/framer-motion/src/animation/utils/is-animation-controls.ts deleted file mode 100644 index 5029b5a3bd..0000000000 --- a/packages/framer-motion/src/animation/utils/is-animation-controls.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { LegacyAnimationControls } from "motion-dom" - -export function isAnimationControls(v?: unknown): v is LegacyAnimationControls { - return ( - v !== null && - typeof v === "object" && - typeof (v as LegacyAnimationControls).start === "function" - ) -} diff --git a/packages/framer-motion/src/motion/utils/use-visual-state.ts b/packages/framer-motion/src/motion/utils/use-visual-state.ts index 7fd3356cab..32ae2f739d 100644 --- a/packages/framer-motion/src/motion/utils/use-visual-state.ts +++ b/packages/framer-motion/src/motion/utils/use-visual-state.ts @@ -2,13 +2,13 @@ import { AnyResolvedKeyframe, + isAnimationControls, isControllingVariants as checkIsControllingVariants, isVariantNode as checkIsVariantNode, ResolvedValues, resolveVariantFromProps, } from "motion-dom" import { useContext } from "react" -import { isAnimationControls } from "../../animation/utils/is-animation-controls" import { MotionContext, MotionContextProps } from "../../context/MotionContext" import { PresenceContext, From c4376e3cbc0ec46fd4d3100a16e81735bf319135 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 13 Jan 2026 09:26:58 +0100 Subject: [PATCH 08/11] Remove duplicate code and use motion-dom directly - Remove scrape-motion-values wrapper files from framer-motion - Update imports to use motion-dom exports directly - Import TransformOrigin and HTMLRenderState from motion-dom - Move scale-correction and svg scrape-motion-values tests to motion-dom Co-Authored-By: Claude Opus 4.5 --- .../framer-motion/src/render/html/types.ts | 34 ++----------------- .../src/render/html/use-html-visual-state.ts | 4 +-- .../render/html/utils/scrape-motion-values.ts | 7 ---- .../src/render/svg/use-svg-visual-state.ts | 4 +-- .../render/svg/utils/scrape-motion-values.ts | 7 ---- .../styles/__tests__/scale-correction.test.ts | 26 +++++++++----- .../__tests__/scrape-motion-values.test.ts | 2 +- 7 files changed, 24 insertions(+), 60 deletions(-) delete mode 100644 packages/framer-motion/src/render/html/utils/scrape-motion-values.ts delete mode 100644 packages/framer-motion/src/render/svg/utils/scrape-motion-values.ts rename packages/{framer-motion => motion-dom}/src/projection/styles/__tests__/scale-correction.test.ts (83%) rename packages/{framer-motion => motion-dom}/src/render/svg/utils/__tests__/scrape-motion-values.test.ts (94%) diff --git a/packages/framer-motion/src/render/html/types.ts b/packages/framer-motion/src/render/html/types.ts index 4ac2dc53a0..5bdde3fb90 100644 --- a/packages/framer-motion/src/render/html/types.ts +++ b/packages/framer-motion/src/render/html/types.ts @@ -1,39 +1,9 @@ -import { ResolvedValues } from "../types" +import { type TransformOrigin, type HTMLRenderState } from "motion-dom" import { PropsWithoutRef, RefAttributes, JSX } from "react" import { MotionProps } from "../../motion/types" import { HTMLElements } from "./supported-elements" -export interface TransformOrigin { - originX?: number | string - originY?: number | string - originZ?: number | string -} - -export interface HTMLRenderState { - /** - * A mutable record of transforms we want to apply directly to the rendered Element - * every frame. We use a mutable data structure to reduce GC during animations. - */ - transform: ResolvedValues - - /** - * A mutable record of transform origins we want to apply directly to the rendered Element - * every frame. We use a mutable data structure to reduce GC during animations. - */ - transformOrigin: TransformOrigin - - /** - * A mutable record of styles we want to apply directly to the rendered Element - * every frame. We use a mutable data structure to reduce GC during animations. - */ - style: ResolvedValues - - /** - * A mutable record of CSS variables we want to apply directly to the rendered Element - * every frame. We use a mutable data structure to reduce GC during animations. - */ - vars: ResolvedValues -} +export type { TransformOrigin, HTMLRenderState } /** * @public diff --git a/packages/framer-motion/src/render/html/use-html-visual-state.ts b/packages/framer-motion/src/render/html/use-html-visual-state.ts index 4c4c10e68f..c00d46d7dd 100644 --- a/packages/framer-motion/src/render/html/use-html-visual-state.ts +++ b/packages/framer-motion/src/render/html/use-html-visual-state.ts @@ -1,10 +1,10 @@ "use client" +import { scrapeHTMLMotionValuesFromProps } from "motion-dom" import { makeUseVisualState } from "../../motion/utils/use-visual-state" import { createHtmlRenderState } from "./utils/create-render-state" -import { scrapeMotionValuesFromProps } from "./utils/scrape-motion-values" export const useHTMLVisualState = /*@__PURE__*/ makeUseVisualState({ - scrapeMotionValuesFromProps, + scrapeMotionValuesFromProps: scrapeHTMLMotionValuesFromProps, createRenderState: createHtmlRenderState, }) diff --git a/packages/framer-motion/src/render/html/utils/scrape-motion-values.ts b/packages/framer-motion/src/render/html/utils/scrape-motion-values.ts deleted file mode 100644 index d6605c01e3..0000000000 --- a/packages/framer-motion/src/render/html/utils/scrape-motion-values.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { - scrapeHTMLMotionValuesFromProps, - type ScrapeMotionValuesFromProps, -} from "motion-dom" - -export const scrapeMotionValuesFromProps = - scrapeHTMLMotionValuesFromProps as ScrapeMotionValuesFromProps diff --git a/packages/framer-motion/src/render/svg/use-svg-visual-state.ts b/packages/framer-motion/src/render/svg/use-svg-visual-state.ts index e6806cab8c..09f29d0fce 100644 --- a/packages/framer-motion/src/render/svg/use-svg-visual-state.ts +++ b/packages/framer-motion/src/render/svg/use-svg-visual-state.ts @@ -1,10 +1,10 @@ "use client" +import { scrapeSVGMotionValuesFromProps } from "motion-dom" import { makeUseVisualState } from "../../motion/utils/use-visual-state" import { createSvgRenderState } from "./utils/create-render-state" -import { scrapeMotionValuesFromProps as scrapeSVGProps } from "./utils/scrape-motion-values" export const useSVGVisualState = /*@__PURE__*/ makeUseVisualState({ - scrapeMotionValuesFromProps: scrapeSVGProps, + scrapeMotionValuesFromProps: scrapeSVGMotionValuesFromProps, createRenderState: createSvgRenderState, }) diff --git a/packages/framer-motion/src/render/svg/utils/scrape-motion-values.ts b/packages/framer-motion/src/render/svg/utils/scrape-motion-values.ts deleted file mode 100644 index 4c9716f134..0000000000 --- a/packages/framer-motion/src/render/svg/utils/scrape-motion-values.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { - scrapeSVGMotionValuesFromProps, - type ScrapeMotionValuesFromProps, -} from "motion-dom" - -export const scrapeMotionValuesFromProps = - scrapeSVGMotionValuesFromProps as ScrapeMotionValuesFromProps diff --git a/packages/framer-motion/src/projection/styles/__tests__/scale-correction.test.ts b/packages/motion-dom/src/projection/styles/__tests__/scale-correction.test.ts similarity index 83% rename from packages/framer-motion/src/projection/styles/__tests__/scale-correction.test.ts rename to packages/motion-dom/src/projection/styles/__tests__/scale-correction.test.ts index 39f108adec..c363b1b383 100644 --- a/packages/framer-motion/src/projection/styles/__tests__/scale-correction.test.ts +++ b/packages/motion-dom/src/projection/styles/__tests__/scale-correction.test.ts @@ -1,10 +1,18 @@ -import { - correctBorderRadius, - pixelsToPercent, - correctBoxShadow, -} from "motion-dom" -import { createTestNode } from "../../node/__tests__/TestProjectionNode" -import { IProjectionNode } from "../../node/types" +import { correctBorderRadius, pixelsToPercent } from "../scale-border-radius" +import { correctBoxShadow } from "../scale-box-shadow" + +interface TestNode { + target?: { x: { min: number; max: number }; y: { min: number; max: number } } + projectionDelta?: { + x: { scale: number; translate: number; origin: number; originPoint: number } + y: { scale: number; translate: number; origin: number; originPoint: number } + } + treeScale?: { x: number; y: number } +} + +function createTestNode(): TestNode { + return {} +} describe("pixelsToPercent", () => { test("Correctly converts pixels to percent", () => { @@ -17,7 +25,7 @@ describe("pixelsToPercent", () => { }) describe("correctBorderRadius", () => { - let node: IProjectionNode + let node: TestNode beforeEach(() => { node = createTestNode() }) @@ -48,7 +56,7 @@ describe("correctBorderRadius", () => { }) describe("correctBoxShadow", () => { - let node: IProjectionNode + let node: TestNode beforeEach(() => { node = createTestNode() node.projectionDelta = { diff --git a/packages/framer-motion/src/render/svg/utils/__tests__/scrape-motion-values.test.ts b/packages/motion-dom/src/render/svg/utils/__tests__/scrape-motion-values.test.ts similarity index 94% rename from packages/framer-motion/src/render/svg/utils/__tests__/scrape-motion-values.test.ts rename to packages/motion-dom/src/render/svg/utils/__tests__/scrape-motion-values.test.ts index 37af044158..3f18bcd12b 100644 --- a/packages/framer-motion/src/render/svg/utils/__tests__/scrape-motion-values.test.ts +++ b/packages/motion-dom/src/render/svg/utils/__tests__/scrape-motion-values.test.ts @@ -1,4 +1,4 @@ -import { motionValue } from "motion-dom" +import { motionValue } from "../../../../value" import { scrapeMotionValuesFromProps } from "../scrape-motion-values" describe("SVG scrapeMotionValuesFromProps", () => { From cd55787404d73684c8e94353794c7848fe7ea454 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 13 Jan 2026 10:16:58 +0100 Subject: [PATCH 09/11] Move projection node system to motion-dom Move the entire projection node system from framer-motion to motion-dom to enable vanilla JS layout animations without React dependency. Files moved: - create-projection-node.ts, types.ts, state.ts, group.ts - HTMLProjectionNode.ts, DocumentProjectionNode.ts - stack.ts, flat-tree.ts, compare-by-depth.ts - delay.ts, add-dom-event.ts, resolve-motion-value.ts, single-value.ts - Projection node tests Co-Authored-By: Claude Opus 4.5 --- .../src/animation/animate/subject.ts | 2 +- .../src/context/LayoutGroupContext.ts | 2 +- .../src/context/SwitchLayoutGroupContext.ts | 3 +- packages/framer-motion/src/dom.ts | 2 +- .../src/events/add-pointer-event.ts | 2 +- .../framer-motion/src/events/use-dom-event.ts | 2 +- .../drag/VisualElementDragControls.ts | 3 +- packages/framer-motion/src/gestures/focus.ts | 3 +- packages/framer-motion/src/index.ts | 8 +- .../src/motion/__tests__/static-prop.test.tsx | 3 +- .../src/motion/features/layout.ts | 2 +- .../motion/features/layout/MeasureLayout.tsx | 3 +- .../src/motion/utils/use-visual-element.ts | 2 +- .../src/motion/utils/use-visual-state.ts | 2 +- packages/framer-motion/src/projection.ts | 5 +- .../use-instant-layout-transition.ts | 2 +- .../src/projection/use-reset-projection.ts | 2 +- .../render/utils/__tests__/flat-tree.test.ts | 3 +- .../src/utils/__tests__/delay.test.ts | 2 +- .../src/value/__tests__/unwrap-value.test.ts | 3 +- .../src/animation/animate/single-value.ts | 14 ++-- .../src/events/add-dom-event.ts | 0 packages/motion-dom/src/index.ts | 39 +++++++++ .../projection/node/DocumentProjectionNode.ts | 0 .../src/projection/node/HTMLProjectionNode.ts | 0 .../node/__tests__/TestProjectionNode.ts | 2 +- .../projection/node/__tests__/group.test.ts | 0 .../projection/node/__tests__/node.test.ts | 4 +- .../src/projection/node/__tests__/utils.ts | 14 ++++ .../projection/node/create-projection-node.ts | 80 +++++++++---------- .../src/projection/node/group.ts | 0 .../src/projection/node/state.ts | 0 .../src/projection/node/types.ts | 26 +++++- .../src/projection/shared/stack.ts | 13 --- .../src/projection}/utils/compare-by-depth.ts | 2 +- .../src/projection}/utils/flat-tree.ts | 0 .../src/utils/delay.ts | 4 +- .../src/value/utils/resolve-motion-value.ts | 6 +- 38 files changed, 156 insertions(+), 104 deletions(-) rename packages/{framer-motion => motion-dom}/src/animation/animate/single-value.ts (76%) rename packages/{framer-motion => motion-dom}/src/events/add-dom-event.ts (100%) rename packages/{framer-motion => motion-dom}/src/projection/node/DocumentProjectionNode.ts (100%) rename packages/{framer-motion => motion-dom}/src/projection/node/HTMLProjectionNode.ts (100%) rename packages/{framer-motion => motion-dom}/src/projection/node/__tests__/TestProjectionNode.ts (93%) rename packages/{framer-motion => motion-dom}/src/projection/node/__tests__/group.test.ts (100%) rename packages/{framer-motion => motion-dom}/src/projection/node/__tests__/node.test.ts (98%) create mode 100644 packages/motion-dom/src/projection/node/__tests__/utils.ts rename packages/{framer-motion => motion-dom}/src/projection/node/create-projection-node.ts (97%) rename packages/{framer-motion => motion-dom}/src/projection/node/group.ts (100%) rename packages/{framer-motion => motion-dom}/src/projection/node/state.ts (100%) rename packages/{framer-motion => motion-dom}/src/projection/node/types.ts (84%) rename packages/{framer-motion => motion-dom}/src/projection/shared/stack.ts (81%) rename packages/{framer-motion/src/render => motion-dom/src/projection}/utils/compare-by-depth.ts (69%) rename packages/{framer-motion/src/render => motion-dom/src/projection}/utils/flat-tree.ts (100%) rename packages/{framer-motion => motion-dom}/src/utils/delay.ts (82%) rename packages/{framer-motion => motion-dom}/src/value/utils/resolve-motion-value.ts (63%) diff --git a/packages/framer-motion/src/animation/animate/subject.ts b/packages/framer-motion/src/animation/animate/subject.ts index 605879385d..fc09b0b7d2 100644 --- a/packages/framer-motion/src/animation/animate/subject.ts +++ b/packages/framer-motion/src/animation/animate/subject.ts @@ -21,7 +21,7 @@ import { } from "../utils/create-visual-element" import { isDOMKeyframes } from "../utils/is-dom-keyframes" import { resolveSubjects } from "./resolve-subjects" -import { animateSingleValue } from "./single-value" +import { animateSingleValue } from "motion-dom" export type AnimationSubject = Element | MotionValue | any diff --git a/packages/framer-motion/src/context/LayoutGroupContext.ts b/packages/framer-motion/src/context/LayoutGroupContext.ts index 6d4dde23d2..bcdc066efb 100644 --- a/packages/framer-motion/src/context/LayoutGroupContext.ts +++ b/packages/framer-motion/src/context/LayoutGroupContext.ts @@ -1,7 +1,7 @@ "use client" import { createContext } from "react" -import { NodeGroup } from "../projection/node/group" +import type { NodeGroup } from "motion-dom" export interface LayoutGroupContextProps { id?: string diff --git a/packages/framer-motion/src/context/SwitchLayoutGroupContext.ts b/packages/framer-motion/src/context/SwitchLayoutGroupContext.ts index 13eba891fb..24726a4d08 100644 --- a/packages/framer-motion/src/context/SwitchLayoutGroupContext.ts +++ b/packages/framer-motion/src/context/SwitchLayoutGroupContext.ts @@ -1,8 +1,7 @@ "use client" -import type { Transition } from "motion-dom" +import type { Transition, IProjectionNode } from "motion-dom" import { createContext } from "react" -import { IProjectionNode } from "../projection/node/types" export interface SwitchLayoutGroup { register?: (member: IProjectionNode) => void diff --git a/packages/framer-motion/src/dom.ts b/packages/framer-motion/src/dom.ts index b92753f471..c2eefc2ed0 100644 --- a/packages/framer-motion/src/dom.ts +++ b/packages/framer-motion/src/dom.ts @@ -16,5 +16,5 @@ export * from "./animation/sequence/types" /** * Utils */ -export { delayInSeconds as delay, DelayedFunction } from "./utils/delay" +export { delayInSeconds as delay, type DelayedFunction } from "motion-dom" export * from "./utils/distance" diff --git a/packages/framer-motion/src/events/add-pointer-event.ts b/packages/framer-motion/src/events/add-pointer-event.ts index 65847c1db2..961940594b 100644 --- a/packages/framer-motion/src/events/add-pointer-event.ts +++ b/packages/framer-motion/src/events/add-pointer-event.ts @@ -1,4 +1,4 @@ -import { addDomEvent } from "./add-dom-event" +import { addDomEvent } from "motion-dom" import { addPointerInfo, EventListenerWithPointInfo } from "./event-info" export function addPointerEvent( diff --git a/packages/framer-motion/src/events/use-dom-event.ts b/packages/framer-motion/src/events/use-dom-event.ts index a053798c21..5a5290888b 100644 --- a/packages/framer-motion/src/events/use-dom-event.ts +++ b/packages/framer-motion/src/events/use-dom-event.ts @@ -1,7 +1,7 @@ "use client" import { RefObject, useEffect } from "react" -import { addDomEvent } from "./add-dom-event" +import { addDomEvent } from "motion-dom" /** * Attaches an event listener directly to the provided DOM element. diff --git a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts index 9fab2ca9af..03d3b1de6e 100644 --- a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts +++ b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts @@ -18,11 +18,10 @@ import { type VisualElement, } from "motion-dom" import { Axis, Point, invariant } from "motion-utils" -import { addDomEvent } from "../../events/add-dom-event" +import { addDomEvent, type LayoutUpdateData } from "motion-dom" import { addPointerEvent } from "../../events/add-pointer-event" import { extractEventInfo } from "../../events/event-info" import { MotionProps } from "../../motion/types" -import type { LayoutUpdateData } from "../../projection/node/types" import { getContextWindow } from "../../utils/get-context-window" import { isRefObject } from "../../utils/is-ref-object" import { PanSession } from "../pan/PanSession" diff --git a/packages/framer-motion/src/gestures/focus.ts b/packages/framer-motion/src/gestures/focus.ts index 7c5fc1f087..54ec25319f 100644 --- a/packages/framer-motion/src/gestures/focus.ts +++ b/packages/framer-motion/src/gestures/focus.ts @@ -1,6 +1,5 @@ -import { Feature } from "motion-dom" +import { Feature, addDomEvent } from "motion-dom" import { pipe } from "motion-utils" -import { addDomEvent } from "../events/add-dom-event" export class FocusGesture extends Feature { private isActive = false diff --git a/packages/framer-motion/src/index.ts b/packages/framer-motion/src/index.ts index a9c3b388dc..422df09ba5 100644 --- a/packages/framer-motion/src/index.ts +++ b/packages/framer-motion/src/index.ts @@ -55,7 +55,7 @@ export { useTransform } from "./value/use-transform" export { useVelocity } from "./value/use-velocity" export { useWillChange } from "./value/use-will-change" export { WillChangeMotionValue } from "./value/use-will-change/WillChangeMotionValue" -export { resolveMotionValue } from "./value/utils/resolve-motion-value" +export { resolveMotionValue } from "motion-dom" /** * Accessibility @@ -138,7 +138,7 @@ export type { MotionTransform, VariantLabels, } from "./motion/types" -export type { IProjectionNode } from "./projection/node/types" +export type { IProjectionNode } from "motion-dom" export type { DOMMotionComponents } from "./render/dom/types" export type { ForwardRefComponent, HTMLMotionProps } from "./render/html/types" export type { @@ -146,7 +146,7 @@ export type { SVGMotionProps, } from "./render/svg/types" export type { CreateVisualElement } from "./render/types" -export type { FlatTree } from "./render/utils/flat-tree" +export type { FlatTree } from "motion-dom" export type { ScrollMotionValues } from "./value/scroll/utils" /** @@ -158,4 +158,4 @@ export { DeprecatedLayoutGroupContext } from "./context/DeprecatedLayoutGroupCon export { useInvertedScale as useDeprecatedInvertedScale } from "./value/use-inverted-scale" // Keep explicit delay in milliseconds export for BC with Framer -export { delay, DelayedFunction } from "./utils/delay" +export { delay, type DelayedFunction } from "motion-dom" diff --git a/packages/framer-motion/src/motion/__tests__/static-prop.test.tsx b/packages/framer-motion/src/motion/__tests__/static-prop.test.tsx index b5a615f691..c081d4bdbe 100644 --- a/packages/framer-motion/src/motion/__tests__/static-prop.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/static-prop.test.tsx @@ -1,9 +1,8 @@ -import { motionValue, stagger } from "motion-dom" +import { motionValue, stagger, globalProjectionState } from "motion-dom" import { useEffect } from "react" import { motion, useMotionValue } from "../.." import { MotionConfig } from "../../components/MotionConfig" import { render } from "../../jest.setup" -import { globalProjectionState } from "../../projection/node/state" describe("isStatic prop", () => { test("it prevents rendering of animated values", async () => { diff --git a/packages/framer-motion/src/motion/features/layout.ts b/packages/framer-motion/src/motion/features/layout.ts index 83e736fcb8..00e7da75e1 100644 --- a/packages/framer-motion/src/motion/features/layout.ts +++ b/packages/framer-motion/src/motion/features/layout.ts @@ -1,4 +1,4 @@ -import { HTMLProjectionNode } from "../../projection/node/HTMLProjectionNode" +import { HTMLProjectionNode } from "motion-dom" import { MeasureLayout } from "./layout/MeasureLayout" import { FeaturePackages } from "./types" diff --git a/packages/framer-motion/src/motion/features/layout/MeasureLayout.tsx b/packages/framer-motion/src/motion/features/layout/MeasureLayout.tsx index eafe0f45ff..1d32bc4dc6 100644 --- a/packages/framer-motion/src/motion/features/layout/MeasureLayout.tsx +++ b/packages/framer-motion/src/motion/features/layout/MeasureLayout.tsx @@ -1,6 +1,6 @@ "use client" -import { frame, microtask, type VisualElement } from "motion-dom" +import { frame, microtask, globalProjectionState, type VisualElement } from "motion-dom" import { Component, useContext } from "react" import { usePresence } from "../../../components/AnimatePresence/use-presence" import { @@ -8,7 +8,6 @@ import { LayoutGroupContextProps, } from "../../../context/LayoutGroupContext" import { SwitchLayoutGroupContext } from "../../../context/SwitchLayoutGroupContext" -import { globalProjectionState } from "../../../projection/node/state" import { MotionProps } from "../../types" interface MeasureContextProps { diff --git a/packages/framer-motion/src/motion/utils/use-visual-element.ts b/packages/framer-motion/src/motion/utils/use-visual-element.ts index dfedd7a455..a76fe01dd5 100644 --- a/packages/framer-motion/src/motion/utils/use-visual-element.ts +++ b/packages/framer-motion/src/motion/utils/use-visual-element.ts @@ -17,7 +17,7 @@ import { SwitchLayoutGroupContext, } from "../../context/SwitchLayoutGroupContext" import { MotionProps } from "../../motion/types" -import { IProjectionNode } from "../../projection/node/types" +import type { IProjectionNode } from "motion-dom" import { DOMMotionComponents } from "../../render/dom/types" import { CreateVisualElement } from "../../render/types" import { isRefObject } from "../../utils/is-ref-object" diff --git a/packages/framer-motion/src/motion/utils/use-visual-state.ts b/packages/framer-motion/src/motion/utils/use-visual-state.ts index 32ae2f739d..5ae93ef5dc 100644 --- a/packages/framer-motion/src/motion/utils/use-visual-state.ts +++ b/packages/framer-motion/src/motion/utils/use-visual-state.ts @@ -16,7 +16,7 @@ import { } from "../../context/PresenceContext" import { ScrapeMotionValuesFromProps } from "../../render/types" import { useConstant } from "../../utils/use-constant" -import { resolveMotionValue } from "../../value/utils/resolve-motion-value" +import { resolveMotionValue } from "motion-dom" import { MotionProps } from "../types" export interface VisualState { diff --git a/packages/framer-motion/src/projection.ts b/packages/framer-motion/src/projection.ts index f62f21e2cd..1f809207dd 100644 --- a/packages/framer-motion/src/projection.ts +++ b/packages/framer-motion/src/projection.ts @@ -10,6 +10,7 @@ export { mix, HTMLVisualElement, buildTransform, + // Re-export projection node system from motion-dom + nodeGroup, + HTMLProjectionNode, } from "motion-dom" -export { nodeGroup } from "./projection/node/group" -export { HTMLProjectionNode } from "./projection/node/HTMLProjectionNode" diff --git a/packages/framer-motion/src/projection/use-instant-layout-transition.ts b/packages/framer-motion/src/projection/use-instant-layout-transition.ts index 5958dd6626..eb17bc21de 100644 --- a/packages/framer-motion/src/projection/use-instant-layout-transition.ts +++ b/packages/framer-motion/src/projection/use-instant-layout-transition.ts @@ -1,4 +1,4 @@ -import { rootProjectionNode } from "./node/HTMLProjectionNode" +import { rootProjectionNode } from "motion-dom" export function useInstantLayoutTransition(): ( cb?: (() => void) | undefined diff --git a/packages/framer-motion/src/projection/use-reset-projection.ts b/packages/framer-motion/src/projection/use-reset-projection.ts index 0111d0795f..241917822f 100644 --- a/packages/framer-motion/src/projection/use-reset-projection.ts +++ b/packages/framer-motion/src/projection/use-reset-projection.ts @@ -1,5 +1,5 @@ import { useCallback } from "react"; -import { rootProjectionNode } from "./node/HTMLProjectionNode" +import { rootProjectionNode } from "motion-dom" export function useResetProjection() { const reset = useCallback(() => { diff --git a/packages/framer-motion/src/render/utils/__tests__/flat-tree.test.ts b/packages/framer-motion/src/render/utils/__tests__/flat-tree.test.ts index 314c534920..1dac1cac38 100644 --- a/packages/framer-motion/src/render/utils/__tests__/flat-tree.test.ts +++ b/packages/framer-motion/src/render/utils/__tests__/flat-tree.test.ts @@ -1,5 +1,4 @@ -import { WithDepth } from "../compare-by-depth" -import { FlatTree } from "../flat-tree" +import { type WithDepth, FlatTree } from "motion-dom" describe("FlatTree", () => { test("Correctly sorts by depth on iteration", () => { diff --git a/packages/framer-motion/src/utils/__tests__/delay.test.ts b/packages/framer-motion/src/utils/__tests__/delay.test.ts index a566a58d2e..ecdc937321 100644 --- a/packages/framer-motion/src/utils/__tests__/delay.test.ts +++ b/packages/framer-motion/src/utils/__tests__/delay.test.ts @@ -1,4 +1,4 @@ -import { delay } from "../delay" +import { delay } from "motion-dom" describe("delay", () => { test("resolves after provided duration", async () => { diff --git a/packages/framer-motion/src/value/__tests__/unwrap-value.test.ts b/packages/framer-motion/src/value/__tests__/unwrap-value.test.ts index 579fec59b5..ebf0b72cb2 100644 --- a/packages/framer-motion/src/value/__tests__/unwrap-value.test.ts +++ b/packages/framer-motion/src/value/__tests__/unwrap-value.test.ts @@ -1,5 +1,4 @@ -import { MotionValue } from "motion-dom" -import { resolveMotionValue } from "../utils/resolve-motion-value" +import { MotionValue, resolveMotionValue } from "motion-dom" describe("resolveMotionValue", () => { it("should leave non-motion values alone", () => { diff --git a/packages/framer-motion/src/animation/animate/single-value.ts b/packages/motion-dom/src/animation/animate/single-value.ts similarity index 76% rename from packages/framer-motion/src/animation/animate/single-value.ts rename to packages/motion-dom/src/animation/animate/single-value.ts index 31d463c9e3..5711cc3aa7 100644 --- a/packages/framer-motion/src/animation/animate/single-value.ts +++ b/packages/motion-dom/src/animation/animate/single-value.ts @@ -1,13 +1,15 @@ -import { - animateMotionValue, +import { animateMotionValue } from "../interfaces/motion-value" +import type { AnimationPlaybackControlsWithThen, AnyResolvedKeyframe, - motionValue as createMotionValue, - isMotionValue, - MotionValue, UnresolvedValueKeyframe, ValueAnimationTransition, -} from "motion-dom" +} from "../types" +import { + motionValue as createMotionValue, + MotionValue, +} from "../../value" +import { isMotionValue } from "../../value/utils/is-motion-value" export function animateSingleValue( value: MotionValue | V, diff --git a/packages/framer-motion/src/events/add-dom-event.ts b/packages/motion-dom/src/events/add-dom-event.ts similarity index 100% rename from packages/framer-motion/src/events/add-dom-event.ts rename to packages/motion-dom/src/events/add-dom-event.ts diff --git a/packages/motion-dom/src/index.ts b/packages/motion-dom/src/index.ts index e71c30c30d..3e42bdc695 100644 --- a/packages/motion-dom/src/index.ts +++ b/packages/motion-dom/src/index.ts @@ -216,6 +216,45 @@ export { buildProjectionTransform } from "./projection/styles/transform" // Projection animation export { mixValues } from "./projection/animation/mix-values" +// Utilities (used by projection system) +export { delay, delayInSeconds } from "./utils/delay" +export type { DelayedFunction } from "./utils/delay" +export { addDomEvent } from "./events/add-dom-event" +export { resolveMotionValue } from "./value/utils/resolve-motion-value" +export { animateSingleValue } from "./animation/animate/single-value" +export { FlatTree } from "./projection/utils/flat-tree" +export { compareByDepth } from "./projection/utils/compare-by-depth" +export type { WithDepth } from "./projection/utils/compare-by-depth" + +// Projection node system +export { + createProjectionNode, + propagateDirtyNodes, + cleanDirtyNodes, +} from "./projection/node/create-projection-node" +export { + HTMLProjectionNode, + rootProjectionNode, +} from "./projection/node/HTMLProjectionNode" +export { DocumentProjectionNode } from "./projection/node/DocumentProjectionNode" +export { globalProjectionState } from "./projection/node/state" +export { nodeGroup } from "./projection/node/group" +export type { NodeGroup } from "./projection/node/group" +export { NodeStack } from "./projection/shared/stack" +export type { + IProjectionNode, + Measurements, + Phase, + ScrollMeasurements, + LayoutEvents, + LayoutUpdateData, + LayoutUpdateHandler, + ProjectionNodeConfig, + ProjectionNodeOptions, + ProjectionEventName, + InitialPromotionConfig, +} from "./projection/node/types" + // HTML/SVG utilities export { buildHTMLStyles } from "./render/html/utils/build-styles" export { buildTransform } from "./render/html/utils/build-transform" diff --git a/packages/framer-motion/src/projection/node/DocumentProjectionNode.ts b/packages/motion-dom/src/projection/node/DocumentProjectionNode.ts similarity index 100% rename from packages/framer-motion/src/projection/node/DocumentProjectionNode.ts rename to packages/motion-dom/src/projection/node/DocumentProjectionNode.ts diff --git a/packages/framer-motion/src/projection/node/HTMLProjectionNode.ts b/packages/motion-dom/src/projection/node/HTMLProjectionNode.ts similarity index 100% rename from packages/framer-motion/src/projection/node/HTMLProjectionNode.ts rename to packages/motion-dom/src/projection/node/HTMLProjectionNode.ts diff --git a/packages/framer-motion/src/projection/node/__tests__/TestProjectionNode.ts b/packages/motion-dom/src/projection/node/__tests__/TestProjectionNode.ts similarity index 93% rename from packages/framer-motion/src/projection/node/__tests__/TestProjectionNode.ts rename to packages/motion-dom/src/projection/node/__tests__/TestProjectionNode.ts index 124d1266c0..fafbfe7273 100644 --- a/packages/framer-motion/src/projection/node/__tests__/TestProjectionNode.ts +++ b/packages/motion-dom/src/projection/node/__tests__/TestProjectionNode.ts @@ -1,6 +1,6 @@ import { Box } from "motion-utils" import { createProjectionNode } from "../create-projection-node" -import { IProjectionNode, ProjectionNodeOptions } from "../types" +import type { IProjectionNode, ProjectionNodeOptions } from "../types" let rootNode: IProjectionNode diff --git a/packages/framer-motion/src/projection/node/__tests__/group.test.ts b/packages/motion-dom/src/projection/node/__tests__/group.test.ts similarity index 100% rename from packages/framer-motion/src/projection/node/__tests__/group.test.ts rename to packages/motion-dom/src/projection/node/__tests__/group.test.ts diff --git a/packages/framer-motion/src/projection/node/__tests__/node.test.ts b/packages/motion-dom/src/projection/node/__tests__/node.test.ts similarity index 98% rename from packages/framer-motion/src/projection/node/__tests__/node.test.ts rename to packages/motion-dom/src/projection/node/__tests__/node.test.ts index 662325b085..450dc6cf0e 100644 --- a/packages/framer-motion/src/projection/node/__tests__/node.test.ts +++ b/packages/motion-dom/src/projection/node/__tests__/node.test.ts @@ -1,7 +1,7 @@ import { createTestNode } from "./TestProjectionNode" import { propagateDirtyNodes, cleanDirtyNodes } from "../create-projection-node" -import { IProjectionNode } from "../types" -import { nextFrame, nextMicrotask } from "../../../gestures/__tests__/utils" +import type { IProjectionNode } from "../types" +import { nextFrame, nextMicrotask } from "./utils" describe("node", () => { test("If a child updates layout, and parent has scale, parent resetsTransform during measurement", async () => { diff --git a/packages/motion-dom/src/projection/node/__tests__/utils.ts b/packages/motion-dom/src/projection/node/__tests__/utils.ts new file mode 100644 index 0000000000..064283f3bc --- /dev/null +++ b/packages/motion-dom/src/projection/node/__tests__/utils.ts @@ -0,0 +1,14 @@ +import { frame } from "../../../frameloop" +import { microtask } from "../../../frameloop/microtask" + +export async function nextFrame() { + return new Promise((resolve) => { + frame.postRender(() => resolve()) + }) +} + +export async function nextMicrotask() { + return new Promise((resolve) => { + microtask.postRender(() => resolve()) + }) +} diff --git a/packages/framer-motion/src/projection/node/create-projection-node.ts b/packages/motion-dom/src/projection/node/create-projection-node.ts similarity index 97% rename from packages/framer-motion/src/projection/node/create-projection-node.ts rename to packages/motion-dom/src/projection/node/create-projection-node.ts index 3c30b8227c..63feb136ea 100644 --- a/packages/framer-motion/src/projection/node/create-projection-node.ts +++ b/packages/motion-dom/src/projection/node/create-projection-node.ts @@ -1,49 +1,44 @@ +import { activeAnimations } from "../../stats/animation-count" +import { JSAnimation } from "../../animation/JSAnimation" +import { Transition, ValueAnimationOptions } from "../../animation/types" +import { getValueTransition } from "../../animation/utils/get-value-transition" +import { cancelFrame, frame, frameData, frameSteps } from "../../frameloop" +import { microtask } from "../../frameloop/microtask" +import { time } from "../../frameloop/sync-time" +import type { Process } from "../../frameloop/types" import { - activeAnimations, applyBoxDelta, applyTreeDeltas, - aspectRatio, - axisDeltaEquals, - boxEquals, - boxEqualsRounded, - buildProjectionTransform, + transformBox, + translateAxis, +} from "../geometry/delta-apply" +import { calcBoxDelta, calcLength, calcRelativeBox, calcRelativePosition, - cancelFrame, - copyAxisDeltaInto, - copyBoxInto, - createBox, - createDelta, - eachAxis, - frame, - frameData, - frameSteps, - getValueTransition, - has2DTranslate, - hasScale, - hasTransform, - isDeltaZero, isNear, - isSVGElement, - isSVGSVGElement, - JSAnimation, - microtask, - mixNumber, - mixValues, - MotionValue, - motionValue, - removeBoxTransforms, - scaleCorrectors, - statsBuffer, - time, - transformBox, - translateAxis, - Transition, - ValueAnimationOptions, - type Process, -} from "motion-dom" +} from "../geometry/delta-calc" +import { removeBoxTransforms } from "../geometry/delta-remove" +import { copyAxisDeltaInto, copyBoxInto } from "../geometry/copy" +import { createBox, createDelta } from "../geometry/models" +import { + aspectRatio, + axisDeltaEquals, + boxEquals, + boxEqualsRounded, + isDeltaZero, +} from "../geometry/utils" +import { buildProjectionTransform } from "../styles/transform" +import { eachAxis } from "../utils/each-axis" +import { has2DTranslate, hasScale, hasTransform } from "../utils/has-transform" +import { mixValues } from "../animation/mix-values" +import { isSVGElement } from "../../utils/is-svg-element" +import { isSVGSVGElement } from "../../utils/is-svg-svg-element" +import { mixNumber } from "../../utils/mix/number" +import { MotionValue, motionValue } from "../../value" +import { scaleCorrectors } from "../../render/utils/is-forced-motion-value" +import { statsBuffer } from "../../stats/buffer" import { Axis, AxisDelta, @@ -55,10 +50,11 @@ import { SubscriptionManager, } from "motion-utils" import { animateSingleValue } from "../../animation/animate/single-value" -import { getOptimisedAppearId } from "motion-dom" -import { MotionStyle } from "../../motion/types" -import { HTMLVisualElement, ResolvedValues, VisualElement } from "motion-dom" -import { FlatTree } from "../../render/utils/flat-tree" +import { getOptimisedAppearId } from "../../animation/optimized-appear/get-appear-id" +import type { ResolvedValues } from "../../render/types" +import type { MotionStyle, VisualElement } from "../../render/VisualElement" +import { HTMLVisualElement } from "../../render/html/HTMLVisualElement" +import { FlatTree } from "../utils/flat-tree" import { delay } from "../../utils/delay" import { resolveMotionValue } from "../../value/utils/resolve-motion-value" import { NodeStack } from "../shared/stack" diff --git a/packages/framer-motion/src/projection/node/group.ts b/packages/motion-dom/src/projection/node/group.ts similarity index 100% rename from packages/framer-motion/src/projection/node/group.ts rename to packages/motion-dom/src/projection/node/group.ts diff --git a/packages/framer-motion/src/projection/node/state.ts b/packages/motion-dom/src/projection/node/state.ts similarity index 100% rename from packages/framer-motion/src/projection/node/state.ts rename to packages/motion-dom/src/projection/node/state.ts diff --git a/packages/framer-motion/src/projection/node/types.ts b/packages/motion-dom/src/projection/node/types.ts similarity index 84% rename from packages/framer-motion/src/projection/node/types.ts rename to packages/motion-dom/src/projection/node/types.ts index 2e8e23de81..59fa8a289c 100644 --- a/packages/framer-motion/src/projection/node/types.ts +++ b/packages/motion-dom/src/projection/node/types.ts @@ -1,8 +1,9 @@ -import type { JSAnimation, ResolvedValues, Transition, ValueTransition, VisualElement } from "motion-dom" +import type { JSAnimation } from "../../animation/JSAnimation" +import type { Transition, ValueTransition } from "../../animation/types" +import type { ResolvedValues } from "../../render/types" +import type { VisualElement, MotionStyle } from "../../render/VisualElement" import { Box, Delta, Point } from "motion-utils" -import { InitialPromotionConfig } from "../../context/SwitchLayoutGroupContext" -import { MotionStyle } from "../../motion/types" -import { FlatTree } from "../../render/utils/flat-tree" +import { FlatTree } from "../utils/flat-tree" import { NodeStack } from "../shared/stack" export interface Measurements { @@ -162,6 +163,23 @@ export interface ProjectionNodeConfig { resetTransform?: (instance: I, value?: string) => void } +/** + * Configuration for initial promotion of shared layout elements. + * This was originally in React's SwitchLayoutGroupContext but is now + * framework-agnostic to support vanilla JS usage. + */ +export interface InitialPromotionConfig { + /** + * The initial transition to use when the elements in this group mount (and automatically promoted). + * Subsequent updates should provide a transition in the promote method. + */ + transition?: Transition + /** + * If the follow tree should preserve its opacity when the lead is promoted on mount + */ + shouldPreserveFollowOpacity?: (member: IProjectionNode) => boolean +} + export interface ProjectionNodeOptions { animate?: boolean layoutScroll?: boolean diff --git a/packages/framer-motion/src/projection/shared/stack.ts b/packages/motion-dom/src/projection/shared/stack.ts similarity index 81% rename from packages/framer-motion/src/projection/shared/stack.ts rename to packages/motion-dom/src/projection/shared/stack.ts index 455c3b4938..e66cd06c5e 100644 --- a/packages/framer-motion/src/projection/shared/stack.ts +++ b/packages/motion-dom/src/projection/shared/stack.ts @@ -80,20 +80,7 @@ export class NodeStack { const { crossfade } = node.options if (crossfade === false) { prevLead.hide() - } else { } - /** - * TODO: - * - Test border radius when previous node was deleted - * - boxShadow mixing - * - Shared between element A in scrolled container and element B (scroll stays the same or changes) - * - Shared between element A in transformed container and element B (transform stays the same or changes) - * - Shared between element A in scrolled page and element B (scroll stays the same or changes) - * --- - * - Crossfade opacity of root nodes - * - layoutId changes after animation - * - layoutId changes mid animation - */ } } diff --git a/packages/framer-motion/src/render/utils/compare-by-depth.ts b/packages/motion-dom/src/projection/utils/compare-by-depth.ts similarity index 69% rename from packages/framer-motion/src/render/utils/compare-by-depth.ts rename to packages/motion-dom/src/projection/utils/compare-by-depth.ts index dbdc13f66e..4127726cbf 100644 --- a/packages/framer-motion/src/render/utils/compare-by-depth.ts +++ b/packages/motion-dom/src/projection/utils/compare-by-depth.ts @@ -1,4 +1,4 @@ -import type { VisualElement } from "motion-dom" +import type { VisualElement } from "../../render/VisualElement" export interface WithDepth { depth: number diff --git a/packages/framer-motion/src/render/utils/flat-tree.ts b/packages/motion-dom/src/projection/utils/flat-tree.ts similarity index 100% rename from packages/framer-motion/src/render/utils/flat-tree.ts rename to packages/motion-dom/src/projection/utils/flat-tree.ts diff --git a/packages/framer-motion/src/utils/delay.ts b/packages/motion-dom/src/utils/delay.ts similarity index 82% rename from packages/framer-motion/src/utils/delay.ts rename to packages/motion-dom/src/utils/delay.ts index f3753f3b20..51ab5dedf3 100644 --- a/packages/framer-motion/src/utils/delay.ts +++ b/packages/motion-dom/src/utils/delay.ts @@ -1,4 +1,6 @@ -import { cancelFrame, frame, FrameData, time } from "motion-dom" +import { cancelFrame, frame } from "../frameloop" +import { time } from "../frameloop/sync-time" +import type { FrameData } from "../frameloop/types" import { secondsToMilliseconds } from "motion-utils" export type DelayedFunction = (overshoot: number) => void diff --git a/packages/framer-motion/src/value/utils/resolve-motion-value.ts b/packages/motion-dom/src/value/utils/resolve-motion-value.ts similarity index 63% rename from packages/framer-motion/src/value/utils/resolve-motion-value.ts rename to packages/motion-dom/src/value/utils/resolve-motion-value.ts index b3bd99d3f0..81d00689b6 100644 --- a/packages/framer-motion/src/value/utils/resolve-motion-value.ts +++ b/packages/motion-dom/src/value/utils/resolve-motion-value.ts @@ -1,9 +1,9 @@ -import { AnyResolvedKeyframe, isMotionValue, MotionValue } from "motion-dom" +import type { AnyResolvedKeyframe } from "../../animation/types" +import { isMotionValue } from "./is-motion-value" +import type { MotionValue } from "../index" /** * If the provided value is a MotionValue, this returns the actual value, otherwise just the value itself - * - * TODO: Remove and move to library */ export function resolveMotionValue( value?: AnyResolvedKeyframe | MotionValue From c512503aae49123584fe091fca4f80655ef64263 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 13 Jan 2026 11:22:00 +0100 Subject: [PATCH 10/11] Updating changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eab08b889f..d71e45bcd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ Motion adheres to [Semantic Versioning](http://semver.org/). Undocumented APIs should be considered internal and may change without warning. +## [12.26.2] 2026-01-13 + +### Fixed + +- Internal refactor of projection system into `motion-dom`. + ## [12.26.1] 2026-01-12 ### Fixed From 89734905d6269bd4e77cc15a5337b3b68ec22745 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 13 Jan 2026 11:22:19 +0100 Subject: [PATCH 11/11] v12.26.2 --- dev/html/package.json | 8 ++++---- dev/next/package.json | 4 ++-- dev/react-19/package.json | 4 ++-- dev/react/package.json | 4 ++-- lerna.json | 2 +- packages/framer-motion/package.json | 4 ++-- packages/motion-dom/package.json | 2 +- packages/motion/package.json | 4 ++-- yarn.lock | 22 +++++++++++----------- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/dev/html/package.json b/dev/html/package.json index 7a5c24ee08..424d9cfe6e 100644 --- a/dev/html/package.json +++ b/dev/html/package.json @@ -1,7 +1,7 @@ { "name": "html-env", "private": true, - "version": "12.26.1", + "version": "12.26.2", "type": "module", "scripts": { "dev": "vite", @@ -10,9 +10,9 @@ "preview": "vite preview" }, "dependencies": { - "framer-motion": "^12.26.1", - "motion": "^12.26.1", - "motion-dom": "^12.24.11", + "framer-motion": "^12.26.2", + "motion": "^12.26.2", + "motion-dom": "^12.26.2", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/dev/next/package.json b/dev/next/package.json index fe11184992..3d69491ffb 100644 --- a/dev/next/package.json +++ b/dev/next/package.json @@ -1,7 +1,7 @@ { "name": "next-env", "private": true, - "version": "12.26.1", + "version": "12.26.2", "type": "module", "scripts": { "dev": "next dev", @@ -10,7 +10,7 @@ "build": "next build" }, "dependencies": { - "motion": "^12.26.1", + "motion": "^12.26.2", "next": "15.4.10", "react": "19.0.0", "react-dom": "19.0.0" diff --git a/dev/react-19/package.json b/dev/react-19/package.json index 409580702e..ae92c80cb7 100644 --- a/dev/react-19/package.json +++ b/dev/react-19/package.json @@ -1,7 +1,7 @@ { "name": "react-19-env", "private": true, - "version": "12.26.1", + "version": "12.26.2", "type": "module", "scripts": { "dev": "vite", @@ -11,7 +11,7 @@ "preview": "vite preview" }, "dependencies": { - "motion": "^12.26.1", + "motion": "^12.26.2", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/dev/react/package.json b/dev/react/package.json index 20fd792163..a8d26058f6 100644 --- a/dev/react/package.json +++ b/dev/react/package.json @@ -1,7 +1,7 @@ { "name": "react-env", "private": true, - "version": "12.26.1", + "version": "12.26.2", "type": "module", "scripts": { "dev": "yarn vite", @@ -11,7 +11,7 @@ "preview": "yarn vite preview" }, "dependencies": { - "framer-motion": "^12.26.1", + "framer-motion": "^12.26.2", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/lerna.json b/lerna.json index d5f40fe3d6..eb0aa44051 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "12.26.1", + "version": "12.26.2", "packages": [ "packages/*", "dev/*" diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index 6334abd71c..4737e7f9d1 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -1,6 +1,6 @@ { "name": "framer-motion", - "version": "12.26.1", + "version": "12.26.2", "description": "A simple and powerful JavaScript animation library", "main": "dist/cjs/index.js", "module": "dist/es/index.mjs", @@ -88,7 +88,7 @@ "measure": "rollup -c ./rollup.size.config.mjs" }, "dependencies": { - "motion-dom": "^12.24.11", + "motion-dom": "^12.26.2", "motion-utils": "^12.24.10", "tslib": "^2.4.0" }, diff --git a/packages/motion-dom/package.json b/packages/motion-dom/package.json index 5be9744359..a48a333865 100644 --- a/packages/motion-dom/package.json +++ b/packages/motion-dom/package.json @@ -1,6 +1,6 @@ { "name": "motion-dom", - "version": "12.24.11", + "version": "12.26.2", "author": "Matt Perry", "license": "MIT", "repository": "https://github.com/motiondivision/motion", diff --git a/packages/motion/package.json b/packages/motion/package.json index 45118f678b..90e529a5dc 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -1,6 +1,6 @@ { "name": "motion", - "version": "12.26.1", + "version": "12.26.2", "description": "An animation library for JavaScript and React.", "main": "dist/cjs/index.js", "module": "dist/es/index.mjs", @@ -76,7 +76,7 @@ "postpublish": "git push --tags" }, "dependencies": { - "framer-motion": "^12.26.1", + "framer-motion": "^12.26.2", "tslib": "^2.4.0" }, "peerDependencies": { diff --git a/yarn.lock b/yarn.lock index 84f9dccd2f..090daab0cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7420,14 +7420,14 @@ __metadata: languageName: node linkType: hard -"framer-motion@^12.26.1, framer-motion@workspace:packages/framer-motion": +"framer-motion@^12.26.2, framer-motion@workspace:packages/framer-motion": version: 0.0.0-use.local resolution: "framer-motion@workspace:packages/framer-motion" dependencies: "@radix-ui/react-dialog": ^1.1.15 "@thednp/dommatrix": ^2.0.11 "@types/three": 0.137.0 - motion-dom: ^12.24.11 + motion-dom: ^12.26.2 motion-utils: ^12.24.10 three: 0.137.0 tslib: ^2.4.0 @@ -8192,9 +8192,9 @@ __metadata: version: 0.0.0-use.local resolution: "html-env@workspace:dev/html" dependencies: - framer-motion: ^12.26.1 - motion: ^12.26.1 - motion-dom: ^12.24.11 + framer-motion: ^12.26.2 + motion: ^12.26.2 + motion-dom: ^12.26.2 react: ^18.3.1 react-dom: ^18.3.1 vite: ^5.2.0 @@ -10936,7 +10936,7 @@ __metadata: languageName: node linkType: hard -"motion-dom@^12.24.11, motion-dom@workspace:packages/motion-dom": +"motion-dom@^12.26.2, motion-dom@workspace:packages/motion-dom": version: 0.0.0-use.local resolution: "motion-dom@workspace:packages/motion-dom" dependencies: @@ -11013,11 +11013,11 @@ __metadata: languageName: unknown linkType: soft -"motion@^12.26.1, motion@workspace:packages/motion": +"motion@^12.26.2, motion@workspace:packages/motion": version: 0.0.0-use.local resolution: "motion@workspace:packages/motion" dependencies: - framer-motion: ^12.26.1 + framer-motion: ^12.26.2 tslib: ^2.4.0 peerDependencies: "@emotion/is-prop-valid": "*" @@ -11134,7 +11134,7 @@ __metadata: version: 0.0.0-use.local resolution: "next-env@workspace:dev/next" dependencies: - motion: ^12.26.1 + motion: ^12.26.2 next: 15.4.10 react: 19.0.0 react-dom: 19.0.0 @@ -12599,7 +12599,7 @@ __metadata: "@typescript-eslint/parser": ^7.2.0 "@vitejs/plugin-react-swc": ^3.5.0 eslint-plugin-react-refresh: ^0.4.6 - motion: ^12.26.1 + motion: ^12.26.2 react: ^19.0.0 react-dom: ^19.0.0 vite: ^5.2.0 @@ -12683,7 +12683,7 @@ __metadata: "@typescript-eslint/parser": ^7.2.0 "@vitejs/plugin-react-swc": ^3.5.0 eslint-plugin-react-refresh: ^0.4.6 - framer-motion: ^12.26.1 + framer-motion: ^12.26.2 react: ^18.3.1 react-dom: ^18.3.1 vite: ^5.2.0