马黑黑 发表于 2025-11-20 19:36

颜色转换

<style>
    .pa { margin: 10px auto; width: 740px; height: 740px; font-size: 16px; position: relative; }
    #msvg { position: absolute; width: 100%; height: 90%; }
    #msvg circle { cursor: pointer; }
    #msvg text { font-size: 10px; text-anchor: middle; dominant-baseline: middle; }
    #msvg line { cursor: pointer; stroke: silver; stroke-width: 4; }
    .pa h1 { margin: 0; padding: 20px; font-size: 2em; text-align: center; }
</style>

<div id="pa" class="pa">
    <h1 class="txtCenter">颜色转换</h1>
    <svg id="msvg" viewBox="-200 -200 400 400">
      <marker id="arrow" viewBox="0 0 5 5" refX="3.5" refY="2.5" markerWidth="3" markerHeight="3" orient="auto">
            <path d="M 0 0 L 5 2.5 L 0 5 z" fill="gray" />
      </marker>
      <circle id="cHex" cx="-120" cy="-100" r="75" fill="transparent" stroke="silver">
            <title>十六进制 : 点击生成随机颜色</title>
      </circle>
      <circle id="cRgb" cx="120" cy="-100" r="75" fill="transparent" stroke="silver">
            <title>RGB : 点击生成随机颜色</title>
      </circle>
      <circle id="cHsl" cx="0" cy="100" r="75" fill="transparent" stroke="silver">
            <title>HSL : 点击生成随机颜色</title>
      </circle>
      <line id="lHex2Rgb" x1="-10" y1="-110" x2="10" y2="-110" marker-end="url(#arrow)">
            <title>十六进制转RGB</title>
      </line>
      <line id="lRgb2Hex" x1="10" y1="-90" x2="-10" y2="-90" marker-end="url(#arrow)">
            <title>RGB转十六进制</title>
      </line>
      <line id="lHex2Hsl" x1="-80" y1="-10" x2="-70" y2="10" marker-end="url(#arrow)">
            <title>十六进制转HSL</title>
      </line>
      <line id="lHsl2Hex" x1="-50" y1="10" x2="-60" y2="-10" marker-end="url(#arrow)">
            <title>HSL转十六进制</title>
      </line>
      <line id="lRgb2Hsl" x1="70" y1="-10" x2="60" y2="10" marker-end="url(#arrow)">
            <title>RGB转HSL</title>
      </line>
      <line id="lHsl2Rgb" x1="40" y1="10" x2="50" y2="-10" marker-end="url(#arrow)">
            <title>HSL转RGB</title>
      </line>

      <text id="tHex" x="-120" y="0">HEX</text>
      <text id="tRgb" x="120" y="0">RGB</text>
      <text id="tHsl" x="0" y="190">HSL</text>
    </svg>
</div>

