Files
scrying-pool/_bmad-output/implementation-artifacts/5-3-freeform-layout-for-floating-camera-windows.md
T
uberwald 76ce992505
CI / ci (push) Successful in 40s
Release Creation / build (release) Successful in 46s
Video over token, free-form video windows
2026-06-07 22:18:08 +02:00

200 lines
7.8 KiB
Markdown

# Story 5.3: Freeform Layout for Floating Camera Windows
**Status:** ready-for-dev
**Epic:** 5 - Full AV Replacement
**Story Key:** 5-3-freeform-layout-floating-windows
**Created:** 2026-06-07
**Last Updated:** 2026-06-07
**Target Version:** v0.2.0
---
## Story Header
| Field | Value |
|-------|-------|
| **Epic** | 5 - Full AV Replacement |
| **Story ID** | 5.3 |
| **Story Key** | 5-3-freeform-layout-floating-windows |
| **Title** | Freeform Layout for Floating Camera Windows |
| **Status** | ready-for-dev |
| **Priority** | Medium |
| **Assigned Agent** | DEV |
| **Created** | 2026-06-07 |
| **Last Updated** | 2026-06-07 |
---
## Story Requirements
### User Story
**As a** GM using Scrying Pool,
**I want to** select a "Windows" layout mode where each participant's camera feed appears in its own freely draggable and resizable window,
**So that** I can arrange camera feeds anywhere on my screen, resize them independently, and close/hide participants individually.
### Acceptance Criteria
#### AC-1: Layout Selector
**Given** the module is active and GM opens the Director's Board
**When** the GM looks at the layout selector
**Then** there is a 7th layout button labeled "Windows" (icon: `fa-window-restore`)
**And** clicking it switches to freeform mode
**And** the previously active layout (e.g. vertical/horizontal/mosaic) is replaced — freeform is mutually exclusive
#### AC-2: Per-Participant Floating Windows
**Given** the GM has selected the freeform layout
**Then** each visible participant (not hidden from table) gets their own `ApplicationV2` floating window
**And** the GM's own feed is included if `showGMSelfFeed` is enabled and GM has video active
**And** each window displays the participant's webcam feed as an `<video>` element with `object-fit: cover`
**And** each window has the participant's name in the title bar
#### AC-3: Drag and Resize
**Given** there is a freeform camera window
**When** the GM drags the window by its header
**Then** the window moves freely to any screen position
**And** when the GM resizes the window (via the resize handle in the bottom-right corner)
**Then** the new dimensions are applied
**And** both position and size are persisted globally (same positions on all scenes)
#### AC-4: Position Persistence
**Given** the GM has arranged freeform windows at certain positions and sizes
**When** the page is reloaded or the GM re-enters freeform mode
**Then** all windows restore to their last saved positions and sizes
#### AC-5: Cascade for New Participants
**Given** a new participant appears (first time or no saved position)
**Then** their window appears at the top-left of the screen with a cascading offset (50,50 + 30px each step)
**And** the cascade wraps around after ~300px offset so windows don't go off-screen
#### AC-6: Volume Control
**Given** a freeform camera window is open
**Then** there is a volume slider in the window footer (range input, 0 to 1, step 0.05)
**And** moving the slider changes the volume of that window's video element
**And** volume is not persisted (resets to 100% on reload)
#### AC-7: Window Controls
**Given** a freeform camera window is open
**Then** the window title bar has two control buttons: "Spotlight" (star icon) and "Hide" (eye-slash icon)
**And** clicking "Spotlight" toggles a visual glow effect (golden box-shadow/outline) on that window
**And** clicking "Hide" hides that participant from the table (same as hiding from the strip or Directors Board)
**And** clicking the window's close (X) button also hides that participant
#### AC-8: Visual Spotlight (Glow Effect)
**Given** a participant is spotlighted in freeform mode
**Then** their window shows a glowing golden border/outline
**And** all other windows remain unchanged (no resize, no hiding)
**And** clicking the spotlight button again removes the glow
**And** only one participant can be spotlighted at a time (switching moves the glow)
#### AC-9: Mode Switching
**Given** the GM is in freeform mode
**When** the GM selects another layout (vertical/horizontal/mosaic)
**Then** all freeform windows are closed
**And** the strip layout opens/re-renders normally
**Given** the GM is in strip mode (not freeform)
**When** the GM selects the freeform layout
**Then** the strip is closed (not rendered)
**And** freeform windows are created for all visible participants
**And** saved positions are restored
#### AC-10: Sync with Visibility Changes
**Given** a participant is hidden (via Directors Board or strip) while in freeform mode
**Then** their freeform window closes
**And** when they are shown again, their window re-opens at its last saved position
**Given** a participant connects or disconnects while in freeform mode
**Then** windows are created/destroyed accordingly
---
## Implementation Plan
### Files to Create
| File | Purpose |
|------|---------|
| `src/ui/gm/FreeformCameraWindow.js` | Individual ApplicationV2 window per participant |
| `src/ui/gm/FreeformLayoutManager.js` | Orchestrates window creation/destruction/sync |
| `templates/freeform-camera.hbs` | Template for each camera window |
| `styles/components/_freeform-camera.less` | Styles for floating camera windows |
### Files to Modify
| File | Change |
|------|--------|
| `module.js` | Import FreeformLayoutManager, construct in ready, add life cycle hooks |
| `src/ui/gm/DirectorsBoard.js` | Add freeform to `DOCK_LAYOUTS`, handle `set-dock-layout` |
| `templates/directors-board.hbs` | Add freeform layout button |
| `lang/en.json` | Add freeform layout i18n keys |
| `lang/fr.json` | Add freeform layout i18n keys |
| `styles/scrying-pool.less` | Import `_freeform-camera.less` |
### Data Contracts
#### World Setting: `freeformLayout`
```json
{
"windows": {
"userId1": { "left": 100, "top": 200, "width": 320, "height": 300 },
"userId2": { "left": 450, "top": 200, "width": 320, "height": 300 }
}
}
```
Not user-visible in config (`config: false`).
---
## Dev Notes
### Architecture
```
FreeformLayoutManager (adapter, controller, stateStore)
├── init() → registers hooks
├── sync() → reconciles visible users with open windows
├── setSpotlight(userId) → toggles visual glow
├── destroy() → closes all windows
└── Map<userId, FreeformCameraWindow>
└── FreeformCameraWindow ({userId, adapter, manager, position})
├── ApplicationV2 window with resizable: true
├── _onRender() → _attachVideo()
├── _onPosition() → manager._scheduleSave()
├── _onClickWindowControl() → spotlight/hide actions
├── _onClose() → _detachVideo(), hide participant
└── Volume slider → videoElement.volume (session only)
```
### Key Decisions (from user discussions)
| Decision | Choice |
|----------|--------|
| Volume persistence | Session only (default 1.0) |
| GM self-feed | Included if `showGMSelfFeed` + GM has video |
| Spotlight behavior | Visual glow only — no resize/hide of others |
| Default position | Top-left cascade: (50,50) + 30px per window, wrap at ~300px |
| Close button | Hides participant from table |
| Hide button | Hidden via controller.action |
### ApplicationV2 Patterns
- **Window controls** in `window.controls` array → override `_onClickWindowControl(event)`
- **Position tracking** → override `_onPosition(position)` → calls manager's save
- **Fallback base class** for test env → follow same pattern as ScryingPoolStrip
- **Constructor side-effect-free** → no render/init in constructor
- **Template** uses Handlebars `localize` helper for i18n
### Stream Access
- Get stream: `adapter.webrtc.getMediaStreamForUser(userId)`
- Create video: `document.createElement('video')``video.srcObject = stream`
- Autoplay + playsInline + mute (mute only for current user)
- Cleanup: `video.pause()`, `video.srcObject = null`, `video.remove()`