From 32392d3ccd88b814993d51fd5221d191ab76e0a1 Mon Sep 17 00:00:00 2001 From: Jules Date: Sat, 21 Mar 2026 23:45:45 +0100 Subject: [PATCH] Mode fond vert : choix du fond apres la photo - Apres la capture, si chroma key active, une barre de fonds s'affiche avec des miniatures cliquables - L'utilisateur choisit son fond parmi la liste - Remplacement du fond vert en temps reel (OpenCV backend) - Onglet "Fonds verts" dans le backoffice pour importer/supprimer des images de fond - Miniatures visuelles dans l'admin et sur l'ecran de preview Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/main.py | 9 +++++ frontend/css/style.css | 75 ++++++++++++++++++++++++++++++++++++++++++ frontend/index.html | 17 ++++++++++ frontend/js/admin.js | 50 ++++++++++++++++++++++++++++ frontend/js/camera.js | 3 ++ frontend/js/effects.js | 71 ++++++++++++++++++++++++++++++++++++--- 6 files changed, 221 insertions(+), 4 deletions(-) diff --git a/backend/main.py b/backend/main.py index 35176ba..1a0a1dd 100644 --- a/backend/main.py +++ b/backend/main.py @@ -172,6 +172,15 @@ async def api_fonds(): return lister_fonds() +@app.delete("/api/fonds/{nom}") +async def api_supprimer_fond(nom: str): + chemin = DOSSIER_FONDS / nom + if chemin.exists() and chemin.parent == DOSSIER_FONDS: + chemin.unlink() + return {"succes": True} + return JSONResponse({"erreur": "Fichier introuvable"}, status_code=404) + + # --- API Collage --- @app.post("/api/strip") diff --git a/frontend/css/style.css b/frontend/css/style.css index 267fae2..49c121c 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -294,6 +294,43 @@ html, body { box-shadow: var(--ombre); } +.barre-fonds { + display: flex; + gap: 0.8rem; + padding: 0.8rem; + overflow-x: auto; + width: 100%; + justify-content: center; +} + +.btn-fond { + width: 80px; + height: 60px; + border-radius: 8px; + border: 3px solid transparent; + cursor: pointer; + transition: var(--transition); + overflow: hidden; + flex-shrink: 0; + padding: 0; + background: var(--fond-carte); +} + +.btn-fond img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.btn-fond.actif { + border-color: var(--primaire); + box-shadow: 0 0 12px rgba(233, 30, 99, 0.5); +} + +.btn-fond:active { + transform: scale(0.93); +} + .barre-filtres, .barre-overlays { display: flex; gap: 0.8rem; @@ -828,6 +865,44 @@ h3 { border-radius: 50%; } +/* Liste fonds admin */ +.liste-fonds { + display: flex; + flex-wrap: wrap; + gap: 0.8rem; +} + +.fond-item { + position: relative; + width: 120px; + height: 80px; + border-radius: 8px; + overflow: hidden; + border: 2px solid #333; +} + +.fond-item img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.fond-item .btn-supprimer { + position: absolute; + top: 2px; right: 2px; + background: rgba(0,0,0,0.6); + border: none; + color: var(--danger); + font-size: 1rem; + cursor: pointer; + border-radius: 50%; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; +} + /* Liste cadres */ .liste-cadres { display: flex; diff --git a/frontend/index.html b/frontend/index.html index 6c57f4e..9e2581c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -67,6 +67,8 @@
+
+
@@ -138,6 +140,7 @@ + @@ -258,6 +261,20 @@
+ +
+

Fonds pour chroma key

+

Importez des images de fond. Les utilisateurs choisiront leur fond apres la photo.

+
+

Aucun fond importe

+
+

Importer un fond

+
+ + +
+
+

Animations CSS

diff --git a/frontend/js/admin.js b/frontend/js/admin.js index 458e1f6..a4b0f73 100644 --- a/frontend/js/admin.js +++ b/frontend/js/admin.js @@ -22,6 +22,7 @@ async function chargerAdmin() { chargerAnimations(); chargerFonctionnalites(); chargerEmailAdmin(); + chargerFondsAdmin(); chargerGalerieAdmin(); } @@ -262,6 +263,55 @@ async function uploaderCadre() { afficherStatut('Cadre importe', 'succes'); } +// === FONDS VERTS === + +async function chargerFondsAdmin() { + const fonds = await apiGet('/api/fonds'); + const liste = document.getElementById('liste-fonds'); + liste.innerHTML = ''; + + if (fonds.length === 0) { + liste.innerHTML = '

Aucun fond importe

