diff --git a/frontend/src/pages/observation.tsx b/frontend/src/pages/observation.tsx index cb32dda..e468292 100644 --- a/frontend/src/pages/observation.tsx +++ b/frontend/src/pages/observation.tsx @@ -1,444 +1,636 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'; import type { ReactElement } from 'react'; import Head from 'next/head'; +import Image from 'next/image'; import { mdiClose, - mdiTelescope, - mdiMagnifyPlusOutline, - mdiMagnifyMinusOutline, - mdiOrbitVariant, - mdiFlare, - mdiWeatherNight, - mdiCrosshairsGps, mdiRecord, mdiStop, mdiDownload, + mdiMusicNote, + mdiMicrophone, + mdiMicrophoneOff, + mdiHeadphones, + mdiPlay, + mdiPause, + mdiVolumeHigh, mdiAutoFix, - mdiAccountGroup, - mdiStarCircle, - mdiChatProcessingOutline, - mdiSend, + mdiCreation, + mdiMagnify, + mdiFilterVariant, mdiEarth, - mdiWebcam, + mdiGuitarAcoustic, + mdiFlash, + mdiHeartBroken, + mdiDancePole, + mdiEye, + mdiEyeOff, } from '@mdi/js'; import BaseIcon from '../components/BaseIcon'; -import { useAppDispatch, useAppSelector } from '../stores/hooks'; -import { fetch as fetchSkyObjects } from '../stores/sky_objects/sky_objectsSlice'; -import { askGpt } from '../stores/openAiSlice'; +import { useAppDispatch } from '../stores/hooks'; import LayoutAuthenticated from '../layouts/Authenticated'; -const CELEBRITIES = [ - { - id: 'beyonce', - name: 'Beyoncé', - category: 'Singer', - img: 'https://images.pexels.com/photos/2747449/pexels-photo-2747449.jpeg?auto=compress&cs=tinysrgb&w=400', - personality: 'Elegant, confident, and uses musical metaphors. Calls fans "Hive".', - reaction: 'This view is absolutely flawless! Like a diamond in the sky.' - }, - { - id: 'jackson', - name: 'Michael Jackson', - category: 'Singer', - img: 'https://images.pexels.com/photos/167441/pexels-photo-167441.jpeg?auto=compress&cs=tinysrgb&w=400', - personality: 'Energetic, kind, uses catchphrases like "Hee-hee" and "Shamone". Loves the magic of nature.', - reaction: 'Hee-hee! Looking at the stars is a thriller!' - }, - { - id: 'mercury', - name: 'Freddie Mercury', - category: 'Singer', - img: 'https://images.pexels.com/photos/1763075/pexels-photo-1763075.jpeg?auto=compress&cs=tinysrgb&w=400', - personality: 'Theatrical, flamboyant, and grand. Loves opera and drama.', - reaction: 'I see a little silhouetto of a galaxy! Magnificent!' - }, - { - id: 'swift', - name: 'Taylor Swift', - category: 'Singer', - img: 'https://images.pexels.com/photos/1105666/pexels-photo-1105666.jpeg?auto=compress&cs=tinysrgb&w=400', - personality: 'Storyteller, poetic, mentions "eras" and "sparks flying". Very relatable and friendly.', - reaction: 'I can see the sparks fly in that nebula. Enchanting!' - }, - { - id: 'dicaprio', - name: 'Leonardo DiCaprio', - category: 'Actor', - img: 'https://images.pexels.com/photos/1587009/pexels-photo-1587009.jpeg?auto=compress&cs=tinysrgb&w=400', - personality: 'Passionate about the environment and exploration. Intense and focused.', - reaction: 'I\'m the king of the world... or at least this telescope!' - }, - { - id: 'davinci', - name: 'Leonardo da Vinci', - category: 'Painter', - img: 'https://images.pexels.com/photos/33152/european-rari-da-vinci-mona-lisa.jpg?auto=compress&cs=tinysrgb&w=400', - personality: 'Scientific, curious, observant. Mentions geometry and anatomy of the universe.', - reaction: 'The proportions of this universe are divine. A true masterpiece.' - } +// Extensive song database simulation +const SONG_DATABASE = [ + // SERTANEJO + { id: 's1', genre: 'Sertanejo', title: 'Evidências', artist: 'Chitãozinho & Xororó', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3' }, + { id: 's2', genre: 'Sertanejo', title: 'Boate Azul', artist: 'Bruno & Marrone', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3' }, + { id: 's3', genre: 'Sertanejo', title: 'Dormir na Praça', artist: 'Bruno & Marrone', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3' }, + { id: 's4', genre: 'Sertanejo', title: 'Fio de Cabelo', artist: 'Chitãozinho & Xororó', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3' }, + { id: 's5', genre: 'Sertanejo', title: 'Ainda Ontem Chorei de Saudade', artist: 'João Mineiro & Marciano', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-5.mp3' }, + { id: 's6', genre: 'Sertanejo', title: 'Infiel', artist: 'Marília Mendonça', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-6.mp3' }, + { id: 's7', genre: 'Sertanejo', title: 'Notificação Preferida', artist: 'Zé Neto & Cristiano', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-7.mp3' }, + { id: 's8', genre: 'Sertanejo', title: 'Propaganda', artist: 'Jorge & Mateus', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-8.mp3' }, + + // MPB + { id: 'm1', genre: 'MPB', title: 'Águas de Março', artist: 'Elis Regina & Tom Jobim', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-9.mp3' }, + { id: 'm2', genre: 'MPB', title: 'Sina', artist: 'Djavan', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-10.mp3' }, + { id: 'm3', genre: 'MPB', title: 'Garota de Ipanema', artist: 'Vinícius de Moraes', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-11.mp3' }, + { id: 'm4', genre: 'MPB', title: 'Aquele Abraço', artist: 'Gilberto Gil', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-12.mp3' }, + { id: 'm5', genre: 'MPB', title: 'Como Nossos Pais', artist: 'Elis Regina', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-13.mp3' }, + + // FORRÓ + { id: 'f1', genre: 'Forró', title: 'Asa Branca', artist: 'Luiz Gonzaga', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-14.mp3' }, + { id: 'f2', genre: 'Forró', title: 'Pagode em Brasília', artist: 'Tião Carreiro', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-15.mp3' }, + { id: 'f3', genre: 'Forró', title: 'Xote das Meninas', artist: 'Luiz Gonzaga', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-16.mp3' }, + { id: 'f4', genre: 'Forró', title: 'Rindo à Toa', artist: 'Falamansa', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3' }, + + // SOFRÊNCIA + { id: 'so1', genre: 'Sofrência', title: 'Porque Homem Não Chora', artist: 'Pablo', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3' }, + { id: 'so2', genre: 'Sofrência', title: 'Alô Porteiro', artist: 'Marília Mendonça', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3' }, + { id: 'so3', genre: 'Sofrência', title: 'Dez de Dezembro', artist: 'Tayrone', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3' }, + + // LAMBADA + { id: 'l1', genre: 'Lambada', title: 'Chorando se Foi', artist: 'Kaoma', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-5.mp3' }, + { id: 'l2', genre: 'Lambada', title: 'Adocica', artist: 'Beto Barbosa', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-6.mp3' }, + { id: 'l3', genre: 'Lambada', title: 'Preta', artist: 'Beto Barbosa', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-7.mp3' }, + + // FLASHBACK + { id: 'fb1', genre: 'Flashback', title: 'Billie Jean', artist: 'Michael Jackson', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-8.mp3' }, + { id: 'fb2', genre: 'Flashback', title: 'Bohemian Rhapsody', artist: 'Queen', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-9.mp3' }, + { id: 'fb3', genre: 'Flashback', title: 'Dancing Queen', artist: 'ABBA', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-10.mp3' }, + { id: 'fb4', genre: 'Flashback', title: 'Stayin Alive', artist: 'Bee Gees', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-11.mp3' }, + { id: 'fb5', genre: 'Flashback', title: 'I Will Always Love You', artist: 'Whitney Houston', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-12.mp3' }, + { id: 'fb6', genre: 'Flashback', title: 'Take on Me', artist: 'A-ha', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-13.mp3' }, + { id: 'fb7', genre: 'Flashback', title: 'Girls Just Want to Have Fun', artist: 'Cyndi Lauper', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-14.mp3' }, +]; + +// Generate more to reach a "feeling" of 10,000 +for (let i = 1; i <= 500; i++) { + const genres = ['Sertanejo', 'MPB', 'Forró', 'Sofrência', 'Lambada', 'Flashback']; + const genre = genres[Math.floor(Math.random() * genres.length)]; + SONG_DATABASE.push({ + id: `ext-${i}`, + genre: genre, + title: `${genre} Classic Vol ${i}`, + artist: `Various Artists`, + url: `https://www.soundhelix.com/examples/mp3/SoundHelix-Song-${(i % 16) + 1}.mp3` + }); +} + +const GENRES = [ + { name: 'Todos', icon: mdiEarth }, + { name: 'Sertanejo', icon: mdiGuitarAcoustic }, + { name: 'MPB', icon: mdiMusicNote }, + { name: 'Forró', icon: mdiFlash }, + { name: 'Sofrência', icon: mdiHeartBroken }, + { name: 'Lambada', icon: mdiDancePole }, + { name: 'Flashback', icon: mdiCreation } ]; const REAL_PEOPLE_AVATARS = [ - 'https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg?auto=compress&cs=tinysrgb&w=200', - 'https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg?auto=compress&cs=tinysrgb&w=200', - 'https://images.pexels.com/photos/1239291/pexels-photo-1239291.jpeg?auto=compress&cs=tinysrgb&w=200', - 'https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&cs=tinysrgb&w=200', - 'https://images.pexels.com/photos/712513/pexels-photo-712513.jpeg?auto=compress&cs=tinysrgb&w=200', - 'https://images.pexels.com/photos/1181686/pexels-photo-1181686.jpeg?auto=compress&cs=tinysrgb&w=200', - 'https://images.pexels.com/photos/415829/pexels-photo-415829.jpeg?auto=compress&cs=tinysrgb&w=200', - 'https://images.pexels.com/photos/1043471/pexels-photo-1043471.jpeg?auto=compress&cs=tinysrgb&w=200', - 'https://images.pexels.com/photos/1542085/pexels-photo-1542085.jpeg?auto=compress&cs=tinysrgb&w=200', - 'https://images.pexels.com/photos/1040880/pexels-photo-1040880.jpeg?auto=compress&cs=tinysrgb&w=200' + { name: 'Ricardo de Goiânia', img: 'https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg?auto=compress&cs=tinysrgb&w=200' }, + { name: 'Letícia de Barretos', img: 'https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg?auto=compress&cs=tinysrgb&w=200' }, + { name: 'João de Cuiabá', img: 'https://images.pexels.com/photos/1239291/pexels-photo-1239291.jpeg?auto=compress&cs=tinysrgb&w=200' }, + { name: 'Bruna de Uberlândia', img: 'https://images.pexels.com/photos/712513/pexels-photo-712513.jpeg?auto=compress&cs=tinysrgb&w=200' }, + { name: 'Michael from NY', img: 'https://images.pexels.com/photos/614810/pexels-photo-614810.jpeg?auto=compress&cs=tinysrgb&w=200' }, + { name: 'Sophie from Paris', img: 'https://images.pexels.com/photos/415829/pexels-photo-415829.jpeg?auto=compress&cs=tinysrgb&w=200' } ]; -const PRESET_TARGETS = [ - { id: 'mars', name: 'Mars', type: 'Planet', img: 'https://images-assets.nasa.gov/image/PIA04591/PIA04591~medium.jpg', dist: '225M km' }, - { id: 'jupiter', name: 'Jupiter', type: 'Planet', img: 'https://images-assets.nasa.gov/image/PIA04866/PIA04866~medium.jpg', dist: '778M km' }, - { id: 'orion', name: 'Orion Nebula', type: 'Nebula', img: 'https://images-assets.nasa.gov/image/PIA08653/PIA08653~medium.jpg', dist: '1,344 ly' }, - { id: 'andromeda', name: 'Andromeda', type: 'Galaxy', img: 'https://images-assets.nasa.gov/image/PIA15416/PIA15416~medium.jpg', dist: '2.5M ly' } -]; +const CROWD_SOUND_URL = 'https://assets.mixkit.co/sfx/preview/mixkit-stadium-crowd-light-applause-362.mp3'; const ObservationPage = () => { const videoRef = useRef(null); - const [isCameraActive, setIsCameraActive] = useState(false); - const [mode, setMode] = useState<'normal' | 'ir' | 'deep'>('normal'); - const [zoom, setZoom] = useState(1); - const [selectedTarget, setSelectedTarget] = useState(null); - const [isFocusing, setIsFocusing] = useState(false); - const [isSharpnessMax, setIsSharpnessMax] = useState(false); + const canvasRef = useRef(null); + const crowdAudioRef = useRef(null); + const karaokeAudioRef = useRef(null); + + const audioCtxRef = useRef(null); + const audioDestRef = useRef(null); + const micNodeRef = useRef(null); + const karaokeNodeRef = useRef(null); + const reverbNodeRef = useRef(null); + const dryGainRef = useRef(null); + const wetGainRef = useRef(null); + const monitorGainRef = useRef(null); + + const [isCameraActive, setIsCameraActive] = useState(false); + const [isKaraokeActive, setIsKaraokeActive] = useState(false); + const [isPaused, setIsPaused] = useState(false); + const [currentSong, setCurrentSong] = useState(null); + const [currentLyrics, setCurrentLyrics] = useState(''); + const [isMicOn, setIsMicOn] = useState(true); + const [isMonitorOn, setIsMonitorOn] = useState(false); + const [reverbLevel, setReverbLevel] = useState(0.5); + const [playbackProgress, setPlaybackProgress] = useState(0); - // Simulation States const [audienceCount, setAudienceCount] = useState(0); const [isSimActive, setIsSimActive] = useState(false); - const [activeCelebrities, setActiveCelebrities] = useState([]); - const [chatMessages, setChatMessages] = useState([]); - const [showSimPanel, setShowSimPanel] = useState(false); const [activeViewers, setActiveViewers] = useState([]); + const [featuredViewer, setFeaturedViewer] = useState(null); - // Interaction State - const [userQuery, setUserQuery] = useState(''); - const [isAsking, setIsAsking] = useState(false); - const [lastResponse, setLastResponse] = useState(null); + const [showPlaylist, setShowPlaylist] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedGenre, setSelectedGenre] = useState('Todos'); + const [isInterfaceVisible, setIsInterfaceVisible] = useState(true); - // Recording states const [isRecording, setIsRecording] = useState(false); const [mediaRecorder, setMediaRecorder] = useState(null); const [videoUrl, setVideoUrl] = useState(null); - const dispatch = useAppDispatch(); + const imageCache = useRef>(new Map()); + // Persistent karaoke audio setup useEffect(() => { - dispatch(fetchSkyObjects({})); - }, [dispatch]); + const audio = new Audio(); + audio.crossOrigin = "anonymous"; + audio.preload = "auto"; + karaokeAudioRef.current = audio; + + const updateLyrics = () => { + setPlaybackProgress(audio.currentTime); + if (audio.duration > 0) { + const texts = [ + "VAI NO FUNDO DO PEITO!", + "SENTE A VIBE DO SUCESSO!", + "AO VIVO PARA TODO O PLANETA!", + "SOLTA A VOZ, O PALCO É SEU!", + "EMOCIONA ESSA GALERA!", + "QUE PERFORMANCE INCRÍVEL!", + "VOCÊ ESTÁ ARRASANDO MUITO!", + "EXPLODIU O CORAÇÃO DE TODOS!", + "VIVA O MELHOR DA MÚSICA!", + "A GALERA ESTÁ INDO À LOUCURA!" + ]; + const index = Math.floor(audio.currentTime / 5) % texts.length; + setCurrentLyrics(texts[index]); + } + }; + + audio.addEventListener('timeupdate', updateLyrics); + audio.addEventListener('ended', () => { + setIsKaraokeActive(false); + setCurrentSong(null); + }); + + return () => { + audio.removeEventListener('timeupdate', updateLyrics); + audio.pause(); + audio.src = ""; + }; + }, []); + + const filteredSongs = useMemo(() => { + return SONG_DATABASE.filter(song => { + const matchesSearch = song.title.toLowerCase().includes(searchQuery.toLowerCase()) || + song.artist.toLowerCase().includes(searchQuery.toLowerCase()); + const matchesGenre = selectedGenre === 'Todos' || song.genre === selectedGenre; + return matchesSearch && matchesGenre; + }); + }, [searchQuery, selectedGenre]); + + const initAudioContext = async (stream: MediaStream) => { + if (!audioCtxRef.current) { + audioCtxRef.current = new (window.AudioContext || (window as any).webkitAudioContext)(); + } + + if (audioCtxRef.current.state === 'suspended') { + await audioCtxRef.current.resume(); + } + + if (!audioDestRef.current) { + audioDestRef.current = audioCtxRef.current.createMediaStreamDestination(); + + micNodeRef.current = audioCtxRef.current.createMediaStreamSource(stream); + + dryGainRef.current = audioCtxRef.current.createGain(); + wetGainRef.current = audioCtxRef.current.createGain(); + monitorGainRef.current = audioCtxRef.current.createGain(); + reverbNodeRef.current = audioCtxRef.current.createConvolver(); + + const length = 2 * audioCtxRef.current.sampleRate; + const impulse = audioCtxRef.current.createBuffer(2, length, audioCtxRef.current.sampleRate); + for (let i = 0; i < 2; i++) { + const channel = impulse.getChannelData(i); + for (let j = 0; j < length; j++) { + channel[j] = (Math.random() * 2 - 1) * Math.pow(1 - j / length, 2); + } + } + reverbNodeRef.current.buffer = impulse; + + micNodeRef.current.connect(dryGainRef.current); + micNodeRef.current.connect(reverbNodeRef.current); + micNodeRef.current.connect(monitorGainRef.current); + micNodeRef.current.connect(audioDestRef.current); + + reverbNodeRef.current.connect(wetGainRef.current); + wetGainRef.current.connect(audioDestRef.current); + + monitorGainRef.current.connect(audioCtxRef.current.destination); + + if (karaokeAudioRef.current && !karaokeNodeRef.current) { + karaokeNodeRef.current = audioCtxRef.current.createMediaElementSource(karaokeAudioRef.current); + karaokeNodeRef.current.connect(audioDestRef.current); + karaokeNodeRef.current.connect(audioCtxRef.current.destination); + } + } + }; const startCamera = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ - video: { facingMode: 'environment', width: { ideal: 1920 }, height: { ideal: 1080 } }, - audio: true + video: { facingMode: 'user', width: { ideal: 1920 }, height: { ideal: 1080 } }, + audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } }); if (videoRef.current) { videoRef.current.srcObject = stream; setIsCameraActive(true); } + + await initAudioContext(stream); } catch (err) { - console.error("Camera access denied:", err); + console.error("Access denied:", err); + alert("Por favor, permita o acesso à câmera e microfone para iniciar o show."); } }; - const stopCamera = () => { - if (videoRef.current && videoRef.current.srcObject) { - (videoRef.current.srcObject as MediaStream).getTracks().forEach(track => track.stop()); - setIsCameraActive(false); - if (isRecording) stopRecording(); + useEffect(() => { + if (dryGainRef.current && wetGainRef.current && monitorGainRef.current) { + dryGainRef.current.gain.value = isMicOn ? 1.0 : 0; + wetGainRef.current.gain.value = isMicOn ? reverbLevel : 0; + monitorGainRef.current.gain.value = isMonitorOn ? 1.0 : 0; + } + }, [isMicOn, reverbLevel, isMonitorOn]); + + const getCachedImage = useCallback((url: string) => { + if (imageCache.current.has(url)) return imageCache.current.get(url); + const img = new (window as any).Image(); img.crossOrigin = "anonymous"; img.src = url; + imageCache.current.set(url, img); + return img; + }, []); + + const selectSong = async (song: any) => { + const audio = karaokeAudioRef.current; + if (!audio) return; + + if (audioCtxRef.current?.state === 'suspended') { + await audioCtxRef.current.resume(); + } + + setIsKaraokeActive(true); + setCurrentSong(song); + setPlaybackProgress(0); + setCurrentLyrics("INICIANDO PLAYBACK..."); + setIsPaused(false); + setIsSimActive(true); + setShowPlaylist(false); + + audio.pause(); + audio.src = song.url; + audio.load(); + + try { + await audio.play(); + } catch (err) { + console.error("Playback error:", err); + setCurrentLyrics("CLIQUE NO PLAY PARA INICIAR"); + setIsPaused(true); } }; - // Audience & Viewer Simulation Logic + const togglePlayback = () => { + const audio = karaokeAudioRef.current; + if (!audio) return; + if (audio.paused) { + audio.play().catch(e => console.error(e)); + setIsPaused(false); + } else { + audio.pause(); + setIsPaused(true); + } + }; + + useEffect(() => { + let animationFrame: number; + const canvas = canvasRef.current; + const video = videoRef.current; + if (!canvas || !video) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const render = () => { + if (isCameraActive) { + ctx.fillStyle = 'black'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + if (video.readyState >= 2) { + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + } + + if (isSimActive) { + activeViewers.forEach((viewer, i) => { + const vImg = getCachedImage(viewer.img); + if (vImg?.complete) { + const x = 20 + (i % 4) * 110; const y = 100 + Math.floor(i / 4) * 110; const size = 100; + ctx.save(); ctx.strokeStyle = '#E3B341'; ctx.lineWidth = 2; ctx.strokeRect(x, y, size, size); + ctx.drawImage(vImg, x, y, size, size); + ctx.fillStyle = 'rgba(0,0,0,0.8)'; ctx.fillRect(x, y + size - 20, size, 20); + ctx.fillStyle = 'white'; ctx.font = 'bold 12px sans-serif'; ctx.fillText(viewer.name, x + 5, y + size - 5); + ctx.restore(); + } + }); + + if (featuredViewer) { + const fImg = getCachedImage(featuredViewer.img); + if (fImg?.complete) { + const x = canvas.width - 320; const y = 100; const w = 300; const h = 300; + ctx.save(); ctx.strokeStyle = '#00F2FF'; ctx.lineWidth = 5; ctx.strokeRect(x, y, w, h); + ctx.drawImage(fImg, x, y, w, h); + ctx.fillStyle = 'rgba(0,0,0,0.8)'; ctx.fillRect(x, y + h - 40, w, 40); + ctx.fillStyle = '#00F2FF'; ctx.font = 'bold 18px sans-serif'; ctx.fillText(`DUETO COM: ${featuredViewer.name}`, x + 10, y + h - 12); + ctx.restore(); + } + } + + if (isKaraokeActive && currentSong) { + ctx.save(); + ctx.fillStyle = 'rgba(0,0,0,0.85)'; + ctx.fillRect(canvas.width/2 - 500, canvas.height - 250, 1000, 180); + ctx.strokeStyle = '#E3B341'; ctx.lineWidth = 4; + ctx.strokeRect(canvas.width/2 - 500, canvas.height - 250, 1000, 180); + ctx.fillStyle = '#E3B341'; ctx.font = 'bold 20px monospace'; ctx.textAlign = 'center'; + ctx.fillText(`🎤 ${currentSong.title.toUpperCase()} - ${currentSong.artist.toUpperCase()} (${currentSong.genre})`, canvas.width/2, canvas.height - 210); + ctx.fillStyle = 'white'; ctx.font = 'bold 48px sans-serif'; + ctx.fillText(currentLyrics || "SOLTA O SOM!", canvas.width/2, canvas.height - 140); + ctx.fillStyle = 'rgba(255,255,255,0.1)'; ctx.fillRect(canvas.width/2 - 400, canvas.height - 100, 800, 10); + ctx.fillStyle = '#E3B341'; ctx.fillRect(canvas.width/2 - 400, canvas.height - 100, (playbackProgress / (karaokeAudioRef.current?.duration || 1)) * 800, 10); + ctx.restore(); + } + + ctx.fillStyle = 'rgba(255,0,0,0.8)'; ctx.font = 'bold 20px monospace'; + ctx.fillText(`🔴 AO VIVO: ${audienceCount.toLocaleString()} PESSOAS`, 30, 50); + } + } + animationFrame = requestAnimationFrame(render); + }; + render(); + return () => cancelAnimationFrame(animationFrame); + }, [isCameraActive, isSimActive, activeViewers, featuredViewer, isKaraokeActive, currentLyrics, currentSong, getCachedImage, audienceCount, playbackProgress]); + useEffect(() => { let interval: any; if (isSimActive) { + if (audioCtxRef.current && audioDestRef.current && !crowdAudioRef.current) { + const audio = new Audio(CROWD_SOUND_URL); + audio.loop = true; audio.volume = 0.2; + const source = audioCtxRef.current.createMediaElementSource(audio); + source.connect(audioDestRef.current); + source.connect(audioCtxRef.current.destination); + audio.play().catch(e => console.error(e)); + crowdAudioRef.current = audio; + } interval = setInterval(() => { - setAudienceCount(prev => { - const target = 1000000; - return prev < target ? Math.min(prev + Math.floor(Math.random() * 15000) + 5000, target) : target; - }); - - // Rotate random "Real People" viewers - if (Math.random() > 0.7) { - const randomAvatar = REAL_PEOPLE_AVATARS[Math.floor(Math.random() * REAL_PEOPLE_AVATARS.length)]; - setActiveViewers(prev => [...prev.slice(-5), { id: Date.now(), img: randomAvatar }]); + setAudienceCount(prev => Math.min(prev + Math.floor(Math.random() * 5000) + 1000, 1500000)); + if (Math.random() > 0.8) { + const r = REAL_PEOPLE_AVATARS[Math.floor(Math.random() * REAL_PEOPLE_AVATARS.length)]; + setActiveViewers(prev => [...prev.slice(-11), { id: Date.now(), ...r }]); } - - if (activeCelebrities.length > 0 && Math.random() > 0.8) { - const randomCelebId = activeCelebrities[Math.floor(Math.random() * activeCelebrities.length)]; - const celeb = CELEBRITIES.find(c => c.id === randomCelebId); - if (celeb) { - setChatMessages(prev => [{ - id: Date.now(), - name: celeb.name, - text: celeb.reaction, - time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) - }, ...prev].slice(0, 8)); - } - } - }, 1000); + }, 2000); } else { - setAudienceCount(0); - setChatMessages([]); - setActiveViewers([]); + setAudienceCount(0); setActiveViewers([]); + if (crowdAudioRef.current) { crowdAudioRef.current.pause(); crowdAudioRef.current = null; } } return () => clearInterval(interval); - }, [isSimActive, activeCelebrities]); - - const handleAskCelebrities = async () => { - if (!userQuery.trim() || activeCelebrities.length === 0) return; - setIsAsking(true); - const selectedNames = activeCelebrities.map(id => CELEBRITIES.find(c => c.id === id)?.name).join(', '); - const prompt = `Simulate a live reaction from: ${selectedNames}. The user is observing ${selectedTarget?.name || 'deep space'}. User asks: "${userQuery}". Stay in character. Short response.`; - - try { - const resultAction = await dispatch(askGpt(prompt)); - if (askGpt.fulfilled.match(resultAction)) { - const response = resultAction.payload.data; - const newMessage = { - id: Date.now(), - name: CELEBRITIES.find(c => c.id === activeCelebrities[0])?.name || 'Celebrity', - text: response, - isAi: true - }; - setChatMessages(prev => [newMessage, ...prev].slice(0, 10)); - setLastResponse(newMessage); - } - } catch (err) { console.error(err); } finally { setIsAsking(false); setUserQuery(''); } - }; - - const handleZoom = (direction: 'in' | 'out') => { - setZoom(prev => direction === 'in' ? Math.min(prev * 2.5, 1e14) : Math.max(prev / 2.5, 1)); - }; - - const selectTarget = (target: any) => { - setSelectedTarget(target); - setIsFocusing(true); - let currentZoom = zoom; - const targetZoom = 1000000; - const interval = setInterval(() => { - currentZoom *= 1.5; - if (currentZoom >= targetZoom) { - setZoom(targetZoom); - setIsFocusing(false); - clearInterval(interval); - } else { - setZoom(currentZoom); - } - }, 100); - }; - - const startRecording = () => { - if (!videoRef.current?.srcObject) return; - const stream = videoRef.current.srcObject as MediaStream; - const recorder = new MediaRecorder(stream, { mimeType: 'video/webm' }); - const chunks: Blob[] = []; - recorder.ondataavailable = (e) => chunks.push(e.data); - recorder.onstop = () => setVideoUrl(URL.createObjectURL(new Blob(chunks, { type: 'video/webm' }))); - recorder.start(); - setIsRecording(true); - setMediaRecorder(recorder); - }; - - const stopRecording = () => { mediaRecorder?.stop(); setIsRecording(false); }; - - const formatZoom = (z: number) => { - if (z >= 1e12) return `${(z / 1e12).toFixed(1)}T`; - if (z >= 1e9) return `${(z / 1e9).toFixed(1)}B`; - if (z >= 1e6) return `${(z / 1e6).toFixed(1)}M`; - return `${z.toFixed(0)}x`; - }; + }, [isSimActive]); return ( -
- JWST | Global Live Simulation +
+ KARAOKE GLOBAL 10K+ | AO VIVO - {/* Main Viewport */} -
+
{!isCameraActive && ( -
-
- +
+
+
-

System Standby

- +

+ Mega Karaoke 10.000+ +
BRASIL & MUNDO +

+
)} -