UI组件之色彩选择器

前言

今天来讲一下如何用javascript写一个色彩选择器。上一篇写到用JS实现RGB,HSL,HSV之间的相互转换,而那一篇写的代码呢,就是为写这个色彩选择器而准备的。

先放上效果图,代码是基于jquery的,所以要引用上哟。
GIF.gif
或者戳这里看效果,源码在这里

功能需求

  • 可以通过点击左侧色彩区域选择色彩,在右侧的输入框中会显示对应的色值,包括RGB,HSL和HSV的值
  • 可以通过调节右侧输入框的各数值,来查看对应的颜色
  • 可以通过在中间的色带上点击来确定大致的颜色区域
  • 提供设定颜色值的接口,指定具体颜色,左侧色区和右侧输入框显示对应数值
  • 提供获取颜色值的接口,可获取色彩选择器选中的颜色值,可返回CSS color、RGB、HSL、HSB四种格式的色值

总体思路

左边色彩区域

通过效果图,你可以试着多取色,看看数值的一些规律。

从HSV数值上来看(如有不了解,请戳上篇)。

  • 左边取色渐变色块的色相是一直保持不变的
  • 从左到右,饱和度是从低到高的趋势
  • 从上到下,色彩明度是从高到低的。
  • 即从左到右,S值从0%到100%,从上到下,V值从100%到0%。

从RGB模型角度来说

  • 点击最上边一排水平来看,会发现最左上角的数值永远都是rgb(255,255,255),即是白色(用上HSV数值也可解释,饱和度最低,明度最高,即是白色)。
  • 而最右上方的数值则与我们中间的色带上的颜色是一致的。即该色相下,饱和度最高,明度最高的颜色。
  • 从最左边垂直而下看,会发现RGB的值由(255,255,255)到(0,0,0)变化,即从白到黑。

    中间色带

  • 至于中间的色带,是用来选择色彩的色相的。从上到下,H值从0-360渐变。
  • 从HSV值来看,会发现S值一直为100%,V值也为100%;从上面即可解释这一原因。
  • 而对于HSL值,S值一直为100%,L值也为50%;这也是两者本身定义不同产生的。

知道这些之后,就大概知道如何下手了。

关键代码

HTML

额…代码写的比较粗糙,html和css都写的比较死,不灵活,对于写成一个真正的插件而言,还需要加入很多样式方面的考虑,笔者比较懒,这先这样吧。整体html如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<div class="colorpicker">
<div class="colorbox"> <!-- 左边渐变区块 -->
<canvas id="canvas" height="300" width="300"></canvas>
<div id="colorpicker-circle"></div>
</div>
<div class="colorslide" id="colorpicker-slide"> <!-- 中间的色带 -->
<div id="colorslide-circle"></div>
</div>
<div class="colorcontent">
<div class="pickerbox" id="pickercolor"></div> <!-- 取色后显示颜色的区块 -->
<hr>
<span>hex:</span><input type="text" id="hex"><!-- 显示颜色的hex值 -->
<hr>
<div class="rgbbox"><!-- 显示颜色的rgb值 -->
<ul>
<li>R<input type="number" name="R" id="colorpicker-rgbr" min="0" max="255"></li>
<li>G<input type="number" name="G" id="colorpicker-rgbg" min="0" max="255"></li>
<li>B<input type="number" name="B" id="colorpicker-rgbb" min="0" max="255"></li>
</ul>
</div>
<hr>
<div class="hslvbox"><!-- 显示颜色的hsl值 -->
<table class="hslbox">
<tr>
<td>H</td>
<td><input type="number" name="H" id="colorpicker-hslh" step="1" min="0" max="360"></td>
</tr>
<tr>
<td>S</td>
<td><input type="number" name="S" id="colorpicker-hsls" step="1" min="0" max="100">%</td>
</tr>
<tr>
<td>L</td>
<td><input type="number" name="L" id="colorpicker-hsll" step="1" min="0" max="100">%</td>
</tr>
</table>
<table class="hsvbox"><!-- 显示颜色的hsv值 -->
<tr>
<td>H</td>
<td><input type="number" name="HH" id="colorpicker-hsvh" step="1" min="0" max="360"></td>
</tr>
<tr>
<td>S</td>
<td><input type="number" name="SS" id="colorpicker-hsvs" step="1" min="0" max="100">%</td>
</tr>
<tr>
<td>V</td>
<td><input type="number" name="V" id="colorpicker-hsvv" step="1" min="0" max="100">%</td>
</tr>
</table>
</div>
</div>
</div>

