git add .
git commit -m "feat: improve wheel UI with gold pointer and center circle sync"
This commit is contained in:
@@ -1,325 +1,61 @@
|
||||
/* =========================
|
||||
ADMIN PANEL
|
||||
========================= */
|
||||
|
||||
.admin-container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.admin-header h1 {
|
||||
color: #FFD700;
|
||||
font-size: 38px;
|
||||
text-shadow: 0 0 15px rgba(255,215,0,0.5);
|
||||
}
|
||||
|
||||
.admin-header p {
|
||||
color: #f5d6a0;
|
||||
margin-top: 10px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.add-section {
|
||||
background: rgba(0,0,0,0.25);
|
||||
backdrop-filter: blur(8px);
|
||||
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
|
||||
border-radius: 20px;
|
||||
|
||||
padding: 20px;
|
||||
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
|
||||
margin-bottom: 30px;
|
||||
|
||||
box-shadow:
|
||||
0 10px 30px rgba(0,0,0,0.4),
|
||||
inset 0 0 20px rgba(255,255,255,0.03);
|
||||
}
|
||||
|
||||
.add-section input {
|
||||
flex: 1;
|
||||
|
||||
padding: 14px;
|
||||
|
||||
border: none;
|
||||
|
||||
border-radius: 12px;
|
||||
|
||||
background: rgba(255,255,255,0.08);
|
||||
|
||||
color: white;
|
||||
|
||||
font-size: 16px;
|
||||
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.add-section input::placeholder {
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
#add-btn {
|
||||
padding: 14px 28px;
|
||||
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
#FFD700,
|
||||
#D4AC0D
|
||||
);
|
||||
|
||||
color: #4a0f0f;
|
||||
|
||||
border: none;
|
||||
|
||||
border-radius: 14px;
|
||||
|
||||
font-weight: bold;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
transition: 0.2s;
|
||||
|
||||
box-shadow:
|
||||
0 4px 20px rgba(255,215,0,0.3);
|
||||
}
|
||||
|
||||
#add-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
|
||||
box-shadow:
|
||||
0 8px 25px rgba(255,215,0,0.45);
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
background: rgba(0,0,0,0.25);
|
||||
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
border-radius: 22px;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
|
||||
box-shadow:
|
||||
0 10px 40px rgba(0,0,0,0.45);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
thead {
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
#FFD700,
|
||||
#D4AC0D
|
||||
);
|
||||
|
||||
color: #4a0f0f;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 18px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 18px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
border-bottom:
|
||||
1px solid rgba(255,255,255,0.06);
|
||||
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background:
|
||||
rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.edit-btn,
|
||||
.delete-btn {
|
||||
border: none;
|
||||
|
||||
padding: 10px 16px;
|
||||
|
||||
border-radius: 10px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
font-weight: bold;
|
||||
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
#9B59B6,
|
||||
#7D3C98
|
||||
);
|
||||
|
||||
color: white;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
#E74C3C,
|
||||
#922B21
|
||||
);
|
||||
|
||||
color: white;
|
||||
}
|
||||
|
||||
.edit-btn:hover,
|
||||
.delete-btn:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
.add-section {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
table {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* =========================
|
||||
WHEEL PAGE
|
||||
========================= */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #8B0000 0%, #4a0f0f 50%, #2d0000 100%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: Tahoma, sans-serif;
|
||||
}
|
||||
|
||||
.page {
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 30px;
|
||||
padding: 40px 20px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.page h1 {
|
||||
color: #FFD700;
|
||||
font-size: 36px;
|
||||
text-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
|
||||
letter-spacing: 2px;
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wheel-wrapper {
|
||||
position: relative;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
canvas {
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.wheel-wrapper canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#spin-btn {
|
||||
padding: 16px 48px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
button {
|
||||
padding: 12px 30px;
|
||||
font-size: 18px;
|
||||
font-family: Tahoma, sans-serif;
|
||||
background: linear-gradient(135deg, #FFD700, #D4AC0D);
|
||||
color: #4a0f0f;
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 6px 25px rgba(255, 215, 0, 0.4);
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
#spin-btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 30px rgba(255, 215, 0, 0.55);
|
||||
button:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
#spin-btn:disabled {
|
||||
opacity: 0.5;
|
||||
button:disabled {
|
||||
background: #aaa;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
#result {
|
||||
min-height: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.winner-text {
|
||||
font-size: 28px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #FFD700;
|
||||
text-shadow: 0 0 20px rgba(255, 215, 0, 0.7);
|
||||
animation: pop 0.4s ease;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
@keyframes pop {
|
||||
0% { transform: scale(0.5); opacity: 0; }
|
||||
70% { transform: scale(1.1); }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
/* ادمین */
|
||||
#add-section {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.wheel-wrapper {
|
||||
width: 340px;
|
||||
height: 340px;
|
||||
}
|
||||
|
||||
.page h1 {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
#spin-btn {
|
||||
font-size: 16px;
|
||||
padding: 12px 36px;
|
||||
}
|
||||
}
|
||||
input {
|
||||
padding: 10px;
|
||||
@@ -1,103 +1,33 @@
|
||||
import {
|
||||
adminGetItems,
|
||||
createItem,
|
||||
deleteItem,
|
||||
updateItem // 👈 باید اینو هم به api.js اضافه کنی
|
||||
} from './api.js';
|
||||
import { adminGetItems, createItem, deleteItem } from './api.js';
|
||||
|
||||
const tableBody = document.getElementById('items-table-body');
|
||||
const titleInput = document.getElementById('title-input');
|
||||
const valueInput = document.getElementById('value-input');
|
||||
const list = document.getElementById('items-list');
|
||||
const input = document.getElementById('new-item');
|
||||
const addBtn = document.getElementById('add-btn');
|
||||
|
||||
let editId = null; // 👈 حالت ویرایش
|
||||
|
||||
// -------------------- Render --------------------
|
||||
async function renderItems() {
|
||||
const items = await adminGetItems();
|
||||
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
items.forEach((item, index) => {
|
||||
const row = document.createElement('tr');
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${index + 1}</td>
|
||||
<td>${item.title}</td>
|
||||
<td>${item.value}</td>
|
||||
<td class="actions">
|
||||
|
||||
<button
|
||||
class="edit-btn"
|
||||
onclick="editItem(${item.id}, '${item.title}', ${item.value})"
|
||||
>
|
||||
✏️ ویرایش
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="delete-btn"
|
||||
onclick="removeItem(${item.id})"
|
||||
>
|
||||
🗑 حذف
|
||||
</button>
|
||||
|
||||
</td>
|
||||
list.innerHTML = '';
|
||||
items.forEach(item => {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `
|
||||
<span>${item.title}</span>
|
||||
<button onclick="removeItem(${item.id})">حذف</button>
|
||||
`;
|
||||
|
||||
tableBody.appendChild(row);
|
||||
list.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------- Delete --------------------
|
||||
window.removeItem = async (id) => {
|
||||
await deleteItem(id);
|
||||
renderItems();
|
||||
};
|
||||
|
||||
// -------------------- Edit --------------------
|
||||
window.editItem = (id, title, value) => {
|
||||
titleInput.value = title;
|
||||
valueInput.value = value;
|
||||
|
||||
editId = id;
|
||||
|
||||
addBtn.textContent = 'آپدیت ✏️';
|
||||
};
|
||||
|
||||
// -------------------- Add / Update --------------------
|
||||
addBtn.onclick = async () => {
|
||||
const title = titleInput.value.trim();
|
||||
const value = valueInput.value.trim();
|
||||
|
||||
if (!title || !value) {
|
||||
alert('همه فیلدها را پر کن');
|
||||
return;
|
||||
}
|
||||
|
||||
// حالت ویرایش
|
||||
if (editId) {
|
||||
await updateItem(editId, {
|
||||
title,
|
||||
value: parseInt(value)
|
||||
});
|
||||
|
||||
editId = null;
|
||||
addBtn.textContent = 'افزودن';
|
||||
}
|
||||
|
||||
// حالت افزودن
|
||||
else {
|
||||
await createItem({
|
||||
title,
|
||||
value: parseInt(value)
|
||||
});
|
||||
}
|
||||
|
||||
titleInput.value = '';
|
||||
valueInput.value = '';
|
||||
|
||||
const title = input.value.trim();
|
||||
if (!title) return;
|
||||
await createItem(title);
|
||||
input.value = '';
|
||||
renderItems();
|
||||
};
|
||||
|
||||
// -------------------- Init --------------------
|
||||
renderItems();
|
||||
@@ -1,40 +1,20 @@
|
||||
const BASE = 'http://127.0.0.1:8000';
|
||||
|
||||
export const getItems = () =>
|
||||
fetch(`${BASE}/user/items`)
|
||||
.then(r => r.json());
|
||||
fetch(`${BASE}/user/items`).then(r => r.json());
|
||||
|
||||
export const spinWheel = () =>
|
||||
fetch(`${BASE}/user/spin`, {
|
||||
method: 'POST'
|
||||
}).then(r => r.json());
|
||||
fetch(`${BASE}/user/spin`, { method: 'POST' }).then(r => r.json());
|
||||
|
||||
export const adminGetItems = () =>
|
||||
fetch(`${BASE}/admin/items`)
|
||||
.then(r => r.json());
|
||||
fetch(`${BASE}/admin/items`).then(r => r.json());
|
||||
|
||||
export const createItem = (data) =>
|
||||
export const createItem = (title) =>
|
||||
fetch(`${BASE}/admin/items`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
}).then(r => r.json());
|
||||
|
||||
export const updateItem = (id, data) =>
|
||||
fetch(`${BASE}/admin/items/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title })
|
||||
}).then(r => r.json());
|
||||
|
||||
export const deleteItem = (id) =>
|
||||
fetch(`${BASE}/admin/items/${id}`, {
|
||||
method: 'DELETE'
|
||||
}).then(r => {
|
||||
if (r.status === 204) return {};
|
||||
return r.json();
|
||||
});
|
||||
fetch(`${BASE}/admin/items/${id}`, { method: 'DELETE' }).then(r => r.json());
|
||||
@@ -4,309 +4,43 @@ const canvas = document.getElementById('wheel');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const btn = document.getElementById('spin-btn');
|
||||
const result = document.getElementById('result');
|
||||
|
||||
const DARK = { edge: '#6D0025', main: '#cb183d', highlight: '#E8005E', text: '#fff' };
|
||||
const LIGHT = { edge: '#D4889A', main: '#FAD4DC', highlight: '#FFF0F3', text: '#7B0030' };
|
||||
const colors = ['#e74c3c','#3498db','#2ecc71','#f39c12','#9b59b6','#1abc9c'];
|
||||
|
||||
let items = [];
|
||||
let angle = 0;
|
||||
let spinning = false;
|
||||
let lampOn = true;
|
||||
|
||||
async function init() {
|
||||
items = await getItems();
|
||||
draw(angle);
|
||||
setInterval(() => {
|
||||
lampOn = !lampOn;
|
||||
draw(angle);
|
||||
}, 600);
|
||||
drawWheel(angle);
|
||||
}
|
||||
|
||||
function getTheme(i, total) {
|
||||
if (total % 2 === 0) {
|
||||
return i % 2 === 0 ? DARK : LIGHT;
|
||||
} else {
|
||||
if (i === total - 1) return DARK;
|
||||
return i % 2 === 0 ? DARK : LIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
function drawSegment(cx, cy, r, start, end, theme) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx, cy);
|
||||
ctx.arc(cx, cy, r, start, end);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = theme.edge;
|
||||
ctx.fill();
|
||||
|
||||
const mid = (start + end) / 2;
|
||||
const gx = cx + r * 0.5 * Math.cos(mid);
|
||||
const gy = cy + r * 0.5 * Math.sin(mid);
|
||||
const grad = ctx.createRadialGradient(gx, gy, 0, cx, cy, r);
|
||||
grad.addColorStop(0, theme.highlight);
|
||||
grad.addColorStop(0.45, theme.main);
|
||||
grad.addColorStop(1, theme.edge);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx, cy);
|
||||
ctx.arc(cx, cy, r - 2, start, end);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fill();
|
||||
|
||||
const hGrad = ctx.createRadialGradient(gx, gy, 0, gx, gy, r * 0.35);
|
||||
hGrad.addColorStop(0, 'rgba(255,255,255,0.28)');
|
||||
hGrad.addColorStop(1, 'rgba(255,255,255,0)');
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx, cy);
|
||||
ctx.arc(cx, cy, r - 2, start, end);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = hGrad;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function draw(rotation) {
|
||||
function drawWheel(rotation) {
|
||||
if (items.length === 0) return;
|
||||
const cx = canvas.width / 2;
|
||||
const cy = canvas.height / 2;
|
||||
const r = cx - 40;
|
||||
const r = cx - 10;
|
||||
const slice = (2 * Math.PI) / items.length;
|
||||
const total = items.length;
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// ═══ سایه کلی ═══
|
||||
ctx.save();
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.6)';
|
||||
ctx.shadowBlur = 30;
|
||||
ctx.shadowOffsetX = 5;
|
||||
ctx.shadowOffsetY = 8;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r + 40, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#1A0008';
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
// ═══ حلقه بیرونی تیره ═══
|
||||
const rimGrad = ctx.createRadialGradient(cx - 28, cy - 28, r, cx, cy, r + 40);
|
||||
rimGrad.addColorStop(0, '#A0003A');
|
||||
rimGrad.addColorStop(0.3, '#6D0025');
|
||||
rimGrad.addColorStop(0.7, '#3D0015');
|
||||
rimGrad.addColorStop(1, '#1A0008');
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r + 40, 0, Math.PI * 2);
|
||||
ctx.fillStyle = rimGrad;
|
||||
ctx.fill();
|
||||
|
||||
// ═══ بخشهای گردونه ═══
|
||||
items.forEach((item, i) => {
|
||||
const start = rotation + i * slice;
|
||||
const end = start + slice;
|
||||
drawSegment(cx, cy, r, start, end, getTheme(i, total));
|
||||
});
|
||||
|
||||
// ═══ خطوط طلایی ═══
|
||||
items.forEach((_, i) => {
|
||||
const lineAngle = rotation + i * slice;
|
||||
const lGrad = ctx.createLinearGradient(
|
||||
cx, cy,
|
||||
cx + r * Math.cos(lineAngle),
|
||||
cy + r * Math.sin(lineAngle)
|
||||
);
|
||||
lGrad.addColorStop(0, 'rgba(700,550,0,5)');
|
||||
lGrad.addColorStop(0.4, '#a2922e');
|
||||
lGrad.addColorStop(1, '#c8b43b');
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx, cy);
|
||||
ctx.lineTo(cx + r * Math.cos(lineAngle), cy + r * Math.sin(lineAngle));
|
||||
ctx.strokeStyle = lGrad;
|
||||
ctx.lineWidth = 5;
|
||||
ctx.arc(cx, cy, r, start, end);
|
||||
ctx.fillStyle = colors[i % colors.length];
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
});
|
||||
|
||||
// ═══ متن آیتمها ═══
|
||||
items.forEach((item, i) => {
|
||||
const start = rotation + i * slice;
|
||||
const theme = getTheme(i, total);
|
||||
ctx.save();
|
||||
ctx.translate(cx, cy);
|
||||
ctx.rotate(start + slice / 2);
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillStyle = theme.text;
|
||||
ctx.font = 'bold 12px Tahoma';
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.6)';
|
||||
ctx.shadowBlur = 4;
|
||||
const text = item.title;
|
||||
const maxWidth = r - 52;
|
||||
if (ctx.measureText(text).width > maxWidth) {
|
||||
const mid = Math.ceil(text.length / 2);
|
||||
ctx.fillText(text.slice(0, mid), r - 20, -7);
|
||||
ctx.fillText(text.slice(mid), r - 20, 9);
|
||||
} else {
|
||||
ctx.fillText(text, r - 20, 5);
|
||||
}
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 16px Tahoma';
|
||||
ctx.fillText(item.title, r - 10, 5);
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
// ═══ حلقه طلایی داخلی ═══
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r + 2, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = '#dfd498';
|
||||
ctx.lineWidth = 4.5;
|
||||
ctx.shadowColor = 'rgba(255,215,0,0.9)';
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// ═══ لامپها ═══
|
||||
const lampCount = 24;
|
||||
const lampR = r + 23;
|
||||
for (let i = 0; i < lampCount; i++) {
|
||||
const lampAngle = (i / lampCount) * Math.PI * 2;
|
||||
const lx = cx + lampR * Math.cos(lampAngle);
|
||||
const ly = cy + lampR * Math.sin(lampAngle);
|
||||
const isOn = lampOn ? i % 2 === 0 : i % 2 !== 0;
|
||||
|
||||
if (isOn) {
|
||||
const glowGrad = ctx.createRadialGradient(lx, ly, 0, lx, ly, 14);
|
||||
glowGrad.addColorStop(0, 'rgba(255,220,80,0.55)');
|
||||
glowGrad.addColorStop(1, 'rgba(255,220,80,0)');
|
||||
ctx.beginPath();
|
||||
ctx.arc(lx, ly, 14, 0, Math.PI * 2);
|
||||
ctx.fillStyle = glowGrad;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
const lGrad = ctx.createRadialGradient(lx - 2.5, ly - 2.5, 0.5, lx, ly, 7.5);
|
||||
if (isOn) {
|
||||
lGrad.addColorStop(0, '#FFFFF5');
|
||||
lGrad.addColorStop(0.2, '#FFE566');
|
||||
lGrad.addColorStop(0.6, '#FFB300');
|
||||
lGrad.addColorStop(1, '#E65100');
|
||||
ctx.shadowColor = 'rgba(255,180,0,1)';
|
||||
ctx.shadowBlur = 14;
|
||||
} else {
|
||||
lGrad.addColorStop(0, '#B8954A');
|
||||
lGrad.addColorStop(1, '#4A3010');
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(lx, ly, 7.5, 0, Math.PI * 2);
|
||||
ctx.fillStyle = lGrad;
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(lx, ly, 7.5, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = isOn ? '#C8960C' : '#6A4A18';
|
||||
ctx.lineWidth = 1.2;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(lx - 2.5, ly - 2.5, 2.5, 0, Math.PI * 2);
|
||||
ctx.fillStyle = isOn ? 'rgba(255,255,255,0.6)' : 'rgba(255,255,255,0.2)';
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// ═══ حلقه طلایی بیرونی ═══
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r + 35, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = '#C8960C';
|
||||
ctx.lineWidth = 4.5;
|
||||
ctx.shadowColor = 'rgba(150,100,12,0.5)';
|
||||
ctx.shadowBlur = 6;
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// ═══ دایره مرکزی — هماهنگ با نشانگر ═══
|
||||
const cGrad = ctx.createRadialGradient(cx - 8, cy - 8, 1, cx, cy, 22);
|
||||
cGrad.addColorStop(0, '#FFFFF0');
|
||||
cGrad.addColorStop(0.15, '#FFE566');
|
||||
cGrad.addColorStop(0.45, '#FFD700');
|
||||
cGrad.addColorStop(0.75, '#ff962a');
|
||||
cGrad.addColorStop(1, '#b37900');
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, 22, 0, Math.PI * 2);
|
||||
ctx.fillStyle = cGrad;
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.6)';
|
||||
ctx.shadowBlur = 14;
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, 22, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = '#8B6914';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// بازتاب روی دایره مرکزی
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx - 6, cy - 6, 7, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.3)';
|
||||
ctx.fill();
|
||||
|
||||
// ═══ نشانگر ═══
|
||||
drawPointer(cx);
|
||||
}
|
||||
|
||||
function drawPointer(cx) {
|
||||
// سایه
|
||||
ctx.save();
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.4)';
|
||||
ctx.shadowBlur = 8;
|
||||
ctx.shadowOffsetX = 3;
|
||||
ctx.shadowOffsetY = 4;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, 20, 17, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.1)';
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
// دایره طلایی — همرنگ مرکز
|
||||
const pGrad = ctx.createRadialGradient(cx - 6, 14, 1, cx, 20, 18);
|
||||
pGrad.addColorStop(0, '#FFFFF0');
|
||||
pGrad.addColorStop(0.15, '#FFE566');
|
||||
pGrad.addColorStop(0.45, '#FFD700');
|
||||
pGrad.addColorStop(0.75, '#ff962a');
|
||||
pGrad.addColorStop(1, '#b37900');
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, 20, 17, 0, Math.PI * 2);
|
||||
ctx.fillStyle = pGrad;
|
||||
ctx.shadowColor = 'rgba(200,150,0,0.8)';
|
||||
ctx.shadowBlur = 12;
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.strokeStyle = '#8B6914';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// بازتاب
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx - 5, 13, 6, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.35)';
|
||||
ctx.fill();
|
||||
|
||||
// مثلث — همرنگ
|
||||
const triGrad = ctx.createLinearGradient(cx, 34, cx, 62);
|
||||
triGrad.addColorStop(0, '#FFE566');
|
||||
triGrad.addColorStop(0.35, '#FFD700');
|
||||
triGrad.addColorStop(1, '#b37900');
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx - 13, 35);
|
||||
ctx.lineTo(cx + 13, 35);
|
||||
ctx.lineTo(cx, 62);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = triGrad;
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.5)';
|
||||
ctx.shadowBlur = 8;
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.strokeStyle = '#8B6914';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
btn.onclick = async () => {
|
||||
@@ -319,30 +53,26 @@ btn.onclick = async () => {
|
||||
const winner = data.winner;
|
||||
const winnerIdx = items.findIndex(i => i.id === winner.id);
|
||||
const slice = (2 * Math.PI) / items.length;
|
||||
const targetAngle = angle + (Math.PI * 2 * 5) +
|
||||
(Math.PI * 2 - (winnerIdx * slice + slice / 2));
|
||||
|
||||
const targetAngle =
|
||||
angle +
|
||||
Math.PI * 2 * 8 +
|
||||
(-Math.PI / 2 - (winnerIdx * slice + slice / 2) - angle % (Math.PI * 2) + Math.PI * 2 * 3);
|
||||
|
||||
const duration = 5500;
|
||||
const startTime = performance.now();
|
||||
const duration = 4000;
|
||||
const start = performance.now();
|
||||
const from = angle;
|
||||
|
||||
function animate(now) {
|
||||
const elapsed = now - startTime;
|
||||
const elapsed = now - start;
|
||||
const t = Math.min(elapsed / duration, 1);
|
||||
const ease = 1 - Math.pow(1 - t, 4);
|
||||
const ease = 1 - Math.pow(1 - t, 3);
|
||||
angle = from + (targetAngle - from) * ease;
|
||||
draw(angle);
|
||||
drawWheel(angle);
|
||||
|
||||
if (t < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
spinning = false;
|
||||
btn.disabled = false;
|
||||
draw(angle);
|
||||
result.innerHTML = `<span class="winner-text">🎉 ${winner.title} 🎉</span>`;
|
||||
result.textContent = `🎉 ${winner.title}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user