Я в очередной раз потратил минут двадцать на подбор палитры для нового проекта - сидел, крутил Coolors, листал Pinterest, смотрел на чужие подборки, которые «почти подходят». В какой‑то момент поймал себя на мысли: почему вообще я начинаю с колеса оттенков? Ведь у меня уже есть образ - слово. «Рассвет». «Шторм». «Лакшери». У каждого слова есть цвет, который чувствуется интуитивно, просто никто не переводит это в HEX за тебя.
Так появился Колорит. В этой статье расскажу, как он устроен внутри - промпты для DeepSeek, кластеризация прямо в браузере, и пара нюансов с iOS, которые я не ожидал.

Стек: намеренно скучный
Бэкенд - Node.js + Express, около 150 строк. Фронтенд - чистый HTML/CSS/JS, без Vite, без React, без всего. DeepSeek API для генерации палитр по словам. Деплой на Linux VPS через pm2 и nginx. Базы данных нет - история запросов в sessionStorage, исчезает при закрытии вкладки.
Сознательный выбор. Хотелось поднять за вечер, а не тратить день на конфигурацию сборщика.
«Слово → Цвет»: как заставить LLM возвращать только JSON
Главная проблема при работе с языковыми моделями для получения структурированных данных - модель всё равно хочет поговорить. Отправляешь запрос, а в ответ получаешь «Конечно! Вот палитра для слова рассвет:» и потом уже JSON. Или markdown‑блок с ```json. Или пояснения после массива.
Перебрал несколько вариантов. В итоге работает такой подход:
async function generatePalette(query, count) { const res = await fetch('https://api.deepseek.com/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.DEEPSEEK_API_KEY}` }, body: JSON.stringify({ model: 'deepseek-chat', temperature: 0.8, messages: [{ role: 'system', content: `Ты - эксперт по цвету и дизайну. Возвращай ТОЛЬКО валидный JSON-массив, без markdown, без пояснений. Формат каждого элемента: {"hex":"#RRGGBB","name_ru":"Название","name_en":"Name"}` }, { role: 'user', content: `Создай палитру из ${count} цветов для слова/фразы: "${query}". Цвета должны передавать настроение, ассоциации и смысл слова. Избегай слишком похожих оттенков.` }] }) }); const data = await res.json(); return JSON.parse(data.choices[0].message.content); }
Три вещи, которые здесь важны. Первое - system prompt говорит «ТОЛЬКО JSON» заглавными буквами, это не случайно: вежливая просьба срабатывает хуже. Второе - temperature 0.8, не выше: при 1.0 модель иногда начинает фантазировать с форматом. Третье - фраза про «избегать похожих оттенков» обязательна, без неё можно получить пять оттенков бежевого для слова «осень».
Ответ приходит примерно за 1.5–3 секунды - вполне приемлемо для интерактивного инструмента.
Режим «Фото → Цвет»: кластеризация в браузере
Фотографии я принципиально не хотел отправлять на сервер - ни по соображениям приватности, ни по нежеланию платить за трафик и хранение. Весь алгоритм живёт в браузере, через Canvas API.
function extractColors(imageElement, count) { // Уменьшаем до 200×200 для производительности const canvas = document.createElement('canvas'); canvas.width = canvas.height = 200; const ctx = canvas.getContext('2d'); ctx.drawImage(imageElement, 0, 0, 200, 200); const { data } = ctx.getImageData(0, 0, 200, 200); const pixels = []; // Пропускаем каждый 4-й пиксель для скорости for (let i = 0; i < data.length; i += 16) { const r = data[i], g = data[i+1], b = data[i+2], a = data[i+3]; if (a < 128) continue; // прозрачные - пропускаем pixels.push([r, g, b]); } return kMeansClustering(pixels, count); }
Кластеризация работает в HSL‑пространстве, а не в RGB — перцептивное расстояние между цветами там куда адекватнее. 200×200 пикселей, несколько итераций k‑means — и у нас доминирующие цвета без единого серверного запроса. В браузере это занимает порядка 40 мс, незаметно.

Два места, где я неожиданно застрял
iOS сохраняет в «Загрузки», а не в «Фото»
Казалось бы, PNG - он и есть PNG. Но стандартный download‑атрибут на iPhone кладёт файл в «Загрузки» в Файлах, откуда его никто не ищет. Пользователь нажимает «скачать», потом удивляется, куда делась палитра.
Решение - Web Share API:
async function saveOrShare(canvas) { canvas.toBlob(async (blob) => { const file = new File([blob], 'palette.png', { type: 'image/png' }); if (navigator.canShare?.({ files: [file] })) { await navigator.share({ files: [file], title: 'Палитра' }); } else { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'palette.png'; a.click(); URL.revokeObjectURL(url); } }); }
На iOS теперь вылезает нативный Share Sheet - можно сохранить в Фото, скинуть в Telegram, скопировать. На десктопе всё по‑старому, через download.
Тёмная тема и то, как цвет «ощущается» по‑разному
Это я не ожидал. Одна и та же палитра на тёмном фоне выглядит теплее, чем на светлом — из‑за симультанного контраста. Тот оттенок, который казался уютным кирпичным на сером, на белом фоне вдруг становится холоднее и резче. Пришлось добавить CSS‑переменные с чуть разными значениями для обеих тем и плавный переход transition: background .35s, чтобы переключение не выглядело как удар по глазам.
Итого
Инструмент живёт на https://konstmax.ru/colorit/ Два режима - «Слово→Цвет» и «Фото→Цвет», переключатель RU/EN, история запросов, экспорт PNG. Фронтенд без зависимостей весит ~40 KB суммарно - CSS плюс JS.
Задачу, с которой всё началось, он решает. Открываю, набираю слово - через пару секунд есть отправная точка. Остальное уже руками.
Что дальше: «Слово‑система»
Колорит - первый модуль в задуманной экосистеме «Слово‑система»: набора инструментов, где точкой входа является обычное слово или образ. Слово→Цвет уже есть. Следующие модули в разработке - Слово→Шрифт, Слово→Форма. Идея в том, чтобы ИИ помогал дизайнеру и разработчику на самом раннем этапе - когда есть только ощущение, но нет ещё ни одного пикселя.
Комментарии (12)

d3d14
23.06.2026 03:23Буквально на днях искал такой сервис. Как развитие можно предложить генерацию базового CSS по скриншоту.

wawont
23.06.2026 03:23Тег 'Open Source' не раскрыт в ссылку.

Konstmax13 Автор
23.06.2026 03:23Не очень понял. Сам код не открыт. Приложение бесплатное

DachnikGarik
23.06.2026 03:23Поэтому и не Open Source

Konstmax13 Автор
23.06.2026 03:23А надо open source?

4Nun4ku
23.06.2026 03:23Вот, то что я делал, букавально недавно
https://habr.com/ru/companies/gnivc/articles/1017060/
Konstmax13 Автор
23.06.2026 03:23Круто! Я понимаю что моя идея не новая ))) но попробовать себя в чем то новом всегда интересно
wizard-worker
Крутое решение! На самом деле давно для себя открыл суть вайбпромпта — это когда есть ощущения, но нет конкретной концепции. ИИ хорошо понимает эмоциональную сторону промпта, поэтому часто в таком режиме получаешь гораздо лучшее решение, чем предполагал изначально.
Вайбпромпт идеально сочетается с творчеством: генерацией музыки, изображений, видео и т.п.
Konstmax13 Автор
Спасибо за комментарий!