写点什么

制作颜色选择器(全)

用户头像
空城机
关注
发布于: 2021 年 04 月 03 日
制作颜色选择器(全)

颜色选择器

最近正在编写一些 Vue 组件进行练手,挑一个颜色选择器组件来写一篇文章进行记录

组件展示



调用方式

组件的使用方式很简单,将组件添加进入后,调用代码如下:


// color1是在父组件中初始化的一个颜色,默认是#FF0000<wzc_color_picker :color.sync="color1"> </wzc_color_picker>
复制代码


在 vue 中添加组件时不要忘记导入到父组件中, 颜色绑定时请记得使用.sync,这样颜色才能够进行双向绑定


import wzc_color_picker from "./ColorPicker/wzc-color-picker"components: {  wzc_color_picker }
复制代码

完整的组件代码在文章最后面


可传递参数


构建思路

仿造

想搭建一个颜色选择器,那首先先要确定自己的需求,需要哪些部分。在这一点上,我编写的大多数组件样式都是仿制 element UI 的颜色选择器 (⑉・̆-・̆⑉)element颜色选择器




分析了一下,自己想要编写的颜色选择器面板需要的部分,准备实现一些基本的功能



制作前需要了解的颜色小知识 ʕ•ﻌ•ʔ ​​​

关于颜色的计算,本次我制作时主要会学习使用 HSV、RGB、十六进制颜色码之间互相转换来实现颜色选择器的制作。


那么接下来分别来介绍一下这三个值

HSV

HSV 是指☞ Hue 色相、Saturation 饱和度、Value 明度(亮度)。


  • 色相就是在不同的光照下,人眼所感觉不同的颜色。色相是色彩的首要特征,是区别各种不同色彩的最准确的标准,指不同的颜色,与亮度、饱和度无关

  • 饱和度是颜色的强度,饱和度表示色相中灰色分量所占的比例,它使用从 0%(灰色)至 100%(完全饱和)的百分比来度量。

  • 明度是不同颜色模式里控制画面明暗关系的可度量的参数。一般绝大多数人理解为亮度。明度也是从 0% - 100%进行度量。

这一部分的学习时参考的文章:如何实现一个颜色选择器


在 HSV 中,最重要的就是色相 H 了



色相

色相分为 360 度,从上面的色相条可以看出,从 0 度的红色开始,再到 360 度的红色截止,为一个周期。而且即使不通过饱和度和明度,只知道色相也可以相应计算出纯色 rgb 值。人眼区分色彩的最佳方式就是通过色相实现的。在最好的光照条件下,我们的眼睛大约能分辨出 180 种色彩的色相。在拍摄中,若能充分、有效的运用这一能力,将有助于我们构建理想的色彩画面。

RGB

RGB 想必对大家来说都很熟悉,是 RGB,而不是 RPG,也不是什么 RBQ ₍ᐢ •⌄• ᐢ₎


RGB 色彩就是常说的光学三原色,R 代表 Red(红色),G 代表 Green(绿色),B 代表 Blue(蓝色)。自然界中肉眼所能看到的任何色彩都可以由这三种色彩混合叠加而成,因此也称为加色模式。


在写 CSS 样式时,我们也经常会使用,例如 rgb(255, 0, 0);这就代表是红色。


RGB 模式又称 RGB 色空间。它是一种色光表色模式,它广泛用于我们的生活中,如电视机、计算机显示屏、幻灯片等都是利用光来呈色。印刷出版中常需扫描图像,扫描仪在扫描时首先提取的就是原稿图像上的 RGB 色光信息。RGB 模式是一种加色法模式,通过 R、G、B 的辐射量,可描述出任一颜色。计算机定义颜色时 R、G、 B 三种成分的取值范围是 0-255,0 表示没有刺激量,255 表示刺激量达最大值。R、G、B 均为 255 时就合成了白光,R、G、B 均为 0 时就形成了黑色。

十六进制颜色码

十六进制颜色码就是在软件中设定颜色值的代码。


在网页上要指定一种颜色,就要使用 RGB 模式来确定,方法是分别指定 R/G/B,也就是红/绿/蓝三种基色的强度,通常规定,每一种颜色强度最低为 0,最高为 255,并通常都以 16 进制数值表示,那么 255 对应于十六进制就是 FF,并把三个数值依次并列起来 ,以 #开头。


  • 颜色值“#FF0000”为红色,因为红色的值达到了最高值 FF(即十进制的 255),其余两种颜色强度为 0。


颜色板

了解了上面关于颜色的知识之后,就开始制作颜色选择器的工作了。首先先绘制主体部分,一块颜色板。在颜色板上左下角的饱和度和明度都是 0%


