Files
fast_api_livetse/frontend/assets/js/wheel.js
Anahita-Mahmoudi ec86cdc528 feat: implement advanced reward management and wheel customization
- Add customizable background color
- Add wheel animations and sound effects
- Add reward description field and winner modal
- Change font to Dana
- Add configurable reward probabilities
- Support reward types (Empty, Discount Code, LiveCoin)
- Replace reward title with reward type selector
- Allow wheel page title customization from admin panel
2026-06-10 14:55:07 +03:30

423 lines
12 KiB
JavaScript

import { getItems, spinWheel, getBgColor, getTitleSettings } from './api.js';
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' };
let audioCtx = null;
function getAudioCtx() {
if (!audioCtx || audioCtx.state === 'closed') {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx.state === 'suspended') audioCtx.resume();
return audioCtx;
}
let items = [];
let angle = 0;
let spinning = false;
let lampOn = true;
function applyBg() {
const color = getBgColor();
document.getElementById('main-body').style.background =
`radial-gradient(ellipse at center, ${color}DD 0%, ${color} 55%, ${color}BB 100%)`;
}
function applyTitle() {
const settings = getTitleSettings();
const h1 = document.querySelector('.page h1');
if (h1) {
h1.textContent = settings.text;
h1.style.color = settings.color;
h1.style.textShadow = `0 0 20px ${settings.color}80`;
}
}
async function init() {
applyBg();
applyTitle();
window.addEventListener('storage', (e) => {
if (e.key === 'wheelBgColor') applyBg();
if (e.key === 'wheelTitleSettings') applyTitle();
});
items = await getItems();
draw(angle);
setInterval(() => {
lampOn = !lampOn;
draw(angle);
}, 600);
}
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) {
if (items.length === 0) return;
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const r = cx - 40;
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(255,215,0,0.3)');
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.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 = 'center';
ctx.fillStyle = theme.text;
ctx.font = 'bold 18px Dana';
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) / 1.4, -9);
ctx.fillText(text.slice(mid), (r - 20) / 1.4, 9);
} else {
ctx.fillText(text, (r - 20) / 1.4, 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();
}
function playTickSound() {
try {
const actx = getAudioCtx();
const osc = actx.createOscillator();
const gain = actx.createGain();
osc.connect(gain);
gain.connect(actx.destination);
osc.frequency.value = 600;
osc.type = 'sine';
gain.gain.setValueAtTime(0.15, actx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, actx.currentTime + 0.08);
osc.start(actx.currentTime);
osc.stop(actx.currentTime + 0.08);
} catch(e) {}
}
function playWinSound() {
try {
const actx = getAudioCtx();
const notes = [523, 659, 784, 1047];
notes.forEach((freq, i) => {
const osc = actx.createOscillator();
const gain = actx.createGain();
osc.connect(gain);
gain.connect(actx.destination);
osc.frequency.value = freq;
osc.type = 'sine';
gain.gain.setValueAtTime(0.2, actx.currentTime + i * 0.15);
gain.gain.exponentialRampToValueAtTime(0.001, actx.currentTime + i * 0.15 + 0.3);
osc.start(actx.currentTime + i * 0.15);
osc.stop(actx.currentTime + i * 0.15 + 0.3);
});
} catch(e) {}
}
function showWinnerModal(winner) {
const modal = document.getElementById('winner-modal');
const iconMap = { 'پوچ': '', 'کد تخفیف': '', 'لایوکوین': '' };
document.getElementById('modal-icon').textContent = iconMap[winner.prize_type] || '🎉';
document.getElementById('modal-prize-type').textContent = winner.prize_type;
document.getElementById('modal-prize').textContent = winner.prize;
document.getElementById('modal-description').textContent = winner.description || '';
modal.classList.remove('hidden');
document.getElementById('modal-close').onclick = () => modal.classList.add('hidden');
}
btn.onclick = async () => {
if (spinning || items.length === 0) return;
spinning = true;
btn.disabled = true;
result.textContent = '';
const data = await spinWheel();
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 * 8 +
(-Math.PI / 2 - (winnerIdx * slice + slice / 2) - angle % (Math.PI * 2) + Math.PI * 2 * 3);
const duration = 5500;
const startTime = performance.now();
const from = angle;
let lastTickIdx = 0;
function animate(now) {
const elapsed = now - startTime;
const t = Math.min(elapsed / duration, 1);
const ease = 1 - Math.pow(1 - t, 4);
angle = from + (targetAngle - from) * ease;
const currentIdx = Math.floor(Math.abs(angle) / slice);
if (currentIdx !== lastTickIdx) {
lastTickIdx = currentIdx;
playTickSound();
}
draw(angle);
if (t < 1) {
requestAnimationFrame(animate);
} else {
spinning = false;
btn.disabled = false;
draw(angle);
playWinSound();
showWinnerModal(winner);
}
}
requestAnimationFrame(animate);
};
init();