技术#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;每条线的 phaseOffset 和 idleFreq 不同,形成自然的错落感。
实战:萤火虫粒子
萤火虫系统更简单——每个粒子是一个发光的圆:
// 呼吸效果: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;性能优化要点
- 避免每帧 GC:预分配数组,不在 draw 里
new对象 - DPR 处理:
canvas.width = displayWidth * dpr,然后ctx.scale(dpr, dpr) - Visibility API:页面不可见时暂停 rAF
- 条件渲染:
prefers-reduced-motion媒体查询时跳过动画 - 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 找到。