-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add contrast function #488
Conversation
I applied the patch from #486 which allowed me to run the test suite and confirm this code passes. |
This is great, but instead of returning white, shouldn't it return an opposite lightness value? Or is that already possible? |
No, that's exactly the common use case that people do wrong, as mentioned in the pull request. Think what happens on 50% grey. For readability there apparently needs to be at least a 10% difference in lightness - I've no idea how to calculate something meaningful for that (some colour theory stuff is insanely complicated!), so falling back to black and white is a simplistic approach but should be reasonably useful, which is why I said it could be more subtle. The calculated colour could of course be modified further using LESS's other colour operations, but at least this way we're ensured it's readable. |
I just noticed that SASS has this function in Compass. It uses variables to define default light and dark colours and a threshold, but I can't do that easily in LESS because it lacks conditionals. I guess light and dark colours could be added as additional parameters and do the conditions in JS. |
I think the upcoming mixin guards should handle this more gracefully. See: #191 |
Well that would deal with the conditional part, but that's not the end of the story because less doesn't provide access to colour components (why I needed to do the contrast calculation in JS), and guards don't fix that. To paraphrase your example I'd need to do something like this:
less lacks a lightness function anyway, but switching on an explicit value (in your example the theme name) is much less useful - it's practically identical to setting the colour value explicitly, which is the same thing as not using a condition at all. While I can see guards being useful for conditions in general, they don't add any functionality themselves - we'd need a whole load of other colour functions (which could include a contrast function anyway) before they could be usable for this. |
It should work like this (I think, as the mixin guards aren't final): .contrast(@color) ? lightness(@color) > 50% {
color: black;
}
.contrast(@color) ? lightness(@color) <= 50% {
color: white;
}
.myselector {
@baseColor: #ABABAB;
.contrast(@baseColor);
} |
That looks workable but it's pretty verbose, and it still needs implementations of colour functions. All seems like quite a long way around? |
Nope, color functions already exist as follows: lighten(@color, 10%); // return a color which is 10% *lighter* than @color
darken(@color, 10%); // return a color which is 10% *darker* than @color
saturate(@color, 10%); // return a color 10% *more* saturated than @color
desaturate(@color, 10%); // return a color 10% *less* saturated than @color
fadein(@color, 10%); // return a color 10% *less* transparent than @color
fadeout(@color, 10%); // return a color 10% *more* transparent than @color
spin(@color, 10); // return a color with a 10 degree larger in hue than @color
spin(@color, -10); // return a color with a 10 degree smaller hue than @color
hue(@color); // returns the `hue` channel of @color
saturation(@color); // returns the `saturation` channel of @color
lightness(@color); // returns the 'lightness' channel of @color It may be more verbose, but a contrast function seems arbitrary in its logic. I wouldn't necessarily expect a color to always return black or white (as a contrasting color), for example. It's what makes sense to you, but it's hard to see that going in the core library. By doing a mixin, you can establish the logic for yourself in your own library, and then simply import the library whenever you want to use your .contrast() mixin. |
OK, sorry I'd missed those colour channel functions. I agree that black and white are a bit arbitrary - A better implementation might be more like the one I linked to in SASS, which takes a background, light and dark colours as params, which could reasonably default to black and white. I like having contrast as a function rather than a mixin as it lets me define the colours globally, independent of any selector, exactly as you typically define other colours in a theme.
I don't really see that this is any different to having things like:
|
Now that I think about it, I think you are identifying a general issue, which is that I can't define a value output using a custom mixin. So, while it can output the entire property, it would be nice to, as you point out, just output a color value. I think LESS mixins can only output property / value pairs, unless I'm mistaken. |
@MatthewDL: Indeed, only pairs work in LESS; it would be awesome to have something like SASS |
For simple color lightness/darkness, you'd have to convert the HEX code to RGB and then to HSL, then isolate the L and increase or decrease this value. You then convert it back to RGB. At this point you can either 1) Create an RGBA output for plenty of situations (with the added bonus of opacity) or 2) Convert another step back to HEX. |
@noeltock: That seems a bit redundant, but sure. The only problem is how to increase or decrease the lightness without mixin guards. @Synchro: With mixin guards, I came up with this, that ensures a minimum contrast level of
By taking advantage of the mixin arity, it's also possible to mimic Compass behaviour:
Color Contrast Verification: http://www.hp.com/hpinfo/abouthp/accessibility/webaccessibility/color_tool.html |
Merged latest from upstream and updated tests too, all pass.
I just committed an expansion of my original version making the contrast function work more like SASS's, with optional params for light and dark values. Are mixin guards usable yet? I find the guards syntax verbose and a bit confusing, and it's a bit limiting not being able to get plain colour values out. With my function it would be:
Plus I could of course turn it around and derive a contrasting background colour from a text colour since it's a generic function unrelated to my classes. Defaulting to black and white seems to give a worst-case contrast ratio of around 4.5. That tool is interesting: I found several combinations at lower contrast values much more readable than higher ones (e.g. a 3.7 that was far easier to read than a 5.9). Curious! I'm also looking at being able to do further manipulations to colour values, so having done the above, I might want to go on to do something like:
and I get the feeling that using guards for that would end up being quite messy. |
@Synchro I agree, was just explaining the process behind a more suitable color lightness/darkness changer, not just b/w (that's what I do on my websites, but via PHP, including the colorized image generation if any). Could be done via JS too, but may be overkill at this point. Cheers |
@Synchro: The
Guards seems to be available as of LESS 1.2.0, and while their syntax may seem strange they are quite powerful. |
Oh I know that spin is there and what it does, I was just using it as an example of processing a colour value. As far as I'm concerned, the contrast function is a generic colour processing function just like spin, lighten etc (put a value in, get a changed value out), and the result could be used anywhere that a colour value can be used, not just for text, so if it's logical to implement spin as a function, then the same applies to contrast. If I was using contrast in a set of LESS rules for creating a complete colour palette (not related to any classes), using guards would result is a much more complicated and less flexible file, which seems to run counter to less' spirit. |
@Synchro: Well, if your |
Why not have a real contrast function like that of photoshop or other image tools? Less will not have |
Huh? Are you proposing to add image processing functions to LESS? What do you mean by a real contrast function? Are you suggesting we use a more complex colour calculation that takes hue into account as well as lightness? |
Image processing? I hope not. That doesn't seem necessary. If LESS can be used to apply CSS to inline SVG elements, you're all set. Of course, I haven't tried styling SVG yet. |
I was being obtuse. I don't understand the rejection of my patch - it seems that the entire premise has not been understood. Cloudhead said "But this is what js functions are for, so let's try to have a real contrast function.", which is exactly what I provided. This has nothing to do with images or photoshop, just CSS; I wrote it because I needed it and there's no other way of doing this in LESS; and it can't be simulated through a combination of other LESS functions. |
It's not understood probably because your function still seems mathematically arbitrary. A spin function has a mathematically expected relationship because of a well-defined color wheel. Contrast, however, is typically not a pre-determined math relationship, but has an adjustable numerical value. And what a contrast function really does in any photo/image editor is "push" the lightness and darkness of relative colors to opposite sides. If you are on the > 50% side, you get X more lightness, and if you are on the < 50%, you get X more darkness (or X less lightness). So, I think that's kind of what you are going for, but your syntax just feels wrong and incompatible with current functions. So, a true contrast function would look like this: .myselector {
color: contrast(@mycolor, 20%);
} The contrast function would be similar to this (similar to @alixaxel's code): .myselector {
.contrast(@mycolor, 20%);
}
.contrast(@color, @percent) when (lightness(@color) > 50%) {
color: lighten(@color, @percent);
}
.contrast(@color, @percent) when (lightness(@color) < 50%) {
color: darken(@color, @percent);
} I don't know how useful that is, but that's an actual contrast function and contains no arbitrary magic numbers, other than the threshold. Of course, you can reverse the logic, or you SHOULD be able to entirely replicate the color-contrast function in Compass: .contrast-color(@color, @dark, @light, @threshold) when (lightness(@color) > @threshold) {
color: @dark;
}
.contrast-color(@color, @dark, @light, @threshold) when (lightness(@color) <= @threshold) {
color: @light;
} I haven't tested that, BUT, if that's your goal, and your syntax was written like that, then I could totally see a need for that.... As I'm writing it out, I'm now getting what you are aiming for, which is to return a conditional color. So, the only thing you had different was the @Threshold. I think the contrast function should work both ways, with an assumption of 50% when the @Threshold is omitted. So that we can do: .myselector {
border: 1px solid contrast(@mycolor, @dark, @light);
color: contrast(@forecolor, @darkForeColor, @lightForeColor, 80%);
} Okay, now it makes sense. I think you're right that what you were aiming at was misunderstood because I think you just described it in a confusing way. But after seeing Compass, I see what you were aiming for, which is not actually achievable with a mixin guard. I think this should be reopened and implemented, but with the above syntax and an optional threshold. |
I guess I don't really understand the purpose of this? How can we increase the contrast of a color unless we know what the colours around it are? Contrast is only relevant with colour pairs, you can't "contrast" a single colour. |
Contrast (the way we use it in Photoshop and on display screens) actually has nothing to do with a color pair. A color can have more contrast based on how light it is. It just depends on which side of a threshold you are on. If it is light, it gets lighter. If it is dark, it gets darker. So, as a result, two colors on either side of a threshold will contrast more with each other, because their lightness have been pushed in opposite relative directions. In this case, like @Synchro was saying, similar to how spin outputs the color with a color relationship on the color wheel, a contrast function would output a color based on the relationship of a color to a lightness threshold (a contrast split point). It's almost like a Of course, @light and @dark are arbitrary and defined by the user, so it's not inherent that they are contrasting, but this function is more useful than simply pushing it darker or lighter based on a threshold. In this case, you are able to select the resulting color based on the threshold point. I think that some variation of it is a good idea in that it's a color function that doesn't exist, and it's basically a way to output a color based on that color's positional values. |
Thanks for coming back to this. |
@MatthewDL: I don't think what Photoshop does applies to the web, we really must work with color pairs here. @cloudhead: I'm still confused what you mean by a "real contrast function", @Synchro function seems pretty much complete. As much as I like LESS, I think As it is, if I want to make a complete contrast feature, I have to write mixins for foreground and background colors, border colors, outline colors and so on (and multiply that * 2, because each mixin has to have two guard) - that's a lot of typing and a lot of wasted bandwidth compared to a return based approach. SASS just seems more appealing to build flexible stylesheets.
I'm sorry, but I just don't understand the reasons behind the current approach and I just can't stop thinking "what if"... |
@alixaxel Semantics do matter, and how contrast is normally defined should at least be a consideration, but it's true that simply contrasting a color to either lighter or darker based on it's current value is not altogether useful, so some flexibility is needed. Not to speak for @cloudhead, but he has said before that he wants to keep LESS declarative. I'm not 100% an expert on the concept, but the reasons for not having some things that SASS has are philosophical, and I understand some of the motivations. LESS is pretty simple right now, so there are trade-offs. It's simplicity vs. scriptability. I look at the SASS frameworks and say, "Holy frig, this is way over-engineered." Having said that, I do sometimes feel cheated that there are built-in functions that can return a property in LESS and that I don't get to. Like you said, sometimes I just want to return a single property, like a color, and not every line of CSS that includes a color component. I don't know the best "declarative" way to solve that stumbling block, but it sounds like we're all circling around a real issue, but maybe haven't tapped the best solution yet. |
I don't think we need to worry too much about usage elsewhere since our context is quite specific - we're only talking about CSS colour values. There isn't really much we can 'import' from the image processing view of your average contrast slider since we have none of the other stuff that that relies on, for example a lightness histogram. In the context of CSS colours, the only thing we can really think about with respect to contrast is how much of it there is relative to another colour. Perhaps to be accurate a pure contrast function should return the difference in lightness between two colours, though I don't see that being very useful as it would still deny us the ability to choose between colours based on the result of it because LESS doesn't do conditions. My function uses only the lightness value to calculate contrast, and while that's not ideal from a perceptual point of view, it's good enough to be useful. If you find the name confusing, perhaps we should rename it to something like 'conditional lightness' or 'pickContrastingColour' (but better!), but whatever we call it, it's still useful as it is. |
I've added a contrast colour function. Given a colour, it returns black for colours > 50% lightness, white for < 50%. This ensures you don't end up with white on white or black on black. It's used like this:
.someblock {
background-color: @background;
color: contrast(@background);
}
There are probably more subtle ways of doing this, but it should always work, unlike say, inverting the lightness value.