@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.
- package/LICENSE +21 -0
- package/README.md +368 -0
- package/dist/builders/anticipate.d.ts +20 -0
- package/dist/builders/anticipate.d.ts.map +1 -0
- package/dist/builders/anticipate.js +52 -0
- package/dist/builders/anticipate.js.map +1 -0
- package/dist/builders/bezier.d.ts +7 -0
- package/dist/builders/bezier.d.ts.map +1 -0
- package/dist/builders/bezier.js +69 -0
- package/dist/builders/bezier.js.map +1 -0
- package/dist/builders/breathe.d.ts +11 -0
- package/dist/builders/breathe.d.ts.map +1 -0
- package/dist/builders/breathe.js +14 -0
- package/dist/builders/breathe.js.map +1 -0
- package/dist/builders/custom.d.ts +13 -0
- package/dist/builders/custom.d.ts.map +1 -0
- package/dist/builders/custom.js +40 -0
- package/dist/builders/custom.js.map +1 -0
- package/dist/builders/expoScale.d.ts +13 -0
- package/dist/builders/expoScale.d.ts.map +1 -0
- package/dist/builders/expoScale.js +27 -0
- package/dist/builders/expoScale.js.map +1 -0
- package/dist/builders/gravity.d.ts +12 -0
- package/dist/builders/gravity.d.ts.map +1 -0
- package/dist/builders/gravity.js +11 -0
- package/dist/builders/gravity.js.map +1 -0
- package/dist/builders/index.d.ts +12 -0
- package/dist/builders/index.d.ts.map +1 -0
- package/dist/builders/index.js +12 -0
- package/dist/builders/index.js.map +1 -0
- package/dist/builders/rough.d.ts +17 -0
- package/dist/builders/rough.d.ts.map +1 -0
- package/dist/builders/rough.js +62 -0
- package/dist/builders/rough.js.map +1 -0
- package/dist/builders/slow.d.ts +16 -0
- package/dist/builders/slow.d.ts.map +1 -0
- package/dist/builders/slow.js +59 -0
- package/dist/builders/slow.js.map +1 -0
- package/dist/builders/spring-presets.d.ts +48 -0
- package/dist/builders/spring-presets.d.ts.map +1 -0
- package/dist/builders/spring-presets.js +19 -0
- package/dist/builders/spring-presets.js.map +1 -0
- package/dist/builders/spring.d.ts +12 -0
- package/dist/builders/spring.d.ts.map +1 -0
- package/dist/builders/spring.js +46 -0
- package/dist/builders/spring.js.map +1 -0
- package/dist/builders/wiggle.d.ts +9 -0
- package/dist/builders/wiggle.d.ts.map +1 -0
- package/dist/builders/wiggle.js +23 -0
- package/dist/builders/wiggle.js.map +1 -0
- package/dist/css.d.ts +40 -0
- package/dist/css.d.ts.map +1 -0
- package/dist/css.js +71 -0
- package/dist/css.js.map +1 -0
- package/dist/define.d.ts +12 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +31 -0
- package/dist/define.js.map +1 -0
- package/dist/easings/back.d.ts +8 -0
- package/dist/easings/back.d.ts.map +1 -0
- package/dist/easings/back.js +20 -0
- package/dist/easings/back.js.map +1 -0
- package/dist/easings/bounce.d.ts +4 -0
- package/dist/easings/bounce.d.ts.map +1 -0
- package/dist/easings/bounce.js +21 -0
- package/dist/easings/bounce.js.map +1 -0
- package/dist/easings/circ.d.ts +4 -0
- package/dist/easings/circ.d.ts.map +1 -0
- package/dist/easings/circ.js +7 -0
- package/dist/easings/circ.js.map +1 -0
- package/dist/easings/elastic.d.ts +9 -0
- package/dist/easings/elastic.d.ts.map +1 -0
- package/dist/easings/elastic.js +33 -0
- package/dist/easings/elastic.js.map +1 -0
- package/dist/easings/expo.d.ts +4 -0
- package/dist/easings/expo.d.ts.map +1 -0
- package/dist/easings/expo.js +11 -0
- package/dist/easings/expo.js.map +1 -0
- package/dist/easings/index.d.ts +11 -0
- package/dist/easings/index.d.ts.map +1 -0
- package/dist/easings/index.js +11 -0
- package/dist/easings/index.js.map +1 -0
- package/dist/easings/linear.d.ts +3 -0
- package/dist/easings/linear.d.ts.map +1 -0
- package/dist/easings/linear.js +4 -0
- package/dist/easings/linear.js.map +1 -0
- package/dist/easings/power.d.ts +25 -0
- package/dist/easings/power.d.ts.map +1 -0
- package/dist/easings/power.js +29 -0
- package/dist/easings/power.js.map +1 -0
- package/dist/easings/sine.d.ts +4 -0
- package/dist/easings/sine.d.ts.map +1 -0
- package/dist/easings/sine.js +6 -0
- package/dist/easings/sine.js.map +1 -0
- package/dist/easings/smooth.d.ts +4 -0
- package/dist/easings/smooth.d.ts.map +1 -0
- package/dist/easings/smooth.js +21 -0
- package/dist/easings/smooth.js.map +1 -0
- package/dist/easings/steps.d.ts +8 -0
- package/dist/easings/steps.d.ts.map +1 -0
- package/dist/easings/steps.js +16 -0
- package/dist/easings/steps.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/modifiers/blend.d.ts +13 -0
- package/dist/modifiers/blend.d.ts.map +1 -0
- package/dist/modifiers/blend.js +21 -0
- package/dist/modifiers/blend.js.map +1 -0
- package/dist/modifiers/chain.d.ts +18 -0
- package/dist/modifiers/chain.d.ts.map +1 -0
- package/dist/modifiers/chain.js +40 -0
- package/dist/modifiers/chain.js.map +1 -0
- package/dist/modifiers/index.d.ts +7 -0
- package/dist/modifiers/index.d.ts.map +1 -0
- package/dist/modifiers/index.js +7 -0
- package/dist/modifiers/index.js.map +1 -0
- package/dist/modifiers/mirror.d.ts +8 -0
- package/dist/modifiers/mirror.d.ts.map +1 -0
- package/dist/modifiers/mirror.js +14 -0
- package/dist/modifiers/mirror.js.map +1 -0
- package/dist/modifiers/reverse.d.ts +4 -0
- package/dist/modifiers/reverse.d.ts.map +1 -0
- package/dist/modifiers/reverse.js +6 -0
- package/dist/modifiers/reverse.js.map +1 -0
- package/dist/modifiers/slice.d.ts +11 -0
- package/dist/modifiers/slice.d.ts.map +1 -0
- package/dist/modifiers/slice.js +28 -0
- package/dist/modifiers/slice.js.map +1 -0
- package/dist/modifiers/yoyo.d.ts +8 -0
- package/dist/modifiers/yoyo.d.ts.map +1 -0
- package/dist/modifiers/yoyo.js +10 -0
- package/dist/modifiers/yoyo.js.map +1 -0
- package/dist/parse.d.ts +14 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +108 -0
- package/dist/parse.js.map +1 -0
- package/dist/reduced-motion.d.ts +17 -0
- package/dist/reduced-motion.d.ts.map +1 -0
- package/dist/reduced-motion.js +24 -0
- package/dist/reduced-motion.js.map +1 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
- package/src/__tests__/anticipate-variants.test.ts +58 -0
- package/src/__tests__/builders.test.ts +349 -0
- package/src/__tests__/contract.test.ts +47 -0
- package/src/__tests__/css.test.ts +93 -0
- package/src/__tests__/easings.bench.ts +66 -0
- package/src/__tests__/endpoint-correctness.test.ts +170 -0
- package/src/__tests__/modifiers.test.ts +134 -0
- package/src/__tests__/parametric.test.ts +148 -0
- package/src/__tests__/parse.test.ts +71 -0
- package/src/__tests__/property.test.ts +110 -0
- package/src/__tests__/reduced-motion.test.ts +66 -0
- package/src/__tests__/slice.test.ts +38 -0
- package/src/__tests__/spring-presets.test.ts +48 -0
- package/src/__tests__/ssr.test.ts +62 -0
- package/src/__tests__/types-check.ts +104 -0
- package/src/builders/anticipate.ts +66 -0
- package/src/builders/bezier.ts +71 -0
- package/src/builders/breathe.ts +31 -0
- package/src/builders/custom.ts +43 -0
- package/src/builders/expoScale.ts +30 -0
- package/src/builders/gravity.ts +27 -0
- package/src/builders/index.ts +24 -0
- package/src/builders/rough.ts +83 -0
- package/src/builders/slow.ts +72 -0
- package/src/builders/spring-presets.ts +20 -0
- package/src/builders/spring.ts +61 -0
- package/src/builders/wiggle.ts +40 -0
- package/src/css.ts +79 -0
- package/src/define.ts +49 -0
- package/src/easings/back.ts +24 -0
- package/src/easings/bounce.ts +23 -0
- package/src/easings/circ.ts +9 -0
- package/src/easings/elastic.ts +52 -0
- package/src/easings/expo.ts +9 -0
- package/src/easings/index.ts +35 -0
- package/src/easings/linear.ts +4 -0
- package/src/easings/power.ts +38 -0
- package/src/easings/sine.ts +7 -0
- package/src/easings/smooth.ts +25 -0
- package/src/easings/steps.ts +25 -0
- package/src/index.ts +9 -0
- package/src/modifiers/blend.ts +24 -0
- package/src/modifiers/chain.ts +63 -0
- package/src/modifiers/index.ts +6 -0
- package/src/modifiers/mirror.ts +14 -0
- package/src/modifiers/reverse.ts +7 -0
- package/src/modifiers/slice.ts +29 -0
- package/src/modifiers/yoyo.ts +15 -0
- package/src/parse.ts +167 -0
- package/src/reduced-motion.ts +26 -0
- 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
|
+
});
|