每篇文章一张生成封面
不画图、不传图:用文章 id 的哈希驱动四种生成艺术,构建时输出 SVG,再靠 <use> 继承颜色,让同一张封面同时适配明暗主题。
这个站的每篇文章都有一张封面,但没有一张是画出来的。封面由文章的 id 决定:同一篇文章永远得到同一张图,换一个 id 就是另一张。
从 id 到随机数
第一步是把字符串变成数字。用的是 FNV-1a 哈希,几行就能写完,分布也足够均匀:
export function hash(input: string): number {
let h = 0x811c9dc5;
for (let i = 0; i < input.length; i++) {
h ^= input.charCodeAt(i);
h = Math.imul(h, 0x01000193);
}
return h >>> 0;
}
哈希值再喂给 mulberry32 伪随机数生成器。之后所有“随机”的决定——噪声的种子、山峰的位置、哪条线是强调色——都来自这一串确定的数字,所以每次构建结果都一样。
四种风格
哈希值对 4 取模,决定用哪种风格(也可以在 frontmatter 里用 cover 指定):
| 风格 | 做法 |
|---|---|
| 流线 flow | 沿 Perlin 噪声场追踪一百多条线 |
| 莫尔纹 moire | 两组错位的同心圆互相干涉 |
| 山脊线 ridge | 致敬《Unknown Pleasures》,后排被前排遮挡 |
| 半调 halftone | 噪声值决定方块大小 |
山脊线有个细节:峰值叠加后可能冲出画布顶边,被截成平顶。解决办法是用 tanh 做软限幅,让高度平滑地收敛到可用空间以内:
const room = y0 - 12;
const lift = room * Math.tanh(raw / room);
一张 SVG,两种主题
封面在构建时输出到 /covers/<id>.svg,页面里用 <use> 引用它。关键在于图形里不写死颜色:墨线用 currentColor,强调色用 var(--cover-accent)。
<use> 引用的内容会继承引用处的样式1,所以同一个文件放在暗色页面里是荧光绿,放在亮色页面里就变成国际橙。浏览器只下载一次,悬停预览、精选卡片和标题背景都复用它。
让线条自己画出来
每条路径都带上 pathLength="1",这样不管实际多长,长度都按 1 计算。stroke-dasharray 和 stroke-dashoffset 同样是可继承的属性,于是只要在 <use> 上把偏移量从 1 动画到 0,所有线条就会同时从起点描到终点——滚动到封面时你看到的就是这个效果。
Footnotes
-
严格地说,是 CSS 的继承属性会穿过
<use>生成的影子树;选择器则穿不过去,所以只能靠继承,不能靠类名。 ↩