All files / src/animations/generators spring.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 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            8x 8x 8x 8x 8x 8x 8x 4x           4x   4x 4x     4x 4x 4x 4x           4x             4x   2x 10x   10x   10x                     2x   10x   10x 10x                               2x   1x 6x 6x                       1x 2x   2x                   4x   4x   18x 18x 18x   18x 18x     18x 18x                   2x     8x  
import { SpringOptions, Animation, AnimationState } from "../types"
 
/**
 * This is based on the spring implementation of Wobble https://github.com/skevy/wobble
 */
export function spring({
    from = 0.0,
    to = 0.0,
    velocity = 0.0,
    stiffness = 100,
    damping = 10,
    mass = 1.0,
    restSpeed = 2,
    restDelta,
}: SpringOptions): Animation<number> {
    /**
     * This is the Iterator-spec return value. We ensure it's mutable rather than using a generator
     * to reduce GC during animation.
     */
    const state: AnimationState<number> = { done: false, value: from }
 
    let resolveSpring = zero
    let resolveVelocity = zero
 
    function createSpring() {
        const initialVelocity = velocity ? -(velocity / 1000) : 0.0
        const initialDelta = to - from
        const dampingRatio = damping / (2 * Math.sqrt(stiffness * mass))
        const angularFreq = Math.sqrt(stiffness / mass) / 1000
 
        /**
         * If we're working within what looks like a 0-1 range, change the default restDelta
         * to 0.01
         */
        restDelta =
            restDelta === undefined
                ? Math.abs(to - from) <= 1
                    ? 0.01
                    : 0.4
                : restDelta
 
        if (dampingRatio < 1) {
            // Underdamped spring
            resolveSpring = (t: number) => {
                const envelope = Math.exp(-dampingRatio * angularFreq * t)
                const expoDecay =
                    angularFreq * Math.sqrt(1.0 - dampingRatio * dampingRatio)
 
                return (
                    to -
                    envelope *
                        (((initialVelocity +
                            dampingRatio * angularFreq * initialDelta) /
                            expoDecay) *
                            Math.sin(expoDecay * t) +
                            initialDelta * Math.cos(expoDecay * t))
                )
            }
 
            resolveVelocity = (t: number) => {
                // TODO Resolve these calculations with the above
                const envelope = Math.exp(-dampingRatio * angularFreq * t)
                const expoDecay =
                    angularFreq * Math.sqrt(1.0 - dampingRatio * dampingRatio)
                return (
                    dampingRatio *
                        angularFreq *
                        envelope *
                        ((Math.sin(expoDecay * t) *
                            (initialVelocity +
                                dampingRatio * angularFreq * initialDelta)) /
                            expoDecay +
                            initialDelta * Math.cos(expoDecay * t)) -
                    envelope *
                        (Math.cos(expoDecay * t) *
                            (initialVelocity +
                                dampingRatio * angularFreq * initialDelta) -
                            expoDecay * initialDelta * Math.sin(expoDecay * t))
                )
            }
        } else if (dampingRatio === 1) {
            // Critically damped spring
            resolveSpring = (t: number) => {
                const envelope = Math.exp(-angularFreq * t)
                return (
                    to -
                    envelope *
                        (initialDelta +
                            (initialVelocity + angularFreq * initialDelta) * t)
                )
            }
        } else {
            // Overdamped spring
            // const dampedAngularFreq =
            //     angularFreq * Math.sqrt(dampingRatio * dampingRatio - 1)
 
            resolveSpring = (t: number) => {
                const envelope = Math.exp(-dampingRatio * angularFreq * t)
 
                return (
                    to -
                    envelope *
                        (initialDelta +
                            (initialVelocity + angularFreq * initialDelta) * t)
                )
            }
        }
    }
 
    createSpring()
 
    return {
        next: (t: number) => {
            const current = resolveSpring(t)
            const velocity = resolveVelocity(t) * 1000
            const isBelowVelocityThreshold = Math.abs(velocity) <= restSpeed
            const isBelowDisplacementThreshold =
                Math.abs(to - current) <= restDelta
            state.done =
                isBelowVelocityThreshold && isBelowDisplacementThreshold
 
            state.value = state.done ? to : current
            return state
        },
        flipTarget: () => {
            velocity = -velocity
            ;[from, to] = [to, from]
            createSpring()
        },
    }
}
 
spring.needsInterpolation = (a: any, b: any) =>
    typeof a === "string" || typeof b === "string"
 
const zero = (_t: number) => 0