From 4e3a3b4393924167e41cc536fad547694d5e04b9 Mon Sep 17 00:00:00 2001 From: Jaume Garriga Maestre Date: Wed, 6 May 2026 13:03:34 +0200 Subject: [PATCH] feat: add zoom controls to map levels 5 and 6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add +/−/⊠ zoom buttons overlay on the SVG map container - Implement viewBox-based zoom (1×–8×) centered on map midpoint - Add drag-to-pan when zoomed in (pointer events, ignores comarca clicks) - Add pinch-to-zoom gesture support for touch devices - Zoom resets to full view at the start of each new game --- index.html | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index 310bd42..25d7ca4 100644 --- a/index.html +++ b/index.html @@ -166,8 +166,17 @@ .map-q-box{background:#fff;border-radius:15px;padding:14px 18px; box-shadow:var(--shadow);margin-bottom:14px;text-align:center;} #map-svg-container{width:100%;background:#D8EEF8;border-radius:16px; - overflow:hidden;box-shadow:var(--shadow);} + overflow:hidden;box-shadow:var(--shadow);position:relative;} #map-svg{width:100%;height:auto;display:block;cursor:pointer;} + /* Zoom controls overlay */ + #map-zoom-controls{position:absolute;bottom:10px;right:10px;display:flex; + flex-direction:column;gap:4px;z-index:10;} + .map-zoom-btn{width:36px;height:36px;border-radius:50%;border:none; + background:rgba(255,255,255,.88);box-shadow:0 2px 6px rgba(0,0,0,.22); + font-size:1.3rem;line-height:1;cursor:pointer;display:flex; + align-items:center;justify-content:center;transition:background .15s,transform .1s;} + .map-zoom-btn:hover{background:#fff;transform:scale(1.12);} + .map-zoom-btn:active{transform:scale(.95);} .comarca-bg{fill:#C5DEB0;stroke:#a8c890;stroke-width:.8;transition:fill .2s;} .comarca-mountain{stroke:#fff;stroke-width:1.4;cursor:pointer; transition:fill .25s,filter .2s;filter:drop-shadow(0 1px 2px rgba(0,0,0,.18));} @@ -518,6 +527,11 @@
+
+ + + +
🟫 Comarques de muntanya  ·  🟩 Resta de Catalunya @@ -877,6 +891,9 @@ let sessionStart = null; let currentLevel=1, currentQ=0, score=0, questions=[]; let fcFlipped=false, fcIndex=0; let mapMode='name', mapTarget=null, mapAnswered=false, mapBuilt=false, mapBlind=false; +// Zoom state: viewBox is 820×600 at scale 1. Pan centre starts at (410, 300). +const MAP_W=820, MAP_H=600; +let mapZoom=1, mapPanX=MAP_W/2, mapPanY=MAP_H/2; let l3Round=0, l3Sel=null, l3BTotal=0, l3BDone=0; const L3B=5; @@ -1513,6 +1530,51 @@ function buildMap(){ addTextSVG(svg,36,310,'ARAGÓ',7,'#999','middle','rotate(-90,36,310)'); addTextSVG(svg,790,430,'Mar Mediterrani',7,'#6BAFD6','middle','rotate(90,790,430)'); addTextSVG(svg,320,575,'COMUNITAT VALENCIANA',7,'#999','middle'); + // Drag-to-pan (only when zoomed in) + let _drag=null; + svg.addEventListener('pointerdown',e=>{ + if(mapZoom<=1)return; + // Ignore clicks on comarca paths (handled separately) + if(e.target.classList.contains('comarca-mountain'))return; + _drag={x:e.clientX,y:e.clientY,px:mapPanX,py:mapPanY}; + svg.style.cursor='grabbing'; + e.preventDefault(); + },{passive:false}); + svg.addEventListener('pointermove',e=>{ + if(!_drag)return; + // Convert screen delta to SVG coordinate delta + const rect=svg.getBoundingClientRect(); + const scaleX=MAP_W/(mapZoom*rect.width); + const scaleY=MAP_H/(mapZoom*rect.height); + mapPanX=_drag.px-(e.clientX-_drag.x)*scaleX; + mapPanY=_drag.py-(e.clientY-_drag.y)*scaleY; + _applyMapViewBox(); + }); + const _endDrag=()=>{_drag=null;svg.style.cursor='pointer';}; + svg.addEventListener('pointerup',_endDrag); + svg.addEventListener('pointerleave',_endDrag); + // Pinch-to-zoom (touch) + let _pinchDist=0; + svg.addEventListener('touchstart',e=>{ + if(e.touches.length===2){ + const dx=e.touches[0].clientX-e.touches[1].clientX; + const dy=e.touches[0].clientY-e.touches[1].clientY; + _pinchDist=Math.hypot(dx,dy); + } + },{passive:true}); + svg.addEventListener('touchmove',e=>{ + if(e.touches.length===2){ + const dx=e.touches[0].clientX-e.touches[1].clientX; + const dy=e.touches[0].clientY-e.touches[1].clientY; + const d=Math.hypot(dx,dy); + if(_pinchDist>0){ + mapZoom=Math.max(1,Math.min(8,mapZoom*(d/_pinchDist))); + _applyMapViewBox(); + } + _pinchDist=d; + e.preventDefault(); + } + },{passive:false}); } function addLabel(svg,x,y,text,small){ const t=document.createElementNS('http://www.w3.org/2000/svg','text'); @@ -1529,6 +1591,7 @@ function resetMapPaths(){ } function initMap(){ currentQ=0;score=0;mapAnswered=false; + mapZoomReset(); // always start at full-view // Both levels show mode tabs — level 6 just removes labels from the map const tabs=document.querySelector('.map-mode-tabs'); tabs.style.display='flex'; @@ -1538,6 +1601,28 @@ function initMap(){ nextMapQ(); } function setMapMode(mode){mapMode=mode;document.getElementById('tab-name').classList.toggle('active',mode==='name');document.getElementById('tab-cap').classList.toggle('active',mode==='capital');initMap();} + +/* ── Zoom helpers ───────────────────────────────────────────────── */ +function _applyMapViewBox(){ + const svg=document.getElementById('map-svg'); + const w=MAP_W/mapZoom, h=MAP_H/mapZoom; + const x=Math.max(0,Math.min(MAP_W-w, mapPanX-w/2)); + const y=Math.max(0,Math.min(MAP_H-h, mapPanY-h/2)); + svg.setAttribute('viewBox',`${x} ${y} ${w} ${h}`); +} +function mapZoomIn(){ + mapZoom=Math.min(mapZoom*1.5, 8); // max 8× zoom + _applyMapViewBox(); +} +function mapZoomOut(){ + mapZoom=Math.max(mapZoom/1.5, 1); // min 1× (full view) + _applyMapViewBox(); +} +function mapZoomReset(){ + mapZoom=1; mapPanX=MAP_W/2; mapPanY=MAP_H/2; + const svg=document.getElementById('map-svg'); + if(svg) svg.setAttribute('viewBox',`0 0 ${MAP_W} ${MAP_H}`); +} function nextMapQ(){ if(currentQ>=questions.length){showResult(5);return;} mapAnswered=false;mapTarget=questions[currentQ];