├── .gitignore ├── .gitmodules ├── LICENSE ├── Lava.as ├── Map.as ├── NoisyEdges.as ├── README.md ├── Roads.as ├── RoadsSpanningTree.as ├── Watersheds.as ├── graph ├── Center.as ├── Corner.as └── Edge.as ├── mapgen2.as ├── prototypes ├── delaunay_set.as ├── hexagonal_drainage_basin.as ├── hexagonal_grid.as ├── noisy_line.as └── quadrilateral_drainage_basin.as └── third-party └── PM_PRNG └── de └── polygonal └── math └── PM_PRNG.as /.gitignore: -------------------------------------------------------------------------------- 1 | mapgen2.swf 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third-party/as3delaunay"] 2 | path = third-party/as3delaunay 3 | url = https://github.com/nodename/as3delaunay.git 4 | [submodule "third-party/as3corelib"] 5 | path = third-party/as3corelib 6 | url = https://github.com/mikechambers/as3corelib.git 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Map generation project 2 | 3 | Copyright 2010 Amit J Patel 4 | 5 | licensed under the MIT Open Source license 6 | 7 | 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining 10 | a copy of this software and associated documentation files (the 11 | "Software"), to deal in the Software without restriction, including 12 | without limitation the rights to use, copy, modify, merge, publish, 13 | distribute, sublicense, and/or sell copies of the Software, and to 14 | permit persons to whom the Software is furnished to do so, subject to 15 | the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included 18 | in all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 23 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 25 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 26 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /Lava.as: -------------------------------------------------------------------------------- 1 | // Randomly place lava on high elevation dry land. 2 | // Author: amitp@cs.stanford.edu 3 | // License: MIT 4 | 5 | package { 6 | import graph.*; 7 | 8 | public class Lava { 9 | static public var FRACTION_LAVA_FISSURES:Number = 0.2; // 0 to 1, probability of fissure 10 | 11 | // The lava array marks the edges that hava lava. 12 | public var lava:Array = []; // edge index -> Boolean 13 | 14 | // Lava fissures are at high elevations where moisture is low 15 | public function createLava(map:Map, randomDouble:Function):void { 16 | var edge:Edge; 17 | for each (edge in map.edges) { 18 | if (!edge.river && !edge.d0.water && !edge.d1.water 19 | && edge.d0.elevation > 0.8 && edge.d1.elevation > 0.8 20 | && edge.d0.moisture < 0.3 && edge.d1.moisture < 0.3 21 | && randomDouble() < FRACTION_LAVA_FISSURES) { 22 | lava[edge.index] = true; 23 | } 24 | } 25 | } 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Map.as: -------------------------------------------------------------------------------- 1 | // Make a map out of a voronoi graph 2 | // Author: amitp@cs.stanford.edu 3 | // License: MIT 4 | 5 | package { 6 | import graph.*; 7 | import flash.geom.*; 8 | import flash.utils.Dictionary; 9 | import flash.utils.getTimer; 10 | import flash.system.System; 11 | import com.nodename.geom.LineSegment; 12 | import com.nodename.Delaunay.Voronoi; 13 | import de.polygonal.math.PM_PRNG; 14 | 15 | public class Map { 16 | static public var LAKE_THRESHOLD:Number = 0.3; // 0 to 1, fraction of water corners for water polygon 17 | 18 | // Passed in by the caller: 19 | public var SIZE:Number; 20 | 21 | // Island shape is controlled by the islandRandom seed and the 22 | // type of island, passed in when we set the island shape. The 23 | // islandShape function uses both of them to determine whether any 24 | // point should be water or land. 25 | public var islandShape:Function; 26 | 27 | // Island details are controlled by this random generator. The 28 | // initial map upon loading is always deterministic, but 29 | // subsequent maps reset this random number generator with a 30 | // random seed. 31 | public var mapRandom:PM_PRNG = new PM_PRNG(); 32 | public var needsMoreRandomness:Boolean; // see comment in PointSelector 33 | 34 | // Point selection is random for the original article, with Lloyd 35 | // Relaxation, but there are other ways of choosing points. Grids 36 | // in particular can be much simpler to start with, because you 37 | // don't need Voronoi at all. HOWEVER for ease of implementation, 38 | // I continue to use Voronoi here, to reuse the graph building 39 | // code. If you're using a grid, generate the graph directly. 40 | public var pointSelector:Function; 41 | public var numPoints:int; 42 | 43 | // These store the graph data 44 | public var points:Vector.; // Only useful during map construction 45 | public var centers:Vector.
; 46 | public var corners:Vector.; 47 | public var edges:Vector.; 48 | 49 | public function Map(size:Number) { 50 | SIZE = size; 51 | numPoints = 1; 52 | reset(); 53 | } 54 | 55 | // Random parameters governing the overall shape of the island 56 | public function newIsland(islandType:String, pointType:String, numPoints_:int, seed:int, variant:int):void { 57 | islandShape = IslandShape['make'+islandType](seed); 58 | pointSelector = PointSelector['generate'+pointType](SIZE, seed); 59 | needsMoreRandomness = PointSelector.needsMoreRandomness(pointType); 60 | numPoints = numPoints_; 61 | mapRandom.seed = variant; 62 | } 63 | 64 | 65 | public function reset():void { 66 | var p:Center, q:Corner, edge:Edge; 67 | 68 | // Break cycles so the garbage collector will release data. 69 | if (points) { 70 | points.splice(0, points.length); 71 | } 72 | if (edges) { 73 | for each (edge in edges) { 74 | edge.d0 = edge.d1 = null; 75 | edge.v0 = edge.v1 = null; 76 | } 77 | edges.splice(0, edges.length); 78 | } 79 | if (centers) { 80 | for each (p in centers) { 81 | p.neighbors.splice(0, p.neighbors.length); 82 | p.corners.splice(0, p.corners.length); 83 | p.borders.splice(0, p.borders.length); 84 | } 85 | centers.splice(0, centers.length); 86 | } 87 | if (corners) { 88 | for each (q in corners) { 89 | q.adjacent.splice(0, q.adjacent.length); 90 | q.touches.splice(0, q.touches.length); 91 | q.protrudes.splice(0, q.protrudes.length); 92 | q.downslope = null; 93 | q.watershed = null; 94 | } 95 | corners.splice(0, corners.length); 96 | } 97 | 98 | // Clear the previous graph data. 99 | if (!points) points = new Vector.(); 100 | if (!edges) edges = new Vector.(); 101 | if (!centers) centers = new Vector.
(); 102 | if (!corners) corners = new Vector.(); 103 | 104 | System.gc(); 105 | } 106 | 107 | 108 | public function go(first:int, last:int):void { 109 | var stages:Array = []; 110 | 111 | function timeIt(name:String, fn:Function):void { 112 | var t:Number = getTimer(); 113 | fn(); 114 | } 115 | 116 | // Generate the initial random set of points 117 | stages.push 118 | (["Place points...", 119 | function():void { 120 | reset(); 121 | points = pointSelector(numPoints); 122 | }]); 123 | 124 | // Create a graph structure from the Voronoi edge list. The 125 | // methods in the Voronoi object are somewhat inconvenient for 126 | // my needs, so I transform that data into the data I actually 127 | // need: edges connected to the Delaunay triangles and the 128 | // Voronoi polygons, a reverse map from those four points back 129 | // to the edge, a map from these four points to the points 130 | // they connect to (both along the edge and crosswise). 131 | stages.push 132 | ( ["Build graph...", 133 | function():void { 134 | var voronoi:Voronoi = new Voronoi(points, null, new Rectangle(0, 0, SIZE, SIZE)); 135 | buildGraph(points, voronoi); 136 | improveCorners(); 137 | voronoi.dispose(); 138 | voronoi = null; 139 | points = null; 140 | }]); 141 | 142 | stages.push 143 | (["Assign elevations...", 144 | function():void { 145 | // Determine the elevations and water at Voronoi corners. 146 | assignCornerElevations(); 147 | 148 | // Determine polygon and corner type: ocean, coast, land. 149 | assignOceanCoastAndLand(); 150 | 151 | // Rescale elevations so that the highest is 1.0, and they're 152 | // distributed well. We want lower elevations to be more common 153 | // than higher elevations, in proportions approximately matching 154 | // concentric rings. That is, the lowest elevation is the 155 | // largest ring around the island, and therefore should more 156 | // land area than the highest elevation, which is the very 157 | // center of a perfectly circular island. 158 | redistributeElevations(landCorners(corners)); 159 | 160 | // Assign elevations to non-land corners 161 | for each (var q:Corner in corners) { 162 | if (q.ocean || q.coast) { 163 | q.elevation = 0.0; 164 | } 165 | } 166 | 167 | // Polygon elevations are the average of their corners 168 | assignPolygonElevations(); 169 | }]); 170 | 171 | 172 | stages.push 173 | (["Assign moisture...", 174 | function():void { 175 | // Determine downslope paths. 176 | calculateDownslopes(); 177 | 178 | // Determine watersheds: for every corner, where does it flow 179 | // out into the ocean? 180 | calculateWatersheds(); 181 | 182 | // Create rivers. 183 | createRivers(); 184 | 185 | // Determine moisture at corners, starting at rivers 186 | // and lakes, but not oceans. Then redistribute 187 | // moisture to cover the entire range evenly from 0.0 188 | // to 1.0. Then assign polygon moisture as the average 189 | // of the corner moisture. 190 | assignCornerMoisture(); 191 | redistributeMoisture(landCorners(corners)); 192 | assignPolygonMoisture(); 193 | }]); 194 | 195 | stages.push 196 | (["Decorate map...", 197 | function():void { 198 | assignBiomes(); 199 | }]); 200 | 201 | for (var i:int = first; i < last; i++) { 202 | timeIt(stages[i][0], stages[i][1]); 203 | } 204 | } 205 | 206 | 207 | // Although Lloyd relaxation improves the uniformity of polygon 208 | // sizes, it doesn't help with the edge lengths. Short edges can 209 | // be bad for some games, and lead to weird artifacts on 210 | // rivers. We can easily lengthen short edges by moving the 211 | // corners, but **we lose the Voronoi property**. The corners are 212 | // moved to the average of the polygon centers around them. Short 213 | // edges become longer. Long edges tend to become shorter. The 214 | // polygons tend to be more uniform after this step. 215 | public function improveCorners():void { 216 | var newCorners:Vector. = new Vector.(corners.length); 217 | var q:Corner, r:Center, point:Point, i:int, edge:Edge; 218 | 219 | // First we compute the average of the centers next to each corner. 220 | for each (q in corners) { 221 | if (q.border) { 222 | newCorners[q.index] = q.point; 223 | } else { 224 | point = new Point(0.0, 0.0); 225 | for each (r in q.touches) { 226 | point.x += r.point.x; 227 | point.y += r.point.y; 228 | } 229 | point.x /= q.touches.length; 230 | point.y /= q.touches.length; 231 | newCorners[q.index] = point; 232 | } 233 | } 234 | 235 | // Move the corners to the new locations. 236 | for (i = 0; i < corners.length; i++) { 237 | corners[i].point = newCorners[i]; 238 | } 239 | 240 | // The edge midpoints were computed for the old corners and need 241 | // to be recomputed. 242 | for each (edge in edges) { 243 | if (edge.v0 && edge.v1) { 244 | edge.midpoint = Point.interpolate(edge.v0.point, edge.v1.point, 0.5); 245 | } 246 | } 247 | } 248 | 249 | 250 | // Create an array of corners that are on land only, for use by 251 | // algorithms that work only on land. We return an array instead 252 | // of a vector because the redistribution algorithms want to sort 253 | // this array using Array.sortOn. 254 | public function landCorners(corners:Vector.):Array { 255 | var q:Corner, locations:Array = []; 256 | for each (q in corners) { 257 | if (!q.ocean && !q.coast) { 258 | locations.push(q); 259 | } 260 | } 261 | return locations; 262 | } 263 | 264 | 265 | // Build graph data structure in 'edges', 'centers', 'corners', 266 | // based on information in the Voronoi results: point.neighbors 267 | // will be a list of neighboring points of the same type (corner 268 | // or center); point.edges will be a list of edges that include 269 | // that point. Each edge connects to four points: the Voronoi edge 270 | // edge.{v0,v1} and its dual Delaunay triangle edge edge.{d0,d1}. 271 | // For boundary polygons, the Delaunay edge will have one null 272 | // point, and the Voronoi edge may be null. 273 | public function buildGraph(points:Vector., voronoi:Voronoi):void { 274 | var p:Center, q:Corner, point:Point, other:Point; 275 | var libedges:Vector. = voronoi.edges(); 276 | var centerLookup:Dictionary = new Dictionary(); 277 | 278 | // Build Center objects for each of the points, and a lookup map 279 | // to find those Center objects again as we build the graph 280 | for each (point in points) { 281 | p = new Center(); 282 | p.index = centers.length; 283 | p.point = point; 284 | p.neighbors = new Vector.
(); 285 | p.borders = new Vector.(); 286 | p.corners = new Vector.(); 287 | centers.push(p); 288 | centerLookup[point] = p; 289 | } 290 | 291 | // Workaround for Voronoi lib bug: we need to call region() 292 | // before Edges or neighboringSites are available 293 | for each (p in centers) { 294 | voronoi.region(p.point); 295 | } 296 | 297 | // The Voronoi library generates multiple Point objects for 298 | // corners, and we need to canonicalize to one Corner object. 299 | // To make lookup fast, we keep an array of Points, bucketed by 300 | // x value, and then we only have to look at other Points in 301 | // nearby buckets. When we fail to find one, we'll create a new 302 | // Corner object. 303 | var _cornerMap:Array = []; 304 | function makeCorner(point:Point):Corner { 305 | var q:Corner; 306 | 307 | if (point == null) return null; 308 | for (var bucket:int = int(point.x)-1; bucket <= int(point.x)+1; bucket++) { 309 | for each (q in _cornerMap[bucket]) { 310 | var dx:Number = point.x - q.point.x; 311 | var dy:Number = point.y - q.point.y; 312 | if (dx*dx + dy*dy < 1e-6) { 313 | return q; 314 | } 315 | } 316 | } 317 | bucket = int(point.x); 318 | if (!_cornerMap[bucket]) _cornerMap[bucket] = []; 319 | q = new Corner(); 320 | q.index = corners.length; 321 | corners.push(q); 322 | q.point = point; 323 | q.border = (point.x == 0 || point.x == SIZE 324 | || point.y == 0 || point.y == SIZE); 325 | q.touches = new Vector.
(); 326 | q.protrudes = new Vector.(); 327 | q.adjacent = new Vector.(); 328 | _cornerMap[bucket].push(q); 329 | return q; 330 | } 331 | 332 | // Helper functions for the following for loop; ideally these 333 | // would be inlined 334 | function addToCornerList(v:Vector., x:Corner):void { 335 | if (x != null && v.indexOf(x) < 0) { v.push(x); } 336 | } 337 | function addToCenterList(v:Vector.
, x:Center):void { 338 | if (x != null && v.indexOf(x) < 0) { v.push(x); } 339 | } 340 | 341 | for each (var libedge:com.nodename.Delaunay.Edge in libedges) { 342 | var dedge:LineSegment = libedge.delaunayLine(); 343 | var vedge:LineSegment = libedge.voronoiEdge(); 344 | 345 | // Fill the graph data. Make an Edge object corresponding to 346 | // the edge from the voronoi library. 347 | var edge:Edge = new Edge(); 348 | edge.index = edges.length; 349 | edge.river = 0; 350 | edges.push(edge); 351 | edge.midpoint = vedge.p0 && vedge.p1 && Point.interpolate(vedge.p0, vedge.p1, 0.5); 352 | 353 | // Edges point to corners. Edges point to centers. 354 | edge.v0 = makeCorner(vedge.p0); 355 | edge.v1 = makeCorner(vedge.p1); 356 | edge.d0 = centerLookup[dedge.p0]; 357 | edge.d1 = centerLookup[dedge.p1]; 358 | 359 | // Centers point to edges. Corners point to edges. 360 | if (edge.d0 != null) { edge.d0.borders.push(edge); } 361 | if (edge.d1 != null) { edge.d1.borders.push(edge); } 362 | if (edge.v0 != null) { edge.v0.protrudes.push(edge); } 363 | if (edge.v1 != null) { edge.v1.protrudes.push(edge); } 364 | 365 | // Centers point to centers. 366 | if (edge.d0 != null && edge.d1 != null) { 367 | addToCenterList(edge.d0.neighbors, edge.d1); 368 | addToCenterList(edge.d1.neighbors, edge.d0); 369 | } 370 | 371 | // Corners point to corners 372 | if (edge.v0 != null && edge.v1 != null) { 373 | addToCornerList(edge.v0.adjacent, edge.v1); 374 | addToCornerList(edge.v1.adjacent, edge.v0); 375 | } 376 | 377 | // Centers point to corners 378 | if (edge.d0 != null) { 379 | addToCornerList(edge.d0.corners, edge.v0); 380 | addToCornerList(edge.d0.corners, edge.v1); 381 | } 382 | if (edge.d1 != null) { 383 | addToCornerList(edge.d1.corners, edge.v0); 384 | addToCornerList(edge.d1.corners, edge.v1); 385 | } 386 | 387 | // Corners point to centers 388 | if (edge.v0 != null) { 389 | addToCenterList(edge.v0.touches, edge.d0); 390 | addToCenterList(edge.v0.touches, edge.d1); 391 | } 392 | if (edge.v1 != null) { 393 | addToCenterList(edge.v1.touches, edge.d0); 394 | addToCenterList(edge.v1.touches, edge.d1); 395 | } 396 | } 397 | } 398 | 399 | 400 | // Determine elevations and water at Voronoi corners. By 401 | // construction, we have no local minima. This is important for 402 | // the downslope vectors later, which are used in the river 403 | // construction algorithm. Also by construction, inlets/bays 404 | // push low elevation areas inland, which means many rivers end 405 | // up flowing out through them. Also by construction, lakes 406 | // often end up on river paths because they don't raise the 407 | // elevation as much as other terrain does. 408 | public function assignCornerElevations():void { 409 | var q:Corner, s:Corner; 410 | var queue:Array = []; 411 | 412 | for each (q in corners) { 413 | q.water = !inside(q.point); 414 | } 415 | 416 | for each (q in corners) { 417 | // The edges of the map are elevation 0 418 | if (q.border) { 419 | q.elevation = 0.0; 420 | queue.push(q); 421 | } else { 422 | q.elevation = Infinity; 423 | } 424 | } 425 | // Traverse the graph and assign elevations to each point. As we 426 | // move away from the map border, increase the elevations. This 427 | // guarantees that rivers always have a way down to the coast by 428 | // going downhill (no local minima). 429 | while (queue.length > 0) { 430 | q = queue.shift(); 431 | 432 | for each (s in q.adjacent) { 433 | // Every step up is epsilon over water or 1 over land. The 434 | // number doesn't matter because we'll rescale the 435 | // elevations later. 436 | var newElevation:Number = 0.01 + q.elevation; 437 | if (!q.water && !s.water) { 438 | newElevation += 1; 439 | if (needsMoreRandomness) { 440 | // HACK: the map looks nice because of randomness of 441 | // points, randomness of rivers, and randomness of 442 | // edges. Without random point selection, I needed to 443 | // inject some more randomness to make maps look 444 | // nicer. I'm doing it here, with elevations, but I 445 | // think there must be a better way. This hack is only 446 | // used with square/hexagon grids. 447 | newElevation += mapRandom.nextDouble(); 448 | } 449 | } 450 | // If this point changed, we'll add it to the queue so 451 | // that we can process its neighbors too. 452 | if (newElevation < s.elevation) { 453 | s.elevation = newElevation; 454 | queue.push(s); 455 | } 456 | } 457 | } 458 | } 459 | 460 | 461 | // Change the overall distribution of elevations so that lower 462 | // elevations are more common than higher 463 | // elevations. Specifically, we want elevation X to have frequency 464 | // (1-X). To do this we will sort the corners, then set each 465 | // corner to its desired elevation. 466 | public function redistributeElevations(locations:Array):void { 467 | // SCALE_FACTOR increases the mountain area. At 1.0 the maximum 468 | // elevation barely shows up on the map, so we set it to 1.1. 469 | var SCALE_FACTOR:Number = 1.1; 470 | var i:int, y:Number, x:Number; 471 | 472 | locations.sortOn('elevation', Array.NUMERIC); 473 | for (i = 0; i < locations.length; i++) { 474 | // Let y(x) be the total area that we want at elevation <= x. 475 | // We want the higher elevations to occur less than lower 476 | // ones, and set the area to be y(x) = 1 - (1-x)^2. 477 | y = i/(locations.length-1); 478 | // Now we have to solve for x, given the known y. 479 | // * y = 1 - (1-x)^2 480 | // * y = 1 - (1 - 2x + x^2) 481 | // * y = 2x - x^2 482 | // * x^2 - 2x + y = 0 483 | // From this we can use the quadratic equation to get: 484 | x = Math.sqrt(SCALE_FACTOR) - Math.sqrt(SCALE_FACTOR*(1-y)); 485 | if (x > 1.0) x = 1.0; // TODO: does this break downslopes? 486 | locations[i].elevation = x; 487 | } 488 | } 489 | 490 | 491 | // Change the overall distribution of moisture to be evenly distributed. 492 | public function redistributeMoisture(locations:Array):void { 493 | var i:int; 494 | locations.sortOn('moisture', Array.NUMERIC); 495 | for (i = 0; i < locations.length; i++) { 496 | locations[i].moisture = i/(locations.length-1); 497 | } 498 | } 499 | 500 | 501 | // Determine polygon and corner types: ocean, coast, land. 502 | public function assignOceanCoastAndLand():void { 503 | // Compute polygon attributes 'ocean' and 'water' based on the 504 | // corner attributes. Count the water corners per 505 | // polygon. Oceans are all polygons connected to the edge of the 506 | // map. In the first pass, mark the edges of the map as ocean; 507 | // in the second pass, mark any water-containing polygon 508 | // connected an ocean as ocean. 509 | var queue:Array = []; 510 | var p:Center, q:Corner, r:Center, numWater:int; 511 | 512 | for each (p in centers) { 513 | numWater = 0; 514 | for each (q in p.corners) { 515 | if (q.border) { 516 | p.border = true; 517 | p.ocean = true; 518 | q.water = true; 519 | queue.push(p); 520 | } 521 | if (q.water) { 522 | numWater += 1; 523 | } 524 | } 525 | p.water = (p.ocean || numWater >= p.corners.length * LAKE_THRESHOLD); 526 | } 527 | while (queue.length > 0) { 528 | p = queue.shift(); 529 | for each (r in p.neighbors) { 530 | if (r.water && !r.ocean) { 531 | r.ocean = true; 532 | queue.push(r); 533 | } 534 | } 535 | } 536 | 537 | // Set the polygon attribute 'coast' based on its neighbors. If 538 | // it has at least one ocean and at least one land neighbor, 539 | // then this is a coastal polygon. 540 | for each (p in centers) { 541 | var numOcean:int = 0; 542 | var numLand:int = 0; 543 | for each (r in p.neighbors) { 544 | numOcean += int(r.ocean); 545 | numLand += int(!r.water); 546 | } 547 | p.coast = (numOcean > 0) && (numLand > 0); 548 | } 549 | 550 | 551 | // Set the corner attributes based on the computed polygon 552 | // attributes. If all polygons connected to this corner are 553 | // ocean, then it's ocean; if all are land, then it's land; 554 | // otherwise it's coast. 555 | for each (q in corners) { 556 | numOcean = 0; 557 | numLand = 0; 558 | for each (p in q.touches) { 559 | numOcean += int(p.ocean); 560 | numLand += int(!p.water); 561 | } 562 | q.ocean = (numOcean == q.touches.length); 563 | q.coast = (numOcean > 0) && (numLand > 0); 564 | q.water = q.border || ((numLand != q.touches.length) && !q.coast); 565 | } 566 | } 567 | 568 | 569 | // Polygon elevations are the average of the elevations of their corners. 570 | public function assignPolygonElevations():void { 571 | var p:Center, q:Corner, sumElevation:Number; 572 | for each (p in centers) { 573 | sumElevation = 0.0; 574 | for each (q in p.corners) { 575 | sumElevation += q.elevation; 576 | } 577 | p.elevation = sumElevation / p.corners.length; 578 | } 579 | } 580 | 581 | 582 | // Calculate downslope pointers. At every point, we point to the 583 | // point downstream from it, or to itself. This is used for 584 | // generating rivers and watersheds. 585 | public function calculateDownslopes():void { 586 | var q:Corner, s:Corner, r:Corner; 587 | 588 | for each (q in corners) { 589 | r = q; 590 | for each (s in q.adjacent) { 591 | if (s.elevation <= r.elevation) { 592 | r = s; 593 | } 594 | } 595 | q.downslope = r; 596 | } 597 | } 598 | 599 | 600 | // Calculate the watershed of every land point. The watershed is 601 | // the last downstream land point in the downslope graph. TODO: 602 | // watersheds are currently calculated on corners, but it'd be 603 | // more useful to compute them on polygon centers so that every 604 | // polygon can be marked as being in one watershed. 605 | public function calculateWatersheds():void { 606 | var q:Corner, r:Corner, i:int, changed:Boolean; 607 | 608 | // Initially the watershed pointer points downslope one step. 609 | for each (q in corners) { 610 | q.watershed = q; 611 | if (!q.ocean && !q.coast) { 612 | q.watershed = q.downslope; 613 | } 614 | } 615 | // Follow the downslope pointers to the coast. Limit to 100 616 | // iterations although most of the time with numPoints==2000 it 617 | // only takes 20 iterations because most points are not far from 618 | // a coast. TODO: can run faster by looking at 619 | // p.watershed.watershed instead of p.downslope.watershed. 620 | for (i = 0; i < 100; i++) { 621 | changed = false; 622 | for each (q in corners) { 623 | if (!q.ocean && !q.coast && !q.watershed.coast) { 624 | r = q.downslope.watershed; 625 | if (!r.ocean) { 626 | q.watershed = r; 627 | changed = true; 628 | } 629 | } 630 | } 631 | if (!changed) break; 632 | } 633 | // How big is each watershed? 634 | for each (q in corners) { 635 | r = q.watershed; 636 | r.watershed_size = 1 + (r.watershed_size || 0); 637 | } 638 | } 639 | 640 | 641 | // Create rivers along edges. Pick a random corner point, then 642 | // move downslope. Mark the edges and corners as rivers. 643 | public function createRivers():void { 644 | var i:int, q:Corner, edge:Edge; 645 | 646 | for (i = 0; i < SIZE/2; i++) { 647 | q = corners[mapRandom.nextIntRange(0, corners.length-1)]; 648 | if (q.ocean || q.elevation < 0.3 || q.elevation > 0.9) continue; 649 | // Bias rivers to go west: if (q.downslope.x > q.x) continue; 650 | while (!q.coast) { 651 | if (q == q.downslope) { 652 | break; 653 | } 654 | edge = lookupEdgeFromCorner(q, q.downslope); 655 | edge.river = edge.river + 1; 656 | q.river = (q.river || 0) + 1; 657 | q.downslope.river = (q.downslope.river || 0) + 1; // TODO: fix double count 658 | q = q.downslope; 659 | } 660 | } 661 | } 662 | 663 | 664 | // Calculate moisture. Freshwater sources spread moisture: rivers 665 | // and lakes (not oceans). Saltwater sources have moisture but do 666 | // not spread it (we set it at the end, after propagation). 667 | public function assignCornerMoisture():void { 668 | var q:Corner, r:Corner, newMoisture:Number; 669 | var queue:Array = []; 670 | // Fresh water 671 | for each (q in corners) { 672 | if ((q.water || q.river > 0) && !q.ocean) { 673 | q.moisture = q.river > 0? Math.min(3.0, (0.2 * q.river)) : 1.0; 674 | queue.push(q); 675 | } else { 676 | q.moisture = 0.0; 677 | } 678 | } 679 | while (queue.length > 0) { 680 | q = queue.shift(); 681 | 682 | for each (r in q.adjacent) { 683 | newMoisture = q.moisture * 0.9; 684 | if (newMoisture > r.moisture) { 685 | r.moisture = newMoisture; 686 | queue.push(r); 687 | } 688 | } 689 | } 690 | // Salt water 691 | for each (q in corners) { 692 | if (q.ocean || q.coast) { 693 | q.moisture = 1.0; 694 | } 695 | } 696 | } 697 | 698 | 699 | // Polygon moisture is the average of the moisture at corners 700 | public function assignPolygonMoisture():void { 701 | var p:Center, q:Corner, sumMoisture:Number; 702 | for each (p in centers) { 703 | sumMoisture = 0.0; 704 | for each (q in p.corners) { 705 | if (q.moisture > 1.0) q.moisture = 1.0; 706 | sumMoisture += q.moisture; 707 | } 708 | p.moisture = sumMoisture / p.corners.length; 709 | } 710 | } 711 | 712 | 713 | // Assign a biome type to each polygon. If it has 714 | // ocean/coast/water, then that's the biome; otherwise it depends 715 | // on low/high elevation and low/medium/high moisture. This is 716 | // roughly based on the Whittaker diagram but adapted to fit the 717 | // needs of the island map generator. 718 | static public function getBiome(p:Center):String { 719 | if (p.ocean) { 720 | return 'OCEAN'; 721 | } else if (p.water) { 722 | if (p.elevation < 0.1) return 'MARSH'; 723 | if (p.elevation > 0.8) return 'ICE'; 724 | return 'LAKE'; 725 | } else if (p.coast) { 726 | return 'BEACH'; 727 | } else if (p.elevation > 0.8) { 728 | if (p.moisture > 0.50) return 'SNOW'; 729 | else if (p.moisture > 0.33) return 'TUNDRA'; 730 | else if (p.moisture > 0.16) return 'BARE'; 731 | else return 'SCORCHED'; 732 | } else if (p.elevation > 0.6) { 733 | if (p.moisture > 0.66) return 'TAIGA'; 734 | else if (p.moisture > 0.33) return 'SHRUBLAND'; 735 | else return 'TEMPERATE_DESERT'; 736 | } else if (p.elevation > 0.3) { 737 | if (p.moisture > 0.83) return 'TEMPERATE_RAIN_FOREST'; 738 | else if (p.moisture > 0.50) return 'TEMPERATE_DECIDUOUS_FOREST'; 739 | else if (p.moisture > 0.16) return 'GRASSLAND'; 740 | else return 'TEMPERATE_DESERT'; 741 | } else { 742 | if (p.moisture > 0.66) return 'TROPICAL_RAIN_FOREST'; 743 | else if (p.moisture > 0.33) return 'TROPICAL_SEASONAL_FOREST'; 744 | else if (p.moisture > 0.16) return 'GRASSLAND'; 745 | else return 'SUBTROPICAL_DESERT'; 746 | } 747 | } 748 | 749 | public function assignBiomes():void { 750 | var p:Center; 751 | for each (p in centers) { 752 | p.biome = getBiome(p); 753 | } 754 | } 755 | 756 | 757 | // Look up a Voronoi Edge object given two adjacent Voronoi 758 | // polygons, or two adjacent Voronoi corners 759 | public function lookupEdgeFromCenter(p:Center, r:Center):Edge { 760 | for each (var edge:Edge in p.borders) { 761 | if (edge.d0 == r || edge.d1 == r) return edge; 762 | } 763 | return null; 764 | } 765 | 766 | public function lookupEdgeFromCorner(q:Corner, s:Corner):Edge { 767 | for each (var edge:Edge in q.protrudes) { 768 | if (edge.v0 == s || edge.v1 == s) return edge; 769 | } 770 | return null; 771 | } 772 | 773 | 774 | // Determine whether a given point should be on the island or in the water. 775 | public function inside(p:Point):Boolean { 776 | return islandShape(new Point(2*(p.x/SIZE - 0.5), 2*(p.y/SIZE - 0.5))); 777 | } 778 | } 779 | } 780 | 781 | 782 | // Factory class to build the 'inside' function that tells us whether 783 | // a point should be on the island or in the water. 784 | import flash.geom.Point; 785 | import flash.display.BitmapData; 786 | import de.polygonal.math.PM_PRNG; 787 | class IslandShape { 788 | // This class has factory functions for generating islands of 789 | // different shapes. The factory returns a function that takes a 790 | // normalized point (x and y are -1 to +1) and returns true if the 791 | // point should be on the island, and false if it should be water 792 | // (lake or ocean). 793 | 794 | 795 | // The radial island radius is based on overlapping sine waves 796 | static public var ISLAND_FACTOR:Number = 1.07; // 1.0 means no small islands; 2.0 leads to a lot 797 | static public function makeRadial(seed:int):Function { 798 | var islandRandom:PM_PRNG = new PM_PRNG(); 799 | islandRandom.seed = seed; 800 | var bumps:int = islandRandom.nextIntRange(1, 6); 801 | var startAngle:Number = islandRandom.nextDoubleRange(0, 2*Math.PI); 802 | var dipAngle:Number = islandRandom.nextDoubleRange(0, 2*Math.PI); 803 | var dipWidth:Number = islandRandom.nextDoubleRange(0.2, 0.7); 804 | 805 | function inside(q:Point):Boolean { 806 | var angle:Number = Math.atan2(q.y, q.x); 807 | var length:Number = 0.5 * (Math.max(Math.abs(q.x), Math.abs(q.y)) + q.length); 808 | 809 | var r1:Number = 0.5 + 0.40*Math.sin(startAngle + bumps*angle + Math.cos((bumps+3)*angle)); 810 | var r2:Number = 0.7 - 0.20*Math.sin(startAngle + bumps*angle - Math.sin((bumps+2)*angle)); 811 | if (Math.abs(angle - dipAngle) < dipWidth 812 | || Math.abs(angle - dipAngle + 2*Math.PI) < dipWidth 813 | || Math.abs(angle - dipAngle - 2*Math.PI) < dipWidth) { 814 | r1 = r2 = 0.2; 815 | } 816 | return (length < r1 || (length > r1*ISLAND_FACTOR && length < r2)); 817 | } 818 | 819 | return inside; 820 | } 821 | 822 | 823 | // The Perlin-based island combines perlin noise with the radius 824 | static public function makePerlin(seed:int):Function { 825 | var perlin:BitmapData = new BitmapData(256, 256); 826 | perlin.perlinNoise(64, 64, 8, seed, false, true); 827 | 828 | return function (q:Point):Boolean { 829 | var c:Number = (perlin.getPixel(int((q.x+1)*128), int((q.y+1)*128)) & 0xff) / 255.0; 830 | return c > (0.3+0.3*q.length*q.length); 831 | }; 832 | } 833 | 834 | 835 | // The square shape fills the entire space with land 836 | static public function makeSquare(seed:int):Function { 837 | return function (q:Point):Boolean { 838 | return true; 839 | }; 840 | } 841 | 842 | 843 | // The blob island is shaped like Amit's blob logo 844 | static public function makeBlob(seed:int):Function { 845 | return function(q:Point):Boolean { 846 | var eye1:Boolean = new Point(q.x-0.2, q.y/2+0.2).length < 0.05; 847 | var eye2:Boolean = new Point(q.x+0.2, q.y/2+0.2).length < 0.05; 848 | var body:Boolean = q.length < 0.8 - 0.18*Math.sin(5*Math.atan2(q.y, q.x)); 849 | return body && !eye1 && !eye2; 850 | }; 851 | } 852 | 853 | } 854 | 855 | 856 | // Factory class to choose points for the graph 857 | import flash.geom.Point; 858 | import flash.geom.Rectangle; 859 | import com.nodename.Delaunay.Voronoi; 860 | import de.polygonal.math.PM_PRNG; 861 | class PointSelector { 862 | static public var NUM_LLOYD_RELAXATIONS:int = 2; 863 | 864 | // The square and hex grid point selection remove randomness from 865 | // where the points are; we need to inject more randomness elsewhere 866 | // to make the maps look better. I do this in the corner 867 | // elevations. However I think more experimentation is needed. 868 | static public function needsMoreRandomness(type:String):Boolean { 869 | return type == 'Square' || type == 'Hexagon'; 870 | } 871 | 872 | 873 | // Generate points at random locations 874 | static public function generateRandom(size:int, seed:int):Function { 875 | return function(numPoints:int):Vector. { 876 | var mapRandom:PM_PRNG = new PM_PRNG(); 877 | mapRandom.seed = seed; 878 | var p:Point, i:int, points:Vector. = new Vector.(); 879 | for (i = 0; i < numPoints; i++) { 880 | p = new Point(mapRandom.nextDoubleRange(10, size-10), 881 | mapRandom.nextDoubleRange(10, size-10)); 882 | points.push(p); 883 | } 884 | return points; 885 | } 886 | } 887 | 888 | 889 | // Improve the random set of points with Lloyd Relaxation 890 | static public function generateRelaxed(size:int, seed:int):Function { 891 | return function(numPoints:int):Vector. { 892 | // We'd really like to generate "blue noise". Algorithms: 893 | // 1. Poisson dart throwing: check each new point against all 894 | // existing points, and reject it if it's too close. 895 | // 2. Start with a hexagonal grid and randomly perturb points. 896 | // 3. Lloyd Relaxation: move each point to the centroid of the 897 | // generated Voronoi polygon, then generate Voronoi again. 898 | // 4. Use force-based layout algorithms to push points away. 899 | // 5. More at http://www.cs.virginia.edu/~gfx/pubs/antimony/ 900 | // Option 3 is implemented here. If it's run for too many iterations, 901 | // it will turn into a grid, but convergence is very slow, and we only 902 | // run it a few times. 903 | var i:int, p:Point, q:Point, voronoi:Voronoi, region:Vector.; 904 | var points:Vector. = generateRandom(size, seed)(numPoints); 905 | for (i = 0; i < NUM_LLOYD_RELAXATIONS; i++) { 906 | voronoi = new Voronoi(points, null, new Rectangle(0, 0, size, size)); 907 | for each (p in points) { 908 | region = voronoi.region(p); 909 | p.x = 0.0; 910 | p.y = 0.0; 911 | for each (q in region) { 912 | p.x += q.x; 913 | p.y += q.y; 914 | } 915 | p.x /= region.length; 916 | p.y /= region.length; 917 | region.splice(0, region.length); 918 | } 919 | voronoi.dispose(); 920 | } 921 | return points; 922 | } 923 | } 924 | 925 | 926 | // Generate points on a square grid 927 | static public function generateSquare(size:int, seed:int):Function { 928 | return function(numPoints:int):Vector. { 929 | var points:Vector. = new Vector.(); 930 | var N:int = Math.sqrt(numPoints); 931 | for (var x:int = 0; x < N; x++) { 932 | for (var y:int = 0; y < N; y++) { 933 | points.push(new Point((0.5 + x)/N * size, (0.5 + y)/N * size)); 934 | } 935 | } 936 | return points; 937 | } 938 | } 939 | 940 | 941 | // Generate points on a hexagon grid 942 | static public function generateHexagon(size:int, seed:int):Function { 943 | return function(numPoints:int):Vector. { 944 | var points:Vector. = new Vector.(); 945 | var N:int = Math.sqrt(numPoints); 946 | for (var x:int = 0; x < N; x++) { 947 | for (var y:int = 0; y < N; y++) { 948 | points.push(new Point((0.5 + x)/N * size, (0.25 + 0.5 * (x%2) + y)/N * size)); 949 | } 950 | } 951 | return points; 952 | } 953 | } 954 | } 955 | -------------------------------------------------------------------------------- /NoisyEdges.as: -------------------------------------------------------------------------------- 1 | // Annotate each edge with a noisy path, to make maps look more interesting. 2 | // Author: amitp@cs.stanford.edu 3 | // License: MIT 4 | 5 | package { 6 | import flash.geom.Point; 7 | import graph.*; 8 | import de.polygonal.math.PM_PRNG; 9 | 10 | public class NoisyEdges { 11 | static public var NOISY_LINE_TRADEOFF:Number = 0.5; // low: jagged vedge; high: jagged dedge 12 | 13 | public var path0:Array = []; // edge index -> Vector. 14 | public var path1:Array = []; // edge index -> Vector. 15 | 16 | public function NoisyEdges() { 17 | } 18 | 19 | // Build noisy line paths for each of the Voronoi edges. There are 20 | // two noisy line paths for each edge, each covering half the 21 | // distance: path0 is from v0 to the midpoint and path1 is from v1 22 | // to the midpoint. When drawing the polygons, one or the other 23 | // must be drawn in reverse order. 24 | public function buildNoisyEdges(map:Map, lava:Lava, random:PM_PRNG):void { 25 | var p:Center, edge:Edge; 26 | for each (p in map.centers) { 27 | for each (edge in p.borders) { 28 | if (edge.d0 && edge.d1 && edge.v0 && edge.v1 && !path0[edge.index]) { 29 | var f:Number = NOISY_LINE_TRADEOFF; 30 | var t:Point = Point.interpolate(edge.v0.point, edge.d0.point, f); 31 | var q:Point = Point.interpolate(edge.v0.point, edge.d1.point, f); 32 | var r:Point = Point.interpolate(edge.v1.point, edge.d0.point, f); 33 | var s:Point = Point.interpolate(edge.v1.point, edge.d1.point, f); 34 | 35 | var minLength:int = 10; 36 | if (edge.d0.biome != edge.d1.biome) minLength = 3; 37 | if (edge.d0.ocean && edge.d1.ocean) minLength = 100; 38 | if (edge.d0.coast || edge.d1.coast) minLength = 1; 39 | if (edge.river || lava.lava[edge.index]) minLength = 1; 40 | 41 | path0[edge.index] = buildNoisyLineSegments(random, edge.v0.point, t, edge.midpoint, q, minLength); 42 | path1[edge.index] = buildNoisyLineSegments(random, edge.v1.point, s, edge.midpoint, r, minLength); 43 | } 44 | } 45 | } 46 | } 47 | 48 | 49 | // Helper function: build a single noisy line in a quadrilateral A-B-C-D, 50 | // and store the output points in a Vector. 51 | static public function buildNoisyLineSegments(random:PM_PRNG, A:Point, B:Point, C:Point, D:Point, minLength:Number):Vector. { 52 | var points:Vector. = new Vector.(); 53 | 54 | function subdivide(A:Point, B:Point, C:Point, D:Point):void { 55 | if (A.subtract(C).length < minLength || B.subtract(D).length < minLength) { 56 | return; 57 | } 58 | 59 | // Subdivide the quadrilateral 60 | var p:Number = random.nextDoubleRange(0.2, 0.8); // vertical (along A-D and B-C) 61 | var q:Number = random.nextDoubleRange(0.2, 0.8); // horizontal (along A-B and D-C) 62 | 63 | // Midpoints 64 | var E:Point = Point.interpolate(A, D, p); 65 | var F:Point = Point.interpolate(B, C, p); 66 | var G:Point = Point.interpolate(A, B, q); 67 | var I:Point = Point.interpolate(D, C, q); 68 | 69 | // Central point 70 | var H:Point = Point.interpolate(E, F, q); 71 | 72 | // Divide the quad into subquads, but meet at H 73 | var s:Number = 1.0 - random.nextDoubleRange(-0.4, +0.4); 74 | var t:Number = 1.0 - random.nextDoubleRange(-0.4, +0.4); 75 | 76 | subdivide(A, Point.interpolate(G, B, s), H, Point.interpolate(E, D, t)); 77 | points.push(H); 78 | subdivide(H, Point.interpolate(F, C, s), C, Point.interpolate(I, D, t)); 79 | } 80 | 81 | points.push(A); 82 | subdivide(A, B, C, D); 83 | points.push(C); 84 | return points; 85 | } 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 2 | 3 | After working on a [Perlin-noise-based map 4 | generator](http://simblob.blogspot.com/2010/01/simple-map-generation.html) 5 | I had wanted something with islands and rivers and volcanoes and 6 | lava. However, I had a lot of trouble getting that map generator to 7 | generate any more than what it did at first. This project was my 8 | exploration of several different techniques for map generation. 9 | 10 | The goal is to make continent/island style maps (surrounded by water) 11 | that can be used by a variety of games. I had originally intended to 12 | write a reusable C++ library but ended up writing the code in 13 | Actionscript. 14 | 15 | The most important features I want are nice island/continent 16 | coastlines, mountains, and rivers. Non goals include impassable areas 17 | (except for the ocean), maze-like structures, or realistic height 18 | maps. The high level approach is to 19 | 20 | 1. Make a coastline. 21 | 2. Set elevation to distance from coastline. Mountains are farthest from the coast. 22 | 3. Create rivers in valleys, flowing down to the coast. 23 | 24 | The implementation generates a vector map with roughly 1,000 polygons, 25 | instead of a tile/grid map with roughly 1,000,000 tiles. In games the 26 | polygons can be used for distinct areas with their own story and 27 | personality, places for towns and resources, quest locations, 28 | conquerable territory, etc. Polygon boundaries are used for 29 | rivers. Polygon-to-polygon routes are used for roads. Forests, oceans, 30 | rivers, swamps, etc. can be named. Polygons are rendered into a bitmap 31 | to produce the tile map, but the underlying polygon structure is still 32 | available. 33 | 34 | The [full process is described here](http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/). 35 | 36 | History 37 | ------- 38 | 39 | * I started out with C++ code that used mountains, soil erosion, water flow, water erosion, water evaporation, volanoes, lava flow, and other physical processes to sculpt terrain expressed in a 2d array of tiles. However as described [in this blog post](http://simblob.blogspot.com/2010/06/teleological-vs-ontogenetic-map.html) I decided to abandon this approach. 40 | 41 | * Since my initial approach failed, I wrote several small prototypes to figure out how to make rivers, coastlines, and mountains. These are the key features I want to support. I will then figure out how to combine them into a map. 42 | 43 | * The voronoi_set.as prototype worked well and I continued adding to it (instead of converting to C++). It supports terrain types: ocean, land, beach, lake, forest, swamp, desert, ice, rocky, grassland, savannah. It has rivers and roads. I decided not to convert it to C++ for now. Instead, I've refactored it into the core map generation (Map.as), display and GUI (mapgen2.as), graph representation (graph/*.as), decorative elements (Roads.as, Lava.as), and noisy edge generation (NoisyEdges.as). 44 | 45 | 46 | Requirements 47 | ------------ 48 | 49 | These third-party requirements have been added to the ``third-party`` directory: 50 | 51 | * [as3delaunay](https://github.com/nodename/as3delaunay) for the Voronoi algorithm 52 | * [as3corelib](https://github.com/mikechambers/as3corelib) for PNG export 53 | * The AS3 version of [de.polygonal.math.PM_PRNG.as](http://lab.polygonal.de/2007/04/21/a-good-pseudo-random-number-generator-prng/) for consistent random numbers 54 | 55 | Make sure you run ``git submodule update --init`` to check out the third-party libraries. 56 | 57 | Compiling 58 | --------- 59 | 60 | To compile ``mapgen2.as`` to ``mapgen2.swf``, use the following command: 61 | 62 | mxmlc -source-path+=third-party/PM_PRNG -source-path+=third-party/as3delaunay/src -source-path+=third-party/as3corelib/src mapgen2.as -static-link-runtime-shared-libraries 63 | 64 | -------------------------------------------------------------------------------- /Roads.as: -------------------------------------------------------------------------------- 1 | // Place roads on the polygonal island map roughly following contour lines. 2 | // Author: amitp@cs.stanford.edu 3 | // License: MIT 4 | 5 | package { 6 | import graph.*; 7 | 8 | public class Roads { 9 | // The road array marks the edges that are roads. The mark is 1, 10 | // 2, or 3, corresponding to the three contour levels. Note that 11 | // these are sparse arrays, only filled in where there are roads. 12 | public var road:Array; // edge index -> int contour level 13 | public var roadConnections:Array; // center index -> array of Edges with roads 14 | 15 | public function Roads() { 16 | road = []; 17 | roadConnections = []; 18 | } 19 | 20 | 21 | // We want to mark different elevation zones so that we can draw 22 | // island-circling roads that divide the areas. 23 | public function createRoads(map:Map):void { 24 | // Oceans and coastal polygons are the lowest contour zone 25 | // (1). Anything connected to contour level K, if it's below 26 | // elevation threshold K, or if it's water, gets contour level 27 | // K. (2) Anything not assigned a contour level, and connected 28 | // to contour level K, gets contour level K+1. 29 | var queue:Array = []; 30 | var p:Center, q:Corner, r:Center, edge:Edge, newLevel:int; 31 | var elevationThresholds:Array = [0, 0.05, 0.37, 0.64]; 32 | var cornerContour:Array = []; // corner index -> int contour level 33 | var centerContour:Array = []; // center index -> int contour level 34 | 35 | for each (p in map.centers) { 36 | if (p.coast || p.ocean) { 37 | centerContour[p.index] = 1; 38 | queue.push(p); 39 | } 40 | } 41 | 42 | while (queue.length > 0) { 43 | p = queue.shift(); 44 | for each (r in p.neighbors) { 45 | newLevel = centerContour[p.index] || 0; 46 | while (r.elevation > elevationThresholds[newLevel] && !r.water) { 47 | // NOTE: extend the contour line past bodies of 48 | // water so that roads don't terminate inside lakes. 49 | newLevel += 1; 50 | } 51 | if (newLevel < (centerContour[r.index] || 999)) { 52 | centerContour[r.index] = newLevel; 53 | queue.push(r); 54 | } 55 | } 56 | } 57 | 58 | // A corner's contour level is the MIN of its polygons 59 | for each (p in map.centers) { 60 | for each (q in p.corners) { 61 | cornerContour[q.index] = Math.min(cornerContour[q.index] || 999, 62 | centerContour[p.index] || 999); 63 | } 64 | } 65 | 66 | // Roads go between polygons that have different contour levels 67 | for each (p in map.centers) { 68 | for each (edge in p.borders) { 69 | if (edge.v0 && edge.v1 70 | && cornerContour[edge.v0.index] != cornerContour[edge.v1.index]) { 71 | road[edge.index] = Math.min(cornerContour[edge.v0.index], 72 | cornerContour[edge.v1.index]); 73 | if (!roadConnections[p.index]) { 74 | roadConnections[p.index] = []; 75 | } 76 | roadConnections[p.index].push(edge); 77 | } 78 | } 79 | } 80 | } 81 | 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /RoadsSpanningTree.as: -------------------------------------------------------------------------------- 1 | // Place roads on the polygonal island map using a minimal spanning tree 2 | // Author: amitp@cs.stanford.edu 3 | // License: MIT 4 | 5 | package { 6 | import graph.*; 7 | 8 | public class RoadsSpanningTree { 9 | // The road array marks the edges that are roads. 10 | public var road:Array; // edge index -> boolean 11 | public var roadConnections:Array; // center index -> array of Edges with roads 12 | 13 | public function RoadsSpanningTree() { 14 | road = []; 15 | roadConnections = []; 16 | } 17 | 18 | 19 | public function createRoads(map:Map):void { 20 | var status:Array = []; // index -> status (undefined for unvisited, 'fringe', or 'closed') 21 | var fringe:Array = []; // locations that are still being analyzed 22 | var p:Center, q:Corner, r:Center, edge:Edge, i:int; 23 | 24 | // Initialize 25 | for each (edge in map.edges) { 26 | road[edge.index] = 0; 27 | } 28 | 29 | // Start with the highest elevation Center -- everything else 30 | // will connect to this location 31 | r = map.centers[0]; 32 | for each (p in map.centers) { 33 | if (p.elevation > r.elevation) { 34 | r = p; 35 | } 36 | } 37 | status[r.index] = 'fringe'; 38 | fringe = [r]; 39 | 40 | while (fringe.length > 0) { 41 | // Pick a node randomly. Also interesting is to always pick the first or last node. 42 | i = Math.floor(Math.random() * fringe.length); 43 | // i = 0; 44 | // i = fringe.length - 1; 45 | if (i > 0 && Math.random() < 0.5) i -= 1; 46 | p = fringe[i]; 47 | fringe[i] = fringe[0]; 48 | fringe.shift(); 49 | status[p.index] = 'closed'; 50 | 51 | for each (edge in p.borders) { 52 | r = (edge.d0 == p)? edge.d1 : edge.d0; 53 | if (r && !r.water) { 54 | if (!status[r.index]) { 55 | // We've never been here, so let's add this to the fringe 56 | status[r.index] = 'fringe'; 57 | fringe.push(r); 58 | road[edge.index] = 1; 59 | } else if (status[r.index] == 'fringe') { 60 | // We've been here -- what if the cost is lower? TODO: ignore for now 61 | } 62 | } 63 | } 64 | } 65 | 66 | // Build the roadConnections list from roads 67 | for each (edge in map.edges) { 68 | if (road[edge.index] > 0) { 69 | for each (p in [edge.d0, edge.d1]) { 70 | if (p) { 71 | if (!roadConnections[p.index]) { 72 | roadConnections[p.index] = []; 73 | } 74 | roadConnections[p.index].push(edge); 75 | } 76 | } 77 | } 78 | } 79 | // Rebuild roads from roadConnections 80 | for each (edge in map.edges) { 81 | if (road[edge.index] > 0) { 82 | for each (p in [edge.d0, edge.d1]) { 83 | if (p) { 84 | road[edge.index] = Math.max(road[edge.index], roadConnections[p.index].length); 85 | } 86 | } 87 | } 88 | road[edge.index] = Math.min(3, Math.max(0, road[edge.index] - 2)); 89 | } 90 | } 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /Watersheds.as: -------------------------------------------------------------------------------- 1 | // Define watersheds: if a drop of rain falls on any polygon, where 2 | // does it exit the island? We follow the map corner downslope field. 3 | // Author: amitp@cs.stanford.edu 4 | // License: MIT 5 | 6 | package { 7 | import graph.*; 8 | 9 | public class Watersheds { 10 | public var lowestCorner:Array = []; // polygon index -> corner index 11 | public var watersheds:Array = []; // polygon index -> corner index 12 | 13 | // We want to mark each polygon with the corner where water would 14 | // exit the island. 15 | public function createWatersheds(map:Map):void { 16 | var p:Center, q:Corner, s:Corner; 17 | 18 | // Find the lowest corner of the polygon, and set that as the 19 | // exit point for rain falling on this polygon 20 | for each (p in map.centers) { 21 | s = null; 22 | for each (q in p.corners) { 23 | if (s == null || q.elevation < s.elevation) { 24 | s = q; 25 | } 26 | } 27 | lowestCorner[p.index] = (s == null)? -1 : s.index; 28 | watersheds[p.index] = (s == null)? -1 : (s.watershed == null)? -1 : s.watershed.index; 29 | } 30 | } 31 | 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /graph/Center.as: -------------------------------------------------------------------------------- 1 | package graph { 2 | import flash.geom.Point; 3 | 4 | public class Center { 5 | public var index:int; 6 | 7 | public var point:Point; // location 8 | public var water:Boolean; // lake or ocean 9 | public var ocean:Boolean; // ocean 10 | public var coast:Boolean; // land polygon touching an ocean 11 | public var border:Boolean; // at the edge of the map 12 | public var biome:String; // biome type (see article) 13 | public var elevation:Number; // 0.0-1.0 14 | public var moisture:Number; // 0.0-1.0 15 | 16 | public var neighbors:Vector.
; 17 | public var borders:Vector.; 18 | public var corners:Vector.; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /graph/Corner.as: -------------------------------------------------------------------------------- 1 | package graph { 2 | import flash.geom.Point; 3 | 4 | public class Corner { 5 | public var index:int; 6 | 7 | public var point:Point; // location 8 | public var ocean:Boolean; // ocean 9 | public var water:Boolean; // lake or ocean 10 | public var coast:Boolean; // touches ocean and land polygons 11 | public var border:Boolean; // at the edge of the map 12 | public var elevation:Number; // 0.0-1.0 13 | public var moisture:Number; // 0.0-1.0 14 | 15 | public var touches:Vector.
; 16 | public var protrudes:Vector.; 17 | public var adjacent:Vector.; 18 | 19 | public var river:int; // 0 if no river, or volume of water in river 20 | public var downslope:Corner; // pointer to adjacent corner most downhill 21 | public var watershed:Corner; // pointer to coastal corner, or null 22 | public var watershed_size:int; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /graph/Edge.as: -------------------------------------------------------------------------------- 1 | package graph { 2 | import flash.geom.Point; 3 | 4 | public class Edge { 5 | public var index:int; 6 | public var d0:Center, d1:Center; // Delaunay edge 7 | public var v0:Corner, v1:Corner; // Voronoi edge 8 | public var midpoint:Point; // halfway between v0,v1 9 | public var river:int; // volume of water, or 0 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /mapgen2.as: -------------------------------------------------------------------------------- 1 | // Display the voronoi graph produced in Map.as 2 | // Author: amitp@cs.stanford.edu 3 | // License: MIT 4 | 5 | package { 6 | import graph.*; 7 | import flash.geom.*; 8 | import flash.display.*; 9 | import flash.events.*; 10 | import flash.text.*; 11 | import flash.utils.ByteArray; 12 | import flash.utils.getTimer; 13 | import flash.utils.Timer; 14 | import flash.net.FileReference; 15 | import flash.system.System; 16 | import de.polygonal.math.PM_PRNG; 17 | import com.adobe.images.PNGEncoder; 18 | 19 | [SWF(width="800", height="600", frameRate=60)] 20 | public class mapgen2 extends Sprite { 21 | static public var SIZE:int = 600; 22 | 23 | static public var displayColors:Object = { 24 | // Features 25 | OCEAN: 0x44447a, 26 | COAST: 0x33335a, 27 | LAKESHORE: 0x225588, 28 | LAKE: 0x336699, 29 | RIVER: 0x225588, 30 | MARSH: 0x2f6666, 31 | ICE: 0x99ffff, 32 | BEACH: 0xa09077, 33 | ROAD1: 0x442211, 34 | ROAD2: 0x553322, 35 | ROAD3: 0x664433, 36 | BRIDGE: 0x686860, 37 | LAVA: 0xcc3333, 38 | 39 | // Terrain 40 | SNOW: 0xffffff, 41 | TUNDRA: 0xbbbbaa, 42 | BARE: 0x888888, 43 | SCORCHED: 0x555555, 44 | TAIGA: 0x99aa77, 45 | SHRUBLAND: 0x889977, 46 | TEMPERATE_DESERT: 0xc9d29b, 47 | TEMPERATE_RAIN_FOREST: 0x448855, 48 | TEMPERATE_DECIDUOUS_FOREST: 0x679459, 49 | GRASSLAND: 0x88aa55, 50 | SUBTROPICAL_DESERT: 0xd2b98b, 51 | TROPICAL_RAIN_FOREST: 0x337755, 52 | TROPICAL_SEASONAL_FOREST: 0x559944 53 | }; 54 | 55 | static public var elevationGradientColors:Object = { 56 | OCEAN: 0x008800, 57 | GRADIENT_LOW: 0x008800, 58 | GRADIENT_HIGH: 0xffff00 59 | }; 60 | 61 | static public var moistureGradientColors:Object = { 62 | OCEAN: 0x4466ff, 63 | GRADIENT_LOW: 0xbbaa33, 64 | GRADIENT_HIGH: 0x4466ff 65 | }; 66 | 67 | // Island shape is controlled by the islandRandom seed and the 68 | // type of island. The islandShape function uses both of them to 69 | // determine whether any point should be water or land. 70 | public var islandType:String = 'Perlin'; 71 | static public var islandSeedInitial:String = "85882-8"; 72 | 73 | // Point distribution 74 | public var pointType:String = 'Relaxed'; 75 | public var numPoints:int = 2000; 76 | 77 | // GUI for controlling the map generation and view 78 | public var controls:Sprite = new Sprite(); 79 | public var islandSeedInput:TextField; 80 | public var statusBar:TextField; 81 | 82 | // This is the current map style. UI buttons change this, and it 83 | // persists when you make a new map. The timer is used only when 84 | // the map mode is '3d'. 85 | public var mapMode:String = 'smooth'; 86 | public var render3dTimer:Timer = new Timer(1000/20, 0); 87 | public var noiseLayer:Bitmap = new Bitmap(new BitmapData(SIZE, SIZE)); 88 | 89 | // These store 3d rendering data 90 | private var rotationAnimation:Number = 0.0; 91 | private var triangles3d:Array = []; 92 | private var graphicsData:Vector.; 93 | 94 | // The map data 95 | public var map:Map; 96 | public var roads:Roads; 97 | public var lava:Lava; 98 | public var watersheds:Watersheds; 99 | public var noisyEdges:NoisyEdges; 100 | 101 | 102 | public function mapgen2() { 103 | stage.scaleMode = 'noScale'; 104 | stage.align = 'TL'; 105 | 106 | addChild(noiseLayer); 107 | noiseLayer.bitmapData.noise(555, 128-10, 128+10, 7, true); 108 | noiseLayer.blendMode = BlendMode.HARDLIGHT; 109 | 110 | controls.x = SIZE; 111 | addChild(controls); 112 | 113 | addExportButtons(); 114 | addViewButtons(); 115 | addIslandShapeButtons(); 116 | addPointSelectionButtons(); 117 | addMiscLabels(); 118 | 119 | map = new Map(SIZE); 120 | go(islandType, pointType, numPoints); 121 | 122 | render3dTimer.addEventListener(TimerEvent.TIMER, function (e:TimerEvent):void { 123 | // TODO: don't draw this while the map is being built 124 | drawMap(mapMode); 125 | }); 126 | } 127 | 128 | 129 | // Random parameters governing the overall shape of the island 130 | public function newIsland(newIslandType:String, newPointType:String, newNumPoints:int):void { 131 | var seed:int = 0, variant:int = 0; 132 | var t:Number = getTimer(); 133 | 134 | if (islandSeedInput.text.length == 0) { 135 | islandSeedInput.text = (Math.random()*100000).toFixed(0); 136 | } 137 | 138 | var match:Object = /\s*(\d+)(?:\-(\d+))\s*$/.exec(islandSeedInput.text); 139 | if (match != null) { 140 | // It's of the format SHAPE-VARIANT 141 | seed = parseInt(match[1]); 142 | variant = parseInt(match[2] || "0"); 143 | } 144 | if (seed == 0) { 145 | // Convert the string into a number. This is a cheesy way to 146 | // do it but it doesn't matter. It just allows people to use 147 | // words as seeds. 148 | for (var i:int = 0; i < islandSeedInput.text.length; i++) { 149 | seed = (seed << 4) | islandSeedInput.text.charCodeAt(i); 150 | } 151 | seed %= 100000; 152 | variant = 1+Math.floor(9*Math.random()); 153 | } 154 | islandType = newIslandType; 155 | pointType = newPointType; 156 | numPoints = newNumPoints; 157 | map.newIsland(islandType, pointType, numPoints, seed, variant); 158 | } 159 | 160 | 161 | public function graphicsReset():void { 162 | triangles3d = []; 163 | graphics.clear(); 164 | graphics.beginFill(0xbbbbaa); 165 | graphics.drawRect(0, 0, 2000, 2000); 166 | graphics.endFill(); 167 | graphics.beginFill(displayColors.OCEAN); 168 | graphics.drawRect(0, 0, SIZE, SIZE); 169 | graphics.endFill(); 170 | } 171 | 172 | 173 | public function go(newIslandType:String, newPointType:String, newNumPoints:int):void { 174 | cancelCommands(); 175 | 176 | roads = new Roads(); 177 | lava = new Lava(); 178 | watersheds = new Watersheds(); 179 | noisyEdges = new NoisyEdges(); 180 | 181 | commandExecute("Shaping map...", 182 | function():void { 183 | newIsland(newIslandType, newPointType, newNumPoints); 184 | }); 185 | 186 | commandExecute("Placing points...", 187 | function():void { 188 | map.go(0, 1); 189 | drawMap('polygons'); 190 | }); 191 | 192 | commandExecute("Building graph...", 193 | function():void { 194 | map.go(1, 2); 195 | map.assignBiomes(); 196 | drawMap('polygons'); 197 | }); 198 | 199 | commandExecute("Features...", 200 | function():void { 201 | map.go(2, 5); 202 | map.assignBiomes(); 203 | drawMap('polygons'); 204 | }); 205 | 206 | commandExecute("Edges...", 207 | function():void { 208 | roads.createRoads(map); 209 | // lava.createLava(map, map.mapRandom.nextDouble); 210 | watersheds.createWatersheds(map); 211 | noisyEdges.buildNoisyEdges(map, lava, map.mapRandom); 212 | drawMap(mapMode); 213 | }); 214 | } 215 | 216 | 217 | // Command queue is processed on ENTER_FRAME. If it's empty, 218 | // remove the handler. 219 | private var _guiQueue:Array = []; 220 | private function _onEnterFrame(e:Event):void { 221 | (_guiQueue.shift()[1])(); 222 | if (_guiQueue.length == 0) { 223 | stage.removeEventListener(Event.ENTER_FRAME, _onEnterFrame); 224 | statusBar.text = ""; 225 | } else { 226 | statusBar.text = _guiQueue[0][0]; 227 | } 228 | } 229 | 230 | public function cancelCommands():void { 231 | if (_guiQueue.length != 0) { 232 | stage.removeEventListener(Event.ENTER_FRAME, _onEnterFrame); 233 | statusBar.text = ""; 234 | _guiQueue = []; 235 | } 236 | } 237 | 238 | public function commandExecute(status:String, command:Function):void { 239 | if (_guiQueue.length == 0) { 240 | statusBar.text = status; 241 | stage.addEventListener(Event.ENTER_FRAME, _onEnterFrame); 242 | } 243 | _guiQueue.push([status, command]); 244 | } 245 | 246 | 247 | // Show some information about the maps 248 | private static var _biomeMap:Array = 249 | ['BEACH', 'LAKE', 'ICE', 'MARSH', 'SNOW', 'TUNDRA', 'BARE', 'SCORCHED', 250 | 'TAIGA', 'SHRUBLAND', 'TEMPERATE_DESERT', 'TEMPERATE_RAIN_FOREST', 251 | 'TEMPERATE_DECIDUOUS_FOREST', 'GRASSLAND', 'SUBTROPICAL_DESERT', 252 | 'TROPICAL_RAIN_FOREST', 'TROPICAL_SEASONAL_FOREST']; 253 | public function drawHistograms():void { 254 | // There are pairs of functions for each chart. The bucket 255 | // function maps the polygon Center to a small int, and the 256 | // color function maps the int to a color. 257 | function landTypeBucket(p:Center):int { 258 | if (p.ocean) return 1; 259 | else if (p.coast) return 2; 260 | else if (p.water) return 3; 261 | else return 4; 262 | } 263 | function landTypeColor(bucket:int):uint { 264 | if (bucket == 1) return displayColors.OCEAN; 265 | else if (bucket == 2) return displayColors.BEACH; 266 | else if (bucket == 3) return displayColors.LAKE; 267 | else return displayColors.TEMPERATE_DECIDUOUS_FOREST; 268 | } 269 | function elevationBucket(p:Center):int { 270 | if (p.ocean) return -1; 271 | else return Math.floor(p.elevation*10); 272 | } 273 | function elevationColor(bucket:int):uint { 274 | return interpolateColor(displayColors.TEMPERATE_DECIDUOUS_FOREST, 275 | displayColors.GRASSLAND, bucket*0.1); 276 | } 277 | function moistureBucket(p:Center):int { 278 | if (p.water) return -1; 279 | else return Math.floor(p.moisture*10); 280 | } 281 | function moistureColor(bucket:int):uint { 282 | return interpolateColor(displayColors.BEACH, displayColors.RIVER, bucket*0.1); 283 | } 284 | function biomeBucket(p:Center):int { 285 | return _biomeMap.indexOf(p.biome); 286 | } 287 | function biomeColor(bucket:int):uint { 288 | return displayColors[_biomeMap[bucket]]; 289 | } 290 | 291 | function computeHistogram(bucketFn:Function):Array { 292 | var p:Center, histogram:Array, bucket:int; 293 | histogram = []; 294 | for each (p in map.centers) { 295 | bucket = bucketFn(p); 296 | if (bucket >= 0) histogram[bucket] = (histogram[bucket] || 0) + 1; 297 | } 298 | return histogram; 299 | } 300 | 301 | function drawHistogram(x:Number, y:Number, bucketFn:Function, colorFn:Function, 302 | width:Number, height:Number):void { 303 | var scale:Number, i:int; 304 | var histogram:Array = computeHistogram(bucketFn); 305 | 306 | scale = 0.0; 307 | for (i = 0; i < histogram.length; i++) { 308 | scale = Math.max(scale, histogram[i] || 0); 309 | } 310 | for (i = 0; i < histogram.length; i++) { 311 | if (histogram[i]) { 312 | graphics.beginFill(colorFn(i)); 313 | graphics.drawRect(SIZE+x+i*width/histogram.length, y+height, 314 | Math.max(0, width/histogram.length-1), -height*histogram[i]/scale); 315 | graphics.endFill(); 316 | } 317 | } 318 | } 319 | 320 | function drawDistribution(x:Number, y:Number, bucketFn:Function, colorFn:Function, 321 | width:Number, height:Number):void { 322 | var scale:Number, i:int, w:Number; 323 | var histogram:Array = computeHistogram(bucketFn); 324 | 325 | scale = 0.0; 326 | for (i = 0; i < histogram.length; i++) { 327 | scale += histogram[i] || 0.0; 328 | } 329 | for (i = 0; i < histogram.length; i++) { 330 | if (histogram[i]) { 331 | graphics.beginFill(colorFn(i)); 332 | w = histogram[i]/scale*width; 333 | graphics.drawRect(SIZE+x, y, Math.max(0, w-1), height); 334 | x += w; 335 | graphics.endFill(); 336 | } 337 | } 338 | } 339 | 340 | var x:Number = 23, y:Number = 200, width:Number = 154; 341 | drawDistribution(x, y, landTypeBucket, landTypeColor, width, 20); 342 | drawDistribution(x, y+25, biomeBucket, biomeColor, width, 20); 343 | 344 | drawHistogram(x, y+55, elevationBucket, elevationColor, width, 30); 345 | drawHistogram(x, y+95, moistureBucket, moistureColor, width, 20); 346 | } 347 | 348 | 349 | // Helper functions for rendering paths 350 | private function drawPathForwards(graphics:Graphics, path:Vector.):void { 351 | for (var i:int = 0; i < path.length; i++) { 352 | graphics.lineTo(path[i].x, path[i].y); 353 | } 354 | } 355 | private function drawPathBackwards(graphics:Graphics, path:Vector.):void { 356 | for (var i:int = path.length-1; i >= 0; i--) { 357 | graphics.lineTo(path[i].x, path[i].y); 358 | } 359 | } 360 | 361 | 362 | // Helper function for color manipulation. When f==0: color0, f==1: color1 363 | private function interpolateColor(color0:uint, color1:uint, f:Number):uint { 364 | var r:uint = uint((1-f)*(color0 >> 16) + f*(color1 >> 16)); 365 | var g:uint = uint((1-f)*((color0 >> 8) & 0xff) + f*((color1 >> 8) & 0xff)); 366 | var b:uint = uint((1-f)*(color0 & 0xff) + f*(color1 & 0xff)); 367 | if (r > 255) r = 255; 368 | if (g > 255) g = 255; 369 | if (b > 255) b = 255; 370 | return (r << 16) | (g << 8) | b; 371 | } 372 | 373 | 374 | // Helper function for drawing triangles with gradients. This 375 | // function sets up the fill on the graphics object, and then 376 | // calls fillFunction to draw the desired path. 377 | private function drawGradientTriangle(graphics:Graphics, 378 | v1:Vector3D, v2:Vector3D, v3:Vector3D, 379 | colors:Array, fillFunction:Function):void { 380 | var m:Matrix = new Matrix(); 381 | 382 | // Center of triangle: 383 | var V:Vector3D = v1.add(v2).add(v3); 384 | V.scaleBy(1/3.0); 385 | 386 | // Normal of the plane containing the triangle: 387 | var N:Vector3D = v2.subtract(v1).crossProduct(v3.subtract(v1)); 388 | N.normalize(); 389 | 390 | // Gradient vector in x-y plane pointing in the direction of increasing z 391 | var G:Vector3D = new Vector3D(-N.x/N.z, -N.y/N.z, 0); 392 | 393 | // Center of the color gradient 394 | var C:Vector3D = new Vector3D(V.x - G.x*((V.z-0.5)/G.length/G.length), V.y - G.y*((V.z-0.5)/G.length/G.length)); 395 | 396 | if (G.length < 1e-6) { 397 | // If the gradient vector is small, there's not much 398 | // difference in colors across this triangle. Use a plain 399 | // fill, because the numeric accuracy of 1/G.length is not to 400 | // be trusted. NOTE: only works for 1, 2, 3 colors in the array 401 | var color:uint = colors[0]; 402 | if (colors.length == 2) { 403 | color = interpolateColor(colors[0], colors[1], V.z); 404 | } else if (colors.length == 3) { 405 | if (V.z < 0.5) { 406 | color = interpolateColor(colors[0], colors[1], V.z*2); 407 | } else { 408 | color = interpolateColor(colors[1], colors[2], V.z*2-1); 409 | } 410 | } 411 | graphics.beginFill(color); 412 | } else { 413 | // The gradient box is weird to set up, so we let Flash set up 414 | // a basic matrix and then we alter it: 415 | m.createGradientBox(1, 1, 0, 0, 0); 416 | m.translate(-0.5, -0.5); 417 | m.scale((1/G.length), (1/G.length)); 418 | m.rotate(Math.atan2(G.y, G.x)); 419 | m.translate(C.x, C.y); 420 | var alphas:Array = colors.map(function (_:*, index:int, A:Array):Number { return 1.0; }); 421 | var spread:Array = colors.map(function (_:*, index:int, A:Array):int { return 255*index/(A.length-1); }); 422 | graphics.beginGradientFill(GradientType.LINEAR, colors, alphas, spread, m, SpreadMethod.PAD); 423 | } 424 | fillFunction(); 425 | graphics.endFill(); 426 | } 427 | 428 | 429 | // Draw the map in the current map mode 430 | public function drawMap(mode:String):void { 431 | graphicsReset(); 432 | noiseLayer.visible = true; 433 | 434 | drawHistograms(); 435 | 436 | if (mode == '3d') { 437 | if (!render3dTimer.running) render3dTimer.start(); 438 | noiseLayer.visible = false; 439 | render3dPolygons(graphics, displayColors, colorWithSlope); 440 | return; 441 | } else if (mode == 'polygons') { 442 | noiseLayer.visible = false; 443 | renderDebugPolygons(graphics, displayColors); 444 | } else if (mode == 'watersheds') { 445 | noiseLayer.visible = false; 446 | renderDebugPolygons(graphics, displayColors); 447 | renderWatersheds(graphics); 448 | return; 449 | } else if (mode == 'biome') { 450 | renderPolygons(graphics, displayColors, null, null); 451 | } else if (mode == 'slopes') { 452 | renderPolygons(graphics, displayColors, null, colorWithSlope); 453 | } else if (mode == 'smooth') { 454 | renderPolygons(graphics, displayColors, null, colorWithSmoothColors); 455 | } else if (mode == 'elevation') { 456 | renderPolygons(graphics, elevationGradientColors, 'elevation', null); 457 | } else if (mode == 'moisture') { 458 | renderPolygons(graphics, moistureGradientColors, 'moisture', null); 459 | } 460 | 461 | if (render3dTimer.running) render3dTimer.stop(); 462 | 463 | if (mode != 'slopes' && mode != 'moisture') { 464 | renderRoads(graphics, displayColors); 465 | } 466 | if (mode != 'polygons') { 467 | renderEdges(graphics, displayColors); 468 | } 469 | if (mode != 'slopes' && mode != 'moisture') { 470 | renderBridges(graphics, displayColors); 471 | } 472 | } 473 | 474 | 475 | // 3D rendering of polygons. If the 'triangles3d' array is empty, 476 | // it's filled and the graphicsData is filled in as well. On 477 | // rendering, the triangles3d array has to be z-sorted and then 478 | // the resulting polygon data is transferred into graphicsData 479 | // before rendering. 480 | public function render3dPolygons(graphics:Graphics, colors:Object, colorFunction:Function):void { 481 | var p:Center, q:Corner, edge:Edge; 482 | var zScale:Number = 0.15*SIZE; 483 | 484 | graphics.beginFill(colors.OCEAN); 485 | graphics.drawRect(0, 0, SIZE, SIZE); 486 | graphics.endFill(); 487 | 488 | if (triangles3d.length == 0) { 489 | graphicsData = new Vector.(); 490 | for each (p in map.centers) { 491 | if (p.ocean) continue; 492 | // Each polygon will be drawn as a series of triangles, 493 | // where the vertices are the center of the polygon and 494 | // the two endpoints of the edge. 495 | for each (edge in p.borders) { 496 | var color:int = colors[p.biome] || 0; 497 | if (colorFunction != null) { 498 | color = colorFunction(color, p, q, edge); 499 | } 500 | 501 | var corner0:Corner = edge.v0; 502 | var corner1:Corner = edge.v1; 503 | 504 | if (corner0 == null || corner1 == null) { 505 | // Edge of the map; we can't deal with it right now 506 | continue; 507 | } 508 | 509 | // We already have elevations for corners and polygon centers: 510 | var zp:Number = zScale*p.elevation; 511 | var z0:Number = zScale*corner0.elevation; 512 | var z1:Number = zScale*corner1.elevation; 513 | triangles3d.push({ 514 | a:new Vector3D(p.point.x, p.point.y, zp), 515 | b:new Vector3D(corner0.point.x, corner0.point.y, z0), 516 | c:new Vector3D(corner1.point.x, corner1.point.y, z1), 517 | rA:null, 518 | rB:null, 519 | rC:null, 520 | z:0.0, 521 | color:color 522 | }); 523 | graphicsData.push(new GraphicsSolidFill()); 524 | graphicsData.push(new GraphicsPath(Vector.([GraphicsPathCommand.MOVE_TO, GraphicsPathCommand.LINE_TO, GraphicsPathCommand.LINE_TO]), 525 | Vector.([0, 0, 0, 0, 0, 0]))); 526 | graphicsData.push(new GraphicsEndFill()); 527 | } 528 | } 529 | } 530 | 531 | var camera:Matrix3D = new Matrix3D(); 532 | camera.appendRotation(rotationAnimation, new Vector3D(0, 0, 1), new Vector3D(SIZE/2, SIZE/2)); 533 | camera.appendRotation(60, new Vector3D(1,0,0), new Vector3D(SIZE/2, SIZE/2)); 534 | rotationAnimation += 1; 535 | 536 | for each (var tri:Object in triangles3d) { 537 | tri.rA = camera.transformVector(tri.a); 538 | tri.rB = camera.transformVector(tri.b); 539 | tri.rC = camera.transformVector(tri.c); 540 | tri.z = (tri.rA.z + tri.rB.z + tri.rC.z)/3; 541 | } 542 | triangles3d.sortOn('z', Array.NUMERIC); 543 | 544 | for (var i:int = 0; i < triangles3d.length; i++) { 545 | tri = triangles3d[i]; 546 | GraphicsSolidFill(graphicsData[i*3]).color = tri.color; 547 | var data:Vector. = GraphicsPath(graphicsData[i*3+1]).data; 548 | data[0] = tri.rA.x; 549 | data[1] = tri.rA.y; 550 | data[2] = tri.rB.x; 551 | data[3] = tri.rB.y; 552 | data[4] = tri.rC.x; 553 | data[5] = tri.rC.y; 554 | } 555 | graphics.drawGraphicsData(graphicsData); 556 | } 557 | 558 | 559 | // Render the interior of polygons 560 | public function renderPolygons(graphics:Graphics, colors:Object, gradientFillProperty:String, colorOverrideFunction:Function):void { 561 | var p:Center, r:Center; 562 | 563 | // My Voronoi polygon rendering doesn't handle the boundary 564 | // polygons, so I just fill everything with ocean first. 565 | graphics.beginFill(colors.OCEAN); 566 | graphics.drawRect(0, 0, SIZE, SIZE); 567 | graphics.endFill(); 568 | 569 | for each (p in map.centers) { 570 | for each (r in p.neighbors) { 571 | var edge:Edge = map.lookupEdgeFromCenter(p, r); 572 | var color:int = colors[p.biome] || 0; 573 | if (colorOverrideFunction != null) { 574 | color = colorOverrideFunction(color, p, r, edge); 575 | } 576 | 577 | function drawPath0():void { 578 | var path:Vector. = noisyEdges.path0[edge.index]; 579 | graphics.moveTo(p.point.x, p.point.y); 580 | graphics.lineTo(path[0].x, path[0].y); 581 | drawPathForwards(graphics, path); 582 | graphics.lineTo(p.point.x, p.point.y); 583 | } 584 | 585 | function drawPath1():void { 586 | var path:Vector. = noisyEdges.path1[edge.index]; 587 | graphics.moveTo(p.point.x, p.point.y); 588 | graphics.lineTo(path[0].x, path[0].y); 589 | drawPathForwards(graphics, path); 590 | graphics.lineTo(p.point.x, p.point.y); 591 | } 592 | 593 | if (noisyEdges.path0[edge.index] == null 594 | || noisyEdges.path1[edge.index] == null) { 595 | // It's at the edge of the map, where we don't have 596 | // the noisy edges computed. TODO: figure out how to 597 | // fill in these edges from the voronoi library. 598 | continue; 599 | } 600 | 601 | if (gradientFillProperty != null) { 602 | // We'll draw two triangles: center - corner0 - 603 | // midpoint and center - midpoint - corner1. 604 | var corner0:Corner = edge.v0; 605 | var corner1:Corner = edge.v1; 606 | 607 | // We pick the midpoint elevation/moisture between 608 | // corners instead of between polygon centers because 609 | // the resulting gradients tend to be smoother. 610 | var midpoint:Point = edge.midpoint; 611 | var midpointAttr:Number = 0.5*(corner0[gradientFillProperty]+corner1[gradientFillProperty]); 612 | drawGradientTriangle 613 | (graphics, 614 | new Vector3D(p.point.x, p.point.y, p[gradientFillProperty]), 615 | new Vector3D(corner0.point.x, corner0.point.y, corner0[gradientFillProperty]), 616 | new Vector3D(midpoint.x, midpoint.y, midpointAttr), 617 | [colors.GRADIENT_LOW, colors.GRADIENT_HIGH], drawPath0); 618 | drawGradientTriangle 619 | (graphics, 620 | new Vector3D(p.point.x, p.point.y, p[gradientFillProperty]), 621 | new Vector3D(midpoint.x, midpoint.y, midpointAttr), 622 | new Vector3D(corner1.point.x, corner1.point.y, corner1[gradientFillProperty]), 623 | [colors.GRADIENT_LOW, colors.GRADIENT_HIGH], drawPath1); 624 | } else { 625 | graphics.beginFill(color); 626 | drawPath0(); 627 | drawPath1(); 628 | graphics.endFill(); 629 | } 630 | } 631 | } 632 | } 633 | 634 | 635 | // Render bridges across every narrow river edge. Bridges are 636 | // straight line segments perpendicular to the edge. Bridges are 637 | // drawn after rivers. TODO: sometimes the bridges aren't long 638 | // enough to cross the entire noisy line river. TODO: bridges 639 | // don't line up with curved road segments when there are 640 | // roads. It might be worth making a shader that draws the bridge 641 | // only when there's water underneath. 642 | public function renderBridges(graphics:Graphics, colors:Object):void { 643 | var edge:Edge; 644 | 645 | for each (edge in map.edges) { 646 | if (edge.river > 0 && edge.river < 4 647 | && !edge.d0.water && !edge.d1.water 648 | && (edge.d0.elevation > 0.05 || edge.d1.elevation > 0.05)) { 649 | var n:Point = new Point(-(edge.v1.point.y - edge.v0.point.y), edge.v1.point.x - edge.v0.point.x); 650 | n.normalize(0.25 + (roads.road[edge.index]? 0.5 : 0) + 0.75*Math.sqrt(edge.river)); 651 | graphics.lineStyle(1.1, colors.BRIDGE, 1.0, false, LineScaleMode.NORMAL, CapsStyle.SQUARE); 652 | graphics.moveTo(edge.midpoint.x - n.x, edge.midpoint.y - n.y); 653 | graphics.lineTo(edge.midpoint.x + n.x, edge.midpoint.y + n.y); 654 | graphics.lineStyle(); 655 | } 656 | } 657 | } 658 | 659 | 660 | // Render roads. We draw these before polygon edges, so that rivers overwrite roads. 661 | public function renderRoads(graphics:Graphics, colors:Object):void { 662 | // First draw the roads, because any other feature should draw 663 | // over them. Also, roads don't use the noisy lines. 664 | var p:Center, A:Point, B:Point, C:Point; 665 | var i:int, j:int, d:Number, edge1:Edge, edge2:Edge, edges:Vector.; 666 | 667 | // Helper function: find the normal vector across edge 'e' and 668 | // make sure to point it in a direction towards 'c'. 669 | function normalTowards(e:Edge, c:Point, len:Number):Point { 670 | // Rotate the v0-->v1 vector by 90 degrees: 671 | var n:Point = new Point(-(e.v1.point.y - e.v0.point.y), e.v1.point.x - e.v0.point.x); 672 | // Flip it around it if doesn't point towards c 673 | var d:Point = c.subtract(e.midpoint); 674 | if (n.x * d.x + n.y * d.y < 0) { 675 | n.x = -n.x; 676 | n.y = -n.y; 677 | } 678 | n.normalize(len); 679 | return n; 680 | } 681 | 682 | for each (p in map.centers) { 683 | if (roads.roadConnections[p.index]) { 684 | if (roads.roadConnections[p.index].length == 2) { 685 | // Regular road: draw a spline from one edge to the other. 686 | edges = p.borders; 687 | for (i = 0; i < edges.length; i++) { 688 | edge1 = edges[i]; 689 | if (roads.road[edge1.index] > 0) { 690 | for (j = i+1; j < edges.length; j++) { 691 | edge2 = edges[j]; 692 | if (roads.road[edge2.index] > 0) { 693 | // The spline connects the midpoints of the edges 694 | // and at right angles to them. In between we 695 | // generate two control points A and B and one 696 | // additional vertex C. This usually works but 697 | // not always. 698 | d = 0.5*Math.min 699 | (edge1.midpoint.subtract(p.point).length, 700 | edge2.midpoint.subtract(p.point).length); 701 | A = normalTowards(edge1, p.point, d).add(edge1.midpoint); 702 | B = normalTowards(edge2, p.point, d).add(edge2.midpoint); 703 | C = Point.interpolate(A, B, 0.5); 704 | graphics.lineStyle(1.1, colors['ROAD'+roads.road[edge1.index]]); 705 | graphics.moveTo(edge1.midpoint.x, edge1.midpoint.y); 706 | graphics.curveTo(A.x, A.y, C.x, C.y); 707 | graphics.lineStyle(1.1, colors['ROAD'+roads.road[edge2.index]]); 708 | graphics.curveTo(B.x, B.y, edge2.midpoint.x, edge2.midpoint.y); 709 | graphics.lineStyle(); 710 | } 711 | } 712 | } 713 | } 714 | } else { 715 | // Intersection or dead end: draw a road spline from 716 | // each edge to the center 717 | for each (edge1 in p.borders) { 718 | if (roads.road[edge1.index] > 0) { 719 | d = 0.25*edge1.midpoint.subtract(p.point).length; 720 | A = normalTowards(edge1, p.point, d).add(edge1.midpoint); 721 | graphics.lineStyle(1.4, colors['ROAD'+roads.road[edge1.index]]); 722 | graphics.moveTo(edge1.midpoint.x, edge1.midpoint.y); 723 | graphics.curveTo(A.x, A.y, p.point.x, p.point.y); 724 | graphics.lineStyle(); 725 | } 726 | } 727 | } 728 | } 729 | } 730 | } 731 | 732 | 733 | // Render the exterior of polygons: coastlines, lake shores, 734 | // rivers, lava fissures. We draw all of these after the polygons 735 | // so that polygons don't overwrite any edges. 736 | public function renderEdges(graphics:Graphics, colors:Object):void { 737 | var p:Center, r:Center, edge:Edge; 738 | 739 | for each (p in map.centers) { 740 | for each (r in p.neighbors) { 741 | edge = map.lookupEdgeFromCenter(p, r); 742 | if (noisyEdges.path0[edge.index] == null 743 | || noisyEdges.path1[edge.index] == null) { 744 | // It's at the edge of the map 745 | continue; 746 | } 747 | if (p.ocean != r.ocean) { 748 | // One side is ocean and the other side is land -- coastline 749 | graphics.lineStyle(2, colors.COAST); 750 | } else if ((p.water > 0) != (r.water > 0) && p.biome != 'ICE' && r.biome != 'ICE') { 751 | // Lake boundary 752 | graphics.lineStyle(1, colors.LAKESHORE); 753 | } else if (p.water || r.water) { 754 | // Lake interior – we don't want to draw the rivers here 755 | continue; 756 | } else if (lava.lava[edge.index]) { 757 | // Lava flow 758 | graphics.lineStyle(1, colors.LAVA); 759 | } else if (edge.river > 0) { 760 | // River edge 761 | graphics.lineStyle(Math.sqrt(edge.river), colors.RIVER); 762 | } else { 763 | // No edge 764 | continue; 765 | } 766 | 767 | graphics.moveTo(noisyEdges.path0[edge.index][0].x, 768 | noisyEdges.path0[edge.index][0].y); 769 | drawPathForwards(graphics, noisyEdges.path0[edge.index]); 770 | drawPathBackwards(graphics, noisyEdges.path1[edge.index]); 771 | graphics.lineStyle(); 772 | } 773 | } 774 | } 775 | 776 | 777 | // Render the polygons so that each can be seen clearly 778 | public function renderDebugPolygons(graphics:Graphics, colors:Object):void { 779 | var p:Center, q:Corner, edge:Edge, point:Point, color:int; 780 | 781 | if (map.centers.length == 0) { 782 | // We're still constructing the map so we may have some points 783 | graphics.beginFill(0xdddddd); 784 | graphics.drawRect(0, 0, SIZE, SIZE); 785 | graphics.endFill(); 786 | for each (point in map.points) { 787 | graphics.beginFill(0x000000); 788 | graphics.drawCircle(point.x, point.y, 1.3); 789 | graphics.endFill(); 790 | } 791 | } 792 | 793 | for each (p in map.centers) { 794 | color = colors[p.biome] || (p.ocean? colors.OCEAN : p.water? colors.RIVER : 0xffffff); 795 | graphics.beginFill(interpolateColor(color, 0xdddddd, 0.2)); 796 | for each (edge in p.borders) { 797 | if (edge.v0 && edge.v1) { 798 | graphics.moveTo(p.point.x, p.point.y); 799 | graphics.lineTo(edge.v0.point.x, edge.v0.point.y); 800 | if (edge.river > 0) { 801 | graphics.lineStyle(2, displayColors.RIVER, 1.0); 802 | } else { 803 | graphics.lineStyle(0, 0x000000, 0.4); 804 | } 805 | graphics.lineTo(edge.v1.point.x, edge.v1.point.y); 806 | graphics.lineStyle(); 807 | } 808 | } 809 | graphics.endFill(); 810 | graphics.beginFill(p.water > 0 ? 0x003333 : 0x000000, 0.7); 811 | graphics.drawCircle(p.point.x, p.point.y, 1.3); 812 | graphics.endFill(); 813 | for each (q in p.corners) { 814 | graphics.beginFill(q.water? 0x0000ff : 0x009900); 815 | graphics.drawRect(q.point.x-0.7, q.point.y-0.7, 1.5, 1.5); 816 | graphics.endFill(); 817 | } 818 | } 819 | } 820 | 821 | 822 | // Render the paths from each polygon to the ocean, showing watersheds 823 | public function renderWatersheds(graphics:Graphics):void { 824 | var edge:Edge, w0:int, w1:int; 825 | 826 | for each (edge in map.edges) { 827 | if (edge.d0 && edge.d1 && edge.v0 && edge.v1 828 | && !edge.d0.ocean && !edge.d1.ocean) { 829 | w0 = watersheds.watersheds[edge.d0.index]; 830 | w1 = watersheds.watersheds[edge.d1.index]; 831 | if (w0 != w1) { 832 | graphics.lineStyle(3.5, 0x000000, 0.1*Math.sqrt((map.corners[w0].watershed_size || 1) + (map.corners[w1].watershed.watershed_size || 1))); 833 | graphics.moveTo(edge.v0.point.x, edge.v0.point.y); 834 | graphics.lineTo(edge.v1.point.x, edge.v1.point.y); 835 | graphics.lineStyle(); 836 | } 837 | } 838 | } 839 | 840 | for each (edge in map.edges) { 841 | if (edge.river) { 842 | graphics.lineStyle(1.0, 0x6699ff); 843 | graphics.moveTo(edge.v0.point.x, edge.v0.point.y); 844 | graphics.lineTo(edge.v1.point.x, edge.v1.point.y); 845 | graphics.lineStyle(); 846 | } 847 | } 848 | } 849 | 850 | 851 | private var lightVector:Vector3D = new Vector3D(-1, -1, 0); 852 | public function calculateLighting(p:Center, r:Corner, s:Corner):Number { 853 | var A:Vector3D = new Vector3D(p.point.x, p.point.y, p.elevation); 854 | var B:Vector3D = new Vector3D(r.point.x, r.point.y, r.elevation); 855 | var C:Vector3D = new Vector3D(s.point.x, s.point.y, s.elevation); 856 | var normal:Vector3D = B.subtract(A).crossProduct(C.subtract(A)); 857 | if (normal.z < 0) { normal.scaleBy(-1); } 858 | normal.normalize(); 859 | var light:Number = 0.5 + 35*normal.dotProduct(lightVector); 860 | if (light < 0) light = 0; 861 | if (light > 1) light = 1; 862 | return light; 863 | } 864 | 865 | public function colorWithSlope(color:int, p:Center, q:Center, edge:Edge):int { 866 | var r:Corner = edge.v0; 867 | var s:Corner = edge.v1; 868 | if (!r || !s) { 869 | // Edge of the map 870 | return displayColors.OCEAN; 871 | } else if (p.water) { 872 | return color; 873 | } 874 | 875 | if (q != null && p.water == q.water) color = interpolateColor(color, displayColors[q.biome], 0.4); 876 | var colorLow:int = interpolateColor(color, 0x333333, 0.7); 877 | var colorHigh:int = interpolateColor(color, 0xffffff, 0.3); 878 | var light:Number = calculateLighting(p, r, s); 879 | if (light < 0.5) return interpolateColor(colorLow, color, light*2); 880 | else return interpolateColor(color, colorHigh, light*2-1); 881 | } 882 | 883 | 884 | public function colorWithSmoothColors(color:int, p:Center, q:Center, edge:Edge):int { 885 | if (q != null && p.water == q.water) { 886 | color = interpolateColor(displayColors[p.biome], displayColors[q.biome], 0.25); 887 | } 888 | return color; 889 | } 890 | 891 | 892 | ////////////////////////////////////////////////////////////////////// 893 | // The following code is used to export the maps to disk 894 | 895 | // We export elevation, moisture, and an override byte. Instead of 896 | // rendering with RGB values, we render with bytes 0x00-0xff as 897 | // colors, and then save these bytes in a ByteArray. For override 898 | // codes, we turn off anti-aliasing. 899 | static public var exportOverrideColors:Object = { 900 | /* override codes are 0:none, 0x10:river water, 0x20:lava, 901 | 0x30:snow, 0x40:ice, 0x50:ocean, 0x60:lake, 0x70:lake shore, 902 | 0x80:ocean shore, 0x90,0xa0,0xb0:road, 0xc0:bridge. These 903 | are ORed with 0x01: polygon center, 0x02: safe polygon 904 | center. */ 905 | POLYGON_CENTER: 0x01, 906 | POLYGON_CENTER_SAFE: 0x03, 907 | OCEAN: 0x50, 908 | COAST: 0x80, 909 | LAKE: 0x60, 910 | LAKESHORE: 0x70, 911 | RIVER: 0x10, 912 | MARSH: 0x10, 913 | ICE: 0x40, 914 | LAVA: 0x20, 915 | SNOW: 0x30, 916 | ROAD1: 0x90, 917 | ROAD2: 0xa0, 918 | ROAD3: 0xb0, 919 | BRIDGE: 0xc0 920 | }; 921 | 922 | static public var exportElevationColors:Object = { 923 | OCEAN: 0x00, 924 | GRADIENT_LOW: 0x00, 925 | GRADIENT_HIGH: 0xff 926 | }; 927 | 928 | static public var exportMoistureColors:Object = { 929 | OCEAN: 0xff, 930 | GRADIENT_LOW: 0x00, 931 | GRADIENT_HIGH: 0xff 932 | }; 933 | 934 | 935 | // This function draws to a bitmap and copies that data into the 936 | // three export byte arrays. The layer parameter should be one of 937 | // 'elevation', 'moisture', 'overrides'. 938 | public function makeExport(layer:String):ByteArray { 939 | var exportBitmap:BitmapData = new BitmapData(2048, 2048); 940 | var exportGraphics:Shape = new Shape(); 941 | var exportData:ByteArray = new ByteArray(); 942 | 943 | var m:Matrix = new Matrix(); 944 | m.scale(2048.0 / SIZE, 2048.0 / SIZE); 945 | 946 | function saveBitmapToArray():void { 947 | for (var x:int = 0; x < 2048; x++) { 948 | for (var y:int = 0; y < 2048; y++) { 949 | exportData.writeByte(exportBitmap.getPixel(x, y) & 0xff); 950 | } 951 | } 952 | } 953 | 954 | if (layer == 'overrides') { 955 | renderPolygons(exportGraphics.graphics, exportOverrideColors, null, null); 956 | renderRoads(exportGraphics.graphics, exportOverrideColors); 957 | renderEdges(exportGraphics.graphics, exportOverrideColors); 958 | renderBridges(exportGraphics.graphics, exportOverrideColors); 959 | 960 | stage.quality = 'low'; 961 | exportBitmap.draw(exportGraphics, m); 962 | stage.quality = 'best'; 963 | 964 | // Mark the polygon centers in the export bitmap 965 | for each (var p:Center in map.centers) { 966 | if (!p.ocean) { 967 | var r:Point = new Point(Math.floor(p.point.x * 2048/SIZE), 968 | Math.floor(p.point.y * 2048/SIZE)); 969 | exportBitmap.setPixel(r.x, r.y, 970 | exportBitmap.getPixel(r.x, r.y) 971 | | (roads.roadConnections[p]? 972 | exportOverrideColors.POLYGON_CENTER_SAFE 973 | : exportOverrideColors.POLYGON_CENTER)); 974 | } 975 | } 976 | 977 | saveBitmapToArray(); 978 | } else if (layer == 'elevation') { 979 | renderPolygons(exportGraphics.graphics, exportElevationColors, 'elevation', null); 980 | exportBitmap.draw(exportGraphics, m); 981 | saveBitmapToArray(); 982 | } else if (layer == 'moisture') { 983 | renderPolygons(exportGraphics.graphics, exportMoistureColors, 'moisture', null); 984 | exportBitmap.draw(exportGraphics, m); 985 | saveBitmapToArray(); 986 | } 987 | return exportData; 988 | } 989 | 990 | 991 | // Export the map visible in the UI as a PNG. Turn OFF the noise 992 | // layer because (1) it's scaled the wrong amount for the big 993 | // image, and (2) it makes the resulting PNG much bigger, and (3) 994 | // it makes it harder to apply your own texturing or noise later. 995 | public function exportPng():ByteArray { 996 | var exportBitmap:BitmapData = new BitmapData(2048, 2048); 997 | var originalNoiseLayerVisible:Boolean = noiseLayer.visible; 998 | 999 | var m:Matrix = new Matrix(); 1000 | m.scale(2048.0 / SIZE, 2048.0 / SIZE); 1001 | noiseLayer.visible = false; 1002 | exportBitmap.draw(this, m); 1003 | noiseLayer.visible = originalNoiseLayerVisible; 1004 | 1005 | return PNGEncoder.encode(exportBitmap); 1006 | } 1007 | 1008 | 1009 | // Export the graph data as XML. 1010 | public function exportPolygons():String { 1011 | // NOTE: For performance, we do not assemble the entire XML in 1012 | // memory and then serialize it. Instead, we incrementally 1013 | // serialize small portions into arrays of strings, and then assemble the 1014 | // strings together. 1015 | var p:Center, q:Corner, r:Center, s:Corner, edge:Edge; 1016 | XML.prettyPrinting = false; 1017 | var top:XML = 1018 | 1021 | 1024 | 1025 | ; 1026 | 1027 | var dnodes:Array = []; 1028 | var edges:Array = []; 1029 | var vnodes:Array = []; 1030 | var outroads:Array = []; 1031 | var accum:Array = []; // temporary accumulator for serialized xml fragments 1032 | var edgeNode:XML; 1033 | 1034 | for each (p in map.centers) { 1035 | accum.splice(0, accum.length); 1036 | 1037 | for each (r in p.neighbors) { 1038 | accum.push(
.toXMLString()); 1039 | } 1040 | for each (edge in p.borders) { 1041 | accum.push(.toXMLString()); 1042 | } 1043 | for each (q in p.corners) { 1044 | accum.push(.toXMLString()); 1045 | } 1046 | 1047 | dnodes.push 1048 | (
1054 | 1055 |
.toXMLString().replace("", accum.join(""))); 1056 | } 1057 | 1058 | for each (edge in map.edges) { 1059 | edgeNode = 1060 | ; 1061 | if (edge.midpoint != null) { 1062 | edgeNode.@x = edge.midpoint.x; 1063 | edgeNode.@y = edge.midpoint.y; 1064 | } 1065 | if (edge.d0 != null) edgeNode.@center0 = edge.d0.index; 1066 | if (edge.d1 != null) edgeNode.@center1 = edge.d1.index; 1067 | if (edge.v0 != null) edgeNode.@corner0 = edge.v0.index; 1068 | if (edge.v1 != null) edgeNode.@corner1 = edge.v1.index; 1069 | edges.push(edgeNode.toXMLString()); 1070 | } 1071 | 1072 | for each (q in map.corners) { 1073 | accum.splice(0, accum.length); 1074 | for each (p in q.touches) { 1075 | accum.push(
.toXMLString()); 1076 | } 1077 | for each (edge in q.protrudes) { 1078 | accum.push(.toXMLString()); 1079 | } 1080 | for each (s in q.adjacent) { 1081 | accum.push(.toXMLString()); 1082 | } 1083 | 1084 | vnodes.push 1085 | ( 1091 | 1092 | .toXMLString().replace("", accum.join(""))); 1093 | } 1094 | 1095 | for (var i:String in roads.road) { 1096 | outroads.push(.toXMLString()); 1097 | } 1098 | 1099 | var out:String = top.toXMLString(); 1100 | accum = [].concat("", 1101 | dnodes, "", 1102 | edges, "", 1103 | vnodes, "", 1104 | outroads, ""); 1105 | out = out.replace("", accum.join("")); 1106 | return out; 1107 | } 1108 | 1109 | 1110 | // Make a button or label. If the callback is null, it's just a label. 1111 | public function makeButton(label:String, x:int, y:int, width:int, callback:Function):TextField { 1112 | var button:TextField = new TextField(); 1113 | var format:TextFormat = new TextFormat(); 1114 | format.font = "Arial"; 1115 | format.align = 'center'; 1116 | button.defaultTextFormat = format; 1117 | button.text = label; 1118 | button.selectable = false; 1119 | button.x = x; 1120 | button.y = y; 1121 | button.width = width; 1122 | button.height = 20; 1123 | if (callback != null) { 1124 | button.background = true; 1125 | button.backgroundColor = 0xffffcc; 1126 | button.addEventListener(MouseEvent.CLICK, callback); 1127 | } 1128 | return button; 1129 | } 1130 | 1131 | 1132 | public function addIslandShapeButtons():void { 1133 | var y:int = 4; 1134 | var islandShapeLabel:TextField = makeButton("Island Shape:", 25, y, 150, null); 1135 | 1136 | var seedLabel:TextField = makeButton("Shape #", 20, y+22, 50, null); 1137 | 1138 | islandSeedInput = makeButton(islandSeedInitial, 70, y+22, 54, null); 1139 | islandSeedInput.background = true; 1140 | islandSeedInput.backgroundColor = 0xccddcc; 1141 | islandSeedInput.selectable = true; 1142 | islandSeedInput.type = TextFieldType.INPUT; 1143 | islandSeedInput.addEventListener(KeyboardEvent.KEY_UP, function (e:KeyboardEvent):void { 1144 | if (e.keyCode == 13) { 1145 | go(islandType, pointType, numPoints); 1146 | } 1147 | }); 1148 | 1149 | function markActiveIslandShape(newIslandType:String):void { 1150 | mapTypes[islandType].backgroundColor = 0xffffcc; 1151 | mapTypes[newIslandType].backgroundColor = 0xffff00; 1152 | } 1153 | 1154 | function setIslandTypeTo(type:String):Function { 1155 | return function(e:Event):void { 1156 | markActiveIslandShape(type); 1157 | go(type, pointType, numPoints); 1158 | } 1159 | } 1160 | 1161 | var mapTypes:Object = { 1162 | 'Radial': makeButton("Radial", 23, y+44, 40, setIslandTypeTo('Radial')), 1163 | 'Perlin': makeButton("Perlin", 65, y+44, 35, setIslandTypeTo('Perlin')), 1164 | 'Square': makeButton("Square", 102, y+44, 44, setIslandTypeTo('Square')), 1165 | 'Blob': makeButton("Blob", 148, y+44, 29, setIslandTypeTo('Blob')) 1166 | }; 1167 | markActiveIslandShape(islandType); 1168 | 1169 | controls.addChild(islandShapeLabel); 1170 | controls.addChild(seedLabel); 1171 | controls.addChild(islandSeedInput); 1172 | controls.addChild(makeButton("Random", 125, y+22, 56, 1173 | function (e:Event):void { 1174 | islandSeedInput.text = 1175 | ( (Math.random()*100000).toFixed(0) 1176 | + "-" 1177 | + (1 + Math.floor(9*Math.random())).toFixed(0) ); 1178 | go(islandType, pointType, numPoints); 1179 | })); 1180 | controls.addChild(mapTypes.Radial); 1181 | controls.addChild(mapTypes.Perlin); 1182 | controls.addChild(mapTypes.Square); 1183 | controls.addChild(mapTypes.Blob); 1184 | } 1185 | 1186 | 1187 | public function addPointSelectionButtons():void { 1188 | function markActivePointSelection(newPointType:String):void { 1189 | pointTypes[pointType].backgroundColor = 0xffffcc; 1190 | pointTypes[newPointType].backgroundColor = 0xffff00; 1191 | } 1192 | 1193 | function setPointsTo(type:String):Function { 1194 | return function(e:Event):void { 1195 | markActivePointSelection(type); 1196 | go(islandType, type, numPoints); 1197 | } 1198 | } 1199 | 1200 | var pointTypes:Object = { 1201 | 'Random': makeButton("Random", 16, y+120, 50, setPointsTo('Random')), 1202 | 'Relaxed': makeButton("Relaxed", 68, y+120, 48, setPointsTo('Relaxed')), 1203 | 'Square': makeButton("Square", 118, y+120, 44, setPointsTo('Square')), 1204 | 'Hexagon': makeButton("Hex", 164, y+120, 28, setPointsTo('Hexagon')) 1205 | }; 1206 | markActivePointSelection(pointType); 1207 | 1208 | var pointTypeLabel:TextField = makeButton("Point Selection:", 25, y+100, 150, null); 1209 | controls.addChild(pointTypeLabel); 1210 | controls.addChild(pointTypes.Random); 1211 | controls.addChild(pointTypes.Relaxed); 1212 | controls.addChild(pointTypes.Square); 1213 | controls.addChild(pointTypes.Hexagon); 1214 | 1215 | function markActiveNumPoints(newNumPoints:int):void { 1216 | pointCounts[""+numPoints].backgroundColor = 0xffffcc; 1217 | pointCounts[""+newNumPoints].backgroundColor = 0xffff00; 1218 | } 1219 | 1220 | function setNumPointsTo(num:int):Function { 1221 | return function(e:Event):void { 1222 | markActiveNumPoints(num); 1223 | go(islandType, pointType, num); 1224 | } 1225 | } 1226 | 1227 | var pointCounts:Object = { 1228 | '500': makeButton("500", 23, y+142, 24, setNumPointsTo(500)), 1229 | '1000': makeButton("1000", 49, y+142, 32, setNumPointsTo(1000)), 1230 | '2000': makeButton("2000", 83, y+142, 32, setNumPointsTo(2000)), 1231 | '4000': makeButton("4000", 117, y+142, 32, setNumPointsTo(4000)), 1232 | '8000': makeButton("8000", 151, y+142, 32, setNumPointsTo(8000)) 1233 | }; 1234 | markActiveNumPoints(numPoints); 1235 | controls.addChild(pointCounts['500']); 1236 | controls.addChild(pointCounts['1000']); 1237 | controls.addChild(pointCounts['2000']); 1238 | controls.addChild(pointCounts['4000']); 1239 | controls.addChild(pointCounts['8000']); 1240 | } 1241 | 1242 | 1243 | public function addViewButtons():void { 1244 | var y:int = 330; 1245 | 1246 | function markViewButton(mode:String):void { 1247 | views[mapMode].backgroundColor = 0xffffcc; 1248 | views[mode].backgroundColor = 0xffff00; 1249 | } 1250 | function switcher(mode:String):Function { 1251 | return function(e:Event):void { 1252 | markViewButton(mode); 1253 | mapMode = mode; 1254 | drawMap(mapMode); 1255 | } 1256 | } 1257 | 1258 | var views:Object = { 1259 | 'biome': makeButton("Biomes", 25, y+22, 74, switcher('biome')), 1260 | 'smooth': makeButton("Smooth", 101, y+22, 74, switcher('smooth')), 1261 | 'slopes': makeButton("2D slopes", 25, y+44, 74, switcher('slopes')), 1262 | '3d': makeButton("3D slopes", 101, y+44, 74, switcher('3d')), 1263 | 'elevation': makeButton("Elevation", 25, y+66, 74, switcher('elevation')), 1264 | 'moisture': makeButton("Moisture", 101, y+66, 74, switcher('moisture')), 1265 | 'polygons': makeButton("Polygons", 25, y+88, 74, switcher('polygons')), 1266 | 'watersheds': makeButton("Watersheds", 101, y+88, 74, switcher('watersheds')) 1267 | }; 1268 | 1269 | markViewButton(mapMode); 1270 | 1271 | controls.addChild(makeButton("View:", 50, y, 100, null)); 1272 | 1273 | controls.addChild(views.biome); 1274 | controls.addChild(views.smooth); 1275 | controls.addChild(views.slopes); 1276 | controls.addChild(views['3d']); 1277 | controls.addChild(views.elevation); 1278 | controls.addChild(views.moisture); 1279 | controls.addChild(views.polygons); 1280 | controls.addChild(views.watersheds); 1281 | } 1282 | 1283 | 1284 | public function addMiscLabels():void { 1285 | controls.addChild(makeButton("Distribution:", 50, 180, 100, null)); 1286 | statusBar = makeButton("", SIZE/2-50, 10, 100, null); 1287 | addChild(statusBar); 1288 | } 1289 | 1290 | 1291 | public function addExportButtons():void { 1292 | var y:Number = 450; 1293 | controls.addChild(makeButton("Export byte arrays:", 25, y, 150, null)); 1294 | 1295 | controls.addChild(makeButton("Elevation", 50, y+22, 100, 1296 | function (e:Event):void { 1297 | new FileReference().save(makeExport('elevation'), 'elevation.data'); 1298 | })); 1299 | controls.addChild(makeButton("Moisture", 50, y+44, 100, 1300 | function (e:Event):void { 1301 | new FileReference().save(makeExport('moisture'), 'moisture.data'); 1302 | })); 1303 | controls.addChild(makeButton("Overrides", 50, y+66, 100, 1304 | function (e:Event):void { 1305 | new FileReference().save(makeExport('overrides'), 'overrides.data'); 1306 | })); 1307 | 1308 | controls.addChild(makeButton("Export:", 25, y+100, 50, null)); 1309 | controls.addChild(makeButton("XML", 77, y+100, 35, 1310 | function (e:Event):void { 1311 | new FileReference().save(exportPolygons(), 'map.xml'); 1312 | })); 1313 | controls.addChild(makeButton("PNG", 114, y+100, 35, 1314 | function (e:Event):void { 1315 | new FileReference().save(exportPng(), 'map.png'); 1316 | })); 1317 | } 1318 | 1319 | } 1320 | 1321 | } 1322 | -------------------------------------------------------------------------------- /prototypes/delaunay_set.as: -------------------------------------------------------------------------------- 1 | // Make a map out of a delaunay graph 2 | // Author: amitp@cs.stanford.edu 3 | // License: MIT 4 | 5 | package { 6 | import flash.geom.*; 7 | import flash.display.*; 8 | import flash.events.*; 9 | import com.indiemaps.delaunay.*; 10 | 11 | public class delaunay_set extends Sprite { 12 | public function delaunay_set() { 13 | stage.scaleMode = 'noScale'; 14 | stage.align = 'TL'; 15 | 16 | go(); 17 | 18 | stage.addEventListener(MouseEvent.CLICK, function (e:MouseEvent):void { go(); } ); 19 | } 20 | 21 | public function go():void { 22 | graphics.clear(); 23 | graphics.beginFill(0x999999); 24 | graphics.drawRect(-1000, -1000, 2000, 2000); 25 | graphics.endFill(); 26 | 27 | // TODO: build voronoi data structure (NOTE: decided to switch 28 | // to a Voronoi library instead) 29 | 30 | // TODO: if a node has only one neighbor with same 31 | // inside/outside status, then change its status. This will 32 | // remove impossible areas. 33 | 34 | var i:int; 35 | var pxyz:Array = []; 36 | 37 | var tiles:Array = []; 38 | for (i = 0; i < 600; i++) { 39 | var xyz:XYZ = new XYZ(600*Math.random(), 500*Math.random(), 0); 40 | pxyz.push(xyz); 41 | if (inside(xyz)) { 42 | graphics.beginFill(0x009900); 43 | } else { 44 | graphics.beginFill(0x7777cc); 45 | } 46 | graphics.drawCircle(xyz.x, xyz.y, 5); 47 | graphics.endFill(); 48 | } 49 | 50 | var triangles:Array = Delaunay.triangulate(pxyz); 51 | 52 | drawOld(pxyz, triangles); 53 | 54 | /* 55 | for each (var tri:ITriangle in triangles) { 56 | var circle:XYZ = new XYZ(); 57 | Delaunay.CircumCircle(0, 0, pxyz[tri.p1].x, pxyz[tri.p1].y, pxyz[tri.p2].x, pxyz[tri.p2].y, pxyz[tri.p3].x, pxyz[tri.p3].y, circle); 58 | graphics.beginFill(0xff0000); 59 | graphics.drawCircle(circle.x, circle.y, 3); 60 | graphics.endFill(); 61 | graphics.lineStyle(1, 0x000000, 0.2); 62 | graphics.drawCircle(circle.x, circle.y, circle.z); 63 | graphics.lineStyle(); 64 | } 65 | */ 66 | } 67 | 68 | public function drawOld(pxyz:Array, triangles:Array):void { 69 | for each (var tri:ITriangle in triangles) { 70 | function D(p1:int, p2:int, p3:int):void { 71 | var i1:Boolean = inside(pxyz[p1]); 72 | var i2:Boolean = inside(pxyz[p2]); 73 | var i3:Boolean = inside(pxyz[p3]); 74 | if (!i1 && !i2 && i3 && p1 > p2) { 75 | var p:int = p1; 76 | p1 = p2; 77 | p2 = p; 78 | } 79 | if (p1 < p2) { 80 | var color:int = 0x666600; 81 | if (i1&&i2) color = 0x007700; 82 | else if (!i1 && !i2 && i3) color = 0x000000; 83 | else if (!i1 && !i2) color = 0x6666cc; 84 | drawNoisyLine(pxyz[p1], pxyz[p2], {color: color, minLength:500, alpha:1, width:1}); 85 | graphics.lineStyle(); 86 | } 87 | } 88 | D(tri.p1, tri.p2, tri.p3); 89 | D(tri.p2, tri.p3, tri.p1); 90 | D(tri.p3, tri.p1, tri.p2); 91 | } 92 | } 93 | 94 | public function drawNoisyLine(p:XYZ, q:XYZ, style:Object=null):void { 95 | // TODO: don't use perp; use voronoi centers 96 | var pv:Vector3D = new Vector3D(p.x, p.y); 97 | var qv:Vector3D = new Vector3D(q.x, q.y); 98 | var p2q:Vector3D = qv.subtract(pv); 99 | var perp:Vector3D = new Vector3D(0.4*p2q.y, -0.4*p2q.x); 100 | var pq:Vector3D = new Vector3D(0.5*(p.x+q.x), 0.5*(p.y+q.y)); 101 | noisy_line.drawLine(graphics, pv, pq.add(perp), qv, pq.subtract(perp), style); 102 | graphics.lineStyle(); 103 | } 104 | 105 | public function inside(xyz:XYZ):Boolean { 106 | var angle:Number = Math.atan2(xyz.y-250, xyz.x-300); 107 | var length:Number = new Point(xyz.x-300, xyz.y-250).length; 108 | var r1:Number = 150 - 30*Math.sin(5*angle); 109 | var r2:Number = 150 - 70*Math.sin(5*angle + 0.5); 110 | if (0.1 < angle && angle < 1.2) r1 = r2 = 30; 111 | return (length < r1 || (length >= r1 && length < r2)); 112 | } 113 | 114 | public static function coordinateToPoint(i:int, j:int):Point { 115 | return new Point(20 + 50*i + 25*j + 3.7*(i % 3) + 2.5*(j % 4), 10 + 50*j + 3.7*(j % 3) + 2.5*(i % 4)); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /prototypes/hexagonal_drainage_basin.as: -------------------------------------------------------------------------------- 1 | // Draw a fractal hexagonal drainage basin 2 | // Author: amitp@cs.stanford.edu 3 | // License: MIT 4 | 5 | package { 6 | import flash.geom.*; 7 | import flash.display.*; 8 | 9 | public class hexagonal_drainage_basin extends Sprite { 10 | 11 | public function hexagonal_drainage_basin() { 12 | graphics.beginFill(0x999999); 13 | graphics.drawRect(-1000, -1000, 2000, 2000); 14 | graphics.endFill(); 15 | 16 | drawHex(new Vector3D(300, 0), 17 | new Vector3D(500, 100), 18 | new Vector3D(500, 250), 19 | new Vector3D(300, 350), 20 | new Vector3D(100, 250), 21 | new Vector3D(100, 100)); 22 | } 23 | 24 | public function drawHex(A:Vector3D, B:Vector3D, C:Vector3D, D:Vector3D, E:Vector3D, F:Vector3D):void { 25 | graphics.beginFill(0x00ff00); 26 | graphics.lineStyle(1, 0x000000, 0.1); 27 | graphics.moveTo(A.x, A.y); 28 | graphics.lineTo(B.x, B.y); 29 | graphics.lineTo(C.x, C.y); 30 | graphics.lineTo(D.x, D.y); 31 | graphics.lineTo(E.x, E.y); 32 | graphics.lineTo(F.x, F.y); 33 | graphics.endFill(); 34 | graphics.lineStyle(); 35 | 36 | var H:Vector3D = between([A, B, C, D, E, F]); 37 | if (H.subtract(D).length < 1) return; 38 | 39 | graphics.lineStyle(2, 0x0000ff); 40 | graphics.moveTo(D.x, D.y); 41 | graphics.lineTo(H.x, H.y); 42 | graphics.lineStyle(); 43 | 44 | var J:Vector3D = between([A, H]); 45 | var K:Vector3D = between([E, H]); 46 | var L:Vector3D = between([H, C]); 47 | var M:Vector3D = between([A, F]); 48 | var N:Vector3D = between([F, E]); 49 | var P:Vector3D = between([A, B]); 50 | var Q:Vector3D = between([B, C]); 51 | 52 | drawHex(F, M, J, H, K, N); 53 | drawHex(B, Q, L, H, J, P); 54 | } 55 | 56 | public function between(points:Array):Vector3D { 57 | var result:Vector3D = new Vector3D(); 58 | var weight:Number = 0.0; 59 | for each (var p:Vector3D in points) { 60 | var w:Number = Math.random() + 0.5; 61 | result.x += p.x * w; 62 | result.y += p.y * w; 63 | result.z += p.z * w; 64 | weight += w; 65 | } 66 | result.scaleBy(1.0/weight); 67 | return result; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /prototypes/hexagonal_grid.as: -------------------------------------------------------------------------------- 1 | // Make a map out of a hexagonal grid 2 | // Author: amitp@cs.stanford.edu 3 | // License: MIT 4 | 5 | package { 6 | import flash.geom.*; 7 | import flash.display.*; 8 | import flash.events.*; 9 | 10 | public class hexagonal_grid extends Sprite { 11 | public function hexagonal_grid() { 12 | stage.scaleMode = 'noScale'; 13 | stage.align = 'TL'; 14 | 15 | go(); 16 | 17 | stage.addEventListener(MouseEvent.CLICK, function (e:MouseEvent):void { go(); } ); 18 | } 19 | 20 | public function go():void { 21 | graphics.clear(); 22 | graphics.beginFill(0x999999); 23 | graphics.drawRect(-1000, -1000, 2000, 2000); 24 | graphics.endFill(); 25 | 26 | var edges:Array = []; 27 | 28 | function valid1(i:int, j:int):Boolean { 29 | return (i >= 0) && (i < 11) 30 | && (j >= 0) && (j < 11) 31 | && (i + j >= 5) && (i + j < 16); 32 | } 33 | 34 | function valid(i:int, j:int):Boolean { 35 | var p:Point = coordinateToPoint(i, j).subtract(new Point(300, 250)); 36 | var angle:Number = Math.atan2(p.y, p.x); 37 | var r1:Number = 200 + 50*Math.sin(5*angle); 38 | var r2:Number = 200 + 100*Math.sin(5*angle + 0.5); 39 | return p.length < r1 || (p.length >= 30+r1 && p.length < r2); 40 | } 41 | 42 | function maybeAddEdge(i1:int, j1:int, i2:int, j2:int):void { 43 | if (valid(i2, j2)) { 44 | edges.push({i1:i1, j1:j1, i2:i2, j2:j2}); 45 | } 46 | } 47 | 48 | for (var i:int = -5; i < 15; i++) { 49 | for (var j:int = -5; j < 15; j++) { 50 | var p:Point = coordinateToPoint(i, j); 51 | if (valid(i, j)) { 52 | graphics.beginFill(0xff0000); 53 | graphics.drawCircle(p.x, p.y, 4); 54 | graphics.endFill(); 55 | maybeAddEdge(i, j, i+1, j); 56 | maybeAddEdge(i, j, i, j+1); 57 | maybeAddEdge(i, j, i+1, j-1); 58 | } 59 | } 60 | } 61 | 62 | function drawNoisyLine(p:Point, q:Point):void { 63 | var pv:Vector3D = new Vector3D(p.x, p.y); 64 | var qv:Vector3D = new Vector3D(q.x, q.y); 65 | var p2q:Vector3D = qv.subtract(pv); 66 | var perp:Vector3D = new Vector3D(0.4*p2q.y, -0.4*p2q.x); 67 | var pq:Vector3D = new Vector3D(0.5*(p.x+q.x), 0.5*(p.y+q.y)); 68 | noisy_line.drawLine(graphics, pv, pq.add(perp), qv, pq.subtract(perp)); 69 | } 70 | 71 | for each (var edge:Object in edges) { 72 | drawNoisyLine(coordinateToPoint(edge.i1, edge.j1), 73 | coordinateToPoint(edge.i2, edge.j2)); 74 | } 75 | } 76 | 77 | public static function coordinateToPoint(i:int, j:int):Point { 78 | return new Point(20 + 50*i + 25*j + 3.7*(i % 3) + 2.5*(j % 4), 10 + 50*j + 3.7*(j % 3) + 2.5*(i % 4)); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /prototypes/noisy_line.as: -------------------------------------------------------------------------------- 1 | // Draw a fractal noisy line inside a quad boundary 2 | // Author: amitp@cs.stanford.edu 3 | // License: MIT 4 | 5 | /* 6 | 7 | Recursive approach: given A-B-C-D, we split horizontally (q) and 8 | vertically (p). 9 | 10 | A ----- G ----- B 11 | | | | 12 | E ----- H ----- F 13 | | I | 14 | D ------I------C 15 | 16 | To draw a line from A to C, we pick H somewhere in the quad, then 17 | recursively draw a noisy line from A to H inside A-G-H-E and from 18 | H to C inside H-F-C-I. 19 | 20 | */ 21 | 22 | package { 23 | import flash.geom.*; 24 | import flash.display.*; 25 | import flash.events.*; 26 | 27 | public class noisy_line extends Sprite { 28 | public function noisy_line() { 29 | stage.scaleMode = 'noScale'; 30 | stage.align = 'TL'; 31 | 32 | go(); 33 | 34 | stage.addEventListener(MouseEvent.CLICK, function (e:MouseEvent):void { go(); } ); 35 | } 36 | 37 | public function go():void { 38 | graphics.clear(); 39 | graphics.beginFill(0x999999); 40 | graphics.drawRect(-1000, -1000, 2000, 2000); 41 | graphics.endFill(); 42 | 43 | drawLine(graphics, 44 | new Vector3D(0, 0), new Vector3D(300, 0), 45 | new Vector3D(300, 200), new Vector3D(0, 200)); 46 | drawLine(graphics, 47 | new Vector3D(300, 200), new Vector3D(600, 200), 48 | new Vector3D(600, 400), new Vector3D(300, 400)); 49 | } 50 | 51 | 52 | public static function drawLineP(g:Graphics, A:Point, B:Point, C:Point, D:Point, style:Object):Number { 53 | return drawLine(g, new Vector3D(A.x, A.y), new Vector3D(B.x, B.y), new Vector3D(C.x, C.y), new Vector3D(D.x, D.y), style); 54 | } 55 | 56 | 57 | public static function drawLine(g:Graphics, A:Vector3D, B:Vector3D, C:Vector3D, D:Vector3D, style:Object=null):Number { 58 | if (!style) style = {}; 59 | 60 | // Draw the quadrilateral 61 | if (style.debug) { 62 | g.beginFill(0xccccbb, 0.1); 63 | g.lineStyle(1, 0x000000, 0.1); 64 | g.moveTo(A.x, A.y); 65 | g.lineTo(B.x, B.y); 66 | g.lineTo(C.x, C.y); 67 | g.lineTo(D.x, D.y); 68 | g.endFill(); 69 | g.lineStyle(); 70 | } 71 | 72 | var minLength:Number = style.minLength != null? style.minLength : 3; 73 | if (A.subtract(C).length < minLength || B.subtract(D).length < minLength) { 74 | g.lineStyle(style.width != null? style.width:1, 75 | style.color != null? style.color:0x000000, 76 | style.alpha != null? style.alpha:1.0); 77 | g.moveTo(A.x, A.y); 78 | g.lineTo(C.x, C.y); 79 | return A.subtract(C).length; 80 | } 81 | 82 | // Subdivide the quadrilateral 83 | var p:Number = random(0.1, 0.9); // vertical (along A-D and B-C) 84 | var q:Number = random(0.3, 0.7); // horizontal (along A-B and D-C) 85 | 86 | // Midpoints 87 | var E:Vector3D = interpolate(A, D, p); 88 | var F:Vector3D = interpolate(B, C, p); 89 | var G:Vector3D = interpolate(A, B, q); 90 | var I:Vector3D = interpolate(D, C, q); 91 | 92 | // Central point 93 | var H:Vector3D = interpolate(E, F, q); 94 | 95 | // Divide the quad into subquads, but meet at H 96 | var s:Number = random(-0.4, 0.4); 97 | var t:Number = random(-0.4, 0.4); 98 | 99 | return drawLine(g, A, interpolate(G, B, s), H, interpolate(E, D, t), style) 100 | + drawLine(g, H, interpolate(F, C, s), C, interpolate(I, D, t), style); 101 | } 102 | 103 | 104 | public static function buildLineSegments(A:Point, B:Point, C:Point, D:Point, minLength:Number):Vector. { 105 | var points:Vector. = new Vector.(); 106 | 107 | function subdivide(A:Point, B:Point, C:Point, D:Point):void { 108 | if (A.subtract(C).length < minLength || B.subtract(D).length < minLength) { 109 | points.push(C); 110 | return; 111 | } 112 | 113 | // Subdivide the quadrilateral 114 | var p:Number = random(0.1, 0.9); // vertical (along A-D and B-C) 115 | var q:Number = random(0.1, 0.9); // horizontal (along A-B and D-C) 116 | 117 | // Midpoints 118 | var E:Point = interpolateP(A, D, p); 119 | var F:Point = interpolateP(B, C, p); 120 | var G:Point = interpolateP(A, B, q); 121 | var I:Point = interpolateP(D, C, q); 122 | 123 | // Central point 124 | var H:Point = interpolateP(E, F, q); 125 | 126 | // Divide the quad into subquads, but meet at H 127 | var s:Number = random(-0.4, 0.4); 128 | var t:Number = random(-0.4, 0.4); 129 | 130 | subdivide(A, interpolateP(G, B, s), H, interpolateP(E, D, t)); 131 | points.push(H); 132 | subdivide(H, interpolateP(F, C, s), C, interpolateP(I, D, t)); 133 | } 134 | 135 | points.push(A); 136 | subdivide(A, B, C, D); 137 | points.push(C); 138 | return points; 139 | } 140 | 141 | 142 | // Convenience: random number in a range 143 | public static function random(low:Number, high:Number):Number { 144 | return low + (high-low) * Math.random(); 145 | } 146 | 147 | 148 | // Interpolate between two points 149 | public static function interpolate(p:Vector3D, q:Vector3D, f:Number):Vector3D { 150 | return new Vector3D(p.x*(1-f) + q.x*f, p.y*(1-f) + q.y*f, p.z*(1-f) + q.z*f); 151 | } 152 | 153 | public static function interpolateP(p:Point, q:Point, f:Number):Point { 154 | return Point.interpolate(p, q, 1.0-f); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /prototypes/quadrilateral_drainage_basin.as: -------------------------------------------------------------------------------- 1 | // Draw a fractal quadrilateral drainage basin 2 | // Author: amitp@cs.stanford.edu 3 | // License: MIT 4 | 5 | /* 6 | 7 | Recursive approach: given A-B-C-D, we split horizontally (q) and 8 | vertically (p) and create two quadrilaterals A-G-H-E and G-B-F-H. 9 | 10 | A ----- G ----- B 11 | | | | 12 | E ----- H ----- F 13 | | | 14 | D -------------C 15 | 16 | The river goes from E-H to D-C and H-F to D-C. 17 | 18 | */ 19 | 20 | package { 21 | import flash.geom.*; 22 | import flash.filters.*; 23 | import flash.display.*; 24 | import flash.events.*; 25 | 26 | public class quadrilateral_drainage_basin extends Sprite { 27 | public var river:Shape = new Shape(); 28 | 29 | public function quadrilateral_drainage_basin() { 30 | stage.scaleMode = 'noScale'; 31 | stage.align = 'TL'; 32 | 33 | river.filters = [new GlowFilter(0x000000, 0.5, 2.0, 2.0)]; 34 | addChild(river); 35 | go(); 36 | 37 | stage.addEventListener(MouseEvent.CLICK, function (e:MouseEvent):void { go(); } ); 38 | } 39 | 40 | public function go():void { 41 | river.graphics.clear(); 42 | graphics.clear(); 43 | graphics.beginFill(0x999999); 44 | graphics.drawRect(-1000, -1000, 2000, 2000); 45 | graphics.endFill(); 46 | 47 | if (false) { 48 | // Draw one large quad 49 | drawQuad(new Vector3D(100, 10, 10), new Vector3D(400, 10, 10), 50 | new Vector3D(600, 300, 0), new Vector3D(10, 400, 0), 51 | 50.0); 52 | } else { 53 | // Draw a bunch of quads 54 | var P:Array = [[100, 20], [200, 200], [160, 300], [200, 500], 55 | [400, 80], [480, 220], [540, 280], [520, 430], 56 | [540, 30], [600, 250], [660, 370], [580, 490], 57 | [300, 25], [300, 550]]; 58 | function draw(A:Array, B:Array, C:Array, D:Array):void { 59 | var aV:Vector3D = new Vector3D(A[0], A[1], 1); 60 | var bV:Vector3D = new Vector3D(B[0], B[1], 1); 61 | var cV:Vector3D = new Vector3D(C[0], C[1], 0); 62 | var dV:Vector3D = new Vector3D(D[0], D[1], 0); 63 | var normal:Vector3D = aV.subtract(cV).crossProduct(bV.subtract(dV)); 64 | normal.normalize(); 65 | var volume:Number = 0.0003 * areaOfQuad(aV, bV, cV, dV); 66 | if (normal.x > 0) volume *= 0.5; 67 | drawQuad(aV, bV, cV, dV, volume); 68 | } 69 | draw(P[4], P[5], P[1], P[0]); 70 | draw(P[5], P[6], P[2], P[1]); 71 | draw(P[6], P[7], P[3], P[2]); 72 | draw(P[5], P[4], P[8], P[9]); 73 | draw(P[6], P[5], P[9], P[10]); 74 | draw(P[7], P[6], P[10], P[11]); 75 | 76 | // Coastlines are drawn from midpoint to midpoint 77 | function drawCoast(i:int, j:int, k:int):void { 78 | var pV:Vector3D = interpolate(new Vector3D(P[i][0], P[i][1]), new Vector3D(P[j][0], P[j][1]), 0.5); 79 | var qV:Vector3D = new Vector3D(P[j][0], P[j][1]); 80 | var rV:Vector3D = interpolate(new Vector3D(P[j][0], P[j][1]), new Vector3D(P[k][0], P[k][1]), 0.5); 81 | drawRiver(graphics, pV, qV, 1, 1, 0x000000, (i == 0 && j == 1)); 82 | drawRiver(graphics, qV, rV, 1, 1, 0x000000, false); 83 | } 84 | graphics.beginFill(0xffffff, 0.7); 85 | drawCoast(0, 1, 2); drawCoast(1, 2, 3); 86 | drawCoast(2, 3, 13); drawCoast(3, 13, 11); 87 | drawCoast(13, 11, 10); drawCoast(11, 10, 9); 88 | drawCoast(10, 9, 8); drawCoast(9, 8, 12); 89 | drawCoast(8, 12, 0); drawCoast(12, 0, 1); 90 | graphics.endFill(); 91 | } 92 | } 93 | 94 | 95 | // Area of the planar projection of the quadrilateral down to z=0. 96 | // Used for rainfall estimate. The area of a quadrilateral is half 97 | // the cross product of the diagonals. 98 | public function areaOfQuad(A:Vector3D, B:Vector3D, C:Vector3D, D:Vector3D):Number { 99 | // NOTE: if we wanted to take into account the z value, use 100 | // 0.5 * AC.crossProduct(BD).length 101 | var AC:Vector3D = A.subtract(C); 102 | var BD:Vector3D = B.subtract(D); 103 | return 0.5 * Math.abs(AC.x * BD.y - BD.x * AC.y); 104 | } 105 | 106 | 107 | // TODO: volume should be based on the moisture level times the area of the quad 108 | public function drawQuad(A:Vector3D, B:Vector3D, C:Vector3D, D:Vector3D, volume:Number):void { 109 | // Draw the quadrilateral 110 | var lightingVector:Vector3D = D.subtract(C); 111 | lightingVector.normalize(); 112 | var lighting:Number = 1.0 + lightingVector.dotProduct(new Vector3D(-0.3, -0.3, -0.6)); 113 | lighting *= 0.6; 114 | if (lighting < 0.2) lighting = 0.2; 115 | if (lighting > 1.0) lighting = 1.0; 116 | lighting = 0.9; 117 | var gray:int = 255*lighting; 118 | graphics.beginFill((int(gray*0.8) << 16) | (gray << 8) | (gray>>1)); 119 | graphics.lineStyle(1, 0x000000, 0.1); 120 | graphics.moveTo(A.x, A.y); 121 | graphics.lineTo(B.x, B.y); 122 | graphics.lineTo(C.x, C.y); 123 | graphics.lineTo(D.x, D.y); 124 | graphics.endFill(); 125 | graphics.lineStyle(); 126 | 127 | if (A.subtract(D).length < 5 || B.subtract(C).length < 5) return; 128 | 129 | // Subdivide the quadrilateral 130 | var p:Number = random(0.5, 0.7); // vertical (along A-D and B-C) 131 | var q:Number = random(0.1, 0.9); // horizontal (along A-B and D-C) 132 | 133 | // Midpoints 134 | var E:Vector3D = interpolate(A, D, p); 135 | var F:Vector3D = interpolate(B, C, p); 136 | var G:Vector3D = interpolate(A, B, q); 137 | 138 | // Central point starts out between E and F but doesn't have to be exact 139 | var H:Vector3D = interpolate(E, F, q); 140 | H = interpolate(H, G, random(-0.2, 0.4)); 141 | 142 | // These are the river locations along edges. Right now they're 143 | // all midpoints but we could change that, if we pass the 144 | // position along in the recursive call. 145 | var DC:Vector3D = interpolate(D, C, 0.5); 146 | var DCH:Vector3D = interpolate(DC, interpolate(E, F, random(0.3, 0.7)), random(0.3, 0.7)); 147 | var EH:Vector3D = interpolate(E, H, 0.5); 148 | var HF:Vector3D = interpolate(H, F, 0.5); 149 | 150 | // Adjust elevations 151 | G.z += 0.5; 152 | H.z = DC.z; 153 | 154 | // River widths. The width is the square root of the volume. We 155 | // assume the volume gets divided non-uniformly between the two 156 | // channels, based on q (the larger side is more likely to have 157 | // the larger tributary). Also, the lower portion of the 158 | // quadrilateral contributes water, based on p, so the two 159 | // tributaries don't add up to the full volume. 160 | var v0:Number = volume * p; // random(0.5, 1.0); // how much comes from tributaries 161 | var volumeFromLeft:Number = q * random(1, 2) / random(1, 2); 162 | var v1:Number = v0 * volumeFromLeft; 163 | var v2:Number = v0 - v1; 164 | 165 | // Draw the river, plus its two tributaries 166 | drawRiver(river.graphics, DC, DCH, volume, v0, 0x0000ff); 167 | drawRiver(river.graphics, DCH, EH, v0, v1, 0x0000ff); 168 | drawRiver(river.graphics, DCH, HF, v0, v2, 0x0000ff); 169 | 170 | // Recursively divide the river 171 | drawQuad(A, G, H, E, v1); 172 | drawQuad(G, B, F, H, v2); 173 | } 174 | 175 | // Draw the river from p to q, volume u to v, and return its length 176 | public function drawRiver(g:Graphics, p:Vector3D, q:Vector3D, u:Number, v:Number, color:int, first:Boolean=true):Number { 177 | if (u < 0.25 || v < 0.25) { 178 | return p.subtract(q).length; 179 | } else if (p.subtract(q).length < 4) { 180 | g.lineStyle(Math.sqrt(0.5*(u+v)), color); 181 | if (first) g.moveTo(p.x, p.y); 182 | g.lineTo(q.x, q.y); 183 | g.lineStyle(); 184 | return p.subtract(q).length; 185 | } else { 186 | // Subdivide and randomly move the midpoint 187 | var r:Vector3D = interpolate(p, q, random(0.3, 0.7)); 188 | var perp:Vector3D = p.subtract(q).crossProduct(new Vector3D(0, 0, 1)); 189 | perp.scaleBy(random(-0.25, +0.25)); 190 | r.incrementBy(perp); 191 | return drawRiver(g, p, r, u, 0.5*(u+v), color, first) 192 | + drawRiver(g, r, q, 0.5*(u+v), v, color, false); 193 | } 194 | } 195 | 196 | 197 | // Convenience: random number in a range 198 | public static function random(low:Number, high:Number):Number { 199 | return low + (high-low) * Math.random(); 200 | } 201 | 202 | 203 | // Interpolate between two points 204 | public function interpolate(p:Vector3D, q:Vector3D, f:Number):Vector3D { 205 | return new Vector3D(p.x*(1-f) + q.x*f, p.y*(1-f) + q.y*f, p.z*(1-f) + q.z*f); 206 | } 207 | 208 | 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /third-party/PM_PRNG/de/polygonal/math/PM_PRNG.as: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2009 Michael Baczynski, http://www.polygonal.de 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining 5 | * a copy of this software and associated documentation files (the 6 | * "Software"), to deal in the Software without restriction, including 7 | * without limitation the rights to use, copy, modify, merge, publish, 8 | * distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so, subject to 10 | * the following conditions: 11 | * The above copyright notice and this permission notice shall be 12 | * included in all copies or substantial portions of the Software. 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 19 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | /** 23 | * Implementation of the Park Miller (1988) "minimal standard" linear 24 | * congruential pseudo-random number generator. 25 | * 26 | * For a full explanation visit: http://www.firstpr.com.au/dsp/rand31/ 27 | * 28 | * The generator uses a modulus constant (m) of 2^31 - 1 which is a 29 | * Mersenne Prime number and a full-period-multiplier of 16807. 30 | * Output is a 31 bit unsigned integer. The range of values output is 31 | * 1 to 2,147,483,646 (2^31-1) and the seed must be in this range too. 32 | * 33 | * David G. Carta's optimisation which needs only 32 bit integer math, 34 | * and no division is actually *slower* in flash (both AS2 & AS3) so 35 | * it's better to use the double-precision floating point version. 36 | * 37 | * @author Michael Baczynski, www.polygonal.de 38 | */ 39 | package de.polygonal.math 40 | { 41 | public class PM_PRNG 42 | { 43 | /** 44 | * set seed with a 31 bit unsigned integer 45 | * between 1 and 0X7FFFFFFE inclusive. don't use 0! 46 | */ 47 | public var seed:uint; 48 | 49 | public function PM_PRNG() 50 | { 51 | seed = 1; 52 | } 53 | 54 | /** 55 | * provides the next pseudorandom number 56 | * as an unsigned integer (31 bits) 57 | */ 58 | public function nextInt():uint 59 | { 60 | return gen(); 61 | } 62 | 63 | /** 64 | * provides the next pseudorandom number 65 | * as a float between nearly 0 and nearly 1.0. 66 | */ 67 | public function nextDouble():Number 68 | { 69 | return (gen() / 2147483647); 70 | } 71 | 72 | /** 73 | * provides the next pseudorandom number 74 | * as an unsigned integer (31 bits) betweeen 75 | * a given range. 76 | */ 77 | public function nextIntRange(min:Number, max:Number):uint 78 | { 79 | min -= .4999; 80 | max += .4999; 81 | return Math.round(min + ((max - min) * nextDouble())); 82 | } 83 | 84 | /** 85 | * provides the next pseudorandom number 86 | * as a float between a given range. 87 | */ 88 | public function nextDoubleRange(min:Number, max:Number):Number 89 | { 90 | return min + ((max - min) * nextDouble()); 91 | } 92 | 93 | /** 94 | * generator: 95 | * new-value = (old-value * 16807) mod (2^31 - 1) 96 | */ 97 | private function gen():uint 98 | { 99 | //integer version 1, for max int 2^46 - 1 or larger. 100 | return seed = (seed * 16807) % 2147483647; 101 | 102 | /** 103 | * integer version 2, for max int 2^31 - 1 (slowest) 104 | */ 105 | //var test:int = 16807 * (seed % 127773 >> 0) - 2836 * (seed / 127773 >> 0); 106 | //return seed = (test > 0 ? test : test + 2147483647); 107 | 108 | /** 109 | * david g. carta's optimisation is 15% slower than integer version 1 110 | */ 111 | //var hi:uint = 16807 * (seed >> 16); 112 | //var lo:uint = 16807 * (seed & 0xFFFF) + ((hi & 0x7FFF) << 16) + (hi >> 15); 113 | //return seed = (lo > 0x7FFFFFFF ? lo - 0x7FFFFFFF : lo); 114 | } 115 | } 116 | } --------------------------------------------------------------------------------