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.
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;
}
}
}
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);
}
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.
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
- Improving Color on the Web (Dean Jackson, WebKit blog)
- Wide Gamut Color in CSS with Display-P3 (Nikita Vasilyev, WebKit blog)
- CSS Color Module Level 4 (W3C)
- HTML Standard — The 2D rendering context (WHATWG)
- W3C Workshop on Wide Color Gamut and High Dynamic Range for the Web (W3C)