anycoder-74e4c2e1 / index.html
Mousco's picture
Upload folder using huggingface_hub
29667c1 verified
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>SnapCam - Effets Photo</title>
<!-- Importation de la bibliothèque d'icônes Phosphor Icons -->
<script src="https://unpkg.com/@phosphor-icons/web"></script>
<style>
/* --- RESET & BASE --- */
:root {
--primary-color: #FFFC00; /* Jaune Snapchat */
--accent-color: #ffffff;
--bg-dark: #000000;
--overlay-bg: rgba(0, 0, 0, 0.6);
--glass-bg: rgba(20, 20, 20, 0.8);
--font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
--transition-speed: 0.3s;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: var(--font-family);
background-color: var(--bg-dark);
color: var(--accent-color);
height: 100dvh; /* Dynamic Viewport Height */
width: 100vw;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
}
/* --- HEADER --- */
header {
position: absolute;
top: 0;
left: 0;
width: 100%;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 100;
background: linear-gradient(to bottom, rgba(0,0,0,0.8), transparent);
pointer-events: none; /* Laisser passer les clics vers la vidéo si besoin */
}
.brand {
font-weight: 800;
font-size: 1.2rem;
letter-spacing: 1px;
pointer-events: auto;
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
}
.anycoder-link {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
pointer-events: auto;
transition: color 0.2s;
}
.anycoder-link:hover {
color: var(--primary-color);
}
/* --- MAIN CAMERA VIEW --- */
main {
flex: 1;
position: relative;
display: flex;
justify-content: center;
align-items: center;
background: #111;
}
video {
width: 100%;
height: 100%;
object-fit: cover;
transform: scaleX(-1); /* Miroir par défaut (caméra frontale) */
transition: filter 0.2s ease;
}
/* Canvas caché pour la capture */
canvas {
display: none;
}
/* Effet Flash lors de la prise de photo */
#flash-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
opacity: 0;
pointer-events: none;
z-index: 50;
transition: opacity 0.1s ease-out;
}
/* --- CONTROLS (BOTTOM) --- */
.controls-container {
position: absolute;
bottom: 0;
width: 100%;
padding: 20px;
background: linear-gradient(to top, rgba(0,0,0,0.9), transparent);
display: flex;
flex-direction: column;
gap: 20px;
z-index: 100;
padding-bottom: max(20px, env(safe-area-inset-bottom));
}
/* --- FILTER SCROLLER --- */
.filter-scroller {
display: flex;
gap: 15px;
overflow-x: auto;
padding-bottom: 10px;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 10+ */
}
.filter-scroller::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
.filter-item {
flex: 0 0 auto;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
opacity: 0.7;
transition: transform 0.2s, opacity 0.2s;
}
.filter-item.active {
opacity: 1;
transform: scale(1.1);
}
.filter-preview {
width: 60px;
height: 60px;
border-radius: 50%;
border: 2px solid transparent;
background-color: #333;
background-size: cover;
background-position: center;
margin-bottom: 5px;
overflow: hidden;
position: relative;
}
/* Un indicateur visuel pour le filtre actif */
.filter-item.active .filter-preview {
border-color: var(--primary-color);
}
.filter-name {
font-size: 0.75rem;
text-shadow: 0 1px 2px black;
}
/* --- ACTION BAR (Shutter, Switch, Gallery) --- */
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
}
.btn-icon {
background: none;
border: none;
color: white;
font-size: 2rem;
cursor: pointer;
padding: 10px;
border-radius: 50%;
transition: background 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon:hover {
background: rgba(255, 255, 255, 0.1);
}
.btn-gallery {
position: relative;
}
.last-photo-thumb {
width: 40px;
height: 40px;
border-radius: 8px;
border: 2px solid white;
background-size: cover;
background-position: center;
display: block;
}
.placeholder-thumb {
width: 40px;
height: 40px;
border-radius: 8px;
border: 2px solid rgba(255,255,255,0.5);
background: rgba(255,255,255,0.1);
display: flex;
align-items: center;
justify-content: center;
color: rgba(255,255,255,0.5);
font-size: 1.2rem;
}
/* --- SHUTTER BUTTON --- */
.shutter-container {
position: relative;
width: 80px;
height: 80px;
display: flex;
justify-content: center;
align-items: center;
}
.shutter-outer {
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
border: 4px solid white;
box-sizing: border-box;
transition: transform 0.1s;
}
.shutter-inner {
width: 65px;
height: 65px;
background-color: white;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 0 15px rgba(0,0,0,0.3);
}
.shutter-inner:active {
transform: scale(0.9);
background-color: #ddd;
}
.shutter-inner:active + .shutter-outer {
transform: scale(0.95);
border-color: #ddd;
}
/* --- TOAST NOTIFICATION --- */
.toast {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.8);
background: rgba(0, 0, 0, 0.85);
color: white;
padding: 15px 25px;
border-radius: 30px;
font-size: 1rem;
opacity: 0;
pointer-events: none;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 200;
text-align: center;
backdrop-filter: blur(5px);
border: 1px solid rgba(255,255,255,0.1);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.toast.show {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
.toast i {
color: var(--primary-color);
font-size: 1.5rem;
}
/* --- PERMISSION ERROR STATE --- */
.error-message {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #111;
z-index: 300;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: 20px;
}
.error-message h2 {
color: #ff4444;
margin-bottom: 10px;
}
.error-message p {
color: #ccc;
max-width: 400px;
}
</style>
</head>
<body>
<!-- Header -->
<header>
<div class="brand">SnapCam</div>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">
Built with anycoder
</a>
</header>
<!-- Main Viewport -->
<main>
<video id="video" autoplay playsinline muted></video>
<div id="flash-overlay"></div>
<!-- Message d'erreur permissions -->
<div id="error-screen" class="error-message">
<i class="ph ph-camera-slash" style="font-size: 3rem; color: #666; margin-bottom: 1rem;"></i>
<h2>Accès Caméra Refusé</h2>
<p>Veuillez autoriser l'accès à la caméra dans votre navigateur pour utiliser cette application.</p>
</div>
</main>
<!-- Canvas pour le traitement image (caché) -->
<canvas id="canvas"></canvas>
<!-- Notification Toast -->
<div id="toast" class="toast">
<i class="ph ph-download-simple"></i>
<span>Photo enregistrée !</span>
</div>
<!-- Contrôles -->
<div class="controls-container">
<!-- Sélecteur de Filtres -->
<div class="filter-scroller" id="filter-list">
<!-- Les filtres seront générés par JS -->
</div>
<!-- Barre d'action (Shutter, Switch, Gallery) -->
<div class="action-bar">
<!-- Bouton Galerie (Miniature) -->
<button class="btn-icon btn-gallery" id="btn-gallery" aria-label="Galerie">
<div class="placeholder-thumb">
<i class="ph ph-image"></i>
</div>
</button>
<!-- Bouton Shutter -->
<div class="shutter-container">
<div class="shutter-inner" id="btn-capture"></div>
<div class="shutter-outer"></div>
</div>
<!-- Bouton Switch Caméra -->
<button class="btn-icon" id="btn-switch" aria-label="Changer de caméra">
<i class="ph ph-arrows-clockwise"></i>
</button>
</div>
</div>
<script>
/**
* Logique de l'application SnapCam
* Gère la caméra, les filtres CSS, la capture canvas et le téléchargement.
*/
// --- Configuration & État ---
const state = {
stream: null,
facingMode: 'user', // 'user' (front) ou 'environment' (back)
currentFilter: 'none',
filters: [
{ name: 'Normal', css: 'none', thumb: '' },
{ name: 'Noir & Blanc', css: 'grayscale(100%) contrast(1.2)', thumb: 'filter: grayscale(100%);' },
{ name: 'Sépia', css: 'sepia(100%)', thumb: 'filter: sepia(100%);' },
{ name: 'Vintage', css: 'sepia(50%) contrast(1.2) brightness(0.9)', thumb: 'filter: sepia(50%) contrast(1.2);' },
{ name: 'Froid', css: 'hue-rotate(180deg) saturate(1.5)', thumb: 'filter: hue-rotate(180deg) saturate(1.5);' },
{ name: 'Chaud', css: 'sepia(30%) saturate(1.4) contrast(1.1)', thumb: 'filter: sepia(30%) saturate(1.4);' },
{ name: 'Cyber', css: 'saturate(2) hue-rotate(20deg) contrast(1.2)', thumb: 'filter: saturate(2) hue-rotate(20deg);' },
{ name: 'Inversé', css: 'invert(100%)', thumb: 'filter: invert(100%);' },
{ name: 'Flou', css: 'blur(2px)', thumb: 'filter: blur(2px);' },
{ name: 'Contraste', css: 'contrast(200%)', thumb: 'filter: contrast(200%);' }
]
};
// --- Éléments DOM ---
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const filterList = document.getElementById('filter-list');
const btnCapture = document.getElementById('btn-capture');
const btnSwitch = document.getElementById('btn-switch');
const btnGallery = document.getElementById('btn-gallery');
const flashOverlay = document.getElementById('flash-overlay');
const toast = document.getElementById('toast');
const errorScreen = document.getElementById('error-screen');
// --- Initialisation ---
async function initCamera() {
try {
if (state.stream) {
state.stream.getTracks().forEach(track => track.stop());
}
const constraints = {
video: {
facingMode: state.facingMode,
width: { ideal: 1920 }, // Essaye d'avoir une bonne résolution
height: { ideal: 1080 }
},
audio: false
};
state.stream = await navigator.mediaDevices.getUserMedia(constraints);
video.srcObject = state.stream;
// Gérer le miroir (seulement pour la caméra frontale)
if (state.facingMode === 'user') {
video.style.transform = 'scaleX(-1)';
} else {
video.style.transform = 'scaleX(1)';
}
errorScreen.style.display = 'none';
} catch (err) {
console.error("Erreur caméra:", err);
errorScreen.style.display = 'flex';
}
}
// --- Génération des Filtres UI ---
function renderFilters() {
filterList.innerHTML = '';
state.filters.forEach((filter, index) => {
const item = document.createElement('div');
item.className = `filter-item ${filter.css === state.currentFilter ? 'active' : ''}`;
// Création de la miniature (placeholder coloré ou icon)
const preview = document.createElement('div');
preview.className = 'filter-preview';
// On simule le filtre sur un fond dégradé pour la preview
preview.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
// On applique le style CSS du filtre sur l'élément preview
if (filter.thumb) {
preview.style.cssText += filter.thumb;
}
const name = document.createElement('span');
name.className = 'filter-name';
name.innerText = filter.name;
item.appendChild(preview);
item.appendChild(name);
// Clic pour changer le filtre
item.addEventListener('click', () => {
// Mise à jour UI
document.querySelectorAll('.filter-item').forEach(el => el.classList.remove('active'));
item.classList.add('active');
// Mise à jour État & Vidéo
state.currentFilter = filter.css;
video.style.filter = state.currentFilter;
});
filterList.appendChild(item);
});
}
// --- Prise de Photo ---
function takePhoto() {
if (!state.stream) return;
// 1. Effet Flash
flashOverlay.style.opacity = 1;
setTimeout(() => { flashOverlay.style.opacity = 0; }, 100);
// 2. Configuration Canvas
// On utilise les dimensions réelles de la vidéo
const width = video.videoWidth;
const height = video.videoHeight;
canvas.width = width;
canvas.height = height;
// 3. Dessin
// Si on est en mode "selfie" (front), on doit flipper le canvas horizontalement pour que le texte soit lisible
// car la vidéo est flippée par CSS pour l'utilisateur, mais le stream raw n'est pas flippé.
if (state.facingMode === 'user') {
ctx.translate(width, 0);
ctx.scale(-1, 1);
}
// Appliquer le filtre sur le contexte
if (state.currentFilter !== 'none') {
ctx.filter = state.currentFilter;
}
ctx.drawImage(video, 0, 0, width, height);
// Reset transform pour éviter les bugs prochains dessins
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.filter = 'none';
// 4. Export & Téléchargement
const dataURL = canvas.toDataURL('image/jpeg', 0.95); // Qualité 95%
// Création d'un lien temporaire
const link = document.createElement('a');
const timestamp = new Date().getTime();
link.download = `snapcam_${timestamp}.jpg`;
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 5. Mise à jour Miniature Galerie
updateGalleryThumbnail(dataURL);
// 6. Feedback Toast
showToast();
}
// --- Mise à jour Miniature ---
function updateGalleryThumbnail(dataUrl) {
// Supprimer l'icône placeholder si elle existe
if (btnGallery.querySelector('.placeholder-thumb')) {
btnGallery.innerHTML = '';
}
// Créer l'image
const img = document.createElement('div');
img.className = 'last-photo-thumb';
img.style.backgroundImage = `url(${dataUrl})`;
btnGallery.appendChild(img);
}
// --- Afficher Toast ---
function showToast() {
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 2500);
}
// --- Changement de Caméra ---
function switchCamera() {
state.facingMode = state.facingMode === 'user' ? 'environment' : 'user';
initCamera();
}
// --- Écouteurs d'événements ---
btnCapture.addEventListener('click', takePhoto);
btnSwitch.addEventListener('click', switchCamera);
// Démarrage
window.addEventListener('DOMContentLoaded', () => {
renderFilters();
initCamera();
});
</script>
</body>
</html>