Video over token, free-form video windows
This commit is contained in:
+199
@@ -0,0 +1,199 @@
|
||||
# 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()`
|
||||
Reference in New Issue
Block a user