在这个超长假期中,无聊。。。,所以动手做一个早就计划要做的小玩意,水果老虎机,嗯,这是一个小程序而不是小游戏...
- 支持自定义水果盘个数
- 支持自定义每个奖品
- 支持中奖后的回调方法

使用结构还是canvas?
使用模板结构(view)生成水果盘的好处一是用户可自定义产出n x 4的定制化老虎机,二是容易通过算法样式生成布局,三是通过wx.selectQueryAll的方法能够很方便的抓到定位数据。但,问题是动画性能过于孱弱,如图构建一个7x4的水果盘,动画性能估计会惨不忍睹,而且纯粹模板结构无论使用animation动画方法还是css的keyframe的动画方法得到的动画效果都非常差(测试过的结论),还有是已知的动画方法可控性很差
使用canvas来生成水果盘好处是动画性能很好(canvas2d),但是定制性和扩展性比较差
so综上考虑,使用模板(view)布局,使用canvas来实现动画。既保证了组件的性能,同时定制型,扩展性也很好
准备计时器方法
动画的生成离不开计时器方法,settimeout/setinterval这两兄弟真的不够看啊,问题还多,做过web开发的一定都知道window.requestAnimationFrame,这货在小程序的计时器方法中不存在,好在canvas2d中可以使用Canvas.requestAnimationFrame(function callback)方法来实现
准备运动算法
在水果老虎机中,激活状态会沿着四方的水果盘做非线性运动(easeInOut比较好用),需要基础的运动算法来计算实际的运动距离。在animation动画方法中,我们可以使用ease-in/ease-out等缓动算法来实现动画效果,但在这里必须要借助tween.js中的缓动算法来实现运动效果(因为需要控制运动节点)。
你会不会想到用css的keyframe动画来做这个运动效果,经过我的测试,css的动画和animation的动画会在每一条边上实现一次(ease)缓动运动(很奇怪的效果)
使用其中一个,节省代码量
/*
* Tween.js
* t: current time(当前时间);
* b: beginning value(初始值);
* c: change in value(变化量);
* d: duration(持续时间)。
*/
// Quart 四次方的缓动
const easeInOutQuart = function (t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b;
return -c / 2 * ((t -= 2) * t * t * t - 2) + b;
}
tween算法是以时间为基准(时间比率 = 距离比率)来计算单位时间的实际运动距离
布局
以上面的图为例,我们需要做一个 7 x 4的水果盘,实际有效的奖品格子数为 7*4-4共24个有效格子
有效格子算法
js
// 0-6 第一行所有格子全部有效
// 21-27 最后一行所有格子全部有效
// 中间部分 i%7===0 和 i%7 === (7-1) 有效
// 算法源码有点无聊,依据上述思路,即可遍历28个格子并标识奖品格子valide=true
// 可以扩展想一想 6x6 5x5,思路是一样的
wxml
<view class="fruits-container" >
<view class="fruits-table" >
<block wx:for="{{ary}}" wx:key="index" >
<view wx:if="{{item.valide}}" class="valide">{{item.title}}</view>
<view wx:else class="in-valide"></view>
</block>
</view>
<canvas type="2d" .... />
</view>
样式
只节选关键样式,目的是让canvas覆盖在水果盘上,长宽一致
.fruits-container {
position: relative;
width: 400px;
height: 400px;
...
}
.fruits-table {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
...
}
抓取位置信息
canvas的绘制需要X轴, Y轴的精确信息,可以使用wx.createSelectorQuery方式抓取类名为‘valide’的view(奖品格子)的位置信息
let query = wx.createSelectorQuery().in(this)
query.selectAll(`.fruits-table .valide`).boundingClientRect(ret => {
....
console.log(ret[0]) // top, left, right, bottom, width, height
console.log(ret[1]) // top, left, right, bottom, width, height
...
...
console.log(ret[23]) // top, left, right, bottom, width, height
})
得到每一个奖品格子的位置信息后,就可以使用canvas的fillRect方法来绘制激活状态了。
绘制一个激活状态
let query = wx.createSelectorQuery().in(this)
query.selectAll(`.fruits-table .valide`).boundingClientRect(ret => {
....
let {top, left, right, bottom, width, height} = ret[0]
const canvasQuery = wx.createSelectorQuery()
canvasQuery.select('#fruit-canvas')
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node
const ctx = canvas.getContext('2d')
let x = top
let y = left
let dx = width
let dy = height
ctx.shadowOffsetX = 2
ctx.shadowOffsetY = -2
ctx.shadowColor = 'red'
ctx.shadowBlur = 50
ctx.lineWidth = 5
ctx.strokeStyle = 'red'
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.strokeRect(x, y, dx, dy)
})
})
跑起来
已经绘制了一个激活状态,接下来使它能够简单动起来
// 抽象激活方法
functon rect(point, canvas){
let {x, y, dx, dy} = getPosition(point)
ctx.shadowOffsetX = 2
ctx.shadowOffsetY = -2
...
...
ctx.clearRect(0, 0, canvas.width, canvas.height) // 擦除整个水果盘
ctx.strokeRect(x, y, dx, dy) // 绘制激活区域
}
function run(){
setTimeout(()=>{
if (ret.length) {
let point = ret.shift()
rect(point, canvas)
run()
}
}, 100)
}
执行run方法后可以看到水果盘的激活状态一步一步的往前走(100毫秒),拖拉机终于可以启动了
配上运动算法
经过上面的试验我们终于可以看到基本的运动效果了,接下来配上运动算法和计时器方法
// Quart 四次方的缓动
const easeInOutQuart = function (t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b;
return -c / 2 * ((t -= 2) * t * t * t - 2) + b;
}
let start = 0 // 开始时间
let begin = 0 // 开始奖品位置
let end = 23 // 终点位置,这里跑一圈
let during = 5000 // 运动总时间
// 1000/60 ≈ 17,
// 17毫秒即表示屏幕60帧刷新率每秒 ≈ requestAnimationFrame计数频率(一般情况)
const steper = () => {
// left为位移距离
// 老虎机的运动位移是节点位移,不是精确位移
// 所以这里用parseInt处理,只取整数部分
// 数据变化为 0,1,2,3,4,5...23
// 间隔时间/距离由easeInOutQuart算法计算
var left = easeInOutQuart(start, begin, end, during);
let idx = parseInt(left)
start = start + 17;
if (idx <= end) {
let point = this.ret[idx] // 取节点位置信息
this.rect(point) // 绘制
}
// 时间递增
if (start <= during) {
this.ctx.requestAnimationFrame(steper); // 计时器
} else {
// 动画结束,这里可以插入回调...
// callback()...
}
};
steper(); // 启动
以上为我的小程序水果老虎机的基本开发思路
安装方法
GITHUB上有详细说明。
配置
wxml
<view wx:if="{{fruitConfig}}" class="fruit-container {{fruitConfig.containerClass||''}}" style="{{fruitConfig.containerStyle||''}}">
<ui-list list="{{fruitConfig}}" />
<canvas type='2d' disable-scroll="true" id="fruit-canvas" class='fruit-canvas'></canvas>
</view>
js
const Pager = require('../../components/aotoo/core/index')
const mkFruits = require('../../components/modules/fruit')
Pager({
data: {
fruitConfig: mkFruits({
id: 'fruitTable',
...
...
},
// callback,动画结束后响应
function (param) {
console.log(param); // 中奖数据
console.log(param.value); // 中奖值
})
}
})
id
配置实例的Id
containerClass
容器样式
containerStyle
容器内联样式
max
设置max值后,根据平方算法,自动计算果盘数量,max定义一行几个水果,默认九宫格,max为3
count
与max的区别在于果盘数量由用户指定,且果盘不再为老虎机的游戏模式,max数量仍为定义一行几个水果
设置count为有效数据后,将以常规抽奖盘代替老虎机抽奖盘
confuse
使果盘数据无序化,只有当count为有效数据时,confuse才能生效
fruitsData
Object,指定对应位置的格子内容,支持文字/图片
回调方法
mkFruits(config, callback)水果盘动画结束后响应方法
如何设置
设置9格的水果盘
默认数字排序
mkFruits({
id: 'fruit',
max: 3,
})
设置果盘内容(文字)
下例为16格水果盘,并指定相关水果内容
mkFruits({
id: 'fruit',
max: 4,
fruitsData: {
1: {title: '香蕉'},
8: {title: '苹果'}
},
})
设置果盘内容(图片)
下例为16格水果盘, 并指定相关水果内容
mkFruits({
id: 'fruit',
max: 4,
fruitsData: {
1: {img: {src: '/images/apple.png', itemStyle: 'css样式串', itemClass: '指定样式类'}},
},
})
设置奖品值
默认所有水果的value值为其在数组中的下标值,也可以手动指定
mkFruits({
id: 'fruit',
max: 4,
fruitsData: {
1: {title: '一等奖', value: '001'},
5: {img: {src: '/images/xxx.jpg'}, value: '003'},
},
})
如何获取水果盘的实例
运行水果盘需要调用实例方法,首先需要通过getElementsById获取水果盘实例
const Pager = require('../../components/aotoo/core/index')
const mkFruits = require('../../components/modules/fruit')
Pager({
data: {
fruitConfig: mkNavball({
id: 'fruit'
}),
},
onReady(){
// 获取水果盘实例
const instance = this.fruit
// 或者
// const instance = this.getElementsById('fruit')
}
})
run 运行水果盘
onReady() {
// 获取水果盘实例
const instance = this.getElementsById('fruit')
instance.run()
}
源码戳这里
下列小程序DEMO包含下拉菜单、通用型筛选列表、索引列表、markdown(包含表格)、评分组件、水果老虎机、折叠面板、双栏分类导航(左右)、刮刮卡、日历等组件
