纯 Canvas 打造 Siri 同款边缘流动波浪光晕

12 阅读3分钟

前言

最近在逛苹果官网时被 Siri 的波浪动画深深吸引,决定用纯 Canvas 实现一个类似的效果。经过多次迭代,终于完成了现在这个「边缘流动波浪光晕」效果,今天来分享一下实现思路。

效果展示

  • 粒子沿屏幕四边循环流动
  • 波浪效果通过粒子大小变化呈现
  • 多层发光营造柔和光晕
  • 渐变色沿路径流动
  • 支持鼠标交互和点击特效

核心原理

1. 路径设计

function getPosition(progress) {
    const w = canvas.width;
    const h = canvas.height;
    const pathLength = 2 * (w + h);  // 总
    路径长度
    
    const distance = progress * 
    pathLength;
    const top = w;
    const right = top + h;
    const bottom = right + w;
    
    // 根据距离判断当前在哪条边上
    if (distance < top) {
        return { x: distance, y: 0, edge: 
        0 };      // 顶边
    } else if (distance < right) {
        return { x: w, y: distance - top, 
        edge: 1 }; // 右边
    } else if (distance < bottom) {
        return { x: w - (distance - 
        right), y: h, edge: 2 }; // 底边
    } else {
        return { x: 0, y: h - (distance - 
        bottom), edge: 3 }; // 左边
    }
}

2. 波浪效果

波浪效果的核心是通过正弦函数控制粒子大小:

function getWaveRadius(progress, 
baseRadius) {
    const wave = Math.sin(
        progress * Math.PI * 2 * 
        waveFrequency - time * waveSpeed
    );
    // 将 -1~1 映射到 0~1
    const normalizedWave = (wave + 1) / 2;
    return baseRadius + normalizedWave * 
    (maxRadius - baseRadius);
}

3. 多层发光

为了营造柔和的光晕效果,我们使用多层径向渐变叠加:

function drawGlow(x, y, radius, 
colorProgress) {
    const layers = 5;
    
    for (let i = layers; i >= 0; i--) {
        const layerRadius = radius * (1 + 
        i * 1.2);
        const alpha = 0.2 * (1 - i / 
        (layers + 1));
        
        const gradient = ctx.
        createRadialGradient(x, y, 0, x, 
        y, layerRadius);
        gradient.addColorStop(0, getColor
        (colorProgress, alpha * 2));
        gradient.addColorStop(0.5, 
        getColor(colorProgress, alpha));
        gradient.addColorStop(1, getColor
        (colorProgress, 0));
        
        ctx.beginPath();
        ctx.arc(x, y, layerRadius, 0, 
        Math.PI * 2);
        ctx.fillStyle = gradient;
        ctx.fill();
    }
}

4. 渐变色方案

颜色沿着路径流动,这里使用了 6 个颜色节点的渐变:

const colorStops = [
    { r: 0, g: 220, b: 160 },    // 青色
    { r: 255, g: 180, b: 50 },   // 橙色
    { r: 255, g: 100, b: 80 },   // 珊瑚红
    { r: 80, g: 200, b: 255 },   // 天蓝
    { r: 160, g: 255, b: 100 },  // 嫩绿
    { r: 0, g: 220, b: 160 }     // 回到青
    色
];

5. 波浪连线

为了让波浪更具整体感,相邻粒子之间用贝塞尔曲线连接:

function drawWaveLine() {
    const edges = [[], [], [], []];
    
    particles.forEach(p => {
        const pos = getPosition(p.
        progress);
        const radius = getWaveRadius(p.
        progress, p.baseRadius);
        edges[pos.edge].push({ x: pos.x, 
        y: pos.y, radius, progress: p.
        progress });
    });
    
    edges.forEach((edge, edgeIndex) => {
        ctx.beginPath();
        for (let i = 0; i < edge.length; i
        ++) {
            const p = edge[i];
            const next = edge[(i + 1) % 
            edge.length];
            const offset = getEdgeOffset
            (edgeIndex, p.radius * 0.8);
            const nextOffset = 
            getEdgeOffset(edgeIndex, next.
            radius * 0.8);
            
            if (i === 0) {
                ctx.moveTo(p.x + offset.
                x, p.y + offset.y);
            }
            
            // 贝塞尔曲线连接
            const cpX = (p.x + next.x) / 
            2 + (offset.x + nextOffset.
            x) / 2;
            const cpY = (p.y + next.y) / 
            2 + (offset.y + nextOffset.
            y) / 2;
            ctx.quadraticCurveTo(cpX, 
            cpY, next.x + nextOffset.x, 
            next.y + nextOffset.y);
        }
        ctx.stroke();
    });
}

6. 鼠标交互

点击时产生额外的波峰效果:

canvas.addEventListener('click', function
(e) {
    const clickProgress = 
    getProgressFromPosition(e.clientX, e.
    clientY);
    
    particles.forEach(p => {
        const diff = Math.abs(p.progress 
        - clickProgress);
        const nearestDiff = Math.min
        (diff, 1 - diff);
        
        if (nearestDiff < 0.1) {
            // 增加附近粒子的大小
            p.baseRadius = baseRadius + 
            15 * (1 - nearestDiff / 0.1);
        }
    });
});

性能优化

  1. 粒子数量控制 :100 个粒子足够流畅
  2. 模糊叠加代替滤镜 :多层半透明叠加比 CSS filter 性能更好
  3. 增量渲染 :使用半透明 fillRect 产生拖尾效果,而非每帧重绘

配置参数

const config = {
    particleCount: 100,    // 粒子数量
    baseRadius: 5,          // 基础半径
    maxRadius: 10,          // 最大半径(波
    峰)
    flowSpeed: 0.0006,      // 流动速度
    waveSpeed: 1.8,         // 波浪速度
    waveFrequency: 4,       // 波浪频率
    glowLayers: 5           // 发光层数
};

完整代码

完整源码已放在文章开头,复制即可运行。

写在最后

这个效果看似复杂,但拆解后无非是:

  • 路径设计
  • 粒子系统
  • 渐变着色
  • 贝塞尔曲线 掌握这几个核心概念,你也能做出炫酷的动画效果。如果有任何问题,欢迎在评论区交流!