颜色板的制作使用了 CSS linear-gradient() 函数主要用于创建黑白渐变的底部面板色简单示例:先制造一个 box 盒子,其中嵌套两个黑白色渐变盒子


<style type="text/css">  .box {    margin: 30px;    width: 280px;    height: 180px;    position: relative;    border: #535353 solid 1px;  }  .colorbox {    width: 100%;    height: 100%;    position: absolute;    top: 0;    left: 0;  }</style><body>  <div class="box">    <div class="whitebox"></div>    <div class="blackbox"></div>  </div></body>
复制代码



之后给白色和黑色面板附上渐变颜色值


.box .whitebox {  background: linear-gradient(90deg,#fff,hsla(0,0%,100%,0));}.box .blackbox {  background: linear-gradient(0deg,#000,transparent);}
复制代码


现在基础的颜色面板就是如此,如果想实现平时颜色选择器的面板效果,还需要给父元素添加一个背景颜色 background-color 即可


.box {  background-color: #FFFF00;  border:none;}
复制代码



这样一个简单的颜色面板就出来了,当然 box 的背景颜色肯定是动态渐变的。


在面板上有一个选择圈,这个圈可以选取面板颜色。关于如何拖动选择器的方法这里就不过多做介绍了。 实现这个可以去看看 onmousedown 和 onmousemove 等事件的绑定使用可以参考:JavaScript 元素拖动

选择器编写和位置可以参考:

简易代码:


<style type="text/css">  ......上面的css代码  .thumb {    position: absolute;    top: 30px;    left: 140px;  }  .thumb > div {    width: 12px;    height: 12px;    border-radius: 6px;    box-shadow: rgb(255, 255, 255) 0px 0px 0px 1px inset;    transform: translate(-6px, -6px);  }</style><body><div class="box">  <div class="whitebox colorbox"></div>  <div class="blackbox colorbox"></div>  <div class="thumb">    <div></div>  </div></div></body>
复制代码


效果:


位置:




选择器位置颜色计算

目前仅仅只是拖动选择器,但是选择器当前位置的颜色需要根据当前的位置进行计算,得出颜色。这里是通过 HSV 来 得到 rgb 颜色值


颜色值转换方法可以参考:颜色值换算(HSV、RGB、十六进制颜色码)


饱和度和明度的计算 --- left,top 是选择器的位置,280 是我颜色面板的宽度,180 是颜色面板的高度


let saturation = Math.round(left / 280 * 100) / 100;let value = Math.round((1 - top / 180) * 100) / 100;
复制代码


H 色相的计算则是通过当前面板右上角设置的颜色来计算,一个颜色面板上的色相度数都是同一个。所以如上面黄色面板上的色相,只需要计算出 #ffff00 的 hue 色相读数即可


let hue = this.getHue(this.getRGB(""+this.backgroundColor));
复制代码


得到 HSV 后可以计算出当前颜色的 rgb 了

色相条


组件右侧有一条色相条,这条色相条的制作原理和颜色面板类似。



简易色相条样式制作:


<style type="text/css">  .wzc_color_right {    width: 12px;    height: 180px;    position: relative;  }  .wzc_hue_slider {    height: 100%;    background: linear-gradient(180deg, red 0, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, red);  }  .wzc_hue_slider_thumb {    position: absolute;    cursor: pointer;    box-sizing: border-box;    left: 0;    top: 0;    width: 12px;    height: 4px;    border-radius: 1px;    background: #fff;    border: 1px solid #f0f0f0;    box-shadow: 0 0 2px rgba(0, 0, 0, .6);    z-index: 1;  }</style><div class="wzc_color_right">  <div class="wzc_hue_slider"></div>  <div class="wzc_hue_slider_thumb"></div></div>
复制代码


效果:


关于色相条上色相度数的获取就更简单了,获取到 thumb 滑块的高度/总高度 * 360 就是当前的色相度数


let hue = Math.round((top / 180) * 360 * 100) / 100;
复制代码


获得 hue 后可以直接计算出纯色


参考文章:从色相值到纯色的快速计算


将此纯色动态替换到面板上就实现了面板颜色的变化


input 输入框

颜色选择器下方需要一个 input 输入框,用来展示当前的选择颜色,或者手动输入颜色当前颜色的计算方式就是与上面的** 选择器位置颜色计算 ** 类似,只不过需要在滑动时计算变化


此处着重于 input 输入改变当前颜色面板上选择器的位置,甚至替换面板颜色


input 标签有一个 oninput 可以绑定输入监听方法,使用 vue 的话就是 @input


<input type="text" class="wzc_input" v-model="currentColor" @input="inputHex">
复制代码


  • 输入监听时需要先进行一次内容判断,判断当前内容是否为有效的颜色,或者能否转换为颜色 rgb 值。

  • 如果可以进行下一步判断,获取当前颜色面板的色相值,和 input 输入框中颜色的色相值进行对比。

  • 如果两者相同,说明输入颜色可以在当前面板上找到。根据 rgb 计算输入颜色的饱和度和明度,通过这两者确定选择器应当处于面板的哪个位置

  • 如果两者不同,说明输入颜色在面板上找不到。可以将面板颜色替换为输入颜色,并且调整选择器位置到面板右上角


颜色展示

颜色展示模块只需要设置一个 div,动态绑定 div 颜色为当前颜色即可

组件完整代码

<template>    <div class="wzc_color_picker" :style="styleVar">        <div class="wzc_color_wrap">            <div class="wzc_color_left" @mousedown="mouseClick($event)">                <div class="white_panel"></div>                <div class="black_panel"></div>                <div class="wzc_color_pointer" ref="wzcColorPointer"  >                    <div ></div>                </div>            </div>            <div class="wzc_color_right" >                <div class="wzc_hue_slider" @mousedown="thumbClick($event)"></div>                <div class="wzc_hue_slider_thumb" ref="wzcthumb"></div>            </div>        </div>        <div class="wzc_color_btns">            <input type="text" class="wzc_input" v-model="currentColor" @input="inputHex">
<div class="wzc_color_show" :style="{ 'background-color': currentColor }"></div> </div> </div></template>
<script>export default { name:"wzc_color_picker", components: {}, props: { color: { type: String, default: "#FF0000" } }, data() { return { currentColor: "", backgroundColor: "", }; }, created() {}, mounted() { this.dragColorPointer(this.$refs.wzcColorPointer); this.dragColorPointer(this.$refs.wzcthumb, "thumb"); this.backgroundColor = this.color; this.currentColor = this.rgbToHex(this.color); this.$refs.wzcColorPointer.style.left = '280px'; this.$refs.wzcColorPointer.style.top = '0px'; }, watch: { }, computed: { styleVar() { return { '--wzc-picker-color': this.backgroundColor , } } }, methods: { dragColorPointer (el, dom) { let dragBox = el; dragBox.onmousedown = (e) => { e = e || window.event; let disX = e.clientX - dragBox.offsetLeft; let disY = e.clientY - dragBox.offsetTop; document.onmousemove = e => { let left = e.clientX - disX; let top = e.clientY - disY; if(left > 280){ left = 280; } if(left < 0) { left = 0; } if(top > 180) { top = 180; } if(top < 0) { top = 0; } dragBox.style.top = top + "px"; if( dom == "thumb" ) { dragBox.style.left = "0px"; this.changeThumbColor(top); } else { dragBox.style.left = left + "px"; this.changeColor(left, top ) } }; document.onmouseup = e => { document.onmousemove = null; document.onmouseup = null; }; } }, mouseClick (e) { if(e.target.className.indexOf("black_panel") != -1) { this.$refs.wzcColorPointer.style.left = e.offsetX + 'px'; this.$refs.wzcColorPointer.style.top = e.offsetY + 'px'; } else { if(e.target.className.indexOf("wzc_color_pointer") != -1) { if(this.$refs.wzcColorPointer.offsetLeft + e.offsetX <= 280){ this.$refs.wzcColorPointer.style.left = this.$refs.wzcColorPointer.offsetLeft + e.offsetX + 'px'; } else { this.$refs.wzcColorPointer.style.left = '280px'; } if(this.$refs.wzcColorPointer.offsetTop + e.offsetY <= 180) { this.$refs.wzcColorPointer.style.top = this.$refs.wzcColorPointer.offsetTop + e.offsetY + 'px'; } else { this.$refs.wzcColorPointer.style.top = '180px'; } } else{ this.$refs.wzcColorPointer.style.left = this.$refs.wzcColorPointer.offsetLeft + e.offsetX - 6 + 'px'; this.$refs.wzcColorPointer.style.top = this.$refs.wzcColorPointer.offsetTop + e.offsetY - 6 + 'px'; if((this.$refs.wzcColorPointer.offsetLeft + e.offsetX - 6) < 0){ this.$refs.wzcColorPointer.style.left = "0px" } if((this.$refs.wzcColorPointer.offsetLeft + e.offsetX - 6) >= 280){ this.$refs.wzcColorPointer.style.left = '280px'; } if((this.$refs.wzcColorPointer.offsetTop + e.offsetY - 6) < 0){ this.$refs.wzcColorPointer.style.top = "0px"; } if((this.$refs.wzcColorPointer.offsetTop + e.offsetY - 6) >= 180){ this.$refs.wzcColorPointer.style.top = "180px"; } } } this.changeColor(parseInt(this.$refs.wzcColorPointer.style.left), parseInt(this.$refs.wzcColorPointer.style.top) ) }, thumbClick (e) { this.$refs.wzcthumb.style.top = e.offsetY + 'px'; this.changeThumbColor(e.offsetY); }, // 计算颜色 HSV方式计算rgb changeColor (left, top) { let saturation = Math.round(left / 280 * 100) / 100; let value = Math.round((1 - top / 180) * 100) / 100; let hue = this.getHue(this.getRGB(""+this.backgroundColor)); this.currentColor = this.rgbToHex(this.HSVtoRGB(hue, saturation, value)); this.$emit('update:color', this.currentColor); }, changeThumbColor (top) { let hue = Math.round((top / 180) * 360 * 100) / 100; this.backgroundColor = this.HuetoRGB(hue); this.changeColor(parseInt(this.$refs.wzcColorPointer.style.left), parseInt(this.$refs.wzcColorPointer.style.top) ) }, getRGB (str){ if(str.indexOf('rgb') == -1 && str.indexOf('#') > -1){ // str = "rgba(" + str.match(/[A-Za-z0-9]{2}/g).map(function(v) { return parseInt(v, 16) }).join(",") + ")"; str = this.HexTorgb( str ); } let match = str.match(/rgba?\((\d{1,3}), ?(\d{1,3}), ?(\d{1,3})\)?(?:, ?(\d(?:\.\d?))\))?/); return match ? { red: match[1], green: match[2], blue: match[3] } : {}; }, // Hex(16进制颜色值) and RGB rgbToHex (color){ if(color.indexOf("#") != -1) { return color; } let arr = color.split(','); let r = +arr[0].split('(')[1]; let g = +arr[1]; let b = +arr[2].split(')')[0]; let value = (1 << 24) + r * (1 << 16) + g * (1 << 8) + b; value = value.toString(16); return '#' + value.slice(1); }, HexTorgb (hex){ var hexNum = hex.substring(1); hexNum = '0x' + (hexNum.length < 6 ? repeatLetter(hexNum, 2) : hexNum); var r = hexNum >> 16; var g = hexNum >> 8 & '0xff'; var b = hexNum & '0xff'; return `rgb(${r},${g},${b})`; function repeatWord(word, num){ var result = ''; for(let i = 0; i < num; i ++){ result += word; } return result; } function repeatLetter(word, num){ var result = ''; for(let letter of word){ result += repeatWord(letter, num); } return result; } }, // 根据Hue色相计算rgb纯色 HuetoRGB(h) { let doHandle = (num) =>{ if( num > 255) { return 255; } else if(num < 0){ return 0; } else { return Math.round(num); } }
let hueRGB = h/60 * 255; let r = doHandle(Math.abs(hueRGB-765)-255); let g = doHandle(510 - Math.abs(hueRGB-510)); let b = doHandle(510 - Math.abs(hueRGB-1020)); return 'rgb(' +r + ',' + g + ',' + b + ')'; }, // rgb to Hue(色相) getHue (rgbArray) { let r, g, b, max, min; for(let i = 0; i < 3; i++){ r = parseInt(rgbArray.red); g = parseInt(rgbArray.green); b = parseInt(rgbArray.blue); } max = Math.max(r, g, b) min = Math.min(r, g, b) if(max == min) { return 0; } else { if( max == r && g >= b) { return 60 * (g - b)/(max - min); } else if ( max == r && g < b) { return 60 * (g - b)/(max - min) + 360; } else if (max == g) { return 60 * (b - r)/(max - min) + 120; } else if (max == b) { return 60 * (r - g)/(max - min) + 240; } } }, // HSV(色相、饱和度、亮度) and RGB RGBtoHSV(rgb) { rgb = this.getRGB(rgb) var rr, gg, bb, r = parseInt(rgb.red) / 255, g = parseInt(rgb.green) / 255, b = parseInt(rgb.blue) / 255, h, s, v = Math.max(r, g, b), diff = v - Math.min(r, g, b), diffc = function(c){ return (v - c) / 6 / diff + 1 / 2; }; if (diff == 0) { h = s = 0; } else { s = diff / v; rr = diffc(r); gg = diffc(g); bb = diffc(b); if (r === v) { h = bb - gg; }else if (g === v) { h = (1 / 3) + rr - bb; }else if (b === v) { h = (2 / 3) + gg - rr; } if (h < 0) { h += 1; }else if (h > 1) { h -= 1; } } return { h: Math.round(h * 360), s: Math.round(s * 100), v: Math.round(v * 100) }; }, HSVtoRGB(h, s, v) { let i, f, p1, p2, p3; let r = 0, g = 0, b = 0; if (s < 0) s = 0; if (s > 1) s = 1; if (v < 0) v = 0; if (v > 1) v = 1; h %= 360; if (h < 0) h += 360; h /= 60; i = Math.floor(h); f = h - i; p1 = v * (1 - s); p2 = v * (1 - s * f); p3 = v * (1 - s * (1 - f)); switch(i) { case 0: r = v; g = p3; b = p1; break; case 1: r = p2; g = v; b = p1; break; case 2: r = p1; g = v; b = p3; break; case 3: r = p1; g = p2; b = v; break; case 4: r = p3; g = p1; b = v; break; case 5: r = v; g = p1; b = p2; break; } return 'rgb(' + Math.round(r * 255) + ',' + Math.round(g * 255) + ',' + Math.round(b * 255) + ')'; }, // 根据HSV计算位置 HSVtoPos (hsv) { let left = hsv.s / 100 * 280; let top = 180 - ( hsv.v / 100 * 180 ); this.$refs.wzcColorPointer.style.left = Math.round(left) + 'px'; this.$refs.wzcColorPointer.style.top = Math.round(top) + 'px'; }, inputHex (item){ let str = item.target.value; if( str.length < 4 ) return ; if( this.getHue(this.getRGB(str)) == undefined || this.getHue(this.getRGB(str)) > 360 || this.getHue(this.getRGB(str)) < 0 ) { return ; } else { let hsv = this.RGBtoHSV(str); let backgroundHue = this.getHue(this.getRGB(this.backgroundColor)); if (hsv.h == backgroundHue) { this.HSVtoPos(hsv); } else { this.backgroundColor = str; this.$refs.wzcColorPointer.style.left = '280px'; this.$refs.wzcColorPointer.style.top = '0px'; } } this.$emit('update:color', this.currentColor); } },};</script><style scoped> .wzc_color_picker { width: 314px; height: 228px; padding: 6px; box-sizing: content-box; background-color: #fff; border: 1px solid #ebeef5; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0,0,0,.1); } .wzc_color_wrap { width: 100%; height: 180px; display: flex; justify-content: space-around; } .wzc_color_left { width: 280px; height: 100%; position: relative; background-color: var(--wzc-picker-color); overflow: hidden; } .wzc_color_right { width: 12px; height: 100%; position: relative; } .wzc_color_left .white_panel, .wzc_color_left .black_panel{ position: absolute; top: 0; left: 0; right: 0; bottom: 0; } .wzc_color_left .white_panel { background: linear-gradient(90deg,#fff,hsla(0,0%,100%,0)); } .wzc_color_left .black_panel { background: linear-gradient(0deg,#000,transparent); } .wzc_color_pointer { position: absolute; top: 0px; left: 280px; } .wzc_color_pointer > div { width: 12px; height: 12px; border-radius: 6px; box-shadow: rgb(255, 255, 255) 0px 0px 0px 1px inset; transform: translate(-6px, -6px); } .wzc_color_right .wzc_hue_slider { height: 100%; background: linear-gradient(180deg,red 0,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,red); } .wzc_hue_slider_thumb { position: absolute; cursor: pointer; box-sizing: border-box; left: 0; top: 0; width: 12px; height: 4px; border-radius: 1px; background: #fff; border: 1px solid #f0f0f0; box-shadow: 0 0 2px rgba(0,0,0,.6); z-index: 1; } .wzc_color_btns .wzc_input { width: 155px; height: 28px; line-height: 28px; background-color: #fff; background-image: none; border-radius: 4px; border: 1px solid #dcdfe6; box-sizing: border-box; color: #606266; display: inline-block; font-size: inherit; outline: none; padding: 0 15px; margin-left: 5px; } .wzc_color_show { width: 28px; height: 28px; border-radius: 5px; margin-left: 15px; } .wzc_color_btns { width: 100%; height: 28px; margin-top: 10px; display: flex; }</style>
复制代码


发布于: 2021 年 04 月 03 日阅读数: 42
用户头像

空城机

关注

曾经沧海难为水,只是当时已惘然 2021.03.22 加入

业余作者,在线水文 主要干前端的活,业余会学学python 欢迎各位关注,互相学习,互相进步

评论

发布
暂无评论
制作颜色选择器(全)