From 8530f008d6b364f0d6642c2befe934debbf60aad Mon Sep 17 00:00:00 2001 From: HikeMap User Date: Sat, 10 Jan 2026 20:39:43 -0600 Subject: [PATCH] Fix fog of war to work correctly with 3D pitch/rotation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Restore 3D building effect (pitch: 45, maxPitch: 60) - Add drawProjectedCircle() helper that samples 48 points and projects each to screen coords - Replace ctx.arc() circles with projected polygons for both homebase and player explore radius - Fog circles now correctly render as ellipses when map is tilted 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- index.html | 70 +++++++++++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/index.html b/index.html index 1af575e..ea90997 100644 --- a/index.html +++ b/index.html @@ -4480,8 +4480,8 @@ zoom: 13, maxZoom: 22, bearing: 0, - pitch: 0, // Disable pitch to keep fog of war circles correct - maxPitch: 0, // Prevent user from tilting map (fog doesn't work with perspective) + pitch: 45, // 3D building perspective + maxPitch: 60, doubleClickZoom: false }); @@ -5125,35 +5125,42 @@ fogCtx.fillStyle = 'rgba(0, 0, 0, 0.6)'; fogCtx.fillRect(0, 0, width, height); + // Helper function to draw a projected circle (handles rotation AND pitch) + function drawProjectedCircle(ctx, centerLngLat, radiusMeters, numPoints = 48) { + const points = []; + for (let i = 0; i < numPoints; i++) { + const angle = (i / numPoints) * 360; + const edgeLngLat = destinationPoint(centerLngLat, radiusMeters, angle); + const edgePoint = map.project(edgeLngLat); + points.push(edgePoint); + } + + // Draw the projected shape + ctx.beginPath(); + points.forEach((pt, i) => { + if (i === 0) ctx.moveTo(pt.x, pt.y); + else ctx.lineTo(pt.x, pt.y); + }); + ctx.closePath(); + } + // Homebase reveal (only if homebase exists) if (playerStats && playerStats.homeBaseLat != null && playerStats.homeBaseLng != null) { - // Calculate reveal circle center using container coordinates (viewport-relative) - // MapLibre uses [lng, lat] order const homeLngLat = [playerStats.homeBaseLng, playerStats.homeBaseLat]; - const centerPoint = map.project(homeLngLat); - - // Calculate radius in pixels using map projection (with multiplier) - // Use actual distance between projected points to handle rotation correctly const effectiveHomebaseRadius = playerRevealRadius * homebaseRadiusMultiplier; - const edgeLngLat = destinationPoint(homeLngLat, effectiveHomebaseRadius, 90); - const edgePoint = map.project(edgeLngLat); - const radiusPixels = Math.hypot(edgePoint.x - centerPoint.x, edgePoint.y - centerPoint.y); // Cut out revealed area using composite operation fogCtx.save(); fogCtx.globalCompositeOperation = 'destination-out'; - // Create gradient for soft edge - const gradient = fogCtx.createRadialGradient( - centerPoint.x, centerPoint.y, radiusPixels * 0.85, - centerPoint.x, centerPoint.y, radiusPixels - ); - gradient.addColorStop(0, 'rgba(0, 0, 0, 1)'); - gradient.addColorStop(1, 'rgba(0, 0, 0, 0)'); + // Draw outer edge with fade (slightly larger, semi-transparent) + fogCtx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + drawProjectedCircle(fogCtx, homeLngLat, effectiveHomebaseRadius * 1.05); + fogCtx.fill(); - fogCtx.beginPath(); - fogCtx.arc(centerPoint.x, centerPoint.y, radiusPixels, 0, Math.PI * 2); - fogCtx.fillStyle = gradient; + // Draw main revealed area (solid cutout) + fogCtx.fillStyle = 'rgba(0, 0, 0, 1)'; + drawProjectedCircle(fogCtx, homeLngLat, effectiveHomebaseRadius * 0.95); fogCtx.fill(); fogCtx.restore(); @@ -5175,26 +5182,19 @@ } if (isOutsideHomebase) { - const playerPoint = map.project(playerLngLat); const effectiveExploreRadius = playerExploreRadius * exploreRadiusMultiplier; - const playerEdge = destinationPoint(playerLngLat, effectiveExploreRadius, 90); - const playerEdgePoint = map.project(playerEdge); - // Use actual distance to handle rotation correctly - const playerRadiusPixels = Math.hypot(playerEdgePoint.x - playerPoint.x, playerEdgePoint.y - playerPoint.y); fogCtx.save(); fogCtx.globalCompositeOperation = 'destination-out'; - const playerGradient = fogCtx.createRadialGradient( - playerPoint.x, playerPoint.y, playerRadiusPixels * 0.85, - playerPoint.x, playerPoint.y, playerRadiusPixels - ); - playerGradient.addColorStop(0, 'rgba(0, 0, 0, 1)'); - playerGradient.addColorStop(1, 'rgba(0, 0, 0, 0)'); + // Draw outer edge with fade (slightly larger, semi-transparent) + fogCtx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + drawProjectedCircle(fogCtx, playerLngLat, effectiveExploreRadius * 1.1); + fogCtx.fill(); - fogCtx.beginPath(); - fogCtx.arc(playerPoint.x, playerPoint.y, playerRadiusPixels, 0, Math.PI * 2); - fogCtx.fillStyle = playerGradient; + // Draw main revealed area (solid cutout) + fogCtx.fillStyle = 'rgba(0, 0, 0, 1)'; + drawProjectedCircle(fogCtx, playerLngLat, effectiveExploreRadius * 0.85); fogCtx.fill(); fogCtx.restore();