raw · 5632 bytes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 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(); }); } |