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... Exemplo: Maria João Ana 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 • 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
