Vue3+ts实现一个颜色选择器(可以自定义)

本文详细描述了如何使用Vue3和TypeScript开发一个自定义颜色选择器,包括默认颜色列表、颜色面板的饱和度和色相滑块以及颜色预览和输入功能。
摘要由CSDN通过智能技术生成

Vue3+ts实现一个颜色选择器

最近需求需要用到颜色选择器,但是选用的ant-design-vue种并没有颜色选择器这个组件,所以就想着自己实现以下

效果图

vue代码

// ColorPicker.vue
<template>
  <div class="modal hu-color-picker">
    <div class="color-panel">
      <!-- 默认颜色列表选择区 -->
      <ul class="colors color-box">
        <li v-for="item in colorsDefault" :key="item" class="item" :style="{ background: item }"
          @click="selectColor(item)"></li>
      </ul>
      <div class="color-set">
        <!-- 颜色面板 -->
        <div class="saturation" @mousedown.prevent.stop="selectSaturation">
          <canvas ref="canvasSaturationRef" width="150" height="80"></canvas>
          <div :style="position.pointPosition" class="slide"></div>
        </div>
        <!-- 颜色卡条 -->
        <div class="hue" @mousedown.prevent.stop="selectHue">
          <canvas ref="canvasHueRef" width="12" height="80"></canvas>
          <div :style="position.slideHueStyle" class="slide"></div>
        </div>
      </div>
    </div>
    <!-- 颜色预览和颜色输入 -->
    <div class="color-view">
      <!-- 颜色预览区 -->
      <div :style="{ background: rgbString }" class="color-show"></div>
      <!-- 颜色输入区 -->
      <div class="input">
        <!-- <div class="color-type">
          <span class="name"> HEX </span>
          <input v-model="attr.modelHex" class="value" @blur="inputHex" />
        </div> -->
        <div class="color-type">
          <span class="name"> RGB </span>
          <input v-model="attr.modelRgb" class="value" @blur="inputRgb" />
        </div>
      </div>
    </div>
    <div class="btn">
      <button>清空</button>
      <button @click="changeColor">确认</button>
    </div>
  </div>
</template>
// ColorPicker.vue
<script setup lang="ts">
import { ref, reactive, computed, nextTick, onMounted, watch } from 'vue'
import {
  rgb2hex,
  createLinearGradient,
  hex2rgb,
  rgb2hsv,
  isHex,
  isRgb,
} from './ColorPicker'
const props = defineProps({
  initColor: {
    type: String,
    default: '#000000',
  },
})
const emits = defineEmits(['changeColor'])

// 默认颜色列表
const colorsDefault = reactive([
  '#ff7e79',
  '#fefe7f',
  '#00ff81',
  '#007ffe',
  '#ff80c0',
  '#ff0104',
  '#00fcff',
  '#847cc2',
  '#fe00fe',
  '#7e0101',
  '#fc7f01',
  '#027e04',
  '#65b2f3',
  '#f9b714',
  '#068081',
  '#8305a1',
  '#b0cf29',
  '#0bfa49',
  '#9e255e',
  '#ffffff',
])
const canvasSaturationRef = ref(null)
const canvasHueRef = ref(null)
const position = reactive({
  pointPosition: {
    top: '0px',
    left: '0px',
  },
  slideHueStyle: {},
})
const attr = reactive({
  modelRgb: '',
  modelHex: '',
  r: 0,
  g: 0,
  b: 0,
  h: 0,
  s: 0,
  v: 0,
})
const rgbString = computed(() => {
  return `rgb(${attr.r}, ${attr.g}, ${attr.b})`
})

