news 2026/7/9 1:29:01

HarmonyKit | 鸿蒙新特性应用:颜色转换 HEX↔RGB↔HSL 三色空间互转算法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyKit | 鸿蒙新特性应用:颜色转换 HEX↔RGB↔HSL 三色空间互转算法

HarmonyKit | 鸿蒙新特性应用:颜色转换 HEX↔RGB↔HSL 三色空间互转算法

引言

在开发者工具箱中,颜色格式转换是一个看似简单实则涉及较多数学细节的功能。前端开发者经常需要在 CSS 的 HEX 表示法、RGB 函数表示法和 HSL 函数表示法之间进行切换。HarmonyKit 项目中的颜色转换工具实现了这三种颜色空间的双向互转,底层依靠纯数学算法,不依赖任何第三方库。本文将从数据结构设计、算法推导到 UI 交互实现,完整解析该工具的开发全过程。

数据结构设计

颜色转换涉及三种表示法,我们定义了两个核心接口来描述中间数据:

exportinterfaceRGB{r:number;g:number;b:number;}exportinterfaceHSL{h:number;s:number;l:number;}

RGB 接口的三个字段分别代表红、绿、蓝三个通道,取值范围均为 0-255 的整数。HSL 接口中,h 代表色相(Hue),取值范围 0-360 度;s 代表饱和度(Saturation),取值范围 0-100%;l 代表亮度(Lightness),取值范围也是 0-100%。

设计上不定义 HEX 的接口类型,因为 HEX 本质上就是一个字符串(如#007aff),没有必要为单一的字符串字段创建接口。而 RGB 和 HSL 都需要在多个转换函数之间传递中间结果,定义接口可以让类型系统在编译期就发现字段拼写错误。

ColorUtils 工具类的整体设计

所有颜色转换逻辑封装在ColorUtils类中,全部为静态方法。选择静态方法而非实例方法的原因是:颜色转换属于无状态计算,输入确定则输出确定,不存在需要跨方法共享的中间状态。每个方法接受明确的输入参数并返回确定的结果,这是纯函数在面向对象语言中的自然表达。

exportclassColorUtils{statichexToRgb(hex:string):RGB|null{...}staticrgbToHex(r:number,g:number,b:number):string{...}staticrgbToHsl(r:number,g:number,b:number):HSL{...}statichslToRgb(h:number,s:number,l:number):RGB{...}}

四个方法构成了完整的转换矩阵:HEX ↔ RGB、RGB ↔ HSL,而 HEX ↔ HSL 的转换通过 RGB 作为中间桥梁来实现,不需要单独编写直接转换函数。这是经典的"中间表示法"设计模式——有 N 种格式时,只需维护 N 个到中间格式的转换器,而不是 N*(N-1) 个直接转换器。

HEX 到 RGB 的解析算法

hexToRgb方法需要处理三种合法的 HEX 输入格式:#RGB(三位简写)、#RRGGBB(六位标准)、以及不带#前缀的裸字符串。实现中先统一去掉#前缀,然后处理三位简写形式:

statichexToRgb(hex:string):RGB|null{leth=hex.replace('#','');if(h.length===3){h=h[0]+h[0]+h[1]+h[1]+h[2]+h[2];}if(h.length!==6)returnnull;letnum=parseInt(h,16);if(isNaN(num))returnnull;return{r:(num>>16)&0xff,g:(num>>8)&0xff,b:num&0xff};}

三位简写的处理逻辑是将每个字符复制一次——例如#abc展开为aabbcc,这符合 CSS 颜色规范的语义。接着用parseInt(h, 16)将 6 位十六进制字符串转换为整数,再利用位运算提取各通道值:

  • (num >> 16) & 0xff:右移 16 位取高 8 位,得到红色通道
  • (num >> 8) & 0xff:右移 8 位取中间 8 位,得到绿色通道
  • num & 0xff:取低 8 位,得到蓝色通道

位运算比字符串切片substring(0,2)parseInt的方式更简洁高效,在循环或频繁调用场景下有明显的性能优势。这里还包含了多层防御:长度校验(非 6 位返回 null)、数值校验(NaN 返回 null),使得调用方可以安全地使用返回值。

RGB 到 HEX 的格式化算法

反向转换需要将三个 0-255 的整数格式化为#RRGGBB形式。关键步骤是将每个通道值转为两位十六进制字符串:

staticrgbToHex(r:number,g:number,b:number):string{lettoHex=(n:number):string=>{letv=Math.max(0,Math.min(255,Math.round(n)));returnv.toString(16).padStart(2,'0');};return`#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();}

内部函数toHex做了边界保护:Math.max(0, Math.min(255, Math.round(n)))先将值钳制在 0-255 范围内并进行四舍五入,然后toString(16)转为十六进制,padStart(2, '0')确保始终输出两位。比如r=0时输出00而非0,保证 HEX 字符串始终是标准的 6 位格式。

最终字符串统一转为大写(toUpperCase()),因为 CSS 中推荐使用大写 HEX,且在视觉上更统一。同时这也意味着hexToRgb必须兼容大写输入——而parseInt(h, 16)天然支持大小写不敏感的十六进制解析,所以两个方向的兼容性得到了保证。

RGB 到 HSL:色彩空间的数学转换

RGB 到 HSL 的转换是最复杂的算法部分。首先将 0-255 的整数值归一化到 0-1 的浮点数空间:

staticrgbToHsl(r:number,g:number,b:number):HSL{letrd=r/255,gd=g/255,bd=b/255;letmax=Math.max(rd,gd,bd),min=Math.min(rd,gd,bd);leth=0,s=0,l=(max+min)/2;if(max!==min){letd=max-min;s=l>0.5?d/(2-max-min):d/(max+min);switch(max){caserd:h=((gd-bd)/d+(gd<bd?6:0))/6;break;casegd:h=((bd-rd)/d+2)/6;break;casebd:h=((rd-gd)/d+4)/6;break;}}return{h:Math.round(h*360),s:Math.round(s*100),l:Math.round(l*100)};}

亮度 L 的计算最简单:取 RGB 最大和最小值的平均值。当 max 等于 min 时,颜色为灰色(无饱和度),色相 h 无意义,直接保持默认值 0。

饱和度 S 的计算分两种情况:当亮度 L > 0.5 时,饱和度较高的颜色会使 max+min 接近 2,公式d/(2-max-min)放大了差异;当 L <= 0.5 时,使用d/(max+min)计算。这两种情况的区别源于 HSL 模型中饱和度定义的分段性质。

色相 H 的计算通过 switch 判断哪个通道是最大值,然后在色轮上定位。每个 case 计算出色相在 0-6 之间的值(对应六段色轮),然后除以 6 得到 0-1 之间的归一化值。特别注意红色通道中的gd < bd ? 6 : 0修正——当绿色的值小于蓝色时,需要加入 6 的偏移来保证色相在正确的位置。

最后将三个归一化值转换为最终输出:h 乘以 360 得到角度值,s 和 l 乘以 100 得到百分比值,全部四舍五入到整数。

HSL 到 RGB:逆变换的数学推导

从 HSL 回到 RGB 需要实现色相到 RGB 分量的映射函数:

statichslToRgb(h:number,s:number,l:number):RGB{lethd=h/360,sd=s/100,ld=l/100;if(sd===0){letv=Math.round(ld*255);return{r:v,g:v,b:v};}letq=ld<0.5?ld*(1+sd):ld+sd-ld*sd;letp=2*ld-q;lethue2rgb=(t:number):number=>{if(t<0)t+=1;if(t>1)t-=1;if(t<1/6)returnp+(q-p)*6*t;if(t<1/2)returnq;if(t<2/3)returnp+(q-p)*(2/3-t)*6;returnp;};return{r:Math.round(hue2rgb(hd+1/3)*255),g:Math.round(hue2rgb(hd)*255),b:Math.round(hue2rgb(hd-1/3)*255)};}

首先将输入参数归一化到 0-1 范围。饱和度为零时,颜色是灰色,三个通道取相同值。核心辅助函数hue2rgb接受一个 0-1 的色相偏移量 t,根据 t 落在六段色轮的哪一段,计算对应的 RGB 分量值。

p 和 q 是中间变量:q 代表亮度的临时最大值,p 代表临时最小值。当亮度 < 0.5 时q = ld*(1+sd),否则q = ld+sd-ld*sd,这两段线性插值保证了颜色从暗到亮的平滑过渡。

三个通道通过色相偏移来区分:红色偏移 +1/3,绿色无偏移,蓝色偏移 -1/3。因为色轮上 R、G、B 三等分 360 度,每个相差 120 度(即色轮的 1/3)。hue2rgb 内部的六段条件判断对应于将色轮六等分(每段 60 度),在每段内做线性插值。

ColorConverter 页面的交互设计

转换页面支持两种转换路径:HEX→RGB/HSL 和 RGB→HEX/HSL。界面使用@State管理多个状态变量:

@StatehexInput:string='#007aff';@StatergbResult:string='';@StatehslResult:string='';@StatergbInput:string='';@StatehexResult:string='';@StatehslResult2:string='';@StatepreviewColor:string='#007aff';

previewColor是圆形颜色预览组件的填充色,它在两种转换路径中都会被更新,始终反映当前操作的颜色。初始化时设为#007aff(iOS 风格的系统蓝色),并在aboutToAppear()中立即执行一次 HEX 转换来填充初始结果。

圆形预览使用Circle组件配合阴影效果:

Circle().width(36).height(36).fill(this.previewColor).shadow({radius:4,color:'#20000000',offsetX:0,offsetY:2});

Circle()是鸿蒙的圆形绘制组件,fill()接受一个 ResourceColor 类型,直接传入 HEX 字符串即可。阴影参数中#20000000是带透明度的黑色——0x20约 12.5% 的透明度,在白色背景上形成微妙的光影效果。

HEX 输入框的交互细节

HEX 输入采用了前缀分离的设计:前面固定显示#字符,输入框本身只接收 6 位十六进制字符串。这样用户无需手动输入#,也避免了#被重复输入的问题:

Row(){Text('#').fontSize(14).fontFamily('monospace').fontColor('#666');TextInput({text:this.hexInput.replace('#',''),placeholder:'007aff'}).fontSize(14).fontFamily('monospace').layoutWeight(1).backgroundColor('#ffffff').borderRadius(8).padding(10).onChange((v:string)=>{this.hexInput='#'+v;});}

TextInputtext属性绑定到this.hexInput.replace('#', '')而非直接绑定this.hexInput,这样即使在onChange中自动拼接#前缀,也不会在输入框中显示重复的#onChange回调中this.hexInput = '#' + v确保状态变量始终存储带#前缀的完整 HEX 值,供后续转换使用。

hexConvert 方法:验证与联动

HEX 转换的核心逻辑:

hexConvert(){letrgb=ColorUtils.hexToRgb(this.hexInput);if(!rgb){this.rgbResult='无效的 HEX 颜色';this.hslResult='';return;}lethsl=ColorUtils.rgbToHsl(rgb.r,rgb.g,rgb.b);this.rgbResult=`${rgb.r},${rgb.g},${rgb.b}`;this.hslResult=`${hsl.h}°,${hsl.s}%,${hsl.l}%`;this.previewColor=ColorUtils.rgbToHex(rgb.r,rgb.g,rgb.b);}

hexToRgb返回 null 时,使用用户友好的错误文本代替技术细节。RGB 结果展示为逗号分隔的三元组,HSL 结果带有单位符号(度和百分号),方便直接复制到 CSSrgb()hsl()函数中使用。

关键的一行:this.previewColor = ColorUtils.rgbToHex(rgb.r, rgb.g, rgb.b)。这里做了一个"往返转换"——将解析出的 RGB 再转回 HEX,目的是规范化颜色值。例如用户输入#abc(简写)或#aabbcc(小写),往返转换后会变成#AABBCC(标准大写六位格式),预览颜色和显示文字保持一致。

rgbConvert 方法:解析与容错

RGB 输入是一个自由文本字段,用户需要手动输入逗号分隔的三个数字。解析时需要处理各种不规范输入:

rgbConvert(){letparts=this.rgbInput.split(',').map((s:string)=>parseInt(s.trim()));if(parts.length!==3||parts.some((n:number)=>isNaN(n))){this.hexResult='无效的 RGB 值,格式: 255, 128, 0';this.hslResult2='';return;}letr=parts[0],g=parts[1],b=parts[2];this.hexResult=ColorUtils.rgbToHex(r,g,b);lethsl=ColorUtils.rgbToHsl(r,g,b);this.hslResult2=`${hsl.h}°,${hsl.s}%,${hsl.l}%`;this.previewColor=this.hexResult;}

首先按逗号分割字符串,每个部分去空格后用parseInt解析。验证逻辑检查两点:分割后必须是恰好三个元素,且每个元素必须能解析为有效数字(isNaN检查)。注意这里没有检查值是否在 0-255 范围——这个检查委托给了ColorUtils.rgbToHex中的Math.max(0, Math.min(255, ...)),演示了"单一职责"原则:页面层负责格式验证,工具类负责值域钳制。

错误提示中包含了期望的格式示例(255, 128, 0),帮助用户快速纠正输入。这种设计在所有 HarmonyKit 工具中是一致的——错误信息不仅要告知失败,还要指导正确的操作方式。

条件渲染与结果展示

结果区域使用if条件渲染,只有在有转换结果时才显示:

if(this.rgbResult){Row(){Text(`RGB:${this.rgbResult}`).fontSize(13).fontFamily('monospace');CopyButton({text:`rgb(${this.rgbResult})`});}.width('100%').padding({left:16,right:16,top:8});Row(){Text(`HSL:${this.hslResult}`).fontSize(13).fontFamily('monospace');CopyButton({text:`hsl(${this.hslResult})`});}.width('100%').padding({left:16,right:16,top:4});}

CopyButton 的文本参数不只是结果字符串,而是完整的 CSS 函数写法——rgb(${this.rgbResult})hsl(${this.hslResult})。这样用户复制后可以直接粘贴到 CSS 代码中使用,无需手动添加rgb()hsl()包裹。这种"复制即用"的设计理念贯穿整个 HarmonyKit 项目。

两种转换路径的独立性

页面中 HEX→RGB/HSL 和 RGB→HEX/HSL 是两条独立的转换管线,各自拥有独立的输入状态、输出状态和结果展示区域。虽然它们共享了previewColor状态(作为视觉反馈的统一出口),但各自的转换结果不会互相干扰。这种隔离设计意味着用户可以在两个方向同时看到不同的转换结果,例如上面是#007aff的 RGB/HSL 解析,下面是255, 128, 0的 HEX/HSL 结果。

在这里插入图片描述

然而,这种设计也引入了一个微妙的问题:previewColor会被两次赋值覆盖。如果用户先转换了 HEX,圆形预览显示蓝色;再转换了 RGB,圆形预览就变成橙色。这符合"预览当前操作的颜色"的交互预期,但如果有用户期望预览始终保持某个颜色,则可能需要独立的锁定机制。当前简化版本满足大多数使用场景。

错误状态的 UI 处理

颜色转换工具中的错误处理分为两个维度。第一个是输入验证维度:hexToRgb返回 null 时显示"无效的 HEX 颜色",RGB 解析失败时显示详细格式提示。第二个是空状态维度:当验证失败时,对应的次要结果字段(如 HEX 解析失败时的 HSL 结果)被设置为空字符串,利用条件渲染的if判断自然隐藏。

这种处理方式的好处是 UI 会"默默地"恢复到初始状态,不会残留上次成功的旧结果来造成困惑。但也意味着:如果用户将一个有效的 HEX 改为无效的 HEX 后点击转换,RGB 和 HSL 结果都会消失——这是一种明确的"此输入无效"的视觉信号。

颜色系统在工具集中的一致性

ColorUtils 中的rgbToHex方法被 ColorConverter 页面用来做颜色预览,但它也可以被项目的其他部分复用——例如,如果未来需要一个颜色主题编辑器或配色方案生成器,HEX↔RGB 的互转逻辑可以直接从 ColorUtils 调用。将颜色算法放在 utils 层而非页面层,是对未来扩展的预先投资。

在 HarmonyKit 的 10 个工具中,ColorConverter 属于"计算"类别,与时间戳转换、哈希计算、UUID 生成、进制转换归为一组。它的特点是:即时计算(同步操作)、数学密集型(辗转三种颜色空间的位运算与浮点运算)、结果可量化(数值型输出)。这些特性区别于编解码类工具的文本处理管道和文本类工具的统计聚合。

小结

颜色转换工具的实现展示了几个值得记取的工程实践:一是用接口定义中间数据结构,让类型系统帮你管理字段;二是用位运算代替字符串操作来提高十六进制解析的效率;三是将数学算法与 UI 状态管理解耦,工具类保持纯函数特性;四是"复制即用"的产品思维——输出的不仅是原始数据,而是可直接使用的代码片段。

从架构角度看,ColorUtils 作为工具类完全独立于 ArkUI 框架,可以在任何 JavaScript/TypeScript 运行时中使用(只要提供基本的 String、Math 和 parseInt 即可)。这种框架无关性正是utils/目录的设计目标——将核心逻辑与呈现层分离,使代码既可单独测试,又能在不同 UI 框架间迁移。


项目仓库:https://atomgit.com/VON-/harmony-kit

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/9 1:28:54

Python 数据可视化之 Matplotlib——从基础到高级图表

数据可视化是数据分析中最重要的环节之一。Matplotlib 是 Python 最基础的可视化库&#xff0c;掌握它之后学习 Seaborn、Plotly 等会非常轻松。 一、基础图表 import matplotlib.pyplot as plt import numpy as np# 设置中文字体 plt.rcParams[font.sans-serif] [SimHei, Mic…

作者头像 李华
网站建设 2026/7/9 1:27:09

HarmonyOS 小游戏《对战五子棋》开发第18篇 - 棋盘设计

棋盘是游戏的"脸面"——Canvas让每一颗棋子都栩栩如生 设计棋盘如下&#xff1a; 代码如下&#xff1a; /*** ChessBoardView.ets - 可复用的五子棋棋盘组件* 使用Canvas绘制棋盘、网格、棋子*/ import { BOARD_SIZE, EMPTY, BLACK, WHITE } from ./GameConstants;c…

作者头像 李华
网站建设 2026/7/9 1:26:56

HarmonyKit | 鸿蒙新特性实践:进制转换器 2/8/10/16 四进制通用互转

HarmonyKit | 鸿蒙新特性实践&#xff1a;进制转换器 2/8/10/16 四进制通用互转 引言 进制转换是计算机科学的基础运算之一。虽然操作系统自带的计算器都提供了进制转换功能&#xff0c;但在移动端开发调试场景中&#xff0c;需要快速在二进制、八进制、十进制和十六进制之间…

作者头像 李华
网站建设 2026/7/9 1:25:53

DeepSeek V3 vs Claude 3.5 Sonnet:程序员日常开发场景深度横向对比

适用人群&#xff1a;正在做AI编程助手选型的开发者、想在日常开发中提效的前后端工程师、对DeepSeek V3和Claude 3.5 Sonnet代码能力感兴趣的技术人 核心结论前置&#xff1a;经过三个真实开发场景的实测&#xff0c;DeepSeek V3在中英文混合注释生成、中文文档编写方面优势明…

作者头像 李华
网站建设 2026/7/9 1:25:19

SFT数据质量评估:3种自动化方法筛选千条指令数据,PPL降低20%

SFT数据质量评估&#xff1a;3种自动化方法筛选千条指令数据&#xff0c;PPL降低20%在大型语言模型&#xff08;LLM&#xff09;的监督微调&#xff08;SFT&#xff09;过程中&#xff0c;数据质量往往比数据数量更为关键。低质量的指令数据不仅会拖慢训练进程&#xff0c;还可…

作者头像 李华
网站建设 2026/7/9 1:25:01

2026抖音动图去水印免费方法,无水印保存抖音GIF技巧

日常整理抖音素材、收藏GIF表情包时&#xff0c;平台自带的边角水印、动态飘字水印&#xff0c;往往会破坏画面整体质感&#xff0c;影响素材整洁度。市面上多数专业去水印工具需要付费解锁功能&#xff0c;普通用户仅做个人收藏、学习使用&#xff0c;无需花费成本购置会员。本…

作者头像 李华