乘风原创程序

  • JavaScript实现HSL拾色器
  • 2020/6/23 9:37:08
  • hsl 和 hsv 在数学上定义为在 rgb 空间中的颜色的 r, g 和 b 的坐标的变换。

    从 rgb 到 hsl 或 hsv 的转换

    设 (r, g, b) 分别是一个颜色的红、绿和蓝坐标,它们的值是在 0 到 1 之间的实数。设 max 等价于 r, g 和 b 中的最大者。设 min 等于这些值中的最小者。要找到在 hsl 空间中的 (h, s, l) 值,这里的 h ∈ [0, 360)是角度的色相角,而 s, l ∈ [0,1] 是饱和度和亮度,计算为:

    h 的值通常规范化到位于 0 到 360°之间。而 h = 0 用于 max = min 的(就是灰色)时候而不是留下 h 未定义。
    hsl 和 hsv 有同样的色相定义,但是其他分量不同。hsv 颜色的 s 和 v 的值定义如下:

    从 hsl 到 rgb 的转换

    给定 hsl 空间中的 (h, s, l) 值定义的一个颜色,带有 h 在指示色相角度的值域 [0, 360)中,分别表示饱和度和亮度的s 和 l 在值域 [0, 1] 中,相应在 rgb 空间中的 (r, g, b) 三原色,带有分别对应于红色、绿色和蓝色的 r, g 和 b 也在值域 [0, 1] 中,它们可计算为:
    首先,如果 s = 0,则结果的颜色是非彩色的、或灰色的。在这个特殊情况,r, g 和 b 都等于 l。注意 h 的值在这种情况下是未定义的。
    当 s ≠ 0 的时候,可以使用下列过程:

    对于每个颜色向量 color = (colorr, colorg, colorb) = (r, g, b),

    从 hsv 到 rgb 的转换

    类似的,给定在 hsv 中 (h, s, v) 值定义的一个颜色,带有如上的 h,和分别表示饱和度和明度的 s 和 v 变化于 0 到 1 之间,在 rgb 空间中对应的 (r, g, b) 三原色可以计算为:

    对于每个颜色向量 (r, g, b),

    <html>
      <style>
        .childdiv {
          display:inline-block;  
          vertical-align:middle;
          margin-left: 30px;
          margin-top: 10px;
          margin-bottom: 10px;
        }
        #colorpickdiv {
          background-color: whitesmoke;
          border: 1px solid lightgrey;
          padding-top: 20px;
          padding-bottom: 20px;
          padding-right: 30px;
        }
        #huetipdiv {
          margin: 0 0 10px 70px;
        }
        #luminancetipdiv {
          margin-top: 20px
        }
        #colordiv {
          width: 100px;
          height: 100px;
          background-color: black;
        }
        #valuediv {
          box-shadow: 0px -5px 10px lightgrey;
          background-color: whitesmoke;
          border: 1px solid lightgrey;
          border-top-width: 0;
          padding-right: 10px;
          padding-bottom: 10px;
        }
      </style>
      <body>
        <div>
          <div id="colorpickdiv">
            <div class="childdiv">
              <div id="huetipdiv">hue:0</div>
              <canvas id="canvas" width="200" height="200">your browser does not support canvas</canvas>
            </div>
            <div class="childdiv">
              <div id="saturationtipdiv" class="divmarginbottom">saturation:0%</div>
              <input id="saturationrange" onchange="onhslrangechange()" type="range" min="0" max="100" step="1" value="100"/>
              <div id="luminancetipdiv" class="divmarginbottom">luminance:0%</div>
              <input id="luminancerange" onchange="onhslrangechange()" type="range" min="0" max="100" step="1" value="50"/>
            </div>
            <div id="colordiv" class="childdiv"></div>
          </div>
          <div id="valuediv">
            <div class="childdiv">
              <div id="hexadecimaltipdiv" class="divmarginbottom">hexadecimal:</div>
              <input id="hexadecimalvaluediv" type="text" disabled="disabled"/>
            </div>
            <div class="childdiv">
              <div id="rgbtipdiv" class="divmarginbottom">rgb:</div>
              <input id="rgbvaluediv" type="text" readonly="readonly"/>
            </div>
            <div class="childdiv">
              <div id="hsltipdiv" class="divmarginbottom">hsl:</div>
              <input id="hslvaluediv" type="text" readonly="readonly"/>
            </div>
          </div>
        </div>
        <script>
          var c = document.getelementbyid("canvas");
          var ctx = c.getcontext("2d");
          var colordiv = document.getelementbyid("colordiv");
          var hexadecimalvaluediv = document.getelementbyid("hexadecimalvaluediv");
          var rgbvaluediv = document.getelementbyid("rgbvaluediv");
          var hslvaluediv = document.getelementbyid("hslvaluediv");
          var hexadecimaltipdiv = document.getelementbyid("hexadecimaltipdiv");
          var saturationtipdiv = document.getelementbyid("saturationtipdiv");
          var saturationrange = document.getelementbyid("saturationrange");
          var luminancetipdiv = document.getelementbyid("luminancetipdiv");
          var luminancerange = document.getelementbyid("luminancerange");
    
          //十字光标颜色
          var crosscursorcolor = "black";
          //十字光标线宽
          var crosscursorlinewidth = 2;
          //十字光标某一边线段长
          var crosscursorhalflinelen = 5;
          //十字光标中间断裂处长度
          var crosscursorhalfbreaklinelen = 2;
    
          //画布中心点x坐标
          var centerx = c.width / 2;
          //画布中心点y坐标
          var centery = c.height / 2;
          //缩放绘制比例
          var scalerate = 10;
          //画布的内切圆半径(之所以减去一个数是为了可以显示完整的十字光标)
          var innerradius = math.min(centerx, centery) - crosscursorhalflinelen - crosscursorhalfbreaklinelen;
          //内切圆半径的平方
          var pow2innerradius = math.pow(innerradius, 2);
          //缩放绘制时的绘制半径,即画布的外径除以缩放比例
          var scaledradius = math.sqrt(math.pow(c.width / 2, 2) + math.pow(c.height / 2, 2)) / scalerate;
          //由于该圆是由绕圆心的多条线段组成,该值表示将圆分割的份数
          var count = 360;
          //一整个圆的弧度值
          var doublepi = math.pi * 2;
          //由于圆心处是多条线段的交汇点,composite是source-over模式,所以后绘制的线段会覆盖前一个线段。另外由于采用线段模拟圆,英雌
          var deprecatedradius = innerradius * 0.3;
          //废弃圆半径的平方
          var pow2deprecatedradius = math.pow(deprecatedradius, 2);
    
          //色相(0-360)
          var hue;
          //饱和度(0%-100%)
          var saturation; 
          //亮度luminance或明度lightness(0%-100%)
          var luminance;
    
          //当前色相位置x坐标
          var currenthueposx = centerx + innerradius - 1;
          //当前色相位置y坐标
          var currenthueposy = centery;
    
          //填充圆
          function fillcircle(cx, cy, r, color) {
            ctx.fillstyle = color;
            ctx.beginpath();
            ctx.arc(cx, cy, r, 0, doublepi);
            ctx.fill();
          }
    
          //绘制线条
          function strokeline(x1, y1, x2, y2) {
            ctx.beginpath();
            ctx.moveto(x1, y1);
            ctx.lineto(x2, y2);
            ctx.stroke();
          }
    
          //将整数转为16进制,至少保留2位
          function tohexstring(intvalue) {
            var str = intvalue.tostring(16);
            if(str.length == 1) {
              str = "0" + str;
            }
            return str;
          }
    
          //判断坐标(x,y)是否在合法的区域内
          function isinvalidrange(x, y) {
            var pow2distance = math.pow(x-centerx, 2) + math.pow(y-centery, 2);
            return pow2distance >= pow2deprecatedradius && pow2distance <= pow2innerradius;
          }
    
          //绘制十字光标
          function strokecrosscursor(x, y) {
            ctx.globalcompositeoperation = "source-over";
            ctx.strokecolor = crosscursorcolor;
            ctx.linewidth = crosscursorlinewidth;
            strokeline(x, y-crosscursorhalfbreaklinelen, x, y-crosscursorhalfbreaklinelen-crosscursorhalflinelen);
            strokeline(x, y+crosscursorhalfbreaklinelen, x, y+crosscursorhalfbreaklinelen+crosscursorhalflinelen);
            strokeline(x-crosscursorhalfbreaklinelen, y, x-crosscursorhalfbreaklinelen-crosscursorhalflinelen, y);
            strokeline(x+crosscursorhalfbreaklinelen, y, x+crosscursorhalfbreaklinelen+crosscursorhalflinelen, y);
          }
    
          //将对象中的hsl分量组成一个hsl颜色(h在0到360之间,s与l均在0到1之间)
          function formhslcolor(obj) {
            return "hsl(" + obj.h + "," + math.round(obj.s * 1000)/10 + "%," + math.round(obj.l * 1000)/10 + "%)"; 
          }
    
          //将对象中的rgb分量组成一个rgb颜色(r,g,b在0到255之间)
          function formrgbcolor(obj) {
            return "rgb(" + [obj.r, obj.g, obj.b].join(",") + ")";
          }
    
          //从画布的某点获取存储rgb的对象
          function getrgbobj(x, y) {
            var w = 1;
            var h = 1;
            var imgdata = ctx.getimagedata(x,y,w,h);
            var obj = {
              r: imgdata.data[0],
              g: imgdata.data[1],
              b: imgdata.data[2],
              a: imgdata.data[3]
            }
            return obj;
          }
    
          //将rgb转换为hsl对象()
          function rgbtohslobj(r, g, b) {
            r /= 255;
            g /= 255;
            b /= 255;
            var max = math.max(r, g, b);
            var min = math.min(r, g, b);
            var diff = max - min;
            var twovalue = max + min;
            var obj = {h:0, s:0, l:0};
            if(max == min) {
              obj.h = 0;
            } else if(max == r && g >= b) {
              obj.h = 60 * (g - b) / diff;
            } else if(max == r && g < b) {
              obj.h = 60 * (g - b) / diff + 360;
            } else if(max == g) {
              obj.h = 60 * (b - r) / diff + 120;
            } else if(max == b) {
              obj.h = 60 * (r - g) / diff + 240;
            }
            obj.l = twovalue / 2;
            if(obj.l == 0 || max == min) {
              obj.s = 0;
            } else if(0 < obj.l && obj.l <= 0.5) {
              obj.s = diff / twovalue;
              //obj.s = diff / (2 * obj.l);
            } else {
              obj.s = diff / (2 - twovalue);
              //obj.s = diff / (2 - 2 * obj.l);
            }
            obj.h = math.round(obj.h);
            return obj;
          }
    
          //创建hue颜色圆环
          function createhuering() {
            ctx.globalcompositeoperation = "source-over";
            ctx.clearrect(0,0,c.width,c.height);
            ctx.save();
            //将绘制原点移动到画布中心
            ctx.translate(centerx, centery);
            //将画布放大相应比例,restore后,绘制内容会缩小
            ctx.scale(scalerate, scalerate);
            for(var i=0; i<count; i++) {
              var degree = i / count * 360;
              var radian = math.pi * degree / 180;
              var x = scaledradius * math.cos(radian);
              var y = scaledradius * math.sin(radian);
              ctx.linewidth=1;
              ctx.strokestyle = "hsl(" + degree +"," + saturation + "," + luminance + ")";
              ctx.beginpath();
              ctx.moveto(x, y);
              ctx.lineto(0,0);
              ctx.stroke();
            }
            ctx.restore();
            ctx.globalcompositeoperation = "destination-out";
            fillcircle(centerx, centery, deprecatedradius, "black");
    
            ctx.globalcompositeoperation = "destination-in";
            fillcircle(centerx, centery, innerradius, "black");
          }
    
          //点击canvas中的hue拾色圈
          function oncanvasclick() {
            var x = event.offsetx;
            var y = event.offsety;
            if(!isinvalidrange(x, y)) {
              return;
            }
            currenthueposx = x;
            currenthueposy = y;
            //创建hue背景圆环
            createhuering();
            setcolorvalue(x, y);
            strokecrosscursor(x, y);
          }
    
          function setcolorvalue(x, y) {
            //获取包含rgb的颜色对象
            var rgbobj = getrgbobj(x, y);
            var rgbcolor = formrgbcolor(rgbobj);
            colordiv.style.backgroundcolor = rgbcolor;
            rgbvaluediv.value = rgbcolor;
            var hex = "#" + tohexstring(rgbobj.r) + tohexstring(rgbobj.g) + tohexstring(rgbobj.b);
            hexadecimalvaluediv.value = hex;
    
            var hslobj = rgbtohslobj(rgbobj.r, rgbobj.g, rgbobj.b);
            hslvaluediv.value = formhslcolor(hslobj);
            huetipdiv.innerhtml = ("hue:" + hslobj.h);
          }
    
          function onhslrangechange() {
            //event.target.value;
            saturation = saturationrange.value + "%";
            luminance = luminancerange.value + "%";
            saturationtipdiv.innerhtml = ("saturation:" + saturation);
            luminancetipdiv.innerhtml = ("luminance:" + luminance);
            createhuering();
            setcolorvalue(currenthueposx, currenthueposy)
            strokecrosscursor(currenthueposx, currenthueposy);
          }
    
          function init() {
            c.addeventlistener("click", oncanvasclick);
            onhslrangechange();
          }
    
          init();
    
        </script>
      </body>
    </html>

    有几个缺陷:

    - 不能根据颜色值来设置hsl。
    - 由于hsl的值是根据从hue环形调色板中取出的rgb颜色值换算为hsl的,因此跟滑动条上的值可能会有出入(如果是5舍6入那就一样了)