1
0
Fork 0
This commit is contained in:
Atridad Lahiji 2025-03-24 21:25:58 -06:00
parent b4a104a860
commit 38f9b7a33a
Signed by: atridad
SSH key fingerprint: SHA256:LGomp8Opq0jz+7kbwNcdfTcuaLRb5Nh0k5AchDDb438

View file

@ -31,20 +31,19 @@ import { clamp } from './utils/math';
import './App.css'; import './App.css';
function App() { function App() {
// Core state // Simulation state
const [targets, setTargets] = useState<Target[]>([]); const [targets, setTargets] = useState<Target[]>([]);
const [currentTarget, setCurrentTarget] = useState<number | null>(null); const [currentTarget, setCurrentTarget] = useState<number | null>(null);
const [zoomLevel, setZoomLevel] = useState<number>(CONSTANTS.ZOOM_LEVELS.default); const [zoomLevel, setZoomLevel] = useState<number>(CONSTANTS.ZOOM_LEVELS.default);
const [stats, setStats] = useState<Stats>(createInitialStats()); const [stats, setStats] = useState<Stats>(createInitialStats());
// Study mode state // Experiment mode configuration
const [mode, setMode] = useState<AppMode>('freestyle'); const [mode, setMode] = useState<AppMode>('freestyle');
const [currentPreset, setCurrentPreset] = useState<ZoomPreset>('A'); const [currentPreset, setCurrentPreset] = useState<ZoomPreset>('A');
const [presetsCompleted, setPresetsCompleted] = useState<ZoomPreset[]>([]); const [presetsCompleted, setPresetsCompleted] = useState<ZoomPreset[]>([]);
const [targetCount, setTargetCount] = useState(0); const [targetCount, setTargetCount] = useState(0);
const [studyComplete, setStudyComplete] = useState(false); const [studyComplete, setStudyComplete] = useState(false);
// Load settings from localStorage or use defaults
const [settings, setSettings] = useState<AccelerationSettings>(() => { const [settings, setSettings] = useState<AccelerationSettings>(() => {
const savedState = loadState(); const savedState = loadState();
const defaultSettings = createDefaultSettings(); const defaultSettings = createDefaultSettings();
@ -56,30 +55,28 @@ function App() {
return savedState?.rounds ?? []; return savedState?.rounds ?? [];
}); });
// UI state // UI visibility states
const [showSettings, setShowSettings] = useState(false); const [showSettings, setShowSettings] = useState(false);
const [showRounds, setShowRounds] = useState(false); const [showRounds, setShowRounds] = useState(false);
const [showInstructions, setShowInstructions] = useState(false); const [showInstructions, setShowInstructions] = useState(false);
const [showResizeWarning, setShowResizeWarning] = useState(false);
const [viewportOffset, setViewportOffset] = useState<ViewportOffset>({ const [viewportOffset, setViewportOffset] = useState<ViewportOffset>({
x: 0, x: 0,
y: 0, y: 0,
}); });
// Add state to track drag operations // Interaction tracking refs
const isDragging = useRef(false); const isDragging = useRef(false);
const dragStartTime = useRef(0); const dragStartTime = useRef(0);
const dragStartPos = useRef({ x: 0, y: 0 }); const dragStartPos = useRef({ x: 0, y: 0 });
const DRAG_THRESHOLD = 5; // pixels const DRAG_THRESHOLD = 5; // pixels
// Refs for handling zoom behavior
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const zoomSpeedSamples = useRef<number[]>([]); const zoomSpeedSamples = useRef<number[]>([]);
const touchStartDistance = useRef<number>(0); const touchStartDistance = useRef<number>(0);
const wasReset = useRef(false); const wasReset = useRef(false);
// Add a new state for the resize warning popup // Load saved state on initial mount
const [showResizeWarning, setShowResizeWarning] = useState(false);
useEffect(() => { useEffect(() => {
const savedState = loadState(); const savedState = loadState();
if (savedState?.stats) { if (savedState?.stats) {
@ -88,6 +85,7 @@ function App() {
setShowInstructions(!savedState?.rounds || savedState.rounds.length === 0); setShowInstructions(!savedState?.rounds || savedState.rounds.length === 0);
}, []); }, []);
// Persist state when it changes
useEffect(() => { useEffect(() => {
saveState({ saveState({
rounds, rounds,
@ -96,6 +94,7 @@ function App() {
}); });
}, [rounds, stats, settings]); }, [rounds, stats, settings]);
// Initialize the simulation on first load
useEffect(() => { useEffect(() => {
initializeGame(); initializeGame();
}, []); }, []);
@ -115,7 +114,7 @@ function App() {
setStudyComplete(false); setStudyComplete(false);
}; };
// Apply preset when it changes // Configure settings when preset changes
useEffect(() => { useEffect(() => {
if (currentPreset) { if (currentPreset) {
setSettings(prev => ({ setSettings(prev => ({
@ -127,32 +126,29 @@ function App() {
} }
}, [currentPreset]); }, [currentPreset]);
// Handle moving to next preset in study mode // Progress through presets in study mode
useEffect(() => { useEffect(() => {
if (mode !== 'study') return; if (mode !== 'study') return;
if (targetCount >= CONSTANTS.STUDY_TARGETS_PER_PRESET) { if (targetCount >= CONSTANTS.STUDY_TARGETS_PER_PRESET) {
// Add current preset to completed list
if (!presetsCompleted.includes(currentPreset)) { if (!presetsCompleted.includes(currentPreset)) {
setPresetsCompleted(prev => [...prev, currentPreset]); setPresetsCompleted(prev => [...prev, currentPreset]);
} }
// Directly move to the next preset without showing break screen
const allPresets: ZoomPreset[] = ['A', 'B', 'C', 'D']; const allPresets: ZoomPreset[] = ['A', 'B', 'C', 'D'];
const currentIndex = allPresets.indexOf(currentPreset); const currentIndex = allPresets.indexOf(currentPreset);
if (currentIndex < allPresets.length - 1) { if (currentIndex < allPresets.length - 1) {
// Move to next preset
const nextPreset = allPresets[currentIndex + 1]; const nextPreset = allPresets[currentIndex + 1];
setCurrentPreset(nextPreset); setCurrentPreset(nextPreset);
setTargetCount(0); setTargetCount(0);
} else { } else {
// All presets completed
setStudyComplete(true); setStudyComplete(true);
} }
} }
}, [targetCount, currentPreset, mode, presetsCompleted]); }, [targetCount, currentPreset, mode, presetsCompleted]);
// Handle zoom wheel events and track timing data
useEffect(() => { useEffect(() => {
const container = containerRef.current; const container = containerRef.current;
if (!container) return; if (!container) return;
@ -171,15 +167,12 @@ function App() {
const scrollAmount = processWheelDelta(-event.deltaY); const scrollAmount = processWheelDelta(-event.deltaY);
const now = Date.now(); const now = Date.now();
// Determine zoom direction (positive scrollAmount = zoom in, negative = zoom out)
const direction = scrollAmount > 0 ? 'in' : 'out'; const direction = scrollAmount > 0 ? 'in' : 'out';
// Update zoom timing stats
setStats(previous => { setStats(previous => {
// If we're already zooming in the same direction, add to the time
if (previous.isZooming && previous.zoomDirection === direction && previous.lastZoomTime) { if (previous.isZooming && previous.zoomDirection === direction && previous.lastZoomTime) {
const timeDelta = now - previous.lastZoomTime; const timeDelta = now - previous.lastZoomTime;
// Only count if it's a reasonable time (less than 500ms between events) // Only accumulate time if events are close together (continuous zooming)
const shouldCount = timeDelta < 500; const shouldCount = timeDelta < 500;
return { return {
@ -191,7 +184,6 @@ function App() {
}; };
} }
// Otherwise start new zoom tracking
return { return {
...previous, ...previous,
isZooming: true, isZooming: true,
@ -233,7 +225,6 @@ function App() {
} }
}; };
// Stop zoom timing when wheel events stop
const handleZoomEnd = () => { const handleZoomEnd = () => {
setStats(previous => ({ setStats(previous => ({
...previous, ...previous,
@ -245,7 +236,7 @@ function App() {
container.addEventListener('wheel', handleWheelEvent, { passive: false }); container.addEventListener('wheel', handleWheelEvent, { passive: false });
// Add a listener to detect when zooming stops (after 300ms of no wheel events) // Detect when zooming stops after a brief pause
let zoomEndTimer: number; let zoomEndTimer: number;
container.addEventListener('wheel', () => { container.addEventListener('wheel', () => {
clearTimeout(zoomEndTimer); clearTimeout(zoomEndTimer);
@ -334,7 +325,6 @@ function App() {
if (distanceToTarget <= target.radius) { if (distanceToTarget <= target.radius) {
const timeElapsed = Date.now() - stats.startTime; const timeElapsed = Date.now() - stats.startTime;
// Create and save the round with zoom timing data
const newRound = createNewRound( const newRound = createNewRound(
timeElapsed, timeElapsed,
stats.misclicks, stats.misclicks,
@ -348,7 +338,6 @@ function App() {
setRounds(previous => [...previous, newRound]); setRounds(previous => [...previous, newRound]);
// Update stats and reset zoom timing data
setStats({ setStats({
startTime: Date.now(), startTime: Date.now(),
misclicks: 0, misclicks: 0,
@ -361,10 +350,8 @@ function App() {
zoomDirection: null zoomDirection: null
}); });
// Set next target
setCurrentTarget(getRandomTarget(currentTarget, CONSTANTS.TARGET_COUNT)); setCurrentTarget(getRandomTarget(currentTarget, CONSTANTS.TARGET_COUNT));
// If in study mode, increment target count
if (mode === 'study') { if (mode === 'study') {
setTargetCount(prev => prev + 1); setTargetCount(prev => prev + 1);
} }
@ -384,24 +371,19 @@ function App() {
wasReset.current = true; wasReset.current = true;
initializeGame(); initializeGame();
// Close any open panels
setShowSettings(false); setShowSettings(false);
setShowRounds(false); setShowRounds(false);
// Show the initial instructions screen
setShowInstructions(true); setShowInstructions(true);
} }
}; };
const handleModeChange = (newMode: AppMode) => { const handleModeChange = (newMode: AppMode) => {
// Reset data when changing modes
if (newMode !== mode) { if (newMode !== mode) {
if (window.confirm('Changing modes will reset your current data. Continue?')) { if (window.confirm('Changing modes will reset your current data. Continue?')) {
setMode(newMode); setMode(newMode);
setRounds([]); setRounds([]);
setStats(createInitialStats()); setStats(createInitialStats());
// Reset study mode stats when changing modes
if (newMode === 'study') { if (newMode === 'study') {
setTargetCount(0); setTargetCount(0);
setPresetsCompleted([]); setPresetsCompleted([]);
@ -409,7 +391,6 @@ function App() {
setStudyComplete(false); setStudyComplete(false);
} }
// Reset the game
initializeGame(); initializeGame();
} }
} else { } else {
@ -417,15 +398,13 @@ function App() {
} }
}; };
// Calculate average time
const averageTime = calculateAverageTime(stats.times); const averageTime = calculateAverageTime(stats.times);
// Add a resize handler to regenerate targets when window size changes // Regenerate targets when window is resized
useEffect(() => { useEffect(() => {
let resizeTimer: number; let resizeTimer: number;
const handleResize = () => { const handleResize = () => {
// Don't regenerate if in study mode to avoid disrupting the test
if (mode !== 'study') { if (mode !== 'study') {
clearTimeout(resizeTimer); clearTimeout(resizeTimer);
@ -433,14 +412,12 @@ function App() {
const newTargets = generateTargets(); const newTargets = generateTargets();
setTargets(newTargets); setTargets(newTargets);
// If we're targeting something, make sure it's still valid
if (currentTarget !== null) { if (currentTarget !== null) {
setCurrentTarget(getRandomTarget(currentTarget, CONSTANTS.TARGET_COUNT)); setCurrentTarget(getRandomTarget(currentTarget, CONSTANTS.TARGET_COUNT));
} }
// Show resize warning
setShowResizeWarning(true); setShowResizeWarning(true);
}, 500); // Add a delay to detect when resizing is "done" }, 500); // Delay to detect when resizing is complete
} }
}; };
@ -569,7 +546,6 @@ function App() {
</div> </div>
)} )}
{/* Resize warning popup */}
{showResizeWarning && ( {showResizeWarning && (
<div className="resize-warning"> <div className="resize-warning">
<div className="resize-warning-content"> <div className="resize-warning-content">