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) <noreply@anthropic.com>
This commit is contained in:
@@ -172,6 +172,15 @@ async def api_fonds():
|
|||||||
return lister_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 ---
|
# --- API Collage ---
|
||||||
|
|
||||||
@app.post("/api/strip")
|
@app.post("/api/strip")
|
||||||
|
|||||||
@@ -294,6 +294,43 @@ html, body {
|
|||||||
box-shadow: var(--ombre);
|
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 {
|
.barre-filtres, .barre-overlays {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
@@ -828,6 +865,44 @@ h3 {
|
|||||||
border-radius: 50%;
|
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 */
|
||||||
.liste-cadres {
|
.liste-cadres {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -67,6 +67,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="barre-overlays" class="barre-overlays cache">
|
<div id="barre-overlays" class="barre-overlays cache">
|
||||||
</div>
|
</div>
|
||||||
|
<div id="barre-fonds" class="barre-fonds cache">
|
||||||
|
</div>
|
||||||
<div class="preview-actions">
|
<div class="preview-actions">
|
||||||
<button class="btn-action" onclick="allerA('partage')">Valider</button>
|
<button class="btn-action" onclick="allerA('partage')">Valider</button>
|
||||||
<button class="btn-secondaire" onclick="recommencer()">Reprendre</button>
|
<button class="btn-secondaire" onclick="recommencer()">Reprendre</button>
|
||||||
@@ -138,6 +140,7 @@
|
|||||||
<button class="onglet" data-onglet="compteur-admin">Compteur</button>
|
<button class="onglet" data-onglet="compteur-admin">Compteur</button>
|
||||||
<button class="onglet" data-onglet="destinations-admin">Destinations</button>
|
<button class="onglet" data-onglet="destinations-admin">Destinations</button>
|
||||||
<button class="onglet" data-onglet="cadres-admin">Cadres</button>
|
<button class="onglet" data-onglet="cadres-admin">Cadres</button>
|
||||||
|
<button class="onglet" data-onglet="fonds-admin">Fonds verts</button>
|
||||||
<button class="onglet" data-onglet="animations-admin">Animation 3-2-1</button>
|
<button class="onglet" data-onglet="animations-admin">Animation 3-2-1</button>
|
||||||
<button class="onglet" data-onglet="evenement">Evenement</button>
|
<button class="onglet" data-onglet="evenement">Evenement</button>
|
||||||
<button class="onglet" data-onglet="fonctionnalites">Fonctions</button>
|
<button class="onglet" data-onglet="fonctionnalites">Fonctions</button>
|
||||||
@@ -258,6 +261,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Panneau Fonds verts -->
|
||||||
|
<div class="admin-panneau" id="panneau-fonds-admin">
|
||||||
|
<h3>Fonds pour chroma key</h3>
|
||||||
|
<p class="aide">Importez des images de fond. Les utilisateurs choisiront leur fond apres la photo.</p>
|
||||||
|
<div id="liste-fonds" class="liste-fonds">
|
||||||
|
<p class="texte-secondaire">Aucun fond importe</p>
|
||||||
|
</div>
|
||||||
|
<h3>Importer un fond</h3>
|
||||||
|
<div class="champ">
|
||||||
|
<input type="file" id="input-fond" accept=".jpg,.jpeg,.png" class="input-fichier">
|
||||||
|
<button class="btn-action" onclick="uploaderFond()">Importer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Panneau Animations -->
|
<!-- Panneau Animations -->
|
||||||
<div class="admin-panneau" id="panneau-animations-admin">
|
<div class="admin-panneau" id="panneau-animations-admin">
|
||||||
<h3>Animations CSS</h3>
|
<h3>Animations CSS</h3>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ async function chargerAdmin() {
|
|||||||
chargerAnimations();
|
chargerAnimations();
|
||||||
chargerFonctionnalites();
|
chargerFonctionnalites();
|
||||||
chargerEmailAdmin();
|
chargerEmailAdmin();
|
||||||
|
chargerFondsAdmin();
|
||||||
chargerGalerieAdmin();
|
chargerGalerieAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,6 +263,55 @@ async function uploaderCadre() {
|
|||||||
afficherStatut('Cadre importe', 'succes');
|
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 = '<p class="texte-secondaire">Aucun fond importe</p>';
|
||||||
|
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 ===
|
// === ANIMATIONS ===
|
||||||
|
|
||||||
const EMOJI_MAP = { 3: '🤪', 2: '😱', 1: '🔥' };
|
const EMOJI_MAP = { 3: '🤪', 2: '😱', 1: '🔥' };
|
||||||
|
|||||||
@@ -208,6 +208,9 @@ function afficherPreviewPhoto(chemin) {
|
|||||||
if (config.fonctionnalites?.overlays) {
|
if (config.fonctionnalites?.overlays) {
|
||||||
chargerOverlays();
|
chargerOverlays();
|
||||||
}
|
}
|
||||||
|
if (config.fonctionnalites?.chroma_key) {
|
||||||
|
chargerFonds();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Compteur accueil ---
|
// --- Compteur accueil ---
|
||||||
|
|||||||
71
frontend/js/effects.js
vendored
71
frontend/js/effects.js
vendored
@@ -1,7 +1,8 @@
|
|||||||
/* Module effets - Filtres et overlays */
|
/* Module effets - Filtres, overlays et fond vert */
|
||||||
|
|
||||||
let filtreActuel = 'original';
|
let filtreActuel = 'original';
|
||||||
let overlayActuel = null;
|
let overlayActuel = null;
|
||||||
|
let fondActuel = null;
|
||||||
|
|
||||||
async function chargerFiltres() {
|
async function chargerFiltres() {
|
||||||
const filtres = await apiGet('/api/filtres');
|
const filtres = await apiGet('/api/filtres');
|
||||||
@@ -18,7 +19,6 @@ async function chargerFiltres() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function appliquerFiltreUI(filtre, btn) {
|
async function appliquerFiltreUI(filtre, btn) {
|
||||||
// Activer visuellement
|
|
||||||
document.querySelectorAll('.btn-filtre').forEach(b => b.classList.remove('actif'));
|
document.querySelectorAll('.btn-filtre').forEach(b => b.classList.remove('actif'));
|
||||||
btn.classList.add('actif');
|
btn.classList.add('actif');
|
||||||
filtreActuel = filtre;
|
filtreActuel = filtre;
|
||||||
@@ -55,7 +55,6 @@ async function chargerOverlays() {
|
|||||||
barre.classList.remove('cache');
|
barre.classList.remove('cache');
|
||||||
barre.innerHTML = '';
|
barre.innerHTML = '';
|
||||||
|
|
||||||
// Bouton "sans overlay"
|
|
||||||
const btnAucun = document.createElement('button');
|
const btnAucun = document.createElement('button');
|
||||||
btnAucun.className = 'btn-overlay actif';
|
btnAucun.className = 'btn-overlay actif';
|
||||||
btnAucun.textContent = 'Sans cadre';
|
btnAucun.textContent = 'Sans cadre';
|
||||||
@@ -91,7 +90,71 @@ function retirerOverlay(btn) {
|
|||||||
btn.classList.add('actif');
|
btn.classList.add('actif');
|
||||||
overlayActuel = null;
|
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 = '<span style="font-size:0.7rem;color:var(--texte-secondaire)">Original</span>';
|
||||||
|
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') {
|
if (filtreActuel !== 'original') {
|
||||||
appliquerFiltreUI(filtreActuel, document.querySelector('.btn-filtre.actif'));
|
appliquerFiltreUI(filtreActuel, document.querySelector('.btn-filtre.actif'));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user