Skip to content

feat(R3.6): ambient ("Ambilight") + theater/fullscreen modes#59

Merged
detain merged 1 commit into
masterfrom
feat/R3-6-ambient-theater
Jun 2, 2026
Merged

feat(R3.6): ambient ("Ambilight") + theater/fullscreen modes#59
detain merged 1 commit into
masterfrom
feat/R3-6-ambient-theater

Conversation

@detain

@detain detain commented Jun 2, 2026

Copy link
Copy Markdown
Owner

R3.6 — Ambient ("Ambilight") + theater/fullscreen modes

The player's ambient glow is now live (canvas-sampled from the video frame), and theater mode is wired. Eighth step of the R3 player track; the locked art direction is the R0 mockup src/dev/mockups/player-chrome.html (.player-wrap::before ambient halo), now made dynamic.

Added

  • src/components/player/ambient.ts — pure, DOM-free helpers: averageRegion (clamped-bounds RGBA averaging; empty rect → black), sampleAmbient (left/right/center colors), rgbString/rgbaString (alpha-clamped), ambientGradient (the mockup's 3-glow radial layout; intensity scales alpha), isBatterySaving (discharging && level ≤ 0.2), + AMBIENT_SAMPLE_W/H (32×18) and AMBIENT_SAMPLE_INTERVAL_MS (250 ≈ 4 Hz).
  • src/components/player/AmbientCanvas.vue — the live "Ambilight" layer. A 32×18 offscreen canvas is sampled on a throttled requestVideoFrameCallback loop (rVFC only fires while the video paints → paused video stops sampling for free; a setInterval fallback while playing when rVFC is absent), painted as a layered radial-gradient glow that spills beyond the framed box. Fully disable-able (off when prefs.atmosphere is false, under reduced-motion, or under a best-effort navigator.getBattery() battery-saver heuristic); degrades to a static fallback glow with no loop under jsdom / SSR / no-canvas / tainted cross-origin frames. pointer-events:none + aria-hidden.

Changed

  • Player.vue — restructured the root into a non-clipping positioning wrapper holding <AmbientCanvas> (z 0, behind, negative inset so the halo spills past the frame) and a new .player__stage (z 1, overflow:hidden, the framed black box). Theater mode: the t shortcut + a new control-bar theater button (aria-pressed) toggle a widened, edge-to-edge layout with a brighter ambient surround and emit theater(active: boolean) for the host page to widen its column + dim the surroundings (PlayerPage, R3.9). True fullscreen unchanged; its button stays last.
  • index.ts — export AmbientCanvas + the ambient helpers/types.

Acceptance criteria

  • ✅ Ambient is canvas-sampled at a throttled cadence (≈4 Hz, 32×18 buffer → well under the <2ms/frame budget) and fully disable-able; static fallback under jsdom/no-canvas.
  • ✅ Theater toggles layout (widen + dim) without reload via the t key or the control-bar button and emits its boolean state.
  • ✅ Fullscreen still works. Anti-slop clean (icons only).

Out of scope (later R3 steps)

PiP/Media-Session/mini-player (R3.7); resume/up-next + mkv/hevc transcode notice (R3.8); PlayerPage page-level dim/widen + poster backdrop (R3.9).

Gates (run by the coordinator)

  • vue-tsc --noEmit clean · vite 8 npm run build clean
  • vitest run 1241 passing (+40: ambient 17, AmbientCanvas 19, Player +4)
  • Coverage: ambient.ts 100% lines · AmbientCanvas.vue 100% lines (98.07% stmts) · components/player 99.56% lines · overall 97.1% lines
  • Anti-slop grep clean over shipped src

Review

Reviewer subagent → 3 findings (all LOW): #1 latent rVFC-handle leak on a mid-loop video swap (FIXED — per-handle rvfcVideo tracking + clean watch restart), #2 two teardown paths not explicitly tested (FIXED — added interval-clear + battery-listener-removal tests), #3 no-change confirmation. Re-verify: NO FINDINGS.

🤖 Generated with Claude Code

Make the player's ambient glow live and wire theater mode.

- ambient.ts: pure, DOM-free helpers (averageRegion, sampleAmbient,
  rgbString/rgbaString, ambientGradient, isBatterySaving + 32x18 sample
  size and ~4 Hz cadence constants).
- AmbientCanvas.vue: live "Ambilight" layer. A 32x18 offscreen canvas is
  sampled on a throttled requestVideoFrameCallback loop (setInterval
  fallback while playing when rVFC is absent) and painted as a layered
  radial-gradient glow that spills beyond the framed video box. Fully
  disable-able (off when prefs.atmosphere is false, under reduced-motion,
  or battery-saver); static fallback glow with no loop under
  jsdom/SSR/no-canvas/tainted frames. pointer-events:none + aria-hidden.
- Player.vue: restructured the root into a non-clipping wrapper holding
  AmbientCanvas (behind) + a new .player__stage (the framed, clipped box).
  Theater mode via the `t` key + a control-bar button (aria-pressed):
  widens edge-to-edge, brightens the ambient surround, and emits
  theater(active: boolean) for the host page (R3.9). Fullscreen unchanged
  and its button stays last.
- index.ts: export AmbientCanvas + the ambient helpers/types.

Gates: vue-tsc clean; vite 8 build clean; vitest 1241 passing (+40);
ambient.ts 100% lines, AmbientCanvas.vue 100% lines, components/player
99.56% lines; overall 97.1% lines. Anti-slop clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@detain detain merged commit 873dbb0 into master Jun 2, 2026
@detain detain deleted the feat/R3-6-ambient-theater branch June 2, 2026 21:41
detain added a commit that referenced this pull request Jun 4, 2026
Make the player's ambient glow live and wire theater mode.

- ambient.ts: pure, DOM-free helpers (averageRegion, sampleAmbient,
  rgbString/rgbaString, ambientGradient, isBatterySaving + 32x18 sample
  size and ~4 Hz cadence constants).
- AmbientCanvas.vue: live "Ambilight" layer. A 32x18 offscreen canvas is
  sampled on a throttled requestVideoFrameCallback loop (setInterval
  fallback while playing when rVFC is absent) and painted as a layered
  radial-gradient glow that spills beyond the framed video box. Fully
  disable-able (off when prefs.atmosphere is false, under reduced-motion,
  or battery-saver); static fallback glow with no loop under
  jsdom/SSR/no-canvas/tainted frames. pointer-events:none + aria-hidden.
- Player.vue: restructured the root into a non-clipping wrapper holding
  AmbientCanvas (behind) + a new .player__stage (the framed, clipped box).
  Theater mode via the `t` key + a control-bar button (aria-pressed):
  widens edge-to-edge, brightens the ambient surround, and emits
  theater(active: boolean) for the host page (R3.9). Fullscreen unchanged
  and its button stays last.
- index.ts: export AmbientCanvas + the ambient helpers/types.

Gates: vue-tsc clean; vite 8 build clean; vitest 1241 passing (+40);
ambient.ts 100% lines, AmbientCanvas.vue 100% lines, components/player
99.56% lines; overall 97.1% lines. Anti-slop clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant