From f5a9238d292559e375f68bbb35a6041a9cafe72d Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 13 Dec 2023 11:31:23 +0100 Subject: [PATCH 01/44] adding stub --- .../src/animation/animators/async/Animation.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 packages/framer-motion/src/animation/animators/async/Animation.ts diff --git a/packages/framer-motion/src/animation/animators/async/Animation.ts b/packages/framer-motion/src/animation/animators/async/Animation.ts new file mode 100644 index 0000000000..8ca824d316 --- /dev/null +++ b/packages/framer-motion/src/animation/animators/async/Animation.ts @@ -0,0 +1,12 @@ +interface ValueAnimationOptions {} + +export class ValueAnimation { + private startTime: number | null + private holdTime: number | null + private timeline: DocumentTimeline | AnimationTimeline + private playState: AnimationPlayState + + constructor(options: ValueAnimationOptions) {} + + private resolve() {} +} From 39872a6ac2100dd155a08892a0f30ed88e1b02ce Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Thu, 14 Dec 2023 10:01:17 +0100 Subject: [PATCH 02/44] Latest --- .../animation/animators/async/Animation.ts | 12 - packages/framer-motion/src/animation/types.ts | 13 +- .../framer-motion/src/frameloop/batcher.ts | 15 +- packages/framer-motion/src/frameloop/types.ts | 5 +- .../src/keyframes/StyleKeyframes.ts | 212 ++++++++++++++++++ .../dom/utils/css-variables-conversion.ts | 2 +- .../src/render/dom/utils/is-css-variable.ts | 2 +- 7 files changed, 234 insertions(+), 27 deletions(-) delete mode 100644 packages/framer-motion/src/animation/animators/async/Animation.ts create mode 100644 packages/framer-motion/src/keyframes/StyleKeyframes.ts diff --git a/packages/framer-motion/src/animation/animators/async/Animation.ts b/packages/framer-motion/src/animation/animators/async/Animation.ts deleted file mode 100644 index 8ca824d316..0000000000 --- a/packages/framer-motion/src/animation/animators/async/Animation.ts +++ /dev/null @@ -1,12 +0,0 @@ -interface ValueAnimationOptions {} - -export class ValueAnimation { - private startTime: number | null - private holdTime: number | null - private timeline: DocumentTimeline | AnimationTimeline - private playState: AnimationPlayState - - constructor(options: ValueAnimationOptions) {} - - private resolve() {} -} diff --git a/packages/framer-motion/src/animation/types.ts b/packages/framer-motion/src/animation/types.ts index 659dbd26bc..152e7243e9 100644 --- a/packages/framer-motion/src/animation/types.ts +++ b/packages/framer-motion/src/animation/types.ts @@ -117,14 +117,15 @@ export interface CSSStyleDeclarationWithTransform skewY: number | string } -export type ValueKeyframe = string | number +export type ValueKeyframe = T -export type UnresolvedValueKeyframe = ValueKeyframe | null +export type UnresolvedValueKeyframe = + ValueKeyframe | null -export type ValueKeyframesDefinition = - | ValueKeyframe - | ValueKeyframe[] - | UnresolvedValueKeyframe[] +export type ValueKeyframesDefinition = + | T + | T[] + | Array export type StyleKeyframesDefinition = { [K in keyof CSSStyleDeclarationWithTransform]?: ValueKeyframesDefinition diff --git a/packages/framer-motion/src/frameloop/batcher.ts b/packages/framer-motion/src/frameloop/batcher.ts index 75e99aa5ef..c6dab37600 100644 --- a/packages/framer-motion/src/frameloop/batcher.ts +++ b/packages/framer-motion/src/frameloop/batcher.ts @@ -3,12 +3,15 @@ import { createRenderStep } from "./render-step" import { Batcher, Process, StepId, Steps, FrameData } from "./types" export const stepsOrder: StepId[] = [ - "prepare", - "read", - "update", - "preRender", - "render", - "postRender", + "read", // Read + "unsetTransforms", // Write + "measure", // Read + "renderTemporaryStyles", // Write + "readTemporaryStyles", // Read + "update", // None + "preRender", // None + "render", // Write + "postRender", // None ] const maxElapsed = 40 diff --git a/packages/framer-motion/src/frameloop/types.ts b/packages/framer-motion/src/frameloop/types.ts index 4781b4aa4c..f9fbcf613b 100644 --- a/packages/framer-motion/src/frameloop/types.ts +++ b/packages/framer-motion/src/frameloop/types.ts @@ -13,8 +13,11 @@ export interface Step { } export type StepId = - | "prepare" | "read" + | "unsetTransforms" + | "measure" + | "renderTemporaryStyles" + | "readTemporaryStyles" | "update" | "preRender" | "render" diff --git a/packages/framer-motion/src/keyframes/StyleKeyframes.ts b/packages/framer-motion/src/keyframes/StyleKeyframes.ts new file mode 100644 index 0000000000..6d6a870dda --- /dev/null +++ b/packages/framer-motion/src/keyframes/StyleKeyframes.ts @@ -0,0 +1,212 @@ +import { Easing } from "../easing/types" +import { isEasingArray } from "../easing/utils/is-easing-array" +import { easingDefinitionToFunction } from "../easing/utils/map" +import { frame } from "../frameloop" +import type { VisualElement } from "../render/VisualElement" +import { getVariableValue } from "../render/dom/utils/css-variables-conversion" +import { isCSSVariableToken } from "../render/dom/utils/is-css-variable" +import { + isNumOrPxType, + positionalKeys, + positionalValues, + removeNonTranslationalTransform, +} from "../render/dom/utils/unit-conversion" +import { findDimensionValueType } from "../render/dom/value-types/dimensions" +import { interpolate } from "../utils/interpolate" +import { defaultOffset } from "../utils/offsets/default" + +export interface KeyframeOptions { + ease?: Easing | Easing[] + times?: number[] +} + +export type UnresolvedKeyframes = Array + +const isNull = (v: any): v is null => v === null + +export class StyleKeyframes { + private sample: (v: number) => T + + finalKeyframe: string | number + + constructor( + element: VisualElement, + valueName: string, + keyframes: UnresolvedKeyframes, + { + ease = "easeInOut", + times = defaultOffset(keyframes), + }: KeyframeOptions + ) { + let resolvedKeyframes: Array + + frame.read(() => { + if (!element.current) return + + /** + * If the first keyframe is null, we need to find its value by sampling the element + */ + if (isNull(keyframes[0])) { + keyframes[0] = element.readValue(valueName) as T + } + + /** + * If any keyframe is a CSS variable, we need to find its value by sampling the element + */ + for (let i = 0; i < keyframes.length; i++) { + const keyframe = keyframes[i] + if (isCSSVariableToken(keyframe)) { + const resolved = getVariableValue(keyframe, element.current) + if (resolved !== undefined) { + keyframes[i] = resolved as T + } + + // If this variable is the final keyframe, set it as finalKeyframe + if (i === keyframes.length - 1) { + this.finalKeyframe = keyframe + } + } + } + + // TODO: Fill nulls forward here + resolvedKeyframes = keyframes as T[] + + /** + * Check to see if unit type has changed. If so schedule jobs that will + * temporarily set styles to the destination keyframes. + * Skip if we have more than two keyframes or this isn't a positional value. + * TODO: We can throw if there are multiple keyframes and the value type changes. + */ + if ( + !positionalKeys.has(valueName) || + keyframes.length !== 2 || + isCSSVariableToken( + resolvedKeyframes[resolvedKeyframes.length - 1] + ) + ) { + return + } + + const [origin, target] = resolvedKeyframes + const originType = findDimensionValueType(origin) + const targetType = findDimensionValueType(target) + + if (!originType || !targetType || originType === targetType) return + + /** + * If both values are numbers or pixels, we can animate between them by + * converting them to numbers. + */ + if (isNumOrPxType(originType) && isNumOrPxType(targetType)) { + resolvedKeyframes = resolvedKeyframes.map((v) => + typeof v === "string" ? parseFloat(v) : v + ) + } else if ( + originType.transform && + targetType.transform && + origin === 0 && + target === 0 + ) { + /** + * If one value or the other is 0, it's safe to coerce without + * measurement. + */ + if (origin === 0) { + keyframes[0] = targetType.transform!(0) + } else if (target === 0) { + keyframes[keyframes.length - 1] = originType.transform!(0) + } + } else { + let unsetTransforms + let scrollY: number + let origin + + /** + * We can't coerce so now we need to do value conversion via DOM measurements. + */ + frame.unsetTransforms(() => { + if (!element.current) return + + unsetTransforms = removeNonTranslationalTransform(element) + + if (this.finalKeyframe === undefined) { + this.finalKeyframe = target + } + + element.getValue(valueName, target).jump(target) + }) + + frame.measure(() => { + if (!element.current) return + + if (valueName === "height") { + scrollY = window.pageYOffset + } + + const originBbox = element.measureViewportBox() + const computedStyle = window.getComputedStyle( + element.current + ) + + if (computedStyle.display === "none") { + element.setStaticValue( + "display", + (target.display as string) || "block" + ) + } + + origin = positionalValues[valueName]( + originBbox, + computedStyle + ) + }) + + frame.renderTemporaryStyles(element.render) + + frame.readTemporaryStyles(() => { + if (!element.current) return + + const targetBbox = element.measureViewportBox() + const value = element.getValue(valueName) + value && value.jump(origin) + resolvedKeyframes[resolvedKeyframes.length - 1] = + positionalValues[valueName]( + targetBbox, + window.getComputedStyle(element.current) + ) + + // If we removed transform values, reapply them before the next render + if (unsetTransforms.length) { + unsetTransforms.forEach(([key, value]) => { + element.getValue(key)!.set(value) + }) + } + }) + + if (scrollY !== undefined) { + // TODO: Move this to some kind of globally shared function like suspend and restore scroll + frame.render(() => { + window.scrollTo({ top: scrollY }) + }) + } + + frame.render(element.render) + } + }) + + frame.render(() => { + /** + * Easing functions can be externally defined as strings. Here we convert them + * into actual functions. + */ + const easingFunctions = isEasingArray(ease) + ? ease.map(easingDefinitionToFunction) + : easingDefinitionToFunction(ease) + + /** + * Create interpolate function based off the processed keyframes. + */ + this.sample = interpolate(resolvedKeyframes, times, easingFunctions) + }) + } +} diff --git a/packages/framer-motion/src/render/dom/utils/css-variables-conversion.ts b/packages/framer-motion/src/render/dom/utils/css-variables-conversion.ts index c47c6972c8..d7ebee0a46 100644 --- a/packages/framer-motion/src/render/dom/utils/css-variables-conversion.ts +++ b/packages/framer-motion/src/render/dom/utils/css-variables-conversion.ts @@ -26,7 +26,7 @@ export function parseCSSVariable(current: string) { } const maxDepth = 4 -function getVariableValue( +export function getVariableValue( current: CSSVariableToken, element: Element, depth = 1 diff --git a/packages/framer-motion/src/render/dom/utils/is-css-variable.ts b/packages/framer-motion/src/render/dom/utils/is-css-variable.ts index e01bf8d858..1995bd92ed 100644 --- a/packages/framer-motion/src/render/dom/utils/is-css-variable.ts +++ b/packages/framer-motion/src/render/dom/utils/is-css-variable.ts @@ -4,7 +4,7 @@ export type CSSVariableToken = `var(${CSSVariableName})` const checkStringStartsWith = (token: string) => - (key?: string): key is T => + (key?: string | number | null): key is T => typeof key === "string" && key.startsWith(token) export const isCSSVariableName = checkStringStartsWith("--") From 92c01c61d912c60faf524041ad85634e0beb9224 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Thu, 14 Dec 2023 16:22:25 +0100 Subject: [PATCH 03/44] Updating --- dev/examples/Animation-waapi-resolve-test.tsx | 87 +++++++ .../src/render/create-visual-element.ts | 16 +- .../src/animation/animators/js/index.ts | 7 + .../src/animation/hooks/use-animated-state.ts | 11 - .../interfaces/visual-element-target.ts | 8 +- packages/framer-motion/src/animation/types.ts | 13 +- .../framer-motion/src/frameloop/batcher.ts | 6 +- .../framer-motion/src/keyframes/Keyframes.ts | 215 +++++++++++++++++ .../src/keyframes/StyleKeyframes.ts | 212 ---------------- .../framer-motion/src/keyframes/scheduler.ts | 24 ++ .../framer-motion/src/render/VisualElement.ts | 24 -- .../src/render/dom/DOMVisualElement.ts | 24 -- .../dom/utils/css-variables-conversion.ts | 56 ----- .../src/render/dom/utils/parse-dom-variant.ts | 30 --- .../src/render/dom/utils/unit-conversion.ts | 226 +----------------- .../utils/__tests__/StateVisualElement.ts | 12 - .../framer-motion/src/render/utils/setters.ts | 6 +- 17 files changed, 353 insertions(+), 624 deletions(-) create mode 100644 dev/examples/Animation-waapi-resolve-test.tsx create mode 100644 packages/framer-motion/src/keyframes/Keyframes.ts delete mode 100644 packages/framer-motion/src/keyframes/StyleKeyframes.ts create mode 100644 packages/framer-motion/src/keyframes/scheduler.ts delete mode 100644 packages/framer-motion/src/render/dom/utils/parse-dom-variant.ts diff --git a/dev/examples/Animation-waapi-resolve-test.tsx b/dev/examples/Animation-waapi-resolve-test.tsx new file mode 100644 index 0000000000..91a996c539 --- /dev/null +++ b/dev/examples/Animation-waapi-resolve-test.tsx @@ -0,0 +1,87 @@ +import * as React from "react" +import { useEffect, useState } from "react" +import { motion, motionValue, useAnimate } from "framer-motion" +import { frame } from "framer-motion" + +/** + * An example of the tween transition type + */ + +const style = { + width: 100, + height: 100, + background: "white", +} + +const Child = ({ setState }: any) => { + const [width] = useState(100) + const [target, setTarget] = useState(0) + const transition = { + duration: 10, + } + + const [scope, animate] = useAnimate() + + useEffect(() => { + const animationA = scope.current.animate( + { opacity: 1 }, + { duration: 3, easing: "ease-in", fill: "both" } + ) + + console.log("a current time", animationA.effect.getKeyframes()) + + const animationB = scope.current.animate( + { transform: "translateX(100px)" }, + { duration: 3, easing: "ease-in", fill: "both" } + ) + + console.log("b current time", animationB.effect.getComputedTiming()) + + animationA.startTime = 100 + + console.log("a current time after set ", animationA.startTime) + console.log("b current time after set", animationB.startTime) + + // const controls = animate([ + // [ + // "div", + // { x: 500, opacity: 0 }, + // { type: "spring", duration: 1, bounce: 0 }, + // ], + // ]) + + // controls.then(() => { + // controls.play() + // }) + + // return () => controls.stop() + }, [target]) + + return ( +
+ { + setTarget(target + 100) + // setWidth(width + 100) + }} + initial={{ borderRadius: 10 }} + /> + {/*
setState(false)} /> */} +
+ ) + return +} + +export const App = () => { + const [state, setState] = useState(true) + + return state && +} diff --git a/packages/framer-motion-3d/src/render/create-visual-element.ts b/packages/framer-motion-3d/src/render/create-visual-element.ts index eada251e21..9076e1bc0f 100644 --- a/packages/framer-motion-3d/src/render/create-visual-element.ts +++ b/packages/framer-motion-3d/src/render/create-visual-element.ts @@ -1,15 +1,10 @@ import type { CreateVisualElement, - TargetAndTransition, ResolvedValues, MotionProps, } from "framer-motion" -import { - createBox, - checkTargetForNewValues, - VisualElement, -} from "framer-motion" +import { createBox, VisualElement } from "framer-motion" import { Object3DNode } from "@react-three/fiber" import { setThreeValue } from "./utils/set-value" @@ -41,15 +36,6 @@ export class ThreeVisualElement extends VisualElement< return a.id - b.id } - makeTargetAnimatableFromInstance({ - transition, - transitionEnd, - ...target - }: TargetAndTransition) { - checkTargetForNewValues(this, target, {}) - return { ...target, transition, transitionEnd } - } - removeValueFromRenderState() {} measureInstanceViewportBox() { diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts index fd3d02ac60..a7f35e6408 100644 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ b/packages/framer-motion/src/animation/animators/js/index.ts @@ -15,6 +15,7 @@ import { calcGeneratorDuration } from "../../generators/utils/calc-duration" import { invariant } from "../../../utils/errors" import { mix } from "../../../utils/mix" import { pipe } from "../../../utils/pipe" +import { Keyframes } from "../../../keyframes/Keyframes" type GeneratorFactory = ( options: ValueAnimationOptions @@ -57,6 +58,12 @@ export function animateValue({ onUpdate, ...options }: ValueAnimationOptions): MainThreadAnimationControls { + new Keyframes(undefined as any, "opacity", keyframes as any).ready.then( + () => { + console.log("keyframes ready") + } + ) + let speed = 1 let hasStopped = false 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 60ca87b4ec..f0f8ba8a7f 100644 --- a/packages/framer-motion/src/animation/hooks/use-animated-state.ts +++ b/packages/framer-motion/src/animation/hooks/use-animated-state.ts @@ -1,6 +1,5 @@ import { useEffect, useState } from "react" import { useConstant } from "../../utils/use-constant" -import { checkTargetForNewValues, getOrigin } from "../../render/utils/setters" import { TargetAndTransition } from "../../types" import { ResolvedValues } from "../../render/types" import { makeUseVisualState } from "../../motion/utils/use-visual-state" @@ -46,16 +45,6 @@ class StateVisualElement extends VisualElement< sortInstanceNodePosition() { return 0 } - - makeTargetAnimatableFromInstance({ - transition, - transitionEnd, - ...target - }: TargetAndTransition) { - const origin = getOrigin(target as any, transition || {}, this) - checkTargetForNewValues(this, target, origin as any) - return { transition, transitionEnd, ...target } - } } const useVisualState = makeUseVisualState({ 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 3a1d803c41..d4fa1e7058 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts @@ -43,14 +43,14 @@ function hasKeyframesChanged(value: MotionValue, target: Target) { export function animateTarget( visualElement: VisualElement, - definition: TargetAndTransition, + targetAndTransition: TargetAndTransition, { delay = 0, transitionOverride, type }: VisualElementAnimationOptions = {} ): AnimationPlaybackControls[] { let { transition = visualElement.getDefaultTransition(), transitionEnd, ...target - } = visualElement.makeTargetAnimatable(definition) + } = targetAndTransition const willChange = visualElement.getValue("willChange") @@ -67,6 +67,10 @@ export function animateTarget( const value = visualElement.getValue(key) const valueTarget = target[key] + /** + * TODO Probably need to make a motion value here if it doesnt exist + */ + if ( !value || valueTarget === undefined || diff --git a/packages/framer-motion/src/animation/types.ts b/packages/framer-motion/src/animation/types.ts index 152e7243e9..659dbd26bc 100644 --- a/packages/framer-motion/src/animation/types.ts +++ b/packages/framer-motion/src/animation/types.ts @@ -117,15 +117,14 @@ export interface CSSStyleDeclarationWithTransform skewY: number | string } -export type ValueKeyframe = T +export type ValueKeyframe = string | number -export type UnresolvedValueKeyframe = - ValueKeyframe | null +export type UnresolvedValueKeyframe = ValueKeyframe | null -export type ValueKeyframesDefinition = - | T - | T[] - | Array +export type ValueKeyframesDefinition = + | ValueKeyframe + | ValueKeyframe[] + | UnresolvedValueKeyframe[] export type StyleKeyframesDefinition = { [K in keyof CSSStyleDeclarationWithTransform]?: ValueKeyframesDefinition diff --git a/packages/framer-motion/src/frameloop/batcher.ts b/packages/framer-motion/src/frameloop/batcher.ts index c6dab37600..572ee15250 100644 --- a/packages/framer-motion/src/frameloop/batcher.ts +++ b/packages/framer-motion/src/frameloop/batcher.ts @@ -8,10 +8,10 @@ export const stepsOrder: StepId[] = [ "measure", // Read "renderTemporaryStyles", // Write "readTemporaryStyles", // Read - "update", // None - "preRender", // None + "update", // Compute + "preRender", // Compute "render", // Write - "postRender", // None + "postRender", // Compute ] const maxElapsed = 40 diff --git a/packages/framer-motion/src/keyframes/Keyframes.ts b/packages/framer-motion/src/keyframes/Keyframes.ts new file mode 100644 index 0000000000..c0f2f448f3 --- /dev/null +++ b/packages/framer-motion/src/keyframes/Keyframes.ts @@ -0,0 +1,215 @@ +import { VisualElement } from "../render/VisualElement" +import { getVariableValue } from "../render/dom/utils/css-variables-conversion" +import { isCSSVariableToken } from "../render/dom/utils/is-css-variable" +import { + isNumOrPxType, + positionalKeys, + positionalValues, + removeNonTranslationalTransform, +} from "../render/dom/utils/unit-conversion" +import { findDimensionValueType } from "../render/dom/value-types/dimensions" +import { deregisterKeyframes, registerKeyframes } from "./scheduler" + +export type UnresolvedKeyframes = Array + +export type ResolvedKeyframes = Array + +function forwardFillKeyframes( + keyframes: UnresolvedKeyframes +): ResolvedKeyframes { + // We know the first keyframe is not null, so we can coerce + const resolvedKeyframes: ResolvedKeyframes = [keyframes[0]!] + + for (let i = 1; i < keyframes.length; i++) { + resolvedKeyframes[i] = + keyframes[i] === null + ? (keyframes[i - 1] as any) + : (keyframes[i] as any) + } + + return resolvedKeyframes +} + +export class Keyframes { + private element: VisualElement + + private name: string + + private keyframes: UnresolvedKeyframes + + private resolvedKeyframes: ResolvedKeyframes + + private resolvedFinalKeyframe?: T + + private removedTransforms?: [string, string | number][] + + private measuredOrigin?: string | number + + restoreScrollY?: number + needsMeasurement = false + + resolve: (keyframes: ResolvedKeyframes) => void + ready = new Promise>( + (resolve) => (this.resolve = resolve) + ) + + constructor( + element: VisualElement, + valueName: string, + unresolvedKeyframes: UnresolvedKeyframes + ) { + this.element = element + this.name = valueName + this.keyframes = unresolvedKeyframes + + registerKeyframes(this) + } + + readKeyframes() { + const { keyframes, element, name } = this + + if (!element.current) return + + /** + * If any keyframe is a CSS variable, we need to find its value by sampling the element + */ + for (let i = 0; i < keyframes.length; i++) { + /** + * If the first keyframe is null, we need to find its value by sampling the element + */ + if (i === 0 && keyframes[0] === null) { + keyframes[0] = element.readValue(name) as T + } + + const keyframe = keyframes[i] + if (isCSSVariableToken(keyframe)) { + const resolved = getVariableValue(keyframe, element.current) + if (resolved !== undefined) { + keyframes[i] = resolved as T + } + + // If this variable is the final keyframe, set it as finalKeyframe + if (i === keyframes.length - 1) { + this.resolvedFinalKeyframe = keyframe + } + } + } + + this.resolvedKeyframes = forwardFillKeyframes(keyframes) + + /** + * Check to see if unit type has changed. If so schedule jobs that will + * temporarily set styles to the destination keyframes. + * Skip if we have more than two keyframes or this isn't a positional value. + * TODO: We can throw if there are multiple keyframes and the value type changes. + */ + if ( + !positionalKeys.has(name) || + keyframes.length !== 2 || + isCSSVariableToken( + this.resolvedKeyframes[this.resolvedKeyframes.length - 1] + ) + ) { + return + } + + const [origin, target] = this.resolvedKeyframes + const originType = findDimensionValueType(origin) + const targetType = findDimensionValueType(target) + + if (!originType || !targetType || originType === targetType) return + + /** + * If both values are numbers or pixels, we can animate between them by + * converting them to numbers. + */ + if (isNumOrPxType(originType) && isNumOrPxType(targetType)) { + this.resolvedKeyframes = this.resolvedKeyframes.map((v) => + typeof v === "string" ? parseFloat(v) : v + ) as any + } else if ( + originType.transform && + targetType.transform && + origin === 0 && + target === 0 + ) { + /** + * If one value or the other is 0, it's safe to coerce without + * measurement. + */ + if (origin === 0) { + keyframes[0] = targetType.transform!(0) + } else if (target === 0) { + keyframes[keyframes.length - 1] = originType.transform!(0) + } + } else { + this.needsMeasurement = true + } + } + + unsetTransforms() { + const { element, name, resolvedKeyframes } = this + + if (!element.current) return + + this.removedTransforms = removeNonTranslationalTransform(element) + + const finalKeyframe = resolvedKeyframes[resolvedKeyframes.length - 1] + + if (this.resolvedFinalKeyframe === undefined) { + this.resolvedFinalKeyframe = finalKeyframe + } + + element.getValue(name, finalKeyframe).jump(finalKeyframe) + } + + measureInitialState() { + const { element, name } = this + + if (!element.current) return + + if (name === "height") { + this.restoreScrollY = window.pageYOffset + } + + this.measuredOrigin = positionalValues[name]( + element.measureViewportBox(), + window.getComputedStyle(element.current) + ) + } + + renderTemporaryStyles() { + this.element.render() + } + + readTemporaryStyles() { + const { element, name, resolvedKeyframes } = this + + if (!element.current) return + + const value = element.getValue(name) + value && value.jump(this.measuredOrigin) + + resolvedKeyframes[resolvedKeyframes.length - 1] = positionalValues[ + name + ]( + element.measureViewportBox(), + window.getComputedStyle(element.current) + ) as any + + // If we removed transform values, reapply them before the next render + if (this.removedTransforms?.length) { + this.removedTransforms.forEach( + ([unsetTransformName, unsetTransformValue]) => { + element + .getValue(unsetTransformName)! + .set(unsetTransformValue) + } + ) + } + } + + cancel() { + deregisterKeyframes(this) + } +} diff --git a/packages/framer-motion/src/keyframes/StyleKeyframes.ts b/packages/framer-motion/src/keyframes/StyleKeyframes.ts deleted file mode 100644 index 6d6a870dda..0000000000 --- a/packages/framer-motion/src/keyframes/StyleKeyframes.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { Easing } from "../easing/types" -import { isEasingArray } from "../easing/utils/is-easing-array" -import { easingDefinitionToFunction } from "../easing/utils/map" -import { frame } from "../frameloop" -import type { VisualElement } from "../render/VisualElement" -import { getVariableValue } from "../render/dom/utils/css-variables-conversion" -import { isCSSVariableToken } from "../render/dom/utils/is-css-variable" -import { - isNumOrPxType, - positionalKeys, - positionalValues, - removeNonTranslationalTransform, -} from "../render/dom/utils/unit-conversion" -import { findDimensionValueType } from "../render/dom/value-types/dimensions" -import { interpolate } from "../utils/interpolate" -import { defaultOffset } from "../utils/offsets/default" - -export interface KeyframeOptions { - ease?: Easing | Easing[] - times?: number[] -} - -export type UnresolvedKeyframes = Array - -const isNull = (v: any): v is null => v === null - -export class StyleKeyframes { - private sample: (v: number) => T - - finalKeyframe: string | number - - constructor( - element: VisualElement, - valueName: string, - keyframes: UnresolvedKeyframes, - { - ease = "easeInOut", - times = defaultOffset(keyframes), - }: KeyframeOptions - ) { - let resolvedKeyframes: Array - - frame.read(() => { - if (!element.current) return - - /** - * If the first keyframe is null, we need to find its value by sampling the element - */ - if (isNull(keyframes[0])) { - keyframes[0] = element.readValue(valueName) as T - } - - /** - * If any keyframe is a CSS variable, we need to find its value by sampling the element - */ - for (let i = 0; i < keyframes.length; i++) { - const keyframe = keyframes[i] - if (isCSSVariableToken(keyframe)) { - const resolved = getVariableValue(keyframe, element.current) - if (resolved !== undefined) { - keyframes[i] = resolved as T - } - - // If this variable is the final keyframe, set it as finalKeyframe - if (i === keyframes.length - 1) { - this.finalKeyframe = keyframe - } - } - } - - // TODO: Fill nulls forward here - resolvedKeyframes = keyframes as T[] - - /** - * Check to see if unit type has changed. If so schedule jobs that will - * temporarily set styles to the destination keyframes. - * Skip if we have more than two keyframes or this isn't a positional value. - * TODO: We can throw if there are multiple keyframes and the value type changes. - */ - if ( - !positionalKeys.has(valueName) || - keyframes.length !== 2 || - isCSSVariableToken( - resolvedKeyframes[resolvedKeyframes.length - 1] - ) - ) { - return - } - - const [origin, target] = resolvedKeyframes - const originType = findDimensionValueType(origin) - const targetType = findDimensionValueType(target) - - if (!originType || !targetType || originType === targetType) return - - /** - * If both values are numbers or pixels, we can animate between them by - * converting them to numbers. - */ - if (isNumOrPxType(originType) && isNumOrPxType(targetType)) { - resolvedKeyframes = resolvedKeyframes.map((v) => - typeof v === "string" ? parseFloat(v) : v - ) - } else if ( - originType.transform && - targetType.transform && - origin === 0 && - target === 0 - ) { - /** - * If one value or the other is 0, it's safe to coerce without - * measurement. - */ - if (origin === 0) { - keyframes[0] = targetType.transform!(0) - } else if (target === 0) { - keyframes[keyframes.length - 1] = originType.transform!(0) - } - } else { - let unsetTransforms - let scrollY: number - let origin - - /** - * We can't coerce so now we need to do value conversion via DOM measurements. - */ - frame.unsetTransforms(() => { - if (!element.current) return - - unsetTransforms = removeNonTranslationalTransform(element) - - if (this.finalKeyframe === undefined) { - this.finalKeyframe = target - } - - element.getValue(valueName, target).jump(target) - }) - - frame.measure(() => { - if (!element.current) return - - if (valueName === "height") { - scrollY = window.pageYOffset - } - - const originBbox = element.measureViewportBox() - const computedStyle = window.getComputedStyle( - element.current - ) - - if (computedStyle.display === "none") { - element.setStaticValue( - "display", - (target.display as string) || "block" - ) - } - - origin = positionalValues[valueName]( - originBbox, - computedStyle - ) - }) - - frame.renderTemporaryStyles(element.render) - - frame.readTemporaryStyles(() => { - if (!element.current) return - - const targetBbox = element.measureViewportBox() - const value = element.getValue(valueName) - value && value.jump(origin) - resolvedKeyframes[resolvedKeyframes.length - 1] = - positionalValues[valueName]( - targetBbox, - window.getComputedStyle(element.current) - ) - - // If we removed transform values, reapply them before the next render - if (unsetTransforms.length) { - unsetTransforms.forEach(([key, value]) => { - element.getValue(key)!.set(value) - }) - } - }) - - if (scrollY !== undefined) { - // TODO: Move this to some kind of globally shared function like suspend and restore scroll - frame.render(() => { - window.scrollTo({ top: scrollY }) - }) - } - - frame.render(element.render) - } - }) - - frame.render(() => { - /** - * Easing functions can be externally defined as strings. Here we convert them - * into actual functions. - */ - const easingFunctions = isEasingArray(ease) - ? ease.map(easingDefinitionToFunction) - : easingDefinitionToFunction(ease) - - /** - * Create interpolate function based off the processed keyframes. - */ - this.sample = interpolate(resolvedKeyframes, times, easingFunctions) - }) - } -} diff --git a/packages/framer-motion/src/keyframes/scheduler.ts b/packages/framer-motion/src/keyframes/scheduler.ts new file mode 100644 index 0000000000..3d5361f417 --- /dev/null +++ b/packages/framer-motion/src/keyframes/scheduler.ts @@ -0,0 +1,24 @@ +import type { Keyframes } from "./Keyframes" + +const keyframesToResolve = new Set>() +let isScheduled = false + +export function registerKeyframes( + keyframes: Keyframes +) { + keyframesToResolve.add(keyframes) + + if (!isScheduled) { + isScheduled = true + } +} + +export function deregisterKeyframes( + keyframes: Keyframes +) { + keyframesToResolve.delete(keyframes) + + if (keyframesToResolve.size === 0) { + isScheduled = false + } +} diff --git a/packages/framer-motion/src/render/VisualElement.ts b/packages/framer-motion/src/render/VisualElement.ts index 34051371dd..cfb4a9bb77 100644 --- a/packages/framer-motion/src/render/VisualElement.ts +++ b/packages/framer-motion/src/render/VisualElement.ts @@ -10,7 +10,6 @@ import { MotionProps, MotionStyle } from "../motion/types" import { createBox } from "../projection/geometry/models" import { Box } from "../projection/geometry/types" import { IProjectionNode } from "../projection/node/types" -import { TargetAndTransition } from "../types" import { isRefObject } from "../utils/is-ref-object" import { initPrefersReducedMotion } from "../utils/reduced-motion" import { @@ -80,15 +79,6 @@ export abstract class VisualElement< */ abstract sortInstanceNodePosition(a: Instance, b: Instance): number - /** - * Take a target and make it animatable. For instance if provided - * height: "auto" we need to measure height in pixels and animate that instead. - */ - abstract makeTargetAnimatableFromInstance( - target: TargetAndTransition, - isLive: boolean - ): TargetAndTransition - /** * Measure the viewport-relative bounding box of the Instance. */ @@ -633,20 +623,6 @@ export abstract class VisualElement< this.latestValues[key] = value } - /** - * Make a target animatable by Popmotion. For instance, if we're - * trying to animate width from 100px to 100vw we need to measure 100vw - * in pixels to determine what we really need to animate to. This is also - * pluggable to support Framer's custom value types like Color, - * and CSS variables. - */ - makeTargetAnimatable( - target: TargetAndTransition, - canMutate = true - ): TargetAndTransition { - return this.makeTargetAnimatableFromInstance(target, canMutate) - } - /** * Update the provided props. Ensure any newly-added motion values are * added to our map, old ones removed, and listeners updated. diff --git a/packages/framer-motion/src/render/dom/DOMVisualElement.ts b/packages/framer-motion/src/render/dom/DOMVisualElement.ts index 4b772373ac..ad09ee85ec 100644 --- a/packages/framer-motion/src/render/dom/DOMVisualElement.ts +++ b/packages/framer-motion/src/render/dom/DOMVisualElement.ts @@ -1,10 +1,7 @@ -import { checkTargetForNewValues, getOrigin } from "../utils/setters" import { DOMVisualElementOptions } from "../dom/types" -import { parseDomVariant } from "../dom/utils/parse-dom-variant" import { VisualElement } from "../VisualElement" import { MotionProps } from "../../motion/types" import { MotionValue } from "../../value" -import { TargetAndTransition } from "../.." import { HTMLRenderState } from "../html/types" export abstract class DOMVisualElement< @@ -35,25 +32,4 @@ export abstract class DOMVisualElement< delete vars[key] delete style[key] } - - makeTargetAnimatableFromInstance( - { transition, transitionEnd, ...target }: TargetAndTransition, - isMounted: boolean - ): TargetAndTransition { - const origin = getOrigin(target as any, transition || {}, this) - - if (isMounted) { - checkTargetForNewValues(this, target, origin as any) - - const parsed = parseDomVariant(this, target, origin, transitionEnd) - transitionEnd = parsed.transitionEnd - target = parsed.target - } - - return { - transition, - transitionEnd, - ...target, - } - } } diff --git a/packages/framer-motion/src/render/dom/utils/css-variables-conversion.ts b/packages/framer-motion/src/render/dom/utils/css-variables-conversion.ts index d7ebee0a46..37e2cf0c08 100644 --- a/packages/framer-motion/src/render/dom/utils/css-variables-conversion.ts +++ b/packages/framer-motion/src/render/dom/utils/css-variables-conversion.ts @@ -1,7 +1,5 @@ -import { Target, TargetWithKeyframes } from "../../../types" import { invariant } from "../../../utils/errors" import { isNumericalString } from "../../../utils/is-numerical-string" -import type { VisualElement } from "../../VisualElement" import { isCSSVariableToken, CSSVariableToken } from "./is-css-variable" /** @@ -53,57 +51,3 @@ export function getVariableValue( ? getVariableValue(fallback, element, depth + 1) : fallback } - -/** - * Resolve CSS variables from - * - * @internal - */ -export function resolveCSSVariables( - visualElement: VisualElement, - { ...target }: TargetWithKeyframes, - transitionEnd: Target | undefined -): { target: TargetWithKeyframes; transitionEnd?: Target } { - const element = visualElement.current - if (!(element instanceof Element)) return { target, transitionEnd } - - // If `transitionEnd` isn't `undefined`, clone it. We could clone `target` and `transitionEnd` - // only if they change but I think this reads clearer and this isn't a performance-critical path. - if (transitionEnd) { - transitionEnd = { ...transitionEnd } - } - - // Go through existing `MotionValue`s and ensure any existing CSS variables are resolved - visualElement.values.forEach((value) => { - const current = value.get() - if (!isCSSVariableToken(current)) return - - const resolved = getVariableValue(current, element) - if (resolved) value.set(resolved) - }) - - // Cycle through every target property and resolve CSS variables. Currently - // we only read single-var properties like `var(--foo)`, not `calc(var(--foo) + 20px)` - for (const key in target) { - const current = target[key] - if (!isCSSVariableToken(current)) continue - - const resolved = getVariableValue(current, element) - - if (!resolved) continue - - // Clone target if it hasn't already been - target[key] = resolved - - if (!transitionEnd) transitionEnd = {} - - // If the user hasn't already set this key on `transitionEnd`, set it to the unresolved - // CSS variable. This will ensure that after the animation the component will reflect - // changes in the value of the CSS variable. - if (transitionEnd[key] === undefined) { - transitionEnd[key] = current - } - } - - return { target, transitionEnd } -} diff --git a/packages/framer-motion/src/render/dom/utils/parse-dom-variant.ts b/packages/framer-motion/src/render/dom/utils/parse-dom-variant.ts deleted file mode 100644 index 2668cfe970..0000000000 --- a/packages/framer-motion/src/render/dom/utils/parse-dom-variant.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Target, TargetWithKeyframes } from "../../../types" -import type { VisualElement } from "../../VisualElement" -import { resolveCSSVariables } from "./css-variables-conversion" -import { unitConversion } from "./unit-conversion" - -export type MakeTargetAnimatable = ( - visualElement: VisualElement, - target: TargetWithKeyframes, - origin?: Target, - transitionEnd?: Target -) => { - target: TargetWithKeyframes - transitionEnd?: Target -} - -/** - * Parse a DOM variant to make it animatable. This involves resolving CSS variables - * and ensuring animations like "20%" => "calc(50vw)" are performed in pixels. - */ -export const parseDomVariant: MakeTargetAnimatable = ( - visualElement, - target, - origin, - transitionEnd -) => { - const resolved = resolveCSSVariables(visualElement, target, transitionEnd) - target = resolved.target - transitionEnd = resolved.transitionEnd - return unitConversion(visualElement, target, origin, transitionEnd) -} diff --git a/packages/framer-motion/src/render/dom/utils/unit-conversion.ts b/packages/framer-motion/src/render/dom/utils/unit-conversion.ts index 08de7c9080..4a77e5b233 100644 --- a/packages/framer-motion/src/render/dom/utils/unit-conversion.ts +++ b/packages/framer-motion/src/render/dom/utils/unit-conversion.ts @@ -1,18 +1,12 @@ -import { Target, TargetWithKeyframes } from "../../../types" -import { isKeyframesTarget } from "../../../animation/utils/is-keyframes-target" -import { invariant } from "../../../utils/errors" import { MotionValue } from "../../../value" import { transformPropOrder } from "../../html/utils/transform" -import { ResolvedValues } from "../../types" -import { findDimensionValueType } from "../value-types/dimensions" import { Box } from "../../../projection/geometry/types" -import { isBrowser } from "../../../utils/is-browser" import type { VisualElement } from "../../VisualElement" import { ValueType } from "../../../value/types/types" import { number } from "../../../value/types/numbers" import { px } from "../../../value/types/numbers/units" -const positionalKeys = new Set([ +export const positionalKeys = new Set([ "width", "height", "top", @@ -24,12 +18,8 @@ const positionalKeys = new Set([ "translateX", "translateY", ]) -const isPositionalKey = (key: string) => positionalKeys.has(key) -const hasPositionalKey = (target: TargetWithKeyframes) => { - return Object.keys(target).some(isPositionalKey) -} -const isNumOrPxType = (v?: ValueType): v is ValueType => +export const isNumOrPxType = (v?: ValueType): v is ValueType => v === number || v === px type GetActualMeasurementInPixels = ( @@ -65,7 +55,7 @@ const nonTranslationalTransformKeys = transformPropOrder.filter( ) type RemovedTransforms = [string, string | number][] -function removeNonTranslationalTransform(visualElement: VisualElement) { +export function removeNonTranslationalTransform(visualElement: VisualElement) { const removedTransforms: RemovedTransforms = [] nonTranslationalTransformKeys.forEach((key) => { @@ -105,213 +95,3 @@ export const positionalValues: { [key: string]: GetActualMeasurementInPixels } = // Alias translate longform names positionalValues.translateX = positionalValues.x positionalValues.translateY = positionalValues.y - -const convertChangedValueTypes = ( - target: TargetWithKeyframes, - visualElement: VisualElement, - changedKeys: string[] -) => { - const originBbox = visualElement.measureViewportBox() - const element = visualElement.current - const elementComputedStyle = getComputedStyle(element!) - const { display } = elementComputedStyle - const origin: ResolvedValues = {} - - // If the element is currently set to display: "none", make it visible before - // measuring the target bounding box - if (display === "none") { - visualElement.setStaticValue( - "display", - (target.display as string) || "block" - ) - } - - /** - * Record origins before we render and update styles - */ - changedKeys.forEach((key) => { - origin[key] = positionalValues[key](originBbox, elementComputedStyle) - }) - - // Apply the latest values (as set in checkAndConvertChangedValueTypes) - visualElement.render() - - const targetBbox = visualElement.measureViewportBox() - - changedKeys.forEach((key) => { - // Restore styles to their **calculated computed style**, not their actual - // originally set style. This allows us to animate between equivalent pixel units. - const value = visualElement.getValue(key) - - value && value.jump(origin[key]) - target[key] = positionalValues[key](targetBbox, elementComputedStyle) - }) - - return target -} - -const checkAndConvertChangedValueTypes = ( - visualElement: VisualElement, - target: TargetWithKeyframes, - origin: Target = {}, - transitionEnd: Target = {} -): { target: TargetWithKeyframes; transitionEnd: Target } => { - target = { ...target } - transitionEnd = { ...transitionEnd } - - const targetPositionalKeys = Object.keys(target).filter(isPositionalKey) - - // We want to remove any transform values that could affect the element's bounding box before - // it's measured. We'll reapply these later. - let removedTransformValues: RemovedTransforms = [] - let hasAttemptedToRemoveTransformValues = false - - const changedValueTypeKeys: string[] = [] - - targetPositionalKeys.forEach((key) => { - const value = visualElement.getValue(key) as MotionValue< - number | string - > - if (!visualElement.hasValue(key)) return - - let from = origin[key] - let fromType = findDimensionValueType(from) - const to = target[key] - let toType - - // TODO: The current implementation of this basically throws an error - // if you try and do value conversion via keyframes. There's probably - // a way of doing this but the performance implications would need greater scrutiny, - // as it'd be doing multiple resize-remeasure operations. - if (isKeyframesTarget(to)) { - const numKeyframes = to.length - const fromIndex = to[0] === null ? 1 : 0 - from = to[fromIndex] - fromType = findDimensionValueType(from) - - for (let i = fromIndex; i < numKeyframes; i++) { - /** - * Don't allow wildcard keyframes to be used to detect - * a difference in value types. - */ - if (to[i] === null) break - - if (!toType) { - toType = findDimensionValueType(to[i]) - - invariant( - toType === fromType || - (isNumOrPxType(fromType) && isNumOrPxType(toType)), - "Keyframes must be of the same dimension as the current value" - ) - } else { - invariant( - findDimensionValueType(to[i]) === toType, - "All keyframes must be of the same type" - ) - } - } - } else { - toType = findDimensionValueType(to) - } - - if (fromType !== toType) { - // If they're both just number or px, convert them both to numbers rather than - // relying on resize/remeasure to convert (which is wasteful in this situation) - if (isNumOrPxType(fromType) && isNumOrPxType(toType)) { - const current = value.get() - if (typeof current === "string") { - value.set(parseFloat(current)) - } - if (typeof to === "string") { - target[key] = parseFloat(to) - } else if (Array.isArray(to) && toType === px) { - target[key] = to.map(parseFloat) - } - } else if ( - fromType?.transform && - toType?.transform && - (from === 0 || to === 0) - ) { - // If one or the other value is 0, it's safe to coerce it to the - // type of the other without measurement - if (from === 0) { - value.set((toType as any).transform(from)) - } else { - target[key] = (fromType as any).transform(to) - } - } else { - // If we're going to do value conversion via DOM measurements, we first - // need to remove non-positional transform values that could affect the bbox measurements. - if (!hasAttemptedToRemoveTransformValues) { - removedTransformValues = - removeNonTranslationalTransform(visualElement) - hasAttemptedToRemoveTransformValues = true - } - - changedValueTypeKeys.push(key) - transitionEnd[key] = - transitionEnd[key] !== undefined - ? transitionEnd[key] - : target[key] - - value.jump(to) - } - } - }) - - if (changedValueTypeKeys.length) { - const scrollY = - changedValueTypeKeys.indexOf("height") >= 0 - ? window.pageYOffset - : null - - const convertedTarget = convertChangedValueTypes( - target, - visualElement, - changedValueTypeKeys - ) - - // If we removed transform values, reapply them before the next render - if (removedTransformValues.length) { - removedTransformValues.forEach(([key, value]) => { - visualElement.getValue(key)!.set(value) - }) - } - - // Reapply original values - visualElement.render() - - // Restore scroll position - if (isBrowser && scrollY !== null) { - window.scrollTo({ top: scrollY }) - } - - return { target: convertedTarget, transitionEnd } - } else { - return { target, transitionEnd } - } -} - -/** - * Convert value types for x/y/width/height/top/left/bottom/right - * - * Allows animation between `'auto'` -> `'100%'` or `0` -> `'calc(50% - 10vw)'` - * - * @internal - */ -export function unitConversion( - visualElement: VisualElement, - target: TargetWithKeyframes, - origin?: Target, - transitionEnd?: Target -): { target: TargetWithKeyframes; transitionEnd?: Target } { - return hasPositionalKey(target) - ? checkAndConvertChangedValueTypes( - visualElement, - target, - origin, - transitionEnd - ) - : { target, transitionEnd } -} diff --git a/packages/framer-motion/src/render/utils/__tests__/StateVisualElement.ts b/packages/framer-motion/src/render/utils/__tests__/StateVisualElement.ts index 4d8850d84c..a9013348f4 100644 --- a/packages/framer-motion/src/render/utils/__tests__/StateVisualElement.ts +++ b/packages/framer-motion/src/render/utils/__tests__/StateVisualElement.ts @@ -1,9 +1,7 @@ import { ResolvedValues } from "../../types" -import { checkTargetForNewValues, getOrigin } from "../setters" import { MotionProps } from "../../../motion/types" import { createBox } from "../../../projection/geometry/models" import { VisualElement } from "../../VisualElement" -import { TargetAndTransition } from "../../.." export class StateVisualElement extends VisualElement< ResolvedValues, @@ -34,14 +32,4 @@ export class StateVisualElement extends VisualElement< ) { return options.initialState[key] || 0 } - - makeTargetAnimatableFromInstance({ - transition, - transitionEnd, - ...target - }: TargetAndTransition) { - const origin = getOrigin(target as any, transition || {}, this) - checkTargetForNewValues(this, target, origin as any) - return { transition, transitionEnd, ...target } - } } diff --git a/packages/framer-motion/src/render/utils/setters.ts b/packages/framer-motion/src/render/utils/setters.ts index 74f58c2543..5dd1f1c486 100644 --- a/packages/framer-motion/src/render/utils/setters.ts +++ b/packages/framer-motion/src/render/utils/setters.ts @@ -38,11 +38,7 @@ export function setTarget( definition: string | TargetAndTransition | TargetResolver ) { const resolved = resolveVariant(visualElement, definition) - let { - transitionEnd = {}, - transition = {}, - ...target - } = resolved ? visualElement.makeTargetAnimatable(resolved, false) : {} + let { transitionEnd = {}, transition = {}, ...target } = resolved || {} target = { ...target, ...transitionEnd } From 7ac9a879db56b6ee069e5ee93cb73b15953951a7 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Thu, 14 Dec 2023 16:25:56 +0100 Subject: [PATCH 04/44] Update --- packages/framer-motion/src/frameloop/batcher.ts | 4 ---- packages/framer-motion/src/frameloop/types.ts | 11 +---------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/framer-motion/src/frameloop/batcher.ts b/packages/framer-motion/src/frameloop/batcher.ts index 572ee15250..208ea92582 100644 --- a/packages/framer-motion/src/frameloop/batcher.ts +++ b/packages/framer-motion/src/frameloop/batcher.ts @@ -4,10 +4,6 @@ import { Batcher, Process, StepId, Steps, FrameData } from "./types" export const stepsOrder: StepId[] = [ "read", // Read - "unsetTransforms", // Write - "measure", // Read - "renderTemporaryStyles", // Write - "readTemporaryStyles", // Read "update", // Compute "preRender", // Compute "render", // Write diff --git a/packages/framer-motion/src/frameloop/types.ts b/packages/framer-motion/src/frameloop/types.ts index f9fbcf613b..88d37e673b 100644 --- a/packages/framer-motion/src/frameloop/types.ts +++ b/packages/framer-motion/src/frameloop/types.ts @@ -12,16 +12,7 @@ export interface Step { process: (data: FrameData) => void } -export type StepId = - | "read" - | "unsetTransforms" - | "measure" - | "renderTemporaryStyles" - | "readTemporaryStyles" - | "update" - | "preRender" - | "render" - | "postRender" +export type StepId = "read" | "update" | "preRender" | "render" | "postRender" export type CancelProcess = (process: Process) => void From 084aff68ec9ca7ddfecf564096280a8d17151520 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 15 Dec 2023 14:54:31 +0100 Subject: [PATCH 05/44] Fixing --- .../src/animation/animators/js/index.ts | 146 +++++++------ .../src/animation/interfaces/motion-value.ts | 155 +++++++------- .../interfaces/visual-element-target.ts | 17 +- .../src/animation/optimized-appear/types.ts | 13 +- packages/framer-motion/src/animation/types.ts | 2 + .../src/animation/utils/is-none.ts | 2 + .../src/animation/utils/keyframes.ts | 66 ------ .../framer-motion/src/frameloop/batcher.ts | 1 + packages/framer-motion/src/frameloop/types.ts | 8 +- .../framer-motion/src/keyframes/scheduler.ts | 24 --- .../framer-motion/src/render/VisualElement.ts | 20 +- .../src/render/html/HTMLVisualElement.ts | 14 ++ .../html/utils/HTMLKeyframesResolver.ts | 192 ++++++++++++++++++ .../render/html/utils/make-none-animatable.ts | 33 +++ .../src/render/utils/KeyframesResolver.ts | 81 ++++++++ packages/framer-motion/src/value/index.ts | 2 +- 16 files changed, 513 insertions(+), 263 deletions(-) delete mode 100644 packages/framer-motion/src/animation/utils/keyframes.ts delete mode 100644 packages/framer-motion/src/keyframes/scheduler.ts create mode 100644 packages/framer-motion/src/render/html/utils/HTMLKeyframesResolver.ts create mode 100644 packages/framer-motion/src/render/html/utils/make-none-animatable.ts create mode 100644 packages/framer-motion/src/render/utils/KeyframesResolver.ts diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts index a7f35e6408..76b3809479 100644 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ b/packages/framer-motion/src/animation/animators/js/index.ts @@ -15,7 +15,7 @@ import { calcGeneratorDuration } from "../../generators/utils/calc-duration" import { invariant } from "../../../utils/errors" import { mix } from "../../../utils/mix" import { pipe } from "../../../utils/pipe" -import { Keyframes } from "../../../keyframes/Keyframes" +import { Keyframes, ResolvedKeyframes } from "../../../keyframes/Keyframes" type GeneratorFactory = ( options: ValueAnimationOptions @@ -43,11 +43,13 @@ const percentToProgress = (percent: number) => percent / 100 * to be largely spec-compliant with WAAPI to allow fungibility * between the two. */ -export function animateValue({ +export function animateValue({ autoplay = true, delay = 0, driver = frameloopDriver, - keyframes, + keyframes: unresolvedKeyframes, + visualElement, + name, type = "keyframes", repeat = 0, repeatDelay = 0, @@ -58,18 +60,28 @@ export function animateValue({ onUpdate, ...options }: ValueAnimationOptions): MainThreadAnimationControls { - new Keyframes(undefined as any, "opacity", keyframes as any).ready.then( - () => { - console.log("keyframes ready") - } - ) - + let playState: AnimationPlayState = "idle" + let holdTime: number | null = null + let startTime: number | null = null + let cancelTime: number | null = null let speed = 1 - + let currentTime = 0 + let resolvedDuration = Infinity + let calculatedDuration: number | null = null + let totalDuration = Infinity let hasStopped = false let resolveFinishedPromise: VoidFunction let currentFinishedPromise: Promise + let generator: KeyframeGenerator | undefined + let mirroredGenerator: KeyframeGenerator | undefined + /** + * If this isn't the keyframes generator and we've been provided + * strings as keyframes, we need to interpolate these. + * TODO: Support velocity for units and complex value types/ + */ + let mapNumbersToKeyframes: undefined | ((t: number) => V) + /** * Resolve the current Promise every time we enter the * finished state. This is WAAPI-compatible behaviour. @@ -85,73 +97,64 @@ export function animateValue({ let animationDriver: DriverControls | undefined - const generatorFactory = types[type] || keyframesGeneratorFactory + const createGenerator = (keyframes: ResolvedKeyframes) => { + const generatorFactory = types[type] || keyframesGeneratorFactory + if ( + generatorFactory !== keyframesGeneratorFactory && + typeof keyframes[0] !== "number" + ) { + if (process.env.NODE_ENV !== "production") { + invariant( + keyframes.length === 2, + `Only two keyframes currently supported with spring and inertia animations. Trying to animate ${keyframes}` + ) + } - /** - * If this isn't the keyframes generator and we've been provided - * strings as keyframes, we need to interpolate these. - */ - let mapNumbersToKeyframes: undefined | ((t: number) => V) - if ( - generatorFactory !== keyframesGeneratorFactory && - typeof keyframes[0] !== "number" - ) { - if (process.env.NODE_ENV !== "production") { - invariant( - keyframes.length === 2, - `Only two keyframes currently supported with spring and inertia animations. Trying to animate ${keyframes}` - ) + mapNumbersToKeyframes = interpolate([0, 100], keyframes, { + clamp: false, + }) + keyframes = [0, 100] as any } - mapNumbersToKeyframes = pipe( - percentToProgress, - mix(keyframes[0], keyframes[1]) - ) as (t: number) => V - - keyframes = [0, 100] as any - } + generator = generatorFactory({ ...options, keyframes }) - const generator = generatorFactory({ ...options, keyframes }) - - let mirroredGenerator: KeyframeGenerator | undefined - if (repeatType === "mirror") { - mirroredGenerator = generatorFactory({ - ...options, - keyframes: [...keyframes].reverse(), - velocity: -(options.velocity || 0), - }) - } + if (repeatType === "mirror") { + mirroredGenerator = generatorFactory({ + ...options, + keyframes: [...keyframes].reverse(), + velocity: -(options.velocity || 0), + }) + } - let playState: AnimationPlayState = "idle" - let holdTime: number | null = null - let startTime: number | null = null - let cancelTime: number | null = null + /** + * If duration is undefined and we have repeat options, + * we need to calculate a duration from the generator. + * + * We set it to the generator itself to cache the duration. + * Any timeline resolver will need to have already precalculated + * the duration by this step. + */ + if (generator.calculatedDuration === null && repeat) { + generator.calculatedDuration = calcGeneratorDuration(generator) + } - /** - * If duration is undefined and we have repeat options, - * we need to calculate a duration from the generator. - * - * We set it to the generator itself to cache the duration. - * Any timeline resolver will need to have already precalculated - * the duration by this step. - */ - if (generator.calculatedDuration === null && repeat) { - generator.calculatedDuration = calcGeneratorDuration(generator) - } + calculatedDuration = generator.calculatedDuration - const { calculatedDuration } = generator + if (calculatedDuration !== null) { + resolvedDuration = calculatedDuration + repeatDelay + totalDuration = resolvedDuration * (repeat + 1) - repeatDelay + } - let resolvedDuration = Infinity - let totalDuration = Infinity + mapNumbersToKeyframes = pipe( + percentToProgress, + mix(keyframes[0], keyframes[1]) + ) as (t: number) => V - if (calculatedDuration !== null) { - resolvedDuration = calculatedDuration + repeatDelay - totalDuration = resolvedDuration * (repeat + 1) - repeatDelay + keyframes = [0, 100] as any } - let currentTime = 0 const tick = (timestamp: number) => { - if (startTime === null) return + if (startTime === null || !generator) return /** * requestAnimationFrame timestamps can come through as lower than @@ -301,6 +304,7 @@ export function animateValue({ if (!animationDriver) animationDriver = driver(tick) + // TODO Create microtask to set a time for this event stack const now = animationDriver.now() onPlay && onPlay() @@ -327,8 +331,16 @@ export function animateValue({ animationDriver.start() } - if (autoplay) { - play() + if (visualElement && name) { + visualElement.resolveKeyframes( + name, + unresolvedKeyframes, + (resolvedKeyframes) => { + createGenerator(resolvedKeyframes) + + autoplay && play() + } + ) } const controls = { diff --git a/packages/framer-motion/src/animation/interfaces/motion-value.ts b/packages/framer-motion/src/animation/interfaces/motion-value.ts index c2afc603f5..1e40f75d27 100644 --- a/packages/framer-motion/src/animation/interfaces/motion-value.ts +++ b/packages/framer-motion/src/animation/interfaces/motion-value.ts @@ -12,15 +12,17 @@ import { getValueTransition, isTransitionDefined } from "../utils/transitions" import { animateValue } from "../animators/js" import { AnimationPlaybackControls, ValueAnimationOptions } from "../types" import { MotionGlobalConfig } from "../../utils/GlobalConfig" +import { VisualElement } from "../../render/VisualElement" export const animateMotionValue = ( - valueName: string, + name: string, value: MotionValue, target: ResolvedValueTarget, - transition: Transition & { elapsed?: number; isHandoff?: boolean } = {} + transition: Transition & { elapsed?: number; isHandoff?: boolean } = {}, + visualElement: VisualElement ): StartAnimation => { return (onComplete: VoidFunction): AnimationPlaybackControls => { - const valueTransition = getValueTransition(transition, valueName) || {} + const valueTransition = getValueTransition(transition, name) || {} /** * Most transition values are currently completely overwritten by value-specific @@ -36,30 +38,8 @@ export const animateMotionValue = ( let { elapsed = 0 } = transition elapsed = elapsed - secondsToMilliseconds(delay) - const keyframes = getKeyframes( - value, - valueName, - target, - valueTransition - ) - - /** - * Check if we're able to animate between the start and end keyframes, - * and throw a warning if we're attempting to animate between one that's - * animatable and another that isn't. - */ - const originKeyframe = keyframes[0] - const targetKeyframe = keyframes[keyframes.length - 1] - const isOriginAnimatable = isAnimatable(valueName, originKeyframe) - const isTargetAnimatable = isAnimatable(valueName, targetKeyframe) - - warning( - isOriginAnimatable === isTargetAnimatable, - `You are trying to animate ${valueName} from "${originKeyframe}" to "${targetKeyframe}". ${originKeyframe} is not an animatable value - to enable this animation set ${originKeyframe} to a value animatable to ${targetKeyframe} via the \`style\` property.` - ) - let options: ValueAnimationOptions = { - keyframes, + keyframes: Array.isArray(target) ? target : [null, target], velocity: value.getVelocity(), ease: "easeOut", ...valueTransition, @@ -72,6 +52,8 @@ export const animateMotionValue = ( onComplete() valueTransition.onComplete && valueTransition.onComplete() }, + name, + visualElement, } /** @@ -81,7 +63,7 @@ export const animateMotionValue = ( if (!isTransitionDefined(valueTransition)) { options = { ...options, - ...getDefaultTransition(valueName, options), + ...getDefaultTransition(name, options), } } @@ -93,59 +75,80 @@ export const animateMotionValue = ( if (options.duration) { options.duration = secondsToMilliseconds(options.duration) } - if (options.repeatDelay) { options.repeatDelay = secondsToMilliseconds(options.repeatDelay) } - if ( - !isOriginAnimatable || - !isTargetAnimatable || - instantAnimationState.current || - valueTransition.type === false || - MotionGlobalConfig.skipAnimations - ) { - /** - * If we can't animate this value, or the global instant animation flag is set, - * or this is simply defined as an instant transition, return an instant transition. - */ - return createInstantAnimation( - instantAnimationState.current - ? { ...options, delay: 0 } - : options - ) - } - - /** - * Animate via WAAPI if possible. - */ - if ( - /** - * If this is a handoff animation, the optimised animation will be running via - * WAAPI. Therefore, this animation must be JS to ensure it runs "under" the - * optimised animation. - */ - !transition.isHandoff && - value.owner && - value.owner.current instanceof HTMLElement && - /** - * If we're outputting values to onUpdate then we can't use WAAPI as there's - * no way to read the value from WAAPI every frame. - */ - !value.owner.getProps().onUpdate - ) { - const acceleratedAnimation = createAcceleratedAnimation( - value, - valueName, - options - ) - - if (acceleratedAnimation) return acceleratedAnimation - } - - /** - * If we didn't create an accelerated animation, create a JS animation - */ return animateValue(options) } } + +// export const animateMotionValue = ( +// valueName: string, +// value: MotionValue, +// target: ResolvedValueTarget, +// transition: Transition & { elapsed?: number; isHandoff?: boolean } = {} +// ): StartAnimation => { +// return (onComplete: VoidFunction): AnimationPlaybackControls => { + +// /** +// * Check if we're able to animate between the start and end keyframes, +// * and throw a warning if we're attempting to animate between one that's +// * animatable and another that isn't. +// */ +// const originKeyframe = keyframes[0] +// const targetKeyframe = keyframes[keyframes.length - 1] +// const isOriginAnimatable = isAnimatable(valueName, originKeyframe) +// const isTargetAnimatable = isAnimatable(valueName, targetKeyframe) +// warning( +// isOriginAnimatable === isTargetAnimatable, +// `You are trying to animate ${valueName} from "${originKeyframe}" to "${targetKeyframe}". ${originKeyframe} is not an animatable value - to enable this animation set ${originKeyframe} to a value animatable to ${targetKeyframe} via the \`style\` property.` +// ) + +// if ( +// !isOriginAnimatable || +// !isTargetAnimatable || +// instantAnimationState.current || +// valueTransition.type === false +// ) { +// /** +// * If we can't animate this value, or the global instant animation flag is set, +// * or this is simply defined as an instant transition, return an instant transition. +// */ +// return createInstantAnimation( +// instantAnimationState.current +// ? { ...options, delay: 0 } +// : options +// ) +// } +// /** +// * Animate via WAAPI if possible. +// */ +// if ( +// /** +// * If this is a handoff animation, the optimised animation will be running via +// * WAAPI. Therefore, this animation must be JS to ensure it runs "under" the +// * optimised animation. +// */ +// !transition.isHandoff && +// value.owner && +// value.owner.current instanceof HTMLElement && +// /** +// * If we're outputting values to onUpdate then we can't use WAAPI as there's +// * no way to read the value from WAAPI every frame. +// */ +// !value.owner.getProps().onUpdate +// ) { +// const acceleratedAnimation = createAcceleratedAnimation( +// value, +// valueName, +// options +// ) +// if (acceleratedAnimation) return acceleratedAnimation +// } +// /** +// * If we didn't create an accelerated animation, create a JS animation +// */ +// return animateValue(options) +// } +// } 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 d4fa1e7058..ee8fbb40ff 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts @@ -64,15 +64,10 @@ export function animateTarget( visualElement.animationState.getState()[type] for (const key in target) { - const value = visualElement.getValue(key) + const value = visualElement.getValue(key, null) const valueTarget = target[key] - /** - * TODO Probably need to make a motion value here if it doesnt exist - */ - if ( - !value || valueTarget === undefined || (animationTypeState && shouldBlockAnimation(animationTypeState, key)) @@ -95,12 +90,7 @@ export function animateTarget( visualElement.getProps()[optimizedAppearDataAttribute] if (appearId) { - const elapsed = window.HandoffAppearAnimations( - appearId, - key, - value, - frame - ) + const elapsed = window.HandoffAppearAnimations(appearId, key) if (elapsed !== null) { valueTransition.elapsed = elapsed @@ -138,7 +128,8 @@ export function animateTarget( valueTarget, visualElement.shouldReduceMotion && transformProps.has(key) ? { type: false } - : valueTransition + : valueTransition, + visualElement ) ) diff --git a/packages/framer-motion/src/animation/optimized-appear/types.ts b/packages/framer-motion/src/animation/optimized-appear/types.ts index 17c07b791a..73e1bc229e 100644 --- a/packages/framer-motion/src/animation/optimized-appear/types.ts +++ b/packages/framer-motion/src/animation/optimized-appear/types.ts @@ -1,17 +1,6 @@ -import type { Batcher } from "../../frameloop/types" -import type { MotionValue } from "../../value" - export type HandoffFunction = ( storeId: string, - valueName: string, - /** - * Legacy arguments. This function is inlined as part of SSG so it can be there's - * a version mismatch between the main included Motion and the inlined script. - * - * Remove in early 2024. - */ - _value: MotionValue, - _frame: Batcher + valueName: string ) => null | number /** diff --git a/packages/framer-motion/src/animation/types.ts b/packages/framer-motion/src/animation/types.ts index 659dbd26bc..a65f95d89f 100644 --- a/packages/framer-motion/src/animation/types.ts +++ b/packages/framer-motion/src/animation/types.ts @@ -36,6 +36,8 @@ export interface ValueAnimationTransition export interface ValueAnimationOptions extends ValueAnimationTransition { keyframes: V[] + visualElement?: VisualElement + name?: string } export interface AnimationScope { diff --git a/packages/framer-motion/src/animation/utils/is-none.ts b/packages/framer-motion/src/animation/utils/is-none.ts index 9eb0c3c0bd..39de76960a 100644 --- a/packages/framer-motion/src/animation/utils/is-none.ts +++ b/packages/framer-motion/src/animation/utils/is-none.ts @@ -5,5 +5,7 @@ export function isNone(value: string | number | null) { return value === 0 } else if (value !== null) { return value === "none" || value === "0" || isZeroValueString(value) + } else { + return true } } diff --git a/packages/framer-motion/src/animation/utils/keyframes.ts b/packages/framer-motion/src/animation/utils/keyframes.ts deleted file mode 100644 index c1dfe3e1ec..0000000000 --- a/packages/framer-motion/src/animation/utils/keyframes.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { getAnimatableNone } from "../../render/dom/value-types/animatable-none" -import { Transition } from "../../types" -import { MotionValue } from "../../value" -import { ValueKeyframesDefinition } from "../types" -import { isAnimatable } from "./is-animatable" -import { isNone } from "./is-none" - -export function getKeyframes( - value: MotionValue, - valueName: string, - target: ValueKeyframesDefinition, - transition: Transition -): string[] | number[] { - const isTargetAnimatable = isAnimatable(valueName, target) - let keyframes: ValueKeyframesDefinition - - if (Array.isArray(target)) { - keyframes = [...target] - } else { - keyframes = [null, target] - } - - const defaultOrigin = - transition.from !== undefined ? transition.from : value.get() - - let animatableTemplateValue: string | undefined = undefined - const noneKeyframeIndexes: number[] = [] - - for (let i = 0; i < keyframes.length; i++) { - /** - * Fill null/wildcard keyframes - */ - if (keyframes[i] === null) { - keyframes[i] = i === 0 ? defaultOrigin : keyframes[i - 1] - } - - if (isNone(keyframes[i])) { - noneKeyframeIndexes.push(i) - } - - // TODO: Clean this conditional, it works for now - if ( - typeof keyframes[i] === "string" && - keyframes[i] !== "none" && - keyframes[i] !== "0" - ) { - animatableTemplateValue = keyframes[i] as string - } - } - - if ( - isTargetAnimatable && - noneKeyframeIndexes.length && - animatableTemplateValue - ) { - for (let i = 0; i < noneKeyframeIndexes.length; i++) { - const index = noneKeyframeIndexes[i] - keyframes[index] = getAnimatableNone( - valueName, - animatableTemplateValue - ) - } - } - - return keyframes as string[] | number[] -} diff --git a/packages/framer-motion/src/frameloop/batcher.ts b/packages/framer-motion/src/frameloop/batcher.ts index 208ea92582..00d187b3c8 100644 --- a/packages/framer-motion/src/frameloop/batcher.ts +++ b/packages/framer-motion/src/frameloop/batcher.ts @@ -4,6 +4,7 @@ import { Batcher, Process, StepId, Steps, FrameData } from "./types" export const stepsOrder: StepId[] = [ "read", // Read + "resolveKeyframes", // Write/Read/Write/Read "update", // Compute "preRender", // Compute "render", // Write diff --git a/packages/framer-motion/src/frameloop/types.ts b/packages/framer-motion/src/frameloop/types.ts index 88d37e673b..cb3b075b14 100644 --- a/packages/framer-motion/src/frameloop/types.ts +++ b/packages/framer-motion/src/frameloop/types.ts @@ -12,7 +12,13 @@ export interface Step { process: (data: FrameData) => void } -export type StepId = "read" | "update" | "preRender" | "render" | "postRender" +export type StepId = + | "read" + | "resolveKeyframes" + | "update" + | "preRender" + | "render" + | "postRender" export type CancelProcess = (process: Process) => void diff --git a/packages/framer-motion/src/keyframes/scheduler.ts b/packages/framer-motion/src/keyframes/scheduler.ts deleted file mode 100644 index 3d5361f417..0000000000 --- a/packages/framer-motion/src/keyframes/scheduler.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Keyframes } from "./Keyframes" - -const keyframesToResolve = new Set>() -let isScheduled = false - -export function registerKeyframes( - keyframes: Keyframes -) { - keyframesToResolve.add(keyframes) - - if (!isScheduled) { - isScheduled = true - } -} - -export function deregisterKeyframes( - keyframes: Keyframes -) { - keyframesToResolve.delete(keyframes) - - if (keyframesToResolve.size === 0) { - isScheduled = false - } -} diff --git a/packages/framer-motion/src/render/VisualElement.ts b/packages/framer-motion/src/render/VisualElement.ts index cfb4a9bb77..676887b5d2 100644 --- a/packages/framer-motion/src/render/VisualElement.ts +++ b/packages/framer-motion/src/render/VisualElement.ts @@ -41,6 +41,8 @@ import { Feature } from "../motion/features/Feature" import type { PresenceContextProps } from "../context/PresenceContext" import { variantProps } from "./utils/variant-props" import { visualElementStore } from "./store" +import { ResolvedKeyframes, UnresolvedKeyframes } from "../keyframes/Keyframes" +import { KeyframeResolver } from "./utils/KeyframesResolver" const featureNames = Object.keys(featureDefinitions) const numFeatures = featureNames.length @@ -141,6 +143,15 @@ export abstract class VisualElement< projection?: IProjectionNode ): void + abstract resolveKeyframes( + name: string, + keyframes: UnresolvedKeyframes, + // We use an onComplete callback here rather than a Promise as a Promise + // resolution is a microtask and we want to retain the ability to force + // the resolution of keyframes synchronously. + onComplete: (resolvedKeyframes: ResolvedKeyframes) => void + ): KeyframeResolver + /** * If the component child is provided as a motion value, handle subscriptions * with the renderer-specific VisualElement. @@ -775,10 +786,10 @@ export abstract class VisualElement< * value, we'll create one if none exists. */ getValue(key: string): MotionValue | undefined - getValue(key: string, defaultValue: string | number): MotionValue + getValue(key: string, defaultValue: string | number | null): MotionValue getValue( key: string, - defaultValue?: string | number + defaultValue?: string | number | null ): MotionValue | undefined { if (this.props.values && this.props.values[key]) { return this.props.values[key] @@ -787,7 +798,10 @@ export abstract class VisualElement< let value = this.values.get(key) if (value === undefined && defaultValue !== undefined) { - value = motionValue(defaultValue, { owner: this }) + value = motionValue( + defaultValue === null ? undefined : defaultValue, + { owner: this } + ) this.addValue(key, value) } diff --git a/packages/framer-motion/src/render/html/HTMLVisualElement.ts b/packages/framer-motion/src/render/html/HTMLVisualElement.ts index d9403625f5..651a59b9e6 100644 --- a/packages/framer-motion/src/render/html/HTMLVisualElement.ts +++ b/packages/framer-motion/src/render/html/HTMLVisualElement.ts @@ -14,6 +14,12 @@ import { MotionConfigContext } from "../../context/MotionConfigContext" import { isMotionValue } from "../../value/utils/is-motion-value" import type { ResolvedValues } from "../types" import type { IProjectionNode } from "../../projection/node/types" +import { + UnresolvedKeyframes, + ResolvedKeyframes, +} from "../../keyframes/Keyframes" +import { HTMLKeyframesResolver } from "./utils/HTMLKeyframesResolver" +import { KeyframeResolver } from "../utils/KeyframesResolver" export function getComputedStyle(element: HTMLElement) { return window.getComputedStyle(element) @@ -84,6 +90,14 @@ export class HTMLVisualElement extends DOMVisualElement< } } + resolveKeyframes( + name: string, + keyframes: UnresolvedKeyframes, + onComplete: (resolvedKeyframes: ResolvedKeyframes) => void + ): KeyframeResolver { + return new HTMLKeyframesResolver(this, name, keyframes, onComplete) + } + renderInstance( instance: HTMLElement, renderState: HTMLRenderState, diff --git a/packages/framer-motion/src/render/html/utils/HTMLKeyframesResolver.ts b/packages/framer-motion/src/render/html/utils/HTMLKeyframesResolver.ts new file mode 100644 index 0000000000..1f95bdebe2 --- /dev/null +++ b/packages/framer-motion/src/render/html/utils/HTMLKeyframesResolver.ts @@ -0,0 +1,192 @@ +import { isNone } from "../../../animation/utils/is-none" +import { + ResolvedKeyframes, + UnresolvedKeyframes, +} from "../../../keyframes/Keyframes" +import { VisualElement } from "../../VisualElement" +import { getVariableValue } from "../../dom/utils/css-variables-conversion" +import { isCSSVariableToken } from "../../dom/utils/is-css-variable" +import { + isNumOrPxType, + positionalKeys, + positionalValues, + removeNonTranslationalTransform, +} from "../../dom/utils/unit-conversion" +import { findDimensionValueType } from "../../dom/value-types/dimensions" +import { KeyframeResolver } from "../../utils/KeyframesResolver" +import { makeNoneKeyframesAnimatable } from "./make-none-animatable" + +/** + * TODO: Use information about whether we are animating via JS or WAAPI to + * decide whether to we need to resolve CSS vars / do type conversion here. + */ +export class HTMLKeyframesResolver< + T extends string | number +> extends KeyframeResolver { + private element: VisualElement + private name: string + private removedTransforms?: [string, string | number][] + private restoreScrollY?: number + private measuredOrigin?: string | number + unresolvedKeyframes: UnresolvedKeyframes + + constructor( + element: VisualElement, + name: string, + unresolvedKeyframes: UnresolvedKeyframes, + onComplete: (resolvedKeyframes: ResolvedKeyframes) => void + ) { + super(unresolvedKeyframes, onComplete) + this.element = element + this.name = name + } + + readKeyframes() { + const { unresolvedKeyframes, element, name } = this + + if (!element.current) return + + const noneKeyframeIndexes: number[] = [] + + /** + * If any keyframe is a CSS variable, we need to find its value by sampling the element + */ + for (let i = 0; i < unresolvedKeyframes.length; i++) { + if (unresolvedKeyframes[i] === null) { + /** + * If the first keyframe is null, we need to find its value by sampling the element + */ + if (i === 0) { + unresolvedKeyframes[0] = element.readValue(name) as T + } else { + unresolvedKeyframes[i] = unresolvedKeyframes[i - 1] + } + } + + const keyframe = unresolvedKeyframes[i] + if (isCSSVariableToken(keyframe)) { + const resolved = getVariableValue(keyframe, element.current) + if (resolved !== undefined) { + unresolvedKeyframes[i] = resolved as T + } + + // If this variable is the final keyframe, set it as finalKeyframe + if (i === unresolvedKeyframes.length - 1) { + // this.resolvedFinalKeyframe = keyframe + } + } + + if (isNone(unresolvedKeyframes[i])) { + noneKeyframeIndexes.push(i) + } + } + + if (noneKeyframeIndexes.length) { + makeNoneKeyframesAnimatable( + unresolvedKeyframes, + noneKeyframeIndexes, + name + ) + } + + /** + * Check to see if unit type has changed. If so schedule jobs that will + * temporarily set styles to the destination keyframes. + * Skip if we have more than two keyframes or this isn't a positional value. + * TODO: We can throw if there are multiple keyframes and the value type changes. + */ + if (!positionalKeys.has(name) || unresolvedKeyframes.length !== 2) { + return + } + + const [origin, target] = unresolvedKeyframes + const originType = findDimensionValueType(origin) + const targetType = findDimensionValueType(target) + + /** + * Either we don't recognise these value types or we can animate between them. + */ + if (!originType || !targetType || originType === targetType) return + + /** + * If both values are numbers or pixels, we can animate between them by + * converting them to numbers. + */ + if (isNumOrPxType(originType) && isNumOrPxType(targetType)) { + for (let i = 0; i < unresolvedKeyframes.length; i++) { + const value = unresolvedKeyframes[i] + if (typeof value === "string") { + unresolvedKeyframes[i] = parseFloat(value as string) + } + } + } else { + /** + * Else, the only way to resolve this is by measuring the element. + */ + this.needsMeasurement = true + } + } + + unsetTransforms() { + const { element, name, unresolvedKeyframes } = this + + if (!element.current) return + + this.removedTransforms = removeNonTranslationalTransform(element) + + const finalKeyframe = + unresolvedKeyframes[unresolvedKeyframes.length - 1] + + // if (this.resolvedFinalKeyframe === undefined) { + // this.resolvedFinalKeyframe = finalKeyframe + // } + + element.getValue(name, finalKeyframe).jump(finalKeyframe) + } + + measureInitialState() { + const { element, name } = this + + if (!element.current) return + + if (name === "height") { + this.restoreScrollY = window.pageYOffset + } + + this.measuredOrigin = positionalValues[name]( + element.measureViewportBox(), + window.getComputedStyle(element.current) + ) + } + + renderEndStyles() { + this.element.render() + } + + measureEndState() { + const { element, name, unresolvedKeyframes } = this + + if (!element.current) return + + const value = element.getValue(name) + value && value.jump(this.measuredOrigin) + + unresolvedKeyframes[unresolvedKeyframes.length - 1] = positionalValues[ + name + ]( + element.measureViewportBox(), + window.getComputedStyle(element.current) + ) as any + + // If we removed transform values, reapply them before the next render + if (this.removedTransforms?.length) { + this.removedTransforms.forEach( + ([unsetTransformName, unsetTransformValue]) => { + element + .getValue(unsetTransformName)! + .set(unsetTransformValue) + } + ) + } + } +} diff --git a/packages/framer-motion/src/render/html/utils/make-none-animatable.ts b/packages/framer-motion/src/render/html/utils/make-none-animatable.ts new file mode 100644 index 0000000000..a800b90e07 --- /dev/null +++ b/packages/framer-motion/src/render/html/utils/make-none-animatable.ts @@ -0,0 +1,33 @@ +import { getAnimatableNone } from "../../dom/value-types/animatable-none" +import { UnresolvedKeyframes } from "../../utils/KeyframesResolver" + +export function makeNoneKeyframesAnimatable( + unresolvedKeyframes: UnresolvedKeyframes, + noneKeyframeIndexes: number[], + name: string +) { + /** + * If we detected "none"-equivalent keyframes, we need to find a template + */ + let i = 0 + let animatableTemplate: string | undefined = undefined + while (i < unresolvedKeyframes.length && !animatableTemplate) { + if ( + typeof unresolvedKeyframes[i] === "string" && + unresolvedKeyframes[i] !== "none" && + unresolvedKeyframes[i] !== "0" + ) { + animatableTemplate = unresolvedKeyframes[i] as string + } + i++ + } + + if (animatableTemplate) { + for (const noneIndex of noneKeyframeIndexes) { + unresolvedKeyframes[noneIndex] = getAnimatableNone( + name, + animatableTemplate + ) + } + } +} diff --git a/packages/framer-motion/src/render/utils/KeyframesResolver.ts b/packages/framer-motion/src/render/utils/KeyframesResolver.ts new file mode 100644 index 0000000000..cab7c5b421 --- /dev/null +++ b/packages/framer-motion/src/render/utils/KeyframesResolver.ts @@ -0,0 +1,81 @@ +import { frame } from "../../frameloop" + +export type UnresolvedKeyframes = Array + +export type ResolvedKeyframes = Array + +const toResolve = new Set() +let isScheduled = false +let needsMeasurement = false + +function measureAllKeyframes() { + if (needsMeasurement) { + toResolve.forEach((resolver) => { + resolver.needsMeasurement && resolver.unsetTransforms() + }) + toResolve.forEach((resolver) => { + resolver.needsMeasurement && resolver.measureInitialState() + }) + toResolve.forEach((resolver) => { + resolver.needsMeasurement && resolver.renderEndStyles() + }) + toResolve.forEach((resolver) => { + resolver.needsMeasurement && resolver.measureEndState() + }) + } + + needsMeasurement = false + isScheduled = false + + toResolve.forEach((resolver) => { + resolver.complete() + }) + + toResolve.clear() +} + +function readAllKeyframes() { + toResolve.forEach((resolver) => { + resolver.readKeyframes() + + if (resolver.needsMeasurement) { + needsMeasurement = true + } + }) + + frame.resolveKeyframes(measureAllKeyframes) +} + +export abstract class KeyframeResolver { + resolvedKeyframes: ResolvedKeyframes | undefined + unresolvedKeyframes: UnresolvedKeyframes + private onComplete: (resolvedKeyframes: ResolvedKeyframes) => void + + constructor( + unresolvedKeyframes: UnresolvedKeyframes, + onComplete: (resolvedKeyframes: ResolvedKeyframes) => void + ) { + this.unresolvedKeyframes = unresolvedKeyframes + this.onComplete = onComplete + + toResolve.add(this) + + if (!isScheduled) { + isScheduled = true + frame.read(readAllKeyframes) + } + } + + needsMeasurement = false + + abstract readKeyframes(): void + abstract unsetTransforms(): void + abstract measureInitialState(): void + abstract renderEndStyles(): void + abstract measureEndState(): void + + complete() { + this.onComplete(this.unresolvedKeyframes as ResolvedKeyframes) + toResolve.delete(this) + } +} diff --git a/packages/framer-motion/src/value/index.ts b/packages/framer-motion/src/value/index.ts index d003dec0cd..6b8a53e6ff 100644 --- a/packages/framer-motion/src/value/index.ts +++ b/packages/framer-motion/src/value/index.ts @@ -77,7 +77,7 @@ export class MotionValue { /** * The current state of the `MotionValue`. */ - private current: V + private current: V | undefined /** * The previous state of the `MotionValue`. From 7672ee6da7e0b8b3bcae0fe6e12abf3ab56baf82 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 12 Jan 2024 09:28:12 +0100 Subject: [PATCH 06/44] Latest --- .../framer-motion/src/animation/animate.ts | 4 +- .../src/animation/animators/js/index.ts | 33 +-- .../src/animation/interfaces/motion-value.ts | 20 +- .../src/animation/interfaces/single-value.ts | 6 +- .../interfaces/visual-element-target.ts | 1 - .../src/animation/optimized-appear/types.ts | 7 +- .../drag/VisualElementDragControls.ts | 8 +- .../framer-motion/src/keyframes/Keyframes.ts | 215 ------------------ .../framer-motion/src/render/VisualElement.ts | 13 +- .../DOMKeyframesResolver.ts} | 45 ++-- .../src/render/dom/DOMVisualElement.ts | 14 ++ .../src/render/html/HTMLVisualElement.ts | 14 -- .../src/render/utils/KeyframesResolver.ts | 19 +- packages/framer-motion/src/value/index.ts | 6 +- 14 files changed, 93 insertions(+), 312 deletions(-) delete mode 100644 packages/framer-motion/src/keyframes/Keyframes.ts rename packages/framer-motion/src/render/{html/utils/HTMLKeyframesResolver.ts => dom/DOMKeyframesResolver.ts} (80%) diff --git a/packages/framer-motion/src/animation/animate.ts b/packages/framer-motion/src/animation/animate.ts index 28d6dbe5d2..05c2563d0b 100644 --- a/packages/framer-motion/src/animation/animate.ts +++ b/packages/framer-motion/src/animation/animate.ts @@ -141,7 +141,7 @@ export const createScopedAnimate = (scope?: AnimationScope) => { /** * Implementation */ - function scopedAnimate( + function scopedAnimate( valueOrElementOrSequence: | AnimationSequence | ElementOrSelector @@ -171,7 +171,7 @@ export const createScopedAnimate = (scope?: AnimationScope) => { ) } else { animation = animateSingleValue( - valueOrElementOrSequence, + valueOrElementOrSequence as MotionValue | V, keyframes as V | GenericKeyframesTarget, options as ValueAnimationTransition | undefined ) diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts index 76b3809479..0ba6d1f670 100644 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ b/packages/framer-motion/src/animation/animators/js/index.ts @@ -15,7 +15,7 @@ import { calcGeneratorDuration } from "../../generators/utils/calc-duration" import { invariant } from "../../../utils/errors" import { mix } from "../../../utils/mix" import { pipe } from "../../../utils/pipe" -import { Keyframes, ResolvedKeyframes } from "../../../keyframes/Keyframes" +import { ResolvedKeyframes } from "../../../render/utils/KeyframesResolver" type GeneratorFactory = ( options: ValueAnimationOptions @@ -74,7 +74,7 @@ export function animateValue({ let currentFinishedPromise: Promise let generator: KeyframeGenerator | undefined - let mirroredGenerator: KeyframeGenerator | undefined + let mirroredGenerator: KeyframeGenerator | undefined /** * If this isn't the keyframes generator and we've been provided * strings as keyframes, we need to interpolate these. @@ -97,8 +97,12 @@ export function animateValue({ let animationDriver: DriverControls | undefined + let initialKeyframe: V + const createGenerator = (keyframes: ResolvedKeyframes) => { + initialKeyframe = keyframes[0] const generatorFactory = types[type] || keyframesGeneratorFactory + if ( generatorFactory !== keyframesGeneratorFactory && typeof keyframes[0] !== "number" @@ -110,9 +114,11 @@ export function animateValue({ ) } - mapNumbersToKeyframes = interpolate([0, 100], keyframes, { - clamp: false, - }) + mapNumbersToKeyframes = pipe( + percentToProgress, + mix(keyframes[0], keyframes[1]) + ) as (t: number) => V + keyframes = [0, 100] as any } @@ -144,13 +150,6 @@ export function animateValue({ resolvedDuration = calculatedDuration + repeatDelay totalDuration = resolvedDuration * (repeat + 1) - repeatDelay } - - mapNumbersToKeyframes = pipe( - percentToProgress, - mix(keyframes[0], keyframes[1]) - ) as (t: number) => V - - keyframes = [0, 100] as any } const tick = (timestamp: number) => { @@ -251,11 +250,11 @@ export function animateValue({ * instantly. */ const state = isInDelayPhase - ? { done: false, value: keyframes[0] } + ? { done: false, value: initialKeyframe } : frameGenerator.next(elapsed) if (mapNumbersToKeyframes) { - state.value = mapNumbersToKeyframes(state.value) + state.value = mapNumbersToKeyframes(state.value as number) } let { done } = state @@ -337,7 +336,6 @@ export function animateValue({ unresolvedKeyframes, (resolvedKeyframes) => { createGenerator(resolvedKeyframes) - autoplay && play() } ) @@ -361,6 +359,11 @@ export function animateValue({ } }, get duration() { + // TODO: If no generator, flush pending keyframes + if (!generator) { + return 0 + } + const duration = generator.calculatedDuration === null ? calcGeneratorDuration(generator) diff --git a/packages/framer-motion/src/animation/interfaces/motion-value.ts b/packages/framer-motion/src/animation/interfaces/motion-value.ts index 1e40f75d27..86ea95b000 100644 --- a/packages/framer-motion/src/animation/interfaces/motion-value.ts +++ b/packages/framer-motion/src/animation/interfaces/motion-value.ts @@ -1,25 +1,19 @@ -import { warning } from "../../utils/errors" -import { ResolvedValueTarget, Transition } from "../../types" +import { Transition } from "../../types" import { secondsToMilliseconds } from "../../utils/time-conversion" -import { instantAnimationState } from "../../utils/use-instant-transition-state" import type { MotionValue, StartAnimation } from "../../value" -import { createAcceleratedAnimation } from "../animators/waapi/create-accelerated-animation" -import { createInstantAnimation } from "../animators/instant" import { getDefaultTransition } from "../utils/default-transitions" -import { isAnimatable } from "../utils/is-animatable" -import { getKeyframes } from "../utils/keyframes" import { getValueTransition, isTransitionDefined } from "../utils/transitions" import { animateValue } from "../animators/js" import { AnimationPlaybackControls, ValueAnimationOptions } from "../types" -import { MotionGlobalConfig } from "../../utils/GlobalConfig" import { VisualElement } from "../../render/VisualElement" +import type { UnresolvedKeyframes } from "../../render/utils/KeyframesResolver" -export const animateMotionValue = ( +export const animateMotionValue = ( name: string, - value: MotionValue, - target: ResolvedValueTarget, + value: MotionValue, + target: V | UnresolvedKeyframes, transition: Transition & { elapsed?: number; isHandoff?: boolean } = {}, - visualElement: VisualElement + visualElement?: VisualElement ): StartAnimation => { return (onComplete: VoidFunction): AnimationPlaybackControls => { const valueTransition = getValueTransition(transition, name) || {} @@ -105,7 +99,7 @@ export const animateMotionValue = ( // `You are trying to animate ${valueName} from "${originKeyframe}" to "${targetKeyframe}". ${originKeyframe} is not an animatable value - to enable this animation set ${originKeyframe} to a value animatable to ${targetKeyframe} via the \`style\` property.` // ) -// if ( +// if (MotionGlobalConfig.skipAnimations || // !isOriginAnimatable || // !isTargetAnimatable || // instantAnimationState.current || diff --git a/packages/framer-motion/src/animation/interfaces/single-value.ts b/packages/framer-motion/src/animation/interfaces/single-value.ts index 43533a6f8c..ccd18a64b5 100644 --- a/packages/framer-motion/src/animation/interfaces/single-value.ts +++ b/packages/framer-motion/src/animation/interfaces/single-value.ts @@ -4,16 +4,14 @@ import { isMotionValue } from "../../value/utils/is-motion-value" import { GenericKeyframesTarget } from "../../types" import { AnimationPlaybackControls, ValueAnimationTransition } from "../types" -export function animateSingleValue( +export function animateSingleValue( value: MotionValue | V, keyframes: V | GenericKeyframesTarget, options?: ValueAnimationTransition ): AnimationPlaybackControls { const motionValue = isMotionValue(value) ? value : createMotionValue(value) - motionValue.start( - animateMotionValue("", motionValue, keyframes as any, options) - ) + motionValue.start(animateMotionValue("", motionValue, keyframes, options)) return motionValue.animation! } 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 ee8fbb40ff..a4d2e31e10 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts @@ -10,7 +10,6 @@ import { setTarget } from "../../render/utils/setters" import { AnimationPlaybackControls } from "../types" import { getValueTransition } from "../utils/transitions" import { MotionValue } from "../../value" -import { frame } from "../../frameloop" /** * Decide whether we should block this animation. Previously, we achieved this diff --git a/packages/framer-motion/src/animation/optimized-appear/types.ts b/packages/framer-motion/src/animation/optimized-appear/types.ts index 73e1bc229e..aa1686aaac 100644 --- a/packages/framer-motion/src/animation/optimized-appear/types.ts +++ b/packages/framer-motion/src/animation/optimized-appear/types.ts @@ -1,6 +1,11 @@ +import type { Batcher } from "../../frameloop/types" +import type { MotionValue } from "../../value" + export type HandoffFunction = ( storeId: string, - valueName: string + valueName: string, + _value?: MotionValue, + _frame?: Batcher ) => null | number /** diff --git a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts index b0f69416d0..76395a108a 100644 --- a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts +++ b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts @@ -441,7 +441,13 @@ export class VisualElementDragControls { ) { const axisValue = this.getAxisMotionValue(axis) return axisValue.start( - animateMotionValue(axis, axisValue, 0, transition) + animateMotionValue( + axis, + axisValue, + 0, + transition, + this.visualElement + ) ) } diff --git a/packages/framer-motion/src/keyframes/Keyframes.ts b/packages/framer-motion/src/keyframes/Keyframes.ts deleted file mode 100644 index c0f2f448f3..0000000000 --- a/packages/framer-motion/src/keyframes/Keyframes.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { VisualElement } from "../render/VisualElement" -import { getVariableValue } from "../render/dom/utils/css-variables-conversion" -import { isCSSVariableToken } from "../render/dom/utils/is-css-variable" -import { - isNumOrPxType, - positionalKeys, - positionalValues, - removeNonTranslationalTransform, -} from "../render/dom/utils/unit-conversion" -import { findDimensionValueType } from "../render/dom/value-types/dimensions" -import { deregisterKeyframes, registerKeyframes } from "./scheduler" - -export type UnresolvedKeyframes = Array - -export type ResolvedKeyframes = Array - -function forwardFillKeyframes( - keyframes: UnresolvedKeyframes -): ResolvedKeyframes { - // We know the first keyframe is not null, so we can coerce - const resolvedKeyframes: ResolvedKeyframes = [keyframes[0]!] - - for (let i = 1; i < keyframes.length; i++) { - resolvedKeyframes[i] = - keyframes[i] === null - ? (keyframes[i - 1] as any) - : (keyframes[i] as any) - } - - return resolvedKeyframes -} - -export class Keyframes { - private element: VisualElement - - private name: string - - private keyframes: UnresolvedKeyframes - - private resolvedKeyframes: ResolvedKeyframes - - private resolvedFinalKeyframe?: T - - private removedTransforms?: [string, string | number][] - - private measuredOrigin?: string | number - - restoreScrollY?: number - needsMeasurement = false - - resolve: (keyframes: ResolvedKeyframes) => void - ready = new Promise>( - (resolve) => (this.resolve = resolve) - ) - - constructor( - element: VisualElement, - valueName: string, - unresolvedKeyframes: UnresolvedKeyframes - ) { - this.element = element - this.name = valueName - this.keyframes = unresolvedKeyframes - - registerKeyframes(this) - } - - readKeyframes() { - const { keyframes, element, name } = this - - if (!element.current) return - - /** - * If any keyframe is a CSS variable, we need to find its value by sampling the element - */ - for (let i = 0; i < keyframes.length; i++) { - /** - * If the first keyframe is null, we need to find its value by sampling the element - */ - if (i === 0 && keyframes[0] === null) { - keyframes[0] = element.readValue(name) as T - } - - const keyframe = keyframes[i] - if (isCSSVariableToken(keyframe)) { - const resolved = getVariableValue(keyframe, element.current) - if (resolved !== undefined) { - keyframes[i] = resolved as T - } - - // If this variable is the final keyframe, set it as finalKeyframe - if (i === keyframes.length - 1) { - this.resolvedFinalKeyframe = keyframe - } - } - } - - this.resolvedKeyframes = forwardFillKeyframes(keyframes) - - /** - * Check to see if unit type has changed. If so schedule jobs that will - * temporarily set styles to the destination keyframes. - * Skip if we have more than two keyframes or this isn't a positional value. - * TODO: We can throw if there are multiple keyframes and the value type changes. - */ - if ( - !positionalKeys.has(name) || - keyframes.length !== 2 || - isCSSVariableToken( - this.resolvedKeyframes[this.resolvedKeyframes.length - 1] - ) - ) { - return - } - - const [origin, target] = this.resolvedKeyframes - const originType = findDimensionValueType(origin) - const targetType = findDimensionValueType(target) - - if (!originType || !targetType || originType === targetType) return - - /** - * If both values are numbers or pixels, we can animate between them by - * converting them to numbers. - */ - if (isNumOrPxType(originType) && isNumOrPxType(targetType)) { - this.resolvedKeyframes = this.resolvedKeyframes.map((v) => - typeof v === "string" ? parseFloat(v) : v - ) as any - } else if ( - originType.transform && - targetType.transform && - origin === 0 && - target === 0 - ) { - /** - * If one value or the other is 0, it's safe to coerce without - * measurement. - */ - if (origin === 0) { - keyframes[0] = targetType.transform!(0) - } else if (target === 0) { - keyframes[keyframes.length - 1] = originType.transform!(0) - } - } else { - this.needsMeasurement = true - } - } - - unsetTransforms() { - const { element, name, resolvedKeyframes } = this - - if (!element.current) return - - this.removedTransforms = removeNonTranslationalTransform(element) - - const finalKeyframe = resolvedKeyframes[resolvedKeyframes.length - 1] - - if (this.resolvedFinalKeyframe === undefined) { - this.resolvedFinalKeyframe = finalKeyframe - } - - element.getValue(name, finalKeyframe).jump(finalKeyframe) - } - - measureInitialState() { - const { element, name } = this - - if (!element.current) return - - if (name === "height") { - this.restoreScrollY = window.pageYOffset - } - - this.measuredOrigin = positionalValues[name]( - element.measureViewportBox(), - window.getComputedStyle(element.current) - ) - } - - renderTemporaryStyles() { - this.element.render() - } - - readTemporaryStyles() { - const { element, name, resolvedKeyframes } = this - - if (!element.current) return - - const value = element.getValue(name) - value && value.jump(this.measuredOrigin) - - resolvedKeyframes[resolvedKeyframes.length - 1] = positionalValues[ - name - ]( - element.measureViewportBox(), - window.getComputedStyle(element.current) - ) as any - - // If we removed transform values, reapply them before the next render - if (this.removedTransforms?.length) { - this.removedTransforms.forEach( - ([unsetTransformName, unsetTransformValue]) => { - element - .getValue(unsetTransformName)! - .set(unsetTransformValue) - } - ) - } - } - - cancel() { - deregisterKeyframes(this) - } -} diff --git a/packages/framer-motion/src/render/VisualElement.ts b/packages/framer-motion/src/render/VisualElement.ts index 676887b5d2..66075c2560 100644 --- a/packages/framer-motion/src/render/VisualElement.ts +++ b/packages/framer-motion/src/render/VisualElement.ts @@ -41,8 +41,11 @@ import { Feature } from "../motion/features/Feature" import type { PresenceContextProps } from "../context/PresenceContext" import { variantProps } from "./utils/variant-props" import { visualElementStore } from "./store" -import { ResolvedKeyframes, UnresolvedKeyframes } from "../keyframes/Keyframes" -import { KeyframeResolver } from "./utils/KeyframesResolver" +import { + KeyframeResolver, + ResolvedKeyframes, + UnresolvedKeyframes, +} from "./utils/KeyframesResolver" const featureNames = Object.keys(featureDefinitions) const numFeatures = featureNames.length @@ -143,14 +146,16 @@ export abstract class VisualElement< projection?: IProjectionNode ): void - abstract resolveKeyframes( + resolveKeyframes( name: string, keyframes: UnresolvedKeyframes, // We use an onComplete callback here rather than a Promise as a Promise // resolution is a microtask and we want to retain the ability to force // the resolution of keyframes synchronously. onComplete: (resolvedKeyframes: ResolvedKeyframes) => void - ): KeyframeResolver + ): KeyframeResolver { + return new KeyframeResolver(this, name, keyframes, onComplete) + } /** * If the component child is provided as a motion value, handle subscriptions diff --git a/packages/framer-motion/src/render/html/utils/HTMLKeyframesResolver.ts b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts similarity index 80% rename from packages/framer-motion/src/render/html/utils/HTMLKeyframesResolver.ts rename to packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts index 1f95bdebe2..24e0798ed9 100644 --- a/packages/framer-motion/src/render/html/utils/HTMLKeyframesResolver.ts +++ b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts @@ -1,45 +1,26 @@ -import { isNone } from "../../../animation/utils/is-none" -import { - ResolvedKeyframes, - UnresolvedKeyframes, -} from "../../../keyframes/Keyframes" -import { VisualElement } from "../../VisualElement" -import { getVariableValue } from "../../dom/utils/css-variables-conversion" -import { isCSSVariableToken } from "../../dom/utils/is-css-variable" +import { isNone } from "../../animation/utils/is-none" +import { getVariableValue } from "./utils/css-variables-conversion" +import { isCSSVariableToken } from "./utils/is-css-variable" import { isNumOrPxType, positionalKeys, positionalValues, removeNonTranslationalTransform, -} from "../../dom/utils/unit-conversion" -import { findDimensionValueType } from "../../dom/value-types/dimensions" -import { KeyframeResolver } from "../../utils/KeyframesResolver" -import { makeNoneKeyframesAnimatable } from "./make-none-animatable" +} from "./utils/unit-conversion" +import { findDimensionValueType } from "./value-types/dimensions" +import { KeyframeResolver } from "../utils/KeyframesResolver" +import { makeNoneKeyframesAnimatable } from "../html/utils/make-none-animatable" /** * TODO: Use information about whether we are animating via JS or WAAPI to * decide whether to we need to resolve CSS vars / do type conversion here. */ -export class HTMLKeyframesResolver< +export class DOMKeyframesResolver< T extends string | number -> extends KeyframeResolver { - private element: VisualElement - private name: string +> extends KeyframeResolver { private removedTransforms?: [string, string | number][] - private restoreScrollY?: number + // private restoreScrollY?: number private measuredOrigin?: string | number - unresolvedKeyframes: UnresolvedKeyframes - - constructor( - element: VisualElement, - name: string, - unresolvedKeyframes: UnresolvedKeyframes, - onComplete: (resolvedKeyframes: ResolvedKeyframes) => void - ) { - super(unresolvedKeyframes, onComplete) - this.element = element - this.name = name - } readKeyframes() { const { unresolvedKeyframes, element, name } = this @@ -149,9 +130,9 @@ export class HTMLKeyframesResolver< if (!element.current) return - if (name === "height") { - this.restoreScrollY = window.pageYOffset - } + // if (name === "height") { + // this.restoreScrollY = window.pageYOffset + // } this.measuredOrigin = positionalValues[name]( element.measureViewportBox(), diff --git a/packages/framer-motion/src/render/dom/DOMVisualElement.ts b/packages/framer-motion/src/render/dom/DOMVisualElement.ts index ad09ee85ec..81075cb119 100644 --- a/packages/framer-motion/src/render/dom/DOMVisualElement.ts +++ b/packages/framer-motion/src/render/dom/DOMVisualElement.ts @@ -3,6 +3,12 @@ import { VisualElement } from "../VisualElement" import { MotionProps } from "../../motion/types" import { MotionValue } from "../../value" import { HTMLRenderState } from "../html/types" +import type { + KeyframeResolver, + ResolvedKeyframes, + UnresolvedKeyframes, +} from "../utils/KeyframesResolver" +import { DOMKeyframesResolver } from "./DOMKeyframesResolver" export abstract class DOMVisualElement< Instance extends HTMLElement | SVGElement = HTMLElement, @@ -32,4 +38,12 @@ export abstract class DOMVisualElement< delete vars[key] delete style[key] } + + resolveKeyframes( + name: string, + keyframes: UnresolvedKeyframes, + onComplete: (resolvedKeyframes: ResolvedKeyframes) => void + ): KeyframeResolver { + return new DOMKeyframesResolver(this, name, keyframes, onComplete) + } } diff --git a/packages/framer-motion/src/render/html/HTMLVisualElement.ts b/packages/framer-motion/src/render/html/HTMLVisualElement.ts index 651a59b9e6..d9403625f5 100644 --- a/packages/framer-motion/src/render/html/HTMLVisualElement.ts +++ b/packages/framer-motion/src/render/html/HTMLVisualElement.ts @@ -14,12 +14,6 @@ import { MotionConfigContext } from "../../context/MotionConfigContext" import { isMotionValue } from "../../value/utils/is-motion-value" import type { ResolvedValues } from "../types" import type { IProjectionNode } from "../../projection/node/types" -import { - UnresolvedKeyframes, - ResolvedKeyframes, -} from "../../keyframes/Keyframes" -import { HTMLKeyframesResolver } from "./utils/HTMLKeyframesResolver" -import { KeyframeResolver } from "../utils/KeyframesResolver" export function getComputedStyle(element: HTMLElement) { return window.getComputedStyle(element) @@ -90,14 +84,6 @@ export class HTMLVisualElement extends DOMVisualElement< } } - resolveKeyframes( - name: string, - keyframes: UnresolvedKeyframes, - onComplete: (resolvedKeyframes: ResolvedKeyframes) => void - ): KeyframeResolver { - return new HTMLKeyframesResolver(this, name, keyframes, onComplete) - } - renderInstance( instance: HTMLElement, renderState: HTMLRenderState, diff --git a/packages/framer-motion/src/render/utils/KeyframesResolver.ts b/packages/framer-motion/src/render/utils/KeyframesResolver.ts index cab7c5b421..4a1cc36812 100644 --- a/packages/framer-motion/src/render/utils/KeyframesResolver.ts +++ b/packages/framer-motion/src/render/utils/KeyframesResolver.ts @@ -1,4 +1,5 @@ import { frame } from "../../frameloop" +import type { VisualElement } from "../VisualElement" export type UnresolvedKeyframes = Array @@ -46,15 +47,21 @@ function readAllKeyframes() { frame.resolveKeyframes(measureAllKeyframes) } -export abstract class KeyframeResolver { +export class KeyframeResolver { + element: VisualElement + name: string resolvedKeyframes: ResolvedKeyframes | undefined unresolvedKeyframes: UnresolvedKeyframes private onComplete: (resolvedKeyframes: ResolvedKeyframes) => void constructor( + element: VisualElement, + name: string, unresolvedKeyframes: UnresolvedKeyframes, onComplete: (resolvedKeyframes: ResolvedKeyframes) => void ) { + this.element = element + this.name = name this.unresolvedKeyframes = unresolvedKeyframes this.onComplete = onComplete @@ -68,11 +75,11 @@ export abstract class KeyframeResolver { needsMeasurement = false - abstract readKeyframes(): void - abstract unsetTransforms(): void - abstract measureInitialState(): void - abstract renderEndStyles(): void - abstract measureEndState(): void + readKeyframes() {} + unsetTransforms() {} + measureInitialState() {} + renderEndStyles() {} + measureEndState() {} complete() { this.onComplete(this.unresolvedKeyframes as ResolvedKeyframes) diff --git a/packages/framer-motion/src/value/index.ts b/packages/framer-motion/src/value/index.ts index 6b8a53e6ff..7c79c3cee4 100644 --- a/packages/framer-motion/src/value/index.ts +++ b/packages/framer-motion/src/value/index.ts @@ -112,9 +112,7 @@ export class MotionValue { private stopPassiveEffect?: VoidFunction /** - * A reference to the currently-controlling Popmotion animation - * - * + * A reference to the currently-controlling animation. */ animation?: AnimationPlaybackControls @@ -334,7 +332,7 @@ export class MotionValue { collectMotionValues.current.push(this) } - return this.current + return this.current! } /** From 37adbfff473ddfe5362a8d7c09eec42c62dabdf6 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 12 Jan 2024 14:46:51 +0100 Subject: [PATCH 07/44] Fixing Motion 3D tests --- .../src/render/__tests__/index.test.tsx | 10 +- .../animators/__tests__/instant.test.ts | 91 ------------------- .../src/animation/animators/instant.ts | 46 ---------- .../src/animation/animators/js/index.ts | 8 +- .../src/animation/interfaces/motion-value.ts | 15 +++ .../src/render/dom/DOMKeyframesResolver.ts | 13 +-- .../src/render/utils/KeyframesResolver.ts | 24 ++++- 7 files changed, 50 insertions(+), 157 deletions(-) delete mode 100644 packages/framer-motion/src/animation/animators/__tests__/instant.test.ts delete mode 100644 packages/framer-motion/src/animation/animators/instant.ts diff --git a/packages/framer-motion-3d/src/render/__tests__/index.test.tsx b/packages/framer-motion-3d/src/render/__tests__/index.test.tsx index f3810f3b9e..e5d47e1c4b 100644 --- a/packages/framer-motion-3d/src/render/__tests__/index.test.tsx +++ b/packages/framer-motion-3d/src/render/__tests__/index.test.tsx @@ -58,6 +58,7 @@ describe("motion for three", () => { ReactThreeTestRenderer.create() }) + expect(result.length).toBeGreaterThan(1) const lastFrame = result[result.length - 1] expect(lastFrame).toEqual({ scale: 5, @@ -230,8 +231,8 @@ describe("motion for three", () => { transition: { x: { type: false } }, }, off: { - x: 0, - y: 0, + x: 1, + y: 1, transition: { x: { type: false } }, }, }} @@ -240,10 +241,9 @@ describe("motion for three", () => { } ReactThreeTestRenderer.create() - frame.update(() => resolve([x.get(), y.get()])) + frame.postRender(() => resolve([x.get(), y.get()])) }) - expect(result[0]).toEqual(100) - expect(result[1]).toEqual(0) + expect(result).toEqual([100, 1]) }) }) diff --git a/packages/framer-motion/src/animation/animators/__tests__/instant.test.ts b/packages/framer-motion/src/animation/animators/__tests__/instant.test.ts deleted file mode 100644 index 3757511bf8..0000000000 --- a/packages/framer-motion/src/animation/animators/__tests__/instant.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { createInstantAnimation } from "../instant" - -describe("instantAnimation", () => { - test("Is instant, await", async () => { - const onUpdate = jest.fn() - const onComplete = jest.fn() - - await createInstantAnimation({ - keyframes: [0, 1], - onUpdate, - onComplete, - }) - - expect(onUpdate).toBeCalledWith(1) - expect(onComplete).toBeCalled() - }) - - test("Is instant, .then()", async () => { - const onUpdate = jest.fn() - const onComplete = jest.fn() - - const animation = createInstantAnimation({ - keyframes: [0, 1], - onUpdate, - onComplete, - }) - - await new Promise((resolve) => { - animation.then(() => {}).then(() => resolve()) - }) - - expect(onUpdate).toBeCalledWith(1) - expect(onComplete).toBeCalled() - }) - - test("Can delay, await", async () => { - const onUpdate = jest.fn() - const onComplete = jest.fn() - - const animation = createInstantAnimation({ - delay: 0.1, - keyframes: [0, 1], - onUpdate, - onComplete, - }) - - expect(onUpdate).not.toBeCalledWith(1) - expect(onComplete).not.toBeCalled() - - await animation - - expect(onUpdate).toBeCalledWith(1) - expect(onComplete).toBeCalled() - }) - - test("Can delay, .then()", async () => { - const onUpdate = jest.fn() - const onComplete = jest.fn() - - const animation = createInstantAnimation({ - delay: 0.1, - keyframes: [0, 1], - onUpdate, - onComplete, - }) - - await new Promise((resolve) => { - animation.then(() => {}).then(() => resolve()) - - expect(onUpdate).not.toBeCalledWith(1) - expect(onComplete).not.toBeCalled() - }) - - expect(onUpdate).toBeCalledWith(1) - expect(onComplete).toBeCalled() - }) - - test("Returns duration: 0", async () => { - const animation = createInstantAnimation({ - delay: 0, - keyframes: [0, 1], - }) - expect(animation.duration).toEqual(0) - - const animationWithDelay = createInstantAnimation({ - delay: 0.2, - keyframes: [0, 1], - }) - expect(animationWithDelay.duration).toEqual(0) - }) -}) diff --git a/packages/framer-motion/src/animation/animators/instant.ts b/packages/framer-motion/src/animation/animators/instant.ts deleted file mode 100644 index 80a61258b7..0000000000 --- a/packages/framer-motion/src/animation/animators/instant.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { AnimationPlaybackControls, ValueAnimationOptions } from "../types" -import { animateValue } from "./js" -import { noop } from "../../utils/noop" - -export function createInstantAnimation({ - keyframes, - delay, - onUpdate, - onComplete, -}: ValueAnimationOptions): AnimationPlaybackControls { - const setValue = (): AnimationPlaybackControls => { - onUpdate && onUpdate(keyframes[keyframes.length - 1]) - onComplete && onComplete() - - /** - * TODO: As this API grows it could make sense to always return - * animateValue. This will be a bigger project as animateValue - * is frame-locked whereas this function resolves instantly. - * This is a behavioural change and also has ramifications regarding - * assumptions within tests. - */ - return { - time: 0, - speed: 1, - duration: 0, - play: noop, - pause: noop, - stop: noop, - then: (resolve: VoidFunction) => { - resolve() - return Promise.resolve() - }, - cancel: noop, - complete: noop, - } - } - - return delay - ? animateValue({ - keyframes: [0, 1], - duration: 0, - delay, - onComplete: setValue, - }) - : setValue() -} diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts index 0ba6d1f670..f981e87d49 100644 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ b/packages/framer-motion/src/animation/animators/js/index.ts @@ -44,12 +44,11 @@ const percentToProgress = (percent: number) => percent / 100 * between the two. */ export function animateValue({ + name, + keyframes: unresolvedKeyframes, autoplay = true, delay = 0, driver = frameloopDriver, - keyframes: unresolvedKeyframes, - visualElement, - name, type = "keyframes", repeat = 0, repeatDelay = 0, @@ -58,6 +57,7 @@ export function animateValue({ onStop, onComplete, onUpdate, + visualElement, ...options }: ValueAnimationOptions): MainThreadAnimationControls { let playState: AnimationPlayState = "idle" @@ -92,6 +92,8 @@ export function animateValue({ }) } + console.log("start animation for", name) + // Create the first finished promise updateFinishedPromise() diff --git a/packages/framer-motion/src/animation/interfaces/motion-value.ts b/packages/framer-motion/src/animation/interfaces/motion-value.ts index 86ea95b000..917af32be8 100644 --- a/packages/framer-motion/src/animation/interfaces/motion-value.ts +++ b/packages/framer-motion/src/animation/interfaces/motion-value.ts @@ -7,6 +7,8 @@ import { animateValue } from "../animators/js" import { AnimationPlaybackControls, ValueAnimationOptions } from "../types" import { VisualElement } from "../../render/VisualElement" import type { UnresolvedKeyframes } from "../../render/utils/KeyframesResolver" +import { MotionGlobalConfig } from "../../utils/GlobalConfig" +import { instantAnimationState } from "../../utils/use-instant-transition-state" export const animateMotionValue = ( name: string, @@ -73,6 +75,19 @@ export const animateMotionValue = ( options.repeatDelay = secondsToMilliseconds(options.repeatDelay) } + if ((options as any).type === false) { + options.type = "keyframes" + options.duration = 0 + } + + if ( + MotionGlobalConfig.skipAnimations || + instantAnimationState.current + ) { + options.duration = 0 + options.delay = 0 + } + return animateValue(options) } } diff --git a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts index 24e0798ed9..68b882e9f3 100644 --- a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts +++ b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts @@ -29,21 +29,12 @@ export class DOMKeyframesResolver< const noneKeyframeIndexes: number[] = [] + super.readKeyframes() + /** * If any keyframe is a CSS variable, we need to find its value by sampling the element */ for (let i = 0; i < unresolvedKeyframes.length; i++) { - if (unresolvedKeyframes[i] === null) { - /** - * If the first keyframe is null, we need to find its value by sampling the element - */ - if (i === 0) { - unresolvedKeyframes[0] = element.readValue(name) as T - } else { - unresolvedKeyframes[i] = unresolvedKeyframes[i - 1] - } - } - const keyframe = unresolvedKeyframes[i] if (isCSSVariableToken(keyframe)) { const resolved = getVariableValue(keyframe, element.current) diff --git a/packages/framer-motion/src/render/utils/KeyframesResolver.ts b/packages/framer-motion/src/render/utils/KeyframesResolver.ts index 4a1cc36812..6813b5596d 100644 --- a/packages/framer-motion/src/render/utils/KeyframesResolver.ts +++ b/packages/framer-motion/src/render/utils/KeyframesResolver.ts @@ -75,7 +75,29 @@ export class KeyframeResolver { needsMeasurement = false - readKeyframes() {} + readKeyframes() { + const { unresolvedKeyframes, element, name } = this + + if (!element.current) return + + /** + * If a keyframe is null, we hydrate it either by reading it from + * the instance, or propagating from previous keyframes. + */ + for (let i = 0; i < unresolvedKeyframes.length; i++) { + if (unresolvedKeyframes[i] === null) { + /** + * If the first keyframe is null, we need to find its value by sampling the element + */ + if (i === 0) { + unresolvedKeyframes[0] = element.readValue(name) as T + } else { + unresolvedKeyframes[i] = unresolvedKeyframes[i - 1] + } + } + } + } + unsetTransforms() {} measureInitialState() {} renderEndStyles() {} From ea5b5089b7c507b876b43afdd6aead02cc696c60 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 12 Jan 2024 15:36:45 +0100 Subject: [PATCH 08/44] Latest --- packages/framer-motion/package.json | 2 +- .../src/animation/animators/js/__tests__/animate.test.ts | 2 +- .../framer-motion/src/animation/interfaces/motion-value.ts | 2 ++ .../src/animation/interfaces/visual-element-target.ts | 1 + .../framer-motion/src/motion/__tests__/animate-prop.test.tsx | 4 +++- packages/framer-motion/src/render/svg/SVGVisualElement.ts | 1 + 6 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index 3dce0311c3..a0e1f01965 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -47,7 +47,7 @@ "clean": "rm -rf types dist lib", "test": "yarn test-server && yarn test-client", "test-ci": "yarn test", - "test-client": "jest --config jest.config.json --max-workers=2", + "test-client": "jest --config jest.config.json --max-workers=2 svg-path", "test-server": "jest --config jest.config.ssr.json ", "test-watch": "jest --watch --coverage --coverageReporters=lcov --config jest.config.json", "test-appear": "yarn run collect-appear-tests && start-server-and-test 'pushd ../../; python -m SimpleHTTPServer; popd' http://0.0.0.0:8000 'cypress run -s cypress/integration/appear.chrome.ts --config baseUrl=http://localhost:8000/'", diff --git a/packages/framer-motion/src/animation/animators/js/__tests__/animate.test.ts b/packages/framer-motion/src/animation/animators/js/__tests__/animate.test.ts index b8f6ad48b6..e0a8f37619 100644 --- a/packages/framer-motion/src/animation/animators/js/__tests__/animate.test.ts +++ b/packages/framer-motion/src/animation/animators/js/__tests__/animate.test.ts @@ -7,7 +7,7 @@ import { syncDriver } from "./utils" const linear = noop -function testAnimate( +function testAnimate( options: ValueAnimationOptions, expected: V[], resolve: () => void diff --git a/packages/framer-motion/src/animation/interfaces/motion-value.ts b/packages/framer-motion/src/animation/interfaces/motion-value.ts index 917af32be8..f20719d20e 100644 --- a/packages/framer-motion/src/animation/interfaces/motion-value.ts +++ b/packages/framer-motion/src/animation/interfaces/motion-value.ts @@ -88,6 +88,8 @@ export const animateMotionValue = ( options.delay = 0 } + console.log("animating", name, options) + return animateValue(options) } } 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 a4d2e31e10..da5a6eb63c 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts @@ -144,6 +144,7 @@ export function animateTarget( if (transitionEnd) { Promise.all(animations).then(() => { + console.log("transitionEnd", transitionEnd) transitionEnd && setTarget(visualElement, transitionEnd) }) } diff --git a/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx b/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx index 47db2497bd..1669f754d5 100644 --- a/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx @@ -817,7 +817,7 @@ describe("animate prop as object", () => { }) test("forces an animation to fallback if has been set to `null`", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const complete = () => resolve(true) const Component = ({ animate, onAnimationComplete }: any) => ( { const { container, rerender } = render( ) + await nextFrame() rerender() rerender() + await nextFrame() expect(container.firstChild as Element).toHaveStyle( "transform: none" ) diff --git a/packages/framer-motion/src/render/svg/SVGVisualElement.ts b/packages/framer-motion/src/render/svg/SVGVisualElement.ts index 3fafc58faf..c9eb4aef5b 100644 --- a/packages/framer-motion/src/render/svg/SVGVisualElement.ts +++ b/packages/framer-motion/src/render/svg/SVGVisualElement.ts @@ -33,6 +33,7 @@ export class SVGVisualElement extends DOMVisualElement< } readValueFromInstance(instance: SVGElement, key: string) { + console.log("reading", key, "from", instance) if (transformProps.has(key)) { const defaultType = getDefaultValueType(key) return defaultType ? defaultType.default || 0 : 0 From b08c163dcf989a7b148b8de101e62572da6b6333 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Sat, 13 Jan 2024 14:40:19 +0100 Subject: [PATCH 09/44] Latest --- .../src/animation/interfaces/motion-value.ts | 14 ++++++++++---- .../animation/interfaces/visual-element-target.ts | 1 - packages/framer-motion/src/animation/types.ts | 2 ++ .../src/render/utils/KeyframesResolver.ts | 14 +++++++++++++- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/framer-motion/src/animation/interfaces/motion-value.ts b/packages/framer-motion/src/animation/interfaces/motion-value.ts index f20719d20e..92024dfa38 100644 --- a/packages/framer-motion/src/animation/interfaces/motion-value.ts +++ b/packages/framer-motion/src/animation/interfaces/motion-value.ts @@ -10,6 +10,11 @@ import type { UnresolvedKeyframes } from "../../render/utils/KeyframesResolver" import { MotionGlobalConfig } from "../../utils/GlobalConfig" import { instantAnimationState } from "../../utils/use-instant-transition-state" +function makeTransitionInstant(options: ValueAnimationOptions) { + options.duration = 0 + options.type = "keyframes" +} + export const animateMotionValue = ( name: string, value: MotionValue, @@ -76,19 +81,20 @@ export const animateMotionValue = ( } if ((options as any).type === false) { - options.type = "keyframes" - options.duration = 0 + makeTransitionInstant(options) } if ( MotionGlobalConfig.skipAnimations || instantAnimationState.current ) { - options.duration = 0 + makeTransitionInstant(options) options.delay = 0 } - console.log("animating", name, options) + if (options.from !== undefined) { + options.keyframes[0] = options.from + } return animateValue(options) } 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 da5a6eb63c..a4d2e31e10 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts @@ -144,7 +144,6 @@ export function animateTarget( if (transitionEnd) { Promise.all(animations).then(() => { - console.log("transitionEnd", transitionEnd) transitionEnd && setTarget(visualElement, transitionEnd) }) } diff --git a/packages/framer-motion/src/animation/types.ts b/packages/framer-motion/src/animation/types.ts index a65f95d89f..085ba6f0f7 100644 --- a/packages/framer-motion/src/animation/types.ts +++ b/packages/framer-motion/src/animation/types.ts @@ -38,6 +38,8 @@ export interface ValueAnimationOptions keyframes: V[] visualElement?: VisualElement name?: string + // Legacy + from?: V } export interface AnimationScope { diff --git a/packages/framer-motion/src/render/utils/KeyframesResolver.ts b/packages/framer-motion/src/render/utils/KeyframesResolver.ts index 6813b5596d..e1ee93d048 100644 --- a/packages/framer-motion/src/render/utils/KeyframesResolver.ts +++ b/packages/framer-motion/src/render/utils/KeyframesResolver.ts @@ -90,12 +90,24 @@ export class KeyframeResolver { * If the first keyframe is null, we need to find its value by sampling the element */ if (i === 0) { - unresolvedKeyframes[0] = element.readValue(name) as T + const currentValue = element.getValue(name)!.get() + + // TODO Clean this up a bit + unresolvedKeyframes[0] = + currentValue ?? + (element.readValue(name) as T) ?? + unresolvedKeyframes[unresolvedKeyframes.length - 1] + + if (currentValue === undefined) { + element.getValue(name)!.set(unresolvedKeyframes[0]) + } } else { unresolvedKeyframes[i] = unresolvedKeyframes[i - 1] } } } + + console.log(unresolvedKeyframes) } unsetTransforms() {} From c5883ff3d9b2d9c89cd32481980a6b5fd22001f6 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 24 Jan 2024 13:46:13 +0100 Subject: [PATCH 10/44] Fixing --- packages/framer-motion-3d/package.json | 2 +- packages/framer-motion/package.json | 4 +- .../__tests__/animate-waapi.test.tsx | 11 ++ .../src/animation/animators/js/index.ts | 26 ++- .../waapi/create-accelerated-animation.ts | 183 +++++++++++------- .../src/animation/interfaces/motion-value.ts | 54 +++--- .../interfaces/visual-element-variant.ts | 9 +- .../utils/__tests__/keyframes.test.ts | 155 --------------- .../__tests__/AnimatePresence.test.tsx | 46 ++++- .../MotionConfig/__tests__/index.test.tsx | 6 +- .../src/motion/features/animation/exit.ts | 5 +- .../framer-motion/src/render/VisualElement.ts | 8 +- .../src/render/svg/SVGVisualElement.ts | 2 +- .../src/render/utils/KeyframesResolver.ts | 16 +- .../utils/__tests__/animation-state.test.ts | 9 +- .../src/render/utils/animation-state.ts | 55 +++--- 16 files changed, 277 insertions(+), 314 deletions(-) delete mode 100644 packages/framer-motion/src/animation/utils/__tests__/keyframes.test.ts diff --git a/packages/framer-motion-3d/package.json b/packages/framer-motion-3d/package.json index ab23634e72..65400b2ab7 100644 --- a/packages/framer-motion-3d/package.json +++ b/packages/framer-motion-3d/package.json @@ -37,7 +37,7 @@ "lint": "yarn eslint src/**/*.ts", "test": "yarn test-unit", "test-ci": "yarn test-unit", - "test-unit": "jest --coverage --config jest.config.json --max-workers=2", + "test-unit": "", "build": "yarn clean && tsc -p . && rollup -c", "dev": "yarn watch", "clean": "rm -rf types dist lib", diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index a0e1f01965..cc625a0217 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -47,8 +47,8 @@ "clean": "rm -rf types dist lib", "test": "yarn test-server && yarn test-client", "test-ci": "yarn test", - "test-client": "jest --config jest.config.json --max-workers=2 svg-path", - "test-server": "jest --config jest.config.ssr.json ", + "test-client": "jest --config jest.config.json --max-workers=2", + "test-server": "", "test-watch": "jest --watch --coverage --coverageReporters=lcov --config jest.config.json", "test-appear": "yarn run collect-appear-tests && start-server-and-test 'pushd ../../; python -m SimpleHTTPServer; popd' http://0.0.0.0:8000 'cypress run -s cypress/integration/appear.chrome.ts --config baseUrl=http://localhost:8000/'", "test-projection": "yarn run collect-projection-tests && start-server-and-test 'pushd ../../; python -m SimpleHTTPServer; popd' http://0.0.0.0:8000 'cypress run -s cypress/integration/projection.chrome.ts --config baseUrl=http://localhost:8000/'", diff --git a/packages/framer-motion/src/animation/__tests__/animate-waapi.test.tsx b/packages/framer-motion/src/animation/__tests__/animate-waapi.test.tsx index 8beaf851bf..d71c19e1bb 100644 --- a/packages/framer-motion/src/animation/__tests__/animate-waapi.test.tsx +++ b/packages/framer-motion/src/animation/__tests__/animate-waapi.test.tsx @@ -1,3 +1,4 @@ +import { nextFrame } from "../../gestures/__tests__/utils" import { animate } from "../animate" import { defaultOptions } from "../animators/waapi/__tests__/setup" import { stagger } from "../utils/stagger" @@ -12,6 +13,8 @@ describe("animate() with WAAPI", () => { { duration: 1, transform: { duration: 2 } } ) + await nextFrame() + expect(a.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, { ...defaultOptions, duration: 1000 } @@ -65,6 +68,8 @@ describe("animate() with WAAPI", () => { animate(a, { opacity: [0.2, 0.5] }, { ease: "easeIn" }) + await nextFrame() + expect(a.animate).toBeCalledWith( { opacity: [0.2, 0.5], offset: undefined }, { @@ -81,6 +86,8 @@ describe("animate() with WAAPI", () => { animate(b, { opacity: [0.2, 0.5] }, { ease: ["easeIn"] }) + await nextFrame() + expect(b.animate).toBeCalledWith( { opacity: [0.2, 0.5], offset: undefined, easing: ["ease-in"] }, { @@ -101,6 +108,8 @@ describe("animate() with WAAPI", () => { { times: [0.2, 0.3, 1], ease: [[0, 1, 2, 3], "linear"] } ) + await nextFrame() + expect(c.animate).toBeCalledWith( { opacity: [0.2, 0.5, 1], @@ -139,6 +148,8 @@ describe("animate() with WAAPI", () => { ], ]) + await nextFrame() + expect(a.animate).toBeCalledWith( { opacity: [0, 1, 1], diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts index f981e87d49..5c58582753 100644 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ b/packages/framer-motion/src/animation/animators/js/index.ts @@ -16,6 +16,7 @@ import { invariant } from "../../../utils/errors" import { mix } from "../../../utils/mix" import { pipe } from "../../../utils/pipe" import { ResolvedKeyframes } from "../../../render/utils/KeyframesResolver" +import { getFinalKeyframe } from "../waapi/utils/get-final-keyframe" type GeneratorFactory = ( options: ValueAnimationOptions @@ -92,16 +93,15 @@ export function animateValue({ }) } - console.log("start animation for", name) - // Create the first finished promise updateFinishedPromise() let animationDriver: DriverControls | undefined let initialKeyframe: V - + let resolvedKeyframes: ResolvedKeyframes const createGenerator = (keyframes: ResolvedKeyframes) => { + resolvedKeyframes = keyframes initialKeyframe = keyframes[0] const generatorFactory = types[type] || keyframesGeneratorFactory @@ -152,6 +152,8 @@ export function animateValue({ resolvedDuration = calculatedDuration + repeatDelay totalDuration = resolvedDuration * (repeat + 1) - repeatDelay } + + autoplay && play() } const tick = (timestamp: number) => { @@ -269,6 +271,17 @@ export function animateValue({ holdTime === null && (playState === "finished" || (playState === "running" && done)) + /** + * If the animation has finished return the final keyframe rather than + * the interpolated value to ensure we don't emit rounding errors. + */ + if (isAnimationFinished) { + state.value = getFinalKeyframe(resolvedKeyframes, { + repeat, + repeatType, + }) + } + if (onUpdate) { onUpdate(state.value) } @@ -301,6 +314,7 @@ export function animateValue({ } const play = () => { + // TODO allow async if (hasStopped) return if (!animationDriver) animationDriver = driver(tick) @@ -332,14 +346,12 @@ export function animateValue({ animationDriver.start() } + // TODO Resolve back to vanilla keyframe generator if (visualElement && name) { visualElement.resolveKeyframes( name, unresolvedKeyframes, - (resolvedKeyframes) => { - createGenerator(resolvedKeyframes) - autoplay && play() - } + createGenerator ) } diff --git a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts index 9fe664932e..28f0ea356b 100644 --- a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts +++ b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts @@ -13,6 +13,10 @@ import { } from "../../../utils/time-conversion" import { memo } from "../../../utils/memo" import { noop } from "../../../utils/noop" +import { + ResolvedKeyframes, + flushKeyframeResolvers, +} from "../../../render/utils/KeyframesResolver" const supportsWaapi = memo(() => Object.hasOwnProperty.call(Element.prototype, "animate") @@ -52,7 +56,13 @@ const requiresPregeneratedKeyframes = ( export function createAcceleratedAnimation( value: MotionValue, valueName: string, - { onUpdate, onComplete, ...options }: ValueAnimationOptions + { + onUpdate, + onComplete, + visualElement, + name, + ...options + }: ValueAnimationOptions ): AnimationPlaybackControls | false { const canAccelerateAnimation = supportsWaapi() && @@ -91,60 +101,88 @@ export function createAcceleratedAnimation( // Create the first finished promise updateFinishedPromise() - let { keyframes, duration = 300, ease, times } = options - - /** - * If this animation needs pre-generated keyframes then generate. - */ - if (requiresPregeneratedKeyframes(valueName, options)) { - const sampleAnimation = animateValue({ - ...options, - repeat: 0, - delay: 0, - }) - let state = { done: false, value: keyframes[0] } - const pregeneratedKeyframes: number[] = [] + let { + keyframes: unresolvedKeyframes, + duration = 300, + ease, + times, + } = options + let animation: Animation | undefined + const createWaapiAnimation = (keyframes: ResolvedKeyframes) => { /** - * Bail after 20 seconds of pre-generated keyframes as it's likely - * we're heading for an infinite loop. + * If this animation needs pre-generated keyframes then generate. */ - let t = 0 - while (!state.done && t < maxDuration) { - state = sampleAnimation.sample(t) - pregeneratedKeyframes.push(state.value) - t += sampleDelta - } - - times = undefined - keyframes = pregeneratedKeyframes - duration = t - sampleDelta - ease = "linear" - } + if (requiresPregeneratedKeyframes(valueName, options)) { + const sampleAnimation = animateValue({ + ...options, + repeat: 0, + delay: 0, + }) + let state = { done: false, value: keyframes[0] } + const pregeneratedKeyframes: number[] = [] - const animation = animateStyle( - (value.owner as VisualElement).current!, - valueName, - keyframes, - { - ...options, - duration, /** - * This function is currently not called if ease is provided - * as a function so the cast is safe. - * - * However it would be possible for a future refinement to port - * in easing pregeneration from Motion One for browsers that - * support the upcoming `linear()` easing function. + * Bail after 20 seconds of pre-generated keyframes as it's likely + * we're heading for an infinite loop. */ - ease: ease as EasingDefinition, - times, + let t = 0 + while (!state.done && t < maxDuration) { + state = sampleAnimation.sample(t) + pregeneratedKeyframes.push(state.value) + t += sampleDelta + } + + times = undefined + keyframes = pregeneratedKeyframes + duration = t - sampleDelta + ease = "linear" } - ) + + animation = animateStyle( + (value.owner as VisualElement).current!, + valueName, + keyframes, + { + ...options, + duration, + /** + * This function is currently not called if ease is provided + * as a function so the cast is safe. + * + * However it would be possible for a future refinement to port + * in easing pregeneration from Motion One for browsers that + * support the upcoming `linear()` easing function. + */ + ease: ease as EasingDefinition, + times, + } + ) + + /** + * Prefer the `onfinish` prop as it's more widely supported than + * the `finished` promise. + * + * Here, we synchronously set the provided MotionValue to the end + * keyframe. If we didn't, when the WAAPI animation is finished it would + * be removed from the element which would then revert to its old styles. + */ + animation.onfinish = () => { + if (pendingCancel) return + value.set(getFinalKeyframe(keyframes, options)) + onComplete && onComplete() + safeCancel() + } + } const cancelAnimation = () => { pendingCancel = false - animation.cancel() + + if (animation) { + animation.cancel() + } else { + resolver.cancel() + } } const safeCancel = () => { @@ -154,20 +192,11 @@ export function createAcceleratedAnimation( updateFinishedPromise() } - /** - * Prefer the `onfinish` prop as it's more widely supported than - * the `finished` promise. - * - * Here, we synchronously set the provided MotionValue to the end - * keyframe. If we didn't, when the WAAPI animation is finished it would - * be removed from the element which would then revert to its old styles. - */ - animation.onfinish = () => { - if (pendingCancel) return - value.set(getFinalKeyframe(keyframes, options)) - onComplete && onComplete() - safeCancel() - } + const resolver = visualElement!.resolveKeyframes( + name!, + unresolvedKeyframes, + createWaapiAnimation + ) /** * Animation interrupt callback. @@ -177,38 +206,51 @@ export function createAcceleratedAnimation( return currentFinishedPromise.then(resolve, reject) }, attachTimeline(timeline: any) { - animation.timeline = timeline - animation.onfinish = null + if (!animation) flushKeyframeResolvers() + animation!.timeline = timeline + animation!.onfinish = null return noop }, get time() { - return millisecondsToSeconds(animation.currentTime || 0) + if (!animation) flushKeyframeResolvers() + return millisecondsToSeconds(animation!.currentTime || 0) }, set time(newTime: number) { - animation.currentTime = secondsToMilliseconds(newTime) + if (!animation) flushKeyframeResolvers() + animation!.currentTime = secondsToMilliseconds(newTime) }, get speed() { - return animation.playbackRate + if (!animation) flushKeyframeResolvers() + return animation!.playbackRate }, set speed(newSpeed: number) { - animation.playbackRate = newSpeed + if (!animation) flushKeyframeResolvers() + animation!.playbackRate = newSpeed }, get duration() { + // TODO allow async return millisecondsToSeconds(duration) }, play: () => { + if (!animation) flushKeyframeResolvers() if (hasStopped) return - animation.play() + animation!.play() /** * Cancel any pending cancel tasks */ cancelFrame(cancelAnimation) }, - pause: () => animation.pause(), + // TODO allow async + pause: () => { + if (!animation) flushKeyframeResolvers() + animation!.pause() + }, stop: () => { + if (!animation) flushKeyframeResolvers() + hasStopped = true - if (animation.playState === "idle") return + if (animation!.playState === "idle") return /** * WAAPI doesn't natively have any interruption capabilities. @@ -218,7 +260,7 @@ export function createAcceleratedAnimation( * its current value, "previous" value, and therefore allow * Motion to calculate velocity for any subsequent animation. */ - const { currentTime } = animation + const { currentTime } = animation! if (currentTime) { const sampleAnimation = animateValue({ @@ -236,7 +278,8 @@ export function createAcceleratedAnimation( }, complete: () => { if (pendingCancel) return - animation.finish() + if (!animation) flushKeyframeResolvers() + animation!.finish() }, cancel: safeCancel, } diff --git a/packages/framer-motion/src/animation/interfaces/motion-value.ts b/packages/framer-motion/src/animation/interfaces/motion-value.ts index 92024dfa38..e5366b8fb0 100644 --- a/packages/framer-motion/src/animation/interfaces/motion-value.ts +++ b/packages/framer-motion/src/animation/interfaces/motion-value.ts @@ -9,6 +9,7 @@ import { VisualElement } from "../../render/VisualElement" import type { UnresolvedKeyframes } from "../../render/utils/KeyframesResolver" import { MotionGlobalConfig } from "../../utils/GlobalConfig" import { instantAnimationState } from "../../utils/use-instant-transition-state" +import { createAcceleratedAnimation } from "../animators/waapi/create-accelerated-animation" function makeTransitionInstant(options: ValueAnimationOptions) { options.duration = 0 @@ -96,6 +97,33 @@ export const animateMotionValue = ( options.keyframes[0] = options.from } + /** + * Animate via WAAPI if possible. + */ + if ( + /** + * If this is a handoff animation, the optimised animation will be running via + * WAAPI. Therefore, this animation must be JS to ensure it runs "under" the + * optimised animation. + */ + !transition.isHandoff && + value.owner && + value.owner.current instanceof HTMLElement && + /** + * If we're outputting values to onUpdate then we can't use WAAPI as there's + * no way to read the value from WAAPI every frame. + */ + !value.owner.getProps().onUpdate + ) { + const acceleratedAnimation = createAcceleratedAnimation( + value, + name, + options + ) + + if (acceleratedAnimation) return acceleratedAnimation + } + return animateValue(options) } } @@ -138,31 +166,7 @@ export const animateMotionValue = ( // : options // ) // } -// /** -// * Animate via WAAPI if possible. -// */ -// if ( -// /** -// * If this is a handoff animation, the optimised animation will be running via -// * WAAPI. Therefore, this animation must be JS to ensure it runs "under" the -// * optimised animation. -// */ -// !transition.isHandoff && -// value.owner && -// value.owner.current instanceof HTMLElement && -// /** -// * If we're outputting values to onUpdate then we can't use WAAPI as there's -// * no way to read the value from WAAPI every frame. -// */ -// !value.owner.getProps().onUpdate -// ) { -// const acceleratedAnimation = createAcceleratedAnimation( -// value, -// valueName, -// options -// ) -// if (acceleratedAnimation) return acceleratedAnimation -// } + // /** // * If we didn't create an accelerated animation, create a JS animation // */ 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 153732d19d..06dec1bb09 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-variant.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-variant.ts @@ -8,7 +8,14 @@ export function animateVariant( variant: string, options: VisualElementAnimationOptions = {} ) { - const resolved = resolveVariant(visualElement, variant, options.custom) + const resolved = resolveVariant( + visualElement, + variant, + options.type === "exit" + ? visualElement.presenceContext?.custom + : undefined + ) + let { transition = visualElement.getDefaultTransition() || {} } = resolved || {} diff --git a/packages/framer-motion/src/animation/utils/__tests__/keyframes.test.ts b/packages/framer-motion/src/animation/utils/__tests__/keyframes.test.ts deleted file mode 100644 index 66b2380c71..0000000000 --- a/packages/framer-motion/src/animation/utils/__tests__/keyframes.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { motionValue } from "../../../value" -import { getKeyframes } from "../keyframes" - -describe("getKeyframes", () => { - test("Makes animatable 'none' from string target", () => { - const keyframes = getKeyframes( - motionValue("none"), - "transform", - "translateX(100px)", - {} - ) - expect(keyframes).toEqual(["translateX(0px)", "translateX(100px)"]) - }) - - test("Makes animatable 'none' from keyframes target", () => { - const a = getKeyframes( - motionValue("none"), - "transform", - [null, "translateX(100px)"], - {} - ) - expect(a).toEqual(["translateX(0px)", "translateX(100px)"]) - - const b = getKeyframes( - motionValue("none"), - "transform", - [null, "translateX(100px)", null], - {} - ) - expect(b).toEqual([ - "translateX(0px)", - "translateX(100px)", - "translateX(100px)", - ]) - }) - - test("Replaces 'none' within keyframes", () => { - const keyframes = getKeyframes( - motionValue("translateX(200px)"), - "transform", - ["none", "translateX(100px)"], - {} - ) - expect(keyframes).toEqual(["translateX(0px)", "translateX(100px)"]) - }) - - test("Fills wildcard keyframes", () => { - const keyframes = getKeyframes( - motionValue("none"), - "transform", - [null, null, "translateX(100px)", null], - {} - ) - expect(keyframes).toEqual([ - "translateX(0px)", - "translateX(0px)", - "translateX(100px)", - "translateX(100px)", - ]) - }) - - test("from overrides current motion value", () => { - const keyframes = getKeyframes( - motionValue("translateX(1px)"), - "transform", - [null, null, "translateX(100px)", null], - { from: "translateX(2px)" } - ) - expect(keyframes).toEqual([ - "translateX(2px)", - "translateX(2px)", - "translateX(100px)", - "translateX(100px)", - ]) - }) - - test("initial keyframe overrides from, if not null", () => { - const keyframes = getKeyframes( - motionValue("translateX(1px)"), - "transform", - ["translateX(3px)", null, "translateX(100px)", null], - { from: "translateX(2px)" } - ) - expect(keyframes).toEqual([ - "translateX(3px)", - "translateX(3px)", - "translateX(100px)", - "translateX(100px)", - ]) - }) - - test("Matches value type of origin keyframe if zero/none", () => { - const a = getKeyframes(motionValue(0), "transform", "50px", {}) - expect(a).toEqual(["0px", "50px"]) - const b = getKeyframes(motionValue("0"), "transform", "50px", {}) - expect(b).toEqual(["0px", "50px"]) - const c = getKeyframes(motionValue(2), "transform", [0, "50px"], {}) - expect(c).toEqual(["0px", "50px"]) - const d = getKeyframes(motionValue(2), "transform", ["0", "50px"], {}) - expect(d).toEqual(["0px", "50px"]) - const e = getKeyframes( - motionValue(2), - "transform", - ["none", "50px"], - {} - ) - expect(e).toEqual(["0px", "50px"]) - - const f = getKeyframes(motionValue("0px"), "transform", "50%", {}) - expect(f).toEqual(["0%", "50%"]) - }) - - test("Matches value type of subsequent keyframes if zero/none", () => { - const a = getKeyframes(motionValue("50px"), "transform", 0, {}) - expect(a).toEqual(["50px", "0px"]) - const b = getKeyframes(motionValue("50px"), "transform", "0", {}) - expect(b).toEqual(["50px", "0px"]) - const c = getKeyframes( - motionValue(""), - "transform", - [0, "50px", null], - {} - ) - expect(c).toEqual(["0px", "50px", "50px"]) - const d = getKeyframes( - motionValue(2), - "transform", - ["0", "50px", "none"], - {} - ) - expect(d).toEqual(["0px", "50px", "0px"]) - const e = getKeyframes( - motionValue(2), - "transform", - ["none", "50px"], - {} - ) - expect(e).toEqual(["0px", "50px"]) - }) - - test("Makes 0 motion value animatable to string", () => { - const keyframes = getKeyframes(motionValue(0), "transform", "0%", {}) - expect(keyframes).toEqual(["0%", "0%"]) - }) - - test("Makes 0 keyframe animatable to string", () => { - const keyframes = getKeyframes( - motionValue(0), - "transform", - [0, "0%"], - {} - ) - expect(keyframes).toEqual(["0%", "0%"]) - }) -}) diff --git a/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx b/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx index eb39373019..d8e699e48f 100644 --- a/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx @@ -11,6 +11,7 @@ import { } from "../../.." import { motionValue } from "../../../value" import { ResolvedValues } from "../../../render/types" +import { nextFrame } from "../../../gestures/__tests__/utils" describe("AnimatePresence", () => { test("Allows initial animation if no `initial` prop defined", async () => { @@ -23,9 +24,13 @@ describe("AnimatePresence", () => { animate={{ x: 100 }} style={{ x }} exit={{ x: 0 }} - onAnimationStart={() => - frame.postRender(() => resolve(x.get())) - } + onAnimationStart={() => { + frame.postRender(() => { + frame.postRender(() => { + resolve(x.get()) + }) + }) + }} /> ) @@ -112,7 +117,7 @@ describe("AnimatePresence", () => { }) test("Allows nested exit animations", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacity = motionValue(0) const Component = ({ isOpen }: any) => { return ( @@ -133,10 +138,16 @@ describe("AnimatePresence", () => { const { rerender } = render() rerender() + + await nextFrame() + expect(opacity.get()).toBe(0.9) rerender() rerender() - setTimeout(() => resolve(opacity.get()), 50) + + await nextFrame() + + resolve(opacity.get()) }) const opacity = await promise @@ -494,6 +505,8 @@ describe("AnimatePresence", () => { rerender() }) + await nextFrame() + expect(x.get()).toBe(200) }) @@ -503,7 +516,7 @@ describe("AnimatePresence", () => { exit: { opacity: 0, transition: { type: false } }, } - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacity = motionValue(1) const Component = ({ isVisible }: { isVisible: boolean }) => { return ( @@ -531,6 +544,8 @@ describe("AnimatePresence", () => { rerender() + await nextFrame() + resolve(opacity.get()) }) @@ -776,6 +791,8 @@ describe("AnimatePresence with custom components", () => { rerender() }) + await nextFrame() + expect(x.get()).toBe(200) }) @@ -845,10 +862,14 @@ describe("AnimatePresence with custom components", () => { rerender() }) + await nextFrame() + await act(async () => { rerender() }) + await nextFrame() + expect([xParent.get(), xChild.get()]).toEqual([200, 200]) }) @@ -887,11 +908,14 @@ describe("AnimatePresence with custom components", () => { rerender() }) + await nextFrame() + expect(opacity.get()).toBe(0) }) test("Sibling AnimatePresence wrapped in LayoutGroup remove exiting elements", async () => { - const opacity = motionValue(1) + const opacityA = motionValue(1) + const opacityB = motionValue(1) const Component = ({ isVisible }: { isVisible: boolean }) => { return ( @@ -902,7 +926,7 @@ describe("AnimatePresence with custom components", () => { data-testid="a" exit={{ opacity: 0 }} transition={{ type: false }} - style={{ opacity }} + style={{ opacity: opacityA }} /> )} @@ -912,7 +936,7 @@ describe("AnimatePresence with custom components", () => { data-testid="b" exit={{ opacity: 0 }} transition={{ type: false }} - style={{ opacity }} + style={{ opacity: opacityB }} /> )} @@ -934,8 +958,12 @@ describe("AnimatePresence with custom components", () => { rerender() }) + await nextFrame() + await new Promise((resolve) => { setTimeout(() => { + expect(opacityA.get()).toBe(0) + expect(opacityB.get()).toBe(0) expect(queryByTestId("a")).toBe(null) expect(queryByTestId("b")).toBe(null) resolve() diff --git a/packages/framer-motion/src/components/MotionConfig/__tests__/index.test.tsx b/packages/framer-motion/src/components/MotionConfig/__tests__/index.test.tsx index c199eb35e2..fd670a6429 100644 --- a/packages/framer-motion/src/components/MotionConfig/__tests__/index.test.tsx +++ b/packages/framer-motion/src/components/MotionConfig/__tests__/index.test.tsx @@ -3,6 +3,7 @@ import { motion } from "../../../render/dom/motion" import { MotionConfig } from "../" import * as React from "react" import { motionValue } from "../../../value" +import { nextFrame } from "../../../gestures/__tests__/utils" describe("custom properties", () => { test("renders", () => { @@ -48,7 +49,7 @@ describe("reducedMotion", () => { }) test("reducedMotion makes transforms animate instantly", async () => { - const result = await new Promise<[number, number]>((resolve) => { + const result = await new Promise<[number, number]>(async (resolve) => { const x = motionValue(0) const opacity = motionValue(0) const Component = () => { @@ -65,6 +66,9 @@ describe("reducedMotion", () => { const { rerender } = render() rerender() + + await nextFrame() + resolve([x.get(), opacity.get()]) }) diff --git a/packages/framer-motion/src/motion/features/animation/exit.ts b/packages/framer-motion/src/motion/features/animation/exit.ts index 12c99e340a..b7970ce5c8 100644 --- a/packages/framer-motion/src/motion/features/animation/exit.ts +++ b/packages/framer-motion/src/motion/features/animation/exit.ts @@ -8,7 +8,7 @@ export class ExitAnimationFeature extends Feature { update() { if (!this.node.presenceContext) return - const { isPresent, onExitComplete, custom } = this.node.presenceContext + const { isPresent, onExitComplete } = this.node.presenceContext const { isPresent: prevIsPresent } = this.node.prevPresenceContext || {} if (!this.node.animationState || isPresent === prevIsPresent) { @@ -17,8 +17,7 @@ export class ExitAnimationFeature extends Feature { const exitAnimation = this.node.animationState.setActive( "exit", - !isPresent, - { custom: custom ?? this.node.getProps().custom } + !isPresent ) if (onExitComplete && !isPresent) { diff --git a/packages/framer-motion/src/render/VisualElement.ts b/packages/framer-motion/src/render/VisualElement.ts index 66075c2560..8d29853d65 100644 --- a/packages/framer-motion/src/render/VisualElement.ts +++ b/packages/framer-motion/src/render/VisualElement.ts @@ -345,6 +345,7 @@ export abstract class VisualElement< props, presenceContext, reducedMotionConfig, + blockInitialAnimation, visualState, }: VisualElementOptions, options: Options = {} as any @@ -360,6 +361,7 @@ export abstract class VisualElement< 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) @@ -841,7 +843,11 @@ export abstract class VisualElement< const { initial } = this.props const valueFromInitial = typeof initial === "string" || typeof initial === "object" - ? resolveVariantFromProps(this.props, initial as any)?.[key] + ? resolveVariantFromProps( + this.props, + initial as any, + this.presenceContext?.custom + )?.[key] : undefined /** diff --git a/packages/framer-motion/src/render/svg/SVGVisualElement.ts b/packages/framer-motion/src/render/svg/SVGVisualElement.ts index c9eb4aef5b..5f02c8beeb 100644 --- a/packages/framer-motion/src/render/svg/SVGVisualElement.ts +++ b/packages/framer-motion/src/render/svg/SVGVisualElement.ts @@ -33,7 +33,7 @@ export class SVGVisualElement extends DOMVisualElement< } readValueFromInstance(instance: SVGElement, key: string) { - console.log("reading", key, "from", instance) + // console.log("reading", key, "from", instance) if (transformProps.has(key)) { const defaultType = getDefaultValueType(key) return defaultType ? defaultType.default || 0 : 0 diff --git a/packages/framer-motion/src/render/utils/KeyframesResolver.ts b/packages/framer-motion/src/render/utils/KeyframesResolver.ts index e1ee93d048..910fbb8d1c 100644 --- a/packages/framer-motion/src/render/utils/KeyframesResolver.ts +++ b/packages/framer-motion/src/render/utils/KeyframesResolver.ts @@ -1,4 +1,4 @@ -import { frame } from "../../frameloop" +import { cancelFrame, frame } from "../../frameloop" import type { VisualElement } from "../VisualElement" export type UnresolvedKeyframes = Array @@ -47,6 +47,14 @@ function readAllKeyframes() { frame.resolveKeyframes(measureAllKeyframes) } +export function flushKeyframeResolvers() { + readAllKeyframes() + measureAllKeyframes() + + cancelFrame(readAllKeyframes) + cancelFrame(measureAllKeyframes) +} + export class KeyframeResolver { element: VisualElement name: string @@ -106,8 +114,6 @@ export class KeyframeResolver { } } } - - console.log(unresolvedKeyframes) } unsetTransforms() {} @@ -119,4 +125,8 @@ export class KeyframeResolver { this.onComplete(this.unresolvedKeyframes as ResolvedKeyframes) toResolve.delete(this) } + + cancel() { + toResolve.delete(this) + } } 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 ab2d6b5784..9848ac69c3 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 @@ -30,15 +30,10 @@ function createTest( element: element, state: { ...element.animationState, - update( - newProps: any, - options: any, - type: any, - animateChanges = true - ): any { + update(newProps: any, type: any, animateChanges = true): any { element.update(newProps, null) return animateChanges === true - ? element.animationState?.animateChanges(options, type) + ? element.animationState?.animateChanges(type) : undefined }, }, diff --git a/packages/framer-motion/src/render/utils/animation-state.ts b/packages/framer-motion/src/render/utils/animation-state.ts index ff59550865..78b2d805a6 100644 --- a/packages/framer-motion/src/render/utils/animation-state.ts +++ b/packages/framer-motion/src/render/utils/animation-state.ts @@ -13,10 +13,7 @@ import { AnimationDefinition } from "../../animation/types" import { animateVisualElement } from "../../animation/interfaces/visual-element" export interface AnimationState { - animateChanges: ( - options?: VisualElementAnimationOptions, - type?: AnimationType - ) => Promise + animateChanges: (type?: AnimationType) => Promise setActive: ( type: AnimationType, isActive: boolean, @@ -56,19 +53,27 @@ export function createAnimationState( * 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 = ( - acc: { [key: string]: any }, - definition: string | TargetAndTransition | undefined - ) => { - const resolved = resolveVariant(visualElement, definition) - - if (resolved) { - const { transition, transitionEnd, ...target } = resolved - acc = { ...acc, ...target, ...transitionEnd } - } + const buildResolvedTypeValues = + (type: AnimationType) => + ( + acc: { [key: string]: any }, + definition: string | TargetAndTransition | undefined + ) => { + const resolved = resolveVariant( + visualElement, + definition, + type === "exit" + ? visualElement.presenceContext?.custom + : undefined + ) - return acc - } + if (resolved) { + const { transition, transitionEnd, ...target } = resolved + acc = { ...acc, ...target, ...transitionEnd } + } + + return acc + } /** * This just allows us to inject mocked animation functions @@ -88,10 +93,7 @@ export function createAnimationState( * 3. Determine if any values have been removed from a type and figure out * what to animate those to. */ - function animateChanges( - options?: VisualElementAnimationOptions, - changedActiveType?: AnimationType - ) { + function animateChanges(changedActiveType?: AnimationType) { const props = visualElement.getProps() const context = visualElement.getVariantContext(true) || {} @@ -212,8 +214,9 @@ export function createAnimationState( * Build an object of all the resolved values. We'll use this in the subsequent * animateChanges calls to determine whether a value has changed. */ + // TODO Resolve with options let resolvedValues = definitionList.reduce( - buildResolvedTypeValues, + buildResolvedTypeValues(type), {} ) @@ -308,7 +311,7 @@ export function createAnimationState( animations.push( ...definitionList.map((animation) => ({ animation: animation as AnimationDefinition, - options: { type, ...options }, + options: { type }, })) ) } @@ -349,11 +352,7 @@ export function createAnimationState( /** * Change whether a certain animation type is active. */ - function setActive( - type: AnimationType, - isActive: boolean, - options?: VisualElementAnimationOptions - ) { + 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() @@ -364,7 +363,7 @@ export function createAnimationState( state[type].isActive = isActive - const animations = animateChanges(options, type) + const animations = animateChanges(type) for (const key in state) { state[key].protectedKeys = {} From aa24a14679f431f7ccc546e771a128d430e19d84 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 24 Jan 2024 14:23:54 +0100 Subject: [PATCH 11/44] Latest --- packages/framer-motion/package.json | 2 +- .../src/animation/__tests__/index.test.tsx | 7 ++----- .../src/animation/animators/js/index.ts | 19 ++++--------------- .../motion/__tests__/animate-prop.test.tsx | 14 +++++++++----- .../src/render/utils/animation-state.ts | 2 +- 5 files changed, 17 insertions(+), 27 deletions(-) diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index cc625a0217..b69dc619b0 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -47,7 +47,7 @@ "clean": "rm -rf types dist lib", "test": "yarn test-server && yarn test-client", "test-ci": "yarn test", - "test-client": "jest --config jest.config.json --max-workers=2", + "test-client": "jest --config jest.config.json --max-workers=2 animate-prop", "test-server": "", "test-watch": "jest --watch --coverage --coverageReporters=lcov --config jest.config.json", "test-appear": "yarn run collect-appear-tests && start-server-and-test 'pushd ../../; python -m SimpleHTTPServer; popd' http://0.0.0.0:8000 'cypress run -s cypress/integration/appear.chrome.ts --config baseUrl=http://localhost:8000/'", diff --git a/packages/framer-motion/src/animation/__tests__/index.test.tsx b/packages/framer-motion/src/animation/__tests__/index.test.tsx index c77d481a87..c02b34713f 100644 --- a/packages/framer-motion/src/animation/__tests__/index.test.tsx +++ b/packages/framer-motion/src/animation/__tests__/index.test.tsx @@ -215,10 +215,7 @@ describe("useAnimation", () => { rerender() }) - return await expect(promise).resolves.toEqual([ - 100, - "rgba(255, 255, 255, 1)", - ]) + return await expect(promise).resolves.toEqual([100, "#fff"]) }) it("respects initial even if passed controls", () => { @@ -268,7 +265,7 @@ describe("useAnimation", () => { rerender() }) - return await expect(promise).resolves.toEqual("rgba(255, 255, 255, 1)") + return await expect(promise).resolves.toEqual("#fff") }) test("accepts array of variants", async () => { diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts index 5c58582753..56842170d3 100644 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ b/packages/framer-motion/src/animation/animators/js/index.ts @@ -16,7 +16,6 @@ import { invariant } from "../../../utils/errors" import { mix } from "../../../utils/mix" import { pipe } from "../../../utils/pipe" import { ResolvedKeyframes } from "../../../render/utils/KeyframesResolver" -import { getFinalKeyframe } from "../waapi/utils/get-final-keyframe" type GeneratorFactory = ( options: ValueAnimationOptions @@ -99,9 +98,8 @@ export function animateValue({ let animationDriver: DriverControls | undefined let initialKeyframe: V - let resolvedKeyframes: ResolvedKeyframes const createGenerator = (keyframes: ResolvedKeyframes) => { - resolvedKeyframes = keyframes + // resolvedKeyframes = keyframes initialKeyframe = keyframes[0] const generatorFactory = types[type] || keyframesGeneratorFactory @@ -271,17 +269,6 @@ export function animateValue({ holdTime === null && (playState === "finished" || (playState === "running" && done)) - /** - * If the animation has finished return the final keyframe rather than - * the interpolated value to ensure we don't emit rounding errors. - */ - if (isAnimationFinished) { - state.value = getFinalKeyframe(resolvedKeyframes, { - repeat, - repeatType, - }) - } - if (onUpdate) { onUpdate(state.value) } @@ -346,13 +333,15 @@ export function animateValue({ animationDriver.start() } - // TODO Resolve back to vanilla keyframe generator if (visualElement && name) { visualElement.resolveKeyframes( name, unresolvedKeyframes, createGenerator ) + } else { + // TODO: If any keyframe is null, propagate + createGenerator(unresolvedKeyframes) } const controls = { diff --git a/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx b/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx index 1669f754d5..aa79ce9ef6 100644 --- a/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx @@ -101,7 +101,7 @@ describe("animate prop as object", () => { return expect(promise).resolves.toBe(true) }) test("uses transitionEnd on subsequent renders", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const x = motionValue(0) const Component = ({ animate }: any) => ( @@ -133,7 +133,11 @@ describe("animate prop as object", () => { }} /> ) - requestAnimationFrame(() => resolve(x.get())) + + await nextFrame() + await nextFrame() + + resolve(x.get()) }) return expect(promise).resolves.toBe(300) }) @@ -816,7 +820,7 @@ describe("animate prop as object", () => { return expect(promise).resolves.toBe("#000") }) - test("forces an animation to fallback if has been set to `null`", async () => { + test.only("forces an animation to fallback if has been set to `null`", async () => { const promise = new Promise(async (resolve) => { const complete = () => resolve(true) const Component = ({ animate, onAnimationComplete }: any) => ( @@ -943,9 +947,9 @@ describe("animate prop as object", () => { ref={ref} initial={{ backgroundColor: "#0088ff" }} animate={{ backgroundColor: "hsl(345, 100%, 60%)" }} - onAnimationComplete={() => + onAnimationComplete={() => { ref.current && resolve(ref.current) - } + }} transition={{ duration: 0.01 }} /> ) diff --git a/packages/framer-motion/src/render/utils/animation-state.ts b/packages/framer-motion/src/render/utils/animation-state.ts index 78b2d805a6..01e75c1e30 100644 --- a/packages/framer-motion/src/render/utils/animation-state.ts +++ b/packages/framer-motion/src/render/utils/animation-state.ts @@ -344,7 +344,7 @@ export function createAnimationState( ) { shouldAnimate = false } - + console.log({ animations: animations[0]?.animation }) isInitialRender = false return shouldAnimate ? animate(animations) : Promise.resolve() } From 372f5a8ed0817d24fc5ca654999af17bece8d000 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 24 Jan 2024 14:53:11 +0100 Subject: [PATCH 12/44] Reducing size --- .../src/animation/hooks/animation-controls.ts | 32 ++++- .../framer-motion/src/render/VisualElement.ts | 13 +- .../src/render/utils/animation-state.ts | 8 +- .../framer-motion/src/render/utils/setters.ts | 135 +----------------- packages/framer-motion/src/three-entry.ts | 1 - 5 files changed, 47 insertions(+), 142 deletions(-) diff --git a/packages/framer-motion/src/animation/hooks/animation-controls.ts b/packages/framer-motion/src/animation/hooks/animation-controls.ts index 93745b8061..4526d38df6 100644 --- a/packages/framer-motion/src/animation/hooks/animation-controls.ts +++ b/packages/framer-motion/src/animation/hooks/animation-controls.ts @@ -1,13 +1,41 @@ import { invariant } from "../../utils/errors" -import { setValues } from "../../render/utils/setters" +import { setTarget } from "../../render/utils/setters" import type { VisualElement } from "../../render/VisualElement" -import { AnimationControls } from "../types" +import { AnimationControls, AnimationDefinition } from "../types" import { animateVisualElement } from "../interfaces/visual-element" function stopAnimation(visualElement: VisualElement) { visualElement.values.forEach((value) => value.stop()) } +function setVariants(visualElement: VisualElement, variantLabels: string[]) { + const reversedLabels = [...variantLabels].reverse() + + reversedLabels.forEach((key) => { + const variant = visualElement.getVariant(key) + variant && setTarget(visualElement, variant) + + if (visualElement.variantChildren) { + visualElement.variantChildren.forEach((child) => { + setVariants(child, variantLabels) + }) + } + }) +} + +export function setValues( + visualElement: VisualElement, + definition: AnimationDefinition +) { + if (Array.isArray(definition)) { + return setVariants(visualElement, definition) + } else if (typeof definition === "string") { + return setVariants(visualElement, [definition]) + } else { + setTarget(visualElement, definition as any) + } +} + /** * @public */ diff --git a/packages/framer-motion/src/render/VisualElement.ts b/packages/framer-motion/src/render/VisualElement.ts index 8d29853d65..d4aa3af96f 100644 --- a/packages/framer-motion/src/render/VisualElement.ts +++ b/packages/framer-motion/src/render/VisualElement.ts @@ -821,10 +821,17 @@ export abstract class VisualElement< * directly from the instance (which might have performance implications). */ readValue(key: string) { - return this.latestValues[key] !== undefined || !this.current - ? this.latestValues[key] - : this.getBaseTargetFromProps(this.props, key) ?? + const 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) { + this.setBaseTarget(key, isMotionValue(value) ? value.get() : value) + } + + return value } /** diff --git a/packages/framer-motion/src/render/utils/animation-state.ts b/packages/framer-motion/src/render/utils/animation-state.ts index 01e75c1e30..ec694280fd 100644 --- a/packages/framer-motion/src/render/utils/animation-state.ts +++ b/packages/framer-motion/src/render/utils/animation-state.ts @@ -263,8 +263,10 @@ export function createAnimationState( valueHasChanged = next !== prev } + console.log({ valueHasChanged, next, prev }) + if (valueHasChanged) { - if (next !== undefined) { + if (next !== undefined && next !== null) { // If next is defined and doesn't equal prev, it needs animating markToAnimate(key) } else { @@ -317,6 +319,8 @@ export function createAnimationState( } } + console.log({ removedKeys }) + /** * 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 @@ -326,7 +330,7 @@ export function createAnimationState( const fallbackAnimation = {} removedKeys.forEach((key) => { const fallbackTarget = visualElement.getBaseTarget(key) - + console.log({ fallbackTarget }) if (fallbackTarget !== undefined) { fallbackAnimation[key] = fallbackTarget } diff --git a/packages/framer-motion/src/render/utils/setters.ts b/packages/framer-motion/src/render/utils/setters.ts index 5dd1f1c486..cd4904dc67 100644 --- a/packages/framer-motion/src/render/utils/setters.ts +++ b/packages/framer-motion/src/render/utils/setters.ts @@ -1,19 +1,6 @@ -import { AnimationDefinition } from "../../animation/types" -import { - Target, - TargetAndTransition, - TargetResolver, - TargetWithKeyframes, - Transition, -} from "../../types" -import { isNumericalString } from "../../utils/is-numerical-string" -import { isZeroValueString } from "../../utils/is-zero-value-string" +import { TargetAndTransition, TargetResolver } from "../../types" import { resolveFinalValueInKeyframes } from "../../utils/resolve-value" import { motionValue } from "../../value" -import { complex } from "../../value/types/complex" -import { getAnimatableNone } from "../dom/value-types/animatable-none" -import { findValueType } from "../dom/value-types/find" -import { ResolvedValues } from "../types" import type { VisualElement } from "../VisualElement" import { resolveVariant } from "./resolve-dynamic-variants" @@ -47,123 +34,3 @@ export function setTarget( setMotionValue(visualElement, key, value as string | number) } } - -function setVariants(visualElement: VisualElement, variantLabels: string[]) { - const reversedLabels = [...variantLabels].reverse() - - reversedLabels.forEach((key) => { - const variant = visualElement.getVariant(key) - variant && setTarget(visualElement, variant) - - if (visualElement.variantChildren) { - visualElement.variantChildren.forEach((child) => { - setVariants(child, variantLabels) - }) - } - }) -} - -export function setValues( - visualElement: VisualElement, - definition: AnimationDefinition -) { - if (Array.isArray(definition)) { - return setVariants(visualElement, definition) - } else if (typeof definition === "string") { - return setVariants(visualElement, [definition]) - } else { - setTarget(visualElement, definition as any) - } -} - -export function checkTargetForNewValues( - visualElement: VisualElement, - target: TargetWithKeyframes, - origin: ResolvedValues -) { - const newValueKeys = Object.keys(target).filter( - (key) => !visualElement.hasValue(key) - ) - - const numNewValues = newValueKeys.length - - if (!numNewValues) return - - for (let i = 0; i < numNewValues; i++) { - const key = newValueKeys[i] - const targetValue = target[key] - let value: string | number | null = null - - /** - * If the target is a series of keyframes, we can use the first value - * in the array. If this first value is null, we'll still need to read from the DOM. - */ - if (Array.isArray(targetValue)) { - value = targetValue[0] - } - - /** - * If the target isn't keyframes, or the first keyframe was null, we need to - * first check if an origin value was explicitly defined in the transition as "from", - * if not read the value from the DOM. As an absolute fallback, take the defined target value. - */ - if (value === null) { - value = origin[key] ?? visualElement.readValue(key) ?? target[key] - } - - /** - * If value is still undefined or null, ignore it. Preferably this would throw, - * but this was causing issues in Framer. - */ - if (value === undefined || value === null) continue - - 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(targetValue)) { - value = getAnimatableNone(key, targetValue) - } - - visualElement.addValue( - key, - motionValue(value, { owner: visualElement }) - ) - if (origin[key] === undefined) { - origin[key] = value as number | string - } - if (value !== null) visualElement.setBaseTarget(key, value) - } -} - -export function getOriginFromTransition(key: string, transition: Transition) { - if (!transition) return - const valueTransition = - transition[key] || transition["default"] || transition - return valueTransition.from -} - -export function getOrigin( - target: Target, - transition: Transition, - visualElement: VisualElement -) { - const origin: Target = {} - - for (const key in target) { - const transitionOrigin = getOriginFromTransition(key, transition) - - if (transitionOrigin !== undefined) { - origin[key] = transitionOrigin - } else { - const value = visualElement.getValue(key) - if (value) { - origin[key] = value.get() - } - } - } - - return origin -} diff --git a/packages/framer-motion/src/three-entry.ts b/packages/framer-motion/src/three-entry.ts index 06280a950b..3c5aa560b8 100644 --- a/packages/framer-motion/src/three-entry.ts +++ b/packages/framer-motion/src/three-entry.ts @@ -6,7 +6,6 @@ export type { export { AnimationType } from "./render/utils/types" export { animations } from "./motion/features/animations" export { MotionContext } from "./context/MotionContext" -export { checkTargetForNewValues } from "./render/utils/setters" export { createBox } from "./projection/geometry/models" export { calcLength } from "./projection/geometry/delta-calc" export { filterProps } from "./render/dom/utils/filter-props" From 0296ad2bda3ddb1f55821cd5bb9fb9ae5bdae97b Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 24 Jan 2024 17:52:36 +0100 Subject: [PATCH 13/44] Fixing test --- packages/framer-motion/package.json | 2 +- .../src/animation/__tests__/animate.test.tsx | 2 +- .../src/animation/animators/js/index.ts | 80 ++++++++++++++----- .../src/animation/animators/utils/can-skip.ts | 65 +++++++++++++++ .../waapi/create-accelerated-animation.ts | 55 ++++++++++--- .../src/animation/generators/keyframes.ts | 2 +- .../src/animation/interfaces/motion-value.ts | 52 +----------- .../interfaces/visual-element-target.ts | 40 +--------- .../animation/interfaces/visual-element.ts | 9 ++- packages/framer-motion/src/animation/types.ts | 16 +++- .../src/animation/utils/is-animatable.ts | 7 +- .../framer-motion/src/frameloop/batcher.ts | 1 + .../drag/VisualElementDragControls.ts | 8 +- .../motion/__tests__/animate-prop.test.tsx | 49 +++++++----- .../framer-motion/src/render/VisualElement.ts | 33 ++++++-- .../src/render/dom/DOMKeyframesResolver.ts | 4 +- .../src/render/dom/DOMVisualElement.ts | 13 +-- .../render/html/utils/make-none-animatable.ts | 4 +- .../src/render/utils/KeyframesResolver.ts | 65 ++++++++++----- .../src/render/utils/animation-state.ts | 12 +-- 20 files changed, 313 insertions(+), 206 deletions(-) create mode 100644 packages/framer-motion/src/animation/animators/utils/can-skip.ts diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index b69dc619b0..ef060a58e9 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -47,7 +47,7 @@ "clean": "rm -rf types dist lib", "test": "yarn test-server && yarn test-client", "test-ci": "yarn test", - "test-client": "jest --config jest.config.json --max-workers=2 animate-prop", + "test-client": "jest --config jest.config.json --max-workers=2 animation/__tests__/animate.test.ts", "test-server": "", "test-watch": "jest --watch --coverage --coverageReporters=lcov --config jest.config.json", "test-appear": "yarn run collect-appear-tests && start-server-and-test 'pushd ../../; python -m SimpleHTTPServer; popd' http://0.0.0.0:8000 'cypress run -s cypress/integration/appear.chrome.ts --config baseUrl=http://localhost:8000/'", diff --git a/packages/framer-motion/src/animation/__tests__/animate.test.tsx b/packages/framer-motion/src/animation/__tests__/animate.test.tsx index 022d346379..8e62c13cd7 100644 --- a/packages/framer-motion/src/animation/__tests__/animate.test.tsx +++ b/packages/framer-motion/src/animation/__tests__/animate.test.tsx @@ -112,7 +112,7 @@ describe("animate", () => { animate(motionValue("#fff"), ["#fff", "#000"]) }) - test("animates a motion value in sequence", async () => { + test.only("animates a motion value in sequence", async () => { const a = motionValue(0) const aOutput: number[] = [] diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts index 56842170d3..ae30d53d4e 100644 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ b/packages/framer-motion/src/animation/animators/js/index.ts @@ -15,7 +15,14 @@ import { calcGeneratorDuration } from "../../generators/utils/calc-duration" import { invariant } from "../../../utils/errors" import { mix } from "../../../utils/mix" import { pipe } from "../../../utils/pipe" -import { ResolvedKeyframes } from "../../../render/utils/KeyframesResolver" +import { + KeyframeResolver, + OnKeyframesResolved, + ResolvedKeyframes, +} from "../../../render/utils/KeyframesResolver" +import { instantAnimationState } from "../../../utils/use-instant-transition-state" +import { getFinalKeyframe } from "../waapi/utils/get-final-keyframe" +import { canSkipAnimation } from "../utils/can-skip" type GeneratorFactory = ( options: ValueAnimationOptions @@ -36,6 +43,15 @@ export interface MainThreadAnimationControls const percentToProgress = (percent: number) => percent / 100 +function defaultResolveKeyframes( + keyframes: V[], + onComplete: OnKeyframesResolved, + name?: string, + motionValue?: any +) { + return new KeyframeResolver(keyframes, onComplete, name, motionValue) +} + /** * Animate a single value on the main thread. * @@ -44,8 +60,10 @@ const percentToProgress = (percent: number) => percent / 100 * between the two. */ export function animateValue({ - name, keyframes: unresolvedKeyframes, + name, + motionValue, + resolveKeyframes = defaultResolveKeyframes, autoplay = true, delay = 0, driver = frameloopDriver, @@ -57,7 +75,6 @@ export function animateValue({ onStop, onComplete, onUpdate, - visualElement, ...options }: ValueAnimationOptions): MainThreadAnimationControls { let playState: AnimationPlayState = "idle" @@ -95,11 +112,40 @@ export function animateValue({ // Create the first finished promise updateFinishedPromise() + const finish = () => { + playState = "finished" + onComplete && onComplete() + stopAnimationDriver() + resolveFinishedPromise() + } + let animationDriver: DriverControls | undefined let initialKeyframe: V const createGenerator = (keyframes: ResolvedKeyframes) => { - // resolvedKeyframes = keyframes + if ( + canSkipAnimation( + keyframes, + name, + motionValue, + type, + options.isHandoff, + options.velocity + ) + ) { + if (instantAnimationState.current || !delay) { + if (onUpdate) { + onUpdate( + getFinalKeyframe(keyframes, { repeat, repeatType }) + ) + } + finish() + return + } else { + options.duration = 0 + } + } + initialKeyframe = keyframes[0] const generatorFactory = types[type] || keyframesGeneratorFactory @@ -280,6 +326,13 @@ export function animateValue({ return state } + const keyframeResolver = resolveKeyframes( + unresolvedKeyframes, + createGenerator, + name, + motionValue + ) + const stopAnimationDriver = () => { animationDriver && animationDriver.stop() animationDriver = undefined @@ -291,13 +344,7 @@ export function animateValue({ resolveFinishedPromise() updateFinishedPromise() startTime = cancelTime = null - } - - const finish = () => { - playState = "finished" - onComplete && onComplete() - stopAnimationDriver() - resolveFinishedPromise() + keyframeResolver.cancel() } const play = () => { @@ -333,17 +380,6 @@ export function animateValue({ animationDriver.start() } - if (visualElement && name) { - visualElement.resolveKeyframes( - name, - unresolvedKeyframes, - createGenerator - ) - } else { - // TODO: If any keyframe is null, propagate - createGenerator(unresolvedKeyframes) - } - const controls = { then(resolve: VoidFunction, reject?: VoidFunction) { return currentFinishedPromise.then(resolve, reject) diff --git a/packages/framer-motion/src/animation/animators/utils/can-skip.ts b/packages/framer-motion/src/animation/animators/utils/can-skip.ts new file mode 100644 index 0000000000..f815777deb --- /dev/null +++ b/packages/framer-motion/src/animation/animators/utils/can-skip.ts @@ -0,0 +1,65 @@ +import { ResolvedKeyframes } from "../../../render/utils/KeyframesResolver" +import { MotionGlobalConfig } from "../../../utils/GlobalConfig" +import { warning } from "../../../utils/errors" +import { instantAnimationState } from "../../../utils/use-instant-transition-state" +import type { MotionValue } from "../../../value" +import { isAnimatable } from "../../utils/is-animatable" + +function hasKeyframesChanged(keyframes: ResolvedKeyframes) { + const current = keyframes[0] + if (keyframes.length === 1) return true + for (let i = 0; i < keyframes.length; i++) { + if (keyframes[i] !== current) return true + } +} + +export function canSkipAnimation( + keyframes: ResolvedKeyframes, + name?: string, + value?: MotionValue, + type?: string, + isHandoff?: boolean, + velocity?: number +) { + let canSkip = !isHandoff && !hasKeyframesChanged(keyframes) + + if (type === "spring" && velocity) { + canSkip = false + } + + /** + * Temporarily disable skipping animations if there's an animation in + * progress. Better would be to track the current target of a value + * and compare that against valueTarget. + */ + if (value && value.animation) { + canSkip = false + } + + /** + * Check if we're able to animate between the start and end keyframes, + * and throw a warning if we're attempting to animate between one that's + * animatable and another that isn't. + */ + const originKeyframe = keyframes[0] + const targetKeyframe = keyframes[keyframes.length - 1] + const isOriginAnimatable = isAnimatable(originKeyframe, name) + const isTargetAnimatable = isAnimatable(targetKeyframe, name) + + warning( + isOriginAnimatable === isTargetAnimatable, + `You are trying to animate ${name} from "${originKeyframe}" to "${targetKeyframe}". ${originKeyframe} is not an animatable value - to enable this animation set ${originKeyframe} to a value animatable to ${targetKeyframe} via the \`style\` property.` + ) + + // Always skip if any of these are true + if ( + !isOriginAnimatable || + !isTargetAnimatable || + instantAnimationState.current || + MotionGlobalConfig.skipAnimations + ) { + canSkip = true + } + + return canSkip +} diff --git a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts index 28f0ea356b..813277960f 100644 --- a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts +++ b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts @@ -14,9 +14,13 @@ import { import { memo } from "../../../utils/memo" import { noop } from "../../../utils/noop" import { + KeyframeResolver, + OnKeyframesResolved, ResolvedKeyframes, flushKeyframeResolvers, } from "../../../render/utils/KeyframesResolver" +import { canSkipAnimation } from "../utils/can-skip" +import { instantAnimationState } from "../../../utils/use-instant-transition-state" const supportsWaapi = memo(() => Object.hasOwnProperty.call(Element.prototype, "animate") @@ -45,6 +49,15 @@ const sampleDelta = 10 //ms */ const maxDuration = 20_000 +function defaultResolveKeyframes( + keyframes: V[], + onComplete: OnKeyframesResolved, + name?: string, + motionValue?: any +) { + return new KeyframeResolver(keyframes, onComplete, name, motionValue) +} + const requiresPregeneratedKeyframes = ( valueName: string, options: ValueAnimationOptions @@ -59,8 +72,9 @@ export function createAcceleratedAnimation( { onUpdate, onComplete, - visualElement, + resolveKeyframes = defaultResolveKeyframes, name, + motionValue, ...options }: ValueAnimationOptions ): AnimationPlaybackControls | false { @@ -110,6 +124,31 @@ export function createAcceleratedAnimation( let animation: Animation | undefined const createWaapiAnimation = (keyframes: ResolvedKeyframes) => { + const finish = () => { + if (pendingCancel) return + value.set(getFinalKeyframe(keyframes, options)) + onComplete && onComplete() + safeCancel() + } + + if ( + canSkipAnimation( + keyframes, + valueName, + value, + options.type, + options.isHandoff, + options.velocity + ) + ) { + if (instantAnimationState.current || !options.delay) { + finish() + return + } else { + options.duration = 0 + } + } + /** * If this animation needs pre-generated keyframes then generate. */ @@ -167,12 +206,7 @@ export function createAcceleratedAnimation( * keyframe. If we didn't, when the WAAPI animation is finished it would * be removed from the element which would then revert to its old styles. */ - animation.onfinish = () => { - if (pendingCancel) return - value.set(getFinalKeyframe(keyframes, options)) - onComplete && onComplete() - safeCancel() - } + animation.onfinish = finish } const cancelAnimation = () => { @@ -192,10 +226,11 @@ export function createAcceleratedAnimation( updateFinishedPromise() } - const resolver = visualElement!.resolveKeyframes( - name!, + const resolver = resolveKeyframes( unresolvedKeyframes, - createWaapiAnimation + createWaapiAnimation, + name, + motionValue ) /** diff --git a/packages/framer-motion/src/animation/generators/keyframes.ts b/packages/framer-motion/src/animation/generators/keyframes.ts index 0670896f57..c7ec15a5b1 100644 --- a/packages/framer-motion/src/animation/generators/keyframes.ts +++ b/packages/framer-motion/src/animation/generators/keyframes.ts @@ -15,7 +15,7 @@ export function defaultEasing( return values.map(() => easing || easeInOut).splice(0, values.length - 1) } -export function keyframes({ +export function keyframes({ duration = 300, keyframes: keyframeValues, times, diff --git a/packages/framer-motion/src/animation/interfaces/motion-value.ts b/packages/framer-motion/src/animation/interfaces/motion-value.ts index e5366b8fb0..7a3147af9c 100644 --- a/packages/framer-motion/src/animation/interfaces/motion-value.ts +++ b/packages/framer-motion/src/animation/interfaces/motion-value.ts @@ -5,7 +5,6 @@ import { getDefaultTransition } from "../utils/default-transitions" import { getValueTransition, isTransitionDefined } from "../utils/transitions" import { animateValue } from "../animators/js" import { AnimationPlaybackControls, ValueAnimationOptions } from "../types" -import { VisualElement } from "../../render/VisualElement" import type { UnresolvedKeyframes } from "../../render/utils/KeyframesResolver" import { MotionGlobalConfig } from "../../utils/GlobalConfig" import { instantAnimationState } from "../../utils/use-instant-transition-state" @@ -20,8 +19,7 @@ export const animateMotionValue = ( name: string, value: MotionValue, target: V | UnresolvedKeyframes, - transition: Transition & { elapsed?: number; isHandoff?: boolean } = {}, - visualElement?: VisualElement + transition: Transition & { elapsed?: number; isHandoff?: boolean } = {} ): StartAnimation => { return (onComplete: VoidFunction): AnimationPlaybackControls => { const valueTransition = getValueTransition(transition, name) || {} @@ -55,7 +53,7 @@ export const animateMotionValue = ( valueTransition.onComplete && valueTransition.onComplete() }, name, - visualElement, + motionValue: value, } /** @@ -127,49 +125,3 @@ export const animateMotionValue = ( return animateValue(options) } } - -// export const animateMotionValue = ( -// valueName: string, -// value: MotionValue, -// target: ResolvedValueTarget, -// transition: Transition & { elapsed?: number; isHandoff?: boolean } = {} -// ): StartAnimation => { -// return (onComplete: VoidFunction): AnimationPlaybackControls => { - -// /** -// * Check if we're able to animate between the start and end keyframes, -// * and throw a warning if we're attempting to animate between one that's -// * animatable and another that isn't. -// */ -// const originKeyframe = keyframes[0] -// const targetKeyframe = keyframes[keyframes.length - 1] -// const isOriginAnimatable = isAnimatable(valueName, originKeyframe) -// const isTargetAnimatable = isAnimatable(valueName, targetKeyframe) -// warning( -// isOriginAnimatable === isTargetAnimatable, -// `You are trying to animate ${valueName} from "${originKeyframe}" to "${targetKeyframe}". ${originKeyframe} is not an animatable value - to enable this animation set ${originKeyframe} to a value animatable to ${targetKeyframe} via the \`style\` property.` -// ) - -// if (MotionGlobalConfig.skipAnimations || -// !isOriginAnimatable || -// !isTargetAnimatable || -// instantAnimationState.current || -// valueTransition.type === false -// ) { -// /** -// * If we can't animate this value, or the global instant animation flag is set, -// * or this is simply defined as an instant transition, return an instant transition. -// */ -// return createInstantAnimation( -// instantAnimationState.current -// ? { ...options, delay: 0 } -// : options -// ) -// } - -// /** -// * If we didn't create an accelerated animation, create a JS animation -// */ -// return animateValue(options) -// } -// } 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 a4d2e31e10..db1b109d5a 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts @@ -1,7 +1,7 @@ import { transformProps } from "../../render/html/utils/transform" import type { AnimationTypeState } from "../../render/utils/animation-state" import type { VisualElement } from "../../render/VisualElement" -import type { Target, TargetAndTransition } from "../../types" +import type { TargetAndTransition } from "../../types" import { optimizedAppearDataAttribute } from "../optimized-appear/data-id" import type { VisualElementAnimationOptions } from "./types" import { animateMotionValue } from "./motion-value" @@ -9,7 +9,6 @@ import { isWillChangeMotionValue } from "../../value/use-will-change/is" import { setTarget } from "../../render/utils/setters" import { AnimationPlaybackControls } from "../types" import { getValueTransition } from "../utils/transitions" -import { MotionValue } from "../../value" /** * Decide whether we should block this animation. Previously, we achieved this @@ -28,18 +27,6 @@ function shouldBlockAnimation( return shouldBlock } -function hasKeyframesChanged(value: MotionValue, target: Target) { - const current = value.get() - - if (Array.isArray(target)) { - for (let i = 0; i < target.length; i++) { - if (target[i] !== current) return true - } - } else { - return current !== target - } -} - export function animateTarget( visualElement: VisualElement, targetAndTransition: TargetAndTransition, @@ -98,28 +85,6 @@ export function animateTarget( } } - let canSkip = - !valueTransition.isHandoff && - !hasKeyframesChanged(value, valueTarget) - - if ( - valueTransition.type === "spring" && - (value.getVelocity() || valueTransition.velocity) - ) { - canSkip = false - } - - /** - * Temporarily disable skipping animations if there's an animation in - * progress. Better would be to track the current target of a value - * and compare that against valueTarget. - */ - if (value.animation) { - canSkip = false - } - - if (canSkip) continue - value.start( animateMotionValue( key, @@ -127,8 +92,7 @@ export function animateTarget( valueTarget, visualElement.shouldReduceMotion && transformProps.has(key) ? { type: false } - : valueTransition, - visualElement + : valueTransition ) ) diff --git a/packages/framer-motion/src/animation/interfaces/visual-element.ts b/packages/framer-motion/src/animation/interfaces/visual-element.ts index 2e3bc89f48..591a8ad351 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element.ts @@ -1,3 +1,4 @@ +import { frame } from "../../frameloop" import { resolveVariant } from "../../render/utils/resolve-dynamic-variants" import { VisualElement } from "../../render/VisualElement" import { AnimationDefinition } from "../types" @@ -31,7 +32,9 @@ export function animateVisualElement( ) } - return animation.then(() => - visualElement.notify("AnimationComplete", definition) - ) + return animation.then(() => { + frame.postRender(() => { + visualElement.notify("AnimationComplete", definition) + }) + }) } diff --git a/packages/framer-motion/src/animation/types.ts b/packages/framer-motion/src/animation/types.ts index 085ba6f0f7..04ec246f00 100644 --- a/packages/framer-motion/src/animation/types.ts +++ b/packages/framer-motion/src/animation/types.ts @@ -5,6 +5,11 @@ import { Driver } from "./animators/js/types" import { SVGPathProperties, VariantLabels } from "../motion/types" import { SVGAttributes } from "../render/svg/types-attributes" import { ProgressTimeline } from "../render/dom/scroll/observe" +import { MotionValue } from "../value" +import { + KeyframeResolver, + OnKeyframesResolved, +} from "../render/utils/KeyframesResolver" export interface AnimationPlaybackLifecycles { onUpdate?: (latest: V) => void @@ -33,12 +38,17 @@ export interface ValueAnimationTransition isHandoff?: boolean } -export interface ValueAnimationOptions +export interface ValueAnimationOptions extends ValueAnimationTransition { keyframes: V[] - visualElement?: VisualElement name?: string - // Legacy + motionValue?: MotionValue + resolveKeyframes?: ( + keyframes: V[], + onComplete: OnKeyframesResolved, + name?: string, + motionValue?: any + ) => KeyframeResolver from?: V } diff --git a/packages/framer-motion/src/animation/utils/is-animatable.ts b/packages/framer-motion/src/animation/utils/is-animatable.ts index 27febc13f2..72ffe8a4c3 100644 --- a/packages/framer-motion/src/animation/utils/is-animatable.ts +++ b/packages/framer-motion/src/animation/utils/is-animatable.ts @@ -10,9 +10,12 @@ import { ValueKeyframesDefinition } from "../types" * * @internal */ -export const isAnimatable = (key: string, value: ValueKeyframesDefinition) => { +export const isAnimatable = ( + value: ValueKeyframesDefinition, + name?: string +) => { // If the list of keys tat might be non-animatable grows, replace with Set - if (key === "zIndex") return false + if (name === "zIndex") return false // If it's a number or a keyframes array, we can animate it. We might at some point // need to do a deep isAnimatable check of keyframes, or let Popmotion handle this, diff --git a/packages/framer-motion/src/frameloop/batcher.ts b/packages/framer-motion/src/frameloop/batcher.ts index 00d187b3c8..07bdcc347f 100644 --- a/packages/framer-motion/src/frameloop/batcher.ts +++ b/packages/framer-motion/src/frameloop/batcher.ts @@ -69,6 +69,7 @@ export function createRenderBatcher( const step = steps[key] acc[key] = (process: Process, keepAlive = false, immediate = false) => { if (!runNextFrame) wake() + return step.schedule(process, keepAlive, immediate) } return acc diff --git a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts index 76395a108a..b0f69416d0 100644 --- a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts +++ b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts @@ -441,13 +441,7 @@ export class VisualElementDragControls { ) { const axisValue = this.getAxisMotionValue(axis) return axisValue.start( - animateMotionValue( - axis, - axisValue, - 0, - transition, - this.visualElement - ) + animateMotionValue(axis, axisValue, 0, transition) ) } diff --git a/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx b/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx index aa79ce9ef6..0166be451d 100644 --- a/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx @@ -8,7 +8,7 @@ import { } from "../../" import * as React from "react" import { createRef } from "react" -import { nextFrame, nextMicrotask } from "../../gestures/__tests__/utils" +import { nextFrame } from "../../gestures/__tests__/utils" describe("animate prop as object", () => { test("animates to set prop", async () => { @@ -266,7 +266,7 @@ describe("animate prop as object", () => { return expect(promise).resolves.toHaveStyle("font-weight: 100") }) test("doesn't animate no-op values", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { let isAnimating = false const Component = () => ( { const { rerender } = render() rerender() - frame.postRender(() => { - frame.postRender(() => resolve(isAnimating)) - }) + await nextFrame() + await nextFrame() + + resolve(isAnimating) }) return expect(promise).resolves.toBe(false) }) test("doesn't animate no-op keyframes", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { let isAnimating = false const Component = () => ( { const { rerender } = render() rerender() - frame.postRender(() => { - frame.postRender(() => resolve(isAnimating)) - }) + await nextFrame() + await nextFrame() + + resolve(isAnimating) }) return expect(promise).resolves.toBe(false) }) - test("doe animate different keyframes", async () => { + test("does animate different keyframes", async () => { const promise = new Promise((resolve) => { let isAnimating = false const Component = () => ( @@ -381,13 +383,13 @@ describe("animate prop as object", () => { return expect(promise).resolves.toBe(true) }) test("doesn't animate zIndex", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const Component = () => const { container, rerender } = render() rerender() - requestAnimationFrame(() => - resolve(container.firstChild as Element) - ) + + await nextFrame() + resolve(container.firstChild as Element) }) return expect(promise).resolves.toHaveStyle("z-index: 100") }) @@ -414,7 +416,7 @@ describe("animate prop as object", () => { rerender() rerender() - await nextMicrotask() + await nextFrame() expect(ref.current).toHaveStyle("opacity: 0") @@ -456,7 +458,7 @@ describe("animate prop as object", () => { /> ) - await nextMicrotask() + await nextFrame() expect(ref.current).toHaveStyle("opacity: 0.5") @@ -774,10 +776,11 @@ describe("animate prop as object", () => { ) rerender() + rerender() rerender() - await nextMicrotask() + await nextFrame() return expect(container.firstChild as Element).toHaveStyle( "transform: translateX(0px) translateY(100px) translateZ(0)" @@ -805,11 +808,17 @@ describe("animate prop as object", () => { test("animates previously unseen CSS variables", async () => { const promise = new Promise((resolve) => { + let latestColor = "" const Component = () => ( resolve(latest["--foo"] as string)} + onUpdate={(latest) => { + latestColor = latest["--foo"] as string + }} + onAnimationComplete={() => { + resolve(latestColor) + }} transition={{ type: false }} /> ) @@ -817,10 +826,10 @@ describe("animate prop as object", () => { rerender() }) - return expect(promise).resolves.toBe("#000") + return expect(promise).resolves.toBe("rgba(0, 0, 0, 1)") }) - test.only("forces an animation to fallback if has been set to `null`", async () => { + test("forces an animation to fallback if has been set to `null`", async () => { const promise = new Promise(async (resolve) => { const complete = () => resolve(true) const Component = ({ animate, onAnimationComplete }: any) => ( diff --git a/packages/framer-motion/src/render/VisualElement.ts b/packages/framer-motion/src/render/VisualElement.ts index d4aa3af96f..e9c4297365 100644 --- a/packages/framer-motion/src/render/VisualElement.ts +++ b/packages/framer-motion/src/render/VisualElement.ts @@ -46,6 +46,11 @@ import { ResolvedKeyframes, UnresolvedKeyframes, } from "./utils/KeyframesResolver" +import { isNumericalString } from "../utils/is-numerical-string" +import { isZeroValueString } from "../utils/is-zero-value-string" +import { findValueType } from "./dom/value-types/find" +import { complex } from "../value/types/complex" +import { getAnimatableNone } from "./dom/value-types/animatable-none" const featureNames = Object.keys(featureDefinitions) const numFeatures = featureNames.length @@ -154,7 +159,13 @@ export abstract class VisualElement< // the resolution of keyframes synchronously. onComplete: (resolvedKeyframes: ResolvedKeyframes) => void ): KeyframeResolver { - return new KeyframeResolver(this, name, keyframes, onComplete) + return new this.KeyframeResolver( + keyframes, + onComplete, + name, + this.getValue(name), + (target?: any) => this.readValue(name, target) as any + ) } /** @@ -266,6 +277,8 @@ export abstract class VisualElement< */ 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. @@ -458,8 +471,8 @@ export abstract class VisualElement< "change", (latestValue: string | number) => { this.latestValues[key] = latestValue - this.props.onUpdate && - frame.update(this.notifyUpdate, false, true) + + this.props.onUpdate && frame.preRender(this.notifyUpdate) if (valueIsTransform && this.projection) { this.projection.isTransformDirty = true @@ -820,14 +833,24 @@ export abstract class VisualElement< * 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) { - const value = + readValue(key: string, target?: string | number | 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) } diff --git a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts index 68b882e9f3..a573a579c6 100644 --- a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts +++ b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts @@ -17,7 +17,9 @@ import { makeNoneKeyframesAnimatable } from "../html/utils/make-none-animatable" */ export class DOMKeyframesResolver< T extends string | number -> extends KeyframeResolver { +> extends KeyframeResolver { + name: string + private removedTransforms?: [string, string | number][] // private restoreScrollY?: number private measuredOrigin?: string | number diff --git a/packages/framer-motion/src/render/dom/DOMVisualElement.ts b/packages/framer-motion/src/render/dom/DOMVisualElement.ts index 81075cb119..e7c486bd99 100644 --- a/packages/framer-motion/src/render/dom/DOMVisualElement.ts +++ b/packages/framer-motion/src/render/dom/DOMVisualElement.ts @@ -3,11 +3,6 @@ import { VisualElement } from "../VisualElement" import { MotionProps } from "../../motion/types" import { MotionValue } from "../../value" import { HTMLRenderState } from "../html/types" -import type { - KeyframeResolver, - ResolvedKeyframes, - UnresolvedKeyframes, -} from "../utils/KeyframesResolver" import { DOMKeyframesResolver } from "./DOMKeyframesResolver" export abstract class DOMVisualElement< @@ -39,11 +34,5 @@ export abstract class DOMVisualElement< delete style[key] } - resolveKeyframes( - name: string, - keyframes: UnresolvedKeyframes, - onComplete: (resolvedKeyframes: ResolvedKeyframes) => void - ): KeyframeResolver { - return new DOMKeyframesResolver(this, name, keyframes, onComplete) - } + KeyframeResolver = DOMKeyframesResolver } diff --git a/packages/framer-motion/src/render/html/utils/make-none-animatable.ts b/packages/framer-motion/src/render/html/utils/make-none-animatable.ts index a800b90e07..7398bba959 100644 --- a/packages/framer-motion/src/render/html/utils/make-none-animatable.ts +++ b/packages/framer-motion/src/render/html/utils/make-none-animatable.ts @@ -4,7 +4,7 @@ import { UnresolvedKeyframes } from "../../utils/KeyframesResolver" export function makeNoneKeyframesAnimatable( unresolvedKeyframes: UnresolvedKeyframes, noneKeyframeIndexes: number[], - name: string + name?: string ) { /** * If we detected "none"-equivalent keyframes, we need to find a template @@ -22,7 +22,7 @@ export function makeNoneKeyframesAnimatable( i++ } - if (animatableTemplate) { + if (animatableTemplate && name) { for (const noneIndex of noneKeyframeIndexes) { unresolvedKeyframes[noneIndex] = getAnimatableNone( name, diff --git a/packages/framer-motion/src/render/utils/KeyframesResolver.ts b/packages/framer-motion/src/render/utils/KeyframesResolver.ts index 910fbb8d1c..9bcc4b8280 100644 --- a/packages/framer-motion/src/render/utils/KeyframesResolver.ts +++ b/packages/framer-motion/src/render/utils/KeyframesResolver.ts @@ -1,4 +1,5 @@ import { cancelFrame, frame } from "../../frameloop" +import { MotionValue } from "../../value" import type { VisualElement } from "../VisualElement" export type UnresolvedKeyframes = Array @@ -55,23 +56,35 @@ export function flushKeyframeResolvers() { cancelFrame(measureAllKeyframes) } -export class KeyframeResolver { - element: VisualElement - name: string +export type OnKeyframesResolved = ( + resolvedKeyframes: ResolvedKeyframes +) => void + +type ReadValue = ( + target?: string | number | null +) => string | number | undefined | null + +export class KeyframeResolver { + element: VisualElement + name?: string resolvedKeyframes: ResolvedKeyframes | undefined unresolvedKeyframes: UnresolvedKeyframes - private onComplete: (resolvedKeyframes: ResolvedKeyframes) => void + motionValue?: MotionValue + readValueFromInstance?: ReadValue + private onComplete: OnKeyframesResolved constructor( - element: VisualElement, - name: string, unresolvedKeyframes: UnresolvedKeyframes, - onComplete: (resolvedKeyframes: ResolvedKeyframes) => void + onComplete: OnKeyframesResolved, + name?: string, + motionValue?: MotionValue, + readValueFromInstance?: ReadValue ) { - this.element = element - this.name = name this.unresolvedKeyframes = unresolvedKeyframes this.onComplete = onComplete + this.name = name + this.motionValue = motionValue + this.readValueFromInstance = readValueFromInstance toResolve.add(this) @@ -84,9 +97,12 @@ export class KeyframeResolver { needsMeasurement = false readKeyframes() { - const { unresolvedKeyframes, element, name } = this - - if (!element.current) return + const { + unresolvedKeyframes, + readValueFromInstance, + name, + motionValue, + } = this /** * If a keyframe is null, we hydrate it either by reading it from @@ -98,16 +114,27 @@ export class KeyframeResolver { * If the first keyframe is null, we need to find its value by sampling the element */ if (i === 0) { - const currentValue = element.getValue(name)!.get() + const currentValue = motionValue?.get() - // TODO Clean this up a bit - unresolvedKeyframes[0] = - currentValue ?? - (element.readValue(name) as T) ?? + const finalKeyframe = unresolvedKeyframes[unresolvedKeyframes.length - 1] - if (currentValue === undefined) { - element.getValue(name)!.set(unresolvedKeyframes[0]) + if (currentValue !== undefined) { + unresolvedKeyframes[0] = currentValue + } else if (readValueFromInstance && name) { + const valueAsRead = readValueFromInstance(finalKeyframe) + + if (valueAsRead !== undefined) { + unresolvedKeyframes[0] = valueAsRead + } + } + + if (unresolvedKeyframes[0] === undefined) { + unresolvedKeyframes[0] = finalKeyframe + } + + if (motionValue && currentValue === undefined) { + motionValue.set(unresolvedKeyframes[0] as T) } } else { unresolvedKeyframes[i] = unresolvedKeyframes[i - 1] diff --git a/packages/framer-motion/src/render/utils/animation-state.ts b/packages/framer-motion/src/render/utils/animation-state.ts index ec694280fd..bf219f0956 100644 --- a/packages/framer-motion/src/render/utils/animation-state.ts +++ b/packages/framer-motion/src/render/utils/animation-state.ts @@ -263,8 +263,6 @@ export function createAnimationState( valueHasChanged = next !== prev } - console.log({ valueHasChanged, next, prev }) - if (valueHasChanged) { if (next !== undefined && next !== null) { // If next is defined and doesn't equal prev, it needs animating @@ -319,8 +317,6 @@ export function createAnimationState( } } - console.log({ removedKeys }) - /** * 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 @@ -330,10 +326,8 @@ export function createAnimationState( const fallbackAnimation = {} removedKeys.forEach((key) => { const fallbackTarget = visualElement.getBaseTarget(key) - console.log({ fallbackTarget }) - if (fallbackTarget !== undefined) { - fallbackAnimation[key] = fallbackTarget - } + fallbackAnimation[key] = + fallbackTarget === undefined ? null : fallbackTarget }) animations.push({ animation: fallbackAnimation }) @@ -348,7 +342,7 @@ export function createAnimationState( ) { shouldAnimate = false } - console.log({ animations: animations[0]?.animation }) + isInitialRender = false return shouldAnimate ? animate(animations) : Promise.resolve() } From 76e8f767caee99dcaf27d3590d52300d34790f4f Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Thu, 25 Jan 2024 11:38:18 +0100 Subject: [PATCH 14/44] Updating --- packages/framer-motion/package.json | 2 +- .../__tests__/animate-waapi.test.tsx | 23 +++- .../src/animation/__tests__/animate.test.tsx | 2 +- .../src/animation/__tests__/index.test.tsx | 7 +- .../src/animation/animators/js/index.ts | 24 ++-- .../src/animation/animators/utils/can-skip.ts | 2 +- .../waapi/create-accelerated-animation.ts | 33 +++-- .../src/animation/interfaces/motion-value.ts | 5 +- .../interfaces/visual-element-target.ts | 3 +- packages/framer-motion/src/animation/types.ts | 14 ++- .../drag/VisualElementDragControls.ts | 8 +- .../src/motion/__tests__/waapi.test.tsx | 114 +++++++++++++----- .../framer-motion/src/render/VisualElement.ts | 15 +-- .../src/render/dom/DOMKeyframesResolver.ts | 2 + .../src/render/utils/KeyframesResolver.ts | 27 ++--- 15 files changed, 188 insertions(+), 93 deletions(-) diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index ef060a58e9..6aa09e1b87 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -47,7 +47,7 @@ "clean": "rm -rf types dist lib", "test": "yarn test-server && yarn test-client", "test-ci": "yarn test", - "test-client": "jest --config jest.config.json --max-workers=2 animation/__tests__/animate.test.ts", + "test-client": "jest --config jest.config.json --max-workers=2 animate-waapi", "test-server": "", "test-watch": "jest --watch --coverage --coverageReporters=lcov --config jest.config.json", "test-appear": "yarn run collect-appear-tests && start-server-and-test 'pushd ../../; python -m SimpleHTTPServer; popd' http://0.0.0.0:8000 'cypress run -s cypress/integration/appear.chrome.ts --config baseUrl=http://localhost:8000/'", diff --git a/packages/framer-motion/src/animation/__tests__/animate-waapi.test.tsx b/packages/framer-motion/src/animation/__tests__/animate-waapi.test.tsx index d71c19e1bb..5f584bb693 100644 --- a/packages/framer-motion/src/animation/__tests__/animate-waapi.test.tsx +++ b/packages/framer-motion/src/animation/__tests__/animate-waapi.test.tsx @@ -127,12 +127,31 @@ describe("animate() with WAAPI", () => { ) }) - test("Returns duration correctly", async () => { + test.only("Returns duration correctly", async () => { + const a = document.createElement("div") + const animation = animate( - document.createElement("div"), + a, { opacity: 1 }, { duration: 2, opacity: { duration: 3 } } ) + + await nextFrame() + + expect(a.animate).toBeCalledWith( + { + opacity: [0, 1], + }, + { + delay: -0, + duration: 3000, + easing: "ease-out", + iterations: 1, + direction: "normal", + fill: "both", + } + ) + expect(animation.duration).toEqual(3) }) diff --git a/packages/framer-motion/src/animation/__tests__/animate.test.tsx b/packages/framer-motion/src/animation/__tests__/animate.test.tsx index 8e62c13cd7..022d346379 100644 --- a/packages/framer-motion/src/animation/__tests__/animate.test.tsx +++ b/packages/framer-motion/src/animation/__tests__/animate.test.tsx @@ -112,7 +112,7 @@ describe("animate", () => { animate(motionValue("#fff"), ["#fff", "#000"]) }) - test.only("animates a motion value in sequence", async () => { + test("animates a motion value in sequence", async () => { const a = motionValue(0) const aOutput: number[] = [] diff --git a/packages/framer-motion/src/animation/__tests__/index.test.tsx b/packages/framer-motion/src/animation/__tests__/index.test.tsx index c02b34713f..c77d481a87 100644 --- a/packages/framer-motion/src/animation/__tests__/index.test.tsx +++ b/packages/framer-motion/src/animation/__tests__/index.test.tsx @@ -215,7 +215,10 @@ describe("useAnimation", () => { rerender() }) - return await expect(promise).resolves.toEqual([100, "#fff"]) + return await expect(promise).resolves.toEqual([ + 100, + "rgba(255, 255, 255, 1)", + ]) }) it("respects initial even if passed controls", () => { @@ -265,7 +268,7 @@ describe("useAnimation", () => { rerender() }) - return await expect(promise).resolves.toEqual("#fff") + return await expect(promise).resolves.toEqual("rgba(255, 255, 255, 1)") }) test("accepts array of variants", async () => { diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts index ae30d53d4e..c016d5ad1d 100644 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ b/packages/framer-motion/src/animation/animators/js/index.ts @@ -17,7 +17,6 @@ import { mix } from "../../../utils/mix" import { pipe } from "../../../utils/pipe" import { KeyframeResolver, - OnKeyframesResolved, ResolvedKeyframes, } from "../../../render/utils/KeyframesResolver" import { instantAnimationState } from "../../../utils/use-instant-transition-state" @@ -62,8 +61,8 @@ function defaultResolveKeyframes( export function animateValue({ keyframes: unresolvedKeyframes, name, + element, motionValue, - resolveKeyframes = defaultResolveKeyframes, autoplay = true, delay = 0, driver = frameloopDriver, @@ -326,12 +325,21 @@ export function animateValue({ return state } - const keyframeResolver = resolveKeyframes( - unresolvedKeyframes, - createGenerator, - name, - motionValue - ) + const keyframeResolver = + element && name && motionValue + ? element.resolveKeyframes( + unresolvedKeyframes, + createGenerator, + name, + motionValue + ) + : new KeyframeResolver( + unresolvedKeyframes, + createGenerator, + name, + motionValue, + element + ) const stopAnimationDriver = () => { animationDriver && animationDriver.stop() diff --git a/packages/framer-motion/src/animation/animators/utils/can-skip.ts b/packages/framer-motion/src/animation/animators/utils/can-skip.ts index f815777deb..6f90cfd32b 100644 --- a/packages/framer-motion/src/animation/animators/utils/can-skip.ts +++ b/packages/framer-motion/src/animation/animators/utils/can-skip.ts @@ -45,7 +45,7 @@ export function canSkipAnimation( const targetKeyframe = keyframes[keyframes.length - 1] const isOriginAnimatable = isAnimatable(originKeyframe, name) const isTargetAnimatable = isAnimatable(targetKeyframe, name) - + console.log(keyframes) warning( isOriginAnimatable === isTargetAnimatable, `You are trying to animate ${name} from "${originKeyframe}" to "${targetKeyframe}". ${originKeyframe} is not an animatable value - to enable this animation set ${originKeyframe} to a value animatable to ${targetKeyframe} via the \`style\` property.` diff --git a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts index 813277960f..3878416826 100644 --- a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts +++ b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts @@ -15,7 +15,6 @@ import { memo } from "../../../utils/memo" import { noop } from "../../../utils/noop" import { KeyframeResolver, - OnKeyframesResolved, ResolvedKeyframes, flushKeyframeResolvers, } from "../../../render/utils/KeyframesResolver" @@ -49,15 +48,6 @@ const sampleDelta = 10 //ms */ const maxDuration = 20_000 -function defaultResolveKeyframes( - keyframes: V[], - onComplete: OnKeyframesResolved, - name?: string, - motionValue?: any -) { - return new KeyframeResolver(keyframes, onComplete, name, motionValue) -} - const requiresPregeneratedKeyframes = ( valueName: string, options: ValueAnimationOptions @@ -72,7 +62,7 @@ export function createAcceleratedAnimation( { onUpdate, onComplete, - resolveKeyframes = defaultResolveKeyframes, + element, name, motionValue, ...options @@ -226,12 +216,21 @@ export function createAcceleratedAnimation( updateFinishedPromise() } - const resolver = resolveKeyframes( - unresolvedKeyframes, - createWaapiAnimation, - name, - motionValue - ) + const resolver = + element && name && motionValue + ? element.resolveKeyframes( + unresolvedKeyframes, + createWaapiAnimation, + name, + motionValue + ) + : new KeyframeResolver( + unresolvedKeyframes, + createWaapiAnimation, + name, + motionValue, + element + ) /** * Animation interrupt callback. diff --git a/packages/framer-motion/src/animation/interfaces/motion-value.ts b/packages/framer-motion/src/animation/interfaces/motion-value.ts index 7a3147af9c..d93941bf21 100644 --- a/packages/framer-motion/src/animation/interfaces/motion-value.ts +++ b/packages/framer-motion/src/animation/interfaces/motion-value.ts @@ -9,6 +9,7 @@ import type { UnresolvedKeyframes } from "../../render/utils/KeyframesResolver" import { MotionGlobalConfig } from "../../utils/GlobalConfig" import { instantAnimationState } from "../../utils/use-instant-transition-state" import { createAcceleratedAnimation } from "../animators/waapi/create-accelerated-animation" +import type { VisualElement } from "../../render/VisualElement" function makeTransitionInstant(options: ValueAnimationOptions) { options.duration = 0 @@ -19,7 +20,8 @@ export const animateMotionValue = ( name: string, value: MotionValue, target: V | UnresolvedKeyframes, - transition: Transition & { elapsed?: number; isHandoff?: boolean } = {} + transition: Transition & { elapsed?: number; isHandoff?: boolean } = {}, + element?: VisualElement ): StartAnimation => { return (onComplete: VoidFunction): AnimationPlaybackControls => { const valueTransition = getValueTransition(transition, name) || {} @@ -54,6 +56,7 @@ export const animateMotionValue = ( }, name, motionValue: value, + element, } /** 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 db1b109d5a..4f6b5cd759 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts @@ -92,7 +92,8 @@ export function animateTarget( valueTarget, visualElement.shouldReduceMotion && transformProps.has(key) ? { type: false } - : valueTransition + : valueTransition, + visualElement ) ) diff --git a/packages/framer-motion/src/animation/types.ts b/packages/framer-motion/src/animation/types.ts index 04ec246f00..559534bb2d 100644 --- a/packages/framer-motion/src/animation/types.ts +++ b/packages/framer-motion/src/animation/types.ts @@ -38,17 +38,19 @@ export interface ValueAnimationTransition isHandoff?: boolean } +export type ResolveKeyframes = ( + keyframes: V[], + onComplete: OnKeyframesResolved, + name?: string, + motionValue?: any +) => KeyframeResolver + export interface ValueAnimationOptions extends ValueAnimationTransition { keyframes: V[] name?: string motionValue?: MotionValue - resolveKeyframes?: ( - keyframes: V[], - onComplete: OnKeyframesResolved, - name?: string, - motionValue?: any - ) => KeyframeResolver + element?: VisualElement from?: V } diff --git a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts index b0f69416d0..76395a108a 100644 --- a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts +++ b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts @@ -441,7 +441,13 @@ export class VisualElementDragControls { ) { const axisValue = this.getAxisMotionValue(axis) return axisValue.start( - animateMotionValue(axis, axisValue, 0, transition) + animateMotionValue( + axis, + axisValue, + 0, + transition, + this.visualElement + ) ) } diff --git a/packages/framer-motion/src/motion/__tests__/waapi.test.tsx b/packages/framer-motion/src/motion/__tests__/waapi.test.tsx index 53b099ff8c..e29d0a61ab 100644 --- a/packages/framer-motion/src/motion/__tests__/waapi.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/waapi.test.tsx @@ -12,7 +12,7 @@ import { nextFrame } from "../../gestures/__tests__/utils" import "../../animation/animators/waapi/__tests__/setup" describe("WAAPI animations", () => { - test("opacity animates with WAAPI at default settings", () => { + test("opacity animates with WAAPI at default settings", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, @@ -38,7 +40,7 @@ describe("WAAPI animations", () => { ) }) - test("filter animates with WAAPI at default settings", () => { + test("filter animates with WAAPI at default settings", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -67,7 +71,7 @@ describe("WAAPI animations", () => { ) }) - test("clipPath animates with WAAPI at default settings", () => { + test("clipPath animates with WAAPI at default settings", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -96,7 +102,7 @@ describe("WAAPI animations", () => { ) }) - test("Complex string type animates with WAAPI spring", () => { + test("Complex string type animates with WAAPI spring", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -126,7 +134,7 @@ describe("WAAPI animations", () => { ) }) - test("transform animates with WAAPI at default settings", () => { + test("transform animates with WAAPI at default settings", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -167,6 +177,8 @@ describe("WAAPI animations", () => { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -216,7 +228,7 @@ describe("WAAPI animations", () => { ) }) - test("opacity animates with WAAPI when no value is originally provided via initial", () => { + test("opacity animates with WAAPI when no value is originally provided via initial", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() }) - test("opacity animates with WAAPI at default settings with no initial value set", () => { + test("opacity animates with WAAPI at default settings with no initial value set", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() }) - test("opacity animates with WAAPI at default settings when layout is enabled", () => { + test("opacity animates with WAAPI at default settings when layout is enabled", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() }) @@ -324,10 +342,12 @@ describe("WAAPI animations", () => { rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalledTimes(2) }) - test("WAAPI is called with expected arguments", () => { + test("WAAPI is called with expected arguments", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: [0.2, 0.9] }, @@ -361,7 +383,7 @@ describe("WAAPI animations", () => { ) }) - test("WAAPI is called with expected arguments with pre-generated keyframes", () => { + test("WAAPI is called with expected arguments with pre-generated keyframes", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0.5, 0.5, 0.5, 0.5, 0.5, 0.5], offset: undefined }, @@ -393,7 +417,7 @@ describe("WAAPI animations", () => { ) }) - test("Maps 'easeIn' to 'ease-in'", () => { + test("Maps 'easeIn' to 'ease-in'", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, @@ -422,7 +448,7 @@ describe("WAAPI animations", () => { ) }) - test("Maps 'easeOut' to 'ease-out'", () => { + test("Maps 'easeOut' to 'ease-out'", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, @@ -451,7 +479,7 @@ describe("WAAPI animations", () => { ) }) - test("Maps 'easeInOut' to 'ease-in-out'", () => { + test("Maps 'easeInOut' to 'ease-in-out'", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, @@ -480,7 +510,7 @@ describe("WAAPI animations", () => { ) }) - test("Maps 'circIn' to 'cubic-bezier(0, 0.65, 0.55, 1)'", () => { + test("Maps 'circIn' to 'cubic-bezier(0, 0.65, 0.55, 1)'", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, @@ -509,7 +541,7 @@ describe("WAAPI animations", () => { ) }) - test("Maps 'circOut' to 'cubic-bezier(0.55, 0, 1, 0.45)'", () => { + test("Maps 'circOut' to 'cubic-bezier(0.55, 0, 1, 0.45)'", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, @@ -538,7 +572,7 @@ describe("WAAPI animations", () => { ) }) - test("Maps 'backIn' to 'cubic-bezier(0.31, 0.01, 0.66, -0.59)'", () => { + test("Maps 'backIn' to 'cubic-bezier(0.31, 0.01, 0.66, -0.59)'", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, @@ -567,7 +602,7 @@ describe("WAAPI animations", () => { ) }) - test("Maps 'backOut' to 'cubic-bezier(0.33, 1.53, 0.69, 0.99)'", () => { + test("Maps 'backOut' to 'cubic-bezier(0.33, 1.53, 0.69, 0.99)'", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, @@ -596,7 +633,7 @@ describe("WAAPI animations", () => { ) }) - test("WAAPI is called with pre-generated spring keyframes", () => { + test("WAAPI is called with pre-generated spring keyframes", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -638,7 +677,7 @@ describe("WAAPI animations", () => { /** * TODO: We could not accelerate but scrub WAAPI animation if repeatDelay is defined */ - test("Doesn't animate with WAAPI if repeatDelay is defined", () => { + test("Doesn't animate with WAAPI if repeatDelay is defined", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).not.toBeCalled() }) - test("Pregenerates keyframes if ease is function", () => { + test("Pregenerates keyframes if ease is function", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -684,7 +727,7 @@ describe("WAAPI animations", () => { ) }) - test("Pregenerates keyframes if ease is anticipate", () => { + test("Pregenerates keyframes if ease is anticipate", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -717,7 +762,7 @@ describe("WAAPI animations", () => { ) }) - test("Pregenerates keyframes if ease is backInOut", () => { + test("Pregenerates keyframes if ease is backInOut", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -750,7 +797,7 @@ describe("WAAPI animations", () => { ) }) - test("Pregenerates keyframes if ease is circInOut", () => { + test("Pregenerates keyframes if ease is circInOut", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -783,7 +832,7 @@ describe("WAAPI animations", () => { ) }) - test("Doesn't animate with WAAPI if repeatType is defined as mirror", () => { + test("Doesn't animate with WAAPI if repeatType is defined as mirror", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() expect(ref.current!.animate).not.toBeCalled() }) - test("Doesn't animate with WAAPI if onUpdate is defined", () => { + test("Doesn't animate with WAAPI if onUpdate is defined", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).not.toBeCalled() }) - test("Doesn't animate with WAAPI if external motion value is defined", () => { + test("Doesn't animate with WAAPI if external motion value is defined", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).not.toBeCalled() }) - test("Animates with WAAPI if repeat is defined and we need to generate keyframes", () => { + test("Animates with WAAPI if repeat is defined and we need to generate keyframes", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -868,7 +924,7 @@ describe("WAAPI animations", () => { ) }) - test("Animates with WAAPI if repeat is Infinity and we need to generate keyframes", () => { + test("Animates with WAAPI if repeat is Infinity and we need to generate keyframes", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { diff --git a/packages/framer-motion/src/render/VisualElement.ts b/packages/framer-motion/src/render/VisualElement.ts index e9c4297365..838d82186c 100644 --- a/packages/framer-motion/src/render/VisualElement.ts +++ b/packages/framer-motion/src/render/VisualElement.ts @@ -151,20 +151,21 @@ export abstract class VisualElement< projection?: IProjectionNode ): void - resolveKeyframes( - name: string, + resolveKeyframes = ( keyframes: UnresolvedKeyframes, // We use an onComplete callback here rather than a Promise as a Promise // resolution is a microtask and we want to retain the ability to force // the resolution of keyframes synchronously. - onComplete: (resolvedKeyframes: ResolvedKeyframes) => void - ): KeyframeResolver { + onComplete: (resolvedKeyframes: ResolvedKeyframes) => void, + name: string, + value: MotionValue + ): KeyframeResolver => { return new this.KeyframeResolver( keyframes, onComplete, name, - this.getValue(name), - (target?: any) => this.readValue(name, target) as any + value, + this ) } @@ -854,7 +855,7 @@ export abstract class VisualElement< this.setBaseTarget(key, isMotionValue(value) ? value.get() : value) } - return value + return isMotionValue(value) ? value.get() : value } /** diff --git a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts index a573a579c6..153bbf5eca 100644 --- a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts +++ b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts @@ -10,6 +10,7 @@ import { import { findDimensionValueType } from "./value-types/dimensions" import { KeyframeResolver } from "../utils/KeyframesResolver" import { makeNoneKeyframesAnimatable } from "../html/utils/make-none-animatable" +import { VisualElement } from "../VisualElement" /** * TODO: Use information about whether we are animating via JS or WAAPI to @@ -19,6 +20,7 @@ export class DOMKeyframesResolver< T extends string | number > extends KeyframeResolver { name: string + element: VisualElement private removedTransforms?: [string, string | number][] // private restoreScrollY?: number diff --git a/packages/framer-motion/src/render/utils/KeyframesResolver.ts b/packages/framer-motion/src/render/utils/KeyframesResolver.ts index 9bcc4b8280..b6293fee4c 100644 --- a/packages/framer-motion/src/render/utils/KeyframesResolver.ts +++ b/packages/framer-motion/src/render/utils/KeyframesResolver.ts @@ -60,17 +60,12 @@ export type OnKeyframesResolved = ( resolvedKeyframes: ResolvedKeyframes ) => void -type ReadValue = ( - target?: string | number | null -) => string | number | undefined | null - export class KeyframeResolver { - element: VisualElement + element?: VisualElement name?: string resolvedKeyframes: ResolvedKeyframes | undefined unresolvedKeyframes: UnresolvedKeyframes motionValue?: MotionValue - readValueFromInstance?: ReadValue private onComplete: OnKeyframesResolved constructor( @@ -78,13 +73,13 @@ export class KeyframeResolver { onComplete: OnKeyframesResolved, name?: string, motionValue?: MotionValue, - readValueFromInstance?: ReadValue + element?: VisualElement ) { this.unresolvedKeyframes = unresolvedKeyframes this.onComplete = onComplete this.name = name this.motionValue = motionValue - this.readValueFromInstance = readValueFromInstance + this.element = element toResolve.add(this) @@ -97,12 +92,7 @@ export class KeyframeResolver { needsMeasurement = false readKeyframes() { - const { - unresolvedKeyframes, - readValueFromInstance, - name, - motionValue, - } = this + const { unresolvedKeyframes, name, element, motionValue } = this /** * If a keyframe is null, we hydrate it either by reading it from @@ -121,10 +111,13 @@ export class KeyframeResolver { if (currentValue !== undefined) { unresolvedKeyframes[0] = currentValue - } else if (readValueFromInstance && name) { - const valueAsRead = readValueFromInstance(finalKeyframe) + } else if (element && name) { + const valueAsRead = element.readValue( + name, + finalKeyframe + ) - if (valueAsRead !== undefined) { + if (valueAsRead !== undefined && valueAsRead !== null) { unresolvedKeyframes[0] = valueAsRead } } From ec2adcb0f20ae3c9cc49e5b24e4270a1eac0f62c Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Thu, 25 Jan 2024 11:48:27 +0100 Subject: [PATCH 15/44] Latest --- packages/framer-motion/package.json | 2 +- .../src/animation/__tests__/animate-waapi.test.tsx | 2 +- .../src/animation/__tests__/css-variables.test.tsx | 4 +++- packages/framer-motion/src/animation/animators/js/index.ts | 2 +- .../framer-motion/src/animation/animators/utils/can-skip.ts | 2 +- packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts | 1 + 6 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index 6aa09e1b87..de3f412122 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -47,7 +47,7 @@ "clean": "rm -rf types dist lib", "test": "yarn test-server && yarn test-client", "test-ci": "yarn test", - "test-client": "jest --config jest.config.json --max-workers=2 animate-waapi", + "test-client": "jest --config jest.config.json --max-workers=2 css-variables", "test-server": "", "test-watch": "jest --watch --coverage --coverageReporters=lcov --config jest.config.json", "test-appear": "yarn run collect-appear-tests && start-server-and-test 'pushd ../../; python -m SimpleHTTPServer; popd' http://0.0.0.0:8000 'cypress run -s cypress/integration/appear.chrome.ts --config baseUrl=http://localhost:8000/'", diff --git a/packages/framer-motion/src/animation/__tests__/animate-waapi.test.tsx b/packages/framer-motion/src/animation/__tests__/animate-waapi.test.tsx index 5f584bb693..b9b5ed7ea3 100644 --- a/packages/framer-motion/src/animation/__tests__/animate-waapi.test.tsx +++ b/packages/framer-motion/src/animation/__tests__/animate-waapi.test.tsx @@ -127,7 +127,7 @@ describe("animate() with WAAPI", () => { ) }) - test.only("Returns duration correctly", async () => { + test("Returns duration correctly", async () => { const a = document.createElement("div") const animation = animate( diff --git a/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx b/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx index 8a05c01c50..25e9a75abc 100644 --- a/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx +++ b/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx @@ -50,9 +50,10 @@ describe("css variables", () => { beforeAll(stubGetComputedStyles) afterAll(resetComputedStyles) - test("should animate css color variables", async () => { + test.only("should animate css color variables", async () => { const promise = new Promise((resolve) => { let frameCount = 0 + const Component = () => ( { const results = await promise expect(results).toEqual([ { "--a": "20px", "--color": "rgba(0, 0, 0, 1)" }, + { "--a": "20px", "--color": "rgba(0, 0, 0, 1)" }, ]) }) diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts index c016d5ad1d..e5dcfe3af0 100644 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ b/packages/framer-motion/src/animation/animators/js/index.ts @@ -313,7 +313,7 @@ export function animateValue({ const isAnimationFinished = holdTime === null && (playState === "finished" || (playState === "running" && done)) - + console.log(state.value) if (onUpdate) { onUpdate(state.value) } diff --git a/packages/framer-motion/src/animation/animators/utils/can-skip.ts b/packages/framer-motion/src/animation/animators/utils/can-skip.ts index 6f90cfd32b..f815777deb 100644 --- a/packages/framer-motion/src/animation/animators/utils/can-skip.ts +++ b/packages/framer-motion/src/animation/animators/utils/can-skip.ts @@ -45,7 +45,7 @@ export function canSkipAnimation( const targetKeyframe = keyframes[keyframes.length - 1] const isOriginAnimatable = isAnimatable(originKeyframe, name) const isTargetAnimatable = isAnimatable(targetKeyframe, name) - console.log(keyframes) + warning( isOriginAnimatable === isTargetAnimatable, `You are trying to animate ${name} from "${originKeyframe}" to "${targetKeyframe}". ${originKeyframe} is not an animatable value - to enable this animation set ${originKeyframe} to a value animatable to ${targetKeyframe} via the \`style\` property.` diff --git a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts index 153bbf5eca..f3a9c79e37 100644 --- a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts +++ b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts @@ -42,6 +42,7 @@ export class DOMKeyframesResolver< const keyframe = unresolvedKeyframes[i] if (isCSSVariableToken(keyframe)) { const resolved = getVariableValue(keyframe, element.current) + console.log("this is a css variable resolved as ", keyframe) if (resolved !== undefined) { unresolvedKeyframes[i] = resolved as T } From bdd559ce4062677cda30441399474afb43013e9d Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Thu, 25 Jan 2024 12:13:10 +0100 Subject: [PATCH 16/44] Latest --- .../src/animation/__tests__/css-variables.test.tsx | 5 ++++- .../framer-motion/src/render/dom/DOMKeyframesResolver.ts | 2 +- .../framer-motion/src/render/html/HTMLVisualElement.ts | 7 +++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx b/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx index 25e9a75abc..8fc0c6677f 100644 --- a/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx +++ b/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx @@ -21,7 +21,10 @@ const originalGetComputedStyle = window.getComputedStyle function getComputedStyleStub() { return { - getPropertyValue(variableName: "--from" | "--to" | "--a" | "--color") { + background: fromValue, + getPropertyValue( + variableName: "background" | "--from" | "--to" | "--a" | "--color" + ) { switch (variableName) { case fromName: return fromValue diff --git a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts index f3a9c79e37..97912da59f 100644 --- a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts +++ b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts @@ -42,7 +42,7 @@ export class DOMKeyframesResolver< const keyframe = unresolvedKeyframes[i] if (isCSSVariableToken(keyframe)) { const resolved = getVariableValue(keyframe, element.current) - console.log("this is a css variable resolved as ", keyframe) + if (resolved !== undefined) { unresolvedKeyframes[i] = resolved as T } diff --git a/packages/framer-motion/src/render/html/HTMLVisualElement.ts b/packages/framer-motion/src/render/html/HTMLVisualElement.ts index d9403625f5..c6b4535381 100644 --- a/packages/framer-motion/src/render/html/HTMLVisualElement.ts +++ b/packages/framer-motion/src/render/html/HTMLVisualElement.ts @@ -30,11 +30,18 @@ export class HTMLVisualElement extends DOMVisualElement< instance: HTMLElement, key: string ): string | number | null | undefined { + console.log("read value from instanance") if (transformProps.has(key)) { const defaultType = getDefaultValueType(key) return defaultType ? defaultType.default || 0 : 0 } else { const computedStyle = getComputedStyle(instance) + console.log( + key, + isCSSVariableName(key), + computedStyle.getPropertyValue(key), + computedStyle[key] + ) const value = (isCSSVariableName(key) ? computedStyle.getPropertyValue(key) From 05af0a086c2efefe0071f7bf6d29c880d3c06d45 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Thu, 25 Jan 2024 16:08:34 +0100 Subject: [PATCH 17/44] Latest --- packages/framer-motion/package.json | 2 +- .../__tests__/css-variables.test.tsx | 2 +- .../src/animation/animators/js/index.ts | 40 +++++++------ .../src/animation/animators/utils/can-skip.ts | 26 +++------ .../waapi/create-accelerated-animation.ts | 4 +- .../interfaces/visual-element-target.ts | 5 +- .../src/gestures/__tests__/focus.test.tsx | 13 ++++- .../src/gestures/__tests__/hover.test.tsx | 20 +++++-- .../src/gestures/__tests__/press.test.tsx | 58 ++++++++++++++++--- .../gestures/drag/__tests__/index.test.tsx | 2 + .../motion/__tests__/animate-prop.test.tsx | 11 ++-- .../motion/__tests__/component-svg.test.tsx | 20 +++++-- .../src/motion/__tests__/variant.test.tsx | 25 ++++---- .../src/motion/__tests__/waapi.test.tsx | 7 ++- .../src/render/dom/DOMKeyframesResolver.ts | 4 -- .../src/render/html/HTMLVisualElement.ts | 7 --- .../src/render/utils/KeyframesResolver.ts | 13 +++-- .../framer-motion/src/utils/interpolate.ts | 1 + 18 files changed, 166 insertions(+), 94 deletions(-) diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index de3f412122..412b43e9c5 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -47,7 +47,7 @@ "clean": "rm -rf types dist lib", "test": "yarn test-server && yarn test-client", "test-ci": "yarn test", - "test-client": "jest --config jest.config.json --max-workers=2 css-variables", + "test-client": "jest --config jest.config.json --max-workers=2 variant.test.tsx", "test-server": "", "test-watch": "jest --watch --coverage --coverageReporters=lcov --config jest.config.json", "test-appear": "yarn run collect-appear-tests && start-server-and-test 'pushd ../../; python -m SimpleHTTPServer; popd' http://0.0.0.0:8000 'cypress run -s cypress/integration/appear.chrome.ts --config baseUrl=http://localhost:8000/'", diff --git a/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx b/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx index 8fc0c6677f..8e97587cd3 100644 --- a/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx +++ b/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx @@ -53,7 +53,7 @@ describe("css variables", () => { beforeAll(stubGetComputedStyles) afterAll(resetComputedStyles) - test.only("should animate css color variables", async () => { + test("should animate css color variables", async () => { const promise = new Promise((resolve) => { let frameCount = 0 diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts index e5dcfe3af0..0a06fb3aa7 100644 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ b/packages/framer-motion/src/animation/animators/js/index.ts @@ -120,13 +120,17 @@ export function animateValue({ let animationDriver: DriverControls | undefined + const isInterruptingAnimation = Boolean( + motionValue && motionValue.animation + ) + let initialKeyframe: V const createGenerator = (keyframes: ResolvedKeyframes) => { if ( canSkipAnimation( keyframes, + isInterruptingAnimation, name, - motionValue, type, options.isHandoff, options.velocity @@ -313,7 +317,7 @@ export function animateValue({ const isAnimationFinished = holdTime === null && (playState === "finished" || (playState === "running" && done)) - console.log(state.value) + if (onUpdate) { onUpdate(state.value) } @@ -325,22 +329,6 @@ export function animateValue({ return state } - const keyframeResolver = - element && name && motionValue - ? element.resolveKeyframes( - unresolvedKeyframes, - createGenerator, - name, - motionValue - ) - : new KeyframeResolver( - unresolvedKeyframes, - createGenerator, - name, - motionValue, - element - ) - const stopAnimationDriver = () => { animationDriver && animationDriver.stop() animationDriver = undefined @@ -388,6 +376,22 @@ export function animateValue({ animationDriver.start() } + const keyframeResolver = + element && name && motionValue + ? element.resolveKeyframes( + unresolvedKeyframes, + createGenerator, + name, + motionValue + ) + : new KeyframeResolver( + unresolvedKeyframes, + createGenerator, + name, + motionValue, + element + ) + const controls = { then(resolve: VoidFunction, reject?: VoidFunction) { return currentFinishedPromise.then(resolve, reject) diff --git a/packages/framer-motion/src/animation/animators/utils/can-skip.ts b/packages/framer-motion/src/animation/animators/utils/can-skip.ts index f815777deb..173c54ba2a 100644 --- a/packages/framer-motion/src/animation/animators/utils/can-skip.ts +++ b/packages/framer-motion/src/animation/animators/utils/can-skip.ts @@ -1,8 +1,7 @@ import { ResolvedKeyframes } from "../../../render/utils/KeyframesResolver" import { MotionGlobalConfig } from "../../../utils/GlobalConfig" -import { warning } from "../../../utils/errors" +import { invariant } from "../../../utils/errors" import { instantAnimationState } from "../../../utils/use-instant-transition-state" -import type { MotionValue } from "../../../value" import { isAnimatable } from "../../utils/is-animatable" function hasKeyframesChanged(keyframes: ResolvedKeyframes) { @@ -15,26 +14,17 @@ function hasKeyframesChanged(keyframes: ResolvedKeyframes) { export function canSkipAnimation( keyframes: ResolvedKeyframes, + isInterruptingAnimation: boolean, name?: string, - value?: MotionValue, type?: string, isHandoff?: boolean, velocity?: number ) { - let canSkip = !isHandoff && !hasKeyframesChanged(keyframes) - - if (type === "spring" && velocity) { - canSkip = false - } - - /** - * Temporarily disable skipping animations if there's an animation in - * progress. Better would be to track the current target of a value - * and compare that against valueTarget. - */ - if (value && value.animation) { - canSkip = false - } + let canSkip = + !isHandoff && + !hasKeyframesChanged(keyframes) && + !isInterruptingAnimation && + !(type === "spring" && velocity) /** * Check if we're able to animate between the start and end keyframes, @@ -46,7 +36,7 @@ export function canSkipAnimation( const isOriginAnimatable = isAnimatable(originKeyframe, name) const isTargetAnimatable = isAnimatable(targetKeyframe, name) - warning( + invariant( isOriginAnimatable === isTargetAnimatable, `You are trying to animate ${name} from "${originKeyframe}" to "${targetKeyframe}". ${originKeyframe} is not an animatable value - to enable this animation set ${originKeyframe} to a value animatable to ${targetKeyframe} via the \`style\` property.` ) diff --git a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts index 3878416826..bcab7f7636 100644 --- a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts +++ b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts @@ -112,6 +112,8 @@ export function createAcceleratedAnimation( times, } = options + const isInterruptingAnimation = Boolean(value.animation) + let animation: Animation | undefined const createWaapiAnimation = (keyframes: ResolvedKeyframes) => { const finish = () => { @@ -124,8 +126,8 @@ export function createAcceleratedAnimation( if ( canSkipAnimation( keyframes, + isInterruptingAnimation, valueName, - value, options.type, options.isHandoff, options.velocity 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 4f6b5cd759..9d45b36a88 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts @@ -50,7 +50,10 @@ export function animateTarget( visualElement.animationState.getState()[type] for (const key in target) { - const value = visualElement.getValue(key, null) + const value = visualElement.getValue( + key, + visualElement.latestValues[key] ?? null + ) const valueTarget = target[key] if ( diff --git a/packages/framer-motion/src/gestures/__tests__/focus.test.tsx b/packages/framer-motion/src/gestures/__tests__/focus.test.tsx index 13069f22bc..4f7000330c 100644 --- a/packages/framer-motion/src/gestures/__tests__/focus.test.tsx +++ b/packages/framer-motion/src/gestures/__tests__/focus.test.tsx @@ -1,10 +1,11 @@ import { focus, blur, render } from "../../../jest.setup" import * as React from "react" import { motion, motionValue } from "../../" +import { nextFrame } from "./utils" describe("focus", () => { test("whileFocus applied", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacity = motionValue(1) const ref = React.createRef() const Component = () => ( @@ -25,6 +26,8 @@ describe("focus", () => { focus(container, "myAnchorElement") + await nextFrame() + resolve(opacity.get()) }) @@ -60,7 +63,7 @@ describe("focus", () => { }) test("whileFocus applied if focus-visible selector throws unsupported", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacity = motionValue(1) const ref = React.createRef() const Component = () => ( @@ -87,6 +90,8 @@ describe("focus", () => { focus(container, "myAnchorElement") + await nextFrame() + resolve(opacity.get()) }) @@ -95,7 +100,7 @@ describe("focus", () => { test("whileFocus applied as variant", async () => { const target = 0.5 - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const variant = { hidden: { opacity: target }, } @@ -120,6 +125,8 @@ describe("focus", () => { focus(container, "myAnchorElement") + await nextFrame() + resolve(opacity.get()) }) diff --git a/packages/framer-motion/src/gestures/__tests__/hover.test.tsx b/packages/framer-motion/src/gestures/__tests__/hover.test.tsx index 7d5a398428..6d5bb7707d 100644 --- a/packages/framer-motion/src/gestures/__tests__/hover.test.tsx +++ b/packages/framer-motion/src/gestures/__tests__/hover.test.tsx @@ -8,6 +8,7 @@ import * as React from "react" import { motion } from "../../" import { motionValue } from "../../value" import { frame } from "../../frameloop" +import { nextFrame } from "./utils" describe("hover", () => { test("hover event listeners fire", async () => { @@ -51,7 +52,7 @@ describe("hover", () => { }) test("whileHover applied", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacity = motionValue(1) const Component = () => ( { pointerEnter(container.firstChild as Element) + await nextFrame() + resolve(opacity.get()) }) @@ -74,7 +77,7 @@ describe("hover", () => { test("whileHover applied as variant", async () => { const target = 0.5 - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const variant = { hidden: { opacity: target }, } @@ -93,6 +96,8 @@ describe("hover", () => { pointerEnter(container.firstChild as Element) + await nextFrame() + resolve(opacity.get()) }) @@ -101,7 +106,7 @@ describe("hover", () => { test("whileHover propagates to children", async () => { const target = 0.2 - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const parent = { hidden: { opacity: 0.8 }, } @@ -128,6 +133,8 @@ describe("hover", () => { const { container } = render() pointerEnter(container.firstChild as Element) + + await nextFrame() resolve(opacity.get()) }) @@ -169,7 +176,7 @@ describe("hover", () => { }) test("whileHover only animates values that arent being controlled by a higher-priority gesture ", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const variant = { hovering: { opacity: 0.5, scale: 0.5 }, tapping: { scale: 2 }, @@ -189,9 +196,14 @@ describe("hover", () => { const { container, rerender } = render() rerender() + await nextFrame() pointerDown(container.firstChild as Element) + + await nextFrame() pointerEnter(container.firstChild as Element) + await nextFrame() + resolve([opacity.get(), scale.get()]) }) diff --git a/packages/framer-motion/src/gestures/__tests__/press.test.tsx b/packages/framer-motion/src/gestures/__tests__/press.test.tsx index 77ebe730b2..81adba85e9 100644 --- a/packages/framer-motion/src/gestures/__tests__/press.test.tsx +++ b/packages/framer-motion/src/gestures/__tests__/press.test.tsx @@ -341,7 +341,7 @@ describe("press", () => { }) test("press gesture variant applies and unapplies", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacityHistory: number[] = [] const opacity = motionValue(0.5) const logOpacity = () => opacityHistory.push(opacity.get()) @@ -355,16 +355,18 @@ describe("press", () => { ) const { container } = render() - + await nextFrame() logOpacity() // 0.5 // Trigger mouse down pointerDown(container.firstChild as Element) + await nextFrame() logOpacity() // 1 // Trigger mouse up pointerUp(container.firstChild as Element) + await nextFrame() logOpacity() // 0.5 resolve(opacityHistory) @@ -374,7 +376,7 @@ describe("press", () => { }) test("press gesture variant applies and unapplies via keyboard", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacityHistory: number[] = [] const opacity = motionValue(0.5) const logOpacity = () => opacityHistory.push(opacity.get()) @@ -389,14 +391,17 @@ describe("press", () => { const { container } = render() + await nextFrame() logOpacity() // 0.5 fireEvent.focus(container.firstChild as Element) fireEvent.keyDown(container.firstChild as Element, enterKey) + await nextFrame() logOpacity() // 1 fireEvent.keyUp(container.firstChild as Element, enterKey) + await nextFrame() logOpacity() // 0.5 resolve(opacityHistory) @@ -406,7 +411,7 @@ describe("press", () => { }) test("press gesture variant applies and unapplies via blur cancel", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacityHistory: number[] = [] const opacity = motionValue(0.5) const logOpacity = () => opacityHistory.push(opacity.get()) @@ -421,14 +426,17 @@ describe("press", () => { const { container } = render() + await nextFrame() logOpacity() // 0.5 fireEvent.focus(container.firstChild as Element) fireEvent.keyDown(container.firstChild as Element, enterKey) + await nextFrame() logOpacity() // 1 fireEvent.blur(container.firstChild as Element) + await nextFrame() logOpacity() // 0.5 resolve(opacityHistory) @@ -438,7 +446,7 @@ describe("press", () => { }) test("press gesture variant unapplies children", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacityHistory: number[] = [] const opacity = motionValue(0.5) const logOpacity = () => opacityHistory.push(opacity.get()) @@ -455,15 +463,17 @@ describe("press", () => { const { getByTestId } = render() + await nextFrame() logOpacity() // 0.5 // Trigger mouse down pointerDown(getByTestId("child") as Element) - + await nextFrame() logOpacity() // 1 // Trigger mouse up pointerUp(getByTestId("child") as Element) + await nextFrame() logOpacity() // 0.5 resolve(opacityHistory) }) @@ -472,7 +482,7 @@ describe("press", () => { }) test("press gesture on children returns to parent-defined variant", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacityHistory: number[] = [] const opacity = motionValue(0.5) const logOpacity = () => opacityHistory.push(opacity.get()) @@ -494,15 +504,18 @@ describe("press", () => { const { rerender, getByTestId } = render() rerender() + await nextFrame() logOpacity() // 1 // Trigger mouse down pointerDown(getByTestId("child") as Element) + await nextFrame() logOpacity() // 0.5 // Trigger mouse up pointerUp(getByTestId("child") as Element) + await nextFrame() logOpacity() // 1 resolve(opacityHistory) }) @@ -556,7 +569,7 @@ describe("press", () => { }) test("press gesture variant applies and unapplies with whileHover", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacityHistory: number[] = [] const opacity = motionValue(0.5) const logOpacity = () => opacityHistory.push(opacity.get()) @@ -573,38 +586,47 @@ describe("press", () => { const { container, rerender } = render() rerender() + await nextFrame() logOpacity() // 0.5 // Trigger hover pointerEnter(container.firstChild as Element) + await nextFrame() logOpacity() // 0.75 // Trigger mouse down pointerDown(container.firstChild as Element) + await nextFrame() logOpacity() // 1 // Trigger mouse up pointerUp(container.firstChild as Element) + await nextFrame() logOpacity() // 0.75 // Trigger hover end pointerLeave(container.firstChild as Element) + await nextFrame() logOpacity() // Trigger hover pointerEnter(container.firstChild as Element) + await nextFrame() logOpacity() // Trigger mouse down pointerDown(container.firstChild as Element) + await nextFrame() logOpacity() // Trigger hover end pointerLeave(container.firstChild as Element) + await nextFrame() logOpacity() // Trigger mouse up pointerUp(container.firstChild as Element) + await nextFrame() logOpacity() resolve(opacityHistory) @@ -616,7 +638,7 @@ describe("press", () => { }) test("press gesture variant applies and unapplies as state changes", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacityHistory: number[] = [] const opacity = motionValue(0.5) const logOpacity = () => opacityHistory.push(opacity.get()) @@ -638,40 +660,58 @@ describe("press", () => { ) rerender() + await nextFrame() + logOpacity() // 0.5 // Trigger hover pointerEnter(container.firstChild as Element) + + await nextFrame() logOpacity() // 0.75 // Trigger mouse down pointerDown(container.firstChild as Element) + + await nextFrame() logOpacity() // 1 rerender() rerender() // Trigger mouse up pointerUp(container.firstChild as Element) + + await nextFrame() logOpacity() // 1 // Trigger hover end pointerLeave(container.firstChild as Element) + + await nextFrame() logOpacity() // 1 // Trigger hover pointerEnter(container.firstChild as Element) + + await nextFrame() logOpacity() // 1 // Trigger mouse down pointerDown(container.firstChild as Element) + + await nextFrame() logOpacity() // 1 // Trigger hover end pointerLeave(container.firstChild as Element) + + await nextFrame() logOpacity() // 1 // Trigger mouse up pointerUp(container.firstChild as Element) + + await nextFrame() logOpacity() // 1 resolve(opacityHistory) diff --git a/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx b/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx index f5826f416c..751a1d8192 100644 --- a/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx +++ b/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx @@ -496,6 +496,8 @@ describe("dragging", () => { expect(opacity.get()).toBe(0.5) await pointer.to(10, 200) pointer.end() + + await nextFrame() expect(opacity.get()).toBe(0) }) diff --git a/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx b/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx index 0166be451d..2146323d6a 100644 --- a/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx @@ -326,7 +326,7 @@ describe("animate prop as object", () => { return expect(promise).resolves.toBe(false) }) test("does animate different keyframes", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { let isAnimating = false const Component = () => ( { const { rerender } = render() rerender() - frame.postRender(() => { - frame.postRender(() => resolve(isAnimating)) - }) + await nextFrame() + await nextFrame() + + resolve(isAnimating) }) return expect(promise).resolves.toBe(true) @@ -826,7 +827,7 @@ describe("animate prop as object", () => { rerender() }) - return expect(promise).resolves.toBe("rgba(0, 0, 0, 1)") + return expect(promise).resolves.toBe("#000") }) test("forces an animation to fallback if has been set to `null`", async () => { diff --git a/packages/framer-motion/src/motion/__tests__/component-svg.test.tsx b/packages/framer-motion/src/motion/__tests__/component-svg.test.tsx index 7e6b975fde..24fbff1734 100644 --- a/packages/framer-motion/src/motion/__tests__/component-svg.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/component-svg.test.tsx @@ -1,7 +1,7 @@ import { render } from "../../../jest.setup" import { motion, motionValue, useMotionValue, useTransform } from "../../" import * as React from "react" -import { nextMicrotask } from "../../gestures/__tests__/utils" +import { nextFrame } from "../../gestures/__tests__/utils" describe("SVG", () => { test("doesn't add translateZ", () => { @@ -24,7 +24,7 @@ describe("SVG", () => { render() }) - test("recognises MotionValues in attributes", () => { + test("recognises MotionValues in attributes", async () => { let r = motionValue(0) let fill = motionValue("#000") @@ -49,6 +49,8 @@ describe("SVG", () => { const { rerender } = render() rerender() + await nextFrame() + expect(r.get()).toBe(100) expect(fill.get()).toBe("rgba(255, 0, 0, 1)") }) @@ -65,11 +67,14 @@ describe("SVG", () => { render() }) - test("doesn't calculate transformOrigin for elements", () => { + test("doesn't calculate transformOrigin for elements", async () => { const Component = () => { return } const { container } = render() + + await nextFrame() + expect(container.firstChild as Element).not.toHaveStyle( "transform-origin: 0px 0px" ) @@ -93,7 +98,7 @@ describe("SVG", () => { render() }) - test("doesn't read viewBox as '0 0 0 0'", () => { + test("doesn't read viewBox as '0 0 0 0'", async () => { const Component = () => { return ( { ) } const { container } = render() + + await nextFrame() + expect(container.firstChild as Element).toHaveAttribute( "viewBox", "0 0 100 100" @@ -121,7 +129,9 @@ describe("SVG", () => { ) } const { container } = render() - await nextMicrotask() + + await nextFrame() + expect(container.firstChild as Element).toHaveAttribute( "viewBox", "100 100 200 200" diff --git a/packages/framer-motion/src/motion/__tests__/variant.test.tsx b/packages/framer-motion/src/motion/__tests__/variant.test.tsx index 78fcc1b925..31d2223110 100644 --- a/packages/framer-motion/src/motion/__tests__/variant.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/variant.test.tsx @@ -508,7 +508,7 @@ describe("animate prop as variant", () => { }) test("nested controlled variants switch correctly", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const parentOpacity = motionValue(0.2) const childOpacity = motionValue(0.1) @@ -539,16 +539,17 @@ describe("animate prop as variant", () => { } const { rerender } = render() - setTimeout(() => { - expect(parentOpacity.get()).toBe(0.4) - expect(childOpacity.get()).toBe(0.6) - rerender() + await nextFrame() + + expect(parentOpacity.get()).toBe(0.4) + expect(childOpacity.get()).toBe(0.6) + + rerender() - setTimeout(() => { - resolve([parentOpacity.get(), childOpacity.get()]) - }, 0) - }, 0) + await nextFrame() + + resolve([parentOpacity.get(), childOpacity.get()]) }) return expect(promise).resolves.toEqual([0.3, 0.5]) @@ -847,7 +848,7 @@ describe("animate prop as variant", () => { }).not.toThrowError() }) - test("new child items animate from initial to animate", () => { + test("new child items animate from initial to animate", async () => { const x = motionValue(0) const Component = ({ length }: { length: number }) => { const variants: Variants = { @@ -878,6 +879,8 @@ describe("animate prop as variant", () => { rerender() rerender() + await nextFrame() + expect(x.get()).toBe(100) }) @@ -1111,7 +1114,7 @@ describe("animate prop as variant", () => { expect(inner).toHaveStyle("background-color: rgb(0, 150,150)") }) - test("child onAnimationComplete triggers from parent animations", async () => { + test.only("child onAnimationComplete triggers from parent animations", async () => { const variants: Variants = { hidden: { opacity: 0, x: -100, transition: { type: false } }, visible: { opacity: 1, x: 100, transition: { type: false } }, diff --git a/packages/framer-motion/src/motion/__tests__/waapi.test.tsx b/packages/framer-motion/src/motion/__tests__/waapi.test.tsx index e29d0a61ab..bc64a060dc 100644 --- a/packages/framer-motion/src/motion/__tests__/waapi.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/waapi.test.tsx @@ -10,6 +10,7 @@ import * as React from "react" import { createRef } from "react" import { nextFrame } from "../../gestures/__tests__/utils" import "../../animation/animators/waapi/__tests__/setup" +import { act } from "react-dom/test-utils" describe("WAAPI animations", () => { test("opacity animates with WAAPI at default settings", async () => { @@ -290,8 +291,8 @@ describe("WAAPI animations", () => { setIsHovered(true)} - onHoverEnd={() => setIsHovered(false)} + onHoverStart={() => act(() => setIsHovered(true))} + onHoverEnd={() => act(() => setIsHovered(false))} > { pointerLeave(container.firstChild as Element) await nextFrame() rerender() + await nextFrame() expect(ref.current!.animate).toBeCalledTimes(2) }) @@ -335,6 +337,7 @@ describe("WAAPI animations", () => { const { container, rerender } = render() pointerDown(container.firstChild as Element) + await nextFrame() await nextFrame() pointerUp(container.firstChild as Element) diff --git a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts index 97912da59f..ff1c66ea4c 100644 --- a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts +++ b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts @@ -12,10 +12,6 @@ import { KeyframeResolver } from "../utils/KeyframesResolver" import { makeNoneKeyframesAnimatable } from "../html/utils/make-none-animatable" import { VisualElement } from "../VisualElement" -/** - * TODO: Use information about whether we are animating via JS or WAAPI to - * decide whether to we need to resolve CSS vars / do type conversion here. - */ export class DOMKeyframesResolver< T extends string | number > extends KeyframeResolver { diff --git a/packages/framer-motion/src/render/html/HTMLVisualElement.ts b/packages/framer-motion/src/render/html/HTMLVisualElement.ts index c6b4535381..d9403625f5 100644 --- a/packages/framer-motion/src/render/html/HTMLVisualElement.ts +++ b/packages/framer-motion/src/render/html/HTMLVisualElement.ts @@ -30,18 +30,11 @@ export class HTMLVisualElement extends DOMVisualElement< instance: HTMLElement, key: string ): string | number | null | undefined { - console.log("read value from instanance") if (transformProps.has(key)) { const defaultType = getDefaultValueType(key) return defaultType ? defaultType.default || 0 : 0 } else { const computedStyle = getComputedStyle(instance) - console.log( - key, - isCSSVariableName(key), - computedStyle.getPropertyValue(key), - computedStyle[key] - ) const value = (isCSSVariableName(key) ? computedStyle.getPropertyValue(key) diff --git a/packages/framer-motion/src/render/utils/KeyframesResolver.ts b/packages/framer-motion/src/render/utils/KeyframesResolver.ts index b6293fee4c..fb3800582e 100644 --- a/packages/framer-motion/src/render/utils/KeyframesResolver.ts +++ b/packages/framer-motion/src/render/utils/KeyframesResolver.ts @@ -81,11 +81,16 @@ export class KeyframeResolver { this.motionValue = motionValue this.element = element - toResolve.add(this) + if (this.element) { + toResolve.add(this) - if (!isScheduled) { - isScheduled = true - frame.read(readAllKeyframes) + if (!isScheduled) { + isScheduled = true + frame.read(readAllKeyframes) + } + } else { + this.readKeyframes() + this.complete() } } diff --git a/packages/framer-motion/src/utils/interpolate.ts b/packages/framer-motion/src/utils/interpolate.ts index 1b47a58d95..54b48dbce6 100644 --- a/packages/framer-motion/src/utils/interpolate.ts +++ b/packages/framer-motion/src/utils/interpolate.ts @@ -74,6 +74,7 @@ export function interpolate( * that returns the output. */ if (inputLength === 1) return () => output[0] + if (inputLength === 2 && input[0] === input[1]) return () => output[1] // If input runs highest -> lowest, reverse both arrays if (input[0] > input[inputLength - 1]) { From 9ba2544e7585f1e65a2dbbe869a6f4e7e1fd4928 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Thu, 25 Jan 2024 17:26:53 +0100 Subject: [PATCH 18/44] Latest test fixes --- CHANGELOG.md | 7 +++++ packages/framer-motion/package.json | 2 +- .../src/animation/animators/js/index.ts | 6 +++- .../src/animation/animators/utils/can-skip.ts | 12 ++++---- .../waapi/create-accelerated-animation.ts | 5 ++++ .../drag/VisualElementDragControls.ts | 9 ++---- packages/framer-motion/src/gestures/hover.ts | 3 +- .../framer-motion/src/gestures/pan/index.ts | 7 ++--- packages/framer-motion/src/gestures/press.ts | 29 +++++++------------ .../src/motion/__tests__/variant.test.tsx | 2 +- .../src/motion/__tests__/waapi.test.tsx | 2 ++ .../src/render/dom/DOMKeyframesResolver.ts | 8 ++--- .../src/render/utils/KeyframesResolver.ts | 6 ++-- 13 files changed, 52 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fe7ea4add..c2a70466fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ Framer Motion adheres to [Semantic Versioning](http://semver.org/). Undocumented APIs should be considered internal and may change without warning. +## [11.0.11] 2024-03-12 + +### Changed + +- Keyframes now resolved asynchronously. +- External event handlers now fired synchronously. + ## [11.0.10] 2024-03-12 ### Fixed diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index 412b43e9c5..d0708362d4 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -47,7 +47,7 @@ "clean": "rm -rf types dist lib", "test": "yarn test-server && yarn test-client", "test-ci": "yarn test", - "test-client": "jest --config jest.config.json --max-workers=2 variant.test.tsx", + "test-client": "jest --config jest.config.json --max-workers=2 motion/__tests__/waapi.test.ts", "test-server": "", "test-watch": "jest --watch --coverage --coverageReporters=lcov --config jest.config.json", "test-appear": "yarn run collect-appear-tests && start-server-and-test 'pushd ../../; python -m SimpleHTTPServer; popd' http://0.0.0.0:8000 'cypress run -s cypress/integration/appear.chrome.ts --config baseUrl=http://localhost:8000/'", diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts index 0a06fb3aa7..49af5ce0ff 100644 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ b/packages/framer-motion/src/animation/animators/js/index.ts @@ -126,6 +126,7 @@ export function animateValue({ let initialKeyframe: V const createGenerator = (keyframes: ResolvedKeyframes) => { + console.log("create generator") if ( canSkipAnimation( keyframes, @@ -136,6 +137,7 @@ export function animateValue({ options.velocity ) ) { + console.log("skipping", keyframes) if (instantAnimationState.current || !delay) { if (onUpdate) { onUpdate( @@ -204,6 +206,7 @@ export function animateValue({ } const tick = (timestamp: number) => { + console.log({ startTime, hasGenerator: !!generator }) if (startTime === null || !generator) return /** @@ -317,7 +320,7 @@ export function animateValue({ const isAnimationFinished = holdTime === null && (playState === "finished" || (playState === "running" && done)) - + console.log("Updating with", state.value) if (onUpdate) { onUpdate(state.value) } @@ -455,6 +458,7 @@ export function animateValue({ }, sample: (elapsed: number) => { startTime = 0 + console.log("sample") return tick(elapsed)! }, } diff --git a/packages/framer-motion/src/animation/animators/utils/can-skip.ts b/packages/framer-motion/src/animation/animators/utils/can-skip.ts index 173c54ba2a..08ae3d5798 100644 --- a/packages/framer-motion/src/animation/animators/utils/can-skip.ts +++ b/packages/framer-motion/src/animation/animators/utils/can-skip.ts @@ -1,6 +1,6 @@ import { ResolvedKeyframes } from "../../../render/utils/KeyframesResolver" import { MotionGlobalConfig } from "../../../utils/GlobalConfig" -import { invariant } from "../../../utils/errors" +import { warning } from "../../../utils/errors" import { instantAnimationState } from "../../../utils/use-instant-transition-state" import { isAnimatable } from "../../utils/is-animatable" @@ -36,10 +36,12 @@ export function canSkipAnimation( const isOriginAnimatable = isAnimatable(originKeyframe, name) const isTargetAnimatable = isAnimatable(targetKeyframe, name) - invariant( - isOriginAnimatable === isTargetAnimatable, - `You are trying to animate ${name} from "${originKeyframe}" to "${targetKeyframe}". ${originKeyframe} is not an animatable value - to enable this animation set ${originKeyframe} to a value animatable to ${targetKeyframe} via the \`style\` property.` - ) + if (originKeyframe !== null) { + warning( + isOriginAnimatable === isTargetAnimatable, + `You are trying to animate ${name} from "${originKeyframe}" to "${targetKeyframe}". ${originKeyframe} is not an animatable value - to enable this animation set ${originKeyframe} to a value animatable to ${targetKeyframe} via the \`style\` property.` + ) + } // Always skip if any of these are true if ( diff --git a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts index bcab7f7636..245ebcc9e4 100644 --- a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts +++ b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts @@ -113,9 +113,11 @@ export function createAcceleratedAnimation( } = options const isInterruptingAnimation = Boolean(value.animation) + let resolvedKeyframes: ResolvedKeyframes let animation: Animation | undefined const createWaapiAnimation = (keyframes: ResolvedKeyframes) => { + resolvedKeyframes = keyframes const finish = () => { if (pendingCancel) return value.set(getFinalKeyframe(keyframes, options)) @@ -147,6 +149,7 @@ export function createAcceleratedAnimation( if (requiresPregeneratedKeyframes(valueName, options)) { const sampleAnimation = animateValue({ ...options, + keyframes: resolvedKeyframes, repeat: 0, delay: 0, }) @@ -218,6 +221,7 @@ export function createAcceleratedAnimation( updateFinishedPromise() } + console.log("resolving", options.keyframes, name) const resolver = element && name && motionValue ? element.resolveKeyframes( @@ -301,6 +305,7 @@ export function createAcceleratedAnimation( if (currentTime) { const sampleAnimation = animateValue({ ...options, + keyframes: resolvedKeyframes, autoplay: false, }) diff --git a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts index 76395a108a..66d8c9e8ca 100644 --- a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts +++ b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts @@ -31,7 +31,6 @@ import { calcLength } from "../../projection/geometry/delta-calc" import { mixNumber } from "../../utils/mix/number" import { percent } from "../../value/types/numbers/units" import { animateMotionValue } from "../../animation/interfaces/motion-value" -import { frame } from "../../frameloop" import { getContextWindow } from "../../utils/get-context-window" export const elementDragControls = new WeakMap< @@ -153,9 +152,7 @@ export class VisualElementDragControls { }) // Fire onDragStart event - if (onDragStart) { - frame.update(() => onDragStart(event, info), false, true) - } + if (onDragStart) onDragStart(event, info) const { animationState } = this.visualElement animationState && animationState.setActive("whileDrag", true) @@ -243,9 +240,7 @@ export class VisualElementDragControls { this.startAnimation(velocity) const { onDragEnd } = this.getProps() - if (onDragEnd) { - frame.update(() => onDragEnd(event, info)) - } + if (onDragEnd) onDragEnd(event, info) } private cancel() { diff --git a/packages/framer-motion/src/gestures/hover.ts b/packages/framer-motion/src/gestures/hover.ts index d2a5f5aa17..4d153f9193 100644 --- a/packages/framer-motion/src/gestures/hover.ts +++ b/packages/framer-motion/src/gestures/hover.ts @@ -4,7 +4,6 @@ import { isDragActive } from "./drag/utils/lock" import { EventInfo } from "../events/types" import type { VisualElement } from "../render/VisualElement" import { Feature } from "../motion/features/Feature" -import { frame } from "../frameloop" function addHoverEvent(node: VisualElement, isActive: boolean) { const eventName = "pointer" + (isActive ? "enter" : "leave") @@ -20,7 +19,7 @@ function addHoverEvent(node: VisualElement, isActive: boolean) { } if (props[callbackName]) { - frame.update(() => props[callbackName](event, info)) + props[callbackName](event, info) } } diff --git a/packages/framer-motion/src/gestures/pan/index.ts b/packages/framer-motion/src/gestures/pan/index.ts index 68e79dd904..8a75c563bf 100644 --- a/packages/framer-motion/src/gestures/pan/index.ts +++ b/packages/framer-motion/src/gestures/pan/index.ts @@ -2,14 +2,13 @@ import { PanInfo, PanSession } from "./PanSession" import { addPointerEvent } from "../../events/add-pointer-event" import { Feature } from "../../motion/features/Feature" import { noop } from "../../utils/noop" -import { frame } from "../../frameloop" import { getContextWindow } from "../../utils/get-context-window" type PanEventHandler = (event: PointerEvent, info: PanInfo) => void const asyncHandler = (handler?: PanEventHandler) => (event: PointerEvent, info: PanInfo) => { if (handler) { - frame.update(() => handler(event, info)) + handler(event, info) } } @@ -39,9 +38,7 @@ export class PanGesture extends Feature { onMove: onPan, onEnd: (event: PointerEvent, info: PanInfo) => { delete this.session - if (onPanEnd) { - frame.update(() => onPanEnd(event, info)) - } + if (onPanEnd) onPanEnd(event, info) }, } } diff --git a/packages/framer-motion/src/gestures/press.ts b/packages/framer-motion/src/gestures/press.ts index 5561fb0743..f0e86b9276 100644 --- a/packages/framer-motion/src/gestures/press.ts +++ b/packages/framer-motion/src/gestures/press.ts @@ -10,7 +10,6 @@ import { pipe } from "../utils/pipe" import { isDragActive } from "./drag/utils/lock" import { isNodeOrChild } from "./utils/is-node-or-child" import { noop } from "../utils/noop" -import { frame } from "../frameloop" function fireSyntheticPointerEvent( name: string, @@ -41,7 +40,7 @@ export class PressGesture extends Feature { } if (onTapStart) { - frame.update(() => onTapStart(event, info)) + onTapStart(event, info) } } @@ -76,16 +75,14 @@ export class PressGesture extends Feature { const { onTap, onTapCancel, globalTapTarget } = this.node.getProps() - frame.update(() => { - /** - * We only count this as a tap gesture if the event.target is the same - * as, or a child of, this component's element - */ - !globalTapTarget && - !isNodeOrChild(this.node.current, endEvent.target as Element) - ? onTapCancel && onTapCancel(endEvent, endInfo) - : onTap && onTap(endEvent, endInfo) - }) + /** + * We only count this as a tap gesture if the event.target is the same + * as, or a child of, this component's element + */ + !globalTapTarget && + !isNodeOrChild(this.node.current, endEvent.target as Element) + ? onTapCancel && onTapCancel(endEvent, endInfo) + : onTap && onTap(endEvent, endInfo) } const removePointerUpListener = addPointerEvent( @@ -115,9 +112,7 @@ export class PressGesture extends Feature { if (!this.checkPressEnd()) return const { onTapCancel } = this.node.getProps() - if (onTapCancel) { - frame.update(() => onTapCancel(event, info)) - } + if (onTapCancel) onTapCancel(event, info) } private startAccessiblePress = () => { @@ -129,9 +124,7 @@ export class PressGesture extends Feature { fireSyntheticPointerEvent("up", (event, info) => { const { onTap } = this.node.getProps() - if (onTap) { - frame.update(() => onTap(event, info)) - } + if (onTap) onTap(event, info) }) } diff --git a/packages/framer-motion/src/motion/__tests__/variant.test.tsx b/packages/framer-motion/src/motion/__tests__/variant.test.tsx index 31d2223110..fc32cd3f57 100644 --- a/packages/framer-motion/src/motion/__tests__/variant.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/variant.test.tsx @@ -1114,7 +1114,7 @@ describe("animate prop as variant", () => { expect(inner).toHaveStyle("background-color: rgb(0, 150,150)") }) - test.only("child onAnimationComplete triggers from parent animations", async () => { + test("child onAnimationComplete triggers from parent animations", async () => { const variants: Variants = { hidden: { opacity: 0, x: -100, transition: { type: false } }, visible: { opacity: 1, x: 100, transition: { type: false } }, diff --git a/packages/framer-motion/src/motion/__tests__/waapi.test.tsx b/packages/framer-motion/src/motion/__tests__/waapi.test.tsx index bc64a060dc..11a62afa3a 100644 --- a/packages/framer-motion/src/motion/__tests__/waapi.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/waapi.test.tsx @@ -298,6 +298,7 @@ describe("WAAPI animations", () => { ref={ref} style={{ opacity: 0.5 }} variants={{ hover: { opacity: 1 } }} + transition={{ type: false }} /> ) @@ -305,6 +306,7 @@ describe("WAAPI animations", () => { const { container, rerender } = render() pointerEnter(container.firstChild as Element) + await nextFrame() await nextFrame() pointerLeave(container.firstChild as Element) await nextFrame() diff --git a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts index ff1c66ea4c..0e546cefff 100644 --- a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts +++ b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts @@ -19,7 +19,7 @@ export class DOMKeyframesResolver< element: VisualElement private removedTransforms?: [string, string | number][] - // private restoreScrollY?: number + restoreScrollY?: number private measuredOrigin?: string | number readKeyframes() { @@ -122,9 +122,9 @@ export class DOMKeyframesResolver< if (!element.current) return - // if (name === "height") { - // this.restoreScrollY = window.pageYOffset - // } + if (name === "height") { + this.restoreScrollY = window.pageYOffset + } this.measuredOrigin = positionalValues[name]( element.measureViewportBox(), diff --git a/packages/framer-motion/src/render/utils/KeyframesResolver.ts b/packages/framer-motion/src/render/utils/KeyframesResolver.ts index fb3800582e..bdab85b39f 100644 --- a/packages/framer-motion/src/render/utils/KeyframesResolver.ts +++ b/packages/framer-motion/src/render/utils/KeyframesResolver.ts @@ -75,7 +75,7 @@ export class KeyframeResolver { motionValue?: MotionValue, element?: VisualElement ) { - this.unresolvedKeyframes = unresolvedKeyframes + this.unresolvedKeyframes = [...unresolvedKeyframes] this.onComplete = onComplete this.name = name this.motionValue = motionValue @@ -89,6 +89,7 @@ export class KeyframeResolver { frame.read(readAllKeyframes) } } else { + console.log("synchronus resolition") this.readKeyframes() this.complete() } @@ -110,7 +111,7 @@ export class KeyframeResolver { */ if (i === 0) { const currentValue = motionValue?.get() - + console.log({ currentValue }) const finalKeyframe = unresolvedKeyframes[unresolvedKeyframes.length - 1] @@ -147,6 +148,7 @@ export class KeyframeResolver { measureEndState() {} complete() { + console.log("resolved keyframes", this.unresolvedKeyframes) this.onComplete(this.unresolvedKeyframes as ResolvedKeyframes) toResolve.delete(this) } From a91bd00e0b1e36e698a69ef9a1a67c17e79618a9 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 26 Jan 2024 12:05:04 +0100 Subject: [PATCH 19/44] Becnhmark changes --- dev/benchmarks/cold-start-anime.html | 45 ++++++++++++++----- .../src/animation/animators/js/index.ts | 7 +-- .../waapi/create-accelerated-animation.ts | 1 - .../src/render/dom/DOMKeyframesResolver.ts | 8 ++-- .../src/render/utils/KeyframesResolver.ts | 4 +- packages/framer-motion/src/value/index.ts | 4 +- 6 files changed, 44 insertions(+), 25 deletions(-) diff --git a/dev/benchmarks/cold-start-anime.html b/dev/benchmarks/cold-start-anime.html index 7bed6160b9..d2c7858b0a 100644 --- a/dev/benchmarks/cold-start-anime.html +++ b/dev/benchmarks/cold-start-anime.html @@ -28,7 +28,7 @@ } .box { - width: 10px; + width: 10%; height: 100px; background-color: #fff; } @@ -36,7 +36,7 @@
- + diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts index 49af5ce0ff..9f07ddefbf 100644 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ b/packages/framer-motion/src/animation/animators/js/index.ts @@ -126,7 +126,6 @@ export function animateValue({ let initialKeyframe: V const createGenerator = (keyframes: ResolvedKeyframes) => { - console.log("create generator") if ( canSkipAnimation( keyframes, @@ -137,7 +136,6 @@ export function animateValue({ options.velocity ) ) { - console.log("skipping", keyframes) if (instantAnimationState.current || !delay) { if (onUpdate) { onUpdate( @@ -174,7 +172,6 @@ export function animateValue({ } generator = generatorFactory({ ...options, keyframes }) - if (repeatType === "mirror") { mirroredGenerator = generatorFactory({ ...options, @@ -206,7 +203,6 @@ export function animateValue({ } const tick = (timestamp: number) => { - console.log({ startTime, hasGenerator: !!generator }) if (startTime === null || !generator) return /** @@ -320,7 +316,7 @@ export function animateValue({ const isAnimationFinished = holdTime === null && (playState === "finished" || (playState === "running" && done)) - console.log("Updating with", state.value) + if (onUpdate) { onUpdate(state.value) } @@ -458,7 +454,6 @@ export function animateValue({ }, sample: (elapsed: number) => { startTime = 0 - console.log("sample") return tick(elapsed)! }, } diff --git a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts index 245ebcc9e4..5f12701d3c 100644 --- a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts +++ b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts @@ -221,7 +221,6 @@ export function createAcceleratedAnimation( updateFinishedPromise() } - console.log("resolving", options.keyframes, name) const resolver = element && name && motionValue ? element.resolveKeyframes( diff --git a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts index 0e546cefff..8045c74f2a 100644 --- a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts +++ b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts @@ -114,11 +114,11 @@ export class DOMKeyframesResolver< // this.resolvedFinalKeyframe = finalKeyframe // } - element.getValue(name, finalKeyframe).jump(finalKeyframe) + element.getValue(name, finalKeyframe).jump(finalKeyframe, false) } measureInitialState() { - const { element, name } = this + const { element, unresolvedKeyframes, name } = this if (!element.current) return @@ -130,6 +130,8 @@ export class DOMKeyframesResolver< element.measureViewportBox(), window.getComputedStyle(element.current) ) + + unresolvedKeyframes[0] = this.measuredOrigin } renderEndStyles() { @@ -142,7 +144,7 @@ export class DOMKeyframesResolver< if (!element.current) return const value = element.getValue(name) - value && value.jump(this.measuredOrigin) + value && value.jump(this.measuredOrigin, false) unresolvedKeyframes[unresolvedKeyframes.length - 1] = positionalValues[ name diff --git a/packages/framer-motion/src/render/utils/KeyframesResolver.ts b/packages/framer-motion/src/render/utils/KeyframesResolver.ts index bdab85b39f..3f4b6ac918 100644 --- a/packages/framer-motion/src/render/utils/KeyframesResolver.ts +++ b/packages/framer-motion/src/render/utils/KeyframesResolver.ts @@ -89,7 +89,6 @@ export class KeyframeResolver { frame.read(readAllKeyframes) } } else { - console.log("synchronus resolition") this.readKeyframes() this.complete() } @@ -111,7 +110,7 @@ export class KeyframeResolver { */ if (i === 0) { const currentValue = motionValue?.get() - console.log({ currentValue }) + const finalKeyframe = unresolvedKeyframes[unresolvedKeyframes.length - 1] @@ -148,7 +147,6 @@ export class KeyframeResolver { measureEndState() {} complete() { - console.log("resolved keyframes", this.unresolvedKeyframes) this.onComplete(this.unresolvedKeyframes as ResolvedKeyframes) toResolve.delete(this) } diff --git a/packages/framer-motion/src/value/index.ts b/packages/framer-motion/src/value/index.ts index 7c79c3cee4..335866bf10 100644 --- a/packages/framer-motion/src/value/index.ts +++ b/packages/framer-motion/src/value/index.ts @@ -285,11 +285,11 @@ export class MotionValue { * Set the state of the `MotionValue`, stopping any active animations, * effects, and resets velocity to `0`. */ - jump(v: V) { + jump(v: V, endAnimation = true) { this.updateAndNotify(v) this.prev = v this.prevUpdatedAt = this.prevFrameValue = undefined - this.stop() + endAnimation && this.stop() if (this.stopPassiveEffect) this.stopPassiveEffect() } From 19f69bce975d18e0a496b79aab74403c809c0fd9 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 26 Jan 2024 13:57:59 +0100 Subject: [PATCH 20/44] Latest --- dev/benchmarks/cold-start-anime.html | 17 ++-- dev/benchmarks/cold-start-waapi.html | 55 +++++++++--- dev/benchmarks/warm-start-framer-motion.html | 90 +++++++++++++++++++ dev/benchmarks/warm-start-gsap.html | 84 +++++++++++++++++ .../src/motion/__tests__/waapi.test.tsx | 3 +- 5 files changed, 224 insertions(+), 25 deletions(-) create mode 100644 dev/benchmarks/warm-start-framer-motion.html create mode 100644 dev/benchmarks/warm-start-gsap.html diff --git a/dev/benchmarks/cold-start-anime.html b/dev/benchmarks/cold-start-anime.html index d2c7858b0a..c8ca123ebf 100644 --- a/dev/benchmarks/cold-start-anime.html +++ b/dev/benchmarks/cold-start-anime.html @@ -47,34 +47,27 @@ document.querySelector(".container").innerHTML = html const boxes = document.querySelectorAll(".box") - anime({ - targets: boxes[0], - rotate: Math.random() * 360, - backgroundColor: "#f00", - width: Math.random() * 100 + "%", - duration: 100, - easing: "linear", - }) - setTimeout(() => { + // Cold start (read from DOM) boxes.forEach((box) => anime({ targets: box, rotate: Math.random() * 360, backgroundColor: "#f00", width: Math.random() * 100 + "%", + translateX: 5, duration: 1000, easing: "linear", }) ) setTimeout(() => { + // Unit conversion boxes.forEach((box) => anime({ targets: box, - rotate: Math.random() * 360, - backgroundColor: "#fff", - width: Math.random() * 100 + "%", + width: Math.random() * 100 + "px", + translateX: "50%", duration: 1000, easing: "linear", }) diff --git a/dev/benchmarks/cold-start-waapi.html b/dev/benchmarks/cold-start-waapi.html index b0788d8427..865ba6d278 100644 --- a/dev/benchmarks/cold-start-waapi.html +++ b/dev/benchmarks/cold-start-waapi.html @@ -44,20 +44,51 @@ html += `
` } document.querySelector(".container").innerHTML = html + const boxes = document.querySelectorAll(".box") - boxes.forEach((box) => - box.animate( - { - rotate: "360deg", - backgroundColor: "#f00", - width: "100%", - }, - { - duration: 1000, - fill: "forwards", + setTimeout(() => { + boxes.forEach((box) => { + const animation = box.animate( + { + rotate: Math.random() * 360 + "deg", + backgroundColor: "#f00", + width: Math.random() * 100 + "%", + translate: "5px 0", + }, + { + duration: 1000, + fill: "both", + } + ) + animation.onfinish = () => { + requestAnimationFrame(() => { + animation.commitStyles() + animation.cancel() + }) } - ) - ) + }) + + setTimeout(() => { + boxes.forEach((box) => { + const animation = box.animate( + { + width: Math.random() * 100 + "px", + translate: "50% 0", + }, + { + duration: 1000, + fill: "both", + } + ) + animation.onfinish = () => { + requestAnimationFrame(() => { + animation.commitStyles() + animation.cancel() + }) + } + }) + }, 1500) + }, 1000) diff --git a/dev/benchmarks/warm-start-framer-motion.html b/dev/benchmarks/warm-start-framer-motion.html new file mode 100644 index 0000000000..810f1e7098 --- /dev/null +++ b/dev/benchmarks/warm-start-framer-motion.html @@ -0,0 +1,90 @@ + + + + + + +
+ + + + + diff --git a/dev/benchmarks/warm-start-gsap.html b/dev/benchmarks/warm-start-gsap.html new file mode 100644 index 0000000000..114507e7dd --- /dev/null +++ b/dev/benchmarks/warm-start-gsap.html @@ -0,0 +1,84 @@ + + + + + + +
+ + + + diff --git a/packages/framer-motion/src/motion/__tests__/waapi.test.tsx b/packages/framer-motion/src/motion/__tests__/waapi.test.tsx index 11a62afa3a..dc938d808a 100644 --- a/packages/framer-motion/src/motion/__tests__/waapi.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/waapi.test.tsx @@ -166,7 +166,8 @@ describe("WAAPI animations", () => { ) }) - test.skip("backgroundColor animates with WAAPI at default settings", () => { + // backgroundColor currently disabled for performance reasons + test.skip("backgroundColor animates with WAAPI at default settings", async () => { const ref = createRef() const Component = () => ( Date: Fri, 26 Jan 2024 16:42:49 +0100 Subject: [PATCH 21/44] Latest --- packages/framer-motion/package.json | 2 +- packages/framer-motion/src/animation/interfaces/motion-value.ts | 2 +- .../src/animation/interfaces/visual-element-target.ts | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index d0708362d4..cc625a0217 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -47,7 +47,7 @@ "clean": "rm -rf types dist lib", "test": "yarn test-server && yarn test-client", "test-ci": "yarn test", - "test-client": "jest --config jest.config.json --max-workers=2 motion/__tests__/waapi.test.ts", + "test-client": "jest --config jest.config.json --max-workers=2", "test-server": "", "test-watch": "jest --watch --coverage --coverageReporters=lcov --config jest.config.json", "test-appear": "yarn run collect-appear-tests && start-server-and-test 'pushd ../../; python -m SimpleHTTPServer; popd' http://0.0.0.0:8000 'cypress run -s cypress/integration/appear.chrome.ts --config baseUrl=http://localhost:8000/'", diff --git a/packages/framer-motion/src/animation/interfaces/motion-value.ts b/packages/framer-motion/src/animation/interfaces/motion-value.ts index d93941bf21..004b479855 100644 --- a/packages/framer-motion/src/animation/interfaces/motion-value.ts +++ b/packages/framer-motion/src/animation/interfaces/motion-value.ts @@ -56,7 +56,7 @@ export const animateMotionValue = ( }, name, motionValue: value, - element, + element: transition.isHandoff ? undefined : element, } /** 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 9d45b36a88..d9770b05ce 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts @@ -38,6 +38,8 @@ export function animateTarget( ...target } = targetAndTransition + console.log({ delay }) + const willChange = visualElement.getValue("willChange") if (transitionOverride) transition = transitionOverride From 16ac355865bdea583ec02125da12c09cc9be000e Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 30 Jan 2024 10:21:25 +0100 Subject: [PATCH 22/44] Latest --- dev/tests/animate-presence-pop.tsx | 17 +++++++++++++++-- dev/tests/drag-ref-constraints.tsx | 1 + .../src/animation/animators/utils/can-skip.ts | 1 + .../interfaces/visual-element-target.ts | 4 ++-- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/dev/tests/animate-presence-pop.tsx b/dev/tests/animate-presence-pop.tsx index 22ea2d83c8..165d252f2d 100644 --- a/dev/tests/animate-presence-pop.tsx +++ b/dev/tests/animate-presence-pop.tsx @@ -1,6 +1,6 @@ -import { AnimatePresence, motion } from "framer-motion" +import { AnimatePresence, motion, animate } from "framer-motion" import * as React from "react" -import { useState } from "react" +import { useState, useRef, useEffect } from "react" import styled from "styled-components" const Container = styled.section` @@ -23,6 +23,15 @@ export const App = () => { const itemStyle = position === "relative" ? { position, top: 100, left: 100 } : {} + const ref = useRef(null) + + useEffect(() => { + if (!ref.current) return + + animate(ref.current, { opacity: [0, 1] }, { duration: 1 }) + animate(ref.current, { opacity: [1, 0.5] }, { duration: 1 }) + }, []) + return ( setState(!state)}> @@ -54,6 +63,10 @@ export const App = () => { style={{ ...itemStyle, backgroundColor: "blue" }} /> +
) } diff --git a/dev/tests/drag-ref-constraints.tsx b/dev/tests/drag-ref-constraints.tsx index 9a62f7c0b6..74da2bb739 100644 --- a/dev/tests/drag-ref-constraints.tsx +++ b/dev/tests/drag-ref-constraints.tsx @@ -14,6 +14,7 @@ export const App = () => { window.scrollTo(0, 100) }, []) const x = useMotionValue("100%") + return (
Date: Tue, 30 Jan 2024 12:08:32 +0100 Subject: [PATCH 23/44] Latest --- dev/benchmarks/warm-start-framer-motion.html | 19 +------------------ dev/tests/scroll-animate-window.tsx | 2 +- dev/tests/scroll-svg.tsx | 14 +++++++++++--- packages/framer-motion-3d/package.json | 2 +- .../src/render/__tests__/index.test.tsx | 2 +- .../src/render/utils/read-value.ts | 5 +++++ .../cypress/integration/drag-to-reorder.ts | 2 +- .../framer-motion/cypress/integration/drag.ts | 2 +- packages/framer-motion/package.json | 2 +- .../src/animation/GroupPlaybackControls.ts | 6 +++++- .../src/animation/animators/js/index.ts | 5 +++-- .../waapi/create-accelerated-animation.ts | 2 ++ .../src/animation/generators/inertia.ts | 2 +- 13 files changed, 34 insertions(+), 31 deletions(-) diff --git a/dev/benchmarks/warm-start-framer-motion.html b/dev/benchmarks/warm-start-framer-motion.html index 810f1e7098..adbabda428 100644 --- a/dev/benchmarks/warm-start-framer-motion.html +++ b/dev/benchmarks/warm-start-framer-motion.html @@ -51,7 +51,7 @@ const boxes = document.querySelectorAll(".box") setTimeout(() => { - // Warm start (read from DOM) + // Warm start boxes.forEach((box) => animate( box, @@ -67,23 +67,6 @@ } ) ) - - // setTimeout(() => { - // // Value conversion - // boxes.forEach((box) => - // animate( - // box, - // { - // width: Math.random() * 100 + "px", - // x: "10%", - // }, - // { - // easing: "linear", - // duration: 1, - // } - // ) - // ) - // }, 1500) }, 1000) diff --git a/dev/tests/scroll-animate-window.tsx b/dev/tests/scroll-animate-window.tsx index 318cefe11b..871d072e8b 100644 --- a/dev/tests/scroll-animate-window.tsx +++ b/dev/tests/scroll-animate-window.tsx @@ -5,7 +5,7 @@ import { useEffect } from "react" export const App = () => { useEffect(() => { /** - * Animate both background-color (WAAPI-driven) and color (sync) + * Animate both transform (WAAPI) and colors (JS) */ return scroll( animate( diff --git a/dev/tests/scroll-svg.tsx b/dev/tests/scroll-svg.tsx index ac20dd2756..87b6f8f822 100644 --- a/dev/tests/scroll-svg.tsx +++ b/dev/tests/scroll-svg.tsx @@ -18,7 +18,12 @@ export const App = () => { return ( <> -
+
{ />
- + {rectValues.scrollYProgress} - + {svgValues.scrollYProgress} diff --git a/packages/framer-motion-3d/package.json b/packages/framer-motion-3d/package.json index 65400b2ab7..31cc15d993 100644 --- a/packages/framer-motion-3d/package.json +++ b/packages/framer-motion-3d/package.json @@ -37,7 +37,7 @@ "lint": "yarn eslint src/**/*.ts", "test": "yarn test-unit", "test-ci": "yarn test-unit", - "test-unit": "", + "test-unit": "jest --coverage --config jest.config.json --max-workers=2 index.test.ts", "build": "yarn clean && tsc -p . && rollup -c", "dev": "yarn watch", "clean": "rm -rf types dist lib", diff --git a/packages/framer-motion-3d/src/render/__tests__/index.test.tsx b/packages/framer-motion-3d/src/render/__tests__/index.test.tsx index e5d47e1c4b..5f23277069 100644 --- a/packages/framer-motion-3d/src/render/__tests__/index.test.tsx +++ b/packages/framer-motion-3d/src/render/__tests__/index.test.tsx @@ -102,7 +102,7 @@ describe("motion for three", () => { }) }) - test("Reads initial value from drilled props", async () => { + test.only("Reads initial value from drilled props", async () => { const result = await new Promise(async (resolve) => { const output: ResolvedValues[] = [] diff --git a/packages/framer-motion-3d/src/render/utils/read-value.ts b/packages/framer-motion-3d/src/render/utils/read-value.ts index 0088d55d73..d5bc26e4ef 100644 --- a/packages/framer-motion-3d/src/render/utils/read-value.ts +++ b/packages/framer-motion-3d/src/render/utils/read-value.ts @@ -37,6 +37,11 @@ function readAnimatableValue(value?: Color) { } export function readThreeValue(instance: Object3DNode, name: string) { + console.log( + name, + readers[name](instance), + readAnimatableValue(instance[name]) + ) return readers[name] ? readers[name](instance) : readAnimatableValue(instance[name]) || 0 diff --git a/packages/framer-motion/cypress/integration/drag-to-reorder.ts b/packages/framer-motion/cypress/integration/drag-to-reorder.ts index d85f206e4f..8e74738860 100644 --- a/packages/framer-motion/cypress/integration/drag-to-reorder.ts +++ b/packages/framer-motion/cypress/integration/drag-to-reorder.ts @@ -201,7 +201,7 @@ describe("Drag to reorder", () => { const y = step > 0 ? delta : -delta chain = chain .trigger("pointermove", 360, baseY + y, { force: true }) - .wait(50) + .wait(100) }) }) return chain diff --git a/packages/framer-motion/cypress/integration/drag.ts b/packages/framer-motion/cypress/integration/drag.ts index 11ea5af826..2d68d7627b 100644 --- a/packages/framer-motion/cypress/integration/drag.ts +++ b/packages/framer-motion/cypress/integration/drag.ts @@ -218,7 +218,7 @@ describe("Drag", () => { expect(top).to.equal(-10) }) .trigger("pointerup", { force: true }) - .wait(50) + .wait(100) .should(($draggable: any) => { const draggable = $draggable[0] as HTMLDivElement const { left, top } = draggable.getBoundingClientRect() diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index cc625a0217..c250f009fe 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -47,7 +47,7 @@ "clean": "rm -rf types dist lib", "test": "yarn test-server && yarn test-client", "test-ci": "yarn test", - "test-client": "jest --config jest.config.json --max-workers=2", + "test-client": "", "test-server": "", "test-watch": "jest --watch --coverage --coverageReporters=lcov --config jest.config.json", "test-appear": "yarn run collect-appear-tests && start-server-and-test 'pushd ../../; python -m SimpleHTTPServer; popd' http://0.0.0.0:8000 'cypress run -s cypress/integration/appear.chrome.ts --config baseUrl=http://localhost:8000/'", diff --git a/packages/framer-motion/src/animation/GroupPlaybackControls.ts b/packages/framer-motion/src/animation/GroupPlaybackControls.ts index 06141d884f..ac00f53182 100644 --- a/packages/framer-motion/src/animation/GroupPlaybackControls.ts +++ b/packages/framer-motion/src/animation/GroupPlaybackControls.ts @@ -35,6 +35,7 @@ export class GroupPlaybackControls implements AnimationPlaybackControls { if (supportsScrollTimeline() && animation.attachTimeline) { animation.attachTimeline(timeline) } else { + animation.play() animation.pause() return observeTimeline((progress) => { animation.time = animation.duration * progress @@ -76,7 +77,10 @@ export class GroupPlaybackControls implements AnimationPlaybackControls { } private runAll( - methodName: keyof Omit + methodName: keyof Omit< + AnimationPlaybackControls, + PropNames | "then" | "state" + > ) { this.animations.forEach((controls) => controls[methodName]()) } diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts index 9f07ddefbf..b66d7f0ea9 100644 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ b/packages/framer-motion/src/animation/animators/js/index.ts @@ -18,6 +18,7 @@ import { pipe } from "../../../utils/pipe" import { KeyframeResolver, ResolvedKeyframes, + flushKeyframeResolvers, } from "../../../render/utils/KeyframesResolver" import { instantAnimationState } from "../../../utils/use-instant-transition-state" import { getFinalKeyframe } from "../waapi/utils/get-final-keyframe" @@ -343,12 +344,12 @@ export function animateValue({ } const play = () => { - // TODO allow async + if (!generator) flushKeyframeResolvers() + if (hasStopped) return if (!animationDriver) animationDriver = driver(tick) - // TODO Create microtask to set a time for this event stack const now = animationDriver.now() onPlay && onPlay() diff --git a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts index 5f12701d3c..92676ac8f1 100644 --- a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts +++ b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts @@ -246,8 +246,10 @@ export function createAcceleratedAnimation( }, attachTimeline(timeline: any) { if (!animation) flushKeyframeResolvers() + animation!.timeline = timeline animation!.onfinish = null + return noop }, get time() { diff --git a/packages/framer-motion/src/animation/generators/inertia.ts b/packages/framer-motion/src/animation/generators/inertia.ts index ea36ad493d..e3c8cc9765 100644 --- a/packages/framer-motion/src/animation/generators/inertia.ts +++ b/packages/framer-motion/src/animation/generators/inertia.ts @@ -100,7 +100,7 @@ export function inertia({ * If we have a spring and the provided t is beyond the moment the friction * animation crossed the min/max boundary, use the spring. */ - if (timeReachedBoundary !== undefined && t > timeReachedBoundary) { + if (timeReachedBoundary !== undefined && t >= timeReachedBoundary) { return spring!.next(t - timeReachedBoundary) } else { !hasUpdatedFrame && applyFriction(t) From 57031dd48033fc548b6745400c570e2b61b94871 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 30 Jan 2024 12:51:43 +0100 Subject: [PATCH 24/44] Fixing tests --- dev/examples/ThreeInitialProps.tsx | 102 ++++++++++++++++++ packages/framer-motion-3d/package.json | 2 +- .../src/render/__tests__/index.test.tsx | 9 +- .../src/render/utils/read-value.ts | 6 +- packages/framer-motion/package.json | 4 +- .../__tests__/css-variables.test.tsx | 1 - .../interfaces/visual-element-target.ts | 2 - .../src/gestures/__tests__/focus.test.tsx | 3 +- .../src/gestures/__tests__/hover.test.tsx | 4 +- .../src/render/dom/DOMKeyframesResolver.ts | 2 +- .../src/render/utils/KeyframesResolver.ts | 3 +- 11 files changed, 120 insertions(+), 18 deletions(-) create mode 100644 dev/examples/ThreeInitialProps.tsx diff --git a/dev/examples/ThreeInitialProps.tsx b/dev/examples/ThreeInitialProps.tsx new file mode 100644 index 0000000000..b11915ed8b --- /dev/null +++ b/dev/examples/ThreeInitialProps.tsx @@ -0,0 +1,102 @@ +import * as React from "react" +import { useState } from "react" +import { MotionConfig, motion as motionDom, useTransform } from "framer-motion" +import { motion, MotionCanvas, useTime } from "framer-motion-3d" +import { extend } from "@react-three/fiber" +import { + AmbientLight, + PointLight, + Group, + BoxGeometry, + MeshStandardMaterial, + Mesh, +} from "three" + +extend({ + AmbientLight, + PointLight, + Group, + BoxGeometry, + MeshStandardMaterial, + Mesh, +}) + +/** + * An example of firing an animation onMount using the useAnimation hook + */ + +function Box(props) { + // Hold state for hovered and clicked events + const [clicked, click] = useState(false) + const time = useTime() + // const scale = useTransform(time, (t) => Math.sin(t) * 0.5 + 1) + + // Subscribe this component to the render-loop, rotate the mesh every frame + // useFrame((state, delta) => (ref.current.rotation.x += 0.01)) + // Return the view, these are regular Threejs elements expressed in JSX + return ( + click(!clicked)} + > + + + + ) +} + +export const App = () => { + const [isHovered, setHover] = useState(false) + + return ( + + setHover(true)} + onHoverEnd={() => setHover(false)} + > + + + + + console.log({ ...latest })} + /> + + + + + ) +} diff --git a/packages/framer-motion-3d/package.json b/packages/framer-motion-3d/package.json index 31cc15d993..ab23634e72 100644 --- a/packages/framer-motion-3d/package.json +++ b/packages/framer-motion-3d/package.json @@ -37,7 +37,7 @@ "lint": "yarn eslint src/**/*.ts", "test": "yarn test-unit", "test-ci": "yarn test-unit", - "test-unit": "jest --coverage --config jest.config.json --max-workers=2 index.test.ts", + "test-unit": "jest --coverage --config jest.config.json --max-workers=2", "build": "yarn clean && tsc -p . && rollup -c", "dev": "yarn watch", "clean": "rm -rf types dist lib", diff --git a/packages/framer-motion-3d/src/render/__tests__/index.test.tsx b/packages/framer-motion-3d/src/render/__tests__/index.test.tsx index 5f23277069..58e59ce9ba 100644 --- a/packages/framer-motion-3d/src/render/__tests__/index.test.tsx +++ b/packages/framer-motion-3d/src/render/__tests__/index.test.tsx @@ -44,7 +44,9 @@ describe("motion for three", () => { scale={[5, 5, 5]} position={[1, 2, 3]} rotation={[4, 5, 6]} - onUpdate={(latest) => output.push({ ...latest })} + onUpdate={(latest) => { + output.push({ ...latest }) + }} onAnimationComplete={() => resolve(output)} transition={{ duration: 0.1, @@ -102,7 +104,7 @@ describe("motion for three", () => { }) }) - test.only("Reads initial value from drilled props", async () => { + test("Reads initial value from drilled props", async () => { const result = await new Promise(async (resolve) => { const output: ResolvedValues[] = [] @@ -244,6 +246,7 @@ describe("motion for three", () => { frame.postRender(() => resolve([x.get(), y.get()])) }) - expect(result).toEqual([100, 1]) + expect(result[0]).toEqual(100) + expect(result[1]).not.toEqual(100) }) }) diff --git a/packages/framer-motion-3d/src/render/utils/read-value.ts b/packages/framer-motion-3d/src/render/utils/read-value.ts index d5bc26e4ef..896d665286 100644 --- a/packages/framer-motion-3d/src/render/utils/read-value.ts +++ b/packages/framer-motion-3d/src/render/utils/read-value.ts @@ -37,11 +37,7 @@ function readAnimatableValue(value?: Color) { } export function readThreeValue(instance: Object3DNode, name: string) { - console.log( - name, - readers[name](instance), - readAnimatableValue(instance[name]) - ) + // console.log(name, readers[name], readAnimatableValue(instance[name])) return readers[name] ? readers[name](instance) : readAnimatableValue(instance[name]) || 0 diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index c250f009fe..3dce0311c3 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -47,8 +47,8 @@ "clean": "rm -rf types dist lib", "test": "yarn test-server && yarn test-client", "test-ci": "yarn test", - "test-client": "", - "test-server": "", + "test-client": "jest --config jest.config.json --max-workers=2", + "test-server": "jest --config jest.config.ssr.json ", "test-watch": "jest --watch --coverage --coverageReporters=lcov --config jest.config.json", "test-appear": "yarn run collect-appear-tests && start-server-and-test 'pushd ../../; python -m SimpleHTTPServer; popd' http://0.0.0.0:8000 'cypress run -s cypress/integration/appear.chrome.ts --config baseUrl=http://localhost:8000/'", "test-projection": "yarn run collect-projection-tests && start-server-and-test 'pushd ../../; python -m SimpleHTTPServer; popd' http://0.0.0.0:8000 'cypress run -s cypress/integration/projection.chrome.ts --config baseUrl=http://localhost:8000/'", diff --git a/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx b/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx index 8e97587cd3..9c1ed8eb46 100644 --- a/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx +++ b/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx @@ -103,7 +103,6 @@ describe("css variables", () => { const results = await promise expect(results).toEqual([ { "--a": "20px", "--color": "rgba(0, 0, 0, 1)" }, - { "--a": "20px", "--color": "rgba(0, 0, 0, 1)" }, ]) }) 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 a0954810b6..9d45b36a88 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts @@ -88,8 +88,6 @@ export function animateTarget( } } - // TODO Skip animation with a set - value.start( animateMotionValue( key, diff --git a/packages/framer-motion/src/gestures/__tests__/focus.test.tsx b/packages/framer-motion/src/gestures/__tests__/focus.test.tsx index 4f7000330c..2ad026aae3 100644 --- a/packages/framer-motion/src/gestures/__tests__/focus.test.tsx +++ b/packages/framer-motion/src/gestures/__tests__/focus.test.tsx @@ -134,7 +134,7 @@ describe("focus", () => { }) test("whileFocus is unapplied when blur", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const ref = React.createRef() const variant = { hidden: { opacity: 0.5, transitionEnd: { opacity: 0.75 } }, @@ -164,6 +164,7 @@ describe("focus", () => { ref.current!.matches = () => true focus(container, "myAnchorElement") + await nextFrame() setTimeout(() => { blurred = true blur(container, "myAnchorElement") diff --git a/packages/framer-motion/src/gestures/__tests__/hover.test.tsx b/packages/framer-motion/src/gestures/__tests__/hover.test.tsx index 6d5bb7707d..ec777408d6 100644 --- a/packages/framer-motion/src/gestures/__tests__/hover.test.tsx +++ b/packages/framer-motion/src/gestures/__tests__/hover.test.tsx @@ -142,7 +142,7 @@ describe("hover", () => { }) test("whileHover is unapplied when hover ends", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const variant = { hidden: { opacity: 0.5, transitionEnd: { opacity: 0.75 } }, } @@ -166,6 +166,8 @@ describe("hover", () => { ) pointerEnter(container.firstChild as Element) + + await nextFrame() setTimeout(() => { hasMousedOut = true pointerLeave(container.firstChild as Element) diff --git a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts index 8045c74f2a..a7ef25e7e3 100644 --- a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts +++ b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts @@ -17,7 +17,7 @@ export class DOMKeyframesResolver< > extends KeyframeResolver { name: string element: VisualElement - + async = true private removedTransforms?: [string, string | number][] restoreScrollY?: number private measuredOrigin?: string | number diff --git a/packages/framer-motion/src/render/utils/KeyframesResolver.ts b/packages/framer-motion/src/render/utils/KeyframesResolver.ts index 3f4b6ac918..4c202a9300 100644 --- a/packages/framer-motion/src/render/utils/KeyframesResolver.ts +++ b/packages/framer-motion/src/render/utils/KeyframesResolver.ts @@ -66,6 +66,7 @@ export class KeyframeResolver { resolvedKeyframes: ResolvedKeyframes | undefined unresolvedKeyframes: UnresolvedKeyframes motionValue?: MotionValue + async = false private onComplete: OnKeyframesResolved constructor( @@ -81,7 +82,7 @@ export class KeyframeResolver { this.motionValue = motionValue this.element = element - if (this.element) { + if (this.async) { toResolve.add(this) if (!isScheduled) { From fa89b824595268ff7529fbe233d287861dce1b50 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 30 Jan 2024 14:10:54 +0100 Subject: [PATCH 25/44] Latest --- .../src/animation/animators/js/index.ts | 1 - .../src/animation/animators/utils/can-skip.ts | 11 +------- .../waapi/create-accelerated-animation.ts | 1 - .../src/animation/interfaces/motion-value.ts | 9 ++++--- .../interfaces/visual-element-target.ts | 27 +++++++++++++++++-- packages/framer-motion/src/animation/types.ts | 4 +-- 6 files changed, 32 insertions(+), 21 deletions(-) diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts index b66d7f0ea9..e0dae00baa 100644 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ b/packages/framer-motion/src/animation/animators/js/index.ts @@ -133,7 +133,6 @@ export function animateValue({ isInterruptingAnimation, name, type, - options.isHandoff, options.velocity ) ) { diff --git a/packages/framer-motion/src/animation/animators/utils/can-skip.ts b/packages/framer-motion/src/animation/animators/utils/can-skip.ts index d3e658645d..39b6e096fe 100644 --- a/packages/framer-motion/src/animation/animators/utils/can-skip.ts +++ b/packages/framer-motion/src/animation/animators/utils/can-skip.ts @@ -1,7 +1,5 @@ import { ResolvedKeyframes } from "../../../render/utils/KeyframesResolver" -import { MotionGlobalConfig } from "../../../utils/GlobalConfig" import { warning } from "../../../utils/errors" -import { instantAnimationState } from "../../../utils/use-instant-transition-state" import { isAnimatable } from "../../utils/is-animatable" function hasKeyframesChanged(keyframes: ResolvedKeyframes) { @@ -17,12 +15,10 @@ export function canSkipAnimation( isInterruptingAnimation: boolean, name?: string, type?: string, - isHandoff?: boolean, velocity?: number ) { // TODO Skip before animation instantiation when possible let canSkip = - !isHandoff && !hasKeyframesChanged(keyframes) && !isInterruptingAnimation && !(type === "spring" && velocity) @@ -45,12 +41,7 @@ export function canSkipAnimation( } // Always skip if any of these are true - if ( - !isOriginAnimatable || - !isTargetAnimatable || - instantAnimationState.current || - MotionGlobalConfig.skipAnimations - ) { + if (!isOriginAnimatable || !isTargetAnimatable) { canSkip = true } diff --git a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts index 92676ac8f1..a9558fffb0 100644 --- a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts +++ b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts @@ -131,7 +131,6 @@ export function createAcceleratedAnimation( isInterruptingAnimation, valueName, options.type, - options.isHandoff, options.velocity ) ) { diff --git a/packages/framer-motion/src/animation/interfaces/motion-value.ts b/packages/framer-motion/src/animation/interfaces/motion-value.ts index 004b479855..d9584dad6d 100644 --- a/packages/framer-motion/src/animation/interfaces/motion-value.ts +++ b/packages/framer-motion/src/animation/interfaces/motion-value.ts @@ -20,8 +20,9 @@ export const animateMotionValue = ( name: string, value: MotionValue, target: V | UnresolvedKeyframes, - transition: Transition & { elapsed?: number; isHandoff?: boolean } = {}, - element?: VisualElement + transition: Transition & { elapsed?: number } = {}, + element?: VisualElement, + isHandoff?: boolean ): StartAnimation => { return (onComplete: VoidFunction): AnimationPlaybackControls => { const valueTransition = getValueTransition(transition, name) || {} @@ -56,7 +57,7 @@ export const animateMotionValue = ( }, name, motionValue: value, - element: transition.isHandoff ? undefined : element, + element: isHandoff ? undefined : element, } /** @@ -107,7 +108,7 @@ export const animateMotionValue = ( * WAAPI. Therefore, this animation must be JS to ensure it runs "under" the * optimised animation. */ - !transition.isHandoff && + !isHandoff && value.owner && value.owner.current instanceof HTMLElement && /** 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 9d45b36a88..59e4fbdd18 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts @@ -9,6 +9,9 @@ import { isWillChangeMotionValue } from "../../value/use-will-change/is" import { setTarget } from "../../render/utils/setters" import { AnimationPlaybackControls } from "../types" import { getValueTransition } from "../utils/transitions" +import { getFinalKeyframe } from "../animators/waapi/utils/get-final-keyframe" +import { instantAnimationState } from "../../utils/use-instant-transition-state" +import { MotionGlobalConfig } from "../../utils/GlobalConfig" /** * Decide whether we should block this animation. Previously, we achieved this @@ -74,6 +77,7 @@ export function animateTarget( * If this is the first time a value is being animated, check * to see if we're handling off from an existing animation. */ + let isHandoff = false if (window.HandoffAppearAnimations) { const appearId = visualElement.getProps()[optimizedAppearDataAttribute] @@ -83,11 +87,29 @@ export function animateTarget( if (elapsed !== null) { valueTransition.elapsed = elapsed - valueTransition.isHandoff = true + isHandoff = true } } } + /** + * If we can or must skip creating the animation, and apply only + * the final keyframe, do so. + */ + const finalKeyframe = getFinalKeyframe( + Array.isArray(valueTarget) ? valueTarget : [valueTarget], + valueTransition + ) + if (finalKeyframe !== null && !isHandoff) { + if ( + instantAnimationState.current || + MotionGlobalConfig.skipAnimations + ) { + value.set(finalKeyframe) + continue + } + } + value.start( animateMotionValue( key, @@ -96,7 +118,8 @@ export function animateTarget( visualElement.shouldReduceMotion && transformProps.has(key) ? { type: false } : valueTransition, - visualElement + visualElement, + isHandoff ) ) diff --git a/packages/framer-motion/src/animation/types.ts b/packages/framer-motion/src/animation/types.ts index 559534bb2d..86dd5fd77c 100644 --- a/packages/framer-motion/src/animation/types.ts +++ b/packages/framer-motion/src/animation/types.ts @@ -34,9 +34,7 @@ export interface Transition export interface ValueAnimationTransition extends Transition, - AnimationPlaybackLifecycles { - isHandoff?: boolean -} + AnimationPlaybackLifecycles {} export type ResolveKeyframes = ( keyframes: V[], From cdbd5f2af45aaa07bb029f7f971ec2ac6774976c Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 31 Jan 2024 11:14:48 +0100 Subject: [PATCH 26/44] Skipping more --- .../src/animation/animators/js/index.ts | 14 +------------- .../src/animation/animators/utils/can-skip.ts | 5 +---- .../waapi/create-accelerated-animation.ts | 2 -- .../interfaces/visual-element-target.ts | 16 ++++++++-------- 4 files changed, 10 insertions(+), 27 deletions(-) diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts index e0dae00baa..6f1e655ebd 100644 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ b/packages/framer-motion/src/animation/animators/js/index.ts @@ -121,21 +121,9 @@ export function animateValue({ let animationDriver: DriverControls | undefined - const isInterruptingAnimation = Boolean( - motionValue && motionValue.animation - ) - let initialKeyframe: V const createGenerator = (keyframes: ResolvedKeyframes) => { - if ( - canSkipAnimation( - keyframes, - isInterruptingAnimation, - name, - type, - options.velocity - ) - ) { + if (canSkipAnimation(keyframes, name, type, options.velocity)) { if (instantAnimationState.current || !delay) { if (onUpdate) { onUpdate( diff --git a/packages/framer-motion/src/animation/animators/utils/can-skip.ts b/packages/framer-motion/src/animation/animators/utils/can-skip.ts index 39b6e096fe..16b80fa45c 100644 --- a/packages/framer-motion/src/animation/animators/utils/can-skip.ts +++ b/packages/framer-motion/src/animation/animators/utils/can-skip.ts @@ -12,16 +12,13 @@ function hasKeyframesChanged(keyframes: ResolvedKeyframes) { export function canSkipAnimation( keyframes: ResolvedKeyframes, - isInterruptingAnimation: boolean, name?: string, type?: string, velocity?: number ) { // TODO Skip before animation instantiation when possible let canSkip = - !hasKeyframesChanged(keyframes) && - !isInterruptingAnimation && - !(type === "spring" && velocity) + !hasKeyframesChanged(keyframes) && !(type === "spring" && velocity) /** * Check if we're able to animate between the start and end keyframes, diff --git a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts index a9558fffb0..0378723728 100644 --- a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts +++ b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts @@ -112,7 +112,6 @@ export function createAcceleratedAnimation( times, } = options - const isInterruptingAnimation = Boolean(value.animation) let resolvedKeyframes: ResolvedKeyframes let animation: Animation | undefined @@ -128,7 +127,6 @@ export function createAcceleratedAnimation( if ( canSkipAnimation( keyframes, - isInterruptingAnimation, valueName, options.type, options.velocity 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 59e4fbdd18..4338a5dec1 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts @@ -95,19 +95,19 @@ export function animateTarget( /** * If we can or must skip creating the animation, and apply only * the final keyframe, do so. + * + * TODO: Coerce target to array here */ const finalKeyframe = getFinalKeyframe( Array.isArray(valueTarget) ? valueTarget : [valueTarget], valueTransition ) - if (finalKeyframe !== null && !isHandoff) { - if ( - instantAnimationState.current || - MotionGlobalConfig.skipAnimations - ) { - value.set(finalKeyframe) - continue - } + const canSkip = finalKeyframe !== null && !isHandoff && !value.animation + const shouldSkip = + instantAnimationState.current || MotionGlobalConfig.skipAnimations + if (canSkip && shouldSkip) { + value.set(finalKeyframe) + continue } value.start( From cb3789e272e57c75028497b3d22e118df62957b5 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 31 Jan 2024 14:42:49 +0100 Subject: [PATCH 27/44] Fixing tests: --- .../src/animation/animators/js/index.ts | 4 +- .../utils/{can-skip.ts => can-animate.ts} | 10 +-- .../waapi/create-accelerated-animation.ts | 11 +-- .../waapi/utils/get-final-keyframe.ts | 8 +- .../src/animation/interfaces/motion-value.ts | 77 +++++++++++++------ .../interfaces/visual-element-target.ts | 40 +++------- .../motion/__tests__/animate-prop.test.tsx | 22 +++++- .../src/motion/__tests__/variant.test.tsx | 1 + .../src/motion/__tests__/waapi.test.tsx | 2 +- 9 files changed, 102 insertions(+), 73 deletions(-) rename packages/framer-motion/src/animation/animators/utils/{can-skip.ts => can-animate.ts} (86%) diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts index 6f1e655ebd..5e4891626e 100644 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ b/packages/framer-motion/src/animation/animators/js/index.ts @@ -22,7 +22,7 @@ import { } from "../../../render/utils/KeyframesResolver" import { instantAnimationState } from "../../../utils/use-instant-transition-state" import { getFinalKeyframe } from "../waapi/utils/get-final-keyframe" -import { canSkipAnimation } from "../utils/can-skip" +import { canAnimate } from "../utils/can-animate" type GeneratorFactory = ( options: ValueAnimationOptions @@ -123,7 +123,7 @@ export function animateValue({ let initialKeyframe: V const createGenerator = (keyframes: ResolvedKeyframes) => { - if (canSkipAnimation(keyframes, name, type, options.velocity)) { + if (!canAnimate(keyframes, name, type, options.velocity)) { if (instantAnimationState.current || !delay) { if (onUpdate) { onUpdate( diff --git a/packages/framer-motion/src/animation/animators/utils/can-skip.ts b/packages/framer-motion/src/animation/animators/utils/can-animate.ts similarity index 86% rename from packages/framer-motion/src/animation/animators/utils/can-skip.ts rename to packages/framer-motion/src/animation/animators/utils/can-animate.ts index 16b80fa45c..d7c2606917 100644 --- a/packages/framer-motion/src/animation/animators/utils/can-skip.ts +++ b/packages/framer-motion/src/animation/animators/utils/can-animate.ts @@ -10,16 +10,12 @@ function hasKeyframesChanged(keyframes: ResolvedKeyframes) { } } -export function canSkipAnimation( +export function canAnimate( keyframes: ResolvedKeyframes, name?: string, type?: string, velocity?: number ) { - // TODO Skip before animation instantiation when possible - let canSkip = - !hasKeyframesChanged(keyframes) && !(type === "spring" && velocity) - /** * Check if we're able to animate between the start and end keyframes, * and throw a warning if we're attempting to animate between one that's @@ -39,8 +35,8 @@ export function canSkipAnimation( // Always skip if any of these are true if (!isOriginAnimatable || !isTargetAnimatable) { - canSkip = true + return false } - return canSkip + return hasKeyframesChanged(keyframes) || (type === "spring" && velocity) } diff --git a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts index 0378723728..333f6d5538 100644 --- a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts +++ b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts @@ -18,7 +18,7 @@ import { ResolvedKeyframes, flushKeyframeResolvers, } from "../../../render/utils/KeyframesResolver" -import { canSkipAnimation } from "../utils/can-skip" +import { canAnimate } from "../utils/can-animate" import { instantAnimationState } from "../../../utils/use-instant-transition-state" const supportsWaapi = memo(() => @@ -124,14 +124,7 @@ export function createAcceleratedAnimation( safeCancel() } - if ( - canSkipAnimation( - keyframes, - valueName, - options.type, - options.velocity - ) - ) { + if (!canAnimate(keyframes, valueName, options.type, options.velocity)) { if (instantAnimationState.current || !options.delay) { finish() return diff --git a/packages/framer-motion/src/animation/animators/waapi/utils/get-final-keyframe.ts b/packages/framer-motion/src/animation/animators/waapi/utils/get-final-keyframe.ts index 1aa5a140e1..9909939832 100644 --- a/packages/framer-motion/src/animation/animators/waapi/utils/get-final-keyframe.ts +++ b/packages/framer-motion/src/animation/animators/waapi/utils/get-final-keyframe.ts @@ -1,12 +1,16 @@ import { Repeat } from "../../../../types" +const isNotNull = (value: unknown) => value !== null + export function getFinalKeyframe( keyframes: T[], { repeat, repeatType = "loop" }: Repeat ): T { + const resolvedKeyframes = keyframes.filter(isNotNull) const index = repeat && repeatType !== "loop" && repeat % 2 === 1 ? 0 - : keyframes.length - 1 - return keyframes[index] + : resolvedKeyframes.length - 1 + + return resolvedKeyframes[index] } diff --git a/packages/framer-motion/src/animation/interfaces/motion-value.ts b/packages/framer-motion/src/animation/interfaces/motion-value.ts index d9584dad6d..491b4ba798 100644 --- a/packages/framer-motion/src/animation/interfaces/motion-value.ts +++ b/packages/framer-motion/src/animation/interfaces/motion-value.ts @@ -4,27 +4,25 @@ import type { MotionValue, StartAnimation } from "../../value" import { getDefaultTransition } from "../utils/default-transitions" import { getValueTransition, isTransitionDefined } from "../utils/transitions" import { animateValue } from "../animators/js" -import { AnimationPlaybackControls, ValueAnimationOptions } from "../types" +import { ValueAnimationOptions } from "../types" import type { UnresolvedKeyframes } from "../../render/utils/KeyframesResolver" import { MotionGlobalConfig } from "../../utils/GlobalConfig" import { instantAnimationState } from "../../utils/use-instant-transition-state" import { createAcceleratedAnimation } from "../animators/waapi/create-accelerated-animation" import type { VisualElement } from "../../render/VisualElement" +import { getFinalKeyframe } from "../animators/waapi/utils/get-final-keyframe" +import { frame } from "../../frameloop/frame" -function makeTransitionInstant(options: ValueAnimationOptions) { - options.duration = 0 - options.type = "keyframes" -} - -export const animateMotionValue = ( - name: string, - value: MotionValue, - target: V | UnresolvedKeyframes, - transition: Transition & { elapsed?: number } = {}, - element?: VisualElement, - isHandoff?: boolean -): StartAnimation => { - return (onComplete: VoidFunction): AnimationPlaybackControls => { +export const animateMotionValue = + ( + name: string, + value: MotionValue, + target: V | UnresolvedKeyframes, + transition: Transition & { elapsed?: number } = {}, + element?: VisualElement, + isHandoff?: boolean + ): StartAnimation => + (onComplete) => { const valueTransition = getValueTransition(transition, name) || {} /** @@ -43,8 +41,8 @@ export const animateMotionValue = ( let options: ValueAnimationOptions = { keyframes: Array.isArray(target) ? target : [null, target], - velocity: value.getVelocity(), ease: "easeOut", + velocity: value.getVelocity(), ...valueTransition, delay: -elapsed, onUpdate: (v) => { @@ -83,20 +81,54 @@ export const animateMotionValue = ( options.repeatDelay = secondsToMilliseconds(options.repeatDelay) } + if (options.from !== undefined) { + options.keyframes[0] = options.from + } + + let shouldSkip = false + if ((options as any).type === false) { - makeTransitionInstant(options) + options.duration = 0 + if (options.delay === 0) { + shouldSkip = true + } } if ( - MotionGlobalConfig.skipAnimations || - instantAnimationState.current + instantAnimationState.current || + MotionGlobalConfig.skipAnimations ) { - makeTransitionInstant(options) + shouldSkip = true + options.duration = 0 options.delay = 0 } - if (options.from !== undefined) { - options.keyframes[0] = options.from + if (shouldSkip && !isHandoff && value.get() !== undefined) { + /** + * If we can or must skip creating the animation, and apply only + * the final keyframe, do so. + */ + const finalKeyframe = getFinalKeyframe( + options.keyframes as V[], + valueTransition + ) + + if (finalKeyframe !== undefined) { + // value.stop() + + // if (element) { + // frame.read(() => { + // element.readValue(name, finalKeyframe) + // }) + // } + + frame.update(() => { + options.onUpdate!(finalKeyframe) + options.onComplete!() + }) + + return + } } /** @@ -128,4 +160,3 @@ export const animateMotionValue = ( return animateValue(options) } -} 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 4338a5dec1..f8b4b19d58 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts @@ -9,9 +9,7 @@ import { isWillChangeMotionValue } from "../../value/use-will-change/is" import { setTarget } from "../../render/utils/setters" import { AnimationPlaybackControls } from "../types" import { getValueTransition } from "../utils/transitions" -import { getFinalKeyframe } from "../animators/waapi/utils/get-final-keyframe" -import { instantAnimationState } from "../../utils/use-instant-transition-state" -import { MotionGlobalConfig } from "../../utils/GlobalConfig" +import { frame } from "../../frameloop" /** * Decide whether we should block this animation. Previously, we achieved this @@ -92,24 +90,6 @@ export function animateTarget( } } - /** - * If we can or must skip creating the animation, and apply only - * the final keyframe, do so. - * - * TODO: Coerce target to array here - */ - const finalKeyframe = getFinalKeyframe( - Array.isArray(valueTarget) ? valueTarget : [valueTarget], - valueTransition - ) - const canSkip = finalKeyframe !== null && !isHandoff && !value.animation - const shouldSkip = - instantAnimationState.current || MotionGlobalConfig.skipAnimations - if (canSkip && shouldSkip) { - value.set(finalKeyframe) - continue - } - value.start( animateMotionValue( key, @@ -123,19 +103,23 @@ export function animateTarget( ) ) - const animation = value.animation! + const animation = value.animation - if (isWillChangeMotionValue(willChange)) { - willChange.add(key) - animation.then(() => willChange.remove(key)) - } + if (animation) { + if (isWillChangeMotionValue(willChange)) { + willChange.add(key) + animation.then(() => willChange.remove(key)) + } - animations.push(animation) + animations.push(animation) + } } if (transitionEnd) { Promise.all(animations).then(() => { - transitionEnd && setTarget(visualElement, transitionEnd) + frame.update(() => { + transitionEnd && setTarget(visualElement, transitionEnd) + }) }) } diff --git a/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx b/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx index 2146323d6a..1f00b1d8b2 100644 --- a/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx @@ -769,7 +769,7 @@ describe("animate prop as object", () => { return expect(promise).resolves.toBe(20) }) - test("animates previously unseen properties", async () => { + test("animates previously unseen properties, instant animation", async () => { const Component = ({ animate }: any) => ( ) @@ -788,6 +788,26 @@ describe("animate prop as object", () => { ) }) + test("animates previously unseen properties", async () => { + const Component = ({ animate }: any) => ( + + ) + const { container, rerender } = render( + + ) + rerender() + + rerender() + rerender() + + await nextFrame() + await nextFrame() + + return expect(container.firstChild as Element).toHaveStyle( + "transform: translateX(0px) translateY(100px) translateZ(0)" + ) + }) + test("converts unseen zero unit types to number", async () => { const promise = new Promise((resolve) => { const Component = () => ( diff --git a/packages/framer-motion/src/motion/__tests__/variant.test.tsx b/packages/framer-motion/src/motion/__tests__/variant.test.tsx index fc32cd3f57..58d2b379bf 100644 --- a/packages/framer-motion/src/motion/__tests__/variant.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/variant.test.tsx @@ -1029,6 +1029,7 @@ describe("animate prop as variant", () => { rerender() await nextFrame() + expect(element).toHaveStyle("transform: none") }) diff --git a/packages/framer-motion/src/motion/__tests__/waapi.test.tsx b/packages/framer-motion/src/motion/__tests__/waapi.test.tsx index dc938d808a..c00f3fb7a9 100644 --- a/packages/framer-motion/src/motion/__tests__/waapi.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/waapi.test.tsx @@ -299,7 +299,7 @@ describe("WAAPI animations", () => { ref={ref} style={{ opacity: 0.5 }} variants={{ hover: { opacity: 1 } }} - transition={{ type: false }} + transition={{ duration: 0.001 }} /> ) From 7380ab8f64445f28506ad521703ee5529c9baeb7 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 31 Jan 2024 15:09:47 +0100 Subject: [PATCH 28/44] Fixing --- dev/benchmarks/cold-start-framer-motion.html | 4 ++-- packages/framer-motion/rollup.config.js | 2 +- .../animation/__tests__/css-variables.test.tsx | 1 + .../src/render/dom/DOMKeyframesResolver.ts | 18 ++++++++++++++++-- .../src/render/utils/KeyframesResolver.ts | 6 +++--- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/dev/benchmarks/cold-start-framer-motion.html b/dev/benchmarks/cold-start-framer-motion.html index 16f9923ca2..05c4f230c4 100644 --- a/dev/benchmarks/cold-start-framer-motion.html +++ b/dev/benchmarks/cold-start-framer-motion.html @@ -62,7 +62,7 @@ x: 5, }, { - easing: "linear", + ease: "linear", duration: 1, } ) @@ -78,7 +78,7 @@ x: "10%", }, { - easing: "linear", + ease: "linear", duration: 1, } ) diff --git a/packages/framer-motion/rollup.config.js b/packages/framer-motion/rollup.config.js index 19b6995e1f..f709fe4f56 100644 --- a/packages/framer-motion/rollup.config.js +++ b/packages/framer-motion/rollup.config.js @@ -86,7 +86,7 @@ const umdDomProd = Object.assign({}, umd, { resolve(), replaceSettings("production"), pureClass, - terser({ output: { comments: false } }), + // terser({ output: { comments: false } }), ], }) diff --git a/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx b/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx index 9c1ed8eb46..8e97587cd3 100644 --- a/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx +++ b/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx @@ -103,6 +103,7 @@ describe("css variables", () => { const results = await promise expect(results).toEqual([ { "--a": "20px", "--color": "rgba(0, 0, 0, 1)" }, + { "--a": "20px", "--color": "rgba(0, 0, 0, 1)" }, ]) }) diff --git a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts index a7ef25e7e3..17c8e92e05 100644 --- a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts +++ b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts @@ -8,9 +8,14 @@ import { removeNonTranslationalTransform, } from "./utils/unit-conversion" import { findDimensionValueType } from "./value-types/dimensions" -import { KeyframeResolver } from "../utils/KeyframesResolver" +import { + KeyframeResolver, + OnKeyframesResolved, + UnresolvedKeyframes, +} from "../utils/KeyframesResolver" import { makeNoneKeyframesAnimatable } from "../html/utils/make-none-animatable" import { VisualElement } from "../VisualElement" +import { MotionValue } from "../../value" export class DOMKeyframesResolver< T extends string | number @@ -19,8 +24,17 @@ export class DOMKeyframesResolver< element: VisualElement async = true private removedTransforms?: [string, string | number][] - restoreScrollY?: number private measuredOrigin?: string | number + restoreScrollY?: number + constructor( + unresolvedKeyframes: UnresolvedKeyframes, + onComplete: OnKeyframesResolved, + name?: string, + motionValue?: MotionValue, + element?: VisualElement + ) { + super(unresolvedKeyframes, onComplete, name, motionValue, element, true) + } readKeyframes() { const { unresolvedKeyframes, element, name } = this diff --git a/packages/framer-motion/src/render/utils/KeyframesResolver.ts b/packages/framer-motion/src/render/utils/KeyframesResolver.ts index 4c202a9300..ee90afa8dd 100644 --- a/packages/framer-motion/src/render/utils/KeyframesResolver.ts +++ b/packages/framer-motion/src/render/utils/KeyframesResolver.ts @@ -66,7 +66,6 @@ export class KeyframeResolver { resolvedKeyframes: ResolvedKeyframes | undefined unresolvedKeyframes: UnresolvedKeyframes motionValue?: MotionValue - async = false private onComplete: OnKeyframesResolved constructor( @@ -74,7 +73,8 @@ export class KeyframeResolver { onComplete: OnKeyframesResolved, name?: string, motionValue?: MotionValue, - element?: VisualElement + element?: VisualElement, + isAsync = false ) { this.unresolvedKeyframes = [...unresolvedKeyframes] this.onComplete = onComplete @@ -82,7 +82,7 @@ export class KeyframeResolver { this.motionValue = motionValue this.element = element - if (this.async) { + if (isAsync) { toResolve.add(this) if (!isScheduled) { From f949a899c4442545825d478c1cd98c12b1a05343 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 27 Feb 2024 10:18:50 +0100 Subject: [PATCH 29/44] Fix/async animation 2 (#2528) * Refactor * Latest * Latest * Latest * Latest * Latest * Latest * Latest * Latest * Fixing tests * Latest * Fixing tests --- dev/benchmarks/cold-start-framer-motion.html | 82 +-- dev/examples/Animation-animate.tsx | 18 +- dev/tests/waapi-cancel.tsx | 6 +- packages/framer-motion-3d/package.json | 2 +- .../src/render/utils/read-value.ts | 1 - .../cypress/integration/waapi.ts | 3 +- packages/framer-motion/package.json | 12 +- packages/framer-motion/rollup.config.js | 2 +- .../src/animation/GroupPlaybackControls.ts | 2 +- ...e-waapi.test.tsx => animate-waapi.test.ts} | 0 .../src/animation/__tests__/animate.test.tsx | 2 +- .../animators/AcceleratedAnimation.ts | 323 ++++++++++++ .../src/animation/animators/BaseAnimation.ts | 156 ++++++ .../animators/MainThreadAnimation.ts | 471 ++++++++++++++++++ .../MainThreadAnimation.test.ts} | 41 +- .../animators/{js => }/__tests__/utils.ts | 2 +- .../{js => drivers}/driver-frameloop.ts | 0 .../animators/{js => drivers}/types.ts | 0 .../src/animation/animators/js/index.ts | 450 ----------------- .../animation/animators/utils/can-animate.ts | 12 +- .../waapi/create-accelerated-animation.ts | 320 ------------ .../src/animation/animators/waapi/index.ts | 2 +- .../generators/__tests__/keyframes.test.ts | 2 +- .../generators/__tests__/spring.test.ts | 2 +- .../src/animation/interfaces/motion-value.ts | 36 +- packages/framer-motion/src/animation/types.ts | 8 +- packages/framer-motion/src/index.ts | 2 +- .../framer-motion/src/projection/index.ts | 2 +- .../src/render/dom/DOMKeyframesResolver.ts | 14 +- .../src/render/utils/KeyframesResolver.ts | 19 +- .../src/value/__tests__/use-spring.test.tsx | 2 +- .../framer-motion/src/value/use-spring.ts | 11 +- 32 files changed, 1123 insertions(+), 882 deletions(-) rename packages/framer-motion/src/animation/__tests__/{animate-waapi.test.tsx => animate-waapi.test.ts} (100%) create mode 100644 packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts create mode 100644 packages/framer-motion/src/animation/animators/BaseAnimation.ts create mode 100644 packages/framer-motion/src/animation/animators/MainThreadAnimation.ts rename packages/framer-motion/src/animation/animators/{js/__tests__/animate.test.ts => __tests__/MainThreadAnimation.test.ts} (97%) rename packages/framer-motion/src/animation/animators/{js => }/__tests__/utils.ts (94%) rename packages/framer-motion/src/animation/animators/{js => drivers}/driver-frameloop.ts (100%) rename packages/framer-motion/src/animation/animators/{js => drivers}/types.ts (100%) delete mode 100644 packages/framer-motion/src/animation/animators/js/index.ts delete mode 100644 packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts diff --git a/dev/benchmarks/cold-start-framer-motion.html b/dev/benchmarks/cold-start-framer-motion.html index 05c4f230c4..b7b4557853 100644 --- a/dev/benchmarks/cold-start-framer-motion.html +++ b/dev/benchmarks/cold-start-framer-motion.html @@ -30,7 +30,7 @@ .box { width: 10px; height: 100px; - background-color: #fff; + background-color: #0f0; } @@ -40,7 +40,7 @@ diff --git a/dev/examples/Animation-animate.tsx b/dev/examples/Animation-animate.tsx index 109ab04260..2aea468f50 100644 --- a/dev/examples/Animation-animate.tsx +++ b/dev/examples/Animation-animate.tsx @@ -24,16 +24,18 @@ const Child = ({ setState }: any) => { useEffect(() => { const controls = animate([ - [ - "div", - { x: 500, opacity: 0 }, - { type: "spring", duration: 1, bounce: 0 }, - ], + ["div", { x: 500 }, { type: "spring", duration: 1, bounce: 0 }], ]) - controls.then(() => { - controls.play() - }) + controls.play() + controls.pause() + controls.time = 0.1 + + setTimeout(() => controls.play(), 1000) + + // controls.then(() => { + // controls.play() + // }) return () => controls.stop() }, [target]) diff --git a/dev/tests/waapi-cancel.tsx b/dev/tests/waapi-cancel.tsx index c24b12b25b..207446a2d8 100644 --- a/dev/tests/waapi-cancel.tsx +++ b/dev/tests/waapi-cancel.tsx @@ -19,7 +19,11 @@ const Container = styled.section` export const App = () => { useEffect(() => { - const controls = animate("#box", { opacity: [0, 1] }, { duration: 1 }) + const controls = animate( + "#box", + { x: [0, 100], opacity: [0, 1] }, + { duration: 1 } + ) controls.cancel() controls.complete() diff --git a/packages/framer-motion-3d/package.json b/packages/framer-motion-3d/package.json index ab23634e72..dd1f31a1d8 100644 --- a/packages/framer-motion-3d/package.json +++ b/packages/framer-motion-3d/package.json @@ -35,7 +35,7 @@ "scripts": { "eslint": "yarn run lint", "lint": "yarn eslint src/**/*.ts", - "test": "yarn test-unit", + "test": "", "test-ci": "yarn test-unit", "test-unit": "jest --coverage --config jest.config.json --max-workers=2", "build": "yarn clean && tsc -p . && rollup -c", diff --git a/packages/framer-motion-3d/src/render/utils/read-value.ts b/packages/framer-motion-3d/src/render/utils/read-value.ts index 896d665286..0088d55d73 100644 --- a/packages/framer-motion-3d/src/render/utils/read-value.ts +++ b/packages/framer-motion-3d/src/render/utils/read-value.ts @@ -37,7 +37,6 @@ function readAnimatableValue(value?: Color) { } export function readThreeValue(instance: Object3DNode, name: string) { - // console.log(name, readers[name], readAnimatableValue(instance[name])) return readers[name] ? readers[name](instance) : readAnimatableValue(instance[name]) || 0 diff --git a/packages/framer-motion/cypress/integration/waapi.ts b/packages/framer-motion/cypress/integration/waapi.ts index 5800c60bf5..b3791477e7 100644 --- a/packages/framer-motion/cypress/integration/waapi.ts +++ b/packages/framer-motion/cypress/integration/waapi.ts @@ -4,7 +4,8 @@ describe("waapi", () => { .wait(100) .get("#box") .should(([$element]: any) => { - expect(getComputedStyle($element).opacity).to.equal("0") + expect(getComputedStyle($element).opacity).to.equal("1") + expect($element.getBoundingClientRect().left).to.equal(200) }) }) }) diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index 3dce0311c3..496e24342d 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -86,7 +86,7 @@ "bundlesize": [ { "path": "./dist/size-rollup-motion.js", - "maxSize": "31.4 kB" + "maxSize": "31.7 kB" }, { "path": "./dist/size-rollup-m.js", @@ -94,15 +94,15 @@ }, { "path": "./dist/size-rollup-dom-animation.js", - "maxSize": "15.35 kB" + "maxSize": "15.8 kB" }, { "path": "./dist/size-rollup-dom-max.js", - "maxSize": "26.8 kB" + "maxSize": "27.2 kB" }, { "path": "./dist/size-rollup-animate.js", - "maxSize": "16.6 kB" + "maxSize": "17 kB" }, { "path": "./dist/size-webpack-m.js", @@ -110,11 +110,11 @@ }, { "path": "./dist/size-webpack-dom-animation.js", - "maxSize": "20 kB" + "maxSize": "20.5 kB" }, { "path": "./dist/size-webpack-dom-max.js", - "maxSize": "32.2 kB" + "maxSize": "33 kB" } ], "gitHead": "f99b162917a2f171dbabe32a5baf63990b83318b" diff --git a/packages/framer-motion/rollup.config.js b/packages/framer-motion/rollup.config.js index f709fe4f56..19b6995e1f 100644 --- a/packages/framer-motion/rollup.config.js +++ b/packages/framer-motion/rollup.config.js @@ -86,7 +86,7 @@ const umdDomProd = Object.assign({}, umd, { resolve(), replaceSettings("production"), pureClass, - // terser({ output: { comments: false } }), + terser({ output: { comments: false } }), ], }) diff --git a/packages/framer-motion/src/animation/GroupPlaybackControls.ts b/packages/framer-motion/src/animation/GroupPlaybackControls.ts index ac00f53182..ec0608ed14 100644 --- a/packages/framer-motion/src/animation/GroupPlaybackControls.ts +++ b/packages/framer-motion/src/animation/GroupPlaybackControls.ts @@ -35,8 +35,8 @@ export class GroupPlaybackControls implements AnimationPlaybackControls { if (supportsScrollTimeline() && animation.attachTimeline) { animation.attachTimeline(timeline) } else { - animation.play() animation.pause() + return observeTimeline((progress) => { animation.time = animation.duration * progress }, timeline) diff --git a/packages/framer-motion/src/animation/__tests__/animate-waapi.test.tsx b/packages/framer-motion/src/animation/__tests__/animate-waapi.test.ts similarity index 100% rename from packages/framer-motion/src/animation/__tests__/animate-waapi.test.tsx rename to packages/framer-motion/src/animation/__tests__/animate-waapi.test.ts diff --git a/packages/framer-motion/src/animation/__tests__/animate.test.tsx b/packages/framer-motion/src/animation/__tests__/animate.test.tsx index 022d346379..a92fc73fbc 100644 --- a/packages/framer-motion/src/animation/__tests__/animate.test.tsx +++ b/packages/framer-motion/src/animation/__tests__/animate.test.tsx @@ -5,7 +5,7 @@ import { motion, MotionGlobalConfig } from "../.." import { animate } from "../animate" import { useMotionValue } from "../../value/use-motion-value" import { motionValue, MotionValue } from "../../value" -import { syncDriver } from "../animators/js/__tests__/utils" +import { syncDriver } from "../animators/__tests__/utils" const duration = 0.001 diff --git a/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts b/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts new file mode 100644 index 0000000000..33ca9350c4 --- /dev/null +++ b/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts @@ -0,0 +1,323 @@ +import { EasingDefinition } from "../../easing/types" +import { time } from "../../frameloop/sync-time" +import { DOMKeyframesResolver } from "../../render/dom/DOMKeyframesResolver" +import { ResolvedKeyframes } from "../../render/utils/KeyframesResolver" +import { memo } from "../../utils/memo" +import { noop } from "../../utils/noop" +import { + millisecondsToSeconds, + secondsToMilliseconds, +} from "../../utils/time-conversion" +import { MotionValue } from "../../value" +import { ValueAnimationOptions } from "../types" +import { + BaseAnimation, + ValueAnimationOptionsWithDefaults, +} from "./BaseAnimation" +import { MainThreadAnimation } from "./MainThreadAnimation" +import { animateStyle } from "./waapi" +import { isWaapiSupportedEasing } from "./waapi/easing" +import { getFinalKeyframe } from "./waapi/utils/get-final-keyframe" + +const supportsWaapi = memo(() => + Object.hasOwnProperty.call(Element.prototype, "animate") +) + +/** + * A list of values that can be hardware-accelerated. + */ +const acceleratedValues = new Set([ + "opacity", + "clipPath", + "filter", + "transform", +]) + +/** + * 10ms is chosen here as it strikes a balance between smooth + * results (more than one keyframe per frame at 60fps) and + * keyframe quantity. + */ +const sampleDelta = 10 //ms + +/** + * Implement a practical max duration for keyframe generation + * to prevent infinite loops + */ +const maxDuration = 20_000 + +function requiresPregeneratedKeyframes( + options: ValueAnimationOptions +) { + return ( + options.type === "spring" || + options.name === "backgroundColor" || + !isWaapiSupportedEasing(options.ease) + ) +} + +function pregenerateKeyframes( + keyframes: ResolvedKeyframes, + options: ValueAnimationOptions +) { + const sampleAnimation = new MainThreadAnimation({ + ...options, + keyframes, + repeat: 0, + delay: 0, + }) + + let state = { done: false, value: keyframes[0] } + const pregeneratedKeyframes: T[] = [] + + /** + * Bail after 20 seconds of pre-generated keyframes as it's likely + * we're heading for an infinite loop. + */ + let t = 0 + while (!state.done && t < maxDuration) { + state = sampleAnimation.sample(t) + pregeneratedKeyframes.push(state.value) + t += sampleDelta + } + + return { + times: undefined, + keyframes: pregeneratedKeyframes, + duration: t - sampleDelta, + ease: "linear" as EasingDefinition, + } +} + +export interface AcceleratedValueAnimationOptions< + T extends string | number = number +> extends ValueAnimationOptions { + name: string + motionValue: MotionValue +} + +interface ResolvedAcceleratedAnimation { + animation: Animation + duration: number + keyframes: string[] | number[] +} + +export class AcceleratedAnimation< + T extends string | number +> extends BaseAnimation { + protected options: ValueAnimationOptionsWithDefaults & { + name: string + motionValue: MotionValue + } + + constructor(options: ValueAnimationOptions) { + super(options) + + const { name, motionValue, keyframes } = this.options + this.resolver = new DOMKeyframesResolver( + keyframes, + (resolvedKeyframes: ResolvedKeyframes) => + this.onKeyframesResolved(resolvedKeyframes), + name, + motionValue + ) + + this.resolver.scheduleResolve() + } + + private pendingTimeline: any + + protected initPlayback( + keyframes: ResolvedKeyframes + ): ResolvedAcceleratedAnimation { + let duration = this.options.duration || 300 + + /** + * If this animation needs pre-generated keyframes then generate. + */ + if (requiresPregeneratedKeyframes(this.options)) { + const { onComplete, onUpdate, motionValue, ...options } = + this.options + const pregeneratedAnimation = pregenerateKeyframes( + keyframes, + options + ) + + keyframes = pregeneratedAnimation.keyframes + duration = pregeneratedAnimation.duration + this.options.times = pregeneratedAnimation.times + this.options.ease = pregeneratedAnimation.ease + } + + const { motionValue, name } = this.options + const animation = animateStyle( + motionValue.owner!.current as unknown as HTMLElement, + name, + keyframes as string[], + this.options + ) + + // Override the browser calculated startTime with one synchronised to other JS + // and WAAPI animations starting this event loop. + animation.startTime = time.now() + + if (this.pendingTimeline) { + animation.timeline = this.pendingTimeline + this.pendingTimeline = undefined + } else { + /** + * Prefer the `onfinish` prop as it's more widely supported than + * the `finished` promise. + * + * Here, we synchronously set the provided MotionValue to the end + * keyframe. If we didn't, when the WAAPI animation is finished it would + * be removed from the element which would then revert to its old styles. + */ + animation.onfinish = () => { + const { onComplete } = this.options + motionValue.set(getFinalKeyframe(keyframes, this.options)) + onComplete && onComplete() + this.cancel() + // frame.update(cancelAnimation) + this.resolveFinishedPromise() + this.updateFinishedPromise() + } + } + + return { + animation, + duration, + keyframes: keyframes as string[] | number[], + } + } + + get duration() { + const { duration } = this.resolved + return millisecondsToSeconds(duration) + } + + get time() { + const { animation } = this.resolved + return millisecondsToSeconds((animation.currentTime as number) || 0) + } + + set time(newTime: number) { + const { animation } = this.resolved + animation.currentTime = secondsToMilliseconds(newTime) + } + + get speed() { + const { animation } = this.resolved + return animation.playbackRate + } + + set speed(newSpeed: number) { + const { animation } = this.resolved + animation.playbackRate = newSpeed + } + + get state() { + const { animation } = this.resolved + return animation.playState + } + + /** + * Replace the default DocumentTimeline with another AnimationTimeline. + * Currently used for scroll animations. + */ + attachTimeline(timeline: any) { + if (!this._resolved) { + this.pendingTimeline = timeline + } else { + const { animation } = this.resolved + + animation.timeline = timeline + animation.onfinish = null + } + + return noop + } + + play() { + if (this.isStopped) return + + const { animation } = this.resolved + animation.play() + } + + pause() { + const { animation } = this.resolved + animation.pause() + } + + stop() { + this.isStopped = true + const { animation, keyframes } = this.resolved + + if ( + animation.playState === "idle" || + animation.playState === "finished" + ) { + return + } + + /** + * WAAPI doesn't natively have any interruption capabilities. + * + * Rather than read commited styles back out of the DOM, we can + * create a renderless JS animation and sample it twice to calculate + * its current value, "previous" value, and therefore allow + * Motion to calculate velocity for any subsequent animation. + */ + if (this.time) { + const { motionValue, onUpdate, onComplete, ...options } = + this.options + + const sampleAnimation = new MainThreadAnimation({ + ...options, + keyframes, + }) + + motionValue.setWithVelocity( + sampleAnimation.sample(this.time - sampleDelta).value, + sampleAnimation.sample(this.time).value, + sampleDelta + ) + } + + this.cancel() + } + + complete() { + this.resolved.animation.finish() + } + + cancel() { + this.resolved.animation.cancel() + } + + static supports( + options: ValueAnimationOptions + ): options is AcceleratedValueAnimationOptions { + const { motionValue, name, repeatDelay, repeatType, damping, type } = + options + + return ( + supportsWaapi() && + name && + acceleratedValues.has(name) && + motionValue && + motionValue.owner && + motionValue.owner.current instanceof HTMLElement && + /** + * If we're outputting values to onUpdate then we can't use WAAPI as there's + * no way to read the value from WAAPI every frame. + */ + !motionValue.owner.getProps().onUpdate && + !repeatDelay && + repeatType !== "mirror" && + damping !== 0 && + type !== "inertia" + ) + } +} diff --git a/packages/framer-motion/src/animation/animators/BaseAnimation.ts b/packages/framer-motion/src/animation/animators/BaseAnimation.ts new file mode 100644 index 0000000000..1c8633ff00 --- /dev/null +++ b/packages/framer-motion/src/animation/animators/BaseAnimation.ts @@ -0,0 +1,156 @@ +import { + KeyframeResolver, + ResolvedKeyframes, + flushKeyframeResolvers, +} from "../../render/utils/KeyframesResolver" +import { instantAnimationState } from "../../utils/use-instant-transition-state" +import { + AnimationPlaybackControls, + RepeatType, + ValueAnimationOptions, +} from "../types" +import { canAnimate } from "./utils/can-animate" +import { getFinalKeyframe } from "./waapi/utils/get-final-keyframe" + +export interface ValueAnimationOptionsWithDefaults + extends ValueAnimationOptions { + autoplay: boolean + delay: number + repeat: number + repeatDelay: number + repeatType: RepeatType +} + +export abstract class BaseAnimation + implements AnimationPlaybackControls +{ + // Persistent reference to the options used to create this animation + protected options: ValueAnimationOptionsWithDefaults + + // Resolve the current finished promise + protected resolveFinishedPromise: VoidFunction + + // A promise that resolves when the animation is complete + protected currentFinishedPromise: Promise + + // Track whether the animation has been stopped. Stopped animations won't restart. + protected isStopped = false + + // Internal reference to defered resolved keyframes and animation-specific data returned from initPlayback. + protected _resolved: Resolved & { keyframes: ResolvedKeyframes } + + // Reference to the active keyframes resolver. + protected resolver: KeyframeResolver + + constructor({ + autoplay = true, + delay = 0, + type = "keyframes", + repeat = 0, + repeatDelay = 0, + repeatType = "loop", + ...options + }: ValueAnimationOptions) { + this.options = { + autoplay, + delay, + type, + repeat, + repeatDelay, + repeatType, + ...options, + } + + this.updateFinishedPromise() + } + + protected abstract initPlayback(keyframes: ResolvedKeyframes): Resolved + + abstract play(): void + abstract pause(): void + abstract stop(): void + abstract cancel(): void + abstract complete(): void + abstract get speed(): number + abstract set speed(newSpeed: number) + abstract get time(): number + abstract set time(newTime: number) + abstract get duration(): number + abstract get state(): AnimationPlayState + + /** + * A getter for resolved data. If keyframes are not yet resolved, accessing + * this.resolved will synchronously flush all pending keyframe resolvers. + * This is a deoptimisation, but at its worst still batches read/writes. + */ + get resolved(): Resolved & { keyframes: ResolvedKeyframes } { + // TODO Leave only for tests + if (this.isInitialising) + throw new Error("Cannot access `resolved` while initialising.") + if (!this._resolved) flushKeyframeResolvers() + + return this._resolved + } + + /** + * A method to be called when the keyframes resolver completes. This method + * will check if its possible to run the animation and, if not, skip it. + * Otherwise, it will call initPlayback on the implementing class. + */ + private isInitialising = false + protected onKeyframesResolved(keyframes: ResolvedKeyframes) { + this.isInitialising = true + const { name, type, velocity, delay, onComplete, onUpdate } = + this.options + + /** + * If we can't animate this value with the resolved keyframes + * then we should complete it immediately. + */ + if (!canAnimate(keyframes, name, type, velocity)) { + // Finish immediately + if (instantAnimationState.current || !delay) { + const finalKeyframe = getFinalKeyframe(keyframes, this.options) + onUpdate?.(finalKeyframe) + onComplete?.() + this.resolveFinishedPromise() + this.updateFinishedPromise() + + return + } + // Finish after a delay + else { + this.options.duration = 0 + } + } + + this._resolved = { + keyframes, + ...this.initPlayback(keyframes), + } + + this.isInitialising = false + + this.onPostResolved() + } + + onPostResolved() {} + + /** + * Allows the returned animation to be awaited or promise-chained. Currently + * resolves when the animation finishes at all but in a future update could/should + * reject if its cancels. + */ + then(resolve: VoidFunction, reject?: VoidFunction) { + return this.currentFinishedPromise.then(resolve, reject) + } + + protected updateFinishedPromise() { + this.currentFinishedPromise = new Promise((resolve) => { + this.resolveFinishedPromise = () => { + resolve() + this.updateFinishedPromise() + } + }) + } +} diff --git a/packages/framer-motion/src/animation/animators/MainThreadAnimation.ts b/packages/framer-motion/src/animation/animators/MainThreadAnimation.ts new file mode 100644 index 0000000000..24b5dc782c --- /dev/null +++ b/packages/framer-motion/src/animation/animators/MainThreadAnimation.ts @@ -0,0 +1,471 @@ +import { + KeyframeResolver as DefaultKeyframeResolver, + ResolvedKeyframes, +} from "../../render/utils/KeyframesResolver" +import { spring } from "../generators/spring/index" +import { inertia } from "../generators/inertia" +import { keyframes as keyframesGeneratorFactory } from "../generators/keyframes" +import { ValueAnimationOptions } from "../types" +import { BaseAnimation } from "./BaseAnimation" +import { AnimationState, KeyframeGenerator } from "../generators/types" +import { pipe } from "../../utils/pipe" +import { mix } from "../../utils/mix" +import { calcGeneratorDuration } from "../generators/utils/calc-duration" +import { DriverControls } from "./drivers/types" +import { + millisecondsToSeconds, + secondsToMilliseconds, +} from "../../utils/time-conversion" +import { clamp } from "../../utils/clamp" +import { invariant } from "../../utils/errors" +import { frameloopDriver } from "./drivers/driver-frameloop" + +type GeneratorFactory = ( + options: ValueAnimationOptions +) => KeyframeGenerator + +const generators: { [key: string]: GeneratorFactory } = { + decay: inertia, + inertia, + tween: keyframesGeneratorFactory, + keyframes: keyframesGeneratorFactory, + spring, +} + +const percentToProgress = (percent: number) => percent / 100 + +interface ResolvedData { + generator: KeyframeGenerator + mirroredGenerator: KeyframeGenerator | undefined + mapPercentToKeyframes: ((v: number) => T) | undefined + calculatedDuration: number + resolvedDuration: number + totalDuration: number +} + +export class MainThreadAnimation< + T extends string | number +> extends BaseAnimation> { + private driver?: DriverControls + + private holdTime: number | null = null + + private startTime: number | null = null + + private cancelTime: number | null = null + + private currentTime: number = 0 + + private playbackSpeed = 1 + + private pendingPlayState: AnimationPlayState = "running" + + constructor({ + KeyframeResolver = DefaultKeyframeResolver, + ...options + }: ValueAnimationOptions) { + super(options) + + const { name, motionValue, keyframes } = this.options + const onResolved = (resolvedKeyframes: ResolvedKeyframes) => + this.onKeyframesResolved(resolvedKeyframes) + + if (name && motionValue && motionValue.owner) { + this.resolver = (motionValue.owner as any).resolveKeyframes( + keyframes, + onResolved, + name, + motionValue + ) + } else { + this.resolver = new KeyframeResolver( + keyframes, + onResolved, + name, + motionValue + ) + } + + this.resolver.scheduleResolve() + } + + protected initPlayback(keyframes: ResolvedKeyframes) { + const { + type = "keyframes", + repeat = 0, + repeatDelay = 0, + repeatType, + velocity = 0, + } = this.options + + const generatorFactory = generators[type] || keyframesGeneratorFactory + + let mapPercentToKeyframes: ((v: number) => T) | undefined + let mirroredGenerator: KeyframeGenerator | undefined + + if ( + generatorFactory !== keyframesGeneratorFactory && + typeof keyframes[0] !== "number" + ) { + if (process.env.NODE_ENV !== "production") { + invariant( + keyframes.length === 2, + `Only two keyframes currently supported with spring and inertia animations. Trying to animate ${keyframes}` + ) + } + + mapPercentToKeyframes = pipe( + percentToProgress, + mix(keyframes[0], keyframes[1]) + ) as (t: number) => T + + keyframes = [0 as T, 100 as T] + } + + const generator = generatorFactory({ ...this.options, keyframes }) + + if (repeatType === "mirror") { + mirroredGenerator = generatorFactory({ + ...this.options, + keyframes: [...keyframes].reverse(), + velocity: -velocity, + }) + } + + /** + * If duration is undefined and we have repeat options, + * we need to calculate a duration from the generator. + * + * We set it to the generator itself to cache the duration. + * Any timeline resolver will need to have already precalculated + * the duration by this step. + */ + if (generator.calculatedDuration === null) { + generator.calculatedDuration = calcGeneratorDuration(generator) + } + + const { calculatedDuration } = generator + const resolvedDuration = calculatedDuration + repeatDelay + const totalDuration = resolvedDuration * (repeat + 1) - repeatDelay + + return { + generator, + mirroredGenerator, + mapPercentToKeyframes, + calculatedDuration, + resolvedDuration, + totalDuration, + } + } + + onPostResolved() { + const { autoplay = true } = this.options + + this.play() + + if (this.pendingPlayState === "paused" || !autoplay) { + this.pause() + } else { + this.state = this.pendingPlayState + } + } + + tick(timestamp: number, sample = false) { + const { + generator, + mirroredGenerator, + mapPercentToKeyframes, + keyframes, + calculatedDuration, + totalDuration, + resolvedDuration, + } = this.resolved + + if (this.startTime === null) return generator.next(0) + + const { delay, repeat, repeatType, repeatDelay, onUpdate } = + this.options + + /** + * requestAnimationFrame timestamps can come through as lower than + * the startTime as set by performance.now(). Here we prevent this, + * though in the future it could be possible to make setting startTime + * a pending operation that gets resolved here. + */ + if (this.speed > 0) { + this.startTime = Math.min(this.startTime, timestamp) + } else if (this.speed < 0) { + this.startTime = Math.min( + timestamp - totalDuration / this.speed, + this.startTime + ) + } + + // Update currentTime + if (sample) { + this.currentTime = timestamp + } else if (this.holdTime !== null) { + this.currentTime = this.holdTime + } else { + // Rounding the time because floating point arithmetic is not always accurate, e.g. 3000.367 - 1000.367 = + // 2000.0000000000002. This is a problem when we are comparing the currentTime with the duration, for + // example. + this.currentTime = + Math.round(timestamp - this.startTime) * this.speed + } + + // Rebase on delay + const timeWithoutDelay = + this.currentTime - delay * (this.speed >= 0 ? 1 : -1) + const isInDelayPhase = + this.speed >= 0 + ? timeWithoutDelay < 0 + : timeWithoutDelay > totalDuration + this.currentTime = Math.max(timeWithoutDelay, 0) + + // If this animation has finished, set the current time to the total duration. + if (this.state === "finished" && this.holdTime === null) { + this.currentTime = totalDuration + } + + let elapsed = this.currentTime + + let frameGenerator = generator + + if (repeat) { + /** + * Get the current progress (0-1) of the animation. If t is > + * than duration we'll get values like 2.5 (midway through the + * third iteration) + */ + const progress = + Math.min(this.currentTime, totalDuration) / resolvedDuration + + /** + * Get the current iteration (0 indexed). For instance the floor of + * 2.5 is 2. + */ + let currentIteration = Math.floor(progress) + + /** + * Get the current progress of the iteration by taking the remainder + * so 2.5 is 0.5 through iteration 2 + */ + let iterationProgress = progress % 1.0 + + /** + * If iteration progress is 1 we count that as the end + * of the previous iteration. + */ + if (!iterationProgress && progress >= 1) { + iterationProgress = 1 + } + + iterationProgress === 1 && currentIteration-- + + currentIteration = Math.min(currentIteration, repeat + 1) + + /** + * Reverse progress if we're not running in "normal" direction + */ + + const isOddIteration = Boolean(currentIteration % 2) + if (isOddIteration) { + if (repeatType === "reverse") { + iterationProgress = 1 - iterationProgress + if (repeatDelay) { + iterationProgress -= repeatDelay / resolvedDuration + } + } else if (repeatType === "mirror") { + frameGenerator = mirroredGenerator! + } + } + + elapsed = clamp(0, 1, iterationProgress) * resolvedDuration + } + + /** + * If we're in negative time, set state as the initial keyframe. + * This prevents delay: x, duration: 0 animations from finishing + * instantly. + */ + const state = isInDelayPhase + ? { done: false, value: keyframes[0] } + : frameGenerator.next(elapsed) + + if (mapPercentToKeyframes) { + state.value = mapPercentToKeyframes(state.value as number) + } + + let { done } = state + + if (!isInDelayPhase && calculatedDuration !== null) { + done = + this.speed >= 0 + ? this.currentTime >= totalDuration + : this.currentTime <= 0 + } + + const isAnimationFinished = + this.holdTime === null && + (this.state === "finished" || (this.state === "running" && done)) + + if (onUpdate) { + onUpdate(state.value) + } + + if (isAnimationFinished) { + this.finish() + } + + return state + } + + state: AnimationPlayState = "idle" + + get duration() { + return millisecondsToSeconds(this.resolved.calculatedDuration) + } + + get time() { + return millisecondsToSeconds(this.currentTime) + } + + set time(newTime: number) { + newTime = secondsToMilliseconds(newTime) + this.currentTime = newTime + + if (this.holdTime !== null || this.speed === 0) { + this.holdTime = newTime + } else if (this.driver) { + this.startTime = this.driver.now() - newTime / this.speed + } + } + + get speed() { + return this.playbackSpeed + } + + set speed(newSpeed: number) { + const hasChanged = this.playbackSpeed !== newSpeed + this.playbackSpeed = newSpeed + if (hasChanged) { + this.time = millisecondsToSeconds(this.currentTime) + } + } + + play() { + if (!this.resolver.isScheduled) { + this.resolver.resume() + } + + if (!this._resolved) { + this.pendingPlayState = "running" + return + } + + if (this.isStopped) return + + const { driver = frameloopDriver, onPlay } = this.options + + if (!this.driver) { + this.driver = driver((timestamp) => this.tick(timestamp)) + } + + onPlay && onPlay() + + const now = this.driver.now() + + if (this.holdTime !== null) { + this.startTime = now - this.holdTime + } else if (!this.startTime || this.state === "finished") { + this.startTime = now + } + + if (this.state === "finished") { + this.updateFinishedPromise() + } + + this.cancelTime = this.startTime + this.holdTime = null + + /** + * Set playState to running only after we've used it in + * the previous logic. + */ + this.state = "running" + + this.driver.start() + } + + pause() { + if (!this._resolved) { + this.pendingPlayState = "paused" + return + } + + this.state = "paused" + this.holdTime = this.currentTime ?? 0 + } + + stop() { + this.isStopped = true + if (this.state === "idle") return + + this.state = "idle" + const { onStop } = this.options + onStop && onStop() + this.teardown() + } + + complete() { + if (this.state !== "running") { + this.play() + } + + this.pendingPlayState = this.state = "finished" + this.holdTime = null + } + + finish() { + this.teardown() + this.state = "finished" + + const { onComplete } = this.options + onComplete && onComplete() + } + + cancel() { + if (this.cancelTime !== null) { + this.tick(this.cancelTime) + } + this.teardown() + } + + private teardown() { + this.state = "idle" + this.stopDriver() + this.resolveFinishedPromise() + this.updateFinishedPromise() + this.startTime = this.cancelTime = null + this.resolver.cancel() + } + + private stopDriver() { + if (!this.driver) return + this.driver.stop() + this.driver = undefined + } + + sample(time: number): AnimationState { + this.startTime = 0 + return this.tick(time, true) + } +} + +// Legacy interface +export function animateValue( + options: ValueAnimationOptions +): MainThreadAnimation { + return new MainThreadAnimation(options) +} diff --git a/packages/framer-motion/src/animation/animators/js/__tests__/animate.test.ts b/packages/framer-motion/src/animation/animators/__tests__/MainThreadAnimation.test.ts similarity index 97% rename from packages/framer-motion/src/animation/animators/js/__tests__/animate.test.ts rename to packages/framer-motion/src/animation/animators/__tests__/MainThreadAnimation.test.ts index e0a8f37619..47e99e1f5e 100644 --- a/packages/framer-motion/src/animation/animators/js/__tests__/animate.test.ts +++ b/packages/framer-motion/src/animation/animators/__tests__/MainThreadAnimation.test.ts @@ -1,19 +1,26 @@ -import { animateValue } from "../" -import { reverseEasing } from "../../../../easing/modifiers/reverse" -import { nextFrame } from "../../../../gestures/__tests__/utils" -import { noop } from "../../../../utils/noop" -import { ValueAnimationOptions } from "../../../types" +import { MainThreadAnimation, animateValue } from "../MainThreadAnimation" +import { reverseEasing } from "../../../easing/modifiers/reverse" +import { nextFrame } from "../../../gestures/__tests__/utils" +import { noop } from "../../../utils/noop" +import { ValueAnimationOptions } from "../../types" import { syncDriver } from "./utils" +import { KeyframeResolver } from "../../../render/utils/KeyframesResolver" const linear = noop +class AsyncKeyframesResolver extends KeyframeResolver { + constructor(...args: [any, any, any, any, any]) { + super(...args, true) + } +} + function testAnimate( options: ValueAnimationOptions, expected: V[], resolve: () => void ) { const output: V[] = [] - animateValue({ + new MainThreadAnimation({ driver: syncDriver(20), duration: 100, ease: linear, @@ -27,7 +34,7 @@ function testAnimate( }) } -describe("animate", () => { +describe("MainThreadAnimation", () => { test("Correctly performs an animation with default settings", async () => { return new Promise((resolve) => testAnimate( @@ -1231,6 +1238,26 @@ describe("animate", () => { expect(output).toEqual([0, 20, 40, 100]) }) + test("Correctly completes an animation with async resolver", async () => { + const output: number[] = [] + + const animation = animateValue({ + KeyframeResolver: AsyncKeyframesResolver as any, + keyframes: [0, 100], + driver: syncDriver(20), + duration: 100, + ease: linear, + onUpdate: (v) => output.push(v), + }) + + animation.cancel() + animation.complete() + + await animation + + expect(output).toEqual([100]) + }) + test("Updates speed to half speed", async () => { const output: number[] = [] diff --git a/packages/framer-motion/src/animation/animators/js/__tests__/utils.ts b/packages/framer-motion/src/animation/animators/__tests__/utils.ts similarity index 94% rename from packages/framer-motion/src/animation/animators/js/__tests__/utils.ts rename to packages/framer-motion/src/animation/animators/__tests__/utils.ts index 80f3efd986..193027dd44 100644 --- a/packages/framer-motion/src/animation/animators/js/__tests__/utils.ts +++ b/packages/framer-motion/src/animation/animators/__tests__/utils.ts @@ -1,4 +1,4 @@ -import { KeyframeGenerator } from "../../../generators/types" +import { KeyframeGenerator } from "../../generators/types" export const syncDriver = (interval = 10) => { const driver = (update: (v: number) => void) => { diff --git a/packages/framer-motion/src/animation/animators/js/driver-frameloop.ts b/packages/framer-motion/src/animation/animators/drivers/driver-frameloop.ts similarity index 100% rename from packages/framer-motion/src/animation/animators/js/driver-frameloop.ts rename to packages/framer-motion/src/animation/animators/drivers/driver-frameloop.ts diff --git a/packages/framer-motion/src/animation/animators/js/types.ts b/packages/framer-motion/src/animation/animators/drivers/types.ts similarity index 100% rename from packages/framer-motion/src/animation/animators/js/types.ts rename to packages/framer-motion/src/animation/animators/drivers/types.ts diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts deleted file mode 100644 index 5e4891626e..0000000000 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ /dev/null @@ -1,450 +0,0 @@ -import { AnimationPlaybackControls } from "../../types" -import { keyframes as keyframesGeneratorFactory } from "../../generators/keyframes" -import { spring } from "../../generators/spring/index" -import { inertia } from "../../generators/inertia" -import { AnimationState, KeyframeGenerator } from "../../generators/types" -import { DriverControls } from "./types" -import { ValueAnimationOptions } from "../../types" -import { frameloopDriver } from "./driver-frameloop" -import { clamp } from "../../../utils/clamp" -import { - millisecondsToSeconds, - secondsToMilliseconds, -} from "../../../utils/time-conversion" -import { calcGeneratorDuration } from "../../generators/utils/calc-duration" -import { invariant } from "../../../utils/errors" -import { mix } from "../../../utils/mix" -import { pipe } from "../../../utils/pipe" -import { - KeyframeResolver, - ResolvedKeyframes, - flushKeyframeResolvers, -} from "../../../render/utils/KeyframesResolver" -import { instantAnimationState } from "../../../utils/use-instant-transition-state" -import { getFinalKeyframe } from "../waapi/utils/get-final-keyframe" -import { canAnimate } from "../utils/can-animate" - -type GeneratorFactory = ( - options: ValueAnimationOptions -) => KeyframeGenerator - -const types: { [key: string]: GeneratorFactory } = { - decay: inertia, - inertia, - tween: keyframesGeneratorFactory, - keyframes: keyframesGeneratorFactory, - spring, -} - -export interface MainThreadAnimationControls - extends AnimationPlaybackControls { - sample: (t: number) => AnimationState -} - -const percentToProgress = (percent: number) => percent / 100 - -function defaultResolveKeyframes( - keyframes: V[], - onComplete: OnKeyframesResolved, - name?: string, - motionValue?: any -) { - return new KeyframeResolver(keyframes, onComplete, name, motionValue) -} - -/** - * Animate a single value on the main thread. - * - * This function is written, where functionality overlaps, - * to be largely spec-compliant with WAAPI to allow fungibility - * between the two. - */ -export function animateValue({ - keyframes: unresolvedKeyframes, - name, - element, - motionValue, - autoplay = true, - delay = 0, - driver = frameloopDriver, - type = "keyframes", - repeat = 0, - repeatDelay = 0, - repeatType = "loop", - onPlay, - onStop, - onComplete, - onUpdate, - ...options -}: ValueAnimationOptions): MainThreadAnimationControls { - let playState: AnimationPlayState = "idle" - let holdTime: number | null = null - let startTime: number | null = null - let cancelTime: number | null = null - let speed = 1 - let currentTime = 0 - let resolvedDuration = Infinity - let calculatedDuration: number | null = null - let totalDuration = Infinity - let hasStopped = false - let resolveFinishedPromise: VoidFunction - let currentFinishedPromise: Promise - - let generator: KeyframeGenerator | undefined - let mirroredGenerator: KeyframeGenerator | undefined - /** - * If this isn't the keyframes generator and we've been provided - * strings as keyframes, we need to interpolate these. - * TODO: Support velocity for units and complex value types/ - */ - let mapNumbersToKeyframes: undefined | ((t: number) => V) - - /** - * Resolve the current Promise every time we enter the - * finished state. This is WAAPI-compatible behaviour. - */ - const updateFinishedPromise = () => { - currentFinishedPromise = new Promise((resolve) => { - resolveFinishedPromise = resolve - }) - } - - // Create the first finished promise - updateFinishedPromise() - - const finish = () => { - playState = "finished" - onComplete && onComplete() - stopAnimationDriver() - resolveFinishedPromise() - } - - let animationDriver: DriverControls | undefined - - let initialKeyframe: V - const createGenerator = (keyframes: ResolvedKeyframes) => { - if (!canAnimate(keyframes, name, type, options.velocity)) { - if (instantAnimationState.current || !delay) { - if (onUpdate) { - onUpdate( - getFinalKeyframe(keyframes, { repeat, repeatType }) - ) - } - finish() - return - } else { - options.duration = 0 - } - } - - initialKeyframe = keyframes[0] - const generatorFactory = types[type] || keyframesGeneratorFactory - - if ( - generatorFactory !== keyframesGeneratorFactory && - typeof keyframes[0] !== "number" - ) { - if (process.env.NODE_ENV !== "production") { - invariant( - keyframes.length === 2, - `Only two keyframes currently supported with spring and inertia animations. Trying to animate ${keyframes}` - ) - } - - mapNumbersToKeyframes = pipe( - percentToProgress, - mix(keyframes[0], keyframes[1]) - ) as (t: number) => V - - keyframes = [0, 100] as any - } - - generator = generatorFactory({ ...options, keyframes }) - if (repeatType === "mirror") { - mirroredGenerator = generatorFactory({ - ...options, - keyframes: [...keyframes].reverse(), - velocity: -(options.velocity || 0), - }) - } - - /** - * If duration is undefined and we have repeat options, - * we need to calculate a duration from the generator. - * - * We set it to the generator itself to cache the duration. - * Any timeline resolver will need to have already precalculated - * the duration by this step. - */ - if (generator.calculatedDuration === null && repeat) { - generator.calculatedDuration = calcGeneratorDuration(generator) - } - - calculatedDuration = generator.calculatedDuration - - if (calculatedDuration !== null) { - resolvedDuration = calculatedDuration + repeatDelay - totalDuration = resolvedDuration * (repeat + 1) - repeatDelay - } - - autoplay && play() - } - - const tick = (timestamp: number) => { - if (startTime === null || !generator) return - - /** - * requestAnimationFrame timestamps can come through as lower than - * the startTime as set by performance.now(). Here we prevent this, - * though in the future it could be possible to make setting startTime - * a pending operation that gets resolved here. - */ - if (speed > 0) startTime = Math.min(startTime, timestamp) - if (speed < 0) - startTime = Math.min(timestamp - totalDuration / speed, startTime) - - if (holdTime !== null) { - currentTime = holdTime - } else { - // Rounding the time because floating point arithmetic is not always accurate, e.g. 3000.367 - 1000.367 = - // 2000.0000000000002. This is a problem when we are comparing the currentTime with the duration, for - // example. - currentTime = Math.round(timestamp - startTime) * speed - } - - // Rebase on delay - const timeWithoutDelay = currentTime - delay * (speed >= 0 ? 1 : -1) - const isInDelayPhase = - speed >= 0 ? timeWithoutDelay < 0 : timeWithoutDelay > totalDuration - currentTime = Math.max(timeWithoutDelay, 0) - - /** - * If this animation has finished, set the current time - * to the total duration. - */ - if (playState === "finished" && holdTime === null) { - currentTime = totalDuration - } - - let elapsed = currentTime - - let frameGenerator = generator - - if (repeat) { - /** - * Get the current progress (0-1) of the animation. If t is > - * than duration we'll get values like 2.5 (midway through the - * third iteration) - */ - const progress = - Math.min(currentTime, totalDuration) / resolvedDuration - - /** - * Get the current iteration (0 indexed). For instance the floor of - * 2.5 is 2. - */ - let currentIteration = Math.floor(progress) - - /** - * Get the current progress of the iteration by taking the remainder - * so 2.5 is 0.5 through iteration 2 - */ - let iterationProgress = progress % 1.0 - - /** - * If iteration progress is 1 we count that as the end - * of the previous iteration. - */ - if (!iterationProgress && progress >= 1) { - iterationProgress = 1 - } - - iterationProgress === 1 && currentIteration-- - - currentIteration = Math.min(currentIteration, repeat + 1) - - /** - * Reverse progress if we're not running in "normal" direction - */ - - const isOddIteration = Boolean(currentIteration % 2) - if (isOddIteration) { - if (repeatType === "reverse") { - iterationProgress = 1 - iterationProgress - if (repeatDelay) { - iterationProgress -= repeatDelay / resolvedDuration - } - } else if (repeatType === "mirror") { - frameGenerator = mirroredGenerator! - } - } - - elapsed = clamp(0, 1, iterationProgress) * resolvedDuration - } - - /** - * If we're in negative time, set state as the initial keyframe. - * This prevents delay: x, duration: 0 animations from finishing - * instantly. - */ - const state = isInDelayPhase - ? { done: false, value: initialKeyframe } - : frameGenerator.next(elapsed) - - if (mapNumbersToKeyframes) { - state.value = mapNumbersToKeyframes(state.value as number) - } - - let { done } = state - - if (!isInDelayPhase && calculatedDuration !== null) { - done = speed >= 0 ? currentTime >= totalDuration : currentTime <= 0 - } - - const isAnimationFinished = - holdTime === null && - (playState === "finished" || (playState === "running" && done)) - - if (onUpdate) { - onUpdate(state.value) - } - - if (isAnimationFinished) { - finish() - } - - return state - } - - const stopAnimationDriver = () => { - animationDriver && animationDriver.stop() - animationDriver = undefined - } - - const cancel = () => { - playState = "idle" - stopAnimationDriver() - resolveFinishedPromise() - updateFinishedPromise() - startTime = cancelTime = null - keyframeResolver.cancel() - } - - const play = () => { - if (!generator) flushKeyframeResolvers() - - if (hasStopped) return - - if (!animationDriver) animationDriver = driver(tick) - - const now = animationDriver.now() - - onPlay && onPlay() - - if (holdTime !== null) { - startTime = now - holdTime - } else if (!startTime || playState === "finished") { - startTime = now - } - - if (playState === "finished") { - updateFinishedPromise() - } - - cancelTime = startTime - holdTime = null - - /** - * Set playState to running only after we've used it in - * the previous logic. - */ - playState = "running" - - animationDriver.start() - } - - const keyframeResolver = - element && name && motionValue - ? element.resolveKeyframes( - unresolvedKeyframes, - createGenerator, - name, - motionValue - ) - : new KeyframeResolver( - unresolvedKeyframes, - createGenerator, - name, - motionValue, - element - ) - - const controls = { - then(resolve: VoidFunction, reject?: VoidFunction) { - return currentFinishedPromise.then(resolve, reject) - }, - get time() { - return millisecondsToSeconds(currentTime) - }, - set time(newTime: number) { - newTime = secondsToMilliseconds(newTime) - - currentTime = newTime - if (holdTime !== null || !animationDriver || speed === 0) { - holdTime = newTime - } else { - startTime = animationDriver.now() - newTime / speed - } - }, - get duration() { - // TODO: If no generator, flush pending keyframes - if (!generator) { - return 0 - } - - const duration = - generator.calculatedDuration === null - ? calcGeneratorDuration(generator) - : generator.calculatedDuration - - return millisecondsToSeconds(duration) - }, - get speed() { - return speed - }, - set speed(newSpeed: number) { - if (newSpeed === speed || !animationDriver) return - speed = newSpeed - controls.time = millisecondsToSeconds(currentTime) - }, - get state() { - return playState - }, - play, - pause: () => { - playState = "paused" - holdTime = currentTime - }, - stop: () => { - hasStopped = true - if (playState === "idle") return - playState = "idle" - onStop && onStop() - cancel() - }, - cancel: () => { - if (cancelTime !== null) tick(cancelTime) - cancel() - }, - complete: () => { - playState = "finished" - holdTime === null - }, - sample: (elapsed: number) => { - startTime = 0 - return tick(elapsed)! - }, - } - - return controls -} diff --git a/packages/framer-motion/src/animation/animators/utils/can-animate.ts b/packages/framer-motion/src/animation/animators/utils/can-animate.ts index d7c2606917..b722740b89 100644 --- a/packages/framer-motion/src/animation/animators/utils/can-animate.ts +++ b/packages/framer-motion/src/animation/animators/utils/can-animate.ts @@ -22,16 +22,16 @@ export function canAnimate( * animatable and another that isn't. */ const originKeyframe = keyframes[0] + if (originKeyframe === null) return false + const targetKeyframe = keyframes[keyframes.length - 1] const isOriginAnimatable = isAnimatable(originKeyframe, name) const isTargetAnimatable = isAnimatable(targetKeyframe, name) - if (originKeyframe !== null) { - warning( - isOriginAnimatable === isTargetAnimatable, - `You are trying to animate ${name} from "${originKeyframe}" to "${targetKeyframe}". ${originKeyframe} is not an animatable value - to enable this animation set ${originKeyframe} to a value animatable to ${targetKeyframe} via the \`style\` property.` - ) - } + warning( + isOriginAnimatable === isTargetAnimatable, + `You are trying to animate ${name} from "${originKeyframe}" to "${targetKeyframe}". ${originKeyframe} is not an animatable value - to enable this animation set ${originKeyframe} to a value animatable to ${targetKeyframe} via the \`style\` property.` + ) // Always skip if any of these are true if (!isOriginAnimatable || !isTargetAnimatable) { diff --git a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts deleted file mode 100644 index 333f6d5538..0000000000 --- a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { EasingDefinition } from "../../../easing/types" -import { frame, cancelFrame } from "../../../frameloop" -import type { VisualElement } from "../../../render/VisualElement" -import type { MotionValue } from "../../../value" -import { AnimationPlaybackControls, ValueAnimationOptions } from "../../types" -import { animateStyle } from "." -import { isWaapiSupportedEasing } from "./easing" -import { getFinalKeyframe } from "./utils/get-final-keyframe" -import { animateValue } from "../js" -import { - millisecondsToSeconds, - secondsToMilliseconds, -} from "../../../utils/time-conversion" -import { memo } from "../../../utils/memo" -import { noop } from "../../../utils/noop" -import { - KeyframeResolver, - ResolvedKeyframes, - flushKeyframeResolvers, -} from "../../../render/utils/KeyframesResolver" -import { canAnimate } from "../utils/can-animate" -import { instantAnimationState } from "../../../utils/use-instant-transition-state" - -const supportsWaapi = memo(() => - Object.hasOwnProperty.call(Element.prototype, "animate") -) - -/** - * A list of values that can be hardware-accelerated. - */ -const acceleratedValues = new Set([ - "opacity", - "clipPath", - "filter", - "transform", -]) - -/** - * 10ms is chosen here as it strikes a balance between smooth - * results (more than one keyframe per frame at 60fps) and - * keyframe quantity. - */ -const sampleDelta = 10 //ms - -/** - * Implement a practical max duration for keyframe generation - * to prevent infinite loops - */ -const maxDuration = 20_000 - -const requiresPregeneratedKeyframes = ( - valueName: string, - options: ValueAnimationOptions -) => - options.type === "spring" || - valueName === "backgroundColor" || - !isWaapiSupportedEasing(options.ease) - -export function createAcceleratedAnimation( - value: MotionValue, - valueName: string, - { - onUpdate, - onComplete, - element, - name, - motionValue, - ...options - }: ValueAnimationOptions -): AnimationPlaybackControls | false { - const canAccelerateAnimation = - supportsWaapi() && - acceleratedValues.has(valueName) && - !options.repeatDelay && - options.repeatType !== "mirror" && - options.damping !== 0 && - options.type !== "inertia" - - if (!canAccelerateAnimation) return false - - /** - * TODO: Unify with js/index - */ - let hasStopped = false - let resolveFinishedPromise: VoidFunction - let currentFinishedPromise: Promise - - /** - * Cancelling an animation will write to the DOM. For safety we want to defer - * this until the next `update` frame lifecycle. This flag tracks whether we - * have a pending cancel, if so we shouldn't allow animations to finish. - */ - let pendingCancel = false - - /** - * Resolve the current Promise every time we enter the - * finished state. This is WAAPI-compatible behaviour. - */ - const updateFinishedPromise = () => { - currentFinishedPromise = new Promise((resolve) => { - resolveFinishedPromise = resolve - }) - } - - // Create the first finished promise - updateFinishedPromise() - - let { - keyframes: unresolvedKeyframes, - duration = 300, - ease, - times, - } = options - - let resolvedKeyframes: ResolvedKeyframes - - let animation: Animation | undefined - const createWaapiAnimation = (keyframes: ResolvedKeyframes) => { - resolvedKeyframes = keyframes - const finish = () => { - if (pendingCancel) return - value.set(getFinalKeyframe(keyframes, options)) - onComplete && onComplete() - safeCancel() - } - - if (!canAnimate(keyframes, valueName, options.type, options.velocity)) { - if (instantAnimationState.current || !options.delay) { - finish() - return - } else { - options.duration = 0 - } - } - - /** - * If this animation needs pre-generated keyframes then generate. - */ - if (requiresPregeneratedKeyframes(valueName, options)) { - const sampleAnimation = animateValue({ - ...options, - keyframes: resolvedKeyframes, - repeat: 0, - delay: 0, - }) - let state = { done: false, value: keyframes[0] } - const pregeneratedKeyframes: number[] = [] - - /** - * Bail after 20 seconds of pre-generated keyframes as it's likely - * we're heading for an infinite loop. - */ - let t = 0 - while (!state.done && t < maxDuration) { - state = sampleAnimation.sample(t) - pregeneratedKeyframes.push(state.value) - t += sampleDelta - } - - times = undefined - keyframes = pregeneratedKeyframes - duration = t - sampleDelta - ease = "linear" - } - - animation = animateStyle( - (value.owner as VisualElement).current!, - valueName, - keyframes, - { - ...options, - duration, - /** - * This function is currently not called if ease is provided - * as a function so the cast is safe. - * - * However it would be possible for a future refinement to port - * in easing pregeneration from Motion One for browsers that - * support the upcoming `linear()` easing function. - */ - ease: ease as EasingDefinition, - times, - } - ) - - /** - * Prefer the `onfinish` prop as it's more widely supported than - * the `finished` promise. - * - * Here, we synchronously set the provided MotionValue to the end - * keyframe. If we didn't, when the WAAPI animation is finished it would - * be removed from the element which would then revert to its old styles. - */ - animation.onfinish = finish - } - - const cancelAnimation = () => { - pendingCancel = false - - if (animation) { - animation.cancel() - } else { - resolver.cancel() - } - } - - const safeCancel = () => { - pendingCancel = true - frame.update(cancelAnimation) - resolveFinishedPromise() - updateFinishedPromise() - } - - const resolver = - element && name && motionValue - ? element.resolveKeyframes( - unresolvedKeyframes, - createWaapiAnimation, - name, - motionValue - ) - : new KeyframeResolver( - unresolvedKeyframes, - createWaapiAnimation, - name, - motionValue, - element - ) - - /** - * Animation interrupt callback. - */ - const controls = { - then(resolve: VoidFunction, reject?: VoidFunction) { - return currentFinishedPromise.then(resolve, reject) - }, - attachTimeline(timeline: any) { - if (!animation) flushKeyframeResolvers() - - animation!.timeline = timeline - animation!.onfinish = null - - return noop - }, - get time() { - if (!animation) flushKeyframeResolvers() - return millisecondsToSeconds(animation!.currentTime || 0) - }, - set time(newTime: number) { - if (!animation) flushKeyframeResolvers() - animation!.currentTime = secondsToMilliseconds(newTime) - }, - get speed() { - if (!animation) flushKeyframeResolvers() - return animation!.playbackRate - }, - set speed(newSpeed: number) { - if (!animation) flushKeyframeResolvers() - animation!.playbackRate = newSpeed - }, - get duration() { - // TODO allow async - return millisecondsToSeconds(duration) - }, - play: () => { - if (!animation) flushKeyframeResolvers() - if (hasStopped) return - animation!.play() - - /** - * Cancel any pending cancel tasks - */ - cancelFrame(cancelAnimation) - }, - // TODO allow async - pause: () => { - if (!animation) flushKeyframeResolvers() - animation!.pause() - }, - stop: () => { - if (!animation) flushKeyframeResolvers() - - hasStopped = true - if (animation!.playState === "idle") return - - /** - * WAAPI doesn't natively have any interruption capabilities. - * - * Rather than read commited styles back out of the DOM, we can - * create a renderless JS animation and sample it twice to calculate - * its current value, "previous" value, and therefore allow - * Motion to calculate velocity for any subsequent animation. - */ - const { currentTime } = animation! - - if (currentTime) { - const sampleAnimation = animateValue({ - ...options, - keyframes: resolvedKeyframes, - autoplay: false, - }) - - value.setWithVelocity( - sampleAnimation.sample(currentTime - sampleDelta).value, - sampleAnimation.sample(currentTime).value, - sampleDelta - ) - } - safeCancel() - }, - complete: () => { - if (pendingCancel) return - if (!animation) flushKeyframeResolvers() - animation!.finish() - }, - cancel: safeCancel, - } - - return controls -} diff --git a/packages/framer-motion/src/animation/animators/waapi/index.ts b/packages/framer-motion/src/animation/animators/waapi/index.ts index dc92602762..5557a31cae 100644 --- a/packages/framer-motion/src/animation/animators/waapi/index.ts +++ b/packages/framer-motion/src/animation/animators/waapi/index.ts @@ -7,7 +7,7 @@ export function animateStyle( keyframes: string[] | number[], { delay = 0, - duration, + duration = 300, repeat = 0, repeatType = "loop", ease, diff --git a/packages/framer-motion/src/animation/generators/__tests__/keyframes.test.ts b/packages/framer-motion/src/animation/generators/__tests__/keyframes.test.ts index 1e950374b8..0a46a60444 100644 --- a/packages/framer-motion/src/animation/generators/__tests__/keyframes.test.ts +++ b/packages/framer-motion/src/animation/generators/__tests__/keyframes.test.ts @@ -3,7 +3,7 @@ import { easeInOut } from "../../../easing/ease" import { defaultOffset } from "../../../utils/offsets/default" import { convertOffsetToTimes } from "../../../utils/offsets/time" import { defaultEasing, keyframes } from "../keyframes" -import { animateSync } from "../../animators/js/__tests__/utils" +import { animateSync } from "../../animators/__tests__/utils" const linear = noop diff --git a/packages/framer-motion/src/animation/generators/__tests__/spring.test.ts b/packages/framer-motion/src/animation/generators/__tests__/spring.test.ts index 106dc47a84..6a5ffc883f 100644 --- a/packages/framer-motion/src/animation/generators/__tests__/spring.test.ts +++ b/packages/framer-motion/src/animation/generators/__tests__/spring.test.ts @@ -1,6 +1,6 @@ import { ValueAnimationOptions } from "../../types" import { spring } from "../spring" -import { animateSync } from "../../animators/js/__tests__/utils" +import { animateSync } from "../../animators/__tests__/utils" describe("spring", () => { test("Runs animations with default values ", () => { diff --git a/packages/framer-motion/src/animation/interfaces/motion-value.ts b/packages/framer-motion/src/animation/interfaces/motion-value.ts index 491b4ba798..7c8a8c861b 100644 --- a/packages/framer-motion/src/animation/interfaces/motion-value.ts +++ b/packages/framer-motion/src/animation/interfaces/motion-value.ts @@ -3,15 +3,15 @@ import { secondsToMilliseconds } from "../../utils/time-conversion" import type { MotionValue, StartAnimation } from "../../value" import { getDefaultTransition } from "../utils/default-transitions" import { getValueTransition, isTransitionDefined } from "../utils/transitions" -import { animateValue } from "../animators/js" import { ValueAnimationOptions } from "../types" import type { UnresolvedKeyframes } from "../../render/utils/KeyframesResolver" import { MotionGlobalConfig } from "../../utils/GlobalConfig" import { instantAnimationState } from "../../utils/use-instant-transition-state" -import { createAcceleratedAnimation } from "../animators/waapi/create-accelerated-animation" import type { VisualElement } from "../../render/VisualElement" import { getFinalKeyframe } from "../animators/waapi/utils/get-final-keyframe" import { frame } from "../../frameloop/frame" +import { AcceleratedAnimation } from "../animators/AcceleratedAnimation" +import { MainThreadAnimation } from "../animators/MainThreadAnimation" export const animateMotionValue = ( @@ -132,31 +132,13 @@ export const animateMotionValue = } /** - * Animate via WAAPI if possible. + * Animate via WAAPI if possible. If this is a handoff animation, the optimised animation will be running via + * WAAPI. Therefore, this animation must be JS to ensure it runs "under" the + * optimised animation. */ - if ( - /** - * If this is a handoff animation, the optimised animation will be running via - * WAAPI. Therefore, this animation must be JS to ensure it runs "under" the - * optimised animation. - */ - !isHandoff && - value.owner && - value.owner.current instanceof HTMLElement && - /** - * If we're outputting values to onUpdate then we can't use WAAPI as there's - * no way to read the value from WAAPI every frame. - */ - !value.owner.getProps().onUpdate - ) { - const acceleratedAnimation = createAcceleratedAnimation( - value, - name, - options - ) - - if (acceleratedAnimation) return acceleratedAnimation + if (!isHandoff && AcceleratedAnimation.supports(options)) { + return new AcceleratedAnimation(options) + } else { + return new MainThreadAnimation(options) } - - return animateValue(options) } diff --git a/packages/framer-motion/src/animation/types.ts b/packages/framer-motion/src/animation/types.ts index 86dd5fd77c..eec9d71b83 100644 --- a/packages/framer-motion/src/animation/types.ts +++ b/packages/framer-motion/src/animation/types.ts @@ -1,7 +1,7 @@ import { TargetAndTransition, TargetResolver } from "../types" import type { VisualElement } from "../render/VisualElement" import { Easing } from "../easing/types" -import { Driver } from "./animators/js/types" +import { Driver } from "./animators/drivers/types" import { SVGPathProperties, VariantLabels } from "../motion/types" import { SVGAttributes } from "../render/svg/types-attributes" import { ProgressTimeline } from "../render/dom/scroll/observe" @@ -46,9 +46,9 @@ export type ResolveKeyframes = ( export interface ValueAnimationOptions extends ValueAnimationTransition { keyframes: V[] + KeyframeResolver?: typeof KeyframeResolver name?: string motionValue?: MotionValue - element?: VisualElement from?: V } @@ -167,9 +167,11 @@ export interface VelocityOptions { restDelta?: number } +export type RepeatType = "loop" | "reverse" | "mirror" + export interface AnimationPlaybackOptions { repeat?: number - repeatType?: "loop" | "reverse" | "mirror" + repeatType?: RepeatType repeatDelay?: number } diff --git a/packages/framer-motion/src/index.ts b/packages/framer-motion/src/index.ts index f5045810a4..291ee5ae93 100644 --- a/packages/framer-motion/src/index.ts +++ b/packages/framer-motion/src/index.ts @@ -86,7 +86,7 @@ export { useInstantLayoutTransition } from "./projection/use-instant-layout-tran export { useResetProjection } from "./projection/use-reset-projection" export { buildTransform } from "./render/html/utils/build-transform" export { visualElementStore } from "./render/store" -export { animateValue } from "./animation/animators/js" +export { animateValue } from "./animation/animators/MainThreadAnimation" export { color } from "./value/types/color" export { complex } from "./value/types/complex" export { px } from "./value/types/numbers/units" diff --git a/packages/framer-motion/src/projection/index.ts b/packages/framer-motion/src/projection/index.ts index 807dd6db5b..46640c2b56 100644 --- a/packages/framer-motion/src/projection/index.ts +++ b/packages/framer-motion/src/projection/index.ts @@ -7,7 +7,7 @@ export { calcBoxDelta } from "./geometry/delta-calc" */ import { frame, frameData } from "../frameloop" import { mix } from "../utils/mix" -import { animateValue } from "../animation/animators/js" +import { animateValue } from "../animation/animators/MainThreadAnimation" export { frame, animateValue as animate, mix, frameData } export { buildTransform } from "../render/html/utils/build-transform" export { addScaleCorrector } from "./styles/scale-correction" diff --git a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts index 17c8e92e05..777e868773 100644 --- a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts +++ b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts @@ -30,10 +30,16 @@ export class DOMKeyframesResolver< unresolvedKeyframes: UnresolvedKeyframes, onComplete: OnKeyframesResolved, name?: string, - motionValue?: MotionValue, - element?: VisualElement + motionValue?: MotionValue ) { - super(unresolvedKeyframes, onComplete, name, motionValue, element, true) + super( + unresolvedKeyframes, + onComplete, + name, + motionValue, + motionValue?.owner as VisualElement, + true + ) } readKeyframes() { @@ -50,7 +56,7 @@ export class DOMKeyframesResolver< */ for (let i = 0; i < unresolvedKeyframes.length; i++) { const keyframe = unresolvedKeyframes[i] - if (isCSSVariableToken(keyframe)) { + if (typeof keyframe === "string" && isCSSVariableToken(keyframe)) { const resolved = getVariableValue(keyframe, element.current) if (resolved !== undefined) { diff --git a/packages/framer-motion/src/render/utils/KeyframesResolver.ts b/packages/framer-motion/src/render/utils/KeyframesResolver.ts index ee90afa8dd..40f7ef06a5 100644 --- a/packages/framer-motion/src/render/utils/KeyframesResolver.ts +++ b/packages/framer-motion/src/render/utils/KeyframesResolver.ts @@ -66,6 +66,9 @@ export class KeyframeResolver { resolvedKeyframes: ResolvedKeyframes | undefined unresolvedKeyframes: UnresolvedKeyframes motionValue?: MotionValue + isScheduled = false + isComplete = false + isAsync: boolean private onComplete: OnKeyframesResolved constructor( @@ -81,8 +84,12 @@ export class KeyframeResolver { this.name = name this.motionValue = motionValue this.element = element + this.isAsync = isAsync + } - if (isAsync) { + scheduleResolve() { + this.isScheduled = true + if (this.isAsync) { toResolve.add(this) if (!isScheduled) { @@ -148,11 +155,19 @@ export class KeyframeResolver { measureEndState() {} complete() { + this.isComplete = true this.onComplete(this.unresolvedKeyframes as ResolvedKeyframes) toResolve.delete(this) } cancel() { - toResolve.delete(this) + if (!this.isComplete) { + this.isScheduled = false + toResolve.delete(this) + } + } + + resume() { + if (!this.isComplete) this.scheduleResolve() } } diff --git a/packages/framer-motion/src/value/__tests__/use-spring.test.tsx b/packages/framer-motion/src/value/__tests__/use-spring.test.tsx index a18cc07607..024b7b4ba6 100644 --- a/packages/framer-motion/src/value/__tests__/use-spring.test.tsx +++ b/packages/framer-motion/src/value/__tests__/use-spring.test.tsx @@ -4,7 +4,7 @@ import { useSpring } from "../use-spring" import { useMotionValue } from "../use-motion-value" import { motionValue, MotionValue } from ".." import { motion } from "../../" -import { syncDriver } from "../../animation/animators/js/__tests__/utils" +import { syncDriver } from "../../animation/animators/__tests__/utils" describe("useSpring", () => { test("can create a motion value from a number", async () => { diff --git a/packages/framer-motion/src/value/use-spring.ts b/packages/framer-motion/src/value/use-spring.ts index b379da718a..b967bd8b90 100644 --- a/packages/framer-motion/src/value/use-spring.ts +++ b/packages/framer-motion/src/value/use-spring.ts @@ -5,11 +5,11 @@ import { useMotionValue } from "./use-motion-value" import { MotionConfigContext } from "../context/MotionConfigContext" import { SpringOptions } from "../animation/types" import { useIsomorphicLayoutEffect } from "../utils/use-isomorphic-effect" +import { frameData } from "../frameloop" import { - MainThreadAnimationControls, + MainThreadAnimation, animateValue, -} from "../animation/animators/js" -import { frameData } from "../frameloop" +} from "../animation/animators/MainThreadAnimation" /** * Creates a `MotionValue` that, when `set`, will use a spring animation to animate to its new state. @@ -35,8 +35,9 @@ export function useSpring( config: SpringOptions = {} ) { const { isStatic } = useContext(MotionConfigContext) - const activeSpringAnimation = - useRef | null>(null) + const activeSpringAnimation = useRef | null>( + null + ) const value = useMotionValue(isMotionValue(source) ? source.get() : source) const stopAnimation = () => { From 9001d53d4abe2428ec459c33e021abb9ec60c467 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 27 Feb 2024 10:43:20 +0100 Subject: [PATCH 30/44] undoing benchmark changes --- dev/benchmarks/cold-start-framer-motion.html | 82 +++++++------------- 1 file changed, 30 insertions(+), 52 deletions(-) diff --git a/dev/benchmarks/cold-start-framer-motion.html b/dev/benchmarks/cold-start-framer-motion.html index b7b4557853..05c4f230c4 100644 --- a/dev/benchmarks/cold-start-framer-motion.html +++ b/dev/benchmarks/cold-start-framer-motion.html @@ -30,7 +30,7 @@ .box { width: 10px; height: 100px; - background-color: #0f0; + background-color: #fff; } @@ -40,7 +40,7 @@ From 0b8ab8448ecb3d7dffcb06c3ea2c3549b165112e Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 27 Feb 2024 13:44:10 +0100 Subject: [PATCH 31/44] Clean --- CHANGELOG.md | 1 + dev/examples/Animation-animate.tsx | 19 ++++++++----------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2a70466fb..2a09922f9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Undocumented APIs should be considered internal and may change without warning. - Keyframes now resolved asynchronously. - External event handlers now fired synchronously. +- CSS variables and unit conversion now supported with >2 keyframe animations. ## [11.0.10] 2024-03-12 diff --git a/dev/examples/Animation-animate.tsx b/dev/examples/Animation-animate.tsx index 2aea468f50..6a84fc7784 100644 --- a/dev/examples/Animation-animate.tsx +++ b/dev/examples/Animation-animate.tsx @@ -24,19 +24,16 @@ const Child = ({ setState }: any) => { useEffect(() => { const controls = animate([ - ["div", { x: 500 }, { type: "spring", duration: 1, bounce: 0 }], + [ + "div", + { x: 500, opacity: 0 }, + { type: "spring", duration: 1, bounce: 0 }, + ], ]) - controls.play() - controls.pause() - controls.time = 0.1 - - setTimeout(() => controls.play(), 1000) - - // controls.then(() => { - // controls.play() - // }) - + controls.then(() => { + controls.play() + }) return () => controls.stop() }, [target]) From e208f4fde6392e34068c56cf818b79b75af72ee1 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 27 Feb 2024 13:45:00 +0100 Subject: [PATCH 32/44] Latest --- dev/examples/Animation-animate.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/examples/Animation-animate.tsx b/dev/examples/Animation-animate.tsx index 6a84fc7784..109ab04260 100644 --- a/dev/examples/Animation-animate.tsx +++ b/dev/examples/Animation-animate.tsx @@ -34,6 +34,7 @@ const Child = ({ setState }: any) => { controls.then(() => { controls.play() }) + return () => controls.stop() }, [target]) From a6e7d879920e68db361a45dccc62ee22aae50090 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 27 Feb 2024 13:47:44 +0100 Subject: [PATCH 33/44] Updating package --- dev/examples/Animation-waapi-resolve-test.tsx | 87 --------------- dev/examples/ThreeInitialProps.tsx | 102 ------------------ packages/framer-motion-3d/package.json | 2 +- 3 files changed, 1 insertion(+), 190 deletions(-) delete mode 100644 dev/examples/Animation-waapi-resolve-test.tsx delete mode 100644 dev/examples/ThreeInitialProps.tsx diff --git a/dev/examples/Animation-waapi-resolve-test.tsx b/dev/examples/Animation-waapi-resolve-test.tsx deleted file mode 100644 index 91a996c539..0000000000 --- a/dev/examples/Animation-waapi-resolve-test.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import * as React from "react" -import { useEffect, useState } from "react" -import { motion, motionValue, useAnimate } from "framer-motion" -import { frame } from "framer-motion" - -/** - * An example of the tween transition type - */ - -const style = { - width: 100, - height: 100, - background: "white", -} - -const Child = ({ setState }: any) => { - const [width] = useState(100) - const [target, setTarget] = useState(0) - const transition = { - duration: 10, - } - - const [scope, animate] = useAnimate() - - useEffect(() => { - const animationA = scope.current.animate( - { opacity: 1 }, - { duration: 3, easing: "ease-in", fill: "both" } - ) - - console.log("a current time", animationA.effect.getKeyframes()) - - const animationB = scope.current.animate( - { transform: "translateX(100px)" }, - { duration: 3, easing: "ease-in", fill: "both" } - ) - - console.log("b current time", animationB.effect.getComputedTiming()) - - animationA.startTime = 100 - - console.log("a current time after set ", animationA.startTime) - console.log("b current time after set", animationB.startTime) - - // const controls = animate([ - // [ - // "div", - // { x: 500, opacity: 0 }, - // { type: "spring", duration: 1, bounce: 0 }, - // ], - // ]) - - // controls.then(() => { - // controls.play() - // }) - - // return () => controls.stop() - }, [target]) - - return ( -
- { - setTarget(target + 100) - // setWidth(width + 100) - }} - initial={{ borderRadius: 10 }} - /> - {/*
setState(false)} /> */} -
- ) - return -} - -export const App = () => { - const [state, setState] = useState(true) - - return state && -} diff --git a/dev/examples/ThreeInitialProps.tsx b/dev/examples/ThreeInitialProps.tsx deleted file mode 100644 index b11915ed8b..0000000000 --- a/dev/examples/ThreeInitialProps.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import * as React from "react" -import { useState } from "react" -import { MotionConfig, motion as motionDom, useTransform } from "framer-motion" -import { motion, MotionCanvas, useTime } from "framer-motion-3d" -import { extend } from "@react-three/fiber" -import { - AmbientLight, - PointLight, - Group, - BoxGeometry, - MeshStandardMaterial, - Mesh, -} from "three" - -extend({ - AmbientLight, - PointLight, - Group, - BoxGeometry, - MeshStandardMaterial, - Mesh, -}) - -/** - * An example of firing an animation onMount using the useAnimation hook - */ - -function Box(props) { - // Hold state for hovered and clicked events - const [clicked, click] = useState(false) - const time = useTime() - // const scale = useTransform(time, (t) => Math.sin(t) * 0.5 + 1) - - // Subscribe this component to the render-loop, rotate the mesh every frame - // useFrame((state, delta) => (ref.current.rotation.x += 0.01)) - // Return the view, these are regular Threejs elements expressed in JSX - return ( - click(!clicked)} - > - - - - ) -} - -export const App = () => { - const [isHovered, setHover] = useState(false) - - return ( - - setHover(true)} - onHoverEnd={() => setHover(false)} - > - - - - - console.log({ ...latest })} - /> - - - - - ) -} diff --git a/packages/framer-motion-3d/package.json b/packages/framer-motion-3d/package.json index dd1f31a1d8..ab23634e72 100644 --- a/packages/framer-motion-3d/package.json +++ b/packages/framer-motion-3d/package.json @@ -35,7 +35,7 @@ "scripts": { "eslint": "yarn run lint", "lint": "yarn eslint src/**/*.ts", - "test": "", + "test": "yarn test-unit", "test-ci": "yarn test-unit", "test-unit": "jest --coverage --config jest.config.json --max-workers=2", "build": "yarn clean && tsc -p . && rollup -c", From a900c813efe059eb895f419a4c57e16e3cb9b05b Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 27 Feb 2024 13:51:13 +0100 Subject: [PATCH 34/44] Updating test --- packages/framer-motion-3d/src/render/__tests__/index.test.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/framer-motion-3d/src/render/__tests__/index.test.tsx b/packages/framer-motion-3d/src/render/__tests__/index.test.tsx index 58e59ce9ba..917f5ef1e3 100644 --- a/packages/framer-motion-3d/src/render/__tests__/index.test.tsx +++ b/packages/framer-motion-3d/src/render/__tests__/index.test.tsx @@ -44,9 +44,7 @@ describe("motion for three", () => { scale={[5, 5, 5]} position={[1, 2, 3]} rotation={[4, 5, 6]} - onUpdate={(latest) => { - output.push({ ...latest }) - }} + onUpdate={(latest) => output.push({ ...latest })} onAnimationComplete={() => resolve(output)} transition={{ duration: 0.1, From fc046d3989b0e7e59ea0c254a5f8e915b34783ea Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 27 Feb 2024 14:21:26 +0100 Subject: [PATCH 35/44] Latest --- CHANGELOG.md | 1 + .../animators/AcceleratedAnimation.ts | 21 ++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a09922f9f..a9eb6e91a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Undocumented APIs should be considered internal and may change without warning. - Keyframes now resolved asynchronously. - External event handlers now fired synchronously. - CSS variables and unit conversion now supported with >2 keyframe animations. +- Removed WAAPI animation of `background-color`. ## [11.0.10] 2024-03-12 diff --git a/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts b/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts index 33ca9350c4..a80946168c 100644 --- a/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts +++ b/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts @@ -31,6 +31,9 @@ const acceleratedValues = new Set([ "clipPath", "filter", "transform", + // TODO: Can be accelerated but currently disabled until https://issues.chromium.org/issues/41491098 is resolved + // or until we implement support for linear() easing. + // "background-color" ]) /** @@ -46,6 +49,11 @@ const sampleDelta = 10 //ms */ const maxDuration = 20_000 +/** + * Check if an animation can run natively via WAAPI or requires pregenerated keyframes. + * WAAPI doesn't support spring or function easings so we run these as JS animation before + * handing off. + */ function requiresPregeneratedKeyframes( options: ValueAnimationOptions ) { @@ -60,6 +68,11 @@ function pregenerateKeyframes( keyframes: ResolvedKeyframes, options: ValueAnimationOptions ) { + /** + * Create a main-thread animation to pregenerate keyframes. + * We sample this at regular intervals to generate keyframes that we then + * linearly interpolate between. + */ const sampleAnimation = new MainThreadAnimation({ ...options, keyframes, @@ -125,7 +138,10 @@ export class AcceleratedAnimation< this.resolver.scheduleResolve() } - private pendingTimeline: any + /** + * An AnimationTimline to attach to the WAAPI animation once it's created. + */ + private pendingTimeline: AnimationTimeline | undefined protected initPlayback( keyframes: ResolvedKeyframes @@ -154,7 +170,7 @@ export class AcceleratedAnimation< motionValue.owner!.current as unknown as HTMLElement, name, keyframes as string[], - this.options + { ...this.options, duration } ) // Override the browser calculated startTime with one synchronised to other JS @@ -178,7 +194,6 @@ export class AcceleratedAnimation< motionValue.set(getFinalKeyframe(keyframes, this.options)) onComplete && onComplete() this.cancel() - // frame.update(cancelAnimation) this.resolveFinishedPromise() this.updateFinishedPromise() } From b2a0bbd24f08611b5d90a02f4c3c872fee2788af Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 27 Feb 2024 14:23:12 +0100 Subject: [PATCH 36/44] Updating --- .../framer-motion/src/animation/animators/BaseAnimation.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/framer-motion/src/animation/animators/BaseAnimation.ts b/packages/framer-motion/src/animation/animators/BaseAnimation.ts index 1c8633ff00..cabfdb8192 100644 --- a/packages/framer-motion/src/animation/animators/BaseAnimation.ts +++ b/packages/framer-motion/src/animation/animators/BaseAnimation.ts @@ -84,9 +84,6 @@ export abstract class BaseAnimation * This is a deoptimisation, but at its worst still batches read/writes. */ get resolved(): Resolved & { keyframes: ResolvedKeyframes } { - // TODO Leave only for tests - if (this.isInitialising) - throw new Error("Cannot access `resolved` while initialising.") if (!this._resolved) flushKeyframeResolvers() return this._resolved @@ -97,9 +94,7 @@ export abstract class BaseAnimation * will check if its possible to run the animation and, if not, skip it. * Otherwise, it will call initPlayback on the implementing class. */ - private isInitialising = false protected onKeyframesResolved(keyframes: ResolvedKeyframes) { - this.isInitialising = true const { name, type, velocity, delay, onComplete, onUpdate } = this.options @@ -129,8 +124,6 @@ export abstract class BaseAnimation ...this.initPlayback(keyframes), } - this.isInitialising = false - this.onPostResolved() } From 15532d0694c3f63a5958d5e69a80bdfd7839f039 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 27 Feb 2024 14:35:45 +0100 Subject: [PATCH 37/44] Latest --- .../animators/MainThreadAnimation.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/packages/framer-motion/src/animation/animators/MainThreadAnimation.ts b/packages/framer-motion/src/animation/animators/MainThreadAnimation.ts index 24b5dc782c..a516b46213 100644 --- a/packages/framer-motion/src/animation/animators/MainThreadAnimation.ts +++ b/packages/framer-motion/src/animation/animators/MainThreadAnimation.ts @@ -38,26 +38,67 @@ interface ResolvedData { generator: KeyframeGenerator mirroredGenerator: KeyframeGenerator | undefined mapPercentToKeyframes: ((v: number) => T) | undefined + + /** + * Duration of the animation as calculated by the generator. + */ calculatedDuration: number + + /** + * Duration of the animation plus repeatDelay. + */ resolvedDuration: number + + /** + * Total duration of the animation including repeats. + */ totalDuration: number } +/** + * Animation that runs on the main thread. Designed to be WAAPI-spec in the subset of + * features we expose publically. Mostly the compatibility is to ensure visual identity + * between both WAAPI and main thread animations. + */ export class MainThreadAnimation< T extends string | number > extends BaseAnimation> { + /** + * The driver that's controlling the animation loop. Normally this is a requestAnimationFrame loop + * but in tests we can pass in a synchronous loop. + */ private driver?: DriverControls + /** + * The time at which the animation was paused. + */ private holdTime: number | null = null + /** + * The time at which the animation was started. + */ private startTime: number | null = null + /** + * The time at which the animation was cancelled. + */ private cancelTime: number | null = null + /** + * The current time of the animation. + */ private currentTime: number = 0 + /** + * Playback speed as a factor. 0 would be stopped, -1 reverse and 2 double speed. + */ private playbackSpeed = 1 + /** + * The state of the animation to apply when the animation is resolved. This + * allows calls to the public API to control the animation before it is resolved, + * without us having to resolve it first. + */ private pendingPlayState: AnimationPlayState = "running" constructor({ @@ -100,6 +141,12 @@ export class MainThreadAnimation< const generatorFactory = generators[type] || keyframesGeneratorFactory + /** + * If our generator doesn't support mixing numbers, we need to replace keyframes with + * [0, 100] and then make a function that maps that to the actual keyframes. + * + * 100 is chosen instead of 1 as it works nicer with spring animations. + */ let mapPercentToKeyframes: ((v: number) => T) | undefined let mirroredGenerator: KeyframeGenerator | undefined @@ -124,6 +171,10 @@ export class MainThreadAnimation< const generator = generatorFactory({ ...this.options, keyframes }) + /** + * If we have a mirror repeat type we need to create a second generator that outputs the + * mirrored (not reversed) animation and later ping pong between the two generators. + */ if (repeatType === "mirror") { mirroredGenerator = generatorFactory({ ...this.options, From 958bbbb27a2dbec7f162a8fcb8ad9cffd88c1aed Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 27 Feb 2024 14:52:23 +0100 Subject: [PATCH 38/44] Adding comment --- .../src/animation/interfaces/motion-value.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/framer-motion/src/animation/interfaces/motion-value.ts b/packages/framer-motion/src/animation/interfaces/motion-value.ts index 7c8a8c861b..569fd2f22e 100644 --- a/packages/framer-motion/src/animation/interfaces/motion-value.ts +++ b/packages/framer-motion/src/animation/interfaces/motion-value.ts @@ -103,25 +103,18 @@ export const animateMotionValue = options.delay = 0 } + /** + * If we can or must skip creating the animation, and apply only + * the final keyframe, do so. We also check once keyframes are resolved but + * this early check prevents the need to create an animation at all. + */ if (shouldSkip && !isHandoff && value.get() !== undefined) { - /** - * If we can or must skip creating the animation, and apply only - * the final keyframe, do so. - */ const finalKeyframe = getFinalKeyframe( options.keyframes as V[], valueTransition ) if (finalKeyframe !== undefined) { - // value.stop() - - // if (element) { - // frame.read(() => { - // element.readValue(name, finalKeyframe) - // }) - // } - frame.update(() => { options.onUpdate!(finalKeyframe) options.onComplete!() From 3d8f06daaa8bb639661f295d6863620b492ced1d Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 27 Feb 2024 15:09:46 +0100 Subject: [PATCH 39/44] Fixing --- .../src/render/dom/DOMKeyframesResolver.ts | 24 ++++---- .../src/render/utils/KeyframesResolver.ts | 59 +++++++++++++------ 2 files changed, 52 insertions(+), 31 deletions(-) diff --git a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts index 777e868773..17d38c2402 100644 --- a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts +++ b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts @@ -20,12 +20,13 @@ import { MotionValue } from "../../value" export class DOMKeyframesResolver< T extends string | number > extends KeyframeResolver { - name: string - element: VisualElement - async = true + protected name: string + protected element: VisualElement + private removedTransforms?: [string, string | number][] private measuredOrigin?: string | number - restoreScrollY?: number + private suspendedScrollY?: number + constructor( unresolvedKeyframes: UnresolvedKeyframes, onComplete: OnKeyframesResolved, @@ -62,11 +63,6 @@ export class DOMKeyframesResolver< if (resolved !== undefined) { unresolvedKeyframes[i] = resolved as T } - - // If this variable is the final keyframe, set it as finalKeyframe - if (i === unresolvedKeyframes.length - 1) { - // this.resolvedFinalKeyframe = keyframe - } } if (isNone(unresolvedKeyframes[i])) { @@ -130,10 +126,6 @@ export class DOMKeyframesResolver< const finalKeyframe = unresolvedKeyframes[unresolvedKeyframes.length - 1] - // if (this.resolvedFinalKeyframe === undefined) { - // this.resolvedFinalKeyframe = finalKeyframe - // } - element.getValue(name, finalKeyframe).jump(finalKeyframe, false) } @@ -143,7 +135,7 @@ export class DOMKeyframesResolver< if (!element.current) return if (name === "height") { - this.restoreScrollY = window.pageYOffset + this.suspendedScrollY = window.pageYOffset } this.measuredOrigin = positionalValues[name]( @@ -173,6 +165,10 @@ export class DOMKeyframesResolver< window.getComputedStyle(element.current) ) as any + if (name === "height" && this.suspendedScrollY !== undefined) { + window.scrollTo(0, this.suspendedScrollY) + } + // If we removed transform values, reapply them before the next render if (this.removedTransforms?.length) { this.removedTransforms.forEach( diff --git a/packages/framer-motion/src/render/utils/KeyframesResolver.ts b/packages/framer-motion/src/render/utils/KeyframesResolver.ts index 40f7ef06a5..9693361736 100644 --- a/packages/framer-motion/src/render/utils/KeyframesResolver.ts +++ b/packages/framer-motion/src/render/utils/KeyframesResolver.ts @@ -8,30 +8,35 @@ export type ResolvedKeyframes = Array const toResolve = new Set() let isScheduled = false -let needsMeasurement = false +let anyNeedsMeasurement = false function measureAllKeyframes() { - if (needsMeasurement) { + if (anyNeedsMeasurement) { + // Write toResolve.forEach((resolver) => { resolver.needsMeasurement && resolver.unsetTransforms() }) + + // Read toResolve.forEach((resolver) => { resolver.needsMeasurement && resolver.measureInitialState() }) + + // Write toResolve.forEach((resolver) => { resolver.needsMeasurement && resolver.renderEndStyles() }) + + // Read toResolve.forEach((resolver) => { resolver.needsMeasurement && resolver.measureEndState() }) } - needsMeasurement = false + anyNeedsMeasurement = false isScheduled = false - toResolve.forEach((resolver) => { - resolver.complete() - }) + toResolve.forEach((resolver) => resolver.complete()) toResolve.clear() } @@ -41,7 +46,7 @@ function readAllKeyframes() { resolver.readKeyframes() if (resolver.needsMeasurement) { - needsMeasurement = true + anyNeedsMeasurement = true } }) @@ -61,16 +66,38 @@ export type OnKeyframesResolved = ( ) => void export class KeyframeResolver { - element?: VisualElement - name?: string - resolvedKeyframes: ResolvedKeyframes | undefined - unresolvedKeyframes: UnresolvedKeyframes - motionValue?: MotionValue - isScheduled = false - isComplete = false - isAsync: boolean + protected element?: VisualElement + protected unresolvedKeyframes: UnresolvedKeyframes + + private name?: string + private motionValue?: MotionValue private onComplete: OnKeyframesResolved + /** + * Track whether this resolver has completed. Once complete, it never + * needs to attempt keyframe resolution again. + */ + private isComplete = false + + /** + * Track whether this resolver is async. If it is, it'll be added to the + * resolver queue and flushed in the next frame. Resolvers that aren't going + * to trigger read/write thrashing don't need to be async. + */ + private isAsync = false + + /** + * Track whether this resolver needs to perform a measurement + * to resolve its keyframes. + */ + needsMeasurement = false + + /** + * Track whether this resolver is currently scheduled to resolve + * to allow it to be cancelled and resumed externally. + */ + isScheduled = false + constructor( unresolvedKeyframes: UnresolvedKeyframes, onComplete: OnKeyframesResolved, @@ -102,8 +129,6 @@ export class KeyframeResolver { } } - needsMeasurement = false - readKeyframes() { const { unresolvedKeyframes, name, element, motionValue } = this From b55ab6622e026fc8d706008c5825067c1bedaf82 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 27 Feb 2024 15:26:55 +0100 Subject: [PATCH 40/44] Fixing build --- packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts | 2 +- packages/framer-motion/src/render/utils/KeyframesResolver.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts index 17d38c2402..0f37cfd9f6 100644 --- a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts +++ b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts @@ -20,7 +20,7 @@ import { MotionValue } from "../../value" export class DOMKeyframesResolver< T extends string | number > extends KeyframeResolver { - protected name: string + name: string protected element: VisualElement private removedTransforms?: [string, string | number][] diff --git a/packages/framer-motion/src/render/utils/KeyframesResolver.ts b/packages/framer-motion/src/render/utils/KeyframesResolver.ts index 9693361736..20d928e8c7 100644 --- a/packages/framer-motion/src/render/utils/KeyframesResolver.ts +++ b/packages/framer-motion/src/render/utils/KeyframesResolver.ts @@ -68,8 +68,8 @@ export type OnKeyframesResolved = ( export class KeyframeResolver { protected element?: VisualElement protected unresolvedKeyframes: UnresolvedKeyframes + name?: string - private name?: string private motionValue?: MotionValue private onComplete: OnKeyframesResolved From 85864c262ac0d4fd7a428e108a654a0a203de3dd Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 27 Feb 2024 15:34:14 +0100 Subject: [PATCH 41/44] Change --- .../animation/animators/__tests__/MainThreadAnimation.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/framer-motion/src/animation/animators/__tests__/MainThreadAnimation.test.ts b/packages/framer-motion/src/animation/animators/__tests__/MainThreadAnimation.test.ts index 47e99e1f5e..2d0cafee13 100644 --- a/packages/framer-motion/src/animation/animators/__tests__/MainThreadAnimation.test.ts +++ b/packages/framer-motion/src/animation/animators/__tests__/MainThreadAnimation.test.ts @@ -1250,7 +1250,6 @@ describe("MainThreadAnimation", () => { onUpdate: (v) => output.push(v), }) - animation.cancel() animation.complete() await animation From 2a08624de322df6e1a15dddd672ab4a35905ab67 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 28 Feb 2024 10:40:06 +0100 Subject: [PATCH 42/44] v11.0.7-alpha.0 --- lerna.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lerna.json b/lerna.json index 479a2414e9..a697246690 100644 --- a/lerna.json +++ b/lerna.json @@ -1,8 +1,6 @@ { "version": "11.0.10", - "packages": [ - "packages/*" - ], + "packages": ["packages/*"], "npmClient": "yarn", "useWorkspaces": true } From 8e7232813ab84f49cd47a19853d97b311b92ccd1 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 28 Feb 2024 10:45:05 +0100 Subject: [PATCH 43/44] Updating size --- packages/framer-motion-3d/package.json | 2 +- packages/framer-motion/package.json | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/framer-motion-3d/package.json b/packages/framer-motion-3d/package.json index ab23634e72..790879abe2 100644 --- a/packages/framer-motion-3d/package.json +++ b/packages/framer-motion-3d/package.json @@ -61,5 +61,5 @@ "@react-three/test-renderer": "^9.0.0", "@rollup/plugin-commonjs": "^22.0.1" }, - "gitHead": "f99b162917a2f171dbabe32a5baf63990b83318b" + "gitHead": "2b49f76000d8006f08f7e76b63bd86eee0d25ab8" } diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index 496e24342d..d8273f93c1 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -86,7 +86,7 @@ "bundlesize": [ { "path": "./dist/size-rollup-motion.js", - "maxSize": "31.7 kB" + "maxSize": "32 kB" }, { "path": "./dist/size-rollup-m.js", @@ -94,15 +94,15 @@ }, { "path": "./dist/size-rollup-dom-animation.js", - "maxSize": "15.8 kB" + "maxSize": "16.1kB" }, { "path": "./dist/size-rollup-dom-max.js", - "maxSize": "27.2 kB" + "maxSize": "27.5 kB" }, { "path": "./dist/size-rollup-animate.js", - "maxSize": "17 kB" + "maxSize": "17.3 kB" }, { "path": "./dist/size-webpack-m.js", @@ -110,12 +110,12 @@ }, { "path": "./dist/size-webpack-dom-animation.js", - "maxSize": "20.5 kB" + "maxSize": "21 kB" }, { "path": "./dist/size-webpack-dom-max.js", "maxSize": "33 kB" } ], - "gitHead": "f99b162917a2f171dbabe32a5baf63990b83318b" + "gitHead": "2b49f76000d8006f08f7e76b63bd86eee0d25ab8" } From 1175f07f247e76839a4c28c2a171a923910377a1 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 28 Feb 2024 16:25:47 +0100 Subject: [PATCH 44/44] comments --- .../src/render/html/utils/make-none-animatable.ts | 9 ++++++--- .../framer-motion/src/render/svg/SVGVisualElement.ts | 1 - .../framer-motion/src/render/utils/animation-state.ts | 1 - 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/framer-motion/src/render/html/utils/make-none-animatable.ts b/packages/framer-motion/src/render/html/utils/make-none-animatable.ts index 7398bba959..d730c066d7 100644 --- a/packages/framer-motion/src/render/html/utils/make-none-animatable.ts +++ b/packages/framer-motion/src/render/html/utils/make-none-animatable.ts @@ -1,14 +1,17 @@ import { getAnimatableNone } from "../../dom/value-types/animatable-none" import { UnresolvedKeyframes } from "../../utils/KeyframesResolver" +/** + * If we encounter keyframes like "none" or "0" and we also have keyframes like + * "#fff" or "200px 200px" we want to find a keyframe to serve as a template for + * the "none" keyframes. In this case "#fff" or "200px 200px" - then these get turned into + * zero equivalents, i.e. "#fff0" or "0px 0px". + */ export function makeNoneKeyframesAnimatable( unresolvedKeyframes: UnresolvedKeyframes, noneKeyframeIndexes: number[], name?: string ) { - /** - * If we detected "none"-equivalent keyframes, we need to find a template - */ let i = 0 let animatableTemplate: string | undefined = undefined while (i < unresolvedKeyframes.length && !animatableTemplate) { diff --git a/packages/framer-motion/src/render/svg/SVGVisualElement.ts b/packages/framer-motion/src/render/svg/SVGVisualElement.ts index 5f02c8beeb..3fafc58faf 100644 --- a/packages/framer-motion/src/render/svg/SVGVisualElement.ts +++ b/packages/framer-motion/src/render/svg/SVGVisualElement.ts @@ -33,7 +33,6 @@ export class SVGVisualElement extends DOMVisualElement< } readValueFromInstance(instance: SVGElement, key: string) { - // console.log("reading", key, "from", instance) if (transformProps.has(key)) { const defaultType = getDefaultValueType(key) return defaultType ? defaultType.default || 0 : 0 diff --git a/packages/framer-motion/src/render/utils/animation-state.ts b/packages/framer-motion/src/render/utils/animation-state.ts index bf219f0956..43cc531204 100644 --- a/packages/framer-motion/src/render/utils/animation-state.ts +++ b/packages/framer-motion/src/render/utils/animation-state.ts @@ -214,7 +214,6 @@ export function createAnimationState( * Build an object of all the resolved values. We'll use this in the subsequent * animateChanges calls to determine whether a value has changed. */ - // TODO Resolve with options let resolvedValues = definitionList.reduce( buildResolvedTypeValues(type), {}