├── README.md ├── index.js ├── lib ├── murmur3.js └── random.js └── package.json /README.md: -------------------------------------------------------------------------------- 1 | procedural 2 | ========== 3 | 4 | Library for defining procedural functions in a hierarchy to allow for complex 5 | procedurally generated content. 6 | 7 | 8 | Introduction 9 | ------------ 10 | 11 | This library aims to make it easy to create procedurally generated content, 12 | which usually means semi-random content that follows a complex set of rules. 13 | One great example of procedural generated content is the world in Minecraft. 14 | It's random for everyone who starts the game, but follows certain rules that 15 | together make a recognizable environment (hills, trees, caves, villages, ...) 16 | 17 | You use this library by defining procedural functions. A procedural function 18 | may *take* any number of parameters, and can *provide* any number of values. 19 | Additionally, it may *generate* other procedural functions that depend on it, 20 | as well as its values. 21 | 22 | Each instance of a procedural function (which you get by calling the function) 23 | has a unique hash which is used when calling any of the built-in pseudo-random 24 | number generators. This means you will always get the same sequence of random 25 | numbers for the same set of parameters, which is the most important aspect of 26 | procedurally generated content. 27 | 28 | Here's how you define a procedural function that takes a single parameter: 29 | 30 | ```javascript 31 | var procedural = require('procedural'); 32 | var world = procedural('world').takes('name'); 33 | ``` 34 | 35 | You may want a world to define if it's habitable: 36 | 37 | ```javascript 38 | world.provides('habitable', function (world) { 39 | // For now, all worlds will be habitable. 40 | return true; 41 | }); 42 | ``` 43 | 44 | You can now create your world instance like this: 45 | 46 | ```javascript 47 | var earth = world('Earth'); 48 | console.log('Does', earth.name, 'support life?'); 49 | console.log(earth.habitable ? 'Yes' : 'No'); 50 | ``` 51 | 52 | Now it makes sense for a region in a world to be accessible... 53 | 54 | ```javascript 55 | var region = world.generates('region') 56 | .takes('x', 'y') 57 | .provides('temperature', function (region) { 58 | // Get a random number generator for this region. 59 | var rnd = region.getRandGen(); 60 | 61 | // Make temperature depend on what kind of world we're on. 62 | if (region.world.habitable) { 63 | return rnd.nextInt(10, 35); 64 | } else { 65 | return rnd.nextInt(1000, 3500); 66 | } 67 | }); 68 | ``` 69 | 70 | Let's find out what temperature we've got at (0, 0). 71 | 72 | ```javascript 73 | console.log('Temperature is:', earth.region(0, 0).temperature); 74 | ``` 75 | 76 | Note that generated random numbers will be different for different parameters: 77 | 78 | ```javascript 79 | var kryptonTemp = world('Krypton').region(0, 0).temperature; 80 | console.log('But on Krypton it is:', kryptonTemp); 81 | ``` 82 | 83 | But wait, Krypton isn't habitable! Let's fix that by revisiting the habitable 84 | value of worlds. 85 | 86 | ```javascript 87 | world.provides('habitable', function (world) { 88 | // Only Earth is known to be habitable (for now...). 89 | return world.name == 'Earth'; 90 | }); 91 | ``` 92 | 93 | If you rerun all the code together now, you'll see that Krypton will be a bit 94 | hotter than before, while Earth is still comfortable. 95 | 96 | I hope this example shows how useful it is to have a set of procedurally 97 | generated values that depend on each other when you want to create random 98 | content that still follows a set of rules. 99 | 100 | For another example, see the bottom of this README, or go check out one of 101 | these demos: 102 | 103 | * TODO: The space demo. 104 | 105 | 106 | API 107 | --- 108 | 109 | TODO: Define the API here. 110 | 111 | 112 | Full example 113 | ------------ 114 | 115 | How to define a procedurally generated avatar. 116 | 117 | ```javascript 118 | var procedural = require('procedural'); 119 | 120 | var avatar = procedural('avatar') 121 | // The block size is just visual, so it shouldn't affect randomization. 122 | .doNotHash('blockSize') 123 | // The username is needed to create a unique avatar for every user. 124 | .takes('username') 125 | // Size, in blocks. Different sizes will create different avatars. 126 | .takes('size', function validate(avatar, blocks) { 127 | // Ensure that size is a positive integer divisible by 2. 128 | return typeof blocks == 'number' && blocks > 0 && !(blocks % 2); 129 | }) 130 | // The pixel size of a single (square) block. 131 | .takes('blockSize', function validate(avatar, px) { 132 | return typeof px == 'number' && px > 0; 133 | }) 134 | // Calculate the colors that make up the avatar. 135 | .provides('hueAngle', function (avatar) { 136 | // Use a named number generator to get an independent sequence. 137 | return avatar.getRandGen('color').nextInt(360); 138 | }) 139 | .provides('background', function (avatar) { 140 | return 'hsl(' + avatar.hueAngle + ', 100%, 50%)'; 141 | }) 142 | .provides('foreground', function (avatar) { 143 | var hueAngle = (avatar.hueAngle + 180) % 360; 144 | return 'hsl(' + hueAngle + ', 100%, 50%)'; 145 | }) 146 | // 75% of avatars have a mirrored effect, others don't. 147 | .provides('isMirrored', function (avatar) { 148 | return avatar.getRandGen('mirror').nextFloat() > .25; 149 | }) 150 | // A particular avatar has a unique set of blocks. 151 | .generates('block') 152 | // The validator will run independently for both parameters. 153 | .takes('x', 'y', function validate(block, xy) { 154 | // We can refer to the parent instance (the avatar). 155 | return typeof xy == 'number' && xy >= 0 && xy < block.avatar.size; 156 | }) 157 | // The color of this block. 158 | .provides('color', function (block) { 159 | // You don't have to use named random generators. 160 | if (block.getRandGen().nextFloat() > .5) { 161 | return block.avatar.foreground; 162 | } else { 163 | return block.avatar.background; 164 | } 165 | }) 166 | // Go back to defining the parent (avatar). 167 | .done() 168 | // Renders to a canvas and returns a URL for . 169 | .provides('url', function (avatar) { 170 | var canvas = document.createElement('canvas'), 171 | context = canvas.getContext('2d'); 172 | 173 | canvas.width = avatar.size * avatar.blockSize; 174 | canvas.height = avatar.size * avatar.blockSize; 175 | 176 | context.fillStyle = avatar.background; 177 | context.fillRect(0, 0, avatar.size, avatar.size); 178 | 179 | var finalX = avatar.isMirrored ? avatar.size / 2 : avatar.size, 180 | blockSize = avatar.blockSize; 181 | 182 | for (var y = 0; y < avatar.size; y++) { 183 | for (var x = 0; x < finalX; x++) { 184 | var realX = x * blockSize, realY = y * blockSize; 185 | 186 | var block = avatar.block(x, y); 187 | context.fillStyle = block.color; 188 | context.fillRect(realX, realY, blockSize, blockSize); 189 | 190 | if (avatar.isMirrored) { 191 | var mirroredX = avatar.size * blockSize - realX - blockSize; 192 | context.fillRect(mirroredX, realY, blockSize, blockSize); 193 | } 194 | } 195 | } 196 | 197 | return canvas.toDataURL(); 198 | }); 199 | 200 | var img = document.createElement('img'); 201 | img.src = avatar('bob', 16, 4).url; 202 | document.body.appendChild(img); 203 | ``` 204 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var murmur3 = require('./lib/murmur3'); 2 | var random = require('./lib/random'); 3 | 4 | // Poor man's enum. 5 | var PARAMETER = {}, COMPUTED = {}; 6 | 7 | // Allows fetching based on a relative key path, e.g., "someParent.xyz". For 8 | // traversing up the tree, "..parentValue" and "...grandParentValue" also 9 | // works, as well as "/rootValue". 10 | function instanceGet(keyPath) { 11 | var value = this; 12 | // Make key path relative to root node if first character is "/". 13 | if (keyPath[0] == '/') { 14 | var parent; 15 | while (parent = value.getParent()) value = parent; 16 | keyPath = keyPath.substr(1); 17 | } 18 | var keys = keyPath.split('.'); 19 | 20 | // Remove first empty argument to make ".bla" refer to current instance. 21 | if (!keys[0]) { 22 | keys.shift(); 23 | } 24 | 25 | for (var i = 0; i < keys.length; i++) { 26 | var key = keys[i]; 27 | value = key ? value[key] : value.getParent(); 28 | } 29 | return value; 30 | } 31 | 32 | module.exports = function procedural(name, parent) { 33 | var values = [], valuesMap = {hash: null}; 34 | var doNotHash = []; 35 | var parameterCount = 0; 36 | 37 | // Prevent defining values with the same name as the parent. 38 | if (parent) { 39 | valuesMap[parent.getName()] = null; 40 | } 41 | 42 | // Utility functions that can be passed for use outside this function. 43 | function getName() { 44 | return name; 45 | } 46 | 47 | function getValues(opt_type) { 48 | var names = []; 49 | for (var i = 0; i < values.length; i++) { 50 | if (opt_type && values[i].type != opt_type) continue; 51 | names.push(values[i].name); 52 | } 53 | return names; 54 | } 55 | 56 | function getParameters() { 57 | return getValues(PARAMETER); 58 | } 59 | 60 | function getProvides() { 61 | return getValues(COMPUTED); 62 | } 63 | 64 | // The constructor for a single instance of this procedural function. 65 | function ProceduralInstance(parentInstance) { 66 | if (parentInstance) { 67 | this[parentInstance.getName()] = parentInstance; 68 | } else if (parent) { 69 | console.warn('Creating detached ' + name + ' instance (expected parent ' + parent + ')'); 70 | } 71 | } 72 | 73 | ProceduralInstance.prototype = { 74 | get: instanceGet, 75 | getName: getName, 76 | getParameters: getParameters, 77 | getProvides: getProvides, 78 | getValues: getValues, 79 | 80 | getParent: function () { 81 | if (!parent) return null; 82 | return this[parent.getName()]; 83 | }, 84 | 85 | getRandGen: function (opt_id) { 86 | var p = this.getParent(), 87 | s0 = murmur3.hash32(opt_id || 'default', this.hash) * 2.3283064365386963e-10, 88 | s1 = this.hash * 2.3283064365386963e-10; 89 | return random(s0, s1); 90 | }, 91 | 92 | toString: function () { 93 | var args, proc = this, pieces = []; 94 | 95 | while (proc) { 96 | args = proc.getParameters().map(function (paramName) { 97 | return paramName + ': ' + JSON.stringify(proc[paramName]); 98 | }); 99 | pieces.unshift(proc.getName() + '(' + args.join(', ') + ')'); 100 | proc = proc.getParent(); 101 | } 102 | 103 | return pieces.join('.'); 104 | } 105 | }; 106 | 107 | function create() { 108 | if (arguments.length != parameterCount) { 109 | throw new Error('Wrong number of parameters for ' + create + ': ' + arguments.length); 110 | } 111 | 112 | /* 113 | // Ensure that the definition doesn't change after an instance has been created. 114 | if (!Object.isFrozen(create)) { 115 | Object.freeze(create); 116 | Object.freeze(ProceduralInstance.prototype); 117 | } 118 | */ 119 | 120 | // We assume that this function is bound to the parent instance. Example: 121 | // jupiter.moon(13) -- "moon" is this function, bound to "jupiter". 122 | var parentInstance = parent && parent.isInstance(this) ? this : null; 123 | 124 | // Create the instance which will hold all the values. 125 | var instance = new ProceduralInstance(parentInstance); 126 | 127 | // Start setting up an array of all values that make up the hash. 128 | var hashParts = [name], hashSeed = parentInstance ? parentInstance.hash : 0; 129 | function createHashOnce() { 130 | if ('hash' in instance) return; 131 | // Calculate the hash for the instance based on parents and parameters. 132 | instance.hash = murmur3.hash32(hashParts.join('\x00'), hashSeed); 133 | } 134 | 135 | // Fill in all the values specified on this procedural function. 136 | var argumentIndex = 0; 137 | for (var i = 0; i < values.length; i++) { 138 | var value = values[i], shouldHash = doNotHash.indexOf(value.name) == -1; 139 | 140 | if (value.type == PARAMETER) { 141 | if ('hash' in instance && shouldHash) { 142 | throw new Error('Cannot define hashed parameters after hash is generated'); 143 | } 144 | 145 | var argument = arguments[argumentIndex++]; 146 | 147 | // Validate the value. 148 | if (value.validator && !value.validator(instance, argument)) { 149 | throw new Error('Invalid value for ' + name + '.' + value.name + ': ' + argument); 150 | } 151 | 152 | // Assign the argument value to the instance. 153 | instance[value.name] = argument; 154 | 155 | if (shouldHash) { 156 | // TODO: Performance check for JSON.stringify, maybe toString is enough. 157 | hashParts.push(JSON.stringify(instance[value.name])); 158 | } 159 | } else if (value.type == COMPUTED) { 160 | // Compute and assign the value to the instance. 161 | if (value.fn) { 162 | // Always create the hash before computing values which may need it. 163 | createHashOnce(); 164 | instance[value.name] = value.fn(instance); 165 | } else { 166 | instance[value.name] = value.constant; 167 | } 168 | } 169 | } 170 | 171 | // Create the hash now if it wasn't created above. 172 | createHashOnce(); 173 | 174 | // Prevent the instance from changing before exposing it. 175 | Object.freeze(instance); 176 | return instance; 177 | } 178 | 179 | create.getName = getName; 180 | create.getParameters = getParameters; 181 | create.getProvides = getProvides; 182 | create.getValues = getValues; 183 | 184 | create.done = function () { 185 | return parent; 186 | }; 187 | 188 | create.doNotHash = function (var_args) { 189 | Array.prototype.push.apply(doNotHash, arguments); 190 | return this; 191 | }; 192 | 193 | create.generates = function (nameValue) { 194 | /* 195 | if (Object.isFrozen(create)) { 196 | throw new Error('Cannot define ' + this + '.' + nameValue + ': instances of ' + name + ' exist'); 197 | } 198 | */ 199 | 200 | var proc = procedural(nameValue, this); 201 | ProceduralInstance.prototype[nameValue] = proc; 202 | this[nameValue] = proc; 203 | 204 | return proc; 205 | }; 206 | 207 | create.getParent = function () { 208 | return parent; 209 | }; 210 | 211 | create.isInstance = function (obj) { 212 | return obj instanceof ProceduralInstance; 213 | }; 214 | 215 | create.provides = function (name, fnOrConstant) { 216 | /* 217 | if (Object.isFrozen(create)) { 218 | throw new Error('Cannot define value after creation'); 219 | } 220 | */ 221 | 222 | if (name in valuesMap) { 223 | throw new Error('A value named ' + name + ' is already defined'); 224 | } 225 | 226 | if (name in ProceduralInstance.prototype) { 227 | throw new Error('Invalid value name "' + name + '"'); 228 | } 229 | 230 | var value = {name: name, type: COMPUTED}; 231 | if (typeof fnOrConstant == 'function') { 232 | value.fn = fnOrConstant; 233 | } else { 234 | value.constant = fnOrConstant; 235 | } 236 | values.push(value); 237 | valuesMap[name] = value; 238 | 239 | return this; 240 | }; 241 | 242 | create.providesMethod = function (name, fn) { 243 | if (typeof fn != 'function') { 244 | throw new Error('Method must be passed in as a function'); 245 | } 246 | 247 | if (name in valuesMap) { 248 | throw new Error('A value named ' + name + ' is already defined'); 249 | } 250 | 251 | if (name in ProceduralInstance.prototype) { 252 | throw new Error('Invalid method name "' + name + '"'); 253 | } 254 | 255 | valuesMap[name] = function wrapper() { 256 | return fn.apply(this, Array.prototype.concat.apply([this], arguments)); 257 | }; 258 | ProceduralInstance.prototype[name] = valuesMap[name]; 259 | 260 | return this; 261 | }; 262 | 263 | create.takes = function (var_args) { 264 | /* 265 | if (Object.isFrozen(create)) { 266 | throw new Error('Cannot define parameter after creation'); 267 | } 268 | */ 269 | 270 | // The last argument may be a validation function. 271 | var numParams = arguments.length, validator; 272 | if (typeof arguments[numParams - 1] == 'function') { 273 | validator = arguments[numParams--]; 274 | } 275 | 276 | if (!numParams) { 277 | throw new Error('At least one parameter must be specified'); 278 | } 279 | 280 | for (var i = 0; i < numParams; i++) { 281 | var name = arguments[i]; 282 | 283 | if (typeof name != 'string') { 284 | throw new Error('Invalid parameter name ' + name); 285 | } 286 | 287 | if (name in valuesMap) { 288 | throw new Error('A value named ' + name + ' is already defined'); 289 | } 290 | 291 | if (name in ProceduralInstance.prototype) { 292 | throw new Error('Invalid parameter name "' + name + '"'); 293 | } 294 | 295 | var param = {name: name, type: PARAMETER}; 296 | if (typeof validator == 'function') { 297 | param.validator = validator; 298 | } 299 | values.push(param); 300 | valuesMap[name] = param; 301 | } 302 | 303 | // Keep track of number of parameters for the constructor validation. 304 | parameterCount += numParams; 305 | 306 | return this; 307 | }; 308 | 309 | create.toString = function () { 310 | var pieces = [], proc = this; 311 | while (proc) { 312 | pieces.unshift(proc.getName()); 313 | proc = proc.getParent(); 314 | } 315 | 316 | var names = this.getParameters().join(', '); 317 | return pieces.join('.') + '(' + names + ')'; 318 | }; 319 | 320 | return create; 321 | }; 322 | -------------------------------------------------------------------------------- /lib/murmur3.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 32-bit implementation of MurmurHash3 (r136) 3 | * By Gary Court 4 | * https://github.com/garycourt/murmurhash-js 5 | * 6 | * MurmurHash by Austin Appleby 7 | * https://sites.google.com/site/murmurhash/ 8 | * 9 | * @param {String} key The string to hash. 10 | * @param {number} seed A salt for the hash. 11 | * @return {number} 32-bit positive integer hash. 12 | */ 13 | exports.hash32 = function hash32(key, seed) { 14 | var remainder, bytes, h1, h1b, c1, c1b, c2, c2b, k1, i; 15 | 16 | remainder = key.length & 3; // key.length % 4 17 | bytes = key.length - remainder; 18 | h1 = seed; 19 | c1 = 0xcc9e2d51; 20 | c2 = 0x1b873593; 21 | i = 0; 22 | 23 | while (i < bytes) { 24 | k1 = 25 | ((key.charCodeAt(i) & 0xff)) | 26 | ((key.charCodeAt(++i) & 0xff) << 8) | 27 | ((key.charCodeAt(++i) & 0xff) << 16) | 28 | ((key.charCodeAt(++i) & 0xff) << 24); 29 | ++i; 30 | 31 | k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff; 32 | k1 = (k1 << 15) | (k1 >>> 17); 33 | k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff; 34 | 35 | h1 ^= k1; 36 | h1 = (h1 << 13) | (h1 >>> 19); 37 | h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff; 38 | h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16)); 39 | } 40 | 41 | k1 = 0; 42 | 43 | switch (remainder) { 44 | case 3: k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16; 45 | case 2: k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8; 46 | case 1: k1 ^= (key.charCodeAt(i) & 0xff); 47 | 48 | k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff; 49 | k1 = (k1 << 15) | (k1 >>> 17); 50 | k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff; 51 | h1 ^= k1; 52 | } 53 | 54 | h1 ^= key.length; 55 | 56 | h1 ^= h1 >>> 16; 57 | h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff; 58 | h1 ^= h1 >>> 13; 59 | h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff; 60 | h1 ^= h1 >>> 16; 61 | 62 | return h1 >>> 0; 63 | }; 64 | -------------------------------------------------------------------------------- /lib/random.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a pseudo-random value generator. Takes three floating point numbers 3 | * in the range [0, 1) for seeding the generator. 4 | * 5 | * This is an adapted version of Johannes Baagøe's Alea algorithm. 6 | * https://github.com/nquinlan/better-random-numbers-for-javascript-mirror 7 | */ 8 | function alea(s0, s1) { 9 | var c = 1; 10 | 11 | var f = function () { 12 | var t = 2091639 * s0 + c * 2.3283064365386963e-10; 13 | s0 = s1; 14 | return s1 = t - (c = t | 0); 15 | }; 16 | 17 | f.nextFloat = aleaNextFloat; 18 | f.nextInt = aleaNextInt; 19 | 20 | return f; 21 | } 22 | 23 | /** 24 | * Returns a pseudo-random floating point number in range [0, 1), or a custom 25 | * range, if specified. The lower bound is inclusive while the upper bound is 26 | * exclusive. 27 | */ 28 | function aleaNextFloat(opt_minOrMax, opt_max) { 29 | var value = this(); 30 | 31 | var min, max; 32 | if (typeof opt_max == 'number') { 33 | min = opt_minOrMax; 34 | max = opt_max; 35 | } else if (typeof opt_minOrMax == 'number') { 36 | min = 0; 37 | max = opt_minOrMax; 38 | } else { 39 | return value; 40 | } 41 | 42 | return min + value * (max - min); 43 | } 44 | 45 | /** 46 | * Returns a pseudo-random integer in the specified range. The lower bound is 47 | * inclusive while the upper bound is exclusive. 48 | */ 49 | function aleaNextInt(minOrMax, opt_max) { 50 | return Math.floor(this.nextFloat(minOrMax, opt_max)); 51 | } 52 | 53 | module.exports = alea; 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "procedural", 3 | "version": "0.1.3", 4 | "description": "Library for defining procedural functions.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/blixt/js-procedural.git" 12 | }, 13 | "author": "Blixt ", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/blixt/js-procedural/issues" 17 | }, 18 | "homepage": "https://github.com/blixt/js-procedural" 19 | } 20 | --------------------------------------------------------------------------------