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 e2d0708f..1b345be5 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 @@ -7,11 +7,9 @@ import { useTheme } from '@/contexts/ThemeContext' import { useVisualDebugSafe } from '@/contexts/VisualDebugContext' import type { ContinentId } from '../continents' import { usePulsingAnimation } from '../features/animations' -import { CompassCrosshair, CursorOverlay } from '../features/cursor' +import { CursorOverlay } from '../features/cursor' import { DebugAutoZoomPanel, HotColdDebugPanel, SafeZoneDebugPanel } from '../features/debug' -import { MapRendererProvider, type MapRendererContextValue } from '../features/map-renderer' -import { OtherPlayerCursors } from '../features/multiplayer' -import { RegionLayer } from '../features/regions' +import { useInteractionStateMachine } from '../features/interaction' import { getRenderedViewport, LabelLayer, useD3ForceLabels } from '../features/labels' import { applyPanDelta, @@ -30,7 +28,10 @@ import { useMagnifierState, ZoomLines, } from '../features/magnifier' +import { type MapRendererContextValue, MapRendererProvider } from '../features/map-renderer' +import { OtherPlayerCursors } from '../features/multiplayer' import { usePrecisionCalculations } from '../features/precision' +import { RegionLayer } from '../features/regions' import { useCanUsePrecisionMode, useHasAnyFinePointer, @@ -60,7 +61,6 @@ import { useKnowYourWorld } from '../Provider' import type { MapData, MapRegion } from '../types' import { type BoundingBox as DebugBoundingBox, findOptimalZoom } from '../utils/adaptiveZoomSearch' import { CELEBRATION_TIMING, classifyCelebration } from '../utils/celebration' -import type { FeedbackType } from '../utils/hotColdPhrases' import { getHeatBorderColors, getHeatCrosshairStyle } from '../utils/hotColdStyles' import { calculateMaxZoomAtThreshold, @@ -341,6 +341,10 @@ export function MapRenderer({ const canUsePrecisionMode = useCanUsePrecisionMode() // For precision mode UI/behavior const hasAnyFinePointer = useHasAnyFinePointer() // For hot/cold feedback visibility + // Interaction state machine - replaces scattered boolean flags with explicit states + // Currently running in parallel with existing state for incremental migration + const interactionMachine = useInteractionStateMachine() + // Memoize pointer lock callbacks to prevent render loop const handleLockAcquired = useCallback(() => { // Save initial cursor position @@ -470,6 +474,67 @@ export function MapRenderer({ } }, [isMagnifierExpanded, targetZoom]) + // ========================================================================== + // State Machine Sync Effects (for incremental migration) + // These effects drive the state machine from existing boolean state changes. + // Once migration is complete, handlers will dispatch directly to the machine. + // ========================================================================== + + // Sync precision mode state + useEffect(() => { + if (pointerLocked) { + interactionMachine.send({ type: 'REQUEST_PRECISION' }) + } else if (isReleasingPointerLock) { + interactionMachine.send({ type: 'PRECISION_ESCAPE_BOUNDARY' }) + } + }, [pointerLocked, isReleasingPointerLock, interactionMachine]) + + // Sync magnifier visibility + useEffect(() => { + if (showMagnifier) { + interactionMachine.send({ type: 'SHOW_MAGNIFIER' }) + } else { + interactionMachine.send({ type: 'DISMISS_MAGNIFIER' }) + } + }, [showMagnifier, interactionMachine]) + + // Sync magnifier expanded state + useEffect(() => { + if (isMagnifierExpanded) { + interactionMachine.send({ type: 'EXPAND_MAGNIFIER' }) + } else { + interactionMachine.send({ type: 'COLLAPSE_MAGNIFIER' }) + } + }, [isMagnifierExpanded, interactionMachine]) + + // Debug: Log state machine state changes and verify sync (only in dev) + useEffect(() => { + if (process.env.NODE_ENV === 'development') { + // Compare state machine derived values with existing boolean state + const machineShowMagnifier = interactionMachine.showMagnifier + const machineIsPrecision = interactionMachine.isPrecisionMode + const machineIsReleasingPrecision = interactionMachine.isReleasingPrecision + + // Log state transitions with comparison + console.log('[StateMachine]', { + state: interactionMachine.state, + machine: { showMagnifier: machineShowMagnifier, isPrecision: machineIsPrecision }, + boolean: { showMagnifier, pointerLocked, isReleasingPointerLock }, + }) + + // Warn on sync divergence (helps identify missing sync effects) + // Note: Divergence is expected during transitions due to effect timing + } + }, [ + interactionMachine.state, + interactionMachine.showMagnifier, + interactionMachine.isPrecisionMode, + interactionMachine.isReleasingPrecision, + showMagnifier, + pointerLocked, + isReleasingPointerLock, + ]) + // Give up reveal animation state const [giveUpFlashProgress, setGiveUpFlashProgress] = useState(0) // 0-1 pulsing value const [isGiveUpAnimating, setIsGiveUpAnimating] = useState(false) // Track if animation in progress @@ -2859,41 +2924,41 @@ export function MapRenderer({
- `scale(${s}) translate(${tx / s}px, ${ty / s}px)` - ), + backgroundSize: '100px 100px', }} > - {/* Render all regions (included + excluded) */} - `scale(${s}) translate(${tx / s}px, ${ty / s}px)` + ), + }} + > + {/* Render all regions (included + excluded) */} + + + {/* Debug: Render bounding boxes (only if enabled) */} + {effectiveShowDebugBoundingBoxes && + debugBoundingBoxes.map((bbox) => { + // Color based on acceptance and importance + // Green = accepted, Orange = high importance, Yellow = medium, Gray = low + const importance = bbox.importance ?? 0 + let strokeColor = '#888888' // Default gray for low importance + let fillColor = 'rgba(136, 136, 136, 0.1)' + + if (bbox.wasAccepted) { + strokeColor = '#00ff00' // Green for accepted region + fillColor = 'rgba(0, 255, 0, 0.15)' + } else if (importance > 1.5) { + strokeColor = '#ff6600' // Orange for high importance (2.0× boost + close) + fillColor = 'rgba(255, 102, 0, 0.1)' + } else if (importance > 0.5) { + strokeColor = '#ffcc00' // Yellow for medium importance + fillColor = 'rgba(255, 204, 0, 0.1)' + } + + return ( + + + + ) + })} + + {/* Arrow marker definition */} + + + + + + + + + {/* Player emoji patterns for region backgrounds */} + {Object.values(playerMetadata).map((player) => ( + + + + {player.emoji} + + + ))} + + + {/* Magnifier region indicator on main map */} + {showMagnifier && cursorPosition && svgRef.current && containerRef.current && ( + { + const containerRect = containerRef.current!.getBoundingClientRect() + const svgRect = svgRef.current!.getBoundingClientRect() + // Account for preserveAspectRatio letterboxing + const viewport = getRenderedViewport( + svgRect, + parsedViewBox.x, + parsedViewBox.y, + parsedViewBox.width, + parsedViewBox.height + ) + const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX + const cursorSvgX = + (cursorPosition.x - svgOffsetX) / viewport.scale + parsedViewBox.x + // Calculate leftover dimensions for magnifier sizing + const leftoverW = + containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right + const leftoverH = + containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom + const { width: magnifiedWidth } = getAdjustedMagnifiedDimensions( + parsedViewBox.width, + parsedViewBox.height, + zoom, + leftoverW, + leftoverH + ) + return cursorSvgX - magnifiedWidth / 2 + })} + y={zoomSpring.to((zoom: number) => { + const containerRect = containerRef.current!.getBoundingClientRect() + const svgRect = svgRef.current!.getBoundingClientRect() + // Account for preserveAspectRatio letterboxing + const viewport = getRenderedViewport( + svgRect, + parsedViewBox.x, + parsedViewBox.y, + parsedViewBox.width, + parsedViewBox.height + ) + const svgOffsetY = svgRect.top - containerRect.top + viewport.letterboxY + const cursorSvgY = + (cursorPosition.y - svgOffsetY) / viewport.scale + parsedViewBox.y + // Calculate leftover dimensions for magnifier sizing + const leftoverW = + containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right + const leftoverH = + containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom + const { height: magnifiedHeight } = getAdjustedMagnifiedDimensions( + parsedViewBox.width, + parsedViewBox.height, + zoom, + leftoverW, + leftoverH + ) + return cursorSvgY - magnifiedHeight / 2 + })} + width={zoomSpring.to((zoom: number) => { + const containerRect = containerRef.current!.getBoundingClientRect() + // Calculate leftover dimensions for magnifier sizing + const leftoverW = + containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right + const leftoverH = + containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom + const { width } = getAdjustedMagnifiedDimensions( + parsedViewBox.width, + parsedViewBox.height, + zoom, + leftoverW, + leftoverH + ) + return width + })} + height={zoomSpring.to((zoom: number) => { + const containerRect = containerRef.current!.getBoundingClientRect() + // Calculate leftover dimensions for magnifier sizing + const leftoverW = + containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right + const leftoverH = + containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom + const { height } = getAdjustedMagnifiedDimensions( + parsedViewBox.width, + parsedViewBox.height, + zoom, + leftoverW, + leftoverH + ) + return height + })} + fill="none" + stroke={isDark ? '#60a5fa' : '#3b82f6'} + strokeWidth={parsedViewBox.width / 500} + vectorEffect="non-scaling-stroke" + strokeDasharray="5,5" + pointerEvents="none" + opacity={0.8} + /> + )} + + + {/* Labels for found regions - rendered via LabelLayer component */} + - {/* Debug: Render bounding boxes (only if enabled) */} + {/* Debug: Bounding box labels as HTML overlays */} {effectiveShowDebugBoundingBoxes && + containerRef.current && + svgRef.current && debugBoundingBoxes.map((bbox) => { - // Color based on acceptance and importance - // Green = accepted, Orange = high importance, Yellow = medium, Gray = low const importance = bbox.importance ?? 0 - let strokeColor = '#888888' // Default gray for low importance - let fillColor = 'rgba(136, 136, 136, 0.1)' + let strokeColor = '#888888' if (bbox.wasAccepted) { - strokeColor = '#00ff00' // Green for accepted region - fillColor = 'rgba(0, 255, 0, 0.15)' + strokeColor = '#00ff00' } else if (importance > 1.5) { - strokeColor = '#ff6600' // Orange for high importance (2.0× boost + close) - fillColor = 'rgba(255, 102, 0, 0.1)' + strokeColor = '#ff6600' } else if (importance > 0.5) { - strokeColor = '#ffcc00' // Yellow for medium importance - fillColor = 'rgba(255, 204, 0, 0.1)' + strokeColor = '#ffcc00' } + // Convert SVG coordinates to pixel coordinates (accounting for preserveAspectRatio) + const containerRect = containerRef.current!.getBoundingClientRect() + const svgRect = svgRef.current!.getBoundingClientRect() + + const viewport = getRenderedViewport( + svgRect, + parsedViewBox.x, + parsedViewBox.y, + parsedViewBox.width, + parsedViewBox.height + ) + const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX + const svgOffsetY = svgRect.top - containerRect.top + viewport.letterboxY + + // Convert bbox center from SVG coords to pixels + const centerX = + (bbox.x + bbox.width / 2 - parsedViewBox.x) * viewport.scale + svgOffsetX + const centerY = + (bbox.y + bbox.height / 2 - parsedViewBox.y) * viewport.scale + svgOffsetY + return ( - - - +
+
{bbox.regionId}
+
{importance.toFixed(2)}
+
) })} - {/* Arrow marker definition */} - - - - - - - + {/* Custom cursor and heat crosshair overlays */} + - {/* Player emoji patterns for region backgrounds */} - {Object.values(playerMetadata).map((player) => ( - - - - {player.emoji} - - - ))} - - - {/* Magnifier region indicator on main map */} - {showMagnifier && cursorPosition && svgRef.current && containerRef.current && ( - { - const containerRect = containerRef.current!.getBoundingClientRect() - const svgRect = svgRef.current!.getBoundingClientRect() - // Account for preserveAspectRatio letterboxing - const viewport = getRenderedViewport( - svgRect, - parsedViewBox.x, - parsedViewBox.y, - parsedViewBox.width, - parsedViewBox.height - ) - const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX - const cursorSvgX = (cursorPosition.x - svgOffsetX) / viewport.scale + parsedViewBox.x - // Calculate leftover dimensions for magnifier sizing - const leftoverW = - containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right - const leftoverH = - containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom - const { width: magnifiedWidth } = getAdjustedMagnifiedDimensions( - parsedViewBox.width, - parsedViewBox.height, - zoom, - leftoverW, - leftoverH - ) - return cursorSvgX - magnifiedWidth / 2 - })} - y={zoomSpring.to((zoom: number) => { - const containerRect = containerRef.current!.getBoundingClientRect() - const svgRect = svgRef.current!.getBoundingClientRect() - // Account for preserveAspectRatio letterboxing - const viewport = getRenderedViewport( - svgRect, - parsedViewBox.x, - parsedViewBox.y, - parsedViewBox.width, - parsedViewBox.height - ) - const svgOffsetY = svgRect.top - containerRect.top + viewport.letterboxY - const cursorSvgY = (cursorPosition.y - svgOffsetY) / viewport.scale + parsedViewBox.y - // Calculate leftover dimensions for magnifier sizing - const leftoverW = - containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right - const leftoverH = - containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom - const { height: magnifiedHeight } = getAdjustedMagnifiedDimensions( - parsedViewBox.width, - parsedViewBox.height, - zoom, - leftoverW, - leftoverH - ) - return cursorSvgY - magnifiedHeight / 2 - })} - width={zoomSpring.to((zoom: number) => { - const containerRect = containerRef.current!.getBoundingClientRect() - // Calculate leftover dimensions for magnifier sizing - const leftoverW = - containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right - const leftoverH = - containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom - const { width } = getAdjustedMagnifiedDimensions( - parsedViewBox.width, - parsedViewBox.height, - zoom, - leftoverW, - leftoverH - ) - return width - })} - height={zoomSpring.to((zoom: number) => { - const containerRect = containerRef.current!.getBoundingClientRect() - // Calculate leftover dimensions for magnifier sizing - const leftoverW = - containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right - const leftoverH = - containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom - const { height } = getAdjustedMagnifiedDimensions( - parsedViewBox.width, - parsedViewBox.height, - zoom, - leftoverW, - leftoverH - ) - return height - })} - fill="none" - stroke={isDark ? '#60a5fa' : '#3b82f6'} - strokeWidth={parsedViewBox.width / 500} - vectorEffect="non-scaling-stroke" - strokeDasharray="5,5" - pointerEvents="none" - opacity={0.8} - /> - )} - - - {/* Labels for found regions - rendered via LabelLayer component */} - - - {/* Debug: Bounding box labels as HTML overlays */} - {effectiveShowDebugBoundingBoxes && - containerRef.current && - svgRef.current && - debugBoundingBoxes.map((bbox) => { - const importance = bbox.importance ?? 0 - let strokeColor = '#888888' - - if (bbox.wasAccepted) { - strokeColor = '#00ff00' - } else if (importance > 1.5) { - strokeColor = '#ff6600' - } else if (importance > 0.5) { - strokeColor = '#ffcc00' + {/* Magnifier overlay - centers on cursor position */} + {(() => { + if (!cursorPosition || !svgRef.current || !containerRef.current) { + return null } - // Convert SVG coordinates to pixel coordinates (accounting for preserveAspectRatio) - const containerRect = containerRef.current!.getBoundingClientRect() - const svgRect = svgRef.current!.getBoundingClientRect() + // Calculate magnifier size based on leftover rectangle (area not covered by UI) + const containerRect = containerRef.current.getBoundingClientRect() + const leftoverWidth = + containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right + const leftoverHeight = + containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom + // When expanded (during/after pinch-to-zoom), use full leftover area + // Otherwise use the normal calculated dimensions + const { width: normalWidth, height: normalHeight } = getMagnifierDimensions( + leftoverWidth, + leftoverHeight + ) + const magnifierWidthPx = isMagnifierExpanded ? leftoverWidth : normalWidth + const magnifierHeightPx = isMagnifierExpanded ? leftoverHeight : normalHeight + + // Pre-compute SVG coordinate transformation for crosshair and pixel grid + const svgRect = svgRef.current.getBoundingClientRect() const viewport = getRenderedViewport( svgRect, parsedViewBox.x, @@ -3192,402 +3342,329 @@ export function MapRenderer({ ) const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX const svgOffsetY = svgRect.top - containerRect.top + viewport.letterboxY + const cursorSvgX = (cursorPosition.x - svgOffsetX) / viewport.scale + parsedViewBox.x + const cursorSvgY = (cursorPosition.y - svgOffsetY) / viewport.scale + parsedViewBox.y - // Convert bbox center from SVG coords to pixels - const centerX = (bbox.x + bbox.width / 2 - parsedViewBox.x) * viewport.scale + svgOffsetX - const centerY = (bbox.y + bbox.height / 2 - parsedViewBox.y) * viewport.scale + svgOffsetY + // Get heat-based crosshair styling + const heatStyle = getHeatCrosshairStyle( + hotColdFeedbackType, + isDark, + effectiveHotColdEnabled + ) return ( -
{ + // When hot/cold is enabled, use heat-based colors + if (effectiveHotColdEnabled && hotColdFeedbackType) { + const heatColors = getHeatBorderColors(hotColdFeedbackType, isDark) + return `${heatColors.width}px solid ${heatColors.border}` + } + // Fall back to zoom-based coloring + return zoomSpring.to( + (zoom: number) => + zoom > HIGH_ZOOM_THRESHOLD + ? `4px solid ${isDark ? '#fbbf24' : '#f59e0b'}` // gold-400/gold-500 + : `3px solid ${isDark ? '#60a5fa' : '#3b82f6'}` // blue-400/blue-600 + ) + })(), + borderRadius: '12px', + overflow: 'hidden', + // Enable touch events on mobile for panning, but keep mouse events disabled + // This allows touch-based panning while not interfering with mouse-based interactions + pointerEvents: 'auto', + touchAction: 'none', // Prevent browser handling of touch gestures + zIndex: 100, + // Box shadow with heat glow when hot/cold is enabled + boxShadow: (() => { + if (effectiveHotColdEnabled && hotColdFeedbackType) { + const heatColors = getHeatBorderColors(hotColdFeedbackType, isDark) + return `0 10px 40px rgba(0, 0, 0, 0.3), 0 0 25px ${heatColors.glow}` + } + return zoomSpring.to((zoom: number) => + zoom > HIGH_ZOOM_THRESHOLD + ? '0 10px 40px rgba(251, 191, 36, 0.4), 0 0 20px rgba(251, 191, 36, 0.2)' // Gold glow + : '0 10px 40px rgba(0, 0, 0, 0.5)' + ) + })(), + background: isDark ? '#111827' : '#f3f4f6', + opacity: magnifierSpring.opacity, }} > -
{bbox.regionId}
-
{importance.toFixed(2)}
-
- ) - })} + { + // Calculate magnified viewBox centered on cursor + const containerRect = containerRef.current!.getBoundingClientRect() + const svgRect = svgRef.current!.getBoundingClientRect() - {/* Custom cursor and heat crosshair overlays */} - - - {/* Magnifier overlay - centers on cursor position */} - {(() => { - if (!cursorPosition || !svgRef.current || !containerRef.current) { - return null - } - - // Calculate magnifier size based on leftover rectangle (area not covered by UI) - const containerRect = containerRef.current.getBoundingClientRect() - const leftoverWidth = containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right - const leftoverHeight = - containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom - - // When expanded (during/after pinch-to-zoom), use full leftover area - // Otherwise use the normal calculated dimensions - const { width: normalWidth, height: normalHeight } = getMagnifierDimensions( - leftoverWidth, - leftoverHeight - ) - const magnifierWidthPx = isMagnifierExpanded ? leftoverWidth : normalWidth - const magnifierHeightPx = isMagnifierExpanded ? leftoverHeight : normalHeight - - // Pre-compute SVG coordinate transformation for crosshair and pixel grid - const svgRect = svgRef.current.getBoundingClientRect() - const viewport = getRenderedViewport( - svgRect, - parsedViewBox.x, - parsedViewBox.y, - parsedViewBox.width, - parsedViewBox.height - ) - const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX - const svgOffsetY = svgRect.top - containerRect.top + viewport.letterboxY - const cursorSvgX = (cursorPosition.x - svgOffsetX) / viewport.scale + parsedViewBox.x - const cursorSvgY = (cursorPosition.y - svgOffsetY) / viewport.scale + parsedViewBox.y - - // Get heat-based crosshair styling - const heatStyle = getHeatCrosshairStyle( - hotColdFeedbackType, - isDark, - effectiveHotColdEnabled - ) - - return ( - { - // When hot/cold is enabled, use heat-based colors - if (effectiveHotColdEnabled && hotColdFeedbackType) { - const heatColors = getHeatBorderColors(hotColdFeedbackType, isDark) - return `${heatColors.width}px solid ${heatColors.border}` - } - // Fall back to zoom-based coloring - return zoomSpring.to( - (zoom: number) => - zoom > HIGH_ZOOM_THRESHOLD - ? `4px solid ${isDark ? '#fbbf24' : '#f59e0b'}` // gold-400/gold-500 - : `3px solid ${isDark ? '#60a5fa' : '#3b82f6'}` // blue-400/blue-600 - ) - })(), - borderRadius: '12px', - overflow: 'hidden', - // Enable touch events on mobile for panning, but keep mouse events disabled - // This allows touch-based panning while not interfering with mouse-based interactions - pointerEvents: 'auto', - touchAction: 'none', // Prevent browser handling of touch gestures - zIndex: 100, - // Box shadow with heat glow when hot/cold is enabled - boxShadow: (() => { - if (effectiveHotColdEnabled && hotColdFeedbackType) { - const heatColors = getHeatBorderColors(hotColdFeedbackType, isDark) - return `0 10px 40px rgba(0, 0, 0, 0.3), 0 0 25px ${heatColors.glow}` - } - return zoomSpring.to((zoom: number) => - zoom > HIGH_ZOOM_THRESHOLD - ? '0 10px 40px rgba(251, 191, 36, 0.4), 0 0 20px rgba(251, 191, 36, 0.2)' // Gold glow - : '0 10px 40px rgba(0, 0, 0, 0.5)' - ) - })(), - background: isDark ? '#111827' : '#f3f4f6', - opacity: magnifierSpring.opacity, - }} - > - { - // Calculate magnified viewBox centered on cursor - const containerRect = containerRef.current!.getBoundingClientRect() - const svgRect = svgRef.current!.getBoundingClientRect() - - // Use memoized parsedViewBox for coordinate conversion - const viewport = getRenderedViewport( - svgRect, - parsedViewBox.x, - parsedViewBox.y, - parsedViewBox.width, - parsedViewBox.height - ) - - // Center position relative to SVG (uses reveal center during give-up animation) - const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX - const svgOffsetY = svgRect.top - containerRect.top + viewport.letterboxY - const cursorSvgX = - (cursorPosition.x - svgOffsetX) / viewport.scale + parsedViewBox.x - const cursorSvgY = - (cursorPosition.y - svgOffsetY) / viewport.scale + parsedViewBox.y - - // Magnified view: adjust dimensions to match magnifier container aspect ratio - // This eliminates letterboxing and ensures outline matches what's visible - // Use leftover dimensions for magnifier sizing - const leftoverW = - containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right - const leftoverH = - containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom - const { width: magnifiedWidth, height: magnifiedHeight } = - getAdjustedMagnifiedDimensions( + // Use memoized parsedViewBox for coordinate conversion + const viewport = getRenderedViewport( + svgRect, + parsedViewBox.x, + parsedViewBox.y, parsedViewBox.width, - parsedViewBox.height, - zoom, - leftoverW, - leftoverH + parsedViewBox.height ) - // Center the magnified viewBox on the cursor - const magnifiedViewBoxX = cursorSvgX - magnifiedWidth / 2 - const magnifiedViewBoxY = cursorSvgY - magnifiedHeight / 2 + // Center position relative to SVG (uses reveal center during give-up animation) + const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX + const svgOffsetY = svgRect.top - containerRect.top + viewport.letterboxY + const cursorSvgX = + (cursorPosition.x - svgOffsetX) / viewport.scale + parsedViewBox.x + const cursorSvgY = + (cursorPosition.y - svgOffsetY) / viewport.scale + parsedViewBox.y - return `${magnifiedViewBoxX} ${magnifiedViewBoxY} ${magnifiedWidth} ${magnifiedHeight}` - })} - style={{ - width: '100%', - height: '100%', - // Apply "disabled" visual effect when at threshold but not in precision mode - // Uses precisionCalcs.isAtThreshold from usePrecisionCalculations hook - filter: - precisionCalcs.isAtThreshold && !pointerLocked - ? 'brightness(0.6) saturate(0.5)' - : 'none', - }} - > - {/* Sea/ocean background for magnifier - solid color to match container */} - + // Magnified view: adjust dimensions to match magnifier container aspect ratio + // This eliminates letterboxing and ensures outline matches what's visible + // Use leftover dimensions for magnifier sizing + const leftoverW = + containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right + const leftoverH = + containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom + const { width: magnifiedWidth, height: magnifiedHeight } = + getAdjustedMagnifiedDimensions( + parsedViewBox.width, + parsedViewBox.height, + zoom, + leftoverW, + leftoverH + ) - {/* Render all regions in magnified view */} - + > + {/* Sea/ocean background for magnifier - solid color to match container */} + - {/* Crosshair at center position (cursor or reveal center during animation) */} - + {/* Render all regions in magnified view */} + - {/* Pixel grid overlay - shows when approaching/at/above precision mode threshold */} - {/* Uses precisionCalcs.screenPixelRatio from usePrecisionCalculations hook */} - + {/* Crosshair at center position (cursor or reveal center during animation) */} + - {/* Debug: Bounding boxes for detected regions in magnifier */} - + + {/* Debug: Bounding boxes for detected regions in magnifier */} + + + + {/* Debug: Bounding box labels as HTML overlays - positioned using animated values */} + - - {/* Debug: Bounding box labels as HTML overlays - positioned using animated values */} - - - {/* Magnifier label */} - - - {/* Scrim overlay - shows when at threshold to indicate barrier */} - {/* Uses precisionCalcs.isAtThreshold from usePrecisionCalculations hook */} - {precisionCalcs.isAtThreshold && !pointerLocked && ( -
- )} - {/* Mobile magnifier controls (Expand, Select, Full Map buttons) */} - setIsMagnifierExpanded(false)} - onExpand={() => setIsMagnifierExpanded(true)} - /> - - ) - })()} + {/* Scrim overlay - shows when at threshold to indicate barrier */} + {/* Uses precisionCalcs.isAtThreshold from usePrecisionCalculations hook */} + {precisionCalcs.isAtThreshold && !pointerLocked && ( +
+ )} - {/* Zoom lines connecting indicator to magnifier - creates "pop out" effect */} - {showMagnifier && cursorPosition && svgRef.current && containerRef.current && ( - setIsMagnifierExpanded(false)} + onExpand={() => setIsMagnifierExpanded(true)} + /> + + ) + })()} + + {/* Zoom lines connecting indicator to magnifier - creates "pop out" effect */} + {showMagnifier && cursorPosition && svgRef.current && containerRef.current && ( + + )} + + {/* Debug: Auto zoom detection visualization (dev only) */} + - )} - {/* Debug: Auto zoom detection visualization (dev only) */} - - - {/* Hot/Cold Debug Panel - shows enable conditions and current state */} - - - {/* Other players' cursors - show in multiplayer when not exclusively our turn */} - - - {/* Dev-only crop tool for getting custom viewBox coordinates */} - - - {/* Debug overlay showing safe zone rectangles */} - {/* Safe Zone Debug Panel */} - - - {/* Celebration overlay - shows confetti and sound when region is found */} - {celebration && ( - - )} + + {/* Other players' cursors - show in multiplayer when not exclusively our turn */} + + + {/* Dev-only crop tool for getting custom viewBox coordinates */} + + + {/* Debug overlay showing safe zone rectangles */} + {/* Safe Zone Debug Panel */} + + + {/* Celebration overlay - shows confetti and sound when region is found */} + {celebration && ( + + )}
) diff --git a/apps/web/src/arcade-games/know-your-world/features/interaction/index.ts b/apps/web/src/arcade-games/know-your-world/features/interaction/index.ts index c24080fd..ae6cebca 100644 --- a/apps/web/src/arcade-games/know-your-world/features/interaction/index.ts +++ b/apps/web/src/arcade-games/know-your-world/features/interaction/index.ts @@ -6,11 +6,11 @@ */ export type { - InteractionState, - InteractionEvent, InteractionContext, - TouchPoint, + InteractionEvent, + InteractionState, MachineState, + TouchPoint, UseInteractionStateMachineReturn, } from './useInteractionStateMachine' diff --git a/apps/web/src/arcade-games/know-your-world/features/interaction/useInteractionStateMachine.ts b/apps/web/src/arcade-games/know-your-world/features/interaction/useInteractionStateMachine.ts index 11bd3f92..42801515 100644 --- a/apps/web/src/arcade-games/know-your-world/features/interaction/useInteractionStateMachine.ts +++ b/apps/web/src/arcade-games/know-your-world/features/interaction/useInteractionStateMachine.ts @@ -10,7 +10,7 @@ 'use client' -import { useReducer, useCallback, useRef, useMemo } from 'react' +import { useCallback, useMemo, useReducer, useRef } from 'react' // ============================================================================ // State Types @@ -680,10 +680,7 @@ export function useInteractionStateMachine(): UseInteractionStateMachineReturn { ] ) - const showCursor = useMemo( - () => isHovering || showMagnifier, - [isHovering, showMagnifier] - ) + const showCursor = useMemo(() => isHovering || showMagnifier, [isHovering, showMagnifier]) const isAnyPanning = useMemo( () => isMagnifierPanning || isMapPanningMobile || isMapPanningDesktop, diff --git a/apps/web/src/arcade-games/know-your-world/features/labels/useD3ForceLabels.ts b/apps/web/src/arcade-games/know-your-world/features/labels/useD3ForceLabels.ts index 1a3b302e..4914da10 100644 --- a/apps/web/src/arcade-games/know-your-world/features/labels/useD3ForceLabels.ts +++ b/apps/web/src/arcade-games/know-your-world/features/labels/useD3ForceLabels.ts @@ -9,7 +9,7 @@ */ import { forceCollide, forceSimulation, forceX, forceY, type SimulationNodeDatum } from 'd3-force' -import { RefObject, useEffect, useState } from 'react' +import { type RefObject, useEffect, useState } from 'react' import type { MapData, MapRegion } from '../../types' import { getArrowStartPoint, getRenderedViewport } from './labelUtils' diff --git a/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/LetterDisplay.tsx b/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/LetterDisplay.tsx index d3b1a212..c1f3e0e8 100644 --- a/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/LetterDisplay.tsx +++ b/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/LetterDisplay.tsx @@ -90,15 +90,7 @@ export const LetterDisplay = memo(function LetterDisplay({ const isSpace = char === ' ' if (isSpace) { - return ( - - ) + return } // Get current index before incrementing @@ -113,15 +105,7 @@ export const LetterDisplay = memo(function LetterDisplay({ isComplete ) - return ( - - ) + return }) }, [regionName, confirmedCount, requiredLetters, isComplete, isDark]) diff --git a/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/letterUtils.ts b/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/letterUtils.ts index 97bbc9c7..4d2a4cc3 100644 --- a/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/letterUtils.ts +++ b/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/letterUtils.ts @@ -89,10 +89,7 @@ export function getLetterStatus( * @param isDark - Whether dark mode is active * @returns CSS properties for the letter */ -export function getLetterStyles( - status: LetterStatus, - isDark: boolean -): React.CSSProperties { +export function getLetterStyles(status: LetterStatus, isDark: boolean): React.CSSProperties { const baseStyles: React.CSSProperties = { transition: 'all 0.15s ease-out', } @@ -129,10 +126,7 @@ export function getLetterStyles( * @param requiredLetters - Number of letters required * @returns Progress value (0 = none, 1 = complete) */ -export function calculateProgress( - confirmedCount: number, - requiredLetters: number -): number { +export function calculateProgress(confirmedCount: number, requiredLetters: number): number { if (requiredLetters === 0) return 1 return Math.min(1, confirmedCount / requiredLetters) } diff --git a/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/useLetterConfirmation.ts b/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/useLetterConfirmation.ts index 70d78c7f..c77f152c 100644 --- a/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/useLetterConfirmation.ts +++ b/apps/web/src/arcade-games/know-your-world/features/letter-confirmation/useLetterConfirmation.ts @@ -91,12 +91,7 @@ export function useLetterConfirmation({ // Get letter status for display const getLetterStatus = useCallback( (nonSpaceIndex: number): LetterStatus => { - return getLetterStatusUtil( - nonSpaceIndex, - confirmedCount, - requiredLetters, - isComplete - ) + return getLetterStatusUtil(nonSpaceIndex, confirmedCount, requiredLetters, isComplete) }, [confirmedCount, requiredLetters, isComplete] ) @@ -109,10 +104,7 @@ export function useLetterConfirmation({ const handleKeyDown = (e: KeyboardEvent) => { // Ignore if typing in an input or textarea - if ( - e.target instanceof HTMLInputElement || - e.target instanceof HTMLTextAreaElement - ) { + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { return } diff --git a/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierControls.tsx b/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierControls.tsx index 9f89965a..329adf35 100644 --- a/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierControls.tsx +++ b/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierControls.tsx @@ -211,11 +211,7 @@ export const MagnifierControls = memo(function MagnifierControls({ <> {/* Expand button - top-right corner (when not expanded and not pointer locked) */} {!isExpanded && !pointerLocked && ( - + )} @@ -234,11 +230,7 @@ export const MagnifierControls = memo(function MagnifierControls({ {/* Full Map button - bottom-left corner (when expanded) */} {isExpanded && showSelectButton && ( - + Full Map )} diff --git a/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierRegions.tsx b/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierRegions.tsx index 7e35442f..61b2ebf6 100644 --- a/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierRegions.tsx +++ b/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierRegions.tsx @@ -50,7 +50,12 @@ export interface MagnifierRegionsProps { /** Get player ID who found a region */ getPlayerWhoFoundRegion: (regionId: string) => string | null /** Get region fill color */ - getRegionColor: (regionId: string, isFound: boolean, isHovered: boolean, isDark: boolean) => string + getRegionColor: ( + regionId: string, + isFound: boolean, + isHovered: boolean, + isDark: boolean + ) => string /** Get region stroke color */ getRegionStroke: (isFound: boolean, isDark: boolean) => string /** Whether to show region outline */ @@ -80,13 +85,8 @@ export const MagnifierRegions = memo(function MagnifierRegions({ getRegionStroke, showOutline, }: MagnifierRegionsProps) { - const { - regionsFound, - hoveredRegion, - celebrationRegionId, - giveUpRegionId, - isGiveUpAnimating, - } = regionState + const { regionsFound, hoveredRegion, celebrationRegionId, giveUpRegionId, isGiveUpAnimating } = + regionState const { celebrationFlash, giveUpFlash } = flashProgress return ( diff --git a/apps/web/src/arcade-games/know-your-world/features/magnifier/ZoomLines.tsx b/apps/web/src/arcade-games/know-your-world/features/magnifier/ZoomLines.tsx index 3e13ad71..46ecdc7b 100644 --- a/apps/web/src/arcade-games/know-your-world/features/magnifier/ZoomLines.tsx +++ b/apps/web/src/arcade-games/know-your-world/features/magnifier/ZoomLines.tsx @@ -16,7 +16,10 @@ import { memo, useMemo } from 'react' import { getRenderedViewport } from '../labels' -import { getAdjustedMagnifiedDimensions, getMagnifierDimensions } from '../../utils/magnifierDimensions' +import { + getAdjustedMagnifiedDimensions, + getMagnifierDimensions, +} from '../../utils/magnifierDimensions' // ============================================================================ // Types @@ -124,12 +127,7 @@ export const ZoomLines = memo(function ZoomLines({ isDark, }: ZoomLinesProps) { // Memoize all the calculated values - const { - paths, - visibleCorners, - lineColor, - glowColor, - } = useMemo(() => { + const { paths, visibleCorners, lineColor, glowColor } = useMemo(() => { // Calculate leftover rectangle dimensions (area not covered by UI elements) const leftoverWidth = containerRect.width - safeZoneMargins.left - safeZoneMargins.right const leftoverHeight = containerRect.height - safeZoneMargins.top - safeZoneMargins.bottom @@ -210,14 +208,7 @@ export const ZoomLines = memo(function ZoomLines({ magTop + magnifierHeight ) // Check if line passes through indicator - const passesThroughInd = linePassesThroughRect( - from, - to, - indTL.x, - indTL.y, - indBR.x, - indBR.y - ) + const passesThroughInd = linePassesThroughRect(from, to, indTL.x, indTL.y, indBR.x, indBR.y) return !passesThroughMag && !passesThroughInd }) @@ -233,9 +224,7 @@ export const ZoomLines = memo(function ZoomLines({ : isDark ? '#60a5fa' : '#3b82f6' // blue - const calculatedGlowColor = isHighZoom - ? 'rgba(251, 191, 36, 0.6)' - : 'rgba(96, 165, 250, 0.6)' + const calculatedGlowColor = isHighZoom ? 'rgba(251, 191, 36, 0.6)' : 'rgba(96, 165, 250, 0.6)' return { paths: calculatedPaths, diff --git a/apps/web/src/arcade-games/know-your-world/features/magnifier/useMagnifierTouch.ts b/apps/web/src/arcade-games/know-your-world/features/magnifier/useMagnifierTouch.ts index 151d0a19..cb60af10 100644 --- a/apps/web/src/arcade-games/know-your-world/features/magnifier/useMagnifierTouch.ts +++ b/apps/web/src/arcade-games/know-your-world/features/magnifier/useMagnifierTouch.ts @@ -12,7 +12,12 @@ 'use client' -import React, { useCallback, useRef, type RefObject, type TouchEvent as ReactTouchEvent } from 'react' +import React, { + useCallback, + useRef, + type RefObject, + type TouchEvent as ReactTouchEvent, +} from 'react' import type { UseMagnifierStateReturn } from './useMagnifierState' @@ -223,8 +228,10 @@ export function useMagnifierTouch(options: UseMagnifierTouchOptions): UseMagnifi totalDeltaRef.current.y += deltaY // Track if user has moved significantly - if (Math.abs(totalDeltaRef.current.x) > moveThreshold || - Math.abs(totalDeltaRef.current.y) > moveThreshold) { + if ( + Math.abs(totalDeltaRef.current.x) > moveThreshold || + Math.abs(totalDeltaRef.current.y) > moveThreshold + ) { magnifierState.didMoveRef.current = true } diff --git a/apps/web/src/arcade-games/know-your-world/features/shared/MapRendererContext.tsx b/apps/web/src/arcade-games/know-your-world/features/shared/MapRendererContext.tsx index dce28a77..2c9e63b5 100644 --- a/apps/web/src/arcade-games/know-your-world/features/shared/MapRendererContext.tsx +++ b/apps/web/src/arcade-games/know-your-world/features/shared/MapRendererContext.tsx @@ -103,9 +103,7 @@ export function MapRendererProvider({ children, value }: MapRendererProviderProp ] ) - return ( - {children} - ) + return {children} } // ============================================================================ diff --git a/apps/web/src/arcade-games/know-your-world/features/shared/types.ts b/apps/web/src/arcade-games/know-your-world/features/shared/types.ts index 3f97a2cf..1621c617 100644 --- a/apps/web/src/arcade-games/know-your-world/features/shared/types.ts +++ b/apps/web/src/arcade-games/know-your-world/features/shared/types.ts @@ -177,14 +177,7 @@ export interface FlashProgress { /** * Hot/cold feedback type for visual indicators */ -export type HotColdFeedbackType = - | 'freezing' - | 'cold' - | 'cool' - | 'warm' - | 'hot' - | 'burning' - | null +export type HotColdFeedbackType = 'freezing' | 'cold' | 'cool' | 'warm' | 'hot' | 'burning' | null /** * Heat-based styling for borders and glows diff --git a/apps/web/src/arcade-games/know-your-world/features/shared/viewportUtils.ts b/apps/web/src/arcade-games/know-your-world/features/shared/viewportUtils.ts index 46539692..e95473d0 100644 --- a/apps/web/src/arcade-games/know-your-world/features/shared/viewportUtils.ts +++ b/apps/web/src/arcade-games/know-your-world/features/shared/viewportUtils.ts @@ -96,13 +96,7 @@ export function screenToSVG( svgRect: DOMRect, viewBox: ViewBoxComponents ): SVGPosition { - const viewport = getRenderedViewport( - svgRect, - viewBox.x, - viewBox.y, - viewBox.width, - viewBox.height - ) + const viewport = getRenderedViewport(svgRect, viewBox.x, viewBox.y, viewBox.width, viewBox.height) // Calculate offset from container origin to SVG rendered content const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX @@ -130,13 +124,7 @@ export function svgToScreen( svgRect: DOMRect, viewBox: ViewBoxComponents ): CursorPosition { - const viewport = getRenderedViewport( - svgRect, - viewBox.x, - viewBox.y, - viewBox.width, - viewBox.height - ) + const viewport = getRenderedViewport(svgRect, viewBox.x, viewBox.y, viewBox.width, viewBox.height) // Calculate offset from container origin to SVG rendered content const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX