# План: Django микросайт для генерации гипно-SVG ## Стек - **Django 5.x** — бэкенд - **HTMX** — интерактивность без перезагрузки - **Pillow** — обработка изображений, color clustering - **scikit-learn** — k-means для дуотон/трейтон/квантование - **numpy** — векторизованные вычисления - **Gunicorn** — WSGI-сервер - **Poetry** — управление зависимостями ## Структура проекта (как в cadpoint-ru) ``` 2026-hypn0-site/ ├── pyproject.toml # Poetry — общие зависимости ├── poetry.lock ├── .env.sample # Переменные окружения ├── .env # Локальные секреты (не в VCS) ├── Dockerfile # Многостадийная сборка ├── docker-compose.local.yml # Dev: bind-mount, --reload ├── docker-compose.prod.yml # Prod: образ из registry │ ├── config/ │ └── nginx/ # Nginx-конфиги (хост, не в Docker) │ └── hypn0-app--external-nginx.conf │ ├── public/ # Статика + медиа (shared dev/prod) │ ├── static/ │ │ ├── css/ │ │ │ └── site.css # Tailwind CDN + кастомные стили │ │ └── js/ │ │ └── site.js # Alpine/минимальный JS │ └── media/ │ ├── temp/ # Публичные SVG (TTL 14 дней) │ └── gallery/ # Отфильтрованные лучшие │ ├── database/ # SQLite БД │ └── .gitignore │ ├── hypn0/ # Django project root │ ├── manage.py │ ├── hypn0/ # Settings package │ │ ├── __init__.py │ │ ├── settings.py # django-environ для конфига │ │ ├── urls.py │ │ ├── wsgi.py │ │ └── templates/ │ │ ├── base.html │ │ └── errors/ │ │ ├── 404.html │ │ └── 500.html │ │ │ └── halftone/ # Django app │ ├── __init__.py │ ├── urls.py │ ├── views.py # главная + генерация │ ├── forms.py # формы параметров │ ├── models.py # FeaturedSVG + votes │ ├── services/ │ │ ├── __init__.py │ │ └── halftone.py # ядро из main.py │ ├── templates/ │ │ └── halftone/ │ │ └── index.html │ └── management/ │ └── commands/ │ ├── cleanup_temp.py # очистка temp (14 дней) │ └── cleanup_reports.py # удаление по жалобам │ ├── .gitea/workflows/ # CI/CD │ └── docker-publish.yaml │ └── main.py # [удаляем] ``` ## Ключевые принципы (перенос из cadpoint-ru) | Принцип | Реализация | |---------------|------------------------------------------| | Django-проект | `hypn0/` — подкаталог в корне | | Статика/медиа | `public/static/` и `public/media/` | | БД | `database/db.sqlite3` | | Конфиги | `config/nginx/` — внешний nginx | | Docker | многостадийный, bind-mount для dev | | Зависимости | Poetry, `pyproject.toml` в корне | | Env | `django-environ`, `.env.sample` → `.env` | ## Архитектура приложения ### 1. Рефакторинг ядра (`hypn0/halftone/services/halftone.py`) Извлечь из `main.py` чистую функцию: ```python def generate_halftone_svg( image: Image.Image, *, cols: int = 80, max_radius: int = 8, color: str = "#800000", opacity: str = "90", # hex alpha для CSS animation_variants: int = 12, ) -> str: # возвращает SVG-строку ``` **Что убираем из CLI-версии:** - Аргументы `image_path` / `output_path` - Форматирование чисел для CSS-переменных - Запись в файл - `print()` ### 2. View-слой (`hypn0/halftone/views.py`) ```python # Главная — GET → рендерим index.html # Генерация — POST (HTMX) → возвращаем SVG фрагмент # Скачивание — GET /download/{temp_id} → отдаём файл + удаляем исходник # Просмотр публичного — GET /view/{hash} → отдаём SVG из temp/ # Галерея — GET /gallery → список FeaturedSVG # Просмотр в галерее — GET /gallery/{hash} # Report — POST /report/{hash} → increment reports_count # Promote (admin) — админ переносит SVG из temp → gallery ``` ### 3. Фронтенд (`hypn0/halftone/templates/halftone/index.html`) ``` ┌──────────────────────────────────────────────┐ │ 📷 [Загрузить изображение] │ │ hx-post="/generate" │ │ hx-target="#preview" │ ├──────────────────────────────────────────────┤ │ Ширина колонок: [80] │ │ Макс. размер: [8] │ │ Цвет: [#800000] │ │ Анимация: [✓] │ │ Опубликовать: [ ] — показать ссылку │ ├──────────────────────────────────────────────┤ │ [Сгенерировать] │ ├──────────────────────────────────────────────┤ │
│ │ │ └──────────────────────────────────────────────┘ ``` Важно: данная форма -- черновик. Настроек генерации будет больше: - Фигуры (не только кружки, но и квадратики, кольца, треугольники, буквы-цифры, линии (переменной толщины), волнистые линии. - Наклон сетки (угол) - Цветовые модели (дуотон/трейтон с разным наклоном сетки для каждого цвета как в полиграфии, полноцвет) - Возможно разные настройки анимации (групп) Рядом с панелью настроек, превью ("на лету") и кнопка скачать. Галерея отобранных изображений. Под картинками в галере кнопки "понравилось" и "подаваться" ## СЕКЦИЯ B: Алгоритмы и фигуры ### F: Типы фигур | ID | Фигура | Параметры | SVG-реализация | |------------|------------------------|------------------------------|-------------------------------------------------| | `circle` | Круг | `max_radius` | `` | | `square` | Квадрат | `max_size` | `` | | `ring` | Кольцо | `max_radius`, `stroke_width` | `` | | `diamond` | Ромб | `max_size` | `` | | `star` | Звезда 5-лучевая | `outer_r`, `inner_r` | `` | | `cross` | Крест | `max_size`, `arm_width` | `` | | `line-h` | Горизонтальная полоска | `max_width`, `thickness` | `` | | `line-v` | Вертикальная полоска | `max_height`, `thickness` | `` | | `line-d` | Диагональная полоска | `max_length`, `thickness` | Повернутый `` на angle | | `wave` | Волна | `amplitude`, `frequency` | `` | | `hex` | Шестигранник | `max_radius` | `` (6 вершин) | | `triangle` | Треугольник | `max_size` | `` | | `typeface` | Буква/цифра | `font_family`, `font_size` | `...` | **Сложность реализации:** - **Быстро** (фигура по центру, один параметр масштаба): circle, square, ring, diamond, hex, triangle - **Средне** (дополнительные параметры): line-h/v/d, cross, star, wave - **Сложно** (загрузка шрифта): typeface ### G: Типы сеток | ID | Описание | Параметры | Сложность | |-------------|-----------------------------------|---------------------|-----------| | `grid` | Прямая прямоугольная (как сейчас) | `cols`, `rows` | — | | `rotated` | Прямая сетка с наклоном | `cols`, `angle_deg` | Низкая | | `hex_grid` | Шестиугольная сетка | `cols`, `radius` | Средняя | | `isometric` | Изометрическая проекция 60° | `cols` | Средняя | | `polar` | Полярные координаты | `rings`, `spokes` | Высокая | | `bend` | Изогнутая сетка | `cols`, `curve` | Высокая | **Реализация `rotated`:** ```python angle = math.radians(angle_deg) cos_a, sin_a = math.cos(angle), math.sin(angle) rotated_x = x * cos_a - y * sin_a rotated_y = x * sin_a + y * cos_a ``` ### P: Цветовые модели | ID | Описание | Параметры | Сложность | |----------------|-----------------------|-------------------------------|-----------| | `single` | Один цвет | `color_hex`, `opacity` | — | | `duotone` | 2 цвета | `color_a`, `color_b`, `blend` | Средняя | | `tritone` | 3 цвета | `colors[3]`, `blend` | Средняя | | `quad` | 4 цвета | `colors[4]`, `blend` | Средняя | | `palette_auto` | Авто-палитра n цветов | `palette_name`, `n` | Низкая | | `fullcolor` | Квантование цветов | `quantization_levels` | Высокая | **Реализация `duotone/tritone/quad`:** ```python from sklearn.cluster import KMeans pixels = image.convert('RGB').resize((100, 100)).getdata() kmeans = KMeans(n_clusters=n, random_state=42, n_init=1) kmeans.fit(pixels) centers = kmeans.cluster_centers_ # RGB-координаты кластеров # Каждый пиксель маппим на ближайший кластер ``` **Реализация `fullcolor`:** ```python # Median-cut квантование pixels = image.quantize(colors=256) # или custom median-cut ``` ### Q: Анимации | ID | Описание | Параметры | |-----------|----------------------------------|-----------------------------------------------| | `pulse` | Пульсация (как сейчас) | `speed`, `variants`, `scale_min`, `scale_max` | | `rotate` | Медленное вращение (покачивание) | `speed`, `direction` (cw/ccw) | | `wave` | Волна по вертикали (покачивание) | `amplitude`, `frequency`, `speed` | | `sparkle` | Мерцание fade in/out | `variants`, `speed_range` | | `flow` | Поток в направлении | `direction`, `speed` | | `breathe` | Всё вместе "дышит" | `speed`, `scale_range` | | `static` | Без анимации | — | **Реализация:** - Каждая группа (CSS-класс) получает свой `@keyframes` + `animation-delay` - N ключевых кадров в ` │ │ │ │●│┌─┐┌─┐┌─┐ │ │ │ │ │ └─┘└─┘└─┘└─┘ │ │ │ │ │ grid │ │ [В клипборд] [Скачать] │ │ │ │ └─────────────────────────────┘ │ │ ─── Цвет ────── │ │ │ ┌─┐┌─┐┌─┐┌─┐ │ │ │ │●│┌─┐┌─┐┌─┐ │ │ │ └─┘└─┘└─┘└─┘ │ │ │ single │ │ │ │ │ │ ─── Параметры ─ │ │ │ Колонки: [80]│ │ │ Макс. размер: [8]│ │ │ Цвет: #[800000]│ │ │ Прозрачность:[.5]│ │ │ Угол сетки: [0°]│ │ │ Анимация:[▼pulse]│ │ │ │ │ │ ───────────────── │ │ │ │ │ │ [Сгенерировать] │ │ │ │ │ ├───────────────────┴───────────────────────────────────┤ │ ───────────── Галерея ───────────── │ │ [thumb] [thumb] [thumb] [thumb] ... │ │ [♥ 12] [♥ 8] [♥ 23] [♥ 5] │ └───────────────────────────────────────────────────────┘ ``` ### Табы настроек - На десктопе: 2 колонки — слева настройки, справа превью - На мобильном: вертикально — превью сверху, настройки снизу - Табы переключают секции: "Стиль / Сетка / Цвет / Параметры" - HTMX: `hx-trigger="change"` на контролах → превью обновляется без перезагрузки ### Галерея - `GET /gallery` — CSS Grid сетка миниатюр (3-4 колонки) - Каждая карточка: SVG-превью (inline), кнопка ♥ (HTMX vote), кнопка поделиться - `POST /gallery/{hash}/vote` — HTMX запрос, обновляет ♥ счётчик - Пагинация: 24 карточки на странице - `GET /gallery/{hash}` — полноэкранный просмотр SVG с кнопками "Скачать" и "Поделиться" #### ВАЖНО: Оптимизация производительности рендеринга галереи (SVG-превью) - **Проблема:** Десятки карточек с тысячами анимированных SVG-элементов могут перегрузить браузер клиента (CPU/GPU при одновременной перерисовке). - **Решение для витрины галереи:** - Карточки галереи выводятся через тег `` (изолированный контекст рендеринга). - Для предотвращения постоянного пожирания ресурсов анимация в SVG по умолчанию спит (`animation-play-state: paused`) и оживает только при наведении (`:hover { animation-play-state: running; }`). - Дополнительно на контейнерах карточек используется CSS-свойство `content-visibility: auto` для отключения рендера невидимых вне экрана карточек. - **Выдача пользователю («Копировать» и «Скачать»):** - Пользователю всегда отдается «чистый» полнофункциональный SVG со стандартно запущенной анимацией (без ограничений `animation-play-state: paused`). - На бэкенде/сервисе при сохранении для галереи модификатор паузы/ховера может вшиваться по умолчанию либо подставляться/вырезаться на лету при выдаче пользователю. **CSS:** - Dev: Tailwind CDN (``) + Purge content в конфиге - Prod: `collectstatic` → `whitenoise` обслуживает скомпилированную статику HTMX: форма с `hx-post`, `hx-encoding="multipart/form-data"` → SVG вставляется в `#preview`. ### 4. Политика хранения, приватность и модели #### 4.1. Принципы приватности и Zero-PII (Без регистрации) - **Полное отсутствие регистрации для посетителей:** Пользователи не создают аккаунты, не вводят email, пароли или личные данные. Единственный пользователь в `auth.User` — администратор сайта для входа в Django Admin. - **Исходные растровые изображения (JPG/PNG/WEBP):** Обрабатываются исключительно в оперативной памяти (через буфер `BytesIO`) и **не сохраняются на диск сервера вообще**. - **Лайки и учет голосов:** - **Бэкенд:** Голоса учитываются анонимно через связку `(item_id, fingerprint)`. Поле `fingerprint` формируется как необратимый SHA-256 хэш: `sha256(IP + User-Agent + SECRET_KEY)`. Это защищает от примитивной накрутки через очистку кук или инкогнито и не сохраняет персональные данные (Zero-PII). - **Фронтенд:** Для исключения переполнения cookies и сетевого оверхеда список лайкнутых картин хранится локально на клиенте в **`localStorage`** (до 5–10 МБ). На базе Alpine.js выполняется мгновенная индикация проголосованных работ. - **Соответствие GDPR / 152-ФЗ:** Персональные данные не собираются и не хранятся. #### 4.2. Генерация названий (AI & Fallback Title) У каждой сгенерированной картины обязательно должно быть название (title): - **Основной способ:** Вызов через **OpenRouter API** с отправкой сильно сжатой миниатюры (thumbnail) исходного изображения в мультимодальную LLM (VLM) для распознавания сюжета и генерации забавного гипно-названия (в стиле Гипножабы/мемов). - **Резервный способ (Fallback):** Если API OpenRouter недоступен, закончились квоты или таймаут — название генерируется локально из случайного набора гипнотических слов, эпитетов и фраз («Мерцающий транс #42», «Глаз Гипножабы», «Астральный вихрь» и т.п.). #### 4.3. Стратегия хранения и умной очистки (Smart Retention) Проект не выступает постоянным файлообменником или бесплатным CDN. Пользователь скачивает сгенерированный SVG для личного использования. Для оптимизации диска на сервере используется динамическая очистка на основе рейтинга популярности («гравитации»), а не слепой 14-дневный таймер: $$\text{Score} = \frac{\text{Likes} + \text{Bonus}_{\text{moderator}}}{(\text{Age in hours} + 2)^\gamma}$$ - **Квота хранилища:** Задается жесткий лимит объема (например, 500 МБ или N тысяч файлов). - **Очистка (`manage.py cleanup_storage`):** При превышении лимита удаляются файлы с наименьшим `Score`. - **Иммунитет к удалению:** Работы, отмеченные модератором (`is_curated=True` / `Bonus`), и закрепленные промо-картины сохраняются бессрочно. #### 4.4. SEO, шеринг и коммерциализация - **Вшивание ссылки в SVG (Zero-cost Branding):** В скачиваемый SVG на этапе генерации добавляется мета-комментарий со ссылкой на генератор (`` и ``). - **Страница просмотра (`/v/`):** Для внешнего шеринга и индексации поисковиками генерируется легкая HTML-страница с OpenGraph разметкой и кнопкой «Открыть настройки в генераторе». - **Возможность промо/коммерциализации:** Поддержка опциональных полей (промо-ссылка, бейдж автора/спонсора, счетчик кликов). #### 4.5. Блог, статические страницы и типографика (HTML + `etpgrf`) - **Отказ от конвертации Markdown «на лету»:** Вместо оверхеда с парсингом Markdown статьи и инфо-страницы хранятся напрямую в формате **HTML**. - **Очистка HTML в админке:** При вводе/сохранении в Django Admin контент проходит валидацию и санитизацию (очистку от вредоносного или мусорного HTML/инлайн-стилей). - **Типографирование через `etpgrf`:** Для безупречной верстки текста (неразрывные пробелы, висячая пунктуация, кавычки-елочки, тире) используется библиотека типографирования **`etpgrf`**. #### 4.6. Архитектура моделей ```python class HalftoneItem(models.Model): """ Единая модель для генераций, публичных шеров и витрины галереи. """ hash_id = models.CharField(max_length=16, unique=True, db_index=True) title = models.CharField(max_length=150, verbose_name="Название картины") svg_file = models.FileField(upload_to="svg/%Y/%m/") # Снимок параметров для возможного клонирования настроек params_json = models.JSONField(default=dict, blank=True) file_size = models.PositiveIntegerField(default=0, help_text="Размер файла в байтах") # Популярность и модерация likes_count = models.PositiveIntegerField(default=0, db_index=True) views_count = models.PositiveIntegerField(default=0) reports_count = models.PositiveIntegerField(default=0) score = models.FloatField(default=0.0, db_index=True, help_text="Рейтинг для Smart Retention") is_curated = models.BooleanField(default=False, help_text="Выбор модератора / галерея (иммунитет к удалению)") is_public = models.BooleanField(default=True, help_text="Доступность по прямой ссылке") # Промо-блок (на перспективу) promo_url = models.URLField(blank=True, default="", verbose_name="Промо-ссылка") promo_title = models.CharField(max_length=100, blank=True, default="", verbose_name="Бейдж/автор") promo_clicks = models.PositiveIntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True, db_index=True) class HalftoneVote(models.Model): """ Анонимный учет голосов (лайков) без сохранения ПДн. """ item = models.ForeignKey(HalftoneItem, on_delete=models.CASCADE, related_name="votes") fingerprint = models.CharField( max_length=64, db_index=True, help_text="SHA256(IP + User-Agent + SECRET_KEY) для базовой защиты от накрутки" ) created_at = models.DateTimeField(auto_now_add=True) class Meta: constraints = [ models.UniqueConstraint(fields=["item", "fingerprint"], name="unique_item_fingerprint_vote") ] class BlogPost(models.Model): """ Статьи блога, документация и инфо-страницы (Privacy Policy и др.). + виртуальные поля для управления типографом etpgrf """ title = models.CharField(max_length=200, help_text="HTML-заголовок, очищенный и типографированный etpgrf") slug = models.SlugField(max_length=200, unique=True) summary = models.TextField(max_length=500, blank=True, help_text="HTML-контент тизера, очищенный и типографированный etpgrf") content = models.TextField(help_text="HTML-контент статьи, очищенный и типографированный etpgrf") cover_image = models.ImageField(upload_to="blog/covers/%Y/", blank=True, null=True) is_published = models.BooleanField(default=False) published_at = models.DateTimeField(null=True, blank=True, db_index=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) ``` ## Шаги реализации ### Шаг 1. Структура каталогов Создать: - `hypn0/` — Django project root - `hypn0/hypn0/` — settings package - `hypn0/halftone/` — Django app - `public/` — static + media - `public/media/temp/` — публичные SVG (TTL 14 дней) - `public/media/gallery/` — лучшие SVG - `database/` — БД - `config/nginx/` — nginx config - `.gitea/workflows/` — CI/CD ### Шаг 2. Зависимости в `pyproject.toml` Добавить: ```toml [tool.poetry.dependencies] django = "^5.2" django-environ = "^0.13" gunicorn = "^25.3" whitenoise = "^6.12" django-htmx = "^1.21" ``` ### Шаг 3. Django settings (`hypn0/hypn0/settings.py`) ```python import environ env = environ.Env(DEBUG=(bool, False)) environ.Env.read_env(os.path.join(BASE_DIR.parent, '.env')) DEBUG = env('DEBUG') SECRET_KEY = env('SECRET_KEY') DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR.parent / 'database' / 'db.sqlite3', } } STATIC_ROOT = BASE_DIR.parent / 'public' / 'static' MEDIA_ROOT = BASE_DIR.parent / 'public' / 'media' MEDIA_URL = '/media/' STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' INSTALLED_APPS += ['halftone'] ROOT_URLCONF = 'hypn0.urls' ``` ### Шаг 4. Docker **Dockerfile** (многостадийный, как в cadpoint-ru): - Stage 1 (builder): Poetry → export → pip install - Stage 2 (runtime): gunicorn, non-root user 1000:1000, `collectstatic` **docker-compose.local.yml**: - Bind-mount: `./hypn0`, `./public`, `./database` - Gunicorn `--reload` - Port `8051:8000` **docker-compose.prod.yml**: - Образ из Gitea registry - Host port: `127.0.0.1:8051:8000` - Watchtower для автообновления ### Шаг 5. Nginx-конфиг (`config/nginx/hypn0-app--external-nginx.conf`) Reverse proxy: `hypn0.local` → `127.0.0.1:8051` - Static/media раздается через Nginx (не Django) - GZip, HTTPS (если есть cert) ### Шаг 6. CI/CD (`.gitea/workflows/docker-publish.yaml`) Trigger: tags `v*` → build → push to Gitea registry. Паттерн как в cadpoint-ru. ### Шаг 7. Удаление `main.py` CLI больше не нужен. ## Файлы для создания (итого) | Файл | Назначение | |---------------------------------------------------------|------------------------| | `hypn0/manage.py` | Django CLI | | `hypn0/hypn0/__init__.py` | Settings pkg | | `hypn0/hypn0/settings.py` | Конфиг | | `hypn0/hypn0/urls.py` | URL routing | | `hypn0/hypn0/wsgi.py` | WSGI entry | | `hypn0/hypn0/templates/base.html` | Базовый шаблон | | `hypn0/halftone/__init__.py` | App pkg | | `hypn0/halftone/urls.py` | URL app | | `hypn0/halftone/views.py` | Views | | `hypn0/halftone/forms.py` | Forms | | `hypn0/halftone/models.py` | FeaturedSVG | | `hypn0/halftone/services/halftone.py` | Ядро | | `hypn0/halftone/templates/halftone/index.html` | Главная | | `hypn0/halftone/management/commands/cleanup_temp.py` | Очистка temp (14 дней) | | `hypn0/halftone/management/commands/cleanup_reports.py` | Очистка по жалобам | | `public/static/css/site.css` | Стили | | `public/static/js/site.js` | JS | | `.env.sample` | Env template | | `Dockerfile` | Сборка | | `docker-compose.local.yml` | Dev | | `docker-compose.prod.yml` | Prod | | `config/nginx/hypn0-app--external-nginx.conf` | Nginx | | `.gitea/workflows/docker-publish.yaml` | CI/CD | | `database/.gitignore` | Игнор БД | ## Файлы для модификации 1. `pyproject.toml` — новые зависимости 2. `poetry.lock` — после `poetry lock` ## Файлы для удаления 1. `main.py` — CLI заменён микросайтом ## Верификация 1. `docker compose -f docker-compose.local.yml up` → сайт открывается 2. Загрузка картинки → параметризация → генерация SVG через HTMX 3. SVG отображается в preview с анимацией 4. Скачивание SVG работает 5. `docker compose -f docker-compose.prod.yml` → prod деплой работает 6. CI/CD: тег `v0.1.0` → сборка образа → push ## Производительность: 1 CPU / 1 GB RAM / 10 GB disk **Storage:** - Один SVG: ~50–200 КБ (зависит от cols/max_radius) - 500 публичных SVG в день × 14 дней = ~700 МБ - 10 ГБ — более чем достаточно (галерея + кэш + ОС) **CPU:** - Генерация SVG: Pillow + цикл по пикселям (~6400 точек при cols=80) - Python: ~0.1–0.5 сек на генерацию - Gunicorn: 2–4 воркера → ~10–30 req/min на генерацию - Просмотры галереи: Nginx раздает статику напрямую (минимум нагрузки) **Трафик:** - 100–200 пользователей/день на генерацию — без проблем - Сотни просмотров галереи — без проблем - 500+ генераций/день → вертикальное масштабирование (2 CPU / 2 GB) ## Долгосрочный Roadmap / Отдаленные TODO (Post-MVP) ### 1. Оптимизация веса SVG через 4-символьную HEX-палитру (`#RGB`) - **Цель:** Максимальная компактность генерируемого SVG и инлайновых стилей/анимаций. - **Идея:** Ограничить цветовую палитру короткими 4-символьными HEX-кодами (`#RGB`, например `#CD0`, `#F00`, `#08F` — всего $16^3 = 4096$ цветов или квантованная выборка из 256 цветов). - **Эффект:** Экономия до 3–4 байт на каждом элементе/переменной цвета в SVG, что при тысячах точек дает существенное уменьшение веса итогового файла. - **UI/Фронтенд:** - Реализация компактного интерфейса выбора из ограниченной палитры без раздувания HTML-кода (через динамический генератор палитры на Alpine.js, цветовую сетку с шагом оттенков или компактный модал/попап). - Округление/квантование цвета на клиенте и бэкенде до ближайшего 3-значного hex-значения.