Problem:
- Portugal's bounding box includes distant Atlantic islands, causing mainland to be ignored
- Algorithm was selecting "largest piece by area" which picked island groups instead of mainland
- This caused Portugal to dominate zoom calculations and prevent Gibraltar from being prioritized
Solution:
- Changed pre-computation to use FIRST piece instead of largest (mainland is typically piece 1)
- Added showDebugBoundingBoxes prop to hide debug rectangles in production
- Improved zoom animation smoothness with gentler spring easing (tension: 120, friction: 30)
Technical details:
- Multi-piece SVG paths split by `z m` separator
- First piece is mainland, subsequent pieces are islands/territories
- Pre-computed sizes cached in useEffect for performance
- Only Portugal logs to console for debugging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
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 <noreply@anthropic.com>
TypeScript error: containerRect was declared twice in the same scope.
The variable is already declared at the top of handleMouseMove, no
need to redeclare it when calculating magnifier position.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Previous approach used smallest region in detection box for zoom, which
caused Spain to zoom to 1000x just because Gibraltar was nearby.
New approach:
- Use region UNDER CURSOR for zoom calculation (not smallest in box)
- Calculate zoom so region occupies 15% of magnifier area
- Formula: zoom = sqrt((magnifierArea × 0.15) / regionArea)
For Gibraltar (0.018px² area) in ~200,000px² magnifier:
- Target: 200,000 × 0.15 = 30,000px² in magnifier
- zoom = sqrt(30,000 / 0.018) ≈ 1,291x → capped at 1000x
- Result: Gibraltar occupies ~14% of magnifier (close to target)
For Spain (5,000px² area):
- Target: 200,000 × 0.15 = 30,000px² in magnifier
- zoom = sqrt(30,000 / 5,000) ≈ 2.4x
- Result: Spain occupies 15% of magnifier
This gives predictable, proportional zoom levels based on what
you're actually hovering over, not what's nearby.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The calculateBoundingBox function doesn't handle relative SVG path
commands correctly. Gibraltar's path uses 'm' (relative move) followed
by tiny relative offsets like -0.009, 0.047, but the function treated
them as absolute coordinates, giving width of 460 units instead of 0.08.
Proper SVG path parsing requires:
- Tracking current position as you iterate through commands
- Handling both absolute (M,L,H,V,C,S,Q,T,A) and relative (m,l,h,v,c,s,q,t,a)
- Curves need Bezier math to find actual bounds
- Too complex for this use case
Solution: Use screen pixels (getBoundingClientRect) for everything.
Screen pixel measurements work correctly for tiny regions:
- Gibraltar: 0.08px (accurate)
- Jersey: 0.82px (accurate)
New zoom formula for screen pixels:
- Sub-pixel (< 1px): 1000/(size+0.05)
- Gibraltar (0.08px): ~7692x → capped at 1000x
- Tiny (1-10px): 500/(size+0.5)
- 1px: ~333x
- 5px: ~91x
- Small (10-50px): Linear up to +50x
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The previous commit broke cursor dampening and magnifier triggers by
using SVG units for everything. We need both measurements:
- **SVG units** (0-1010): Accurate zoom calculation
- **Screen pixels**: Cursor dampening and magnifier triggers
Changes:
- Track detectedSmallestSize (SVG units) for zoom calculation
- Track detectedSmallestScreenSize (screen pixels) for dampening
- Use screen pixels for getMovementMultiplier() (expects <1px, <5px, <15px)
- Use screen pixels for hasSmallRegion trigger (<15px)
- Debug logging now shows both values
Example for Gibraltar:
- SVG size: 0.08 units → 1000x zoom
- Screen size: 0.08px → 3% cursor speed (ultra precision)
This restores cursor dampening behavior while keeping accurate
SVG-based zoom calculations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The previous implementation used getBoundingClientRect() which returns
axis-aligned bounding boxes in screen pixels. This has two problems:
1. **Inaccurate sizing**: Bounding boxes for irregular/rotated shapes
are much larger than the actual visible area
2. **Floating-point precision**: Sub-pixel screen measurements lose
precision and cause most regions to trigger max zoom
Solution: Use SVG path bounding box from viewBox coordinates
The world map viewBox is 1010 × 666 SVG units:
- Gibraltar: ~0.08 SVG units (extremely tiny!)
- Jersey: ~0.5 SVG units
- Most countries: 10-100 SVG units
New zoom formula for SVG units:
- Tiny regions (< 5 units): 200 / (size + 0.1)
- Gibraltar (0.08): 200/0.18 ≈ 1111x → capped at 1000x
- 1 unit: 200/1.1 ≈ 182x
- 5 units: 200/5.1 ≈ 39x
- Small regions (5-100 units): Linear scaling up to +50x
- Large regions (>100 units): Minimal size-based zoom
This avoids floating-point precision issues and gives accurate
zoom levels based on the region's true SVG path dimensions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Increase MAX_ZOOM from 500x to 1000x and double the exponential
scaling formula to handle Gibraltar's extreme size.
New zoom calculation for sub-pixel regions:
- Formula: 1000 / (size + 0.05)
- Gibraltar (0.08px): ~770x size zoom → ~800x total (capped at 1000x)
- Jersey (0.82px): ~115x size zoom → ~145x total
- 1px: ~95x size zoom → ~125x total
Added detailed debug logging for Gibraltar to diagnose:
- Exact region size (4 decimal places)
- Breakdown: base (10) + density + size zoom
- Total before/after clamping
- Whether we're hitting max zoom limit
This will help determine if we're hitting floating-point precision
limits or if Gibraltar needs even more magnification.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Gibraltar at 0.08px needs extreme magnification to be clickable.
Increased max zoom from 120x to 500x and added exponential scaling
for sub-pixel regions.
New zoom calculation:
- Base: 10x
- Density: up to +20x
- Size-based (exponential for <1px): 500 / (size + 0.05)
- Gibraltar (0.08px): ~385x + base/density = ~415x total
- Jersey (0.82px): ~57x + base/density = ~87x total
- 1px regions: ~50x + base/density = ~80x total
- Size-based (linear for >=1px): up to +150x
Gold border threshold increased to 100x to indicate extreme zoom.
This exponential scaling ensures the tiniest regions get massive
magnification while regular small regions get reasonable zoom.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Remove leftover velocity calculation code that was used for quick-escape
logic (no longer needed with unified zoom).
Fix TypeScript errors:
- Remove lastMoveTimeRef reference (not needed)
- Fix detectedRegions type - it's string[], not MapRegion[]
- Use .includes('gi') instead of .some(r => r.id === 'gi')
- Log detectedRegionIds instead of trying to map to names
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Simplify precision controls by removing dual zoom modes (normal + super).
Now uses a single unified adaptive zoom that immediately calculates the
right magnification based on region size.
Changes:
- Remove hover delay timer (HOVER_DELAY_MS)
- Remove super zoom state (superZoomActive)
- Remove quick-escape velocity threshold
- Unify zoom calculation: 8x base + up to 16x density + up to 96x size
- Gold border now shows for any zoom >60x (not just "super zoom")
Zoom calculation for Gibraltar (0.08px):
- Base: 8x
- Density (4 regions): +6x
- Size factor (0.996): +92x
- Total: ~106x (immediately, no delay)
Benefits:
- Simpler UX - no waiting for super zoom to activate
- More responsive - zoom adjusts immediately to region size
- Less code complexity - no timer management or state tracking
- Predictable behavior - zoom level purely based on what's visible
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Gibraltar is only 0.08px wide and needs extreme magnification.
Previous multiplier of 2.5x only achieved ~48x zoom, which was
insufficient to make Gibraltar visible and clickable.
New calculation:
- Base adaptive zoom: ~19x (based on size + density)
- Super zoom multiplier: 5.0x
- Result: ~95-120x zoom for Gibraltar
This should make Gibraltar clearly visible in the magnifier at
maximum super zoom.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add console logging to help debug why Gibraltar isn't visible:
1. Map filtering: Log whether Gibraltar is included or excluded
2. Region sizing: Log Gibraltar's actual pixel dimensions
3. Magnifier detection: Log when Gibraltar is detected in hover area
4. Zoom levels: Log final zoom when Gibraltar is in view
Logs are targeted to avoid excessive output:
- Only log Gibraltar specifically (id='gi')
- Only log ultra-tiny regions (< 1-2px)
- Skip logging for normal-sized regions
This will help diagnose if Gibraltar is:
- Being filtered out by difficulty
- Too small to render on screen
- Not being detected by precision controls
- Not receiving sufficient zoom magnification
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Increase MAX_ZOOM_SUPER from 60x to 120x to handle extremely small
regions like Gibraltar (0.08px). The previous 60x zoom was still
insufficient for clicking such tiny regions.
Changes:
- Add MAX_ZOOM_NORMAL constant (24x)
- Add MAX_ZOOM_SUPER constant (120x)
- Use constants instead of hardcoded values in zoom calculations
- Super zoom now reaches up to 120x magnification for sub-pixel regions
This should make Gibraltar and other extremely tiny territories
clickable with the pointer lock precision controls.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add SUPER_ZOOM_SIZE_THRESHOLD constant (default: 3px)
- Previously hardcoded at < 1px (only sub-pixel regions)
- Now activates for regions < 3px (more helpful range)
- Easy to tune for different preferences
Update logging to reference the configurable threshold.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
**Crosshair positioning:**
- Fix vertical line: left: 50%, top: 0 (was top: 50%)
- Fix horizontal line: left: 0, top: 50% (was left: 50%)
- Both now properly centered in the circle
**Re-enable pointer lock:**
- Show prompt overlay again when pointer lock is released
- User can press Escape to exit, then click to re-enter
- Handles both user-initiated unlock (Escape) and errors
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
**Problem:** Pointer lock was activating on MapRenderer's container,
but PlayingPhase was trying to manage it, so state never updated.
**Solution:** Move all pointer lock management into MapRenderer:
- Add pointerLocked state in MapRenderer
- Add pointer lock event listeners in MapRenderer
- Add click handler to request lock on MapRenderer container
- Add "Enable Precision Controls" overlay in MapRenderer
- Remove all pointer lock code from PlayingPhase
Now pointer lock state tracks correctly and custom cursor will render.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add extensive console logging to debug cursor visibility:
**PlayingPhase:**
- Log when pointer lock listeners attach/detach
- Log pointer lock state changes with element details
- Log container click events with conditions
- Log pointerLocked state changes
**MapRenderer:**
- Log custom cursor render conditions on every render
- Log when cursor should be visible
- Log cursor position when rendering
This will help diagnose why custom cursor isn't visible.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
When pointer lock is active, the browser hides the native cursor.
Add a custom crosshair cursor that follows the tracked position:
- 20px circle with crosshair lines
- Blue color (matches theme)
- Positioned at tracked cursor coordinates
- Only visible when pointerLocked is true
This ensures users can always see where they're pointing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Major refactoring of precision controls for Know Your World game:
**New Flow:**
- Pointer lock requested once when PlayingPhase mounts (user click)
- Active for entire game session until unmount
- Simpler: use movementX/movementY with adaptive multiplier
**PlayingPhase.tsx:**
- Add pointer lock management with event listeners
- Show "Enable Precision Controls" overlay on mount
- Request pointer lock on first click (user gesture)
- Release on unmount (game ends)
- Pass pointerLocked prop to MapRenderer
**MapRenderer.tsx:**
- Accept pointerLocked as prop (don't manage internally)
- Remove complex cursor dampening (dual refs: raw vs dampened)
- Simplified: apply multiplier to movementX/movementY based on region size
- Sub-pixel regions (<1px): 3% speed
- Tiny regions (1-5px): 10% speed
- Small regions (5-15px): 25% speed
- Larger regions: 100% speed
- Remove precision mode state (not needed)
- Remove cooldown logic (not needed)
- Remove click capture handler (not needed)
- Update cursor style and mouse handlers to use pointerLocked
**Benefits:**
- Much simpler code (removed ~100 lines of complexity)
- No lag when changing direction (no interpolation)
- Pointer lock active entire session (can't drift off map)
- Movement multiplier clearer than dampening
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Remove verbose console logging that was used to debug issues that are now fixed:
- Velocity logging (quick-escape threshold is now correct)
- Dampening state logs (dampening lag is fixed)
- Region detection logs (crosshair accuracy is fixed)
- Per-frame precision mode state checks (too verbose)
Keep only important logs:
- Quick escape triggers
- Precision mode state changes
- Pointer lock debugging (current issue)
- Magnifier state changes
This reduces console noise while keeping useful feedback.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add detailed logging to debug pointer lock activation:
- Log useEffect triggers with all conditions
- Log pointer lock request attempts and results
- Log pointer lock state in precision mode checks
- Add try/catch around requestPointerLock
- Warn if container ref is missing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
When cursor dampening is active, the real mouse can drift off the map while the dampened cursor is still on it, causing magnifier to disappear. Pointer Lock solves this.
Changes:
- Add pointerLocked state and event listeners
- Request pointer lock when precision mode activates
- Use movementX/Y deltas instead of absolute position when locked
- Clamp calculated position to container bounds
- Update SVG bounds check for both locked and normal modes
- Ignore mouse leave events when pointer is locked
- Release pointer lock when precision mode deactivates
Benefits:
- No more hitting viewport edges during precision mode
- Real cursor disappears, only dampened cursor visible
- Smooth movement tracking without boundary issues
- Magnifier stays active even if real mouse would leave container
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Console logs showed user reaching 22px/frame when trying to escape, but 30px threshold was too high. This required an unrealistically aggressive mouse flick.
Changes:
- Lower QUICK_MOVE_THRESHOLD from 30px to 15px/frame (2x easier to trigger)
- Increase PRECISION_MODE_COOLDOWN from 800ms to 1200ms (more time to escape area)
With these values, moderate-speed mouse movements will trigger escape, and users have more time to move away before precision mode can re-activate.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add detailed console logging to debug quick-escape issues:
- Log velocity every frame when in precision mode or moving
- Show velocity vs threshold and whether escape will trigger
- Log when quick-escape actually triggers
- Log precision mode state check every frame:
- shouldShow, cooldownActive, time remaining
- current vs target precision mode state
- smallest region size
- Log when precision mode state actually changes
This will help identify why quick-escape isn't working as expected.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
After quick-escape, precision mode was immediately re-enabling because the user was still hovering over small regions. This made it feel impossible to escape dampening.
Changes:
- Add precisionModeCooldownRef to track cooldown expiration time
- Lower QUICK_MOVE_THRESHOLD from 50px to 30px (easier to trigger)
- Add PRECISION_MODE_COOLDOWN_MS = 800ms grace period after escape
- Precision mode won't re-activate during cooldown even if over small regions
- Cooldown resets on mouse leave
Result: Users can now easily escape precision mode with a quick mouse flick, and it won't immediately re-engage. After 800ms cooldown, precision mode can auto-activate again if still over small regions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fix cursor dampening feedback loop that caused lag when reversing mouse direction. The previous implementation tracked the dampened position, creating momentum/lag when changing direction.
Changes:
- Split lastCursorRef into two separate refs:
- lastRawCursorRef: tracks actual raw mouse position
- dampenedCursorRef: tracks current dampened position
- Calculate delta from raw-to-raw (not dampened-to-raw)
- Direction changes now respond instantly, just at reduced speed
- No more "continuation" effect when reversing direction
Technical details:
- Velocity calculation now uses raw positions for accuracy
- First frame of precision mode initializes dampened cursor at raw position
- Both refs reset on mouse leave
- Debug logging updated to show both raw and dampened positions
Result: Cursor dampening feels responsive and precise, no lag when changing direction.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implement automatic cursor dampening, super zoom on hover, and quick-escape to make sub-pixel regions (Gibraltar 0.08px, Jersey 0.82px) clickable. Fix crosshair accuracy to match dampened cursor position, add excluded region visualization (gray pre-labeled), and increase unfound region contrast (0.3→0.7 opacity).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add intelligent magnifying glass feature with smooth animations:
Magnifier Features:
- Appears when 7+ regions overlap in 50×50px box OR region <8px
- Adaptive zoom (8×-24×) based on region density and size
- Smooth zoom/opacity animations using react-spring
- Dynamic positioning to avoid covering cursor
- Visual indicator box on main map shows magnified area
- Crosshair shows exact cursor position in magnified view
Implementation Details:
- Uses react-spring for smooth zoom and fade transitions
- Position calculated per quadrant (opposite corner from cursor)
- Zoom formula: base 8× + density factor + size factor
- Animated SVG viewBox for seamless zooming
- Dashed blue indicator rectangle tracks magnified region
UI/UX Improvements:
- Remove duplicate turn indicator (use player avatar dock)
- Hide arrow labels feature behind flag (disabled by default)
- Add Storybook stories for map renderer with tuning controls
This makes clicking tiny island nations and crowded regions much easier!
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add continent-based filtering for the world map to make the game more playable by reducing visual clutter:
Continent Selection:
- Add continent selector to setup screen (only for world map)
- 7 continents + "All" option: Africa, Asia, Europe, North America, South America, Oceania, Antarctica
- Each continent button shows emoji and name
Map Filtering:
- Filter world map regions by selected continent using ISO 3166-1 country codes
- Calculate bounding box for filtered regions
- Automatically crop and scale viewBox to show only selected continent
- Add 10% padding around continent bounding box for better visibility
Technical Implementation:
- Create continents.ts with comprehensive country-to-continent mappings (256 countries)
- Add getFilteredMapData() function to filter regions and adjust viewBox
- Add calculateBoundingBox() to compute min/max coordinates from SVG paths
- Add selectedContinent field to game state and config (persisted to database)
- Add SET_CONTINENT move type and validator
- Update all map rendering components (StudyPhase, PlayingPhase, MapRenderer)
Benefits:
- Solves "too many small countries" problem on world map
- Allows focused study of specific geographic regions
- Dynamic viewBox adjustment provides optimal zoom level
- Maintains full world map option for comprehensive play
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add new arcade game for testing geography knowledge:
Game Features:
- 4 phases: Setup, Study, Playing, Results
- 3 multiplayer modes: Cooperative, Race, Turn-Based
- 2 maps: World countries, USA states
- Configurable study mode (0, 30, 60, or 120 seconds)
- Return to Setup and New Game options in game menu
- Small region labels with arrows for improved visibility
Map Rendering:
- 8-color deterministic palette with hash-based assignment
- Opacity-based states (20-27% unfound, 100% found)
- Enhanced label visibility with text shadows
- Smart bounding box calculation for small regions
- Supports both easy (outlines always visible) and hard (outlines on hover/found) difficulty
Game Modes:
- Cooperative: All players work together to find all regions
- Race: First to click gets the point
- Turn-Based: Players take turns finding regions
Study Phase:
- Optional timed study period before quiz starts
- Shows all region labels for memorization
- Countdown timer with skip option
Dependencies:
- Add @svg-maps/world and @svg-maps/usa packages
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>