Skip to content

Instantly share code, notes, and snippets.

@muath-ye
Created September 17, 2024 06:27
Show Gist options
  • Save muath-ye/3653edd969288a477b5526f022096ba7 to your computer and use it in GitHub Desktop.
Save muath-ye/3653edd969288a477b5526f022096ba7 to your computer and use it in GitHub Desktop.
Grim Reaper's Used Cars - Deal Wheel
<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>
/**
* 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();
@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);
}
}
<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