motion 12.16.1-alpha.0 → 12.17.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,648 +2,13 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var react = require('react');
5
+ var mini = require('framer-motion/mini');
6
6
 
7
- /**
8
- * Creates a constant value over the lifecycle of a component.
9
- *
10
- * Even if `useMemo` is provided an empty array as its final argument, it doesn't offer
11
- * a guarantee that it won't re-run for performance reasons later on. By using `useConstant`
12
- * you can ensure that initialisers don't execute twice or more.
13
- */
14
- function useConstant(init) {
15
- const ref = react.useRef(null);
16
- if (ref.current === null) {
17
- ref.current = init();
18
- }
19
- return ref.current;
20
- }
21
7
 
22
- function useUnmountEffect(callback) {
23
- return react.useEffect(() => () => callback(), []);
24
- }
25
8
 
26
- let invariant = () => { };
27
- if (process.env.NODE_ENV !== "production") {
28
- invariant = (check, message) => {
29
- if (!check) {
30
- throw new Error(message);
31
- }
32
- };
33
- }
34
-
35
- /*#__NO_SIDE_EFFECTS__*/
36
- function memo(callback) {
37
- let result;
38
- return () => {
39
- if (result === undefined)
40
- result = callback();
41
- return result;
42
- };
43
- }
44
-
45
- /*#__NO_SIDE_EFFECTS__*/
46
- const noop = (any) => any;
47
-
48
- /**
49
- * Converts seconds to milliseconds
50
- *
51
- * @param seconds - Time in seconds.
52
- * @return milliseconds - Converted time in milliseconds.
53
- */
54
- /*#__NO_SIDE_EFFECTS__*/
55
- const secondsToMilliseconds = (seconds) => seconds * 1000;
56
- /*#__NO_SIDE_EFFECTS__*/
57
- const millisecondsToSeconds = (milliseconds) => milliseconds / 1000;
58
-
59
- const isBezierDefinition = (easing) => Array.isArray(easing) && typeof easing[0] === "number";
60
-
61
- const generateLinearEasing = (easing, duration, // as milliseconds
62
- resolution = 10 // as milliseconds
63
- ) => {
64
- let points = "";
65
- const numPoints = Math.max(Math.round(duration / resolution), 2);
66
- for (let i = 0; i < numPoints; i++) {
67
- points += easing(i / (numPoints - 1)) + ", ";
68
- }
69
- return `linear(${points.substring(0, points.length - 2)})`;
70
- };
71
-
72
- const isNotNull = (value) => value !== null;
73
- function getFinalKeyframe(keyframes, { repeat, repeatType = "loop" }, finalKeyframe, speed = 1) {
74
- const resolvedKeyframes = keyframes.filter(isNotNull);
75
- const useFirstKeyframe = speed < 0 || (repeat && repeatType !== "loop" && repeat % 2 === 1);
76
- const index = useFirstKeyframe ? 0 : resolvedKeyframes.length - 1;
77
- return !index || finalKeyframe === undefined
78
- ? resolvedKeyframes[index]
79
- : finalKeyframe;
80
- }
81
-
82
- class WithPromise {
83
- constructor() {
84
- this.updateFinished();
85
- }
86
- get finished() {
87
- return this._finished;
88
- }
89
- updateFinished() {
90
- this._finished = new Promise((resolve) => {
91
- this.resolve = resolve;
92
- });
93
- }
94
- notifyFinished() {
95
- this.resolve();
96
- }
97
- /**
98
- * Allows the animation to be awaited.
99
- *
100
- * @deprecated Use `finished` instead.
101
- */
102
- then(onResolve, onReject) {
103
- return this.finished.then(onResolve, onReject);
104
- }
105
- }
106
-
107
- function fillWildcards(keyframes) {
108
- for (let i = 1; i < keyframes.length; i++) {
109
- keyframes[i] ?? (keyframes[i] = keyframes[i - 1]);
110
- }
111
- }
112
-
113
- const isCSSVar = (name) => name.startsWith("--");
114
-
115
- function setStyle(element, name, value) {
116
- isCSSVar(name)
117
- ? element.style.setProperty(name, value)
118
- : (element.style[name] = value);
119
- }
120
-
121
- const supportsScrollTimeline = /* @__PURE__ */ memo(() => window.ScrollTimeline !== undefined);
122
-
123
- /**
124
- * Add the ability for test suites to manually set support flags
125
- * to better test more environments.
126
- */
127
- const supportsFlags = {};
128
-
129
- function memoSupports(callback, supportsFlag) {
130
- const memoized = memo(callback);
131
- return () => supportsFlags[supportsFlag] ?? memoized();
132
- }
133
-
134
- const supportsLinearEasing = /*@__PURE__*/ memoSupports(() => {
135
- try {
136
- document
137
- .createElement("div")
138
- .animate({ opacity: 0 }, { easing: "linear(0, 1)" });
139
- }
140
- catch (e) {
141
- return false;
142
- }
143
- return true;
144
- }, "linearEasing");
145
-
146
- const cubicBezierAsString = ([a, b, c, d]) => `cubic-bezier(${a}, ${b}, ${c}, ${d})`;
147
-
148
- const supportedWaapiEasing = {
149
- linear: "linear",
150
- ease: "ease",
151
- easeIn: "ease-in",
152
- easeOut: "ease-out",
153
- easeInOut: "ease-in-out",
154
- circIn: /*@__PURE__*/ cubicBezierAsString([0, 0.65, 0.55, 1]),
155
- circOut: /*@__PURE__*/ cubicBezierAsString([0.55, 0, 1, 0.45]),
156
- backIn: /*@__PURE__*/ cubicBezierAsString([0.31, 0.01, 0.66, -0.59]),
157
- backOut: /*@__PURE__*/ cubicBezierAsString([0.33, 1.53, 0.69, 0.99]),
158
- };
159
-
160
- function mapEasingToNativeEasing(easing, duration) {
161
- if (!easing) {
162
- return undefined;
163
- }
164
- else if (typeof easing === "function") {
165
- return supportsLinearEasing()
166
- ? generateLinearEasing(easing, duration)
167
- : "ease-out";
168
- }
169
- else if (isBezierDefinition(easing)) {
170
- return cubicBezierAsString(easing);
171
- }
172
- else if (Array.isArray(easing)) {
173
- return easing.map((segmentEasing) => mapEasingToNativeEasing(segmentEasing, duration) ||
174
- supportedWaapiEasing.easeOut);
175
- }
176
- else {
177
- return supportedWaapiEasing[easing];
178
- }
179
- }
180
-
181
- function startWaapiAnimation(element, valueName, keyframes, { delay = 0, duration = 300, repeat = 0, repeatType = "loop", ease = "easeOut", times, } = {}, pseudoElement = undefined) {
182
- const keyframeOptions = {
183
- [valueName]: keyframes,
184
- };
185
- if (times)
186
- keyframeOptions.offset = times;
187
- const easing = mapEasingToNativeEasing(ease, duration);
188
- /**
189
- * If this is an easing array, apply to keyframes, not animation as a whole
190
- */
191
- if (Array.isArray(easing))
192
- keyframeOptions.easing = easing;
193
- const options = {
194
- delay,
195
- duration,
196
- easing: !Array.isArray(easing) ? easing : "linear",
197
- fill: "both",
198
- iterations: repeat + 1,
199
- direction: repeatType === "reverse" ? "alternate" : "normal",
200
- };
201
- if (pseudoElement)
202
- options.pseudoElement = pseudoElement;
203
- const animation = element.animate(keyframeOptions, options);
204
- return animation;
205
- }
206
-
207
- function isGenerator(type) {
208
- return typeof type === "function" && "applyToOptions" in type;
209
- }
210
-
211
- function applyGeneratorOptions({ type, ...options }) {
212
- if (isGenerator(type) && supportsLinearEasing()) {
213
- return type.applyToOptions(options);
214
- }
215
- else {
216
- options.duration ?? (options.duration = 300);
217
- options.ease ?? (options.ease = "easeOut");
218
- }
219
- return options;
220
- }
221
-
222
- /**
223
- * NativeAnimation implements AnimationPlaybackControls for the browser's Web Animations API.
224
- */
225
- class NativeAnimation extends WithPromise {
226
- constructor(options) {
227
- super();
228
- this.finishedTime = null;
229
- this.isStopped = false;
230
- if (!options)
231
- return;
232
- const { element, name, keyframes, pseudoElement, allowFlatten = false, finalKeyframe, onComplete, } = options;
233
- this.isPseudoElement = Boolean(pseudoElement);
234
- this.allowFlatten = allowFlatten;
235
- this.options = options;
236
- invariant(typeof options.type !== "string", `animateMini doesn't support "type" as a string. Did you mean to import { spring } from "motion"?`);
237
- const transition = applyGeneratorOptions(options);
238
- this.animation = startWaapiAnimation(element, name, keyframes, transition, pseudoElement);
239
- if (transition.autoplay === false) {
240
- this.animation.pause();
241
- }
242
- this.animation.onfinish = () => {
243
- this.finishedTime = this.time;
244
- if (!pseudoElement) {
245
- const keyframe = getFinalKeyframe(keyframes, this.options, finalKeyframe, this.speed);
246
- if (this.updateMotionValue) {
247
- this.updateMotionValue(keyframe);
248
- }
249
- else {
250
- /**
251
- * If we can, we want to commit the final style as set by the user,
252
- * rather than the computed keyframe value supplied by the animation.
253
- */
254
- setStyle(element, name, keyframe);
255
- }
256
- this.animation.cancel();
257
- }
258
- onComplete?.();
259
- this.notifyFinished();
260
- };
261
- }
262
- play() {
263
- if (this.isStopped)
264
- return;
265
- this.animation.play();
266
- if (this.state === "finished") {
267
- this.updateFinished();
268
- }
269
- }
270
- pause() {
271
- this.animation.pause();
272
- }
273
- complete() {
274
- this.animation.finish?.();
275
- }
276
- cancel() {
277
- try {
278
- this.animation.cancel();
279
- }
280
- catch (e) { }
281
- }
282
- stop() {
283
- if (this.isStopped)
284
- return;
285
- this.isStopped = true;
286
- const { state } = this;
287
- if (state === "idle" || state === "finished") {
288
- return;
289
- }
290
- if (this.updateMotionValue) {
291
- this.updateMotionValue();
292
- }
293
- else {
294
- this.commitStyles();
295
- }
296
- if (!this.isPseudoElement)
297
- this.cancel();
298
- }
299
- /**
300
- * WAAPI doesn't natively have any interruption capabilities.
301
- *
302
- * In this method, we commit styles back to the DOM before cancelling
303
- * the animation.
304
- *
305
- * This is designed to be overridden by NativeAnimationExtended, which
306
- * will create a renderless JS animation and sample it twice to calculate
307
- * its current value, "previous" value, and therefore allow
308
- * Motion to also correctly calculate velocity for any subsequent animation
309
- * while deferring the commit until the next animation frame.
310
- */
311
- commitStyles() {
312
- if (!this.isPseudoElement) {
313
- this.animation.commitStyles?.();
314
- }
315
- }
316
- get duration() {
317
- const duration = this.animation.effect?.getComputedTiming?.().duration || 0;
318
- return millisecondsToSeconds(Number(duration));
319
- }
320
- get time() {
321
- return millisecondsToSeconds(Number(this.animation.currentTime) || 0);
322
- }
323
- set time(newTime) {
324
- this.finishedTime = null;
325
- this.animation.currentTime = secondsToMilliseconds(newTime);
326
- }
327
- /**
328
- * The playback speed of the animation.
329
- * 1 = normal speed, 2 = double speed, 0.5 = half speed.
330
- */
331
- get speed() {
332
- return this.animation.playbackRate;
333
- }
334
- set speed(newSpeed) {
335
- // Allow backwards playback after finishing
336
- if (newSpeed < 0)
337
- this.finishedTime = null;
338
- this.animation.playbackRate = newSpeed;
339
- }
340
- get state() {
341
- return this.finishedTime !== null
342
- ? "finished"
343
- : this.animation.playState;
344
- }
345
- get startTime() {
346
- return Number(this.animation.startTime);
347
- }
348
- set startTime(newStartTime) {
349
- this.animation.startTime = newStartTime;
350
- }
351
- /**
352
- * Attaches a timeline to the animation, for instance the `ScrollTimeline`.
353
- */
354
- attachTimeline({ timeline, observe }) {
355
- if (this.allowFlatten) {
356
- this.animation.effect?.updateTiming({ easing: "linear" });
357
- }
358
- this.animation.onfinish = null;
359
- if (timeline && supportsScrollTimeline()) {
360
- this.animation.timeline = timeline;
361
- return noop;
362
- }
363
- else {
364
- return observe(this);
365
- }
366
- }
367
- }
368
-
369
- class GroupAnimation {
370
- constructor(animations) {
371
- // Bound to accomadate common `return animation.stop` pattern
372
- this.stop = () => this.runAll("stop");
373
- this.animations = animations.filter(Boolean);
374
- }
375
- get finished() {
376
- return Promise.all(this.animations.map((animation) => animation.finished));
377
- }
378
- /**
379
- * TODO: Filter out cancelled or stopped animations before returning
380
- */
381
- getAll(propName) {
382
- return this.animations[0][propName];
383
- }
384
- setAll(propName, newValue) {
385
- for (let i = 0; i < this.animations.length; i++) {
386
- this.animations[i][propName] = newValue;
387
- }
388
- }
389
- attachTimeline(timeline) {
390
- const subscriptions = this.animations.map((animation) => animation.attachTimeline(timeline));
391
- return () => {
392
- subscriptions.forEach((cancel, i) => {
393
- cancel && cancel();
394
- this.animations[i].stop();
395
- });
396
- };
397
- }
398
- get time() {
399
- return this.getAll("time");
400
- }
401
- set time(time) {
402
- this.setAll("time", time);
403
- }
404
- get speed() {
405
- return this.getAll("speed");
406
- }
407
- set speed(speed) {
408
- this.setAll("speed", speed);
409
- }
410
- get state() {
411
- return this.getAll("state");
412
- }
413
- get startTime() {
414
- return this.getAll("startTime");
415
- }
416
- get duration() {
417
- let max = 0;
418
- for (let i = 0; i < this.animations.length; i++) {
419
- max = Math.max(max, this.animations[i].duration);
420
- }
421
- return max;
422
- }
423
- runAll(methodName) {
424
- this.animations.forEach((controls) => controls[methodName]());
425
- }
426
- play() {
427
- this.runAll("play");
428
- }
429
- pause() {
430
- this.runAll("pause");
431
- }
432
- cancel() {
433
- this.runAll("cancel");
434
- }
435
- complete() {
436
- this.runAll("complete");
437
- }
438
- }
439
-
440
- class GroupAnimationWithThen extends GroupAnimation {
441
- then(onResolve, _onReject) {
442
- return this.finished.finally(onResolve).then(() => { });
443
- }
444
- }
445
-
446
- const animationMaps = new WeakMap();
447
- const animationMapKey = (name, pseudoElement = "") => `${name}:${pseudoElement}`;
448
- function getAnimationMap(element) {
449
- const map = animationMaps.get(element) || new Map();
450
- animationMaps.set(element, map);
451
- return map;
452
- }
453
-
454
- function getValueTransition(transition, key) {
455
- return (transition?.[key] ??
456
- transition?.["default"] ??
457
- transition);
458
- }
459
-
460
- const pxValues = new Set([
461
- // Border props
462
- "borderWidth",
463
- "borderTopWidth",
464
- "borderRightWidth",
465
- "borderBottomWidth",
466
- "borderLeftWidth",
467
- "borderRadius",
468
- "radius",
469
- "borderTopLeftRadius",
470
- "borderTopRightRadius",
471
- "borderBottomRightRadius",
472
- "borderBottomLeftRadius",
473
- // Positioning props
474
- "width",
475
- "maxWidth",
476
- "height",
477
- "maxHeight",
478
- "top",
479
- "right",
480
- "bottom",
481
- "left",
482
- // Spacing props
483
- "padding",
484
- "paddingTop",
485
- "paddingRight",
486
- "paddingBottom",
487
- "paddingLeft",
488
- "margin",
489
- "marginTop",
490
- "marginRight",
491
- "marginBottom",
492
- "marginLeft",
493
- // Misc
494
- "backgroundPositionX",
495
- "backgroundPositionY",
496
- ]);
497
-
498
- function applyPxDefaults(keyframes, name) {
499
- for (let i = 0; i < keyframes.length; i++) {
500
- if (typeof keyframes[i] === "number" && pxValues.has(name)) {
501
- keyframes[i] = keyframes[i] + "px";
502
- }
503
- }
504
- }
505
-
506
- function resolveElements(elementOrSelector, scope, selectorCache) {
507
- if (elementOrSelector instanceof EventTarget) {
508
- return [elementOrSelector];
509
- }
510
- else if (typeof elementOrSelector === "string") {
511
- let root = document;
512
- if (scope) {
513
- root = scope.current;
514
- }
515
- const elements = selectorCache?.[elementOrSelector] ??
516
- root.querySelectorAll(elementOrSelector);
517
- return elements ? Array.from(elements) : [];
518
- }
519
- return Array.from(elementOrSelector);
520
- }
521
-
522
- function getComputedStyle(element, name) {
523
- const computedStyle = window.getComputedStyle(element);
524
- return isCSSVar(name)
525
- ? computedStyle.getPropertyValue(name)
526
- : computedStyle[name];
527
- }
528
-
529
- function animateElements(elementOrSelector, keyframes, options, scope) {
530
- const elements = resolveElements(elementOrSelector, scope);
531
- const numElements = elements.length;
532
- invariant(Boolean(numElements), "No valid element provided.");
533
- /**
534
- * WAAPI doesn't support interrupting animations.
535
- *
536
- * Therefore, starting animations requires a three-step process:
537
- * 1. Stop existing animations (write styles to DOM)
538
- * 2. Resolve keyframes (read styles from DOM)
539
- * 3. Create new animations (write styles to DOM)
540
- *
541
- * The hybrid `animate()` function uses AsyncAnimation to resolve
542
- * keyframes before creating new animations, which removes style
543
- * thrashing. Here, we have much stricter filesize constraints.
544
- * Therefore we do this in a synchronous way that ensures that
545
- * at least within `animate()` calls there is no style thrashing.
546
- *
547
- * In the motion-native-animate-mini-interrupt benchmark this
548
- * was 80% faster than a single loop.
549
- */
550
- const animationDefinitions = [];
551
- /**
552
- * Step 1: Build options and stop existing animations (write)
553
- */
554
- for (let i = 0; i < numElements; i++) {
555
- const element = elements[i];
556
- const elementTransition = { ...options };
557
- /**
558
- * Resolve stagger function if provided.
559
- */
560
- if (typeof elementTransition.delay === "function") {
561
- elementTransition.delay = elementTransition.delay(i, numElements);
562
- }
563
- for (const valueName in keyframes) {
564
- let valueKeyframes = keyframes[valueName];
565
- if (!Array.isArray(valueKeyframes)) {
566
- valueKeyframes = [valueKeyframes];
567
- }
568
- const valueOptions = {
569
- ...getValueTransition(elementTransition, valueName),
570
- };
571
- valueOptions.duration && (valueOptions.duration = secondsToMilliseconds(valueOptions.duration));
572
- valueOptions.delay && (valueOptions.delay = secondsToMilliseconds(valueOptions.delay));
573
- /**
574
- * If there's an existing animation playing on this element then stop it
575
- * before creating a new one.
576
- */
577
- const map = getAnimationMap(element);
578
- const key = animationMapKey(valueName, valueOptions.pseudoElement || "");
579
- const currentAnimation = map.get(key);
580
- currentAnimation && currentAnimation.stop();
581
- animationDefinitions.push({
582
- map,
583
- key,
584
- unresolvedKeyframes: valueKeyframes,
585
- options: {
586
- ...valueOptions,
587
- element,
588
- name: valueName,
589
- allowFlatten: !elementTransition.type && !elementTransition.ease,
590
- },
591
- });
592
- }
593
- }
594
- /**
595
- * Step 2: Resolve keyframes (read)
596
- */
597
- for (let i = 0; i < animationDefinitions.length; i++) {
598
- const { unresolvedKeyframes, options: animationOptions } = animationDefinitions[i];
599
- const { element, name, pseudoElement } = animationOptions;
600
- if (!pseudoElement && unresolvedKeyframes[0] === null) {
601
- unresolvedKeyframes[0] = getComputedStyle(element, name);
602
- }
603
- fillWildcards(unresolvedKeyframes);
604
- applyPxDefaults(unresolvedKeyframes, name);
605
- /**
606
- * If we only have one keyframe, explicitly read the initial keyframe
607
- * from the computed style. This is to ensure consistency with WAAPI behaviour
608
- * for restarting animations, for instance .play() after finish, when it
609
- * has one vs two keyframes.
610
- */
611
- if (!pseudoElement && unresolvedKeyframes.length < 2) {
612
- unresolvedKeyframes.unshift(getComputedStyle(element, name));
613
- }
614
- animationOptions.keyframes = unresolvedKeyframes;
615
- }
616
- /**
617
- * Step 3: Create new animations (write)
618
- */
619
- const animations = [];
620
- for (let i = 0; i < animationDefinitions.length; i++) {
621
- const { map, key, options: animationOptions } = animationDefinitions[i];
622
- const animation = new NativeAnimation(animationOptions);
623
- map.set(key, animation);
624
- animation.finished.finally(() => map.delete(key));
625
- animations.push(animation);
626
- }
627
- return animations;
628
- }
629
-
630
- const createScopedWaapiAnimate = (scope) => {
631
- function scopedAnimate(elementOrSelector, keyframes, options) {
632
- return new GroupAnimationWithThen(animateElements(elementOrSelector, keyframes, options, scope));
633
- }
634
- return scopedAnimate;
635
- };
636
-
637
- function useAnimateMini() {
638
- const scope = useConstant(() => ({
639
- current: null, // Will be hydrated by React
640
- animations: [],
641
- }));
642
- const animate = useConstant(() => createScopedWaapiAnimate(scope));
643
- useUnmountEffect(() => {
644
- scope.animations.forEach((animation) => animation.stop());
645
- });
646
- return [scope, animate];
647
- }
648
-
649
- exports.useAnimate = useAnimateMini;
9
+ Object.keys(mini).forEach(function (k) {
10
+ if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
11
+ enumerable: true,
12
+ get: function () { return mini[k]; }
13
+ });
14
+ });
@@ -1,8 +1,8 @@
1
1
  import { useState, useLayoutEffect } from 'react';
