用Canvas粒子系统打造点击爆炸动效:轻量、可定制、高性能
2026/9/10 20:17:34 网站建设 项目流程

“Ready, Set, BANG 💘💥”这个标题,第一眼看上去像是游戏里的开火指令,或者是某个线上发布流程的代号。但如果你是一名经常写页面交互的前端开发者,你可能会心一笑:这不就是“点击触发 → 动画播放 → 效果爆发”的三段式节奏吗?把一个点击动作变成一次让人记住的视觉反馈,正是很多网页在“仪式感”上欠缺的部分。

我见过太多团队做这类特效时走两条弯路:一是直接贴一张 GIF 素材,体积大、清晰度差、风格难统一,换个背景色就露馅;二是用 CSS 死磕粒子飞散,写几十个 keyframes,结果效果生硬,连挤压感都做不出来。真正能在一两天内搞定、且效果能打的方案,反而是一个看起来有点“底层”的技术:Canvas 粒子系统。

这篇文章不会只给模板代码。我会从“为什么推荐用 Canvas 做爆炸类动效”讲起,把粒子系统的核心概念拆明白,然后带你从零写一个可复用的“心动爆炸”交互动画。点按页面任意位置,会炸开一颗由红色心形粒子构成的“爱心爆弹”,粒子飞散后会受重力影响回落,像一场浪漫的微型烟花。整个过程不依赖任何框架,纯 HTML + CSS + JavaScript 就能跑通,非常适合直接嵌入个人主页、节日活动页或前端作品集。

1. 这篇文章真正要解决的问题

先问你一个问题:当你需要在页面上实现一个“爆炸”类动效时,第一反应是什么?是去找一个现成的 JS 动画库,还是用 CSS 硬写,或者直接下载一张动图?实际情况是,这三条路都不够理想。

动画库的问题在于“杀鸡用牛刀”。像 GSAP、Framer Motion 这类库很强大,但如果你只需要一个几十毫秒的粒子飞出效果,引入一整库的成本太高,尤其是在移动端环境;CSS 动效的问题在于“算不出物理规律”。爆炸类效果的精髓是粒子的随机方向、速度衰减、重力影响,CSS 动画不是不能做,但它的关键帧是离散的,一旦粒子数量上到几十颗,代码就急剧膨胀,性能也开始吃紧;GIF 素材就更不用说了,它是一帧一帧的位图序列,放大会糊、体积会胀、无法与页面背景交互。

所以,这篇文章要解决的痛点很具体:如何用最轻量的原生技术栈,实现一个可定制、可交互、性能可控的爆炸型动效。读完你会得到三样东西:一套理解粒子动画的心智模型、一段能直接运行的完整代码、一份能在真实项目里落地时避开常见坑的清单。

如果你正在做以下类型的工作,这篇文章最值得读:

  • 为自己的个人主页或作品集加一个让人印象深刻的点击反馈;
  • 在公司活动页、节日专题页里做“点击送礼”“甜蜜告白”类互动;
  • 在移动端 H5 中做轻量级特效,但又不想引入重量级动画库;
  • 想深入理解 Canvas 2D 动画与 requestAnimationFrame 配合的开发模式。

2. 核心概念:粒子系统与 Canvas 2D

在写代码之前,先把几个绕不开的概念讲明白。这节不会铺开讲图形学,只讲你马上要用的那一部分。

2.1 粒子系统不是“一个东西”,而是一套流程

“粒子系统”这个名字听起来高大上,其实它本质上是一套循环流程。你只需要记住四个环节:发射、更新、渲染、销毁。

  • 发射:在某个位置生成一批粒子,给每个粒子一个初始位置、速度、颜色、大小、存活时间;
  • 更新:每一帧告诉所有粒子“你现在该移动到哪、速度衰减多少、受不受重力影响”;
  • 渲染:在画布上画出每个粒子的当前位置;
  • 销毁:某个粒子存活时间到了,或者飞出可视区域太远,就把它从数组里移除。

把这个流程对应到“心动爆炸”里,就是:手指点击的位置是爆点,爆点一次性生成 80 个心形粒子,它们以随机角度向外飞散;飞行过程中逐渐减速,同时受重力影响向下坠落;粒子淡出之后被清理出数组。整个过程每秒钟执行约 60 次,人眼看起来就是一场顺畅的爆炸。

