232 lines
7.9 KiB
JavaScript
232 lines
7.9 KiB
JavaScript
|
|
|
||
|
|
document.addEventListener('DOMContentLoaded', () => {
|
||
|
|
// DOM Elements
|
||
|
|
const drawBtn = document.getElementById('draw-btn');
|
||
|
|
const resetBtn = document.getElementById('reset-btn');
|
||
|
|
const statusMsg = document.getElementById('status-message');
|
||
|
|
const historyList = document.getElementById('history-list');
|
||
|
|
const slots = [
|
||
|
|
document.getElementById('slot-1'),
|
||
|
|
document.getElementById('slot-2'),
|
||
|
|
document.getElementById('slot-3')
|
||
|
|
];
|
||
|
|
|
||
|
|
// Configuration
|
||
|
|
const SLOT_HEIGHT = 160; // px, must match CSS
|
||
|
|
const NUM_DUPLICATES = 3; // How many set of 0-9 to stack for spinning illusion
|
||
|
|
|
||
|
|
// State
|
||
|
|
let config = { maxNumber: 100 };
|
||
|
|
let appState = {
|
||
|
|
remainingNumbers: [],
|
||
|
|
drawHistory: []
|
||
|
|
};
|
||
|
|
|
||
|
|
// Initialize
|
||
|
|
async function init() {
|
||
|
|
try {
|
||
|
|
const response = await fetch('config.json');
|
||
|
|
const data = await response.json();
|
||
|
|
config = { ...config, ...data };
|
||
|
|
} catch (e) {
|
||
|
|
console.warn('Could not load config.json, using defaults.');
|
||
|
|
}
|
||
|
|
|
||
|
|
initializeSlotStrips();
|
||
|
|
loadState();
|
||
|
|
|
||
|
|
// If no state exists (first run), initialize numbers
|
||
|
|
if (appState.remainingNumbers.length === 0 && appState.drawHistory.length === 0) {
|
||
|
|
resetPool();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sync input
|
||
|
|
document.getElementById('max-number-input').value = config.maxNumber;
|
||
|
|
|
||
|
|
renderHistory();
|
||
|
|
updateUI();
|
||
|
|
|
||
|
|
drawBtn.addEventListener('click', handleDraw);
|
||
|
|
resetBtn.addEventListener('click', confirmReset);
|
||
|
|
}
|
||
|
|
|
||
|
|
function initializeSlotStrips() {
|
||
|
|
slots.forEach(slot => {
|
||
|
|
const strip = slot.querySelector('.digit-strip');
|
||
|
|
strip.innerHTML = '';
|
||
|
|
// Create a long strip: 0-9 repeated NUM_DUPLICATES times + final set for landing
|
||
|
|
let html = '';
|
||
|
|
for (let i = 0; i < NUM_DUPLICATES; i++) {
|
||
|
|
for (let d = 0; d <= 9; d++) {
|
||
|
|
html += `<div class="digit-box">${d}</div>`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Add one final set of 0-9 for the final target
|
||
|
|
for (let d = 0; d <= 9; d++) {
|
||
|
|
html += `<div class="digit-box">${d}</div>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
strip.innerHTML = html;
|
||
|
|
// Set initial position to '0'
|
||
|
|
strip.style.transform = `translateY(0px)`;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Core Logic
|
||
|
|
function resetPool() {
|
||
|
|
appState.remainingNumbers = Array.from({ length: config.maxNumber }, (_, i) => i + 1);
|
||
|
|
appState.drawHistory = [];
|
||
|
|
saveState();
|
||
|
|
renderHistory();
|
||
|
|
updateUI();
|
||
|
|
statusMsg.textContent = 'Ready to draw!';
|
||
|
|
|
||
|
|
// Reset slots visually to 000
|
||
|
|
slots.forEach(slot => {
|
||
|
|
const strip = slot.querySelector('.digit-strip');
|
||
|
|
strip.style.transition = 'none';
|
||
|
|
strip.style.transform = `translateY(0px)`;
|
||
|
|
strip.classList.remove('spinning');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async function handleDraw() {
|
||
|
|
if (appState.remainingNumbers.length === 0) {
|
||
|
|
statusMsg.textContent = 'No numbers remaining!';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
drawBtn.disabled = true;
|
||
|
|
statusMsg.textContent = 'Spinning...';
|
||
|
|
|
||
|
|
// 1. Pick winner
|
||
|
|
const randomIndex = Math.floor(Math.random() * appState.remainingNumbers.length);
|
||
|
|
const winner = appState.remainingNumbers[randomIndex];
|
||
|
|
|
||
|
|
// 2. Remove from pool
|
||
|
|
appState.remainingNumbers.splice(randomIndex, 1);
|
||
|
|
|
||
|
|
// 3. Add to history
|
||
|
|
appState.drawHistory.unshift(winner);
|
||
|
|
saveState(); // Save immediately in case of refresh
|
||
|
|
|
||
|
|
// 4. Animate
|
||
|
|
await animateSlots(winner);
|
||
|
|
|
||
|
|
// 5. Finalize
|
||
|
|
renderHistory();
|
||
|
|
updateUI();
|
||
|
|
drawBtn.disabled = false;
|
||
|
|
statusMsg.textContent = `Winner: ${winner}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function animateSlots(winnerNumber) {
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
const strNum = winnerNumber.toString().padStart(3, '0');
|
||
|
|
const targetDigits = strNum.split('').map(Number);
|
||
|
|
|
||
|
|
slots.forEach((slot, index) => {
|
||
|
|
const strip = slot.querySelector('.digit-strip');
|
||
|
|
const targetDigit = targetDigits[index];
|
||
|
|
|
||
|
|
// Remove transition to reset position instantly if needed
|
||
|
|
// For a simple effect, we just assume we start from 0 or current position?
|
||
|
|
// To simplify: we reset to 0 (top) without transition, then spin down to target
|
||
|
|
strip.style.transition = 'none';
|
||
|
|
strip.style.transform = 'translateY(0px)';
|
||
|
|
|
||
|
|
// Force reflow
|
||
|
|
strip.offsetHeight;
|
||
|
|
|
||
|
|
// Calculate target Y
|
||
|
|
// We want to land on the LAST occurrence of this digit in our strip to mimic a long spin
|
||
|
|
// The strip has (NUM_DUPLICATES * 10) items before the final set.
|
||
|
|
// The final set starts at index (NUM_DUPLICATES * 10).
|
||
|
|
// Target index = (NUM_DUPLICATES * 10) + targetDigit
|
||
|
|
const targetIndex = (NUM_DUPLICATES * 10) + targetDigit;
|
||
|
|
const translateY = -(targetIndex * SLOT_HEIGHT);
|
||
|
|
|
||
|
|
// Add staggered delay for each slot (e.g., 0ms, 500ms, 1000ms)
|
||
|
|
const delay = index * 500;
|
||
|
|
|
||
|
|
setTimeout(() => {
|
||
|
|
strip.style.transition = `transform ${2 + (index * 0.5)}s cubic-bezier(0.1, 0.7, 0.1, 1)`;
|
||
|
|
strip.style.transform = `translateY(${translateY}px)`;
|
||
|
|
}, delay);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Resolve after the last slot finishes (+ buffer)
|
||
|
|
const totalDuration = (2 * 1000) + (2 * 500) + 1000; // rough estimate
|
||
|
|
setTimeout(resolve, totalDuration);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderHistory() {
|
||
|
|
historyList.innerHTML = '';
|
||
|
|
appState.drawHistory.forEach((num) => {
|
||
|
|
const li = document.createElement('li');
|
||
|
|
li.textContent = num.toString().padStart(3, '0');
|
||
|
|
historyList.appendChild(li);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateUI() {
|
||
|
|
// Toggle disable state based on pool
|
||
|
|
if (appState.remainingNumbers.length === 0) {
|
||
|
|
drawBtn.disabled = true;
|
||
|
|
statusMsg.textContent = 'All numbers drawn!';
|
||
|
|
} else {
|
||
|
|
drawBtn.disabled = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Persistence
|
||
|
|
function saveState() {
|
||
|
|
const stateToSave = {
|
||
|
|
...appState,
|
||
|
|
config: config // Save config too so maxNumber persists
|
||
|
|
};
|
||
|
|
localStorage.setItem('luckyDrawState', JSON.stringify(stateToSave));
|
||
|
|
}
|
||
|
|
|
||
|
|
function loadState() {
|
||
|
|
const saved = localStorage.getItem('luckyDrawState');
|
||
|
|
if (saved) {
|
||
|
|
try {
|
||
|
|
const parsed = JSON.parse(saved);
|
||
|
|
if (parsed.config) {
|
||
|
|
config = parsed.config;
|
||
|
|
}
|
||
|
|
appState = {
|
||
|
|
remainingNumbers: parsed.remainingNumbers || [],
|
||
|
|
drawHistory: parsed.drawHistory || []
|
||
|
|
};
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Failed to parse saved state');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function confirmReset() {
|
||
|
|
const newMax = parseInt(document.getElementById('max-number-input').value, 10);
|
||
|
|
if (newMax && newMax !== config.maxNumber) {
|
||
|
|
if (confirm(`Change max number to ${newMax} and reset pool?`)) {
|
||
|
|
config.maxNumber = newMax;
|
||
|
|
resetPool();
|
||
|
|
} else {
|
||
|
|
// Revert input interaction if cancelled (optional, but good UX)
|
||
|
|
document.getElementById('max-number-input').value = config.maxNumber;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
if (confirm('Are you sure you want to reset the pool and history?')) {
|
||
|
|
resetPool();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add input listener to warn/reset on change
|
||
|
|
document.getElementById('max-number-input').addEventListener('change', confirmReset);
|
||
|
|
|
||
|
|
init();
|
||
|
|
});
|