<!doctype html>

<html lang="en">

<head>

<meta charset="utf-8">

<meta name="viewport" content="width=device-width, initial-scale=1">

<title>Rotating Earth</title>

<style>

  html, body {

    margin: 0;

    padding: 0;

    width: 100%;

    height: 100%;

    background: radial-gradient(ellipse at center, #0b0e1a 0%, #000000 100%);

    overflow: hidden;

  }

  canvas { display: block; }

  #caption {

    position: fixed;

    bottom: 32px;

    left: 0;

    right: 0;

    text-align: center;

    color: #7fa8ff;

    font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;

    letter-spacing: 0.35em;

    text-transform: uppercase;

    opacity: 0.55;

    pointer-events: none;

    user-select: none;

  }

  #loading {

    position: fixed;

    inset: 0;

    display: flex;

    align-items: center;

    justify-content: center;

    color: #4a6fb5;

    font: 13px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;

    letter-spacing: 0.2em;

    text-transform: uppercase;

    transition: opacity 0.6s ease;

  }

</style>

</head>

<body>

  <div id="loading">Loading Earth…</div>

  <div id="caption">Planet Earth</div>


  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>

  <script>

    const scene = new THREE.Scene();

    const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

    camera.position.z = 3.4;


    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });

    renderer.setSize(window.innerWidth, window.innerHeight);

    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

    document.body.appendChild(renderer.domElement);


    // ── Starfield ──────────────────────────────────────────────────────────

    const starGeometry = new THREE.BufferGeometry();

    const starCount = 6000;

    const starPositions = new Float32Array(starCount * 3);

    for (let i = 0; i < starCount * 3; i++) {

      starPositions[i] = (Math.random() - 0.5) * 400;

    }

    starGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));

    const starMaterial = new THREE.PointsMaterial({ color: 0xffffff, size: 0.18, sizeAttenuation: true, transparent: true, opacity: 0.85 });

    scene.add(new THREE.Points(starGeometry, starMaterial));


    // ── Earth ──────────────────────────────────────────────────────────────

    const textureLoader = new THREE.TextureLoader();

    let pendingTextures = 4;

    const onTextureLoaded = () => {

      pendingTextures -= 1;

      if (pendingTextures <= 0) {

        const el = document.getElementById('loading');

        el.style.opacity = '0';

        setTimeout(() => el.remove(), 600);

      }

    };


    const earthMap = textureLoader.load('https://threejs.org/examples/textures/planets/earth_atmos_2048.jpg', onTextureLoaded, undefined, onTextureLoaded);

    const bumpMap = textureLoader.load('https://threejs.org/examples/textures/planets/earth_normal_2048.jpg', onTextureLoaded, undefined, onTextureLoaded);

    const specularMap = textureLoader.load('https://threejs.org/examples/textures/planets/earth_specular_2048.jpg', onTextureLoaded, undefined, onTextureLoaded);

    const cloudMap = textureLoader.load('https://threejs.org/examples/textures/planets/earth_clouds_1024.png', onTextureLoaded, undefined, onTextureLoaded);


    const earthGeometry = new THREE.SphereGeometry(1, 96, 96);

    const earthMaterial = new THREE.MeshPhongMaterial({

      map: earthMap,

      bumpMap: bumpMap,

      bumpScale: 0.025,

      specularMap: specularMap,

      specular: new THREE.Color(0x333333),

      shininess: 12,

    });

    const earth = new THREE.Mesh(earthGeometry, earthMaterial);

    scene.add(earth);


    // Slight axial tilt, like the real thing (~23.4°)

    earth.rotation.z = (23.4 * Math.PI) / 180;


    // ── Cloud layer ────────────────────────────────────────────────────────

    const cloudGeometry = new THREE.SphereGeometry(1.012, 96, 96);

    const cloudMaterial = new THREE.MeshPhongMaterial({

      map: cloudMap,

      transparent: true,

      opacity: 0.45,

      depthWrite: false,

    });

    const clouds = new THREE.Mesh(cloudGeometry, cloudMaterial);

    clouds.rotation.z = earth.rotation.z;

    scene.add(clouds);


    // ── Atmosphere glow (rim-light shader) ────────────────────────────────

    const atmosphereGeometry = new THREE.SphereGeometry(1.12, 96, 96);

    const atmosphereMaterial = new THREE.ShaderMaterial({

      vertexShader: `

        varying vec3 vNormal;

        void main() {

          vNormal = normalize(normalMatrix * normal);

          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

        }

      `,

      fragmentShader: `

        varying vec3 vNormal;

        void main() {

          float intensity = pow(0.7 - dot(vNormal, vec3(0.0, 0.0, 1.0)), 3.5);

          gl_FragColor = vec4(0.35, 0.65, 1.0, 1.0) * intensity;

        }

      `,

      blending: THREE.AdditiveBlending,

      side: THREE.BackSide,

      transparent: true,

    });

    scene.add(new THREE.Mesh(atmosphereGeometry, atmosphereMaterial));


    // ── Lighting: a "sun" plus soft fill so the night side isn't pitch black ──

    const sun = new THREE.DirectionalLight(0xffffff, 1.6);

    sun.position.set(5, 2, 4);

    scene.add(sun);

    scene.add(new THREE.AmbientLight(0x30508a, 1.1));


    // ── Gentle mouse parallax ──────────────────────────────────────────────

    let targetRotationX = 0;

    let targetRotationY = 0;

    window.addEventListener('mousemove', (e) => {

      targetRotationY = ((e.clientX / window.innerWidth) - 0.5) * 0.4;

      targetRotationX = ((e.clientY / window.innerHeight) - 0.5) * 0.2;

    });


    window.addEventListener('resize', () => {

      camera.aspect = window.innerWidth / window.innerHeight;

      camera.updateProjectionMatrix();

      renderer.setSize(window.innerWidth, window.innerHeight);

    });


    const group = new THREE.Group();

    group.add(camera);


    function animate() {

      requestAnimationFrame(animate);

      earth.rotation.y += 0.0022;

      clouds.rotation.y += 0.0028;


      camera.position.x += (targetRotationY - camera.position.x * 0.15) * 0.02;

      camera.position.y += (-targetRotationX - camera.position.y * 0.15) * 0.02;

      camera.lookAt(scene.position);


      renderer.render(scene, camera);

    }

    animate();

  </script>

</body>

</html>