initial commit
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>ESCAPE FROM TARKOV COMPANION</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<!-- Global site styles -->
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<!-- Page-specific styles for Maps -->
|
||||
<link rel="stylesheet" href="maps.css" />
|
||||
|
||||
<!-- Preload default map for faster first paint -->
|
||||
<link rel="preload" as="image" href="/images/TARKOV.webp" fetchpriority="high" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<a href="index.html" class="logo">ESCAPE FROM TARKOV COMPANION</a>
|
||||
|
||||
<!-- HAMBURGER BUTTON -->
|
||||
<button class="nav-toggle" type="button" aria-controls="primary-nav" aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<span class="nav-toggle__bar" aria-hidden="true"></span>
|
||||
<span class="nav-toggle__bar" aria-hidden="true"></span>
|
||||
<span class="nav-toggle__bar" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
<nav class="main-nav" id="primary-nav" aria-label="Primary navigation">
|
||||
<ul>
|
||||
<li><a href="index.html">Home</a></li>
|
||||
<li><a href="goons.html">Goons</a></li>
|
||||
<li><a href="maps.html">Maps</a></li>
|
||||
<li><a href="quests.html">Quests</a></li>
|
||||
<li><a href="ammo.html">Ammo</a></li>
|
||||
<li><a href="traders.html">Traders</a></li>
|
||||
<li><a href="pricewatch.html">Price Watch</a></li>
|
||||
<li><a href="crafts.html">Craft Calculator</a></li>
|
||||
<li><a href="https://tarkovgunsmith.com/ballistics_simulator" target="_blank" rel="noopener">Ballistics Simulator</a></li>
|
||||
<li><a href="https://tarkov-market.com/" target="_blank" rel="noopener">Flea Market</a></li>
|
||||
<li><a href="https://escapefromtarkov.fandom.com/wiki/Escape_from_Tarkov_Wiki" target="_blank" rel="noopener">Wiki</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="content">
|
||||
<h1 class="hero">Maps</h1>
|
||||
<p class="page-title">Pinch or scroll to zoom • Drag to pan</p>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="trader-toolbar toolbar-flex map-toolbar" role="region" aria-label="Map tools">
|
||||
<label for="mapSelect" class="visually-hidden">Select map</label>
|
||||
<select id="mapSelect" class="input" aria-label="Select map">
|
||||
<option value="TARKOV" selected>Tarkov (Global)</option>
|
||||
<option value="CUSTOMS">Customs</option>
|
||||
<option value="FACTORY">Factory</option>
|
||||
<option value="GROUND_ZERO">Ground Zero</option>
|
||||
<option value="INTERCHANGE">Interchange</option>
|
||||
<option value="LABS">The Lab</option>
|
||||
<option value="LABYRINTH">Labyrinth</option>
|
||||
<option value="LIGHTHOUSE">Lighthouse</option>
|
||||
<option value="RESERVE">Reserve</option>
|
||||
<option value="SHORELINE">Shoreline</option>
|
||||
<option value="STREETS">Streets</option>
|
||||
<option value="WOODS">Woods</option>
|
||||
</select>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<!-- Zoom controls -->
|
||||
<div class="zoom-controls" role="group" aria-label="Zoom controls">
|
||||
<button id="zoomOut" class="btn" type="button" aria-label="Zoom out">−</button>
|
||||
<button id="zoomReset" class="btn" type="button" aria-label="Reset zoom">100%</button>
|
||||
<button id="zoomIn" class="btn" type="button" aria-label="Zoom in">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Viewer -->
|
||||
<section class="map-viewer" id="mapViewer" aria-label="Map viewer">
|
||||
<img
|
||||
id="mapImage"
|
||||
src="/images/TARKOV.webp"
|
||||
alt="Tarkov (Global) map"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
fetchpriority="high"
|
||||
draggable="false"
|
||||
/>
|
||||
<div class="map-grid-overlay" aria-hidden="true"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- HAMBURGER MENU SCRIPT -->
|
||||
<script>
|
||||
const btn = document.querySelector('.nav-toggle');
|
||||
const nav = document.getElementById('primary-nav');
|
||||
btn.addEventListener('click', () => {
|
||||
const open = btn.getAttribute('aria-expanded') === 'true';
|
||||
btn.setAttribute('aria-expanded', !open);
|
||||
nav.classList.toggle('is-open', !open);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- MAP SWITCH + ZOOM/PAN SCRIPT (desktop pan fix + no double-tap/dblclick) -->
|
||||
<script>
|
||||
(function () {
|
||||
// ---------- Available maps ----------
|
||||
const MAPS = [
|
||||
{ id: 'TARKOV', label: 'Tarkov (Global)', file: '/images/TARKOV.webp' },
|
||||
{ id: 'CUSTOMS', label: 'Customs', file: '/images/CUSTOMS.webp' },
|
||||
{ id: 'FACTORY', label: 'Factory', file: '/images/FACTORY.webp' },
|
||||
{ id: 'GROUND_ZERO', label: 'Ground Zero', file: '/images/GROUND_ZERO.webp' },
|
||||
{ id: 'INTERCHANGE', label: 'Interchange', file: '/images/INTERCHANGE.webp' },
|
||||
{ id: 'LABS', label: 'The Lab', file: '/images/LABS.webp' },
|
||||
{ id: 'LABYRINTH', label: 'Labyrinth', file: '/images/LABYRINTH.webp' },
|
||||
{ id: 'LIGHTHOUSE', label: 'Lighthouse', file: '/images/LIGHTHOUSE.webp' },
|
||||
{ id: 'RESERVE', label: 'Reserve', file: '/images/RESERVE.webp' },
|
||||
{ id: 'SHORELINE', label: 'Shoreline', file: '/images/SHORELINE.webp' },
|
||||
{ id: 'STREETS', label: 'Streets', file: '/images/STREETS.webp' },
|
||||
{ id: 'WOODS', label: 'Woods', file: '/images/WOODS.webp' }
|
||||
];
|
||||
|
||||
// ---------- Elements ----------
|
||||
const select = document.getElementById('mapSelect');
|
||||
const viewer = document.getElementById('mapViewer');
|
||||
const img = document.getElementById('mapImage');
|
||||
const zoomInBtn = document.getElementById('zoomIn');
|
||||
const zoomOutBtn = document.getElementById('zoomOut');
|
||||
const zoomResetBtn = document.getElementById('zoomReset');
|
||||
|
||||
// Block native image drag on all browsers
|
||||
img.addEventListener('dragstart', (e) => e.preventDefault());
|
||||
viewer.addEventListener('dragstart', (e) => e.preventDefault());
|
||||
|
||||
// ---------- Zoom/Pan state ----------
|
||||
let baseScale = 1; // fit-to-viewer "contain" scale (recomputed per image)
|
||||
let scale = 1; // current scale
|
||||
let minScale = 1; // lower bound = baseScale
|
||||
const maxScale = 5; // adjust if you want more/less max zoom
|
||||
let x = 0, y = 0; // pan offsets, in CSS px relative to center
|
||||
let isPanning = false;
|
||||
let lastX = 0, lastY = 0;
|
||||
|
||||
// Pointer tracking for pinch
|
||||
const pointers = new Map();
|
||||
let initialPinchDistance = 0;
|
||||
let pinchStart = null; // { x, y, scale }
|
||||
|
||||
// Keep transform origin centered
|
||||
img.style.transformOrigin = '50% 50%';
|
||||
|
||||
// Center + pan/zoom
|
||||
function applyTransform() {
|
||||
img.style.transform =
|
||||
`translate3d(-50%, -50%, 0) translate3d(${x}px, ${y}px, 0) scale(${scale})`;
|
||||
}
|
||||
|
||||
function clampPan() {
|
||||
const rect = viewer.getBoundingClientRect();
|
||||
const naturalW = img.naturalWidth || 1;
|
||||
const naturalH = img.naturalHeight || 1;
|
||||
const displayW = naturalW * scale;
|
||||
const displayH = naturalH * scale;
|
||||
|
||||
const maxOffsetX = Math.max(0, (displayW - rect.width) / 2);
|
||||
const maxOffsetY = Math.max(0, (displayH - rect.height) / 2);
|
||||
|
||||
x = Math.min(maxOffsetX, Math.max(-maxOffsetX, x));
|
||||
y = Math.min(maxOffsetY, Math.max(-maxOffsetY, y));
|
||||
}
|
||||
|
||||
// Fit image to viewer ("contain")
|
||||
function computeBaseScale() {
|
||||
const rect = viewer.getBoundingClientRect();
|
||||
const iw = img.naturalWidth || 1;
|
||||
const ih = img.naturalHeight || 1;
|
||||
baseScale = Math.min(rect.width / iw, rect.height / ih);
|
||||
if (!isFinite(baseScale) || baseScale <= 0) baseScale = 1;
|
||||
minScale = baseScale;
|
||||
}
|
||||
|
||||
function resetView() {
|
||||
computeBaseScale();
|
||||
scale = baseScale;
|
||||
x = 0; y = 0;
|
||||
clampPan();
|
||||
applyTransform();
|
||||
updateZoomUI();
|
||||
}
|
||||
|
||||
function updateZoomUI() {
|
||||
const pct = Math.round((scale / baseScale) * 100); // relative to fit
|
||||
zoomResetBtn.textContent = `${pct}%`;
|
||||
zoomOutBtn.disabled = scale <= minScale + 0.0001;
|
||||
zoomInBtn.disabled = scale >= maxScale - 0.0001;
|
||||
}
|
||||
|
||||
function zoomAtPoint(factor, clientX, clientY) {
|
||||
const rect = viewer.getBoundingClientRect();
|
||||
const cx = clientX - rect.left - rect.width / 2;
|
||||
const cy = clientY - rect.top - rect.height / 2;
|
||||
|
||||
const target = Math.min(maxScale, Math.max(minScale, scale * factor));
|
||||
const scaleFactor = target / scale;
|
||||
|
||||
x = cx - (cx - x) * scaleFactor;
|
||||
y = cy - (cy - y) * scaleFactor;
|
||||
|
||||
scale = target;
|
||||
clampPan();
|
||||
applyTransform();
|
||||
updateZoomUI();
|
||||
}
|
||||
|
||||
function setMap(id) {
|
||||
const m = MAPS.find(m => m.id === id) || MAPS[0];
|
||||
img.src = m.file;
|
||||
img.alt = `${m.label} map`;
|
||||
|
||||
const doReset = () => resetView();
|
||||
if ('decode' in img && typeof img.decode === 'function') {
|
||||
img.decode().then(doReset).catch(doReset);
|
||||
} else if (img.complete) {
|
||||
doReset();
|
||||
} else {
|
||||
img.onload = doReset;
|
||||
}
|
||||
}
|
||||
|
||||
// Map selection
|
||||
select.addEventListener('change', () => setMap(select.value));
|
||||
|
||||
// Initial load
|
||||
setMap(select.value || 'TARKOV');
|
||||
|
||||
// Handle viewer resize/rotation (keep relative zoom)
|
||||
let resizeTimer = 0;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
const prevRatio = scale / baseScale;
|
||||
computeBaseScale();
|
||||
scale = Math.max(minScale, Math.min(maxScale, baseScale * prevRatio));
|
||||
clampPan();
|
||||
applyTransform();
|
||||
updateZoomUI();
|
||||
}, 120);
|
||||
});
|
||||
|
||||
// ----- Mouse/trackpad zoom -----
|
||||
viewer.addEventListener('wheel', (e) => {
|
||||
e.preventDefault();
|
||||
const factor = e.deltaY > 0 ? 0.9 : 1.1;
|
||||
zoomAtPoint(factor, e.clientX, e.clientY);
|
||||
}, { passive: false });
|
||||
|
||||
// ----- Buttons (zoom around center) -----
|
||||
zoomInBtn.addEventListener('click', () => {
|
||||
const rect = viewer.getBoundingClientRect();
|
||||
zoomAtPoint(1.15, rect.left + rect.width / 2, rect.top + rect.height / 2);
|
||||
});
|
||||
zoomOutBtn.addEventListener('click', () => {
|
||||
const rect = viewer.getBoundingClientRect();
|
||||
zoomAtPoint(1 / 1.15, rect.left + rect.width / 2, rect.top + rect.height / 2);
|
||||
});
|
||||
zoomResetBtn.addEventListener('click', resetView);
|
||||
|
||||
// ====== POINTER EVENTS (pan + pinch) ======
|
||||
|
||||
// POINTER DOWN
|
||||
viewer.addEventListener('pointerdown', (e) => {
|
||||
// On desktop, only pan with primary (left) mouse button
|
||||
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
||||
|
||||
// Prevent native drag/select on desktop immediately
|
||||
if (e.pointerType === 'mouse') e.preventDefault();
|
||||
|
||||
viewer.setPointerCapture(e.pointerId);
|
||||
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||
|
||||
if (pointers.size === 1) {
|
||||
// Single pointer → begin pan
|
||||
isPanning = true;
|
||||
viewer.classList.add('is-panning'); // for grab/grabbing cursor
|
||||
lastX = e.clientX; lastY = e.clientY;
|
||||
} else if (pointers.size === 2) {
|
||||
// Two pointers → pinch start
|
||||
const pts = [...pointers.values()];
|
||||
initialPinchDistance = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
|
||||
pinchStart = { x, y, scale };
|
||||
}
|
||||
});
|
||||
|
||||
// POINTER MOVE
|
||||
viewer.addEventListener('pointermove', (e) => {
|
||||
if (!pointers.has(e.pointerId)) return;
|
||||
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||
|
||||
if (pointers.size === 1 && isPanning) {
|
||||
// Prevent default drag/select/scroll on desktop while panning
|
||||
if (e.pointerType === 'mouse') e.preventDefault();
|
||||
|
||||
const dx = e.clientX - lastX;
|
||||
const dy = e.clientY - lastY;
|
||||
lastX = e.clientX; lastY = e.clientY;
|
||||
x += dx; y += dy;
|
||||
clampPan();
|
||||
applyTransform();
|
||||
} else if (pointers.size === 2 && pinchStart) {
|
||||
// Pinch zoom
|
||||
const pts = [...pointers.values()];
|
||||
const dist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
|
||||
const centerX = (pts[0].x + pts[1].x) / 2;
|
||||
const centerY = (pts[0].y + pts[1].y) / 2;
|
||||
|
||||
const newScale = Math.min(maxScale, Math.max(minScale, pinchStart.scale * (dist / initialPinchDistance)));
|
||||
|
||||
const rect = viewer.getBoundingClientRect();
|
||||
const cx = centerX - rect.left - rect.width / 2;
|
||||
const cy = centerY - rect.top - rect.height / 2;
|
||||
|
||||
const scaleFactor = newScale / scale;
|
||||
x = cx - (cx - x) * scaleFactor;
|
||||
y = cy - (cy - y) * scaleFactor;
|
||||
scale = newScale;
|
||||
|
||||
clampPan();
|
||||
applyTransform();
|
||||
updateZoomUI();
|
||||
}
|
||||
});
|
||||
|
||||
// POINTER UP / CANCEL / LEAVE
|
||||
function endPointer(e) {
|
||||
if (pointers.has(e.pointerId)) pointers.delete(e.pointerId);
|
||||
if (pointers.size < 2) {
|
||||
initialPinchDistance = 0;
|
||||
pinchStart = null;
|
||||
}
|
||||
if (pointers.size === 0) {
|
||||
isPanning = false;
|
||||
viewer.classList.remove('is-panning'); // restore cursor
|
||||
}
|
||||
viewer.releasePointerCapture?.(e.pointerId);
|
||||
}
|
||||
viewer.addEventListener('pointerup', endPointer);
|
||||
viewer.addEventListener('pointercancel', endPointer);
|
||||
viewer.addEventListener('pointerleave', endPointer);
|
||||
|
||||
// Prevent native gesture zoom (iOS Safari)
|
||||
viewer.addEventListener('gesturestart', (e) => e.preventDefault());
|
||||
viewer.addEventListener('gesturechange', (e) => e.preventDefault());
|
||||
viewer.addEventListener('gestureend', (e) => e.preventDefault());
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user