有很多主题,但这一款属于你

在Earendil,我们希望打造尊重使用者选择的产品。所以当我们刷新Pi的主题时,我们希望有一个能适应其运行终端的主题。大多数在终端里待的人都会为它选择自己的主题。
为什么不让Pi反映他们的选择呢?
结果就是新的系统主题,现在是 Pi 的默认主题。它会向终端请求颜色,并从中构建 Pi 的主题。在这篇文章中,我们分享了它是如何工作的。
在你阅读后续内容之前,我们想先剧透一下:这一切都关乎对比度。在终端界面中选择颜色是个品味问题,用户已经回答过了。接下来,所有其他决定都归结为一个问题:颜色在背景中有多突出?
如果我们做错了,就会给用户带来糟糕的体验。
但关键是对比是感知的问题,不是我们能轻易测量的东西。两个人看同一对颜色时,可能会对其是否可读性产生分歧。我们做了一个小的你可以参与的互动调查向你展示你如何看待这种对比,以及它与其他人相比如何。
你可以在文章末尾找到关于那份调查的详细信息。
你能信任ANSI调色板吗?
每个终端主题都定义了16种ANSI颜色,最简单的匹配方式就是直接使用它们。但ANSI调色板其实没有什么规则。原始标准,ECMA-48于1976年被采纳。
其1979年几乎相同的美国对应标准ANSI X3.64正是其名称的来源。该标准列出了八种颜色(黑色、红色、绿色、黄色、蓝色、品红色、青色和白色),但并未说明它们应呈现什么样子,也未说明它们应如何与背景相关联。
八种明亮变体不属于标准。有不少终端用更亮的颜色渲染粗体文字,实际上增加了八种颜色。直接选择鲜艳颜色的代码后来由IBM的aixterm添加,并被其他终端采用,如xterm.
由于鲜艳的颜色最初是粗体文字,我们会认为“亮”是为了更突出。而且早期终端大多显示浅色文字,暗屏上更亮也意味着对比度更高。我们看过Ghostty自带的460多个主题,如今这只是勉强成立。
在深色主题中,亮色变体在大约60%的情况下对比度更高。在浅色主题中,对比度只有大约四分之一,因为亮通常仍意味着更亮,而浅色背景下则意味着对比度较低。
单个主题也不一致:在Gruvbox Dark中,亮蓝色比蓝色强,Catppuccin Mocha中对比度较低,而Tokyo Night中它们颜色相同。与背景的对比度变化很大。
我们用WCAG 2对比度,比较两种颜色的亮度,比例从1:1(无对比度)到21:1(黑底白底)不等。普通文本应至少达到4.5:1,大文字和UI元素应达到3:1。
许多软件用于辅助文本的亮黑色,在大多数暗色主题中甚至达不到3:1。
这并不是主题的缺陷。调色板是为简单应用的输出(比如红色错误或绿色成功信息)着色的,许多主题设计上是为了美观而非满足对比度最低标准。
但这也让构建一个更易上手、更复杂的 TUI 变得困难。Pi 大约有 60 种颜色角色,从正文和暗色文本到工具调用后的面板和红色错误面板上的红色文字。
我们希望使用你的颜色,同时保证所有颜色都可读。
对比度就是你所需要的
对比度是用户界面中色彩最重要的方面之一。如果对比度太低,用户会很难使用你的产品。对比度主要由亮度决定。饱和度会有一定影响,但最重要的因素是颜色与其背后的颜色相比,颜色的明暗程度。
RGB,也就是我们通常写颜色的方式,没有明度轴。它是为机器在显示器上显示颜色而设计的,而不是让人类理解颜色。作为一个三维形状,它是一个整洁的立方体,每个通道只有一个轴。
但在这个立方体中彼此接近的颜色,不一定是人类会描述为相似的颜色。例如,#0000ff 和 #00ff00 都是全光道,但在白色上,蓝色的WCAG对比度是8.6:1,绿色只有1.4:1。
感知色彩空间,如OKLCH而是围绕人类感知构建的。在OKLCH中彼此接近的颜色也是人类描述为相似的颜色,其轴线是人类用来描述颜色的:明度、色度(颜色的丰富度)和色调。
因为它遵循人类感知而非显示器硬件,形状比立方体要奇怪得多。
有了明度轴,系统主题的理念很简单:Pi根据对比度要求决定每种颜色的明度,并从终端调色板中取色相和色度。
轻盈
为了确定每种颜色需要的亮度,我们写下了UI中两种颜色交汇处的每个位置。每个面板需要与终端背景有足够的对比度,以显示为独立区域,但又不能过多以免分散注意力。
每个前景颜色在每个可能出现的背景上都需要足够的对比度。例如,错误信息必须在背景、选定行和三个工具面板上都能读到。在代码中,这是一份规则列表:
const COLORS = ["accent", "success", "error", "warning"];
const SURFACES = ["background", "selectedBg", ...TOOL_PANELS];
{ token: "text", on: ["background"], level: "text" },
...each(COLORS, SURFACES, "readable"),
{ token: "dim", on: [...SURFACES, "customMessageBg"], level: "subtle" },
对比算法通常会接收两种颜色并返回它们之间的对比度。这里我们需要相反的情况:我们知道背景和想要多少对比度,需要颜色。我以前做过反对比度算法,你可以找到两种实现WCAG以及感知对比在GitHub上。
在反向算法中,主题计算变成了一个循环:从面板开始,Pi计算每种颜色在每个规则中所需的亮度,并取最严格的那条。
算法
我们的第一个原型正是做到了这一点,配合一个调校对比度最低值的评测应用。该应用可以渲染包含Ghostty自带的任何主题的Pi,因此我们可以检查一些非常不同的主题样本,确保系统在默认之外依然耐用。
该原型使用了一种知名的感知对比算法和参考实现。我们用它对大量幽灵主题进行分析,并确保它在所有主题中看起来都很好。因此我们不想直接发布该算法。
我们尝试使用更简单的测量方法,但无法近似结果。最终,我们让编码代理完成拟合。对于参考中的每个对比度级别,代理在从白色到黑色的灰色背景上运行原始算法,记录亮度,并拟合一个多项式。
最终确定了一个五次多项式,发现该多项式足够接近参考亮度。Pi 现在只带有这些系数。
下面的图表展示了这些多项式,上面规则中每个对比度水平的曲线。底部是颜色绘制表面的明度,侧面则是颜色所需的亮度。对角线完全没有对比:颜色和其表面一样浅。
垂直线是表面:背景和面板,Pi先在背景上解。当规则的曲线穿过其一个曲面时,你可以读取该规则所需的明度,最严格的那个获胜。拖动背景可以看到每个颜色如何跟随它。
我们首先将查询的终端颜色从 RGB 转换为 OKLCH,然后OKHSL.由此我们得到原始的明度L、色度C和色相H,以及相对于sRGB在该亮度下显示的饱和度S。
一旦多项式与背景的亮度进行评估。所得的亮度不再直接替代,而是转换为OKHSL亮度,并利用原始色调和调整后的饱和度计算出新的颜色。
应用钟形饱和曲线。在中间亮度处最强,接近黑白时较弱。最后将其转换为OKLCH,Pi限制原始色度,使H保持不变,L来自对比度规则,C则成为调整后的OKHSL产生的颜色。
这样,例如浅粉色在向更深的色调移动时,可能会使其过于鲜艳。调整确保它永远不会比原始颜色更丰富。
色调与色度
Pi保持调色板颜色的色调不变。色度则更复杂。OKLCH的奇特形状显示了屏幕能显示多少色度,这取决于色相和明度。在0.9的亮度下,屏幕能显示的最丰富黄色的色度约为0.2,而最鲜艳的蓝色只有大约0.05。
所以当Pi将调色板颜色调整到所需的亮度时,色度可能不存在于该亮度,映射回可显示颜色时,Pi计算的亮度会改变。
这就是为什么 Pi 用 OKHSL 构建颜色。OKHSL 基于与 OKLCH 相同的基础,使用相同的色调,但它把奇怪的形状拉伸成了一个圆柱体。它的饱和度从 0% 到 100%,100% 总是意味着“在这个亮度下,这个色调可以达到最丰富的色彩”。
它试图兼顾两全其美:尽可能贴近人类感知,同时带回了简单的几何结构,使基于 RGB 的色彩空间易于操作。每一种色调、饱和度和明度的组合都是你的屏幕可以显示的颜色,这使它非常适合生成主题和调色板。
Pi 还允许饱和度向黑白逐渐下降,这样非常暗和非常浅的颜色,比如比背景稍微浅一点的面板,会呈现出一丝色彩,而不是明亮的块。
但保持饱和度不变,也无法让颜色同样丰富。由于饱和度是相对于屏幕显示的,相同百分比在不同亮度下可能意味着色度量差异很大。发布后不久,一个漏洞报告显示Pi的颜色比用Catppuccin冰沙的终端更鲜艳。
Catppuccin的粉色 #f4b8e4 有84%的OKHSL饱和度,但这占了屏幕在如此高亮度下能显示的少量色度的84%。Pi的口音需要更深才能清晰辨认,而由于形状更宽,84%的饱和度变得 #eb76d1,约是原粉色的两倍。
解决方法是同时封顶色度:调色板颜色可以变为不同的明度,但永远不会比调色板中更丰富。加了顶盖后,重音变得 #cc92bd,看起来又像Catppuccin。
所以最终,调色板颜色的色度会受到三次限制:通过OKHSL屏幕显示的范围;向黑白的衰减;以及调色板中的色度。
下面的两个视图将色彩空间与Pi在其中的表现分开。OKHSL的圆柱体保持固定。Pi的范围取决于源颜色和UI角色的族群:源的饱和度以自身的亮度为单位,衰减是相对于该点的。
向中间移动不会让饱和度高于源的饱和度。不同族使用不同的衰减;黄色警告和紫色点缀不会使用相同的曲线。
选择一个颜色来检查Pi是否能从源头、这里或上方的终端中生成。点是源点;方块是Pi在本主题中使用的颜色。拖动任一视图即可旋转两个视图。
结果
Pi 启动时会向终端请求前景、背景和 ANSI 颜色,并在终端切换明暗时重建主题。如果终端只报告背景,Pi 会使用自己的色调。如果没有报告,Pi 会回到 ANSI 颜色,让终端绘制。
生成的颜色保留了终端的色调,但亮度调整为相似的对比度。Gruvbox 的深红色变得更浅,Catppuccin Latte 的红色稍暗,Nord 的颜色几乎不变。
如果你还没选定主题,说明你已经在使用系统主题了。否则,你可以在主题下的 /settings 里切换到它。如果你的终端主题在树莓派里看起来怪怪的,请开启一期主题名称。
调查
本文中所有与对比度相关的数字都是根据现有对比感知算法估算的普通人对对比度的感知。但目前没有真正的开放数据集能告诉我们真相。
我们正在开发自己的开源对比度算法,为此我们很想看看真实人们在真实屏幕上如何感知对比度。
调查大约需要3分钟完成,结束时我们会展示你的回答与其他参与者相比的对比。
我们非常希望你能接受。你可以在contrastsurvey.earendil.com如果你参与,我们将用你的答案训练对比算法。