/* делаем везде так, чтобы свойства width и height задавали не размеры контента, а размеры блока */
* {
box-sizing: border-box;
}
/* общие настройки страницы */
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-size: calc(var(--size) / 21);
}
/* всё, что относится ко внутренним элементам главного блока, будет находиться в области сетки с названием spinner */
.deal-wheel > * {
grid-area: spinner;
}
/* сам блок и кнопка будут находиться в области сетки с названием trigger и будут выровнены по центру */
.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%;
}
/* всё, что внутри этого блока, будет находиться в области сетки с названием spinner */
.spinner * {
grid-area: spinner;
}
/* текст на секторах */
.prize {
/* включаем «гибкую» вёрстку */
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;
}
/* язычок */
.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: white;
background: black;
border: none;
/* берём размер шрифта такой же, как в колесе */
font-size: inherit;
/* добавляем отступы от текста внутри кнопки */
padding: 0.9rem 2rem 1rem;
/* скругляем углы */
border-radius: 0.5rem;
/* меняем внешний вид курсора над кнопкой на руку*/
cursor: pointer;
}
/* если кнопка нажата и неактивна */
.btn-spin:disabled {
/* меняем внешний вид курсора */
cursor: progress;
/* делаем кнопку полупрозрачной */
opacity: 0.25;
}
/* анимация вращения */
.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);
}
}
/* анимируем выпавший сектор */
.prize.selected .text {
/* делаем текст белым */
color: white;
/* настраиваем длительность анимации */
animation: selected 800ms ease;
}
/* настраиваем анимацию текста на выпавшем секторе по кадрам */
@keyframes selected {
/* что происходит на 25% от начала анимации */
25% {
/* увеличиваем текст в 1,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);
}
}
// надписи и цвета на секторах
const prizes = [
{
text: "Скидка 10%",
color: "hsl(197 30% 43%)",
},
{
text: "Дизайн в подарок",
color: "hsl(173 58% 39%)",
},
{
text: "Второй сайт бесплатно",
color: "hsl(43 74% 66%)",
},
{
text: "Скидка 50%",
color: "hsl(27 87% 67%)",
},
{
text: "Блог в подарок",
color: "hsl(12 76% 61%)",
},
{
text: "Скидок нет",
color: "hsl(350 60% 52%)",
},
{
text: "Таргет в подарок",
color: "hsl(91 43% 54%)",
},
{
text: "Скидка 30% на всё",
color: "hsl(140 36% 74%)",
}
];
// создаём переменные для быстрого доступа ко всем объектам на странице — блоку в целом, колесу, кнопке и язычку
const wheel = document.querySelector(".deal-wheel");
const spinner = wheel.querySelector(".spinner");
const trigger = wheel.querySelector(".btn-spin");
const ticker = wheel.querySelector(".ticker");
// на сколько секторов нарезаем круг
const prizeSlice = 360 / prizes.length;
// на какое расстояние смещаем сектора друг относительно друга
const prizeOffset = Math.floor(180 / prizes.length);
// прописываем CSS-классы, которые будем добавлять и убирать из стилей
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
spinner.insertAdjacentHTML(
"beforeend",
// текст при этом уже оформлен нужными стилями
`
${text}
`
);
});
};
// рисуем разноцветные секторы
const createConicGradient = () => {
// устанавливаем нужное значение стиля у элемента spinner
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";
// и через 10 миллисекунд отменяем это, чтобы он вернулся в первоначальное положение
setTimeout(() => ticker.style.animation = null, 10);
// после того, как язычок прошёл сектор - делаем его текущим
currentSlice = slice;
}
// запускаем анимацию
tickerAnim = requestAnimationFrame(runTickerAnimation);
};
// функция выбора призового сектора
const selectPrize = () => {
const selected = Math.floor(rotation / prizeSlice);
prizeNodes[selected].classList.add(selectedClass);
};
// отслеживаем нажатие на кнопку
trigger.addEventListener("click", () => {
// делаем её недоступной для нажатия
trigger.disabled = true;
// задаём начальное вращение колеса
rotation = Math.floor(Math.random() * 360 + spinertia(2000, 5000));
// убираем прошлый приз
prizeNodes.forEach((prize) => prize.classList.remove(selectedClass));
// добавляем колесу класс is-spinning, с помощью которого реализуем нужную отрисовку
wheel.classList.add(spinClass);
// через CSS говорим секторам, как им повернуться
spinner.style.setProperty("--rotate", rotation);
// возвращаем язычок в горизонтальную позицию
ticker.style.animation = "none";
// запускаем анимацию вращение
runTickerAnimation();
});
// отслеживаем, когда закончилась анимация вращения колеса
spinner.addEventListener("transitionend", () => {
// останавливаем отрисовку вращения
cancelAnimationFrame(tickerAnim);
// получаем текущее значение поворота колеса
rotation %= 360;
// выбираем приз
selectPrize();
// убираем класс, который отвечает за вращение
wheel.classList.remove(spinClass);
// отправляем в CSS новое положение поворота колеса
spinner.style.setProperty("--rotate", rotation);
// делаем кнопку снова активной
trigger.disabled = false;
});
// подготавливаем всё к первому запуску
setupWheel();