本文共10076字,预计阅读20分钟
大家好啊,又是我GaoNeng
。最近在给OpenTiny做贡献,感觉renderless
这个架构还是挺有意思的,就贡献了一个color-picker
组件,简单写篇文章稍微记录一下。
也欢迎朋友们给 TinyVue 开源项目点个 Star ?支持下:
https://github.com/opentiny/tiny-vue
阅读完本文,你将会获得如下收获
- HSV,HSL,HEX,RGB的区别
- HSV色彩空间下,SV到XY的双向转换
- ColorPicker 组件的实现原理
- OpenTiny 新组件开发全流程
1 事情的起因
故事的发生非常的偶然。我在翻opentiny仓库issue的时候,偶然看到了这么一条
之前也在掘金上看过opentiny的介绍,感觉还不错,但是又抢不到组件。这一次终于让我抢到一个空闲组件了,于是我立刻就回复了。
2 初步分析
一般写组件前只考虑两个问题
- 长什么样
- 逻辑是什么
color-picker
颜色选择组件用于在应用程序和界面中让用户选择颜色。它是一个交互式的元素,通常由一个色彩光谱、色相环和颜色值输入框组成,用户可以通过这些元素来选择所需的颜色。ColorPicker的主要功能是让用户能够精确地选择特定的颜色,以便在应用程序的各种元素中使用。
ColorPicker 组件主要包含四个子组件 饱和度选择
, 色相选择
, alpha选择
, 工具栏
。比较简单,所以就没画图。主要的问题在于逻辑,也就是选择什么样的色彩空间更贴合用户的日常使用和直观体验。
常见的色彩空间分为 HSV
, HSL
, CMY
, CMYK
, HSB
,RGB
, LAB
, YUB
, YCrCb
前端最常见的应该是HSV
,HSL
,RGB
这三种。LAB
, YUB
, YCrCb
在日常业务中比较少见。
3 色彩空间基础知识
HSV, HSL, HEX, RGB 都是什么呢?
HSV,HSL,RGB都是色彩空间。而HEX可以看作是RGB的另一种表达方法。
3.1 什么是色彩空间?
色彩空间是为了让人们更好的认识色彩而建立的一种抽象的数学模型,它是将数值分布在N维的坐标系中,帮助人们更好地认识和理解色彩。
例如RGB色彩空间,就是将RGB分量映射在三维笛卡尔坐标系中。分量的数量代表该分量的亮度值。下图是经过归一处理的RGB色彩空间示意图
而HSV与HSL色彩空间都是将颜色映射到了柱坐标系。下图展示了HSV与HSL的示意图

HSV
HSL
3.2 HSV,HSL,RGB 孰优孰劣?
了解了HSV,HSL,RGB色彩空间及其表达方法,我们需要考虑究竟哪一种色彩空间对于人类更加的直观呢?要不问问万能的音理吧

啊这,她说不知道。那看来只能问问万能的chat-gpt
了
不愧是你,chatgpt总是能救我于危难之间。不过话又说回来,HSL与HSV都很直观,只是一个是V(Value)另一个是L(lightness)。两种色彩空间的柱坐标系如下图所示