2
+ import { useConstant } from '../../utils/use-constant.mjs';
2
3
  import { makeUseVisualState } from '../../motion/utils/use-visual-state.mjs';
3
4
  import { createBox } from '../../projection/geometry/models.mjs';
4
5
  import { VisualElement } from '../../render/VisualElement.mjs';
5
- import { useConstant } from '../../utils/use-constant.mjs';
6
6
  import { animateVisualElement } from '../interfaces/visual-element.mjs';
7
7
 
8
8
  const createObject = () => ({});
@@ -1,12 +1,12 @@
1
+ import { animationControls } from './animation-controls.mjs';
1
2
  import { useConstant } from '../../utils/use-constant.mjs';
2
3
  import { useIsomorphicLayoutEffect } from '../../utils/use-isomorphic-effect.mjs';
3
- import { animationControls } from './animation-controls.mjs';
4
4
 
5
5
  /**
6
- * Creates `LegacyAnimationControls`, which can be used to manually start, stop
6
+ * Creates `AnimationControls`, which can be used to manually start, stop
7
7
  * and sequence animations on one or more components.
8
8
  *
9
- * The returned `LegacyAnimationControls` should be passed to the `animate` property
9
+ * The returned `AnimationControls` should be passed to the `animate` property
10
10
  * of the components you want to animate.
11
11
  *
12
12
  * These components can then be animated with the `start` method.
@@ -72,7 +72,7 @@ function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...seq
72
72
  const numKeyframes = valueKeyframesAsList.length;
73
73
  const createGenerator = isGenerator(type)
74
74
  ? type
75
- : generators?.[type || "keyframes"];
75
+ : generators?.[type];
76
76
  if (numKeyframes <= 2 && createGenerator) {
77
77
  /**
78
78
  * As we're creating an easing function from a spring,
@@ -4,7 +4,7 @@ resolution = 10 // as milliseconds
4
4
  let points = "";
5
5
  const numPoints = Math.max(Math.round(duration / resolution), 2);
6
6
  for (let i = 0; i < numPoints; i++) {
7
- points += easing(i / (numPoints - 1)) + ", ";
7
+ points += Math.round(easing(i / (numPoints - 1)) * 10000) / 10000 + ", ";
8
8
  }
9
9
  return `linear(${points.substring(0, points.length - 2)})`;
10
10
  };