mcpField
P
pointer-trail-emitter

pointer-trail-emitter

github.com/mengto设计与 UI7.1k1.3k 次安装3 天前更新
代码仓库

关于 pointer-trail-emitter

用于制作指针驱动粒子轨迹的设计技能,粒子间距依据指针移动距离而非时间。内容涵盖粒子定位、漂移、渲染约束、输入可访问性和减少动态效果处理。

主要功能

  1. 01按移动距离发射粒子
  2. 02沿指针路径均匀定位
  3. 03发射器滞后与粒子漂移
  4. 04限制大幅移动时的生成量
  5. 05键盘与触屏输入考虑
  6. 06减少动态效果静态画面

适用场景

  1. 01让不同指针速度下的轨迹保持连贯
  2. 02避免快速移动时出现断点或堆积
  3. 03为场景内 3D 效果添加漂浮粒子
  4. 04提供可访问及减少动态效果方案

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mengto/skills --skill pointer-trail-emitter

安装后在 Claude Code 中输入/pointer-trail-emitter确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

agents/
openai.yaml
demo/
index.html
PROMPT.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:附着到摄像机;即使装置漂移,轨迹仍保持在手部下方
  • 散布范围以平面尺寸为基准,而不是使用绝对值
  • 微粒会滑行一段距离,而不是在生成点附近的一小段距离内就停下
  • 指针静止时仍保持光晕,而不会形成柱状粒子流
  • 圆形精灵不设置逐粒子角度属性
  • 生成循环对瞬移的指针设有上限保护
  • 可通过键盘操作;触摸操作不会形成粒子流
  • 减少动态效果时渲染的是设计好的静止画面,而不是隐藏图层
  • 性能开销的结论来自性能分析;报告任何回归前都已重新运行验证

评论

支持 Markdown