前言

上个月帮朋友调一张海报,设计师甩过来一串 #3F7CAC,让我在另一款软件里用 RGB 数值复刻。我盯着那六个字符,在调色板里拖了半天,结果色差大到朋友以为我是色盲。后来我才知道,那款软件只认 HSL,而我对这三个字母的理解仅限于“H 好像是色相”。那天晚上我翻出了色彩科学的资料,把 RGB、HEX、HSL 的换算公式老老实实推导了一遍。弄明白之后我忽然觉得,这种互转其实很适合做成一个随身小工具——三个滑块拖一拖,颜色就在屏幕上实时变化,对应的六种格式(RGB 数值、HEX 字符串、HSL 数值)自动同步。于是我打开 DevEco Studio 6.1.1 Beta1,在 Pura X Max 模拟器上花了一个晚上,把这座“颜色转换实验室”搭了出来。这篇文章就是那个晚上的成果,里面不仅有一份能直接跑起来的代码,还夹带着 RGB 的叠加原理、HSL 的几何直觉、以及为什么 #FF0000rgb(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 的十六进制是 FF000,所以 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 调节面板。界面从上到下分为几层:

  1. 颜色预览大色块:一个高约 150 像素的圆角矩形,背景色动态绑定到当前 RGB 值。用户拖动任何滑块,这个色块的颜色都实时变化。它的存在让整个工具有了“所见即所得”的直观感受,比看数字快得多。
  2. RGB 滑块区域:三条横向滑块,分别控制 R(红色)、G(绿色)、B(蓝色),取值范围 0~255,步长 1。每条滑块左侧标注通道字母,右侧显示当前数值。滑块的颜色也用对应通道的颜色来渲染(R 的滑块轨道是红色系),这样一眼就能分清哪根管子控制什么。
  3. 格式转换结果区:在色块下方,用四行文本分别展示当前颜色的 HEX 值(如 #FF5733)、RGB 值(rgb(255, 87, 51))、HSL 值(hsl(12, 100%, 60%))和 CMYK 近似值(可选)。每行左侧是标签,右侧是数值,用户可以长按复制。
  4. 预设颜色快捷按钮:为了方便,我放了几个常用的颜色方块(红、橙、黄、绿、青、蓝、紫、白、灰、黑),点击即可把 RGB 滑块调到对应数值。这些小色块排成两排,点击哪一个,背景预览块就变成哪个颜色,滑块同步更新。

所有状态用 @State 管理:redgreenblue 三个数字变量。滑块 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: #FF5733RGB: 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% 亮度——一种浓郁的青色。颜色不再是神秘的代码,而是你手里滑块的实时映射。这就是从消费者到创造者转变的乐趣。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