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:
@@ -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