├── README.md ├── index.html ├── random.js ├── perlin.js └── scallywag.js /README.md: -------------------------------------------------------------------------------- 1 | # Scallywag 2 | 3 | *Scallywag* is an attempt at implementing a "Procedural Pirate Map" as proposed by the First Monthly Challenge on /r/proceduralgeneration 4 | 5 | https://www.reddit.com/r/proceduralgeneration/comments/3vcbb3/monthly_challenge_1_dec_2015_procedural_pirate_map/ 6 | 7 | Enjoy the [**demo**](https://rawgit.com/wlievens/scallywag/master/index.html) 8 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scallywag! 5 | 6 | 7 | 8 | 9 | 10 |

Scallywag!

11 | Seed number: 12 | 13 | 14 |
15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /random.js: -------------------------------------------------------------------------------- 1 | /** 2 | @class A fast, deterministic, seedable random number generator. 3 | @description Unlike the native random number generator built into most browsers, this one is deterministic, and so it will produce the same sequence of outputs each time it is given the same seed. It is based on George Marsaglia's MWC algorithm from the v8 Javascript engine. 4 | */ 5 | 6 | function SeedableRandom() { 7 | /** 8 | Get the next random number between 0 and 1 in the current sequence. 9 | */ 10 | this.next = function next() { 11 | // Random number generator using George Marsaglia's MWC algorithm. 12 | // Got this from the v8 js engine 13 | 14 | // don't let them get stuck 15 | if (this.x == 0) this.x == -1; 16 | if (this.y == 0) this.y == -1; 17 | 18 | // Mix the bits. 19 | this.x = this.nextX(); 20 | this.y = this.nextY(); 21 | return ((this.x << 16) + (this.y & 0xFFFF)) / 0xFFFFFFFF + 0.5; 22 | } 23 | 24 | this.nextX = function() { 25 | return 36969 * (this.x & 0xFFFF) + (this.x >> 16); 26 | } 27 | 28 | this.nextY = function() { 29 | return 18273 * (this.y & 0xFFFF) + (this.y >> 16); 30 | } 31 | 32 | /** 33 | Get the next random integer in the current sequence. 34 | @param a The lower bound of integers (inclusive). 35 | @param gs The upper bound of integers (exclusive). 36 | */ 37 | this.nextInt = function nextInt(a, b) { 38 | if (!b) { 39 | a = 0; 40 | b = 0xFFFFFFFF; 41 | } 42 | // fetch an integer between a and b inclusive 43 | return Math.floor(this.next() * (b - a)) + a; 44 | } 45 | 46 | /** 47 | Seed the random number generator. The same seed will always yield the same sequence. Seed with the current time if you want it to vary each time. 48 | @param x The seed. 49 | */ 50 | this.seed = function(x) { 51 | this.x = x * 3253; 52 | this.y = this.nextX(); 53 | } 54 | 55 | /** 56 | Seed the random number generator with a two dimensional seed. 57 | @param x First seed. 58 | @param y Second seed. 59 | */ 60 | this.seed2d = function seed(x, y) { 61 | this.x = x * 2549 + y * 3571; 62 | this.y = y * 2549 + x * 3571; 63 | } 64 | 65 | /** 66 | Seed the random number generator with a three dimensional seed. 67 | @param x First seed. 68 | @param y Second seed. 69 | @param z Third seed. 70 | */ 71 | this.seed3d = function seed(x, y, z) { 72 | this.x = x * 2549 + y * 3571 + z * 3253; 73 | this.y = x * 3253 + y * 2549 + z * 3571; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /perlin.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A speed-improved perlin and simplex noise algorithms for 2D. 3 | * 4 | * Based on example code by Stefan Gustavson (stegu@itn.liu.se). 5 | * Optimisations by Peter Eastman (peastman@drizzle.stanford.edu). 6 | * Better rank ordering method by Stefan Gustavson in 2012. 7 | * Converted to Javascript by Joseph Gentle. 8 | * 9 | * Version 2012-03-09 10 | * 11 | * This code was placed in the public domain by its original author, 12 | * Stefan Gustavson. You may use it as you see fit, but 13 | * attribution is appreciated. 14 | * 15 | */ 16 | 17 | (function(global){ 18 | var module = global.noise = {}; 19 | 20 | function Grad(x, y, z) { 21 | this.x = x; this.y = y; this.z = z; 22 | } 23 | 24 | Grad.prototype.dot2 = function(x, y) { 25 | return this.x*x + this.y*y; 26 | }; 27 | 28 | Grad.prototype.dot3 = function(x, y, z) { 29 | return this.x*x + this.y*y + this.z*z; 30 | }; 31 | 32 | var grad3 = [new Grad(1,1,0),new Grad(-1,1,0),new Grad(1,-1,0),new Grad(-1,-1,0), 33 | new Grad(1,0,1),new Grad(-1,0,1),new Grad(1,0,-1),new Grad(-1,0,-1), 34 | new Grad(0,1,1),new Grad(0,-1,1),new Grad(0,1,-1),new Grad(0,-1,-1)]; 35 | 36 | var p = [151,160,137,91,90,15, 37 | 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, 38 | 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, 39 | 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, 40 | 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, 41 | 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, 42 | 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, 43 | 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, 44 | 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, 45 | 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, 46 | 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, 47 | 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, 48 | 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180]; 49 | // To remove the need for index wrapping, double the permutation table length 50 | var perm = new Array(512); 51 | var gradP = new Array(512); 52 | 53 | // This isn't a very good seeding function, but it works ok. It supports 2^16 54 | // different seed values. Write something better if you need more seeds. 55 | module.seed = function(seed) { 56 | if(seed > 0 && seed < 1) { 57 | // Scale the seed out 58 | seed *= 65536; 59 | } 60 | 61 | seed = Math.floor(seed); 62 | if(seed < 256) { 63 | seed |= seed << 8; 64 | } 65 | 66 | for(var i = 0; i < 256; i++) { 67 | var v; 68 | if (i & 1) { 69 | v = p[i] ^ (seed & 255); 70 | } else { 71 | v = p[i] ^ ((seed>>8) & 255); 72 | } 73 | 74 | perm[i] = perm[i + 256] = v; 75 | gradP[i] = gradP[i + 256] = grad3[v % 12]; 76 | } 77 | }; 78 | 79 | module.seed(0); 80 | 81 | /* 82 | for(var i=0; i<256; i++) { 83 | perm[i] = perm[i + 256] = p[i]; 84 | gradP[i] = gradP[i + 256] = grad3[perm[i] % 12]; 85 | }*/ 86 | 87 | // Skewing and unskewing factors for 2, 3, and 4 dimensions 88 | var F2 = 0.5*(Math.sqrt(3)-1); 89 | var G2 = (3-Math.sqrt(3))/6; 90 | 91 | var F3 = 1/3; 92 | var G3 = 1/6; 93 | 94 | // 2D simplex noise 95 | module.simplex2 = function(xin, yin) { 96 | var n0, n1, n2; // Noise contributions from the three corners 97 | // Skew the input space to determine which simplex cell we're in 98 | var s = (xin+yin)*F2; // Hairy factor for 2D 99 | var i = Math.floor(xin+s); 100 | var j = Math.floor(yin+s); 101 | var t = (i+j)*G2; 102 | var x0 = xin-i+t; // The x,y distances from the cell origin, unskewed. 103 | var y0 = yin-j+t; 104 | // For the 2D case, the simplex shape is an equilateral triangle. 105 | // Determine which simplex we are in. 106 | var i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords 107 | if(x0>y0) { // lower triangle, XY order: (0,0)->(1,0)->(1,1) 108 | i1=1; j1=0; 109 | } else { // upper triangle, YX order: (0,0)->(0,1)->(1,1) 110 | i1=0; j1=1; 111 | } 112 | // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and 113 | // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where 114 | // c = (3-sqrt(3))/6 115 | var x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords 116 | var y1 = y0 - j1 + G2; 117 | var x2 = x0 - 1 + 2 * G2; // Offsets for last corner in (x,y) unskewed coords 118 | var y2 = y0 - 1 + 2 * G2; 119 | // Work out the hashed gradient indices of the three simplex corners 120 | i &= 255; 121 | j &= 255; 122 | var gi0 = gradP[i+perm[j]]; 123 | var gi1 = gradP[i+i1+perm[j+j1]]; 124 | var gi2 = gradP[i+1+perm[j+1]]; 125 | // Calculate the contribution from the three corners 126 | var t0 = 0.5 - x0*x0-y0*y0; 127 | if(t0<0) { 128 | n0 = 0; 129 | } else { 130 | t0 *= t0; 131 | n0 = t0 * t0 * gi0.dot2(x0, y0); // (x,y) of grad3 used for 2D gradient 132 | } 133 | var t1 = 0.5 - x1*x1-y1*y1; 134 | if(t1<0) { 135 | n1 = 0; 136 | } else { 137 | t1 *= t1; 138 | n1 = t1 * t1 * gi1.dot2(x1, y1); 139 | } 140 | var t2 = 0.5 - x2*x2-y2*y2; 141 | if(t2<0) { 142 | n2 = 0; 143 | } else { 144 | t2 *= t2; 145 | n2 = t2 * t2 * gi2.dot2(x2, y2); 146 | } 147 | // Add contributions from each corner to get the final noise value. 148 | // The result is scaled to return values in the interval [-1,1]. 149 | return 70 * (n0 + n1 + n2); 150 | }; 151 | 152 | // 3D simplex noise 153 | module.simplex3 = function(xin, yin, zin) { 154 | var n0, n1, n2, n3; // Noise contributions from the four corners 155 | 156 | // Skew the input space to determine which simplex cell we're in 157 | var s = (xin+yin+zin)*F3; // Hairy factor for 2D 158 | var i = Math.floor(xin+s); 159 | var j = Math.floor(yin+s); 160 | var k = Math.floor(zin+s); 161 | 162 | var t = (i+j+k)*G3; 163 | var x0 = xin-i+t; // The x,y distances from the cell origin, unskewed. 164 | var y0 = yin-j+t; 165 | var z0 = zin-k+t; 166 | 167 | // For the 3D case, the simplex shape is a slightly irregular tetrahedron. 168 | // Determine which simplex we are in. 169 | var i1, j1, k1; // Offsets for second corner of simplex in (i,j,k) coords 170 | var i2, j2, k2; // Offsets for third corner of simplex in (i,j,k) coords 171 | if(x0 >= y0) { 172 | if(y0 >= z0) { i1=1; j1=0; k1=0; i2=1; j2=1; k2=0; } 173 | else if(x0 >= z0) { i1=1; j1=0; k1=0; i2=1; j2=0; k2=1; } 174 | else { i1=0; j1=0; k1=1; i2=1; j2=0; k2=1; } 175 | } else { 176 | if(y0 < z0) { i1=0; j1=0; k1=1; i2=0; j2=1; k2=1; } 177 | else if(x0 < z0) { i1=0; j1=1; k1=0; i2=0; j2=1; k2=1; } 178 | else { i1=0; j1=1; k1=0; i2=1; j2=1; k2=0; } 179 | } 180 | // A step of (1,0,0) in (i,j,k) means a step of (1-c,-c,-c) in (x,y,z), 181 | // a step of (0,1,0) in (i,j,k) means a step of (-c,1-c,-c) in (x,y,z), and 182 | // a step of (0,0,1) in (i,j,k) means a step of (-c,-c,1-c) in (x,y,z), where 183 | // c = 1/6. 184 | var x1 = x0 - i1 + G3; // Offsets for second corner 185 | var y1 = y0 - j1 + G3; 186 | var z1 = z0 - k1 + G3; 187 | 188 | var x2 = x0 - i2 + 2 * G3; // Offsets for third corner 189 | var y2 = y0 - j2 + 2 * G3; 190 | var z2 = z0 - k2 + 2 * G3; 191 | 192 | var x3 = x0 - 1 + 3 * G3; // Offsets for fourth corner 193 | var y3 = y0 - 1 + 3 * G3; 194 | var z3 = z0 - 1 + 3 * G3; 195 | 196 | // Work out the hashed gradient indices of the four simplex corners 197 | i &= 255; 198 | j &= 255; 199 | k &= 255; 200 | var gi0 = gradP[i+ perm[j+ perm[k ]]]; 201 | var gi1 = gradP[i+i1+perm[j+j1+perm[k+k1]]]; 202 | var gi2 = gradP[i+i2+perm[j+j2+perm[k+k2]]]; 203 | var gi3 = gradP[i+ 1+perm[j+ 1+perm[k+ 1]]]; 204 | 205 | // Calculate the contribution from the four corners 206 | var t0 = 0.6 - x0*x0 - y0*y0 - z0*z0; 207 | if(t0<0) { 208 | n0 = 0; 209 | } else { 210 | t0 *= t0; 211 | n0 = t0 * t0 * gi0.dot3(x0, y0, z0); // (x,y) of grad3 used for 2D gradient 212 | } 213 | var t1 = 0.6 - x1*x1 - y1*y1 - z1*z1; 214 | if(t1<0) { 215 | n1 = 0; 216 | } else { 217 | t1 *= t1; 218 | n1 = t1 * t1 * gi1.dot3(x1, y1, z1); 219 | } 220 | var t2 = 0.6 - x2*x2 - y2*y2 - z2*z2; 221 | if(t2<0) { 222 | n2 = 0; 223 | } else { 224 | t2 *= t2; 225 | n2 = t2 * t2 * gi2.dot3(x2, y2, z2); 226 | } 227 | var t3 = 0.6 - x3*x3 - y3*y3 - z3*z3; 228 | if(t3<0) { 229 | n3 = 0; 230 | } else { 231 | t3 *= t3; 232 | n3 = t3 * t3 * gi3.dot3(x3, y3, z3); 233 | } 234 | // Add contributions from each corner to get the final noise value. 235 | // The result is scaled to return values in the interval [-1,1]. 236 | return 32 * (n0 + n1 + n2 + n3); 237 | 238 | }; 239 | 240 | // ##### Perlin noise stuff 241 | 242 | function fade(t) { 243 | return t*t*t*(t*(t*6-15)+10); 244 | } 245 | 246 | function lerp(a, b, t) { 247 | return (1-t)*a + t*b; 248 | } 249 | 250 | // 2D Perlin Noise 251 | module.perlin2 = function(x, y) { 252 | // Find unit grid cell containing point 253 | var X = Math.floor(x), Y = Math.floor(y); 254 | // Get relative xy coordinates of point within that cell 255 | x = x - X; y = y - Y; 256 | // Wrap the integer cells at 255 (smaller integer period can be introduced here) 257 | X = X & 255; Y = Y & 255; 258 | 259 | // Calculate noise contributions from each of the four corners 260 | var n00 = gradP[X+perm[Y]].dot2(x, y); 261 | var n01 = gradP[X+perm[Y+1]].dot2(x, y-1); 262 | var n10 = gradP[X+1+perm[Y]].dot2(x-1, y); 263 | var n11 = gradP[X+1+perm[Y+1]].dot2(x-1, y-1); 264 | 265 | // Compute the fade curve value for x 266 | var u = fade(x); 267 | 268 | // Interpolate the four results 269 | return lerp( 270 | lerp(n00, n10, u), 271 | lerp(n01, n11, u), 272 | fade(y)); 273 | }; 274 | 275 | // 3D Perlin Noise 276 | module.perlin3 = function(x, y, z) { 277 | // Find unit grid cell containing point 278 | var X = Math.floor(x), Y = Math.floor(y), Z = Math.floor(z); 279 | // Get relative xyz coordinates of point within that cell 280 | x = x - X; y = y - Y; z = z - Z; 281 | // Wrap the integer cells at 255 (smaller integer period can be introduced here) 282 | X = X & 255; Y = Y & 255; Z = Z & 255; 283 | 284 | // Calculate noise contributions from each of the eight corners 285 | var n000 = gradP[X+ perm[Y+ perm[Z ]]].dot3(x, y, z); 286 | var n001 = gradP[X+ perm[Y+ perm[Z+1]]].dot3(x, y, z-1); 287 | var n010 = gradP[X+ perm[Y+1+perm[Z ]]].dot3(x, y-1, z); 288 | var n011 = gradP[X+ perm[Y+1+perm[Z+1]]].dot3(x, y-1, z-1); 289 | var n100 = gradP[X+1+perm[Y+ perm[Z ]]].dot3(x-1, y, z); 290 | var n101 = gradP[X+1+perm[Y+ perm[Z+1]]].dot3(x-1, y, z-1); 291 | var n110 = gradP[X+1+perm[Y+1+perm[Z ]]].dot3(x-1, y-1, z); 292 | var n111 = gradP[X+1+perm[Y+1+perm[Z+1]]].dot3(x-1, y-1, z-1); 293 | 294 | // Compute the fade curve value for x, y, z 295 | var u = fade(x); 296 | var v = fade(y); 297 | var w = fade(z); 298 | 299 | // Interpolate 300 | return lerp( 301 | lerp( 302 | lerp(n000, n100, u), 303 | lerp(n001, n101, u), w), 304 | lerp( 305 | lerp(n010, n110, u), 306 | lerp(n011, n111, u), w), 307 | v); 308 | }; 309 | 310 | })(this); 311 | -------------------------------------------------------------------------------- /scallywag.js: -------------------------------------------------------------------------------- 1 | function generatePirateMap(canvas, seed) 2 | { 3 | var DEBUG = false; 4 | 5 | var seaLevel = -0.30; 6 | var mountainLevel = +0.22; 7 | var margin = 12; 8 | var borderSegmentSize = 30; 9 | var coastRange = 12; 10 | var mountainRange = 6; 11 | 12 | function logMatrix(data, w, h) { 13 | var offset = 0; 14 | var line = ''; 15 | 16 | for (var x = 0; x < w; x++) { 17 | var xx = (x < 10 ? ' ' : '') + x; 18 | line += xx + ","; 19 | } 20 | console.log(" " + line); 21 | 22 | for (var y = 0; y < h; y++) { 23 | line = ''; 24 | for (var x = 0; x < w; x++) { 25 | var d = data[offset].toFixed(); 26 | if (d.length < 2) 27 | d = ' ' + d; 28 | 29 | line += d + ","; 30 | offset++; 31 | } 32 | 33 | var yy = (y < 10 ? ' ' : '') + y; 34 | console.log(yy + " " + line); 35 | } 36 | }; 37 | 38 | function UnionFind() 39 | { 40 | this.parent = []; 41 | this.rank = []; 42 | } 43 | 44 | UnionFind.prototype.add = function(value) 45 | { 46 | parent = this.parent; 47 | rank = this.rank; 48 | while (parent.length <= value) 49 | { 50 | parent.push(parent.length); 51 | rank.push(0); 52 | } 53 | }; 54 | 55 | UnionFind.prototype.union = function(a, b) 56 | { 57 | parent = this.parent; 58 | rank = this.rank; 59 | aroot = this.find(a); 60 | broot = this.find(b); 61 | if (aroot != broot) 62 | { 63 | if (rank[aroot] < rank[broot]) 64 | { 65 | parent[aroot] = broot; 66 | } 67 | else if (rank[aroot] > rank[broot]) 68 | { 69 | parent[broot] = aroot; 70 | } 71 | else 72 | { 73 | parent[broot] = aroot; 74 | rank[aroot]++; 75 | } 76 | } 77 | }; 78 | 79 | UnionFind.prototype.find = function(value) 80 | { 81 | parent = this.parent; 82 | if (parent[value] != value) 83 | parent[value] = this.find(parent[value]); 84 | return parent[value]; 85 | }; 86 | 87 | function labelConnectedComponents(input, width, height) 88 | { 89 | output = []; 90 | var nextLabel = 1; 91 | var equivalences = new UnionFind(); 92 | for (var y = 0; y < height; ++y) 93 | { 94 | for (var x = 0; x < width; ++x) 95 | { 96 | output.push(0); 97 | var index = x + y * width; 98 | if (input[index]) 99 | { 100 | var label = 0; 101 | var west = 0; 102 | var north = 0; 103 | if (y > 0 && input[index - width]) 104 | { 105 | north = output[index - width]; 106 | } 107 | if (x > 0 && input[index - 1]) 108 | { 109 | west = output[index - 1]; 110 | } 111 | if (west) 112 | { 113 | if (north) 114 | { 115 | if (west == north) 116 | { 117 | label = west; 118 | } 119 | else 120 | { 121 | min = Math.min(west, north); 122 | max = Math.max(west, north); 123 | label = min; 124 | equivalences.union(min, max); 125 | } 126 | } 127 | else 128 | { 129 | label = west; 130 | } 131 | } 132 | else 133 | { 134 | if (north) 135 | { 136 | label = north; 137 | } 138 | else 139 | { 140 | label = nextLabel++; 141 | equivalences.add(label); 142 | } 143 | } 144 | output[index] = label; 145 | } 146 | } 147 | } 148 | 149 | for (var y = 0; y < height; ++y) 150 | { 151 | for (var x = 0; x < width; ++x) 152 | { 153 | var index = x + y * width; 154 | var label = output[index]; 155 | if (label) 156 | { 157 | output[index] = equivalences.find(label); 158 | } 159 | } 160 | } 161 | 162 | return output; 163 | } 164 | 165 | var canvasWidth = canvas.width; 166 | var canvasHeight = canvas.height; 167 | var borderSegmentsX = Math.floor((canvasWidth - margin * 2) / borderSegmentSize); 168 | var borderSegmentsY = Math.floor((canvasHeight - margin * 2) / borderSegmentSize); 169 | borderSegmentsX -= (1 - borderSegmentsX % 2); 170 | borderSegmentsY -= (1 - borderSegmentsY % 2); 171 | 172 | var width = margin * 2 + borderSegmentsX * borderSegmentSize; 173 | var height = margin * 2 + borderSegmentsY * borderSegmentSize; 174 | var maxSize = Math.max(width, height); 175 | var minSize = Math.min(width, height); 176 | 177 | random = new SeedableRandom(); 178 | random.seed(seed); 179 | 180 | noise.seed(random.nextInt(0, 0xFFFF)); 181 | 182 | canvas.onclick = function(e) 183 | { 184 | var mouseX, mouseY; 185 | 186 | if(e.offsetX) { 187 | mouseX = e.offsetX; 188 | mouseY = e.offsetY; 189 | } 190 | else if(e.layerX) { 191 | mouseX = e.layerX; 192 | mouseY = e.layerY; 193 | } 194 | alert(mouseX + ", " + mouseY); 195 | }; 196 | 197 | function distance(x1, y1, x2, y2) 198 | { 199 | var dx = x1 - x2; 200 | var dy = y1 - y2; 201 | return Math.sqrt(dx * dx + dy * dy); 202 | } 203 | 204 | function distance4(x1, y1, x2, y2) 205 | { 206 | var dx = Math.abs(x1 - x2); 207 | var dy = Math.abs(y1 - y2); 208 | return dx + dy; 209 | } 210 | 211 | function distance8(x1, y1, x2, y2) 212 | { 213 | var dx = Math.abs(x1 - x2); 214 | var dy = Math.abs(y1 - y2); 215 | return Math.max(dx, dy); 216 | } 217 | 218 | function isLand(height) 219 | { 220 | return height >= seaLevel; 221 | } 222 | 223 | function isMountain(height) 224 | { 225 | return height >= mountainLevel; 226 | } 227 | 228 | var gfx = canvas.getContext('2d'); 229 | gfx.lineWidth = 0; 230 | gfx.fillStyle = 'white'; 231 | gfx.beginPath(); 232 | gfx.rect(0, 0, canvasWidth, canvasHeight); 233 | gfx.fill(); 234 | gfx.closePath(); 235 | 236 | var image = gfx.createImageData(width, height); 237 | var data = image.data; 238 | var heightMap = []; 239 | for (var y = 0; y < height; ++y) 240 | { 241 | for (var x = 0; x < width; ++x) 242 | { 243 | edge = minSize * 0.7; 244 | centerX = width * 0.5; 245 | centerY = height * 0.5; 246 | dist = distance(x, y, centerX, centerY); 247 | scale = 4.0; 248 | sx = scale * x / maxSize; 249 | sy = scale * y / maxSize; 250 | var octaves = 5; 251 | var value = 0; 252 | persistence = 0.65; 253 | for (var n = 0; n < octaves; ++n) 254 | { 255 | var frequency = Math.pow(2, n); 256 | var amplitude = Math.pow(persistence, n); 257 | value += noise.perlin2(sx * frequency, sy * frequency) * amplitude; 258 | } 259 | fall = 1.8 * Math.pow(dist / edge, 1.5); 260 | mapHeight = value - fall; 261 | heightMap.push(mapHeight); 262 | } 263 | } 264 | 265 | // Run CCL to find the islands 266 | var landMask = []; 267 | for (var y = 0; y < height; ++y) 268 | { 269 | for (var x = 0; x < width; ++x) 270 | { 271 | landMask.push(isLand(heightMap[x + y * width]) ? 1 : 0); 272 | } 273 | } 274 | islandMap = labelConnectedComponents(landMask, width, height); 275 | 276 | // Compute the area of each island 277 | var islandAreas = {}; 278 | for (var y = 0; y < height; ++y) 279 | { 280 | for (var x = 0; x < width; ++x) 281 | { 282 | var island = islandMap[x + y * width]; 283 | if (island) 284 | { 285 | if (island in islandAreas) 286 | { 287 | islandAreas[island]++; 288 | } 289 | else 290 | { 291 | islandAreas[island] = 1; 292 | } 293 | } 294 | } 295 | } 296 | 297 | // Find the largest island 298 | var largestIsland = 0; 299 | var largestIslandArea = 0; 300 | for (island in islandAreas) 301 | { 302 | area = islandAreas[island]; 303 | if (area > largestIslandArea) 304 | { 305 | largestIsland = island; 306 | largestIslandArea = area; 307 | } 308 | } 309 | 310 | // Find the bounds of the largest island 311 | var largestIslandBounds = {x1: width, y1: height, x2: 0, y2: 0}; 312 | for (var y = 0; y < height; ++y) 313 | { 314 | for (var x = 0; x < width; ++x) 315 | { 316 | if (islandMap[x + y * width] == largestIsland) 317 | { 318 | largestIslandBounds.x1 = Math.min(largestIslandBounds.x1, x); 319 | largestIslandBounds.y1 = Math.min(largestIslandBounds.y1, y); 320 | largestIslandBounds.x2 = Math.max(largestIslandBounds.x2, x); 321 | largestIslandBounds.y2 = Math.max(largestIslandBounds.y2, y); 322 | } 323 | } 324 | } 325 | largestIslandBounds.width = largestIslandBounds.x2 - largestIslandBounds.x1 + 1; 326 | largestIslandBounds.height = largestIslandBounds.y2 - largestIslandBounds.y1 + 1; 327 | 328 | function buildRangeMap(heightMap, discriminator, maxRange) 329 | { 330 | var map = []; 331 | for (var y = 0; y < height; ++y) 332 | { 333 | for (var x = 0; x < width; ++x) 334 | { 335 | var index = x + y * width; 336 | value = discriminator(heightMap[index]); 337 | var nx1 = Math.max(0, x - maxRange); 338 | var ny1 = Math.max(0, y - maxRange); 339 | var nx2 = Math.min(width - 1, x + maxRange); 340 | var ny2 = Math.min(height - 1, y + maxRange); 341 | var minDist = maxRange; 342 | for (var ny = ny1; ny <= ny2; ++ny) 343 | { 344 | for (var nx = nx1; nx <= nx2; ++nx) 345 | { 346 | nindex = nx + ny * width; 347 | var nvalue = discriminator(heightMap[nindex]); 348 | if (nvalue != value) 349 | { 350 | dist = distance(nx, ny, x, y); 351 | if (dist < minDist) 352 | { 353 | minDist = dist; 354 | } 355 | } 356 | } 357 | } 358 | map[index] = minDist; 359 | } 360 | } 361 | return map; 362 | } 363 | 364 | // Determine coast distances 365 | var coastMap = buildRangeMap(heightMap, isLand, coastRange); 366 | 367 | // Determine mountain distances 368 | var mountainMap = buildRangeMap(heightMap, isMountain, mountainRange); 369 | 370 | // Determine the goal 371 | function pickIslandPoint(island, others, minDistance) 372 | { 373 | var attempts = 0; 374 | while (true) 375 | { 376 | var x = largestIslandBounds.x1 + random.nextInt(0, largestIslandBounds.width); 377 | var y = largestIslandBounds.y1 + random.nextInt(0, largestIslandBounds.height); 378 | var index = x + y * width; 379 | if (islandMap[index] == island && coastMap[index] == coastRange) 380 | { 381 | var valid = true; 382 | if (others) 383 | { 384 | for (var n = 0; n < others.length; ++n) 385 | { 386 | var dist = distance(others[n].x, others[n].y, x, y); 387 | if (dist < minDistance) 388 | { 389 | valid = false; 390 | break; 391 | } 392 | } 393 | } 394 | if (valid) 395 | { 396 | return {x: x, y: y}; 397 | } 398 | } 399 | ++attempts; 400 | if (attempts > 1000) 401 | { 402 | return null; 403 | } 404 | } 405 | } 406 | 407 | // Pick the treasure goal 408 | var goal = pickIslandPoint(largestIsland); 409 | 410 | var points = []; 411 | var path = []; 412 | if (goal) 413 | { 414 | // Pick the points that will make up the path 415 | points.push(goal); 416 | for (var n = 0; n < 50; ++n) 417 | { 418 | var point = pickIslandPoint(largestIsland, points, 30.0); 419 | if (point) 420 | { 421 | points.push(point); 422 | } 423 | } 424 | 425 | // Sort them sensibly 426 | var current = goal; 427 | while (points.length) 428 | { 429 | // Pick the nearest 430 | var nearest = null; 431 | var nearestIndex = 0; 432 | var nearestDist = 0; 433 | for (var n = 0; n < points.length; ++n) 434 | { 435 | var point = points[n]; 436 | var dist = distance(point.x, point.y, current.x, current.y); 437 | if (nearest == null || dist < nearestDist) 438 | { 439 | nearest = point; 440 | nearestIndex = n; 441 | nearestDist = dist; 442 | } 443 | } 444 | if (nearest) 445 | { 446 | if (nearestDist < 80) 447 | { 448 | // Test if the path is mostly land of the same island 449 | var valid = true; 450 | if (nearest != goal) 451 | { 452 | var steps = Math.ceil(nearestDist); 453 | var good = 0; 454 | var bad = 0; 455 | for (var n = 0; n <= steps; ++n) 456 | { 457 | var r = n / steps; 458 | var x = Math.round(current.x + (nearest.x - current.x) * r); 459 | var y = Math.round(current.y + (nearest.y - current.y) * r); 460 | var index = x + y * width; 461 | if (islandMap[index] == largestIsland && !isMountain(heightMap[index])) 462 | { 463 | ++good; 464 | } 465 | else 466 | { 467 | ++bad; 468 | } 469 | } 470 | valid = (good / (good + bad)) > 0.80; 471 | } 472 | if (valid) 473 | { 474 | path.push(nearest); 475 | current = nearest; 476 | } 477 | } 478 | points.splice(nearestIndex, 1); 479 | } 480 | } 481 | } 482 | 483 | 484 | var rgbLand = [ 200, 190, 120 ]; 485 | var rgbMountain = [ 170, 160, 125 ]; 486 | var rgbSea = [ 190, 200, 180 ]; 487 | var rgbBorder = [ 60, 60, 60 ]; 488 | var rgbBorderFill1 = [ 240, 240, 240 ]; 489 | var rgbBorderFill2 = [ 160, 0, 40 ]; 490 | 491 | for (var y = 0; y < height; ++y) 492 | { 493 | for (var x = 0; x < width; ++x) 494 | { 495 | var index = x + y * width; 496 | factor = 1; 497 | if (x > margin && y > margin && x < width - margin - 1 && y < height - margin - 1) 498 | { 499 | mapHeight = heightMap[index]; 500 | coastRatio = coastMap[index] / coastRange; 501 | mountainRatio = mountainMap[index] / mountainRange; 502 | ratio = Math.min(coastRatio, mountainRatio); 503 | land = isLand(mapHeight); 504 | mountain = isMountain(mapHeight); 505 | rgb = mountain ? rgbMountain : land ? rgbLand : rgbSea; 506 | if (land) 507 | { 508 | factor = 0.75 + 0.2 * Math.pow(ratio, 2.0); 509 | } 510 | else 511 | { 512 | if (ratio == 1) 513 | { 514 | factor = 1; 515 | } 516 | else if (ratio >= 0.5) 517 | { 518 | factor = 0.5 + 0.5 * ratio; 519 | } 520 | else 521 | { 522 | factor = 1 - ratio * 0.5; 523 | } 524 | } 525 | } 526 | else 527 | { 528 | rgb = rgbBorderFill1; 529 | if (x == 0 || y == 0 || x == width - 1 || y == height - 1 || 530 | ((x == margin || x == width - margin - 1) && y >= margin && y <= height - margin - 1) || 531 | ((x >= margin && x <= width - margin - 1) && (y == margin || y == height - margin - 1))) 532 | { 533 | rgb = rgbBorder; 534 | } 535 | else if (y < margin || y >= height - margin) 536 | { 537 | segment = Math.floor((x - margin) / borderSegmentSize); 538 | if (segment % 2 == 0) 539 | { 540 | rgb = rgbBorderFill2; 541 | } 542 | } 543 | else if (x < margin || x >= height - margin) 544 | { 545 | segment = Math.floor((y - margin) / borderSegmentSize); 546 | if (segment % 2 == 0) 547 | { 548 | rgb = rgbBorderFill2; 549 | } 550 | } 551 | } 552 | data[index * 4 + 0] = rgb[0] * factor; 553 | data[index * 4 + 1] = rgb[1] * factor; 554 | data[index * 4 + 2] = rgb[2] * factor; 555 | data[index * 4 + 3] = 0xFF; 556 | 557 | if (DEBUG && isLand(mapHeight) && islandMap[index] == largestIsland) 558 | { 559 | rgb = [0xFF, 0xA0, 0xFF]; 560 | data[index * 4 + 0] = rgb[0]; 561 | data[index * 4 + 1] = rgb[1]; 562 | data[index * 4 + 2] = rgb[2]; 563 | } 564 | } 565 | } 566 | 567 | gfx.putImageData(image, 0, 0); 568 | 569 | // Draw the path 570 | gfx.setLineDash([3, 5]); 571 | gfx.strokeStyle = 'rgba(30, 30, 30, 0.75)'; 572 | gfx.lineWidth = 2.0; 573 | for (var n = 0; n < path.length - 1; ++n) 574 | { 575 | var x1 = path[n].x; 576 | var y1 = path[n].y; 577 | var x2 = path[n + 1].x; 578 | var y2 = path[n + 1].y; 579 | var length = distance(x1, y1, x2, y2); 580 | var factor1 = 0.1; 581 | var factor2 = 0.2; 582 | var angle = Math.atan2(y2 - y1, x2 - x1) + Math.PI / 2; 583 | if (random.next() > 0.5) 584 | { 585 | angle += Math.PI; 586 | } 587 | var radius = length * (factor1 + factor2 * random.next()); 588 | var mx = (x1 + x2) * 0.5 + Math.cos(angle) * radius; 589 | var my = (y1 + y2) * 0.5 + Math.sin(angle) * radius; 590 | gfx.beginPath(); 591 | gfx.moveTo(x1, y1); 592 | gfx.quadraticCurveTo(mx, my, x2, y2); 593 | gfx.stroke(); 594 | 595 | if (DEBUG) 596 | { 597 | gfx.beginPath(); 598 | gfx.rect(x1 - 1, y1 - 1, 3, 3); 599 | gfx.fillStyle = 'red'; 600 | gfx.fill(); 601 | 602 | gfx.beginPath(); 603 | gfx.rect(mx - 1, my - 1, 3, 3); 604 | gfx.fillStyle = 'cyan'; 605 | gfx.fill(); 606 | } 607 | } 608 | 609 | // Draw the goal 610 | for (var n = 4; n >= 1; n--) 611 | { 612 | gfx.beginPath(); 613 | gfx.setLineDash([]); 614 | gfx.strokeStyle = 'rgba(220, 30, 40, ' + (0.5 - 0.1 * n) + ')'; 615 | gfx.lineWidth = n*1.7; 616 | var radius = 9.0; 617 | var spread = 0*1.8; 618 | var x1 = goal.x - radius + (random.next() * 2 - 1) * spread; 619 | var y1 = goal.y - radius + (random.next() * 2 - 1) * spread; 620 | var x2 = goal.x + radius + (random.next() * 2 - 1) * spread; 621 | var y2 = goal.y + radius + (random.next() * 2 - 1) * spread; 622 | var x3 = goal.x - radius + (random.next() * 2 - 1) * spread; 623 | var y3 = goal.y + radius + (random.next() * 2 - 1) * spread; 624 | var x4 = goal.x + radius + (random.next() * 2 - 1) * spread; 625 | var y4 = goal.y - radius + (random.next() * 2 - 1) * spread; 626 | gfx.moveTo(x1, y1); 627 | gfx.lineTo(x2, y2); 628 | gfx.moveTo(x3, y3); 629 | gfx.lineTo(x4, y4); 630 | gfx.stroke(); 631 | } 632 | 633 | // Draw the largest island outline 634 | if (DEBUG) 635 | { 636 | gfx.beginPath(); 637 | gfx.setLineDash([]); 638 | gfx.strokeStyle = 'magenta'; 639 | gfx.lineWidth = 1; 640 | gfx.rect(largestIslandBounds.x1, largestIslandBounds.y1, largestIslandBounds.x2 - largestIslandBounds.x1 + 1, largestIslandBounds.y2 - largestIslandBounds.y1 + 1); 641 | gfx.stroke(); 642 | } 643 | 644 | function drawCompassRose(gfx, radius) 645 | { 646 | var sqrt2 = Math.sqrt(2); 647 | 648 | var r1 = radius * 1.00; 649 | var r2 = radius * 0.24; 650 | var r4 = radius * 0.90; 651 | var r5 = radius * 0.80; 652 | 653 | var r3 = (-r1 * r2) / (2 * r2 - r1); 654 | var p4 = (r2*(r1*r2-r1*r1-r2*r1))/(r1*r2-r1*r1-2*r2*r2+r2*r1); 655 | var q4 = -(r2*r1*(2*r2-r1))/(2*r2*r2-2*r2*r1+r1*r1); 656 | var p5 = q4; 657 | var q5 = p4; 658 | 659 | color1 = 'rgb(80,65,60)'; 660 | color2 = 'rgb(' + rgbSea[0] + ',' + rgbSea[1] + ',' + rgbSea[2] + ')'; 661 | 662 | gfx.setLineDash([]); 663 | gfx.lineWidth = 1.0; 664 | gfx.strokeStyle = color1; 665 | 666 | steps = 16; 667 | step = 2 * Math.PI / steps; 668 | for (var n = 0; n < steps; ++n) 669 | { 670 | var a1 = (n + 0.5) * step; 671 | var a2 = (n + 1.5) * step; 672 | gfx.fillStyle = (n % 2 == 0) ? color1 : color2; 673 | gfx.beginPath(); 674 | gfx.moveTo(Math.cos(a1) * r4, Math.sin(a1) * r4); 675 | gfx.arc(0, 0, r4, a1, a2, false); 676 | gfx.lineTo(Math.cos(a2) * r5, Math.sin(a2) * r5); 677 | gfx.arc(0, 0, r5, a2, a1, true); 678 | gfx.lineTo(Math.cos(a1) * r5, Math.sin(a1) * r5); 679 | gfx.closePath(); 680 | gfx.fill(); 681 | } 682 | 683 | gfx.beginPath(); 684 | gfx.arc(0, 0, r4, 0, Math.PI * 2); 685 | gfx.stroke(); 686 | 687 | gfx.beginPath(); 688 | gfx.arc(0, 0, r5, 0, Math.PI * 2); 689 | gfx.stroke(); 690 | 691 | gfx.fillStyle = color2; 692 | gfx.beginPath(); 693 | gfx.moveTo( 0, 0); 694 | gfx.lineTo( 0, -r1); 695 | gfx.lineTo(+r2, -r2); 696 | gfx.lineTo(+r3, -r3); 697 | gfx.lineTo(+p4, -q4); 698 | gfx.lineTo(+r2, -r2); 699 | gfx.lineTo( 0, 0); 700 | gfx.lineTo(+r1, 0); 701 | gfx.lineTo(+r2, +r2); 702 | gfx.lineTo(+r3, +r3); 703 | gfx.lineTo(+p5, +q5); 704 | gfx.lineTo(+r2, +r2); 705 | gfx.lineTo( 0, 0); 706 | gfx.lineTo( 0, +r1); 707 | gfx.lineTo(-r2, +r2); 708 | gfx.lineTo(-r3, +r3); 709 | gfx.lineTo(-p4, +q4); 710 | gfx.lineTo(-r2, +r2); 711 | gfx.lineTo( 0, 0); 712 | gfx.lineTo(-r1, 0); 713 | gfx.lineTo(-r2, -r2); 714 | gfx.lineTo(-r3, -r3); 715 | gfx.lineTo(-p5, -q5); 716 | gfx.lineTo(-r2, -r2); 717 | gfx.lineTo( 0, 0); 718 | gfx.fill(); 719 | gfx.stroke(); 720 | 721 | gfx.fillStyle = color1; 722 | gfx.beginPath(); 723 | gfx.moveTo( 0, 0); 724 | gfx.lineTo( 0, -r1); 725 | gfx.lineTo(-r2, -r2); 726 | gfx.lineTo(-r3, -r3); 727 | gfx.lineTo(-p4, -q4); 728 | gfx.lineTo(-r2, -r2); 729 | gfx.lineTo( 0, 0); 730 | gfx.lineTo(+r1, 0); 731 | gfx.lineTo(+r2, -r2); 732 | gfx.lineTo(+r3, -r3); 733 | gfx.lineTo(+p5, -q5); 734 | gfx.lineTo(+r2, -r2); 735 | gfx.lineTo( 0, 0); 736 | gfx.lineTo( 0, +r1); 737 | gfx.lineTo(+r2, +r2); 738 | gfx.lineTo(+r3, +r3); 739 | gfx.lineTo(+p4, +q4); 740 | gfx.lineTo(+r2, +r2); 741 | gfx.lineTo( 0, 0); 742 | gfx.lineTo(-r1, 0); 743 | gfx.lineTo(-r2, +r2); 744 | gfx.lineTo(-r3, +r3); 745 | gfx.lineTo(-p5, +q5); 746 | gfx.lineTo(-r2, +r2); 747 | gfx.lineTo( 0, 0); 748 | gfx.fill(); 749 | gfx.stroke(); 750 | 751 | gfx.font = '22px Georgia'; 752 | gfx.fillText('N', -gfx.measureText('N').width / 2, -r1 - 2); 753 | 754 | gfx.font = '14px Georgia'; 755 | gfx.fillText('S', -gfx.measureText('S').width / 2, +r1 + 12); 756 | gfx.fillText('W', -r1 - gfx.measureText('W').width, +6); 757 | gfx.fillText('E', +r1 + 1, +6); 758 | } 759 | 760 | var compassPosition = margin + 80; 761 | gfx.save(); 762 | gfx.translate(compassPosition, height - compassPosition); 763 | gfx.rotate(Math.PI * 0.04*0); 764 | drawCompassRose(gfx, 60); 765 | gfx.restore(); 766 | 767 | noise.seed(random.nextInt(0, 0xFFFF)); 768 | var bitmap = gfx.getImageData(0, 0, width, height); 769 | var data = bitmap.data; 770 | for (var y = 0; y < height; ++y) 771 | { 772 | for (var x = 0; x < width; ++x) 773 | { 774 | var noiseAmplitude = 0.05; 775 | var noiseScale = 0.02; 776 | var grainAmplitude = 0.02; 777 | var factor = 1.0 + noiseAmplitude * noise.perlin2(x * noiseScale, y * noiseScale) + random.next() * grainAmplitude * 2 - grainAmplitude; 778 | data[(x + y * width) * 4 + 0] *= factor; 779 | data[(x + y * width) * 4 + 1] *= factor; 780 | data[(x + y * width) * 4 + 2] *= factor; 781 | } 782 | } 783 | gfx.putImageData(bitmap, 0, 0); 784 | } 785 | --------------------------------------------------------------------------------