From 44744e0b209d3ebe79837befc27dc744f2550c1b Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 13 Dec 2022 11:49:48 +0100 Subject: [PATCH 1/3] Adding animation events to motion value --- dev/examples/Drag-external-handlers.tsx | 4 +- dev/examples/ThreeSwitch.tsx | 2 +- dev/examples/useScroll.tsx | 2 +- dev/examples/useViewportScroll.tsx | 2 +- dev/tests/drag-to-reorder.tsx | 2 +- .../motion/__tests__/animate-prop.test.tsx | 2 +- .../src/motion/__tests__/variant.test.tsx | 4 +- .../framer-motion/src/render/VisualElement.ts | 8 +- .../value/__tests__/use-motion-value.test.tsx | 4 +- .../src/value/__tests__/use-spring.test.tsx | 9 +- .../src/value/__tests__/use-velocity.test.tsx | 6 +- packages/framer-motion/src/value/index.ts | 112 ++++++++++-------- .../src/value/use-motion-value.ts | 2 +- .../framer-motion/src/value/use-on-change.ts | 4 +- .../framer-motion/src/value/use-velocity.ts | 2 +- 15 files changed, 89 insertions(+), 76 deletions(-) diff --git a/dev/examples/Drag-external-handlers.tsx b/dev/examples/Drag-external-handlers.tsx index 35c54bb8e1..295ac40459 100644 --- a/dev/examples/Drag-external-handlers.tsx +++ b/dev/examples/Drag-external-handlers.tsx @@ -44,7 +44,7 @@ export const App = () => { ) useEffect(() => { - return transform.onChange(v => console.log(v)) + return transform.on("change", (v) => console.log(v)) }) return ( @@ -54,7 +54,7 @@ export const App = () => { _dragX={x} _dragY={y} dragConstraints={ref} - onMeasureDragConstraints={constraints => constraints} + onMeasureDragConstraints={(constraints) => constraints} style={{ backgroundColor: color, ...child, diff --git a/dev/examples/ThreeSwitch.tsx b/dev/examples/ThreeSwitch.tsx index e633819b2d..c1e97c0701 100644 --- a/dev/examples/ThreeSwitch.tsx +++ b/dev/examples/ThreeSwitch.tsx @@ -114,7 +114,7 @@ export function useAnimatedText(target, textTransition) { useEffect(() => { ref.current.innerText = target.toFixed(2) - return value.onChange((v) => { + return value.on("change", (v) => { ref.current.innerText = v.toFixed(2) }) }) diff --git a/dev/examples/useScroll.tsx b/dev/examples/useScroll.tsx index d06667d9d1..ff6dfb21dc 100644 --- a/dev/examples/useScroll.tsx +++ b/dev/examples/useScroll.tsx @@ -47,7 +47,7 @@ export const Example = () => { const yRange = useTransform(scrollYProgress, [0, 0.9], [0, 1]) const pathLength = useSpring(yRange, { stiffness: 400, damping: 90 }) - useEffect(() => yRange.onChange((v) => setIsComplete(v >= 1)), [yRange]) + useEffect(() => yRange.on("change", (v) => setIsComplete(v >= 1)), [yRange]) return (
{ const yRange = useTransform(scrollYProgress, [0, 0.9], [0, 1]) const pathLength = useSpring(yRange, { stiffness: 400, damping: 90 }) - useEffect(() => yRange.onChange((v) => setIsComplete(v >= 1)), [yRange]) + useEffect(() => yRange.on("change", (v) => setIsComplete(v >= 1)), [yRange]) return ( <> diff --git a/dev/tests/drag-to-reorder.tsx b/dev/tests/drag-to-reorder.tsx index ef096402db..12bef4483b 100644 --- a/dev/tests/drag-to-reorder.tsx +++ b/dev/tests/drag-to-reorder.tsx @@ -13,7 +13,7 @@ const Item = ({ item, axis }) => { useEffect(() => { let isActive = false - axisValue.onChange((latestY) => { + axisValue.on("change", (latestY) => { const wasActive = isActive if (latestY !== 0) { isActive = true 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 92aca0d5c4..9e437c177c 100644 --- a/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx @@ -385,7 +385,7 @@ describe("animate prop as object", () => { test("respects repeatDelay prop", async () => { const promise = new Promise((resolve) => { const x = motionValue(0) - x.onChange(() => { + x.on("change", () => { setTimeout(() => resolve(x.get()), 50) }) 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 23bd4aaa63..e92b15f541 100644 --- a/packages/framer-motion/src/motion/__tests__/variant.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/variant.test.tsx @@ -511,7 +511,7 @@ describe("animate prop as variant", () => { React.useEffect( () => - a.onChange((latest) => { + a.on("change", (latest) => { if (latest >= 1 && b.get() === 0) resolve(true) }), [a, b] @@ -564,7 +564,7 @@ describe("animate prop as variant", () => { React.useEffect( () => - a.onChange((latest) => { + a.on("change", (latest) => { if (latest >= 1 && b.get() === 0) resolve(true) }), [a, b] diff --git a/packages/framer-motion/src/render/VisualElement.ts b/packages/framer-motion/src/render/VisualElement.ts index 654feeaf1a..d25ecd937f 100644 --- a/packages/framer-motion/src/render/VisualElement.ts +++ b/packages/framer-motion/src/render/VisualElement.ts @@ -412,7 +412,8 @@ export abstract class VisualElement< private bindToMotionValue(key: string, value: MotionValue) { const valueIsTransform = transformProps.has(key) - const removeOnChange = value.onChange( + const removeOnChange = value.on( + "change", (latestValue: string | number) => { this.latestValues[key] = latestValue this.props.onUpdate && @@ -424,7 +425,10 @@ export abstract class VisualElement< } ) - const removeOnRenderRequest = value.onRenderRequest(this.scheduleRender) + const removeOnRenderRequest = value.on( + "renderRequest", + this.scheduleRender + ) this.valueSubscriptions.set(key, () => { removeOnChange() diff --git a/packages/framer-motion/src/value/__tests__/use-motion-value.test.tsx b/packages/framer-motion/src/value/__tests__/use-motion-value.test.tsx index 5c8541dac3..1cdf365416 100644 --- a/packages/framer-motion/src/value/__tests__/use-motion-value.test.tsx +++ b/packages/framer-motion/src/value/__tests__/use-motion-value.test.tsx @@ -52,8 +52,8 @@ describe("useMotionValue", () => { const onRenderRequest = jest.fn() const Component = () => { const x = useMotionValue(100) - x.onChange(onChange) - x.onRenderRequest(onRenderRequest) + x.on("change", onChange) + x.on("renderRequest", onRenderRequest) x.set(500) 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 421c92e6d1..68155a12ee 100644 --- a/packages/framer-motion/src/value/__tests__/use-spring.test.tsx +++ b/packages/framer-motion/src/value/__tests__/use-spring.test.tsx @@ -13,7 +13,7 @@ describe("useSpring", () => { const x = useSpring(0) React.useEffect(() => { - x.onChange((v) => resolve(v)) + x.on("change", (v) => resolve(v)) x.set(100) }) @@ -37,7 +37,7 @@ describe("useSpring", () => { const y = useSpring(x) React.useEffect(() => { - y.onChange((v) => resolve(v)) + y.on("change", (v) => resolve(v)) x.set(100) }) @@ -64,7 +64,7 @@ describe("useSpring", () => { } as any) React.useEffect(() => { - return y.onChange((v) => { + return y.on("change", (v) => { if (output.length >= 10) { resolve(output) } else { @@ -105,6 +105,7 @@ describe("useSpring", () => { rerender() rerender() - expect(((a! as any).updateSubscribers! as any).getSize()).toBe(1) + // Cast to any here as `.events` is private API + expect((a as any).events.change.getSize()).toBe(1) }) }) diff --git a/packages/framer-motion/src/value/__tests__/use-velocity.test.tsx b/packages/framer-motion/src/value/__tests__/use-velocity.test.tsx index 4aa0131a2b..e79f1d4586 100644 --- a/packages/framer-motion/src/value/__tests__/use-velocity.test.tsx +++ b/packages/framer-motion/src/value/__tests__/use-velocity.test.tsx @@ -46,13 +46,13 @@ describe("useVelocity", () => { React.useEffect(() => { const unsubscribe = pipe( - x.onChange((v) => { + x.on("change", (v) => { output.push(Math.round(v)) }), - xVelocity.onChange((v) => { + xVelocity.on("change", (v) => { outputVelocity.push(Math.round(v)) }), - xAcceleration.onChange((v) => { + xAcceleration.on("change", (v) => { outputAcceleration.push(Math.round(v)) }) ) diff --git a/packages/framer-motion/src/value/index.ts b/packages/framer-motion/src/value/index.ts index f18ff58be2..bcb4a674fd 100644 --- a/packages/framer-motion/src/value/index.ts +++ b/packages/framer-motion/src/value/index.ts @@ -19,6 +19,15 @@ export type PassiveEffect = (v: T, safeSetter: (v: T) => void) => void export type StartAnimation = (complete: () => void) => () => void +export interface MotionValueEventCallbacks { + animationStart: () => void + animationComplete: () => void + animationCancel: () => void + change: (latest: V) => void + renderRequest: () => void + velocityChange: (latest: number) => void +} + const isFloat = (value: any): value is string => { return !isNaN(parseFloat(value)) } @@ -73,29 +82,6 @@ export class MotionValue { */ private lastUpdated: number = 0 - /** - * Functions to notify when the `MotionValue` updates. - * - * @internal - */ - private updateSubscribers = new SubscriptionManager>() - - /** - * Functions to notify when the velocity updates. - * - * @internal - */ - public velocityUpdateSubscribers = new SubscriptionManager< - Subscriber - >() - - /** - * Functions to notify when the `MotionValue` updates and `render` is set to `true`. - * - * @internal - */ - private renderSubscribers = new SubscriptionManager>() - /** * Add a passive effect to this `MotionValue`. * @@ -159,8 +145,8 @@ export class MotionValue { * opacity.set(newOpacity) * } * - * const unsubscribeX = x.onChange(updateOpacity) - * const unsubscribeY = y.onChange(updateOpacity) + * const unsubscribeX = x.on("change", updateOpacity) + * const unsubscribeY = y.on("change", updateOpacity) * * return () => { * unsubscribeX() @@ -183,28 +169,34 @@ export class MotionValue { * @param subscriber - A function that receives the latest value. * @returns A function that, when called, will cancel this subscription. * - * @public + * @deprecated */ onChange(subscription: Subscriber): () => void { - return this.updateSubscribers.add(subscription) - } - - clearListeners() { - this.updateSubscribers.clear() + return this.on("change", subscription) } /** - * Adds a function that will be notified when the `MotionValue` requests a render. - * - * @param subscriber - A function that's provided the latest value. - * @returns A function that, when called, will cancel this subscription. - * - * @internal + * An object containing a SubscriptionManager for each active event. */ - onRenderRequest(subscription: Subscriber) { - // Render immediately - subscription(this.get()) - return this.renderSubscribers.add(subscription) + private events: { + [key: string]: SubscriptionManager + } = {} + + on>( + eventName: EventName, + callback: MotionValueEventCallbacks[EventName] + ) { + if (!this.events[eventName]) { + this.events[eventName] = new SubscriptionManager() + } + + return this.events[eventName].add(callback) + } + + clearListeners() { + for (const eventManagers in this.events) { + this.events[eventManagers].clear() + } } /** @@ -258,18 +250,18 @@ export class MotionValue { } // Update update subscribers - if (this.prev !== this.current) { - this.updateSubscribers.notify(this.current) + if (this.prev !== this.current && this.events.change) { + this.events.change.notify(this.current) } // Update velocity subscribers - if (this.velocityUpdateSubscribers.getSize()) { - this.velocityUpdateSubscribers.notify(this.getVelocity()) + if (this.events.velocityChange) { + this.events.velocityChange.notify(this.getVelocity()) } // Update render subscribers - if (render) { - this.renderSubscribers.notify(this.current) + if (render && this.events.renderRequest) { + this.events.renderRequest.notify(this.current) } } @@ -332,7 +324,10 @@ export class MotionValue { private velocityCheck = ({ timestamp }: FrameData) => { if (timestamp !== this.lastUpdated) { this.prev = this.current - this.velocityUpdateSubscribers.notify(this.getVelocity()) + + if (this.events.velocityChange) { + this.events.velocityChange.notify(this.getVelocity()) + } } } @@ -356,7 +351,16 @@ export class MotionValue { return new Promise((resolve) => { this.hasAnimated = true this.stopAnimation = animation(resolve) - }).then(() => this.clearAnimation()) + + if (this.events.animationStart) { + this.events.animationStart.notify() + } + }).then(() => { + if (this.events.animationComplete) { + this.events.animationComplete.notify() + } + this.clearAnimation() + }) } /** @@ -365,7 +369,12 @@ export class MotionValue { * @public */ stop() { - if (this.stopAnimation) this.stopAnimation() + if (this.stopAnimation) { + this.stopAnimation() + if (this.events.animationCancel) { + this.events.animationCancel.notify() + } + } this.clearAnimation() } @@ -392,8 +401,7 @@ export class MotionValue { * @public */ destroy() { - this.updateSubscribers.clear() - this.renderSubscribers.clear() + this.clearListeners() this.stop() } } diff --git a/packages/framer-motion/src/value/use-motion-value.ts b/packages/framer-motion/src/value/use-motion-value.ts index 24380ba856..b0a9153b01 100644 --- a/packages/framer-motion/src/value/use-motion-value.ts +++ b/packages/framer-motion/src/value/use-motion-value.ts @@ -31,7 +31,7 @@ export function useMotionValue(initial: T): MotionValue { const { isStatic } = useContext(MotionConfigContext) if (isStatic) { const [, setLatest] = useState(initial) - useEffect(() => value.onChange(setLatest), []) + useEffect(() => value.on("change", setLatest), []) } return value diff --git a/packages/framer-motion/src/value/use-on-change.ts b/packages/framer-motion/src/value/use-on-change.ts index a5651eae46..142fcb2013 100644 --- a/packages/framer-motion/src/value/use-on-change.ts +++ b/packages/framer-motion/src/value/use-on-change.ts @@ -9,7 +9,7 @@ export function useOnChange( useIsomorphicLayoutEffect(() => { if (isMotionValue(value)) { callback(value.get()) - return value.onChange(callback) + return value.on("change", callback) } }, [value, callback]) } @@ -20,7 +20,7 @@ export function useMultiOnChange( cleanup: () => void ) { useIsomorphicLayoutEffect(() => { - const subscriptions = values.map((value) => value.onChange(handler)) + const subscriptions = values.map((value) => value.on("change", handler)) return () => { subscriptions.forEach((unsubscribe) => unsubscribe()) diff --git a/packages/framer-motion/src/value/use-velocity.ts b/packages/framer-motion/src/value/use-velocity.ts index b99fe4e029..af9bf6fbbf 100644 --- a/packages/framer-motion/src/value/use-velocity.ts +++ b/packages/framer-motion/src/value/use-velocity.ts @@ -16,7 +16,7 @@ export function useVelocity(value: MotionValue): MotionValue { const velocity = useMotionValue(value.getVelocity()) useEffect(() => { - return value.velocityUpdateSubscribers.add((newVelocity) => { + return value.on("velocityChange", (newVelocity) => { velocity.set(newVelocity) }) }, [value]) From 624003cae87786a655e89890a5c7cd43c7373904 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 13 Dec 2022 12:39:44 +0100 Subject: [PATCH 2/3] Improving test --- CHANGELOG.md | 7 ++ .../src/value/__tests__/index.test.ts | 73 +++++++++++++++++++ packages/framer-motion/src/value/index.ts | 4 +- 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 packages/framer-motion/src/value/__tests__/index.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 20a5f5b7c3..00c886ff8a 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. +## [7.10.0] 2022-12-15 + +### Added + +- `.on()` event method to `MotionValue`. +- `"animationStart"`, `"animationComplete"`, and `"animationCancel"` events for `MotionValue`. + ## [7.9.1] 2022-12-14 ### Fixed diff --git a/packages/framer-motion/src/value/__tests__/index.test.ts b/packages/framer-motion/src/value/__tests__/index.test.ts new file mode 100644 index 0000000000..c430a8b2f8 --- /dev/null +++ b/packages/framer-motion/src/value/__tests__/index.test.ts @@ -0,0 +1,73 @@ +import { motionValue } from "../" +import { animate } from "../../animation/animate" + +describe("motionValue", () => { + test("change event fires when value changes", () => { + const value = motionValue(0) + const callback = jest.fn() + + value.on("change", callback) + + expect(callback).not.toBeCalled() + value.set(1) + expect(callback).toBeCalledTimes(1) + value.set(1) + expect(callback).toBeCalledTimes(1) + }) + + test("renderRequest event fires", () => { + const value = motionValue(0) + const callback = jest.fn() + + value.on("renderRequest", callback) + + expect(callback).not.toBeCalled() + value.set(1) + expect(callback).toBeCalledTimes(1) + }) + + test("animationStart event fires", () => { + const value = motionValue(0) + const callback = jest.fn() + + value.on("animationStart", callback) + + expect(callback).not.toBeCalled() + + animate(value, 2) + + expect(callback).toBeCalledTimes(1) + }) + + test("animationCancel event fires", () => { + const value = motionValue(0) + const callback = jest.fn() + + value.on("animationCancel", callback) + + expect(callback).not.toBeCalled() + + animate(value, 1) + animate(value, 2) + + expect(callback).toBeCalledTimes(1) + }) + + test("animationComplete event fires", async () => { + const value = motionValue(0) + const callback = jest.fn() + + value.on("animationComplete", callback) + + expect(callback).not.toBeCalled() + + animate(value, 1, { duration: 0.01 }) + + return new Promise((resolve) => { + setTimeout(() => { + expect(callback).toBeCalledTimes(1) + resolve() + }, 100) + }) + }) +}) diff --git a/packages/framer-motion/src/value/index.ts b/packages/framer-motion/src/value/index.ts index bcb4a674fd..1fa670db0c 100644 --- a/packages/framer-motion/src/value/index.ts +++ b/packages/framer-motion/src/value/index.ts @@ -23,9 +23,9 @@ export interface MotionValueEventCallbacks { animationStart: () => void animationComplete: () => void animationCancel: () => void - change: (latest: V) => void + change: (latestValue: V) => void renderRequest: () => void - velocityChange: (latest: number) => void + velocityChange: (latestVelocity: number) => void } const isFloat = (value: any): value is string => { From bc5c76affc18c2140055e39b84c8caf2fe1ecbd4 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 14 Dec 2022 12:01:21 +0100 Subject: [PATCH 3/3] Updating changelog --- CHANGELOG.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00c886ff8a..458d6efa94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,19 @@ Undocumented APIs should be considered internal and may change without warning. ### Added - `.on()` event method to `MotionValue`. -- `"animationStart"`, `"animationComplete"`, and `"animationCancel"` events for `MotionValue`. +- `"animationStart"`, `"animationComplete"`, `"animationCancel"` and `"change"` events for `MotionValue`. + +## [7.9.0] 2022-12-14 + +### Added + +- Hardware-accelerated `opacity` animations. + +## [7.8.1] 2022-12-14 + +### Changed + +- Refactored animation pipeline to better accomodate WAAPI. ## [7.9.1] 2022-12-14