npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@geozelot/treelet

v0.1.0

Published

A lightweight 3D terrain and overlay map library using Three.js - this is Leaflet for 3D terrain

Readme

npm CI license gzip

Treelet

A lightweight 3D terrain & overlay map library for the web - inspired by Leaflet and powered by Three.js.

Treelet renders tiled elevation data as 3D terrain with drape overlays, LOD scheduling, and shader-based visualization - in the spirit of Leaflet's simple high-level API, solid low-level access, and zero bloat.

Live Demo

Features

  • Tile sources — XYZ, OGC WMS, OGC WMTS (RESTful + KVP)
  • Elevation decoding — Terrain-RGB, Terrarium, Mapbox, custom decoders
  • Visualization modes — Elevation, slope, aspect, contours, wireframe
  • Drape layers — Overlay satellite, OSM, or any raster imagery onto terrain
  • LOD scheduling — Nadir-based quadtree with cross-LOD seam resolution
  • Web Workers — Off-thread tile fetch, decode, and mesh generation
  • Fluent API — Chainable setters, Leaflet-style factory, EventEmitter

This had been a project related to my PHD and that I needed to keep private until now - thoroughly overhauled and elevated to current tech standards. 3D terrain rendering has become more commonly supported, but this library still shines with simplicity, ease-of-use and performance. It's especially useful for self-hosted elevation and overlay data.

Quick Start

Installation

npm install @geozelot/treelet three

CDN / Script Tag

For direct browser use without a bundler, use the standalone build which bundles Three.js:

<script src="https://unpkg.com/@geozelot/treelet/dist/treelet.cdn.standalone.js"></script>
<script>
  const map = Treelet.map('map', {
    center: { lng: 11.39, lat: 47.27 },
    zoom: 10,
  });
  // ...
  map.start();
</script>

Minimal Example

import { Treelet } from '@geozelot/treelet';

const map = Treelet.map('map', {
  center: { lng: 11.39, lat: 47.27 },
  zoom: 10,
});

map.addBaseLayer({
  id: 'terrain',
  name: 'AWS Terrain',
  source: {
    type: 'xyz',
    url: 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png',
    tileSize: 512,
    maxZoom: 15,
  },
  decoder: 'terrarium',
});

map.addDrapeLayer({
  id: 'osm',
  name: 'OpenStreetMap',
  source: {
    type: 'xyz',
    url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
    tileSize: 512,
    maxZoom: 19,
  },
});

map.start();

API Reference

Factory

Treelet.map(container, options)

Create a new Treelet map instance (static factory).

| Param | Type | Description | |---|---|---| | container | string \| HTMLElement | DOM element or element ID | | options | TreeletOptions | Map configuration |

Returns Treelet. Equivalent to new Treelet(container, options).

Treelet.version

Library version string (static).


TreeletOptions

| Option | Type | Default | Description | |---|---|---|---| | center | LngLat | required | Initial map center { lng, lat } | | zoom | number | required | Initial zoom level | | minZoom | number | 0 | Minimum allowed zoom | | maxZoom | number | 20 | Maximum allowed zoom | | tileSegments | number | 128 | Mesh subdivisions per tile | | worldScale | number | 40075.02 | CRS meters → scene units scale | | exaggeration | number | 1.0 | Vertical exaggeration factor | | elevationRange | [number, number] | [0, 4000] | Min/max elevation for color mapping | | contourInterval | number | 100 | Contour line spacing in meters | | antialias | boolean | true | WebGL antialiasing | | gui | boolean | true | Show compass + layer panel | | minPitch | number | 25 | Minimum pitch (most tilted) in degrees | | maxPitch | number | 90 | Maximum pitch (top-down) in degrees | | compassPosition | UICorner | 'top-right' | Compass widget corner | | layerPosition | UICorner | 'bottom-right' | Layer panel corner | | workerCount | number | navigator.hardwareConcurrency | Web Worker pool size |


Treelet

The main map class. Extends EventEmitter.

View

| Method | Returns | Description | |---|---|---| | setView(center, zoom) | this | Set map center and zoom | | getCenter() | LngLat | Get current map center | | getZoom() | number | Get current zoom level | | getBounds() | { sw, ne } \| null | Get visible bounding box | | getTileCount() | number | Get number of cached tiles |

Base Layers

Only one base layer is active at a time. The active base layer provides elevation data for the terrain mesh.

| Method | Returns | Description | |---|---|---| | addBaseLayer(options) | this | Add an elevation data layer | | removeBaseLayer(id) | this | Remove a base layer by ID | | setActiveBaseLayer(id) | this | Switch active base layer | | getBaseLayers() | BaseLayer[] | Get all registered base layers |

BaseLayerOptions

| Option | Type | Default | Description | |---|---|---|---| | id | string | required | Unique layer identifier | | name | string | required | Display name | | source | LayerSourceOptions | required | Tile source configuration | | decoder | DecoderType | 'terrain-rgb' | Elevation decoder | | decoderFn | CustomDecoderFn | — | Custom per-pixel decoder function | | exaggeration | number | 1.0 | Per-layer exaggeration multiplier | | active | boolean | true | Set as active on add |

