UNPKG

@wandelbots/wandelbots-js-react-components

Version:

React UI toolkit for building applications on top of the Wandelbots platform

215 lines (191 loc) • 5.88 kB
import { useCallback, useEffect, useRef, useState } from "react" import { useInterpolation } from "../utils/interpolation" import type { TimerState } from "./types" interface UseTimerLogicProps { autoStart: boolean hasError: boolean onPauseAnimation: () => void onErrorAnimation: () => void onClearErrorAnimation: () => void } export const useTimerLogic = ({ autoStart, hasError, onPauseAnimation, onErrorAnimation, onClearErrorAnimation, }: UseTimerLogicProps) => { const [timerState, setTimerState] = useState<TimerState>({ elapsedTime: 0, isRunning: false, isPausedState: false, currentProgress: 0, wasRunningBeforeError: false, }) // Timer-related refs const animationRef = useRef<number | null>(null) const startTimeRef = useRef<number | null>(null) const pausedTimeRef = useRef<number>(0) const lastProgressRef = useRef<number>(0) // Spring-based interpolator for smooth gauge progress animations const [progressInterpolator] = useInterpolation([0], { tension: 80, friction: 18, onChange: ([progress]) => { setTimerState((prev) => ({ ...prev, currentProgress: progress })) }, }) const start = useCallback( (elapsedSeconds: number = 0) => { const initialProgress = ((elapsedSeconds / 60) % 1) * 100 setTimerState((prev) => ({ ...prev, elapsedTime: elapsedSeconds, isPausedState: false, currentProgress: initialProgress, })) pausedTimeRef.current = 0 lastProgressRef.current = initialProgress progressInterpolator.setImmediate([initialProgress]) if (autoStart) { startTimeRef.current = Date.now() - elapsedSeconds * 1000 setTimerState((prev) => ({ ...prev, isRunning: true })) } else { startTimeRef.current = null } }, [autoStart, progressInterpolator], ) const pause = useCallback(() => { if (startTimeRef.current && timerState.isRunning) { const now = Date.now() const totalElapsed = (now - startTimeRef.current) / 1000 + pausedTimeRef.current const currentProgress = ((totalElapsed / 60) % 1) * 100 progressInterpolator.setTarget([currentProgress]) setTimerState((prev) => ({ ...prev, elapsedTime: Math.floor(totalElapsed), })) } setTimerState((prev) => ({ ...prev, isRunning: false, isPausedState: true, })) onPauseAnimation() }, [ timerState.isRunning, progressInterpolator, onPauseAnimation, ]) const resume = useCallback(() => { if (timerState.isPausedState) { pausedTimeRef.current = timerState.elapsedTime startTimeRef.current = Date.now() setTimerState((prev) => ({ ...prev, isRunning: true, isPausedState: false, })) } }, [timerState.isPausedState, timerState.elapsedTime]) const reset = useCallback(() => { setTimerState((prev) => ({ ...prev, elapsedTime: 0, isRunning: false, isPausedState: false, currentProgress: 0, })) pausedTimeRef.current = 0 startTimeRef.current = null lastProgressRef.current = 0 progressInterpolator.setImmediate([0]) }, [progressInterpolator]) const isPaused = useCallback(() => { return timerState.isPausedState }, [timerState.isPausedState]) // Handle error state changes useEffect(() => { if (hasError) { if (timerState.isRunning) { setTimerState((prev) => ({ ...prev, wasRunningBeforeError: true })) pause() } onErrorAnimation() } else { if (timerState.wasRunningBeforeError && !timerState.isRunning) { setTimerState((prev) => ({ ...prev, wasRunningBeforeError: false })) resume() } onClearErrorAnimation() } }, [ hasError, timerState.isRunning, timerState.wasRunningBeforeError, pause, resume, onErrorAnimation, onClearErrorAnimation, ]) // Main timer loop useEffect(() => { if (timerState.isRunning) { const updateTimer = () => { if (startTimeRef.current) { const now = Date.now() const totalElapsed = (now - startTimeRef.current) / 1000 + pausedTimeRef.current const currentProgress = ((totalElapsed / 60) % 1) * 100 setTimerState((prev) => ({ ...prev, elapsedTime: Math.floor(totalElapsed), })) // Only update progress interpolator if progress changed significantly const progressDiff = Math.abs(currentProgress - lastProgressRef.current) if (progressDiff > 0.1) { progressInterpolator.setTarget([currentProgress]) lastProgressRef.current = currentProgress } } animationRef.current = requestAnimationFrame(updateTimer) } animationRef.current = requestAnimationFrame(updateTimer) } else { if (animationRef.current) { cancelAnimationFrame(animationRef.current) animationRef.current = null } } return () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current) animationRef.current = null } } }, [timerState.isRunning, progressInterpolator]) // Interpolation animation loop useEffect(() => { let interpolationAnimationId: number | null = null const animateInterpolation = () => { progressInterpolator.update() interpolationAnimationId = requestAnimationFrame(animateInterpolation) } interpolationAnimationId = requestAnimationFrame(animateInterpolation) return () => { if (interpolationAnimationId) { cancelAnimationFrame(interpolationAnimationId) } } }, [progressInterpolator]) return { timerState, controls: { start, pause, resume, reset, isPaused, }, } }