All files / easing cubic-bezier.ts

76.92% Statements 40/52
60% Branches 12/20
90% Functions 9/10
75% Lines 36/48

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137                                83x 83x 83x     2x 47x     2x 36x   2x 2x                                                         2x 2x             4x 32x 32x     32x 32x   4x     2x 2x                 2x     1x   1x 11x       4x 4x 4x   4x         19x   4x       4x   4x   4x 4x 4x                             1x 6x    
/*
  Bezier function generator
 
  Gaƫtan Renaudeau's BezierEasing
  https://github.com/gre/bezier-easing/blob/master/src/index.js
  https://github.com/gre/bezier-easing/blob/master/LICENSE
  You're a hero
 
  Use
 
    const easeOut = cubicBezier(.17,.67,.83,.67);
    const x = easeOut(0.5); // returns 0.627...
*/
 
import { linear } from "."
 
const a = (a1: number, a2: number) => 1.0 - 3.0 * a2 + 3.0 * a1
const b = (a1: number, a2: number) => 3.0 * a2 - 6.0 * a1
const c = (a1: number) => 3.0 * a1
 
// Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
const calcBezier = (t: number, a1: number, a2: number) =>
    ((a(a1, a2) * t + b(a1, a2)) * t + c(a1)) * t
 
// Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
const getSlope = (t: number, a1: number, a2: number) =>
    3.0 * a(a1, a2) * t * t + 2.0 * b(a1, a2) * t + c(a1)
 
const subdivisionPrecision = 0.0000001
const subdivisionMaxIterations = 10
 
function binarySubdivide(
    aX: number,
    aA: number,
    aB: number,
    mX1: number,
    mX2: number
) {
    let currentX: number
    let currentT: number
    let i: number = 0
 
    do {
        currentT = aA + (aB - aA) / 2.0
        currentX = calcBezier(currentT, mX1, mX2) - aX
        if (currentX > 0.0) {
            aB = currentT
        } else {
            aA = currentT
        }
    } while (
        Math.abs(currentX) > subdivisionPrecision &&
        ++i < subdivisionMaxIterations
    )
 
    return currentT
}
 
const newtonIterations = 8
const newtonMinSlope = 0.001
function newtonRaphsonIterate(
    aX: number,
    aGuessT: number,
    mX1: number,
    mX2: number
) {
    for (let i = 0; i < newtonIterations; ++i) {
        const currentSlope = getSlope(aGuessT, mX1, mX2)
        Iif (currentSlope === 0.0) {
            return aGuessT
        }
        const currentX = calcBezier(aGuessT, mX1, mX2) - aX
        aGuessT -= currentX / currentSlope
    }
    return aGuessT
}
 
const kSplineTableSize = 11
const kSampleStepSize = 1.0 / (kSplineTableSize - 1.0)
 
export function cubicBezier(
    mX1: number,
    mY1: number,
    mX2: number,
    mY2: number
) {
    // If this is a linear gradient, return linear easing
    if (mX1 === mY1 && mX2 === mY2) return linear
 
    // Precompute samples table
    const sampleValues = new Float32Array(kSplineTableSize)
 
    for (let i = 0; i < kSplineTableSize; ++i) {
        sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2)
    }
 
    function getTForX(aX: number) {
        let intervalStart = 0.0
        let currentSample = 1
        const lastSample = kSplineTableSize - 1
 
        for (
            ;
            currentSample !== lastSample && sampleValues[currentSample] <= aX;
            ++currentSample
        ) {
            intervalStart += kSampleStepSize
        }
        --currentSample
 
        // Interpolate to provide an initial guess for t
        const dist =
            (aX - sampleValues[currentSample]) /
            (sampleValues[currentSample + 1] - sampleValues[currentSample])
        const guessForT = intervalStart + dist * kSampleStepSize
 
        const initialSlope = getSlope(guessForT, mX1, mX2)
        Eif (initialSlope >= newtonMinSlope) {
            return newtonRaphsonIterate(aX, guessForT, mX1, mX2)
        } else if (initialSlope === 0.0) {
            return guessForT
        } else {
            return binarySubdivide(
                aX,
                intervalStart,
                intervalStart + kSampleStepSize,
                mX1,
                mX2
            )
        }
    }
 
    // If animation is at start/end, return t without easing
    return (t: number) =>
        t === 0 || t === 1 ? t : calcBezier(getTForX(t), mY1, mY2)
}