import {useCallback, useMemo, useState} from "react"; import {AnimatedProps, SpringConfig, SpringValue, to, useSpring, useTrail} from "@react-spring/web"; import {FluidValue} from "@react-spring/shared"; export interface UseSpringyValueProps { current?: number starting?: number max?: number flash?: boolean springDelays?: readonly [number, number, number] springConfigs?: readonly [SpringConfig, SpringConfig, SpringConfig] flashConfig?: SpringConfig } export interface UseSpringyValueOutput { springs: {v: SpringValue}[] flashSpring: {v: SpringValue} interpolate: SpringyValueInterpolate } export interface SpringyValues { currentValue?: number // The true current value of the resource, after the recent delta. displayedValue: number // The displayed current value of the resource, after the recent delta. newValue: number // The displayed value of the resource as of ~2 seconds ago. recentValue: number // The end of the delta that's being applied. maxValue?: number // The maximum value of the resource. previousValue?: number // The original value before the most recent delta. flashValue: number // The position in the oscillation of the flashing resource. flashing?: boolean // Whether the resource is actively flashing. } const literalSymbol: unique symbol = Symbol("SpringyValueLiteralSymbol") export type SpringyValueLiteral = {[literalSymbol]: T} export function markSpringyValueLiteral(value: T): SpringyValueLiteral { return {[literalSymbol]: value} } export function isSpringyValueLiteral(value: SpringyValueInterpolatable): value is SpringyValueLiteral { return typeof value === "object" && value !== null && Object.hasOwn(value, literalSymbol) } export type SpringyValueInterpolator = (v: SpringyValues) => T export type SpringyValueInterpolatable = SpringyValueLiteral | Exclude | SpringyValueInterpolator export type SpringyValueInterpolate = (v: SpringyValueInterpolatable) => SpringyValueInterpolated export type SpringyValueInterpolated = T | FluidValue export type SpringyValueInterpolatables = {[Property in keyof TargetType]: SpringyValueInterpolatable} export function evaluateSpringyValueInterpolator(f: SpringyValueInterpolatable, v: SpringyValues): T { if (f instanceof Function) { return f(v) } else if (isSpringyValueLiteral(f)) { return f[literalSymbol] } else { return f } } export function interpolateSpringyValueInterpolatables(values: SpringyValueInterpolatables, interpolator: SpringyValueInterpolate): AnimatedProps { const result: {[key: string]: SpringyValueInterpolated} = {} for (const [key, value] of Object.entries(values) as [string, SpringyValueInterpolatable][]) { result[key] = interpolator(value) } return result as AnimatedProps } const DEFAULT_SPRING_DELAYS = [0, 500, 50] as const const DEFAULT_SPRING_CONFIGS: readonly [SpringConfig, SpringConfig, SpringConfig] = [{tension: 1200, friction: 40, precision: 0.1, round: 0.001, clamp: true}, {tension: 200, friction: 20, precision: 0.1, round: 0.001}, {mass: 2, tension: 200, friction: 90, precision: 0.1, round: 0.001}] const DEFAULT_FLASH_CONFIG: SpringConfig = { tension: 170, friction: 26, clamp: true, } export function useSpringyValue({current, starting, max, flash, springConfigs=DEFAULT_SPRING_CONFIGS, springDelays=DEFAULT_SPRING_DELAYS, flashConfig=DEFAULT_FLASH_CONFIG}: UseSpringyValueProps): UseSpringyValueOutput { const [lastCurrent, setLastCurrent] = useState(current) const [wasFlashing, setWasFlashing] = useState(flash) const [springs, barApi] = useTrail(3, (i) => ({ from: {v: starting ?? current ?? 0}, to: {v: current ?? 0}, config: springConfigs[i], delay: springDelays[i], immediate: false, }), []) const [flashSpring, flashApi] = useSpring({ from: {v: 0}, to: flash ? [{v: 1}, {v: 0}] : [{v: 0}], loop: flash, config: flashConfig, }, []) const interpolate = useCallback(function (v: SpringyValueInterpolatable): SpringyValueInterpolated { if (v instanceof Function) { return to([...springs.map(s => s.v), flashSpring.v], (displayedValue: number, newValue: number, recentValue: number, flashValue: number) => v({ currentValue: current, displayedValue, newValue, recentValue, flashValue, maxValue: max, previousValue: lastCurrent, flashing: flash ?? false })) } else if (isSpringyValueLiteral(v)) { return v[literalSymbol] } else { return v } }, [current, lastCurrent, max, flash, springs, flashSpring.v]) if (flash !== wasFlashing) { if (flash && !wasFlashing) { flashApi.start({to: [{v: 1}, {v: 0}], loop: true}) } else if (!flash && wasFlashing) { flashApi.start({to: [{v: 0}], loop: false}) } setWasFlashing(flash) } if (current !== lastCurrent) { barApi.stop(true) if ((lastCurrent ?? 0) < (current ?? 0)) { barApi.set((idx, ctrl) => ({ v: Math.max(lastCurrent ?? 0, ctrl.get().v) })) } else { barApi.set((idx, ctrl) => ({ v: Math.min(lastCurrent ?? 0, ctrl.get().v) })) } barApi.start((i) => ({to: [{v: current ?? 0}], immediate: false, delay: springDelays[i], config: springConfigs[i]})) setLastCurrent(current) } return useMemo(() => ({ springs, flashSpring, interpolate, }), [springs, flashSpring, interpolate]) }