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

最近需求需要用到颜色选择器,但是选用的ant-design-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>
样式
  .hu-color-picker {
    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;
      }
    }
  }
交互逻辑
// 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>

渲染颜色以及颜色转换的方法

// 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
}
GitHub 加速计划 / vu / vue
207.54 K
33.66 K
下载
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:2 个月前 )
73486cb5 * chore: fix link broken Signed-off-by: snoppy <michaleli@foxmail.com> * Update packages/template-compiler/README.md [skip ci] --------- Signed-off-by: snoppy <michaleli@foxmail.com> Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 4 个月前
e428d891 Updated Browser Compatibility reference. The previous currently returns HTTP 404. 5 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