2.2 Canvas 2D 的两种绘制方式

Canvas 2D 把粒子画到屏幕上有两条路:

  • 路径绘制:用beginPath()+moveTo()+lineTo()+fill()画矢量图形,优点是边缘清晰,缺点是大批量绘制时路径开销高;
  • 位图绘制:用drawImage()把一张预先准备的小图贴到画布上,优点是速度极快,缺点是快速缩放时可能轻微失真。

对于心形粒子,最稳妥的做法是“先离屏绘制一张心形小图,再通过 drawImage 批量贴图”。这样一来,心形的路径只计算一次,之后每帧只是做坐标偏移和透明度变化,性能远远优于每帧重新计算 Bezier 曲线。

2.3 requestAnimationFrame 与 setInterval 的差别

很多新手做动画时会写setInterval(() => {}, 16),想当然以为这就是 60fps。但实际上 setInterval 无法与显示器刷新率同步,容易掉帧、卡顿,甚至出现残影。

requestAnimationFrame是浏览器专门为动画提供的 API,它允许浏览器在下一次重绘前调用你的更新函数。这样做有几个直接好处:

  • 自动与屏幕刷新率同步,通常是 60fps,高刷屏上还能更高;
  • 页面切到后台时自动暂停,省电省性能;
  • 不用猜“该延迟多少毫秒”,浏览器会告诉你什么时候可以画。
对比项setIntervalrequestAnimationFrame
与刷新率同步不同步,可能跳帧自动同步
页面隐藏时继续执行,浪费资源自动暂停
时间精度受事件循环阻塞影响以系统时间戳为基准
适合场景定时轮询、非视觉任务动画、滚动特效、Canvas 绘制

2.4 重力与阻尼:让粒子看起来“真实”

爆炸粒子如果直直向外飞,看起来像礼花刚射出来,缺少“火药味”。真实世界里的爆炸碎片会受两个因素影响:初速度带来的惯性,以及空气阻力/引力带来的减速与回落。

在代码里,这两个因素可以用两个变量表达:

  • 阻尼(damping):每帧把速度乘以一个小于 1 的系数,例如 0.98,模拟空气阻力;
  • 重力(gravity):每帧给速度的 y 分量增加一个固定值,例如 0.15,模拟重物向下拉。

只要加上这两行,效果就会从“机械匀速直线”变成“爆炸后舒缓散开再落地”,感官差异非常明显。这也是为什么很多人照着网上的粒子代码抄,却总是觉得效果“飘”,多半是没加重力。

3. 环境准备与前置条件

这个项目不需要安装任何依赖,也不需要配置构建工具。你只需要三样东西:

  • 一个现代浏览器,推荐 Chrome、Edge 或 Firefox,用于运行和调试;
  • 一个文本编辑器,VS Code、Sublime Text、记事本都行;
  • 一个能开启本地静态服务的工具。如果不想装工具,直接双击 index.html 也能跑。

如果你希望保存代码后自动刷新,可以安装 VS Code 的 Live Server 插件,或者使用 Node 环境下的npx serve。但这些都是可选项,不影响核心代码运行。

以下是本文使用的运行环境:

  • HTML5 + CSS3 基本语法;
  • Canvas 2D API,所有主流浏览器均支持;
  • 原生 JavaScript,建议使用支持 ES6+ 的浏览器环境;
  • 没有使用 TypeScript,因为示例希望足够精简,方便你快速读懂并移植。

整个项目的文件结构非常简单,总共三个文件:

heart-bang/ ├── index.html ├── style.css └── main.js

如果你是在已有项目中嵌入这段代码,也可以只拿main.js中的核心逻辑,配合你自己的容器元素来用。

4. 核心流程拆解:从“点击”到“爆炸”

在给出完整代码前,我们先拆清楚整个实现的工作流。很多人写动画代码喜欢想到哪写到哪,结果设置状态、更新逻辑、渲染逻辑全混在一起,最后调起来非常痛苦。这个项目虽然小,但我建议你把逻辑分成四层,一层管一件事。

4.1 点击与事件绑定

页面上会有一层全屏的 Canvas 画布,点击事件绑定在canvas上。监听click事件,拿到鼠标相对画布的坐标,把这个坐标作为爆点(爆发中心)。

