Files
OBS-overlay/docs/plans/2026-03-20-header-meter-design.md
2026-03-20 14:56:17 -04:00

84 lines
3.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Header Text Player Meter — Design
## Summary
Add a gradient fill meter to the "ROOM CODE:" header text that visually represents lobby fill (playerCount / maxPlayers). The fill sweeps left-to-right from the configured header color (pink) to white. A pulse/glow fires when the lobby is full.
Also: revert the player list checkbox to unchecked by default.
## Requirements
- Smooth CSS gradient across the header text, not per-character or per-word.
- Fill percentage = `playerCount / maxPlayers`, clamped to `[0, 1]`.
- 0 players → 100% pink. All players → 100% white.
- Gradient edge animates smoothly (~400ms ease) when player count changes.
- Brief pulse/glow animation when fill reaches 100%.
- Existing header settings (text, color, size, offset) continue to work. The configured header color becomes the "unfilled" color; white is always the "filled" color.
- Player list disabled by default.
## Approach
CSS `background-clip: text` with a dynamic `linear-gradient`.
### CSS Changes (`optimized-controls.html`)
Replace the static `#header` color with gradient-compatible styles:
```css
#header {
/* existing position, font, letter-spacing, opacity, transition stay */
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
/* drop-shadow replaces text-shadow (incompatible with transparent text) */
filter: drop-shadow(3px 3px 8px rgba(0, 0, 0, 0.8));
text-shadow: none;
}
```
New keyframes for the full-lobby pulse:
```css
@keyframes meter-full-pulse {
0% { filter: drop-shadow(3px 3px 8px rgba(0,0,0,0.8)); transform: scale(1) translateY(var(--header-offset)); }
50% { filter: drop-shadow(0 0 20px rgba(255,255,255,0.6)) drop-shadow(3px 3px 8px rgba(0,0,0,0.8)); transform: scale(1.05) translateY(var(--header-offset)); }
100% { filter: drop-shadow(3px 3px 8px rgba(0,0,0,0.8)); transform: scale(1) translateY(var(--header-offset)); }
}
#header.meter-full-pulse {
animation: meter-full-pulse 0.6s ease-out;
}
```
### JS Changes (`js/room-code-display.js`)
1. **Track meter state** — new private fields: `#meterFill` (current 01), `#meterTarget` (target 01), `#meterRafId`.
2. **`#applySettings()`** — replace `header.style.color = headerColor` with `header.style.background = linear-gradient(...)` using current `#meterFill` and the configured header color.
3. **`update(ctx)`** — compute new target from `ctx.playerCount / ctx.maxPlayers`. If different from current target, call `#animateMeterTo(newTarget)`.
4. **`#animateMeterTo(target)`** — `requestAnimationFrame` loop that interpolates `#meterFill` toward target over ~400ms with ease-out. Each frame calls `#applyMeterGradient()`.
5. **`#applyMeterGradient()`** — sets `header.style.background` to `linear-gradient(to right, #fff 0%, #fff ${pct}%, ${headerColor} ${pct}%, ${headerColor} 100%)` plus re-applies `background-clip` properties.
6. **`#triggerFullPulse()`** — adds `meter-full-pulse` class, removes after `animationend` event.
7. **`deactivate()`** — cancels RAF, resets `#meterFill` to 0.
### HTML Change (`optimized-controls.html`)
Remove `checked` from `<input type="checkbox" id="player-list-enabled" checked>`.
## Data Flow
```
WebSocket event (lobby.player-joined / player-count-updated)
→ OverlayManager updates context.playerCount
→ OverlayManager calls RoomCodeDisplay.update(ctx)
→ RoomCodeDisplay computes target = playerCount / maxPlayers
→ #animateMeterTo(target) runs RAF interpolation
→ When target reaches 1.0, #triggerFullPulse() fires
```
## Edge Cases
- `maxPlayers` is 0 or missing → fill stays at 0%.
- `playerCount > maxPlayers` → clamp to 100%.
- Rapid successive joins → each new target interrupts the current animation, smoothly redirecting.
- Lobby reset (new game) → `deactivate()` resets fill to 0; next `activate()` starts fresh.