侧边栏顶上那张头像,挂的是一张 AI 生成的紫色鸟。用了几个月,越看越别扭——不是说它丑,是它跟这个站的气质不搭。
同一时间我还惦记着另一件事:侧边栏那个名字,能不能换成手写签名。
两件事凑一起,就有了这一轮返工。过程比预想曲折:三换字体、两换方向,还挖出两个藏了很久的 CSS 问题。
第一回合:让 AI 再生成一张,还是去图库找
原先那张紫鸟就是生成的。问题在于生成物总带着一股说不清的”平均感”——构图挑不出错,但也没有任何地方让人记住。再来一张大概率还是这样,所以换个思路:去公共图库找真人拍的图。
关键词锁定极简 / 抽象。下了三十张,第一轮把太花的、有明显主体的、一眼看着像壁纸的都筛掉,剩九张:

怎么从九张里定一张?我用了一个很实际的判据——站点主题色。
本站主色是 oklch(0.70 0.14 250),约等于 #53A3F2,一个偏亮的天蓝。手写签名要压在头像上,所以头像最好是低饱和的中性底,能把天蓝衬出来。按这个标准排,3 号(蓝紫幻境)和 6 号(青蓝流线)先出局:它们本身就是蓝紫调,签名压上去等于蓝叠蓝,糊成一团。
最后选中的是 9 号,黑白灰的抽象几何。理由是它几乎是纯灰阶,签名不管什么颜色都压得住。
第二回合:我按圆形裁切,但头像框是方的
选完图,我做了件想当然的事:把九张候选全按圆形裁切,拼成对比图。上面那张图就是这么来的。
但这是错的——回头翻 Profile.astro 才看清,头像外面套的是 rounded-xl + overflow-hidden,配 object-cover 填一个 256×256 的方框。是圆角方形,根本不是圆形。
后果很实际:那九张圆形预览完全不能代表真实观感。一张靠斜向线条构图的图(比如选中的 9 号),裁成圆形后只剩中间一坨,看不出原来的走向;反过来,方框下会露出来的四个角,在圆形预览里也全被切掉了。
重写预览脚本,按真方框、真圆角、真尺寸渲染:
![]()
裁切落地就一条命令,源图 → 中心裁 1:1 → 640×640 PNG,体积从 378KB 降到 192KB:
node .workbuddy/tools/set-avatar.mjs <源图> public/avatar.png这里还有个容易忽略的细节:object-cover 是从中心裁的,所以选图时画面重点必须落在正中,否则会被裁掉。九张候选里就有两张主体的位置偏下,方框一裁只剩背景。
第三回合:签名换了三次字
先说我想要的机制。签名不是文字,是一张透明底的单色图,视觉上压在头像下缘。显示时用 CSS mask-image 指向这张图,再给元素填 background: var(--primary):
<span class="signature-mask" style={`mask-image: url('${signature}')`}></span>.signature-mask { position: absolute; inset: 0; background-color: var(--primary); /* 图只负责形状,颜色由主题色决定 */ -webkit-mask-size: contain; mask-size: contain; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; -webkit-mask-position: center; mask-position: center;}好处是图只管形状、颜色由主题色决定。哪天改主题色,签名自动跟着变,不用重新出图。
接下来是选字,一共换了三轮:
- 华文行楷「留心」——系统自带。落在图上太”印刷”,笔画粗细几乎一样,没有飞白,看着像从字帖上截下来的。
- 志莽行书——比行楷活,但两个方块字撑不起一条签名,字形也偏”设计字体”。
- 英文
Watch Your Back——最后定的。理由很实际:侧边栏文字栏是横长的一条(240×47),两个中文字放进去,左右全是留白;换成西文连笔,正好横向铺满,内容也和站点name对得上。
字体选 Great Vibes。它的笔画是 hairline 级的细,直接压在照片上几乎看不见,所以生成时给它描了一道同色的 0.8% 细边当”加墨”:

