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

mudlet-map-renderer

v2.6.1

Published

A rendering library for [Mudlet](https://www.mudlet.org/) map data. Takes Mudlet's JSON map format and renders interactive, zoomable maps using Konva, with SVG and PNG export support.

Readme

mudlet-map-renderer

A rendering library for Mudlet map data. Takes Mudlet's JSON map format and renders interactive, zoomable maps using Konva, with SVG and PNG export support.

Nuln area rendered with default settings

Install

yarn add mudlet-map-renderer

Quick start

import { MudletMapReader } from 'mudlet-map-binary-reader';
import { MapRenderer, MapReader, createSettings } from 'mudlet-map-renderer';

// Read a Mudlet binary map file and export renderer-compatible data
const map = MudletMapReader.read('map.dat');
const { mapData, colors } = MudletMapReader.export(map);

const mapReader = new MapReader(mapData, colors);

// Create an interactive renderer attached to a DOM element
const container = document.getElementById('map') as HTMLDivElement;
const renderer = new MapRenderer(mapReader, createSettings(), container);

// Display an area (area ID, z-level)
renderer.drawArea(42, 0);

// Show the player position (centers the viewport on the room)
renderer.setPosition(1234);

Usage

Settings

Create a settings object with createSettings() and customize before passing to the renderer:

import { createSettings } from 'mudlet-map-renderer';

const settings = createSettings();
settings.roomSize = 0.8;
settings.roomShape = 'circle';           // 'rectangle' | 'circle' | 'roundedRectangle'
settings.backgroundColor = '#1a1a2e';
settings.lineColor = 'rgb(200, 220, 255)';
settings.gridEnabled = true;
settings.emboss = true;
settings.areaName = true;

// Player position marker
settings.playerMarker.strokeColor = '#ff6600';
settings.playerMarker.fillColor = '#ff6600';
settings.playerMarker.fillAlpha = 0.3;
settings.playerMarker.matchRoomShape = true;

const renderer = new MapRenderer(mapReader, settings, container);

Settings are a shared mutable object. To change settings after the renderer is already running, modify the properties and call refresh():

// Change appearance at runtime
renderer.settings.roomShape = 'circle';
renderer.settings.emboss = true;
renderer.refresh();

// Background color has its own update method (CSS-only, no scene rebuild)
renderer.settings.backgroundColor = '#1a1a2e';
renderer.updateBackground();

Navigation

// Switch to a different area and z-level
renderer.drawArea(areaId, zIndex);

// Move player position (auto-switches area if needed)
renderer.setPosition(roomId);

// Move player without centering the viewport
renderer.setPosition(roomId, false);

// Update the marker without triggering area switch logic
renderer.updatePositionMarker(roomId);

// Center viewport on a room (with animation)
renderer.centerOn(roomId);

// Center instantly (no animation)
renderer.centerOn(roomId, true);

// Remove the position marker
renderer.clearPosition();

Highlights and paths

// Highlight rooms with a color
renderer.renderHighlight(roomId, '#ff0000');
renderer.renderHighlight(otherRoomId, 'rgba(0, 255, 0, 0.5)');

// Remove a specific highlight
renderer.removeHighlight(roomId);

// Check if a room is highlighted
if (renderer.hasHighlight(roomId)) { /* ... */ }

// Clear all highlights
renderer.clearHighlights();

// Render a path through a list of room IDs
renderer.renderPath([101, 102, 103, 104], '#66E64D');

// Clear all paths
renderer.clearPaths();

Viewport control

// Zoom
renderer.setZoom(1.5);
renderer.zoomToCenter(2.0);
console.log(renderer.getZoom());

// Fit the entire area in view
renderer.fitArea();

// Get current viewport bounds (in map coordinates)
const bounds = renderer.getViewportBounds();
// { minX, maxX, minY, maxY }

// Control resize behavior
renderer.centerOnResize = true;   // re-center on window resize
renderer.minZoom = 0.1;

Events

renderer.on('roomclick', ({ roomId, position }) => {
  console.log(`Clicked room ${roomId} at (${position.x}, ${position.y})`);
});

renderer.on('roomcontextmenu', ({ roomId, position }) => {
  // Right-click or long-press on a room
});

renderer.on('areaexitclick', ({ targetRoomId, position }) => {
  // Clicked an area exit — navigate to the target room's area
  renderer.setPosition(targetRoomId);
});

renderer.on('mapclick', () => {
  // Clicked on empty space
});

renderer.on('zoom', ({ zoom }) => {
  console.log(`Zoom level: ${zoom}`);
});

renderer.on('pan', (bounds) => {
  // Viewport moved
});

// Remove a listener
const handler = ({ roomId }) => { /* ... */ };
renderer.on('roomclick', handler);
renderer.off('roomclick', handler);

Culling

For large maps, spatial culling hides off-screen rooms for better performance:

// "indexed" (default) — bucket-based spatial index, best for large maps
// "basic" — simple bounds check per room
// "none" — render everything (useful for debugging)
renderer.setCullingMode('indexed');

Big maps (LOD + skeleton reader)

Very dense planes (tens of thousands to millions of rooms on one z-level) can't be drawn as one vector scene. Two core features combine to handle them:

Raster LOD overview (settings.lodEnabled, works with any reader): when the current plane holds more rooms than settings.lodRoomBudget (default 16 000) and the zoom is far enough out that a viewport could exceed that budget, the vector scene is replaced by a pixel overview — one filled box per room, painted on an underlay below the grid. Zooming in switches back to full vector detail. The switch is zoom-based (pan-invariant, no mid-pan flips) and reported through the lod renderer event ({mode: 'vector' | 'roomsOnly' | 'raster', planeRoomCount, visibleEstimate, hitTestActive}). Position marker, highlights, paths and overlays stay visible in both raster and roomsOnly modes.

Between settings.lodExitBudget (default 12 000) and lodRoomBudget, a third tier — roomsOnly — drops exit lines while rooms keep rendering as real vector shapes (env colour, border, shape): a bridge between full detail and the flat raster pixels. Exit pairing and exit-line shape building are typically the single largest share of a dense rebuild's cost (exit count often runs ~2x room count in a well-connected area), so this tier buys back most of that cost while still showing actual room geometry. Set lodExitBudget above lodRoomBudget (or to Infinity) to disable this tier and jump straight from full vector to raster.

Above settings.lodHitTestBudget (default 10 000 rooms, same unit as lodRoomBudget) the scene still renders at full vector detail (with or without exits) but the hit-test index is skipped, so clicks/hover stop resolving to a room until you zoom in a bit further — rebuilding that index is a real cost on a large scene, and precise pointer picking rarely matters that zoomed out. Rebuild-on-pan also pads the viewport generously (50% per side): panning inside that padding is free, so it buys a lot more distance between rebuilds without making any single rebuild bigger — the LOD decision compensates for the padding so the actual room count at each tier's flip stays pinned near its configured budget regardless.

SkeletonMapReader (mudlet-map-renderer/bigmap): an IMapReader over a compact typed-array MapSkeleton instead of a room object graph. Planes materialise only the rooms inside the current viewport; the interactive backend detects this (ViewportDataSource) and pushes padded camera bounds before every build, rebuilding on pan only when the camera leaves the padding.

import { MapRenderer, createSettings } from 'mudlet-map-renderer';
import { buildSkeleton, SkeletonMapReader } from 'mudlet-map-renderer/bigmap';

// Eager path: the map parses fine, it's the rendering that chokes.
const reader = new SkeletonMapReader(buildSkeleton(mapData, colors));
const settings = { ...createSettings(), lodEnabled: true };
const renderer = new MapRenderer(reader, settings, container);
renderer.on('lod', ({ mode, visibleEstimate }) => console.log(mode, visibleEstimate));

For maps too large to parse eagerly (hundreds of MB), produce the MapSkeleton out-of-process — e.g. a Web Worker that streams the binary map and transfers the typed arrays zero-copy. demo/streaming/ is a reference implementation. MapSkeleton is a stable seam: coordinates are raw Mudlet map space (the reader converts once at construction), bulk rooms live in parallel Int32Array columns, and only rooms with visual detail (symbols, custom lines, special exits…) are materialised in full.

Notes: exports through a viewport-narrowed reader only contain the applied viewport (call reader.setViewport(...) with larger bounds first if you need everything); lodEnabled is currently a no-op for the OffscreenCanvas backend; decorator styles with coordinate transforms (e.g. isometric) are not applied to the raster overview.

OffscreenCanvas rendering (Web Worker)

For large maps, an opt-in backend moves per-frame rasterisation into a Web Worker via OffscreenCanvas, keeping the main thread responsive during pan/zoom. The default Konva backend is unchanged — pass the worker backend to MapRenderer's backendFactory parameter:

import { MapRenderer, createSettings } from 'mudlet-map-renderer';
import { createOffscreenBackend } from 'mudlet-map-renderer/offscreen';

const renderer = new MapRenderer(
  mapReader,
  createSettings(),
  container,
  createOffscreenBackend(container), // ← opt in
);

Everything else (drawing, navigation, styles, highlights, paths, hit-testing, events) works as usual. Hit-testing and getDrawnExits() stay synchronous; the one caveat is that exportCanvas() returns undefined (use the headless exporters instead). Image labels, the ambient-light overlay, and live effects are all supported.

See docs/offscreen-rendering.md for the full guide — architecture, feature support, limitations, benchmarks, and the API.

Styles

A Style is a target-agnostic visual transformer. One style drives the interactive canvas and every exporter — set it once and it applies to SVG, PNG, and anything else you export.

import {
  compose, identityStyle,
  Parchment, Blueprint, Neon, Sketchy, Isometric,
} from 'mudlet-map-renderer';

// Single style
renderer.setStyle(Parchment);

// Compose a chain (left → right = inner → outer)
renderer.setStyle(compose(
  Parchment,
  Sketchy({ jitter: 0.012, color: '#4a3728' }),
  Isometric({ rotation: 30, depth: 0.18 }),
));

// Clear the current style
renderer.setStyle(identityStyle);
// or
renderer.clearStyle();

Built-in styles:

| Style | Effect | |---|---| | Parchment | Warm sepia / aged-paper palette | | Blueprint | White lines on deep blue | | Neon | Glowing neon outlines on dark background | | Construction | Safety-yellow hazard stripes on asphalt, orange exits | | SciFi | Holographic cyan glow on void black | | StainedGlass | Saturated jewel panes framed by fat near-black leading | | GraphPaper | Pale rooms inked in navy — old-school D&D graph paper | | Topographic | Earthy elevation palette with concentric contour rings | | Sketchy({ jitter, color }) | Hand-drawn pencil wobble | | Watercolor({ bleed?, layers?, alpha? }) | Translucent edge-bled washes that pool on overlap | | Isometric({ rotation?, depth? }) | 2:1 iso projection with optional cubes | | GradientRooms(opts?) | Replace flat fills with a vertical gradient |

Custom styles extend BaseStyle<Inner> and override only the draw calls they transform — see the built-ins for examples.

Export

Exporters are plug-ins: a new output format is a new Exporter<T> class, not a new MapRenderer method.

import {
  SvgExporter, PngExporter, PngBlobExporter, CanvasExporter,
} from 'mudlet-map-renderer';

// SVG (string)
const svg         = renderer.export(new SvgExporter());
const svgCentered = renderer.export(new SvgExporter({ roomId: 1234, padding: 5 }));
const svgTyped    = renderer.export(new SvgExporter({
  overlays: {
    position: { roomId: 1234 },
    highlights: [{ roomId: 100, color: '#ff0000' }],
    paths: [{ locations: [101, 102, 103], color: '#00ff00' }],
  },
}));

// PNG data URL
const pngUrl = renderer.export(new PngExporter({ pixelRatio: 2 }));

// PNG as Blob
const blob = await renderer.export(new PngBlobExporter({ pixelRatio: 2 }));

// Headless PNG bytes at a specific size (portable — works in browser + Node)
const png = renderer.export(new PngBytesExporter({
  width: 1920,
  height: 1080,
  roomId: 1234,
  padding: 5,
}));
// fs.writeFileSync('out.png', png!);            // Node
// new Blob([png!], { type: 'image/png' });      // Browser

// Canvas handle (if you need to draw more on it, attach to DOM, etc.)
const canvas = renderer.export(new CanvasExporter({
  width: 1920,
  height: 1080,
}));

Style + export compose: the style currently applied with setStyle is passed to every exporter, so the SVG, the PNG, and the on-screen canvas stay in sync.

Writing a new exporter — e.g. PDF — means shipping a class that implements Exporter<Uint8Array>. No changes to MapRenderer.

Headless rendering (no DOM)

For server-side or offscreen rendering (Node.js), install canvas alongside this package and import Konva's canvas backend once at startup. canvas is a peer dependency — it isn't shipped transitively because browsers don't need it.

yarn add canvas
import "konva/canvas-backend";   // must run before creating a MapRenderer
import { MapRenderer, createSettings, SvgExporter, PngBytesExporter } from "mudlet-map-renderer";

// Omit the container argument for headless rendering.
const renderer = new MapRenderer(mapReader, createSettings());

renderer.drawArea(42, 0);
renderer.state.positionRoomId = 1234;  // mark player position without auto-centering

const svg = renderer.export(new SvgExporter({ padding: 5 }));
const png = renderer.export(new PngBytesExporter({ width: 1920, height: 1080 }));

SVG export doesn't actually hit Konva, so if you only need SVG you can skip both the canvas install and the konva/canvas-backend import. PNG / canvas exporters and anything that builds a Konva stage do require them.

Overlays

Two kinds, picked by what you need:

  • SceneOverlay — target-agnostic, appears in every output (interactive canvas + every exporter). Use for static scene content (badges, annotations).
  • LiveEffect — interactive canvas only; receives a Konva layer and viewport updates so it can run its own animation loop. Skipped by exporters.
import type { SceneOverlay, LiveEffect } from 'mudlet-map-renderer';

// Scene overlay — emits target-agnostic Shapes; appears in every output
class BadgeOverlay implements SceneOverlay {
  render(state, bounds) {
    return { type: 'circle', cx: 5, cy: 5, radius: 0.4, paint: { fill: '#ff0' } };
  }
}

renderer.addSceneOverlay('badge', new BadgeOverlay());
renderer.removeSceneOverlay('badge');

// Live effect — gets a Konva layer for animation; interactive canvas only
class Pulse implements LiveEffect {
  attach(layer) { /* add Konva shapes */ }
  updateViewport(bounds, scale) { /* react to pan/zoom */ }
  destroy() { /* cleanup */ }
}

renderer.addLiveEffect('pulse', new Pulse());
renderer.removeLiveEffect('pulse');

Cleanup

Call destroy() when you're done with a renderer to release all resources — DOM event listeners, Konva stages, and internal subscriptions:

const renderer = new MapRenderer(mapReader, settings, container);

// ... use the renderer ...

// Tear down completely
renderer.destroy();

This is important in SPAs and frameworks like React where components mount and unmount. Without calling destroy(), event listeners on window and the container element will leak.

React example

import { useEffect, useRef } from 'react';
import { MapRenderer, MapReader, createSettings } from 'mudlet-map-renderer';

function MudletMap({ mapData, envData, areaId, roomId }) {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!containerRef.current) return;

    const mapReader = new MapReader(mapData, envData);
    const renderer = new MapRenderer(mapReader, createSettings(), containerRef.current);

    renderer.drawArea(areaId, 0);
    if (roomId) renderer.setPosition(roomId);

    return () => {
      renderer.destroy();
    };
  }, [mapData, envData, areaId, roomId]);

  return <div ref={containerRef} style={{ width: '100%', height: '100%' }} />;
}

