最近想给图标换一套风格。参考对象是 Anthropic:他们的图标是手绘的,线条微微不齐,有一种”人画的”温度。

但他们那套是真的一张张画出来的。我有几十个图标,散在导航、文章、页脚各处,逐个重画不现实,重画完还得维护。

后来发现根本不用画。

一个 SVG 滤镜,加一条 CSS 规则,就能让全站所有图标统一变成手绘风——本站现在就是这么跑的。

先看效果,这是本站导航图标,滤镜关闭时:

打开之后:

没有改任何一个 SVG 文件,没有引入任何新图标资源。变化只发生在渲染这一层。

原理

靠的是两个 SVG 滤镜原语:

  • feTurbulence:生成一张噪声图,像老电视的雪花
  • feDisplacementMap:拿这张噪声图当”位移地图”,把原图形的像素往不同方向推

线条的每个像素被噪声推得偏离原位一点点,看起来就像手不稳画出来的。这个技法有个老名字,叫 Squigglevision,早年的手绘动画里就在用。

完整定义就这么几行:

1
2
3
4
5
6
7
8
9
10
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="sketch" x="-20%" y="-20%" width="140%" height="140%">
<feTurbulence type="fractalNoise" baseFrequency="0.062"
numOctaves="1" seed="0" result="n"/>
<feDisplacementMap in="SourceGraphic" in2="n" scale="6"
xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
</svg>

这个 <svg> 宽高都是 0,页面上看不见,它只负责存滤镜定义。放在 layout 里,每个页面渲染一次。

然后是应用它的 CSS:

1
2
.nav-icon svg, .icon-link svg, .theme-toggle svg,
.search-toggle svg, .back-top-link svg { filter: url(#sketch); }

完了。就这两段。

关键在”切面”

这套做法真正让我满意的不是效果,是结构。

滤镜参数只存在一份,所有图标通过 url(#sketch) 引用它。 想全局调风格,改一个 filter 的属性,整站跟着变。以后新增图标,只要落在选择器范围内,自动继承手绘风,不需要任何额外操作。

这就是切面的思路:横切关注点(图标风格)从每个图标里抽出来,集中到一处。CSS 的级联天生就是干这个的。

反过来,不想被切到的元素也很好豁免。比如本站左上角的钢笔 logo,我希望它保持精致,把它排除在选择器外就行:

logo 是干净的,右边的导航图标和社交图标全是手绘。同一个页面,一条规则的事。

参数怎么调

手感基本由两个参数决定:

  • baseFrequency:噪声的频率,决定抖动的”密度”
  • scale:位移幅度,决定抖动的”力度”

太小没感觉,太大就不是手绘而是抽象画了。三档对比,都是本站真实截图:

baseFrequency 0.02 / scale 2,很克制,凑近才能看出来:

baseFrequency 0.062 / scale 6,我最后选的参数,手绘感明确但图形还稳:

baseFrequency 0.09 / scale 12,翻车现场,留作警示:

调参建议:先把 scale 拉到夸张,找到图形开始”认不出来”的临界点,再退回来一半。numOctaves 保持 1 就够,调高了线条会发毛。

还能动起来

feTurbulence 有个 seed 属性,换一个 seed 就换一套噪声,抖动形态跟着变。在滤镜里嵌一段 SMIL 动画,让 seed 循环:

1
2
3
4
<feTurbulence type="fractalNoise" baseFrequency="0.062" numOctaves="1" seed="0">
<animate attributeName="seed" values="0;7;13" dur="0.36s"
calcMode="discrete" repeatCount="indefinite"/>
</feTurbulence>

线条就会以每秒约 8 帧的节奏”沸腾”,像有人在反复重描——手绘动画的经典质感,零 JS。

我试了一晚上,最后还是关掉了。满屏图标一起抖,看久了有点晕。但如果只用在少数强调元素上,效果很好。

几个注意点

  • 小图标要收着点。 同样的 scale,40px 的图标是”手绘”,16px 的图标可能就是”糊”。图标普遍偏小的话,把 scale 降到 2~4。
  • 位图滤镜,不是矢量变形。 feDisplacementMap 推的是像素,不是路径锚点。所以这套效果只存在于渲染时,SVG 源文件还是原来的直线。想要真正弯曲的矢量路径(比如给 Figma 用),那是另一条路,得用 rough.js 这类库重新生成路径。
  • 兼容性总体没问题。 现代浏览器都支持这两个滤镜原语,静态部署(比如 GitHub Pages)完全不受影响,因为一切都发生在读者的浏览器里。

最后

从”想要一套手绘图标”到全站上线,实际改动是:layout 里加一段隐藏的滤镜定义,CSS 里加一条规则。

图标一个都没动。

这大概是我最喜欢的一类前端方案:效果看起来像重做了设计,实现上只是换了个杠杆的支点。