chore(openspec): архивировать photo-effects-editing и обновить photos
Изменение применено к основной спеке photos: эффекты, виньетка, зерно, живой предпросмотр, свёрнутые секции панели и редактирование из просмотрщика. Change перемещён в archive.
This commit is contained in:
@@ -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` применена к основной спеке.
|
||||
Reference in New Issue
Block a user