На некоторых сайтах можно встретить колесо фортуны с призами. Работает так: нажимаете кнопку, колесо начинает крутиться, и на что показывает стрелка после остановки — это и есть ваш приз. Есть сервисы, которые предоставляют такое колесо как платную услугу, а мы сделаем своё и бесплатно:
❗️ В этом проекте довольно люто используется CSS 3. Мы о нём ещё не писали, но мы исправимся и напишем. Многие штуки в CSS-коде будут выглядеть непривычно, поэтому мы их объясним прямо в комментариях. Крепитесь.
Готовим страницу
Как обычно в наших проектах, на странице будет только разметка невидимых блоков — всё содержимое появится потом, из скрипта. Внутреннее устройство будет такое:
- делаем главный блок deal-wheel, внутри которого будут находиться все элементы;
- внутрь этого блока добавляем список spinner — это будут наши надписи на секторах;
- туда же кладём блок с язычком барабана ticker, который укажет на приз и кнопку с классом btn-spin — она запустит колесо.
За остальное будет отвечать скрипт.
<!DOCTYPE html>
<html lang="ru" >
<head>
<meta charset="UTF-8">
<title>Колесо удачи</title>
<!-- используем всю ширину экрана -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- подключаем стили -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- главный блок -->
<div class="deal-wheel">
<!-- блок с призами -->
<ul class="spinner"></ul>
<!-- язычок барабана -->
<div class="ticker"></div>
<!-- кнопка -->
<button class="btn-spin">Испытай удачу</button>
</div>
<!-- подключаем скрипт -->
<script src="script.js"></script>
</body>
</html>
Сразу добавим стили в отдельный файл style.css:
/* делаем везде так, чтобы свойства width и height задавали не размеры контента, а размеры блока */
* {
box-sizing: border-box;
}
/* общие настройки страницы */
body {
/* подключаем сетку */
display: grid;
/* ставим всё по центру */
place-items: center;
/* если что-то не помещается на своё место — скрываем то, что не поместилось */
overflow: hidden;
}
Настраиваем общий блок
Задача общего блока — установить связи между элементами, распределить их внутри виртуальной сетки и настроить параметры отображения внутренних элементов.
Чтобы не перегружать код одними и теми же параметрами, будем использовать CSS-переменные. Они начинаются с двух дефисов и работают внутри того блока, в котором прописаны. Также эти переменные понадобятся нам при настройке анимации в скрипте.
Добавим стили в файл style.css. Читайте комментарии, тут всё подробно объяснено:
/* общий блок для всех элементов */
.deal-wheel {
/* задаём переменные блока, внутри которого всё будет рисоваться */
/* размеры колеса */
--size: clamp(250px, 80vmin, 700px);
/* clamp — функция CSS, которая задаёт три размера: минимальное, предпочтительное и максимальное. В данном случае мы хотим, чтобы колесо было не меньше 250 пикселей, не больше 700 пикселей, но в идеале — 80% от безопасно малой высоты окна браузера */
/* настройки яркости и заливки фона секторов. Нам понадобится описать поведение градиента, это у нас делается через много переменных */
--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;
/* подключаем стандартную CSS-сетку */
display: grid;
grid-gap: calc(var(--size) / 20);
/* выравниваем содержимое блока по центру */
align-items: center;
/* задаём имена областей внутри сетки — в CSS теперь можно прямо назвать эти области */
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;
}
Готовим переменные в скрипте
Так как на самой странице у нас только блоки, всё остальное содержимое будем делать и добавлять через скрипт script.js.
Первое, что нам понадобится, — завести все переменные, которые будем использовать в проекте. Начнём со списка призов. Обратите внимание, что цвета здесь указаны в системе HSL — hue, saturation, lightness (оттенок, насыщенность, яркость). Это не необходимость, можно было указать и в RGB, и в hex-значениях:
// надписи и цвета на секторах
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;
Добавляем секторы и призы на экран
Теперь, когда у нас есть все нужные переменные, добавим призы в блок со списком ".spinner". Логика такая:
- Перебираем весь список с призами, один за одним, по очереди.
- Сразу считаем угол поворота для каждой надписи.
- Добавляем в конец списка HTML-код, чтобы у нас появился новый элемент маркированного списка.
- В этом же коде добавляем ему стиль для поворота на нужный угол.
// расставляем текст по секторам
const createPrizeNodes = () => {
// обрабатываем каждую подпись
prizes.forEach(({ text, color, reaction }, i) => {
// каждой из них назначаем свой угол поворота
const rotation = ((prizeSlice * i) * -1) - prizeOffset;
// добавляем код с размещением текста на страницу в конец блока spinner
spinner.insertAdjacentHTML(
"beforeend",
// текст при этом уже оформлен нужными стилями
`<li class="prize" data-reaction=${reaction} style="--rotate: ${rotation}deg">
<span class="text">${text}</span>
</li>`
);
});
};
Также сделаем разбивку по цветным секторам: просто добавим нужные параметры к стилю у класса ".spinner":
// рисуем разноцветные секторы
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");
};
// подготавливаем всё к первому запуску
setupWheel();
После запуска вам может показаться, что наш код не работает. Но на самом деле это не так: код работает как нужно, просто мы не добавили в стили новые параметры, которые использовали в коде — spinner и prize. Исправим это на следующем шаге.
Исправляем внешний вид колеса
Сейчас блок с колесом выглядит как прямоугольник, потому что зависит от содержимого с текстом. Чтобы это стало похоже на круг, добавим стили специально для секторов колеса:
/* сектор колеса */
.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;
}
Стало лучше, но кнопка теперь слишком мелкая. Нужно исправить.
Кнопка запуска
Сделаем текст на кнопке того же размера, что и надписи на секторах. Заодно пропишем внешний вид неактивной кнопки: пусть она будет полупрозрачной и с другим курсором. Тогда сразу будет понятно — кнопка работает, нажимать пока нельзя.
/* кнопка запуска колеса */
.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;
}
Добавляем язычок
Язычок — это такой указатель на колесе, который всё время указывает на какой-то сектор. При вращении настоящего колеса фортуны металлический язычок касается столбиков на границе секторов и отклоняется в сторону. Так легко можно определить — перескочил язычок на новый сектор или скорости колеса не хватило и он остался на столбике, указывая на предыдущее значение.
Пока просто нарисуем язычок, а механику добавим чуть позже:
/* язычок */
.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;
}
Задаём количество оборотов
Если мы в жизни запустим такое колесо, то оно постепенно будет замедляться. За это отвечает сила трения и разные физические факторы. Чтобы нам реализовать такую же механику, мы заранее определим количество градусов, на которое повернётся колесо. Для этого добавим функцию, которая вернёт нам случайным образом некоторое число в зависимости от минимального и максимального параметра вращения:
// функция запуска вращения с плавной остановкой
const spinertia = (min, max) => {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
};
Запускаем колесо
Чтобы запустить колесо, нужно нажать на кнопку. Но так как мы в HTML-файле не прописывали обработчик нажатия, добавим такой обработчик в JS-файле. Читайте комментарии, чтобы разобраться подробнее, что происходит в этом блоке:
// отслеживаем нажатие на кнопку
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();
});
👉 Готовый код анимации вращения мы взяли с сайта css-tricks.com — там много интересного; если знаете английский, то загляните на досуге.
// функция запуска вращения с плавной остановкой
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);
};
Чтобы магия анимации сработала, добавим нужные свойства в CSS-файл:
/* анимация вращения */
.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);
}
}
Видно, что колесо запускается и останавливается, но больше ничего не происходит: кнопка не отжимается и приз никак не подсвечивается. Этим и займёмся.
Отжимаем кнопку
Кнопку можно отжимать только после того, как колесо полностью остановилось и нам выпал какой-то приз. Чтобы это сработало именно после остановки колеса, добавим ещё один обработчик событий. Он будет следить за анимацией секторов, и если она закончилась — запустится код обработчика:
// отслеживаем, когда закончилась анимация вращения колеса
spinner.addEventListener("transitionend", () => {
// останавливаем отрисовку вращения
cancelAnimationFrame(tickerAnim);
// получаем текущее значение поворота колеса
rotation %= 360;
// выбираем приз
selectPrize();
// убираем класс, который отвечает за вращение
wheel.classList.remove(spinClass);
// отправляем в CSS новое положение поворота колеса
spinner.style.setProperty("--rotate", rotation);
// делаем кнопку снова активной
trigger.disabled = false;
});
И сразу добавим код, который добавит спецэффектов в выпавший сектор:
// функция выбора призового сектора
const selectPrize = () => {
const selected = Math.floor(rotation / prizeSlice);
prizeNodes[selected].classList.add(selectedClass);
};
Добавляем спецэффект в приз
Последнее, что нам осталось сделать в этом проекте, — добавить спецэффект в призовой сектор. Сделаем его так: сделаем текст белым, а потом на мгновение сделаем его чуть больше, как будто он выпрыгивает на нас, а потом возвращается на место. И добавим тень, которую текст будет отбрасывать при вылете:
/* анимируем выпавший сектор */
.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);
}
}
Покрутить колесо фортуны на сайте проекта
<!DOCTYPE html>
<html lang="ru" >
<head>
<meta charset="UTF-8">
<title>Колесо удачи</title>
<!-- используем всю ширину экрана -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- подключаем стили -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- главный блок -->
<div class="deal-wheel">
<!-- блок с призами -->
<ul class="spinner"></ul>
<!-- язычок барабана -->
<div class="ticker"></div>
<!-- кнопка -->
<button class="btn-spin">Испытай удачу</button>
</div>
<!-- подключаем скрипт -->
<script src="script.js"></script>
</body>
</html>
/* делаем везде так, чтобы свойства 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",
// текст при этом уже оформлен нужными стилями
`<li class="prize" data-reaction=${reaction} style="--rotate: ${rotation}deg">
<span class="text">${text}</span>
</li>`
);
});
};
// рисуем разноцветные секторы
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();