├── 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 |
--------------------------------------------------------------------------------