跳到主要内容纯 HTML 实现的交互式星球漫游效果 | 极客日志HTML / CSS大前端
纯 HTML 实现的交互式星球漫游效果
展示了一个基于纯 HTML、CSS 和 JavaScript 实现的交互式星球漫游网页。项目使用 Three.js 渲染行星模型,结合 Tailwind CSS 进行样式设计,并利用 GSAP 库实现平滑滚动动画。支持地球、火星、木星、土星等天体的展示,包含光照计算、法线贴图及环绕光环效果。代码结构清晰,适合前端开发者学习 WebGL 可视化交互开发。
kaikai26K 浏览 项目简介
这是一个基于纯 HTML、CSS 和 JavaScript 实现的交互式星球漫游页面。利用 WebGL 渲染行星模型,配合 Tailwind CSS 进行样式设计,并利用 GSAP 库实现平滑滚动动画。
功能特性
- 真实纹理支持:支持法线贴图与位移贴图(当前示例使用程序化生成以确保开箱即用)
- 高级光照计算:包含环境光、点光源及菲涅尔反射效果
- 交互体验:滚动切换星球,左右交替布局,支持键盘与触摸导航
- 视觉效果:高质量球体几何体,平滑过渡动画,进度条指示器
代码实现
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>星际探索 - 沉浸式滚动体验</title>
<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">
Loading Universe...
01 / 05
Scroll
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online
<script src="https://cdn.tailwindcss.com">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js">
</script>
<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-container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
#content-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 10; pointer-events: none; }
.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 { 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 { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.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); }
.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 { 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 { 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 { 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 { 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 { 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-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); }
@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>
<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">
</div>
</div>
<div class="progress-bar" id="progress-bar">
</div>
<div id="canvas-container">
</div>
<div class="section-indicator">
<span id="current-section">
</span>
<span id="total-sections">
</span>
</div>
<div id="content-overlay">
</div>
<div class="nav-dots" id="nav-dots">
</div>
<div class="scroll-hint" id="scroll-hint">
<span>
</span>
<div class="scroll-mouse">
<div class="scroll-wheel">
</div>
</div>
</div>
<script>
const CONFIG = {
cameraDistance: 12,
transitionDuration: 1.2,
scrollThreshold: 50,
lerpFactor: 0.1,
autoRotateSpeed: 0.002,
planetEntryScale: 0,
planetIdleScale: 1,
};
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 }, 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: '这只是开始。宇宙中有数十亿颗恒星和行星等待着被发现。让我们一起踏上探索未知的旅程,去追寻那些遥远的星辰。' }
];
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();
}
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;
}
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);
}
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') {
planetX = 6;
} else if (planetData.textPosition === 'right') {
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() {
camera.lookAt(0, 0, 0);
}});
}
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>${planet.name}</h2><p>${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 style="margin-top: 2rem;"><button class="cta-button" onclick="alert('即将开启星际之旅!')"><span>开始探索</span><svg 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'; }
}
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);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
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);
}
document.addEventListener('DOMContentLoaded', () => { init(); });
</script>
</body>
</html>
配置说明
- 纹理资源:当前示例使用程序化生成纹理以确保开箱即用。实际项目中可替换为本地纹理文件(如
2k_earth_daymap.jpg),需确保 /textures/ 文件夹存在。
- 依赖库:通过 CDN 引入了 Three.js、GSAP 和 Tailwind CSS,无需本地安装即可运行。
- 运行方式:保存文件后直接在浏览器中打开即可查看效果。