<script type="module">
    const getRanHex = () => `#${Math.random().toString(16).substring(2,8)}`;

    const getRanRgb = () => {
      const r = Math.floor(Math.random() * 256);
      const g = Math.floor(Math.random() * 256);
      const b = Math.floor(Math.random() * 256);
      return `rgb(${r}, ${g}, ${b})`;
    };

    const getRanHsl = () => `hsl(${Math.floor(Math.random() * 361)}, 100%, 50%)`;

    const HexColor2Rgb = (str) => {
      const fullHex = (str) => {
            let newStr = str.replace('#', '');
            if (newStr.length === 3) {
                return newStr.split('').map(i => i.repeat(2)).join('');
            }
            return newStr;
      };
      const Hex2Dec = (str) => parseInt(str, 16);
      const ar = fullHex(str).match(/.{2}/g).map(a => Hex2Dec(a));
      return 'rgb(' + ar.join(', ') + ')';
    };

    const RgbColor2Hex = (str) => {
      const regex = /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(+))?\s*\)/i;
      const matches = str.match(regex);
          if (!matches) return null;
      let r = Math.min(255, Math.max(0, parseInt(matches, 10)));
      let g = Math.min(255, Math.max(0, parseInt(matches, 10)));
      let b = Math.min(255, Math.max(0, parseInt(matches, 10)));
      const toHex = (n) => n.toString(16).padStart(2, '0');
      let hex = `#${toHex(r)}${toHex(g)}${toHex(b)}`;
      return hex;
    };

    const RgbColor2Hsl = (str) => {
      const rgb2Hsl = (r, g, b) => {
            r /= 255;
            g /= 255;
            b /= 255;
            const max = Math.max(r, g, b);
            const min = Math.min(r, g, b);
            let h, s, l = (max + min) / 2;
            if (max === min) {
                h = s = 0;
            } else {
                const d = max - min;
                s = l > 0.5 ? d / (2 - max - min) : d /(max + min);
                switch (max) {
                  case r: h = (g - b) / d + (g < b ? 6 : 0); break;
                  case g: h = (b - r) / d + 2; break;
                  case b: h = (r - g) / d + 4; break;
                }
                h /= 6;
            }
            return ;
      };

      const regex = /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(+))?\s*\)/i;
      const matches = str.match(regex);
      if (!matches) return null;
      let r = Math.min(255, Math.max(0, parseInt(matches, 10)));
      let g = Math.min(255, Math.max(0, parseInt(matches, 10)));
      let b = Math.min(255, Math.max(0, parseInt(matches, 10)));
      const colors = rgb2Hsl(r, g, b);
      const h = Math.round(colors * 360);
      const s = Math.round(colors * 100) + '%';
      const l = Math.round(colors * 100) + '%';
      return `hsl(${h}, ${s}, ${l})`;
    };

    const HslColor2Rgb = (str) => {
      const hsl2Rgb = (h, s, l) => {
            let r, g, b;
            if(s === 0) {
                r = g = b = l;
            } else {
                const hue2rgb = function hue2rgb(p, q, t) {
                  if(t < 0) t += 1;
                  if(t > 1) t -= 1;
                  if(t < 1/6) return p + (q - p) * 6 * t;
                  if(t < 1/2) return q;
                  if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
                  return p;
               }
                const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
                const p = 2 * l - q;
                r = hue2rgb(p, q, h + 1/3);
                g = hue2rgb(p, q, h);
                b = hue2rgb(p, q, h - 1/3);
            }
            return ;
      };

      const regex = /hsla?\((\d+),\s*([\d.]+)%,\s*([\d.]+)%(\s*,\s*[\d.]+)?\)/i;
      const matches = str.match(regex);
      if (!matches) return null;
      const h = parseInt(matches, 10) / 360;
      const s = parseFloat(matches) / 100;
      const l = parseFloat(matches) / 100;
      const colors = hsl2Rgb(h, s, l);
      const r = colors;
      const g = colors;
      const b = colors;
      return `rgb(${r}, ${g}, ${b})`;
    };

    cHex.onclick = () => {
      const color = getRanHex();
      cHex.setAttribute('fill', color);
      tHex.textContent = color;
    }

    cRgb.onclick = () => {
      const color = getRanRgb();
      cRgb.setAttribute('fill', color);
      tRgb.textContent = color;
    };

    cHsl.onclick = () => {
      const color = getRanHsl();
      cHsl.setAttribute('fill', color);
      tHsl.textContent = color;
    };

    lHex2Rgb.onclick = () => {
      const color = HexColor2Rgb(tHex.textContent);
      cRgb.setAttribute('fill', color);
      tRgb.textContent = color;
    };

    lRgb2Hex.onclick = () => {
      const color = RgbColor2Hex(tRgb.textContent);
      cHex.setAttribute('fill', color);
      tHex.textContent = color;
    };

    lRgb2Hsl.onclick = () => {
      const color = RgbColor2Hsl(tRgb.textContent)
      cHsl.setAttribute('fill', color);
      tHsl.textContent = color;
    };

    lHsl2Rgb.onclick = () => {
      const color = HslColor2Rgb(tHsl.textContent);
      cRgb.setAttribute('fill', color);
      tRgb.textContent = color;
    };

    lHsl2Hex.onclick = () => {
      const color = RgbColor2Hex(HslColor2Rgb(tHsl.textContent));
      cHex.setAttribute('fill', color);
      tHex.textContent = color;
    };

    lHex2Hsl.onclick = () => {
      const color = RgbColor2Hsl(HexColor2Rgb(tHex.textContent));
      cHsl.setAttribute('fill', color);
      tHsl.textContent = color;
    };

    window.onload = () => {
      const hexColor = getRanHex();
      const rgbColor = getRanRgb();
      const hslColor = getRanHsl();
      cHex.setAttribute('fill', hexColor);
      tHex.textContent = hexColor;
      cRgb.setAttribute('fill', rgbColor);
      tRgb.textContent = rgbColor;
      cHsl.setAttribute('fill', hslColor);
      tHsl.textContent = hslColor;
    };
