Published
Edited
Aug 8, 2018
Fork of Soup Layout
1 star
Insert cell
Insert cell
Insert cell
spheres = poissonDisk()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function poissonDisk() {
let firstSphere = Sphere.random(r, radius);
let candidates = [firstSphere];
let allSpheres = [firstSphere];
let octs = octree();
let simplex = new SimplexNoise(randomSeed);
// Adapted from https://bl.ocks.org/wonga00/3987b9736d5077501ef5ee0409ea93c1
let inSphere = (x, y, z, cx, cy, cz, radius) => {
return sqr(x - cx) + sqr(y - cy) + sqr(z - cz) < sqr(radius);
};

let cubeSphereIntersect = (rx, ry, rz, rwidth, rheight, rdepth, cx, cy, cz, radius) => {
let dx = cx - Math.max(rx, Math.min(cx, rx + rwidth));
let dy = cy - Math.max(ry, Math.min(cy, ry + rheight));
let dz = cz - Math.max(rz, Math.min(cz, rz + rdepth));
return (sqr(dx) + sqr(dy) + sqr(dz)) < sqr(radius);
};
// Find the nodes within the specified sphere.
let search = (octtree, cx, cy, cz, radius) => {
let results = [];
octs.visit(function(node, x1, y1, z1, x2, y2, z2) {
if (!node.length) {
do {
var d = node.data;
if (inSphere(d[0], d[1], d[2], cx, cy, cz, radius)) {
results.push({
x: d[0],
y: d[1],
z: d[2],
});
}
} while (node = node.next);
}

return !cubeSphereIntersect(x1, y1, z1, x2 - x1, y2 - y1, z2 - z1, cx, cy, cz, radius);
});
return results;
};
// A generator for nearby spheres using the octree.
let nearbySpheres = (sphere, radius = searchRadius) => {
return search(octs, sphere.x, sphere.y, sphere.z, radius);
};
// Checks all created spheres to see if the candidate is too close.
let tooClose = (candidate, radius) => {
for (let sphere of nearbySpheres(candidate, radius)) {
if (candidate.tooClose(sphere, radius)) return true;
}
return false;
};
// Try to generate a candidate and return it (null on fail).
let generateCandidate = (active) => {
let radius = searchRadius * ((simplex.noise4D(active.x * spacing, active.y * spacing, active.z * spacing, time) / 2 + 0.5) * radiusMultiplier + radiusShift);
// Generate random spheres filled disc from radius r to 2r
for (var i = 0; i < searchAttempts; i++) {
let candidate = active.candidate(radius);
if (candidate.inBounds() && !tooClose(candidate, radius)) return candidate;
}
active.radius = radius;
return null;
}
// Add sphere to the results and candidates.
let addSphere = (sphere) => {
candidates.push(sphere);
allSpheres.push(sphere);
octs.add([sphere.x, sphere.y, sphere.z]);
};
while (candidates.length > 0) {
let active = candidates[candidates.length - 1];
let candidate = generateCandidate(active);
if (candidate == null) {
candidates.pop();
} else {
addSphere(candidate);
}
}
return allSpheres;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class Sphere {
constructor(x, y, z, radius) {
this.x = x;
this.y = y;
this.z = z;
this.radius = radius;
this.r = Math.sqrt(x * x + y * y + z * z);
this.theta = Math.acos(this.z / this.r);
this.phi = Math.atan2(this.y, this.x);
}
candidate(radius = searchRadius) {
const circumference = 2 * Math.PI * this.r;
const fraction = radius / circumference;
const angleRadius = fraction * 2 * Math.PI;
const newAngleRadius = Math.random() * angleRadius + angleRadius;
const angle = Math.random() * 2 * Math.PI;
const angleX = Math.sin(angle);
const angleY = Math.cos(angle);
let newTheta = this.theta + newAngleRadius * angleX;
let newPhi = this.phi + newAngleRadius * angleY;
// while (newTheta >= Math.PI) {
// newTheta -= Math.PI;
// newPhi += Math.PI;
// }
// while (newTheta < 0) {
// newTheta += Math.PI;
// newPhi += Math.PI;
// }
// while (newPhi >= Math.PI * 2) {
// newPhi -= Math.PI * 2;
// }
// while (newPhi < 0) {
// newPhi += Math.PI * 2;
// }
return new Sphere(
this.r * Math.sin(newTheta) * Math.cos(newPhi),
this.r * Math.sin(newTheta) * Math.sin(newPhi),
this.r * Math.cos(newTheta),
this.radius,
);
}
tooClose(other, radius = searchRadius) {
let distance = Math.sqrt(sqr(this.x - other.x) + sqr(this.y - other.y) + sqr(this.z - other.z));
return distance < radius;
}
inBounds() {
// Everything on sphere is "in bounds".
return true;
}
static random(r, radius) {
const theta = Math.random() * Math.PI;
const phi = Math.random() * 2 * Math.PI;
const x = r * Math.sin(theta) * Math.cos(phi);
const y = r * Math.sin(theta) * Math.sin(phi);
const z = r * Math.cos(theta);
return new Sphere(x, y, z, radius);
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more