畫源碼拆解:從粒子系統(tǒng)到可配置特效模塊)
簡(jiǎn)介這是一份面向前端開發(fā)者與網(wǎng)頁(yè)動(dòng)效愛好者的H5 Canvas粒子爆炸動(dòng)畫特效源碼包適合用于個(gè)人主頁(yè)、活動(dòng)頁(yè)或產(chǎn)品展示頁(yè)的背景動(dòng)畫開發(fā)幫助解決靜態(tài)頁(yè)面缺乏視覺沖擊力的問題。壓縮包共6個(gè)文件以3個(gè)JavaScript腳本為核心配合1個(gè)HTML與1個(gè)HTM頁(yè)面、1個(gè)CSS樣式表整體約59KB體積輕量便于直接嵌入現(xiàn)有項(xiàng)目。代碼圍繞Canvas 2D渲染上下文展開通過requestAnimationFrame驅(qū)動(dòng)動(dòng)畫循環(huán)構(gòu)建粒子系統(tǒng)模擬彩色粒子的爆炸、位移、變色與消散過程并可將文字輪廓拆解為粒子形成字符隨爆炸動(dòng)態(tài)聚合與散開的視覺效果。讀者可從中學(xué)習(xí)粒子對(duì)象的屬性定義、生命周期管理、重力與速度衰減等物理規(guī)則以及批量數(shù)組操作與閉包狀態(tài)保持的寫法。目前已有321人學(xué)習(xí)適合具備一定JavaScript基礎(chǔ)、希望掌握高級(jí)前端動(dòng)態(tài)效果制作技巧的開發(fā)者參考借鑒。1. 從一次頁(yè)面改版說起這個(gè)粒子爆炸動(dòng)畫到底能干什么前陣子幫朋友改一個(gè)活動(dòng)落地頁(yè)對(duì)方丟過來(lái)一個(gè)壓縮包說“就要那種點(diǎn)一下炸開的效果越炫越好但別太重”。打開一看就是這份 H5 Canvas 粒子爆炸動(dòng)畫特效。它干的事很純粹在 Canvas 畫布上生成一批粒子鼠標(biāo)點(diǎn)擊或頁(yè)面加載時(shí)粒子從中心向四周炸開帶拖尾、漸隱、重力回落最后自然消散。整個(gè)包不依賴任何框架純?cè)?JavaScript 加 Canvas 2D 繪制丟進(jìn)任何 H5 頁(yè)面、活動(dòng)頁(yè)、游戲加載頁(yè)都能直接用。適合誰(shuí)一是做營(yíng)銷活動(dòng)頁(yè)、需要點(diǎn)擊反饋動(dòng)效的前端二是學(xué) Canvas 動(dòng)畫、想搞懂粒子系統(tǒng)怎么寫的初學(xué)者三是手里有現(xiàn)成項(xiàng)目想找個(gè)輕量特效模塊嵌進(jìn)去的開發(fā)者。它不解決業(yè)務(wù)邏輯只解決“視覺炸裂”這一件事但這件事它做得足夠干凈代碼量小改起來(lái)不心疼。下面我按“能干什么 → 怎么跑 → 怎么改”的順序把這份源碼拆開講。2. 核心架構(gòu)拆解一個(gè)粒子從生成到消散的完整鏈路2.1 目錄結(jié)構(gòu)與文件職責(zé)拿到壓縮包解壓后結(jié)構(gòu)通常很扁平常見做法是下面這樣particle-explosion/ ├── index.html # 演示入口含 canvas 容器和基礎(chǔ)樣式 ├── css/ │ └── style.css # 頁(yè)面重置、canvas 全屏鋪滿 ├── js/ │ ├── particle.js # 粒子類位置、速度、顏色、生命周期 │ ├── explosion.js # 爆炸控制器生成粒子、驅(qū)動(dòng)動(dòng)畫循環(huán) │ └── main.js # 入口綁定事件、初始化畫布 └── README.mdindex.html只做兩件事放一個(gè)canvas元素按順序引入三個(gè) JS。style.css把 body 的 margin 清零canvas 設(shè)成display:block避免出現(xiàn)滾動(dòng)條——這是 Canvas 全屏動(dòng)畫最常見的翻車點(diǎn)很多人第一次跑發(fā)現(xiàn)右邊和下邊有白邊就是沒處理這個(gè)。particle.js是單個(gè)粒子的抽象explosion.js是粒子集合的管理器main.js負(fù)責(zé)把兩者和 DOM 事件接起來(lái)。這種分層的好處是你想換爆炸形狀只動(dòng)explosion.js的生成邏輯想改粒子外觀只動(dòng)particle.js的繪制方法互不干擾。2.2 粒子類位置、速度與生命周期的三要素粒子系統(tǒng)的核心就三個(gè)量位置、速度、壽命。位置決定畫在哪速度決定往哪飛壽命決定什么時(shí)候消失。看一段典型的粒子類實(shí)現(xiàn)// particle.js class Particle { constructor(x, y, options {}) { this.x x; // 當(dāng)前橫坐標(biāo) this.y y; // 當(dāng)前縱坐標(biāo) this.vx options.vx || 0; // 橫向速度分量 this.vy options.vy || 0; // 縱向速度分量 this.size options.size || 2; // 粒子半徑 this.color options.color || #ff6b35; this.life 1; // 生命值1 為滿血 this.decay options.decay || 0.015; // 每幀衰減量 this.gravity options.gravity || 0.05; // 重力加速度 } update() { this.vy this.gravity; // 縱向速度受重力影響 this.x this.vx; // 位置隨速度更新 this.y this.vy; this.life - this.decay; // 生命值遞減 } draw(ctx) { ctx.globalAlpha Math.max(this.life, 0); // 透明度跟隨生命值 ctx.fillStyle this.color; ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill(); } isDead() { return this.life 0; } }update()每幀調(diào)用一次先加重力再更新位置順序不能反——先更新位置再加重力粒子會(huì)顯得“飄”下落感不自然。draw()里用globalAlpha控制透明度粒子越接近死亡越透明這是漸隱效果的關(guān)鍵。decay和gravity是最值得調(diào)的兩個(gè)參數(shù)decay越大粒子消失越快gravity越大下落越明顯。我一般把decay設(shè)在 0.01 到 0.02 之間gravity設(shè)在 0.03 到 0.08 之間具體看頁(yè)面節(jié)奏。2.3 爆炸控制器批量生成與動(dòng)畫循環(huán)單個(gè)粒子沒意義爆炸是一批粒子同時(shí)朝不同方向飛。控制器負(fù)責(zé)在觸發(fā)點(diǎn)周圍生成 N 個(gè)粒子每個(gè)粒子的速度方向按角度均勻分布// explosion.js class Explosion { constructor(canvas) { this.canvas canvas; this.ctx canvas.getContext(2d); this.particles []; this.rafId null; } burst(x, y, count 80) { for (let i 0; i count; i) { // 角度均勻分布保證炸開是個(gè)圓 const angle (Math.PI * 2 * i) / count; // 速度加隨機(jī)擾動(dòng)避免過于規(guī)整 const speed 2 Math.random() * 4; this.particles.push(new Particle(x, y, { vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, size: 1 Math.random() * 2, color: hsl(${Math.random() * 60 10}, 90%, 60%), decay: 0.012 Math.random() * 0.008 })); } this.start(); } start() { if (this.rafId) return; // 防止重復(fù)啟動(dòng)多個(gè)循環(huán) const loop () { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // 倒序遍歷方便刪除死亡粒子 for (let i this.particles.length - 1; i 0; i--) { const p this.particles[i]; p.update(); p.draw(this.ctx); if (p.isDead()) this.particles.splice(i, 1); } if (this.particles.length 0) { this.rafId requestAnimationFrame(loop); } else { this.rafId null; // 粒子清空后停掉循環(huán)省性能 } }; this.rafId requestAnimationFrame(loop); } }burst()里角度用(Math.PI * 2 * i) / count均勻鋪開保證爆炸是個(gè)完整的圓而不是一坨。速度加Math.random()擾動(dòng)是為了避免所有粒子飛得一樣遠(yuǎn)看起來(lái)像機(jī)械的同心圓。顏色用 HSL 的色相隨機(jī)控制在暖色區(qū)間10 到 70出來(lái)的效果偏火焰橙紅符合“爆炸”的直覺。start()里有兩個(gè)細(xì)節(jié)值得說。一是if (this.rafId) return防止連續(xù)點(diǎn)擊時(shí)啟動(dòng)多個(gè)requestAnimationFrame循環(huán)那樣粒子會(huì)越跑越快是新手最容易踩的坑。二是粒子清空后把rafId置空并停止循環(huán)不然后臺(tái)會(huì)一直空轉(zhuǎn)頁(yè)面卡頓的元兇往往就在這。2.4 入口綁定畫布尺寸與事件接入main.js把畫布尺寸和事件接起來(lái)// main.js const canvas document.getElementById(stage); const explosion new Explosion(canvas); function resize() { canvas.width window.innerWidth; canvas.height window.innerHeight; } resize(); window.addEventListener(resize, resize); canvas.addEventListener(click, (e) { const rect canvas.getBoundingClientRect(); explosion.burst(e.clientX - rect.left, e.clientY - rect.top, 100); }); // 頁(yè)面加載后自動(dòng)炸一次作為入場(chǎng)動(dòng)效 window.addEventListener(load, () { explosion.burst(canvas.width / 2, canvas.height / 2, 120); });resize()里直接改canvas.width/height注意這會(huì)清空畫布內(nèi)容并重置坐標(biāo)系所以要在初始化時(shí)調(diào)用一次。點(diǎn)擊坐標(biāo)用getBoundingClientRect()換算不能直接用e.clientX因?yàn)?canvas 不一定貼在頁(yè)面左上角這是坐標(biāo)偏移問題的根源。load事件里自動(dòng)炸一次適合做首屏入場(chǎng)如果不需要?jiǎng)h掉即可。3. 跑起來(lái)本地啟動(dòng)、參數(shù)調(diào)試與效果驗(yàn)證3.1 三種運(yùn)行方式與選擇這份源碼是純靜態(tài)的不需要構(gòu)建工具。最省事的方式是直接雙擊index.html瀏覽器就能跑。但如果你后續(xù)要改代碼、加模塊建議起個(gè)本地服務(wù)避免file://協(xié)議下某些瀏覽器對(duì)資源的限制。用 Python 起服務(wù)# 在項(xiàng)目根目錄執(zhí)行Python 3 自帶 python -m http.server 8080 # 瀏覽器訪問 http://localhost:8080用 Node 的話裝個(gè)serve也行npx serve . # 默認(rèn) 3000 端口按提示訪問如果你打算把它嵌進(jìn)現(xiàn)有項(xiàng)目直接把js/下三個(gè)文件和 canvas 標(biāo)簽搬過去注意引入順序particle.js→explosion.js→main.js因?yàn)楹笳咭蕾嚽罢叩念惗x。3.2 關(guān)鍵參數(shù)速查與調(diào)參方向調(diào)參是這份特效最常做的事把幾個(gè)核心參數(shù)列出來(lái)改的時(shí)候心里有數(shù)參數(shù)位置默認(rèn)范圍調(diào)大效果調(diào)小效果countburst 調(diào)用處80~120粒子更多更密稀疏、性能更好speedexplosion.js2~6炸得更遠(yuǎn)聚在中心decayparticle.js0.012~0.02消失更快拖尾更久gravityparticle.js0.03~0.08下落明顯懸浮感強(qiáng)sizeparticle.js1~3粒子更粗更細(xì)膩調(diào)參有個(gè)血淚經(jīng)驗(yàn)別一次改好幾個(gè)改一個(gè)刷新看一次。粒子效果是多個(gè)參數(shù)疊加出來(lái)的同時(shí)改容易找不到是哪個(gè)起的作用。我一般先定count和speed定整體形態(tài)再調(diào)decay和gravity定節(jié)奏最后微調(diào)size和顏色。3.3 驗(yàn)證效果是否正常跑起來(lái)后正常表現(xiàn)是頁(yè)面加載后中心炸開一團(tuán)暖色粒子向四周飛散并逐漸透明消失點(diǎn)擊任意位置在點(diǎn)擊處再炸一次。如果出現(xiàn)下面幾種情況說明哪里不對(duì)粒子全部往一個(gè)方向飛檢查angle計(jì)算是不是漏了Math.PI * 2粒子不消失越積越多檢查isDead()和splice邏輯多半是decay沒生效點(diǎn)擊位置偏移檢查有沒有用getBoundingClientRect()換算連續(xù)點(diǎn)擊越來(lái)越快檢查start()里的rafId防重入判斷。4. 避坑與常見問題排查4.1 粒子越點(diǎn)越多、頁(yè)面越來(lái)越卡現(xiàn)象連續(xù)快速點(diǎn)擊后粒子數(shù)量暴漲幀率明顯下降。原因每次burst()都往this.particles里 push但沒有上限控制點(diǎn)擊頻率高時(shí)粒子數(shù)會(huì)失控。解決在burst()開頭加個(gè)上限判斷比如if (this.particles.length 500) return;或者用對(duì)象池復(fù)用死亡粒子避免頻繁創(chuàng)建銷毀。4.2 畫布模糊、粒子邊緣發(fā)虛現(xiàn)象在高分屏上粒子看起來(lái)糊邊緣不銳利。原因canvas 的 CSS 尺寸和實(shí)際像素尺寸不一致瀏覽器做了拉伸。解決按devicePixelRatio放大畫布實(shí)際尺寸再用ctx.scale()縮放坐標(biāo)系const dpr window.devicePixelRatio || 1; canvas.width window.innerWidth * dpr; canvas.height window.innerHeight * dpr; canvas.style.width window.innerWidth px; canvas.style.height window.innerHeight px; ctx.scale(dpr, dpr);4.3 頁(yè)面切到后臺(tái)再回來(lái)粒子瞬移或堆積現(xiàn)象切走標(biāo)簽頁(yè)一段時(shí)間再切回粒子位置突變或一次性涌出。原因requestAnimationFrame在后臺(tái)會(huì)暫停但update()里的速度是按幀累加的恢復(fù)后時(shí)間差沒補(bǔ)償。解決用時(shí)間戳計(jì)算 delta把速度乘以時(shí)間因子而不是每幀固定加。簡(jiǎn)單點(diǎn)也可以在visibilitychange事件里暫停和恢復(fù)動(dòng)畫。4.4 移動(dòng)端點(diǎn)擊沒反應(yīng)或延遲現(xiàn)象手機(jī)上點(diǎn)半天不炸或者炸的位置不對(duì)。原因移動(dòng)端有 300ms 點(diǎn)擊延遲且觸摸事件和鼠標(biāo)事件模型不同。解決改用touchstart或pointerdown事件pointerdown能同時(shí)覆蓋鼠標(biāo)和觸摸是現(xiàn)在更推薦的做法。坐標(biāo)換算邏輯不變e.clientX在 pointer 事件里同樣可用。4.5 嵌入現(xiàn)有頁(yè)面后樣式?jīng)_突現(xiàn)象搬進(jìn)項(xiàng)目后 canvas 尺寸不對(duì)或者被其他元素蓋住。原因canvas 默認(rèn)是 inline 元素受父容器 line-height 和字體影響且 z-index 沒設(shè)。解決給 canvas 加display:block; position:fixed; top:0; left:0; z-index:9999; pointer-events:none;pointer-events:none讓 canvas 不攔截點(diǎn)擊這樣它作為背景特效時(shí)不會(huì)擋住下面的按鈕。5. 二次開發(fā)從固定爆炸到可配置特效模塊把這份源碼改造成能復(fù)用的模塊是我拿到手后做的第一件事。原始代碼把參數(shù)寫死在類里換個(gè)頁(yè)面就得改源碼不優(yōu)雅。我的做法是抽一個(gè)配置對(duì)象出來(lái)讓調(diào)用方傳參// 改造后的調(diào)用方式 const fx new Explosion(canvas, { count: 100, speedRange: [2, 6], decayRange: [0.012, 0.02], gravity: 0.05, hueRange: [10, 70], // 暖色 autoBurst: true // 是否加載后自動(dòng)炸 }); // 想換冷色調(diào)只改配置 fx.setOptions({ hueRange: [180, 240] });配置化的關(guān)鍵是給每個(gè)參數(shù)一個(gè)合理的默認(rèn)值調(diào)用方不傳也能跑。hueRange控制色相區(qū)間暖色是 10 到 70冷色是 180 到 240改一個(gè)數(shù)組就能換風(fēng)格比翻源碼找顏色值高效得多。再進(jìn)一步可以把爆炸形狀從圓形改成其他圖案。核心在burst()里生成速度向量的方式。圓形是角度均勻分布想改成心形就把角度和速度按心形參數(shù)方程映射// 心形爆炸的速度方向映射示意 const t (Math.PI * 2 * i) / count; const heartX 16 * Math.pow(Math.sin(t), 3); const heartY -(13 * Math.cos(t) - 5 * Math.cos(2*t) - 2 * Math.cos(3*t) - Math.cos(4*t)); const scale 0.3; this.particles.push(new Particle(x, y, { vx: heartX * scale, vy: heartY * scale, // ...其余參數(shù) }));這樣粒子會(huì)朝心形輪廓飛散形成心形爆炸。同理?yè)Q成星形、文字輪廓都可以只要把目標(biāo)形狀的參數(shù)方程代進(jìn)去。這是粒子系統(tǒng)好玩的地方形狀邏輯和粒子邏輯解耦改形狀不影響粒子的生命周期管理。驗(yàn)證改造是否成功我一般用三步先看默認(rèn)配置下效果和原版一致再改一個(gè)配置項(xiàng)確認(rèn)生效最后在移動(dòng)端和桌面端各跑一遍確認(rèn)沒有平臺(tái)差異。這套流程走下來(lái)基本能保證模塊在別的項(xiàng)目里不會(huì)翻車。從那以后我每次拿到這類特效源碼都強(qiáng)制先跑通默認(rèn)效果、再抽配置、最后做跨端驗(yàn)證三步缺一不可。希望這份拆解幫到你拿到包能直接跑起來(lái)也能按自己的需求改出想要的樣子。本文還有配套的精品資源點(diǎn)擊獲取