HarmonyOS ArkTS 颜色提取器的色彩算法与 Builder 复用
引子取色不难配色才难颜色提取器有两件事一是让用户手动调 RGB 滑块选颜色二是根据当前颜色自动生成配色方案。前者是交互问题后者是算法问题。手动调滑块谁都会做但配色方案涉及色彩理论——互补色、类似色、三等分色每个概念背后都是 HSL 色彩模型的数学计算。今天就从代码出发拆解 RGB 和 HSL 的互转、配色方案的生成逻辑、Builder 的组件复用。完整效果数据模型三个 State 管理颜色Statered:number175;Stategreen:number82;Stateblue:number222;Statehex:string#AF52DE;StateextractedColors:string[][];Stateharmonies:string[][];为什么 RGB 和 hex 都要存RGB 用于滑块显示Slider的值是 0-255hex 用于显示和存储用户看到的是#AF52DE。两者是同一个颜色的不同表示需要同步更新。extractedColors 和 harmonies 的区别extractedColors随机色盘生成的 5 个颜色harmonies基于当前颜色的配色方案互补、类似、三等分两者独立计算互不影响。RGB 转 hex进制转换updateHex():void{constrthis.red.toString(16).padStart(2,0).toUpperCase();constgthis.green.toString(16).padStart(2,0).toUpperCase();constbthis.blue.toString(16).padStart(2,0).toUpperCase();this.hex#rgb;this.genHarmonies();}toString(16) 的作用把数字转成 16 进制字符串。比如175→af82→52222→de。padStart(2, ‘0’) 的作用如果 16 进制只有一位比如10→a前面补 0 变成0a。确保每个颜色分量都是两位。toUpperCase() 的作用16 进制默认小写af52de转成大写AF52DE更符合设计规范。为什么每次 updateHex 都要 genHarmoniesRGB 变了hex 就变了配色方案也要重新计算。保证颜色和配色方案始终同步。RGB 转 HSL色彩模型的转换hexToHsl(hex:string):[number,number,number]{constrparseInt(hex.slice(1,3),16)/255;constgparseInt(hex.slice(3,5),16)/255;constbparseInt(hex.slice(5,7),16)/255;constmaxMath.max(r,g,b);constminMath.min(r,g,b);leth0,s0,l(maxmin)/2;if(max!min){constdmax-min;sl0.5?d/(2-max-min):d/(maxmin);if(maxr)h((g-b)/d(gb?6:0))*60;elseif(maxg)h((b-r)/d2)*60;elseh((r-g)/d4)*60;}return[Math.round(h),Math.round(s*100),Math.round(l*100)];}什么是 HSLHSL 是另一种表示颜色的方式HHue色相0-360 度表示颜色种类0红120绿240蓝SSaturation饱和度0-100%表示颜色的鲜艳程度LLightness亮度0-100%表示颜色的明暗程度转换算法的原理先把 RGB 归一化到 0-1 范围除以 255找到最大值和最小值亮度 L (max min) / 2如果 max ≠ min不是灰色计算饱和度 S 和色相 H色相的计算色相根据最大值是 R、G 还是 B 来决定最大值是 RH ((G - B) / d) × 60最大值是 GH ((B - R) / d 2) × 60最大值是 BH ((R - G) / d 4) × 60d是最大值和最小值的差。如果 G BH 要加 360确保 H 在 0-360 范围内。饱和度的计算S l 0.5 ? d / (2 - max - min) : d / (max min)亮度高时l 0.5用d / (2 - max - min)亮度低时用d / (max min)。这样能保证 S 始终在 0-1 范围内。HSL 转 RGB逆向转换hslToHex(h:number,s:number,l:number):string{s/100;l/100;constas*Math.min(l,1-l);constf(n:number){constk(nh/30)%12;returnMath.round((l-a*Math.max(-1,Math.min(k-3,9-k,1)))*255);};return#[f(0),f(8),f(4)].map(vv.toString(16).padStart(2,0).toUpperCase()).join();}算法的核心这个算法比 RGB→HSL 简洁得多。核心公式k (n h/30) % 12 RGB l - a × max(-1, min(k-3, 9-k, 1))其中a s × min(l, 1-l)。为什么用 f(0)、f(8)、f(4)这三个值分别对应 R、G、B 三个通道。通过不同的n值让三个通道在色轮的不同位置取值实现颜色的转换。为什么不直接用 RGB 公式标准的 RGB→HSL 转换需要 6 个 if-else 分支根据色相所在的区间。这个算法用一个公式覆盖了所有区间代码更简洁。配色方案基于色轮的数学genHarmonies():void{consthslArrthis.hexToHsl(this.hex);consthhslArr[0],shslArr[1],lhslArr[2];constharm:string[][];harm.push(this.hslToHex((h180)%360,s,l));// 互补色harm.push(this.hslToHex((h30)%360,s,l));// 类似色harm.push(this.hslToHex((h60)%360,s,l));// 类似色harm.push(this.hslToHex((h120)%360,s,l));// 三等分harm.push(this.hslToHex((h240)%360,s,l));// 三等分this.harmoniesharm;}五种配色方案的原理方案色相偏移原理互补色180°色轮对面的颜色对比最强类似色30°色轮相邻的颜色和谐自然类似色60°稍远的相邻色有变化但不冲突三等分120°色轮三分之一处平衡感强三等分240°色轮三分之二处和三等分对称饱和度和亮度保持不变所有配色方案都保持原色的 S 和 L 不变只改变 H。这样生成的颜色在视觉上是同一色调的不同方向不会出现一个鲜艳一个灰暗的不协调。取余 360 的作用色相是 0-360 度的循环。偏移后可能超过 360比如 h350偏移 30 度变成 380取余后回到 20 度。随机色盘基于色相的随机生成pickPalette():void{constbaseHMath.floor(Math.random()*360);constcols:string[][];cols.push(this.hslToHex(baseH,70,55));cols.push(this.hslToHex((baseH30)%360,60,50));cols.push(this.hslToHex((baseH120)%360,65,55));cols.push(this.hslToHex((baseH180)%360,55,45));cols.push(this.hslToHex((baseH240)%360,70,50));this.extractedColorscols;}随机的策略不是完全随机——先随机一个基础色相0-360然后基于这个色相生成 5 个颜色。5 个颜色的色相偏移分别是 0°、30°、120°、180°、240°覆盖了色轮的主要区域。饱和度和亮度的微调每个颜色的 S 和 L 略有不同比如 70/55、60/50、65/55让色盘看起来更自然。如果所有颜色的 S 和 L 都一样会显得单调。点击色块的效果.onClick((){this.redparseInt(c.slice(1,3),16);this.greenparseInt(c.slice(3,5),16);this.blueparseInt(c.slice(5,7),16);this.updateHex();})点击随机色盘的颜色会把 RGB 滑块设成对应值用户可以在此基础上微调。Builder可复用的滑块组件BuildersliderRed(){Row(){Text(R).fontSize(12).fontWeight(FontWeight.Bold).fontColor(#FF2D55).width(20)Slider({value:this.red,min:0,max:255,style:SliderStyle.InSet}).blockColor(#FF2D55).trackColor(#2A2A3E).selectedColor(#FF2D55).onChange((v:number){this.redv;this.updateHex();}).layoutWeight(1).margin({left:10,right:10})Text(this.red.toFixed(0)).fontSize(12).fontColor(#FFFFFF).width(30).textAlign(TextAlign.End)}.width(100%).margin({bottom:14})}Builder 的作用Builder定义了一个可复用的 UI 片段。三个滑块R、G、B的结构完全一样只有颜色和绑定的变量不同。用Builder避免了三份重复代码。为什么不用 ComponentComponent是独立的组件有自己的生命周期和状态。滑块不需要独立的状态RGB 值由父组件管理用Builder更轻量。Slider 的样式SliderStyle.InSet滑块嵌入轨道内更紧凑blockColor滑块的颜色trackColor轨道的颜色深色背景selectedColor已选部分的颜色三个滑块分别用红、绿、蓝色视觉上直观。配色方案的展示ForEach(this.harmonies,(c:string,idx:number){Row(){Column().width(40).height(40).borderRadius(8).backgroundColor(c).margin({right:12})Column(){Text(this.harmonyLabel(idx)).fontSize(13).fontColor(#FFFFFF)Text(c).fontSize(11).fontColor(#888)}Blank()Button(用这个).fontSize(11).height(28).borderRadius(8).backgroundColor(#2A2A3E).fontColor(#AF52DE).onClick((){...})}.width(100%).padding({top:8,bottom:8}).border({width:{bottom:0.5},color:#333})})每行的结构左边40×40 的颜色方块中间配色方案名称 hex 值右边用这个按钮用这个按钮的功能点击后把当前配色方案的颜色设为 RGB 值用户可以在此基础上继续调整。底部分割线.border({width:{bottom:0.5}, color:#333})给每行加了 0.5px 的底部边框作为行与行之间的分隔。踩坑记录坑 1hexToHsl 的精度RGB 转 HSL 再转回 RGB可能有微小的精度误差。比如(175, 82, 222)→ HSL → hex 可能变成#AF51DE而不是#AF52DE。这是因为浮点数计算的舍入误差。坑 2padStart 的类型toString(16).padStart(2, 0)必须先toString(16)再padStart。如果顺序反了padStart会报错。坑 3Slider 的值类型Slider的onChange回调参数是number不是string。如果用字符串接收会报错。坑 4Builder 的 this 指向Builder里的this指向父组件可以直接访问this.red、this.updateHex()等。但Builder不能有自己的State。坑 5ForEach 的 keyForEach遍历配色方案时用索引idx作为隐式 key。如果数组顺序不变不会有问题。但如果数组会动态增删需要显式指定 key。代码改进建议1. 图片取色当前只有手动调滑块和随机色盘。可以加图片取色功能——用户选择图片点击某个像素获取颜色。2. 颜色历史保存最近使用的颜色方便用户回溯。3. 导出配色方案把配色方案导出为 JSON 或图片方便设计师使用。4. 色盲友好当前配色方案基于正常视觉。可以加色盲模拟功能让色弱用户也能看清。5. 渐变生成根据两个颜色生成渐变色用于 UI 设计。总结颜色提取器的核心是色彩模型的转换——RGB 用于显示和交互HSL 用于计算配色方案。配色方案基于色轮的数学关系互补、类似、三等分保持饱和度和亮度不变只改变色相。适用边界这个部分适合用作 ArkUI 颜色处理和 Builder 复用的学习案例涵盖了 RGB/HSL 互转、配色方案生成、Slider 组件、Builder 用法、ForEach 渲染等核心知识点。但如果要上架应用商店还需要补充图片取色、颜色历史、导出色板、色盲友好、渐变生成等内容。

相关新闻