<!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>