Анимация векторных объектов

Анимация векторных объектов в OpenLayers опирается на механизм перерисовки слоёв и динамическое изменение геометрии или стиля объектов. Векторный слой (Vector Layer) в библиотеке не является статичным: каждый кадр может пересчитываться визуальное представление features, что позволяет реализовать движение, морфинг форм, пульсации и визуальные эффекты высокой сложности.


В основе анимации лежит цикл отрисовки карты. Карта вызывает перерисовку слоя в следующих случаях:

  • изменение геометрии feature
  • изменение стиля feature или слоя
  • изменение состояния источника данных
  • принудительный вызов render()
  • внутренний animation frame loop

Ключевой момент: OpenLayers не анимирует объекты автоматически — разработчик управляет состоянием объектов, а библиотека лишь отображает изменения.


Обновление геометрии как базовый механизм анимации

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

Пример: движение точки по таймеру

import Feature from 'ol/Feature.js';
import Point from 'ol/geom/Point.js';

const feature = new Feature({
  geometry: new Point([0, 0])
});

let x = 0;

function animate() {
  x += 0.01;

  const geometry = feature.getGeometry();
  geometry.setCoordinates([x, Math.sin(x)]);

  requestAnimationFrame(animate);
}

animate();

Особенности подхода

  • изменение координат вызывает автоматическую перерисовку слоя
  • подходит для точек, линий и полигонов
  • требует контроля частоты обновления

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

Основной механизм синхронизации анимации — requestAnimationFrame, который:

  • синхронизирован с частотой обновления экрана
  • снижает нагрузку по сравнению с setInterval
  • обеспечивает плавность движения

Типичный цикл:

function loop(timestamp) {
  updateState(timestamp);
  requestAnimationFrame(loop);
}

requestAnimationFrame(loop);

Интерполяция координат и плавность движения

Резкое изменение координат приводит к «дёрганому» движению. Для сглаживания используется интерполяция.

Линейная интерполяция

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

Применение к координатам

const start = [0, 0];
const end = [10, 10];

function animate(t) {
  const k = Math.min(t / 1000, 1);

  const coord = [
    lerp(start[0], end[0], k),
    lerp(start[1], end[1], k)
  ];

  feature.getGeometry().setCoordinates(coord);

  requestAnimationFrame((ts) => animate(ts));
}

Анимация через стиль (Style Function)

Изменение геометрии — не единственный подход. Более лёгкий способ для визуальных эффектов — динамический стиль.

Пример пульсирующей точки

import Style from 'ol/style/Style.js';
import CircleStyle from 'ol/style/Circle.js';
import Fill from 'ol/style/Fill.js';

let t = 0;

const style = new Style({
  image: new CircleStyle({
    radius: 10,
    fill: new Fill({ color: 'rgba(255,0,0,0.5)' })
  })
});

feature.setStyle(() => {
  const radius = 10 + Math.sin(t) * 5;

  t += 0.1;

  return new Style({
    image: new CircleStyle({
      radius,
      fill: new Fill({ color: 'rgba(255,0,0,0.5)' })
    })
  });
});

Характеристика метода

  • не требует изменения геометрии
  • подходит для эффектов (пульсация, мигание, подсветка)
  • менее затратен для CPU

Анимация маршрутов (line animation)

Частый кейс — «рисование» линии по мере продвижения объекта.

Подход через подмножество координат

const fullRoute = [
  [0, 0],
  [5, 5],
  [10, 10]
];

let index = 0;

const lineFeature = new Feature({
  geometry: new LineString([])
});

function step() {
  index++;

  lineFeature.getGeometry().setCoordinates(
    fullRoute.slice(0, index)
  );

  if (index < fullRoute.length) {
    requestAnimationFrame(step);
  }
}

step();

Плавное движение объектов по маршруту

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

Принцип

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

Анимация вращения объектов

Геометрия может оставаться неизменной, но стиль — вращаться.

import Icon from 'ol/style/Icon.js';

let angle = 0;

feature.setStyle(() => {
  angle += 0.05;

  return new Style({
    image: new Icon({
      src: 'arrow.png',
      rotation: angle
    })
  });
});

Применение:

  • направление транспорта
  • индикация движения ветра
  • визуализация потоков

Пульсация и эффекты внимания

Эффект пульса достигается изменением радиуса или прозрачности.

Через прозрачность

let alpha = 1;
let direction = -0.05;

feature.setStyle(() => {
  alpha += direction;

  if (alpha < 0.3 || alpha > 1) {
    direction *= -1;
  }

  return new Style({
    image: new CircleStyle({
      radius: 8,
      fill: new Fill({
        color: `rgba(0, 150, 255, ${alpha})`
      })
    })
  });
});

Оптимизация анимации большого количества объектов

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

Рекомендации архитектуры

  • минимизировать создание новых Style объектов
  • переиспользовать геометрии
  • группировать обновления в один цикл
  • избегать тяжёлых вычислений внутри style function

Использование общего таймера

let globalTime = 0;

function tick() {
  globalTime += 1;

  features.forEach(f => {
    const geom = f.getGeometry();
    // обновление состояния
  });

  requestAnimationFrame(tick);
}

Координатные преобразования в анимации

При работе с картой важно учитывать проекции.

  • данные часто в EPSG:4326
  • отображение в EPSG:3857

Используется:

import { fromLonLat } from 'ol/proj.js';

const coord = fromLonLat([37.6173, 55.7558]);

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


Временные функции и easing

Для реалистичных движений применяются easing-функции.

Пример ease-in-out

function easeInOut(t) {
  return t < 0.5
    ? 2 * t * t
    : -1 + (4 - 2 * t) * t;
}

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

  • ускорение в начале
  • замедление в конце

Морфинг геометрии

Более сложный тип анимации — трансформация формы.

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

function morph(a, b, t) {
  return a.map((coord, i) => [
    lerp(coord[0], b[i][0], t),
    lerp(coord[1], b[i][1], t)
  ]);
}

Синхронизация с рендер-циклом карты

Вместо самостоятельного requestAnimationFrame можно использовать:

map.on('postrender', function () {
  // обновление состояния
  feature.changed();
});

Это позволяет синхронизировать анимацию с отрисовкой карты и снижает рассинхронизацию кадров.


Частицы и псевдо-анимация потока

Для визуализации потоков используются точки, движущиеся вдоль линий.

Принцип

  • создаётся массив частиц
  • каждая имеет позицию вдоль линии (0..1)
  • на каждом кадре позиция увеличивается
particles.forEach(p => {
  p.t += 0.01;
  p.coord = line.getCoordinateAt(p.t);
});

Итоговые паттерны анимации

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

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

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