From 1a54f0981446c6b81b0a572e0b64e6be03e2f9a3 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Wed, 19 Nov 2025 11:33:46 -0600 Subject: [PATCH] feat: implement binary search for optimal zoom level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace area-based formula with binary search to find zoom where regions occupy 10-20% of magnifier area. Binary search algorithm: 1. Start with minZoom=1, maxZoom=1000 2. Test midpoint zoom level 3. Calculate how much of magnified view each region occupies 4. If no regions fit → zoom out (maxZoom = mid) 5. If regions < 10% → zoom in (minZoom = mid) 6. If regions > 20% → zoom out (maxZoom = mid) 7. If 10-20% → perfect, done! 8. Iterate max 20 times or until range < 0.1 This handles all regions in the detection box, not just the one under cursor, giving better overall framing. For Gibraltar area: - Binary search will find zoom ~800-1000x where Gibraltar occupies 10-20% of magnifier - Converges in ~10-15 iterations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../components/MapRenderer.tsx | 113 +++++++++++++----- 1 file changed, 81 insertions(+), 32 deletions(-) diff --git a/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx b/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx index d1b7d456..0266859f 100644 --- a/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx +++ b/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx @@ -870,43 +870,92 @@ export function MapRenderer({ } if (shouldShow) { - // Calculate zoom to make the region under cursor occupy ~15% of magnifier area - // Magnifier area is 50% of container width × 25% height (aspect 2:1) - // Target: region should occupy 15% of magnifier - let adaptiveZoom = 10 // Default zoom + // Binary search for optimal zoom level + // Goal: Find zoom where regions fit nicely in magnifier (taking 10-20% of area) + const TARGET_AREA_MIN = 0.10 // 10% of magnifier + const TARGET_AREA_MAX = 0.20 // 20% of magnifier - if (regionUnderCursor && regionUnderCursorArea > 0) { - // Get magnifier dimensions in screen pixels - const magnifierWidth = containerRect.width * 0.5 - const magnifierHeight = magnifierWidth / 2 - const magnifierArea = magnifierWidth * magnifierHeight + // Get magnifier dimensions + const magnifierWidth = containerRect.width * 0.5 + const magnifierHeight = magnifierWidth / 2 + const magnifierArea = magnifierWidth * magnifierHeight - // Calculate zoom so region occupies 15% of magnifier area - // regionArea * zoom^2 = magnifierArea * 0.15 - // zoom = sqrt((magnifierArea * 0.15) / regionArea) - const targetAreaRatio = 0.15 - adaptiveZoom = Math.sqrt((magnifierArea * targetAreaRatio) / regionUnderCursorArea) + // Get SVG viewBox for coordinate conversion + const viewBoxParts = mapData.viewBox.split(' ').map(Number) + const viewBoxWidth = viewBoxParts[2] || 1000 + const viewBoxHeight = viewBoxParts[3] || 1000 - // Clamp zoom between reasonable bounds - const preClampZoom = adaptiveZoom - adaptiveZoom = Math.max(8, Math.min(MAX_ZOOM, adaptiveZoom)) + // Binary search bounds + let minZoom = 1 + let maxZoom = MAX_ZOOM + let adaptiveZoom = 10 + let iterations = 0 + const MAX_ITERATIONS = 20 - // Debug logging for Gibraltar - const hasGibraltar = regionUnderCursor === 'gi' - if (hasGibraltar) { - console.log(`[Zoom] 🎯 GIBRALTAR BREAKDOWN:`, { - regionArea: `${regionUnderCursorArea.toFixed(6)}px²`, - magnifierArea: `${magnifierArea.toFixed(0)}px²`, - targetRatio: `${(targetAreaRatio * 100).toFixed(0)}%`, - calculatedZoom: preClampZoom.toFixed(1), - finalZoom: adaptiveZoom.toFixed(1), - hitMaxZoom: preClampZoom > MAX_ZOOM, - }) + while (iterations < MAX_ITERATIONS && maxZoom - minZoom > 0.1) { + iterations++ + const testZoom = (minZoom + maxZoom) / 2 + + // Calculate magnified viewBox dimensions at this zoom + const magnifiedViewBoxWidth = viewBoxWidth / testZoom + const magnifiedViewBoxHeight = viewBoxHeight / testZoom + const magnifiedViewBoxArea = magnifiedViewBoxWidth * magnifiedViewBoxHeight + + // Check regions in detection box to see how they fit + let anyRegionFullyInside = false + let largestRegionRatio = 0 + + detectedRegions.forEach((regionId) => { + const region = mapData.regions.find((r) => r.id === regionId) + if (!region) return + + const regionPath = svgRef.current?.querySelector(`path[data-region-id="${regionId}"]`) + if (!regionPath) return + + const pathRect = regionPath.getBoundingClientRect() + const regionPixelArea = pathRect.width * pathRect.height + + // Convert pixel area to viewBox area (approximate) + const scaleX = viewBoxWidth / svgRect.width + const scaleY = viewBoxHeight / svgRect.height + const regionViewBoxArea = regionPixelArea * scaleX * scaleY + + // Check if region fits in magnified view + const regionRatioInMagnifier = regionViewBoxArea / magnifiedViewBoxArea + + if (regionRatioInMagnifier < 1.0) { + anyRegionFullyInside = true + largestRegionRatio = Math.max(largestRegionRatio, regionRatioInMagnifier) + } + }) + + // Binary search logic + if (!anyRegionFullyInside) { + // No regions fit - zoom out + maxZoom = testZoom + } else if (largestRegionRatio < TARGET_AREA_MIN) { + // Regions too small - zoom in + minZoom = testZoom + } else if (largestRegionRatio > TARGET_AREA_MAX) { + // Regions too large - zoom out + maxZoom = testZoom + } else { + // Just right! + adaptiveZoom = testZoom + break } - } else { - // No region under cursor - use density-based zoom - const countFactor = Math.min(regionsInBox / 10, 1) - adaptiveZoom = 10 + countFactor * 20 + + adaptiveZoom = testZoom + } + + // Debug logging for Gibraltar + const hasGibraltar = detectedRegions.includes('gi') + if (hasGibraltar) { + console.log(`[Zoom] 🎯 BINARY SEARCH RESULT:`, { + iterations, + finalZoom: adaptiveZoom.toFixed(1), + detectedRegions, + }) } // Calculate magnifier position (opposite corner from cursor)