import * as THREE from 'three'; import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; import { ConvexGeometry } from 'three/addons/geometries/ConvexGeometry.js'; import { createStage } from './lib/stage.js'; import { makeTube } from './lib/edgeTubes.js'; // The vector equilibrium (cuboctahedron): the only form where every vertex is // exactly as far from the centre as from its neighbours - Fuller's "zero point" // of the isotropic vector matrix. Its 12 vertices plus centre are the 13 circles // of Metatron's Cube, and 12 spheres kiss one central sphere on those points. const { scene, colors, onUpdate } = createStage(); const params = { scale: 3, edges: true, spokes: true, fill: true, spheres: false, spin: false, }; const group = new THREE.Group(); scene.add( group ); let parts = []; let sphereRings = []; function renderMatrix() { for ( const part of parts ) group.remove( part ); parts = []; sphereRings = []; const a = params.scale; // all permutations of (+-a, +-a, 0): the 12 cuboctahedron vertices const vertices = []; for ( const s1 of [ -1, 1 ] ) { for ( const s2 of [ -1, 1 ] ) { vertices.push( new THREE.Vector3( s1 * a, s2 * a, 0 ) ); vertices.push( new THREE.Vector3( s1 * a, 0, s2 * a ) ); vertices.push( new THREE.Vector3( 0, s1 * a, s2 * a ) ); } } const edgeLength = a * Math.SQRT2; // equals the distance from centre to vertex const edgeMaterial = new THREE.MeshBasicMaterial( { color: colors.line } ); const spokeMaterial = new THREE.MeshBasicMaterial( { color: colors.line, opacity: 0.35, transparent: true } ); const edges = new THREE.Group(); for ( let i = 0; i < vertices.length; i++ ) { for ( let j = i + 1; j < vertices.length; j++ ) { if ( Math.abs( vertices[ i ].distanceTo( vertices[ j ] ) - edgeLength ) < 1e-6 ) { edges.add( makeTube( vertices[ i ], vertices[ j ], edgeMaterial ) ); } } } edges.visible = params.edges; group.add( edges ); parts.push( edges ); // the 12 radial spokes are as long as the edges - that's the equilibrium const spokes = new THREE.Group(); const centre = new THREE.Vector3( 0, 0, 0 ); for ( const vertex of vertices ) { spokes.add( makeTube( centre, vertex, spokeMaterial, 0.02 ) ); } spokes.visible = params.spokes; group.add( spokes ); parts.push( spokes ); const fillMaterial = new THREE.MeshStandardMaterial( { color: colors.fills.fire, metalness: 0.7, roughness: 0.3, opacity: 0.8, transparent: true, } ); // the fill casts no shadow: only the edges and spokes project, as outlines const fill = new THREE.Mesh( new ConvexGeometry( vertices ), fillMaterial ); fill.visible = params.fill; group.add( fill ); parts.push( fill ); // 12 spheres of radius edge/2 kiss each other on the edges and all kiss the // 13th sphere in the centre: closest packing of equal spheres const sphereRadius = edgeLength / 2; const sphereMaterial = new THREE.MeshStandardMaterial( { color: colors.fills.water, metalness: 0.7, roughness: 0.3, opacity: 0.8, transparent: true, } ); const spheres = new THREE.Group(); const sphereGeometry = new THREE.SphereGeometry( sphereRadius, 48, 48 ); const sphereRingGeometry = new THREE.TorusGeometry( sphereRadius, 0.025, 16, 96 ); const sphereRingMaterial = new THREE.MeshBasicMaterial( { color: colors.line, opacity: 0.5, transparent: true } ); // the spheres themselves cast nothing; each ring draws the outline and // projects it onto the wall for ( const point of [ centre, ...vertices ] ) { const sphere = new THREE.Mesh( sphereGeometry, sphereMaterial ); sphere.position.copy( point ); spheres.add( sphere ); const ring = new THREE.Mesh( sphereRingGeometry, sphereRingMaterial ); ring.rotation.y = Math.PI / 2; ring.position.copy( point ); ring.castShadow = true; spheres.add( ring ); sphereRings.push( ring ); } spheres.visible = params.spheres; group.add( spheres ); parts.push( spheres ); } renderMatrix(); onUpdate( ( time ) => { if ( params.spin ) group.rotation.y = time * 0.15; // the outline rings counter the group's spin so they keep facing the wall for ( const ring of sphereRings ) { ring.rotation.y = Math.PI / 2 - group.rotation.y; } } ); const gui = new GUI(); if ( window.innerWidth < 700 ) gui.close(); gui.add( params, 'scale', 1, 4 ).step( 0.1 ).onChange( renderMatrix ); gui.add( params, 'edges' ).onChange( ( value ) => { parts[ 0 ].visible = value; } ); gui.add( params, 'spokes' ).onChange( ( value ) => { parts[ 1 ].visible = value; } ); gui.add( params, 'fill' ).onChange( ( value ) => { parts[ 2 ].visible = value; } ); gui.add( params, 'spheres' ).onChange( ( value ) => { parts[ 3 ].visible = value; } ); gui.add( params, 'spin' );