前言
好久没动弹了,最近刚好写了个“老虎机”,发上来和大家一起讨论一下,有哪些实现方案以及一些实现方案的优劣度,本文主要借助backgroundImage的repeat、backgroundPositionY等属性进行技术实现,开发环境是在VUE下,不过好在基本函数化了,并不涉及太多DOM结构,改造components function 成本也不会很大;
其他方案
UI
- 先看下静态UI,以便于有个图像概念
方案分析
-
一:参数配置,后期灵活调整
入参,包括移动肚肚、圈数、参考值等
-
二:初始化UI起始位置、动态设置背景
1.动态设置背景,便于后期组件维护,避免css的改写 2.组起始的展示位置
-
三.配置分组,每一条线需要定义结束位置、运行总线等
三条单线,每条单线的配置类,并且运行的入口操作也在这里
-
四.轨迹线路计算
轨迹计算,递归模式下,每次运行的距离数组,即轨迹线
-
五.轨迹运行
requestAnimationFrame 帧动画开启
初始参考各值参考图
- 注,为了更好的适配与计算,以固定单位px作为计量,涉及各类机型,均以参考值进行动态计算,以及下文中大部分 “/2” 的操作均为2倍psd的处理;
- 并以下运行奖项线条图,均简称为线图;
方案分析-参数配置
这里我就不传入了,直接default模式;
props: {
img: {
type: String,
default:
''
},
rlc: {
type: Object,
default: () => {
return {
eh: 158 / 2, // 每块区域从空白区域开始 - 单一模块的末尾结束
ew: 119 / 2, // 参考宽度
ih: 63 / 2, // 初始移动的高度 63 = (312/2 - 93) 312 = 容器高度,计算中间距离 93 = 第一个空白区域到第一卡片中间的位置
ah: 2368 / 2, // 全图高度
rfh: (158 - 63) / 2, // 起始基准值 第一个元素运行到中间的基础位置
sp: 50, // 速度
ln: 7, // 圈数
dt: 750 // 默认多少市场后执行 下一轨迹
}
}
}
},
方案分析-初始化UI起始位置
-
mounted:当线图有总高度传入时与非传入的自动读取设置
mounted () { // 初始化入口高度 var vm = this if (this.rlc.ah) { this.c_ah = this.rlc.ah this.initPx() } else { var img = new Image() img.src = this.img img.onload = function (e) { vm.c_ah = img.height / 2 vm.initPx() } } },
-
initPx
initPx () { // 自动设置定位高度 window.addEventListener('DOMContentLoaded', e => { const ps = document.getElementById('tiger-wrap').getElementsByTagName('p') var width = null var newheight = null width = ps[0].clientWidth // 当前单个子容器实际宽度 newheight = parseInt(width / (this.rlc.ew / this.c_ah)) // 比例计算 px适配机型 this.initStart = parseInt((this.rlc.ih / this.c_ah) * newheight) // 根据参照下沉高度计算出真实高度 this.ph = newheight // 精灵图总高度 ps.forEach(ele => { ele.style.backgroundImage = `url(${this.img})` ele.style.backgroundSize = `100% ${newheight}px` ele.style.backgroundPositionY = `${this.initStart}px` }) }) }
方案分析-配置分组
-
得到每条线图的运行参数,并此函数作为对外的入口;
-
scale的计算,-2的操作是作为将第一个中奖单位定性为1,即运动距离减,运动距离为 -Y;
start (drawArr, endBack) { // 配置处理区 完成各组配置 // drawArr 入口传入的运动索引数组 // endBack 开始运行轨迹回调 var prevConfig = this.drawConfig this.drawConfig = [] const reference = this.rlc.rfh const img1 = this.$refs.img1 const img2 = this.$refs.img2 const img3 = this.$refs.img3 const domArr = [img1, img2, img3] drawArr.forEach((val, index) => { const scale = reference + this.rlc.eh * (val - 2) // 以第一个下沉作为参照 this.rlc.eh * (val - 2)完整的距离会多一个 reference const pp = prevConfig[index] ? Math.abs(prevConfig[index].endPoint) : 0 this.drawConfig.push({ endPoint: -parseInt((scale / this.c_ah) * this.ph), // 结束为止高度 startPoint: (prevConfig[index] && prevConfig[index].endPoint) || 0, // 开始位置高度 index: index, // 当前抽奖位置 dom: domArr[index], // 当前dom turn: true, // 当前组是否抽奖开关 stotal: -this.ph * this.rlc.ln - parseInt((scale / this.c_ah) * this.ph), // 理想情况下的参考 运动路线总长 total: -this.ph * this.rlc.ln - parseInt((scale / this.c_ah) * this.ph) + pp // 兼容上一次停留位置 计算总线 }) }) // 根据设定间隔开始运行 this.drawConfig.forEach((val, index) => { setTimeout(() => { this.Rundom(val) }, index * this.rlc.dt) }) this.endBack = endBack },
方案分析-轨迹线路计算
-
即得到运行数据组,简单理解,维护一个合理数组线,以减大数值加速,减小数值减速;
-
结束的末尾数据串,应为像素1,以便于停止到指定位置,不产生偏移,本文定为10;
Runline (domPage) { // 轨迹 每一条线对应一组轨迹运动 const { total, startPoint, stotal } = domPage const lineArr = [] const num = this.rlc.sp // 每次轨迹距离/运行速度 let numwrap = 0 let slow = 0 let newnumwrap = 0 const forlength = -parseInt(total / 2 / num) // 初始值的跑秒 if (this.initStart > startPoint && startPoint === 0) { lineArr.push([this.initStart, 0, 3]) } for (let i = 0; i < forlength; i++) { slow = i * 0.5 + 3 lineArr.push([-i * num + startPoint, num * -i - num + startPoint, slow]) } numwrap = lineArr[lineArr.length - 1][1] * 1 for (let j = 0; j < forlength; j++) { lineArr.push([ -j * num + numwrap, num * -j - num + numwrap, slow - j * 0.5 + 1 ]) } newnumwrap = lineArr[lineArr.length - 1][1] // 停止的间隔优化 初始化 第一次结束 起始位置不为 >= 0 if (newnumwrap > total && total === stotal) { if (Math.abs(total) - Math.abs(newnumwrap) > 10) { const next2 = newnumwrap - (Math.abs(total) - Math.abs(newnumwrap) - 10) lineArr.push([newnumwrap, next2, 3]) } lineArr.push([newnumwrap, total, 1]) } // 起始位置不为 0 // 此时需注意 每组跑完停留的位置并非0 if (total > stotal) { if (Math.abs(stotal) - Math.abs(total) > 10) { const next2 = newnumwrap - (Math.abs(stotal) - Math.abs(newnumwrap) - 10) lineArr.push([newnumwrap, next2, 3]) } lineArr.push([newnumwrap, stotal, 1]) } return lineArr },
方案分析-轨迹运行
-
以上述,处理好的配置,进行跑数据,即循环数据组,--数据值
Rundom (domPage) { // 开始运行轨迹 var num = domPage.startPoint || this.initStart // 默认从 0 开始 var lineConfig = this.Runline(domPage) // 拿到轨迹线 var ph = this.ph // RunBody 轨迹执行器 this.RunBody(() => { lineConfig.forEach(ele => { if (num > ele[1] && num <= ele[0]) { num = num - ele[2] } }) if (num <= lineConfig[lineConfig.length - 1][1]) { this.drawConfig[domPage.index].turn = false if (!this.drawConfig[2].turn) { this.endBack && this.endBack() } } domPage.dom.style.backgroundPositionY = `${num}px` if (!this.drawConfig[domPage.index].turn) { domPage.dom.style.backgroundPositionY = `${ this.drawConfig[domPage.index].endPoint }px` } }, domPage) },
-
帧动画
RunBody (into, domPage) { // 运动 requestAnimationFrame webkitRequestAnimationFrame // 帧动画处理方式 递归执行 // 非兼容模式下 采用setTimeout if (!this.drawConfig[domPage.index].turn) return const RunBody = () => { into() this.RunBody(into, domPage) } if (window.requestAnimationFrame) { window.requestAnimationFrame(RunBody) } else if (window.webkitRequestAnimationFrame) { window.webkitRequestAnimationFrame(RunBody) } else { window.setTimeout(RunBody, 10) } },
组件使用
- 引入
import tigerDraw from './components/tiger/tiger'
<tigerDraw ref="tigerDraw" :img="img"></tigerDraw>
- 调用
this.$refs.tigerDraw.start([1, 2, 3], () => {})
抽奖效果
结语
以上就是大概的实现思路了,有些地方还是略显粗糙的;另外从整体的描述来看,一些兼容的参考数值还是有点绕的,还是需要结合源码去跑一下,不然可能没有太直接的代入感;到最后也是希望能抛砖引玉了,探讨下多种实现方案,对比分析一些优缺点。
另附本文-源码地址,欢迎探讨哈~