气泡背景特效(Canvas 气泡上升动画,可点击戳破)
Canvas 气泡背景特效:半透明气泡从底部升起并左右摇曳,点击可戳破并扩散消失,数量、速度、大小实时可调,已适配高分屏,复制即用。
效果演示(真实可交互)
气泡从底部缓缓升起,边上升边按正弦轨迹左右摇曳,模拟水中浮力与流体的扰动。直接点击画面中的气泡会触发「戳破」动画:气泡位置扩散出一个逐渐变淡的圆环,同时底部立即补上一颗新气泡,保证画面密度恒定。演示面板可实时调整气泡数量(10-80)、上升速度(1-10)和最大半径(10-40px)。每个气泡带相位偏移和独立的摇曳幅度,因此即使数量很多也不会出现整齐划一的”僵尸队列”感。整个实现只依赖一个 canvas 与 requestAnimationFrame 循环,无任何第三方库。
完整代码
<canvas id="bubble" style="width:100%;height:340px;display:block;background:linear-gradient(180deg,#0e1a3a,#0b2a4a);"></canvas>
const canvas = document.getElementById('bubble');
const ctx = canvas.getContext('2d');
let W, H, bubbles = [], popping = [];
const cfg = { count: 30, speed: 4, maxR: 22 };
function resize() {
const r = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.round(r.width * dpr); // 物理像素
canvas.height = Math.round(r.height * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // 之后一律用 CSS 像素绘图
W = r.width; H = r.height;
}
function spawn() {
return {
x: Math.random() * W, y: H + 30,
r: Math.random() * cfg.maxR + 6,
speed: (Math.random() * .6 + .4) * cfg.speed,
phase: Math.random() * Math.PI * 2, // 摇曳相位
wobble: Math.random() * 20 + 8, // 摇曳幅度
};
}
function init() {
bubbles = [];
for (let i = 0; i < cfg.count; i++) {
const b = spawn(); b.y = Math.random() * H; bubbles.push(b);
}
}
function drawBubble(b, alpha) {
ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(140,190,255,${.08 * alpha})`; ctx.fill();
ctx.strokeStyle = `rgba(170,210,255,${.5 * alpha})`; ctx.lineWidth = 1.2; ctx.stroke();
ctx.beginPath(); ctx.arc(b.x - b.r * .35, b.y - b.r * .35, b.r * .18, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255,255,255,${.7 * alpha})`; ctx.fill(); // 高光
}
function step() {
ctx.clearRect(0, 0, W, H);
for (const b of bubbles) {
b.y -= b.speed; b.phase += .02;
b.x += Math.sin(b.phase) * .8;
if (b.y < -b.r - 20) Object.assign(b, spawn());
drawBubble(b, 1);
}
for (let i = popping.length - 1; i >= 0; i--) { // 戳破扩散环
const p = popping[i];
p.r += p.grow; p.alpha -= .04;
if (p.alpha <= 0) { popping.splice(i, 1); continue; }
ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(180,215,255,${p.alpha * .8})`; ctx.lineWidth = 2; ctx.stroke();
}
requestAnimationFrame(step);
}
resize(); init(); step();
window.addEventListener('resize', () => { resize(); init(); });
// 点击戳破
canvas.addEventListener('click', e => {
const r = canvas.getBoundingClientRect();
const mx = e.clientX - r.left, my = e.clientY - r.top;
for (let i = bubbles.length - 1; i >= 0; i--) {
const b = bubbles[i];
if (Math.hypot(b.x - mx, b.y - my) < b.r + 4) {
popping.push({ x: b.x, y: b.y, r: b.r, grow: 1.2, alpha: 1 });
bubbles.splice(i, 1); bubbles.push(spawn()); break;
}
}
});
参数说明
| 参数 | 位置 | 说明 |
|---|---|---|
| 气泡数量 | cfg.count | 10-80,演示面板实时生效 |
| 上升速度 | cfg.speed | 1-10,越大升得越快 |
| 最大半径 | cfg.maxR | 10-40px,实际半径在其 0-100% 间随机 |
| 摇曳幅度 | b.wobble | 默认 8-28px,越大摆动越明显 |
| 气泡颜色 | rgba(...) 三处 | 填充、描边、高光的色值可整体替换 |
兼容性与性能
- 已做
devicePixelRatio适配:画布位图按物理像素创建,再setTransform(dpr,...)用 CSS 像素绘图,高分屏下气泡边缘清晰不发虚。 - 单次循环只需绘制 count 个气泡 + 少量扩散环,O(n) 复杂度,80 个气泡在低端安卓机也能跑满 60fps。
- 气泡高光用透明填充圆实现,避免
createRadialGradient的高频创建开销。 - 移动端建议数量 ≤ 50,并在页面不可见时(
visibilitychange/IntersectionObserver)暂停 rAF 循环以省电。 - 点击事件监听在 canvas 上,不会阻塞页面滚动;触摸设备同样支持 tap 戳破。
常见问题
Q: 气泡为什么从底部冒出来? 因为初始位置 y = H + 30 在画布下边缘之外,随 y 逐帧减小进入可视区,形成”上浮”观感;底部补位同样复用 spawn()。
Q: 想让气泡更真实一点? 可以给每个气泡叠加一个椭圆形的二次高光,或让半径随高度轻微变化(越靠近水面越大),这些都在 drawBubble 内即可完成。
Q: 气泡背景和 CSS 动画气泡有什么区别? CSS 方案每个气泡都要一个 DOM 节点,几十个就很吃力;Canvas 方案所有气泡共用一个画布,数量翻倍几乎不增加 DOM 开销,适合做全屏背景。
Q: 页面滚动时动画会不会卡? 画布固定尺寸且循环只画自身内容,滚动卡顿通常来自页面其他重排;建议给画布容器加 will-change: transform 或 contain: paint 隔离。
许可与来源
- 许可证:自研原创 —— 粤喵自研原创,可免费用于个人与商业项目,无需署名。查看来源公示与许可清单
- 来源:粤喵团队原创自研(从效果思路到代码实现均为独立完成,未直接复制第三方代码)
- 首次发布:2026-09-01 · 最近更新:2026-09-01
- 修改日志:2026-09-01 首次发布;2026-09-13 补充许可证与来源标注、复制按钮说明与法务页入口
- 纠错:联系页面 / min@cactus.asia,2 个工作日内核实处理
常见问题
这个组件可以商用吗?
可以。本组件为粤喵自研原创,可免费用于个人和商业项目,无需署名。
浏览器兼容性如何?
支持所有现代浏览器(Chrome / Firefox / Safari / Edge 最新版本)。旧版 IE 不支持 CSS 动画相关特性。
怎么修改动画参数?
直接调整 CSS 中的 animation-duration(时长)、animation-timing-function(缓动曲线)等属性即可,代码中有注释说明。