Skip to content

03 跟随光标

让圆点绕着指针围成圆环;指针离开页面后,再散回空闲漂移

指针事件可参考 MDN Pointer events

对照扩展里 particlesfollow 的 Diff。这一章会改粒子的 step(),并加上 src/pointer/

1. 新建 src/pointer/pointer.js

创建 src/pointer/ 和这个文件。它记下指针位置,并在离开时让每个粒子散开:

js
import { particles } from "../scene/loop.js";

export const pointer = { x: 0, y: 0, active: false };

/**
 * Records the pointer position in canvas coordinates.
 *
 * @param {PointerEvent} event
 */
export function onPointerMove(event) {
  pointer.x = event.clientX;
  pointer.y = event.clientY;
  pointer.active = true;
}

/**
 * Releases the swarm so particles scatter back into idle drift.
 */
export function onPointerLeave() {
  if (!pointer.active) {
    return;
  }
  pointer.active = false;
  for (const particle of particles) {
    particle.scatterFrom(pointer.x, pointer.y);
  }
}

2. src/particle/particle.js — 圆环运动

文件顶部保留 canvas import,并增加

js
import { pointer } from "../pointer/pointer.js";

constructor 里,设置完 xyradius 之后,不要再直接写死 vx / vy,改成空闲漂移加上各自的轨道半径和角速度:

js
this.wander();
this.vx = this.driftVx;
this.vy = this.driftVy;
this.orbitRadius = 76 + Math.random() * 16;
this.orbitSpeed = 1.05 + Math.random() * 0.25;

在类上增加这两个方法(例如写在 constructor 后面):

js
  /**
   * Picks a new idle drift velocity.
   */
  wander() {
    this.driftVx = (Math.random() - 0.5) * 3.8;
    this.driftVy = (Math.random() - 0.5) * 3.8;
  }

  /**
   * Pushes this particle away from a point and assigns a new idle drift.
   *
   * @param {number} originX
   * @param {number} originY
   */
  scatterFrom(originX, originY) {
    const dx = this.x - originX;
    const dy = this.y - originY;
    const dist = Math.hypot(dx, dy) || 1;
    this.vx += (dx / dist) * 3.2;
    this.vy += (dy / dist) * 3.2;
    this.wander();
  }

替换 step() 的函数体:指针激活时缓向圆环(限速,不要瞬移),末尾仍调用原来的 wrap()

js
  step() {
    if (pointer.active) {
      const dx = this.x - pointer.x;
      const dy = this.y - pointer.y;
      const dist = Math.hypot(dx, dy) || 1;
      const nx = dx / dist;
      const ny = dy / dist;
      const radial = Math.max(-1.8, Math.min(1.8, (this.orbitRadius - dist) * 0.035));
      const tx = nx * radial - ny * this.orbitSpeed;
      const ty = ny * radial + nx * this.orbitSpeed;
      this.vx += (tx - this.vx) * 0.05;
      this.vy += (ty - this.vy) * 0.05;
    } else {
      this.vx += (this.driftVx - this.vx) * 0.04;
      this.vy += (this.driftVy - this.vy) * 0.04;
    }
    this.x += this.vx;
    this.y += this.vy;
    this.wrap();
  }

wrap()draw() 保持不变。

3. src/main.js — 监听指针

在文件顶部和其他 import 放在一起:

js
import { onPointerLeave, onPointerMove } from "./pointer/pointer.js";

boot() 里,createParticles(); 之后frame(); 之前

js
window.addEventListener("pointermove", onPointerMove);
document.documentElement.addEventListener("pointerleave", onPointerLeave);

不要删除 boot()

验收

http:// 打开。移动鼠标:点应围成空心圆环,而不是堆在指针尖上。移出窗口后,它们应重新散开漂移。

下一章:光晕与拖尾