ChatGPT解决这个技术问题 Extra ChatGPT

Programmatically Lighten or Darken a hex color (or rgb, and blend colors)

Here is a function I was working on to programmatically lighten or darken a hex color by a specific amount. Just pass in a string like "3F6D2A" for the color (col) and a base10 integer (amt) for the amount to lighten or darken. To darken, pass in a negative number (i.e. -20).

The reason for me to do this was because of all the solutions I found, thus far, they seemed to over-complicate the issue. And I had a feeling it could be done with just a couple lines of code. Please let me know if you find any problems, or have any adjustments to make that would speed it up.

function LightenDarkenColor(col, amt) { col = parseInt(col, 16); return (((col & 0x0000FF) + amt) | ((((col >> 8) & 0x00FF) + amt) << 8) | (((col >> 16) + amt) << 16)).toString(16); } // TEST console.log( LightenDarkenColor("3F6D2A",40) );

For Development use here is an easier to read version:

function LightenDarkenColor(col, amt) { var num = parseInt(col, 16); var r = (num >> 16) + amt; var b = ((num >> 8) & 0x00FF) + amt; var g = (num & 0x0000FF) + amt; var newColor = g | (b << 8) | (r << 16); return newColor.toString(16); } // TEST console.log(LightenDarkenColor("3F6D2A", -40));

And finally a version to handle colors that may (or may not) have the "#" in the beginning. Plus adjusting for improper color values:

function LightenDarkenColor(col,amt) {
    var usePound = false;
    if ( col[0] == "#" ) {
        col = col.slice(1);
        usePound = true;
    }

    var num = parseInt(col,16);

    var r = (num >> 16) + amt;

    if ( r > 255 ) r = 255;
    else if  (r < 0) r = 0;

    var b = ((num >> 8) & 0x00FF) + amt;

    if ( b > 255 ) b = 255;
    else if  (b < 0) b = 0;

    var g = (num & 0x0000FF) + amt;

    if ( g > 255 ) g = 255;
    else if  ( g < 0 ) g = 0;

    return (usePound?"#":"") + (g | (b << 8) | (r << 16)).toString(16);
}

OK, so now it's not just a couple of lines, but it seems far simpler and if you're not using the "#" and don't need to check for colors out of range, it is only a couple of lines.

If not using the "#", you can just add it in code like:

var myColor = "3F6D2A";
myColor = LightenDarkenColor(myColor,10);
thePlaceTheColorIsUsed = ("#" + myColor);

I guess my main question is, am I correct here? Does this not encompass some (normal) situations?

If you don't get expected results when modifying colors, I suggest looking into LAB color space, which is closer to human vision. Many languages have libraries for conversion. In my experience especially shades of orange can be problematic when darkening or lightening.
Very good point. However, the main purpose of this question was to find, firstly, the fastest runtime and smallest size formula... and secondly, its accuracy. Hence, why I didn't deal with converting to HSL or whatever. Here speed and size are more important. But, as you can see with my version 2 of the formula. Using LERP to shade will result in pleasant oranges through out the shade range. Take a look at the color chart below and let me know if that shade range isn't pretty darn close to actual accurate.
I got a bit confused with the structure in here, but you're right, the orange levels for shadeColor1 seem to be very good.
Lol, you mean shadeColor2. I guess the structure you are talking about is the overall layout of the answer itself? Any hints to make more clear?
There is just one issue in the function with # above is that it doesn't create the leading zeroes if the final hex code starts with zeroes. For example, if the hex code is #00a6b7 it will output it as #a6b7, which will not working if using as a css. You can correct that by replacing the return line by this: var string = "000000" + (g | (b << 8) | (r << 16)).toString(16); return (usePound?"#":"") + string.substr(string.length-6);

C
Community

Well, this answer has become its own beast. Many new versions, it was getting stupid long. Many thanks to all of the great many contributors to this answer. But, in order to keep it simple for the masses. I archived all the versions/history of this answer's evolution to my github. And started it over clean on StackOverflow here with the newest version. A special thanks goes out to Mike 'Pomax' Kamermans for this version. He gave me the new math.

This function (pSBC) will take a HEX or RGB web color. pSBC can shade it darker or lighter, or blend it with a second color, and can also pass it right thru but convert from Hex to RGB (Hex2RGB) or RGB to Hex (RGB2Hex). All without you even knowing what color format you are using.

This runs really fast, probably the fastest, especially considering its many features. It was a long time in the making. See the whole story on my github. If you want the absolutely smallest and fastest possible way to shade or blend, see the Micro Functions below and use one of the 2-liner speed demons. They are great for intense animations, but this version here is fast enough for most animations.

This function uses Log Blending or Linear Blending. However, it does NOT convert to HSL to properly lighten or darken a color. Therefore, results from this function will differ from those much larger and much slower functions that use HSL.

jsFiddle with pSBC github > pSBC Wiki

Features:

Auto-detects and accepts standard Hex colors in the form of strings. For example: "#AA6622" or "#bb551144".

Auto-detects and accepts standard RGB colors in the form of strings. For example: "rgb(123,45,76)" or "rgba(45,15,74,0.45)".

Shades colors to white or black by percentage.

Blends colors together by percentage.

Does Hex2RGB and RGB2Hex conversion at the same time, or solo.

Accepts 3 digit (or 4 digit w/ alpha) HEX color codes, in the form #RGB (or #RGBA). It will expand them. For Example: "#C41" becomes "#CC4411".

Accepts and (Linear) blends alpha channels. If either the c0 (from) color or the c1 (to) color has an alpha channel, then the returned color will have an alpha channel. If both colors have an alpha channel, then the returned color will be a linear blend of the two alpha channels using the percentage given (just as if it were a normal color channel). If only one of the two colors has an alpha channel, this alpha will just be passed thru to the returned color. This allows one to blend/shade a transparent color while maintaining the transparency level. Or, if the transparency levels should blend as well, make sure both colors have alphas. When shading, it will pass the alpha channel straight thru. If you want basic shading that also shades the alpha channel, then use rgb(0,0,0,1) or rgb(255,255,255,1) as your c1 (to) color (or their hex equivalents). For RGB colors, the returned color's alpha channel will be rounded to 3 decimal places.

