Letter Badges 复刻记录
这篇记录整理今天复刻 Letter Badges 交互 demo 的过程。目标不是完全照搬截图,而是用 canvas 做出类似的粒子扰动、鼠标追随和隐藏角落彩蛋。
复刻目标
最初的画面重点有三个:
- 背景里铺满圆形字母徽章;
- 黑色小生物经过时,附近徽章被推开并变成彩色;
- 整体保持轻微漂浮感,而不是静态排版。
后续又加了一个隐藏交互:鼠标进入四个角时触发文字雨。上方角落出现“我想你了”,下方角落出现 LOVE。
粒子徽章
徽章没有用 DOM 元素,而是全部放在 canvas 里绘制。每个徽章都保存自己的位置、原始位置、速度、旋转角度、颜色和漂移方向。
这样做的好处是状态很集中,适合每帧统一更新:
- 小生物靠近时,根据距离给徽章一个向外的斥力;
- 徽章离开原位后,用一个很小的回弹力拉回
homeX/homeY; - 再叠加轻微随机漂移,让画面不死板;
- 靠近小生物的徽章画成彩色实心圆,远处则保留灰色描边。
一开始徽章数量偏多,画面虽然密,但和文字雨叠加后容易增加绘制压力。后来把徽章密度减半,视觉上更清爽,也给后面的文字雨留出空间。
小生物追随鼠标
最初的小生物是直接跟着鼠标位置移动,反应太硬。后来改成 PID 风格的追随算法:鼠标只是目标点,小生物用比例项、积分项和微分项慢慢靠近。
这个改动让小生物更像一个有惯性的活物,但也暴露了一个问题:如果角落触发逻辑看的是小生物位置,PID 的延迟会导致鼠标已经进入角落,文字雨却还没出现。
最后的处理是把“触发逻辑”和“视觉表现”分开:
- 鼠标位置负责判断是否进入隐藏角落;
- 小生物位置只负责视觉运动和推开徽章;
pointer.active控制小生物是否追随鼠标;pointer.inside控制鼠标是否还在画布内,从而决定文字雨能不能触发。
这个拆分很重要:用户期待的是“我把鼠标移到角落就触发”,而不是“等小生物慢慢追到角落才触发”。
隐藏角落和彩蛋
一开始四个角画了半透明色块,用来提示触发区域。实际看起来有点破坏画面,于是把区域隐藏,只保留逻辑判断。
四角判断很简单:根据 CORNER_SIZE 判断鼠标是否同时靠近左右边和上下边。左上、右上触发中文文字雨;左下、右下触发英文文字雨。
这里有一个细节:如果只在 pointermove 里触发,鼠标停住后文字雨就不会继续出现。所以最终把持续触发放进每帧 update() 里,只要鼠标还在角落,就按照冷却时间重复生成。
文字雨调试
文字雨经历了几轮调整:
- 从角落附近生成:效果太集中,不像满屏文字雨;
- 全屏随机生成:能铺满,但容易局部拥挤;
- 减少数量:不卡了,但画面有时显得空;
- 分栏随机生成:不增加数量,也能更均匀地覆盖整个画布。
最终使用横向分栏:每一批文字雨按画布宽度分成若干 lane,每个 lane 里随机一个 x 坐标。这样数量不变,但分布比纯随机稳定。
性能经验
这次最明显的性能问题来自文字雨。canvas 里每个雨滴每帧都要更新位置、旋转、透明度,并执行 fillText()。如果再加上 shadowBlur,成本会更高。
当鼠标停在角落时,文字雨会持续生成。如果只生成不限制,总数量会越来越多,每帧绘制压力也会越来越大。所以最后加了两个限制:
- 降低每次生成的文字数量;
- 设置
MAX_RAIN_DROPS,超过上限就删除最早的雨滴。
这个经验可以复用到其他 canvas 小 demo:粒子效果可以持续生成,但一定要有生命周期和总量上限。
今天的结论
这次复刻最大的收获是:交互 demo 不能只看“代码逻辑对不对”,还要看用户直觉是否一致。
PID 追随让运动更自然,但不能让触发反馈变慢;文字雨要铺满画布,但不能靠无限增加数量解决;隐藏区域可以存在,但不一定要画出来。最后的实现更像是几组取舍叠在一起:视觉上柔和,触发上直接,性能上有上限。
Demo 页面:/pretext-demo/letter-badges.html




