138 lines
5.0 KiB
JavaScript
138 lines
5.0 KiB
JavaScript
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' );
|