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