@vysmo/easings 0.1.0

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.
Files changed (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +368 -0
  3. package/dist/builders/anticipate.d.ts +20 -0
  4. package/dist/builders/anticipate.d.ts.map +1 -0
  5. package/dist/builders/anticipate.js +52 -0
  6. package/dist/builders/anticipate.js.map +1 -0
  7. package/dist/builders/bezier.d.ts +7 -0
  8. package/dist/builders/bezier.d.ts.map +1 -0
  9. package/dist/builders/bezier.js +69 -0
  10. package/dist/builders/bezier.js.map +1 -0
  11. package/dist/builders/breathe.d.ts +11 -0
  12. package/dist/builders/breathe.d.ts.map +1 -0
  13. package/dist/builders/breathe.js +14 -0
  14. package/dist/builders/breathe.js.map +1 -0
  15. package/dist/builders/custom.d.ts +13 -0
  16. package/dist/builders/custom.d.ts.map +1 -0
  17. package/dist/builders/custom.js +40 -0
  18. package/dist/builders/custom.js.map +1 -0
  19. package/dist/builders/expoScale.d.ts +13 -0
  20. package/dist/builders/expoScale.d.ts.map +1 -0
  21. package/dist/builders/expoScale.js +27 -0
  22. package/dist/builders/expoScale.js.map +1 -0
  23. package/dist/builders/gravity.d.ts +12 -0
  24. package/dist/builders/gravity.d.ts.map +1 -0
  25. package/dist/builders/gravity.js +11 -0
  26. package/dist/builders/gravity.js.map +1 -0
  27. package/dist/builders/index.d.ts +12 -0
  28. package/dist/builders/index.d.ts.map +1 -0
  29. package/dist/builders/index.js +12 -0
  30. package/dist/builders/index.js.map +1 -0
  31. package/dist/builders/rough.d.ts +17 -0
  32. package/dist/builders/rough.d.ts.map +1 -0
  33. package/dist/builders/rough.js +62 -0
  34. package/dist/builders/rough.js.map +1 -0
  35. package/dist/builders/slow.d.ts +16 -0
  36. package/dist/builders/slow.d.ts.map +1 -0
  37. package/dist/builders/slow.js +59 -0
  38. package/dist/builders/slow.js.map +1 -0
  39. package/dist/builders/spring-presets.d.ts +48 -0
  40. package/dist/builders/spring-presets.d.ts.map +1 -0
  41. package/dist/builders/spring-presets.js +19 -0
  42. package/dist/builders/spring-presets.js.map +1 -0
  43. package/dist/builders/spring.d.ts +12 -0
  44. package/dist/builders/spring.d.ts.map +1 -0
  45. package/dist/builders/spring.js +46 -0
  46. package/dist/builders/spring.js.map +1 -0
  47. package/dist/builders/wiggle.d.ts +9 -0
  48. package/dist/builders/wiggle.d.ts.map +1 -0
  49. package/dist/builders/wiggle.js +23 -0
  50. package/dist/builders/wiggle.js.map +1 -0
  51. package/dist/css.d.ts +40 -0
  52. package/dist/css.d.ts.map +1 -0
  53. package/dist/css.js +71 -0
  54. package/dist/css.js.map +1 -0
  55. package/dist/define.d.ts +12 -0
  56. package/dist/define.d.ts.map +1 -0
  57. package/dist/define.js +31 -0
  58. package/dist/define.js.map +1 -0
  59. package/dist/easings/back.d.ts +8 -0
  60. package/dist/easings/back.d.ts.map +1 -0
  61. package/dist/easings/back.js +20 -0
  62. package/dist/easings/back.js.map +1 -0
  63. package/dist/easings/bounce.d.ts +4 -0
  64. package/dist/easings/bounce.d.ts.map +1 -0
  65. package/dist/easings/bounce.js +21 -0
  66. package/dist/easings/bounce.js.map +1 -0
  67. package/dist/easings/circ.d.ts +4 -0
  68. package/dist/easings/circ.d.ts.map +1 -0
  69. package/dist/easings/circ.js +7 -0
  70. package/dist/easings/circ.js.map +1 -0
  71. package/dist/easings/elastic.d.ts +9 -0
  72. package/dist/easings/elastic.d.ts.map +1 -0
  73. package/dist/easings/elastic.js +33 -0
  74. package/dist/easings/elastic.js.map +1 -0
  75. package/dist/easings/expo.d.ts +4 -0
  76. package/dist/easings/expo.d.ts.map +1 -0
  77. package/dist/easings/expo.js +11 -0
  78. package/dist/easings/expo.js.map +1 -0
  79. package/dist/easings/index.d.ts +11 -0
  80. package/dist/easings/index.d.ts.map +1 -0
  81. package/dist/easings/index.js +11 -0
  82. package/dist/easings/index.js.map +1 -0
  83. package/dist/easings/linear.d.ts +3 -0
  84. package/dist/easings/linear.d.ts.map +1 -0
  85. package/dist/easings/linear.js +4 -0
  86. package/dist/easings/linear.js.map +1 -0
  87. package/dist/easings/power.d.ts +25 -0
  88. package/dist/easings/power.d.ts.map +1 -0
  89. package/dist/easings/power.js +29 -0
  90. package/dist/easings/power.js.map +1 -0
  91. package/dist/easings/sine.d.ts +4 -0
  92. package/dist/easings/sine.d.ts.map +1 -0
  93. package/dist/easings/sine.js +6 -0
  94. package/dist/easings/sine.js.map +1 -0
  95. package/dist/easings/smooth.d.ts +4 -0
  96. package/dist/easings/smooth.d.ts.map +1 -0
  97. package/dist/easings/smooth.js +21 -0
  98. package/dist/easings/smooth.js.map +1 -0
  99. package/dist/easings/steps.d.ts +8 -0
  100. package/dist/easings/steps.d.ts.map +1 -0
  101. package/dist/easings/steps.js +16 -0
  102. package/dist/easings/steps.js.map +1 -0
  103. package/dist/index.d.ts +10 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +8 -0
  106. package/dist/index.js.map +1 -0
  107. package/dist/modifiers/blend.d.ts +13 -0
  108. package/dist/modifiers/blend.d.ts.map +1 -0
  109. package/dist/modifiers/blend.js +21 -0
  110. package/dist/modifiers/blend.js.map +1 -0
  111. package/dist/modifiers/chain.d.ts +18 -0
  112. package/dist/modifiers/chain.d.ts.map +1 -0
  113. package/dist/modifiers/chain.js +40 -0
  114. package/dist/modifiers/chain.js.map +1 -0
  115. package/dist/modifiers/index.d.ts +7 -0
  116. package/dist/modifiers/index.d.ts.map +1 -0
  117. package/dist/modifiers/index.js +7 -0
  118. package/dist/modifiers/index.js.map +1 -0
  119. package/dist/modifiers/mirror.d.ts +8 -0
  120. package/dist/modifiers/mirror.d.ts.map +1 -0
  121. package/dist/modifiers/mirror.js +14 -0
  122. package/dist/modifiers/mirror.js.map +1 -0
  123. package/dist/modifiers/reverse.d.ts +4 -0
  124. package/dist/modifiers/reverse.d.ts.map +1 -0
  125. package/dist/modifiers/reverse.js +6 -0
  126. package/dist/modifiers/reverse.js.map +1 -0
  127. package/dist/modifiers/slice.d.ts +11 -0
  128. package/dist/modifiers/slice.d.ts.map +1 -0
  129. package/dist/modifiers/slice.js +28 -0
  130. package/dist/modifiers/slice.js.map +1 -0
  131. package/dist/modifiers/yoyo.d.ts +8 -0
  132. package/dist/modifiers/yoyo.d.ts.map +1 -0
  133. package/dist/modifiers/yoyo.js +10 -0
  134. package/dist/modifiers/yoyo.js.map +1 -0
  135. package/dist/parse.d.ts +14 -0
  136. package/dist/parse.d.ts.map +1 -0
  137. package/dist/parse.js +108 -0
  138. package/dist/parse.js.map +1 -0
  139. package/dist/reduced-motion.d.ts +17 -0
  140. package/dist/reduced-motion.d.ts.map +1 -0
  141. package/dist/reduced-motion.js +24 -0
  142. package/dist/reduced-motion.js.map +1 -0
  143. package/dist/types.d.ts +8 -0
  144. package/dist/types.d.ts.map +1 -0
  145. package/dist/types.js +2 -0
  146. package/dist/types.js.map +1 -0
  147. package/package.json +63 -0
  148. package/src/__tests__/anticipate-variants.test.ts +58 -0
  149. package/src/__tests__/builders.test.ts +349 -0
  150. package/src/__tests__/contract.test.ts +47 -0
  151. package/src/__tests__/css.test.ts +93 -0
  152. package/src/__tests__/easings.bench.ts +66 -0
  153. package/src/__tests__/endpoint-correctness.test.ts +170 -0
  154. package/src/__tests__/modifiers.test.ts +134 -0
  155. package/src/__tests__/parametric.test.ts +148 -0
  156. package/src/__tests__/parse.test.ts +71 -0
  157. package/src/__tests__/property.test.ts +110 -0
  158. package/src/__tests__/reduced-motion.test.ts +66 -0
  159. package/src/__tests__/slice.test.ts +38 -0
  160. package/src/__tests__/spring-presets.test.ts +48 -0
  161. package/src/__tests__/ssr.test.ts +62 -0
  162. package/src/__tests__/types-check.ts +104 -0
  163. package/src/builders/anticipate.ts +66 -0
  164. package/src/builders/bezier.ts +71 -0
  165. package/src/builders/breathe.ts +31 -0
  166. package/src/builders/custom.ts +43 -0
  167. package/src/builders/expoScale.ts +30 -0
  168. package/src/builders/gravity.ts +27 -0
  169. package/src/builders/index.ts +24 -0
  170. package/src/builders/rough.ts +83 -0
  171. package/src/builders/slow.ts +72 -0
  172. package/src/builders/spring-presets.ts +20 -0
  173. package/src/builders/spring.ts +61 -0
  174. package/src/builders/wiggle.ts +40 -0
  175. package/src/css.ts +79 -0
  176. package/src/define.ts +49 -0
  177. package/src/easings/back.ts +24 -0
  178. package/src/easings/bounce.ts +23 -0
  179. package/src/easings/circ.ts +9 -0
  180. package/src/easings/elastic.ts +52 -0
  181. package/src/easings/expo.ts +9 -0
  182. package/src/easings/index.ts +35 -0
  183. package/src/easings/linear.ts +4 -0
  184. package/src/easings/power.ts +38 -0
  185. package/src/easings/sine.ts +7 -0
  186. package/src/easings/smooth.ts +25 -0
  187. package/src/easings/steps.ts +25 -0
  188. package/src/index.ts +9 -0
  189. package/src/modifiers/blend.ts +24 -0
  190. package/src/modifiers/chain.ts +63 -0
  191. package/src/modifiers/index.ts +6 -0
  192. package/src/modifiers/mirror.ts +14 -0
  193. package/src/modifiers/reverse.ts +7 -0
  194. package/src/modifiers/slice.ts +29 -0
  195. package/src/modifiers/yoyo.ts +15 -0
  196. package/src/parse.ts +167 -0
  197. package/src/reduced-motion.ts +26 -0
  198. package/src/types.ts +8 -0
@@ -0,0 +1,170 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ circIn,
4
+ circInOut,
5
+ circOut,
6
+ cubicIn,
7
+ cubicInOut,
8
+ cubicOut,
9
+ expoIn,
10
+ expoInOut,
11
+ expoOut,
12
+ linear,
13
+ none,
14
+ power1In,
15
+ power1InOut,
16
+ power1Out,
17
+ power2In,
18
+ power2InOut,
19
+ power2Out,
20
+ power3In,
21
+ power3InOut,
22
+ power3Out,
23
+ power4In,
24
+ power4InOut,
25
+ power4Out,
26
+ quadIn,
27
+ quadInOut,
28
+ quadOut,
29
+ quartIn,
30
+ quartInOut,
31
+ quartOut,
32
+ quintIn,
33
+ quintInOut,
34
+ quintOut,
35
+ sineIn,
36
+ sineInOut,
37
+ sineOut,
38
+ smoothIn,
39
+ smoothInOut,
40
+ smoothOut,
41
+ type EasingFn,
42
+ } from "../index.js";
43
+
44
+ const ALL: ReadonlyArray<[string, EasingFn]> = [
45
+ ["linear", linear],
46
+ ["none", none],
47
+ ["power1.in", power1In],
48
+ ["power1.out", power1Out],
49
+ ["power1.inOut", power1InOut],
50
+ ["power2.in", power2In],
51
+ ["power2.out", power2Out],
52
+ ["power2.inOut", power2InOut],
53
+ ["power3.in", power3In],
54
+ ["power3.out", power3Out],
55
+ ["power3.inOut", power3InOut],
56
+ ["power4.in", power4In],
57
+ ["power4.out", power4Out],
58
+ ["power4.inOut", power4InOut],
59
+ ["sine.in", sineIn],
60
+ ["sine.out", sineOut],
61
+ ["sine.inOut", sineInOut],
62
+ ["circ.in", circIn],
63
+ ["circ.out", circOut],
64
+ ["circ.inOut", circInOut],
65
+ ["expo.in", expoIn],
66
+ ["expo.out", expoOut],
67
+ ["expo.inOut", expoInOut],
68
+ ["smooth.in", smoothIn],
69
+ ["smooth.out", smoothOut],
70
+ ["smooth.inOut", smoothInOut],
71
+ ];
72
+
73
+ describe("endpoint correctness", () => {
74
+ it.each(ALL)("%s returns exactly 0 at t=0", (_, fn) => {
75
+ expect(fn(0)).toBe(0);
76
+ });
77
+
78
+ it.each(ALL)("%s returns exactly 1 at t=1", (_, fn) => {
79
+ expect(fn(1)).toBe(1);
80
+ });
81
+ });
82
+
83
+ describe("monotonicity (non-overshooting easings)", () => {
84
+ it.each(ALL)("%s is monotonically non-decreasing across [0, 1]", (_, fn) => {
85
+ let prev = fn(0);
86
+ for (let i = 1; i <= 100; i++) {
87
+ const curr = fn(i / 100);
88
+ expect(curr).toBeGreaterThanOrEqual(prev - 1e-9);
89
+ prev = curr;
90
+ }
91
+ });
92
+ });
93
+
94
+ describe("aliases are identical to their power counterparts", () => {
95
+ it("quad == power1", () => {
96
+ expect(quadIn).toBe(power1In);
97
+ expect(quadOut).toBe(power1Out);
98
+ expect(quadInOut).toBe(power1InOut);
99
+ });
100
+ it("cubic == power2", () => {
101
+ expect(cubicIn).toBe(power2In);
102
+ expect(cubicOut).toBe(power2Out);
103
+ expect(cubicInOut).toBe(power2InOut);
104
+ });
105
+ it("quart == power3", () => {
106
+ expect(quartIn).toBe(power3In);
107
+ expect(quartOut).toBe(power3Out);
108
+ expect(quartInOut).toBe(power3InOut);
109
+ });
110
+ it("quint == power4", () => {
111
+ expect(quintIn).toBe(power4In);
112
+ expect(quintOut).toBe(power4Out);
113
+ expect(quintInOut).toBe(power4InOut);
114
+ });
115
+ it("none == linear", () => {
116
+ expect(none).toBe(linear);
117
+ });
118
+ });
119
+
120
+ describe("midpoint sanity", () => {
121
+ it("linear(0.5) === 0.5", () => {
122
+ expect(linear(0.5)).toBe(0.5);
123
+ });
124
+ it("inOut variants cross 0.5 at t=0.5", () => {
125
+ const inOutEasings: EasingFn[] = [
126
+ power1InOut,
127
+ power2InOut,
128
+ power3InOut,
129
+ power4InOut,
130
+ sineInOut,
131
+ circInOut,
132
+ expoInOut,
133
+ smoothInOut,
134
+ ];
135
+ for (const fn of inOutEasings) {
136
+ expect(fn(0.5)).toBeCloseTo(0.5, 6);
137
+ }
138
+ });
139
+ it("in variants stay below 0.5 at t=0.5 (except linear)", () => {
140
+ const inEasings: EasingFn[] = [power1In, power2In, power3In, power4In, sineIn, circIn, expoIn, smoothIn];
141
+ for (const fn of inEasings) {
142
+ expect(fn(0.5)).toBeLessThan(0.5);
143
+ }
144
+ });
145
+ it("out variants exceed 0.5 at t=0.5 (except linear)", () => {
146
+ const outEasings: EasingFn[] = [
147
+ power1Out,
148
+ power2Out,
149
+ power3Out,
150
+ power4Out,
151
+ sineOut,
152
+ circOut,
153
+ expoOut,
154
+ smoothOut,
155
+ ];
156
+ for (const fn of outEasings) {
157
+ expect(fn(0.5)).toBeGreaterThan(0.5);
158
+ }
159
+ });
160
+ });
161
+
162
+ describe("easing names are set", () => {
163
+ it.each(ALL)("%s has correct easingName property", (name, fn) => {
164
+ if (name === "none") {
165
+ expect(fn.easingName).toBe("linear");
166
+ } else {
167
+ expect(fn.easingName).toBe(name);
168
+ }
169
+ });
170
+ });
@@ -0,0 +1,134 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ blend,
4
+ chain,
5
+ compose,
6
+ linear,
7
+ mirror,
8
+ power2In,
9
+ power2Out,
10
+ power3In,
11
+ reverse,
12
+ sineOut,
13
+ yoyo,
14
+ } from "../index.js";
15
+
16
+ describe("reverse", () => {
17
+ it("reverse(power2In) matches power2Out", () => {
18
+ const rev = reverse(power2In);
19
+ for (let i = 0; i <= 10; i++) {
20
+ const t = i / 10;
21
+ expect(rev(t)).toBeCloseTo(power2Out(t), 10);
22
+ }
23
+ });
24
+ it("reverse(reverse(f)) === f (up to FP)", () => {
25
+ const double = reverse(reverse(power2In));
26
+ for (let i = 0; i <= 10; i++) {
27
+ const t = i / 10;
28
+ expect(double(t)).toBeCloseTo(power2In(t), 10);
29
+ }
30
+ });
31
+ it("propagates name", () => {
32
+ expect(reverse(power2In).easingName).toBe("reverse(power2.in)");
33
+ });
34
+ });
35
+
36
+ describe("mirror", () => {
37
+ it("mirror(linear) is still linear", () => {
38
+ const m = mirror(linear);
39
+ for (let i = 0; i <= 10; i++) {
40
+ const t = i / 10;
41
+ expect(m(t)).toBeCloseTo(t, 10);
42
+ }
43
+ });
44
+ it("mirror of an in-ease crosses 0.5 at t=0.5", () => {
45
+ const m = mirror(power2In);
46
+ expect(m(0.5)).toBeCloseTo(0.5, 10);
47
+ });
48
+ it("mirror hits endpoints", () => {
49
+ const m = mirror(power3In);
50
+ expect(m(0)).toBe(0);
51
+ expect(m(1)).toBe(1);
52
+ });
53
+ });
54
+
55
+ describe("yoyo", () => {
56
+ it("returns to 0 at t=1", () => {
57
+ const y = yoyo(power2In);
58
+ expect(y(1)).toBeCloseTo(0, 10);
59
+ });
60
+ it("peaks at t=0.5", () => {
61
+ const y = yoyo(power2In);
62
+ expect(y(0.5)).toBeCloseTo(1, 10);
63
+ });
64
+ it("is symmetric around t=0.5", () => {
65
+ const y = yoyo(power2In);
66
+ for (let i = 0; i <= 10; i++) {
67
+ const t = i / 10;
68
+ expect(y(t)).toBeCloseTo(y(1 - t), 10);
69
+ }
70
+ });
71
+ });
72
+
73
+ describe("chain", () => {
74
+ it("two equal-duration linear segments reproduce linear", () => {
75
+ const c = chain([
76
+ { ease: linear, duration: 1, from: 0, to: 0.5 },
77
+ { ease: linear, duration: 1, from: 0.5, to: 1 },
78
+ ]);
79
+ for (let i = 0; i <= 10; i++) {
80
+ const t = i / 10;
81
+ expect(c(t)).toBeCloseTo(t, 10);
82
+ }
83
+ });
84
+ it("segments respect duration ratios", () => {
85
+ const c = chain([
86
+ { ease: linear, duration: 3, from: 0, to: 0.9 },
87
+ { ease: linear, duration: 1, from: 0.9, to: 1 },
88
+ ]);
89
+ expect(c(0.75)).toBeCloseTo(0.9, 10);
90
+ });
91
+ it("auto-fills endpoints when omitted", () => {
92
+ const c = chain([{ ease: sineOut, duration: 1 }]);
93
+ expect(c(0)).toBeCloseTo(0, 10);
94
+ expect(c(1)).toBeCloseTo(1, 10);
95
+ });
96
+ it("rejects empty segments", () => {
97
+ expect(() => chain([])).toThrow(RangeError);
98
+ });
99
+ });
100
+
101
+ describe("blend", () => {
102
+ it("weight 0 returns pure a", () => {
103
+ const b = blend(power2In, power2Out, 0);
104
+ expect(b).toBe(power2In);
105
+ });
106
+ it("weight 1 returns pure b", () => {
107
+ const b = blend(power2In, power2Out, 1);
108
+ expect(b).toBe(power2Out);
109
+ });
110
+ it("weight 0.5 is the midpoint average", () => {
111
+ const b = blend(power2In, power2Out, 0.5);
112
+ for (let i = 1; i < 10; i++) {
113
+ const t = i / 10;
114
+ expect(b(t)).toBeCloseTo((power2In(t) + power2Out(t)) / 2, 10);
115
+ }
116
+ });
117
+ });
118
+
119
+ describe("compose", () => {
120
+ it("compose(a, linear) === a", () => {
121
+ const c = compose(power2In, linear);
122
+ for (let i = 0; i <= 10; i++) {
123
+ const t = i / 10;
124
+ expect(c(t)).toBeCloseTo(power2In(t), 10);
125
+ }
126
+ });
127
+ it("compose(power2In, power2In) applies it twice", () => {
128
+ const c = compose(power2In, power2In);
129
+ for (let i = 0; i <= 10; i++) {
130
+ const t = i / 10;
131
+ expect(c(t)).toBeCloseTo(power2In(power2In(t)), 10);
132
+ }
133
+ });
134
+ });
@@ -0,0 +1,148 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ backIn,
4
+ backInOut,
5
+ backOut,
6
+ bounceIn,
7
+ bounceInOut,
8
+ bounceOut,
9
+ elasticIn,
10
+ elasticInOut,
11
+ elasticOut,
12
+ steps,
13
+ type EasingFn,
14
+ type ParametricEasing,
15
+ } from "../index.js";
16
+
17
+ const PARAMETRIC = [
18
+ ["back.in", backIn],
19
+ ["back.out", backOut],
20
+ ["back.inOut", backInOut],
21
+ ["elastic.in", elasticIn],
22
+ ["elastic.out", elasticOut],
23
+ ["elastic.inOut", elasticInOut],
24
+ ["steps", steps],
25
+ ] as const;
26
+
27
+ const BOUNCE = [
28
+ ["bounce.in", bounceIn],
29
+ ["bounce.out", bounceOut],
30
+ ["bounce.inOut", bounceInOut],
31
+ ] as const;
32
+
33
+ describe("parametric easings hit endpoints", () => {
34
+ it.each(PARAMETRIC)("%s(0) === 0 at defaults", (_, fn) => {
35
+ expect((fn as EasingFn)(0)).toBe(0);
36
+ });
37
+ it.each(PARAMETRIC)("%s(1) === 1 at defaults", (_, fn) => {
38
+ expect((fn as EasingFn)(1)).toBe(1);
39
+ });
40
+ it.each(BOUNCE)("%s(0) === 0", (_, fn) => {
41
+ expect(fn(0)).toBe(0);
42
+ });
43
+ it.each(BOUNCE)("%s(1) === 1", (_, fn) => {
44
+ expect(fn(1)).toBe(1);
45
+ });
46
+ });
47
+
48
+ describe("back has overshoot", () => {
49
+ it("back.out exceeds 1 somewhere in (0, 1)", () => {
50
+ let max = 0;
51
+ for (let i = 0; i <= 100; i++) max = Math.max(max, backOut(i / 100));
52
+ expect(max).toBeGreaterThan(1);
53
+ });
54
+ it("back.in goes below 0 somewhere in (0, 1)", () => {
55
+ let min = 1;
56
+ for (let i = 0; i <= 100; i++) min = Math.min(min, backIn(i / 100));
57
+ expect(min).toBeLessThan(0);
58
+ });
59
+ it("custom overshoot changes curve shape", () => {
60
+ const soft = backOut.with({ overshoot: 0.5 });
61
+ const hard = backOut.with({ overshoot: 4 });
62
+ let softMax = 0;
63
+ let hardMax = 0;
64
+ for (let i = 0; i <= 100; i++) {
65
+ softMax = Math.max(softMax, soft(i / 100));
66
+ hardMax = Math.max(hardMax, hard(i / 100));
67
+ }
68
+ expect(hardMax).toBeGreaterThan(softMax);
69
+ });
70
+ });
71
+
72
+ describe("elastic oscillates", () => {
73
+ it("elastic.out crosses 1 multiple times (oscillation)", () => {
74
+ let crossings = 0;
75
+ let prev = 0;
76
+ for (let i = 1; i <= 100; i++) {
77
+ const curr = elasticOut(i / 100);
78
+ if ((prev < 1 && curr >= 1) || (prev > 1 && curr <= 1)) crossings++;
79
+ prev = curr;
80
+ }
81
+ expect(crossings).toBeGreaterThanOrEqual(2);
82
+ });
83
+ it("custom amplitude changes overshoot", () => {
84
+ const big = elasticOut.with({ amplitude: 3 });
85
+ const small = elasticOut.with({ amplitude: 1 });
86
+ let bigMax = 0;
87
+ let smallMax = 0;
88
+ for (let i = 0; i <= 100; i++) {
89
+ bigMax = Math.max(bigMax, big(i / 100));
90
+ smallMax = Math.max(smallMax, small(i / 100));
91
+ }
92
+ expect(bigMax).toBeGreaterThan(smallMax);
93
+ });
94
+ });
95
+
96
+ describe("bounce produces stepped peaks", () => {
97
+ it("bounce.out has multiple local maxima between 0 and 1", () => {
98
+ let peaks = 0;
99
+ let prev = bounceOut(0);
100
+ let trend: 1 | -1 = 1;
101
+ for (let i = 1; i <= 200; i++) {
102
+ const curr = bounceOut(i / 200);
103
+ if (trend === 1 && curr < prev) {
104
+ peaks++;
105
+ trend = -1;
106
+ } else if (trend === -1 && curr > prev) {
107
+ trend = 1;
108
+ }
109
+ prev = curr;
110
+ }
111
+ expect(peaks).toBeGreaterThanOrEqual(3);
112
+ });
113
+ });
114
+
115
+ describe("steps produces discrete values", () => {
116
+ it("steps (default end, count 5) produces exactly 5 unique values across [0, 1)", () => {
117
+ const values = new Set<number>();
118
+ for (let i = 0; i < 100; i++) values.add(steps(i / 100));
119
+ expect(values.size).toBeLessThanOrEqual(5);
120
+ });
121
+ it("steps with count 10 produces finer granularity", () => {
122
+ const fine = steps.with({ count: 10 });
123
+ const values = new Set<number>();
124
+ for (let i = 0; i < 100; i++) values.add(fine(i / 100));
125
+ expect(values.size).toBeGreaterThan(5);
126
+ });
127
+ it("steps position: start outputs 1/n at t=0", () => {
128
+ const s = steps.with({ count: 4, position: "start" });
129
+ expect(s(0)).toBeCloseTo(0.25, 10);
130
+ });
131
+ it("steps position: none outputs 0 at t=0 and 1 at t=1", () => {
132
+ const s = steps.with({ count: 5, position: "none" });
133
+ expect(s(0)).toBe(0);
134
+ expect(s(1)).toBe(1);
135
+ });
136
+ });
137
+
138
+ describe("parametric factories preserve defaults immutably", () => {
139
+ it.each(PARAMETRIC)("%s.defaults is frozen", (_, fn) => {
140
+ const parametric = fn as ParametricEasing<object>;
141
+ expect(Object.isFrozen(parametric.defaults)).toBe(true);
142
+ });
143
+ it(".with() produces independent EasingFn", () => {
144
+ const a = backOut.with({ overshoot: 2 });
145
+ const b = backOut.with({ overshoot: 5 });
146
+ expect(a(0.5)).not.toBe(b(0.5));
147
+ });
148
+ });
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ backOut,
4
+ bounceInOut,
5
+ elasticOut,
6
+ linear,
7
+ parseEasing,
8
+ power2Out,
9
+ sineInOut,
10
+ } from "../index.js";
11
+
12
+ describe("parseEasing", () => {
13
+ it("parses plain linear", () => {
14
+ expect(parseEasing("linear")).toBe(linear);
15
+ });
16
+
17
+ it("parses dotted variants", () => {
18
+ expect(parseEasing("power2.out")).toBe(power2Out);
19
+ expect(parseEasing("sine.inOut")).toBe(sineInOut);
20
+ expect(parseEasing("bounce.inOut")).toBe(bounceInOut);
21
+ });
22
+
23
+ it("parses parametric with one arg", () => {
24
+ const fn = parseEasing("back.out(2)");
25
+ const native = backOut.with({ overshoot: 2 });
26
+ for (let i = 0; i <= 10; i++) {
27
+ const t = i / 10;
28
+ expect(fn(t)).toBeCloseTo(native(t), 10);
29
+ }
30
+ });
31
+
32
+ it("parses parametric with multiple args", () => {
33
+ const fn = parseEasing("elastic.out(1.2, 0.4)");
34
+ const native = elasticOut.with({ amplitude: 1.2, period: 0.4 });
35
+ for (let i = 0; i <= 10; i++) {
36
+ const t = i / 10;
37
+ expect(fn(t)).toBeCloseTo(native(t), 10);
38
+ }
39
+ });
40
+
41
+ it("parses steps with numeric + string arg", () => {
42
+ const fn = parseEasing("steps(4, start)");
43
+ expect(fn(0)).toBe(0.25);
44
+ expect(fn(1)).toBe(1);
45
+ });
46
+
47
+ it("parses CSS cubic-bezier() form", () => {
48
+ const fn = parseEasing("cubic-bezier(0.42, 0, 0.58, 1)");
49
+ expect(fn(0)).toBe(0);
50
+ expect(fn(1)).toBe(1);
51
+ expect(fn(0.5)).toBeCloseTo(0.5, 3);
52
+ });
53
+
54
+ it("trims whitespace", () => {
55
+ expect(parseEasing(" power2.out ")).toBe(power2Out);
56
+ });
57
+
58
+ it("throws on unknown names", () => {
59
+ expect(() => parseEasing("unknownEase")).toThrow(RangeError);
60
+ expect(() => parseEasing("power2.foo")).toThrow(RangeError);
61
+ });
62
+
63
+ it("throws on malformed input", () => {
64
+ expect(() => parseEasing("")).toThrow(RangeError);
65
+ expect(() => parseEasing(".power2")).toThrow(RangeError);
66
+ });
67
+
68
+ it("throws when passing args to non-parametric", () => {
69
+ expect(() => parseEasing("linear(2)")).toThrow(RangeError);
70
+ });
71
+ });
@@ -0,0 +1,110 @@
1
+ import fc from "fast-check";
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ circIn,
5
+ circInOut,
6
+ circOut,
7
+ cubicIn,
8
+ cubicInOut,
9
+ cubicOut,
10
+ expoIn,
11
+ expoInOut,
12
+ expoOut,
13
+ linear,
14
+ quartIn,
15
+ quartInOut,
16
+ quartOut,
17
+ quintIn,
18
+ quintInOut,
19
+ quintOut,
20
+ sineIn,
21
+ sineInOut,
22
+ sineOut,
23
+ type EasingFn,
24
+ } from "../index.js";
25
+
26
+ // Non-overshooting easings: output in [0, 1] for t in [0, 1]
27
+ const NON_OVERSHOOT: Array<[string, EasingFn]> = [
28
+ ["linear", linear],
29
+ ["cubicIn", cubicIn],
30
+ ["cubicOut", cubicOut],
31
+ ["cubicInOut", cubicInOut],
32
+ ["quartIn", quartIn],
33
+ ["quartOut", quartOut],
34
+ ["quartInOut", quartInOut],
35
+ ["quintIn", quintIn],
36
+ ["quintOut", quintOut],
37
+ ["quintInOut", quintInOut],
38
+ ["sineIn", sineIn],
39
+ ["sineOut", sineOut],
40
+ ["sineInOut", sineInOut],
41
+ ["circIn", circIn],
42
+ ["circOut", circOut],
43
+ ["circInOut", circInOut],
44
+ ["expoIn", expoIn],
45
+ ["expoOut", expoOut],
46
+ ["expoInOut", expoInOut],
47
+ ];
48
+
49
+ const tArb = fc.double({ min: 0, max: 1, noNaN: true });
50
+
51
+ describe("property: endpoints always clamp", () => {
52
+ it.each(NON_OVERSHOOT)("%s(0) is exactly 0", (_, fn) => {
53
+ expect(fn(0)).toBe(0);
54
+ });
55
+ it.each(NON_OVERSHOOT)("%s(1) is exactly 1", (_, fn) => {
56
+ expect(fn(1)).toBe(1);
57
+ });
58
+ });
59
+
60
+ describe("property: output stays in [0, 1] for non-overshoot eases", () => {
61
+ it.each(NON_OVERSHOOT)("%s output is always in [0, 1]", (_, fn) => {
62
+ fc.assert(
63
+ fc.property(tArb, (t) => {
64
+ const y = fn(t);
65
+ return y >= -1e-9 && y <= 1 + 1e-9;
66
+ }),
67
+ { numRuns: 200 },
68
+ );
69
+ });
70
+ });
71
+
72
+ describe("property: monotonicity for non-overshoot eases", () => {
73
+ it.each(NON_OVERSHOOT)("%s is monotonically non-decreasing", (_, fn) => {
74
+ fc.assert(
75
+ fc.property(
76
+ fc.double({ min: 0, max: 0.9999, noNaN: true }),
77
+ fc.double({ min: 0, max: 0.01, noNaN: true }),
78
+ (t, delta) => {
79
+ const a = fn(t);
80
+ const b = fn(Math.min(1, t + delta));
81
+ return b >= a - 1e-9;
82
+ },
83
+ ),
84
+ { numRuns: 200 },
85
+ );
86
+ });
87
+ });
88
+
89
+ describe("property: defensive input handling", () => {
90
+ it("all eases return finite output for any finite input", () => {
91
+ const EXTENDED: EasingFn[] = NON_OVERSHOOT.map(([, fn]) => fn);
92
+ fc.assert(
93
+ fc.property(
94
+ fc.double({ min: -100, max: 100, noNaN: true }),
95
+ fc.integer({ min: 0, max: EXTENDED.length - 1 }),
96
+ (t, idx) => {
97
+ const result = EXTENDED[idx]!(t);
98
+ return Number.isFinite(result);
99
+ },
100
+ ),
101
+ { numRuns: 300 },
102
+ );
103
+ });
104
+
105
+ it("all eases return 0 on NaN input", () => {
106
+ for (const [, fn] of NON_OVERSHOOT) {
107
+ expect(fn(Number.NaN)).toBe(0);
108
+ }
109
+ });
110
+ });
@@ -0,0 +1,66 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ linear,
4
+ power2Out,
5
+ prefersReducedMotion,
6
+ respectReducedMotion,
7
+ steps,
8
+ } from "../index.js";
9
+
10
+ describe("prefersReducedMotion", () => {
11
+ afterEach(() => {
12
+ vi.unstubAllGlobals();
13
+ });
14
+
15
+ it("returns false when window is undefined (SSR)", () => {
16
+ vi.stubGlobal("window", undefined);
17
+ expect(prefersReducedMotion()).toBe(false);
18
+ });
19
+
20
+ it("returns false when matchMedia is unavailable", () => {
21
+ vi.stubGlobal("window", {} as unknown);
22
+ expect(prefersReducedMotion()).toBe(false);
23
+ });
24
+
25
+ it("returns true when media query matches", () => {
26
+ vi.stubGlobal("window", {
27
+ matchMedia: () => ({ matches: true }),
28
+ } as unknown);
29
+ expect(prefersReducedMotion()).toBe(true);
30
+ });
31
+
32
+ it("returns false when media query does not match", () => {
33
+ vi.stubGlobal("window", {
34
+ matchMedia: () => ({ matches: false }),
35
+ } as unknown);
36
+ expect(prefersReducedMotion()).toBe(false);
37
+ });
38
+ });
39
+
40
+ describe("respectReducedMotion", () => {
41
+ afterEach(() => {
42
+ vi.unstubAllGlobals();
43
+ });
44
+
45
+ it("returns the original ease when reduced motion is off", () => {
46
+ vi.stubGlobal("window", {
47
+ matchMedia: () => ({ matches: false }),
48
+ } as unknown);
49
+ expect(respectReducedMotion(power2Out)).toBe(power2Out);
50
+ });
51
+
52
+ it("returns linear fallback by default when reduced motion is on", () => {
53
+ vi.stubGlobal("window", {
54
+ matchMedia: () => ({ matches: true }),
55
+ } as unknown);
56
+ expect(respectReducedMotion(power2Out)).toBe(linear);
57
+ });
58
+
59
+ it("accepts a custom fallback", () => {
60
+ vi.stubGlobal("window", {
61
+ matchMedia: () => ({ matches: true }),
62
+ } as unknown);
63
+ const fallback = steps;
64
+ expect(respectReducedMotion(power2Out, fallback)).toBe(fallback);
65
+ });
66
+ });