// 渲染面板颜色
const renderSaturationColor = (color: string) => {
  const canvas: any = canvasSaturationRef.value
  const height = canvas.height
  const width = canvas.width
  const ctx = canvas.getContext('2d')
  ctx.fillStyle = color
  ctx.fillRect(0, 0, width, height)
  createLinearGradient('l', ctx, width, height, '#FFFFFF', 'rgba(255,255,255,0)')
  createLinearGradient('p', ctx, width, height, 'rgba(0,0,0,0)', '#000000')
}
// 颜色面板点击
const selectSaturation = (e: MouseEvent) => {
  const canvas: any = canvasSaturationRef.value
  const height = canvas.height
  const width = canvas.width
  let x = e.offsetX,
    y = e.offsetY
  if (x < 0) x = 0
  if (x > width) x = width
  if (y < 0) y = 0
  if (y > height) y = height
  position.pointPosition = {
    top: y - 5 + 'px',
    left: x - 5 + 'px',
  }
  var ctx = canvas.getContext('2d')
  var imageData = ctx.getImageData(Math.max(x - 5, 0), Math.max(0, y - 5), 1, 1)
  setRGBHSV(imageData.data)
  attr.modelHex = rgb2hex({ r: attr.r, g: attr.g, b: attr.b }, true)
}

// 渲染调色器颜色
const renderHueColor = () => {
  const canvas: any = canvasHueRef.value
  const ctx = canvas.getContext('2d')
  const width = canvas.width
  const height = canvas.height
  const gradient = ctx.createLinearGradient(0, 0, 0, height)
  gradient.addColorStop(0, '#FF0000') // red
  gradient.addColorStop(0.17 * 1, '#FF00FF') // purple
  gradient.addColorStop(0.17 * 2, '#0000FF') // blue
  gradient.addColorStop(0.17 * 3, '#00FFFF') // green
  gradient.addColorStop(0.17 * 4, '#00FF00') // green
  gradient.addColorStop(0.17 * 5, '#FFFF00') // yellow
  gradient.addColorStop(1, '#FF0000') // red
  ctx.fillStyle = gradient
  ctx.fillRect(0, 0, width, height)
}
// 颜色条选中
const selectHue = (e: any) => {
  const canvas: any = canvasHueRef.value
  const { top: hueTop, height } = canvas.getBoundingClientRect()
  const ctx = e.target.getContext('2d')
  const mousemove = (e: any) => {
    let y = e.clientY - hueTop
    if (y < 0) y = 0
    if (y > height) y = height
    position.slideHueStyle = {
      top: y - 2 + 'px',
    }
    // 先获取颜色条上的颜色在颜色面板上进行渲染
    const imgData = ctx.getImageData(0, Math.min(y, height - 1), 1, 1)
    const [r, g, b] = imgData.data
    renderSaturationColor(`rgb(${r},${g},${b})`)
    // 再根据颜色面板上选中的点的颜色,来修改输入框的值
    nextTick(() => {
      const canvas: any = canvasSaturationRef.value
      const ctx = canvas.getContext('2d')
      const pointX = parseFloat(position.pointPosition.left)
      const pointY = parseFloat(position.pointPosition.top)
      const pointRgb = ctx.getImageData(Math.max(0, pointX), Math.max(0, pointY), 1, 1)
      setRGBHSV(pointRgb.data)
      attr.modelHex = rgb2hex({ r: attr.r, g: attr.g, b: attr.b }, true)
    })
  }
  mousemove(e)
  const mouseup = () => {
    document.removeEventListener('mousemove', mousemove)
    document.removeEventListener('mouseup', mouseup)
  }
  document.addEventListener('mousemove', mousemove)
  document.addEventListener('mouseup', mouseup)
}

// 默认颜色选择区选择颜色
function selectColor(color: string) {
  setRGBHSV(color)
  attr.modelRgb = rgbString.value.substring(4, rgbString.value.length - 1)
  attr.modelHex = rgb2hex({ r: attr.r, g: attr.g, b: attr.b }, true)
  renderSaturationColor(rgbString.value)
  position.pointPosition = {
    left: Math.max(attr.s * 150 - 5, 0) + 'px',
    top: Math.max((1 - attr.v) * 80 - 5, 0) + 'px',
  }
  renderSlide()
}

