Closed facelessuser closed 1 year ago
In the end, it may not be worth it to add tint()
and shade()
as it is just color.mix('white', 0.5)
or color.mix('black', 0.5)
. A user can really do that on their own without much effort. I guess if people really, really wanted it as a convenience, then we could add it, but if not, I don't see this as really being that useful.
If we were to go the tint/shade route for lightening and darkening, then it may not make sense for us to bother adding a function unless it was just really, really wanted as a convenience function as well.
Something to think about.
For now, I think we are going to pass on this. I imagine we could change our mind sometime in the future or maybe if people really want some these maybe that would cause us to change our mind.
I understand that this feature was ultimately decided against, but I wanted to share my thoughts that it might still be worth including these functions in the documentation, even though they included in the library. Not only are they pretty useful, but the code serve as a good example of how to use coloraid
for simple color space manipulation tasks.
For reference, in case anyone finds this issue through a web search, I wrote slightly modified versions of the lighten
and darken
functions using the OKHSL space (just because the ranges are simpler to manage than OKLCH's, but I realize this limits manipulation to the sRGB gamut). I also implemented saturate
and desaturate
functions.
from coloraide import Color as Base
from coloraide.spaces.okhsl import Okhsl
class Color(Base):
...
Color.register(Okhsl())
def lighten(
color: Color,
amount: float,
) -> Color:
"""
Lighten a color by a given amount.
"""
space = color.space()
color = color.convert("okhsl")
color["l"] += amount * (1 - color["l"])
return color.convert(space)
def darken(
color: Color,
amount: float,
) -> Color:
"""
Darken a color by a given amount.
"""
space = color.space()
color = color.convert("okhsl")
color["l"] -= amount * color["l"]
return color.convert(space)
def saturate(
color: Color,
amount: float,
) -> Color:
"""
Saturate a color by a given amount.
"""
space = color.space()
color = color.convert("okhsl")
color["s"] += amount * (1 - color["s"])
return color.convert(space)
def desaturate(
color: Color,
amount: float,
) -> Color:
"""
Desaturate a color by a given amount.
"""
space = color.space()
color = color.convert("okhsl")
color["s"] -= amount * color["s"]
return color.convert(space)
It's so much easier just using mix, even for your case, but if you want functions, it is easy to do, you can even make them part of the class:
from coloraide import Color as Base
from coloraide.spaces.okhsl import Okhsl
class Color(Base):
def lighten(self, amount):
"""Lighten."""
return self.mix(Color('okhsl', [NaN, NaN, 1]), amount, space='okhsl', out_space=self.space())
def darken(self, amount):
"""Darken."""
return self.mix(Color('okhsl', [NaN, NaN, 0]), amount, space='okhsl', out_space=self.space())
def saturate(self, amount):
"""Saturate."""
return self.mix(Color('okhsl', [NaN, 1, NaN]), amount, space='okhsl', out_space=self.space())
def desaturate(self, amount):
"""Desaturate."""
return self.mix(Color('okhsl', [NaN, 0, NaN]), amount, space='okhsl', out_space=self.space())
Results are identical to yours (yours left, mix version right):
>>> pink = Color('pink')
>>> Steps([lighten(pink, 0.25), pink.lighten(0.25)])
[color(srgb 1 0.81691 0.84708 / 1), color(srgb 1 0.81691 0.84708 / 1)]
>>> Steps([darken(pink, 0.75), pink.darken(0.75)])
[color(srgb 0.37409 0.00001 0.1479 / 1), color(srgb 0.37409 0.00001 0.1479 / 1)]
>>> Steps([desaturate(pink, 0.75), pink.desaturate(0.75)])
[color(srgb 0.88737 0.80565 0.81814 / 1), color(srgb 0.88737 0.80565 0.81814 / 1)]
>>> Steps([saturate(pink, 0.75), pink.saturate(0.75)])
[color(srgb 1 0.75294 0.79608 / 1), color(srgb 1 0.75294 0.79608 / 1)]
As for putting examples in the documentation, It's probably not a bad idea to show some of these more advanced topics in extending the Color object.
That's a much smarter approach :)
But yeah, my main point is that having these as examples in the documentation would be useful both to illustrate different ways to use the Color
object and to show people how to perform those common manipulations.
Recently been thinking about adding some sort of quick and easy lightening/darkening methods.
Initially, the thought was to just create a simple method that adjusts the lightness in a perceptually uniform color space like Oklab.
The thought also occurred to me that maybe we just want to mix white or black in to lighten or darken.
We can consider these methods:
Adjusting lightness in Oklab via
lighten()
anddarken()
works fine, but results are a bit funny when you gamut map. Currently, we are gamut mapping in LCh, which is not perfect, but it avoids some quirks of gamut mapping with OkLCh. In order to have the best lightening and darkening, you really need to gamut map in OkLCh.tint()
andshade()
on the other hand seems to work well in either LCh or OkLCh gamut mapping as we are just linearly interpolating between the color and white or black. In the ideal cases, the results are very similar to the bestlighten()
/darken()
cases.Top two cases are using OkLCh gamut mapping and the bottom two are using LCh gamut mapping. Out of the two pairs, the first row is using
tint()
and the second row is usinglighten()
:Top two cases are using OkLCh gamut mapping and the bottom two are using LCh gamut mapping. Out of the two pairs, the first row is using
shade()
and the second row is usingdarken()
:I'm wondering if something like
tint()
andshade()
is really all we need. We could even just call themlighten()
anddarken()
if we want.