Skip to content
glyphcss

Terrain

Loading terrain…

ETOPO1 elevations become a shaded relief mesh, with contours every 500 metres. Exaggeration 40 makes the Alps readable; projection defaults use 1 for true-scale relief.

The site’s terrain is a baked ETOPO1 pyramid (zoom 0–4, plus curated regional tiles). Below it, /maps and the demos on these pages stream the Mapzen terrain tiles hosted on AWS (terrarium encoding, about 30 m over most land) down to zoom 12, from s3.amazonaws.com/elevation-tiles-prod, and credit them in the map attribution. A streamed tile that fails to load falls back to the baked pyramid.

map.addLayer({
type: "raster", source: provider,
classifier: GlyphMapClassifiers.etopo1V1, colors: terrainColors,
});

A raster source is a GlyphMapGeoTile or GlyphMapProvider. The widget selects detail from degrees per glyph cell and keeps coarser terrain while finer tiles load. One tile pyramid serves every projection.

SourceRequired data
GlyphMapGeoTilebounds, cols, rows, elevation: Float32Array, source, sampler
GlyphMapProviderid, zooms, bounds(z, x, y), async loadTile(z, x, y)

A tile’s grid is (cols + 1) × (rows + 1), row-major, north first; elevations are metres. Both sources accept attribution. Split tiles crossing east > 180 with splitGlyphMapGeoTileAtAntimeridian before projecting.

import {
GlyphMapClassifiers, glyphMapBreaks, glyphMapQuantile,
glyphMapEqualInterval, glyphMapLog,
} from "@glyphcss/maps";
GlyphMapClassifiers.etopo1V1;
glyphMapBreaks([0, 250, 800, 1500, 3000]);
glyphMapQuantile(9);
glyphMapEqualInterval(9, [-8000, 6000]);
glyphMapLog(9);

Supply one colour per band through colors. Quantile and log classifiers reject categorical fields. Classifier choice changes colour bands; projection exaggeration changes relief height.

map.addLayer({ type: "raster", source: provider, minElevation: 0 });
map.addLayer({ type: "contour", source: provider, levels: { interval: 500 }, minElevation: 0 });

minElevation / maxElevation are optional metre limits. Raster positions outside the window flatten to its edge; colours still read original elevations. Contours have their own window: match its floor to avoid burying lines under flattened terrain.

createGlyphMap(host, {
view, projection,
groundElevation: (lon, lat) => myDem.sample(lon, lat) ?? null,
layers: [],
});

Roads, draped fills, extrusions, and point layers normally follow mounted terrain, using the finest available tiles. groundElevation overrides that lookup; null or non-finite values use the datum. Without either source, features sit on the datum.

A custom lookup is read live, but cannot announce changes: move the camera or re-add the layer after loading data. Contours need a field/provider; a point lookup cannot supply them.

Loading terrain…

map.setSun({ mode: "manual", date: new Date("2026-06-21T12:00:00Z") });
map.setSun({ mode: "realtime" });
map.setSun({ mode: "off" });
map.setKeyLight("headlight"); // camera-following light

Sun lighting overrides the key light; headlight overrides your scene direction. getKeyLightDirection() reports the active direction. getSunDirection() and getSubsolarPoint() expose the sun position. Turning the sun off leaves the last light direction in place.

Sun optionDefault
mode"off"
dateNow; used in "manual" mode
tickMs30000; realtime updates
twilightDeg6
nightOpacity / nightColor0.72 / "#000000"
nightLevels4

On sheets, day/night shading needs useColors: true. keyLight defaults to "fixed"; headlight preserves relief while lighting the visible face. Set light intensity and ambient fill through scene.directionalLight / scene.ambientLight.

map.setShadow({ color: "#000000", opacity: 0.25 });
map.setShadow(null); // off, the default
  • fill-extrusion and model cast; those plus raster, fill, and heatmap receive.
  • Terrain never casts; stamped lines and contours do not receive shadows.
  • Direction follows the active scene light; headlight produces no visible shadows.
  • lift defaults to 0; large extra bias erases short-building shadows. Low sun angles can dither edges.
import { loadGlyphMapSource } from "@glyphcss/maps/node";
import {
glyphMapBounds, sampleGlyphMapField, classifyGlyphMapField,
compileGlyphMap, GlyphMapClassifiers,
} from "@glyphcss/maps";
const src = await loadGlyphMapSource({ path: "ETOPO1.asc.gz", id: "etopo1-2009" });
const view = glyphMapBounds({ west: 84.5, east: 86.5, south: 27.6, north: 28.8, cols: 140, rows: 48 });
const field = await sampleGlyphMapField(src, view, { sampler: "max" });
const bands = classifyGlyphMapField(field, GlyphMapClassifiers.etopo1V1);
const { html, css } = compileGlyphMap(bands, { ramp: " .:-=+*#%@", water: "~" });

This produces a frozen map without browser JavaScript. "max" preserves peaks; "mean" smooths them; "nearest" can alias. For a mesh without the widget, glyphMapPolygons(tile, projection, options) returns Polygon[]; keep resolution consistent across each tile level to avoid seams.