viewer_sgos / fullscreen_playcanvas.js
MikaFil's picture
Update fullscreen_playcanvas.js
4a72230 verified
raw
history blame
11.7 kB
// fullscreen.js
// Intègre un projet PlayCanvas via une balise <script data-src="..." data-aspect="16:9">
// Gère le plein écran natif (desktop/Android) et fake-fullscreen (iOS).
(function () {
// ─── 1. Localiser la balise <script> ────────────────────────────────────────
const scriptTag =
document.currentScript ||
(function () {
const all = document.getElementsByTagName('script');
for (let i = all.length - 1; i >= 0; i--) {
if (all[i].src && all[i].src.includes('fullscreen.js')) return all[i];
}
return all[all.length - 1];
})();
const playcanvasUrl = scriptTag.getAttribute('data-src');
if (!playcanvasUrl) {
console.warn('[fullscreen.js] Attribut data-src manquant.');
return;
}
// ─── 2. DΓ©tection plateforme ─────────────────────────────────────────────────
const isIOS =
/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
const isMobile =
isIOS || /Android/i.test(navigator.userAgent);
// ─── 3. ID d'instance (support multi-embed sur la mΓͺme page) ─────────────────
const id = Math.random().toString(36).substr(2, 8);
// ─── 4. Calcul du padding-bottom (aspect ratio) ──────────────────────────────
function computeAspectPadding(aspectStr) {
if (!aspectStr) return null;
if (aspectStr.includes(':')) {
const [w, h] = aspectStr.split(':').map(Number);
if (w > 0 && h > 0) return (h / w) * 100 + '%';
} else {
const v = parseFloat(aspectStr);
if (v > 0) return (100 / v) + '%';
}
return null;
}
const aspectPadding =
computeAspectPadding(scriptTag.getAttribute('data-aspect')) || '56.25%'; // dΓ©faut 16:9
// ─── 5. Injection du CSS ─────────────────────────────────────────────────────
const style = document.createElement('style');
style.textContent = `
.pc-embed-wrapper-${id} {
position: relative;
width: 100%;
height: 0;
padding-bottom: ${aspectPadding};
overflow: hidden;
background: #000;
box-sizing: border-box;
}
.pc-embed-wrapper-${id}.fake-fullscreen {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
max-width: 100vw !important;
max-height: 100vh !important;
padding-bottom: 0 !important;
margin: 0 !important;
z-index: 99999;
}
.pc-embed-inner-${id} {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
}
.pc-embed-inner-${id} iframe {
width: 100%; height: 100%;
border: none;
display: block;
}
.pc-fs-btn-${id} {
position: absolute;
bottom: 10px;
right: 10px;
z-index: 10;
width: 36px;
height: 36px;
border-radius: 50%;
border: none;
background: rgba(0,0,0,0.55);
color: #fff;
font-size: 18px;
line-height: 36px;
text-align: center;
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
transition: background 0.2s;
}
.pc-fs-btn-${id}:hover {
background: rgba(0,0,0,0.8);
}
`;
document.head.appendChild(style);
// ─── 6. Construction du DOM ──────────────────────────────────────────────────
const wrapper = document.createElement('div');
wrapper.className = `pc-embed-wrapper-${id}`;
wrapper.setAttribute('data-original-padding', aspectPadding);
const inner = document.createElement('div');
inner.className = `pc-embed-inner-${id}`;
const iframe = document.createElement('iframe');
iframe.src = playcanvasUrl;
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allow', 'autoplay; fullscreen');
iframe.setAttribute(
'sandbox',
'allow-scripts allow-same-origin allow-pointer-lock allow-popups allow-forms'
);
const fsBtn = document.createElement('button');
fsBtn.className = `pc-fs-btn-${id}`;
fsBtn.title = 'Plein Γ©cran';
fsBtn.textContent = '⇱';
fsBtn.setAttribute('aria-label', 'Plein Γ©cran');
inner.appendChild(iframe);
wrapper.appendChild(inner);
wrapper.appendChild(fsBtn);
scriptTag.parentNode.insertBefore(wrapper, scriptTag.nextSibling);
// ─── 7. Γ‰tat fullscreen ──────────────────────────────────────────────────────
let isFullscreen = false;
let savedPadding = aspectPadding;
let savedParent = null;
let savedNextSibling = null;
// ─── 8. Styles communs fullscreen (sans dΓ©placement DOM) ─────────────────────
// UtilisΓ© par le fullscreen natif (desktop/Android).
// NE PAS modifier cette fonction pour iOS β€” voir applyFakeFullscreenStyles.
function applyFullscreenStyles() {
wrapper.style.position = 'fixed';
wrapper.style.top = '0';
wrapper.style.left = '0';
wrapper.style.width = '100vw';
wrapper.style.height = '100vh';
wrapper.style.maxWidth = '100vw';
wrapper.style.maxHeight = '100vh';
wrapper.style.paddingBottom = '0';
wrapper.style.margin = '0';
wrapper.style.zIndex = '99999';
wrapper.classList.add('fake-fullscreen');
fsBtn.textContent = '⇲';
isFullscreen = true;
}
// ─── 9. Fake-fullscreen iOS avec tΓ©lΓ©portation DOM + correction hauteur ───────
// Sur iOS Safari, 100vh inclut la barre de navigation β†’ le bas est masquΓ©.
// On utilise 100dvh (dynamic viewport height) si supportΓ©, sinon on compense
// avec env(safe-area-inset-bottom).
// La tΓ©lΓ©portation dans <body> permet d'Γ©chapper aux stacking contexts parents.
function applyFakeFullscreenStyles() {
savedParent = wrapper.parentNode;
savedNextSibling = wrapper.nextSibling;
document.body.appendChild(wrapper);
// Styles communs
applyFullscreenStyles();
// Correction hauteur iOS uniquement
if (isIOS) {
if (CSS.supports('height', '1dvh')) {
// iOS 16+ : dvh exclut exactement la barre Safari
wrapper.style.height = '100dvh';
wrapper.style.maxHeight = '100dvh';
} else {
// iOS 15 et moins : compensation via safe-area-inset-bottom
wrapper.style.height = 'calc(100vh - env(safe-area-inset-bottom, 0px))';
wrapper.style.maxHeight = 'calc(100vh - env(safe-area-inset-bottom, 0px))';
}
}
}
// ─── 10. Restaurer les styles normaux ────────────────────────────────────────
function restoreStyles() {
wrapper.style.position = '';
wrapper.style.top = '';
wrapper.style.left = '';
wrapper.style.width = '100%';
wrapper.style.height = '0';
wrapper.style.maxWidth = '';
wrapper.style.maxHeight = '';
wrapper.style.paddingBottom = savedPadding;
wrapper.style.margin = '';
wrapper.style.zIndex = '';
wrapper.classList.remove('fake-fullscreen');
fsBtn.textContent = '⇱';
isFullscreen = false;
// Remettre en place uniquement si on avait tΓ©lΓ©portΓ© (iOS / fallback)
if (savedParent) {
savedParent.insertBefore(wrapper, savedNextSibling);
savedParent = null;
savedNextSibling = null;
}
}
// ─── 11. EntrΓ©e en plein Γ©cran ───────────────────────────────────────────────
function enterFullscreen() {
savedPadding =
wrapper.getAttribute('data-original-padding') || aspectPadding;
if (isIOS) {
// iOS : fake-fullscreen avec tΓ©lΓ©portation DOM et correction hauteur
applyFakeFullscreenStyles();
document.body.style.overflow = 'hidden';
return;
}
// Tentative via l'API Fullscreen standard (desktop / Android)
const el = wrapper;
const req =
el.requestFullscreen ||
el.webkitRequestFullscreen ||
el.mozRequestFullScreen ||
el.msRequestFullscreen;
if (req) {
req.call(el).catch(() => {
// API refusΓ©e β†’ fallback fake avec tΓ©lΓ©portation (sans correction iOS)
applyFakeFullscreenStyles();
document.body.style.overflow = 'hidden';
});
// Sur succès, applyFullscreenStyles est appelé par onFullscreenChange
} else {
applyFakeFullscreenStyles();
document.body.style.overflow = 'hidden';
}
}
// ─── 12. Sortie du plein Γ©cran ───────────────────────────────────────────────
function exitFullscreen() {
if (
document.fullscreenElement === wrapper ||
document.webkitFullscreenElement === wrapper
) {
(document.exitFullscreen || document.webkitExitFullscreen || (() => {}))
.call(document)
.catch(() => {});
}
restoreStyles();
document.body.style.overflow = '';
}
// ─── 13. Bouton fullscreen ───────────────────────────────────────────────────
fsBtn.addEventListener('click', function (e) {
e.stopPropagation();
isFullscreen ? exitFullscreen() : enterFullscreen();
});
// Touch explicite pour iOS (Γ©vite le dΓ©lai 300ms)
fsBtn.addEventListener('touchend', function (e) {
e.preventDefault();
e.stopPropagation();
isFullscreen ? exitFullscreen() : enterFullscreen();
});
// ─── 14. Γ‰couter les changements natifs fullscreen ───────────────────────────
// NB : on appelle applyFullscreenStyles (sans dvh) car le fullscreen natif
// gΓ¨re lui-mΓͺme la hauteur β€” pas besoin de correction iOS ici.
function onFullscreenChange() {
const fsEl =
document.fullscreenElement || document.webkitFullscreenElement;
if (!fsEl && isFullscreen) {
restoreStyles();
document.body.style.overflow = '';
} else if (fsEl === wrapper && !isFullscreen) {
applyFullscreenStyles();
}
}
document.addEventListener('fullscreenchange', onFullscreenChange);
document.addEventListener('webkitfullscreenchange', onFullscreenChange);
// ─── 15. Touche Γ‰chap (fake-fullscreen iOS / fallback) ───────────────────────
document.addEventListener('keydown', function (e) {
if ((e.key === 'Escape' || e.key === 'Esc') && isFullscreen) {
exitFullscreen();
}
});
// ─── 16. Resize / orientation ────────────────────────────────────────────────
window.addEventListener('resize', function () {
if (isFullscreen) {
wrapper.style.width = '100vw';
wrapper.style.height = '100vh';
}
});
window.addEventListener('orientationchange', function () {
if (isFullscreen) {
setTimeout(function () {
wrapper.style.width = '100vw';
wrapper.style.height = '100vh';
}, 200);
}
});
})();