开发H5项目,有时会遇到一个需求,需要制作抢红包,或者下红包雨的网页,这个实现步骤,如果拿现成的改来做是容易的,但是想着全靠自己做是不容易的,接下来开始讲,想不想自己做,有把握学到吗
目录一览
- 1. 设计网页
- 2. 编写脚本
- 3. 编写模块
- 4. 实现方法
- 1. 绘制红包和钱袋子
- 2. 下红包雨
- 3. 移动钱袋子
- 5.运行效果
1. 设计网页
首先创建一个网页文件,例如index.html
,制作下红包雨的页面,源代码如下,通过修改样式<style>
里设置好背景色,还有组件要填充到全屏,再加一个开始按钮(按钮可以不要,自动开始吧),写好大概逻辑,还有需要调用的一些方法
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Red packet rain</title>
<style>
html{
height: 100%;
}
body{
margin: 0;
height: 100%;
}
#box{
width: 100%;
height: 100%;
background: linear-gradient(#f00,#fff);
}
.float-box{
position: absolute;
left: 0;
top: 0;
right: 0;
color: #fff;
text-align: center;
padding: 20px;
}
</style>
</head>
<body>
<div class="float-box">
<span id="timer"></span>
</div>
<div id="box"></div>
<script type="module">
import RedPacketRain from './red_packet_rain.js';
window.onload=()=>{
//加载脚本的处理逻辑...
}
</script>
</body>
</html>
2. 编写脚本
接着,写一个加载脚本的处理逻辑,代码如下,使用RedPacketRain
对象创建前,需要先引用一个模块
const elemTimer = document.getElementById('timer');
new RedPacketRain({
id:'box',
success:res=>{
res.onStart();
let time=10;//这个是倒计时,单位s
let timer = setInterval(()=>{
elemTimer.innerText = `距离结束还有${time--}s`;
if (time<0){
clearInterval(timer);
res.onStop({
success:(res)=>{
alert('游戏结束,\n钱袋有¥'+res.money)
}
});
}
},1000);
}
},window);
3. 编写模块
接下来,看上面有引用的一个模块文件red_packet_rain.js
,没有的就把它新建好,在一个模块中去实现上面未实现的调用方法,代码如下
export default class RedPacketRain{
constructor(conf,window){
// 这里做一些初始化的工作...
}
}
4. 实现方法
接下来,在初始化中去写方法的实现细节要复杂得多,如果看着比较吃力,就先收藏好,以后有时间慢慢摸索,边学边做
1. 绘制红包和钱袋子
在构造方法constructor()
里做初始化,可以先把需要的东西,就是红包和钱袋子两个,都绘制出来,代码如下
export default class RedPacketRain{
// 定义需要用到的一些私有属性
#canvasCtx;
#imgBg;
#drawImgPurse;
#drawImgRedPacket;
#isContinue=false;
constructor(conf,window){
// 这里做一些初始化的工作...
const { document } = context;
if(conf.id==undefined) throw new Error('not find element id');
Object.assign(conf,{
redPackW:50,//红包的宽度
//...
});
let box = document.getElementById(conf.id);
let canvas = document.createElement('canvas');
canvas.width = box.offsetWidth;
canvas.height = Math.max(box.offsetHeight,canvas.width);
const ctx = canvas.getContext('2d');
ctx.textAlign = 'center';
ctx.font = ctx.font.replace(/\d+/,23);
//红包数据
const redPack = {
w: conf.redPackW,
h: conf.redPackW*1.4,
absX: 10,
absY: 10
};
//钱袋子数据
const purse = {
w: conf.redPackW*2,
h: conf.redPackW*2,
x: 0,
y: 0,
absX: 100,
absY: 10,
count: 0,
money: 0
};
//清空画布方法
const drawClearEmpty = ()=>{
ctx.clearRect(0,0,canvas.width,canvas.height);
};
//绘制红包方法
const drawRedPacket = (x,y)=>{
//绘制红包形状
let r = redPack.w*0.1;
ctx.fillStyle='#f00';
ctx.strokeStyle='#333';
ctx.beginPath();
ctx.arc(x+r,y+r,r,Math.PI,-0.5*Math.PI);
let x2 = x+redPack.w;
ctx.lineTo(x2-r,y);
ctx.arc(x2-r,y+r,r,-0.5*Math.PI,0);
let y2 = y+redPack.h;
ctx.lineTo(x2,y2-r);
ctx.arc(x2-r,y2-r,r,0,0.5*Math.PI);
ctx.lineTo(x+r,y2);
ctx.arc(x+r,y2-r,r,0.5*Math.PI,Math.PI);
ctx.closePath();
ctx.fill();
//绘制盖子
let centerX = x+redPack.w/2;
let centerY = y-redPack.w*0.6;
ctx.save();
ctx.clip();
ctx.arc(centerX,centerY,redPack.w,-0.5*Math.PI,1.5*Math.PI);
ctx.stroke();
ctx.restore();
//绘制盖子纽扣
centerY = y+redPack.w*0.4;
ctx.fillStyle='#991';
ctx.beginPath();
ctx.arc(centerX,centerY,redPack.w*0.15,0,2*Math.PI);
ctx.fill();
ctx.stroke();
//绘制纽扣中间
r = redPack.w*0.05;
ctx.fillStyle='#000';
ctx.beginPath();
ctx.rect(centerX-r,centerY-r,2*r,2*r);
ctx.fill();
};
//绘制钱袋子方法
const drawPurse = (x,y)=>{
let r = purse.w/2;
let centerX = x + r;
let centerY = y + purse.h-conf.redPackW*0.8;
//绘制袋子容器形状
ctx.fillStyle='#f55';
ctx.beginPath();
ctx.arc(centerX,centerY,r,0,2*Math.PI);
ctx.fill();
ctx.stroke();
//绘制袋口
ctx.beginPath();
ctx.save();
ctx.translate(-centerX*0.6,0);
ctx.scale(1.6,1);
ctx.arc(centerX,centerY-r*0.62,r*0.6,0,2*Math.PI);
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.fillStyle='#333';
ctx.arc(centerX,centerY-r*0.62,r*0.5,0,2*Math.PI);
ctx.fill();
ctx.stroke();
ctx.restore();
purse.h = purse.h+conf.redPackW*0.2;
};
//绘制袋子图片方法
this.#drawImgPurse = (isSaved)=>{
if(!isSaved) drawClearEmpty();
ctx.drawImage(this.#imgBg,purse.absX,purse.absY,purse.w,purse.h,purse.x,purse.y,purse.w,purse.h);
ctx.fillStyle='#ff5';
let r = purse.w/2;
ctx.fillText('¥'+purse.money.toFixed(2),purse.x+r,purse.y+(purse.h+r)*0.55,purse.w);
};
//绘制红包图片方法
this.#drawImgRedPacket = (x,y)=>{
ctx.drawImage(this.#imgBg,redPack.absX,redPack.absY,redPack.w,redPack.h,x,y,redPack.w,redPack.h);
};
//其它逻辑省略...
//重绘方法
const redraw = () => {
//...
};
//封装上下文,开始和结束方法通过初始化完成后返回给外部调用
const Context = {
onStart:()=>{
if(this.#isContinue) return;
this.#isContinue=true;
redraw();//开始时,调用重绘方法
},
onStop:(conf={})=>{
if(!this.#isContinue) return;
this.#isContinue=false;
//停止后,将钱袋子的数据传给外部调用者
if(typeof conf.success=='function') conf.success({
count: purse.count,
money: purse.money
})
}
};
//加载图片...
(new Promise((resolve,reject)=>{
drawRedPacket(redPack.absX,redPack.absY);
drawPurse(purse.absX,purse.absY);
let img = new Image();
img.onload = ()=>resolve(img);
img.onerror = reject;
img.src = canvas.toDataURL();
})).then(res=>{
this.#imgBg=res;//绘制好后生成图片
purse.x=(canvas.width-purse.w)/2;
purse.y=canvas.height-purse.h;
//接下来我们会以这个图片作为素材来绘制动画
this.#drawImgPurse();
if(typeof conf.success=='function') conf.success(Context);//传给外部
}).catch(err=>{
throw new Error(err)
});
this.#canvasCtx = ctx;
box.appendChild(canvas);
}
}
2. 下红包雨
绘制搞定了,接下来,就在重绘方法redraw()
中实现红包雨效果,代码如下
export default class RedPacketRain{
//...
constructor(conf,window){
//...
Object.assign(conf,{
redPackW:50,//红包的宽度
refreshDelay:60,//刷新延迟 ms
speedDown:10,//下落速度
waitTime:20,//60*60ms
waitTimeRadom:5,//紧密度
moneyRadom:0.05,//随机金额最大值
});
//...
//生成随机位置的红包
const createRedPacket = () => {
let padding = 10;
return {
x: padding + Math.trunc(Math.random()*(canvas.width-redPack.w-padding)),
y: 0-redPack.h,
money: 0.01 + Math.trunc(Math.random()*100*conf.moneyRadom)/100,
}
};
//存放所有红包数据的集合
const redPackets = [];
redPackets.push(createRedPacket());
let i=0;
//重绘方法
const redraw = () => {
if(!this.#isContinue) return;
//考虑到性能,建议每次调用
window.requestAnimationFrame(()=>{
this.#drawImgPurse();
let outIndex=[];
let x = purse.x+purse.w;
let y = purse.y+purse.h;
redPackets.forEach((p,index)=>{
this.#drawImgRedPacket(p.x,p.y);
p.y+=conf.speedDown;
//判断红包是否在钱袋子上面,收红包处理一下
if(p.x>purse.x && p.x+redPack.w<x && p.y>purse.y && p.y+redPack.h<y) {
purse.count++;
purse.money+=p.money;//将收到的红包金额加到钱袋子中
outIndex.push(index);
} else if(p.y>canvas.height) {
outIndex.push(index);//将不再出现的红包加入标记
}
});
outIndex.forEach(i=>redPackets.splice(i,1));//删除被标记的红包
setTimeout(redraw,conf.refreshDelay);
if (i>conf.waitTime){
if (conf.waitTimeRadom>0) {
if (Math.random()*conf.waitTimeRadom>conf.waitTimeRadom/2) {
i=0;
i++;
return;
}
}
redPackets.push(createRedPacket());//再生成随机红包
i=0;
}
i++;
})
};
//...
}
}
3. 移动钱袋子
这样红包雨就能开始下了,还差个游戏互动,要实现移动钱袋子,玩家会在画布Canvas
元素上按下鼠标健,我们只需要在这里加上监听事件做处理即可,代码如下
export default class RedPacketRain{
//...
constructor(conf,window){
//...
//鼠标左键按下时监听事件做处理
canvas.addEventListener('mousedown',(event)=>{
const { x, y } = event;
if (x>purse.x && x<purse.x+purse.w) {
if (y>purse.y && y<purse.y+purse.h) {
purse.touch={ x:x-purse.x, y:y-purse.y };
}
}
});
//鼠标左键按下并移动时监听事件做处理
canvas.addEventListener('mousemove',(event)=>{
if(!purse.touch) return;
const { x, y } = event;
if (!(y>purse.y && y<purse.y+purse.h)) return;
else if (x<purse.touch.x) return;
else if (x>=canvas.width-(purse.w-purse.touch.x)) return;
purse.x = x-purse.w/2;//如果是合法的左右移动,就改变钱袋子的坐标
});
//鼠标左键松开时监听事件做处理
canvas.addEventListener('mouseup',(event)=>{
if(!purse.touch) return;
purse.touch=null;
});
//...
}
}
5.运行效果
讲到最后,用浏览器打开网页index.html
浏览看看,正常的话,运行效果图如下
💡小提示 试试修改传入的参数,例如
//...
new RedPacketRain({
redPackW:50,//红包的宽度
refreshDelay:60,//刷新延迟 ms
speedDown:10,//下落速度
waitTime:20,//60*60ms
waitTimeRadom:5,//紧密度
moneyRadom:0.05,//随机金额最大值
success:(res)=>{
//...
}
});
说句实在话,画得红包和钱袋子看起来算不上美丽,献丑了,如果读者自己学会后,自己改就好了😄,
到此结束,如阅读中有遇到什么问题,请在文章结尾评论处留言,ヾ( ̄▽ ̄)ByeBye