You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
131 lines
5.7 KiB
131 lines
5.7 KiB
import {useCallback, useMemo, useState} from "react";
|
|
import {Interpolation, SpringConfig, SpringValue, to, useSpring, useTrail} from "@react-spring/web";
|
|
|
|
export interface UseSpringyValueProps {
|
|
current?: 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<T> = {[literalSymbol]: T}
|
|
export function markSpringyValueLiteral<T>(value: T): SpringyValueLiteral<T> {
|
|
return {[literalSymbol]: value}
|
|
}
|
|
export function isSpringyValueLiteral<T>(value: SpringyValueInterpolatable<T>): value is SpringyValueLiteral<T> {
|
|
return typeof value === "object" && value !== null && Object.hasOwn(value, literalSymbol)
|
|
}
|
|
export type SpringyValueInterpolator<T> = (v: SpringyValues) => T
|
|
export type SpringyValueInterpolatable<T> = SpringyValueLiteral<T> | Exclude<T, Function> | SpringyValueInterpolator<T>
|
|
export type SpringyValueInterpolate = <T>(v: SpringyValueInterpolatable<T>) => SpringyValueInterpolated<T>
|
|
export type SpringyValueInterpolated<T> = T | Interpolation<T>
|
|
export type SpringyValueInterpolatables<TargetType extends object> =
|
|
{[Property in keyof TargetType]: SpringyValueInterpolatable<TargetType[Property]>}
|
|
export type SpringyValueInterpolateds<TargetType extends object> =
|
|
{[Property in keyof TargetType]: SpringyValueInterpolated<TargetType[Property]>}
|
|
|
|
export function evaluateSpringyValueInterpolator<T>(f: SpringyValueInterpolatable<T>, v: SpringyValues): T {
|
|
if (f instanceof Function) {
|
|
return f(v)
|
|
} else if (isSpringyValueLiteral(f)) {
|
|
return f[literalSymbol]
|
|
} else {
|
|
return f
|
|
}
|
|
}
|
|
export function interpolateSpringyValueInterpolatables<T extends object>(values: SpringyValueInterpolatables<T>, interpolator: SpringyValueInterpolate): SpringyValueInterpolateds<T> {
|
|
const result: {[key: string]: SpringyValueInterpolated<any>} = {}
|
|
for (const [key, value] of Object.entries(values) as [string, SpringyValueInterpolatable<any>][]) {
|
|
result[key] = interpolator(value)
|
|
}
|
|
return result as SpringyValueInterpolateds<T>
|
|
}
|
|
|
|
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, 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, () => ({
|
|
v: current ?? 0
|
|
}), [])
|
|
const [flashSpring, flashApi] = useSpring({
|
|
from: {v: 0},
|
|
to: flash ? [{v: 1}, {v: 0}] : [{v: 0}],
|
|
loop: flash,
|
|
config: flashConfig,
|
|
}, [])
|
|
|
|
const interpolate = useCallback(function <T>(v: SpringyValueInterpolatable<T>): SpringyValueInterpolated<T> {
|
|
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])
|
|
} |