</script>

马黑黑 发表于 2025-11-20 19:37

源码

<style>
    .pa { margin: 10px auto; width: 740px; height: 740px; font-size: 16px; position: relative; }
    #msvg { position: absolute; width: 100%; height: 90%; }
    #msvg circle { cursor: pointer; }
    #msvg text { font-size: 10px; text-anchor: middle; dominant-baseline: middle; }
    #msvg line { cursor: pointer; stroke: silver; stroke-width: 4; }
    .pa h1 { margin: 0; padding: 20px; font-size: 2em; text-align: center; }
</style>

<div id="pa" class="pa">
    <h1 class="txtCenter">颜色转换</h1>
    <svg id="msvg" viewBox="-200 -200 400 400">
      <marker id="arrow" viewBox="0 0 5 5" refX="3.5" refY="2.5" markerWidth="3" markerHeight="3" orient="auto">
            <path d="M 0 0 L 5 2.5 L 0 5 z" fill="gray" />
      </marker>
      <circle id="cHex" cx="-120" cy="-100" r="75" fill="transparent" stroke="silver">
            <title>十六进制 : 点击生成随机颜色</title>
      </circle>
      <circle id="cRgb" cx="120" cy="-100" r="75" fill="transparent" stroke="silver">
            <title>RGB : 点击生成随机颜色</title>
      </circle>
      <circle id="cHsl" cx="0" cy="100" r="75" fill="transparent" stroke="silver">
            <title>HSL : 点击生成随机颜色</title>
      </circle>
      <line id="lHex2Rgb" x1="-10" y1="-110" x2="10" y2="-110" marker-end="url(#arrow)">
            <title>十六进制转RGB</title>
      </line>
      <line id="lRgb2Hex" x1="10" y1="-90" x2="-10" y2="-90" marker-end="url(#arrow)">
            <title>RGB转十六进制</title>
      </line>
      <line id="lHex2Hsl" x1="-80" y1="-10" x2="-70" y2="10" marker-end="url(#arrow)">
            <title>十六进制转HSL</title>
      </line>
      <line id="lHsl2Hex" x1="-50" y1="10" x2="-60" y2="-10" marker-end="url(#arrow)">
            <title>HSL转十六进制</title>
      </line>
      <line id="lRgb2Hsl" x1="70" y1="-10" x2="60" y2="10" marker-end="url(#arrow)">
            <title>RGB转HSL</title>
      </line>
      <line id="lHsl2Rgb" x1="40" y1="10" x2="50" y2="-10" marker-end="url(#arrow)">
            <title>HSL转RGB</title>
      </line>

      <text id="tHex" x="-120" y="0">HEX</text>
      <text id="tRgb" x="120" y="0">RGB</text>
      <text id="tHsl" x="0" y="190">HSL</text>
    </svg>
</div>

