Skip to content

Instantly share code, notes, and snippets.

@Azgaar
Last active November 22, 2023 14:26
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save Azgaar/b845ce22ea68090d43a4ecfb914f51bd to your computer and use it in GitHub Desktop.
Save Azgaar/b845ce22ea68090d43a4ecfb914f51bd to your computer and use it in GitHub Desktop.
Fantasy Map Generator
license: gpl-3.0
height: 760
border: no

It is a Fantasy Map Generator based on D3 Voronoi diagram rendered to scalable svg.

Use random to genarate the map with default options, customize to make your own shape.

Project goal is to create a procedural generated map for my Medieval Dinasty game simulator. So a map should be interactive, fast and plausible-looking. The easiest way is to generate Isles that will have enought place to locate at least 500 manors within 7 cultural areas. The imagined area is about 20.000 km2 (like Wales shaped as Isles of Scilly).

As I am just a beginner in programming please leave a comment on github page in case of any thoughts.

Inspiration:

Used JS Libraries:

TO-DO:

  • Add Mimimap like the Bill White's one
  • Different trees types (spruce, dry wood etc.)
  • Use different namesets for areas
  • Resize relief signs and names on map rescale
@import url('https://fonts.googleapis.com/css?family=Bitter:400,400i&subset=latin-ext');
svg {
background-color: #5E4FA2;
cursor: default;
}
.terrs {
stroke-width: 0.67px;
stroke-linejoin: round;
stroke-linecap: round;
-webkit-filter: saturate(0.8) contrast(1.1);
filter: saturate(0.8) contrast(1.1);
}
.areas {
stroke-width: 0.67px;
stroke-linejoin: round;
stroke-linecap: round;
opacity: 0.8;
}
.rivers {
fill: none;
stroke: #4D83AE;
stroke-width: 0.4px;
stroke-linecap: round;
}
.coastline {
stroke-width: 0.74px;
stroke: rgb(86, 86, 109);
stroke-linecap: round;
}
.burgs {
stroke-width: 0.2px;
opacity: 0.8;
font-family: verdana;
font-size: 2px;
text-anchor: middle;
cursor: pointer;
}
.capital {
fill: white;
stroke: black;
opacity: 0.8;
}
.manor {
stroke: none;
fill: black;
opacity: 0.8;
}
.capital:hover,
.manor:hover {
stroke: blue;
cursor: pointer;
}
.names {
font-family: 'Bitter', verdana;
text-anchor: middle;
fill: #3e3e4b;
text-shadow: 0 0 6px white;
}
.active {
text-shadow: 0 0 6px red;
cursor: grabbing;
cursor: -webkit-grabbing;
}
.borders {
stroke-width: 0.72px;
stroke: rgb(86, 86, 109);
stroke-dasharray: 0.5, 0.5;
stroke-linecap: butt;
}
.hills {
stroke-width: 0.1px;
fill: #999999;
}
.mounts {
stroke-width: 0.1px;
fill: white;
}
.strokes {
stroke-width: 0.08px;
width: 2px;
stroke: #5c5c70;
stroke-dasharray: 0.5, 0.7;
stroke-linecap: round;
}
.swamps {
stroke-width: 0.05px;
fill: none;
stroke: #5c5c70;
}
.forests {
stroke-width: 0.1px;
stroke: #5c5c70;
}
<!DOCTYPE html>
<meta charset="utf-8">
<svg width="960" height="540">
<defs>
<radialGradient id="g346" gradientUnits="userSpaceOnUse" cx="50%" cy="50%" r="60%">
<stop stop-color="#4697B3" offset="0" />
<stop stop-color="#5E4FA2" offset="1" />
</radialGradient>
</defs>
<rect x="0" y="-250" width="960" height="960" fill="url(#g346)" />
</svg>
<br>Toggle: <button id="highmap" status=1 onclick="toggleHigh(this)">Highmap</button>
<button id="relief" status=1 onclick="toggleRelief(this)">Relief</button>
<button onclick="$('.names').fadeToggle()">Names</button>
<button id="area" status=1 onclick="toggleAreas(this)">Areas</button>
<button onclick="$('.borders').toggle()">Borders</button>
<button id="fluxmap" status=1 onclick="toggleFlux(this)">Flux</button>
<br>Coodr: <span id="lx">0</span>/<span id="ly">0</span>; Cell: <span id="cell">0</span>; High: <span id="high">0</span>; Flux: <span id="flux">0</span>; Region: <span id="capital">no</span>; River: <span id="river">no</span>;
<br>
<button onclick="undraw(), generate()">Generate!</button>
<button onclick="$('#options').fadeToggle()">Options</button>
<button onclick="$('#custom').fadeToggle()">Customize</button>
<div id="options" hidden>
Manors:
<input id="manorsInput" type="range" min="0" max="700" value="500" oninput="manorsOutpoot.value = manorsInput.valueAsNumber">
<output id="manorsOutpoot">500</output>
<br> Regions:
<input id="regionsInput" type="range" min="0" max="100" value="7" oninput="regionsOutpoot.value = regionsInput.valueAsNumber">
<output id="regionsOutpoot">7</output>
<br> Regions Disbalance:
<input id="powerInput" type="range" min="0" max="3" step="0.3" value="0.6" oninput="powerOutpoot.value = powerInput.valueAsNumber">
<output id="powerOutpoot">0.6</output>
<br> Swampiness:
<input id="swampinessInput" type="range" min="0" max="100" value="10" oninput="swampinessOutpoot.value = swampinessInput.valueAsNumber">
<output id="swampinessOutpoot">10</output>
<br> Sharpness:
<input id="sharpnessInput" type="range" min="0.15" max="0.3" value="0.2" step="0.05" oninput="sharpnessOutpoot.value = sharpnessInput.valueAsNumber">
<output id="sharpnessOutpoot">0.2</output>
</div>
<div id="custom" hidden>
<button onclick="undraw()">Clear</button>
<button onclick="island(), drawCoastline()">Add Island</button>
<button onclick="hill(1), drawCoastline()">Add Hill</button>
<button onclick="rescale(1.1)">+</button>
<button onclick="rescale(0.9)">-</button>
<button onclick="redrawCoastline()">Redraw Coastline</button>
<button onclick="getMap()">Get map!</button>
</div>
<link rel="stylesheet" type="text/css" href="index.css" />
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://ariutta.github.io/svg-pan-zoom/dist/svg-pan-zoom.js"></script>
<script>
// Fantasy Map Generator main script
var svg = d3.select("svg"),
terrs = svg.append("g").attr("class", "terrs").on("touchmove mousemove", moved),
areas = svg.append("g").attr("class", "areas"),
borders = svg.append("g").attr("class", "borders"),
rivers = svg.append("g").attr("class", "rivers"),
coastline = svg.append("g").attr("class", "coastline"),
terrain = svg.append("g").attr("class", "terrain"),
names = svg.append("g").attr("class", "names"),
burgs = svg.append("g").attr("class", "burgs"),
width = +svg.attr("width"),
height = +svg.attr("height"),
color = d3.scaleSequential(d3.interpolateSpectral),
colorFlux = d3.scaleSequential(d3.interpolateBlues),
colors8 = d3.scaleOrdinal(d3.schemeSet2),
manorNames = ["Abingdon", "Albrighton", "Alcester", "Almondbury", "Altrincham", "Amersham", "Andover", "Appleby", "Ashboume", "Atherstone", "Aveton", "Axbridge", "Aylesbury", "Baldock", "Bamburgh", "Barton", "Basingstoke", "Berden", "Bere", "Berkeley", "Berwick", "Betley", "Bideford", "Bingley", "Birmingham", "Blandford", "Blechingley", "Bodmin", "Bolton", "Bootham", "Boroughbridge", "Boscastle", "Bossinney", "Bramber", "Brampton", "Brasted", "Bretford", "Bridgetown", "Bridlington", "Bromyard", "Bruton", "Buckingham", "Bungay", "Burton", "Calne", "Cambridge", "Canterbury", "Carlisle", "Castleton", "Caus", "Charmouth", "Chawleigh", "Chichester", "Chillington", "Chinnor", "Chipping", "Chisbury", "Cleobury", "Clifford", "Clifton", "Clitheroe", "Cockermouth", "Coleshill", "Combe", "Congleton", "Crafthole", "Crediton", "Cuddenbeck", "Dalton", "Darlington", "Dodbrooke", "Drax", "Dudley", "Dunstable", "Dunster", "Dunwich", "Durham", "Dymock", "Exeter", "Exning", "Faringdon", "Felton", "Fenny", "Finedon", "Flookburgh", "Fowey", "Frampton", "Gateshead", "Gatton", "Godmanchester", "Grampound", "Grantham", "Guildford", "Halesowen", "Halton", "Harbottle", "Harlow", "Hatfield", "Hatherleigh", "Haydon", "Helston", "Henley", "Hertford", "Heytesbury", "Hinckley", "Hitchin", "Holme", "Hornby", "Horsham", "Kendal", "Kenilworth", "Kilkhampton", "Kineton", "Kington", "Kinver", "Kirby", "Knaresborough", "Knutsford", "Launceston", "Leighton", "Lewes", "Linton", "Louth", "Luton", "Lyme", "Lympstone", "Macclesfield", "Madeley", "Malborough", "Maldon", "Manchester", "Manningtree", "Marazion", "Marlborough", "Marshfield", "Mere", "Merryfield", "Middlewich", "Midhurst", "Milborne", "Mitford", "Modbury", "Montacute", "Mousehole", "Newbiggin", "Newborough", "Newbury", "Newenden", "Newent", "Norham", "Northleach", "Noss", "Oakham", "Olney", "Orford", "Ormskirk", "Oswestry", "Padstow", "Paignton", "Penkneth", "Penrith", "Penzance", "Pershore", "Petersfield", "Pevensey", "Pickering", "Pilton", "Pontefract", "Portsmouth", "Preston", "Quatford", "Reading", "Redcliff", "Retford", "Rockingham", "Romney", "Rothbury", "Rothwell", "Salisbury", "Saltash", "Seaford", "Seasalter", "Sherston", "Shifnal", "Shoreham", "Sidmouth", "Skipsea", "Skipton", "Solihull", "Somerton", "Southam", "Southwark", "Standon", "Stansted", "Stapleton", "Stottesdon", "Sudbury", "Swavesey", "Tamerton", "Tarporley", "Tetbury", "Thatcham", "Thaxted", "Thetford", "Thornbury", "Tintagel", "Tiverton", "Torksey", "Totnes", "Towcester", "Tregoney", "Trematon", "Tutbury", "Uxbridge", "Wallingford", "Wareham", "Warenmouth", "Wargrave", "Warton", "Watchet", "Watford", "Wendover", "Westbury", "Westcheap", "Weymouth", "Whitford", "Wickwar", "Wigan", "Wigmore", "Winchelsea", "Winkleigh", "Wiscombe", "Witham", "Witheridge", "Wiveliscombe", "Woodbury", "Yeovil"];
generate(); // genarate map on load
function generate() {
// get options values
manorsCount = manorsInput.value,
capitalsCount = regionsInput.value,
power = powerInput.value,
swampiness = swampinessInput.value,
sharpness = sharpnessInput.value;
// update buttons state
highmap.setAttribute("status", 1);
area.setAttribute("status", 1);
relief.setAttribute("status", 1);
fluxmap.setAttribute("status", 1);
// set global variables (is it correct way?)
land = [], usedCells = [], riversData = [], seashore = [], manors = [], capitals = [], queue = [];
// generate voronoi diagram using d3
sites = d3.range(8000).map(function(d) {
// do not generate sites near borders to increase cells density in a map center
return [Math.random() * width * 0.9 + width * 0.05, Math.random() * height * 0.9 + height * 0.05];
}),
voronoi = d3.voronoi().extent([[0, 0],[width, height]]),
diagram = voronoi(sites);
// generation routine
console.time('Total');
console.time('relax');
relax();
console.timeEnd('relax');
console.time('island');
island();
console.timeEnd('island');
console.time('hill');
hill(10);
console.timeEnd('hill');
console.time('coastline');
drawCoastline();
console.timeEnd('coastline');
console.time('flux');
resolveDepressions();
console.timeEnd('flux');
console.time('drawLand');
drawLand();
console.timeEnd('drawLand');
console.time('toggleHigh');
toggleHigh(highmap);
console.timeEnd('toggleHigh');
console.time('defineManors');
prepareManors();
defineCapitals();
drawManors();
defineAreas();
console.timeEnd('defineManors');
console.time('defineBorders');
defineBorders();
console.timeEnd('defineBorders');
console.timeEnd('Total');
}
// Apply Pan and Zoom library for the map; should be replaced by native D3 functionality
$(function() {
panZoomInstance = svgPanZoom("svg", {
zoomEnabled: true,
controlIconsEnabled: true,
fit: false,
center: false,
maxZoom: 30,
minZoom: 0.8
});
panZoomInstance.zoom(1);
})
// Get polygon info on mouse move (useful for debugging)
function moved() {
var point = d3.mouse(this),
nearest = diagram.find(point[0], point[1]).index;
$("#lx").text(point[0].toFixed(0));
$("#ly").text(point[1].toFixed(0));
$("#cell").text(nearest);
$("#high").text((polygons[nearest].high).toFixed(2));
$("#flux").text((polygons[nearest].flux).toFixed(3));
if (polygons[nearest].river) {
$("#river").text(polygons[nearest].river);
} else {
$("#river").text("no");
}
$("#capital").text((polygons[nearest].capital));
}
// one iteration of Lloyd's ralaxation (tried more iterations but didn't get much better result)
function relax() {
sites = diagram.polygons().map(d3.polygonCentroid);
diagram = voronoi(sites);
polygons = diagram.polygons();
for (var i = 0; i < polygons.length; i++) {
polygons[i].id = i;
polygons[i].high = 0;
if (polygons[i].data[1] >= height / 2) {
polygons[i].flux = 0.01;
} else {
polygons[i].flux = 0.007;
}
var neighbours = [];
diagram.cells[i].halfedges.forEach(function(e) {
edge = diagram.edges[e];
if (edge.left && edge.right) {
ea = edge.left.index;
if (ea === i) {
ea = edge.right.index;
}
neighbours.push(ea);
}
})
polygons[i].neighbours = neighbours;
}
}
// Clear the map and regenerate the voronoi diagram (for "customize" mode)
function undraw() {
$(".svg-pan-zoom_viewport > g").empty();
land = [], usedCells = [], riversData = [], seashore = [], manors = [], capitals = [], queue = [];
sites = d3.range(8000).map(function(d) {
return [Math.random() * width * 0.9 + width * 0.05, Math.random() * height * 0.9 + height * 0.05];
}),
voronoi = d3.voronoi().extent([[0, 0],[width, height]]),
diagram = voronoi(sites);
relax();
}
// Add big blob is center ("Island")
function island() {
var high = Math.random() * 0.2 + 0.8,
x = Math.random() * width / 4 + width / 2,
y = Math.random() * height / 8 + height * 0.45,
rnd = diagram.find(x, y);
polygons[rnd.index].high += high;
polygons[rnd.index].used = 1;
neighbours(rnd.index, high * 0.95);
for (var i = 0; i < queue.length && high > 0.01; i++) {
high = polygons[queue[i]].high * 0.9;
neighbours(queue[i], high);
};
}
// Add small blob in a random low place far from borders ("Hill"). Please change to avoid 'while' loop!
function hill(count) {
var c, i, high, rnd;
for (c = 0; c < count; c++) {
clear();
do {
rnd = Math.floor(Math.random() * polygons.length);
} while (polygons[rnd].high > 0.2 || polygons[rnd].data[0] < width * 0.2 || polygons[rnd].data[0] > width * 0.8 || polygons[rnd].data[1] < height * 0.2 || polygons[rnd].data[1] > height * 0.8)
high = Math.random() * 0.4 + 0.1;
polygons[rnd].high += high;
polygons[rnd].used = 1;
high *= 0.9;
neighbours(rnd, high);
for (i = 0; i < queue.length && high > 0.01; i++) {
// decrease High for every new set of neighbours (to get slopes)
high *= 0.99;
neighbours(queue[i], high);
}
}
}
// Get polygone neighbours and update their high with small optional modifier
function neighbours(i, high) {
polygons[i].neighbours.forEach(function(e) {
if (!polygons[e].used) {
var mod = Math.random() * sharpness + 1.1 - sharpness;
polygons[e].high += high * mod;
polygons[e].used = 1;
queue.push(e);
}
});
}
// Clear the queue. Please change with a non-global variable!
function clear() {
queue = [];
for (var i = 0; i < polygons.length; i++) {
polygons[i].used = undefined;
}
}
// Detect and draw the coasline
function drawCoastline() {
var line = "",
seashore = [],
edge, ea, oposite, i, xDiff, yDiff;
for (i = 0; i < polygons.length; i++) {
if (polygons[i].high >= 0.2) {
cell = diagram.cells[i];
cell.halfedges.forEach(function(e) {
edge = diagram.edges[e];
if (edge.left && edge.right) {
ea = edge.left.index;
if (ea === i) {
ea = edge.right.index;
}
if (polygons[ea].high < 0.2) {
line += "M" + edge.join("L");
xDiff = (edge[0][0] + edge[1][0]) / 2;
yDiff = (edge[0][1] + edge[1][1]) / 2;
// Add costline edge's centers to array to use later as a place for manors
// It will deform the graph structure so I need a way to do it
seashore.push({
cell: i,
x: xDiff,
y: yDiff
});
}
}
})
}
}
// draw the coastline
// Need help to implement function to get a single continuous line!
coastline.append("path").attr("d", line + "Z");
}
// Redraw Coastline (used for "customize" mode)
function redrawCoastline() {
$(".coastline").empty();
drawCoastline();
}
// Resolve Highmap Depressions (used for a correct water flux modeling)
function resolveDepressions() {
clear();
land = $.grep(polygons, function(e) {
return (e.high >= 0.2);
});
land.sort(compareHigh);
var depression = 1,
minCell, minHigh;
while (depression > 0) {
// 0 to resolve all the depression, its slow, but allows good rivers
depression = 0;
for (var i = 0; i < land.length; i++) {
minHigh = 10;
land[i].neighbours.forEach(function(e) {
if (polygons[e].high < minHigh) {
minHigh = polygons[e].high;
minCell = e;
}
});
if (land[i].high <= polygons[minCell].high) {
depression += 1;
land[i].high = polygons[minCell].high + 0.01;
}
}
}
land.sort(compareHigh);
flux();
}
// calculate water flux and create rivers
function flux() {
var id, oposite, edge, ea, xDiff, yDiff, riverNext = 0;
for (var i = 0; i < land.length; i++) {
var index = [],
peak = [],
pour = [],
id = land[i].id;
cell = diagram.cells[id];
cell.halfedges.forEach(function(e) {
edge = diagram.edges[e];
ea = edge.left.index;
if (ea === id || !ea) {
ea = edge.right.index;
}
if (ea) {
index.push(ea);
peak.push(polygons[ea].high);
// Define neighbour ocean cells for river Deltas
if (polygons[ea].high < 0.2) {
xDiff = (edge[0][0] + edge[1][0]) / 2;
yDiff = (edge[0][1] + edge[1][1]) / 2;
pour.push({
x: xDiff,
y: yDiff
});
}
}
})
min = peak.indexOf(Math.min(...peak));
min = index[min];
// Define river number (I need continuos lines for rivers to interpolate them as curves
if (land[i].flux > 0.03) {
if (!land[i].river) {
// State new River
land[i].river = riverNext;
riverNext += 1;
riversData.push({
river: land[i].river,
cell: id,
x: land[i].data[0],
y: land[i].data[1],
type: "source"
});
}
if ((land[i].flux > polygons[min].flux) && land[i].flux > 0.03) {
// Assing existing River to the downhill cell
polygons[min].river = land[i].river;
}
}
polygons[min].flux += land[i].flux;
if (land[i].flux > 0.03) {
if (polygons[min].high < 0.2) {
// Pour water into the Ocean
if (land[i].flux > 0.3 && pour.length > 1) {
// Pour as a River Delta
for (var c = 0; c < pour.length; c++) {
if (c == 0) {
riversData.push({
river: land[i].river,
cell: id,
x: pour[0].x,
y: pour[0].y,
type: "delta"
});
} else {
riversData.push({
river: riverNext,
cell: id,
x: land[i].data[0],
y: land[i].data[1],
type: "course"
});
riversData.push({
river: riverNext,
cell: id,
x: pour[c].x,
y: pour[c].y,
type: "delta"
});
}
riverNext += 1;
}
} else {
// Pour as a River Estuary
riversData.push({
river: land[i].river,
cell: id,
x: pour[0].x,
y: pour[0].y,
type: "estuary"
});
}
} else {
// add next River segment
riversData.push({
river: land[i].river,
cell: id,
x: polygons[min].data[0],
y: polygons[min].data[1],
type: "course"
});
}
}
}
drawRiverLines(riverNext);
}
// Draw Rivers with d3 curve interpolation
function drawRiverLines(riversCount) {
var dataRiver, x, y, line;
x = d3.scaleLinear().domain([0, width]).range([0, width]);
y = d3.scaleLinear().domain([0, height]).range([0, height]);
for (var i = 0; i < riversCount; i++) {
dataRiver = $.grep(riversData, function(e) {
return (e.river == i);
});
if (dataRiver.length > 1) {
if (dataRiver.length > 2 || dataRiver[1].type == "delta") {
line = d3.line().x(function(d) {
return x(d.x);
}).y(function(d) {
return y(d.y);
}).curve(d3.curveCatmullRom); // change interpolation type if you want
rivers.append("path").attr("d", line(dataRiver));
}
}
}
}
// Define manor places fron a sets of good points
function prepareManors() {
var rnd, mod, x, y, i, cell, type,
estuaries = $.grep(riversData, function(e) {
return (e.type == "estuary" || e.type == "delta");
}),
riverbanks = $.grep(polygons, function(e) {
return (e.flux >= 0.04 && e.high >= 0.2 && e.high < 0.6); // "high < 0.6" as I don't want manors in mountains
}),
lowlands = $.grep(polygons, function(e) {
return (e.high >= 0.23 && e.high <= 0.3);
}),
flatlands = $.grep(polygons, function(e) {
return (e.high > 0.3 && e.high < 0.6);
});
while (manors.length < manorsCount) {
rnd = Math.random();
Math.random() >= 0.5 ? mod = 0.5 : mod = -0.5; // small modifier to place manors not exactly on site
// Estuaries are the best candidate for manors; use them all
if (estuaries.length > 0) {
x = estuaries[0].x + mod / 2;
y = estuaries[0].y + mod / 2;
cell = estuaries[0].cell;
type = "estuary";
estuaries.splice(0, 1);
// Seashore is also good; use with 40% chanse
} else if (rnd > 0.6 && seashore.length > 0) {
i = Math.floor(Math.random() * seashore.length);
x = seashore[i].x + mod / 3;
y = seashore[i].y + mod / 3;
cell = seashore[i].cell;
i = seashore.indexOf(i);
type = "seashore";
seashore.splice(i, 1);
// Riverbanks are also good; use with 40% chanse
} else if (rnd > 0.2 && riverbanks.length > 0) {
i = Math.floor(Math.random() * riverbanks.length);
x = riverbanks[i].data[0] + mod;
y = riverbanks[i].data[1] + mod;
cell = riverbanks[i].id;
type = "riverbank";
i = riverbanks.indexOf(i);
riverbanks.splice(i, 1);
// Lowlands without rivers are not so good; use with 19% chanse
} else if (rnd > 0.01 && lowlands.length > 0) {
i = Math.floor(Math.random() * lowlands.length);
x = lowlands[i].data[0] + mod;
y = lowlands[i].data[1] + mod;
cell = lowlands[i].id;
type = "lowland";
i = lowlands.indexOf(i);
lowlands.splice(i, 1);
// Flatlands without rivers are not good; use with 1% chanse
} else if (flatlands.length > 0) {
i = Math.floor(Math.random() * flatlands.length);
x = flatlands[i].data[0] + mod;
y = flatlands[i].data[1] + mod;
cell = flatlands[i].id;
type = "flatlang";
i = flatlands.indexOf(i);
flatlands.splice(i, 1);
}
if (usedCells.indexOf(cell) == -1) {
usedCells.push(cell);
manors.push({
i: manors.length,
type,
x,
y
});
}
}
}
// Define capitals from the best manors rather far from each other
function defineCapitals() {
var rnd, candidates = [], dist = [], l, max, selection,
manorsSample = manors.slice(0),
sample = $.grep(manorsSample, function(e) {
return e.type == "estuary";
});
// Define the canditates count based on the capitals counts and good spots
if (capitalsCount <= sample.length / 5) {
selection = Math.floor(sample.length / capitalsCount);
} else {
sample = $.grep(manorsSample, function(e) {
return (e.type == "estuary" || e.type == "seashore" || e.type == "riverbank");
});
}
if (capitalsCount <= sample.length) {
selection = Math.floor(sample.length / capitalsCount);
} else {
alert("Too many Regions! Cannot procced.");
}
capitals[0] = sample[0];
capitals[0].power = Math.random() * power + 1;
sample.splice(0, 1);
manors[0].rang = "capital";
manors[0].capital = 0;
for (var i = 1; i < capitalsCount; i++) {
// select the futhers site from a random candidates
for (var c = 0; c < selection; c++) {
rnd = Math.floor(Math.random() * sample.length);
candidates[c] = sample[rnd];
sample.splice(rnd, 1);
for (var d = 0; d < capitals.length; d++) {
l = Math.hypot(capitals[d].x - candidates[c].x, capitals[d].y - candidates[c].y);
if (d == 0) {
dist[c] = l;
} else if (l - dist[c] < 0) {
dist[c] = l;
}
}
}
max = dist.indexOf(Math.max(...dist));
capitals[i] = candidates[max];
capitals[i].power = Math.random() * power + 1;
l = candidates[max].i;
manors[l].rang = "capital";
manors[l].capital = i;
}
}
// Append manors with random draggable names
// For each non-capital manor defect the closes capital (used for areas)
function drawManors() {
var dist = [],
min, i, c, name, x, y;
for (i = 0; i < manors.length; i++) {
name = manorNames[Math.floor(Math.random() * manorNames.length)];
x = manors[i].x;
y = manors[i].y;
if (manors[i].rang == "capital") {
burgs.append("circle").attr("r", 1).attr("cx", x).attr("cy", y).attr("class", "capital").attr("id", "b" + manors[i].i);
names.append("text").attr("x", x).attr("y", y).attr("dy", -1.4).text(name).attr("id", "n" + manors[i].i).attr("font-size", 3).call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
} else {
for (c = 0; c < capitals.length; c++) {
dist[c] = Math.hypot(capitals[c].x - x, capitals[c].y - y) / capitals[c].power;
}
min = dist.indexOf(Math.min(...dist));
manors[i].capital = min;
manors[i].rang = "manor";
burgs.append("circle").attr("r", 0.6).attr("cx", x).attr("cy", y).attr("class", manors[i].rang).attr("id", "b" + manors[i].i);
names.append("text").attr("x", x).attr("y", y).attr("dy", -0.8).text(name).attr("id", "n" + manors[i].i).attr("font-size", 1.4).call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
}
}
$('.names').hide(); // do not show names by default
}
// Define areas based on the closest manor to polygon
function defineAreas() {
var i, c, xMin, xMax, yMin, yMax;
for (i = 0; i < land.length; i++) {
var closestManors = [],
dist = [],
r = 10;
do {
xMin = land[i].data[0] - r;
xMax = land[i].data[0] + r;
yMin = land[i].data[1] - r;
yMax = land[i].data[1] + r;
closestManors = $.grep(manors, function(e) {
return (e.x >= xMin && e.x <= xMax && e.y >= yMin && e.y <= yMax);
});
r += 10;
} while (closestManors.length < 1)
for (c = 0; c < closestManors.length; c++) {
dist[c] = Math.hypot(closestManors[c].x - land[i].data[0], closestManors[c].y - land[i].data[1]);
}
min = dist.indexOf(Math.min(...dist));
land[i].capital = closestManors[min].capital;
if (Math.min(...dist) < 15) {
land[i].manor = closestManors[min].i;
}
}
}
// Define and draw borders (edges) on areas changes
// To be recoded to have continuous lines
function defineBorders() {
var line = "", id, edge, ea, i;
for (i = 0; i < land.length; i++) {
id = land[i].id;
cell = diagram.cells[id];
cell.halfedges.forEach(function(e) {
edge = diagram.edges[e];
if (edge.left && edge.right) {
ea = edge.left.index;
if (ea === i) {
ea = edge.right.index;
}
if (polygons[ea].capital != land[i].capital) {
line += "M" + edge.join("L");
}
}
})
}
borders.append("path").attr("d", line + "Z");
}
// Draw the land polygons
function drawLand() {
// use "polygons.map" to draw land and water!
land.map(function(i) {
terrs.append("path").attr("d", "M" + i.join("L") + "Z").attr("id", i.id);
});
}
// Color land polygons with its high (draw the Highmap)
function toggleHigh(id) {
if (id.getAttribute("status") == 1) {
id.setAttribute("status", 0);
// use "polygons.map" to draw land and water!
land.map(function(i) {
$("#" + i.id).attr("fill", color(1 - i.high)).attr("stroke", color(1 - i.high));
});
} else {
id.setAttribute("status", 1);
$(".terrs").children().attr("fill", "#eaf3fa").attr("stroke", "#eaf3fa");
}
}
// Draw the water flux system (for dubugging)
function toggleFlux(id) {
if (id.getAttribute("status") == 1) {
id.setAttribute("status", 0);
land.map(function(i) {
$("#" + i.id).attr("fill", colorFlux(0.1 + i.flux)).attr("stroke", colorFlux(0.1 + i.flux));
});
} else {
id.setAttribute("status", 1);
$(".terrs").children().attr("fill", "#eaf3fa").attr("stroke", "#eaf3fa");
}
}
// Draw/undraw the areas
function toggleAreas(id) {
if (id.getAttribute("status") == 1) {
id.setAttribute("status", 0);
land.map(function(i) {
$("#" + i.id).attr("fill", colors8(i.capital + 1 / capitalsCount)).attr("stroke", colors8(i.capital + 1 / capitalsCount));
});
} else {
id.setAttribute("status", 1);
$(".terrs").children().attr("fill", "#eaf3fa").attr("stroke", "#eaf3fa");
}
}
// Draw the Relief (still in progress, need to create more beautiness)
function toggleRelief(id) {
if (id.getAttribute("status") == 1) {
id.setAttribute("status", 0);
var ea, edge, id, cell, x, y, high, path, dash = "", hill = [], hShade = [], swamp = "", swampCount = 0, forest = "", fShade = "", fLight = "", swamp = "";
hill[0] = "", hill[1] = "", hShade[0] = "", hShade[1] = "";
var strokes = terrain.append("g").attr("class", "strokes"),
hills = terrain.append("g").attr("class", "hills"),
mounts = terrain.append("g").attr("class", "mounts"),
swamps = terrain.append("g").attr("class", "swamps"),
forests = terrain.append("g").attr("class", "forests");
// sort the land to Draw the top element first (reduce the elements overlapping)
land.sort(compareY);
for (i = 0; i < land.length; i++) {
x = land[i].data[0];
y = land[i].data[1];
high = land[i].high;
if (high >= 0.7 && !land[i].river) {
h = (high - 0.55) * 12;
if (high < 0.8) {
count = 2;
} else {
count = 1;
}
rnd = Math.random() * 0.8 + 0.2;
for (c = 0; c < count; c++) {
cx = x - h * 0.9 - c;
cy = y + h / 4 + c / 2;
path = "M " + cx + "," + cy + " L " + (cx + h / 3 + rnd) + "," + (cy - h / 4 - rnd * 1.2) + " L " + (cx + h / 1.1) + "," + (cy - h) + " L " + (cx + h + rnd) + "," + (cy - h / 1.2 + rnd) + " L " + (cx + h * 2) + "," + cy;
mounts.append("path").attr("d", path).attr("stroke", "#5c5c70");
path = "M " + cx + "," + cy + " L " + (cx + h / 3 + rnd) + "," + (cy - h / 4 - rnd * 1.2) + " L " + (cx + h / 1.1) + "," + (cy - h) + " L " + (cx + h / 1.5) + "," + cy;
mounts.append("path").attr("d", path).attr("fill", "#999999");
dash += "M" + (cx - 0.1) + "," + (cy + 0.3) + " L" + (cx + 2 * h + 0.1) + "," + (cy + 0.3);
}
dash += "M" + (cx + 0.4) + "," + (cy + 0.6) + " L" + (cx + 2 * h - 0.3) + "," + (cy + 0.6);
} else if (high > 0.5 && !land[i].river) {
h = (high - 0.4) * 10;
count = Math.floor(4 - h);
if (h > 1.8) {
h = 1.8
}
for (c = 0; c < count; c++) {
cx = x - h - c;
cy = y + h / 4 + c / 2;
hill[c] += "M" + cx + "," + cy + " Q" + (cx + h) + "," + (cy - h) + " " + (cx + 2 * h) + "," + cy;
hShade[c] += "M" + (cx + 0.6 * h) + "," + (cy + 0.1) + " Q" + (cx + h * 0.95) + "," + (cy - h * 0.91) + " " + (cx + 2 * h * 0.97) + "," + cy;
dash += "M" + (cx - 0.1) + "," + (cy + 0.2) + " L" + (cx + 2 * h + 0.1) + "," + (cy + 0.2);
}
dash += "M" + (cx + 0.4) + "," + (cy + 0.4) + " L" + (cx + 2 * h - 0.3) + "," + (cy + 0.4);
}
if (high >= 0.21 && high < 0.22 && !land[i].river && swampCount < swampiness && land[i].used != 1) {
swampCount++;
land[i].used = 1;
swamp += drawSwamp(x, y);
id = land[i].id, cell = diagram.cells[id];
cell.halfedges.forEach(function(e) {
edge = diagram.edges[e];
ea = edge.left.index;
if (ea === id || !ea) {
ea = edge.right.index;
}
if (polygons[ea].high >= 0.2 && polygons[ea].high < 0.3 && !polygons[ea].river && polygons[ea].used != 1) {
polygons[ea].used = 1;
swamp += drawSwamp(polygons[ea].data[0], polygons[ea].data[1]);
}
})
}
if (Math.random() < high && high >= 0.22 && high < 0.48 && !land[i].river) {
for (c = 0; c < Math.floor(high * 8); c++) {
h = 0.6;
if (c == 0) {
cx = x - h - Math.random();
cy = y - h - Math.random();
}
if (c == 1) {
cx = x + h + Math.random();
cy = y + h + Math.random();
}
if (c == 2) {
cx = x - h - Math.random();
cy = y + 2 * h + Math.random();
}
forest += "M " + cx + " " + cy + " q -1 0.8 -0.05 1.25 v 0.75 h 0.1 v -0.75 q 0.95 -0.47 -0.05 -1.25 z";
fLight += "M " + cx + " " + cy + " q -1 0.8 -0.05 1.25 h 0.1 q 0.95 -0.47 -0.05 -1.25 z";
fShade += "M " + cx + " " + cy + " q -1 0.8 -0.05 1.25 q -0.2 -0.55 0 -1.1 z";
}
}
}
// draw all that stuff
strokes.append("path").attr("d", dash);
hills.append("path").attr("d", hill[0]).attr("stroke", "#5c5c70");
hills.append("path").attr("d", hShade[0]).attr("fill", "white");
hills.append("path").attr("d", hill[1]).attr("stroke", "#5c5c70");
hills.append("path").attr("d", hShade[1]).attr("fill", "white").attr("stroke", "white");
swamps.append("path").attr("d", swamp);
forests.append("path").attr("d", forest);
forests.append("path").attr("d", fLight).attr("fill", "white").attr("stroke", "none");
forests.append("path").attr("d", fShade).attr("fill", "#999999").attr("stroke", "none");
} else {
// Delete relief if you don't need it (not just hide as I want map to be fast and clear)
id.setAttribute("status", 1);
$(".terrain").children().empty();
clear();
}
}
function compareHigh(a, b) {
if (a.high < b.high) return 1;
if (a.high > b.high) return -1;
return 0;
}
function compareY(a, b) {
if (a.data[1] > b.data[1]) return 1;
if (a.data[1] < b.data[1]) return -1;
return 0;
}
function drawSwamp(x, y) {
var h = 0.6, line = "";
for (c = 0; c < 3; c++) {
if (c == 0) {
cx = x;
cy = y - 0.5 - Math.random();
}
if (c == 1) {
cx = x + h + Math.random();
cy = y + h + Math.random();
}
if (c == 2) {
cx = x - h - Math.random();
cy = y + 2 * h + Math.random();
}
line += "M" + cx + "," + cy + " H" + (cx - h / 6) + " M" + cx + "," + cy + " H" + (cx + h / 6) + " M" + cx + "," + cy + " L" + (cx - h / 3) + "," + (cy - h / 2) + " M" + cx + "," + cy + " V" + (cy - h / 1.5) + " M" + cx + "," + cy + " L" + (cx + h / 3) + "," + (cy - h / 2);
line += "M" + (cx - h) + "," + cy + " H" + (cx - h / 2) + " M" + (cx + h / 2) + "," + cy + " H" + (cx + h);
}
return line;
}
// Toggle burg names on click, allow burgs dragging
$(".manor, .capital").click(function() {
$("#n" + this.id.slice(1)).fadeToggle();
});
function dragstarted(e) {
d3.select(this).raise().classed("active", true);
}
function dragged(e) {
d3.select(this).attr("x", d3.event.x).attr("y", d3.event.y + 0.8);
}
function dragended(d) {
d3.select(this).classed("active", false);
}
// Complete the map for the "customize" mode
function getMap() {
resolveDepressions();
drawLand();
toggleHigh(highmap);
prepareManors();
defineCapitals();
drawManors();
defineAreas();
defineBorders();
}
// Change high of all polygons by modifier
function rescale(scale) {
for (var i = 0; i < polygons.length; i++) {
polygons[i].high *= scale;
}
drawCoastline();
}
</script>
@sbryfcz
Copy link

