feat(about): widen and fade the river

This commit is contained in:
2026-09-06 22:41:16 +03:30
parent 28b60b9b4c
commit e9384f321a

View File

@@ -12,15 +12,10 @@ import {
} from './scenePrimitives'; } from './scenePrimitives';
import styles from './Scenes.module.css'; import styles from './Scenes.module.css';
// Sharp 90-degree orthogonal isometric river path: // Straight isometric river running between the two platform groups.
// 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.
const riverPath = [ const riverPath = [
new THREE.Vector2(-1.3, -16), new THREE.Vector2(0, -20),
new THREE.Vector2(-1.3, 0), new THREE.Vector2(0, 20)
new THREE.Vector2(1.3, 0),
new THREE.Vector2(1.3, 16),
]; ];
const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => { 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), 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({ const waterMaterial = new THREE.MeshStandardMaterial({
color: palette.water, color: palette.water,
emissive: palette.water, emissive: palette.water,
emissiveIntensity: theme === 'dark' ? 0.28 : 0.1, emissiveIntensity: theme === 'dark' ? 0.28 : 0.1,
roughness: 0.52, roughness: 0.52,
alphaMap: riverFadeTexture,
transparent: true,
}); });
const river = new THREE.Mesh(riverGeometry, waterMaterial); const river = new THREE.Mesh(riverGeometry, waterMaterial);
river.rotation.x = -Math.PI / 2; river.rotation.x = -Math.PI / 2;
river.position.y = -0.23; river.position.y = -0.23;
world.add(river); 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.rotation.copy(river.rotation);
riverEdge.position.y = -0.21; riverEdge.position.y = -0.21;
world.add(riverEdge); world.add(riverEdge);
@@ -133,7 +205,8 @@ const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => {
const direction = end.clone().sub(start); const direction = end.clone().sub(start);
const length = direction.length(); const length = direction.length();
direction.normalize(); 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 at = start.clone().addScaledVector(direction, distance);
const seam = new THREE.Mesh( const seam = new THREE.Mesh(
new THREE.PlaneGeometry(1.72, 0.016), new THREE.PlaneGeometry(1.72, 0.016),