chore(openspec): архивировать photo-effects-editing и обновить photos

Изменение применено к основной спеке photos: эффекты, виньетка, зерно,
живой предпросмотр, свёрнутые секции панели и редактирование из просмотрщика.
Change перемещён в archive.
This commit is contained in:
iu
2026-10-24 22:58:07 +03:00
parent c953008a86
commit cb9433666a
6 changed files with 707 additions and 3 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-24
@@ -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` (двойной клик сбрасывает значение).
- Виньетка: три кнопки-вкладки («Свет», «Цвет», «Закарасить»), под ними сила
(«Виньетка»), средняя точка, растушёвка, округлость, света; для цветных
режимов — свотч цвета (`<input type="color">`), значение хранится 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
Нет.
@@ -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-клиент не затрагиваются.
@@ -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** вкладка «Редактирование» скрыта, панель показывает «Сведения»
@@ -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` применена к основной спеке.
+135 -3
View File
@@ -283,10 +283,10 @@ SHALL быть доступно и для фото без альбома (нап
- **THEN** контрольные суммы входят в данные полученных фото - **THEN** контрольные суммы входят в данные полученных фото
### Requirement: Неразрушающее редактирование фотографии ### 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: Сохранение правок #### Scenario: Сохранение правок
- **WHEN** пользователь сохраняет правки фото (свет и/или цвет) - **WHEN** пользователь сохраняет правки фото (свет, цвет и/или эффекты)
- **THEN** параметры сохраняются в метаданных, отредактированная версия рендерится в фоне, а после рендера фото получает URL отредактированных вариантов - **THEN** параметры сохраняются в метаданных, отредактированная версия рендерится в фоне, а после рендера фото получает URL отредактированных вариантов
#### Scenario: Фоновая подготовка версий #### Scenario: Фоновая подготовка версий
@@ -295,7 +295,7 @@ SHALL быть доступно и для фото без альбома (нап
#### Scenario: Рендер по оригиналу #### Scenario: Рендер по оригиналу
- **WHEN** готовится отредактированная версия фото - **WHEN** готовится отредактированная версия фото
- **THEN** световые и цветовые правки применяются к пикселям исходного объекта, а не к его уменьшенной копии - **THEN** световые и цветовые правки, а также эффекты применяются к пикселям исходного объекта, а не к его уменьшенной копии
#### Scenario: Неразрушачность #### Scenario: Неразрушачность
- **WHEN** фото отредактировано - **WHEN** фото отредактировано
@@ -325,6 +325,10 @@ SHALL быть доступно и для фото без альбома (нап
- **WHEN** в цветовых параметрах заданы только наложение/баланс или оттенок колеса без силы и яркости - **WHEN** в цветовых параметрах заданы только наложение/баланс или оттенок колеса без силы и яркости
- **THEN** правки считаются пустыми и фото не получает цветовую версию - **THEN** правки считаются пустыми и фото не получает цветовую версию
#### Scenario: Пустые правки эффектов не сохраняются
- **WHEN** во всех эффектах заданы только значения по умолчанию (сила эффекта, виньетки и зерна равна нулю)
- **THEN** правки считаются пустыми и фото не получает новую отредактированную версию
### Requirement: Пипетка баланса белого ### Requirement: Пипетка баланса белого
Клиент SHALL позволять выбрать нейтральную точку на фото и SHALL устанавливать температуру и оттенок так, чтобы цвет этой точки стал нейтральным. Выборка SHALL браться из исходного (неотредактированного) превью, а пресет баланса белого SHALL переходить в «Пользовательский». Клиент SHALL позволять выбрать нейтральную точку на фото и SHALL устанавливать температуру и оттенок так, чтобы цвет этой точки стал нейтральным. Выборка SHALL браться из исходного (неотредактированного) превью, а пресет баланса белого SHALL переходить в «Пользовательский».
@@ -427,3 +431,131 @@ SHALL быть доступно и для фото без альбома (нап
#### Scenario: Некорректный файл или пресет #### Scenario: Некорректный файл или пресет
- **WHEN** имя или кривая хотя бы одного канала недопустимы - **WHEN** имя или кривая хотя бы одного канала недопустимы
- **THEN** система отклоняет пресет и не меняет профиль - **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** вкладка «Редактирование» скрыта, панель показывает «Сведения»