From b8514aeedd1fae78aa54a059bfe1249be41b6516 Mon Sep 17 00:00:00 2001 From: JonathonRP Date: Wed, 13 Nov 2024 23:13:17 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20more=20rewrite=20work=20?= =?UTF-8?q?on=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hooks rewriting BREAKING CHANGE: ๐Ÿงจ all โœ… Closes: none --- .../animation/hooks/UseAnimatedState.svelte | 93 ++-- .../PresenceChild/PresenceChild.svelte | 2 +- .../context/ScaleCorrectionProvider.svelte | 2 +- .../gestures/UseHoverGesture.svelte | 15 +- .../gestures/drag/UseDragControls.svelte | 2 +- .../motion/features/layout/Animate.svelte | 2 +- .../motion/utils/UseVisualElement.svelte | 2 +- src/lib/motion-start/render/VisualElement.ts | 8 +- .../motion-start/render/html/UseStyle.svelte | 2 +- .../utils/UseMotionValueEvent.svelte | 10 +- .../motion-start/utils/use-animation-frame.ts | 24 +- src/lib/motion-start/utils/use-cycle.ts | 64 ++- src/lib/motion-start/utils/use-in-view.ts | 19 +- .../utils/use-motion-value-event.ts | 25 + src/lib/motion-start/value/index.ts | 447 +++++++++++------- src/lib/motion-start/value/scroll/utils.ts | 8 +- .../motion-start/value/use-combine-values.ts | 37 +- src/lib/motion-start/value/use-computed.ts | 21 + .../motion-start/value/use-inverted-scale.ts | 67 +++ .../motion-start/value/use-motion-template.ts | 51 +- .../motion-start/value/use-motion-value.ts | 44 +- src/lib/motion-start/value/use-scroll.ts | 147 +----- src/lib/motion-start/value/use-spring.ts | 121 +++-- src/lib/motion-start/value/use-time.ts | 13 + src/lib/motion-start/value/use-transform.ts | 93 ++-- src/lib/motion-start/value/use-velocity.ts | 54 ++- 26 files changed, 796 insertions(+), 577 deletions(-) create mode 100644 src/lib/motion-start/value/use-computed.ts create mode 100644 src/lib/motion-start/value/use-inverted-scale.ts create mode 100644 src/lib/motion-start/value/use-time.ts diff --git a/src/lib/motion-start/animation/hooks/UseAnimatedState.svelte b/src/lib/motion-start/animation/hooks/UseAnimatedState.svelte index cc87cf5..ef34c99 100755 --- a/src/lib/motion-start/animation/hooks/UseAnimatedState.svelte +++ b/src/lib/motion-start/animation/hooks/UseAnimatedState.svelte @@ -1,9 +1,9 @@ - @@ -120,7 +127,7 @@ class StateVisualElement extends VisualElement diff --git a/src/lib/motion-start/components/AnimatePresence/PresenceChild/PresenceChild.svelte b/src/lib/motion-start/components/AnimatePresence/PresenceChild/PresenceChild.svelte index baf8be1..15db863 100755 --- a/src/lib/motion-start/components/AnimatePresence/PresenceChild/PresenceChild.svelte +++ b/src/lib/motion-start/components/AnimatePresence/PresenceChild/PresenceChild.svelte @@ -1,7 +1,7 @@ - - diff --git a/src/lib/motion-start/motion/features/layout/Animate.svelte b/src/lib/motion-start/motion/features/layout/Animate.svelte index 6962063..c570a02 100755 --- a/src/lib/motion-start/motion/features/layout/Animate.svelte +++ b/src/lib/motion-start/motion/features/layout/Animate.svelte @@ -1,7 +1,7 @@ - diff --git a/src/lib/motion-start/utils/use-animation-frame.ts b/src/lib/motion-start/utils/use-animation-frame.ts index c1e80f8..ced2cd8 100644 --- a/src/lib/motion-start/utils/use-animation-frame.ts +++ b/src/lib/motion-start/utils/use-animation-frame.ts @@ -1,26 +1,32 @@ -// TODO: update +/** +based on framer-motion@11.11.11, +Copyright (c) 2018 Framer B.V. +*/ import { frame, cancelFrame } from '../frameloop'; -import { useContext, useEffect, useRef } from 'react'; import { MotionConfigContext } from '../context/MotionConfigContext'; import type { FrameData } from '../frameloop/types'; +import { getContext, tick } from 'svelte'; +import { get, type Writable } from 'svelte/store'; export type FrameCallback = (timestamp: number, delta: number) => void; -export function useAnimationFrame(callback: FrameCallback) { - const initialTimestamp = useRef(0); - const { isStatic } = useContext(MotionConfigContext); +export function useAnimationFrame(callback: FrameCallback, isCustom = false) { + let initialTimestamp = 0; + const { isStatic } = get( + getContext>(MotionConfigContext) || MotionConfigContext(isCustom) + ); - useEffect(() => { + tick().then(() => { if (isStatic) return; const provideTimeSinceStart = ({ timestamp, delta }: FrameData) => { - if (!initialTimestamp.current) initialTimestamp.current = timestamp; + if (!initialTimestamp) initialTimestamp = timestamp; - callback(timestamp - initialTimestamp.current, delta); + callback(timestamp - initialTimestamp, delta); }; frame.update(provideTimeSinceStart, true); return () => cancelFrame(provideTimeSinceStart); - }, [callback]); + }); } diff --git a/src/lib/motion-start/utils/use-cycle.ts b/src/lib/motion-start/utils/use-cycle.ts index e8e9fb1..9fea763 100755 --- a/src/lib/motion-start/utils/use-cycle.ts +++ b/src/lib/motion-start/utils/use-cycle.ts @@ -1,33 +1,16 @@ /** -based on framer-motion@4.1.17, +based on framer-motion@11.11.11, Copyright (c) 2018 Framer B.V. */ -// TODO: update -// export { default as UseCycle } from './UseCycle.svelte'; -// import { wrap } from "popmotion"; import type { Writable } from 'svelte/store'; -import { useCallback, useRef, useState } from 'react'; import { wrap } from './wrap'; -import { writable } from 'svelte/store'; +import { get, writable } from 'svelte/store'; +import { tick } from 'svelte'; export type Cycle = (i?: number) => void; export type CycleState = [T, Cycle]; -export const useCycle = (...items: T[]) => { - let index = 0; - const x = writable(items[index]) as any satisfies Writable & { - /** Cycle through to next value or set the next value by index. */ - next: (index?: number) => void; - }; - const next = (i?: number) => { - index = typeof i !== 'number' ? wrap(0, items.length, index + 1) : i; - x.set(items[index]); - }; - x.next = next; - return x; -}; - /** * Cycles through a series of visual properties. Can be used to toggle between or cycle through animations. It works similar to `useState` in React. It is provided an initial array of possible states, and returns an array of two arguments. * @@ -55,19 +38,34 @@ export const useCycle = (...items: T[]) => { * @public */ export function useCycle(...items: T[]): CycleState { - const index = useRef(0); - const [item, setItem] = useState(items[index.current]); + let index = 0; + const item = writable(items[index]) as Writable & { + /** Cycle through to next value or set the next value by index. */ + cycle: (length: number, ..._items: T[]) => (next?: number) => void; + }; + + item.cycle = + (_length: number, ..._items: T[]) => + () => { + (next?: number) => { + index = typeof next !== 'number' ? wrap(0, items.length, index + 1) : next; + item.set(items[index]); + }; + }; - const runCycle = useCallback( - (next?: number) => { - index.current = typeof next !== 'number' ? wrap(0, items.length, index.current + 1) : next; + tick().then(() => { + item.cycle = + (_length: number, ..._items: T[]) => + () => { + (next?: number) => { + index = typeof next !== 'number' ? wrap(0, items.length, index + 1) : next; + item.set(items[index]); + }; + }; + }); - setItem(items[index.current]); - }, - // The array will change on each call, but by putting items.length at - // the front of this array, we guarantee the dependency comparison will match up - // eslint-disable-next-line react-hooks/exhaustive-deps - [items.length, ...items] - ); - return [item, runCycle]; + // The array will change on each call, but by putting items.length at + // the front of this array, we guarantee the dependency comparison will match up + // eslint-disable-next-line react-hooks/exhaustive-deps + return [get(item), item.cycle(items.length, ...items)]; } diff --git a/src/lib/motion-start/utils/use-in-view.ts b/src/lib/motion-start/utils/use-in-view.ts index ad600eb..1610325 100644 --- a/src/lib/motion-start/utils/use-in-view.ts +++ b/src/lib/motion-start/utils/use-in-view.ts @@ -1,6 +1,11 @@ -// TODO: update -import { type RefObject, useEffect, useState } from 'react'; +/** +based on framer-motion@11.11.11, +Copyright (c) 2018 Framer B.V. +*/ + +import type { RefObject } from './safe-react-types'; import { inView, type InViewOptions } from '../render/dom/viewport'; +import { tick } from 'svelte'; export interface UseInViewOptions extends Omit { root?: RefObject; @@ -9,15 +14,15 @@ export interface UseInViewOptions extends Omit } export function useInView(ref: RefObject, { root, margin, amount, once = false }: UseInViewOptions = {}) { - const [isInView, setInView] = useState(false); + let isInView = false; - useEffect(() => { + tick().then(() => { if (!ref.current || (once && isInView)) return; const onEnter = () => { - setInView(true); + isInView = true; - return once ? undefined : () => setInView(false); + return once ? undefined : () => (isInView = false); }; const options: InViewOptions = { @@ -27,7 +32,7 @@ export function useInView(ref: RefObject, { root, margin, amount, once }; return inView(ref.current, onEnter, options); - }, [root, ref, margin, once, amount]); + }); return isInView; } diff --git a/src/lib/motion-start/utils/use-motion-value-event.ts b/src/lib/motion-start/utils/use-motion-value-event.ts index 4322bc1..e9cfc65 100644 --- a/src/lib/motion-start/utils/use-motion-value-event.ts +++ b/src/lib/motion-start/utils/use-motion-value-event.ts @@ -1 +1,26 @@ +/** +based on framer-motion@11.11.11, +Copyright (c) 2018 Framer B.V. +*/ + +import { beforeUpdate, tick } from 'svelte'; +import type { MotionValue, MotionValueEventCallbacks } from '../value'; + +export function useMotionValueEvent>( + value: MotionValue, + event: EventName, + callback: MotionValueEventCallbacks[EventName] +) { + const memo = () => value.on(event, callback); + /** + * useInsertionEffect will create subscriptions before any other + * effects will run. Effects run upwards through the tree so it + * can be that binding a useLayoutEffect higher up the tree can + * miss changes from lower down the tree. + */ + beforeUpdate(() => memo()); + + tick().then((_) => memo()); +} + export { default as UseMotionValueEvent } from './UseMotionValueEvent.svelte'; diff --git a/src/lib/motion-start/value/index.ts b/src/lib/motion-start/value/index.ts index 08c6404..bfba79e 100755 --- a/src/lib/motion-start/value/index.ts +++ b/src/lib/motion-start/value/index.ts @@ -1,16 +1,17 @@ /** -based on framer-motion@4.0.3, +based on framer-motion@11.11.11, Copyright (c) 2018 Framer B.V. */ + import { fixed } from '../utils/fix-process-env.js'; -import sync, { getFrameData } from 'framesync'; -import { velocityPerSecond } from 'popmotion'; -import { SubscriptionManager } from '../utils/subscription-manager.js'; -/** -based on framer-motion@4.1.17, -Copyright (c) 2018 Framer B.V. -*/ import type { Writable, Unsubscriber } from 'svelte/store'; +import type { AnimationPlaybackControls } from '../animation/types'; +import { frame } from '../frameloop'; +import { SubscriptionManager } from '../utils/subscription-manager'; +import { velocityPerSecond } from '../utils/velocity-per-second'; +import { warnOnce } from '../utils/warn-once'; +import { time } from '../frameloop/sync-time'; + export type Transformer = (v: T) => T; /** * @public @@ -20,7 +21,44 @@ export type Subscriber = (v: T) => void; * @public */ export type PassiveEffect = (v: T, safeSetter: (v: T) => void) => void; -export type StartAnimation = (complete: () => void) => (() => void) | undefined; +export type StartAnimation = (complete: () => void) => AnimationPlaybackControls | undefined; + +export interface MotionValueEventCallbacks { + animationStart: () => void; + animationComplete: () => void; + animationCancel: () => void; + change: (latestValue: V) => void; + renderRequest: () => void; +} + +/** + * 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: any): value is string => { + return !Number.isNaN(Number.parseFloat(value)); +}; + +interface ResolvedValues { + [key: string]: string | number; +} + +export interface Owner { + current: HTMLElement | unknown; + getProps: () => { onUpdate?: (latest: ResolvedValues) => void }; +} + +export interface MotionValueOptions { + owner?: Owner; + startStopNotifier?: () => () => void; +} + +export const collectMotionValues: { current: MotionValue[] | undefined } = { + current: undefined, +}; + /** * `MotionValue` is used to track the state and velocity of motion values. * @@ -44,48 +82,48 @@ export class MotionValue implements Writable { update = (cb: (value: V) => V): void => { this.set(cb(this.get())); }; + /** - * The current state of the `MotionValue`. - * - * @internal + * 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. */ - private current: V; + version = '__VERSION__'; + /** - * The previous state of the `MotionValue`. - * - * @internal + * If a MotionValue has an owner, it was created internally within Motion + * and therefore has no external listeners. It is therefore safe to animate via WAAPI. */ - private prev: V; + owner?: Owner; + /** - * Duration, in milliseconds, since last updating frame. + * The current state of the `MotionValue`. * * @internal */ - private timeDelta = 0; + private current: V | undefined; + /** - * Timestamp of the last time this `MotionValue` was updated. + * The previous state of the `MotionValue`. * * @internal */ - private lastUpdated = 0; + private prev: V | undefined; + /** - * Functions to notify when the `MotionValue` updates. - * - * @internal + * The previous state of the `MotionValue` at the end of the previous frame. */ - private updateSubscribers = new SubscriptionManager(); + private prevFrameValue: V | undefined; + /** - * Functions to notify when the velocity updates. - * - * @internal + * The last time the `MotionValue` was updated. */ - velocityUpdateSubscribers: SubscriptionManager> = new SubscriptionManager(); + private updatedAt: number; + /** - * Functions to notify when the `MotionValue` updates and `render` is set to `true`. - * - * @internal + * The time `prevFrameValue` was updated. */ - private renderSubscribers = new SubscriptionManager(); + private prevUpdatedAt: number | undefined; + /** * Add a passive effect to this `MotionValue`. * @@ -96,12 +134,13 @@ export class MotionValue implements Writable { * @internal */ private passiveEffect?: PassiveEffect; + private stopPassiveEffect?: VoidFunction; + /** - * A reference to the currently-controlling Popmotion animation - * - * @internal + * A reference to the currently-controlling animation. */ - private stopAnimation?: (() => void) | null | undefined; + animation?: AnimationPlaybackControls; + /** * 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 @@ -109,42 +148,39 @@ export class MotionValue implements Writable { * * @internal */ - private canTrackVelocity = false; + private canTrackVelocity: boolean | null = null; + + /** + * Tracks whether this value should be removed + * @internal + */ + liveStyle?: boolean; private onSubscription = () => {}; private onUnsubscription = () => {}; /** * @param init - The initiating value - * @param startStopNotifier - a function that is called, once the first subscriber is added to this motion value. - * The return function is called, when the last subscriber unsubscribes. + * @param config - Optional configuration options * * - `transformer`: A function to transform incoming values with. * * @internal */ - constructor(init: V, startStopNotifier?: () => () => void) { - this.prev = this.current = init; - this.canTrackVelocity = isFloat(this.current); + constructor(init: V, options: MotionValueOptions = {}) { + this.setCurrent(init); + this.owner = options.owner; + + const { startStopNotifier } = options; if (startStopNotifier) { this.onSubscription = () => { - if ( - this.updateSubscribers.getSize() + - this.velocityUpdateSubscribers.getSize() + - this.renderSubscribers.getSize() === - 0 - ) { + if (Object.entries(this.events).reduce((acc, [_key, currEvent]) => acc + currEvent.getSize(), 0) === 0) { const unsub = startStopNotifier(); this.onUnsubscription = () => {}; if (unsub) { this.onUnsubscription = () => { - if ( - this.updateSubscribers.getSize() + - this.velocityUpdateSubscribers.getSize() + - this.renderSubscribers.getSize() === - 0 - ) { + if (Object.entries(this.events).reduce((acc, [_key, currEvent]) => acc + currEvent.getSize(), 0) === 0) { unsub(); } }; @@ -153,6 +189,21 @@ export class MotionValue implements Writable { }; } } + + setCurrent(current: V) { + this.current = current; + this.updatedAt = time.now(); + + if (this.canTrackVelocity === null && current !== undefined) { + this.canTrackVelocity = isFloat(this.current); + } + } + + setPrevFrameValue(prevFrameValue: V | undefined = this.current) { + this.prevFrameValue = prevFrameValue; + this.prevUpdatedAt = this.updatedAt; + } + /** * Adds a function that will be notified when the `MotionValue` is updated. * @@ -195,46 +246,74 @@ export class MotionValue implements Writable { * @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) => { - this.onSubscription(); - const unsub = this.updateSubscribers.add(subscription); - return () => { - unsub(); - this.onUnsubscription(); - }; - }; - clearListeners = (): void => { - this.updateSubscribers.clear(); - this.onUnsubscription(); + if (process.env.NODE_ENV !== 'production') { + warnOnce(false, `value.onChange(callback) is deprecated. Switch to value.on("change", callback).`); + } + 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): (() => void) => { + private events: { + [key: string]: SubscriptionManager; + } = {}; + + on>( + eventName: EventName, + callback: MotionValueEventCallbacks[EventName] + ) { + if (!this.events[eventName]) { + this.events[eventName] = new SubscriptionManager(); + } + this.onSubscription(); - // Render immediately - subscription(this.get()); - const unsub = this.renderSubscribers.add(subscription); + + const unsubscribe = this.events[eventName].add(callback); + + if (eventName === 'change') { + return () => { + unsubscribe(); + this.onUnsubscription(); + + /** + * 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 () => { - unsub(); + unsubscribe(); this.onUnsubscription(); }; - }; + } + + clearListeners() { + for (const eventManagers in this.events) { + this.events[eventManagers].clear(); + } + this.onUnsubscription(); + } + /** * Attaches a passive effect to the `MotionValue`. * * @internal */ - attach = (passiveEffect: PassiveEffect): void => { + attach(passiveEffect: PassiveEffect, stopPassiveEffect: VoidFunction) { this.passiveEffect = passiveEffect; - }; + this.stopPassiveEffect = stopPassiveEffect; + } + /** * Sets the state of the `MotionValue`. * @@ -250,44 +329,60 @@ export class MotionValue implements Writable { * * @public */ - set = (v: V, render?: boolean): void => { - if (render === void 0) { - render = true; - } + set(v: V, render = true) { if (!render || !this.passiveEffect) { this.updateAndNotify(v, render); } else { this.passiveEffect(v, this.updateAndNotify); } - }; - updateAndNotify = (v: V, render?: boolean): void => { - if (render === void 0) { - render = true; + } + + setWithVelocity(prev: V, current: V, delta: number) { + 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: V, endAnimation = true) { + this.updateAndNotify(v); + this.prev = v; + this.prevUpdatedAt = this.prevFrameValue = undefined; + endAnimation && this.stop(); + if (this.stopPassiveEffect) this.stopPassiveEffect(); + } + + updateAndNotify = (v: 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.current = v; - // Update timestamp - var _a = getFrameData(), - delta = _a.delta, - timestamp = _a.timestamp; - if (this.lastUpdated !== timestamp) { - this.timeDelta = delta; - this.lastUpdated = timestamp; - sync.postRender(this.scheduleVelocityCheck); - } + + this.setCurrent(v); + // Update update subscribers - if (this.prev !== this.current) { - this.updateSubscribers.notify(this.current); - } - // Update velocity subscribers - if (this.velocityUpdateSubscribers.getSize()) { - this.velocityUpdateSubscribers.notify(this.getVelocity()); + if (this.current !== this.prev && this.events.change) { + this.events.change.notify(this.current); } + // Update render subscribers - if (render) { - this.renderSubscribers.notify(this.current); + if (render && this.events.renderRequest) { + this.events.renderRequest.notify(this.current); } }; + /** * Returns the latest state of `MotionValue` * @@ -295,18 +390,25 @@ export class MotionValue implements Writable { * * @public */ - get = (): V => { + get() { this.onSubscription(); - const curr = this.current; + + if (collectMotionValues.current) { + collectMotionValues.current.push(this); + } + const curr = this.current!; + this.onUnsubscription(); return curr; - }; + } + /** * @public */ - getPrevious = (): V => { + getPrevious() { return this.prev; - }; + } + /** * Returns the latest velocity of `MotionValue` * @@ -314,47 +416,33 @@ export class MotionValue implements Writable { * * @public */ - getVelocity = (): number => { - // This could be isFloat(this.prev) && isFloat(this.current), but that would be wasteful + getVelocity() { + const currentTime = time.now(); + + if ( + !this.canTrackVelocity || + this.prevFrameValue === undefined || + currentTime - this.updatedAt > MAX_VELOCITY_DELTA + ) { + return 0; + } + this.onSubscription(); - const vel = this.canTrackVelocity - ? // These casts could be avoided if parseFloat would be typed better - velocityPerSecond( - Number.parseFloat(String(this.current)) - Number.parseFloat(String(this.prev)), - this.timeDelta - ) - : 0; + + const delta = Math.min(this.updatedAt - this.prevUpdatedAt!, MAX_VELOCITY_DELTA); + + // Casts because of parseFloat's poor typing + const vel = velocityPerSecond( + Number.parseFloat(this.current as any) - Number.parseFloat(this.prevFrameValue as any), + delta + ); + this.onUnsubscription(); return vel; - }; - /** - * 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 = () => { - return 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 = (_a: { timestamp: any }) => { - var timestamp = _a.timestamp; - if (timestamp !== this.lastUpdated) { - this.prev = this.current; - this.velocityUpdateSubscribers.notify(this.getVelocity()); - } - }; + } + hasAnimated = false; + /** * Registers a new animation to control this `MotionValue`. Only one * animation can drive a `MotionValue` at one time. @@ -367,35 +455,49 @@ export class MotionValue implements Writable { * * @internal */ - start = (animation: StartAnimation): Promise => { + start(startAnimation: StartAnimation) { this.stop(); - return new Promise((resolve) => { - this.hasAnimated = true; // @ts-expect-error - this.stopAnimation = animation(resolve); - }).then(() => { - return this.clearAnimation(); + const { promise, resolve } = Promise.withResolvers(); + + this.hasAnimated = true; + this.animation = startAnimation(resolve); + if (this.events.animationStart) { + this.events.animationStart.notify(); + } + + return promise.then(() => { + if (this.events.animationComplete) { + this.events.animationComplete.notify(); + } + this.clearAnimation(); }); - }; + } /** * Stop the currently active animation. * * @public */ - stop = (): void => { - if (this.stopAnimation) this.stopAnimation(); + 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 = (): boolean => { - return !!this.stopAnimation; - }; - private clearAnimation = () => { - this.stopAnimation = null; - }; + isAnimating() { + return !!this.animation; + } + + private clearAnimation() { + delete this.animation; + } /** * Destroy and clean up subscribers to this `MotionValue`. * @@ -405,19 +507,20 @@ export class MotionValue implements Writable { * * @public */ - destroy = (): void => { - this.updateSubscribers.clear(); - this.renderSubscribers.clear(); + destroy() { + this.clearListeners(); this.stop(); + + if (this.passiveEffect) { + this.stopPassiveEffect?.(); + } this.onUnsubscription(); - }; + } } -var isFloat = (value: unknown) => !isNaN(Number.parseFloat(value as string)); - /** * @internal */ -export function motionValue(init: V, startStopNotifier?: () => () => void): MotionValue { - return new MotionValue(init, startStopNotifier); +export function motionValue(init: V, options?: MotionValueOptions): MotionValue { + return new MotionValue(init, options); } diff --git a/src/lib/motion-start/value/scroll/utils.ts b/src/lib/motion-start/value/scroll/utils.ts index 194ac13..99aa145 100755 --- a/src/lib/motion-start/value/scroll/utils.ts +++ b/src/lib/motion-start/value/scroll/utils.ts @@ -43,10 +43,10 @@ export function createScrollMotionValues(startStopNotifier?: () => Promise<() => : () => void {}; return { - scrollX: motionValue(0, jointNotifier('x')), - scrollY: motionValue(0, jointNotifier('y')), - scrollXProgress: motionValue(0, jointNotifier('xp')), - scrollYProgress: motionValue(0, jointNotifier('yp')), + scrollX: motionValue(0, { startStopNotifier: jointNotifier('x') }), + scrollY: motionValue(0, { startStopNotifier: jointNotifier('y') }), + scrollXProgress: motionValue(0, { startStopNotifier: jointNotifier('xp') }), + scrollYProgress: motionValue(0, { startStopNotifier: jointNotifier('yp') }), }; } diff --git a/src/lib/motion-start/value/use-combine-values.ts b/src/lib/motion-start/value/use-combine-values.ts index 6cebbc6..bb9a8fb 100755 --- a/src/lib/motion-start/value/use-combine-values.ts +++ b/src/lib/motion-start/value/use-combine-values.ts @@ -1,17 +1,14 @@ /** -based on framer-motion@4.1.17, +based on framer-motion@11.11.11, Copyright (c) 2018 Framer B.V. */ -import type { MotionValue } from '.'; -/** -based on framer-motion@4.0.3, -Copyright (c) 2018 Framer B.V. -*/ -import sync from 'framesync'; +import type { MotionValue } from '.'; import { motionValue } from '.'; +import { beforeUpdate } from 'svelte'; +import { cancelFrame, frame } from '../frameloop'; -export const useCombineMotionValues = (values: (MotionValue | (() => R))[], combineValues: () => R) => { +export const useCombineMotionValues = (values: MotionValue[], combineValues: () => R) => { let subscriptions: (() => void)[] = []; let vals = values; @@ -21,13 +18,15 @@ export const useCombineMotionValues = (values: (MotionValue | (() => R))[], c } }; const subscribe = () => { - subscriptions = vals.map((val) => (val as MotionValue).onChange(handler)); + subscriptions = vals.map((val) => val.on('change', handler)); updateValue(); }; - const value = motionValue(combineValues(), () => { - unsubscribe(); - subscribe(); - return unsubscribe; + const value = motionValue(combineValues(), { + startStopNotifier: () => { + unsubscribe(); + subscribe(); + return unsubscribe; + }, }) as MotionValue & { reset: (values: MotionValue[], combineValues: () => R) => void }; let updateValue = () => { @@ -35,7 +34,7 @@ export const useCombineMotionValues = (values: (MotionValue | (() => R))[], c }; const handler = () => { - sync.update(updateValue, false, true); + frame.update(updateValue, false, true); }; value.reset = (_values, _combineValues) => { @@ -48,6 +47,16 @@ export const useCombineMotionValues = (values: (MotionValue | (() => R))[], c subscribe(); }; + beforeUpdate(() => { + const scheduleUpdate = () => frame.preRender(updateValue, false, true); + const subscriptions = values.map((v) => v.on('change', scheduleUpdate)); + + return () => { + subscriptions.forEach((unsubscribe) => unsubscribe()); + cancelFrame(updateValue); + }; + }); + return value; }; // export { default as UseCombineMotionValues } from "./UseCombineValues.svelte"; diff --git a/src/lib/motion-start/value/use-computed.ts b/src/lib/motion-start/value/use-computed.ts new file mode 100644 index 0000000..b325bd0 --- /dev/null +++ b/src/lib/motion-start/value/use-computed.ts @@ -0,0 +1,21 @@ +import { collectMotionValues, type MotionValue } from '.'; +import { useCombineMotionValues } from './use-combine-values'; + +export function useComputed(compute: () => O) { + /** + * Open session of collectMotionValues. Any MotionValue that calls get() + * will be saved into this array. + */ + collectMotionValues.current = []; + + compute(); + + const value = useCombineMotionValues(collectMotionValues.current, compute); + + /** + * Synchronously close session of collectMotionValues. + */ + collectMotionValues.current = undefined; + + return value; +} diff --git a/src/lib/motion-start/value/use-inverted-scale.ts b/src/lib/motion-start/value/use-inverted-scale.ts new file mode 100644 index 0000000..95bcd41 --- /dev/null +++ b/src/lib/motion-start/value/use-inverted-scale.ts @@ -0,0 +1,67 @@ +import { useTransform } from '../value/use-transform'; +import type { MotionValue } from './'; +import { invariant, warning } from '../utils/errors'; +import { useMotionValue } from './use-motion-value'; +import { MotionContext, type MotionContextProps } from '../context/MotionContext'; +import { getContext } from 'svelte'; +import { get, type Writable } from 'svelte/store'; + +interface ScaleMotionValues { + scaleX: MotionValue; + scaleY: MotionValue; +} + +// Keep things reasonable and avoid scale: Infinity. In practise we might need +// to add another value, opacity, that could interpolate scaleX/Y [0,0.01] => [0,1] +// to simply hide content at unreasonable scales. +const maxScale = 100000; +export const invertScale = (scale: number) => (scale > 0.001 ? 1 / scale : maxScale); + +let hasWarned = false; + +/** + * Returns a `MotionValue` each for `scaleX` and `scaleY` that update with the inverse + * of their respective parent scales. + * + * This is useful for undoing the distortion of content when scaling a parent component. + * + * By default, `useInvertedScale` will automatically fetch `scaleX` and `scaleY` from the nearest parent. + * By passing other `MotionValue`s in as `useInvertedScale({ scaleX, scaleY })`, it will invert the output + * of those instead. + * + * ```jsx + * const MyComponent = () => { + * const { scaleX, scaleY } = useInvertedScale() + * return + * } + * ``` + * + * @deprecated + */ +export function useInvertedScale(scale?: Partial, isCustom = false): ScaleMotionValues { + let parentScaleX = useMotionValue(1); + let parentScaleY = useMotionValue(1); + const { visualElement } = get(getContext>(MotionContext) || MotionContext(isCustom)); + + invariant( + !!(scale || visualElement), + 'If no scale values are provided, useInvertedScale must be used within a child of another motion component.' + ); + + warning(hasWarned, 'useInvertedScale is deprecated and will be removed in 3.0. Use the layout prop instead.'); + + hasWarned = true; + + if (scale) { + parentScaleX = scale.scaleX || parentScaleX; + parentScaleY = scale.scaleY || parentScaleY; + } else if (visualElement) { + parentScaleX = visualElement.getValue('scaleX', 1); + parentScaleY = visualElement.getValue('scaleY', 1); + } + + const scaleX = useTransform(parentScaleX, invertScale); + const scaleY = useTransform(parentScaleY, invertScale); + + return { scaleX, scaleY }; +} diff --git a/src/lib/motion-start/value/use-motion-template.ts b/src/lib/motion-start/value/use-motion-template.ts index 49912d4..a9c2683 100755 --- a/src/lib/motion-start/value/use-motion-template.ts +++ b/src/lib/motion-start/value/use-motion-template.ts @@ -2,9 +2,10 @@ based on framer-motion@4.1.17, Copyright (c) 2018 Framer B.V. */ -import { MotionValue } from "."; +import type { MotionValue } from '.'; -import { useCombineMotionValues } from "./use-combine-values" +import { useCombineMotionValues } from './use-combine-values'; +import { isMotionValue } from './utils/is-motion-value'; /** * Combine multiple motion values into a new one using a string template literal. @@ -29,29 +30,31 @@ import { useCombineMotionValues } from "./use-combine-values" * @public */ -export const useMotionTemplate = (fragments: TemplateStringsArray, ...values: MotionValue[]) => { - /** - * Create a function that will build a string from the latest motion values. - */ - let numFragments = fragments.length; - const buildValue = () => { - let output = `` +export const useMotionTemplate = (fragments: TemplateStringsArray, ...values: Array) => { + /** + * Create a function that will build a string from the latest motion values. + */ + let numFragments = fragments.length; + const buildValue = () => { + let output = ``; - for (let i = 0; i < numFragments; i++) { - output += fragments[i] - const value = values[i] - if (value) output += values[i].get() - } + for (let i = 0; i < numFragments; i++) { + output += fragments[i]; + const value = values[i]; + if (value) output += isMotionValue(value) ? value.get() : value; + } - return output - } - const value = useCombineMotionValues(values, buildValue) as any; - value.resetInner = value.reset; + return output; + }; + const value: any = useCombineMotionValues(values.filter(isMotionValue), buildValue); + value.resetInner = value.reset; - value.reset = (f: TemplateStringsArray, ...vs: MotionValue[]) => { - numFragments = f.length; - value.resetInner(vs,buildValue) - } + value.reset = (f: TemplateStringsArray, ...vs: MotionValue[]) => { + numFragments = f.length; + value.resetInner(vs, buildValue); + }; - return value as MotionValue & { reset: (fragments: TemplateStringsArray, ...values: MotionValue[]) => void }; -} \ No newline at end of file + return value as MotionValue & { + reset: (fragments: TemplateStringsArray, ...values: MotionValue[]) => void; + }; +}; diff --git a/src/lib/motion-start/value/use-motion-value.ts b/src/lib/motion-start/value/use-motion-value.ts index 6207565..644ce8f 100755 --- a/src/lib/motion-start/value/use-motion-value.ts +++ b/src/lib/motion-start/value/use-motion-value.ts @@ -1,27 +1,47 @@ /** -based on framer-motion@4.1.17, +based on framer-motion@11.11.11, Copyright (c) 2018 Framer B.V. */ + +import { getContext, onMount } from 'svelte'; +import { motionValue, type MotionValue } from '.'; +import { MotionConfigContext } from '../context/MotionConfigContext'; +import { get, type Writable } from 'svelte/store'; + /** * Creates a `MotionValue` to track the state and velocity of a value. * * Usually, these are created automatically. For advanced use-cases, like use with `useTransform`, you can create `MotionValue`s externally and pass them into the animated component via the `style` prop. * - * @motion - * * ```jsx - * - *
- * + * + * return + * } * ``` - * + * * @param initial - The initial state. * * @public */ -export { motionValue as useMotionValue } from './index.js'; -// export { default as UseMotionValue } from './UseMotionValue.svelte'; +export function useMotionValue(initial: T, isCustom = false): MotionValue { + const value = motionValue(initial); + + /** + * If this motion value is being used in static mode, like on + * the Framer canvas, force components to rerender when the motion + * value is updated. + */ + const { isStatic } = get( + getContext>(MotionConfigContext) || MotionConfigContext(isCustom) + ); + + onMount(() => { + if (isStatic) { + value.on('change', (value) => (initial = value)); + } + }); + + return value; +} diff --git a/src/lib/motion-start/value/use-scroll.ts b/src/lib/motion-start/value/use-scroll.ts index cbb8849..a0183c9 100644 --- a/src/lib/motion-start/value/use-scroll.ts +++ b/src/lib/motion-start/value/use-scroll.ts @@ -1,146 +1,11 @@ /** -based on framer-motion@4.1.17, +based on framer-motion@11.11.11, Copyright (c) 2018 Framer B.V. */ -import type { ScrollMotionValues } from './scroll/utils'; -/** -based on framer-motion@4.1.16, -Copyright (c) 2018 Framer B.V. -*/ -import { createScrollMotionValues, createScrollUpdater } from './scroll/utils'; -import { addDomEvent } from '../events/use-dom-event'; +import { createScrollMotionValues } from './scroll/utils'; import { tick } from 'svelte'; - -let viewportScrollValues: ScrollMotionValues; - -function getViewportScrollOffsets() { - return { - xOffset: window.pageXOffset, - yOffset: window.pageYOffset, - xMaxOffset: document.body.clientWidth - window.innerWidth, - yMaxOffset: document.body.clientHeight - window.innerHeight, - }; -} - -let hasListeners = false; - -function addEventListeners() { - hasListeners = true; - if (typeof window === 'undefined') return; - - const updateScrollValues = createScrollUpdater(viewportScrollValues, getViewportScrollOffsets); - - addDomEvent(window, 'scroll', updateScrollValues, { passive: true }); - addDomEvent(window, 'resize', updateScrollValues); -} - -/** - * Returns MotionValues that update when the viewport scrolls: - * - * - `scrollX` โ€” Horizontal scroll distance in pixels. - * - `scrollY` โ€” Vertical scroll distance in pixels. - * - `scrollXProgress` โ€” Horizontal scroll progress between `0` and `1`. - * - `scrollYProgress` โ€” Vertical scroll progress between `0` and `1`. - * - * **Warning:** Setting `body` or `html` to `height: 100%` or similar will break the `Progress` - * values as this breaks the browser's capability to accurately measure the page length. - * - * @motion - * - * ```jsx - * export const MyComponent = () => { - * const { scrollYProgress } = useViewportScroll() - * return - * } - * ``` - * - * @public - */ -export function useViewportScroll() { - /** - * Lazy-initialise the viewport scroll values - */ - if (!viewportScrollValues) { - viewportScrollValues = createScrollMotionValues(); - } - - tick().then((_) => { - !hasListeners && addEventListeners(); - }); - - return viewportScrollValues as ScrollMotionValues; -} - -/** -based on framer-motion@4.1.17, -Copyright (c) 2018 Framer B.V. -*/ -import type { ScrollMotionValues } from './utils'; - -/** -based on framer-motion@4.1.16, -Copyright (c) 2018 Framer B.V. -*/ -import { createScrollMotionValues, createScrollUpdater } from './utils'; -import { addDomEvent } from '../../events/use-dom-event'; - -const getElementScrollOffsets = - (element: { - scrollLeft: any; - scrollTop: any; - scrollWidth: number; - offsetWidth: number; - scrollHeight: number; - offsetHeight: number; - }) => - () => { - return { - xOffset: element.scrollLeft, - yOffset: element.scrollTop, - xMaxOffset: element.scrollWidth - element.offsetWidth, - yMaxOffset: element.scrollHeight - element.offsetHeight, - }; - }; - -export const useElementScroll = (ref: { current: HTMLElement | null }) => { - const values = {}; - - const setScroll = async () => { - if (typeof window === 'undefined') return () => {}; - - let times = 10; - while ((!ref || !ref.current) && !values.ref) { - if (times-- < 1) { - return () => {}; - } - - await new Promise((r) => setTimeout(() => r(), 200)); - } - const element = ref && ref.current ? ref : values.ref; - - const updateScrollValues = createScrollUpdater(values, getElementScrollOffsets(element)); - - const scrollListener = addDomEvent(element, 'scroll', updateScrollValues, { passive: true }); - - const resizeListener = addDomEvent(element, 'resize', updateScrollValues); - return () => { - scrollListener && scrollListener(); - resizeListener && resizeListener(); - }; - }; - Object.assign(values, createScrollMotionValues(setScroll)); - - return values as ScrollMotionValues; -}; - -//export { default as UseElementScroll } from './UseElementScroll.svelte'; - import type { RefObject } from '../utils/safe-react-types'; -import { motionValue } from '.'; -import { useConstant } from '../utils/use-constant'; -import { useEffect } from 'react'; -import { useIsomorphicLayoutEffect } from '../three-entry'; import { warning } from '../utils/errors'; import { scroll } from '../render/dom/scroll'; import type { ScrollInfoOptions } from '../render/dom/scroll/types'; @@ -159,11 +24,9 @@ function refWarning(name: string, ref?: RefObject) { } export function useScroll({ container, target, layoutEffect = true, ...options }: UseScrollOptions = {}) { - const values = useConstant(createScrollMotionValues); + const values = createScrollMotionValues(); - const useLifecycleEffect = layoutEffect ? useIsomorphicLayoutEffect : useEffect; - - useLifecycleEffect(() => { + tick().then(() => { refWarning('target', target); refWarning('container', container); @@ -180,7 +43,7 @@ export function useScroll({ container, target, layoutEffect = true, ...options } target: target?.current || undefined, } ); - }, [container, target, JSON.stringify(options.offset)]); + }); return values; } diff --git a/src/lib/motion-start/value/use-spring.ts b/src/lib/motion-start/value/use-spring.ts index 71731bf..fc7f2af 100755 --- a/src/lib/motion-start/value/use-spring.ts +++ b/src/lib/motion-start/value/use-spring.ts @@ -1,22 +1,18 @@ -/** -based on framer-motion@4.1.17, -Copyright (c) 2018 Framer B.V. -*/ -import type { SpringOptions } from "popmotion"; -import { MotionValue } from "."; - -/** -based on framer-motion@4.1.16, -Copyright (c) 2018 Framer B.V. -*/ - import { fixed } from '../utils/fix-process-env'; -import { getContext } from "svelte" -import { MotionConfigContext, type MotionConfigContextObject } from "../context/MotionConfigContext" +import { beforeUpdate, getContext, tick } from 'svelte'; import { get, type Writable } from 'svelte/store'; -import { useMotionValue } from "./use-motion-value"; -import { isMotionValue } from "./utils/is-motion-value"; -import { animate } from "popmotion" +import type { MotionValue } from '../value'; +import { isMotionValue } from './utils/is-motion-value'; +import { useMotionValue } from './use-motion-value'; +import { MotionConfigContext } from '../context/MotionConfigContext'; +import type { SpringOptions } from '../animation/types'; +import { frame, frameData } from '../frameloop'; +import { type MainThreadAnimation, animateValue } from '../animation/animators/MainThreadAnimation'; + +function toNumber(v: string | number) { + if (typeof v === 'number') return v; + return Number.parseFloat(v); +} /** * Creates a `MotionValue` that, when `set`, will use a spring animation to animate to its new state. @@ -38,47 +34,76 @@ import { animate } from "popmotion" * @public */ export const useSpring = (source: MotionValue | number, config: SpringOptions = {}, isCustom = false) => { + const mcc = getContext>(MotionConfigContext) || MotionConfigContext(isCustom); - const mcc = getContext>(MotionConfigContext) || MotionConfigContext(isCustom); + let activeSpringAnimation: MainThreadAnimation | null = null; - let activeSpringAnimation: { stop: () => void } | null = null; + const value = useMotionValue(isMotionValue(source) ? toNumber(source.get()) : source) as MotionValue & { + reset: (_: MotionValue, config: SpringOptions) => void; + }; - let value = useMotionValue(isMotionValue(source) ? source.get() : source) as MotionValue & { reset: (_: any, config: SpringOptions) => void }; + let latestValue = value.get(); + let latestSetter: (v: number) => void = () => {}; - let cleanup: () => void; - const update = (_source: typeof source, _config: typeof config) => { - value.attach((v, set) => { + const startAnimation = () => { + /** + * If the previous animation hasn't had the chance to even render a frame, render it now. + */ + const animation = activeSpringAnimation; + if (animation && animation.time === 0) { + animation.sample(frameData.delta); + } - const { isStatic } = get(mcc); + stopAnimation(); - if (isStatic) { - return set(v); - } - if (activeSpringAnimation) { - activeSpringAnimation.stop(); - } - activeSpringAnimation = animate({ - from: value.get(), - to: v, - velocity: value.getVelocity(), - ..._config, - onUpdate: set, - }) + activeSpringAnimation = animateValue({ + keyframes: [value.get(), latestValue], + velocity: value.getVelocity(), + type: 'spring', + restDelta: 0.001, + restSpeed: 0.01, + ...config, + onUpdate: latestSetter, + }); + }; - return value.get(); - }) - cleanup?.() - return isMotionValue(_source) ? - _source.onChange(v => value.set(parseFloat(v))) : - undefined - } + const stopAnimation = () => { + if (activeSpringAnimation) { + activeSpringAnimation.stop(); + } + }; - update(source, config); + const update = (_config: typeof config) => { + value.attach((v, set) => { + const { isStatic } = get(mcc); - value.reset = update; + if (isStatic) { + return set(v); + } - return value; -} + latestValue = v; + latestSetter = set; + + frame.update(startAnimation); + + return value.get(); + }, stopAnimation); + }; + + beforeUpdate(() => { + update(config); + }); + + tick().then(() => { + if (isMotionValue(source)) { + return source.on('change', (v) => value.set(Number.parseFloat(v))); + } + }); + + value.reset = (_value, _config) => update(_config); + + return value; +}; //export { default as UseSpring } from './UseSpring.svelte'; diff --git a/src/lib/motion-start/value/use-time.ts b/src/lib/motion-start/value/use-time.ts new file mode 100644 index 0000000..6dc2c5b --- /dev/null +++ b/src/lib/motion-start/value/use-time.ts @@ -0,0 +1,13 @@ +/** +based on framer-motion@11.11.11, +Copyright (c) 2018 Framer B.V. +*/ + +import { useAnimationFrame } from '../utils/use-animation-frame'; +import { useMotionValue } from './use-motion-value'; + +export function useTime() { + const time = useMotionValue(0); + useAnimationFrame((t) => time.set(t)); + return time; +} diff --git a/src/lib/motion-start/value/use-transform.ts b/src/lib/motion-start/value/use-transform.ts index a9d10d1..1d01325 100755 --- a/src/lib/motion-start/value/use-transform.ts +++ b/src/lib/motion-start/value/use-transform.ts @@ -1,12 +1,12 @@ -import type { TransformOptions } from '../utils/transform'; /** - based on framer-motion@4.1.17, - Copyright (c) 2018 Framer B.V. - */ -import type { MotionValue } from '.'; +based on framer-motion@11.11.11, +Copyright (c) 2018 Framer B.V. +*/ -import { transform } from '../utils/transform'; +import type { MotionValue } from '.'; +import { transform, type TransformOptions } from '../utils/transform'; import { useCombineMotionValues } from './use-combine-values'; +import { useComputed } from './use-computed'; export type InputRange = number[]; type SingleTransformer = (input: I) => O; @@ -152,49 +152,68 @@ export function useTransform( * * @public */ +export function useTransform( + input: MotionValue[] | MotionValue[] | MotionValue[], + transformer: MultiTransformer +): MotionValue; +export function useTransform(transformer: () => O): MotionValue; export function useTransform( input: MotionValue | MotionValue[] | MotionValue[] | MotionValue[] | (() => O), inputRangeOrTransformer?: InputRange | Transformer, outputRange?: O[], options?: TransformOptions ) { - type Input = typeof input; - type inputRangeOrTransformer = typeof inputRangeOrTransformer; - type OutputRange = typeof outputRange; - type Options = typeof options;// @ts-expect-error - const latest: I & (string | number)[] & number & any[{}] = [] as any; - + let transformer: any; const update = ( - input: Input, - inputRangeOrTransformer?: inputRangeOrTransformer, - outputRange?: OutputRange, - options?: Options + _input: typeof input, + _inputRangeOrTransformer?: typeof inputRangeOrTransformer, + _outputRange?: typeof outputRange, + _options?: typeof options ) => { - const transformer = - typeof inputRangeOrTransformer === 'function' - ? inputRangeOrTransformer - : transform(inputRangeOrTransformer!, outputRange!, options); - const values = Array.isArray(input) ? input : [input]; - const _transformer = Array.isArray(input) ? transformer : ([latest]: any[]) => transformer(latest); - return [ - values, - () => { - latest.length = 0; - const numValues = values.length; - for (let i = 0; i < numValues; i++) { - // @ts-expect-error - latest[i] = values[i].get(); - } - return _transformer(latest); - }, - ] as const; + if (typeof _input === 'function') { + return useComputed(_input); + } + const _transformer = + typeof _inputRangeOrTransformer === 'function' + ? _inputRangeOrTransformer + : transform(_inputRangeOrTransformer!, _outputRange!, _options); + transformer = Array.isArray(_input) + ? _transformer + : ([_latest]) => (_transformer as SingleTransformer)(_latest); + return Array.isArray(_input) + ? useListTransform(_input, transformer as MultiTransformer) + : useListTransform([_input], ([_latest]) => (transformer as SingleTransformer)(_latest)); }; - const comb = useCombineMotionValues(...update(input, inputRangeOrTransformer, outputRange, options)); + const comb = update(input, inputRangeOrTransformer, outputRange, options); (comb as any).updateInner = comb.reset; - comb.reset = (input, inputRangeOrTransformer?, outputRange?: OutputRange, options?: Options) => - (comb as any).updateInner(...update(input, inputRangeOrTransformer, outputRange, options)); + const latest: I[] = []; + comb.reset = (_input, _inputRangeOrTransformer, _outputRange?: typeof outputRange, _options?: typeof options) => + (comb as any).updateInner(_input, () => { + latest.length = 0; + const numValues = _input.length; + for (let i = 0; i < numValues; i++) { + latest[i] = _input[i].get(); + } + + return transformer(latest); + }); return comb; } + +function useListTransform(values: MotionValue[], transformer: MultiTransformer) { + const latest: I[] = []; + + return useCombineMotionValues(values, () => { + latest.length = 0; + const numValues = values.length; + for (let i = 0; i < numValues; i++) { + latest[i] = values[i].get(); + } + + return transformer(latest); + }); +} + // export { default as UseTransform } from './UseTransform.svelte'; diff --git a/src/lib/motion-start/value/use-velocity.ts b/src/lib/motion-start/value/use-velocity.ts index 6230015..f018227 100755 --- a/src/lib/motion-start/value/use-velocity.ts +++ b/src/lib/motion-start/value/use-velocity.ts @@ -1,9 +1,12 @@ /** -based on framer-motion@4.1.17, +based on framer-motion@11.11.11, Copyright (c) 2018 Framer B.V. */ -import type { MotionValue } from './index.js'; -import { useMotionValue } from './use-motion-value.js'; + +import { motionValue, type MotionValue } from '.'; +import { frame } from '../frameloop'; +import { useMotionValueEvent } from '../utils/use-motion-value-event'; + /** * Creates a `MotionValue` that updates when the velocity of the provided `MotionValue` changes. * @@ -16,28 +19,43 @@ import { useMotionValue } from './use-motion-value.js'; * @public */ export const useVelocity = (value: MotionValue) => { - let val = value; + let latest: number; let cleanup: () => void; + const updateVelocity = () => { + latest = value.getVelocity(); + velocity.set(latest); + + /** + * If we still have velocity, schedule an update for the next frame + * to keep checking until it is zero. + */ + if (latest) frame.update(updateVelocity); + }; - const reset = (value: MotionValue) => { - cleanup?.(); - val = value; - cleanup = val.velocityUpdateSubscribers.add((newVelocity) => { - velocity.set(newVelocity); - }); + const reset = (_value: MotionValue) => { + updateVelocity?.(); + latest = _value.getVelocity(); + cleanup = updateVelocity; }; - const velocity = useMotionValue(value.getVelocity(), () => { - cleanup?.(); - cleanup = val.velocityUpdateSubscribers.add((newVelocity) => { - velocity.set(newVelocity); - }); - return () => { - cleanup?.(); - }; + const velocity = motionValue(value.getVelocity(), { + startStopNotifier: () => { + updateVelocity?.(); + cleanup = updateVelocity; + return () => { + cleanup?.(); + }; + }, }) as MotionValue & { reset: typeof reset }; + // do we need this reset or cleanup and startStopNotifier? + velocity.reset = reset; + useMotionValueEvent(value, 'change', () => { + // Schedule an update to this value at the end of the current frame. + frame.update(updateVelocity, false, true); + }); + return velocity; };