UNPKG

@jakebeamish/penplotting

Version:

A JavaScript framework for making SVG files for penplotters.

355 lines (326 loc) 11.2 kB
import { Vector } from "./Vector.js"; /** * A PRNGAlgorithm is a class that implements some technique for random number * generation. It should have a seed and max value, as well as a next function. * @typedef PRNGAlgorithm * @prop {number} seed - The initial state of the PRNG. * @prop {number} max - The maximum possible value of the PRNG. * @prop {function(): number} next - The next psuedo-random number between 0 and 1. */ /** * @class */ export class PRNG { /** * Creates an instance of PRNG. * @param {Object} [options={}] - The configuration options for the PRNG. * @param {number} [options.seed=Date.now()] - The default seed value for the PRNG is the current timestamp. * @param {PRNGAlgorithm} [options.algorithm=XORShift32] - The algorithm to use * for generating psuedo-random numbers. */ constructor({ seed = Date.now(), algorithm = XORShift32 } = {}) { this.seed = seed; this.algorithm = new algorithm(this.seed); } /** * Generate a random float in the range [0, 1). * @param {number} [max=1] - A single argument specifies a range of between 0 and that number. * @param {number} [min=0] - Two numeric arguments specifies a range. * @returns {number} A floating-point number in a given range, 0 (inclusive) and 1 (exclusive) if called without arguments. */ randomFloat(max = 1, min = 0) { let range = max - min; return range * (this.algorithm.next() / this.algorithm.max) + min; } /** * Generate a random float in the range [-1, 1). * @returns {number} A number between -1 (inclusive) and 1 (exclusive). */ randomBipolarFloat() { return this.randomFloat() * 2 - 1; } /** * Generate a random float that fits a normal distribution. * * The most likely values are at the mean. The standard deviation describes * the spread of the distribution. * * With a mean of 0, and standard deviation of 1 (default values); * - 68% of results fall within +/- 1 * - 95% of results fall within +/- 2 * - 99% of results fall within +/- 3 * * @param {number} [mean=0] - The mean of the distribution. * @param {number} [sd=1] - The standard deviation of the distribution. * @returns {number} */ randomGaussian(mean = 0, sd = 1) { let u, v, radius; while (true) { u = this.randomBipolarFloat(); v = this.randomBipolarFloat(); radius = u * u + v * v; if (radius < 1 && radius >= 0) { break; } } const scale = Math.sqrt((-2 * Math.log(radius)) / radius); return mean + sd * u * scale; } /** * Generate a random unit vector. * @returns {Vector} A {@link Vector} with a magnitude of 1 and a random angle between `0` and `TWO_PI`. */ randomUnitVector() { const angle = this.randomFloat() * Math.PI * 2; return Vector.fromAngle(angle); } /** * Generate a random vector within the bounds of a given AABB. * @param {AABB} box - The box to generate the vector within. * @returns {Vector} A {@link Vector} within the {@link AABB}. */ randomVectorInAABB(box) { return new Vector( this.randomFloat(box.x - box.width, box.x + box.width), this.randomFloat(box.y - box.height, box.y + box.height) ); } /** * Generate a random {@link Vector} within a given {@link Circle}. * @param {Circle} circle - The circle to generate the {@link Vector} within. * @returns {Vector} A {@link Vector} within the {@link Circle}. */ randomVectorInCircle(circle) { let angle = this.randomFloat() * Math.PI * 2; let magnitude = Math.sqrt(this.randomFloat()) * circle.radius; return Vector.add( Vector.fromAngle(angle).multiply(magnitude), new Vector(circle.x, circle.y) ); } /** * Generate a random integer from a specified range of values. * @param {number} min - The minimum integer value (inclusive). * @param {number} max - The maximum integer value (exclusive). * @returns {number} A random integer between min (inclusive) and max (exclusive). */ randomInteger(min, max) { const range = max - min; return min + Math.floor(this.randomFloat() * range); } /** * Select a random element from an array. * @param {array} array - The array from which to select an element. * @returns {*} A randomly selected element from the array. */ randomElement(array) { const index = Math.floor(this.randomFloat() * array.length); return array[index]; } /** * Returns a sample without replacement from a given array and sample size. * @param {array} array - The array from which to sample elements. * @param {number} size - The number of samples to generate (must be a positive integer). * @returns {array} An array of randomly selected elements from an array. */ randomSample(array, size) { if (size < 1 || !Number.isInteger(size)) { throw new TypeError("Sample size should be a positive integer."); } if (array.length < size) { throw new Error("Sample size must not be greater than array length."); } let availableElements = [...array]; let result = []; for (let i = 0; i < size; i++) { const index = Math.floor(this.randomFloat() * availableElements.length); result.push(availableElements[index]); availableElements.splice(index, 1); } return result; } /** * Returns a boolean based on a specified probability. * @param {number} [chance=0.5] - The probability of returning true (between 0 and 1). * @returns {boolean} True if the random float is less than the chance, otherwise false. */ randomChance(chance = 0.5) { return this.randomFloat() < chance; } /** * Selects an option probabalistically from a set of weighted choices. * @param {Array<Object>} choices - An array of objects with `option` and `weight` properties. * @param {function|number|string} choices[].option - The outcome of the choice. * @param {number} choices[].weight - The weight of the choice. Higher weights (relative to the other choices) increase the likelihood of selection. * @returns {function|number|string} The selected option. */ randomWeighted(choices) { let totalWeight = 0; for (let choice of choices) { totalWeight += choice.weight; } const randomNumberInRange = this.randomFloat() * totalWeight; let cumulativeWeight = 0; for (let choice of choices) { cumulativeWeight += choice.weight; if (randomNumberInRange < cumulativeWeight) { if (typeof choice.option === "function") { return choice.option(); } return choice.option; } } } } /** * LCG (Linear Congruential Generator) Pseudorandom Number Generator Class * @implements {PRNGAlgorithm} * * @summary Implements the LCG algorithm to generate pseudorandom numbers. * @description * This class provides the LCG algorithm for generating psuedorandom * numbers to the {@link PRNG} class. * * @example * const rng = new LCG(123456789); * console.log(rng.next()); // Generates a pseudorandom number * * @see https://en.wikipedia.org/wiki/Linear_congruential_generator */ export class LCG { /** * Creates an instance of LCG. * @param {number} seed - The seed value for the LCG PRNG. */ constructor(seed) { this.modulus = 2 ** 32; this.multiplier = 1664525; this.increment = 1013904223; this.state = seed; this.max = this.modulus; } /** * Generates the next pseudorandom number. * @returns {number} A number in the range [0 to this.modulus - 1). */ next() { this.state = (this.multiplier * this.state + this.increment) % this.modulus; return this.state; } } /** * Mulberry32 Pseudorandom Number Generator Class * @implements {PRNGAlgorithm} * * @summary Implements the Mulberry32 algorithm to generate pseudorandom numbers. * @description * This class provides the Mulberry32 method of generating psuedorandom * numbers to the {@link PRNG} class. * * The Mulberry32 algorithm was written by Tommy Ettinger in 2017 and is released to the public domain, meaning it can be freely used, modified, and distributed without restrictions. * * @example * const rng = new Mulberry32(123456789); * console.log(rng.next()); // Generates a pseudorandom number * * @see https://gist.github.com/tommyettinger/46a874533244883189143505d203312c */ export class Mulberry32 { /** * Creates an instance of Mulberry32. * @param {number} seed - The seed value for the Mulberry32 PRNG. */ constructor(seed) { this.state = seed; this.max = 2 ** 32; } /** * Generates the next psuedo-random number. * @returns {number} A number in the range [0-1). * * @example * // Returns a random number in the range [0-1) * const rng = new Mulberry32(); * console.log(rng.next()); */ next() { let t = (this.state += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), 1 | t); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; this.state = t ^ (t >>> 14); return this.state >>> 0; } } /** * XORShift32 Pseudorandom Number Generator Class * @implements {PRNGAlgorithm} * * @summary Implements the XORShift32 algorithm to generate pseudorandom * numbers. * @description * This class provides the XORShift32 method of generating psuedorandom * numbers to the {@link PRNG} class. It uses bitwise operations * to generate a sequence of pseudorandom 32-bit unsigned integers. * * The XORShift algorithm was written by George Marsaglia in 2003 and is * released to the public domain, meaning it can be freely used, modified, * and distributed without restrictions. * * @example * const prng = new XORShift32(123456789); * console.log(prng.next()); // Generates a pseudorandom number * * @see Marsaglia, G. (2003) "XORShift RNGs", Journal of Statistical * Software https://www.jstatsoft.org/article/view/v008i14 */ export class XORShift32 { /** * Creates an instance of XORShift32. * @param {number} seed - The seed value for the PRNG. */ constructor(seed) { this.state = seed; this.max = 2 ** 32; } /** * Generates the next psuedo-random number. * @returns {number} A number in the range [0-1). * * @example * // Returns a random number in the range [0-1) * const rng = new XORShift32(); * console.log(rng.next()); */ next() { let a = this.state; a ^= a << 13; a ^= a >>> 17; a ^= a << 5; this.state = a >>> 0; // Ensure it stays a 32-bit unsigned integer return this.state; } } /** * @summary Create a random hexadecimal string of a specified length. * @description Uses Math.random() to make a hexadecimal string for * setting the seed of a {@link Plot} to be used in a seedable {@link PRNG} such as {@link LCG} * @param {number} n - Number of digits * @returns {string} Hexadecimal string of length n */ export function unseededRandomHex(n) { const hexArray = Array.from( { length: n, }, () => { return Math.floor(Math.random() * 16).toString(16); } ); const hex = hexArray.join(""); const decimal = parseInt(hex, 16); return { hex, decimal, }; }