2D-режим на Three.js и всплывающий список авиакомпаний

Изначально 2D-карта работала на Canvas API с Web Worker-ом для оффскрин-рендеринга. Схема была рабочей — тайлы, кэш, dirty-флаги для отдельных слоёв. Но при активном зуме или выборе новой авиакомпании всё равно чувствовались подтормаживания: Canvas приходилось перерисовывать слои заново, пусть и по частям.
Решение напрашивалось само: раз уж 3D-режим уже работает на Three.js и летает, логично было перевести и 2D на тот же движок. В итоге — получилось существенно быстрее.
Проекция и камера

Для плоской карты выбрал проекцию Миллера вместо Меркатора. Меркатор даёт бесконечность на полюсах, Миллер — нет:
x = lon / 180
y = 1.25 × ln(tan(π/4 + 0.4 × latRad)) / π
Карта охватывает диапазон от 78°N до 60°S — достаточно, чтобы вместить все коммерческие аэропорты и крупные хабы.
Камера — ортографическая. Никакой перспективной дисторсии, честная плоская проекция. Зум ограничен снизу, чтобы карта не уходила за границы видимости. Плавный влёт к выбранному аэропорту реализован через линейную интерполяцию (LERP) каждый кадр — ощущение мягкого «подлёта» без скачков.
Слои сцены
Сцена строится из нескольких слоёв с явным порядком рендеринга:
- Суша — меш из полигонов TopoJSON, треугольники через Earcut, opacity 0.04
- Границы стран — линии без дублирования рёбер, opacity 0.09
- Маршруты — квадратичные кривые Безье, 36 сегментов на маршрут, opacity 0.16
- Выделенные маршруты — толстые линии с анимированным свечением, opacity 0.95
- Точки аэропортов — инстансированный меш, один draw call на всё множество точек
Для суши и границ действует LOD: при зуме меньше 2.5 используется упрощённый датасет (countries-110m), при приближении автоматически подгружается детальный (countries-50m). Тяжёлый датасет строится в requestIdleCallback, чтобы не блокировать UI при старте.
Отдельно пришлось решить проблему антимеридиана — разрыв полигонов на ±180° даёт артефакты при триангуляции. Лечится размоткой долгот до непрерывного диапазона перед построением меша.
Маршруты и параллельные дуги
Маршруты — квадратичные дуги Безье. Для пар аэропортов, между которыми летает несколько авиакомпаний одновременно, реализовано lane-разнесение: нулевой лейн идёт по прямой хорде, остальные смещаются перпендикулярно на величину N × min(distance × 0.08, 0.06) единиц мировых координат. Небольшой сдвиг вдоль хорды предотвращает пиксельное слияние параллельных маршрутов.
Анимация выделения
Для выделенных маршрутов — кастомный GLSL-шейдер с анимацией бегущего свечения:
glsl
float wave = fract(uTime * speed - vProgress);
Переменная vProgress интерполируется от 0 до 1 вдоль кривой. Результат — бегущий гребень свечения с хвостом длиной 0.35 от длины маршрута. Цвет смешивается между базовым оранжевым (#eb610b) и жёлтым пиком (#ffe757).
Прирост производительности
Переход на Three.js дал несколько ощутимых преимуществ:
- GPU-рендеринг вместо CPU Canvas: геометрия отправляется на видеокарту один раз, дальше только трансформации матрицей
- Инстансированный меш для точек аэропортов: сотни точек — один draw call
- Dirty-флаг анимационного цикла: если нет активного лёта и нет анимации — рендер пропускается полностью
- LOD с deferred-загрузкой: тяжёлый датасет не блокирует первый фрейм
В итоге карта открывается быстро, зум и панорамирование работают без рывков, переключение авиакомпаний отрабатывает мгновенно.
Список авиакомпаний по аэропорту

Параллельно добавил функцию, которой мне самому давно не хватало при работе с картой.
Раньше, чтобы посмотреть маршруты авиакомпании из конкретного аэропорта, нужно было знать, кто вообще там летает — и искать в списке. Неудобно, особенно если аэропорт незнакомый.
Теперь рядом с названием активного аэропорта есть иконка ℹ. По нажатию появляется панель с двумя блоками:
Карточка аэропорта — IATA/ICAO-коды, координаты, город и страна.
Список авиакомпаний — все перевозчики, которые выполняют рейсы из этого аэропорта. Для каждой показывается IATA-код, название, количество направлений и частота рейсов. Клик на авиакомпанию — и карта сразу переключается на её маршрутную сеть с автоматической подгонкой камеры под охват сети.
Реализация
Данные подтягиваются двумя параллельными запросами:
GET /airlines/airports/iata/{code}?lang=ru — детали аэропорта
GET /airlines/routes/airport/{code} — авиакомпании из аэропорта
Результаты кэшируются локально, повторный клик на тот же аэропорт не делает новых запросов.
Позиционирование панели — с логикой умного размещения: сначала считается доступное пространство справа и слева от точки привязки, панель открывается там, где места больше. Вертикальная позиция прижата к краям вьюпорта с отступом 8px. Стрелка-указатель выравнивается по Y-координате аэропорта через CSS-переменную --arrow-offset.
Закрытие — клик вне панели или скролл карты.
TODO
Следующий крупный блок, который хочу реализовать — интерфейс редактирования данных. Сейчас вся информация об аэропортах, авиакомпаниях и маршрутных сетях поступает из внешних источников и обновляется автоматически. Но автоматика не всегда точна: бывают устаревшие маршруты, отсутствующие названия на русском, неверные координаты.
Планирую добавить закрытый раздел с формами для ручного редактирования:
- Аэропорты — коррекция названий, координат, привязки к городу
- Авиакомпании — обновление названий, логотипов, статуса активности
- Маршрутные сети — добавление и удаление маршрутов, корректировка частоты рейсов
Это даст возможность поддерживать актуальность данных независимо от внешних источников.