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 += `
${d}
`; } } // Add one final set of 0-9 for the final target for (let d = 0; d <= 9; d++) { html += `
${d}
`; } 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(); });