Динамическое освещение по времени суток и точке на карте
Освещение в стиле карты (light) — это глобальный параметр: его источники вычисляются один раз на кадр и не зависят от отдельных объектов. Поэтому «солнце над точкой» задаётся не положением объекта, а временем суток, которое вы вычисляете в стилевом выражении. Меняя это время при перемещении карты, можно получить реалистичную картину: в Москве день, а на востоке — вечер, с соответствующим положением теней.
В этой инструкции показано, как собрать такое освещение из двух глобальных стилевых переменных:
timeOffsetMinutes— смещение таймзоны относительно UTC (в минутах к востоку) для текущей геоточки;dynamicLightingEnabled— флаг, включающий динамическое освещение.
Освещение — общий параметр кадра, поэтому «включить разное освещение одновременно для разных точек экрана» нельзя: свет соответствует центру карты. При перемещении карта полностью переключается в локальное время новой точки.
Как это работает
Минуты суток в точке вычисляются комбинацией трёх выражений:
["time", ["utc-time"], "day-minutes"]— текущие минуты от полуночи по UTC (диапазон0…1439). Обратите внимание:utc-timeобязательно оборачивается вtime(...)— без обёртки выражение возвращает метку времени в миллисекундах, а не минуты суток.["global", "timeOffsetMinutes"]— смещение таймзоны точки, которое вы передаёте из кода карты.
Итоговый ключ времени:
["time", ["utc-time", ["global", "timeOffsetMinutes"]], "day-minutes"]
Знак смещения — минуты к востоку от UTC: Москва (UTC+3) → 180, Екатеринбург (UTC+5) → 300, Якутск (UTC+9) → 540.
Настройка стиля
Все выражения освещения задаются в объекте light стиля. Ниже — пример для источников sun (направленный, отвечает в том числе за тени) и atmosphere (рассеянный). Возьмите эти фрагменты за основу в Редакторе стилей.
Цвет и интенсивность солнца меняются по времени суток, азимут и высота — положение солнца, которое определяет направление теней:
{
"sun": {
"type": "directional",
"color": ["interpolate", ["linear"], ["zoom"],
16, "#FFFFFF",
17,
["when", ["global", "dynamicLightingEnabled"],
["interpolate", ["linear"],
["time", ["utc-time", ["global", "timeOffsetMinutes"]], "day-minutes"],
0, "#8FA6D8",
360, "#FFB27A",
720, "#FFFFFF",
1080, "#FF9E6E",
1440, "#8FA6D8"
],
"#FFFFFF"
]
],
"azimuth": ["when", ["global", "dynamicLightingEnabled"],
["interpolate", ["linear"],
["time", ["utc-time", ["global", "timeOffsetMinutes"]], "day-minutes"],
0, 0,
360, 90,
720, 180,
1080, 270,
1440, 360
],
230
],
"altitude": ["when", ["global", "dynamicLightingEnabled"],
["interpolate", ["linear"],
["time", ["utc-time", ["global", "timeOffsetMinutes"]], "day-minutes"],
0, 5,
720, 50,
1440, 5
],
50
]
},
"atmosphere": {
"type": "ambient",
"color": ["interpolate", ["linear"], ["zoom"],
16, "#FFFFFF",
17,
["when", ["global", "dynamicLightingEnabled"],
["interpolate", ["linear"],
["time", ["utc-time", ["global", "timeOffsetMinutes"]], "day-minutes"],
0, "#AFC0DE",
360, "#FFD9C0",
720, "#FFFFFF",
1080, "#FFCBB0",
1440, "#AFC0DE"
],
"#FFFFFF"
]
]
}
}
Разбор конструкции:
interpolateпо["zoom"]сохраняет текущее поведение на малых зумах и включает динамику только на крупном масштабе (17и выше). При необходимости уберите внешнийinterpolateи оставьте только ветку по времени.whenвыбирает между динамическим (флагtrue) и статичным (флагfalseили глобальная переменная не задана) значением.whenработает и как обёртка над всей веткой зума, и над одним свойством.azimuthиaltitude— положение солнца по времени. Азимут отсчитывается от севера:90— восток (восход),180— юг (полдень),270— запад (закат). Именно эти значения задают направление теней.- Тени включаются привязкой источника к теням в секции
shadows("shadows": { "source": "sun" }) — тогда направление теней следует заazimuth/altitude.
Расчёт смещения времени в коде
Значение timeOffsetMinutes нужно вычислять для центра карты и передавать в стиль через метод map.patchStyleState(). Пересчитывайте его при завершении перемещения карты (moveend) и раз в минуту — на случай смены суток или перехода на летнее время.
Приближённо, по долготе
Самый простой вариант без зависимостей: часовой пояс оценивается по долготе (15° = 1 час). Не учитывает переход на летнее время и «сдвинутые» зоны (Китай, Индия), но для визуального эффекта этого обычно достаточно.
function offsetMinutesForLng(lng) {
return Math.round(lng / 15) * 60;
}
function updatePointTimeOffset(map) {
const [lng] = map.getCenter();
map.patchStyleState({ timeOffsetMinutes: offsetMinutesForLng(lng) });
}
Точно, по IANA-зоне
Если важны политические границы зон и переход на летнее время, определите IANA-зону по координатам (например, с помощью пакета tz-lookup) и получите смещение через Intl:
function zoneOffsetMinutes(timeZone, date = new Date()) {
const name = new Intl.DateTimeFormat('en-US', { timeZone, timeZoneName: 'longOffset' })
.formatToParts(date)
.find((part) => part.type === 'timeZoneName').value;
const match = /GMT([+-])(\d{2}):(\d{2})/.exec(name);
return match ? (match[1] === '-' ? -1 : 1) * (+match[2] * 60 + +match[3]) : 0;
}
function updatePointTimeOffset(map) {
const [lng, lat] = map.getCenter();
const timeZone = tzlookup(lat, lng);
map.patchStyleState({ timeOffsetMinutes: zoneOffsetMinutes(timeZone) });
}
Инициализация и подписки
function enableDynamicLighting(map) {
let lastOffset = NaN;
const refresh = () => {
const [lng] = map.getCenter();
const offset = offsetMinutesForLng(lng);
if (offset === lastOffset) {
return;
}
lastOffset = offset;
map.patchStyleState({ timeOffsetMinutes: offset });
};
document.querySelector('#lighting-toggle').addEventListener('change', (event) => {
map.patchStyleState({ dynamicLightingEnabled: event.target.checked });
});
map.on('moveend', refresh);
setInterval(refresh, 60 * 1000);
refresh();
}
enableDynamicLighting(map);
Пример использования
В примере чекбокс включает и выключает динамическое освещение через глобальную переменную dynamicLightingEnabled. Само освещение должно быть предварительно настроено в стиле (см. раздел Настройка стиля). Перемещая карту, вы увидите, как меняется свет и направление теней.
Ограничения
- Один свет на кадр. Освещение — глобальный параметр, поэтому одновременно «день в Москве и вечер в Якутске» на одном экране не отобразить. Свет всегда соответствует центру карты.
- Точность часового пояса. Приближение по долготе (
lng / 15) игнорирует границы зон и переход на летнее время. Для точного результата используйте IANA-зону иIntl(см. выше).