Public
Edited
Jan 23, 2023
5 forks
41 stars
Insert cell
Insert cell
Insert cell
function curv(a) {
return a[a.length - 1] - a[a.length - 2];
}
Insert cell
Insert cell
function invCoords(x, r) {
const kappa = 1 / r;
const dot2 = x.reduce((acc, d) => acc + d * d, 0);
return [
...x.map((d) => d / r),
((dot2 - r * r - 1) * kappa) / 2,
((dot2 - r * r + 1) * kappa) / 2
];
}
Insert cell
function regCoords(a) {
const n = a.length - 2;
const kappa = curv(a);
const r = 1 / kappa;
return [a.slice(0, n).map((d) => d * r), r];
}
Insert cell
Insert cell
basis = [
[1, 0, 0, 0, 0],
[0, 1, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 0, 1]
]
Insert cell
specialToCartesian = {
const rt2o2 = Math.sqrt(2) / 2;
const rt6o2 = Math.sqrt(6) / 2;
return (A, B, C, D, E) =>
regCoords([
(-B + C + D - E) * rt2o2,
(B - C + D - E) * rt2o2,
(B + C - D - E) * rt2o2,
A - B - C - D - E,
(B + C + D + E) * rt6o2
]);
}
Insert cell
Insert cell
Insert cell
Insert cell
I = [
(A, B, C, D, E) => [-A, A + B, A + C, A + D, A + E],
(A, B, C, D, E) => [B + A, -B, B + C, B + D, B + E],
(A, B, C, D, E) => [C + A, C + B, -C, C + D, C + E],
(A, B, C, D, E) => [D + A, D + B, D + C, -D, D + E],
(A, B, C, D, E) => [E + A, E + B, E + C, E + D, -E]
]
Insert cell
Insert cell
function generate(basis) {
const result = [];
const rt6o2 = Math.sqrt(6) / 2;
for (let j = 0; j < basis.length; j++) {
let [A, B, C, D, E] = basis[j];
const curvJ = (B + C + D + E) * rt6o2 - (A - B - C - D - E);
for (let i of [0, 1, 2, 3, 4]) {
const tmp = I[i](A, B, C, D, E);
const [A1, B1, C1, D1, E1] = tmp;
const curvI = (B1 + C1 + D1 + E1) * rt6o2 - (A1 - B1 - C1 - D1 - E1);
if (
curvI <= curvJ ||
(i == 0 && (B1 < A1 || C1 < A1 || D1 < A1 || E1 < A1)) ||
(i == 1 && (A1 <= B1 || C1 < B1 || D1 < B1 || E1 < B1)) ||
(i == 2 && (A1 <= C1 || B1 <= C1 || D1 < C1 || E1 < C1)) ||
(i == 3 && (A1 <= D1 || B1 <= D1 || C1 <= D1 || E1 < D1)) ||
(i == 4 && (A1 <= E1 || B1 <= E1 || C1 <= E1 || D1 <= E1))
)
continue;
result.push([A1, B1, C1, D1, E1]);
}
}
return result;
}
Insert cell
Insert cell
firstGen = generate(basis)
Insert cell
secondGen = generate(firstGen)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const colors = d3["scheme" + colorPalette];
const ncolors = colors.length;
return showSpheres(
apollonian
.map((gen, igen) =>
gen
.filter((s) => s[1] <= maxRadiusDisplay && s[0][1] <= maxCenterY)
.map((s) => ({
center: s[0],
radius: s[1],
color: colors[igen % ncolors]
}))
)
.flat(),
visibility,
invalidation
);
}
Insert cell
apollonian = {
let gen = basis;
let result = [basis.map((s) => specialToCartesian(...s))];
let i = 0;
for (let i = 1; i <= generations; i++) {
const spheres = [];
const nextGen = [];
for (let s of gen) {
const sph = specialToCartesian(...s);
if (Math.abs(sph[1]) >= minRadiusCompute) {
spheres.push(sph);
nextGen.push(s);
}
}
if (spheres.length == 0) break;
gen = generate(nextGen);
result.push(spheres);
}
return result;
}
Insert cell
//
// Uncomment to animate the max center y slider
// {
// for (let v = -1; v < 1; v += 0.01) {
// viewof maxCenterY.value = v;
// viewof maxCenterY.dispatchEvent(new CustomEvent("input"));
// yield;
// await Promises.delay(100);
// }
// }
Insert cell
Insert cell
THREE = {
const THREE = (window.THREE = await require("three@0.119.1"));
await require("three@0.119.1/examples/js/controls/OrbitControls.js").catch(
() => {}
);
return THREE;
}
Insert cell
function drawSpheres(spheres, canvas, options = {}) {
const { width, height } = canvas;
const { background = "lightgray" } = options;

const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(width, height);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

const scene = new THREE.Scene();
scene.background = new THREE.Color(background);

const camera = new THREE.PerspectiveCamera(40, width / height, 1, 10000);
camera.position.set(0, 2, 3);

const controls = new THREE.OrbitControls(camera, renderer.domElement);

const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(10, 10, 10);

const ambLight = new THREE.AmbientLight(0xffffff, 0.5);
camera.add(light, ambLight);
light.castShadow = true;

scene.add(camera);
const materials = [];
const colors = [];
const sphereGeom = new THREE.SphereGeometry(1, 32, 16);
for (let { center, radius, color } of spheres) {
if (radius < 0) continue;
color = new THREE.Color(color || "white");
let colorIndex = colors.findIndex((c) => color.equals(c));
let mat;
if (colorIndex == -1) {
colors.push(color);
mat = new THREE.MeshStandardMaterial({
metalness: 0.5,
roughness: 0.8,
color
});
materials.push(mat);
} else mat = materials[colorIndex];
const mesh = new THREE.Mesh(sphereGeom, mat);
mesh.position.set(...center);
mesh.scale.set(radius, radius, radius);
mesh.castShadow = true;
mesh.receiveShadow = true;

scene.add(mesh);
}
return {
draw: function () {
renderer.render(scene, camera);
},
dispose: function () {
renderer.dispose();
controls.dispose();
}
};
}
Insert cell
async function* showSpheres(spheres, visibility, invalidation) {
let canvas = DOM.canvas(width, (width * 9) / 16);
let {draw, dispose} = drawSpheres(spheres, canvas);
invalidation.then(dispose)
draw();
yield canvas;
while (true) {
await visibility();
await Promises.delay(1000 / 60);
draw();
yield canvas;
}
}
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