Saturation.vue 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. <template>
  2. <div
  3. class="saturation"
  4. ref="saturationRef"
  5. :style="{ background: bgColor }"
  6. @mousedown="$event => handleMouseDown($event)"
  7. >
  8. <div class="saturation-white"></div>
  9. <div class="saturation-black"></div>
  10. <div class="saturation-pointer"
  11. :style="{
  12. top: pointerTop,
  13. left: pointerLeft,
  14. }"
  15. >
  16. <div class="saturation-circle"></div>
  17. </div>
  18. </div>
  19. </template>
  20. <script lang="ts" setup>
  21. import { computed, onUnmounted, useTemplateRef } from 'vue'
  22. import tinycolor, { type ColorFormats } from 'tinycolor2'
  23. import { throttle, clamp } from 'lodash'
  24. const props = defineProps<{
  25. value: ColorFormats.RGBA
  26. hue: number
  27. }>()
  28. const emit = defineEmits<{
  29. (event: 'colorChange', payload: ColorFormats.HSVA): void
  30. }>()
  31. const color = computed(() => {
  32. const hsva = tinycolor(props.value).toHsv()
  33. if (props.hue !== -1) hsva.h = props.hue
  34. return hsva
  35. })
  36. const bgColor = computed(() => `hsl(${color.value.h}, 100%, 50%)`)
  37. const pointerTop = computed(() => (-(color.value.v * 100) + 1) + 100 + '%')
  38. const pointerLeft = computed(() => color.value.s * 100 + '%')
  39. const emitChangeEvent = throttle(function(param: ColorFormats.HSVA) {
  40. emit('colorChange', param)
  41. }, 20, { leading: true, trailing: false })
  42. const saturationRef = useTemplateRef<HTMLElement>('saturationRef')
  43. const handleChange = (e: MouseEvent) => {
  44. e.preventDefault()
  45. if (!saturationRef.value) return
  46. const containerWidth = saturationRef.value.clientWidth
  47. const containerHeight = saturationRef.value.clientHeight
  48. const xOffset = saturationRef.value.getBoundingClientRect().left + window.pageXOffset
  49. const yOffset = saturationRef.value.getBoundingClientRect().top + window.pageYOffset
  50. const left = clamp(e.pageX - xOffset, 0, containerWidth)
  51. const top = clamp(e.pageY - yOffset, 0, containerHeight)
  52. const saturation = left / containerWidth
  53. const bright = clamp(-(top / containerHeight) + 1, 0, 1)
  54. emitChangeEvent({
  55. h: color.value.h,
  56. s: saturation,
  57. v: bright,
  58. a: color.value.a,
  59. })
  60. }
  61. const unbindEventListeners = () => {
  62. window.removeEventListener('mousemove', handleChange)
  63. window.removeEventListener('mouseup', unbindEventListeners)
  64. }
  65. const handleMouseDown = (e: MouseEvent) => {
  66. handleChange(e)
  67. window.addEventListener('mousemove', handleChange)
  68. window.addEventListener('mouseup', unbindEventListeners)
  69. }
  70. onUnmounted(unbindEventListeners)
  71. </script>
  72. <style lang="scss" scoped>
  73. .saturation,
  74. .saturation-white,
  75. .saturation-black {
  76. @include absolute-0();
  77. cursor: pointer;
  78. }
  79. .saturation-white {
  80. background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
  81. }
  82. .saturation-black {
  83. background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
  84. }
  85. .saturation-pointer {
  86. cursor: pointer;
  87. position: absolute;
  88. }
  89. .saturation-circle {
  90. width: 4px;
  91. height: 4px;
  92. box-shadow: 0 0 0 1.5px #fff, inset 0 0 1px 1px rgba(0, 0, 0, .3), 0 0 1px 2px rgba(0, 0, 0, .4);
  93. border-radius: 50%;
  94. transform: translate(-2px, -2px);
  95. }
  96. </style>