装修完成后的博客首页

最近把博客的主题重新装修了一遍。

倒也不是原来的主题不能用,只是看久了总觉得页面有点沉,背景图明明很亮,前面却压着一层暗色遮罩,像显示器亮度被谁偷偷往下拉了两格。文章、分类、友情链接、搜索和音乐这些入口都还挺顺手,所以这次没打算推倒重来,只想换掉外壳,不碰里面已经放好的东西。

目标一开始很简单:背景要透亮,内容要像浮在图上,玻璃要有厚度,但不能糊成一片白。

然后理所当然地越改越多了。

先把背景还给背景

原主题把头图和正文背景分成了两层。上面是亮的,下面又叠了一层遮罩,滚动时能很明显地看到接缝。第一步就是把这些历史层级拆掉,只保留一张固定背景铺满页面。

html {
  background-image: var(--liquid-hero-image, none);
  background-repeat: no-repeat;
  background-position: 30% 30%;
  background-size: cover;
  background-attachment: fixed;
}

body.index-page {
  background: transparent;
}

背景图没有换,也没有为了“高级感”故意把亮度压暗。页面上的内容块改成半透明浮层,让同一张图从顶部一直延伸到底部。这样滚动时背景是一个完整空间,文章卡片只是悬在上面,不会再有上下两张底图拼起来的感觉。

我把它叫做高斯玻璃着色器

这次最花时间的其实是玻璃。

严格来说,它不是我手写的一段 WebGL 或 GLSL Fragment Shader,而是交给浏览器合成器完成的一组 CSS 渲染管线。backdrop-filter 会先采样浮块后面的画面,对背景做模糊和饱和度处理,再把半透明表面、边缘高光和阴影叠上去。浏览器内部通常会用适合 GPU 的多次采样或可分离模糊来近似高斯效果,所以我还是习惯把这一套叫做“高斯玻璃着色器”。

核心参数大概是这样:

.glass-panel {
  background: rgba(255, 255, 255, 0.68);
  border: 1px solid rgba(255, 255, 255, 0.88);
  -webkit-backdrop-filter: saturate(150%) blur(22px);
  backdrop-filter: saturate(150%) blur(22px);
  box-shadow:
    0 22px 54px rgba(11, 45, 66, 0.20),
    0 4px 14px rgba(14, 55, 78, 0.12),
    inset 0 1px 0 rgba(255, 255, 255, 0.98),
    inset 1px 0 0 rgba(255, 255, 255, 0.58),
    inset -1px 0 0 rgba(255, 255, 255, 0.30);
}

这里有三层东西一起工作:blur(22px) 负责把背后的背景打散,saturate(150%) 把被模糊吃掉的颜色稍微拉回来,半透明白色负责形成玻璃本体。最后几层 inset 阴影专门画边缘高光,外阴影把浮块从背景上抬起来。

如果只有透明和模糊,效果更像一块磨砂塑料;多了边缘那一圈接近白色的高光,玻璃才会像真的接住了环境光。它没有复杂到需要单独开一个 WebGL Canvas,但每一层都在悄悄影响最后的质感。

版式没有重做,只是重新排队

内容没有改,入口也都保留了。首页仍然是文章列表加右侧栏,只是从原来的浮动和固定宽度组合,换成了更容易控制的 Grid:

.liquid-home-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 310px;
  gap: 28px;
  align-items: start;
}

.sidebar > .position-sticky {
  position: sticky;
  top: 90px;
}

左边允许收缩,右边保持稳定宽度。右栏滚动到顶部导航下面以后会固定住,但它自己没有多余的滚动框和底板。文章页也用了相同的规则,所以从首页点进正文以后,分类、近期文章和音乐不会突然换一种行为。

中间有一阵子 sticky 怎么都不生效。最后发现不是它坏了,而是某个祖先节点的高度和 overflow 把它关在了一个看不见的笼子里。CSS 有时很像在排查内核问题:表面上报错的是这一行,真正的原因在三层父节点之外。

100%、150%,以及那块神秘的右侧白边

这次还有一个比较怪的坑。同一台电脑,系统 100% 和 150% DPI 下,页面看起来像两个不同的网站;浏览器缩放到 67% 时又刚好顺眼。简单地给页面写死宽度,会让内容整体向右漂,像所有元素都错误地绑在了右下角锚点上。

最后没有继续补宽度,而是只保留统一缩放,并用 devicePixelRatio 算出当前应该使用的倍率:

function updateDpiLayoutScale() {
  var root = document.documentElement;
  var dpr = Math.max(window.devicePixelRatio || 1, 1);
  var physicalWidth = window.innerWidth * dpr;
  var finePointer = window.matchMedia('(pointer: fine)').matches;
  var normalize = finePointer && physicalWidth >= 1800 && dpr <= 1.6;
  var scale = normalize
    ? Math.max(0.76, Math.min(1.71, 1.14 / dpr))
    : 1;

  root.style.setProperty('--liquid-dpi-scale', scale.toFixed(4));
}

这里的 1.14 是最后一点点放大后的目标尺寸。100%、125%、150% DPI 和浏览器 67% 缩放,会尽量回到接近同一个视觉大小。之前为了补偿缩放写过一版反向宽度,结果正是它制造了右侧白块和整体偏移,删掉以后反而正常了。

顶部导航也遇到过类似的问题。CSS 的 left: 50% 在缩放后的坐标系里并不总是视觉中心,于是又补了一次实际位置测量:

var rect = navigation.getBoundingClientRect();
var offset = (window.innerWidth / 2 - (rect.left + rect.width / 2)) / scale;
navigation.style.setProperty('--liquid-nav-offset', offset.toFixed(2) + 'px');

窗口尺寸变化、导航吸顶状态切换时都会重新计算。比起再塞一组 magic number,这种做法至少知道自己为什么在移动。

Codex 在旁边干了什么

这次基本是我看页面、说哪里不顺眼,Codex 在服务器和主题代码之间来回跑。

先完整备份 WordPress 文件和数据库,再复制出测试站;之后每一轮根据截图改 CSS、JavaScript 和模板覆盖规则,上传到测试端口检查。确认文章、媒体、分类、后台和原来的入口都还在以后,才把测试主题替换到主站。上线后的修改也会先在服务器留下即时备份,上传完再比对本地和远端 SHA256,避免出现“看起来传上去了,其实缓存里还是上一版”的情况。

它比较像一个一直在线的远程结对程序员,不是一键生成主题。很多要求其实非常主观,比如“再抬高一点”“玻璃边缘亮一点”“这个块不要了”“整体再放大一点”,只能一轮一轮看效果再收敛。代码是 Codex 辅助写的,审美方向和每次取舍还是由我来决定。

期间也有过命令太重把 Codex 弄闪退、布局突然集体向右漂、文章图片被拉成奇怪比例、底部冒出一条白色地板之类的现场。好在都有备份,出问题就继续拆。

暂时收工

现在的主题没有动原来的文章内容,也没有换掉背景图。它只是把页面从一层偏暗的传统博客外壳里拿出来,重新放进一组明亮、带模糊和边缘光的浮空玻璃里。

我挺喜欢现在这种感觉:背景依然是背景,文章仍然是文章,中间那层 UI 不再抢画面,但仔细看又能发现不少渲染和布局的小机关。

主题大概不会真的“完成”。哪天又觉得某块偏了两像素,估计还是会继续改。