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 stretchedstretchY— pixel ranges along the Y axis that can be stretchedcontent— 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.