index.mjs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. import { warnOnce, SubscriptionManager, velocityPerSecond } from 'motion-utils';
  2. import { time } from '../frameloop/sync-time.mjs';
  3. import { frame } from '../frameloop/frame.mjs';
  4. /**
  5. * Maximum time between the value of two frames, beyond which we
  6. * assume the velocity has since been 0.
  7. */
  8. const MAX_VELOCITY_DELTA = 30;
  9. const isFloat = (value) => {
  10. return !isNaN(parseFloat(value));
  11. };
  12. const collectMotionValues = {
  13. current: undefined,
  14. };
  15. /**
  16. * `MotionValue` is used to track the state and velocity of motion values.
  17. *
  18. * @public
  19. */
  20. class MotionValue {
  21. /**
  22. * @param init - The initiating value
  23. * @param config - Optional configuration options
  24. *
  25. * - `transformer`: A function to transform incoming values with.
  26. */
  27. constructor(init, options = {}) {
  28. /**
  29. * This will be replaced by the build step with the latest version number.
  30. * When MotionValues are provided to motion components, warn if versions are mixed.
  31. */
  32. this.version = "12.7.3";
  33. /**
  34. * Tracks whether this value can output a velocity. Currently this is only true
  35. * if the value is numerical, but we might be able to widen the scope here and support
  36. * other value types.
  37. *
  38. * @internal
  39. */
  40. this.canTrackVelocity = null;
  41. /**
  42. * An object containing a SubscriptionManager for each active event.
  43. */
  44. this.events = {};
  45. this.updateAndNotify = (v, render = true) => {
  46. const currentTime = time.now();
  47. /**
  48. * If we're updating the value during another frame or eventloop
  49. * than the previous frame, then the we set the previous frame value
  50. * to current.
  51. */
  52. if (this.updatedAt !== currentTime) {
  53. this.setPrevFrameValue();
  54. }
  55. this.prev = this.current;
  56. this.setCurrent(v);
  57. // Update update subscribers
  58. if (this.current !== this.prev && this.events.change) {
  59. this.events.change.notify(this.current);
  60. }
  61. // Update render subscribers
  62. if (render && this.events.renderRequest) {
  63. this.events.renderRequest.notify(this.current);
  64. }
  65. };
  66. this.hasAnimated = false;
  67. this.setCurrent(init);
  68. this.owner = options.owner;
  69. }
  70. setCurrent(current) {
  71. this.current = current;
  72. this.updatedAt = time.now();
  73. if (this.canTrackVelocity === null && current !== undefined) {
  74. this.canTrackVelocity = isFloat(this.current);
  75. }
  76. }
  77. setPrevFrameValue(prevFrameValue = this.current) {
  78. this.prevFrameValue = prevFrameValue;
  79. this.prevUpdatedAt = this.updatedAt;
  80. }
  81. /**
  82. * Adds a function that will be notified when the `MotionValue` is updated.
  83. *
  84. * It returns a function that, when called, will cancel the subscription.
  85. *
  86. * When calling `onChange` inside a React component, it should be wrapped with the
  87. * `useEffect` hook. As it returns an unsubscribe function, this should be returned
  88. * from the `useEffect` function to ensure you don't add duplicate subscribers..
  89. *
  90. * ```jsx
  91. * export const MyComponent = () => {
  92. * const x = useMotionValue(0)
  93. * const y = useMotionValue(0)
  94. * const opacity = useMotionValue(1)
  95. *
  96. * useEffect(() => {
  97. * function updateOpacity() {
  98. * const maxXY = Math.max(x.get(), y.get())
  99. * const newOpacity = transform(maxXY, [0, 100], [1, 0])
  100. * opacity.set(newOpacity)
  101. * }
  102. *
  103. * const unsubscribeX = x.on("change", updateOpacity)
  104. * const unsubscribeY = y.on("change", updateOpacity)
  105. *
  106. * return () => {
  107. * unsubscribeX()
  108. * unsubscribeY()
  109. * }
  110. * }, [])
  111. *
  112. * return <motion.div style={{ x }} />
  113. * }
  114. * ```
  115. *
  116. * @param subscriber - A function that receives the latest value.
  117. * @returns A function that, when called, will cancel this subscription.
  118. *
  119. * @deprecated
  120. */
  121. onChange(subscription) {
  122. if (process.env.NODE_ENV !== "production") {
  123. warnOnce(false, `value.onChange(callback) is deprecated. Switch to value.on("change", callback).`);
  124. }
  125. return this.on("change", subscription);
  126. }
  127. on(eventName, callback) {
  128. if (!this.events[eventName]) {
  129. this.events[eventName] = new SubscriptionManager();
  130. }
  131. const unsubscribe = this.events[eventName].add(callback);
  132. if (eventName === "change") {
  133. return () => {
  134. unsubscribe();
  135. /**
  136. * If we have no more change listeners by the start
  137. * of the next frame, stop active animations.
  138. */
  139. frame.read(() => {
  140. if (!this.events.change.getSize()) {
  141. this.stop();
  142. }
  143. });
  144. };
  145. }
  146. return unsubscribe;
  147. }
  148. clearListeners() {
  149. for (const eventManagers in this.events) {
  150. this.events[eventManagers].clear();
  151. }
  152. }
  153. /**
  154. * Attaches a passive effect to the `MotionValue`.
  155. */
  156. attach(passiveEffect, stopPassiveEffect) {
  157. this.passiveEffect = passiveEffect;
  158. this.stopPassiveEffect = stopPassiveEffect;
  159. }
  160. /**
  161. * Sets the state of the `MotionValue`.
  162. *
  163. * @remarks
  164. *
  165. * ```jsx
  166. * const x = useMotionValue(0)
  167. * x.set(10)
  168. * ```
  169. *
  170. * @param latest - Latest value to set.
  171. * @param render - Whether to notify render subscribers. Defaults to `true`
  172. *
  173. * @public
  174. */
  175. set(v, render = true) {
  176. if (!render || !this.passiveEffect) {
  177. this.updateAndNotify(v, render);
  178. }
  179. else {
  180. this.passiveEffect(v, this.updateAndNotify);
  181. }
  182. }
  183. setWithVelocity(prev, current, delta) {
  184. this.set(current);
  185. this.prev = undefined;
  186. this.prevFrameValue = prev;
  187. this.prevUpdatedAt = this.updatedAt - delta;
  188. }
  189. /**
  190. * Set the state of the `MotionValue`, stopping any active animations,
  191. * effects, and resets velocity to `0`.
  192. */
  193. jump(v, endAnimation = true) {
  194. this.updateAndNotify(v);
  195. this.prev = v;
  196. this.prevUpdatedAt = this.prevFrameValue = undefined;
  197. endAnimation && this.stop();
  198. if (this.stopPassiveEffect)
  199. this.stopPassiveEffect();
  200. }
  201. /**
  202. * Returns the latest state of `MotionValue`
  203. *
  204. * @returns - The latest state of `MotionValue`
  205. *
  206. * @public
  207. */
  208. get() {
  209. if (collectMotionValues.current) {
  210. collectMotionValues.current.push(this);
  211. }
  212. return this.current;
  213. }
  214. /**
  215. * @public
  216. */
  217. getPrevious() {
  218. return this.prev;
  219. }
  220. /**
  221. * Returns the latest velocity of `MotionValue`
  222. *
  223. * @returns - The latest velocity of `MotionValue`. Returns `0` if the state is non-numerical.
  224. *
  225. * @public
  226. */
  227. getVelocity() {
  228. const currentTime = time.now();
  229. if (!this.canTrackVelocity ||
  230. this.prevFrameValue === undefined ||
  231. currentTime - this.updatedAt > MAX_VELOCITY_DELTA) {
  232. return 0;
  233. }
  234. const delta = Math.min(this.updatedAt - this.prevUpdatedAt, MAX_VELOCITY_DELTA);
  235. // Casts because of parseFloat's poor typing
  236. return velocityPerSecond(parseFloat(this.current) -
  237. parseFloat(this.prevFrameValue), delta);
  238. }
  239. /**
  240. * Registers a new animation to control this `MotionValue`. Only one
  241. * animation can drive a `MotionValue` at one time.
  242. *
  243. * ```jsx
  244. * value.start()
  245. * ```
  246. *
  247. * @param animation - A function that starts the provided animation
  248. */
  249. start(startAnimation) {
  250. this.stop();
  251. return new Promise((resolve) => {
  252. this.hasAnimated = true;
  253. this.animation = startAnimation(resolve);
  254. if (this.events.animationStart) {
  255. this.events.animationStart.notify();
  256. }
  257. }).then(() => {
  258. if (this.events.animationComplete) {
  259. this.events.animationComplete.notify();
  260. }
  261. this.clearAnimation();
  262. });
  263. }
  264. /**
  265. * Stop the currently active animation.
  266. *
  267. * @public
  268. */
  269. stop() {
  270. if (this.animation) {
  271. this.animation.stop();
  272. if (this.events.animationCancel) {
  273. this.events.animationCancel.notify();
  274. }
  275. }
  276. this.clearAnimation();
  277. }
  278. /**
  279. * Returns `true` if this value is currently animating.
  280. *
  281. * @public
  282. */
  283. isAnimating() {
  284. return !!this.animation;
  285. }
  286. clearAnimation() {
  287. delete this.animation;
  288. }
  289. /**
  290. * Destroy and clean up subscribers to this `MotionValue`.
  291. *
  292. * The `MotionValue` hooks like `useMotionValue` and `useTransform` automatically
  293. * handle the lifecycle of the returned `MotionValue`, so this method is only necessary if you've manually
  294. * created a `MotionValue` via the `motionValue` function.
  295. *
  296. * @public
  297. */
  298. destroy() {
  299. this.clearListeners();
  300. this.stop();
  301. if (this.stopPassiveEffect) {
  302. this.stopPassiveEffect();
  303. }
  304. }
  305. }
  306. function motionValue(init, options) {
  307. return new MotionValue(init, options);
  308. }
  309. export { MotionValue, collectMotionValues, motionValue };