UNPKG

framer-motion

Version:

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

394 lines (356 loc) • 10.2 kB
import sync, { getFrameData, FrameData } from "framesync" import { velocityPerSecond } from "popmotion" import { subscriptionManager } from "../utils/subscription-manager" export type Transformer<T> = (v: T) => T /** * @public */ export type Subscriber<T> = (v: T) => void /** * @public */ export type PassiveEffect<T> = (v: T, safeSetter: (v: T) => void) => void export type StartAnimation = (complete: () => void) => () => void const isFloat = (value: any): value is string => { return !isNaN(parseFloat(value)) } /** * `MotionValue` is used to track the state and velocity of motion values. * * @public */ export class MotionValue<V = any> { /** * The current state of the `MotionValue`. * * @internal */ private current: V /** * The previous state of the `MotionValue`. * * @internal */ private prev: V /** * Duration, in milliseconds, since last updating frame. * * @internal */ private timeDelta: number = 0 /** * Timestamp of the last time this `MotionValue` was updated. * * @internal */ private lastUpdated: number = 0 /** * Functions to notify when the `MotionValue` updates. * * @internal */ private updateSubscribers = subscriptionManager<Subscriber<V>>() /** * Functions to notify when the `MotionValue` updates and `render` is set to `true`. * * @internal */ private renderSubscribers = subscriptionManager<Subscriber<V>>() /** * Add a passive effect to this `MotionValue`. * * A passive effect intercepts calls to `set`. For instance, `useSpring` adds * a passive effect that attaches a `spring` to the latest * set value. Hypothetically there could be a `useSmooth` that attaches an input smoothing effect. * * @internal */ private passiveEffect?: PassiveEffect<V> /** * A reference to the currently-controlling Popmotion animation * * @internal */ private stopAnimation?: null | (() => void) /** * Tracks whether this value can output a velocity. Currently this is only true * if the value is numerical, but we might be able to widen the scope here and support * other value types. * * @internal */ private canTrackVelocity = false /** * @param init - The initiating value * @param config - Optional configuration options * * - `transformer`: A function to transform incoming values with. * * @internal */ constructor(init: V) { this.current = init this.canTrackVelocity = isFloat(this.current) } /** * Adds a function that will be notified when the `MotionValue` is updated. * * It returns a function that, when called, will cancel the subscription. * * When calling `onChange` inside a React component, it should be wrapped with the * `useEffect` hook. As it returns an unsubscribe function, this should be returned * from the `useEffect` function to ensure you don't add duplicate subscribers.. * * @library * * ```jsx * function MyComponent() { * const x = useMotionValue(0) * const y = useMotionValue(0) * const opacity = useMotionValue(1) * * useEffect(() => { * function updateOpacity() { * const maxXY = Math.max(x.get(), y.get()) * const newOpacity = transform(maxXY, [0, 100], [1, 0]) * opacity.set(newOpacity) * } * * const unsubscribeX = x.onChange(updateOpacity) * const unsubscribeY = y.onChange(updateOpacity) * * return () => { * unsubscribeX() * unsubscribeY() * } * }, []) * * return <Frame x={x} /> * } * ``` * * @motion * * ```jsx * export const MyComponent = () => { * const x = useMotionValue(0) * const y = useMotionValue(0) * const opacity = useMotionValue(1) * * useEffect(() => { * function updateOpacity() { * const maxXY = Math.max(x.get(), y.get()) * const newOpacity = transform(maxXY, [0, 100], [1, 0]) * opacity.set(newOpacity) * } * * const unsubscribeX = x.onChange(updateOpacity) * const unsubscribeY = y.onChange(updateOpacity) * * return () => { * unsubscribeX() * unsubscribeY() * } * }, []) * * return <motion.div style={{ x }} /> * } * ``` * * @internalremarks * * We could look into a `useOnChange` hook if the above lifecycle management proves confusing. * * ```jsx * useOnChange(x, () => {}) * ``` * * @param subscriber - A function that receives the latest value. * @returns A function that, when called, will cancel this subscription. * * @public */ onChange(subscription: Subscriber<V>): () => void { return this.updateSubscribers.add(subscription) } clearListeners() { this.updateSubscribers.clear() } /** * 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 */ onRenderRequest(subscription: Subscriber<V>) { // Render immediately subscription(this.get()) return this.renderSubscribers.add(subscription) } /** * Attaches a passive effect to the `MotionValue`. * * @internal */ attach(passiveEffect: PassiveEffect<V>) { this.passiveEffect = passiveEffect } /** * Sets the state of the `MotionValue`. * * @remarks * * ```jsx * const x = useMotionValue(0) * x.set(10) * ``` * * @param latest - Latest value to set. * @param render - Whether to notify render subscribers. Defaults to `true` * * @public */ set(v: V, render = true) { if (!render || !this.passiveEffect) { this.updateAndNotify(v, render) } else { this.passiveEffect(v, this.updateAndNotify) } } updateAndNotify = (v: V, render = true) => { this.prev = this.current this.current = v if (this.prev !== this.current) { this.updateSubscribers.notify(this.current) } if (render) { this.renderSubscribers.notify(this.current) } // Update timestamp const { delta, timestamp } = getFrameData() if (this.lastUpdated !== timestamp) { this.timeDelta = delta this.lastUpdated = timestamp sync.postRender(this.scheduleVelocityCheck) } } /** * Returns the latest state of `MotionValue` * * @returns - The latest state of `MotionValue` * * @public */ get() { return this.current } /** * @public */ getPrevious() { return this.prev } /** * Returns the latest velocity of `MotionValue` * * @returns - The latest velocity of `MotionValue`. Returns `0` if the state is non-numerical. * * @public */ getVelocity() { // This could be isFloat(this.prev) && isFloat(this.current), but that would be wasteful return this.canTrackVelocity ? // These casts could be avoided if parseFloat would be typed better velocityPerSecond( parseFloat(this.current as any) - parseFloat(this.prev as any), this.timeDelta ) : 0 } /** * Schedule a velocity check for the next frame. * * This is an instanced and bound function to prevent generating a new * function once per frame. * * @internal */ private scheduleVelocityCheck = () => sync.postRender(this.velocityCheck) /** * Updates `prev` with `current` if the value hasn't been updated this frame. * This ensures velocity calculations return `0`. * * This is an instanced and bound function to prevent generating a new * function once per frame. * * @internal */ private velocityCheck = ({ timestamp }: FrameData) => { if (timestamp !== this.lastUpdated) { this.prev = this.current } } hasAnimated = false /** * Registers a new animation to control this `MotionValue`. Only one * animation can drive a `MotionValue` at one time. * * ```jsx * value.start() * ``` * * @param animation - A function that starts the provided animation * * @internal */ start(animation: StartAnimation) { this.stop() return new Promise((resolve) => { this.hasAnimated = true this.stopAnimation = animation(resolve) }).then(() => this.clearAnimation()) } /** * Stop the currently active animation. * * @public */ stop() { if (this.stopAnimation) this.stopAnimation() this.clearAnimation() } /** * Returns `true` if this value is currently animating. * * @public */ isAnimating() { return !!this.stopAnimation } private clearAnimation() { this.stopAnimation = null } /** * Destroy and clean up subscribers to this `MotionValue`. * * The `MotionValue` hooks like `useMotionValue` and `useTransform` automatically * handle the lifecycle of the returned `MotionValue`, so this method is only necessary if you've manually * created a `MotionValue` via the `motionValue` function. * * @public */ destroy() { this.updateSubscribers.clear() this.renderSubscribers.clear() this.stop() } } /** * @internal */ export function motionValue<V>(init: V) { return new MotionValue<V>(init) }