diff options
| author | godosa <godosa@godosa.eu> | 2026-10-07 00:14:38 +0200 |
|---|---|---|
| committer | godosa <godosa@godosa.eu> | 2026-10-07 00:14:38 +0200 |
| commit | 3443c1c65e9f1753e1e656b35d08416c1fa298f2 (patch) | |
| tree | 4e43236f460145a4d75d1b4616dcb7aa6ef08f51 /viewer/js/nav.js | |
| download | worldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.tar.gz worldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.zip | |
worldmap-viewer: initial public history
Diffstat (limited to 'viewer/js/nav.js')
| -rw-r--r-- | viewer/js/nav.js | 126 |
1 files changed, 126 insertions, 0 deletions
diff --git a/viewer/js/nav.js b/viewer/js/nav.js new file mode 100644 index 0000000..1e15adc --- /dev/null +++ b/viewer/js/nav.js @@ -0,0 +1,126 @@ +// nav.js — zoom, home, fullscreen, link and export buttons and keyboard navigation +import { PLANET } from './planet.js'; +import { $, toast } from './ui.js'; + +export const KEYS = { ArrowLeft: ['pan', -0.2, 0], ArrowRight: ['pan', 0.2, 0], ArrowUp: ['pan', 0, -0.2], + ArrowDown: ['pan', 0, 0.2], '+': ['zoom', 0.6], '=': ['zoom', 0.6], '-': ['zoom', 1 / 0.6], Home: ['home'], + f: ['fullscreen'], '/': ['search'], n: ['north'], g: ['ground'], '?': ['help'] }; +export const TURN_KEYS = { ArrowLeft: ['turn', -15, 0], ArrowRight: ['turn', 15, 0], ArrowUp: ['turn', 0, 10], ArrowDown: ['turn', 0, -10] }; +const MARKER_COLOR = { pin: '#e0b44c', measure: '#7fd4ff', search: '#ff6a4d', draft: '#ffffff', sun: '#ffd166', probe: '#ffffff' }; + +export const GROUND_RANGE = 3.5 / PLANET.radius_km; // ground view: 3.5 km from the ground point (planet radii) … +export const GROUND_PITCH = 78; // … looking out toward the horizon + +export function groundPose(p) { // the eye comes down over the spot looked at; the target lies ahead along the heading + const d = GROUND_RANGE * Math.sin(GROUND_PITCH * Math.PI / 180), r = Math.PI / 180; // arc from eye to target + const la = p.lat * r, h = p.heading * r; + const lat = Math.asin(Math.sin(la) * Math.cos(d) + Math.cos(la) * Math.sin(d) * Math.cos(h)); + const lon = p.lon * r + Math.atan2(Math.sin(h) * Math.sin(d) * Math.cos(la), Math.cos(d) - Math.sin(la) * Math.sin(lat)); + return { lat: lat / r, lon: lon / r, range: GROUND_RANGE, heading: p.heading, pitch: GROUND_PITCH }; +} + +export function keyAction(e) { + const tag = e.target?.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || e.ctrlKey || e.metaKey || e.altKey) return null; + return (e.shiftKey && TURN_KEYS[e.key]) || KEYS[e.key] || null; +} + +export const exportNotes = app => (app.view?.tiles?.active ? ['procedural detail — not data'] : []); + +const slug = n => n.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'world'; +export const exportName = s => `${slug(PLANET.name)}-${s.layer}${s.over ? `+${s.over}` : ''}-${s.view}.png`; + +function toggleHelp(show) { + const h = $('#help'); + h.hidden = !(show ?? h.hidden); +} + +function toggleFullscreen() { + if (document.fullscreenElement) document.exitFullscreen(); + else document.documentElement.requestFullscreen().catch(e => toast(`Fullscreen: ${e.message}`)); +} + +async function copyLink(app) { + app.saveUrl(); + try { + await navigator.clipboard.writeText(location.href); + toast('Link copied', 'info'); + } catch (e) { + toast(`Copy failed: ${e.message}`); + } +} + +export function exportPng(app) { + app.view.render(); // same task as drawImage: the WebGL buffer is still valid + const src = app.renderer.domElement, c = document.createElement('canvas'); + c.width = src.width; + c.height = src.height; + const g = c.getContext('2d'), k = src.width / src.clientWidth; + g.drawImage(src, 0, 0); + g.font = `${12 * k}px system-ui, sans-serif`; + g.textBaseline = 'middle'; + for (const m of app.overlays.markers) { + if (m.el?.style.display === 'none') continue; + const p = app.view.toScreen(m.lat, m.lon); + if (!p) continue; + const x = p.x * k, y = p.y * k; + g.fillStyle = MARKER_COLOR[m.kind] ?? '#e0b44c'; + g.strokeStyle = '#000'; + g.lineWidth = k; + g.beginPath(); + g.arc(x, y, 4 * k, 0, 2 * Math.PI); + g.fill(); + g.stroke(); + if (m.label) { + g.lineWidth = 3 * k; + g.strokeText(m.label, x + 9 * k, y); + g.fillStyle = '#e6e8ee'; + g.fillText(m.label, x + 9 * k, y); + } + } + let y = c.height - 10 * k; + for (const note of exportNotes(app)) { // the same caveat the status bar shows + g.font = `${13 * k}px system-ui, sans-serif`; + g.lineWidth = 3 * k; + g.strokeStyle = '#000'; + g.strokeText(note, 10 * k, y); + g.fillStyle = '#f0a050'; + g.fillText(note, 10 * k, y); + y -= 18 * k; + } + const a = document.createElement('a'); + a.download = exportName(app.state); + a.href = c.toDataURL('image/png'); + a.click(); +} + +export function installNav(app) { + $('#nav-in').addEventListener('click', () => app.zoomBy(0.6)); + $('#nav-out').addEventListener('click', () => app.zoomBy(1 / 0.6)); + $('#nav-home').addEventListener('click', () => app.home()); + $('#nav-north').addEventListener('click', () => app.resetNorth()); + $('#nav-ground').addEventListener('click', () => app.groundView()); + $('#nav-help').addEventListener('click', () => toggleHelp()); + $('#help').addEventListener('click', () => toggleHelp(false)); + $('#nav-3d').addEventListener('click', () => app.setTerrain(app.state.terrain > 0 ? 0 : app.lastExag ?? 1)); + $('#nav-water').addEventListener('click', () => app.setWater(!app.state.water)); + $('#exag').addEventListener('input', e => app.setTerrain(Number(e.target.value))); + $('#nav-full').addEventListener('click', toggleFullscreen); + $('#nav-link').addEventListener('click', () => copyLink(app)); + $('#nav-export').addEventListener('click', () => exportPng(app)); + addEventListener('keydown', e => { + if (e.key === 'Escape' && !$('#help').hidden) return toggleHelp(false); + const a = keyAction(e); + if (!a) return; + e.preventDefault(); + if (a[0] === 'pan') app.panBy(a[1], a[2]); + else if (a[0] === 'zoom') app.zoomBy(a[1]); + else if (a[0] === 'home') app.home(); + else if (a[0] === 'fullscreen') toggleFullscreen(); + else if (a[0] === 'turn') app.turnBy(a[1], a[2]); + else if (a[0] === 'north') app.resetNorth(); + else if (a[0] === 'ground') app.groundView(); + else if (a[0] === 'help') toggleHelp(); + else document.getElementById('search')?.focus(); + }); +} |
