程序化地使十六进制颜色变浅或变暗(或RGB,并混合颜色)

695
这是我正在编写的函数,用于以编程方式将十六进制颜色值的亮度调暗或者调亮特定量。传递一个字符串作为颜色值(col),例如"3F6D2A",和一个十进制整数作为要调整的量(amt)。如果要调暗,则传递一个负数(如-20)。 我这样做的原因是因为我发现了所有的解决方案似乎都过于复杂化了问题。我有一种感觉可以只用几行代码就能实现。如果您发现任何问题,或者有任何调整建议,请告诉我。

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

开发时可以使用以下更易于阅读的版本:

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

最后,还有一个版本可以处理开头可能带有 "#" 的颜色。此外,还要调整不正确的颜色值:

最后,还有一个版本可处理开头可能带有"#"的颜色,同时还会修正不合规范的颜色值:

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

好的,现在不仅仅是几行代码了,但似乎更简单了,如果你不使用“#”,也不需要检查颜色是否超出范围,则只需要几行代码。

如果不使用“#”,可以像这样在代码中添加:

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

我猜我的主要问题是,我这么做对吗?这不包括大多数(正常)情况吗?如果是这样,最快最小的方法是什么?我想在动画中使用它并在小环境中使用,所以速度是最重要的因素,大小第二,准确性第三,易读性?哎呀?不在要求清单上(抱歉,我知道你们当中一半现在在揉眼睛!)。


1
如果你在修改颜色时没有得到预期的结果,我建议你研究一下LAB颜色空间,它更接近人类视觉。许多编程语言都有用于转换的库。根据我的经验,特别是在变暗或变亮橙色的阴影方面可能会出现问题。 - Henrik
非常好的观点。然而,这个问题的主要目的是首先找到最快的运行时间和最小的尺寸公式...其次是准确性。因此,我没有处理转换为HSL等。在这里速度和大小更重要。但是,正如您可以看到我的第二个公式版本。使用LERP进行着色将导致遍布阴影范围的愉悦橙色。请查看下面的颜色图表,如果该色调范围不是非常接近实际精确,请告诉我。 - Pimp Trizkit
我对这里的结构有点困惑,但你是对的,shadeColor1的橙色级别似乎非常好。 - Henrik
你是说shadeColor2吧。我猜你所说的结构是指答案的整体布局?有什么提示可以使其更清晰明了吗? - Pimp Trizkit
10
上述函数中唯一的问题是,如果最终的十六进制代码以零开头,则该函数无法创建前导零。例如,如果十六进制代码为 #00a6b7,则输出结果为 #a6b7,在作为 CSS 使用时将无法工作。您可以通过将 return 行替换为以下内容来更正此问题: var string = "000000" + (g | (b << 8) | (r << 16)).toString(16); return (usePound?"#":"") + string.substr(string.length-6); - Rafael Levy
显示剩余11条评论
22个回答

1054

好的,这个答案已经变成了自己的怪物。有很多新版本,它变得非常冗长。非常感谢所有为这个答案做出贡献的人。但是,为了让大众更容易理解,我把这个答案演化的所有版本/历史记录都存档到了我的github上。并在StackOverflow上以最新版本重新开始。特别鸣谢Mike 'Pomax' Kamermans为这个版本所做出的贡献。他给了我新的数学公式。


这个函数 (pSBC) 将使用HEX或RGB web颜色。 pSBC可以将其变暗或变亮,或将其与第二个颜色混合,并且还可以通过转换从Hex转换为RGB(Hex2RGB)或RGB转换为Hex(RGB2Hex)来传递它。而不需要您甚至知道您正在使用什么颜色格式。

它运行速度非常快,可能是最快的,特别是考虑到它的许多特性。它经过了漫长的制作过程。在我的github上查看整个故事。如果您想要绝对最小和最快的方式来阴影或混合,请参见下面的Micro函数并使用其中一个2行速度妖怪。它们非常适合强烈的动画效果,但是这个版本已经足够快,可以满足大多数动画效果。

该函数使用对数融合或线性融合。但是,它不会转换为HSL以正确地使颜色变亮或变暗。因此,这个函数的结果与那些使用HSL的更大而且速度更慢的函数会有所不同

带有pSBC的jsFiddle

github > pSBC Wiki

