如何实现一个颜色选择器

在开发公司UI组件库的过程当中,颜色组件ColorPicker因为时间关系没有去深刻研究,看着花花绿绿的色谱图,觉得实现起来会很复杂,就直接将一个开源的颜色选择器封装了一下。这大概是半年之前的事了,这篇文章也在个人博客中以草稿形式(只有标题没有内容,当时是作了一个记录,想着后来有时间了去研究,目前这种草稿还有不少😂)存放了半年了。前段时间请教了公司UI同事一些颜色相关的概念,又去搜索了下这方面的知识,收获仍是蛮大的,尤为是知乎色彩空间中的 HSL、HSV、HSB 有什么区别?这篇提问中,@Forrest近乎大白话的回答,超赞👍。javascript


在了解了相关概念以后,再去审视这个组件的话,就不会有那种陌生行业的抵触感了。最终也实现了这个这个组件,DEMO,并将实现过程记录以下:php

【解析Photoshop调色器】小章节是基于维基百科和知乎文章对一些颜色概念的总结。开发视角总结的可能并不太专业,了解便可。css

解析Photoshop调色器

PhotoShop拾色器

HSL、HSV、HSB的区别

做为前端,咱们了解HEX,了解RGB,但对于HSL、HSV、HSB这几个颜色概念每每是模糊不清的。如上图Photoshop的调色器,其中下部分别有HSBRGB等几个输入框,RGB分别表明红绿蓝三原色的颜色通道,经过对这三个颜色通道的变化以及相互之间的叠加能够获得各类不一样的颜色,这是咱们所熟知的。那么HSB又是什么呢?html

事实上,HSBRGB同样,也是一种颜色模式,其中H(hue)表示色相(什么颜色),S(saturation)表示饱和度(颜色的纯度),B(brightness)表示明度亮度(颜色的明亮程度)。相较于RGB模式,HSB颜色模式更加人性化,它定义了颜色“是什么颜色?颜色艳不艳?颜色亮不亮?”。前端

什么是色相

色相就是在不一样的光照下,人眼所感受不一样的颜色。 ”赤橙黄绿青蓝紫,谁持彩练当空舞?“,伟人的诗词早就描绘过了。 在HSV/HSL色彩模式下, 色相是以红色为0度(360度),黄色为60度,绿色为120度,青色为180度,蓝色为240度,品红色为300度的表现,其值范围为0 - 360度,以下图: java

色相

什么是饱和度

饱和度是指颜色的强度或纯度。饱和度表示色相中彩色成分所占的比例,用从 0 (透明) ~ 100% (彻底饱和)的百分比来度量。纯度是色彩感受强弱的标志,纯度高的颜色人眼看上去显得比较鲜艳。git

什么是明度

明度是颜色的相对明暗程度。一般是从 0 (黑) ~ 100% (白)的百分比来度量的。github


一样的,HSLHSVHSB同样,都是基于色相、饱和度、明度三方面对色彩的解释。其中HSB 和 HSV 是同一个东西,只是叫法上不一样。后面咱们统一使用HSV来表示。正则表达式

对三种颜色模式的区别,知乎上(文末附连接)有个很浅显易懂的回答:算法

在原理和表现上,HSL 和 HSB 中的 H(色相) 彻底一致,但两者的 S(饱和度)不同, L 和 B (明度 )也不同:

  • HSB 中的 S 控制纯色中混入白色的量,值越大,白色越少,颜色越纯;
  • HSB 中的 B 控制纯色中混入黑色的量,值越大,黑色越少,明度越高
  • HSL 中的 S 和黑白没有关系,饱和度不控制颜色中混入黑白的多寡;
  • HSL 中的 L 控制纯色中的混入的黑白两种颜色。

HSV的三个值表达范围分别为: H [0-360] float S [0-1] float V [0-1] float

上述概念,可能并不太专业,了解便可。 用程序思惟来解释上述概念的话,就是说咱们在一个颜色中混入不一样程度的黑和白就能变为另一种颜色值,以下图:

PhotoShop拾色器
也就是说,抛开 RGB三个颜色通道以外,还能够经过色相 H、饱和度 S、明度 V另外三个通道来表达颜色。

实现流程

