前端搞一个扭蛋抽奖小动画?

您所在的位置:网站首页 扭蛋机视频 前端搞一个扭蛋抽奖小动画?

前端搞一个扭蛋抽奖小动画?

2023-12-18 11:56| 来源: 网络整理| 查看: 265

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

最近新增一个抽奖小模块,就是扭蛋机的形式,产品给了参考网页,奈何不好扒下来用,只得自己动手干了,不多bb,先看效果吧!

效果图: 在这里插入图片描述

动画分析

由上面gif可看出,整个动画分为四个部分

扭蛋随机(也不算随机吧)在固定盒子内跳动 中奖扭蛋下落 中奖扭蛋移动到中心,并且逐渐放大 中奖扭蛋做出扭开姿势,缓慢打开

整个过程分析好了,貌似还不难,那就一步一步来实现

实现步骤一,盒子内随机跳动

在实现跳动前,先要做的一步是,尽可能把蛋摆放的随机,自然一点,怎么做?当然是定位啦。 我比较懒,于是计算了大概边界位置(我将整个球的摆放,分为三层,第一层,当然是贴近盒子边缘,第二层就再其上方了,第三层类推,同时再找好左右边界位置)

初始位置计算:

// 这里用的是vue框架,扭蛋是通过v-for渲染出来的 computed: { //动态绑定style calcStyle() { return function (index) { let top = index < 4 ? ( [1,2].includes( index ) ? '78%' : '71%') : ( index < 8 ? ( [5,6].includes(index) ? '61%' : `${getRandomArbitrary(54,56)}%` ) : `${getRandomArbitrary(45,46)}%`); return { width: '18%', transform: `rotate(${getRandomArbitrary(8,20) * 15}deg)`, top } } } }, // 生成随机数 export const getRandomArbitrary = (min = 0, max)=> { min = Math.ceil(min) max = Math.floor(max) return Math.floor(Math.random() * (max - min + 1)) + min //含最大值,含最小值 }

随机跳动,其实就是写好的动画,需要时只需添加上即可

// 其中一个动画 @keyframes move1 { 0% { transform: rotate(-30deg); left: 12.7%; top: 57.9%; } 26% { transform: rotate(60deg); left: 41.2%; top: 8.9%; } 44% { transform: rotate(110deg); left: 52.2%; top: 21.8%; } 64% { transform: rotate(56deg); left: 72%; top: 38%; } 100% { transform: rotate(-30deg); left: 12.7%; top: 57.9%; } }

添加动画

itemBoxStyle.animation = `move$1 0.75s 6 linear` 实现步骤二,扭蛋下落

下落动画不难,定义好初始位置,和结束位置,同样添加合适动画就可以了 tips: 要注意一个问题,开始扭蛋是看不见的(可能需要定位层级改变),然后下落一定高度扭蛋可以看见了(我用 overflow: hidden; 去解决) css:

/* 下降动画 */ @keyframes upInDown { 0% { opacity: 0; } 100% { opacity: 1; top: 43%; } }

js添加:

resNode.classList.add('resulteDown') 实现步骤三,扭蛋移动到中心

要实现扭蛋移动到中心,并且逐渐放大,整个动画看似复杂,其实看你的思路,由于接触了之前的 flip思想,不懂的可以去了解下,只需知道中心位置和起始位置就可以计算出平移量,其他的就是细节处理 中心位置计算:

// 这里我采取先将其定位到中心位置,然后在获取位置,建议在加载时,就计算好 getEggEndLocation() { const eggEnd = this.$refs.hitEgg const style = { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%) scale(1.8)', 'z-index': '-1', opacity: 0 } for (const key in style) { if (Object.hasOwnProperty.call(style, key)) { eggEnd.style[key] = style[key] } } this.lastSite = this.getEleLocation(eggEnd) // 清除设置 for (const key in style) { if (Object.hasOwnProperty.call(style, key)) { eggEnd.style[key] = '' } } }, // 获取元素位置 getEleLocation(ele) { const { top,left } = ele.getBoundingClientRect() return { top,left } },

初始位置,直接用 getEleLocation 就可以了,有了起始和结束位置,就可以计算动画过程了