// 调色卡的位置
const renderSlide = () => {
  position.slideHueStyle = {
    top: (1 - attr.h / 360) * 78 + 'px',
  }
}

// hex输入框失去焦点
function inputHex() {
  if (isHex(attr.modelHex)) {
    selectColor(attr.modelHex)
  } else {
    alert('请输入3位或者6位合法十六进制值')
  }
}
function inputRgb() {
  if (isRgb(attr.modelRgb)) {
    const [r, g, b] = attr.modelRgb.split(',')
    const hex = rgb2hex({ r, g, b }, true)
    attr.modelHex = hex
    selectColor(attr.modelHex)
  } else {
    alert('请输入合法的rgb数值')
  }
}

// color可能是 #fff 也可能是 123,21,11  这两种格式
function setRGBHSV(color: any, initHex = false) {
  let rgb: any = { r: '0', g: '0', b: '0' }
  if (typeof color !== 'string') {
    rgb = { r: color[0], g: color[1], b: color[2] }
  } else {
    if (!color.includes('#')) {
      const [r, g, b] = color.split(',')
      rgb = { r: r, g: g, b: b }
    } else {
      rgb = hex2rgb(color)
    }
  }
  const hsv = rgb2hsv(rgb)
  attr.r = rgb.r
  attr.g = rgb.g
  attr.b = rgb.b
  attr.h = hsv.h
  attr.s = hsv.s
  attr.v = hsv.v
  if (initHex) attr.modelHex = rgb2hex(rgb, true)
  attr.modelRgb = rgbString.value.substring(4, rgbString.value.length - 1)
}

// 确认选择的颜色
function changeColor() {
  if (!isHex(attr.modelHex) || !isRgb(attr.modelRgb)) {
    return
  } else {
    emits('changeColor', attr.modelHex)
  }
}

watch(
  () => props.initColor,
  (newVal, _) => {
    selectColor(newVal)
  },
)

onMounted(() => {
  selectColor(props.initColor)
  renderHueColor()
})
</script>
<style>
.hu-color-picker {
  width: 200px;
  background: #242c3e;
  border-radius: 4px;
  box-shadow: 0 0 16px 0 rgba(0, 0, 0, 0.16);
  z-index: 1;

  canvas {
    vertical-align: top;
  }

  .color-set {
    display: flex;
    margin-left: 5px;
    padding: 8px 0 8px 8px;
    border-left: 1px solid #323e53;
  }

  .color-show {
    height: 56px;
    width: 100px;
    margin: 8px auto;
    display: flex;
  }
}

.color-panel {
  display: flex;
  padding: 8px 8px 0 8px;

  .color-box {
    width: 100px;
  }
}

.color-view {
  display: flex;
  padding: 0 8px;
}

.input {
  flex: 1;
  margin-left: 8px;
}

// 颜色面板
.saturation {
  position: relative;
  cursor: pointer;

  .slide {
    position: absolute;
    left: 100px;
    top: 0;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    border: 1px solid #fff;
    box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.3);
    pointer-events: none;
  }
}

// 颜色调节条
.hue {
  position: relative;
  margin-left: 8px;
  cursor: pointer;

  .slide {
    box-sizing: border-box;
    position: absolute;
    left: 0;
    width: 100%;
    height: 4px;
    background: #fff;
    border: 1px solid #f0f0f0;
    box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
    pointer-events: none;
    border-radius: 1px;
  }
}

.color-type {
  display: flex;
  margin: 8px auto;
  font-size: 12px;

  .name {
    width: 32px;
    height: 24px;
    float: left;
    display: flex;
    justify-content: center;
    align-items: center;
    color: #a0acc0;
  }

  .value {
    display: block;
    box-sizing: border-box;
    flex: 1;
    height: 24px;
    padding: 0 8px;
    border: 1px solid #42516c;
    color: #eff0f4;
    background: #2e3850;
    box-sizing: border-box;
    width: 100px;
    caret-color: #49a4ff;

    &:focus-visible {
      outline: 1px solid rgba(18, 107, 190, 0.5);
    }
  }
}

