跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
HTML / CSS大前端

纯 HTML 实现的交互式星球漫游效果

展示了一个基于纯 HTML、CSS 和 JavaScript 实现的交互式星球漫游网页。项目使用 Three.js 渲染行星模型,结合 Tailwind CSS 进行样式设计,并利用 GSAP 库实现平滑滚动动画。支持地球、火星、木星、土星等天体的展示,包含光照计算、法线贴图及环绕光环效果。代码结构清晰,适合前端开发者学习 WebGL 可视化交互开发。

kaikai发布于 2026/3/22更新于 2026/10/2426K 浏览
纯 HTML 实现的交互式星球漫游效果

项目简介

这是一个基于纯 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>
    <!-- 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">
    
    
    
    
    
    
    
    


    
    
        
            
            
            
        
        Loading Universe...
    
    
    
    
    
    
    
        01 / 05
    
    
    
    
    
    
    
        Scroll
        
            
        
    
    


<!-- 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">
</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">
</span>
<span id="total-sections">
</span>
</div>
<!-- Content Overlay -->
<div id="content-overlay">
</div>
<!-- Navigation Dots -->
<div class="nav-dots" id="nav-dots">
</div>
<!-- Scroll Hint -->
<div class="scroll-hint" id="scroll-hint">
<span>
</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 }, 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>${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'; } } // ============================================ // 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>

配置说明

  1. 纹理资源:当前示例使用程序化生成纹理以确保开箱即用。实际项目中可替换为本地纹理文件(如 2k_earth_daymap.jpg),需确保 /textures/ 文件夹存在。
  2. 依赖库:通过 CDN 引入了 Three.js、GSAP 和 Tailwind CSS,无需本地安装即可运行。
  3. 运行方式:保存文件后直接在浏览器中打开即可查看效果。

注:本示例仅供学习参考,请勿用于商业用途。

目录

  1. 项目简介
  2. 功能特性
  3. 代码实现
  4. 配置说明

更多推荐文章

查看全部
  • Spring Web MVC 从入门到实战
  • Agent Skills 详解:如何赋予 AI 执行特定任务的能力
  • C++ 继承:消除代码冗余的有效方案
  • 基于 Vue3 的电影网站项目实战
  • C++ 核心面试题与知识点详解
  • 基于 SpringBoot2+Vue3 的城镇保障性住房管理系统设计与实现
  • 在线图书借阅平台的设计与实现:AI 辅助开发实践
  • Linux 进程概念(上)
  • Visual Studio 中 GitHub Copilot 隐私设置与数据共享控制
  • Java CountDownLatch 原理、应用与最佳实践
  • Python 列表内存存储本质:差异原因与优化建议
  • C++ STL unordered_set/unordered_map 使用介绍
  • TRAE 与 VSCode 的 Git 工作流实战指南
  • VisionTransformer(ViT)基础原理及时间序列行为识别应用
  • 企业落地大模型的路径、方法与避坑指南
  • Vue3 模板调用方法提示不存在?检查 Script Setup 暴露方式
  • HarmonyOS RcList 组件综合示例与尺寸计算
  • 10 个实用的 Python 编程技巧
  • 基于 Whisper-large-v3 的多语言翻译系统开发
  • PyCharm 隐藏实用技巧与常用快捷键指南

相关免费在线工具

  • 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