RGB2Hex and Hex2RGB conversions are implicit when using blending. Regardless of the c0 (from) color; the returned color will always be in the color format of the c1 (to) color, if one exists. If there is no c1 (to) color, then pass 'c' in as the c1 color and it will shade and convert whatever the c0 color is. If conversion only is desired, then pass 0 in as the percentage (p) as well. If the c1 color is omitted or a non-string is passed in, it will not convert.

A secondary function is added to the global as well. pSBCr can be passed a Hex or RGB color and it returns an object containing this color information. Its in the form: {r: XXX, g: XXX, b: XXX, a: X.XXX}. Where .r, .g, and .b have range 0 to 255. And when there is no alpha: .a is -1. Otherwise: .a has range 0.000 to 1.000.

For RGB output, it outputs rgba() over rgb() when a color with an alpha channel was passed into c0 (from) and/or c1 (to).

Minor Error Checking has been added. It's not perfect. It can still crash or create jibberish. But it will catch some stuff. Basically, if the structure is wrong in some ways or if the percentage is not a number or out of scope, it will return null. An example: pSBC(0.5,"salt") == null, where as it thinks #salt is a valid color. Delete the four lines which end with return null; to remove this feature and make it faster and smaller.

Uses Log Blending. Pass true in for l (the 4th parameter) to use Linear Blending.

Code:

// Version 4.0
const pSBC=(p,c0,c1,l)=>{
    let r,g,b,P,f,t,h,i=parseInt,m=Math.round,a=typeof(c1)=="string";
    if(typeof(p)!="number"||p<-1||p>1||typeof(c0)!="string"||(c0[0]!='r'&&c0[0]!='#')||(c1&&!a))return null;
    if(!this.pSBCr)this.pSBCr=(d)=>{
        let n=d.length,x={};
        if(n>9){
            [r,g,b,a]=d=d.split(","),n=d.length;
            if(n<3||n>4)return null;
            x.r=i(r[3]=="a"?r.slice(5):r.slice(4)),x.g=i(g),x.b=i(b),x.a=a?parseFloat(a):-1
        }else{
            if(n==8||n==6||n<4)return null;
            if(n<6)d="#"+d[1]+d[1]+d[2]+d[2]+d[3]+d[3]+(n>4?d[4]+d[4]:"");
            d=i(d.slice(1),16);
            if(n==9||n==5)x.r=d>>24&255,x.g=d>>16&255,x.b=d>>8&255,x.a=m((d&255)/0.255)/1000;
            else x.r=d>>16,x.g=d>>8&255,x.b=d&255,x.a=-1
        }return x};
    h=c0.length>9,h=a?c1.length>9?true:c1=="c"?!h:false:h,f=this.pSBCr(c0),P=p<0,t=c1&&c1!="c"?this.pSBCr(c1):P?{r:0,g:0,b:0,a:-1}:{r:255,g:255,b:255,a:-1},p=P?p*-1:p,P=1-p;
    if(!f||!t)return null;
    if(l)r=m(P*f.r+p*t.r),g=m(P*f.g+p*t.g),b=m(P*f.b+p*t.b);
    else r=m((P*f.r**2+p*t.r**2)**0.5),g=m((P*f.g**2+p*t.g**2)**0.5),b=m((P*f.b**2+p*t.b**2)**0.5);
    a=f.a,t=t.a,f=a>=0||t>=0,a=f?a<0?t:t<0?a:a*P+t*p:0;
    if(h)return"rgb"+(f?"a(":"(")+r+","+g+","+b+(f?","+m(a*1000)/1000:"")+")";
    else return"#"+(4294967296+r*16777216+g*65536+b*256+(f?m(a*255):0)).toString(16).slice(1,f?undefined:-2)
}

Usage:

// Setup:

let color1 = "rgb(20,60,200)";
let color2 = "rgba(20,60,200,0.67423)";
let color3 = "#67DAF0";
let color4 = "#5567DAF0";
let color5 = "#F3A";
let color6 = "#F3A9";
let color7 = "rgb(200,60,20)";
let color8 = "rgba(200,60,20,0.98631)";

// Tests:

/*** Log Blending ***/
// Shade (Lighten or Darken)
pSBC ( 0.42, color1 ); // rgb(20,60,200) + [42% Lighter] => rgb(166,171,225)
pSBC ( -0.4, color5 ); // #F3A + [40% Darker] => #c62884
pSBC ( 0.42, color8 ); // rgba(200,60,20,0.98631) + [42% Lighter] => rgba(225,171,166,0.98631)

// Shade with Conversion (use "c" as your "to" color)
pSBC ( 0.42, color2, "c" ); // rgba(20,60,200,0.67423) + [42% Lighter] + [Convert] => #a6abe1ac

// RGB2Hex & Hex2RGB Conversion Only (set percentage to zero)
pSBC ( 0, color6, "c" ); // #F3A9 + [Convert] => rgba(255,51,170,0.6)

// Blending
pSBC ( -0.5, color2, color8 ); // rgba(20,60,200,0.67423) + rgba(200,60,20,0.98631) + [50% Blend] => rgba(142,60,142,0.83)
pSBC ( 0.7, color2, color7 ); // rgba(20,60,200,0.67423) + rgb(200,60,20) + [70% Blend] => rgba(168,60,111,0.67423)
pSBC ( 0.25, color3, color7 ); // #67DAF0 + rgb(200,60,20) + [25% Blend] => rgb(134,191,208)
pSBC ( 0.75, color7, color3 ); // rgb(200,60,20) + #67DAF0 + [75% Blend] => #86bfd0

