Animations custom (video/GIF) + selection aleatoire
- Upload de GIF/MP4/WebM comme animations de compte a rebours - Checkboxes pour activer/desactiver chaque animation (CSS + custom) - Si plusieurs actives, tirage aleatoire a chaque photo - Preview dans le backoffice avec tirage aleatoire - Suppression des animations custom importees - Les animations custom se jouent en plein ecran avant la capture Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -888,6 +888,66 @@ h3 {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 1rem 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-anim-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Overlay video plein ecran pendant capture */
|
||||
.video-countdown-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: #000;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.video-countdown-overlay video,
|
||||
.video-countdown-overlay img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Animation item avec checkbox */
|
||||
.anim-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
padding: 0.6rem 1rem;
|
||||
background: var(--fond);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.anim-item input[type="checkbox"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
accent-color: var(--primaire);
|
||||
}
|
||||
|
||||
.anim-item-preview {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.anim-item .btn-supprimer {
|
||||
margin-left: auto;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--danger);
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.anim-chiffre {
|
||||
|
||||
@@ -257,16 +257,27 @@
|
||||
|
||||
<!-- Panneau Animations -->
|
||||
<div class="admin-panneau" id="panneau-animations-admin">
|
||||
<h3>Animation du compte a rebours</h3>
|
||||
<p class="aide">Choisissez le style d'animation pour le 3... 2... 1...</p>
|
||||
<h3>Animations CSS</h3>
|
||||
<p class="aide">Cochez les animations a utiliser. Si plusieurs sont cochees, une sera choisie au hasard a chaque photo.</p>
|
||||
<div id="liste-animations" class="liste-animations">
|
||||
</div>
|
||||
|
||||
<h3>Animations personnalisees (video/GIF)</h3>
|
||||
<p class="aide">Importez vos propres animations (GIF, MP4, WebM). Elles seront jouees en plein ecran avant la capture.</p>
|
||||
<div id="liste-animations-custom" class="liste-animations">
|
||||
</div>
|
||||
<div class="champ">
|
||||
<input type="file" id="input-animation" accept=".gif,.mp4,.webm" class="input-fichier">
|
||||
<button class="btn-action" onclick="uploaderAnimation()">Importer</button>
|
||||
</div>
|
||||
|
||||
<div class="animation-preview">
|
||||
<h4>Apercu</h4>
|
||||
<div id="preview-animation" class="preview-animation-box">
|
||||
<span id="preview-anim-chiffre" class="anim-chiffre">3</span>
|
||||
<video id="preview-anim-video" class="preview-anim-video cache" muted playsinline></video>
|
||||
</div>
|
||||
<button class="btn-secondaire" onclick="testerAnimation()">Tester</button>
|
||||
<button class="btn-secondaire" onclick="testerAnimationPreview()">Tester</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -264,60 +264,181 @@ async function uploaderCadre() {
|
||||
|
||||
// === ANIMATIONS ===
|
||||
|
||||
let animationChoisie = 'classique';
|
||||
const EMOJI_MAP = { 3: '🤪', 2: '😱', 1: '🔥' };
|
||||
|
||||
async function chargerAnimations() {
|
||||
// Animations CSS internes
|
||||
const anims = await apiGet('/api/animations');
|
||||
animationChoisie = config.camera?.animation_compte_a_rebours || 'classique';
|
||||
|
||||
const cssActives = config.camera?.animations_css_actives || ['classique'];
|
||||
const liste = document.getElementById('liste-animations');
|
||||
liste.innerHTML = '';
|
||||
|
||||
for (const [id, nom] of Object.entries(anims)) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'anim-option' + (id === animationChoisie ? ' actif' : '');
|
||||
div.textContent = nom;
|
||||
div.onclick = () => choisirAnimation(id, div);
|
||||
div.className = 'anim-item';
|
||||
|
||||
const cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
cb.checked = cssActives.includes(id);
|
||||
cb.dataset.animId = id;
|
||||
cb.onchange = () => majAnimationsCss();
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.textContent = nom;
|
||||
|
||||
div.appendChild(cb);
|
||||
div.appendChild(label);
|
||||
liste.appendChild(div);
|
||||
}
|
||||
|
||||
// Animations custom (video/GIF)
|
||||
await chargerAnimationsCustom();
|
||||
}
|
||||
|
||||
async function chargerAnimationsCustom() {
|
||||
const data = await apiGet('/api/animations/custom');
|
||||
const liste = document.getElementById('liste-animations-custom');
|
||||
liste.innerHTML = '';
|
||||
|
||||
if (data.tous.length === 0) {
|
||||
liste.innerHTML = '<p class="texte-secondaire">Aucune animation importee</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const nom of data.tous) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'anim-item';
|
||||
|
||||
const cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
cb.checked = data.actives.includes(nom);
|
||||
cb.dataset.animNom = nom;
|
||||
cb.onchange = () => majAnimationsCustom();
|
||||
|
||||
// Preview miniature
|
||||
const ext = nom.split('.').pop().toLowerCase();
|
||||
let preview;
|
||||
if (ext === 'gif') {
|
||||
preview = document.createElement('img');
|
||||
preview.src = `/assets/animations/${nom}`;
|
||||
preview.className = 'anim-item-preview';
|
||||
} else {
|
||||
preview = document.createElement('video');
|
||||
preview.src = `/assets/animations/${nom}`;
|
||||
preview.className = 'anim-item-preview';
|
||||
preview.muted = true;
|
||||
preview.preload = 'metadata';
|
||||
}
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.textContent = nom;
|
||||
|
||||
const btnSuppr = document.createElement('button');
|
||||
btnSuppr.className = 'btn-supprimer';
|
||||
btnSuppr.innerHTML = '✕';
|
||||
btnSuppr.onclick = () => supprimerAnimation(nom);
|
||||
|
||||
div.appendChild(cb);
|
||||
div.appendChild(preview);
|
||||
div.appendChild(label);
|
||||
div.appendChild(btnSuppr);
|
||||
liste.appendChild(div);
|
||||
}
|
||||
}
|
||||
|
||||
async function choisirAnimation(id, el) {
|
||||
animationChoisie = id;
|
||||
document.querySelectorAll('.anim-option').forEach(e => e.classList.remove('actif'));
|
||||
el.classList.add('actif');
|
||||
|
||||
await apiPost('/api/config', {
|
||||
camera: { animation_compte_a_rebours: id },
|
||||
});
|
||||
|
||||
testerAnimation();
|
||||
async function majAnimationsCss() {
|
||||
const checkboxes = document.querySelectorAll('#liste-animations .anim-item input[type="checkbox"]');
|
||||
const actives = [];
|
||||
checkboxes.forEach(cb => { if (cb.checked) actives.push(cb.dataset.animId); });
|
||||
// Au moins une doit rester active
|
||||
if (actives.length === 0) actives.push('classique');
|
||||
await apiPost('/api/config', { camera: { animations_css_actives: actives } });
|
||||
}
|
||||
|
||||
const EMOJI_MAP = { 3: '🤪', 2: '😱', 1: '🔥' };
|
||||
async function majAnimationsCustom() {
|
||||
const checkboxes = document.querySelectorAll('#liste-animations-custom .anim-item input[type="checkbox"]');
|
||||
const actives = [];
|
||||
checkboxes.forEach(cb => { if (cb.checked) actives.push(cb.dataset.animNom); });
|
||||
await apiPost('/api/animations/custom', { actives });
|
||||
}
|
||||
|
||||
function testerAnimation() {
|
||||
async function uploaderAnimation() {
|
||||
const input = document.getElementById('input-animation');
|
||||
if (!input.files.length) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('fichier', input.files[0]);
|
||||
|
||||
await fetch('/api/upload/animation', { method: 'POST', body: formData });
|
||||
input.value = '';
|
||||
chargerAnimationsCustom();
|
||||
afficherStatut('Animation importee', 'succes');
|
||||
}
|
||||
|
||||
async function supprimerAnimation(nom) {
|
||||
if (!confirm(`Supprimer l'animation "${nom}" ?`)) return;
|
||||
await fetch(`/api/animations/custom/${encodeURIComponent(nom)}`, { method: 'DELETE' });
|
||||
chargerAnimationsCustom();
|
||||
}
|
||||
|
||||
function testerAnimationPreview() {
|
||||
const chiffre = document.getElementById('preview-anim-chiffre');
|
||||
let count = 3;
|
||||
const video = document.getElementById('preview-anim-video');
|
||||
|
||||
function afficher() {
|
||||
if (animationChoisie === 'emoji') {
|
||||
chiffre.textContent = EMOJI_MAP[count] || count;
|
||||
} else {
|
||||
chiffre.textContent = count;
|
||||
// Verifier s'il y a des animations custom actives
|
||||
const customActives = [];
|
||||
document.querySelectorAll('#liste-animations-custom .anim-item input[type="checkbox"]:checked').forEach(cb => {
|
||||
customActives.push(cb.dataset.animNom);
|
||||
});
|
||||
|
||||
// Verifier les CSS actives
|
||||
const cssActives = [];
|
||||
document.querySelectorAll('#liste-animations .anim-item input[type="checkbox"]:checked').forEach(cb => {
|
||||
cssActives.push(cb.dataset.animId);
|
||||
});
|
||||
|
||||
const toutes = [...cssActives.map(a => ({type: 'css', id: a})), ...customActives.map(a => ({type: 'custom', nom: a}))];
|
||||
if (toutes.length === 0) return;
|
||||
|
||||
const choix = toutes[Math.floor(Math.random() * toutes.length)];
|
||||
|
||||
if (choix.type === 'custom') {
|
||||
// Jouer la video/gif
|
||||
chiffre.classList.add('cache');
|
||||
video.classList.remove('cache');
|
||||
video.src = `/assets/animations/${choix.nom}`;
|
||||
video.currentTime = 0;
|
||||
video.play();
|
||||
video.onended = () => {
|
||||
video.classList.add('cache');
|
||||
chiffre.classList.remove('cache');
|
||||
};
|
||||
// Pour les GIF, masquer apres 3s
|
||||
if (choix.nom.endsWith('.gif')) {
|
||||
setTimeout(() => {
|
||||
video.classList.add('cache');
|
||||
chiffre.classList.remove('cache');
|
||||
}, 3000);
|
||||
}
|
||||
// Reset : retirer toute classe d'animation
|
||||
chiffre.className = 'anim-chiffre';
|
||||
// Force reflow pour que le navigateur enregistre le retrait
|
||||
void chiffre.offsetWidth;
|
||||
// Appliquer la nouvelle animation
|
||||
chiffre.classList.add('anim-' + animationChoisie);
|
||||
|
||||
count--;
|
||||
if (count > 0) setTimeout(afficher, 1000);
|
||||
} else {
|
||||
// Animation CSS
|
||||
video.classList.add('cache');
|
||||
chiffre.classList.remove('cache');
|
||||
let count = 3;
|
||||
function afficher() {
|
||||
if (choix.id === 'emoji') {
|
||||
chiffre.textContent = EMOJI_MAP[count] || count;
|
||||
} else {
|
||||
chiffre.textContent = count;
|
||||
}
|
||||
chiffre.className = 'anim-chiffre';
|
||||
void chiffre.offsetWidth;
|
||||
chiffre.classList.add('anim-' + choix.id);
|
||||
count--;
|
||||
if (count > 0) setTimeout(afficher, 1000);
|
||||
}
|
||||
afficher();
|
||||
}
|
||||
|
||||
afficher();
|
||||
}
|
||||
|
||||
// === FONCTIONNALITES ===
|
||||
|
||||
@@ -76,32 +76,83 @@ function getNombrePhotos() {
|
||||
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;
|
||||
const anim = config.camera?.animation_compte_a_rebours || 'classique';
|
||||
|
||||
conteneur.classList.remove('cache');
|
||||
// Choisir une animation au hasard parmi les actives
|
||||
const anim = choisirAnimationAleatoire();
|
||||
|
||||
for (let i = duree; i > 0; i--) {
|
||||
// Contenu selon le type d'animation
|
||||
if (anim === 'emoji') {
|
||||
chiffre.textContent = EMOJI_CAR[i] || i;
|
||||
} else {
|
||||
chiffre.textContent = i;
|
||||
if (anim.type === 'custom') {
|
||||
// Jouer une video/gif en plein ecran
|
||||
await jouerAnimationCustom(anim.nom, duree);
|
||||
} 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);
|
||||
}
|
||||
|
||||
// Appliquer l'animation
|
||||
chiffre.className = 'anim-chiffre anim-' + anim;
|
||||
chiffre.style.animation = 'none';
|
||||
chiffre.offsetHeight;
|
||||
chiffre.className = 'anim-chiffre anim-' + anim;
|
||||
conteneur.classList.add('cache');
|
||||
}
|
||||
}
|
||||
|
||||
await pause(1000);
|
||||
async function jouerAnimationCustom(nom, duree) {
|
||||
// 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');
|
||||
img.src = `/assets/animations/${nom}`;
|
||||
overlay.appendChild(img);
|
||||
document.body.appendChild(overlay);
|
||||
await pause(duree * 1000);
|
||||
} else {
|
||||
const video = document.createElement('video');
|
||||
video.src = `/assets/animations/${nom}`;
|
||||
video.muted = true;
|
||||
video.playsInline = true;
|
||||
video.autoplay = true;
|
||||
overlay.appendChild(video);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Attendre la fin de la video ou le timeout
|
||||
await Promise.race([
|
||||
new Promise(resolve => { video.onended = resolve; }),
|
||||
pause(duree * 1000 + 2000),
|
||||
]);
|
||||
}
|
||||
|
||||
conteneur.classList.add('cache');
|
||||
overlay.remove();
|
||||
}
|
||||
|
||||
function afficherFlash() {
|
||||
|
||||
Reference in New Issue
Block a user