Wild
返回文章列表
技术#Canvas#Animation#React#TypeScript#Performance

用 Canvas 打造网页微交互动效:从风铃线条到粒子系统

从零实现 Canvas 动画系统,涵盖贝塞尔曲线、鼠标交互、粒子物理等核心技术,附带完整代码示例。

Cheeeliy2026/5/58 分钟阅读

前言

在追求用户体验的今天,微交互已经不再是"可有可无"的装饰——它们是产品性格的一部分。当用户的鼠标划过页面,看到线条轻轻摇曳、粒子缓缓漂浮,这种细腻的反馈会让整个网站"活"起来。

本文将分享我在个人博客中实现 Canvas 微交互动效的完整思路与技术细节。

为什么选择 Canvas?

相比 CSS Animation 和 SVG,Canvas 在以下场景更具优势:

  • 大量粒子:数百个独立元素同时运动,DOM 方案会卡顿
  • 自由绘制:贝塞尔曲线、自定义图形不受 DOM 盒模型限制
  • 高帧率:配合 requestAnimationFrame 实现丝滑 60fps

核心架构:useCanvas Hook

export type DrawCallback = (
  ctx: CanvasRenderingContext2D,
  dt: number,        // 帧间隔(秒)
  dims: CanvasDimensions,
) => void;

export function useCanvas(
  canvasRef: RefObject<HTMLCanvasElement>,
  draw: DrawCallback,
) {
  // 处理 DPR 缩放
  // ResizeObserver 自适应
  // Visibility API 页面隐藏时暂停
  // rAF 循环 + dt 时间增量
}

将"画布管理"和"绘制逻辑"分离是关键设计。draw 回调只需关心"画什么",不用管缩放、循环、暂停等基础设施。

实战:风铃线条

风铃线条的核心是 二次贝塞尔曲线 + 鼠标推力

// 每条线从顶部锚点垂下
const anchorX = line.anchorX * width;
const endX = anchorX + swayOffset;
const endY = line.endY * height;

// 控制点决定曲线弯曲程度
const cpX = anchorX + line.cpOffsetX * width + swayOffset * 0.7;
const cpY = line.cpOffsetY * height;

ctx.quadraticCurveTo(cpX, cpY, endX, endY);

鼠标交互

当鼠标靠近某条线时,计算推力方向和强度:

const dist = Math.abs(mouseX - anchorX);
const maxDist = width * 0.3;
if (dist < maxDist) {
  const strength = 1 - dist / maxDist;
  mouseInfluence = direction * strength * 20;
}

配合弹性阻尼实现平滑过渡:

line.currentSwayX += (target - line.currentSwayX) * Math.min(1, dt * 3);

空闲摇摆

没有鼠标交互时,用正弦波模拟微风:

const idleSway = Math.sin(time * line.idleFreq + line.phaseOffset) * line.idleAmp;

每条线的 phaseOffsetidleFreq 不同,形成自然的错落感。

实战:萤火虫粒子

萤火虫系统更简单——每个粒子是一个发光的圆:

// 呼吸效果:alpha 周期性变化
const alpha = 0.3 + 0.5 * Math.sin(time * particle.pulseFreq + particle.phase);

// 渐变发光
const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius * 3);
gradient.addColorStop(0, `rgba(${rgb}, ${alpha})`);
gradient.addColorStop(1, `rgba(${rgb}, 0)`);

粒子运动用简单的漂移 + 随机扰动:

particle.x += particle.vx * dt;
particle.y += particle.vy * dt;
// 边界反弹
if (particle.x < 0 || particle.x > width) particle.vx *= -1;

性能优化要点

  1. 避免每帧 GC:预分配数组,不在 draw 里 new 对象
  2. DPR 处理canvas.width = displayWidth * dpr,然后 ctx.scale(dpr, dpr)
  3. Visibility API:页面不可见时暂停 rAF
  4. 条件渲染prefers-reduced-motion 媒体查询时跳过动画
  5. dt 限制Math.min(dt, 0.1) 防止切tab后的帧暴涨

最终效果

将多个系统组合:风铃线条作为主背景,萤火虫漂浮其间,再加上秋叶飘落——三层叠加形成丰富而不喧宾夺主的视觉层次。

// HeroCanvasLayer 中组合多个系统
windChimes.draw(ctx, dt, dims);    // 底层:线条
fireflies.draw(ctx, dt, dims);     // 中层:萤火虫
fallingLeaves.draw(ctx, dt, dims); // 顶层:落叶

总结

Canvas 微交互的核心哲学是 克制

  • 透明度控制在 0.08~0.15,不抢内容的视觉优先级
  • 运动幅度小而缓,模拟自然界的微风和浮动
  • 响应鼠标但不跟踪鼠标,保持"背景感"

希望这篇文章能给你一些灵感。完整代码可以在我的 GitHub 找到。