Рисование на canvas

Mapbox GL JS опирается на WebGL как основной механизм отрисовки, однако поверх него предусмотрен слой абстракции для работы с 2D-canvas через пользовательские слои. Это позволяет интегрировать произвольную графику, генерируемую через HTMLCanvasElement, в систему координат карты, синхронизируя её с масштабированием, вращением и перемещением сцены.

Внутри рендеринга карты существует несколько стадий:

  • вычисление матрицы камеры (zoom, pitch, bearing)
  • преобразование географических координат в экранные (mercator projection)
  • отрисовка базовых слоёв WebGL
  • наложение пользовательских слоёв (custom layers)
  • финальная композиция кадра

Canvas-интеграция реализуется через интерфейс CustomLayerInterface, который предоставляет жизненный цикл слоя:

  • onAdd
  • render
  • onRemove

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

Базовая структура canvas custom layer

Canvas-слой создаётся как объект, реализующий интерфейс пользовательского слоя:

const canvasLayer = {
    id: 'custom-canvas-layer',
    type: 'custom',
    renderingMode: '2d',

    onAdd: function (map, gl) {
        this.map = map;

        this.canvas = document.createElement('canvas');
        this.canvas.width = map.getCanvas().width;
        this.canvas.height = map.getCanvas().height;

        this.context = this.canvas.getContext('2d');
    },

    render: function (gl, matrix) {
        const ctx = this.context;

        ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

        ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
        ctx.beginPath();
        ctx.arc(200, 200, 50, 0, Math.PI * 2);
        ctx.fill();

        this.map.triggerRepaint();
    }
};

В данном примере canvas существует как независимый буфер, который затем может быть композитно наложен поверх карты.

Связь canvas с географическими координатами

Основная сложность интеграции заключается в необходимости преобразования координат из географической системы (longitude/latitude) в пиксельные координаты.

Mapbox GL JS предоставляет методы:

  • map.project(lngLat) — перевод географических координат в экранные
  • map.unproject(point) — обратное преобразование

Пример отрисовки географической точки на canvas

render: function (gl) {
    const ctx = this.context;

    ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

    const lngLat = [69.589, 42.315]; // координаты
    const point = this.map.project(lngLat);

    ctx.fillStyle = 'blue';
    ctx.beginPath();
    ctx.arc(point.x, point.y, 8, 0, Math.PI * 2);
    ctx.fill();

    this.map.triggerRepaint();
}

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

Синхронизация размеров canvas с картой

При изменении размеров контейнера карты необходимо синхронизировать canvas:

onAdd: function (map) {
    this.map = map;

    this.canvas = document.createElement('canvas');
    this.context = this.canvas.getContext('2d');

    this.resize();
},

resize: function () {
    const canvas = this.map.getCanvas();

    this.canvas.width = canvas.width;
    this.canvas.height = canvas.height;
}

Без такой синхронизации графика будет искажаться при изменении масштаба окна.

Использование матрицы трансформации

В ряде случаев требуется не только позиционирование точек, но и работа с матрицей камеры, передаваемой в render(matrix).

Матрица представляет собой 4×4 transformation matrix, используемую WebGL для преобразования координат мира в clip space.

При использовании 2D canvas обычно применяется обходной путь: вычисление координат через map.project, однако для высокой плотности данных возможно использование матричного умножения.

Отрисовка динамических объектов

Canvas-слой часто используется для динамических визуализаций:

  • потоковые данные (GPS треки)
  • тепловые карты (heatmaps)
  • анимации частиц
  • пользовательские геометрические фигуры

Пример простого анимационного слоя:

