Files

6.9 KiB

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.