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:
parent
80dd2ff21c
commit
5b6ef295f8
2 changed files with 69 additions and 4 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 left→right 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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue