前言
昨晚我都要睡觉了,领导突然找我,希望我做一个红包雨活动,给用户发积分,还要能支持自定义积分上限。
实现
说干就干,先根据领导说的这几句话分析一下需求,梳理出需求流程才能更好更快的去开发。
toDoList:
- 生成红包
- 红包雨游戏
- 积分设置上限
1.生成红包
创建一个红包类,红包包含基本属性:宽、高、起始位置(x,y)、包含的积分、下落速度,以及其他用来计算的自定义属性。
class RedPacket {
constructor(opt) {
this.w = opt.w
this.h = opt.h
this.x = opt.x
this.y = opt.y
this.point = opt.point || 0
this.image = opt.image
this.step = opt.step
this.isClick = false
this.clickY = 0
}
}
2.红包雨游戏
红包雨游戏需要不断的创建和销毁元素,最好使用Canvas去实现,能够避免引起浏览器重绘和回流引起的性能问题。
class RedPacketGame {
constructor(opt) {
//...
}
setImage() {
const img = new Image()
img.src = this.image
return img
}
setCanvas() {
const canvas = document.getElementById(this.targerId)
canvas.addEventListener('click', this.handleClick.bind(this))
canvas.width = this.width
canvas.height = this.height
return canvas.getContext("2d")
}
// 开始执行动画
start() {
this.redPacketMove()
}
// 结束执行动画
end() {
this.redPackets = []
this.timer && clearInterval(this.timer)
this.moveTimer && window.cancelAnimationFrame(this.moveTimer)
this.ctx.clearRect(0, 0, this.width, this.height);
}
// 点击事件,获取被点击的红包
handleClick(e) {
e.preventDefault && e.preventDefault()
const pos = {
x: e.clientX,
y: e.clientY
}
for (let i = 0, length = this.redPackets.length; i < length; i++) {
const redPacket = this.redPackets[i]
if (
pos.x >= redPacket.x && pos.x - redPacket.x <= redPacket.w &&
pos.y >= redPacket.y && pos.y - redPacket.y <= redPacket.h
) {
this.redPackets[i].isClick = true
this.redPackets[i].clickY = redPacket.y
this.clicks.push(this.redPackets[i])
break
}
}
}
// 创建红包元素
createRedPacket(point) {
point = point || 0
this.redPackets.push(new RedPacket({
w: 60,
h: 60,
x: this.getPointX(),
y: -60,
step:2,
image: this.redPacketImg,
point: point
}))
}
// 红包动画:先清空画布,在绘制一次
redPacketMove() {
this.ctx.clearRect(0, 0, this.width, this.height);
for (let i = 0, length = this.redPackets.length; i < length; i++) {
const redPacket = this.redPackets[i]
if (redPacket.isClick) {
//...
} else {
redPacket.y = redPacket.y + redPacket.step
if (redPacket.y >= this.height - redPacket.h) {
this.redPackets.splice(i, 1)
i--
length--
continue
}
this.ctx.drawImage(redPacket.image, redPacket.x, redPacket.y, redPacket.w, redPacket.h)
}
}
// this.moveTimer && window.cancelAnimationFrame(this.moveTimer)
this.moveTimer = window.requestAnimationFrame(this.redPacketMove.bind(this))
}
}
requestAnimationFrame实现动画相比定时器或者延时器的优势在于能利用显示器的屏幕刷新频率,改善视觉效果减少卡顿。并且当页面未激活时(切后台)动画会停止。浏览器基本都支持该属性(图片来自 can i use),低版本可以使用延时器进行兼容。
requestAnimationFrame兼容ie10以下以及其他浏览器
(function() {
var lastTime = 0;
var vendors = ['webkit', 'moz'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame'];
}
if(!window.requestAnimationFrame) {
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16.7 - (currTime - lastTime));
var id = window.setTimeout(function() {
callback(currTime + timeToCall);
}, timeToCall);
lastTime = currTime + timeToCall;
return id;
}
}
if(!window.cancelAnimationFrame) {
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
}
}
})();
红包元素重叠问题:红包的初始位置是随机的所以很大几率会出现红包重叠问题,采取的解决方案是:将页面宽度根据红包的宽度切割,如下图,红包的起始x坐标为每个区域的左边界,y坐标固定某个数值,避免红包重叠。
// 设置每一列的起点,红包宽度60,屏幕两侧各留出10像素空间防止红包超出左右屏幕
setColXs() {
let arr = [], step = Math.floor((this.width - 20) / 60)
for (let i = 0; i < step; i++) {
arr.push(10 + 60 * i)
}
return arr
}
判断红包被点击:监听canvas容器的点击事件,获取触发事件的x、y坐标,当 x > 红包的x坐标,并且 x < 红包的x坐标+红包的宽度,并且y坐标也满足这个条件时,说明这个红包被点击了。
handleClick(e) {
e.preventDefault && e.preventDefault()
if (!this.isIng) return
const pos = {
x: e.clientX,
y: e.clientY
}
for (let i = 0, length = this.redPackets.length; i < length; i++) {
const redPacket = this.redPackets[i]
// 点击的x坐标大于区域的x坐标,并且小于x+width
// 并且点击的y坐标大于区域的y坐标,并且小于y+height
if (
pos.x >= redPacket.x && pos.x - redPacket.x <= redPacket.w &&
pos.y >= redPacket.y && pos.y - redPacket.y <= redPacket.h
) {
this.redPackets[i].isClick = true
this.redPackets[i].clickY = redPacket.y
this.clicks.push(this.redPackets[i])
break
}
}
}
3.积分设置上限
为了增加趣味性,每个红包设置的积分值可以不一样,但是积分的总数又要是一个固定值,如果随机生成红包并且红包的积分值也随机的话,积分总值就无法确认了。因此我的想法是,将生成红包的方法由实例去调用,传入积分值生成的红包总数应该固定住,相当于思路变为了:生成x个相加的和为y个数,即第一个数x1 为随机 0 到 y 之间的一个数、第二个数 x2 为随机 0 到 y-x1 之间的任意一个数。防止数值差值太大已经0出现次数太多,可以在生成随机数时设定最大不超过某个数值。
// 生成n项和为max的数组
function randnum(n, max) {
function random(Min, Max) {
if (Max > 5)Max = 5
var Range = Max - Min;
var Rand = Math.random();
return (Min + Math.round(Rand * Range));
}
var arr = [];
if (max > 0) {
for (var i = 0; i < n; i++) {
var num = 0;
if (i === (n - 1)) {
num = max;
} else {
if (max <= 0) {
num = max = 0;
} else {
num = random(0, max);
max -= num;
}
}
arr.push(num);
}
}
return arr;
}
结尾
因为使用了requestAnimationFrame去保证动画流畅性,才会导致在高刷屏幕下,动画执行频率过快的问题。目前没找到比较完美的解决方案,只写了个打点的脚本,检测fps大于60的话就用延时器重写requestAnimationFrame方法。
打点计算fps代码 查看
ps:本文聊天记录为“微信对话生成器”伪造,如有雷同纯属雷同。
最终实现效果:
完整代码查看:GitHub