@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,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
|
+
);
|