特点:

  • 自动识别并接受以字符串形式表示的标准十六进制颜色。例如:"#AA6622""#bb551144"
  • 自动识别并接受以字符串形式表示的标准RGB颜色。例如:"rgb(123,45,76)""rgba(45,15,74,0.45)"
  • 按百分比将颜色调节到白色或黑色。
  • 按百分比混合颜色。
  • 可以同时进行Hex2RGB和RGB2Hex转换,也可以单独进行转换。
  • 接受以#RGB(或#RGBA)的形式表示的3位数字(或4位数字带透明度)HEX颜色代码。它会将其扩展。例如:"#C41"变成"#CC4411"
  • 接受和(线性)混合alpha通道。如果(来自)颜色或(到)颜色中有一个alpha通道,则返回的颜色将具有alpha通道。如果两个颜色都有alpha通道,则返回的颜色将是两个alpha通道的线性混合,使用给定的百分比(就像正常颜色通道一样)。如果只有其中一个颜色有alpha通道,则该alpha通道将直接传递到返回的颜色中。这允许在保持透明度水平的同时混合/调节透明颜色。或者,如果透明度水平也应混合,则确保两种颜色都有alpha值。进行阴影处理时,它会直接通过alpha通道。如果想要基本阴影,也带有阴影通道,则使用rgb(0,0,0,1)rgb(255,255,255,1)作为(到)的颜色(或其十六进制等效)。对于RGB颜色,返回的颜色的alpha通道将舍入到3个小数位。
  • 在混合时隐含了RGB2Hex和Hex2RGB转换。无论(来自)颜色是什么,返回的颜色都将始终以(到)颜色的颜色格式呈现,如果存在(到)颜色。如果没有(到)颜色,则将其作为颜色传递,并将转换任何颜色。如果只需要转换,则还可以将百分比(

    )传递为0。如果省略颜色或传入非字符串,则不会转换。

  • 全局还添加了一个辅助函数。pSBCr可以传递一个十六进制或RGB颜色,并返回包含此颜色信息的对象。它的格式为:{ r:XXX, g:XXX, b:XXX, a:X.XXX }。其中.r.g.b的范围为0到255。当没有alpha时:.a为-1。否则:.a的范围为0.000到1.000。
  • 对于RGB输出,如果向(来自)和/或(到)中传递了带有alpha通道的颜色,则输出rgba()而不是rgb()
  • 添加了一些错误检查。它并不完美。它仍然可能崩溃或创建无意义的东西。但它会捕获一

    代码:

    // 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)
    }
    

    使用方法:

    // 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}
    

    下面的图片将帮助展示两种混合方法的区别:


    微型功能

    如果您真的想要速度和大小,您将需要使用RGB而不是HEX。RGB更直观和简单,HEX写起来太慢,也有太多的变化,对于一个简单的两行代码来说(例如:它可以是3、4、6或8位十六进制代码)。您还需要牺牲一些功能,没有错误检查,没有HEX2RGB或RGB2HEX。此外,您需要选择一个特定的函数(基于其函数名称),用于颜色混合数学,以及是否需要阴影或混合。这些函数支持Alpha通道。当两个输入颜色都具有Alpha时,它们将进行线性混合。如果只有其中一个颜色具有Alpha,则它将直接传递给结果颜色。以下是两个非常快速且小巧的双行函数:

    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:")");
    }
    

    需要更多信息吗?请阅读Github上的完整介绍

    PT

    (附注:如果有其他混合方法的数学公式,请分享。)


9
需要 PHP 版本的用户可以访问以下链接:https://gist.github.com/chaoszcat/5325115#file-gistfile1-php - Lionel Chan
61
我使用了TinyColor库-- tinycolor.darken(color,amount);。这行代码的作用是将颜色变暗,color参数表示要变暗的颜色,amount参数表示变暗的程度。 - FWrnr
4
很棒的帖子... :) ... 我刚刚创建了它的 Swift 扩展:https://gist.github.com/matejukmar/1da47f7a950d1ba68a95 - Matej Ukmar
3
这样想。如果我拿数字 100 并降低了它的 10%,那么它将变成 90。然后,如果我将 90 提高 10%,它将变成 99。反过来...如果我将 100 提高 10%,我将得到 110,然后如果我将其降低 10%...我将得到 99。我认为这个技巧在金融系统中被用来赚(偷)钱。 - Pimp Trizkit
4
“pSBCr”未定义。no-undef - 5ervant - techintel.github.io
显示剩余68条评论

237

我做了一个对我非常有效的解决方案:

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;  

    R = Math.round(R)
    G = Math.round(G)
    B = Math.round(B)

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

示例:轻量级

shadeColor("#63C6FF",40);

示例暗化:

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

