关于 pointer-trail-emitter
用于制作指针驱动粒子轨迹的设计技能,粒子间距依据指针移动距离而非时间。内容涵盖粒子定位、漂移、渲染约束、输入可访问性和减少动态效果处理。
主要功能
- 01按移动距离发射粒子
- 02沿指针路径均匀定位
- 03发射器滞后与粒子漂移
- 04限制大幅移动时的生成量
- 05键盘与触屏输入考虑
- 06减少动态效果静态画面
适用场景
- 01让不同指针速度下的轨迹保持连贯
- 02避免快速移动时出现断点或堆积
- 03为场景内 3D 效果添加漂浮粒子
- 04提供可访问及减少动态效果方案
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mengto/ skills --skill pointer-trail-emitter安装后在 Claude Code 中输入/pointer-trail-emitter确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamldemo/index.htmlPROMPT.md详细介绍
指针轨迹发射器
当轨迹密度需要随手部移动速度变化时,请自行构建发射器。
如果想要 Shaders 库中打包好的 WebGPU 效果,请使用 add-shader-cursor-trail 或 shaders-cursor-ripples。如果想让光标通过蒙版显露第二张图像,请使用 reveal-hover-effect。如果想让微粒布满一个区块,而指针只对它们产生扰动,请使用 ambient-section-particles。如果需要让指针铺出微粒,请使用此方案。
随附的演示刻意让舞台保持中性。纯色深色背景便于判断间距、散布和滑行效果,不会有背景图像与轨迹争抢视觉焦点。这些微光由无依赖的 Vanilla JavaScript 实现,通过 Canvas 2D API 渲染;CSS 只负责界面样式。没有着色器、WebGL 或 Three.js。请将实时画布与界面分开,这样发射器就能保持可测试,而不是被嵌入到某个组合效果中。
按距离而不是时间发射
这就是整个机制。累积发射器移动的距离,并按固定步长生成微粒:
E.acc += moved;
let guard = 0;
while (E.acc >= STEP && guard++ < 14) {
E.acc -= STEP;
spawn(/* … */);
}
这样,沿路径的间距始终是 STEP,无论手部如何移动,因此无论是缓慢移动还是快速挥动,轨迹看起来都像一条连续的带状线。
如果改为按计时器发射,间距就会与速度成正比——指针在两次生成之间移动 speed × interval 的距离。**快速挥动会让线条断成零散的点,而静止的手会把所有微粒堆在同一个位置。**这正是此方案要避免的问题;值得构建这个切换开关,亲眼看看差别。
在一段固定路径上测量:按距离发射,缓慢移动时生成了 1885 个微粒,快速移动时生成了 1738 个,差异为 1.08 倍——数量取决于路径长度。相同的两次移动若按计时器发射,则生成了 2537 个和 1545 个,差异为 1.64 倍——数量取决于时钟。
为循环设置上限。窗口失去焦点、标签页恢复,或指针瞬间跳转,都可能让你一次收到一个极大的 moved;如果没有保护机制,单帧就会生成数千个微粒并导致卡顿。
在各自应处的位置生成微粒
如果在一帧内生成的所有微粒都放在指针当前位置,它们就会挤在这段路径的一端。快速挥动时,轨迹看起来会像一个团块,后面还留着空隙。应根据每个微粒沿路径的距离,将其放在各自的位置:
const t = moved > 1e-6 ? Math.min(1, guard * STEP / moved) : 0;
spawn(E.lx + dx * t, E.ly + dy * t, ang);
先获取环形缓冲区槽位,再推进索引
const i = E.i; E.i = (i + 1) % N; // correct
如果先推进索引,就会把位置写入下一个槽位,却把生命值写入当前槽位,因此**每个微粒都会出现在前一个微粒开始的位置。**密集轨迹会掩盖这个问题;稀疏轨迹则会在每次生成时都显露出来。可识别的症状是:微粒看起来总比光标落后一个步长,而且是突然出现,而不是淡入。
让发射器滞后于指针
不要将发射器固定在指针位置,而是让它逐渐靠近指针:
E.x = damp(E.x, px, 16, dt);
发射器若被刚性固定,快速挥动时会让人觉得轨迹焊在光标上。正是这种滞后,为漂移带来了松弛感。
让轨迹锚定在屏幕上,而不是世界空间中
对于场景内的 3D 轨迹,应将点设为摄像机的子对象,并在摄像机空间中处理。通过视锥自身的半高将指针映射到场景中:
const hh = Math.tan(camera.fov * Math.PI / 360) * D;
const x = nx * hh * camera.aspect, y = ny * hh;
如果反投影到世界平面,轨迹就会固定在场景中:一旦机位移动或产生视差,轨迹就会在屏幕上游移,而无法始终停留在手部下方。
使用不受深度影响的四边形或点(depthTest:false、depthWrite:false),并为它们设置独立的渲染顺序。如果场景还有其他渲染通道——例如镜面或反射探针——请将轨迹放在独立图层中,确保它不会出现在这些通道里。
根据轨迹所在平面的尺寸调整散布范围
散布范围没有绝对意义。在距离为 3.4 个单位、摄像机视角为 36° 时,轨迹所在的平面高度只有约 2.2 个单位——因此 ±0.03 个单位的抖动只会形成紧贴光标的细线,而非漂移效果。
先计算平面的范围,再将散布量表示为它的一个比例。同一个数值在一个摄像机视角下看起来像柔和的云团,在另一个视角下却可能是一条硬直线。
让它们缓缓漂浮
阻尼比初始速度更重要。设为 1 - 1.1 * dt 时,每个微粒都会在距离生成位置不到 0.1 个单位处停下,轨迹也始终无法散开;将其减半,微粒就会四散开来。
加入缓慢的旋涡运动,让漂移变得纷乱,而不是沿一条直线吹走;再加上轻微的恒定上升力,让它们表现得像是浮起来的,而不是被扔出去的。
舍弃圆形微粒用不到的内容
圆形精灵没有朝向。移除每个粒子的角度属性和旋转后的 gl_PointCoord 查找,不要只是把它们设为 0——这样可以省去一个属性、一次上传,以及片元着色器中的多条指令,而这些原本都只是为了实现肉眼看不出的旋转。
让微粒保持小巧:核心只有几像素,外面带一圈微弱的光晕。小精灵能让你增加数量,而不必承担满屏大精灵造成的加法混合填充成本。
手静止时也留一口气
按距离发射意味着指针静止时没有移动,因此不会发射微粒——静止的手下方,轨迹会消失。所以,只针对这种情况,按计时器缓慢发射微粒。
关键在于要少量:每 ~0.4 秒发射一个。若从静止的指针频繁发射,画面中央就会积起一根永久的烟柱——这正是这种机制要避免的计时器问题,却又会被人为地重新引入。
数值
在 36° 摄像机视角下,轨迹悬于 3.4 个单位处,平面高度约为 2.2 个单位时调校得出。请根据自己的平面范围缩放空间数值。
| 参数 | 数值 | 说明 |
|---|---|---|
| 发射步长 | 0.030 个单位 | 两次生成之间的距离 |
| 每帧生成数上限 | 14 | 防止瞬移 |
| 发射器阻尼 | damp(…, 16, dt) | 发射器相对指针的滞后 |
| 散布 | ±0.30 个单位 | 约为平面高度的 13% |
| 深度抖动 | ±0.45 个单位 | 打破平面薄片感 |
| 生命周期 | 1.45–2.75 秒 | 静止微粒为 2.1–3.4 秒 |
| 初始速度 | 沿运动方向 −0.09,侧向 ±0.19 | 逆着运动方向 |
| 滑行阻尼 | 1 − 0.5 * dt | 从 1.1 减半;见上文 |
| 浮力 | +0.022 · dt | |
| 旋涡 | sin(t·1.3 + φ)·0.17、cos(t·1.1 + 1.7φ)·0.14 | 每个微粒的相位 φ |
| 大小 | 0.018–0.050,×(1 + 0.55u) | 微粒会变柔和,不会膨胀 |
| 不透明度 | 在 u 0–0.12 内渐入,在 0.22–1 内渐出,×0.9 | |
| 数量 | 桌面端 190,低配档 90 | |
| 静止发射 | 每 0.42 秒一次 |
不要为了提高 z-index 而把它移到 DOM 覆盖层
WebGL 画布内的任何内容都无法高于页面——画布本身是一个位于自身堆叠层级的元素——所以 2D 覆盖层画布看起来像是实现该图层的唯一方法。的确如此,但仍不值得这样做。
移植会牺牲后处理链:微粒会变成没有辉光的硬点,而复制一层更宽、更淡的效果也并不等同于原效果。还必须换算每个常量,而不是重新挑选——px_per_unit = (innerHeight / 2) / (tan(fov / 2) * D),精灵直径为 innerHeight * size / D——而不是通过换算复现效果、凭肉眼重建,最终会得到一个不同的效果,还得重新审批。
如果确实需要这一图层,请按纯粹的换算方式移植,并在给任何人看之前,将渲染帧与旧版本进行差异对比。
根据性能分析结果说明开销
每个微粒的更新几乎没有开销。对含 190 个微粒的轨迹进行 CPU 性能分析时,更新占0.00% 的采样——低于分析器的采样下限。开销完全来自加法混合填充,因此应依次调整精灵大小和数量。
报告回归前先测量。有一次,这条轨迹的帧时间对比显示 p90 上升了 20–30%,后来发现只是噪声:对完全相同的代码运行三次,结果分别为 226 / 374 / 243 ms。多跑几次再相信结果。
生命周期与减少动态效果
- 使用
matchMedia('(hover: none)')进行判断。触摸设备上没有可跟随的悬停位置;应停放发射器,或仅在拖动时由pointermove驱动,否则静止的发射器会不断形成永久粒子流。 - 在
prefers-reduced-motion: reduce下,渲染设计好的静止画面——一条已铺展在画面上的组合轨迹——而不是将其隐藏。保持控件可用,以便它们能重新绘制该画面。 - 在
document.hidden时暂停,恢复时重置时间基准,将dt限制为约 1/30 秒,并将 DPR 上限设为 2。 - 效果不能只依赖指针。为发射器提供键盘操作方式,让效果完整且无需鼠标也能操作。
验证
- 指针移动前,轨迹在中性背景上清晰可辨
- 演示不依赖背景图像来让效果显得完整
- 沿路径的间距恒定;快速划动和缓慢移动绘制出的丝带相同
- 通过测量而非假设确认:沿固定路径的微粒数量几乎不随速度变化
- 快速划动会沿整段路径铺开微粒,而不是聚集在一端
- 环形缓冲区的槽位先被占用,再推进索引
- 3D:附着到摄像机;即使装置漂移,轨迹仍保持在手部下方
- 散布范围以平面尺寸为基准,而不是使用绝对值
- 微粒会滑行一段距离,而不是在生成点附近的一小段距离内就停下
- 指针静止时仍保持光晕,而不会形成柱状粒子流
- 圆形精灵不设置逐粒子角度属性
- 生成循环对瞬移的指针设有上限保护
- 可通过键盘操作;触摸操作不会形成粒子流
- 减少动态效果时渲染的是设计好的静止画面,而不是隐藏图层
- 性能开销的结论来自性能分析;报告任何回归前都已重新运行验证