| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- import { warnOnce, SubscriptionManager, velocityPerSecond } from 'motion-utils';
- import { time } from '../frameloop/sync-time.mjs';
- import { frame } from '../frameloop/frame.mjs';
- /**
- * Maximum time between the value of two frames, beyond which we
- * assume the velocity has since been 0.
- */
- const MAX_VELOCITY_DELTA = 30;
- const isFloat = (value) => {
- return !isNaN(parseFloat(value));
- };
- const collectMotionValues = {
- current: undefined,
- };
- /**
- * `MotionValue` is used to track the state and velocity of motion values.
- *
- * @public
- */
- class MotionValue {
- /**
- * @param init - The initiating value
- * @param config - Optional configuration options
- *
- * - `transformer`: A function to transform incoming values with.
- */
- constructor(init, options = {}) {
- /**
- * This will be replaced by the build step with the latest version number.
- * When MotionValues are provided to motion components, warn if versions are mixed.
- */
- this.version = "12.7.3";
- /**
- * 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
- */
- this.canTrackVelocity = null;
- /**
- * An object containing a SubscriptionManager for each active event.
- */
- this.events = {};
- this.updateAndNotify = (v, render = true) => {
- const currentTime = time.now();
- /**
- * If we're updating the value during another frame or eventloop
- * than the previous frame, then the we set the previous frame value
- * to current.
- */
- if (this.updatedAt !== currentTime) {
- this.setPrevFrameValue();
- }
- this.prev = this.current;
- this.setCurrent(v);
- // Update update subscribers
- if (this.current !== this.prev && this.events.change) {
- this.events.change.notify(this.current);
- }
- // Update render subscribers
- if (render && this.events.renderRequest) {
- this.events.renderRequest.notify(this.current);
- }
- };
- this.hasAnimated = false;
- this.setCurrent(init);
- this.owner = options.owner;
- }
- setCurrent(current) {
- this.current = current;
- this.updatedAt = time.now();
- if (this.canTrackVelocity === null && current !== undefined) {
- this.canTrackVelocity = isFloat(this.current);
- }
- }
- setPrevFrameValue(prevFrameValue = this.current) {
- this.prevFrameValue = prevFrameValue;
- this.prevUpdatedAt = this.updatedAt;
- }
- /**
- * 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..
- *
- * ```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.on("change", updateOpacity)
- * const unsubscribeY = y.on("change", updateOpacity)
- *
- * return () => {
- * unsubscribeX()
- * unsubscribeY()
- * }
- * }, [])
- *
- * return <motion.div style={{ x }} />
- * }
- * ```
- *
- * @param subscriber - A function that receives the latest value.
- * @returns A function that, when called, will cancel this subscription.
- *
- * @deprecated
- */
- onChange(subscription) {
- if (process.env.NODE_ENV !== "production") {
- warnOnce(false, `value.onChange(callback) is deprecated. Switch to value.on("change", callback).`);
- }
- return this.on("change", subscription);
- }
- on(eventName, callback) {
- if (!this.events[eventName]) {
- this.events[eventName] = new SubscriptionManager();
- }
- const unsubscribe = this.events[eventName].add(callback);
- if (eventName === "change") {
- return () => {
- unsubscribe();
- /**
- * If we have no more change listeners by the start
- * of the next frame, stop active animations.
- */
- frame.read(() => {
- if (!this.events.change.getSize()) {
- this.stop();
- }
- });
- };
- }
- return unsubscribe;
- }
- clearListeners() {
- for (const eventManagers in this.events) {
- this.events[eventManagers].clear();
- }
- }
- /**
- * Attaches a passive effect to the `MotionValue`.
- */
- attach(passiveEffect, stopPassiveEffect) {
- this.passiveEffect = passiveEffect;
- this.stopPassiveEffect = stopPassiveEffect;
- }
- /**
- * 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, render = true) {
- if (!render || !this.passiveEffect) {
- this.updateAndNotify(v, render);
- }
- else {
- this.passiveEffect(v, this.updateAndNotify);
- }
- }
- setWithVelocity(prev, current, delta) {
- this.set(current);
- this.prev = undefined;
- this.prevFrameValue = prev;
- this.prevUpdatedAt = this.updatedAt - delta;
- }
- /**
- * Set the state of the `MotionValue`, stopping any active animations,
- * effects, and resets velocity to `0`.
- */
- jump(v, endAnimation = true) {
- this.updateAndNotify(v);
- this.prev = v;
- this.prevUpdatedAt = this.prevFrameValue = undefined;
- endAnimation && this.stop();
- if (this.stopPassiveEffect)
- this.stopPassiveEffect();
- }
- /**
- * Returns the latest state of `MotionValue`
- *
- * @returns - The latest state of `MotionValue`
- *
- * @public
- */
- get() {
- if (collectMotionValues.current) {
- collectMotionValues.current.push(this);
- }
- 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() {
- const currentTime = time.now();
- if (!this.canTrackVelocity ||
- this.prevFrameValue === undefined ||
- currentTime - this.updatedAt > MAX_VELOCITY_DELTA) {
- return 0;
- }
- const delta = Math.min(this.updatedAt - this.prevUpdatedAt, MAX_VELOCITY_DELTA);
- // Casts because of parseFloat's poor typing
- return velocityPerSecond(parseFloat(this.current) -
- parseFloat(this.prevFrameValue), delta);
- }
- /**
- * 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
- */
- start(startAnimation) {
- this.stop();
- return new Promise((resolve) => {
- this.hasAnimated = true;
- this.animation = startAnimation(resolve);
- if (this.events.animationStart) {
- this.events.animationStart.notify();
- }
- }).then(() => {
- if (this.events.animationComplete) {
- this.events.animationComplete.notify();
- }
- this.clearAnimation();
- });
- }
- /**
- * Stop the currently active animation.
- *
- * @public
- */
- stop() {
- if (this.animation) {
- this.animation.stop();
- if (this.events.animationCancel) {
- this.events.animationCancel.notify();
- }
- }
- this.clearAnimation();
- }
- /**
- * Returns `true` if this value is currently animating.
- *
- * @public
- */
- isAnimating() {
- return !!this.animation;
- }
- clearAnimation() {
- delete this.animation;
- }
- /**
- * 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.clearListeners();
- this.stop();
- if (this.stopPassiveEffect) {
- this.stopPassiveEffect();
- }
- }
- }
- function motionValue(init, options) {
- return new MotionValue(init, options);
- }
- export { MotionValue, collectMotionValues, motionValue };
|