星球浏览 漫游(纯html 开源)

「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.jpg
  • 2k_earth_normal_map.tif
  • 2k_mars.jpg
  • 2k_jupiter.jpg
  • 2k_saturn.jpg

你可以从 Solar System Scope 下载这些纹理(CC Attribution 4.0 许可证)。

现在打开 interactive-planets-textures.html 就可以看到带有真实纹理的精美星球了

相关推荐
郝学胜-神的一滴1 小时前
FastAPI:Python 高性能 Web 框架的优雅之选
开发语言·前端·数据结构·python·算法·fastapi
慧一居士2 小时前
vite 使用说明和示例演示
前端
牢七2 小时前
反序列化重点模块 private Object readOrdinaryObject(boolean unshared)废案与反思
java·服务器·前端
NEXT062 小时前
数组转树与树转数组
前端·数据结构·面试
We་ct2 小时前
浏览器 Reflow(重排)与Repaint(重绘)全解析
前端·面试·edge·edge浏览器
笨笨狗吞噬者2 小时前
【uniapp】小程序端解决分包的uni_modules打包后产物进入主包中的问题
前端·微信小程序·uni-app
WebInfra2 小时前
Modern.js 3.0 发布:聚焦 Web 框架,拥抱生态发展
前端·javascript·前端框架
AngelPP3 小时前
OpenClaw Memory 模块完整分析
前端·aigc·ai编程
ID_180079054733 小时前
淘宝商品详情 API 接口 item_get: 高效获取商品数据的技术方案
java·前端·数据库