80 lines
2.3 KiB
TypeScript
80 lines
2.3 KiB
TypeScript
import type Decimal from 'break_eternity.js';
|
|
import { useEffect, useRef } from 'react';
|
|
import type { PointerEvent } from 'react';
|
|
|
|
interface Props {
|
|
time: Decimal;
|
|
onTick: () => void;
|
|
onReset: () => void;
|
|
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<ReturnType<typeof setTimeout> | 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<HTMLButtonElement>) {
|
|
if (e.button !== 0)
|
|
return;
|
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
stopHolding();
|
|
doTick();
|
|
}
|
|
|
|
useEffect(() => {
|
|
window.addEventListener('blur', stopHolding);
|
|
return () => {
|
|
window.removeEventListener('blur', stopHolding);
|
|
stopHolding();
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<div className="topbar">
|
|
<h1 className="topbar__title">
|
|
Граф ресурсов
|
|
<span>детерминированная тиковая симуляция</span>
|
|
</h1>
|
|
<div className="tick-counter">
|
|
Время <b>{time.toStringWithDecimalPlaces(4)}</b>
|
|
</div>
|
|
<div className="spacer" />
|
|
<button className="btn" onClick={onInitialize}>
|
|
Initialize
|
|
</button>
|
|
<button className="btn" onClick={onReset}>
|
|
Reset
|
|
</button>
|
|
<button
|
|
className="btn btn--primary"
|
|
onPointerDown={startHolding}
|
|
onPointerUp={stopHolding}
|
|
onPointerCancel={stopHolding}
|
|
>
|
|
Tick ▶
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|