在 JavaScript 中转换颜色空间

Avatar of Jon Kantner
Jon Kantner

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

我在构建一个 图像“表情符号化” 时遇到的一个挑战是,我需要将使用 getImageData() 获得的值的颜色空间从 RGB 更改为 HSL。我使用了按亮度和饱和度排列的表情符号数组,并且它们基于 HSL,以便平均像素颜色与表情符号最佳匹配。

在本文中,我们将研究一些对转换不透明和启用 alpha 的颜色值有用的函数。现代浏览器目前支持 RGB(A)、十六进制和 HSL(A) 颜色空间。这些函数和表示法分别是 rgb()rgba()#rgb/#rrggbb#rgba/#rrggbbaahsl()hsla()。浏览器始终支持像 aliceblue 这样的内置名称。

Balls with color values being inserted into a machine and coming out as HSL

在此过程中,我们将遇到 CSS 颜色模块的新 第 4 级 提供的一些颜色语法的使用。例如,我们现在有了带 alpha 的十六进制(#rgba/#rrggbbaa),并且 RGB 和 HSL 语法不再需要逗号(像 rgb(255 0 0)hsl(240 100% 50%) 这样的值成为合法的!)。

截至本文撰写之时,CSS 颜色第 4 级的浏览器支持并不普遍,因此不要期望新的颜色语法在 Microsoft 浏览器或 Safari 中使用 CSS 时能够正常工作。

RGB 到十六进制

将 RGB 转换为十六进制仅仅是基数的改变。我们使用 toString(16) 将红色、绿色和蓝色值从十进制转换为十六进制。在为单个数字和以下数字添加前导 0 后,我们可以将它们和 # 连接到单个 return 语句中。

function RGBToHex(r,g,b) {
  r = r.toString(16);
  g = g.toString(16);
  b = b.toString(16);

  if (r.length == 1)
    r = "0" + r;
  if (g.length == 1)
    g = "0" + g;
  if (b.length == 1)
    b = "0" + b;

  return "#" + r + g + b;
}

字符串中的 RGB 到十六进制

或者,我们可以使用一个带有红色、绿色和蓝色(例如 "rgb(255,25,2)""rgb(255 25 2)")以逗号或空格分隔的单个字符串参数。使用子字符串消除 rgb(,使用 ) 分割剩下的部分,然后使用分隔符 (sep) 分割该结果的第一项。rgb 现在将成为局部变量。然后,我们在分割字符串之前使用 + 将它们转换回数字,然后再获取十六进制值。

function RGBToHex(rgb) {
  // Choose correct separator
  let sep = rgb.indexOf(",") > -1 ? "," : " ";
  // Turn "rgb(r,g,b)" into [r,g,b]
  rgb = rgb.substr(4).split(")")[0].split(sep);

  let r = (+rgb[0]).toString(16),
      g = (+rgb[1]).toString(16),
      b = (+rgb[2]).toString(16);

  if (r.length == 1)
    r = "0" + r;
  if (g.length == 1)
    g = "0" + g;
  if (b.length == 1)
    b = "0" + b;

  return "#" + r + g + b;
}

此外,我们可以通过在重新定义 rgb 后添加循环来允许字符串使用通道值作为百分比。它将去除 % 并将剩下的内容转换为 255 之内的值。

function RGBToHex(rgb) {
  let sep = rgb.indexOf(",") > -1 ? "," : " ";
  rgb = rgb.substr(4).split(")")[0].split(sep);

  // Convert %s to 0–255
  for (let R in rgb) {
    let r = rgb[R];
    if (r.indexOf("%") > -1)
      rgb[R] = Math.round(r.substr(0,r.length - 1) / 100 * 255);
      /* Example:
      75% -> 191
      75/100 = 0.75, * 255 = 191.25 -> 191
      */
  }

  ...
}

现在我们可以提供如下值中的任意一个

  • rgb(255,25,2)
  • rgb(255 25 2)
  • rgb(50%,30%,10%)
  • rgb(50% 30% 10%)

RGBA 到十六进制 (#rrggbbaa)

将 RGBA 转换为带 #rgba 或 #rrggbbaa 表示法的十六进制遵循与不透明对应物几乎相同的过程。由于 alpha (a) 通常是 0 到 1 之间的值,因此我们需要将其乘以 255,对结果进行四舍五入,然后将其转换为十六进制。

function RGBAToHexA(r,g,b,a) {
  r = r.toString(16);
  g = g.toString(16);
  b = b.toString(16);
  a = Math.round(a * 255).toString(16);

  if (r.length == 1)
    r = "0" + r;
  if (g.length == 1)
    g = "0" + g;
  if (b.length == 1)
    b = "0" + b;
  if (a.length == 1)
    a = "0" + a;

  return "#" + r + g + b + a;
}

要使用一个字符串(包括百分比)执行此操作,我们可以按照我们之前所做的操作。还要注意额外的一步,即剪切斜杠。由于 CSS 颜色第 4 级支持 rgba(r g b / a) 的语法,因此我们在此处允许它。Alpha 值现在可以是百分比!这消除了我们过去使用的 0-1-only 限制。因此,循环遍历 rgbafor 循环将包括一个部分,用于在不乘以 255 的情况下(当 R 为 alpha 的 3 时)从 alpha 中删除 %。很快我们就可以使用像 rgba(255 128 0 / 0.8)rgba(100% 21% 100% / 30%) 这样的值!

function RGBAToHexA(rgba) {
  let sep = rgba.indexOf(",") > -1 ? "," : " "; 
  rgba = rgba.substr(5).split(")")[0].split(sep);
                
  // Strip the slash if using space-separated syntax
  if (rgba.indexOf("/") > -1)
    rgba.splice(3,1);

  for (let R in rgba) {
    let r = rgba[R];
    if (r.indexOf("%") > -1) {
      let p = r.substr(0,r.length - 1) / 100;

      if (R < 3) {
        rgba[R] = Math.round(p * 255);
      } else {
        rgba[R] = p;
      }
    }
  }
}

然后,在将通道转换为十六进制的地方,我们调整 a 以使用 rgba[] 的一项。

function RGBAToHexA(rgba) {
  ...
    
  let r = (+rgba[0]).toString(16),
      g = (+rgba[1]).toString(16),
      b = (+rgba[2]).toString(16),
      a = Math.round(+rgba[3] * 255).toString(16);

  if (r.length == 1)
    r = "0" + r;
  if (g.length == 1)
    g = "0" + g;
  if (b.length == 1)
    b = "0" + b;
  if (a.length == 1)
    a = "0" + a;

  return "#" + r + g + b + a;
}

现在该函数支持以下内容

  • rgba(255,25,2,0.5)
  • rgba(255 25 2 / 0.5)
  • rgba(50%,30%,10%,0.5)
  • rgba(50%,30%,10%,50%)
  • rgba(50% 30% 10% / 0.5)
  • rgba(50% 30% 10% / 50%)

十六进制到 RGB

我们知道十六进制值的长度必须是 3 或 6(加上 #)。在任何一种情况下,我们都以 "0x" 开头每个红色 (r)、绿色 (g) 和蓝色 (b) 值以将其转换为十六进制。如果我们提供一个 3 位数字的值,我们将为每个通道连接相同的值两次。如果是 6 位数字的值,我们将连接前两位用于红色,接下来的两位用于绿色,最后两位用于蓝色。为了获取最终 rgb() 字符串的值,我们在变量前面添加 + 以将其从字符串转换回数字,这将产生我们需要的十进制数。

function hexToRGB(h) {
  let r = 0, g = 0, b = 0;

  // 3 digits
  if (h.length == 4) {
    r = "0x" + h[1] + h[1];
    g = "0x" + h[2] + h[2];
    b = "0x" + h[3] + h[3];

  // 6 digits
  } else if (h.length == 7) {
    r = "0x" + h[1] + h[2];
    g = "0x" + h[3] + h[4];
    b = "0x" + h[5] + h[6];
  }
  
  return "rgb("+ +r + "," + +g + "," + +b + ")";
}

使用 % 从十六进制输出 RGB

如果我们想使用百分比返回 rgb(),那么我们可以修改函数以使用可选的 isPct 参数,如下所示

function hexToRGB(h,isPct) {
  let r = 0, g = 0, b = 0;
  isPct = isPct === true;

  if (h.length == 4) {
    r = "0x" + h[1] + h[1];
    g = "0x" + h[2] + h[2];
    b = "0x" + h[3] + h[3];
    
  } else if (h.length == 7) {
    r = "0x" + h[1] + h[2];
    g = "0x" + h[3] + h[4];
    b = "0x" + h[5] + h[6];
  }
    
  if (isPct) {
    r = +(r / 255 * 100).toFixed(1);
    g = +(g / 255 * 100).toFixed(1);
    b = +(b / 255 * 100).toFixed(1);
  }
  
  return "rgb(" + (isPct ? r + "%," + g + "%," + b + "%" : +r + "," + +g + "," + +b) + ")";
}

在最后一个 if 语句下,使用 +rgb 转换为数字。每个 toFixed(1) 以及它们将结果四舍五入到最接近的十分之一。此外,我们不会有带有 .0 的整数或产生像 0.30000000000000004 这样的数字的已有几十年的怪癖。因此,在 return 中,我们省略了第一个 rgb 之前的 + 以防止由 % 引起的 NaN。现在我们可以使用 hexToRGB("#ff0",true) 获取 rgb(100%,100%,0%)

十六进制 (#rrggbbaa) 到 RGBA

带 alpha 的十六进制值的程序应该与最后一个类似。我们只需检测 4 位或 8 位数字的值(加上 #),然后转换 alpha 并将其除以 255。为了获得更精确的输出,但不是 alpha 的长十进制数字,我们可以使用 toFixed(3)

function hexAToRGBA(h) {
  let r = 0, g = 0, b = 0, a = 1;

  if (h.length == 5) {
    r = "0x" + h[1] + h[1];
    g = "0x" + h[2] + h[2];
    b = "0x" + h[3] + h[3];
    a = "0x" + h[4] + h[4];

  } else if (h.length == 9) {
    r = "0x" + h[1] + h[2];
    g = "0x" + h[3] + h[4];
    b = "0x" + h[5] + h[6];
    a = "0x" + h[7] + h[8];
  }
  a = +(a / 255).toFixed(3);

  return "rgba(" + +r + "," + +g + "," + +b + "," + a + ")";
}

使用 % 从十六进制输出 RGBA

对于输出百分比的版本,我们可以像在 hexToRGB() 中那样做——当 isPcttrue 时,将 rgb 切换到 0-100%。

function hexAToRGBA(h,isPct) {
  let r = 0, g = 0, b = 0, a = 1;
  isPct = isPct === true;
    
  // Handling of digits
  ...

  if (isPct) {
    r = +(r / 255 * 100).toFixed(1);
    g = +(g / 255 * 100).toFixed(1);
    b = +(b / 255 * 100).toFixed(1);
  }
  a = +(a / 255).toFixed(3);

  return "rgba(" + (isPct ? r + "%," + g + "%," + b + "%," + a : +r + "," + +g + "," + +b + "," + a) + ")";
}

如果 alpha 也应该是一个百分比,这里有一个快速修复:将 a 被重新定义的语句移到最后一个 if 语句之上。然后在该语句中,修改 a 使其类似于 rgb。当 isPct 为 true 时,a 也必须获得 %

function hexAToRGBA(h,isPct) {
  ...
    
  a = +(a / 255).toFixed(3);
  if (isPct) {
    r = +(r / 255 * 100).toFixed(1);
    g = +(g / 255 * 100).toFixed(1);
    b = +(b / 255 * 100).toFixed(1);
    a = +(a * 100).toFixed(1);
  }

  return "rgba(" + (isPct ? r + "%," + g + "%," + b + "%," + a + "%" : +r + "," + +g + "," + +b + "," + a) + ")";
}

当我们现在输入 #7f7fff80 时,我们应该得到 rgba(127,127,255,0.502)rgba(49.8%,49.8%,100%,50.2%)

RGB 到 HSL

从 RGB 或十六进制获取 HSL 值有点更具挑战性,因为涉及更大的公式。首先,我们必须将红色、绿色和蓝色除以 255 以使用 0 到 1 之间的值。然后我们找到这些值的最小值和最大值 (cmincmax) 以及它们之间的差值 (delta)。我们需要该结果作为计算色调和饱和度的一部分。在 delta 之后,让我们初始化色调 (h)、饱和度 (s) 和亮度 (l)。

function RGBToHSL(r,g,b) {
  // Make r, g, and b fractions of 1
  r /= 255;
  g /= 255;
  b /= 255;

  // Find greatest and smallest channel values
  let cmin = Math.min(r,g,b),
      cmax = Math.max(r,g,b),
      delta = cmax - cmin,
      h = 0,
      s = 0,
      l = 0;
}

接下来,我们需要计算色调,它将由 cmax 中的最大通道值确定(或者如果所有通道都相同)。如果通道之间没有差异,则色调将为 0。如果 cmax 是红色,则公式将为 ((g - b) / delta) % 6。如果是绿色,则为 (b - r) / delta + 2。然后,如果是蓝色,则为 (r - g) / delta + 4。最后,将结果乘以 60(以获得度数值)并四舍五入。由于色调不应该为负数,因此如果需要,我们将 360 加到它上面。

function RGBToHSL(r,g,b) {
  ...
  // Calculate hue
  // No difference
  if (delta == 0)
    h = 0;
  // Red is max
  else if (cmax == r)
    h = ((g - b) / delta) % 6;
  // Green is max
  else if (cmax == g)
    h = (b - r) / delta + 2;
  // Blue is max
  else
    h = (r - g) / delta + 4;

  h = Math.round(h * 60);
    
  // Make negative hues positive behind 360°
  if (h < 0)
      h += 360;
}

剩下的只有饱和度和亮度了。在我们计算饱和度之前,先计算亮度,因为饱和度将取决于亮度。它是最大和最小通道值之和的一半 ((cmax + cmin) / 2)。然后 delta 将决定饱和度是多少。如果它是 0(cmaxcmin 之间没有差异),则饱和度自动为 0。否则,它将是 1 减去亮度乘以 2 再减 1 的绝对值 (1 - Math.abs(2 * l - 1))。获得这些值后,我们必须将其转换为 100% 的值,因此我们将它们乘以 100 并四舍五入到最接近的十分之一。现在我们可以将我们的 hsl() 串联起来。

function RGBToHSL(r,g,b) {
  ...
  // Calculate lightness
  l = (cmax + cmin) / 2;

  // Calculate saturation
  s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
    
  // Multiply l and s by 100
  s = +(s * 100).toFixed(1);
  l = +(l * 100).toFixed(1);

  return "hsl(" + h + "," + s + "%," + l + "%)";
}

字符串中的 RGB 到 HSL

对于一个字符串,用逗号或空格分割参数,去除 %,并像之前一样本地化 rgb

function RGBToHSL(rgb) {
  let sep = rgb.indexOf(",") > -1 ? "," : " ";
  rgb = rgb.substr(4).split(")")[0].split(sep);

  for (let R in rgb) {
    let r = rgb[R];
    if (r.indexOf("%") > -1) 
      rgb[R] = Math.round(r.substr(0,r.length - 1) / 100 * 255);
  }

  // Make r, g, and b fractions of 1
  let r = rgb[0] / 255,
      g = rgb[1] / 255,
      b = rgb[2] / 255;

  ...
}

RGBA 到 HSLA

与我们刚刚将 RGB 转换为 HSL 所做的相比,alpha 的对应部分基本上什么都不需要做!我们只需重用 RGB 到 HSL 的代码(多参数版本),保持 a 不变,并将 a 传递到返回的 HSLA 中。请记住,它应该在 0 和 1 之间。

function RGBAToHSLA(r,g,b,a) {
  // Code for RGBToHSL(r,g,b) before return
  ...

  return "hsla(" + h + "," + s + "%," +l + "%," + a + ")";
}

字符串中的 RGBA 到 HSLA

对于字符串值,我们再次应用分割和去除逻辑,但使用 rgba 中的第四个项目作为 a。还记得新的 rgba(r g b / a) 语法吗?我们像 RGBAToHexA() 一样采用了它的接受。然后其余代码是正常的 RGB 到 HSL 转换。

function RGBAToHSLA(rgba) {
  let sep = rgba.indexOf(",") > -1 ? "," : " ";
  rgba = rgba.substr(5).split(")")[0].split(sep);

  // Strip the slash if using space-separated syntax
  if (rgba.indexOf("/") > -1) 
    rgba.splice(3,1);

  for (let R in rgba) {
    let r = rgba[R];
    if (r.indexOf("%") > -1) {
      let p = r.substr(0,r.length - 1) / 100;

      if (R < 3) { 
        rgba[R] = Math.round(p * 255);
      } else {
        rgba[R] = p;
      }
    }
  }

  // Make r, g, and b fractions of 1
  let r = rgba[0] / 255,
      g = rgba[1] / 255,
      b = rgba[2] / 255,
      a = rgba[3];

  // Rest of RGB-to-HSL logic
  ...
}

希望将 alpha 保持原样?从 for 循环中删除 else 语句。

for (let R in rgba) {
  let r = rgba[R];
  if (r.indexOf("%") > -1) {
    let p = r.substr(0,r.length - 1) / 100;

    if (R < 3) {
      rgba[R] = Math.round(p * 255);
    }
  }
}

HSL 到 RGB

将 HSL 转换回 RGB 比反向转换需要更少的逻辑。由于我们将使用 0-100 的范围表示饱和度和亮度,因此第一步是将它们除以 100 以获得 0 到 1 之间的值。接下来,我们找到色度 (c),它是颜色强度,因此它是 (1 - Math.abs(2 * l - 1)) * s。然后我们使用 x 表示第二大的分量(第一个是色度),每个通道需要添加的量以匹配亮度 (m),并初始化 rgb

function HSLToRGB(h,s,l) {
  // Must be fractions of 1
  s /= 100;
  l /= 100;

  let c = (1 - Math.abs(2 * l - 1)) * s,
      x = c * (1 - Math.abs((h / 60) % 2 - 1)),
      m = l - c/2,
      r = 0,
      g = 0,
      b = 0;
}

色调将决定红色、绿色和蓝色应该是什么,具体取决于它位于色轮的哪个 60° 扇区。

Color wheel
色轮被分成 60° 的扇区

然后 cx 将按如下所示分配,使一个通道为 0。要获得最终的 RGB 值,我们将 m 添加到每个通道,乘以 255,并四舍五入。

function HSLToRGB(h,s,l) {
  ...

  if (0 <= h && h < 60) {
    r = c; g = x; b = 0;  
  } else if (60 <= h && h < 120) {
    r = x; g = c; b = 0;
  } else if (120 <= h && h < 180) {
    r = 0; g = c; b = x;
  } else if (180 <= h && h < 240) {
    r = 0; g = x; b = c;
  } else if (240 <= h && h < 300) {
    r = x; g = 0; b = c;
  } else if (300 <= h && h < 360) {
    r = c; g = 0; b = x;
  }
  r = Math.round((r + m) * 255);
  g = Math.round((g + m) * 255);
  b = Math.round((b + m) * 255);

  return "rgb(" + r + "," + g + "," + b + ")";
}

字符串中的 HSL 到 RGB

对于单个字符串版本,我们修改前几个语句的方式与 RGBToHSL(r,g,b) 基本相同。删除 s /= 100;l /= 100;,我们将使用新的语句擦除 HSL 值数组的前 4 个字符和 ),然后在将 sl 除以 100 之前,擦除 sl 中的 %

function HSLToRGB(hsl) {
  let sep = hsl.indexOf(",") > -1 ? "," : " ";
  hsl = hsl.substr(4).split(")")[0].split(sep);

  let h = hsl[0],
      s = hsl[1].substr(0,hsl[1].length - 1) / 100,
      l = hsl[2].substr(0,hsl[2].length - 1) / 100;

  ...
}

接下来的几个语句将处理带有单位(度、弧度或圈数)提供的色调。我们将弧度乘以 180/π,并将圈数乘以 360。如果结果超过 360,我们将进行复合模数除法以将其保持在范围内。所有这些都将在我们处理 cxm 之前发生。

function HSLToRGB(hsl) {
  ...

  // Strip label and convert to degrees (if necessary)
  if (h.indexOf("deg") > -1) 
    h = h.substr(0,h.length - 3);
  else if (h.indexOf("rad") > -1)
    h = Math.round(h.substr(0,h.length - 3) * (180 / Math.PI));
  else if (h.indexOf("turn") > -1)
    h = Math.round(h.substr(0,h.length - 4) * 360);
  // Keep hue fraction of 360 if ending up over
  if (h >= 360)
    h %= 360;
    
  // Conversion to RGB begins
  ...
}

在实施上述步骤后,现在可以安全地使用以下内容

  • hsl(180 100% 50%)
  • hsl(180deg,100%,50%)
  • hsl(180deg 100% 50%)
  • hsl(3.14rad,100%,50%)
  • hsl(3.14rad 100% 50%)
  • hsl(0.5turn,100%,50%)
  • hsl(0.5turn 100% 50%)

哇,这真是太灵活了!

使用 % 从 HSL 输出 RGB

类似地,我们可以修改此函数以返回百分比值,就像我们在 hexToRGB() 中所做的那样。

function HSLToRGB(hsl,isPct) {
  let sep = hsl.indexOf(",") > -1 ? "," : " ";
  hsl = hsl.substr(4).split(")")[0].split(sep);
  isPct = isPct === true;

  ...

  if (isPct) {
    r = +(r / 255 * 100).toFixed(1);
    g = +(g / 255 * 100).toFixed(1);
    b = +(b / 255 * 100).toFixed(1);
  }

  return "rgb("+ (isPct ? r + "%," + g + "%," + b + "%" : +r + "," + +g + "," + +b) + ")";
}

HSLA 到 RGBA

再次,处理 alpha 将非常简单。我们可以重新应用原始 HSLToRGB(h,s,l) 的代码并将 a 添加到 return 中。

function HSLAToRGBA(h,s,l,a) {
  // Code for HSLToRGB(h,s,l) before return
  ...

  return "rgba(" + r + "," + g + "," + b + "," + a + ")";
}

字符串中的 HSLA 到 RGBA

将其更改为一个参数,我们在这里处理字符串的方式与之前没有什么不同。颜色级别 4 中的新 HSLA 语法使用 (value value value / value),就像 RGBA 一样,因此拥有处理它的代码,我们将能够在这里插入类似 hsla(210 100% 50% / 0.5) 的内容。

function HSLAToRGBA(hsla) { 
  let sep = hsla.indexOf(",") > -1 ? "," : " ";
  hsla = hsla.substr(5).split(")")[0].split(sep);

  if (hsla.indexOf("/") > -1)
    hsla.splice(3,1);

  let h = hsla[0],
      s = hsla[1].substr(0,hsla[1].length - 1) / 100,
      l = hsla[2].substr(0,hsla[2].length - 1) / 100,
      a = hsla[3];
        
  if (h.indexOf("deg") > -1)
    h = h.substr(0,h.length - 3);
  else if (h.indexOf("rad") > -1)
    h = Math.round(h.substr(0,h.length - 3) * (180 / Math.PI));
  else if (h.indexOf("turn") > -1)
    h = Math.round(h.substr(0,h.length - 4) * 360);
  if (h >= 360)
    h %= 360;

  ...
}

此外,以下其他组合也成为可能

  • hsla(180,100%,50%,50%)
  • hsla(180 100% 50% / 50%)
  • hsla(180deg,100%,50%,0.5)
  • hsla(3.14rad,100%,50%,0.5)
  • hsla(0.5turn 100% 50% / 50%)

使用 % 从 HSLA 输出 RGBA

然后我们可以复制相同的逻辑来输出百分比,包括 alpha。如果 alpha 应该是一个百分比(在 pctFound 中搜索),以下是如何处理它

  1. 如果 rgb 要转换为百分比,则 a 应乘以 100,如果它还不是百分比。否则,删除 %,它将在 return 中添加回来。
  2. 如果 rgb 应该保持不变,则从 a 中删除 % 并将 a 除以 100。
function HSLAToRGBA(hsla,isPct) {
  // Code up to slash stripping
  ...
    
  isPct = isPct === true;
    
  // h, s, l, a defined to rounding of r, g, b
  ...
    
  let pctFound = a.indexOf("%") > -1;
    
  if (isPct) {
    r = +(r / 255 * 100).toFixed(1);
    g = +(g / 255 * 100).toFixed(1);
    b = +(b / 255 * 100).toFixed(1);
    if (!pctFound) {
      a *= 100;
    } else {
      a = a.substr(0,a.length - 1);
    }
        
  } else if (pctFound) {
    a = a.substr(0,a.length - 1) / 100;
  }

  return "rgba("+ (isPct ? r + "%," + g + "%," + b + "%," + a + "%" : +r + ","+ +g + "," + +b + "," + +a) + ")";
}

十六进制到 HSL

您可能会认为这个和下一个过程比其他的更复杂,但它们只是由两部分组成,并使用了循环逻辑。首先,我们将十六进制转换为 RGB。这给了我们转换为 HSL 所需的十进制数。

function hexToHSL(H) {
  // Convert hex to RGB first
  let r = 0, g = 0, b = 0;
  if (H.length == 4) {
    r = "0x" + H[1] + H[1];
    g = "0x" + H[2] + H[2];
    b = "0x" + H[3] + H[3];
  } else if (H.length == 7) {
    r = "0x" + H[1] + H[2];
    g = "0x" + H[3] + H[4];
    b = "0x" + H[5] + H[6];
  }
  // Then to HSL
  r /= 255;
  g /= 255;
  b /= 255;
  let cmin = Math.min(r,g,b),
      cmax = Math.max(r,g,b),
      delta = cmax - cmin,
      h = 0,
      s = 0,
      l = 0;

  if (delta == 0)
    h = 0;
  else if (cmax == r)
    h = ((g - b) / delta) % 6;
  else if (cmax == g)
    h = (b - r) / delta + 2;
  else
    h = (r - g) / delta + 4;

  h = Math.round(h * 60);

  if (h < 0)
    h += 360;

  l = (cmax + cmin) / 2;
  s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
  s = +(s * 100).toFixed(1);
  l = +(l * 100).toFixed(1);

  return "hsl(" + h + "," + s + "%," + l + "%)";
}

十六进制 (#rrggbbaa) 到 HSLA

这个函数中变化的行不多。我们将重复我们最近为获取 alpha 而执行的操作,通过转换十六进制,但不会立即将其除以 255。首先,我们必须像在其他 to-HSL 函数中那样获取色调、饱和度和亮度。然后,在结束的 return 之前,我们除以 alpha 并设置小数位数。

function hexAToHSLA(H) {
  let r = 0, g = 0, b = 0, a = 1;

  if (H.length == 5) {
    r = "0x" + H[1] + H[1];
    g = "0x" + H[2] + H[2];
    b = "0x" + H[3] + H[3];
    a = "0x" + H[4] + H[4];
  } else if (H.length == 9) {
    r = "0x" + H[1] + H[2];
    g = "0x" + H[3] + H[4];
    b = "0x" + H[5] + H[6];
    a = "0x" + H[7] + H[8];
  }

  // Normal conversion to HSL
  ...
        
  a = (a / 255).toFixed(3);
                
  return "hsla("+ h + "," + s + "%," + l + "%," + a + ")";
}

HSL 转十六进制

这个转换首先是转换为 RGB,但在将 RGB 结果转换为十六进制的 Math.round() 中多了一个步骤。

function HSLToHex(h,s,l) {
  s /= 100;
  l /= 100;

  let c = (1 - Math.abs(2 * l - 1)) * s,
      x = c * (1 - Math.abs((h / 60) % 2 - 1)),
      m = l - c/2,
      r = 0,
      g = 0, 
      b = 0; 

  if (0 <= h && h < 60) {
    r = c; g = x; b = 0;
  } else if (60 <= h && h < 120) {
    r = x; g = c; b = 0;
  } else if (120 <= h && h < 180) {
    r = 0; g = c; b = x;
  } else if (180 <= h && h < 240) {
    r = 0; g = x; b = c;
  } else if (240 <= h && h < 300) {
    r = x; g = 0; b = c;
  } else if (300 <= h && h < 360) {
    r = c; g = 0; b = x;
  }
  // Having obtained RGB, convert channels to hex
  r = Math.round((r + m) * 255).toString(16);
  g = Math.round((g + m) * 255).toString(16);
  b = Math.round((b + m) * 255).toString(16);

  // Prepend 0s, if necessary
  if (r.length == 1)
    r = "0" + r;
  if (g.length == 1)
    g = "0" + g;
  if (b.length == 1)
    b = "0" + b;

  return "#" + r + g + b;
}

字符串形式的 HSL 转十六进制

如果我们将它修改为接受单个字符串,那么此函数的前几行将类似于 HSLToRGB() 中的那些行。这就是我们最初分别获取色相、饱和度和亮度的方式。我们也不要忘记删除色相标签并转换为角度的步骤。所有这些都将替换 s /= 100;l /= 100;

function HSLToHex(hsl) { 
  let sep = hsl.indexOf(",") > -1 ? "," : " ";
  hsl = hsl.substr(4).split(")")[0].split(sep);

  let h = hsl[0],
      s = hsl[1].substr(0,hsl[1].length - 1) / 100,
      l = hsl[2].substr(0,hsl[2].length - 1) / 100;
        
  // Strip label and convert to degrees (if necessary)
  if (h.indexOf("deg") > -1)
    h = h.substr(0,h.length - 3);
  else if (h.indexOf("rad") > -1)
    h = Math.round(h.substr(0,h.length - 3) * (180 / Math.PI));
  else if (h.indexOf("turn") > -1)
    h = Math.round(h.substr(0,h.length - 4) * 360);
  if (h >= 360)
    h %= 360;

  ...
}

HSLA 转十六进制(#rrggbbaa)

添加 alpha 后,我们将 a 转换为十六进制并添加第四个 if,如果需要,则在前面添加一个 0。您可能已经熟悉此逻辑,因为我们上次在 RGBAToHexA() 中使用了它。

function HSLAToHexA(h,s,l,a) {
  // Repeat code from HSLToHex(h,s,l) until 3 `toString(16)`s
  ...

  a = Math.round(a * 255).toString(16);

  if (r.length == 1)
    r = "0" + r;
  if (g.length == 1)
    g = "0" + g;
  if (b.length == 1)
    b = "0" + b;
  if (a.length == 1)
    a = "0" + a;

  return "#" + r + g + b + a;
}

字符串形式的 HSLA 转十六进制(#rrggbbaa)

最后,单参数版本的行到 a = hsla[3]HSLAToRGBA() 的行没有区别。

function HSLAToHexA(hsla) {
  let sep = hsla.indexOf(",") > -1 ? "," : " ";
  hsla = hsla.substr(5).split(")")[0].split(sep);
    
  // Strip the slash
  if (hsla.indexOf("/") > -1)
    hsla.splice(3,1);
    
  let h = hsla[0],
      s = hsla[1].substr(0,hsla[1].length - 1) / 100,
      l = hsla[2].substr(0,hsla[2].length - 1) / 100,
      a = hsla[3];
            
  ...
}

内置名称

要将命名颜色转换为 RGB、十六进制或 HSL,您可以考虑将此包含 140 多个名称和十六进制值的表格转换为一个大型对象,作为开始。事实是,我们真的不需要一个,因为我们可以这样做

  1. 创建一个元素
  2. 为其指定文本颜色
  3. 获取该属性的值
  4. 删除元素
  5. 返回存储的颜色值,默认情况下该值将为 RGB

因此,我们获取 RGB 的函数将只有七个语句!

function nameToRGB(name) {
  // Create fake div
  let fakeDiv = document.createElement("div");
  fakeDiv.style.color = name;
  document.body.appendChild(fakeDiv);

  // Get color of div
  let cs = window.getComputedStyle(fakeDiv),
      pv = cs.getPropertyValue("color");

  // Remove div after obtaining desired color value
  document.body.removeChild(fakeDiv);

  return pv;
}

让我们更进一步。我们如何将输出更改为十六进制?

function nameToHex(name) {
  // Get RGB from named color in temporary div
  let fakeDiv = document.createElement("div");
  fakeDiv.style.color = name;
  document.body.appendChild(fakeDiv);

  let cs = window.getComputedStyle(fakeDiv),
      pv = cs.getPropertyValue("color");

  document.body.removeChild(fakeDiv);

  // Code ripped from RGBToHex() (except pv is substringed)
  let rgb = pv.substr(4).split(")")[0].split(","),
      r = (+rgb[0]).toString(16),
      g = (+rgb[1]).toString(16),
      b = (+rgb[2]).toString(16);

  if (r.length == 1)
    r = "0" + r;
  if (g.length == 1)
    g = "0" + g;
  if (b.length == 1)
    b = "0" + b;

  return "#" + r + g + b;
}

或者,为什么不使用 HSL 呢?😉

function nameToHSL(name) {
  let fakeDiv = document.createElement("div");
  fakeDiv.style.color = name;
  document.body.appendChild(fakeDiv);

  let cs = window.getComputedStyle(fakeDiv),
      pv = cs.getPropertyValue("color");

  document.body.removeChild(fakeDiv);

  // Code ripped from RGBToHSL() (except pv is substringed)
  let rgb = pv.substr(4).split(")")[0].split(","),
      r = rgb[0] / 255,
      g = rgb[1] / 255,
      b = rgb[2] / 255,
      cmin = Math.min(r,g,b),
      cmax = Math.max(r,g,b),
      delta = cmax - cmin,
      h = 0,
      s = 0,
      l = 0;

  if (delta == 0)
    h = 0;
  else if (cmax == r)
    h = ((g - b) / delta) % 6;
  else if (cmax == g)
    h = (b - r) / delta + 2;
  else
    h = (r - g) / delta + 4;

  h = Math.round(h * 60);

  if (h < 0)
    h += 360;

  l = (cmax + cmin) / 2;
  s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
  s = +(s * 100).toFixed(1);
  l = +(l * 100).toFixed(1);

  return "hsl(" + h + "," + s + "%," + l + "%)";
}

从长远来看,从名称到 RGB 的每次转换,在破解名称后都会变成从 RGB 的转换。

验证颜色

在所有这些函数中,都没有任何措施来防止或纠正荒谬的输入(例如超过 360 的色相或超过 100 的百分比)。如果我们只是操作使用 getImageData() 获取的 上的像素,则在转换之前不需要验证颜色值,因为无论如何它们都是正确的。如果我们正在创建用户提供颜色的颜色转换工具,那么验证将非常必要。

像这样处理 RGB 的单独参数作为通道的错误输入很容易

// Correct red
if (r > 255)
  r = 255;
else if (r < 0)
  r = 0;

如果要验证整个字符串,则需要正则表达式。例如,这是给定验证步骤和表达式的 RGBToHex() 函数

function RGBToHex(rgb) {
  // Expression for rgb() syntaxes
  let ex = /^rgb\((((((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]),\s?)){2}|((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5])\s)){2})((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]))|((((([1-9]?\d(\.\d+)?)|100|(\.\d+))%,\s?){2}|((([1-9]?\d(\.\d+)?)|100|(\.\d+))%\s){2})(([1-9]?\d(\.\d+)?)|100|(\.\d+))%))\)$/i;

  if (ex.test(rgb)) {
    // Logic to convert RGB to hex
    ...

  } else {
    // Something to do if color is invalid
  }
}

要测试其他类型的值,下表列出了涵盖不透明和启用 alpha 的表达式

颜色值正则表达式
RGB/^rgb\((((((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]),\s?)){2}|((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5])\s)){2})((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]))|((((([1-9]?\d(\.\d+)?)|100|(\.\d+))%,\s?){2}|((([1-9]?\d(\.\d+)?)|100|(\.\d+))%\s){2})(([1-9]?\d(\.\d+)?)|100|(\.\d+))%))\)$/i
RGBA/^rgba\((((((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]),\s?)){3})|(((([1-9]?\d(\.\d+)?)|100|(\.\d+))%,\s?){3}))|(((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5])\s){3})|(((([1-9]?\d(\.\d+)?)|100|(\.\d+))%\s){3}))\/\s)((0?\.\d+)|[01]|(([1-9]?\d(\.\d+)?)|100|(\.\d+))%)\)$/i
十六进制/^#([\da-f]{3}){1,2}$/i
十六进制(带 Alpha)/^#([\da-f]{4}){1,2}$/i
HSL/^hsl\(((((([12]?[1-9]?\d)|[12]0\d|(3[0-5]\d))(\.\d+)?)|(\.\d+))(deg)?|(0|0?\.\d+)turn|(([0-6](\.\d+)?)|(\.\d+))rad)((,\s?(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2}|(\s(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2})\)$/i
HSLA/^hsla\(((((([12]?[1-9]?\d)|[12]0\d|(3[0-5]\d))(\.\d+)?)|(\.\d+))(deg)?|(0|0?\.\d+)turn|(([0-6](\.\d+)?)|(\.\d+))rad)(((,\s?(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2},\s?)|((\s(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2}\s\/\s))((0?\.\d+)|[01]|(([1-9]?\d(\.\d+)?)|100|(\.\d+))%)\)$/i

查看 RGB(A) 和 HSL(A) 的表达式,您现在可能睁大了眼睛;这些表达式已经变得足够全面,可以包含 CSS Colors Level 4 中的大多数新语法。另一方面,十六进制不需要像其他表达式那样长,因为它只包含数字计数。稍后,我们将分析这些表达式并解读各个部分。请注意,不区分大小写的值 (/i) 通过所有这些验证。

RGB

/^rgb\((((((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]),\s?)){2}|((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5])\s)){2})((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]))|((((([1-9]?\d(\.\d+)?)|100|(\.\d+))%,\s?){2}|((([1-9]?\d(\.\d+)?)|100|(\.\d+))%\s){2})(([1-9]?\d(\.\d+)?)|100|(\.\d+))%))\)$/i