Drape Layers

Drape layers overlay raster imagery on the terrain mesh. Only one drape (external or BaseDrape) is active at a time.

| Method | Returns | Description | |---|---|---| | addDrapeLayer(options) | this | Add an imagery overlay layer | | removeDrapeLayer(id) | this | Remove a drape layer by ID | | activateDrapeLayer(id) | this | Activate an external drape | | activateBaseDrape(mode?) | this | Switch to elevation-derived visualization | | setBaseDrapeMode(mode) | this | Change BaseDrape mode | | getBaseDrapeMode() | BaseDrapeMode | Get current BaseDrape mode | | getActiveDrapeId() | string \| null | Active drape ID ('__base_drape__' for BaseDrape) | | isBaseDrapeActive() | boolean | Check if BaseDrape is active | | setDrapeOpacity(id, opacity) | this | Set drape opacity (0–1) | | getDrapeOpacity(id) | number | Get drape opacity | | getDrapeLayers() | DrapeLayer[] | Get all registered drape layers |

DrapeLayerOptions

| Option | Type | Default | Description | |---|---|---|---| | id | string | required | Unique layer identifier | | name | string | required | Display name | | source | LayerSourceOptions | required | Tile source configuration | | opacity | number | 1.0 | Layer opacity | | blendMode | BlendMode | 'normal' | Blend mode: 'normal', 'multiply', 'screen' | | active | boolean | true | Set as active on add |

BaseDrapeMode

Elevation-derived visualization modes:

| Mode | Description | |---|---| | 'wireframe' | Mesh wireframe (normals-colored or flat white) | | 'elevation' | Elevation-colored with lighting | | 'slope' | Slope angle visualization | | 'aspect' | Cardinal direction coloring | | 'contours' | Elevation coloring with contour line overlay |

Visualization

| Method | Returns | Description | |---|---|---| | setColorRamp(ramp) | this | Set color ramp for current BaseDrape mode | | getColorRamp() | ColorRamp | Get current color ramp | | setExaggeration(value) | this | Set global vertical exaggeration | | getExaggeration() | number | Get exaggeration factor | | setContourInterval(meters) | this | Set contour line spacing | | getContourInterval() | number | Get contour interval | | setContourThickness(value) | this | Set contour line thickness | | getContourThickness() | number | Get contour thickness | | setContourColor(r, g, b) | this | Set contour color (RGB, 0–1 each) | | getContourColor() | [r, g, b] | Get contour color | | setWireframeWhite(white) | this | Toggle wireframe flat-white mode | | getWireframeWhite() | boolean | Get wireframe white state |

ColorRamp

| Ramp | Description | |---|---| | 'hypsometric' | Green → tan → brown → white (elevation default) | | 'viridis' | Purple → teal → yellow (perceptually uniform) | | 'inferno' | Black → purple → orange → white (high contrast) | | 'grayscale' | Black → white |

Camera

| Method | Returns | Description | |---|---|---| | setMinPitch(degrees) | this | Set minimum pitch (most tilted), 0–90 | | getMinPitch() | number | Get minimum pitch | | setMaxPitch(degrees) | this | Set maximum pitch (top-down), 0–90 | | getMaxPitch() | number | Get maximum pitch | | getCameraController() | CameraController | Direct camera access |

Lifecycle

| Method | Returns | Description | |---|---|---| | start() | this | Begin rendering and tile loading | | stop() | this | Pause rendering | | destroy() | void | Clean up all resources |

Events

map.on('ready', () => { /* initial tiles loaded */ });
map.on('move', ({ center }) => { /* camera moving */ });

| Event | Data | Description | |---|---|---| | 'ready' | {} | Initial tiles loaded | | 'zoom' | { zoom } | Zoom level changed | | 'move' | { center: LngLat } | Camera moving | | 'moveend' | { center, zoom } | Camera movement complete | | 'tileload' | { coord: TileCoord } | Tile loaded | | 'tileunload' | { coord: TileCoord } | Tile unloaded | | 'layeradd' | { id } | Layer added | | 'layerremove' | { id } | Layer removed | | 'layerchange' | {} | Layer configuration changed |


Tile Sources

All layers accept a source object conforming to LayerSourceOptions.

LayerSourceOptions

| Option | Type | Default | Used by | Description | |---|---|---|---|---| | type | 'xyz' \| 'wms' \| 'wmts' | required | all | Source type | | url | string | required | all | Endpoint URL or template | | subdomains | string[] | [] | XYZ, WMTS | Subdomain rotation via {s} | | tileSize | number | 256 | all | Tile pixel size | | maxZoom | number | 22 | all | Maximum zoom level | | attribution | string | '' | all | Attribution text | | layers | string | '' | WMS, WMTS | Layer name(s) | | format | string | 'image/png' | WMS, WMTS | Image format | | crs | string | 'EPSG:3857' | WMS | Coordinate reference system | | style | string | 'default' | WMTS | WMTS style | | tilematrixSet | string | 'EPSG:3857' | WMTS | WMTS TileMatrixSet |

