简介:H5开红包特效是一套面向网页前端开发者的轻量级互动组件代码,服务于社交活动、电商营销等需要模拟开红包玩法的场景,解决从零搭建交互逻辑与视觉动效的重复劳动问题,覆盖从点击到展示结果的全流程。压缩包共9个文件,包含5张png图片、1张jpg背景、1个css样式表、1个html入口页面及1个jquery-1.10.1.min.js脚本,整体仅130KB,素材与逻辑分离,结构清晰方便直接引入现有项目。实现上利用CSS3动画完成红包开启过程,配合jQuery处理点击事件,借助Math.random生成随机金额,并通过DOM操作动态更新祝福语和结果区域,代码简洁易懂,便于按需替换图片、调整动效或扩展新玩法。已有1077人学习下载,适合需要快速上线红包互动页面、又想保留二次开发灵活性的前端开发者参考使用。
1. 开红包H5到底在做什么:一次点击背后的动效链路
同样是开红包,有的页面用户拆完就划走,有的页面用户拆完会录屏发出去,差别不在金额,而在拆开那一下的反馈节奏。h5开红包特效不是把红包画得多好看,而是用动效把「点击 - 拆开 - 揭晓」串成一个有层次的即时反馈链路:手按下去要有触感,盖子翻开要有惯性,金币飞出来要有重量,金额亮出来要有停顿。这篇文章面向要做活动页、抽奖页的开发者,把翻盖、粒子、音效、震动的完整实现和边界讲透。新手能照着建出第一个能跑起来的红包,熟手能直接抄走参数和避坑清单。
2. 红包翻盖与结果弹出:CSS+JS 把核心交互先跑通
2.1 拆三层结构:红包盖、结果层与合成层的关系
做开红包特效第一件事不是写动画,而是把页面拆成三层:红包盖负责「关上」的状态,结果层负责「打开后」的内容,特效层负责金光和金币。三层在视觉上重叠在同一个容器里,只有红包盖需要 3D 变换,其他两层保持平面。这个结构决定了后面所有动画的复杂度上限。
我一般用.redpacket作为定位容器,里面放rp-cover和rp-result,特效 Canvas 单独铺在容器上方。红包盖用绝对定位盖住结果层,初始状态是盖上的,点击后盖子弹开,结果层再放大显现。动画全部用transform和opacity,这是移动端性能的第一条底线:这两者走合成器,不触发 layout 和 paint 重算。
<div class="redpacket" id="redpacket"> <div class="rp-cover"> <span class="rp-seal">開</span> </div> <div class="rp-result"> <p class="rp-amount">88.88</p> </div> </div>.redpacket { position: relative; width: 320px; height: 420px; margin: 80px auto; perspective: 800px; touch-action: manipulation; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; } .rp-cover { position: absolute; inset: 0; background: linear-gradient(160deg, #e8433a, #c0392b); border-radius: 16px; z-index: 2; transform-origin: top center; transition: transform 0.55s cubic-bezier(0.22, 1.4, 0.36, 1), opacity 0.45s ease; will-change: transform; backface-visibility: hidden; -webkit-backface-visibility: hidden; } .rp-cover.open { transform: rotateX(-25deg) scale(0.92) translateY(-20px); opacity: 0.4; } .rp-result { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: linear-gradient(180deg, #fff4e6, #ffe0c2); border-radius: 16px; opacity: 0; transform: scale(0.8); transition: opacity 0.3s ease 0.15s, transform 0.5s cubic-bezier(0.18, 1.6, 0.4, 1) 0.1s; } .rp-result.show { opacity: 1; transform: scale(1); } .rp-seal { position: absolute; left: 50%; top: 50%; width: 72px; height: 72px; margin: -36px 0 0 -36px; background: #f1c40f; color: #b8860b; font-size: 36px; font-weight: 700; text-align: center; line-height: 72px; border-radius: 50%; box-shadow: inset 0 -3px 0 rgba(0, 0, 0, 0.15); }2.2 transform-origin 与 cubic-bezier:翻盖动效的两个关键参数
红包盖翻开的观感好坏,90% 由transform-origin和transition-timing-function决定,不是由动画时长决定。transform-origin: top center让盖子绕着顶边翻转,这是「盖子从下往上掀开」的物理基础。如果改成center,整个盖子会原地旋转,观感变成「牌子转动」,不像红包。
cubic-bezier(0.22, 1.4, 0.36, 1)是一个前段快、后段过冲的缓动,模拟塑料薄片翻过去之后轻微回弹的惯性。第二个参数1.4大于 1,意味着中间某个时刻变换值会超过终点再拉回来,这种过冲是「有手感」的关键。普通ease是一潭死水,linear像机械臂,都不适合这种轻量翻盖。
const packet = document.getElementById('redpacket'); const cover = document.querySelector('.rp-cover'); const resultEl = document.querySelector('.rp-result'); let opened = false; packet.addEventListener('pointerdown', () => { if (opened) return; opened = true; cover.classList.add('open'); resultEl.classList.add('show'); // 把事件广播出去,给粒子层和音效层用 packet.dispatchEvent(new CustomEvent('packet-open', { detail: { x: packet.clientWidth / 2, y: 180 } })); });pointerdown在移动端比click提前约 100ms 触发,且不受 300ms 延迟影响,配合 CSS 里的touch-action: manipulation能同时干掉双击缩放。opened标志位防止用户连续点击导致动画重放,这是所有拆红包页面都必须有的状态锁。事件广播用CustomEvent而不是直接调用粒子层函数,好处是红包动效和特效层解耦,之后换掉任何一种特效都不动主流程。
2.3 pointer 事件接管点击:去掉 300ms 延迟与误触
移动端 H5 里最坑的交互陷阱是click事件:部分老 WebView 在手指抬起后要等 300ms 才判断是否双击,期间整个页面像卡住一样。用pointerdown接管触发,动画立刻响应,误触概率也低,因为它在手指接触屏幕的瞬间就执行了。
注意pointerdown的副作用:用户可能只是滑动屏幕路过红包,手指一碰就触发了开红包。所以我会在容器上加touch-action: manipulation,它表示这个区域的触摸操作只处理点击和滚动,不做浏览器默认的缩放识别。如果业务上要求「滑过不触发、按下才触发」,就再加一层位移判断:记录pointerdown时的坐标,pointerup时位移小于 15px 才算有效点击。
let startX = 0; let startY = 0; packet.addEventListener('pointerdown', (e) => { startX = e.clientX; startY = e.clientY; }); packet.addEventListener('pointerup', (e) => { const dx = Math.abs(e.clientX - startX); const dy = Math.abs(e.clientY - startY); if (dx < 15 && dy < 15 && !opened) { // 视为有效点击,触发开红包逻辑 packet.dispatchEvent(new CustomEvent('tap-confirm')); } });这个场景下还有一个细节:红包容器不应该拦截整个页面的滚动。pointerdown里不要调用preventDefault,否则 Android WebView 上整个页面会失去滚动手势。正确的做法是只处理事件、不阻止默认行为,再配合 CSS 的touch-action控制浏览器手势。真机测试时重点试「手指按在红包上然后上下滑动页面」,如果页面不滚就是事件处理越界了。
3. 用 Canvas 粒子把特效撑起来:金光、金币与彩带的参数化实现
3.1 粒子系统的最小模型:位置、速度、生命周期
盖子弹开只是骨架,真正让用户觉得「这红包高级」的是那一下迸发出来的金光和金币。这部分我不建议用 GIF 或序列帧,体积大、缩放糊、还不能动态调颜色。Canvas 粒子系统是移动端 H5 做爆发特效的常见方案,一套粒子模型能同时模拟金币、星芒、彩带三种效果,差别只在参数。
粒子系统的最小模型是四个属性:位置(x/y)、速度(vx/vy)、生命周期(life/decay)、外观(颜色/大小/旋转)。每一帧先更新速度和位置,再按衰减系数减少生命值,生命值归零就移出数组。这套模型在桌面端和移动端通用,Canvas 2D 就能跑,不需要引入任何图形库。
const canvas = document.getElementById('fx-layer'); const ctx = canvas.getContext('2d'); const DPR = Math.min(window.devicePixelRatio || 1, 2); const PARTICLES = []; const COLORS = ['#ffd54f', '#ffb300', '#ff8f00', '#ffca28', '#ffe082']; function resizeCanvas() { const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * DPR; canvas.height = rect.height * DPR; ctx.setTransform(DPR, 0, 0, DPR, 0, 0); } function spawnBurst(cx, cy, count) { for (let i = 0; i < count; i++) { const angle = Math.random() * Math.PI * 2; const speed = 5 + Math.random() * 10; PARTICLES.push({ x: cx, y: cy, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 7, // 先向上窜 size: 3 + Math.random() * 6, color: COLORS[Math.floor(Math.random() * COLORS.length)], life: 1, decay: 0.006 + Math.random() * 0.012, rot: Math.random() * Math.PI, vr: (Math.random() - 0.5) * 0.2 }); } } function update() { ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight); for (let i = PARTICLES.length - 1; i >= 0; i--) { const p = PARTICLES[i]; p.vy += 0.22; // 重力加速度 p.vx *= 0.99; // 空气阻力 p.x += p.vx; p.y += p.vy; p.rot += p.vr; p.life -= p.decay; if (p.life <= 0) { PARTICLES.splice(i, 1); continue; } ctx.save(); ctx.globalAlpha = Math.max(0, p.life); ctx.translate(p.x, p.y); ctx.rotate(p.rot); ctx.fillStyle = p.color; ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size); ctx.restore(); } requestAnimationFrame(update); } function burst(cx, cy) { spawnBurst(cx, cy, 36); if (PARTICLES.length <= 36) { requestAnimationFrame(update); } }这段代码里有三个参数决定观感,调的时候不要乱动逻辑。p.vy += 0.22是重力,数值越大粒子落得越快,过大像下雨,过小像太空漂浮,0.18 到 0.26 是金币掉落的安全区间。speed的随机范围 5 到 15 决定爆发半径,移动端建议上限不超过 18,否则粒子飞出屏幕外,用户只能看到零星几个。decay是生命衰减,0.006 到 0.018 对应粒子可见时长约 0.6 秒到 1.6 秒,金光适合短而亮,金币适合长而缓。
提示:
fillRect画方块来代替金币贴图,看起来太硬?把fillRect换成ctx.arc加fill就是圆点金币,成本不变。
3.2 调参是玄学?把金光参数化的四个调节点
很多开发者调粒子特效时是「改个数看一眼,再改一个」,最后调出来的效果说不上哪里怪。我把这套特效拆成四个参数调节点,每个点只影响一个维度,出问题不用瞎猜。
第一个是发射范围。angle用Math.random() * Math.PI * 2是 360 度全向发射,适合「炸开」的感觉。如果想要「向上喷涌」的效果,把 angle 范围压到-Math.PI * 0.6到-Math.PI * 0.4。第二个是速度分布。均匀分布会让粒子轨迹雷同,我习惯用Math.pow(Math.random(), 0.6)做加权:少数粒子跑得远,多数粒子集中在爆心附近,这样视觉重心稳,画面不散。第三个是尺寸层级。大粒子 6px 以上占比不超过 20%,小的 3px 做背景闪烁,大小混搭才有景深。第四个是颜色数量。一次爆发控制在 4 到 6 个相邻色相,比如金色系就只在#ffd54f到#ff8f00之间取,颜色跨度太大立刻变成「彩带派对」,和红包质感不匹配。
function spawnBurst(cx, cy, count, config) { const { angleRange = Math.PI * 2, angleOffset = 0, speedMin = 5, speedMax = 15, sizeMin = 3, sizeMax = 9 } = config; for (let i = 0; i < count; i++) { const angle = angleOffset + Math.random() * angleRange; const speed = speedMin + Math.pow(Math.random(), 0.6) * (speedMax - speedMin); // ... 其余属性同上 } }这样设计之后,活动运营说「金光不够多」「金币太飘」这类模糊需求就能直接对到参数:不够多就加count,太飘就降speedMax或者抬vy的初始负值,不再靠玄学改代码。
3.3 Canvas 层与红包 DOM 的层级和时间线对齐
Canvas 画布要放在红包容器内、红包盖的下方还是上方,直接影响效果。我的方案是让 Canvas 绝对定位在.redpacket容器内、红包盖的下方、结果层的上方,粒子从红包中间炸出来时,一部分会被翻开的盖子遮挡,产生「从红包里喷出」的层次感。如果 Canvas 盖在最上层,粒子就变成「贴在红包表面」,立体感直接丢一半。
时间线对齐是另一个容易翻车的地方。红包盖翻开动画时长 0.55s,结果层内容要在第 0.5s 时才开始显现,而粒子最好在pointerdown后 100ms 内爆发——那时盖子刚离开视线,粒子正好占据视觉焦点,金额慢慢出来。这个顺序我一般用一个轻量状态机管理,而不是散落的setTimeout:
const timeline = { opened: false, run() { if (this.opened) return; this.opened = true; cover.classList.add('open'); // 0ms: 翻开盖子 burst(160, 180, 36); // 100ms: 粒子爆发 setTimeout(() => { resultEl.classList.add('show'); // 400ms: 金额显现 }, 400); } }; packet.addEventListener('packet-open', () => timeline.run());用状态机的原因是:用户可能在第 200ms 时又触发一次pointerdown,如果只靠setTimeout散落管理,第二次触发会让结果层提前出现,甚至粒子系统重复累积同一个位置的粒子导致掉帧。状态锁opened保证整条时间线只执行一次,后续所有入口都要先过这个锁。
注意:Canvas 的
burst事件源坐标要用「红包容器内的相对坐标」,不是event.clientX的页面坐标。容器可能做不到全屏居中,算错坐标的后果是粒子从红包外 100px 的地方炸开,一眼假。
4. 开红包特效避坑清单:移动端兼容性、性能与交互细节
4.1 现象:iOS 上红包盖动效开始前闪一下原样
拆开封面动画前,红包盖在屏幕上闪了大约一帧的「盖子弹开后的样子」,然后立刻跳回闭合状态,再正常播放动画。这个现象在部分 iOS 设备上概率出现,尤其在低电量或系统内存吃紧时更频繁。
原因:翻盖动画涉及 3D transform,iOS Safari 在首次渲染时没有为元素提前建立合成层,动画首帧直接暴露了元素的默认状态,动画开始时缓存层才补上,导致闪帧。
解决:给红包盖加上will-change: transform和translateZ(0)强制提前建层。will-change是给浏览器的提示,translateZ(0)是兜底,两者配合在 iOS 上效果稳定。
.rp-cover { will-change: transform; transform: translateZ(0); -webkit-transform: translateZ(0); }4.2 现象:低端安卓机上 Canvas 粒子掉帧成幻灯片
中等价位 Android 手机上,金币爆发的瞬间帧率跌到 20fps 以下,整个页面像是隔两秒才跳一帧。桌面 Chrome 模拟器里完全复现不了,因为桌面 CPU 和移动 GPU 的处理差异只在真机上暴露。
原因:第一个坑是 Canvas 尺寸没有做 devicePixelRatio 适配,画布尺寸等于 CSS 尺寸,在高 DPR 屏幕上被拉伸放大,每个粒子都模糊,必须用 DPR 放大画布;第二个更大的坑是ctx.shadowBlur,这个属性在移动 GPU 上走慢路径,单个粒子的阴影渲染成本接近绘制本身的十几倍。
解决:DPR 上限截到 2,避免 3 倍屏产生过大的像素填充。粒子绘制全部去掉shadowBlur,用预渲染离屏 Sprite 实现光晕:提前把带渐变的光斑画到一个小画布上,主循环里drawImage,速度远快于每帧重新计算渐变。
const DPR = Math.min(window.devicePixelRatio || 1, 2); // 预渲染一个 32x32 的光斑 function buildGlowSprite() { const off = document.createElement('canvas'); off.width = 32; off.height = 32; const octx = off.getContext('2d'); const radius = 16; const gradient = octx.createRadialGradient(16, 16, 2, 16, 16, radius); gradient.addColorStop(0, 'rgba(255, 200, 50, 1)'); gradient.addColorStop(0.6, 'rgba(255, 180, 20, 0.4)'); gradient.addColorStop(1, 'rgba(255, 180, 20, 0)'); octx.fillStyle = gradient; octx.fillRect(0, 0, 32, 32); return off; }主循环里用ctx.drawImage(sprite, p.x - 16, p.y - 16, 32, 32)替代fillRect,单粒子绘制成本从「路径 + 渐变 + 阴影」降到一次位图拷贝,粒子数量可以翻倍。注意:Canvas 的高刷新最好只跑在特效期间的几百毫秒,特效结束就取消下一帧,不要让requestAnimationFrame空转一整晚,空转的耗电不可忽略。
4.3 现象:点击红包时页面跟着滚动、双击缩放
手指点到红包上,页面轻微抖动了一下,手指快速点两下时旁边的文字被放大了。这类问题在活动页里非常常见,用户在拆红包时会被意外的缩放打断操作。
原因:click和部分触摸手势触发了浏览器的默认行为,包括 300ms 延迟后的双击缩放,以及滚动穿透。根子是没把「点击」和「浏览器手势」的边界划清楚。
解决:三步齐全。第一步在红包容器上写touch-action: manipulation,第二步给可点击元素设置-webkit-tap-highlight-color: transparent去掉点击灰色蒙层,第三步对页面根节点设置overscroll-behavior: none阻止滚动链传播。
.redpacket { touch-action: manipulation; -webkit-tap-highlight-color: transparent; } html, body { overscroll-behavior: none; }touch-action不是preventDefault,它不拦截滚动,只告诉浏览器这个区域不需要给你处理双击缩放和双击滚动。这样手指在红包上滑动页面依然滚动,但快速点按不会触发缩放,交互干净且不牺牲页面可用性。
4.4 现象:拆开瞬间的音效和震动被系统拦截
第一次点击红包时没声音,第二次点才出声音;震动有时生效有时失效。这个现象在不同系统上也不一样,iOS 上比较少见,Android WebView 上频繁。
原因:Web Audio 规范强制要求音频上下文在用户手势内创建或恢复。第一次pointerdown如果只是播放动画、在事件外延迟创建AudioContext,浏览器会判定为「非用户手势触发的自动播放」,直接把声音掐掉。震动失效是同一类问题:部分 WebView 要求navigator.vibrate在手势栈内调用。
解决:把音频上下文创建和震动调用都放进pointerdown的主流程里,在动画触发的同时完成,不要在setTimeout回调里做。
let audioCtx = null; function ensureAudio() { if (!audioCtx) { const Ctor = window.AudioContext || window.webkitAudioContext; if (Ctor) audioCtx = new Ctor(); } if (audioCtx && audioCtx.state === 'suspended') { audioCtx.resume(); } return audioCtx; }震动调用写在同一同步流程里:if (navigator.vibrate) navigator.vibrate([40, 30, 80]);。不要先await再震动,任何异步步骤都会把手势栈弄丢。iOS Safari 完全不支持navigator.vibrate,调用前必须判空,并且把震动设计成「锦上添花」而非「必要反馈」,否则 iOS 用户会以为自己的手机坏了。
4.5 现象:金币还没落地,结果金额就弹出来了
动画投入太多精力在「好看」上,结果层弹出的时间却定死在某个固定值。动画慢设备上盖子弹到一半,金额已经显示出来,用户一边看盖子飞一边看数字,注意力完全被撕开。
原因:结果层显现用固定延迟,和封面动画的实际进度脱钩。网络差、主线程忙时,CSS 动画时长被拉长,固定延迟毫秒级偏移就会造成视觉错位。
解决:用transitionend事件或动画中间点作为结果层的触发时机。简单做法是监听封面动画的事件回调,在回调里让结果层入场,确保顺序是严格的「先合上 → 打开 → 再显金额」。
cover.addEventListener('transitionend', (e) => { if (e.propertyName === 'transform' && !resultShown) { resultShown = true; resultEl.classList.add('show'); } });用transitionend的代价是事件回调有跨设备的微小时间差,但它是按设备实际播放进度触发的,不会出现「金额抢跑」的错位。如果同时监听了多个属性,注意判断e.propertyName,只对transform响应,否则opacity的过渡结束会二次触发结果层逻辑。
5. 让开红包更有手感:合成音效、震动反馈与验证技巧
5.1 用 Web Audio 合成拆封音,不加载任何音频文件
拆红包的声音不需要 mp3,用 Web Audio 振荡器可以合成两声:第一声是盖子弹开的「啪」,第二声是金币落定的「叮」。合成音的好处是零网络加载、体积为零、音高可调,而且第一次加载就可用,不需要预加载音频文件。
function playPop() { const ctx = ensureAudio(); if (!ctx) return; const now = ctx.currentTime; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(320, now); osc.frequency.exponentialRampToValueAtTime(90, now + 0.12); gain.gain.setValueAtTime(0.25, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15); osc.connect(gain).connect(ctx.destination); osc.start(now); osc.stop(now + 0.16); }频率从 320Hz 拉到 90Hz 形成短促的「啵」声,包络控制在 150ms 内收束到静音,避免杂音拖尾。如果活动页面里还有金币入袋的连续音效,可以连续触发 3 到 5 次不同频率的短音,每次间隔 60ms 左右,营造出「金币一枚枚落入袋中」的听觉序列。
5.2 用 vibrate 做震动反馈,iOS 上要降级
拆红包那一瞬间震一下,整个反馈的完成度会高一个档次。Android WebView 支持标准navigator.vibrate(['0.2s']),注意它是一个队列参数,比如[30, 40, 80]表示震 30ms、停 40ms、再震 80ms,可以模拟出「先短促再厚重的两段式」。iOS Safari 完全不支持这个 API,调用时先判空,同时不要依赖震动作为开红包的唯一反馈,不然 iOS 上会显得很干。
震动触发点和音效一样,必须在同步的用户手势栈里调用,放到回调或延迟任务里之后很可能静默失效。我自己会把它和playPop()放在同一个时间线里,第一段震动配合盖子翻开,第二段配合金币爆发的视觉峰值。
5.3 用图层边框和帧率验证动效是否走 GPU
验证动效有没有「偷工减料」最直接的方法是看浏览器渲染层。Chrome DevTools 里打开 Rendering 面板,勾选 Layer Borders,运行开红包动画:红包盖闪烁绿框或蓝框说明它被提升为独立合成层,整个翻盖过程没有走主线程重排。如果红包盖只有橙色边框,说明它还在主线程做普通绘制,动画期间主线程一忙就会抖动。
性能判断的量化标准我习惯这样:启用帧率 HUD 或者直接看 Performance 面板,目标在 500 元价位 Android 真机上稳定 30fps 以上,主流机型上要满 60fps;Canvas 粒子爆发瞬间主线程单帧任务不超过 16ms 属于正常,如果超过 25ms 基本就掉帧了。如果掉帧,先看是不是粒子数量过多,再看是不是调用了shadowBlur,最后看 Canvas 尺寸是否超出了CSS 尺寸 × DPR的合理倍数。
我用这套方案做过好几个抽奖和营销落地页,最大的教训是:把动画时长、粒子数量、颜色、音效开关全部集中在一个参数对象里,方便运营在不同渠道换肤,不会每次都要翻代码定位一个魔法数字。参数集中之后调整成本很低,也是这套方案能持续复用的关键。希望帮到你。
本文还有配套的精品资源,点击获取