news 2026/10/27 3:45:55

Canvas烟花拖尾实现:残影法与轨迹法对比及性能调优

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Canvas烟花拖尾实现:残影法与轨迹法对比及性能调优

1. 先搞清楚“拖尾”到底在画什么

烟花这个东西,看起来花里胡哨,但拆开看只有两件事:粒子在动,和粒子动过的地方留下痕迹。前者靠运动学和一点随机数,后者就是标题里说的“拖尾”。

我见过不少人第一次做烟花特效,写完粒子系统发现火花是“一颗颗断开的点”,像撒了一把沙子在天上飘,完全没有烟花那种拉丝、扫尾、余烬慢慢熄灭的味道。问题不在粒子数量,也不在颜色,纯粹是拖尾没做对。所以这篇东西我不打算从“如何画一个圆”讲起,直接切进正题:canvas 里实现拖尾有两条主流路线,一条靠逐帧半透明覆盖,一条靠历史轨迹点连线,它们解决的问题一样,但代价、手感、适用场景完全不同,选错了后面会一路别扭。

先说清楚这两条路线分别是什么,方便你对号入座。

第一条,残影法(也叫衰减法、覆盖法)。核心动作是每一帧不清屏,而是用一层带透明度的纯色矩形盖住整个画布,比如rgba(0,0,0,0.15)。上一帧的内容被压暗 15%,再画新的粒子,于是亮的点越来越暗,形成一条渐隐的尾巴。这是最省事、最像“老式显像管余辉”的做法,代码量极小。

第二条,轨迹法。每个粒子自己记住最近 N 个位置,每帧把这些历史点按顺序连成线或者画成一串半径递减的圆,然后在整个画布上做一次彻底清屏。拖尾的长短由 N 决定,形状由历史点的分布决定,你能精确控制尾巴的粗细变化和弯曲程度。

两种方法谁更好?没有绝对答案,但我可以给一个直接的经验判断:要“糊”、要“光晕感”、要省性能,用残影法;要“准”、要“丝带感”、要能单独控制每条尾巴,用轨迹法。烟花场景其实两种都用得上,很多看起来高级的效果,是把两者叠在一起用的。

对比维度残影法(半透明覆盖)轨迹法(历史点连线)
清屏方式每帧盖一层半透明色每帧彻底清空
拖尾长度控制靠透明度系数间接控制靠历史点数量直接控制
性能开销极低,一次 fillRect中等,每粒子每帧多条线段
尾巴形状均匀渐隐,无方向性可做锥形、弯曲、粗细渐变
典型问题全屏发灰、残影堆积点数多时锯齿、断线
适合的烟花部分爆炸瞬间的光晕、地面反光上升的引线、下坠的余烬拉丝

理解这张表比记住任何代码都重要。下面我会把两条路线分别拆开,从原理到参数到踩坑,一路讲到能直接抄的程度,最后再把它们拼成一个完整的烟花。

2. 残影法:一行 fillRect 背后的三个隐藏参数

残影法的骨架短得离谱,短到很多人以为它没技术含量,结果调出来一片灰蒙蒙的脏画布。真正的难点全在参数上。

2.1 透明度系数决定尾巴的“寿命”

假设每帧覆盖透明度是 α,某个像素原本亮度是 1,经过 n 帧后亮度变成(1-α)^n。这就是拖尾的衰减公式,也是你调参的唯一依据。

几个常用值的手感,我实测下来大致是这样:

  • α = 0.05:尾巴极长,能拖出七八十帧的残影,适合做星云、极光那种缥缈感。但缺点很明显,画面容易糊成一团,旧粒子散不干净。
  • α = 0.15:我个人的甜点值,尾巴大概 15 到 25 帧,烟花余烬的拉丝长度刚好,既有速度感又不脏。
  • α = 0.3:尾巴短促,只有五六帧的痕迹,适合做干脆的爆闪、火花四溅的瞬态。
  • α 超过 0.5:基本等于清屏,拖尾几乎看不出来。

