Синхронизация анимаций

Базовая модель анимации в Deck.gl

В основе анимационной модели Deck.gl лежит принцип реактивного обновления состояния: визуализация пересчитывается при изменении входных данных — viewState, свойств слоёв или глобального времени анимации. Ключевой особенностью является разделение ответственности между:

  • состоянием камеры (viewState)
  • состоянием слоёв (layers)
  • временной осью (анимационный тик или таймлайн)
  • WebGL-рендерингом

Каждое изменение приводит к повторному вызову рендера через внутренний цикл Deck или компонент <DeckGL />, что позволяет строить как дискретные, так и непрерывные анимации.


Единый источник времени

Синхронизация анимаций в сложных сценах требует введения общего временного контекста. Без него различные слои начинают «жить своей жизнью», что приводит к рассинхронизации визуальных эффектов.

На практике используется один из подходов:

  • глобальный таймер на основе performance.now()
  • инкрементируемый deltaTime в цикле рендера
  • внешняя система времени (например, игровой движок или поток данных)

Ключевая идея — все анимационные вычисления должны зависеть от одного источника:

const t = performance.now();

или нормализованного времени:

const time = (performance.now() - startTime) / 1000;

requestAnimationFrame и animation loop

Основной механизм синхронизации кадров — requestAnimationFrame. Deck.gl предоставляет встроенную поддержку анимационного цикла через параметр animationLoop.

В низкоуровневом варианте цикл выглядит следующим образом:

function loop() {
  const now = performance.now();

  deck.setProps({
    time: now
  });

  requestAnimationFrame(loop);
}

requestAnimationFrame(loop);

Внутри Deck.gl этот подход используется для триггера повторного рендера без необходимости ручного управления сценой.

Важно учитывать:

  • частота кадров зависит от браузера и GPU
  • requestAnimationFrame синхронизирован с refresh rate дисплея
  • любые тяжёлые вычисления внутри цикла напрямую влияют на плавность

Синхронизация переходов viewState

Одна из самых частых задач — синхронизация анимации камеры с другими визуальными эффектами.

Deck.gl использует систему переходов (transitions), основанную на интерполяторах:

  • линейная интерполяция
  • сглаженные easing-функции
  • специализированные интерполяторы (например, полёт камеры)

Пример синхронизации движения камеры:

import {FlyToInterpolator} from '@deck.gl/core';

const viewState = {
  longitude: 30,
  latitude: 50,
  zoom: 10,
  transitionDuration: 2000,
  transitionInterpolator: new FlyToInterpolator()
};

Для синхронизации с другими анимациями важно учитывать:

  • начало перехода (t0)
  • длительность transitionDuration
  • единый clock для слоёв и камеры

Распространённая практика — запуск всех анимаций в одном кадре:

const t0 = performance.now();

deck.setProps({
  viewState: {
    ...viewState,
    transitionDuration: 2000
  },
  animationStart: t0
});

Координация нескольких слоёв

Сложные сцены включают десятки слоёв: линии, точки, extruded polygons, heatmap и кастомные WebGL-слои. Без централизованного времени каждый слой может обновляться независимо.

Подход к синхронизации:

  1. ввод общего параметра time
  2. передача его во все слои
  3. использование одного и того же преобразования времени внутри getColor, getPosition, updateTriggers

Пример:

const layers = [
  new ScatterplotLayer({
    id: 'points',
    data,
    getPosition: d => d.position,
    getRadius: d => 100 * Math.sin(time * 0.001 + d.phase)
  }),

  new LineLayer({
    id: 'lines',
    data,
    getSourcePosition: d => d.source,
    getTargetPosition: d => d.target,
    getWidth: d => 2 + Math.cos(time * 0.001)
  })
];

Ключевой момент — одинаковая шкала времени обеспечивает фазовую согласованность эффектов.


Шейдерная синхронизация времени

Максимальная производительность достигается, когда анимация переносится в GPU через шейдеры. В этом случае синхронизация становится задачей передачи единого uniform-параметра.

Deck.gl автоматически прокидывает time в кастомные шейдеры через uniforms:

const layer = new ScatterplotLayer({
  id: 'animated-points',
  data,
  getPosition: d => d.position,

  fp64: false,

  // кастомный шейдерный параметр
  getShaders: () => ({
    inject: {
      'vs:#decl': 'uniform float time;',
      'vs:#main-end': `
        gl_PointSize *= (1.0 + sin(time * 0.001) * 0.5);
      `
    }
  }),

  updateTriggers: {
    time
  }
});

Синхронизация достигается тем, что все GPU-эффекты получают один и тот же uniform time.


Практика: общий таймер сцены

Для сложных визуализаций вводится централизованный контроллер времени:

class TimeController {
  constructor() {
    this.start = performance.now();
    this.speed = 1.0;
  }

  now() {
    return (performance.now() - this.start) * this.speed;
  }

  setSpeed(s) {
    this.speed = s;
  }
}

const clock = new TimeController();

Использование:

function animate() {
  const time = clock.now();

  deck.setProps({
    time,
    layers: createLayers(time)
  });

  requestAnimationFrame(animate);
}

animate();

Такой подход обеспечивает:

  • синхронное ускорение/замедление всей сцены
  • возможность паузы без разрушения состояния
  • согласованность CPU и GPU анимаций

Плавные переходы и easing

Несогласованность анимаций часто возникает из-за различий в функциях сглаживания. Deck.gl использует easing как отдельный слой логики над интерполяцией.

Типичные функции:

  • linear
  • cubic
  • sine
  • exponential

При синхронизации важно использовать одинаковый easing для всех компонентов сцены:

transitionInterpolator: new FlyToInterpolator(),
transitionEasing: t => 1 - Math.pow(1 - t, 3)

Если камера использует cubic easing, а слои — linear, визуальная сцена распадается на несвязанные элементы движения.


Типичные проблемы рассинхронизации

На практике рассинхронизация возникает не из-за Deck.gl как такового, а из-за архитектуры приложения.

Основные источники проблем:

  • использование разных источников времени (Date.now() и performance.now())
  • независимые requestAnimationFrame циклы
  • обновление части слоёв вне основного рендера
  • различие частоты обновления props в React-обёртке
  • вычисление времени внутри слоя вместо передачи сверху

Особенно критична ситуация, когда:

  • камера анимируется через transition
  • слои обновляются через отдельный state loop
  • шейдеры используют локальный time

В результате сцена распадается на несогласованные движения.


Централизованная модель синхронизации

Наиболее устойчивая архитектура строится вокруг единого источника состояния:

  • один animation loop
  • один time value
  • единый update pipeline
  • детерминированная генерация layers

Схематически:

requestAnimationFrame
        ↓
   global time
        ↓
  create layers
        ↓
   deck.setProps
        ↓
      render

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