89 lines
3.3 KiB
TypeScript
89 lines
3.3 KiB
TypeScript
import { useEffect, useRef } from 'react'
|
|
import * as THREE from 'three'
|
|
import surfaceUrl from '../../assets/language-world-v2/hololake-world-surface.png'
|
|
|
|
interface LivingPlanetProps { awake: boolean }
|
|
|
|
export function LivingPlanet({ awake }: LivingPlanetProps) {
|
|
const mountRef = useRef<HTMLDivElement>(null)
|
|
const awakeRef = useRef(awake)
|
|
awakeRef.current = awake
|
|
|
|
useEffect(() => {
|
|
const mount = mountRef.current
|
|
if (!mount) return
|
|
const scene = new THREE.Scene()
|
|
const camera = new THREE.PerspectiveCamera(34, 1, 0.1, 100)
|
|
camera.position.z = 3.2
|
|
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, powerPreference: 'high-performance' })
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
|
renderer.outputColorSpace = THREE.SRGBColorSpace
|
|
renderer.toneMapping = THREE.ACESFilmicToneMapping
|
|
renderer.toneMappingExposure = 0.92
|
|
mount.appendChild(renderer.domElement)
|
|
|
|
const group = new THREE.Group()
|
|
group.rotation.z = -0.08
|
|
scene.add(group)
|
|
const texture = new THREE.TextureLoader().load(surfaceUrl)
|
|
texture.colorSpace = THREE.SRGBColorSpace
|
|
texture.anisotropy = renderer.capabilities.getMaxAnisotropy()
|
|
const planet = new THREE.Mesh(
|
|
new THREE.SphereGeometry(1, 128, 128),
|
|
new THREE.MeshStandardMaterial({ map: texture, roughness: 0.78, metalness: 0.02 }),
|
|
)
|
|
group.add(planet)
|
|
|
|
scene.add(new THREE.HemisphereLight('#bfefff', '#010711', 1.25))
|
|
const sun = new THREE.DirectionalLight('#e8fbff', 2.75)
|
|
sun.position.set(-3.2, 2.2, 4.8)
|
|
scene.add(sun)
|
|
const lakeLight = new THREE.PointLight('#47d8e9', 0.72, 8)
|
|
lakeLight.position.set(2.8, 1.8, 3)
|
|
scene.add(lakeLight)
|
|
|
|
let pointerX = 0
|
|
let pointerY = 0
|
|
const onPointer = (event: PointerEvent) => {
|
|
pointerX = (event.clientX / window.innerWidth - 0.5) * 0.12
|
|
pointerY = (event.clientY / window.innerHeight - 0.5) * 0.08
|
|
}
|
|
window.addEventListener('pointermove', onPointer, { passive: true })
|
|
const resize = () => {
|
|
const width = mount.clientWidth || 720
|
|
const height = mount.clientHeight || 720
|
|
renderer.setSize(width, height, false)
|
|
camera.aspect = width / height
|
|
camera.updateProjectionMatrix()
|
|
}
|
|
const observer = new ResizeObserver(resize)
|
|
observer.observe(mount)
|
|
resize()
|
|
|
|
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
let animation = 0
|
|
let previous = performance.now()
|
|
const frame = (now: number) => {
|
|
const delta = Math.min((now - previous) / 1000, 0.05)
|
|
previous = now
|
|
if (!reduced) planet.rotation.y += delta * (awakeRef.current ? 0.065 : 0.038)
|
|
group.rotation.y += (pointerX - group.rotation.y) * 0.025
|
|
group.rotation.x += (-pointerY - group.rotation.x) * 0.025
|
|
renderer.render(scene, camera)
|
|
animation = requestAnimationFrame(frame)
|
|
}
|
|
animation = requestAnimationFrame(frame)
|
|
return () => {
|
|
cancelAnimationFrame(animation)
|
|
observer.disconnect()
|
|
window.removeEventListener('pointermove', onPointer)
|
|
texture.dispose()
|
|
planet.geometry.dispose()
|
|
;(planet.material as THREE.Material).dispose()
|
|
renderer.dispose()
|
|
renderer.domElement.remove()
|
|
}
|
|
}, [])
|
|
|
|
return <div className="living-planet" ref={mountRef} aria-label="正在转动的光湖世界星球"/>
|
|
}
|