import * as THREE from 'three'; import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; import { createStage } from './lib/stage.js'; // The torus: the self-sustaining "energy field" form - field lines spiral around // the surface, through the centre and back around the outside. const { scene, colors, onUpdate } = createStage(); const params = { radius: 4, tube: 1.7, strands: 12, windings: 4, surface: true, spin: true, }; const group = new THREE.Group(); // face the camera the same way the circle pages do group.rotation.y = Math.PI / 2; scene.add( group ); let parts = []; let particles = []; let curves = []; // point on the torus surface: phi around the main axis, theta around the tube const torusPoint = ( phi, theta, R, r ) => new THREE.Vector3( ( R + r * Math.cos( theta ) ) * Math.cos( phi ), ( R + r * Math.cos( theta ) ) * Math.sin( phi ), r * Math.sin( theta ) ); function renderTorus() { for ( const part of parts ) group.remove( part ); parts = []; particles = []; curves = []; const R = params.radius; const r = params.tube; const surfaceGeometry = new THREE.TorusGeometry( R, r, 48, 128 ); const surfaceMaterial = new THREE.MeshStandardMaterial( { color: colors.fills.fire, metalness: 0.7, roughness: 0.3, opacity: 0.35, transparent: true, } ); // the surface casts no shadow: only the strands project, as outlines const surface = new THREE.Mesh( surfaceGeometry, surfaceMaterial ); surface.visible = params.surface; group.add( surface ); parts.push( surface ); const lineMaterial = new THREE.MeshBasicMaterial( { color: colors.line, opacity: 0.5, transparent: true } ); const particleMaterial = new THREE.MeshBasicMaterial( { color: colors.fills.aether } ); const particleGeometry = new THREE.SphereGeometry( 0.08, 16, 16 ); // each field line goes once around the main axis while winding `windings` // times around the tube, each strand offset around the ring for ( let s = 0; s < params.strands; s++ ) { const offset = ( s / params.strands ) * Math.PI * 2; const samples = []; for ( let i = 0; i <= 256; i++ ) { const t = i / 256; samples.push( torusPoint( t * Math.PI * 2 + offset, t * Math.PI * 2 * params.windings, R, r ) ); } const curve = new THREE.CatmullRomCurve3( samples, true ); const strand = new THREE.Mesh( new THREE.TubeGeometry( curve, 384, 0.025, 8, true ), lineMaterial ); strand.castShadow = true; group.add( strand ); parts.push( strand ); curves.push( curve ); const particle = new THREE.Mesh( particleGeometry, particleMaterial ); particle.castShadow = true; particle.userData.phase = s / params.strands; group.add( particle ); parts.push( particle ); particles.push( particle ); } } renderTorus(); onUpdate( ( time ) => { if ( params.spin ) group.rotation.z = time * 0.1; for ( let i = 0; i < particles.length; i++ ) { const t = ( time * 0.05 + particles[ i ].userData.phase ) % 1; particles[ i ].position.copy( curves[ i ].getPointAt( t ) ); } } ); const gui = new GUI(); gui.add( params, 'radius', 1, 5 ).step( 0.1 ).onChange( renderTorus ); gui.add( params, 'tube', 0.2, 3 ).step( 0.1 ).onChange( renderTorus ); gui.add( params, 'strands', 1, 32 ).step( 1 ).onChange( renderTorus ); gui.add( params, 'windings', 1, 12 ).step( 1 ).onChange( renderTorus ); gui.add( params, 'surface' ).onChange( ( value ) => { parts[ 0 ].visible = value; } ); gui.add( params, 'spin' );