Merge pull request #1 from Influence-Tools/board-zoom-pan
Board: zoomable + pannable county map
This commit is contained in:
+42
-6
@@ -28,10 +28,13 @@
|
||||
.lens:hover{color:var(--ink); border-color:var(--statel)}
|
||||
.lens[aria-pressed="true"]{color:var(--ground); background:var(--teal); border-color:var(--teal); font-weight:600}
|
||||
.lens:focus-visible{outline:2px solid var(--teal); outline-offset:2px}
|
||||
.mapwrap{position:absolute; inset:132px 8px 34px}
|
||||
.mapwrap{position:absolute; inset:132px 8px 34px; cursor:grab; touch-action:none}
|
||||
.mapwrap.grab{cursor:grabbing}
|
||||
.maphint{position:absolute; left:10px; bottom:8px; font-size:10px; letter-spacing:.04em;
|
||||
color:#5c6b7a; opacity:.6; pointer-events:none; user-select:none}
|
||||
svg{width:100%; height:100%; display:block}
|
||||
.county{stroke:#0a0f16; stroke-width:.15; transition:opacity .5s}
|
||||
.stateline{fill:none; stroke:var(--statel); stroke-width:.5; opacity:.55; pointer-events:none}
|
||||
.county{stroke:#0a0f16; stroke-width:.15; vector-effect:non-scaling-stroke; transition:opacity .5s}
|
||||
.stateline{fill:none; stroke:var(--statel); stroke-width:.5; vector-effect:non-scaling-stroke; opacity:.55; pointer-events:none}
|
||||
.county.hot{stroke:var(--ink); stroke-width:.7}
|
||||
.booting .county{opacity:0}
|
||||
@media(prefers-reduced-motion:reduce){ .county{transition:none} .booting .county{opacity:1} }
|
||||
@@ -109,7 +112,7 @@
|
||||
<div class="eyebrow">Republic OS · the board</div>
|
||||
<h1 id="title">Every county in America, and what it's made of.</h1>
|
||||
<div class="lenses" id="lenses"></div>
|
||||
<div class="mapwrap"><svg id="map" preserveAspectRatio="xMidYMid meet"></svg></div>
|
||||
<div class="mapwrap"><svg id="map" preserveAspectRatio="xMidYMid meet"></svg><div class="maphint">scroll to zoom · drag to pan · double-click to reset</div></div>
|
||||
<div class="stagefoot" id="stagefoot"></div>
|
||||
</section>
|
||||
|
||||
@@ -198,10 +201,43 @@ function render(){
|
||||
html+=`<path class="county" data-f="${f.id}" fill="${colorFor(f.id)}" d="${pathFor(f.geometry)}"></path>`;
|
||||
});
|
||||
STATES.features.forEach(f=>{ html+=`<path class="stateline" d="${pathFor(f.geometry)}"></path>`; });
|
||||
map.innerHTML=html;
|
||||
map.innerHTML='<g id="mapg">'+html+'</g>'; mapg=document.getElementById("mapg"); applyTransform();
|
||||
}
|
||||
function recolor(){ map.querySelectorAll(".county").forEach(p=>p.setAttribute("fill",colorFor(p.dataset.f))); }
|
||||
|
||||
// ---- zoom / pan (transform on <g id="mapg">; strokes are non-scaling) ----
|
||||
let mapg=null, zk=1, ztx=0, zty=0;
|
||||
const ZMIN=1, ZMAX=16;
|
||||
function clampPan(){ ztx=Math.min(0,Math.max((1-zk)*W, ztx)); zty=Math.min(0,Math.max((1-zk)*H, zty)); }
|
||||
function applyTransform(){ if(!mapg)return; clampPan();
|
||||
mapg.setAttribute("transform",`translate(${ztx.toFixed(2)} ${zty.toFixed(2)}) scale(${zk.toFixed(4)})`); }
|
||||
function vbPoint(e){ const pt=map.createSVGPoint(); pt.x=e.clientX; pt.y=e.clientY;
|
||||
const m=map.getScreenCTM(); return m?pt.matrixTransform(m.inverse()):{x:0,y:0}; }
|
||||
map.addEventListener("wheel",e=>{
|
||||
e.preventDefault();
|
||||
const v=vbPoint(e), old=zk;
|
||||
zk=Math.min(ZMAX,Math.max(ZMIN, zk*(e.deltaY<0?1.18:1/1.18)));
|
||||
if(zk===old)return;
|
||||
ztx=v.x-(v.x-ztx)*(zk/old); zty=v.y-(v.y-zty)*(zk/old);
|
||||
applyTransform();
|
||||
},{passive:false});
|
||||
const wrap=document.querySelector(".mapwrap");
|
||||
let dragging=false, dragLast=null, dragStart=null, dragMoved=false;
|
||||
map.addEventListener("pointerdown",e=>{
|
||||
dragging=true; dragMoved=false; dragStart=dragLast=vbPoint(e);
|
||||
wrap.classList.add("grab"); try{map.setPointerCapture(e.pointerId);}catch(_){}
|
||||
});
|
||||
map.addEventListener("pointermove",e=>{
|
||||
if(!dragging)return;
|
||||
const v=vbPoint(e); ztx+=v.x-dragLast.x; zty+=v.y-dragLast.y; dragLast=v;
|
||||
if(Math.hypot(v.x-dragStart.x, v.y-dragStart.y)>3) dragMoved=true;
|
||||
applyTransform();
|
||||
});
|
||||
function endDrag(e){ dragging=false; wrap.classList.remove("grab"); try{map.releasePointerCapture(e.pointerId);}catch(_){} }
|
||||
map.addEventListener("pointerup",endDrag);
|
||||
map.addEventListener("pointercancel",endDrag);
|
||||
map.addEventListener("dblclick",e=>{ e.preventDefault(); zk=1; ztx=0; zty=0; applyTransform(); });
|
||||
|
||||
// ---- readout ----
|
||||
const readout=document.getElementById("readout");
|
||||
function show(fips){
|
||||
@@ -259,7 +295,7 @@ function closeModal(){ modal.hidden=true; }
|
||||
document.getElementById("close").onclick=closeModal;
|
||||
document.getElementById("backdrop").onclick=closeModal;
|
||||
document.addEventListener("keydown",e=>{ if(e.key==="Escape"&&!modal.hidden)closeModal(); });
|
||||
map.addEventListener("click",e=>{ const t=e.target; if(t.dataset&&t.dataset.f)openDetail(t.dataset.f); });
|
||||
map.addEventListener("click",e=>{ const t=e.target; if(dragMoved||e.detail>1)return; if(t.dataset&&t.dataset.f)openDetail(t.dataset.f); });
|
||||
|
||||
// ---- legend ----
|
||||
function renderLegend(){
|
||||
|
||||
Reference in New Issue
Block a user