color icon indicating copy to clipboard operation
color copied to clipboard

lighten and darken should be absolute

Open finnp opened this issue 10 years ago • 17 comments

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

finnp avatar Feb 16 '15 22:02 finnp

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.

MoOx avatar Feb 19 '15 07:02 MoOx

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.

rickyvetter avatar Jun 02 '15 04:06 rickyvetter

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

MoOx avatar Jun 02 '15 04:06 MoOx

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.

Qix- avatar Oct 22 '18 14:10 Qix-

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);

diegohaz avatar Apr 30 '19 05:04 diegohaz

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.

Qix- avatar Apr 30 '19 07:04 Qix-

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

moritzhabegger avatar May 13 '19 14:05 moritzhabegger

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

Qix- avatar May 13 '19 16:05 Qix-

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);

shinonomeiro avatar Jul 10 '20 09:07 shinonomeiro

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.

elsurudo avatar Dec 26 '20 00:12 elsurudo

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 avatar May 25 '21 08:05 Fuzzypeg

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!)

Fuzzypeg avatar May 27 '21 01:05 Fuzzypeg

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".

Qix- avatar May 27 '21 18:05 Qix-

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 .

Fuzzypeg avatar May 27 '21 21:05 Fuzzypeg

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

Qix- avatar May 28 '21 10:05 Qix-

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 .

Fuzzypeg avatar May 29 '21 11:05 Fuzzypeg

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(),
  }
}

elauser avatar Sep 15 '22 14:09 elauser