6
不错,我喜欢这个百分比!+1。但是,为了提高速度,我可能会这样做:R = ((R<255)?R:255).toString(16); 然后 R = R.length==1 ? "0"+R : R。而且我不确定转换成大写字母的意义在哪里? - Pimp Trizkit
非常好。然而,100%的亮度不应该变成完全白色,而100%的黑暗则始终是黑色,无论颜色如何?似乎-100确实会使任何颜色变成黑色,但是100(正数)并不能使其完全变成白色。 - Kevin M
7
不能与纯色(如 #ff0000、#00ff00、#0000ff)一起使用。 - Hitori
2
为了让它与黑色配合使用,我只需进行以下修改: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; - Irfan Raza
es6版本:const shadeColor = (h, p) => '#' + [1, 3, 5] .map(s => parseInt(h.substr(s, 2), 16)) .map(c => parseInt((c * (100 + p)) / 100)) .map(c => (c < 255 ? c : 255)) .map(c => c.toString(16).padStart(2, '0')) .join(''); - Ali Almoullim
显示剩余4条评论

104

这是基于Eric的回答的超级简单的一行代码

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

示例:

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

39
超级简单。 - Andrew
5
数量是多少?需要澄清它是像素(px),百分比(percent)等单位。 - Mote Zart
1
你可以通过在正则表达式中使用\w\w(不匹配#)来使它变得更短,而不是删除和重新添加#。我也不太明白为什么要添加0,然后再移除它?这是一些我没有理解的JS方法吗(你已经使用了toString(),所以不需要进行转换吗)?不管怎样,最终代码如下,看起来能够达到相同的效果:var adj = (c, a) => c.replace(/\w\w/g, m => Math.min(255, Math.max(0, parseInt(m, 16) + a)).toString(16)) - Martin Tournoij
1
增加了一个演示程序... https://jsfiddle.net/Abeeee/xeos0p42/20/ - user1432181
1
@MartinTournoij:'0'+substr(-2)是为了那些介于0和15之间的数字而设计的,这些数字在十六进制中只有一个数字,因此会产生格式不正确的十六进制颜色字符串。他在前面添加了'0'并保留了最后两个数字,以便9变成'09',而不仅仅是'9'。不过这是个好的起点! - Jedai

53

我在这里提供我的意见,它是不同答案的简单结合:

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}`
}

接受以 # 开头的颜色, 长度为 6 个或 3 个字符。

使用示例: colorShade('#54b946', -40)

以下是 4 种颜色的输出,每种颜色分别有 3 种较浅和 3 种较深的阴影(此处的数量是 40 的倍数)。

enter image description here


10
这是我根据您的功能所使用的内容。我更喜欢使用步骤而不是百分比,因为这对我来说更加直观。 例如,200蓝色值的20%与40蓝色值的20%相差很大。 无论如何,这是我做出的修改,感谢您原始的功能。
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;

}

1
发现这个比最佳答案更有用,因为最佳答案会使我的颜色变得非常强烈,而不仅仅是变暗。谢谢Eric。 - Worm
迄今为止最好的答案,赞! - Roi Mulia

8

基于David Sherret和Pablo的回答,以上内容将解决方案转换为更安全的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}`;
}
  

6

我试了一下你的函数,发现有一个小bug:如果某个最终的 'r' 值只有1位数字,结果会显示为 'a0a0a',而正确的值应该是 '0a0a0a'。例如。 我只是快速修复了它,使用了以下代码替换了你的返回值:

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;
也许它不太好看,但它能完成工作。顺便说一句,这是一个很棒的功能。正是我所需要的。 :)

1
谢谢你的调试和夸奖!不过很遗憾这并不是回答我主要问题——是否有更快的方法。比如可能使用全部十六进制而不进行任何基数转换。虽然,我猜你告诉我了如果我的代码正确的话(+1)。不幸的是,修复增加了相当多的开销(现在你调用了6次toString),而且稍微不那么简单。也许在进行十六进制转换之前检查十进制数是否小于等于15会更快。但是,我喜欢! - Pimp Trizkit

5

我想将一种颜色的亮度级别更改为特定值 - 无论之前颜色的亮度是多少 - 这里有一个简单的JS函数,似乎可以很好地工作,虽然我相信它可以更短。

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

