Vue 3 + CSS动画实战:手把手教你复刻一个丝滑的扭蛋抽奖组件(附完整源码)
Vue 3 CSS动画实战打造丝滑扭蛋抽奖组件的完整指南最近在开发一个H5活动页面时产品经理突然提出要加入一个让人眼前一亮的扭蛋抽奖动画效果。这种需求在前端开发中很常见——时间紧、效果要求高而且需要兼顾性能和用户体验。本文将分享如何用Vue 3的Composition API和纯CSS动画实现一个完整的扭蛋抽奖组件从基础架构到动画细节优化带你一步步完成这个有趣的前端挑战。1. 项目准备与基础架构在开始编码前我们需要明确扭蛋动画的核心流程。一个完整的扭蛋抽奖通常包含四个阶段扭蛋随机跳动、中奖扭蛋下落、移动到中心放大、扭开展示奖品。这种分阶段的设计不仅符合用户心理预期也便于我们进行代码组织和动画控制。首先创建Vue 3项目基础结构npm init vuelatest gashapon-demo cd gashapon-demo npm install组件的基础模板结构如下template div classgashapon-container button clickstartLottery classstart-btn开始抽奖/button div classegg-machine !-- 扭蛋容器 -- div classegg-container refeggContainer div v-for(egg, index) in eggs :keyindex classegg :stylegetEggStyle(index) img :srceggImage alt扭蛋 /div /div !-- 中奖扭蛋展示区 -- div classprize-display refprizeDisplay div classwinning-egg img src./assets/egg-top.png classegg-top img src./assets/egg-bottom.png classegg-bottom /div /div /div /div /template关键CSS基础样式.gashapon-container { position: relative; width: 100%; max-width: 375px; margin: 0 auto; height: 500px; background: url(./assets/machine-bg.png) no-repeat center; background-size: contain; } .egg-machine { position: relative; width: 100%; height: 100%; } .egg-container { position: absolute; top: 20%; left: 50%; transform: translateX(-50%); width: 70%; height: 50%; } .egg { position: absolute; width: 18%; transition: all 0.3s ease; }2. 实现扭蛋随机跳动效果扭蛋的随机跳动是吸引用户注意力的第一步。我们需要实现两个关键点初始位置的随机分布和跳动的动画轨迹。使用Composition API设置响应式数据import { ref, computed } from vue export default { setup() { const eggs ref(Array(10).fill(0)) const isAnimating ref(false) // 生成随机位置样式 const getEggStyle computed(() { return (index) { const layer Math.floor(index / 3) const baseTop 70 - layer * 15 const randomTop baseTop Math.random() * 5 const randomLeft 10 (index % 3) * 30 Math.random() * 10 const rotation Math.random() * 60 - 30 return { top: ${randomTop}%, left: ${randomLeft}%, transform: rotate(${rotation}deg), zIndex: layer 1 } } }) return { eggs, isAnimating, getEggStyle } } }跳动动画使用CSS关键帧实现keyframes jump { 0% { transform: rotate(-30deg) translateY(0); } 25% { transform: rotate(15deg) translateY(-30px); } 50% { transform: rotate(30deg) translateY(0); } 75% { transform: rotate(-15deg) translateY(-20px); } 100% { transform: rotate(-30deg) translateY(0); } } .jumping { animation: jump 0.8s infinite ease-in-out; }在Vue中控制动画的启动和停止const startJumping () { isAnimating.value true const eggs document.querySelectorAll(.egg) eggs.forEach((egg, index) { egg.style.animation jump 0.${5 index % 5}s infinite ease-in-out egg.style.animationDelay ${index * 0.1}s }) } const stopJumping () { const eggs document.querySelectorAll(.egg) eggs.forEach(egg { egg.style.animation none }) isAnimating.value false }3. 中奖扭蛋下落与中心放大当抽奖结果确定后中奖扭蛋需要从众多扭蛋中脱颖而出经历下落、移动到中心并放大的过程。这个阶段需要特别注意动画时序和缓动函数的选择。下落动画实现keyframes fall { 0% { opacity: 0; transform: translateY(-100px) rotate(-45deg); } 100% { opacity: 1; transform: translateY(0) rotate(-45deg); } } .falling { animation: fall 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; }中心放大动画使用FLIP技术实现流畅过渡const animateToCenter async (eggElement) { // 获取初始位置 const startRect eggElement.getBoundingClientRect() // 应用最终样式但不渲染 eggElement.style.position fixed eggElement.style.top 50% eggElement.style.left 50% eggElement.style.transform translate(-50%, -50%) scale(2.5) eggElement.style.zIndex 100 // 获取最终位置 const endRect eggElement.getBoundingClientRect() // 计算差值并重置初始状态 const deltaX startRect.left - endRect.left const deltaY startRect.top - endRect.top const deltaScale 2.5 / 1 eggElement.style.position eggElement.style.top eggElement.style.left eggElement.style.transform eggElement.style.zIndex // 应用FLIP动画 eggElement.style.transition none eggElement.style.transform translate(${deltaX}px, ${deltaY}px) scale(1) // 触发动画 requestAnimationFrame(() { eggElement.style.transition transform 1s cubic-bezier(0.165, 0.84, 0.44, 1) eggElement.style.transform translate(0, 0) scale(2.5) }) }提示使用cubic-bezier(0.165, 0.84, 0.44, 1)缓动函数可以创造更自然的放大效果模拟真实物理运动。4. 扭蛋打开与奖品展示扭蛋打开的动画是整个交互的高潮部分需要创造令人惊喜的效果。我们将扭蛋分为上下两部分分别应用不同的动画。扭蛋打开动画设计keyframes openTop { 0% { transform: rotate(0); transform-origin: left bottom; } 50% { transform: rotate(-5deg); } 100% { transform: rotate(-30deg); transform-origin: left bottom; } } keyframes openBottom { 0% { transform: rotate(0); transform-origin: right top; } 50% { transform: rotate(5deg); } 100% { transform: rotate(30deg); transform-origin: right top; } } .egg-top.open { animation: openTop 1s ease-out forwards; } .egg-bottom.open { animation: openBottom 1s ease-out forwards; }配合光效增强视觉冲击力keyframes glow { 0% { opacity: 0; transform: scale(0.8); } 50% { opacity: 0.8; transform: scale(1.1); } 100% { opacity: 0; transform: scale(1.3); } } .glow-effect { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 200px; height: 200px; background: radial-gradient(circle, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0) 70%); animation: glow 1s ease-out forwards; z-index: 99; }5. 完整流程控制与性能优化将各个动画阶段串联起来并添加必要的延迟和控制逻辑const startLottery async () { if (isAnimating.value) return isAnimating.value true // 1. 开始跳动 startJumping() // 2. 随机选择中奖扭蛋 await delay(2000) stopJumping() const winnerIndex Math.floor(Math.random() * eggs.value.length) const winnerEgg document.querySelectorAll(.egg)[winnerIndex] // 3. 中奖扭蛋下落 winnerEgg.classList.add(falling) await delay(600) // 4. 移动到中心并放大 await animateToCenter(winnerEgg) await delay(1000) // 5. 扭蛋打开 const prizeDisplay document.querySelector(.prize-display) prizeDisplay.innerHTML winnerEgg.innerHTML prizeDisplay.style.opacity 1 const eggTop prizeDisplay.querySelector(.egg-top) const eggBottom prizeDisplay.querySelector(.egg-bottom) eggTop.classList.add(open) eggBottom.classList.add(open) // 添加光效 const glow document.createElement(div) glow.className glow-effect prizeDisplay.appendChild(glow) // 6. 显示奖品 await delay(1000) showPrize(winnerIndex) isAnimating.value false } const delay (ms) new Promise(resolve setTimeout(resolve, ms))性能优化关键点使用will-change属性提前告知浏览器哪些属性会变化尽量减少重绘和回流合理使用硬件加速动画结束后移除不必要的元素.egg { will-change: transform, opacity; backface-visibility: hidden; perspective: 1000px; }6. 组件化与可复用设计为了使扭蛋组件能够在不同项目中复用我们需要设计良好的props接口和自定义事件export default { props: { eggCount: { type: Number, default: 10 }, prizes: { type: Array, required: true, validator: value value.length 0 }, duration: { type: Number, default: 3000 } }, emits: [start, end], setup(props, { emit }) { // ...其他逻辑 const startLottery async () { emit(start) // ...动画逻辑 emit(end, selectedPrize) } return { startLottery } } }使用示例Gashapon :prizesprizes startonLotteryStart endonLotteryEnd /7. 响应式设计与移动端适配确保扭蛋组件在不同设备上都有良好的表现media (max-width: 768px) { .gashapon-container { transform: scale(0.9); } .egg { width: 22%; } } media (max-width: 480px) { .gashapon-container { transform: scale(0.8); } keyframes jump { /* 调整跳动幅度 */ 25% { transform: rotate(15deg) translateY(-20px); } } }触屏交互增强const setupTouchEvents () { const container document.querySelector(.gashapon-container) let startY container.addEventListener(touchstart, (e) { startY e.touches[0].clientY }, { passive: true }) container.addEventListener(touchend, (e) { const endY e.changedTouches[0].clientY if (startY - endY 50) { // 上滑手势 startLottery() } }, { passive: true }) }8. 调试技巧与常见问题解决在开发过程中我们可能会遇到以下典型问题动画卡顿确保使用transform和opacity进行动画避免动画期间改变布局属性使用will-change提示浏览器z-index层级问题建立清晰的层级系统关键阶段动态调整z-index动画不同步使用transitionend和animationend事件确保延迟时间计算准确调试工具推荐Chrome DevTools的Animation面板Firefox的Animation Inspector使用console.time和console.timeEnd测量性能const debugAnimation async () { console.time(lottery-animation) await startLottery() console.timeEnd(lottery-animation) }9. 进阶优化与创意扩展基础功能完成后可以考虑以下增强功能3D效果增强.egg { transform-style: preserve-3d; transition: transform 0.5s ease; } .egg:hover { transform: rotateY(20deg); }物理引擎集成import { Engine, Bodies, Composite } from matter-js const setupPhysics () { const engine Engine.create() const eggs document.querySelectorAll(.egg) eggs.forEach(egg { const body Bodies.rectangle(/* 参数 */) Composite.add(engine.world, body) }) Engine.run(engine) }声音效果const playSound (type) { const audio new Audio(/sounds/${type}.mp3) audio.volume 0.3 audio.play().catch(e console.log(Autoplay prevented)) }奖品预览效果div classprize-preview div v-for(prize, index) in prizes :keyindex classprize-item mouseentershowPreview(prize) {{ prize.name }} /div /div10. 完整实现与项目集成最后我们将所有部分整合成一个完整的Vue单文件组件template !-- 整合所有模板代码 -- /template script // 整合所有脚本代码 /script style scoped /* 整合所有样式代码 */ /style项目集成建议作为独立组件发布到私有npm仓库提供详细的props和events文档准备多种主题样式提供不同复杂度的示例// 组件注册示例 import Gashapon from ./components/Gashapon.vue export default { components: { Gashapon }, data() { return { prizes: [ { id: 1, name: 一等奖, image: prize1.png }, // ...其他奖品 ] } } }在实际项目中使用时可以根据需要调整动画时长、缓动函数和视觉效果确保与整体产品风格协调一致。通过合理的组件设计和参数化配置这个扭蛋抽奖组件可以灵活适应各种营销场景需求。