生成脚本走的不是文字渲染,而是取字形轮廓:opentype.js 取 path → 拼成一条 SVG path → sharp 按 300dpi 渲染 → 紧贴笔画外框裁切 → 输出 webp。这样字距、裁切边距、笔画粗细全部可控,也不受机器上装没装这个字体影响。
NOTE这里踩了个 ESM 的坑:opentype.js 的
dist/opentype.js是 UMD 包,在 ESM 下import会报self is not defined;它的loadSync()内部走require('fs'),ESM 下同样用不了。必须换成dist/opentype.module.js,再自己fs.readFileSync+opentype.parse()。
落款:短横 + 一句话
到这一步其实就能上线了。真正让我决定再改一轮的,是一张手写题款的图:主体字下面跟一条短横,再下面一行小字。三层一凑,签名才像”落款”,而不是”贴了张字上去”。
于是补上:签名 → 短横(主题色)→ 一句话(取 profileConfig.bio):

落点(原生 px,侧栏宽 280、文字栏宽 240):
| 元素 | 位置(相对头像下缘) |
|---|---|
| 签名底边 | +23.7 |
| 短横(32×4,主题色) | +33.7 |
| 小字 | +45.7 |
代价是卡片高度从 328.8 涨到 396.7(+68px),多一横一句的必然结果。
期间挖出来的两个真问题
一、短横和简介”本来就在”,只是被头像盖住了
动手前我以为要给卡片添加短横和简介。翻 DOM 才发现这两行一直都在,只是完全看不见。两个原因叠在一起:
- 层级——头像外面套的是
<a class="relative">,是定位元素;短横和简介是普通static元素。CSS 绘制顺序里定位元素盖在普通元素之上,所以头像把它们挡住了。 - 位置也不对——签名图当时挂着
margin-bottom: -40%,把下方元素往上拽了 96px,短横实际落在头像内部(下缘往上 33px)。等于双重看不见。
修法是给这两行加 position: relative; z-index: 60 抬到头像之上,再把 margin-bottom 从 -40% 调成 -12.125%,让它们落在签名正下方:
.signature-rule,.signature-caption { position: relative; z-index: 60;}二、margin 的百分比,基准是”宽度”不是”高度”
这是我自己记错过的一个点。签名浮层用了两个位移:margin-bottom: -40% 和 transform: translateY(-75%)。我一度以为加起来就是”上移自身高度的 115%“——错了。
margin的百分比,基准是包含块的宽度。哪怕设的是margin-top/margin-bottom这类纵向属性,也照样按宽度算。translateY的百分比,基准才是元素自身的高度。
两者基准不同,不能相加。算签名落点得分开算:文字栏宽 240 → 签名图高 = 240 × 273 / 1400 = 46.8px,translateY(-75%) 上移 35.1px。基准搞错,预览图会偏上十几像素,而且很难发现——因为拉不出明显错误,只是”感觉有点高”。
几个能复用的小工具
这一轮攒下几个不用开 dev server 就能验效果的脚本:
| 脚本 | 干什么 |
|---|---|
gen-signature.mjs | 取字形轮廓生成签名图,参数:文字 / 字体 / 字距 / 加墨 |
set-avatar.mjs | 任意图 → 中心裁 1:1 → 640×640 PNG |
preview-avatar.mjs | 按真实方框 + 真实位移渲染侧栏头像效果(亮/暗两套) |
preview-signature-block.mjs | 按 CSS 盒模型算落点,渲染落款三层结构 |
好处是不依赖浏览器:改完样式直接出 PNG,比开 dev server 快得多,也不会因为”本地看着对”就放过一个只在真实宽度下才暴露的偏差。
一点体会:这一轮真正花时间的不是写代码,是把”看起来对”变成”量出来对”。圆形预览、记错的 margin 基准、被盖住却以为不存在的元素——三个坑同一个来源,拿感觉代替了测量。
相关阅读
- 右侧悬浮工具栏:回顶部、随机一篇、目录抽屉与评论跳转是怎么做的:侧栏周边的按钮同样在这套工具栏里。
- 读完那篇「跳出舒适区」,我把侧栏折了起来:下一篇,侧栏折叠。