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 (ie -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);
}
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);
}
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?
TL;DR? --Want simple lighten/darken(shading)? Skip down to Version 2, pick the one for RGB or Hex. --Want a full featured shader/blender/converter with errorcheck and alpha and 3 Digit hex? Use Version 3 near the bottom.
Play with version 3.1: jsfiddle > shadeBlendConvert Example
Version 3.1 at GitHub: Goto GitHub > PJs > pSBC
After some pondering... I decided to answer my own question. A year and a half later. This was truly an adventure with ideas from several helpful users, and I thank you all! This one is for the team! While its not necessarily the answer I was looking for. Because if what James Khoury is saying is true, then there is no true hex math in javascript, I have to use decimals, this double conversion is necessary. If we make this assumption, then this is probably the fastest way I've seen (or can think of) to lighten (add white) or darken (add black) an arbitrary RBG color by percentage. It also accounts for the issues Cool Acid mentioned on his answer to this question (it pads 0s). But this version calls toString
only once. This also accounts for out of range (it will enforce 0 and 255 as limits).
But beware, the color input has to be EXACTLY 7 characters, like #08a35c
. (or 6 if using the top version)
Thanks to Pablo for the inspiration and idea for using percentage. For this I will keep the function name the same! lol! However, this one is different, as it normalizes the percentage to 255 and thus adding the same amount to each color (more white). If you pass in 100 for percent
it will make your color pure white. If you pass in 0 for percent
, nothing will happen. If you pass in 1 for percent
it will add 3 shades to all colors (2.55 shades per 1%, rounded). So your really passing in a percentage of white (or black, use negative). Therefore, this version allows you to lighten pure red (FF0000), for example.
I also used insight from Keith Mashinter's answer to this question: How to convert decimal to hex in JavaScript?
I removed some, seemly, unnecessary parenthesis. (like in the double ternary statement and in crafting G) Not sure if this will mess with the operator precedence in some environments. Tested good in FireFox.
function shadeColor1(color, percent) { // deprecated. See below.
var num = parseInt(color,16),
amt = Math.round(2.55 * percent),
R = (num >> 16) + amt,
G = (num >> 8 & 0x00FF) + amt,
B = (num & 0x0000FF) + amt;
return (0x1000000 + (R<255?R<1?0:R:255)*0x10000 + (G<255?G<1?0:G:255)*0x100 + (B<255?B<1?0:B:255)).toString(16).slice(1);
}
Or, if you want it to handle the "#":
function shadeColor1(color, percent) { // deprecated. See below.
var num = parseInt(color.slice(1),16), amt = Math.round(2.55 * percent), R = (num >> 16) + amt, G = (num >> 8 & 0x00FF) + amt, B = (num & 0x0000FF) + amt;
return "#" + (0x1000000 + (R<255?R<1?0:R:255)*0x10000 + (G<255?G<1?0:G:255)*0x100 + (B<255?B<1?0:B:255)).toString(16).slice(1);
}
Hows that for two lines of code?
EDIT: Fix B<->G swap goof. Thanks svachalek!
-- UPDATE - Version 2 with Blending --
A little over a year later, again, and its still going. But this time I think its done. Noting the problems mentioned about not using HSL to properly lighten the color. There is a technique that eliminates most of that inaccuracy without having to convert to HSL. The main problem is that a color channel will get fully saturated before the rest of the color. Causing a shift in the hue after that point. I found these questions here, here and here which got me on track. Mark Ransom's post showed me the difference, and Keith's post showed me the way. Lerp is the savior. It is the same as blending colors, so I created a blendColors
function as well.
TL;DR - For simple lighten/darken use this function shadeColor2
below. Or its RGB counterpart shadeRGBColor
further below, and give me one vote. But, if you want any and/or all the goodies. Such as the ability to use both RGB and Hex colors, Error Checking, 3 Digit hex decoding, Blending, Alpha Channels, and RGB2Hex / Hex2RGB conversions. Then, skip down to Version 3 for shadeBlendConvert
to get all the bells and whistles and give me two votes. You can then delete a few lines to remove some of these features, if desired. And you get a vote if you remember that Version 1 shadeColor1
above is deprecated for all uses.
So, without further ado:
-Version 2 Hex-
function shadeColor2(color, percent) {
var f=parseInt(color.slice(1),16),t=percent<0?0:255,p=percent<0?percent*-1:percent,R=f>>16,G=f>>8&0x00FF,B=f&0x0000FF;
return "#"+(0x1000000+(Math.round((t-R)*p)+R)*0x10000+(Math.round((t-G)*p)+G)*0x100+(Math.round((t-B)*p)+B)).toString(16).slice(1);
}
function blendColors(c0, c1, p) {
var f=parseInt(c0.slice(1),16),t=parseInt(c1.slice(1),16),R1=f>>16,G1=f>>8&0x00FF,B1=f&0x0000FF,R2=t>>16,G2=t>>8&0x00FF,B2=t&0x0000FF;
return "#"+(0x1000000+(Math.round((R2-R1)*p)+R1)*0x10000+(Math.round((G2-G1)*p)+G1)*0x100+(Math.round((B2-B1)*p)+B1)).toString(16).slice(1);
}
Further ado:
There is no error checking, so values that get passed in which are out of range will cause unexpected results. As well, the color input has to be EXACTLY 7 characters, like #08a35c
. But all the other goodies are still here like output range capping (00-FF outputs), padding (0A), handles #
, and usable on solid colors, like #FF0000
.
This new version of shadeColor takes in a float for its second parameter. For shadeColor2
the valid range for the second (percent) parameter is -1.0
to 1.0
.
And for blendColors
the valid range for the third (percent) parameter is 0.0
to 1.0
, negatives not allowed here.
This new version is no longer taking in a percentage of pure white, like the old version. Its taking in a percentage of the DISTANCE from the color given to pure white. In the old version, it was easy to saturate the color, and as a result, many colors would compute to pure white when using a sizable percentage. This new way, it only computes to pure white if you pass in 1.0
, or pure black, use -1.0
.
Calling blendColors(color, "#FFFFFF", 0.5)
is the same as shadeColor2(color,0.5)
. As well as, blendColors(color,"#000000", 0.5)
is the same as shadeColor2(color,-0.5)
. Just a touch slower.
shadeColor2
is slower than shadeColor1
, but not by a notable amount. (Wait, thats a self-contradicting statement!)
The accuracy gained can be seen here:
-- Version 2 RGB --
function shadeRGBColor(color, percent) {
var f=color.split(","),t=percent<0?0:255,p=percent<0?percent*-1:percent,R=parseInt(f[0].slice(4)),G=parseInt(f[1]),B=parseInt(f[2]);
return "rgb("+(Math.round((t-R)*p)+R)+","+(Math.round((t-G)*p)+G)+","+(Math.round((t-B)*p)+B)+")";
}
function blendRGBColors(c0, c1, p) {
var f=c0.split(","),t=c1.split(","),R=parseInt(f[0].slice(4)),G=parseInt(f[1]),B=parseInt(f[2]);
return "rgb("+(Math.round((parseInt(t[0].slice(4))-R)*p)+R)+","+(Math.round((parseInt(t[1])-G)*p)+G)+","+(Math.round((parseInt(t[2])-B)*p)+B)+")";
}
Usages:
var color1 = "rbg(63,131,163)";
var lighterColor = shadeRGBColor(color1, 0.5); // rgb(159,193,209)
var darkerColor = shadeRGBColor(color1, -0.25); // rgb(47,98,122)
var color2 = "rbg(244,128,0)";
var blend1 = blendRGBColors(color1, color2, 0.75); // rgb(199,129,41)
var blend2 = blendRGBColors(color2, color1, 0.62); // rgb(132,130,101)
-- Version 2 Universal A --
function shade(color, percent){
if (color.length > 7 ) return shadeRGBColor(color,percent);
else return shadeColor2(color,percent);
}
function blend(color1, color2, percent){
if (color1.length > 7) return blendRGBColors(color1,color2,percent);
else return blendColors(color1,color2,percent);
}
Usage:
var color1 = shade("rbg(63,131,163)", 0.5);
var color2 = shade("#3f83a3", 0.5);
var color3 = blend("rbg(63,131,163)", "rbg(244,128,0)", 0.5);
var color4 = blend("#3f83a3", "#f48000", 0.5);
-- Version 2 Universal B --
Ok, fine! The popularity of this answer made me think I could do a much better Universal version of this. So here you go! This version is an All-In-One function copy/paste-able shader/blender for both RGB and Hex colors. This one is not really any different than the other Uni version provided above. Except that its much much smaller and just one function to paste and use. I think the size went from about 1,592 characters to 557 characters, if you compress it into one line. Of course, if you don't need to use it interchangeably between RGB and Hex, then you don't need a Universal version such as this anyhow, lol. Just use one of the much tinier and faster versions above; appropriate for your color scheme. Moving on... In some ways its a little faster, in some ways its a little slower. I didn't do any final speed test analysis. There are two usage differences: First, the percentage is now the first parameter of the function, instead of the last. Second, when blending, you can use negative numbers. They will just get converted to positive numbers.
No more ado:
function shadeBlend(p,c0,c1) {
var n=p<0?p*-1:p,u=Math.round,w=parseInt;
if(c0.length>7){
var f=c0.split(","),t=(c1?c1:p<0?"rgb(0,0,0)":"rgb(255,255,255)").split(","),R=w(f[0].slice(4)),G=w(f[1]),B=w(f[2]);
return "rgb("+(u((w(t[0].slice(4))-R)*n)+R)+","+(u((w(t[1])-G)*n)+G)+","+(u((w(t[2])-B)*n)+B)+")"
}else{
var f=w(c0.slice(1),16),t=w((c1?c1:p<0?"#000000":"#FFFFFF").slice(1),16),R1=f>>16,G1=f>>8&0x00FF,B1=f&0x0000FF;
return "#"+(0x1000000+(u(((t>>16)-R1)*n)+R1)*0x10000+(u(((t>>8&0x00FF)-G1)*n)+G1)*0x100+(u(((t&0x0000FF)-B1)*n)+B1)).toString(16).slice(1)
}
}
Usage:
var color1 = "#FF343B";
var color2 = "#343BFF";
var color3 = "rgb(234,47,120)";
var color4 = "rgb(120,99,248)";
var shadedcolor1 = shadeBlend(0.75,color1);
var shadedcolor3 = shadeBlend(-0.5,color3);
var blendedcolor1 = shadeBlend(0.333,color1,color2);
var blendedcolor34 = shadeBlend(-0.8,color3,color4); // Same as using 0.8
Now it might be perfect! ;) @ Mevin
* V2 OTHER LANGUAGES *
-- Swift Extension - RGB (by Matej Ukmar) --
extension UIColor {
func shadeColor(factor: CGFloat) -> UIColor {
var r: CGFloat = 0
var g: CGFloat = 0
var b: CGFloat = 0
var a: CGFloat = 0
var t: CGFloat = factor < 0 ? 0 : 1
var p: CGFloat = factor < 0 ? -factor : factor
getRed(&r, green: &g, blue: &b, alpha: &a)
r = (t-r)*p+r
g = (t-g)*p+g
b = (t-b)*p+b
return UIColor(red: r, green: g, blue: b, alpha: a)
}
}
-- PHP Version - HEX (by Kevin M) --
function shadeColor2($color, $percent) {
$color = str_replace("#", "", $color);
$t=$percent<0?0:255;
$p=$percent<0?$percent*-1:$percent;
$RGB = str_split($color, 2);
$R=hexdec($RGB[0]);
$G=hexdec($RGB[1]);
$B=hexdec($RGB[2]);
return '#'.substr(dechex(0x1000000+(round(($t-$R)*$p)+$R)*0x10000+(round(($t-$G)*$p)+$G)*0x100+(round(($t-$B)*$p)+$B)),1);
}
-- UPDATE -- Version 3.1 Universal --
(This has been added to my library at GitHub)
In a couple months it will have been yet another year since the last universal version. So... thanks to sricks's insightful comment. I have decided to take it to the next level, again. It's no longer the two line speed demon as it had started, lol. But, for what it does, it is quite fast and small. Its around 1600 bytes. If you remove ErrorChecking and remove 3 digit decoding you can get it down to around 1200 bytes and its faster. This is a lot of power in about a K. Just imagine, you could load this onto a Commodore64 and still have space for 50 more of them! (Disregarding the fact that the JavaScript Engine is larger than 63k)
Apparently there was more adoing to be doing:
const shadeBlendConvert = function (p, from, to) {
if(typeof(p)!="number"||p<-1||p>1||typeof(from)!="string"||(from[0]!='r'&&from[0]!='#')||(to&&typeof(to)!="string"))return null; //ErrorCheck
if(!this.sbcRip)this.sbcRip=(d)=>{
let l=d.length,RGB={};
if(l>9){
d=d.split(",");
if(d.length<3||d.length>4)return null;//ErrorCheck
RGB[0]=i(d[0].split("(")[1]),RGB[1]=i(d[1]),RGB[2]=i(d[2]),RGB[3]=d[3]?parseFloat(d[3]):-1;
}else{
if(l==8||l==6||l<4)return null; //ErrorCheck
if(l<6)d="#"+d[1]+d[1]+d[2]+d[2]+d[3]+d[3]+(l>4?d[4]+""+d[4]:""); //3 or 4 digit
d=i(d.slice(1),16),RGB[0]=d>>16&255,RGB[1]=d>>8&255,RGB[2]=d&255,RGB[3]=-1;
if(l==9||l==5)RGB[3]=r((RGB[2]/255)*10000)/10000,RGB[2]=RGB[1],RGB[1]=RGB[0],RGB[0]=d>>24&255;
}
return RGB;}
var i=parseInt,r=Math.round,h=from.length>9,h=typeof(to)=="string"?to.length>9?true:to=="c"?!h:false:h,b=p<0,p=b?p*-1:p,to=to&&to!="c"?to:b?"#000000":"#FFFFFF",f=this.sbcRip(from),t=this.sbcRip(to);
if(!f||!t)return null; //ErrorCheck
if(h)return "rgb"+(f[3]>-1||t[3]>-1?"a(":"(")+r((t[0]-f[0])*p+f[0])+","+r((t[1]-f[1])*p+f[1])+","+r((t[2]-f[2])*p+f[2])+(f[3]<0&&t[3]<0?")":","+(f[3]>-1&&t[3]>-1?r(((t[3]-f[3])*p+f[3])*10000)/10000:t[3]<0?f[3]:t[3])+")");
else return "#"+(0x100000000+r((t[0]-f[0])*p+f[0])*0x1000000+r((t[1]-f[1])*p+f[1])*0x10000+r((t[2]-f[2])*p+f[2])*0x100+(f[3]>-1&&t[3]>-1?r(((t[3]-f[3])*p+f[3])*255):t[3]>-1?r(t[3]*255):f[3]>-1?r(f[3]*255):255)).toString(16).slice(1,f[3]>-1||t[3]>-1?undefined:-2);
}
Play with version 3.1: jsfiddle > shadeBlendConvert Example
The core math of this version is the same as before. But, I did some major refactoring. This has allowed for much greater functionality and control. It now inherently converts RGB2Hex and Hex2RGB.
All the old features from v2 above should still be here. I have tried to test it all, please post a comment if you find anything wrong. Anyhow, here are the new features:
//3 digit
to remove this feature. from
color or the to
color has an alpha channel, then the result will have an alpha channel. If both colors have an alpha channel, the result will be a 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 result. This allows one to blend/shade a transparent color while maintaining the transparent level. Or, if the transparent level should blend as well, make sure both colors have alphas. Shading will pass thru the alpha channel, if you want basic shading that also blends the alpha channel, then use rgb(0,0,0,1)
or rgb(255,255,255,1)
as your to
color (or their hex equivalents). For RGB colors, the resulting alpha channel will be rounded to 4 decimal places. to
color, if one exists. If there is no to
color, then pass 'c'
in as the to
color and it will shade and convert. If conversion only is desired, then pass 0
as the percentage as well. sbcRip
can be passed a hex or rbg color and it returns an object containing this color information. Its in the form: {0:R,1:G,2:B,3:A}
. Where R G and B have range 0
to 255
. And when there is no alpha: A is -1
. Otherwise: A has range 0.0000
to 1.0000
. null
. An example: shadeBlendConvert(0.5,"salt") = null
, where as it thinks #salt
is a valid color. Delete the four lines marked with //ErrorCheck
to remove this feature. Usages:
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)";
let c;
// Shade (Lighten or Darken)
c = shadeBlendConvert ( 0.42, color1 ); // rgb(20,60,200) + [42% Lighter] => rgb(119,142,223)
c = shadeBlendConvert ( -0.4, color5 ); // #F3A + [40% Darker] => #991f66
c = shadeBlendConvert ( 0.42, color8 ); // rgba(200,60,20,0.98631) + [42% Lighter] => rgba(223,142,119,0.98631)
// Shade with Conversion (use "c" as your "to" color)
c = shadeBlendConvert ( 0.42, color2, "c" ); // rgba(20,60,200,0.67423) + [42% Lighter] + [Convert] => #778edfac
// RGB2Hex & Hex2RGB Conversion Only (set percentage to zero)
c = shadeBlendConvert ( 0, color6, "c" ); // #F3A9 + [Convert] => rgba(255,51,170,0.6)
// Blending
c = shadeBlendConvert ( -0.5, color2, color8 ); // rgba(20,60,200,0.67423) + rgba(200,60,20,0.98631) + [50% Blend] => rgba(110,60,110,0.8303)
c = shadeBlendConvert ( 0.7, color2, color7 ); // rgba(20,60,200,0.67423) + rgb(200,60,20) + [70% Blend] => rgba(146,60,74,0.67423)
c = shadeBlendConvert ( 0.25, color3, color7 ); // #67DAF0 + rgb(200,60,20) + [25% Blend] => rgb(127,179,185)
c = shadeBlendConvert ( 0.75, color7, color3 ); // rgb(200,60,20) + #67DAF0 + [75% Blend] => #7fb3b9
// Error Checking
c = shadeBlendConvert ( 0.42, "#FFBAA" ); // #FFBAA + [42% Lighter] => null (Invalid Input Color)
c = shadeBlendConvert ( 42, color1, color5 ); // rgb(20,60,200) + #F3A + [4200% Blend] => null (Invalid Percentage Range)
c = shadeBlendConvert ( 0.42, {} ); // [object Object] + [42% Lighter] => null (Strings Only for Color)
c = shadeBlendConvert ( "42", color1 ); // rgb(20,60,200) + ["42"] => null (Numbers Only for Percentage)
c = shadeBlendConvert ( 0.42, "salt" ); // salt + [42% Lighter] => null (A Little Salt is No Good...)
// Error Check Fails (Some Errors are not Caught)
c = shadeBlendConvert ( 0.42, "#salt" ); // #salt + [42% Lighter] => #6b6b6b00 (...and a Pound of Salt is Jibberish)
// Ripping
c = sbcRip ( color4 ); // #5567DAF0 + [Rip] => [object Object] => {'0':85,'1':103,'2':218,'3':0.9412}
I now hesitate to call this done... again...
PT
--EDIT: Switched version 3 to use let
, and an arrow function, and added this
to sbcRip
calls.
----===<| MAJOR EDIT(3/9/18) |>===----
I am so ashamed! (and surprised that no-one mentioned this) Apparently I don't use alpha channels in my own projects... AND... apparently I did terrible testing. The Version 3 did not read nor write colors with alpha channels correctly. There were a few points that I either just had wrong or never actually learned:
rgba()
and not rgb()
; version 3 never output rgba()
. rgba()
but did accept alphas in rgb()
, which should not happen. I just now replaced Version 3 with Version 3.1 where these issues are addressed. I didn't post it as a separate function here; seeing as the old Version 3 should be removed from existence and replaced with this one. And so that is what I did. Version 3 above is actually Version 3.1.
All the old features from above are still here with these updates:
to
color now accepts a String Color or a falsy (which can still be undefined
). ... I'm glad I hesitated to call it done again. Here we are, another year or so later ... still perfecting it...
PT
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);
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. :)
链接地址: http://www.djcxy.com/p/87614.html上一篇: 使用百分比字体大小?