const layer = {
    id: 'particles',
    type: 'custom',
    renderingMode: '2d',

    onAdd: function (map) {
        this.map = map;
        this.canvas = document.createElement('canvas');
        this.ctx = this.canvas.getContext('2d');

        this.particles = Array.from({ length: 1000 }, () => ({
            lng: 69 + Math.random(),
            lat: 42 + Math.random(),
            speed: Math.random() * 0.01
        }));
    },

    render: function () {
        const ctx = this.ctx;

        ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

        for (let p of this.particles) {
            p.lat += p.speed;

            const point = this.map.project([p.lng, p.lat]);

            ctx.fillStyle = 'rgba(0, 200, 255, 0.6)';
            ctx.fillRect(point.x, point.y, 2, 2);
        }

        this.map.triggerRepaint();
    }
};

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

Особенности работы с плотностью пикселей

На устройствах с высоким DPI требуется учитывать devicePixelRatio:

const ratio = window.devicePixelRatio;

this.canvas.width = map.getCanvas().width * ratio;
this.canvas.height = map.getCanvas().height * ratio;

this.canvas.style.width = map.getCanvas().width + 'px';
this.canvas.style.height = map.getCanvas().height + 'px';

this.ctx.scale(ratio, ratio);

Без масштабирования изображение становится размытым при высокой плотности экрана.

Комбинация canvas и WebGL слоёв

Mapbox GL JS допускает совместное использование canvas и WebGL-слоёв, где canvas применяется для:

  • кастомной отрисовки поверх данных
  • упрощённых вычислений визуализации
  • интеграции сторонних 2D библиотек

При этом WebGL продолжает отвечать за основную географическую сцену, а canvas действует как overlay.

Географическая привязка сложных фигур

Для полигонов используется проекция каждой вершины:

const polygon = [
    [69.58, 42.31],
    [69.60, 42.31],
    [69.60, 42.33],
    [69.58, 42.33]
];

ctx.beginPath();

polygon.forEach((coord, i) => {
    const p = this.map.project(coord);

    if (i === 0) ctx.moveTo(p.x, p.y);
    else ctx.lineTo(p.x, p.y);
});

ctx.closePath();
ctx.stroke();

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

Управление перерисовкой

Метод map.triggerRepaint() играет ключевую роль в анимации. Он принудительно запускает новый цикл рендеринга даже при отсутствии событий взаимодействия пользователя.

Без этого вызова динамические canvas-слои перестают обновляться.

Ограничения canvas в Mapbox GL JS

Несмотря на гибкость, существуют ограничения:

  • отсутствие аппаратного ускорения 2D canvas
  • высокая нагрузка при большом количестве объектов
  • необходимость ручного управления синхронизацией
  • зависимость от частоты render-цикла карты

При масштабировании до десятков тысяч объектов предпочтение обычно отдаётся WebGL-слоям.

Интеграция с событиями карты

Canvas может реагировать на события карты:

this.map.on('move', () => this.render());
this.map.on('zoom', () => this.render());

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

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

Для повышения производительности применяется предварительная отрисовка:

  • создание offscreen buffer
  • кеширование статических элементов
  • копирование результата в основной canvas

Такой подход снижает нагрузку на главный render thread.

Координатные преобразования и точность

При работе с глобальными координатами возникает проблема округления на больших zoom-уровнях. Mapbox GL JS использует сферическую проекцию Web Mercator, где:

  • искажения увеличиваются к полюсам
  • расстояния не сохраняются линейно
  • пиксельная точность зависит от zoom

Это влияет на точность canvas-оверлеев, особенно при работе с геометрией высокой плотности.

Взаимодействие canvas с пользовательским вводом

Canvas сам по себе не знает о географических координатах, поэтому обработка кликов требует обратного преобразования:

canvas.addEventListener('click', (e) => {
    const rect = canvas.getBoundingClientRect();

    const point = {
        x: e.clientX - rect.left,
        y: e.clientY - rect.top
    };

    const lngLat = this.map.unproject(point);
});

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

Роль canvas в кастомных визуализациях

Canvas-слой в контексте Mapbox GL JS часто используется как промежуточный уровень между сырыми данными и WebGL-рендерингом, особенно при необходимости:

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

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