制作颜色选择器(全)

1,161 阅读9分钟

文章目录

颜色选择器

最近正在编写一些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 
}

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

\

可传递参数
参数说明类型默认值可选值
color传入的颜色,并且进行颜色属性双向绑定,v-bind绑定color时需使用.sync来使得双向绑定成功string#FF0000可使用十六进制颜色或者rgb颜色

构建思路

仿造

想搭建一个颜色选择器,那首先先要确定自己的需求,需要哪些部分。在这一点上,我编写的大多数组件样式都是仿制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>