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