/*** Linear Blending ***/
// Shade (Lighten or Darken)
pSBC ( 0.42, color1, false, true ); // rgb(20,60,200) + [42% Lighter] => rgb(119,142,223)
pSBC ( -0.4, color5, false, true ); // #F3A + [40% Darker] => #991f66
pSBC ( 0.42, color8, false, true ); // rgba(200,60,20,0.98631) + [42% Lighter] => rgba(223,142,119,0.98631)

// Shade with Conversion (use "c" as your "to" color)
pSBC ( 0.42, color2, "c", true ); // rgba(20,60,200,0.67423) + [42% Lighter] + [Convert] => #778edfac

// RGB2Hex & Hex2RGB Conversion Only (set percentage to zero)
pSBC ( 0, color6, "c", true ); // #F3A9 + [Convert] => rgba(255,51,170,0.6)

// Blending
pSBC ( -0.5, color2, color8, true ); // rgba(20,60,200,0.67423) + rgba(200,60,20,0.98631) + [50% Blend] => rgba(110,60,110,0.83)
pSBC ( 0.7, color2, color7, true ); // rgba(20,60,200,0.67423) + rgb(200,60,20) + [70% Blend] => rgba(146,60,74,0.67423)
pSBC ( 0.25, color3, color7, true ); // #67DAF0 + rgb(200,60,20) + [25% Blend] => rgb(127,179,185)
pSBC ( 0.75, color7, color3, true ); // rgb(200,60,20) + #67DAF0 + [75% Blend] => #7fb3b9

/*** Other Stuff ***/
// Error Checking
pSBC ( 0.42, "#FFBAA" ); // #FFBAA + [42% Lighter] => null  (Invalid Input Color)
pSBC ( 42, color1, color5 ); // rgb(20,60,200) + #F3A + [4200% Blend] => null  (Invalid Percentage Range)
pSBC ( 0.42, {} ); // [object Object] + [42% Lighter] => null  (Strings Only for Color)
pSBC ( "42", color1 ); // rgb(20,60,200) + ["42"] => null  (Numbers Only for Percentage)
pSBC ( 0.42, "salt" ); // salt + [42% Lighter] => null  (A Little Salt is No Good...)

// Error Check Fails (Some Errors are not Caught)
pSBC ( 0.42, "#salt" ); // #salt + [42% Lighter] => #a5a5a500  (...and a Pound of Salt is Jibberish)

// Ripping
pSBCr ( color4 ); // #5567DAF0 + [Rip] => [object Object] => {'r':85,'g':103,'b':218,'a':0.941}

The picture below will help show the difference in the two blending methods:

https://i.imgur.com/FBe90R8.png

Micro Functions

If you really want speed and size, you will have to use RGB not HEX. RGB is more straightforward and simple, HEX writes too slow and comes in too many flavors for a simple two-liner (IE. it could be a 3, 4, 6, or 8 digit HEX code). You will also need to sacrifice some features, no error checking, no HEX2RGB nor RGB2HEX. As well, you will need to choose a specific function (based on its function name below) for the color blending math, and if you want shading or blending. These functions do support alpha channels. And when both input colors have alphas it will Linear Blend them. If only one of the two colors has an alpha, it will pass it straight thru to the resulting color. Below are two liner functions that are incredibly fast and small:

const RGB_Linear_Blend=(p,c0,c1)=>{
    var i=parseInt,r=Math.round,P=1-p,[a,b,c,d]=c0.split(","),[e,f,g,h]=c1.split(","),x=d||h,j=x?","+(!d?h:!h?d:r((parseFloat(d)*P+parseFloat(h)*p)*1000)/1000+")"):")";
    return"rgb"+(x?"a(":"(")+r(i(a[3]=="a"?a.slice(5):a.slice(4))*P+i(e[3]=="a"?e.slice(5):e.slice(4))*p)+","+r(i(b)*P+i(f)*p)+","+r(i(c)*P+i(g)*p)+j;
}

const RGB_Linear_Shade=(p,c)=>{
    var i=parseInt,r=Math.round,[a,b,c,d]=c.split(","),P=p<0,t=P?0:255*p,P=P?1+p:1-p;
    return"rgb"+(d?"a(":"(")+r(i(a[3]=="a"?a.slice(5):a.slice(4))*P+t)+","+r(i(b)*P+t)+","+r(i(c)*P+t)+(d?","+d:")");
}

const RGB_Log_Blend=(p,c0,c1)=>{
    var i=parseInt,r=Math.round,P=1-p,[a,b,c,d]=c0.split(","),[e,f,g,h]=c1.split(","),x=d||h,j=x?","+(!d?h:!h?d:r((parseFloat(d)*P+parseFloat(h)*p)*1000)/1000+")"):")";
    return"rgb"+(x?"a(":"(")+r((P*i(a[3]=="a"?a.slice(5):a.slice(4))**2+p*i(e[3]=="a"?e.slice(5):e.slice(4))**2)**0.5)+","+r((P*i(b)**2+p*i(f)**2)**0.5)+","+r((P*i(c)**2+p*i(g)**2)**0.5)+j;
}

const RGB_Log_Shade=(p,c)=>{
    var i=parseInt,r=Math.round,[a,b,c,d]=c.split(","),P=p<0,t=P?0:p*255**2,P=P?1+p:1-p;
    return"rgb"+(d?"a(":"(")+r((P*i(a[3]=="a"?a.slice(5):a.slice(4))**2+t)**0.5)+","+r((P*i(b)**2+t)**0.5)+","+r((P*i(c)**2+t)**0.5)+(d?","+d:")");
}

Want more info? Read the full writeup on github.

PT

(P.s. If anyone has the math for another blending method, please share.)


