Синхронизация с внешними медиа-элементами

Синхронизация Lottie-анимации с внешними медиа-элементами требует понимания того, что Lottie Web не работает как самостоятельная временная шкала мультимедиа в классическом смысле. Библиотека оперирует кадровой моделью, где ключевым параметром выступает текущий кадр анимации, а управление осуществляется через методы программного API.


Базовая модель управления временем в Lottie Web

В основе управления анимацией лежит кадровая система:

  • анимация разбивается на фиксированное количество кадров
  • текущее состояние определяется значением текущего кадра
  • проигрывание может быть линейным или сегментированным

Ключевые элементы управления:

  • goToAndStop(frame, isFrame)
  • play()
  • pause()
  • stop()
  • playSegments(segments, forceFlag)

Дополнительно используется внутренний механизм:

  • animation.currentFrame
  • animation.totalFrames
  • animation.frameRate

Эти значения позволяют построить внешнюю временную шкалу, синхронизированную с аудио или видео.


Синхронизация с HTMLVideoElement

Видео является наиболее прямым источником временной базы благодаря свойству currentTime.

Базовое отображение времени видео в кадры Lottie

Пусть:

  • video.currentTime — время видео в секундах
  • duration — длительность видео
  • totalFrames — общее количество кадров Lottie

Тогда привязка кадра:

function syncVideoToLottie(video, animation) {
  const frame = (video.currentTime / video.duration) * animation.totalFrames;
  animation.goToAndStop(frame, true);
}

Постоянная синхронизация через requestAnimationFrame

function startSync(video, animation) {
  function tick() {
    if (!video.paused && !video.ended) {
      const frame = (video.currentTime / video.duration) * animation.totalFrames;
      animation.goToAndStop(frame, true);
    }
    requestAnimationFrame(tick);
  }
  tick();
}

Компенсация рассинхронизации

В реальных условиях возникает дрейф из-за:

  • пропуска кадров
  • различий в тайминге браузера
  • буферизации видео

Добавляется порог коррекции:

const EPSILON = 0.5;

function sync(video, animation) {
  const targetFrame = (video.currentTime / video.duration) * animation.totalFrames;
  const delta = Math.abs(animation.currentFrame - targetFrame);

  if (delta > EPSILON) {
    animation.goToAndStop(targetFrame, true);
  }
}

Синхронизация с аудио через Web Audio API

Аудио предоставляет более точную временную шкалу при использовании AudioContext.

Использование AudioContext.currentTime

const audioContext = new AudioContext();
const audio = new Audio("track.mp3");
const source = audioContext.createMediaElementSource(audio);
source.connect(audioContext.destination);

Привязка времени к кадрам

function syncAudio(animation, startTime) {
  const currentTime = audioContext.currentTime - startTime;
  const frame = currentTime * animation.frameRate;
  animation.goToAndStop(frame, true);
}

Запуск с фиксированной точкой отсчёта

let startTime = null;

audio.addEventListener("play", () => {
  startTime = audioContext.currentTime;
  requestAnimationFrame(update);
});

function update() {
  if (!audio.paused) {
    const t = audioContext.currentTime - startTime;
    const frame = t * animation.frameRate;
    animation.goToAndStop(frame, true);
    requestAnimationFrame(update);
  }
}

Привязка к произвольной временной шкале

Часто требуется независимая временная система, например:

  • скролл страницы
  • drag-события
  • game loop
  • кастомные таймлайны

Общая функция маппинга

function map(value, inMin, inMax, outMin, outMax) {
  return outMin + (value - inMin) * (outMax - outMin) / (inMax - inMin);
}

Синхронизация со скроллом

window.addEventListener("scroll", () => {
  const scrollTop = window.scrollY;
  const docHeight = document.body.scrollHeight - window.innerHeight;

  const progress = scrollTop / docHeight;
  const frame = progress * animation.totalFrames;

  animation.goToAndStop(frame, true);
});

Сегментированная синхронизация

Lottie поддерживает работу с сегментами анимации, что важно при синхронизации отдельных фаз.

animation.playSegments([0, 30], true);

Привязка сегментов к внешним событиям

function syncState(state) {
  switch (state) {
    case "intro":
      animation.playSegments([0, 45], true);
      break;
    case "loop":
      animation.playSegments([45, 120], true);
      break;
    case "outro":
      animation.playSegments([120, 180], true);
      break;
  }
}

Синхронизация нескольких Lottie-анимаций

При наличии нескольких слоёв анимации важно использовать единый источник времени.

function syncAll(masterFrame, animations) {
  animations.forEach(anim => {
    const frame = (masterFrame / 100) * anim.totalFrames;
    anim.goToAndStop(frame, true);
  });
}

Использование GSAP как внешнего таймлайна

GSAP может выступать центральным контроллером времени.

const tl = gsap.timeline({ paused: true });

tl.to({}, {
  duration: 5,
  onUpdate: () => {
    const progress = tl.progress();
    const frame = progress * animation.totalFrames;
    animation.goToAndStop(frame, true);
  }
});

Управление скоростью и нелинейными временными функциями

Линейное соответствие времени часто недостаточно. Применяются easing-функции:

function easeInOut(t) {
  return t < 0.5
    ? 2 * t * t
    : -1 + (4 - 2 * t) * t;
}
const progress = easeInOut(rawProgress);
const frame = progress * animation.totalFrames;

Обработка зацикливания и петлевых синхронизаций

При loop-анимации возникает проблема скачков фазы.

function loopSync(time, duration, totalFrames) {
  const mod = time % duration;
  return (mod / duration) * totalFrames;
}

Коррекция дрейфа при длительном воспроизведении

При длительных сессиях синхронизации необходимо учитывать накопление ошибки.

Подход с периодической реалигнацией:

let lastCorrection = 0;
const CORRECTION_INTERVAL = 2000;

function correctedSync(time, animation) {
  if (time - lastCorrection > CORRECTION_INTERVAL) {
    animation.goToAndStop(time * animation.frameRate, true);
    lastCorrection = time;
  }
}

Синхронизация с пользовательскими событиями DOM

Lottie может быть привязана к интерактивным состояниям интерфейса.

button.addEventListener("mouseenter", () => {
  animation.playSegments([0, 20], true);
});

button.addEventListener("mouseleave", () => {
  animation.playSegments([20, 40], true);
});

Использование нормализованного прогресса

Единый подход к различным источникам данных заключается в использовании нормализованного значения progress:

  • диапазон 0..1
  • независимость от источника времени
function applyProgress(animation, progress) {
  const frame = progress * animation.totalFrames;
  animation.goToAndStop(frame, true);
}

Приоритизация источников синхронизации

При комбинировании нескольких источников требуется система приоритетов:

  1. пользовательский input (drag, scroll)
  2. видео/аудио
  3. системный таймер
  4. fallback requestAnimationFrame
function resolveFrame(sources) {
  return sources.user ??
         sources.media ??
         sources.timer ??
         sources.fallback;
}

Оптимизация частоты обновлений

Частое обновление кадров может приводить к лишней нагрузке.

Используются техники:

  • throttling через requestAnimationFrame
  • обновление только при изменении кадра
  • кэширование вычисленного frame
let lastFrame = -1;

function update(frame) {
  const rounded = Math.round(frame);
  if (rounded !== lastFrame) {
    animation.goToAndStop(rounded, true);
    lastFrame = rounded;
  }
}