Как сделать колесо фортуны на сайте

Как сделать колесо фортуны на сайте

Достаточно одного скрипта и немного CSS

На некоторых сайтах можно встретить колесо фортуны с призами. Работает так: нажимаете кнопку, колесо начинает крутиться, и на что показывает стрелка после остановки — это и есть ваш приз. Есть сервисы, которые предоставляют такое колесо как платную услугу, а мы сделаем своё и бесплатно:

Как сделать колесо фортуны на сайте

❗️ В этом проекте довольно люто используется 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". Логика такая:

  1. Перебираем весь список с призами, один за одним, по очереди.
  2. Сразу считаем угол поворота для каждой надписи.
  3. Добавляем в конец списка HTML-код, чтобы у нас появился новый элемент маркированного списка.
  4. В этом же коде добавляем ему стиль для поворота на нужный угол.

// расставляем текст по секторам
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();

Текст:

Михаил Полянин

Редактор:

Максим Ильяхов

Художник:

Даня Берковский

Корректор:

Ирина Михеева

Вёрстка:

Кирилл Климентьев

Соцсети:

Олег Вешкурцев

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Начать карьеру в ИТ
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
Как добавить плавающий блок на страницу
Как добавить плавающий блок на страницу

Иногда это и правда нужно.

easy
Запускаем телеграм-бота на сервере

Тогда он будет работать круглые сутки, а вы — отдыхать.

hard
Делаем сами: адаптивный сайт

С котиками!

medium
Как работает язык SQL
Как работает язык SQL

Управляем базой данных из командной строки

medium
ReferenceError: math is not defined — что это означает

Противная и неочевидная ошибка, которую очень легко исправить.

medium
Правильные цитаты: добавляем первоисточник в ворованный с вашего сайта контент
Правильные цитаты: добавляем первоисточник в ворованный с вашего сайта контент

При вставке все будут видеть, откуда скопирован текст

medium
Как накопить на Айфон ещё быстрее
Как накопить на Айфон ещё быстрее

Excel нам в помощь.

easy
Простейший генератор креативного текста

Без нейросетей, регистрации и СМС.

hard
Генератор паролей
Превращаем генератор паролей в настоящую программу

Настало время настоящего программирования — собираем приложение из исходников.

hard
Решаем задачу коммивояжёра простым перебором
Решаем задачу коммивояжёра простым перебором

Простое решение, но много кода.

easy
Делаем игру Quatro
Делаем игру Quatro

Интеллектуальная игра для двоих

hard
Подключаем нейросеть к веб-камере

Готовьтесь — искусственный интеллект уберёт вас из кадра.

medium
Ваш собственный телеграм-секретарь: делаем вместе
Ваш собственный телеграм-секретарь: делаем вместе

Пошаговая инструкция для тех, кому нужен секретарь.

easy
medium