XYZ Source

Standard slippy-map tiles with {z}/{x}/{y} URL placeholders.

source: {
  type: 'xyz',
  url: 'https://{s}.tile.example.com/{z}/{x}/{y}.png',
  subdomains: ['a', 'b', 'c'],
  tileSize: 256,
  maxZoom: 19,
}

WMS Source

OGC WMS GetMap requests. Computes BBOX from tile coordinates with CRS reprojection.

source: {
  type: 'wms',
  url: 'https://example.com/geoserver/wms',
  layers: 'elevation',
  format: 'image/png',
  crs: 'EPSG:4326',
  tileSize: 256,
}

Supported CRS values: EPSG:3857, EPSG:900913, EPSG:4326, CRS:84.

WMTS Source

OGC WMTS GetTile requests. Two modes, auto-detected:

RESTful — URL contains {z} placeholder (treated like XYZ):

source: {
  type: 'wmts',
  url: 'https://example.com/wmts/rest/dem/default/EPSG3857/{z}/{y}/{x}.png',
}

KVP — URL without placeholders (constructs GetTile query):

source: {
  type: 'wmts',
  url: 'https://example.com/wmts',
  layers: 'dem',
  tilematrixSet: 'EPSG:3857',
  style: 'default',
  format: 'image/png',
}

Elevation Decoders

Built-in decoders for common DEM tile formats:

| Decoder | Formula | Precision | |---|---|---| | 'terrain-rgb' | -10000 + (R×65536 + G×256 + B) × 0.1 | 0.1 m | | 'mapbox' | Alias for terrain-rgb | 0.1 m | | 'terrarium' | R×256 + G + B/256 - 32768 | ~0.004 m |

Custom Decoder

import { createCustomDecoder, registerDecoder } from '@geozelot/treelet';

// Inline: pass directly as decoderFn
map.addBaseLayer({
  id: 'dem',
  name: 'Custom DEM',
  source: { type: 'xyz', url: '...' },
  decoder: 'custom',
  decoderFn: (r, g, b, a) => r * 100 + g,
});

// Or register globally by name
registerDecoder('myformat', createCustomDecoder((r, g, b, a) => r * 100 + g));
map.addBaseLayer({
  id: 'dem',
  name: 'Custom DEM',
  source: { type: 'xyz', url: '...' },
  decoder: 'myformat' as any,
});

Types

interface LngLat { lng: number; lat: number }
interface WorldPoint { x: number; y: number }
interface TileCoord { z: number; x: number; y: number }
interface TileBounds { west: number; south: number; east: number; north: number }

type BaseDrapeMode = 'wireframe' | 'elevation' | 'slope' | 'aspect' | 'contours';
type ColorRamp = 'hypsometric' | 'viridis' | 'inferno' | 'grayscale';
type BlendMode = 'normal' | 'multiply' | 'screen';
type DecoderType = 'terrain-rgb' | 'mapbox' | 'terrarium' | 'custom';
type UICorner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';

Low-Level API

For advanced use, treelet re-exports its internal building blocks:

| Export | Description | |---|---| | Treelet | Main class with static Treelet.map() factory | | LayerSource | Abstract source base class | | createSource() | Factory: create a source from LayerSourceOptions | | XYZSource, WMSSource, WMTSSource | Concrete tile sources | | BaseLayer, DrapeLayer | Layer wrappers | | LayerRegistry | Layer management registry | | DrapeCompositor | Multi-resolution drape compositing | | SeamResolver | Cross-LOD edge seam resolution | | CameraController | Camera state + controls | | WebMercator | EPSG:3857 projection utilities | | TileGrid | Tile math (world ↔ tile coordinate conversion) | | EventEmitter | Base event system | | createTerrainMaterial() | Unified shader material factory | | terrainRGBDecoder, terrariumDecoder | Decoder instances | | createCustomDecoder(), registerDecoder() | Decoder registration |


Controls

| Input | Action | |---|---| | Left-click drag | Pan | | Scroll wheel | Zoom | | Right-click drag | Orbit (rotate + tilt) | | Compass click | Reset orientation |

Bundles

Treelet ships two minified bundles:

| File | Format | Three.js | Use case | |---|---|---|---| | treelet.es.js | ESM | External | Bundlers (Vite, Webpack, Rollup) | | treelet.cdn.standalone.js | IIFE | Included | <script> tag, CDN, no dependencies |

Slim (treelet.es.js) — ~140 KB minified. Three.js is a peer dependency; install it alongside treelet via npm. Best for projects that already use Three.js or want control over the Three.js version.

Standalone (treelet.cdn.standalone.js) — ~580 KB minified. Bundles only the Three.js modules treelet actually uses (tree-shaken). Drop-in ready for CDN or direct <script> tag usage — zero external dependencies.

Both bundles require the tileWorker asset file (dist/assets/tileWorker-*.js) to be served from the same origin.

Browser Support

Modern browsers with WebGL2 and Web Workers. Requires ES2020+.

What's next

  • compositional overlay blend modes
  • additional elevation-derived visualization modes
  • support for more sources and formats

License

MIT