From 8c8b46634495dd0ff866b0d255de6a3d2ceb10d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 08:22:57 +0000 Subject: [PATCH 1/4] Initial plan From 1f8e89c0b66f373e05802b1472fae172c1da0d99 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 08:30:38 +0000 Subject: [PATCH 2/4] Implement collision detection to prevent thought bubble overlap - Add calculateDistance helper to compute Euclidean distance between positions - Add isPositionValid helper to check for overlapping bubbles - Modify spawnBubble to try up to 20 positions before spawning - Add fallback positioning if no valid position found after max attempts - Set minimum distance threshold of 25% viewport units between bubbles - Tested manually with multiple bubbles spawning successfully without overlap Co-authored-by: Theaxiom <57013+Theaxiom@users.noreply.github.com> --- src/hooks/useThoughtBubbles.js | 78 ++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/src/hooks/useThoughtBubbles.js b/src/hooks/useThoughtBubbles.js index d2b94ff..3341619 100644 --- a/src/hooks/useThoughtBubbles.js +++ b/src/hooks/useThoughtBubbles.js @@ -25,6 +25,33 @@ export const useThoughtBubbles = ({ isActive, onBubbleExpired }) => { onBubbleExpiredRef.current = onBubbleExpired; }, [onBubbleExpired]); + /** + * Calculate distance between two bubble positions + * Uses Euclidean distance formula accounting for viewport percentages + */ + const calculateDistance = (pos1, pos2) => { + const dx = pos1.x - pos2.x; + const dy = pos1.y - pos2.y; + return Math.sqrt(dx * dx + dy * dy); + }; + + /** + * Check if a position overlaps with existing bubbles + * Minimum distance threshold is ~25% of viewport to ensure clear separation + * (accounts for bubble width of ~20rem max, which is roughly 15-20% of typical mobile viewport) + */ + const isPositionValid = (newPos, existingBubbles) => { + const MIN_DISTANCE = 25; // Minimum distance in percentage units + + for (const bubble of existingBubbles) { + const distance = calculateDistance(newPos, bubble.position); + if (distance < MIN_DISTANCE) { + return false; + } + } + return true; + }; + /** * Spawn new thought bubble * @@ -32,6 +59,7 @@ export const useThoughtBubbles = ({ isActive, onBubbleExpired }) => { * - Top HUD: ~15% of viewport height * - Bottom controls: ~30% of viewport height * - Horizontal margins: ~10% on each side + * - Collision detection: Ensures bubbles don't overlap * * Bubbles use dynamic max-width constraints (via CSS calc) to prevent * overflow on the right edge, ensuring full visibility across all viewports. @@ -40,20 +68,44 @@ export const useThoughtBubbles = ({ isActive, onBubbleExpired }) => { const randomWord = WORDS_OF_THE_VOICE[Math.floor(Math.random() * WORDS_OF_THE_VOICE.length)]; const bubbleId = `bubble_${bubbleIdCounter.current++}`; - // Calculate safe spawn area accounting for bubble dimensions - // Horizontal: 10-80% (leaving ~10% margin on each side for bubble width) - // Vertical: 20-55% (avoiding HUD at top 15%, controls at bottom 30%, with margins) - const newBubble = { - id: bubbleId, - word: randomWord, - spawnTime: Date.now(), - position: { - x: Math.random() * 70 + 10, // 10-80% from left - y: Math.random() * 35 + 20, // 20-55% from top - }, - }; + setActiveBubbles(prev => { + // Try to find a non-overlapping position + let position = null; + const MAX_ATTEMPTS = 20; // Maximum attempts to find valid position + + for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { + const candidatePosition = { + x: Math.random() * 70 + 10, // 10-80% from left + y: Math.random() * 35 + 20, // 20-55% from top + }; + + if (isPositionValid(candidatePosition, prev)) { + position = candidatePosition; + break; + } + } + + // Fallback: If no valid position found after max attempts, + // use a position anyway but space it vertically from others + if (!position) { + position = { + x: Math.random() * 70 + 10, + y: (prev.length * 12 + 20) % 35 + 20, // Stagger vertically based on count + }; + } - setActiveBubbles(prev => [...prev, newBubble]); + // Calculate safe spawn area accounting for bubble dimensions + // Horizontal: 10-80% (leaving ~10% margin on each side for bubble width) + // Vertical: 20-55% (avoiding HUD at top 15%, controls at bottom 30%, with margins) + const newBubble = { + id: bubbleId, + word: randomWord, + spawnTime: Date.now(), + position, + }; + + return [...prev, newBubble]; + }); // Auto-remove after duration setTimeout(() => { From c0d74883844ff576ca71dee5309958628b91790c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 08:33:10 +0000 Subject: [PATCH 3/4] Address code review feedback: improve code clarity - Extract magic numbers to named constants in fallback logic - Add clear comments explaining vertical staggering parameters - Remove misleading comments about old implementation - Improve readability and maintainability of collision detection code Co-authored-by: Theaxiom <57013+Theaxiom@users.noreply.github.com> --- src/hooks/useThoughtBubbles.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/hooks/useThoughtBubbles.js b/src/hooks/useThoughtBubbles.js index 3341619..58ae9f7 100644 --- a/src/hooks/useThoughtBubbles.js +++ b/src/hooks/useThoughtBubbles.js @@ -86,17 +86,18 @@ export const useThoughtBubbles = ({ isActive, onBubbleExpired }) => { } // Fallback: If no valid position found after max attempts, - // use a position anyway but space it vertically from others + // use a position anyway but space it vertically from others to minimize overlap if (!position) { + const VERTICAL_STAGGER_OFFSET = 12; // % offset between staggered bubbles + const MAX_VERTICAL_RANGE = 35; // Max vertical range (55% - 20%) + const MIN_VERTICAL_POSITION = 20; // Min Y position (top margin) + position = { - x: Math.random() * 70 + 10, - y: (prev.length * 12 + 20) % 35 + 20, // Stagger vertically based on count + x: Math.random() * 70 + 10, // Random horizontal position + y: (prev.length * VERTICAL_STAGGER_OFFSET + MIN_VERTICAL_POSITION) % MAX_VERTICAL_RANGE + MIN_VERTICAL_POSITION, }; } - // Calculate safe spawn area accounting for bubble dimensions - // Horizontal: 10-80% (leaving ~10% margin on each side for bubble width) - // Vertical: 20-55% (avoiding HUD at top 15%, controls at bottom 30%, with margins) const newBubble = { id: bubbleId, word: randomWord, From d49e81c1ac13391322508a872a092b9802934ce4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 08:34:26 +0000 Subject: [PATCH 4/4] Fix modulo calculation in fallback positioning - Correct VERTICAL_RANGE calculation to produce 20-55% range (not 20-54%) - Update constant name from MAX_VERTICAL_RANGE to VERTICAL_RANGE for clarity - Simplify modulo expression for better readability - Ensure fallback positioning matches documented behavior Co-authored-by: Theaxiom <57013+Theaxiom@users.noreply.github.com> --- src/hooks/useThoughtBubbles.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useThoughtBubbles.js b/src/hooks/useThoughtBubbles.js index 58ae9f7..9234d55 100644 --- a/src/hooks/useThoughtBubbles.js +++ b/src/hooks/useThoughtBubbles.js @@ -89,12 +89,12 @@ export const useThoughtBubbles = ({ isActive, onBubbleExpired }) => { // use a position anyway but space it vertically from others to minimize overlap if (!position) { const VERTICAL_STAGGER_OFFSET = 12; // % offset between staggered bubbles - const MAX_VERTICAL_RANGE = 35; // Max vertical range (55% - 20%) + const VERTICAL_RANGE = 36; // Total vertical range (55% - 20% + 1) const MIN_VERTICAL_POSITION = 20; // Min Y position (top margin) position = { x: Math.random() * 70 + 10, // Random horizontal position - y: (prev.length * VERTICAL_STAGGER_OFFSET + MIN_VERTICAL_POSITION) % MAX_VERTICAL_RANGE + MIN_VERTICAL_POSITION, + y: (prev.length * VERTICAL_STAGGER_OFFSET) % VERTICAL_RANGE + MIN_VERTICAL_POSITION, // 20-55% }; }