前言
最近在逛苹果官网时被 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);
}
});
});
性能优化
- 粒子数量控制 :100 个粒子足够流畅
- 模糊叠加代替滤镜 :多层半透明叠加比 CSS filter 性能更好
- 增量渲染 :使用半透明 fillRect 产生拖尾效果,而非每帧重绘
配置参数
const config = {
particleCount: 100, // 粒子数量
baseRadius: 5, // 基础半径
maxRadius: 10, // 最大半径(波
峰)
flowSpeed: 0.0006, // 流动速度
waveSpeed: 1.8, // 波浪速度
waveFrequency: 4, // 波浪频率
glowLayers: 5 // 发光层数
};
完整代码
完整源码已放在文章开头,复制即可运行。
写在最后
这个效果看似复杂,但拆解后无非是:
- 路径设计
- 粒子系统
- 渐变着色
- 贝塞尔曲线 掌握这几个核心概念,你也能做出炫酷的动画效果。如果有任何问题,欢迎在评论区交流!