Перейти к содержанию

Техническая спецификация анимаций для Telegram Mini App

Оценка технологий и рекомендации по реализации дыхательных анимаций

Содержание

  1. Ограничения Telegram Mini App
  2. Сравнение технологий
  3. Рекомендация для CalmTrader
  4. Архитектура анимаций
  5. Примеры реализации
  6. Производительность
  7. Чеклист перед релизом

Ограничения 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

Почему этот выбор

  1. Zero dependencies — минимальный bundle size
  2. Native performance — CSS transform/opacity на GPU
  3. Full control — JS управляет timing, CSS рендерит
  4. Compatibility — работает везде
  5. 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)

  1. Coherent Breathing — CSS transitions + JS timing
  2. Box Breathing — SVG + CSS + JS
  3. Общие компоненты: PhaseLabel, ProgressBar, HapticController

Для v1.1

  1. Extended Exhale — вариация Coherent
  2. 4-7-8 Breathing — SVG rings

Для v1.2

  1. Physiological Sigh — сложная анимация с двойным кругом
  2. Canvas эффекты (glow, particles) — опционально

Не рекомендуется

  • Lottie (размер)
  • WebGL/Three.js (избыточно)
  • Framer Motion без React

История изменений

Версия Дата Изменения
1.0 2024-12-03 Первоначальная версия

Документ подготовлен для задачи A4A-74