Files
2026-06-25 21:26:53 +00:00

359 lines
14 KiB
HTML
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>