Создание попапа с виджетом "Колесо фортуны"
Специальный виджет "Колесо фортуны" позволяет создать интерактивный попап, который вовлекает посетителей сайта в игру с призами. Пользователь заполняет простую форму и вращает колесо, чтобы получить случайный подарок: скидку, промокод или другой бонус. Данное руководство описывает процесс создания такого попапа в конструкторе 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">
<canvas id="canvas" width="420" height="420"></canvas>
<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-код, который отвечает за логику вращения колеса, определение выигрыша и работу с формой.
function initWheel() {
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 = ['black', 'orange'];
let currentAngle = 0;
const minSpinTime = 4;
const maxSpinTime = 7;
const pageSwitchDelay = 2;
const textColor = "white";
let segmentAngle = Math.PI / (options.length / 2);
let spinTimeout = null;
let spinAngleStart = 0;
let spinTime = 0;
let spinTimeTotal = 0;
let ctx = null;
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length || mutation.type === 'childList') {
initializeWheelIfPopupFound();
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
setTimeout(initializeWheelIfPopupFound, 100);
function initializeWheelIfPopupFound() {
const allPopups = document.querySelectorAll('[data-ac-popup-container]');
allPopups.forEach(item => {
try {
if (item.dataset.wheelInitialized === 'true') {
return;
}
const popupId = "{{popup('copywriting' 'popupId' 'text')}}";
const shadowRoot = item.shadowRoot;
if (!shadowRoot) {
return;
}
const idContainer = shadowRoot.querySelector(`[data-popupId="${popupId}"]`);
if (idContainer) {
item.dataset.wheelInitialized = 'true';
initWheelComponents(shadowRoot);
observer.disconnect();
}
} catch (e) {
console.log('Error accessing popup:', e);
}
});
}
function initWheelComponents(myPopup) {
try {
const winBtn = myPopup.getElementById('winBtn');
const canvas = myPopup.getElementById('canvas');
const emailInput = myPopup.getElementById('email');
const nameInput = myPopup.getElementById('name');
const policyInput = myPopup.getElementById('privacyPolicy');
const form = myPopup.getElementById('ac-form');
if (!winBtn || !canvas || !form) {
return;
}
form.submit = () => {};
if (canvas.getContext) {
ctx = canvas.getContext("2d");
drawRouletteWheel();
}
winBtn.addEventListener("click", spin);
function updateButtonState() {
const isEmailEmpty = !emailInput?.value.trim();
const isNameEmpty = !nameInput?.value.trim();
const isPolicyChecked = policyInput?.checked;
if (winBtn) {
winBtn.disabled = isEmailEmpty || isNameEmpty || !isPolicyChecked;
}
}
if (emailInput) emailInput.addEventListener('input', updateButtonState);
if (nameInput) nameInput.addEventListener('input', updateButtonState);
if (policyInput) policyInput.addEventListener('input', updateButtonState);
updateButtonState();
if (form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
return false;
});
}
} catch (error) {
console.error('Error in initWheelComponents:', error);
}
}
function drawRouletteWheel() {
const allPopups = document.querySelectorAll('[data-ac-popup-container]');
let canvas = null;
let myPopup = null;
allPopups.forEach(item => {
if (item.dataset.wheelInitialized === 'true') {
try {
canvas = item.shadowRoot.getElementById('canvas');
myPopup = item.shadowRoot;
} catch (e) {}
}
});
if (!canvas || !canvas.getContext || !myPopup) {
return;
}
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) {
if (e) e.preventDefault();
const allPopups = document.querySelectorAll('[data-ac-popup-container]');
let winBtn = null;
allPopups.forEach(item => {
if (item.dataset.wheelInitialized === 'true') {
try {
winBtn = item.shadowRoot.getElementById('winBtn');
} catch (e) {}
}
});
if (!winBtn) return;
winBtn.disabled = true;
winBtn.removeEventListener('click', spin);
spinAngleStart = Math.random() * 10 + 10;
spinTime = 0;
spinTimeTotal = Math.random() * (maxSpinTime - minSpinTime) + minSpinTime * 1000;
rotateWheel();
}
function handlePrize(prize) {
const allPopups = document.querySelectorAll('[data-ac-popup-container]');
let myPopup = null;
allPopups.forEach(item => {
if (item.dataset.wheelInitialized === 'true') {
try {
myPopup = item.shadowRoot;
} catch (e) {}
}
});
if (!myPopup) return;
const prizeField = myPopup.getElementById('prizeField');
if (prizeField) {
prizeField.value = prize.text;
}
const prizeNameElement = myPopup.getElementById('prizeName');
if (prizeNameElement) {
prizeNameElement.innerText = prize.text;
}
const submitBtn = myPopup.getElementById('submitBtn');
if (submitBtn) {
setTimeout(() => {
submitBtn.click();
}, 100);
}
}
function showSecondPage() {
const allPopups = document.querySelectorAll('[data-ac-popup-container]');
let myPopup = null;
allPopups.forEach(item => {
if (item.dataset.wheelInitialized === 'true') {
try {
myPopup = item.shadowRoot;
} catch (e) {}
}
});
if (!myPopup) return;
const firstPage = myPopup.getElementById('firstPage');
const secondPage = myPopup.getElementById('secondPage');
if (firstPage) firstPage.style.display = 'none';
if (secondPage) 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) % options.length;
handlePrize(options[index]);
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);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initWheel);
} else {
initWheel();
}
window.addEventListener('load', initWheel);
window.reinitWheel = function() {
const allPopups = document.querySelectorAll('[data-ac-popup-container]');
allPopups.forEach(item => {
if (item.dataset.wheelInitialized === 'true') {
delete item.dataset.wheelInitialized;
}
});
initWheel();
};
Настройка контента и дизайна в визуальном редакторе
После интеграции кода перейдите в раздел "Дизайн". Здесь вы можете наполнить попап текстом и настроить его внешний вид, используя привычный визуальный интерфейс, без правки кода.
- Ширина окна
- Текстовое наполнение
- Внешний вид
Первым делом задайте ширину окна (Форма > width). Размеры холста с колесом фиксированы (420px). Поэтому ширина всего попапа должна быть не менее 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".
Изменения, внесённые в JS-код, влияют только на поведение виджета для конечного пользователя. В окне предпросмотра редактора актуальный вид колеса отображаться не будет, так как превью формируется на основе статического HTML/CSS шаблона.
Сохранение данных о выигранном призе
После того как пользователь вращает колесо и выигрывает приз, информация о выигрыше автоматически заполняется в скрытом поле формы prize. Чтобы сохранить эти данные в профиле пользователя в Altcraft, необходимо настроить действие импорта в разделе "Действия".
В редакторе попапа откройте вкладку "Действия":

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

По умолчанию JavaScript-код записывает в поле prize текст выигранного приза (prize.text). Если вам нужно сохранять идентификатор приза (prize.id), отредактируйте функцию handlePrize() в JS-коде, заменив prize.text на prize.id.
После настройки импорта при каждой отправке формы данные о выигрыше (текст или ID приза) будут сохраняться в профиле пользователя вместе с его email и именем.
Публикация попапа
После полной настройки сохраните попап и убедитесь, что его статус установлен в "Активен".
Доступно два основных способа размещения попапа на сайте:
- Через Менеджер тегов (рекомендуется). В разделе "Появление" привяжите попап к контейнеру Менедже ра тегов. Затем настройте триггер — условие, при котором попап будет показан (например, "Таймер" для показа через 10 секунд или "Глубина прокрутки" для показа при скролле 50% страницы). Подробнее о настройке триггеров читайте в отдельной статье.
- Вручную. Нажмите кнопку "Опубликовать", скопируйте сгенерированный код и разместите его на нужных страницах вашего сайта.
После публикации попап с колесом фортуны будет готов к работе и начнёт появляться у посетителей вашего сайта согласно заданным правилам.