'; + return; + } + + for (const nom of fonds) { + const div = document.createElement('div'); + div.className = 'fond-item'; + + const img = document.createElement('img'); + img.src = `/assets/backgrounds/${nom}`; + img.alt = nom; + + const btnSuppr = document.createElement('button'); + btnSuppr.className = 'btn-supprimer'; + btnSuppr.innerHTML = '✕'; + btnSuppr.onclick = () => supprimerFond(nom); + + div.appendChild(img); + div.appendChild(btnSuppr); + liste.appendChild(div); + } +} + +async function uploaderFond() { + const input = document.getElementById('input-fond'); + if (!input.files.length) return; + + const formData = new FormData(); + formData.append('fichier', input.files[0]); + await fetch('/api/upload/fond', { method: 'POST', body: formData }); + input.value = ''; + chargerFondsAdmin(); + afficherStatut('Fond importe', 'succes'); +} + +async function supprimerFond(nom) { + if (!confirm(`Supprimer le fond "${nom}" ?`)) return; + await fetch(`/api/fonds/${encodeURIComponent(nom)}`, { method: 'DELETE' }); + chargerFondsAdmin(); +} + // === ANIMATIONS === const EMOJI_MAP = { 3: '🤪', 2: '😱', 1: '🔥' }; diff --git a/frontend/js/camera.js b/frontend/js/camera.js index ee703eb..ee713dd 100644 --- a/frontend/js/camera.js +++ b/frontend/js/camera.js @@ -208,6 +208,9 @@ function afficherPreviewPhoto(chemin) { if (config.fonctionnalites?.overlays) { chargerOverlays(); } + if (config.fonctionnalites?.chroma_key) { + chargerFonds(); + } } // --- Compteur accueil --- diff --git a/frontend/js/effects.js b/frontend/js/effects.js index b7335a1..07fbb2b 100644 --- a/frontend/js/effects.js +++ b/frontend/js/effects.js @@ -1,7 +1,8 @@ -/* Module effets - Filtres et overlays */ +/* Module effets - Filtres, overlays et fond vert */ let filtreActuel = 'original'; let overlayActuel = null; +let fondActuel = null; async function chargerFiltres() { const filtres = await apiGet('/api/filtres'); @@ -18,7 +19,6 @@ async function chargerFiltres() { } async function appliquerFiltreUI(filtre, btn) { - // Activer visuellement document.querySelectorAll('.btn-filtre').forEach(b => b.classList.remove('actif')); btn.classList.add('actif'); filtreActuel = filtre; @@ -55,7 +55,6 @@ async function chargerOverlays() { barre.classList.remove('cache'); barre.innerHTML = ''; - // Bouton "sans overlay" const btnAucun = document.createElement('button'); btnAucun.className = 'btn-overlay actif'; btnAucun.textContent = 'Sans cadre'; @@ -91,7 +90,71 @@ function retirerOverlay(btn) { btn.classList.add('actif'); overlayActuel = null; - // Revenir a la photo avec filtre actuel (ou originale) + if (filtreActuel !== 'original') { + appliquerFiltreUI(filtreActuel, document.querySelector('.btn-filtre.actif')); + } else { + const chemin = '/data/photos/' + photosSession[0]; + document.getElementById('photo-resultat').src = chemin; + document.getElementById('photo-partage').src = chemin; + photoFinale = photosSession[0]; + } +} + +// === Fond vert (chroma key) === + +async function chargerFonds() { + const fonds = await apiGet('/api/fonds'); + const barre = document.getElementById('barre-fonds'); + + if (!config.fonctionnalites?.chroma_key || fonds.length === 0) { + barre.classList.add('cache'); + return; + } + + barre.classList.remove('cache'); + barre.innerHTML = ''; + fondActuel = null; + + // Bouton "original" (pas de chroma key) + const btnOriginal = document.createElement('button'); + btnOriginal.className = 'btn-fond actif'; + btnOriginal.innerHTML = 'Original'; + btnOriginal.addEventListener('click', () => retirerFond(btnOriginal)); + barre.appendChild(btnOriginal); + + for (const nom of fonds) { + const btn = document.createElement('button'); + btn.className = 'btn-fond'; + const img = document.createElement('img'); + img.src = `/assets/backgrounds/${nom}`; + img.alt = nom; + btn.appendChild(img); + btn.addEventListener('click', () => appliquerFondUI(nom, btn)); + barre.appendChild(btn); + } +} + +async function appliquerFondUI(nom, btn) { + document.querySelectorAll('.btn-fond').forEach(b => b.classList.remove('actif')); + btn.classList.add('actif'); + fondActuel = nom; + + const photo = photosSession[0]; + const resultat = await apiPost('/api/chroma', { photo, fond: nom }); + + if (resultat.nom) { + photoFinale = resultat.nom; + document.getElementById('photo-resultat').src = '/data/exports/' + resultat.nom; + document.getElementById('photo-partage').src = '/data/exports/' + resultat.nom; + } +} + +function retirerFond(btn) { + document.querySelectorAll('.btn-fond').forEach(b => b.classList.remove('actif')); + btn.classList.add('actif'); + fondActuel = null; + + // Revenir a la photo originale ou avec filtre if (filtreActuel !== 'original') { appliquerFiltreUI(filtreActuel, document.querySelector('.btn-filtre.actif')); } else {