7.8 KiB
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
{
"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.controlsarray → 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
localizehelper 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()