ASCII Earth
Open the globe. Drag with the mouse or touch to change the viewpoint; automatic rotation pauses during pointer interaction and resumes smoothly afterward. Zoom and pan are disabled to prevent accidental gesture interception while reading. The same renderer powers the homepage preview and the standalone experiment page.
How the Image Becomes Text
The underlying object is a 3D Three.js sphere rather than a flat texture map. A custom ShaderMaterial samples two textures: an ocean specular mask to separate water from land, and a relief map to vary luminance across land masses:
// Separate land detection from terrain detail.
// The specular map is nearly white over oceans; inverting it keeps oceans black.
// The relief map then modulates terrain brightness without dynamic lighting.
const material = new THREE.ShaderMaterial({
uniforms: {
oceanMap: {value: oceanTexture},
reliefMap: {value: reliefTexture},
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D oceanMap;
uniform sampler2D reliefMap;
varying vec2 vUv;
void main() {
float ocean = texture2D(oceanMap, vUv).g;
float land = 1.0 - smoothstep(0.28, 0.72, ocean);
float relief = texture2D(reliefMap, vUv).r;
float luminance = land * (0.48 + relief * 0.52);
gl_FragColor = vec4(vec3(luminance), 1.0);
}
`,
});
The shader reads the specular map’s green channel, turns it into a smoothed land mask, and modulates land luminance with the relief map. Ocean regions remain dark without a moving light source.
Three.js AsciiEffect renders this sphere into a temporary canvas, downsamples the pixels, and converts luminance values into characters from a fixed palette:
const effect = new AsciiEffect(renderer, ' .:-+*=%~@#01o', {
invert: true,
resolution: 0.16,
});
The leading space keeps dark oceanic regions completely empty, preserving crisp continent silhouettes without drawing a solid dark disc around the globe. Because oceans are empty, certain viewing angles with predominantly ocean coverage (such as the central Pacific) display relatively few characters; this is geometrically correct and does not mean rendering has stalled.
Character selection balances visual density and glyph shape. Horizontal glyphs such as -, =, and ~ form subtle contour bands over gently varying terrain.
Rotation and Animation Controls
The globe rotates along its vertical axis at 0.28 rad/s, capped at approximately 30 frames per second to reduce continuous CPU and GPU load:
const controls = new OrbitControls(camera, effect.domElement);
controls.enableZoom = false;
controls.enablePan = false;
controls.rotateSpeed = 0.6;
controls.enableDamping = true;
controls.dampingFactor = 0.06;
Dragging changes the camera viewing angle via OrbitControls and stops automatic rotation. The pause button stops automatic rotation, but camera changes still redraw the globe; pending drag damping is allowed to finish before idle frame requests stop. Animation pauses offscreen or in a hidden tab. Visibility changes and playback wake the renderer when appropriate.
Safeguarding Zero-Sized Sampling Buffers
AsciiEffect calculates sampling canvas dimensions as Math.floor(width * resolution) and Math.floor(height * resolution). When resolution is 0.16, any container dimension smaller than pixels rounds down to zero sample pixels. Calling getImageData(0, 0, 0, 0) on a canvas then throws a browser runtime exception.
The component checks the container dimensions before resizing. A host that is too small clears hasSize, so draw() skips rendering until valid dimensions return:
const resize = () => {
const {width, height} = mountNode.getBoundingClientRect();
const nextWidth = Math.round(width);
const nextHeight = Math.round(height);
if (disposed || nextWidth < 1 / ASCII_RESOLUTION || nextHeight < 1 / ASCII_RESOLUTION) {
hasSize = false;
return;
}
camera.aspect = nextWidth / nextHeight;
camera.updateProjectionMatrix();
effect.setSize(nextWidth, nextHeight);
hasSize = true;
draw();
};
Component Lifecycle and Cleanup
On unmount, the component releases all allocated resources:
- Cancels pending animation frames.
- Disconnects
ResizeObserverandIntersectionObserver. - Disposes
OrbitControls, textures, sphere geometry, shader material, and the WebGL renderer, and forces context release. - Removes the generated ASCII DOM element.