Tracker made in React for keeping track of HP and MP and so on.
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.
 
 
 
fabula-ultima-react/src/ui/SpringyValueHook.ts

135 lines
5.8 KiB

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<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 | FluidValue<T>
export type SpringyValueInterpolatables<TargetType extends object> =
{[Property in keyof TargetType]: SpringyValueInterpolatable<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): AnimatedProps<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 AnimatedProps<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, 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 <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])
}