1. 程式人生 > >JQ外掛案例-基於jquery和canvas的調色盤

JQ外掛案例-基於jquery和canvas的調色盤

最近在研究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之間的轉換公式