A PHP version for those who need it: gist.github.com/chaoszcat/5325115#file-gistfile1-php
I used TinyColor -- tinycolor.darken(color,amount);
Great post ... :) ... just created Swift extension of it: gist.github.com/matejukmar/1da47f7a950d1ba68a95
Sorry, I apparently missed that point. There are maybe two reasons. The first and obvious is that I use Math.Round and you don't get to use decimal numbers for accurate coloring (colors dont have decimals in hex). For example, if the Red channel is 8, add 10% you get 8.8 which rounds to 9. Then take 9.09% away from 9 and you get 8.1819. Which rounds to 8 so that's a bad example. But it still illustrates that you are taking 9.09% of 9 and not 8.8. So there might be some numbers in there that don't round back out exactly the same as my example here.
'pSBCr' is not defined no-undef
D
David Sherret

I made a solution that works very nice for me:

function shadeColor(color, percent) {

    var R = parseInt(color.substring(1,3),16);
    var G = parseInt(color.substring(3,5),16);
    var B = parseInt(color.substring(5,7),16);

    R = parseInt(R * (100 + percent) / 100);
    G = parseInt(G * (100 + percent) / 100);
    B = parseInt(B * (100 + percent) / 100);

    R = (R<255)?R:255;  
    G = (G<255)?G:255;  
    B = (B<255)?B:255;  

    var RR = ((R.toString(16).length==1)?"0"+R.toString(16):R.toString(16));
    var GG = ((G.toString(16).length==1)?"0"+G.toString(16):G.toString(16));
    var BB = ((B.toString(16).length==1)?"0"+B.toString(16):B.toString(16));

    return "#"+RR+GG+BB;
}

Example Lighten:

shadeColor("#63C6FF",40);

Example Darken:

shadeColor("#63C6FF",-40);

Nice, I like the percentage! +1 Tho, I would might do R = ((R<255)?R:255).toString(16); then R = R.length==1 ? "0"+R : R for speed. And im not sure the point of the toUpperCase?
It's unnecessary. I only add that for pretty print while test. I will edit that.
Very nice. However, should 100% lighter not become fully white and 100% dark always black, no matter what color? it seems -100 does make any color black, but 100 (positive) does not make it fully white.
doesnt work with solid colors like #ff0000, #00ff00, #0000ff
To make it work with black color I just did this hack var R = parseInt(color.substring(1, 3), 16) var G = parseInt(color.substring(3, 5), 16) var B = parseInt(color.substring(5, 7), 16) if (R == 0) R = 32; if (G == 0) G = 32; if (B == 0) B = 32;
s
supersan

Here is a super simple one liner based on Eric's answer