sbryfcz commented Mar 22, 2017

Since you asked for comments (as a first timer), I'd suggest looking into ways to format your JS for easier readability. Maybe look at jslint or jsbeautify. Sorry but I'd love to read the code but its difficult to read with the current spacing/formatting.

Very cool work! Nice job.

@Azgaar
Copy link
Author

Azgaar commented Mar 23, 2017

Hi @sbryfcz.
Thank you! Sure, I will do some formatting. I understand the code is unreadable and not neat.

P,S. Done, code a bit formatted now. I've also added comments on how it works. Please go ahead with such a good suggestions!

@ikarth
Copy link

ikarth commented Mar 23, 2017

Suggestion for feature expansion: since you have talked about wanting to expand this into a dynasty simulator, you might want to look at using something like Tracery for text generation (which I'm guessing you're planning to do quite a lot of). It's already a Javascript library, so you can just import it and it'll simplify name generation and things like that.

@Azgaar
Copy link
Author

Azgaar commented Mar 23, 2017

Hi @ikarth! Thank you, I will think regarding using this library.

@XCJT
Copy link

XCJT commented Jun 27, 2017

Awesome work!

To change it to d3 pan/zoom you just need to do the following:

  // Fantasy Map Generator main script
  var svg = d3.select("svg"),
    g = svg.append("g"),
    terrs = g.append("g").attr("class", "terrs").on("touchmove mousemove", moved),
    areas = g.append("g").attr("class", "areas"),
    borders = g.append("g").attr("class", "borders"),
    rivers = g.append("g").attr("class", "rivers"),
    coastline = g.append("g").attr("class", "coastline"),
    terrain = g.append("g").attr("class", "terrain"), 
    names = g.append("g").attr("class", "names"),
    burgs = g.append("g").attr("class", "burgs");
  
  svg.call(d3.zoom()
    .scaleExtent([1 / 2, 4])
    .on("zoom", zoomed));
    
  console.log(g.attr("width"))
    
  function zoomed() {
    g.attr("transform", d3.event.transform);
  }
  
  generate(); // genarate map on load

