实现效果
相较于上次发布的颜色选择器,这次加入了圆形的选择器,并且优化了代码。
<SquareColor ref="squareColor" :color="color" @change="changeColor1" />setColor1() { // this.color = 'rgba(255, 82, 111, 0.5)' this.$refs.squareColor.changeColor('rgba(255, 82, 111, 0.5)') }
使用方式:可以使用color属性传入默认颜色,目前支持hex16进制,rgb,rgba,hsl这四种格式,使用change事件获取颜色修改后的值,如果需要父组件动态修改其中的颜色则需要调用组件中的changeColor方法。
如果需要修改传出的颜色类型可以在组件中的getRGBA方法中进行修改,或者在父组件中使用color-convert进行转换。
在使用该组件之前需要引入color-convert依赖
npm install color-convert
完整代码:该代码主要分为四个部分(方形选择器组件,圆形选择器组件,utils,父组件示例)
方形选择器:
<template> <div class="color-box"> <div class="color-panel" ref="colorPanel" @mousedown="colorPanelMD" :style="{ background: colorPanelColor }"> <div class="color-white-panel"></div> <div class="color-black-panel"></div> <div ref="colorPanelSliderThumb" class="color-panel-slider-thumb"></div> </div> <div class="hue-panel" ref="huePanel" @mousedown="huePanelMD"> <div ref="huePanelSliderThumb" class="hue-slider-thumb"></div> </div> <div class="alpha-panel" ref="alphaPanel" @mousedown="alphaPanelMD"> <div class="alpha-panel-cover" :style="{ background: `linear-gradient(to right, rgba(255, 255, 255, 0) 0, ${colorPanelColor} 100%)` }"> </div> <div ref="alphaPanelSliderThumb" class="alpha-slider-thumb"></div> </div> </div></template><script>import convert from 'color-convert'import { colorTypeConversion } from '@/utils'export default { props: { color: { type: String, required: true, default: '' } }, data() { return { colorPanelColor: 'red', h: 0, s: 0, v: 100, alpha: 1, }; }, mounted() { this.changeColor(this.color) }, methods: { // 设置颜色 转换颜色为hsv类型 changeColor(val) { let colorObj = colorTypeConversion(val) if (colorObj?.alpha) { this.alpha = colorObj.alpha } if (colorObj?.color?.length == 3) { let [h, s, v] = colorObj.color // 判断当前颜色是否和需要转换的颜色是否一致,一致则不进行转换 if (this.h !== h || this.s !== s || this.v !== v) { this.h = colorObj.color[0] this.s = colorObj.color[1] this.v = colorObj.color[2] this.$nextTick(() => { this.initPosi(); }) } } }, // 初始化hsv初始位置 initPosi() { // 设置色相条按钮位置 this.$refs.huePanelSliderThumb.style.left = this.h / 360 * this.$refs.huePanel.offsetWidth + 'px' // 设置透明度条按钮位置 this.$refs.alphaPanelSliderThumb.style.left = this.alpha * this.$refs.alphaPanel.offsetWidth + 'px' // 设置色盘按钮位置 this.$refs.colorPanelSliderThumb.style.left = this.s / 100 * this.$refs.colorPanel.offsetWidth + 'px' this.$refs.colorPanelSliderThumb.style.top = (100 - this.v) / 100 * this.$refs.colorPanel.offsetHeight + 'px' // 设置色盘和透明度背景色 this.colorPanelColor = '#' + convert.hsv.hex(this.h, this.s, this.v) }, // 色盘鼠标事件 colorPanelMD(e) { let that = this let colorPanel = that.$refs.colorPanel let colorPanelSliderThumb = that.$refs.colorPanelSliderThumb let { width, height } = colorPanel.getBoundingClientRect() colorPanelSliderThumb.style.left = that.judgeBoundary(e.offsetX, 0, width) + 'px' colorPanelSliderThumb.style.top = that.judgeBoundary(e.offsetY, 0, height) + 'px' that.getSV() let initLeft = colorPanelSliderThumb.offsetLeft let initTop = colorPanelSliderThumb.offsetTop let initX = e.pageX let initY = e.pageY document.addEventListener('mousemove', mouseMove) function mouseMove(e) { colorPanelSliderThumb.style.left = that.judgeBoundary(e.pageX - initX + initLeft, 0, width) + 'px' colorPanelSliderThumb.style.top = that.judgeBoundary(e.pageY - initY + initTop, 0, height) + 'px' that.getSV() } document.addEventListener('mouseup', mouseUp) function mouseUp() { document.removeEventListener('mousemove', mouseMove) document.removeEventListener('mouseup', mouseUp) } }, // 获取饱和度和明值 getSV() { let that = this let colorPanel = that.$refs.colorPanel let colorPanelSliderThumb = that.$refs.colorPanelSliderThumb let { width, height } = colorPanel.getBoundingClientRect() let left = colorPanelSliderThumb.offsetLeft let top = colorPanelSliderThumb.offsetTop let s = left / width * 100 let v = 100 - top / height * 100 that.s = s that.v = v that.getRGBA(); }, // 色相鼠标事件 huePanelMD(e) { let that = this let huePanel = that.$refs.huePanel let huePanelSliderThumb = that.$refs.huePanelSliderThumb let { width } = huePanel.getBoundingClientRect() huePanelSliderThumb.style.left = that.judgeBoundary(e.offsetX, 0, width) + 'px' that.getHue(); let initLeft = huePanelSliderThumb.offsetLeft let initX = e.pageX document.addEventListener('mousemove', mouseMove) function mouseMove(e) { huePanelSliderThumb.style.left = that.judgeBoundary(e.pageX - initX + initLeft, 0, width) + 'px' that.getHue(); } document.addEventListener('mouseup', mouseUp) function mouseUp() { document.removeEventListener('mousemove', mouseMove) document.removeEventListener('mouseup', mouseUp) } }, // 获取色相并转换成颜色 getHue() { let that = this let huePanel = that.$refs.huePanel let huePanelSliderThumb = that.$refs.huePanelSliderThumb let { width } = huePanel.getBoundingClientRect() let hue = huePanelSliderThumb.offsetLeft / width * 360 that.h = hue let color = convert.hsv.hex(hue, 100, 100) that.colorPanelColor = '#' + color that.getRGBA(); }, // 透明度鼠标事件 alphaPanelMD(e) { let that = this let alphaPanel = that.$refs.alphaPanel let alphaPanelSliderThumb = that.$refs.alphaPanelSliderThumb let { width } = alphaPanel.getBoundingClientRect() alphaPanelSliderThumb.style.left = that.judgeBoundary(e.offsetX, 0, width) + 'px' that.getAlpha(); let initLeft = alphaPanelSliderThumb.offsetLeft let initX = e.pageX document.addEventListener('mousemove', mouseMove) function mouseMove(e) { alphaPanelSliderThumb.style.left = that.judgeBoundary(e.pageX - initX + initLeft, 0, width) + 'px' that.getAlpha(); } document.addEventListener('mouseup', mouseUp) function mouseUp() { document.removeEventListener('mousemove', mouseMove) document.removeEventListener('mouseup', mouseUp) } }, getAlpha() { let that = this let alphaPanel = that.$refs.alphaPanel let alphaPanelSliderThumb = that.$refs.alphaPanelSliderThumb let { width } = alphaPanel.getBoundingClientRect() let alpha = (alphaPanelSliderThumb.offsetLeft / width).toFixed(2) that.alpha = alpha this.getRGBA(); }, // 获取RGBA色值 getRGBA() { let color = convert.hsv.rgb(this.h, this.s, this.v) let rgba = `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${this.alpha})` this.$emit('change', rgba) }, // 边界判断 judgeBoundary(value, min, max) { if (value < min) { return min } if (value > max) { return max } return value }, },};</script><style lang="scss" scoped>.color-box { width: 300px; .color-panel { position: relative; height: 200px; // background-color: red; .color-white-panel { position: absolute; inset: 0; background: linear-gradient(to right, #fff 0%, transparent 100%); } .color-black-panel { position: absolute; inset: 0; background: linear-gradient(to top, #000 0%, transparent 100%); } .color-panel-slider-thumb { position: absolute; top: 0; left: 0; transform: translate(-50%, -50%); width: 5px; height: 5px; box-shadow: 0 0 2px #5a5a5a; border: 3px solid #fff; border-radius: 50%; pointer-events: none; } } .hue-panel { position: relative; height: 12px; margin: 20px 0; background: linear-gradient(to right, red 0, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, red); } .alpha-panel { position: relative; height: 12px; background: url('../../assets/images/alpha.png'); .alpha-panel-cover { position: absolute; inset: 0; } .alpha-slider-thumb { left: 100%; } }}.hue-slider-thumb,.alpha-slider-thumb { position: absolute; left: 0; top: 50%; transform: translate(-50%, -50%); width: 5px; height: 140%; border-radius: 4px; background-color: #fff; box-shadow: 0 0 2px #5a5a5a; cursor: pointer; pointer-events: none;}</style>
圆形选择器:
<template> <div class="color-box"> <div class="top-part"> <div :style="{ width: width + 'px', height: width + 'px' }" class="color-panel" ref="colorPanel" @mousedown="colorPanelMD"> <div class="value-bg" ref="valueBg"></div> <div ref="colorPanelSliderThumb" class="color-panel-slider-thumb"></div> </div> <div class="value-panel" @mousedown="valuePanelMD" ref="valuePanel" :style="{ height: width + 'px' }"> <div ref="valuePanelSliderThumb" class="value-slider-thumb"></div> </div> </div> <div :style="{ width: width + 'px' }" class="alpha-panel" ref="alphaPanel" @mousedown="alphaPanelMD"> <div class="alpha-panel-cover" :style="{ background: `linear-gradient(to right, rgba(255, 255, 255, 0) 0, ${colorPanelColor} 100%)` }"> </div> <div ref="alphaPanelSliderThumb" class="alpha-slider-thumb"></div> </div> </div></template><script>import convert from 'color-convert'import { colorTypeConversion } from '@/utils'export default { props: { width: { type: Number, default: 200, required: false, }, color: { type: String, required: true, default: '' } }, data() { return { h: 0, s: 0, v: 100, alpha: 1, colorPanelColor: '#fff', }; }, mounted() { this.changeColor(this.color) }, methods: { // 设置颜色 转换颜色为hsv类型 changeColor(val) { let colorObj = colorTypeConversion(val) if (colorObj?.alpha) { this.alpha = colorObj.alpha } if (colorObj?.color?.length == 3) { let [h, s, v] = colorObj.color // 判断当前颜色是否和需要转换的颜色是否一致,一致则不进行转换 if (this.h !== h || this.s !== s || this.v !== v) { this.h = colorObj.color[0] this.s = colorObj.color[1] this.v = colorObj.color[2] this.$nextTick(() => { this.initPosi(); }) } } }, // 初始化hsv初始位置 initPosi() { // 设置明值位置即右侧柱子按钮位置 let height = this.$refs.valuePanel.getBoundingClientRect().height this.$refs.valuePanelSliderThumb.style.top = height - height * this.v / 100 + 'px' this.$refs.valueBg.style.background = `rgba(0, 0, 0, ${Number(1 - (this.v / 100).toFixed(2))})` // 获取饱和度(长度) 和 色相(夹角) const vector = [0, 1]; // 假设圆心为坐标原点,并使用夹角度数配合旋转矩阵以及按钮距离圆心的长度求取按钮在色盘中的位置 let r = this.width / 2 let length = this.s / 100 * r let angle = -Number((this.h - Math.PI / 180).toFixed(2)) // 将角度转为弧度 const angleInRadians = angle * Math.PI / 180; // 计算另一个向量 const x = vector[0] * Math.cos(angleInRadians) - vector[1] * Math.sin(angleInRadians); const y = vector[0] * Math.sin(angleInRadians) + vector[1] * Math.cos(angleInRadians); // 根据长度进行缩放 const scaleFactor = length / Math.sqrt(x * x + y * y); const newX = x * scaleFactor; const newY = y * scaleFactor; this.$refs.colorPanelSliderThumb.style.left = newX + r + 'px' this.$refs.colorPanelSliderThumb.style.top = r - newY + 'px' // 给底部透明度设置背景 this.colorPanelColor = '#' + convert.hsv.hex(this.h, this.s, this.v) // 设置透明度按钮位置 let { width } = this.$refs.alphaPanel.getBoundingClientRect() this.$refs.alphaPanelSliderThumb.style.left = width * this.alpha + 'px' }, colorPanelMD(e) { let that = this const colorPanelSliderThumb = that.$refs.colorPanelSliderThumb colorPanelSliderThumb.style.left = e.offsetX + 'px' colorPanelSliderThumb.style.top = e.offsetY + 'px' that.calculateAngle() // 计算夹角 获取色相 let initLeft = colorPanelSliderThumb.offsetLeft let initTop = colorPanelSliderThumb.offsetTop let initX = e.pageX let initY = e.pageY document.addEventListener('mousemove', mouseMove) function mouseMove(e) { let x = e.pageX - initX + initLeft let y = e.pageY - initY + initTop colorPanelSliderThumb.style.left = that.circleJudgeBoundary(x, y).targetX + 'px' colorPanelSliderThumb.style.top = that.circleJudgeBoundary(x, y).targetY + 'px' that.calculateAngle() // 计算夹角 获取色相 } document.addEventListener('mouseup', mouseUp) function mouseUp(e) { document.removeEventListener('mousemove', mouseMove) document.removeEventListener('mouseup', mouseUp) } }, // 获取当前滑块的向量坐标归一化 并获取其饱和度值 calcSliderThumbVector() { let r = this.width / 2 const colorPanel = this.$refs.colorPanel const colorPanelSliderThumb = this.$refs.colorPanelSliderThumb let { width, height, left, top } = colorPanel.getBoundingClientRect() let originX = left + width / 2 - colorPanel.offsetLeft let originY = top + height / 2 - colorPanel.offsetTop let x = (colorPanelSliderThumb.getBoundingClientRect().left - left + colorPanelSliderThumb.getBoundingClientRect().width / 2) - originX let y = originY - (colorPanelSliderThumb.getBoundingClientRect().top - top + colorPanelSliderThumb.getBoundingClientRect().height / 2) // 获取饱和度 let s = Number((Math.sqrt(x ** 2 + y ** 2) / r * 100).toFixed(2)) this.s = s <= 100 ? s : 100 return this.normalizeVector([x, y]) }, // 归一化二维向量 normalizeVector(vector) { // 计算向量的模长 const magnitude = Math.sqrt(vector[0] ** 2 + vector[1] ** 2); // 将向量的每个分量除以模长 const normalizedVector = [vector[0] / magnitude, vector[1] / magnitude]; return normalizedVector; }, // 计算两个二维向量的夹角 夹角度数即色相 calculateAngle(vectorA = [0, 1], vectorB = this.calcSliderThumbVector(), isGetRGBA = true) { // 计算向量的点积 const dotProduct = vectorA[0] * vectorB[0] + vectorA[1] * vectorB[1]; // 计算向量的模长 const magnitudeA = Math.sqrt(vectorA[0] ** 2 + vectorA[1] ** 2); const magnitudeB = Math.sqrt(vectorB[0] ** 2 + vectorB[1] ** 2); // 计算夹角的余弦值 let cosTheta; if (magnitudeA * magnitudeB === 0) { cosTheta = 0 } else { cosTheta = dotProduct / (magnitudeA * magnitudeB); } // 计算夹角的弧度值 const angleRad = Math.acos(cosTheta); let h; if (vectorB[0] < 0 && isGetRGBA) { h = Number((((2 * Math.PI - angleRad) * 180) / Math.PI).toFixed(2)); } else { h = Number(((angleRad * 180) / Math.PI).toFixed(2)); } this.h = h if (isGetRGBA) this.getRGBA(); return h; }, // 圆盘边界判断 circleJudgeBoundary(targetX, targetY) { const colorPanel = this.$refs.colorPanel let { width, height, left, top } = colorPanel.getBoundingClientRect() let originX = left + width / 2 - colorPanel.offsetLeft let originY = top + height / 2 - colorPanel.offsetTop let x = targetX - originX let y = originY - targetY // 判断鼠标是否已经超出的圆盘 如果超出了圆盘 则将鼠标位置限制在圆盘内 if (Math.sqrt(x ** 2 + y ** 2) <= this.width / 2) { return { targetX, targetY } } else { // 计算目标坐标的夹角 let angle = this.calculateAngle([x, 0], [x, y], false) // 计算标记点在圆盘边缘的坐标位置 以圆盘中心为原点 let r = this.width / 2 // 半径 let realX = (r * Math.cos(angle * Math.PI / 180)) * (x / Math.abs(x)) + r let realY = r - (r * Math.sin(angle * Math.PI / 180)) * (y / Math.abs(y)) return { targetX: Number(realX.toFixed(2)), targetY: Number(realY.toFixed(2)) } } }, valuePanelMD(e) { let that = this let valuePanel = that.$refs.valuePanel let { top, height } = valuePanel.getBoundingClientRect() let valuePanelSliderThumb = that.$refs.valuePanelSliderThumb let initY = e.pageY valuePanelSliderThumb.style.top = initY - top - valuePanelSliderThumb.getBoundingClientRect().height / 2 + 'px' that.getValue() let initTop = valuePanelSliderThumb.offsetTop document.addEventListener('mousemove', mouseMove) function mouseMove(e) { valuePanelSliderThumb.style.top = that.judgeBoundary(initTop + e.pageY - initY, 0, height) + 'px' that.getValue() } document.addEventListener('mouseup', mouseUp) function mouseUp() { document.removeEventListener('mousemove', mouseMove) document.removeEventListener('mouseup', mouseUp) } }, // 获取明值 getValue() { let valuePanel = this.$refs.valuePanel let { height } = valuePanel.getBoundingClientRect() let offsetTop = this.$refs.valuePanelSliderThumb.offsetTop this.v = 100 - Number((offsetTop / height * 100).toFixed(2)) this.$refs.valueBg.style.background = `rgba(0, 0, 0, ${Number(1 - (this.v / 100).toFixed(2))})` this.getRGBA() }, // 透明度鼠标事件 alphaPanelMD(e) { let that = this let alphaPanel = that.$refs.alphaPanel let alphaPanelSliderThumb = that.$refs.alphaPanelSliderThumb let { width } = alphaPanel.getBoundingClientRect() alphaPanelSliderThumb.style.left = that.judgeBoundary(e.offsetX, 0, width) + 'px' that.getAlpha(); let initLeft = alphaPanelSliderThumb.offsetLeft let initX = e.pageX document.addEventListener('mousemove', mouseMove) function mouseMove(e) { alphaPanelSliderThumb.style.left = that.judgeBoundary(e.pageX - initX + initLeft, 0, width) + 'px' that.getAlpha(); } document.addEventListener('mouseup', mouseUp) function mouseUp() { document.removeEventListener('mousemove', mouseMove) document.removeEventListener('mouseup', mouseUp) } }, getAlpha() { let that = this let alphaPanel = that.$refs.alphaPanel let alphaPanelSliderThumb = that.$refs.alphaPanelSliderThumb let { width } = alphaPanel.getBoundingClientRect() let alpha = (alphaPanelSliderThumb.offsetLeft / width).toFixed(2) that.alpha = alpha this.getRGBA(); }, // 获取RGBA色值 getRGBA() { let color = convert.hsv.rgb(this.h, this.s, this.v) let rgba = `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${this.alpha})` this.colorPanelColor = `rgb(${color[0]}, ${color[1]}, ${color[2]})` this.$emit('change', rgba) }, // 明值和透明度边界判断 judgeBoundary(value, min, max) { if (value < min) { return min } if (value > max) { return max } return value }, },};</script><style lang="scss" scoped>.color-box { .top-part { display: flex; .color-panel { position: relative; border-radius: 50%; background: conic-gradient(red 0, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, red); &::before { content: ''; position: absolute; inset: 0; border-radius: 50%; z-index: 10000; background: radial-gradient(#fff 0, transparent 80%); } .value-bg { position: absolute; inset: 0; z-index: 10010; border-radius: 50%; } .color-panel-slider-thumb { position: absolute; top: 50%; left: 50%; z-index: 10999; transform: translate(-50%, -50%); width: 6px; height: 6px; background: #fff; border: 1px solid #000; border-radius: 50%; pointer-events: none; box-sizing: border-box; } } .value-panel { position: relative; width: 12px; background: linear-gradient(to bottom, #fff 0, #000 100%); margin-left: 20px; .value-slider-thumb { position: absolute; top: 0; left: 50%; transform: translate(-50%, 0); width: 120%; height: 5px; border-radius: 4px; background-color: #fff; box-shadow: 0 0 2px #5a5a5a; pointer-events: none; } } } .alpha-panel { position: relative; height: 12px; background: url('../../assets/images/alpha.png'); margin-top: 20px; .alpha-panel-cover { position: absolute; inset: 0; } .alpha-slider-thumb { position: absolute; left: 100%; top: 50%; transform: translate(-50%, -50%); width: 5px; height: 140%; border-radius: 4px; background-color: #fff; box-shadow: 0 0 2px #5a5a5a; cursor: pointer; pointer-events: none; } }}</style>
uitls>index.js:
import convert from 'color-convert'// color: 颜色,type: 需要转换的类型export function colorTypeConversion(color, type = 'hsv') { let reg = /(\d+(\.\d+)?)|(\.\d+)/g // 判断颜色类型 并转换 if (color.startsWith('#')) { return { color: convert.hex[type](color), alpha: 1 } } else if (color.startsWith('rgba')) { let arr = color.match(reg).map(item => Number(item)) return { color: convert.rgb[type](arr[0], arr[1], arr[2]), alpha: arr[3] } } else if (color.startsWith('rgb')) { let arr = color.match(reg).map(item => Number(item)) return { color: convert.rgb[type](arr[0], arr[1], arr[2]), alpha: 1 } } else if (color.startsWith('hsl')) { let arr = color.match(reg) return { color: convert.hsl[type]([arr[0], arr[1], arr[2]]), alpha: 1 } } else { return { color: convert.hex[type](color), alpha: 1 } }}
父组件示例:
<template> <div class="page1"> <div class="box"> <div style="margin-bottom: 25px;"> <span>{{ color }}</span> <div class="show-color" :style="{ background: color }"></div> </div> <SquareColor ref="squareColor" :color="color" @change="changeColor1" /> <button @click="setColor1">修改为 rgba(255, 82, 111, 0.5)</button> </div> <div class="box"> <div style="margin-bottom: 25px;"> <span>{{ color2 }}</span> <div class="show-color" :style="{ background: color2 }"></div> </div> <CircleColor ref="circleColor" :color="color2" @change="changeColor2" /> <button @click="setColor2">修改为 #5fff45</button> </div> </div></template><script>import SquareColor from '@/components/color/square.vue'import CircleColor from '@/components/color/circle.vue'export default { name: 'ComponentPage1', components: { SquareColor, CircleColor, }, data() { return { color: 'hsl(107.74deg 88.62% 47.73%)', color2: 'rgba(255, 66, 237, 1)', }; }, mounted() { }, methods: { setColor1() { this.color = 'rgba(255, 82, 111, 0.5)' this.$refs.squareColor.changeColor('rgba(255, 82, 111, 0.5)') }, setColor2() { this.color2 = '#5fff45' this.$refs.circleColor.changeColor('#5fff45') }, changeColor1(color) { this.color = color }, changeColor2(color) { this.color2 = color } },};</script><style lang="scss" scoped>.page1 { display: flex; flex-wrap: wrap; gap: 50px; padding: 100px; .box { padding: 20px; box-shadow: 0 0 8px #b8b7b7; border-radius: 10px; .show-color { display: inline-block; width: 100px; height: 25px; margin-left: 25px; vertical-align: middle; box-shadow: 0 0 8px #b8b7b7; border-radius: 4px; } }}button { background-color: #409eff; color: #fff; border: none; padding: 8px 15px; border-radius: 4px; margin: 20px 0; cursor: pointer;}</style>
完整项目代码压缩包:colorPicker