最近工作中重构了抽奖转盘,给大家提供一个开发转盘抽奖的思路
需求
1、转盘根据奖品数量不同而有变化
2、canvas
目录结构
由于业务需要所以开发了两个版本抽奖,dom和canvas,不过editor.js部分只能替换图片,没有功能逻辑。
需要注意的是此目录隐藏了一个动态数据类(dataStore),因为集成在项目里了,所以没有体现。
Spirts
精灵类生成实例,会包括基础属性:width、height、x、y和方法:setOpacity、drawCircular、setRotate、draw
下面是几个重要的精灵构造器:背景、转盘背景、单独奖品和转盘组
主要说一下转盘组group概念,其实就是单独生成一个canvas,把需要转动的精灵放在其中,最后旋转group。
/*
* 精灵核心 基类
* */
class Spirt {
constructor({}) {}
// 精灵透明度调节
setOpacity(opy, callback) {}
// 画圆形图片
drawCircular(fn) {}
// 精灵旋转调节
setRotate() {}
// 画精灵
draw() {}
}
// 背景
class Bg extends Spirt {
constructor({ ...args }) {
super({ ...args });
if (args.height == '100%') {
this.height = this.canvas.height;
}
}
}
// 转盘背景
class Turn extends Spirt {
constructor({ ...args }) {
super({ ...args });
}
draw() {
this.drawCircular(() => {
super.draw();
});
}
}
// 每一个奖品
class Item extends Spirt {
constructor({ ...args }, rid) {
super({ ...args });
this.rid = rid;
}
draw(angle, x, y) {
this.setRotate(angle, () => super.draw(), x, y);
}
}
// 转盘组
class Group {
constructor({ canvas, width, height, x, y }) {
this.ctx = canvas.getContext('2d');
this.createElement(width, height);
this.x = x;
this.y = y;
}
createElement(width, height) {
this.group_canvas = document.createElement('canvas');
this.group_ctx = this.group_canvas.getContext('2d');
this.group_canvas.width = +width;
this.group_canvas.height = +height;
this.group_canvas.fillStyle = 'red';
}
draw(angle, x, y) {
this.setRotate(
angle,
() => this.ctx.drawImage(this.group_canvas, this.x, this.y),
x,
y
);
}
// 精灵旋转调节
setRotate(
angle,
fn,
x = this.canvas.width / 2,
y = this.canvas.height / 2
) {
this.ctx.save();
this.ctx.beginPath();
this.ctx.translate(x, y); // 将绘图原点移到画布中点
this.ctx.rotate((Math.PI / 180) * angle); // 旋转角度
this.ctx.translate(-x, -y); // 将画布原点移动
fn && fn();
this.ctx.closePath();
this.ctx.restore();
}
}
Config
基础数据类,包括基础数据:转盘分块、角度、半径、每一块对应奖品、旋转总时长、旋转速度等
主要说一下转盘分块:如果符合规律,就用函数代替,如果不符合规律就用映射
let layout = {
1: [1, 10, 1, 10, 1, 10],
2: [1, 2, 10, 1, 2, 10],
3: [1, 10, 2, 10, 3, 10],
4: [2, 10, 3, 10, 4, 10, 1, 10],
5: [2, 3, 4, 10, 5, 10, 1, 10],
6: [2, 3, 4, 10, 5, 6, 1, 10],
7: [3, 4, 10, 5, 6, 7, 10, 1, 2, 10],
8: [3, 4, 10, 5, 6, 7, 8, 10, 1, 2]
};
下面为部分代码
class Config {
constructor(prize = new Array(3), resImg) {
this.awards_len = prize.length >= 7 ? 10 : prize.length >= 4 ? 8 : 6;
this.awards_angle = 360 / this.awards_len;
this.awards_r = 320;
this.awards_cir = 2 * Math.PI * this.awards_r;
let nums = {
6: 2.5,
8: 2,
10: 2
};
this.awards_item_margin = 40;
this.award_item_size =
this.awards_cir / this.awards_len / nums[this.awards_len];
this.duration = 2000;
// 奖品详情
this.awards = getAwards(resImg, prize.length);
}
}
/**
* 获取奖品列表
* @param {*} num
*/
function getAwards(resImg, num) {
let arr = layout[num];
return arr.map(rid => {
let res = resImg[mapAwards[rid]];
return { rid, res, className: mapAwards[rid] };
});
}
Res
资源类主要做一些图片初始化的操作
// 获取游戏资源
class Res extends Resource {
constructor(dataStore) {
super({ dataStore });
let { gameJson } = dataStore;
this.res = {
...gameJson.staticSpirts.BG
};
this.dataStore = dataStore;
}
// 编辑页面改变页面图片能力。
setImg(data) {
this.res[data.num].imgUrl = data.imgUrl;
if (['BG', 'TITLE', 'TURNTABLE_BG', 'PLAYBTN'].includes(data.num)) {
$(`.turnTableNew_${data.num}`).css(
'background-image',
`url('${HOST.FILE + data.imgUrl}')`
);
} else {
$(`.turnTableNew_${data.num}`).attr(
'src',
`${HOST.FILE + data.imgUrl}`
);
}
return {
staticSpirts: this.res
};
}
}
Director
导演类,主要操作的是转盘动画的逻辑
主要逻辑是:
1、addCLick: canvas添加点击事件
2、drawStatic:画静态元素
3、drawZhuanPan:这个为单独canvas,group内部包括画转盘,奖品
4、drawPlayBtn: 画按钮
5、当点击抽奖按钮执行updatedRotate函数让单独转盘canvas旋转即可
6、当旋转角度和获取奖品角度一致时停止
class turnTable extends Director {
constructor(dataStore) {
let { gameManager } = dataStore;
super(gameManager);
// 从仓库中获取基础数据,canvas和config总配置
this.dataStore = dataStore;
this.canvas = dataStore.canvas;
this.config = dataStore.$gameConfig;
// 当前抽奖的一些基础数据
this.angle = 0;
this.isAnimate = true;
this.lastTime = 0;
this.num = 0;
this.addCLick();
}
// 抽奖结束,需要初始化抽奖
initGame() {
this.state = this.START;
this.angle = 0;
this.num = 0;
this.prizeId = null;
this.isAnimate = true;
this.turnAudio.pause();
this.drawAllElements(this.res, this.set);
}
/**
* 画所有元素
* @param {*} store
* @param {*} res
*/
drawAllElements(res, set) {
this.res = res;
this.set = set;
this.drawStatic(res);
this.drawZhuanPan(this.angle);
this.drawPlayBtn(this.canvas, res);
}
/**
* 画静态元素
*/
drawStatic(res) {
['BG', 'TITLE'].forEach(item => {
let str = item.toLowerCase();
str = str.replace(str[0], str[0].toUpperCase());
let ele = new Spirts[str]({
canvas: this.canvas,
...res[item]
});
ele.draw();
});
}
// 画转盘组
drawZhuanPan(angle) {
this.group = new Spirts['Group']({
canvas: this.canvas,
...this.res['TURNTABLE_BG']
});
this.items = this.drawDynamic(this.group.group_canvas, this.res);
this.group.draw(
angle,
+this.res['TURNTABLE_BG'].x + +this.res['TURNTABLE_BG'].width / 2,
+this.res['TURNTABLE_BG'].y + +this.res['TURNTABLE_BG'].height / 2
);
}
// 画动态元素
drawDynamic(canvas, res) {
let set = this.set;
let items = [];
// 转盘背景1,装饰物
let turnBg = new Spirts['Turn']({
canvas,
img: res['TURNTABLE_BG'].img,
width: res['TURNTABLE_BG'].width,
height: res['TURNTABLE_BG'].height,
x: 0,
y: 0
});
turnBg.draw();
// 转盘背景2,盘面
let turnPan = new Spirts['Turn']({
canvas,
img: res['TURNTABLE_PAN'].img,
width: res['TURNTABLE_PAN'].width,
height: res['TURNTABLE_PAN'].height,
x: (res['TURNTABLE_BG'].width - res['TURNTABLE_PAN'].width) / 2,
y: (res['TURNTABLE_BG'].height - res['TURNTABLE_PAN'].height) / 2
});
turnPan.draw();
for (let i = 0; i < set.awards_len; i++) {
// 每一个奖品
let item = new Spirts['Item'](
{
canvas,
img: set.awards[i].res.img,
width: set.award_item_size,
height: set.award_item_size,
x: turnBg.width / 2 - set.award_item_size / 2,
y:
(turnBg.height - turnPan.height) / 2 +
set.awards_item_margin
},
set.awards[i].rid
);
item.draw(
set.awards_angle / 2 + set.awards_angle * i,
turnBg.width / 2,
turnBg.height / 2
);
// 画线
let line = new Spirts['Item']({
canvas,
img: res['LINE'].img,
width: res['LINE'].width,
height: res['LINE'].height,
x: turnBg.width / 2 - res['LINE'].width / 2,
y: (turnBg.height - turnPan.height) / 2
});
line.draw(
set.awards_angle * i,
turnBg.width / 2,
turnBg.height / 2
);
// 放到items数组内,后期转盘停止校验用
items.push(item);
}
return items;
}
// 画按钮
drawPlayBtn(canvas, res) {
let playBtn = new Spirts['PlayBtn']({
canvas,
...res['PLAYBTN']
});
playBtn.draw();
this.playBtn = playBtn;
}
// 点击事件
addCLick() {
let initX,
isClickState,
cScale = this.config['cScale'] || 1;
this.canvas.addEventListener(tapstart, event => {
initX = event.targetTouches
? event.targetTouches[0].clientX
: event.offsetX / cScale;
let y = event.targetTouches
? event.targetTouches[0].clientY
: event.offsetY / cScale;
isClickState = isCheck.call(this.playBtn, initX, y);
// 点击回调
if (isClickState && this.isAnimate) {
/**
* 按钮不可点击
* 初始化总时长
* 初始化速度
* 初始化当前时间
*/
this.isAnimate = false;
this.set.is_animate = true;
this.set.jumping_total_time =
Math.random() * 1000 + this.set.duration;
this.set.speed = (this.set.jumping_total_time / 2000) * 10;
this.lastTime = new Date().getTime();
this.run();
this.getPrize()
.then(res => {
if (!res) {
this.prizeId = 10;
return;
}
this.prizeId = +res.prizeLevel + 1;
})
.catch(_ => {
this.prizeId = 10;
this.initGame();
this.state = this.END;
});
}
});
}
updatedRotate() {
let curTime = new Date().getTime(),
set = this.set,
speed = 1;
/**
* 转盘停止,需要满足一下条件
* 1.大于总时间
* 2.有奖品id
* 3.速度降为1
* 4.转盘角度对应奖品id位置
* 角度做了容错处理,当前角度范围中心位置,偏移量为5
* 公式:通过旋转角度计算当前奖品index
* 通过items奖品列表计算当前奖品rid
* rid和prizeId对比,如果结束抽奖
*/
if (
curTime - this.lastTime >= set.jumping_total_time &&
this.prizeId &&
speed == 1
) {
let resultAngle = 360 - (this.angle % 360);
let index = (resultAngle / set.awards_angle) >> 0;
let centerAngle = set.awards_angle * (index + 0.5);
if (
this.items[index].rid == this.prizeId &&
(resultAngle > centerAngle - 5) &
(resultAngle < centerAngle + 5)
) {
this.comAudio.play();
this.state = this.PAUSE;
}
}
this.num++;
speed = Math.max(
set.speed -
(18 * this.num * (set.speed - 1)) / set.jumping_total_time,
1
);
this.angle += speed;
this.drawAllElements(this.res, this.set);
}
// 渲染画布
render() {
switch (this.state) {
case this.START:
this.updatedRotate();
break;
case this.ERROR:
break;
case this.PAUSE:
this.state = this.END;
setTimeout(() => {
this.showResult();
this.initGame();
}, 1000);
break;
case this.END:
// 打开指定页面
break;
}
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。