开发H5项目,有时会遇到一个需求,需要制作抽奖转盘的网页,这个实现步骤,如果拿现成的改来做是容易的,但是想着全靠自己做是不容易的,下面会讲,全靠自己做,能掌握到吗
目录一览
- 1.设计网页
- 2. 编写脚本
- 3. 编写模块
- 4. 实现方法
-
- 1. 绘制转盘
- 2. 开始抽奖
- 4.运行效果
1.设计网页
首先创建一个网页文件,例如index.html
,制作抽奖转盘页面,源代码如下,通过修改样式<style>
里设置好背景色,还有转盘组件的位置,再加一个抽奖按钮,写好大概逻辑,还有需要调用的一些方法
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Turntable 转盘</title>
<style>
body{
background-color: #333;
}
.box{
text-align: center;
}
.box #box{
width: 280px;
height: 280px;
}
.box button{
margin-top: 20px;
padding: 0.6em 2.5em;
font-size: 1em;
border-radius: 10px;
border-color: rgba(0, 0, 0, 0.4);
color: #fff;
background: linear-gradient(#eee,#f50);
}
</style>
</head>
<body>
<div class="box">
<div id="box"></div>
<div>
<button id="btnStart">抽奖</button>
</div>
</div>
<script type="module">
import Turntable from './turntable.js';//引用模块
window.onload = () => {
//...加载脚本
}
</script>
</body>
</html>
2. 编写脚本
接着,写一个加载脚本的处理逻辑,代码如下,使用Turntable
对象创建前,需要先引用一个模块
const t = new Turntable({
window,
elemId: 'box',
});
//TODO: 最多7个
const resouce = [
{
image: './img/e7b2e38c8d66613d1dd869b199031d8e.jpeg',
title: '特等奖'
},
{
image: './img/b5ff4601d9679f502f8f9e737bdd7049.jpeg',
title: '谢谢惠顾'
},
{
image: './img/e7b2e38c8d66613d1dd869b199031d8e.jpeg',
title: '一等奖'
},
{
image: './img/b5ff4601d9679f502f8f9e737bdd7049.jpeg',
title: '谢谢惠顾'
},
{
image: './img/e7b2e38c8d66613d1dd869b199031d8e.jpeg',
title: '二等奖'
},
{
image: './img/e7b2e38c8d66613d1dd869b199031d8e.jpeg',
title: '三等奖'
},
{
image: './img/b5ff4601d9679f502f8f9e737bdd7049.jpeg',
title: '谢谢惠顾'
},
];
//加载图片资源可能有延迟,通过异步处理
Promise.all(resouce.map((item)=>{
return new Promise((resolve,reject)=>{
let img = new Image();
img.onload = () => {
item.image = img;
resolve(item);
};
img.onerror = reject;
img.src = item.image;
});
})).then((res)=>{
t.draw({
// mode:1,
goods:res,
});
});
//设置按钮点击事件
document.getElementById('btnStart').onclick = () => {
t.onStart({
// index: 3,//抽奖概率自己写,传入预定奖品的index
success:(res)=>{
// console.log('ok', res);
const good = res.goods[res.index];
if (good.title) {
if (good.title.indexOf('奖')>=0) alert(`🙂恭喜恭喜您!抽到奖品${good.title}.`)
else alert('🙆很遗憾!未中奖.')
}else {
alert(`🙂恭喜恭喜您!抽到奖品${res.index+1}.`)
}
}
});
}
3. 编写模块
接下来,看上面有引用的一个模块文件
turntable.js
,没有的就把它新建好,在一个模块中去实现上面未实现的调用方法,代码如下
export default class Turntable {
//定义私有属性
#elemBgImg;
#elemPointerImg;
#bgImgCanvas;
#pointerImgCanvas;
/**
* 构造函数
* */
constructor(e){
const { document } = e.window;
// 获取占位元素(盒子)
const elemBox = document.getElementById(e.elemId);
// 创建元素
const elemBgImg = document.createElement('img');
const elemPointerImg = document.createElement('img');
const size = elemBox.offsetWidth;
// 设置元素样式
elemBox.style.display = 'inline-block';
elemBox.style.position = 'relative';
elemBgImg.style.transform = `rotate(0deg)`;
elemBgImg.style.pointerEvents = 'none';//屏蔽触摸点击
elemPointerImg.style.pointerEvents = 'none';
elemBgImg.style.position = 'absolute';
elemPointerImg.style.position = 'absolute';
elemPointerImg.style.margin = 'auto';
elemBgImg.style.margin = 'auto';
elemBgImg.style.left = 0;
elemBgImg.style.top = 0;
elemBgImg.style.right = 0;
elemBgImg.style.bottom = 0;
elemPointerImg.style.left = 0;
elemPointerImg.style.top = 0;
elemPointerImg.style.right = 0;
elemPointerImg.style.bottom = 0;
elemBgImg.width = size;
elemBgImg.height = size;
elemPointerImg.width = size*0.3;
elemPointerImg.height = size*0.3;
//将元素添加到占位元素(盒子)组件中
elemBox.appendChild(elemBgImg);
elemBox.appendChild(elemPointerImg);
this.#elemBgImg = elemBgImg;
this.#elemPointerImg = elemPointerImg;
//转盘
const bgImgCanvas = document.createElement('canvas');
bgImgCanvas.width = size;
bgImgCanvas.height = size;
this.#bgImgCanvas = bgImgCanvas;
//指针
const pointerImgCanvas = document.createElement('canvas');
pointerImgCanvas.width = elemPointerImg.width;
pointerImgCanvas.height = elemPointerImg.height;
this.#pointerImgCanvas = pointerImgCanvas;
}
/**
* 销毁
* */
destory(){
this.#bgImgCanvas.remove();
this.#pointerImgCanvas.remove();
}
/**
* 绘制转盘组件
* */
draw(config){
//...
}
/**
* 开始抽奖
* */
onStart(config){
//...
}
}
4. 实现方法
接下来,写方法的实现细节要复杂得多,如果看着比较吃力,就需要补充数学知识哦,关键点是三角图形学中的勾股定理,请慢慢摸索,边学边做
1. 绘制转盘
先实现绘制转盘组件方法draw()
,其中用到了数学的一个知识点:三角函数,代码如下
class Turntable {
//定义私有属性
#goods=[];
#pointerDeg = 0;
#mode = 0;
//...
/**
* 绘制转盘组件
* */
draw(config){
const data = {
padding: 5,//组件内边距
goods: ['#f00','#0f0','#00f'],//默认三基色填充礼品区
pointerColor: '#fa0',//指针色
borderWidth: 10,//边框大小
borderColor: '#fa0',//边框色
imgSize: 40,//礼品图片大小
mode:0,//工作模式: 0:转动转盘;1:转动指针
};
Object.assign(data,config);
this.#mode = data.mode==0 ? 0 : 1;
const bgImgCanvas = this.#bgImgCanvas;
const bgImgCtx = bgImgCanvas.getContext('2d');
const coodrinte = {
padding: data.padding,
r: bgImgCanvas.width/2-data.padding
};
coodrinte.centerO = coodrinte.padding + coodrinte.r;
//先绘制转盘底座
bgImgCtx.strokeStyle = data.borderColor;
bgImgCtx.lineWidth = data.borderWidth;
bgImgCtx.fillStyle = '#eee';
bgImgCtx.beginPath();
bgImgCtx.arc(coodrinte.centerO,coodrinte.centerO,coodrinte.r,0,Math.PI*2);
bgImgCtx.fill();
bgImgCtx.stroke();
//再绘制转盘上的
bgImgCtx.strokeStyle = 'rgba(255,255,255,0.3)';
bgImgCtx.lineWidth = Math.max(1,data.borderWidth/3);
bgImgCtx.textAlign = 'center';
const r = coodrinte.r-bgImgCtx.lineWidth;
//转盘角度(弧边)
let startAngle = 0;
let endAngle = 0;
data.goods.forEach((item,index)=>{
let good = {
proportion: Math.round(1000/data.goods.length)/1000,//默认平分概率
};
switch(typeof item){
case 'string':
if (item.charAt(0)=='#') good.bgColor=item;
else good.title=item;
break;
case 'object':
Object.assign(good,item);
break;
default:
throw new Error('定义参数goods有误');
}
good.startAngle = startAngle;
good.endAngle = good.startAngle+Math.PI*2*good.proportion;
//计算角度
let angle = (good.endAngle-good.startAngle)/2-Math.PI*0.5+(index*good.proportion*Math.PI*2);
//余弦函数cosA:表示在一个直角三角形中,∠A(非直角)的邻边与三角形的斜边的比
let x = Math.cos(angle)*(r/2);
//正弦函数sinA:表示在一个直角三角形中,∠A(非直角)的对边与三角形的斜边的比
let y = Math.sin(angle)*(r/2);
// console.log('angle '+angle, 'x='+x+',y='+y);
good.center = {
x:coodrinte.centerO+x,
y:coodrinte.centerO+y,
};
data.goods[index] = good;
startAngle = good.endAngle;
});
//绘制分布在转盘中的图案
data.goods.forEach((item,index)=>{
if (item.bgColor){
bgImgCtx.fillStyle = item.bgColor;
}
//画划分的区域(弧边)
bgImgCtx.beginPath();
bgImgCtx.moveTo(coodrinte.centerO,coodrinte.centerO);
bgImgCtx.arc(coodrinte.centerO,coodrinte.centerO,r,item.startAngle-Math.PI*0.5,item.endAngle-Math.PI*0.5);
bgImgCtx.closePath();
if (!item.bgColor) {
bgImgCtx.stroke();
bgImgCtx.fillStyle = '#f50';
}else{
}
bgImgCtx.fill();
//是否是转动底盘
if (this.#mode==0) {
bgImgCtx.save();
let cX = item.center.x;
let cY = item.center.y;
let angle = Math.round(Math.atan(Math.abs(coodrinte.centerO-cY)/Math.abs(coodrinte.centerO-cX))*180/Math.PI);
// console.log(index+'. angle > '+angle)
//TODO: 暂时适配最多7个
switch(angle){
case 0:
if (cX<coodrinte.centerO) angle+=90;
else angle-=90;
break;
case 90:
angle=0;
break;
default:
if (cX<coodrinte.centerO){
if (cY<coodrinte.centerO){
angle+=90;
}else if (angle<20) {
angle+=45;
}else if (angle<38) {
angle-=25;
}else if (angle<=40) {
angle+=10;
}else if (angle==45) {
}else if (angle<=60) {
angle-=30;
}else {
angle+=10;
}
}else{
if (cY<coodrinte.centerO){
angle=270-angle;
}else{
angle-=90;
}
}
}
if (angle!=0){
//旋转角度,以转盘中心点对齐
angle=Math.PI*(angle/180);
bgImgCtx.translate(cX,cY);
bgImgCtx.rotate(angle);
bgImgCtx.translate(-cX,-cY);
}
}
if (item.image) {
bgImgCtx.drawImage(item.image,item.center.x-data.imgSize*0.5,item.center.y-data.imgSize*0.5,data.imgSize,data.imgSize);
}
if (item.title) {
bgImgCtx.fillStyle = '#fff';
bgImgCtx.fillText(item.title,item.center.x,item.image ? (item.center.y+data.imgSize*0.9) : item.center.y);
}
if (this.#mode==0){
bgImgCtx.restore();
}
//画辅助线
// bgImgCtx.beginPath();
// bgImgCtx.moveTo(coodrinte.centerO,coodrinte.centerO);
// bgImgCtx.lineTo(item.center.x,item.center.y);
// bgImgCtx.stroke();
});
this.#goods = data.goods;
this.#elemBgImg.src = bgImgCanvas.toDataURL();
//绘制指针
const pointerImgCanvas = this.#pointerImgCanvas;
const pointerImgCtx = pointerImgCanvas.getContext('2d');
const pointerData = {
r: pointerImgCanvas.width/2
};
pointerData.r1 = pointerData.r*0.36;
pointerData.r2 = pointerData.r*0.60;
pointerImgCtx.fillStyle = data.pointerColor;
startAngle = Math.PI*1.58;
endAngle = startAngle + Math.PI*1.86;
pointerImgCtx.lineWidth = 2;
pointerImgCtx.beginPath();
pointerImgCtx.arc(pointerData.r,pointerData.r,pointerData.r2,startAngle,endAngle);
pointerImgCtx.lineTo(pointerData.r,0);
pointerImgCtx.closePath();
pointerImgCtx.fill();
pointerImgCtx.stroke();
//将绘制的图形设置到图片元素
this.#elemPointerImg.src = pointerImgCanvas.toDataURL();
}
}
2. 开始抽奖
实现开始抽奖方法onStart()
,可通过传入参数对象config
,修改默认配置,代码如下,抽奖结果会通过回调方法succes()
返回
class Turntable {
//定义私有属性
#animing = false;
//...
/**
* 开始抽奖
* */
onStart(config){
if (this.#animing) return;//防止双击(误操作)
this.#animing = true;
const data = {
minRotationNum: 3,//至少转动圈数
duration:3,//转动耗时3s
success(res){},//结束时回调
index: -1,//抽得预定奖品,默认随机
};
Object.assign(data,config);
const goods = this.#goods;
if (data.index<0 || data.index>=goods.length) {
//抽得随机奖品
data.index = Math.trunc(Math.random()*10%goods.length);
}
const elemActive = this.#mode==0 ? this.#elemBgImg : this.#elemPointerImg;
const style = elemActive.style;
const pointerDeg = this.#pointerDeg;
const index = data.index;
//定义动画结束监听
const listener = (event) => {
event.preventDefault();
elemActive.removeEventListener('transitionend', listener);
//重置动画样式
style.transition = 'none';
style.transform = `rotate(${this.#pointerDeg}deg)`;
//结束和回调
this.#animing = false;
data.success({
goods,
index
});
};
elemActive.addEventListener('transitionend', listener, false);
//处理过渡动画
let inDeg = Math.round(goods[index].startAngle/Math.PI*180);
let outDeg = Math.round(goods[index].endAngle/Math.PI*180);
let deg = (Math.round(Math.random()*(outDeg-inDeg)))+inDeg;
// console.log('rand '+index, `${inDeg}°~${outDeg}° ${deg}° current:${pointerDeg}`);
//转盘是反向旋转的
if (this.#mode==0) deg = 360-deg;
deg += (Math.round(Math.random()*10)+data.minRotationNum)*360;
//简化角度
this.#pointerDeg = deg%360;
//修改完样式,就可开始动画
style.transition = `all ${data.duration}s ease-out`;
style.transform = `rotate(${deg}deg)`;
}
}
4.运行效果
讲到最后,用浏览器打开网页index.html
浏览看看,正常的话,运行效果图如下
💡小提示
试试修改传入的参数,例如
t.draw({ //... mode:1,//改变为指针转动 /... }); //... t.onStart({ index: 3,//抽奖概率自己写,传入预定奖品的index success:(res)=>{ //... } });
可根据其它的需求改
到此结束,如阅读中有遇到什么问题,请在文章结尾评论处留言,ヾ( ̄▽ ̄)ByeBye