新建colorPicker对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var colorPicker= function(id,config){
this.element=$("#"+id)[0];//左边取色区域的DOM对象
this.jqelement=$("#"+id);//左边取色区域的jq对象
this.slide=$("#colorpicker-slide")[0];//中间色带的DOM对象
this.jqslide=$("#colorpicker-slide");
this.width = this.element.width || 300;//左边取色区域的宽度
this.height = this.element.height ||300;//左边取色区域的高度
this.pickercircle=$("#colorpicker-circle");//左边取色小圆点
this.context = this.element.getContext("2d");
this._init();
}
colorPicker.prototype = {
_init:function(){
this.canvasX=0;//左边取色区域相对于canvas画布水平位置
this.canvasY=0;//左边取色区域相对于canvas画布垂直位置
this.hsl=[0,100,50];//初始化hsl
this.rgb=[255,0,0];//初始化rgb
this.hsv=[0,100,100];//初始化hsv
this.hex="#ffffff";//初始化hex
this._drawbg();//绘制左边取色渐变区域的函数
this._pickcolor();//在左边点击取色的函数
this._slidecolor();//在中间色块点击取色相的函数
this._updatergb();//更新rgb,hex的数值
this._updatehslv();//更新hsl,hsv的数值
this._contentchange();//监听表单的输入,并作出相应的更新
}
}

色彩转换函数

不多说了,看上一篇或者源码

  • rgbtohsl(r,g,b)
  • rgbtohsv(r,g,b)
  • rgbtohex(r,g,b)
  • hsltorgb(h,s,l)
  • hsvtorgb(h,s,v)
  • hextorgb(hex)

绘制左边取色渐变区块

左边的渐变区块我是采用canvas绘图的方式来绘制渐变的效果。用canvas有什么好处呢,canvas可以确定某个画布位置的颜色,返回该点的rgb颜色,这样省事不少。

【注意】这里绘制渐变时要注意需要绘制两个渐变,一个是水平的渐变,一个是垂直的渐变,垂直的渐变是对rgb(0,0,0)的透明度进行改变,一定是透明的改变,因为只有这样,才能叠加上颜色,否则便是灰色的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//填充左边颜色的渐变
_drawbg:function(color){
var x1 = 0;
var y1 = 0;
//水平的渐变
var linear_gradient_hori = this.context.createLinearGradient(x1, y1, this.width, 0);
linear_gradient_hori.addColorStop(0, 'rgba(255,255,255,1)');
linear_gradient_hori.addColorStop(1,color?color:'rgba(255,0,0,1)');//添加上颜色
this.context.fillStyle=linear_gradient_hori;
this.context.fillRect(0, 0, this.width, this.height);
//垂直的渐变
var linear_gradient_ver = this.context.createLinearGradient(x1, y1, 0, this.height);
linear_gradient_ver.addColorStop(0, 'rgba(0,0,0,0)');
linear_gradient_ver.addColorStop(1,'rgba(0,0,0,1)');
this.context.fillStyle=linear_gradient_ver;
this.context.fillRect(0, 0, this.width, this.height);
}

在前期看别人写的插件时,发现很多都是用了一个这样一个灰色的渐变的透明背景图片,然后在父级的div设置一个背景颜色,该背景颜色就是来自中间色带,两者叠加来形成渐变区域,其实细细想来与上面用的原理都是一样的。
colorpicker_overlay.png
这里是别人写的一个色彩选择器插件,它就是用的这个方法。

