301 lines
10 KiB
TypeScript
301 lines
10 KiB
TypeScript
/** Cubic Bezier Implementation */
|
|
|
|
/**
|
|
* Finds the parameter t for a given x value on a cubic Bézier curve.
|
|
* Uses Newton's method first (fast), then falls back to binary search (reliable).
|
|
* Based on WebKit/Chromium's UnitBezier implementation.
|
|
*
|
|
* @param {number} targetX - The x value to find parameter t for
|
|
* @param {number} x1 - x-coordinate of first control point
|
|
* @param {number} x2 - x-coordinate of second control point
|
|
* @returns {number} The parameter t that gives the specified x value
|
|
*/
|
|
function solveCubicBezierX(targetX: number, x1: number, x2: number): number {
|
|
const epsilon = 1e-6;
|
|
|
|
// Pre-compute polynomial coefficients for performance
|
|
const cx = 3.0 * x1;
|
|
const bx = 3.0 * (x2 - x1) - cx;
|
|
const ax = 1.0 - cx - bx;
|
|
|
|
function sampleCurveX(t: number): number {
|
|
// Evaluate: ax*t³ + bx*t² + cx*t using Horner's rule
|
|
return ((ax * t + bx) * t + cx) * t;
|
|
}
|
|
|
|
function sampleCurveDerivativeX(t: number): number {
|
|
return (3.0 * ax * t + 2.0 * bx) * t + cx;
|
|
}
|
|
|
|
/**
|
|
* Newton's method - fast convergence with good initial guess
|
|
* Try this first as it's normally very fast
|
|
*/
|
|
function newtonRaphsonMethod(targetX: number): number | null {
|
|
let t = targetX;
|
|
|
|
for (let iteration = 0; iteration < 8; iteration++) {
|
|
const currentX = sampleCurveX(t);
|
|
const error = currentX - targetX;
|
|
|
|
if (Math.abs(error) < epsilon) {
|
|
return t;
|
|
}
|
|
|
|
const derivative = sampleCurveDerivativeX(t);
|
|
|
|
// Break if derivative is too small (avoid division by zero)
|
|
if (Math.abs(derivative) < epsilon) {
|
|
break;
|
|
}
|
|
|
|
// Newton-Raphson step: t_new = t_old - f(t_old) / f'(t_old)
|
|
t = t - error / derivative;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Binary search fallback - guaranteed to converge
|
|
* Use this when Newton's method fails
|
|
*/
|
|
function binarySearchFallback(targetX: number): number {
|
|
let lowerBound = 0.0;
|
|
let upperBound = 1.0;
|
|
let t = targetX;
|
|
|
|
while (lowerBound < upperBound) {
|
|
const currentX = sampleCurveX(t);
|
|
|
|
if (Math.abs(currentX - targetX) < epsilon) {
|
|
return t;
|
|
}
|
|
|
|
if (targetX > currentX) {
|
|
lowerBound = t;
|
|
} else {
|
|
upperBound = t;
|
|
}
|
|
|
|
t = (upperBound - lowerBound) * 0.5 + lowerBound;
|
|
}
|
|
|
|
return t;
|
|
}
|
|
|
|
const newtonResult = newtonRaphsonMethod(targetX);
|
|
if (newtonResult !== null) {
|
|
return newtonResult;
|
|
}
|
|
|
|
return binarySearchFallback(targetX);
|
|
}
|
|
|
|
/**
|
|
* Evaluates the Y coordinate of a cubic Bézier curve at parameter t.
|
|
* Optimized for CSS timing functions where P0=(0,0) and P3=(1,1).
|
|
* Uses pre-computed polynomial coefficients and Horner's rule for performance.
|
|
*
|
|
* @param {number} t - Parameter value along the curve, typically in [0, 1]
|
|
* @param {number} y1 - y-coordinate of first control point
|
|
* @param {number} y2 - y-coordinate of second control point
|
|
* @returns {number} The Y value of the curve at parameter t
|
|
*/
|
|
function sampleCurveY(t: number, y1: number, y2: number): number {
|
|
// Pre-compute polynomial coefficients for performance
|
|
const cy = 3.0 * y1;
|
|
const by = 3.0 * (y2 - y1) - cy;
|
|
const ay = 1.0 - cy - by;
|
|
|
|
// Evaluate: ay*t³ + by*t² + cy*t using Horner's rule
|
|
return ((ay * t + by) * t + cy) * t;
|
|
}
|
|
|
|
/**
|
|
* Cubic Bézier easing function implementation following CSS specifications.
|
|
*
|
|
* A cubic Bézier curve is defined by four points: P0, P1, P2, and P3.
|
|
* In CSS animations, P0 is fixed at (0, 0) and P3 is fixed at (1, 1).
|
|
* This function allows you to specify the intermediate control points P1 and P2.
|
|
*
|
|
* @param {number} x1 - x-coordinate of the first control point P1 (must be in [0, 1])
|
|
* @param {number} y1 - y-coordinate of the first control point P1
|
|
* @param {number} x2 - x-coordinate of the second control point P2 (must be in [0, 1])
|
|
* @param {number} y2 - y-coordinate of the second control point P2
|
|
* @returns {function} A function that takes time t ∈ [0, 1] and returns the eased value
|
|
*/
|
|
export function cubicBezier(x1: number, y1: number, x2: number, y2: number) {
|
|
if (x1 < 0 || x1 > 1 || x2 < 0 || x2 > 1) {
|
|
throw new Error('x1 and x2 must be in the range [0, 1]');
|
|
}
|
|
|
|
return function (timeProgress: number): number {
|
|
if (timeProgress <= 0) return 0;
|
|
if (timeProgress >= 1) return 1;
|
|
|
|
if (x1 === y1 && x2 === y2) {
|
|
return timeProgress;
|
|
}
|
|
|
|
const curveParameter = solveCubicBezierX(timeProgress, x1, x2);
|
|
return sampleCurveY(curveParameter, y1, y2);
|
|
};
|
|
}
|
|
|
|
/** Core Easing Functions */
|
|
|
|
/**
|
|
* Linear easing function - constant speed throughout the animation.
|
|
* Equivalent to CSS: cubic-bezier(0.0, 0.0, 1.0, 1.0)
|
|
*
|
|
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
|
* @returns {number} The linear eased value.
|
|
*/
|
|
const linearCore = cubicBezier(0.0, 0.0, 1.0, 1.0);
|
|
|
|
/**
|
|
* Ease-in easing function - starts slow and accelerates.
|
|
* Equivalent to CSS: cubic-bezier(0.42, 0, 1.0, 1.0)
|
|
*
|
|
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
|
* @returns {number} The eased value.
|
|
*/
|
|
const easeInCore = cubicBezier(0.42, 0, 1.0, 1.0);
|
|
|
|
/**
|
|
* Ease-out easing function - starts fast and decelerates.
|
|
* Equivalent to CSS: cubic-bezier(0, 0, 0.58, 1.0)
|
|
*
|
|
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
|
* @returns {number} The eased value.
|
|
*/
|
|
const easeOutCore = cubicBezier(0, 0, 0.58, 1.0);
|
|
|
|
/**
|
|
* Ease-in-out easing function - starts slow, accelerates, then decelerates.
|
|
* Equivalent to CSS: cubic-bezier(0.42, 0, 0.58, 1.0)
|
|
*
|
|
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
|
* @returns {number} The eased value.
|
|
*/
|
|
const easeInOutCore = cubicBezier(0.42, 0, 0.58, 1.0);
|
|
|
|
/**
|
|
* Standard ease easing function (CSS default).
|
|
* Equivalent to CSS: cubic-bezier(0.25, 0.1, 0.25, 1.0)
|
|
*
|
|
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
|
* @returns {number} The eased value.
|
|
*/
|
|
const easeCore = cubicBezier(0.25, 0.1, 0.25, 1.0);
|
|
|
|
/** Flexible Easing Function Factory */
|
|
|
|
/**
|
|
* Flexible factory function that creates easing functions with optional baseline and scale support.
|
|
* Provides bell-curve behavior: baseline → baseline*scale → baseline
|
|
*
|
|
* @param {Function} coreEasingFn - The core easing function to wrap
|
|
* @returns {Function} A function that accepts (timeProgress, baseline?, scale?) and provides flexible behavior
|
|
*/
|
|
function flexibleEasingFunctionFactory(coreEasingFn: (timeProgress: number) => number) {
|
|
return function (timeProgress: number, baseline: number = 0, scale?: number): number {
|
|
if (baseline === 0) {
|
|
return coreEasingFn(timeProgress);
|
|
}
|
|
|
|
const easedProgress = coreEasingFn(timeProgress);
|
|
|
|
const targetValue = scale ? baseline * scale : 1;
|
|
const range = targetValue - baseline;
|
|
|
|
// Create bell-curve: baseline → targetValue → baseline
|
|
const bellMultiplier = 1 - Math.abs(2 * easedProgress - 1);
|
|
return baseline + bellMultiplier * range;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Linear easing function with optional baseline and scale support.
|
|
* - No params: standard linear progression [0, 1]
|
|
* - With baseline only: bell-curve baseline → 1 → baseline
|
|
* - With baseline+scale: bell-curve baseline → baseline*scale → baseline
|
|
*
|
|
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
|
* @param {number} baseline - Optional baseline value (default: 0). Creates bell-curve effect.
|
|
* @param {number} scale - Optional scale multiplier (default: 1). Peak value = baseline * scale.
|
|
* @returns {number} The linear eased value.
|
|
*/
|
|
export const linear = flexibleEasingFunctionFactory(linearCore);
|
|
|
|
/**
|
|
* Standard ease easing function with optional baseline and scale support.
|
|
* - No params: standard ease progression [0, 1]
|
|
* - With baseline only: bell-curve baseline → 1 → baseline
|
|
* - With baseline+scale: bell-curve baseline → baseline*scale → baseline
|
|
*
|
|
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
|
* @param {number} baseline - Optional baseline value (default: 0). Creates bell-curve effect.
|
|
* @param {number} scale - Optional scale multiplier (default: 1). Peak value = baseline * scale.
|
|
* @returns {number} The eased value.
|
|
*/
|
|
export const ease = flexibleEasingFunctionFactory(easeCore);
|
|
|
|
/**
|
|
* Ease-in easing function with optional baseline and scale support.
|
|
* - No params: standard ease-in progression [0, 1]
|
|
* - With baseline only: bell-curve baseline → 1 → baseline
|
|
* - With baseline+scale: bell-curve baseline → baseline*scale → baseline
|
|
*
|
|
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
|
* @param {number} baseline - Optional baseline value (default: 0). Creates bell-curve effect.
|
|
* @param {number} scale - Optional scale multiplier (default: 1). Peak value = baseline * scale.
|
|
* @returns {number} The eased value.
|
|
*/
|
|
export const easeIn = flexibleEasingFunctionFactory(easeInCore);
|
|
|
|
/**
|
|
* Ease-out easing function with optional baseline and scale support.
|
|
* - No params: standard ease-out progression [0, 1]
|
|
* - With baseline only: bell-curve baseline → 1 → baseline
|
|
* - With baseline+scale: bell-curve baseline → baseline*scale → baseline
|
|
*
|
|
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
|
* @param {number} baseline - Optional baseline value (default: 0). Creates bell-curve effect.
|
|
* @param {number} scale - Optional scale multiplier (default: 1). Peak value = baseline * scale.
|
|
* @returns {number} The eased value.
|
|
*/
|
|
export const easeOut = flexibleEasingFunctionFactory(easeOutCore);
|
|
|
|
/**
|
|
* Ease-in-out easing function with optional baseline and scale support.
|
|
* - No params: standard ease-in-out progression [0, 1]
|
|
* - With baseline only: bell-curve baseline → 1 → baseline
|
|
* - With baseline+scale: bell-curve baseline → baseline*scale → baseline
|
|
*
|
|
* @param {number} timeProgress - The animation progress, in the range [0, 1].
|
|
* @param {number} baseline - Optional baseline value (default: 0). Creates bell-curve effect.
|
|
* @param {number} scale - Optional scale multiplier (default: 1). Peak value = baseline * scale.
|
|
* @returns {number} The eased value.
|
|
*/
|
|
export const easeInOut = flexibleEasingFunctionFactory(easeInOutCore);
|
|
|
|
/** Export interfaces */
|
|
|
|
export enum EasingFunctionEnum {
|
|
EASE = 'ease',
|
|
EASE_IN = 'ease-in',
|
|
EASE_OUT = 'ease-out',
|
|
EASE_IN_OUT = 'ease-in-out',
|
|
LINEAR = 'linear',
|
|
}
|
|
|
|
export const EasingFunctionMap = new Map([
|
|
[EasingFunctionEnum.EASE, ease],
|
|
[EasingFunctionEnum.EASE_IN, easeIn],
|
|
[EasingFunctionEnum.EASE_OUT, easeOut],
|
|
[EasingFunctionEnum.EASE_IN_OUT, easeInOut],
|
|
[EasingFunctionEnum.LINEAR, linear],
|
|
]);
|