Skip to content

packages/core/src/lib/animation/easing.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

1 import type { tween } from './definition.js';
2 import type { PibblEasing } from './types.js';
3 
4 const NEWTON_ITERATIONS = 8;
5 const BISECTION_ITERATIONS = 50;
6 
7 function clampProgress(progress: number): number {
8   return Math.min(1, Math.max(0, progress));
9 }
10 
11 function assertFiniteControl(value: number, name: string): void {
12   if (!Number.isFinite(value)) {
13     throw new RangeError(`Pibbl cubic bezier ${name} must be finite.`);
14   }
15 }
16 
17 function sampleCurve(t: number, first: number, second: number): number {
18   const inverse = 1 - t;
19   return 3 * inverse * inverse * t * first +
20     3 * inverse * t * t * second +
21     t * t * t;
22 }
23 
24 function sampleDerivative(t: number, first: number, second: number): number {
25   const inverse = 1 - t;
26   return 3 * inverse * inverse * first +
27     6 * inverse * t * (second - first) +
28     3 * t * t * (1 - second);
29 }
30 
31 function cubicBezier(
32   x1: number,
33   y1: number,
34   x2: number,
35   y2: number,
36 ): PibblEasing {
37   assertFiniteControl(x1, 'x1');
38   assertFiniteControl(y1, 'y1');
39   assertFiniteControl(x2, 'x2');
40   assertFiniteControl(y2, 'y2');
41   if (x1 < 0 || x1 > 1 || x2 < 0 || x2 > 1) {
42     throw new RangeError('Pibbl cubic bezier x1 and x2 must be within [0, 1].');
43   }
44 
45   return (input: number): number => {
46     const progress = clampProgress(input);
47     if (progress === 0 || progress === 1) return progress;
48 
49     let parameter = progress;
50     for (let iteration = 0; iteration < NEWTON_ITERATIONS; iteration++) {
51       const x = sampleCurve(parameter, x1, x2) - progress;
52       const slope = sampleDerivative(parameter, x1, x2);
53       if (Math.abs(slope) < 1e-7) break;
54       const candidate = parameter - x / slope;
55       if (candidate < 0 || candidate > 1) break;
56       parameter = candidate;
57     }
58 
59     let lower = 0;
60     let upper = 1;
61     for (let iteration = 0; iteration < BISECTION_ITERATIONS; iteration++) {
62       const x = sampleCurve(parameter, x1, x2);
63       if (x < progress) lower = parameter;
64       else upper = parameter;
65       parameter = (lower + upper) / 2;
66     }
67     return sampleCurve(parameter, y1, y2);
68   };
69 }
70 
71 function steps(count: number, position: 'start' | 'end' = 'end'): PibblEasing {
72   if (!Number.isInteger(count) || count <= 0) {
73     throw new RangeError('Pibbl steps count must be a positive integer.');
74   }
75   if (position !== 'start' && position !== 'end') {
76     throw new TypeError('Pibbl steps position must be "start" or "end".');
77   }
78 
79   return (input: number): number => {
80     const progress = clampProgress(input);
81     if (position === 'start') {
82       return Math.min(1, (Math.floor(progress * count) + 1) / count);
83     }
84     return progress === 1 ? 1 : Math.floor(progress * count) / count;
85   };
86 }
87 
88 /**
89  * Built-in easing functions for mapping normalized animation progress.
90  *
91  * @see {@link PibblEasing}
92  * @see {@link tween}
93  */
94 export const easing = Object.freeze({
95   /**
96    * Returns clamped progress unchanged. See {@link PibblEasing}.
97    * @param progress - Input progress, clamped to the interval [0, 1].
98    * @returns The clamped progress.
99    */
100   linear: (progress: number): number => clampProgress(progress),
101   /**
102    * Creates a cubic Bézier easing; control-point x coordinates must be finite and within [0, 1].
103    * See {@link PibblEasing}.
104    * @param x1 - First control point horizontal coordinate, in [0, 1].
105    * @param y1 - Finite first control point vertical coordinate.
106    * @param x2 - Second control point horizontal coordinate, in [0, 1].
107    * @param y2 - Finite second control point vertical coordinate.
108    * @returns A cubic Bézier easing function. See {@link PibblEasing}.
109    */
110   cubicBezier,
111   /**
112    * Accelerates from rest using the CSS ease-in control points. See {@link easing}.
113    * @param progress - Input progress, clamped to [0, 1].
114    * @returns Eased progress. See {@link PibblEasing}.
115    */
116   easeIn: cubicBezier(0.42, 0, 1, 1),
117   /**
118    * Decelerates toward rest using the CSS ease-out control points. See {@link easing}.
119    * @param progress - Input progress, clamped to [0, 1].
120    * @returns Eased progress. See {@link PibblEasing}.
121    */
122   easeOut: cubicBezier(0, 0, 0.58, 1),
123   /**
124    * Accelerates then decelerates using the CSS ease-in-out control points. See {@link easing}.
125    * @param progress - Input progress, clamped to [0, 1].
126    * @returns Eased progress. See {@link PibblEasing}.
127    */
128   easeInOut: cubicBezier(0.42, 0, 0.58, 1),
129   /**
130    * Creates a stepped easing with a positive integer count and start or end jump placement. See
131    * {@link easing}.
132    * @param count - Positive integer number of steps.
133    * @param position - Whether jumps occur at the start or end of each interval; defaults to end.
134    * @returns A stepped easing function. See {@link PibblEasing}.
135    */
136   steps,
137 });
138 

Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.