JQ插件案例-基于jquery和canvas的调色板

版权声明:本文为sam的原创文章,转载请添加出处:http://blog.csdn.net/samed https://blog.csdn.net/samed/article/details/51385872

最近在研究canvas,想要弄一个canvas的所见所得工具。

在研究的过程中,猛然发现调色板不太好实现。

通过多方面研究及翻阅文献,发现网上对于调色板的实现大都是产生一个色块列表而已。


这种方式丑爆了好吧,而且选颜色麻烦死了,绿色还分那么多个块,怎么能好好选到自己心仪的颜色呢?

论插件来说的话,有一个插件还不错,基本和Photoshop的调色板差不多:

官网:spectrum


这款调色板还算比较符合我个人喜好,而且demo显示的功能也非常不错。

我没有下载,也没有去仔细研究它的实现方式,粗看了一下不是使用canvas的。

可是,这种UI并不是我心目中的best。

我想要达到的效果是类似painter里面的调色板UI:


这个不但简洁,而且色环的表达方式非常符合现实色彩的展现。



最终效果:



实现思路:

一、 画色环,最难的部分,想了很多种办法啦,最后还是通过基于像素画曲线产生。

二、 画方形灰度色块,矩形容易画,难在渐变的算法,没有找到文献可以研究。

三、 画透明度滑动条,这个简单。

四、 画预览窗,最简单。


具体实现:

一、 画色环。

首先,canvas 画圆的话,首选 arc 方法,不过渐变填充却只能线性或者径向,没有办法沿着路径渐变。

所以这里不能使用渐变进行色彩填充,需要基于像素画出一段段曲线并着色。

画完一圈之后半径减少 1px 之后(实际项目中是0,5px),画第二圈,直到预期的内径大小。

这里重点要搞清楚着实过程色彩的变化规律(算法)。

预览 web rgb 过程序号 属性变化
  #FF0000 (255,0,0) 1 g++
  #FFFF00 (255,255,0) 2 r--
  #00FF00 (0,255,0) 3 b++
  #00FFFF (0,255,255) 4 g--
  #0000FF (0,0,255) 5 r++
  #FF00FF (255,0,255) 6 b--
  #FF0000 (255,0,0) - -
上面这个表格的意思是色彩从 #F00 变化到 #F00 ,7种颜色中间有6个变化过程,每个变化过程中需要变化的值按照最后一列所展示。

想清楚之后实现起来其实挺简单:

    /**
    * 产生色环
    * @params: ctx canvas_context 已经初始化后的 canvas context
    * @params: x float 圆心 x 坐标
    * @params: y float 圆心 y 坐标
    * @params: outterRadius float 圆的外径
    * @params: innerRadius float 圆的内径
    * @params: wearProof float 细腻度(>0,越小越细腻)
    * @returs: false
    */
    var colorRing = function(ctx, x, y, outterRadius, innerRadius, wearProof){
        for (var i = outterRadius; i >= innerRadius; i-=wearProof) {
            var r=255,g=0,b=0,flag=1;    // rgb 对应红绿蓝三色的数值, flag 指色彩渐变过程序号
            for (var j = 0; j < Math.PI*2; j+=Math.PI/720) {
                ctx.strokeStyle = 'rgb('+r+','+g+','+b+')';
                ctx.beginPath();
                ctx.arc(x,y,i,j,j+Math.PI/720,false);
                ctx.stroke();
                // 变化规则
                switch(flag){
                    case 1:
                        if(g>=255){g=255;r=254;flag=2;break;}
                        g++;break;
                    case 2:
                        if(r<=0){r=0;b=1;flag=3;break;}
                        r--;break;
                    case 3:
                        if(b>=255){b=255;g=254;flag=4;break;}
                        b++;break;
                    case 4:
                        if(g<=0){g=0;r=1;flag=5;break;}
                        g--;break;
                    case 5:
                        if(r>=255){r=255;b=254;flag=6;break;}
                        r++;break;
                    case 6:
                        if(b<=0){flag=null;break;}
                        b--;break;
                    default:break;
                }
            };
        };
        return false;
    }


图我就不截了,就是预览图的色环(下同)。

