import maplibregl from 'maplibre-gl'; import 'maplibre-gl/dist/maplibre-gl.css'; import { FogCanvas, metersToPixels } from './fog.js'; import { MapSync } from './sync.js'; const BOTTOM_STYLE = 'https://tiles.openfreemap.org/styles/bright'; const TOP_STYLE = 'https://tiles.openfreemap.org/styles/liberty'; let mode = 'explore'; // 'explore' | 'navigate' let brushRadius = 8000; // ── Maps ── const mapBottom = new maplibregl.Map({ container: 'map-bottom', style: BOTTOM_STYLE, center: [0, 30], zoom: 3, attributionControl: false, }); const mapTop = new maplibregl.Map({ container: 'map-top', style: TOP_STYLE, center: [0, 30], zoom: 3, attributionControl: false, interactive: false, // will be toggled }); // Make top map transparent background and mute layers mapTop.on('load', () => { const layers = mapTop.getStyle().layers; for (const layer of layers) { const id = layer.id.toLowerCase(); const src = (layer['source-layer'] || '').toLowerCase(); const isWater = id.includes('ocean') || id.includes('sea') || id.includes('water') || src.includes('water') || src.includes('ocean'); const isAdmin = id.includes('admin') || id.includes('boundar') || src.includes('admin') || src.includes('boundar'); const isPlace = id.includes('place') || id.includes('label') || id.includes('capital') || id.includes('city') || id.includes('country') || id.includes('continent') || src.includes('place'); if (!isWater && !isAdmin && !isPlace) { mapTop.setLayoutProperty(layer.id, 'visibility', 'none'); } else if (isPlace && layer.type === 'symbol') { // Only keep country labels and major/capital cities, hide small places const isCountryOrCapital = id.includes('country') || id.includes('capital') || id.includes('continent') || id.includes('state'); if (!isCountryOrCapital) { // Filter to only show cities with high rank (major cities) try { mapTop.setFilter(layer.id, ['<=', ['get', 'rank'], 3]); } catch {} } try { mapTop.setPaintProperty(layer.id, 'text-color', 'rgba(200,215,255,0.45)'); } catch {} try { mapTop.setPaintProperty(layer.id, 'text-halo-color', 'rgba(6,10,24,0.7)'); } catch {} } else if (isAdmin && layer.type === 'line') { try { mapTop.setPaintProperty(layer.id, 'line-color', 'rgba(200,215,255,0.25)'); } catch {} try { mapTop.setPaintProperty(layer.id, 'line-opacity', 0.4); } catch {} } else if (isWater) { // Mute water to a subtle blue-grey if (layer.type === 'fill') { try { mapTop.setPaintProperty(layer.id, 'fill-color', 'rgba(140,160,185,0.3)'); } catch {} } } } }); // Sync maps const sync = new MapSync(mapTop, mapBottom); mapTop.on('load', () => { mapBottom.on('load', () => sync.enable()); }); // ── Fog ── const fogCanvas = new FogCanvas(document.getElementById('fog'), mapTop); // Re-render fog on map move mapTop.on('move', () => fogCanvas.markDirty()); mapBottom.on('load', () => { // initial render once both ready fogCanvas.render(); }); // Resize const ro = new ResizeObserver(() => fogCanvas.resize()); ro.observe(document.getElementById('container')); // ── Brush cursor ── const cursor = document.getElementById('brush-cursor'); function updateCursorSize(e) { const r = metersToPixels(mapTop, [e.lngLat.lng, e.lngLat.lat], brushRadius); const d = r * 2; cursor.style.width = d + 'px'; cursor.style.height = d + 'px'; cursor.style.left = e.point.x + 'px'; cursor.style.top = e.point.y + 'px'; } // ── Interaction ── let dragging = false; let lastPt = null; function setMode(m) { mode = m; document.getElementById('btn-explore').classList.toggle('active', m === 'explore'); document.getElementById('btn-navigate').classList.toggle('active', m === 'navigate'); if (m === 'explore') { mapTop.dragPan.disable(); mapTop.scrollZoom.enable(); mapTop.doubleClickZoom.enable(); document.getElementById('map-top').style.cursor = 'none'; cursor.style.display = 'block'; } else { mapTop.dragPan.enable(); mapTop.scrollZoom.enable(); mapTop.doubleClickZoom.enable(); document.getElementById('map-top').style.cursor = ''; cursor.style.display = 'none'; } } mapTop.on('load', () => setMode('explore')); mapTop.on('mousedown', (e) => { if (mode !== 'explore') return; dragging = true; lastPt = e.point; addPoint(e); }); mapTop.on('mousemove', (e) => { if (mode === 'explore') updateCursorSize(e); if (!dragging || mode !== 'explore') return; // Interpolate between last and current to avoid gaps const dx = e.point.x - lastPt.x; const dy = e.point.y - lastPt.y; const dist = Math.sqrt(dx * dx + dy * dy); const step = 10; if (dist > step) { const steps = Math.ceil(dist / step); for (let i = 1; i <= steps; i++) { const t = i / steps; const px = lastPt.x + dx * t; const py = lastPt.y + dy * t; const ll = mapTop.unproject([px, py]); fogCanvas.addErasePoint([ll.lng, ll.lat], brushRadius); } } else { addPoint(e); } lastPt = e.point; }); mapTop.on('mouseup', () => { dragging = false; lastPt = null; }); mapTop.on('mouseleave', () => { dragging = false; lastPt = null; cursor.style.display = 'none'; }); mapTop.on('mouseenter', () => { if (mode === 'explore') cursor.style.display = 'block'; }); function addPoint(e) { fogCanvas.addErasePoint([e.lngLat.lng, e.lngLat.lat], brushRadius); } // ── Toolbar ── document.getElementById('btn-explore').addEventListener('click', () => setMode('explore')); document.getElementById('btn-navigate').addEventListener('click', () => setMode('navigate')); document.getElementById('btn-reset').addEventListener('click', () => fogCanvas.reset()); document.getElementById('btn-zoom-in').addEventListener('click', () => mapTop.zoomIn()); document.getElementById('btn-zoom-out').addEventListener('click', () => mapTop.zoomOut()); document.getElementById('brush-slider').addEventListener('input', (e) => { brushRadius = Number(e.target.value); }); // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.key === 'e' || e.key === 'E') setMode('explore'); if (e.key === 'n' || e.key === 'N') setMode('navigate'); });