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 {
|
.content {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 10000px;
|
width: calc(100vw / 0.01); /* Width based on minimum zoom (0.01) */
|
||||||
/* Match VIRTUAL_CANVAS_SIZE */
|
height: calc(100vh / 0.01); /* Height based on minimum zoom (0.01) */
|
||||||
height: 10000px;
|
|
||||||
transform-origin: 0 0;
|
transform-origin: 0 0;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
will-change: transform;
|
||||||
|
contain: layout paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
.target {
|
.target {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: #888888;
|
background-color: #444444; /* Darker gray for better visibility */
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transition: background-color 0.2s ease;
|
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%);
|
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 {
|
.target.active {
|
||||||
background-color: #44aa44;
|
background-color: #22aa22; /* Darker green for better visibility */
|
||||||
box-shadow: 0 0 20px rgba(68, 170, 68, 0.4);
|
box-shadow: 0 0 30px rgba(34, 170, 34, 0.8);
|
||||||
|
border: 4px solid white;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
|
@ -271,4 +277,89 @@ body,
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
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 { Settings } from './components/Settings';
|
||||||
import { RoundsHistory } from './components/RoundsHistory';
|
import { RoundsHistory } from './components/RoundsHistory';
|
||||||
import { Instructions } from './components/Instructions';
|
import { Instructions } from './components/Instructions';
|
||||||
|
@ -65,7 +65,7 @@ function App() {
|
||||||
y: 0,
|
y: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Interaction tracking refs
|
// Refs for interaction tracking
|
||||||
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 });
|
||||||
|
@ -75,6 +75,11 @@ function App() {
|
||||||
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);
|
||||||
|
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
|
// Load saved state on initial mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -104,7 +109,8 @@ function App() {
|
||||||
setTargets(newTargets);
|
setTargets(newTargets);
|
||||||
setCurrentTarget(getRandomTarget(null, CONSTANTS.TARGET_COUNT));
|
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);
|
setZoomLevel(initialZoom);
|
||||||
setViewportOffset(calculateInitialViewport(initialZoom));
|
setViewportOffset(calculateInitialViewport(initialZoom));
|
||||||
setStats(createInitialStats());
|
setStats(createInitialStats());
|
||||||
|
@ -126,7 +132,7 @@ function App() {
|
||||||
}
|
}
|
||||||
}, [currentPreset]);
|
}, [currentPreset]);
|
||||||
|
|
||||||
// Progress through presets in study mode
|
// Modify the useEffect for preset progression to show the transition
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode !== 'study') return;
|
if (mode !== 'study') return;
|
||||||
|
|
||||||
|
@ -139,9 +145,17 @@ function App() {
|
||||||
const currentIndex = allPresets.indexOf(currentPreset);
|
const currentIndex = allPresets.indexOf(currentPreset);
|
||||||
|
|
||||||
if (currentIndex < allPresets.length - 1) {
|
if (currentIndex < allPresets.length - 1) {
|
||||||
|
// Show the transition to the next preset
|
||||||
const nextPreset = allPresets[currentIndex + 1];
|
const nextPreset = allPresets[currentIndex + 1];
|
||||||
setCurrentPreset(nextPreset);
|
setTransitionPreset(nextPreset);
|
||||||
setTargetCount(0);
|
setShowPresetTransition(true);
|
||||||
|
|
||||||
|
// Hide the transition after animation completes
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowPresetTransition(false);
|
||||||
|
setCurrentPreset(nextPreset);
|
||||||
|
setTargetCount(0);
|
||||||
|
}, 1500); // Match the animation duration
|
||||||
} else {
|
} else {
|
||||||
setStudyComplete(true);
|
setStudyComplete(true);
|
||||||
}
|
}
|
||||||
|
@ -402,32 +416,65 @@ function App() {
|
||||||
|
|
||||||
// Regenerate targets when window is resized
|
// Regenerate targets when window is resized
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let resizeTimer: number;
|
let lastWidth = window.innerWidth;
|
||||||
|
let lastHeight = window.innerHeight;
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
if (mode !== 'study') {
|
if (mode !== 'study') {
|
||||||
clearTimeout(resizeTimer);
|
// Clear any existing timer
|
||||||
|
if (resizeTimerRef.current) {
|
||||||
|
window.clearTimeout(resizeTimerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
resizeTimer = window.setTimeout(() => {
|
// Only regenerate if the size changed significantly
|
||||||
const newTargets = generateTargets();
|
const widthDiff = Math.abs(window.innerWidth - lastWidth);
|
||||||
setTargets(newTargets);
|
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;
|
||||||
setShowResizeWarning(true);
|
lastHeight = window.innerHeight;
|
||||||
}, 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);
|
window.addEventListener('resize', handleResize);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', handleResize);
|
window.removeEventListener('resize', handleResize);
|
||||||
clearTimeout(resizeTimer);
|
if (resizeTimerRef.current) {
|
||||||
|
window.clearTimeout(resizeTimerRef.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [currentTarget, mode]);
|
}, [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 (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<div className="stats">
|
<div className="stats">
|
||||||
|
@ -448,6 +495,24 @@ function App() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</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
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="container"
|
className="container"
|
||||||
|
@ -466,18 +531,7 @@ function App() {
|
||||||
transformOrigin: '0 0',
|
transformOrigin: '0 0',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{targets.map((target, index) => (
|
{renderedTargets}
|
||||||
<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`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
12
src/types.ts
12
src/types.ts
|
@ -43,21 +43,21 @@ export interface Round {
|
||||||
|
|
||||||
export const CONSTANTS = {
|
export const CONSTANTS = {
|
||||||
TARGET_COUNT: 200,
|
TARGET_COUNT: 200,
|
||||||
TARGET_MIN_RADIUS: 100,
|
TARGET_MIN_RADIUS: 20,
|
||||||
TARGET_MAX_RADIUS: 800,
|
TARGET_MAX_RADIUS: 40,
|
||||||
VIRTUAL_CANVAS_SIZE: 50000,
|
VIRTUAL_CANVAS_SIZE: 50000,
|
||||||
ZOOM_LEVELS: {
|
ZOOM_LEVELS: {
|
||||||
min: 0.01,
|
min: 0.01,
|
||||||
max: 10,
|
max: 10,
|
||||||
default: 0.2,
|
default: 0.01,
|
||||||
},
|
},
|
||||||
MOUSE_WHEEL_SAMPLES: 5,
|
MOUSE_WHEEL_SAMPLES: 5,
|
||||||
NUM_CURVE_POINTS: 5,
|
NUM_CURVE_POINTS: 5,
|
||||||
STUDY_TARGETS_PER_PRESET: 25,
|
STUDY_TARGETS_PER_PRESET: 25,
|
||||||
PRESETS: {
|
PRESETS: {
|
||||||
A: [1, 1, 1, 1, 1], // Control: no acceleration
|
A: [1, 2, 3, 4, 5], // Linear increase
|
||||||
B: [1, 2, 3, 4, 5], // Linear increase
|
B: [1, 1.5, 2.5, 4, 6], // Slow start, fast end
|
||||||
C: [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
|
D: [1, 3, 4, 3, 1], // Bell curve
|
||||||
} as Record<ZoomPreset, number[]>,
|
} as Record<ZoomPreset, number[]>,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -8,10 +8,22 @@ export const calculateZoomDelta = (
|
||||||
baseMultiplier = 0.001
|
baseMultiplier = 0.001
|
||||||
): number => clamp(wheelDelta * baseMultiplier * accelerationFactor, -0.5, 0.5);
|
): number => clamp(wheelDelta * baseMultiplier * accelerationFactor, -0.5, 0.5);
|
||||||
|
|
||||||
export const calculateInitialViewport = (initialZoom: number): ViewportOffset => ({
|
export const calculateInitialViewport = (initialZoom: number): ViewportOffset => {
|
||||||
x: (CONSTANTS.VIRTUAL_CANVAS_SIZE - window.innerWidth / initialZoom) / 2,
|
// When fully zoomed out, we want to see the entire content area
|
||||||
y: (CONSTANTS.VIRTUAL_CANVAS_SIZE - window.innerHeight / initialZoom) / 2,
|
// 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 =>
|
export const clampZoom = (zoom: number): number =>
|
||||||
clamp(zoom, CONSTANTS.ZOOM_LEVELS.min, CONSTANTS.ZOOM_LEVELS.max);
|
clamp(zoom, CONSTANTS.ZOOM_LEVELS.min, CONSTANTS.ZOOM_LEVELS.max);
|
||||||
|
|
|
@ -2,59 +2,50 @@ import { Target, CONSTANTS } from '../types';
|
||||||
|
|
||||||
export const generateTargets = () => {
|
export const generateTargets = () => {
|
||||||
const newTargets: Target[] = [];
|
const newTargets: Target[] = [];
|
||||||
const PADDING = CONSTANTS.TARGET_MAX_RADIUS * 2;
|
|
||||||
|
// Fixed target size for better visibility
|
||||||
// Get browser window aspect ratio
|
const MIN_RADIUS = 100;
|
||||||
|
const MAX_RADIUS = 600;
|
||||||
|
|
||||||
|
// Get browser window dimensions
|
||||||
const windowWidth = window.innerWidth;
|
const windowWidth = window.innerWidth;
|
||||||
const windowHeight = window.innerHeight;
|
const windowHeight = window.innerHeight;
|
||||||
const aspectRatio = windowWidth / windowHeight;
|
|
||||||
|
|
||||||
// Calculate grid dimensions based on aspect ratio
|
// Base the target area on the minimum zoom level
|
||||||
const gridWidth = CONSTANTS.VIRTUAL_CANVAS_SIZE;
|
// This ensures when fully zoomed out, we see the entire grid
|
||||||
const gridHeight = gridWidth / aspectRatio;
|
const minZoom = CONSTANTS.ZOOM_LEVELS.min;
|
||||||
|
const areaWidth = windowWidth / minZoom;
|
||||||
|
const areaHeight = windowHeight / minZoom;
|
||||||
|
|
||||||
// Calculate cells based on aspect ratio
|
// Create a grid-like distribution
|
||||||
const totalCells = CONSTANTS.TARGET_COUNT;
|
const gridSize = Math.ceil(Math.sqrt(CONSTANTS.TARGET_COUNT));
|
||||||
const numColumns = Math.ceil(Math.sqrt(totalCells * aspectRatio));
|
const cellWidth = areaWidth / gridSize;
|
||||||
const numRows = Math.ceil(totalCells / numColumns);
|
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++) {
|
for (let i = 0; i < CONSTANTS.TARGET_COUNT; i++) {
|
||||||
let validPosition = false;
|
// Calculate grid position
|
||||||
let newTarget: Target;
|
const gridX = i % gridSize;
|
||||||
|
const gridY = Math.floor(i / gridSize);
|
||||||
// Calculate the grid position
|
|
||||||
const column = i % numColumns;
|
// Add some randomness within each grid cell
|
||||||
const row = Math.floor(i / numColumns);
|
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 +
|
// Calculate final position
|
||||||
Math.random() * (CONSTANTS.TARGET_MAX_RADIUS - CONSTANTS.TARGET_MIN_RADIUS);
|
const x = cellWidth * (gridX + 0.5) + randomOffsetX;
|
||||||
|
const y = cellHeight * (gridY + 0.5) + randomOffsetY;
|
||||||
// Base position from grid
|
|
||||||
const baseX = PADDING + column * cellWidth;
|
// Random radius
|
||||||
const baseY = PADDING + row * cellHeight;
|
const radius = MIN_RADIUS + Math.random() * (MAX_RADIUS - MIN_RADIUS);
|
||||||
|
|
||||||
// Add some randomness within the cell
|
// Add the target to the array
|
||||||
const x = baseX + Math.random() * (cellWidth - radius * 2);
|
newTargets.push({
|
||||||
const y = baseY + Math.random() * (cellHeight - radius * 2);
|
x,
|
||||||
|
y,
|
||||||
newTarget = { x, y, radius };
|
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!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return newTargets;
|
return newTargets;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue