Произвольные красивые цвета

22 февраля 2023

RGB какой-то отстой. 

Модель RGB, мало чем отличающаяся от ASCII, адресов памяти и наличия 86 400 секунд в сутках, является одной из тех инструментов, которые немного упрощают программирование, до поры до времени.

Теоретически RGB — это группа цветовых пространств, которая позволяет указать дисплею, какое напряжение требуется каждому субпикселю. Однако на практике теперь у нас есть телефоны с дисплеями, которые позволяют отображать более 100% красного цвета, что является новым типом красного цвета, называемым суперкрасным. У нас есть другие дисплеи, в которых синего в два раза больше, чем красного или зелёного. И, вероятно, уже некоторое время ваши значения RGB не соответствуют напряжениям дисплея.

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

Наконец, RGB трудно настраивать. Если вы начнёте с чёрного, вы можете увеличить количество “red” (красного) в палитре цветов RGB, что сделает всё более красным. Всё идет нормально. Затем вы начинаете увеличивать “green” (зелёный), и вы получаете… жёлтый? Это не очень интуитивно понятное цветовое пространство для навигации. Есть и другие представления цветов, которые легче поддаются изменению.
 

 

Цвета на годы

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

Мне необходимы некоторые цвета, которые:

   а) произвольны при генерации кода; 
   б) красиво выглядят; 
   в) определяются исключительно целым числом года. 

Нам требуется реализовать такую функцию:

func color(for year: Int) -> Color

 

RGB действительно может удовлетворить только первому из моих критериев — эта модель может создавать случайные цвета со случайными числами:

Color(red: .random(in: 0..<1), blue: .random(in: 0..<1), green: .random(in: 0..<1))

 

К сожалению, цвета, сгенерированные таким образом, выглядят очень плохо. Они часто получаются грязными и румяными, а создание более чем одного цвета не сопровождается каким-либо узором или структурой. Цвета повсюду.

Это структурная проблема с RGB. RGB фокусируется на том, как создается цвет, а не на том, как он воспринимается.

К счастью, решение этой проблемы хорошо задокументировано. Есть несколько сообщений в блогах и постах (предупреждение: JavaScript), в которых излагается подход. Идея такова: используя цветовое пространство, основанное на оттенках, такое как HSL, вы можете сохранять два параметра постоянными saturation и  lightness (насыщенность и яркость) и изменять только оттенок, давая вам несколько цветов, которые живут в одном и том же “family” (семействе).

(Существуют тонкие различия между HSL, HSB, HSV и HWB, но чередование оттенков в основном одинаково во всех цветовых моделях, и любая из них будет хорошо работать с этой техникой.)

Например, использование значения 0.8 для saturation и lightness дает хорошие пастельные тона:

Color(hue: .random(in: 0..<360), saturation: 0.8, lightness: 0.8)

 

Вы можете играть с этой палитрой цветов; переместите ползунок “hue” (оттенок), чтобы увидеть множество цветов в этом семействе.
С другой стороны, значения 0.6 для saturation и 0.5 для lightness дают более насыщенные цвета:

Color(hue: .random(in: 0..<360), saturation: 0.6, lightness: 0.5)

 

 

Эта палитра цветов показывает примеры этих цветов.

Проницательные читатели заметят, что в то время как собственные API-интерфейсы Apple принимают число от 0 до 1, этот фальшивый инициализатор, который я сделал, ожидает оттенок от 0 до 360. Я нахожу это более наглядным, потому что это значение представляет собой некоторое количество градусов. Здесь есть физическая аналогия с кругом оттенков. Круги зацикливаются сами на себе, поэтому 3590 в основном того же цвета, что и 10. Это позволяет вам выйти за пределы круга оттенков и изменить его на 3600, чтобы вернуться к разумному цвету.

Это позволяет нам реализовать большую часть нашей функции color(for year: Int):

func color(for year: Int) -> Color {
    let spacing = ...
    return Color(hue: (year * spacing) % 360, saturation: 0.8, lightness: 0.5)
}

 

Интервал представляет собой количество градусов, которое необходимо пройти по кругу  оттенков каждый раз, когда нам необходимо выбрать следующий цвет.

Какое оптимальное число выбрать здесь?
 

 

Вращение в пространстве оттенков

Если мы сделаем этот угол слишком близким к нулю, цвета будут располагаться слишком близко друг к другу на круге оттенков, что сделает их слишком похожими. Однако, если мы сделаем это слишком близко к значению 3600 (полный оборот), после изменения градусов на 360 они всё равно будут слишком похожи, за исключением того, что они будут идти назад по кругу оттенков. Может быть, мы хотим попробовать 1800 ? Это делает все остальные цвета абсолютно одинаковыми, так что это тоже не совсем правильно.

На самом деле, любое вращение, которое равномерно делится на 3600, через некоторое время приведет к повторению. А у 360 много факторов!

Одно из решений состоит в том, чтобы разложить компоненты на 360 значений, разделенных на количество лет, которые у нас есть, и тогда цвета будут меняться каждый раз, когда наступает новый год. Это создает радугу, которая, хотя и выглядит красиво, не имеет случайного представления, к которому я стремлюсь. 

Однако есть лучший способ сделать это, и ответ содержится в видео на YouTube, которое я смотрел более 10 лет назад. Замечательный Ви Харт (Vi Hart) опубликовал серию видеороликов (раз, два, три) о том, как растениям необходимо отрастить свои новые листья таким образом, чтобы они не были заблокированы верхними листьями, что позволяет им получать максимум солнечного света. Во втором видео из этой серии есть соответствующий бит.

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

Поскольку любое рациональное число приведет к повторяющимся цветам или перекрывающимся листьям, она ищет иррациональное число. В идеале “самое” иррациональное число. Она находит его в ϕ, это примерно 1.618. Нам необходимо проходить 1/1.618 часть круга оттенков каждый раз, когда нам требуется новый цвет, и это даст нам необходимые цвета.

func color(for year: Int) -> Color {
    let spacing = 360/1.618
    return Color(hue: (year * spacing) % 360, saturation: 0.8, lightness: 0.5)
}


Если цвета вам не нравятся, вы можете дополнительно повернуть, добавив фазовый сдвиг в уравнение:

func color(for year: Int) -> Color {
    let spacing = 360/1.618
    return Color(hue: 300 + (year * spacing) % 360, saturation: 0.8, lightness: 0.5)
}

 

Эта функция соответствует нашим критериям, цвета, которые получаются из неё: 

   а) произвольны; 
   б) выглядят неплохо; 
   в) определяются исключительно годом.
 

 

Следующий шаг

Если ваша единственная цель — несколько простых цветов для прототипа или побочного проекта, то того, что я рассказал до сих пор, будет достаточно. Но если вы хотите использовать это в более серьёзных и масштабных приложениях, вы можете сделать ещё один шаг.

Цветовая модель HSL имеет несколько серьезных недостатков. Она, как и RGB, была разработана для простоты вычислений, а не для точности базовых цветов. В частности, при повороте значения оттенка (что мы и делаем с помощью этой техники) вы обнаружите, что некоторые оттенки окрашены намного светлее, чем другие, даже если saturation и lightness остаются постоянными. Эти цвета выглядят светлее, хотя технически они являются одной и той же “lightness”.

Цветовое пространство LCH (luminance, chroma, hue) решает эту проблему. Насколько я могу судить, это золотой стандарт цветов на дисплее. Это даёт вам единообразие восприятия, что позволяет вам поворачивать оттенок и получать цвета, которые даже больше похожи друг на друга, чем вы могли бы получить с помощью HSL. Это также даёт некоторые преимущества, когда дело доходит до контраста при чтении текста.

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

Здесь вы можете поиграть с палитрой цветов LCH. Чтобы заставить LCH работать с UIColor, вы можете использовать эти четыре полезных принципа.
Использование LCH для создания моих цветов с помощью описанной выше техники вращения оттенков дало красивые цвета.

func color(for year: Int) -> Color {
    let spacing = 360/1.618
    return Color(luminance: 0.7, chroma: 120, hue: 300 + (year * spacing) % 360)
}


Этот браузер не поддерживает цвет LCH. Попробуйте Safari или мобильную версию Safari 15 или выше.

Все эти цвета имеют одинаковую lightness, и они отлично смотрятся для чего-то полностью процедурно сгенерированного. Они яркие, однородные и замечательные.

Модель, которую вы выбираете для жизни, создает ограничения, которые вы, возможно, не предполагали ограничивать. Любой цвет из любого из этих цветовых пространств может быть (более или менее) переведён в любое другое цветовое пространство с небольшой разницей. Поэтому цвета, которые мы получили в итоге, могут быть записаны в терминах красных, зеленых и синих значений (опять же, здесь махнув немного рукой). Но хотя RGB может представлять эти цвета, это не означает, что вы можете легко перемещаться по пространству таким образом, чтобы получить цвета, которые хорошо смотрятся вместе. Выбор правильного цветового пространства для начала делает проблему, по крайней мере, решаемой.

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

В конце концов, стремление растений к солнечному свету стало ключом к созданию приятных цветов для моей маленькой диаграммы.
 

Оригинал статьи

Содержание