最近想给图标换一套风格。参考对象是 Anthropic:他们的图标是手绘的,线条微微不齐,有一种”人画的”温度。
但他们那套是真的一张张画出来的。我有几十个图标,散在导航、文章、页脚各处,逐个重画不现实,重画完还得维护。
后来发现根本不用画。
一个 SVG 滤镜,加一条 CSS 规则,就能让全站所有图标统一变成手绘风——本站现在就是这么跑的。
先看效果,这是本站导航图标,滤镜关闭时:
![]()
打开之后:
![]()
没有改任何一个 SVG 文件,没有引入任何新图标资源。变化只发生在渲染这一层。
原理
靠的是两个 SVG 滤镜原语:
feTurbulence:生成一张噪声图,像老电视的雪花feDisplacementMap:拿这张噪声图当”位移地图”,把原图形的像素往不同方向推
线条的每个像素被噪声推得偏离原位一点点,看起来就像手不稳画出来的。这个技法有个老名字,叫 Squigglevision,早年的手绘动画里就在用。
完整定义就这么几行:
1 | <svg width="0" height="0" style="position:absolute" aria-hidden="true"> |
这个 <svg> 宽高都是 0,页面上看不见,它只负责存滤镜定义。放在 layout 里,每个页面渲染一次。
然后是应用它的 CSS:
1 | .nav-icon svg, .icon-link svg, .theme-toggle svg, |
完了。就这两段。
关键在”切面”
这套做法真正让我满意的不是效果,是结构。
滤镜参数只存在一份,所有图标通过 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 | <feTurbulence type="fractalNoise" baseFrequency="0.062" numOctaves="1" seed="0"> |
线条就会以每秒约 8 帧的节奏”沸腾”,像有人在反复重描——手绘动画的经典质感,零 JS。
我试了一晚上,最后还是关掉了。满屏图标一起抖,看久了有点晕。但如果只用在少数强调元素上,效果很好。
几个注意点
- 小图标要收着点。 同样的
scale,40px 的图标是”手绘”,16px 的图标可能就是”糊”。图标普遍偏小的话,把 scale 降到 2~4。 - 位图滤镜,不是矢量变形。
feDisplacementMap推的是像素,不是路径锚点。所以这套效果只存在于渲染时,SVG 源文件还是原来的直线。想要真正弯曲的矢量路径(比如给 Figma 用),那是另一条路,得用 rough.js 这类库重新生成路径。 - 兼容性总体没问题。 现代浏览器都支持这两个滤镜原语,静态部署(比如 GitHub Pages)完全不受影响,因为一切都发生在读者的浏览器里。
最后
从”想要一套手绘图标”到全站上线,实际改动是:layout 里加一段隐藏的滤镜定义,CSS 里加一条规则。
图标一个都没动。
这大概是我最喜欢的一类前端方案:效果看起来像重做了设计,实现上只是换了个杠杆的支点。
评论