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

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

2026-05-05 02:00:008 分钟阅读

前言


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


本文将分享我在个人博客中实现 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` 对象
  • 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 找到。