veilmap/main.js
2026-04-20 05:56:05 -04:00

183 lines
6.2 KiB
JavaScript

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');
});