diff --git a/openspec/changes/archive/2026-10-24-photo-effects-editing/.openspec.yaml b/openspec/changes/archive/2026-10-24-photo-effects-editing/.openspec.yaml new file mode 100644 index 0000000..1f5d280 --- /dev/null +++ b/openspec/changes/archive/2026-10-24-photo-effects-editing/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-24 diff --git a/openspec/changes/archive/2026-10-24-photo-effects-editing/design.md b/openspec/changes/archive/2026-10-24-photo-effects-editing/design.md new file mode 100644 index 0000000..5254a5c --- /dev/null +++ b/openspec/changes/archive/2026-10-24-photo-effects-editing/design.md @@ -0,0 +1,256 @@ +# Design + +## Context + +См. `proposal.md` — «Why». Текущее состояние, важное для подхода: + +- `EditParams` хранится JSON-блобом в `photo.edit_params` (Postgres `jsonb`, + SQLite `TEXT`); роли `edit`, `edit_thumb`, `edit_medium`, `edit_preview` + рендерит фоновый воркер по исходному объекту. Источник истины — Go-рендер. +- Конвейер правок попиксельный: `applyLight`, затем `applyColor`; цвет уже + использует HSL-переход и быстрый путь без него. +- Живой предпросмотр: `WebGLPreview` рисует видимую область одним проходом + фрагментного шейдера; при недоступности WebGL2 — CPU-фолбэк (WASM, затем JS), + который применяет свет и цвет к пикселям кадра. +- `edit_params` — открытый JSON: новые ключи не требуют миграции; фото без + ключа `effects` рендерятся как раньше. Копии фото правки не наследуют. + +Ключевое ограничение: текстура, чёткость и дымка — пространственные операции, +им нужны размытие и локальная статистика. Текущие три реализации (Go, GLSL, +WASM) этого не умеют; виньетка и зерно остаются попиксельными. + +## Goals / Non-Goals + +**Goals:** + +- Полный блок «Эффекты» из макета: текстура, чёткость, дымка, виньетка + (режимы «Свет», «Цвет», «Закарасить»), зерно. +- Живой предпросмотр всех эффектов на WebGL2 и его деградация без WebGL. +- Детерминированный серверный рендер и обратная совместимость `edit_params`. +- Разумная стоимость рендера полноразмерного фото в фоновом воркере. + +**Non-Goals:** + +- Попиксельное совпадение с Lightroom и точное повторение его формул. +- Локальные маски, кисти, градиенты, синхронизация правок между устройствами. +- Пресеты эффектов, экспорт XMP, редактирование на Android. +- Изменение API-контракта правок: по-прежнему `PATCH /photos/:id/edit` с JSON. + +## Decisions + +**1. `EffectsParams` — новый блок в `EditParams`, без миграции БД.** + +```json +{ + "light": { "...": "..." }, + "color": { "...": "..." }, + "effects": { + "texture": 0, + "clarity": 0, + "dehaze": 0, + "vignette": { + "mode": "light", + "amount": 0, + "midpoint": 50, + "feather": 50, + "roundness": 0, + "highlights": 0, + "color": "#000000" + }, + "grain": { "amount": 0, "size": 25, "roughness": 50 } + } +} +``` + +- `mode`: `light` | `color` | `colorize`; неизвестный режим нормализуется в + `light`. `color` — hex-строка `#rrggbb`, нормализуется к нижнему регистру. +- Нормализация заполняет структурные дефолты (midpoint 50, feather 50, size 25, + roughness 50, mode light, color `#000000`) только для полей, отсутствующих в + JSON: явный `0` — валидное значение геометрии и сохраняется. Для этого модель + запоминает факт присутствия поля при разборе (`midpointSet`, `featherSet`, + `sizeSet`, `roughnessSet`). `IsEmpty` игнорирует дефолты при нулевой силе: + пустыми считаются только ненулевые `texture`, `clarity`, `dehaze`, + `vignette.amount`, `grain.amount`. Так `{}` и «дефолты без силы» одинаково + означают отсутствие правок. +- Альтернатива: отдельная таблица/колонка — отвергнута, потребовала бы миграции + и не даёт выгоды для JSON-параметров. + +**2. Порядок рендера: свет → цвет → дымка → текстура → чёткость → виньетка → зерно.** + +- Дымка — глобальная тональная коррекция, её логично применять до локального + контраста; текстура работает с высокими частотами, чёткость — со средними, + поэтому текстура идёт раньше. Виньетка и зерно — финальные операции над уже + сведённым кадром (аналог post-crop в Lightroom). +- Альтернатива: применять эффекты до цвета — отвергнута: цветокоррекция должна + работать с уже очищенным от дымки кадром, иначе дымка «перекрашивает» цвета. +- Порядок одинаков во всех реализациях; расхождение с сервером допустимо только + в деталях предпросмотра. + +**3. Пространственные эффекты считаются в яркости, цвет не смещается.** + +- Для каждого пикселя берётся яркость `Y = 0.299r + 0.587g + 0.114b`; + локальный контраст считается по `Y`, а разница применяется аддитивно ко всем + трём каналам. Это сохраняет оттенок и насыщенность и совпадает с принятым в + проекте подходом «яркость аддитивно». +- Текстура: unsharp mask с малым радиусом, `Y' = Y + k * (Y - blur_small(Y))`. +- Чёткость: unsharp mask с большим радиусом и подавлением ореолов — дельта + ограничивается и взвешивается по средним тонам, чтобы не звонить на границах + контрастных объектов. +- Отрицательные значения интерполируют к размытой версии (сглаживание), а не + просто инвертируют знак дельты. + +**4. Размытие — быстрый box-blur (три прохода ≈ гаусс), радиус — доля размера кадра.** + +- Радиус задаётся относительно `min(width, height)` (текстура ≈ 0.15 %, чёткость + ≈ 0.8 %), с минимумом 1 px. Это ключевое решение для совпадения превью и + полноразмерного рендера: сервер считает по оригиналу (например, 4000 px), а + превью — по preview 1200 px; относительный радиус даёт одинаковую частоту. +- Box-blur через скользящее окно по строкам/столбцам — O(n) независимо от + радиуса, поэтому большой радиус чёткости не бьёт по времени. Размытие идёт по + строкам параллельно, как текущие проходы. +- Альтернатива: `imaging.Blur` — проще, но стоимость растёт с радиусом; для + полноразмерных фото и больших радиусов отвергнуто. + +**5. Дымка — dark channel prior на уменьшенной копии.** + +- Оценка атмосферного света `A`: по уменьшенной копии (≈512 px) берутся самые + яркие пиксели тёмного канала (верхний 0.1 %). +- Карта пропускания `t = 1 - ω * min_c(I_c / A_c)` сглаживается box-blur и + ограничивается снизу (например, 0.1), затем интерполируется на полный кадр. +- `J_c = (I_c - A_c) / max(t, t0) + A_c`; сила эффекта интерполирует между `I` и + `J`. Отрицательные значения — обратная операция: подмешивание `A` и снижение + контраста. +- Альтернатива: простое «поднять контраст и насыщенность» — дешевле, но не + убирает дымку на дальних планах; отвергнуто как не соответствующее смыслу + контрола. + +**6. Виньетка — нормализованная геометрия и три режима.** + +- Координаты нормализуются к половине кадра; расстояние до центра возводится в + степень, зависящую от округлости (эллипс ↔ суперэллипс/скруглённый + прямоугольник). Средняя точка задаёт начало зоны, растушёвка — ширину + перехода, знак силы выбирает затемнение или осветление. +- «Свет»: аддитивная поправка яркости с весом маски; «Света» защищает яркие + пиксели (уменьшает поправку при высокой `Y`), чтобы виньетка не «съедала» + блики. +- «Цвет»: подмешивание выбранного цвета с сохранением светлоты (смещение в + хроматические координаты). «Закарасить»: заливка выбранным цветом поверх + кадра с весом маски и силы. В обоих цветных режимах «Света» ограничивает + влияние на яркие участки. +- Альтернатива: отдельные реализации на каждый режим — отвергнута, общая маска + даёт одинаковую геометрию и меньше расхождений между сервером и превью. + +**7. Зерно — детерминированный процедурный шум.** + +- PRNG (например, splitmix64) инициализируется хешем `photo.id`, а не случайным + зерном: повторный рендер тех же параметров даёт тот же рисунок. Это требование + спеки; иначе `edit_thumb` и полноразмерный рендер расходились бы при каждом + прогоне. +- «Размер» задаёт шаг сетки шума (доля размера кадра, с минимумом 1 px), + «шероховатость» смешивает высокочастотный и сглаженный шум. Шум добавляется в + яркость, чтобы не вносить цветные пятна; сила 0 — шум не считается. +- Превью и полноразмерный рендер используют один PRNG, но разные сетки + дискретизации: рисунок визуально похож, побитового совпадения нет. + +**8. WebGL2-предпросмотр переходит на много­проходную схему.** + +- Проход 1: существующий шейдер света/цвета рисует кадр в FBO-текстуру вместо + экрана. +- Проход 2: яркость `Y`, разделимое box-размытие (горизонталь + вертикаль) в + две ping-pong текстуры; для чёткости — уменьшенное разрешение, для текстуры — + несколько тапов на полном. +- Дымка: `A` и карта `t` считаются на CPU по базовому превью при смене + изображения/параметров дымки и загружаются текстурой (например, 256 px); + шейдер интерполирует. +- Финальный проход: `A`-текстура, размытая яркость, карта дымки, виньетка и + зерно (хеш-шум в шейдере) → экран. +- Во время перетаскивания ползунка (`previewLow`) уменьшаются разрешение + размытия и число проходов; после паузы — полное качество, как сейчас. +- Альтернатива: один проход с приближением (несколько тапов) — проще, но + большой радиус чёткости требует десятков выборок и даёт хуже картинку; + отвергнуто. + +**9. CPU-фолбэк: виньетка и зерно — да, пространственные эффекты — после сервера.** + +- В JS-фолбэке применяются свет, цвет, виньетка и зерно (попиксельно); текстура, + чёткость и дымка не считаются, чтобы не блокировать main thread размытием + полного кадра, и появляются после серверного рендера. Это допустимо по спеке + («предпросмотр без WebGL… после серверного рендера»). +- WASM-модуль не трогаем: он остаётся попиксельным и покрывает свет/цвет. + Альтернатива — расширять `no_std`-модуль буферами под blur — отвергнута как + отдельный крупный проект без выигрыша для основного GPU-пути. + +**10. Гистограмма остаётся по свету и цвету.** + +- Она считается по сэмплу 256 px (`sampleImage` + `applyLight`/`applyColor`); + на таком размере пространственные эффекты бессмысленны, а виньетка/зерно + искажали бы оценку тональности. Оставляем текущий расчёт и явно фиксируем это + решение в коде/дизайне. + +**11. UI: секция «Эффекты» после «Цвет».** + +- Заголовок сворачиваемый, как «Свет»/«Цвет». Слайдеры — существующий + `EditSlider` (двойной клик сбрасывает значение). +- Виньетка: три кнопки-вкладки («Свет», «Цвет», «Закарасить»), под ними сила + («Виньетка»), средняя точка, растушёвка, округлость, света; для цветных + режимов — свотч цвета (``), значение хранится hex-строкой. +- «Зерно»: сила, размер, шероховатость. +- Глобальная кнопка «Сброс» в секции «Свет» продолжает сбрасывать все правки + целиком (включая эффекты) — новых точек сброса не добавляем, чтобы не + дублировать поведение. +- Сохранение — через существующий debounce и `photosApi.edit`; сервер сам решает, + что параметры пустые. Индикатор «рендерится» уже привязан к наличию правок и + отсутствию `edit_preview_url`, поэтому заработает без изменений. + +**12. Свёрнутость секций редактора — в localStorage, по умолчанию всё свёрнуто.** + +- Все сворачиваемые блоки панели («Свет», «Кривая», «Цвет», «Цветовой микшер», + «Точный цвет», «Цветокоррекция», «Эффекты») по умолчанию свёрнуты: панель + открывается компактно и не перегружает экран. +- Выбор пользователя хранится в браузере, в localStorage-ключе + `galo.edit.sections` (JSON `{section: collapsed}`), и восстанавливается при + перезагрузке и смене фото. Неизвестные ключи и небулевы значения + игнорируются, ошибки хранилища не ломают панель. +- Альтернатива: хранить в `preferences` профиля — отвергнута: это настройка + конкретного браузера, а не аккаунта; сервер и Android не затрагиваются. + +**13. Вкладка «Редактирование» доступна только в просмотрщике.** + +- В режиме сетки правая панель показывает только «Сведения»: редактирование + открывается при просмотре фото в просмотрщике, где доступны живой предпросмотр + и пипетка. +- Реализация: у `PhotoPanel` уже есть признак `viewer`; активная вкладка — + `activeTab = viewer ? tab : 'info'`, кнопка «Редактирование» рендерится только + при `viewer`. Сохранённая вкладка не теряется: вернувшись в просмотрщик, + пользователь увидит тот же раздел. +- Альтернатива: показывать вкладку неактивной — отвергнута: лишний элемент в + режиме просмотра сетки и риск случайных правок. + +## Risks / Trade-offs + +- **[Расхождение превью и серверного рендера]** → относительные радиусы и + геометрия; CPU/GL предпросмотр явно считается приблизительным, источник истины + — сервер. Это документируется в спеке («MAY отличаться деталями»). +- **[Стоимость рендера полноразмерного фото]** → O(n) box-blur вместо + гауссова с большим радиусом; дымка считается на уменьшенной копии; проходы + пропускаются, если эффект пуст; параллелизм по строкам. Добавить бенчмарк на + 12 Мп и следить за временем воркера. +- **[Ореолы чёткости]** → ограничение дельты и веса по тонам; при сильных + значениях полностью убрать ореолы нельзя, это осознанный компромисс. +- **[Зерно и thumbnail]** → seed от `photo.id`, иначе фоновая пересборка даёт + разный шум; цена — визуально разные рисунки превью и полного кадра. +- **[Обратная совместимость]** → ключ `effects` игнорируется старым бинарником; + `IsEmpty` учитывает только силы, поэтому дефолты не «залипают» как правка. +- **[Шум в гистограмме]** → эффекты не входят в гистограмму; это осознанно и + может быть пересмотрено отдельным изменением. + +## Migration Plan + +Схемных миграций нет. Выкатка сервер + фронтенд одновременно; старые фото и +старые `edit_params` читаются с дефолтами. Откат — возврат предыдущей сборки: +неизвестный ключ `effects` старые версии просто игнорируют, параметры остаются в +метаданных и сбрасываются штатной кнопкой «Сброс». + +## Open Questions + +Нет. diff --git a/openspec/changes/archive/2026-10-24-photo-effects-editing/proposal.md b/openspec/changes/archive/2026-10-24-photo-effects-editing/proposal.md new file mode 100644 index 0000000..5347317 --- /dev/null +++ b/openspec/changes/archive/2026-10-24-photo-effects-editing/proposal.md @@ -0,0 +1,72 @@ +# Proposal + +## Why + +В неразрушающем редакторе Galo сейчас есть только блоки «Свет» и «Цвет»: +экспозиция, тональная кривая, баланс белого, микшер и цветокоррекция. Не хватает +эффектов, которые в Lightroom собраны в панели «Эффекты»: текстура, чёткость, +удаление дымки, виньетка и зерно. Это последний крупный блок базовой обработки, +после которого веб-редактор покрывает привычный набор Lightroom. + +## What Changes + +- Добавить в `EditParams` блок `Effects`: + - **Текстура** — локальный контраст высоких частот (unsharp mask по яркости). + - **Чёткость** — локальный контраст средних частот с защитой от ореолов. + - **Убрать дымку** — оценка дымки (dark channel prior) и возврат контраста и + насыщенности. + - **Виньетка** — три режима, как в макете: «Свет», «Цвет», «Закарасить» + (заливка). Параметры: сила, средняя точка, растушёвка, округлость, света; + для цветных режимов — цвет виньетки. + - **Зерно** — сила, размер, шероховатость; шум детерминирован для повторных + рендеров. +- Сервер (Go): применять эффекты после света и цвета при рендере редактированной + версии; результат по-прежнему пишется в роли `edit`, `edit_thumb`, + `edit_medium`, `edit_preview`, оригинал не меняется. +- Клиент (Svelte/TS): раздел «Эффекты» в панели правок с вкладками виньетки; + живой предпросмотр эффектов в просмотрщике до готовности серверного рендера. +- Панель правок: все секции инструментов по умолчанию свёрнуты, состояние + каждой секции сохраняется в браузере (`localStorage`). +- Вкладка «Редактирование» доступна только в просмотрщике: в режиме сетки + правая панель показывает только «Сведения». +- WebGL2-превью: перейти с одного прохода на много­проходную схему (FBO, + разделимое размытие, карта дымки) для пространственных эффектов. +- Обратная совместимость: `edit_params` остаётся JSON-блобом, фото без ключа + `effects` рендерятся как раньше; миграции схемы БД не требуется. +- Android-клиент не меняется: он показывает уже отрендеренные версии. + +## Capabilities + +### New Capabilities + +Нет — эффекты входят в существующую способность `photos`. + +### Modified Capabilities + +- `photos`: требование «Неразрушающее редактирование фотографии» расширяется + параметрами эффектов; добавляются требования к виньетке, зерну, пространственным + эффектам и их живому предпросмотру. + +## Impact + +- `backend/internal/photos/edit.go` — модель `EffectsParams`, нормализация, + `IsEmpty`, сериализация. +- `backend/internal/photos/handlers.go` — приём блока `effects` в + `PATCH /photos/:id/edit` (у обработчика собственный request-структ). +- `backend/internal/photos/edit_render.go` — порядок проходов рендера и новые + эффекты; вероятно отдельный файл с пространственными операциями (blur, + dark channel, виньетка, зерно). +- `frontend/src/lib/edit.ts` — типы, дефолты, нормализация, CPU-реализация + попиксельных эффектов (виньетка, зерно) для фолбэка. +- `frontend/src/lib/webgl-preview.ts`, `Viewer.svelte` — много­проходный + предпросмотр эффектов. +- `frontend/src/lib/components/PhotoEditPanel.svelte` — раздел «Эффекты», + вкладки виньетки, цвет виньетки; свёрнутые по умолчанию секции; `EditSlider.svelte` + переиспользуется. +- `frontend/src/lib/editor-sections.ts` — состояние сворачивания секций в + `localStorage` и его нормализация. +- `frontend/src/lib/components/PhotoPanel.svelte` — вкладка «Редактирование» + только при открытом просмотрщике. +- Тесты: `edit_test.go`, `edit.test.ts`, новый e2e-сценарий правки с эффектами, + бенчмарк пространственных проходов; проверка компиляции GL-шейдеров. +- Миграции БД и Android-клиент не затрагиваются. diff --git a/openspec/changes/archive/2026-10-24-photo-effects-editing/specs/photos/spec.md b/openspec/changes/archive/2026-10-24-photo-effects-editing/specs/photos/spec.md new file mode 100644 index 0000000..97ae8ef --- /dev/null +++ b/openspec/changes/archive/2026-10-24-photo-effects-editing/specs/photos/spec.md @@ -0,0 +1,180 @@ +# Spec Delta + +## MODIFIED Requirements + +### Requirement: Неразрушающее редактирование фотографии +Система SHALL поддерживать неразрушающее редактирование фотографий. Параметры правок (свет, цвет и эффекты) SHALL храниться в метаданных фото, а отредактированная версия SHALL создаваться как отдельные объекты (роли `edit`, `edit_thumb`, `edit_medium`, `edit_preview`) рядом с исходными вариантами. Рендер SHALL выполняться по исходному объекту фото, а не по его уменьшенным вариантам. Исходный объект SHALL оставаться неизменным, а скачивание SHALL отдавать оригинал. Сброс правок SHALL удалять и параметры, и отредактированную версию. Правки SHALL применяться только к изображениям. + +#### Scenario: Сохранение правок +- **WHEN** пользователь сохраняет правки фото (свет, цвет и/или эффекты) +- **THEN** параметры сохраняются в метаданных, отредактированная версия рендерится в фоне, а после рендера фото получает URL отредактированных вариантов + +#### Scenario: Фоновая подготовка версий +- **WHEN** правки фото сохранены +- **THEN** система готовит варианты сетки и полноразмерную версию в фоне и сообщает о готовности + +#### Scenario: Рендер по оригиналу +- **WHEN** готовится отредактированная версия фото +- **THEN** световые и цветовые правки, а также эффекты применяются к пикселям исходного объекта, а не к его уменьшенной копии + +#### Scenario: Неразрушачность +- **WHEN** фото отредактировано +- **THEN** исходный объект и его варианты остаются неизменными + +#### Scenario: Сброс правок +- **WHEN** пользователь сбрасывает правки +- **THEN** параметры и отредактированная версия удаляются, а фото снова показывается в исходном виде + +#### Scenario: Права на правки +- **WHEN** правки сохраняет пользователь без прав на это фото +- **THEN** система отклоняет запрос + +#### Scenario: Редактирование видео +- **WHEN** пользователь пытается отредактировать видео +- **THEN** система отклоняет запрос + +#### Scenario: Цветовые правки сохраняются и рендерятся +- **WHEN** пользователь сохраняет цветовые правки фото: баланс белого (температура/оттенок), сочность (vibrance), насыщенность, цветовой микшер по оттенкам, цветокоррекцию (тени/средние тона/света, яркость зон, наложение, баланс) или поканальную коррекцию +- **THEN** параметры цвета сохраняются в метаданных, отредактированная версия рендерится в фоне, а после рендера фото получает URL отредактированных вариантов + +#### Scenario: Цветовые правки применяются к пикселям исходного объекта +- **WHEN** готовится отредактированная версия фото с цветовыми правками +- **THEN** цветовые правки применяются к пикселям исходного объекта после световых правок, а не к его уменьшенной копии + +#### Scenario: Пустые цветовые правки не сохраняются +- **WHEN** в цветовых параметрах заданы только наложение/баланс или оттенок колеса без силы и яркости +- **THEN** правки считаются пустыми и фото не получает цветовую версию + +#### Scenario: Пустые правки эффектов не сохраняются +- **WHEN** во всех эффектах заданы только значения по умолчанию (сила эффекта, виньетки и зерна равна нулю) +- **THEN** правки считаются пустыми и фото не получает новую отредактированную версию + +## ADDED Requirements + +### Requirement: Текстура +Клиент SHALL позволять настраивать текстуру фото в диапазоне -100..100. Положительные значения SHALL усиливать контраст мелких деталей, отрицательные — ослаблять его. Эффект SHALL применяться к исходному кадру после световых и цветовых правок. + +#### Scenario: Усиление текстуры +- **WHEN** пользователь увеличивает значение текстуры +- **THEN** мелкие детали становятся контрастнее, а общая тональность кадра не меняется + +#### Scenario: Ослабление текстуры +- **WHEN** пользователь уменьшает значение текстуры ниже нуля +- **THEN** мелкие детали сглаживаются + +#### Scenario: Текстура в пределах допустимого +- **WHEN** клиент присылает значение текстуры вне диапазона -100..100 +- **THEN** сервер ограничивает его ближайшей границей диапазона + +### Requirement: Чёткость +Клиент SHALL позволять настраивать чёткость фото в диапазоне -100..100. Положительные значения SHALL усиливать локальный контраст средних частот, отрицательные — смягчать его. Чёткость SHALL NOT создавать заметных ореолов на границах контрастных объектов. Эффект SHALL применяться после световых и цветовых правок. + +#### Scenario: Усиление чёткости +- **WHEN** пользователь увеличивает значение чёткости +- **THEN** локальный контраст средних тонов растёт, а границы контрастных объектов остаются без заметных ореолов + +#### Scenario: Ослабление чёткости +- **WHEN** пользователь уменьшает значение чёткости ниже нуля +- **THEN** изображение становится мягче + +### Requirement: Убрать дымку +Клиент SHALL позволять ослаблять или усиливать дымку в диапазоне -100..100. При положительных значениях сервер SHALL оценивать дымку по кадру и возвращать контраст и насыщенность удалённых участков; отрицательные значения SHALL добавлять дымку. + +#### Scenario: Удаление дымки +- **WHEN** пользователь увеличивает значение «Убрать дымку» на кадре с дымкой +- **THEN** дальние планы становятся контрастнее и насыщеннее + +#### Scenario: Добавление дымки +- **WHEN** пользователь уменьшает значение «Убрать дымку» ниже нуля +- **THEN** кадр становится менее контрастным, как при дымке + +### Requirement: Виньетка +Клиент SHALL позволять настраивать виньетку: силу -100..100, среднюю точку и растушёвку 0..100 (по умолчанию 50), округлость -100..100, света 0..100, а также режим «Свет», «Цвет» или «Закарасить». Режим «Свет» SHALL затемнять или осветлять края, «Цвет» — тонировать их выбранным цветом с сохранением светлоты, «Закарасить» — заливать их выбранным цветом. + +#### Scenario: Затемнение краёв +- **WHEN** пользователь задаёт положительную силу виньетки в режиме «Свет» +- **THEN** края кадра затемняются, а центр остаётся без изменений + +#### Scenario: Осветление краёв +- **WHEN** пользователь задаёт отрицательную силу виньетки в режиме «Свет» +- **THEN** края кадра осветляются + +#### Scenario: Цветная виньетка +- **WHEN** пользователь выбирает режим «Цвет» или «Закарасить» и задаёт цвет виньетки +- **THEN** края кадра тонируются или заливаются этим цветом с силой, заданной ползунком виньетки + +#### Scenario: Геометрия виньетки +- **WHEN** пользователь меняет среднюю точку, растушёвку или округлость +- **THEN** меняются размер, мягкость и форма зоны виньетки + +#### Scenario: Защита светов +- **WHEN** пользователь увеличивает значение «Света» в режиме «Свет» +- **THEN** яркие участки в зоне виньетки сохраняют яркость + +#### Scenario: Нулевая сила виньетки +- **WHEN** сила виньетки равна нулю +- **THEN** виньетка не влияет на изображение независимо от остальных параметров + +### Requirement: Зерно +Клиент SHALL позволять добавлять зерно с силой, размером и шероховатостью в диапазоне 0..100 (по умолчанию размер 25, шероховатость 50). Зерно SHALL быть детерминированным: повторный рендер одних и тех же параметров SHALL давать одинаковый шум. + +#### Scenario: Добавление зерна +- **WHEN** пользователь увеличивает силу зерна +- **THEN** на изображении появляется шум, заметный в средних тонах + +#### Scenario: Размер и шероховатость +- **WHEN** пользователь меняет размер или шероховатость зерна +- **THEN** меняются крупность и контрастность шума + +#### Scenario: Детерминированность зерна +- **WHEN** сервер повторно рендерит фото с теми же параметрами зерна +- **THEN** рисунок шума совпадает с предыдущим рендером + +#### Scenario: Нулевая сила зерна +- **WHEN** сила зерна равна нулю +- **THEN** зерно не влияет на изображение + +### Requirement: Живой предпросмотр эффектов +Клиент SHALL показывать эффекты в просмотрщике до завершения серверного рендера. После готовности серверной версии клиент SHALL показывать её. Предпросмотр пространственных эффектов (текстура, чёткость, дымка) MAY отличаться от серверного рендера деталями, но SHALL передавать характер эффекта. + +#### Scenario: Эффекты видны сразу +- **WHEN** пользователь двигает ползунок эффекта +- **THEN** просмотрщик обновляет изображение без ожидания серверного рендера + +#### Scenario: Серверная версия заменяет предпросмотр +- **WHEN** серверный рендер эффектов завершён +- **THEN** просмотрщик показывает серверную версию + +#### Scenario: Предпросмотр без WebGL +- **WHEN** аппаратное ускорение недоступно +- **THEN** клиент показывает эффекты доступным способом или показывает их после серверного рендера, не блокируя интерфейс + +### Requirement: Свёрнутые секции редактора +Редактор SHALL показывать все сворачиваемые секции инструментов свёрнутыми по умолчанию. Клиент SHALL сохранять состояние каждой секции (свёрнута или развёрнута) в браузере пользователя и восстанавливать его при повторном открытии редактора и перезагрузке страницы. Состояние SHALL NOT зависеть от выбранного фото. + +#### Scenario: Первое открытие редактора +- **WHEN** пользователь впервые открывает редактор фото +- **THEN** все секции инструментов («Свет», «Цвет», «Эффекты» и их вложенные блоки) свёрнуты + +#### Scenario: Состояние запоминается +- **WHEN** пользователь разворачивает секцию «Эффекты» и перезагружает страницу +- **THEN** секция «Эффекты» остаётся развёрнутой + +#### Scenario: Состояние переживает смену фото +- **WHEN** пользователь переключает фото в редакторе +- **THEN** свёрнутость секций не меняется + +### Requirement: Редактирование только из просмотрщика +Клиент SHALL показывать вкладку «Редактирование» только когда фото открыто в просмотрщике. В режиме сетки правая панель SHALL показывать только «Сведения» и SHALL NOT отображать инструменты редактирования. + +#### Scenario: Фото выбрано в сетке +- **WHEN** пользователь выбирает фото в сетке, не открывая просмотрщик +- **THEN** в правой панели отображается только вкладка «Сведения» + +#### Scenario: Фото открыто в просмотрщике +- **WHEN** пользователь открывает фото в просмотрщике +- **THEN** в правой панели доступны «Сведения» и «Редактирование» + +#### Scenario: Возврат из просмотрщика в сетку +- **WHEN** пользователь возвращается из просмотрщика в сетку +- **THEN** вкладка «Редактирование» скрыта, панель показывает «Сведения» diff --git a/openspec/changes/archive/2026-10-24-photo-effects-editing/tasks.md b/openspec/changes/archive/2026-10-24-photo-effects-editing/tasks.md new file mode 100644 index 0000000..5b0c6ba --- /dev/null +++ b/openspec/changes/archive/2026-10-24-photo-effects-editing/tasks.md @@ -0,0 +1,62 @@ +# Tasks + +## 1. Модель правок и нормализация (Go) + +- [x] 1.1 Добавить `EffectsParams`, `VignetteParams`, `GrainParams` и поле `EditParams.Effects` в `backend/internal/photos/edit.go`, включая режимы виньетки и hex-цвет — проверить `cd backend && go build ./internal/photos/` +- [x] 1.2 Реализовать нормализацию: диапазоны, дефолты midpoint 50 / feather 50 / size 25 / roughness 50, неизвестный режим → `light`, валидный hex-цвет; обновить `IsEmpty`/`JSON` так, чтобы дефолты без силы не считались правкой — проверить `cd backend && go test ./internal/photos/ -run TestEffectsParams -v` +- [x] 1.3 Проверить обратную совместимость: `{}` и старые параметры только со светом/цветом читаются как пустые эффекты и рендерятся без изменений — проверить `cd backend && go test ./internal/photos/ -run TestEditParamsLegacy -v` + +## 2. Пространственный рендер (Go) + +- [x] 2.1 Добавить хелпер яркости и разделимый box-blur со скользящим окном (O(n) от радиуса, параллельно по строкам) — проверить `cd backend && go test ./internal/photos/ -run TestBoxBlur -v` и бенчмарк на 4000×3000 +- [x] 2.2 Реализовать текстуру и чёткость как unsharp mask по яркости с ограничением дельты и весами по тонам (радиусы — доли `min(width,height)`) — проверить тест, что локальная дисперсия растёт при положительных и падает при отрицательных значениях +- [x] 2.3 Реализовать «Убрать дымку» через dark channel prior: атмосферный свет и карта пропускания на уменьшенной копии, интерполяция на полный кадр, положительные и отрицательные значения — проверить тест на синтетическом «задымлённом» изображении: контраст и насыщенность растут +- [x] 2.4 Реализовать виньетку: нормализованная геометрия (midpoint, feather, roundness), режимы «Свет»/«Цвет»/«Закарасить», защита светов — проверить тест: края темнеют/светлеют, центр не меняется, цветные режимы меняют оттенок краёв +- [x] 2.5 Реализовать зерно: PRNG с seed от `photo.id`, размер и шероховатость, аддитивный шум в яркость — проверить тест: два рендера с одинаковыми параметрами дают идентичные пиксели, сила 0 не меняет кадр +- [x] 2.6 Встроить эффекты в `decodeAndAdjust` в порядке свет → цвет → дымка → текстура → чёткость → виньетка → зерно, с пропуском пустых проходов — проверить `cd backend && go test ./internal/photos/ -v` и бенчмарк полного рендера +- [x] 2.7 Добавить e2e-сценарий: сохранение правки с эффектами, фоновый рендер `edit_*`, сброс, повторный рендер с тем же зерном — проверить `cd backend && go test ./internal/e2e/ -run TestEffectsEdit -v` + +## 3. Клиентская модель и CPU-фолбэк (TypeScript) + +- [x] 3.1 Добавить `EffectsParams` и дефолты в `frontend/src/lib/edit.ts`, обновить `readEditParams`/`normalizeEditParams`/`isEmptyEditParams` — проверить `cd frontend && npm run test` (`edit.test.ts`) и `npm run check` +- [x] 3.2 Реализовать CPU-применение виньетки и зерна (по образцу `applyColorToPixels`), seed и геометрия — как на сервере; тесты на затемнение краёв и детерминированность шума — проверить `cd frontend && npm run test` +- [x] 3.3 Убедиться, что гистограмма по-прежнему считается только по свету и цвету (без пространственных эффектов) — проверить `npm run check` и отсутствие изменений в расчёте `histogram` + +## 4. WebGL-предпросмотр + +- [x] 4.1 Перевести `frontend/src/lib/webgl-preview.ts` на много­проходную схему: свет/цвет в FBO-текстуру вместо экрана, два ping-pong FBO — проверить компиляцию/линковку шейдеров и `npm run build` +- [x] 4.2 Добавить проход яркости и разделимое box-размытие: малое разрешение для чёткости, несколько тапов для текстуры; включать проход только при ненулевых значениях — проверить, что текстура и чёткость видны в превью и не ломают свет/цвет +- [x] 4.3 Добавить предварительный расчёт дымки на CPU (атмосферный свет + карта пропускания по базовому превью) и передачу её текстурой в финальный проход — проверить, что ползунок дымки меняет превью +- [x] 4.4 Добавить в финальный проход виньетку (три режима) и зерно (хеш-шум) с параметрами из `liveParams.effects` — проверить визуально смену режимов и силы +- [x] 4.5 Подключить эффекты в `Viewer.svelte` для GL-пути и CPU-фолбэка: при `previewLow` снижать качество размытия, после паузы — полное; в CPU-фолбэке применять только виньетку и зерно — проверить, что интерфейс не блокируется при перетаскивании +- [x] 4.6 Проверить деградацию без WebGL: пространственные эффекты появляются после серверного рендера, а виньетка/зерно видны сразу — проверить ручным сценарием с отключённым WebGL2 + +## 5. UI: раздел «Эффекты» + +- [x] 5.1 Добавить сворачиваемую секцию «Эффекты» в `PhotoEditPanel.svelte` со слайдерами текстуры, чёткости и дымки (`EditSlider`) — проверить открытие/сворачивание и изменение превью +- [x] 5.2 Добавить вкладки виньетки «Свет»/«Цвет»/«Закарасить», ползунки силы/средней точки/растушёвки/округлости/светов и свотч цвета для цветных режимов — проверить сохранение режима и цвета в параметрах +- [x] 5.3 Добавить ползунки зерна (сила, размер, шероховатость) — проверить, что сила 0 не добавляет правку +- [x] 5.4 Подключить эффекты к существующему сохранению (`scheduleSave`/`photosApi.edit`), переключению фото и глобальному сбросу — проверить `PATCH /api/photos/:id/edit` с `effects`, сброс очищает параметры и `edit_*`, переключение фото не теряет несохранённые эффекты +- [x] 5.5 Проверить сборку и типы: `cd frontend && npm run check && npm run build` + +## 6. Интеграционная проверка + +- [x] 6.1 Ручной прогон матрицы: только эффекты; эффекты вместе со светом/цветом; сброс; правка во время переключения фото; фото без `effects`; копия фото не наследует эффекты; видео отклоняется — проверить в запущенном инстансе +- [x] 6.2 Замерить время фонового рендера полноразмерного фото с включёнными текстурой/чёткостью/дымкой и сравнить с текущим рендером света/цвета — проверить по логам воркера и бенчмарку +- [x] 6.3 Прогнать общие проверки репозитория: `make test`, фронтенд-тесты `npm run test` и сборку `make build` + +## 7. Свёрнутые секции редактора + +- [x] 7.1 Добавить `frontend/src/lib/editor-sections.ts` (дефолты, чтение/запись localStorage, нормализация) и тесты — проверить `cd frontend && npm run test` (`editor-sections.test.ts`) +- [x] 7.2 Сделать все сворачиваемые секции `PhotoEditPanel.svelte` свёрнутыми по умолчанию и подключить сохранение состояния — проверить `npm run check` +- [x] 7.3 Проверить в Chrome: по умолчанию всё свёрнуто, развёрнутое состояние переживает перезагрузку и смену фото — сценарий с CDP, без ошибок в консоли + +## 8. Вкладка редактирования только в просмотрщике + +- [x] 8.1 Скрыть вкладку «Редактирование» в режиме сетки через `activeTab` в `PhotoPanel.svelte` — проверить `cd frontend && npm run check` +- [x] 8.2 Проверить в Chrome: в сетке доступна только «Сведения», в просмотрщике — «Редактирование», после возврата в сетку вкладка снова скрыта — CDP-сценарий, без ошибок в консоли + +## Workflow follow-up + +- Архивировать изменение после удовлетворения требований ревью проекта. +- Проверить архивированный результат: дельта-спека `photos` применена к основной спеке. diff --git a/openspec/specs/photos/spec.md b/openspec/specs/photos/spec.md index 739a2c1..31e05b2 100644 --- a/openspec/specs/photos/spec.md +++ b/openspec/specs/photos/spec.md @@ -283,10 +283,10 @@ SHALL быть доступно и для фото без альбома (нап - **THEN** контрольные суммы входят в данные полученных фото ### Requirement: Неразрушающее редактирование фотографии -Система SHALL поддерживать неразрушающее редактирование фотографий. Параметры правок (свет и цвет) SHALL храниться в метаданных фото, а отредактированная версия SHALL создаваться как отдельные объекты (роли `edit`, `edit_thumb`, `edit_medium`, `edit_preview`) рядом с исходными вариантами. Рендер SHALL выполняться по исходному объекту фото, а не по его уменьшенным вариантам. Исходный объект SHALL оставаться неизменным, а скачивание SHALL отдавать оригинал. Сброс правок SHALL удалять и параметры, и отредактированную версию. Правки SHALL применяться только к изображениям. +Система SHALL поддерживать неразрушающее редактирование фотографий. Параметры правок (свет, цвет и эффекты) SHALL храниться в метаданных фото, а отредактированная версия SHALL создаваться как отдельные объекты (роли `edit`, `edit_thumb`, `edit_medium`, `edit_preview`) рядом с исходными вариантами. Рендер SHALL выполняться по исходному объекту фото, а не по его уменьшенным вариантам. Исходный объект SHALL оставаться неизменным, а скачивание SHALL отдавать оригинал. Сброс правок SHALL удалять и параметры, и отредактированную версию. Правки SHALL применяться только к изображениям. #### Scenario: Сохранение правок -- **WHEN** пользователь сохраняет правки фото (свет и/или цвет) +- **WHEN** пользователь сохраняет правки фото (свет, цвет и/или эффекты) - **THEN** параметры сохраняются в метаданных, отредактированная версия рендерится в фоне, а после рендера фото получает URL отредактированных вариантов #### Scenario: Фоновая подготовка версий @@ -295,7 +295,7 @@ SHALL быть доступно и для фото без альбома (нап #### Scenario: Рендер по оригиналу - **WHEN** готовится отредактированная версия фото -- **THEN** световые и цветовые правки применяются к пикселям исходного объекта, а не к его уменьшенной копии +- **THEN** световые и цветовые правки, а также эффекты применяются к пикселям исходного объекта, а не к его уменьшенной копии #### Scenario: Неразрушачность - **WHEN** фото отредактировано @@ -325,6 +325,10 @@ SHALL быть доступно и для фото без альбома (нап - **WHEN** в цветовых параметрах заданы только наложение/баланс или оттенок колеса без силы и яркости - **THEN** правки считаются пустыми и фото не получает цветовую версию +#### Scenario: Пустые правки эффектов не сохраняются +- **WHEN** во всех эффектах заданы только значения по умолчанию (сила эффекта, виньетки и зерна равна нулю) +- **THEN** правки считаются пустыми и фото не получает новую отредактированную версию + ### Requirement: Пипетка баланса белого Клиент SHALL позволять выбрать нейтральную точку на фото и SHALL устанавливать температуру и оттенок так, чтобы цвет этой точки стал нейтральным. Выборка SHALL браться из исходного (неотредактированного) превью, а пресет баланса белого SHALL переходить в «Пользовательский». @@ -427,3 +431,131 @@ SHALL быть доступно и для фото без альбома (нап #### Scenario: Некорректный файл или пресет - **WHEN** имя или кривая хотя бы одного канала недопустимы - **THEN** система отклоняет пресет и не меняет профиль + +### Requirement: Текстура +Клиент SHALL позволять настраивать текстуру фото в диапазоне -100..100. Положительные значения SHALL усиливать контраст мелких деталей, отрицательные — ослаблять его. Эффект SHALL применяться к исходному кадру после световых и цветовых правок. + +#### Scenario: Усиление текстуры +- **WHEN** пользователь увеличивает значение текстуры +- **THEN** мелкие детали становятся контрастнее, а общая тональность кадра не меняется + +#### Scenario: Ослабление текстуры +- **WHEN** пользователь уменьшает значение текстуры ниже нуля +- **THEN** мелкие детали сглаживаются + +#### Scenario: Текстура в пределах допустимого +- **WHEN** клиент присылает значение текстуры вне диапазона -100..100 +- **THEN** сервер ограничивает его ближайшей границей диапазона + +### Requirement: Чёткость +Клиент SHALL позволять настраивать чёткость фото в диапазоне -100..100. Положительные значения SHALL усиливать локальный контраст средних частот, отрицательные — смягчать его. Чёткость SHALL NOT создавать заметных ореолов на границах контрастных объектов. Эффект SHALL применяться после световых и цветовых правок. + +#### Scenario: Усиление чёткости +- **WHEN** пользователь увеличивает значение чёткости +- **THEN** локальный контраст средних тонов растёт, а границы контрастных объектов остаются без заметных ореолов + +#### Scenario: Ослабление чёткости +- **WHEN** пользователь уменьшает значение чёткости ниже нуля +- **THEN** изображение становится мягче + +### Requirement: Убрать дымку +Клиент SHALL позволять ослаблять или усиливать дымку в диапазоне -100..100. При положительных значениях сервер SHALL оценивать дымку по кадру и возвращать контраст и насыщенность удалённых участков; отрицательные значения SHALL добавлять дымку. + +#### Scenario: Удаление дымки +- **WHEN** пользователь увеличивает значение «Убрать дымку» на кадре с дымкой +- **THEN** дальние планы становятся контрастнее и насыщеннее + +#### Scenario: Добавление дымки +- **WHEN** пользователь уменьшает значение «Убрать дымку» ниже нуля +- **THEN** кадр становится менее контрастным, как при дымке + +### Requirement: Виньетка +Клиент SHALL позволять настраивать виньетку: силу -100..100, среднюю точку и растушёвку 0..100 (по умолчанию 50), округлость -100..100, света 0..100, а также режим «Свет», «Цвет» или «Закарасить». Режим «Свет» SHALL затемнять или осветлять края, «Цвет» — тонировать их выбранным цветом с сохранением светлоты, «Закарасить» — заливать их выбранным цветом. + +#### Scenario: Затемнение краёв +- **WHEN** пользователь задаёт положительную силу виньетки в режиме «Свет» +- **THEN** края кадра затемняются, а центр остаётся без изменений + +#### Scenario: Осветление краёв +- **WHEN** пользователь задаёт отрицательную силу виньетки в режиме «Свет» +- **THEN** края кадра осветляются + +#### Scenario: Цветная виньетка +- **WHEN** пользователь выбирает режим «Цвет» или «Закарасить» и задаёт цвет виньетки +- **THEN** края кадра тонируются или заливаются этим цветом с силой, заданной ползунком виньетки + +#### Scenario: Геометрия виньетки +- **WHEN** пользователь меняет среднюю точку, растушёвку или округлость +- **THEN** меняются размер, мягкость и форма зоны виньетки + +#### Scenario: Защита светов +- **WHEN** пользователь увеличивает значение «Света» в режиме «Свет» +- **THEN** яркие участки в зоне виньетки сохраняют яркость + +#### Scenario: Нулевая сила виньетки +- **WHEN** сила виньетки равна нулю +- **THEN** виньетка не влияет на изображение независимо от остальных параметров + +### Requirement: Зерно +Клиент SHALL позволять добавлять зерно с силой, размером и шероховатостью в диапазоне 0..100 (по умолчанию размер 25, шероховатость 50). Зерно SHALL быть детерминированным: повторный рендер одних и тех же параметров SHALL давать одинаковый шум. + +#### Scenario: Добавление зерна +- **WHEN** пользователь увеличивает силу зерна +- **THEN** на изображении появляется шум, заметный в средних тонах + +#### Scenario: Размер и шероховатость +- **WHEN** пользователь меняет размер или шероховатость зерна +- **THEN** меняются крупность и контрастность шума + +#### Scenario: Детерминированность зерна +- **WHEN** сервер повторно рендерит фото с теми же параметрами зерна +- **THEN** рисунок шума совпадает с предыдущим рендером + +#### Scenario: Нулевая сила зерна +- **WHEN** сила зерна равна нулю +- **THEN** зерно не влияет на изображение + +### Requirement: Живой предпросмотр эффектов +Клиент SHALL показывать эффекты в просмотрщике до завершения серверного рендера. После готовности серверной версии клиент SHALL показывать её. Предпросмотр пространственных эффектов (текстура, чёткость, дымка) MAY отличаться от серверного рендера деталями, но SHALL передавать характер эффекта. + +#### Scenario: Эффекты видны сразу +- **WHEN** пользователь двигает ползунок эффекта +- **THEN** просмотрщик обновляет изображение без ожидания серверного рендера + +#### Scenario: Серверная версия заменяет предпросмотр +- **WHEN** серверный рендер эффектов завершён +- **THEN** просмотрщик показывает серверную версию + +#### Scenario: Предпросмотр без WebGL +- **WHEN** аппаратное ускорение недоступно +- **THEN** клиент показывает эффекты доступным способом или показывает их после серверного рендера, не блокируя интерфейс + +### Requirement: Свёрнутые секции редактора +Редактор SHALL показывать все сворачиваемые секции инструментов свёрнутыми по умолчанию. Клиент SHALL сохранять состояние каждой секции (свёрнута или развёрнута) в браузере пользователя и восстанавливать его при повторном открытии редактора и перезагрузке страницы. Состояние SHALL NOT зависеть от выбранного фото. + +#### Scenario: Первое открытие редактора +- **WHEN** пользователь впервые открывает редактор фото +- **THEN** все секции инструментов («Свет», «Цвет», «Эффекты» и их вложенные блоки) свёрнуты + +#### Scenario: Состояние запоминается +- **WHEN** пользователь разворачивает секцию «Эффекты» и перезагружает страницу +- **THEN** секция «Эффекты» остаётся развёрнутой + +#### Scenario: Состояние переживает смену фото +- **WHEN** пользователь переключает фото в редакторе +- **THEN** свёрнутость секций не меняется + +### Requirement: Редактирование только из просмотрщика +Клиент SHALL показывать вкладку «Редактирование» только когда фото открыто в просмотрщике. В режиме сетки правая панель SHALL показывать только «Сведения» и SHALL NOT отображать инструменты редактирования. + +#### Scenario: Фото выбрано в сетке +- **WHEN** пользователь выбирает фото в сетке, не открывая просмотрщик +- **THEN** в правой панели отображается только вкладка «Сведения» + +#### Scenario: Фото открыто в просмотрщике +- **WHEN** пользователь открывает фото в просмотрщике +- **THEN** в правой панели доступны «Сведения» и «Редактирование» + +#### Scenario: Возврат из просмотрщика в сетку +- **WHEN** пользователь возвращается из просмотрщика в сетку +- **THEN** вкладка «Редактирование» скрыта, панель показывает «Сведения»