function adjust(color, amount) {
    return '#' + color.replace(/^#/, '').replace(/../g, color => ('0'+Math.min(255, Math.max(0, parseInt(color, 16) + amount)).toString(16)).substr(-2));
}

Examples:

adjust('#ffffff', -20) => "#ebebeb"
adjust('000000', 20) => "#141414"

"super simple" .
What is the amount? Could help to clarify if it's px, percent, etc.
You can make it a little bit shorter by using \w\w (which doesn't match #) in that regexp instead of removing and re-adding the #. I also don't quite follow why you're adding the 0 and then removing it? Is this some JS thing I'm not following (you're already using toString(), so you don't need to cast it?) At any rate, I ended up with the following, which seems to behave the same: var adj = (c, a) => c.replace(/\w\w/g, m => Math.min(255, Math.max(0, parseInt(m, 16) + a)).toString(16))
Added a demonstrator for this... jsfiddle.net/Abeeee/xeos0p42/20
@MartinTournoij : The '0'+ and substr(-2) are there for those numbers between 0 and 15, which will be only one digit in hexadecimal and thus give a malformed hexadecimal color string. He add the '0' in front and keep the last two digits so that 9 becomes '09', not just '9'. Good first point though! 👍
a
antoni

I am adding my 2 cents here, a satisfyingly small combination of different answers:

const colorShade = (col, amt) => {
  col = col.replace(/^#/, '')
  if (col.length === 3) col = col[0] + col[0] + col[1] + col[1] + col[2] + col[2]

  let [r, g, b] = col.match(/.{2}/g);
  ([r, g, b] = [parseInt(r, 16) + amt, parseInt(g, 16) + amt, parseInt(b, 16) + amt])

  r = Math.max(Math.min(255, r), 0).toString(16)
  g = Math.max(Math.min(255, g), 0).toString(16)
  b = Math.max(Math.min(255, b), 0).toString(16)

  const rr = (r.length < 2 ? '0' : '') + r
  const gg = (g.length < 2 ? '0' : '') + g
  const bb = (b.length < 2 ? '0' : '') + b

  return `#${rr}${gg}${bb}`
}

accepts a color starting with # or not, with 6 characters or 3 characters.

example of use: colorShade('#54b946', -40)

Here is the output of 4 colors with 3 shades lighter and 3 shades darker for each of them (amount is a multiple of 40 here).

https://i.stack.imgur.com/QY7ja.png


W
Worm

This is what I used based on your function. I prefer to use steps over percentage because it's more intuitive for me.

For example, 20% of a 200 blue value is much different than 20% of a 40 blue value.

Anyways, here's my modification, thanks for your original function.

function adjustBrightness(col, amt) {

    var usePound = false;

    if (col[0] == "#") {
        col = col.slice(1);
        usePound = true;
    }

    var R = parseInt(col.substring(0,2),16);
    var G = parseInt(col.substring(2,4),16);
    var B = parseInt(col.substring(4,6),16);

    // to make the colour less bright than the input
    // change the following three "+" symbols to "-"
    R = R + amt;
    G = G + amt;
    B = B + amt;

    if (R > 255) R = 255;
    else if (R < 0) R = 0;

    if (G > 255) G = 255;
    else if (G < 0) G = 0;

    if (B > 255) B = 255;
    else if (B < 0) B = 0;

    var RR = ((R.toString(16).length==1)?"0"+R.toString(16):R.toString(16));
    var GG = ((G.toString(16).length==1)?"0"+G.toString(16):G.toString(16));
    var BB = ((B.toString(16).length==1)?"0"+B.toString(16):B.toString(16));

    return (usePound?"#":"") + RR + GG + BB;

}

Found this much more useful than the top answer because the top answer was making my colours very intense instead of just darker. Cheers Eric
By far the best answer, kudos!
C
Cool Acid

I tried your function and there was a little bug: If some final 'r' value is 1 digit only, the result comes up like: 'a0a0a' when the right value is '0a0a0a', for example. I just quick-fixed it by adding this instead of your return:

var rStr = (r.toString(16).length < 2)?'0'+r.toString(16):r.toString(16);
var gStr = (g.toString(16).length < 2)?'0'+g.toString(16):g.toString(16);
var bStr = (b.toString(16).length < 2)?'0'+b.toString(16):b.toString(16);

return (usePound?"#":"") + rStr + gStr + bStr;

Maybe it's not so nice but it do the work. Great function, BTW. Just what I needed. :)


Thanks for the debug and compliment! Too bad its not an answer to whether or not there is a faster way, which is my main question. Like possibly one using all hex and no base conversions. Tho, I guess you did tell me if i had correct code (+1). Unfortunately, the fix added considerably more overhead (now your calling toString 6 times), and slightly less KISS. Maybe it would be faster to check if the base10 number is 15 or less, before the base16 conversion. But, I like!
N
Natan Braslavski

Based on the, David Sherret and Pablo, answer above converted the solution to safer version for Typescript

/**
 * @param color Hex value format: #ffffff or ffffff
 * @param decimal lighten or darken decimal value, example 0.5 to lighten by 50% or 1.5 to darken by 50%.
 */
static shadeColor(color: string, decimal: number): string {
    const base = color.startsWith('#') ? 1 : 0;

    let r = parseInt(color.substring(base, 3), 16);
    let g = parseInt(color.substring(base + 2, 5), 16);
    let b = parseInt(color.substring(base + 4, 7), 16);

    r = Math.round(r / decimal);
    g = Math.round(g / decimal);
    b = Math.round(b / decimal);

    r = (r < 255)? r : 255;
    g = (g < 255)? g : 255;
    b = (b < 255)? b : 255;

    const rr = ((r.toString(16).length === 1)? `0${r.toString(16)}` : r.toString(16));
    const gg = ((g.toString(16).length === 1)? `0${g.toString(16)}` : g.toString(16));
    const bb = ((b.toString(16).length === 1)? `0${b.toString(16)}` : b.toString(16));

    return `#${rr}${gg}${bb}`;
}
  

C
Community

have you thought about an rgb > hsl conversion? then just move the Luminosity up and down? thats the way I would go.

A quick look for some algorithms got me the following sites.

PHP: http://serennu.com/colour/rgbtohsl.php

Javascript: http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript

EDIT the above link is no longer valid. You can view git hub for the page source or the gist

Alternatively another StackOverflow question might be a good place to look.

Even though this is not the right choice for the OP the following is an approximation of the code I was originally suggesting. (Assuming you have rgb/hsl conversion functions)

var SHADE_SHIFT_AMOUNT = 0.1; 

function lightenShade(colorValue)
{
    if(colorValue && colorValue.length >= 6)
    {
        var redValue = parseInt(colorValue.slice(-6,-4), 16);
        var greenValue = parseInt(colorValue.slice(-4,-2), 16);
        var blueValue = parseInt(colorValue.slice(-2), 16);

        var hsl = rgbToHsl(redValue, greenValue, blueValue);
        hsl[2]= Math.min(hsl[2] + SHADE_SHIFT_AMOUNT, 1);
        var rgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
        return "#" + rgb[0].toString(16) + rgb[1].toString(16) + rgb[2].toString(16);
    }
    return null;
}

function darkenShade(colorValue)
{
    if(colorValue && colorValue.length >= 6)
    {
        var redValue = parseInt(colorValue.slice(-6,-4), 16);
        var greenValue = parseInt(colorValue.slice(-4,-2), 16);
        var blueValue = parseInt(colorValue.slice(-2), 16);

        var hsl = rgbToHsl(redValue, greenValue, blueValue);
        hsl[2]= Math.max(hsl[2] - SHADE_SHIFT_AMOUNT, 0);
        var rgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
        return "#" + rgb[0].toString(16) + rgb[1].toString(16) + rgb[2].toString(16);
    }
    return null;
}

This assumes:

You have functions hslToRgb and rgbToHsl. The parameter colorValue is a string in the form #RRGGBB

Although if we are discussing css there is a syntax for specifying hsl/hsla for IE9/Chrome/Firefox.


Interesting, but then wouldn't I have to convert from hex string to rgb to hsl? Seems like its more complicated. Maybe I'm missing something. But, I am looking for a KISS way to do it, as well as fast as possible (execution time). I feel ideally, if i could do it all in hex that would be the fastest. But, the solution I've developed here involves going to rgb to be able to add an incremental amount.
Yes i assume it would be slower, more complicated and if you don't use rgb to hsl conversion anywhere else then it probably wouldn't be the most simplistic solution. It would, however, be more accurate than adding to rgb values although I'm not much of a colour person myself. It all depends on how accurate you want to be I guess.
Whats the loss of accuracy you mention? I assume you mean all [web] colors are not reachable with rgb or something?
As I said I don't know that much about colour: wiki Color Theory
@Pimp Trizkit: It's less accurate because (and this is just my theory... I'm not a color expert) you're shifting each channel the same amount, no matter how much of that color there was to begin with. I think that would result in decreasing the saturation because you're bringing the channels closer to each other (by percentage). Of course, if you overflow/underflow that's unavoidable anyway.
K
Kamil Kiełczewski

Your approach is ok :) I simplify your shortest version a little (for saturation control look here)

(col,amt)=> (+('0x'+col)+amt*0x010101).toString(16).padStart(6,0)

// Similar to OP shortest version, we not have here # and colors range checking var LightenDarkenColor = (col,amt) => (+('0x'+col)+amt*0x010101).toString(16).padStart(6,0); // ------ // TEST // ------ function update() { let c= col.value.padEnd(6,'0').slice(0,6); let color = '#'+LightenDarkenColor(c, +amt.value); oldColor.innerHTML = 'Old: #'+c; oldColor.style = `background: #${c}`; newColor.innerHTML = 'New: '+color newColor.style = `background: ${color}`; } update(); .box{ width: 100px; height: 100px; margin: 10px; display: inline-block}

And version with # and color ranges checking

// # and colors range checking var LightenDarkenColor = (col,amt) => '#'+col.slice(1).match(/../g) .map(x=>(x=+`0x${x}`+amt,x<0?0:(x>255?255:x)) .toString(16).padStart(2,0)).join``; // ------ // TEST // ------ function update() { let c= col.value.padEnd(6,'0').slice(0,7); let color = LightenDarkenColor(c, +amt.value); oldColor.innerHTML = 'Old: '+c; oldColor.style = `background: ${c}`; newColor.innerHTML = 'New: '+color newColor.style = `background: ${color}`; } update(); .box{ width: 100px; height: 100px; margin: 10px; display: inline-block}


I really liked the idea of this short and powerful function, but it does not seem to work for all the colors. :/ For instance: #54b946 apply -30, -60, -90, -120, it returns a purple instead of lighter green.
@antoni I update answer and make more interaction to snippets (but not change main procedure code). In bottom snippet I check your color - and it seems that eveything works ok. If not - can you provide hex number with exact result you expect for #54b946 and -120 ?
That was my bad @kamil, thanks for your function. Smart and concise. +1 Your tester helped me see the mistake was elsewhere in my code.
Great answer, simple and very useful!
u
user1618171

C# Version... note that I am getting color strings in this format #FF12AE34, and need to cut out the #FF.

    private string GetSmartShadeColorByBase(string s, float percent)
    {
        if (string.IsNullOrEmpty(s))
            return "";
        var r = s.Substring(3, 2);
        int rInt = int.Parse(r, NumberStyles.HexNumber);
        var g = s.Substring(5, 2);
        int gInt = int.Parse(g, NumberStyles.HexNumber);
        var b = s.Substring(7, 2);
        int bInt = int.Parse(b, NumberStyles.HexNumber);

        var t = percent < 0 ? 0 : 255;
        var p = percent < 0 ? percent*-1 : percent;

        int newR = Convert.ToInt32(Math.Round((t - rInt) * p) + rInt);
        var newG = Convert.ToInt32(Math.Round((t - gInt) * p) + gInt);
        var newB = Convert.ToInt32(Math.Round((t - bInt) * p) + bInt);

        return String.Format("#{0:X2}{1:X2}{2:X2}", newR, newG, newB);
    }

Never used C# before, but it looks like the last three variable declarations are wierd. An int and two vars for the same type of data.
The var keyword in C# means let the compiler infer the type at compile time. So in the example above int and var define a variable the same type - int. This is useful if you have a long type name, or if you want to reference an anonymous type. It's weird because user1618171 has mixed two variable declaration styles - probably a typo.
T
Torbjörn Josefsson

I wanted to change a color to a specific brightness level - no matter what brightness the color was before - here's a simple JS function that seems to work well, although I'm sure it could be shorter

function setLightPercentage(col: any, p: number) {
    const R = parseInt(col.substring(1, 3), 16);
    const G = parseInt(col.substring(3, 5), 16);
    const B = parseInt(col.substring(5, 7), 16);
    const curr_total_dark = (255 * 3) - (R + G + B);

    // calculate how much of the current darkness comes from the different channels
    const RR = ((255 - R) / curr_total_dark);
    const GR = ((255 - G) / curr_total_dark);
    const BR = ((255 - B) / curr_total_dark);

    // calculate how much darkness there should be in the new color
    const new_total_dark = ((255 - 255 * (p / 100)) * 3);

    // make the new channels contain the same % of available dark as the old ones did
    const NR = 255 - Math.round(RR * new_total_dark);
    const NG = 255 - Math.round(GR * new_total_dark);
    const NB = 255 - Math.round(BR * new_total_dark);

    const RO = ((NR.toString(16).length === 1) ? "0" + NR.toString(16) : NR.toString(16));
    const GO = ((NG.toString(16).length === 1) ? "0" + NG.toString(16) : NG.toString(16));
    const BO = ((NB.toString(16).length === 1) ? "0" + NB.toString(16) : NB.toString(16));

    return "#" + RO + GO + BO;}

Coolio! I assume that p has range 0-100? I don't even know how to correctly define brightness in RGB, thats an HSL thing. For instance, is #FF00FF brighter than #FF0000? If so, that would imply that magenta is twice as bright as red. Therefore, the pure red test is used. Pass in pure red #FF0000, set to 50% brightness, and here we get #FF4040, is that right? I would of guessed to make red 50% brightness, we would be getting darker, seeing that its already fully bright.. as in #800000 or 150% brightness would be #FF8080. Is pink a brighter red? or is red already fully bright?
You're right - I should have mentioned that p must be in the range 1-100!
#FF00FF has 255 as value in the red channel, 0 in the green channel and 255 in the blue channel. The higher the combined values in the channels are, the higher the brightness of the color. The number p states that we want the new color to be 50% as bright as the original color can be. I'm not 100% that #FF4040 is the correct answer to "50% as bright as possible Red". Producing darker shades (with, in this case, lower value in the red channel) would require modification
Yes, I was just pointing out the ambiguity in talking about brightness in RGB. If converted to HSL, the L channel is literally brightness. My [personal mental] issue here is that, to me, #FF0000 is fully bright. And #FF4040 is Lighter but not brighter.... to me lighter means closer to white, like pink is. And brightness is how much its got, and its got full red, so red, is full bright. Therefore, #FF0000 can't be made brighter.. but rather.. lighter... maybe I'm just a freak, lol!! I really don't know color theory, sooo, I'm really just talking out my a...
But I know that when I change the brightness on my monitor, the Reds don't turn pink... to me. So this is probably where I started my logic.
D
Dev

I just used the hex number preceded by '#'.

var x = 0xf0f0f0;
x=x+0xf00; //set this value as you wish programatically
document.getElementById("heading").style = 'background-color: #'+x.toString(16);

higher the number ..lighter the color


C
Chris Catignani

I needed it in C#, it may help .net developers

public static string LightenDarkenColor(string color, int amount)
    {
        int colorHex = int.Parse(color, System.Globalization.NumberStyles.HexNumber);
        string output = (((colorHex & 0x0000FF) + amount) | ((((colorHex >> 0x8) & 0x00FF) + amount) << 0x8) | (((colorHex >> 0xF) + amount) << 0xF)).ToString("x6");
        return output;
    }

You converted the "non working" function that doesn't deal with leading zeros, and can go above FF in the sums, = modify the color component above...
When I tried the function it didn't work because of non hex values (16 = 0xF) and (8 = 0x8) and gives a color in 8 positions, but now it works very well
did you try increasing from FF? (say from 0xFFFFFF + 0x000004) : your code rolls over that max value (say to 0x1000003), instead of not increasing, and especially setting the 2 upper color components to 00 which is the biggest change they could do...
you are right, thank you for the remark, except an input of limits arround fff and 000 it will work fine.
i
izzaki

My version written in typescript:

function changeColorLightness(color: number, lightness: number): number {
    return (Math.max(0, Math.min(((color & 0xFF0000) / 0x10000) + lightness, 0xFF)) * 0x10000) +
        (Math.max(0, Math.min(((color & 0x00FF00) / 0x100) + lightness, 0xFF)) * 0x100) +
        (Math.max(0, Math.min(((color & 0x0000FF)) + lightness, 0xFF)));
}

explanation:

export function changeColorLightness(color: number, lightness: number): number {
    const r = (color & 0xFF0000) / 0x10**4;
    const g = (color & 0x00FF00) / 0x10**2;
    const b = (color & 0x0000FF);

    const changedR = Math.max(0, Math.min(r + lightness, 0xFF));
    const changedG = Math.max(0, Math.min(g + lightness, 0xFF));
    const changedB = Math.max(0, Math.min(b + lightness, 0xFF));

    return (changedR * 0x10**4) + (changedG * 0x10**2) + changedB;
}

usage:

changeColorLightness(0x00FF00, 0x50);
changeColorLightness(parseInt("#00FF00".replace('#',''), 16), 0x50);
changeColorLightness(0x00FF00, 127.5);

J
Jason Williams

The following method will allow you to lighten or darken the exposure value of a Hexadecimal (Hex) color string:

private static string GetHexFromRGB(byte r, byte g, byte b, double exposure)
{
    exposure = Math.Max(Math.Min(exposure, 1.0), -1.0);
    if (exposure >= 0)
    {
        return "#"
            + ((byte)(r + ((byte.MaxValue - r) * exposure))).ToString("X2")
            + ((byte)(g + ((byte.MaxValue - g) * exposure))).ToString("X2")
            + ((byte)(b + ((byte.MaxValue - b) * exposure))).ToString("X2");
    }
    else
    {
        return "#"
            + ((byte)(r + (r * exposure))).ToString("X2")
            + ((byte)(g + (g * exposure))).ToString("X2")
            + ((byte)(b + (b * exposure))).ToString("X2");
    }

}

For the last parameter value in GetHexFromRGB(), Pass in a double value somewhere between -1 and 1 (-1 is black, 0 is unchanged, 1 is white):

// split color (#e04006) into three strings
var r = Convert.ToByte("e0", 16);
var g = Convert.ToByte("40", 16);
var b = Convert.ToByte("06", 16);

GetHexFromRGB(r, g, b, 0.25);  // Lighten by 25%;

J
Jijin P

There is lack of support for colors starting from 00 ie "#000623" but here is the fix

function lightenDarkenColor(colorCode, amount) {
 let usePound = false;

 if (colorCode[0] == "#") {
     colorCode = colorCode.slice(1);
     usePound = true;
 }
 const num = parseInt(colorCode, 16);
 let r = (num >> 16) + amount;

 if (r > 255) {
     r = 255;
 } else if (r < 0) {
     r = 0;
 }

 let b = ((num >> 8) & 0x00FF) + amount;

 if (b > 255) {
     b = 255;
 } else if (b < 0) {
     b = 0;
 }

 let g = (num & 0x0000FF) + amount;

 if (g > 255) {
     g = 255;
 } else if (g < 0) {
     g = 0;
 }
 let color = (g | (b << 8) | (r << 16)).toString(16);
 while (color.length < 6){
   color = 0 + color;
 }
 return (usePound ? '#' : '') + color;  
}

j
jsebestyan

How to simple shade color in PHP?

<?php
function shadeColor ($color='#cccccc', $percent=-25) {

  $color = Str_Replace("#",Null,$color);

  $r = Hexdec(Substr($color,0,2));
  $g = Hexdec(Substr($color,2,2));
  $b = Hexdec(Substr($color,4,2));

  $r = (Int)($r*(100+$percent)/100);
  $g = (Int)($g*(100+$percent)/100);
  $b = (Int)($b*(100+$percent)/100);

  $r = Trim(Dechex(($r<255)?$r:255));  
  $g = Trim(Dechex(($g<255)?$g:255));  
  $b = Trim(Dechex(($b<255)?$b:255));

  $r = ((Strlen($r)==1)?"0{$r}":$r);
  $g = ((Strlen($g)==1)?"0{$g}":$g);
  $b = ((Strlen($b)==1)?"0{$b}":$b);

  return (String)("#{$r}{$g}{$b}");
}

echo shadeColor(); // #999999

This is a php version of Pablo's answer. Unfortunately, its longer and slower than the final solution and it does not lighten colors accurately. It does darken them accurately tho. Test with pure red (#FF0000), a lighten of 25% should be (#FF4040). Check out the end of my answer for Kevin M's PHP version of the final solution v2.
B
B T

I made a port of the excellent xcolor library to remove its jQuery dependency. There are a ton of functions in there including lightening and darkening colors.

Really, converting hex to RGB is a completely separate function from lightening or darkening colors. Keep things DRY please. In any case, once you have an RGB color, you can just add the difference between the light level you want and the light level you have to each of the RGB values:

var lightness = function(level) {
    if(level === undefined) {
        return Math.max(this.g,this.r,this.b)
    } else {
        var roundedLevel = Math.round(level) // fractions won't work here
        var levelChange = roundedLevel - this.lightness()

        var r = Math.max(0,this.r+levelChange)
        var g = Math.max(0,this.g+levelChange)
        var b = Math.max(0,this.b+levelChange)

        if(r > 0xff) r = 0xff
        if(g > 0xff) g = 0xff
        if(b > 0xff) b = 0xff

        return xolor({r: r, g: g, b: b})
    }
}

var lighter = function(amount) {
    return this.lightness(this.lightness()+amount)
}

See https://github.com/fresheneesz/xolor for more of the source.


I haven't yet analyzed the code as it pertains to my OP (speed/size/accuracy). But at first read there are some comments to be made:1) I agree that converting hex to RGB can be seen as a completely separate function.. IF my problem was intended to be solved with a dry function, which was not a requirement. The intention here was to have an answer (see my Version 2) that was super fast and super tiny (2 lines!) and one that lightened and darkened a hex color... specifically... as a stand-alone self-contained function. So that, in its final usage, it will be a simple single function call.
2) And the case with Version 3, by popular demand, is the intention to have a completely stand-alone self-contained universal function, as fast and as small as possible, that can blindly take a hex or RGB color and in all their variations. Hence, a conversion of hex to RGB is needed.
3) Upon simple analysis of the code. It does appear that it would run much slower and it is obviously much larger than my Version 2 (which is the real answer to my OP; version 3 was for the masses). To be fair, I should compare this code with my RGB Version 2 which does not do a conversion and seems to answer your point about dryness. And truthfully speaking, your port is not much more simple to understand than my 2 liner for hex. So, while its dryer, its not actually that much, if any, simpler. (the dryness didn't help much for understand-ability)
4) My RGB Version 2 is a no conversion 2 line function if you want that. My particular solution for my original OP wanted hex. That's why there are two different types of Version 2. But you mention the point about dryness and hex conversions so we are now really focusing on version 3. Version 3 came much later; only after Version 2 was popular.
5) While I will agree that dryness generally helps for universality. And in most cases, for understand-ability. Unfortunately its at a cost in this example. These costs are that its much larger and seemingly much slower and seemingly uses more memory both on the stack (with its recursive-like nature) and in global (2 functions; as compared to v2).
u
user2655360