HSV
HSL
可以看到,HSV越偏向右上角饱和度和亮度越高。但HSL则是偏向于截面的中间饱和度和亮度越高。
在PS和其他软件中,也大都选择了HSV作为选色时的色彩空间。为了保持统一,color-picker组件也选择了HSV作为选色时的色彩空间。
3.3 SV与XY的双向转换
饱和度选择的时候,我们需要将XY分量转为SV分量。这存在一种表达方式。SV与XY存在一种计算关系
$$
\begin{cases}
S=\frac{x}{width} \times 100& S \in [0,100]\
V=100 – \frac{y}{height} \times 100& V\in [0,100] \
X=\max(\frac{(S \times width)}{100}, 0)& X \in [0,width] \
Y=\max(\frac{(V \times height)}{100},0)& Y \in [0,heght]
\end{cases}
$$
其中width与height均为容器的宽度和高度, XY为光标位置。
4 组件设计
和普通组件开发不同,tinyvue
是将逻辑抽离到了renderless
下。这样做可以让开发者更着重于逻辑的编写。单测也更好测,测试的时候如果你想,可以只测renderless和被抽象的逻辑,UI层面甚至可以不测(因为UI主要是各个库来做渲染和依赖跟踪,单测是最小的可测试单元,所以库可以mock掉,只测renderless)。
一个完整的组件至少要有以下几个要素
- 组件
- UI
- 逻辑
- 类型
- 文档
- 中文
- 英文
- 测试
- 单测
- E2E测试
4.1 目录梳理
tiny-vue
简化目录如下所示. 带有!
前缀的文件表示必选,?
前缀的文件表示可选
例如!index.js
表示index.js
是必选的。
examplesdocspublicsites<mpt> app![component-name]!webdoc![component-name].cn.md // 中文文档![component-name].cn.md //英文文档![component-name].js // 组件文档配置![demo].vue //示例文件?[demo].spec.ts //示例的e2e测试overviewimage //图标resourcewebdoc //对应使用指南config.js!menu.js // 目录文件,需要在此追加你的组件packagesrenderlesssrc![component-name]?[component-name]vue.tsindex.ts //函数抽象的地方vue.tsindex.ts //函数抽象的地方theme // 桌面端样式src?[component-name] // 有些组件不一定需要样式(例如: config-provider)index.less // 样式vars.less // 变量声明theme-mobile // 移动端样式src?[component-name]index.less // 样式vars.less // 变量声明vuesrc![component-name]!__tests__![component-name].spec.vue // 至少要有一个单元测试文件srcpc.vue // 桌面端模板?mobile.vue // 移动端模板,如果你的组件不需要移动端那么可以删除index.ts // 组件导出package.jsonexamples docs public sites <mpt> app ![component-name] !webdoc ![component-name].cn.md // 中文文档 ![component-name].cn.md //英文文档 ![component-name].js // 组件文档配置 ![demo].vue //示例文件 ?[demo].spec.ts //示例的e2e测试 overviewimage //图标 resource webdoc //对应使用指南 config.js !menu.js // 目录文件,需要在此追加你的组件 packages renderless src ![component-name] ?[component-name] vue.ts index.ts //函数抽象的地方 vue.ts index.ts //函数抽象的地方 theme // 桌面端样式 src ?[component-name] // 有些组件不一定需要样式(例如: config-provider) index.less // 样式 vars.less // 变量声明 theme-mobile // 移动端样式 src ?[component-name] index.less // 样式 vars.less // 变量声明 vue src ![component-name] !__tests__ ![component-name].spec.vue // 至少要有一个单元测试文件 src pc.vue // 桌面端模板 ?mobile.vue // 移动端模板,如果你的组件不需要移动端那么可以删除 index.ts // 组件导出 package.jsonexamples docs public sites <mpt> app ![component-name] !webdoc ![component-name].cn.md // 中文文档 ![component-name].cn.md //英文文档 ![component-name].js // 组件文档配置 ![demo].vue //示例文件 ?[demo].spec.ts //示例的e2e测试 overviewimage //图标 resource webdoc //对应使用指南 config.js !menu.js // 目录文件,需要在此追加你的组件 packages renderless src ![component-name] ?[component-name] vue.ts index.ts //函数抽象的地方 vue.ts index.ts //函数抽象的地方 theme // 桌面端样式 src ?[component-name] // 有些组件不一定需要样式(例如: config-provider) index.less // 样式 vars.less // 变量声明 theme-mobile // 移动端样式 src ?[component-name] index.less // 样式 vars.less // 变量声明 vue src ![component-name] !__tests__ ![component-name].spec.vue // 至少要有一个单元测试文件 src pc.vue // 桌面端模板 ?mobile.vue // 移动端模板,如果你的组件不需要移动端那么可以删除 index.ts // 组件导出 package.json
4.2 模块设计
在tiny-vue
下输入 pnpm create:ui color-picker
就可以创建最基本的模板了。
color-picker
组件主要分为以下几个部分。因为时间原因,在这里只讲解trigger
与tools
- trigger
- color-select
- sv-select
- hue-select
- alpha-select
- tools
他们的层级关系是这样的
triggercolor-selectsv-selecthue-selectalpha-selecttoolstrigger color-select sv-select hue-select alpha-select toolstrigger color-select sv-select hue-select alpha-select tools
4.3 Props 定义
开发组件,我习惯先思考入参和事件。入参我是设计这样的
{modelValue: String, // 默认颜色,不存在即为transparentvisible: Boolean, // 默认color-select是否可见alpha: Boolean // 是否启用alpha选择}{ modelValue: String, // 默认颜色,不存在即为transparent visible: Boolean, // 默认color-select是否可见 alpha: Boolean // 是否启用alpha选择 }{ modelValue: String, // 默认颜色,不存在即为transparent visible: Boolean, // 默认color-select是否可见 alpha: Boolean // 是否启用alpha选择 }
事件则是
{confirm: (hex: string)=>void, // 当用户点击confirm时,返回选择的颜色cancel: ()=>void // 当用户点击取消或除了color-select子代的dom元素时,触发的事件}{ confirm: (hex: string)=>void, // 当用户点击confirm时,返回选择的颜色 cancel: ()=>void // 当用户点击取消或除了color-select子代的dom元素时,触发的事件 }{ confirm: (hex: string)=>void, // 当用户点击confirm时,返回选择的颜色 cancel: ()=>void // 当用户点击取消或除了color-select子代的dom元素时,触发的事件 }
设计完成后,我们就可以开始开发了
5 组件开发
trigger是ColorPicker组件的关键模块,主要控制color-select
, alpha-select
, tools
的显示状态。
5.1 组件模板开发
我们先来描述一下trigger的状态都有哪些