因为 rgb() 接受所有整数或所有百分比,所以两种情况都涵盖了。在最外层组中,在 ^rgb\(\)$ 之间,有两个内部组分别用于整数和百分比,所有组都以逗号空格或仅空格作为分隔符

  1. (((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]),\s?){2}|(((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5])\s){2})((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]))
  2. ((((([1-9]?\d(\.\d+)?)|100|(\.\d+))%,\s?){2}|((([1-9]?\d(\.\d+)?)|100|(\.\d+))%\s){2})(([1-9]?\d(\.\d+)?)|100|(\.\d+))%)

在前半部分,我们接受红色和绿色的两个整数实例,范围从 0 到 99 或 111 到 199 ((1?[1-9]?\d))、100 到 109 (10\d)、200 到 249 ((2[0-4]\d)) 或 250 到 255 (25[0-5])。我们不能简单地使用 \d{1,3},因为像 03 或 017 以及大于 255 的值都不应该被允许。之后是逗号和可选空格 (,\s?)。在 | 的另一侧,在第一个 {2}(表示两个整数实例)之后,如果左侧为假,我们将检查相同内容,但使用空格分隔符。然后对于蓝色,应该接受相同的内容,但没有分隔符。

在另一半中,应该接受包括浮点数在内的百分比的可接受值,这些值可以是 0 到 99,明确地是 100 而不是浮点数,或者小于 1 且省略了 0 的浮点数。因此,此处的片段为 (([1-9]?\d(\.\d+)?)|100|(\.\d+)),它出现了三次;两次带分隔符 (,\s?){2}%\s){2}),一次不带分隔符。

