license import { invariant , withDefault } from "./utils" ; export interface SpringConfig { fromValue : number ; toValue : number ; stiffness : number ; damping : number ; mass : number ; initialVelocity : number ; allowsOverdamping : boolean ; overshootClamping : boolean ; restVelocityThreshold : number ; restDisplacementThreshold : number ; } export type PartialSpringConfig = Partial < SpringConfig > ; export type SpringListenerFn = ( spring : Spring ) => void ; export interface SpringListener { onUpdate ? : SpringListenerFn ; onStart ? : SpringListenerFn ; onStop ? : SpringListenerFn ; } /**
 * Implements a spring physics simulation based on the equations behind
 * damped harmonic oscillators (https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator).
 */ export class Spring { static MAX_DELTA_TIME_MS = 1 / 60 * 1000 * 4 ; _listeners : SpringListener [ ] = [ ] ; private _config : SpringConfig ; private _currentAnimationStep : number = 0 ; private _currentTime : number = 0 ; private _springTime : number = 0 ; private _currentValue : number = 0 ; private _currentVelocity : number = 0 ; private _isAnimating : boolean = false ; private _oscillationVelocityPairs = [ ] ; constructor ( config = { } ) { this . _config = { fromValue : withDefault ( config . fromValue , 0 ) , toValue : withDefault ( config . toValue , 1 ) , stiffness : withDefault ( config . stiffness , 100 ) , damping : withDefault ( config . damping , 10 ) , mass : withDefault ( config . mass , 1 ) , initialVelocity : withDefault ( config . initialVelocity , 0 ) , overshootClamping : withDefault ( config . overshootClamping , false ) , allowsOverdamping : withDefault ( config . allowsOverdamping , false ) , restVelocityThreshold : withDefault ( config . restVelocityThreshold , 0.001 ) , restDisplacementThreshold : withDefault ( config . restDisplacementThreshold , 0.001 ) } ; this . _currentValue = this . _config . fromValue ; this . _currentVelocity = this . _config . initialVelocity ; } /**
     * If `fromValue` differs from `toValue`, or `initialVelocity` is non-zero,
     * start the simulation and call the `onStart` listeners.
     */ start ( ) { const { fromValue , toValue , initialVelocity } = this . _config ; if ( fromValue !== toValue || initialVelocity !== 0 ) { this . _reset ( ) ; this . _isAnimating = true ; if ( ! this . _currentAnimationStep ) { this . _notifyListeners ( "onStart" ) ; this . _currentAnimationStep = requestAnimationFrame ( ( t ) => { this . _step ( Date . now ( ) ) ; } ) ; } } return this ; } /**
     * If a simulation is in progress, stop it and call the `onStop` listeners.
     */ stop ( ) { if ( ! this . _isAnimating ) { return this ; } this . _isAnimating = false ; this . _notifyListeners ( "onStop" ) ; if ( this . _currentAnimationStep ) { cancelAnimationFrame ( this . _currentAnimationStep ) ; this . _currentAnimationStep = 0 ; } return this ; } /**
     * The spring's current position.
     */ get currentValue ( ) : number { return this . _currentValue ; } /**
     * The spring's current velocity in units / ms.
     */ get currentVelocity ( ) : number { return this . _currentVelocity ; } /**
     * If the spring has reached its `toValue`, or if its velocity is below the
     * `restVelocityThreshold`, it is considered at rest. If `stop()` is called
     * during a simulation, both `isAnimating` and `isAtRest` will be false.
     */ get isAtRest ( ) : boolean { return this . _isSpringAtRest ( ) ; } /**
     * Whether or not the spring is currently emitting values.
     *
     * Note: this is distinct from whether or not it is at rest.
     * See also `isAtRest`.
     */ get isAnimating ( ) : boolean { return this . _isAnimating ; } /**
     * Updates the spring config with the given values.  Values not explicitly
     * supplied will be reused from the existing config.
     */ updateConfig ( updatedConfig ) { this . _advanceSpringToTime ( Date . now ( ) ) ; const baseConfig = { fromValue : this . _currentValue , initialVelocity : this . _currentVelocity } ; this . _config = { ... this . _config , ... baseConfig , ... updatedConfig } ; this . _reset ( ) ; return this ; } /**
     * The provided callback will be invoked when the simulation begins.
     */ onStart ( listener ) { this . _listeners . push ( { onStart : listener } ) ; return this ; } /**
     * The provided callback will be invoked on each frame while the simulation is
     * running.
     */ onUpdate ( listener ) { this . _listeners . push ( { onUpdate : listener } ) ; return this ; } /**
     * The provided callback will be invoked when the simulation ends.
     */ onStop ( listener ) { this . _listeners . push ( { onStop : listener } ) ; return this ; } /**
     * Remove a single listener from this spring.
     */ removeListener ( listenerFn ) { this . _listeners = this . _listeners . reduce ( ( result , listener ) => { const foundListenerFn = Object . values ( listener ) . indexOf ( listenerFn ) !== - 1 ; if ( ! foundListenerFn ) { result . push ( listener ) ; } return result ; } , [ ] as SpringListener [ ] ) ; return this ; } /**
     * Removes all listeners from this spring.
     */ removeAllListeners ( ) { this . _listeners = [ ] ; return this ; } private _reset ( ) { this . _currentTime = Date . now ( ) ; this . _springTime = 0.0 ; this . _currentValue = this . _config . fromValue ; this . _currentVelocity = this . _config . initialVelocity ; } private _notifyListeners ( eventName ) { this . _listeners . forEach ( ( listener ) => { const maybeListenerFn = listener [ eventName ] ; if ( typeof maybeListenerFn === "function" ) { maybeListenerFn ( this ) ; } } ) ; } /**
     * `_step` is the main loop.  While the animation is running, it updates the
     * current state once per frame, and schedules the next frame if the spring is
     * not yet at rest.
     */ private _step ( timestamp ) { this . _advanceSpringToTime ( timestamp , true ) ; if ( this . _isAnimating ) { this . _currentAnimationStep = requestAnimationFrame ( ( t ) => this . _step ( Date . now ( ) ) ) ; } } private _advanceSpringToTime ( timestamp , shouldNotifyListeners = false ) { if ( ! this . _isAnimating ) { return ; } let deltaTime = timestamp - this . _currentTime ; if ( deltaTime > Spring . MAX_DELTA_TIME_MS ) { deltaTime = Spring . MAX_DELTA_TIME_MS ; } this . _springTime += deltaTime ; const c = this . _config . damping ; const m = this . _config . mass ; const k = this . _config . stiffness ; const fromValue = this . _config . fromValue ; const toValue = this . _config . toValue ; const v0 = - this . _config . initialVelocity ; invariant ( m > 0 , "Mass value must be greater than 0" ) ; invariant ( k > 0 , "Stiffness value must be greater than 0" ) ; let zeta = c / ( 2 * Math . sqrt ( k * m ) ) ; const omega0 = Math . sqrt ( k / m ) / 1000 ; const omega1 = omega0 * Math . sqrt ( 1.0 - zeta * zeta ) ; const omega2 = omega0 * Math . sqrt ( zeta * zeta - 1.0 ) ; const x0 = toValue - fromValue ; if ( zeta > 1 && ! this . _config . allowsOverdamping ) { zeta = 1 ; } let oscillation = 0.0 ; let velocity = 0.0 ; const t = this . _springTime ; if ( zeta < 1 ) { const envelope = Math . exp ( - zeta * omega0 * t ) ; oscillation = toValue - envelope * ( ( v0 + zeta * omega0 * x0 ) / omega1 * Math . sin ( omega1 * t ) + x0 * Math . cos ( omega1 * t ) ) ; velocity = zeta * omega0 * envelope * ( Math . sin ( omega1 * t ) * ( v0 + zeta * omega0 * x0 ) / omega1 + x0 * Math . cos ( omega1 * t ) ) - envelope * ( Math . cos ( omega1 * t ) * ( v0 + zeta * omega0 * x0 ) - omega1 * x0 * Math . sin ( omega1 * t ) ) ; } else if ( zeta === 1 ) { const envelope = Math . exp ( - omega0 * t ) ; oscillation = toValue - envelope * ( x0 + ( v0 + omega0 * x0 ) * t ) ; velocity = envelope * ( v0 * ( t * omega0 - 1 ) + t * x0 * ( omega0 * omega0 ) ) ; } else { const envelope = Math . exp ( - zeta * omega0 * t ) ; oscillation = toValue - envelope * ( ( v0 + zeta * omega0 * x0 ) * Math . sinh ( omega2 * t ) + omega2 * x0 * Math . cosh ( omega2 * t ) ) / omega2 ; velocity = envelope * zeta * omega0 * ( Math . sinh ( omega2 * t ) * ( v0 + zeta * omega0 * x0 ) + x0 * omega2 * Math . cosh ( omega2 * t ) ) / omega2 - envelope * ( omega2 * Math . cosh ( omega2 * t ) * ( v0 + zeta * omega0 * x0 ) + omega2 * omega2 * x0 * Math . sinh ( omega2 * t ) ) / omega2 ; } this . _currentTime = timestamp ; this . _currentValue = oscillation ; this . _currentVelocity = velocity ; if ( ! shouldNotifyListeners ) { return ; } this . _notifyListeners ( "onUpdate" ) ; if ( ! this . _isAnimating ) { return ; } if ( this . _isSpringOvershooting ( ) || this . _isSpringAtRest ( ) ) { if ( k !== 0 ) { this . _currentValue = toValue ; this . _currentVelocity = 0 ; this . _notifyListeners ( "onUpdate" ) ; } this . stop ( ) ; return ; } } private _isSpringOvershooting ( ) { const { stiffness , fromValue , toValue , overshootClamping } = this . _config ; let isOvershooting = false ; if ( overshootClamping && stiffness !== 0 ) { if ( fromValue < toValue ) { isOvershooting = this . _currentValue > toValue ; } else { isOvershooting = this . _currentValue < toValue ; } } return isOvershooting ; } private _isSpringAtRest ( ) { const { stiffness , toValue , restDisplacementThreshold , restVelocityThreshold } = this . _config ; const isNoVelocity = Math . abs ( this . _currentVelocity ) <= restVelocityThreshold ; const isNoDisplacement = stiffness !== 0 && Math . abs ( toValue - this . _currentValue ) <= restDisplacementThreshold ; return isNoDisplacement && isNoVelocity ; } }