换算成“尾巴多少帧消失”,公式是n = ln(阈值) / ln(1-α)。假设你觉得亮度低于 2% 就算看不见了,代入 α=0.15 得到ln(0.02)/ln(0.85) ≈ 24帧,和我上面的手感描述对得上。这个公式的价值在于:当你想改帧率或者改尾巴长度时,不用瞎试,直接算。比如把 60fps 降到 30fps,同样的 α 会让尾巴在时间上缩短一半,视觉上明显变短,这时候要把 α 调小,用公式反推就行。

注意:α 是每帧作用的,所以它和帧率强耦合。任何写成固定值的 α,在掉帧的设备上都会表现为“尾巴突然变短”。后面第 5 节会讲怎么处理这个耦合。

2.2 覆盖色不能随手写 black

这是新手最容易翻车的地方:为了让背景是黑的,直接fillStyle = 'rgba(0,0,0,0.15)'。如果你的背景本来就是纯黑,没问题。但如果背景是深蓝夜空、渐变、或者带一层星星贴图,这个操作会把背景一点点吃掉,最后全画布变成纯黑,你的夜空背景消失了。

正确做法有两种。第一种是覆盖色取背景色的同色系,比如夜空是#0a0e27,那覆盖层就用rgba(10,14,39,0.15),这样衰减的方向是回到背景色,而不是回到黑。第二种更稳妥,背景不参与衰减:把背景画在一个独立的 DOM 层(或者离屏 canvas)上,烟花画布用globalCompositeOperation = 'destination-out'配合半透明白色矩形来做擦除,这样擦掉的是透明度,露出下层背景,永远不会污染颜色。

第二种做法我强烈推荐给所有要做背景图的场景,代价只是多一层 DOM 或者一次 drawImage。代码大概长这样:

// 擦除模式:把画布上的内容按比例擦成透明 ctx.globalCompositeOperation = 'destination-out'; ctx.fillStyle = 'rgba(0,0,0,0.15)'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.globalCompositeOperation = 'source-over';

注意destination-out模式下,填充色的 alpha 才是关键,RGB 值无所谓,但 alpha 的语义变成了“擦除强度”。很多教程写rgba(0,0,0,0.15)在source-over下当作压暗用,直接照搬到destination-out会得到同样的数值、不同的行为,容易让人困惑,所以务必记住模式的切换。

2.3 高分屏下的一次 fillRect 为什么会拖影不均

devicePixelRatio大于 1 的设备上,如果 canvas 的 CSS 尺寸和绘图缓冲区尺寸没对齐,fillRect(0,0,canvas.width,canvas.height)里的canvas.width到底是 CSS 像素还是物理像素,很容易搞混。标准的处理是先设canvas.width = cssWidth * dpr,然后用ctx.scale(dpr, dpr)把坐标系拉回 CSS 像素,之后所有绘制按 CSS 尺寸写。但在残影法里,scale之后你调的fillRect用的是 CSS 尺寸,覆盖范围是够的,问题不大;真正会出问题的是你只对部分内容做了缩放,而覆盖矩形用的是物理尺寸,导致每帧只擦掉左上角一块,右边和下边持续堆积残影,画面上出现一条斜向的“脏区”。

排查这种问题的思路很直接:把覆盖矩形临时改成rgba(255,0,0,0.15),跑几帧看红色是否均匀铺满全屏。如果某个角落始终不被覆盖,就是尺寸或变换矩阵的问题。这个手法我用了很多次,比盯着代码看快得多。

2.4 残影法的两个真实缺陷和缓解手段

缺陷一:残影会“记住”一切,包括不该记的东西。比如你在画布上画了 UI 文字、帧率显示,残影法会让它们也拖出鬼影。解决办法是把 UI 画在独立图层,或者每帧先清一次再重画 UI——但这样又破坏了残影。所以残影法天生适合“画布里只有粒子”的纯粹场景。

缺陷二:长时间运行后画布会积累一层低亮度噪点。因为浮点精度和浏览器合成的原因,理论趋近于零的像素实际可能停在 1 到 2 的亮度上,几十秒后整屏会有一层极淡的灰。缓解办法是每隔几百帧做一次硬清屏(直接clearRect),或者每隔一段时间把 α 临时调大一点冲刷一次。我在一个长时间展示的项目里就是这么干的:每 600 帧执行一次clearRect然后立即重绘当前所有粒子,肉眼几乎察觉不到,但噪点被彻底清掉了。

3. 轨迹法:每个粒子背着自己的历史

