红绿蓝的炼金术——我在 HarmonyOS 里搭了一座颜色转换实验室
前言

上个月帮朋友调一张海报,设计师甩过来一串 #3F7CAC,让我在另一款软件里用 RGB 数值复刻。我盯着那六个字符,在调色板里拖了半天,结果色差大到朋友以为我是色盲。后来我才知道,那款软件只认 HSL,而我对这三个字母的理解仅限于“H 好像是色相”。那天晚上我翻出了色彩科学的资料,把 RGB、HEX、HSL 的换算公式老老实实推导了一遍。弄明白之后我忽然觉得,这种互转其实很适合做成一个随身小工具——三个滑块拖一拖,颜色就在屏幕上实时变化,对应的六种格式(RGB 数值、HEX 字符串、HSL 数值)自动同步。于是我打开 DevEco Studio 6.1.1 Beta1,在 Pura X Max 模拟器上花了一个晚上,把这座“颜色转换实验室”搭了出来。这篇文章就是那个晚上的成果,里面不仅有一份能直接跑起来的代码,还夹带着 RGB 的叠加原理、HSL 的几何直觉、以及为什么 #FF0000 和 rgb(255, 0, 0) 其实是同一种红色。
一、RGB——光的三种原色
RGB 的全称是 Red(红)、Green(绿)、Blue(蓝)。这三色是加色法的三原色,混合在一起可以生成几乎所有人眼能感知的颜色。每种子色的取值范围是 0 到 255(一个字节),所以 RGB 能表达的颜色总数是 256³ ≈ 1677 万色,通常称为真彩色。
从计算机显卡到手机屏幕,所有发光设备都是用 RGB 来显示颜色的。屏幕上每一个像素,其实就是三个微小的发光单元(红、绿、蓝)按不同强度组合在一起。当红和绿全亮、蓝不亮时,你看到的是黄色;红和蓝全亮、绿不亮时,是品红色;三者全亮时是白色,全灭时是黑色。
RGB 的表示方法也很直接,比如 rgb(255, 0, 0) 就是纯红色。但在代码和设计工具里,更常见的写法是十六进制 HEX 格式,比如 #FF0000。它其实就是把 R、G、B 三个十进制数分别转成两位十六进制数拼在一起。255 的十六进制是 FF,0 是 00,所以 rgb(255, 0, 0) 就是 #FF0000。反过来,把 HEX 字符串每两位切一段转回十进制,就得到了 RGB 数值。这个转换在我们的小工具里是最基础的功能。
二、HSL——让颜色变得“可描述”
RGB 虽然精确,但对人类来说很不直观。如果让你“调一个暖一点的橙色”,你应该增加 R 还是减少 G?HSL 就是为解决这个痛点而生的,它用三个更符合直觉的维度来描述颜色:
- H(色相 Hue):颜色的基本属性,在色环上用 0°~360° 表示。0° 是红色,120° 是绿色,240° 是蓝色。
- S(饱和度 Saturation):颜色的纯度,0%~100%。100% 是完全饱和的鲜艳颜色,0% 则是灰色(无论色相如何)。
- L(亮度 Lightness):颜色的明暗程度,0% 是纯黑,100% 是纯白,50% 是正常亮度。
从 RGB 到 HSL 的转换涉及一些三角函数和比较运算,但原理并不复杂:首先把 R、G、B 除以 255 归一化到 0~1,然后找到最大值 max 和最小值 min,算出它们的差值 delta。亮度 L 就是 (max + min) / 2。饱和度 S 则取决于 delta:如果 delta 为 0(即 R=G=B),S 为 0;否则 S = delta / (1 - |2L - 1|)。色相 H 的计算需要根据哪个颜色通道是最大值来分别处理,最后归一化到 0°~360°。
反过来,从 HSL 回算 RGB 也是一套固定的公式。这些公式在计算机图形学里已经用了几十年,我们直接“拿来主义”,封装成几个转换函数就行。在工具里,用户拖 RGB 滑块时,程序自动计算对应的 HEX 和 HSL;反过来,如果将来扩展 HSL 输入模式,也能同步更新 RGB 和 HEX。不过为了操作直观,我选择用三个 RGB 滑块作为主要交互方式,因为 RGB 最贴近硬件,滑块调起来最直接。
三、界面怎么搭——三个滑块、一个预览方块、四行文字
工具的核心是一块颜色预览区域和一个 RGB 调节面板。界面从上到下分为几层:

- 颜色预览大色块:一个高约 150 像素的圆角矩形,背景色动态绑定到当前 RGB 值。用户拖动任何滑块,这个色块的颜色都实时变化。它的存在让整个工具有了“所见即所得”的直观感受,比看数字快得多。
- RGB 滑块区域:三条横向滑块,分别控制 R(红色)、G(绿色)、B(蓝色),取值范围 0~255,步长 1。每条滑块左侧标注通道字母,右侧显示当前数值。滑块的颜色也用对应通道的颜色来渲染(R 的滑块轨道是红色系),这样一眼就能分清哪根管子控制什么。
- 格式转换结果区:在色块下方,用四行文本分别展示当前颜色的 HEX 值(如
#FF5733)、RGB 值(rgb(255, 87, 51))、HSL 值(hsl(12, 100%, 60%))和 CMYK 近似值(可选)。每行左侧是标签,右侧是数值,用户可以长按复制。 - 预设颜色快捷按钮:为了方便,我放了几个常用的颜色方块(红、橙、黄、绿、青、蓝、紫、白、灰、黑),点击即可把 RGB 滑块调到对应数值。这些小色块排成两排,点击哪一个,背景预览块就变成哪个颜色,滑块同步更新。
所有状态用 @State 管理:red、green、blue 三个数字变量。滑块 onChange 时更新对应变量,然后通过计算属性(或直接在 build 中调用的函数)得到 HEX、HSL 等结果字符串。由于 ArkUI 是声明式的,这些结果会自动刷新,不需要手动操作 DOM。
颜色预览方块的背景色用 backgroundColor 属性,但它只支持直接的颜色字符串或资源引用。我们可以动态拼接一个 rgb(r, g, b) 字符串赋给一个样式变量。在 ArkUI 中,backgroundColor 可以接受字符串,比如 'rgb(255, 100, 50)',所以直接用模板字符串拼出来就行。
四、几个关键的转换公式
RGB 转 HEX 很简单:

function toHex(r: number, g: number, b: number): string {
let rh = r.toString(16).padStart(2, '0').toUpperCase();
let gh = g.toString(16).padStart(2, '0').toUpperCase();
let bh = b.toString(16).padStart(2, '0').toUpperCase();
return `#${rh}${gh}${bh}`;
}
RGB 转 HSL 的代码稍微复杂,但照着标准公式写就行:

function rgbToHsl(r: number, g: number, b: number): { h: number, s: number, l: number } {
r /= 255; g /= 255; b /= 255;
let max = Math.max(r, g, b), min = Math.min(r, g, b);
let h = 0, s = 0, l = (max + min) / 2;
let d = max - min;
if (d !== 0) {
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
case g: h = ((b - r) / d + 2) / 6; break;
case b: h = ((r - g) / d + 4) / 6; break;
}
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100)
};
}
这两组函数构成了工具的运算核心。用户拖滑块时,直接调用它们更新显示。
五、完整代码——红绿蓝的即时转换实验室
以下代码适配 DevEco Studio 6.1.1 Beta1、SDK22 语法,Pura X Max 模拟器。新建 Empty Ability 项目,替换 entry/src/main/ets/pages/Index.ets。无需任何权限,纯本地运算。

