【canvas系列】用canvas實現一個colorpicker(相似PS的顏色選擇器)

每一個瀏覽器都有本身的特色,好比今天要作的colorpicker就是,一千個瀏覽器,一千個哈姆雷特,一千個colorpicker。今天canvas系列就用canvas作一個colorpicker。css

 **********************************************************************html

效果圖和demo

忽然翻到了以前用js和dom寫的一個colorpicker,比較挫,扔張圖就好(old)vue

這個真的很挫,性能不好,由於每個可選的顏色值都是一個dom,若是要實現256*256,那瀏覽器就爆了~~~~~css3

 

好,回到今天的demo(new)git

demo連接: https://win7killer.github.io/demo_set/html_demo/canvas/can_ps/color_picker.html
github

 

沒錯,就是照着PS的顏色選擇器的樣子仿的。chrome

**********************************************************************canvas

實現

 

首先咱們來看效果圖分析怎麼作:數組

1.左側colorbar

    左側提供一系列過渡色,不難看出,這個是「紅黃綠青藍紫」這六種顏色,而後加以過渡色處理來的。最後紫色還要過渡回到紅色。瀏覽器

 

另外換成環狀的可能更加好識別,以下圖:

那麼,咱們就能夠用canvas的過渡色來實現左側這個區域,

代碼以下:

 1 function colorBar() {
 2     var gradientBar = ctx.createLinearGradient(0, 0, 0, width);
 3     gradientBar.addColorStop(0, '#f00');
 4     gradientBar.addColorStop(1 / 6, '#f0f');
 5     gradientBar.addColorStop(2 / 6, '#00f');
 6     gradientBar.addColorStop(3 / 6, '#0ff');
 7     gradientBar.addColorStop(4 / 6, '#0f0');
 8     gradientBar.addColorStop(5 / 6, '#ff0');
 9     gradientBar.addColorStop(1, '#f00');
10 
11     ctx.fillStyle = gradientBar;
12     ctx.fillRect(0, 0, 20, width);
13 }

這裏涉及到canvas的fillStyle或者strokenStyle的填充對象,可使用過渡色對象(本身瞎叫的名字),瞭解更多能夠去w3cschool。

 

 2.中間顏色區

 

中間這塊乍看很簡單,再看有點蒙bi,三看才搞清楚怎麼搞。

乍看:其實就是左側選中的那個顏色(好比A),而後進行過渡處理,不仍是過渡麼。

再看:恩,顏色,而後黑色,白色,三種顏色三個角怎麼過渡~~~~(若是有快捷的過渡實現方式請留言告知我,THX)。

三看:那麼,拆解一下,好比紅色到白色過渡,而後加一層黑色到透明過渡?是滴,就是這麼個方案。(我本身以前彎路到了紅色到黑色,白色到透明)

那麼就是藉助兩次過渡色的填充,實現中間色塊區域。

代碼以下:

 1 function colorBox(color) {
 2     // 底色填充,也就是(舉例紅色)到白色
 3     var gradientBase = ctx.createLinearGradient(30, 0, width + 30, 0);
 4     gradientBase.addColorStop(1, color);
 5     gradientBase.addColorStop(0, 'rgba(255,255,255,1)');
 6     ctx.fillStyle = gradientBase;
 7     ctx.fillRect(30, 0, width, width);
 8     
 9     // 第二次填充,黑色到透明
10     var my_gradient1 = ctx.createLinearGradient(0, 0, 0, width);
11     my_gradient1.addColorStop(0, 'rgba(0,0,0,0)');
12     my_gradient1.addColorStop(1, 'rgba(0,0,0,1)');
13     ctx.fillStyle = my_gradient1;
14     ctx.fillRect(30, 0, width, width);
15 }

須要注意,第一次填充,是從橫向填充,這時候中間色塊的左邊已經不是canvas的原點,因此加了偏移量30px

第二次填充縱向,Y軸仍是0。

這個在實際應用中要注意。

 

到這裏,左側canvas繪製的東西就差很少了。

 

