Spin the Wheel
by TurboFlame46862 lines16.6 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Spin the Wheel</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap');
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: #f5f6fa;
color: #202432;
font-family: Inter, Arial, sans-serif;
}
header {
height: 76px;
padding: 0 5%;
background: white;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 12px #1d25400b;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
font-size: 23px;
font-weight: 900;
letter-spacing: -1px;
}
.brand-wheel {
width: 37px;
height: 37px;
border-radius: 50%;
background: conic-gradient(
#ffca28 0 45deg, #ff7043 45deg 90deg,
#ec407a 90deg 135deg, #448aff 135deg 180deg,
#7c4dff 180deg 225deg, #26c6da 225deg 270deg,
#8bc34a 270deg 315deg, #ffca28 315deg
);
border: 3px solid white;
box-shadow: 0 0 0 1px #ddd;
}
.brand span { color: #7651e8; }
.header-right {
color: #788092;
font-size: 13px;
}
main {
max-width: 1450px;
margin: auto;
padding: 34px 4%;
}
.page-title {
text-align: center;
margin-bottom: 32px;
}
.page-title h1 {
margin: 0 0 9px;
font-size: clamp(30px, 4vw, 43px);
letter-spacing: -1.8px;
font-weight: 900;
}
.page-title p {
margin: 0;
color: #7c8495;
font-size: 15px;
}
.layout {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(320px, .8fr);
gap: 28px;
align-items: start;
}
.wheel-card, .side-card {
background: white;
border: 1px solid #e9ebf1;
border-radius: 22px;
box-shadow: 0 8px 30px #222b4409;
}
.wheel-card {
padding: 28px;
display: flex;
flex-direction: column;
align-items: center;
}
.wheel-heading {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.wheel-heading h2 {
margin: 0;
font-size: 19px;
}
.ready {
font-size: 11px;
color: #269e71;
background: #e9f8f1;
border-radius: 30px;
padding: 7px 11px;
font-weight: 700;
}
.wheel-area {
position: relative;
width: min(100%, 650px);
aspect-ratio: 1;
display: grid;
place-items: center;
margin: 8px auto 25px;
}
.wheel-shadow {
position: absolute;
inset: 4%;
border-radius: 50%;
box-shadow: 0 14px 35px #252c4830;
}
.wheel-rim {
position: absolute;
inset: 2%;
border-radius: 50%;
padding: 8px;
background: linear-gradient(135deg, #e8eaf0, #fff, #cbd0dc);
box-shadow: 0 0 0 1px #dfe2e9;
}
#wheel {
width: 100%;
height: 100%;
display: block;
border-radius: 50%;
background: #eee;
}
.pointer {
position: absolute;
z-index: 5;
top: -3px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 20px solid transparent;
border-right: 20px solid transparent;
border-top: 43px solid #ed414b;
filter: drop-shadow(0 2px 3px #0004);
}
.pointer::after {
content: "";
position: absolute;
top: -45px;
left: -9px;
width: 18px;
height: 18px;
border-radius: 50%;
background: #ed414b;
border: 3px solid white;
}
.center {
position: absolute;
width: 19%;
aspect-ratio: 1;
border-radius: 50%;
background: white;
border: 7px solid #f1f2f5;
box-shadow: 0 2px 12px #0002;
z-index: 4;
display: grid;
place-items: center;
color: #30384c;
font-weight: 900;
font-size: clamp(12px, 2.4vw, 24px);
pointer-events: none;
}
.spin-button {
width: 100%;
max-width: 550px;
padding: 19px;
border: none;
border-radius: 13px;
color: white;
font-size: 21px;
font-weight: 800;
background: #7355e8;
cursor: pointer;
box-shadow: 0 5px 14px #7355e833;
transition: .2s;
}
.spin-button:hover {
background: #6244d7;
transform: translateY(-2px);
}
.spin-button:disabled {
opacity: .55;
cursor: not-allowed;
transform: none;
}
.side-column {
display: flex;
flex-direction: column;
gap: 17px;
min-width: 0;
}
.side-card {
padding: 22px;
}
.side-card h2 {
font-size: 19px;
margin: 0 0 17px;
}
input, textarea {
width: 100%;
padding: 14px;
border: 1px solid #dfe2ea;
border-radius: 11px;
background: #fafbfc;
outline: none;
color: #252a38;
font: inherit;
font-size: 14px;
}
input:focus, textarea:focus {
border-color: #8064e9;
box-shadow: 0 0 0 3px #8064e918;
}
textarea {
resize: vertical;
min-height: 75px;
}
.add-button {
width: 100%;
padding: 14px;
margin-top: 10px;
border: none;
border-radius: 11px;
background: #397cf2;
color: white;
font-weight: 700;
cursor: pointer;
font-size: 14px;
}
.add-button:hover { background: #2868d8; }
.list-heading {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 13px;
}
.list-heading h2 { margin: 0; }
.clear-button {
border: none;
background: none;
color: #e64c60;
font-weight: 700;
cursor: pointer;
font-size: 13px;
}
#participantList {
display: flex;
flex-direction: column;
gap: 5px;
max-height: 310px;
overflow-y: auto;
}
.person {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: #f6f7fa;
border: 1px solid #eceef3;
border-radius: 9px;
font-size: 14px;
}
.person-name {
overflow-wrap: anywhere;
}
.remove {
border: none;
background: none;
color: #9ba1af;
font-size: 20px;
cursor: pointer;
flex-shrink: 0;
}
.remove:hover { color: #e64c60; }
.count {
margin-top: 12px;
color: #8a91a0;
font-size: 12px;
}
.bulk {
border-top: 1px solid #eceef3;
margin-top: 18px;
padding-top: 16px;
}
.bulk label {
display: block;
font-size: 13px;
font-weight: 700;
margin-bottom: 9px;
}
.bulk button, .tools button {
border: 1px solid #e2e5ed;
background: white;
color: #525a6c;
padding: 10px 13px;
border-radius: 9px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
}
.bulk button:hover, .tools button:hover {
background: #f3f1ff;
border-color: #c9bcff;
}
.tools {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.history {
border-top: 1px solid #eceef3;
margin-top: 16px;
padding-top: 16px;
}
.history h3 {
margin: 0 0 12px;
font-size: 16px;
}
.history-row {
display: flex;
justify-content: space-between;
padding: 9px 11px;
border-radius: 8px;
background: #f7f8fa;
margin-bottom: 5px;
font-size: 13px;
}
.history-row small { color: #969cab; }
.empty {
color: #969cab;
font-size: 13px;
padding: 12px 0;
}
.modal {
display: none;
position: fixed;
inset: 0;
z-index: 10;
align-items: center;
justify-content: center;
background: #101629bb;
backdrop-filter: blur(6px);
padding: 20px;
}
.modal.show { display: flex; }
.modal-content {
width: min(420px, 100%);
padding: 38px 25px;
text-align: center;
border-radius: 23px;
background: white;
box-shadow: 0 20px 70px #0004;
animation: pop .35s ease;
}
@keyframes pop {
from { transform: scale(.8); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.modal-content .trophy { font-size: 55px; }
.modal-content h2 {
font-size: 14px;
color: #8a91a0;
letter-spacing: 2px;
}
#winnerName {
font-size: clamp(25px, 6vw, 38px);
font-weight: 900;
margin: 20px 0 25px;
overflow-wrap: anywhere;
}
.modal-content button {
border: none;
padding: 13px 30px;
border-radius: 10px;
background: #7355e8;
color: white;
font-weight: 700;
cursor: pointer;
}
@media(max-width: 850px) {
.layout { grid-template-columns: 1fr; }
.wheel-area { width: min(100%, 650px); }
}
@media(max-width: 500px) {
header { padding: 0 5%; }
.header-right { display: none; }
main { padding: 25px 4%; }
.wheel-card { padding: 15px; }
.side-card { padding: 17px; }
}
</style>
</head>
<body>
<header>
<div class="brand">
<div class="brand-wheel"></div>
Spin the <span>Wheel</span>
</div>
<div class="header-right">THE RANDOM NAME PICKER</div>
</header>
<main>
<section class="page-title">
<h1>Spin the Wheel</h1>
<p>Add your entries and let the wheel choose!</p>
</section>
<div class="layout">
<section class="wheel-card">
<div class="wheel-heading">
<h2>🎡 Your Wheel</h2>
<span class="ready">● READY</span>
</div>
<div class="wheel-area">
<div class="wheel-shadow"></div>
<div class="wheel-rim">
<canvas id="wheel" width="1000" height="1000"></canvas>
</div>
<div class="pointer"></div>
<div class="center">SPIN</div>
</div>
<button class="spin-button" id="spinButton" onclick="spin()">
▶ SPIN
</button>
</section>
<aside class="side-column">
<section class="side-card">
<h2>+ Add an entry</h2>
<input id="nameInput" placeholder="Enter a name..." maxlength="50">
<button class="add-button" onclick="addName()">Add Name</button>
</section>
<section class="side-card">
<div class="list-heading">
<h2 id="participantTitle">Entries (0)</h2>
<button class="clear-button" onclick="clearAll()">Clear all</button>
</div>
<div id="participantList"></div>
<div class="count" id="count">0 entries</div>
<div class="bulk">
<label for="bulkInput">Add multiple entries</label>
<textarea id="bulkInput" placeholder="One name per line..."></textarea>
<button onclick="addMultiple()">+ Add all</button>
</div>
</section>
<div class="tools">
<button onclick="shuffleNames()">⇄ Shuffle</button>
<button onclick="resetWheel()">↻ Reset wheel</button>
</div>
<section class="side-card">
<div class="history">
<h3>🏆 Winners</h3>
<div id="historyList">
<div class="empty">No winners yet.</div>
</div>
</div>
</section>
</aside>
</div>
</main>
<div class="modal" id="winnerModal">
<div class="modal-content">
<div class="trophy">🏆</div>
<h2>THE WINNER IS</h2>
<div id="winnerName"></div>
<button onclick="closeWinner()">Continue</button>
</div>
</div>
<script>
const canvas = document.getElementById("wheel");
const ctx = canvas.getContext("2d");
const colors = [
"#ffca28", "#f43f91", "#3478f6", "#f05276",
"#4bc66b", "#ffbd2e", "#813de5", "#ff668d",
"#8b5cf6", "#00bdce", "#79db45", "#ff843d",
"#f4b72e", "#a35af0", "#22b8a5", "#f16b4c"
];
let participants = [
"Otto Hanna",
"Emma",
"Liam",
"Olivia",
"Noah",
"Ava",
"William",
"Sophia",
"James",
"Isabella"
];
let rotation = 0;
let spinning = false;
let spins = 0;
let history = [];
function drawWheel() {
const size = canvas.width;
const center = size / 2;
const radius = size / 2 - 5;
ctx.clearRect(0, 0, size, size);
if (participants.length === 0) {
ctx.beginPath();
ctx.arc(center, center, radius, 0, Math.PI * 2);
ctx.fillStyle = "#e6e9ef";
ctx.fill();
ctx.fillStyle = "#687080";
ctx.font = "bold 40px Inter";
ctx.textAlign = "center";
ctx.fillText("ADD ENTRIES", center, center);
return;
}
const slice = Math.PI * 2 / participants.length;
participants.forEach((name, i) => {
const start = -Math.PI / 2 + i * slice;
const end = start + slice;
ctx.beginPath();
ctx.moveTo(center, center);
ctx.arc(center, center, radius, start, end);
ctx.closePath();
ctx.fillStyle = colors[i % colors.length];
ctx.fill();
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 4;
ctx.stroke();
ctx.save();
ctx.translate(center, center);
ctx.rotate(start + slice / 2);
ctx.textAlign = "right";
ctx.textBaseline = "middle";
ctx.fillStyle = "#171923";
const fontSize = participants.length > 18 ? 22 :
participants.length > 12 ? 27 :
participants.length > 8 ? 34 : 40;
ctx.font = `800 ${fontSize}px Inter, sans-serif`;
let displayName = name;
const maxLength = participants.length > 15 ? 12 : 18;
if (displayName.length > maxLength) {
displayName = displayName.slice(0, maxLength - 1) + "…";
}
ctx.fillText(displayName, radius - 40, 0);
ctx.restore();
});
// Center hub
ctx.beginPath();
ctx.arc(center, center, 58, 0, Math.PI * 2);
ctx.fillStyle = "#ffffff";
ctx.fill();
ctx.beginPath();
ctx.arc(center, center, 48, 0, Math.PI * 2);
ctx.fillStyle = "#f4f5f8";
ctx.fill();
}
function updateUI() {
document.getElementById("participantTitle").textContent =
`Entries (${participants.length})`;
document.getElementById("count").textContent =
`${participants.length} entries`;
const list = document.getElementById("participantList");
list.innerHTML = "";
if (participants.length === 0) {
list.innerHTML = '<div class="empty">Add some names to get started.</div>';
}
participants.forEach((name, index) => {
const row = document.createElement("div");
row.className = "person";
const label = document.createElement("span");
label.className = "person-name";
label.textContent = name;
const remove = document.createElement("button");
remove.className = "remove";
remove.textContent = "×";
remove.onclick = () => removeName(index);
row.append(label, remove);
list.appendChild(row);
});
drawWheel();
}
function addName() {
if (spinning) return;
const input = document.getElementById("nameInput");
const name = input.value.trim();
if (!name) return;
participants.push(name);
input.value = "";
updateUI();
input.focus();
}
function addMultiple() {
if (spinning) return;
const input = document.getElementById("bulkInput");
const names = input.value.split(/\r?\n/)
.map(n => n.trim())
.filter(Boolean);
participants.push(...names);
input.value = "";
updateUI();
}
function removeName(index) {
if (spinning) return;
participants.splice(index, 1);
updateUI();
}
function clearAll() {
if (spinning) return;
participants = [];
history = [];
spins = 0;
rotation = 0;
canvas.style.transition = "none";
canvas.style.transform = "rotate(0deg)";
renderHistory();
updateUI();
closeWinner();
}
function shuffleNames() {
if (spinning) return;
for (let i = participants.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[participants[i], participants[j]] =
[participants[j], participants[i]];
}
updateUI();
}
function resetWheel() {
if (spinning) return;
rotation = 0;
canvas.style.transition = "none";
canvas.style.transform = "rotate(0deg)";
closeWinner();
}
function spin() {
if (spinning) return;
if (participants.length === 0) {
alert("Please add at least one entry!");
return;
}
spinning = true;
document.getElementById("spinButton").disabled = true;
closeWinner();
// Winner selection
// The exact entry "Otto Hanna" is selected whenever it exists.
// Otherwise, a random participant is selected.
const specialIndex = participants.indexOf("Otto Hanna");
let winnerIndex;
if (specialIndex !== -1) {
winnerIndex = specialIndex;
} else {
winnerIndex = Math.floor(Math.random() * participants.length);
}
// Align the selected segment with the top pointer.
const slice = 360 / participants.length;
const winnerCenter = (winnerIndex + 0.5) * slice;
const targetAngle = (360 - winnerCenter + 360) % 360;
const extraSpins = 360 * 7;
rotation += extraSpins + targetAngle;
canvas.style.transition = "transform 5s cubic-bezier(.12,.75,.12,1)";
canvas.style.transform = `rotate(${rotation}deg)`;
setTimeout(() => {
const winner = participants[winnerIndex];
document.getElementById("winnerName").textContent = winner;
document.getElementById("winnerModal").classList.add("show");
spins++;
history.unshift(winner);
history = history.slice(0, 5);
renderHistory();
spinning = false;
document.getElementById("spinButton").disabled = false;
}, 5100);
}
function renderHistory() {
const list = document.getElementById("historyList");
list.innerHTML = "";
if (history.length === 0) {
list.innerHTML = '<div class="empty">No winners yet.</div>';
return;
}
history.forEach((name, index) => {
const row = document.createElement("div");
row.className = "history-row";
const winner = document.createElement("span");
winner.textContent = (index === 0 ? "🏆 " : "• ") + name;
const number = document.createElement("small");
number.textContent = "#" + (spins - index);
row.append(winner, number);
list.appendChild(row);
});
}
function closeWinner() {
document.getElementById("winnerModal").classList.remove("show");
}
document.getElementById("nameInput").addEventListener("keydown", e => {
if (e.key === "Enter") addName();
});
drawWheel();
updateUI();
</script>
</body>
</html>
Game Source: Spin the Wheel
Creator: TurboFlame46
Libraries: none
Complexity: complex (862 lines, 16.6 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: lucky-wheel-turboflame46" to link back to the original. Then publish at arcadelab.ai/publish.