Video detail: trailer billboard reveals with a left→right wipe + fade

Replace the flat opacity crossfade with a soft, feathered left→right wipe of the
trailer in (mask via @property, falls back to crossfade) — "a rollover and a
fade". The wipe now fires ONLY when YouTube reports PLAYING (handshake + state
events), not on a blind timer, with a 4.5s safety net. When the trailer ENDS the
original backdrop fades back in and the iframe is torn down.
This commit is contained in:
BoulderBadgeDad 2026-06-15 16:55:00 -07:00
parent 80dd2ff21c
commit 5b6ef295f8
2 changed files with 69 additions and 4 deletions

View file

@ -724,6 +724,7 @@
// ── billboard autoplay trailer (opt-in setting) ───────────────────────────
var prefs = null, bbTrailerTimer = null, bbMuted = true;
var bbMsgHandler = null, bbRevealTimer = null;
function loadPrefs(cb) {
if (prefs) { cb(prefs); return; }
fetch('/api/video/prefs', { headers: { 'Accept': 'application/json' } })
@ -745,6 +746,7 @@
function startBillboardTrailer(key) {
var bb = q('.vd-billboard'); if (!bb || bb.querySelector('[data-vd-bb-trailer]')) return;
bbMuted = true;
bb.classList.remove('vd-billboard--restoring');
var wrap = document.createElement('div');
wrap.className = 'vd-bb-trailer'; wrap.setAttribute('data-vd-bb-trailer', '');
wrap.innerHTML = '<iframe allow="autoplay; encrypted-media" frameborder="0" ' +
@ -753,14 +755,52 @@
'<div class="vd-bb-tctrls"><button class="vd-bb-tbtn" type="button" data-vd-bb-mute aria-label="Unmute">🔇</button>' +
'<button class="vd-bb-tbtn" type="button" data-vd-bb-stop aria-label="Stop">✕</button></div>';
bb.appendChild(wrap);
var iframe = wrap.querySelector('iframe');
// Ask YouTube to report playback state, then reveal ONLY once it's truly
// PLAYING (state 1) — so the wipe doesn't fire over a black/buffering frame
// — and restore the backdrop when it ENDS (state 0).
function handshake() {
try {
iframe.contentWindow.postMessage(
'{"event":"listening","id":"vd-bb","channel":"widget"}', '*');
} catch (e) { /* cross-origin not ready yet */ }
}
iframe.addEventListener('load', function () { handshake(); setTimeout(handshake, 500); });
bbMsgHandler = function (e) {
if (!iframe.contentWindow || e.source !== iframe.contentWindow) return;
var msg; try { msg = JSON.parse(e.data); } catch (x) { return; }
var st = null;
if (msg && msg.event === 'infoDelivery' && msg.info && typeof msg.info.playerState === 'number') st = msg.info.playerState;
else if (msg && msg.event === 'onStateChange' && typeof msg.info === 'number') st = msg.info;
if (st === 1) revealBillboardTrailer(bb);
else if (st === 0) restoreBillboard(bb);
};
window.addEventListener('message', bbMsgHandler);
// Safety net: if YouTube never reports PLAYING (blocked handshake), reveal
// anyway so a playing-but-hidden trailer doesn't sit behind the backdrop.
bbRevealTimer = setTimeout(function () { revealBillboardTrailer(bb); }, 4500);
}
function revealBillboardTrailer(bb) {
clearTimeout(bbRevealTimer); bbRevealTimer = null;
bb.classList.remove('vd-billboard--restoring');
bb.classList.add('vd-billboard--trailer');
}
function restoreBillboard(bb) {
// Trailer finished → fade the original backdrop back in, then tear down.
bb.classList.remove('vd-billboard--trailer');
bb.classList.add('vd-billboard--restoring');
setTimeout(function () {
if (bb.classList.contains('vd-billboard--restoring')) stopBillboardTrailer();
}, 950);
}
function stopBillboardTrailer() {
clearTimeout(bbTrailerTimer); bbTrailerTimer = null;
clearTimeout(bbRevealTimer); bbRevealTimer = null;
if (bbMsgHandler) { window.removeEventListener('message', bbMsgHandler); bbMsgHandler = null; }
var ws = document.querySelectorAll('[data-vd-bb-trailer]');
for (var i = 0; i < ws.length; i++) ws[i].remove();
var bbs = document.querySelectorAll('.vd-billboard--trailer');
for (var j = 0; j < bbs.length; j++) bbs[j].classList.remove('vd-billboard--trailer');
var bbs = document.querySelectorAll('.vd-billboard--trailer, .vd-billboard--restoring');
for (var j = 0; j < bbs.length; j++) bbs[j].classList.remove('vd-billboard--trailer', 'vd-billboard--restoring');
}
function toggleBillboardMute(btn) {
bbMuted = !bbMuted;

View file

@ -1409,12 +1409,37 @@ a.vd-prov:hover img, a.vd-prov:hover .vd-prov-ph { border-color: rgba(var(--vd-a
}
/* ── billboard autoplay trailer ──────────────────────────────────────────── */
.vd-bb-trailer { position: absolute; inset: 0; z-index: 0; overflow: hidden; background: #000; animation: vd-fade 0.6s ease; }
.vd-bb-trailer { position: absolute; inset: 0; z-index: 0; overflow: hidden; background: #000; opacity: 0; }
.vd-bb-trailer iframe {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 100vw; height: 56.25vw; min-height: 100%; min-width: 177.78vh; border: 0; pointer-events: none;
}
.vd-billboard--trailer .vd-bb-bg { opacity: 0; transition: opacity 0.6s ease; }
/* Reveal only once the trailer is actually PLAYING (gated in video-detail.js).
The trailer wipes in leftright behind a soft feathered edge while it
crossfades, so the backdrop appears to roll away from the left AND fade
"both a rollover and a fade." @property animates the mask gradient smoothly;
browsers without it still get the opacity crossfade. */
@property --vd-wipe { syntax: '<percentage>'; inherits: false; initial-value: 0%; }
.vd-billboard--trailer .vd-bb-trailer {
-webkit-mask-image: linear-gradient(100deg, #000 var(--vd-wipe), transparent calc(var(--vd-wipe) + 16%));
mask-image: linear-gradient(100deg, #000 var(--vd-wipe), transparent calc(var(--vd-wipe) + 16%));
animation: vdTrailerWipeIn 1.15s cubic-bezier(0.62, 0.04, 0.18, 1) forwards;
}
@keyframes vdTrailerWipeIn { from { --vd-wipe: -16%; opacity: 0; } to { --vd-wipe: 116%; opacity: 1; } }
.vd-billboard--trailer .vd-bb-bg { animation: vdBgFadeOut 1.15s ease forwards; }
@keyframes vdBgFadeOut { from { opacity: 1; } to { opacity: 0; } }
/* Restore when the trailer ends, fade the original backdrop back in and the
trailer out, then it's torn down. */
.vd-billboard--restoring .vd-bb-bg { animation: vdBgFadeIn 0.9s ease forwards; }
@keyframes vdBgFadeIn { from { opacity: 0; } to { opacity: 1; } }
.vd-billboard--restoring .vd-bb-trailer { animation: vdTrailerFadeOut 0.7s ease forwards; }
@keyframes vdTrailerFadeOut { from { opacity: 1; } to { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
.vd-billboard--trailer .vd-bb-trailer { -webkit-mask-image: none; mask-image: none; animation: vd-fade 0.4s ease forwards; }
.vd-billboard--trailer .vd-bb-bg { animation: vdBgFadeOut 0.4s ease forwards; }
}
.vd-bb-tctrls { position: absolute; bottom: 20px; right: 24px; z-index: 4; display: flex; gap: 8px; }
.vd-bb-tbtn {
width: 38px; height: 38px; border-radius: 50%; border: 1px solid rgba(255, 255, 255, 0.25); cursor: pointer;