<script type="module">
    const getRanHex = () => `#${Math.random().toString(16).substring(2,8)}`;

    const getRanRgb = () => {
      const r = Math.floor(Math.random() * 256);
      const g = Math.floor(Math.random() * 256);
      const b = Math.floor(Math.random() * 256);
      return `rgb(${r}, ${g}, ${b})`;
    };

    const getRanHsl = () => `hsl(${Math.floor(Math.random() * 361)}, 100%, 50%)`;

    const HexColor2Rgb = (str) => {
      const fullHex = (str) => {
            let newStr = str.replace('#', '');
            if (newStr.length === 3) {
                return newStr.split('').map(i => i.repeat(2)).join('');
            }
            return newStr;
      };
      const Hex2Dec = (str) => parseInt(str, 16);
      const ar = fullHex(str).match(/.{2}/g).map(a => Hex2Dec(a));
      return 'rgb(' + ar.join(', ') + ')';
    };

    const RgbColor2Hex = (str) => {
      const regex = /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(+))?\s*\)/i;
      const matches = str.match(regex);
          if (!matches) return null;
      let r = Math.min(255, Math.max(0, parseInt(matches, 10)));
      let g = Math.min(255, Math.max(0, parseInt(matches, 10)));
      let b = Math.min(255, Math.max(0, parseInt(matches, 10)));
      const toHex = (n) => n.toString(16).padStart(2, '0');
      let hex = `#${toHex(r)}${toHex(g)}${toHex(b)}`;
      return hex;
    };

    const RgbColor2Hsl = (str) => {
      const rgb2Hsl = (r, g, b) => {
            r /= 255;
            g /= 255;
            b /= 255;
            const max = Math.max(r, g, b);
            const min = Math.min(r, g, b);
            let h, s, l = (max + min) / 2;
            if (max === min) {
                h = s = 0;
            } else {
                const d = max - min;
                s = l > 0.5 ? d / (2 - max - min) : d /(max + min);
                switch (max) {
                  case r: h = (g - b) / d + (g < b ? 6 : 0); break;
                  case g: h = (b - r) / d + 2; break;
                  case b: h = (r - g) / d + 4; break;
                }
                h /= 6;
            }
            return ;
      };

      const regex = /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(+))?\s*\)/i;
      const matches = str.match(regex);
      if (!matches) return null;
      let r = Math.min(255, Math.max(0, parseInt(matches, 10)));
      let g = Math.min(255, Math.max(0, parseInt(matches, 10)));
      let b = Math.min(255, Math.max(0, parseInt(matches, 10)));
      const colors = rgb2Hsl(r, g, b);
      const h = Math.round(colors * 360);
      const s = Math.round(colors * 100) + '%';
      const l = Math.round(colors * 100) + '%';
      return `hsl(${h}, ${s}, ${l})`;
    };

    const HslColor2Rgb = (str) => {
      const hsl2Rgb = (h, s, l) => {
            let r, g, b;
            if(s === 0) {
                r = g = b = l;
            } else {
                const hue2rgb = function hue2rgb(p, q, t) {
                  if(t < 0) t += 1;
                  if(t > 1) t -= 1;
                  if(t < 1/6) return p + (q - p) * 6 * t;
                  if(t < 1/2) return q;
                  if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
                  return p;
               }
                const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
                const p = 2 * l - q;
                r = hue2rgb(p, q, h + 1/3);
                g = hue2rgb(p, q, h);
                b = hue2rgb(p, q, h - 1/3);
            }
            return ;
      };

      const regex = /hsla?\((\d+),\s*([\d.]+)%,\s*([\d.]+)%(\s*,\s*[\d.]+)?\)/i;
      const matches = str.match(regex);
      if (!matches) return null;
      const h = parseInt(matches, 10) / 360;
      const s = parseFloat(matches) / 100;
      const l = parseFloat(matches) / 100;
      const colors = hsl2Rgb(h, s, l);
      const r = colors;
      const g = colors;
      const b = colors;
      return `rgb(${r}, ${g}, ${b})`;
    };

    cHex.onclick = () => {
      const color = getRanHex();
      cHex.setAttribute('fill', color);
      tHex.textContent = color;
    }

    cRgb.onclick = () => {
      const color = getRanRgb();
      cRgb.setAttribute('fill', color);
      tRgb.textContent = color;
    };

    cHsl.onclick = () => {
      const color = getRanHsl();
      cHsl.setAttribute('fill', color);
      tHsl.textContent = color;
    };

    lHex2Rgb.onclick = () => {
      const color = HexColor2Rgb(tHex.textContent);
      cRgb.setAttribute('fill', color);
      tRgb.textContent = color;
    };

    lRgb2Hex.onclick = () => {
      const color = RgbColor2Hex(tRgb.textContent);
      cHex.setAttribute('fill', color);
      tHex.textContent = color;
    };

    lRgb2Hsl.onclick = () => {
      const color = RgbColor2Hsl(tRgb.textContent)
      cHsl.setAttribute('fill', color);
      tHsl.textContent = color;
    };

    lHsl2Rgb.onclick = () => {
      const color = HslColor2Rgb(tHsl.textContent);
      cRgb.setAttribute('fill', color);
      tRgb.textContent = color;
    };

    lHsl2Hex.onclick = () => {
      const color = RgbColor2Hex(HslColor2Rgb(tHsl.textContent));
      cHex.setAttribute('fill', color);
      tHex.textContent = color;
    };

    lHex2Hsl.onclick = () => {
      const color = RgbColor2Hsl(HexColor2Rgb(tHex.textContent));
      cHsl.setAttribute('fill', color);
      tHsl.textContent = color;
    };

    window.onload = () => {
      const hexColor = getRanHex();
      const rgbColor = getRanRgb();
      const hslColor = getRanHsl();
      cHex.setAttribute('fill', hexColor);
      tHex.textContent = hexColor;
      cRgb.setAttribute('fill', rgbColor);
      tRgb.textContent = rgbColor;
      cHsl.setAttribute('fill', hslColor);
      tHsl.textContent = hslColor;
    };
