diff --git a/src/components/scenes/AboutRiverScene.tsx b/src/components/scenes/AboutRiverScene.tsx index 89a3d65..7284898 100644 --- a/src/components/scenes/AboutRiverScene.tsx +++ b/src/components/scenes/AboutRiverScene.tsx @@ -12,15 +12,10 @@ import { } from './scenePrimitives'; import styles from './Scenes.module.css'; -// Sharp 90-degree orthogonal isometric river path: -// Enters from bottom-left along the isometric Z axis, -// takes a sharp 90-degree orthogonal step across the isometric X axis at the center, -// and turns 90 degrees to continue along the isometric Z axis towards the top-right. +// Straight isometric river running between the two platform groups. const riverPath = [ - new THREE.Vector2(-1.3, -16), - new THREE.Vector2(-1.3, 0), - new THREE.Vector2(1.3, 0), - new THREE.Vector2(1.3, 16), + new THREE.Vector2(0, -20), + new THREE.Vector2(0, 20) ]; const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => { @@ -111,19 +106,96 @@ const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => { addPlate(secondaryPlates, x, z, width, depth, 0.5, theme === 'dark' ? 0x0b2f5f : 0xe6f0fe), ); - const riverGeometry = new THREE.ShapeGeometry(createRibbonShape(riverPath, 1.8)); + const riverGeometry = new THREE.ShapeGeometry(createRibbonShape(riverPath, 4.5)); + const riverEndFade = 0.16; + const fadeTextureSize = 256; + const fadeTextureData = new Uint8Array(fadeTextureSize * 4); + + for (let index = 0; index < fadeTextureSize; index += 1) { + const progress = index / (fadeTextureSize - 1); + const distanceFromEnd = Math.min(progress, 1 - progress); + const opacity = Math.round(255 * Math.min(1, distanceFromEnd / riverEndFade)); + const offset = index * 4; + + fadeTextureData[offset] = opacity; + fadeTextureData[offset + 1] = opacity; + fadeTextureData[offset + 2] = opacity; + fadeTextureData[offset + 3] = 255; + } + + const riverFadeTexture = new THREE.DataTexture( + fadeTextureData, + 1, + fadeTextureSize, + THREE.RGBAFormat, + ); + riverFadeTexture.minFilter = THREE.LinearFilter; + riverFadeTexture.magFilter = THREE.LinearFilter; + riverFadeTexture.needsUpdate = true; + + riverGeometry.computeBoundingBox(); + const riverBounds = riverGeometry.boundingBox; + const riverMinY = riverBounds?.min.y ?? -16; + const riverLength = Math.max((riverBounds?.max.y ?? 16) - riverMinY, 1); + const riverPositions = riverGeometry.getAttribute('position'); + const riverUvs = riverGeometry.getAttribute('uv'); + + for (let index = 0; index < riverPositions.count; index += 1) { + const pathProgress = (riverPositions.getY(index) - riverMinY) / riverLength; + riverUvs.setXY(index, 0.5, pathProgress); + } + riverUvs.needsUpdate = true; + const waterMaterial = new THREE.MeshStandardMaterial({ color: palette.water, emissive: palette.water, emissiveIntensity: theme === 'dark' ? 0.28 : 0.1, roughness: 0.52, + alphaMap: riverFadeTexture, + transparent: true, }); const river = new THREE.Mesh(riverGeometry, waterMaterial); river.rotation.x = -Math.PI / 2; river.position.y = -0.23; world.add(river); - const riverEdge = new THREE.LineSegments(new THREE.EdgesGeometry(riverGeometry), edgeMaterial); + const riverEdgeMaterial = new THREE.ShaderMaterial({ + transparent: true, + depthWrite: false, + uniforms: { + edgeColor: { value: new THREE.Color(palette.edge) }, + edgeOpacity: { value: theme === 'dark' ? 0.48 : 0.56 }, + fadeFraction: { value: riverEndFade }, + pathMinY: { value: riverMinY }, + pathLength: { value: riverLength }, + }, + vertexShader: ` + uniform float pathMinY; + uniform float pathLength; + varying float pathProgress; + + void main() { + pathProgress = clamp((position.y - pathMinY) / pathLength, 0.0, 1.0); + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + uniform vec3 edgeColor; + uniform float edgeOpacity; + uniform float fadeFraction; + varying float pathProgress; + + void main() { + float startFade = smoothstep(0.0, fadeFraction, pathProgress); + float endFade = smoothstep(0.0, fadeFraction, 1.0 - pathProgress); + gl_FragColor = vec4(edgeColor, edgeOpacity * startFade * endFade); + } + `, + }); + const riverEdge = new THREE.LineSegments( + new THREE.EdgesGeometry(riverGeometry), + riverEdgeMaterial, + ); riverEdge.rotation.copy(river.rotation); riverEdge.position.y = -0.21; world.add(riverEdge); @@ -133,7 +205,8 @@ const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => { const direction = end.clone().sub(start); const length = direction.length(); direction.normalize(); - for (let distance = 1.3; distance < length - 0.7; distance += 1.35) { + const fadeDistance = length * riverEndFade; + for (let distance = fadeDistance; distance < length - fadeDistance; distance += 1.35) { const at = start.clone().addScaledVector(direction, distance); const seam = new THREE.Mesh( new THREE.PlaneGeometry(1.72, 0.016),