resNode.style.transform = `translate3d(${ this.lastSite.left - left }px, ${ this.lastSite.top - top }px,0px) scale(${1.8}) rotate(-45deg)` resNode.classList.add('active2') .active2{ transition: all 1.4s linear; } 实现步骤四,扭一扭,然后打开

这一步就全是动画了,就不过多叙说

@keyframes upOpen { 0% { transform: translate(0px,0px); } 25% { transform: translate(5px,0px); } 50% { transform: translate(-5px,0px); } 70% { transform-origin: -10% 85%; transform: translate(0px,0px) rotate(0deg); } 100% { transform: rotate(-30deg); transform-origin: -10% 85%; } } @keyframes bottomOpen { 0% { transform: translate(0px,0px); } 25% { transform: translate(-5px,0px); } 50% { transform: translate(5px,0px); } 70% { transform-origin: 6% 16%; transform: translate(0px,0px) rotate(0deg); } 100% { transform: rotate(30deg); transform-origin: 6% 16%; } }

最后就动画效果的复位,删除添加的class就可以了

全部代码:

点击抽奖 import { getRandomArbitrary } from '../../../utils/lib' import { Overlay, } from "vant" export default { name: 'EggMachine', components: { [Overlay.name]: Overlay, }, data() { return { imgIndex: [1, 2, 3, 2, 2, 3, 1, 1, 2, 1], moveIng: false, lastSite: {}, show: false, lightShow: false } }, created() { }, async mounted() { this.$nextTick(() => { // 获取中心位置 setTimeout(() => { this.getEggEndLocation() },400) }) }, computed: { calcStyle() { return function (index) { let top = index < 4 ? ( [1,2].includes( index ) ? '78%' : '71%') : ( index < 8 ? ( [5,6].includes(index) ? '61%' : `${getRandomArbitrary(54,56)}%` ) : `${getRandomArbitrary(45,46)}%`); return { width: '18%', transform: `rotate(${getRandomArbitrary(8,20) * 15}deg)`, top } } } }, methods: { async toStart() { this.setNodeClass(true) await this.delay(1500) this.setNodeClass(false) // 页面滚动到顶部,保证动画在中 window.scroll(0,0) // 下降 this.eggDown() }, // 节点class处理 async setNodeClass(add = true) { const eggChild = this.$refs.eggBody.childNodes for (let i = 0; i < 10; i++) { const itemBoxStyle = eggChild[i].style add ? itemBoxStyle.animation = `move${i+1} 0.75s 6 linear` : itemBoxStyle.animation = '' } this.moveIng = add }, // 下降 async eggDown() { const resNode = this.$refs.hitEgg this.show = true resNode.style.zIndex = '2' resNode.classList.add('resulteDown') await this.delay(1000) // 记录当前位置 const { top,left } = resNode.getBoundingClientRect() // 设置转变 this.$refs.hitEggBox.style.overflow = 'visible' if(!Object.keys(this.lastSite).length) { this.getEggEndLocation() } resNode.style.transform = `translate3d(${ this.lastSite.left - left }px, ${ this.lastSite.top - top }px,0px) scale(${1.8}) rotate(-45deg)` resNode.classList.add('active2') await this.delay(1800) this.openEgg() }, // 获取扭蛋结束中心位置 getEggEndLocation() { const eggEnd = this.$refs.hitEgg const style = { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%) scale(1.8)', 'z-index': '-1', opacity: 0 } for (const key in style) { if (Object.hasOwnProperty.call(style, key)) { eggEnd.style[key] = style[key] } } this.lastSite = this.getEleLocation(eggEnd) // 清除设置 for (const key in style) { if (Object.hasOwnProperty.call(style, key)) { eggEnd.style[key] = '' } } }, // 打开动画 async openEgg() { // 测试 const resNode = this.$refs.hitEgg const resNodeImg = resNode.childNodes // 添加打开动画 resNodeImg[1].classList.add('eggOpenTop') resNodeImg[2].classList.add('eggOpenBottom') await this.delay(900) this.lightShow = true await this.delay(900) // console.log('抽奖结束') this.$emit('darw-succes') // 复位 this.$refs.hitEggBox.style.overflow = 'hidden' resNodeImg[1].classList.remove('eggOpenTop') resNodeImg[2].classList.remove('eggOpenBottom') resNode.classList.remove('resulteDown') resNode.classList.remove('active2') resNode.style.transform = '' resNode.style.zIndex = '' this.show = false this.lightShow = false }, // 获取元素位置 getEleLocation(ele) { const { top,left } = ele.getBoundingClientRect() return { top,left } }, // 延迟函数 async delay(time = 2000) { return new Promise((res) => { setTimeout(() => { res() },time) }) }, } } .active2{ transition: all 1.4s linear; } .eggOpenTop { animation: upOpen 1.2s linear; animation-fill-mode: forwards; } .eggOpenBottom { animation: bottomOpen 1.2s linear; animation-fill-mode: forwards; } img{ width: 100%; } .gashapon{ min-height: 8rem; background: url('../../../assets/egg/gashapon.png') no-repeat center; background-size: 100% 100%; position: relative; } .egg_area{ position: absolute; left: 54.5%; transform: translateX(-50%); width: 5.2rem; height: 4.5rem; background-color: transparent; border-radius: 50%; top: 0.1rem; z-index: 1; } .egg_box { position: absolute; } .egg_box img { width: 100%; } .hit_egg{ position: absolute; width: 0.8rem; top: -80%; left: 49%; transform: rotate(-45deg) translateX(-50%); transform-origin:50% 50%; img{ width: 100%; &:nth-child(3){ margin-top: -0.1rem; } } .light_box{ position: absolute; width: 1rem; overflow: hidden; top: -0.1rem; .light_img{ animation: rotateAni 0.8s infinite linear; } } } .hit_box{ position: absolute; width: 1.6rem; height: 2rem; top: 71%; left: 29%; overflow: hidden; } .resulteDown { animation: upInDown 0.6s cubic-bezier(0.390, 0.575, 0.565, 1.000); animation-fill-mode: forwards; } /* ------- 10个蛋 ------- */ /* 前4个 */ .egg_box1 { left: 16%; } .egg_box2 { left: 32%; } .egg_box3 { left: 48%; } .egg_box4 { left: 64%; } /* 后四个 */ .egg_box5 { left: 21%; } .egg_box6 { left: 34%; } .egg_box7 { left: 48%; } .egg_box8 { left: 60%; } /* 后两个 */ .egg_box9 { left: 48%; } .egg_box10 { left: 37%; } // 放大动画 @keyframes rotateAni { 0%{ transform: scale(0.9); } 100% { transform: scale(1.1); } } /* 打开动画 */ @keyframes upOpen { 0% { transform: translate(0px,0px); } 25% { transform: translate(5px,0px); } 50% { transform: translate(-5px,0px); } 70% { transform-origin: -10% 85%; transform: translate(0px,0px) rotate(0deg); } 100% { transform: rotate(-30deg); transform-origin: -10% 85%; } } @keyframes bottomOpen { 0% { transform: translate(0px,0px); } 25% { transform: translate(-5px,0px); } 50% { transform: translate(5px,0px); } 70% { transform-origin: 6% 16%; transform: translate(0px,0px) rotate(0deg); } 100% { transform: rotate(30deg); transform-origin: 6% 16%; } } /* 下降动画 */ @keyframes upInDown { 0% { opacity: 0; } 100% { opacity: 1; top: 43%; } } /* 蛋滚动 */ @keyframes move1 { 0% { transform: rotate(-30deg); left: 12.7%; top: 57.9%; } 26% { transform: rotate(60deg); left: 41.2%; top: 8.9%; } 44% { transform: rotate(110deg); left: 52.2%; top: 21.8%; } 64% { transform: rotate(56deg); left: 72%; top: 38%; } 100% { transform: rotate(-30deg); left: 12.7%; top: 57.9%; } } @keyframes move2 { 0% { transform: rotate(85deg); left: 31.2%; top: 57.9%; } 23% { transform: rotate(210deg); left: 70%; top: 36%; } 45% { transform: rotate(120deg); left: 45%; top: 8%; } 72% { transform: rotate(30deg); left: 8%; top: 34%; } 100% { transform: rotate(85deg); left: 31.2%; top: 57.9%; } } @keyframes move3 { 0% { transform: rotate(-10deg); left: 50%; top: 57.9%; } 38% { transform: rotate(-30deg); left: 38%; top: 11.4%; } 65% { transform: rotate(-50deg); left: 7%; top: 38.7%; } 100% { transform: rotate(-10deg); left: 50%; top: 57.9%; } } @keyframes move4 { 0% { transform: rotate(20deg); left: 65%; top: 59.9%; } 35% { transform: rotate(-30deg); left: 53.4%; top: 11.3%; } 64% { transform: rotate(-53deg); left: 24.3%; top: 56%; } 100% { transform: rotate(20deg); left: 65%; top: 59.9%; } } @keyframes move5 { 0% { transform: rotate(-65deg); left: 61.4%; top: 38%; } 29% { transform: rotate(-180deg); left: 40%; top: 11.5%; } 53% { transform: rotate(-222deg); left: 9%; top: 41.3%; } 76% { transform: rotate(-160deg); left: 21.8%; top: 57.9%; } 100% { transform: rotate(-65deg); left: 61.4%; top: 38%; } } @keyframes move6 { 0% { transform: rotate(16deg); left: 44.2%; top: 42%; } 28% { transform: rotate(-60deg); left: 18%; top: 57%; } 40% { transform: rotate(-45deg); left: 8%; top: 41.3%; } 80% { transform: rotate(70deg); left: 52.7%; top: 9.9%; } 100% { transform: rotate(16deg); left: 44.2%; top: 42%; } } @keyframes move7 { 0% { transform: rotate(-13deg); left: 27.5%; top: 39.9%; } 17% { transform: rotate(50deg); left: 37.5%; top: 57.9%; } 44% { transform: rotate(75deg); left: 75%; top: 41.3%; } 67% { transform: rotate(42deg); left: 50.18%; top: 8%; } 100% { transform: rotate(-13deg); left: 27.5%; top: 39.9%; } } @keyframes move8 { 0% { transform: rotate(46deg); left: 14.4%; top: 33.9%; } 20% { transform: rotate(97deg); left: 45.6%; top: 7.8%; } 45% { transform: rotate(143deg); left: 76.8%; top: 41.6%; } 65% { transform: rotate(85deg); left: 64.6%; top: 57%; } 100% { transform: rotate(46deg); left: 14.4%; top: 33.9%; } } @keyframes move9 { 0% { transform: rotate(65deg); left: 36.4%; top: 20%; } 41% { transform: rotate(130deg); left: 74.3%; top: 42.9%; } 76% { transform: rotate(94deg); left: 46.5%; top: 57.9%; } 100% { transform: rotate(65deg); left: 36.4%; top: 20%; } } @keyframes move10 { 0% { transform: rotate(-92deg); left: 53.6%; top: 22.11%; } 20% { transform: rotate(-142deg); left: 37%; top: 58.5%; } 47% { transform: rotate(-198deg); left: 6.7%; top: 37.3%; } 67% { transform: rotate(-135deg); left: 23%; top: 10.7%; } 100% { transform: rotate(-92deg); left: 53.6%; top: 22.11%; } } ps: 7/22 修复一个bug

由于笔者在小球 style 处采用了绑定 calcStyle 方法,vue在更新dom时会调用这个方法,导致这个方法会运行多次,小球位置也随之发生变化,暂时采取如下方式

data() { return { // data 处新增缓存变量 styleCacheMap: {} } }, computed: { calcStyle() { return function (index) { // 作一次缓存处理 if(Object.keys(this.styleCacheMap).length && this.styleCacheMap[index]) { return this.styleCacheMap[index] } let top = index < 4 ? ( [1,2].includes( index ) ? '78%' : '71%') : ( index < 8 ? ( [5,6].includes(index) ? '61%' : `${getRandomArbitrary(54,56)}%` ) : `${getRandomArbitrary(45,46)}%`); this.styleCacheMap[index] = { width: '18%', transform: `rotate(${getRandomArbitrary(8,20) * 15}deg)`, top } return this.styleCacheMap[index] } } },


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3