initial commit

This commit is contained in:
2026-06-25 21:26:53 +00:00
commit e5a1511098
68 changed files with 186669 additions and 0 deletions
Executable
+358
View File
@@ -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>