news 2026/4/25 14:11:31

Vue 3 + CSS动画实战:手把手教你复刻一个丝滑的扭蛋抽奖组件(附完整源码)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue 3 + CSS动画实战:手把手教你复刻一个丝滑的扭蛋抽奖组件(附完整源码)

Vue 3 + CSS动画实战:打造丝滑扭蛋抽奖组件的完整指南

最近在开发一个H5活动页面时,产品经理突然提出要加入一个"让人眼前一亮"的扭蛋抽奖动画效果。这种需求在前端开发中很常见——时间紧、效果要求高,而且需要兼顾性能和用户体验。本文将分享如何用Vue 3的Composition API和纯CSS动画实现一个完整的扭蛋抽奖组件,从基础架构到动画细节优化,带你一步步完成这个有趣的前端挑战。

1. 项目准备与基础架构

在开始编码前,我们需要明确扭蛋动画的核心流程。一个完整的扭蛋抽奖通常包含四个阶段:扭蛋随机跳动、中奖扭蛋下落、移动到中心放大、扭开展示奖品。这种分阶段的设计不仅符合用户心理预期,也便于我们进行代码组织和动画控制。

首先创建Vue 3项目基础结构:

npm init vue@latest gashapon-demo cd gashapon-demo npm install

组件的基础模板结构如下:

<template> <div class="gashapon-container"> <button @click="startLottery" class="start-btn">开始抽奖</button> <div class="egg-machine"> <!-- 扭蛋容器 --> <div class="egg-container" ref="eggContainer"> <div v-for="(egg, index) in eggs" :key="index" class="egg" :style="getEggStyle(index)" > <img :src="eggImage" alt="扭蛋"> </div> </div> <!-- 中奖扭蛋展示区 --> <div class="prize-display" ref="prizeDisplay"> <div class="winning-egg"> <img src="./assets/egg-top.png" class="egg-top"> <img src="./assets/egg-bottom.png" class="egg-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 :prizes="prizes" @start="onLotteryStart" @end="onLotteryEnd" />

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. 调试技巧与常见问题解决

在开发过程中,我们可能会遇到以下典型问题:

  1. 动画卡顿

    • 确保使用transform和opacity进行动画
    • 避免动画期间改变布局属性
    • 使用will-change提示浏览器
  2. z-index层级问题

    • 建立清晰的层级系统
    • 关键阶段动态调整z-index
  3. 动画不同步

    • 使用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. 进阶优化与创意扩展

基础功能完成后,可以考虑以下增强功能:

  1. 3D效果增强

    .egg { transform-style: preserve-3d; transition: transform 0.5s ease; } .egg:hover { transform: rotateY(20deg); }
  2. 物理引擎集成

    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) }
  3. 声音效果

    const playSound = (type) => { const audio = new Audio(`/sounds/${type}.mp3`) audio.volume = 0.3 audio.play().catch(e => console.log('Autoplay prevented')) }
  4. 奖品预览效果

    <div class="prize-preview"> <div v-for="(prize, index) in prizes" :key="index" class="prize-item" @mouseenter="showPreview(prize)" > {{ prize.name }} </div> </div>

10. 完整实现与项目集成

最后,我们将所有部分整合成一个完整的Vue单文件组件:

<template> <!-- 整合所有模板代码 --> </template> <script> // 整合所有脚本代码 </script> <style scoped> /* 整合所有样式代码 */ </style>

项目集成建议:

  1. 作为独立组件发布到私有npm仓库
  2. 提供详细的props和events文档
  3. 准备多种主题样式
  4. 提供不同复杂度的示例
// 组件注册示例 import Gashapon from './components/Gashapon.vue' export default { components: { Gashapon }, data() { return { prizes: [ { id: 1, name: '一等奖', image: 'prize1.png' }, // ...其他奖品 ] } } }

在实际项目中使用时,可以根据需要调整动画时长、缓动函数和视觉效果,确保与整体产品风格协调一致。通过合理的组件设计和参数化配置,这个扭蛋抽奖组件可以灵活适应各种营销场景需求。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/25 14:09:25

3种高效方案:解决阅读APP书源导入难题的终极指南

3种高效方案&#xff1a;解决阅读APP书源导入难题的终极指南 【免费下载链接】Yuedu &#x1f4da;「阅读」自用书源分享 项目地址: https://gitcode.com/gh_mirrors/yu/Yuedu 还在为小说阅读APP频繁失效的书源而烦恼吗&#xff1f;是否厌倦了在不同小说网站间来回切换寻…

作者头像 李华
网站建设 2026/4/25 14:09:00

【FPGA技术全景解析】从核心原理到前沿应用

1. FPGA技术基础&#xff1a;从晶体管到可编程逻辑 第一次接触FPGA时&#xff0c;我被这个火柴盒大小的芯片震撼到了——它既不像CPU有固定指令集&#xff0c;也不像GPU专为图形计算优化&#xff0c;却能通过编程重构内部电路结构。这种"硬件可编程"的特性&#xff0…

作者头像 李华