
HY
折腾进行时
Hello, I'm HY.








这次把整站的鼠标指针换了一遍。
亮色主题配高坂桐乃,深色主题配五更琉璃(黑猫),各 28 枚、一共 56 枚。正常选择和链接、正文和表单的文本光标、禁用状态、拖拽、忙碌与加载,以及滚动和调整大小那一批,全都换掉了;代码块除外 —— 那里我特意留着系统原生的 I 型光标,下面会说为什么。切换亮暗主题时整套指针自动跟着换,不需要手动挑。

手上只有两张总览图:一张桐乃、一张黑猫,每张 7 列 4 行的图标阵,底下压着中文标签。要变成能用的素材,得先把 28 个格子一个个切出来、去掉背景、缩到光标尺寸。
剪贴板里存的那份只有 1920 宽,翻回原始文件是 6144×4096,我直接用后者当源。
版面我用投影扫出来的:先按行统计「非白像素」,得到图标带和标签带交替的几条横带;再在图标带里按列扫一遍。这活看着机械,实际有两个坑等着我。
我以前处理贴纸用的是「全局去白」—— 把所有接近纯白的像素设成透明,那套在贴纸上很好使。放到这批图上,一跑就废。
桐乃那枚「正常选择」是个深色描边 + 白色填充的箭头,箭头内部是白的,人物的衣服也是白的,而且跟我页面背景一个色号(都是 255,255,255,我数了一下,那一格 63% 的像素是纯白)。全局去白的结果是箭头被掏成空壳,只剩一圈线。
我换了个思路:被描边围住的白色,本来就是图形的一部分。 先用阈值取「墨」(min(R,G,B) < 244),再对墨迹做孔洞填充,只补那些不与图像边缘连通的空白区域:
1ink = np.asarray(rgb).min(axis=2) < 2442# 四周先补一圈纯白,保证背景一定与边缘连通3pad = np.zeros((h + 320, w + 320), bool)4pad[160:-160, 160:-160] = ink5filled = ndimage.binary_fill_holes(pad)[160:-160, 160:-160]这下对了:箭头内部的白色补了回来,人物衣服也保住了,只有外圈那片真正连到边缘的白变成透明。我又给边缘补了一道 1px 高斯羽化,缩放走预乘 alpha(先乘再除回来),免得透明区的白色从描边外侧渗出一圈白边。
第一版我给两张图套了同一组裁切框,省事。结果黑猫那张有一半格子报警。
翻回原图才看明白:黑猫那张比桐乃多了一圈外框和一个页脚标,表头也略高一点。它的表头底边正好落在第 1 行裁切框里 —— 那一整行都是墨,填充之后整个框被填满;第 3 行的中文标签顶边又比桐乃那张高 7px,切进去就把「斜向调整大小 2」的标签文字粘成了图形的一部分。
所以裁切框我改成按主题各测一套:行框取「上一行标签底 + 6px」到「本行标签顶 − 6px」,列框取相邻列中心的中点、两端避开图集外框。改完两组各 28 格,零告警。
热点(点击判定点)我也让它跟着自动算:箭头和手指这类有尖端的,取内容区里 x + y 最小的那个像素;圆形、十字、调整大小这类取正中。桐乃和黑猫的指尖差 1~2px,所以两套分别算,不共用。
我要的是「亮色桐乃、深色黑猫」。最直白的写法是每条规则写两遍 —— 一套挂 html, body,一套挂 html.dark,28 个指针就是 56 条,以后改一个热点还得改两处。这种迟早会漂。
我试了一下 CSS 自定义属性,Chromium 里能跑通:
1:root {2 --ffc-normal-1: url("/images/cursors/kirino/normal-32.png");3 --ffc-normal-2: url("/images/cursors/kirino/normal-64.png");4 --ffc-normal-hx: 4;5 --ffc-normal-hy: 2;6}7html.dark {8 --ffc-normal-1: url("/images/cursors/kuroneko/normal-32.png");9 --ffc-normal-2: url("/images/cursors/kuroneko/normal-64.png");10 /* 黑猫的箭头尖位置不同,热点跟着换 */11 --ffc-normal-hx: 4;12 --ffc-normal-hy: 2;13}14html,15body {16 cursor: var(--ffc-normal-1) var(--ffc-normal-hx) var(--ffc-normal-hy), auto;17 cursor: -webkit-image-set(var(--ffc-normal-1) 1x, var(--ffc-normal-2) 2x)18 var(--ffc-normal-hx) var(--ffc-normal-hy), auto;19 cursor: image-set(var(--ffc-normal-1) 1x, var(--ffc-normal-2) 2x)20 var(--ffc-normal-hx) var(--ffc-normal-hy), auto;21}有两处是我先单独验过才敢往下写的。
一是变量能承载整个 <image>,也能承载热点那两个数字 —— cursor: <image> <number> <number>, <keyword> 里的每一段都可以是 var(),替换发生在计算值阶段,解析器不挑。于是「用哪张图」和「点在哪儿」全进了变量,切主题只换变量,28 条规则写一遍就够。顺手也确认了变量名写错时的兜底:整条声明失效、回退到 auto,不会把光标搞成一个奇怪的样子。
二是两种倍率共用同一组 CSS px 热点。2x 图渲染时也缩到 32 CSS px,所以热点只写一份(3 2 / 6 4 / 16 16),不用跟着倍率换算 —— 这也是我没选「直接塞一张 64px 大图」的原因:CSS 的 cursor 压根不能声明图片的固有尺寸,塞进去会得到一个 64px 的巨无霸光标。
每条规则我写了三档声明:纯 url() 兜底 → -webkit-image-set() → 标准 image-set()。不认识 image-set 的浏览器自动落回 1x 位图,而不是把整条声明丢掉。
正文和表单的文本光标我换成了魔法棒,但 pre、code 我特意留了系统原生的 I 型光标 —— 代码块是选命令行、选配置去复制的地方,32px 的位图不如一根细十字好瞄。
本来一句 pre, code { cursor: text } 就该收工,结果我一看 pre 上的 computed style,是 body 那个黄箭头。查下来是 expressive-code 自带的一条规则:
1.expressive-code :not(:is(svg, svg *)) {2 cursor: revert;3}revert 会把属性回滚到用户代理那一层 —— 对 pre 来说等于「我没写过」,于是它顺着继承链一路向上,继承到了 body 的箭头。它的特异性是 0,1,1,正好压过我那条 0,0,1 的 pre。
好在 revert 属于非 important 声明,所以 pre, code, kbd, samp, pre *, code * { cursor: text !important } 才压得住。pre 定住之后,内部那些同样写着 revert 的后代自然一路继承下来;而 expressive-code 的复制按钮在 <figcaption> 里、不在 pre 内,不会被这条误伤 —— 我单独回查了一遍,它仍然是链接指针。
这一环比实现麻烦:光标是浏览器在合成层上画出来的一层东西,不属于页面内容,任何截图方式都抓不到它。所以「指针换了没有」没法用一张图自证,能拿到的只有计算样式和图片能不能解码。
于是我把验证脚本写成这样:拿目标元素的 getComputedStyle(el).cursor,把里面那个文件路径抓出来,new Image() 加载一遍,确认 naturalWidth 是 32 —— 这一条同时证明了「声明命中了」和「图片存在且格式正确」。
在本地 dev 上跑下来:
| 元素 | 结果 |
|---|---|
body | kirino/normal 热点 4 2,兜底 auto |
a | kirino/link 热点 8 3,兜底 pointer |
p / h1 / td / input / textarea | kirino/text 热点 16 16,兜底 text |
pre / pre code / .ec-line | text(原生 I 型光标,没被覆盖) |
abbr[title] | kirino/help 兜底 help |
:disabled | kirino/unavailable 兜底 not-allowed |
[draggable] | kirino/move 兜底 move |
.expressive-code button | kirino/link(复制按钮没被 pre 那条误伤) |
28 个 [data-cursor="…"] | 全部命中对应图标 |
加上 html.dark 之后 | 整套换成 kuroneko,热点跟着变 |
112 个 PNG(28 枚 × 2 套 × 2 倍率)我逐个请求了一遍,全是 200,没有一个 404。这类「路径拼错、但因为一直没人点所以从没暴露」的问题,不挨个点一遍是发现不了的。
28 枚我全都挂上了 [data-cursor="…"] 点名,其中十来个有天然触发点、不用手写属性:

深色主题下同一页换整套素材,热点也跟着换:

既然拍不到光标本身,我至少能拍「鼠标划过去会变成什么」。顺手做了个自包含的单文件演示页,56 张图全部内嵌成 data URI,打开就能划,右侧那个按钮切主题 —— 这篇的配图也是从那上面截的。

| 文件 | 状态 | 干什么 |
|---|---|---|
src/config/FooterConfig.html | 改(追加) | 光标样式块:变量定义 + 28 条规则 + 代码块兜底 |
public/images/cursors/kirino/* | 新增 | 桐乃 28 枚 × 2 倍率 |
public/images/cursors/kuroneko/* | 新增 | 黑猫 28 枚 × 2 倍率 |
public/assets/js/dynamic-comment-enhance.js | 改 | STYLE_IDS 补一条,让动态评论 iframe 也吃同一套 |
还是本站用惯的路子 —— 注入点仍在 src/config/FooterConfig.html,一个主题源文件都没动。素材总共 700 KB,单张 1.4~9 KB,浏览器按需取用。
最后一条我想单独说。动态页的评论区跑在一个同源 iframe 里,而那个页面走的是不带页脚的 Layout,页脚注入到不了它。之前给回复折叠和回复引用补过一个「父页面代投」,所以这次我只往那份代投名单里加了一个 ff-cursor-style 就完事 —— 那个样式块内部自己分了两套主题,iframe 那边同样从 localStorage 拿到 dark 类,两边自然一致。我点开动态页的评论面板回查过,iframe 里的 body 和链接也都是俺妹指针。
两张总览图我也收进了相册(俺妹指针主题),原图字节一致、零压缩。
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!
部分内容可能已过时
分享你的想法,与大家交流讨论