在 CSS 中,使用不带空格分隔符的百分比 (例如 rgb(100%50%10%)) 是合法的,但我们编写的函数不支持该功能。对于 rgba(100%50%10%/50%)hsl(40 100%50%)hsla(40 100%50%/0.5) 也是如此。这对于代码高尔夫和压缩来说可能是一个很大的优势!

RGBA

/^rgba\((((((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]),\s?)){3})|(((([1-9]?\d(\.\d+)?)|100|(\.\d+))%,\s?){3}))|(((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5])\s){3})|(((([1-9]?\d(\.\d+)?)|100|(\.\d+))%\s){3}))\/\s)((0?\.\d+)|[01]|(([1-9]?\d(\.\d+)?)|100|(\.\d+))%)\)$/i

下一个表达式与上一个非常相似,但检查了三个整数实例 (((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]),\s?){3})) 或百分比 ((((([1-9]?\d(\.\d+)?)|100|(\.\d+))%,\s?){3})),以及逗号可选空格。否则,它将查找相同的内容,但使用空格分隔符,以及蓝色的后斜杠和空格 (\/\s)。接下来是 ((0?\.\d+)|[01]|(([1-9]?\d(\.\d+)?)|100|(\.\d+))%),我们接受带或不带第一个 0 的浮点数 ((0?\.\d+))、点上的 0 或 1 ([01]) 或 0 到 100% ((([1-9]?\d(\.\d+)?)|100|(\.\d+))%)。

