MainThreadAnimation.mjs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. import { isGenerator, calcGeneratorDuration, activeAnimations } from 'motion-dom';
  2. import { invariant, millisecondsToSeconds, secondsToMilliseconds } from 'motion-utils';
  3. import { KeyframeResolver } from '../../render/utils/KeyframesResolver.mjs';
  4. import { clamp } from '../../utils/clamp.mjs';
  5. import { mix } from '../../utils/mix/index.mjs';
  6. import { pipe } from '../../utils/pipe.mjs';
  7. import { inertia } from '../generators/inertia.mjs';
  8. import { keyframes } from '../generators/keyframes.mjs';
  9. import { spring } from '../generators/spring/index.mjs';
  10. import { BaseAnimation } from './BaseAnimation.mjs';
  11. import { frameloopDriver } from './drivers/driver-frameloop.mjs';
  12. import { getFinalKeyframe } from './waapi/utils/get-final-keyframe.mjs';
  13. const generators = {
  14. decay: inertia,
  15. inertia,
  16. tween: keyframes,
  17. keyframes: keyframes,
  18. spring,
  19. };
  20. const percentToProgress = (percent) => percent / 100;
  21. /**
  22. * Animation that runs on the main thread. Designed to be WAAPI-spec in the subset of
  23. * features we expose publically. Mostly the compatibility is to ensure visual identity
  24. * between both WAAPI and main thread animations.
  25. */
  26. class MainThreadAnimation extends BaseAnimation {
  27. constructor(options) {
  28. super(options);
  29. /**
  30. * The time at which the animation was paused.
  31. */
  32. this.holdTime = null;
  33. /**
  34. * The time at which the animation was cancelled.
  35. */
  36. this.cancelTime = null;
  37. /**
  38. * The current time of the animation.
  39. */
  40. this.currentTime = 0;
  41. /**
  42. * Playback speed as a factor. 0 would be stopped, -1 reverse and 2 double speed.
  43. */
  44. this.playbackSpeed = 1;
  45. /**
  46. * The state of the animation to apply when the animation is resolved. This
  47. * allows calls to the public API to control the animation before it is resolved,
  48. * without us having to resolve it first.
  49. */
  50. this.pendingPlayState = "running";
  51. /**
  52. * The time at which the animation was started.
  53. */
  54. this.startTime = null;
  55. this.state = "idle";
  56. /**
  57. * This method is bound to the instance to fix a pattern where
  58. * animation.stop is returned as a reference from a useEffect.
  59. */
  60. this.stop = () => {
  61. this.resolver.cancel();
  62. this.isStopped = true;
  63. if (this.state === "idle")
  64. return;
  65. this.teardown();
  66. const { onStop } = this.options;
  67. onStop && onStop();
  68. };
  69. const { name, motionValue, element, keyframes } = this.options;
  70. const KeyframeResolver$1 = element?.KeyframeResolver || KeyframeResolver;
  71. const onResolved = (resolvedKeyframes, finalKeyframe) => this.onKeyframesResolved(resolvedKeyframes, finalKeyframe);
  72. this.resolver = new KeyframeResolver$1(keyframes, onResolved, name, motionValue, element);
  73. this.resolver.scheduleResolve();
  74. }
  75. flatten() {
  76. super.flatten();
  77. // If we've already resolved the animation, re-initialise it
  78. if (this._resolved) {
  79. Object.assign(this._resolved, this.initPlayback(this._resolved.keyframes));
  80. }
  81. }
  82. initPlayback(keyframes$1) {
  83. const { type = "keyframes", repeat = 0, repeatDelay = 0, repeatType, velocity = 0, } = this.options;
  84. const generatorFactory = isGenerator(type)
  85. ? type
  86. : generators[type] || keyframes;
  87. /**
  88. * If our generator doesn't support mixing numbers, we need to replace keyframes with
  89. * [0, 100] and then make a function that maps that to the actual keyframes.
  90. *
  91. * 100 is chosen instead of 1 as it works nicer with spring animations.
  92. */
  93. let mapPercentToKeyframes;
  94. let mirroredGenerator;
  95. if (process.env.NODE_ENV !== "production" &&
  96. generatorFactory !== keyframes) {
  97. invariant(keyframes$1.length <= 2, `Only two keyframes currently supported with spring and inertia animations. Trying to animate ${keyframes$1}`);
  98. }
  99. if (generatorFactory !== keyframes &&
  100. typeof keyframes$1[0] !== "number") {
  101. mapPercentToKeyframes = pipe(percentToProgress, mix(keyframes$1[0], keyframes$1[1]));
  102. keyframes$1 = [0, 100];
  103. }
  104. const generator = generatorFactory({ ...this.options, keyframes: keyframes$1 });
  105. /**
  106. * If we have a mirror repeat type we need to create a second generator that outputs the
  107. * mirrored (not reversed) animation and later ping pong between the two generators.
  108. */
  109. if (repeatType === "mirror") {
  110. mirroredGenerator = generatorFactory({
  111. ...this.options,
  112. keyframes: [...keyframes$1].reverse(),
  113. velocity: -velocity,
  114. });
  115. }
  116. /**
  117. * If duration is undefined and we have repeat options,
  118. * we need to calculate a duration from the generator.
  119. *
  120. * We set it to the generator itself to cache the duration.
  121. * Any timeline resolver will need to have already precalculated
  122. * the duration by this step.
  123. */
  124. if (generator.calculatedDuration === null) {
  125. generator.calculatedDuration = calcGeneratorDuration(generator);
  126. }
  127. const { calculatedDuration } = generator;
  128. const resolvedDuration = calculatedDuration + repeatDelay;
  129. const totalDuration = resolvedDuration * (repeat + 1) - repeatDelay;
  130. return {
  131. generator,
  132. mirroredGenerator,
  133. mapPercentToKeyframes,
  134. calculatedDuration,
  135. resolvedDuration,
  136. totalDuration,
  137. };
  138. }
  139. onPostResolved() {
  140. const { autoplay = true } = this.options;
  141. activeAnimations.mainThread++;
  142. this.play();
  143. if (this.pendingPlayState === "paused" || !autoplay) {
  144. this.pause();
  145. }
  146. else {
  147. this.state = this.pendingPlayState;
  148. }
  149. }
  150. tick(timestamp, sample = false) {
  151. const { resolved } = this;
  152. // If the animations has failed to resolve, return the final keyframe.
  153. if (!resolved) {
  154. const { keyframes } = this.options;
  155. return { done: true, value: keyframes[keyframes.length - 1] };
  156. }
  157. const { finalKeyframe, generator, mirroredGenerator, mapPercentToKeyframes, keyframes, calculatedDuration, totalDuration, resolvedDuration, } = resolved;
  158. if (this.startTime === null)
  159. return generator.next(0);
  160. const { delay, repeat, repeatType, repeatDelay, onUpdate } = this.options;
  161. /**
  162. * requestAnimationFrame timestamps can come through as lower than
  163. * the startTime as set by performance.now(). Here we prevent this,
  164. * though in the future it could be possible to make setting startTime
  165. * a pending operation that gets resolved here.
  166. */
  167. if (this.speed > 0) {
  168. this.startTime = Math.min(this.startTime, timestamp);
  169. }
  170. else if (this.speed < 0) {
  171. this.startTime = Math.min(timestamp - totalDuration / this.speed, this.startTime);
  172. }
  173. // Update currentTime
  174. if (sample) {
  175. this.currentTime = timestamp;
  176. }
  177. else if (this.holdTime !== null) {
  178. this.currentTime = this.holdTime;
  179. }
  180. else {
  181. // Rounding the time because floating point arithmetic is not always accurate, e.g. 3000.367 - 1000.367 =
  182. // 2000.0000000000002. This is a problem when we are comparing the currentTime with the duration, for
  183. // example.
  184. this.currentTime =
  185. Math.round(timestamp - this.startTime) * this.speed;
  186. }
  187. // Rebase on delay
  188. const timeWithoutDelay = this.currentTime - delay * (this.speed >= 0 ? 1 : -1);
  189. const isInDelayPhase = this.speed >= 0
  190. ? timeWithoutDelay < 0
  191. : timeWithoutDelay > totalDuration;
  192. this.currentTime = Math.max(timeWithoutDelay, 0);
  193. // If this animation has finished, set the current time to the total duration.
  194. if (this.state === "finished" && this.holdTime === null) {
  195. this.currentTime = totalDuration;
  196. }
  197. let elapsed = this.currentTime;
  198. let frameGenerator = generator;
  199. if (repeat) {
  200. /**
  201. * Get the current progress (0-1) of the animation. If t is >
  202. * than duration we'll get values like 2.5 (midway through the
  203. * third iteration)
  204. */
  205. const progress = Math.min(this.currentTime, totalDuration) / resolvedDuration;
  206. /**
  207. * Get the current iteration (0 indexed). For instance the floor of
  208. * 2.5 is 2.
  209. */
  210. let currentIteration = Math.floor(progress);
  211. /**
  212. * Get the current progress of the iteration by taking the remainder
  213. * so 2.5 is 0.5 through iteration 2
  214. */
  215. let iterationProgress = progress % 1.0;
  216. /**
  217. * If iteration progress is 1 we count that as the end
  218. * of the previous iteration.
  219. */
  220. if (!iterationProgress && progress >= 1) {
  221. iterationProgress = 1;
  222. }
  223. iterationProgress === 1 && currentIteration--;
  224. currentIteration = Math.min(currentIteration, repeat + 1);
  225. /**
  226. * Reverse progress if we're not running in "normal" direction
  227. */
  228. const isOddIteration = Boolean(currentIteration % 2);
  229. if (isOddIteration) {
  230. if (repeatType === "reverse") {
  231. iterationProgress = 1 - iterationProgress;
  232. if (repeatDelay) {
  233. iterationProgress -= repeatDelay / resolvedDuration;
  234. }
  235. }
  236. else if (repeatType === "mirror") {
  237. frameGenerator = mirroredGenerator;
  238. }
  239. }
  240. elapsed = clamp(0, 1, iterationProgress) * resolvedDuration;
  241. }
  242. /**
  243. * If we're in negative time, set state as the initial keyframe.
  244. * This prevents delay: x, duration: 0 animations from finishing
  245. * instantly.
  246. */
  247. const state = isInDelayPhase
  248. ? { done: false, value: keyframes[0] }
  249. : frameGenerator.next(elapsed);
  250. if (mapPercentToKeyframes) {
  251. state.value = mapPercentToKeyframes(state.value);
  252. }
  253. let { done } = state;
  254. if (!isInDelayPhase && calculatedDuration !== null) {
  255. done =
  256. this.speed >= 0
  257. ? this.currentTime >= totalDuration
  258. : this.currentTime <= 0;
  259. }
  260. const isAnimationFinished = this.holdTime === null &&
  261. (this.state === "finished" || (this.state === "running" && done));
  262. if (isAnimationFinished && finalKeyframe !== undefined) {
  263. state.value = getFinalKeyframe(keyframes, this.options, finalKeyframe);
  264. }
  265. if (onUpdate) {
  266. onUpdate(state.value);
  267. }
  268. if (isAnimationFinished) {
  269. this.finish();
  270. }
  271. return state;
  272. }
  273. get duration() {
  274. const { resolved } = this;
  275. return resolved ? millisecondsToSeconds(resolved.calculatedDuration) : 0;
  276. }
  277. get time() {
  278. return millisecondsToSeconds(this.currentTime);
  279. }
  280. set time(newTime) {
  281. newTime = secondsToMilliseconds(newTime);
  282. this.currentTime = newTime;
  283. if (this.holdTime !== null || this.speed === 0) {
  284. this.holdTime = newTime;
  285. }
  286. else if (this.driver) {
  287. this.startTime = this.driver.now() - newTime / this.speed;
  288. }
  289. }
  290. get speed() {
  291. return this.playbackSpeed;
  292. }
  293. set speed(newSpeed) {
  294. const hasChanged = this.playbackSpeed !== newSpeed;
  295. this.playbackSpeed = newSpeed;
  296. if (hasChanged) {
  297. this.time = millisecondsToSeconds(this.currentTime);
  298. }
  299. }
  300. play() {
  301. if (!this.resolver.isScheduled) {
  302. this.resolver.resume();
  303. }
  304. if (!this._resolved) {
  305. this.pendingPlayState = "running";
  306. return;
  307. }
  308. if (this.isStopped)
  309. return;
  310. const { driver = frameloopDriver, onPlay, startTime } = this.options;
  311. if (!this.driver) {
  312. this.driver = driver((timestamp) => this.tick(timestamp));
  313. }
  314. onPlay && onPlay();
  315. const now = this.driver.now();
  316. if (this.holdTime !== null) {
  317. this.startTime = now - this.holdTime;
  318. }
  319. else if (!this.startTime) {
  320. this.startTime = startTime ?? this.calcStartTime();
  321. }
  322. else if (this.state === "finished") {
  323. this.startTime = now;
  324. }
  325. if (this.state === "finished") {
  326. this.updateFinishedPromise();
  327. }
  328. this.cancelTime = this.startTime;
  329. this.holdTime = null;
  330. /**
  331. * Set playState to running only after we've used it in
  332. * the previous logic.
  333. */
  334. this.state = "running";
  335. this.driver.start();
  336. }
  337. pause() {
  338. if (!this._resolved) {
  339. this.pendingPlayState = "paused";
  340. return;
  341. }
  342. this.state = "paused";
  343. this.holdTime = this.currentTime ?? 0;
  344. }
  345. complete() {
  346. if (this.state !== "running") {
  347. this.play();
  348. }
  349. this.pendingPlayState = this.state = "finished";
  350. this.holdTime = null;
  351. }
  352. finish() {
  353. this.teardown();
  354. this.state = "finished";
  355. const { onComplete } = this.options;
  356. onComplete && onComplete();
  357. }
  358. cancel() {
  359. if (this.cancelTime !== null) {
  360. this.tick(this.cancelTime);
  361. }
  362. this.teardown();
  363. this.updateFinishedPromise();
  364. }
  365. teardown() {
  366. this.state = "idle";
  367. this.stopDriver();
  368. this.resolveFinishedPromise();
  369. this.updateFinishedPromise();
  370. this.startTime = this.cancelTime = null;
  371. this.resolver.cancel();
  372. activeAnimations.mainThread--;
  373. }
  374. stopDriver() {
  375. if (!this.driver)
  376. return;
  377. this.driver.stop();
  378. this.driver = undefined;
  379. }
  380. sample(time) {
  381. this.startTime = 0;
  382. return this.tick(time, true);
  383. }
  384. get finished() {
  385. return this.currentFinishedPromise;
  386. }
  387. }
  388. // Legacy interface
  389. function animateValue(options) {
  390. return new MainThreadAnimation(options);
  391. }
  392. export { MainThreadAnimation, animateValue };