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

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.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()