# 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.

## Elevation sources

```ts
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.

| Source | Required data |
|---|---|
| `GlyphMapGeoTile` | `bounds`, `cols`, `rows`, `elevation: Float32Array`, `source`, `sampler` |
| `GlyphMapProvider` | `id`, `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.

## Classifiers

```ts
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.

## The terrain elevation window

```ts
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.

## Where the ground comes from

```ts
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.

## Lighting

```ts
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 option | Default |
|---|---|
| `mode` | `"off"` |
| `date` | Now; used in `"manual"` mode |
| `tickMs` | `30000`; realtime updates |
| `twilightDeg` | `6` |
| `nightOpacity` / `nightColor` | `0.72` / `"#000000"` |
| `nightLevels` | `4` |

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`.

## Cast shadows

```ts
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.

## Baking a static map

```ts
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.
