/** * Implementation of Saarna's Bluing Algorithm for generating bluish noise. * * This algorithm is reported to create blue noise with a +3 dB per octave power * increase (see the reference below). * Unlike traditional blue noise generation methods that require extensive computation * to massage random values, this algorithm uses an exponentially decaying running * average to prevent clumping of values with minimal computational overhead. * * The algorithm works by: * 1. Generating two random values * 2. Selecting the value that's further from the running average * 3. Updating the running average with the selected value * * Reference: https://7800.8bitdev.org/index.php/Creating_Bluish_Noise_With_Minimal_Compute * Snippet Source: https://tangled.org/strings/did:plc:qi44hxa2obihb2th2lthd5ww/3mwpzmecuy522 * * @class BluishNoiseGenerator */ class BluishNoiseGenerator { /** * Creates a new bluish noise generator with optional range constraints. * @param {number} [min] - Minimum value of the range (inclusive) * @param {number} [max] - Maximum value of the range (exclusive) * @param {boolean} [integer=false] - Quantize candidate values to integers. * Only valid together with min and max. * @throws {Error} If integer is true but min or max is undefined */ constructor(min, max, integer = false) { if (integer && (min === undefined || max === undefined)) { throw new Error("integer mode requires both min and max"); } else if (min === undefined && max === undefined) { // unbounded, ok } else { if (min === undefined || max === undefined) { throw new Error("both min and max must be defined, or neither"); } if (!(min < max)) { throw new RangeError(`min (${min}) must be less than max (${max})`); } } this.min = min; this.max = max; this.integer = integer; // Running average must live in the same range as A/B // (the average stays a float in integer mode) this.C = this.getRandom(); } /** * Generates a random candidate value directly in the target range. * @returns {number} Random value in the target range (integer if enabled) * @private */ getRandom() { // Any masking or range-reduction must happen here, // before comparison, or it reintroduces clumping. if (this.min === undefined || this.max === undefined) { return Math.random(); } const v = Math.random() * (this.max - this.min) + this.min; return this.integer ? Math.floor(v) : v; } /** * Generates the next bluish noise value using Saarna's Bluing Algorithm. * Candidates and the running average are kept in the same range (the * average stays a float in integer mode), so the comparison sees exactly * the values that get returned. * @returns {number} Next bluish noise value */ next() { const A = this.getRandom(); const B = this.getRandom(); let result = A; if (Math.abs(B - this.C) > Math.abs(A - this.C)) { result = B; } this.C = (this.C + result) / 2; return result; } /** * Generates an array of bluish noise values. * @param {number} n - Number of values to generate * @returns {number[]} Array of bluish noise values */ generate(n) { return Array.from({ length: n }, () => this.next()); } } // Usage examples: const unboundedNoise = new BluishNoiseGenerator(); const rangedNoise = new BluishNoiseGenerator(1, 100, true); // Generate a single value const singleValue = unboundedNoise.next(); // Generate multiple values const values = rangedNoise.generate(100); values.forEach((each_element) => { console.log(each_element); });