开发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浏览看看,正常的话,运行效果图如下
【JavaScript】制作一个抽奖转盘页面

💡小提示

试试修改传入的参数,例如

t.draw({
//...
mode:1,//改变为指针转动
/...
});
//...
t.onStart({
	index: 3,//抽奖概率自己写,传入预定奖品的index
	success:(res)=>{
 		//...
	}
});

可根据其它的需求改

到此结束,如阅读中有遇到什么问题,请在文章结尾评论处留言,ヾ( ̄▽ ̄)ByeBye

发表回复