Wide Gamut 2D Graphics using HTML Canvas

Most colors used on the Web today are sRGB colors. These are the colors that you specify with the familiar #rrggbb and rgb(r, g, b) CSS syntax, and whose individual color components are given as values in the range [0, 255]. For example, rgb(255, 0, 0) is the most saturated, pure red in the sRGB color space. But the range of colors in sRGB — its color gamut — does not encompass all colors that can be perceived by the human visual system, and there are displays that can produce a broader range of colors.

sRGB is based on the color capabilities of computer monitors that existed at the time of its standardization, in the late 1990s. Since then, other, wider gamut color spaces have been defined for use in digital content, and which cover more of the colors that humans can perceive. One such color space is Display P3, which contains colors with significantly higher saturation than sRGB.

This browser reports that the display does not support Display P3 colors; figures in this post may not appear as intended.
A conic gradient showing a range of sRGB colors A conic gradient showing a range of Display P3 colors
Conic gradients showing fully saturated sRGB (left) and Display P3 (right) colors. Viewed in a browser and on a display supporting Display P3, the colors in the circle on the right will show as more intense than those on the left. (View as standalone page.)
For a more in depth introduction to color spaces, see Dean Jackson’s earlier post, Improving Color on the Web.

Today, there are many computer and mobile devices on the market with displays that can reproduce all the colors of the Display P3 gamut, and the Web platform has been evolving over the last few years to allow authors to make best use of these displays. WebKit has supported wide color images and video since 2016, and last year became the first browser engine to implement the new color syntax defined in CSS Color Module Level 4 where colors can be specified in a given color space (like color(display-p3 1 0 0), a fully saturated Display P3 red).

One notable omission in wide gamut color support, until now, has been in the HTML canvas element. The 2D canvas API was introduced before wide gamut displays were common, and until now has only handled drawing and manipulating sRGB pixel values. Earlier this year, a proposal for creating canvas contexts using other color spaces was added to the HTML standard, and we’ve recently added support for this to WebKit.

Drawing on a wide gamut canvas rendering context

The getContext method on a canvas element, which is used to create a rendering context object with 2D drawing APIs, accepts a new option to set the canvas backing store’s color space.

<canvas id="canvas" width="400" height="300"></canvas>
<script>
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d", { colorSpace: "display-p3" });
// ... draw on context ...
</script>

The default color space remains sRGB, rather than having the browser automatically use the wider color space, to avoid the performance overhead of color space conversions with existing content. The two explicit color spaces that can be requested are "srgb" and "display-p3".

Fill and stroke styles can be specified using any supported CSS color syntax.

let position = 0;
for (let green of [1, 0]) {
    for (let blue of [1, 0]) {
        for (let red of [1, 0]) {
            context.fillStyle = `color(display-p3 ${red} ${green} ${blue})`;
            context.fillRect(position, position, 40, 40);
            position += 20;
        }
    }
}
Colored squares that have been clamped to sRGB Colored squares using Display P3 colors that are outside the sRGB gamut
Display P3 colors used as fill styles on an sRGB (left) and Display P3 (right) canvas. Colors on the left are clamped to remain within the sRGB gamut. (View as standalone page.)

Any drawing that uses a color outside the color space of the canvas will be clamped so that it is in gamut. For example, filling a rectangle with color(display-p3 1 0 0) on an sRGB canvas will end up using a fully saturated sRGB red. Similarly, drawing on a Display P3 canvas with color(rec2020 0.9 0 0.9), an almost full magenta in the Rec.2020 color space, will result in pixels of approximately color(display-p3 1.0 0 0.923) being used, since that is the closest in the Display P3 color gamut.

const COLORS = ["#0f0", "color(display-p3 0 1 0)"];
for (let y = 20; y < 180; y += 20) {
    context.fillStyle = COLORS[(y / 20) % 2];
    context.fillRect(20, y, 160, 20);
}
A filled square of full sRGB green Stripes of full sRGB green and full Display P3 green
Stripes of interleaved Display P3 and sRGB colors on an sRGB (left) and Display P3 (right) canvas. Because colors are clamped to remain within the gamut of the canvas, the two shades of green are indistinguishable on the sRGB canvas. (View as standalone page.)
On macOS, you can use the ColorSync Utility to convert color values between sRGB, Display P3, Rec.2020, and some other predefined color spaces.

Wide gamut colors are usable in all canvas drawing primitives:

  • as the fill and stroke of rectangles, paths, and text
  • in gradient stops
  • as a shadow color

Pixel manipulation in sRGB and Display P3

getImageData and putImageData can be used to get and set pixel values on a wide gamut canvas. By default, getImageData will return an ImageData object with pixel values in the color space of the canvas, but it is possible to specify an explicit color space that does not match the canvas, and a conversion will be performed.

let context = canvas.getContext("2d", { colorSpace: "display-p3" });
context.fillStyle = "color(display-p3 0.5 0 0)";
context.fillRect(0, 0, 100, 100);

let imageData;

// Get ImageData in the canvas color space (Display P3).
imageData = context.getImageData(0, 0, 1, 1);
console.log(imageData.colorSpace);  // "display-p3"
console.log([...imageData.data]);   // [128, 0, 0, 255]

// Get ImageData in Display P3 explicitly.
imageData = context.getImageData(0, 0, 1, 1, { colorSpace: "display-p3" });
console.log(imageData.colorSpace);  // "display-p3"
console.log([...imageData.data]);   // [128, 0, 0, 255]

