La PageSize CUPS w288h432-div2 declare la meme taille de page que w288h432 (confirme par le PPD Mitsubishi) : le K60 coupe la feuille pleine apres impression, il ne duplique pas une image fournie a taille reduite. On envoyait la bande seule (600x1800) sous cette PageSize, donc CUPS l'etirait pour remplir la feuille 1200x1800 puis le K60 la coupait en 2 — d'ou la photo etiree et coupee au lieu de 2 bandes identiques. Ajoute aussi le miroir horizontal sur le preview live (selfie) et un flou de chargement pendant le bref delai de remontee du miroir DSLR apres capture, pour eviter l'impression d'image figee sur la photo precedente.
436 lines
14 KiB
JavaScript
436 lines
14 KiB
JavaScript
/* 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: '🔥' };
|
|
|
|
// --- 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 = 'Appareil photo deconnecte';
|
|
el.querySelector('span').textContent = 'Reconnexion en cours...';
|
|
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);
|
|
// Flou le temps que la 1ere frame fraiche arrive (le miroir DSLR doit remonter
|
|
// apres la capture precedente) : evite l'impression d'image figee sur l'ancienne photo.
|
|
document.getElementById('img-preview')?.classList.add('preview-chargement');
|
|
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;
|
|
}
|
|
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 = 34;
|
|
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('Appareil photo déconnecté', 'Reconnexion en cours...');
|
|
}
|
|
}
|
|
});
|
|
|
|
wsOnMessage('camera_ok', () => {
|
|
captureAbortee = false;
|
|
afficherErreurPreview(false);
|
|
});
|
|
|
|
// --- Capture ---
|
|
|
|
async function lancerCapture() {
|
|
captureEnCours = true;
|
|
captureAbortee = false;
|
|
photosSession = [];
|
|
photoImpression = null;
|
|
formatImpression = null;
|
|
|
|
document.getElementById('photo-resultat').src = '';
|
|
document.getElementById('photo-partage').src = '';
|
|
|
|
majCompteurAccueil();
|
|
|
|
// Verifier l'etat de la camera avant de commencer
|
|
try {
|
|
const statut = await apiGet('/api/camera/statut');
|
|
if (!statut.connectee || statut.mode === 'erreur') {
|
|
captureEnCours = false;
|
|
afficherErreurCapture('Appareil photo non disponible', 'Verifiez la connexion USB du DSLR');
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
captureEnCours = false;
|
|
afficherErreurCapture('Impossible de joindre le serveur', 'Verifiez que le photobooth est bien demarre');
|
|
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}`;
|
|
}
|
|
|
|
lancerPreview(); // Miroir live pendant le compte à rebours
|
|
await compteARebours();
|
|
arreterPreview();
|
|
await pause(150);
|
|
|
|
const promesseCapture = apiPost('/api/capturer').catch(() => null);
|
|
let resultat = await promesseCapture;
|
|
document.getElementById('capture-en-cours').classList.add('cache');
|
|
let erreurReseau = false;
|
|
|
|
if (!resultat) {
|
|
erreurReseau = true;
|
|
} else if (resultat.erreur === 'Limite de photos atteinte') {
|
|
captureEnCours = false;
|
|
afficherErreurCapture('Limite de photos atteinte', 'La session est terminee');
|
|
return;
|
|
}
|
|
|
|
if (erreurReseau) {
|
|
captureEnCours = false;
|
|
afficherErreurCapture('Erreur reseau', 'Le serveur ne repond pas — redemarrez le photobooth');
|
|
return;
|
|
}
|
|
|
|
if (!resultat || resultat.erreur) {
|
|
captureEnCours = false;
|
|
const msg = resultat?.erreur || 'Erreur inconnue';
|
|
afficherErreurCapture('Echec de la capture', msg.includes('Echec') ? 'Verifiez le DSLR et reessayez' : msg);
|
|
return;
|
|
}
|
|
|
|
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() {
|
|
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');
|
|
|
|
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('Aucune photo capturee', 'Une erreur inattendue s\'est produite');
|
|
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('Erreur creation de la planche', resultat.erreur);
|
|
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('Erreur creation du collage', resultat.erreur);
|
|
return;
|
|
}
|
|
}
|
|
if (!resultat.nom) {
|
|
afficherErreurCapture('Erreur traitement des photos', 'Le fichier final n\'a pas ete genere');
|
|
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');
|
|
if (etat.actif) {
|
|
el.classList.remove('cache');
|
|
document.getElementById('compteur-restant').textContent = etat.restantes;
|
|
document.getElementById('compteur-limite').textContent = etat.limite;
|
|
} else {
|
|
el.classList.add('cache');
|
|
}
|
|
}
|
|
|
|
function pause(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|