这里有一个很容易踩坑的细节:如果你用clientXclientY直接作为画布上的坐标,在页面发生滚动,或者 Canvas 尺寸不等于视口尺寸时,爆点会错位。正确的做法是使用getBoundingClientRect()把点击坐标换算成画布内部坐标:

const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top;

4.2 粒子的数据结构

一个粒子并不是一个“类”,而是一个普通对象。每帧循环只需要读取和更新这些字段:

  • xy:粒子当前位置;
  • vxvy:粒子当前速度,单位是像素/帧;
  • size:粒子绘制尺寸,心形小图的宽高;
  • rotation:粒子的旋转角度;
  • alpha:粒子透明度,用于淡出;
  • decay:每帧透明度衰减量;
  • life:剩余存活帧数,或用一个布尔值表示“是否还需要渲染”。

把粒子设计成普通对象而不是实例化类,是因为在批量场景下,普通对象的创建和回收速度更快,适合高频创建与销毁的场景。如果你喜欢用 class,当然也可以,但在性能敏感场景里,批量对象池才是更优解。

4.3 粒子更新:物理模拟

每帧对每个粒子做以下更新:

  1. 位置加上速度:x += vx; y += vy;
  2. 速度乘以阻尼:vx *= damping; vy *= damping;
  3. 速度叠加重力:vy += gravity;
  4. 透明度衰减:alpha -= decay;
  5. 旋转速度(如果有):rotation += rotationSpeed;

当粒子的透明度小于等于 0,或者粒子飞出画布边界太远,就标记为“死亡”。注意,“死亡”不等于马上从数组中删除,而是可以用一个alive属性标记,后续统一过滤。

4.4 渲染:绘制心形粒子

渲染分两步:

第一步,初始化时用离屏 canvas 画出一张心形小图,比如heartImage对象; 第二步,每帧对每个存活粒子调用ctx.save()ctx.globalAlpha = particle.alphactx.translate()ctx.rotate()ctx.drawImage()ctx.restore()

save()restore()的作用是隔离画布状态,避免一个粒子的旋转角度或透明度影响下一个粒子。如果你忘了调用,就会出现“所有粒子都跟着上一个粒子的旋转角跑”的奇怪问题。

4.5 动画循环:启动与停止

动画不是无限常驻的。页面初始化时不启动循环,等用户第一次点击时启动。当粒子数组为空时,停止循环,避免后台空转消耗 CPU。这里我用一个animationId变量记录当前帧的 ID,通过cancelAnimationFrame停止。

5. 完整示例与代码实现

下面给出三个文件。先看 HTML 骨架,然后是样式,最后是核心 JavaScript 逻辑。代码里我会写大量注释,方便直接跑通后对照理解。

5.1 index.html

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Ready, Set, BANG - 心动爆炸动画</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="page-wrapper"> <h1 class="page-title">Ready, Set, BANG</h1> <p class="page-tip">点一下页面,看心动爆炸效果</p> <canvas id="bangCanvas"></canvas> </div> <script src="main.js"></script> </body> </html>

5.2 style.css

/* 文件路径:style.css */ html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; font-family: "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(160deg, #1b1035 0%, #321f5c 100%); } .page-wrapper { position: relative; width: 100vw; height: 100vh; overflow: hidden; } .page-title { position: absolute; top: 12%; left: 50%; transform: translateX(-50%); margin: 0; color: #fff; font-size: 48px; font-weight: 700; letter-spacing: 4px; text-shadow: 0 0 18px rgba(255, 105, 180, 0.6); user-select: none; pointer-events: none; z-index: 2; } .page-tip { position: absolute; top: 22%; left: 50%; transform: translateX(-50%); margin: 0; color: rgba(255, 255, 255, 0.65); font-size: 16px; letter-spacing: 2px; user-select: none; pointer-events: none; z-index: 2; } #bangCanvas { position: absolute; inset: 0; width: 100%; height: 100%; cursor: pointer; z-index: 1; }

5.3 main.js

