Maplibre gl emoji symbols
Maplibre gl renders maps using WebGL, which provides better performance for large datasets than html/svg markers. However, this WebGL rendering strategy doesn’t support complex text rendering for special characters like emojis 🤡. So I made an emoji sprite sheet for maplibre.
If you’d like to skip to the end, here’s a sprite map of all 4,293 openmoji emojis at the time of this writing.
Sprite Generation
First download openmoji, and extract the color/svg
folder to and svg
sub directory your working directory. If you don’t have node installed, install it with a tool like volta and then
npm install --save-dev sharp canvas
Here’s the program you can copy into a generate-emoji-sprite.ts
script:
import fs from "fs";import path from "path";import sharp from "sharp";import { createCanvas, ImageData } from "canvas";
const OUTPUT_DIR = path.join(process.cwd(), "sprite");const SVG_DIR = path.join(OUTPUT_DIR, "openmoji");
// Ensure directories existif (!fs.existsSync(OUTPUT_DIR)) { fs.mkdirSync(OUTPUT_DIR, { recursive: true });}if (!fs.existsSync(SVG_DIR)) { fs.mkdirSync(SVG_DIR, { recursive: true });}
interface SpriteData { x: number; y: number; width: number; height: number; pixelRatio: number;}
async function generateSprites(svgFiles: string[], outputPrefix: string) { console.log(`Generating ${outputPrefix} sprite maps...`);
// Calculate grid size const gridSize = Math.ceil(Math.sqrt(svgFiles.length)); const spriteSize = 64; const padding = 2; const canvasSize = gridSize * (spriteSize + padding);
// Create canvas for sprite const canvas = createCanvas(canvasSize, canvasSize); const ctx = canvas.getContext("2d");
// Create canvas for retina sprite const retinaCanvas = createCanvas(canvasSize * 2, canvasSize * 2); const retinaCtx = retinaCanvas.getContext("2d");
// Process each SVG const spriteData: Record<string, SpriteData> = {}; const spriteData2x: Record<string, SpriteData> = {};
for (let i = 0; i < svgFiles.length; i++) { const svgFile = svgFiles[i]; const emojiCode = path.basename(svgFile, ".svg");
// Calculate position in grid const row = Math.floor(i / gridSize); const col = i % gridSize; const x = col * (spriteSize + padding); const y = row * (spriteSize + padding);
// Read and resize SVG const svgBuffer = fs.readFileSync(svgFile); const resized = await sharp(svgBuffer) .resize(spriteSize, spriteSize) .toBuffer();
// Composite onto sprite const image = await sharp(resized).raw().toBuffer(); ctx.putImageData( new ImageData(new Uint8ClampedArray(image), spriteSize, spriteSize), x, y );
// Composite onto retina sprite const resized2x = await sharp(svgBuffer) .resize(spriteSize * 2, spriteSize * 2) .toBuffer(); const image2x = await sharp(resized2x).raw().toBuffer(); retinaCtx.putImageData( new ImageData( new Uint8ClampedArray(image2x), spriteSize * 2, spriteSize * 2 ), x * 2, y * 2 );
// Store sprite data spriteData[emojiCode] = { x, y, width: spriteSize, height: spriteSize, pixelRatio: 1, };
spriteData2x[emojiCode] = { x: x * 2, y: y * 2, width: spriteSize * 2, height: spriteSize * 2, pixelRatio: 2, }; }
// Save sprite files const spriteBuffer = canvas.toBuffer("image/png"); const sprite2xBuffer = retinaCanvas.toBuffer("image/png");
fs.writeFileSync(path.join(OUTPUT_DIR, `${outputPrefix}.png`), spriteBuffer); fs.writeFileSync( path.join(OUTPUT_DIR, `${outputPrefix}@2x.png`), sprite2xBuffer );
// Save sprite data fs.writeFileSync( path.join(OUTPUT_DIR, `${outputPrefix}.json`), JSON.stringify(spriteData, null, 2) ); fs.writeFileSync( path.join(OUTPUT_DIR, `${outputPrefix}@2x.json`), JSON.stringify(spriteData2x, null, 2) );
console.log(`${outputPrefix} sprite generation complete!`);}
async function main() { try { // Get all SVG files const allSvgFiles = fs .readdirSync(SVG_DIR) .filter((file) => file.endsWith(".svg")) .map((file) => path.join(SVG_DIR, file));
console.log(`Found ${allSvgFiles.length} SVG files`);
// Generate sprite sheet await generateSprites(allSvgFiles, "sprite"); } catch (error) { console.error("Error:", error); process.exit(1); }}
main();
and then you can
ts-node generate-emoji-sprite.ts
This script generates png sprites at two resolutions sheet and the JSON metadata files that MapLibre uses to determine the coordinates of each emoji within the sprite.
Implementing in React with MapLibre
Here’s how I’m using the spritesheet generated by that script in react:
import Map, { Layer, Source, type StyleSpecification,} from "react-map-gl/maplibre";import "maplibre-gl/dist/maplibre-gl.css";import { useEffect, useState } from "react";
export function ListMapGL(geoJSON: GeoJSON.FeatureCollection) { const [mapStyle, setMapStyle] = useState<StyleSpecification | undefined>();
useEffect(() => { fetch( "https://api.maptiler.com/maps/streets/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL" ) .then((res) => res.json()) .then((json) => { setMapStyle({ ...json, sprite: `${window.location.origin}/assets/sprite/sprite-popular`, }); }); }, []);
return ( <> {mapStyle ? ( <Map initialViewState={{ latitude: 34, longitude: -118, zoom: 10, }} projection="mercator" mapStyle={mapStyle} > <Source id="places" type="geojson" data={geoJSON} generateId> <Layer id="place-symbols" type="symbol" layout={{ "icon-image": ["get", "emoji"], "icon-size": 0.36, "icon-allow-overlap": true, "icon-ignore-placement": true, }} /> </Source> </Map> ) : null} </> );}
Using the emoji sprites in react
This gets us emojis in the map, but if we want these emojis to match in the rest of the app, we need a react emoji rendering plan. We could load in svgs, or use the sprite sheet. I opted for the spritesheet for the performance boost. Here’s a component for rendering these sprites in react:
interface SpriteData { x: number; y: number; width: number; height: number; pixelRatio: number;}
interface SpriteMap { [key: string]: SpriteData;}
import spriteData from "path/to/sprite-popular.json";import spriteData2x from "path/to/sprite-popular@2x.json";
export function EmojiSprite({ emojiCode, scale = 0.5,}: { emojiCode: string; className?: string; scale?: number;}) { const sprite = (spriteData as SpriteMap)[emojiCode]; const sprite2x = (spriteData2x as SpriteMap)[emojiCode];
if (!sprite || !sprite2x) { return null; }
return ( <div style={ { backgroundImage: "url('/assets/sprite/sprite-popular.png')", backgroundPosition: `-${sprite.x}px -${sprite.y}px`, width: `${sprite.width}px`, height: `${sprite.height}px`, transform: `scale(${scale})`, transformOrigin: "top left", } as React.CSSProperties } /> );}
I wouldn’t recommend using the full 4k emojis since that’s a lot of megabytes, but you can pick your subset. Happy emoji mapping!