draw-your-world / index.html
Kingrane's picture
Add 3 files
6d6ebdf verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>World Map Editor</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/chroma.min.js"></script>
<script src="https://unpkg.com/[email protected]/countries-110m.json"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://d3js.org/topojson.v3.min.js"></script>
<style>
.country {
stroke: #333;
stroke-width: 0.5px;
transition: all 0.3s ease;
}
.country:hover {
stroke-width: 1.5px;
stroke: #000;
}
.color-picker {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: 40px;
height: 40px;
background-color: transparent;
border: none;
cursor: pointer;
}
.color-picker::-webkit-color-swatch {
border-radius: 8px;
border: 2px solid #ccc;
}
.color-picker::-moz-color-swatch {
border-radius: 8px;
border: 2px solid #ccc;
}
.tooltip {
position: absolute;
padding: 8px;
background: rgba(0, 0, 0, 0.8);
color: white;
border-radius: 4px;
pointer-events: none;
font-size: 12px;
z-index: 10;
}
.zoom-controls {
position: absolute;
top: 20px;
right: 20px;
z-index: 100;
display: flex;
flex-direction: column;
gap: 8px;
}
.zoom-btn {
width: 36px;
height: 36px;
border-radius: 50%;
background: white;
border: 1px solid #ddd;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
font-size: 18px;
font-weight: bold;
}
.zoom-btn:hover {
background: #f0f0f0;
}
</style>
</head>
<body class="bg-gray-100 min-h-screen">
<div class="container mx-auto px-4 py-8">
<header class="mb-8">
<h1 class="text-4xl font-bold text-center text-blue-800 mb-2">World Map Editor</h1>
<p class="text-center text-gray-600">Customize your world map - color countries, adjust borders, and create your perfect visualization</p>
</header>
<div class="flex flex-col lg:flex-row gap-8">
<!-- Controls Panel -->
<div class="w-full lg:w-1/4 bg-white p-6 rounded-lg shadow-md">
<h2 class="text-xl font-semibold mb-4 text-gray-800">Controls</h2>
<div class="mb-6">
<h3 class="font-medium mb-2 text-gray-700">Borders</h3>
<div class="flex flex-wrap gap-2">
<button id="showAllBorders" class="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 transition">Show All</button>
<button id="hideAllBorders" class="px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition">Hide All</button>
<button id="toggleBorders" class="px-3 py-1 bg-gray-500 text-white rounded hover:bg-gray-600 transition">Toggle</button>
</div>
<div class="mt-3">
<label class="flex items-center">
<input type="range" id="borderWidth" min="0" max="5" step="0.5" value="0.5" class="w-full">
<span class="ml-2 text-sm text-gray-600">Border width: <span id="borderWidthValue">0.5</span>px</span>
</label>
</div>
</div>
<div class="mb-6">
<h3 class="font-medium mb-2 text-gray-700">Country Color</h3>
<div class="flex items-center gap-3 mb-3">
<input type="color" id="countryColor" value="#4CAF50" class="color-picker">
<button id="applyColor" class="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 transition">Apply</button>
<button id="resetColors" class="px-3 py-1 bg-gray-500 text-white rounded hover:bg-gray-600 transition">Reset</button>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" id="randomColors" class="h-4 w-4">
<label for="randomColors" class="text-sm text-gray-600">Random colors</label>
</div>
</div>
<div class="mb-6">
<h3 class="font-medium mb-2 text-gray-700">Background</h3>
<input type="color" id="bgColor" value="#F3F4F6" class="color-picker">
<span class="ml-2 text-sm text-gray-600">Map background</span>
</div>
<div class="mb-6">
<h3 class="font-medium mb-2 text-gray-700">Selected Country</h3>
<div id="selectedCountryInfo" class="p-3 bg-gray-100 rounded text-sm text-gray-700">
Click on a country to select it
</div>
</div>
<div>
<h3 class="font-medium mb-2 text-gray-700">Save/Load</h3>
<div class="flex flex-wrap gap-2">
<button id="saveMap" class="px-3 py-1 bg-purple-500 text-white rounded hover:bg-purple-600 transition">Save Map</button>
<button id="loadMap" class="px-3 py-1 bg-yellow-500 text-white rounded hover:bg-yellow-600 transition">Load Map</button>
<button id="exportPNG" class="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 transition">Export PNG</button>
</div>
</div>
</div>
<!-- Map Container -->
<div class="w-full lg:w-3/4 relative">
<div class="bg-white p-4 rounded-lg shadow-md">
<div id="mapContainer" class="relative overflow-hidden border border-gray-300 rounded">
<svg id="map" width="100%" height="600"></svg>
<div id="tooltip" class="tooltip"></div>
<div class="zoom-controls">
<button id="zoomIn" class="zoom-btn">+</button>
<button id="zoomOut" class="zoom-btn"></button>
<button id="resetZoom" class="zoom-btn"></button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Initialize variables
let selectedCountry = null;
let countryColors = {};
let showBorders = true;
let borderWidth = 0.5;
let transform = d3.zoomIdentity;
let svg, g, projection, path;
// Wait for DOM to load
document.addEventListener('DOMContentLoaded', function() {
// Initialize map
initMap();
// Set up event listeners
setupEventListeners();
});
function initMap() {
const width = document.getElementById('mapContainer').clientWidth;
const height = 600;
// Create SVG
svg = d3.select("#map")
.attr("width", width)
.attr("height", height)
.call(d3.zoom()
.scaleExtent([1, 8])
.on("zoom", zoomed))
.on("click", stopped, true);
// Create group for all map elements
g = svg.append("g");
// Set up projection
projection = d3.geoMercator()
.scale(width / 2 / Math.PI)
.translate([width / 2, height / 2]);
// Create path generator
path = d3.geoPath().projection(projection);
// Load and draw world map
d3.json("https://unpkg.com/[email protected]/countries-110m.json").then(function(world) {
const countries = topojson.feature(world, world.objects.countries).features;
// Draw countries
g.selectAll(".country")
.data(countries)
.enter()
.append("path")
.attr("class", "country")
.attr("d", path)
.attr("id", d => "country-" + d.id)
.attr("fill", d => getRandomColor(d.id))
.attr("stroke", "#333")
.attr("stroke-width", borderWidth)
.on("mouseover", function(event, d) {
d3.select(this).attr("stroke-width", borderWidth * 2);
showTooltip(event, d);
})
.on("mouseout", function() {
if (selectedCountry !== this) {
d3.select(this).attr("stroke-width", borderWidth);
}
hideTooltip();
})
.on("click", function(event, d) {
// Reset previously selected country
if (selectedCountry) {
d3.select(selectedCountry).attr("stroke-width", borderWidth);
}
// Set new selected country
selectedCountry = this;
d3.select(this).attr("stroke-width", borderWidth * 3);
// Update selected country info
updateSelectedCountryInfo(d);
});
// Store country data for later use
window.countriesData = countries;
});
}
function setupEventListeners() {
// Border controls
document.getElementById('showAllBorders').addEventListener('click', function() {
showBorders = true;
updateBorders();
});
document.getElementById('hideAllBorders').addEventListener('click', function() {
showBorders = false;
updateBorders();
});
document.getElementById('toggleBorders').addEventListener('click', function() {
showBorders = !showBorders;
updateBorders();
});
document.getElementById('borderWidth').addEventListener('input', function() {
borderWidth = parseFloat(this.value);
document.getElementById('borderWidthValue').textContent = borderWidth;
updateBorders();
});
// Color controls
document.getElementById('applyColor').addEventListener('click', function() {
if (!selectedCountry) {
alert("Please select a country first");
return;
}
const color = document.getElementById('countryColor').value;
const countryId = selectedCountry.id.replace('country-', '');
// Store color for this country
countryColors[countryId] = color;
// Apply color to selected country
d3.select(selectedCountry).attr("fill", color);
// Update info panel
updateSelectedCountryInfo();
});
document.getElementById('resetColors').addEventListener('click', function() {
countryColors = {};
d3.selectAll(".country").attr("fill", d => getRandomColor(d.id));
updateSelectedCountryInfo();
});
document.getElementById('randomColors').addEventListener('change', function() {
if (this.checked) {
d3.selectAll(".country").attr("fill", d => getRandomColor(d.id));
}
});
// Background color
document.getElementById('bgColor').addEventListener('input', function() {
document.getElementById('mapContainer').style.backgroundColor = this.value;
});
// Zoom controls
document.getElementById('zoomIn').addEventListener('click', function() {
svg.transition().call(d3.zoom().scaleBy, 2);
});
document.getElementById('zoomOut').addEventListener('click', function() {
svg.transition().call(d3.zoom().scaleBy, 0.5);
});
document.getElementById('resetZoom').addEventListener('click', function() {
svg.transition().call(d3.zoom().transform, d3.zoomIdentity);
});
// Save/Load
document.getElementById('saveMap').addEventListener('click', saveMap);
document.getElementById('loadMap').addEventListener('click', loadMap);
document.getElementById('exportPNG').addEventListener('click', exportPNG);
}
function updateBorders() {
const strokeValue = showBorders ? "#333" : "none";
d3.selectAll(".country")
.attr("stroke", strokeValue)
.attr("stroke-width", showBorders ? borderWidth : 0);
if (selectedCountry) {
d3.select(selectedCountry).attr("stroke-width", showBorders ? borderWidth * 3 : 0);
}
}
function updateSelectedCountryInfo(d) {
const infoPanel = document.getElementById('selectedCountryInfo');
if (!selectedCountry) {
infoPanel.innerHTML = "Click on a country to select it";
return;
}
const countryId = selectedCountry.id.replace('country-', '');
const countryData = window.countriesData.find(c => c.id == countryId);
const countryName = countryData?.properties?.name || "Unknown Country";
const currentColor = countryColors[countryId] || d3.select(selectedCountry).attr("fill");
infoPanel.innerHTML = `
<strong>${countryName}</strong><br>
<div class="mt-1 flex items-center">
<span>Color: </span>
<div class="w-4 h-4 ml-2 border border-gray-300" style="background-color: ${currentColor}"></div>
<span class="ml-1">${currentColor}</span>
</div>
<div class="mt-1">ID: ${countryId}</div>
`;
}
function showTooltip(event, d) {
const tooltip = document.getElementById('tooltip');
const countryName = d.properties?.name || "Unknown Country";
tooltip.style.display = 'block';
tooltip.style.left = (event.pageX + 10) + 'px';
tooltip.style.top = (event.pageY + 10) + 'px';
tooltip.innerHTML = countryName;
}
function hideTooltip() {
document.getElementById('tooltip').style.display = 'none';
}
function getRandomColor(id) {
// If we have a stored color for this country, use it
if (countryColors[id]) {
return countryColors[id];
}
// Otherwise generate a random color or use default
if (document.getElementById('randomColors')?.checked) {
return chroma.random().hex();
}
return "#DDD";
}
function zoomed(event) {
transform = event.transform;
g.attr("transform", transform);
g.attr("stroke-width", borderWidth / transform.k);
}
function stopped() {
if (d3.event.defaultPrevented) return;
if (!d3.event.target.closest('.country')) {
// Clicked on empty space - deselect country
if (selectedCountry) {
d3.select(selectedCountry).attr("stroke-width", borderWidth);
selectedCountry = null;
updateSelectedCountryInfo();
}
}
}
function saveMap() {
const mapData = {
countryColors: countryColors,
showBorders: showBorders,
borderWidth: borderWidth,
bgColor: document.getElementById('bgColor').value,
transform: transform
};
const dataStr = JSON.stringify(mapData);
localStorage.setItem('worldMapEditorData', dataStr);
alert("Map saved successfully!");
}
function loadMap() {
const savedData = localStorage.getItem('worldMapEditorData');
if (!savedData) {
alert("No saved map found");
return;
}
try {
const mapData = JSON.parse(savedData);
// Apply saved settings
countryColors = mapData.countryColors || {};
showBorders = mapData.showBorders !== false;
borderWidth = mapData.borderWidth || 0.5;
document.getElementById('borderWidth').value = borderWidth;
document.getElementById('borderWidthValue').textContent = borderWidth;
document.getElementById('bgColor').value = mapData.bgColor || "#F3F4F6";
document.getElementById('mapContainer').style.backgroundColor = mapData.bgColor || "#F3F4F6";
// Apply colors to countries
d3.selectAll(".country").each(function() {
const countryId = this.id.replace('country-', '');
if (countryColors[countryId]) {
d3.select(this).attr("fill", countryColors[countryId]);
}
});
// Apply borders
updateBorders();
// Apply transform if available
if (mapData.transform) {
transform = mapData.transform;
g.attr("transform", transform);
g.attr("stroke-width", borderWidth / transform.k);
}
alert("Map loaded successfully!");
} catch (e) {
console.error("Error loading map:", e);
alert("Error loading saved map");
}
}
function exportPNG() {
const svgElement = document.getElementById('map');
const svgData = new XMLSerializer().serializeToString(svgElement);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = function() {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const pngFile = canvas.toDataURL('image/png');
const downloadLink = document.createElement('a');
downloadLink.download = 'world-map.png';
downloadLink.href = pngFile;
downloadLink.click();
};
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData)));
}
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Kingrane/draw-your-world" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>