fourdjs/main.js
2026-01-04 14:48:25 +11:00

238 lines
5.5 KiB
JavaScript

import * as THREE from 'three';
const RELEASE_NOTES = `
<p><b>v1.1 - 1/1/2026</b></p>
<p>The 120-cell now includes a visualisation of its inscribed 5-cells, which honestly
looks like less of a mess than I expected it to.</p>
<p><b>v1.0 - 16/11/2025</b></p>
<p>It's been <a target="_blank" href="https://mikelynch.org/2023/Sep/02/120-cell/">two years</a> since
I first made this, and I haven't updated it in a while, but I got tapered links to
work without too much performance overhead, so that seemed worth a version.</p>
<p>The results flicker a bit at low opacities but otherwise I'm pretty happy with
it.</p>
`;
import * as POLYTOPES from './polytopes.js';
import { rotfn } from './rotation.js';
import { FourDGUI, DEFAULTS } from './gui.js';
import { FourDShape } from './fourDShape.js';
import { get_colours } from './colours.js';
const FACE_OPACITY = 0.3;
const CAMERA_K = 5;
// scene, lights and camera
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, 1, 0.1, 1000 );
const light = new THREE.PointLight(0xffffff, 2);
light.position.set(10, 10, 10);
scene.add(light);
const light2 = new THREE.PointLight(0xffffff, 2);
light2.position.set(-10, 5, 10);
scene.add(light);
const amblight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(amblight);
camera.position.set(0, 0, CAMERA_K / 2);
camera.lookAt(0, 0, 0);
//camera.position.z = 4;
const canvas = document.getElementById("canvas");
const renderer = new THREE.WebGLRenderer({antialias: true, canvas: canvas});
renderer.setSize(40,40);
canvas.style.width="400px";
canvas.style.height="400px";
renderer.localClippingEnabled = true;
const gl = renderer.getContext();
// set up colours and materials for gui callbacks
scene.background = new THREE.Color(DEFAULTS.background);
const node_colours = get_colours(DEFAULTS.color);
const node_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) => {
m.transparent = true;
m.opacity = 0.5;
}
);
console.log("link_ms", link_ms);
const face_ms = [
new THREE.MeshStandardMaterial( { color: 0x44ff44 } )
];
for( const face_m of face_ms ) {
face_m.transparent = true;
face_m.opacity = FACE_OPACITY;
}
const STRUCTURES = POLYTOPES.build_all();
const STRUCTURES_BY_NAME = {};
STRUCTURES.map((s) => STRUCTURES_BY_NAME[s.name] = s);
let shape = false;
let structure = false;
let node_show = [];
let link_show = [];
function createShape(name, option) {
if( shape ) {
scene.remove(shape);
}
structure = STRUCTURES_BY_NAME[name];
shape = new FourDShape(node_ms, link_ms, face_ms, structure);
scene.add(shape);
setVisibility(option ? option : structure.options[0].name);
}
// initialise gui and read params from URL
// callbacks to do things which are triggered by controls: reset the shape,
// change the colors. Otherwise we just read stuff from gui.params.
function setColors(c) {
const nc = get_colours(c);
for( let i = 0; i < node_ms.length; i++ ) {
node_ms[i].color = new THREE.Color(nc[i]);
link_ms[i].color = new THREE.Color(nc[i]);
}
if( shape ) {
// taperedLink.set_color updates according to the link index
shape.links.map((l) => l.object.set_color(nc));
}
}
function setBackground(c) {
scene.background = new THREE.Color(c)
}
// taperedLinks have their own materials so we have to set opacity
// on them individually. And also set the base materials as they
// will get updated from it when the shape changes
function setLinkOpacity(o, primary) {
link_ms.map((lm) => lm.opacity = o);
if( shape ) {
shape.links.map((l) => {
if( (primary && l.label == 0) || (!primary && l.label !== 0) ) {
l.object.material.opacity = o
}
});
}
}
function setNodeOpacity(o) {
node_ms.map((nm) => nm.opacity = o);
}
let gui;
function changeShape() {
createShape(gui.params.shape);
}
function setVisibility(option_name) {
console.log("setVisibility", option_name);
console.log(structure.options);
const option = structure.options.filter((o) => o.name === option_name);
if( option.length ) {
node_show = option[0].nodes;
link_show = option[0].links;
} else {
console.log(`Error: option '${option_name}' not found`);
}
}
gui = new FourDGUI(
{
shapes: STRUCTURES,
changeShape: changeShape,
setColors: setColors,
setBackground: setBackground,
setNodeOpacity: setNodeOpacity,
setLinkOpacity: setLinkOpacity,
setVisibility: setVisibility,
showDocs: () => {},
}
);
// these are here to pick up colour settings from the URL params
setColors(gui.params.color);
setBackground(gui.params.background);
const dragK = 0.005;
const damping = 0.99;
const dtheta = 2 * Math.PI / 480;
const dpsi = 2 * Math.PI / 480;
let theta = 0;
let psi = 0;
let theta0 = 0;
let psi0 = 0;
let dragx0 = 0;
let dragy0 = 0;
let dragging = false;
let frame = 0;
const FRAME_MAX = 8;
let completed = false;
createShape(gui.params.shape, gui.params.option);
function animate() {
requestAnimationFrame( animate );
theta += dtheta;
psi += dpsi;
const rotations = [
rotfn[gui.params.xRotate](theta),
rotfn[gui.params.yRotate](psi)
];
shape.hyperplane = 1 / gui.params.hyperplane;
camera.position.set(0, 0, gui.params.zoom * CAMERA_K * gui.params.hyperplane);
shape.node_scale = gui.params.nodesize;
shape.link_scale = gui.params.linksize * gui.params.nodesize * 0.5;
shape.render3(rotations, node_show, link_show);
renderer.render( scene, camera );
}
animate();