В стандартной архитектуре Leaflet визуализация карты основана на DOM-тайлах и HTML5 Canvas. Такой подход обеспечивает простоту и предсказуемость рендеринга, но накладывает ограничения при работе с большими массивами данных и сложной графикой. WebGL вводит аппаратное ускорение через GPU, позволяя выполнять отрисовку десятков и сотен тысяч объектов без критической деградации производительности.
WebGL в контексте Leaflet не заменяет основной рендерер карты, а расширяет его, подключаясь как дополнительный слой поверх тайловой системы. Основная задача интеграции — синхронизация координатной системы карты с WebGL-канвасом и управление жизненным циклом рендера в рамках событий карты (zoom, move, rotate-like transformations через pan/zoom).
Canvas 2D в Leaflet работает в императивной модели: каждый объект перерисовывается CPU-ом, что приводит к росту затрат при увеличении числа элементов. WebGL использует pipeline GPU:
Ключевое отличие для картографических приложений — стоимость количества объектов:
Однако WebGL требует управления:
WebGL-слой в Leaflet обычно строится как расширение
L.Layer, где используется собственный canvas с контекстом
webgl:
L.WebGLLayer = L.Layer.extend({
onAdd: function (map) {
this._map = map;
this._canvas = L.DomUtil.create('canvas', 'leaflet-webgl-layer');
this._canvas.style.position = 'absolute';
this._gl = this._canvas.getContext('webgl', {
antialias: false,
alpha: true
});
map.getPanes().overlayPane.appendChild(this._canvas);
this._initGL();
this._reset();
this._update();
map.on('move zoom resize', this._reset, this);
map.on('move', this._update, this);
},
onRemove: function (map) {
map.getPanes().overlayPane.removeChild(this._canvas);
map.off('move zoom resize', this._reset, this);
map.off('move', this._update, this);
}
});
Такой слой живёт в рамках overlayPane и синхронизируется с матрицей карты.
Leaflet использует проекцию Web Mercator. В WebGL необходимо
переводить LatLng в пиксельные координаты карты:
const point = map.latLngToLayerPoint([lat, lng]);
Далее координаты нормализуются под размер canvas:
const x = point.x;
const y = point.y;
Однако WebGL работает в нормализованных координатах
[-1, 1], поэтому требуется матрица преобразования:
function toClipSpace(x, y, width, height) {
return [
(x / width) * 2 - 1,
1 - (y / height) * 2
];
}
Ключевая проблема интеграции — синхронизация pan/zoom с GPU-матрицей.
Leaflet предоставляет пиксельный offset карты:
const topLeft = map.containerPointToLayerPoint([0, 0]);
WebGL-матрица строится как комбинация:
Пример матричного подхода:
function createTransform(map) {
const zoomScale = map.getZoomScale(map.getZoom());
const offset = map.getPixelOrigin();
return {
scale: zoomScale,
offsetX: -offset.x,
offsetY: -offset.y
};
}
Наиболее типичный кейс WebGL в картографии — отрисовка точек (markers-like data).
const positions = new Float32Array(data.length * 2);
for (let i = 0; i < data.length; i++) {
const p = map.latLngToLayerPoint(data[i]);
positions[i * 2] = p.x;
positions[i * 2 + 1] = p.y;
}
Загрузка в GPU:
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
Vertex shader отвечает за позиционирование:
attribute vec2 a_position;
uniform vec2 u_resolution;
void main() {
vec2 zeroToOne = a_position / u_resolution;
vec2 clipSpace = zeroToOne * 2.0 - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
gl_PointSize = 4.0;
}
Fragment shader задаёт цвет:
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
WebGL-слой должен учитывать, что Leaflet управляет перерисовкой через события, а не через игровой loop.
map.on('move', () => {
requestAnimationFrame(() => {
layer._render();
});
});
При этом важно избегать постоянного рендера без изменений состояния карты.
Интеграция WebGL в Leaflet редко реализуется вручную полностью. Используются промежуточные библиотеки:
deck.gl часто используется как внешний WebGL-рендерер, синхронизируемый с Leaflet через viewport:
const viewState = {
longitude: center.lng,
latitude: center.lat,
zoom: map.getZoom(),
width: map.getSize().x,
height: map.getSize().y
};
Leaflet разделяет карту на panes:
WebGL canvas обычно помещается в overlayPane:
map.getPanes().overlayPane.appendChild(canvas);
Важно учитывать порядок отрисовки DOM-слоёв, иначе WebGL может оказаться под тайлами.
WebGL слой обязан реагировать на:
Пример:
map.on('zoomend move resize', () => {
layer._resize();
layer._updateTransform();
layer._render();
});
При zoom изменяется масштаб буфера или пересчитываются координаты.
Основные техники оптимизации:
Все точки передаются одним буфером вместо множества draw calls.
Удаление объектов вне видимой области:
if (p.x < 0 || p.x > width || p.y < 0 || p.y > height) return;
Пересчёт LatLng → pixel выполняется только при zoom/move, а не в каждом кадре.
При масштабировании данных используются:
Кластеризация часто предобрабатывается:
function cluster(points, zoom) {
const size = Math.pow(2, zoom);
const grid = new Map();
for (const p of points) {
const key = `${Math.floor(p.x / size)}:${Math.floor(p.y / size)}`;
if (!grid.has(key)) grid.set(key, []);
grid.get(key).push(p);
}
return grid;
}
Несмотря на производительность, WebGL-слой имеет ограничения:
Leaflet использует пиксельную систему относительно контейнера карты. WebGL использует clip space.
Связующий слой:
Этот pipeline должен пересчитываться строго при изменении view state, иначе возникают дрейфы и рассинхронизация слоёв.
Для большого количества одинаковых объектов применяется instanced rendering:
gl.drawArraysInstanced(gl.POINTS, 0, 1, instanceCount);
Это позволяет отрисовывать тысячи объектов с одним draw call, передавая данные через instance buffers.
Leaflet не управляет глубиной WebGL, поэтому z-index реализуется вручную:
Для потоковых данных (например, GPS или IoT):
bufferSubDatagl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array(updatedPositions));