「04-星球浏览 漫游」
链接:https://pan.quark.cn/s/ef2a1b53b341
- 真实的行星纹理(来自
/textures/文件夹)- 高级着色器材质(ShaderMaterial)
- 法线贴图支持
- 位移贴图
- 真实的光照计算(环境光、点光源、菲涅尔反射)
- 高质量球体几何体(IcosahedronGeometry 64段)
- 滚动切换星球
- 左右交替布局(文字左↔星球右,文字右↔星球左)
- 平滑的过渡动画
- 导航点
- 进度条
- 信息卡片
- 键盘和触摸支持
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>星际探索 - 沉浸式滚动体验</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Three.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<!-- GSAP -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
overflow: hidden;
height: 100%;
}
body {
background: #0a0a0f;
color: #fafafa;
font-family: 'Inter', sans-serif;
overflow: hidden;
height: 100vh;
width: 100vw;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Space Grotesk', sans-serif;
}
/* Canvas */
#canvas-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
/* Content overlay */
#content-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
pointer-events: none;
}
/* Planet info card */
.planet-info {
position: absolute;
width: 90%;
max-width: 480px;
padding: 2rem;
background: rgba(10, 10, 15, 0.85);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 1.5rem;
opacity: 0;
transform: translateY(30px);
transition: all 0.8s cubic-bezier(0.16, 1, 0.3, 1);
pointer-events: auto;
}
.planet-info.visible {
opacity: 1;
transform: translateY(0);
}
.planet-info.position-left {
left: 5%;
top: 50%;
transform: translateY(-50%) translateX(-50px);
}
.planet-info.position-left.visible {
transform: translateY(-50%) translateX(0);
}
.planet-info.position-right {
right: 5%;
left: auto;
top: 50%;
transform: translateY(-50%) translateX(50px);
}
.planet-info.position-right.visible {
transform: translateY(-50%) translateX(0);
}
.planet-info.position-center {
left: 50%;
top: 50%;
transform: translate(-50%, -40%);
text-align: center;
}
.planet-info.position-center.visible {
transform: translate(-50%, -50%);
}
/* Planet badge */
.planet-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 2rem;
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 1rem;
}
/* Gradient text */
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Progress bar */
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 3px;
background: linear-gradient(90deg, #667eea, #764ba2, #f093fb);
z-index: 100;
width: 0%;
transition: width 0.1s linear;
box-shadow: 0 0 10px rgba(102, 126, 234, 0.5);
}
/* Navigation */
.nav-dots {
position: fixed;
right: 2rem;
top: 50%;
transform: translateY(-50%);
z-index: 20;
display: flex;
flex-direction: column;
gap: 1rem;
}
.nav-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
cursor: pointer;
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
pointer-events: auto;
border: 2px solid transparent;
position: relative;
}
.nav-dot::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.3);
opacity: 0;
transition: all 0.3s ease;
}
.nav-dot:hover {
background: rgba(255, 255, 255, 0.5);
transform: scale(1.2);
}
.nav-dot:hover::before {
opacity: 1;
transform: translate(-50%, -50%) scale(1.2);
}
.nav-dot.active {
background: #667eea;
transform: scale(1.3);
box-shadow: 0 0 20px rgba(102, 126, 234, 0.6);
}
.nav-dot.active::before {
opacity: 1;
border-color: #667eea;
transform: translate(-50%, -50%) scale(1.5);
}
/* Stats grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
margin-top: 1.5rem;
}
.stat-item {
text-align: center;
padding: 1rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.05);
transition: all 0.3s ease;
}
.stat-item:hover {
background: rgba(255, 255, 255, 0.08);
transform: translateY(-2px);
}
.stat-value {
font-size: 1.5rem;
font-weight: 700;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.stat-label {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
margin-top: 0.25rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Feature list */
.feature-list {
margin-top: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.feature-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.05);
transition: all 0.3s ease;
}
.feature-item:hover {
background: rgba(255, 255, 255, 0.08);
transform: translateX(5px);
}
.feature-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2), rgba(118, 75, 162, 0.2));
border-radius: 0.75rem;
font-size: 1.25rem;
flex-shrink: 0;
}
.feature-text {
flex: 1;
}
.feature-title {
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 0.25rem;
font-size: 0.95rem;
}
.feature-desc {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.5);
line-height: 1.4;
}
/* Loading screen */
#loading-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #0a0a0f;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
transition: opacity 0.8s ease, visibility 0.8s ease;
}
#loading-screen.hidden {
opacity: 0;
visibility: hidden;
}
.loader-container {
position: relative;
width: 80px;
height: 80px;
}
.loader {
position: absolute;
width: 100%;
height: 100%;
border: 3px solid transparent;
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loader:nth-child(2) {
width: 60px;
height: 60px;
top: 10px;
left: 10px;
border-top-color: #764ba2;
animation-duration: 1.5s;
animation-direction: reverse;
}
.loader:nth-child(3) {
width: 40px;
height: 40px;
top: 20px;
left: 20px;
border-top-color: #f093fb;
animation-duration: 2s;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
margin-top: 2rem;
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.5);
letter-spacing: 0.2em;
text-transform: uppercase;
}
/* Section indicator */
.section-indicator {
position: fixed;
top: 2rem;
left: 50%;
transform: translateX(-50%);
z-index: 20;
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.4);
letter-spacing: 0.3em;
text-transform: uppercase;
display: flex;
align-items: center;
gap: 1rem;
}
.section-indicator span {
color: rgba(255, 255, 255, 0.8);
font-weight: 600;
}
/* Scroll hint */
.scroll-hint {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
z-index: 20;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
opacity: 0.6;
animation: fadeInOut 2s ease-in-out infinite;
pointer-events: none;
}
@keyframes fadeInOut {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.8; }
}
.scroll-hint span {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
text-transform: uppercase;
letter-spacing: 0.15em;
}
.scroll-mouse {
width: 24px;
height: 36px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 12px;
position: relative;
}
.scroll-wheel {
position: absolute;
top: 6px;
left: 50%;
transform: translateX(-50%);
width: 4px;
height: 6px;
background: rgba(255, 255, 255, 0.5);
border-radius: 2px;
animation: scrollWheel 1.5s ease-in-out infinite;
}
@keyframes scrollWheel {
0%, 100% { top: 6px; opacity: 1; }
50% { top: 18px; opacity: 0.3; }
}
/* CTA Buttons */
.cta-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 1rem 2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 1rem;
font-weight: 600;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
border: none;
cursor: pointer;
pointer-events: auto;
position: relative;
overflow: hidden;
}
.cta-button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition: left 0.5s ease;
}
.cta-button:hover::before {
left: 100%;
}
.cta-button:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
}
.cta-button.secondary {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.cta-button.secondary:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.4);
}
/* Responsive */
@media (max-width: 768px) {
.planet-info {
width: 85%;
padding: 1.5rem;
}
.planet-info.position-left,
.planet-info.position-right {
left: 50%;
right: auto;
transform: translate(-50%, -50%) translateY(30px);
}
.planet-info.position-left.visible,
.planet-info.position-right.visible {
transform: translate(-50%, -50%) translateY(0);
}
.nav-dots {
right: 1rem;
gap: 0.75rem;
}
.nav-dot {
width: 10px;
height: 10px;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
<base target="_blank">
</head>
<body>
<!-- Loading Screen -->
<div id="loading-screen">
<div class="loader-container">
<div class="loader"></div>
<div class="loader"></div>
<div class="loader"></div>
</div>
<div class="loading-text">Loading Universe...</div>
</div>
<!-- Progress Bar -->
<div class="progress-bar" id="progress-bar"></div>
<!-- Canvas Container -->
<div id="canvas-container"></div>
<!-- Section Indicator -->
<div class="section-indicator">
<span id="current-section">01</span> / <span id="total-sections">05</span>
</div>
<!-- Content Overlay -->
<div id="content-overlay">
<!-- Planet info cards will be dynamically generated -->
</div>
<!-- Navigation Dots -->
<div class="nav-dots" id="nav-dots"></div>
<!-- Scroll Hint -->
<div class="scroll-hint" id="scroll-hint">
<span>Scroll</span>
<div class="scroll-mouse">
<div class="scroll-wheel"></div>
</div>
</div>
<script>
// ============================================
// CONFIGURATION
// ============================================
const CONFIG = {
cameraDistance: 12,
transitionDuration: 1.2,
scrollThreshold: 50,
lerpFactor: 0.1,
autoRotateSpeed: 0.002,
planetEntryScale: 0,
planetIdleScale: 1,
};
// ============================================
// PLANET DATA - 左右交替布局
// ============================================
const PLANETS = [
{
id: 'earth',
name: '地球',
nameEn: 'Earth',
emoji: '🌍',
color: '#4a90e2',
type: 'earth',
scale: 2.5,
cameraOffset: { x: 4, y: 0, z: 0 },
textPosition: 'left',
stats: [
{ value: '45亿', label: '年历史' },
{ value: '71%', label: '海洋覆盖' },
{ value: '78亿', label: '人口' },
{ value: '1', label: '唯一生命' }
],
description: '地球是太阳系中唯一已知存在生命的行星。从太空中看,它是一颗美丽的蓝色宝石,被大气层和广阔的海洋所环绕。我们的家园,值得用一生去探索。'
},
{
id: 'mars',
name: '火星',
nameEn: 'Mars',
emoji: '🔴',
color: '#e74c3c',
type: 'mars',
scale: 2.2,
cameraOffset: { x: -4, y: 0, z: 0 },
textPosition: 'right',
features: [
{ icon: '🚀', title: '探索任务', desc: '多个国家和私人公司正在计划火星殖民任务' },
{ icon: '💧', title: '水资源', desc: '极地冰盖和地下可能存在液态水' },
{ icon: '🌡️', title: '极端环境', desc: '平均温度-63℃,沙尘暴频繁' }
],
description: '火星是太阳系第四颗行星,因其表面覆盖的氧化铁而呈现独特的红色。它是人类探索太空的下一个目标,也是未来殖民的首选星球。'
},
{
id: 'jupiter',
name: '木星',
nameEn: 'Jupiter',
emoji: '🟠',
color: '#f39c12',
type: 'jupiter',
scale: 4,
cameraOffset: { x: 5, y: 0, z: 0 },
textPosition: 'left',
stats: [
{ value: '79', label: '已知卫星' },
{ value: '11x', label: '地球直径' },
{ value: '9.9h', label: '自转周期' },
{ value: '318x', label: '地球质量' }
],
description: '木星是太阳系最大的行星,其质量是其他所有行星总和的2.5倍。它的著名大红斑是一个持续了数百年的巨大风暴,比地球还要大。'
},
{
id: 'saturn',
name: '土星',
nameEn: 'Saturn',
emoji: '🪐',
color: '#f1c40f',
type: 'saturn',
scale: 3.5,
cameraOffset: { x: -5, y: 0, z: 0 },
textPosition: 'right',
hasRings: true,
features: [
{ icon: '💫', title: '光环系统', desc: '宽度达28万公里,但厚度仅10-100米' },
{ icon: '❄️', title: '冰粒组成', desc: '主要由水冰组成,含有少量岩石和尘埃' },
{ icon: '📊', title: '7个环层', desc: '拥有7个主要环层,以字母A-G命名' }
],
description: '土星以其壮观的环系统而闻名,这些环主要由冰粒和岩石碎片组成。它是太阳系中最美丽的行星之一,也是密度最小的行星。'
},
{
id: 'universe',
name: '探索无限',
nameEn: 'Universe',
emoji: '🌌',
color: '#9b59b6',
type: 'none',
scale: 0,
cameraOffset: { x: 0, y: 0, z: 0 },
textPosition: 'center',
isFinal: true,
description: '这只是开始。宇宙中有数十亿颗恒星和行星等待着被发现。让我们一起踏上探索未知的旅程,去追寻那些遥远的星辰。'
}
];
// ============================================
// THREE.JS SETUP
// ============================================
let scene, camera, renderer, clock;
let currentPlanetMesh = null;
let ringMesh = null;
let starfield = null;
let atmosphereMesh = null;
let currentPlanetIndex = 0;
let isTransitioning = false;
let accumulatedScroll = 0;
let lastScrollTime = 0;
let scrollCooldown = false;
function init() {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0a0a0f, 0.02);
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, CONFIG.cameraDistance);
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
powerPreference: "high-performance"
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setClearColor(0x0a0a0f, 1);
document.getElementById('canvas-container').appendChild(renderer.domElement);
clock = new THREE.Clock();
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
const sunLight = new THREE.DirectionalLight(0xffffff, 1.5);
sunLight.position.set(5, 3, 5);
scene.add(sunLight);
const backLight = new THREE.DirectionalLight(0x667eea, 0.5);
backLight.position.set(-5, 0, -5);
scene.add(backLight);
createStarfield();
createPlanet(0);
setupUI();
setupScroll();
window.addEventListener('resize', onWindowResize);
setTimeout(() => {
document.getElementById('loading-screen').classList.add('hidden');
}, 1500);
animate();
}
// ============================================
// PROCEDURAL TEXTURE GENERATION
// ============================================
function createProceduralTexture(type) {
const canvas = document.createElement('canvas');
canvas.width = 1024;
canvas.height = 512;
const ctx = canvas.getContext('2d');
if (type === 'earth') {
const gradient = ctx.createLinearGradient(0, 0, 0, 512);
gradient.addColorStop(0, '#1e3a5f');
gradient.addColorStop(0.3, '#2e5a8f');
gradient.addColorStop(0.5, '#1e3a5f');
gradient.addColorStop(0.7, '#2e5a8f');
gradient.addColorStop(1, '#1e3a5f');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 1024, 512);
ctx.fillStyle = '#2d5016';
for (let i = 0; i < 20; i++) {
const x = Math.random() * 1024;
const y = Math.random() * 512;
const w = 50 + Math.random() * 150;
const h = 30 + Math.random() * 80;
ctx.beginPath();
ctx.ellipse(x, y, w, h, Math.random() * Math.PI, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
for (let i = 0; i < 50; i++) {
const x = Math.random() * 1024;
const y = Math.random() * 512;
const w = 30 + Math.random() * 100;
const h = 10 + Math.random() * 30;
ctx.beginPath();
ctx.ellipse(x, y, w, h, 0, 0, Math.PI * 2);
ctx.fill();
}
} else if (type === 'mars') {
const gradient = ctx.createLinearGradient(0, 0, 0, 512);
gradient.addColorStop(0, '#8B4513');
gradient.addColorStop(0.5, '#A0522D');
gradient.addColorStop(1, '#8B4513');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 1024, 512);
for (let i = 0; i < 100; i++) {
const x = Math.random() * 1024;
const y = Math.random() * 512;
const r = 5 + Math.random() * 20;
const shade = Math.random() > 0.5 ? '#654321' : '#CD853F';
ctx.fillStyle = shade;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = '#F5F5DC';
ctx.beginPath();
ctx.ellipse(512, 30, 200, 40, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(512, 482, 150, 30, 0, 0, Math.PI * 2);
ctx.fill();
} else if (type === 'jupiter') {
for (let y = 0; y < 512; y += 20) {
const hue = 30 + Math.sin(y * 0.02) * 10;
const lightness = 40 + Math.sin(y * 0.05) * 20;
ctx.fillStyle = `hsl(${hue}, 70%, ${lightness}%)`;
ctx.fillRect(0, y, 1024, 20);
}
ctx.fillStyle = '#8B0000';
ctx.beginPath();
ctx.ellipse(700, 300, 80, 40, 0, 0, Math.PI * 2);
ctx.fill();
} else if (type === 'saturn') {
for (let y = 0; y < 512; y += 15) {
const lightness = 50 + Math.sin(y * 0.03) * 15;
ctx.fillStyle = `hsl(45, 60%, ${lightness}%)`;
ctx.fillRect(0, y, 1024, 15);
}
}
const texture = new THREE.CanvasTexture(canvas);
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
return texture;
}
// ============================================
// STARFIELD
// ============================================
function createStarfield() {
const geometry = new THREE.BufferGeometry();
const count = 8000;
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
const sizes = new Float32Array(count);
for (let i = 0; i < count; i++) {
const i3 = i * 3;
positions[i3] = (Math.random() - 0.5) * 300;
positions[i3 + 1] = (Math.random() - 0.5) * 300;
positions[i3 + 2] = (Math.random() - 0.5) * 300;
const colorType = Math.random();
if (colorType < 0.7) {
colors[i3] = 1; colors[i3 + 1] = 1; colors[i3 + 2] = 1;
} else if (colorType < 0.85) {
colors[i3] = 0.8; colors[i3 + 1] = 0.9; colors[i3 + 2] = 1;
} else {
colors[i3] = 1; colors[i3 + 1] = 0.9; colors[i3 + 2] = 0.7;
}
sizes[i] = Math.random() * 2;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const material = new THREE.PointsMaterial({
size: 0.5,
vertexColors: true,
transparent: true,
opacity: 0.8,
sizeAttenuation: true
});
starfield = new THREE.Points(geometry, material);
scene.add(starfield);
}
// ============================================
// PLANET CREATION
// ============================================
function createPlanet(index) {
const planetData = PLANETS[index];
if (!planetData) return;
currentPlanetIndex = index;
if (currentPlanetMesh) {
scene.remove(currentPlanetMesh);
currentPlanetMesh.geometry.dispose();
currentPlanetMesh.material.dispose();
currentPlanetMesh = null;
}
if (ringMesh) {
scene.remove(ringMesh);
ringMesh.geometry.dispose();
ringMesh.material.dispose();
ringMesh = null;
}
if (atmosphereMesh) {
scene.remove(atmosphereMesh);
atmosphereMesh.geometry.dispose();
atmosphereMesh.material.dispose();
atmosphereMesh = null;
}
if (planetData.isFinal) {
gsap.to(camera.position, {
x: 0,
y: 5,
z: 20,
duration: 1.5,
ease: 'power2.inOut'
});
gsap.to(camera.rotation, {
x: -0.3,
duration: 1.5,
ease: 'power2.inOut'
});
return;
}
const geometry = new THREE.SphereGeometry(planetData.scale, 64, 64);
const texture = createProceduralTexture(planetData.type);
const material = new THREE.MeshPhongMaterial({
map: texture,
shininess: planetData.type === 'earth' ? 25 : 5,
specular: planetData.type === 'earth' ? new THREE.Color(0x333333) : new THREE.Color(0x000000)
});
currentPlanetMesh = new THREE.Mesh(geometry, material);
// 根据文字位置设置星球位置:文字在左时星球在右,文字在右时星球在左
let planetX;
if (planetData.textPosition === 'left') {
// 文字在左边,星球放在右边(正X方向)
planetX = 6;
} else if (planetData.textPosition === 'right') {
// 文字在右边,星球放在左边(负X方向)
planetX = -6;
} else {
planetX = 0;
}
console.log(`${planetData.name}: textPosition=${planetData.textPosition}, planetX=${planetX}`);
currentPlanetMesh.position.set(planetX, 0, 0);
currentPlanetMesh.scale.set(0, 0, 0);
scene.add(currentPlanetMesh);
if (planetData.type === 'earth') {
const atmGeometry = new THREE.SphereGeometry(planetData.scale * 1.05, 64, 64);
const atmMaterial = new THREE.MeshPhongMaterial({
color: 0x4a90e2,
transparent: true,
opacity: 0.2,
side: THREE.BackSide
});
atmosphereMesh = new THREE.Mesh(atmGeometry, atmMaterial);
atmosphereMesh.position.copy(currentPlanetMesh.position);
atmosphereMesh.scale.set(0, 0, 0);
scene.add(atmosphereMesh);
}
if (planetData.hasRings) {
const ringGeometry = new THREE.RingGeometry(
planetData.scale * 1.3,
planetData.scale * 2.0,
128
);
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 512;
const ctx = canvas.getContext('2d');
const gradient = ctx.createRadialGradient(256, 256, 100, 256, 256, 256);
gradient.addColorStop(0, 'rgba(201, 169, 97, 0)');
gradient.addColorStop(0.2, 'rgba(201, 169, 97, 0.8)');
gradient.addColorStop(0.4, 'rgba(201, 169, 97, 0.4)');
gradient.addColorStop(0.6, 'rgba(201, 169, 97, 0.6)');
gradient.addColorStop(0.8, 'rgba(201, 169, 97, 0.3)');
gradient.addColorStop(1, 'rgba(201, 169, 97, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 512, 512);
const ringTexture = new THREE.CanvasTexture(canvas);
const ringMaterial = new THREE.MeshBasicMaterial({
map: ringTexture,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.9
});
ringMesh = new THREE.Mesh(ringGeometry, ringMaterial);
ringMesh.position.copy(currentPlanetMesh.position);
ringMesh.rotation.x = Math.PI / 2.2;
ringMesh.scale.set(0, 0, 0);
scene.add(ringMesh);
}
gsap.to(currentPlanetMesh.scale, {
x: 1,
y: 1,
z: 1,
duration: 1,
ease: 'elastic.out(1, 0.5)',
delay: 0.2
});
if (atmosphereMesh) {
gsap.to(atmosphereMesh.scale, {
x: 1, y: 1, z: 1,
duration: 1,
ease: 'elastic.out(1, 0.5)',
delay: 0.3
});
}
if (ringMesh) {
gsap.to(ringMesh.scale, {
x: 1, y: 1, z: 1,
duration: 1.2,
ease: 'elastic.out(1, 0.4)',
delay: 0.4
});
}
// 相机保持居中,始终看向中心点
gsap.to(camera.position, {
x: 0,
y: 0,
z: CONFIG.cameraDistance,
duration: 1.2,
ease: 'power2.inOut'
});
gsap.to({}, {
duration: 1.2,
onUpdate: function() {
// 相机始终看向中心 (0, 0, 0),这样星球偏移才明显
camera.lookAt(0, 0, 0);
}
});
}
// ============================================
// UI SETUP
// ============================================
function setupUI() {
const overlay = document.getElementById('content-overlay');
const navDots = document.getElementById('nav-dots');
PLANETS.forEach((planet, index) => {
const card = createPlanetCard(planet, index);
overlay.appendChild(card);
const dot = document.createElement('div');
dot.className = `nav-dot ${index === 0 ? 'active' : ''}`;
dot.dataset.index = index;
dot.addEventListener('click', () => {
if (!isTransitioning && index !== currentPlanetIndex) {
transitionToSection(index);
}
});
navDots.appendChild(dot);
});
document.getElementById('total-sections').textContent =
String(PLANETS.length).padStart(2, '0');
updateCardVisibility(0);
}
function createPlanetCard(planet, index) {
const card = document.createElement('div');
card.className = `planet-info position-${planet.textPosition}`;
card.id = `planet-card-${index}`;
let content = `
<div class="planet-badge">
<span>${planet.emoji}</span>
<span>${planet.nameEn}</span>
</div>
<h2 class="text-4xl md:text-5xl font-bold text-white mb-4 tracking-tight">
${planet.name}
</h2>
<p class="text-base md:text-lg text-white/70 mb-6 leading-relaxed">
${planet.description}
</p>
`;
if (planet.stats) {
content += `<div class="stats-grid">`;
planet.stats.forEach(stat => {
content += `
<div class="stat-item">
<div class="stat-value">${stat.value}</div>
<div class="stat-label">${stat.label}</div>
</div>
`;
});
content += `</div>`;
}
if (planet.features) {
content += `<div class="feature-list">`;
planet.features.forEach(feature => {
content += `
<div class="feature-item">
<div class="feature-icon">${feature.icon}</div>
<div class="feature-text">
<div class="feature-title">${feature.title}</div>
<div class="feature-desc">${feature.desc}</div>
</div>
</div>
`;
});
content += `</div>`;
}
if (planet.isFinal) {
content += `
<div class="flex flex-col sm:flex-row gap-4 justify-center mt-8">
<button class="cta-button" onclick="alert('即将开启星际之旅!')">
<span>开始探索</span>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
</svg>
</button>
<button class="cta-button secondary" onclick="alert('联系我们')">
<span>了解更多</span>
</button>
</div>
`;
}
card.innerHTML = content;
return card;
}
function updateCardVisibility(index) {
document.querySelectorAll('.planet-info').forEach((card, i) => {
if (i === index) {
card.classList.add('visible');
} else {
card.classList.remove('visible');
}
});
document.querySelectorAll('.nav-dot').forEach((dot, i) => {
if (i === index) {
dot.classList.add('active');
} else {
dot.classList.remove('active');
}
});
document.getElementById('current-section').textContent =
String(index + 1).padStart(2, '0');
const progress = (index / (PLANETS.length - 1)) * 100;
document.getElementById('progress-bar').style.width = `${progress}%`;
if (index === PLANETS.length - 1) {
document.getElementById('scroll-hint').style.opacity = '0';
} else {
document.getElementById('scroll-hint').style.opacity = '1';
}
}
// ============================================
// SCROLL HANDLING
// ============================================
function setupScroll() {
window.addEventListener('wheel', handleWheel, { passive: false });
window.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
e.preventDefault();
if (!isTransitioning && currentPlanetIndex < PLANETS.length - 1) {
transitionToSection(currentPlanetIndex + 1);
}
} else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
e.preventDefault();
if (!isTransitioning && currentPlanetIndex > 0) {
transitionToSection(currentPlanetIndex - 1);
}
}
});
let touchStartY = 0;
let touchStartTime = 0;
window.addEventListener('touchstart', (e) => {
touchStartY = e.touches[0].clientY;
touchStartTime = Date.now();
}, { passive: true });
window.addEventListener('touchend', (e) => {
if (isTransitioning) return;
const touchEndY = e.changedTouches[0].clientY;
const touchEndTime = Date.now();
const deltaY = touchStartY - touchEndY;
const deltaTime = touchEndTime - touchStartTime;
if (Math.abs(deltaY) > 50 || (Math.abs(deltaY) > 30 && deltaTime < 300)) {
if (deltaY > 0 && currentPlanetIndex < PLANETS.length - 1) {
transitionToSection(currentPlanetIndex + 1);
} else if (deltaY < 0 && currentPlanetIndex > 0) {
transitionToSection(currentPlanetIndex - 1);
}
}
}, { passive: true });
}
function handleWheel(e) {
e.preventDefault();
if (isTransitioning || scrollCooldown) return;
const now = Date.now();
const delta = e.deltaY;
accumulatedScroll += delta;
if (Math.abs(accumulatedScroll) > CONFIG.scrollThreshold) {
if (accumulatedScroll > 0 && currentPlanetIndex < PLANETS.length - 1) {
transitionToSection(currentPlanetIndex + 1);
} else if (accumulatedScroll < 0 && currentPlanetIndex > 0) {
transitionToSection(currentPlanetIndex - 1);
}
accumulatedScroll = 0;
scrollCooldown = true;
setTimeout(() => {
scrollCooldown = false;
}, 800);
}
lastScrollTime = now;
}
function transitionToSection(index) {
if (isTransitioning || index === currentPlanetIndex) return;
isTransitioning = true;
const direction = index > currentPlanetIndex ? 1 : -1;
if (currentPlanetMesh) {
gsap.to(currentPlanetMesh.scale, {
x: 0,
y: 0,
z: 0,
duration: 0.5,
ease: 'power2.in'
});
gsap.to(currentPlanetMesh.rotation, {
y: currentPlanetMesh.rotation.y + Math.PI * direction,
duration: 0.5,
ease: 'power2.in'
});
}
if (ringMesh) {
gsap.to(ringMesh.scale, {
x: 0, y: 0, z: 0,
duration: 0.4,
ease: 'power2.in'
});
}
if (atmosphereMesh) {
gsap.to(atmosphereMesh.scale, {
x: 0, y: 0, z: 0,
duration: 0.4,
ease: 'power2.in'
});
}
document.querySelectorAll('.planet-info').forEach(card => {
card.classList.remove('visible');
});
setTimeout(() => {
createPlanet(index);
updateCardVisibility(index);
setTimeout(() => {
isTransitioning = false;
}, 1000);
}, 500);
}
// ============================================
// WINDOW RESIZE
// ============================================
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// ============================================
// ANIMATION LOOP
// ============================================
function animate() {
requestAnimationFrame(animate);
const elapsedTime = clock.getElapsedTime();
if (currentPlanetMesh && !isTransitioning) {
currentPlanetMesh.rotation.y += CONFIG.autoRotateSpeed;
}
if (ringMesh) {
ringMesh.rotation.z += CONFIG.autoRotateSpeed * 0.5;
}
if (atmosphereMesh) {
const scale = 1 + Math.sin(elapsedTime * 0.5) * 0.02;
atmosphereMesh.scale.setScalar(scale);
}
if (starfield) {
starfield.rotation.y = elapsedTime * 0.0002;
starfield.rotation.x = Math.sin(elapsedTime * 0.0001) * 0.1;
}
renderer.render(scene, camera);
}
// ============================================
// INITIALIZATION
// ============================================
document.addEventListener('DOMContentLoaded', () => {
init();
});
</script>
</body>
</html>
行星配置:
- 🌍 地球:使用
2k_earth_daymap.jpg+2k_earth_normal_map.tif- 🔴 火星:使用
2k_mars.jpg- 🟠 木星:使用
2k_jupiter.jpg- 🪐 土星:使用
2k_saturn.jpg+ 光环使用前准备: 确保你的项目中有
/textures/文件夹,包含以下纹理文件:
2k_earth_daymap.jpg2k_earth_normal_map.tif2k_mars.jpg2k_jupiter.jpg2k_saturn.jpg你可以从 Solar System Scope 下载这些纹理(CC Attribution 4.0 许可证)。
现在打开
interactive-planets-textures.html就可以看到带有真实纹理的精美星球了


