1
0
Fork 0

Adjustments based on feedback

This commit is contained in:
Atridad Lahiji 2025-03-24 23:47:40 -06:00
parent d11f6d10d9
commit 7081aa7bca
Signed by: atridad
SSH key fingerprint: SHA256:LGomp8Opq0jz+7kbwNcdfTcuaLRb5Nh0k5AchDDb438
5 changed files with 242 additions and 94 deletions

View file

@ -40,25 +40,31 @@
.content {
position: absolute;
width: 10000px;
/* Match VIRTUAL_CANVAS_SIZE */
height: 10000px;
width: calc(100vw / 0.01); /* Width based on minimum zoom (0.01) */
height: calc(100vh / 0.01); /* Height based on minimum zoom (0.01) */
transform-origin: 0 0;
background-color: white;
will-change: transform;
contain: layout paint;
}
.target {
position: absolute;
background-color: #888888;
background-color: #444444; /* Darker gray for better visibility */
border-radius: 50%;
transition: background-color 0.2s ease;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
box-shadow: 0 0 15px rgba(0, 0, 0, 0.3);
transform: translate(-50%, -50%);
will-change: transform;
contain: layout;
border: 4px solid rgba(0, 0, 0, 0.4); /* Darker border for better contrast */
}
.target.active {
background-color: #44aa44;
box-shadow: 0 0 20px rgba(68, 170, 68, 0.4);
background-color: #22aa22; /* Darker green for better visibility */
box-shadow: 0 0 30px rgba(34, 170, 34, 0.8);
border: 4px solid white;
z-index: 10;
}
html,
@ -272,3 +278,88 @@ body,
opacity: 1;
}
}
/* Preset Indicator Styles */
.preset-indicator {
position: fixed;
top: 50%;
right: 30px;
transform: translateY(-50%);
z-index: 1000;
font-size: 24px;
font-weight: bold;
font-family: Inter, system-ui, Arial, sans-serif;
padding: 20px;
border-radius: 12px;
color: white;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
text-align: center;
transition: background-color 0.3s ease;
}
.preset-indicator.preset-A {
background-color: #4285f4; /* Blue for Preset A */
}
.preset-indicator.preset-B {
background-color: #34a853; /* Green for Preset B */
}
.preset-indicator.preset-C {
background-color: #ea4335; /* Red for Preset C */
}
.preset-indicator.preset-D {
background-color: #fbbc05; /* Yellow for Preset D */
}
/* Preset Transition Effect */
.preset-transition-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 1500;
pointer-events: none;
display: flex;
justify-content: center;
align-items: center;
animation: fadeInOut 1.5s ease-in-out;
opacity: 0;
}
.preset-transition-content {
font-size: 36px;
font-weight: bold;
color: white;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
padding: 30px 60px;
border-radius: 16px;
text-align: center;
transition: background-color 0.3s ease;
}
.preset-transition-overlay.preset-A .preset-transition-content {
background-color: rgba(66, 133, 244, 0.85); /* Blue for Preset A */
}
.preset-transition-overlay.preset-B .preset-transition-content {
background-color: rgba(52, 168, 83, 0.85); /* Green for Preset B */
}
.preset-transition-overlay.preset-C .preset-transition-content {
background-color: rgba(234, 67, 53, 0.85); /* Red for Preset C */
}
.preset-transition-overlay.preset-D .preset-transition-content {
background-color: rgba(251, 188, 5, 0.85); /* Yellow for Preset D */
}
@keyframes fadeInOut {
0% { opacity: 0; }
30% { opacity: 1; }
70% { opacity: 1; }
100% { opacity: 0; }
}

View file

