鼠标经过导航,表面的高光缓缓移动,边缘轻微变形。这种反馈很容易越做越多:再加一点倾斜,再铺一层波纹,最后连正在阅读的文字也跟着晃。
博客需要长时间阅读。交互首先要回答的,是哪些东西可以动、什么时候停,以及没有鼠标时页面还能不能正常使用。
让表面动,文字留在原位
这版导航把装饰表面和链接拆成两层。变换只写到表面,链接的位置、焦点框和点击区域保持稳定。当前目标位移的水平幅度约 2px,垂直约 1.3px;这些值是页面的视觉取舍,不是通用标准。
如果把变换施加到整个导航,文字的抗锯齿可能变化,目标也会在点击前移动。把两层分开后,可以调整材质,又不必重新处理文字与点击区域。
Apple 的材质指南把 Liquid Glass 放在导航、控件这一功能层,并建议克制使用。对文章正文,稳定的底色更容易维护。
先把指针换算成局部坐标
clientX 和 getBoundingClientRect() 都使用视口坐标,可以直接相减。不要把页面坐标 pageX 混进来,否则滚动页面后高光会偏移。
const bounds = surface.getBoundingClientRect();
if (!bounds.width || !bounds.height) return;
const clamp01 = (n: number) => Math.max(0, Math.min(1, n));
const x = clamp01((event.clientX - bounds.left) / bounds.width);
const y = clamp01((event.clientY - bounds.top) / bounds.height);
targetX = x * 2 - 1;
targetY = y * 2 - 1;
归一化后,左右边缘分别是 -1 和 1,中间是 0。CSS 高光用百分比,Canvas 采样则乘画布内部尺寸。两者共享比例,不共享像素数;高分屏上的 CSS 像素和画布像素未必相等。
这也是要限制采样量的原因。首页示意图把内部宽度限制在 540 像素、总量约 18 万像素以内,像素比例上限为 1.5。它表达材质,不承担正文的清晰度要求。
输入更新目标,帧循环负责靠近
pointermove 只记录目标和扰动强度,不在每次事件里完成逐像素绘制。已有帧任务时也不重复申请。帧回调根据时间差推进位置,让快速移动和慢速移动使用同一套规则。
这里使用带阻尼的回弹。判断结束时,同时看速度和离目标的距离;只看距离,可能在经过目标的那一刻错误停止。波纹还要把剩余能量算进去。当前残余量小于 0.006 后画出最终状态,不再续订下一帧。
停止也是交互的一部分
指针离开时,目标归零,允许短暂回弹。组件离开视口、页面隐藏、用户开启减少动态效果时,则取消帧任务并复位。回到页面只绘制静态状态,等新的输入再唤醒。
此外,只有支持悬停的精细指针会启用跟随,并检查事件的 pointerType。触屏保留静态材质,滑动仍由浏览器处理。页面切换时清理监听器和观察器,避免旧组件继续占用资源。
这些边界可以直接验收:移动时有反馈,停下来会安静;键盘能操作所有链接;关闭脚本后,文章仍然能读。
