颜色选择器组件的实现
一、实现效果
点击颜色预览块,弹出操作面板,其中包括:
- 颜色面板
- 取色器
- 色相柱(即色阶柱)
- 透明度柱
- 颜色类型选择器
- 各颜色值输入框
- 预设颜色组
二、实现过程
1、点击弹出操作
先实现一个 Popup
弹窗组件,组件包裹颜色预览块,操作面板作为弹窗内容。
接下来是操作面板中各个部分的实现。
2、颜色面板的展示
颜色面板由面板底色及拖拽滑块组成,这里先来看面板底色。
面板底色由三层背景色叠加,分别为:
- 色相颜色层
- 白色渐变层
- 黑色渐变层
结构如下:
<section className="i-color-panel-block">
<div className="i-color-panel-block__white" />
<div className="i-color-panel-block__black" />
</section>
其中,父级可直接作为色相颜色层,颜色只由色相柱来控制,因此颜色值需为 HSL
(色相、饱和度、亮度)类型,其中 H
(色相)是变量,S
(饱和度)和 L
(亮度)是固定值:
<section
className="i-color-panel-block"
style={{ background: `hsl(${h}, 100%, 50%)` }}
>
...
</section>
黑白两色渐变层的样式如下:
.i-color-panel-block__white {
background: linear-gradient(90deg, #fff, rgba(255, 255, 255, 0));
}
.i-color-panel-block__black {
background: linear-gradient(0deg, #000, transparent);
}
以上是颜色面板的面板底色样式原理,接下来需要实现色相柱,以控制颜色面板的色相层颜色。
3、色相柱的实现
色相柱由柱形底色及拖拽滑块组成:
<div className="i-color-panel-bar__rgb">
<div className="i-color-picker__cursor" />
</div>
其中父级作为柱形底色,是红橙黄绿青蓝紫七种颜色组成的彩虹渐变色,样式如下:
.i-color-panel-bar__rgb {
background: linear-gradient(
90deg,
#f00 0,
#ff0 17%,
#0f0 33%,
#0ff 50%,
#00f 67%,
#f0f 83%,
#f00
);
}
以上是色相柱的柱形底色样式原理,接下来需要实现色相柱的功能,主要为:
- 拖拽滑块自身计算出对应的色相
- 点击色相柱任一位置会自动定位滑块并计算色相
- 点击色相柱任一位置后进行拖拽可模拟滑块的拖拽过程
首先需要将色相与滑块的位置联系起来,而色相的取值范围是 0 - 360
(其中红色为 0° / 360°
,黄色为 60°
,绿色为 120°
,青色为 180°
,蓝色为 240°
,品红色为 300°
),因此当滑块在最左侧时,对应色相的 0°
,在最右侧时对应色相的 360°
,只需要拿滑块在柱形中所在位置的比例去乘 360
,即可得到对应位置的色相值。
有了基本思路后,就是滑块的实现了:
- 拖拽滑块自身计算出对应的色相
- 监听滑块的点击事件计算开始位置,监听滑块的移动事件计算变化值来更新滑块位置,实现滑块拖拽功能,然后再将实时位置与柱形位置宽度联系起来,计算出所需的位置比例。
- 点击色相柱任一位置会自动定位滑块并计算色相
- 监听色相柱的点击事件计算开始位置,传给滑块并更新滑块位置,从而计算出位置比例。
- 点击色相柱任一位置后进行拖拽可模拟滑块的拖拽过程
- 监听色相柱的点击及移动事件,将实时位置数据传给滑块并更新滑块位置,从而计算出位置比例。
上述实现过程可以省略第一步,只对色相柱进行事件监听,而滑块自身禁用任意事件,从而通过色相柱的点击和移动事件模拟出滑块自身的拖拽。
其中对滑块的禁用事件样式处理如下:
.i-color-picker__cursor {
pointer-events: none;
}
到这里,已经实现了色相柱的点击拖拽计算位置比例功能,拿到位置比例后,乘以 360
,即可得到色相值。
拿到色相值(H
)后,将色相值传给颜色面板,即可计算出颜色面板的色相层颜色:
<section
className="i-color-panel-block"
style={{ background: `hsl(${h}, 100%, 50%)` }}
>
<div className="i-color-panel-block__white" />
<div className="i-color-panel-block__black" />
</section>
以上是色相柱的实现,而透明度柱的实现原理与色相柱相似,下面继续实现透明度柱。
4、透明度柱的实现
透明度柱也由柱形底色及拖拽滑块组成,其中柱形底色包括:
- 色相渐变层
- 透明格子背景层
结构如下:
<div className="i-color-panel-bar__a">
<div className="i-color-picker__cursor" />
<section className="i-color-panel-bar__a-color" />
<section className="i-color-panel-bar__a-bg" />
</div>
其中,色相渐变层的颜色只由 H
(色相)控制:
<section
className="i-color-panel-bar__a-color"
style={{ background: `linear-gradient(90deg, rgba(255, 0, 0, 0) 0%, hsl(${h}, 100%, 50%) 100%)` }}
/>
透明格子背景层的样式如下:
.i-color-panel-bar__a-bg {
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==');
}
注意这里需要设置一下 z-index
层级顺序,透明格子背景层在色相渐变层下方。
以上是透明度柱的柱形底色样式原理,接下来需要实现其功能,实现方式与色相柱相似,最后计算出滑块的位置比例,即可直接作为透明度值使用。
实现了色相柱和透明度柱,接下来继续完善颜色面板的功能。
5、颜色面板的功能实现
上面已经实现了颜色面板的展示效果,接下来是功能的实现。
其中,滑块的拖拽与色相柱相似,可通过点击及移动滑块所在面板来模拟滑块自身的拖拽功能,注意上面色相柱和透明度柱的滑块只需要计算 x
轴方向的位置比例,而颜色面板需要同时计算 x、y
轴上的位置比例。
实现了滑块的拖拽并拿到所在的位置比例后,就需要将 x、y
轴上的位置比例与实际颜色值联系起来,这里就需要用到 HSV
类型的颜色值。
什么是
HSV
?
HSV
即HSB
,其 H(色相)与 HSL 的 H(色相)一致,而 SL 和 SB 的区别如下:
- HSL 中的 S 是饱和度比例;
- HSL 中的 L 是亮度比例。
HSB
中的 S 控制纯色中混入白色比,值越大,颜色越纯,白色越少;HSB
中的 B 控制纯色中混入黑色比,值越大,明度越高,黑色越少。
假设滑块在 x、y
轴上的位置比例分别为 x、y
,则 HSV
的计算如下:
- H = 色相柱提供的色相值
- S = 100x
- V = 100(1 - y)
通过滑块的 x、y
轴位置比例计算得到 HSV
颜色值后,注意 HSV
格式无法直接在 CSS 中使用,需要转换成 RGB / HEX / HSL
等格式的有效值,这里就需要用到 HSV
的转换函数,点击查看具体转换公式。
网上有很多转换函数可以参考,但考虑到后面的颜色合法值判断、类型切换等功能,建议直接使用 tinycolor2 颜色值转换库。
使用方式很简单,拿到 HSV
后直接调用即可:
const result = tinycolor(hsv)
result
将输出以下格式:
{
"_originalInput": "hsv(224, 35%, 60%)",
"_r": 99.45,
"_g": 113.73,
"_b": 153,
"_a": 1,
"_roundA": 1,
"_format": "hsv",
"_ok": true,
"_tc_id": 352
}
可以继续调用 tinycolor
提供的转换函数,转为 CSS 可用的 RGB
等格式:
// 转 RGB
tinycolor(hsv).toRgbString() // rgb(80, 104, 171)
// 转 HEX
tinycolor(hsv).toHexString() // #5068ab
注意以上结果无法体现颜色透明度,因此转换前需要对 result
的透明度进行单独设置:
result.setAlpha(a)
// 转 RGB
tinycolor(hsv).toRgbString() // rgba(83, 101, 153, 0.62)
// 转 HEX,注意这里需要用 HEX8 输出才能体现透明度
tinycolor(hsv).toHex8String() // #5365999e
到这里,就可以通过颜色面板的滑块拖拽来计算出最终颜色值了。
6、取色器
取色器是整个颜色选择器中最复杂的部分,实现思路如下:
- 对整个页面进行截图;
- 将截图绘制到
canvas
,并获取指定区域像素点颜色值; - 获取鼠标定位,显示放大镜和颜色值;
另一种实现思路是直接使用原生的 EyeDropper 类:
const handleClickDropper = async () => {
const eyeDropper = new EyeDropper();
const result = await eyeDropper.open();
const color = result.sRGBHex // #ffffff
}
注意这种方式的兼容性较差:
使用时需要做 !!window.EyeDropper
的判断,否则会报错。
7、其它功能
操作面板其它功能如下:
- 颜色类型选择器
- 各颜色值输入框
- 预设颜色组
这几种功能相对简单,在各自独立组件的基础上结合 tinycolor 即可实现。
三、踩坑记录
1、滑块拖拽
- 全局滚动后打开操作面板,滑块的定位错位。
可通过 window.scrollX
及 window.scrollY
的计算加以矫正。
- 滑块在滑动过程中进行滚轮滚动导致的错位。
可在打开操作面板时设置 document.documentElement.style.overflowY = 'hidden'
,关闭时还原的方式来解决。
2、滑块的边界处理
颜色面板滑块的边界为面板宽高额外加上滑块宽高:
色相柱和透明度柱的边界即为柱形的宽度:
因此,滑块对传入的位置比例需做以下处理:
if (色阶柱 || 透明度柱) {
translate.x = x * (parentWidth - cursorWidth)
} else {
translate.x = (x * parentWidth) - (cursorWidth / 2)
translate.y = (y * parentHeight) - (cursorWidth / 2)
}
3、操作面板的性能问题
操作面板作为 Popup
弹窗组件的弹窗内容,大致结构如下:
const [innerValue, setInnerValue] = useState(value)
const handleChange = (val: string) => {
setInnerValue(val)
}
return (
<Popup
visible={visible}
content={
<ColorPanel
value={innerValue}
colorList={colorList}
onChange={handleChange}
onClose={handleClose}
/>
}
>
<ColorBlock style={{ background: innerValue }} />
</Popup>
)
操作面板每次触发颜色更新时,会更新操作面板组件外层的颜色值 innerValue
,使得操作面板重新渲染,这样会出现以下问题:
每次拖拽滑块时,操作面板内部颜色值在频繁更新,而作为弹窗内容的操作面板由于 innerValue
在同步变化,导致操作面板自身也在同步的频繁渲染,导致卡顿。解决方式如下:
使用 useMemo 控制当操作面板弹出时才对其进行渲染,避免 innerValue
变化时导致不必要的渲染:
const [innerValue, setInnerValue] = useState(value)
const handleChange = (val: string) => {
setInnerValue(val)
}
const colorPanel = useMemo(() =>
<ColorPanel
value={innerValue}
colorList={colorList}
onChange={handleChange}
onClose={handleClose}
/>, [visible]
)
return (
<Popup
visible={visible}
content={
<ColorPanel
value={innerValue}
colorList={colorList}
onChange={handleChange}
onClose={handleClose}
/>
}
content={colorPanel}
>
<ColorBlock style={{ background: innerValue }} />
</Popup>
)