- Detect 'pano360' tag and open the photo in a spherical viewer - Mini-WebGL equirect viewer (no deps): drag to look, wheel FOV, Space reset - Load original as origin-clean bitmap; fall back to flat image without WebGL - 360 indicator in viewer and badge on grid tiles
91 lines
6.9 KiB
Markdown
91 lines
6.9 KiB
Markdown
# Design
|
|
|
|
## Context
|
|
|
|
См. proposal.md — Why. См. сценарии в specs/ui-workspace/spec.md. Опорные факты по коду:
|
|
|
|
- `Viewer.svelte` — единая область просмотра: `scroller` c `<img>` (плоский зум/панорамирование,
|
|
`Space`, колесо), топбар, лента миниатюр, контекстное меню, `f`/fullscreen. Используется и в
|
|
маршруте фото, и на публичной share-странице.
|
|
- Теги персональные (`tags.user_id`); `photo.tags` приходят и в списке, и в `/photos/:id`.
|
|
На share-страницах `GetPublic` читает фото с пустым пользователем → тегов нет.
|
|
- Варианты: `thumb` 200 и `preview` 1200 (webp); плюс `original_url` (подписанный, 1 час).
|
|
- В проекте нет runtime-зависимостей; статика вшивается в Go-бинарник.
|
|
|
|
## Goals / Non-Goals
|
|
|
|
**Goals:**
|
|
- 360-обзор для фото с личным тегом `pano360`, без сторонних библиотек.
|
|
- Сохранить плоский просмотр и все прочие функции Viewer без регрессий.
|
|
- Деградация до плоского изображения, когда 360 невозможен.
|
|
|
|
**Non-Goals:**
|
|
- Бэкенд, новый pano-вариант, автоопределение GPano/XMP.
|
|
- 360 на share-страницах и у других участников (теги персональные).
|
|
- Редактор панорам, горячие точки, стерео/VR.
|
|
|
|
## Decisions
|
|
|
|
### 1. Детект: личный тег `pano360`
|
|
`isPanoTag(photo)` — `photo.tags?.some(t => t.name.trim().toLowerCase() === 'pano360')`.
|
|
Источник — уже отдаваемые теги; бэкенд не трогаем. Следствие: на share и у других участников
|
|
режим не включается (осознанное ограничение, покрыто сценарием спеки).
|
|
|
|
### 2. Рендер: мини-WebGL equirect без зависимостей
|
|
Отдельный `PanoViewer.svelte`, владеющий `<canvas>`:
|
|
|
|
```
|
|
экран (uv) --inv(fov,yaw,pitch)--> луч dir --atan2/asin--> lon/lat --> equirect uv --> sample
|
|
```
|
|
|
|
- Вершинный шейдер: полноэкранный quad.
|
|
- Фрагментный: направление луча из `uv` + `uFov/uYaw/uPitch/uRes`; выборка текстуры equirect.
|
|
- WebGL1: NPOT-текстура → `CLAMP_TO_EDGE`/`LINEAR`, без мипмапов.
|
|
- `ResizeObserver` + `devicePixelRatio` для ресайза канвы.
|
|
|
|
Альтернативы: three.js (быстрее писать, но +~150 КБ gz и первая runtime-зависимость) и готовая
|
|
панорамная библиотека (чужая тема/UI). Отклонены в пользу 0 зависимостей.
|
|
|
|
### 3. Текстура: оригинал с даунскейлом, фолбэк preview
|
|
`original_url` загружается через `fetch` того же origin (подписанный URL может быть абсолютным на
|
|
хост API) → `blob` → `createImageBitmap`, чтобы пиксели были origin-clean (иначе `texImage2D`
|
|
бросает «cross-origin data»). Далее даунскейл до `min(4096, gl.MAX_TEXTURE_SIZE)` и `texImage2D`.
|
|
Если оригинал недоступен — `preview_url`. Новый pano-вариант не заводим (иначе бэкенд).
|
|
|
|
### 3a. Жизненный цикл WebGL
|
|
GL инициализируется один раз на элемент канвы; повторные запуски эффекта (в т.ч. из-за
|
|
`bind:this`) — no-op. Освобождение ресурсов (текстура/программа, `WEBGL_lose_context`) — только
|
|
при уничтожении компонента. Раньше `teardown()` вызывался на перезапуске эффекта, терял контекст,
|
|
и повторная инициализация компилировала шейдеры на «потерянном» контексте → ложный фолбэк.
|
|
|
|
### 4. Фолбэк на плоский `<img>`
|
|
`PanoViewer` вызывает `onFallback()` при отсутствии WebGL-контекста или сбое загрузки/декодирования;
|
|
`Viewer` тогда рендерит обычный `<img>` и плоские жесты.
|
|
|
|
### 5. Разделение ответственности и интеракции
|
|
- `Viewer` владеет топбаром, лентой, клавишами и контекстным меню.
|
|
- `PanoViewer` владеет канвой, указателем и колесом: drag/свайп — `yaw/pitch`, колесо — `fov`
|
|
(клэмп, напр. 30°..100°).
|
|
- `Space` в `Viewer` в 360-режиме вызывает `panoRef.reset()` (вместо плоского `toggleActual`).
|
|
- В 360-режиме плоский зум/панорамирование и `wheel`/`pointer`-обработчики scroller не активны;
|
|
свайп не листает фото (смена — стрелками и лентой); `f`/fullscreen и стрелки — как прежде.
|
|
- Компонент создаётся с `key={current.id}`, ресурсы освобождаются при смене фото/размонтировании.
|
|
|
|
### 6. Индикация
|
|
Признак «360» в топбаре при активном режиме; бейдж «360» на плитке сетки для фото с тегом
|
|
(теги есть в списочном ответе).
|
|
|
|
## Risks / Trade-offs
|
|
|
|
- [Оригинал больше лимита текстуры/много VRAM] → даунскейл до `min(4096, MAX_TEXTURE_SIZE)`.
|
|
- [WebGL недоступен] → `onFallback()` и плоский `<img>`.
|
|
- [Конфликт жестов с плоским режимом] → режим-специфичные обработчики; свайп в 360 только осмотр.
|
|
- [Утечки WebGL-контекста] → `deleteTexture/deleteProgram/loseContext` при размонтировании/смене фото.
|
|
- [Личный тег: 360 не виден другим/на share] → покрыто сценарием спеки как ожидаемое поведение.
|
|
- [Производительность на слабых GPU] → простая сцена (один quad), опционально ограничение DPR.
|
|
|
|
## Migration Plan
|
|
|
|
Только фронтенд; данных и API не трогаем. Откат — удаление нового режима. Проверка:
|
|
`cd frontend && npm run check` и `npm run build`, плюс панo-сценарии в CDP‑QA.
|