Spaces:
Sleeping
Sleeping
| import React, { useRef, useMemo, useEffect } from 'react'; | |
| import { useFrame } from '@react-three/fiber'; | |
| import * as THREE from 'three'; | |
| // ----------------------------------------------------------------------------- | |
| // SHADERS | |
| // ----------------------------------------------------------------------------- | |
| const vertexShader = ` | |
| uniform float uTime; | |
| uniform float uScroll; | |
| uniform float uMorph; // 0.0 = Sphere, 1.0 = Triangle | |
| uniform float uExplode; // 0.0 = Normal, 1.0 = Exploded/Work Mode | |
| uniform float uShiftX; | |
| uniform float uShiftY; | |
| attribute float aRandom; | |
| attribute vec3 aOriginalPos; | |
| attribute vec3 aTrianglePos; | |
| varying float vDepth; | |
| varying float vRim; | |
| varying vec3 vPos; | |
| varying float vExplodeAlpha; | |
| // Simplex 3D Noise | |
| vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } | |
| vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } | |
| vec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); } | |
| vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; } | |
| float snoise(vec3 v) { | |
| const vec2 C = vec2(1.0/6.0, 1.0/3.0); | |
| const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); | |
| // First corner | |
| vec3 i = floor(v + dot(v, C.yyy)); | |
| vec3 x0 = v - i + dot(i, C.xxx); | |
| // Other corners | |
| vec3 g = step(x0.yzx, x0.xyz); | |
| vec3 l = 1.0 - g; | |
| vec3 i1 = min( g.xyz, l.zxy ); | |
| vec3 i2 = max( g.xyz, l.zxy ); | |
| vec3 x1 = x0 - i1 + C.xxx; | |
| vec3 x2 = x0 - i2 + C.yyy; | |
| vec3 x3 = x0 - D.yyy; | |
| // Permutations | |
| i = mod289(i); | |
| vec4 p = permute( permute( permute( | |
| i.z + vec4(0.0, i1.z, i2.z, 1.0 )) | |
| + i.y + vec4(0.0, i1.y, i2.y, 1.0 )) | |
| + i.x + vec4(0.0, i1.x, i2.x, 1.0 )); | |
| // Gradients | |
| float n_ = 0.142857142857; | |
| vec3 ns = n_ * D.wyz - D.xzx; | |
| vec4 j = p - 49.0 * floor(p * ns.z * ns.z); | |
| vec4 x_ = floor(j * ns.z); | |
| vec4 y_ = floor(j - 7.0 * x_ ); | |
| vec4 x = x_ *ns.x + ns.yyyy; | |
| vec4 y = y_ *ns.x + ns.yyyy; | |
| vec4 h = 1.0 - abs(x) - abs(y); | |
| vec4 b0 = vec4( x.xy, y.xy ); | |
| vec4 b1 = vec4( x.zw, y.zw ); | |
| vec4 s0 = floor(b0)*2.0 + 1.0; | |
| vec4 s1 = floor(b1)*2.0 + 1.0; | |
| vec4 sh = -step(h, vec4(0.0)); | |
| vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ; | |
| vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ; | |
| vec3 p0 = vec3(a0.xy,h.x); | |
| vec3 p1 = vec3(a0.zw,h.y); | |
| vec3 p2 = vec3(a1.xy,h.z); | |
| vec3 p3 = vec3(a1.zw,h.w); | |
| vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3))); | |
| p0 *= norm.x; | |
| p1 *= norm.y; | |
| p2 *= norm.z; | |
| p3 *= norm.w; | |
| vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0); | |
| m = m * m; | |
| return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1), | |
| dot(p2,x2), dot(p3,x3) ) ); | |
| } | |
| void main() { | |
| // 0. Rotation Logic | |
| // Slow down rotation significantly when exploded to stabilize the backdrop | |
| float rotSpeed = mix(0.02, 0.002, uExplode); | |
| float c = cos(uTime * rotSpeed); | |
| float s = sin(uTime * rotSpeed); | |
| mat3 rotate = mat3(c, 0.0, -s, 0.0, 1.0, 0.0, s, 0.0, c); | |
| vec3 spherePos = rotate * aOriginalPos; | |
| vec3 triPos = aTrianglePos; | |
| // 1. MORPH INTERPOLATION | |
| vec3 pos = mix(spherePos, triPos, uMorph); | |
| // 2. Noise Field (Internal Motion) | |
| float noiseScale = 0.8; | |
| float timeScale = 0.15; | |
| float noise1 = snoise(pos * noiseScale + vec3(uTime * timeScale)); | |
| // Dampen noise when exploded so it's less chaotic background | |
| float displacement = noise1 * 0.6 * (1.0 - uExplode * 0.5); | |
| // 3. Apply Displacement | |
| vec3 normal = normalize(pos); | |
| float dispAmount = mix(0.5, 0.15, uMorph); | |
| vec3 newPos = pos + normal * displacement * dispAmount; | |
| // 4. Shift Logic (World Space) | |
| // When exploded, we remove lateral shifts so the tunnel is centered | |
| float effectiveShiftX = mix(uShiftX, 0.0, uExplode); | |
| float effectiveShiftY = mix(uShiftY, 0.0, uExplode); | |
| newPos.x += effectiveShiftX; | |
| newPos.y += effectiveShiftY; | |
| // 5. EXPLOSION / WIPE LOGIC (Enhanced) | |
| // Instead of just pushing away, we create a "Tunnel" or "Stargate" effect. | |
| // We push X and Y radially outward based on the uExplode factor, clearing the center. | |
| float radius2D = length(newPos.xy); | |
| // Radial push: The closer to the center, the harder we push out, ensuring a clear text area | |
| float pushFactor = smoothstep(0.0, 1.0, uExplode) * 20.0; | |
| // Direction from center | |
| vec2 dir2D = normalize(newPos.xy); | |
| // Apply Push | |
| // We add a nonlinear expansion so the middle clears fast, but the edges stay visible longer | |
| vec3 explodedPos = newPos; | |
| explodedPos.xy += dir2D * pushFactor * (1.0 + aRandom); // Add random to break uniformity | |
| explodedPos.z -= uExplode * 10.0; // Push deep into background | |
| newPos = mix(newPos, explodedPos, uExplode); | |
| // 6. Final Position | |
| vec4 mvPosition = modelViewMatrix * vec4(newPos, 1.0); | |
| gl_Position = projectionMatrix * mvPosition; | |
| // 7. Point Size | |
| // Make them smaller and sharper when they are background stars | |
| float sizeBase = mix(2.0, 1.5, uExplode); | |
| gl_PointSize = sizeBase * (15.0 / -mvPosition.z); | |
| // 8. Varyings | |
| vPos = newPos; | |
| // Alpha Logic for Explosion: | |
| // Fade out particles that are still too close to center-screen after explosion | |
| // This guarantees text legibility | |
| float screenCenterDist = length(newPos.xy); | |
| float centerClearMask = smoothstep(2.0, 8.0, screenCenterDist); // 0 at center, 1 at edges | |
| vExplodeAlpha = mix(1.0, centerClearMask, uExplode); | |
| vec3 viewDir = normalize(-mvPosition.xyz); | |
| vec3 viewNormal = normalize(normalMatrix * normal); | |
| float dotNV = dot(viewDir, viewNormal); | |
| vRim = 1.0 - max(0.0, abs(dotNV)); | |
| vRim = pow(vRim, 2.5); | |
| vDepth = smoothstep(-2.0, 5.0, newPos.z); | |
| } | |
| `; | |
| const fragmentShader = ` | |
| varying float vDepth; | |
| varying float vRim; | |
| varying vec3 vPos; | |
| varying float vExplodeAlpha; | |
| uniform vec3 uColorCore; | |
| uniform vec3 uColorMid; | |
| void main() { | |
| vec2 coord = gl_PointCoord - vec2(0.5); | |
| float dist = length(coord); | |
| if (dist > 0.5) discard; | |
| float alpha = 1.0 - smoothstep(0.3, 0.5, dist); | |
| // Colors | |
| vec3 cCore = uColorCore; | |
| vec3 cMid = uColorMid; | |
| vec3 cRim = vec3(0.8, 0.1, 0.0); | |
| vec3 finalColor; | |
| float midMix = smoothstep(0.0, 0.6, vRim); | |
| finalColor = mix(cCore, cMid, midMix); | |
| float rimMix = smoothstep(0.6, 1.0, vRim); | |
| finalColor = mix(finalColor, cRim, rimMix); | |
| float intensity = 1.0 + (midMix * 0.5); | |
| // Adjust depth alpha calculation to be more forgiving in exploded state | |
| float depthAlpha = smoothstep(-10.0, 5.0, vPos.z) * 0.9 + 0.1; | |
| // Combine standard alpha with explosion alpha (hollows out the center) | |
| float finalAlpha = alpha * depthAlpha * vExplodeAlpha; | |
| gl_FragColor = vec4(finalColor * intensity, finalAlpha); | |
| } | |
| `; | |
| // ----------------------------------------------------------------------------- | |
| // COMPONENT | |
| // ----------------------------------------------------------------------------- | |
| interface GenerativeSphereProps { | |
| scrollRef?: React.MutableRefObject<number>; | |
| shiftRef?: React.MutableRefObject<number>; | |
| mode: 'sphere' | 'triangle' | 'explode'; | |
| isMobile: boolean; | |
| } | |
| export const GenerativeSphere: React.FC<GenerativeSphereProps> = ({ scrollRef, shiftRef, mode, isMobile }) => { | |
| const pointsRef = useRef<THREE.Points>(null); | |
| const materialRef = useRef<THREE.ShaderMaterial>(null); | |
| // Smoothing refs | |
| const smoothedScroll = useRef(0); | |
| const currentMorph = useRef(0); | |
| const currentExplode = useRef(0); | |
| const currentShiftX = useRef(0); | |
| const currentShiftY = useRef(0); | |
| const COUNT = 32000; | |
| // Adjusted radius for mobile to be even smaller | |
| const RADIUS = isMobile ? 1.4 : 3.5; | |
| const { positions, originalPositions, trianglePositions, randoms } = useMemo(() => { | |
| const pos = new Float32Array(COUNT * 3); | |
| const origPos = new Float32Array(COUNT * 3); | |
| const triPos = new Float32Array(COUNT * 3); | |
| const rnd = new Float32Array(COUNT); | |
| const phi = Math.PI * (3 - Math.sqrt(5)); | |
| const triangleScale = RADIUS * 0.6; | |
| for (let i = 0; i < COUNT; i++) { | |
| // 1. SPHERE | |
| const y = 1 - (i / (COUNT - 1)) * 2; | |
| const radiusAtY = Math.sqrt(1 - y * y); | |
| const theta = phi * i; | |
| const x = Math.cos(theta) * radiusAtY; | |
| const z = Math.sin(theta) * radiusAtY; | |
| const sx = x * RADIUS; | |
| const sy = y * RADIUS; | |
| const sz = z * RADIUS; | |
| pos[i * 3] = sx; | |
| pos[i * 3 + 1] = sy; | |
| pos[i * 3 + 2] = sz; | |
| origPos[i * 3] = sx; | |
| origPos[i * 3 + 1] = sy; | |
| origPos[i * 3 + 2] = sz; | |
| // 2. TRIANGLE | |
| const angle = Math.atan2(sy, sx); | |
| const distXY = Math.sqrt(sx*sx + sy*sy); | |
| const segmentAngle = (2 * Math.PI) / 3; | |
| const offsetAngle = angle + Math.PI / 2; | |
| const constrainedAngle = offsetAngle - segmentAngle * Math.floor((offsetAngle + segmentAngle / 2) / segmentAngle); | |
| const r_sharp = 1.0 / Math.cos(constrainedAngle); | |
| const r_factor = r_sharp; | |
| const normalizedRadialDist = distXY / RADIUS; | |
| const finalR = normalizedRadialDist * r_factor * triangleScale; | |
| const tx = Math.cos(angle) * finalR; | |
| const ty = Math.sin(angle) * finalR; | |
| const tz = sz * 0.35; | |
| triPos[i * 3] = tx; | |
| triPos[i * 3 + 1] = ty; | |
| triPos[i * 3 + 2] = tz; | |
| rnd[i] = Math.random(); | |
| } | |
| return { | |
| positions: pos, | |
| originalPositions: origPos, | |
| trianglePositions: triPos, | |
| randoms: rnd | |
| }; | |
| }, [RADIUS]); | |
| const uniforms = useMemo(() => ({ | |
| uTime: { value: 0 }, | |
| uScroll: { value: 0 }, | |
| uMorph: { value: 0 }, | |
| uExplode: { value: 0 }, | |
| uShiftX: { value: 0 }, | |
| uShiftY: { value: 0 }, | |
| uColorCore: { value: new THREE.Color('#ff5522') }, | |
| uColorMid: { value: new THREE.Color('#ffddaa') }, | |
| }), []); | |
| useFrame((state, delta) => { | |
| if (materialRef.current) { | |
| materialRef.current.uniforms.uTime.value = state.clock.getElapsedTime(); | |
| // Scroll Physics | |
| if (scrollRef) { | |
| smoothedScroll.current = THREE.MathUtils.lerp(smoothedScroll.current, scrollRef.current, 0.05); | |
| materialRef.current.uniforms.uScroll.value = smoothedScroll.current; | |
| } | |
| // Physics State Logic | |
| const isTriangle = mode === 'triangle'; | |
| const isExplode = mode === 'explode'; | |
| const targetMorph = isTriangle ? 1.0 : 0.0; | |
| const targetExplode = isExplode ? 1.0 : 0.0; | |
| // Interolate values | |
| currentMorph.current = THREE.MathUtils.lerp(currentMorph.current, targetMorph, 2.0 * delta); | |
| // Explode needs to be slightly snappy but smooth | |
| currentExplode.current = THREE.MathUtils.lerp(currentExplode.current, targetExplode, 3.0 * delta); | |
| materialRef.current.uniforms.uMorph.value = currentMorph.current; | |
| materialRef.current.uniforms.uExplode.value = currentExplode.current; | |
| // Position Physics (Shift) | |
| let targetX = 0; | |
| let targetY = 0; | |
| if (isMobile) { | |
| targetX = 0; | |
| targetY = 0.5; // Adjusted Y shift for smaller sphere on mobile | |
| } else { | |
| if (shiftRef && !isExplode) { | |
| // Only shift if NOT exploded. If exploded, we want center screen. | |
| targetX = shiftRef.current; | |
| } | |
| targetY = 0; | |
| } | |
| currentShiftX.current = THREE.MathUtils.lerp(currentShiftX.current, targetX, 2.0 * delta); | |
| currentShiftY.current = THREE.MathUtils.lerp(currentShiftY.current, targetY, 2.0 * delta); | |
| materialRef.current.uniforms.uShiftX.value = currentShiftX.current; | |
| materialRef.current.uniforms.uShiftY.value = currentShiftY.current; | |
| } | |
| }); | |
| return ( | |
| <points ref={pointsRef}> | |
| <bufferGeometry> | |
| <bufferAttribute | |
| attach="attributes-position" | |
| count={positions.length / 3} | |
| array={positions} | |
| itemSize={3} | |
| /> | |
| <bufferAttribute | |
| attach="attributes-aOriginalPos" | |
| count={originalPositions.length / 3} | |
| array={originalPositions} | |
| itemSize={3} | |
| /> | |
| <bufferAttribute | |
| attach="attributes-aTrianglePos" | |
| count={trianglePositions.length / 3} | |
| array={trianglePositions} | |
| itemSize={3} | |
| /> | |
| <bufferAttribute | |
| attach="attributes-aRandom" | |
| count={randoms.length} | |
| array={randoms} | |
| itemSize={1} | |
| /> | |
| </bufferGeometry> | |
| <shaderMaterial | |
| ref={materialRef} | |
| vertexShader={vertexShader} | |
| fragmentShader={fragmentShader} | |
| uniforms={uniforms} | |
| transparent={true} | |
| depthWrite={false} | |
| blending={THREE.AdditiveBlending} | |
| /> | |
| </points> | |
| ); | |
| }; |