All files / src/animations inertia.ts

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                                                                                                                                                                                                                             
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
        if (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,
        })
    }
 
    if (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
        if (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(),
    }
}