// Get ImageData converted to sRGB.
imageData = context.getImageData(0, 0, 1, 1, { colorSpace: "srgb" });
console.log(imageData.colorSpace);  // "srgb"
console.log([...imageData.data]);   // [141, 0, 0, 255]

The ImageData constructor similarly takes an optional options object with a colorSpace key.

let context = canvas.getContext("2d", { colorSpace: "display-p3" });

// Create and fill an ImageData with full Display P3 yellow.
let imageData = new ImageData(10, 10, { colorSpace: "display-p3" });
for (let i = 0; i < 10 * 10 * 4; ++i)
    imageData.data[i] = [255, 255, 0, 255][i % 4];

context.putImageData(imageData, 0, 0);

As when drawing shapes using colors of a different color space, any mismatch between the ImageData and the target canvas color space will cause putImageData to perform a conversion and potentially clamp the resulting pixels.

Serializing canvas content

The toDataURL and toBlob methods on a canvas DOM element produce a raster image with the canvas contents. In WebKit, these methods now embed an appropriate color profile in the generated PNG or JPEG when called on a Display P3 canvas, ensuring that the full range of color is preserved.

Drawing wide gamut images

Like putImageData, the drawImage method will perform any color space conversion needed when drawing an image whose color space differs from that of the canvas. Any color profile used by a raster image referenced by an img, and any color space information in a video referenced by a video (be it a video file or a WebRTC stream), will be honored when drawn to a canvas. This ensures that when drawing into a canvas whose color space matches the display’s (be that Display P3 or sRGB), the source image/video and the canvas pixels will look the same.

Here is an interactive demonstration of using canvas to make a sliding tile puzzle. The tiles are drawn by applying a clip path and calling drawImage pointing to the img element on the left, which references a wide gamut JPEG. Toggling the checkbox shows how the colors are muted when an sRGB canvas is used.

Sliding tile puzzle. Toggling the checkbox will change whether an sRGB or a Display P3 canvas is used. (View as standalone page.)

Web Inspector support

Web Inspector also now shows color space information for canvases to help ensure your canvases’ backing stores are in the expected color space.

In the Graphics tab, the Canvases Overview will display the color space for each canvas next to the context type (e.g. 2D) on each canvas overview tile.

After clicking on a Canvas overview tile to inspect it, the color space is shown in the Details Sidebar in the Attributes section.

Browser support

Wide gamut canvas is supported in the macOS and iOS ports of WebKit as of r283541, and is available in Safari on:

  • macOS Monterey 12.1 and above
  • iOS 15.1 and above

Safari is the first browser to support drawing shapes, text, gradients, and shadows with wide gamut CSS colors on Display P3 canvases. All other features, including getImageData, putImageData, and drawImage on Display P3 canvases, are supported in Safari and in Chrome 94 and above.

Feature detection

There are a few techniques you can use to detect whether wide gamut display and canvas support is available.

Display support: To check whether the display supports Display P3 colors, use the color-gamut media query.

function displaySupportsP3Color() {
    return matchMedia("(color-gamut: p3)").matches;
}

Canvas color space support: To check whether the browser supports wide gamut canvases, try creating one and checking the resulting color space.

function canvasSupportsDisplayP3() {
    let canvas = document.createElement("canvas");
    try {
        // Safari throws a TypeError if the colorSpace option is supported, but
        // the system requirements (minimum macOS or iOS version) for Display P3
        // support are not met.
        let context = canvas.getContext("2d", { colorSpace: "display-p3" });
        return context.getContextAttributes().colorSpace == "display-p3";
    } catch {
    }
    return false;
}

CSS Color Module Level 4 syntax support: To check whether the browser supports specifying wide gamut colors on canvas, try setting one and checking it wasn’t ignored.

function canvasSupportsWideGamutCSSColors() {
    let context = document.createElement("canvas").getContext("2d");
    let initialFillStyle = context.fillStyle;
    context.fillStyle = "color(display-p3 0 1 0)";
    return context.fillStyle != initialFillStyle;
}

Future work

There are a few areas where wide gamut canvas support could be improved.

  • 2D canvas still exposes image data as 8 bit RGBA values through ImageData objects. It may be useful to support other pixel formats for a greater color depth, such as 16 bit integers, or single precision or half precision floating point values, especially when wider color gamuts are used, since increased precision can help avoid banding artifacts. This has been proposed in an HTML Standard issue.
  • The two predefined color spaces that are supported are sRGB and Display P3, but as High Dynamic Range videos and displays that support HDR become more common, it’s worth consdering allowing 2D canvas to use these and other color spaces too. See this presentation at the W3C Workshop on Wide Color Gamut and High Dynamic Range for the Web from earlier this year, which talks about proposed new color space and HDR support.
  • Canvas can be used with context types other than 2D, such as WebGL and WebGPU. A proposal for wide gamut and HDR support in these contexts was presented at that same workshop.

In summary

WebKit now has support for creating 2D canvas contexts using the Display P3 color space, allowing authors to make best use of the displays that are becoming increasingly common. This feature is enabled in Safari on macOS Monterey 12.1 and iOS 15.1.

If you have any comments or questions about the feature, please feel free to send me a message at @heycam, and more general comments can be sent to the @webkit Twitter account.

Further reading