Skip to content

Repository files navigation

map-sprite

Framework-independent TypeScript utilities for generating MapLibre / Mapbox sprite assets from SVG icons.

The package includes a small React example, but the core library does not depend on React.

Features

  • Parse SVG text into icon inputs.
  • Reject active or externally referenced SVG content at the import boundary.
  • Normalize SVG file names into map icon IDs.
  • Pack icons with maxrects-packer.
  • Generate MapLibre / Mapbox-compatible sprite.json.
  • Render transparent sprite.png in the browser.
  • Export map-sprite.zip contents with both normal and retina assets:
    • sprite.png
    • sprite.json
    • sprite@2x.png
    • sprite@2x.json

Install

npm install map-sprite

Basic Usage

import { createSprite, exportSpriteZip, parseSvgText } from "map-sprite";

const svgText = '<svg width="32" height="32" viewBox="0 0 32 32"></svg>';
const icon = parseSvgText(svgText, "Gas Valve.svg");
const sprite = createSprite([icon]);

console.log(sprite.json);

const zipBlob = await exportSpriteZip(sprite);

MapLibre / Mapbox Style Usage

Serve the generated sprite files over HTTP and set the style sprite field to the base URL without a file extension:

const style = {
  version: 8,
  sprite: "https://example.com/sprites/demo/sprite",
  sources: {
    test: {
      type: "geojson",
      data: {
        type: "FeatureCollection",
        features: [
          {
            type: "Feature",
            geometry: {
              type: "Point",
              coordinates: [116.397, 39.908]
            },
            properties: {
              icon: "gas-valve"
            }
          }
        ]
      }
    }
  },
  layers: [
    {
      id: "test-icon",
      type: "symbol",
      source: "test",
      layout: {
        "icon-image": ["get", "icon"],
        "icon-size": 1,
        "icon-allow-overlap": true
      }
    }
  ]
};

MapLibre / Mapbox automatically request the matching files:

https://example.com/sprites/demo/sprite.json
https://example.com/sprites/demo/sprite.png
https://example.com/sprites/demo/sprite@2x.json
https://example.com/sprites/demo/sprite@2x.png

The icon-image value must match a key in sprite.json, not the original SVG file name.

SVG Input Safety

parseSvgText is the intended import boundary for user-provided SVG files. It stores the original SVG text for rendering, but rejects SVG content that can execute code or fetch external resources:

  • <script> elements
  • event handler attributes such as onload
  • <foreignObject> HTML embedding
  • href / xlink:href values that start with http:, https:, data:, or javascript:
parseSvgText('<svg width="16" height="16"><script>alert(1)</script></svg>', "bad.svg");
// throws: Unsafe SVG content in "bad.svg".

If you bypass parseSvgText and construct SvgIconInput objects manually, validate or trust the SVG text before passing it to createSprite, renderSpritePng, exportSpriteZip, or MapSpriteEditor.

DOM Editor Usage

The package also exposes a DOM-mounted editor, similar to map SDK initialization:

import { MapSpriteEditor } from "map-sprite";

const editor = new MapSpriteEditor({
  container: document.getElementById("sprite-editor")!,
  logic: "max-edge",
  padding: 2,
  preserveOrder: true,
  themeColor: "#3fb572",
  onChange({ sprite }) {
    console.log(sprite.json);
  }
});

editor.setThemeColor("#2f9e6a");

// Later, when unmounting:
editor.destroy();

The editor owns its UI and supports SVG upload, drag-and-drop import, icon order reordering, icon rotation, layout strategy switching, gap configuration, theme color switching, transparent checkerboard preview, JSON preview, and ZIP export.

The editor also owns its icon array internally. Constructor icons, setIcons(icons), getState().icons, and onChange({ icons }) use array copies so callers cannot mutate editor state by keeping a reference to a previously passed or returned array.

Editor layout modes:

  • max-edge and max-area use maxrects-packer; icon dragging is disabled because positions are generated by the packer. Toggle Keep order to preserve import order or let the packer search compact layout candidates.
  • custom freezes the current positions and enables dragging. Dropping one icon on another swaps those two icons in the order, then recalculates custom positions with the configured gap without using maxrects-packer.

Switch packing logic when creating a sprite:

createSprite(icons, { logic: "max-edge" }); // default
createSprite(icons, { logic: "max-area" });

Set spacing between SVG icons:

createSprite(icons, { padding: 4 });

Rotate an icon in 90-degree steps before packing:

const rotatedIcon = {
  ...icon,
  rotation: 90
} as const;

const sprite = createSprite([rotatedIcon]);
console.log(sprite.json[rotatedIcon.name].width); // uses the rotated bounding box

Preserve the caller-provided icon order instead of letting addArray sort inputs first:

createSprite(icons, { preserveOrder: true });

exportSpriteZip includes normal and @2x assets by default. To export only one form:

await exportSpriteZip(sprite, { includeNormal: true, includeRetina: false });
await exportSpriteZip(sprite, { includeNormal: false, includeRetina: true });

Core API

normalizeIconName(fileName: string): string
parseSvgText(svgText: string, fileName: string): SvgIconInput
createSprite(icons: SvgIconInput[], options?: SpriteOptions): SpriteResult
renderSpritePng(sprite: SpriteResult, options?: RenderSpriteOptions): Promise<Blob>
exportSpriteZip(sprite: SpriteResult, options?: ExportSpriteZipOptions): Promise<Blob>
createSpriteJson(icons: PackedIcon[], pixelRatio?: 1 | 2): SpriteJson
createRetinaSpriteJson(icons: PackedIcon[]): SpriteJson
resolveSpriteOptions(options?: SpriteOptions): Required<SpriteOptions>

Default packing options:

{
  maxWidth: 1024,
  maxHeight: 1024,
  padding: 2,
  border: 1,
  smart: true,
  pot: false,
  square: false,
  allowRotation: false,
  logic: "max-edge",
  preserveOrder: false
}

Packer-level rotation is always disabled because automatic rectangle rotation would desynchronize the rendered icon from downstream map usage. Per-icon rotation is supported in 90-degree steps and is reflected in the rendered PNG and generated JSON bounds.

Packing logic options:

  • "max-edge": sorts by longest edge and picks tighter edge fits. This is the default.
  • "max-area": sorts by area and picks lower wasted-area fits.

When preserveOrder is false, packing tries multiple deterministic order and width candidates, then keeps the single-sprite result with the smallest area.

React Example

Run the included example:

npm install
npm run dev

The example supports SVG upload, drag-and-drop import, layout mode switching, gap changes, theme color changes, transparent checkerboard canvas preview, click selection, custom-mode reorder dragging, rotation, deletion, JSON preview, and ZIP export.

It also includes a MapLibre Test view. Upload SVG icons in Sprite Editor first, then switch to MapLibre Test. The test view uses the current editor onChange output, renders sprite.png, sprite.json, sprite@2x.png, and sprite@2x.json from that same SpriteResult, serves them through the local Vite dev server, sets the MapLibre style sprite URL to that HTTP endpoint, and renders the icons through a real symbol layer.

Development

npm run format:check
npm run format
npm test
npm run typecheck
npm run build

npm run build uses vite.lib.config.ts for the distributable library bundle and tsconfig.build.json for declaration output. The default vite.config.ts is kept for the React example and Vitest environment.

The ZIP export test uses an injected PNG renderer so core ZIP behavior can be verified in Node without a real browser canvas.

Changelog

See CHANGELOG.md for release notes.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages