@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,38 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { linear, power2Out, slice } from "../index.js";
3
+
4
+ describe("slice", () => {
5
+ it("slice of linear across full range is identity", () => {
6
+ const s = slice(linear, 0, 1);
7
+ expect(s).toBe(linear);
8
+ });
9
+
10
+ it("slice of linear over a sub-range is still linear", () => {
11
+ const s = slice(linear, 0.25, 0.75);
12
+ for (let i = 0; i <= 10; i++) {
13
+ const t = i / 10;
14
+ expect(s(t)).toBeCloseTo(t, 10);
15
+ }
16
+ });
17
+
18
+ it("hits new endpoints exactly", () => {
19
+ const s = slice(power2Out, 0.2, 0.8);
20
+ expect(s(0)).toBe(0);
21
+ expect(s(1)).toBe(1);
22
+ });
23
+
24
+ it("slice output at t=0 corresponds to ease(start) remapped", () => {
25
+ const ease = power2Out;
26
+ const s = slice(ease, 0.3, 0.9);
27
+ // At t=0.5 inside the slice, we should see ease(0.6) normalized
28
+ const expected = (ease(0.6) - ease(0.3)) / (ease(0.9) - ease(0.3));
29
+ expect(s(0.5)).toBeCloseTo(expected, 10);
30
+ });
31
+
32
+ it("rejects invalid ranges", () => {
33
+ expect(() => slice(linear, -0.1, 0.5)).toThrow(RangeError);
34
+ expect(() => slice(linear, 0.5, 0.5)).toThrow(RangeError);
35
+ expect(() => slice(linear, 0.8, 0.2)).toThrow(RangeError);
36
+ expect(() => slice(linear, 0, 1.1)).toThrow(RangeError);
37
+ });
38
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ SPRING_PRESETS,
4
+ gentleSpring,
5
+ molassesSpring,
6
+ slowSpring,
7
+ stiffSpring,
8
+ wobblySpring,
9
+ } from "../index.js";
10
+
11
+ describe("spring presets", () => {
12
+ it("all presets hit endpoints", () => {
13
+ for (const preset of [gentleSpring, wobblySpring, stiffSpring, slowSpring, molassesSpring]) {
14
+ expect(preset(0)).toBe(0);
15
+ expect(preset(1)).toBe(1);
16
+ }
17
+ });
18
+
19
+ it("wobbly has more oscillation than stiff", () => {
20
+ let wobblyCrossings = 0;
21
+ let stiffCrossings = 0;
22
+ let prevW = wobblySpring(0);
23
+ let prevS = stiffSpring(0);
24
+ for (let i = 1; i <= 200; i++) {
25
+ const w = wobblySpring(i / 200);
26
+ const s = stiffSpring(i / 200);
27
+ if ((prevW < 1 && w >= 1) || (prevW > 1 && w <= 1)) wobblyCrossings++;
28
+ if ((prevS < 1 && s >= 1) || (prevS > 1 && s <= 1)) stiffCrossings++;
29
+ prevW = w;
30
+ prevS = s;
31
+ }
32
+ expect(wobblyCrossings).toBeGreaterThanOrEqual(stiffCrossings);
33
+ });
34
+
35
+ it("molasses is heavily damped (no overshoot)", () => {
36
+ let max = 0;
37
+ for (let i = 0; i <= 200; i++) max = Math.max(max, molassesSpring(i / 200));
38
+ expect(max).toBeLessThanOrEqual(1 + 1e-6);
39
+ });
40
+
41
+ it("SPRING_PRESETS values are sensible", () => {
42
+ for (const [, cfg] of Object.entries(SPRING_PRESETS)) {
43
+ expect(cfg.stiffness).toBeGreaterThan(0);
44
+ expect(cfg.damping).toBeGreaterThan(0);
45
+ expect(cfg.mass).toBeGreaterThan(0);
46
+ }
47
+ });
48
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ /**
4
+ * easings vitest config runs in Node (no `browser.enabled`) so the mere
5
+ * fact these tests execute proves the library is SSR-safe at import time.
6
+ * Additional checks verify no DOM globals are required.
7
+ */
8
+
9
+ describe("SSR safety", () => {
10
+ it("window is undefined in this runtime", () => {
11
+ expect(typeof window).toBe("undefined");
12
+ });
13
+
14
+ it("module loads without DOM globals", async () => {
15
+ const mod = await import("../index.js");
16
+ expect(mod).toBeDefined();
17
+ expect(typeof mod.linear).toBe("function");
18
+ expect(typeof mod.defineEasing).toBe("function");
19
+ expect(typeof mod.parseEasing).toBe("function");
20
+ });
21
+
22
+ it("all catalog functions execute without DOM", async () => {
23
+ const {
24
+ linear,
25
+ power2Out,
26
+ backOut,
27
+ elasticOut,
28
+ bounceOut,
29
+ steps,
30
+ spring,
31
+ bezier,
32
+ rough,
33
+ wiggle,
34
+ } = await import("../index.js");
35
+ expect(linear(0.5)).toBe(0.5);
36
+ expect(power2Out(0.5)).toBeCloseTo(0.875, 3);
37
+ expect(backOut(0.5)).toBeGreaterThan(0.9);
38
+ expect(elasticOut(0.5)).toBeGreaterThan(0);
39
+ expect(bounceOut(0.5)).toBeGreaterThan(0);
40
+ expect(steps(0.5)).toBe(0.4);
41
+ expect(spring(0.5)).toBeGreaterThan(0);
42
+ expect(bezier(0.42, 0, 0.58, 1)(0.5)).toBeCloseTo(0.5, 3);
43
+ expect(rough.with({ seed: 1 })(0.5)).toBeGreaterThan(0);
44
+ expect(wiggle.with({ wiggles: 5 })(0.5)).toBeCloseTo(0, 6);
45
+ });
46
+
47
+ it("CSS subpath loads without DOM", async () => {
48
+ const { toCSSLinear, toCSSBezier } = await import("../css.js");
49
+ expect(typeof toCSSLinear).toBe("function");
50
+ expect(typeof toCSSBezier).toBe("function");
51
+ });
52
+
53
+ it("parse subpath loads without DOM", async () => {
54
+ const { parseEasing } = await import("../parse.js");
55
+ expect(typeof parseEasing).toBe("function");
56
+ });
57
+
58
+ it("reduced-motion subpath loads without DOM and returns false", async () => {
59
+ const { prefersReducedMotion } = await import("../reduced-motion.js");
60
+ expect(prefersReducedMotion()).toBe(false);
61
+ });
62
+ });
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Compile-time inference assertions. Not a runtime test — `pnpm typecheck`
3
+ * must pass with ZERO emitted errors, and every `@ts-expect-error` must
4
+ * actually catch a real type error. Guards against regressions in the
5
+ * parametric .with() API and the defineEasing factories.
6
+ */
7
+
8
+ import {
9
+ anticipateIn,
10
+ backOut,
11
+ bezier,
12
+ custom,
13
+ defineEasing,
14
+ defineParametricEasing,
15
+ elasticOut,
16
+ linear,
17
+ parseEasing,
18
+ power2Out,
19
+ spring,
20
+ steps,
21
+ toCSSLinear,
22
+ type EasingFn,
23
+ type ParametricEasing,
24
+ } from "../index.js";
25
+
26
+ // --------- Valid usage: should compile ---------
27
+
28
+ const _a: EasingFn = power2Out;
29
+ const _b: number = power2Out(0.5);
30
+ const _c: string = power2Out.easingName;
31
+
32
+ // Parametric eases: callable AND have .with() + .defaults
33
+ const _d: EasingFn = backOut;
34
+ const _e: EasingFn = backOut.with({ overshoot: 2 });
35
+ const _f: EasingFn = backOut.with({});
36
+ const _g: Readonly<{ overshoot: number }> = backOut.defaults;
37
+
38
+ // Spring with full params
39
+ const _h = spring.with({ stiffness: 200, damping: 20, mass: 1, velocity: 0 });
40
+
41
+ // Partial is allowed
42
+ const _i = spring.with({ stiffness: 200 });
43
+
44
+ // Elastic params
45
+ const _j = elasticOut.with({ amplitude: 1.2, period: 0.4 });
46
+
47
+ // Steps params with correct string literal
48
+ const _k = steps.with({ count: 5, position: "start" });
49
+ const _l = steps.with({ count: 5, position: "end" });
50
+ const _m = steps.with({ count: 5, position: "none" });
51
+
52
+ // Anticipate
53
+ const _n = anticipateIn.with({ overshoot: 2 });
54
+
55
+ // Builders
56
+ const _o: EasingFn = bezier(0.42, 0, 0.58, 1);
57
+ const _p: EasingFn = custom([
58
+ [0, 0],
59
+ [0.5, 0.8],
60
+ [1, 1],
61
+ ]);
62
+
63
+ // parseEasing returns EasingFn
64
+ const _q: EasingFn = parseEasing("power2.out");
65
+
66
+ // CSS
67
+ const _r: string = toCSSLinear(power2Out);
68
+ const _s: string = toCSSLinear((t: number) => t);
69
+
70
+ // defineEasing factory
71
+ const _customEase: EasingFn = defineEasing("myEase", (t) => t * t);
72
+ const _customParametric: ParametricEasing<{ strength: number }> = defineParametricEasing(
73
+ "myParam",
74
+ { strength: 1 },
75
+ ({ strength }) => (t) => t * strength,
76
+ );
77
+
78
+ // Linear is a plain EasingFn, not parametric
79
+ const _t: EasingFn = linear;
80
+
81
+ // --------- Invalid usage: must be caught ---------
82
+
83
+ // @ts-expect-error — power2Out has no .with() method (not parametric)
84
+ power2Out.with({});
85
+
86
+ // @ts-expect-error — unknown param key
87
+ backOut.with({ nonexistent: 1 });
88
+
89
+ // @ts-expect-error — wrong value type (string for numeric param)
90
+ backOut.with({ overshoot: "big" });
91
+
92
+ // @ts-expect-error — wrong value type for steps.position
93
+ steps.with({ count: 5, position: "both" });
94
+
95
+ // @ts-expect-error — input must be number
96
+ power2Out("not a number");
97
+
98
+ // @ts-expect-error — no args
99
+ power2Out();
100
+
101
+ // @ts-expect-error — linear is not parametric
102
+ linear.with({});
103
+
104
+ export {};
@@ -0,0 +1,66 @@
1
+ import { defineParametricEasing } from "../define.js";
2
+
3
+ export type AnticipateParams = {
4
+ /** Depth of the backward dip before the forward motion. Default 1.525 (Framer Motion default). */
5
+ overshoot: number;
6
+ };
7
+
8
+ const DEFAULTS: AnticipateParams = { overshoot: 1.525 };
9
+
10
+ function anticipateInBuild({ overshoot: s }: AnticipateParams) {
11
+ const n = Math.max(1, s + 3);
12
+ return (t: number) => {
13
+ if (t < 0.5) {
14
+ const p = t * 2;
15
+ return 0.5 * (p * p * ((s + 1) * p - s));
16
+ }
17
+ const p = t * 2 - 1;
18
+ return 0.5 + 0.5 * (1 - (1 - p) ** n);
19
+ };
20
+ }
21
+
22
+ function anticipateOutBuild({ overshoot: s }: AnticipateParams) {
23
+ const n = Math.max(1, s + 3);
24
+ return (t: number) => {
25
+ if (t < 0.5) {
26
+ const p = t * 2;
27
+ return 0.5 * (p ** n);
28
+ }
29
+ const u = 1 - t;
30
+ const p = u * 2;
31
+ return 1 - 0.5 * (p * p * ((s + 1) * p - s));
32
+ };
33
+ }
34
+
35
+ function anticipateInOutBuild({ overshoot: s }: AnticipateParams) {
36
+ return (t: number) => {
37
+ if (t < 0.5) {
38
+ const p = t * 2;
39
+ return 0.5 * (p * p * ((s + 1) * p - s));
40
+ }
41
+ const u = 1 - t;
42
+ const p = u * 2;
43
+ return 1 - 0.5 * (p * p * ((s + 1) * p - s));
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Character-animation-style anticipation: wind-up before the forward motion.
49
+ *
50
+ * - `anticipateIn` (formerly just `anticipate`): dip at start, smooth arrival.
51
+ * - `anticipateOut`: smooth departure, overshoot dip near the end.
52
+ * - `anticipateInOut`: dip at start AND overshoot near the end (wind-up + follow-through).
53
+ *
54
+ * The second half of `anticipateIn` uses a power-n ease where n matches the
55
+ * first-half slope at t=0.5, giving C¹ continuity regardless of overshoot.
56
+ */
57
+ export const anticipateIn = defineParametricEasing("anticipate.in", DEFAULTS, anticipateInBuild);
58
+ export const anticipateOut = defineParametricEasing("anticipate.out", DEFAULTS, anticipateOutBuild);
59
+ export const anticipateInOut = defineParametricEasing(
60
+ "anticipate.inOut",
61
+ DEFAULTS,
62
+ anticipateInOutBuild,
63
+ );
64
+
65
+ /** Alias for `anticipateIn` (Framer Motion's default shape). */
66
+ export const anticipate = anticipateIn;
@@ -0,0 +1,71 @@
1
+ import { defineEasing } from "../define.js";
2
+ import type { EasingFn } from "../types.js";
3
+
4
+ const NEWTON_ITERATIONS = 8;
5
+ const NEWTON_MIN_SLOPE = 1e-6;
6
+ const SUBDIVISION_PRECISION = 1e-7;
7
+ const SUBDIVISION_MAX_ITERATIONS = 32;
8
+
9
+ function a(c1: number, c2: number): number {
10
+ return 1 - 3 * c2 + 3 * c1;
11
+ }
12
+ function b(c1: number, c2: number): number {
13
+ return 3 * c2 - 6 * c1;
14
+ }
15
+ function c(c1: number): number {
16
+ return 3 * c1;
17
+ }
18
+
19
+ function sampleCurve(t: number, A: number, B: number, C: number): number {
20
+ return ((A * t + B) * t + C) * t;
21
+ }
22
+
23
+ function sampleDerivative(t: number, A: number, B: number, C: number): number {
24
+ return (3 * A * t + 2 * B) * t + C;
25
+ }
26
+
27
+ function solveCurveX(x: number, ax: number, bx: number, cx: number): number {
28
+ let t = x;
29
+ for (let i = 0; i < NEWTON_ITERATIONS; i++) {
30
+ const slope = sampleDerivative(t, ax, bx, cx);
31
+ if (Math.abs(slope) < NEWTON_MIN_SLOPE) break;
32
+ const currentX = sampleCurve(t, ax, bx, cx) - x;
33
+ t -= currentX / slope;
34
+ }
35
+ let lo = 0;
36
+ let hi = 1;
37
+ t = x;
38
+ if (t < lo) return lo;
39
+ if (t > hi) return hi;
40
+ for (let i = 0; i < SUBDIVISION_MAX_ITERATIONS; i++) {
41
+ const currentX = sampleCurve(t, ax, bx, cx);
42
+ if (Math.abs(currentX - x) < SUBDIVISION_PRECISION) return t;
43
+ if (currentX < x) lo = t;
44
+ else hi = t;
45
+ t = (lo + hi) / 2;
46
+ }
47
+ return t;
48
+ }
49
+
50
+ export function bezier(p1x: number, p1y: number, p2x: number, p2y: number): EasingFn {
51
+ if (p1x < 0 || p1x > 1 || p2x < 0 || p2x > 1) {
52
+ throw new RangeError(
53
+ `bezier: control point x coordinates must be in [0, 1]; got p1x=${p1x}, p2x=${p2x}`,
54
+ );
55
+ }
56
+ const ax = a(p1x, p2x);
57
+ const bx = b(p1x, p2x);
58
+ const cx = c(p1x);
59
+ const ay = a(p1y, p2y);
60
+ const by = b(p1y, p2y);
61
+ const cy = c(p1y);
62
+ const linear = p1x === p1y && p2x === p2y;
63
+ const name = `bezier(${p1x}, ${p1y}, ${p2x}, ${p2y})`;
64
+ if (linear) return defineEasing(name, (t) => t);
65
+ return defineEasing(name, (t) => sampleCurve(solveCurveX(t, ax, bx, cx), ay, by, cy));
66
+ }
67
+
68
+ export const bezierEase = bezier(0.25, 0.1, 0.25, 1);
69
+ export const bezierEaseIn = bezier(0.42, 0, 1, 1);
70
+ export const bezierEaseOut = bezier(0, 0, 0.58, 1);
71
+ export const bezierEaseInOut = bezier(0.42, 0, 0.58, 1);
@@ -0,0 +1,31 @@
1
+ import { defineParametricEasing } from "../define.js";
2
+
3
+ export type BreatheParams = {
4
+ /**
5
+ * Number of in-out breath cycles across [0, 1]. 0.5 = single inhale
6
+ * (0 → 1). 1 = inhale + exhale (0 → 1 → 0). 2 = two full cycles. The
7
+ * curve oscillates in [0, 1] so it maps cleanly to opacity / scale /
8
+ * any normalised animatable.
9
+ */
10
+ cycles: number;
11
+ };
12
+
13
+ const DEFAULTS: BreatheParams = { cycles: 1 };
14
+
15
+ const TAU = Math.PI * 2;
16
+
17
+ // (1 - cos(τ·c·t)) / 2 — cosine wave shifted into [0, 1]. Distinct from
18
+ // `wiggle` (which oscillates [-1, 1] for shake/vibration); breathe
19
+ // stays positive and is intended for idle/ambient animations.
20
+ //
21
+ // exactEndpoints: false — for cycles ≠ 0.5n, the natural curve doesn't
22
+ // land at 1 at t=1, and clamping defeats the rhythmic intent.
23
+ export const breathe = defineParametricEasing(
24
+ "breathe",
25
+ DEFAULTS,
26
+ ({ cycles }) => {
27
+ const c = Math.max(0, cycles);
28
+ return (t) => (1 - Math.cos(TAU * c * t)) / 2;
29
+ },
30
+ { exactEndpoints: false },
31
+ );
@@ -0,0 +1,43 @@
1
+ import { defineEasing } from "../define.js";
2
+ import type { EasingFn } from "../types.js";
3
+
4
+ export type CustomPoint = readonly [x: number, y: number];
5
+
6
+ /**
7
+ * Build an easing from a sequence of (x, y) control points, interpolated
8
+ * linearly between them. Use for hand-drawn curves, exported visualiser
9
+ * data, or any non-analytic easing shape.
10
+ *
11
+ * Points must be in ascending x order with x in [0, 1]. Endpoints are
12
+ * clamped to (0, 0) and (1, 1) by the framework — provide those points
13
+ * explicitly if you want to see the un-clamped curve.
14
+ */
15
+ export function custom(points: ReadonlyArray<CustomPoint>): EasingFn {
16
+ if (points.length < 2) {
17
+ throw new RangeError(`custom: at least 2 points required; got ${points.length}`);
18
+ }
19
+ for (let i = 1; i < points.length; i++) {
20
+ const prev = points[i - 1]!;
21
+ const curr = points[i]!;
22
+ if (curr[0] < prev[0]) {
23
+ throw new RangeError(
24
+ `custom: points must be sorted by ascending x; got ${prev[0]} then ${curr[0]} at index ${i}`,
25
+ );
26
+ }
27
+ }
28
+ const name = `custom(${points.length} points)`;
29
+ return defineEasing(name, (t) => {
30
+ if (t <= points[0]![0]) return points[0]![1];
31
+ for (let i = 1; i < points.length; i++) {
32
+ const [x1, y1] = points[i]!;
33
+ if (t <= x1) {
34
+ const [x0, y0] = points[i - 1]!;
35
+ const span = x1 - x0;
36
+ if (span === 0) return y1;
37
+ const u = (t - x0) / span;
38
+ return y0 + (y1 - y0) * u;
39
+ }
40
+ }
41
+ return points[points.length - 1]![1];
42
+ });
43
+ }
@@ -0,0 +1,30 @@
1
+ import { defineEasing } from "../define.js";
2
+ import { linear } from "../easings/linear.js";
3
+ import type { EasingFn } from "../types.js";
4
+
5
+ /**
6
+ * Ease tuned for scale animations spanning large ratios (e.g. 0.01 → 100).
7
+ * Linear interpolation across such ranges feels jerky because perception
8
+ * is logarithmic. `expoScale` maps t so that when used as
9
+ * `scale = startScale + (endScale - startScale) * expoScale(t)`, the
10
+ * visual motion is uniform across orders of magnitude.
11
+ *
12
+ * Optionally combine with another ease: the result of `ease(t)` is what
13
+ * gets log-mapped.
14
+ */
15
+ export function expoScale(startScale: number, endScale: number, ease: EasingFn = linear): EasingFn {
16
+ if (startScale <= 0 || endScale <= 0) {
17
+ throw new RangeError(
18
+ `expoScale: startScale and endScale must be positive; got ${startScale}, ${endScale}`,
19
+ );
20
+ }
21
+ if (startScale === endScale) {
22
+ return defineEasing(`expoScale(${startScale}, ${endScale})`, () => 0);
23
+ }
24
+ const ratio = endScale / startScale;
25
+ const span = endScale - startScale;
26
+ return defineEasing(`expoScale(${startScale}, ${endScale})`, (t) => {
27
+ const eased = ease(t);
28
+ return (startScale * ratio ** eased - startScale) / span;
29
+ });
30
+ }
@@ -0,0 +1,27 @@
1
+ import { defineParametricEasing } from "../define.js";
2
+
3
+ export type GravityParams = {
4
+ /**
5
+ * How heavy the falling object feels. 0 = floats (linear), 1 ≈ Earth
6
+ * gravity (quadratic fall, equivalent to power1.in), 2 = cubic fall
7
+ * (power2.in), higher = molasses. Continuous, so designers get a
8
+ * single "weight" knob instead of choosing between named power
9
+ * curves.
10
+ */
11
+ weight: number;
12
+ };
13
+
14
+ const DEFAULTS: GravityParams = { weight: 1 };
15
+
16
+ // Math: f(t) = t^(1 + weight). Keeps endpoints exact, monotonic for
17
+ // weight ≥ 0, and continuously interpolates the catalog's power-in
18
+ // family so a designer can dial "heavier" without picking a number
19
+ // from a discrete set.
20
+ export const gravity = defineParametricEasing(
21
+ "gravity",
22
+ DEFAULTS,
23
+ ({ weight }) => {
24
+ const exp = 1 + Math.max(0, weight);
25
+ return (t) => Math.pow(t, exp);
26
+ },
27
+ );
@@ -0,0 +1,24 @@
1
+ export { bezier, bezierEase, bezierEaseIn, bezierEaseOut, bezierEaseInOut } from "./bezier.js";
2
+ export { spring, type SpringParams } from "./spring.js";
3
+ export { custom, type CustomPoint } from "./custom.js";
4
+ export { rough, type RoughParams } from "./rough.js";
5
+ export { wiggle, type WiggleParams, type WiggleType } from "./wiggle.js";
6
+ export { slow, type SlowParams } from "./slow.js";
7
+ export { expoScale } from "./expoScale.js";
8
+ export { gravity, type GravityParams } from "./gravity.js";
9
+ export { breathe, type BreatheParams } from "./breathe.js";
10
+ export {
11
+ anticipate,
12
+ anticipateIn,
13
+ anticipateOut,
14
+ anticipateInOut,
15
+ type AnticipateParams,
16
+ } from "./anticipate.js";
17
+ export {
18
+ SPRING_PRESETS,
19
+ gentleSpring,
20
+ wobblySpring,
21
+ stiffSpring,
22
+ slowSpring,
23
+ molassesSpring,
24
+ } from "./spring-presets.js";
@@ -0,0 +1,83 @@
1
+ import { defineParametricEasing } from "../define.js";
2
+ import { linear } from "../easings/linear.js";
3
+ import type { EasingFn } from "../types.js";
4
+
5
+ export type RoughParams = {
6
+ /** Base ease to perturb. Default linear. */
7
+ template: EasingFn;
8
+ /** Magnitude of jitter. Typical 0.05–0.3. Default 0.15. */
9
+ strength: number;
10
+ /** Number of jitter points along the curve. More = finer noise. Default 20. */
11
+ points: number;
12
+ /** Taper the jitter so it dies toward endpoints. */
13
+ taper: "none" | "in" | "out" | "both";
14
+ /** Randomize x-spacing of jitter points. Default true. */
15
+ randomize: boolean;
16
+ /** Seed for deterministic output. Pass to get the same rough curve every call. */
17
+ seed: number;
18
+ };
19
+
20
+ const DEFAULTS: RoughParams = {
21
+ template: linear,
22
+ strength: 0.15,
23
+ points: 20,
24
+ taper: "both",
25
+ randomize: true,
26
+ seed: 0,
27
+ };
28
+
29
+ function mulberry32(seed: number) {
30
+ let s = seed >>> 0;
31
+ return () => {
32
+ s = (s + 0x6d2b79f5) >>> 0;
33
+ let t = s;
34
+ t = Math.imul(t ^ (t >>> 15), t | 1);
35
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
36
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
37
+ };
38
+ }
39
+
40
+ function taperAmount(taper: RoughParams["taper"], x: number): number {
41
+ switch (taper) {
42
+ case "in":
43
+ return x;
44
+ case "out":
45
+ return 1 - x;
46
+ case "both":
47
+ return 2 * (0.5 - Math.abs(x - 0.5));
48
+ case "none":
49
+ return 1;
50
+ }
51
+ }
52
+
53
+ export const rough = defineParametricEasing(
54
+ "rough",
55
+ DEFAULTS,
56
+ ({ template, strength, points, taper, randomize, seed }) => {
57
+ const seedValue = seed === 0 ? Math.floor(Math.random() * 2 ** 31) : seed;
58
+ const rng = mulberry32(seedValue);
59
+ const n = Math.max(3, Math.floor(points));
60
+ const xs: number[] = [0];
61
+ for (let i = 1; i < n - 1; i++) xs.push(randomize ? rng() : i / (n - 1));
62
+ xs.push(1);
63
+ xs.sort((a, b) => a - b);
64
+ const ys: number[] = xs.map((x) => {
65
+ const base = template(x);
66
+ const jitter = (rng() - 0.5) * 2 * strength * taperAmount(taper, x);
67
+ return base + jitter;
68
+ });
69
+ return (t: number) => {
70
+ for (let i = 1; i < xs.length; i++) {
71
+ const x1 = xs[i]!;
72
+ if (t <= x1) {
73
+ const x0 = xs[i - 1]!;
74
+ const span = x1 - x0;
75
+ if (span === 0) return ys[i]!;
76
+ const u = (t - x0) / span;
77
+ return ys[i - 1]! + (ys[i]! - ys[i - 1]!) * u;
78
+ }
79
+ }
80
+ return ys[ys.length - 1]!;
81
+ };
82
+ },
83
+ );