elvhack
← 全部文章

每篇文章一张生成封面

不画图、不传图:用文章 id 的哈希驱动四种生成艺术,构建时输出 SVG,再靠 <use> 继承颜色,让同一张封面同时适配明暗主题。

发布
阅读
约 1 分钟
信号
SIG-3DE9 · flow

这个站的每篇文章都有一张封面,但没有一张是画出来的。封面由文章的 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

  1. 严格地说,是 CSS 的继承属性会穿过 <use> 生成的影子树;选择器则穿不过去,所以只能靠继承,不能靠类名。 ↩