这个功能可以分几步实现:
- 界面设计:
转盘区域: 使用 canvas 绘制转盘,可配置扇形数量、颜色、文字等。
按钮: "开始/停止" 按钮控制转盘转动。
编辑按钮: 点击弹出弹窗,编辑转盘项目。
中奖弹窗: 显示中奖结果。
- 数据结构:
使用数组存储转盘项目数据,例如:
javascript
const prizeList = [
{ name: '一等奖', color: '#FFD700' },
{ name: '二等奖', color: '#C0C0C0' },
{ name: '三等奖', color: '#CD7F32' },
// ... 其他奖项
];
- 功能实现:
绘制转盘:
根据 prizeList 数据计算每个扇形的角度。
使用 canvas API 绘制扇形、文字、边框等。
javascript
drawPrizeWheel() {
// 创建离屏 2D canvas 实例,创建canvas元素
const canvas = wx.createOffscreenCanvas({type: '2d', width: 300, height: 300});
// 获取 context。注意这里必须要与创建时的 type 一致const ctx = canvas.getContext('2d');
const centerX = 150;
const centerY = 150;
const radius = 130;
const prizeCount = this.data.prizeList.length;
const anglePerItem = 360 / prizeCount;
// 记录当前角度,初始为 0/90 度
let currentAngle = 90;
for (let i = 0; i < prizeCount; i++) {
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(
centerX,
centerY,
radius,
(i * anglePerItem * Math.PI) / 180,
((i + 1) * anglePerItem * Math.PI) / 180,
);
ctx.closePath();
ctx.fillStyle = this.data.prizeList[i].color;
ctx.fill();
// --- 绘制文字 ---
ctx.save();
// 保存当前画布状态
ctx.font = '12px sans-serif';
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const textAngle = (i * anglePerItem + anglePerItem / 2) * (Math.PI) / 180;
const textX = centerX + radius * 0.7 * Math.cos(textAngle);
const textY = centerY + radius * 0.7 * Math.sin(textAngle);
ctx.fillText(this.data.prizeList[i].name, textX, textY);
ctx.restore();
// 恢复之前的画布状态// --- 文字绘制结束 ---
// 计算当前奖项区域的结束角度
let endAngle = currentAngle + anglePerItem;
if(endAngle > 360){
endAngle %= 360;
}
// 计算当前奖项区域的起始角度和结束角度
// 将角度信息存储到 prizeList 数组中
this.data.prizeList[i].startAngle = currentAngle;
this.data.prizeList[i].endAngle = endAngle;
// 更新 currentAngle,准备绘制下一个区域
currentAngle = endAngle;
}
// 将canvas转换为DataURL格式的图片var dataURL = canvas.toDataURL('image/png', 1);this.setData({wheelImg: dataURL});
},
代码解释:
初始化 currentAngle: 在循环开始之前,将 currentAngle 初始化为 90,表示从 90 度开始绘制。因为canvas 绘制圆弧的起始角度是水平向右的 x 轴正方向,而不是竖直向上的 y 轴正方向。所以这里实际起始角度是从90度开始。
计算结束角度: 在每次循环中,根据 currentAngle 和 anglePerItem 计算当前奖项区域的结束角度 endAngle。
绘制扇形: 使用 currentAngle 和 endAngle 绘制当前奖项区域的扇形。将 currentAngle 和 endAngle 存储到 prizeList 数组中,方便后续判断中奖区域。更新 currentAngle: 将 currentAngle 更新为 endAngle,以便绘制下一个奖项区域。最后把canvas转换为base64图片地址。
记录角度信息: 在 drawPrizeWheel 方法中,我们为prizeList数组中每个奖项对象添加了 startAngle 和 endAngle 属性,用来存储该奖项区域的起始和结束角度。
判断指针位置: 在 stopRotate 方法中,我们循环遍历 prizeList 数组,并根据每个奖项的 startAngle 和 endAngle 判断指针是否落在该区域内。
注意处理了跨越 0 度的情况,如果 startAngle 大于 endAngle,则表示该区域跨越了 0 度,需要分别判断指针是否大于等于 startAngle 或者小于 endAngle。
通过以上修改,奖项区域将从 0 度开始绘制,并且每个区域的角度信息会被正确记录在 prizeList 数组中,然后在 stopRotate方法中准确判断指针落在哪个区域,从而确定中奖结果。
转盘转动:
使用 setInterval/setTimeout 或 requestAnimationFrame 实现动画效果。
控制转速和停止位置。
javascript
<view class="pointer" style="transform: rotate({{pointerAngle}}deg)"></view>
//开始旋转
startRotate() {
if (this.data.isRotating) return;
this.setData({ isRotating: true });
// 生成随机旋转圈数(至少旋转 6 圈)
const randomRounds = 6 + Math.floor(Math.random() * 3);
// 生成随机停止角度
const finalAngle = Math.floor(Math.random() * 360);
// 计算总旋转角度
const totalRotation = randomRounds * 360 + finalAngle;
// 使用 setInterval 实现动画
const startTime = Date.now();const frameRate = 80;
const rotateAnimation = () => {
const currentTime = Date.now();
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / this.data.animationDuration, 1);
// 使用 ease-out 动画曲线
const easeOut = (t) => 1 - Math.pow(1 - t, 3);
// 计算当前角度
const currentAngle = easeOut(progress) * totalRotation;
this.setData({ pointerAngle: currentAngle });
if (progress < 1) {
setTimeout(rotateAnimation, 1000 / frameRate);
} else {
this.stopRotate();
}
};
rotateAnimation();
},
代码解释:
随机旋转圈数: 在 startRotate 方法中,我们使用 randomRounds 变量来生成一个随机的旋转圈数,至少 3 圈,最多 6 圈。
随机停止角度: 使用 finalAngle 变量生成一个 0 到 360 之间的随机角度,表示指针最终停止的位置。
计算总旋转角度: 将旋转圈数转换为角度,再加上最终停止角度,得到总旋转角度 totalRotation。
使用 setInterval/setTimeout 实现动画: 使用 setInterval/setTimeout 按照帧率更新指针角度,并使用 easeOut 动画曲线使指针旋转更自然。
停止动画和判断中奖区域: 在 stopRotate 方法中,根据最终指针角度 finalAngle 计算中奖区域索引,并弹出中奖信息。
旋转指针: wxml 中使用 style="transform: rotate({{pointerAngle}}deg)" 控制指针旋转,pointerAngle 存储指针旋转角度。
指针旋转中心: wxss 中设置 .pointer 的 transform-origin: 50% 100%; 将旋转中心点设置为指针底部。
stopRotate 方法: 修改逻辑,计算指针需要旋转到的角度 targetAngle,并更新 pointerAngle 数据。
现在,转盘指针将会随机旋转几圈,然后随机停止在某个角度,并显示指针指向的奖项作为中奖结果。
中奖判断:
根据停止位置计算中奖索引。
显示中奖弹窗,展示 prizeList 中对应的数据。
javascript
stopRotate() {
let winningIndex = null;
const prizeCount = this.data.prizeList.length;
const anglePerItem = 360 / prizeCount;
const finalAngle = this.data.pointerAngle;
const pointerAngle = (finalAngle) % 360;
// 循环遍历每个奖项区域,判断指针是否落在该区域内
for (let i = 0; i < prizeCount; i++) {
const { startAngle, endAngle } = this.data.prizeList[i];
// 处理跨越 0 度的情况,跨越0度就是该区域的开始角度大于结束角度,如340-20
if (startAngle > endAngle) {
if (pointerAngle >= startAngle || pointerAngle < endAngle) {
winningIndex = i;
break;
}
} else {
if (pointerAngle >= startAngle && pointerAngle < endAngle) {
winningIndex = i;
break;
}
}
}
this.setData({
isRotating: false
});
const winningPrize = this.data.prizeList[winningIndex].name;
wx.showModal({
title: "恭喜!",
content: `您获得了${winningPrize}!`,
showCancel: false,
});
},
代码解释:
获取旋转后的角度: finalAngle 表示指针旋转后的随机角度
计算中奖索引:将 finalAngle取余 360,得到相对于第一个奖项区域起始位置的角度pointerAngle 。
遍历奖项区域: 循环遍历每个奖项,计算每个奖项区域的起始角度 startAngle 和结束角度 endAngle。
判断指针位置: 判断 pointerAngle 是否落在当前奖项区域内 (startAngle 到 endAngle 之间)。
确定中奖索引: 如果指针落在某个奖项区域内,记录下该奖项的索引 winningIndex,并跳出循环。
处理结果: 根据 winningIndex 获取中奖信息,并进行后续处理。如果 winningIndex 为 null,则表示出现异常,需要进行相应的处理。
注意:
(startAngle 到 endAngle 之间)存在跨越0度的情况,要进行判断
如果你的奖项区域绘制顺序或方向与示例不同,你需要相应地调整计算逻辑。
希望这个解决方案可以帮助你!
编辑转盘:
弹窗中使用列表展示 prizeList 数据,可以进行增删改操作。
修改 prizeList 数据后,重新绘制转盘。
javascript
editPrize() {
this.setData({
showModal: true
});
},
closeModal() {
this.setData({
showModal: false
});
},
addNewPrize() {
const colorsLength = this.data.colorsList.length;
const random = Math.floor(Math.random() * colorsLength);
this.setData({
prizeList: [...this.data.prizeList, {
name: '',
color: this.data.colorsList[random] || '#000000'
}],
});
},
deletePrizeItem(e) {
const index = e.currentTarget.dataset.index;
let updatedPrizeList = [...this.data.prizeList];
updatedPrizeList.splice(index, 1); // 从数组中移除对应奖项
this.setData({
prizeList: updatedPrizeList,
});
},
updatePrizeName(e) {
const index = e.currentTarget.dataset.index;
const value = e.detail.value;
let updatedPrizeList = this.data.prizeList;
updatedPrizeList[index].name = value;
this.setData({
prizeList: updatedPrizeList,
});
},
updatePrizeColor(e) {
const index = e.currentTarget.dataset.index;
const color = e.detail.value;
let updatedPrizeList = this.data.prizeList;
updatedPrizeList[index].color = color;
this.setData({
prizeList: updatedPrizeList,
});
},
savePrizeList() {
// 将 prizeList 转换为 JSON 字符串并存储
// 这里可以根据你的需求修改存储方式
const prizeListStr = JSON.stringify(this.data.prizeList);
// console.log("保存的 JSON 字符串:", prizeListStr);
this.closeModal();
this.drawPrizeWheel(); // 重新绘制转盘
},
- 代码示例:
index.wxml
javascript
<view class="container"><view class="wheel-container">
<view class="prize-wheel">
<image class="canvas" src="{{wheelImg}}"></image></view><view class="pointer" style="transform: rotate({{pointerAngle}}deg)"></view>
</view><button class="btn" bindtap="startRotate" disabled="{{isRotating}}">开始</button><button class="btn edit-btn" bindtap="editPrize">编辑</button>
<modal title="编辑奖项" hidden="{{!showModal}}" bindcancel="closeModal">
<view class="edit-area">
<!-- <view class="color-picker-slider">
<view>R: <slider min="0" max="255" value="{{r}}" bindchange="onColorSliderChange" data-channel="r"/></view>
<view>G: <slider min="0" max="255" value="{{g}}" bindchange="onColorSliderChange" data-channel="g"/></view>
<view>B: <slider min="0" max="255" value="{{b}}" bindchange="onColorSliderChange" data-channel="b"/></view>
<view class="color-preview" style="background-color: rgb({{r}}, {{g}}, {{b}});"></view>
</view> -->
<view class="prize-item" wx:for="{{prizeList}}" wx:key="index"><input
class="prize-name"
placeholder="奖项名称"
value="{{item.name}}"
data-index="{{index}}"bindinput="updatePrizeName"
/><input
class="color-picker"
type="color"
value="{{item.color}}"
data-index="{{index}}"bindchange="updatePrizeColor"
/>
<button class="delete-btn" data-index="{{index}}" bindtap="deletePrizeItem">-</button>
</view>
</view>
<button class="btn add-btn" bindtap="addNewPrize">+</button><button class="btn save-btn" bindtap="savePrizeList">保存</button>
</modal>
</view>
index.js
javascript
Page({
data: {
prizeList: [{
name: '一等奖',
color: '#FFD700'
},
{
name: '谢谢参与',
color: '#C0C0C0'
},
{
name: '二等奖',
color: '#CD7F32'
},
{
name: '谢谢参与',
color: '#C0C0C0'
},
{
name: '三等奖',
color: '#A0522D'
},
{
name: '谢谢参与',
color: '#C0C0C0'
},
],
prizeListStr: '',
showModal: false,
rotateAngle: 0, // 用于模拟转盘动画,实际并未使用
animation: null, // 未使用
pointerAngle: 0, // 指针角度
isRotating: false, // 是否正在旋转
animationDuration: 5000, // 动画持续时间 (毫秒)
showColorPickerVisible: false,
currentPrizeIndex: null,
r: 0,
g: 0,
b: 0,
colorsList: [
'#FFD700', '#C0C0C0', '#CD7F32', '#A0522D', '#DC143C', '#FF69B4',
'#BA55D3', '#7B68EE', '#6A5ACD', '#483D8B', '#4682B4', '#00CED1',
'#5F9EA0', '#2E8B57', '#9ACD32', '#FFFF00', '#FFA500', '#FF4500',
'#8B4513', '#D2691E', '#B8860B', '#808000', '#556B2F', '#228B22',
'#008000', '#006400', '#90EE90', '#00FF7F', '#00FA9A', '#20B2AA',
'#235788', '#697656', '#D935E9', '#B69754', '#BFA476', '#BAC289',
'#983471', '#EA4586', '#EABCDE', '#ACBEAD', '#825673', '#65C946',
],
},
onLoad() {
this.drawPrizeWheel();
},
onReady() {
},
onColorSliderChange(e) {
const channel = e.currentTarget.dataset.channel;
const value = e.detail.value;
this.setData({
[channel]: value
});
},
drawPrizeWheel() {
// 创建离屏 2D canvas 实例,创建canvas元素
const canvas = wx.createOffscreenCanvas({type: '2d', width: 300, height: 300});
// 获取 context。注意这里必须要与创建时的 type 一致
const ctx = canvas.getContext('2d');
const centerX = 150;
const centerY = 150;
const radius = 130;
const prizeCount = this.data.prizeList.length;
const anglePerItem = 360 / prizeCount;
// 记录当前角度,初始为 0/90 度
let currentAngle = 90;
for (let i = 0; i < prizeCount; i++) {
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(
centerX,
centerY,
radius,
(i * anglePerItem * Math.PI) / 180,
((i + 1) * anglePerItem * Math.PI) / 180,
);
ctx.closePath();
ctx.fillStyle = this.data.prizeList[i].color;
ctx.fill();
// --- 绘制文字 ---
ctx.save(); // 保存当前画布状态
ctx.font = '12px sans-serif';
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const textAngle = (i * anglePerItem + anglePerItem / 2) * (Math.PI) / 180;
const textX = centerX + radius * 0.7 * Math.cos(textAngle);
const textY = centerY + radius * 0.7 * Math.sin(textAngle);
ctx.fillText(this.data.prizeList[i].name, textX, textY);
ctx.restore(); // 恢复之前的画布状态
// --- 文字绘制结束 ---
// 计算当前奖项区域的结束角度
let endAngle = currentAngle + anglePerItem;
if(endAngle > 360){
endAngle %= 360;
}
// 计算当前奖项区域的起始角度和结束角度
// 将角度信息存储到 prizeList 数组中
this.data.prizeList[i].startAngle = currentAngle;
this.data.prizeList[i].endAngle = endAngle;
// 更新 currentAngle,准备绘制下一个区域
currentAngle = endAngle;
}
// 将canvas转换为DataURL格式的图片
var dataURL = canvas.toDataURL('image/png', 1);
this.setData({wheelImg: dataURL});
},
startRotate() {
if (this.data.isRotating) return;
this.setData({ isRotating: true });
// 生成随机旋转圈数(至少旋转 6 圈)
const randomRounds = 6 + Math.floor(Math.random() * 3);
// 生成随机停止角度
const finalAngle = Math.floor(Math.random() * 360);
// 计算总旋转角度
const totalRotation = randomRounds * 360 + finalAngle;
// 使用 setInterval 实现动画
const startTime = Date.now();
const frameRate = 80;
const rotateAnimation = () => {
const currentTime = Date.now();
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / this.data.animationDuration, 1);
// 使用 ease-out 动画曲线
const easeOut = (t) => 1 - Math.pow(1 - t, 3);
// 计算当前角度
const currentAngle = easeOut(progress) * totalRotation;
this.setData({ pointerAngle: currentAngle });
if (progress < 1) {
setTimeout(rotateAnimation, 1000 / frameRate);
} else {
this.stopRotate();
}
};
rotateAnimation();
},
stopRotate() {
let winningIndex = null;
const prizeCount = this.data.prizeList.length;
const anglePerItem = 360 / prizeCount;
const finalAngle = this.data.pointerAngle;
const pointerAngle = (finalAngle) % 360;
// 循环遍历每个奖项区域,判断指针是否落在该区域内
for (let i = 0; i < prizeCount; i++) {
const { startAngle, endAngle } = this.data.prizeList[i];
// 处理跨越 0 度的情况,跨越0度就是该区域的开始角度大于结束角度,如340-20
if (startAngle > endAngle) {
if (pointerAngle >= startAngle || pointerAngle < endAngle) {
winningIndex = i;
break;
}
} else {
if (pointerAngle >= startAngle && pointerAngle < endAngle) {
winningIndex = i;
break;
}
}
}
this.setData({
isRotating: false
});
const winningPrize = this.data.prizeList[winningIndex].name;
wx.showModal({
title: "恭喜!",
content: `您获得了${winningPrize}!`,
showCancel: false,
});
},
editPrize() {
this.setData({
showModal: true
});
},
closeModal() {
this.setData({
showModal: false
});
},
addNewPrize() {
const colorsLength = this.data.colorsList.length;
const random = Math.floor(Math.random() * colorsLength);
this.setData({
prizeList: [...this.data.prizeList, {
name: '',
color: this.data.colorsList[random] || '#000000'
}],
});
},
deletePrizeItem(e) {
const index = e.currentTarget.dataset.index;
let updatedPrizeList = [...this.data.prizeList];
updatedPrizeList.splice(index, 1); // 从数组中移除对应奖项
this.setData({
prizeList: updatedPrizeList,
});
},
updatePrizeName(e) {
const index = e.currentTarget.dataset.index;
const value = e.detail.value;
let updatedPrizeList = this.data.prizeList;
updatedPrizeList[index].name = value;
this.setData({
prizeList: updatedPrizeList,
});
},
shufflePrizeList() {
let updatedPrizeList = [...this.data.prizeList];
for (let i = updatedPrizeList.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[updatedPrizeList[i], updatedPrizeList[j]] = [updatedPrizeList[j], updatedPrizeList[i]];
}
this.setData({
prizeList: updatedPrizeList,
});
},
updatePrizeColor(e) {
const index = e.currentTarget.dataset.index;
const color = e.detail.value;
let updatedPrizeList = this.data.prizeList;
updatedPrizeList[index].color = color;
this.setData({
prizeList: updatedPrizeList,
});
},
savePrizeList() {
// 将 prizeList 转换为 JSON 字符串并存储
// 这里可以根据你的需求修改存储方式
const prizeListStr = JSON.stringify(this.data.prizeList);
// console.log("保存的 JSON 字符串:", prizeListStr);
this.shufflePrizeList(); //打乱奖项的排序,实现随机性
this.closeModal();
this.drawPrizeWheel(); // 重新绘制转盘
},
})
index.wxss
css
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: auto;
padding-bottom: 0;
padding-bottom: constant(safe-area-inset-bottom); /*兼容 IOS<11.2*/
padding-bottom: env(safe-area-inset-bottom); /*兼容 IOS>11.2*/
}
.wheel-container {
margin-top: 50rpx;
width: 300px;
height: 300px;
position: relative;
}
.prize-wheel {
width: 100%;
height: 100%;
}
.canvas {
width: 100%;
height: 100%;
}
.pointer {
width: 0px;
height: 0px;
border-bottom: 50px solid #ff0000;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
/* background-color: red; */
position: absolute;
bottom: 50%;
left: 50%;
transform-origin: 50% 100%; /* 设置旋转中心点为指针底部 */
z-index: 10;
margin-left: -10px; /* 添加 margin-left */
margin-top: -10px;
}
.pointer::before{
content: "";
width: 20px;
height: 20px;
border: 0;
padding: 0;
border-radius: 50%;
background-color: #ff0000;
position: absolute;
bottom: -60px;
left: -10px;
z-index: 10;
}
.btn {
margin: 10px;
padding: 10px 20px;
border: none;
border-radius: 5px;
background-color: #007bff;
color: #fff;
font-size: 16px;
}
.edit-btn {
background-color: #28a745;
}
.save-btn {
background-color: #28a745;
margin-top: 20px;
}
.add-btn {
background-color: #1989fa;
margin-top: 10px;
}
.color-preview {
width: 50px;
height: 20px;
border: 1px solid #ccc;
}
.edit-area {
padding: 10rpx;
overflow: auto;
height: auto;
max-height: 620rpx;
}
.delete-btn {
background-color: #dc3545;
/* 红色背景 */
color: #fff;
/* 白色文字 */
border: none;
border-radius: 50%;
/* 圆形按钮 */
display: flex;
align-items: center;
justify-content: center;
width: 55rpx !important;
height: 55rpx !important;
padding: 0;
line-height: 55rpx;
text-align: center;
font-size: 12px;
margin-left: 5px;
}
.prize-item {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.prize-name,
.color-picker {
flex: 1;
padding: 5px;
border: 1px solid #ccc;
margin-right: 5px;
}
注意: 以上代码只是一个示例,你需要根据实际需求进行调整和完善,例如:
添加动画效果,例如转盘加速、减速。
处理用户交互,例如点击开始按钮后禁用按钮,防止重复点击。
优化代码结构,提高代码可读性和可维护性。
另外,我们可以添加一个方法来打乱 prizeList 数组的顺序,从而增加抽奖的随机性。
在 page 对象中添加以下方法:
javascript
// ... 其他代码 ...
shufflePrizeList() {
let updatedPrizeList = [...this.data.prizeList];
for (let i = updatedPrizeList.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[updatedPrizeList[i], updatedPrizeList[j]] = [updatedPrizeList[j], updatedPrizeList[i]];
}
this.setData({
prizeList: updatedPrizeList,
});
},
// ... 其他代码 ...
方法说明:
shufflePrizeList 方法使用 Fisher-Yates shuffle 算法来随机打乱数组元素的顺序。该算法会遍历数组,并将当前元素与随机选取的另一个元素交换位置。
调用该方法:
你可以在以下几种情况下调用 shufflePrizeList 方法:
在开始旋转转盘之前调用: 这样每次点击 "开始" 按钮时,奖项的顺序都会被打乱,增加随机性。
javascript
startRotate() {
if (this.data.isRotating) return;
this.shufflePrizeList(); // 打乱奖项顺序// ... 其他代码 ...
},
在保存奖项列表之后调用: 这样每次编辑完奖项并保存后,奖项的顺序也会被打乱。
javascript
savePrizeList() {
// ... 保存奖项列表逻辑 ...
this.shufflePrizeList();
// 打乱奖项顺序
this.closeModal();
this.drawPrizeWheel();
},
在其他合适的时机调用: 根据你的需求,你也可以在其他时机调用该方法,例如在页面加载完成后调用。
选择一个合适的时机调用 shufflePrizeList 方法,就可以实现打乱奖项顺序,提高抽奖的概率性了。
你也可以进行点修改,实现指针不懂,转盘转动。
希望以上信息能够帮助你! 😊