I've long wanted to be able to produce tints/shades of colours, here is my JavaScript solution:

const varyHue = function (hueIn, pcIn) {
    const truncate = function (valIn) {
        if (valIn > 255) {
            valIn = 255;
        } else if (valIn < 0)  {
            valIn = 0;
        }
        return valIn;
    };

    let red   = parseInt(hueIn.substring(0, 2), 16);
    let green = parseInt(hueIn.substring(2, 4), 16);
    let blue  = parseInt(hueIn.substring(4, 6), 16);
    let pc    = parseInt(pcIn, 10);    //shade positive, tint negative
    let max   = 0;
    let dif   = 0;

    max = red;

    if (pc < 0) {    //tint: make lighter
        if (green < max) {
            max = green;
        }

        if (blue < max) {
            max = blue;
        }

        dif = parseInt(((Math.abs(pc) / 100) * (255 - max)), 10);

        return leftPad(((truncate(red + dif)).toString(16)), '0', 2)  + leftPad(((truncate(green + dif)).toString(16)), '0', 2) + leftPad(((truncate(blue + dif)).toString(16)), '0', 2);
    } else {    //shade: make darker
        if (green > max) {
            max = green;
        }

        if (blue > max) {
            max = blue;
        }

        dif = parseInt(((pc / 100) * max), 10);

        return leftPad(((truncate(red - dif)).toString(16)), '0', 2)  + leftPad(((truncate(green - dif)).toString(16)), '0', 2) + leftPad(((truncate(blue - dif)).toString(16)), '0', 2);
    }
};

