Векторная анимация через стили

Векторная анимация в OpenLayers основана на динамическом изменении визуального представления геометрий без обязательного пересоздания самих объектов данных. В основе лежит разделение модели (Feature и Geometry) и представления (Style), где стиль может вычисляться на каждом кадре отрисовки карты.

Ключевой механизм — повторный расчёт стилей через функции стиля, а также использование событий рендеринга карты и API перерисовки слоёв. Такой подход позволяет создавать анимации пульсации, движения маркеров, изменения толщины линий, эффекты свечения и имитацию потока данных вдоль маршрутов.

Основные источники анимации:

  • StyleFunction, возвращающая разные стили в зависимости от времени
  • изменение свойств Feature и вызов feature.changed()
  • обработка событий postrender у слоя или карты
  • использование requestAnimationFrame для синхронизации с циклом рендеринга
  • WebGL-рендеринг для высокопроизводительных сцен

StyleFunction как основа динамического рендеринга

Стиль в OpenLayers может быть функцией, вычисляющей визуальное состояние объекта при каждом рендере. Это фундамент для большинства анимаций.

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

const styleFunction = (feature, resolution) => {
  const time = Date.now() / 1000;

  const radius = 6 + Math.sin(time * 3) * 3;

  return new Style({
    image: new CircleStyle({
      radius: radius,
      fill: new Fill({
        color: 'rgba(30, 144, 255, 0.7)'
      })
    })
  });
};

При каждом перерисовывании карты функция вызывается заново, а параметры стиля пересчитываются динамически. Это позволяет реализовать:

  • пульсацию маркеров
  • мигание объектов
  • плавное изменение прозрачности
  • динамическое масштабирование символов

Управление анимацией через время и состояние

Для создания стабильных анимационных эффектов важно использовать единый временной источник, а не Date.now() в каждом вызове.

let start = performance.now();

const styleFunction = (feature) => {
  const elapsed = (performance.now() - start) / 1000;

  const scale = 1 + Math.sin(elapsed * 2) * 0.5;

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

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


Анимация геометрии через обновление Feature

Помимо изменения стиля, возможно прямое изменение координат геометрии. Это используется для имитации движения объектов по маршрутам.

function moveFeature(feature, delta) {
  const geometry = feature.getGeometry();
  const coordinates = geometry.getCoordinates();

  coordinates[0] += delta.x;
  coordinates[1] += delta.y;

  geometry.setCoordinates(coordinates);
}

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

function animate() {
  requestAnimationFrame(animate);

  vectorSource.getFeatures().forEach((f) => {
    moveFeature(f, { x: 0.5, y: 0.2 });
  });
}

animate();

Изменение геометрии автоматически вызывает перерасчёт слоя и перерисовку карты.


Использование событий postrender для кастомной анимации

Событие postrender позволяет рисовать поверх стандартного рендера, используя canvas контекст. Это особенно полезно для эффектов частиц, следов и сложной графики.

vectorLayer.on('postrender', function (event) {
  const ctx = event.context;
  const frameState = event.frameState;

  ctx.save();

  ctx.globalAlpha = 0.6;
  ctx.fillStyle = 'blue';

  const pixel = event.vectorContext.getPixelFromCoordinate([0, 0]);

  ctx.beginPath();
  ctx.arc(pixel[0], pixel[1], 10, 0, Math.PI * 2);
  ctx.fill();

  ctx.restore();

  frameState.animate = true;
});

Установка frameState.animate = true принудительно удерживает цикл рендеринга активным, что критично для непрерывной анимации.


Анимация линий и эффект «движущегося потока»

Часто применяется эффект движения вдоль линии маршрута. Он реализуется через смещение штриховки (stroke.dashOffset).

let offset = 0;

const lineStyle = new Style({
  stroke: new Stroke({
    color: '#3399CC',
    width: 3,
    lineDash: [10, 10],
    lineDashOffset: offset
  })
});

function animateLine() {
  offset -= 1;
  lineStyle.getStroke().setLineDashOffset(offset);
  vectorSource.changed();
  requestAnimationFrame(animateLine);
}

animateLine();

Такой подход создаёт иллюзию движения по линии без изменения геометрии.


Пульсация и «радиоволны» вокруг точек

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

const pulseStyle = (feature) => {
  const t = (performance.now() % 2000) / 2000;

  return new Style({
    image: new CircleStyle({
      radius: 10 + t * 20,
      stroke: new Stroke({
        color: `rgba(0, 150, 255, ${1 - t})`,
        width: 2
      })
    })
  });
};

Комбинация изменения радиуса и альфа-канала создаёт эффект расширяющейся волны.


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

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

const clock = {
  start: performance.now(),
  getTime() {
    return (performance.now() - this.start) / 1000;
  }
};

const styleFunction = () => {
  const t = clock.getTime();

  return new Style({
    image: new CircleStyle({
      radius: 5 + Math.sin(t * 5) * 2
    })
  });
};

Такой подход позволяет:

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

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

Производительность напрямую зависит от количества перерасчётов стилей и перерисовок слоя.

Ключевые приёмы оптимизации:

  • минимизация логики внутри StyleFunction
  • кэширование стилей для одинаковых состояний
  • ограничение количества feature.changed() вызовов
  • использование VectorImageLayer для упрощённого рендеринга
  • переход на WebGL-слои при большом количестве объектов

Пример кэширования:

const cache = new Map();

function getStyle(size) {
  if (cache.has(size)) return cache.get(size);

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

  cache.set(size, style);
  return style;
}

WebGL-анимация векторных данных

Для больших наборов точек используется WebGL-рендеринг, позволяющий переносить вычисления на GPU. В OpenLayers это реализуется через специализированные слои.

Анимация в таком случае часто задаётся через uniform-параметры времени:

const layer = new WebGLPointsLayer({
  style: {
    symbol: {
      symbolType: 'circle',
      size: ['+', 5, ['*', 3, ['sin', ['*', 2, ['time']]]]],
      color: 'rgba(255, 0, 0, 0.6)'
    }
  }
});

Здесь time обновляется автоматически движком рендеринга, что позволяет строить плавные GPU-эффекты без JavaScript-циклов.


Комбинирование геометрической и стилевой анимации

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

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

Это позволяет разделить вычислительную нагрузку и визуальную сложность.

feature.getGeometry().translate(1, 0);

feature.setStyle((feature) => {
  const speed = feature.get('speed');

  return new Style({
    image: new CircleStyle({
      radius: 4 + speed
    })
  });
});

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


Управление циклом рендеринга

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

function keepAlive() {
  map.render();
  requestAnimationFrame(keepAlive);
}

keepAlive();

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