Roleta de Decisões Personalizada

<!DOCTYPE html> <html lang="pt"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Roleta de Decisões Personalizada</title> <!-- Tailwind CSS para design moderno e responsivo --> <script src="https://cdn.tailwindcss.com"></script> <!-- Canvas Confetti para celebração do vencedor --> <script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script> <!-- Lucide Icons --> <script src="https://unpkg.com/lucide@latest"></script> <style> @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap'); body { font-family: 'Plus Jakarta Sans', sans-serif; background-color: #0f172a; color: #f8fafc; } /* Indicador / Seta da Roleta */ .wheel-pointer { width: 0; height: 0; border-left: 18px solid transparent; border-right: 18px solid transparent; border-top: 32px solid #ef4444; filter: drop-shadow(0px 4px 6px rgba(0,0,0,0.4)); } /* Animação suave para botões */ .btn-hover { transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); } .btn-hover:hover { transform: translateY(-2px); } .btn-hover:active { transform: translateY(1px); } /* Personalização do Scrollbar */ ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: #1e293b; border-radius: 4px; } ::-webkit-scrollbar-thumb { background: #475569; border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: #64748b; } </style> </head> <body class="min-h-screen flex flex-col justify-between bg-slate-900 text-slate-100 antialiased"> <!-- Cabeçalho --> <header class="border-b border-slate-800 bg-slate-950/50 backdrop-blur-md sticky top-0 z-20"> <div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between"> <div class="flex items-center gap-3"> <div class="p-2 bg-indigo-600 rounded-xl shadow-lg shadow-indigo-500/30"> <i data-lucide="disc" class="w-6 h-6 text-white animate-spin-slow"></i> </div> <h1 class="text-xl md:text-2xl font-extrabold bg-gradient-to-r from-indigo-400 via-purple-300 to-pink-400 bg-clip-text text-transparent"> Roda da Sorte & Decisões </h1> </div> <div class="flex items-center gap-2"> <button id="soundToggle" class="p-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 transition-colors" title="Ativar/Desativar Som"> <i data-lucide="volume-2" id="soundIcon" class="w-5 h-5"></i> </button> </div> </div> </header> <!-- Conteúdo Principal --> <main class="max-w-7xl mx-auto px-4 py-6 w-full flex-1 grid grid-cols-1 lg:grid-cols-12 gap-8 items-start"> <!-- Coluna da Roleta (Esquerda/Topo em mobile) --> <section class="lg:col-span-7 flex flex-col items-center justify-center bg-slate-950/40 border border-slate-800/80 rounded-3xl p-6 shadow-2xl relative"> <!-- Indicador superior da roleta --> <div class="relative flex flex-col items-center z-10 -mb-3"> <div class="wheel-pointer"></div> </div> <!-- Canvas da Roleta --> <div class="relative w-full max-w-[420px] aspect-square flex items-center justify-center my-2"> <canvas id="wheelCanvas" width="800" height="800" class="w-full h-full rounded-full shadow-2xl shadow-indigo-500/10"></canvas> <!-- Botão Central da Roleta --> <button id="centerSpinBtn" onclick="spinWheel()" class="absolute w-20 h-20 md:w-24 md:h-24 bg-gradient-to-br from-indigo-500 to-purple-600 hover:from-indigo-400 hover:to-purple-500 text-white font-extrabold text-lg md:text-xl rounded-full border-4 border-slate-900 shadow-xl btn-hover flex flex-col items-center justify-center gap-0.5 active:scale-95 transition-transform z-10"> <span>RODAR</span> <i data-lucide="play" class="w-4 h-4 fill-current"></i> </button> </div> <!-- Botão Principal de Ação abaixo da Roleta --> <div class="mt-6 w-full max-w-sm flex gap-3"> <button id="spinBtn" onclick="spinWheel()" class="flex-1 py-3.5 px-6 bg-gradient-to-r from-indigo-600 via-purple-600 to-pink-600 hover:from-indigo-500 hover:to-pink-500 text-white font-bold rounded-2xl shadow-lg shadow-indigo-600/30 btn-hover flex items-center justify-center gap-2 text-lg"> <i data-lucide="sparkles" class="w-5 h-5"></i> Girar a Roda </button> <button onclick="shuffleNames()" class="p-3.5 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-2xl btn-hover" title="Baralhar Nomes"> <i data-lucide="shuffle" class="w-5 h-5"></i> </button> </div> </section> <!-- Coluna de Opções e Personalização (Direita) --> <section class="lg:col-span-5 flex flex-col gap-6"> <!-- Painel de Nomes --> <div class="bg-slate-950/40 border border-slate-800/80 rounded-3xl p-6 shadow-xl flex flex-col gap-4"> <div class="flex items-center justify-between"> <h2 class="text-lg font-bold flex items-center gap-2 text-slate-200"> <i data-lucide="list" class="w-5 h-5 text-indigo-400"></i> Lista de Nomes / Opções </h2> <span id="itemCount" class="text-xs font-semibold px-2.5 py-1 bg-indigo-950 text-indigo-300 border border-indigo-800/50 rounded-full"> 0 opções </span> </div> <!-- Textarea de entrada de nomes --> <div class="relative"> <textarea id="namesInput" rows="7" placeholder="Escreva um nome por linha...&#10;Exemplo:&#10;Maria&#10;João&#10;Ana&#10;Carlos" class="w-full bg-slate-900 border border-slate-700/70 rounded-2xl p-4 text-slate-200 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent resize-none text-sm leading-relaxed transition-all"></textarea> </div> <!-- Controlos Rápidos --> <div class="flex flex-wrap gap-2 pt-1"> <button onclick="clearNames()" class="px-3 py-2 text-xs font-semibold bg-slate-800 hover:bg-red-950/50 hover:text-red-400 text-slate-400 border border-slate-700/50 hover:border-red-800/50 rounded-xl transition-all flex items-center gap-1.5"> <i data-lucide="trash-2" class="w-3.5 h-3.5"></i> Limpar TUDO </button> <button onclick="removeDuplicates()" class="px-3 py-2 text-xs font-semibold bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700/50 rounded-xl transition-all flex items-center gap-1.5"> <i data-lucide="filter" class="w-3.5 h-3.5"></i> Remover Duplicados </button> </div> <!-- Opção para remover o vencedor automaticamente --> <label class="flex items-center gap-3 pt-2 text-sm text-slate-300 cursor-pointer select-none"> <input type="checkbox" id="autoRemoveWinner" class="w-4 h-4 rounded bg-slate-900 border-slate-700 text-indigo-600 focus:ring-indigo-500 focus:ring-offset-slate-900"> <span>Remover nome sorteado automaticamente</span> </label> </div> <!-- Modelos Pré-definidos (Presets) --> <div class="bg-slate-950/40 border border-slate-800/80 rounded-3xl p-6 shadow-xl flex flex-col gap-3"> <h3 class="text-xs font-bold uppercase tracking-wider text-slate-400">Exemplos Rápidos</h3> <div class="flex flex-wrap gap-2"> <button onclick="loadPreset('pagaConta')" class="px-3 py-2 text-xs bg-slate-800/80 hover:bg-indigo-600/20 hover:border-indigo-500/50 text-slate-300 border border-slate-700/60 rounded-xl transition-all"> 💳 Quem paga a conta? </button> <button onclick="loadPreset('oQueComer')" class="px-3 py-2 text-xs bg-slate-800/80 hover:bg-indigo-600/20 hover:border-indigo-500/50 text-slate-300 border border-slate-700/60 rounded-xl transition-all"> 🍕 O que comer hoje? </button> <button onclick="loadPreset('simNao')" class="px-3 py-2 text-xs bg-slate-800/80 hover:bg-indigo-600/20 hover:border-indigo-500/50 text-slate-300 border border-slate-700/60 rounded-xl transition-all"> 🎲 Sim ou Não? </button> <button onclick="loadPreset('numeros')" class="px-3 py-2 text-xs bg-slate-800/80 hover:bg-indigo-600/20 hover:border-indigo-500/50 text-slate-300 border border-slate-700/60 rounded-xl transition-all"> 🔢 Números (1 a 10) </button> </div> </div> <!-- Histórico de Vencedores --> <div class="bg-slate-950/40 border border-slate-800/80 rounded-3xl p-6 shadow-xl flex flex-col gap-3"> <div class="flex items-center justify-between"> <h3 class="text-sm font-bold flex items-center gap-2 text-slate-300"> <i data-lucide="history" class="w-4 h-4 text-indigo-400"></i> Histórico de Sorteados </h3> <button onclick="clearHistory()" class="text-xs text-slate-500 hover:text-slate-300 transition-colors">Limpar</button> </div> <div id="historyList" class="flex flex-wrap gap-1.5 max-h-28 overflow-y-auto pr-1"> <span class="text-xs text-slate-500 italic">Nenhum sorteio realizado ainda.</span> </div> </div> </section> </main> <!-- Rodapé --> <footer class="border-t border-slate-800/60 py-4 text-center text-xs text-slate-500"> Roleta de Decisões Personalizada &bull; Utilize a barra de espaço para girar rapidamente </footer> <!-- Modal do Resultado / Vencedor --> <div id="winnerModal" class="fixed inset-0 bg-slate-950/80 backdrop-blur-md flex items-center justify-center p-4 z-50 opacity-0 pointer-events-none transition-opacity duration-300"> <div class="bg-slate-900 border border-indigo-500/30 rounded-3xl p-8 max-w-md w-full text-center shadow-2xl transform scale-90 transition-transform duration-300 relative overflow-hidden" id="modalCard"> <!-- Efeito de brilho de fundo --> <div class="absolute -top-24 -left-24 w-48 h-48 bg-indigo-500/20 rounded-full blur-3xl"></div> <div class="absolute -bottom-24 -right-24 w-48 h-48 bg-pink-500/20 rounded-full blur-3xl"></div> <div class="relative z-10 flex flex-col items-center gap-4"> <div class="w-16 h-16 bg-gradient-to-tr from-amber-400 to-amber-200 rounded-2xl flex items-center justify-center text-slate-950 shadow-lg shadow-amber-500/20 animate-bounce"> <i data-lucide="trophy" class="w-8 h-8"></i> </div> <div class="space-y-1"> <span class="text-xs font-extrabold tracking-widest text-indigo-400 uppercase">O Sorteado é:</span> <h2 id="winnerName" class="text-3xl md:text-4xl font-extrabold text-white break-words"> --- </h2> </div> <div class="flex gap-3 w-full mt-4"> <button id="modalRemoveBtn" onclick="removeWinnerAndClose()" class="flex-1 py-3 px-4 bg-slate-800 hover:bg-red-900/40 hover:text-red-300 border border-slate-700 hover:border-red-800 text-slate-300 font-semibold text-sm rounded-xl transition-all flex items-center justify-center gap-2"> <i data-lucide="user-minus" class="w-4 h-4"></i> Remover Nome </button> <button onclick="closeModal()" class="flex-1 py-3 px-4 bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-sm rounded-xl shadow-lg shadow-indigo-600/30 transition-all"> Continuar </button> </div> </div> </div> </div> <!-- Lógica JavaScript --> <script> // Inicialização dos Ícones Lucide lucide.createIcons(); // Elementos do DOM const canvas = document.getElementById('wheelCanvas'); const ctx = canvas.getContext('2d'); const namesInput = document.getElementById('namesInput'); const itemCount = document.getElementById('itemCount'); const spinBtn = document.getElementById('spinBtn'); const centerSpinBtn = document.getElementById('centerSpinBtn'); const winnerModal = document.getElementById('winnerModal'); const modalCard = document.getElementById('modalCard'); const winnerNameText = document.getElementById('winnerName'); const historyList = document.getElementById('historyList'); const soundToggleBtn = document.getElementById('soundToggle'); const soundIcon = document.getElementById('soundIcon'); const autoRemoveWinnerCheckbox = document.getElementById('autoRemoveWinner'); // Paleta de Cores Vibrantes para os setores da Roleta const colors = [ '#6366f1', '#ec4899', '#8b5cf6', '#10b981', '#f59e0b', '#06b6d4', '#ef4444', '#3b82f6', '#14b8a6', '#a855f7', '#f97316', '#84cc16' ]; // Estado da Roleta let items = []; let currentAngle = 0; // Ângulo atual em radianos let isSpinning = false; let lastWinner = null; let winnersHistory = []; let soundEnabled = true; // Contexto de Áudio Web para efeitos sonoros let audioCtx = null; function getAudioContext() { if (!audioCtx) { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } if (audioCtx.state === 'suspended') { audioCtx.resume(); } return audioCtx; } // Tocar som de "click" ao passar pelas divisões function playTickSound() { if (!soundEnabled) return; try { const ctx = getAudioContext(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'triangle'; osc.frequency.setValueAtTime(600, ctx.currentTime); osc.frequency.exponentialRampToValueAtTime(120, ctx.currentTime + 0.04); gain.gain.setValueAtTime(0.15, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.04); osc.connect(gain); gain.connect(ctx.destination); osc.start(); osc.stop(ctx.currentTime + 0.04); } catch (e) { console.error(e); } } // Tocar som de Fanfarra ao vencer function playVictorySound() { if (!soundEnabled) return; try { const ctx = getAudioContext(); const now = ctx.currentTime; const notes = [261.63, 329.63, 392.00, 523.25]; // C4, E4, G4, C5 notes.forEach((freq, index) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.value = freq; const startTime = now + (index * 0.1); const duration = index === notes.length - 1 ? 0.6 : 0.12; gain.gain.setValueAtTime(0.2, startTime); gain.gain.exponentialRampToValueAtTime(0.001, startTime + duration); osc.connect(gain); gain.connect(ctx.destination); osc.start(startTime); osc.stop(startTime + duration); }); } catch (e) { console.error(e); } } // Toggle do Som soundToggleBtn.addEventListener('click', () => { soundEnabled = !soundEnabled; soundIcon.setAttribute('data-lucide', soundEnabled ? 'volume-2' : 'volume-x'); soundToggleBtn.classList.toggle('text-red-400', !soundEnabled); lucide.createIcons(); }); // Valores Iniciais const defaultNames = [ "Ana Silva", "Carlos Bruno", "Beatriz", "Daniel Santos", "Eduarda", "Fernando", "Gabriela" ]; // Atualizar lista de nomes através do Textarea function updateItemsFromInput() { const rawText = namesInput.value; items = rawText.split('\n') .map(item => item.trim()) .filter(item => item.length > 0); itemCount.textContent = `${items.length} ${items.length === 1 ? 'opção' : 'opções'}`; drawWheel(); } namesInput.addEventListener('input', updateItemsFromInput); // Desenhar a Roleta no Canvas function drawWheel() { const width = canvas.width; const height = canvas.height; const centerX = width / 2; const centerY = height / 2; const radius = width / 2 - 20; ctx.clearRect(0, 0, width, height); if (items.length === 0) { // Desenhar roleta vazia ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.fillStyle = '#1e293b'; ctx.fill(); ctx.lineWidth = 8; ctx.strokeStyle = '#334155'; ctx.stroke(); ctx.fillStyle = '#64748b'; ctx.font = 'bold 30px "Plus Jakarta Sans"'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('Adicione nomes para começar', centerX, centerY); return; } const sliceAngle = (Math.PI * 2) / items.length; items.forEach((item, index) => { const startAngle = currentAngle + index * sliceAngle; const endAngle = startAngle + sliceAngle; // Setor/Fatia ctx.beginPath(); ctx.moveTo(centerX, centerY); ctx.arc(centerX, centerY, radius, startAngle, endAngle); ctx.closePath(); ctx.fillStyle = colors[index % colors.length]; ctx.fill(); ctx.lineWidth = 4; ctx.strokeStyle = '#0f172a'; ctx.stroke(); // Texto do Nome na Fatia ctx.save(); ctx.translate(centerX, centerY); ctx.rotate(startAngle + sliceAngle / 2); ctx.textAlign = 'right'; ctx.textBaseline = 'middle';

Public Last updated: 2026-08-05 12:23:06 AM