feat: add zoom controls to map levels 5 and 6
- 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
This commit is contained in:
87
index.html
87
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 @@
|
||||
</div>
|
||||
<div id="map-svg-container">
|
||||
<svg id="map-svg" viewBox="0 0 820 600" xmlns="http://www.w3.org/2000/svg"></svg>
|
||||
<div id="map-zoom-controls">
|
||||
<button class="map-zoom-btn" onclick="mapZoomIn()" title="Apropar">+</button>
|
||||
<button class="map-zoom-btn" onclick="mapZoomOut()" title="Allunyar">−</button>
|
||||
<button class="map-zoom-btn" onclick="mapZoomReset()" title="Vista completa" style="font-size:.9rem;">⊠</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:12px;justify-content:center;font-size:.8rem;color:#888;">
|
||||
<span>🟫 Comarques de muntanya · 🟩 Resta de Catalunya</span>
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user