Skip to content

Instantly share code, notes, and snippets.

@joshcarr
Forked from Azgaar/.block
Created April 19, 2017 18:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joshcarr/0cbfc84782569c873af4715d05084b3d to your computer and use it in GitHub Desktop.
Save joshcarr/0cbfc84782569c873af4715d05084b3d 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 islands.

Project goal is a procedurally generated map for my Medieval Dinasty simulator. Map should be interactive, fast and plausible. There should be enought place to locate at least 500 manors within 7 cultural areas. The imagined area is about 200.000 km2.

This version is a bit outdated, I'll deploy improved one soon. Details are covered in my blog Fantasy Maps for fun and glory. Comments, change requests, ideas, spelling and code error corrections are highly welcomed.

Inspiration:

TO-DO:

  • Add Mimimap like the Bill White's one (planned)
  • Use different namesets for areas (to be deployed)
  • Use Markov's chaines for manes (to be deployed)
  • Different trees types (planned)
  • Resize relief signs and names on map rescale (planned)
@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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment