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{lethhex.replace(#,);if(h.length3){hh[0]h[0]h[1]h[1]h[2]h[2];}if(h.length!6)returnnull;letnumparseInt(h,16);if(isNaN(num))returnnull;return{r:(num16)0xff,g:(num8)0xff,b:num0xff};}三位简写的处理逻辑是将每个字符复制一次——例如#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{letvMath.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)确保始终输出两位。比如r0时输出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{letrdr/255,gdg/255,bdb/255;letmaxMath.max(rd,gd,bd),minMath.min(rd,gd,bd);leth0,s0,l(maxmin)/2;if(max!min){letdmax-min;sl0.5?d/(2-max-min):d/(maxmin);switch(max){caserd:h((gd-bd)/d(gdbd?6:0))/6;break;casegd:h((bd-rd)/d2)/6;break;casebd:h((rd-gd)/d4)/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 时饱和度较高的颜色会使 maxmin 接近 2公式d/(2-max-min)放大了差异当 L 0.5 时使用d/(maxmin)计算。这两种情况的区别源于 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{lethdh/360,sds/100,ldl/100;if(sd0){letvMath.round(ld*255);return{r:v,g:v,b:v};}letqld0.5?ld*(1sd):ldsd-ld*sd;letp2*ld-q;lethue2rgb(t:number):number{if(t0)t1;if(t1)t-1;if(t1/6)returnp(q-p)*6*t;if(t1/2)returnq;if(t2/3)returnp(q-p)*(2/3-t)*6;returnp;};return{r:Math.round(hue2rgb(hd1/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*(1sd)否则q ldsd-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是圆形颜色预览组件的填充色它在两种转换路径中都会被更新始终反映当前操作的颜色。初始化时设为#007affiOS 风格的系统蓝色并在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;});}TextInput的text属性绑定到this.hexInput.replace(#, )而非直接绑定this.hexInput这样即使在onChange中自动拼接#前缀也不会在输入框中显示重复的#。onChange回调中this.hexInput # v确保状态变量始终存储带#前缀的完整 HEX 值供后续转换使用。hexConvert 方法验证与联动HEX 转换的核心逻辑hexConvert(){letrgbColorUtils.hexToRgb(this.hexInput);if(!rgb){this.rgbResult无效的 HEX 颜色;this.hslResult;return;}lethslColorUtils.rgbToHsl(rgb.r,rgb.g,rgb.b);this.rgbResult${rgb.r},${rgb.g},${rgb.b};this.hslResult${hsl.h}°,${hsl.s}%,${hsl.l}%;this.previewColorColorUtils.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(){letpartsthis.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;}letrparts[0],gparts[1],bparts[2];this.hexResultColorUtils.rgbToHex(r,g,b);lethslColorUtils.rgbToHsl(r,g,b);this.hslResult2${hsl.h}°,${hsl.s}%,${hsl.l}%;this.previewColorthis.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