Покадровая анимация

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

Ключевой особенностью архитектуры является декларативный подход: визуализация описывается через слои (layers), а изменение сцены достигается обновлением свойств, приводящим к повторному вычислению геометрии и шейдерных атрибутов.

Цикл рендеринга и requestAnimationFrame

Базовый механизм анимации строится вокруг requestAnimationFrame, обеспечивающего синхронизацию с частотой обновления экрана.

Каждый кадр включает последовательность операций:

  • вычисление нового состояния времени;
  • обновление параметров слоёв;
  • пересчёт атрибутов при необходимости;
  • рендер сцены WebGL.

Типовой цикл:

let t0 = performance.now();

function animate() {
  const t = performance.now();
  const time = (t - t0) / 1000;

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

  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

Важным аспектом является минимизация перерасчётов: обновление слоёв должно происходить только при изменении значимых параметров.

Управление временной осью

Покадровая анимация часто опирается на явную временную переменную. В Deck.gl она не встроена как глобальная сущность, но используется как пользовательское состояние.

Распространённая модель — нормализованное время:

const speed = 0.01;
let time = 0;

function animate() {
  time += speed;

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

  requestAnimationFrame(animate);
}

Нормализация времени позволяет создавать циклические анимации без переполнения значений.

В более сложных сценариях используется синхронизация с реальными данными (например, GPS-треками или временными рядами), где время становится ключом выборки.

Анимация через изменение viewState

Одним из распространённых типов покадровой анимации является движение камеры. В Deck.gl камера управляется объектом viewState.

Изменение позиции, масштаба и угла наклона выполняется на каждом кадре:

function animate() {
  const bearing = (performance.now() * 0.01) % 360;

  deck.setProps({
    viewState: {
      longitude: 37.6,
      latitude: 55.7,
      zoom: 10,
      bearing,
      pitch: 45
    }
  });

  requestAnimationFrame(animate);
}

Такой подход используется для вращения сцены, плавного зума или следования за объектом.

Пересчёт атрибутов слоёв

Deck.gl оптимизирует работу через систему атрибутов WebGL. Каждый слой имеет набор accessor-функций (getPosition, getColor, getWidth), которые пересчитываются только при изменении зависимостей.

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

new ScatterplotLayer({
  id: 'points',
  data,
  getPosition: d => [d.x, d.y],
  getRadius: d => d.size * Math.sin(time),
  updateTriggers: {
    getRadius: time
  }
});

updateTriggers выступает механизмом контроля пересчёта: изменение времени инициирует перерасчёт только конкретного атрибута, а не всего слоя.

Интерполяция состояний (lerp)

Для обеспечения плавности анимации используется линейная интерполяция между состояниями.

Формально:

x(t) = x_0 + (x_1 - x_0)t

где параметр t изменяется от 0 до 1 между кадрами.

Практическая реализация:

function lerp(a, b, t) {
  return a + (b - a) * t;
}

function interpolatePosition(p0, p1, t) {
  return [
    lerp(p0[0], p1[0], t),
    lerp(p0[1], p1[1], t)
  ];
}

Интерполяция применяется для координат, цветов, радиусов и параметров камеры.

Анимация траекторий через PathLayer

PathLayer используется для отображения линий, где анимация строится на постепенном «прорисовывании» пути.

Принцип заключается в использовании временного среза массива координат:

new PathLayer({
  id: 'path',
  data,
  getPath: d => d.path,
  getColor: [255, 0, 0],
  widthMinPixels: 2,
  getDashArray: [10, 5]
});

Для покадровой анимации путь ограничивается по индексу:

function getAnimatedPath(path, t) {
  const len = Math.floor(path.length * t);
  return path.slice(0, len);
}

Такой подход создаёт эффект «рисования» маршрута во времени.

TripsLayer и временные треки

TripsLayer специально предназначен для временной анимации перемещений объектов. Он интерпретирует третью координату как время.

new TripsLayer({
  id: 'trips',
  data,
  getPath: d => d.path,
  getTimestamps: d => d.timestamps,
  currentTime: time,
  trailLength: 200
});

Каждый кадр изменяет currentTime, создавая эффект движения объектов вдоль траектории.

Ключевой особенностью является GPU-ускоренная интерполяция внутри шейдеров, что значительно снижает нагрузку на CPU.

Custom Layer и ручная анимация

При необходимости низкоуровневого контроля используется Layer с переопределением жизненного цикла:

  • initializeState
  • updateState
  • draw

Пример структуры анимируемого слоя:

class AnimatedLayer extends DeckGL.Layer {
  initializeState() {
    const { gl } = this.context;
  }

  updateState({ props, oldProps }) {
    if (props.time !== oldProps.time) {
      this.setNeedsUpdate();
    }
  }

  draw({ uniforms }) {
    const { time } = this.props;

    this.state.model.setUniforms({
      u_time: time
    });

    this.state.model.draw();
  }
}

Шейдеры GLSL становятся основным инструментом покадровой анимации:

uniform float u_time;

void main() {
  float offset = sin(u_time + position.x);
  gl_Position = project_position(vec3(position.xy, offset));
}

Управление производительностью

Покадровая анимация в WebGL чувствительна к стоимости пересчётов и перерисовки. Основные оптимизационные принципы:

  • минимизация пересоздания слоёв;
  • использование updateTriggers вместо полного обновления props;
  • перенос вычислений в шейдеры;
  • использование бинарных форматов данных (TypedArray);
  • ограничение частоты обновления времени (throttling).

Пример ограничения частоты:

let last = 0;

function animate(t) {
  if (t - last > 16) {
    last = t;

    deck.setProps({
      layers: createLayers(t)
    });
  }

  requestAnimationFrame(animate);
}

Синхронизация нескольких слоёв

В сложных сценах несколько слоёв должны анимироваться синхронно. Для этого используется единый источник времени:

function renderFrame(time) {
  const t = time * 0.001;

  deck.setProps({
    layers: [
      layerA(t),
      layerB(t),
      layerC(t)
    ]
  });

  requestAnimationFrame(renderFrame);
}

Согласованность времени предотвращает десинхронизацию визуальных эффектов, особенно при комбинировании TripsLayer, ScatterplotLayer и ArcLayer.

Анимация на основе данных

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

Пример структуры данных:

{
  "time": [0, 1, 2, 3],
  "value": [10, 15, 12, 20]
}

Отображение через фильтрацию:

function getFrame(data, t) {
  return data.filter(d => d.time <= t);
}

Такой подход применяется для визуализации потоков, транспорта и изменений плотности данных.

Детерминированные анимации слоёв

При строгой необходимости воспроизводимости используется детерминированное обновление состояния:

  • фиксированный шаг времени;
  • отсутствие зависимости от реального времени;
  • вычисление состояния как функции от кадра.
const frame = 0;

function animate() {
  const t = frame * 0.016;
  frame++;

  deck.setProps({
    layers: createLayers(t)
  });

  requestAnimationFrame(animate);
}

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