阅读 2162

前端实现一个颜色选择器的功能

先让我找找思路

虽然我们有现成的HTML控件,但是有两点可以让我们放弃它。

  • 功能无法定制
  • 不同浏览器样式不统一

所以因此我们就自己来实现一个吧~

老规矩,没思路找项目,这次我找到的项目叫做Iconfont。

然后再找一个有缘图标。

咳,不要在意图标本身,我们可以发现这里有一个颜色选择器,我们来点开它来看一看。

可以看出这个颜色选择器拥有三个部分

  • 颜色存储板
  • 颜色选择区
  • 输入框and确认按钮

先来颜色选择区是如何实现的,毕竟这是颜色选择器的核心。

可以看出它是通过svg来实现的,svg是什么这里就不赘述了。

标记一的颜色块是通过svg的两个rect来定义的。

<rect x="0" y="0" width="100%" height="100%" fill="url(#gradient-white)"></rect>
<rect x="0" y="0" width="100%" height="100%" fill="url(#gradient-black)"></rect>
复制代码

xywidthheight这四个参数将这个颜色块设置为整个父元素的宽高。

那么fill="url(#gradient-white)"这个参数代表着什么呢?

经过我搜索我得出如下结果:

这是为了实现渐变的效果,可以将url(#gradient-white)对应元素的渐变设置到rect上。

#gradient-white是一个svgdefs一个叫做linearGradient的元素。

<defs>是个嘛玩意:

SVG 允许我们定义以后需要重复使用的图形元素。 建议把所有需要再次使用的引用元素定义在defs元素里面。这样做可以增加SVG内容的易读性和可访问性。 在defs元素中定义的图形元素不会直接呈现。 你可以在你的视口的任意地方利用<use>元素呈现这些元素。- MDN

<linearGradient>是个嘛玩意:

linearGradient元素用来定义线性渐变,用于图形元素的填充或描边。- MDN

Iconfont中我们也可以找到对应的元素。

可以看出有两个不同的渐变,一个是从白开始,一个是从黑开始。

这里为什么要有两个渐变?

正常情况下:

将黑色渐变块隐藏掉后:

对比两者后,我们不难发现,黑色渐变块是从上往下,白色渐变块是从左往右,两者重叠就是我们看到的那种效果。

接下来看看它的<linearGradient>是如何定义的。

<defs>
    <!-- 渐变1 -->
    <linearGradient id="gradient-black" x1="0%" y1="100%" x2="0%" y2="0%">
    	<stop offset="0%" stop-color="#000000" stop-opacity="1"></stop>
        <stop offset="100%" stop-color="#CC9A81" stop-opacity="0"></stop>
    </linearGradient>
    <!-- 渐变2 -->
    <linearGradient id="gradient-white" x1="0%" y1="100%" x2="100%" y2="100%">
    	<stop offset="0%" stop-color="#FFFFFF" stop-opacity="1"></stop>
        <stop offset="100%" stop-color="#CC9A81" stop-opacity="0"></stop>
    </linearGradient>
</defs>
复制代码

<linearGradient> 标签的 x1、x2、y1、y2 属性可定义渐变的开始和结束位置

渐变的颜色范围可由两种或多种颜色组成。每种颜色通过一个 <stop> 标签来规定。offset 属性用来定义渐变的开始和结束位置。 - w3school

元素<stop>的定义:

一个渐变上的颜色坡度,是用stop元素定义的。stop元素可以是<linearGradient>元素或者<radialGradient>元素的子元素。 - MDN

那么<stop>上面的stop-color以及stop-opacity是什么呢?

其实顾名思义,就是渐变停止处的颜色以及透明度。

那么由此可见的:

  • 渐变1
    • <linearGradient>定义的x1="0%" y1="100%" x2="0%" y2="0%"表示从左下到左上的线性渐变
    • 第一个<stop>定义的offset="0%" stop-color="#FFFFFF" stop-opacity="1"表示开始位置的颜色为#FFFFFF、透明度为1
    • 第二个<stop>定义的offset="100%" stop-color="#CC9A81" stop-opacity="0"表示结束位置的颜色为#CC9A81、透明度为0
  • 渐变2
    • <linearGradient>定义的x1="0%" y1="100%" x2="100%" y2="100%"表示从左下到右下的线性渐变

再看看右边那个长条形的颜色块是怎么实现的:

<linearGradient id="gradient-hsv" x1="0%" y1="100%" x2="0%" y2="0%">
    <stop offset="0%" stop-color="#FF0000" stop-opacity="1"></stop>
    <stop offset="13%" stop-color="#FF00FF" stop-opacity="1"></stop>
    <stop offset="25%" stop-color="#8000FF" stop-opacity="1"></stop>
    <stop offset="38%" stop-color="#0040FF" stop-opacity="1"></stop>
    <stop offset="50%" stop-color="#00FFFF" stop-opacity="1"></stop>
    <stop offset="63%" stop-color="#00FF40" stop-opacity="1"></stop>
    <stop offset="75%" stop-color="#0BED00" stop-opacity="1"></stop>
    <stop offset="88%" stop-color="#FFFF00" stop-opacity="1"></stop>
    <stop offset="100%" stop-color="#FF0000" stop-opacity="1"></stop>
</linearGradient>
复制代码

这里的话,运用上面我们学到知识可以很轻松的明白它是个什么意思了。

现在我会画了,该怎样获取指定位置的颜色呢?

通过报错,我得知了它是使用HSV颜色模型来设置的。

这里值得一提的是,认识阿里大佬的赶紧让他修bug了,颜色块拖动不了,一拖就报错。

那么HSV颜色模型是什么东西呢?

HSV(Hue, Saturation, Value)是根据颜色的直观特性由A. R. Smith在1978年创建的一种颜色空间, 也称六角锥体模型(Hexcone Model)。

这个模型中颜色的参数分别是:色调(H),饱和度(S),明度(V)。 - 百度百科

  • 色调H使用角度度量,取值范围0°~360°
  • 饱和度S表示颜色接近光谱色的程度,取值范围0%~100%
  • 明度V表示颜色的明亮程度,取值范围0%~100%

由上面的取值范围通过计算就可以得出我们想要的颜色了。

那么颜色该如何计算呢?

我们先抛弃Iconfont的代码,毕竟经过混淆了,看得有点累。

首先第一步,我们需要将HSV中的H色调给整出来,也就是右边那个长条形的颜色块。

这里是一个简单实现:

// index.html
<div class="H">
  <svg>
    <defs>
      <linearGradient id="gradient-hsv" x1="0%" y1="100%" x2="0%" y2="0%">
        <stop offset="0%" stop-color="#FF0000" stop-opacity="1"></stop>
        <stop offset="13%" stop-color="#FF00FF" stop-opacity="1"></stop>
        <stop offset="25%" stop-color="#8000FF" stop-opacity="1"></stop>
        <stop offset="38%" stop-color="#0040FF" stop-opacity="1"></stop>
        <stop offset="50%" stop-color="#00FFFF" stop-opacity="1"></stop>
        <stop offset="63%" stop-color="#00FF40" stop-opacity="1"></stop>
        <stop offset="75%" stop-color="#0BED00" stop-opacity="1"></stop>
        <stop offset="88%" stop-color="#FFFF00" stop-opacity="1"></stop>
        <stop offset="100%" stop-color="#FF0000" stop-opacity="1"></stop>
      </linearGradient>
    </defs>
    <rect x="0" y="0" width="100%" height="100%" fill="url(#gradient-hsv)"></rect>
  </svg>
  <div class="slide"></div>
</div>

<div class="color">
  <h2>rgb(102, 81, 192)</h2>
  <div class="show" style="background: rgb(102, 81, 192);">

  </div>
</div>
复制代码
// style.css
svg{
  width: 100%;
  height: 100%;
}

.H{
  width: 20px;
  height: 200px;
  position: relative;
}

.slide{
  position: absolute;
  left: 4px;
  top: -8px;
  border: 8px solid transparent;
  border-right-color: #888;
  width: 0;
  height: 0;
  pointer-events: none;
}

.show{
  width: 300px;
  height: 100px;
}
复制代码
// app.js
let H = document.querySelector('.H');
let HRect = H.querySelector('rect');
let HSlide = H.querySelector('.slide')
let HFlag = false;

let Hval = 0;
let Sval = 100;
let Vval = 100;

HRect.addEventListener('mousedown', () => {
  HFlag = true;
})
HRect.addEventListener('mousemove', ev => {
  if(!HFlag) return;

  // ev.offsetY这个值为鼠标相对于源元素的Y坐标,算出滑块在元素中的定位比例
  let offsetY = ev.offsetY / H.offsetHeight;

  HSlide.style.top = ev.offsetY - 8 + 'px'

  // 因为H值的范围是0~360,乘以比例就可以得出一个颜色值了
  Hval = 360 * offsetY;

  setHSV();
})
HRect.addEventListener('mouseup', () => {
  HFlag = false;
})

let colorEl = document.querySelector('.color');
let colorTitle = colorEl.querySelector('h2');
let colorShow = colorEl.querySelector('.show');

function setHSV(){
  // 这里算出对应的RGB值
  let color = `
    rgb(${hsvtorgb(Hval, Sval, Vval).join(',')})
  `

  colorTitle.innerHTML = color;
  colorShow.style.background = color;
}

setHSV();
复制代码

还有一个函数hsvtorgb,因为这个代码是我拷贝别人的,就另外贴出来吧。

来源于: www.cnblogs.com/brainworld/…

function hsvtorgb(h, s, v) {
  s = s / 100;
  v = v / 100;
  var h1 = Math.floor(h / 60) % 6;
  var f = h / 60 - h1;
  var p = v * (1 - s);
  var q = v * (1 - f * s);
  var t = v * (1 - (1 - f) * s);
  var r, g, b;
  switch (h1) {
    case 0:
      r = v;
      g = t;
      b = p;
      break;
    case 1:
      r = q;
      g = v;
      b = p;
      break;
    case 2:
      r = p;
      g = v;
      b = t;
      break;
    case 3:
      r = p;
      g = q;
      b = v;
      break;
    case 4:
      r = t;
      g = p;
      b = v;
      break;
    case 5:
      r = v;
      g = p;
      b = q;
      break;
  }
  return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
复制代码

实际上我看不懂上面的算法。狗头🐶

上面的代码实现了如下效果:

然后我们实现出HSV中代表SV的颜色块

因为不想写太长,我就将修改部分的代码展示出来:

// index.html
<div class="HSV">
  <div class="SV">
    <svg>
      <defs>
        <linearGradient id="gradient-black" x1="0%" y1="100%" x2="0%" y2="0%">
          <stop offset="0%" stop-color="#000000" stop-opacity="1"></stop>
          <!-- endColor -->
          <stop class="endColor" offset="100%" stop-color="#CC9A81" stop-opacity="0"></stop>
        </linearGradient>
        <linearGradient id="gradient-white" x1="0%" y1="100%" x2="100%" y2="100%">
          <stop offset="0%" stop-color="#FFFFFF" stop-opacity="1"></stop>
          <!-- endColor -->
          <stop class="endColor" offset="100%" stop-color="#CC9A81" stop-opacity="0"></stop>
        </linearGradient>
      </defs>
      <rect x="0" y="0" width="100%" height="100%" fill="url(#gradient-white)"></rect>
      <rect x="0" y="0" width="100%" height="100%" fill="url(#gradient-black)"></rect>
    </svg>
    <div class="slide"></div>
  </div>

  <div class="H">
  	// ......
  </div>
</div>
复制代码
// style.css
.HSV{
  display: flex;
}

.SV{
  width: 200px;
  margin-right: 10px;
}
复制代码
// app.js
let SV = document.querySelector('.SV');
// 这里我们选择第二个矩形,这样就不会被另一个矩形给遮住点击事件
let SVRect = SV.querySelector('rect:last-child');
let SVSlide = SV.querySelector('.slide');
let SVFlag = false;

SVRect.addEventListener('mousedown', () => {
  SVFlag = true;
})
SVRect.addEventListener('mousemove', ev => {
  if(!SVFlag) return;

  // 一波换算得出滑块的比例然后乘以100就是对应的SV的值了
  Sval = ev.offsetX / SV.offsetWidth * 100;
  // 透明度因为方向的问题所以就反转一下
  Vval = (1 - ev.offsetY / SV.offsetHeight) * 100;

  SVSlide.style.left = ev.offsetX + 3 + 'px';
  SVSlide.style.top = ev.offsetY + 3 + 'px';

  setHSV();
})
SVRect.addEventListener('mouseup', () => {
  SVFlag = false;
})

let endColors = document.querySelectorAll('.endColor');

function setHSV(){
  // 这里算出对应的RGB值
  // 这个是最终结果的颜色
  let color = `
    rgb(${hsvtorgb(Hval, Sval, Vval).join(',')})
  `;
  // 这个是给SV设置的结束颜色
  let StopColor = `
    rgb(${hsvtorgb(Hval, 100, 100).join(',')})
  `;

  // 为在页面中的rect设置结束颜色
  [...endColors].map(el => el.setAttribute('stop-color', StopColor));

  colorTitle.innerHTML = color;
  colorShow.style.background = color;
}
复制代码

由上面的代码,我可以得出以下效果:

实际上,上面的代码还不完全,从下面的图我们可以看出它的两端都是白色。

我找了半天svg的问题,为啥跟Iconfont的结果不一样...

然后我发现,原来Iconfont给整个颜色块有一个纯色的背景色

我们加上去瞅一瞅效果吧。

// app.js
function setHSV(){
  // ...

  SV.style.background = StopColor;
  
  // ...
}
复制代码

到这里,颜色选择器的核心功能就大功告成了。

结束了

虽然还有另外两个版块的功能,但是因为不想写太长的文章就到这里结束吧。

虽然这篇文章结束了,但是你还可以继续完善它的功能,比如:

  • 完成其他两个版块的功能:颜色存储板、输入框
  • 将RGB转换成16进制
  • 将它编写成弹出框
  • 使用Vue组件化它

最后感谢你的阅读。