P.S.:这里的函数我还没有封装起来,我打算封装成JQ插件,所以我的项目最终的代码会稍微有点区别(下同)。


二、 画方形灰度色块。

这个姑且称为灰度色块啦,其实是颜色的微调,因为色彩是rgb三维的,仅仅有二维的东西无法表达,所以需要表达第三维的变化。

这个色块由于三个顶点的颜色值是不同的:

左上角固定白色,右上角固定为当前选择的颜色,左下和右下固定为黑色。

所以应该是一个渐变的过程,但是如何渐变呢?着实困难。

打开Photoshop,一个个像素点地研究,发现左侧边缘和右侧边缘都是递减的变化,而横向的变化规律不明显。

如下图(以#F00色彩为例):


所以,算法就是水平方向上做渐变(lineargradient),垂直方向做等分分割。

    /**
    * 产生中间方形灰度选择块
    * @params: ctx canvas_context 已经初始化后的 canvas context
    * @params: x float 左上顶点 x 坐标
    * @params: y float 左上顶点 y 坐标
    * @params: w float 色块的宽
    * @params: h float 色块的高
    * @params: baseColor string/dict 定义基准色(右上角的色彩),接受一个色彩字符串或者含有 R/G/B 元素的字典
    * @returs: false
    */
    var colorPalatte = function(ctx, x, y, w, h, baseColor){
        var r,g,b;
        var unitI = h/255;
        baseColor = colorStringToRGB(baseColor);    // 处理字符串类型的色彩,转化为 {R:#,G:#,B:#}
        if(!baseColor)
            return false;
        for (var i = 0; i < h; i+=unitI) {
            var lg6 = ctx.createLinearGradient(x,y,x+w,y);
            r=g=b=Math.floor(255-i*255/h);    // 左侧边缘色彩
            lg6.addColorStop(0,'rgb('+r+','+g+','+b+')');
            r=baseColor.R-i*255/h;        // 右侧边缘色彩
            g=baseColor.G-i*255/h;        // 因为i被等分了,
            b=baseColor.B-i*255/h;        // 所以需要反转单位
            r=r<0?0:r;g=g<0?0:g;b=b<0?0:b;    // 保证不能小于0,因为是减法,所以也不可能大于 255
            r=Math.floor(r);g=Math.floor(g);b=Math.floor(b);    //rgb 函数只接受整数
            lg6.addColorStop(1,'rgb('+r+','+g+','+b+')');
            ctx.strokeStyle = lg6;
            ctx.beginPath();
            ctx.moveTo(x,y+i);
            ctx.lineTo(x+w,y+i);
            ctx.stroke();
        };
        return false;
    }



三、 画透明度滑动条。

其实就是画一个渐变条罢了,不多说。

不过为了好看,加上方格背景能更好地表示“透明”这个概念。

    /**
    * 产生透明度滑动条
    * @params: ctx canvas_context 已经初始化后的 canvas context
    * @params: x float 左上顶点 x 坐标
    * @params: y float 左上顶点 y 坐标
    * @params: w float 滑动条的宽
    * @params: h float 滑动条的高
    * @params: baseColor string/dict 定义基准色(右侧的色彩),接受一个色彩字符串或者含有 R/G/B 元素的字典
    * @returs: false
    */
    var colorSlider = function(ctx, x, y, w, h, baseColor){
        baseColor = colorStringToRGB(baseColor);    // 处理字符串类型的色彩,转化为 {R:#,G:#,B:#}
        if(!baseColor)
            return false;
        // 画背景透明方格
        ctx.fillStyle = 'rgba(0,0,0,0.3)';
        var _halfH = Math.floor(h/2),_gridCnt = Math.floor(w/_halfH);
        for (var i = 0; i < _gridCnt; i+=2) {
            if( (x+i*_halfH) < (x+w) )
                ctx.fillRect(x+i*_halfH,y,_halfH,_halfH);
            if( (x+(i+1)*_halfH) < (x+w) )
                ctx.fillRect(x+(i+1)*_halfH,y+_halfH,_halfH,_halfH);
        };
        // 产生透明条
        var lg6 = ctx.createLinearGradient(x,y,w,y);
        lg6.addColorStop(0,'rgba('+baseColor.R+','+baseColor.G+','+baseColor.B+',0)');
        lg6.addColorStop(1,'rgba('+baseColor.R+','+baseColor.G+','+baseColor.B+',1)');
        ctx.fillStyle = lg6;
        ctx.strokeStyle = '#000000';
        ctx.fillRect(x,y,w,h);
        ctx.strokeRect(x,y,w,h);
        return false;
    }

四、 画预览窗。

这个不多说了,自己体会一下。

    /**
    * 产生预览
    * @params: ctx canvas_context 已经初始化后的 canvas context
    * @params: x float 左上顶点 x 坐标
    * @params: y float 左上顶点 y 坐标
    * @params: w float 预览的宽
    * @params: h float 预览的高
    * @params: currentColor string/dict 定义当前颜色,接受一个色彩字符串或者含有 R/G/B/A 元素的字典
    * @params: newColor string/dict 定义新选择的颜色,接受一个色彩字符串或者含有 R/G/B/A 元素的字典
    * @returs: false
    */
    var colorPreview = function(ctx, x, y, w, h, currentColor, newColor){
        currentColor = colorStringToRGB(currentColor);    // 处理字符串类型的色彩,转化为 {R:#,G:#,B:#}
        if(!currentColor)
            return false;
        newColor = colorStringToRGB(newColor);    // 处理字符串类型的色彩,转化为 {R:#,G:#,B:#}
        if(!newColor)
            return false;
        
        // 产生预览(当前颜色)
        ctx.fillStyle = 'rgba('+currentColor.R+','+currentColor.G+','+currentColor.B+','+(currentColor.A?currentColor.A:1)+')';
        ctx.fillRect(x,y,w/2,h);
        
        // 产生预览(新颜色)
        ctx.fillStyle = 'rgba('+newColor.R+','+newColor.G+','+newColor.B+','+(newColor.A?newColor.A:1)+')';
        ctx.fillRect(x+w/2,y,w/2,h);

        // 边框
        ctx.strokeStyle = '#000000';
        ctx.strokeRect(x,y,w,h);
        return false;
    }

对了,这中间还用到一个自定义函数:colorStringToRGB:

    /**
    * 处理字符串类型的色彩,转化为 {R:#,G:#,B:#}
    * @params: baseColor string 十六进制色彩字符串
    * @returs: {R:#,G:#,B:#} dict #为对应的十进制数值
    */
    var colorStringToRGB = function(baseColor){
        if( typeof baseColor === 'string' ){
            // 形如 #FF0000 的色彩字符串
            baseColor = baseColor.replace('#','');
            if(baseColor.length != 3 && baseColor.length != 6){
                console.log('Error color string format');
                return null;
            }
            if(baseColor.length == 3){
                var tmpArr = baseColor.split('');
                baseColor = '';
                for (var i = 0; i < tmpArr.length; i++) {
                    baseColor += tmpArr[i]+tmpArr[i];
                };
            }
            baseColor = {
                R: parseInt(baseColor.slice(0,2), 16),
                G: parseInt(baseColor.slice(2,4), 16),
                B: parseInt(baseColor.slice(4,6), 16),
            }
        }
        return baseColor;
    }


以上就是使用canvas画出调色板的实现方法。

要使用的话,还需要一些基本的参数传入,下面是demo:

colorPalatte_demo

P.S.:本人是JQ狗,基本上都会用jqery做东西,所以这个是打算做成JQ插件的,目前demo已经去除对JQ的依赖,后续源码可能会加入JQ,注意了。



=================================================================

2016-05-18 更新

在写插件的过程中,发现获取颜色很困难,如果使用canvas自带的getImageData来获取的颜色点很不精准,圆心偏差在3°左右。

然后就去翻资料,要通过计算出来才行。

偶然发现HSB的资料(参考文献1、参考文献2),才发现原来外面的圈圈其实就是HSB中的H参数,术语叫做“色相环”。

有了这个资料就好办多了。

重新定义取色的逻辑,并且所有涉及颜色的地方采用计算的方式取数。

最后经过测试,如果手工输入颜色值,大约会有0.01度的偏差,这个肉眼是看不出来的,而且也不会在结果里面体现。


说了那么多,RGB和HSB(因为这里有两个B,所有HSB下文有HSV表示,是一个意思。)的转换方式如下:

RGB --> HSV:

其中:max为RGB颜色中三个分量数值最大的那个;min就是最小的那个。

r/g/b三个字母就是对应RGB颜色中三个分量



HSV -->RGB

解释一下,下面的 hi 其实就是 h/60 的整数部分(向下取整),f 就是 h/60 的小数部分。其它都很好理解。



按照上述公式,可以写出基于 javascript 的代码如下:

RGB --> HSV:(以下函数已经使用在实际的案例中了)

/**
* 处理字符串类型的色彩,转化为 {R:#,G:#,B:#}
* @params: color string 十六进制色彩字符串
* @returs: {R:#,G:#,B:#} dict #为对应的十进制数值
*/
_colorStringToRGB = function(color){
    var oriColor = color;
    if( typeof color === 'string' && color.charAt(0) === '#' ){
        // 形如 #FF0000 的色彩字符串
        color = color.replace('#','');
        if(color.length != 3 && color.length != 6){
            console.error('Error HEX color string: '+oriColor);
            return null;
        };
        if(color.length == 3){
            var tmpArr = color.split('');
            color = '';
            for (var i = 0; i < tmpArr.length; i++) {
                color += tmpArr[i]+tmpArr[i];
            };
        };
        color = {
            R: parseInt(color.slice(0,2), 16),
            G: parseInt(color.slice(2,4), 16),
            B: parseInt(color.slice(4,6), 16),
        };
    }else if( typeof color === 'string' && color.slice(0, 3).toLowerCase() === 'rgb' ){
        // 形如 rgb() / rgba()
        var matchArr = color.match(/rgba?\( *(\d+) *, *(\d+) *, *(\d+) *(?:, *(1|0\.\d+) *)?\)/i);
        if(!matchArr)
            return null;
        color = {
            R: matchArr[1]*1,
            G: matchArr[2]*1,
            B: matchArr[3]*1,
        };
        if(matchArr[4] !== undefined)
            color.A = matchArr[4]*1;
    };
    return color;
};
/**
* 处理{R:#,G:#,B:#},转化为字符串类型的色彩
* @params: {R:#,G:#,B:#} dict #为对应的十进制数值
* @returs: Color string 十六进制色彩字符串
*/
_RGBToColorString = function(rgb){
    if( typeof rgb === 'object' && rgb.R !== undefined ){
        var r, g, b, colorString;
        // 形如 {R:#,G:#,B:#}
        r = (rgb.R).toString(16);
        r < 16 && (r = '0' + r);
        g = rgb.G.toString(16);
        g < 16 && (g = '0' + g);
        b = rgb.B.toString(16);
        b < 16 && (b = '0' + b);
        colorString = '#' + r + g + b;
        return colorString;
    };
    return rgb;
};
/**
* 处理{R:#,G:#,B:#}/colorString,转化为 {H:#,S:#,V:#} 色彩值
* @params: rgb dict/string
* @returs: {H:#,S:#,V:#} dict
*/
_RGBToHSV = function(rgb){
    var color
    if(typeof rgb == 'string' && rgb.charAt(0) == '#')
        color = _colorStringToRGB(rgb);
    else if(typeof rgb === 'object' && rgb.R !== undefined)
        color = rgb;
    else
        return undefined;

    var r = color.R, g = color.G, b = color.B;
    var max = r>g?(r>b?r:b):(g>b?g:b),
        min = r<g?(r<b?r:b):(g<b?g:b),
        h, s, v;
    // rgb --> hsv(hsb)
    if(max == min){
        h = 0;    // 定义里面应该是undefined的,不过为了简化运算,还是赋予0算了。
    }else if(max == r){
        h = 60*(g-b)/(max-min);
        if(g<b)
            h += 360;
    }else if(max == g){
        h = 60*(b-r)/(max-min)+120;
    }else if(max == b){
        h = 60*(r-g)/(max-min)+240;
    };
    if( max == 0)
        s = 0;
    else
        s = (max - min)/max;
    v = max;
    return {H: h,S: s,V: v};
}
HSV -->  R GB :(注意:这个函数我并没有测试过,仅仅按照公式进行书写,因为色相环采用直角坐标系,和H的定义还是有点区别的,所以我并没有用。)

/**
* 处理{H:#,S:#,V:#}/colorString,转化为 {R:#,G:#,B:#} 色彩值
* @params: hsv{H:#,S:#,V:#} dict
* @returs: rgb{R:#,G:#,B:#} dict
*/
_HSVToRGB = function(hsv){
    if(!(typeof hsv === 'object' && hsv.H !== undefined))
        return undefined;

    var h = hsv.H, s = hsv.S, v = hsv.V,
        r, g, b;
    var hi = Math.floor(h/60),
        f = h/60 - hi,
        p = v * (1 - s),
        q = v * (1 - f * s ),
        t = v * (1 - (1 - f) * s);
    switch(hi){
        case 0:r=v;g=t;b=p;break;
        case 1:r=q;g=v;b=p;break;
        case 2:r=p;g=v;b=t;break;
        case 3:r=p;g=q;b=v;break;
        case 4:r=t;g=p;b=v;break;
        case 5:r=v;g=p;b=q;break;
    }
    return {R: r,G: g,B: b};
}

在实际项目中,由于我并不知道HSV的值,而仅仅知道当前选取点的坐标(x, y),所以,采用HSV转RGB的算法并不可取,因此有了下面的直角坐标转RGB的代码片段:

/**
* 根据给出的坐标,计算色相环上的点的颜色
* @params: pos{x:#,y:#} dict
* @params: center{x:#,y:#} dict
* @returs: rgb{R:#,G:#,B:#} dict
*/
_posToRGB = function(pos, center){
    var newColor;
    // 计算色相环的值
    var x = pos.x, y = pos.y,    // 选色点的坐标(已经经过处理,此处相对于色相环所在矩形的左上角)
        b = x-center.x, a = y-center.y,  // a/b的位置看图, >0/=0/<0 均有可能
        alpha, r, g, b;    // alpha 是圆心角的弧度 的绝对值(方便起见,采用正数进行运算)
    // 处理 b 为0的情况(不能做除数)
    if(b === 0)
        alpha = Math.PI/2;
    else
        alpha = Math.abs(Math.atan(a/b));
    // 开始枚举
    if(a>=0 && b>0 && alpha<=Math.PI/3){
        r = 255;
        g = alpha*255*3/Math.PI;
        b = 0;
    }else if(a>0 && b>=0 && Math.PI/3<alpha){
        r = 255*2 - alpha*255*3/Math.PI;
        g = 255;
        b = 0;
    }else if(a>0 && b<0 && Math.PI/3<alpha){
        r = alpha*255*3/Math.PI - 255;
        g = 255;
        b = 0;
    }else if(a>=0 && b<0 && alpha<=Math.PI/3){
        r = 0;
        g = 255;
        b = 255 - alpha*255*3/Math.PI;
    }else if(a<0 && b<0 && alpha<=Math.PI/3){
        r = 0;
        g = 255 - alpha*255*3/Math.PI;
        b = 255;
    }else if(a<0 && b<0 && Math.PI/3<alpha){
        r = alpha*255*3/Math.PI - 255;
        g = 0;
        b = 255;
    }else if(a<0 && b>=0 && Math.PI/3<alpha){
        r = 255*2 - alpha*255*3/Math.PI;
        g = 0;
        b = 255;
    }else if(a<0 && b>0 && alpha<=Math.PI/3){
        r = 255;
        g = 0;
        b = alpha*255*3/Math.PI;
    }
    // 取整数--这个地方就是误差来源
    r=Math.floor(r);g=Math.floor(g);b=Math.floor(b);
    newColor = {R: r, G: g, B: b};
    return newColor;
}

这里面枚举的情况有点多,用图表示会比较好:



根据这个图形,然后按照之前画色环中的颜色变化规律,就可以得到上述代码了。

完整的项目当前是存放在git上面,有兴趣可以看看:

csdn code: colorPalatte

github: colorPalatte




-------------------------

参考文献:

1. 颜色的前世今生11·HSB拾色器详解

2. RGB与HSB之间的转换公式



猜你喜欢

转载自blog.csdn.net/samed/article/details/51385872