Техническая спецификация анимаций для Telegram Mini App¶
Оценка технологий и рекомендации по реализации дыхательных анимаций
Содержание¶
- Ограничения Telegram Mini App
- Сравнение технологий
- Рекомендация для CalmTrader
- Архитектура анимаций
- Примеры реализации
- Производительность
- Чеклист перед релизом
Ограничения Telegram Mini App¶
WebView Environment¶
Telegram Mini App работает внутри WebView: - iOS: WKWebView (Safari engine) - Android: Android WebView (Chromium-based) - Desktop: Chromium Embedded Framework
Ключевые ограничения¶
| Аспект | Ограничение | Решение |
|---|---|---|
| Bundle size | Рекомендуется < 500KB | Минимизировать зависимости |
| Memory | Ограничена, особенно на старых устройствах | Избегать утечек памяти |
| CPU | Анимации конкурируют с UI | Использовать GPU-ускорение |
| Battery | Длительные сессии расходуют батарею | Оптимизировать repaints |
| Network | Медленный старт при загрузке | Lazy loading, кэширование |
Telegram WebApp API для анимаций¶
// Haptic Feedback (доступно)
Telegram.WebApp.HapticFeedback.impactOccurred('light' | 'medium' | 'heavy');
Telegram.WebApp.HapticFeedback.notificationOccurred('success' | 'error' | 'warning');
Telegram.WebApp.HapticFeedback.selectionChanged();
// Theme (для адаптации цветов)
Telegram.WebApp.themeParams.bg_color;
Telegram.WebApp.themeParams.text_color;
Telegram.WebApp.themeParams.button_color;
// Viewport
Telegram.WebApp.viewportHeight;
Telegram.WebApp.viewportStableHeight;
Telegram.WebApp.isExpanded;
Поддержка браузеров¶
| Фича | iOS Safari | Android Chrome | Desktop |
|---|---|---|---|
| CSS Animations | ✅ | ✅ | ✅ |
| CSS Transitions | ✅ | ✅ | ✅ |
| SVG SMIL | ⚠️ Deprecated | ⚠️ Deprecated | ⚠️ Deprecated |
CSS @keyframes |
✅ | ✅ | ✅ |
requestAnimationFrame |
✅ | ✅ | ✅ |
| Canvas 2D | ✅ | ✅ | ✅ |
| WebGL | ✅ | ✅ | ✅ |
CSS will-change |
✅ | ✅ | ✅ |
| Backdrop-filter | ⚠️ Partial | ✅ | ✅ |
Сравнение технологий¶
1. CSS Animations / Transitions¶
Плюсы:
- Zero dependencies
- GPU-ускорение для transform и opacity
- Отличная производительность
- Простота реализации
Минусы: - Ограниченная интерактивность - Сложно синхронизировать с JS
Размер: 0 KB (встроено)
Пример:
.breathing-circle {
animation: breathe 10s ease-in-out infinite;
will-change: transform;
}
@keyframes breathe {
0%, 100% { transform: scale(0.5); }
50% { transform: scale(1); }
}
Оценка: ⭐⭐⭐⭐⭐ для простых анимаций
2. JavaScript + CSS (Hybrid)¶
Плюсы: - Полный контроль над timing - Динамическое изменение параметров - Интерактивность (pause, resume, skip) - Синхронизация с состоянием приложения
Минусы: - Нужно аккуратно управлять памятью - requestAnimationFrame требует cleanup
Размер: ~1-2 KB кода
Пример:
class BreathingAnimation {
private element: HTMLElement;
private phase: 'inhale' | 'exhale' | 'hold' = 'inhale';
private startTime: number = 0;
private animationId: number = 0;
animate(timestamp: number) {
const elapsed = timestamp - this.startTime;
const progress = Math.min(elapsed / this.phaseDuration, 1);
const scale = this.calculateScale(progress);
this.element.style.transform = `scale(${scale})`;
if (progress < 1) {
this.animationId = requestAnimationFrame(this.animate.bind(this));
} else {
this.nextPhase();
}
}
stop() {
cancelAnimationFrame(this.animationId);
}
}
Оценка: ⭐⭐⭐⭐⭐ — рекомендуемый подход
3. SVG Animations¶
Плюсы: - Векторная графика (crisp на любом экране) - CSS-стилизация - Сложные формы (progress rings)
Минусы: - SMIL deprecated - Нужен JS для сложной логики
Размер: Inline SVG ~0.5-2 KB
Пример (Progress Ring):
<svg viewBox="0 0 100 100" class="progress-ring">
<circle cx="50" cy="50" r="45" class="ring-bg"/>
<circle cx="50" cy="50" r="45" class="ring-progress"
stroke-dasharray="283"
stroke-dashoffset="283"
style="--progress: 0"/>
</svg>
.ring-progress {
stroke-dashoffset: calc(283 * (1 - var(--progress)));
transition: stroke-dashoffset 0.1s linear;
transform: rotate(-90deg);
transform-origin: center;
}
Оценка: ⭐⭐⭐⭐⭐ для колец и путей
4. Lottie (After Effects → JSON)¶
Плюсы: - Сложные анимации от дизайнера - Готовые анимации на LottieFiles - Интерактивность через API
Минусы: - Размер библиотеки: 150-300 KB (критично!) - JSON-файлы анимаций: 20-200 KB каждая - Оверхед для простых анимаций
Пример:
import lottie from 'lottie-web';
const animation = lottie.loadAnimation({
container: element,
renderer: 'svg',
loop: true,
path: 'breathing.json'
});
animation.setSpeed(0.5);
animation.goToAndPlay(100, true);
Оценка: ⭐⭐⭐ — избыточно для MVP, возможно для премиум-версии
5. Canvas 2D API¶
Плюсы: - Полный контроль над рендерингом - Particle effects, blur, gradients - Хорошая производительность при правильном использовании
Минусы: - Больше кода - Нужно вручную управлять retina (devicePixelRatio) - Accessibility challenges
Размер: ~3-5 KB кода
Пример:
class CanvasBreathing {
private ctx: CanvasRenderingContext2D;
draw(scale: number) {
const { width, height } = this.canvas;
this.ctx.clearRect(0, 0, width, height);
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(width, height) * 0.3 * scale;
// Gradient fill
const gradient = this.ctx.createRadialGradient(
centerX, centerY, 0,
centerX, centerY, radius
);
gradient.addColorStop(0, '#7FDBFF');
gradient.addColorStop(1, '#4A9B9B');
this.ctx.beginPath();
this.ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
this.ctx.fillStyle = gradient;
this.ctx.fill();
}
}
Оценка: ⭐⭐⭐⭐ для кастомных эффектов (glow, particles)
6. WebGL / Three.js¶
Плюсы: - 3D эффекты - Высокая производительность для сложных сцен
Минусы: - Размер Three.js: 500+ KB — неприемлемо - Излишне сложно для 2D анимаций - Battery drain
Оценка: ⭐ — не рекомендуется для данного проекта
7. Framer Motion / GSAP¶
Framer Motion: - Размер: ~30 KB (gzipped) - React-only - Отличный DX
GSAP: - Размер: ~25 KB (core) - Framework-agnostic - Мощные easing и timeline
Пример GSAP:
import gsap from 'gsap';
gsap.to('.circle', {
scale: 1,
duration: 5,
ease: 'sine.inOut',
yoyo: true,
repeat: -1
});
Оценка: ⭐⭐⭐⭐ — хороший выбор, если нужна библиотека
Сводная таблица¶
| Технология | Bundle Size | Производительность | Сложность | Рекомендация |
|---|---|---|---|---|
| CSS Animations | 0 KB | ⭐⭐⭐⭐⭐ | Низкая | ✅ MVP |
| JS + CSS | ~2 KB | ⭐⭐⭐⭐⭐ | Средняя | ✅ MVP |
| SVG | ~1 KB | ⭐⭐⭐⭐⭐ | Средняя | ✅ MVP |
| Lottie | 150-300 KB | ⭐⭐⭐⭐ | Низкая | ⚠️ Позже |
| Canvas 2D | ~4 KB | ⭐⭐⭐⭐ | Высокая | ⚠️ Для эффектов |
| WebGL | 500+ KB | ⭐⭐⭐⭐⭐ | Очень высокая | ❌ Нет |
| GSAP | ~25 KB | ⭐⭐⭐⭐⭐ | Средняя | ⚠️ Опционально |
Рекомендация для CalmTrader¶
MVP Stack¶
┌─────────────────────────────────────────────┐
│ Animations │
├─────────────────────────────────────────────┤
│ CSS Transitions + Transforms │ ← Простые анимации
│ JavaScript requestAnimationFrame │ ← Timing контроль
│ Inline SVG │ ← Progress rings, paths
├─────────────────────────────────────────────┤
│ Telegram WebApp HapticFeedback API │ ← Haptic
│ CSS Custom Properties (variables) │ ← Theming
└─────────────────────────────────────────────┘
Total additional bundle: ~0 KB
Почему этот выбор¶
- Zero dependencies — минимальный bundle size
- Native performance — CSS transform/opacity на GPU
- Full control — JS управляет timing, CSS рендерит
- Compatibility — работает везде
- Maintainability — понятный код без абстракций
Архитектура анимаций¶
Структура файлов¶
apps/mini-app/
├── public/
│ └── index.html
├── src/
│ ├── index.ts # Entry point
│ ├── styles/
│ │ ├── base.css # Reset, variables
│ │ └── animations.css # Keyframes, transitions
│ ├── components/
│ │ ├── BreathingCircle.ts # Coherent, Extended Exhale
│ │ ├── BreathingSquare.ts # Box Breathing
│ │ ├── BreathingRings.ts # 4-7-8 Breathing
│ │ ├── PhaseLabel.ts # "Вдох" / "Выдох"
│ │ └── ProgressBar.ts # Session progress
│ ├── core/
│ │ ├── AnimationController.ts # Main orchestrator
│ │ ├── HapticController.ts # Telegram haptic
│ │ └── TimingEngine.ts # Phase timing
│ └── techniques/
│ ├── coherent.ts # 5-5 config
│ ├── box.ts # 4-4-4-4 config
│ ├── extended.ts # 4-8 config
│ ├── breathing478.ts # 4-7-8 config
│ └── physiological.ts # Double inhale config
└── package.json
Core Classes¶
// TimingEngine.ts
interface Phase {
name: 'inhale' | 'hold' | 'exhale' | 'holdOut';
duration: number; // ms
label: string;
haptic?: 'light' | 'medium' | 'heavy';
}
interface Technique {
id: string;
name: string;
phases: Phase[];
cycles: number;
}
class TimingEngine {
private technique: Technique;
private currentPhase: number = 0;
private currentCycle: number = 0;
private startTime: number = 0;
onPhaseChange?: (phase: Phase, progress: number) => void;
onCycleComplete?: (cycle: number) => void;
onSessionComplete?: () => void;
start() { /* ... */ }
pause() { /* ... */ }
resume() { /* ... */ }
stop() { /* ... */ }
}
// AnimationController.ts
class AnimationController {
private element: HTMLElement;
private timing: TimingEngine;
private haptic: HapticController;
constructor(element: HTMLElement, technique: Technique) {
this.timing = new TimingEngine(technique);
this.haptic = new HapticController();
this.timing.onPhaseChange = this.handlePhaseChange.bind(this);
}
private handlePhaseChange(phase: Phase, progress: number) {
// Update CSS custom property
this.element.style.setProperty('--progress', String(progress));
// Trigger haptic on phase start
if (progress === 0 && phase.haptic) {
this.haptic.impact(phase.haptic);
}
}
}
Примеры реализации¶
1. Coherent Breathing (Пульсирующий круг)¶
<div class="breathing-container">
<div class="breathing-circle" data-phase="inhale">
<div class="glow"></div>
</div>
<div class="phase-label">Вдох</div>
</div>
:root {
--circle-min-scale: 0.5;
--circle-max-scale: 1;
--inhale-duration: 5s;
--exhale-duration: 5s;
--accent-color: #4A9B9B;
--glow-color: #7FDBFF;
}
.breathing-circle {
width: 150px;
height: 150px;
border-radius: 50%;
background: radial-gradient(circle, var(--glow-color) 0%, var(--accent-color) 100%);
transform: scale(var(--circle-min-scale));
will-change: transform;
transition: transform var(--inhale-duration) cubic-bezier(0.4, 0, 0.6, 1);
}
.breathing-circle[data-phase="inhale"] {
transform: scale(var(--circle-max-scale));
}
.breathing-circle[data-phase="exhale"] {
transform: scale(var(--circle-min-scale));
transition-duration: var(--exhale-duration);
}
.glow {
position: absolute;
inset: -20px;
border-radius: 50%;
background: var(--accent-color);
filter: blur(30px);
opacity: 0.4;
}
// coherent.ts
const coherentBreathing: Technique = {
id: 'coherent',
name: 'Coherent Breathing',
phases: [
{ name: 'inhale', duration: 5000, label: 'Вдох', haptic: 'light' },
{ name: 'exhale', duration: 5000, label: 'Выдох', haptic: 'light' },
],
cycles: 6,
};
// Usage
const circle = document.querySelector('.breathing-circle');
const label = document.querySelector('.phase-label');
const controller = new AnimationController(circle, coherentBreathing);
controller.onPhaseChange = (phase) => {
circle.dataset.phase = phase.name;
label.textContent = phase.label;
};
controller.start();
2. Box Breathing (Квадрат с точкой)¶
<div class="box-container">
<svg viewBox="0 0 200 200" class="box-path">
<!-- Стороны квадрата -->
<path d="M 20,180 L 20,20" class="side left" />
<path d="M 20,20 L 180,20" class="side top" />
<path d="M 180,20 L 180,180" class="side right" />
<path d="M 180,180 L 20,180" class="side bottom" />
<!-- Углы -->
<circle cx="20" cy="180" r="4" class="corner" />
<circle cx="20" cy="20" r="4" class="corner" />
<circle cx="180" cy="20" r="4" class="corner" />
<circle cx="180" cy="180" r="4" class="corner" />
<!-- Движущаяся точка -->
<circle cx="20" cy="180" r="8" class="dot" />
</svg>
<div class="timer">4</div>
</div>
.box-path {
width: 200px;
height: 200px;
}
.side {
fill: none;
stroke: var(--text-secondary);
stroke-width: 3;
stroke-linecap: round;
opacity: 0.3;
transition: opacity 0.3s, stroke 0.3s;
}
.side.active {
stroke: var(--accent-color);
opacity: 1;
filter: drop-shadow(0 0 8px var(--accent-color));
}
.dot {
fill: var(--glow-color);
filter: drop-shadow(0 0 10px var(--glow-color));
transition: cx 4s linear, cy 4s linear;
}
.corner {
fill: var(--text-secondary);
opacity: 0.5;
}
// box.ts
const boxBreathing: Technique = {
id: 'box',
name: 'Box Breathing',
phases: [
{ name: 'inhale', duration: 4000, label: 'Вдох', haptic: 'light' },
{ name: 'hold', duration: 4000, label: 'Задержка' },
{ name: 'exhale', duration: 4000, label: 'Выдох', haptic: 'medium' },
{ name: 'holdOut', duration: 4000, label: 'Задержка' },
],
cycles: 4,
};
// Позиции точки для каждой фазы
const dotPositions = {
inhale: { start: { cx: 20, cy: 180 }, end: { cx: 20, cy: 20 } },
hold: { start: { cx: 20, cy: 20 }, end: { cx: 180, cy: 20 } },
exhale: { start: { cx: 180, cy: 20 }, end: { cx: 180, cy: 180 } },
holdOut: { start: { cx: 180, cy: 180 }, end: { cx: 20, cy: 180 } },
};
function updateDotPosition(phase: string, progress: number) {
const pos = dotPositions[phase];
const cx = pos.start.cx + (pos.end.cx - pos.start.cx) * progress;
const cy = pos.start.cy + (pos.end.cy - pos.start.cy) * progress;
dot.setAttribute('cx', String(cx));
dot.setAttribute('cy', String(cy));
}
3. 4-7-8 Breathing (Три кольца)¶
<svg viewBox="0 0 200 200" class="rings-478">
<!-- Background rings -->
<circle cx="100" cy="100" r="40" class="ring-bg" />
<circle cx="100" cy="100" r="60" class="ring-bg" />
<circle cx="100" cy="100" r="80" class="ring-bg" />
<!-- Progress rings -->
<circle cx="100" cy="100" r="40" class="ring inhale"
stroke-dasharray="251.2" stroke-dashoffset="251.2" />
<circle cx="100" cy="100" r="60" class="ring hold"
stroke-dasharray="376.8" stroke-dashoffset="376.8" />
<circle cx="100" cy="100" r="80" class="ring exhale"
stroke-dasharray="502.4" stroke-dashoffset="502.4" />
<!-- Center timer -->
<text x="100" y="108" class="timer">4</text>
</svg>
.ring-bg {
fill: none;
stroke: var(--bg-secondary);
stroke-width: 8;
}
.ring {
fill: none;
stroke-width: 8;
stroke-linecap: round;
transform: rotate(-90deg);
transform-origin: center;
transition: stroke-dashoffset linear;
}
.ring.inhale {
stroke: var(--inhale-color);
transition-duration: 4s;
}
.ring.hold {
stroke: var(--hold-color);
transition-duration: 7s;
}
.ring.exhale {
stroke: var(--exhale-color);
transition-duration: 8s;
}
.ring.active {
filter: drop-shadow(0 0 8px currentColor);
}
.ring.complete {
stroke-dashoffset: 0 !important;
}
// breathing478.ts
const breathing478: Technique = {
id: '478',
name: '4-7-8 Breathing',
phases: [
{ name: 'inhale', duration: 4000, label: 'Вдох', haptic: 'light' },
{ name: 'hold', duration: 7000, label: 'Задержка' },
{ name: 'exhale', duration: 8000, label: 'Выдох', haptic: 'medium' },
],
cycles: 4,
};
function updateRing(phase: string, progress: number) {
const ring = document.querySelector(`.ring.${phase}`) as SVGCircleElement;
const circumference = parseFloat(ring.getAttribute('stroke-dasharray')!);
const offset = circumference * (1 - progress);
ring.style.strokeDashoffset = String(offset);
ring.classList.toggle('active', progress > 0 && progress < 1);
ring.classList.toggle('complete', progress >= 1);
}
Производительность¶
GPU-ускоренные свойства¶
Только эти CSS свойства не вызывают reflow/repaint:
/* GPU-ускоренные (используйте их!) */
transform: scale() | translate() | rotate();
opacity: 0-1;
/* Избегайте анимировать */
width, height; /* Reflow */
margin, padding; /* Reflow */
background-color; /* Repaint */
box-shadow; /* Repaint (дорого!) */
Оптимизации¶
/* 1. will-change для анимируемых элементов */
.breathing-circle {
will-change: transform;
}
/* 2. contain для изоляции */
.animation-container {
contain: layout style paint;
}
/* 3. GPU layer для сложных элементов */
.glow-effect {
transform: translateZ(0); /* Force GPU layer */
}
Очистка ресурсов¶
class AnimationController {
private animationId: number = 0;
destroy() {
// Обязательно останавливаем requestAnimationFrame
cancelAnimationFrame(this.animationId);
// Удаляем event listeners
this.element.removeEventListener('transitionend', this.onTransitionEnd);
// Очищаем ссылки
this.element = null;
this.timing = null;
}
}
// При смене страницы/техники
window.addEventListener('beforeunload', () => {
controller.destroy();
});
Бенчмарки (целевые показатели)¶
| Метрика | Цель | Критично |
|---|---|---|
| FPS | 60 fps | < 30 fps |
| JS Execution | < 5ms/frame | > 16ms/frame |
| Memory | < 50 MB | > 100 MB |
| First Paint | < 500ms | > 1s |
| Time to Interactive | < 1s | > 2s |
Чеклист перед релизом¶
Производительность¶
- Анимации работают на 60 FPS
- Нет memory leaks (проверить DevTools → Memory)
- will-change применён к анимируемым элементам
- requestAnimationFrame очищается при unmount
Совместимость¶
- Тестирование на iOS (Safari WebView)
- Тестирование на Android (Chrome WebView)
- Тестирование на Telegram Desktop
- Тестирование на старых устройствах (iPhone 8, Android 8+)
UX¶
- Haptic feedback работает
- Анимации плавные при pause/resume
- Корректное отображение при смене темы Telegram
- Phase labels читаемы
Accessibility¶
-
prefers-reduced-motionобрабатывается - Достаточный контраст цветов
- Screen reader тексты для прогресса
/* Уважаем предпочтения пользователя */
@media (prefers-reduced-motion: reduce) {
.breathing-circle {
transition-duration: 0.01ms !important;
}
}
Итоговые рекомендации¶
Для MVP (Sprint 1)¶
- Coherent Breathing — CSS transitions + JS timing
- Box Breathing — SVG + CSS + JS
- Общие компоненты: PhaseLabel, ProgressBar, HapticController
Для v1.1¶
- Extended Exhale — вариация Coherent
- 4-7-8 Breathing — SVG rings
Для v1.2¶
- Physiological Sigh — сложная анимация с двойным кругом
- Canvas эффекты (glow, particles) — опционально
Не рекомендуется¶
- Lottie (размер)
- WebGL/Three.js (избыточно)
- Framer Motion без React
История изменений¶
| Версия | Дата | Изменения |
|---|---|---|
| 1.0 | 2024-12-03 | Первоначальная версия |
Документ подготовлен для задачи A4A-74