149 lines
11 KiB
Markdown
149 lines
11 KiB
Markdown
# Design
|
|
|
|
## Context
|
|
|
|
Сейчас кривая хранится в `light.curve` как точки на канал, `normalizeCurve`
|
|
сортирует/зажимает/дедуплицирует их, а по умолчанию массив пуст. UI рисует
|
|
статичные углы и прямые сегменты; `buildCurveLUT` (TS) и `curveLUT` (Go)
|
|
интерполируют линейно. Пресетов нет. Настройки профиля лежат в
|
|
`users.preferences` (JSON-блоб); `PATCH /auth/me/preferences` сейчас **заменяет**
|
|
весь объект и знает только `photo_info`, поэтому добавление пресетов без мержа
|
|
затирало бы другие разделы.
|
|
|
|
См. `proposal.md` — Why и `specs/photos/spec.md`, `specs/auth/spec.md` — требования.
|
|
|
|
## Goals / Non-Goals
|
|
|
|
**Goals:**
|
|
- Крайние узлы как обычные точки; первый клик вставляет между ними; узлы двигаются
|
|
по x/y; вне них кривая плоская.
|
|
- Плавная монотонная интерполяция в SVG-пути и в LUT; Go и TS считают одинаково.
|
|
- Встроенные пресеты контраста и пользовательские пресеты (кривые всех четырёх
|
|
каналов) в профиле; сохранение/применение/переименование/удаление;
|
|
импорт/экспорт JSON.
|
|
- Серверный мерж разделов настроек с валидацией.
|
|
|
|
**Non-Goals:**
|
|
- Пресеты как «look» из четырёх кривых; Android-клиент; шаринг пресетов между
|
|
пользователями (только файл); undo/redo.
|
|
|
|
## Decisions
|
|
|
|
**Крайние точки — обычные узлы.**
|
|
- Первая и последняя точки массива являются крайними; по умолчанию это `(0,0)` и
|
|
`(1,1)`. Пустой канал в UI показывает эти две точки; первое взаимодействие
|
|
записывает их в `light.curve`.
|
|
- Вне крайних точек значение постоянно (текущее поведение `curveEval`), поэтому
|
|
сдвинутый по x узел даёт плоскую «полку», как на скрине.
|
|
- При перетаскивании точки остаются отсортированными по x; UI не даёт точке
|
|
пересечь соседнюю (x ограничивается между соседями), на коммите — `normalizeCurve`.
|
|
- Альтернатива: фиксировать x крайних на 0/1 — отвергнуто, пользователь хочет
|
|
двигать и по x.
|
|
|
|
**Интерполяция — монотонный кубический Эрмит (PCHIP).**
|
|
- Наклоны отрезков `d_i`, касательные по Fritsch–Carlson (нулевые при смене
|
|
знака/нулевом наклоне, ограничение против выбросов). Участок между точками
|
|
считается по кубической формуле Эрмита.
|
|
- SVG-путь: те же касательные переводятся в кубические Безье
|
|
(`P1 = P0 + (h/3, m_i*h/3)`, `P2 = P3 - (h/3, m_{i+1}*h/3)`), плюс плоские
|
|
сегменты от x=0 до первой и от последней до x=1.
|
|
- LUT 256: те же значения; `buildCurveLUT` (TS) и `curveLUT` (Go) обязаны
|
|
совпадать. WASM и WebGL получают готовый LUT из TS — их менять не нужно.
|
|
- Альтернативы: Catmull-Rom (выбросы/гало на тоновой кривой), линейная
|
|
интерполяция (текущая, отвергнута).
|
|
|
|
**Пресет — кривые всех четырёх каналов.**
|
|
- `CurvePreset { name: string; channels: { rgb: CurvePoint[]; red: CurvePoint[];
|
|
green: CurvePoint[]; blue: CurvePoint[] } }`.
|
|
- Каждый канал содержит 2..16 точек; пустых каналов в пресете нет — линейный
|
|
канал хранится как `(0,0),(1,1)`.
|
|
- Встроенные `Линейный`, `Средний контраст`, `Сильный контраст` — клиентские
|
|
константы. Контраст задаётся кривой RGB, каналы R/G/B линейные. Предлагаемые
|
|
точки RGB:
|
|
- Средний: `(0,0),(0.25,0.2),(0.5,0.5),(0.75,0.8),(1,1)`
|
|
- Сильный: `(0,0),(0.2,0.12),(0.5,0.5),(0.8,0.88),(1,1)`
|
|
- Сохранение берёт `effectiveCurvePoints` каждого канала, поэтому в пресет
|
|
всегда попадают все четыре кривые.
|
|
- Применение заменяет кривые всех четырёх каналов; текущий выбранный канал не
|
|
меняется.
|
|
- Подпись: имя пресета, если кривые всех четырёх каналов совпадают с пресетом
|
|
(с допуском), иначе `Пользовательский`.
|
|
|
|
**Хранение и мерж настроек.**
|
|
- `preferencesPayload` расширяется `curve_presets`; поля-указатели позволяют
|
|
отличить «не передано» от «пусто».
|
|
- Handler читает текущие `users.preferences`, мержит переданные разделы
|
|
(`photo_info`, `curve_presets`), валидирует, пишет и возвращает полный объект.
|
|
Сохранение «Сведений» с `{photo_info}` сохраняет пресеты; сохранение пресетов
|
|
сохраняет `photo_info`.
|
|
- Валидация: не больше 100 пресетов, имя 1..64, все четыре канала присутствуют
|
|
и содержат 2..16 точек, `x,y` конечные в 0..1. Некорректный раздел
|
|
отклоняется целиком.
|
|
- Альтернативы: отдельный endpoint (больше поверхности), клиент шлёт весь объект
|
|
(устаревшие вкладки затирают чужие изменения).
|
|
|
|
**Переключение фото.**
|
|
- Вкладка правой панели хранится в `AlbumPanel` и передаётся в `PhotoPanel`
|
|
через `bind:tab`, поэтому переживает перемонтирование панели при загрузке
|
|
следующего фото.
|
|
- Cleanup эффекта `PhotoEditPanel` регистрируется на каждом запуске; при смене
|
|
`photo.id` накопленные правки отправляются до инициализации нового фото.
|
|
- Ответы сохранений проверяются по id фото и номеру операции: устаревшее
|
|
сохранение не подменяет текущее фото и его параметры. Панель рендерит только
|
|
фото, совпадающее с текущим маршрутом.
|
|
|
|
**Пользовательские действия и файлы.**
|
|
- Сохранить: диалог имени, кривые всех четырёх каналов; при совпадении имени —
|
|
подтверждение перезаписи.
|
|
- Переименовать/удалить: действия в меню пользовательского пресета.
|
|
- Экспорт: JSON `{ version: 1, presets: CurvePreset[] }` с кривыми всех каналов,
|
|
только пользовательские пресеты (встроенные всегда доступны).
|
|
- Импорт: файл-пикер, валидация, upsert по имени, сохранение через preferences;
|
|
при некорректном файле профиль не меняется.
|
|
|
|
**Совместимость формата.**
|
|
- Старый формат `{ name, channel, points }` читается из профиля и импорта:
|
|
точки попадают в указанный канал, остальные становятся линейными.
|
|
- При следующем сохранении список перезаписывается новым форматом со всеми
|
|
четырьмя каналами.
|
|
|
|
**UI.**
|
|
- Статичный текст «Линейный» заменяется кнопкой, открывающей popover-меню:
|
|
встроенные, пользовательские, «Сохранить…», «Импортировать…», «Экспортировать…».
|
|
- В SVG крайние точки — такие же перетаскиваемые кружки, как остальные; первый
|
|
клик по свободному полю вставляет точку между крайними; двойной клик сбрасывает.
|
|
- Сохраняется существующая клавиатурная правка точек.
|
|
|
|
## Risks / Trade-offs
|
|
|
|
[Старые кривые начнут выглядеть иначе (плавно вместо линейно)] → это осознанное
|
|
изменение рендера; точки не мигрируются, плоские участки вне крайних сохраняются.
|
|
|
|
[Расхождение PCHIP в Go и TS] → одна формула, тесты с захардкоженными значениями
|
|
LUT в обоих языках; WASM/WebGL используют готовый LUT.
|
|
|
|
[Гонка при мерже preferences] → мерж на сервере по разделам: разные разделы не
|
|
затирают друг друга; одновременная запись одного раздела — last-write-wins,
|
|
приемлемо для настроек одного пользователя.
|
|
|
|
[Рост списка пресетов / большой импорт] → лимиты количества, размера имени и
|
|
точек; handler уже ограничивает тело 1 МБ.
|
|
|
|
[Старые пресеты хранятся в формате одного канала] → клиент конвертирует их при
|
|
чтении/импорте, а первое сохранение пресетов перезаписывает профиль новым
|
|
форматом.
|
|
|
|
[Пересечение точек при перетаскивании] → ограничение x соседями и нормализация
|
|
на коммите.
|
|
|
|
## Migration Plan
|
|
|
|
Схема не меняется: `curve_presets` — ключ в JSON-блобе `users.preferences`.
|
|
Старый формат пресетов конвертируется на клиенте при чтении. Откат: удалить
|
|
ключ; старые кривые остаются в `light.curve` без изменений данных.
|
|
|
|
## Open Questions
|
|
|
|
Нет. Точные точки «Среднего/Сильного контраста» — настраиваемый дефолт, не влияет
|
|
на контракт.
|