1111/**
1212 * BezierEasing - use bezier curve for transition easing function
1313 * https://github.com/gre/bezier-easing
14- * @copyright 2014-2015 Gaëtan Renaudeau. MIT License.
14+ * @copyright 2014-2026 Gaëtan Renaudeau. MIT License.
1515 */
1616
1717'use strict' ;
1818
19- // These values are established by empiricism with tests (tradeoff: performance VS precision)
20- const NEWTON_ITERATIONS = 4 ;
21- const NEWTON_MIN_SLOPE = 0.001 ;
22- const SUBDIVISION_PRECISION = 0.0000001 ;
23- const SUBDIVISION_MAX_ITERATIONS = 10 ;
24-
25- const kSplineTableSize = 11 ;
26- const kSampleStepSize = 1.0 / ( kSplineTableSize - 1.0 ) ;
27-
28- const float32ArraySupported = typeof Float32Array === 'function' ;
29-
30- function A ( aA1 : number , aA2 : number ) {
31- return 1.0 - 3.0 * aA2 + 3.0 * aA1 ;
32- }
33- function B ( aA1 : number , aA2 : number ) {
34- return 3.0 * aA2 - 6.0 * aA1 ;
35- }
36- function C ( aA1 : number ) {
37- return 3.0 * aA1 ;
38- }
39-
40- // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
41- function calcBezier ( aT : number , aA1 : number , aA2 : number ) {
42- return ( ( A ( aA1 , aA2 ) * aT + B ( aA1 , aA2 ) ) * aT + C ( aA1 ) ) * aT ;
43- }
44-
45- // Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
46- function getSlope ( aT : number , aA1 : number , aA2 : number ) {
47- return 3.0 * A ( aA1 , aA2 ) * aT * aT + 2.0 * B ( aA1 , aA2 ) * aT + C ( aA1 ) ;
48- }
49-
50- function binarySubdivide (
51- aX : number ,
52- _aA : number ,
53- _aB : number ,
54- mX1 : number ,
55- mX2 : number ,
56- ) {
57- let currentX ,
58- currentT ,
59- i = 0 ,
60- aA = _aA ,
61- aB = _aB ;
62- do {
63- currentT = aA + ( aB - aA ) / 2.0 ;
64- currentX = calcBezier ( currentT , mX1 , mX2 ) - aX ;
65- if ( currentX > 0.0 ) {
66- aB = currentT ;
67- } else {
68- aA = currentT ;
69- }
70- } while (
71- Math . abs ( currentX ) > SUBDIVISION_PRECISION &&
72- ++ i < SUBDIVISION_MAX_ITERATIONS
73- ) ;
74- return currentT ;
75- }
76-
77- function newtonRaphsonIterate (
78- aX : number ,
79- _aGuessT : number ,
80- mX1 : number ,
81- mX2 : number ,
82- ) {
83- let aGuessT = _aGuessT ;
84- for ( let i = 0 ; i < NEWTON_ITERATIONS ; ++ i ) {
85- const currentSlope = getSlope ( aGuessT , mX1 , mX2 ) ;
86- if ( currentSlope === 0.0 ) {
87- return aGuessT ;
88- }
89- const currentX = calcBezier ( aGuessT , mX1 , mX2 ) - aX ;
90- aGuessT -= currentX / currentSlope ;
19+ // Solves x(t) = ((2a * t + 3b) * t + 3c) * t = x for t, with x in (0, 1):
20+ // u = 1/t is the largest real root of x·u³ − 3c·u² − 3b·u − 2a = 0
21+ function solveTForX ( x : number , a : number , b : number , c : number ) : number {
22+ const j = 1 / Math . max ( c , Math . sqrt ( x ) ) ;
23+ const k = x * j ;
24+ const l = k * j ;
25+ const s = c * j ;
26+ const q = b * l ;
27+ const m = s * s + q ;
28+ const h = - s * ( s * s + 1.5 * q ) - a * k * l ;
29+ const D = h * h - m * m * m ;
30+ let v : number ;
31+ if ( m === 0 || D > 1e-12 * h * h ) {
32+ // one real root (Cardano)
33+ const U = - Math . cbrt ( h < 0 ? h - Math . sqrt ( D ) : h + Math . sqrt ( D ) ) ;
34+ v = U + m / U || 0 ;
35+ } else {
36+ // three real roots, take the largest
37+ const r = Math . sqrt ( m ) ;
38+ v =
39+ 2 * r * Math . cos ( Math . acos ( Math . max ( - 1 , Math . min ( 1 , - h / ( m * r ) ) ) ) / 3 ) ;
9140 }
92- return aGuessT ;
41+ return Math . min ( 1 , k / ( v + s ) ) ;
9342}
9443
9544export default function bezier (
@@ -102,63 +51,29 @@ export default function bezier(
10251 throw new Error ( 'bezier x values must be in [0, 1] range' ) ;
10352 }
10453
105- // Precompute samples table
106- const sampleValues = float32ArraySupported
107- ? new Float32Array ( kSplineTableSize )
108- : new Array < number > ( kSplineTableSize ) ;
109- if ( mX1 !== mY1 || mX2 !== mY2 ) {
110- for ( let i = 0 ; i < kSplineTableSize ; ++ i ) {
111- sampleValues [ i ] = calcBezier ( i * kSampleStepSize , mX1 , mX2 ) ;
112- }
54+ if ( mX1 === mY1 && mX2 === mY2 ) {
55+ return function LinearEasing ( x : number ) : number {
56+ return x ;
57+ } ;
11358 }
11459
115- function getTForX ( aX : number ) {
116- let intervalStart = 0.0 ;
117- let currentSample = 1 ;
118- const lastSample = kSplineTableSize - 1 ;
119-
120- for (
121- ;
122- currentSample !== lastSample && sampleValues [ currentSample ] <= aX ;
123- ++ currentSample
124- ) {
125- intervalStart += kSampleStepSize ;
126- }
127- -- currentSample ;
128-
129- // Interpolate to provide an initial guess for t
130- const dist =
131- ( aX - sampleValues [ currentSample ] ) /
132- ( sampleValues [ currentSample + 1 ] - sampleValues [ currentSample ] ) ;
133- const guessForT = intervalStart + dist * kSampleStepSize ;
134-
135- const initialSlope = getSlope ( guessForT , mX1 , mX2 ) ;
136- if ( initialSlope >= NEWTON_MIN_SLOPE ) {
137- return newtonRaphsonIterate ( aX , guessForT , mX1 , mX2 ) ;
138- } else if ( initialSlope === 0.0 ) {
139- return guessForT ;
140- } else {
141- return binarySubdivide (
142- aX ,
143- intervalStart ,
144- intervalStart + kSampleStepSize ,
145- mX1 ,
146- mX2 ,
147- ) ;
148- }
149- }
60+ // x(t) = ((2a * t + 3b) * t + 3c) * t, y(t) = ((ay * t + by) * t + cy) * t
61+ const a = ( 3 * mX1 - 3 * mX2 + 1 ) / 2 ;
62+ const b = mX2 - 2 * mX1 ;
63+ const c = mX1 ;
64+ const ay = 3 * mY1 - 3 * mY2 + 1 ;
65+ const by = 3 * ( mY2 - 2 * mY1 ) ;
66+ const cy = 3 * mY1 ;
15067
15168 return function BezierEasing ( x : number ) : number {
152- if ( mX1 === mY1 && mX2 === mY2 ) {
153- return x ; // linear
154- }
155- // Because JavaScript number are imprecise, we should guarantee the extremes are right.
156- if ( x === 0 ) {
69+ // x outside (0, 1) saturates to 0 / 1
70+ if ( x <= 0 ) {
15771 return 0 ;
15872 }
159- if ( x == = 1 ) {
73+ if ( x > = 1 ) {
16074 return 1 ;
16175 }
162- return calcBezier ( getTForX ( x ) , mY1 , mY2 ) ;
76+ const t = solveTForX ( x , a , b , c ) ;
77+ return ( ( ay * t + by ) * t + cy ) * t ;
16378 } ;
16479}
0 commit comments