Compare commits

..

No commits in common. "main" and "feature-node-foreshortening" have entirely different histories.

5 changed files with 100 additions and 306 deletions

View File

@ -2,7 +2,7 @@ import * as THREE from 'three';
const HYPERPLANE = 2.0; const HYPERPLANE = 2.0;
const W_FORESHORTENING = 0.04;
class FourDShape extends THREE.Group { class FourDShape extends THREE.Group {
@ -15,10 +15,11 @@ class FourDShape extends THREE.Group {
this.nodes3 = {}; this.nodes3 = {};
this.links = structure.links; this.links = structure.links;
this.faces = ( "faces" in structure ) ? structure.faces : []; this.faces = ( "faces" in structure ) ? structure.faces : [];
this.node_size = structure.geometry.node_size;
this.link_size = structure.geometry.link_size;
this.node_scale = 1; this.node_scale = 1;
this.link_scale = 1; this.link_scale = 1;
this.hyperplane = HYPERPLANE; this.hyperplane = HYPERPLANE;
this.foreshortening = W_FORESHORTENING;
this.initShapes(); this.initShapes();
} }
@ -43,43 +44,32 @@ class FourDShape extends THREE.Group {
} }
makeLink(material, link) { makeLink(material, link) {
const n1 = this.nodes3[link.source]; const n1 = this.nodes3[link.source].v3;
const n2 = this.nodes3[link.target]; const n2 = this.nodes3[link.target].v3;
const s1 = n1.scale; const length = n1.distanceTo(n2);
const s2 = n2.scale;
const length = n1.v3.distanceTo(n2.v3);
const centre = new THREE.Vector3(); const centre = new THREE.Vector3();
centre.lerpVectors(n1.v3, n2.v3, 0.5); centre.lerpVectors(n1, n2, 0.5);
const geometry = new THREE.CylinderGeometry( const geometry = new THREE.CylinderGeometry(this.link_size, this.link_size, 1);
this.link_scale * s2, this.link_scale * s1, 1,
16, 1, true
);
const cyl = new THREE.Mesh(geometry, material); const cyl = new THREE.Mesh(geometry, material);
const edge = new THREE.Group(); const edge = new THREE.Group();
edge.add(cyl); edge.add(cyl);
edge.position.copy(centre); edge.position.copy(centre);
edge.scale.copy(new THREE.Vector3(1, 1, length)); edge.scale.copy(new THREE.Vector3(1, 1, length));
edge.lookAt(n2.v3); edge.lookAt(n2);
cyl.rotation.x = Math.PI / 2.0; cyl.rotation.x = Math.PI / 2.0;
this.add(edge); this.add(edge);
return edge; return edge;
} }
updateLink(link, links_show) { updateLink(link, links_show) {
const n1 = this.nodes3[link.source]; const n1 = this.nodes3[link.source].v3;
const n2 = this.nodes3[link.target]; const n2 = this.nodes3[link.target].v3;
const s1 = n1.scale; const length = n1.distanceTo(n2);
const s2 = n2.scale;
const length = n1.v3.distanceTo(n2.v3);
const centre = new THREE.Vector3(); const centre = new THREE.Vector3();
centre.lerpVectors(n1.v3, n2.v3, 0.5); centre.lerpVectors(n1, n2, 0.5);
// take the average of the ends as the thickness - as a workaround, link.object.scale.copy(new THREE.Vector3(this.link_scale, this.link_scale, length));
// because I haven't worked out how to reshape tapered links without
// having to reassign a new geometry to every link
const link_mean = this.link_scale * (s1 + s2) * 0.5;
link.object.scale.copy(new THREE.Vector3(link_mean, link_mean, length));
link.object.position.copy(centre); link.object.position.copy(centre);
link.object.lookAt(n2.v3); link.object.lookAt(n2);
link.object.children[0].rotation.x = Math.PI / 2.0; link.object.children[0].rotation.x = Math.PI / 2.0;
link.object.visible = (!links_show || link.label in links_show); link.object.visible = (!links_show || link.label in links_show);
} }
@ -143,7 +133,6 @@ class FourDShape extends THREE.Group {
const material = this.getMaterial(n, this.node_ms); const material = this.getMaterial(n, this.node_ms);
this.nodes3[n.id] = { this.nodes3[n.id] = {
v3: v3, v3: v3,
scale: k,
label: n.label, label: n.label,
object: this.makeNode(material, v3, k) object: this.makeNode(material, v3, k)
}; };
@ -165,10 +154,9 @@ class FourDShape extends THREE.Group {
const v4 = this.fourDrotate(n.x, n.y, n.z, n.w, rotations); const v4 = this.fourDrotate(n.x, n.y, n.z, n.w, rotations);
const k = this.fourDscale(v4.w); const k = this.fourDscale(v4.w);
const v3 = new THREE.Vector3(v4.x * k, v4.y * k, v4.z * k); const v3 = new THREE.Vector3(v4.x * k, v4.y * k, v4.z * k);
const s4 = k * this.node_scale * this.foreshortening; const s4 = k * this.node_scale;
const s3 = new THREE.Vector3(s4, s4, s4); const s3 = new THREE.Vector3(s4, s4, s4);
this.nodes3[n.id].v3 = v3; this.nodes3[n.id].v3 = v3;
this.nodes3[n.id].scale = k * this.foreshortening;
this.nodes3[n.id].object.position.copy(v3); this.nodes3[n.id].object.position.copy(v3);
this.nodes3[n.id].object.scale.copy(s3); this.nodes3[n.id].object.scale.copy(s3);
this.nodes3[n.id].object.visible = ( !nodes_show || n.label in nodes_show ); this.nodes3[n.id].object.visible = ( !nodes_show || n.label in nodes_show );

39
gui.js
View File

@ -2,11 +2,10 @@ import { GUI } from 'lil-gui';
const DEFAULTS = { const DEFAULTS = {
nodesize: 0.25, thickness: 1.0,
nodeopacity: 1, nodesize: 2.0,
linksize: 0.2, linkopacity: 0.5,
linkopacity: 0.75, link2opacity: 0.5,
link2opacity: 0.75,
shape: '120-cell', shape: '120-cell',
option: 'none', option: 'none',
visibility: 5, visibility: 5,
@ -14,13 +13,11 @@ const DEFAULTS = {
inscribe_all: false, inscribe_all: false,
color: 0x3293a9, color: 0x3293a9,
background: 0xd4d4d4, background: 0xd4d4d4,
hyperplane: 0.93, hyperplane: 1.5,
zoom: 1, zoom: 1,
xRotate: 'YW', xRotate: 'YW',
yRotate: 'XW', yRotate: 'XZ',
dtheta: 0, dtheta: 0,
damping: false,
captions: true,
dpsi: 0, dpsi: 0,
} }
@ -28,7 +25,7 @@ const DEFAULTS = {
class FourDGUI { class FourDGUI {
constructor(shapes, changeShape, setColor, setBackground, setNodeOpacity,setLinkOpacity, setVisibility, showDocs) { constructor(shapes, changeShape, setColor, setBackground, setLinkOpacity, setVisibility) {
this.gui = new GUI(); this.gui = new GUI();
const SHAPE_NAMES = shapes.map((s) => s.name); const SHAPE_NAMES = shapes.map((s) => s.name);
@ -39,11 +36,10 @@ class FourDGUI {
option: this.link['option'], option: this.link['option'],
inscribed: this.link['inscribed'], inscribed: this.link['inscribed'],
inscribe_all: this.link['inscribe_all'], inscribe_all: this.link['inscribe_all'],
linksize: this.link['linksize'], thickness: this.link['thickness'],
linkopacity: this.link['linkopacity'], linkopacity: this.link['linkopacity'],
link2opacity: this.link['linkopacity'], link2opacity: this.link['linkopacity'],
nodesize: this.link['nodesize'], nodesize: this.link['nodesize'],
nodeopacity: this.link['nodeopacity'],
depth: this.link['depth'], depth: this.link['depth'],
color: this.link['color'], color: this.link['color'],
background: this.link['background'], background: this.link['background'],
@ -52,7 +48,6 @@ class FourDGUI {
xRotate: this.link['xRotate'], xRotate: this.link['xRotate'],
yRotate: this.link['yRotate'], yRotate: this.link['yRotate'],
damping: false, damping: false,
captions: true,
dtheta: this.link['dtheta'], dtheta: this.link['dtheta'],
dpsi: this.link['dpsi'], dpsi: this.link['dpsi'],
"copy link": function () { guiObj.copyUrl() } "copy link": function () { guiObj.copyUrl() }
@ -70,22 +65,20 @@ class FourDGUI {
options_ctrl = this.gui.add(this.params, 'option').options(options).onChange((option) => { options_ctrl = this.gui.add(this.params, 'option').options(options).onChange((option) => {
setVisibility(option) setVisibility(option)
}); });
this.gui.add(this.params, 'hyperplane', 0.5, 1 / 0.8); this.gui.add(this.params, 'hyperplane', 1.4, 2.0);
this.gui.add(this.params, 'zoom', 0.1, 2.0); this.gui.add(this.params, 'zoom', 0.1, 2.0);
this.gui.add(this.params, 'nodesize', 0, 1); this.gui.add(this.params, 'thickness', 0, 2);
this.gui.add(this.params, 'nodeopacity', 0, 1).onChange(setNodeOpacity);
this.gui.add(this.params, 'linksize', 0, 1);
this.gui.add(this.params, 'linkopacity', 0, 1).onChange( this.gui.add(this.params, 'linkopacity', 0, 1).onChange(
(v) => setLinkOpacity(v, true) (v) => setLinkOpacity(v, true)
); );
this.gui.add(this.params, 'link2opacity', 0, 1).onChange( this.gui.add(this.params, 'link2opacity', 0, 1).onChange(
(v) => setLinkOpacity(v, false) (v) => setLinkOpacity(v, false)
); );
this.gui.add(this.params, 'nodesize', 0.1, 4);
this.gui.addColor(this.params, 'color').onChange(setColor); this.gui.addColor(this.params, 'color').onChange(setColor);
this.gui.addColor(this.params, 'background').onChange(setBackground); this.gui.addColor(this.params, 'background').onChange(setBackground);
this.gui.add(this.params, 'xRotate', [ 'YW', 'YZ', 'ZW' ]); this.gui.add(this.params, 'xRotate', [ 'YW', 'YZ', 'ZW' ]);
this.gui.add(this.params, 'yRotate', [ 'XZ', 'XY', 'XW' ]); this.gui.add(this.params, 'yRotate', [ 'XZ', 'XY', 'XW' ]);
this.gui.add(this.params, 'captions').onChange(showDocs);
this.gui.add(this.params, 'damping'); this.gui.add(this.params, 'damping');
this.gui.add(this.params, 'copy link'); this.gui.add(this.params, 'copy link');
@ -141,11 +134,10 @@ class FourDGUI {
} }
this.link['hyperplane'] = this.numParam('hyperplane', parseFloat); this.link['hyperplane'] = this.numParam('hyperplane', parseFloat);
this.link['zoom'] = this.numParam('zoom', parseFloat); this.link['zoom'] = this.numParam('zoom', parseFloat);
this.link['linksize'] = this.numParam('linksize', parseFloat); this.link['thickness'] = this.numParam('thickness', parseFloat);
this.link['linkopacity'] = this.numParam('linkopacity', parseFloat); this.link['linkopacity'] = this.numParam('linkopacity', parseFloat);
this.link['link2opacity'] = this.numParam('link2opacity', parseFloat); this.link['link2opacity'] = this.numParam('link2opacity', parseFloat);
this.link['nodesize'] = this.numParam('nodesize', parseFloat); this.link['nodesize'] = this.numParam('nodesize', parseFloat);
this.link['nodeopacity'] = this.numParam('nodeopacity', parseFloat);
this.link['color'] = this.numParam('color', (s) => guiObj.stringToHex(s)); this.link['color'] = this.numParam('color', (s) => guiObj.stringToHex(s));
this.link['background'] = this.numParam('background', (s) => guiObj.stringToHex(s)); this.link['background'] = this.numParam('background', (s) => guiObj.stringToHex(s));
this.link['dpsi'] = this.numParam('dpsi', parseFloat); this.link['dpsi'] = this.numParam('dpsi', parseFloat);
@ -159,11 +151,10 @@ class FourDGUI {
url.searchParams.append("option", this.params.option); url.searchParams.append("option", this.params.option);
url.searchParams.append("inscribed", this.params.inscribed ? 'y': 'n'); url.searchParams.append("inscribed", this.params.inscribed ? 'y': 'n');
url.searchParams.append("inscribe_all", this.params.inscribe_all ? 'y': 'n'); url.searchParams.append("inscribe_all", this.params.inscribe_all ? 'y': 'n');
url.searchParams.append("linksize", this.params.linksize.toString()); url.searchParams.append("thickness", this.params.thickness.toString());
url.searchParams.append("nodesize", this.params.nodesize.toString()); url.searchParams.append("nodesize", this.params.nodesize.toString());
url.searchParams.append("nodeopacity", this.params.nodesize.toString()); url.searchParams.append("linkopacity", this.params.thickness.toString());
url.searchParams.append("linkopacity", this.params.nodeopacity.toString()); url.searchParams.append("link2opacity", this.params.nodesize.toString());
url.searchParams.append("link2opacity", this.params.link2opacity.toString());
url.searchParams.append("color", this.hexToString(this.params.color)); url.searchParams.append("color", this.hexToString(this.params.color));
url.searchParams.append("background", this.hexToString(this.params.background)); url.searchParams.append("background", this.hexToString(this.params.background));
url.searchParams.append("hyperplane", this.params.hyperplane.toString()); url.searchParams.append("hyperplane", this.params.hyperplane.toString());

View File

@ -5,15 +5,6 @@
<title>FourD</title> <title>FourD</title>
<style> <style>
body { margin: 0; } body { margin: 0; }
div#description {
position: fixed;
top: 0;
left: 0;
width: 20%;
z-index: 2;
font-family: sans-serif;
padding: 1em;
}
div#info { div#info {
position: fixed; position: fixed;
bottom:0; bottom:0;
@ -25,7 +16,6 @@
</head> </head>
<body> <body>
<script type="module" src="/main.js"></script> <script type="module" src="/main.js"></script>
<div id="description"></div>
<div id="info">by <a target="_blank" href="https://mikelynch.org/">Mike Lynch</a> - <div id="info">by <a target="_blank" href="https://mikelynch.org/">Mike Lynch</a> -
<a target="_blank" href="https://git.tilde.town/bombinans/fourdjs">source</a></div> <a target="_blank" href="https://git.tilde.town/bombinans/fourdjs">source</a></div>
</body> </body>

51
main.js
View File

@ -9,7 +9,7 @@ import { FourDShape } from './fourDShape.js';
import { get_colours } from './colours.js'; import { get_colours } from './colours.js';
const FACE_OPACITY = 0.3; const FACE_OPACITY = 0.3;
const CAMERA_K = 5; const CAMERA_K = 10;
// scene, lights and camera // scene, lights and camera
@ -47,19 +47,11 @@ material.opacity = 0.5;
const node_ms = node_colours.map((c) => new THREE.MeshStandardMaterial({color: c})); const node_ms = node_colours.map((c) => new THREE.MeshStandardMaterial({color: c}));
const link_ms = node_colours.map((c) => new THREE.MeshStandardMaterial({color: c})); const link_ms = node_colours.map((c) => new THREE.MeshStandardMaterial({color: c}));
node_ms.map((m) => {
m.transparent = true;
m.opacity = 1.0;
}
);
link_ms.map((m) => { link_ms.map((m) => {
m.transparent = true; m.transparent = true;
m.opacity = 0.5; m.opacity = 0.5;
} }
); )
const face_ms = [ const face_ms = [
new THREE.MeshLambertMaterial( { color: 0x44ff44 } ) new THREE.MeshLambertMaterial( { color: 0x44ff44 } )
@ -93,26 +85,6 @@ function createShape(name, option) {
setVisibility(option ? option : structure.options[0].name); setVisibility(option ? option : structure.options[0].name);
} }
function displayDocs(name) {
const docdiv = document.getElementById("description");
const description = STRUCTURES_BY_NAME[name].description;
if( description ) {
docdiv.innerHTML =`<p>${name}</p><p>${description}</p>`;
} else {
docdiv.innerHTML =`<p>${name}</p>`;
}
}
function showDocs(visible) {
console.log(`showDocs ${visible}`);
const docdiv = document.getElementById("description");
if( visible ) {
docdiv.style.display = '';
} else {
docdiv.style.display = 'none';
}
}
// initialise gui and read params from URL // initialise gui and read params from URL
// callbacks to do things which are triggered by controls: reset the shape, // callbacks to do things which are triggered by controls: reset the shape,
@ -143,17 +115,11 @@ function setLinkOpacity(o, primary) {
} }
} }
function setNodeOpacity(o) {
node_ms.map((nm) => nm.opacity = o);
}
let gui; let gui;
function changeShape() { function changeShape() {
createShape(gui.params.shape); createShape(gui.params.shape);
displayDocs(gui.params.shape);
} }
function setVisibility(option_name) { function setVisibility(option_name) {
@ -172,10 +138,8 @@ gui = new FourDGUI(
changeShape, changeShape,
setColors, setColors,
setBackground, setBackground,
setNodeOpacity,
setLinkOpacity, setLinkOpacity,
setVisibility, setVisibility
showDocs
); );
// these are here to pick up colour settings from the URL params // these are here to pick up colour settings from the URL params
@ -220,7 +184,6 @@ renderer.domElement.addEventListener("pointerup", (event) => {
}) })
createShape(gui.params.shape, gui.params.option); createShape(gui.params.shape, gui.params.option);
displayDocs(gui.params.shape);
function animate() { function animate() {
requestAnimationFrame( animate ); requestAnimationFrame( animate );
@ -238,11 +201,11 @@ function animate() {
rotfn[gui.params.xRotate](theta), rotfn[gui.params.xRotate](theta),
rotfn[gui.params.yRotate](psi) rotfn[gui.params.yRotate](psi)
]; ];
shape.hyperplane = 1 / gui.params.hyperplane; shape.hyperplane = gui.params.hyperplane;
camera.position.set(0, 0, gui.params.zoom * CAMERA_K * gui.params.hyperplane); camera.position.set(0, 0, gui.params.zoom * CAMERA_K / gui.params.hyperplane);
shape.link_scale = gui.params.thickness;
shape.node_scale = gui.params.nodesize; shape.node_scale = gui.params.nodesize;
shape.link_scale = gui.params.linksize * gui.params.nodesize * 0.5;
shape.render3(rotations, node_show, link_show); shape.render3(rotations, node_show, link_show);

View File

@ -58,15 +58,16 @@ export function auto_detect_edges(nodes, neighbours, debug=false) {
// too small and simple to calculate // too small and simple to calculate
export const cell5 = () => { export const cell5 = () => {
const c1 = Math.sqrt(5) / 4; const r5 = Math.sqrt(5);
const r2 = Math.sqrt(2) / 2;
return { return {
name: '5-cell', name: '5-cell',
nodes: [ nodes: [
{id:1, label: 1, x: c1, y: c1, z: c1, w: -0.25 }, {id:1, label: 1, x: r2, y: r2, z: r2, w: -r2 / r5 },
{id:2, label: 2, x: c1, y: -c1, z: -c1, w: -0.25 }, {id:2, label: 2, x: r2, y: -r2, z: -r2, w: -r2 / r5 },
{id:3, label: 3, x: -c1, y: c1, z: -c1, w: -0.25 }, {id:3, label: 3, x: -r2, y: r2, z: -r2, w: -r2 / r5 },
{id:4, label: 4, x: -c1, y: -c1, z: c1, w: -0.25 }, {id:4, label: 4, x: -r2, y: -r2, z: r2, w: -r2 / r5 },
{id:5, label: 5, x: 0, y: 0, z: 0, w: 1 }, {id:5, label: 5, x: 0, y: 0, z: 0, w: 4 * r2 / r5 },
], ],
links: [ links: [
{ id:1, source:1, target: 2}, { id:1, source:1, target: 2},
@ -80,12 +81,11 @@ export const cell5 = () => {
{ id:9, source:3, target: 5}, { id:9, source:3, target: 5},
{ id:10, source:4, target: 5}, { id:10, source:4, target: 5},
], ],
options: [ { name: '--' }], geometry: {
description: `Five tetrahedra joined at ten faces with three node_size: 0.02,
tetrahedra around each edge. The 5-cell is the simplest regular link_size: 0.02
four-D polytope and the four-dimensional analogue of the tetrahedron. },
A corresponding polytope, or simplex, exists for every n-dimensional options: [ { name: '--' }]
space.`,
}; };
}; };
@ -104,18 +104,18 @@ export const cell16 = () => {
nodes[1].label = 4; nodes[1].label = 4;
index_nodes(nodes); index_nodes(nodes);
scale_nodes(nodes, 0.5); scale_nodes(nodes, 0.75);
const links = auto_detect_edges(nodes, 6); const links = auto_detect_edges(nodes, 6);
return { return {
name: '16-cell', name: '16-cell',
nodes: nodes, nodes: nodes,
links: links, links: links,
options: [ { name: '--' }], geometry: {
description: `Sixteen tetrahedra joined at 32 faces with four node_size: 0.02,
tetrahedra around each edge. The 16-cell is the four-dimensional link_size: 0.02
analogue of the octahedron and is dual to the tesseract. Every },
n-dimensional space has a corresponding polytope in this family.`, options: [ { name: '--' }]
}; };
}; };
@ -133,7 +133,7 @@ export const tesseract = () => {
} }
} }
scale_nodes(nodes, 0.5); scale_nodes(nodes, Math.sqrt(2) / 2);
const links = auto_detect_edges(nodes, 4); const links = auto_detect_edges(nodes, 4);
links.map((l) => { l.label = 0 }); links.map((l) => { l.label = 0 });
@ -146,20 +146,18 @@ export const tesseract = () => {
return { return {
name: 'Tesseract', name: 'tesseract',
nodes: nodes, nodes: nodes,
links: links, links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
options: [ options: [
{ name: 'none', links: [ 0 ] }, { name: 'none', links: [ 0 ] },
{ name: 'one 16-cell', links: [ 0, 1 ] }, { name: 'one 16-cell', links: [ 0, 1 ] },
{ name: 'both 16-cells', links: [ 0, 1, 2 ] }, { name: 'both 16-cells', links: [ 0, 1, 2 ] },
], ],
description: `The most well-known four-dimensional shape, the
tesseract is analogous to the cube, and is constructed by placing two
cubes in parallel hyperplanes and joining their corresponding
vertices. It consists of eight cubes joined at 32 face with three
cubes around each edge, and is dual to the 16-cell. Every
n-dimensional space has a cube analogue or measure polytope.`,
}; };
} }
@ -185,7 +183,6 @@ export const cell24 = () => {
n.label = CELL24_INDEXING[axes[0]][axes[1]]; n.label = CELL24_INDEXING[axes[0]][axes[1]];
} }
scale_nodes(nodes, Math.sqrt(2) / 2);
index_nodes(nodes); index_nodes(nodes);
const links = auto_detect_edges(nodes, 8); const links = auto_detect_edges(nodes, 8);
links.map((l) => l.label = 0); links.map((l) => l.label = 0);
@ -209,16 +206,16 @@ export const cell24 = () => {
name: '24-cell', name: '24-cell',
nodes: nodes, nodes: nodes,
links: links, links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
base: {}, base: {},
options: [ options: [
{ name: 'none', links: [ 0 ] }, { name: 'none', links: [ 0 ] },
{ name: 'one 16-cell', links: [ 0, 1 ] }, { name: 'one 16-cell', links: [ 0, 1 ] },
{ name: 'three 16-cells', links: [ 0, 1, 2, 3 ] } { name: 'three 16-cells', links: [ 0, 1, 2, 3 ] }
], ]
description: `A unique object without an exact analogue in higher
or lower dimensions, the 24-cell is made of twenty-four octahedra
joined at 96 faces, with three around each edge. The 24-cell is
self-dual.`,
}; };
} }
@ -322,7 +319,7 @@ export function make_120cell_vertices() {
PERMUTE.coordinates([2, 1, phi, phiinv], 0, true), PERMUTE.coordinates([2, 1, phi, phiinv], 0, true),
].flat(); ].flat();
index_nodes(nodes); index_nodes(nodes);
scale_nodes(nodes, 0.25 * Math.sqrt(2)); scale_nodes(nodes, 0.5);
return nodes; return nodes;
} }
@ -396,10 +393,12 @@ export const cell120_layered = (max) => {
name: '120-cell layered', name: '120-cell layered',
nodes: nodes, nodes: nodes,
links: links, links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
nolink2opacity: true, nolink2opacity: true,
options: options, options: options
description: `This version of the 120-cell lets you explore its
structure by building each layer from the 'north pole' onwards.`,
} }
} }
@ -427,15 +426,15 @@ export const cell120_inscribed = () => {
name: '120-cell', name: '120-cell',
nodes: nodes, nodes: nodes,
links: links, links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
options: [ options: [
{ name: "none", links: [ 0 ]}, { name: "none", links: [ 0 ]},
{ name: "one inscribed 600-cell", links: [ 0, 1 ] }, { name: "one inscribed 600-cell", links: [ 0, 1 ] },
{ name: "five inscribed 600-cells", links: [ 0, 1, 2, 3, 4, 5 ] } { name: "five inscribed 600-cells", links: [ 0, 1, 2, 3, 4, 5 ] }
], ]
description: `The 120-cell is the four-dimensional analogue of the
dodecahedron, and consists of 120 dodecahedra joined at 720 faces,
with three dodecahedra around each edge. It is dual to the 600-cell,
and five 600-cells can be inscribed in its vertices.`,
} }
} }
@ -516,7 +515,7 @@ export function make_600cell_vertices() {
index_nodes(nodes); index_nodes(nodes);
scale_nodes(nodes, 0.5); scale_nodes(nodes, 0.75);
return nodes; return nodes;
} }
@ -558,16 +557,15 @@ export const cell600 = () => {
name: '600-cell', name: '600-cell',
nodes: nodes, nodes: nodes,
links: links, links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
options: [ options: [
{ name: "none", links: [ 0 ]}, { name: "none", links: [ 0 ]},
{ name: "one 24-cell", links: [ 0, 1 ] }, { name: "one 24-cell", links: [ 0, 1 ] },
{ name: "five 24-cells", links: [ 0, 1, 2, 3, 4, 5 ] } { name: "five 24-cells", links: [ 0, 1, 2, 3, 4, 5 ] }
], ]
description: `The 600-cell is the four-dimensional analogue of the
icosahedron, and consists of 600 tetrahedra joined at 1200 faces
with five tetrahedra around each edge. It is dual to the 120-cell.
Its 120 vertices can be partitioned into five sets which form the
vertices of five inscribed 24-cells.`,
} }
} }
@ -607,10 +605,12 @@ export const cell600_layered = () => {
name: '600-cell layered', name: '600-cell layered',
nodes: nodes, nodes: nodes,
links: links, links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
nolink2opacity: true, nolink2opacity: true,
options: options, options: options
description: `This version of the 600-cell lets you explore its
structure by building each layer from the 'north pole' onwards.`,
} }
@ -631,15 +631,14 @@ export const snub24cell = () => {
links.map((l) => l.label = 0); links.map((l) => l.label = 0);
return { return {
name: 'Snub 24-cell', name: 'snub 24-cell',
nodes: nodes, nodes: nodes,
links: links, links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
options: [ { name: "--" } ], options: [ { name: "--" } ],
description: `The snub 24-cell is a semiregular polytope which
connects the 24-cell with the 600-cell. It consists of 24 icosahedra
and 120 tetrahedra, and is constructed by removing one of the
five inscribed 24-cells from a 600-cell.`
} }
@ -679,7 +678,6 @@ function make_dodecahedron_vertices() {
{ x: -phi, y: phiinv, z:0, w: 0 , label: 4}, { x: -phi, y: phiinv, z:0, w: 0 , label: 4},
{ x: -phi, y: -phiinv, z:0, w: 0 , label: 2}, { x: -phi, y: -phiinv, z:0, w: 0 , label: 2},
]; ];
scale_nodes(nodes, 1 / Math.sqrt(3));
index_nodes(nodes); index_nodes(nodes);
return nodes; return nodes;
} }
@ -697,146 +695,18 @@ export const dodecahedron = () => {
} }
return { return {
name: 'Dodecahedron', name: 'dodecahedron',
nodes: nodes, nodes: nodes,
links: links, links: links,
geometry: {
node_size: 0.02,
link_size: 0.02
},
options: [ options: [
{ name: "none", links: [ 0 ]}, { name: "none", links: [ 0 ]},
{ name: "one tetrahedron", links: [ 0, 1 ] }, { name: "one tetrahedron", links: [ 0, 1 ] },
{ name: "five tetrahedra", links: [ 0, 1, 2, 3, 4, 5 ] } { name: "five tetrahedra", links: [ 0, 1, 2, 3, 4, 5 ] }
], ]
description: `The dodecahedron is a three-dimensional polyhedron
which is included here so that you can see the partition of its
vertices into five interlocked tetrahedra. This structure is the
basis for the partition of the 120-cell's vertices into five
600-cells.`
}
}
export const tetrahedron = () => {
const r2 = Math.sqrt(2);
const r3 = Math.sqrt(3);
return {
name: 'Tetrahedron',
nodes: [
{id:1, label: 1, x: 2 * r2 / 3, y: 0, z: -1/3, w: 0 },
{id:2, label: 2, x: -r2 / 3, y: r2 / r3, z: -1/3, w: 0 },
{id:3, label: 3, x: -r2 / 3, y: -r2 / r3, z: -1/3, w: 0 },
{id:4, label: 4, x: 0, y: 0, z: 1, w: 0 },
],
links: [
{ id:1, source:1, target: 2},
{ id:2, source:1, target: 3},
{ id:3, source:1, target: 4},
{ id:4, source:2, target: 3},
{ id:5, source:2, target: 4},
{ id:6, source:3, target: 4},
],
options: [ { name: '--' }],
description: `The simplest three-dimensional polytope, consisting of four triangles joined at six edges. The 5-cell is its four-dimensional analogue.`,
};
};
export const octahedron = () => {
const nodes = [
{id: 1, label: 1, x: 1, y: 0, z: 0, w: 0},
{id: 2, label: 1, x: -1, y: 0, z: 0, w: 0},
{id: 3, label: 2, x: 0, y: 1, z: 0, w: 0},
{id: 4, label: 2, x: 0, y: -1, z: 0, w: 0},
{id: 5, label: 3, x: 0, y: 0, z: 1, w: 0},
{id: 6, label: 3, x: 0, y: 0, z: -1, w: 0},
];
const links = [
{id:1, source: 1, target: 3},
{id:2, source: 1, target: 4},
{id:3, source: 1, target: 5},
{id:4, source: 1, target: 6},
{id:5, source: 2, target: 3},
{id:6, source: 2, target: 4},
{id:7, source: 2, target: 5},
{id:8, source: 2, target: 6},
{id:9, source: 3, target: 5},
{id:10, source: 3, target: 6},
{id:11, source: 4, target: 5},
{id:12, source: 4, target: 6},
]
links.map((l) => { l.label = 0 });
return {
name: 'Octahedron',
nodes: nodes,
links: links,
options: [ { name: '--' }],
description: `The three-dimensional cross-polytope, the 16-cell is its four-dimensional analogue.`,
};
}
export const cube = () => {
const nodes = [
{id: 1, label: 1, x: 1, y: 1, z: 1, w: 0},
{id: 2, label: 2, x: -1, y: 1, z: 1, w: 0},
{id: 3, label: 2, x: 1, y: -1, z: 1, w: 0},
{id: 4, label: 1, x: -1, y: -1, z: 1, w: 0},
{id: 5, label: 2, x: 1, y: 1, z: -1, w: 0},
{id: 6, label: 1, x: -1, y: 1, z: -1, w: 0},
{id: 7, label: 1, x: 1, y: -1, z: -1, w: 0},
{id: 8, label: 2, x: -1, y: -1, z: -1, w: 0},
];
scale_nodes(nodes, 1/Math.sqrt(3));
const links = auto_detect_edges(nodes, 3);
links.map((l) => { l.label = 0 });
return {
name: 'Cube',
nodes: nodes,
links: links,
options: [ { name: '--' }],
description: `The three-dimensional measure polytope, the tesseract is its four-dimensional analogue.`,
};
}
function make_icosahedron_vertices() {
const phi = 0.5 * (1 + Math.sqrt(5));
const nodes = [
{ x: 0, y: 1, z: phi, w: 0, label: 1 },
{ x: 0, y: -1, z: phi, w: 0, label: 1 },
{ x: 0, y: 1, z: -phi, w: 0, label: 1 },
{ x: 0, y: -1, z: -phi, w: 0, label: 1 },
{ x: 1, y: phi, z: 0, w: 0, label: 2 },
{ x: -1, y: phi, z: 0, w: 0, label: 2 },
{ x: 1, y: -phi, z: 0, w: 0, label: 2 },
{ x: -1, y: -phi, z: 0, w: 0, label: 2 },
{ x: phi, y: 0, z: 1, w: 0, label: 3},
{ x: phi, y: 0, z: -1, w: 0, label: 3},
{ x: -phi, y: 0, z: 1, w: 0, label: 3},
{ x: -phi, y: 0, z: -1, w: 0, label: 3},
];
scale_nodes(nodes, 1/Math.sqrt((5 + Math.sqrt(5)) / 2));
index_nodes(nodes);
return nodes;
}
export const icosahedron = () => {
const nodes = make_icosahedron_vertices();
const links = auto_detect_edges(nodes, 5);
links.map((l) => l.label = 0);
return {
name: 'Icosahedron',
nodes: nodes,
links: links,
options: [
{ name: "--"},
],
description: `The icosahedron is a twenty-sided polyhedron and is dual to the dodecahedron. Its four-dimensional analogue is the 600-cell.`
} }
} }
@ -844,10 +714,6 @@ export const icosahedron = () => {
export const build_all = () => { export const build_all = () => {
return [ return [
tetrahedron(),
octahedron(),
cube(),
icosahedron(),
dodecahedron(), dodecahedron(),
cell5(), cell5(),
cell16(), cell16(),
@ -859,8 +725,4 @@ export const build_all = () => {
cell120_inscribed(), cell120_inscribed(),
cell120_layered() cell120_layered()
]; ];
}
export const radii = (shape) => {
return shape.nodes.map(n => Math.sqrt(n.x * n.x + n.y * n.y + n.z * n.z + n.w * n.w))
} }