用 Canvas 做一层会呼吸的纸纤维背景

2026 年 10 月 28 日·编程开发笔记·9 分钟阅读
木质桌面上打开代码编辑器的场景

做这个站点最开始的想法很简单:背景不要一块死板的米白色。纸是有纤维的,翻动时会有一点明暗变化。但如果直接挂一张大图,首屏就要背几百 KB,手机端还得为清晰度再翻一倍。

先确定上限,再动手画

我的预算很紧:只允许一块 512×512 的离屏画布反复平铺,每一帧最多增加一次合成。所以规则定死三条——不逐像素刷新、不叠加模糊滤镜、滚动时不重绘整屏。

  • 纤维用短线段随机撒,线宽 0.5,透明度压在 0.03 以内
  • 只画一次到离屏画布,之后当贴图使用
  • 滚动时只改容器的透明度与位移,不重画纤维
纸感不是靠一张图给出来的,而是靠留白、边框和一点点不均匀撑起来的。纤维只是最后那 3%。

生成一层可平铺的纤维

关键在可平铺:线段跨过边界时要在对侧补一段,否则平铺后会出现明显的接缝。下面这段只做一件事,把随机线段画进画布。

ts
const size = 512;
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d")!;

for (let i = 0; i < 2400; i += 1) {
  const x = Math.random() * size;
  const y = Math.random() * size;
  const angle = Math.random() * Math.PI;
  const length = 6 + Math.random() * 16;
  ctx.strokeStyle = `rgba(46, 42, 38, ${0.012 + Math.random() * 0.018})`;
  ctx.lineWidth = 0.5;
  ctx.beginPath();
  ctx.moveTo(x, y);
  ctx.lineTo(x + Math.cos(angle) * length, y + Math.sin(angle) * length);
  ctx.stroke();
}
放大到 200% 时的纤维密度,实际使用时不透明度会再压一半

呼吸感交给 CSS,不要交给逐帧重绘

最初我在 requestAnimationFrame 里逐帧调透明度,手机上风扇都会转起来。后来改成一段 12 秒的 CSS 动画,只改 opacity,浏览器可以把它丢给合成线程,成本几乎为零。页面隐藏时动画自然暂停,也不用额外处理。

剩下的判断是取舍:这层纤维在低亮度屏幕上几乎看不见,在阅读正文时会有一点点干扰。最后我让它只出现在首屏和区块之间的过渡带,正文区域保持干净的纸底。

这一页记于 2026 年 10 月 28 日。内容会随实践继续修改,欢迎按自己的材料再试一遍。

皖ICP备2026041033号-1