Меня зовут Владимир, я занимаюсь мобильным фронтендом в Яндекс.Почте. В нашем приложении уже была тёмная тема, но недожатая: мы умели перекрашивать интерфейс и простые письма. Но письма с форматированием оставались светлыми и контрастировали с тёмным интерфейсом, из-за чего глаза ночью могли уставать.  Сегодня я расскажу читателям Хабра о том, как мы решили эту проблему. Вы узнаете про два простых способа, которые нам не подошли, затем — про наш основной способ адаптивной перекраски страниц и, наконец, про направление для следующей итерации: перекраску картинок. Хотя сама задача — перекрашивать страницы с произвольным форматированием — специфическая, думаю, наш опыт будет полезен и вам.  Прежде чем дойти до нашего волшебного «перекрашивателя», мы опробовали два простых, как пробка, варианта: навесить на элемент дополнительный тёмный стиль или CSS-фильтр. Нам они не подошли, но, возможно, для каких-то случаев будут даже лучше (потому что просто = круто). Самый простецкий способ, логично расширяющий тёмную тему самого приложения в CSS: повесим тёмные стили на контейнер для писем (в общем случае — для чужого контента, который нужно перекрасить): Но если у элементов внутри письма есть свои стили, они переопределят наш корневой стиль. Нет, !important не поможет. Идею можно дожать, отрубив наследование: В данном случае без !important не обойтись, потому что сам по себе селектор не очень специфичный. Тем более это понадобится, чтобы переопределить инлайн-стили (а инлайн-стили с !important всё равно пролезут, ничего не поделать). Наш стиль довольно топорный и красит всё одинаково, так что вылезает другая проблема: вероятно, дизайнер хотел что-то сказать расстановкой цветов (приоритеты элементов и прочие дизайнерские штуки), но мы взяли и выкинули всю эту задумку.  Если вы уважаете дизайнеров меньше, чем я, и всё же решите использовать такой метод, не забудьте допилить неочевидные мелочи: Технически способ неплох: это три строчки кода (ладно, тридцать для продакшн-реди версии с корнеркейсами), совместимость со всеми браузерами мира, обработка динамических страниц из коробки и никакой привязки к способу подключения стилей в исходном документе. Особый бонус —можно легко подкрутить цвета в стиле, чтобы они подходили к основному приложению (скажем, сделать фон #bbbbb8 вместо чёрного). Кстати, раньше мы перекрашивали письма именно так, но, если находили внутри письма любые стили, пугались и оставляли письмо светлым. Очень остроумный и элегантный вариант. Перекрасить страницу можно CSS-фильтром: После этого фотографии станут криповыми, но это не беда — их перекрасим обратно:  Остаются проблемы с контентными картинками, привязанными через background (знаем, так удобнее подстраивать соотношение сторон, но как же семантика?). Допустим, что мы сможем найти все такие элементы, явно их пометить и перекрасить обратно. Способ хорош тем, что сохраняет оригинальное соотношение яркостей и контрастов. С другой стороны, проблем куча, и они скорее перевешивают достоинства:  Этот метод подошёл бы для писем, состоящих из текста в нейтральных тонах, но кто те эстеты, что набирают себе полный инбокс такого своеобразного контента? Зато фильтрами можно перекрашивать элементы, к содержимому которых нет доступа — фреймы, веб-компоненты, картинки. Настало время волшебства! Из недостатков первых двух подходов собираем чеклист: Нам нужно изменить цвета стилей так, чтобы фон был тёмным. И почему бы не сделать это буквально? Просто берём все стили, ищем правила, связанные с цветами (color, background, border, box-shadow, их подсвойства), и заменяем его на «затемнённое» — затемняем фон, осветляем текст, границы затемняем меньше фона и т. д. У такого метода есть одно невероятное достоинство, которое согреет душу любому разработчику. Каждому свойству можно настраивать (да, прямо кодом описывать!) свои правила преобразования цветов. При достаточном воображении можно интегрироваться с любой внешней темой, делать любую цветокоррекцию (например, вместо тёмной темы делать светлую или серо-буро-малиновую) и даже добавлять немного контекстности — скажем, по-разному обрабатывать широкие и узкие границы. Недостатки — стандартные для «everything-in-js». Да, мы гоняем скрипты, ломаем инкапсуляцию стилей и парсим CSS регэкспами. Ну, в отличие от HTML, последнее не так уж позорно, потому что грамматика (нужного нам уровня) CSS всё-таки регулярная. План перекраски такой: Обвязка (нормализация, поиск стилей, парсинг) довольно простая. Разберёмся с тем, как именно работает наш волшебный преобразователь. «Затемнить цвет» — не такое простое действие, как может показаться, особенно если мы хотим сохранить тон (голубой становится тёмно-синим, а не оранжевым). Сделать это в нормальном RGB можно, но проблематично. Любители алгоритмического дизайна знают, что там даже градиенты получаются кривоватые. Зато работать с цветами в HSL — чистое наслаждение: вместо Red, Green и Blue, с которыми непонятно, что делать, у нас появляются другие три канала: Такое пространство удобно представлять в виде цилиндра. А наша задача — перевернуть этот цилиндр с ног на голову. Функции цветокоррекции делают что-то вроде (h, s, l) => [h, s, 1 - l]. Иногда ситуация складывается удачно: эксклюзивный дизайн письма (или его части) уже тёмный. В таком случае не нужно ничего менять, лучше просто тихонько порадоваться — вероятно, дизайнер подобрал цвета не хуже нашего алгоритма. В HSL достаточно смотреть на L — яркость. Если она выше (для текста) или ниже (для фона) порога (который, конечно, настраивается) — ничего не делаем. Хотя нам это и не понадобилось (ещё раз спасибо, санитайзер, ты спас меня от безумия!), но я всё же расскажу, какие допилы нужны адаптивной теме, чтобы затемнять полные страницы, а не только дурацкие статические письма из девяностых. Аккуратнее, это задача для тех, кто любит запах селекторов по утрам. Самый простой кейс, который ломает нашу затемнённую страницу — изменение инлайн-стилей. Операция частая, но и фикс простой: добавляем MutationObserver и оперативно чиним инлайн-стили при изменениях. Работать со стилями из <link> изнутри страницы довольно мучительно из-за асинхронности и @import, да и от CORS не веселее. Кажется, эту проблему можно было бы довольно элегантно решить через веб-воркер (прокси для *.css). Наконец, собирая в кучу все наши проблемы, вспоминаем, что скрипт вообще может добавлять, удалять и переставлять (специфичность! каскад!) <style> и <link>, да ещё и менять правила в <style>. Решается всё тем же MutationObserver на элементы стилей, но на каждое изменение — больше обработки. Совершенно новый виток безумия наступает, когда в игру входят CSS-переменные. Затемнять сами переменные мы не можем: даже если допустить, что мы по формату угадаем, что в переменной находится цвет (хотя я бы не советовал этим заниматься), неизвестно, в какой роли он нам встретится — фон, текст, граница, всё сразу? Более того, значения переменных наследуются, так что нам уже нужно учитывать не только стили, но и элементы, к которым они применяются, и всё это быстро эскалируется и взрывается. Если CSS-переменные доедут до мейнстрима, у нас проблемы. С другой стороны, к тому времени уже начнут подвозить color(), с которым можно будет не менять цвета в JS, а просто заменять цвета на color(var(--bg) lightness(-50%)).  Для нашего случая, когда санитайзер оставляет только инлайн-стили, адаптивное затемнение на уровне CSS подходит отлично: даёт лучшее качество затемнения, не ломает письма и работает относительно просто и быстро. Не уверен, что вариант со всем фаршем для динамики стоит того. К счастью, если вы работаете с пользовательским контентом и при этом пишете не браузер, ваш санитайзер должен делать то же самое. На практике адаптивный режим нужно использовать вместе с переопределением стилей: к стандартным элементам вроде <input> или <a> стили обычно явно не применяют, а по умолчанию они светлые. Перекраска картинок — отдельная проблема, которая очень беспокоит меня лично. Это интересно, и у меня наконец-то есть шанс использовать словосочетание «спектральный анализ». С картинками в тёмной теме есть несколько типичных проблем. Во-первых, слишком светлые картинки. Работает так же, как непрокрашенные письма, с которых всё и началось. Часто (но не обязательно) это обычные фотографии. Поскольку верстать рассылки не очень весело, многие ребята просто экспортируют сложную часть письма как картинку, она не перекрашивается и по ночам освещает мой перфекционизм. Такие картинки нужно затемнять, но не инвертировать — иначе выйдет страшный негатив.  Во-вторых, тёмные картинки с настоящей прозрачностью. Эта проблема часто встречается на логотипах — они рассчитаны на светлый фон и, когда мы подменяем его на тёмный, сливаются с ним. Такие картинки нужно инвертировать.  Где-то посередине есть картинки, для которых белый изображал «прозрачный фон», но теперь они просто стоят в каком-то непонятном белом прямоугольнике. В идеальном мире мы бы подменили белый фон на прозрачный, но если вы когда-либо работали с волшебной палочкой в фоторедакторе, то знаете, что сделать это автоматически не так-то просто.  Интересно, что иногда картинки вообще не несут никакой смысловой нагрузки — это трекинг-пиксели и «держатели формата» в особо извращённой вёрстке. Такие можно смело сделать невидимыми (скажем, opacity: 0).  Чтобы решить, что делать с картинкой, нам нужно залезть внутрь и проанализировать её содержимое — причём простым и быстрым способом. По нашему набору проблем вырисовывается первая версия алгоритма. Вот она. Считаем тёмные, светлые и прозрачные пиксели на картинке, причём не все, а выборочно — очевидная оптимизация. Определяем общую яркость картинки (светлая, тёмная, средняя) и наличие прозрачности. Тёмные картинки с прозрачностью инвертируем, светлые без прозрачности — приглушаем, остальные не трогаем. Радость от этой чудесной эвристики кончилась, когда мне попалась рассылка про благотворительность с фотографией урока в африканской школе. Всё было бы нормально, но дизайнер отцентрировал её, добавив по краям прозрачные пиксели. Оказаться в центре новой истории про оскорбительное распознавание картинок не хотелось, и мы решили в первой версии обработку картинок не делать совсем. В будущем от таких проблем должна защитить дополнительная эвристика, которую я как раз и называю «спектральный анализ» — считаем количество разных цветов на картинке и инвертируем, только если их мало. Этим же критерием можно искать графичные светлые картинки и их тоже перекрашивать — звучит заманчиво.  Для полноценной тёмной темы в почте нам не хватало перекраски писем со стилями, и мы выяснили, как это устроить. Два простых варианта на чистом CSS — переопределение стилей и CSS-фильтр — не подошли: первый слишком сурово обходится с исходным дизайном, второй просто плохо работает. В итоге мы используем адаптивное затемнение — разбираем стили, заменяем цвета более подходящими и собираем обратно. Сейчас работаем над расширением темы на картинки — для этого нужно анализировать их содержимое и перекрашивать только некоторые. Если вам когда-нибудь потребуется перекрашивать произвольный пользовательский HTML под тёмную тему, держите в голове три метода: Даже если вы никогда не будете этим заниматься, надеюсь, вы интересно провели время! Полезные ссылки: Если вам интересно обсудить эту тему вживую и в контексте разработки под Android, то приглашаем в гости 18 апреля в петербургский офис Яндекса.  Недавно мы рассказывали о решении другой проблемы пользователей почты — проблемы рассылок. Фронтендер