/*
* 颜色格式互转器 — RGB / HEX / HSL 实时转换
* 环境:DevEco Studio 6.1.1 Beta1,Pura X Max 模拟器,SDK22
*/
@Entry
@Component
struct Index {
@State red: number = 255;
@State green: number = 87;
@State blue: number = 51;
// 预设颜色列表
private readonly presets: { name: string, r: number, g: number, b: number }[] = [
{ name: '红', r: 255, g: 0, b: 0 },
{ name: '橙', r: 255, g: 165, b: 0 },
{ name: '黄', r: 255, g: 255, b: 0 },
{ name: '绿', r: 0, g: 255, b: 0 },
{ name: '青', r: 0, g: 255, b: 255 },
{ name: '蓝', r: 0, g: 0, b: 255 },
{ name: '紫', r: 128, g: 0, b: 128 },
{ name: '白', r: 255, g: 255, b: 255 },
{ name: '灰', r: 128, g: 128, b: 128 },
{ name: '黑', r: 0, g: 0, b: 0 }
];
// RGB 转 HEX
private getHex(): string {
let rh = this.red.toString(16).padStart(2, '0').toUpperCase();
let gh = this.green.toString(16).padStart(2, '0').toUpperCase();
let bh = this.blue.toString(16).padStart(2, '0').toUpperCase();
return `#${rh}${gh}${bh}`;
}
// RGB 转 HSL
private getHsl(): { h: number, s: number, l: number } {
let r = this.red / 255, g = this.green / 255, b = this.blue / 255;
let max = Math.max(r, g, b), min = Math.min(r, g, b);
let h = 0, s = 0, l = (max + min) / 2;
let d = max - min;
if (d !== 0) {
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
else if (max === g) h = ((b - r) / d + 2) / 6;
else h = ((r - g) / d + 4) / 6;
}
return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
}
// 设置预设颜色
private setPreset(r: number, g: number, b: number): void {
this.red = r;
this.green = g;
this.blue = b;
}
build() {
let hex = this.getHex();
let hsl = this.getHsl();
let rgbStr = `rgb(${this.red}, ${this.green}, ${this.blue})`;
let hslStr = `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`;
Column() {
Text('颜色格式转换器')
.fontSize(26)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 8 })
Text('RGB / HEX / HSL 实时互转')
.fontSize(15)
.fontColor('#888')
.margin({ bottom: 15 })
// 颜色预览色块
Row() {
Column()
.width(120)
.height(120)
.backgroundColor(rgbStr)
.borderRadius(12)
.shadow({ radius: 8, color: '#20000000', offsetY: 4 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ bottom: 15 })
// 格式结果
Column() {
Row() { Text('HEX').fontSize(14).fontColor('#888').width(60); Text(hex).fontSize(16).fontWeight(FontWeight.Medium).fontFamily('monospace') }.margin({ bottom: 6 })
Row() { Text('RGB').fontSize(14).fontColor('#888').width(60); Text(rgbStr).fontSize(16).fontWeight(FontWeight.Medium).fontFamily('monospace') }.margin({ bottom: 6 })
Row() { Text('HSL').fontSize(14).fontColor('#888').width(60); Text(hslStr).fontSize(16).fontWeight(FontWeight.Medium).fontFamily('monospace') }
}
.width('88%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ bottom: 15 })
// RGB 滑块
Column() {
Row() {
Text('R').fontSize(16).fontColor('#E53935').width(24)
Slider({ value: this.red, min: 0, max: 255, step: 1, style: SliderStyle.OutSet })
.layoutWeight(1).color('#E53935')
.onChange((v) => { this.red = v; })
Text(`${this.red}`).fontSize(14).fontColor('#666').width(36).textAlign(TextAlign.End)
}.width('100%').margin({ bottom: 6 })
Row() {
Text('G').fontSize(16).fontColor('#4CAF50').width(24)
Slider({ value: this.green, min: 0, max: 255, step: 1, style: SliderStyle.OutSet })
.layoutWeight(1).color('#4CAF50')
.onChange((v) => { this.green = v; })
Text(`${this.green}`).fontSize(14).fontColor('#666').width(36).textAlign(TextAlign.End)
}.width('100%').margin({ bottom: 6 })
Row() {
Text('B').fontSize(16).fontColor('#2196F3').width(24)
Slider({ value: this.blue, min: 0, max: 255, step: 1, style: SliderStyle.OutSet })
.layoutWeight(1).color('#2196F3')
.onChange((v) => { this.blue = v; })
Text(`${this.blue}`).fontSize(14).fontColor('#666').width(36).textAlign(TextAlign.End)
}.width('100%')
}
.width('88%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ bottom: 15 })
// 预设颜色
Column() {
Text('预设颜色').fontSize(14).fontColor('#888').margin({ bottom: 6 }).alignSelf(ItemAlign.Start)
Row() {
ForEach(this.presets.slice(0, 5), (preset) => {
Button(preset.name)
.fontSize(12).height(32).margin(2)
.backgroundColor(`rgb(${preset.r}, ${preset.g}, ${preset.b})`)
.fontColor(preset.r + preset.g + preset.b > 400 ? '#333' : '#FFF')
.onClick(() => { this.setPreset(preset.r, preset.g, preset.b); })
})
}
Row() {
ForEach(this.presets.slice(5, 10), (preset) => {
Button(preset.name)
.fontSize(12).height(32).margin(2)
.backgroundColor(`rgb(${preset.r}, ${preset.g}, ${preset.b})`)
.fontColor(preset.r + preset.g + preset.b > 400 ? '#333' : '#FFF')
.onClick(() => { this.setPreset(preset.r, preset.g, preset.b); })
})
}
}
.width('88%')
Text('💡 拖动 RGB 滑块,实时生成 HEX 与 HSL 值')
.fontSize(12).fontColor('#AAA').width('90%').textAlign(TextAlign.Center).margin({ top: 10 })
}
.width('100%').height('100%').backgroundColor('#FAFAFA')
}
}
六、运行效果
把代码粘贴进 DevEco Studio,Run 到 Pura X Max 模拟器。屏幕上方是一个大色块,默认显示暖橙色(255,87,51)。下方清晰列出 HEX: #FF5733、RGB: rgb(255, 87, 51)、HSL: hsl(12, 100%, 60%)。拖动 R 滑块,色块逐渐变红,HEX 值同步变化;拖动 G 滑块,色块变黄绿,HSL 色相跟着移动。三个滑块互相配合,可以调出任意颜色。点击预设区域里的“蓝”按钮,RGB 滑块自动跳到 (0,0,255),色块变成纯蓝。整个过程实时响应,滑块跟手,没有任何延迟。

总结
这个颜色转换工具其实是一个微型的色彩数学实验室,通过它,我们可以直观地看到:
- RGB 与 HEX 的等价关系:两种表示法只是同一组数字的不同进制,十六进制不过是把 0-255 的数字变成了两位字符,方便在 CSS 和设计文件中书写。
- HSL 的直觉优势:用色相、饱和度、亮度来描述颜色,比红绿蓝三色混合更贴近人的思维方式。当我们说“深一点的蓝”,实际上是在调低 L 值,而不是同时降低 R 和 G,这在 RGB 里很难一眼看穿。
- 声明式 UI 的数据驱动:三个
@State变量控制整个界面的颜色,任何改动都会自动刷新预览色块和所有格式的数字,这是 ArkUI 最核心的开发体验。 - 数学在视觉中的应用:RGB 转 HSL 的公式虽然较长,但本质上是把三维的 RGB 空间映射到更适合人类操作的 HSL 圆柱坐标系。理解这种映射关系,对处理任何颜色工具都很有帮助。
下次当你看到 #00CED1 这种暗语时,打开这个小工具拖一拖,你就会知道它是 R=0, G=206, B=209,HSL 是 181 度、100% 饱和、41% 亮度——一种浓郁的青色。颜色不再是神秘的代码,而是你手里滑块的实时映射。这就是从消费者到创造者转变的乐趣。
更多推荐


所有评论(0)