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

Меня зовут Эмиль Фролов, я техлид в команде внутренних сервисов в ДомКлике. В этой статье я расскажу, как родилась идея библиотеки, которая теперь сильно экономит время при выборе цветов. Решение простое, но очень полезное, берите на вооружение.

Особенно, если нужно подобрать цвета для тёмной схемы.

Задача


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

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

Идея и решение


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

Многие знают что есть множество разнообразных алгоритмов подбора цветов:



Расписывать их не вижу смысла, это уже сделали сотни раз до меня. Но есть несколько ключевых моментов:

  1. Для нас они избыточны.
  2. Хотелось подбирать цвета под себя.

Поэтому слившиеся в едином порыве дизайнер и разработчик решили единожды подобрать схему вручную.

Для начала описали базовые цвета:

export const getColors = (projectColor, inverse) => {
  ...
  const BASE_SUCCESS = '#00985f';
  const BASE_WARNING = '#ff9900';
  const BASE_PROGRESS = '#fe5c05';
  const BASE_ALERT = '#ff3333';
  const BASE_SYSTEM = '#778a9b';
  const BASE_NORMAL = '#dde3e5';
  const BASE_WHITE = '#ffffff';
  const BASE_BLACK = '#000';
  const TYPO_BASE_BLACK = '#242629';
  const TYPO_LINK = '#33BDFF';
  ...
}

Отталкиваясь от этого, можно начать творить. Получаем объекты для работы с базовыми цветами.

  import color from 'color-js';

  export const getColors = (projectColor, inverse) => {
    ...
    const baseWhite = color(BASE_WHITE);
    const baseBlack = color(BASE_BLACK);
    const baseTypoBlack = color(TYPO_BASE_BLACK);
    ...
  }

Думаю, нет смысла полностью описывать весь подбор цветов, но для примера приведу пару строчек:

export const getColors = (projectColor, inverse) => {
  ...
  const bgBrand = baseProject;
  const bgHard = baseColor.setLightness(0.4);
  const bgSharp = baseColor.setLightness(0.18);
  const bgStripe = baseColor.setLightness(0.1);
  const bgGhost = baseColor.setLightness(0.07);
  ...
}

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

А каким цветом отображать текст на элементах?

Задача оказалась не такой сложной, как казалось. Для её решения из hex-значения нам нужно получить яркость элемента. Сделали мы это двумя вспомогательными функциями. Первая переводит hex в RGB:

const hexToRgb = (hex) => {
  const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
  const newHex = hex.replace(shorthandRegex, (
    magenta,
    red,
    green,
    blue
  ) => red + red + green + green + blue + blue);

  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(newHex);

  return result ? {
    red: parseInt(result[1], 16),
    green: parseInt(result[2], 16),
    blue: parseInt(result[3], 16)
  } : null;
};

Вторая функция из RGB получает яркость фона и решает, темным будет цвет или светлым:

export const getBgTextColor = (bgColor) => {
  const rgb = hexToRgb(bgColor);
  const light = (rgb.red * 0.8 + rgb.green + rgb.blue * 0.2) / 510 * 100;

  return light > 70 ? '#000000' : '#ffffff';
};

Вы можете подумать, что теперь всё готово к следующему этапу. Но нет, мы ведь ещё хотим поддержку тёмной темы из коробки? Да! Хотим!

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

import color from 'color-js';

export const getColors = (projectColor, inverse) => {
  ...
  const BASE_SUCCESS = '#00985f';
  const BASE_WARNING = '#ff9900';
  const BASE_PROGRESS = '#fe5c05';
  const BASE_ALERT = '#ff3333';
  const BASE_SYSTEM = '#778a9b';
  const BASE_NORMAL = '#dde3e5';
  const BASE_WHITE = '#ffffff';
  const BASE_BLACK = '#000';
  const TYPO_BASE_BLACK = '#242629';
  const TYPO_LINK = '#33BDFF';

  ...

  const baseWhite = color(BASE_WHITE);
  const baseBlack = color(BASE_BLACK);
  const baseTypoBlack = color(TYPO_BASE_BLACK);
  const baseColor = inverse ? baseWhite : baseBlack;
  const typoColor = inverse ? baseWhite : baseTypoBlack;

  ...

  const bgHard = inverse ? baseColor.setLightness(0.4) : baseColor.lightenByAmount(0.85);
  const bgSharp = inverse ? baseColor.setLightness(0.18) : baseColor.lightenByAmount(0.95);
  const bgStripe = inverse ? baseColor.setLightness(0.1) : baseColor.lightenByAmount(0.96);
  const bgGhost = inverse ? baseColor.setLightness(0.07) : baseColor.lightenByAmount(0.99);

  ...
}

Вот и всё. Можем отдать список цветов:

return {
  ...
    // BG
    'color-bg-hard': bgHard.toString(),
    'color-bg-sharp': bgSharp.toString(),
    'color-bg-stripe': bgStripe.toString(),
    'color-bg-ghost': bgGhost.toString(),
    'color-bg-primary': bgDefault.toString(),
  ...
}

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

Следующий вопрос: как этим пользоваться?

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

  const colors = getColors(color, themeKey === 'dark');
  const colorsVars = Object.keys(colors).map((key) => `--${key}: ${customColors[key]}`).join(';');
  
  const link = document.createElement('style');
  const headTag = document.getElementsByTagName('head')[0];

  link.type = 'text/css';
  link.id = 'project-theme-scope';
  const stylesBody = `:root {${colorsVars}}`;

  link.innerText = stylesBody;

  headTag.append(link);

А теперь самое вкусное. Внимание-внимание! Сейчас с помощью нескольких строчек когда мы добавим поддержку тёмной темы для половины элементов:

  body {
    background: var(--color-bg-ghost);
    color: var(--color-typo-primary);
  }

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

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

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

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

Спасибо за внимание.