当前位置: 首页 > news >正文

HarmonyOS6 实战案例之HSV 颜色模型到底在算什么?ColorUtils 代码逐行拆解

文章目录

      • 先说 HSV 是什么
      • HEX 转 HSV
      • HSV 转 HEX
      • 两个转换函数:一个是类方法,一个是独立函数
      • 在 ColorSelector 里是怎么用的
      • 验证一个例子
      • 写在最后

做颜色选择器绕不开 HSV 颜色模型。这个东西讲起来很容易变成一堆公式,但其实真的不难——只要搞清楚ColorUtils.ets这两个函数在干什么,HSV 基本就懂了。

先说 HSV 是什么

RGB 描述颜色靠的是红绿蓝三个通道的混合量,直觉上不太好用。你很难说"这个颜色要淡一点"应该怎么调 RGB。

HSV 把颜色描述成三个更直观的维度:

  • H(Hue,色相):颜色的"种类",取值 0~360。0/360 是红,60 是黄,120 是绿,180 是青,240 是蓝,300 是紫
  • S(Saturation,饱和度):颜色有多"纯",0~1。0 是灰色,1 是纯色
  • V(Value,明度):颜色有多亮,0~1。0 是黑色,1 是最亮

颜色选择器用 HSV 的原因:用户拖动色相条选"红/蓝/绿",在面板里选"深/浅/纯/灰",比直接调 RGB 直观多了。


HEX 转 HSV

// ColorUtils.etshexToHsv(hex:string):[number,number,number]{constr=parseInt(hex.slice(1,3),16)/255;constg=parseInt(hex.slice(3,5),16)/255;constb=parseInt(hex.slice(5,7),16)/255;

先把#rrggbb格式的 HEX 字符串拆开,slice(1,3)切出红色分量,parseInt(..., 16)转成 0~255 的整数,再除以 255 归一化到[0, 1]

constmax=Math.max(r,g,b);constmin=Math.min(r,g,b);constdelta=max-min;

max就是 V(明度)的原始值,delta是最大最小值之差,用来判断饱和度和色相。

明度 V

constv=max;

就是 RGB 里最大的那个分量,值越高颜色越亮。

饱和度 S

![Hand-drawn educational flowchart on warm cream pap](https://files.mdnice.com/user/47561/3127cc87-df81-4cb7-95b8-d6471900a468.png)consts=max===0?0:delta/max;

如果max = 0说明是纯黑,饱和度定义为 0。否则delta/max就是饱和度——三个通道差异越大,颜色越"纯",饱和度越高。

色相 H

leth=0;if(delta!==0){if(max===r){h=((g-b)/delta)%6;}elseif(max===g){h=(b-r)/delta+2;}else{h=(r-g)/delta+4;}h*=60;if(h<0){h+=360;}}

色相计算是这里最复杂的部分。HSV 的色相是基于"哪个通道最大"来定位颜色在色轮上的位置:

  • max === r(红色主导):h 在 0~60° 或 300~360° 之间,公式(g-b)/delta % 6算出偏移
  • max === g(绿色主导):h 在 60°~180° 之间,加 2(乘以 60 后是 +120°)
  • max === b(蓝色主导):h 在 180°~300° 之间,加 4(乘以 60 后是 +240°)

最后乘以 60,把[0,6)的值映射到[0,360)。如果算出负数,加 360 补正。

return[h,s,v];}

返回[H, S, V]的元组。


HSV 转 HEX

反向转换稍微复杂一点,但思路是一样的——先算出 RGB,再格式化成 HEX 字符串。

hsvToHex(h:number,s:number,v:number):string{letr:number=0,g:number=0,b:number=0;leti=Math.floor(h/60);// 色相所在的 60° 区间(0~5)letf=h/60-i;// 区间内的小数偏移letp=v*(1-s);// 最小亮度分量letq=v*(1-f*s);// 次小亮度分量lett=v*(1-(1-f)*s);// 次大亮度分量

HSV 转 RGB 把色相环分成 6 个 60° 的扇区,每个扇区里 RGB 三个分量按不同的规律变化:

switch(i%6){case0:r=v;g=t;b=p;break;// 红 → 黄![Single-page hand-drawn educational infographicin](https://files.mdnice.com/user/47561/cda4a0fb-7b26-43a7-a218-3998a8fdd9dc.png)case1:r=q;g=v;b=p;break;// 黄 → 绿case2:r=p;g=v;b=t;break;// 绿 → 青case3:r=p;g=q;b=v;break;// 青 → 蓝case4:r=t;g=p;b=v;break;// 蓝 → 紫case5:r=v;g=p;b=q;break;// 紫 → 红}

六个 case 覆盖了完整的色相环。每个区间内,哪个通道主导(值为 v)、哪个通道上升(值为 t)、哪个通道下降(值为 q)、哪个通道最低(值为 p)都是固定的。

r=Math.round(r*255);g=Math.round(g*255);b=Math.round(b*255);return`#${this.toHex(r)}${this.toHex(g)}${this.toHex(b)}`;}

归一化的 RGB 乘以 255 并取整,然后格式化成两位 HEX 字符串。

toHex(n:number){lethex=n.toString(16);returnhex.length===1?'0'+hex:hex;}

toHex做了个小处理——如果转出来只有一位(比如0aa),前面补0,保证每个分量都是两位。


两个转换函数:一个是类方法,一个是独立函数

ColorUtils.ets里其实有两套实现。ColorUtils类里是hexToHsvhsvToHex,文件末尾还有一个独立的hsv2rgb函数:

// 独立函数(未被导出,内部逻辑相同)functionhsv2rgb(h:number,s:number,v:number):ColorRgb{letr:number=0,g:number=0,b:number=0;// ... 同样的 switch 逻辑 ...return{r:r,g:g,b:b};}exportinterfaceColorRgb{r:number;g:number;b:number;}

hsv2rgb返回的是ColorRgb接口类型(包含r,g,b三个字段),而hsvToHex返回 HEX 字符串。前者在项目当前代码里没被调用,应该是保留的备用方法。

export default new ColorUtils()导出的是ColorUtils的单例,所以使用时直接ColorUtils.hexToHsv(...)调用实例方法。


在 ColorSelector 里是怎么用的

// ColorSelector.etsimportColorUtilsfrom'../utils/ColorUtils';// 打开时:HEX → HSVaboutToAppear():void{consthsv=ColorUtils.hexToHsv(this.color)this.hue=hsv[0]this.sat=hsv[1]this.val=hsv[2]}// 绘制颜色条渐变时:HSV → HEXdrawColorBar(){for(leti=Constants.HEU_SCALE;i>=0;i--,count++){grad.addColorStop(1-i/Constants.HEU_SCALE,ColorUtils.hsvToHex(i,1,1))}}// 用户选色后输出:HSV → HEXgetColor():string{returnColorUtils.hsvToHex(this.hue,this.sat,this.val);}

三处用法:初始化时把外部颜色转成内部 HSV 表示,绘制颜色条时用不同色相生成渐变色,用户操作后把 HSV 转回 HEX 输出。


验证一个例子

试着手算一下蓝色#0000ff

  • r=0, g=0, b=1
  • max=1 (b), min=0, delta=1
  • v = 1
  • s = 1/1 = 1
  • max === b,h = (r-g)/delta + 4 = (0-0)/1 + 4 = 4,乘以 60 = 240

结果是[240, 1, 1],H=240 确实是蓝色。

反过来,hsvToHex(240, 1, 1)

  • i = floor(240/60) = 4
  • f = 240/60 - 4 = 0
  • p = 1*(1-1) = 0, q = 1*(1-01) = 1, t = 1(1-1*1) = 0
  • case 4: r=t=0, g=p=0, b=v=1
  • #0000ff

写在最后

HSV 转换这块数学上并不难,就是映射关系多了点。ColorUtils里把这两个互转函数封装好,ColorSelector用起来就很干净,不用在 UI 代码里夹杂颜色计算逻辑。这种"工具类单独抽出来"的做法,在任何稍大一点的项目里都值得坚持。

到这里五篇文章就写完了,从项目架构到每个模块的细节都过了一遍。如果是第一次接触 HarmonyOS ArkUI 开发,这个项目代码量适中,涉及的知识点又比较全面,很适合拿来边读边练。

http://www.jsqmd.com/news/990114/

相关文章:

  • 广州六区黄金回收实测:谁更值得信赖 - 余生黄金回收
  • AI写论文新选择!这4款AI论文写作工具,为你的学术创作助力!
  • 质量好的潜水排污泵厂家哪家好?2026年行业厂商综合能力分析 - 优质品牌商家
  • 番茄小说下载器:3个技巧让你随时随地畅享离线阅读
  • java+vue+SpringBoot校园体育场馆使用管理系统(程序+数据库+报告+部署教程+答辩指导)
  • 别再傻等下载了!一个脚本把百度网盘分享链接先批量‘收藏’再统一处理
  • Java(数组)
  • 别再只把Voronoi图当数学概念了!用Python从零生成艺术纹理,附完整代码
  • 终极文档下载革命:如何用kill-doc脚本一键获取30+平台文档资源
  • Linphone 6.0.7:你的通讯工具如何变得更懂你?
  • 用原生JS和Canvas从零撸一个功能齐全的在线画板(支持撤销/恢复/保存PNG)
  • 数据的加密与解密(05:00)
  • 例会/晨会/早会/周会录音转文字神器亲测推荐:效率翻倍不踩坑
  • 5个技巧掌握Pywinauto:Windows自动化测试的终极指南
  • 火箭六自由度姿态仿真MATLAB工具包:含气动力建模、四元数解算与PID闭环控制
  • 2026广州黄金回收市场红黑榜实测 - 余生黄金回收
  • 35GHz八单元偶极子MIMO射频链路Simulink建模包:含OFDM波束赋形与天线互耦仿真
  • 终极免费解决方案:3分钟搭建个人专属付费墙绕过工具
  • 从NVD到你的工单:如何用Python脚本自动抓取并解析CVE的CVSS 3.1评分?
  • 计算机毕业设计之django基于计算机专业的考研志愿填报模拟系统
  • 华硕笔记本性能优化指南:5个技巧告别奥创中心卡顿
  • C#写的30个PPT式图片切换动画源码,拉幕旋转分块淡入全都有
  • STM32F103C8T6驱动TM1616数码管模块:从硬件连接到完整代码移植(附避坑点)
  • FPGA做FFT时,你的输入数据格式对了吗?手把手解决锯齿波分析的实部虚部拼接问题
  • 抖音无水印下载神器:批量保存视频、直播、音乐的全能解决方案
  • 正规的佛山老酒回收推荐:2026年本地市场格局与服务机构分析 - 优质品牌商家
  • 苹果CMS V10站长专用:萌芽采集Pro插件v10.7.3一键部署包(含后台入口+配置说明)
  • 2026免费抠图软件保姆级教程:电脑手机在线无水印,一篇搞定
  • 终极倒计时解决方案:jQuery.countdown完整使用指南
  • APA 7th Edition格式生成器:一键解决学术写作格式烦恼的终极方案