带 Alpha 的十六进制

// #rgb/#rrggbb
/^#([\da-f]{3}){1,2}$/i
// #rgba/#rrggbbaa
/^#([\da-f]{4}){1,2}$/i

对于带和不带 alpha 的十六进制,都接受数字或字母 a 到 f 的实例 ([\da-f])。然后计算此实例的一个或两个实例,以获取提供的简写或长格式值(#rgb 或 #rrggbb)。举例来说,我们有这个相同的简写模式:/^#([\da-f]{n}){1,2}$/i。只需将 n 更改为 3 或 4 即可。

HSL 和 HSLA

// HSL
/^hsl\((((((\[12]?[1-9]?\d)|[12]0\d|(3[0-5]\d))(\.\d+)?)|(\.\d+))(deg)?|(0|0?\.\d+)turn|(([0-6\\.\d+)?)|(\.\d+))rad)((,\s?(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2}|(\s(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2})\)$/i
// HSLA
/^hsla\((((((\[12]?[1-9]?\d)|[12]0\d|(3[0-5]\d))(\.\d+)?)|(\.\d+))(deg)?|(0|0?\.\d+)turn|(([0-6\\.\d+)?)|(\.\d+))rad)(((,\s?(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2},\s?)|((\s(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2}\s\/\s))((0?\.\d+)|[01]|(([1-9]?\d(\.\d+)?)|100|(\.\d+))%)\)$/i

在 HSL 和 HSLA 的两个表达式中的 \( 之后,这大块代码用于色相

(((((\[12]?[1-9]?\d)|[12]0\d|(3[0-5]\d))(\.\d+)?)|(\.\d+))(deg)?|(0|0?\.\d+)turn|(([0-6\\.\d+)?)|(\.\d+))rad)

([12]?[1-9]?\d) 涵盖 0 到 99、110 到 199 和 210 到 299。[12]0\d 涵盖 110 到 109 和 200 到 209。然后 (3[0-5]\d) 处理 300 到 359。这样划分范围的原因与 rgb() 语法中的整数类似:排除第一个为零的值和大于最大值的值。由于色相可以是浮点数,因此第一个 (\.\d+)? 用于表示浮点数。

在上述代码段之后的 | 旁边,第二个 (\.\d+) 用于表示没有前导零的浮点数。

现在让我们向上提升一级并解读下一个小块

(deg)?|(0|0?\.\d+)turn|((\[0-6\\.\d+)?)|(\.\d+))rad

这段代码包含了我们可以用于色相的标签——度数、圈数或弧度。我们可以包含所有或不包含degturn中的值必须小于1。对于弧度,我们可以接受0到7之间的任何浮点数。但是我们知道,一个360°的圆周是2π,它大约在6.28处停止。你可能会认为6.3及以上不应该被接受。因为2π是一个无理数,在这个例子中尝试满足JavaScript控制台提供的每个小数位会太麻烦了。此外,如果色相为360°或更大,我们在HSLTo_()函数中有一个作为第二层安全措施的代码片段。

// Keep hue fraction of 360 if ending up over
if (h >= 360)
  h %= 360;

现在让我们向上提升一级,解读第二部分。

(,\s?(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2}

我们正在统计饱和度和亮度两个逗号-空格-百分比的实例(空格可选)。在,\s?后面的组中,我们测试0到99之间的值,是否带有小数点(([1-9]?\d(\.\d+)?)),正好是100,或者小于1的浮点数,不带前导0((\.\d+))。

HSL表达式的最后一部分,在结束符(\)$/i)之前,是一个类似的表达式,如果空格是唯一的分隔符。

(\s(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2}

\s在开头而不是,\s?。然后在HSLA表达式中,这个相同的代码块位于另一个组中,其后跟着,\s?{2}

((,\s?(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2},\s?)

这计算了亮度和alpha之间的逗号-空格。然后,如果我们使用空格作为分隔符,我们需要在统计两个空格和一个百分比后检查空格-斜杠-空格(\s\/\s)。

((\s(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2}\s\/\s))

之后,我们剩下要检查alpha值。

(((0?\.\d+)|[01])|(([1-9]?\d(\.\d+)?)|100|(\.\d+))%)

(0?\.\d+)的匹配项包括小于1的浮点数,是否带前导0,[01]表示0或1,以及0到100%。

结论

如果你的当前挑战是将一个颜色空间转换为另一个颜色空间,你现在有一些关于如何处理它的想法。因为在一个帖子中遍历所有发明过的颜色空间会很乏味,所以我们讨论了最实用和浏览器支持的颜色空间。如果你想超越支持的颜色空间(比如CMYK、XYZ或CIE Lab*),EasyRGB提供了一套很棒的代码就绪公式。

为了查看此处演示的所有转换,我设置了一个CodePen 演示,它在一个表格中显示输入和输出。你可以在第2到10行尝试不同的颜色,并在JavaScript面板中查看完整的函数。