// 默认颜色
.colors {
  display: flex;
  flex-wrap: wrap;
  padding: 0;
  margin: 0;

  .item {
    flex-basis: calc(20% - 4px);
    margin: 2px;
    width: 16px;
    height: 16px;
    border-radius: 1px;
    box-sizing: border-box;
    vertical-align: top;
    display: inline-block;
    transition: all 0.1s;
    cursor: pointer;

    &:hover {
      transform: scale(1.2);
    }
  }
}

.btn {
  margin-top: 0 8px 8px;
  border-top: 1px solid #323e53;
  text-align: right;
  padding: 8px;

  button {
    margin-left: 8px;
    width: 52px;
    height: 20px;
    font-size: 12px;
    font-weight: 400;
    color: #fff;
    background-color: #49a4ff;
    border-radius: 2px;
    border: none;

    &:active {
      background-color: #1890ff;
    }
  }
}
</style>

ColorPicker.ts

// ColorPicker.ts
// rgb转hex
export function rgb2hex({ r, g, b }: any, toUpper: boolean) {
  const change = (val: any) => ('0' + Number(val).toString(16)).slice(-2)
  const color = `#${change(r)}${change(g)}${change(b)}`
  return toUpper ? color.toUpperCase() : color
}

export function createLinearGradient(
  direction: any,
  ctx: any,
  width: any,
  height: any,
  color1: any,
  color2: any,
) {
  // l horizontal p vertical
  const isL = direction === 'l'
  const gradient = ctx.createLinearGradient(0, 0, isL ? width : 0, isL ? 0 : height)
  gradient.addColorStop(0.01, color1)
  gradient.addColorStop(0.99, color2)
  ctx.fillStyle = gradient
  ctx.fillRect(0, 0, width, height)
}

// hex转rgb
export function hex2rgb(hex: any) {
  hex = hex.slice(1)
  if (hex.length === 3) {
    hex = '' + hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
  }
  const change = (val: any) => parseInt(val, 16) || 0 // Avoid NaN situations
  return {
    r: change(hex.slice(0, 2)),
    g: change(hex.slice(2, 4)),
    b: change(hex.slice(4, 6)),
  }
}

// rgb转hsv
export function rgb2hsv({ r, g, b }: any) {
  r = r / 255
  g = g / 255
  b = b / 255
  const max = Math.max(r, g, b)
  const min = Math.min(r, g, b)
  const delta = max - min
  let h = 0
  if (max === min) {
    h = 0
  } else if (max === r) {
    if (g >= b) {
      h = (60 * (g - b)) / delta
    } else {
      h = (60 * (g - b)) / delta + 360
    }
  } else if (max === g) {
    h = (60 * (b - r)) / delta + 120
  } else if (max === b) {
    h = (60 * (r - g)) / delta + 240
  }
  h = Math.floor(h)
  const s = parseFloat((max === 0 ? 0 : 1 - min / max).toFixed(2))
  const v = parseFloat(max.toFixed(2))
  return { h, s, v }
}

// 验证输入的hex是否合法
export function isHex(str: string) {
  return /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/.test(str)
}
// 验证输入的rgb是否合法
export function isRgb(str: string) {
  const regex = /^(\d{1,3}),\s?(\d{1,3}),\s?(\d{1,3})$/ // 匹配rgb格式的正则表达式
  const match = str.match(regex) // 使用match方法进行匹配
  if (match) {
    // 如果匹配成功
    const r = parseInt(match[1]) // 获取红色值
    const g = parseInt(match[2]) // 获取绿色值
    const b = parseInt(match[3]) // 获取蓝色值
    if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
      // 判断RGB值是否在合法范围内
      return true // 如果合法,返回true
    }
  }
  return false // 如果不合法,返回false
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值