facelessuser / coloraide

A library to aid in using colors
https://facelessuser.github.io/coloraide
MIT License
194 stars 12 forks source link

Implement a `_repr_html_` method for the `Color` class #429

Closed apcamargo closed 2 months ago

apcamargo commented 2 months ago

When working with colors in a Jupyter notebook (or Quarto), it can be helpful to visualize the colors directly. While there are several ways to do this, having this functionality built into coloraid via a _repr_html_ method would be very convenient.

Although this feature might fall outside the intended scope of the library, here’s a quick example I wrote:

def _repr_html_(self) -> str:
    """
    Returns an HTML representation of the color as a colored square,
    with the color's string representation displayed underneath.
    """
    c = self.convert("oklch").to_string()
    return f"""
    <div style="text-align: center;">
        <div style="width: 50px; height: 50px; background-color: {c}; margin: 0 auto;"></div>
        <div>{self.to_string()}</div>
    </div>
    """
image

This is just a quick implementation, and I’m sure there are a number of ways it could be improved.

facelessuser commented 2 months ago

It's an interesting idea. It definitely has limited use cases, but I can see it being useful in Jupyter. So Jupyter specifically uses __repr_html__ visualize an object?

facelessuser commented 2 months ago

I guess you'd run into the issue of what gamut do we format the colors in...you'd need some class option to output in display-p3 if your monitor supports it. I guess sRGB would be the safe default.

apcamargo commented 2 months ago

I converted the color to OKLCH because, as far as I know, the browser takes care of converting it to a color that the monitor can display.

Regarding _repr_html_, IPython has a number of methods for rich display: https://ipython.readthedocs.io/en/stable/config/integrating.html#rich-display

facelessuser commented 2 months ago

Yeah, you could let the browser gamut map out itself. Currently most just clip though which isn't great.

apcamargo commented 2 months ago

The cmap library has a good example of rich representations: https://github.com/tlambert03/cmap/blob/c6a74dbcf29bddbebc3cc5f6cabc042863ec6c29/src/cmap/_colormap.py#L611-L652

facelessuser commented 2 months ago

Thanks for the example. Yeah, it's not a bad idea, and it doesn't hurt us to have it. Let me play around with it.

facelessuser commented 2 months ago

Something like this might work. We turn off gamut mapping for string output by default at let the browser do whatever it does. You can obviously gamut map it before output if you want, but this will preserve the current color values for text output as well.

It also has a background for when colors are transparent and a border when color is too similar to background on which the color is displayed on.

from coloraide import Color as Base
from IPython.display import display

class Color(Base):
    def _repr_html_(self) -> str:
        """
        Returns an HTML representation of the color as a colored square,
        with the color's string representation displayed underneath.
        """
        c = self.convert("oklab").to_string()
        return f"""
        <div style="text-align: center;">
            <div style="width: 52px; height: 52px; background: url(&quot;data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' fill-opacity='0.1'><rect width='50' height='50' /><rect x='50' y='50' width='50' height='50' /></svg>&quot;)
  0 0 / 0.5em 0.5em #fefefe; margin: 0 auto;"><div style="width: 50px; height: 50px; background-color: {c}; border: 2px ridge hsl(0, 0%, 85%);"></div></div>
            <div>{self.to_string(fit=False)}</div>
        </div>
        """

display(Color('color(srgb 1 0 0 / 0.5)'))
display(Color('color(srgb 0 0 1 / 0.75)'))
display(Color('color(srgb 0 1 0 / 1)'))
Screenshot 2024-08-11 at 2 52 58 PM
apcamargo commented 2 months ago

I just found out about https://github.com/w3c/csswg-drafts/issues/9449. I wasn't really aware of this issue.

I think this looks good! I had forgotten about the transparency grid.

Personally, I'm not a big fan of ridge borders. But that's minor

facelessuser commented 2 months ago

Open to suggestions, we could do some kind of double border. I was more looking for something where the border stood out on light and dark backgrounds.

facelessuser commented 2 months ago

Yeah, we'll go with a double border:

Screenshot 2024-08-11 at 4 30 18 PM Screenshot 2024-08-11 at 4 30 33 PM
apcamargo commented 2 months ago

Much better! Thanks for working on this so quickly :)

facelessuser commented 2 months ago

Much better! Thanks for working on this so quickly :)

Yeah, I'm not particularly fond of ridge either, but I also threw it together pretty quickly :).

I just found out about https://github.com/w3c/csswg-drafts/issues/9449. I wasn't really aware of this issue.

Yeah, they are still trying to figure out how browsers will gamut map things. Basically what it boils down is CSS offers a proposal to gamut map and preserve lightness in a perceptual color space, which has many uses, but admittedly, it is not as good for image gamut mapping etc. Preserving chroma can be quite desirable with images. Some browsers want one GMA to rule them all, but supporting preserving chroma with GMA and preserving lightness with GMA are at odds with each other and have different use cases. There are also a lot of opinions on a great number of little details as well. I think Chrome is pushing hard to have some kind of baked in logic for each supported gamut while CSS has a generic (and slow) algorithm. Nobody has been super clear on what the criteria should be, so now it seems all the browsers are waiting to see what Chrome resolves with the CSS WG. Who knows what the final solution will look like.

There a lot of other opinions out there too about a great number of things.

apcamargo commented 2 months ago

Thanks for the explanation!