3. 顏色選擇事件處理

首先明確交互事件:

選擇左側colorbar(好比#ff0),中間base顏色要跟着變化,右上角也要是對應顏色(#ff0)【這個時候其實也能夠獲得選擇的顏色,能夠結束交互】;

選擇中間區域的顏色,左側不變,能夠獲取到對應的顏色值,結束交互。

最終就是在右側的dom區域展現所選到的顏色。

 

canvas中沒有dom對象,因此鼠標點擊事件要靠鼠標的位置來肯定是否進行相應處理。並且咱們繪製的不是path對象,也沒法使用inpath之類的方法來判斷。

點擊事件代碼:

 1 can.addEventListener('click', function(e) {
 2     var ePos = {
 3         x: e.offsetX || e.layerX,
 4         y: e.offsetY || e.layerY
 5     }
 6     var rgbaStr = '#000';
 7     if (ePos.x >= 0 && ePos.x < 20 && ePos.y >= 0 && ePos.y < width) {
 8         // in
 9         rgbaStr = getRgbaAtPoint(ePos, 'bar');
10         colorBox('rgba(' + rgbaStr + ')');
11     } else if (ePos.x >= 30 && ePos.x < 30 + width && ePos.y >= 0 && ePos.y < width) {
12         rgbaStr = getRgbaAtPoint(ePos, 'box');
13     } else {
14         return;
15     }
16     outColor(rgbaStr.slice(0, 3).join());
17     cur.style.left = ePos.x + 'px';
18     cur.style.top = ePos.y + 'px';
19     cur.style.outlineColor = (rgbaStr[0] > 256 / 2 || rgbaStr[1] > 256 / 2 || rgbaStr[2] > 256 / 2) ? '#000' : '#fff';
20 });

其中,getRgbaAtPoint是最終的獲取顏色值的方法,須要根據不一樣的鼠標位置傳參來決定選取左側仍是右側圖像

 

獲取顏色就比較簡單了,就是拿到對應區域的imageData,而後從顏色數組中獲取到對應位置的顏色值便可。

作過canvas像素處理的同窗會比較明白,不明白的建議先去把getImageData方法看一看,瞭解一下

獲取顏色代碼:

 1 function getRgbaAtPoint(pos, area) {
 2     if (area == 'bar') {
 3         var imgData = ctx.getImageData(0, 0, 20, width);
 4     } else {
 5         var imgData = ctx.getImageData(0, 0, can.width, can.height);
 6     }
 7 
 8     var data = imgData.data;
 9     var dataIndex = (pos.y * imgData.width + pos.x) * 4;
10     return [
11         data[dataIndex],
12         data[dataIndex + 1],
13         data[dataIndex + 2],
14         (data[dataIndex + 3] / 255).toFixed(2),
15     ];
16 }

這時候拿到的就是rgba顏色對應的值。

須要注意,最後一個數據時alpha通道,canvas的imageData裏是0-255【沒記錯的話】,而不是咱們日常用的0-1,因此要作轉換

 

顏色輸出&轉換:

拿到顏色後就能夠輸出到右側了。

右側只是用了rgb三通道,因此取數組前三位就好。

至於hex顏色,則用rgb來轉換。

轉換代碼以下:

 1 function rgb2hex(rgb) {
 2     var aRgb = rgb instanceof Array ? rgb : (rgb.split(',') || [0, 0, 0]);
 3     var temp;
 4     return [
 5         (temp = Number(aRgb[0]).toString(16)).length == 1 ? ('0' + temp) : temp,
 6         (temp = Number(aRgb[1]).toString(16)).length == 1 ? ('0' + temp) : temp,
 7         (temp = Number(aRgb[2]).toString(16)).length == 1 ? ('0' + temp) : temp,
 8     ].join('');
 9 }
10 
11 function hex2rgb(hex) {
12     if (hex.length == 3) {
13         hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
14     }
15     return [
16         parseInt(hex[0] + hex[1], 16),
17         parseInt(hex[2] + hex[3], 16),
18         parseInt(hex[4] + hex[5], 16),
19     ].join();
20 }

簡單來講,就是10進制與16進制的轉換。

有個點,就是rgb的三個值,分別對應的是hex的每兩個值,好比rgb(255,0,255)對用到hex則分別是 「ff,00,ff」,綜合起來就是「#ff00ff」,能夠簡寫「#f0f」。

 

額外效果:

中間的顏色選擇還有個效果,就是鼠標拖拽到哪裏,就選中相應的顏色。

鼠標拖拽事件你們都不陌生,直接上代碼,不廢話

 1 can.addEventListener('mousedown', function(e) {
 2     var ePos = {
 3         x: e.layerX || e.offsetX,
 4         y: e.layerY || e.offsetY
 5     }
 6     if (ePos.x >= 30 && ePos.x < 30 + width && ePos.y >= 0 && ePos.y < width) {
 7         document.onmousemove = function(e) {
 8             var pos = {
 9                 x: e.clientX,
10                 y: e.clientY
11             }
12 
13             pos.x = pos.x < 30 ? 30 : pos.x && (pos.x > (30 + width - 1) ? (30 + width - 1) : pos.x);
14             pos.y = pos.y < 0 ? 0 : pos.y && (pos.y > (width - 1) ? (width - 1) : pos.y);
15 
16             rgbaStr = getRgbaAtPoint(pos, 'box');
17             cur.style.left = pos.x + 'px';
18             cur.style.top = pos.y + 'px';
19             cur.style.outlineColor = (rgbaStr[0] > 256 / 2 || rgbaStr[1] > 256 / 2 || rgbaStr[2] > 256 / 2) ? '#000' : '#fff';
20             outColor(rgbaStr.slice(0, 3).join());
21         };
22         document.onmouseup = function() {
23             // outColor(rgbaStr.slice(0, 3).join());
24             document.onmouseup = document.onmousemove = null;
25         }
26     }
27 
28 });

 

 

這樣,每段代碼拼湊起來,就是總體的架子了,附上最終代碼(比較長,摺疊了):

  1 <!DOCTYPE html>
  2 <html lang="zh">
  3 
  4 <head>
  5     <meta charset="UTF-8">
  6     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7     <meta http-equiv="X-UA-Compatible" content="ie=edge">
  8     <title>Document</title>
  9     <style>
 10         body {
 11             background: #535353;
 12             padding: 0;
 13             margin: 0;
 14         }
 15         canvas {
 16             cursor: crosshair;
 17         }
 18         #cur {
 19             width: 3px;
 20             height: 3px;
 21             outline: 2px solid #535353;
 22             margin-left: -1px;
 23             margin-top: -1px;
 24             position: absolute;
 25         }
 26         .wrapper {
 27             position: relative;
 28         }
 29         #color_show {
 30             width: 50px;
 31             height: 50px;
 32             background: #f00;
 33         }
 34         .panel {
 35             width: 200px;
 36             height: 200px;
 37             position: fixed;
 38             top: 20px;
 39             right: 20px;
 40             background-color: #fff;
 41             padding: 10px;
 42             text-align: center;
 43             line-height: 2em;
 44         }
 45     </style>
 46 </head>
 47 
 48 <body>
 49     <div class="wrapper">
 50         <canvas id="canvas" width="600" height="600"></canvas>
 51         <em id="cur"></em>
 52         <div class="panel">
 53             <div id="color_show"></div>
 54             <label>
 55             rgb <input type="text"  class="color_input" value="" id="rgb_value">
 56         </label><br>
 57             <label>
 58             hex <input type="text"  class="color_input" value="" id="hex_value">
 59         </label>
 60         </div>
 61     </div>
 62     <script>
 63         (function(){
 64             var width = 256;
 65             var can = document.getElementById('canvas');
 66             var ctx = can.getContext('2d');
 67             var curColor = 'rgba(255,0,0,1)';
 68             var cur = document.getElementById('cur');
 69             var rgbValue = document.getElementById('rgb_value');
 70             var hexValue = document.getElementById('hex_value');
 71             var colorShow = document.getElementById('color_show');
 72 
 73             var aColorInput = document.getElementsByClassName('color_input');
 74 
 75             function colorBar() {
 76                 var gradientBar = ctx.createLinearGradient(0, 0, 0, width);
 77                 gradientBar.addColorStop(0, '#f00');
 78                 gradientBar.addColorStop(1 / 6, '#f0f');
 79                 gradientBar.addColorStop(2 / 6, '#00f');
 80                 gradientBar.addColorStop(3 / 6, '#0ff');
 81                 gradientBar.addColorStop(4 / 6, '#0f0');
 82                 gradientBar.addColorStop(5 / 6, '#ff0');
 83                 gradientBar.addColorStop(1, '#f00');
 84 
 85                 ctx.fillStyle = gradientBar;
 86                 ctx.fillRect(0, 0, 20, width);
 87             }
 88 
 89             function rgb2hex(rgb) {
 90                 var aRgb = rgb instanceof Array ? rgb : (rgb.split(',') || [0, 0, 0]);
 91                 var temp;
 92                 return [
 93                     (temp = Number(aRgb[0]).toString(16)).length == 1 ? ('0' + temp) : temp,
 94                     (temp = Number(aRgb[1]).toString(16)).length == 1 ? ('0' + temp) : temp,
 95                     (temp = Number(aRgb[2]).toString(16)).length == 1 ? ('0' + temp) : temp,
 96                 ].join('');
 97             }
 98 
 99             function hex2rgb(hex) {
100                 if (hex.length == 3) {
101                     hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
102                 }
103                 return [
104                     parseInt(hex[0] + hex[1], 16),
105                     parseInt(hex[2] + hex[3], 16),
106                     parseInt(hex[4] + hex[5], 16),
107                 ].join();
108             }
109 
110             function putCurDom(color) {
111                 if (/([0-9a-f]{3}|[0-9a-f]{6})/i.test(color)) {
112                     // hex
113                     color = hex2rgb(color);
114                 } else if (color instanceof Array) {
115                     color = color.join(',');
116                 } else if (/\d{1,3}(\,\d{1,3}){2}/i.test(color)) {
117 
118                 } else {
119                     return;
120                 }
121             }
122 
123             function colorBox(color) {
124                 // 底色填充,也就是(舉例紅色)到白色
125                 var gradientBase = ctx.createLinearGradient(30, 0, width + 30, 0);
126                 gradientBase.addColorStop(1, color);
127                 gradientBase.addColorStop(0, 'rgba(255,255,255,1)');
128                 ctx.fillStyle = gradientBase;
129                 ctx.fillRect(30, 0, width, width);
130                 // 第二次填充,黑色到透明
131                 var my_gradient1 = ctx.createLinearGradient(0, 0, 0, width);
132                 my_gradient1.addColorStop(0, 'rgba(0,0,0,0)');
133                 my_gradient1.addColorStop(1, 'rgba(0,0,0,1)');
134                 ctx.fillStyle = my_gradient1;
135                 ctx.fillRect(30, 0, width, width);
136             }
137 
138             function init() {
139                 colorBar();
140                 colorBox(curColor);
141                 bind();
142             }
143 
144             function bind() {
145                 can.addEventListener('click', function(e) {
146                     var ePos = {
147                         x: e.offsetX || e.layerX,
148                         y: e.offsetY || e.layerY
149                     }
150                     var rgbaStr = '#000';
151                     if (ePos.x >= 0 && ePos.x < 20 && ePos.y >= 0 && ePos.y < width) {
152                         // in
153                         rgbaStr = getRgbaAtPoint(ePos, 'bar');
154                         colorBox('rgba(' + rgbaStr + ')');
155                     } else if (ePos.x >= 30 && ePos.x < 30 + width && ePos.y >= 0 && ePos.y < width) {
156                         rgbaStr = getRgbaAtPoint(ePos, 'box');
157                     } else {
158                         return;
159                     }
160                     outColor(rgbaStr.slice(0, 3).join());
161                     cur.style.left = ePos.x + 'px';
162                     cur.style.top = ePos.y + 'px';
163                     cur.style.outlineColor = (rgbaStr[0] > 256 / 2 || rgbaStr[1] > 256 / 2 || rgbaStr[2] > 256 / 2) ? '#000' : '#fff';
164                 });
165 
166                 can.addEventListener('mousedown', function(e) {
167                     var ePos = {
168                         x: e.layerX || e.offsetX,
169                         y: e.layerY || e.offsetY
170                     }
171                     if (ePos.x >= 30 && ePos.x < 30 + width && ePos.y >= 0 && ePos.y < width) {
172                         document.onmousemove = function(e) {
173                             var pos = {
174                                 x: e.clientX,
175                                 y: e.clientY
176                             }
177 
178                             pos.x = pos.x < 30 ? 30 : pos.x && (pos.x > (30 + width - 1) ? (30 + width - 1) : pos.x);
179                             pos.y = pos.y < 0 ? 0 : pos.y && (pos.y > (width - 1) ? (width - 1) : pos.y);
180 
181                             rgbaStr = getRgbaAtPoint(pos, 'box');
182                             cur.style.left = pos.x + 'px';
183                             cur.style.top = pos.y + 'px';
184                             cur.style.outlineColor = (rgbaStr[0] > 256 / 2 || rgbaStr[1] > 256 / 2 || rgbaStr[2] > 256 / 2) ? '#000' : '#fff';
185                             outColor(rgbaStr.slice(0, 3).join());
186                         };
187                         document.onmouseup = function() {
188                             // outColor(rgbaStr.slice(0, 3).join());
189                             document.onmouseup = document.onmousemove = null;
190                         }
191                     }
192 
193                 });
194             }
195 
196             function outColor(rgb) {
197                 rgbValue.value = rgb;
198                 hexValue.value = rgb2hex(rgb);
199                 colorShow.style.backgroundColor = 'rgb(' + rgb + ')';
200             }
201 
202             function getRgbaAtPoint(pos, area) {
203                 if (area == 'bar') {
204                     var imgData = ctx.getImageData(0, 0, 20, width);
205                 } else {
206                     var imgData = ctx.getImageData(0, 0, can.width, can.height);
207                 }
208 
209                 var data = imgData.data;
210                 var dataIndex = (pos.y * imgData.width + pos.x) * 4;
211                 return [
212                     data[dataIndex],
213                     data[dataIndex + 1],
214                     data[dataIndex + 2],
215                     (data[dataIndex + 3] / 255).toFixed(2),
216                 ];
217             }
218 
219             init();
220         })()
221     </script>
222 </body>
223 
224 </html>
View Code

 

