# QQ 超级调色盘的基础色派生算法

适用版本：Windows QQ 9.9.36-53644。输入一个不透明 RGB 基础色和模板编号（1001–1004），输出三条色阶、模板根色及界面 Token。这里整理的是安装文件中恢复的算法；纹理和渐变合成不属于颜色求解器。

## 基础色先转换成 HCT

一套界面配色要分别控制颜色的倾向、鲜艳程度和明暗层级。RGB 的三个通道适合保存和显示颜色，但「把绿色通道减一点」很难直接表达「保留紫色倾向，让背景更浅、更柔和」这样的设计要求。

HSL 把颜色改写成色相、饱和度和亮度，也还没有解决感知明度的问题。例如下面两个纯色，HSL 的 L 都是 50%，转换成 CIE L* 后却差得很远。数值由配套的颜色模块计算：

| 颜色 | HEX | HSL | CIE L* / HCT T |
| --- | --- | --- | ---: |
| 纯蓝 | `#0000FF` | `240° / 100% / 50%` | 32.303 |
| 纯黄 | `#FFFF00` | `60° / 100% / 50%` | 97.138 |

如果用同一组 HSL 亮度直接生成蓝色、黄色主题，组件的明暗层级会随着色相改变。HCT 将色相 H、彩度 C 与感知明度 T 分开处理，方便用相同的明度规则生成不同主题。H、C 来自 CAM16 色貌模型，T 使用 CIE L*；H 是 0–360° 的角度，T 从 0 的黑到 100 的白，C 表示颜色偏离中性色的程度，它的可用上限随 H、T 改变。[HCT 的实现与定义](https://github.com/material-foundation/material-color-utilities/blob/main/typescript/hct/hct.ts)说明了这三个量的组合方式。

RGB 到感知明度之间还隔着一次线性化。HEX 里的 sRGB 通道经过非线性编码，字节值 128 并不代表白色一半的光强。把一个通道除以 255 得到 `u`，先按 sRGB 传递函数还原为线性通道值，再计算相对亮度 Y。以 0–1 为取值范围，公式是：

```text
linear(u) = u / 12.92                         当 u ≤ 0.04045
          = ((u + 0.055) / 1.055) ^ 2.4       其他情况

Y = 0.2126 × Rlinear + 0.7152 × Glinear + 0.0722 × Blinear
```

绿色与蓝色对 Y 的权重不同，因此三个通道的数值平均不能表示明度。sRGB 的解码方式见 [CSS Color 4 的颜色转换定义](https://www.w3.org/TR/css-color-4/#color-conversion-code)。T 再由 Y 计算成 CIE L*，使用分段函数近似感知明度：

```text
T = (24389 / 27) × Y          当 Y ≤ 216 / 24389
  = 116 × Y^(1/3) − 16       其他情况
```

H、C 则使用 RGB 转成的 XYZ 三刺激值继续计算。XYZ 是颜色测量的一种表示，Y 就是其中的亮度分量。CAM16 再根据参考白、背景和适应亮度等观察条件处理这些量，得到色相与彩度。[CAM16 的实现](https://github.com/material-foundation/material-color-utilities/blob/main/typescript/hct/cam16.ts)包含这条转换链。本次复现使用库的默认观察条件，没有根据真实屏幕亮度或 QQ 窗口背景动态调整它们。

记基础色转换结果为 `(h, c, t)`，QQ 的主题规则先定义两个明度范围：

```js
const low = Math.max(10, Math.min(70, t));
const high = Math.max(70, Math.min(95, t));
const F = (h, c, t) => Hct.from(h, c, t).toInt();
```

例如种子明度是 30，low 为 30，high 为 70；种子明度是 90，low 为 70，high 为 90。它们把任意输入约束到两个范围，供不同界面角色选择。具体上限、下限属于 QQ 的主题规则，HCT 本身不会自动规定这些数值。

## HCT 转回 RGB 时怎样处理色域

F 返回可显示的不透明 ARGB。它要同时寻找接近目标 H、C、T 的 sRGB 颜色，但屏幕能表示的 RGB 组合有限。

> 色域是一个颜色空间能够表示的颜色范围。同一个色相在不同明度下，可达到的最大彩度不同；接近白色或黑色时，许多鲜艳颜色无法同时满足指定的明度。

配套模块的 `Hct.from()` 会调用 `HctSolver.solveToInt()`。对普通有彩色目标，求解器先把 T 还原为目标 Y，再固定 H、C，迭代调整 CAM16 的明度参数 J，让反算的 RGB 具有目标 Y。J 与 HCT 的 T 是不同的量，因此这一步需要求解。本次附带版本使用近似牛顿迭代，最多进行 5 轮。

如果找不到范围合法的 RGB，求解器沿固定 Y 的平面与 RGB 色域边界的交线二分，寻找接近目标色相的可显示颜色。最后进行 sRGB 编码和 8 位量化。靠近黑、白或彩度接近零的输入有单独的灰阶分支。这些路径可以对照 [HCT 求解器的实现](https://github.com/material-foundation/material-color-utilities/blob/main/typescript/hct/hct_solver.ts)。

以「香芋啵啵」的色相 304.365°、目标彩度 57.019 为例，只改变目标 T，求解器的实际结果如下：

| 目标 T | 求解后的 HEX | 实际 H | 实际 C | 实际 T |
| ---: | --- | ---: | ---: | ---: |
| 20 | `#400E81` | 304.011° | 57.102 | 20.000 |
| 50 | `#8A61CD` | 304.309° | 57.146 | 49.930 |
| 80 | `#D5BAFF` | 304.338° | 37.882 | 79.888 |
| 95 | `#F7EDFF` | 303.553° | 11.670 | 94.976 |

目标 T 为 80、95 时，实际彩度分别降到了 37.882、11.670，才能留在 sRGB 色域中；色相和明度也会有量化误差。因此根色规则里的三元组表示目标参数，读者拿最终 HEX 反算 HCT 时，不应要求三个参数逐字相等。派生时每个目标都从同一份原始 `(h, c, t)` 独立求解，避免把上一层已经降低的彩度继续传给下一层。


## 三条色阶

| 系列 | 编号 | 主色阶彩度 `cp` |
| --- | ---: | --- |
| 缤纷时刻 | 1001 | `max(c, 50)` |
| 幻彩机核 | 1002 | `c` |
| 水墨国风 | 1003 | `min(c, 30)` |
| 神秘美学 | 1004 | `c` |

三条色阶都保持基础色色相 h：primary 使用 cp，secondary 使用 cp/3，neutral 使用 cp/6。`primary_root`、`secondary_root`、`neutral_root` 的明度都是 low；其余明度取固定列表：

```text
100, 99, 98, 95, 90, 80, 70, 60, 50, 40, 35, 30, 25, 20, 15, 10, 5, 0
```

例如 `secondary_95 = F(h, cp / 3, 95)`。这三条色阶各包含 19 个颜色，并不等于 Material 的标准 CorePalette / Scheme。复用实现只调用 HCT 求解器，不套用标准主题角色和新版 TonalPalette 的额外调整。

降低 secondary、neutral 的彩度，会让辅助文字、边框和表面色保留一点主题色倾向，同时减弱鲜艳程度。再按角色选不同明度，就能用同一种色相形成清楚的层级。这是这组公式的效果；cp/3、cp/6 和固定明度列表是 QQ 的具体选择，其他产品可以采用不同的比例与层级。

## 四个系列的根色规则

下表每个三元组表示 F 的参数。`s` 只用于缤纷时刻：基础色色相 h 在 **150°–330°（含端点）**时为 +1，其他情况为 −1；偏移方向由基础色判断一次。

| 根色 | 缤纷时刻 1001 | 幻彩机核 1002 | 水墨国风 1003 | 神秘美学 1004 |
| --- | --- | --- | --- | --- |
| root_a | `(h, cp, 60)` | `(h, c, low)` | `(h, cp, low)` | `(h, 60, 80)` |
| root_b | `(h, cp, 50)` | `(h, c, high)` | `(h, cp, high)` | `(h, 50, 50)` |
| root_c | `(h, cp, 60)` | `(h, c, high)` | `(h, cp, low)` | `(h, 60, 90)` |
| root_d | `(h+s×45, cp+20, 60)` | `(h, c, high)` | `(h, cp, low)` | `(h−30, 60, 90)` |
| root_e | `(h, cp, 80)` | `(h+210, c+10, 55)` | `(h, 2×cp/3, 80)` | `(h+30, 60, 90)` |
| root_f | `(h+s×60, cp, 80)` | `(h+160, c, 50)` | `(h, cp/3, 90)` | `(h+30, 60, 80)` |
| root_g | `(h+s×120, cp, 80)` | `(h+120, c+10, 40)` | `(h, 2×cp/3, 80)` | `(h−120, 60, 90)` |
| root_h | 未生成 | 未生成 | `(h, cp/3, 90)` | 固定 `#FFFFFF` |
| root_i | `(h, cp, 40)` | `(h, c, high)` | `(h, cp, high)` | `(h, 50, 60)` |
| root_j | `(h+s×45, cp+20, 40)` | `(h, c, high)` | `(h, cp, high)` | `(h−30, 50, 60)` |
| root_k | `(h, cp, 60)`，Alpha=204 | 同 root_e | `(h, 2×cp/3, 40)` | `(h+30, 50, 50)`，Alpha=127 |
| root_l | `(h+s×60, cp, 60)`，Alpha=204 | 同 root_f | `(h, cp/3, 50)` | `(h+30, 50, 50)`，Alpha=165 |
| root_m | `(h+s×120, cp, 60)`，Alpha=204 | 同 root_g | `(h, 2×cp/3, 40)` | `(h−120, 50, 50)`，Alpha=127 |
| root_n | 未生成 | 未生成 | `(h, cp/3, 50)` | 固定 `#323232` |

未标 Alpha 的根色完全不透明。204/255 是 80% 不透明度；127/255 约 49.8039%，165/255 约 64.7059%。保留字节原值可以避免把 `7F` 或 `A5` 误写成四舍五入后的透明度。

## 把根色和色阶绑定到界面角色

原始 `colorful_theme` 保存了 91 个 Token 的浅色／深色绑定。先把字符串引用查成颜色，整数值保持不变。例如：

| Token | 浅色引用 | 深色引用 |
| --- | --- | --- |
| brand_standard | primary_root | root_b |
| text_primary | secondary_10 | secondary_90 |
| bg_aio_1 / 2 / 3 / 4 | root_e / f / g / h | root_k / l / m / n |
| bubble_host_1 / 2 | root_c / d | root_i / j |

`on_brand*` 和 `on_bubble*` 是额外分支：它们表示主色或气泡上的前景色，按背景明度选择原始绑定的第一项（偏深）或第二项（偏浅）。

| 前景角色 | 浅色模式选择第一项的条件 | 深色模式选择第一项的条件 |
| --- | --- | --- |
| on_brand* | 基础色 `t > 60` | 基础色 `t > 60` |
| on_bubble*，缤纷时刻 | 恒为真 | 恒为假 |
| on_bubble*，幻彩机核 | `high > 60` | `high > 60` |
| on_bubble*，水墨国风 | `low > 60` | `high > 60` |
| on_bubble*，神秘美学 | 恒为真 | 恒为真 |

严格大于 60，等于 60 时选第二项。缤纷时刻与幻彩机核缺少 root_h / root_n，原生 Token 解析函数遇到 `bg_aio_4` 的缺失引用会跳过整个 Token。复用实现把它保存在 `unresolvedTokens`；每套这两个系列的预设得到 90 个 Token，其余两个系列得到 91 个。没有补造第四个聊天背景色，也没有推断界面最终使用的回退色。

## 用一个主题串起整条计算链

「香芋啵啵」的种子 `#B389F8` 转成 HCT，得到 `h ≈ 304.365°`、`c ≈ 57.019`、`t ≈ 65.162`。它属于缤纷时刻，所以 `cp = max(c, 50) ≈ 57.019`，low 为 65.162，high 为 70；h 落在 150°–330° 内，色相偏移方向 s 为 +1。

三条色阶使用相同的 h，目标彩度分别是 57.019、19.006、9.503。根色按缤纷时刻的规则另行生成，再与色阶一起交给 Token 映射：

| 界面角色 | 浅色模式的计算路径 | 浅色色值 | 深色模式的计算路径 | 深色色值 |
| --- | --- | --- | --- | --- |
| 主色 | `primary_root = F(h, cp, low)` | `#B389F8` | `root_b = F(h, cp, 50)` | `#8A61CD` |
| 我的气泡 1 | `root_c = F(h, cp, 60)` | `#A57BE9` | `root_i = F(h, cp, 40)` | `#7047B2` |
| 我的气泡 2 | `root_d = F(h+45, cp+20, 60)` | `#EF55B3` | `root_j = F(h+45, cp+20, 40)` | `#AF197E` |
| 聊天背景 1 | `root_e = F(h, cp, 80)` | `#D5BAFF` | `root_k = F(h, cp, 60)`，Alpha=204 | `#A57BE9CC` |
| 主要文字 | `secondary_10 = F(h, cp/3, 10)` | `#20172E` | `secondary_90 = F(h, cp/3, 90)` | `#EBDDFC` |

同一个主题的主色和气泡色也会不同，因为它们采用不同的明度目标；深色模式又重新选择一组角色颜色。知道角色的用途后，才知道该从色阶中取哪一层。`primary_50` 这样的名字描述颜色来源，`brand_standard`、`text_primary` 这样的名字描述界面用途，二者通过映射连接起来。

## 把这套思路用于其他 App

一套自动主题至少要有基础色、配色规则和角色映射。基础色提供色相、彩度、明度的起点；规则决定保留多少彩度、哪些角色旋转色相、各角色使用什么明度；映射把计算出的颜色安排到背景、文字、按钮、边框等组件用途上。

以一个阅读 App 为例，先定义页面背景、正文文字、次要文字、链接、按钮背景和按钮文字这几种角色。长时间占据屏幕的页面背景可以从低彩度色阶取值，链接与主按钮从主色阶取值；正文与背景拉开明度，再分别为浅色、深色模式安排映射。这样换一个基础色时，组件可以继续使用相同的角色名。QQ 的 `bg_aio`、`bubble_host` 描述聊天场景，阅读 App 则需要自己的角色和明度规则。

从 QQ 的规则输出可以分析出几种配色手法：水墨国风限制主色阶彩度，并进一步降低大面积背景的彩度；缤纷时刻设定彩度下限、旋转部分色相，让气泡与背景出现多种颜色；神秘美学的多组根色使用固定彩度，使这些根色的风格较少受种子彩度影响。这里分析的是规则产生的效果，原始设计稿和各常量的选择过程没有包含在安装配置中。

### 检查前景和背景的实际对比

生成了颜色，还要检查文字与它背后颜色的组合。QQ 的前景分支只是按照固定条件选深色或浅色，没有在这段解析逻辑中逐个计算对比度，也没有对不达标的组合做搜索修正。

对两个最终、不透明的 sRGB 颜色，沿用线性化得到的相对亮度 Y，按下式计算对比度：

```text
contrast = (max(Yforeground, Ybackground) + 0.05)
         / (min(Yforeground, Ybackground) + 0.05)
```

这里 Y 是 0–1 的相对亮度，不是 0–100 的 HCT T。[WCAG 2.2 的文字对比要求](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html)为普通文字规定至少 4.5:1，大号文字至少 3:1，并另列标识等例外。用于自己的产品时，可以在主题生成后逐对检查正文／页面背景、按钮文字／按钮背景、链接／页面背景；不满足目标时，调整对应角色的明度并重新求解，而不是只根据颜色名称判断是否清楚。

带 Alpha 的颜色还依赖下面的背景；渐变和纹理则会让文字背后的颜色随位置变化。应先按目标渲染环境取得合成后的背景，再测量文字对比。换用 HCT 能让明度控制更方便，但最终可读性仍由前景、背景和合成方式共同决定。

### 检查基础色变化的边界

纯黑、纯白、灰色、鲜艳的蓝黄绿都值得单独检查。中性色的色相信息很弱，设置彩度下限后可能出现输入中原本不明显的色彩倾向；鲜艳色容易遇到色域压缩；极亮、极暗输入会触发 low / high 的限制。这些边界会改变主色与种子的关系。

此外，阈值分支会造成跳变。例如缤纷时刻的偏移方向在 h 越过 150° 或 330° 时切换，`on_brand*` 的前景色在 t 越过 60 时切换。如果产品支持连续拖动颜色选择器，需要检查这些边界两侧的结果。可以按自己的设计目标采用连续插值、调整阈值或重新选择前景，但这些变化属于新的配色规则。

复用到 Figma 时，可以把主色阶、辅助色阶、中性色阶存成基础颜色变量，再建立引用它们的语义变量，并为语义变量配置浅色、深色模式；[变量引用与模式](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes)可以承载这两层关系。交给 Agent 时，同时提供基础色、模板规则、角色映射和实际 HEX，才能让它区分「颜色怎样算出」与「颜色用在哪里」。


## 验证依据

安装模块 SHA-256 为 `0d8b94448f4dafe7337b88d6a568b004224c37fcbe2bd1142c1af7624fbd534c`。原生模板派生函数 RVA 是 `0x581976`，Token 解析函数是 `0x57E7BA`，明度列表生成函数是 `0x577D2F`，HCT 转换入口分别是 `0x63E1F4` 和 `0x63E064`。

使用 Unicorn 对该模块的 x86-64 指令做隔离仿真：输入查找、C++ 容器和字符串读写用适配函数替代，pow / fmod 使用标准数学函数，栈保护检查跳过；颜色运算、模板分支和 Token 选择分支执行原始指令，没有启动 QQ 或改动安装文件。

验证输入包括 20 个预设，以及黑、白、灰、RGB 纯色、额外基础色和 12 个固定随机色在四种模板中的结果，共 100 组。JavaScript 实现与原生仿真的 **7000 个派生色、18100 个 Token 色值**逐项整数相等。对照样本、模块哈希与函数入口保存在配套数据中。这个核对证明的是这些颜色函数和分支的输出，不包含纹理、渐变布局或 QQ 最终界面合成。
