// touch.mjs — two-finger gestures on a phone: the map follows the fingers (twist turns it with them, a spread zooms // in about them, both fingers up tilt the globe); flat maps pinch about the fingers. export const viewport = { width: 390, height: 844 }; const helpers = `const a = window.mapViewer, c = document.querySelector('#map canvas'), r = c.getBoundingClientRect(); const ev = (t, id, x, y) => c.dispatchEvent(new PointerEvent(t, { clientX: r.left + x, clientY: r.top + y, bubbles: true, pointerId: id, pointerType: 'touch', isPrimary: id === 11, button: 0, buttons: t === 'pointerup' ? 0 : 1 })); const gesture = (from, to, steps = 10) => { // from/to: [[x, y], [x, y]] for fingers 11 and 12 ev('pointerdown', 11, ...from[0]); ev('pointerdown', 12, ...from[1]); for (let s = 1; s <= steps; s++) for (const [k, id] of [[0, 11], [1, 12]]) ev('pointermove', id, from[k][0] + (to[k][0] - from[k][0]) * s / steps, from[k][1] + (to[k][1] - from[k][1]) * s / steps); ev('pointerup', 11, ...to[0]); ev('pointerup', 12, ...to[1]); }; const W = r.width, H = r.height, cx = W / 2, cy = H / 2;`; export const steps = [ { name: 'boots', wait: 5000, expect: v => v, js: `window.mapViewer.setView('globe'); return !!window.mapViewer.view.pose;` }, { name: 'twist clockwise 30° (about the view centre): a ground point turns ~30° clockwise with the fingers', expect: v => Math.abs(v - 30) < 6, js: `${helpers} a.view.setPose({ ...a.view.pose, alt: undefined, pitch: 0, range: a.view.pose.range }); const p = a.view.pick(cx + 60, cy, false); const s0 = a.view.toScreen(p.lat, p.lon); const R = 80, rad = d => d * Math.PI / 180, at = d => [cx + R * Math.cos(rad(d)), cy + R * Math.sin(rad(d))]; gesture([at(180), at(0)], [at(210), at(30)]); const s1 = a.view.toScreen(p.lat, p.lon); return (Math.atan2(s1.y - cy, s1.x - cx) - Math.atan2(s0.y - cy, s0.x - cx)) * 180 / Math.PI;` }, { name: 'spread to twice the distance: zoomed in ~2× (range halves)', expect: v => Math.abs(v - 0.5) < 0.08, js: `${helpers} const r0 = a.view.pose.range; gesture([[cx - 40, cy], [cx + 40, cy]], [[cx - 80, cy], [cx + 80, cy]]); return a.view.pose.range / r0;` }, { name: 'both fingers up: the globe tilts toward the horizon', expect: v => v > 5, js: `${helpers} a.view.setPose({ ...a.view.pose, range: a.view.pose.range * 0.02, pitch: 0 }); const p0 = a.view.pose.pitch; gesture([[cx - 60, cy + 100], [cx + 60, cy + 100]], [[cx - 60, cy], [cx + 60, cy]]); return a.view.pose.pitch - p0;` }, { name: 'one finger after a pinch keeps panning (no jump, no stuck drag)', expect: v => v.moved && !v.jump, js: `${helpers} a.view.setPose({ ...a.view.pose, pitch: 0 }); const before = { ...a.view.pose }; ev('pointerdown', 11, cx - 40, cy); ev('pointerdown', 12, cx + 40, cy); ev('pointermove', 12, cx + 45, cy); ev('pointerup', 12, cx + 45, cy); const mid = { ...a.view.pose }; ev('pointermove', 11, cx - 40, cy + 30); ev('pointerup', 11, cx - 40, cy + 30); const after = a.view.pose; return { moved: Math.abs(after.lat - mid.lat) > 1e-4, jump: Math.abs(after.lat - mid.lat) > 20 };` }, { name: 'flat map: spread zooms 2× about the fingers (the point between them stays put)', expect: v => Math.abs(v.z - 2) < 0.1 && v.drift < 3, js: `${helpers} a.setView('equal_earth'); a.view.setState({ lat: 0, lon: 0, zoom: 2 }); const z0 = a.view.getState().zoom, mx = cx + 30, my = cy + 20, p = a.view.pick(mx, my, false); gesture([[mx - 40, my], [mx + 40, my]], [[mx - 80, my], [mx + 80, my]]); const s = a.view.toScreen(p.lat, p.lon), z = a.view.getState().zoom / z0; a.setView('globe'); return { z, drift: Math.hypot(s.x - mx, s.y - my) };` }, ];