// 文件路径:main.js (() => { const canvas = document.getElementById("bangCanvas"); const ctx = canvas.getContext("2d"); let width = 0; let height = 0; const dpr = Math.min(window.devicePixelRatio || 1, 2); // 粒子数组 let particles = []; let animationId = null; // 物理参数,你可以按喜好调整 const PARTICLE_COUNT = 80; // 每次爆炸粒子数量 const DAMPING = 0.98; // 阻尼,模拟空气阻力 const GRAVITY = 0.15; // 重力值,越大粒子下落越快 const MAX_ALPHA = 0.95; // 最大透明度 const ALPHA_DECAY = 0.012; // 每帧透明度衰减 // 离屏画布:预绘制一张心形小图 const heartCanvas = document.createElement("canvas"); const heartSize = 24; heartCanvas.width = heartSize; heartCanvas.height = heartSize; const heartCtx = heartCanvas.getContext("2d"); drawHeart(heartCtx, heartSize / 2, heartSize / 2, heartSize / 2 - 2); // 画心形:使用贝塞尔曲线,绘制一个实心心形 function drawHeart(context, x, y, size) { context.save(); context.translate(x, y); context.scale(size / 12, size / 12); context.beginPath(); context.moveTo(0, 4); context.bezierCurveTo(0, 0, -4, -2, -4, -6); context.bezierCurveTo(-4, -10, 0, -10, 0, -6); context.bezierCurveTo(0, -10, 4, -10, 4, -6); context.bezierCurveTo(4, -2, 0, 0, 0, 4); context.closePath(); context.fillStyle = "#ff4d6d"; context.fill(); context.restore(); } // 核心:生成一个粒子 function createParticle(x, y) { const angle = Math.random() * Math.PI * 2; const speed = 2 + Math.random() * 7; return { x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 1.5, rotation: Math.random() * Math.PI * 2, rotationSpeed: (Math.random() - 0.5) * 0.2, size: 12 + Math.random() * 16, alpha: MAX_ALPHA, alive: true, }; } // 在一次点击的爆点位置生成一批粒子 function explode(x, y) { for (let i = 0; i < PARTICLE_COUNT; i++) { particles.push(createParticle(x, y)); } if (animationId === null) { animationLoop(); } } // 更新所有粒子的位置、速度、透明度 function updateParticles() { for (let i = 0; i < particles.length; i++) { const p = particles[i]; if (!p.alive) continue; // 位置更新 p.x += p.vx; p.y += p.vy; // 速度更新:阻尼 + 重力 p.vx *= DAMPING; p.vy *= DAMPING; p.vy += GRAVITY; // 旋转更新 p.rotation += p.rotationSpeed; // 透明度衰减 p.alpha -= ALPHA_DECAY; // 判断是否死亡 if ( p.alpha <= 0 || p.x < -30 || p.x > width + 30 || p.y < -30 || p.y > height + 30 ) { p.alive = false; } } // 过滤掉已经死亡的粒子 particles = particles.filter((p) => p.alive); } // 绘制所有存活粒子 function drawParticles() { ctx.clearRect(0, 0, width, height); for (let i = 0; i < particles.length; i++) { const p = particles[i]; if (!p.alive) continue; ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rotation); // 通过透明度渐隐实现“爆炸后消失”的效果 ctx.globalAlpha = Math.max(0, p.alpha); // 以图片中心为原点绘制 ctx.drawImage( heartCanvas, -p.size / 2, -p.size / 2, p.size, p.size ); ctx.restore(); } } // 动画主循环 function animationLoop() { updateParticles(); drawParticles(); if (particles.length > 0) { animationId = requestAnimationFrame(animationLoop); } else { animationId = null; ctx.clearRect(0, 0, width, height); } } // 调整画布大小,适配高分屏 function resizeCanvas() { width = window.innerWidth; height = window.innerHeight; canvas.width = width * dpr; canvas.height = height * dpr; canvas.style.width = width + "px"; canvas.style.height = height + "px"; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } // 点击事件:把鼠标坐标换算成画布内部坐标 function handleClick(e) { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; explode(x, y); } // 初始化 window.addEventListener("resize", resizeCanvas); canvas.addEventListener("click", handleClick); resizeCanvas(); })();

这段代码的完整逻辑是关键。你可以直接把三个文件放到同一个目录下,双击 index.html,在浏览器里点击页面任意位置,就能看到心形粒子从爆点向四周炸开,再在重力作用下坠落的视觉效果。

6. 运行结果与效果验证