Pathfinding

import { MapReader, PathFinder, MapGraph, computePathData } from 'mudlet-map-renderer';

const mapReader = new MapReader(mapData, envData);
const pathFinder = new PathFinder(mapReader);

// Find shortest path between two rooms (Dijkstra or A*)
const roomIds = pathFinder.findPath(startRoomId, endRoomId, 'astar');

if (roomIds) {
  // Render the path on the map
  renderer.renderPath(roomIds, '#66E64D');

  // Or compute detailed path geometry for custom rendering
  const pathData = computePathData(mapReader, renderer.settings, roomIds, '#66E64D');
}

Exploration mode

Show only rooms the player has visited (fog of war):

const mapReader = new MapReader(mapData, envData);

// Enable exploration with a set of visited room IDs
const visitedRooms = mapReader.decorateWithExploration(new Set([100, 101, 102, 200]));

// Add newly visited rooms as the player explores
visitedRooms.add(103);

// Areas automatically filter to only show visited rooms
const renderer = new MapRenderer(mapReader, createSettings(), container);
renderer.drawArea(areaId, 0);

// Disable exploration mode (show all rooms again)
mapReader.clearExplorationDecoration();
renderer.refresh();

Map data format

The renderer expects data produced by mudlet-map-binary-reader, which reads Mudlet's binary map.dat format and exports renderer-compatible structures:

