№ 007
Globe
A cinematic night-side Earth with gold city lights and satellite trails, floating above a reflective pedestal.
- Version
- 1.0.0
- Download
- 4.0 MB · 8 files
- Added
- Oct 26, 2026
- Built with
- HTML, CSS, JS
- Needs
- three.js (CDN)
- Tags
- three.js, webgl, earth
Usage
Copy globe.css and globe.js (and assets/, if there is one) into your project, then:
Globe — a cinematic night-side Earth with gold city lights, a thin electric-blue atmosphere and
satellite trails, floating above a metal display pedestal on a reflective floor.
Usage (ES module, needs the "three" + "postprocessing" import map from index.html):
import { Globe } from './globe.js';
new Globe(element, { textures: './textures/', interactive: true });
For video/offline rendering pass { autoplay: false } and drive it yourself:
await globe.ready; globe.setView([x, y, z], [tx, ty, tz]); globe.renderAt(seconds);
Earth and star textures: Solar System Scope (CC BY 4.0), based on NASA imagery.Source
/* Globe — the container fills whatever box you give it; the canvas follows. */
.gl {
position: relative;
overflow: hidden;
touch-action: pan-y;
cursor: grab;
}
.gl:active { cursor: grabbing; }
.gl canvas {
display: block;
width: 100%;
height: 100%;
}
/* soft vignette over the scene */
.gl::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(ellipse 75% 65% at 50% 45%, transparent 55%, rgba(1, 3, 8, 0.75) 100%);
} /* Globe — a cinematic night-side Earth with gold city lights, a thin electric-blue atmosphere and
* satellite trails, floating above a metal display pedestal on a reflective floor.
*
* Usage (ES module, needs the "three" + "postprocessing" import map from index.html):
* import { Globe } from './globe.js';
* new Globe(element, { textures: './textures/', interactive: true });
*
* For video/offline rendering pass { autoplay: false } and drive it yourself:
* await globe.ready; globe.setView([x, y, z], [tx, ty, tz]); globe.renderAt(seconds);
*
* Earth and star textures: Solar System Scope (CC BY 4.0), based on NASA imagery.
*/
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { Reflector } from 'three/addons/objects/Reflector.js';
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
import { EffectComposer, RenderPass, EffectPass, BloomEffect, ToneMappingEffect, ToneMappingMode, SMAAEffect } from 'postprocessing';
const RADIUS = 1;
const GLOBE_Y = 0.12; // globe centre height
const PED_TOP = -1.13; // pedestal top surface (leaves a floating gap under the globe)
const PED_R = 0.75; // pedestal radius — a little narrower than the globe
const PED_H = 0.16;
const SPIN = 0.04; // idle spin, radians per second
const CLOUD_SPIN = 0.012; // clouds drift slowly against the surface
const C = {
bg: new THREE.Color('#000105'),
ocean: new THREE.Color('#020e20'),
atmo: new THREE.Color('#348dff'),
rim: new THREE.Color('#8bcaff'),
gold: new THREE.Color('#ffb347'),
trail: new THREE.Color('#7cc0ff'),
neon: new THREE.Color('#8fcbff'),
};
// Satellite trails: radius, plane tilt (x lays the ring down, z slants it on screen),
// and the particles riding each one.
const ORBITS = [
{ r: 1.13, rx: 1.76, rz: -0.4, sats: [{ speed: 0.22, at: 3.3 }, { speed: 0.22, at: 0.2 }] },
{ r: 1.1, rx: 1.67, rz: 0.3, sats: [{ speed: -0.17, at: 2.0 }, { speed: -0.17, at: 4.9 }] },
{ r: 1.17, rx: 2.2, rz: 0.55, sats: [{ speed: 0.12, at: 1.9 }] },
{ r: 1.12, rx: 1.95, rz: -0.95, sats: [{ speed: 0.1, at: 1.0 }] },
{ r: 1.14, rx: 2.0, rz: 0.9, sats: [{ speed: -0.14, at: 4.2 }] },
];
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
/* ── shaders ── */
const sphereVertex = /* glsl */ `
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vView;
varying vec3 vWorld;
void main() {
vUv = uv;
vec4 world = modelMatrix * vec4(position, 1.0);
vWorld = world.xyz;
vNormal = normalize(mat3(modelMatrix) * normal);
vView = normalize(cameraPosition - world.xyz);
gl_Position = projectionMatrix * viewMatrix * world;
}
`;
// Earth surface: natural land dimmed for night, deep navy oceans, emissive gold city lights,
// blue scattering towards the limb and a bright thin rim.
const earthFragment = /* glsl */ `
uniform sampler2D dayMap;
uniform sampler2D nightMap;
uniform sampler2D oceanMap;
uniform vec3 sunDir;
uniform vec3 oceanColor;
uniform vec3 atmo;
uniform vec3 rim;
uniform vec3 gold;
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vView;
float luma(vec3 c) { return dot(c, vec3(0.2126, 0.7152, 0.0722)); }
void main() {
vec3 n = normalize(vNormal);
vec3 v = normalize(vView);
vec3 day = texture2D(dayMap, vUv).rgb;
float ocean = texture2D(oceanMap, vUv).r;
float night = texture2D(nightMap, vUv).r;
float ndl = dot(n, sunDir);
float lit = 0.3 + 0.7 * smoothstep(-0.35, 0.85, ndl);
// land: real colours, dimmed and slightly cooled; bright deserts and ice are compressed
vec3 land = day * vec3(0.75, 0.84, 0.95) * 0.56 * lit;
land *= 1.0 - 0.4 * smoothstep(0.1, 0.36, luma(land));
// oceans: deep navy, keeping a trace of the map's depth variation
vec3 sea = oceanColor * (0.7 + 1.1 * lit) + day * vec3(0.0, 0.02, 0.07) * lit;
vec3 color = mix(land, sea, ocean);
// faint sun glint on the water
vec3 h = normalize(sunDir + v);
color += vec3(0.2, 0.4, 0.8) * pow(max(dot(n, h), 0.0), 90.0) * ocean * 0.25;
// city lights stay emissive on the dark side; dense regions go past 1.0 for the bloom pass
float city = smoothstep(0.035, 0.6, night);
color += gold * (pow(city, 1.4) * 6.5 + city * 0.3) * (1.0 - ocean * 0.9);
// blue scattering towards the edge, then the luminous rim
float fres = 1.0 - max(dot(n, v), 0.0);
color = mix(color, atmo * 0.2, pow(fres, 3.0) * 0.5);
color += mix(atmo, rim, pow(fres, 8.0)) * pow(fres, 6.0) * 3.6;
gl_FragColor = vec4(color, 1.0);
#include <colorspace_fragment>
}
`;
// Clouds: grey-white, lit like the surface, thinning out at the limb so the rim stays clean.
const cloudFragment = /* glsl */ `
uniform sampler2D cloudMap;
uniform vec3 sunDir;
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vView;
void main() {
vec3 n = normalize(vNormal);
float c = texture2D(cloudMap, vUv).r;
float lit = 0.25 + 0.75 * smoothstep(-0.3, 0.9, dot(n, sunDir));
float fres = 1.0 - max(dot(n, normalize(vView)), 0.0);
float a = smoothstep(0.3, 0.9, c) * 0.66 * (1.0 - pow(fres, 3.0) * 0.6);
gl_FragColor = vec4(vec3(0.55, 0.63, 0.82) * lit, a);
#include <colorspace_fragment>
}
`;
// Outer atmosphere on the back faces of a slightly larger sphere: brightest at the limb, gone by its edge.
const haloFragment = /* glsl */ `
uniform vec3 atmo;
uniform float edge;
varying vec3 vNormal;
varying vec3 vView;
void main() {
float d = -dot(normalize(vNormal), normalize(vView));
float a = pow(clamp(d / edge, 0.0, 1.0), 3.0);
gl_FragColor = vec4(atmo * 2.2 * a, a);
}
`;
// Trails: brighter where they pass in front of the globe, dimmer behind.
const trailFragment = /* glsl */ `
uniform vec3 color;
uniform vec3 center;
varying vec3 vWorld;
void main() {
float front = smoothstep(-1.0, 1.0, vWorld.z - center.z);
gl_FragColor = vec4(color * mix(0.5, 1.9, front), mix(0.4, 1.0, front));
}
`;
const starVertex = /* glsl */ `
attribute float size;
attribute float phase;
uniform float time;
uniform float dpr;
varying float vGlow;
void main() {
vGlow = phase > 5.7 ? 1.8 : 0.45 + 0.45 * (0.5 + 0.5 * sin(time * 0.8 + phase * 10.0));
gl_PointSize = size * dpr;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const starFragment = /* glsl */ `
varying float vGlow;
void main() {
float d = length(gl_PointCoord - 0.5);
float a = smoothstep(0.5, 0.0, d);
gl_FragColor = vec4(vec3(0.8, 0.88, 1.0) * vGlow * 1.6 * a, a);
}
`;
const skyFragment = /* glsl */ `
uniform sampler2D map;
uniform vec3 bg;
varying vec2 vUv;
void main() {
vec3 stars = texture2D(map, vUv).rgb;
gl_FragColor = vec4(bg + pow(stars, vec3(1.7)) * vec3(0.55, 0.65, 1.0) * 1.1, 1.0);
#include <colorspace_fragment>
}
`;
/* ── helpers ── */
const radialTexture = (stops, size = 128) => {
const c = document.createElement('canvas');
c.width = c.height = size;
const g = c.getContext('2d');
const grad = g.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
stops.forEach(([at, color]) => grad.addColorStop(at, color));
g.fillStyle = grad;
g.fillRect(0, 0, size, size);
const tex = new THREE.CanvasTexture(c);
tex.colorSpace = THREE.SRGBColorSpace;
return tex;
};
// Grainy alpha so the floor reads as polished stone rather than a perfect mirror.
const grainTexture = () => {
const c = document.createElement('canvas');
c.width = c.height = 256;
const g = c.getContext('2d');
const img = g.createImageData(256, 256);
for (let i = 0; i < img.data.length; i += 4) {
const v = 205 + Math.random() * 50 - (Math.random() < 0.012 ? 120 : 0);
img.data[i] = img.data[i + 1] = img.data[i + 2] = v;
img.data[i + 3] = 255;
}
g.putImageData(img, 0, 0);
const tex = new THREE.CanvasTexture(c);
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(10, 10);
return tex;
};
export class Globe {
constructor(root, { textures = './textures/', interactive = true, autoplay = true, pixelRatio } = {}) {
this.root = root;
this.size = [0, 0];
this.clock = new THREE.Clock();
this.satellites = [];
this.spin = 1; // eases towards 0.15 while the user drags
this.dragging = false;
// tone mapping and antialiasing happen in the post-processing chain
this.renderer = new THREE.WebGLRenderer({
antialias: false, powerPreference: 'high-performance', stencil: false, depth: true,
preserveDrawingBuffer: !autoplay, // offline frames are read back after drawing
});
this.renderer.setPixelRatio(pixelRatio ?? Math.min(window.devicePixelRatio, 2));
this.renderer.toneMapping = THREE.NoToneMapping;
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
root.classList.add('gl');
root.appendChild(this.renderer.domElement);
this.scene = new THREE.Scene();
this.scene.background = C.bg;
this.scene.fog = new THREE.Fog(C.bg, 6.2, 9.5); // floor fades into the background
this.camera = new THREE.PerspectiveCamera(30, 1, 0.1, 120);
this.camera.position.set(0, 0.3, 5.3);
const pmrem = new THREE.PMREMGenerator(this.renderer);
this.env = pmrem.fromScene(new RoomEnvironment(this.renderer), 0.04).texture;
const manager = new THREE.LoadingManager();
this.ready = new Promise((resolve, reject) => {
manager.onLoad = resolve;
manager.onError = (url) => reject(new Error(`Globe: failed to load ${url}`));
});
this.loader = new THREE.TextureLoader(manager).setPath(textures);
this.buildSky();
this.buildLights();
this.buildEarth();
this.buildOrbits();
this.buildPedestal();
this.buildPost();
if (interactive) {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.target.set(0, -0.04, 0);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.06;
this.controls.enablePan = false;
this.controls.enableZoom = false;
this.controls.rotateSpeed = 0.55;
this.controls.minPolarAngle = Math.PI * 0.36;
this.controls.maxPolarAngle = Math.PI * 0.53;
this.controls.addEventListener('start', () => { this.dragging = true; });
this.controls.addEventListener('end', () => { this.dragging = false; });
} else {
this.camera.lookAt(0, -0.04, 0);
}
this.observer = new ResizeObserver(() => this.resize());
this.observer.observe(root);
this.resize();
if (autoplay) this.renderer.setAnimationLoop(() => this.frame());
}
texture(name, color = true) {
const tex = this.loader.load(name);
tex.colorSpace = color ? THREE.SRGBColorSpace : THREE.NoColorSpace;
tex.anisotropy = Math.min(8, this.renderer.capabilities.getMaxAnisotropy());
return tex;
}
buildSky() {
// milky-way photo on a big inner sphere, kept very dim
const sky = new THREE.Mesh(
new THREE.SphereGeometry(60, 64, 32),
new THREE.ShaderMaterial({
uniforms: { map: { value: this.texture('stars_milky_way_4k.jpg') }, bg: { value: C.bg } },
vertexShader: sphereVertex,
fragmentShader: skyFragment,
side: THREE.BackSide,
depthWrite: false,
}),
);
sky.rotation.set(0.5, 2.3, -0.6); // band of the milky way towards the upper right
this.scene.add(sky);
// sharp stars with varied brightness: most packed into the sky the camera looks at (behind the globe),
// the rest scattered all around for when the view is dragged
const n = 6000, framed = 4500;
const pos = new Float32Array(n * 3), size = new Float32Array(n), phase = new Float32Array(n);
for (let i = 0; i < n; i++) {
const r = 40;
if (i < framed) {
// within ~28° of straight behind the globe
const a = Math.random() * Math.PI * 2, spread = Math.sqrt(Math.random()) * 0.5;
pos.set([Math.cos(a) * spread * r, Math.sin(a) * spread * r * 1.25, -r], i * 3);
} else {
const u = Math.random() * 2 - 1, a = Math.random() * Math.PI * 2, s = Math.sqrt(1 - u * u);
pos.set([Math.cos(a) * s * r, u * r, Math.sin(a) * s * r], i * 3);
}
size[i] = 1.2 + Math.pow(Math.random(), 5) * 3.8;
phase[i] = Math.random() * 6.0;
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
geo.setAttribute('size', new THREE.BufferAttribute(size, 1));
geo.setAttribute('phase', new THREE.BufferAttribute(phase, 1));
this.stars = new THREE.Points(
geo,
new THREE.ShaderMaterial({
uniforms: { time: { value: 0 }, dpr: { value: this.renderer.getPixelRatio() } },
vertexShader: starVertex,
fragmentShader: starFragment,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
}),
);
this.scene.add(this.stars);
// faint blue haze behind the globe
const haze = new THREE.Sprite(new THREE.SpriteMaterial({
map: radialTexture([[0, 'rgba(40,110,255,0.12)'], [0.45, 'rgba(25,80,200,0.04)'], [1, 'rgba(10,40,120,0)']]),
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
}));
haze.position.set(0, GLOBE_Y, -2.5);
haze.scale.setScalar(7.5);
this.scene.add(haze);
}
buildLights() {
this.scene.add(new THREE.AmbientLight(0x4a6db8, 0.35));
this.sun = new THREE.DirectionalLight(0xdfe9ff, 1.6);
this.sun.position.set(-2.2, 2.4, 4);
this.scene.add(this.sun);
// cool rim from high behind (kept steep so the flat floor and pedestal top don't mirror it)
const rim = new THREE.DirectionalLight(0x4f8dff, 0.7);
rim.position.set(0, 4, -1.4);
this.scene.add(rim);
// very faint blue light between the globe and the pedestal
const under = new THREE.PointLight(0x3d8bff, 0.18, 1.4, 2);
under.position.set(0, PED_TOP + 0.2, 0);
this.scene.add(under);
}
buildEarth() {
this.float = new THREE.Group(); // gentle bob
this.float.position.y = GLOBE_Y;
this.scene.add(this.float);
this.tilt = new THREE.Group(); // north tipped slightly towards the viewer, like the reference
this.tilt.rotation.set(0.16, 0, 0.08);
this.float.add(this.tilt);
this.earth = new THREE.Mesh(
new THREE.SphereGeometry(RADIUS, 160, 80),
new THREE.ShaderMaterial({
uniforms: {
dayMap: { value: this.texture('earth_day_4k.jpg') },
nightMap: { value: this.texture('earth_night_4k.jpg', false) },
oceanMap: { value: this.texture('earth_ocean_2k.jpg', false) },
sunDir: { value: new THREE.Vector3() },
oceanColor: { value: C.ocean },
atmo: { value: C.atmo },
rim: { value: C.rim },
gold: { value: C.gold },
},
vertexShader: sphereVertex,
fragmentShader: earthFragment,
}),
);
this.earth.rotation.y = -0.16; // North and South America facing the camera
this.earthStart = this.earth.rotation.y;
this.tilt.add(this.earth);
this.clouds = new THREE.Mesh(
new THREE.SphereGeometry(RADIUS * 1.008, 128, 64),
new THREE.ShaderMaterial({
uniforms: { cloudMap: { value: this.texture('earth_clouds_4k.jpg', false) }, sunDir: this.earth.material.uniforms.sunDir },
vertexShader: sphereVertex,
fragmentShader: cloudFragment,
transparent: true,
depthWrite: false,
}),
);
this.clouds.rotation.y = this.earth.rotation.y;
this.tilt.add(this.clouds);
const haloR = RADIUS * 1.05;
this.float.add(new THREE.Mesh(
new THREE.SphereGeometry(haloR, 128, 64),
new THREE.ShaderMaterial({
uniforms: { atmo: { value: C.atmo }, edge: { value: Math.sqrt(1 - (RADIUS / haloR) ** 2) } },
vertexShader: sphereVertex,
fragmentShader: haloFragment,
side: THREE.BackSide,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
}),
));
}
buildOrbits() {
const flare = radialTexture([
[0, 'rgba(255,255,255,1)'],
[0.1, 'rgba(225,240,255,0.95)'],
[0.3, 'rgba(110,175,255,0.35)'],
[1, 'rgba(60,130,255,0)'],
]);
const coreMat = new THREE.MeshBasicMaterial({ color: new THREE.Color(0xffffff).multiplyScalar(3) });
const center = new THREE.Vector3(0, GLOBE_Y, 0);
ORBITS.forEach((o) => {
const orbit = new THREE.Group();
orbit.rotation.order = 'ZXY'; // lay the ring down (x), then slant it on screen (z)
orbit.rotation.set(o.rx, 0, o.rz);
this.float.add(orbit);
orbit.add(new THREE.Mesh(
new THREE.TorusGeometry(o.r, 0.0021, 6, 400),
new THREE.ShaderMaterial({
uniforms: { color: { value: C.trail }, center: { value: center } },
vertexShader: sphereVertex,
fragmentShader: trailFragment,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
}),
));
o.sats.forEach((s) => {
const sat = new THREE.Group();
sat.add(new THREE.Mesh(new THREE.SphereGeometry(0.011, 12, 8), coreMat));
const glow = new THREE.Sprite(new THREE.SpriteMaterial({ map: flare, blending: THREE.AdditiveBlending, depthWrite: false, transparent: true }));
glow.scale.setScalar(0.2);
sat.add(glow);
orbit.add(sat);
this.satellites.push({ mesh: sat, r: o.r, at: s.at, angle: s.at, speed: s.speed });
});
});
}
buildPedestal() {
const base = new THREE.Group();
base.position.y = PED_TOP - PED_H;
this.scene.add(base);
// gunmetal drum with a bevelled top edge (lathe profile: radius, height)
const b = 0.03;
const profile = [
[0, 0], [PED_R - 0.012, 0], [PED_R, 0.012],
[PED_R, PED_H - b], [PED_R - b * 0.25, PED_H - b * 0.25], [PED_R - b, PED_H],
[0, PED_H],
].map(([x, y]) => new THREE.Vector2(x, y));
const metal = new THREE.MeshStandardMaterial({ color: 0x0b0e15, metalness: 1, roughness: 0.16, envMap: this.env, envMapIntensity: 0.85 });
base.add(new THREE.Mesh(new THREE.LatheGeometry(profile, 192), metal));
// darker inset top with fine concentric grooves
const inset = new THREE.Mesh(
new THREE.CircleGeometry(PED_R - 0.07, 128),
new THREE.MeshStandardMaterial({ color: 0x05070c, metalness: 0.9, roughness: 0.55, envMap: this.env, envMapIntensity: 0.06 }),
);
inset.rotation.x = -Math.PI / 2;
inset.position.y = PED_H + 0.001;
base.add(inset);
const groove = new THREE.MeshBasicMaterial({ color: 0x15213a, transparent: true, opacity: 0.35 });
[0.28, 0.46, 0.6].forEach((r) => {
const g = new THREE.Mesh(new THREE.RingGeometry(r, r + 0.004, 160), groove);
g.rotation.x = -Math.PI / 2;
g.position.y = PED_H + 0.002;
base.add(g);
});
// neon ring around the top perimeter (HDR colour so only it blooms)
const neon = new THREE.Mesh(
new THREE.TorusGeometry(PED_R - 0.045, 0.009, 16, 256),
new THREE.MeshBasicMaterial({ color: C.neon.clone().multiplyScalar(2.2) }),
);
neon.rotation.x = Math.PI / 2;
neon.position.y = PED_H + 0.004;
base.add(neon);
// reflective floor: a half-resolution mirror under a grainy, dark, semi-transparent stone layer
this.mirror = new Reflector(new THREE.CircleGeometry(2.3, 96), {
clipBias: 0.003,
textureWidth: 512,
textureHeight: 512,
color: 0x4a5a7a,
});
this.mirror.rotation.x = -Math.PI / 2;
base.add(this.mirror);
const stone = new THREE.Mesh(
new THREE.CircleGeometry(14, 64),
new THREE.MeshStandardMaterial({ color: 0x02050c, metalness: 0.3, roughness: 0.6, transparent: true, opacity: 0.74, alphaMap: grainTexture() }),
);
stone.rotation.x = -Math.PI / 2;
stone.position.y = 0.002;
base.add(stone);
// blue light spilling onto the floor around the base
const spill = new THREE.Mesh(
new THREE.CircleGeometry(2.4, 64),
new THREE.MeshBasicMaterial({
map: radialTexture([[0, 'rgba(50,120,255,0.14)'], [0.45, 'rgba(40,100,255,0.05)'], [1, 'rgba(30,80,255,0)']]),
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
}),
);
spill.rotation.x = -Math.PI / 2;
spill.position.y = 0.004;
base.add(spill);
}
buildPost() {
const gl = this.renderer.capabilities;
this.composer = new EffectComposer(this.renderer, {
frameBufferType: THREE.HalfFloatType,
multisampling: gl.isWebGL2 ? Math.min(4, gl.maxSamples) : 0,
});
this.composer.addPass(new RenderPass(this.scene, this.camera));
// high threshold: only HDR emitters (city lights, rim, trails, particles, neon ring) bloom
this.bloom = new BloomEffect({ intensity: 1.3, luminanceThreshold: 0.85, luminanceSmoothing: 0.15, mipmapBlur: true, radius: 0.68 });
const effects = [this.bloom, new ToneMappingEffect({ mode: ToneMappingMode.ACES_FILMIC })];
if (!gl.isWebGL2) effects.unshift(new SMAAEffect());
this.composer.addPass(new EffectPass(this.camera, ...effects));
}
resize() {
const w = this.root.clientWidth, h = this.root.clientHeight;
if (!w || !h || (w === this.size[0] && h === this.size[1])) return; // resizing clears the canvas
this.size = [w, h];
this.camera.aspect = w / h;
// frame the globe + pedestal vertically; pull back on narrow screens so nothing is cropped
const dist = 5.6 * Math.max(1, 0.8 / this.camera.aspect);
this.camera.position.setLength(dist);
this.scene.fog.near = dist + 0.4; // floor fades out behind the pedestal at any distance
this.scene.fog.far = dist + 3.7;
this.camera.updateProjectionMatrix();
this.renderer.setSize(w, h);
this.composer.setSize(w, h);
const px = this.renderer.getPixelRatio();
this.stars.material.uniforms.dpr.value = px;
this.mirror.getRenderTarget().setSize(Math.round(w * px * 0.5), Math.round(h * px * 0.5));
}
frame() {
const dt = Math.min(this.clock.getDelta(), 0.05);
const t = this.clock.elapsedTime;
const motion = reducedMotion.matches ? 0.15 : 1;
// idle spin eases down while dragging and back up afterwards
this.spin += ((this.dragging ? 0.15 : 1) - this.spin) * Math.min(1, dt * 2.5);
this.earth.rotation.y += SPIN * this.spin * dt * motion;
this.clouds.rotation.y += (SPIN * this.spin + CLOUD_SPIN) * dt * motion;
this.float.position.y = GLOBE_Y + Math.sin(t * 0.7) * 0.025 * motion;
for (const s of this.satellites) {
s.angle += s.speed * dt * motion;
s.mesh.position.set(Math.cos(s.angle) * s.r, Math.sin(s.angle) * s.r, 0);
}
this.earth.material.uniforms.sunDir.value.copy(this.sun.position).normalize();
this.stars.material.uniforms.time.value = t;
this.controls?.update();
this.composer.render(dt);
}
// Place the camera (world coordinates) and keep the floor fade relative to the globe.
setView(position, target = [0, -0.04, 0]) {
this.camera.position.set(...position);
this.camera.lookAt(...target);
const dist = this.camera.position.distanceTo(new THREE.Vector3(0, GLOBE_Y, 0));
this.scene.fog.near = dist + 0.4;
this.scene.fog.far = dist + 3.7;
}
// Deterministic render at time t (seconds) for offline/video use; same motion as the live loop.
// spinTime is the idle spin's accumulated time (pass a smaller value to show it slowing during a drag).
renderAt(t, dt = 1 / 30, spinTime = t) {
this.earth.rotation.y = this.earthStart + SPIN * spinTime;
this.clouds.rotation.y = this.earthStart + SPIN * spinTime + CLOUD_SPIN * t;
this.float.position.y = GLOBE_Y + Math.sin(t * 0.7) * 0.025;
for (const s of this.satellites) {
s.angle = s.at + s.speed * t;
s.mesh.position.set(Math.cos(s.angle) * s.r, Math.sin(s.angle) * s.r, 0);
}
this.earth.material.uniforms.sunDir.value.copy(this.sun.position).normalize();
this.stars.material.uniforms.time.value = t;
this.composer.render(dt);
}
dispose() {
this.renderer.setAnimationLoop(null);
this.observer.disconnect();
this.controls?.dispose();
this.composer.dispose();
this.renderer.dispose();
this.renderer.domElement.remove();
}
} <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Globe</title>
<link rel="stylesheet" href="globe.css">
<style>
html, body { height: 100%; margin: 0; }
body {
background: #03060e;
overflow: hidden;
}
#globe {
position: fixed;
inset: 0;
}
</style>
<!-- Three.js from a CDN; this page must be served over http (not opened as file://). -->
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/",
"postprocessing": "https://cdn.jsdelivr.net/npm/postprocessing@6.35.5/build/index.js"
}
}
</script>
</head>
<body>
<div id="globe"></div>
<script type="module">
import { Globe } from './globe.js';
new Globe(document.getElementById('globe'), { textures: './textures/' });
</script>
</body>
</html> All 8 files in the zip
-
globe.css -
globe.js -
index.html -
textures/earth_clouds_4k.jpg -
textures/earth_day_4k.jpg -
textures/earth_night_4k.jpg -
textures/earth_ocean_2k.jpg -
textures/stars_milky_way_4k.jpg