| | <!DOCTYPE html> |
| | <html lang="fr"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>HypnoMap Live</title> |
| | <script src="https://cdn.tailwindcss.com"></script> |
| | <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| | <style> |
| | .map-container { |
| | background-image: url('https://maps.googleapis.com/maps/api/staticmap?center=48.8566,2.3522&zoom=13&size=800x600&scale=2'); |
| | background-size: cover; |
| | background-position: center; |
| | } |
| | |
| | .bubble { |
| | position: absolute; |
| | border-radius: 50%; |
| | display: flex; |
| | align-items: center; |
| | justify-content: center; |
| | cursor: pointer; |
| | transition: all 0.3s ease; |
| | } |
| | |
| | .bubble:hover { |
| | transform: scale(1.05); |
| | } |
| | |
| | .message-banner { |
| | animation: slideIn 0.5s forwards, fadeOut 0.5s 5s forwards; |
| | } |
| | |
| | @keyframes slideIn { |
| | from { transform: translateY(100px); opacity: 0; } |
| | to { transform: translateY(0); opacity: 1; } |
| | } |
| | |
| | @keyframes fadeOut { |
| | from { opacity: 1; } |
| | to { opacity: 0; } |
| | } |
| | |
| | .range-slider::-webkit-slider-thumb { |
| | -webkit-appearance: none; |
| | width: 20px; |
| | height: 20px; |
| | border-radius: 50%; |
| | background: #4f46e5; |
| | cursor: pointer; |
| | } |
| | |
| | .participant-marker { |
| | position: absolute; |
| | width: 24px; |
| | height: 24px; |
| | border-radius: 50%; |
| | display: flex; |
| | align-items: center; |
| | justify-content: center; |
| | font-weight: bold; |
| | color: white; |
| | } |
| | </style> |
| | </head> |
| | <body class="bg-gray-900 text-white min-h-screen"> |
| | |
| | <div id="home-screen" class="flex flex-col items-center justify-center min-h-screen p-4"> |
| | <div class="text-center mb-12"> |
| | <h1 class="text-5xl font-bold mb-4 text-purple-500">HypnoMap Live</h1> |
| | <p class="text-xl text-gray-300">Expérience sonore géolocalisée</p> |
| | </div> |
| | |
| | <div class="w-full max-w-md space-y-6"> |
| | <button id="join-btn" class="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-4 px-6 rounded-lg text-xl transition flex items-center justify-center"> |
| | <i class="fas fa-user-friends mr-3"></i> Rejoindre une session |
| | </button> |
| | |
| | <button id="create-btn" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-4 px-6 rounded-lg text-xl transition flex items-center justify-center"> |
| | <i class="fas fa-music mr-3"></i> Créer une session (DJ) |
| | </button> |
| | </div> |
| | </div> |
| | |
| | |
| | <div id="join-modal" class="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50 hidden"> |
| | <div class="bg-gray-800 rounded-xl p-6 w-full max-w-md"> |
| | <div class="flex justify-between items-center mb-4"> |
| | <h2 class="text-2xl font-bold">Rejoindre une session</h2> |
| | <button id="close-join-modal" class="text-gray-400 hover:text-white"> |
| | <i class="fas fa-times"></i> |
| | </button> |
| | </div> |
| | |
| | <div class="mb-6"> |
| | <label class="block text-gray-300 mb-2">Code de session</label> |
| | <input type="text" id="session-code" class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-purple-500" placeholder="Entrez le code"> |
| | </div> |
| | |
| | <div class="mb-6"> |
| | <label class="block text-gray-300 mb-2">Votre pseudo</label> |
| | <input type="text" id="participant-name" class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-purple-500" placeholder="Optionnel"> |
| | </div> |
| | |
| | <button id="confirm-join" class="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-6 rounded-lg transition"> |
| | Rejoindre |
| | </button> |
| | </div> |
| | </div> |
| | |
| | |
| | <div id="dj-screen" class="hidden min-h-screen flex flex-col"> |
| | <div class="bg-gray-800 p-4 flex justify-between items-center"> |
| | <div> |
| | <h2 class="text-xl font-bold">Mode DJ</h2> |
| | <p class="text-sm text-gray-400">Session: <span id="dj-session-code" class="font-mono">XXXXXX</span></p> |
| | </div> |
| | <div class="flex space-x-2"> |
| | <button id="dj-settings" class="p-2 rounded-full hover:bg-gray-700"> |
| | <i class="fas fa-cog"></i> |
| | </button> |
| | <button id="leave-dj" class="p-2 rounded-full hover:bg-gray-700"> |
| | <i class="fas fa-sign-out-alt"></i> |
| | </button> |
| | </div> |
| | </div> |
| | |
| | <div class="relative flex-1"> |
| | <div id="dj-map" class="map-container w-full h-full relative overflow-hidden"> |
| | |
| | <div id="dj-bubbles-container"></div> |
| | <div id="dj-participants-container"></div> |
| | </div> |
| | |
| | <div class="absolute bottom-4 left-0 right-0 flex justify-center"> |
| | <button id="create-bubble-btn" class="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-6 rounded-full shadow-lg flex items-center"> |
| | <i class="fas fa-plus-circle mr-2"></i> Créer une bulle |
| | </button> |
| | </div> |
| | |
| | <div id="dj-controls" class="absolute top-4 right-4 bg-gray-800 bg-opacity-80 rounded-lg p-3 space-y-3"> |
| | <button id="play-all" class="p-2 bg-green-600 hover:bg-green-700 rounded-full"> |
| | <i class="fas fa-play"></i> |
| | </button> |
| | <button id="pause-all" class="p-2 bg-yellow-600 hover:bg-yellow-700 rounded-full"> |
| | <i class="fas fa-pause"></i> |
| | </button> |
| | <button id="global-message" class="p-2 bg-blue-600 hover:bg-blue-700 rounded-full"> |
| | <i class="fas fa-bullhorn"></i> |
| | </button> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | |
| | <div id="participant-screen" class="hidden min-h-screen flex flex-col"> |
| | <div class="bg-gray-800 p-4 flex justify-between items-center"> |
| | <div> |
| | <h2 class="text-xl font-bold">Mode Participant</h2> |
| | <p class="text-sm text-gray-400">Session: <span id="participant-session-code" class="font-mono">XXXXXX</span></p> |
| | </div> |
| | <div class="flex space-x-2"> |
| | <button id="leave-participant" class="p-2 rounded-full hover:bg-gray-700"> |
| | <i class="fas fa-sign-out-alt"></i> |
| | </button> |
| | </div> |
| | </div> |
| | |
| | <div class="relative flex-1"> |
| | <div id="participant-map" class="map-container w-full h-full relative overflow-hidden"> |
| | |
| | <div id="participant-bubbles-container"></div> |
| | <div id="participant-participants-container"></div> |
| | </div> |
| | |
| | <div id="message-banner" class="message-banner absolute bottom-20 left-0 right-0 mx-auto bg-purple-600 bg-opacity-90 rounded-lg p-4 max-w-md text-center hidden"> |
| | <p id="message-content" class="font-medium"></p> |
| | </div> |
| | |
| | <div class="absolute bottom-4 left-0 right-0 flex justify-center px-4"> |
| | <div class="flex w-full max-w-md bg-gray-800 bg-opacity-80 rounded-full p-2"> |
| | <input type="text" id="participant-message" class="flex-1 bg-transparent px-4 py-2 focus:outline-none" placeholder="Envoyer un message..." maxlength="140"> |
| | <button id="send-message" class="bg-purple-600 hover:bg-purple-700 rounded-full px-4"> |
| | <i class="fas fa-paper-plane"></i> |
| | </button> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | |
| | <div id="bubble-modal" class="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50 hidden"> |
| | <div class="bg-gray-800 rounded-xl p-6 w-full max-w-md"> |
| | <div class="flex justify-between items-center mb-4"> |
| | <h2 class="text-2xl font-bold">Nouvelle bulle sonore</h2> |
| | <button id="close-bubble-modal" class="text-gray-400 hover:text-white"> |
| | <i class="fas fa-times"></i> |
| | </button> |
| | </div> |
| | |
| | <div class="mb-4"> |
| | <label class="block text-gray-300 mb-2">Nom de la bulle</label> |
| | <input type="text" id="bubble-name" class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-purple-500" placeholder="Ma bulle sonore"> |
| | </div> |
| | |
| | <div class="mb-4"> |
| | <label class="block text-gray-300 mb-2">Fichier audio</label> |
| | <div class="flex items-center"> |
| | <label for="audio-upload" class="bg-gray-700 hover:bg-gray-600 border border-gray-600 rounded-lg px-4 py-3 cursor-pointer flex-1"> |
| | <span id="audio-file-name" class="text-gray-300">Sélectionner un fichier...</span> |
| | </label> |
| | <input type="file" id="audio-upload" class="hidden" accept="audio/*"> |
| | </div> |
| | </div> |
| | |
| | <div class="mb-6"> |
| | <label class="block text-gray-300 mb-2">Rayon d'activation: <span id="radius-value">25</span>m</label> |
| | <input type="range" id="bubble-radius" min="5" max="50" value="25" class="w-full range-slider"> |
| | </div> |
| | |
| | <button id="confirm-bubble" class="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-6 rounded-lg transition"> |
| | Créer la bulle |
| | </button> |
| | </div> |
| | </div> |
| | |
| | |
| | <div id="global-message-modal" class="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50 hidden"> |
| | <div class="bg-gray-800 rounded-xl p-6 w-full max-w-md"> |
| | <div class="flex justify-between items-center mb-4"> |
| | <h2 class="text-2xl font-bold">Message global</h2> |
| | <button id="close-global-message-modal" class="text-gray-400 hover:text-white"> |
| | <i class="fas fa-times"></i> |
| | </button> |
| | </div> |
| | |
| | <div class="mb-6"> |
| | <label class="block text-gray-300 mb-2">Message</label> |
| | <textarea id="global-message-text" class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-3 h-32 focus:outline-none focus:ring-2 focus:ring-purple-500" placeholder="Écrivez un message pour tous les participants..." maxlength="140"></textarea> |
| | </div> |
| | |
| | <button id="send-global-message" class="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-6 rounded-lg transition"> |
| | Envoyer |
| | </button> |
| | </div> |
| | </div> |
| | |
| | <script> |
| | |
| | const appState = { |
| | currentScreen: 'home', |
| | isDJ: false, |
| | sessionCode: '', |
| | bubbles: [], |
| | participants: [], |
| | messages: [], |
| | currentUser: { |
| | id: Math.random().toString(36).substring(2, 10), |
| | name: 'Participant ' + Math.floor(Math.random() * 1000), |
| | position: { x: 50, y: 50 } |
| | } |
| | }; |
| | |
| | |
| | const homeScreen = document.getElementById('home-screen'); |
| | const joinModal = document.getElementById('join-modal'); |
| | const djScreen = document.getElementById('dj-screen'); |
| | const participantScreen = document.getElementById('participant-screen'); |
| | const bubbleModal = document.getElementById('bubble-modal'); |
| | const globalMessageModal = document.getElementById('global-message-modal'); |
| | |
| | |
| | document.getElementById('join-btn').addEventListener('click', () => { |
| | joinModal.classList.remove('hidden'); |
| | }); |
| | |
| | document.getElementById('create-btn').addEventListener('click', () => { |
| | createSession(true); |
| | }); |
| | |
| | document.getElementById('close-join-modal').addEventListener('click', () => { |
| | joinModal.classList.add('hidden'); |
| | }); |
| | |
| | document.getElementById('confirm-join').addEventListener('click', () => { |
| | const code = document.getElementById('session-code').value; |
| | const name = document.getElementById('participant-name').value; |
| | |
| | if (code) { |
| | if (name) appState.currentUser.name = name; |
| | joinSession(code); |
| | joinModal.classList.add('hidden'); |
| | } else { |
| | alert('Veuillez entrer un code de session'); |
| | } |
| | }); |
| | |
| | document.getElementById('leave-dj').addEventListener('click', () => { |
| | showScreen('home'); |
| | }); |
| | |
| | document.getElementById('leave-participant').addEventListener('click', () => { |
| | showScreen('home'); |
| | }); |
| | |
| | document.getElementById('create-bubble-btn').addEventListener('click', () => { |
| | bubbleModal.classList.remove('hidden'); |
| | }); |
| | |
| | document.getElementById('close-bubble-modal').addEventListener('click', () => { |
| | bubbleModal.classList.add('hidden'); |
| | }); |
| | |
| | document.getElementById('confirm-bubble').addEventListener('click', () => { |
| | const name = document.getElementById('bubble-name').value; |
| | const radius = parseInt(document.getElementById('bubble-radius').value); |
| | |
| | if (name) { |
| | createBubble(name, radius); |
| | bubbleModal.classList.add('hidden'); |
| | } else { |
| | alert('Veuillez donner un nom à votre bulle'); |
| | } |
| | }); |
| | |
| | document.getElementById('bubble-radius').addEventListener('input', (e) => { |
| | document.getElementById('radius-value').textContent = e.target.value; |
| | }); |
| | |
| | document.getElementById('audio-upload').addEventListener('change', (e) => { |
| | const file = e.target.files[0]; |
| | if (file) { |
| | document.getElementById('audio-file-name').textContent = file.name; |
| | } |
| | }); |
| | |
| | document.getElementById('global-message').addEventListener('click', () => { |
| | globalMessageModal.classList.remove('hidden'); |
| | }); |
| | |
| | document.getElementById('close-global-message-modal').addEventListener('click', () => { |
| | globalMessageModal.classList.add('hidden'); |
| | }); |
| | |
| | document.getElementById('send-global-message').addEventListener('click', () => { |
| | const message = document.getElementById('global-message-text').value; |
| | if (message) { |
| | sendGlobalMessage(message); |
| | globalMessageModal.classList.add('hidden'); |
| | document.getElementById('global-message-text').value = ''; |
| | } |
| | }); |
| | |
| | document.getElementById('send-message').addEventListener('click', () => { |
| | const message = document.getElementById('participant-message').value; |
| | if (message) { |
| | sendParticipantMessage(message); |
| | document.getElementById('participant-message').value = ''; |
| | } |
| | }); |
| | |
| | |
| | function showScreen(screen) { |
| | homeScreen.classList.add('hidden'); |
| | djScreen.classList.add('hidden'); |
| | participantScreen.classList.add('hidden'); |
| | |
| | appState.currentScreen = screen; |
| | |
| | if (screen === 'home') { |
| | homeScreen.classList.remove('hidden'); |
| | } else if (screen === 'dj') { |
| | djScreen.classList.remove('hidden'); |
| | renderDJMap(); |
| | } else if (screen === 'participant') { |
| | participantScreen.classList.remove('hidden'); |
| | renderParticipantMap(); |
| | } |
| | } |
| | |
| | function createSession(isDJ) { |
| | appState.isDJ = isDJ; |
| | appState.sessionCode = generateSessionCode(); |
| | |
| | if (isDJ) { |
| | document.getElementById('dj-session-code').textContent = appState.sessionCode; |
| | showScreen('dj'); |
| | } else { |
| | document.getElementById('participant-session-code').textContent = appState.sessionCode; |
| | showScreen('participant'); |
| | } |
| | |
| | |
| | setTimeout(() => { |
| | addParticipant('DJ Master', { x: 30, y: 30 }); |
| | addParticipant('Sound Explorer', { x: 70, y: 70 }); |
| | addParticipant('Music Traveler', { x: 60, y: 40 }); |
| | |
| | if (isDJ) { |
| | renderDJMap(); |
| | } else { |
| | renderParticipantMap(); |
| | } |
| | }, 1000); |
| | } |
| | |
| | function joinSession(code) { |
| | appState.isDJ = false; |
| | appState.sessionCode = code; |
| | document.getElementById('participant-session-code').textContent = code; |
| | showScreen('participant'); |
| | |
| | |
| | setTimeout(() => { |
| | addBubble('Ambiance Chill', 20, { x: 40, y: 60 }); |
| | addBubble('Rythme Urbain', 15, { x: 70, y: 30 }); |
| | |
| | renderParticipantMap(); |
| | }, 1000); |
| | } |
| | |
| | function generateSessionCode() { |
| | return Math.random().toString(36).substring(2, 8).toUpperCase(); |
| | } |
| | |
| | function createBubble(name, radius) { |
| | const bubble = { |
| | id: Math.random().toString(36).substring(2, 10), |
| | name, |
| | radius, |
| | position: { x: 50, y: 50 }, |
| | audio: 'sample.mp3' |
| | }; |
| | |
| | appState.bubbles.push(bubble); |
| | renderDJMap(); |
| | } |
| | |
| | function addBubble(name, radius, position) { |
| | const bubble = { |
| | id: Math.random().toString(36).substring(2, 10), |
| | name, |
| | radius, |
| | position, |
| | audio: 'sample.mp3' |
| | }; |
| | |
| | appState.bubbles.push(bubble); |
| | } |
| | |
| | function addParticipant(name, position) { |
| | const participant = { |
| | id: Math.random().toString(36).substring(2, 10), |
| | name, |
| | position |
| | }; |
| | |
| | appState.participants.push(participant); |
| | } |
| | |
| | function sendGlobalMessage(message) { |
| | |
| | console.log('Global message sent:', message); |
| | } |
| | |
| | function sendParticipantMessage(message) { |
| | const messageObj = { |
| | id: Math.random().toString(36).substring(2, 10), |
| | sender: appState.currentUser.name, |
| | content: message, |
| | timestamp: new Date() |
| | }; |
| | |
| | |
| | const banner = document.getElementById('message-banner'); |
| | const content = document.getElementById('message-content'); |
| | |
| | content.textContent = `${messageObj.sender}: ${messageObj.content}`; |
| | banner.classList.remove('hidden'); |
| | |
| | |
| | setTimeout(() => { |
| | banner.classList.add('hidden'); |
| | }, 5500); |
| | } |
| | |
| | function renderDJMap() { |
| | const container = document.getElementById('dj-bubbles-container'); |
| | const participantsContainer = document.getElementById('dj-participants-container'); |
| | |
| | container.innerHTML = ''; |
| | participantsContainer.innerHTML = ''; |
| | |
| | |
| | appState.bubbles.forEach(bubble => { |
| | const bubbleEl = document.createElement('div'); |
| | bubbleEl.className = 'bubble bg-purple-500 bg-opacity-30 border-2 border-purple-400'; |
| | bubbleEl.style.width = `${bubble.radius * 6}px`; |
| | bubbleEl.style.height = `${bubble.radius * 6}px`; |
| | bubbleEl.style.left = `${bubble.position.x}%`; |
| | bubbleEl.style.top = `${bubble.position.y}%`; |
| | bubbleEl.title = bubble.name; |
| | |
| | |
| | bubbleEl.draggable = true; |
| | bubbleEl.addEventListener('dragstart', (e) => { |
| | e.dataTransfer.setData('text/plain', bubble.id); |
| | }); |
| | |
| | container.appendChild(bubbleEl); |
| | }); |
| | |
| | |
| | appState.participants.forEach(participant => { |
| | const participantEl = document.createElement('div'); |
| | participantEl.className = 'participant-marker bg-blue-500'; |
| | participantEl.style.left = `${participant.position.x}%`; |
| | participantEl.style.top = `${participant.position.y}%`; |
| | participantEl.textContent = participant.name.charAt(0); |
| | participantEl.title = participant.name; |
| | |
| | participantsContainer.appendChild(participantEl); |
| | }); |
| | |
| | |
| | const map = document.getElementById('dj-map'); |
| | map.addEventListener('dragover', (e) => { |
| | e.preventDefault(); |
| | }); |
| | |
| | map.addEventListener('drop', (e) => { |
| | e.preventDefault(); |
| | const bubbleId = e.dataTransfer.getData('text/plain'); |
| | const bubble = appState.bubbles.find(b => b.id === bubbleId); |
| | |
| | if (bubble) { |
| | const rect = map.getBoundingClientRect(); |
| | const x = ((e.clientX - rect.left) / rect.width) * 100; |
| | const y = ((e.clientY - rect.top) / rect.height) * 100; |
| | |
| | bubble.position = { x, y }; |
| | renderDJMap(); |
| | } |
| | }); |
| | } |
| | |
| | function renderParticipantMap() { |
| | const container = document.getElementById('participant-bubbles-container'); |
| | const participantsContainer = document.getElementById('participant-participants-container'); |
| | |
| | container.innerHTML = ''; |
| | participantsContainer.innerHTML = ''; |
| | |
| | |
| | appState.bubbles.forEach(bubble => { |
| | const bubbleEl = document.createElement('div'); |
| | bubbleEl.className = 'bubble bg-purple-500 bg-opacity-20 border border-purple-400'; |
| | bubbleEl.style.width = `${bubble.radius * 6}px`; |
| | bubbleEl.style.height = `${bubble.radius * 6}px`; |
| | bubbleEl.style.left = `${bubble.position.x}%`; |
| | bubbleEl.style.top = `${bubble.position.y}%`; |
| | bubbleEl.title = bubble.name; |
| | |
| | container.appendChild(bubbleEl); |
| | }); |
| | |
| | |
| | appState.participants.forEach(participant => { |
| | const participantEl = document.createElement('div'); |
| | participantEl.className = 'participant-marker bg-blue-500'; |
| | participantEl.style.left = `${participant.position.x}%`; |
| | participantEl.style.top = `${participant.position.y}%`; |
| | participantEl.textContent = participant.name.charAt(0); |
| | participantEl.title = participant.name; |
| | |
| | participantsContainer.appendChild(participantEl); |
| | }); |
| | |
| | |
| | const currentUserEl = document.createElement('div'); |
| | currentUserEl.className = 'participant-marker bg-green-500'; |
| | currentUserEl.style.left = `${appState.currentUser.position.x}%`; |
| | currentUserEl.style.top = `${appState.currentUser.position.y}%`; |
| | currentUserEl.textContent = appState.currentUser.name.charAt(0); |
| | currentUserEl.title = appState.currentUser.name; |
| | |
| | participantsContainer.appendChild(currentUserEl); |
| | } |
| | |
| | |
| | showScreen('home'); |
| | </script> |
| | <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=PierreH/test" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| | </html> |