Цветовая схема без помощи дизайнера


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

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

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

    Задача


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

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

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


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

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



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

    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-переменных, которыми затем пользуемся для раскрашивания нашего проекта.

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

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

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

    Спасибо за внимание.
    ДомКлик
    Место силы

    Комментарии 10

      0
      Что-то Ваша демка не работает в Firefox
      Заголовок спойлера
      image
        +1
        Спасибо за багрепорт, поправим.
          +3
          Исправили, спасибо еще раз
          0
          Можно ещё по картинке с красивым сочетанием генерить базовые в сервисах типа этого.
            0
            Можно конечно, но тут смысл в том чтоб это было динамически прям в приложении. За ссылку спасибо.
            0
            colourlovers мне нравится, особенно для поиска базового цвета
              –1
              Особо не вникал в статью, но просто интересно — ваше решение чем-то отличается от онлайн-сервисов типа таких?:
              www.colorhexa.com
              palx.jxnblk.com
                +1
                Эти сервисы не дают возможность делать это прям в проекте, плюс тут еще есть поддержка темной темы
                –1
                Не могли бы вы объяснить, какой в этом смысл, если подбирается всего 2 цвета, причём один — вручную?
                  +1
                  А почему вы решили что всего 2? Это на демке я показал 2, но даже там есть еще инверсия цветов. Смысл в том что за 20 минут я перевел на темную тему сервис который вообще для этого не подходил. Спасибо за вопрос

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

                Самое читаемое