Skip to content

Maps

@glyphcss/maps turns geographic data into a glyphcss scene. It projects lon/lat to a 3D world, builds a real relief mesh from elevation tiles, mounts a MapLibre-shaped layer vocabulary on top, and hands the whole thing to one createGlyphScene — so a map is the same single <pre> write per frame as any other glyphcss render.

Terminal window
npm install @glyphcss/maps

There is no React or Vue binding. The package is imperative: you own the host element, and createGlyphMap returns a handle.

ImportContainsSafe in
@glyphcss/mapsEverything — projections, tiles, the relief mesh, the widget, layers, vector providersBrowser, Node, a worker
@glyphcss/maps/nodeThe root entry re-exported, plus loadGlyphMapSourceNode only

The root is pure: no fs, no native modules, nothing that assumes a filesystem. The /node subpath is the only place a filesystem reader lives, and the root never imports it, so bundling the root can never pull node:fs into a browser build.

gdal-async is deliberately not a dependency of either. The one reader that ships reads Esri/Arc-Info ASCII Grid.

Everything a map needs is a view (where you are looking) and a projection (how the world flattens). Layers are optional and can be added later.

import { createGlyphMap, glyphMapGlobe } from "@glyphcss/maps";
const host = document.querySelector<HTMLElement>("#map")!;
const map = createGlyphMap(host, {
view: { center: [0, 20], span: 140, cols: 140, rows: 63 },
projection: glyphMapGlobe({ exaggeration: 24 }),
layers: [{ type: "background", color: "#05070c" }],
});
map.on("click", ({ lngLat }) => console.log(lngLat));
map.fitBounds({ west: -74, east: -34, south: -56, north: 13 });

Drag to pan or orbit, wheel to zoom, Ctrl+drag (or right-drag) to tilt and turn. On touch: pinch to zoom, twist to turn, two fingers together to tilt. The widget owns one requestAnimationFrame motion loop and issues at most one render per displayed frame, so a gesture never queues renders it will throw away.

Call map.destroy() when you are done — it removes the scene’s DOM, every host listener, and any pending debounced tile fetch.

GlyphMapView is the whole camera state a consumer ever sets:

interface GlyphMapView {
center: readonly [lon: number, lat: number];
span: number; // degrees across the viewport
cols: number; // output grid width, in character columns
rows: number; // output grid height, in character rows
bounds?: GlyphMapBounds; // only on a view built by glyphMapBounds()
}

The vertical extent is derived from span * (rows / cols) — the aspect lock — so terrain can never shear. Grid and cell coordinates never appear in a public parameter or return value on their own; project/unproject are the two places they cross the boundary, and both are explicit about it:

const { col, row, visible } = map.project([-58.38, -34.6]);
const lngLat = map.unproject([70, 20]); // null off any projected surface

view.cols/view.rows are the authoritative grid shape. Pass autoSize: true to let the host element’s pixel size drive them instead.

createGlyphMap returns a GlyphMapHandle. The whole surface, grouped:

GroupMethods
ViewsetView, getView, getMaxSpan, fitBounds, flyTo, resize
CamerasetTilt, getTilt, getMaxTilt, setBearing, getBearing, setProjection
Street levelsetWalk, getWalk
LayersaddLayer, removeLayer, moveLayer, getContourFieldRange
LightingsetSun, getSun, getSunDirection, getSubsolarPoint, setKeyLight, getKeyLight, getKeyLightDirection, setShadow, getShadow
OverlaysaddMarker
Coordinatesproject, unproject
CreditgetAttributions
Eventson, off
Teardowndestroy

Plus two properties: host, and scene — the underlying GlyphSceneHandle. scene is the deliberate escape hatch: anything glyphcss can do that the map does not model (effect layers, charMode, the font atlas, direct camera reads) you reach through it.

map.scene.setOptions({ charMode: "braille" });

Scene options you want set from the start go through GlyphMapOptions.scene instead — they are merged under the widget’s own camera/cols/rows, so shading, colour encoding and shadows compose without fighting the map for the grid:

createGlyphMap(host, {
view, projection,
scene: { useColors: true, colorEncoding: "atlas", ambientLight: { intensity: 0.25 } },
});
map.on("load", () => {}); // construction-time layers finished their first load
map.on("move", ({ view }) => {}); // pan / flyTo / setView
map.on("zoom", ({ view }) => {});
map.on("click", ({ lngLat, originalEvent }) => {}); // lngLat is null off-surface
map.on("sun", ({ at, subsolar, direction }) => {}); // only while sun mode is on

off(type, handler) takes the same handler identity back.

Real map data comes with real licence terms. Every source in this package declares its own provenance at the type level, and the handle aggregates whatever the currently mounted layers carry:

for (const credit of map.getAttributions()) {
console.log(credit.name, credit.license, credit.url);
// → OpenStreetMap contributors ODbL https://www.openstreetmap.org/copyright
}
interface GlyphMapAttribution {
name: string;
url?: string;
license: string;
date?: string;
}

The list is recomputed per call over the live layer order, so it grows when you mount an OSM layer and shrinks when you remove it. Render it somewhere the reader can see. OpenStreetMap data is ODbL and requires the credit; OpenMapTiles is CC-BY 4.0. Nothing in this package hardcodes a credit string on a page, precisely so the displayed credit cannot drift from the data actually on screen.

  • Projections — the lon/lat contract, the four projections, and capabilities.
  • Layers — the full layer vocabulary and what each one takes.
  • Camera & Navigation — tilt, bearing, their gestures, flights, and street-level walk mode.
  • Terrain — elevation tiles, the relief mesh, draping, sun and shadows.
  • OpenStreetMap — streaming the planet through OpenFreeMap.

The live maps workbench drives every option on this page against real ETOPO1 terrain and OpenStreetMap tiles, and emits the createGlyphMap call for whatever you tuned.