Files
ds-galo/openspec/specs/photos/spec.md
T
iu cb9433666a chore(openspec): архивировать photo-effects-editing и обновить photos
Изменение применено к основной спеке photos: эффекты, виньетка, зерно,
живой предпросмотр, свёрнутые секции панели и редактирование из просмотрщика.
Change перемещён в archive.
2026-10-24 22:58:07 +03:00

562 lines
45 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# photos Specification
## Purpose
Загрузка, хранение в S3 и просмотр фотографий с генерацией вариантов и извлечением EXIF.
## Requirements
### Requirement: Загрузка фото
Пользователь с правом загрузки SHALL иметь возможность загружать медиа (изображения и
видео) по одному и массово; система SHALL принимать ввод `image/*` и `video/*`.
#### Scenario: Успешная загрузка
- **WHEN** участник с правом загрузки отправляет файл изображения
- **THEN** система сохраняет оригинал и создаёт запись фото с метаданными
#### Scenario: Успешная загрузка видео
- **WHEN** участник с правом загрузки отправляет видеофайл
- **THEN** система сохраняет оригинал и создаёт медиа-запись с признаком видео
#### Scenario: Загрузка без права
- **WHEN** наблюдатель пытается загрузить фото
- **THEN** система отклоняет запрос
#### Scenario: Неподдерживаемый файл
- **WHEN** загружается файл, не являющийся изображением или видео
- **THEN** система отклоняет загрузку
### Requirement: Варианты изображений
Система SHALL генерировать четыре варианта: thumb (200×200, квадратный кроп), medium
(800×800, квадратный кроп), preview (1200×1200, вписанный) и оригинал, в WebP, и извлекать
EXIF (дата, координаты, размеры).
#### Scenario: Генерация вариантов
- **WHEN** фото загружено
- **THEN** система асинхронно генерирует thumb, medium и preview и заполняет EXIF-поля
#### Scenario: Форма вариантов сетки
- **WHEN** генерируются thumb и medium
- **THEN** они обрезаны до квадрата, а preview вписан с сохранением пропорций
### Requirement: Листинг фото с пагинацией
Система SHALL возвращать фото альбома с cursor-пагинацией.
#### Scenario: Пагинация по курсору
- **WHEN** клиент запрашивает список с курсором и лимитом
- **THEN** система возвращает не более лимита фото и курсор следующей страницы
### Requirement: Получение фото по требованию
Клиент SHALL иметь возможность получить актуальные данные фото с подписанным URL,
обслуживаемым бэкендом, независимо от бэкенда хранения. Подпись SHALL быть
детерминированной в пределах окна действия, чтобы повторные выдачи одного и того же
объекта давали идентичный URL.
#### Scenario: Выдача свежего URL
- **WHEN** клиент запрашивает фото по id
- **THEN** система возвращает подписанный URL бэкенда для просмотра
#### Scenario: Повторная выдача совпадает
- **WHEN** один и тот же объект запрашивается повторно в пределах окна действия подписи
- **THEN** система возвращает идентичный URL
### Requirement: Скачивание одиночного фото
Любой участник, включая viewer, SHALL иметь возможность скачать одиночное фото.
#### Scenario: Скачивание фото
- **WHEN** участник запрашивает скачивание фото
- **THEN** система возвращает подписанный URL оригинала, обслуживаемый бэкендом
### Requirement: Удаление фото
Автор SHALL иметь возможность удалить своё фото; владелец и админ SHALL иметь возможность удалить любое фото. Удаление SHALL помещать фото в корзину и SHALL не удалять объекты хранилища.
#### Scenario: Удаление своего фото
- **WHEN** автор удаляет своё фото
- **THEN** фото помещается в корзину, а объекты хранилища сохраняются
#### Scenario: Удаление чужого фото участником
- **WHEN** участник с ролью member пытается удалить чужое фото
- **THEN** система отклоняет запрос
### Requirement: Обложка по умолчанию
При загрузке фото система SHALL назначать его обложкой альбома, если обложка ещё не задана,
то есть обложкой становится первое загруженное фото. Когда фото, выбранное обложкой, покидает
альбом (помещено в корзину или перенесено в другой альбом), система SHALL снять обложку.
#### Scenario: Первое фото
- **WHEN** фото загружено, и у альбома нет обложки
- **THEN** система назначает это фото обложкой альбома
#### Scenario: Обложка уже задана
- **WHEN** фото загружено, а обложка уже задана
- **THEN** система не меняет обложку
#### Scenario: Фото-обложка в корзине
- **WHEN** фото, назначенное обложкой, помещается в корзину
- **THEN** система снимает обложку, и альбом отображается без обложки
#### Scenario: Фото-обложка перенесено
- **WHEN** фото, назначенное обложкой, переносится в другой альбом
- **THEN** система снимает обложку исходного альбома
### Requirement: Переименование фото
Автор фото, владелец и админ альбома SHALL иметь возможность изменить имя файла фото;
остальные SHALL получать отказ, а пустое имя SHALL отклоняться.
#### Scenario: Переименование автором
- **WHEN** автор меняет имя файла своего фото на непустое
- **THEN** система сохраняет новое имя и транслирует изменение подписчикам альбома
#### Scenario: Пустое имя
- **WHEN** пользователь подтверждает пустое имя файла
- **THEN** система отклоняет запрос
#### Scenario: Нет прав
- **WHEN** участник без прав переименовывает чужое фото
- **THEN** система отклоняет запрос
### Requirement: Ограничение размера загрузки
Система SHALL ограничивать размер загружаемых файлов отдельно для изображений и видео и
SHALL отвечать понятной ошибкой при превышении лимита, а не ошибкой разбора формы.
#### Scenario: Превышение лимита
- **WHEN** загружаемый файл превышает максимальный размер
- **THEN** система отклоняет загрузку с явной ошибкой о превышении размера
#### Scenario: Превышение лимита видео
- **WHEN** загружаемое видео превышает лимит видео
- **THEN** система отклоняет загрузку с явной ошибкой о превышении размера
#### Scenario: Файл в пределах лимита
- **WHEN** файл не превышает лимит
- **THEN** система принимает multipart-загрузку и обрабатывает фото
### Requirement: Метаданные фото (EXIF)
При загрузке фото система SHALL извлекать распознаваемые EXIF-атрибуты и SHALL возвращать их
вместе с данными фото; при отсутствии метаданных SHALL возвращать пустой набор.
#### Scenario: Фото с метаданными
- **WHEN** загружается фото с EXIF
- **THEN** данные фото содержат извлечённые атрибуты (камера, экспозиция, ISO, дата съёмки и др.)
#### Scenario: Фото без метаданных
- **WHEN** загружается фото без читаемых EXIF
- **THEN** данные фото не содержат атрибутов, а загрузка выполняется успешно
### Requirement: Перенос и копирование фото между альбомами
Пользователь с правом удаления в исходном альбоме и правом загрузки в целевом SHALL иметь
возможность перенести или скопировать фото в другой альбом. Операции SHALL изменять только
метаданные и SHALL не изменять объекты хранилища.
#### Scenario: Перенос
- **WHEN** пользователь переносит фото в другой альбом
- **THEN** фото исчезает из исходного альбома и появляется в целевом, объекты хранилища не меняются
#### Scenario: Копирование
- **WHEN** пользователь копирует фото в другой альбом
- **THEN** в целевом альбоме появляется новая запись, ссылающаяся на те же объекты хранилища
### Requirement: Корзина фото
Удаление фото SHALL помещать его в корзину (мягкое удаление). Восстановление SHALL возвращать
фото в его альбом. Окончательное удаление SHALL удалять запись, а объекты хранилища SHALL
удаляться только когда на них не остаётся ссылок ни в одном альбоме. Окончательное удаление
SHALL быть доступно и для фото без альбома (например, после удаления альбома).
#### Scenario: Удаление в корзину
- **WHEN** пользователь удаляет фото
- **THEN** фото пропадает из альбома и появляется в корзине
#### Scenario: Восстановление
- **WHEN** пользователь восстанавливает фото из корзины
- **THEN** фото возвращается в свой альбом
#### Scenario: Окончательное удаление без ссылок
- **WHEN** пользователь удаляет фото из корзины навсегда, и на те же объекты нет ссылок в других альбомах
- **THEN** запись и объекты хранилища удаляются
#### Scenario: Окончательное удаление при наличии ссылок
- **WHEN** на объект хранилища ссылается копия в другом альбоме
- **THEN** запись удаляется, а объект хранилища сохраняется
#### Scenario: Окончательное удаление фото без альбома
- **WHEN** пользователь удаляет навсегда фото из корзины, у которого нет альбома
- **THEN** запись удаляется, а операция не завершается ошибкой отсутствующего альбома
### Requirement: Удаление альбома и восстановление фото без альбома
Удаление альбома SHALL помещать его фото в корзину, не удаляя объекты хранилища. Если при
восстановлении фото его альбом отсутствует, система SHALL предложить выбрать существующий
альбом или создать новый.
#### Scenario: Удаление альбома
- **WHEN** владелец удаляет альбом
- **THEN** его фото оказываются в корзине без привязки к альбому, файлы сохраняются
#### Scenario: Восстановление без альбома
- **WHEN** пользователь восстанавливает фото, у которого нет альбома
- **THEN** система предлагает выбрать альбом или создать новый и восстанавливает фото в него
### Requirement: Метки фото
Пользователь SHALL присваивать фото свои метки (область — пользователь): создавать новые и
выбирать из списка с поиском; метки SHALL отдаваться вместе с фото и сниматься.
#### Scenario: Создание метки
- **WHEN** пользователь вводит новую метку и подтверждает
- **THEN** метка создаётся и привязывается к фото
#### Scenario: Выбор из списка с поиском
- **WHEN** пользователь вводит часть названия метки
- **THEN** показываются подходящие метки пользователя для выбора
#### Scenario: Снятие метки
- **WHEN** пользователь убирает метку с фото
- **THEN** метка отвязывается от фото
### Requirement: Кэширование объектов
Доставка объектов SHALL помечать ответы как кэшируемые, чтобы браузер переиспользовал
уже полученные превью, а повторное открытие альбома не вызывало повторную загрузку
неизменившихся объектов.
#### Scenario: Заголовки кэширования
- **WHEN** клиент получает объект (оригинал, превью или thumb) по подписанному URL
- **THEN** ответ содержит заголовок кэширования с ограниченным сроком годности
#### Scenario: Повторный показ без перезагрузки
- **WHEN** сетка превью перерисовывается, а URL объектов не изменились
- **THEN** браузер отдаёт объекты из кэша без повторного запроса к бэкенду
### Requirement: Медиа-запись фото
Данные фото SHALL включать вид медиа (`image` | `video`), длительность для видео и адрес
воспроизведения, когда видео-вариант готов; для изображений адрес воспроизведения
отсутствует.
#### Scenario: Запись изображения
- **WHEN** клиент получает данные изображения
- **THEN** вид медиа — изображение, длительность и адрес воспроизведения отсутствуют
#### Scenario: Запись видео без готового варианта
- **WHEN** клиент получает данные видео, а вариант ещё не готов
- **THEN** вид медиа — видео, длительность присутствует, адрес воспроизведения отсутствует
#### Scenario: Запись видео с готовым вариантом
- **WHEN** видео-вариант готов
- **THEN** данные медиа содержат адрес воспроизведения
### Requirement: Очистка корзины
Пользователь SHALL иметь возможность окончательно удалить все элементы корзины одним действием.
Система SHALL удалять только те элементы, которые пользователь вправе удалить навсегда, и SHALL
пропускать остальные. Операция SHALL возвращать число удалённых элементов.
#### Scenario: Очистка корзины
- **WHEN** пользователь очищает корзину
- **THEN** все элементы, которые он вправе удалить навсегда, удаляются
#### Scenario: Чужие элементы
- **WHEN** в корзине есть фото, которые пользователь не вправе удалить навсегда
- **THEN** они остаются в корзине, а остальные удаляются
#### Scenario: Пустая корзина
- **WHEN** пользователь очищает пустую корзину
- **THEN** операция завершается успешно, удалив ноль элементов
### Requirement: Контрольная сумма оригинала
Данные фото SHALL включать контрольную сумму содержимого оригинала, чтобы клиенты
могли сопоставлять локальные файлы с серверными фото и избегать повторной загрузки.
Контрольная сумма SHALL быть детерминированной для одного и того же содержимого.
#### Scenario: Выдача контрольной суммы
- **WHEN** клиент получает данные фото, у которого зарегистрирован объект-оригинал
- **THEN** ответ содержит контрольную сумму оригинала
#### Scenario: Одинаковое содержимое
- **WHEN** два фото ссылаются на объекты с одинаковым содержимым
- **THEN** их контрольные суммы совпадают
#### Scenario: Контрольная сумма доступна в списке
- **WHEN** клиент запрашивает список фото альбома
- **THEN** контрольные суммы входят в данные полученных фото
### 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** правки считаются пустыми и фото не получает новую отредактированную версию
### Requirement: Пипетка баланса белого
Клиент SHALL позволять выбрать нейтральную точку на фото и SHALL устанавливать температуру и оттенок так, чтобы цвет этой точки стал нейтральным. Выборка SHALL браться из исходного (неотредактированного) превью, а пресет баланса белого SHALL переходить в «Пользовательский».
#### Scenario: Выбор нейтральной точки
- **WHEN** пользователь включает пипетку и кликает по нейтральному участку фото
- **THEN** температура и оттенок обновляются, и цвет выбранной точки становится нейтральным
#### Scenario: Пипетка доступна только в просмотрщике
- **WHEN** фото не открыто в просмотрщике
- **THEN** пипетка недоступна
### Requirement: Живой предпросмотр цветовых правок
Клиент SHALL показывать результат цветовых правок в просмотрщике до завершения серверного рендера. После завершения рендера SHALL показываться серверная версия.
#### Scenario: Правки видны сразу
- **WHEN** пользователь двигает цветовой контрол
- **THEN** просмотрщик обновляет изображение без ожидания серверного рендера
#### Scenario: Серверная версия заменяет предпросмотр
- **WHEN** серверный рендер цветовых правок завершён
- **THEN** просмотрщик показывает отрендеренную версию
### Requirement: Тоновая кривая
Тоновая кривая SHALL задаваться контрольными точками для каналов RGB, R, G и B. Крайние точки SHALL существовать по умолчанию, SHALL двигаться по обеим осям и SHALL задавать плоские участки вне себя. Кривая SHALL быть плавной и монотонной между точками; первый клик по полю SHALL добавлять точку между крайними. Сброс SHALL возвращать линейную кривую.
#### Scenario: Крайние узлы существуют с самого начала
- **WHEN** пользователь открывает инструмент «Кривая»
- **THEN** у каждого канала уже есть крайние точки, и их можно перетаскивать
#### Scenario: Добавление точки
- **WHEN** пользователь кликает по свободному полю кривой
- **THEN** между крайними точками добавляется новая точка, а крайние остаются на месте
#### Scenario: Подвижные крайние узлы
- **WHEN** пользователь двигает крайнюю точку по x или y
- **THEN** вне крайних точек кривая остаётся плоской, а между точками — плавной
#### Scenario: Плавная интерполяция
- **WHEN** кривая сохранена и отрендерена
- **THEN** предпросмотр и итоговое фото используют одну и ту же плавную монотонную кривую
#### Scenario: Встроенные пресеты
- **WHEN** пользователь выбирает «Линейный», «Средний контраст» или «Сильный контраст»
- **THEN** кривые всех четырёх каналов заменяются кривыми пресета (контраст — в RGB, R/G/B — линейные), а подпись показывает выбранный пресет
#### Scenario: Пользовательская правка
- **WHEN** пользователь вручную меняет точки кривой
- **THEN** подпись показывает «Пользовательский»
#### Scenario: Сброс
- **WHEN** пользователь сбрасывает кривую
- **THEN** текущий канал возвращается к линейной кривой с крайними точками
#### Scenario: Независимые каналы
- **WHEN** пользователь выбирает канал R, G или B
- **THEN** он меняет только выбранный канал, остальные каналы остаются прежними
### Requirement: Переключение фото в редакторе
При переключении фото правая панель SHALL сохранять выбранную вкладку. Несохранённые правки SHALL сохраняться тому фото, для которого они сделаны, в том числе если пользователь уже переключился на другое фото. Ответы фоновых запросов SHALL NOT подменять текущее фото или его параметры.
#### Scenario: Вкладка не меняется
- **WHEN** пользователь переключает фото
- **THEN** выбранная вкладка правой панели остаётся прежней
#### Scenario: Правки уходят текущему фото
- **WHEN** пользователь меняет настройки после переключения
- **THEN** изменения сохраняются текущему фото, а не предыдущему
#### Scenario: Фоновое сохранение не подменяет фото
- **WHEN** сохранение предыдущего фото завершается после переключения
- **THEN** панель продолжает показывать текущее фото, а его параметры не меняются
### Requirement: Пользовательские пресеты кривой
Пользователь SHALL сохранять текущие кривые всех каналов (RGB, R, G и B) как именованный пресет, применять, переименовывать и удалять его. Пресет SHALL содержать кривую для каждого из четырёх каналов. Пресеты SHALL храниться в профиле пользователя и SHALL быть приватными. Импорт и экспорт SHALL выполняться JSON-файлом; импорт SHALL добавлять пресеты в профиль.
#### Scenario: Сохранение пресета
- **WHEN** пользователь сохраняет текущие кривые под именем
- **THEN** пресет появляется в списке и сохраняется в профиле вместе с кривыми RGB, R, G и B
#### Scenario: Применение пресета
- **WHEN** пользователь выбирает пользовательский пресет
- **THEN** кривые всех четырёх каналов заменяются сохранёнными, а текущий канал не меняется
#### Scenario: Переименование и удаление
- **WHEN** пользователь переименовывает или удаляет пресет
- **THEN** список пресетов и профиль обновляются
#### Scenario: Приватность пресетов
- **WHEN** другой пользователь открывает свои пресеты
- **THEN** он не видит пресеты первого пользователя
#### Scenario: Экспорт пресетов
- **WHEN** пользователь экспортирует пресеты
- **THEN** он получает JSON-файл с пользовательскими пресетами и кривыми всех четырёх каналов
#### Scenario: Импорт пресетов
- **WHEN** пользователь импортирует корректный JSON-файл
- **THEN** пресеты добавляются или обновляются в его профиле
#### 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** вкладка «Редактирование» скрыта, панель показывает «Сведения»