UNPKG

fast-2d-poisson-disk-sampling

Version:

Fast 2D Poisson Disk Sampling based on a modified Bridson algorithm

263 lines (214 loc) 8.45 kB
"use strict"; var tinyNDArray = require('./tiny-ndarray'); var piDiv3 = Math.PI / 3; var neighbourhood = [ [ 0, 0 ], [ 0, -1 ], [ -1, 0 ], [ 1, 0 ], [ 0, 1 ], [ -1, -1 ], [ 1, -1 ], [ -1, 1 ], [ 1, 1 ], [ 0, -2 ], [ -2, 0 ], [ 2, 0 ], [ 0, 2 ], [ -1, -2 ], [ 1, -2 ], [ -2, -1 ], [ 2, -1 ], [ -2, 1 ], [ 2, 1 ], [ -1, 2 ], [ 1, 2 ] ]; var neighbourhoodLength = neighbourhood.length; /** * FastPoissonDiskSampling constructor * @param {object} options Options * @param {Array} options.shape Shape of the space * @param {float} options.radius Minimum distance between each points * @param {int} [options.tries] Number of times the algorithm will try to place a point in the neighbourhood of another points, defaults to 30 * @param {function|null} [rng] RNG function, defaults to Math.random * @constructor */ function FastPoissonDiskSampling (options, rng) { this.width = options.shape[0]; this.height = options.shape[1]; this.radius = options.radius || options.minDistance; this.maxTries = Math.max(3, Math.ceil(options.tries || 30)); this.rng = rng || Math.random; const floatPrecisionMitigation = Math.max(1, Math.max(this.width, this.height) / 64 | 0); const epsilonRadius = 1e-14 * floatPrecisionMitigation; const epsilonAngle = 2e-14; this.squaredRadius = this.radius * this.radius; this.radiusPlusEpsilon = this.radius + epsilonRadius; this.cellSize = this.radius * Math.SQRT1_2; this.angleIncrement = Math.PI * 2 / this.maxTries; this.angleIncrementOnSuccess = piDiv3 + epsilonAngle; this.triesIncrementOnSuccess = Math.ceil(this.angleIncrementOnSuccess / this.angleIncrement); this.processList = []; this.samplePoints = []; // cache grid this.gridShape = [ Math.ceil(this.width / this.cellSize), Math.ceil(this.height / this.cellSize) ]; this.grid = tinyNDArray(this.gridShape); //will store references to samplePoints } FastPoissonDiskSampling.prototype.width = null; FastPoissonDiskSampling.prototype.height = null; FastPoissonDiskSampling.prototype.radius = null; FastPoissonDiskSampling.prototype.radiusPlusEpsilon = null; FastPoissonDiskSampling.prototype.squaredRadius = null; FastPoissonDiskSampling.prototype.cellSize = null; FastPoissonDiskSampling.prototype.angleIncrement = null; FastPoissonDiskSampling.prototype.angleIncrementOnSuccess = null; FastPoissonDiskSampling.prototype.triesIncrementOnSuccess = null; FastPoissonDiskSampling.prototype.maxTries = null; FastPoissonDiskSampling.prototype.rng = null; FastPoissonDiskSampling.prototype.processList = null; FastPoissonDiskSampling.prototype.samplePoints = null; FastPoissonDiskSampling.prototype.gridShape = null; FastPoissonDiskSampling.prototype.grid = null; /** * Add a totally random point in the grid * @returns {Array} The point added to the grid */ FastPoissonDiskSampling.prototype.addRandomPoint = function () { return this.directAddPoint([ this.rng() * this.width, this.rng() * this.height, this.rng() * Math.PI * 2, 0 ]); }; /** * Add a given point to the grid * @param {Array} point Point * @returns {Array|null} The point added to the grid, null if the point is out of the bound or not of the correct dimension */ FastPoissonDiskSampling.prototype.addPoint = function (point) { var valid = point.length === 2 && point[0] >= 0 && point[0] < this.width && point[1] >= 0 && point[1] < this.height; return valid ? this.directAddPoint([ point[0], point[1], this.rng() * Math.PI * 2, 0 ]) : null; }; /** * Add a given point to the grid, without any check * @param {Array} point Point * @returns {Array} The point added to the grid * @protected */ FastPoissonDiskSampling.prototype.directAddPoint = function (point) { var coordsOnly = [point[0], point[1]]; this.processList.push(point); this.samplePoints.push(coordsOnly); var internalArrayIndex = ((point[0] / this.cellSize) | 0) * this.grid.strideX + ((point[1] / this.cellSize) | 0); this.grid.data[internalArrayIndex] = this.samplePoints.length; // store the point reference return coordsOnly; }; /** * Check whether a given point is in the neighbourhood of existing points * @param {Array} point Point * @returns {boolean} Whether the point is in the neighbourhood of another point * @protected */ FastPoissonDiskSampling.prototype.inNeighbourhood = function (point) { var strideX = this.grid.strideX, boundX = this.gridShape[0], boundY = this.gridShape[1], cellX = point[0] / this.cellSize | 0, cellY = point[1] / this.cellSize | 0, neighbourIndex, internalArrayIndex, currentDimensionX, currentDimensionY, existingPoint; for (neighbourIndex = 0; neighbourIndex < neighbourhoodLength; neighbourIndex++) { currentDimensionX = cellX + neighbourhood[neighbourIndex][0]; currentDimensionY = cellY + neighbourhood[neighbourIndex][1]; internalArrayIndex = ( currentDimensionX < 0 || currentDimensionY < 0 || currentDimensionX >= boundX || currentDimensionY >= boundY ? -1 : currentDimensionX * strideX + currentDimensionY ); if (internalArrayIndex !== -1 && this.grid.data[internalArrayIndex] !== 0) { existingPoint = this.samplePoints[this.grid.data[internalArrayIndex] - 1]; if (Math.pow(point[0] - existingPoint[0], 2) + Math.pow(point[1] - existingPoint[1], 2) < this.squaredRadius) { return true; } } } return false; }; /** * Try to generate a new point in the grid, returns null if it wasn't possible * @returns {Array|null} The added point or null */ FastPoissonDiskSampling.prototype.next = function () { var tries, currentPoint, currentAngle, newPoint; while (this.processList.length > 0) { var index = this.processList.length * this.rng() | 0; currentPoint = this.processList[index]; currentAngle = currentPoint[2]; tries = currentPoint[3]; if (tries === 0) { currentAngle = currentAngle + (this.rng() - 0.5) * piDiv3 * 4; } for (; tries < this.maxTries; tries++) { newPoint = [ currentPoint[0] + Math.cos(currentAngle) * this.radiusPlusEpsilon, currentPoint[1] + Math.sin(currentAngle) * this.radiusPlusEpsilon, currentAngle, 0 ]; if ( (newPoint[0] >= 0 && newPoint[0] < this.width) && (newPoint[1] >= 0 && newPoint[1] < this.height) && !this.inNeighbourhood(newPoint) ) { currentPoint[2] = currentAngle + this.angleIncrementOnSuccess + this.rng() * this.angleIncrement; currentPoint[3] = tries + this.triesIncrementOnSuccess; return this.directAddPoint(newPoint); } currentAngle = currentAngle + this.angleIncrement; } if (tries >= this.maxTries) { const r = this.processList.pop(); if (index < this.processList.length) { this.processList[index] = r; } } } return null; }; /** * Automatically fill the grid, adding a random point to start the process if needed. * Will block the thread, probably best to use it in a web worker or child process. * @returns {Array[]} Sample points */ FastPoissonDiskSampling.prototype.fill = function () { if (this.samplePoints.length === 0) { this.addRandomPoint(); } while(this.next()) {} return this.samplePoints; }; /** * Get all the points in the grid. * @returns {Array[]} Sample points */ FastPoissonDiskSampling.prototype.getAllPoints = function () { return this.samplePoints; }; /** * Reinitialize the grid as well as the internal state */ FastPoissonDiskSampling.prototype.reset = function () { var gridData = this.grid.data, i; // reset the cache grid for (i = 0; i < gridData.length; i++) { gridData[i] = 0; } // new array for the samplePoints as it is passed by reference to the outside this.samplePoints = []; // reset the internal state this.processList.length = 0; }; module.exports = FastPoissonDiskSampling;