最近项目中遇到了需要做个h5的抽奖活动需求:要求支持九宫格模式和圆形抽奖盘模式。这边基于vue和vantUI的loading组件造了这2个轮子。
思路
动态构造几次方的正方形抽键盘的4条边;
requestAnimationFrame来控制动画执行过程。
重点过程
// 兼容性raf初始化
window.requestAniFrame = (function() {
return window.requestAnimationFrame
// Older versions Chrome/Webkit
window.webkitRequestAnimationFrame ||
// Firefox < 23
window.mozRequestAnimationFrame ||
// opera
window.oRequestAnimationFrame ||
// ie
window.msRequestAnimationFrame ||
function(callback) {
return window.setTimeout(callback, 1000 / 60)
}
})()
// 兼容性取消初始化
window.cancelAnimation = (function() {
return (
window.cancelAnimationFrame ||
window.mozCancelAnimationFrame ||
window.cancelRequestAnimationFrame ||
function(id) {
clearTimeout(id)
}
)
})()
DINGDIAN:4,
BIAN:4
// 获取几次幂方阵
this.firstTurn = (this.ds.length + this.DINGDIAN) / this.BIAN
// 获取画第二条边 最多可画几个
this.secondNeedNumber = this.firstTurn - 1
// 获取位置
getPosition(index, isSelect) {
// 选中样式
let selectStyle = ``
if (isSelect) {
const selectWidth = this.tWidth - 4
const selectHeight = this.tHeight - 4
selectStyle = `width:${px2vw(selectWidth)};height:${px2vw(
selectHeight
)};border:${px2vw(2)} solid ${this.turnBackground}`
}
// 第一条边 index 0 1 2
if (index < this.firstTurn) {
let i = index - 0 * (this.firstTurn - 1)
return `top:0;left:${px2vw(i * this.tWidth)};${
this.commonStyle
};${selectStyle}`
}
// 第二条边 index值从 3 4
if (index - this.firstTurn < this.secondNeedNumber) {
let i = index - 1 * (this.firstTurn - 1)
return `top:${px2vw(i * this.tWidth)};left:${px2vw(this.abWidth)};${
this.commonStyle
};${selectStyle}`
}
// 第三条边 5 6
if (index <= 3 * (this.firstTurn - 1)) {
let i = index - 2 * (this.firstTurn - 1)
return `top:${px2vw(this.abHeight)};left:${px2vw(
this.abWidth - i * this.tWidth
)};${this.commonStyle};${selectStyle}`
}
// 第四条边 7
if (index < this.ds.length) {
let i = index - 3 * (this.firstTurn - 1)
return `top:${px2vw(this.abHeight - i * this.tWidth)};left:0;${
this.commonStyle
};${selectStyle}`
}
}
// runfunc
onRunFunc() {
let rafId = window.requestAniFrame(this.onRunFunc)
this.curRafID = rafId
let nowTimer = Date.now()
if (nowTimer - this.lastSuccessTimer >= this.spaceTimer) {
this.onRender(() => {
window.cancelAnimation(rafId)
// 恢复时间间隔
this.spaceTimer = this.whichStartSpaceTimer
this.isFinalCircle = false
this.onOff = true
this.isStart = false
})
}
}
onRender(cb) {
if (this.isFinalCircle) {
// 固定圈数转完后 在1位置
if (this.curSelect === this.bingoIndex && this.onOff) {
cb()
return
}
this.spaceTimer += this.eachTimer
this.curSelect++
if (this.curSelect < this.ds.length) {
this.ds[this.curSelect]['isSelect'] = true
this.ds[this.curSelect - 1]['isSelect'] = false
} else if (this.curSelect === this.ds.length) {
this.ds[this.curSelect - 1]['isSelect'] = false
this.ds[0]['isSelect'] = true
} else {
// 转完一圈
this.curSelect = 0
this.onOff = true
}
this.$nextTick(() => {
this.lastSuccessTimer = Date.now()
})
} else {
this.curSelect++
if (this.curSelect < this.ds.length) {
this.ds[this.curSelect]['isSelect'] = true
this.ds[this.curSelect - 1]['isSelect'] = false
} else if (this.curSelect === this.ds.length) {
this.ds[this.curSelect - 1]['isSelect'] = false
this.ds[0]['isSelect'] = true
} else {
// 转完一圈
this.curSelect = 1
this.ds[0]['isSelect'] = false
this.ds[1]['isSelect'] = true
this.countCircle++
if (this.countCircle === this.defaultCircle) {
this.countCircle = 0
this.spaceTimer = this.finalStartTimer
this.isFinalCircle = true
if (this.bingoIndex === 1) {
this.onOff = false
}
}
}
this.$nextTick(() => {
this.lastSuccessTimer = Date.now()
})
}
}
<nine-prize
:ds="dsNinePrize4"
prize-background="#BFEFFF"
prize-item-background="#F0F0F0"
pointer-text="GO!"
pointer-background="#EEB422"
turn-background="blue"
></nine-prize>
<nine-prize :ds="dsNinePrize"></nine-prize>
<nine-prize :ds="dsNinePrize12"></nine-prize>
思路
根据指定跑马灯数量加css3旋转动态构造灯泡外层(交替变化是使用keyframe);
分为奖盘和奖品两个div;
根据奖品数量加css3旋转动态构造奖品项(360除以个数可得到圆心角);
第一种是只支持偶数个的线型转盘模式(旋转计算);
第二种是可指定块级转盘样式模式(三角函数算扇形);
旋转方式通过transition来指定rotate的角度(固定角度+中奖商品角度);
通过监听transitionend来得知动画结束。
重点过程
// 跑马灯
&-lamp {
position: absolute;
width: 100%;
height: 100%;
// 小灯泡
&__item {
// 绝对居中来旋转
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 10px;
height: 100%;
margin: 0 auto;
transform-origin: center center;
@keyframes change-color {
0% {
background: #fff;
}
100% {
background: red;
}
}
// 画小圆点
&::before {
content: "";
position: absolute;
top: 5px;
right: 0;
left: 0;
width: 10px;
height: 10px;
margin: 0 auto;
border-radius: 50%;
}
// 圆点颜色
&:nth-of-type(even):before {
background: #fff;
animation: change-color 1s linear infinite;
}
&:nth-of-type(odd):before {
background: red;
animation: change-color 1s linear reverse infinite;
}
}
}
// 背景盘
&__background {
position: absolute;
overflow: hidden;
width: 100%;
height: 100%;
// 画线
.background-line-item {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 1px;
height: 50%;
margin: 0 auto;
transform-origin: center bottom;
}
}
这里正方形一个角为90度,居中取一半为固定值45度: prizeRotateDegree
一圈为360度: CIRCLEDEGREE
// 整个奖品盘旋转量 (奖品起始于左上角 则旋转量为)
// 这里目前该公式只适合锐角也就是奖品个数大于等于4
// 这里公式是90 - 360/len - (45 - 360/len/2) 下面进行化简
this.prizePlateRotate = this.prizeRotateDegree - this.CIRCLEDEGREE / 2 / len
// 扇形背景渲染
renderSectorBackground() {
const len = this.ds.length
if (len === 1) {
// 此时扇形为正方形
this.sectorWidth = this.wheelWidth - 2 * 20
this.sectorHeight = this.wheelWidth - 2 * 20
} else if (len === 2) {
// 此时扇形为半圆的长方形
this.sectorWidth = this.wheelWidth - 2 * 20
this.sectorHeight = (this.wheelWidth - 2 * 20) / 2
} else {
const sectorR = (this.wheelWidth - 2 * 20) / 2 // 半径
// 这里看似画的扇形其实是画的三角形 利用overflow显示出来就是扇形了
// 画三角形 其实是利用Border来画的 这里的起始点在右上角开始画
// 这里保留的是上border 不要下border 而上border高度就是半径 需要求一手border的宽度左右高度
// 而宽度是已半径作高 作切线 公式:x/半径 = tan(360/len/2)
// js的math三角函数的参数不是度数而是弧度参数 所以角度再转弧度 360deg/2 等于 PI
const radian = Math.PI / len // 弧度
const bWidth = Math.tan(radian) * sectorR // 宽度
this.borderWidth = px2vw(Math.ceil(bWidth)) // border宽度(左右border)
this.borderHeight = px2vw(sectorR) // border高度(上border)
}
this.sectorRotateDegree = Math.ceil(this.CIRCLEDEGREE / len / 2)
this.dsSector = len
},
// 开始转
onRun() {
this.isStart = true
const n = ~~(Math.random() * this.ds.length)
// 这里有个区别 就是我渲染奖品是顺时针渲染
// 但是旋转是逆时针的旋转 所以下标是反的
let realN
if (n === 0) {
realN = 0
} else {
realN = this.ds.length - n
}
console.log('中奖下标为', realN)
// 需要旋转度数
// 这里公式是 i=0 -> 扇形圆心角 / 2 + 扇形圆心角 * 0
// i=1 -> 扇形圆心角 / 2 + 扇形圆心角 * 1
const degree = this.eachSectorDegree / 2 + this.eachSectorDegree * n
const fDegree = degree + this.turnTableCircle * this.CIRCLEDEGREE
this.$refs.myWheelPrize.setAttribute(
'style',
`transition: transform ${this.turnTableTimer}s ease-out 0s;transform: rotate(${fDegree}deg);${this.commonStyle}`
)
// 监听transition事件完
this.$refs.myWheelPrize.addEventListener('transitionend', () =>
this.onTransitionEnd(degree)
)
},
// 动画结束
onTransitionEnd(d) {
this.$refs.myWheelPrize.setAttribute(
'style',
`transition: transform 0s ease-out 0s;transform: rotate(${d}deg);${this.commonStyle}`
)
this.$refs.myWheelPrize.removeEventListener(
'transitionend',
this.onTransitionEnd
)
this.isStart = false
}
<wheel-prize
:ds="dsLinePrize"
wheel-background-type="line"
wheel-background="#0887f2"
turn-table-background="#c6c7ca"
turn-table-line-color="#333"
pointer-background="yellow"
pointer-text-color="#c6c7ca"
pointer-text="GO!"
></wheel-prize>
<wheel-prize :ds="dsSectorPrize" wheel-background-type="sector"></wheel-prize>
其中代码中使用了js的加减乘除;使用了px2vw,css中是本地的mixin也就是Flex布局。若需使用则拿下来根据自己项目要求更改,其中配置项可在组件内查看,就没描述的那么清楚了。具体的业务逻辑可看源码!
九宫格及圆形源码