@flighthq/easing 0.3.0-next.906.07cea63 → 0.3.1-next.1041.35b950c
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/dist/contract.d.ts +2 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +2 -0
- package/dist/contract.js.map +1 -1
- package/dist/easeDampedSine.d.ts +34 -0
- package/dist/easeDampedSine.d.ts.map +1 -0
- package/dist/easeDampedSine.js +81 -0
- package/dist/easeDampedSine.js.map +1 -0
- package/dist/easeSteps.d.ts +2 -1
- package/dist/easeSteps.d.ts.map +1 -1
- package/dist/easeSteps.js +15 -4
- package/dist/easeSteps.js.map +1 -1
- package/dist/enableEasingGuards.d.ts +3 -0
- package/dist/enableEasingGuards.d.ts.map +1 -0
- package/dist/enableEasingGuards.js +25 -0
- package/dist/enableEasingGuards.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
- package/src/easeDampedSine.test.ts +100 -0
- package/src/easeSteps.test.ts +33 -1
- package/src/easingFamilies.test.ts +276 -0
- package/src/enableEasingGuards.test.ts +70 -0
package/dist/contract.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export * from './createEasingSamples';
|
|
2
2
|
export * from './easeBack';
|
|
3
|
+
export * from './enableEasingGuards';
|
|
3
4
|
export * from './easeBounce';
|
|
4
5
|
export * from './easeCircular';
|
|
5
6
|
export * from './easeCombinators';
|
|
6
7
|
export * from './easeCubic';
|
|
7
8
|
export * from './easeCubicBezier';
|
|
9
|
+
export * from './easeDampedSine';
|
|
8
10
|
export * from './easeElastic';
|
|
9
11
|
export * from './easeExponential';
|
|
10
12
|
export * from './easeLinear';
|
package/dist/contract.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,YAAY,CAAC;AAC3B,cAAc,sBAAsB,CAAC;AACrC,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,uBAAuB,CAAC"}
|
package/dist/contract.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export * from './createEasingSamples';
|
|
2
2
|
export * from './easeBack';
|
|
3
|
+
export * from './enableEasingGuards';
|
|
3
4
|
export * from './easeBounce';
|
|
4
5
|
export * from './easeCircular';
|
|
5
6
|
export * from './easeCombinators';
|
|
6
7
|
export * from './easeCubic';
|
|
7
8
|
export * from './easeCubicBezier';
|
|
9
|
+
export * from './easeDampedSine';
|
|
8
10
|
export * from './easeElastic';
|
|
9
11
|
export * from './easeExponential';
|
|
10
12
|
export * from './easeLinear';
|
package/dist/contract.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,YAAY,CAAC;AAC3B,cAAc,sBAAsB,CAAC;AACrC,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { EasingFunction } from '@flighthq/types/contract';
|
|
2
|
+
/**
|
|
3
|
+
* The parameterized form of `easeInElastic`: a decaying sine that winds up before departing, with the
|
|
4
|
+
* overshoot and wavelength stated rather than fixed.
|
|
5
|
+
*/
|
|
6
|
+
export declare function easeInDampedSine(amplitude: number, period: number): EasingFunction;
|
|
7
|
+
/**
|
|
8
|
+
* The parameterized form of `easeInOutElastic`: a decaying sine that winds up, crosses, and settles,
|
|
9
|
+
* with the overshoot and wavelength stated rather than fixed.
|
|
10
|
+
*/
|
|
11
|
+
export declare function easeInOutDampedSine(amplitude: number, period: number): EasingFunction;
|
|
12
|
+
/**
|
|
13
|
+
* The parameterized form of `easeOutElastic`: a decaying sine that overshoots the target and settles
|
|
14
|
+
* back onto it.
|
|
15
|
+
*
|
|
16
|
+
* `easeInElastic` / `easeOutElastic` / `easeInOutElastic` are this same published curve with the two
|
|
17
|
+
* parameters fixed. They stay the right choice when those constants suit; this family exists for a
|
|
18
|
+
* caller that states its own — an importer whose source file names an amplitude and a period, where
|
|
19
|
+
* substituting the fixed curve would be confidently wrong rather than approximately right.
|
|
20
|
+
*
|
|
21
|
+
* **Both degenerate cases are Flight's own choices, not anyone else's**, and both are stated here
|
|
22
|
+
* because they are the only places this can differ from another implementation of the same curve:
|
|
23
|
+
*
|
|
24
|
+
* - **An amplitude below 1 is raised to 1.** The phase term is `asin(1 / amplitude)`, which has no real
|
|
25
|
+
* solution below unit amplitude, so the published curve is simply not defined there. Rather than
|
|
26
|
+
* invent a shape for it we take the smallest curve the form does define, which also keeps the
|
|
27
|
+
* endpoints exact. An elastic with less than unit overshoot is a contradiction in terms.
|
|
28
|
+
* - **A non-positive period falls back to 0.4.** It divides the wave, so zero has no meaning. 0.4 is the
|
|
29
|
+
* constant our own `easeOutElastic` already uses, so a caller who states nothing gets the curve this
|
|
30
|
+
* package's preset would have given — the least surprising answer available, and one that keeps the
|
|
31
|
+
* two families consistent with each other.
|
|
32
|
+
*/
|
|
33
|
+
export declare function easeOutDampedSine(amplitude: number, period: number): EasingFunction;
|
|
34
|
+
//# sourceMappingURL=easeDampedSine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"easeDampedSine.d.ts","sourceRoot":"","sources":["../src/easeDampedSine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE/D;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,CASlF;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,CAYrF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,CAQnF"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parameterized form of `easeInElastic`: a decaying sine that winds up before departing, with the
|
|
3
|
+
* overshoot and wavelength stated rather than fixed.
|
|
4
|
+
*/
|
|
5
|
+
export function easeInDampedSine(amplitude, period) {
|
|
6
|
+
const wavelength = toDampedSineWavelength(period);
|
|
7
|
+
const overshoot = toDampedSineOvershoot(amplitude);
|
|
8
|
+
const phase = toDampedSinePhase(overshoot, wavelength);
|
|
9
|
+
return (t) => {
|
|
10
|
+
if (t === 0 || t === 1)
|
|
11
|
+
return t;
|
|
12
|
+
const time = t - 1;
|
|
13
|
+
return -(overshoot * Math.pow(2, 10 * time) * Math.sin(((time - phase) * TAU) / wavelength));
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The parameterized form of `easeInOutElastic`: a decaying sine that winds up, crosses, and settles,
|
|
18
|
+
* with the overshoot and wavelength stated rather than fixed.
|
|
19
|
+
*/
|
|
20
|
+
export function easeInOutDampedSine(amplitude, period) {
|
|
21
|
+
const wavelength = toDampedSineWavelength(period);
|
|
22
|
+
const overshoot = toDampedSineOvershoot(amplitude);
|
|
23
|
+
const phase = toDampedSinePhase(overshoot, wavelength);
|
|
24
|
+
return (t) => {
|
|
25
|
+
if (t === 0 || t === 1)
|
|
26
|
+
return t;
|
|
27
|
+
const time = t * 2 - 1;
|
|
28
|
+
if (time < 0) {
|
|
29
|
+
return -0.5 * overshoot * Math.pow(2, 10 * time) * Math.sin(((time - phase) * TAU) / wavelength);
|
|
30
|
+
}
|
|
31
|
+
return 0.5 * overshoot * Math.pow(2, -10 * time) * Math.sin(((time - phase) * TAU) / wavelength) + 1;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The parameterized form of `easeOutElastic`: a decaying sine that overshoots the target and settles
|
|
36
|
+
* back onto it.
|
|
37
|
+
*
|
|
38
|
+
* `easeInElastic` / `easeOutElastic` / `easeInOutElastic` are this same published curve with the two
|
|
39
|
+
* parameters fixed. They stay the right choice when those constants suit; this family exists for a
|
|
40
|
+
* caller that states its own — an importer whose source file names an amplitude and a period, where
|
|
41
|
+
* substituting the fixed curve would be confidently wrong rather than approximately right.
|
|
42
|
+
*
|
|
43
|
+
* **Both degenerate cases are Flight's own choices, not anyone else's**, and both are stated here
|
|
44
|
+
* because they are the only places this can differ from another implementation of the same curve:
|
|
45
|
+
*
|
|
46
|
+
* - **An amplitude below 1 is raised to 1.** The phase term is `asin(1 / amplitude)`, which has no real
|
|
47
|
+
* solution below unit amplitude, so the published curve is simply not defined there. Rather than
|
|
48
|
+
* invent a shape for it we take the smallest curve the form does define, which also keeps the
|
|
49
|
+
* endpoints exact. An elastic with less than unit overshoot is a contradiction in terms.
|
|
50
|
+
* - **A non-positive period falls back to 0.4.** It divides the wave, so zero has no meaning. 0.4 is the
|
|
51
|
+
* constant our own `easeOutElastic` already uses, so a caller who states nothing gets the curve this
|
|
52
|
+
* package's preset would have given — the least surprising answer available, and one that keeps the
|
|
53
|
+
* two families consistent with each other.
|
|
54
|
+
*/
|
|
55
|
+
export function easeOutDampedSine(amplitude, period) {
|
|
56
|
+
const wavelength = toDampedSineWavelength(period);
|
|
57
|
+
const overshoot = toDampedSineOvershoot(amplitude);
|
|
58
|
+
const phase = toDampedSinePhase(overshoot, wavelength);
|
|
59
|
+
return (t) => {
|
|
60
|
+
if (t === 0 || t === 1)
|
|
61
|
+
return t;
|
|
62
|
+
return overshoot * Math.pow(2, -10 * t) * Math.sin(((t - phase) * TAU) / wavelength) + 1;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Where the decaying sine crosses its endpoint, which is what makes t=0 land exactly on 0.
|
|
66
|
+
function toDampedSinePhase(overshoot, wavelength) {
|
|
67
|
+
return (wavelength / TAU) * Math.asin(1 / overshoot);
|
|
68
|
+
}
|
|
69
|
+
// Below unit overshoot the phase term has no real solution, so the curve is undefined rather than
|
|
70
|
+
// merely small. Clamping is Flight's choice: the smallest defined curve beats an invented one.
|
|
71
|
+
function toDampedSineOvershoot(amplitude) {
|
|
72
|
+
return amplitude < 1 ? 1 : amplitude;
|
|
73
|
+
}
|
|
74
|
+
// A period divides the wave, so a non-positive one is meaningless. Flight's choice is the constant its
|
|
75
|
+
// own fixed elastic already uses, so the two families agree with each other.
|
|
76
|
+
function toDampedSineWavelength(period) {
|
|
77
|
+
return period > 0 ? period : DEFAULT_DAMPED_SINE_PERIOD;
|
|
78
|
+
}
|
|
79
|
+
const TAU = 2 * Math.PI;
|
|
80
|
+
const DEFAULT_DAMPED_SINE_PERIOD = 0.4;
|
|
81
|
+
//# sourceMappingURL=easeDampedSine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"easeDampedSine.js","sourceRoot":"","sources":["../src/easeDampedSine.ts"],"names":[],"mappings":"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"}
|
package/dist/easeSteps.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type { EasingFunction, StepPosition } from '@flighthq/types/contract';
|
|
1
|
+
import type { EasingFunction, EasingStepsGuard, StepPosition } from '@flighthq/types/contract';
|
|
2
2
|
export declare function easeSteps(count: number, position?: StepPosition): EasingFunction;
|
|
3
|
+
export declare function setEasingStepsGuard(guard: EasingStepsGuard | null): void;
|
|
3
4
|
//# sourceMappingURL=easeSteps.d.ts.map
|
package/dist/easeSteps.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"easeSteps.d.ts","sourceRoot":"","sources":["../src/easeSteps.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"easeSteps.d.ts","sourceRoot":"","sources":["../src/easeSteps.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAe/F,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,GAAE,YAAwB,GAAG,cAAc,CAe3F;AAMD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,GAAG,IAAI,CAExE"}
|
package/dist/easeSteps.js
CHANGED
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
// jumpBoth - jump at both edges; count+1 levels from 0 to 1 inclusive.
|
|
7
7
|
// Follows the CSS Easing Level 1 step algorithm.
|
|
8
8
|
//
|
|
9
|
-
// Sharp edge: easeSteps(1, 'jumpNone') has jumps = count - 1 = 0, so the returned
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
9
|
+
// Sharp edge: easeSteps(1, 'jumpNone') has jumps = count - 1 = 0, so the returned function divides by
|
|
10
|
+
// zero and yields NaN for every input. This mirrors the CSS spec, which forbids steps(1, jump-none) for
|
|
11
|
+
// the same reason; `count >= 2` is required with 'jumpNone', while every other position is well-defined
|
|
12
|
+
// for count >= 1. The failure is SILENT — NaN propagates into whatever the easing drives — so the
|
|
13
|
+
// degenerate call routes through the guard seam below, which enableEasingGuards turns into a warning.
|
|
13
14
|
export function easeSteps(count, position = 'jumpEnd') {
|
|
15
|
+
if (position === 'jumpNone' && count < 2)
|
|
16
|
+
_stepsGuard?.(count, position);
|
|
14
17
|
const jumps = position === 'jumpNone' ? count - 1 : position === 'jumpBoth' ? count + 1 : count;
|
|
15
18
|
const startOffset = position === 'jumpStart' || position === 'jumpBoth' ? 1 : 0;
|
|
16
19
|
return (t) => {
|
|
@@ -24,4 +27,12 @@ export function easeSteps(count, position = 'jumpEnd') {
|
|
|
24
27
|
return step / jumps;
|
|
25
28
|
};
|
|
26
29
|
}
|
|
30
|
+
// The diagnostics seam for the degenerate `easeSteps` call, not the caller-facing entry point — use
|
|
31
|
+
// `enableEasingGuards`, which installs the @flighthq/log reporter through here. Null uninstalls it, and a
|
|
32
|
+
// null slot is what production sees: the check above is one comparison and the message text plus the
|
|
33
|
+
// @flighthq/log dependency live only in the separately-imported guard module.
|
|
34
|
+
export function setEasingStepsGuard(guard) {
|
|
35
|
+
_stepsGuard = guard;
|
|
36
|
+
}
|
|
37
|
+
let _stepsGuard = null;
|
|
27
38
|
//# sourceMappingURL=easeSteps.js.map
|
package/dist/easeSteps.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"easeSteps.js","sourceRoot":"","sources":["../src/easeSteps.ts"],"names":[],"mappings":"AAEA,4EAA4E;AAC5E,gEAAgE;AAChE,yEAAyE;AACzE,+EAA+E;AAC/E,uFAAuF;AACvF,0EAA0E;AAC1E,iDAAiD;AACjD,EAAE;AACF,
|
|
1
|
+
{"version":3,"file":"easeSteps.js","sourceRoot":"","sources":["../src/easeSteps.ts"],"names":[],"mappings":"AAEA,4EAA4E;AAC5E,gEAAgE;AAChE,yEAAyE;AACzE,+EAA+E;AAC/E,uFAAuF;AACvF,0EAA0E;AAC1E,iDAAiD;AACjD,EAAE;AACF,sGAAsG;AACtG,wGAAwG;AACxG,wGAAwG;AACxG,kGAAkG;AAClG,sGAAsG;AACtG,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,WAAyB,SAAS;IACzE,IAAI,QAAQ,KAAK,UAAU,IAAI,KAAK,GAAG,CAAC;QAAE,WAAW,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IACzE,MAAM,KAAK,GAAG,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAChG,MAAM,WAAW,GAAG,QAAQ,KAAK,WAAW,IAAI,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEhF,OAAO,CAAC,CAAC,EAAE,EAAE;QACX,IAAI,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,WAAW,CAAC;QAC/C,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;YACvB,IAAI,GAAG,CAAC,CAAC;QACX,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,KAAK,EAAE,CAAC;YAC3B,IAAI,GAAG,KAAK,CAAC;QACf,CAAC;QACD,OAAO,IAAI,GAAG,KAAK,CAAC;IACtB,CAAC,CAAC;AACJ,CAAC;AAED,oGAAoG;AACpG,0GAA0G;AAC1G,qGAAqG;AACrG,8EAA8E;AAC9E,MAAM,UAAU,mBAAmB,CAAC,KAA8B;IAChE,WAAW,GAAG,KAAK,CAAC;AACtB,CAAC;AAED,IAAI,WAAW,GAA4B,IAAI,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enableEasingGuards.d.ts","sourceRoot":"","sources":["../src/enableEasingGuards.ts"],"names":[],"mappings":"AAOA,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAWD,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { logOnce } from '@flighthq/log/contract';
|
|
2
|
+
import { LogLevel } from '@flighthq/types/contract';
|
|
3
|
+
import { setEasingStepsGuard } from './easeSteps';
|
|
4
|
+
// Uninstalls the guard installed by enableEasingGuards.
|
|
5
|
+
export function disableEasingGuards() {
|
|
6
|
+
setEasingStepsGuard(null);
|
|
7
|
+
}
|
|
8
|
+
// Installs the caller-facing easing guard (opt-in, dev-only). This package has one silent footgun:
|
|
9
|
+
// `easeSteps(count, 'jumpNone')` with `count < 2` leaves zero jumps, so the returned function divides by
|
|
10
|
+
// zero and hands back NaN for every input. Nothing throws and nothing logs — the NaN just propagates into
|
|
11
|
+
// whatever the easing drives, which is why a comment telling callers "pass count >= 2" was not enough.
|
|
12
|
+
//
|
|
13
|
+
// @flighthq/easing is a CORE package, and core may otherwise depend only on types/core. This module is the
|
|
14
|
+
// sanctioned exception: it is separately importable and shakeable, so a build that never imports it never
|
|
15
|
+
// pulls @flighthq/log, and the layer rule's actual concern — feature weight in core's always-loaded graph —
|
|
16
|
+
// does not arise. `packages:check` enforces that the import appears in guard modules only. Idempotent.
|
|
17
|
+
export function enableEasingGuards() {
|
|
18
|
+
setEasingStepsGuard(warnOnDegenerateSteps);
|
|
19
|
+
}
|
|
20
|
+
function warnOnDegenerateSteps(count, position) {
|
|
21
|
+
logOnce('easing:degenerate-steps', LogLevel.Warn, {
|
|
22
|
+
message: `easeSteps(${count}, '${position}'): '${position}' has count - 1 jumps, so it needs count >= 2. With ${count} there are no jumps, the easing divides by zero, and every input returns NaN. Pass count >= 2, or use a position that jumps at an edge ('jumpEnd', 'jumpStart', 'jumpBoth') — those are defined for count >= 1.`,
|
|
23
|
+
}, 'easing');
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=enableEasingGuards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enableEasingGuards.js","sourceRoot":"","sources":["../src/enableEasingGuards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAElD,wDAAwD;AACxD,MAAM,UAAU,mBAAmB;IACjC,mBAAmB,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC;AAED,mGAAmG;AACnG,yGAAyG;AACzG,0GAA0G;AAC1G,uGAAuG;AACvG,EAAE;AACF,2GAA2G;AAC3G,0GAA0G;AAC1G,4GAA4G;AAC5G,uGAAuG;AACvG,MAAM,UAAU,kBAAkB;IAChC,mBAAmB,CAAC,qBAAqB,CAAC,CAAC;AAC7C,CAAC;AAED,SAAS,qBAAqB,CAAC,KAAa,EAAE,QAAsB;IAClE,OAAO,CACL,yBAAyB,EACzB,QAAQ,CAAC,IAAI,EACb;QACE,OAAO,EAAE,aAAa,KAAK,MAAM,QAAQ,QAAQ,QAAQ,uDAAuD,KAAK,iNAAiN;KACvU,EACD,QAAQ,CACT,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { createEasingSamples, easeClamp, easeClampOutput, easeCubicBezier, easeInBack, easeInBounce, easeInCircular, easeInCubic, easeInElastic, easeInExponential, easeInOutBack, easeInOutBounce, easeInOutCircular, easeInOutCubic, easeInOutElastic, easeInOutExponential, easeInOutPower, easeInOutQuadratic, easeInOutQuartic, easeInOutQuintic, easeInOutSine, easeInPower, easeInQuadratic, easeInQuartic, easeInQuintic, easeInSine, easeInvert, easeLinear, easeMirror, easeOutBack, easeOutBounce, easeOutCircular, easeOutCubic, easeOutElastic, easeOutExponential, easeOutPower, easeOutQuadratic, easeOutQuartic, easeOutQuintic, easeOutSine, easePiecewise, easeReverse, easeScaleOutput, easeSmootherstep, easeSmoothstep, easeSmoothstepRange, easeSteps, getEasingDerivative, } from './contract';
|
|
1
|
+
export { createEasingSamples, easeClamp, easeClampOutput, easeCubicBezier, easeInBack, easeInBounce, easeInCircular, easeInCubic, easeInDampedSine, easeInElastic, easeInExponential, easeInOutBack, easeInOutBounce, easeInOutCircular, easeInOutCubic, easeInOutDampedSine, easeInOutElastic, easeInOutExponential, easeInOutPower, easeInOutQuadratic, easeInOutQuartic, easeInOutQuintic, easeInOutSine, easeInPower, easeInQuadratic, easeInQuartic, easeInQuintic, easeInSine, easeInvert, easeLinear, easeMirror, easeOutBack, easeOutBounce, easeOutCircular, easeOutCubic, easeOutDampedSine, easeOutElastic, easeOutExponential, easeOutPower, easeOutQuadratic, easeOutQuartic, easeOutQuintic, easeOutSine, easePiecewise, easeReverse, easeScaleOutput, easeSmootherstep, easeSmoothstep, easeSmoothstepRange, easeSteps, getEasingDerivative, } from './contract';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,SAAS,EACT,eAAe,EACf,eAAe,EACf,UAAU,EACV,YAAY,EACZ,cAAc,EACd,WAAW,EACX,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,eAAe,EACf,aAAa,EACb,aAAa,EACb,UAAU,EACV,UAAU,EACV,UAAU,EACV,UAAU,EACV,WAAW,EACX,aAAa,EACb,eAAe,EACf,YAAY,EACZ,cAAc,EACd,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,WAAW,EACX,aAAa,EACb,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,mBAAmB,EACnB,SAAS,EACT,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,SAAS,EACT,eAAe,EACf,eAAe,EACf,UAAU,EACV,YAAY,EACZ,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,eAAe,EACf,aAAa,EACb,aAAa,EACb,UAAU,EACV,UAAU,EACV,UAAU,EACV,UAAU,EACV,WAAW,EACX,aAAa,EACb,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,WAAW,EACX,aAAa,EACb,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,mBAAmB,EACnB,SAAS,EACT,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { createEasingSamples, easeClamp, easeClampOutput, easeCubicBezier, easeInBack, easeInBounce, easeInCircular, easeInCubic, easeInElastic, easeInExponential, easeInOutBack, easeInOutBounce, easeInOutCircular, easeInOutCubic, easeInOutElastic, easeInOutExponential, easeInOutPower, easeInOutQuadratic, easeInOutQuartic, easeInOutQuintic, easeInOutSine, easeInPower, easeInQuadratic, easeInQuartic, easeInQuintic, easeInSine, easeInvert, easeLinear, easeMirror, easeOutBack, easeOutBounce, easeOutCircular, easeOutCubic, easeOutElastic, easeOutExponential, easeOutPower, easeOutQuadratic, easeOutQuartic, easeOutQuintic, easeOutSine, easePiecewise, easeReverse, easeScaleOutput, easeSmootherstep, easeSmoothstep, easeSmoothstepRange, easeSteps, getEasingDerivative, } from './contract';
|
|
1
|
+
export { createEasingSamples, easeClamp, easeClampOutput, easeCubicBezier, easeInBack, easeInBounce, easeInCircular, easeInCubic, easeInDampedSine, easeInElastic, easeInExponential, easeInOutBack, easeInOutBounce, easeInOutCircular, easeInOutCubic, easeInOutDampedSine, easeInOutElastic, easeInOutExponential, easeInOutPower, easeInOutQuadratic, easeInOutQuartic, easeInOutQuintic, easeInOutSine, easeInPower, easeInQuadratic, easeInQuartic, easeInQuintic, easeInSine, easeInvert, easeLinear, easeMirror, easeOutBack, easeOutBounce, easeOutCircular, easeOutCubic, easeOutDampedSine, easeOutElastic, easeOutExponential, easeOutPower, easeOutQuadratic, easeOutQuartic, easeOutQuintic, easeOutSine, easePiecewise, easeReverse, easeScaleOutput, easeSmootherstep, easeSmoothstep, easeSmoothstepRange, easeSteps, getEasingDerivative, } from './contract';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,SAAS,EACT,eAAe,EACf,eAAe,EACf,UAAU,EACV,YAAY,EACZ,cAAc,EACd,WAAW,EACX,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,eAAe,EACf,aAAa,EACb,aAAa,EACb,UAAU,EACV,UAAU,EACV,UAAU,EACV,UAAU,EACV,WAAW,EACX,aAAa,EACb,eAAe,EACf,YAAY,EACZ,cAAc,EACd,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,WAAW,EACX,aAAa,EACb,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,mBAAmB,EACnB,SAAS,EACT,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,SAAS,EACT,eAAe,EACf,eAAe,EACf,UAAU,EACV,YAAY,EACZ,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,eAAe,EACf,aAAa,EACb,aAAa,EACb,UAAU,EACV,UAAU,EACV,UAAU,EACV,UAAU,EACV,WAAW,EACX,aAAa,EACb,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,WAAW,EACX,aAAa,EACb,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,mBAAmB,EACnB,SAAS,EACT,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/easing",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1-next.1041.35b950c",
|
|
4
|
+
"author": "Joshua Granick and other contributors",
|
|
5
|
+
"license": "MIT",
|
|
4
6
|
"repository": {
|
|
5
7
|
"type": "git",
|
|
6
8
|
"url": "https://github.com/flighthq/flight.git",
|
|
@@ -36,7 +38,8 @@
|
|
|
36
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
39
|
-
"@flighthq/
|
|
41
|
+
"@flighthq/log": "0.3.1-next.1041.35b950c",
|
|
42
|
+
"@flighthq/types": "0.3.1-next.1041.35b950c"
|
|
40
43
|
},
|
|
41
44
|
"devDependencies": {
|
|
42
45
|
"typescript": "^5.3.0"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { easeInDampedSine, easeInOutDampedSine, easeOutDampedSine } from './easeDampedSine';
|
|
2
|
+
import { easeInElastic, easeInOutElastic, easeOutElastic } from './easeElastic';
|
|
3
|
+
|
|
4
|
+
describe('easeInDampedSine', () => {
|
|
5
|
+
it('returns 0 at t=0 and 1 at t=1', () => {
|
|
6
|
+
const ease = easeInDampedSine(1, 0.4);
|
|
7
|
+
|
|
8
|
+
expect(ease(0)).toBe(0);
|
|
9
|
+
expect(ease(1)).toBe(1);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('winds up below 0 before departing', () => {
|
|
13
|
+
expect(easeInDampedSine(1, 0.4)(0.8)).toBeLessThan(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('matches the fixed easeInElastic at the constants that one hardcodes', () => {
|
|
17
|
+
// The fixed curve is this one at amplitude 1, period 0.4 — the check that the general form is
|
|
18
|
+
// right rather than merely plausible.
|
|
19
|
+
const ease = easeInDampedSine(1, 0.4);
|
|
20
|
+
for (const t of [0.1, 0.25, 0.4, 0.6, 0.75, 0.9]) {
|
|
21
|
+
expect(ease(t)).toBeCloseTo(easeInElastic(t), 10);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('easeInOutDampedSine', () => {
|
|
27
|
+
it('returns 0 at t=0 and 1 at t=1', () => {
|
|
28
|
+
const ease = easeInOutDampedSine(1, 0.45);
|
|
29
|
+
|
|
30
|
+
expect(ease(0)).toBe(0);
|
|
31
|
+
expect(ease(1)).toBe(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('matches the fixed easeInOutElastic at the constants that one hardcodes', () => {
|
|
35
|
+
const ease = easeInOutDampedSine(1, 0.45);
|
|
36
|
+
for (const t of [0.1, 0.3, 0.5, 0.7, 0.9]) {
|
|
37
|
+
expect(ease(t)).toBeCloseTo(easeInOutElastic(t), 10);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('easeOutDampedSine', () => {
|
|
43
|
+
it('returns 0 at t=0 and 1 at t=1', () => {
|
|
44
|
+
const ease = easeOutDampedSine(1, 0.4);
|
|
45
|
+
|
|
46
|
+
expect(ease(0)).toBe(0);
|
|
47
|
+
expect(ease(1)).toBe(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('overshoots past 1 near the start', () => {
|
|
51
|
+
expect(easeOutDampedSine(1, 0.4)(0.6)).toBeGreaterThan(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('matches the fixed easeOutElastic at the constants that one hardcodes', () => {
|
|
55
|
+
const ease = easeOutDampedSine(1, 0.4);
|
|
56
|
+
for (const t of [0.1, 0.25, 0.4, 0.6, 0.75, 0.9]) {
|
|
57
|
+
expect(ease(t)).toBeCloseTo(easeOutElastic(t), 10);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('separates the curves that the fixed form collapses together', () => {
|
|
62
|
+
// The whole reason this family exists: a source stating its own period gets a materially
|
|
63
|
+
// different curve, so substituting the fixed one is wrong rather than approximate. Measured as
|
|
64
|
+
// the widest gap across the curve — any single sample can land where two curves happen to cross.
|
|
65
|
+
const wide = easeOutDampedSine(1, 0.9);
|
|
66
|
+
const narrow = easeOutDampedSine(1, 0.2);
|
|
67
|
+
const widest = Math.max(
|
|
68
|
+
...Array.from({ length: 99 }, (_, index) => Math.abs(wide((index + 1) / 100) - narrow((index + 1) / 100))),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(widest).toBeGreaterThan(0.5);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('overshoots further as amplitude grows', () => {
|
|
75
|
+
const peak = (amplitude: number): number =>
|
|
76
|
+
Math.max(...Array.from({ length: 99 }, (_, index) => easeOutDampedSine(amplitude, 0.4)((index + 1) / 100)));
|
|
77
|
+
|
|
78
|
+
expect(peak(3)).toBeGreaterThan(peak(1));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('raises an amplitude below 1 to the smallest defined curve', () => {
|
|
82
|
+
// The phase term is asin(1 / amplitude), which has no real solution below unit amplitude, so the
|
|
83
|
+
// curve is undefined there rather than merely small. Flight clamps rather than inventing a shape.
|
|
84
|
+
const clamped = easeOutDampedSine(0.5, 0.4);
|
|
85
|
+
const unit = easeOutDampedSine(1, 0.4);
|
|
86
|
+
|
|
87
|
+
for (const t of [0.2, 0.5, 0.8]) expect(clamped(t)).toBeCloseTo(unit(t), 10);
|
|
88
|
+
expect(Number.isFinite(clamped(0.3))).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('falls back to this package own fixed period when the period is not positive', () => {
|
|
92
|
+
// A period divides the wave, so zero has no meaning. Flight's choice is 0.4 — the constant
|
|
93
|
+
// easeOutElastic already uses — so the parameterized and fixed families agree with each other.
|
|
94
|
+
for (const period of [0, -1]) {
|
|
95
|
+
const ease = easeOutDampedSine(1, period);
|
|
96
|
+
expect(Number.isFinite(ease(0.5))).toBe(true);
|
|
97
|
+
expect(ease(0.5)).toBeCloseTo(easeOutDampedSine(1, 0.4)(0.5), 10);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
package/src/easeSteps.test.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { StepPosition } from '@flighthq/types/contract';
|
|
2
|
+
|
|
3
|
+
import { easeSteps, setEasingStepsGuard } from './easeSteps';
|
|
2
4
|
|
|
3
5
|
describe('easeSteps', () => {
|
|
4
6
|
it('jumpEnd holds at 0 first and reaches 1 only at t=1', () => {
|
|
@@ -63,3 +65,33 @@ describe('easeSteps', () => {
|
|
|
63
65
|
}
|
|
64
66
|
});
|
|
65
67
|
});
|
|
68
|
+
|
|
69
|
+
describe('setEasingStepsGuard', () => {
|
|
70
|
+
it('reports the degenerate jumpNone call that silently returns NaN', () => {
|
|
71
|
+
const seen: [number, StepPosition][] = [];
|
|
72
|
+
setEasingStepsGuard((count, position) => seen.push([count, position]));
|
|
73
|
+
try {
|
|
74
|
+
// The failure being reported: one step with no edge jump leaves zero jumps to divide by.
|
|
75
|
+
expect(Number.isNaN(easeSteps(1, 'jumpNone')(0.5))).toBe(true);
|
|
76
|
+
} finally {
|
|
77
|
+
setEasingStepsGuard(null);
|
|
78
|
+
}
|
|
79
|
+
expect(seen).toEqual([[1, 'jumpNone']]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('stays silent for well-defined calls, and once uninstalled', () => {
|
|
83
|
+
const seen: number[] = [];
|
|
84
|
+
setEasingStepsGuard((count) => seen.push(count));
|
|
85
|
+
try {
|
|
86
|
+
easeSteps(2, 'jumpNone')(0.5); // count >= 2 is the documented minimum for jumpNone
|
|
87
|
+
easeSteps(1, 'jumpEnd')(0.5); // edge-jumping positions are defined for count >= 1
|
|
88
|
+
easeSteps(1, 'jumpBoth')(0.5);
|
|
89
|
+
} finally {
|
|
90
|
+
setEasingStepsGuard(null);
|
|
91
|
+
}
|
|
92
|
+
expect(seen).toEqual([]);
|
|
93
|
+
// Uninstalled: the degenerate call still returns NaN, but nothing is reported.
|
|
94
|
+
easeSteps(1, 'jumpNone')(0.5);
|
|
95
|
+
expect(seen).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type { EasingFunction } from '@flighthq/types/contract';
|
|
2
|
+
|
|
3
|
+
// Cross-family invariants: relationships BETWEEN the In/Out/InOut siblings, which is where this package's
|
|
4
|
+
// real defects live. A per-curve value assertion is close to blind here — `easeInQuadratic(0.5) === 0.25`
|
|
5
|
+
// also accepts curves that are not easeInQuadratic, and every easing function satisfies f(0)=0 and f(1)=1,
|
|
6
|
+
// so endpoint assertions accept nearly everything. In ten families of near-identical arithmetic the likely
|
|
7
|
+
// defect is a copy-paste between neighbours, and only a relationship between siblings can see it.
|
|
8
|
+
//
|
|
9
|
+
// This file is cross-family by construction, which is why it has no single source sibling: each assertion
|
|
10
|
+
// is about a PAIR or the whole set. Per-curve behaviour stays in each family's own test file.
|
|
11
|
+
import {
|
|
12
|
+
easeInBack,
|
|
13
|
+
easeInBounce,
|
|
14
|
+
easeInCircular,
|
|
15
|
+
easeInCubic,
|
|
16
|
+
easeInElastic,
|
|
17
|
+
easeInExponential,
|
|
18
|
+
easeInOutBack,
|
|
19
|
+
easeInOutBounce,
|
|
20
|
+
easeInOutCircular,
|
|
21
|
+
easeInOutCubic,
|
|
22
|
+
easeInOutElastic,
|
|
23
|
+
easeInOutExponential,
|
|
24
|
+
easeInOutQuadratic,
|
|
25
|
+
easeInOutQuartic,
|
|
26
|
+
easeInOutQuintic,
|
|
27
|
+
easeInOutSine,
|
|
28
|
+
easeInQuadratic,
|
|
29
|
+
easeInQuartic,
|
|
30
|
+
easeInQuintic,
|
|
31
|
+
easeInSine,
|
|
32
|
+
easeOutBack,
|
|
33
|
+
easeOutBounce,
|
|
34
|
+
easeOutCircular,
|
|
35
|
+
easeOutCubic,
|
|
36
|
+
easeOutElastic,
|
|
37
|
+
easeOutExponential,
|
|
38
|
+
easeOutQuadratic,
|
|
39
|
+
easeOutQuartic,
|
|
40
|
+
easeOutQuintic,
|
|
41
|
+
easeOutSine,
|
|
42
|
+
} from './contract';
|
|
43
|
+
|
|
44
|
+
interface Family {
|
|
45
|
+
easeIn: EasingFunction;
|
|
46
|
+
easeInOut: EasingFunction;
|
|
47
|
+
easeOut: EasingFunction;
|
|
48
|
+
// Whether easeInOut is the two halves of easeIn/easeOut scaled into place. FALSE for the two families
|
|
49
|
+
// whose canonical definition gives the InOut variant a DIFFERENT constant — Back scales its overshoot
|
|
50
|
+
// by 1.525 and Elastic widens its period from 0.4 to 0.45 — so forcing the relation here would be
|
|
51
|
+
// asserting a curve the family does not have.
|
|
52
|
+
halvesMatch: boolean;
|
|
53
|
+
// Whether easeIn rises without ever stepping back. False where the shape is the point: Back undershoots
|
|
54
|
+
// below 0, Elastic oscillates, Bounce bounces.
|
|
55
|
+
monotonic: boolean;
|
|
56
|
+
name: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const FAMILIES: readonly Family[] = [
|
|
60
|
+
{
|
|
61
|
+
easeIn: easeInBack,
|
|
62
|
+
easeInOut: easeInOutBack,
|
|
63
|
+
easeOut: easeOutBack,
|
|
64
|
+
halvesMatch: false,
|
|
65
|
+
monotonic: false,
|
|
66
|
+
name: 'Back',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
easeIn: easeInBounce,
|
|
70
|
+
easeInOut: easeInOutBounce,
|
|
71
|
+
easeOut: easeOutBounce,
|
|
72
|
+
halvesMatch: true,
|
|
73
|
+
monotonic: false,
|
|
74
|
+
name: 'Bounce',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
easeIn: easeInCircular,
|
|
78
|
+
easeInOut: easeInOutCircular,
|
|
79
|
+
easeOut: easeOutCircular,
|
|
80
|
+
halvesMatch: true,
|
|
81
|
+
monotonic: true,
|
|
82
|
+
name: 'Circular',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
easeIn: easeInCubic,
|
|
86
|
+
easeInOut: easeInOutCubic,
|
|
87
|
+
easeOut: easeOutCubic,
|
|
88
|
+
halvesMatch: true,
|
|
89
|
+
monotonic: true,
|
|
90
|
+
name: 'Cubic',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
easeIn: easeInElastic,
|
|
94
|
+
easeInOut: easeInOutElastic,
|
|
95
|
+
easeOut: easeOutElastic,
|
|
96
|
+
halvesMatch: false,
|
|
97
|
+
monotonic: false,
|
|
98
|
+
name: 'Elastic',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
easeIn: easeInExponential,
|
|
102
|
+
easeInOut: easeInOutExponential,
|
|
103
|
+
easeOut: easeOutExponential,
|
|
104
|
+
halvesMatch: true,
|
|
105
|
+
monotonic: true,
|
|
106
|
+
name: 'Exponential',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
easeIn: easeInQuadratic,
|
|
110
|
+
easeInOut: easeInOutQuadratic,
|
|
111
|
+
easeOut: easeOutQuadratic,
|
|
112
|
+
halvesMatch: true,
|
|
113
|
+
monotonic: true,
|
|
114
|
+
name: 'Quadratic',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
easeIn: easeInQuartic,
|
|
118
|
+
easeInOut: easeInOutQuartic,
|
|
119
|
+
easeOut: easeOutQuartic,
|
|
120
|
+
halvesMatch: true,
|
|
121
|
+
monotonic: true,
|
|
122
|
+
name: 'Quartic',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
easeIn: easeInQuintic,
|
|
126
|
+
easeInOut: easeInOutQuintic,
|
|
127
|
+
easeOut: easeOutQuintic,
|
|
128
|
+
halvesMatch: true,
|
|
129
|
+
monotonic: true,
|
|
130
|
+
name: 'Quintic',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
easeIn: easeInSine,
|
|
134
|
+
easeInOut: easeInOutSine,
|
|
135
|
+
easeOut: easeOutSine,
|
|
136
|
+
halvesMatch: true,
|
|
137
|
+
monotonic: true,
|
|
138
|
+
name: 'Sine',
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
// 201 points rather than a handful: a copy-paste between neighbours can agree at the sample points a
|
|
143
|
+
// hand-picked list would choose, and cannot agree across the whole unit interval.
|
|
144
|
+
const SAMPLES: readonly number[] = Array.from({ length: 201 }, (_, index) => index / 200);
|
|
145
|
+
|
|
146
|
+
// The measured worst reflection error across every family is 1.1e-15, so this bound is roughly three
|
|
147
|
+
// orders of magnitude tighter than the smallest real difference a wrong constant could produce, and
|
|
148
|
+
// still far above double-precision noise.
|
|
149
|
+
const EPSILON = 1e-12;
|
|
150
|
+
|
|
151
|
+
describe('easing family distinctness', () => {
|
|
152
|
+
// The direct test for the copy-paste defect, and the only one that can see it: two families whose
|
|
153
|
+
// bodies were pasted from each other agree everywhere, and every other assertion in this file passes
|
|
154
|
+
// for both. The measured closest pair is Quadratic against Sine at 5.6e-2, so this bound sits an order
|
|
155
|
+
// of magnitude below the nearest real neighbours and far above any rounding difference.
|
|
156
|
+
it.each(['easeIn', 'easeInOut', 'easeOut'] as const)('%s curves are pairwise distinct', (direction) => {
|
|
157
|
+
for (let a = 0; a < FAMILIES.length; a += 1) {
|
|
158
|
+
for (let b = a + 1; b < FAMILIES.length; b += 1) {
|
|
159
|
+
const first = FAMILIES[a][direction];
|
|
160
|
+
const second = FAMILIES[b][direction];
|
|
161
|
+
const separation = SAMPLES.reduce((worst, t) => Math.max(worst, Math.abs(first(t) - second(t))), 0);
|
|
162
|
+
expect({ pair: `${FAMILIES[a].name}/${FAMILIES[b].name}`, separated: separation > 0.005 }).toEqual({
|
|
163
|
+
pair: `${FAMILIES[a].name}/${FAMILIES[b].name}`,
|
|
164
|
+
separated: true,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('easing family endpoints', () => {
|
|
172
|
+
// EXACT, not approximate — except where the formula itself cannot be exact in binary floating point,
|
|
173
|
+
// and those two are named rather than folded into a loose tolerance for everyone. easeInSine(1) is
|
|
174
|
+
// 1 - cos(pi/2) and cos(pi/2) is 6.1e-17 rather than 0; easeInBack(1) is (s + 1) - s, which cancels to
|
|
175
|
+
// 0.9999999999999998 for s = 1.70158. Both are one ulp, both are properties of the arithmetic, and
|
|
176
|
+
// special-casing the code to force a rounder number would be making the curve fit the test.
|
|
177
|
+
const INEXACT_AT_ONE = new Set(['Back', 'Sine']);
|
|
178
|
+
|
|
179
|
+
// OBSERVED, and harmless: easeInBack(0) and easeInOutSine(0) return NEGATIVE zero, because both bodies
|
|
180
|
+
// end in a multiplication by zero with a negative factor. It is numerically equal to 0 and behaves
|
|
181
|
+
// identically everywhere an easing value is consumed; only Object.is separates them, which is what
|
|
182
|
+
// `toBe` uses. Normalising rather than loosening keeps the assertion EXACT for every other value.
|
|
183
|
+
it.each(FAMILIES)('$name: starts at exactly 0', ({ easeIn, easeInOut, easeOut }) => {
|
|
184
|
+
expect(normalizeZero(easeIn(0))).toBe(0);
|
|
185
|
+
expect(normalizeZero(easeInOut(0))).toBe(0);
|
|
186
|
+
expect(Math.abs(easeOut(0))).toBeLessThan(EPSILON);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it.each(FAMILIES)('$name: ends at 1, exactly where the arithmetic allows', ({ easeIn, easeInOut, easeOut }) => {
|
|
190
|
+
expect(easeOut(1)).toBe(1);
|
|
191
|
+
expect(easeInOut(1)).toBe(1);
|
|
192
|
+
if (INEXACT_AT_ONE.has(name(easeIn))) expect(easeIn(1)).toBeCloseTo(1, 15);
|
|
193
|
+
else expect(easeIn(1)).toBe(1);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
function normalizeZero(value: number): number {
|
|
197
|
+
return value === 0 ? 0 : value;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function name(easeIn: EasingFunction): string {
|
|
201
|
+
return FAMILIES.find((family) => family.easeIn === easeIn)!.name;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('easing family halves', () => {
|
|
206
|
+
it.each(FAMILIES.filter((family) => family.halvesMatch))(
|
|
207
|
+
'$name: easeInOut is easeIn on the first half and easeOut on the second',
|
|
208
|
+
({ easeIn, easeInOut, easeOut }) => {
|
|
209
|
+
for (const t of SAMPLES) {
|
|
210
|
+
const expected = t <= 0.5 ? easeIn(t * 2) / 2 : 0.5 + easeOut(t * 2 - 1) / 2;
|
|
211
|
+
expect(Math.abs(easeInOut(t) - expected)).toBeLessThan(EPSILON);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// The two exceptions are asserted as exceptions rather than skipped, so a later edit that "fixes" one
|
|
217
|
+
// of them into the scaled-halves shape fails here and has to be a deliberate change to the curve.
|
|
218
|
+
it.each(FAMILIES.filter((family) => !family.halvesMatch))(
|
|
219
|
+
'$name: easeInOut deliberately differs from the scaled halves, because its canonical constant differs',
|
|
220
|
+
({ easeIn, easeInOut }) => {
|
|
221
|
+
const worst = SAMPLES.filter((t) => t <= 0.5).reduce(
|
|
222
|
+
(accumulated, t) => Math.max(accumulated, Math.abs(easeInOut(t) - easeIn(t * 2) / 2)),
|
|
223
|
+
0,
|
|
224
|
+
);
|
|
225
|
+
expect(worst).toBeGreaterThan(0.01);
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Continuity asserted as the gap SHRINKING with the interval, not as the gap being small. A small-gap
|
|
230
|
+
// test is the wrong instrument here: Circular has a vertical tangent at the midpoint by construction —
|
|
231
|
+
// it is a quarter circle — so its slope there measures ~1000 against 0.58 at t=0.25, and any fixed
|
|
232
|
+
// bound either fails a correct curve or is loose enough to accept a real step. A genuine discontinuity
|
|
233
|
+
// has a gap that stops shrinking; a steep one has a gap that keeps going.
|
|
234
|
+
it.each(FAMILIES)('$name: easeInOut passes through the midpoint without a step', ({ easeInOut }) => {
|
|
235
|
+
expect(easeInOut(0.5)).toBeCloseTo(0.5, 12);
|
|
236
|
+
const wide = Math.abs(easeInOut(0.5 + 1e-4) - easeInOut(0.5 - 1e-4));
|
|
237
|
+
const narrow = Math.abs(easeInOut(0.5 + 1e-6) - easeInOut(0.5 - 1e-6));
|
|
238
|
+
expect(narrow).toBeLessThan(Math.max(wide / 2, 1e-9));
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('easing family reflection', () => {
|
|
243
|
+
// The workhorse. easeOut is easeIn rotated 180 degrees about the centre of the unit square, and that
|
|
244
|
+
// holds BY DEFINITION for every family here — so a constant that differs between the two, or a body
|
|
245
|
+
// pasted from the wrong neighbour, breaks it at once.
|
|
246
|
+
it.each(FAMILIES)('$name: easeOut(t) is 1 - easeIn(1 - t) across the interval', ({ easeIn, easeOut }) => {
|
|
247
|
+
for (const t of SAMPLES) {
|
|
248
|
+
expect(Math.abs(easeOut(t) - (1 - easeIn(1 - t)))).toBeLessThan(EPSILON);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('easing family shape', () => {
|
|
254
|
+
it.each(FAMILIES.filter((family) => family.monotonic))('$name: easeIn never steps back', ({ easeIn }) => {
|
|
255
|
+
for (let index = 1; index < SAMPLES.length; index += 1) {
|
|
256
|
+
expect(easeIn(SAMPLES[index])).toBeGreaterThanOrEqual(easeIn(SAMPLES[index - 1]));
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// The overshoot IS the family. A wrong constant removes it silently and leaves a curve that still runs
|
|
261
|
+
// 0 to 1 monotonically — which every endpoint assertion in this package would happily accept.
|
|
262
|
+
it.each([
|
|
263
|
+
{ easeIn: easeInBack, easeOut: easeOutBack, name: 'Back' },
|
|
264
|
+
{ easeIn: easeInElastic, easeOut: easeOutElastic, name: 'Elastic' },
|
|
265
|
+
])('$name: easeIn dips below 0 and easeOut rises above 1', ({ easeIn, easeOut }) => {
|
|
266
|
+
expect(Math.min(...SAMPLES.map(easeIn))).toBeLessThan(-0.05);
|
|
267
|
+
expect(Math.max(...SAMPLES.map(easeOut))).toBeGreaterThan(1.05);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('Bounce reverses without ever leaving the unit interval', () => {
|
|
271
|
+
const values = SAMPLES.map(easeInBounce);
|
|
272
|
+
expect(Math.min(...values)).toBeGreaterThanOrEqual(0);
|
|
273
|
+
expect(Math.max(...values)).toBeLessThanOrEqual(1);
|
|
274
|
+
expect(values.some((value, index) => index > 0 && value < values[index - 1])).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
|
|
2
|
+
import type { LogEntry } from '@flighthq/types/contract';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { easeSteps } from './easeSteps';
|
|
6
|
+
import { disableEasingGuards, enableEasingGuards } from './enableEasingGuards';
|
|
7
|
+
|
|
8
|
+
function captureLog(run: () => void): readonly LogEntry[] {
|
|
9
|
+
const sink = createMemoryLogSink(8);
|
|
10
|
+
addLogSink(sink.sink);
|
|
11
|
+
try {
|
|
12
|
+
run();
|
|
13
|
+
return getMemoryLogSinkEntries(sink);
|
|
14
|
+
} finally {
|
|
15
|
+
removeLogSink(sink.sink);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function messageOf(entry: Readonly<LogEntry>): string {
|
|
20
|
+
const data = entry.data;
|
|
21
|
+
return typeof data === 'string' ? data : String(data.message);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('disableEasingGuards', () => {
|
|
25
|
+
it('uninstalls the guard, returning the degenerate call to silence', () => {
|
|
26
|
+
const entries = captureLog(() => {
|
|
27
|
+
enableEasingGuards();
|
|
28
|
+
disableEasingGuards();
|
|
29
|
+
// Still NaN — disabling removes the reporting, not the sharp edge.
|
|
30
|
+
expect(Number.isNaN(easeSteps(1, 'jumpNone')(0.5))).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
expect(entries.length).toBe(0);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('enableEasingGuards', () => {
|
|
37
|
+
it('WARNS on the degenerate jumpNone step count that silently returns NaN', () => {
|
|
38
|
+
const entries = captureLog(() => {
|
|
39
|
+
enableEasingGuards();
|
|
40
|
+
try {
|
|
41
|
+
expect(Number.isNaN(easeSteps(1, 'jumpNone')(0.5))).toBe(true);
|
|
42
|
+
} finally {
|
|
43
|
+
disableEasingGuards();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
expect(entries.length).toBe(1);
|
|
47
|
+
expect(messageOf(entries[0])).toContain('count >= 2');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('stays SILENT for well-defined calls, and without the guard at all', () => {
|
|
51
|
+
const guarded = captureLog(() => {
|
|
52
|
+
enableEasingGuards();
|
|
53
|
+
try {
|
|
54
|
+
easeSteps(2, 'jumpNone')(0.5);
|
|
55
|
+
easeSteps(1, 'jumpEnd')(0.5);
|
|
56
|
+
easeSteps(1, 'jumpStart')(0.5);
|
|
57
|
+
easeSteps(1, 'jumpBoth')(0.5);
|
|
58
|
+
} finally {
|
|
59
|
+
disableEasingGuards();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
expect(guarded.length).toBe(0);
|
|
63
|
+
|
|
64
|
+
// Production default: still NaN, but nothing is logged — the guard is opt-in.
|
|
65
|
+
const unguarded = captureLog(() => {
|
|
66
|
+
expect(Number.isNaN(easeSteps(1, 'jumpNone')(0.5))).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
expect(unguarded.length).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
});
|