Some usage examples would help. And maybe some explanation as to why this version over the others. This version seems to run considerably slower. And its much longer. And it doesn't seem to shade accurately. It looks like your using LERP, or something similar..which is good. Unfortunately, its only from one channel, then that same value is used across all channels. This is not right, in order to get higher accuracy, you should LERP each channel individually. As my answer to this question does. Plus its smaller and faster and checks for errors and handles rgb, and does conversions,I could go on
A usage example: varyHue("6e124c", 77) where the first argument is the colour in hex and the second the percentage change. A positive percentage change shades (darkens) whilst a negative value tints (lightens) the result. I wrote the routine as my first attempt just a few hours before I came upon this page and posted it simply as a matter of interest. I was not aware that I had to better your effort or that I required your approval before doing so. Its entirely my own work without reference to anybody else. I've not heard of LERP I'll check it out, thanks for the suggestion.
Hehe, well, of course you don't have to do anything! And we all thank you for your efforts! My first main concerns were the ones listed first. Trying to help you with your answer so that it might get votes. (show usages, and explanation of how it works, etc) The other stuff is obviously a quick analysis to help further everyone's knowledge. Sorry, if it seemed a bit aggressive. But another suggestion is to make it accept the # of hex colors. Sorry if it seemed like.. "approval"... I saw it as peer review. If you don't want someone to analysis your code, or offer feedback, I apologize.