diff --git a/src/components/Topbar.tsx b/src/components/Topbar.tsx index 23e8390..2c6c258 100644 --- a/src/components/Topbar.tsx +++ b/src/components/Topbar.tsx @@ -1,4 +1,6 @@ import type Decimal from 'break_eternity.js'; +import { useEffect, useRef } from 'react'; +import type { PointerEvent } from 'react'; interface Props { time: Decimal; @@ -7,7 +9,47 @@ interface Props { onInitialize: () => void; } +const HOLD_TICK_INTERVAL_MS = 1000; +const MIN_TICK_INTERVAL_MS = 50; + export default function Topbar({ time, onTick, onReset, onInitialize }: Props) { + const timeoutRef = useRef | null>(null); + // Mutable, not React state: doTick reschedules itself via setTimeout, so it must always read + // and write the CURRENT speed directly — a value captured in a render's closure (e.g. via + // useState) would stay frozen for the lifetime of that closure, since nothing ever forces + // doTick to be re-created from a fresh render while a hold is in progress. + const speedRef = useRef(HOLD_TICK_INTERVAL_MS); + + function stopHolding() { + if (timeoutRef.current !== null) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + speedRef.current = HOLD_TICK_INTERVAL_MS; + } + + function doTick() { + onTick(); + speedRef.current = Math.max(MIN_TICK_INTERVAL_MS, Math.pow(speedRef.current, 0.97)); + timeoutRef.current = setTimeout(doTick, speedRef.current); + } + + function startHolding(e: PointerEvent) { + if (e.button !== 0) + return; + e.currentTarget.setPointerCapture(e.pointerId); + stopHolding(); + doTick(); + } + + useEffect(() => { + window.addEventListener('blur', stopHolding); + return () => { + window.removeEventListener('blur', stopHolding); + stopHolding(); + }; + }, []); + return (

@@ -24,7 +66,12 @@ export default function Topbar({ time, onTick, onReset, onInitialize }: Props) { -