Generate dynamic map with zoom and pan functionality

by der_robert

HTML

<div id="map-wrapper">
  <div id="map-content">
  </div>
</div>

CSS

#map-wrapper {
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  position: relative;
}

#map-content {
  position: absolute;
  transform-origin: top left;
}

.map-island, .map-event, .map-ship {
  position: absolute;
  width: 12px;
  height: 12px;
  border-radius: 50%;
}

.map-island {
  background-color: sandybrown;
}

.map-event.wrack {
  background-color: gray;
}

.map-event.storm {
  background-color: green;
}

.map-ship {
  background-color: white;
}

.map-route {
  height: 2px;
  background: white;
  position: absolute;
  transform-origin: 0 0;
  opacity: 0.4;
}

.map-route-marker {
  width: 16px;
  height: 16px;
  position: absolute;
  pointer-events: none;
}

JavaScript

const wrapper = document.getElementById('map-wrapper');
const content = document.getElementById('map-content');

let scale = 2;
let offsetX = 0;
let offsetY = 0;

const mapData = {
  "islands": [
    { "id": 1, "name": "Insel Alpha", "x": 100, "y": 150 },
    { "id": 2, "name": "Insel Beta", "x": 200, "y": 80 },
    { "id": 3, "name": "Insel Gamma", "x": 300, "y": 220 },
    { "id": 4, "name": "Insel Delta", "x": 400, "y": 180 },
    { "id": 5, "name": "Insel Epsilon", "x": 500, "y": 300 },
    { "id": 6, "name": "Insel Zeta", "x": 150, "y": 350 },
    { "id": 7, "name": "Insel Eta", "x": 250, "y": 400 },
    { "id": 8, "name": "Insel Theta", "x": 350, "y": 120 },
    { "id": 9, "name": "Insel Iota", "x": 450, "y": 250 },
    { "id": 10, "name": "Insel Kappa", "x": 550, "y": 100 }
  ],
  "events": [
    { "type": "storm", "x": 130, "y": 330 },
    { "type": "storm", "x": 230, "y": 370 },
    { "type": "storm", "x": 330, "y": 310 },
    { "type": "storm", "x": 430, "y": 290 },
    { "type": "storm", "x": 530, "y": 350 },
    { "type": "wrack", "x": 160, "y": 210 },
    { "type": "wrack", "x": 260, "y": 190 },
    { "type": "wrack", "x": 360, "y": 230 },
    { "type": "wrack", "x": 460, "y": 170 },
    { "type": "wrack", "x": 560, "y": 200 }
  ],
  "ships": [
    {
      "id": 1,
      "from": [100, 100],
      "to": [400, 300],
      "progress": 0.2
    },
    {
      "id": 2,
      "from": [150, 400],
      "to": [500, 100],
      "progress": 0.75
    }
  ],
  "routes": [
    {
      "from": [100, 100],
      "to": [300, 250],
      "image": "/assets/icon.png",
      "at": [200, 170]
    }
  ]
};

function renderMap(data) {
  content.innerHTML = '';

  // Wassertextur-Hintergrund
  content.style.backgroundImage = 'repeating-radial-gradient(circle at 0 0, #0077aa, #0099cc 20px)';
  content.style.backgroundSize = '100px 100px';

  // Routen (als Linien mit optionalem Bildpunkt)
  data.routes?.forEach(r...