Pull to refresh
776.02
Яндекс
Как мы делаем Яндекс

Как создать тёмную тему и не навредить. Опыт команды Яндекс.Почты

Reading time 8 min
Views 32K


Меня зовут Владимир, я занимаюсь мобильным фронтендом в Яндекс.Почте. В нашем приложении уже была тёмная тема, но недожатая: мы умели перекрашивать интерфейс и простые письма. Но письма с форматированием оставались светлыми и контрастировали с тёмным интерфейсом, из-за чего глаза ночью могли уставать.


Сегодня я расскажу читателям Хабра о том, как мы решили эту проблему. Вы узнаете про два простых способа, которые нам не подошли, затем — про наш основной способ адаптивной перекраски страниц и, наконец, про направление для следующей итерации: перекраску картинок. Хотя сама задача — перекрашивать страницы с произвольным форматированием — специфическая, думаю, наш опыт будет полезен и вам.


Простые способы


Прежде чем дойти до нашего волшебного «перекрашивателя», мы опробовали два простых, как пробка, варианта: навесить на элемент дополнительный тёмный стиль или CSS-фильтр. Нам они не подошли, но, возможно, для каких-то случаев будут даже лучше (потому что просто = круто).


Переопределение стилей


Самый простецкий способ, логично расширяющий тёмную тему самого приложения в CSS: повесим тёмные стили на контейнер для писем (в общем случае — для чужого контента, который нужно перекрасить):


.message--dark {
    background-color: black;
    color: white;
}

Но если у элементов внутри письма есть свои стили, они переопределят наш корневой стиль. Нет, !important не поможет. Идею можно дожать, отрубив наследование:


.message--dark * {
    background-color: black !important;
    color: white !important;
    border-color: #333 !important;
}

В данном случае без !important не обойтись, потому что сам по себе селектор не очень специфичный. Тем более это понадобится, чтобы переопределить инлайн-стили (а инлайн-стили с !important всё равно пролезут, ничего не поделать).


Наш стиль довольно топорный и красит всё одинаково, так что вылезает другая проблема: вероятно, дизайнер хотел что-то сказать расстановкой цветов (приоритеты элементов и прочие дизайнерские штуки), но мы взяли и выкинули всю эту задумку.



Если вы уважаете дизайнеров меньше, чем я, и всё же решите использовать такой метод, не забудьте допилить неочевидные мелочи:


  • box-shadow — только цвет переопределить не выйдет, придётся все тени убрать или жить со светлыми.
  • Цвета семантических элементов — ссылок, элементов ввода.
  • Инлайн-SVG — вместо background им нужно выставлять fill, а вместо colorstroke, но это не точно, смотря какой SVG — может быть и наоборот.

Технически способ неплох: это три строчки кода (ладно, тридцать для продакшн-реди версии с корнеркейсами), совместимость со всеми браузерами мира, обработка динамических страниц из коробки и никакой привязки к способу подключения стилей в исходном документе. Особый бонус —можно легко подкрутить цвета в стиле, чтобы они подходили к основному приложению (скажем, сделать фон #bbbbb8 вместо чёрного).


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


CSS-фильтр


Очень остроумный и элегантный вариант. Перекрасить страницу можно CSS-фильтром:


.message--dark {
    filter: invert(100) hue-rotate(180deg); /* hue-rotate возвращает тона обратно */
}

После этого фотографии станут криповыми, но это не беда — их перекрасим обратно:


.message-dark img {
    filter: invert(100) hue-rotate(180deg);
}


Остаются проблемы с контентными картинками, привязанными через background (знаем, так удобнее подстраивать соотношение сторон, но как же семантика?). Допустим, что мы сможем найти все такие элементы, явно их пометить и перекрасить обратно.


Способ хорош тем, что сохраняет оригинальное соотношение яркостей и контрастов. С другой стороны, проблем куча, и они скорее перевешивают достоинства:



  1. Тёмные страницы осветляются.
  2. Итоговыми цветами невозможно управлять — какой фильтр наложить, чтобы подстроить фон к вашему фирменному #bbbbb8? Загадка.
  3. После двух перекрасов картинки выцветают.
  4. Всё тормозит (особенно на телефонах) — логично, теперь вместо простой отрисовки браузеру нужно на каждом экране гонять обработку изображений.

Этот метод подошёл бы для писем, состоящих из текста в нейтральных тонах, но кто те эстеты, что набирают себе полный инбокс такого своеобразного контента? Зато фильтрами можно перекрашивать элементы, к содержимому которых нет доступа — фреймы, веб-компоненты, картинки.


Адаптивная тема


Настало время волшебства! Из недостатков первых двух подходов собираем чеклист:


  1. Делать фон тёмным, текст — светлым, границы — средними.
  2. Определять уже тёмные страницы и не перекрашивать их.
  3. Сохранять оригинальное соотношение яркостей и контрастов.
  4. Давать возможность настройки цветов.
  5. Оставлять тона такими, какими они были вначале.

Нам нужно изменить цвета стилей так, чтобы фон был тёмным. И почему бы не сделать это буквально? Просто берём все стили, ищем правила, связанные с цветами (color, background, border, box-shadow, их подсвойства), и заменяем его на «затемнённое» — затемняем фон, осветляем текст, границы затемняем меньше фона и т. д.


У такого метода есть одно невероятное достоинство, которое согреет душу любому разработчику. Каждому свойству можно настраивать (да, прямо кодом описывать!) свои правила преобразования цветов. При достаточном воображении можно интегрироваться с любой внешней темой, делать любую цветокоррекцию (например, вместо тёмной темы делать светлую или серо-буро-малиновую) и даже добавлять немного контекстности — скажем, по-разному обрабатывать широкие и узкие границы.


Недостатки — стандартные для «everything-in-js». Да, мы гоняем скрипты, ломаем инкапсуляцию стилей и парсим CSS регэкспами. Ну, в отличие от HTML, последнее не так уж позорно, потому что грамматика (нужного нам уровня) CSS всё-таки регулярная.


План перекраски такой:


  1. Нормализуем легаси-свойства стиля (bgcolor и друзей), перекладываем их в style="...".
  2. Находим все инлайн-стили.
  3. В каждом стиле находим все цветные правила (background-color, color, box-shadow и т. д.).
  4. Из всех цветных правил достаём цвета, находим нужный преобразователь (затемнитель для фона, осветлитель для текста).
  5. Вызываем преобразователь.
  6. Собираем преобразованные правила обратно в CSS.

Обвязка (нормализация, поиск стилей, парсинг) довольно простая. Разберёмся с тем, как именно работает наш волшебный преобразователь.


HSL-преобразования


«Затемнить цвет» — не такое простое действие, как может показаться, особенно если мы хотим сохранить тон (голубой становится тёмно-синим, а не оранжевым). Сделать это в нормальном RGB можно, но проблематично. Любители алгоритмического дизайна знают, что там даже градиенты получаются кривоватые. Зато работать с цветами в HSL — чистое наслаждение: вместо Red, Green и Blue, с которыми непонятно, что делать, у нас появляются другие три канала:


  • Hue — как раз тон, который мы хотим сохранить.
  • Saturaion — насыщенность, которая нам сейчас не очень важна.
  • Lightness — яркость, которую мы будем менять.

Такое пространство удобно представлять в виде цилиндра. А наша задача — перевернуть этот цилиндр с ног на голову. Функции цветокоррекции делают что-то вроде (h, s, l) => [h, s, 1 - l].


Цвета, с которыми и так всё хорошо


Иногда ситуация складывается удачно: эксклюзивный дизайн письма (или его части) уже тёмный. В таком случае не нужно ничего менять, лучше просто тихонько порадоваться — вероятно, дизайнер подобрал цвета не хуже нашего алгоритма. В HSL достаточно смотреть на L — яркость. Если она выше (для текста) или ниже (для фона) порога (который, конечно, настраивается) — ничего не делаем.


Динамический цирк


Хотя нам это и не понадобилось (ещё раз спасибо, санитайзер, ты спас меня от безумия!), но я всё же расскажу, какие допилы нужны адаптивной теме, чтобы затемнять полные страницы, а не только дурацкие статические письма из девяностых. Аккуратнее, это задача для тех, кто любит запах селекторов по утрам.


Динамические инлайн-стили


Самый простой кейс, который ломает нашу затемнённую страницу — изменение инлайн-стилей. Операция частая, но и фикс простой: добавляем MutationObserver и оперативно чиним инлайн-стили при изменениях.


Внешние стили


Работать со стилями из <link> изнутри страницы довольно мучительно из-за асинхронности и @import, да и от CORS не веселее. Кажется, эту проблему можно было бы довольно элегантно решить через веб-воркер (прокси для *.css).


Динамические стили


Наконец, собирая в кучу все наши проблемы, вспоминаем, что скрипт вообще может добавлять, удалять и переставлять (специфичность! каскад!) <style> и <link>, да ещё и менять правила в <style>. Решается всё тем же MutationObserver на элементы стилей, но на каждое изменение — больше обработки.


CSS-переменные


Совершенно новый виток безумия наступает, когда в игру входят CSS-переменные. Затемнять сами переменные мы не можем: даже если допустить, что мы по формату угадаем, что в переменной находится цвет (хотя я бы не советовал этим заниматься), неизвестно, в какой роли он нам встретится — фон, текст, граница, всё сразу? Более того, значения переменных наследуются, так что нам уже нужно учитывать не только стили, но и элементы, к которым они применяются, и всё это быстро эскалируется и взрывается.


Если CSS-переменные доедут до мейнстрима, у нас проблемы. С другой стороны, к тому времени уже начнут подвозить color(), с которым можно будет не менять цвета в JS, а просто заменять цвета на color(var(--bg) lightness(-50%)).


Резюме



Для нашего случая, когда санитайзер оставляет только инлайн-стили, адаптивное затемнение на уровне CSS подходит отлично: даёт лучшее качество затемнения, не ломает письма и работает относительно просто и быстро. Не уверен, что вариант со всем фаршем для динамики стоит того. К счастью, если вы работаете с пользовательским контентом и при этом пишете не браузер, ваш санитайзер должен делать то же самое.


На практике адаптивный режим нужно использовать вместе с переопределением стилей: к стандартным элементам вроде <input> или <a> стили обычно явно не применяют, а по умолчанию они светлые.


Как затемнять картинки


Перекраска картинок — отдельная проблема, которая очень беспокоит меня лично. Это интересно, и у меня наконец-то есть шанс использовать словосочетание «спектральный анализ». С картинками в тёмной теме есть несколько типичных проблем.


Во-первых, слишком светлые картинки. Работает так же, как непрокрашенные письма, с которых всё и началось. Часто (но не обязательно) это обычные фотографии. Поскольку верстать рассылки не очень весело, многие ребята просто экспортируют сложную часть письма как картинку, она не перекрашивается и по ночам освещает мой перфекционизм. Такие картинки нужно затемнять, но не инвертировать — иначе выйдет страшный негатив.



Во-вторых, тёмные картинки с настоящей прозрачностью. Эта проблема часто встречается на логотипах — они рассчитаны на светлый фон и, когда мы подменяем его на тёмный, сливаются с ним. Такие картинки нужно инвертировать.



Где-то посередине есть картинки, для которых белый изображал «прозрачный фон», но теперь они просто стоят в каком-то непонятном белом прямоугольнике. В идеальном мире мы бы подменили белый фон на прозрачный, но если вы когда-либо работали с волшебной палочкой в фоторедакторе, то знаете, что сделать это автоматически не так-то просто.



Интересно, что иногда картинки вообще не несут никакой смысловой нагрузки — это трекинг-пиксели и «держатели формата» в особо извращённой вёрстке. Такие можно смело сделать невидимыми (скажем, opacity: 0).



Эвристики с интроспекцией


Чтобы решить, что делать с картинкой, нам нужно залезть внутрь и проанализировать её содержимое — причём простым и быстрым способом. По нашему набору проблем вырисовывается первая версия алгоритма. Вот она.


Считаем тёмные, светлые и прозрачные пиксели на картинке, причём не все, а выборочно — очевидная оптимизация. Определяем общую яркость картинки (светлая, тёмная, средняя) и наличие прозрачности. Тёмные картинки с прозрачностью инвертируем, светлые без прозрачности — приглушаем, остальные не трогаем.


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


В будущем от таких проблем должна защитить дополнительная эвристика, которую я как раз и называю «спектральный анализ» — считаем количество разных цветов на картинке и инвертируем, только если их мало. Этим же критерием можно искать графичные светлые картинки и их тоже перекрашивать — звучит заманчиво.



Итог


Для полноценной тёмной темы в почте нам не хватало перекраски писем со стилями, и мы выяснили, как это устроить. Два простых варианта на чистом CSS — переопределение стилей и CSS-фильтр — не подошли: первый слишком сурово обходится с исходным дизайном, второй просто плохо работает. В итоге мы используем адаптивное затемнение — разбираем стили, заменяем цвета более подходящими и собираем обратно. Сейчас работаем над расширением темы на картинки — для этого нужно анализировать их содержимое и перекрашивать только некоторые.


Если вам когда-нибудь потребуется перекрашивать произвольный пользовательский HTML под тёмную тему, держите в голове три метода:


  • Переопределение стилей — нужно в любом случае для вашего основного приложения, дёшево и сердито, но убивает все исходные цвета.
  • CSS-фильтр — прикольно, но работает так себе. Используйте только для непрозрачных (в смысле доступа) элементов вроде фреймов или веб-компонентов.
  • Преобразование стилей — затемняет очень качественно, но сложнее других методов.

Даже если вы никогда не будете этим заниматься, надеюсь, вы интересно провели время!


Полезные ссылки:


  1. Если вам интересно обсудить эту тему вживую и в контексте разработки под Android, то приглашаем в гости 18 апреля в петербургский офис Яндекса.


  2. Недавно мы рассказывали о решении другой проблемы пользователей почты — проблемы рассылок.


Tags:
Hubs:
+51
Comments 49
Comments Comments 49

Articles

Information

Website
www.ya.ru
Registered
Founded
Employees
over 10,000 employees
Location
Россия