轨迹法的思路完全反过来。它不依赖画布的残留,而是让粒子自己带一本“日记”,记录最近若干个位置,每次重绘时把日记里的点连起来。

3.1 环形缓冲比数组 shift 更适合高频写入

最直觉的实现是positions.push(p)然后positions.shift()保持长度。在粒子数量少的时候没问题,但烟花场景轻松上千粒子,每帧每粒子一次shift(),数组内部要整体搬移元素,开销不低,还容易触发 GC 抖动。

更稳的做法是用环形缓冲(固定长度数组加一个写指针),写入时覆盖旧值,索引取模。读的时候从写指针位置开始按环序遍历,就得到从旧到新的轨迹。这样每个粒子的内存是固定的,零分配,跑多久都不涨内存。代码不复杂:

class Trail { constructor(size = 8) { this.size = size; this.buf = new Float32Array(size * 2); // x,y 交错存 this.head = 0; this.count = 0; } push(x, y) { const i = this.head * 2; this.buf[i] = x; this.buf[i + 1] = y; this.head = (this.head + 1) % this.size; if (this.count < this.size) this.count++; } // 从最旧到最新遍历 forEach(cb) { const start = this.count < this.size ? 0 : this.head; for (let k = 0; k < this.count; k++) { const idx = ((start + k) % this.size) * 2; cb(this.buf[idx], this.buf[idx + 1], k / this.count); } } }

用Float32Array而不是普通对象数组,是因为坐标是纯数值,类型化数组没有对象头和指针追踪,对 GC 友好得多。这个细节在几百粒子时无所谓,几千粒子加长拖尾时差距就出来了。

3.2 尾巴长度不是“点越多越好看”

历史点数量 N 直接决定尾巴的视觉长度,但 N 不是越大越好。N 太大有两个副作用:一是每帧的线段绘制次数线性增长,二是当粒子速度很快时,相邻历史点的间距变大,连出来的线会变成明显的折线,看起来像锯齿。

我一般的取值参考:粒子速度在每秒 300 到 600 像素这个区间,N 取 8 到 12 比较舒服;如果粒子是爆炸后高速飞散的,速度上千,那 N 反而要降到 4 到 6,因为点太稀反而更容易暴露折线。核心原则是让相邻历史点的间距保持在 5 到 20 像素之间,太密了看不出拖尾,太疏了断线。

这里有个估算公式可以帮你定初始值:N ≈ 期望尾巴像素长度 / (速度 * 帧间隔)。比如想要 120 像素的尾巴,速度 500 像素每秒,60fps 即每帧 16.7ms,那么每帧位移 8.3 像素,N ≈ 120 / 8.3 ≈ 14。这个值可以当作起点,再按视觉效果微调。

3.3 用线宽渐变代替逐点画圆

初学轨迹法最常见的画法是遍历历史点,每个点画一个arc,半径随位置递减,形成一串珠子。效果能用,但性能很差:N 个点就是 N 次路径构建加填充,粒子多的时候直接把帧率打下来。

更划算的做法是只画一条折线,通过分段设置lineWidth实现粗细渐变。lineWidth在一次stroke内不能逐段变化,所以要按段拆开:从尾到头遍历,每一段单独beginPath、设线宽、moveTo、lineTo、stroke。这样是 N-1 次 stroke,比 N 次 arc 加 fill 便宜,而且线宽渐变是连续的,视觉上更像真正的丝带。

draw(ctx, color, baseWidth) { const pts = []; this.forEach((x, y, t) => pts.push([x, y, t])); for (let i = 1; i < pts.length; i++) { const t = pts[i][2]; // 0 最旧,1 最新 ctx.beginPath(); ctx.moveTo(pts[i - 1][0], pts[i - 1][1]); ctx.lineTo(pts[i][0], pts[i][1]); ctx.lineWidth = baseWidth * t; // 越新越粗 ctx.strokeStyle = color; ctx.stroke(); } }

线宽随 t 线性变化会显得头重脚轻,实际用t * t或者Math.pow(t, 1.5)让尾部收得更快,看起来更自然。这个幂次就是手感参数,想要尾巴“尖”就加大指数,想要“钝”就减小。

3.4 圆头线帽和亮度衰减:两个被低估的细节

lineCap默认是butt,画出来的线段两端是平口,多段拼接时接缝处会出现细小的缺口,尤其是角度大的转折处特别明显。改成lineCap = 'round'后接缝被圆头盖住,整体顺滑很多。这个小细节能让轨迹法的观感提升一个档次,成本几乎为零。

亮度衰减则要靠颜色控制。同一个粒子从新到旧,颜色应该从亮白逐渐过渡到粒子本色的暗端。很多人只在两端设色,中间靠插值,但注意strokeStyle是每段单独设的,你可以完全控制。用 HSL 做最方便:色相固定,明度从 90% 降到 20%,饱和度略微上升,让尾巴末端显得更“实”一点。

提示:轨迹法下如果粒子的历史点包含极早的位置(比如粒子刚生成时位置都堆在发射点),起手几帧会看到一团密集的短线挤在原点。解决办法是新粒子在填充历史缓冲前,先用当前位置把所有槽位预填一遍,这样一开始轨迹长度为 0,然后逐渐“长”出来,非常自然。

4. 把烟花拆成三段:两种拖尾各自的分工

光讲技术不看场景是耍流氓。烟花的结构其实很稳定,通常分三段:上升、爆炸、下坠。这三段对拖尾的需求完全不同,分开讨论才有实际意义。

4.1 上升段:引线要的是“准”,用轨迹法

烟花升空的主体是一颗亮球,后面拖着火星。这段的视觉关键是方向感——观众要能看出它往哪飞,速度多快。残影法做这个会糊成一团光斑,方向性丢失;轨迹法则是天然优势,历史点沿着飞行方向排开,连出来就是一根笔直的引线,末尾用亮度衰减做余烬效果。

上升段的粒子数量少(通常就一颗主球加几颗随机火星),所以轨迹法的性能开销完全无所谓,可以直接用多点、宽线、圆头帽,做到丝滑。主球本身的位置每帧推进一步,历史缓冲固定 10 到 14 个点,尾部线宽收到 0.5px 以下,视觉上就像一根正在燃烧的引线。

这里有个经验:上升段的加速度要逐渐减小,不能恒定。真实烟花出膛瞬间加速度大,接近顶点时明显 느려(速度衰减到几乎没有),然后在顶点爆炸。很多人写匀速上升,看起来像电梯,很假。用一个简单的速度衰减,vy *= 0.985,配合初始较大的速度,就能得到很自然的“冲到顶点停住”的手感。

4.2 爆炸瞬间:闪光要的是“糊”,用残影法

爆炸那一帧,正确的做法是瞬间铺一层高亮的光晕,然后让它快速衰减。这一下如果也用轨迹法逐点画,会得到一堆清晰的短线,反而失去了爆炸的爆发感。残影法在这里完美契合:爆炸瞬间画一个大的径向渐变圆(白心到透明),之后每帧继续用半透明覆盖,这个圆自己会快速暗掉、散开,形成那种“先刺眼后消散”的余辉。

残影法此时还有一个额外好处:爆炸前上升的轨迹残影会一起被炸开的光晕吞掉,视觉上像是被冲击波抹平了,这正好符合爆炸的物理直觉。如果你用轨迹法,就必须手动清理所有上升粒子的历史缓冲,否则引线还挂在那儿,穿帮感很强。

所以一个很自然的组合是:同一块画布,先跑残影法的覆盖,再画轨迹法的新帧。上升段因为粒子少、轨迹细,残影的压暗不明显;爆炸瞬间因为光晕面积大,残影的衰减效果突出。两者共存,并不需要切换模式。这个方案我在多个项目里用过,属于“既省事又好看”的甜点。

4.3 下坠段:余烬的拉丝是两种方法的分水岭

爆炸后的余烬是烟花最好看的部分,也是最考验拖尾的。粒子向四周飞散,受重力影响逐渐下坠,速度从快变慢,亮度从亮变暗。这段的理想拖尾是锥形、带方向、随速度变长的丝。

轨迹法在这里的表现明显更好:每个粒子独立控制尾巴长度,速度快时自然拉长,减速后尾巴缩短,完全符合物理直觉。可以用速度模长动态调整 N 值(或者固定 N 但点间距变小),配合lineWidth的幂次渐变,得到很像真实火花的拉丝。

但纯轨迹法也有短板:余烬数量大(几百到上千),如果每个粒子都做线段级绘制,性能会吃紧。所以我的常见折中是:大部分余烬用残影法做短拖尾(α 调大一点,尾巴只有几帧),少数亮度高的主粒子用轨迹法做长拉丝。观众的眼睛只会被少数亮点吸引,糊的那些当作背景气氛,这个“主次分离”的技巧既省性能,画面层次也更丰富。

4.4 帧率与时间的解耦:所有拖尾的通用前提

不管是 α 还是 N,只要写成固定值,就和帧率绑死了。高刷屏上尾巴变短,掉帧时尾巴变长,同一份代码在不同设备上手感不一致。

解决思路是把“每帧变化量”换成“每秒变化量”。对残影法,α 要按时间缩放:如果基准是 60fps 下的 0.15,那实际值应该是1 - Math.pow(1 - 0.15, dt / (1/60)),其中 dt 是真实的帧间隔秒数。这样在 120fps 下每帧的 α 变小,但单位时间的衰减量一致,尾巴长度在时间维度上就稳定了。

对轨迹法,则可以让历史点按时间采样而不是按帧采样——每积累一定时间才写入一个新点,或者按dt调整写入频率。更简单的偷懒办法是固定上限 N,但让尾巴的视觉长度通过线宽和颜色的时间衰减来补偿,允许轻微差异。实话说,烟花这种瞬态效果,普通用户根本看不出 30fps 和 60fps 下尾巴长度的细微差别,所以我一般对残影法做严格的时间解耦,对轨迹法放宽松一点,把精力花在视觉调优上。

let last = performance.now(); function loop(now) { const dt = Math.min((now - last) / 1000, 0.05); // 夹住防止切页回来爆炸 last = now; // 残影覆盖,按时间解耦 const alpha = 1 - Math.pow(1 - 0.15, dt * 60); ctx.globalCompositeOperation = 'destination-out'; ctx.fillStyle = `rgba(0,0,0,${alpha})`; ctx.fillRect(0, 0, W, H); ctx.globalCompositeOperation = 'source-over'; update(dt); render(); requestAnimationFrame(loop); }

dt上夹一个上限(0.05 秒)非常关键:页面切到后台再切回来,第一帧的 dt 可能是好几秒,不做保护就会一次性把所有粒子推出屏幕、把残影一次擦干净。这个坑我踩过不止一次,表现是“切出去一会儿回来,烟花全没了”。

5. 调参实战:从能跑到好看的四个抓手

技术骨架搭好之后,决定成败的其实是审美参数。这一节讲四个我认为影响最大的抓手,都是靠反复试出来的。

5.1 颜色要“两头虚中间实”

真实的烟花余烬不是纯色,而是中心白热、外围偏橙红、末端暗红。用 HSL 表达最省事:明度高、饱和度低时接近白色,明度降下来、饱和度升上去就是橙红。所以轨迹法的每段颜色可以这样算:hsl(30, ${40 + 50 * t}%, ${20 + 70 * t}%),t 从 0 到 1,尾部暗灰、头部白亮。

一个反直觉的发现:最亮的点不要放在轨迹末端,放在倒数第二三个点。因为末端还在高速移动,视觉上会有运动模糊感,把亮度峰值稍微内收,看起来更像有质量的燃烧体,而不是一根发光棒。这个调整很小,但能让质感明显上一个台阶。

5.2 重力必须给,但别给满

烟花粒子必须受重力,否则余烬水平飞散,像爆炸的气球而不是火花。但重力加速度不能给满,真实世界的 9.8 单位在像素坐标系里往往太大——因为像素和米的换算是不定的。我的经验是把重力当作“每帧速度增量”来调,取值在每秒几百像素的平方量级,从200开始试,看下坠的弧线是否自然。

一个更稳的判断方法:观察余烬从最高点落到爆炸点水平线所用的时间。真实烟花大概 1 到 2 秒,如果你调出来 0.3 秒就落完了,重力太大;如果 5 秒还在飘,重力太小。按这个节奏微调,比死记数值靠谱。

5.3 随机数要“聚”不要“散”

爆炸粒子的方向如果完全均匀随机,会得到一个完美的圆球,看起来像礼花弹的标准图,反而呆板。真实烟花的爆炸有方向性聚集,某些方向粒子多、速度快,形成“花瓣”。

实现上很简单:不要用Math.random() * Math.PI * 2生成角度,而是先生成若干个“主方向”(比如 7 个均匀分布再加抖动),每个粒子以 70% 概率从主方向里选、加小扰动,30% 概率完全随机。速度也一样,主方向上的粒子速度给得更高。这样出来的形状有层次,不会像一个模子刻的。

5.4 生命周期末尾要“跳变”而不是“渐隐”

粒子的亮度到生命末期如果线性降到 0,会出现一种很怪的“慢慢变淡然后突然消失”的感觉。更好的做法是在末期加速衰减,比如亮度按剩余生命的平方来算,最后 20% 的生命里亮度掉到 5% 以下,视觉上就是“闪一下然后熄灭”,非常接近真实火花的爆灭。

配合轨迹法,还可以在熄灭前让尾巴突然缩短(N 减半),模拟火花断掉的瞬间。这个小技巧效果拔群,做出来的余烬有“噼啪”的呼吸感。

5.5 一份可直接跑的整合参数表

我把上面所有可调项整理成表,方便对照修改。数值是 60fps、画布 1280×720 下的起点值,实际按视觉微调。

参数建议起点作用调整方向
残影 α0.15控制残影尾巴长度想更长就减小
轨迹点数 N10控制拉丝长度粒子快则减小
线宽基值3px拉丝粗度配合 N 调整
线宽幂次1.5尾部收尖程度想更尖就加大
重力200 px/s²下坠弧线按落点时间校准
上升速度600 px/s引线冲顶速度配合画布高度
上升衰减0.985/帧顶点减速越小减速越快
爆炸粒子数120单发烟花密度性能允许可加
主方向数7爆炸花瓣感5 到 9 之间
粒子寿命1.2 到 2.5s余烬存活时长随机分布

这张表不是标准答案,但能让你少走很多弯路。真正要调的时候,我建议一次只改一个参数,改完观察三到五秒,否则会陷入“改了一堆结果更难看”的困境。

6. 那些只有真跑起来才会遇到的问题

前面讲的都是设计层面的东西,这一节讲实际运行中冒出来的毛病,每个都是我在真实项目里遇到过的。

6.1 残影和轨迹混用时,透明度的双重衰减

当你先做一次destination-out擦除,再画轨迹线,会有一个隐蔽的副作用:轨迹线是半透明的(因为亮度衰减),它们叠加在被擦除过的画布上,多层半透明叠加会让中间的交叉点异常亮。粒子密集时,爆炸中心会出现一块过曝的白斑,失去层次。

解决办法是控制轨迹线的透明度下限,不要让每一段都带 alpha。我一般让线本身完全不透明,只靠颜色明度做衰减,这样叠加时是颜色覆盖而不是 alpha 累积,白斑问题基本消失。如果一定要用 alpha,就减少层数,别让几十条半透明线糊在同一小块区域。

6.2 移动端的高 DPI 和发热

手机上devicePixelRatio常见 2 到 3,画布物理像素轻松破千万。每帧一次全屏fillRect加上几百条线段的绘制,在低端机上会明显发热掉帧。实测下来,手机上把粒子数砍到桌面的一半、把轨迹点数从 10 降到 6、把画布分辨率封顶在 2 倍 dpr,帧率能稳定不少,视觉损失很小——因为小屏幕上本来也看不清太细的拉丝。

还有一个不容易想到的点:destination-out的擦除在全屏范围上比source-over的半透明填充更耗一点,因为涉及合成运算。移动端如果性能吃紧,可以直接用黑色覆盖法(前提是背景纯黑),省下一点开销。

6.3 粒子回收对象池必须上

烟花是高频生成销毁粒子的场景。一次爆炸 120 个粒子,一秒放两发,就是每秒几百个对象,如果每帧都new Particle(),GC 会周期性地卡一下,表现为烟花偶尔“顿一下”。

对象池是标准解法:预分配固定数量的粒子对象,死的标记为不活跃,需要时从池里取。轨迹法里每个粒子的Float32Array也随对象一起复用,不要每次新建。这个改动代码量不大,但对长时间运行的流畅度提升明显,属于必须做的优化。

6.4 页面不可见时要暂停

requestAnimationFrame在页面切到后台时会自动暂停,但有个陷阱:切回来时performance.now()的差值巨大。前面 4.4 节提到的dt上限就是为此。除此之外,最好监听visibilitychange,隐藏时直接停掉循环、记录时间戳,回来时重置last,这样既不浪费 CPU,也不会出现时间跳变。移动端浏览器尤其需要这个处理,否则后台滚几分钟再回来,整个画面会乱掉。

6.5 淡入时的第一帧

最后一个小细节:初始化时如果第一帧就做残影擦除,而画布是全新的,destination-out对空白画布无异于白干,没问题;但如果用黑色覆盖法,第一帧会在透明画布上铺一层半透明黑,后续的轨迹会显得发灰。解决办法是初始化时先完整清一次画布,再开始正常循环,把第一帧的状态归零。这种初始化顺序的小事,往往就是“别人跑出来好看我跑出来发灰”的原因。

总体思路就这些:两种拖尾各有脾气,残影法省事但要防脏、防帧率耦合,轨迹法精准但要管好内存和绘制次数;烟花的三段结构刚好让两者各得其所,上升用轨迹、爆炸用残影、下坠混合用。参数没有圣杯,靠的是理解每个数字背后的视觉含义,然后一次次试到你满意为止。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 3:45:55

Windows下Hadoop连接避坑:winutils.exe与hadoop.dll配置指南

简介&#xff1a;面向在 Windows 平台开展 Hadoop 跨系统调试、搭建本地开发环境的工程师与学习者&#xff0c;这份 winutils-master.zip 汇集 2.6.0 至 3.0.0 多个版本的 Windows 适配组件&#xff0c;可缓解原生 Hadoop 在 Windows 下缺少 winutils.exe、hadoop.dll 等依赖时…

作者头像 李华
网站建设 2026/10/27 3:45:06

Vibe Coding:AI时代高效代码审查的契约驱动法

1. 什么是 Vibe Coding&#xff1f;它不是偷懒&#xff0c;而是一种新型协作节奏“Vibe Coding”这个词最近在开发者社区里冒头&#xff0c;不是某个新框架、也不是某家大厂发布的工具&#xff0c;而是一种正在被大量一线工程师自发实践、却还没来得及被教科书定义的工作状态。…

作者头像 李华
网站建设 2026/10/27 3:44:08

解决Unity Cursor编辑器代码提示失效的实用指南

1. 问题现象与背景分析最近在Unity项目开发中使用Cursor编辑器时&#xff0c;不少开发者遇到了代码提示突然失效的情况。具体表现为&#xff1a;智能补全不显示、函数参数无提示、类型推断失效等。这个问题尤其影响Unity特有的API调用&#xff0c;比如GameObject、Transform等类…

作者头像 李华
网站建设 2026/10/27 3:44:02

数据库系统核心架构与实战优化指南

1. 数据库系统概念入门&#xff1a;从零开始的认知框架第一次接触数据库系统时&#xff0c;我被各种术语和概念搞得晕头转向。记得当时最困惑的问题是&#xff1a;为什么不能直接把数据存在Excel里&#xff1f;直到参与了一个用户订单管理系统项目&#xff0c;才真正理解数据库…

作者头像 李华
网站建设 2026/10/27 3:42:42

工业视觉落地四大支柱:数据、部署、硬件与运维

1. 这不是一场算法竞赛&#xff0c;而是一场工程耐力赛“计算机视觉进入下半场”——这句话最近半年在技术社区、产业峰会和甲方汇报PPT里高频出现。但很多人没意识到&#xff0c;它背后不是一句轻飘飘的趋势判断&#xff0c;而是过去五年密集堆算力、卷指标、发论文之后&#…

作者头像 李华
网站建设 2026/10/27 3:42:42

Windows下Git提交时nul文件问题的解决方案

1. 问题现象描述最近在Windows系统下使用Git提交代码时&#xff0c;遇到了一个令人头疼的问题&#xff1a;每次执行git add或git commit命令时&#xff0c;系统都会生成一个名为"nul"的奇怪文件&#xff0c;而且这个文件无法通过常规方式删除。错误提示显示"MS-…

作者头像 李华