/* Module capture photo - Preview live, compte a rebours, declenchement */ let previewActif = false; let previewTimeoutId = null; let previewInterval = null; const PREVIEW_TIMEOUT_MS = 3000; let captureAbortee = false; let captureEnCours = false; // true pendant tout le flux lancerCapture() const EMOJI_CAR = { 3: '🤪', 2: '😱', 1: '🔥' }; // --- Surprise avant capture --- let _surprisePreloaded = null; function prechargerSurprise() { const surprise = (config || {}).surprise; if (!surprise || !surprise.actif || !surprise.fichier) return; if (surprise.type === 'video') return; const img = new Image(); img.src = '/api/surprise/media?t=' + Date.now(); _surprisePreloaded = img; } async function afficherSurprise() { const surprise = (config || {}).surprise; if (!surprise || !surprise.actif || !surprise.fichier) return; const delai = surprise.delai_ms || 1500; const overlay = document.getElementById('surprise-overlay'); if (!overlay) return; if (surprise.type === 'video') { overlay.innerHTML = ''; } else { if (_surprisePreloaded && _surprisePreloaded.complete) { _surprisePreloaded.style.cssText = 'max-width:100%;max-height:100%;object-fit:contain'; overlay.innerHTML = ''; overlay.appendChild(_surprisePreloaded); } else { overlay.innerHTML = ''; const img = overlay.querySelector('img'); if (img && !img.complete) { await new Promise(r => { img.onload = r; img.onerror = r; setTimeout(r, 500); }); } } } overlay.style.opacity = '0'; overlay.classList.remove('cache'); overlay.style.transition = 'opacity 0.2s'; overlay.style.opacity = '1'; await new Promise(r => setTimeout(r, delai)); overlay.style.opacity = '0'; await new Promise(r => setTimeout(r, 200)); overlay.classList.add('cache'); overlay.style.transition = ''; overlay.style.opacity = ''; prechargerSurprise(); } // --- Preview live --- function afficherErreurCapture(titre, detail = '') { arreterPreview(); document.getElementById('capture-en-cours')?.classList.add('cache'); const el = document.getElementById('preview-erreur-camera'); if (!el) { allerA('accueil'); return; } el.querySelector('p').textContent = titre; el.querySelector('span').textContent = detail || 'Retour dans 4 secondes...'; el.classList.remove('cache'); setTimeout(() => { el.classList.add('cache'); allerA('accueil'); }, 4000); } function afficherErreurPreview(visible) { const el = document.getElementById('preview-erreur-camera'); if (!el) return; if (visible) { el.querySelector('p').textContent = 'Un instant...'; el.querySelector('span').textContent = 'L\'appareil photo se reconnecte'; el.classList.remove('cache'); } else { el.classList.add('cache'); } } function resetPreviewTimeout() { if (previewTimeoutId) clearTimeout(previewTimeoutId); previewTimeoutId = setTimeout(() => { if (previewActif) afficherErreurPreview(true); }, PREVIEW_TIMEOUT_MS); } function lancerPreview() { if (previewActif) return; previewActif = true; afficherErreurPreview(false); document.getElementById('img-preview')?.classList.add('preview-chargement'); wsEnvoyer({ type: 'preview_start' }); resetPreviewTimeout(); } function demarrerEclairageLive() { if (!previewActif) { previewActif = true; wsEnvoyer({ type: 'preview_start' }); resetPreviewTimeout(); } } function arreterPreview() { if (!previewActif) return; previewActif = false; wsEnvoyer({ type: 'preview_stop' }); if (previewInterval) { clearInterval(previewInterval); previewInterval = null; } if (previewTimeoutId) { clearTimeout(previewTimeoutId); previewTimeoutId = null; } afficherErreurPreview(false); document.getElementById('cadre-guide-recadrage')?.classList.add('cache'); } wsOnMessage('preview', (msg) => { if (!previewActif) return; const img = document.getElementById('img-preview'); if (img) { img.onload = () => { img.classList.remove('preview-chargement'); positionnerGuideRecadrage(); }; img.src = msg.image; } const adminPrev = document.getElementById('eclairage-preview'); if (adminPrev && document.getElementById('panneau-eclairage')?.classList.contains('actif')) { adminPrev.src = msg.image; } afficherErreurPreview(false); resetPreviewTimeout(); }); // --- Cadre de visee : montre en live la zone qui sera gardee apres recadrage --- // (la pellicule/le collage recadrent chaque photo au centre selon un ratio different du plein cadre camera) function getRatioCelluleAttendu() { if (modeActuel !== 'multi') return null; const nb = getNombrePhotos(); const mode = config.multi_shot?.mode || 'strip'; if (mode === 'strip') { // Reproduit creer_strip() dans backend/collage.py (bande portrait 600x1800, photos empilees) const bandeLargeur = 600, bandeHauteur = 1800, marge = 10, sep = 12, perfLargeur = 55; const zonePhotosLargeur = bandeLargeur - perfLargeur * 2; const espacePhotos = bandeHauteur - marge * 2 - sep * (nb - 1); const hauteurCellule = Math.floor(espacePhotos / nb); return zonePhotosLargeur / hauteurCellule; } else { // Reproduit creer_collage() dans backend/collage.py const colonnes = 2; const largeurPage = 1200, hauteurPage = 1800, margeC = 25; const lignes = Math.ceil(nb / colonnes); const largeurCellule = Math.floor((largeurPage - margeC * (colonnes + 1)) / colonnes); const hauteurCellule = Math.floor((hauteurPage - margeC * (lignes + 1)) / lignes); return largeurCellule / hauteurCellule; } } function positionnerGuideRecadrage() { const guide = document.getElementById('cadre-guide-recadrage'); const img = document.getElementById('img-preview'); const container = document.getElementById('preview-live'); if (!guide || !img || !container) return; const ratioCible = getRatioCelluleAttendu(); if (!ratioCible || !img.naturalWidth || !img.naturalHeight) { guide.classList.add('cache'); return; } const cw = container.clientWidth, ch = container.clientHeight; const ratioImg = img.naturalWidth / img.naturalHeight; const ratioContainer = cw / ch; // Boite affichee de l'image dans le conteneur (object-fit: contain) let imgW, imgH, imgTop, imgLeft; if (ratioImg > ratioContainer) { imgW = cw; imgH = cw / ratioImg; imgLeft = 0; imgTop = (ch - imgH) / 2; } else { imgH = ch; imgW = ch * ratioImg; imgTop = 0; imgLeft = (cw - imgW) / 2; } // Boite du recadrage final, centree dans l'image affichee (meme logique que _crop_center cote backend) let guideW, guideH; if (ratioImg > ratioCible) { guideH = imgH; guideW = imgH * ratioCible; } else { guideW = imgW; guideH = imgW / ratioCible; } guide.style.left = `${imgLeft + (imgW - guideW) / 2}px`; guide.style.top = `${imgTop + (imgH - guideH) / 2}px`; guide.style.width = `${guideW}px`; guide.style.height = `${guideH}px`; guide.classList.remove('cache'); } window.addEventListener('resize', positionnerGuideRecadrage); // Erreur camera pendant preview idle : retour accueil // Si on est en cours de capture (compte à rebours ou attente API), on ignore : // l'API /api/capturer retournera elle-même une erreur si le DSLR est vraiment mort. wsOnMessage('camera_erreur', () => { arreterPreview(); if (!captureEnCours) { captureAbortee = true; if (ecranActuel === 'capture') { afficherErreurCapture('Un instant...', 'L\'appareil photo se prepare'); } } }); wsOnMessage('camera_ok', () => { captureAbortee = false; afficherErreurPreview(false); }); wsOnMessage('eclairage', (msg) => { if (captureEnCours) return; // Admin panel eclairage const scoreBox = document.getElementById('eclairage-score-box'); const actionEl = document.getElementById('eclairage-action'); const visagesEl = document.getElementById('eclairage-visages'); if (scoreBox) { const score = msg.score >= 0 ? msg.score : '--'; scoreBox.textContent = score; if (msg.action === 'allumer') { scoreBox.style.background = '#dc2626'; scoreBox.style.color = '#fff'; actionEl.textContent = 'ALLUMER LA LUMIERE'; actionEl.style.color = '#f87171'; } else if (msg.action === 'attention') { scoreBox.style.background = '#d97706'; scoreBox.style.color = '#fff'; actionEl.textContent = 'Eclairage faible'; actionEl.style.color = '#fbbf24'; } else if (msg.action === 'ok') { scoreBox.style.background = '#16a34a'; scoreBox.style.color = '#fff'; actionEl.textContent = 'Eclairage OK'; actionEl.style.color = '#4ade80'; } else { scoreBox.style.background = '#222'; scoreBox.style.color = '#888'; actionEl.textContent = 'Aucun visage'; actionEl.style.color = '#888'; } visagesEl.textContent = msg.visages > 0 ? msg.visages + ' visage' + (msg.visages > 1 ? 's' : '') + ' detecte' + (msg.visages > 1 ? 's' : '') : ''; } }); // --- Capture --- async function lancerCapture() { captureEnCours = true; captureAbortee = false; photosSession = []; photoImpression = null; formatImpression = null; document.getElementById('photo-resultat').src = ''; document.getElementById('photo-partage').src = ''; await chargerCadreImpose(); majCompteurAccueil(); // Verifier l'etat de la camera — attendre si reveil en cours try { let statut = await apiGet('/api/camera/statut'); if ((!statut.connectee || statut.mode === 'erreur') && statut.reveil_en_cours) { afficherStatut('L\'appareil photo se prepare, un instant...', 'succes'); for (let att = 0; att < 30; att++) { await pause(2000); statut = await apiGet('/api/camera/statut'); if (statut.connectee && statut.mode !== 'erreur') break; } afficherStatut('', ''); } if (!statut.connectee || statut.mode === 'erreur') { captureEnCours = false; afficherErreurCapture('Appareil photo indisponible', 'Reessayez dans quelques instants'); return; } } catch (e) { captureEnCours = false; afficherErreurCapture('Le photobooth demarre', 'Reessayez dans quelques instants'); return; } const nbPhotos = getNombrePhotos(); const compteurEl = document.getElementById('capture-compteur'); for (let i = 0; i < nbPhotos; i++) { if (nbPhotos > 1) { compteurEl.textContent = `Photo ${i + 1} / ${nbPhotos}`; } else { compteurEl.textContent = ''; } lancerPreview(); // Miroir live pendant le compte à rebours wsEnvoyer({type: 'prepare_capture'}); const avecPreMessages = (i === 0); await compteARebours(avecPreMessages); // Surprise : seulement en photo simple (pas strip/multi) if (modeActuel === 'simple') await afficherSurprise(); // Flash blanc immédiat + lancer capture en parallèle const flash = document.getElementById('flash-blanc'); flash.classList.remove('cache'); flash.style.animation = 'none'; flash.offsetHeight; flash.style.animation = ''; arreterPreview(); let resultat = await apiPost('/api/capturer').catch(() => null); if (!resultat && !resultat?.erreur) { flash.classList.add('cache'); document.getElementById('capture-en-cours').classList.add('cache'); captureEnCours = false; afficherErreurCapture('Oups !', 'La photo n\'a pas pu etre prise, reessayez'); return; } if (resultat?.erreur) { flash.classList.add('cache'); document.getElementById('capture-en-cours').classList.add('cache'); captureEnCours = false; const msg = resultat.erreur; afficherErreurCapture('Oups !', 'La photo n\'a pas pu etre prise, reessayez'); return; } flash.classList.add('cache'); document.getElementById('capture-en-cours').classList.add('cache'); if (resultat.nom) { photosSession.push(resultat.nom); } } captureEnCours = false; await traiterCapture(); } function getNombrePhotos() { if (modeActuel === 'simple') return 1; if (modeActuel === 'multi') return config.multi_shot?.nombre_photos || 4; return 1; } function choisirAnimationAleatoire() { // Construire la liste de toutes les animations actives (CSS + custom) const cssActives = config.camera?.animations_css_actives || ['classique']; const customActives = config.animations_custom?.actives || []; const pool = [ ...cssActives.map(id => ({ type: 'css', id })), ...customActives.map(nom => ({ type: 'custom', nom })), ]; if (pool.length === 0) return { type: 'css', id: 'classique' }; return pool[Math.floor(Math.random() * pool.length)]; } async function compteARebours(avecPreMessages = true) { const conteneur = document.getElementById('compte-a-rebours'); const chiffre = document.getElementById('chiffre-car'); const duree = config.camera?.compte_a_rebours || 3; // Choisir une animation au hasard parmi les actives const anim = choisirAnimationAleatoire(); if (anim.type === 'custom') { // Jouer la video/gif en plein ecran, shoot des qu'elle finit await jouerAnimationCustom(anim.nom); return; // pas de compte a rebours CSS } else { // Animation CSS classique conteneur.classList.remove('cache'); if (avecPreMessages) { const preMessages = ['Attention !', 'Préparez-vous !']; for (const msg of preMessages) { chiffre.textContent = msg; chiffre.className = 'anim-chiffre anim-texte'; void chiffre.offsetWidth; chiffre.classList.add('anim-' + anim.id); await pause(1000); } } for (let i = duree; i > 0; i--) { if (anim.id === 'emoji') { chiffre.textContent = EMOJI_CAR[i] || i; } else { chiffre.textContent = i; } chiffre.className = 'anim-chiffre'; void chiffre.offsetWidth; chiffre.classList.add('anim-' + anim.id); await pause(1000); } conteneur.classList.add('cache'); } } async function jouerAnimationCustom(nom) { // Creer un overlay plein ecran pour la video/gif const overlay = document.createElement('div'); overlay.className = 'video-countdown-overlay'; const ext = nom.split('.').pop().toLowerCase(); if (ext === 'gif') { const img = document.createElement('img'); // Ajouter un timestamp pour forcer le rechargement (relancer le GIF depuis le debut) img.src = `/assets/animations/${nom}?t=${Date.now()}`; overlay.appendChild(img); document.body.appendChild(overlay); // Pour un GIF, on attend sa duree naturelle // Comme on ne peut pas detecter la fin d'un GIF, on utilise un fallback de 3s // L'utilisateur peut configurer la duree dans le nom du fichier (ex: countdown_5s.gif) const match = nom.match(/(\d+)s/); const dureeGif = match ? parseInt(match[1]) * 1000 : 3000; await pause(dureeGif); } else { // Video MP4/WebM : on attend la fin exacte de la video const video = document.createElement('video'); video.src = `/assets/animations/${nom}`; video.muted = true; video.playsInline = true; overlay.appendChild(video); document.body.appendChild(overlay); await new Promise(resolve => { video.onended = resolve; video.onerror = resolve; video.play().catch(resolve); }); } overlay.remove(); } function afficherFlash() { const flash = document.getElementById('flash-blanc'); flash.classList.remove('cache'); flash.style.animation = 'none'; flash.offsetHeight; flash.style.animation = 'flash 0.3s ease-out forwards'; setTimeout(() => flash.classList.add('cache'), 400); } let photoImpression = null; // Fichier à imprimer (strip ou photo finale) let formatImpression = null; // Format CUPS à utiliser (ex: "10x15-2up") async function traiterCapture() { if (photosSession.length === 0) { afficherErreurCapture('Oups !', 'La photo n\'a pas pu etre prise, reessayez'); return; } photoImpression = null; formatImpression = null; try { if (modeActuel === 'multi') { const mode = config.multi_shot?.mode || 'strip'; let resultat; if (mode === 'strip') { resultat = await apiPost('/api/strip', { photos: photosSession }); if (resultat.erreur) { afficherErreurCapture('Oups !', 'La planche n\'a pas pu etre creee, reessayez'); return; } if (resultat.impression) photoImpression = resultat.impression; if (resultat.format_impression) formatImpression = resultat.format_impression; } else { resultat = await apiPost('/api/collage', { photos: photosSession }); if (resultat.erreur) { afficherErreurCapture('Oups !', 'Le collage n\'a pas pu etre cree, reessayez'); return; } } if (!resultat.nom) { afficherErreurCapture('Oups !', 'Un probleme est survenu, reessayez'); return; } photoFinale = resultat.nom; afficherPreviewPhoto('/data/exports/' + resultat.nom); } else { photoFinale = photosSession[0]; afficherPreviewPhoto('/data/photos/' + photosSession[0]); } } catch (e) { afficherErreurCapture('Erreur reseau', 'Impossible de traiter les photos'); return; } allerA('preview'); } function afficherPreviewPhoto(chemin) { document.getElementById('photo-resultat').src = chemin; document.getElementById('photo-partage').src = chemin; afficherCadreOverlay(cadreChoisi); if (config.fonctionnalites?.filtres) { chargerFiltres(); } if (config.fonctionnalites?.overlays) { chargerOverlays(); } if (config.fonctionnalites?.chroma_key) { chargerFonds(); } } // --- Compteur accueil --- async function majCompteurAccueil() { const etat = await apiGet('/api/compteur'); const el = document.getElementById('compteur-accueil'); el.classList.remove('cache'); document.getElementById('compteur-restant').textContent = etat.restantes; document.getElementById('compteur-limite').textContent = (etat.actif && etat.limite > 0) ? etat.limite : etat.capacite; } async function majBoutonImprimerCompteur() { const etat = await apiGet('/api/compteur'); const btn = document.getElementById('btn-imprimer'); if (!btn) return; if (etat.restantes <= 0) { btn.classList.add('cache'); } } async function chargerCadreImpose() { if (modeActuel === 'multi') { afficherCadreOverlay(null); return; } const fmt = (config.impression?.format || '15x20').replace('-2up', ''); const data = await apiGet(`/api/cadres-impression/${fmt}`).catch(() => null); if (!data) { afficherCadreOverlay(null); return; } if (data.event_mode === 'impose' && data.event_cadre) { cadreChoisi = data.event_cadre; afficherCadreOverlay(cadreChoisi); } else if (data.event_mode === 'defaut' && data.event_cadre) { cadreChoisi = data.event_cadre; afficherCadreOverlay(cadreChoisi); } else { afficherCadreOverlay(null); } } function pause(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }