Spaces:
Running
Running
Update index.html
Browse files- index.html +336 -153
index.html
CHANGED
@@ -3,42 +3,48 @@
|
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
-
<title>Video Reactivo
|
7 |
<script src="https://cdn.tailwindcss.com"></script>
|
8 |
<style>
|
9 |
/* Estilos personalizados que complementan Tailwind */
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
|
|
|
|
19 |
}
|
20 |
|
21 |
-
|
22 |
-
|
23 |
-
top: 0;
|
24 |
-
left: 0;
|
25 |
-
width: 100%;
|
26 |
-
height: 100%;
|
27 |
-
/* Usar una imagen o patrón para el efecto glitch */
|
28 |
-
background: url('https://getsamplefiles.com/download/webm/sample-2.webm'); /* Podría ser un patrón o ruido */
|
29 |
-
background-size: cover;
|
30 |
-
mix-blend-mode: screen; /* Modo de mezcla para el efecto */
|
31 |
-
opacity: 0; /* Inicialmente invisible */
|
32 |
-
transition: opacity 0.1s ease-in-out; /* Transición para la opacidad */
|
33 |
}
|
34 |
|
35 |
-
/*
|
36 |
-
.
|
37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
}
|
39 |
|
40 |
-
|
41 |
-
|
|
|
|
|
|
|
|
|
42 |
}
|
43 |
|
44 |
.visualizer {
|
@@ -96,8 +102,9 @@
|
|
96 |
|
97 |
/* Estilos responsivos con Media Queries personalizadas si es necesario */
|
98 |
@media (max-width: 640px) { /* Ejemplo de breakpoint para móviles */
|
99 |
-
.
|
100 |
-
|
|
|
101 |
}
|
102 |
|
103 |
.visualizer {
|
@@ -116,18 +123,14 @@
|
|
116 |
</head>
|
117 |
<body class="bg-gray-900 text-white min-h-screen flex flex-col items-center justify-center p-4 sm:p-6 lg:p-8 font-sans">
|
118 |
<div class="text-center mb-8">
|
119 |
-
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-extrabold mb-2">Video Reactivo
|
120 |
-
<p class="text-gray-300 text-sm sm:text-base">
|
121 |
</div>
|
122 |
|
123 |
-
<
|
124 |
-
|
125 |
-
|
126 |
-
</
|
127 |
-
<div class="glitch-effect"></div>
|
128 |
-
<div class="glitch-effect"></div>
|
129 |
-
<div class="glitch-effect"></div>
|
130 |
-
</div>
|
131 |
|
132 |
<div class="controls w-full max-w-sm sm:max-w-md lg:max-w-lg mt-8">
|
133 |
<div class="frequency-label">
|
@@ -150,20 +153,43 @@
|
|
150 |
|
151 |
<script>
|
152 |
document.addEventListener('DOMContentLoaded', () => {
|
153 |
-
const
|
154 |
-
const
|
155 |
const startButton = document.getElementById('startButton');
|
156 |
const visualizer = document.getElementById('visualizer');
|
157 |
const statusText = document.getElementById('status');
|
158 |
|
159 |
-
let audioContext = null;
|
160 |
let analyser = null;
|
161 |
let microphone = null;
|
162 |
let dataArray = null;
|
163 |
let isPlaying = false;
|
164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
165 |
// Crear barras del visualizador dinámicamente
|
166 |
-
const numberOfBars = 64;
|
167 |
for (let i = 0; i < numberOfBars; i++) {
|
168 |
const bar = document.createElement('div');
|
169 |
bar.className = 'bar';
|
@@ -171,11 +197,10 @@
|
|
171 |
}
|
172 |
const bars = document.querySelectorAll('.bar');
|
173 |
|
174 |
-
//
|
175 |
-
|
176 |
-
console.error('Error al intentar reproducir el video automáticamente:', error);
|
177 |
-
//
|
178 |
-
// No cambiamos el statusText aquí para no sobrescribir el mensaje de "Esperando permiso..."
|
179 |
});
|
180 |
|
181 |
|
@@ -186,46 +211,41 @@
|
|
186 |
statusText.textContent = 'Solicitando acceso al micrófono...';
|
187 |
await initAudio();
|
188 |
startButton.textContent = 'Micrófono Activado';
|
189 |
-
// Cambiar colores del botón usando clases de Tailwind
|
190 |
startButton.classList.remove('bg-blue-600', 'hover:bg-blue-700', 'pulse');
|
191 |
startButton.classList.add('bg-green-600', 'hover:bg-green-700');
|
192 |
-
statusText.textContent = 'Micrófono activado.
|
193 |
isPlaying = true;
|
194 |
|
195 |
-
//
|
196 |
-
|
197 |
-
console.error('Error al intentar reproducir el video tras activar micrófono:', error);
|
198 |
-
//
|
|
|
199 |
});
|
200 |
|
|
|
|
|
|
|
201 |
}
|
202 |
} catch (err) {
|
203 |
-
|
204 |
-
console.error('Error completo al inicializar el audio:', err); // Loguear el objeto de error completo
|
205 |
|
206 |
let userMessage = 'Error desconocido al acceder al micrófono.';
|
207 |
-
|
208 |
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
|
209 |
userMessage = 'Permiso de micrófono denegado. Por favor, permite el acceso al micrófono en la configuración de tu navegador.';
|
210 |
} else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
|
211 |
userMessage = 'No se encontró ningún micrófono. Asegúrate de que uno esté conectado y configurado correctamente.';
|
212 |
} else if (err.name === 'NotReadableError' || err.name === 'TrackStartError') {
|
213 |
userMessage = 'El micrófono está en uso por otra aplicación o hay un problema de hardware.';
|
214 |
-
} else if (err.name === 'OverconstrainedError' || err.name === 'ConstraintNotSatisfiedError') {
|
215 |
-
userMessage = 'Error de configuración del micrófono. Intenta con la configuración predeterminada.';
|
216 |
} else if (err.name === 'SecurityError') {
|
217 |
userMessage = 'Se requiere una conexión segura (HTTPS) para acceder al micrófono.';
|
218 |
} else if (err.message) {
|
219 |
-
// Si hay un mensaje de error estándar, lo usamos
|
220 |
userMessage = `Error al acceder al micrófono: ${err.message}.`;
|
221 |
} else {
|
222 |
-
// Último recurso: intentar stringify el error si no tiene nombre o mensaje
|
223 |
userMessage = `Error al acceder al micrófono: ${typeof err === 'object' ? JSON.stringify(err) : err}.`;
|
224 |
}
|
225 |
|
226 |
-
statusText.textContent = userMessage;
|
227 |
-
// --- Fin de la sección de manejo de errores mejorado ---
|
228 |
-
|
229 |
startButton.classList.remove('bg-green-600', 'hover:bg-green-700');
|
230 |
startButton.classList.add('bg-red-600', 'hover:bg-red-700');
|
231 |
startButton.textContent = 'Error de Micrófono';
|
@@ -234,152 +254,302 @@
|
|
234 |
|
235 |
// Función para inicializar el AudioContext y conectar el micrófono
|
236 |
async function initAudio() {
|
237 |
-
// Crear AudioContext si no existe
|
238 |
if (!audioContext) {
|
239 |
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
240 |
}
|
241 |
|
242 |
-
// Crear analizador
|
243 |
analyser = audioContext.createAnalyser();
|
244 |
-
analyser.fftSize = 128; // Tamaño del FFT
|
|
|
|
|
|
|
|
|
|
|
245 |
|
246 |
-
// Solicitar acceso al micrófono
|
247 |
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
248 |
microphone = audioContext.createMediaStreamSource(stream);
|
249 |
-
microphone.connect(analyser);
|
250 |
|
251 |
-
|
252 |
-
dataArray = new Uint8Array(analyser.frequencyBinCount); // analyser.frequencyBinCount es la mitad del fftSize
|
253 |
|
254 |
// Iniciar el procesamiento de audio
|
255 |
processAudio();
|
256 |
}
|
257 |
|
258 |
-
// Bucle principal para procesar el audio
|
259 |
function processAudio() {
|
260 |
if (!analyser || !isPlaying) {
|
261 |
-
|
262 |
-
requestAnimationFrame(processAudio); // Seguir intentando en el siguiente frame
|
263 |
return;
|
264 |
}
|
265 |
|
266 |
-
// Obtener los datos de frecuencia
|
267 |
analyser.getByteFrequencyData(dataArray);
|
268 |
|
269 |
-
// Dividir las frecuencias en bandas
|
270 |
-
|
271 |
-
const
|
272 |
-
const
|
273 |
-
const highFreq = getAverageVolume(dataArray, Math.floor(dataArray.length * 0.4), dataArray.length - 1); // Agudos (aprox. 1000+ Hz)
|
274 |
|
275 |
-
// Aplicar efectos
|
276 |
-
|
277 |
-
applyMidFrequencyEffect(midFreq);
|
278 |
-
applyHighFrequencyEffect(highFreq);
|
279 |
|
280 |
// Actualizar el visualizador de barras
|
281 |
updateVisualizer(dataArray);
|
282 |
|
283 |
-
// Continuar el bucle en el siguiente frame de animación
|
284 |
requestAnimationFrame(processAudio);
|
285 |
}
|
286 |
|
287 |
// Calcula el volumen promedio en un rango de datos
|
288 |
function getAverageVolume(data, start, end) {
|
289 |
let sum = 0;
|
|
|
290 |
for (let i = start; i <= end; i++) {
|
291 |
-
|
|
|
|
|
|
|
292 |
}
|
293 |
-
return sum /
|
294 |
}
|
295 |
|
296 |
-
//
|
297 |
-
function
|
298 |
-
//
|
299 |
-
const
|
300 |
-
|
301 |
-
const
|
302 |
-
const
|
303 |
-
video.style.filter = `hue-rotate(${hue}deg) brightness(${brightness})`;
|
304 |
-
}
|
305 |
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
// Los medios controlan la rotación
|
310 |
-
const threshold = 10; // Umbral para detectar un cambio significativo
|
311 |
-
const maxRotation = 10; // Rotación máxima en grados
|
312 |
-
|
313 |
-
const diff = value - lastMidValue;
|
314 |
-
lastMidValue = value;
|
315 |
-
|
316 |
-
let rotation = 0;
|
317 |
-
if (diff > threshold) {
|
318 |
-
rotation = maxRotation; // Rotación positiva si sube
|
319 |
-
} else if (diff < -threshold) {
|
320 |
-
rotation = -maxRotation; // Rotación negativa si baja
|
321 |
-
}
|
322 |
|
323 |
-
|
324 |
-
|
325 |
-
}
|
326 |
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
const
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
//
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
351 |
}
|
352 |
|
|
|
353 |
// Actualiza la altura y color de las barras del visualizador
|
354 |
function updateVisualizer(data) {
|
355 |
-
const barWidth = visualizer.offsetWidth / numberOfBars;
|
356 |
bars.forEach((bar, i) => {
|
357 |
-
// Mapear los datos de frecuencia a las barras
|
358 |
const dataIndex = Math.floor(i * (data.length / numberOfBars));
|
359 |
-
const value = data[dataIndex] || 0;
|
360 |
-
|
361 |
-
// Calcular la altura de la barra (escalada)
|
362 |
const maxHeight = visualizer.offsetHeight;
|
363 |
-
const height = `${(value / 255) * maxHeight}px`;
|
364 |
|
365 |
bar.style.height = height;
|
366 |
-
bar.style.width = `${barWidth - 1}px`;
|
367 |
|
368 |
-
|
369 |
-
if (i < numberOfBars / 3) {
|
370 |
-
// Graves - azul
|
371 |
bar.style.background = `linear-gradient(to top, #60a5fa, #3b82f6)`;
|
372 |
-
} else if (i <
|
373 |
-
// Medios - morado
|
374 |
bar.style.background = `linear-gradient(to top, #c084fc, #9333ea)`;
|
375 |
} else {
|
376 |
-
// Agudos - rosa/rojo
|
377 |
bar.style.background = `linear-gradient(to top, #f472b6, #ec4899)`;
|
378 |
}
|
379 |
});
|
380 |
}
|
381 |
|
382 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
383 |
window.addEventListener('beforeunload', () => {
|
384 |
if (microphone) {
|
385 |
microphone.disconnect();
|
@@ -387,7 +557,20 @@
|
|
387 |
if (audioContext) {
|
388 |
audioContext.close();
|
389 |
}
|
|
|
|
|
|
|
|
|
|
|
390 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
391 |
});
|
392 |
</script>
|
393 |
</body>
|
|
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Video Reactivo en Cuadrícula</title>
|
7 |
<script src="https://cdn.tailwindcss.com"></script>
|
8 |
<style>
|
9 |
/* Estilos personalizados que complementan Tailwind */
|
10 |
+
body {
|
11 |
+
background-color: #1a202c; /* Fondo oscuro */
|
12 |
+
color: #e2e8f0; /* Texto claro */
|
13 |
+
font-family: sans-serif;
|
14 |
+
display: flex;
|
15 |
+
flex-direction: column;
|
16 |
+
align-items: center;
|
17 |
+
justify-content: center;
|
18 |
+
min-height: 100vh;
|
19 |
+
padding: 20px;
|
20 |
+
overflow-x: hidden; /* Prevenir scroll horizontal */
|
21 |
}
|
22 |
|
23 |
+
h1, p {
|
24 |
+
text-align: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
}
|
26 |
|
27 |
+
/* Contenedor de la cuadrícula */
|
28 |
+
.grid-container {
|
29 |
+
display: grid;
|
30 |
+
/* Definir una cuadrícula de 4x4 */
|
31 |
+
grid-template-columns: repeat(4, 1fr);
|
32 |
+
grid-template-rows: repeat(4, 1fr);
|
33 |
+
gap: 5px; /* Espacio entre las celdas de la cuadrícula */
|
34 |
+
width: 100%;
|
35 |
+
max-width: 800px; /* Ancho máximo similar al contenedor de video anterior */
|
36 |
+
margin: 20px auto; /* Centrar con margen */
|
37 |
+
aspect-ratio: 16 / 9; /* Mantener una relación de aspecto común de video */
|
38 |
+
border-radius: 8px;
|
39 |
+
overflow: hidden; /* Asegurar que los bordes redondeados se apliquen */
|
40 |
}
|
41 |
|
42 |
+
/* Estilo para cada celda de la cuadrícula (canvas) */
|
43 |
+
.grid-cell {
|
44 |
+
width: 100%;
|
45 |
+
height: 100%;
|
46 |
+
background-color: #2d3748; /* Color de fondo por si el video no carga */
|
47 |
+
transition: transform 0.2s ease-out, filter 0.2s ease-out; /* Transiciones para efectos */
|
48 |
}
|
49 |
|
50 |
.visualizer {
|
|
|
102 |
|
103 |
/* Estilos responsivos con Media Queries personalizadas si es necesario */
|
104 |
@media (max-width: 640px) { /* Ejemplo de breakpoint para móviles */
|
105 |
+
.grid-container {
|
106 |
+
gap: 2px; /* Menor espacio en pantallas pequeñas */
|
107 |
+
border-radius: 4px;
|
108 |
}
|
109 |
|
110 |
.visualizer {
|
|
|
123 |
</head>
|
124 |
<body class="bg-gray-900 text-white min-h-screen flex flex-col items-center justify-center p-4 sm:p-6 lg:p-8 font-sans">
|
125 |
<div class="text-center mb-8">
|
126 |
+
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-extrabold mb-2">Video Reactivo en Cuadrícula</h1>
|
127 |
+
<p class="text-gray-300 text-sm sm:text-base">Observa cómo cada sección del video reacciona a la música</p>
|
128 |
</div>
|
129 |
|
130 |
+
<video id="videoSource" src="https://getsamplefiles.com/download/webm/sample-2.webm" loop muted playsinline class="hidden"></video>
|
131 |
+
|
132 |
+
<div class="grid-container" id="gridContainer">
|
133 |
+
</div>
|
|
|
|
|
|
|
|
|
134 |
|
135 |
<div class="controls w-full max-w-sm sm:max-w-md lg:max-w-lg mt-8">
|
136 |
<div class="frequency-label">
|
|
|
153 |
|
154 |
<script>
|
155 |
document.addEventListener('DOMContentLoaded', () => {
|
156 |
+
const videoSource = document.getElementById('videoSource');
|
157 |
+
const gridContainer = document.getElementById('gridContainer');
|
158 |
const startButton = document.getElementById('startButton');
|
159 |
const visualizer = document.getElementById('visualizer');
|
160 |
const statusText = document.getElementById('status');
|
161 |
|
162 |
+
let audioContext = null;
|
163 |
let analyser = null;
|
164 |
let microphone = null;
|
165 |
let dataArray = null;
|
166 |
let isPlaying = false;
|
167 |
|
168 |
+
const gridRows = 4;
|
169 |
+
const gridCols = 4;
|
170 |
+
const totalCells = gridRows * gridCols;
|
171 |
+
const canvasElements = []; // Array para almacenar los elementos canvas
|
172 |
+
const canvasContexts = []; // Array para almacenar los contextos 2D
|
173 |
+
|
174 |
+
// Crear las celdas de la cuadrícula (elementos canvas)
|
175 |
+
function createGridCells() {
|
176 |
+
gridContainer.innerHTML = ''; // Limpiar contenedor por si acaso
|
177 |
+
for (let i = 0; i < totalCells; i++) {
|
178 |
+
const canvas = document.createElement('canvas');
|
179 |
+
canvas.classList.add('grid-cell');
|
180 |
+
// Establecer tamaño inicial para que CSS pueda escalarlos
|
181 |
+
canvas.width = 200; // Tamaño arbitrario, se ajustará con CSS y drawImage
|
182 |
+
canvas.height = 112; // Mantener relación de aspecto 16:9
|
183 |
+
gridContainer.appendChild(canvas);
|
184 |
+
canvasElements.push(canvas);
|
185 |
+
canvasContexts.push(canvas.getContext('2d'));
|
186 |
+
}
|
187 |
+
}
|
188 |
+
|
189 |
+
createGridCells(); // Crear la cuadrícula al cargar la página
|
190 |
+
|
191 |
// Crear barras del visualizador dinámicamente
|
192 |
+
const numberOfBars = 64;
|
193 |
for (let i = 0; i < numberOfBars; i++) {
|
194 |
const bar = document.createElement('div');
|
195 |
bar.className = 'bar';
|
|
|
197 |
}
|
198 |
const bars = document.querySelectorAll('.bar');
|
199 |
|
200 |
+
// Intentar reproducir el video fuente (oculto)
|
201 |
+
videoSource.play().catch(error => {
|
202 |
+
console.error('Error al intentar reproducir el video fuente automáticamente:', error);
|
203 |
+
// El video se iniciará al activar el micrófono si falla aquí
|
|
|
204 |
});
|
205 |
|
206 |
|
|
|
211 |
statusText.textContent = 'Solicitando acceso al micrófono...';
|
212 |
await initAudio();
|
213 |
startButton.textContent = 'Micrófono Activado';
|
|
|
214 |
startButton.classList.remove('bg-blue-600', 'hover:bg-blue-700', 'pulse');
|
215 |
startButton.classList.add('bg-green-600', 'hover:bg-green-700');
|
216 |
+
statusText.textContent = 'Micrófono activado. Observa la cuadrícula.';
|
217 |
isPlaying = true;
|
218 |
|
219 |
+
// Asegurarse de que el video fuente esté reproduciéndose
|
220 |
+
videoSource.play().catch(error => {
|
221 |
+
console.error('Error al intentar reproducir el video fuente tras activar micrófono:', error);
|
222 |
+
// Mensaje al usuario si el video no inicia
|
223 |
+
statusText.textContent = 'Micrófono activado, pero no se pudo reproducir el video. Intenta recargar la página.';
|
224 |
});
|
225 |
|
226 |
+
// Iniciar el bucle de renderizado del canvas
|
227 |
+
renderGrid();
|
228 |
+
|
229 |
}
|
230 |
} catch (err) {
|
231 |
+
console.error('Error completo al inicializar el audio:', err);
|
|
|
232 |
|
233 |
let userMessage = 'Error desconocido al acceder al micrófono.';
|
|
|
234 |
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
|
235 |
userMessage = 'Permiso de micrófono denegado. Por favor, permite el acceso al micrófono en la configuración de tu navegador.';
|
236 |
} else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
|
237 |
userMessage = 'No se encontró ningún micrófono. Asegúrate de que uno esté conectado y configurado correctamente.';
|
238 |
} else if (err.name === 'NotReadableError' || err.name === 'TrackStartError') {
|
239 |
userMessage = 'El micrófono está en uso por otra aplicación o hay un problema de hardware.';
|
|
|
|
|
240 |
} else if (err.name === 'SecurityError') {
|
241 |
userMessage = 'Se requiere una conexión segura (HTTPS) para acceder al micrófono.';
|
242 |
} else if (err.message) {
|
|
|
243 |
userMessage = `Error al acceder al micrófono: ${err.message}.`;
|
244 |
} else {
|
|
|
245 |
userMessage = `Error al acceder al micrófono: ${typeof err === 'object' ? JSON.stringify(err) : err}.`;
|
246 |
}
|
247 |
|
248 |
+
statusText.textContent = userMessage;
|
|
|
|
|
249 |
startButton.classList.remove('bg-green-600', 'hover:bg-green-700');
|
250 |
startButton.classList.add('bg-red-600', 'hover:bg-red-700');
|
251 |
startButton.textContent = 'Error de Micrófono';
|
|
|
254 |
|
255 |
// Función para inicializar el AudioContext y conectar el micrófono
|
256 |
async function initAudio() {
|
|
|
257 |
if (!audioContext) {
|
258 |
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
259 |
}
|
260 |
|
|
|
261 |
analyser = audioContext.createAnalyser();
|
262 |
+
analyser.fftSize = 128; // Tamaño del FFT
|
263 |
+
// Ajustes para suavizar y dar más detalle a la respuesta en frecuencia
|
264 |
+
analyser.smoothingTimeConstant = 0.8;
|
265 |
+
analyser.minDecibels = -90;
|
266 |
+
analyser.maxDecibels = -10;
|
267 |
+
|
268 |
|
|
|
269 |
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
270 |
microphone = audioContext.createMediaStreamSource(stream);
|
271 |
+
microphone.connect(analyser);
|
272 |
|
273 |
+
dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
|
274 |
|
275 |
// Iniciar el procesamiento de audio
|
276 |
processAudio();
|
277 |
}
|
278 |
|
279 |
+
// Bucle principal para procesar el audio
|
280 |
function processAudio() {
|
281 |
if (!analyser || !isPlaying) {
|
282 |
+
requestAnimationFrame(processAudio);
|
|
|
283 |
return;
|
284 |
}
|
285 |
|
|
|
286 |
analyser.getByteFrequencyData(dataArray);
|
287 |
|
288 |
+
// Dividir las frecuencias en bandas
|
289 |
+
const lowFreq = getAverageVolume(dataArray, 0, Math.floor(dataArray.length * 0.15));
|
290 |
+
const midFreq = getAverageVolume(dataArray, Math.floor(dataArray.length * 0.15), Math.floor(dataArray.length * 0.5));
|
291 |
+
const highFreq = getAverageVolume(dataArray, Math.floor(dataArray.length * 0.5), dataArray.length - 1);
|
|
|
292 |
|
293 |
+
// Aplicar efectos a las celdas basado en las frecuencias y otros "canales"
|
294 |
+
applyEffectsToGrid(dataArray, lowFreq, midFreq, highFreq);
|
|
|
|
|
295 |
|
296 |
// Actualizar el visualizador de barras
|
297 |
updateVisualizer(dataArray);
|
298 |
|
|
|
299 |
requestAnimationFrame(processAudio);
|
300 |
}
|
301 |
|
302 |
// Calcula el volumen promedio en un rango de datos
|
303 |
function getAverageVolume(data, start, end) {
|
304 |
let sum = 0;
|
305 |
+
let count = 0;
|
306 |
for (let i = start; i <= end; i++) {
|
307 |
+
if (data[i] !== undefined) {
|
308 |
+
sum += data[i];
|
309 |
+
count++;
|
310 |
+
}
|
311 |
}
|
312 |
+
return count > 0 ? sum / count : 0;
|
313 |
}
|
314 |
|
315 |
+
// --- Nueva función para aplicar efectos a la cuadrícula ---
|
316 |
+
function applyEffectsToGrid(frequencyData, low, mid, high) {
|
317 |
+
// Normalizar frecuencias para una intensidad entre 0 y 1
|
318 |
+
const lowIntensity = low / 255;
|
319 |
+
const midIntensity = mid / 255;
|
320 |
+
const highIntensity = high / 255;
|
321 |
+
const overallIntensity = getAverageVolume(frequencyData, 0, frequencyData.length - 1) / 255;
|
|
|
|
|
322 |
|
323 |
+
canvasContexts.forEach((ctx, index) => {
|
324 |
+
// Guardar el estado actual del contexto antes de aplicar transformaciones/filtros
|
325 |
+
ctx.save();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
326 |
|
327 |
+
// Limpiar el canvas (opcional, dependiendo del efecto)
|
328 |
+
// ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
|
329 |
|
330 |
+
// Calcular la posición de la celda en la cuadrícula
|
331 |
+
const row = Math.floor(index / gridCols);
|
332 |
+
const col = index % gridCols;
|
333 |
+
|
334 |
+
// Calcular la porción del video fuente para esta celda
|
335 |
+
const videoWidth = videoSource.videoWidth;
|
336 |
+
const videoHeight = videoSource.videoHeight;
|
337 |
+
const sourceX = (videoWidth / gridCols) * col;
|
338 |
+
const sourceY = (videoHeight / gridRows) * row;
|
339 |
+
const sourceWidth = videoWidth / gridCols;
|
340 |
+
const sourceHeight = videoHeight / gridRows;
|
341 |
+
|
342 |
+
// Calcular el tamaño de destino en el canvas
|
343 |
+
const destWidth = ctx.canvas.width;
|
344 |
+
const destHeight = ctx.canvas.height;
|
345 |
+
|
346 |
+
// --- Aplicar efectos basados en diferentes "canales" (frecuencias, posición, etc.) ---
|
347 |
+
|
348 |
+
// Canal 1-3: Frecuencias de Audio (Graves, Medios, Agudos)
|
349 |
+
// Canal 4: Volumen General
|
350 |
+
|
351 |
+
// Efecto de Tono/Color (influenciado por Graves)
|
352 |
+
const hueRotate = lowIntensity * 360;
|
353 |
+
// Efecto de Saturación (influenciado por Medios)
|
354 |
+
const saturate = 1 + midIntensity * 2; // Aumentar saturación hasta 3x
|
355 |
+
// Efecto de Contraste (influenciado por Agudos)
|
356 |
+
const contrast = 1 + highIntensity * 1; // Aumentar contraste hasta 2x
|
357 |
+
|
358 |
+
// Aplicar filtros CSS al contexto (si es compatible) o simular con manipulación de píxeles
|
359 |
+
// La propiedad filter en CanvasRenderingContext2D es experimental, usamos CSS filters en el elemento canvas
|
360 |
+
// ctx.filter = `hue-rotate(${hueRotate}deg) saturate(${saturate}) contrast(${contrast})`;
|
361 |
+
canvasElements[index].style.filter = `hue-rotate(${hueRotate}deg) saturate(${saturate}) contrast(${contrast})`;
|
362 |
+
|
363 |
+
|
364 |
+
// Canal 5-8: Posición de la Celda en la Cuadrícula
|
365 |
+
// Usar la posición para modular otros efectos o crear patrones
|
366 |
+
const positionFactorX = col / (gridCols - 1); // Normalizado entre 0 y 1
|
367 |
+
const positionFactorY = row / (gridRows - 1); // Normalizado entre 0 y 1
|
368 |
+
|
369 |
+
// Canal 9: Índice de la Celda
|
370 |
+
const indexFactor = index / (totalCells - 1); // Normalizado entre 0 y 1
|
371 |
+
|
372 |
+
// Efecto de Rotación (influenciado por Medios y Posición)
|
373 |
+
const rotation = midIntensity * 30 * (positionFactorX - 0.5) * 2; // Rotación basada en medios y posición X
|
374 |
+
ctx.translate(destWidth / 2, destHeight / 2); // Mover origen al centro
|
375 |
+
ctx.rotate(rotation * Math.PI / 180); // Aplicar rotación en radianes
|
376 |
+
ctx.translate(-destWidth / 2, -destHeight / 2); // Restaurar origen
|
377 |
+
|
378 |
+
|
379 |
+
// Canal 10: Tiempo Actual del Video (para efectos basados en el tiempo)
|
380 |
+
const videoTime = videoSource.currentTime % 1; // Usar la parte decimal para un ciclo rápido
|
381 |
+
|
382 |
+
// Efecto de Escala/Zoom (influenciado por Graves y Tiempo)
|
383 |
+
const scale = 1 + lowIntensity * 0.2 * Math.sin(videoTime * Math.PI * 2); // Pequeño zoom pulsante
|
384 |
+
ctx.scale(scale, scale);
|
385 |
+
// Ajustar traslación después de escalar para mantener centrado
|
386 |
+
ctx.translate(destWidth * (1 - scale) / 2, destHeight * (1 - scale) / 2);
|
387 |
+
|
388 |
+
|
389 |
+
// Canal 11: Combinación de Frecuencias (ej. Graves + Agudos)
|
390 |
+
const lowHighCombo = (lowIntensity + highIntensity) / 2;
|
391 |
+
|
392 |
+
// Efecto de Opacidad (influenciado por Combinación Graves/Agudos)
|
393 |
+
const opacity = 0.5 + lowHighCombo * 0.5; // Opacidad entre 0.5 y 1
|
394 |
+
ctx.globalAlpha = opacity;
|
395 |
+
|
396 |
+
|
397 |
+
// Canal 12: Detección de Picos (simplificado: si el volumen general supera un umbral)
|
398 |
+
const peakThreshold = 0.6; // Umbral de intensidad general
|
399 |
+
const isPeak = overallIntensity > peakThreshold;
|
400 |
+
|
401 |
+
// Efecto de Glitch/Inversión (activado por Picos)
|
402 |
+
if (isPeak) {
|
403 |
+
// Aplicar un filtro de inversión momentáneo o un desplazamiento
|
404 |
+
canvasElements[index].style.filter += ' invert(1)';
|
405 |
+
// Podríamos añadir un pequeño desplazamiento aleatorio aquí también
|
406 |
+
canvasElements[index].style.transform = `translate(${(Math.random() - 0.5) * 5}px, ${(Math.random() - 0.5) * 5}px)`;
|
407 |
+
} else {
|
408 |
+
// Asegurarse de que el efecto de pico se desactive
|
409 |
+
if (canvasElements[index].style.filter.includes('invert')) {
|
410 |
+
canvasElements[index].style.filter = canvasElements[index].style.filter.replace('invert(1)', '').trim();
|
411 |
+
}
|
412 |
+
// Restaurar la transformación si se aplicó desplazamiento por pico
|
413 |
+
if (canvasElements[index].style.transform !== `rotate(${rotation}deg)`) {
|
414 |
+
canvasElements[index].style.transform = `rotate(${rotation}deg)`; // Volver a la rotación normal
|
415 |
+
}
|
416 |
+
}
|
417 |
+
|
418 |
+
|
419 |
+
// Canal 13: Frecuencia Específica (ej. una banda estrecha de medios)
|
420 |
+
// Esto requeriría un análisis más detallado del dataArray o múltiples analizadores
|
421 |
+
// Por ahora, lo simulamos usando una combinación de medios y posición
|
422 |
+
const specificFreqInfluence = midIntensity * (1 - Math.abs(positionFactorX - 0.5) * 2); // Más influencia en el centro horizontal
|
423 |
+
|
424 |
+
// Efecto de Desenfoque (influenciado por Frecuencia Específica)
|
425 |
+
const blurAmount = specificFreqInfluence * 5; // Desenfoque máximo de 5px
|
426 |
+
canvasElements[index].style.filter += ` blur(${blurAmount}px)`;
|
427 |
+
|
428 |
+
|
429 |
+
// Canal 14: Alternancia Basada en el Tiempo (ej. cada segundo)
|
430 |
+
const timeBasedToggle = Math.floor(videoSource.currentTime) % 2 === 0;
|
431 |
+
|
432 |
+
// Efecto de Escala Alterna (influenciado por Alternancia y Agudos)
|
433 |
+
if (timeBasedToggle) {
|
434 |
+
const scaleAlt = 1 + highIntensity * 0.1;
|
435 |
+
// Esto sobrescribiría la escala anterior, hay que combinar transformaciones
|
436 |
+
// Para simplificar, aplicaremos este efecto a un subconjunto de celdas o de forma alterna
|
437 |
+
// Apliquemos a celdas pares/impares
|
438 |
+
if (index % 2 === 0) {
|
439 |
+
// Re-aplicar transformaciones combinadas
|
440 |
+
ctx.restore(); ctx.save(); // Restaurar y guardar de nuevo
|
441 |
+
const combinedRotation = midIntensity * 30 * (positionFactorX - 0.5) * 2;
|
442 |
+
const combinedScale = (1 + lowIntensity * 0.2 * Math.sin(videoTime * Math.PI * 2)) * scaleAlt;
|
443 |
+
ctx.translate(destWidth / 2, destHeight / 2);
|
444 |
+
ctx.rotate(combinedRotation * Math.PI / 180);
|
445 |
+
ctx.scale(combinedScale, combinedScale);
|
446 |
+
ctx.translate(-destWidth / 2, -destHeight / 2);
|
447 |
+
}
|
448 |
+
}
|
449 |
+
|
450 |
+
|
451 |
+
// Canal 15: Interacción del Usuario (ej. click en una celda) - Requiere listeners de eventos
|
452 |
+
// No implementado en este bucle, se gestionaría con addEventListener en las celdas
|
453 |
+
|
454 |
+
// Canal 16: Datos Externos (simulado, ej. un valor aleatorio que cambia lentamente)
|
455 |
+
// Esto requeriría una fuente externa (WebSocket, API, etc.)
|
456 |
+
// Simulemos un valor que cambia con el tiempo del video
|
457 |
+
const externalDataInfluence = Math.sin(videoSource.currentTime * 0.5) * 0.5 + 0.5; // Valor entre 0 y 1
|
458 |
+
|
459 |
+
// Efecto de Desplazamiento (influenciado por Datos Externos y Posición Y)
|
460 |
+
const translateY = externalDataInfluence * 20 * positionFactorY; // Desplazamiento vertical
|
461 |
+
// Combinar con la rotación y escala existentes
|
462 |
+
if (index % 2 !== 0) { // Aplicar a celdas impares para variar
|
463 |
+
ctx.restore(); ctx.save(); // Restaurar y guardar de nuevo
|
464 |
+
const combinedRotation = midIntensity * 30 * (positionFactorX - 0.5) * 2;
|
465 |
+
const combinedScale = 1 + lowIntensity * 0.2 * Math.sin(videoTime * Math.PI * 2);
|
466 |
+
ctx.translate(destWidth / 2, destHeight / 2);
|
467 |
+
ctx.rotate(combinedRotation * Math.PI / 180);
|
468 |
+
ctx.scale(combinedScale, combinedScale);
|
469 |
+
ctx.translate(-destWidth / 2, -destHeight / 2);
|
470 |
+
ctx.translate(0, translateY); // Aplicar desplazamiento vertical
|
471 |
+
}
|
472 |
+
|
473 |
+
|
474 |
+
// --- Fin de la aplicación de efectos ---
|
475 |
+
|
476 |
+
// Dibujar la porción del video en el canvas
|
477 |
+
// drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
|
478 |
+
if (videoSource.readyState >= 2) { // Asegurarse de que el video esté listo
|
479 |
+
ctx.drawImage(videoSource, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, destWidth, destHeight);
|
480 |
+
}
|
481 |
+
|
482 |
+
|
483 |
+
// Restaurar el estado del contexto para que los efectos no afecten a la siguiente celda
|
484 |
+
ctx.restore();
|
485 |
+
});
|
486 |
}
|
487 |
|
488 |
+
|
489 |
// Actualiza la altura y color de las barras del visualizador
|
490 |
function updateVisualizer(data) {
|
491 |
+
const barWidth = visualizer.offsetWidth / numberOfBars;
|
492 |
bars.forEach((bar, i) => {
|
|
|
493 |
const dataIndex = Math.floor(i * (data.length / numberOfBars));
|
494 |
+
const value = data[dataIndex] || 0;
|
|
|
|
|
495 |
const maxHeight = visualizer.offsetHeight;
|
496 |
+
const height = `${(value / 255) * maxHeight}px`;
|
497 |
|
498 |
bar.style.height = height;
|
499 |
+
bar.style.width = `${barWidth - 1}px`;
|
500 |
|
501 |
+
if (i < numberOfBars * 0.3) { // Ajustar rangos de color a las nuevas bandas
|
|
|
|
|
502 |
bar.style.background = `linear-gradient(to top, #60a5fa, #3b82f6)`;
|
503 |
+
} else if (i < numberOfBars * 0.6) {
|
|
|
504 |
bar.style.background = `linear-gradient(to top, #c084fc, #9333ea)`;
|
505 |
} else {
|
|
|
506 |
bar.style.background = `linear-gradient(to top, #f472b6, #ec4899)`;
|
507 |
}
|
508 |
});
|
509 |
}
|
510 |
|
511 |
+
// Bucle de renderizado para dibujar el video en los canvas
|
512 |
+
function renderGrid() {
|
513 |
+
if (!videoSource.paused && !videoSource.ended) {
|
514 |
+
// Redibujar la cuadrícula con los efectos aplicados
|
515 |
+
// applyEffectsToGrid ya dibuja el video en cada canvas
|
516 |
+
if (isPlaying && analyser) { // Solo aplicar efectos si el audio está activo
|
517 |
+
// applyEffectsToGrid se llama desde processAudio, que ya está en un requestAnimationFrame loop
|
518 |
+
// No necesitamos otro bucle de renderizado aquí si processAudio ya lo maneja
|
519 |
+
// Si processAudio no estuviera en RAF, este bucle sería necesario.
|
520 |
+
// Mantengamos applyEffectsToGrid dentro de processAudio por ahora.
|
521 |
+
} else {
|
522 |
+
// Si el audio no está activo, solo dibujar el video sin efectos dinámicos
|
523 |
+
canvasContexts.forEach((ctx, index) => {
|
524 |
+
ctx.save();
|
525 |
+
const row = Math.floor(index / gridCols);
|
526 |
+
const col = index % gridCols;
|
527 |
+
const videoWidth = videoSource.videoWidth;
|
528 |
+
const videoHeight = videoSource.videoHeight;
|
529 |
+
const sourceX = (videoWidth / gridCols) * col;
|
530 |
+
const sourceY = (videoHeight / gridRows) * row;
|
531 |
+
const sourceWidth = videoWidth / gridCols;
|
532 |
+
const sourceHeight = videoHeight / gridRows;
|
533 |
+
const destWidth = ctx.canvas.width;
|
534 |
+
const destHeight = ctx.canvas.height;
|
535 |
+
|
536 |
+
if (videoSource.readyState >= 2) {
|
537 |
+
ctx.drawImage(videoSource, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, destWidth, destHeight);
|
538 |
+
}
|
539 |
+
ctx.restore();
|
540 |
+
});
|
541 |
+
}
|
542 |
+
}
|
543 |
+
// Continuar el bucle de renderizado independientemente del estado del audio
|
544 |
+
// Esto asegura que el video se muestre incluso sin efectos de audio
|
545 |
+
requestAnimationFrame(renderGrid);
|
546 |
+
}
|
547 |
+
|
548 |
+
// Iniciar el bucle de renderizado del canvas al cargar la página
|
549 |
+
renderGrid();
|
550 |
+
|
551 |
+
|
552 |
+
// Limpieza al cerrar la página
|
553 |
window.addEventListener('beforeunload', () => {
|
554 |
if (microphone) {
|
555 |
microphone.disconnect();
|
|
|
557 |
if (audioContext) {
|
558 |
audioContext.close();
|
559 |
}
|
560 |
+
if (videoSource) {
|
561 |
+
videoSource.pause();
|
562 |
+
videoSource.removeAttribute('src'); // Liberar recurso de video
|
563 |
+
videoSource.load();
|
564 |
+
}
|
565 |
});
|
566 |
+
|
567 |
+
// Asegurarse de que el video se pueda reproducir después de la interacción del usuario
|
568 |
+
startButton.addEventListener('click', () => {
|
569 |
+
videoSource.play().catch(error => {
|
570 |
+
console.error('Error al intentar reproducir el video fuente después del clic:', error);
|
571 |
+
});
|
572 |
+
});
|
573 |
+
|
574 |
});
|
575 |
</script>
|
576 |
</body>
|