@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect } from 'react';
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
import { Settings } from './components/Settings';
import { RoundsHistory } from './components/RoundsHistory';
import { Instructions } from './components/Instructions';
@ -65,7 +65,7 @@ function App() {
y: 0,
});
// Interaction tracking refs
// Refs for interaction tracking
const isDragging = useRef(false);
const dragStartTime = useRef(0);
const dragStartPos = useRef({ x: 0, y: 0 });
@ -75,6 +75,11 @@ function App() {
const zoomSpeedSamples = useRef<number[]>([]);
const touchStartDistance = useRef<number>(0);
const wasReset = useRef(false);
const resizeTimerRef = useRef<number | undefined>(undefined);
// Add state for tracking preset transitions
const [showPresetTransition, setShowPresetTransition] = useState(false);
const [transitionPreset, setTransitionPreset] = useState<ZoomPreset | null>(null);
// Load saved state on initial mount
useEffect(() => {
@ -104,7 +109,8 @@ function App() {
setTargets(newTargets);
setCurrentTarget(getRandomTarget(null, CONSTANTS.TARGET_COUNT));
const initialZoom = 0.2;
// Initialize at minimum zoom to show all targets
const initialZoom = CONSTANTS.ZOOM_LEVELS.min;
setZoomLevel(initialZoom);
setViewportOffset(calculateInitialViewport(initialZoom));
setStats(createInitialStats());
@ -126,7 +132,7 @@ function App() {
}
}, [currentPreset]);
// Progress through presets in study mode
// Modify the useEffect for preset progression to show the transition
useEffect(() => {
if (mode !== 'study') return;
@ -139,9 +145,17 @@ function App() {
const currentIndex = allPresets.indexOf(currentPreset);
if (currentIndex < allPresets.length - 1) {
// Show the transition to the next preset
const nextPreset = allPresets[currentIndex + 1];
setCurrentPreset(nextPreset);
setTargetCount(0);
setTransitionPreset(nextPreset);
setShowPresetTransition(true);
// Hide the transition after animation completes
setTimeout(() => {
setShowPresetTransition(false);
setCurrentPreset(nextPreset);
setTargetCount(0);
}, 1500); // Match the animation duration
} else {
setStudyComplete(true);
}
@ -402,32 +416,65 @@ function App() {
// Regenerate targets when window is resized
useEffect(() => {
let resizeTimer: number;
let lastWidth = window.innerWidth;
let lastHeight = window.innerHeight;
const handleResize = () => {
if (mode !== 'study') {
clearTimeout(resizeTimer);
// Clear any existing timer
if (resizeTimerRef.current) {
window.clearTimeout(resizeTimerRef.current);
}
resizeTimer = window.setTimeout(() => {
const newTargets = generateTargets();
setTargets(newTargets);
// Only regenerate if the size changed significantly
const widthDiff = Math.abs(window.innerWidth - lastWidth);
const heightDiff = Math.abs(window.innerHeight - lastHeight);
const significantChange = widthDiff > 50 || heightDiff > 50;
if (currentTarget !== null) {
setCurrentTarget(getRandomTarget(currentTarget, CONSTANTS.TARGET_COUNT));
}
if (significantChange) {
resizeTimerRef.current = window.setTimeout(() => {
lastWidth = window.innerWidth;
lastHeight = window.innerHeight;
setShowResizeWarning(true);
}, 500); // Delay to detect when resizing is complete
const newTargets = generateTargets();
setTargets(newTargets);
if (currentTarget !== null) {
setCurrentTarget(getRandomTarget(currentTarget, CONSTANTS.TARGET_COUNT));
}
setShowResizeWarning(true);
}, 800); // Longer debounce to prevent excessive regeneration
}
}
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
clearTimeout(resizeTimer);
if (resizeTimerRef.current) {
window.clearTimeout(resizeTimerRef.current);
}
};
}, [currentTarget, mode]);
// Simplified target rendering that doesn't filter by viewport to ensure all targets are visible
const renderedTargets = useMemo(() => {
return targets.map((target, index) => (
<div
key={`target-${index}`}
className={`target ${index === currentTarget ? 'active' : ''}`}
style={{
left: `${target.x}px`,
top: `${target.y}px`,
width: `${target.radius * 2}px`,
height: `${target.radius * 2}px`,
willChange: 'transform',
}}
/>
));
}, [targets, currentTarget]);
return (
<div className="app">
<div className="stats">
@ -448,6 +495,24 @@ function App() {
)}
</div>
{/* Add persistent preset indicator when in study mode */}
{mode === 'study' && (
<div className={`preset-indicator preset-${currentPreset}`}>
<div>Preset</div>
<div style={{ fontSize: '36px' }}>{currentPreset}</div>
<div>{targetCount}/{CONSTANTS.STUDY_TARGETS_PER_PRESET}</div>
</div>
)}
{/* Add preset transition overlay */}
{showPresetTransition && transitionPreset && (
<div className={`preset-transition-overlay preset-${transitionPreset}`}>
<div className="preset-transition-content">
Switching to Preset {transitionPreset}
</div>
</div>
)}
<div
ref={containerRef}
className="container"
@ -466,18 +531,7 @@ function App() {
transformOrigin: '0 0',
}}
>
{targets.map((target, index) => (
<div
key={index}
className={`target ${index === currentTarget ? 'active' : ''}`}
style={{
left: `${target.x}px`,
top: `${target.y}px`,
width: `${target.radius * 2}px`,
height: `${target.radius * 2}px`,
}}
/>
))}
{renderedTargets}
</div>
</div>

View file

@ -43,21 +43,21 @@ export interface Round {
export const CONSTANTS = {
TARGET_COUNT: 200,
TARGET_MIN_RADIUS: 100,
TARGET_MAX_RADIUS: 800,
TARGET_MIN_RADIUS: 20,
TARGET_MAX_RADIUS: 40,
VIRTUAL_CANVAS_SIZE: 50000,
ZOOM_LEVELS: {
min: 0.01,
max: 10,
default: 0.2,
default: 0.01,
},
MOUSE_WHEEL_SAMPLES: 5,
NUM_CURVE_POINTS: 5,
STUDY_TARGETS_PER_PRESET: 25,
PRESETS: {
A: [1, 1, 1, 1, 1], // Control: no acceleration
B: [1, 2, 3, 4, 5], // Linear increase
C: [1, 1.5, 2.5, 4, 6], // Slow start, fast end
A: [1, 2, 3, 4, 5], // Linear increase
B: [1, 1.5, 2.5, 4, 6], // Slow start, fast end
C: [3, 2.5, 2, 1.5, 1], // Inverse/decreasing
D: [1, 3, 4, 3, 1], // Bell curve
} as Record<ZoomPreset, number[]>,
} as const;

View file

@ -8,10 +8,22 @@ export const calculateZoomDelta = (
baseMultiplier = 0.001
): number => clamp(wheelDelta * baseMultiplier * accelerationFactor, -0.5, 0.5);
export const calculateInitialViewport = (initialZoom: number): ViewportOffset => ({
x: (CONSTANTS.VIRTUAL_CANVAS_SIZE - window.innerWidth / initialZoom) / 2,
y: (CONSTANTS.VIRTUAL_CANVAS_SIZE - window.innerHeight / initialZoom) / 2,
});
export const calculateInitialViewport = (initialZoom: number): ViewportOffset => {
// When fully zoomed out, we want to see the entire content area
// The target distribution is sized to match the viewport at minimum zoom
// So at any higher zoom level, we need to position appropriately
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// Center the viewport on the target area
const areaWidth = windowWidth / CONSTANTS.ZOOM_LEVELS.min;
const areaHeight = windowHeight / CONSTANTS.ZOOM_LEVELS.min;
return {
x: (areaWidth - windowWidth / initialZoom) / 2,
y: (areaHeight - windowHeight / initialZoom) / 2
};
};
export const clampZoom = (zoom: number): number =>
clamp(zoom, CONSTANTS.ZOOM_LEVELS.min, CONSTANTS.ZOOM_LEVELS.max);

View file

@ -2,59 +2,50 @@ import { Target, CONSTANTS } from '../types';
export const generateTargets = () => {
const newTargets: Target[] = [];
const PADDING = CONSTANTS.TARGET_MAX_RADIUS * 2;
// Get browser window aspect ratio
// Fixed target size for better visibility
const MIN_RADIUS = 100;
const MAX_RADIUS = 600;
// Get browser window dimensions
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const aspectRatio = windowWidth / windowHeight;
// Calculate grid dimensions based on aspect ratio
const gridWidth = CONSTANTS.VIRTUAL_CANVAS_SIZE;
const gridHeight = gridWidth / aspectRatio;
// Base the target area on the minimum zoom level
// This ensures when fully zoomed out, we see the entire grid
const minZoom = CONSTANTS.ZOOM_LEVELS.min;
const areaWidth = windowWidth / minZoom;
const areaHeight = windowHeight / minZoom;
// Calculate cells based on aspect ratio
const totalCells = CONSTANTS.TARGET_COUNT;
const numColumns = Math.ceil(Math.sqrt(totalCells * aspectRatio));
const numRows = Math.ceil(totalCells / numColumns);
const cellWidth = (gridWidth - PADDING * 2) / numColumns;
const cellHeight = (gridHeight - PADDING * 2) / numRows;
// Create a grid-like distribution
const gridSize = Math.ceil(Math.sqrt(CONSTANTS.TARGET_COUNT));
const cellWidth = areaWidth / gridSize;
const cellHeight = areaHeight / gridSize;
for (let i = 0; i < CONSTANTS.TARGET_COUNT; i++) {
let validPosition = false;
let newTarget: Target;
// Calculate grid position
const gridX = i % gridSize;
const gridY = Math.floor(i / gridSize);
// Calculate the grid position
const column = i % numColumns;
const row = Math.floor(i / numColumns);
// Add some randomness within each grid cell
const randomOffsetX = (Math.random() - 0.5) * cellWidth * 0.6;
const randomOffsetY = (Math.random() - 0.5) * cellHeight * 0.6;
while (!validPosition) {
const radius = CONSTANTS.TARGET_MIN_RADIUS +
Math.random() * (CONSTANTS.TARGET_MAX_RADIUS - CONSTANTS.TARGET_MIN_RADIUS);
// Calculate final position
const x = cellWidth * (gridX + 0.5) + randomOffsetX;
const y = cellHeight * (gridY + 0.5) + randomOffsetY;
// Base position from grid
const baseX = PADDING + column * cellWidth;
const baseY = PADDING + row * cellHeight;
// Random radius
const radius = MIN_RADIUS + Math.random() * (MAX_RADIUS - MIN_RADIUS);
// Add some randomness within the cell
const x = baseX + Math.random() * (cellWidth - radius * 2);
const y = baseY + Math.random() * (cellHeight - radius * 2);
newTarget = { x, y, radius };
validPosition = true;
// Check for overlaps
for (const existing of newTargets) {
const distance = Math.hypot(existing.x - x, existing.y - y);
if (distance < (existing.radius + radius) * 1.5) {
validPosition = false;
break;
}
}
}
newTargets.push(newTarget!);
// Add the target to the array
newTargets.push({
x,
y,
radius
});
}
return newTargets;
};