启动方式非常简单:

  • 直接双击 index.html,用 Chrome 或 Edge 打开;
  • 或者用 VS Code 的 Live Server 插件启动一个本地服务,在浏览器中访问http://127.0.0.1:5500

正常效果描述如下:页面背景是深紫色渐变,居中显示“Ready, Set, BANG”标题;你点击任意位置,会从点击点炸出约 80 个粉红色心形粒子,粒子先向四周飞散,之后速度逐渐减慢并向下飘落,同时透明度递减,几秒钟内完全消失。每点一次,都会重复这个过程,多点几次会形成叠加的爆炸效果。

如果你没有看到效果,不要急着改代码。按以下顺序检查:

  1. 打开浏览器开发者工具(F12),进入 Console 面板,看有没有报错信息。最常见的是document.getElementById("bangCanvas")返回 null,说明 Canvas 元素没找到,或脚本加载顺序不对;
  2. 观察 Network 面板,确认main.jsstyle.css都成功加载;
  3. 在 Console 里手动执行document.querySelector('canvas'),确认对象存在。

判断性能是否合格的标准是:连续点击 5 次以上,动画依然流畅,没有明显的卡顿或掉帧。如果你的设备比较旧,还能接受,但如果你发现点击后动画明显变卡,可以尝试把PARTICLE_COUNT从 80 降到 50,或者把dpr的最大值从 2 改成 1.5。

如果你希望验证自己的代码不是只在这一台设备上能用,还可以顺手用 Chrome DevTools 的设备模拟模式测试移动端表现。注意,移动端点击会触发touch事件而不是click事件,在真机上直接用该代码可能不会响应手指点击。真机测试时,需要把事件绑定改为同时绑定clicktouchstart,并做一次去重处理。

7. 常见问题与排查思路

写这类动画最怕的不是代码看不懂,而是效果出来但不知道哪里出了问题。下面这份排查表是我觉得最常见的几种情况。

问题现象可能原因排查方式解决方案
点击后没反应Canvas 元素没找到,或脚本顺序错误Console 看报错,Elements 面板检查 canvas 是否存在把 script 放到 body 底部,或使用 DOMContentLoaded
心形显示成方块离屏心形绘制失败,或缩放比例不对单独绘制 heartCanvas 并输出到临时图片检查 drawHeart 的贝塞尔曲线参数
动画卡顿粒子数量过多,或高分屏渲染压力大Performance 面板采样记录降低粒子数量,限制 dpr 最大为 2
爆炸位置偏移未使用 getBoundingClientRect 换算坐标点击边缘位置观察爆点用 clientX - rect.left 和 clientY - rect.top
粒子一直不消失透明度衰减设置为 0,或过滤逻辑有误Console 打印 particles 数组长度确保 ALPHA_DECAY 大于 0,并使用 filter 清理
Canvas 模糊未按 devicePixelRatio 放大画布检查 canvas.width 与画布 CSS 宽度关系resizeCanvas 中设置画布物理尺寸并用 setTransform 缩放
移动端点击无效click 事件在部分设备的响应有延迟或被拦截DevTools 开启移动模拟,触发点击改为 click 与 touchstart 双绑定,并做防重复处理

这里单独说一下最容易被忽略的一条:Canvas 的分辨率适配。Canvas 的 CSS 宽高和画布物理宽高是两套系统,如果你只设置了canvas.style.width = "100%",而不设置canvas.width,画布默认物理分辨率是 300×150,在高分屏上会被放大,看起来就是糊的。因此,代码里resizeCanvas同时处理了物理尺寸和 CSS 尺寸,并用setTransform把坐标系统一为 CSS 像素,这是保证清晰度的关键。

移动端事件是另一个高频问题。很多人在 PC 上写完直接打包上线,结果用户手机上点了没反应,原因可能是移动端对click有 300ms 延迟,或者元素被其他层遮挡。更稳妥的绑定方式是同时监听两种事件:

canvas.addEventListener("click", handleClick); canvas.addEventListener("touchstart", handleClick, { passive: true });

passive: true可以告诉浏览器不需要调用preventDefault,这有助于减少滚动过程中的卡顿。如果你同时绑定了两个事件,需要在上层做一次简单的去重,防止一次手指点击触发两次爆炸。

8. 最佳实践与工程建议

