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) | - | - |
想清楚之後實現起來其實挺簡單:
/**
* 產生色環
* @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:
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
-------------------------
參考文獻: