🎮ArcadeLab

Spin the Wheel

by TurboFlame46
862 lines16.6 KB
▶ Play

<!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()">
        ▶ &nbsp; 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.