酷毙啦!我假设 p 的范围为 0-100?我甚至不知道如何正确定义 RGB 中的亮度,那是 HSL 的事情。例如,#FF00FF 是否比 #FF0000 更亮?如果是这样,那就意味着品红比红色亮两倍。因此,我们使用纯红测试。传入纯红色 #FF0000,设置为50%的亮度,这里我们得到了 #FF4040,对吗?我本来会猜想让红色达到50%亮度,它会变暗,因为它已经完全亮了,就像 #800000 或150%亮度会变成 #FF8080 一样。粉红色比红色更亮吗?还是说红色已经完全亮了? - Pimp Trizkit
你说得对 - 我应该提到p必须在1-100范围内! - Torbjörn Josefsson
#FF00FF在红色通道中的值为255,在绿色通道中为0,在蓝色通道中为255。通道中组合值越高,颜色亮度越高。数字p表示我们希望新颜色的亮度为原始颜色的50%。我不确定#FF4040是否是“可能的红色亮度的50%”的正确答案。要生成较暗的色调(在这种情况下,红色通道中的值较低),需要进行修改。 - Torbjörn Josefsson
是的,我只是指出了在谈论RGB亮度时存在的模糊性。如果转换为HSL,L通道实际上就是亮度。我[个人精神]的问题在于,对我来说,#FF0000是完全明亮的。而#FF4040则更浅色但不更亮....对我来说浅色意味着更接近白色,比如粉色。而亮度是它拥有的程度,它拥有完整的红色,所以红色是完全明亮的。因此,#FF0000不能更加明亮..而是更浅..也许我只是个怪物,哈哈!我真的不懂色彩理论,所以...我只是在胡说八道。 - Pimp Trizkit
但是我知道当我改变显示器的亮度时,红色不会在我的眼中变成粉色...所以这可能是我开始逻辑推理的地方。 - Pimp Trizkit

5

你有没有考虑过RGB转HSL?然后只需上下移动亮度?这是我会选择的方法。

快速查找了一些算法,得到了以下网站。

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

上面的链接已经无效。你可以查看 Git Hub 的页面源代码gist。 另外,StackOverflow 的问题可能是一个不错的选择。
尽管这不是 OP 的正确选择,但以下是我最初建议的代码的近似值。(假设您有 rgb/hsl 转换函数)
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;
}
这里假设:
  1. 你拥有函数 hslToRgbrgbToHsl
  2. 参数 colorValue 是一个形如#RRGGBB的字符串。

虽然如果我们讨论CSS,那么IE9 / Chrome / Firefox有一种指定hsl/hsla的语法。


链接腐败:第二个链接已损坏。 - slashdottir
除了不完整(如果答案需要两个转换函数,则包括它们),这也不能正确地适应r、g或b为单个数字的情况,因此通常会返回格式不正确的十六进制代码。 - Jimbo Jonny
我个人不同意。这个问题是关于明暗度的...但你的明暗度解决方案包括RGB / HSL转换,因此答案也应该包括该转换。你已经有一个链接失效了。让你的代码自己正常工作而不添加另一个问题的代码并不会让人感到困惑...事实上可能会更少让人感到困惑。 - Jimbo Jonny
@JimboJonny,至少有其他3个更好的答案,其中两个将涵盖大多数人。如果读者正在寻找hsl / rgb转换,他们将首先找到其他问题。如果不是OP答案中引用了这个答案,我会删除它。 - James Khoury
读者不需要寻找hsl/rgb转换。这个答案需要它,而且这个答案是关于这个问题的。没有必要围绕着理性化为什么我们不应该费心让这个答案中的代码实际工作,当你自己说你不应该删除这个答案时。如果它足够重要而不被删除,那么修复它就很重要。编辑在队列中。 - Jimbo Jonny
显示剩余12条评论

5

您的方法是可行的 :) 我稍微简化了您最短的版本(有关饱和度控制,请查看此处

(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}
<input id="col" value="3F6D2A" oninput="update()">
<input id="amt" value="30" oninput="update()"><br>
<div id="oldColor" class="box"></div>
<div id="newColor" class="box"></div>

带有#和颜色范围检查的版本

// # 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}
<input id="col" value="#3F6D2A" oninput="update()">
<input id="amt" value="40" oninput="update()"><br>
<div id="oldColor" class="box"></div>
<div id="newColor" class="box"></div>


1
我非常喜欢这个简短而强大的函数的想法,但它似乎并不适用于所有颜色。 :/ 例如:#54b946 应用 -30、-60、-90、-120,它返回紫色而不是浅绿色。 - antoni
1
@antoni 我更新了答案,并对代码片段进行了更多的交互(但没有改变主要的过程代码)。在底部的代码片段中,我检查了你的颜色 - 看起来一切都正常。如果不是这样的话 - 你能提供一下 #54b946 和 -120 的确切结果的十六进制数吗? - Kamil Kiełczewski
1
那是我的错@kamil,谢谢你的函数。聪明而简洁。+1你的测试程序帮助我看到了错误出现在代码其他地方。 - antoni
很棒的答案,简单而且非常有用! - zardilior

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,