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

动画分析
由上面gif可看出,整个动画分为四个部分
- 扭蛋随机(也不算随机吧)在固定盒子内跳动
- 中奖扭蛋下落
- 中奖扭蛋移动到中心,并且逐渐放大
- 中奖扭蛋做出扭开姿势,缓慢打开
整个过程分析好了,貌似还不难,那就一步一步来实现
实现步骤一,盒子内随机跳动
在实现跳动前,先要做的一步是,尽可能把蛋摆放的随机,自然一点,怎么做?当然是定位啦。 我比较懒,于是计算了大概边界位置(我将整个球的摆放,分为三层,第一层,当然是贴近盒子边缘,第二层就再其上方了,第三层类推,同时再找好左右边界位置)
初始位置计算:
1// 这里用的是vue框架,扭蛋是通过v-for渲染出来的 2computed: { 3 //动态绑定style 4 calcStyle() { 5 return function (index) { 6 let top = index < 4 ? ( [1,2].includes( index ) ? '78%' : '71%') : 7 ( index < 8 ? ( [5,6].includes(index) ? '61%' : `${getRandomArbitrary(54,56)}%` ) : `${getRandomArbitrary(45,46)}%`); 8 return { 9 width: '18%', 10 transform: `rotate(${getRandomArbitrary(8,20) * 15}deg)`, 11 top 12 } 13 } 14 } 15}, 16// 生成随机数 17export const getRandomArbitrary = (min = 0, max)=> { 18 min = Math.ceil(min) 19 max = Math.floor(max) 20 return Math.floor(Math.random() * (max - min + 1)) + min //含最大值,含最小值 21}
随机跳动,其实就是写好的动画,需要时只需添加上即可
1// 其中一个动画 2@keyframes move1 { 3 0% { 4 transform: rotate(-30deg); 5 left: 12.7%; 6 top: 57.9%; 7 } 8 9 26% { 10 transform: rotate(60deg); 11 left: 41.2%; 12 top: 8.9%; 13 } 14 15 44% { 16 transform: rotate(110deg); 17 left: 52.2%; 18 top: 21.8%; 19 } 20 21 64% { 22 transform: rotate(56deg); 23 left: 72%; 24 top: 38%; 25 } 26 27 100% { 28 transform: rotate(-30deg); 29 left: 12.7%; 30 top: 57.9%; 31 } 32}
添加动画
1itemBoxStyle.animation = `move$1 0.75s 6 linear`
实现步骤二,扭蛋下落
下落动画不难,定义好初始位置,和结束位置,同样添加合适动画就可以了 tips: 要注意一个问题,开始扭蛋是看不见的(可能需要定位层级改变),然后下落一定高度扭蛋可以看见了(我用 overflow: hidden; 去解决) css:
1/* 下降动画 */ 2@keyframes upInDown { 3 0% { 4 opacity: 0; 5 } 6 100% { 7 opacity: 1; 8 top: 43%; 9 } 10}
js添加:
1resNode.classList.add('resulteDown')
实现步骤三,扭蛋移动到中心
要实现扭蛋移动到中心,并且逐渐放大,整个动画看似复杂,其实看你的思路,由于接触了之前的 flip思想,不懂的可以看之前的文章,只需知道中心位置和起始位置就可以计算出平移量,其他的就是细节处理 中心位置计算:
1// 这里我采取先将其定位到中心位置,然后在获取位置,建议在加载时,就计算好 2getEggEndLocation() { 3 const eggEnd = this.$refs.hitEgg 4 const style = { 5 position: 'fixed', 6 top: '50%', 7 left: '50%', 8 transform: 'translate(-50%, -50%) scale(1.8)', 9 'z-index': '-1', 10 opacity: 0 11 } 12 for (const key in style) { 13 if (Object.hasOwnProperty.call(style, key)) { 14 eggEnd.style[key] = style[key] 15 } 16 } 17 this.lastSite = this.getEleLocation(eggEnd) 18 // 清除设置 19 for (const key in style) { 20 if (Object.hasOwnProperty.call(style, key)) { 21 eggEnd.style[key] = '' 22 } 23 } 24}, 25// 获取元素位置 26getEleLocation(ele) { 27 const { top,left } = ele.getBoundingClientRect() 28 return { top,left } 29},
初始位置,直接用 getEleLocation 就可以了,有了起始和结束位置,就可以计算动画过程了
1resNode.style.transform = `translate3d(${ this.lastSite.left - left }px, ${ this.lastSite.top - top }px,0px) scale(${1.8}) rotate(-45deg)` 2resNode.classList.add('active2')
1.active2{ 2 transition: all 1.4s linear; 3}
实现步骤四,扭一扭,然后打开
这一步就全是动画了,就不过多叙说
1@keyframes upOpen { 2 0% { 3 transform: translate(0px,0px); 4 } 5 25% { 6 transform: translate(5px,0px); 7 } 8 50% { 9 transform: translate(-5px,0px); 10 } 11 70% { 12 transform-origin: -10% 85%; 13 transform: translate(0px,0px) rotate(0deg); 14 } 15 100% { 16 transform: rotate(-30deg); 17 transform-origin: -10% 85%; 18 } 19} 20 21@keyframes bottomOpen { 22 0% { 23 transform: translate(0px,0px); 24 } 25 25% { 26 transform: translate(-5px,0px); 27 } 28 50% { 29 transform: translate(5px,0px); 30 } 31 70% { 32 transform-origin: 6% 16%; 33 transform: translate(0px,0px) rotate(0deg); 34 } 35 100% { 36 transform: rotate(30deg); 37 transform-origin: 6% 16%; 38 } 39}
最后就动画效果的复位,删除添加的class就可以了
全部代码:
1<template> 2 <div> 3 <div class="gashapon" > 4 <button @click="toStart" >点击抽奖</button> 5 <!-- 蛋区 --> 6 <div class="egg_area" > 7 <div ref="eggBody" > 8 <div class="egg_box" v-for="(item,index) in imgIndex" 9 :style="calcStyle(index)" 10 :class="`egg_box${index+1}`" 11 :key="index"> 12 <img :src="require(`../../../assets/egg/egg${item}.png`)" alt=""/> 13 </div> 14 </div> 15 </div> 16 <!-- 出口 --> 17 <div class="hit_box" ref="hitEggBox"> 18 <!-- 出口蛋 --> 19 <div class="hit_egg" ref="hitEgg" > 20 <!-- 光 --> 21 <div class="light_box" v-show="lightShow" > 22 <img class="light_img" src="../../../assets/egg/e_sun.jpg" alt=""> 23 </div> 24 <img src="../../../assets/egg/egg_top.png" alt=""> 25 <img src="../../../assets/egg/egg_foot.png" alt=""> 26 </div> 27 </div> 28 </div> 29 <van-overlay :show="show" :lock-scroll="true" /> 30 </div> 31</template> 32 33<script> 34 import { getRandomArbitrary } from '../../../utils/lib' 35 import { 36 Overlay, 37 } from "vant" 38 export default { 39 name: 'EggMachine', 40 components: { 41 [Overlay.name]: Overlay, 42 }, 43 data() { 44 return { 45 imgIndex: [1, 2, 3, 2, 2, 3, 1, 1, 2, 1], 46 moveIng: false, 47 lastSite: {}, 48 show: false, 49 lightShow: false 50 } 51 }, 52 created() { 53 54 }, 55 async mounted() { 56 this.$nextTick(() => { 57 // 获取中心位置 58 setTimeout(() => { 59 this.getEggEndLocation() 60 },400) 61 }) 62 63 }, 64 computed: { 65 calcStyle() { 66 return function (index) { 67 let top = index < 4 ? ( [1,2].includes( index ) ? '78%' : '71%') : 68 ( index < 8 ? ( [5,6].includes(index) ? '61%' : `${getRandomArbitrary(54,56)}%` ) : `${getRandomArbitrary(45,46)}%`); 69 return { 70 width: '18%', 71 transform: `rotate(${getRandomArbitrary(8,20) * 15}deg)`, 72 top 73 } 74 } 75 } 76 }, 77 methods: { 78 async toStart() { 79 this.setNodeClass(true) 80 await this.delay(1500) 81 this.setNodeClass(false) 82 // 页面滚动到顶部,保证动画在中 83 window.scroll(0,0) 84 // 下降 85 this.eggDown() 86 }, 87 // 节点class处理 88 async setNodeClass(add = true) { 89 const eggChild = this.$refs.eggBody.childNodes 90 for (let i = 0; i < 10; i++) { 91 const itemBoxStyle = eggChild[i].style 92 add ? itemBoxStyle.animation = `move${i+1} 0.75s 6 linear` : itemBoxStyle.animation = '' 93 } 94 this.moveIng = add 95 }, 96 // 下降 97 async eggDown() { 98 const resNode = this.$refs.hitEgg 99 this.show = true 100 resNode.style.zIndex = '2' 101 resNode.classList.add('resulteDown') 102 await this.delay(1000) 103 104 // 记录当前位置 105 const { top,left } = resNode.getBoundingClientRect() 106 107 // 设置转变 108 this.$refs.hitEggBox.style.overflow = 'visible' 109 if(!Object.keys(this.lastSite).length) { 110 this.getEggEndLocation() 111 } 112 resNode.style.transform = `translate3d(${ this.lastSite.left - left }px, ${ this.lastSite.top - top }px,0px) scale(${1.8}) rotate(-45deg)` 113 resNode.classList.add('active2') 114 await this.delay(1800) 115 this.openEgg() 116 }, 117 // 获取扭蛋结束中心位置 118 getEggEndLocation() { 119 const eggEnd = this.$refs.hitEgg 120 const style = { 121 position: 'fixed', 122 top: '50%', 123 left: '50%', 124 transform: 'translate(-50%, -50%) scale(1.8)', 125 'z-index': '-1', 126 opacity: 0 127 } 128 for (const key in style) { 129 if (Object.hasOwnProperty.call(style, key)) { 130 eggEnd.style[key] = style[key] 131 } 132 } 133 this.lastSite = this.getEleLocation(eggEnd) 134 // 清除设置 135 for (const key in style) { 136 if (Object.hasOwnProperty.call(style, key)) { 137 eggEnd.style[key] = '' 138 } 139 } 140 }, 141 // 打开动画 142 async openEgg() { 143 // 测试 144 const resNode = this.$refs.hitEgg 145 const resNodeImg = resNode.childNodes 146 // 添加打开动画 147 resNodeImg[1].classList.add('eggOpenTop') 148 resNodeImg[2].classList.add('eggOpenBottom') 149 await this.delay(900) 150 this.lightShow = true 151 await this.delay(900) 152 // console.log('抽奖结束') 153 154 this.$emit('darw-succes') 155 // 复位 156 this.$refs.hitEggBox.style.overflow = 'hidden' 157 resNodeImg[1].classList.remove('eggOpenTop') 158 resNodeImg[2].classList.remove('eggOpenBottom') 159 resNode.classList.remove('resulteDown') 160 resNode.classList.remove('active2') 161 resNode.style.transform = '' 162 resNode.style.zIndex = '' 163 this.show = false 164 this.lightShow = false 165 }, 166 // 获取元素位置 167 getEleLocation(ele) { 168 const { top,left } = ele.getBoundingClientRect() 169 return { top,left } 170 }, 171 // 延迟函数 172 async delay(time = 2000) { 173 return new Promise((res) => { 174 setTimeout(() => { 175 res() 176 },time) 177 }) 178 }, 179 } 180 } 181</script> 182 183<style lang="less" scoped> 184 .active2{ 185 transition: all 1.4s linear; 186 } 187 .eggOpenTop { 188 animation: upOpen 1.2s linear; 189 animation-fill-mode: forwards; 190 } 191 .eggOpenBottom { 192 animation: bottomOpen 1.2s linear; 193 animation-fill-mode: forwards; 194 } 195 img{ 196 width: 100%; 197 } 198 .gashapon{ 199 min-height: 8rem; 200 background: url('../../../assets/egg/gashapon.png') no-repeat center; 201 background-size: 100% 100%; 202 position: relative; 203 } 204 .egg_area{ 205 position: absolute; 206 left: 54.5%; 207 transform: translateX(-50%); 208 width: 5.2rem; 209 height: 4.5rem; 210 background-color: transparent; 211 border-radius: 50%; 212 top: 0.1rem; 213 z-index: 1; 214 } 215 .egg_box { 216 position: absolute; 217 } 218 .egg_box img { 219 width: 100%; 220 } 221 .hit_egg{ 222 position: absolute; 223 width: 0.8rem; 224 top: -80%; 225 left: 49%; 226 transform: rotate(-45deg) translateX(-50%); 227 transform-origin:50% 50%; 228 img{ 229 width: 100%; 230 &:nth-child(3){ 231 margin-top: -0.1rem; 232 } 233 } 234 .light_box{ 235 position: absolute; 236 width: 1rem; 237 overflow: hidden; 238 top: -0.1rem; 239 .light_img{ 240 animation: rotateAni 0.8s infinite linear; 241 } 242 } 243 } 244 .hit_box{ 245 position: absolute; 246 width: 1.6rem; 247 height: 2rem; 248 top: 71%; 249 left: 29%; 250 overflow: hidden; 251 } 252 .resulteDown { 253 animation: upInDown 0.6s cubic-bezier(0.390, 0.575, 0.565, 1.000); 254 animation-fill-mode: forwards; 255 } 256 257 258 /* ------- 10个蛋 ------- */ 259 /* 前4个 */ 260 .egg_box1 { 261 left: 16%; 262 } 263 .egg_box2 { 264 left: 32%; 265 } 266 .egg_box3 { 267 left: 48%; 268 } 269 .egg_box4 { 270 left: 64%; 271 } 272 /* 后四个 */ 273 .egg_box5 { 274 left: 21%; 275 } 276 .egg_box6 { 277 left: 34%; 278 } 279 .egg_box7 { 280 left: 48%; 281 } 282 .egg_box8 { 283 left: 60%; 284 } 285 /* 后两个 */ 286 .egg_box9 { 287 left: 48%; 288 } 289 .egg_box10 { 290 left: 37%; 291 } 292 // 放大动画 293 @keyframes rotateAni { 294 0%{ 295 transform: scale(0.9); 296 } 297 100% { 298 transform: scale(1.1); 299 } 300 } 301</style> 302<style> 303/* 打开动画 */ 304@keyframes upOpen { 305 0% { 306 transform: translate(0px,0px); 307 } 308 25% { 309 transform: translate(5px,0px); 310 } 311 50% { 312 transform: translate(-5px,0px); 313 } 314 70% { 315 transform-origin: -10% 85%; 316 transform: translate(0px,0px) rotate(0deg); 317 } 318 100% { 319 transform: rotate(-30deg); 320 transform-origin: -10% 85%; 321 } 322} 323 324@keyframes bottomOpen { 325 0% { 326 transform: translate(0px,0px); 327 } 328 25% { 329 transform: translate(-5px,0px); 330 } 331 50% { 332 transform: translate(5px,0px); 333 } 334 70% { 335 transform-origin: 6% 16%; 336 transform: translate(0px,0px) rotate(0deg); 337 } 338 100% { 339 transform: rotate(30deg); 340 transform-origin: 6% 16%; 341 } 342} 343 344/* 下降动画 */ 345@keyframes upInDown { 346 0% { 347 opacity: 0; 348 } 349 100% { 350 opacity: 1; 351 top: 43%; 352 } 353} 354 355 356 /* 蛋滚动 */ 357@keyframes move1 { 358 0% { 359 transform: rotate(-30deg); 360 left: 12.7%; 361 top: 57.9%; 362 } 363 364 26% { 365 transform: rotate(60deg); 366 left: 41.2%; 367 top: 8.9%; 368 } 369 370 44% { 371 transform: rotate(110deg); 372 left: 52.2%; 373 top: 21.8%; 374 } 375 376 64% { 377 transform: rotate(56deg); 378 left: 72%; 379 top: 38%; 380 } 381 382 100% { 383 transform: rotate(-30deg); 384 left: 12.7%; 385 top: 57.9%; 386 } 387} 388 389@keyframes move2 { 390 0% { 391 transform: rotate(85deg); 392 left: 31.2%; 393 top: 57.9%; 394 } 395 396 23% { 397 transform: rotate(210deg); 398 left: 70%; 399 top: 36%; 400 } 401 402 45% { 403 transform: rotate(120deg); 404 left: 45%; 405 top: 8%; 406 } 407 408 72% { 409 transform: rotate(30deg); 410 left: 8%; 411 top: 34%; 412 } 413 414 100% { 415 transform: rotate(85deg); 416 left: 31.2%; 417 top: 57.9%; 418 } 419} 420 421@keyframes move3 { 422 0% { 423 transform: rotate(-10deg); 424 left: 50%; 425 top: 57.9%; 426 } 427 428 38% { 429 transform: rotate(-30deg); 430 left: 38%; 431 top: 11.4%; 432 } 433 434 65% { 435 transform: rotate(-50deg); 436 left: 7%; 437 top: 38.7%; 438 } 439 440 100% { 441 transform: rotate(-10deg); 442 left: 50%; 443 top: 57.9%; 444 } 445} 446 447@keyframes move4 { 448 0% { 449 transform: rotate(20deg); 450 left: 65%; 451 top: 59.9%; 452 } 453 454 35% { 455 transform: rotate(-30deg); 456 left: 53.4%; 457 top: 11.3%; 458 } 459 460 64% { 461 transform: rotate(-53deg); 462 left: 24.3%; 463 top: 56%; 464 } 465 466 100% { 467 transform: rotate(20deg); 468 left: 65%; 469 top: 59.9%; 470 } 471} 472 473@keyframes move5 { 474 0% { 475 transform: rotate(-65deg); 476 left: 61.4%; 477 top: 38%; 478 } 479 480 29% { 481 transform: rotate(-180deg); 482 left: 40%; 483 top: 11.5%; 484 } 485 486 53% { 487 transform: rotate(-222deg); 488 left: 9%; 489 top: 41.3%; 490 } 491 492 76% { 493 transform: rotate(-160deg); 494 left: 21.8%; 495 top: 57.9%; 496 } 497 498 100% { 499 transform: rotate(-65deg); 500 left: 61.4%; 501 top: 38%; 502 } 503} 504 505@keyframes move6 { 506 0% { 507 transform: rotate(16deg); 508 left: 44.2%; 509 top: 42%; 510 } 511 512 28% { 513 transform: rotate(-60deg); 514 left: 18%; 515 top: 57%; 516 } 517 518 40% { 519 transform: rotate(-45deg); 520 left: 8%; 521 top: 41.3%; 522 } 523 524 80% { 525 transform: rotate(70deg); 526 left: 52.7%; 527 top: 9.9%; 528 } 529 530 100% { 531 transform: rotate(16deg); 532 left: 44.2%; 533 top: 42%; 534 } 535} 536 537@keyframes move7 { 538 0% { 539 transform: rotate(-13deg); 540 left: 27.5%; 541 top: 39.9%; 542 } 543 544 17% { 545 transform: rotate(50deg); 546 left: 37.5%; 547 top: 57.9%; 548 } 549 550 44% { 551 transform: rotate(75deg); 552 left: 75%; 553 top: 41.3%; 554 } 555 556 67% { 557 transform: rotate(42deg); 558 left: 50.18%; 559 top: 8%; 560 } 561 562 100% { 563 transform: rotate(-13deg); 564 left: 27.5%; 565 top: 39.9%; 566 } 567} 568 569@keyframes move8 { 570 0% { 571 transform: rotate(46deg); 572 left: 14.4%; 573 top: 33.9%; 574 } 575 576 20% { 577 transform: rotate(97deg); 578 left: 45.6%; 579 top: 7.8%; 580 } 581 582 45% { 583 transform: rotate(143deg); 584 left: 76.8%; 585 top: 41.6%; 586 } 587 588 65% { 589 transform: rotate(85deg); 590 left: 64.6%; 591 top: 57%; 592 } 593 594 100% { 595 transform: rotate(46deg); 596 left: 14.4%; 597 top: 33.9%; 598 } 599} 600 601@keyframes move9 { 602 0% { 603 transform: rotate(65deg); 604 left: 36.4%; 605 top: 20%; 606 } 607 608 41% { 609 transform: rotate(130deg); 610 left: 74.3%; 611 top: 42.9%; 612 } 613 614 76% { 615 transform: rotate(94deg); 616 left: 46.5%; 617 top: 57.9%; 618 } 619 620 100% { 621 transform: rotate(65deg); 622 left: 36.4%; 623 top: 20%; 624 } 625} 626 627@keyframes move10 { 628 0% { 629 transform: rotate(-92deg); 630 left: 53.6%; 631 top: 22.11%; 632 } 633 634 20% { 635 transform: rotate(-142deg); 636 left: 37%; 637 top: 58.5%; 638 } 639 640 47% { 641 transform: rotate(-198deg); 642 left: 6.7%; 643 top: 37.3%; 644 } 645 646 67% { 647 transform: rotate(-135deg); 648 left: 23%; 649 top: 10.7%; 650 } 651 652 100% { 653 transform: rotate(-92deg); 654 left: 53.6%; 655 top: 22.11%; 656 } 657} 658</style>
