mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-07-30 17:59:13 -04:00
- Introduced `useCountUp` hook to facilitate animated counting of numbers with easing effects. - Updated About page to display member, country, and conversation stats with count-up animations. - Added API data retrieval and Intersection Observer logic to trigger animations on scroll. - Replaced `GeneralButton` in About page with a custom `<Link>` for consistent styling and hover effects. - Enhanced visual hierarchy by applying animations and styles adapted for reduced-motion users.
53 lines
2.0 KiB
TypeScript
53 lines
2.0 KiB
TypeScript
import {useRef, useState} from 'react'
|
|
import {useSafeLayoutEffect} from 'web/hooks/use-safe-layout-effect'
|
|
|
|
/**
|
|
* Drives a number from 0 up to `target` on a rAF timeline.
|
|
*
|
|
* Returns the eased `value` and the *raw* (un-eased) `progress`, because the two are wanted for
|
|
* different things: the value should decelerate hard so the last digits settle rather than crawl, while
|
|
* the styling that rides along (scale, weight, blur) reads better on linear time — easing it twice makes
|
|
* the type snap to its final size long before the number stops moving.
|
|
*
|
|
* Callers pass `run: false` for reduced-motion or not-yet-visible, and get `progress: 1` immediately —
|
|
* the final number, no animation, no flash of zero.
|
|
*/
|
|
|
|
// Expo-out: ~85% of the distance is covered in the first third, so it looks like a counter spinning
|
|
// down rather than a linear tween, which reads as a progress bar.
|
|
const easeOutExpo = (t: number) => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t))
|
|
|
|
export function useCountUp(
|
|
target: number,
|
|
{run, duration = 1500, delay = 0}: {run: boolean; duration?: number; delay?: number},
|
|
) {
|
|
const [progress, setProgress] = useState(1)
|
|
const frame = useRef<number | null>(null)
|
|
|
|
// Layout effect, not `useEffect`: the zero has to be in place before the browser paints the frame in
|
|
// which `run` flips on, otherwise the final number flashes for one frame and then jumps back to 0.
|
|
useSafeLayoutEffect(() => {
|
|
if (!run) {
|
|
setProgress(1)
|
|
return
|
|
}
|
|
|
|
setProgress(0)
|
|
let start: number | null = null
|
|
|
|
const tick = (now: number) => {
|
|
if (start === null) start = now
|
|
const t = Math.min(1, Math.max(0, (now - start - delay) / duration))
|
|
setProgress(t)
|
|
if (t < 1) frame.current = requestAnimationFrame(tick)
|
|
}
|
|
frame.current = requestAnimationFrame(tick)
|
|
|
|
return () => {
|
|
if (frame.current !== null) cancelAnimationFrame(frame.current)
|
|
}
|
|
}, [run, target, duration, delay])
|
|
|
|
return {value: Math.round(target * easeOutExpo(progress)), progress}
|
|
}
|