Реализация Spin-the-Wheel (колесо фортуны) в мобильном приложении
Колесо фортуны — геймификационный элемент с узнаваемой механикой. Пользователь тянет, колесо раскручивается, замедляется с нарастающим саспенсом и останавливается на призе. Простая идея, но дьявол в деталях: физика вращения, точность остановки на нужном секторе и визуальный фидбек при победе определяют, будет ли это ощущаться как «казино» или как «сломанный счётчик».
Физика вращения
Колесо должно вести себя как реальный физический объект с инерцией. Не просто «крутиться N секунд и останавливаться» — а замедляться по экспоненциальному закону, имитируя трение.
Базовая модель:
// Угловая скорость уменьшается каждый кадр
angularVelocity *= decelerationFactor // 0.96–0.98 для медленного замедления
// CADisplayLink обновляет угол каждый кадр
currentAngle += angularVelocity * dt
wheelLayer.transform = CATransform3DMakeRotation(currentAngle, 0, 0, 1)
// Остановка при достижении минимальной скорости
if abs(angularVelocity) < 0.01 { stopAndSnap() }
decelerationFactor — ключевой параметр. 0.99 даёт долгое замедление (ощущение тяжёлого барабана), 0.95 — резкое (лёгкое). Настраиваем под ощущение бренда.
Точная остановка на нужном секторе
Честная рандомизация + точная остановка на определённом секторе — не противоречие. Алгоритм:
- Определяем победный сектор до начала вращения (серверная логика или клиентская с seed)
- Вычисляем целевой угол остановки:
targetAngle = sectorMidpoint + randomOffset(±halfSectorAngle) - Корректируем
decelerationFactorтак, чтобы колесо завершило нужное число оборотов и остановилось вtargetAngle
Формула для decelerationFactor при известных initialVelocity и targetAngle:
let totalRotation = currentAngle + (Double(minFullRotations) * 2 * .pi) + targetAngle
// Решаем геометрическую прогрессию: sum = v0 / (1 - factor)
decelerationFactor = 1 - initialVelocity / totalRotation
Это скрывает предопределённость результата — колесо выглядит случайным, но останавливается там, где нужно.
Snap-анимация при остановке
Небольшой «отскок» в конце усиливает ощущение реальности. Реализуем через CASpringAnimation:
let snapAnimation = CASpringAnimation(keyPath: "transform.rotation.z")
snapAnimation.fromValue = currentAngle
snapAnimation.toValue = snappedAngle
snapAnimation.stiffness = 200
snapAnimation.damping = 15
snapAnimation.initialVelocity = angularVelocity
Визуальный фидбек при победе
-
Haptic feedback:
UINotificationFeedbackGenerator.notificationOccurred(.success)в момент остановки на iOS;VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE)на Android - Lottie-конфетти: отдельный overlay поверх колеса, воспроизводится после остановки
-
Подсветка сектора:
CALayerс анимациейshadowOpacity0 → 1 → 0 трижды (мигание)
Реализация по платформам
iOS: CADisplayLink для физики + CALayer или UIView вращение. Сектора рисуем через CAShapeLayer с UIBezierPath.
Android: ValueAnimator с кастомным TimeInterpolator (логарифмическое замедление) + кастомный View с Canvas.drawArc.
Flutter: AnimationController с кастомным Simulation (наследник SpringSimulation или собственный) + CustomPainter для рисования секторов.
React Native: Animated.Value с useNativeDriver: true + react-native-svg для графики секторов, либо готовый пакет react-native-wheel-pick.
Срок: 2–3 дня включает физику, логику остановки, визуальный фидбек и интеграцию с серверной логикой призов.