绘制中间色带

用css的渐变来绘制,这里方法有很多,就不多说了。

1
2
3
4
5
6
7
.colorslide{
height: 300px;
width: 28px;
position: relative;
background: linear-gradient(to bottom, hsla(0,100%,50%,1) 10%,hsla(36,100%,50%,1) 20%,hsla(72,100%,50%,1) 30%,hsla(108,100%,50%,1) 40%,hsla(180,100%,50%,1) 50%,hsla(216,100%,50%,1) 60%,hsla(252,100%,50%,1) 70%,hsla(288,100%,50%,1) 80%,hsla(324,100%,50%,1) 90%, hsla(360,100%,50%,1) 100%);
overflow: hidden;
}

点击左边区域取色

在左边取色区域点击之后,首先获取点击的位置,通过canvas的getImageData方法获得该位置的rgb颜色,进行转换等到相应hex,hsl,hsv的值,更新表达的值,把取色小圆圈的位置设置到该点位置上去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_pickcolor:function(){
var that=this;
that.jqelement.bind("click",function(e){
var canvasrect=that.element.getBoundingClientRect();
that.canvasX = e.clientX-canvasrect.left*(that.width/canvasrect.width);//获取点击的位置
that.canvasY = e.clientY-canvasrect.top*(that.height/canvasrect.height);
var imageData = that.context.getImageData(that.canvasX, that.canvasY, 1, 1);//用canvas的getImageData方法来获得该处的颜色
var pixel = imageData.data;
that.rgb=pixel.slice(0,3);
that.hex=rgbtohex(pixel[0],pixel[1],pixel[2]);
that.hsl=rgbtohsl(pixel[0],pixel[1],pixel[2]);
that.hsv=rgbtohsv(pixel[0],pixel[1],pixel[2]);
that._updatergb();
that._updatehslv();
that.pickercircle.show();
that.pickercircle.css({"top":that.canvasY-5,"left":that.canvasX-5});//设置取色小圆圈的位置。-5是因为是圆形,需要减去宽高和边框;
})
}

中间色带点击取色相

点击之后,由于中间色带的色相是递增的,获取取色相小圆圈相对于整个中间色带的高度即可获取色相,此时需要重新绘制左边的渐变区域,由于这里只变化了h值,即可知hsl值,根据hsl值获得其他格式的颜色值更新即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_slidecolor:function(){
var that=this;
that.jqslide.bind("click",function(e){
if(e.target.id=="colorpicker-slide"){
$("#colorslide-circle").css({"top":e.offsetY-8});//减去圆圈本身的高度和边框;
that.hsl[0]=that.hsv[0]=Math.round(e.offsetY/that.height*360);//在这里选择只是变化了h值
var hsl=that.hsl[0]+",100%,50%,1";
that._drawbg("hsla("+hsl+")");//重新绘制左边的渐变图;
that.rgb=hsltorgb(that.hsl[0],that.hsl[1],that.hsl[2]);
that._updatergb();
$("#colorpicker-hslh").val(that.hsl[0]);
$("#colorpicker-hsvh").val(that.hsv[0]);
}
})
}

更新颜色表单的数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//更新rgb ,hex 和颜色块数据
_updatergb:function(){
$("#colorpicker-rgbr").val(this.rgb[0]);
$("#colorpicker-rgbg").val(this.rgb[1]);
$("#colorpicker-rgbb").val(this.rgb[2]);
this.hex=rgbtohex(this.rgb[0],this.rgb[1],this.rgb[2]);
$("#hex").val(this.hex);
$("#pickercolor").css({"background-color":this.hex});
}
//更新hsl和hsv的数据
_updatehslv:function(){
this.hsv=rgbtohsv(this.rgb[0],this.rgb[1],this.rgb[2]);
$("#colorpicker-hsvh").val(this.hsv[0]);
$("#colorpicker-hsvs").val(this.hsv[1]);
$("#colorpicker-hsvv").val(this.hsv[2]);
$("#colorpicker-hslh").val(this.hsl[0]);
$("#colorpicker-hsls").val(this.hsl[1]);
$("#colorpicker-hsll").val(this.hsl[2]);
}

