|
<!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> |