Hjul 1
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: Inter, ui-rounded, "Segoe UI", Arial, sans-serif;
color: var(--ink);
background:
radial-gradient(circle at top left, rgba(36,87,255,.12), transparent 35%),
radial-gradient(circle at bottom right, rgba(0,168,112,.13), transparent 35%),
linear-gradient(145deg, var(--bg1), var(--bg2));
}
.wrap {
width: min(1200px, 96vw);
margin: 0 auto;
padding: 28px 0 44px;
}
header {
text-align: center;
margin-bottom: 22px;
}
h1 {
margin: 0;
font-size: clamp(2rem, 4vw, 3.4rem);
letter-spacing: -.04em;
}
.subtitle {
margin: 10px auto 0;
max-width: 760px;
color: var(--muted);
font-size: 1.05rem;
line-height: 1.5;
}
.toolbar {
display: flex;
gap: 12px;
flex-wrap: wrap;
justify-content: center;
margin: 20px 0 26px;
}
button {
border: 0;
border-radius: 14px;
padding: 12px 18px;
font-size: 1rem;
font-weight: 750;
cursor: pointer;
transition: transform .15s ease, box-shadow .15s ease, opacity .15s ease;
}
button:hover { transform: translateY(-1px); }
button:active { transform: translateY(1px); }
.secondary {
background: #fff;
color: var(--ink);
box-shadow: 0 8px 22px rgba(41,76,110,.10);
border: 1px solid var(--line);
}
.all-spin {
color: white;
background: linear-gradient(135deg, var(--accent), #6d4cff);
box-shadow: 0 12px 28px rgba(36,87,255,.25);
}
.wheels {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
gap: 18px;
align-items: center;
}
.wheel-card {
background: var(--card);
border: 1px solid rgba(216,228,239,.95);
border-radius: 26px;
padding: 22px;
box-shadow: var(--shadow);
backdrop-filter: blur(8px);
}
.wheel-title {
margin: 0 0 16px;
text-align: center;
font-size: 1.35rem;
}
.wheel-stage {
display: grid;
place-items: center;
min-height: 390px;
position: relative;
}
.pointer {
position: absolute;
top: 7px;
z-index: 4;
width: 0;
height: 0;
border-left: 18px solid transparent;
border-right: 18px solid transparent;
border-top: 34px solid #ff6846;
filter: drop-shadow(0 4px 3px rgba(0,0,0,.18));
}
canvas {
width: min(360px, 80vw);
height: min(360px, 80vw);
max-width: 100%;
display: block;
filter: drop-shadow(0 14px 18px rgba(24,50,74,.17));
}
.controls {
display: grid;
gap: 12px;
margin-top: 10px;
}
.spin-btn {
color: white;
background: linear-gradient(135deg, var(--accent), #4d7dff);
box-shadow: 0 10px 24px rgba(36,87,255,.22);
}
.wheel-card.right .spin-btn {
background: linear-gradient(135deg, var(--accent2), #22bf84);
box-shadow: 0 10px 24px rgba(0,168,112,.20);
}
.center-result {
display: grid;
gap: 12px;
justify-items: center;
align-content: center;
padding: 18px 4px;
}
.center-label {
margin: 0;
color: var(--muted);
font-weight: 800;
text-align: center;
}
.result-pair {
display: flex;
gap: 6px;
align-items: center;
justify-content: center;
}
.result {
width: 92px;
height: 92px;
background: #ffffff;
border: 3px solid var(--line);
border-radius: 18px;
display: grid;
place-items: center;
text-align: center;
font-size: 2.35rem;
font-weight: 900;
letter-spacing: .03em;
box-shadow: 0 12px 28px rgba(41,76,110,.13);
}
#result1 { border-color: rgba(36,87,255,.55); }
#result2 { border-color: rgba(0,168,112,.55); }
details {
margin-top: 16px;
border-top: 1px solid var(--line);
padding-top: 14px;
}
summary {
cursor: pointer;
font-weight: 800;
color: var(--ink);
}
.inputs {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
margin-top: 12px;
}
.inputs input {
width: 100%;
border: 1px solid var(--line);
border-radius: 11px;
padding: 10px 6px;
text-align: center;
text-transform: uppercase;
font-size: 1.05rem;
font-weight: 800;
color: var(--ink);
background: white;
outline: none;
}
.inputs input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(36,87,255,.12);
}
.tip {
margin: 20px auto 0;
padding: 14px 18px;
max-width: 820px;
text-align: center;
color: var(--muted);
background: rgba(255,255,255,.75);
border-radius: 16px;
border: 1px solid rgba(216,228,239,.9);
line-height: 1.45;
}
@media (max-width: 900px) {
.wheels { grid-template-columns: 1fr; }
.center-result { order: 2; padding: 4px 0; }
.wheel-card.left { order: 1; }
.wheel-card.right { order: 3; }
.wheel-stage { min-height: 340px; }
.inputs { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
Skriv op til to bogstaver i hvert felt. Drej hjulene hver for sig eller samtidig.
Læs sammen
Tip: Brug fx BR, KL, ST på det ene hjul og A, I, O, Å på det andet.
Så kan eleverne kombinere begyndelser og endelser.
const colors1 = ["#2457ff","#5f7dff","#8d66ff","#c65de8","#ff6f91","#ff8d5d","#ffc14d","#55c990"]; const colors2 = ["#00a870","#19ba8a","#55c990","#9acb52","#e3c643","#ffb04d","#ff7d63","#3f8ce8"];
class Wheel { constructor(canvasId, inputContainerId, resultId, buttonId, colors, defaults) { this.canvas = document.getElementById(canvasId); this.ctx = this.canvas.getContext("2d"); this.inputContainer = document.getElementById(inputContainerId); this.resultEl = document.getElementById(resultId); this.button = document.getElementById(buttonId); this.colors = colors; this.values = [...defaults]; this.rotation = 0; this.isSpinning = false; this.buildInputs(); this.draw(); this.button.addEventListener("click", () => this.spin()); }
buildInputs() { this.inputContainer.innerHTML = ""; this.values.forEach((value, index) => { const input = document.createElement("input"); input.value = value; input.maxLength = 2; input.setAttribute("aria-label", `Felt ${index + 1}`); input.addEventListener("input", () => { input.value = input.value.toUpperCase().slice(0, 2); this.values[index] = input.value || " "; this.draw(); }); this.inputContainer.appendChild(input); }); }
draw() { const ctx = this.ctx; const w = this.canvas.width; const h = this.canvas.height; const cx = w / 2; const cy = h / 2; const radius = Math.min(w, h) * 0.43; const slice = (Math.PI * 2) / this.values.length;
ctx.clearRect(0, 0, w, h); ctx.save(); ctx.translate(cx, cy); ctx.rotate(this.rotation);
this.values.forEach((value, i) => { const start = -Math.PI / 2 + i * slice; const end = start + slice;
ctx.beginPath(); ctx.moveTo(0, 0); ctx.arc(0, 0, radius, start, end); ctx.closePath(); ctx.fillStyle = this.colors[i % this.colors.length]; ctx.fill(); ctx.lineWidth = 8; ctx.strokeStyle = "#ffffff"; ctx.stroke();
const angle = start + slice / 2; ctx.save(); ctx.rotate(angle); ctx.translate(radius * 0.66, 0); ctx.rotate(Math.PI / 2); ctx.fillStyle = "#ffffff"; ctx.font = "900 58px Segoe UI, Arial, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.shadowColor = "rgba(0,0,0,.18)"; ctx.shadowBlur = 4; ctx.fillText(value || " ", 0, 0); ctx.restore(); });
ctx.beginPath(); ctx.arc(0, 0, radius * 0.17, 0, Math.PI * 2); ctx.fillStyle = "#ffffff"; ctx.fill(); ctx.lineWidth = 8; ctx.strokeStyle = "#d9e4ee"; ctx.stroke();
ctx.beginPath(); ctx.arc(0, 0, radius * 0.07, 0, Math.PI * 2); ctx.fillStyle = "#18324a"; ctx.fill();
ctx.restore(); }
selectedIndex() { const full = Math.PI * 2; const slice = full / this.values.length; const normalized = ((-this.rotation) % full + full) % full; return Math.floor(normalized / slice) % this.values.length; }
spin() { if (this.isSpinning) return Promise.resolve(); this.isSpinning = true; this.button.disabled = true; this.resultEl.textContent = "…";
const start = this.rotation; const full = Math.PI * 2; const slice = full / this.values.length; const targetIndex = Math.floor(Math.random() * this.values.length); const extraTurns = 5 + Math.floor(Math.random() * 3); const desiredRotation = -(targetIndex + 0.5) * slice; let forwardDistance = ((desiredRotation - start) % full + full) % full; const target = start + extraTurns * full + forwardDistance; const duration = 2600; const startTime = performance.now();
return new Promise(resolve => { const animate = now => { const t = Math.min(1, (now - startTime) / duration); const eased = 1 - Math.pow(1 - t, 4); this.rotation = start + (target - start) * eased; this.draw();
if (t { await Promise.all([wheel1.spin(), wheel2.spin()]); });
document.getElementById("resetAll").addEventListener("click", () => { wheel1.reset(defaults1); wheel2.reset(defaults2); });