监听表单的数值变化

这里提一下,改变了某一表单数值后,其他格式的颜色数值相对应改变,还需要改变左边区域取色小圆圈的位置,让其跳转到对一个颜色的像素上,中间色带的取色相小圆圈也需要转到对应的色相颜色上。
思路是:表单改变后,得到所有格式的颜色数值,根据h值来使得中间色带的取色相小圆圈变化到对应的色相颜色上。而根据hsv中的s和v可以获取左边区域取色小圆圈应该在位置(渐变的原理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
//监听表单数据变化并做更新
_contentchange(){
var that=this;
$("#hex").bind("change",function(e){
// $("#hex").change=function(){
var hex=$("#hex").val();
var rgb;
if(rgb=hextorgb(hex)){
that.hex=hex;
that.rgb=rgb;
that.hsl=rgbtohsl(rgb[0],rgb[1],rgb[2]);
that.hsv=rgbtohsv(that.rgb[0],that.rgb[1],that.rgb[2]);
that._updatechange();
}
})
$(".rgbbox input").each(function(index,obj){
$(obj).bind("change",function(e){
var value=parseInt($(obj).val());
if(value>0 && value<=255){
that.rgb[index]=value;
that.hex=rgbtohex(that.rgb[0],that.rgb[1],that.rgb[2]);
that.hsl=rgbtohsl(that.rgb[0],that.rgb[1],that.rgb[2]);
that.hsv=rgbtohsv(that.rgb[0],that.rgb[1],that.rgb[2]);
that._updatechange();
}
})
})
$(".hslbox input").each(function(index,obj){
$(obj).bind("change",function(e){
var value=parseInt($(obj).val());
if((index==0 && value<=360 && value>=0)||(value<=100 && value>=0)){
that.hsl[index]=value;
that.rgb=hsltorgb(that.hsl[0],that.hsl[1],that.hsl[2]);
that.hsv=rgbtohsv(that.rgb[0],that.rgb[1],that.rgb[2]);
that.hex=rgbtohex(that.rgb[0],that.rgb[1],that.rgb[2]);
that._updatechange();
}
})
})
$(".hsvbox input").each(function(index,obj){
$(obj).bind("change",function(e){
var value=parseInt($(obj).val());
if((index==0 && value<=360 && value>=0)||(value<=100 && value>=0)){
that.hsv[index]=value;
that.rgb=hsvtorgb(that.hsv[0],that.hsv[1],that.hsv[2]);
that.hsl=rgbtohsl(that.rgb[0],that.rgb[1],that.rgb[2]);
that.hex=rgbtohex(that.rgb[0],that.rgb[1],that.rgb[2]);
that._updatechange();
}
})
})
},
// 更新表单输入而产生的变化
_updatechange(){
this._updatergb();
this._updatehslv();
var offestheight=this.hsl[0]/360*this.height;
//改变颜色条选择圆圈的位置
$("#colorslide-circle").show();
this.pickercircle.show();
$("#colorslide-circle").css({"top":offestheight-8});
var hsl=this.hsl[0]+",100%,50%,1";
this._drawbg("hsla("+hsl+")");//重新绘制左边的渐变图;
// 根据hsb来确定渐变区域选色圆圈的位置
this.hsv=rgbtohsv(this.rgb[0],this.rgb[1],this.rgb[2]);
var canvaswidth=this.hsv[1]/100*this.width;
var canvasheight=(1-this.hsv[2]/100)*this.height;
this.pickercircle.css({"top":canvasheight-5,"left":canvaswidth-5});//-5是因为是圆形,需要减去宽高和边框;
}
};

代码写的有点乱,还有很多需要改进的地方,暂时就先这样吧。