Adjustments based on feedback
This commit is contained in:
parent
d11f6d10d9
commit
7081aa7bca
5 changed files with 242 additions and 94 deletions
105
src/App.css
105
src/App.css
|
@ -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,
|
||||
|
@ -271,4 +277,89 @@ body,
|
|||
to {
|
||||
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; }
|
||||
}
|
116
src/App.tsx
116
src/App.tsx
|
@ -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);
|
||||
|
||||
if (currentTarget !== null) {
|
||||
setCurrentTarget(getRandomTarget(currentTarget, CONSTANTS.TARGET_COUNT));
|
||||
}
|
||||
|
||||
setShowResizeWarning(true);
|
||||
}, 500); // Delay to detect when resizing is complete
|
||||
// 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 (significantChange) {
|
||||
resizeTimerRef.current = window.setTimeout(() => {
|
||||
lastWidth = window.innerWidth;
|
||||
lastHeight = window.innerHeight;
|
||||
|
||||
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>
|
||||
|
||||
|
|
12
src/types.ts
12
src/types.ts
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
// Create a grid-like distribution
|
||||
const gridSize = Math.ceil(Math.sqrt(CONSTANTS.TARGET_COUNT));
|
||||
const cellWidth = areaWidth / gridSize;
|
||||
const cellHeight = areaHeight / gridSize;
|
||||
|
||||
const cellWidth = (gridWidth - PADDING * 2) / numColumns;
|
||||
const cellHeight = (gridHeight - PADDING * 2) / numRows;
|
||||
|
||||
for (let i = 0; i < CONSTANTS.TARGET_COUNT; i++) {
|
||||
let validPosition = false;
|
||||
let newTarget: Target;
|
||||
|
||||
// Calculate the grid position
|
||||
const column = i % numColumns;
|
||||
const row = Math.floor(i / numColumns);
|
||||
|
||||
while (!validPosition) {
|
||||
const radius = CONSTANTS.TARGET_MIN_RADIUS +
|
||||
Math.random() * (CONSTANTS.TARGET_MAX_RADIUS - CONSTANTS.TARGET_MIN_RADIUS);
|
||||
|
||||
// Base position from grid
|
||||
const baseX = PADDING + column * cellWidth;
|
||||
const baseY = PADDING + row * cellHeight;
|
||||
|
||||
// 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!);
|
||||
// Calculate grid position
|
||||
const gridX = i % gridSize;
|
||||
const gridY = Math.floor(i / gridSize);
|
||||
|
||||
// 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;
|
||||
|
||||
// Calculate final position
|
||||
const x = cellWidth * (gridX + 0.5) + randomOffsetX;
|
||||
const y = cellHeight * (gridY + 0.5) + randomOffsetY;
|
||||
|
||||
// Random radius
|
||||
const radius = MIN_RADIUS + Math.random() * (MAX_RADIUS - MIN_RADIUS);
|
||||
|
||||
// Add the target to the array
|
||||
newTargets.push({
|
||||
x,
|
||||
y,
|
||||
radius
|
||||
});
|
||||
}
|
||||
|
||||
return newTargets;
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue