看起来像 bug 的设计,就是 bug
只要访客分不清「这是设计」还是「这坏了」,无论代码多正确,界面都已经失败了——以及怎么用可测量的方式提前发现它。
向下阅读
代码没有报错,类型检查通过,评审也没人提出异议。然后用户发来一张截图,说:这个字显示有问题。
打开一看,确实有问题——一个装饰图形压在字母上,两个字母的尾巴被切平。但更值得记下的是:在用户指出之前,我盯着这段代码看了很久,一直觉得它是对的。
意图必须被看见,而不是被解释
界面不是向编译器负责的,它向站在屏幕前的人负责。一个人看见一朵花盖住了字母,他的结论就是"这里坏了"。此时解释"这是刻意做的装饰"没有任何意义——解释的成本已经转嫁给了使用者。
这跟代码里的"能跑就行"是同一类问题,只是验收者从机器变成了人:
- 编译器接受的东西,人未必接受
- 能通过评审的代码,未必能通过眼睛
- 解释得清楚的意图,不如不需要解释的呈现
所以判断标准要改:不是"这段代码是否符合规范",而是"这个画面是否把意图传达出去了"。
正确性不等于可读性
真正麻烦的是,这类问题往往不是"写错了",而是"写对了一半"。
替换元素不吃 inset。 用四个方向的 inset 把一个元素撑成居中的圆,对 div 成立,对 img 不成立。img 是替换元素,它取固有尺寸、锚在左上角,多余的约束被忽略。于是封面贴在左上角,还超出了容器。
留白对多数字母够用。 逐字入场动画需要裁切层,裁切层要留一点底部空白。常见写法给 0.06em,对没有下伸部的字母完全够用;但 y 和 g 的下伸部约 0.24em,尾巴直接被切平。
百分比在某个宽度下是精确的。 装饰用 right: 14% 定位,在一个窗口宽度下贴合得很准;但文字用 clamp(4rem, 14vw, 12.5rem) 缩放,装饰按容器缩放,两者比例不同。换个宽度,装饰就漂移到了别的字母上。
动画覆盖在多数时机是无害的。 悬停动效带 overwrite: "auto",在动画播完后再触发完全正常;但入场还没结束时触发,它会杀掉入场进度,把字母冻在半空。
这四条的共性是:它们只在特定输入、特定尺寸或特定时机下失效。静态审查几乎抓不到。
把"我觉得"换成可测量的数字
既然肉眼不可靠、代码审查也不可靠,那就换成能算出来的东西。
判断装饰有没有压住文字,比两个矩形:
const overlap = !(decor.left >= letter.right || decor.right <= letter.left);
判断字母有没有被裁切,用 canvas 的字体度量算出墨迹底部,再和裁切盒高度比较:
const ctx = document.createElement("canvas").getContext("2d");
ctx.font = `${weight} ${size} ${family}`;
const inkBottom = baseline + ctx.measureText("y").actualBoundingBoxDescent;
判断动效有没有在跑,读两次 transform 矩阵,看角度或模长是否变化。
这些都不难写,难的是养成习惯:在觉得"差不多"的时候,先去量一下。
把疑惑当成信号
"这里是不是坏了?"本身就是一条有效反馈,而且是最贵的那种——它意味着有人已经替你完成了验收,并且发现了问题。
收到这类反馈时,第一反应不该是解释设计初衷,而该承认:界面没有把意图传达出去。修的方式也不是加一句说明文字,而是让设计自己说清楚——装饰退开、字母完整、动效有始有终。
一个需要说明书才能被理解的界面,和一个确实坏掉的界面,在使用者那里的区别并不大。