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

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

Достаточно одного скрипта и немного 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();

Текст и иллюстрации:

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

Художник:

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

Корректор:

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

Вёрстка:

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

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