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
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { getItems, spinWheel } from './api.js';
|
||||
import { getItems, spinWheel, getBgColor, getTitleSettings } from './api.js';
|
||||
|
||||
const canvas = document.getElementById('wheel');
|
||||
const ctx = canvas.getContext('2d');
|
||||
@@ -8,12 +8,43 @@ 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(() => {
|
||||
@@ -75,7 +106,6 @@ function draw(rotation) {
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// ═══ سایه کلی ═══
|
||||
ctx.save();
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.6)';
|
||||
ctx.shadowBlur = 30;
|
||||
@@ -87,7 +117,6 @@ function draw(rotation) {
|
||||
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');
|
||||
@@ -98,14 +127,12 @@ function draw(rotation) {
|
||||
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(
|
||||
@@ -113,7 +140,7 @@ function draw(rotation) {
|
||||
cx + r * Math.cos(lineAngle),
|
||||
cy + r * Math.sin(lineAngle)
|
||||
);
|
||||
lGrad.addColorStop(0, 'rgba(700,550,0,5)');
|
||||
lGrad.addColorStop(0, 'rgba(255,215,0,0.3)');
|
||||
lGrad.addColorStop(0.4, '#a2922e');
|
||||
lGrad.addColorStop(1, '#c8b43b');
|
||||
ctx.beginPath();
|
||||
@@ -124,31 +151,29 @@ function draw(rotation) {
|
||||
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.textAlign = 'center';
|
||||
ctx.fillStyle = theme.text;
|
||||
ctx.font = 'bold 12px Tahoma';
|
||||
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, -7);
|
||||
ctx.fillText(text.slice(mid), r - 20, 9);
|
||||
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, 5);
|
||||
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';
|
||||
@@ -158,7 +183,6 @@ function draw(rotation) {
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// ═══ لامپها ═══
|
||||
const lampCount = 24;
|
||||
const lampR = r + 23;
|
||||
for (let i = 0; i < lampCount; i++) {
|
||||
@@ -208,7 +232,6 @@ function draw(rotation) {
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// ═══ حلقه طلایی بیرونی ═══
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r + 35, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = '#C8960C';
|
||||
@@ -218,7 +241,6 @@ function draw(rotation) {
|
||||
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');
|
||||
@@ -240,18 +262,15 @@ function draw(rotation) {
|
||||
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;
|
||||
@@ -263,7 +282,6 @@ function drawPointer(cx) {
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
// دایره طلایی — همرنگ مرکز
|
||||
const pGrad = ctx.createRadialGradient(cx - 6, 14, 1, cx, 20, 18);
|
||||
pGrad.addColorStop(0, '#FFFFF0');
|
||||
pGrad.addColorStop(0.15, '#FFE566');
|
||||
@@ -282,13 +300,11 @@ function drawPointer(cx) {
|
||||
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');
|
||||
@@ -309,6 +325,52 @@ function drawPointer(cx) {
|
||||
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;
|
||||
@@ -328,12 +390,20 @@ btn.onclick = async () => {
|
||||
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) {
|
||||
@@ -342,7 +412,8 @@ btn.onclick = async () => {
|
||||
spinning = false;
|
||||
btn.disabled = false;
|
||||
draw(angle);
|
||||
result.innerHTML = `<span class="winner-text">🎉 ${winner.title} 🎉</span>`;
|
||||
playWinSound();
|
||||
showWinnerModal(winner);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user