├── LICENSE.md ├── README.md └── terrain.js /LICENSE.md: -------------------------------------------------------------------------------- 1 | This code is licensed under the MIT License: 2 | 3 | > Copyright (c) 2016: Martin O'Leary 4 | > 5 | > Permission is hereby granted, free of charge, to any person obtaining 6 | > a copy of this software and associated documentation files (the 7 | > "Software"), to deal in the Software without restriction, including 8 | > without limitation the rights to use, copy, modify, merge, publish, 9 | > distribute, sublicense, and/or sell copies of the Software, and to 10 | > permit persons to whom the Software is furnished to do so, subject to 11 | > the following conditions: 12 | > 13 | > The above copyright notice and this permission notice shall be 14 | > included in all copies or substantial portions of the Software. 15 | > 16 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | > NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | > LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | > OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | > WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fantasy map generator 2 | 3 | This is code for generating fantasy maps, using the algorithm behind [@unchartedatlas][uncharted]. For more details, see [these notes][notes]. 4 | 5 | ## Dependencies 6 | 7 | This code depends on the following: 8 | 9 | * [D3.js][d3] (tested with version 4.2.0) 10 | * Adam Hooper's [js-priority-queue][priority] 11 | * My [language generation code][language] 12 | 13 | ## Support, licensing, ongoing development 14 | 15 | This project is, from my perspective, finished. 16 | 17 | The code is available under the [MIT license][license], so you can fork it, 18 | improve it, learn from it, build upon it. However, I have no interest in 19 | maintaining it as an ongoing open source project, nor in providing support for 20 | it. Pull requests will be either ignored or closed. 21 | 22 | If you do make something interesting with this code, please do still let me know! I'm sorry that I can't provide any support, but I am still genuinely interested in seeing creative applications of the code. 23 | 24 | [uncharted]: https://twitter.com/unchartedatlas 25 | [notes]: https://mewo2.com/notes/terrain/ 26 | [language]: https://github.com/mewo2/naming-language/ 27 | [priority]: https://github.com/adamhooper/js-priority-queue 28 | [d3]: https://d3js.org/ 29 | [license]: https://github.com/mewo2/terrain/blob/master/LICENSE.md 30 | -------------------------------------------------------------------------------- /terrain.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function runif(lo, hi) { 4 | return lo + Math.random() * (hi - lo); 5 | } 6 | 7 | var rnorm = (function () { 8 | var z2 = null; 9 | function rnorm() { 10 | if (z2 != null) { 11 | var tmp = z2; 12 | z2 = null; 13 | return tmp; 14 | } 15 | var x1 = 0; 16 | var x2 = 0; 17 | var w = 2.0; 18 | while (w >= 1) { 19 | x1 = runif(-1, 1); 20 | x2 = runif(-1, 1); 21 | w = x1 * x1 + x2 * x2; 22 | } 23 | w = Math.sqrt(-2 * Math.log(w) / w); 24 | z2 = x2 * w; 25 | return x1 * w; 26 | } 27 | return rnorm; 28 | })(); 29 | 30 | function randomVector(scale) { 31 | return [scale * rnorm(), scale * rnorm()]; 32 | } 33 | 34 | var defaultExtent = { 35 | width: 1, 36 | height: 1 37 | }; 38 | 39 | function generatePoints(n, extent) { 40 | extent = extent || defaultExtent; 41 | var pts = []; 42 | for (var i = 0; i < n; i++) { 43 | pts.push([(Math.random() - 0.5) * extent.width, (Math.random() - 0.5) * extent.height]); 44 | } 45 | return pts; 46 | } 47 | 48 | function centroid(pts) { 49 | var x = 0; 50 | var y = 0; 51 | for (var i = 0; i < pts.length; i++) { 52 | x += pts[i][0]; 53 | y += pts[i][1]; 54 | } 55 | return [x/pts.length, y/pts.length]; 56 | } 57 | 58 | function improvePoints(pts, n, extent) { 59 | n = n || 1; 60 | extent = extent || defaultExtent; 61 | for (var i = 0; i < n; i++) { 62 | pts = voronoi(pts, extent) 63 | .polygons(pts) 64 | .map(centroid); 65 | } 66 | return pts; 67 | } 68 | 69 | function generateGoodPoints(n, extent) { 70 | extent = extent || defaultExtent; 71 | var pts = generatePoints(n, extent); 72 | pts = pts.sort(function (a, b) { 73 | return a[0] - b[0]; 74 | }); 75 | return improvePoints(pts, 1, extent); 76 | } 77 | 78 | function voronoi(pts, extent) { 79 | extent = extent || defaultExtent; 80 | var w = extent.width/2; 81 | var h = extent.height/2; 82 | return d3.voronoi().extent([[-w, -h], [w, h]])(pts); 83 | } 84 | 85 | function makeMesh(pts, extent) { 86 | extent = extent || defaultExtent; 87 | var vor = voronoi(pts, extent); 88 | var vxs = []; 89 | var vxids = {}; 90 | var adj = []; 91 | var edges = []; 92 | var tris = []; 93 | for (var i = 0; i < vor.edges.length; i++) { 94 | var e = vor.edges[i]; 95 | if (e == undefined) continue; 96 | var e0 = vxids[e[0]]; 97 | var e1 = vxids[e[1]]; 98 | if (e0 == undefined) { 99 | e0 = vxs.length; 100 | vxids[e[0]] = e0; 101 | vxs.push(e[0]); 102 | } 103 | if (e1 == undefined) { 104 | e1 = vxs.length; 105 | vxids[e[1]] = e1; 106 | vxs.push(e[1]); 107 | } 108 | adj[e0] = adj[e0] || []; 109 | adj[e0].push(e1); 110 | adj[e1] = adj[e1] || []; 111 | adj[e1].push(e0); 112 | edges.push([e0, e1, e.left, e.right]); 113 | tris[e0] = tris[e0] || []; 114 | if (!tris[e0].includes(e.left)) tris[e0].push(e.left); 115 | if (e.right && !tris[e0].includes(e.right)) tris[e0].push(e.right); 116 | tris[e1] = tris[e1] || []; 117 | if (!tris[e1].includes(e.left)) tris[e1].push(e.left); 118 | if (e.right && !tris[e1].includes(e.right)) tris[e1].push(e.right); 119 | } 120 | 121 | var mesh = { 122 | pts: pts, 123 | vor: vor, 124 | vxs: vxs, 125 | adj: adj, 126 | tris: tris, 127 | edges: edges, 128 | extent: extent 129 | } 130 | mesh.map = function (f) { 131 | var mapped = vxs.map(f); 132 | mapped.mesh = mesh; 133 | return mapped; 134 | } 135 | return mesh; 136 | } 137 | 138 | 139 | 140 | function generateGoodMesh(n, extent) { 141 | extent = extent || defaultExtent; 142 | var pts = generateGoodPoints(n, extent); 143 | return makeMesh(pts, extent); 144 | } 145 | function isedge(mesh, i) { 146 | return (mesh.adj[i].length < 3); 147 | } 148 | 149 | function isnearedge(mesh, i) { 150 | var x = mesh.vxs[i][0]; 151 | var y = mesh.vxs[i][1]; 152 | var w = mesh.extent.width; 153 | var h = mesh.extent.height; 154 | return x < -0.45 * w || x > 0.45 * w || y < -0.45 * h || y > 0.45 * h; 155 | } 156 | 157 | function neighbours(mesh, i) { 158 | var onbs = mesh.adj[i]; 159 | var nbs = []; 160 | for (var i = 0; i < onbs.length; i++) { 161 | nbs.push(onbs[i]); 162 | } 163 | return nbs; 164 | } 165 | 166 | function distance(mesh, i, j) { 167 | var p = mesh.vxs[i]; 168 | var q = mesh.vxs[j]; 169 | return Math.sqrt((p[0] - q[0]) * (p[0] - q[0]) + (p[1] - q[1]) * (p[1] - q[1])); 170 | } 171 | 172 | function quantile(h, q) { 173 | var sortedh = []; 174 | for (var i = 0; i < h.length; i++) { 175 | sortedh[i] = h[i]; 176 | } 177 | sortedh.sort(d3.ascending); 178 | return d3.quantile(sortedh, q); 179 | } 180 | 181 | function zero(mesh) { 182 | var z = []; 183 | for (var i = 0; i < mesh.vxs.length; i++) { 184 | z[i] = 0; 185 | } 186 | z.mesh = mesh; 187 | return z; 188 | } 189 | 190 | function slope(mesh, direction) { 191 | return mesh.map(function (x) { 192 | return x[0] * direction[0] + x[1] * direction[1]; 193 | }); 194 | } 195 | 196 | function cone(mesh, slope) { 197 | return mesh.map(function (x) { 198 | return Math.pow(x[0] * x[0] + x[1] * x[1], 0.5) * slope; 199 | }); 200 | } 201 | 202 | function map(h, f) { 203 | var newh = h.map(f); 204 | newh.mesh = h.mesh; 205 | return newh; 206 | } 207 | 208 | function normalize(h) { 209 | var lo = d3.min(h); 210 | var hi = d3.max(h); 211 | return map(h, function (x) {return (x - lo) / (hi - lo)}); 212 | } 213 | 214 | function peaky(h) { 215 | return map(normalize(h), Math.sqrt); 216 | } 217 | 218 | function add() { 219 | var n = arguments[0].length; 220 | var newvals = zero(arguments[0].mesh); 221 | for (var i = 0; i < n; i++) { 222 | for (var j = 0; j < arguments.length; j++) { 223 | newvals[i] += arguments[j][i]; 224 | } 225 | } 226 | return newvals; 227 | } 228 | 229 | function mountains(mesh, n, r) { 230 | r = r || 0.05; 231 | var mounts = []; 232 | for (var i = 0; i < n; i++) { 233 | mounts.push([mesh.extent.width * (Math.random() - 0.5), mesh.extent.height * (Math.random() - 0.5)]); 234 | } 235 | var newvals = zero(mesh); 236 | for (var i = 0; i < mesh.vxs.length; i++) { 237 | var p = mesh.vxs[i]; 238 | for (var j = 0; j < n; j++) { 239 | var m = mounts[j]; 240 | newvals[i] += Math.pow(Math.exp(-((p[0] - m[0]) * (p[0] - m[0]) + (p[1] - m[1]) * (p[1] - m[1])) / (2 * r * r)), 2); 241 | } 242 | } 243 | return newvals; 244 | } 245 | 246 | function relax(h) { 247 | var newh = zero(h.mesh); 248 | for (var i = 0; i < h.length; i++) { 249 | var nbs = neighbours(h.mesh, i); 250 | if (nbs.length < 3) { 251 | newh[i] = 0; 252 | continue; 253 | } 254 | newh[i] = d3.mean(nbs.map(function (j) {return h[j]})); 255 | } 256 | return newh; 257 | } 258 | 259 | function downhill(h) { 260 | if (h.downhill) return h.downhill; 261 | function downfrom(i) { 262 | if (isedge(h.mesh, i)) return -2; 263 | var best = -1; 264 | var besth = h[i]; 265 | var nbs = neighbours(h.mesh, i); 266 | for (var j = 0; j < nbs.length; j++) { 267 | if (h[nbs[j]] < besth) { 268 | besth = h[nbs[j]]; 269 | best = nbs[j]; 270 | } 271 | } 272 | return best; 273 | } 274 | var downs = []; 275 | for (var i = 0; i < h.length; i++) { 276 | downs[i] = downfrom(i); 277 | } 278 | h.downhill = downs; 279 | return downs; 280 | } 281 | 282 | function findSinks(h) { 283 | var dh = downhill(h); 284 | var sinks = []; 285 | for (var i = 0; i < dh.length; i++) { 286 | var node = i; 287 | while (true) { 288 | if (isedge(h.mesh, node)) { 289 | sinks[i] = -2; 290 | break; 291 | } 292 | if (dh[node] == -1) { 293 | sinks[i] = node; 294 | break; 295 | } 296 | node = dh[node]; 297 | } 298 | } 299 | } 300 | 301 | function fillSinks(h, epsilon) { 302 | epsilon = epsilon || 1e-5; 303 | var infinity = 999999; 304 | var newh = zero(h.mesh); 305 | for (var i = 0; i < h.length; i++) { 306 | if (isnearedge(h.mesh, i)) { 307 | newh[i] = h[i]; 308 | } else { 309 | newh[i] = infinity; 310 | } 311 | } 312 | while (true) { 313 | var changed = false; 314 | for (var i = 0; i < h.length; i++) { 315 | if (newh[i] == h[i]) continue; 316 | var nbs = neighbours(h.mesh, i); 317 | for (var j = 0; j < nbs.length; j++) { 318 | if (h[i] >= newh[nbs[j]] + epsilon) { 319 | newh[i] = h[i]; 320 | changed = true; 321 | break; 322 | } 323 | var oh = newh[nbs[j]] + epsilon; 324 | if ((newh[i] > oh) && (oh > h[i])) { 325 | newh[i] = oh; 326 | changed = true; 327 | } 328 | } 329 | } 330 | if (!changed) return newh; 331 | } 332 | } 333 | 334 | function getFlux(h) { 335 | var dh = downhill(h); 336 | var idxs = []; 337 | var flux = zero(h.mesh); 338 | for (var i = 0; i < h.length; i++) { 339 | idxs[i] = i; 340 | flux[i] = 1/h.length; 341 | } 342 | idxs.sort(function (a, b) { 343 | return h[b] - h[a]; 344 | }); 345 | for (var i = 0; i < h.length; i++) { 346 | var j = idxs[i]; 347 | if (dh[j] >= 0) { 348 | flux[dh[j]] += flux[j]; 349 | } 350 | } 351 | return flux; 352 | } 353 | 354 | function getSlope(h) { 355 | var dh = downhill(h); 356 | var slope = zero(h.mesh); 357 | for (var i = 0; i < h.length; i++) { 358 | var s = trislope(h, i); 359 | slope[i] = Math.sqrt(s[0] * s[0] + s[1] * s[1]); 360 | continue; 361 | if (dh[i] < 0) { 362 | slope[i] = 0; 363 | } else { 364 | slope[i] = (h[i] - h[dh[i]]) / distance(h.mesh, i, dh[i]); 365 | } 366 | } 367 | return slope; 368 | } 369 | 370 | function erosionRate(h) { 371 | var flux = getFlux(h); 372 | var slope = getSlope(h); 373 | var newh = zero(h.mesh); 374 | for (var i = 0; i < h.length; i++) { 375 | var river = Math.sqrt(flux[i]) * slope[i]; 376 | var creep = slope[i] * slope[i]; 377 | var total = 1000 * river + creep; 378 | total = total > 200 ? 200 : total; 379 | newh[i] = total; 380 | } 381 | return newh; 382 | } 383 | 384 | function erode(h, amount) { 385 | var er = erosionRate(h); 386 | var newh = zero(h.mesh); 387 | var maxr = d3.max(er); 388 | for (var i = 0; i < h.length; i++) { 389 | newh[i] = h[i] - amount * (er[i] / maxr); 390 | } 391 | return newh; 392 | } 393 | 394 | function doErosion(h, amount, n) { 395 | n = n || 1; 396 | h = fillSinks(h); 397 | for (var i = 0; i < n; i++) { 398 | h = erode(h, amount); 399 | h = fillSinks(h); 400 | } 401 | return h; 402 | } 403 | 404 | function setSeaLevel(h, q) { 405 | var newh = zero(h.mesh); 406 | var delta = quantile(h, q); 407 | for (var i = 0; i < h.length; i++) { 408 | newh[i] = h[i] - delta; 409 | } 410 | return newh; 411 | } 412 | 413 | function cleanCoast(h, iters) { 414 | for (var iter = 0; iter < iters; iter++) { 415 | var changed = 0; 416 | var newh = zero(h.mesh); 417 | for (var i = 0; i < h.length; i++) { 418 | newh[i] = h[i]; 419 | var nbs = neighbours(h.mesh, i); 420 | if (h[i] <= 0 || nbs.length != 3) continue; 421 | var count = 0; 422 | var best = -999999; 423 | for (var j = 0; j < nbs.length; j++) { 424 | if (h[nbs[j]] > 0) { 425 | count++; 426 | } else if (h[nbs[j]] > best) { 427 | best = h[nbs[j]]; 428 | } 429 | } 430 | if (count > 1) continue; 431 | newh[i] = best / 2; 432 | changed++; 433 | } 434 | h = newh; 435 | newh = zero(h.mesh); 436 | for (var i = 0; i < h.length; i++) { 437 | newh[i] = h[i]; 438 | var nbs = neighbours(h.mesh, i); 439 | if (h[i] > 0 || nbs.length != 3) continue; 440 | var count = 0; 441 | var best = 999999; 442 | for (var j = 0; j < nbs.length; j++) { 443 | if (h[nbs[j]] <= 0) { 444 | count++; 445 | } else if (h[nbs[j]] < best) { 446 | best = h[nbs[j]]; 447 | } 448 | } 449 | if (count > 1) continue; 450 | newh[i] = best / 2; 451 | changed++; 452 | } 453 | h = newh; 454 | } 455 | return h; 456 | } 457 | 458 | function trislope(h, i) { 459 | var nbs = neighbours(h.mesh, i); 460 | if (nbs.length != 3) return [0,0]; 461 | var p0 = h.mesh.vxs[nbs[0]]; 462 | var p1 = h.mesh.vxs[nbs[1]]; 463 | var p2 = h.mesh.vxs[nbs[2]]; 464 | 465 | var x1 = p1[0] - p0[0]; 466 | var x2 = p2[0] - p0[0]; 467 | var y1 = p1[1] - p0[1]; 468 | var y2 = p2[1] - p0[1]; 469 | 470 | var det = x1 * y2 - x2 * y1; 471 | var h1 = h[nbs[1]] - h[nbs[0]]; 472 | var h2 = h[nbs[2]] - h[nbs[0]]; 473 | 474 | return [(y2 * h1 - y1 * h2) / det, 475 | (-x2 * h1 + x1 * h2) / det]; 476 | } 477 | 478 | function cityScore(h, cities) { 479 | var score = map(getFlux(h), Math.sqrt); 480 | for (var i = 0; i < h.length; i++) { 481 | if (h[i] <= 0 || isnearedge(h.mesh, i)) { 482 | score[i] = -999999; 483 | continue; 484 | } 485 | score[i] += 0.01 / (1e-9 + Math.abs(h.mesh.vxs[i][0]) - h.mesh.extent.width/2) 486 | score[i] += 0.01 / (1e-9 + Math.abs(h.mesh.vxs[i][1]) - h.mesh.extent.height/2) 487 | for (var j = 0; j < cities.length; j++) { 488 | score[i] -= 0.02 / (distance(h.mesh, cities[j], i) + 1e-9); 489 | } 490 | } 491 | return score; 492 | } 493 | function placeCity(render) { 494 | render.cities = render.cities || []; 495 | var score = cityScore(render.h, render.cities); 496 | var newcity = d3.scan(score, d3.descending); 497 | render.cities.push(newcity); 498 | } 499 | 500 | function placeCities(render) { 501 | var params = render.params; 502 | var h = render.h; 503 | var n = params.ncities; 504 | for (var i = 0; i < n; i++) { 505 | placeCity(render); 506 | } 507 | } 508 | 509 | function contour(h, level) { 510 | level = level || 0; 511 | var edges = []; 512 | for (var i = 0; i < h.mesh.edges.length; i++) { 513 | var e = h.mesh.edges[i]; 514 | if (e[3] == undefined) continue; 515 | if (isnearedge(h.mesh, e[0]) || isnearedge(h.mesh, e[1])) continue; 516 | if ((h[e[0]] > level && h[e[1]] <= level) || 517 | (h[e[1]] > level && h[e[0]] <= level)) { 518 | edges.push([e[2], e[3]]); 519 | } 520 | } 521 | return mergeSegments(edges); 522 | } 523 | 524 | function getRivers(h, limit) { 525 | var dh = downhill(h); 526 | var flux = getFlux(h); 527 | var links = []; 528 | var above = 0; 529 | for (var i = 0; i < h.length; i++) { 530 | if (h[i] > 0) above++; 531 | } 532 | limit *= above / h.length; 533 | for (var i = 0; i < dh.length; i++) { 534 | if (isnearedge(h.mesh, i)) continue; 535 | if (flux[i] > limit && h[i] > 0 && dh[i] >= 0) { 536 | var up = h.mesh.vxs[i]; 537 | var down = h.mesh.vxs[dh[i]]; 538 | if (h[dh[i]] > 0) { 539 | links.push([up, down]); 540 | } else { 541 | links.push([up, [(up[0] + down[0])/2, (up[1] + down[1])/2]]); 542 | } 543 | } 544 | } 545 | return mergeSegments(links).map(relaxPath); 546 | } 547 | 548 | function getTerritories(render) { 549 | var h = render.h; 550 | var cities = render.cities; 551 | var n = render.params.nterrs; 552 | if (n > render.cities.length) n = render.cities.length; 553 | var flux = getFlux(h); 554 | var terr = []; 555 | var queue = new PriorityQueue({comparator: function (a, b) {return a.score - b.score}}); 556 | function weight(u, v) { 557 | var horiz = distance(h.mesh, u, v); 558 | var vert = h[v] - h[u]; 559 | if (vert > 0) vert /= 10; 560 | var diff = 1 + 0.25 * Math.pow(vert/horiz, 2); 561 | diff += 100 * Math.sqrt(flux[u]); 562 | if (h[u] <= 0) diff = 100; 563 | if ((h[u] > 0) != (h[v] > 0)) return 1000; 564 | return horiz * diff; 565 | } 566 | for (var i = 0; i < n; i++) { 567 | terr[cities[i]] = cities[i]; 568 | var nbs = neighbours(h.mesh, cities[i]); 569 | for (var j = 0; j < nbs.length; j++) { 570 | queue.queue({ 571 | score: weight(cities[i], nbs[j]), 572 | city: cities[i], 573 | vx: nbs[j] 574 | }); 575 | } 576 | } 577 | while (queue.length) { 578 | var u = queue.dequeue(); 579 | if (terr[u.vx] != undefined) continue; 580 | terr[u.vx] = u.city; 581 | var nbs = neighbours(h.mesh, u.vx); 582 | for (var i = 0; i < nbs.length; i++) { 583 | var v = nbs[i]; 584 | if (terr[v] != undefined) continue; 585 | var newdist = weight(u.vx, v); 586 | queue.queue({ 587 | score: u.score + newdist, 588 | city: u.city, 589 | vx: v 590 | }); 591 | } 592 | } 593 | terr.mesh = h.mesh; 594 | return terr; 595 | } 596 | 597 | function getBorders(render) { 598 | var terr = render.terr; 599 | var h = render.h; 600 | var edges = []; 601 | for (var i = 0; i < terr.mesh.edges.length; i++) { 602 | var e = terr.mesh.edges[i]; 603 | if (e[3] == undefined) continue; 604 | if (isnearedge(terr.mesh, e[0]) || isnearedge(terr.mesh, e[1])) continue; 605 | if (h[e[0]] < 0 || h[e[1]] < 0) continue; 606 | if (terr[e[0]] != terr[e[1]]) { 607 | edges.push([e[2], e[3]]); 608 | } 609 | } 610 | return mergeSegments(edges).map(relaxPath); 611 | } 612 | 613 | function mergeSegments(segs) { 614 | var adj = {}; 615 | for (var i = 0; i < segs.length; i++) { 616 | var seg = segs[i]; 617 | var a0 = adj[seg[0]] || []; 618 | var a1 = adj[seg[1]] || []; 619 | a0.push(seg[1]); 620 | a1.push(seg[0]); 621 | adj[seg[0]] = a0; 622 | adj[seg[1]] = a1; 623 | } 624 | var done = []; 625 | var paths = []; 626 | var path = null; 627 | while (true) { 628 | if (path == null) { 629 | for (var i = 0; i < segs.length; i++) { 630 | if (done[i]) continue; 631 | done[i] = true; 632 | path = [segs[i][0], segs[i][1]]; 633 | break; 634 | } 635 | if (path == null) break; 636 | } 637 | var changed = false; 638 | for (var i = 0; i < segs.length; i++) { 639 | if (done[i]) continue; 640 | if (adj[path[0]].length == 2 && segs[i][0] == path[0]) { 641 | path.unshift(segs[i][1]); 642 | } else if (adj[path[0]].length == 2 && segs[i][1] == path[0]) { 643 | path.unshift(segs[i][0]); 644 | } else if (adj[path[path.length - 1]].length == 2 && segs[i][0] == path[path.length - 1]) { 645 | path.push(segs[i][1]); 646 | } else if (adj[path[path.length - 1]].length == 2 && segs[i][1] == path[path.length - 1]) { 647 | path.push(segs[i][0]); 648 | } else { 649 | continue; 650 | } 651 | done[i] = true; 652 | changed = true; 653 | break; 654 | } 655 | if (!changed) { 656 | paths.push(path); 657 | path = null; 658 | } 659 | } 660 | return paths; 661 | } 662 | 663 | function relaxPath(path) { 664 | var newpath = [path[0]]; 665 | for (var i = 1; i < path.length - 1; i++) { 666 | var newpt = [0.25 * path[i-1][0] + 0.5 * path[i][0] + 0.25 * path[i+1][0], 667 | 0.25 * path[i-1][1] + 0.5 * path[i][1] + 0.25 * path[i+1][1]]; 668 | newpath.push(newpt); 669 | } 670 | newpath.push(path[path.length - 1]); 671 | return newpath; 672 | } 673 | function visualizePoints(svg, pts) { 674 | var circle = svg.selectAll('circle').data(pts); 675 | circle.enter() 676 | .append('circle'); 677 | circle.exit().remove(); 678 | d3.selectAll('circle') 679 | .attr('cx', function (d) {return 1000*d[0]}) 680 | .attr('cy', function (d) {return 1000*d[1]}) 681 | .attr('r', 100 / Math.sqrt(pts.length)); 682 | } 683 | 684 | function makeD3Path(path) { 685 | var p = d3.path(); 686 | p.moveTo(1000*path[0][0], 1000*path[0][1]); 687 | for (var i = 1; i < path.length; i++) { 688 | p.lineTo(1000*path[i][0], 1000*path[i][1]); 689 | } 690 | return p.toString(); 691 | } 692 | 693 | function visualizeVoronoi(svg, field, lo, hi) { 694 | if (hi == undefined) hi = d3.max(field) + 1e-9; 695 | if (lo == undefined) lo = d3.min(field) - 1e-9; 696 | var mappedvals = field.map(function (x) {return x > hi ? 1 : x < lo ? 0 : (x - lo) / (hi - lo)}); 697 | var tris = svg.selectAll('path.field').data(field.mesh.tris) 698 | tris.enter() 699 | .append('path') 700 | .classed('field', true); 701 | 702 | tris.exit() 703 | .remove(); 704 | 705 | svg.selectAll('path.field') 706 | .attr('d', makeD3Path) 707 | .style('fill', function (d, i) { 708 | return d3.interpolateViridis(mappedvals[i]); 709 | }); 710 | } 711 | 712 | function visualizeDownhill(h) { 713 | var links = getRivers(h, 0.01); 714 | drawPaths('river', links); 715 | } 716 | 717 | function drawPaths(svg, cls, paths) { 718 | var paths = svg.selectAll('path.' + cls).data(paths) 719 | paths.enter() 720 | .append('path') 721 | .classed(cls, true) 722 | paths.exit() 723 | .remove(); 724 | svg.selectAll('path.' + cls) 725 | .attr('d', makeD3Path); 726 | } 727 | 728 | function visualizeSlopes(svg, render) { 729 | var h = render.h; 730 | var strokes = []; 731 | var r = 0.25 / Math.sqrt(h.length); 732 | for (var i = 0; i < h.length; i++) { 733 | if (h[i] <= 0 || isnearedge(h.mesh, i)) continue; 734 | var nbs = neighbours(h.mesh, i); 735 | nbs.push(i); 736 | var s = 0; 737 | var s2 = 0; 738 | for (var j = 0; j < nbs.length; j++) { 739 | var slopes = trislope(h, nbs[j]); 740 | s += slopes[0] / 10; 741 | s2 += slopes[1]; 742 | } 743 | s /= nbs.length; 744 | s2 /= nbs.length; 745 | if (Math.abs(s) < runif(0.1, 0.4)) continue; 746 | var l = r * runif(1, 2) * (1 - 0.2 * Math.pow(Math.atan(s), 2)) * Math.exp(s2/100); 747 | var x = h.mesh.vxs[i][0]; 748 | var y = h.mesh.vxs[i][1]; 749 | if (Math.abs(l*s) > 2 * r) { 750 | var n = Math.floor(Math.abs(l*s/r)); 751 | l /= n; 752 | if (n > 4) n = 4; 753 | for (var j = 0; j < n; j++) { 754 | var u = rnorm() * r; 755 | var v = rnorm() * r; 756 | strokes.push([[x+u-l, y+v+l*s], [x+u+l, y+v-l*s]]); 757 | } 758 | } else { 759 | strokes.push([[x-l, y+l*s], [x+l, y-l*s]]); 760 | } 761 | } 762 | var lines = svg.selectAll('line.slope').data(strokes) 763 | lines.enter() 764 | .append('line') 765 | .classed('slope', true); 766 | lines.exit() 767 | .remove(); 768 | svg.selectAll('line.slope') 769 | .attr('x1', function (d) {return 1000*d[0][0]}) 770 | .attr('y1', function (d) {return 1000*d[0][1]}) 771 | .attr('x2', function (d) {return 1000*d[1][0]}) 772 | .attr('y2', function (d) {return 1000*d[1][1]}) 773 | } 774 | 775 | 776 | function visualizeContour(h, level) { 777 | level = level || 0; 778 | var links = contour(h, level); 779 | drawPaths('coast', links); 780 | } 781 | 782 | function visualizeBorders(h, cities, n) { 783 | var links = getBorders(h, getTerritories(h, cities, n)); 784 | drawPaths('border', links); 785 | } 786 | 787 | 788 | function visualizeCities(svg, render) { 789 | var cities = render.cities; 790 | var h = render.h; 791 | var n = render.params.nterrs; 792 | 793 | var circs = svg.selectAll('circle.city').data(cities); 794 | circs.enter() 795 | .append('circle') 796 | .classed('city', true); 797 | circs.exit() 798 | .remove(); 799 | svg.selectAll('circle.city') 800 | .attr('cx', function (d) {return 1000*h.mesh.vxs[d][0]}) 801 | .attr('cy', function (d) {return 1000*h.mesh.vxs[d][1]}) 802 | .attr('r', function (d, i) {return i >= n ? 4 : 10}) 803 | .style('fill', 'white') 804 | .style('stroke-width', 5) 805 | .style('stroke-linecap', 'round') 806 | .style('stroke', 'black') 807 | .raise(); 808 | } 809 | 810 | function dropEdge(h, p) { 811 | p = p || 4 812 | var newh = zero(h.mesh); 813 | for (var i = 0; i < h.length; i++) { 814 | var v = h.mesh.vxs[i]; 815 | var x = 2.4*v[0] / h.mesh.extent.width; 816 | var y = 2.4*v[1] / h.mesh.extent.height; 817 | newh[i] = h[i] - Math.exp(10*(Math.pow(Math.pow(x, p) + Math.pow(y, p), 1/p) - 1)); 818 | } 819 | return newh; 820 | } 821 | 822 | function generateCoast(params) { 823 | var mesh = generateGoodMesh(params.npts, params.extent); 824 | var h = add( 825 | slope(mesh, randomVector(4)), 826 | cone(mesh, runif(-1, -1)), 827 | mountains(mesh, 50) 828 | ); 829 | for (var i = 0; i < 10; i++) { 830 | h = relax(h); 831 | } 832 | h = peaky(h); 833 | h = doErosion(h, runif(0, 0.1), 5); 834 | h = setSeaLevel(h, runif(0.2, 0.6)); 835 | h = fillSinks(h); 836 | h = cleanCoast(h, 3); 837 | return h; 838 | } 839 | 840 | function terrCenter(h, terr, city, landOnly) { 841 | var x = 0; 842 | var y = 0; 843 | var n = 0; 844 | for (var i = 0; i < terr.length; i++) { 845 | if (terr[i] != city) continue; 846 | if (landOnly && h[i] <= 0) continue; 847 | x += terr.mesh.vxs[i][0]; 848 | y += terr.mesh.vxs[i][1]; 849 | n++; 850 | } 851 | return [x/n, y/n]; 852 | } 853 | 854 | function drawLabels(svg, render) { 855 | var params = render.params; 856 | var h = render.h; 857 | var terr = render.terr; 858 | var cities = render.cities; 859 | var nterrs = render.params.nterrs; 860 | var avoids = [render.rivers, render.coasts, render.borders]; 861 | var lang = makeRandomLanguage(); 862 | var citylabels = []; 863 | function penalty(label) { 864 | var pen = 0; 865 | if (label.x0 < -0.45 * h.mesh.extent.width) pen += 100; 866 | if (label.x1 > 0.45 * h.mesh.extent.width) pen += 100; 867 | if (label.y0 < -0.45 * h.mesh.extent.height) pen += 100; 868 | if (label.y1 > 0.45 * h.mesh.extent.height) pen += 100; 869 | for (var i = 0; i < citylabels.length; i++) { 870 | var olabel = citylabels[i]; 871 | if (label.x0 < olabel.x1 && label.x1 > olabel.x0 && 872 | label.y0 < olabel.y1 && label.y1 > olabel.y0) { 873 | pen += 100; 874 | } 875 | } 876 | 877 | for (var i = 0; i < cities.length; i++) { 878 | var c = h.mesh.vxs[cities[i]]; 879 | if (label.x0 < c[0] && label.x1 > c[0] && label.y0 < c[1] && label.y1 > c[1]) { 880 | pen += 100; 881 | } 882 | } 883 | for (var i = 0; i < avoids.length; i++) { 884 | var avoid = avoids[i]; 885 | for (var j = 0; j < avoid.length; j++) { 886 | var avpath = avoid[j]; 887 | for (var k = 0; k < avpath.length; k++) { 888 | var pt = avpath[k]; 889 | if (pt[0] > label.x0 && pt[0] < label.x1 && pt[1] > label.y0 && pt[1] < label.y1) { 890 | pen++; 891 | } 892 | } 893 | } 894 | } 895 | return pen; 896 | } 897 | for (var i = 0; i < cities.length; i++) { 898 | var x = h.mesh.vxs[cities[i]][0]; 899 | var y = h.mesh.vxs[cities[i]][1]; 900 | var text = makeName(lang, 'city'); 901 | var size = i < nterrs ? params.fontsizes.city : params.fontsizes.town; 902 | var sx = 0.65 * size/1000 * text.length; 903 | var sy = size/1000; 904 | var posslabels = [ 905 | { 906 | x: x + 0.8 * sy, 907 | y: y + 0.3 * sy, 908 | align: 'start', 909 | x0: x + 0.7 * sy, 910 | y0: y - 0.6 * sy, 911 | x1: x + 0.7 * sy + sx, 912 | y1: y + 0.6 * sy 913 | }, 914 | { 915 | x: x - 0.8 * sy, 916 | y: y + 0.3 * sy, 917 | align: 'end', 918 | x0: x - 0.9 * sy - sx, 919 | y0: y - 0.7 * sy, 920 | x1: x - 0.9 * sy, 921 | y1: y + 0.7 * sy 922 | }, 923 | { 924 | x: x, 925 | y: y - 0.8 * sy, 926 | align: 'middle', 927 | x0: x - sx/2, 928 | y0: y - 1.9*sy, 929 | x1: x + sx/2, 930 | y1: y - 0.7 * sy 931 | }, 932 | { 933 | x: x, 934 | y: y + 1.2 * sy, 935 | align: 'middle', 936 | x0: x - sx/2, 937 | y0: y + 0.1*sy, 938 | x1: x + sx/2, 939 | y1: y + 1.3*sy 940 | } 941 | ]; 942 | var label = posslabels[d3.scan(posslabels, function (a, b) {return penalty(a) - penalty(b)})]; 943 | label.text = text; 944 | label.size = size; 945 | citylabels.push(label); 946 | } 947 | var texts = svg.selectAll('text.city').data(citylabels); 948 | texts.enter() 949 | .append('text') 950 | .classed('city', true); 951 | texts.exit() 952 | .remove(); 953 | svg.selectAll('text.city') 954 | .attr('x', function (d) {return 1000*d.x}) 955 | .attr('y', function (d) {return 1000*d.y}) 956 | .style('font-size', function (d) {return d.size}) 957 | .style('text-anchor', function (d) {return d.align}) 958 | .text(function (d) {return d.text}) 959 | .raise(); 960 | 961 | var reglabels = []; 962 | for (var i = 0; i < nterrs; i++) { 963 | var city = cities[i]; 964 | var text = makeName(lang, 'region'); 965 | var sy = params.fontsizes.region / 1000; 966 | var sx = 0.6 * text.length * sy; 967 | var lc = terrCenter(h, terr, city, true); 968 | var oc = terrCenter(h, terr, city, false); 969 | var best = 0; 970 | var bestscore = -999999; 971 | for (var j = 0; j < h.length; j++) { 972 | var score = 0; 973 | var v = h.mesh.vxs[j]; 974 | score -= 3000 * Math.sqrt((v[0] - lc[0]) * (v[0] - lc[0]) + (v[1] - lc[1]) * (v[1] - lc[1])); 975 | score -= 1000 * Math.sqrt((v[0] - oc[0]) * (v[0] - oc[0]) + (v[1] - oc[1]) * (v[1] - oc[1])); 976 | if (terr[j] != city) score -= 3000; 977 | for (var k = 0; k < cities.length; k++) { 978 | var u = h.mesh.vxs[cities[k]]; 979 | if (Math.abs(v[0] - u[0]) < sx && 980 | Math.abs(v[1] - sy/2 - u[1]) < sy) { 981 | score -= k < nterrs ? 4000 : 500; 982 | } 983 | if (v[0] - sx/2 < citylabels[k].x1 && 984 | v[0] + sx/2 > citylabels[k].x0 && 985 | v[1] - sy < citylabels[k].y1 && 986 | v[1] > citylabels[k].y0) { 987 | score -= 5000; 988 | } 989 | } 990 | for (var k = 0; k < reglabels.length; k++) { 991 | var label = reglabels[k]; 992 | if (v[0] - sx/2 < label.x + label.width/2 && 993 | v[0] + sx/2 > label.x - label.width/2 && 994 | v[1] - sy < label.y && 995 | v[1] > label.y - label.size) { 996 | score -= 20000; 997 | } 998 | } 999 | if (h[j] <= 0) score -= 500; 1000 | if (v[0] + sx/2 > 0.5 * h.mesh.extent.width) score -= 50000; 1001 | if (v[0] - sx/2 < -0.5 * h.mesh.extent.width) score -= 50000; 1002 | if (v[1] > 0.5 * h.mesh.extent.height) score -= 50000; 1003 | if (v[1] - sy < -0.5 * h.mesh.extent.height) score -= 50000; 1004 | if (score > bestscore) { 1005 | bestscore = score; 1006 | best = j; 1007 | } 1008 | } 1009 | reglabels.push({ 1010 | text: text, 1011 | x: h.mesh.vxs[best][0], 1012 | y: h.mesh.vxs[best][1], 1013 | size:sy, 1014 | width:sx 1015 | }); 1016 | } 1017 | texts = svg.selectAll('text.region').data(reglabels); 1018 | texts.enter() 1019 | .append('text') 1020 | .classed('region', true); 1021 | texts.exit() 1022 | .remove(); 1023 | svg.selectAll('text.region') 1024 | .attr('x', function (d) {return 1000*d.x}) 1025 | .attr('y', function (d) {return 1000*d.y}) 1026 | .style('font-size', function (d) {return 1000*d.size}) 1027 | .style('text-anchor', 'middle') 1028 | .text(function (d) {return d.text}) 1029 | .raise(); 1030 | 1031 | } 1032 | function drawMap(svg, render) { 1033 | render.rivers = getRivers(render.h, 0.01); 1034 | render.coasts = contour(render.h, 0); 1035 | render.terr = getTerritories(render); 1036 | render.borders = getBorders(render); 1037 | drawPaths(svg, 'river', render.rivers); 1038 | drawPaths(svg, 'coast', render.coasts); 1039 | drawPaths(svg, 'border', render.borders); 1040 | visualizeSlopes(svg, render); 1041 | visualizeCities(svg, render); 1042 | drawLabels(svg, render); 1043 | } 1044 | 1045 | function doMap(svg, params) { 1046 | var render = { 1047 | params: params 1048 | }; 1049 | var width = svg.attr('width'); 1050 | svg.attr('height', width * params.extent.height / params.extent.width); 1051 | svg.attr('viewBox', -1000 * params.extent.width/2 + ' ' + 1052 | -1000 * params.extent.height/2 + ' ' + 1053 | 1000 * params.extent.width + ' ' + 1054 | 1000 * params.extent.height); 1055 | svg.selectAll().remove(); 1056 | render.h = params.generator(params); 1057 | placeCities(render); 1058 | drawMap(svg, render); 1059 | } 1060 | 1061 | var defaultParams = { 1062 | extent: defaultExtent, 1063 | generator: generateCoast, 1064 | npts: 16384, 1065 | ncities: 15, 1066 | nterrs: 5, 1067 | fontsizes: { 1068 | region: 40, 1069 | city: 25, 1070 | town: 20 1071 | } 1072 | } 1073 | 1074 | --------------------------------------------------------------------------------