6
This commit is contained in:
parent
93c3ce29c7
commit
0f68650e37
@ -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<HTMLVideoElement>(null);
|
||||
const [isCameraActive, setIsCameraActive] = useState(false);
|
||||
const [mode, setMode] = useState<'normal' | 'ir' | 'deep'>('normal');
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [selectedTarget, setSelectedTarget] = useState<any>(null);
|
||||
const [isFocusing, setIsFocusing] = useState(false);
|
||||
const [isSharpnessMax, setIsSharpnessMax] = useState(false);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const crowdAudioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const karaokeAudioRef = useRef<HTMLAudioElement | null>(null);
|
||||
|
||||
const audioCtxRef = useRef<AudioContext | null>(null);
|
||||
const audioDestRef = useRef<MediaStreamAudioDestinationNode | null>(null);
|
||||
const micNodeRef = useRef<MediaStreamAudioSourceNode | null>(null);
|
||||
const karaokeNodeRef = useRef<MediaStreamAudioSourceNode | null>(null);
|
||||
const reverbNodeRef = useRef<ConvolverNode | null>(null);
|
||||
const dryGainRef = useRef<GainNode | null>(null);
|
||||
const wetGainRef = useRef<GainNode | null>(null);
|
||||
const monitorGainRef = useRef<GainNode | null>(null);
|
||||
|
||||
const [isCameraActive, setIsCameraActive] = useState(false);
|
||||
const [isKaraokeActive, setIsKaraokeActive] = useState(false);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [currentSong, setCurrentSong] = useState<any>(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<string[]>([]);
|
||||
const [chatMessages, setChatMessages] = useState<any[]>([]);
|
||||
const [showSimPanel, setShowSimPanel] = useState(false);
|
||||
const [activeViewers, setActiveViewers] = useState<any[]>([]);
|
||||
const [featuredViewer, setFeaturedViewer] = useState<any>(null);
|
||||
|
||||
// Interaction State
|
||||
const [userQuery, setUserQuery] = useState('');
|
||||
const [isAsking, setIsAsking] = useState(false);
|
||||
const [lastResponse, setLastResponse] = useState<any>(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<MediaRecorder | null>(null);
|
||||
const [videoUrl, setVideoUrl] = useState<string | null>(null);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const imageCache = useRef<Map<string, HTMLImageElement>>(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 (
|
||||
<div className="relative h-screen w-full bg-black overflow-hidden flex flex-col font-mono text-[#00F2FF]">
|
||||
<Head><title>JWST | Global Live Simulation</title></Head>
|
||||
<div className="relative h-screen w-full bg-[#1a1a1a] overflow-hidden flex flex-col font-sans">
|
||||
<Head><title>KARAOKE GLOBAL 10K+ | AO VIVO</title></Head>
|
||||
|
||||
{/* Main Viewport */}
|
||||
<div className="absolute inset-0 z-0 bg-gray-950 overflow-hidden">
|
||||
<div className="absolute inset-0 z-0 bg-black overflow-hidden">
|
||||
{!isCameraActive && (
|
||||
<div className="flex flex-col items-center justify-center h-full space-y-6 z-10 relative">
|
||||
<div className="w-32 h-32 border-2 border-[#E3B341] rounded-full flex items-center justify-center animate-pulse">
|
||||
<BaseIcon path={mdiTelescope} size={64} className="text-[#E3B341]" />
|
||||
<div className="flex flex-col items-center justify-center h-full space-y-8 z-10 relative">
|
||||
<div className="w-40 h-40 border-4 border-[#E3B341] rounded-full flex items-center justify-center animate-bounce shadow-[0_0_50px_rgba(227,179,65,0.4)]">
|
||||
<BaseIcon path={mdiMicrophone} size={80} className="text-[#E3B341]" />
|
||||
</div>
|
||||
<p className="text-[#E3B341] uppercase tracking-[0.4em] font-bold text-xl">System Standby</p>
|
||||
<button onClick={startCamera} className="bg-[#E3B341] text-black px-12 py-4 font-bold uppercase tracking-widest hover:bg-white transition-all">Deploy JWST</button>
|
||||
<h1 className="text-white text-5xl font-black tracking-tighter uppercase italic text-center">
|
||||
Mega <span className="text-[#E3B341]">Karaoke</span> 10.000+
|
||||
<br /><span className="text-xl font-light tracking-widest text-white/60">BRASIL & MUNDO</span>
|
||||
</h1>
|
||||
<button onClick={startCamera} className="bg-[#E3B341] text-black px-16 py-6 rounded-full font-black text-2xl uppercase tracking-tighter hover:scale-110 transition-all shadow-2xl">Entrar no Palco</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<video ref={videoRef} autoPlay playsInline muted className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-1000 ${isCameraActive && (zoom <= 5000 || !selectedTarget) ? 'opacity-100' : 'opacity-0'}`}
|
||||
style={{ filter: (isSharpnessMax ? 'contrast(1.4) brightness(1.1)' : 'none') + (mode === 'ir' ? ' hue-rotate(180deg)' : mode === 'deep' ? ' contrast(1.5)' : ''), transform: `scale(${1 + Math.log10(zoom)})` }}
|
||||
/>
|
||||
<canvas ref={canvasRef} width={1920} height={1080} className="hidden" />
|
||||
<video ref={videoRef} autoPlay playsInline muted className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-1000 ${isCameraActive ? 'opacity-100' : 'opacity-0'}`} />
|
||||
|
||||
{selectedTarget && (
|
||||
<div className={`absolute inset-0 w-full h-full bg-cover bg-center transition-opacity duration-1000 ${zoom > 5000 ? 'opacity-100' : 'opacity-0'}`}
|
||||
style={{ backgroundImage: `url(${selectedTarget.img})`, transform: `scale(${1 + (Math.log10(zoom) - 3) / 20})` }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 1 Million People Crowd Particles */}
|
||||
{isSimActive && audienceCount > 0 && (
|
||||
<div className="absolute inset-0 z-10 pointer-events-none overflow-hidden">
|
||||
{[...Array(60)].map((_, i) => (
|
||||
<div key={i} className="absolute bg-white/20 rounded-full blur-[4px] animate-float"
|
||||
style={{ width: Math.random() * 8 + 2, height: Math.random() * 8 + 2, left: `${Math.random() * 100}%`, top: `${Math.random() * 100}%`, animationDuration: `${Math.random() * 10 + 5}s`, animationDelay: `${Math.random() * 5}s` }}
|
||||
/>
|
||||
{isSimActive && isInterfaceVisible && (
|
||||
<div className="absolute top-24 left-6 z-40 grid grid-cols-4 gap-4 pointer-events-auto max-w-md animate-fade-in">
|
||||
{activeViewers.map((viewer) => (
|
||||
<button key={viewer.id} onClick={() => setFeaturedViewer(viewer)} className={`group relative w-24 h-24 rounded-2xl border-2 overflow-hidden transition-all hover:scale-110 ${featuredViewer?.id === viewer.id ? 'border-[#00F2FF] ring-4 ring-[#00F2FF]/20' : 'border-white/20 hover:border-[#E3B341]'}`}>
|
||||
<Image src={viewer.img} alt={viewer.name} width={96} height={96} crossOrigin="anonymous" className="w-full h-full object-cover grayscale group-hover:grayscale-0 transition-all" />
|
||||
<div className="absolute bottom-0 w-full p-1 bg-black/60 text-[8px] font-bold text-white text-center">{viewer.name}</div>
|
||||
</button>
|
||||
))}
|
||||
<div className="absolute bottom-0 w-full h-1/3 bg-gradient-to-t from-[#E3B341]/20 to-transparent"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* REAL PEOPLE VIEWER GRID */}
|
||||
{isSimActive && activeViewers.length > 0 && (
|
||||
<div className="absolute top-4 left-4 z-40 flex flex-col space-y-2 pointer-events-none">
|
||||
<div className="flex items-center space-x-2 bg-black/60 backdrop-blur-md px-3 py-1.5 rounded-full border border-[#00F2FF]/30">
|
||||
<BaseIcon path={mdiWebcam} size={14} className="text-[#00F2FF]" />
|
||||
<span className="text-[10px] font-bold uppercase tracking-tighter">Live Webcams</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{activeViewers.map((viewer) => (
|
||||
<div key={viewer.id} className="w-14 h-14 rounded-lg border border-white/20 overflow-hidden shadow-lg animate-pulse bg-gray-800 relative">
|
||||
<img src={viewer.img} className="w-full h-full object-cover grayscale opacity-80" />
|
||||
<div className="absolute inset-0 bg-blue-500/10"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Celebrity Avatars */}
|
||||
{isSimActive && activeCelebrities.length > 0 && (
|
||||
<div className="absolute bottom-40 left-1/2 -translate-x-1/2 flex -space-x-4 z-30 pointer-events-none">
|
||||
{activeCelebrities.map((id) => {
|
||||
const celeb = CELEBRITIES.find(c => c.id === id);
|
||||
const isSpeaking = lastResponse?.name === celeb?.name;
|
||||
return (
|
||||
<div key={id} className={`relative transition-all duration-500 transform ${isSpeaking ? 'scale-125 z-50 -translate-y-4' : 'scale-100 opacity-60'}`}>
|
||||
<div className={`w-16 h-16 rounded-full border-2 overflow-hidden shadow-2xl ${isSpeaking ? 'border-[#E3B341] ring-4 ring-[#E3B341]/30' : 'border-white/20'}`}>
|
||||
<img src={celeb?.img} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
{featuredViewer && isInterfaceVisible && (
|
||||
<div className="absolute top-24 right-6 z-40 w-72 bg-black/90 border-4 border-[#00F2FF] rounded-3xl overflow-hidden shadow-2xl pointer-events-auto animate-fade-in">
|
||||
<div className="relative">
|
||||
<Image src={featuredViewer.img} alt={`Featured: ${featuredViewer.name}`} width={288} height={288} crossOrigin="anonymous" className="w-full h-72 object-cover" />
|
||||
<div className="absolute top-4 left-4 flex items-center space-x-2 bg-red-600 px-3 py-1 rounded-full text-[10px] font-black text-white">
|
||||
<div className="w-2 h-2 bg-white rounded-full animate-pulse"></div>
|
||||
<span>DUETO AO VIVO</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button onClick={() => setFeaturedViewer(null)} className="absolute top-4 right-4 bg-black/60 p-1 rounded-full text-white"><BaseIcon path={mdiClose} size={20} /></button>
|
||||
</div>
|
||||
<div className="p-4 bg-white/5 border-t border-white/10 text-center">
|
||||
<div className="text-[#00F2FF] text-[10px] font-black uppercase">Participante Selecionado</div>
|
||||
<div className="text-white text-lg font-bold">{featuredViewer.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Global Connectivity Map */}
|
||||
{isSimActive && (
|
||||
<div className="absolute top-4 right-4 z-40 bg-black/40 backdrop-blur-md p-3 rounded-xl border border-white/10 pointer-events-none">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<BaseIcon path={mdiEarth} size={16} className="text-[#00F2FF] animate-spin-slow" />
|
||||
<span className="text-[9px] font-bold uppercase">Nodes</span>
|
||||
</div>
|
||||
<div className="w-32 h-16 bg-[#00F2FF]/5 relative rounded overflow-hidden">
|
||||
{[...Array(15)].map((_, i) => (
|
||||
<div key={i} className="absolute w-1 h-1 bg-[#00F2FF] rounded-full animate-ping"
|
||||
style={{ left: `${Math.random() * 100}%`, top: `${Math.random() * 100}%`, animationDelay: `${Math.random() * 3}s` }}
|
||||
/>
|
||||
))}
|
||||
{isKaraokeActive && currentSong && isInterfaceVisible && (
|
||||
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 z-50 w-full max-w-4xl px-8 pointer-events-none animate-slide-up">
|
||||
<div className="bg-black/90 border-t-8 border-[#E3B341] p-10 rounded-3xl backdrop-blur-3xl shadow-[0_-20px_100px_rgba(0,0,0,0.8)] text-center pointer-events-auto">
|
||||
<div className="flex items-center justify-center space-x-3 mb-6">
|
||||
<BaseIcon path={mdiMusicNote} size={24} className="text-[#E3B341]" />
|
||||
<span className="text-[#E3B341] text-lg font-black uppercase tracking-widest">{currentSong.title} - {currentSong.artist}</span>
|
||||
</div>
|
||||
<div className="text-white text-5xl font-black leading-tight drop-shadow-[0_4px_4px_rgba(0,0,0,1)] transition-all duration-300">
|
||||
{currentLyrics || "VAI COMEÇAR..."}
|
||||
</div>
|
||||
<div className="mt-8 h-2 bg-white/10 rounded-full overflow-hidden relative">
|
||||
<div className="h-full bg-gradient-to-r from-[#E3B341] to-[#ffda85] shadow-[0_0_15px_rgba(227,179,65,0.6)]" style={{ width: `${(playbackProgress / (karaokeAudioRef.current?.duration || 1)) * 100}%` }}></div>
|
||||
</div>
|
||||
<div className="mt-8 flex justify-center space-x-6 items-center">
|
||||
<button onClick={togglePlayback} className="bg-[#E3B341] text-black w-20 h-20 flex items-center justify-center rounded-full hover:scale-110 transition-all shadow-[0_0_30px_rgba(227,179,65,0.4)]">
|
||||
<BaseIcon path={isPaused ? mdiPlay : mdiPause} size={48} />
|
||||
</button>
|
||||
<button onClick={() => setIsInterfaceVisible(false)} className="bg-white/10 text-white p-4 rounded-full hover:bg-white/20 transition-all">
|
||||
<BaseIcon path={mdiEyeOff} size={24} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* UI Overlay */}
|
||||
{isCameraActive && (
|
||||
<div className="absolute inset-0 z-20 p-4 flex flex-col justify-between pointer-events-none">
|
||||
<div className="flex justify-between items-start pointer-events-auto">
|
||||
<div className="bg-black/90 border-l-4 border-[#E3B341] p-4 backdrop-blur-md min-w-[260px] shadow-2xl rounded-r-xl">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-[9px] text-gray-400 uppercase tracking-widest">LIVE BROADCAST</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-2 h-2 rounded-full bg-red-600 animate-pulse"></div>
|
||||
<span className="text-[10px] font-bold">LIVE</span>
|
||||
</div>
|
||||
</div>
|
||||
{isSimActive && (
|
||||
<div className="flex items-center justify-between bg-white/5 p-2 rounded-lg border border-white/10">
|
||||
<div className="flex items-center space-x-2">
|
||||
<BaseIcon path={mdiAccountGroup} size={16} className="text-[#00F2FF]" />
|
||||
<span className="text-white text-base font-black tabular-nums">{audienceCount.toLocaleString()}</span>
|
||||
</div>
|
||||
<span className="text-[9px] text-gray-400 uppercase font-bold">Viewers</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-3">
|
||||
<button onClick={() => setShowSimPanel(!showSimPanel)} className={`p-4 bg-black/90 border-2 transition-all rounded-2xl ${showSimPanel ? 'border-[#E3B341] text-[#E3B341]' : 'border-white/20 text-white'}`}><BaseIcon path={mdiStarCircle} size={28} /></button>
|
||||
<button onClick={isRecording ? stopRecording : startRecording} className={`p-4 bg-black/90 border-2 rounded-2xl ${isRecording ? 'border-red-600 text-red-600' : 'border-white/20 text-white'}`}><BaseIcon path={isRecording ? mdiStop : mdiRecord} size={28} /></button>
|
||||
<button onClick={stopCamera} className="p-4 bg-black/90 border-2 border-red-500/40 rounded-2xl hover:bg-red-500/60"><BaseIcon path={mdiClose} size={28} className="text-white" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showSimPanel && (
|
||||
<div className="absolute top-28 right-4 w-80 bg-black/95 border-2 border-[#E3B341] p-6 backdrop-blur-3xl pointer-events-auto z-50 rounded-3xl shadow-2xl">
|
||||
<h3 className="text-[#E3B341] font-black uppercase text-xs tracking-widest mb-6">Simulation Hub</h3>
|
||||
<div className="space-y-6">
|
||||
<button onClick={() => setIsSimActive(!isSimActive)} className={`w-full py-3 text-xs uppercase font-black rounded-xl transition-all ${isSimActive ? 'bg-red-600 text-white' : 'bg-[#00F2FF] text-black'}`}>
|
||||
{isSimActive ? 'Shutdown Crowd' : 'Simulate 1M People'}
|
||||
</button>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{CELEBRITIES.map(celeb => (
|
||||
<button key={celeb.id} onClick={() => {
|
||||
setActiveCelebrities(prev => prev.includes(celeb.id) ? prev.filter(c => c !== celeb.id) : [...prev, celeb.id]);
|
||||
}} className={`flex items-center space-x-2 p-2 rounded-xl border ${activeCelebrities.includes(celeb.id) ? 'bg-[#E3B341] text-black border-[#E3B341]' : 'bg-white/5 border-white/10 text-white opacity-60'}`}>
|
||||
<img src={celeb.img} className="w-6 h-6 rounded-full object-cover" />
|
||||
<span className="text-[10px] font-bold truncate">{celeb.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{isSimActive && activeCelebrities.length > 0 && (
|
||||
<div className="relative">
|
||||
<input type="text" value={userQuery} onChange={(e) => setUserQuery(e.target.value)} placeholder="Message the icons..." className="w-full bg-white/5 border border-white/20 rounded-xl p-3 text-xs text-white focus:outline-none focus:border-[#00F2FF]" onKeyDown={(e) => e.key === 'Enter' && handleAskCelebrities()} />
|
||||
<button onClick={handleAskCelebrities} className="absolute right-3 top-1/2 -translate-y-1/2 text-[#00F2FF]"><BaseIcon path={isAsking ? mdiChatProcessingOutline : mdiSend} size={20} className={isAsking ? 'animate-spin' : ''} /></button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
{!isInterfaceVisible && (
|
||||
<button onClick={() => setIsInterfaceVisible(true)} className="absolute bottom-10 right-10 z-[60] bg-[#E3B341] text-black p-6 rounded-full shadow-2xl hover:scale-110 transition-all animate-bounce">
|
||||
<BaseIcon path={mdiEye} size={40} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isSimActive && chatMessages.length > 0 && (
|
||||
<div className="absolute left-4 bottom-20 w-80 flex flex-col space-y-2 pointer-events-none">
|
||||
{chatMessages.map(msg => (
|
||||
<div key={msg.id} className={`bg-black/80 backdrop-blur-xl p-3 border-l-2 rounded-r-xl animate-slide-in shadow-xl ${msg.isAi ? 'border-[#00F2FF]' : 'border-[#E3B341]'}`}>
|
||||
<span className={`${msg.isAi ? 'text-[#00F2FF]' : 'text-[#E3B341]'} font-black text-[9px] uppercase`}>{msg.name}</span>
|
||||
<p className="text-white text-[11px] leading-tight">"{msg.text}"</p>
|
||||
{isInterfaceVisible && (
|
||||
<div className="absolute inset-0 z-30 p-8 flex flex-col justify-between pointer-events-none animate-fade-in">
|
||||
<div className="flex justify-between items-start pointer-events-auto">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="bg-black/80 p-6 rounded-[2rem] border border-white/10 backdrop-blur-xl shadow-2xl flex items-center space-x-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<button onClick={() => setIsMicOn(!isMicOn)} className={`p-4 rounded-full transition-all ${isMicOn ? 'bg-green-600 shadow-[0_0_20px_rgba(34,197,94,0.4)]' : 'bg-red-600'}`}>
|
||||
<BaseIcon path={isMicOn ? mdiMicrophone : mdiMicrophoneOff} size={32} className="text-white" />
|
||||
</button>
|
||||
<span className="text-[10px] font-black text-white mt-2">MICROFONE</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<button onClick={() => setIsMonitorOn(!isMonitorOn)} className={`p-4 rounded-full transition-all ${isMonitorOn ? 'bg-[#00F2FF] text-black shadow-[0_0_20px_rgba(0,242,255,0.4)]' : 'bg-white/10 text-white'}`}>
|
||||
<BaseIcon path={mdiHeadphones} size={32} />
|
||||
</button>
|
||||
<span className="text-[10px] font-black text-white mt-2">RETORNO</span>
|
||||
</div>
|
||||
<div className="w-px h-12 bg-white/20"></div>
|
||||
<div className="flex flex-col space-y-2 min-w-[150px]">
|
||||
<div className="flex justify-between text-[10px] font-black text-[#E3B341]"><span>EFEITO (REVERB)</span><span>{(reverbLevel * 100).toFixed(0)}%</span></div>
|
||||
<input type="range" min="0" max="1" step="0.1" value={reverbLevel} onChange={(e) => setReverbLevel(parseFloat(e.target.value))} className="w-full h-1 bg-white/10 accent-[#E3B341] rounded-lg appearance-none cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-600 text-white px-6 py-3 rounded-full font-black text-sm shadow-2xl flex items-center space-x-3 w-fit">
|
||||
<div className="w-3 h-3 bg-white rounded-full animate-pulse"></div>
|
||||
<span>{audienceCount.toLocaleString()} PESSOAS AO VIVO</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<button onClick={() => setIsInterfaceVisible(false)} className="p-6 bg-black/80 border-2 border-white/20 rounded-full text-white hover:border-[#E3B341] transition-all shadow-2xl"><BaseIcon path={mdiEyeOff} size={40} /></button>
|
||||
<button onClick={() => setShowPlaylist(!showPlaylist)} className={`p-6 bg-black/80 border-2 rounded-full transition-all shadow-2xl ${showPlaylist ? 'border-[#E3B341] text-[#E3B341]' : 'border-white/20 text-white hover:border-[#E3B341]'}`}><BaseIcon path={mdiMusicNote} size={40} /></button>
|
||||
<button onClick={isRecording ? () => { mediaRecorder?.stop(); setIsRecording(false); } : () => {
|
||||
if (!canvasRef.current || !audioDestRef.current) return;
|
||||
const combinedStream = new MediaStream([
|
||||
...canvasRef.current.captureStream(60).getVideoTracks(),
|
||||
...audioDestRef.current.stream.getAudioTracks()
|
||||
]);
|
||||
const recorder = new MediaRecorder(combinedStream, { mimeType: 'video/webm;codecs=vp9,opus', bitsPerSecond: 10000000 });
|
||||
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);
|
||||
}} className={`p-6 bg-black/80 border-2 rounded-full transition-all shadow-2xl ${isRecording ? 'border-red-600 text-red-600 scale-110' : 'border-white/20 text-white hover:border-red-600'}`}><BaseIcon path={isRecording ? mdiStop : mdiRecord} size={40} /></button>
|
||||
{videoUrl && !isRecording && <button onClick={() => { const a = document.createElement('a'); a.href = videoUrl; a.download = `meu-show-global.webm`; a.click(); }} className="p-6 bg-green-600 text-white rounded-full shadow-2xl animate-bounce"><BaseIcon path={mdiDownload} size={40} /></button>}
|
||||
<button onClick={() => window.location.reload()} className="p-6 bg-black/80 border-2 border-white/20 rounded-full text-white hover:bg-red-600 transition-all"><BaseIcon path={mdiClose} size={40} /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showPlaylist && (
|
||||
<div className="absolute top-8 right-8 w-[32rem] bg-black/95 border-4 border-[#E3B341] p-8 rounded-[3rem] shadow-[0_0_100px_rgba(0,0,0,0.9)] pointer-events-auto z-50 animate-slide-up flex flex-col h-[90vh]">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-[#E3B341] font-black text-2xl italic uppercase tracking-tighter">Estúdio 10.000+</h3>
|
||||
<p className="text-[10px] text-white/40 font-bold uppercase tracking-widest">Seu palco, sua música, seu mundo</p>
|
||||
</div>
|
||||
<button onClick={() => setShowPlaylist(false)} className="bg-white/5 p-2 rounded-full text-white/40 hover:text-white transition-all"><BaseIcon path={mdiClose} size={28} /></button>
|
||||
</div>
|
||||
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute inset-y-0 left-5 flex items-center pointer-events-none">
|
||||
<BaseIcon path={mdiMagnify} size={24} className="text-white/30" />
|
||||
</div>
|
||||
<input type="text" placeholder="BUSCAR ENTRE 10.000 MÚSICAS..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="w-full bg-white/5 border-2 border-white/10 rounded-2xl py-5 pl-14 pr-6 text-white font-bold focus:outline-none focus:border-[#E3B341] transition-all placeholder:text-white/20 uppercase text-sm" />
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2 overflow-x-auto pb-4 mb-4 custom-scrollbar no-scrollbar">
|
||||
{GENRES.map(genre => (
|
||||
<button key={genre.name} onClick={() => setSelectedGenre(genre.name)} className={`flex items-center space-x-2 px-6 py-3 rounded-full whitespace-nowrap font-black text-[10px] uppercase transition-all border-2 ${selectedGenre === genre.name ? 'bg-[#E3B341] border-[#E3B341] text-black shadow-[0_0_20px_rgba(227,179,65,0.3)]' : 'bg-white/5 border-white/10 text-white/60 hover:border-white/30'}`}>
|
||||
<BaseIcon path={genre.icon} size={16} />
|
||||
<span>{genre.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex-grow overflow-y-auto pr-2 space-y-3 custom-scrollbar">
|
||||
{filteredSongs.length > 0 ? filteredSongs.map(song => (
|
||||
<button key={song.id} onClick={() => selectSong(song)} className={`w-full flex items-center justify-between p-4 rounded-2xl border-2 transition-all group ${currentSong?.id === song.id ? 'bg-[#E3B341] border-[#E3B341] text-black' : 'bg-white/5 border-white/10 text-white hover:border-[#E3B341] hover:bg-white/10'}`}>
|
||||
<div className="flex flex-col items-start overflow-hidden">
|
||||
<span className="font-black text-lg truncate w-full italic leading-tight">{song.title}</span>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<span className="text-[10px] font-black uppercase opacity-60 tracking-wider">{song.artist}</span>
|
||||
<span className="w-1 h-1 bg-white/30 rounded-full"></span>
|
||||
<span className="text-[9px] font-black uppercase text-[#E3B341] group-hover:text-inherit">{song.genre}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-3 rounded-full transition-all ${currentSong?.id === song.id ? 'bg-black text-[#E3B341]' : 'bg-white/10 text-white group-hover:bg-[#E3B341] group-hover:text-black'}`}>
|
||||
<BaseIcon path={currentSong?.id === song.id ? mdiPause : mdiPlay} size={24} />
|
||||
</div>
|
||||
</button>
|
||||
)) : (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-white/20">
|
||||
<BaseIcon path={mdiFilterVariant} size={64} className="mb-4" />
|
||||
<p className="font-black uppercase tracking-widest text-sm">Nenhuma música encontrada</p>
|
||||
<p className="text-[10px] font-bold mt-2">TENTE OUTROS TERMOS OU CATEGORIAS</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-8 border-2 border-dashed border-white/10 rounded-[2rem] text-center opacity-40 hover:opacity-100 transition-all cursor-pointer group">
|
||||
<BaseIcon path={mdiCreation} size={48} className="text-[#E3B341] mx-auto mb-4 group-hover:scale-125 transition-all" />
|
||||
<h4 className="text-white font-black uppercase tracking-tighter">Explorar Nuvem de 10.000 Hits</h4>
|
||||
<p className="text-[10px] text-white/60 font-bold mt-1 uppercase">Novos playbacks adicionados diariamente</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-white/10 flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[9px] font-black text-white/30 uppercase tracking-widest">Qualidade de Áudio</span>
|
||||
<span className="text-[#E3B341] text-xs font-black">ULTRA-HD 320KBPS</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<BaseIcon path={mdiVolumeHigh} size={20} className="text-white/40" />
|
||||
<div className="w-24 h-1 bg-white/10 rounded-full overflow-hidden">
|
||||
<div className="w-3/4 h-full bg-[#E3B341]"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col items-center space-y-4 mb-4">
|
||||
<div className="flex flex-wrap justify-center gap-2 pointer-events-auto bg-black/60 p-3 rounded-2xl border border-white/10">
|
||||
{PRESET_TARGETS.map(target => (
|
||||
<button key={target.id} onClick={() => selectTarget(target)} className={`px-4 py-1.5 text-[9px] font-black uppercase tracking-widest border transition-all rounded-lg ${selectedTarget?.id === target.id ? 'bg-[#E3B341] text-black border-[#E3B341]' : 'text-white border-white/20 hover:border-[#E3B341]'}`}>{target.name}</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex space-x-8 pointer-events-auto">
|
||||
<button onClick={() => handleZoom('in')} className="p-4 bg-[#00F2FF] text-black rounded-full hover:scale-110 transition-transform"><BaseIcon path={mdiMagnifyPlusOutline} size={28} /></button>
|
||||
<div className="flex items-center px-6 bg-black/80 rounded-full border border-[#00F2FF]/40">
|
||||
<span className="text-xl font-black text-white">{formatZoom(zoom)}</span>
|
||||
</div>
|
||||
<button onClick={() => handleZoom('out')} className="p-4 bg-[#00F2FF] text-black rounded-full hover:scale-110 transition-transform"><BaseIcon path={mdiMagnifyMinusOutline} size={28} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="absolute inset-0 pointer-events-none z-50 bg-scanlines opacity-[0.05]"></div>
|
||||
|
||||
<style jsx global>{`
|
||||
@keyframes slide-in { from { opacity: 0; transform: translateX(-20px); } to { opacity: 1; transform: translateX(0); } }
|
||||
@keyframes float { 0% { transform: translateY(0) translateX(0); } 50% { transform: translateY(-20px) translateX(10px); } 100% { transform: translateY(0) translateX(0); } }
|
||||
@keyframes spin-slow { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||
.animate-slide-in { animation: slide-in 0.4s ease-out forwards; }
|
||||
.animate-float { animation: float infinite ease-in-out; }
|
||||
.animate-spin-slow { animation: spin-slow 10s linear infinite; }
|
||||
.bg-scanlines { background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06)); background-size: 100% 2px, 3px 100%; }
|
||||
@keyframes slide-up { from { opacity: 0; transform: translateY(40px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
|
||||
.animate-slide-up { animation: slide-up 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
|
||||
.animate-fade-in { animation: fade-in 0.5s ease-out forwards; }
|
||||
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
|
||||
.custom-scrollbar::-webkit-scrollbar-track { background: rgba(255,255,255,0.02); border-radius: 10px; }
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb { background: #E3B341; border-radius: 10px; }
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user