diff --git a/src/css/styles.scss b/src/css/styles.scss index ee49b27..9cd2e01 100644 --- a/src/css/styles.scss +++ b/src/css/styles.scss @@ -198,6 +198,14 @@ html[data-theme='light'] .lil-gui { overflow-y: auto; } + // centred instead of right-anchored, so the full width stays on screen + .lil-gui.root.autoPlace { + margin: 0; + right: auto; + left: 50%; + transform: translateX(-50%); + } + .page-nav { gap: 8px; diff --git a/src/flower.js b/src/flower.js index 1c978f3..59dde1a 100644 --- a/src/flower.js +++ b/src/flower.js @@ -18,7 +18,7 @@ const colors = sceneColors(); const scene = new THREE.Scene(); const world = new THREE.Group(); scene.add( world ); -const updateWorld = bindWorld( world, 1.5 ); +const updateWorld = bindWorld( world, 1.2 ); const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 500 ); @@ -206,7 +206,7 @@ const params = { xangle: 45, yangle: 0, zangle: 54.72972973, - size: 3, + size: 4, radius: 1 }; @@ -274,17 +274,16 @@ let renderFlower = (size = 3, radius = 1) => { } } - // Big circle - let circleGeometry = new THREE.TorusGeometry( 5 * radius, 0.02, 64, 64); - let circleMaterial = new THREE.MeshBasicMaterial( { - color: 0x000000, - opacity: 0, - transparent: true - } ); - circleCircum = new THREE.Mesh( circleGeometry, circleMaterial ); - circleCircum.rotation.y = Math.PI / 2; - circleCircum.castShadow = true; - world.add( circleCircum ); + // outer boundary: one circle hugging the grid, a second just outside it + for ( const outerRadius of [ 4, 4.2 ] ) { + const circleGeometry = new THREE.TorusGeometry( outerRadius * radius, 0.02, 64, 64 ); + const 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 ); + spheresLines.push( circleCircum ); + } const gui = new GUI(); @@ -323,7 +322,6 @@ let renderFlower = (size = 3, radius = 1) => { for (let sphereLine of spheresLines) { world.remove(sphereLine) } - world.remove(circleCircum) renderFlower (value, params['radius']) }); gui.add( params, 'radius', 0.5, 3).onChange( (value) => { @@ -333,13 +331,12 @@ let renderFlower = (size = 3, radius = 1) => { for (let sphereLine of spheresLines) { world.remove(sphereLine) } - world.remove(circleCircum) renderFlower (params['size'], value) }); gui.close(); } -renderFlower() +renderFlower(params['size'], params['radius']) // 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 ) ); diff --git a/src/matrix.js b/src/matrix.js index fdb8a2b..0481295 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -25,10 +25,12 @@ 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; @@ -93,11 +95,21 @@ function renderMatrix() { } ); const spheres = new THREE.Group(); const sphereGeometry = new THREE.SphereGeometry( sphereRadius, 48, 48 ); - // no castShadow: the projection stays an outline of edges and spokes + 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 ); @@ -108,6 +120,11 @@ 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(); diff --git a/src/seed.js b/src/seed.js index 0575282..0d9a72b 100644 --- a/src/seed.js +++ b/src/seed.js @@ -1,3 +1,5 @@ +// Seed of Life: exactly the Flower of Life code, at grid size 2 with smaller +// outer rings and a larger base scale so it fills the frame the same way. import * as THREE from 'three'; @@ -18,7 +20,7 @@ const colors = sceneColors(); const scene = new THREE.Scene(); const world = new THREE.Group(); scene.add( world ); -const updateWorld = bindWorld( world, 3 ); +const updateWorld = bindWorld( world, 2.4 ); const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 500 ); @@ -150,7 +152,7 @@ const updateGlow = bindGlow( camera ); // circleLine.castShadow = true; // world.add( circleLine ); -let rotatePoint = (cx, cy, angle, px, py) => { +let rotatePoint2D = (cx, cy, angle, px, py) => { angle = - angle * (Math.PI / 180) let s = Math.sin(angle); let c = Math.cos(angle); @@ -159,7 +161,7 @@ let rotatePoint = (cx, cy, angle, px, py) => { px -= cx; py -= cy; - // rotate point + // rotatePoint3D point let xnew = px * c + py * s; let ynew = px * s - py * c; @@ -169,7 +171,7 @@ let rotatePoint = (cx, cy, angle, px, py) => { return [px, py]; } -function rotate(px, py, pz, pitch, roll, yaw) { +function rotatePoint3D(px, py, pz, pitch, roll, yaw) { pitch = - pitch * (Math.PI / 180) roll = - roll * (Math.PI / 180) yaw = - yaw * (Math.PI / 180) @@ -202,12 +204,11 @@ function rotate(px, py, pz, pitch, roll, yaw) { return [x, y, z] } - const params = { xangle: 45, yangle: 0, zangle: 54.72972973, - size: 3, + size: 2, radius: 1 }; @@ -228,15 +229,15 @@ let renderFlower = (size = 3, radius = 1) => { let xtemp2, ytemp2, ztemp2; - // [xtemp, ytemp, ztemp] = rotate(x, y, z, 0, 45, 0) + // [xtemp, ytemp, ztemp] = rotatePoint3D(x, y, z, 0, 45, 0) // console.log(xtemp, ytemp, ztemp); - [xtemp2, ytemp2, ztemp2] = rotate(x, y, z, params['yangle'], params['xangle'], params['zangle']) + [xtemp2, ytemp2, ztemp2] = rotatePoint3D(x, y, z, params['yangle'], params['xangle'], params['zangle']) let sphereGeometry = new THREE.SphereGeometry( radius, 64, 64 ); - let sphereMaterial = new THREE.MeshStandardMaterial( { - color: 0x0063e4, + let sphereMaterial = new THREE.MeshStandardMaterial( { + color: colors.fills.sphere, metalness: 0.7, roughness: 0.3, opacity: 0.8, @@ -249,8 +250,8 @@ let renderFlower = (size = 3, radius = 1) => { world.add( sphere ); let circleGeometry = new THREE.TorusGeometry( radius, 0.02, 64, 64 ); - let circleMaterial = new THREE.MeshBasicMaterial( { - color: 0x000000, + let circleMaterial = new THREE.MeshBasicMaterial( { + color: colors.line, opacity: 0.1, transparent: true } ); @@ -272,75 +273,73 @@ let renderFlower = (size = 3, radius = 1) => { spheres.push(sphere) spheresLines.push(circleCircum) } - z_count += 1; } } - // Big circle - let circleGeometry = new THREE.TorusGeometry( 5 * radius, 0.02, 64, 64); - let circleMaterial = new THREE.MeshBasicMaterial( { - color: 0x000000, - opacity: 0, - transparent: true - } ); - circleCircum = new THREE.Mesh( circleGeometry, circleMaterial ); - circleCircum.rotation.y = Math.PI / 2; - circleCircum.castShadow = true; - world.add( circleCircum ); -} - -// renderFlower() - -// ---------- Seed of Life ------------ // -let z_count = 0 -for (let x_angle of Array.from(Array(4), (_, i) => 360 / 4 * i)) { - for (let z_angle of Array.from(Array(6), (_, i) => 360 / 6 * i)) { - for (let i of Array.from(Array(2), (_, i) => i* 0.5)) { - if (z_count > 0 && i === 0) { - continue; - } - let x, y, z; - - z = rotatePoint(0, 0, z_angle, i, 0)[1] - y = rotatePoint(0, 0, z_angle, i, 0)[0] - x = 0; - - [x, y ,z] = rotate(x, y, z, 0, x_angle, 0) - - console.log(x_angle, z_angle, x, y, z) - - let sphereGeometry = new THREE.SphereGeometry( 0.5, 64, 64 ); - let sphereMaterial = new THREE.MeshBasicMaterial( { - color: colors.fills.sphere, - metalness: 1, - roughness: 1, - opacity: 0.5, - transparent: true - } ); - let sphere = new THREE.Mesh( sphereGeometry, sphereMaterial ); - sphere.position.x = x; - sphere.position.y = y; - sphere.position.z = z; - world.add( sphere ); - - let circleGeometry = new THREE.TorusGeometry( 0.5, 0.01, 64, 64); - let circleMaterial = new THREE.MeshBasicMaterial( { - color: colors.line, - opacity: 0.1, - transparent: true - } ); - let circleCircum = new THREE.Mesh( circleGeometry, circleMaterial ); - circleCircum.castShadow = true; - circleCircum.rotation.y = Math.PI / 2; - circleCircum.position.x = x; - circleCircum.position.y = y; - circleCircum.position.z = z; - world.add( circleCircum ); - } - z_count += 1; + // outer boundary: one circle hugging the grid, a second just outside it + for ( const outerRadius of [ 2, 2.1 ] ) { + const circleGeometry = new THREE.TorusGeometry( outerRadius * radius, 0.02, 64, 64 ); + const 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 ); + spheresLines.push( circleCircum ); } + + + const gui = new GUI(); + if ( window.innerWidth < 700 ) gui.close(); + gui.add( params, 'xangle', 0, 360 ).step(0.1).onChange( (value) => { + for (let sphere of spheres) { + [sphere.position.x, sphere.position.y, sphere.position.z] = rotatePoint3D(sphere.start_x, sphere.start_y, sphere.start_z, params['yangle'], value, params['zangle']) + } + + for (let sphereLine of spheresLines) { + [sphereLine.position.x, sphereLine.position.y, sphereLine.position.z] = rotatePoint3D(sphereLine.start_x, sphereLine.start_y, sphereLine.start_z, params['yangle'], value, params['zangle']) + } + }); + gui.add( params, 'yangle', 0, 360 ).step(0.1).onChange( (value) => { + for (let sphere of spheres) { + [sphere.position.x, sphere.position.y, sphere.position.z] = rotatePoint3D(sphere.start_x, sphere.start_y, sphere.start_z, value, params['xangle'], params['zangle']) + } + + for (let sphereLine of spheresLines) { + [sphereLine.position.x, sphereLine.position.y, sphereLine.position.z] = rotatePoint3D(sphereLine.start_x, sphereLine.start_y, sphereLine.start_z, value, params['xangle'], params['zangle']) + } + }); + gui.add( params, 'zangle', 0, 360 ).step(0.1).onChange( (value) => { + for (let sphere of spheres) { + [sphere.position.x, sphere.position.y, sphere.position.z] = rotatePoint3D(sphere.start_x, sphere.start_y, sphere.start_z, params['yangle'], params['xangle'], value) + } + + for (let sphereLine of spheresLines) { + [sphereLine.position.x, sphereLine.position.y, sphereLine.position.z] = rotatePoint3D(sphereLine.start_x, sphereLine.start_y, sphereLine.start_z, params['yangle'], params['xangle'], value) + } + }); + gui.add( params, 'size', 2, 5).step(1).onChange( (value) => { + for (let sphere of spheres) { + world.remove(sphere) + } + for (let sphereLine of spheresLines) { + world.remove(sphereLine) + } + renderFlower (value, params['radius']) + }); + gui.add( params, 'radius', 0.5, 3).onChange( (value) => { + for (let sphere of spheres) { + world.remove(sphere) + } + for (let sphereLine of spheresLines) { + world.remove(sphereLine) + } + renderFlower (params['size'], value) + }); + gui.close(); } +renderFlower(params['size'], params['radius']) + // 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 ) ); @@ -360,8 +359,7 @@ spotLight.shadow.camera.bottom = -10; scene.add( spotLight ); // the wall itself is invisible: only the shadow "projection" shows, in light -// orange, floating on the gradient; front side only so the shapes stay fully -// visible when orbiting behind it +// 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 ); @@ -380,57 +378,6 @@ const axesHelper = new THREE.AxesHelper( 40 ); // const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 0.2 ); // world.add( light ); -// const gui = new GUI(); - -// gui.add( params, 'xangle', 0, 360 ).step(0.1).onChange( (value) => { -// for (let sphere of spheres) { -// [sphere.position.x, sphere.position.y, sphere.position.z] = rotate(sphere.start_x, sphere.start_y, sphere.start_z, params['yangle'], value, params['zangle']) -// } - -// for (let sphereLine of spheresLines) { -// [sphereLine.position.x, sphereLine.position.y, sphereLine.position.z] = rotate(sphereLine.start_x, sphereLine.start_y, sphereLine.start_z, params['yangle'], value, params['zangle']) -// } -// }); -// gui.add( params, 'yangle', 0, 360 ).step(0.1).onChange( (value) => { -// for (let sphere of spheres) { -// [sphere.position.x, sphere.position.y, sphere.position.z] = rotate(sphere.start_x, sphere.start_y, sphere.start_z, value, params['xangle'], params['zangle']) -// } - -// for (let sphereLine of spheresLines) { -// [sphereLine.position.x, sphereLine.position.y, sphereLine.position.z] = rotate(sphereLine.start_x, sphereLine.start_y, sphereLine.start_z, value, params['xangle'], params['zangle']) -// } -// }); -// gui.add( params, 'zangle', 0, 360 ).step(0.1).onChange( (value) => { -// for (let sphere of spheres) { -// [sphere.position.x, sphere.position.y, sphere.position.z] = rotate(sphere.start_x, sphere.start_y, sphere.start_z, params['yangle'], params['xangle'], value) -// } - -// for (let sphereLine of spheresLines) { -// [sphereLine.position.x, sphereLine.position.y, sphereLine.position.z] = rotate(sphereLine.start_x, sphereLine.start_y, sphereLine.start_z, params['yangle'], params['xangle'], value) -// } -// }); -// gui.add( params, 'size', 2, 5).step(1).onChange( (value) => { -// for (let sphere of spheres) { -// world.remove(sphere) -// } -// for (let sphereLine of spheresLines) { -// world.remove(sphereLine) -// } -// world.remove(circleCircum) -// renderFlower (value, params['radius']) -// }); -// gui.add( params, 'radius', 0.5, 3).onChange( (value) => { -// for (let sphere of spheres) { -// world.remove(sphere) -// } -// for (let sphereLine of spheresLines) { -// world.remove(sphereLine) -// } -// world.remove(circleCircum) -// renderFlower (params['size'], value) -// }); -// gui.close(); - // var axis = new THREE.Vector3(0, 1, 0); diff --git a/src/torus.js b/src/torus.js index 28ca737..3916159 100644 --- a/src/torus.js +++ b/src/torus.js @@ -14,6 +14,7 @@ const params = { strands: 12, windings: 4, surface: true, + outline: true, spin: true, }; @@ -25,6 +26,7 @@ scene.add( group ); let parts = []; let particles = []; let curves = []; +let outline; // point on the torus surface: phi around the main axis, theta around the tube const torusPoint = ( phi, theta, R, r ) => @@ -58,6 +60,19 @@ function renderTorus() { parts.push( surface ); const lineMaterial = new THREE.MeshBasicMaterial( { color: colors.line, opacity: 0.5, transparent: true } ); + + // the torus silhouette seen from the light is an annulus: a ring on the + // outer edge and one on the hole edge draw its outline in the projection + outline = new THREE.Group(); + for ( const ringRadius of [ R + r, R - r ] ) { + if ( ringRadius < 0.05 ) continue; + const ring = new THREE.Mesh( new THREE.TorusGeometry( ringRadius, 0.025, 12, 128 ), lineMaterial ); + ring.castShadow = true; + outline.add( ring ); + } + outline.visible = params.outline; + group.add( outline ); + parts.push( outline ); const particleMaterial = new THREE.MeshBasicMaterial( { color: colors.fills.aether } ); const particleGeometry = new THREE.SphereGeometry( 0.08, 16, 16 ); @@ -108,4 +123,7 @@ gui.add( params, 'windings', 1, 12 ).step( 1 ).onChange( renderTorus ); gui.add( params, 'surface' ).onChange( ( value ) => { parts[ 0 ].visible = value; } ); +gui.add( params, 'outline' ).onChange( ( value ) => { + outline.visible = value; +} ); gui.add( params, 'spin' );