@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,72 @@
|
|
|
1
|
+
import { defineParametricEasing } from "../define.js";
|
|
2
|
+
|
|
3
|
+
export type SlowParams = {
|
|
4
|
+
/** Fraction of duration spent in the slow linear middle. 0–1. Default 0.7. */
|
|
5
|
+
linearRatio: number;
|
|
6
|
+
/** Strength of the fast edges. Higher = more dramatic edges + slower middle. Default 0.7. */
|
|
7
|
+
power: number;
|
|
8
|
+
/** If true, reverse the second half so the curve returns to start. */
|
|
9
|
+
yoyoMode: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const DEFAULTS: SlowParams = {
|
|
13
|
+
linearRatio: 0.7,
|
|
14
|
+
power: 0.7,
|
|
15
|
+
yoyoMode: false,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* "Slow motion" ease: fast at the start and end, slow (nearly constant)
|
|
20
|
+
* through the middle. Opposite of an inOut shape — the linear middle
|
|
21
|
+
* section lives off the diagonal, so the midpoint value changes slowly
|
|
22
|
+
* with time.
|
|
23
|
+
*/
|
|
24
|
+
export const slow = defineParametricEasing(
|
|
25
|
+
"slow",
|
|
26
|
+
DEFAULTS,
|
|
27
|
+
({ linearRatio, power, yoyoMode }) => {
|
|
28
|
+
const lr = Math.max(0, Math.min(1, linearRatio));
|
|
29
|
+
const p = Math.max(0, power);
|
|
30
|
+
const edge = (1 - lr) / 2;
|
|
31
|
+
// Off-diagonal offset of the linear section. At power=0 the linear
|
|
32
|
+
// section sits ON the diagonal (slow degenerates to linear); at higher
|
|
33
|
+
// powers it collapses toward y=0.5, making the middle genuinely slow.
|
|
34
|
+
const edgeY = (0.5 - edge) / (1 + p * 2);
|
|
35
|
+
const mid1 = 0.5 - edgeY;
|
|
36
|
+
const mid2 = 0.5 + edgeY;
|
|
37
|
+
const middleSpan = mid2 - mid1;
|
|
38
|
+
const middleSlope = lr === 0 ? 0 : middleSpan / lr;
|
|
39
|
+
// Edges blend a power-n ease-out with a linear segment whose slope
|
|
40
|
+
// matches the middle. That way the corner is a smooth curve, not a
|
|
41
|
+
// visible kink. Higher `power` sharpens the edge initial rise.
|
|
42
|
+
const edgeN = 2 + p * 6;
|
|
43
|
+
// Fraction contributed by the linear component of the blend, chosen so
|
|
44
|
+
// that the edge's end-slope equals the middle slope exactly → C¹
|
|
45
|
+
// continuity at the join. (Derived: y'(u=1) in edge = mid1 * (1-a) / edge.)
|
|
46
|
+
const linearShare = edge === 0 || mid1 === 0 ? 0 : Math.min(1, (middleSlope * edge) / mid1);
|
|
47
|
+
const powerShare = 1 - linearShare;
|
|
48
|
+
|
|
49
|
+
return (t: number) => {
|
|
50
|
+
let result: number;
|
|
51
|
+
if (edge === 0) {
|
|
52
|
+
result = t;
|
|
53
|
+
} else if (t < edge) {
|
|
54
|
+
const u = t / edge;
|
|
55
|
+
const v = powerShare * (1 - (1 - u) ** edgeN) + linearShare * u;
|
|
56
|
+
result = mid1 * v;
|
|
57
|
+
} else if (t > 1 - edge) {
|
|
58
|
+
const u = (t - (1 - edge)) / edge;
|
|
59
|
+
const v = powerShare * (u ** edgeN) + linearShare * u;
|
|
60
|
+
result = mid2 + (1 - mid2) * v;
|
|
61
|
+
} else {
|
|
62
|
+
const u = (t - edge) / lr;
|
|
63
|
+
result = mid1 + middleSpan * u;
|
|
64
|
+
}
|
|
65
|
+
if (yoyoMode) {
|
|
66
|
+
return result < 0.5 ? result * 2 : (1 - result) * 2;
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
{ exactEndpoints: false },
|
|
72
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { spring, type SpringParams } from "./spring.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pre-configured spring feels matching React Spring's well-known configs.
|
|
5
|
+
* Import directly when you want a named spring rather than tuning by hand.
|
|
6
|
+
*/
|
|
7
|
+
export const SPRING_PRESETS = {
|
|
8
|
+
gentle: { stiffness: 100, damping: 15, mass: 1, velocity: 0 },
|
|
9
|
+
default: { stiffness: 170, damping: 26, mass: 1, velocity: 0 },
|
|
10
|
+
wobbly: { stiffness: 180, damping: 12, mass: 1, velocity: 0 },
|
|
11
|
+
stiff: { stiffness: 210, damping: 20, mass: 1, velocity: 0 },
|
|
12
|
+
slow: { stiffness: 80, damping: 20, mass: 1, velocity: 0 },
|
|
13
|
+
molasses: { stiffness: 280, damping: 120, mass: 1, velocity: 0 },
|
|
14
|
+
} as const satisfies Record<string, SpringParams>;
|
|
15
|
+
|
|
16
|
+
export const gentleSpring = spring.with(SPRING_PRESETS.gentle);
|
|
17
|
+
export const wobblySpring = spring.with(SPRING_PRESETS.wobbly);
|
|
18
|
+
export const stiffSpring = spring.with(SPRING_PRESETS.stiff);
|
|
19
|
+
export const slowSpring = spring.with(SPRING_PRESETS.slow);
|
|
20
|
+
export const molassesSpring = spring.with(SPRING_PRESETS.molasses);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { defineParametricEasing } from "../define.js";
|
|
2
|
+
|
|
3
|
+
export type SpringParams = {
|
|
4
|
+
/** Spring stiffness (higher = faster, more aggressive). Default 170 matches Framer Motion. */
|
|
5
|
+
stiffness: number;
|
|
6
|
+
/** Damping coefficient (higher = less oscillation). Default 26 is underdamped but close to critical. */
|
|
7
|
+
damping: number;
|
|
8
|
+
/** Object mass (higher = slower, more inertia). Default 1. */
|
|
9
|
+
mass: number;
|
|
10
|
+
/** Initial velocity at t=0. Default 0. */
|
|
11
|
+
velocity: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const DEFAULTS: SpringParams = {
|
|
15
|
+
stiffness: 170,
|
|
16
|
+
damping: 26,
|
|
17
|
+
mass: 1,
|
|
18
|
+
velocity: 0,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const SETTLE_EPSILON = 1e-4;
|
|
22
|
+
const MAX_SETTLE_TIME = 10;
|
|
23
|
+
const SETTLE_SAMPLES = 1000;
|
|
24
|
+
|
|
25
|
+
function makeSpringFunction({ stiffness, damping, mass, velocity }: SpringParams) {
|
|
26
|
+
const w0 = Math.sqrt(stiffness / mass);
|
|
27
|
+
const zeta = damping / (2 * Math.sqrt(stiffness * mass));
|
|
28
|
+
|
|
29
|
+
if (zeta < 1) {
|
|
30
|
+
const wd = w0 * Math.sqrt(1 - zeta * zeta);
|
|
31
|
+
const B = (velocity - zeta * w0 * -1) / wd;
|
|
32
|
+
return (t: number) => 1 + Math.exp(-zeta * w0 * t) * (-Math.cos(wd * t) + B * Math.sin(wd * t));
|
|
33
|
+
}
|
|
34
|
+
if (zeta === 1) {
|
|
35
|
+
const B = velocity - w0;
|
|
36
|
+
return (t: number) => 1 + (-1 + B * t) * Math.exp(-w0 * t);
|
|
37
|
+
}
|
|
38
|
+
const r = w0 * Math.sqrt(zeta * zeta - 1);
|
|
39
|
+
const lPlus = -zeta * w0 + r;
|
|
40
|
+
const lMinus = -zeta * w0 - r;
|
|
41
|
+
const det = lMinus - lPlus;
|
|
42
|
+
const A = (-lMinus - velocity) / det;
|
|
43
|
+
const B = (lPlus + velocity) / det;
|
|
44
|
+
return (t: number) => 1 + A * Math.exp(lPlus * t) + B * Math.exp(lMinus * t);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function findSettleTime(fn: (t: number) => number): number {
|
|
48
|
+
let lastUnsettled = 0;
|
|
49
|
+
const step = MAX_SETTLE_TIME / SETTLE_SAMPLES;
|
|
50
|
+
for (let i = 1; i <= SETTLE_SAMPLES; i++) {
|
|
51
|
+
const t = i * step;
|
|
52
|
+
if (Math.abs(fn(t) - 1) > SETTLE_EPSILON) lastUnsettled = t;
|
|
53
|
+
}
|
|
54
|
+
return Math.max(lastUnsettled + step * 2, step * 10);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const spring = defineParametricEasing("spring", DEFAULTS, (params) => {
|
|
58
|
+
const raw = makeSpringFunction(params);
|
|
59
|
+
const settle = findSettleTime(raw);
|
|
60
|
+
return (t) => raw(t * settle);
|
|
61
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineParametricEasing } from "../define.js";
|
|
2
|
+
|
|
3
|
+
export type WiggleType = "easeOut" | "easeInOut" | "anticipate" | "uniform";
|
|
4
|
+
|
|
5
|
+
export type WiggleParams = {
|
|
6
|
+
/** Number of back-and-forth oscillations across [0, 1]. Default 10. */
|
|
7
|
+
wiggles: number;
|
|
8
|
+
/** Envelope shape. */
|
|
9
|
+
type: WiggleType;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const DEFAULTS: WiggleParams = {
|
|
13
|
+
wiggles: 10,
|
|
14
|
+
type: "easeOut",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const TAU = Math.PI * 2;
|
|
18
|
+
|
|
19
|
+
function envelope(type: WiggleType, t: number): number {
|
|
20
|
+
switch (type) {
|
|
21
|
+
case "easeOut":
|
|
22
|
+
return 1 - t;
|
|
23
|
+
case "easeInOut":
|
|
24
|
+
return 4 * t * (1 - t);
|
|
25
|
+
case "anticipate":
|
|
26
|
+
return t < 0.5 ? 2 * t : 2 - 2 * t;
|
|
27
|
+
case "uniform":
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const wiggle = defineParametricEasing(
|
|
33
|
+
"wiggle",
|
|
34
|
+
DEFAULTS,
|
|
35
|
+
({ wiggles, type }) => {
|
|
36
|
+
const n = Math.max(1, wiggles);
|
|
37
|
+
return (t: number) => envelope(type, t) * Math.sin(t * TAU * n);
|
|
38
|
+
},
|
|
39
|
+
{ exactEndpoints: false },
|
|
40
|
+
);
|
package/src/css.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { EasingFn } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert any easing to a CSS `linear()` timing-function value by sampling
|
|
5
|
+
* the curve at evenly-spaced points.
|
|
6
|
+
*
|
|
7
|
+
* ```css
|
|
8
|
+
* animation-timing-function: linear(0, 0.3, 0.7, 1);
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* CSS interpolates linearly between the sampled points, so higher sample
|
|
12
|
+
* counts produce smoother-looking motion. 40 samples is a good default
|
|
13
|
+
* balance between fidelity and string length. Requires browsers that
|
|
14
|
+
* support CSS Easing Functions Level 2 (~all majors since 2024).
|
|
15
|
+
*
|
|
16
|
+
* @param ease An EasingFn or plain (t: number) => number
|
|
17
|
+
* @param samples Number of samples + 1 across [0, 1]. Default 40.
|
|
18
|
+
*/
|
|
19
|
+
export function toCSSLinear(ease: EasingFn | ((t: number) => number), samples = 40): string {
|
|
20
|
+
if (samples < 1 || !Number.isInteger(samples)) {
|
|
21
|
+
throw new RangeError(`toCSSLinear: samples must be a positive integer; got ${samples}`);
|
|
22
|
+
}
|
|
23
|
+
const values: string[] = [];
|
|
24
|
+
for (let i = 0; i <= samples; i++) {
|
|
25
|
+
const t = i / samples;
|
|
26
|
+
values.push(formatNumber(ease(t)));
|
|
27
|
+
}
|
|
28
|
+
return `linear(${values.join(", ")})`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Emit a CSS cubic-bezier() timing function. Exact representation; no
|
|
33
|
+
* sampling. Use this when you'd otherwise build an easing via `bezier()`
|
|
34
|
+
* from this package.
|
|
35
|
+
*
|
|
36
|
+
* ```css
|
|
37
|
+
* animation-timing-function: cubic-bezier(0.42, 0, 0.58, 1);
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function toCSSBezier(p1x: number, p1y: number, p2x: number, p2y: number): string {
|
|
41
|
+
return `cubic-bezier(${formatNumber(p1x)}, ${formatNumber(p1y)}, ${formatNumber(p2x)}, ${formatNumber(p2y)})`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Emit a `@keyframes` block driving a numeric CSS property (e.g. translate,
|
|
46
|
+
* opacity) through an ease. The animation should be declared with
|
|
47
|
+
* `animation-timing-function: linear` because the easing is pre-baked into
|
|
48
|
+
* the keyframes.
|
|
49
|
+
*
|
|
50
|
+
* ```css
|
|
51
|
+
* ${toCSSKeyframes("slideIn", "transform", (v) => `translateX(${v * 100}%)`, ease)}
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function toCSSKeyframes(
|
|
55
|
+
name: string,
|
|
56
|
+
property: string,
|
|
57
|
+
valueForProgress: (easedValue: number) => string,
|
|
58
|
+
ease: EasingFn | ((t: number) => number),
|
|
59
|
+
samples = 20,
|
|
60
|
+
): string {
|
|
61
|
+
if (samples < 2) {
|
|
62
|
+
throw new RangeError(`toCSSKeyframes: samples must be >= 2; got ${samples}`);
|
|
63
|
+
}
|
|
64
|
+
const lines: string[] = [`@keyframes ${name} {`];
|
|
65
|
+
for (let i = 0; i <= samples; i++) {
|
|
66
|
+
const t = i / samples;
|
|
67
|
+
const pct = (t * 100).toFixed(2).replace(/\.?0+$/, "");
|
|
68
|
+
const value = valueForProgress(ease(t));
|
|
69
|
+
lines.push(` ${pct}% { ${property}: ${value}; }`);
|
|
70
|
+
}
|
|
71
|
+
lines.push("}");
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatNumber(n: number): string {
|
|
76
|
+
if (!Number.isFinite(n)) return "0";
|
|
77
|
+
if (Number.isInteger(n)) return n.toString();
|
|
78
|
+
return Number(n.toFixed(5)).toString();
|
|
79
|
+
}
|
package/src/define.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { EasingFn, ParametricEasing } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export type DefineEasingOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* When true (default), t ≤ 0 returns 0 and t ≥ 1 returns 1 regardless of
|
|
6
|
+
* the underlying formula. Disable for easings that intentionally don't
|
|
7
|
+
* hit (0, 0) or (1, 1) — notably `steps(n, "start")`.
|
|
8
|
+
*/
|
|
9
|
+
exactEndpoints?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function defineEasing(
|
|
13
|
+
name: string,
|
|
14
|
+
fn: (t: number) => number,
|
|
15
|
+
options: DefineEasingOptions = {},
|
|
16
|
+
): EasingFn {
|
|
17
|
+
const { exactEndpoints = true } = options;
|
|
18
|
+
const wrapped = ((t: number) => {
|
|
19
|
+
// Defensive: NaN and non-finite inputs collapse to 0. Better than
|
|
20
|
+
// propagating NaN through a whole animation pipeline.
|
|
21
|
+
if (!Number.isFinite(t)) return 0;
|
|
22
|
+
if (exactEndpoints) {
|
|
23
|
+
if (t <= 0) return 0;
|
|
24
|
+
if (t >= 1) return 1;
|
|
25
|
+
}
|
|
26
|
+
return fn(t);
|
|
27
|
+
}) as EasingFn;
|
|
28
|
+
Object.defineProperty(wrapped, "easingName", { value: name, enumerable: true });
|
|
29
|
+
return wrapped;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function defineParametricEasing<P extends object>(
|
|
33
|
+
name: string,
|
|
34
|
+
defaults: P,
|
|
35
|
+
build: (params: P) => (t: number) => number,
|
|
36
|
+
options: DefineEasingOptions = {},
|
|
37
|
+
): ParametricEasing<P> {
|
|
38
|
+
const base = defineEasing(name, build(defaults), options) as ParametricEasing<P>;
|
|
39
|
+
Object.defineProperty(base, "defaults", {
|
|
40
|
+
value: Object.freeze({ ...defaults }),
|
|
41
|
+
enumerable: true,
|
|
42
|
+
});
|
|
43
|
+
Object.defineProperty(base, "with", {
|
|
44
|
+
value: (params: Partial<P>): EasingFn =>
|
|
45
|
+
defineEasing(name, build({ ...defaults, ...params }), options),
|
|
46
|
+
enumerable: true,
|
|
47
|
+
});
|
|
48
|
+
return base;
|
|
49
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineParametricEasing } from "../define.js";
|
|
2
|
+
|
|
3
|
+
type BackParams = { overshoot: number };
|
|
4
|
+
|
|
5
|
+
const DEFAULTS: BackParams = { overshoot: 1.70158 };
|
|
6
|
+
|
|
7
|
+
export const backIn = defineParametricEasing("back.in", DEFAULTS, ({ overshoot: s }) => (t) => {
|
|
8
|
+
return t * t * ((s + 1) * t - s);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const backOut = defineParametricEasing("back.out", DEFAULTS, ({ overshoot: s }) => (t) => {
|
|
12
|
+
const u = t - 1;
|
|
13
|
+
return u * u * ((s + 1) * u + s) + 1;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const backInOut = defineParametricEasing("back.inOut", DEFAULTS, ({ overshoot }) => {
|
|
17
|
+
const s = overshoot * 1.525;
|
|
18
|
+
return (t) => {
|
|
19
|
+
const u = t * 2;
|
|
20
|
+
if (u < 1) return 0.5 * (u * u * ((s + 1) * u - s));
|
|
21
|
+
const v = u - 2;
|
|
22
|
+
return 0.5 * (v * v * ((s + 1) * v + s) + 2);
|
|
23
|
+
};
|
|
24
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineEasing } from "../define.js";
|
|
2
|
+
|
|
3
|
+
function bounceOutImpl(t: number): number {
|
|
4
|
+
const n = 7.5625;
|
|
5
|
+
const d = 2.75;
|
|
6
|
+
if (t < 1 / d) return n * t * t;
|
|
7
|
+
if (t < 2 / d) {
|
|
8
|
+
const u = t - 1.5 / d;
|
|
9
|
+
return n * u * u + 0.75;
|
|
10
|
+
}
|
|
11
|
+
if (t < 2.5 / d) {
|
|
12
|
+
const u = t - 2.25 / d;
|
|
13
|
+
return n * u * u + 0.9375;
|
|
14
|
+
}
|
|
15
|
+
const u = t - 2.625 / d;
|
|
16
|
+
return n * u * u + 0.984375;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const bounceOut = defineEasing("bounce.out", bounceOutImpl);
|
|
20
|
+
export const bounceIn = defineEasing("bounce.in", (t) => 1 - bounceOutImpl(1 - t));
|
|
21
|
+
export const bounceInOut = defineEasing("bounce.inOut", (t) =>
|
|
22
|
+
t < 0.5 ? (1 - bounceOutImpl(1 - 2 * t)) / 2 : (1 + bounceOutImpl(2 * t - 1)) / 2,
|
|
23
|
+
);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineEasing } from "../define.js";
|
|
2
|
+
|
|
3
|
+
export const circIn = defineEasing("circ.in", (t) => 1 - Math.sqrt(1 - t * t));
|
|
4
|
+
export const circOut = defineEasing("circ.out", (t) => Math.sqrt(1 - (t - 1) * (t - 1)));
|
|
5
|
+
export const circInOut = defineEasing("circ.inOut", (t) =>
|
|
6
|
+
t < 0.5
|
|
7
|
+
? (1 - Math.sqrt(1 - (2 * t) ** 2)) / 2
|
|
8
|
+
: (Math.sqrt(1 - (-2 * t + 2) ** 2) + 1) / 2,
|
|
9
|
+
);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineParametricEasing } from "../define.js";
|
|
2
|
+
|
|
3
|
+
type ElasticParams = { amplitude: number; period: number };
|
|
4
|
+
|
|
5
|
+
const DEFAULTS: ElasticParams = { amplitude: 1, period: 0.3 };
|
|
6
|
+
const TAU = Math.PI * 2;
|
|
7
|
+
|
|
8
|
+
function resolveAmplitude(a: number): number {
|
|
9
|
+
return a < 1 ? 1 : a;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function resolvePhase(a: number, p: number): number {
|
|
13
|
+
return a < 1 ? p / 4 : (p / TAU) * Math.asin(1 / a);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const elasticIn = defineParametricEasing(
|
|
17
|
+
"elastic.in",
|
|
18
|
+
DEFAULTS,
|
|
19
|
+
({ amplitude, period }) => {
|
|
20
|
+
const a = resolveAmplitude(amplitude);
|
|
21
|
+
const s = resolvePhase(amplitude, period);
|
|
22
|
+
return (t) => -(a * 2 ** (10 * (t - 1)) * Math.sin(((t - 1 - s) * TAU) / period));
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export const elasticOut = defineParametricEasing(
|
|
27
|
+
"elastic.out",
|
|
28
|
+
DEFAULTS,
|
|
29
|
+
({ amplitude, period }) => {
|
|
30
|
+
const a = resolveAmplitude(amplitude);
|
|
31
|
+
const s = resolvePhase(amplitude, period);
|
|
32
|
+
return (t) => a * 2 ** (-10 * t) * Math.sin(((t - s) * TAU) / period) + 1;
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export const elasticInOut = defineParametricEasing(
|
|
37
|
+
"elastic.inOut",
|
|
38
|
+
DEFAULTS,
|
|
39
|
+
({ amplitude, period }) => {
|
|
40
|
+
const a = resolveAmplitude(amplitude);
|
|
41
|
+
const p = period * 1.5;
|
|
42
|
+
const s = resolvePhase(amplitude, p);
|
|
43
|
+
return (t) => {
|
|
44
|
+
const u = t * 2;
|
|
45
|
+
if (u < 1) {
|
|
46
|
+
return -0.5 * (a * 2 ** (10 * (u - 1)) * Math.sin(((u - 1 - s) * TAU) / p));
|
|
47
|
+
}
|
|
48
|
+
const v = u - 1;
|
|
49
|
+
return 0.5 * a * 2 ** (-10 * v) * Math.sin(((v - s) * TAU) / p) + 1;
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineEasing } from "../define.js";
|
|
2
|
+
|
|
3
|
+
export const expoIn = defineEasing("expo.in", (t) => (t === 0 ? 0 : 2 ** (10 * t - 10)));
|
|
4
|
+
export const expoOut = defineEasing("expo.out", (t) => (t === 1 ? 1 : 1 - 2 ** (-10 * t)));
|
|
5
|
+
export const expoInOut = defineEasing("expo.inOut", (t) => {
|
|
6
|
+
if (t === 0) return 0;
|
|
7
|
+
if (t === 1) return 1;
|
|
8
|
+
return t < 0.5 ? 2 ** (20 * t - 10) / 2 : (2 - 2 ** (-20 * t + 10)) / 2;
|
|
9
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export { linear, none } from "./linear.js";
|
|
2
|
+
export {
|
|
3
|
+
power1In,
|
|
4
|
+
power1Out,
|
|
5
|
+
power1InOut,
|
|
6
|
+
power2In,
|
|
7
|
+
power2Out,
|
|
8
|
+
power2InOut,
|
|
9
|
+
power3In,
|
|
10
|
+
power3Out,
|
|
11
|
+
power3InOut,
|
|
12
|
+
power4In,
|
|
13
|
+
power4Out,
|
|
14
|
+
power4InOut,
|
|
15
|
+
quadIn,
|
|
16
|
+
quadOut,
|
|
17
|
+
quadInOut,
|
|
18
|
+
cubicIn,
|
|
19
|
+
cubicOut,
|
|
20
|
+
cubicInOut,
|
|
21
|
+
quartIn,
|
|
22
|
+
quartOut,
|
|
23
|
+
quartInOut,
|
|
24
|
+
quintIn,
|
|
25
|
+
quintOut,
|
|
26
|
+
quintInOut,
|
|
27
|
+
} from "./power.js";
|
|
28
|
+
export { sineIn, sineOut, sineInOut } from "./sine.js";
|
|
29
|
+
export { circIn, circOut, circInOut } from "./circ.js";
|
|
30
|
+
export { expoIn, expoOut, expoInOut } from "./expo.js";
|
|
31
|
+
export { smoothIn, smoothOut, smoothInOut } from "./smooth.js";
|
|
32
|
+
export { backIn, backOut, backInOut } from "./back.js";
|
|
33
|
+
export { elasticIn, elasticOut, elasticInOut } from "./elastic.js";
|
|
34
|
+
export { bounceIn, bounceOut, bounceInOut } from "./bounce.js";
|
|
35
|
+
export { steps, type StepPosition } from "./steps.js";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { defineEasing } from "../define.js";
|
|
2
|
+
|
|
3
|
+
const makeIn = (n: number) => (t: number) => t ** n;
|
|
4
|
+
const makeOut = (n: number) => (t: number) => 1 - (1 - t) ** n;
|
|
5
|
+
const makeInOut = (n: number) => (t: number) =>
|
|
6
|
+
t < 0.5 ? 2 ** (n - 1) * t ** n : 1 - (-2 * t + 2) ** n / 2;
|
|
7
|
+
|
|
8
|
+
export const power1In = defineEasing("power1.in", makeIn(2));
|
|
9
|
+
export const power1Out = defineEasing("power1.out", makeOut(2));
|
|
10
|
+
export const power1InOut = defineEasing("power1.inOut", makeInOut(2));
|
|
11
|
+
|
|
12
|
+
export const power2In = defineEasing("power2.in", makeIn(3));
|
|
13
|
+
export const power2Out = defineEasing("power2.out", makeOut(3));
|
|
14
|
+
export const power2InOut = defineEasing("power2.inOut", makeInOut(3));
|
|
15
|
+
|
|
16
|
+
export const power3In = defineEasing("power3.in", makeIn(4));
|
|
17
|
+
export const power3Out = defineEasing("power3.out", makeOut(4));
|
|
18
|
+
export const power3InOut = defineEasing("power3.inOut", makeInOut(4));
|
|
19
|
+
|
|
20
|
+
export const power4In = defineEasing("power4.in", makeIn(5));
|
|
21
|
+
export const power4Out = defineEasing("power4.out", makeOut(5));
|
|
22
|
+
export const power4InOut = defineEasing("power4.inOut", makeInOut(5));
|
|
23
|
+
|
|
24
|
+
export const quadIn = power1In;
|
|
25
|
+
export const quadOut = power1Out;
|
|
26
|
+
export const quadInOut = power1InOut;
|
|
27
|
+
|
|
28
|
+
export const cubicIn = power2In;
|
|
29
|
+
export const cubicOut = power2Out;
|
|
30
|
+
export const cubicInOut = power2InOut;
|
|
31
|
+
|
|
32
|
+
export const quartIn = power3In;
|
|
33
|
+
export const quartOut = power3Out;
|
|
34
|
+
export const quartInOut = power3InOut;
|
|
35
|
+
|
|
36
|
+
export const quintIn = power4In;
|
|
37
|
+
export const quintOut = power4Out;
|
|
38
|
+
export const quintInOut = power4InOut;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineEasing } from "../define.js";
|
|
2
|
+
|
|
3
|
+
const HALF_PI = Math.PI / 2;
|
|
4
|
+
|
|
5
|
+
export const sineIn = defineEasing("sine.in", (t) => 1 - Math.cos(t * HALF_PI));
|
|
6
|
+
export const sineOut = defineEasing("sine.out", (t) => Math.sin(t * HALF_PI));
|
|
7
|
+
export const sineInOut = defineEasing("sine.inOut", (t) => -(Math.cos(Math.PI * t) - 1) / 2);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineEasing } from "../define.js";
|
|
2
|
+
|
|
3
|
+
// Hermite smoothstep. C¹-continuous at both endpoints — zero velocity
|
|
4
|
+
// at t=0 AND t=1. The canonical inOut shape from GLSL `smoothstep`,
|
|
5
|
+
// distinct from `power2.inOut` whose first derivative is non-zero at
|
|
6
|
+
// the endpoints. The in/out variants are derived by rescaling halves
|
|
7
|
+
// of the smoothstep curve, so they preserve Hermite smoothness on the
|
|
8
|
+
// "interior" endpoint (t=0 for in, t=1 for out).
|
|
9
|
+
|
|
10
|
+
const smoothstep = (t: number) => t * t * (3 - 2 * t);
|
|
11
|
+
|
|
12
|
+
export const smoothInOut = defineEasing("smooth.inOut", smoothstep);
|
|
13
|
+
|
|
14
|
+
// 2·smoothstep(t/2): first half of the inOut curve, scaled to [0, 1].
|
|
15
|
+
// f(0)=0, f(1)=1, f'(0)=0, f'(1)=1.5 — slow start, accelerates out.
|
|
16
|
+
export const smoothIn = defineEasing("smooth.in", (t) => {
|
|
17
|
+
const u = t / 2;
|
|
18
|
+
return 2 * smoothstep(u);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Mirror of smoothIn. Fast start, decelerates to zero velocity at t=1.
|
|
22
|
+
export const smoothOut = defineEasing("smooth.out", (t) => {
|
|
23
|
+
const u = (1 - t) / 2;
|
|
24
|
+
return 1 - 2 * smoothstep(u);
|
|
25
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineParametricEasing } from "../define.js";
|
|
2
|
+
|
|
3
|
+
export type StepPosition = "start" | "end" | "none";
|
|
4
|
+
|
|
5
|
+
type StepsParams = { count: number; position: StepPosition };
|
|
6
|
+
|
|
7
|
+
const DEFAULTS: StepsParams = { count: 5, position: "end" };
|
|
8
|
+
|
|
9
|
+
export const steps = defineParametricEasing(
|
|
10
|
+
"steps",
|
|
11
|
+
DEFAULTS,
|
|
12
|
+
({ count, position }) => {
|
|
13
|
+
const n = Math.max(1, Math.floor(count));
|
|
14
|
+
switch (position) {
|
|
15
|
+
case "end":
|
|
16
|
+
return (t) => Math.min(Math.floor(t * n), n) / n;
|
|
17
|
+
case "start":
|
|
18
|
+
return (t) => Math.min(Math.floor(t * n) + 1, n) / n;
|
|
19
|
+
case "none":
|
|
20
|
+
if (n === 1) return () => 0;
|
|
21
|
+
return (t) => Math.min(Math.floor(t * n), n - 1) / (n - 1);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{ exactEndpoints: false },
|
|
25
|
+
);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type { EasingFn, ParametricEasing } from "./types.js";
|
|
2
|
+
export { defineEasing, defineParametricEasing } from "./define.js";
|
|
3
|
+
export type { DefineEasingOptions } from "./define.js";
|
|
4
|
+
export * from "./easings/index.js";
|
|
5
|
+
export * from "./builders/index.js";
|
|
6
|
+
export * from "./modifiers/index.js";
|
|
7
|
+
export { toCSSLinear, toCSSBezier, toCSSKeyframes } from "./css.js";
|
|
8
|
+
export { parseEasing } from "./parse.js";
|
|
9
|
+
export { prefersReducedMotion, respectReducedMotion } from "./reduced-motion.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineEasing } from "../define.js";
|
|
2
|
+
import type { EasingFn } from "../types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Linearly interpolate between two eases. At weight=0 returns pure `a`;
|
|
6
|
+
* at weight=1 returns pure `b`; between is the weighted sum. Useful for
|
|
7
|
+
* morphing between easings or dialling in a custom shape from known eases.
|
|
8
|
+
*/
|
|
9
|
+
export function blend(a: EasingFn, b: EasingFn, weight: number): EasingFn {
|
|
10
|
+
if (weight <= 0) return a;
|
|
11
|
+
if (weight >= 1) return b;
|
|
12
|
+
return defineEasing(
|
|
13
|
+
`blend(${a.easingName}, ${b.easingName}, ${weight})`,
|
|
14
|
+
(t) => a(t) * (1 - weight) + b(t) * weight,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Compose two eases: apply `a` first, then `b` to `a`'s output. The result
|
|
20
|
+
* is `b(a(t))`. Useful for doubly-applied curves ("ease then ease again").
|
|
21
|
+
*/
|
|
22
|
+
export function compose(a: EasingFn, b: EasingFn): EasingFn {
|
|
23
|
+
return defineEasing(`compose(${b.easingName}, ${a.easingName})`, (t) => b(a(t)));
|
|
24
|
+
}
|