javascript - Programmatisk ljusa upp eller mörka ner en hex-färg (eller rgb, och blanda färger)
Här är en funktion jag arbetade på för att programmatisk ljusa upp eller mörka ned en hex-färg med ett specifikt belopp. Skicka bara in en sträng som "3F6D2A" för färgen (col) och ett decimaltal (amt) för mängden att ljusa upp eller mörka ned. För att mörka ned, skicka in ett negativt tal (dvs -20).
Anledningen till att jag gjorde detta var på grund av alla lösningar jag hittat hittills, de verkade överkomplicera problemet. Och jag hade en känsla av att det kunde göras med bara ett par rader kod. Vänligen meddela mig om du hittar några problem, eller har några justeringar som skulle kunna göra det snabbare.
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) );För utveckling använd här en lättare att läsa 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));Och slutligen en version för att hantera färger som kanske (eller kanske inte) har "#" i början. Plus justering för felaktiga färgvärden:
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);
}
Okej, så nu är det inte bara ett par rader, men det verkar mycket enklare och om du inte använder "#" och inte behöver kontrollera färger utanför intervallet, är det bara ett par rader.
Om du inte använder "#", kan du bara lägga till det i koden som:
var myColor = "3F6D2A";
myColor = LightenDarkenColor(myColor,10);
thePlaceTheColorIsUsed = ("#" + myColor);
Jag antar att min huvudfråga är, har jag rätt här? Täcker detta inte de flesta (normala) situationer? Och i så fall, vad är det snabbaste och minsta sättet att göra detta på? Jag vill använda det i animationer och i en liten miljö, så hastighet är den allra viktigaste faktorn här, storlek andra, noggrannhet tredje, läsbarhet? huh? inte på listan över krav (förlåt, jag vet att hälften av er just nu river ut era ögon!).
Svar #1
Nåväl, detta svar har blivit ett eget odjur. Många nya versioner, det höll på att bli löjligt långt. Stort tack till alla de många fantastiska bidragsgivarna till detta svar. Men, för att hålla det enkelt för massorna, arkiverade jag alla versioner/historik av detta svars utveckling på min GitHub. Och började om på nytt här på StackOverflow med den senaste versionen. Ett särskilt tack går till Mike 'Pomax' Kamermans för denna version. Han gav mig den nya matematiken.
Denna funktion (pSBC) tar en HEX- eller RGB-webbfärg. pSBC kan göra färgen mörkare eller ljusare, eller blanda den med en andra färg, och kan också bara passera igenom den men konvertera från Hex till RGB (Hex2RGB) eller RGB till Hex (RGB2Hex). Allt detta utan att du ens behöver veta vilket färgformat du använder.
Det här körs väldigt snabbt, förmodligen snabbast, särskilt med tanke på dess många funktioner. Det tog lång tid att skapa. Se hela historien på min GitHub. Om du vill ha det absolut minsta och snabbaste möjliga sättet att skugga eller blanda, se Micro Functions nedan och använd en av de tvåraders hastighetsdjävlarna. De är utmärkta för intensiva animationer, men den här versionen är snabb nog för de flesta animationer.
Denna funktion använder Log-blandning eller linjär blandning. Den konverterar dock INTE till HSL för att korrekt ljusa upp eller mörka en färg. Därför kan resultaten från denna funktion skilja sig från de mycket större och mycket långsammare funktioner som använder HSL (som TinyColor? som faktiskt är ganska stor, i jämförelse).
jsFiddle med pSBC
github > pSBC Wiki
Funktioner:
- Identifierar automatiskt och accepterar standard Hex-färger i form av strängar. Till exempel:
"#AA6622"eller"#bb551144". - Autoidentifierar och accepterar standard RGB-färger i form av strängar. Till exempel:
"rgb(123,45,76)"eller"rgba(45,15,74,0.45)". - Skuggar färger till vitt eller svart efter procent.
- Blandar färger tillsammans efter procentandel.
- Utför Hex2RGB- och RGB2Hex-omvandling samtidigt eller ensam.
- Accepterar 3-siffriga (eller 4-siffriga med bokstav) HEX-färgkoder, i formen #RGB (eller #RGBA). Den kommer att expandera dem. Till exempel:
"#C41"blir"#CC4411". - Accepterar och (linjärt) blandar alfakanaler. Om antingen
c0(från) färgen ellerc1(till) färgen har en alfakanal, så kommer den returnerade färgen att ha en alfakanal. Om båda färgerna har en alfakanal, kommer den returnerade färgen att vara en linjär blandning av de två alfakanalerna med hjälp av den angivna procentandelen (precis som om det vore en vanlig färgkanal). Om endast en av de två färgerna har en alfakanal, kommer denna alfakanal bara att föras vidare till den returnerade färgen. Detta gör det möjligt att blanda/skugga en genomskinlig färg samtidigt som transparensnivån bibehålls. Eller, om transparensnivåerna också ska blandas, se till att båda färgerna har alfa. Vid skuggning kommer alfakanalen att passera rakt igenom. Om du vill ha grundläggande skuggning som också skuggar alfakanalen, använd dårgb(0,0,0,1)ellerrgb(255,255,255,1)som dinc1-färg (till) (eller deras hex-motsvarigheter). För RGB-färger kommer den returnerade färgens alfakanal att avrundas till 3 decimaler. - RGB2Hex och Hex2RGB-konverteringar är implicita när man använder blandning. Oavsett vilken
c0(från) färg som används; kommer den returnerade färgen alltid att vara i färgformatet förc1(till) färgen, om en sådan finns. Om det inte finns någonc1(till) färg, skicka då'c'somc1-färg och den kommer att skugga och konvertera vilkenc0-färg som helst. Om endast konvertering önskas, skicka även0som procent (p). Omc1-färgen utelämnas eller något som inte är enstringskickas in, kommer det inte att konverteras. - En sekundär funktion läggs också till globalt.
pSBCrkan ta emot en Hex- eller RGB-färg och returnerar ett objekt som innehåller denna färginformation. Det är i formen: {r: XXX, g: XXX, b: XXX, a: X.XXX}. Där.r,.g, och.bhar värden från 0 till 255. Och när det inte finns någon alfa:.aär -1. Annars:.ahar värden från 0.000 till 1.000. - För RGB-utgång, returnerar det
rgba()istället förrgb()när en färg med en alfakanal skickades in ic0(från) och/ellerc1(till). - Mindre felkontroll har lagts till. Den är inte perfekt. Den kan fortfarande krascha eller skapa nonsens. Men den kommer att fånga vissa saker. I princip, om strukturen är fel på vissa sätt eller om procenttalet inte är en siffra eller utanför området, kommer den att returnera
null. Ett exempel:pSBC(0.5,"salt") == null, medan den anser att#saltär en giltig färg. Ta bort de fyra raderna som slutar medreturn null;för att ta bort denna funktion och göra den snabbare och mindre. - Använder loggblandning. Skicka
trueförl(den fjärde parametern) för att använda linjär blandning.
Kod :
// 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)
}
Användning :
// 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}
Bilden nedan kommer att hjälpa till att visa skillnaden mellan de två blandningsmetoderna:
Mikrofunktioner
Om du verkligen vill ha snabbhet och liten storlek, måste du använda RGB och inte HEX. RGB är mer direkt och enkelt, HEX skrivs för långsamt och finns i för många varianter för en enkel tvåraderskod (t.ex. det kan vara en 3, 4, 6 eller 8-siffrig HEX-kod). Du kommer också behöva offra vissa funktioner, ingen felkontroll, ingen HEX2RGB eller RGB2HEX. Dessutom måste du välja en specifik funktion (baserat på dess funktionsnamn nedan) för färgblandningsmatematiken, och om du vill ha skuggning eller blandning. Dessa funktioner stöder alfakanaler. Och när båda inmatningsfärgerna har alpha kommer de blandas linjärt. Om endast en av de två färgerna har alpha, kommer den att föras direkt till resultatfärgen. Nedan är tvåradersfunktioner som är otroligt snabba och små:
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:")");
}
Vill du ha mer information? Läs hela genomgången på GitHub.
PT
(P.s. Om någon har matematiken för en annan blandningsmetod, vänligen dela.)
Svar #2
Jag gjorde en lösning som fungerar mycket bra för mig:
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;
}
Exempel Ljusna:
shadeColor("#63C6FF",40);
Exempel Mörka:
shadeColor("#63C6FF",-40);
Svar #3
Här är en superenkel enradare baserad på Erics svar
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));
}
Exempel:
adjust('#ffffff', -20) => "#ebebeb"
adjust('000000', 20) => "#141414"
Svar #4
Jag lägger till mina två cent här, en tillfredsställande liten kombination av olika svar:
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}`
}
accepterar en färg som börjar med # eller inte, med 6 tecken eller 3 tecken.
exempel på användning: colorShade('#54b946', -40)
Här är utdata för 4 färger med 3 nyanser ljusare och 3 nyanser mörkare för var och en av dem (mängden är en multipel av 40 här).
Svar #5
Detta är vad jag använde baserat på din funktion. Jag föredrar att använda steg istället för procent eftersom det är mer intuitivt för mig.
Till exempel är 20 % av ett blått värde på 200 mycket annorlunda än 20 % av ett blått värde på 40.
Hur som helst, här är min ändring, tack för din ursprungliga funktion.
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;
}

