@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,349 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
anticipate,
|
|
4
|
+
bezier,
|
|
5
|
+
bezierEase,
|
|
6
|
+
bezierEaseIn,
|
|
7
|
+
bezierEaseInOut,
|
|
8
|
+
bezierEaseOut,
|
|
9
|
+
breathe,
|
|
10
|
+
custom,
|
|
11
|
+
expoScale,
|
|
12
|
+
gravity,
|
|
13
|
+
power1In,
|
|
14
|
+
power2Out,
|
|
15
|
+
rough,
|
|
16
|
+
slow,
|
|
17
|
+
spring,
|
|
18
|
+
wiggle,
|
|
19
|
+
} from "../index.js";
|
|
20
|
+
|
|
21
|
+
describe("bezier", () => {
|
|
22
|
+
it("linear (0, 0, 1, 1) matches identity", () => {
|
|
23
|
+
const linear = bezier(0, 0, 1, 1);
|
|
24
|
+
for (let i = 0; i <= 10; i++) {
|
|
25
|
+
const t = i / 10;
|
|
26
|
+
expect(linear(t)).toBeCloseTo(t, 10);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
it("hits (0, 0) and (1, 1) exactly", () => {
|
|
30
|
+
const e = bezier(0.17, 0.67, 0.83, 0.67);
|
|
31
|
+
expect(e(0)).toBe(0);
|
|
32
|
+
expect(e(1)).toBe(1);
|
|
33
|
+
});
|
|
34
|
+
it("CSS ease-in matches approximate shape (slow start, fast finish)", () => {
|
|
35
|
+
expect(bezierEaseIn(0.25)).toBeLessThan(0.25);
|
|
36
|
+
expect(bezierEaseIn(0.75)).toBeGreaterThan(0.5);
|
|
37
|
+
});
|
|
38
|
+
it("CSS ease-out matches approximate shape (fast start, slow finish)", () => {
|
|
39
|
+
expect(bezierEaseOut(0.25)).toBeGreaterThan(0.25);
|
|
40
|
+
expect(bezierEaseOut(0.75)).toBeGreaterThan(0.75);
|
|
41
|
+
});
|
|
42
|
+
it("CSS ease-in-out crosses 0.5 at t=0.5", () => {
|
|
43
|
+
expect(bezierEaseInOut(0.5)).toBeCloseTo(0.5, 3);
|
|
44
|
+
});
|
|
45
|
+
it("CSS ease is preset (slight head-start, overshoot-free)", () => {
|
|
46
|
+
expect(bezierEase(0.2)).toBeGreaterThan(0.2);
|
|
47
|
+
});
|
|
48
|
+
it("rejects out-of-range x control points", () => {
|
|
49
|
+
expect(() => bezier(-0.1, 0, 0.5, 1)).toThrow(RangeError);
|
|
50
|
+
expect(() => bezier(0.5, 0, 1.1, 1)).toThrow(RangeError);
|
|
51
|
+
});
|
|
52
|
+
it("permits out-of-range y (for overshoot bezier)", () => {
|
|
53
|
+
const overshoot = bezier(0.5, 1.5, 0.5, 1.5);
|
|
54
|
+
let max = 0;
|
|
55
|
+
for (let i = 0; i <= 100; i++) max = Math.max(max, overshoot(i / 100));
|
|
56
|
+
expect(max).toBeGreaterThan(1);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("spring", () => {
|
|
61
|
+
it("hits endpoints exactly at defaults", () => {
|
|
62
|
+
expect(spring(0)).toBe(0);
|
|
63
|
+
expect(spring(1)).toBe(1);
|
|
64
|
+
});
|
|
65
|
+
it("default spring shows oscillation", () => {
|
|
66
|
+
let crossings = 0;
|
|
67
|
+
let prev = spring(0);
|
|
68
|
+
for (let i = 1; i <= 200; i++) {
|
|
69
|
+
const curr = spring(i / 200);
|
|
70
|
+
if ((prev < 1 && curr >= 1) || (prev > 1 && curr <= 1)) crossings++;
|
|
71
|
+
prev = curr;
|
|
72
|
+
}
|
|
73
|
+
expect(crossings).toBeGreaterThanOrEqual(1);
|
|
74
|
+
});
|
|
75
|
+
it("overdamped spring does not overshoot", () => {
|
|
76
|
+
const heavy = spring.with({ damping: 40, stiffness: 100, mass: 1 });
|
|
77
|
+
let max = 0;
|
|
78
|
+
for (let i = 0; i <= 100; i++) max = Math.max(max, heavy(i / 100));
|
|
79
|
+
expect(max).toBeLessThanOrEqual(1 + 1e-6);
|
|
80
|
+
});
|
|
81
|
+
it("critically damped spring (zeta = 1) works", () => {
|
|
82
|
+
const k = 100;
|
|
83
|
+
const m = 1;
|
|
84
|
+
const b = 2 * Math.sqrt(k * m);
|
|
85
|
+
const critical = spring.with({ stiffness: k, mass: m, damping: b });
|
|
86
|
+
expect(critical(0)).toBe(0);
|
|
87
|
+
expect(critical(1)).toBe(1);
|
|
88
|
+
expect(critical(0.5)).toBeGreaterThan(0);
|
|
89
|
+
expect(critical(0.5)).toBeLessThanOrEqual(1);
|
|
90
|
+
});
|
|
91
|
+
it("stiffer spring settles faster relative to its own duration", () => {
|
|
92
|
+
const soft = spring.with({ stiffness: 50 });
|
|
93
|
+
const stiff = spring.with({ stiffness: 400 });
|
|
94
|
+
expect(soft(0.5)).not.toBe(stiff(0.5));
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("custom", () => {
|
|
99
|
+
it("two-point linear ease is identity", () => {
|
|
100
|
+
const line = custom([
|
|
101
|
+
[0, 0],
|
|
102
|
+
[1, 1],
|
|
103
|
+
]);
|
|
104
|
+
for (let i = 0; i <= 10; i++) {
|
|
105
|
+
const t = i / 10;
|
|
106
|
+
expect(line(t)).toBeCloseTo(t, 10);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
it("three-point piecewise linear interpolates correctly", () => {
|
|
110
|
+
const e = custom([
|
|
111
|
+
[0, 0],
|
|
112
|
+
[0.5, 0.8],
|
|
113
|
+
[1, 1],
|
|
114
|
+
]);
|
|
115
|
+
expect(e(0.25)).toBeCloseTo(0.4, 10);
|
|
116
|
+
expect(e(0.75)).toBeCloseTo(0.9, 10);
|
|
117
|
+
});
|
|
118
|
+
it("hits provided endpoints (framework clamp)", () => {
|
|
119
|
+
const e = custom([
|
|
120
|
+
[0, 0],
|
|
121
|
+
[1, 1],
|
|
122
|
+
]);
|
|
123
|
+
expect(e(0)).toBe(0);
|
|
124
|
+
expect(e(1)).toBe(1);
|
|
125
|
+
});
|
|
126
|
+
it("rejects fewer than 2 points", () => {
|
|
127
|
+
expect(() => custom([[0.5, 0.5]])).toThrow(RangeError);
|
|
128
|
+
});
|
|
129
|
+
it("rejects unsorted points", () => {
|
|
130
|
+
expect(() =>
|
|
131
|
+
custom([
|
|
132
|
+
[0, 0],
|
|
133
|
+
[0.7, 0.3],
|
|
134
|
+
[0.3, 0.8],
|
|
135
|
+
[1, 1],
|
|
136
|
+
]),
|
|
137
|
+
).toThrow(RangeError);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("rough", () => {
|
|
142
|
+
it("hits endpoints via framework clamp", () => {
|
|
143
|
+
const r = rough.with({ seed: 1 });
|
|
144
|
+
expect(r(0)).toBe(0);
|
|
145
|
+
expect(r(1)).toBe(1);
|
|
146
|
+
});
|
|
147
|
+
it("seeded rough is deterministic", () => {
|
|
148
|
+
const a = rough.with({ seed: 42, strength: 0.2 });
|
|
149
|
+
const b = rough.with({ seed: 42, strength: 0.2 });
|
|
150
|
+
for (let i = 0; i <= 20; i++) {
|
|
151
|
+
expect(a(i / 20)).toBeCloseTo(b(i / 20), 10);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
it("different seeds produce different curves", () => {
|
|
155
|
+
const a = rough.with({ seed: 1, strength: 0.2 });
|
|
156
|
+
const b = rough.with({ seed: 2, strength: 0.2 });
|
|
157
|
+
let diffs = 0;
|
|
158
|
+
for (let i = 1; i < 20; i++) {
|
|
159
|
+
if (Math.abs(a(i / 20) - b(i / 20)) > 1e-6) diffs++;
|
|
160
|
+
}
|
|
161
|
+
expect(diffs).toBeGreaterThan(10);
|
|
162
|
+
});
|
|
163
|
+
it("zero strength + linear template is identity", () => {
|
|
164
|
+
const r = rough.with({ seed: 7, strength: 0 });
|
|
165
|
+
for (let i = 1; i < 20; i++) {
|
|
166
|
+
const t = i / 20;
|
|
167
|
+
expect(r(t)).toBeCloseTo(t, 10);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
it("zero strength approximates curved template (within piecewise-linear error)", () => {
|
|
171
|
+
const r = rough.with({ seed: 7, strength: 0, points: 60, template: power2Out });
|
|
172
|
+
for (let i = 1; i < 20; i++) {
|
|
173
|
+
const t = i / 20;
|
|
174
|
+
expect(r(t)).toBeCloseTo(power2Out(t), 1);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("wiggle", () => {
|
|
180
|
+
it("wiggles crosses zero multiple times", () => {
|
|
181
|
+
const w = wiggle.with({ wiggles: 5, type: "uniform" });
|
|
182
|
+
let crossings = 0;
|
|
183
|
+
let prev = w(0);
|
|
184
|
+
for (let i = 1; i <= 200; i++) {
|
|
185
|
+
const curr = w(i / 200);
|
|
186
|
+
if ((prev >= 0 && curr < 0) || (prev <= 0 && curr > 0)) crossings++;
|
|
187
|
+
prev = curr;
|
|
188
|
+
}
|
|
189
|
+
expect(crossings).toBeGreaterThanOrEqual(8);
|
|
190
|
+
});
|
|
191
|
+
it("easeOut envelope decays toward the end", () => {
|
|
192
|
+
const w = wiggle.with({ wiggles: 4, type: "easeOut" });
|
|
193
|
+
let earlyMax = 0;
|
|
194
|
+
let lateMax = 0;
|
|
195
|
+
for (let i = 0; i <= 50; i++) earlyMax = Math.max(earlyMax, Math.abs(w(i / 200)));
|
|
196
|
+
for (let i = 150; i <= 200; i++) lateMax = Math.max(lateMax, Math.abs(w(i / 200)));
|
|
197
|
+
expect(earlyMax).toBeGreaterThan(lateMax);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("slow", () => {
|
|
202
|
+
it("is symmetric around t=0.5", () => {
|
|
203
|
+
const s = slow.with({ linearRatio: 0.7 });
|
|
204
|
+
expect(s(0.5)).toBeCloseTo(0.5, 6);
|
|
205
|
+
expect(s(0.3) + s(0.7)).toBeCloseTo(1, 6);
|
|
206
|
+
expect(s(0.1) + s(0.9)).toBeCloseTo(1, 6);
|
|
207
|
+
});
|
|
208
|
+
it("middle section is genuinely slow (slope < 1)", () => {
|
|
209
|
+
const s = slow.with({ linearRatio: 0.8, power: 0.7 });
|
|
210
|
+
const slope = (s(0.55) - s(0.45)) / 0.1;
|
|
211
|
+
expect(slope).toBeLessThan(0.8);
|
|
212
|
+
});
|
|
213
|
+
it("edges are fast (curve ahead of diagonal in first half)", () => {
|
|
214
|
+
const s = slow.with({ linearRatio: 0.7, power: 0.7 });
|
|
215
|
+
expect(s(0.1)).toBeGreaterThan(0.1);
|
|
216
|
+
});
|
|
217
|
+
it("higher power exaggerates the effect (slower middle)", () => {
|
|
218
|
+
const soft = slow.with({ linearRatio: 0.7, power: 0.3 });
|
|
219
|
+
const hard = slow.with({ linearRatio: 0.7, power: 2 });
|
|
220
|
+
const softSlope = (soft(0.55) - soft(0.45)) / 0.1;
|
|
221
|
+
const hardSlope = (hard(0.55) - hard(0.45)) / 0.1;
|
|
222
|
+
expect(hardSlope).toBeLessThan(softSlope);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("anticipate", () => {
|
|
227
|
+
it("dips below zero before reaching 1", () => {
|
|
228
|
+
let min = 0;
|
|
229
|
+
for (let i = 0; i <= 100; i++) min = Math.min(min, anticipate(i / 100));
|
|
230
|
+
expect(min).toBeLessThan(-0.01);
|
|
231
|
+
});
|
|
232
|
+
it("hits endpoints exactly", () => {
|
|
233
|
+
expect(anticipate(0)).toBe(0);
|
|
234
|
+
expect(anticipate(1)).toBe(1);
|
|
235
|
+
});
|
|
236
|
+
it("crosses zero exactly at t=0.5 (end of back.in phase)", () => {
|
|
237
|
+
expect(anticipate(0.5)).toBeCloseTo(0.5, 6);
|
|
238
|
+
});
|
|
239
|
+
it("higher overshoot means deeper dip", () => {
|
|
240
|
+
const soft = anticipate.with({ overshoot: 0.5 });
|
|
241
|
+
const hard = anticipate.with({ overshoot: 3 });
|
|
242
|
+
let softMin = 0;
|
|
243
|
+
let hardMin = 0;
|
|
244
|
+
for (let i = 0; i <= 100; i++) {
|
|
245
|
+
softMin = Math.min(softMin, soft(i / 100));
|
|
246
|
+
hardMin = Math.min(hardMin, hard(i / 100));
|
|
247
|
+
}
|
|
248
|
+
expect(hardMin).toBeLessThan(softMin);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("gravity", () => {
|
|
253
|
+
it("hits endpoints exactly at all weights", () => {
|
|
254
|
+
for (const w of [0, 0.5, 1, 2, 3]) {
|
|
255
|
+
const g = gravity.with({ weight: w });
|
|
256
|
+
expect(g(0)).toBe(0);
|
|
257
|
+
expect(g(1)).toBe(1);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
it("weight=0 is linear", () => {
|
|
261
|
+
const g = gravity.with({ weight: 0 });
|
|
262
|
+
for (let i = 1; i < 10; i++) {
|
|
263
|
+
const t = i / 10;
|
|
264
|
+
expect(g(t)).toBeCloseTo(t, 10);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
it("weight=1 matches power1.in (quadratic)", () => {
|
|
268
|
+
const g = gravity.with({ weight: 1 });
|
|
269
|
+
for (let i = 1; i < 10; i++) {
|
|
270
|
+
const t = i / 10;
|
|
271
|
+
expect(g(t)).toBeCloseTo(power1In(t), 10);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
it("higher weight stays below at the same t", () => {
|
|
275
|
+
const light = gravity.with({ weight: 0.5 });
|
|
276
|
+
const heavy = gravity.with({ weight: 2 });
|
|
277
|
+
expect(heavy(0.5)).toBeLessThan(light(0.5));
|
|
278
|
+
});
|
|
279
|
+
it("monotonically non-decreasing across [0, 1]", () => {
|
|
280
|
+
const g = gravity.with({ weight: 1.5 });
|
|
281
|
+
let prev = g(0);
|
|
282
|
+
for (let i = 1; i <= 100; i++) {
|
|
283
|
+
const curr = g(i / 100);
|
|
284
|
+
expect(curr).toBeGreaterThanOrEqual(prev - 1e-9);
|
|
285
|
+
prev = curr;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("breathe", () => {
|
|
291
|
+
it("starts at 0 (cosine wave shifted into [0, 1])", () => {
|
|
292
|
+
expect(breathe(0)).toBe(0);
|
|
293
|
+
});
|
|
294
|
+
it("default cycles=1 returns to 0 at t=1 (full breath cycle)", () => {
|
|
295
|
+
expect(breathe(1)).toBeCloseTo(0, 10);
|
|
296
|
+
});
|
|
297
|
+
it("cycles=0.5 reaches 1 at t=1 (half cycle / single inhale)", () => {
|
|
298
|
+
const b = breathe.with({ cycles: 0.5 });
|
|
299
|
+
expect(b(1)).toBeCloseTo(1, 10);
|
|
300
|
+
});
|
|
301
|
+
it("default cycles=1 hits peak ~1 at t=0.5 (mid-breath)", () => {
|
|
302
|
+
expect(breathe(0.5)).toBeCloseTo(1, 10);
|
|
303
|
+
});
|
|
304
|
+
it("output stays in [0, 1] for any cycles count", () => {
|
|
305
|
+
for (const c of [0.5, 1, 2, 4, 8]) {
|
|
306
|
+
const b = breathe.with({ cycles: c });
|
|
307
|
+
for (let i = 0; i <= 200; i++) {
|
|
308
|
+
const v = b(i / 200);
|
|
309
|
+
expect(v).toBeGreaterThanOrEqual(-1e-9);
|
|
310
|
+
expect(v).toBeLessThanOrEqual(1 + 1e-9);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
it("cycles=2 has two peaks across [0, 1]", () => {
|
|
315
|
+
const b = breathe.with({ cycles: 2 });
|
|
316
|
+
let peaks = 0;
|
|
317
|
+
let prev = b(0);
|
|
318
|
+
let rising = true;
|
|
319
|
+
for (let i = 1; i <= 200; i++) {
|
|
320
|
+
const curr = b(i / 200);
|
|
321
|
+
if (rising && curr < prev) {
|
|
322
|
+
peaks++;
|
|
323
|
+
rising = false;
|
|
324
|
+
} else if (!rising && curr > prev) {
|
|
325
|
+
rising = true;
|
|
326
|
+
}
|
|
327
|
+
prev = curr;
|
|
328
|
+
}
|
|
329
|
+
expect(peaks).toBe(2);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe("expoScale", () => {
|
|
334
|
+
it("maps large scale ratios evenly", () => {
|
|
335
|
+
const e = expoScale(1, 100);
|
|
336
|
+
expect(e(0)).toBe(0);
|
|
337
|
+
expect(e(1)).toBe(1);
|
|
338
|
+
expect(e(0.5)).toBeGreaterThan(0);
|
|
339
|
+
expect(e(0.5)).toBeLessThan(0.5);
|
|
340
|
+
});
|
|
341
|
+
it("rejects non-positive scales", () => {
|
|
342
|
+
expect(() => expoScale(0, 10)).toThrow(RangeError);
|
|
343
|
+
expect(() => expoScale(1, -10)).toThrow(RangeError);
|
|
344
|
+
});
|
|
345
|
+
it("collapses to zero motion when start === end", () => {
|
|
346
|
+
const e = expoScale(2, 2);
|
|
347
|
+
expect(e(0.5)).toBe(0);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
backOut,
|
|
4
|
+
bounceOut,
|
|
5
|
+
elasticOut,
|
|
6
|
+
linear,
|
|
7
|
+
power2Out,
|
|
8
|
+
spring,
|
|
9
|
+
steps,
|
|
10
|
+
} from "../index.js";
|
|
11
|
+
|
|
12
|
+
const ALL_EASES = [linear, power2Out, backOut, elasticOut, bounceOut, spring, steps];
|
|
13
|
+
// Steps opts out of exactEndpoints (stepped "start"/"both" positions need non-zero output at t=0).
|
|
14
|
+
const CLAMPING_EASES = [linear, power2Out, backOut, elasticOut, bounceOut, spring];
|
|
15
|
+
|
|
16
|
+
describe("contract: input validation", () => {
|
|
17
|
+
it.each(ALL_EASES.map((fn) => [fn.easingName, fn] as const))(
|
|
18
|
+
"%s returns 0 on NaN input",
|
|
19
|
+
(_, fn) => {
|
|
20
|
+
expect(fn(Number.NaN)).toBe(0);
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
it.each(ALL_EASES.map((fn) => [fn.easingName, fn] as const))(
|
|
24
|
+
"%s returns 0 on +Infinity input",
|
|
25
|
+
(_, fn) => {
|
|
26
|
+
expect(fn(Number.POSITIVE_INFINITY)).toBe(0);
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
it.each(ALL_EASES.map((fn) => [fn.easingName, fn] as const))(
|
|
30
|
+
"%s returns 0 on -Infinity input",
|
|
31
|
+
(_, fn) => {
|
|
32
|
+
expect(fn(Number.NEGATIVE_INFINITY)).toBe(0);
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
it.each(CLAMPING_EASES.map((fn) => [fn.easingName, fn] as const))(
|
|
36
|
+
"%s clamps t<0 to 0 for exactEndpoint eases",
|
|
37
|
+
(_, fn) => {
|
|
38
|
+
expect(fn(-0.5)).toBe(0);
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
it.each(CLAMPING_EASES.map((fn) => [fn.easingName, fn] as const))(
|
|
42
|
+
"%s clamps t>1 to 1 for exactEndpoint eases",
|
|
43
|
+
(_, fn) => {
|
|
44
|
+
expect(fn(1.5)).toBe(1);
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
backOut,
|
|
4
|
+
bezierEaseInOut,
|
|
5
|
+
linear,
|
|
6
|
+
power2Out,
|
|
7
|
+
toCSSBezier,
|
|
8
|
+
toCSSKeyframes,
|
|
9
|
+
toCSSLinear,
|
|
10
|
+
} from "../index.js";
|
|
11
|
+
|
|
12
|
+
describe("toCSSLinear", () => {
|
|
13
|
+
it("emits linear() prefix with sample count + 1 values", () => {
|
|
14
|
+
const css = toCSSLinear(linear, 4);
|
|
15
|
+
expect(css.startsWith("linear(")).toBe(true);
|
|
16
|
+
expect(css.endsWith(")")).toBe(true);
|
|
17
|
+
const values = css.slice("linear(".length, -1).split(", ");
|
|
18
|
+
expect(values.length).toBe(5);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("linear ease maps to evenly-spaced values 0..1", () => {
|
|
22
|
+
const css = toCSSLinear(linear, 4);
|
|
23
|
+
expect(css).toBe("linear(0, 0.25, 0.5, 0.75, 1)");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("power2Out produces increasing but decelerating values", () => {
|
|
27
|
+
const css = toCSSLinear(power2Out, 4);
|
|
28
|
+
const values = css.slice("linear(".length, -1).split(", ").map(Number);
|
|
29
|
+
expect(values[0]).toBe(0);
|
|
30
|
+
expect(values[values.length - 1]).toBe(1);
|
|
31
|
+
for (let i = 1; i < values.length; i++) {
|
|
32
|
+
expect(values[i]!).toBeGreaterThan(values[i - 1]!);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("includes overshoot values for back.out", () => {
|
|
37
|
+
const css = toCSSLinear(backOut, 40);
|
|
38
|
+
const values = css.slice("linear(".length, -1).split(", ").map(Number);
|
|
39
|
+
const max = Math.max(...values);
|
|
40
|
+
expect(max).toBeGreaterThan(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("rejects zero or negative samples", () => {
|
|
44
|
+
expect(() => toCSSLinear(linear, 0)).toThrow(RangeError);
|
|
45
|
+
expect(() => toCSSLinear(linear, -5)).toThrow(RangeError);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("accepts plain easing functions", () => {
|
|
49
|
+
const css = toCSSLinear((t) => t * t, 4);
|
|
50
|
+
expect(css).toMatch(/^linear\(0, .+, 1\)$/);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("toCSSBezier", () => {
|
|
55
|
+
it("emits cubic-bezier() string", () => {
|
|
56
|
+
expect(toCSSBezier(0.42, 0, 0.58, 1)).toBe("cubic-bezier(0.42, 0, 0.58, 1)");
|
|
57
|
+
});
|
|
58
|
+
it("trims trailing zeros and handles integers", () => {
|
|
59
|
+
expect(toCSSBezier(0, 0.5, 1, 1)).toBe("cubic-bezier(0, 0.5, 1, 1)");
|
|
60
|
+
});
|
|
61
|
+
it("matches our own bezier easing sampled output (sanity)", () => {
|
|
62
|
+
const samples = 12;
|
|
63
|
+
const css = toCSSLinear(bezierEaseInOut, samples);
|
|
64
|
+
const native = toCSSBezier(0.42, 0, 0.58, 1);
|
|
65
|
+
expect(css).toContain("linear(");
|
|
66
|
+
expect(native).toContain("cubic-bezier(");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("toCSSKeyframes", () => {
|
|
71
|
+
it("generates @keyframes block with opening/closing braces", () => {
|
|
72
|
+
const kf = toCSSKeyframes("slide", "transform", (v) => `translateX(${v * 100}px)`, linear, 4);
|
|
73
|
+
expect(kf.startsWith("@keyframes slide {")).toBe(true);
|
|
74
|
+
expect(kf.endsWith("}")).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("emits 0% and 100% markers", () => {
|
|
78
|
+
const kf = toCSSKeyframes("x", "opacity", (v) => String(v), linear, 4);
|
|
79
|
+
expect(kf).toContain("0% { opacity: 0; }");
|
|
80
|
+
expect(kf).toContain("100% { opacity: 1; }");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("uses the easing to compute property values at each keyframe", () => {
|
|
84
|
+
const kf = toCSSKeyframes("x", "opacity", (v) => String(v), power2Out, 4);
|
|
85
|
+
expect(kf).toMatch(/50% \{ opacity: 0\.875; \}/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("rejects samples below 2", () => {
|
|
89
|
+
expect(() =>
|
|
90
|
+
toCSSKeyframes("x", "opacity", (v) => String(v), linear, 1),
|
|
91
|
+
).toThrow(RangeError);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { bench, describe } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
backOut,
|
|
4
|
+
bezier,
|
|
5
|
+
bounceOut,
|
|
6
|
+
circOut,
|
|
7
|
+
elasticOut,
|
|
8
|
+
linear,
|
|
9
|
+
parseEasing,
|
|
10
|
+
power2Out,
|
|
11
|
+
rough,
|
|
12
|
+
sineOut,
|
|
13
|
+
spring,
|
|
14
|
+
steps,
|
|
15
|
+
toCSSLinear,
|
|
16
|
+
wiggle,
|
|
17
|
+
} from "../index.js";
|
|
18
|
+
|
|
19
|
+
const ITERS = 1000;
|
|
20
|
+
|
|
21
|
+
function runIter(fn: (t: number) => number) {
|
|
22
|
+
for (let i = 0; i < ITERS; i++) fn(i / ITERS);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("core easings (1000 samples)", () => {
|
|
26
|
+
bench("linear", () => runIter(linear));
|
|
27
|
+
bench("power2Out (cubic)", () => runIter(power2Out));
|
|
28
|
+
bench("sineOut", () => runIter(sineOut));
|
|
29
|
+
bench("circOut", () => runIter(circOut));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("parametric easings (1000 samples)", () => {
|
|
33
|
+
bench("backOut", () => runIter(backOut));
|
|
34
|
+
bench("elasticOut", () => runIter(elasticOut));
|
|
35
|
+
bench("bounceOut", () => runIter(bounceOut));
|
|
36
|
+
bench("steps (count=5)", () => runIter(steps));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("builders (1000 samples)", () => {
|
|
40
|
+
const bez = bezier(0.42, 0, 0.58, 1);
|
|
41
|
+
const rgh = rough.with({ seed: 1 });
|
|
42
|
+
const wgl = wiggle.with({ wiggles: 5 });
|
|
43
|
+
|
|
44
|
+
bench("bezier (Newton + subdivision)", () => runIter(bez));
|
|
45
|
+
bench("spring", () => runIter(spring));
|
|
46
|
+
bench("rough", () => runIter(rgh));
|
|
47
|
+
bench("wiggle", () => runIter(wgl));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("factory + one-shot usage (100 iterations)", () => {
|
|
51
|
+
bench("backOut.with({overshoot: n}) each loop", () => {
|
|
52
|
+
for (let i = 0; i < 100; i++) {
|
|
53
|
+
const fn = backOut.with({ overshoot: 1 + i / 100 });
|
|
54
|
+
fn(0.5);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
bench("parseEasing('power2.out')", () => {
|
|
58
|
+
for (let i = 0; i < 100; i++) parseEasing("power2.out");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("CSS export (40 samples)", () => {
|
|
63
|
+
bench("toCSSLinear(power2Out)", () => {
|
|
64
|
+
for (let i = 0; i < 50; i++) toCSSLinear(power2Out);
|
|
65
|
+
});
|
|
66
|
+
});
|