Emoji to image with Canvas2D

Let’s say you want to display an emoji as an image (for instance, to render in WebGL). You can achieve this using your device’s existing font by painting the emoji on an HTML canvas and retrieving its image data.

Note

It’s more practical to read emoji images from a shared resource such as https://github.com/benborgers/emojicdn as to not rely on device-specific fonts.

But in a pinch, it’s worth knowing how to produce these images on-device.

Text can be painted on an HTML canvas using .fillText()

      const canvas = document.createElement("canvas")
document.body.appendChild(canvas)

const ctx = canvas.getContext("2d")
ctx.textBaseline = "top"
ctx.fillText("Hello World", 0, 0)

    

This works for emoji too. Our goal is to draw an emoji character centered on a square canvas.

      const size = 100
canvas.width = size
canvas.height = size

ctx.textBaseline = "middle"
ctx.textAlign = "center"
ctx.font = `${size}px sans-serif`
ctx.fillText("😄", size / 2, size / 2)

    

Despite .textBaseline and .textAlign seemingly configured to center the text, there is a vertical offset.

One workaround is to apply a fudge factor to offset the height. A square character like ✅ helps to align all the edges. For me, adjusting the height by a factor of 0.125 looks close enough.

      ctx.fillText("😄", size / 2, (size * 1.125) / 2)

    

Another approach is to use .measureText() to measure the bounding box of the text. The interpretation of these values is a little alien to me, but the MDN docs provide helpful visual examples.

The following computes pixel distances to the center of the text (measured from the configured alignment points) and uses them to offset the text from the center of the canvas.

      const {
	actualBoundingBoxRight,
	actualBoundingBoxLeft,
	actualBoundingBoxDescent,
	actualBoundingBoxAscent,
} = ctx.measureText(text)

const centerX = 0.5 * (actualBoundingBoxRight - actualBoundingBoxLeft)
const centerY = 0.5 * (actualBoundingBoxDescent - actualBoundingBoxAscent)

ctx.fillText(emoji, size / 2 - centerX, size / 2 - centerY)

    

The result is still looks slightly off but close enough for most characters, and at least this way we aren’t hardcoding offset factors.

Lastly call .toDataURL() to turn the canvas pixel data into a base64 encoded data URL for consumption by <img> or other image loaders.

Complete solution

      /**
 * Usage:
 * img.src = characterToDataURL("💯")
 */
function characterToDataURL(text, size = 64) {
	const canvas = document.createElement("canvas")
	canvas.width = size
	canvas.height = size

	const ctx = canvas.getContext("2d")
	ctx.font = `${size}px sans-serif`

	const {
		actualBoundingBoxRight,
		actualBoundingBoxLeft,
		actualBoundingBoxDescent,
		actualBoundingBoxAscent,
	} = ctx.measureText(text)

	const centerX = 0.5 * (actualBoundingBoxRight - actualBoundingBoxLeft)
	const centerY = 0.5 * (actualBoundingBoxDescent - actualBoundingBoxAscent)

	ctx.fillText(text, size / 2 - centerX, size / 2 - centerY)

	return canvas.toDataURL()
}

    

Or a more fun example 💩