资讯详情

H5开红包特效实现:状态机驱动CSS动画与Canvas粒子,附移动端避坑指南

📅 2026/10/18 8:26:57 | 华诺云谱 👁 阅读
H5开红包特效实现:状态机驱动CSS动画与Canvas粒子,附移动端避坑指南
简介一套“h5开红包特效”互动功能示例代码包面向Web前端初学者与初中级开发者帮助解决页面中红包开启动画、随机金额显示、多设备适配等常见交互需求。资源共9个文件以HTML入口、CSS样式、JS脚本为主并配有5张PNG背景素材和1张JPG底图整体约130KB结构精简方便直接运行或嵌入现有项目。已有1077人学习下载。通过这份示例读者可掌握CSS3过渡与关键帧动画、jQuery事件绑定、DOM动态更新以及Math.random()生成随机红包金额的方法同时看到响应式布局、浏览器兼容处理和轻量性能优化的思路。示例中的图片素材便于替换整体代码可按模块抽离方便后续二次封装复用适合作为前端互动特效的入门参考。1. H5开红包特效一次点击三段动画串起完整的开箱体验距离活动上线还有四天运营丢过来一个需求页面上放一个红包用户点开要有金光、要炸出礼花、最好还能先晃一晃再打开。直接用 GIF 动图会显得廉价视频又要等加载于是就有了这套 h5开红包特效。它把开红包拆成三段动画——红包摇晃、卡片上下分离、Canvas 粒子礼花——再用一个轻量的状态机把三段串在一起。适合正在维护移动端活动页的开发者年会摇一摇、电商大促、抽奖拉新都可以直接用红包文案、粒子数量、拆开节奏都能改。2. 技术选型与目录结构为什么用 CSS 动画加 Canvas而不是一张 GIF先给结论这套组件适合做低频、装饰性强、交互固定的活动动效。低频意味着粒子动画不会长时间占用主线程装饰性强意味着视觉上限比性能上限更重要交互固定意味着不需要引入复杂的动画编排库。用这套标准去筛GIF 和视频基本都被淘汰了。2.1 动效方案的取舍三类方案谁更适合GIF 的颜色深度只有 8 位红包上的金色渐变一压缩就出现明显色带画面像蒙了一层雾体积从 800KB 到 2MB 不等放在首屏还会拖慢加载。视频方案画质更好、体积更小但开红包需要点击结束的瞬间立刻停在展开画面视频的时间轴很难对得上交互时机要做切帧、循环、再盖一层 DOM复杂度反而更高。剩下的是 CSS transform/keyframes 加 Canvas 2D 的组合。常见做法是让 CSS 负责红包卡片这类 DOM 元素的位移动画让 Canvas 负责粒子这类逐帧变化的特效。CSS 动画运行在合成线程主线程被 JavaScript 卡住时动画还能继续粒子如果也用 CSS几十个元素的 transform 同时变化浏览器要先做样式计算再布局帧率很容易掉到 40fps 以下。有人会问为什么不用 Lottie 这类动效方案Lottie 适合 UI 已经用 AE 排好版、整体贴图的场景而开红包的金额和礼花是运行时随机生成的AE 做不了动态文案。GSAP 很强大但为了一个 5 秒的动效引一个 30KB 的动画库对活动页来说不划算。这份源码全程零依赖拷贝即用。那为什么红包卡片不用 Canvas 绘制因为礼花是一次性特效而卡片要在屏幕上停留、被点击、响应状态变化用 DOM 更方便做事件绑定和文本填充。如果项目里已经用了 Vue 或 React卡片走模板渲染粒子留在 Canvas二者通过状态对接改动成本最低。2.2 目录结构与职责划分下面是这份源码的完整目录只有 5 个 JS 文件没有框架依赖h5-open-redpacket/ ├── index.html # 页面骨架动效容器和金额展示都在这里 ├── css/ │ ├── base.css # 基础布局与动画时长 CSS 变量 │ └── redpacket.css # 红包卡片、光晕、拆盖的全部 keyframes ├── js/ │ ├── config.js # 粒子数量、动画时长、领奖接口地址 │ ├── state-machine.js # 状态机closed - shaking - splitting - bursting - done │ ├── card-animation.js # 负责操作红包卡片的 class 切换 │ ├── particle-burst.js # Canvas 粒子系统出生、重力、透明度衰减 │ └── main.js # 入口绑定点击事件、串联完整流程 └── assets/ └── seal.svg # 封面印章贴图不需要可以直接删从依赖方向看config 是纯数据state-machine 只维护状态particle-burst 不知道 DOM 的存在main 负责把各部分拼起来。这是这类动画组件该有的拆分粒度卡片、粒子、状态各管一摊将来换红包皮肤时不用动 Canvas 代码。另外这份目录刻意没有 package.json说明它不是一个 npm 包而是一段可以直接拷进项目里的源码。如果你要维护多个活动我一般会直接把 js 目录拷进目标项目按项目现成的构建工具处理import 路径不用改。2.3 本地跑起来一条命令的事动画是纯前端静态资源不需要构建。本地验证我用静态文件服务器两种方式任选npx serve . --listen 8100如果没有 Node 环境也可以用 Python 自带的静态服务器python3 -m http.server 8100然后浏览器打开 localhost:8100 就能看到红包居中显示。注意不要用 file:// 协议直接双击 index.html后面避坑章节会专门讲这个问题。部署时把整个目录丢到 Nginx 或对象存储的某个子目录改一下接口地址就能上线不需要额外的服务端配置。在安卓 WebView 里有一个隐形坑部分厂商默认会缩放宽屏页面导致动效整体被缩小。建议在 index.html 里显式加上 user-scalableno避免开红包的动画被缩放失真也能减少双击触发放大的干扰。2.4 动画参数表手感是调出来的开红包这类动效手感完全由几个数值决定配置集中在 js/config.js参数默认值作用shake.times3点击后红包摇晃次数过多显得拖沓shake.duration600摇晃总时长毫秒太小会像抽搐split.duration420上下盖分离时长毫秒越短越脆burst.count42礼花粒子总数低端机建议不超过 24burst.power7.5粒子初速度像素/帧决定爆炸范围burst.gravity0.16重力加速度像素/帧^2越大回落越快burst.fadeSpeed0.018透明度衰减速度控制礼花停留时间先定数值再写代码比写完再慢慢猜要快很多。比如 shake.duration 从 600 改成 900摇晃就从轻点两下变成摇头晃脑这种改动只需要改 config动画函数一行不用碰。数值全部用帧级小数为单位的另一个原因是 Canvas 粒子模拟要求每帧累加整数会出现帧率不同时的阶梯感。60fps 下每帧 0.16 像素的重力一秒累计约 9.6 像素恰好接近自然下落的速度视觉上不会发飘。3. 核心模块拆解从闭合红包到礼花落地的三段链路一次完整的开红包操作状态流转是 closed - shaking - splitting - bursting - done五个状态之间不允许跳转每个状态的持续时间在 config 里统一管理。这段链路是整个项目的地基后面的 CSS 和 Canvas 都围绕它展开。3.1 状态机为什么用显式转换如果不用状态机直接写嵌套 setTimeout三个动画之后回调就快成金字塔了。状态机把动画的因果关系从时间回调变成状态转换新增动画只在 allow 表里加一行。// state-machine.js const STATES [closed, shaking, splitting, bursting, done]; class RedPacketSM { constructor({ onChange }) { this.state closed; this.onChange onChange; } setState(next) { this.state next; this.onChange?.(next); } transition(next) { const allow { closed: [shaking], shaking: [splitting], splitting: [bursting], bursting: [done], done: [] }; if (!allow[this.state].includes(next)) return false; this.setState(next); return true; } }这段代码的思路是转换表不允许任意跳转从 closed 直接到 bursting 会返回 false点击事件拿不到预期结果。时序完全交给业务层决定组件层只保证状态不越级。参数说明onChange 是状态变动的钩子实际项目中用它来切换红包卡片的 class 和触发粒子transition 的返回值用来判断当前点击是否有效比如 done 状态下再点红包transition 返回 false事件里可以直接忽略。3.2 红包开盖CSS 动画如何模拟纸片拆开开盖动画没有用位移而是让上下两半各自向相反方向旋转。旋转比位移更接近真实纸片的手感配合 transform-origin 在中间偏左的位置会有一种从中间撕开的错觉。/* redpacket.css */ .packet.splitting .packet__top { animation: splitTop 0.42s ease forwards; } .packet.splitting .packet__bottom { animation: splitBottom 0.42s ease forwards; } .packet.bursting { animation: packetRise 0.9s ease forwards; } keyframes splitTop { to { transform: rotate(-18deg) translateY(-14px); } } keyframes splitBottom { to { transform: rotate(12deg) translateY(10px); } } keyframes packetRise { to { transform: translateY(-24px); opacity: 0; } }注意两个细节。第一animation-fill-mode 用了 forwards保证动画结束后保持展开的最后一帧。第二如果状态切到 bursting 时直接把 splitting class 摘掉卡片会瞬间弹回关闭状态所以这里给 .packet.bursting 单独定义了一个上移淡出的动画让卡片让位给粒子。0.42s 的时长对应 config 中 split.duration 420CSS 和配置数据保持一致改节奏时两边都要动。很多人在这一步只改 CSS 不改 config结果摇晃结束和拆盖开始的时间错位看起来像机器人卡顿。3.3 Canvas 粒子礼花从出生到消亡的一帧循环粒子系统是这次动效冲击力的来源。每次 burst 在指定坐标生成 count 个粒子每帧更新位置、增加重力、降低透明度寿命耗尽就从数组里移除。// particle-burst.js export class ParticleBurst { constructor(canvas, { count, power, gravity, fadeSpeed }) { this.canvas canvas; this.ctx canvas.getContext(2d); this.count count; this.power power; this.gravity gravity; this.fadeSpeed fadeSpeed; this.particles []; } burst(x, y) { for (let i 0; i this.count; i) { const angle Math.random() * Math.PI * 2; const speed this.power * (0.6 Math.random() * 0.8); this.particles.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 1.5, life: 1, size: 2 Math.random() * 4, color: hsl(${20 Math.random() * 50}, 90%, 60%) }); } } update() { 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.vy this.gravity; p.x p.vx; p.y p.vy; p.life - this.fadeSpeed; if (p.life 0) { this.particles.splice(i, 1); continue; } this.ctx.globalAlpha Math.max(p.life, 0); this.ctx.fillStyle p.color; this.ctx.beginPath(); this.ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); this.ctx.fill(); } return this.particles.length; } }参数在代码里的直观作用power 决定粒子初速度区间0.6~1.4 的随机系数让爆炸不是均匀的圆而是有疏密层次vy 初始减 1.5 让粒子先往上冲再被重力拉回来fadeSpeed 越大礼花消失越快。颜色用 hsl 色相 20~70正好覆盖金色到橙色区间比写死十六进制颜色更接近火焰质感。逆序遍历的细节不能省从尾部删除粒子避免 splice 后索引错位。运行时 update 由 requestAnimationFrame 驱动绘制结束返回剩余粒子数main.js 用它判断是否该结束动画循环。3.4 点击链路一次点击如何串起动画和请求main.js 把模块接起来点击事件先切到 shaking摇晃结束后拆盖拆盖进行到一半时粒子已经在红包中心爆开同时并发请求领奖接口金额回来后再进入 done 状态展示。// main.js import { RedPacketSM } from ./state-machine.js; import { ParticleBurst } from ./particle-burst.js; import { ANIMATION_CFG } from ./config.js; const packet document.querySelector(.packet); const canvas document.querySelector(.particle-layer); const sm new RedPacketSM({ onChange(state) { packet.dataset.state state; packet.classList.remove(closed, shaking, splitting, bursting, done); packet.classList.add(state); } }); const burst new ParticleBurst(canvas, ANIMATION_CFG.burst); packet.addEventListener(click, () { if (!sm.transition(shaking)) return; setTimeout(() sm.transition(splitting), ANIMATION_CFG.shake.duration); setTimeout(() { sm.transition(bursting); const rect packet.getBoundingClientRect(); burst.burst(rect.left rect.width / 2, rect.top rect.height / 2); fetch(ANIMATION_CFG.apiUrl, { method: POST }) .then(res res.json()) .then(data { document.querySelector(ANIMATION_CFG.amountEl).textContent ¥${data.amount}; setTimeout(() sm.transition(done), 900); }) .catch(() { document.querySelector(ANIMATION_CFG.amountEl).textContent 已领取; setTimeout(() sm.transition(done), 900); }); }, ANIMATION_CFG.shake.duration ANIMATION_CFG.split.duration); });时序计算摇晃 600ms拆盖 420ms所以在 1020ms 后触发 bursting。粒子爆点用 getBoundingClientRect 取红包中心而不是写死坐标无论页面放在多小的屏幕上粒子都能炸在红包正中央。fetch 请求和粒子动画是并行的接口慢不会卡动画接口失败时 catch 分支把金额区域写成已领取照样进入 done 状态。这个兜底逻辑在弱网环境下很关键否则接口超时后整个动效会卡在 bursting 状态用户再点红包也没反应。4. 移动端适配避坑真机上才会翻车的五件事网上聊 H5 动效避坑大多只聊 viewport真机上的麻烦往往来自系统 WebView 的默认手势、缩放策略和 GPU 性能。下面这五件事按我实际遇到频率从高到低排。4.1 坑一Canvas 粒子边缘发虚像蒙了一层雾现象PC 预览一切正常一到真机上炸礼花边缘明显锯齿像低分辨率图被强行拉升。 原因Canvas 位图分辨率没有跟随设备的 devicePixelRatio真机 dpr 通常是 2 或 3CSS 像素 100px 对应物理像素 200px绘图缓冲区不够大。 解决初始化时按 dpr 缩放画布const dpr window.devicePixelRatio || 1; canvas.width canvas.clientWidth * dpr; canvas.height canvas.clientHeight * dpr; canvas.getContext(2d).scale(dpr, dpr);此后所有绘制坐标仍按 CSS 像素写逻辑层完全感知不到缩放。这个坑平时不会触发因为 PC 浏览器 dpr 为 1只有换到真机调试才会暴露。4.2 坑二低端安卓机上点开就一卡一卡现象同一套代码在 iOS 真机上满帧在一台低端安卓机上点击后红包裂开到一半停住过一两秒才继续。 原因粒子数量太多加上动画期间触发了频繁的布局抖动GPU 和主线程双双过载。 解决在 config 里把 burst.count 从 42 降到 24关掉 shadowBlur 阴影并统一用 requestAnimationFrame 驱动 Canvas 循环。const isLowEnd navigator.hardwareConcurrency 4; if (isLowEnd) { burst.count 24; }我一般还会给低端机加一个开关检测到硬件线程数小于等于 4 时直接用降级参数让摇晃和拆盖保留礼花数量减半。视觉效果少了一点华丽但至少不卡到点不动。4.3 坑三长按红包弹出系统保存图片菜单现象安卓上手指按在红包上不放弹出保存图片/识别二维码这类系统菜单把动画手势打断。 原因WebView 默认对图片和部分可点元素开启长按手势动效页面没有禁用。 解决在红包容器上加 user-select 和 touch-callout 属性并让触摸事件不被默认行为消耗掉.packet, .packet * { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; touch-action: manipulation; }touch-action: manipulation 顺手消掉双击缩放这样点击后不会因为系统等双击判定而延迟触发动画。4.4 坑四滚动长页面时动画容器被带飞现象把红包放在一个可滚动的活动页中段点击后动画到一半整个动效容器跑到屏幕外面或停在错误位置。 原因容器用了 position: fixed但某个祖先节点带 transformfixed 的参考基准变成 transform 祖先滚动时定位失效。 解决开红包前把动效容器临时移动到 document.body 下缓存原位置动画结束再放回去const originParent animRoot.parentNode; const originNext animRoot.nextSibling; document.body.appendChild(animRoot); // 动画结束后 originParent.insertBefore(animRoot, originNext);更省事的做法是开红包前先禁止页面滚动动画结束恢复避免滚动状态和动效坐标互相干扰。4.5 坑五file:// 协议下直接打开页面红包点了没反应现象本地双击 index.html点击红包没反应控制台报 CORS 错误或 Uncaught TypeError。 原因ES module 受浏览器同源策略限制file:// 被当作跨域来源Canvas 读取资源时也容易触发安全错误。 解决用本地静态服务器访问这是最省事的方案。部署时如果 Canvas 需要加载外部图片做红包皮肤图片服务器要返回 CORS 头否则真机上礼花正常、皮肤显示不出来。5. 二次开发思路把固定流程变成一套可配置的动效引擎我拿到这套源码后的第一反应不是看动画写了多少行而是看它的边界状态机、粒子、卡片三个模块互相不知道对方存在。改成自己的活动页只需要两步。5.1 第一步把主流程改成配置驱动export const ANIMATION_CFG { shake: { times: 3, duration: 600 }, split: { duration: 420 }, burst: { count: 42, power: 7.5, gravity: 0.16, fadeSpeed: 0.018 }, apiUrl: /api/lucky/claim, amountEl: .amount, onDone: null };想调快就缩 duration想更炸就调 count注意低端机上把 count 调到 24 以下。我现在接这类需求会把运营改文案和程序改时序分开文案放数据时序放状态机这样运营改价格的时候完全不用碰代码。5.2 第二步验证三件事改完以后我一般过一遍这套验证弱网下接口失败时金额区有没有兜底文案低端安卓机上粒子数是否掉帧点击瞬间的快照位置和红包中心是否对齐。这三件都是状态机边界最容易翻车的地方。以前我拿到动画需求就急着写 CSS后来在某个活动页上被滚动后点击坐标错位的 bug 折腾了一下午才发现把状态、动画、坐标分开管理才是这类组件的核心。从那以后我每次接 H5 动效需求都会强制先画一张状态流转图再动手写代码返工的次数少了一半希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