import { MudletMapReader } from 'mudlet-map-binary-reader';

const map = MudletMapReader.read('map.dat');
const { mapData, colors } = MudletMapReader.export(map);
// mapData: MapData.Map (Area[])
// colors:  MapData.Env[]

MapReader accepts these two values directly:

const mapReader = new MapReader(mapData, colors);

The binary reader can also write exported data to files for static use:

MudletMapReader.export(map, 'output');  // writes .js files
MudletMapReader.exportJson(map, 'map.json');

API reference

MapRenderer

| Method | Description | |--------|-------------| | drawArea(id, zIndex) | Display an area at a z-level | | setPosition(roomId, center?) | Set player position (auto-switches area) | | updatePositionMarker(roomId) | Update marker without area switch | | clearPosition() | Remove position marker | | centerOn(roomId, instant?) | Center viewport on a room | | renderHighlight(roomId, color) | Highlight a room | | removeHighlight(roomId) | Remove a highlight | | clearHighlights() | Clear all highlights | | renderPath(roomIds, color?) | Draw a path | | clearPaths() | Clear all paths | | setZoom(zoom) | Set zoom level | | zoomToCenter(zoom) | Zoom keeping center fixed | | fitArea() | Fit the full area in view | | setStyle(style) | Apply a visual style (interactive + exporters) | | clearStyle() | Remove the current style | | export(exporter) | Run an Exporter<T> and return its output | | addSceneOverlay(id, overlay) | Add a target-agnostic overlay | | removeSceneOverlay(id) | Remove a scene overlay | | addLiveEffect(id, effect) | Add an interactive-only animated effect | | removeLiveEffect(id) | Remove a live effect | | refresh() | Force a full re-render | | on(event, handler) | Subscribe to an event | | off(event, handler) | Unsubscribe from an event | | setCullingMode(mode) | Set culling strategy | | destroy() | Release all resources |

