Кейс: Создание попапа с виджетом "Колесо фортуны"
Специальный виджет "Колесо фортуны" позволяет создать интерактивный попап, который вовлекает посетителей сайта в игру с призами. Пользователь заполняет простую форму и вращает колесо, чтобы получить случайный подарок: скидку, промокод или другой бонус. Данное руководство описывает процесс создания такого попапа в конструкторе Altcraft:

Перед настройкой колеса рекомендуется создать поле в базе профилей, в которое будет сохраняться информация о выигранном призе. О том, как создавать дополнительные поля, вы можете узнать в этой статье.
Создание попапа и выбор шаблона
Начните с создания нового попапа в разделе Веб-слой — Попапы, нажав кнопку Создать. Выберите тип попапа. Для колеса фортуны идеально подходит Модальное окно — оно появляет ся в центре экрана:

Выберите шаблон. Воспользуйтесь любым из готовых шаблонов как основой. Для упрощения кастомизации рекомендуется использовать шаблоны с минималистичным дизайном (например, с космонавтом):

Задайте имя и описание. Укажите системное имя и при необходимости — описание:

Интеграция кода виджета
Виджет "Колесо фортуны" реализован на HTML, CSS и JavaScript. Для его добавления необходимо заменить код стандартного шаблона на предоставленный готовый код.
- CSS
- HTML
- JavaScript
Перейдите на вкладку CSS. Не удаляйте существующие стили. Прокрутите редактор в самый конец и добавьте предоставленный CSS-код для стилизации колеса и его контейнера.
.formWrapper {
display: flex;
align-items: center;
justify-content: space-around;
flex-wrap: wrap;
}
.ac-button:disabled {
opacity: 0.6;
cursor: not-allowed;
background-color: #cccccc;
color: #666666;
transform: none;
box-shadow: none;
}
.ac-button:disabled:hover {
background-color: #cccccc;
color: #666666;
transform: none;
box-shadow: none;
}
В разделе "Настройки" откройте вкладку HTML. Полностью удалите текущее содержимое редактора и вставьте предоставленный HTML-код для колеса.
<div class="ac-popup-overlay">
<div class="ac-modal">
<div class="ac-modal_content">
<div style="display: none;" data-popupId="{{popup('copywriting' 'popupId' 'text')}}"></div>
<div id="firstPage">
<h4 class="ac-title">
{{popup('copywriting' 'title' 'text')}}
</h4>
<div class="ac-description">
{{popup('copywriting' 'text' 'text')}}
</div>
<div class="formWrapper">
<div class="canvas-container">
<canvas id="canvas" width="420" height="420"></canvas>
</div>
<form id="ac-form" class="ac-form" action>
<div class="ac-field">
<label class="ac-label" for="email">
{{popup('copywriting' 'emailLabel' 'text')}}
</label>
<input
id="email"
type="email"
class="ac-input"
name="{{popup('form' 'email' 'fieldData')}}"
placeholder="{{popup('copywriting' 'emailPlaceholder' 'text')}}"
/>
</div>
<div class="ac-field">
<label class="ac-label" for="name">
{{popup('copywriting' 'nameLabel' 'text')}}
</label>
<input
id="name"
class="ac-input"
name="{{popup('form' 'name' 'fieldData')}}"
placeholder="{{popup('copywriting' 'namePlaceholder' 'text')}}"
/>
</div>
<div class="ac-field">
<label>
<input type="checkbox" name="{{popup('form' 'privacyPolicy' 'fieldData')}}" id="privacyPolicy" required>
<span>С <a href="{{popup('copywriting' 'policy' 'link')}}" target="_blank">политикой конфиденциальности</a> ознакомлен</span>
</label>
</div>
<div class="ac-validation-msg" for="email"></div>
<input type="text" style="display: none;" class="ac-input" id="prizeField" name="{{popup('form' 'prize' 'fieldData')}}">
<button class="ac-button" type="button" id="winBtn" disabled>
{{popup('copywriting' 'winBtn' 'text')}}
</button>
<div class="ac-form_submit" style="display: none;">
<button type="submit" class="ac-button" id="submitBtn">
{{popup('cta' 'text' 'text')}}
</button>
</div>
</form>
</div>
</div>
<div id="secondPage" style="display: none;">
<h4 class="ac-title">
{{popup('copywriting' 'title-congrats' 'text')}}
</h4>
<div class="ac-description">
{{popup('copywriting' 'text-before-prize' 'text')}}
<span id="prizeName"></span>
<div>
{{popup('copywriting' 'text-after-prize' 'text')}}
</div>
</div>
</div>
</div>
{{popup('closeButton' 'closeButton' 'closeBtn')}}
</div>
</div>
Переключитесь на вкладку JavaScript. Полностью удалите текущее содержимое и вставьте предоставленный JS-код, который отвечает за логику вращения колеса, определение выигрыша и работу с формой.
// Список выигрышей. Длина может варьироваться
// id - Идентификатор, отправляемый на сервер
// text - Подпись у сектора колеса
const options = [
{id: 1, text: '$100'},
{id: 2, text: '$10'},
{id: 3, text: '$25'},
{id: 4, text: '$250'},
{id: 5, text: '$30'},
{id: 6, text: '$1000'},
{id: 7, text: '$1'},
{id: 8, text: '$200'},
{id: 9, text: '$45'},
{id: 10, text: '$500'},
{id: 11, text: '$5'},
{id: 12, text: '$20'}
]
// Цвета секторов колеса. Если цветов меньше, чем секторов, то цвета повторяются циклично
const colors = ['white', '#3583FF']
// Начальный угол поворота колеса
let currentAngle = 0;
// Длительность вращения колеса задаётся случайно
// Минимальное время вращения (сек)
const minSpinTime = 4;
// Максимальное время вращения (сек)
const maxSpinTime = 7;
// Задержка между остановкой колеса и сообщением о выигрыше (сек)
const pageSwitchDelay = 2;
// Цвет подписей секторов на колесе
const textColor = "black";
let isPreview = true;
let myPopup = null;
const allPopups = document.querySelectorAll('[data-ac-popup-container]');
allPopups.forEach(item =>{
const idContainer = item.shadowRoot.querySelector('[data-popupId={{popup('copywriting' 'popupId' 'text')}}]');
if(idContainer){
myPopup = item.shadowRoot;
isPreview = false;
}
})
if (!myPopup) {
myPopup = document;
}
const winBtn = myPopup.getElementById('winBtn');
function initInputs(){
let emailInput = myPopup.getElementById('email');
let nameInput = myPopup.getElementById('name');
let policyInput = myPopup.getElementById('privacyPolicy');
function updateButtonState() {
const isEmailEmpty = !emailInput.value.trim();
const isNameEmpty = !nameInput.value.trim();
const isPolicyChecked = policyInput.checked;
console.log(myPopup.getElementById('winBtn'))
myPopup.getElementById('winBtn').disabled = isEmailEmpty || isNameEmpty || !isPolicyChecked
}
emailInput.addEventListener('input', updateButtonState);
nameInput.addEventListener('input', updateButtonState);
policyInput.addEventListener('input', updateButtonState);
myPopup.getElementById("winBtn").addEventListener("click", spin);
updateButtonState();
}
if(isPreview) {
setTimeout(()=>{initInputs()}, 1000);
} else {
initInputs();
}
let isInitialized = false;
function initializeWheel() {
if (isInitialized) return;
const canvas = myPopup.getElementById("canvas");
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (ctx && !canvas.dataset.initialized) {
drawRouletteWheel();
canvas.dataset.initialized = "true";
isInitialized = true;
}
}
const observer = new MutationObserver((mutations) => {
for (let mutation of mutations) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1 && (node.id === 'canvas' || node.querySelector?.('#canvas'))) {
setTimeout(initializeWheel, 100);
}
});
}
}
});
if (document.body) {
observer.observe(document.body, { childList: true, subtree: true });
}
document.addEventListener('DOMContentLoaded', initializeWheel);
let segmentAngle = Math.PI / (options.length / 2);
let spinTimeout = null;
let spinAngleStart = 0;
let spinTime = 0;
let spinTimeTotal = 0;
let ctx;
function drawRouletteWheel() {
const canvas = myPopup.getElementById("canvas");
if (canvas.getContext) {
const centerX = 210;
const centerY = 210;
const outsideRadius = 200;
const textRadius = 130;
const insideRadius = 0;
ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, 400, 400);
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
ctx.font = 'bold 12px Helvetica, Arial';
ctx.strokeStyle = "white";
ctx.lineWidth = 15;
ctx.beginPath();
ctx.arc(centerX, centerY, outsideRadius, 0, 2 * Math.PI);
ctx.stroke();
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
for(let i = 0; i < options.length; i++) {
const angle = currentAngle + i * segmentAngle;
const segmentMiddleAngle = angle + segmentAngle / 2;
ctx.fillStyle = colors[i % colors.length];
ctx.beginPath();
ctx.arc(centerX, centerY, outsideRadius, angle, angle + segmentAngle, false);
ctx.arc(centerX, centerY, insideRadius, angle + segmentAngle, angle, true);
ctx.stroke();
ctx.fill();
ctx.save();
ctx.fillStyle = textColor;
const textX = centerX + Math.cos(segmentMiddleAngle) * textRadius;
const textY = centerY + Math.sin(segmentMiddleAngle) * textRadius;
ctx.translate(textX, textY);
ctx.rotate(segmentMiddleAngle);
const text = options[i].text;
ctx.fillText(text, -ctx.measureText(text).width / 2, 0);
ctx.restore();
}
ctx.fillStyle = "white";
ctx.strokeStyle = "#666666";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(centerX - 10, centerY - 15);
ctx.lineTo(centerX + 30, centerY);
ctx.lineTo(centerX - 10, centerY + 15);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
}
function rotateWheel() {
spinTime += 30;
if(spinTime >= spinTimeTotal) {
stopRotateWheel();
return;
}
const spinAngle = spinAngleStart - easeOut(spinTime, 0, spinAngleStart, spinTimeTotal);
currentAngle += (spinAngle * Math.PI / 180);
drawRouletteWheel();
spinTimeout = setTimeout(rotateWheel, 30);
}
function spin(e) {
e.preventDefault();
const form = myPopup.getElementById('ac-form');
const isValid = form.checkValidity();
if(!isValid){
myPopup.getElementById('submitBtn').click();
return;
}
winBtn.removeEventListener('click', spin);
spinAngleStart = Math.random() * 10 + 10;
spinTime = 0;
spinTimeTotal = Math.random() * (maxSpinTime - minSpinTime) + minSpinTime * 1000;
rotateWheel();
}
function handlePrize(prize){
myPopup.getElementById("prizeField").value = prize.id;
myPopup.getElementById('submitBtn').click();
myPopup.getElementById('prizeName').innerText = prize.text;
}
function showSecondPage() {
myPopup.getElementById('firstPage').style.display = 'none';
myPopup.getElementById('secondPage').style.display = 'block';
}
function stopRotateWheel() {
clearTimeout(spinTimeout);
const degrees = currentAngle * 180 / Math.PI;
const segmentDegrees = segmentAngle * 180 / Math.PI;
const index = Math.floor((360 - degrees % 360) / segmentDegrees);
handlePrize(options[index]);
const form = myPopup.getElementById('ac-form');
const isValid = form.checkValidity();
setTimeout(()=>{
showSecondPage();
}, pageSwitchDelay * 1000);
}
function easeOut(currentTime, startValue, changeInValue, duration) {
const normalizedTime = currentTime / duration;
const squaredTime = normalizedTime * normalizedTime;
const cubedTime = squaredTime * normalizedTime;
return startValue + changeInValue * (cubedTime + -3 * squaredTime + 3 * normalizedTime);
}
drawRouletteWheel();
myPopup.getElementById('ac-form').submit = () => {}
Настройка контента и дизайна в визуальном редакторе
После интеграции кода перейдите в раздел Дизайн. Здесь вы можете наполнить попап текстом и настроить его внешний вид, используя привычный визуальный интерфейс, без правки кода.
- Ширина окна
- Текстовое наполнение
- Внешний вид
Первым делом задайте ширину окна (Форма — width). Размеры холста с колесом фиксированы (420 px). Поэтому ширина всего попапа должна быть не менее 420 px. Если вы хотите разместить колесо и форму для ввода данных на одной горизонтальной линии, установите ширину не менее 810 px.
На вкладке Копирайтинг заполните ключевые текстовые параметры:
title— главный заголовок на первом экране (например, "Испытай удачу!").text— поясняющий текст или описание акции.emailLabelиemailPlaceholder— подпись и подсказка для поля ввода email.nameLabelиnamePlaceholder— подпись и подсказка для поля ввода имени.policy— обязательная ссылка на страницу с политикой конфиденциальности. Без заполнения этого поля сохранить попап не получится.winBtn— текст на кнопке, которая запускает вращение колеса (например, "Крутить колесо!").popupId— обязательный параметр. Уникальный текстовый идентификатор попапа (например,bookverse_wheel). Необходим для корректной работы, если на странице используется несколько попапов.
Попап состоит из двух экранов. Параметры для экрана с сообщением о выигрыше:
title-congrats— заголовок поздравления.text-before-prize— текст, который будет показан перед названием выигранного приза.text-after-prize— дополнительный текст под сообщением о призе.
Используйте остальные вкладки раздела Дизайн для тонкой настройки внешнего вида:
- Фон — измените цвет фона попапа и прозрачность затемнения страницы (
overlay). - Рамка — настройте скругление углов всплывающего окна.
- Форма — задайте стили для полей ввода: отступы, цвета границ, фона и текста.
- Действия — настройте внешний вид кнопок (цвет, с кругление, шрифт).
- Типографика — выберите общий шрифт и стили для всех текстовых элементов.
Любые изменения в визуальном редакторе сразу отображаются в окне предпросмотра справа.
Кастомизация колеса фортуны
Внешний вид и поведение самого колеса (призы, цвета секторов, скорость вращения) настраиваются путём правки переменных в JavaScript-коде.
Вернитесь в раздел Настройки — JavaScript. В начале файла найдите и отредактируйте следующие переменные.
- Призы (options)
- Цвета секторов (colors)
- Скорость вращения
- Задержка показа
- Цвет текста (textColor)
Переменная options определяет список призов на колесе. Это массив объектов, где каждый объект содержит id и текст приза text.
const options = [
{ id: 1, text: '$100' },
{ id: 2, text: '$10' },
{ id: 3, text: '$25' },
{ id: 4, text: '$250' },
{ id: 5, text: '$30' },
{ id: 6, text: '$1000' },
{ id: 7, text: '$1' },
{ id: 8, text: '$200' },
{ id: 9, text: '$45' },
{ id: 10, text: '$500' },
{ id: 11, text: '$5' },
{ id: 12, text: '$20' }
];
Как изменить:
- Текст приза: Замените значение
textна нужное (например,'10% скидка'или'Бесплатная доставка'). - Количество призов: Добавьте новые объекты в массив или удалите существующие. Количество секторов на колесе изменится автоматически.
Переменная colors — это массив цветов для заливки секторов колеса. Цвета применяются по порядку, начиная с первого сектора.
const colors = ['black', 'orange'];
Как изменить: Замените цвета в массиве на нужные. Вы можете использовать:
- Названия цветов на английском:
'red','blue','green' - HEX-коды:
'#4f46e5','#f59e0b','#10b981' - RGB/RGBA значения:
'rgb(79, 70, 229)'
Длительность вращения колеса задаётся двумя переменными:
minSpinTime— минимальное время вращения (в секундах).maxSpinTime— максимальное время вращения (в секундах).
Фактическое время каждого вращения будет случайным числом в этом диапазоне.
const minSpinTime = 4;
const maxSpinTime = 7;
Как изменить: Увеличьте значения, чтобы колесо вращалось дольше, или уменьшите значения для более быстрого результата.
Переменная pageSwitchDelay задаёт паузу (в секундах) между остановкой колеса и автоматическим переходом на экран с сообщением о выигрыше.
const pageSwitchDelay = 2;
Как изменить: Измените значение на необходимое количество секунд.
Переменная textColor определяет цвет надписей с названиями призов на секторах колеса.
const textColor = "white";
Как изменить:
Замените значение на любой валидный CSS-цвет, чтобы текст хорошо читался на фоне сектора. Например, "#ffffff", "black", "#1f2937".
Сохранение данных о выигранном призе
После того как пользователь вращает колесо и выигрывает приз, информация о выигрыше автоматически заполняется в скрытом поле формы prize. Чтобы сохранить эти данные в профиле пользователя в Altcraft, необходимо настроить действие импорта в раздел е Действия.
В редакторе попапа откройте вкладку Действия:

Настройте импорт профилей:
- Следуйте стандартной процедуре настройки импорта, описанной в статье о создании попапов.
- В настройках импорта обязательно настройте сопоставление поля
prizeиз формы с нужным полем в вашей базе данных Altcraft.

По умолчанию JavaScript-код записывает в поле prize текст выигранного приза (prize.text). Если вам нужно сохранять идентификатор приза (prize.id), отредактируйте функцию handlePrize() в JS-коде, заменив prize.text на prize.id.
После настройки импорта при каждой отправке формы данные о выигрыше (текст или ID приза) будут сохраняться в профиле пользователя вместе с его email и именем.