每个浏览器都有自己的特点,比如今天要做的colorpicker就是,一千个浏览器,一千个哈姆雷特,一千个colorpicker。今天canvas系列就用canvas做一个colorpicker。
**********************************************************************
效果图和demo
突然翻到了之前用js和dom写的一个colorpicker,比较挫,扔张图就好(old)
这个真的很挫,性能很差,因为每一个可选的颜色值都是一个dom,如果要实现256*256,那浏览器就爆了~~~~~
好,回到今天的demo(new)
demo链接: https://win7killer.github.io/demo_set/html_demo/canvas/can_ps/color_picker.html
没错,就是照着PS的颜色选择器的样子仿的。
**********************************************************************
实现
首先我们来看效果图分析怎么做:
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>
**********************************************************************
写在最后:
最终写完效果在自己玩耍的过程中,发现浏览器对于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
更多专业前端知识,请上 【猿2048】www.mk2048.com