JSFiddle - React, Tailwind, and code Playground

by der_robert

HTML

<div class="viewport" id="viewport">
    <div class="canvas" id="canvas">
      <svg class="lines" id="lines"></svg>
    </div>
  </div>

CSS

body {
      margin: 0;
      background: #0b1e2d;
      color: #fff;
      font-family: Arial, sans-serif;
    }
    .viewport {
      position: absolute;
      inset: 0;
      overflow: auto;
    }
    .canvas {
      position: relative;
      width: 2000px;
      height: 3000px;
    }
    svg.lines {
      position: absolute;
      inset: 0;
      pointer-events: none;
      overflow: visible;
    }
    .node {
      position: absolute;
      width: 180px;
      background: rgba(0,0,0,0.6);
      border: 2px solid #654321;
      border-radius: 8px;
      padding: 8px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.5);
      text-align: center;
    }
    .node .icon {
      font-size: 24px;
      margin-bottom: 4px;
    }
    .node .name {
      font-weight: bold;
      margin-bottom: 4px;
    }
    .edge {
      stroke: #c2b280;
      stroke-width: 2;
      fill: none;
    }

JavaScript

const BUILDINGS = [
  {id:1,  name:'Goldmine', icon:'⚜️'},
  {id:2,  name:'Steinbruch', icon:'⛏️'},
  {id:3,  name:'Sägewerk', icon:'🪓'},
  {id:4,  name:'Kohlemine', icon:'🪨'},
  {id:5,  name:'Eisenerzmine', icon:'⛏️'},
  {id:6,  name:'Schmiede', icon:'⚒️'},
  {id:7,  name:'Kaserne', icon:'🛡️'},
  {id:8,  name:'Werft', icon:'🚢'},
  {id:9,  name:'Lagerhaus', icon:'📦'},
  {id:10, name:'Steinwall', icon:'🧱'},
  {id:11, name:'Akademie', icon:'🎓'},
  {id:12, name:'Wachturm', icon:'🗼'},
  {id:13, name:'Marktplatz', icon:'🏪'},
  {id:15, name:'Stallungen', icon:'🐎'},
  {id:16, name:'Belagerungswerkstatt', icon:'⚙️'},
  {id:17, name:'Kirche', icon:'⛪'},
  {id:18, name:'Friedhof', icon:'🪦'},
  {id:19, name:'Hafen', icon:'⚓️'},
  {id:20, name:'Brunnen', icon:'⛲'},
  {id:21, name:'Windmühle', icon:'💨'},
];
const REQUIREMENTS = [
  {from:1,  to:4}, {from:3,  to:4}, {from:2,  to:4},
  {from:4,  to:5}, {from:5,  to:6}, {from:6,  to:7},
  {from:6,  to:8}, {from:8,  to:10}, {from:7,  to:10},
  {from:2,  to:10}, {from:6,  to:11}, {from:1,  to:11},
  {from:11, to:12}, {from:7,  to:12}, {from:8,  to:13},
  {from:9,  to:13}, {from:6,  to:15}, {from:7,  to:15},
  {from:11, to:15}, {from:6,  to:16}, {from:7,  to:16},
  {from:15, to:16}, {from:8,  to:16}, {from:8,  to:19},
  {from:13, to:17}, {from:17, to:18}, {from:13, to:21}
];

function computeRanks(){
  const rank = new Map(BUILDINGS.map(b=>[b.id,0]));
  let changed = true;
  while(changed){
    changed=false;
    for(const e of REQUIREMENTS){
      if(rank.get(e.to) <= rank.get(e.from)){
        rank.set(e.to, rank.get(e.from)+1);
        changed=true;
      }
    }
  }
  return rank;
}
const rank = computeRanks();
const rows = {};
for(const b of BUILDINGS){
  const r = rank.get(b.id);
  if(!rows[r]) rows[r]=[];
  rows[r].push(b);
}
const rowGap = 200;
const colGap = 220;
const positions = {};
Object.keys(rows).forEach(r=>{
  rows[r].forEach((b,i)=>{
    positions[b.id] =...