@Saint-Ajora
Copy link

Saint-Ajora commented Jul 4, 2017

Firstly let me say this is one of the greatest things I have ever seen; I have been looking for this for years now. Secondly, I have literally zero experience in coding of any kind but I would like to know if there is a way to either save the image generated by your generator or a way to get the generator to make the same map again and again. For now I have been taking screenshots and matching up the edges in Paint.net (with some success) . I have tried clicking on the "Get Map" button and that seems to just change the island either black or white and draw lines (or show lines) from each manor/town/city.

@Azgaar
Copy link
Author

Azgaar commented Jul 18, 2017

Hi @XCJT. Thank you, zooming is already on D3, just not added to this old version. There are some new cool features developed but not yet deployed, see my blog and jsfiddle. I want to create a stable version and update this page or move to another host as blocks/gist are not really good for that kind of demo. Could you suggest a good platform for a working generator?

@Azgaar
Copy link
Author

Azgaar commented Jul 18, 2017

Hi @Saint-Ajora. Thank you for the feedback! "Get Map" is to complete the custom map, not to download it, sorry for the misleading labeling. I have developed download function (in svg), just not deployed it to this demo version as working on rivers, biomes and UI. I will try to update the page by the end of this week.

@Azgaar
Copy link
Author

Azgaar commented Jul 22, 2017

