Created
February 22, 2025 22:24
-
-
Save badu-ser/9889ecd5f49ba2221e7c01963aa9ead9 to your computer and use it in GitHub Desktop.
Spinning Wheel
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class="deal-wheel"> | |
<ul class="spinner"></ul> | |
<figure class="cap"> | |
<!-- Grim reaper SVG import --> | |
[[[https://codepen.io/hexagoncircle/pen/vYxKLOa]]] | |
</figure> | |
<div class="ticker"></div> | |
<button class="btn-spin">Spin the wheel</button> | |
</div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Prize data will space out evenly on the deal wheel based on the amount of items available. | |
* @param text [string] name of the prize | |
* @param color [string] background color of the prize | |
* @param reaction ['resting' | 'dancing' | 'laughing' | 'shocked'] Sets the reaper's animated reaction | |
*/ | |
const prizes = [ | |
{ | |
text: "10% Off Sticker Price", | |
color: "hsl(197 30% 43%)", | |
reaction: "dancing" | |
}, | |
{ | |
text: "Free Car", | |
color: "hsl(173 58% 39%)", | |
reaction: "shocked" | |
}, | |
{ | |
text: "No Money Down", | |
color: "hsl(43 74% 66%)", | |
reaction: "shocked" | |
}, | |
{ | |
text: "Half Off Sticker Price", | |
color: "hsl(27 87% 67%)", | |
reaction: "shocked" | |
}, | |
{ | |
text: "Free DIY Carwash", | |
color: "hsl(12 76% 61%)", | |
reaction: "dancing" | |
}, | |
{ | |
text: "Eternal Damnation", | |
color: "hsl(350 60% 52%)", | |
reaction: "laughing" | |
}, | |
{ | |
text: "Used Travel Mug", | |
color: "hsl(91 43% 54%)", | |
reaction: "laughing" | |
}, | |
{ | |
text: "One Solid Hug", | |
color: "hsl(140 36% 74%)", | |
reaction: "dancing" | |
} | |
]; | |
const wheel = document.querySelector(".deal-wheel"); | |
const spinner = wheel.querySelector(".spinner"); | |
const trigger = wheel.querySelector(".btn-spin"); | |
const ticker = wheel.querySelector(".ticker"); | |
const reaper = wheel.querySelector(".grim-reaper"); | |
const prizeSlice = 360 / prizes.length; | |
const prizeOffset = Math.floor(180 / prizes.length); | |
const spinClass = "is-spinning"; | |
const selectedClass = "selected"; | |
const spinnerStyles = window.getComputedStyle(spinner); | |
let tickerAnim; | |
let rotation = 0; | |
let currentSlice = 0; | |
let prizeNodes; | |
const createPrizeNodes = () => { | |
prizes.forEach(({ text, color, reaction }, i) => { | |
const rotation = ((prizeSlice * i) * -1) - prizeOffset; | |
spinner.insertAdjacentHTML( | |
"beforeend", | |
`<li class="prize" data-reaction=${reaction} style="--rotate: ${rotation}deg"> | |
<span class="text">${text}</span> | |
</li>` | |
); | |
}); | |
}; | |
const createConicGradient = () => { | |
spinner.setAttribute( | |
"style", | |
`background: conic-gradient( | |
from -90deg, | |
${prizes | |
.map(({ color }, i) => `${color} 0 ${(100 / prizes.length) * (prizes.length - i)}%`) | |
.reverse() | |
} | |
);` | |
); | |
}; | |
const setupWheel = () => { | |
createConicGradient(); | |
createPrizeNodes(); | |
prizeNodes = wheel.querySelectorAll(".prize"); | |
}; | |
const spinertia = (min, max) => { | |
min = Math.ceil(min); | |
max = Math.floor(max); | |
return Math.floor(Math.random() * (max - min + 1)) + min; | |
}; | |
const runTickerAnimation = () => { | |
// https://css-tricks.com/get-value-of-css-rotation-through-javascript/ | |
const values = spinnerStyles.transform.split("(")[1].split(")")[0].split(","); | |
const a = values[0]; | |
const b = values[1]; | |
let rad = Math.atan2(b, a); | |
if (rad < 0) rad += (2 * Math.PI); | |
const angle = Math.round(rad * (180 / Math.PI)); | |
const slice = Math.floor(angle / prizeSlice); | |
if (currentSlice !== slice) { | |
ticker.style.animation = "none"; | |
setTimeout(() => ticker.style.animation = null, 10); | |
currentSlice = slice; | |
} | |
tickerAnim = requestAnimationFrame(runTickerAnimation); | |
}; | |
const selectPrize = () => { | |
const selected = Math.floor(rotation / prizeSlice); | |
prizeNodes[selected].classList.add(selectedClass); | |
reaper.dataset.reaction = prizeNodes[selected].dataset.reaction; | |
}; | |
trigger.addEventListener("click", () => { | |
if (reaper.dataset.reaction !== "resting") { | |
reaper.dataset.reaction = "resting"; | |
} | |
trigger.disabled = true; | |
rotation = Math.floor(Math.random() * 360 + spinertia(2000, 5000)); | |
prizeNodes.forEach((prize) => prize.classList.remove(selectedClass)); | |
wheel.classList.add(spinClass); | |
spinner.style.setProperty("--rotate", rotation); | |
ticker.style.animation = "none"; | |
runTickerAnimation(); | |
}); | |
spinner.addEventListener("transitionend", () => { | |
cancelAnimationFrame(tickerAnim); | |
trigger.disabled = false; | |
trigger.focus(); | |
rotation %= 360; | |
selectPrize(); | |
wheel.classList.remove(spinClass); | |
spinner.style.setProperty("--rotate", rotation); | |
}); | |
setupWheel(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@import url("https://fonts.googleapis.com/css2?family=Girassol&display=swap"); | |
* { | |
box-sizing: border-box; | |
} | |
html, | |
body { | |
height: 100%; | |
} | |
body { | |
display: grid; | |
place-items: center; | |
overflow: hidden; | |
} | |
.deal-wheel { | |
--size: clamp(250px, 80vmin, 700px); | |
--lg-hs: 0 3%; | |
--lg-stop: 50%; | |
--lg: linear-gradient( | |
hsl(var(--lg-hs) 0%) 0 var(--lg-stop), | |
hsl(var(--lg-hs) 20%) var(--lg-stop) 100% | |
); | |
position: relative; | |
display: grid; | |
grid-gap: calc(var(--size) / 20); | |
align-items: center; | |
grid-template-areas: | |
"spinner" | |
"trigger"; | |
font-family: "Girassol", sans-serif; | |
font-size: calc(var(--size) / 21); | |
line-height: 1; | |
text-transform: lowercase; | |
} | |
.deal-wheel > * { | |
grid-area: spinner; | |
} | |
.deal-wheel .btn-spin { | |
grid-area: trigger; | |
justify-self: center; | |
} | |
.spinner { | |
position: relative; | |
display: grid; | |
align-items: center; | |
grid-template-areas: "spinner"; | |
width: var(--size); | |
height: var(--size); | |
transform: rotate(calc(var(--rotate, 25) * 1deg)); | |
border-radius: 50%; | |
box-shadow: inset 0 0 0 calc(var(--size) / 40) hsl(0deg 0% 0% / 0.06); | |
} | |
.spinner * { | |
grid-area: spinner; | |
} | |
.prize { | |
position: relative; | |
display: flex; | |
align-items: center; | |
padding: 0 calc(var(--size) / 6) 0 calc(var(--size) / 20); | |
width: 50%; | |
height: 50%; | |
transform-origin: center right; | |
transform: rotate(var(--rotate)); | |
user-select: none; | |
} | |
.cap { | |
--cap-size: calc(var(--size) / 4); | |
position: relative; | |
justify-self: center; | |
width: var(--cap-size); | |
height: var(--cap-size); | |
} | |
/* Hide select dropdown from SVG import file */ | |
.cap select { | |
display: none; | |
} | |
.cap svg { | |
width: 100%; | |
} | |
.ticker { | |
position: relative; | |
left: calc(var(--size) / -15); | |
width: calc(var(--size) / 10); | |
height: calc(var(--size) / 20); | |
background: var(--lg); | |
z-index: 1; | |
clip-path: polygon(20% 0, 100% 50%, 20% 100%, 0% 50%); | |
transform-origin: center left; | |
} | |
.btn-spin { | |
color: hsl(0deg 0% 100%); | |
background: var(--lg); | |
border: none; | |
font-family: inherit; | |
font-size: inherit; | |
line-height: inherit; | |
text-transform: inherit; | |
padding: 0.9rem 2rem 1rem; | |
border-radius: 0.25rem; | |
cursor: pointer; | |
transition: opacity 200ms ease-out; | |
} | |
.btn-spin:focus { | |
outline-offset: 2px; | |
} | |
.btn-spin:active { | |
transform: translateY(1px); | |
} | |
.btn-spin:disabled { | |
cursor: progress; | |
opacity: 0.25; | |
} | |
/* Spinning animation */ | |
.is-spinning .spinner { | |
transition: transform 8s cubic-bezier(0.1, -0.01, 0, 1); | |
} | |
.is-spinning .ticker { | |
animation: tick 700ms cubic-bezier(0.34, 1.56, 0.64, 1); | |
} | |
@keyframes tick { | |
40% { | |
transform: rotate(-12deg); | |
} | |
} | |
/* Selected prize animation */ | |
.prize.selected .text { | |
color: white; | |
animation: selected 800ms ease; | |
} | |
@keyframes selected { | |
25% { | |
transform: scale(1.25); | |
text-shadow: 1vmin 1vmin 0 hsla(0 0% 0% / 0.1); | |
} | |
40% { | |
transform: scale(0.92); | |
text-shadow: 0 0 0 hsla(0 0% 0% / 0.2); | |
} | |
60% { | |
transform: scale(1.02); | |
text-shadow: 0.5vmin 0.5vmin 0 hsla(0 0% 0% / 0.1); | |
} | |
75% { | |
transform: scale(0.98); | |
} | |
85% { | |
transform: scale(1); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<link href="https://codepen.io/hexagoncircle/pen/vYxKLOa.css" rel="stylesheet" /> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment