Qix- / color

:rainbow: Javascript color conversion and manipulation library
MIT License
4.78k stars 266 forks source link

lighten and darken should be absolute #53

Open finnp opened 9 years ago

finnp commented 9 years ago

Hey,

in LESS and SASS the darken nand lighten function increase by an absolute amount. Example here: https://github.com/less/less.js/blob/fb5280f16f124e5062634a22be2f3c99e650d0a4/lib/less/functions/color.js#L163

This is a bit confusing because in color.js e.g. `color('#000000').lighten(1)' is still black.

I am not sure though if it would be better to change the function or create a new one like lightenAbsolute.

Best, Finn

MoOx commented 9 years ago

I am not sure less and sass are even doing the same computation (from what I remember, I already saw issue about difference in less and sass). That being said, this names are confusing, we should change that to whiteness, blackness, lightness.

rickyvetter commented 9 years ago

I think the names that exist are pretty solid. color.lightness(<value>) gives a way to update absolutely and color.lighten(<value>) gives a way to update relative to the current value.

The names are difficult because they are so similar. But they accurately describe what they do.

MoOx commented 9 years ago

css color function allow to use absolute percentage (10%) or relative ones (+/-10%).

Qix- commented 5 years ago

Right now, it's a multiplicative function. For backwards compatibility, I don't see that changing.

However, I would be open to something like .lightenBy() and .darkenBy() which would take offset values (addition). The same thing as what OP proposes as .lightenAbsolute(), just with a different name. I don't see it as an "absolute" operation as that just mean you're setting the lightness to a certain absolute value.

However, I'd even go so far as to suggest switching .lighten() and .lightenBy() since you would normally say Lighten by 10% rather than Lighten by 0.2. But that means a major release instead of a minor release, which means requiring a migration.

diegohaz commented 5 years ago

In case someone is looking for it, I'm using these functions in my project:

function lightenBy(color, ratio) {
  const lightness = color.lightness();
  return color.lightness(lightness + (100 - lightness) * ratio);
}

function darkenBy(color, ratio) {
  const lightness = color.lightness();
  return color.lightness(lightness - lightness * ratio);
}

lightenBy(Color("black"), 0.5);
Qix- commented 5 years ago

Thanks! I think it's less of a concern for implemention and more of getting input about the API. I haven't seen a massive push for this in any definitive direction yet. Would love to hear more input.

moritzhabegger commented 5 years ago

I was also confused by this implementation.

console.log(Color('#c880b6').lighten(0.5).hex()) => #FAF2F8
console.log(Color('#c880b6').lighten(0.6).hex()) => #FFFFFF

The function of @diegohaz did exactly what I was looking for

console.log(lightenBy(Color('#c880b6'), 0.5).hex()) => #E3C0DA
Qix- commented 5 years ago

If someone wants to submit a PR for @diegohaz's implementations and call them lightenAbs() and darkenAbs() I would accept/release it posthaste.

shinonomeiro commented 4 years ago

For those like me who are wondering why this library's lighten doesn't behave like the SASS one:

@diegohaz 's answer didn't do it for me. From SASS' documentation, their lighten function increases the lightness (L) of the color's HSL by a fixed amount, e.g. #414141 lightened by 60% should yield #dadada, but this library's lighten yields

> new Color('#414141').lighten(0.6).hex()
'#686868'

instead, which is computed in the following way:

> new Color('#414141').lightness()
25.49019607843137
> new Color('#414141').lightness(25.49019607843137 + (25.49019607843137 * 0.6)).hex()
'#686868'

while SASS computes it this way:

> new Color('#414141').lightness(25.49019607843137 + 60).hex()
'#DADADA'

as shown in online color generators such as http://scg.ar-ch.org/.

Slight changes to diegohaz's helper functions (thanks by the way!):

function lightenBy(color, amount) {
  const lightness = color.lightness();
  return color.lightness(lightness + amount);
}

function darkenBy(color, amount) {
  const lightness = color.lightness();
  return color.lightness(lightness - amount);
}

lightenBy(Color("black"), 50);
elsurudo commented 3 years ago

Just as another data point, as a first-time user the current functionality was a surprise to me, and the helpers in this thread give the behaviour that I would have expected.

Fuzzypeg commented 3 years ago

I've been confused and frustrated by these functions as well, but I'm not trying to replicate LESS or SASS. I'll explain my use case and experience in case it's useful.

I've been trying to use lighten to create lighter versions of branding colours configured by the user, for styling css background-colour to ensure sufficient contrast with foreground text. Obviously, I don't control what branding colours will be supplied by the user, so I don't know whether lightening by 50% will max out to white or still be drowning in the murky depths of near-black. This makes it useless for my purpose.

Furthermore, it turns out that 100% lightening is the greatest possible by design. (You can go higher, but not in a useful way, since values of 101 or greater are treated as simple factors, rather than percentages. Thus 101 means 10100%. So it's possible to lighten by 100% or 10100%, but nothing in between!) Given the 100% limit, that would imply that 100% should give the maximum lightening possible, i.e. full white. But for many input colours this is far from the case. E.g., I lighten the colour #0A9DD9 by 100%, and I get #C9EDFC, which is a bit lighter, but still a long way from white. Another way of looking this is, given the multiplicative approach used by the lighten function, I should legitimately be allowed to lighten by > 100% -- but I cannot. To me, then the function seems clearly broken.

For my purposes, multiplicative (or even absolute additive) approaches are not very useful, since a piece of code isn't going to know how by how much it is appropriate to multiply (or add) unless it knows what the shade of the colour is to begin with. Diegohaz's functions, on the other hand, reliably gives sensible results, and it also has a nice intuitive analogy: lighten by 60% is the same as mixing with 60% white and darken by 60% is the same as mixing with 60% black. This suggests some possible names: whiten and blacken.

Whatever the case, lighten and darken are broken as far as I'm concerned, and people should be discouraged against using them.

Fuzzypeg commented 3 years ago

On second thoughts, Diegohaz's functions are not analogous to adding white or black, since they preserve saturation. Adding white to a very dark colour would give something close to grey, e.g. #000005 + 50% white = #7F7F82, not #0505FF, as Diegohaz's lightenBy would give. To me, the original lighten would have been better named brighten (and should have permitted > 100% brightening) analogous to brightening a photograph or brightening a monitor display, and Diegohaz's variant would ideally be named lighten. I don't know the best naming then, but I still feel Diegohaz's lightenBy function is the most useful for my purposes.

Also on second thoughts, It looks like darken does exactly the same as Diegohaz's version, which actually makes sense as a counterpart both to brighten and to lighten. It nicely fits both paradigms. So that's not broken.

(Sorry for multiple edits/posts. As well as writing software, I'm an artist trained in photography and studio lighting, and a frequent user of image editing tools, so that may partly explain my pedantry!)

Qix- commented 3 years ago

Again, I've said it a plethora of times on this project: The API creep is insanely high in color already and no good propositions on how to manage color operations across models has been brought forth.

Remember that increasing/decreasing "lightness", "brightness", "whiteness", whatever you want to call it, is confusing because it can mean different things to different people with different goals and different understandings of colors using different color models altogether.

The opposite of "dull" (low saturation) is often "bright" (which could mean higher saturation), but "bright" also means white in some cases, or a lighter tint, etc.

This is a hard naming problem. As-is, the methods provided now are not broken, so please do not claim they are. They simply do not do what you want them to do - just as a toaster not freezing your bread does not make the toaster "broken".

Fuzzypeg commented 3 years ago

What is the rationale for not allowing a colour to be lightened >100%? If we assume a multiplication model, then we should reasonably be allowed to lighten by any factor up to 25400% (so we can lighten #000001 up to

ffffff). I'm not just saying that I prefer a different colour model; I'm

saying that the function doesn't properly support its chosen model. If I've misunderstood something, please clarify. Thanks.

On Fri, 28 May 2021, 06:50 Qix, @.***> wrote:

Again, I've said it a plethora of times on this project: The API creep is insanely high in color already and no good propositions on how to manage color operations across models has been brought forth.

Remember that increasing/decreasing "lightness", "brightness", "whiteness", whatever you want to call it, is confusing because it can mean different things to different people with different goals and different understandings of colors using different color models altogether.

The opposite of "dull" (low saturation) is often "bright" (which could mean higher saturation), but "bright" also means white in some cases, or a lighter tint, etc.

This is a hard naming problem. As-is, the methods provided now are not broken, so please do not claim they are. They simply do not do what you want them to do - just as a toaster not freezing your bread does not make the toaster "broken".

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/Qix-/color/issues/53#issuecomment-849860888, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABRYCIU7N3KX7QGD37YMH5DTP2H6PANCNFSM4A4EZF5Q .

Qix- commented 3 years ago

What is preventing you from passing higher values to lighten or whiten?

Fuzzypeg commented 3 years ago

I apologise, I'm entirely wrong about that. I did not check carefully enough, and it's a wrapper library that imposes that buggy 100% limit in my code, not the color package. I'm sorry for spamming you with nonsense; nothing is broken. I still prefer Diegohaz's lightenBy, but you've made this very simple to achieve (nice work, thank-you!).

On Fri, 28 May 2021, 22:07 Qix, @.***> wrote:

What is preventing you from passing higher values to lighten or whiten?

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/Qix-/color/issues/53#issuecomment-850308467, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABRYCITMIT6BHB625UGFWALTP5TNRANCNFSM4A4EZF5Q .

elauser commented 2 years ago

While it's true that technically multiplying 0 by a ratio returns 0, that functionality would be needed. I'd go for pragmaticism and not purity.

To solve this in my code, and to lighten black I had to reverse the color, darken it, then reverse it again. Feels pretty retarded to do but it works for me.

Maybe that code snippet helps anyone else having this problem.

function getShades (color: string) {
  return {
    50: Color(color).negate().darken(0.9).negate().hex(),
    100: Color(color).negate().darken(0.8).negate().hex(),
    150: Color(color).negate().darken(0.7).negate().hex(),
    200: Color(color).negate().darken(0.6).negate().hex(),
    250: Color(color).negate().darken(0.5).negate().hex(),
    300: Color(color).negate().darken(0.4).negate().hex(),
    350: Color(color).negate().darken(0.3).negate().hex(),
    400: Color(color).negate().darken(0.2).negate().hex(),
    450: Color(color).negate().darken(0.1).negate().hex(),
    500: color,
    550: Color(color).darken(0.1).hex(),
    600: Color(color).darken(0.2).hex(),
    650: Color(color).darken(0.3).hex(),
    700: Color(color).darken(0.4).hex(),
    750: Color(color).darken(0.5).hex(),
    800: Color(color).darken(0.6).hex(),
    850: Color(color).darken(0.7).hex(),
    900: Color(color).darken(0.8).hex(),
    950: Color(color).darken(0.9).hex(),
  }
}