Hi All,
I've added download button ("save in SVG") and changed zooming to D3 built-in function.
Hope you will enjoy the update.

@dhbahr
Copy link

dhbahr commented Jul 22, 2017

Hi there @Azgaar, let me congratulate you on a really nice job, I discovered this today and I'm already looking forward to the new features.
1 issue though: when I download the image as SVG the Toogle Highmap, Area and Flux stop working, so I can only download 1 version of the map..
Best

@Azgaar
Copy link
Author

Azgaar commented Jul 23, 2017

Hi @dhbahr. Thank you for the bug report! I'll try to fix it. Looks weird.

@Azgaar
Copy link
Author

Azgaar commented Jul 23, 2017

Hi @dhbahr. The issue is fixed now. The root cause is that d3-save-svg library adds computed style attribute to all the elements. So I have to removed style after downloading. Not ideal, but I don't want to re-write d3-save-svg library, so it's OK as still works pretty fast. Please re-test :)

@Azgaar
Copy link
Author

Azgaar commented Aug 24, 2017

New changes are deployed.

@Azgaar
Copy link
Author

Azgaar commented Oct 15, 2017

Hi Guys. Generator is updated and now is more usable, even the project is still in progress and this is a demo version. Please use the main project GitHub page for bug reports or suggestions: https://github.com/Azgaar/Fantasy-Map-Generator/issues

And please let me know if you need manual on how to use the Generator (there are quite a lot of unobvious options / possibilities).

@Azgaar
Copy link
Author

Azgaar commented Mar 29, 2018

The Generator demo is moved to https://azgaar.github.io/Fantasy-Map-Generator

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment