Наверняка многие из вас бывали в ситуации, когда нужно быстро подобрать цвета для оформления, а дизайнер занят более важными задачами, в плохом настроении или в отпуске. Задача несложная, но иногда ответа приходится ждать по несколько дней.
Меня зовут Эмиль Фролов, я техлид в команде внутренних сервисов в ДомКлике. В этой статье я расскажу, как родилась идея библиотеки, которая теперь сильно экономит время при выборе цветов. Решение простое, но очень полезное, берите на вооружение.
Особенно, если нужно подобрать цвета для тёмной схемы.
Задача
В ДомКлик есть корпоративный портал, каждый раздел которого имеет свою цветовую схему. Раньше для создания нового раздела каждый раз приходилось мучить дизайнеров и просить их подобрать новый набор цветов и перенести в интерфейс. Получалось огромное количество лишнего кода, тратили кучу времени на переписку, ожидание и согласования. Очень хотелось упростить и ускорить весь процесс.
Поискали в интернете готовые решения, но ничего подходящего не нашлось. И тогда решили написать библиотеку: вводишь в неё цвет (брендовый), который даёт дизайнер, а библиотека подбирает еще несколько подходящих цветов. Также нам хотелось, чтобы библиотека генерировала ещё и тёмные цветовые схемы.
Это не рецепт счастья, а, скорее, идея, которую каждый сможет развить в рамках своего проекта. Небольшую демку можно посмотреть тут.
Идея и решение
Подумали, как это можно сделать. Начали с алгоритма генерирования цветов на основе базового. В этих ваших интернетах снова ничего готового не нашли. Зато нашли библиотеку, которая может менять разные параметры цвета.
Многие знают что есть множество разнообразных алгоритмов подбора цветов:
Расписывать их не вижу смысла, это уже сделали сотни раз до меня. Но есть несколько ключевых моментов:
- Для нас они избыточны.
- Хотелось подбирать цвета под себя.
Поэтому слившиеся в едином порыве дизайнер и разработчик решили единожды подобрать схему вручную.
Для начала описали базовые цвета:
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-переменных, которыми затем пользуемся для раскрашивания нашего проекта.
Больше всего времени ушло на подбор цветовой схемы. Пришлось вручную перебирать кучу разных параметров, чтобы цвета сочетались друг с другом. Зато теперь на каждой итерации подбора цветов для новых разделов портала мы экономим по несколько дней.
Поскольку дизайнер принимал участие в создании алгоритма, ещё не было случая, чтобы он был недоволен сгенерированными библиотекой цветами. Да и цветовые схемы не слишком велики, в них трудно ошибиться.
Ещё раз подчеркну, что мы не претендуем на единственно верное решение. Эта идея реализована, и она работает хорошо. Я постарался донести до вас основные моменты, а детали и масштабы реализации зависят только от вас и вашего проекта.
Спасибо за внимание.
san-smith
Что-то Ваша демка не работает в Firefox
emilfrolov Автор
Спасибо за багрепорт, поправим.
emilfrolov Автор
Исправили, спасибо еще раз
Zifix
Можно ещё по картинке с красивым сочетанием генерить базовые в сервисах типа этого.
emilfrolov Автор
Можно конечно, но тут смысл в том чтоб это было динамически прям в приложении. За ссылку спасибо.
eri
colourlovers мне нравится, особенно для поиска базового цвета
noodles
Особо не вникал в статью, но просто интересно — ваше решение чем-то отличается от онлайн-сервисов типа таких?:
www.colorhexa.com
palx.jxnblk.com
emilfrolov Автор
Эти сервисы не дают возможность делать это прям в проекте, плюс тут еще есть поддержка темной темы
fire_engel
Не могли бы вы объяснить, какой в этом смысл, если подбирается всего 2 цвета, причём один — вручную?
emilfrolov Автор
А почему вы решили что всего 2? Это на демке я показал 2, но даже там есть еще инверсия цветов. Смысл в том что за 20 минут я перевел на темную тему сервис который вообще для этого не подходил. Спасибо за вопрос