小项目跑通之后,如果你想把这个效果真正用进公司项目、作品集或开源仓库,下面这几点值得认真考虑。

8.1 把效果封装成独立模块

不要直接把所有代码贴在页面底部。建议把粒子逻辑封装成独立的类或工厂函数,暴露一个fire(x, y)方法。这样,调用方不需要关心内部状态,而且你可以方便地在多个页面复用。

下面的代码展示了一个最小封装思路:

export class HeartBoom { constructor(canvas) { this.canvas = canvas; this.ctx = canvas.getContext("2d"); this.particles = []; this.animationId = null; } fire(x, y) { // 内部生成粒子并启动动画 } destroy() { // 取消动画循环并清理资源 } }

8.2 加入音量反馈与场景音频

如果这个动画用在活动页上,可以配合 Web Audio API 生成一个短促的“砰”声。注意,音频需要由用户手势触发,浏览器才允许播放,所以把AudioContext.resume()放在点击事件处理函数里最合适。

8.3 控制粒子数量的动态调配

在移动端弱机型和桌面端旗舰机上的渲染压力完全不同。更工程化的做法是根据设备能力动态调整粒子数量,比如检测屏幕宽度和devicePixelRatio,小屏设备自动把粒子数量减半。或者用requestAnimationFrame的时间戳计算实际帧率,当帧率低于某个阈值时降低粒子增量。

8.4 使用对象池减少内存波动

本示例中粒子数量少,过滤数组的写法完全没问题。但如果只是扩展到上千粒子,频繁创建、销毁对象会导致 GC 频繁触发,表现为动画偶尔停顿。更进阶的做法是预分配一个粒子对象池,死亡粒子不删除,而是重置状态后复用。对象池的代码会增加不少复杂度,建议在确认性能问题确实存在后再引入。

8.5 设计上考虑背景与色调可配置

粒子颜色、背景渐变、心形大小在不同场景里应该可配置。你可以把颜色提取到配置文件里,或者直接通过 CSS 变量切换主题。这样,后续做情人节、妇女节、店庆等不同活动时,只需要改几个参数,不用动业务代码。

8.6 关于无障碍与用户偏好

动画类效果要尊重用户的“减少动态效果”系统设置。你可以通过window.matchMedia("(prefers-reduced-motion: reduce)")检测用户是否开启了“减弱动态效果”,如果开启了,就直接跳过爆炸动画,或者只显示一个静态心形标记。这个细节虽然小,但在无障碍审查中非常重要,也体现了前端开发的专业度。

8.7 测试与构建

如果你是通过构建工具引入,记得把main.js打包进项目。如果使用 Vite、Webpack,这段代码不需要任何第三方依赖,直接作为模块引入即可。在 TypeScript 项目中使用时,建议给粒子的属性定义 Interface,避免使用any。如果后续有 Canvas 2D 类型的强校验需求,可以引入@types/node中提供的 NodeJS 类型声明,或者直接依赖 DOM 库内置的 TS 类型。

9. 总结与后续学习方向

“Ready, Set, BANG 💘💥”并不是一个复杂到需要用大型动画库才能实现的效果。它背后真正值得掌握的,是“粒子系统 + Canvas 2D + requestAnimationFrame”这套组合拳。这三个概念在任何用 Canvas 做可视化、游戏特效、数据动效的项目里都会反复出现。把本文的示例代码抄一遍、跑一遍、调一遍参数,你对前端动画的理解会比刷一百个 CSS 动画教程更扎实。

下一步,我建议你做三个小实验来加深理解:

  1. 把心形换成五角星或圆形,观察不同图形对爆发感的影响;
  2. 把爆炸改为“连续发射”,让粒子在鼠标移动路径上持续生成,体会从“一次点击”到“实时流式”的变化;
  3. 把 Canvas 2D 换成 WebGL 或 CSS 3D Transform,对比同样的粒子数量在不同渲染方案下的性能差异。

如果你打算把这个动效用于某个上线项目,记得在真实设备上测一遍移动端效果,特别是触摸事件的绑定和高分屏适配。这篇文章的代码可以作为你的起点,但真正好用的动效库,从来都是建立在你自己调试过的细节之上的。建议先收藏这篇,周末花一小时跑一遍整个流程,再改成你想要的样子。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询