2025-11-22 19:38:14 +08:00
|
|
|
/* ===================================
|
|
|
|
|
3D场景管理器 - 主控制器
|
|
|
|
|
=================================== */
|
|
|
|
|
|
|
|
|
|
import { CONFIG } from '../config.js';
|
|
|
|
|
import { StarSystem } from './StarSystem.js';
|
|
|
|
|
import { EarthModel } from './EarthModel.js';
|
|
|
|
|
import { Transition } from './Transition.js';
|
|
|
|
|
|
|
|
|
|
export class SceneManager {
|
|
|
|
|
constructor(container, onTransitionComplete) {
|
|
|
|
|
this.container = container;
|
|
|
|
|
this.onTransitionComplete = onTransitionComplete;
|
|
|
|
|
|
|
|
|
|
// 状态标志
|
|
|
|
|
this.isIntro = true;
|
|
|
|
|
this.isHovering = false;
|
|
|
|
|
this.parallaxX = 0;
|
|
|
|
|
this.parallaxY = 0;
|
|
|
|
|
|
|
|
|
|
// Three.js核心对象
|
|
|
|
|
this.scene = null;
|
|
|
|
|
this.camera = null;
|
|
|
|
|
this.renderer = null;
|
|
|
|
|
this.raycaster = new THREE.Raycaster();
|
|
|
|
|
this.mouse = new THREE.Vector2();
|
|
|
|
|
|
|
|
|
|
// 子系统
|
|
|
|
|
this.starSystem = null;
|
|
|
|
|
this.earthModel = null;
|
|
|
|
|
this.transition = null;
|
|
|
|
|
|
2025-12-04 20:02:38 +08:00
|
|
|
// 动画控制
|
|
|
|
|
this.animationFrameId = null;
|
|
|
|
|
this.introTimeline = null;
|
|
|
|
|
|
2025-11-22 19:38:14 +08:00
|
|
|
this.init();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 初始化场景
|
|
|
|
|
init() {
|
|
|
|
|
// 创建场景
|
|
|
|
|
this.scene = new THREE.Scene();
|
|
|
|
|
this.scene.fog = new THREE.FogExp2(CONFIG.scene.fog.color, CONFIG.scene.fog.density);
|
|
|
|
|
|
|
|
|
|
// 创建相机
|
|
|
|
|
const camConfig = CONFIG.scene.camera;
|
|
|
|
|
this.camera = new THREE.PerspectiveCamera(
|
|
|
|
|
camConfig.fov,
|
|
|
|
|
window.innerWidth / window.innerHeight,
|
|
|
|
|
camConfig.near,
|
|
|
|
|
camConfig.far
|
|
|
|
|
);
|
|
|
|
|
this.camera.position.z = camConfig.initialZ; // 开场动画:从深空开始
|
|
|
|
|
|
|
|
|
|
// 创建渲染器
|
|
|
|
|
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
|
|
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
|
|
|
this.renderer.setPixelRatio(window.devicePixelRatio);
|
|
|
|
|
this.renderer.setClearColor(0x000000, 1); // 设置黑色背景
|
|
|
|
|
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
|
|
|
this.renderer.outputEncoding = THREE.sRGBEncoding;
|
|
|
|
|
this.container.appendChild(this.renderer.domElement);
|
|
|
|
|
|
|
|
|
|
// 创建星空系统
|
|
|
|
|
this.starSystem = new StarSystem(this.scene);
|
|
|
|
|
|
|
|
|
|
// 创建地球模型
|
|
|
|
|
this.earthModel = new EarthModel(this.scene);
|
|
|
|
|
|
|
|
|
|
// 添加灯光
|
|
|
|
|
this.setupLights();
|
|
|
|
|
|
|
|
|
|
// 绑定事件
|
|
|
|
|
this.bindEvents();
|
|
|
|
|
|
|
|
|
|
// 启动动画循环
|
|
|
|
|
this.animate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 设置灯光
|
|
|
|
|
setupLights() {
|
|
|
|
|
const lights = CONFIG.scene.lights;
|
|
|
|
|
|
|
|
|
|
// 环境光
|
|
|
|
|
this.scene.add(new THREE.AmbientLight(lights.ambient));
|
|
|
|
|
|
|
|
|
|
// 太阳光
|
|
|
|
|
const sunLight = new THREE.DirectionalLight(lights.sun.color, lights.sun.intensity);
|
|
|
|
|
sunLight.position.set(...lights.sun.position);
|
|
|
|
|
this.scene.add(sunLight);
|
|
|
|
|
|
|
|
|
|
// 边缘光
|
|
|
|
|
const rimLight = new THREE.SpotLight(lights.rim.color, lights.rim.intensity);
|
|
|
|
|
rimLight.position.set(...lights.rim.position);
|
|
|
|
|
rimLight.lookAt(0, 0, 0);
|
|
|
|
|
this.scene.add(rimLight);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 开场动画
|
|
|
|
|
startIntroSequence(uiElements) {
|
|
|
|
|
const animConfig = CONFIG.animation.intro;
|
|
|
|
|
|
2025-12-04 20:02:38 +08:00
|
|
|
// 保存 timeline 引用,以便在 dispose 时清理
|
|
|
|
|
this.introTimeline = gsap.timeline({
|
2025-11-22 19:38:14 +08:00
|
|
|
onComplete: () => {
|
|
|
|
|
this.isIntro = false; // 动画结束,允许交互
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-04 20:02:38 +08:00
|
|
|
const tl = this.introTimeline;
|
|
|
|
|
|
2025-11-22 19:38:14 +08:00
|
|
|
// A. 摄像机缓慢推进 (从 z=100 -> z=16)
|
|
|
|
|
tl.to(this.camera.position, {
|
|
|
|
|
z: CONFIG.scene.camera.defaultZ,
|
|
|
|
|
duration: animConfig.cameraDuration,
|
|
|
|
|
ease: "power2.out"
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// B. UI 淡入 (标题和提示依次出现)
|
2025-12-04 18:35:19 +08:00
|
|
|
tl.fromTo('.v1-title',
|
|
|
|
|
{ opacity: 0 },
|
|
|
|
|
{
|
|
|
|
|
opacity: 1,
|
|
|
|
|
duration: 1.5,
|
|
|
|
|
ease: "power2.out"
|
|
|
|
|
},
|
|
|
|
|
`-=${animConfig.titleDelay}`
|
|
|
|
|
);
|
2025-11-22 19:38:14 +08:00
|
|
|
|
2025-12-04 18:35:19 +08:00
|
|
|
tl.fromTo('.v1-subtitle',
|
|
|
|
|
{ opacity: 0 },
|
|
|
|
|
{
|
|
|
|
|
opacity: 1,
|
|
|
|
|
duration: 1.2,
|
|
|
|
|
ease: "power2.out"
|
|
|
|
|
},
|
|
|
|
|
`-=${animConfig.subtitleDelay}`
|
|
|
|
|
);
|
2025-11-22 19:38:14 +08:00
|
|
|
|
2025-12-04 18:35:19 +08:00
|
|
|
tl.fromTo('.instruction-hint',
|
|
|
|
|
{ opacity: 0 },
|
|
|
|
|
{
|
|
|
|
|
opacity: 1,
|
|
|
|
|
duration: 1.0,
|
|
|
|
|
ease: "power2.out"
|
|
|
|
|
},
|
|
|
|
|
`-=${animConfig.hintDelay}`
|
|
|
|
|
);
|
2025-11-22 19:38:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 初始化转场系统
|
|
|
|
|
initTransition(uiElements) {
|
|
|
|
|
this.transition = new Transition(
|
|
|
|
|
this.camera,
|
|
|
|
|
this.earthModel,
|
|
|
|
|
uiElements,
|
|
|
|
|
this.onTransitionComplete
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 绑定事件
|
|
|
|
|
bindEvents() {
|
2025-12-04 19:53:37 +08:00
|
|
|
// 保存事件处理器引用,以便后续移除
|
|
|
|
|
this.eventHandlers = {
|
|
|
|
|
mouseMove: (e) => this.onMouseMove(e),
|
|
|
|
|
mouseDown: () => this.onMouseDown(),
|
|
|
|
|
touchMove: (e) => this.onTouchMove(e),
|
|
|
|
|
touchStart: (e) => this.onTouchStart(e),
|
|
|
|
|
resize: () => this.onWindowResize()
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-22 19:38:14 +08:00
|
|
|
// 鼠标移动
|
2025-12-04 19:53:37 +08:00
|
|
|
window.addEventListener('mousemove', this.eventHandlers.mouseMove);
|
2025-11-22 19:38:14 +08:00
|
|
|
|
|
|
|
|
// 鼠标点击
|
2025-12-04 19:53:37 +08:00
|
|
|
window.addEventListener('mousedown', this.eventHandlers.mouseDown);
|
2025-11-22 19:38:14 +08:00
|
|
|
|
|
|
|
|
// 触摸移动(移动端)
|
2025-12-04 19:53:37 +08:00
|
|
|
window.addEventListener('touchmove', this.eventHandlers.touchMove, { passive: false });
|
2025-11-22 19:38:14 +08:00
|
|
|
|
|
|
|
|
// 触摸点击(移动端)
|
2025-12-04 19:53:37 +08:00
|
|
|
window.addEventListener('touchstart', this.eventHandlers.touchStart);
|
2025-11-22 19:38:14 +08:00
|
|
|
|
|
|
|
|
// 窗口resize
|
2025-12-04 19:53:37 +08:00
|
|
|
window.addEventListener('resize', this.eventHandlers.resize);
|
2025-11-22 19:38:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 鼠标移动事件
|
|
|
|
|
onMouseMove(e) {
|
|
|
|
|
if (this.transition && this.transition.isActive()) return;
|
|
|
|
|
|
|
|
|
|
this.mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
|
|
|
|
|
this.mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
|
|
|
|
|
|
|
|
|
|
// 只有开场动画结束后才检测悬停
|
|
|
|
|
if (!this.isIntro) {
|
|
|
|
|
this.raycaster.setFromCamera(this.mouse, this.camera);
|
|
|
|
|
const intersects = this.raycaster.intersectObjects(
|
|
|
|
|
this.earthModel.getInteractiveObjects()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (intersects.length > 0) {
|
|
|
|
|
if (!this.isHovering) {
|
|
|
|
|
this.isHovering = true;
|
|
|
|
|
this.container.classList.add('interactive');
|
|
|
|
|
gsap.to(this.earthModel.getAtmosphere().scale, {
|
|
|
|
|
x: 1.03,
|
|
|
|
|
y: 1.03,
|
|
|
|
|
z: 1.03,
|
|
|
|
|
duration: 0.3
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (this.isHovering) {
|
|
|
|
|
this.isHovering = false;
|
|
|
|
|
this.container.classList.remove('interactive');
|
|
|
|
|
gsap.to(this.earthModel.getAtmosphere().scale, {
|
|
|
|
|
x: 1,
|
|
|
|
|
y: 1,
|
|
|
|
|
z: 1,
|
|
|
|
|
duration: 0.3
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 视差效果
|
|
|
|
|
this.parallaxX = (e.clientX - window.innerWidth / 2) * 0.001;
|
|
|
|
|
this.parallaxY = (e.clientY - window.innerHeight / 2) * 0.001;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 鼠标点击事件
|
|
|
|
|
onMouseDown() {
|
|
|
|
|
if (this.isIntro || !this.isHovering) return;
|
|
|
|
|
if (this.transition && this.transition.isActive()) return;
|
|
|
|
|
|
|
|
|
|
// 触发转场
|
|
|
|
|
if (this.transition) {
|
|
|
|
|
this.transition.trigger();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 触摸移动事件(移动端)
|
|
|
|
|
onTouchMove(e) {
|
|
|
|
|
if (this.transition && this.transition.isActive()) return;
|
|
|
|
|
|
|
|
|
|
const touch = e.touches[0];
|
|
|
|
|
this.mouse.x = (touch.clientX / window.innerWidth) * 2 - 1;
|
|
|
|
|
this.mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1;
|
|
|
|
|
|
|
|
|
|
// 视差效果
|
|
|
|
|
this.parallaxX = (touch.clientX - window.innerWidth / 2) * 0.001;
|
|
|
|
|
this.parallaxY = (touch.clientY - window.innerHeight / 2) * 0.001;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 触摸点击事件(移动端)
|
|
|
|
|
onTouchStart(e) {
|
|
|
|
|
if (this.isIntro) return;
|
|
|
|
|
if (this.transition && this.transition.isActive()) return;
|
|
|
|
|
|
|
|
|
|
const touch = e.touches[0];
|
|
|
|
|
this.mouse.x = (touch.clientX / window.innerWidth) * 2 - 1;
|
|
|
|
|
this.mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1;
|
|
|
|
|
|
|
|
|
|
// 检测是否点击地球
|
|
|
|
|
this.raycaster.setFromCamera(this.mouse, this.camera);
|
|
|
|
|
const intersects = this.raycaster.intersectObjects(
|
|
|
|
|
this.earthModel.getInteractiveObjects()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (intersects.length > 0) {
|
|
|
|
|
// 触发转场
|
|
|
|
|
if (this.transition) {
|
|
|
|
|
this.transition.trigger();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 窗口resize
|
|
|
|
|
onWindowResize() {
|
|
|
|
|
this.camera.aspect = window.innerWidth / window.innerHeight;
|
|
|
|
|
this.camera.updateProjectionMatrix();
|
|
|
|
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 动画循环
|
|
|
|
|
animate() {
|
2025-12-04 20:02:38 +08:00
|
|
|
this.animationFrameId = requestAnimationFrame(() => this.animate());
|
2025-11-22 19:38:14 +08:00
|
|
|
|
|
|
|
|
// 如果不在转场中
|
|
|
|
|
if (!this.transition || !this.transition.isActive()) {
|
|
|
|
|
// 地球和云层旋转
|
|
|
|
|
this.earthModel.rotate();
|
|
|
|
|
|
|
|
|
|
// 只有开场动画结束后才启用视差效果
|
|
|
|
|
if (!this.isIntro) {
|
|
|
|
|
this.camera.position.x += (this.parallaxX * 8 - this.camera.position.x) * 0.05;
|
|
|
|
|
this.camera.position.y += (-this.parallaxY * 8 - this.camera.position.y) * 0.05;
|
|
|
|
|
this.camera.lookAt(0, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.renderer.render(this.scene, this.camera);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 销毁场景
|
|
|
|
|
dispose() {
|
2025-12-04 20:02:38 +08:00
|
|
|
// 停止动画循环
|
|
|
|
|
if (this.animationFrameId) {
|
|
|
|
|
cancelAnimationFrame(this.animationFrameId);
|
|
|
|
|
this.animationFrameId = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 杀掉所有正在运行的 GSAP 动画
|
|
|
|
|
if (this.introTimeline) {
|
|
|
|
|
this.introTimeline.kill();
|
|
|
|
|
this.introTimeline = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 杀掉转场相关的 GSAP 动画(杀掉所有针对相机、地球等对象的动画)
|
|
|
|
|
if (this.camera) {
|
|
|
|
|
gsap.killTweensOf(this.camera.position);
|
|
|
|
|
}
|
|
|
|
|
if (this.earthModel) {
|
|
|
|
|
const earthGroup = this.earthModel.getGroup();
|
|
|
|
|
const clouds = this.earthModel.getClouds();
|
|
|
|
|
const atmosphere = this.earthModel.getAtmosphere();
|
|
|
|
|
if (earthGroup) gsap.killTweensOf(earthGroup.rotation);
|
|
|
|
|
if (clouds) gsap.killTweensOf(clouds.material);
|
|
|
|
|
if (atmosphere) gsap.killTweensOf(atmosphere.material);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-04 19:53:37 +08:00
|
|
|
// 移除事件监听器,防止内存泄漏和重复触发
|
|
|
|
|
if (this.eventHandlers) {
|
|
|
|
|
window.removeEventListener('mousemove', this.eventHandlers.mouseMove);
|
|
|
|
|
window.removeEventListener('mousedown', this.eventHandlers.mouseDown);
|
|
|
|
|
window.removeEventListener('touchmove', this.eventHandlers.touchMove);
|
|
|
|
|
window.removeEventListener('touchstart', this.eventHandlers.touchStart);
|
|
|
|
|
window.removeEventListener('resize', this.eventHandlers.resize);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-04 20:02:38 +08:00
|
|
|
// 清理转场动画
|
|
|
|
|
if (this.transition) {
|
|
|
|
|
this.transition.dispose();
|
|
|
|
|
this.transition = null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 19:38:14 +08:00
|
|
|
if (this.starSystem) this.starSystem.dispose();
|
|
|
|
|
if (this.earthModel) this.earthModel.dispose();
|
|
|
|
|
if (this.renderer) {
|
|
|
|
|
this.renderer.dispose();
|
|
|
|
|
this.container.removeChild(this.renderer.domElement);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|