简介:这是一份面向前端开发者与H5互动页面学习者的开红包特效实战资源,聚焦社交、电商场景中点击拆红包的完整交互实现。包内共9个文件,以5个png与1个jpg图片素材、1个js脚本、1个css样式表和1个html页面为主,压缩包约130KB,结构轻量,便于直接运行与二次改造。素材覆盖红包开启前后的多张状态图,脚本部分基于jQuery处理点击事件与DOM更新,样式表负责过渡与关键帧动画,页面文件串联整体流程。读者可从中了解CSS3动画与JavaScript事件如何配合完成拆包动效,学习随机金额生成、图片资源替换与响应式适配的基本思路,并参考模块化组织方式,将红包组件快速迁移到自己的活动页中。目前已有1075人学习下载,适合希望低成本掌握H5互动特效实现细节的前端入门与进阶开发者参考。
1. 拆开一个「h5开红包特效」:它到底在哪些场景里跑,谁需要它
春节活动页、直播间抽奖弹窗、线下扫码领券,用户点下去的那一瞬间,屏幕中央弹出一个红包,金色粒子炸开、红包翻转、金额数字滚动,最后停在「8.88 元」。这套动作就是通常说的 h5开红包特效。它不是一段 CSS 动画那么简单,而是「触发时机 + 视觉层叠 + 金额数据 + 兼容兜底」四件事咬在一起的结果。做活动运营的、做直播的前端 h5 的、接企业微信里嵌页面的,基本都会碰到它。真正难的地方不在动画本身,而在于:低端安卓机上粒子一多就掉帧,微信内置浏览器对某些合成属性支持不一致,金额还没从接口回来动画就已经跑完了。这篇就把这套特效从选型、实现、参数到踩坑完整走一遍,目标是你看完能直接在自己项目里复现,并且知道哪些参数不能乱调。
2. 红包特效的技术选型:CSS 动画、Canvas 还是 Lottie
2.1 三种主流方案的能力边界
在动手写第一行代码之前,先想清楚用哪条路。h5开红包特效的视觉构成一般分三层:底层是红包本体(图片或 DOM),中层是粒子/金币飞散,顶层是金额数字和按钮。不同层适合的技术不一样。
纯 CSS 动画(transform + opacity + keyframes)适合红包翻转、缩放、按钮呼吸这类结构简单的动作。优点是代码量小、GPU 合成友好、调试直观;缺点是粒子数量一多,DOM 节点爆炸,低端机上直接卡成幻灯片。我一般把 CSS 动画用在红包本体和按钮上,粒子另想办法。
Canvas 2D 适合粒子系统、金币雨、光晕扩散这类需要大量重复绘制的效果。一个 canvas 上画几百个粒子,性能远好于几百个 div。代价是要自己管 requestAnimationFrame 循环、自己处理 devicePixelRatio,代码复杂度上去了。
Lottie 适合设计师已经用 AE 做好、导出成 JSON 的复杂序列动画。它的优势是还原度高、设计师能直接交付;劣势是 JSON 体积可能几百 KB,首屏加载慢,而且动态改金额数字这类需求不好接。如果活动页对包体积敏感,Lottie 要谨慎。
| 方案 | 适合层 | 包体积 | 低端机表现 | 动态改数据 |
|---|---|---|---|---|
| CSS 动画 | 红包本体、按钮 | 极小 | 好 | 容易 |
| Canvas 2D | 粒子、金币雨 | 小 | 较好 | 容易 |
| Lottie | 整体序列动画 | 较大 | 一般 | 较难 |
2.2 我的组合策略与选型理由
实际项目里我不会只用一种。常见做法是:红包本体用一张 webp 图 + CSS transform 做翻转和缩放,粒子层单独开一个绝对定位的 canvas 盖在上面,金额数字用 DOM 做滚动。这样三层各司其职,任何一层出问题都好定位。
为什么红包本体不用 canvas 画?因为红包的视觉细节(纹理、阴影、圆角)用图片还原成本最低,而且图片可以被浏览器缓存。为什么粒子一定要用 canvas?因为粒子数量通常在 80 到 300 之间,用 DOM 的话每次重排重绘都是灾难。为什么金额用 DOM 而不是画在 canvas 上?因为金额需要能被无障碍读取、能被自动化测试抓到、字体渲染也更清晰。
选型定下来之后,接下来就是把这套结构真正搭起来。下一章从零开始写一个能跑的最小版本。
3. 从零实现一个可复用的开红包特效
3.1 页面结构与层级约定
先把 DOM 骨架定好。层级关系决定了 z-index 和定位方式,一开始乱后面很难改。我的习惯是三层容器:外层负责全屏遮罩和居中,中层放红包本体,粒子 canvas 单独一层盖在最上面但 pointer-events 设为 none,避免挡住按钮点击。
<!-- 红包特效容器:fixed 全屏,默认隐藏 --> <div id="redpacket-layer" class="rp-layer" aria-hidden="true"> <!-- 遮罩:点击可关闭,也可设为不可关闭 --> <div class="rp-mask"></div> <!-- 红包本体:用图片 + CSS 动画 --> <div class="rp-body"> <img class="rp-cover" src="./redpacket.webp" alt="红包" /> <div class="rp-amount"><span id="rp-number">0.00</span> 元</div> <button id="rp-open-btn" class="rp-btn">开</button> </div> <!-- 粒子层:盖在最上面,不接收点击 --> <canvas id="rp-canvas" class="rp-canvas"></canvas> </div>这段结构里,rp-layer用position: fixed铺满视口,rp-canvas用pointer-events: none保证不拦截按钮。aria-hidden在隐藏时设为 true,显示时改 false,兼顾无障碍。红包本体用图片而不是纯 CSS 画,是为了还原设计稿的质感。
3.2 红包翻转与金额滚动的 CSS 实现
红包「开」的动作,本质是封面向上翻开或缩放消失,露出金额。我用的是封面rotateX翻转配合scale,时长控制在 400ms 左右,太快看不清,太慢用户等得烦。
.rp-layer { position: fixed; inset: 0; display: none; z-index: 9999; } .rp-layer.show { display: flex; align-items: center; justify-content: center; } .rp-mask { position: absolute; inset: 0; background: rgba(0,0,0,.55); } .rp-body { position: relative; width: 60vw; max-width: 320px; text-align: center; } .rp-cover { width: 100%; display: block; transform-origin: top center; transition: transform .4s cubic-bezier(.4,0,.2,1), opacity .4s; } /* 打开后封面向上翻走 */ .rp-body.opened .rp-cover { transform: rotateX(-120deg) scale(.9); opacity: 0; } .rp-amount { font-size: 32px; font-weight: 700; color: #ffd24d; opacity: 0; transition: opacity .3s .2s; } .rp-body.opened .rp-amount { opacity: 1; } .rp-canvas { position: absolute; inset: 0; pointer-events: none; }这里的关键参数是transform-origin: top center,让封面从顶部翻下去,符合「掀开红包盖」的直觉。cubic-bezier(.4,0,.2,1)是常用的缓出曲线,起步快、收尾稳。金额的transition-delay设 0.2s,等封面翻到一半再出现,视觉上更连贯。注意rotateX在部分老安卓 WebView 上需要加-webkit-前缀,构建工具一般会自动补,但手写时别漏。
3.3 Canvas 粒子系统的核心循环
粒子是这套特效里最吃性能的部分。核心思路是:维护一个粒子数组,每帧更新位置和透明度,画到 canvas 上,生命周期结束就移除。下面是一个精简但可用的实现。
// 初始化 canvas,处理高清屏模糊问题 function initCanvas(canvas) { const dpr = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); // 关键:按 dpr 缩放,否则高清屏发虚 return { ctx, w: rect.width, h: rect.height }; } // 生成一批粒子,从中心向外飞散 function spawnParticles(count, w, h) { const cx = w / 2, cy = h / 2; const list = []; for (let i = 0; i < count; i++) { const angle = Math.random() * Math.PI * 2; const speed = 2 + Math.random() * 4; list.push({ x: cx, y: cy, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 2, // 整体略微向上 life: 1, // 生命值,从 1 衰减到 0 decay: 0.012 + Math.random() * 0.01, size: 2 + Math.random() * 3, color: Math.random() > 0.5 ? '#ffd24d' : '#ff5a5a' }); } return list; } // 每帧更新并绘制 function tick(ctx, particles, w, h) { ctx.clearRect(0, 0, w, h); for (let i = particles.length - 1; i >= 0; i--) { const p = particles[i]; p.x += p.vx; p.y += p.vy; p.vy += 0.12; // 重力,让粒子有下落感 p.life -= p.decay; if (p.life <= 0) { particles.splice(i, 1); continue; } ctx.globalAlpha = p.life; ctx.fillStyle = p.color; ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1; }逻辑说明:initCanvas里按devicePixelRatio放大画布再scale回来,这是高清屏不发虚的关键,漏了这步在 iPhone 上粒子边缘会糊。spawnParticles用极坐标随机方向,vy额外减 2 让粒子整体偏上,模拟「喷出来」的感觉。tick里重力0.12是调过的值,太大粒子掉得太快没有飘散感,太小会一直往上飘。decay控制粒子存活时长,0.012 大约对应 80 帧、1.3 秒左右。
参数怎么改:粒子数量建议 80 到 150,超过 200 在千元机上会明显掉帧;speed范围 2 到 6 比较自然;重力 0.1 到 0.15 之间微调。这些值没有标准答案,拿真机试。
3.4 触发时机与金额数据的对接
动画再好看,如果金额还没从接口回来就开跑,用户会看到「0.00」闪一下再跳成真实金额,体验很差。正确顺序是:用户点击 → 请求接口 → 拿到金额 → 先渲染金额 → 再启动动画。
async function openRedPacket() { const layer = document.getElementById('redpacket-layer'); const body = layer.querySelector('.rp-body'); const numberEl = document.getElementById('rp-number'); const btn = document.getElementById('rp-open-btn'); btn.disabled = true; // 防重复点击 try { const res = await fetch('/api/redpacket/draw', { method: 'POST' }); const data = await res.json(); if (!data || typeof data.amount !== 'number') throw new Error('bad data'); // 先写金额,再触发动画,避免数字跳变 numberEl.textContent = data.amount.toFixed(2); body.classList.add('opened'); // 动画启动后再跑粒子 const canvas = document.getElementById('rp-canvas'); const { ctx, w, h } = initCanvas(canvas); const particles = spawnParticles(120, w, h); let rafId; (function loop() { tick(ctx, particles, w, h); if (particles.length) rafId = requestAnimationFrame(loop); else cancelAnimationFrame(rafId); })(); } catch (e) { // 失败兜底:提示重试,不要卡在 loading btn.disabled = false; alert('网络开小差了,请重试'); } }这段代码里几个细节值得说:btn.disabled = true是防连点的第一道闸,接口慢的时候用户会狂点。金额先写 DOM 再classList.add('opened'),保证动画开始时数字已经是对的。粒子循环用requestAnimationFrame而不是setInterval,前者跟随屏幕刷新率,后台标签页会自动暂停,省电。失败分支一定要把按钮恢复可点,否则用户卡死只能刷新页面。
到这里一个能跑的最小版本就完整了。但能跑不等于线上稳,下一章专门讲那些真机上才会暴露的坑。
4. 真机上的避坑清单:从掉帧到微信兼容
4.1 低端安卓机粒子掉帧
现象:开发机上丝滑,一到千元安卓机上粒子变成一格一格,甚至整个页面卡住。
原因:粒子数量过多,或者每帧都在创建新对象触发 GC。spawnParticles里如果每帧都push新粒子,垃圾回收会周期性卡顿。
解决:把粒子数量压到 80 到 120;粒子对象复用,用对象池而不是每帧新建;tick里避免splice频繁操作大数组,可以标记死亡后统一清理。另外把 canvas 尺寸控制在红包区域大小,不要铺满全屏,绘制面积小一半性能就好一截。
4.2 微信内置浏览器的合成层问题
现象:红包翻转动画在微信里出现闪烁、残影,或者翻转时红包图片撕裂。
原因:微信 X5 内核(部分安卓机型)对transform和opacity的合成层处理不一致,尤其是元素同时有border-radius和overflow: hidden时。
解决:给做动画的元素加will-change: transform或transform: translateZ(0)强制提升为合成层;避免在动画元素上同时用box-shadow和border-radius;如果还是闪,把翻转改成scale+opacity的组合,牺牲一点立体感换稳定。这个坑没有银弹,只能真机测。
4.3 金额接口慢导致的动画错位
现象:用户点了「开」,红包先翻开了,金额过了两秒才跳出来,中间显示 0.00。
原因:动画触发和接口返回没有做顺序控制,代码里先add('opened')再await fetch。
解决:严格按 3.4 的顺序,先await拿到金额、写进 DOM,再触发动画。如果接口确实慢,加一个 loading 态(比如按钮转圈),让用户知道在请求,而不是干等。接口超时设 3 秒,超时走兜底文案。
4.4 重复点击导致多次抽奖
现象:用户手快点了三下,接口被调了三次,可能多领红包或报错。
原因:只靠btn.disabled不够,因为异步请求发出到按钮禁用之间有时间差,快速点击仍可能穿透。
解决:在函数入口用一个同步的标志位锁,比如if (isOpening) return; isOpening = true;,请求结束或失败再置回 false。disabled是给用户看的视觉反馈,标志位才是真正的闸。
4.5 高清屏 canvas 发虚
现象:iPhone 上粒子边缘模糊,像蒙了一层雾。
原因:canvas 的width/height用的是 CSS 像素,没有乘以devicePixelRatio。
解决:就是 3.3 里initCanvas的做法,先按 dpr 放大画布尺寸,再ctx.scale(dpr, dpr)。注意每次窗口 resize 或横竖屏切换都要重新初始化,否则尺寸对不上。
5. 进阶:把开红包特效做成可配置、可复用的组件
5.1 参数外置与主题切换
线上活动经常要换皮:春节红色、618 橙色、双十一紫色。如果每次都改源码,维护成本很高。我的做法是把颜色、粒子数量、动画时长抽成配置对象,组件初始化时传入。
const RP_CONFIG = { theme: { particleColors: ['#ffd24d', '#ff5a5a'], amountColor: '#ffd24d', maskOpacity: 0.55 }, particleCount: 120, gravity: 0.12, flipDuration: 400, // 与 CSS transition 时长保持一致 apiTimeout: 3000 }; function createRedPacket(userConfig = {}) { const cfg = { ...RP_CONFIG, ...userConfig }; // 把主题色写进 CSS 变量,CSS 里用 var() 引用 const layer = document.getElementById('redpacket-layer'); layer.style.setProperty('--rp-amount-color', cfg.theme.amountColor); layer.style.setProperty('--rp-mask-opacity', cfg.theme.maskOpacity); return { cfg, open: () => openRedPacket(cfg) }; }配置外置之后,换主题只改一个对象,不用碰动画逻辑。flipDuration要和 CSS 里的transition时长对齐,否则金额出现的时机和封面翻转对不上。CSS 变量在微信内置浏览器里支持良好,可以放心用。
5.2 用真机验证代替「我觉得没问题」
特效类需求最忌讳只在 Chrome DevTools 里看。DevTools 的模拟器性能远好于真机,很多掉帧、闪烁问题在模拟器里根本复现不出来。我的习惯是准备三台设备:一台 iPhone(看高清屏和 Safari 表现)、一台中端安卓(看主流表现)、一台千元机(看性能下限)。每次改完动画参数,三台都过一遍。
验证时重点看三个指标:动画是否掉帧(用 Chrome 远程调试看 FPS)、金额出现时机是否和动画对齐、连续点击是否只触发一次。这三个过了,基本就能上线。
5.3 一个我踩过的教训
早期做这类特效,我总想把粒子调得越多越炫,觉得「炸得越满越有氛围」。结果一次春节活动,千元机上直接卡到用户以为页面死了,投诉一堆。后来我把粒子数量从 300 砍到 100,反而没人说不好看,因为动画流畅本身就是体验的一部分。特效这东西,克制比堆料难,也更值钱。参数能调小就别调大,真机跑一遍再决定。希望帮到你。
本文还有配套的精品资源,点击获取