Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test has a WPT meta file that expects 17 subtest issues.
- This WPT test may be referenced by the following Test IDs:
- /css/css-color/canvas-display-p3-gamut-mapping.html - WPT Dashboard Interop Dashboard
<!DOCTYPE html>
<meta charset="utf-8">
<title>CSS Color 4: colors outside display-p3 are gamut mapped, not clipped, on a display-p3 unorm8 canvas</title>
<meta name="assert" content="Painting a color outside the display-p3 gamut into a 2d canvas whose colorSpace is display-p3 and colorType is unorm8 applies one of the three CSS gamut mapping algorithms, which reduce chroma at constant Oklch lightness and hue, rather than clipping each channel.">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<body>
<script>
// mapped: the mean of the three CSS gamut mapping algorithms, in 8-bit display-p3
// tolerance: per channel; all three algorithms are within it of mapped, and the clipped color is not
// clipped: the color clipped per channel, in 8-bit display-p3, for reporting what happened
// The comment above each case gives the three algorithms' own results.
const cases = [
// MINDE [183.85, 228.53, 255.00], EdgeSeeker [195.26, 226.94, 255.00], Ray Trace [195.31, 226.93, 255.00]
{ color: "oklch(90% 0.3 240)", mapped: [191, 227, 255], tolerance: 9, clipped: [0, 231, 255], clipTolerance: 1 },
// MINDE [188.72, 98.96, 0.00], EdgeSeeker [182.36, 103.91, 0.00], Ray Trace [182.72, 103.59, 0.00]
{ color: "oklch(60% 0.35 60)", mapped: [185, 102, 0], tolerance: 5, clipped: [236, 0, 0], clipTolerance: 1 },
// MINDE [255.00, 202.78, 189.01], EdgeSeeker [255.00, 208.33, 197.95], Ray Trace [255.00, 208.33, 197.95]
{ color: "oklch(90% 0.35 30)", mapped: [255, 206, 195], tolerance: 7, clipped: [255, 104, 72], clipTolerance: 1 },
// MINDE [227.04, 210.11, 255.00], EdgeSeeker [225.48, 213.74, 255.00], Ray Trace [225.46, 213.80, 255.00]
{ color: "oklch(90% 0.35 300)", mapped: [226, 213, 255], tolerance: 4, clipped: [247, 146, 255], clipTolerance: 1 },
// MINDE [148.39, 75.09, 0.00], EdgeSeeker [142.34, 79.87, 0.00], Ray Trace [142.87, 79.40, 0.00]
{ color: "oklch(50% 0.35 60)", mapped: [145, 78, 0], tolerance: 5, clipped: [200, 0, 0], clipTolerance: 1 },
// MINDE [199.17, 166.21, 255.00], EdgeSeeker [197.38, 171.05, 255.00], Ray Trace [197.36, 171.11, 255.00]
{ color: "oklch(80% 0.35 300)", mapped: [198, 169, 255], tolerance: 4, clipped: [215, 108, 255], clipTolerance: 1 },
// MINDE [255.00, 148.92, 127.06], EdgeSeeker [255.00, 156.70, 137.31], Ray Trace [255.00, 156.71, 137.32]
{ color: "oklch(80% 0.35 30)", mapped: [255, 154, 134], tolerance: 8, clipped: [255, 52, 22], clipTolerance: 1 },
// MINDE [0.00, 54.75, 68.49], EdgeSeeker [0.00, 53.21, 63.42], Ray Trace [0.00, 53.21, 63.42]
{ color: "oklch(30% 0.35 210)", mapped: [0, 54, 65], tolerance: 5, clipped: [0, 56, 134], clipTolerance: 1 },
// MINDE [59.90, 42.83, 0.00], EdgeSeeker [57.03, 43.96, 0.00], Ray Trace [57.03, 43.96, 0.00]
{ color: "oklch(30% 0.3 90)", mapped: [58, 44, 0], tolerance: 3, clipped: [90, 2, 0], clipTolerance: 1 },
// MINDE [0.00, 47.44, 93.90], EdgeSeeker [0.00, 48.36, 82.72], Ray Trace [0.00, 47.93, 84.22]
{ color: "oklch(30% 0.35 240)", mapped: [0, 48, 87], tolerance: 8, clipped: [0, 0, 191], clipTolerance: 1 },
// MINDE [255.00, 138.44, 183.70], EdgeSeeker [255.00, 148.16, 184.60], Ray Trace [255.00, 148.19, 184.60]
{ color: "oklch(80% 0.35 0)", mapped: [255, 145, 184], tolerance: 8, clipped: [255, 42, 179], clipTolerance: 1 },
// MINDE [0.00, 115.10, 138.38], EdgeSeeker [0.00, 113.30, 132.63], Ray Trace [0.00, 113.30, 132.63]
{ color: "oklch(50% 0.35 210)", mapped: [0, 114, 135], tolerance: 5, clipped: [0, 125, 199], clipTolerance: 1 },
// MINDE [255.00, 207.43, 153.66], EdgeSeeker [254.99, 211.89, 172.91], Ray Trace [255.00, 211.88, 172.89]
{ color: "oklch(90% 0.25 60)", mapped: [255, 210, 166], tolerance: 14, clipped: [255, 180, 0], clipTolerance: 1 },
// MINDE [255.00, 207.42, 153.65], EdgeSeeker [254.99, 211.89, 172.91], Ray Trace [255.00, 211.88, 172.89]
{ color: "oklch(90% 0.3 60)", mapped: [255, 210, 166], tolerance: 14, clipped: [255, 166, 0], clipTolerance: 1 },
// MINDE [205.94, 219.95, 255.00], EdgeSeeker [210.40, 220.72, 255.00], Ray Trace [210.49, 220.73, 255.00]
{ color: "oklch(90% 0.35 270)", mapped: [209, 220, 255], tolerance: 5, clipped: [149, 185, 255], clipTolerance: 1 },
// MINDE [90.88, 67.87, 0.00], EdgeSeeker [87.79, 69.05, 0.00], Ray Trace [87.79, 69.05, 0.00]
{ color: "oklch(40% 0.35 90)", mapped: [89, 69, 0], tolerance: 3, clipped: [128, 30, 0], clipTolerance: 1 },
];
// A color inside the gamut, in the canvas's own color space, which neither conversion
// nor gamut mapping should touch. If it changes, the canvas alters every color (for
// example by converting to the display's color profile) and the cases above cannot be judged.
const control = { color: "color(display-p3 0.8 0.4 0.2)", expected: [204, 102, 51], tolerance: 1 };
// Paint a 5×5 patch and sample its center pixel, well clear of any antialiased edge
function paint(color) {
const canvas = document.createElement("canvas");
canvas.width = canvas.height = 5;
const ctx = canvas.getContext("2d", { colorSpace: "display-p3", colorType: "unorm8" });
assert_equals(ctx.getContextAttributes().colorSpace, "display-p3", "canvas colorSpace");
ctx.fillStyle = color;
ctx.fillRect(0, 0, 5, 5);
return Array.from(ctx.getImageData(2, 2, 1, 1, { colorSpace: "display-p3" }).data.slice(0, 3));
}
function within(actual, expected, tolerance) {
return actual.every((value, i) => Math.abs(value - expected[i]) <= tolerance);
}
test(() => {
const actual = paint(control.color);
assert_true(within(actual, control.expected, control.tolerance),
`read back [${actual}], expected [${control.expected}] ± ${control.tolerance}: this canvas changes in-gamut colors, so gamut mapping cannot be judged`);
}, `control: in-gamut ${control.color} is unchanged on a display-p3 canvas`);
for (const { color, mapped, tolerance, clipped, clipTolerance } of cases) {
test(() => {
const actual = paint(color);
const result = within(actual, mapped, tolerance) ? "gamut mapped"
: within(actual, clipped, clipTolerance) ? "clipped"
: "neither gamut mapped nor clipped";
assert_equals(result, "gamut mapped",
`read back [${actual}]; gamut mapped is [${mapped}] ± ${tolerance}, clipped is [${clipped}]`);
}, `${color} painted on a display-p3 canvas`);
}
</script>