</script>

马黑黑 发表于 2025-11-20 19:45

.pa 选择器规范了 svg 的尺寸,所以本演示具备自适应高宽能力,只需设置好 .pa 选择器的 width 和 height 属性值。

颜色的转换难度最大的是 hsl,由于颜色空间的体系不同,转换时会涉及到浮点数和三角函数运算,由于JS处理浮点数的天生缺陷,rgb - hsl 之间的往返转换会导致浮点数精准度的流失,特别是 hsl 转为 rgb 时表现明显,不过颜色效果人眼无法识别。

本演示颜色转换的核心算法,有一部分使用或参考了开源的代码,这是允许的也是合理的。特此声明。

马黑黑 发表于 2025-11-20 19:50

另外:

一、演示代码已经支持对 hsl 的饱和度、亮度的支持,处于简化,演示固定了正常饱和度为 100%,亮度为 50%,s 和 l 的变化未参与到演示中。

二、演示未处理颜色 alpha 通道,但所使用的匹配颜色表达式德尔正则均一纳入 alpha通道,需要时可以随时启用。

小辣椒 发表于 2025-11-20 23:05

魔术一样{:4_170:}

小辣椒 发表于 2025-11-20 23:06

马黑黑 发表于 2025-11-20 19:45
.pa 选择器规范了 svg 的尺寸,所以本演示具备自适应高宽能力,只需设置好 .pa 选择器的 width 和 height...

惊叹,代码的魔力{:4_170:}

小辣椒 发表于 2025-11-20 23:07

马黑黑 发表于 2025-11-20 19:50
另外:

一、演示代码已经支持对 hsl 的饱和度、亮度的支持,处于简化,演示固定了正常饱和度为 100%,亮 ...

先睡觉去了,明天抽时间再上来

红影 发表于 2025-11-20 23:53

三种颜色可以相互转换,这个厉害了{:4_199:}

红影 发表于 2025-11-21 00:01

马黑黑 发表于 2025-11-20 19:50
另外:

一、演示代码已经支持对 hsl 的饱和度、亮度的支持,处于简化,演示固定了正常饱和度为 100%,亮 ...

固定了饱和度和亮度也很不易了,否则需要考虑的更多了{:4_187:}

马黑黑 发表于 2025-11-21 08:13

红影 发表于 2025-11-21 00:01
固定了饱和度和亮度也很不易了,否则需要考虑的更多了

算法都已经集成了,这已经不是问题。

主要演示页面的设计没有提供改变饱和度和亮度,使用的是正常情况下的颜色呈现,所以固定s和l分量

马黑黑 发表于 2025-11-21 08:19

红影 发表于 2025-11-20 23:53
三种颜色可以相互转换,这个厉害了

这是现在Web页最常用到的三种颜色表示法。Hex和RGB同属于一个颜色体系,二者的颜色转换仅需要在进制中完成便可;hsl是另外的颜色空转,和前二者间的换有些难度,需要掌握的原理很多很深(维基百科上有详细介绍换算方法),但在实际应用中它更好控制,也能产生出更多更细致的颜色,很多场景需要用到它。

马黑黑 发表于 2025-11-21 08:19

小辣椒 发表于 2025-11-20 23:07
先睡觉去了,明天抽时间再上来

88

马黑黑 发表于 2025-11-21 08:20

小辣椒 发表于 2025-11-20 23:06
惊叹,代码的魔力

{:4_172:}

马黑黑 发表于 2025-11-21 08:20

小辣椒 发表于 2025-11-20 23:05
魔术一样

{:4_190:}
页: [1]
查看完整版本: 颜色转换