卷 I · 看清· CH 03· 真高斯模糊 深度 3/28

眯起眼睛:层级是能量出来

「眯起眼睛看看」是设计师最常给的建议,也是听起来最玄的一条。这一章告诉你它一点都不玄——眯眼就是低通滤波,而滤完之后剩下的东西,可以精确地积分出来。

视觉权重 = 面积 × 亮度差可分离高斯卷积意图 vs 实际

眯眼到底在干什么

你眯起眼睛的时候,物理上发生了两件事:睫毛遮挡加上焦点失准,让高频信息(笔画、边缘、细节)糊掉,只剩下低频信息(块面、明暗、位置)。

用信号处理的话说:这是一次低通滤波。用图像处理的话说:这是一次高斯模糊

而「视觉层级」正是低频信息。你第一眼看到什么、第二眼看到什么,不是由「这个标题写得好」决定的,是由哪一块又大又跟背景差得多决定的。

视觉权重,写成公式

把一屏界面画成灰度图:每个块按它和背景的相对亮度差落墨(第 14 章会讲这个亮度怎么算),然后做一次高斯模糊,最后在每个块所在的区域上积分

积出来的值,就是这个块的视觉权重。它大致等于:

视觉权重 ≈ 面积 × 与背景的亮度差

注意里面没有「好看」这一项,也没有「重要」这一项。视觉系统不知道你觉得哪个重要,它只会把又大又深的东西推到前面。

算给你看

下面这台引擎做的就是刚才说的三步:栅格化 → 可分离高斯卷积 → 分区积分。拉动 σ 滑杆,就是把眼睛越眯越紧。

看「常见写法」那一版的排序。

你以为的顺序是:标题 → 主按钮 → 插图 → 正文 → 副标题 → 次要链接

实际算出来的是:主按钮 → 插图 → 标题 → …

标题掉到了第三。原因很朴素:那个标题只有 200 × 26 像素,而按钮是 312 × 48 且是饱和的蓝色,插图是 312 × 150 的大灰块。面积上你就输了两个数量级不到一个数量级,但足够了。

为什么这件事重要

因为你写代码的时候,是按语义顺序写的:先写标题,再写副标题,再写内容,最后写按钮。你在 IDE 里读到的顺序,和用户眼睛接收的顺序,是两回事

而你每天盯着这个界面,早就知道标题在哪儿了——你已经没法用第一次的眼睛看它。眯眼测试的真正价值,是把你从「知道」的状态强行拉回「只能看见」的状态。

改过之后:只动了量,没动审美

切到「改过之后」那一版。顺序对上了。而改的只有三件事:

标题26px → 40px,#333333 → #111111
插图#BDBDBD → #E4E4E4(调浅),高度 150 → 120
次要链接#9E9E9E → #AFAFAF(调淡)

没有换字体,没有加阴影,没有「重新设计」。只是把量调对了。

这里有一个很值钱的推论:

想让 A 更突出,最省的办法通常是把 B 压下去

视觉权重是相对的。当你觉得标题不够醒目,第一反应是把标题做大——但界面上的空间是有限的,做大标题会挤掉别的东西,而且很容易做过头。

更省的做法是把周围的东西调轻:插图调浅一档,次要文字调淡一档。上面那个例子里,把插图从 #BDBDBD 调到 #E4E4E4,标题一个像素没动就升上去了。

这也是新手界面和成熟界面最大的观感差别:新手界面上所有东西都在用力,成熟界面上只有一两个东西在用力,其余全在让路。

这台引擎是真的

为了让你相信上面那些数字不是编的,说一下它是怎么算的。

可分离卷积。二维高斯核可以拆成横竖两个一维核先后作用,复杂度从 O(r²) 降到 O(2r)。这就是 Photoshop 的高斯模糊快的原因,也是这里 σ 拉到 24 还能实时响应的原因。

// 一维核:3σ 之外的权重小到可以忽略
function gaussKernel(sigma) {
  var r = Math.ceil(sigma * 3), k = [], sum = 0;
  for (var i = -r; i <= r; i++) {
    var v = Math.exp(-(i * i) / (2 * sigma * sigma));
    k.push(v); sum += v;
  }
  return { r: r, k: k.map(function (v) { return v / sum; }) };
}

// 先横后纵,边界夹取
tmp[y][x] = Σ src[y][clamp(x + i)] * k[i]
out[y][x] = Σ tmp[clamp(y + i)][x] * k[i]

亮度差用的是 WCAG 的相对亮度。不是简单的 (R+G+B)/3——那个是错的,因为绿色对人眼的贡献远大于蓝色。正确的做法是先把 sRGB 做逆伽马变换回线性空间,再按 0.2126 / 0.7152 / 0.0722 加权。第 14 章会详细讲这三个系数从哪来。

顺序一致度用逆序对。把「意图顺序」和「实际顺序」当成两个排列,数其中有多少对元素的相对次序反了(Kendall 距离),除以总对数。100% 表示完全一致。

眯眼测试在 Compose 里没有对应的 API,但有两个很实用的近似做法:

// ① 预览里直接糊一层 —— 最快的自查
@Preview
@Composable
fun SquintPreview() {
    OrderScreen(
        modifier = Modifier.blur(12.dp)   // API 31+
    )
}

// ② 抽掉颜色,只留明暗 —— 顺带查了「只靠颜色区分」的问题
val grayscale = ColorMatrix().apply { setToSaturation(0f) }
Box(Modifier.graphicsLayer {
    renderEffect = null
}.drawWithContent {
    drawContent()
}) { OrderScreen() }

更土但更有效的办法:截图,缩到指甲盖大小,再放大看。缩放本身就是一次低通滤波,而且不需要任何 API。

什么时候用它

这一测试的正确用法是做完之后,不是做的过程中。

做的过程中你在处理细节,糊了看什么都看不清。做完之后糊一下,回答一个问题就行:

一句话验收

眯眼之后,最重的那一块,是不是你想让人先看的那一块?

是 —— 过。
不是 —— 要么把想让人先看的做重,要么把抢戏的那个压轻。优先选后者。

本章关掉的自由度本章 2 条 · 累计 6 / 78
  • 「层级对不对」——不再靠「感觉还行」。糊一下,看排序,一分钟出结论。
  • 「怎么让它更突出」——默认答案是把周围压轻,而不是把它做大。只有压无可压时才动主角。