color-picker效果图
如上效果图,就是最终要实现的颜色选择器的效果。整个布局由饱和度/明度面板、色相面板、透明度面板三个元件组成,三个面板都是滑块模型,其中饱和度/明度面板上的滑块能够在其面板上自由滑动,而色相和透明度面板上的滑块仅容许左右滑动。滑块滑动最终产生的坐标值将是用来计算 HSV值的依据。 “夫君在手,天下我有”。基于上述概念和思路,咱们只须要经过坐标计算出色相 H、饱和度 S、明度 V三个值,就能够将其转换为指定格式的颜色。
流程图

  • 饱和度/明度面板由饱和度和明度两层组成,滑块坐标值的Left值用来计算饱和度,Top值用来计算明度;
  • 色相面板的滑块坐标值的Left值用来计算色相值;
  • 透明度面板的滑块坐标值的Left值用来计算透明度,透明度做为颜色的一种补充,和颜色自己没有任何关系;
  • 最终产生的HSVA四个值将经过不一样算法转换成不一样的颜色格式;

饱和度/亮度面板的实现

通俗点来讲,饱和度就是在一个纯色中“混入”了百分之多少的白色,明度就是在一个纯色中“混入”了百分之多少的黑色;这些量都用百分比来表示,其值范围在0% ~ 100%

饱和度是从左往右,混入的白色越少,表示颜色的纯度越高。也就是在面板最左边混入的白色达到纯白的峰值,而最右边混入的白色是接近于无的透明,这就是一条从左往右,由白色到透明的径向渐变;一样的道理能够得出,明度能够由一条从下往上,由纯黑到彻底透明的径向渐变表示。因为这两条渐变大部分区域是透明的,所以,当在这两层的下方设置赤橙黄绿青蓝紫等不一样色相时,整个区域就会造成一个色谱。以下图所示:

饱和度/亮度面板的实现原理
为了更方便理解,我录制了一个小视频,经过调整面板元素的背景色,能够更清晰的表现出这两条径向渐变覆盖在一个纯色背景色上产生的效果。 在清楚了色谱的造成过程以后,饱和度/亮度面板的实现就变得容易多了,为了简化结构,直接使用 ::before/::after伪元素来实现饱和度层和明度层。

布局
<div class="mo-color-sat-val">
  <!-- 饱和度渐变元素 -->
  ::before
  <!-- 滑块 -->
  <div class="mo-color-thumb" role="slider" tabindex="0">
    <span></span>
  </div>
  <!-- 明度渐变元素 -->
  ::after
</div>
复制代码
.mo-color-sat-val {
  position: relative;
  // 色相将直接做用于面板的背景色上
  background: transparent;

  &::before,
  &::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }

  // 饱和度 一条从左往右,由纯白到透明的径向渐变
  &::before {
    background: linear-gradient(to right, white, transparent);
  }

  // 明度 一条从下往上,由纯黑到透明的径向渐变
  &::after {
    background: linear-gradient(to top, black, transparent);
  }
}
复制代码
经过坐标计算S值和V值

当滑块在面板上自由滑动时,滑块所在的位置就是计算S值和V值所须要的坐标值。如何滑动和计算坐标不是本文的重点,能够直接参考源码,我已将其封装为一个Draggable类,该类会在滑动过程当中产生一个坐标,而且经过一系列钩子函数返回实例。

// 初始化$sat面板的Draggable类
states.satDragIns = new Draggable(states.$sat, {
  drag: satDrag,
  end: satDrag
})

function satDrag (coordinate: Coordinate) {
  // satWidth 面板的宽度
  // satHeight 面板的高度
  // 饱和度和明度的范围都是从 0 - 100 的一个百分比值,为了便于计算,这里直接保存为小数, 坐标原点是左下角
  const saturation = Math.round(coordinate.left / satWidth * 100) / 100
  const value = Math.round((1 - coordinate.top / satHeight) * 100) / 100
}

复制代码
色相改变时重置面板背景色

饱和度/亮度面板的最后一环就是当色相改变时,要去设置面板的背景色,以便造成最终的色谱(如上面视频效果)。只须要将色相改变后生成的H值(参考色相的实现)生成一个hsl格式的颜色便可:

states.$sat.style.background = `hsl(${h}, 100%, 50%)`
复制代码

色相的实现

色相
能够看出,HSV色彩空间下,色相条由红色、黄色、绿色、青色、蓝色、品红色六中颜色渐变过渡造成,最终再由平红色过渡到红色闭环。将其还原成环状可能更容易理解(黑色小点表示角度):
色相环
那么,只要计算出每一个颜色的角度就能够实现这个渐变条,渐变从红色开始(0度),从红色闭环(360度);中间5个色值依次平分角度:

0 deg: 红色
1/6 deg: 黄色
2/6 deg: 绿色
...
5/6 deg: 品红色
360 deg: 红色
复制代码
布局

转换成最终的CSS代码以下:

// 色相
.mo-color-hue>.mo-color-rail {
  background: linear-gradient(to right, #f00 0%, #ff0 16.66%, #0f0 33.33%, #0ff 50%, #00f 66.66%, #f0f 83.33%, #f00 100%);
}
复制代码
<div class="mo-color-hue">
  <!-- 渐变条 -->
  <div class="mo-color-rail"></div>
  <!-- 滑块 -->
  <div class="mo-color-thumb" role="slider" tabindex="0">
    <span></span>
  </div>
</div>
复制代码
经过坐标计算H值
// 经过left坐标和hue元素宽度计算出比例值,而后乘以360度计算出当前位置所处的度数
hue = Math.round((left / hueWidth) * 360 * 100) / 100
复制代码

透明度的实现

透明度面板的实现是最简单的,格子背景一样使用CSS渐变来生成,具体能够参考CSS大神张鑫旭的这篇文章《CSS届的绘图板CSS Paint API简介》。

布局
<!-- 格子背景直接做用在容器上 -->
<div class="mo-color-alpha">
  <!-- 颜色背景层 -->
  <div class="mo-color-rail"></div>
  <div class="mo-color-thumb" role="slider" tabindex="0">
    <span></span>
  </div>
</div>
复制代码
.mo-color-alpha {
  background-color: white;
  background-image: linear-gradient(45deg, #c5c5c5 25%, transparent 0, transparent 75%, #c5c5c5 0, #c5c5c5), linear-gradient(45deg, #c5c5c5 25%, transparent 0, transparent 75%, #c5c5c5 0, #c5c5c5);
  background-size: 10px 10px;
  background-position: 0 0, 5px 5px;
}
复制代码
经过坐标计算透明度值
alpha = Math.round(left / alpWidth * 100) / 100
复制代码
色相改变时更改颜色层的背景色
const hsl = hsv2hsl(h, s, v)
states.$alpRail.style.background = `linear-gradient(to right, transparent, hsl(${hsl.h}, ${hsl.s * 100}%, ${hsl.l * 100}%))`
复制代码

如上,咱们获得了生成一个颜色所必需的HSV三个重要的参数。有了这三个参数,就能够经过颜色转换算法,如hsv2rgb等,将hsv三个参数转换为不一样格式的颜色值,至于转换算法,本文就再也不展开,这个网站详细的罗列了各类色彩的转换算法,能够参考。

小技巧:如何验证一个颜色是否有效

不可忽略的是,颜色选择器可能会接收来自用户的传入值,而且ColorPicker组件自己也内置了setValue方法,咱们没法保证用户传入的是不是一个有效的颜色值,那么如何去验证这个颜色是否有效呢?起初我考虑用正则表达式去校验,但因为又会涉及到如传入rgb(260, 260, 260)这种某个通道超出范围的值,校验起来比较困难。而后我在Chrome浏览器中给某个元素设置了一个不规范的颜色,发现Chrome将这个颜色自动降级为白色了,那么,若是将一个错误颜色赋值给一个DOM的color样式,而后再获取这个color,再去验证获取到的color和传入的color是否一致是否能够作为校验的依据呢?Chrome浏览器经过了这个小测验,其余浏览器没有测试。

测试用例较少,该技巧可能存在问题。

/** * 校验颜色是否合法 * 测试用例较少,该技巧可能存在问题 * * 原理,若是颜色不合法,将会被转换为rgb(255,255,255) * 只需验证设置后的颜色是否等于传入的颜色 * @param color */
function checkColor(color: string) {
  // todo
  const style = new Option().style
  style.color = color
  return style.color === color 
}

// 只负责将传入的值转换为 h, s, v 三个通道值,而不去校验值是否超出范围或有效
function parseColor(color) {
  // hex2hsv
  if (HEX_REG.test(color)) {}
  // rgb2hsv
  if (RGB_REG.test(color)) {}
  // ...

  return {
    h,
    s,
    v,
    a
  }
}

setValue (color) {
  const { h, s, v, a } = parseColor(color)
  const {r, g, b} = hsv2rgb(h, s, v,)
  if (checkColor(`rgb(${r},${g},${b})`)) {
    // todo
  }
}

复制代码

源码

本文首发于个人博客

参考文献

相关文章
相关标签/搜索