
简介这是一款基于HTML5 Canvas技术实现的粒子爆炸动画特效源码面向前端开发者和网页设计师可用于打造个性文字背景或动态视觉焦点。它通过大量彩色粒子的生成、运动、消散与重组将文字轮廓动态呈现为爆炸般的视觉冲击适合学习Canvas绘图、requestAnimationFrame动画循环与粒子系统设计。压缩包共6个文件大小59KB结构清晰主要包含3个JavaScript文件含EaselJS、PreloadJS等库及核心粒子脚本、1个HTML页面、1个CSS样式表及1个说明文档便于直接运行与拆解学习。已有321人学习适合具备一定前端基础、希望深入掌握Canvas高级效果的读者。资源内含完整可运行示例代码中通过数组管理粒子状态、使用闭包维持属性并实现了粒子随机生成、速度衰减、生命周期控制及文字轮廓吸附等机制可帮助理解从零搭建动画框架的思路也能作为网页动效素材快速复用或二次改造。1. 把 H5 Canvas 粒子爆炸做顺手先从性能焦虑说起近一年接了几个活动页和大屏可视化的活每次提报里都少不了一个「上线级」的粒子爆炸特效。这类效果在短视频平台上一抓一大把看着很炫但真正把它放进 H5 里跑起来帧率、内存、真机适配这些硬指标会立刻给你颜色看。这份 H5 Canvas 粒子爆炸动画特效资源拆开来看就是一套纯 JavaScript 实现的粒子动画方案点击或定时触发成百上千个粒子从一个中心点飞散出去带重力、阻力和渐隐渐缩覆盖了从粒子对象设计、运动方程、渲染循环到性能调优的完整链路。适合刚接触 Canvas、想拿一个练手项目入门的开发者也适合已经在业务里写过简单动画、想看看粒子系统怎么做资源管理和回收的老手。它解决的不是「画几个圆点」的问题而是「粒子多了以后浏览器为什么卡」的问题。2. Canvas 渲染基石先把坐标系、画布缩放和动画循环对齐2.1 高清屏适配为什么canvas.width不能直接等于 CSS 宽度粒子爆炸要放在 H5 页面里第一步其实是把画布坐标系和屏幕物理像素对齐。很多初次接触 Canvas 的开发者会直接写canvas.width canvas.clientWidth在普通 PC 上看起来没毛病但放到 iPhone 或高分屏笔记本上画面会明显发虚。原因很简单CSS 像素和物理像素之间有一个设备像素比DPR的概念Canvas 的内部绘图缓冲区和它占用的 CSS 尺寸尺寸如果不匹配浏览器就得做一次非整数倍的拉伸缩放圆点和线条边缘就会糊。常见的做法是在初始化时读取一下 DPR然后同时设置 Canvas 的缓冲尺寸和 CSS 尺寸function setupCanvas(canvas, cssWidth, cssHeight) { const dpr window.devicePixelRatio || 1; canvas.width cssWidth * dpr; // 缓冲区的物理像素宽度 canvas.height cssHeight * dpr; // 缓冲区的物理像素高度 canvas.style.width cssWidth px; canvas.style.height cssHeight px; const ctx canvas.getContext(2d); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // 把逻辑坐标系压缩到 CSS 像素单位 return ctx; }这段代码的关键在setTransform这一行。如果不调用它后面所有绘图坐标都得手动乘 DPR爆炸粒子的物理计算会变得很绕——你把粒子中心点算成了 100,100但实际上它在缓冲区里的真实位置是 200,200DPR 为 2 时。把变换矩阵直接设置成 DPR 缩放之后整个后续逻辑都用 CSS 像素做单位既直观也不会出错。这里要特别注意的是setTransform必须在拿到的ctx上调用一次而且只在初始化时调一次就够了频繁调用反而会引发额外的矩阵运算开销。为什么这个问题和粒子爆炸相关因为粒子的运动计算是每帧都要更新坐标的如果坐标系不统一调试起来会极度痛苦。你可能在 PC 上调整好了一个爆炸效果放到真机上所有粒子的落点都偏了一半却怎么都找不到原因——十有八九就是 DPR 的锅。2.2 动画循环选型requestAnimationFrame的节奏控制与帧率陷阱Canvas 粒子动画的第二个基础问题是动画循环怎么写。最朴素的做法是setInterval但它在移动端的表现很不稳定浏览器标签页切到后台时定时器会被节流而且定时器无法和屏幕刷新频率保持同步容易出现拖影或跳帧。正确的选择是requestAnimationFrame以下简称 rAF浏览器会保证回调在下次绘制之前执行帧率对齐屏幕刷新率。一个容易得罪性能的写法是让粒子更新的步长直接依赖帧间隔let lastTime 0; function animate(time) { const delta time - lastTime; // 两次回调之间的时间差单位毫秒 lastTime time; updateParticles(delta); // 按实际经过的时间推进粒子运动 renderParticles(); requestAnimationFrame(animate); } requestAnimationFrame(animate);这里的delta不能简单粗暴地直接当作速度系数用。正常 60Hz 刷新下相邻两帧的 delta 大约是 16.7ms但实际上浏览器偶尔会出现 33ms 或 50ms 的间隔比如主线程一时繁忙、系统卡顿。如果你的粒子速度计算是按固定步长推进的30ms 和 15ms 两帧的粒子位移差距会非常明显看起来就是「卡一下后又突然鬼畜」。我通常会做一个 delta 上限截断比如把超过 50ms 的 delta 直接按 50ms 算避免用户切后台回来后粒子瞬间飞没影了const clampedDelta Math.min(delta, 50);另外还有一个很多人忽略的细节requestAnimationFrame在页面隐藏时会自动暂停。如果产品要求页面切后台后回来爆炸动画能无缝继续那你需要在visibilitychange事件里重置lastTime的值否则第一个 delta 会异常大。做法很简单document.addEventListener(visibilitychange, () { if (!document.hidden) lastTime performance.now(); });2.3 双缓冲与clearRect的取舍为什么爆炸痕迹会「断」说到渲染循环几乎所有入门教程都会写ctx.clearRect(0, 0, w, h)清空整块画布。在粒子爆炸这个场景里这种写法在逻辑上是对的每个粒子都是独立移动的对象上一帧的位置不擦掉就会留下整个屏幕的粒子尾巴。但这里有个潜在问题——clearRect的清除范围是矩形如果你把画布设了圆角或某个不规则蒙层矩形清除会带着上一帧的残留粒子出现在不该显示的区域。我在实际项目中的做法是如果 Canvas 是铺满全屏、且没有 CSS 蒙版遮挡直接用clearRect没问题但如果 Canvas 被裁切过或者页面有半透明浮层叠加最好在初始化时记录裁剪区域并在清屏时按区域逐个清除ctx.save(); ctx.beginPath(); ctx.rect(0, 0, canvasWidth, canvasHeight); ctx.clip(); // 裁切出有效绘制区域curve/圆形爆炸范围时很有用 ctx.clearRect(0, 0, canvasWidth, canvasHeight); ctx.restore();这里的核心思路是清屏和绘图必须走同一个裁剪路径否则粒子会在边缘留下残留的「半截」痕迹。这个坑在高分屏加 CSS 圆角容器时尤其高频排查时你会发现粒子明明消失了画布边缘却还挂着上一帧的半圆点。3. 粒子对象的建模与运动方程核心参数怎么定才对味3.1 粒子结构设计位置、速度、加速度与生命周期的四项默认值粒子爆炸听起来玄学实际上核心就是一个对象数组每个对象自己管自己的位置、速度、尺寸、透明度和剩余寿命。设计数据结构时我倾向于把「可被外部修改的物理量」和「渲染属性」分开这样后续做扩展会舒服很多。下面这组代码是这份资源里比较典型的粒子对象结构结合了多个真实项目的实践经验class Particle { constructor(options) { this.x options.x; // 初始横坐标爆炸中心 this.y options.y; // 初始纵坐标爆炸中心 this.vx options.vx; // 水平方向初速度 this.vy options.vy; // 垂直方向初速度 this.gravity options.gravity || 0.18; // 重力加速度越大下落越快 this.damping options.damping || 0.94; // 阻力系数小于1会逐步减速 this.size options.size || 3; // 当前半径 this.initialSize this.size; // 记录初始值方便按比例收缩 this.alpha options.alpha || 1; // 当前透明度 this.decay options.decay || 0.015; // 每帧透明度衰减量 this.color options.color || #ff6600; // 粒子的主色 this.life options.life || 120; // 寿命帧数 this.maxLife this.life; } }大部分初学者会忽略initialSize和maxLife这两个「反向参照值」。它们的作用是粒子在飞散过程中大小和透明度是不断变化的如果你直接拿当前值做百分比计算会出现越来越难收敛的问题。正确的思路是始终用初始值和最大寿命做归一化让粒子的尺寸、透明度沿着生命进度线性下降这样爆炸视觉上才会均匀、连贯。具体运动方程的更新逻辑如下注意每帧执行一次update() { this.vx * this.damping; // 水平方向空气阻力 this.vy * this.damping; // 垂直方向空气阻力 this.vy this.gravity; // 重力累加到垂直速度 this.x this.vx; // 更新位置 this.y this.vy; this.life - 1; if (this.life 0) this.life 0; // 生命周期百分比1 表示刚出生0 表示寿命耗尽 const lifeRatio this.life / this.maxLife; this.size this.initialSize * lifeRatio; this.alpha lifeRatio; if (this.alpha 0.02) this.alpha 0; // 低于阈值直接省略绘制 }这套运动模型里最影响视觉手感的是damping和gravity两个参数。damping设置在 0.92~0.96 之间能模拟出「空中摩擦消散」的感觉如果设成 1 或大于 1粒子永远不会减速整个画面会像炸毛一样乱飞gravity则决定了粒子飞出去后的下落强度0.15~0.25 是一个比较自然的区间太大粒子会像石头一样笔直下坠失去爆炸的喷射感。生命值的单位是帧数120 帧约等于 2 秒60Hz 下具体数值建议放到配置对象里方便设计师在真机上反复调。3.2 爆炸初速度生成随机分布里的「中心偏密、边缘稀疏」粒子爆炸的视觉冲击力很大程度上取决于初速度的分布方式。最省事的写法是生成 0~2π 之间的随机角度和固定的速度大小const angle Math.random() * Math.PI * 2; const speed 8; // 固定速度值 const vx Math.cos(angle) * speed; const vy Math.sin(angle) * speed;但这种写法做出来的爆炸效果非常生硬——所有粒子的速度大小一致飞散出去像一朵均匀的蒲公英缺了「爆发感」。真实爆炸往往是大量高速粒子集中在核心区域少部分粒子飞得特别远。我一般会引入一个速度阶梯函数让速度分布呈现「核心密、外围稀」的特征function getRandomVelocity(minSpeed, maxSpeed) { const ratio Math.pow(Math.random(), 1.5); // rand^1.5 让小值出现概率更高 const speed minSpeed (maxSpeed - minSpeed) * ratio; const angle Math.random() * Math.PI * 2; return { vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed }; }Math.pow(Math.random(), 1.5)是关键Math.random()本身是均匀分布取 1.5 次方后数值偏向 0~0.5 区间的概率显著增加这就让更多粒子拿了较低的初速度、留在核心附近燃烧少部分粒子拿到高速度向外突围形成「一团炸开 零星飞溅」的层次感。指数幂越大比如 2.0核心聚集效果越明显越小比如 0.5速度越均匀、爆炸越散。这个技巧在调「火药味」和「烟雾感」两种风格时搭配着用非常出效果。如果你希望粒子的颜色也有随机性而不只是一个纯色可以在生成时选一组相邻色相的颜色const hue 20 Math.random() * 30; // 橙色系色相 20~50 之间浮动 const color hsl(${hue}, 90%, 60%);用 HSL 色值而不是具体的#xx色号是为了让同一次爆炸里的粒子颜色视觉上属于同一个家族又不会完全一样。这在 Canvas 里没有额外性能开销但整体效果会立刻脱离「小学生配色」的观感。3.3 对象池与粒子复用不要每帧 new 一堆对象很多从动画转过来的开发者写粒子系统最容易犯的错误是触发爆炸时new几百个粒子对象粒子死亡后直接不管它。这种写法在小数量比如 50 个以内时问题不大但到一个全屏爆炸效果动不动一两千粒子的场景GC垃圾回收压力会让帧率陡降——现象是 FPS 从 60 掉到 30 左右而且抖动严重因为垃圾回收是周期性触发的。对象池是这类问题最经典的解法——提前初始化一批 Particle 实例爆炸时从池子里取「活的」粒子死亡后把状态归零并还给池子。代码结构大致如下class ParticlePool { constructor(total, spawnOptions) { this.pool []; this.active []; for (let i 0; i total; i) { this.pool.push(new Particle({ ...spawnOptions })); } } spawn(position, velocityOptions) { const p this.pool.pop(); // 从空闲池取一个 if (!p) return null; // 池子空了本帧不再爆炸更多粒子 p.x position.x; p.y position.y; p.life velocityOptions.life; p.vx velocityOptions.vx; p.vy velocityOptions.vy; p.alpha 1; p.size velocityOptions.size; this.active.push(p); return p; } updateAll() { for (let i this.active.length - 1; i 0; i--) { const p this.active[i]; p.update(); if (p.life 0 || p.alpha 0.02) { this.active.splice(i, 1); this.pool.push(p); // 死后回收 } } } }这里要提醒一下this.active的遍历顺序。我习惯从数组尾部往前遍历因为splice(i, 1)会触发数组元素的前移从后往前遍历可以避免「跳过下一个待检查粒子」的遍历坑。对象池总容量按项目需求落位如果页面最多允许同时存在 800 个粒子那池子初始化到 850~900 个多出来的几十个作为缓冲。对象池虽然看着代码量多了一些但对性能的收益是决定性的从「反复创建销毁对象」变成「复用一组对象」GC 频率大幅降低真机上的帧率曲线会稳定很多。如果一个 Canvas 项目从首帧卡到尾帧先去排查是不是每帧都在 new 对象、有没有做复用往往比调运动参数更有效。4. 爆炸触发与交互控制从「点一下炸一次」到「连续爆炸」4.1 点击坐标换算解决滚动偏移和相对定位的错位问题粒子爆炸的常见触发方式是点击页面任意位置然后在点击坐标处生成一次爆炸。这个需求看似简单但在 PC 和移动端混合场景里坐标换算的坑非常隐蔽。如果你直接监听click事件并取clientX、clientY在 Canvas 绝对定位不滚动的情况下没问题但一旦 Canvas 所在容器不是位于页面左上角、或者页面本身可以滚动粒子就会从「点击处偏移一段距离」的位置炸开。通常的写法是把clientX和clientY转成相对于 Canvas 左上角的坐标function getCanvasCoords(canvas, clientX, clientY) { const rect canvas.getBoundingClientRect(); return { x: clientX - rect.left, y: clientY - rect.top }; }但getBoundingClientRect()拿到的是 CSS 尺寸下的矩形位置如果你的 Canvas 做了 DPR 适配参考第 2 章的setTransform这里不需要再额外除以 DPR——因为我们在逻辑坐标系里已经用setTransform把单位统一成了 CSS 像素所以直接用clientX - rect.left可以得到正确的逻辑坐标。另一个坑是移动端touchstart事件里的触摸点坐标。移动端没有click的实时性如果你等click触发用户会感到明显的延迟移动端 click 约延迟 300ms。更好的做法是同时监听pointerdown和click并做一个简单的去重例如canvas.addEventListener(pointerdown, (e) { const { x, y } getCanvasCoords(canvas, e.clientX, e.clientY); spawnExplosion(x, y); }); canvas.addEventListener(click, (e) { // 防止 PC 上 click 与 pointerdown 重复触发 if (e.detail 1 didPointerHandle) return; const { x, y } getCanvasCoords(canvas, e.clientX, e.clientY); spawnExplosion(x, y); });这里的关键是连续事件之间需要一个状态标志位来避免双重爆炸。didPointerHandle在pointerdown里置为 true在click里判断后重置。这是实际项目里最容易犯的「重复开火」错误——看起来粒子爆炸了两次实际上是两个事件都监听了。4.2 粒子池多爆炸并发管理让每次爆炸独立不互相干扰当用户快速连续点击、或同一个页面同时触发两个爆炸时如果粒子池没有做「每爆炸一次分配一段独立生命周期」的管理就会看到两批粒子互相串味——A 爆炸的粒子被 B 爆炸回收或者同一批粒子在两个爆炸中心之间横跳。这个问题的根源在于粒子对象本身没有「归属爆炸」的概念。为了解决它可以在粒子对象的更新逻辑里让每粒粒子携带一个batchId或者在自己的更新回调里判断生命阶段。更简单直接的做法是把粒子的存活字段独立出来每次spawn对整个粒子池里所有物理量做一次「硬性重置」spawn(position, velocityOptions) { const p this.pool.pop(); if (!p) return null; p.x position.x; p.y position.y; p.vx velocityOptions.vx; p.vy velocityOptions.vy; p.life velocityOptions.life; p.maxLife velocityOptions.life; p.alpha 1; p.size velocityOptions.size; p.initialSize velocityOptions.size; p.gravity velocityOptions.gravity; p.damping velocityOptions.damping; this.active.push(p); return p; }注意到没有——一定要重置maxLife。如果上一次爆炸设置的maxLife是 200 帧这次的life是 80 帧而maxLife忘了更新那么this.life / this.maxLife这个比值会从一开始只有 0.4粒子的透明度和尺寸初始值就不对视觉上会出现「炸出来就是半透明、而且快速消失」的问题。这类问题极难靠肉眼定位因为你会在参数调整上花一晚上最后发现只是初始化时漏了一行。4.3 定时爆炸与随机延迟做出「连续迸发」的高级感除了点击触发活动页里经常需要一个「自动连续爆炸」的模式比如用户进入页面后每秒钟在随机位置炸一次。直接拿setInterval每 1 秒触发一次爆炸效果会显得非常机械。经验性做法是基础间隔 随机延迟让爆炸之间看起来是有人手动操作的节奏感。function scheduleAutoExplosion() { const delay 600 Math.random() * 1000; // 600ms~1600ms 之间随机 setTimeout(() { const x Math.random() * canvasWidth; const y Math.random() * canvasHeight * 0.8; // 避开顶部状态栏区域 spawnExplosion(x, y, { power: medium }); scheduleAutoExplosion(); }, delay); }Math.random() * canvasHeight * 0.8这个数值是基于实际场景的经验取值页面顶部往往有标题、返回按钮或产品 Logo爆炸经常遮挡那里的 UI 会很影响观感限制到 80% 高度区域既保证视觉冲击力又不遮核心内容。定时爆炸的触发函数不应该在页面不可见时继续执行——如果用户切走了后台依然疯狂生成粒子这会导致不必要的 CPU 占用和电池消耗所以上面要配合visibilitychange事件暂停自动爆炸的调度let autoTimer null; function stopAutoExplosion() { if (autoTimer) clearTimeout(autoTimer); autoTimer null; }结合第 2 章的 delta 截断这套自动爆炸模式即使在主线程偶尔繁忙、帧率波动的情况下也能保持视觉上的连续感不会出现「卡一下之后所有粒子同时跳走」的抽风现象。5. 实践中的排障手册渲染模糊、内存泄漏、透明合成等五个高频坑5.1 画面整体模糊粒子边缘有白色或半透明光晕现象部署到真机上后粒子轮廓发虚有明显的光晕感但 PC 端开发时看起来一切正常。原因这个问题几乎 100% 是 DPR 适配没做对。一种情况是完全没适配——Canvas 缓冲尺寸小于 CSS 尺寸浏览器对位图做了放大另一种情况是适配了 DPR但在页面动态旋转或窗口缩放后Canvas 的缓冲尺寸没有重新同步。后者最常见因为很多人只在初始化时设置了一次canvas.width窗口resize后画布被 CSS 拉伸了缓冲还是老尺寸。解决把第 2 章的setupCanvas封装成一个可重复调用的函数在resize事件里调用一次。注意resize事件不要直接绑定高频的 Canvas 重设用debounce控制到 150ms 内最多执行一次即可let resizeTimer null; window.addEventListener(resize, () { if (resizeTimer) clearTimeout(resizeTimer); resizeTimer setTimeout(() { setupCanvas(canvas, window.innerWidth, window.innerHeight); }, 150); });另外如果setupCanvas里调用了setTransform重设后必须再次调用setTransform否则坐标系会按新缓冲尺寸但旧矩阵缩放出现粒子飞不准的情况。5.2 帧率从稳定 60 突然掉到 20且周期性抖动现象粒子数量没变但运行几秒后性能明显恶化FPS 曲线像锯齿一样。原因大量粒子对象没有被正确回收。常见于没有用对象池、直接在触发时new Particle的写法或者虽然用了对象池但对象池的active数组里粒子已经死亡、却没有被移出导致渲染循环里还在对这些「死粒子」做无谓的绘制。另一种可能是 Canvas 尺寸异常——DPR 适配后缓冲区尺寸比 CSS 尺寸大 4 倍DPR2 时面积是 4 倍绘制效率大幅下降。解决在渲染循环里加一个简单的粒子状态日志每 60 帧打一次active.length看一下数量是否持续增长不回落let frameCount 0; function debugMonitor() { frameCount; if (frameCount % 60 0) { console.log(active particles:, particlePool.active.length); console.log(pool remaining:, particlePool.pool.length); } }如果active.length持续上升且不下降优先检查粒子的死亡条件是否达成了——比如life减到 0 后是否还被持续绘制或者alpha的判断阈值是不是设得太低、导致粒子永远不进入回收分支。把阈值从0.02调整到0.05往往能快速止住内存累积趋势。5.3 粒子出现不连续的「跳变」或「瞬移」现象粒子飞行的路径不是平滑的弧线而是每隔几帧跳动一次看起来像卡帧。原因最常见的是requestAnimationFrame的 delta 没有做截断切后台回来后首帧 delta 可能高达几千毫秒导致粒子一次性走了几十帧的路程另一种可能是因为在setInterval里改用了 rAF、但粒子更新的步长是基于固定帧率的假设例如每帧固定位移 3px在 120Hz 刷新率的设备上速度会放大两倍视觉上像是在加速。解决更新逻辑统一改成基于 delta 的物理模拟delta 单位换算成秒或帧数并加上上限截断。建议把粒子更新从「绝对的每帧一步」改成「速度乘以时间步长」const deltaInSeconds Math.min(delta, 50) / 1000; p.vx * Math.pow(p.damping, deltaInSeconds * 60); p.vy * Math.pow(p.damping, deltaInSeconds * 60); p.vy p.gravity * deltaInSeconds * 60; p.x p.vx * deltaInSeconds * 60; p.y p.vy * deltaInSeconds * 60;这里的deltaInSeconds * 60是一个换算系数——把秒换算回 60 帧/秒的基准单位这样就算屏幕刷新率是 120Hz 或 90Hz粒子的物理速度也能保持一致。这行代码是跨机型适配的关键很多人在 120Hz 的手机上发现爆炸速度快了一倍原因就在这里。5.4 粒子重叠部分产生奇怪的深色或黑色边缘现象粒子密度高的区域尤其是爆炸中心颜色突然变暗甚至出现黑色轮廓。原因Canvas 默认的合成模式是source-over即后绘制的像素会覆盖先绘制的像素。粒子爆炸初期几百个粒子堆叠在同一个地方每个粒子都带半透明度叠加区域会多次加深颜色特别是用rgba或hsl的透明度时重叠部分自然变深。这本身不算「错」但视觉效果上会显得脏。解决改用globalCompositeOperation lighter让粒子的颜色叠加变亮这样核心区域会出现明亮的「燃烧感」而不是浑浊的暗色块ctx.globalCompositeOperation lighter; // ...绘制所有粒子... ctx.globalCompositeOperation source-over;注意lighter模式在有纯白背景的环境下可能会过曝需要搭配半透明度的粒子颜色一起用。如果希望粒子叠出一个由亮到暗的闪燃效果可以在此基础上让粒子的颜色在早期偏白、后期偏向主题色而不是只用单色到底。5.5 粒子爆炸后留下一条「尾巴」怎么都清不掉现象粒子本身已经移出屏幕但画布上仍然有淡淡的痕迹或细线清理不掉。原因要么是clearRect的清除范围没有覆盖全部画布比如你在适配 DPR 时用了 CSS 宽高而忘记乘 DPR要么是使用了带蒙层/裁剪区域的画布清除矩形和裁剪路径的并集没有完全对齐。解决把清除范围直接对齐 Canvas 的物理缓冲尺寸而不是 CSS 尺寸。如果 Canvas 缓冲宽 1920、CSS 宽 960出问题的先例往往是写了clearRect(0, 0, 960, 600)实际缓冲区还需要清除 1920 宽的区间。一行修正ctx.clearRect(0, 0, canvas.width, canvas.height);注意这里用的是canvas.width而不是 CSS 尺寸变量因为canvas.width是缓冲区的真实像素宽度。结合第 2 章的正确定义clearRect的清除区域和 Draw 坐标系可以不一致但清除目的就是「恢复物理缓冲区为透明」用物理尺寸最保险。6. 把爆炸调出质感关于颜色曲线、残影和事件解绑的三个技巧粒子爆炸做到能跑起来、不掉帧只是及格线。真正决定项目验收时设计是否点头的是爆炸的视觉「手感」——这个很难用指标横量说穿了就是几个参数和微调手法之间的小配合我把它们压到三个维度里说。第一个技巧是颜色随生命周期的动态变化而不是固定一个颜色画到底。粒子出生瞬间一般是亮白色或浅色飞行过程中逐步过渡到主题色最后消散时变暗。这样强调前 0.2 秒的「爆发闪烁感」后面才是颜色的主体展示。实现上不需要引入复杂插值给粒子追加一个startColor和endColor在update里按lifeRatio做一次线性插值就行updateColor() { const t 1 - this.lifeRatio; // 0 出生1 死亡 this.r this.startColor.r (this.endColor.r - this.startColor.r) * t; this.g this.startColor.g (this.endColor.g - this.startColor.g) * t; this.b this.startColor.b (this.endColor.b - this.startColor.b) * t; }实际调参时你会发现r/g/b每件单独做线性插值会有点「平」但作为基线够了。更进阶的可以做两段插值前 30% 生命周期保持亮色后 70% 才往暗色过渡——对应到代码上就是把t做一个分段映射if (t 0.3) t 0。第二个技巧是运动残影但这里的「残影」不是不清屏留下的拖尾而是有控制地保留上一帧的 1%~2% 透明度让前几帧的粒子在视觉上「停留」一小会儿叠加出丝滑的流动感。具体做法是在清屏时不完全清空而是用半透明白色覆盖ctx.fillStyle rgba(255, 255, 255, 0.02); ctx.fillRect(0, 0, canvas.width, canvas.height);这个rgba(255, 255, 255, 0.02)的 0.02 是经验值——太小看不出残影太大会变成整个屏幕蒙白雾。它要求画布背景是纯色且不透底的场景才适用如果是透明底叠加在页面内容上用回clearRect。项目里一般可以做成参数让设计自己调标注「残影强度建议 0~0.05超过 0.03 会发白」。第三个技巧是事件解绑与页面卸载时的内存清理。很多 H5 项目做完动画上线后从 A 页面跳到 B 页面浏览器内存占用不减然后被系统杀掉。大部分原因是 Canvas 动画循环还在跑、事件监听还挂着。正确姿势是在页面卸载或组件销毁时主动结束动画并解绑事件function destroy() { cancelAnimationFrame(animationId); canvas.removeEventListener(pointerdown, pointerHandler); canvas.removeEventListener(click, clickHandler); document.removeEventListener(visibilitychange, visibilityHandler); particlePool.active.length 0; particlePool.pool.length 0; }cancelAnimationFrame(animationId)需要你在animate循环里把每次requestAnimationFrame的返回值保存下来很多初级写法的坑就在于此——animationId变量没在循环外声明解绑时拿不到。这个看起来不起眼的「收尾工作」决定了你的项目在低端机上长期运行会不会越跑越烫。说起来有点丢人我第一次做粒子爆炸上线后设计反馈「刺眼」开发反馈「内存一直在涨」运营反馈「手机发烫」。那版代码就是颜色固定、没有残影、循环永不停止的典型「三无」版本。后来整整调了两天把颜色动态、残影控制、事件解绑这三个坑补齐才真正验收通过。从那以后我每次做 Canvas 特效都要强制走一遍这三点颜色有生命周期、残影强度可配置、销毁时一切归零。希望帮到你。本文还有配套的精品资源点击获取