**********************************************************************

寫在最後:

最終寫完效果在本身玩耍的過程當中,發現瀏覽器對於canvas的過渡色實現有點問題。chrome很明顯,FF稍微好一點。

 

如圖: 按道理來講,最下邊選到的顏色應該都是rgb(0,0,0)纔對,可是圖上可見,有些地方並非~~~

大多數仍是000,某些點某個通道有可能會出現1。緣由未知。

 

嘗試了email給chrome郵箱,可能我英語比較差人家沒看懂,也可能我問題沒描述清楚,反正後來沒有回覆,以後的瀏覽器更新也沒有處理。

相應的,css3的過渡色則沒有一丁點問題。

**********************************************************************

圖片、代碼啥的貼了一堆,其中涉及的知識點可能有點多。看到這裏的同窗,建議回過頭再看一遍哈。須要注意的我儘可能特殊顏色標出來了。

最後,歡迎留言提建議什麼的。

 *******************************************

另附新款color-picker, 基於css和js計算去實現,規避上班canvas過渡色問題。

不過用了vue組件去實現,有些小問題懶得去處理【是有多懶,多年不寫文章就知道(也多是忙呢)】,回頭在不一篇隨筆,介紹js版本的實現方法。

https://win7killer.github.io/#/vue_demo/ColorPicker

相關文章
相關標籤/搜索