All files / animations inertia.ts

91.49% Statements 43/47
66.67% Branches 36/54
87.5% Functions 7/8
91.89% Lines 34/37

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                      2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x         4x       2x 1x           4x   4x       18x 18x             2x                 2x                       2x 2x 2x 2x       2x 10x 10x 10x   10x       2x       2x                       2x        
import {
    InertiaOptions,
    PlaybackControls,
    AnimationOptions,
    SpringOptions,
} from "./types"
import { animate } from "."
import { velocityPerSecond } from "../utils/velocity-per-second"
import { getFrameData } from "framesync"
 
export function inertia({
    from = 0,
    velocity = 0,
    min,
    max,
    power = 0.8,
    timeConstant = 750,
    bounceStiffness = 500,
    bounceDamping = 10,
    restDelta = 1,
    modifyTarget,
    driver,
    onUpdate,
    onComplete,
}: InertiaOptions) {
    let currentAnimation: PlaybackControls
 
    function isOutOfBounds(v: number) {
        return (min !== undefined && v < min) || (max !== undefined && v > max)
    }
 
    function boundaryNearest(v: number) {
        if (min === undefined) return max
        Eif (max === undefined) return min
 
        return Math.abs(min - v) < Math.abs(max - v) ? min : max
    }
 
    function startAnimation(options: AnimationOptions<number>) {
        currentAnimation?.stop()
 
        currentAnimation = animate({
            ...options,
            driver,
            onUpdate: (v: number) => {
                onUpdate?.(v)
                options.onUpdate?.(v)
            },
            onComplete,
        })
    }
 
    function startSpring(options: SpringOptions) {
        startAnimation({
            type: "spring",
            stiffness: bounceStiffness,
            damping: bounceDamping,
            restDelta,
            ...options,
        })
    }
 
    Iif (isOutOfBounds(from)) {
        // Start the animation with spring if outside the defined boundaries
        startSpring({ from, velocity, to: boundaryNearest(from) })
    } else {
        /**
         * Or if the value is out of bounds, simulate the inertia movement
         * with the decay animation.
         *
         * Pre-calculate the target so we can detect if it's out-of-bounds.
         * If it is, we want to check per frame when to switch to a spring
         * animation
         */
        let target = power * velocity + from
        Iif (typeof modifyTarget !== "undefined") target = modifyTarget(target)
        const boundary = boundaryNearest(target)
        const heading = boundary === min ? -1 : 1
        let prev: number
        let current: number
 
        const checkBoundary = (v: number) => {
            prev = current
            current = v
            velocity = velocityPerSecond(v - prev, getFrameData().delta)
 
            if (
                (heading === 1 && v > boundary) ||
                (heading === -1 && v < boundary)
            ) {
                startSpring({ from: v, to: boundary, velocity })
            }
        }
 
        startAnimation({
            type: "decay",
            from,
            velocity,
            timeConstant,
            power,
            restDelta,
            modifyTarget,
            onUpdate: isOutOfBounds(target) ? checkBoundary : undefined,
        })
    }
 
    return {
        stop: () => currentAnimation?.stop(),
    }
}