randomizer.js

/**
 * @module Randomizer
 */

import Vector from './datastructures/vector.js';

/**
 * Get a random integer between low to high, inclusive.
 * If only one parameter is given, a random integer
 * from (0, low-1) inclusive.
 * @param {number} min - Lower bound on range of random int.
 * @param {number} max - Upper bound on range of random int.
 * @returns {number} Random number between low and high, inclusive.
 */
export function nextInt(min, max) {
    if (max === undefined) {
        max = min - 1;
        min = 0;
    }

    min = Math.floor(min);
    var r = Math.random();
    return min + Math.floor(r * (max - min + 1));
}

/**
 * Get a random float between low to high, inclusive.
 * If only one parameter is given, a random float
 * from (0, low-1) inclusive.
 * @param {number} min - Lower bound on range of random int.
 * @param {number} max - Upper bound on range of random int.
 * @returns {number} Random number between low and high, inclusive.
 */
export function nextFloat(min, max) {
    if (max === undefined) {
        max = min;
        min = 0;
    }
    return min + (max - min) * Math.random();
}

/**
 * Generates a random number in range (0,255) in hexadecimal.
 * @returns {string} Random number in hexadecimal form.
 */
export function nextHex() {
    var val = nextInt(0, 255);
    if (val < 16) {
        return '0' + val.toString(16);
    }
    return val.toString(16);
}

/**
 * Generate a random hexadecimal color code of the format #RRGGBB.
 * @returns {string} Hexadecimal representation of random color.
 */
export function nextColor() {
    var r = nextHex();
    var g = nextHex();
    var b = nextHex();
    return '#' + r + g + b;
}

/**
 * Generate a random boolean via fair probability coin toss.
 * If `probabilityTrue` is supplied, the coin toss is skewed by that value.
 * @param {number} probabilityTrue - Skewed probability of true.
 * @returns {boolean} Result of coin flip skewed toward `probabilityTrue`.
 */
export function nextBoolean(probabilityTrue) {
    if (probabilityTrue === undefined) {
        probabilityTrue = 0.5;
    }

    return Math.random() < probabilityTrue;
}

// stores numbers 0-1
let perlin;
// stores unit vectors
let perlin2;
const PERLIN_SIZE = 4095;
const PERLIN_SIZE_2D = 63;

const lerp = (a, b, x) => {
    return a * (1 - x) + b * x;
};

const fade = t => {
    return t * t * (3 - 2 * t);
};

/**
 * A noise function for generating a smooth, random value between 0 and 1.
 * @param {number} x - Any number. Adjacent numbers will have similar noise values, by definition
 * of Perlin noise.
 * @param {number} [y] - Any number. If y is present, 2d will be used.
 * @returns {number}
 */
export function noise(x, y) {
    if (!perlin) {
        perlin = new Array(PERLIN_SIZE + 1);
        for (let i = 0; i < PERLIN_SIZE + 1; i++) {
            perlin[i] = Math.random();
        }
    }

    if (y !== undefined) {
        if (!perlin2) {
            perlin2 = new Array(PERLIN_SIZE_2D + 1).fill(0).map(row => {
                return new Array(PERLIN_SIZE_2D + 1).fill(0).map(() => {
                    return new Vector(1, 0).rotate(Math.random() * 360);
                });
            });
        }
        /*
         * 2D perlin noise creates a 2-dimensional array of gradients (random unit vectors)
         * then calculates the value for an (x, y) pair by doing the following:
         * 1. clip the (x, y) pair to a cell within the 2-dimensional array of unit vectors
         * 2. calculate the dot product of the vector between (x, y) and the gradient at
         *    each corner
         * 3. use a fade function to interpolate those values. the top left and top right
         *    are interpolated by dx, then those values are interpolated by dy
         *
         * Here's an example cell in the 2-dimensional array of gradients, showing (x, y)
         * and the four corners of the cell the value is clipped to.
         *
         *  (x0, y0)    (x1, y0)
         *     +------------+
         *     |            | \
         *     |  (x, y)    |  } dy
         *     |  _ * _     | /
         *     |/  dx   \   |
         *     +------------+
         *  (x0, y1)    (x1, y1)
         *
         * Each of the corners (top left, top right, etc.) has a pre-computed gradient.
         * The vectors from (x, y) to each of the four corners are dotted with the gradient
         * at that corner. For example, perlin2[x0][y0].dot(x - x0, y - y0).
         *
         */

        const x0 = Math.floor(x) % PERLIN_SIZE_2D;
        const x1 = (x0 + 1) % PERLIN_SIZE_2D;
        const y0 = Math.floor(y) % PERLIN_SIZE_2D;
        const y1 = (y0 + 1) % PERLIN_SIZE_2D;

        // dx and dy represent the local position of the x, y pair within the perlin unit space
        // because the case of wrapping around (x is 63 and x0 is 0), they must be restricted with modulus
        const dx = (x - x0) % PERLIN_SIZE_2D;
        const dy = (y - y0) % PERLIN_SIZE_2D;

        const gradientTL = perlin2[x0][y0];
        const gradientTR = perlin2[x1][y0];
        const gradientBL = perlin2[x0][y1];
        const gradientBR = perlin2[x1][y1];

        const noiseTL = gradientTL.dot(dx, dy);
        const noiseTR = gradientTR.dot(dx - 1, dy);
        const noiseBL = gradientBL.dot(dx, dy - 1);
        const noiseBR = gradientBR.dot(dx - 1, dy - 1);
        const xFade = fade(dx);

        return (
            (lerp(lerp(noiseTL, noiseTR, xFade), lerp(noiseBL, noiseBR, xFade), fade(dy)) + 1) / 2
        );
    }

    x = Math.abs(x);
    const xFloor = Math.floor(x);
    const t = x - xFloor;

    // get the left and right neighbors of x
    const xMin = xFloor % PERLIN_SIZE;
    const xMax = (xMin + 1) % PERLIN_SIZE;

    return lerp(perlin[xMin], perlin[xMax], fade(t));
}