实现效果
相较于上次发布的颜色选择器,这次加入了圆形的选择器,并且优化了代码。
<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 = thislet colorPanel = that.$refs.colorPanellet colorPanelSliderThumb = that.$refs.colorPanelSliderThumblet { 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.offsetLeftlet initTop = colorPanelSliderThumb.offsetToplet initX = e.pageXlet initY = e.pageYdocument.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 = thislet colorPanel = that.$refs.colorPanellet colorPanelSliderThumb = that.$refs.colorPanelSliderThumblet { width, height } = colorPanel.getBoundingClientRect()let left = colorPanelSliderThumb.offsetLeftlet top = colorPanelSliderThumb.offsetToplet s = left / width * 100let v = 100 - top / height * 100that.s = sthat.v = vthat.getRGBA();},// 色相鼠标事件huePanelMD(e) {let that = thislet huePanel = that.$refs.huePanellet huePanelSliderThumb = that.$refs.huePanelSliderThumblet { width } = huePanel.getBoundingClientRect()huePanelSliderThumb.style.left = that.judgeBoundary(e.offsetX, 0, width) + 'px'that.getHue();let initLeft = huePanelSliderThumb.offsetLeftlet initX = e.pageXdocument.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 = thislet huePanel = that.$refs.huePanellet huePanelSliderThumb = that.$refs.huePanelSliderThumblet { width } = huePanel.getBoundingClientRect()let hue = huePanelSliderThumb.offsetLeft / width * 360that.h = huelet color = convert.hsv.hex(hue, 100, 100)that.colorPanelColor = '#' + colorthat.getRGBA();},// 透明度鼠标事件alphaPanelMD(e) {let that = thislet alphaPanel = that.$refs.alphaPanellet alphaPanelSliderThumb = that.$refs.alphaPanelSliderThumblet { width } = alphaPanel.getBoundingClientRect()alphaPanelSliderThumb.style.left = that.judgeBoundary(e.offsetX, 0, width) + 'px'that.getAlpha();let initLeft = alphaPanelSliderThumb.offsetLeftlet initX = e.pageXdocument.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 = thislet alphaPanel = that.$refs.alphaPanellet alphaPanelSliderThumb = that.$refs.alphaPanelSliderThumblet { width } = alphaPanel.getBoundingClientRect()let alpha = (alphaPanelSliderThumb.offsetLeft / width).toFixed(2)that.alpha = alphathis.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().heightthis.$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 / 2let length = this.s / 100 * rlet 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 = thisconst colorPanelSliderThumb = that.$refs.colorPanelSliderThumbcolorPanelSliderThumb.style.left = e.offsetX + 'px'colorPanelSliderThumb.style.top = e.offsetY + 'px'that.calculateAngle() // 计算夹角 获取色相let initLeft = colorPanelSliderThumb.offsetLeftlet initTop = colorPanelSliderThumb.offsetToplet initX = e.pageXlet initY = e.pageYdocument.addEventListener('mousemove', mouseMove)function mouseMove(e) {let x = e.pageX - initX + initLeftlet y = e.pageY - initY + initTopcolorPanelSliderThumb.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 / 2const colorPanel = this.$refs.colorPanelconst colorPanelSliderThumb = this.$refs.colorPanelSliderThumblet { width, height, left, top } = colorPanel.getBoundingClientRect()let originX = left + width / 2 - colorPanel.offsetLeftlet originY = top + height / 2 - colorPanel.offsetToplet x = (colorPanelSliderThumb.getBoundingClientRect().left - left + colorPanelSliderThumb.getBoundingClientRect().width / 2) - originXlet 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 : 100return 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 = hif (isGetRGBA) this.getRGBA();return h;},// 圆盘边界判断circleJudgeBoundary(targetX, targetY) {const colorPanel = this.$refs.colorPanellet { width, height, left, top } = colorPanel.getBoundingClientRect()let originX = left + width / 2 - colorPanel.offsetLeftlet originY = top + height / 2 - colorPanel.offsetToplet x = targetX - originXlet 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)) + rlet 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 = thislet valuePanel = that.$refs.valuePanellet { top, height } = valuePanel.getBoundingClientRect()let valuePanelSliderThumb = that.$refs.valuePanelSliderThumblet initY = e.pageYvaluePanelSliderThumb.style.top = initY - top - valuePanelSliderThumb.getBoundingClientRect().height / 2 + 'px'that.getValue()let initTop = valuePanelSliderThumb.offsetTopdocument.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.valuePanellet { height } = valuePanel.getBoundingClientRect()let offsetTop = this.$refs.valuePanelSliderThumb.offsetTopthis.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 = thislet alphaPanel = that.$refs.alphaPanellet alphaPanelSliderThumb = that.$refs.alphaPanelSliderThumblet { width } = alphaPanel.getBoundingClientRect()alphaPanelSliderThumb.style.left = that.judgeBoundary(e.offsetX, 0, width) + 'px'that.getAlpha();let initLeft = alphaPanelSliderThumb.offsetLeftlet initX = e.pageXdocument.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 = thislet alphaPanel = that.$refs.alphaPanellet alphaPanelSliderThumb = that.$refs.alphaPanelSliderThumblet { width } = alphaPanel.getBoundingClientRect()let alpha = (alphaPanelSliderThumb.offsetLeft / width).toFixed(2)that.alpha = alphathis.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