├── .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 | [](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 | ;
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 | }
--------------------------------------------------------------------------------