- 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
6.9 KiB
Design
Context
См. proposal.md — Why. См. сценарии в specs/ui-workspace/spec.md. Опорные факты по коду:
Viewer.svelte— единая область просмотра:scrollerc<img>(плоский зум/панорамирование,Space, колесо), топбар, лента миниатюр, контекстное меню,f/fullscreen. Используется и в маршруте фото, и на публичной share-странице.- Теги персональные (
tags.user_id);photo.tagsприходят и в списке, и в/photos/:id. На share-страницахGetPublicчитает фото с пустым пользователем → тегов нет. - Варианты:
thumb200 иpreview1200 (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.