Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

Add a Stretchable Image to the Map

A stretchable image (nine-patch) allows certain regions to stretch while keeping corners and edges crisp. Use stretchX, stretchY, and content in map.addImage() to define which parts can stretch and where text is placed.

What is a Stretchable Image?

A stretchable image is similar to Android's nine-patch format. You define:

  • stretchX — pixel ranges along the X axis that can be stretched
  • stretchY — pixel ranges along the Y axis that can be stretched
  • content — the bounding box [left, top, right, bottom] where text/icon content is placed

This lets the image scale gracefully without distorting corners or borders.

Create and Register a Stretchable Image

javascript
const width = 60, height = 24;
const canvas = document.createElement('canvas');
canvas.width = width; canvas.height = height;
const ctx = canvas.getContext('2d');

// Draw a pill shape (rounded rect)
const r = height / 2;
ctx.fillStyle = '#3b82f6';
ctx.beginPath();
ctx.moveTo(r, 0);
ctx.arcTo(width, 0, width, height, r);
ctx.arcTo(width, height, 0, height, r);
ctx.arcTo(0, height, 0, 0, r);
ctx.arcTo(0, 0, width, 0, r);
ctx.closePath();
ctx.fill();

const imageData = ctx.getImageData(0, 0, width, height);

map.addImage('stretchable-pill', imageData, {
  stretchX: [[8, width - 8]],       // horizontal stretch zone
  stretchY: [[4, height - 4]],      // vertical stretch zone
  content: [8, 4, width - 8, height - 4], // where text goes
  pixelRatio: 1
});

Use with icon-text-fit

javascript
map.addLayer({
  id: 'labels',
  type: 'symbol',
  source: 'points',
  layout: {
    'icon-image': 'stretchable-pill',
    'icon-text-fit': 'both',             // 'none' | 'width' | 'height' | 'both'
    'icon-text-fit-padding': [4, 8, 4, 8], // padding inside the content box
    'icon-allow-overlap': true,
  }
});

stretchX / stretchY Format

javascript
// Array of [from, to] pixel ranges (0-indexed)
stretchX: [[8, 52]]    // pixels 8–52 can stretch horizontally
stretchY: [[4, 20]]    // pixels 4–20 can stretch vertically

// Multiple stretch zones
stretchX: [[4, 12], [48, 56]]

Complete Example

html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <link href="https://cdn.mapmetrics-atlas.net/versions/latest/mapmetrics-gl.css" rel="stylesheet" />
    <script src="https://cdn.mapmetrics-atlas.net/versions/latest/mapmetrics-gl.js"></script>
    <style>#map { height: 500px; width: 100%; }</style>
  </head>
  <body>
    <div id="map"></div>
    <script>
      const map = new mapmetricsgl.Map({
        container: 'map',
        style: '<StyleFile_URL_with_Token>',
        center: [10, 30],
        zoom: 2
      });

      map.on('load', () => {
        const w = 60, h = 24, r = h / 2;
        const canvas = document.createElement('canvas');
        canvas.width = w; canvas.height = h;
        const ctx = canvas.getContext('2d');
        ctx.fillStyle = '#3b82f6';
        ctx.beginPath();
        ctx.moveTo(r, 0);
        ctx.arcTo(w, 0, w, h, r); ctx.arcTo(w, h, 0, h, r);
        ctx.arcTo(0, h, 0, 0, r); ctx.arcTo(0, 0, w, 0, r);
        ctx.closePath(); ctx.fill();

        map.addImage('pill', ctx.getImageData(0, 0, w, h), {
          stretchX: [[8, w - 8]],
          stretchY: [[4, h - 4]],
          content: [8, 4, w - 8, h - 4]
        });

        map.addSource('cities', {
          type: 'geojson',
          data: {
            type: 'FeatureCollection',
            features: [
              { type: 'Feature', geometry: { type: 'Point', coordinates: [-74, 40.7] }, properties: { label: 'New York' } },
              { type: 'Feature', geometry: { type: 'Point', coordinates: [2.35, 48.85] }, properties: { label: 'Paris' } },
            ]
          }
        });

        map.addLayer({
          id: 'labels',
          type: 'symbol',
          source: 'cities',
          layout: {
            'icon-image': 'pill',
            'icon-text-fit': 'both',
            'icon-text-fit-padding': [4, 8, 4, 8],
            'icon-allow-overlap': true
          }
        });
      });
    </script>
  </body>
</html>
jsx
import React, { useEffect, useRef } from 'react';
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';

const citiesData = {
  type: 'FeatureCollection',
  features: [
    { type: 'Feature', geometry: { type: 'Point', coordinates: [-74, 40.7] }, properties: { label: 'New York' } },
    { type: 'Feature', geometry: { type: 'Point', coordinates: [2.35, 48.85] }, properties: { label: 'Paris' } },
    { type: 'Feature', geometry: { type: 'Point', coordinates: [139.7, 35.7] }, properties: { label: 'Tokyo' } },
  ]
};

const AddStretchableImage = () => {
  const mapContainer = useRef(null);
  const map = useRef(null);

  useEffect(() => {
    if (map.current) return;
    map.current = new mapmetricsgl.Map({
      container: mapContainer.current,
      style: '<StyleFile_URL_with_Token>',
      center: [10, 30],
      zoom: 2
    });

    map.current.on('load', () => {
      const w = 60, h = 24, r = h / 2;
      const canvas = document.createElement('canvas');
      canvas.width = w; canvas.height = h;
      const ctx = canvas.getContext('2d');
      ctx.fillStyle = '#3b82f6';
      ctx.beginPath();
      ctx.moveTo(r, 0);
      ctx.arcTo(w, 0, w, h, r); ctx.arcTo(w, h, 0, h, r);
      ctx.arcTo(0, h, 0, 0, r); ctx.arcTo(0, 0, w, 0, r);
      ctx.closePath(); ctx.fill();

      map.current.addImage('pill', ctx.getImageData(0, 0, w, h), {
        stretchX: [[8, w - 8]],
        stretchY: [[4, h - 4]],
        content: [8, 4, w - 8, h - 4]
      });

      map.current.addSource('cities', { type: 'geojson', data: citiesData });
      map.current.addLayer({
        id: 'labels',
        type: 'symbol',
        source: 'cities',
        layout: {
          'icon-image': 'pill',
          'icon-text-fit': 'both',
          'icon-text-fit-padding': [4, 8, 4, 8],
          'icon-allow-overlap': true
        }
      });
    });

    return () => { map.current?.remove(); map.current = null; };
  }, []);

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

export default AddStretchableImage;

For more information, visit the MapMetrics GitHub repository.