Exporters

| Exporter | Output | |---|---| | SvgExporter({ roomId?, padding?, overlays? }) | string — SVG document | | PngExporter({ pixelRatio? }) | string — PNG data URL (current viewport) | | PngBlobExporter({ pixelRatio? }) | Promise<Blob> — PNG Blob (browser only) | | PngBytesExporter({ width, height, roomId?, padding?, overlays?, mimeType?, quality? }) | Uint8Array — headless PNG/JPEG bytes; portable (browser + Node) | | CanvasExporter({ width, height, roomId?, padding?, overlays? }) | ExportCanvas — canvas handle; reframes to fit |

Styles

| Style | Constructor / Usage | |---|---| | Parchment | setStyle(Parchment) | | Blueprint | setStyle(Blueprint) | | Neon | setStyle(Neon) | | Construction | setStyle(Construction) | | SciFi | setStyle(SciFi) | | StainedGlass | setStyle(StainedGlass) | | GraphPaper | setStyle(GraphPaper) | | Topographic | setStyle(Topographic) | | Sketchy(opts) | setStyle(Sketchy({ jitter, color })) | | Watercolor(opts?) | setStyle(Watercolor({ bleed?, layers?, alpha? })) | | Isometric(opts) | setStyle(Isometric({ rotation?, depth? })) | | GradientRooms(opts?) | setStyle(GradientRooms()) | | compose(...) | Chain multiple styles into one | | identityStyle | Pass-through; equivalent to clearStyle() |

PathFinder

| Method | Description | |--------|-------------| | findPath(from, to, algorithm?) | Find shortest path ('dijkstra' or 'astar') |

MapReader

| Method | Description | |--------|-------------| | getArea(areaId) | Get an area by ID | | getAreas() | Get all areas | | getRoom(roomId) | Get a room by ID | | getRooms() | Get all rooms | | decorateWithExploration(visited?) | Enable fog of war | | clearExplorationDecoration() | Disable fog of war |

License

MIT