Skip to content

Use context2D to convert CSS4 colors to display-p3#2145

Open
Fil wants to merge 7 commits intomainfrom
fil/colorSpace-hack
Open

Use context2D to convert CSS4 colors to display-p3#2145
Fil wants to merge 7 commits intomainfrom
fil/colorSpace-hack

Conversation

@Fil
Copy link
Copy Markdown
Contributor

@Fil Fil commented Aug 22, 2024

Since we rely on the browser to parse CSS4, and to create a canvas, we can rely on it to also convert these colors to whatever color space it supports.

Maybe this is a hack—or maybe it's the only correct way.

If it's the correct way, maybe we can apply this technique to also support CSS4 colors in the sRGB color space, instead of relying on d3.rgb.

@Fil Fil mentioned this pull request Aug 22, 2024
* memoize (with some decent limit) for faster renders
* clear rect to guard against semi-transparent color leaks
@Fil
Copy link
Copy Markdown
Contributor Author

Fil commented Aug 22, 2024

OK now it's fast in both examples (p3 and CSS4 + sRGB).
(It is slower than the usual call to d3.rgb when there are more than 256 different colors.)

@Fil
Copy link
Copy Markdown
Contributor Author

Fil commented Aug 22, 2024

One thing that doesn't work here is the serialization to disk made by our unit tests; you can see the two new SVGs appear black (in this PR's preview on github, as well as on dick for me).

I think it's okay for now, but it means that node-canvas doesn't support CSS4 nor display-p3? Needs a bit more investigation.

@Fil
Copy link
Copy Markdown
Contributor Author

Fil commented Aug 22, 2024

After investigation:

  • node-canvas does not support colors expressed as CSS4 strings (e.g. color(display-p3 0 1 0.1) is going to be black)
  • when a canvas prepared with the display-p3 colorSpace option is serialized to disk, it is binary-identical to a canvas prepared with the sRGB option. I think this means that node-canvas ignores the colorSpace option.

return {r, g, b};
};
let p;
return colorSpace === "srgb" ? (c) => (isNaN((p = rgb(c)).opacity) ? canvasConverter(c) : p) : canvasConverter;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t like switching to the canvasConverter in the sRGB case when the opacity is NaN… I would prefer to always use the simple rgbConvert in the sRGB case (the current behavior), and allow the user to opt-in to the canvasConverter implementation if desired (to have the browser handle color conversion), and make the canvasConverter implementation the default in the non-sRGB case. We could also expose these converter implementations similar to how we expose interpolatorBarycentric etc. This would also allow you to force “direct” (or “vivid”) conversion from RGB bytes to P3 XYZ if desired, though perhaps ill-advised.

Copy link
Copy Markdown
Contributor Author

@Fil Fil Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This converter adds support both for (a) CSS4 color strings in the sRGB space, and (b) display-p3. (The NaN check is only for additional speed in the sRGB case where d3.rgb would be faster, so we try that first, then switch to canvas as a fall-back; but it has an additional cost for all invalid color strings.)

I can see how we could want (a) to be opt-in, so that by default we only use d3.rgb, for simplicity—I'm not sure if it's better for the user but at least it would not be slower in the case where many colors are invalid strings.

I feel that the fact that we are converting the color string through canvas is an implementation detail, so I'd be wary about exposing this under the name "canvas". Maybe colorConverter: "css4" would be a better name?

And imo using this converter with sRGB should still try to parse with d3.rgb first, for speed.

PS: Re-reading the code I see that it doesn't support rgba colors, as it drops the a part. This feels wrong. (Edit: FIXED)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I think given the complexity it is inherently a leaky abstraction and the user can decide whether to use our limited color parser or (at the expense of performance) the canvas-based one.

@Fil Fil changed the base branch from mbostock/raster-color-space to main April 6, 2026 14:27
@Fil
Copy link
Copy Markdown
Contributor Author

Fil commented Apr 8, 2026

I want to rethink that.

If we implemented a css4 color string parser, we wouldn't have to juggle with that complexity and speed hit and incompatibility with SSR.

There is a reference implementation in https://colorjs.io/ (https://github.com/color-js/color.js). The library is too big to include as is, but I can extract the formulas into a smaller file, either to extend d3-color, or tailored for our purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants