import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; import './lib/chrome.js'; import { sceneColors } from './lib/theme.js'; import { bindWorld } from './lib/transit.js'; import { bindGlow } from './lib/glow.js'; import { bindCameraMemory } from './lib/cameraMemory.js'; const colors = sceneColors(); // the landing gradient shows through a transparent canvas; all shapes live in // `world`, which page transitions fly in and out const scene = new THREE.Scene(); const world = new THREE.Group(); scene.add( world ); const updateWorld = bindWorld( world ); const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 500 ); camera.position.z = 12; camera.position.x = 10; camera.position.y = 4; // debug switches to bisect driver issues: ?noaa disables MSAA, ?noshadow disables shadow maps const debugFlags = new URLSearchParams( window.location.search ); const renderer = new THREE.WebGLRenderer( { antialias: !debugFlags.has( 'noaa' ), alpha: true } ); renderer.setPixelRatio( Math.min( window.devicePixelRatio, 2 ) ); renderer.setSize( window.innerWidth, window.innerHeight ); renderer.shadowMap.enabled = !debugFlags.has( 'noshadow' ); renderer.shadowMap.type = THREE.PCFSoftShadowMap; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1; document.body.appendChild( renderer.domElement ); window.addEventListener( 'resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize( window.innerWidth, window.innerHeight ); } ); const controls = new OrbitControls( camera, renderer.domElement ); controls.target.set( -4, 0, 0 ); bindCameraMemory( camera, controls ); const updateGlow = bindGlow( camera ); // const geometry_torus = new THREE.TorusGeometry( 2.5, 2.5, 32, 128 ); // const material_torus = new THREE.MeshBasicMaterial( { color: 0x049ef4, wireframe: true, wireframeLinewidth: 1} ); // const torus = new THREE.Mesh( geometry_torus, material_torus ); // torus.rotation.x = Math.PI / 2; // torus.castShadow = true; // world.add( torus ); // const icoGeometry = new THREE.IcosahedronGeometry( 5 ); // const icoMaterial= new THREE.MeshBasicMaterial( {color: 0x049ef4, wireframe: true, wireframeLinewidth: 8 } ); // const iso = new THREE.Mesh( icoGeometry, icoMaterial ); // iso.castShadow = true; // world.add( iso ); // --------- Mekaraba ---------- // // const tetraGeometry = new THREE.TetrahedronGeometry( 2 ); // const tetraMaterial = new THREE.MeshBasicMaterial( {color: 0xffff00 } ); // const tetra = new THREE.Mesh( tetraGeometry, tetraMaterial ); // // tetra.position.y = - 0.81 // // tetra.rotation.x = Math.PI / 4 ; // tetra.rotation.z = Math.PI / 6 ; // // world.add( tetra ); // const tetraEdges = new THREE.EdgesGeometry( tetraGeometry ); // const tetraEdgesLine = new THREE.LineSegments( tetraEdges, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 3 } ) ); // // tetraEdgesLine.position.y = -0.81 // tetraEdgesLine.rotation.x = Math.PI / 4; // // tetraEdgesLine.rotation.y = Math.PI ; // // tetraEdgesLine.rotation.y = Math.PI / 6; // tetraEdgesLine.castShadow = true; // world.add( tetraEdgesLine ); // const tetraGeometry2 = new THREE.TetrahedronGeometry( 2 ); // const tetraMaterial2 = new THREE.MeshBasicMaterial( {color: 0xffff00 } ); // const tetra2 = new THREE.Mesh( tetraGeometry2, tetraMaterial2 ); // tetra2.position.y = 0.81 // tetra2.rotation.x = Math.PI / 4 ; // // tetra2.rotation.y = Math.PI / 4 ; // tetra2.rotation.z = Math.PI / 4 ; // // world.add( tetra2 ); // const tetraEdges2 = new THREE.EdgesGeometry( tetraGeometry2 ); // const tetraEdgesLine2 = new THREE.LineSegments( tetraEdges2, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 3 } ) ); // tetraEdgesLine2.castShadow = true; // tetraEdgesLine2.position.y = 0.81 // tetraEdgesLine2.rotation.x = Math.PI / 4 ; // // tetraEdgesLine2.rotation.y = Math.PI / 4 ; // tetraEdgesLine2.rotation.z = Math.PI / 4 ; // world.add( tetraEdgesLine2 ); // ---------- David ------------ // // const tetraGeometry = new THREE.ConeGeometry( 2, 3, 3 ); // const tetraMaterial = new THREE.MeshBasicMaterial( {color: 0xffff00 } ); // const tetra = new THREE.Mesh( tetraGeometry, tetraMaterial ); // tetra.rotation.y = Math.PI / 6 ; // tetra.position.y = 1 / 2 // world.add( tetra ); // const tetraEdges = new THREE.EdgesGeometry( tetraGeometry ); // const tetraEdgesLine = new THREE.LineSegments( tetraEdges, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 15 } ) ); // tetraEdgesLine.rotation.y = Math.PI / 6 ; // tetraEdgesLine.position.y = 1 / 2 // tetraEdgesLine.castShadow = true; // world.add( tetraEdgesLine ); // const tetraGeometry2 = new THREE.ConeGeometry( 2, 3, 3 ); // const tetraMaterial2 = new THREE.MeshBasicMaterial( {color: 0xffff00 } ); // const tetra2 = new THREE.Mesh( tetraGeometry2, tetraMaterial2 ); // tetra2.rotation.y = Math.PI + Math.PI / 6 ; // tetra2.rotation.z = Math.PI; // tetra2.position.y = - 1 / 2 // world.add( tetra2 ); // const tetraEdges2 = new THREE.EdgesGeometry( tetraGeometry2 ); // const tetraEdgesLine2 = new THREE.LineSegments( tetraEdges2, new THREE.LineBasicMaterial( { color: 0x000000, linewidth: 15 } ) ); // tetraEdgesLine2.castShadow = true; // tetraEdgesLine2.rotation.y = Math.PI + Math.PI / 6 ; // tetraEdgesLine2.rotation.z = Math.PI; // tetraEdgesLine2.position.y = - 1 / 2 // world.add( tetraEdgesLine2 ); // ---------- Flower of life ------------ // let rotatePoint2D = (cx, cy, angle, px, py) => { angle = - angle * (Math.PI / 180) let s = Math.sin(angle); let c = Math.cos(angle); // translate point back to origin: px -= cx; py -= cy; // rotatePoint3D point let xnew = px * c + py * s; let ynew = px * s - py * c; // translate point back: px = xnew + cx; py = ynew + cy; return [px, py]; } function rotatePoint3D(px, py, pz, pitch, roll, yaw) { pitch = - pitch * (Math.PI / 180) roll = - roll * (Math.PI / 180) yaw = - yaw * (Math.PI / 180) var cosa = Math.cos(yaw); var sina = Math.sin(yaw); var cosb = Math.cos(pitch); var sinb = Math.sin(pitch); var cosc = Math.cos(roll); var sinc = Math.sin(roll); var Axx = cosa*cosb; var Axy = cosa*sinb*sinc - sina*cosc; var Axz = cosa*sinb*cosc + sina*sinc; var Ayx = sina*cosb; var Ayy = sina*sinb*sinc + cosa*cosc; var Ayz = sina*sinb*cosc - cosa*sinc; var Azx = -sinb; var Azy = cosb*sinc; var Azz = cosb*cosc; let x = Axx*px + Axy*py + Axz*pz; let y = Ayx*px + Ayy*py + Ayz*pz; let z = Azx*px + Azy*py + Azz*pz; return [x, y, z] } // WebGL draws lines at 1px regardless of linewidth, so the shape edges are built // from real cylinder tubes instead - same 0.03 thickness as the circle tori function makeEdgeTubes( geometry, color, tubeRadius = 0.03 ) { const edges = new THREE.EdgesGeometry( geometry ); const positions = edges.attributes.position; const group = new THREE.Group(); const material = new THREE.MeshBasicMaterial( { color } ); const up = new THREE.Vector3( 0, 1, 0 ); const start = new THREE.Vector3(); const end = new THREE.Vector3(); for ( let i = 0; i < positions.count; i += 2 ) { start.fromBufferAttribute( positions, i ); end.fromBufferAttribute( positions, i + 1 ); const direction = new THREE.Vector3().subVectors( end, start ); const length = direction.length(); const cylinder = new THREE.CylinderGeometry( tubeRadius, tubeRadius, length, 12 ); cylinder.translate( 0, length / 2, 0 ); const tube = new THREE.Mesh( cylinder, material ); tube.position.copy( start ); tube.quaternion.setFromUnitVectors( up, direction.normalize() ); tube.castShadow = true; group.add( tube ); } return group; } const params = { xangle: 45, yangle: 0, zangle: 54.72972973, radius: 1, tube: 0.015, }; // const params = { // xangle: 0, // yangle: 0, // zangle: 0, // size: 5, // radius: 1 // }; let circleCircum, cube, cubeLines; let gui; let solids = []; let renderFlower = (radius = 1) => { // tear down the previous render's shapes and GUI so re-rendering never stacks duplicates if ( gui ) gui.destroy(); for ( let obj of solids ) world.remove( obj ); solids = []; const d = 4 * 0.612 * radius; // circles on demand: a translucent sphere and ring on every vertex of a // solid, toggled per element from its folder const circleSphereGeometry = new THREE.SphereGeometry( radius, 48, 48 ); const circleSphereMaterial = new THREE.MeshStandardMaterial( { color: colors.fills.sphere, metalness: 0.7, roughness: 0.3, opacity: 0.8, transparent: true } ); const ringGeometry = new THREE.TorusGeometry( radius, params['tube'], 48, 64 ); const ringMaterial = new THREE.MeshBasicMaterial( { color: colors.line, opacity: 0.35, transparent: true } ); const occupiedSpots = new Set(); // one circle per spot: if any group already placed a circle here (fire and // earth share all eight cube corners), later ones are skipped const addCircleMeshes = ( point, group ) => { const spot = point.toArray().map( ( n ) => n.toFixed( 2 ) ).join( ',' ); if ( occupiedSpots.has( spot ) ) return; occupiedSpots.add( spot ); const sphere = new THREE.Mesh( circleSphereGeometry, circleSphereMaterial ); sphere.position.copy( point ); group.add( sphere ); const ring = new THREE.Mesh( ringGeometry, ringMaterial ); ring.rotation.y = Math.PI / 2; ring.position.copy( point ); ring.castShadow = true; group.add( ring ); }; // circles on a solid's vertices const makeVertexCircles = ( geometry, rotation ) => { const group = new THREE.Group(); const seen = new Set(); const positions = geometry.attributes.position; const vertex = new THREE.Vector3(); for ( let i = 0; i < positions.count; i++ ) { vertex.fromBufferAttribute( positions, i ); const key = vertex.toArray().map( ( n ) => n.toFixed( 3 ) ).join( ',' ); if ( seen.has( key ) ) continue; seen.add( key ); const point = vertex.clone().applyMatrix4( rotation ); addCircleMeshes( point, group ); } return group; }; const addCircles = ( group, visible ) => { group.visible = visible; world.add( group ); solids.push( group ); return group; }; // the exact rotation the circle grid uses, as a matrix - solids aligned with // this coincide with the dots instead of approximating them with tuned angles const gridRotation = new THREE.Matrix4().makeBasis( new THREE.Vector3( ...rotatePoint3D( 1, 0, 0, params['yangle'], params['xangle'], params['zangle'] ) ), new THREE.Vector3( ...rotatePoint3D( 0, 1, 0, params['yangle'], params['xangle'], params['zangle'] ) ), new THREE.Vector3( ...rotatePoint3D( 0, 0, 1, params['yangle'], params['xangle'], params['zangle'] ) ) ); // earth - width 2d puts every corner exactly on a corner dot const boxGeometry = new THREE.BoxGeometry( 2 * d, 2 * d, 2 * d ); const boxMaterial = new THREE.MeshStandardMaterial( { color: colors.fills.earth, metalness: 0.7, roughness: 0.3, opacity: 0.8, transparent: true, // wireframe: true, } ); cube = new THREE.Mesh( boxGeometry, boxMaterial ); cube.setRotationFromMatrix( gridRotation ); cubeLines = makeEdgeTubes( boxGeometry, colors.line, params['tube'] ); cubeLines.setRotationFromMatrix( gridRotation ); cubeLines.castShadow = true; world.add( cubeLines ); world.add( cube ); const cubeCircles = addCircles( makeVertexCircles( boxGeometry, gridRotation ), true ); // fire - radius sqrt(3)d puts every vertex exactly on a corner dot const tetraGeometry = new THREE.TetrahedronGeometry( Math.sqrt( 3 ) * d ) const tetraMaterial = new THREE.MeshStandardMaterial( { color: colors.fills.fire, metalness: 0.7, roughness: 0.3, opacity: 0.8, transparent: true } ); let tetra = new THREE.Mesh( tetraGeometry, tetraMaterial ); let tetraLines = makeEdgeTubes( tetraGeometry, colors.line, params['tube'] ); tetraLines.setRotationFromMatrix( gridRotation ); tetra.setRotationFromMatrix( gridRotation ); tetraLines.castShadow = true; // second tetrahedron of the star tetrahedron (Merkaba): the same shape // rotated a quarter turn so it interlocks with the first const tetraGeometry2 = new THREE.TetrahedronGeometry( Math.sqrt( 3 ) * d ); tetraGeometry2.rotateZ( Math.PI / 2 ); let tetra2 = new THREE.Mesh( tetraGeometry2, tetraMaterial ); let tetraLines2 = makeEdgeTubes( tetraGeometry2, colors.line, params['tube'] ); tetra2.rotation.copy( tetra.rotation ); tetraLines2.rotation.copy( tetraLines.rotation ); // both tetrahedra form one element, controlled together const fireFill = new THREE.Group(); fireFill.add( tetra, tetra2 ); const fireFrame = new THREE.Group(); fireFrame.add( tetraLines, tetraLines2 ); world.add( fireFrame ); world.add( fireFill ); const fireCircles = new THREE.Group(); fireCircles.add( makeVertexCircles( tetraGeometry, gridRotation ) ); fireCircles.add( makeVertexCircles( tetraGeometry2, gridRotation ) ); addCircles( fireCircles, true ); // radius 2d puts the projected corners exactly on the outer circle centres const octaGeometry = new THREE.OctahedronGeometry( 2 * d ) const octaMaterial = new THREE.MeshStandardMaterial( { color: colors.fills.air, metalness: 0.7, roughness: 0.3, opacity: 0.8, transparent: true } ); let octa = new THREE.Mesh( octaGeometry, octaMaterial ); let octaLines = makeEdgeTubes( octaGeometry, colors.line, params['tube'] ); octaLines.setRotationFromMatrix( gridRotation ); octa.setRotationFromMatrix( gridRotation ); octaLines.castShadow = true; world.add( octaLines ); world.add( octa ); const octaCircles = addCircles( makeVertexCircles( octaGeometry, gridRotation ), true ); // radius 1.663d puts the projected corners on the outer circle centres const icosaGeometry = new THREE.IcosahedronGeometry( 1.663 * d ) const icosaMaterial = new THREE.MeshStandardMaterial( { color: colors.fills.water, metalness: 0.7, roughness: 0.3, opacity: 0.8, transparent: true } ); let icosa = new THREE.Mesh( icosaGeometry, icosaMaterial ); let icosaLines = makeEdgeTubes( icosaGeometry, colors.line, params['tube'] ); // a grid-aligned icosahedron projects its corners 22.2 degrees off the dot // hexagon, so it gets an extra twist about the view diagonal to meet the dots const PHI = ( 1 + Math.sqrt( 5 ) ) / 2; const icosaTwist = Math.PI / 6 - Math.atan( ( PHI + 2 ) / ( Math.sqrt( 3 ) * PHI ) ); const icosaRotation = new THREE.Matrix4().multiplyMatrices( gridRotation, new THREE.Matrix4().makeRotationAxis( new THREE.Vector3( 1, 1, 1 ).normalize(), icosaTwist ) ); icosaLines.setRotationFromMatrix( icosaRotation ); icosa.setRotationFromMatrix( icosaRotation ); icosaLines.castShadow = true; world.add( icosaLines ); world.add( icosa ); const icosaCircles = addCircles( makeVertexCircles( icosaGeometry, icosaRotation ), false ); // 2.29 * 1.003: tuned against the circle pattern const dodecaGeometry = new THREE.DodecahedronGeometry( 2.297 * radius ) const dodecaMaterial = new THREE.MeshStandardMaterial( { color: colors.fills.aether, metalness: 0.7, roughness: 0.3, opacity: 0.8, transparent: true } ); let dodeca = new THREE.Mesh( dodecaGeometry, dodecaMaterial ); let dodecaLines = makeEdgeTubes( dodecaGeometry, colors.line, params['tube'] ); // aether: a dodecahedron vertex sits on the cube diagonal, so the plain grid // rotation points a vertex straight at the light and shares the cube's frame, // with a tuned x-angle on top dodecaLines.setRotationFromMatrix( gridRotation ); dodeca.setRotationFromMatrix( gridRotation ); dodecaLines.rotation.x = 52.25 * (Math.PI / 180); dodeca.rotation.x = 52.25 * (Math.PI / 180); dodecaLines.castShadow = true; world.add( dodecaLines ); world.add( dodeca ); // the in-between circles of the 2D pattern: the six edge midpoints that // form the inner ring const innerCircles = new THREE.Group(); const innerPoints = []; for ( let s of [ -1, 1 ] ) { innerPoints.push( [ s * d, s * d, 0 ], [ s * d, 0, s * d ], [ 0, s * d, s * d ] ); } for ( const p of innerPoints ) { addCircleMeshes( new THREE.Vector3( ...p ).applyMatrix4( gridRotation ), innerCircles ); } addCircles( innerCircles, true ); // Big circle: visible as a solid tube, same thickness as the shape edges let circleGeometry = new THREE.TorusGeometry( 5 * radius, params['tube'], 64, 64); let circleMaterial = new THREE.MeshBasicMaterial( { color: colors.line } ); circleCircum = new THREE.Mesh( circleGeometry, circleMaterial ); circleCircum.rotation.y = Math.PI / 2; circleCircum.castShadow = true; world.add( circleCircum ); solids.push( cube, cubeLines, fireFill, fireFrame, octa, octaLines, icosa, icosaLines, dodeca, dodecaLines, circleCircum ); gui = new GUI(); let guiDict = {}; let fire = gui.addFolder('Fire'); guiDict['fire'] = {'folder': fire, 'object': fireFill, 'objectLines': fireFrame, 'circles': fireCircles}; let air = gui.addFolder('Air'); guiDict['air'] = {'folder': air, 'object': octa, 'objectLines': octaLines, 'circles': octaCircles}; let earth = gui.addFolder('Earth'); guiDict['earth'] = {'folder': earth, 'object': cube, 'objectLines': cubeLines, 'circles': cubeCircles}; let water = gui.addFolder('Water'); guiDict['water'] = {'folder': water, 'object': icosa, 'objectLines': icosaLines, 'circles': icosaCircles}; let aether = gui.addFolder('Aether') guiDict['aether'] = {'folder': aether, 'object': dodeca, 'objectLines': dodecaLines}; let folderStandard = { show: true, fill: true, frame: true, scale: 1, } for (let fld of Object.keys(guiDict)) { guiDict[fld]['folder'].close() folderStandard.show = true; guiDict[fld]['folder'].add( folderStandard, 'scale', 0, 2 ).step(0.001).onChange( (value) => { guiDict[fld]['object'].scale.x = value guiDict[fld]['object'].scale.y = value guiDict[fld]['object'].scale.z = value guiDict[fld]['objectLines'].scale.x = value guiDict[fld]['objectLines'].scale.y = value guiDict[fld]['objectLines'].scale.z = value if (guiDict[fld]['circles']) guiDict[fld]['circles'].scale.setScalar(value); }); guiDict[fld]['folder'].add( folderStandard, 'show', true).onChange((value) => { if (value) { guiDict[fld]['object'].visible = true; guiDict[fld]['objectLines'].visible = true; } else { guiDict[fld]['object'].visible = false; guiDict[fld]['objectLines'].visible = false; } }); guiDict[fld]['folder'].add( folderStandard, 'fill', true).onChange((value) => { if (value) { guiDict[fld]['object'].visible = true; } else { guiDict[fld]['object'].visible = false; } }); guiDict[fld]['folder'].add( folderStandard, 'frame', true).onChange((value) => { if (value) { guiDict[fld]['objectLines'].visible = true; } else { guiDict[fld]['objectLines'].visible = false; } }); if (guiDict[fld]['circles']) { guiDict[fld]['folder'].add( { circles: guiDict[fld]['circles'].visible }, 'circles' ).onChange( (value) => { guiDict[fld]['circles'].visible = value; }); } } gui.add( { innerCircles: true }, 'innerCircles' ).onChange( (value) => { innerCircles.visible = value; }); // rebuilding destroys and recreates the GUI, which would kill the very // slider being dragged - so the scene only rebuilds when the drag ends gui.add( params, 'radius', 0.5, 1.5).onFinishChange( (value) => { renderFlower (value) }); gui.add( params, 'tube', 0.01, 0.1 ).step( 0.005 ).onFinishChange( () => { renderFlower (params['radius']) }); // ?only= hides the other solids, for // checking a single shape against the reference const only = debugFlags.get( 'only' ); if ( only && guiDict[ only ] ) { for ( let fld of Object.keys( guiDict ) ) { if ( fld === only || ! guiDict[ fld ][ 'object' ] ) continue; guiDict[ fld ][ 'object' ].visible = false; guiDict[ fld ][ 'objectLines' ].visible = false; } } } renderFlower() // dark mode gets a soft warm ambient so unlit faces keep their colour if ( colors.dark ) scene.add( new THREE.AmbientLight( colors.light, 0.9 ) ); // legacy lighting was removed in three r155: intensities need the old value times pi const spotLight = new THREE.DirectionalLight( colors.light, 1.5 * Math.PI ); spotLight.position.set( 10, 0, 0 ); spotLight.castShadow = true; spotLight.shadow.mapSize.width = 2048; spotLight.shadow.mapSize.height = 2048; spotLight.shadow.camera.far = 21; spotLight.shadow.camera.left = -10; spotLight.shadow.camera.right = 10; spotLight.shadow.camera.top = 10; spotLight.shadow.camera.bottom = -10; // spotLight.shadow.camera.fov = 35; scene.add( spotLight ); // the wall itself is invisible: only the shadow "projection" shows, in light // orange, floating on the gradient const planeGeometry = new THREE.PlaneGeometry( 20, 20, 10, 10 ); const planeMaterial = new THREE.ShadowMaterial( { color: colors.shadow, opacity: 0.9 } ) const plane = new THREE.Mesh( planeGeometry, planeMaterial ); plane.rotation.y = Math.PI / 2; plane.position.x = -10 plane.receiveShadow = true; scene.add( plane ); //Create a helper for the shadow camera (optional) // const helper = new THREE.CameraHelper( spotLight.shadow.camera ); // world.add( helper ); // const axesHelper = new THREE.AxesHelper( 40 ); // world.add( axesHelper ); // var axis = new THREE.Vector3(0, 1, 0); function animate () { requestAnimationFrame( animate ); const time = performance.now() / 1000; // for (let sphere of spheres) { // sphere.position.x = Math.cos( time ) * sphere.start_x; // sphere.position.z = - Math.sin( time ) * sphere.start_z; // } // for (let sphereLine of spheresLines) { // sphereLine.position.x = Math.cos( time ) * sphereLine.start_x; // sphereLine.position.z = - Math.sin( time ) * sphereLine.start_z; // } updateWorld(); updateGlow(); controls.update(); renderer.render( scene, camera ); }; animate();