Merlintxu commited on
Commit
baf2169
·
verified ·
1 Parent(s): 2a72ea4

Update index.html

Browse files
Files changed (1) hide show
  1. 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 al Audio Mejorado</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <style>
9
  /* Estilos personalizados que complementan Tailwind */
10
- .video-container {
11
- position: relative;
12
- width: 100%;
13
- max-width: 800px; /* Ancho máximo para el contenedor del video */
14
- margin: 0 auto; /* Centrar el contenedor */
15
- overflow: hidden;
16
- border-radius: 8px; /* Bordes redondeados */
17
- /* Transiciones suaves para los efectos visuales */
18
- transition: filter 0.3s ease-out, transform 0.3s ease-out;
 
 
19
  }
20
 
21
- .glitch-effect {
22
- position: absolute;
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
- /* Variaciones para las capas del efecto glitch */
36
- .glitch-effect:nth-child(2) {
37
- transform: translate(2px, -2px);
 
 
 
 
 
 
 
 
 
 
38
  }
39
 
40
- .glitch-effect:nth-child(3) {
41
- transform: translate(-2px, 2px);
 
 
 
 
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
- .video-container {
100
- border-radius: 4px; /* Bordes menos redondeados en pantallas pequeñas */
 
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 al Audio</h1>
120
- <p class="text-gray-300 text-sm sm:text-base">Conecta tu micrófono y observa cómo el video reacciona a las frecuencias de tu voz</p>
121
  </div>
122
 
123
- <div class="video-container" id="videoContainer">
124
- <video id="video" class="w-full h-auto block" loop muted playsinline> <source src="https://getsamplefiles.com/download/webm/sample-2.webm" type="video/webm">
125
- Tu navegador no soporta el elemento de video.
126
- </video>
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 video = document.getElementById('video');
154
- const videoContainer = document.getElementById('videoContainer');
155
  const startButton = document.getElementById('startButton');
156
  const visualizer = document.getElementById('visualizer');
157
  const statusText = document.getElementById('status');
158
 
159
- let audioContext = null; // Inicializado a 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; // Número de barras para el visualizador
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
- // Inicializar el video
175
- video.play().catch(error => {
176
- console.error('Error al intentar reproducir el video automáticamente:', error);
177
- // Manejar el error si la reproducción automática falla (ej. por políticas del navegador)
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. Habla o reproduce música para ver los efectos.';
193
  isPlaying = true;
194
 
195
- // Si el video no se reprodujo automáticamente, intentar reproducirlo ahora
196
- video.play().catch(error => {
197
- console.error('Error al intentar reproducir el video tras activar micrófono:', error);
198
- // No cambiamos el statusText aquí para no sobrescribir el mensaje de éxito del micrófono
 
199
  });
200
 
 
 
 
201
  }
202
  } catch (err) {
203
- // --- Inicio de la sección de manejo de errores mejorado ---
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; // Mostrar el mensaje específico al usuario
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, afecta la granularidad de las frecuencias
 
 
 
 
 
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); // Conectar el micrófono al analizador
250
 
251
- // Preparar array para los datos de frecuencia
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 y actualizar la visualización/efectos
259
  function processAudio() {
260
  if (!analyser || !isPlaying) {
261
- // Si el analizador no está listo o no estamos reproduciendo, salir
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 (aproximadas)
270
- // Los índices dependen del fftSize y sampleRate del AudioContext
271
- const lowFreq = getAverageVolume(dataArray, 0, Math.floor(dataArray.length * 0.1)); // Graves (aprox. 0-200 Hz)
272
- const midFreq = getAverageVolume(dataArray, Math.floor(dataArray.length * 0.1), Math.floor(dataArray.length * 0.4)); // Medios (aprox. 200-1000 Hz)
273
- const highFreq = getAverageVolume(dataArray, Math.floor(dataArray.length * 0.4), dataArray.length - 1); // Agudos (aprox. 1000+ Hz)
274
 
275
- // Aplicar efectos visuales basados en las frecuencias
276
- applyLowFrequencyEffect(lowFreq);
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
- sum += data[i] || 0; // Sumar valores, usar 0 si el índice es inválido
 
 
 
292
  }
293
- return sum / (end - start + 1);
294
  }
295
 
296
- // Aplica efectos basados en las frecuencias graves
297
- function applyLowFrequencyEffect(value) {
298
- // Los graves cambian el tono y el brillo general
299
- const intensity = value / 255; // Normalizar el valor entre 0 y 1
300
- // Ajustar la intensidad del filtro para que sea más notable
301
- const hue = intensity * 180; // Rotación de tono hasta 180 grados
302
- const brightness = 1 + intensity * 0.5; // Brillo hasta 1.5
303
- video.style.filter = `hue-rotate(${hue}deg) brightness(${brightness})`;
304
- }
305
 
306
- let lastMidValue = 0; // Para detectar cambios en la frecuencia media
307
- // Aplica efectos basados en las frecuencias medias
308
- function applyMidFrequencyEffect(value) {
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
- // Aplicar la rotación al contenedor del video
324
- videoContainer.style.transform = `rotate(${rotation}deg)`;
325
- }
326
 
327
- // Aplica efectos basados en las frecuencias agudas
328
- function applyHighFrequencyEffect(value) {
329
- // Los agudos activan el efecto glitch
330
- const glitchEffects = document.querySelectorAll('.glitch-effect');
331
- const activationThreshold = 100; // Umbral para activar el glitch
332
-
333
- if (value > activationThreshold) {
334
- const intensity = (value - activationThreshold) / (255 - activationThreshold); // Normalizar por encima del umbral
335
- glitchEffects.forEach((effect, index) => {
336
- // Aumentar la opacidad y aplicar un desplazamiento aleatorio basado en la intensidad
337
- effect.style.opacity = intensity * 0.6; // Opacidad máxima del 60%
338
- const maxOffset = intensity * 20; // Desplazamiento máximo basado en la intensidad
339
- effect.style.transform = `translate(${(Math.random() - 0.5) * maxOffset}px, ${(Math.random() - 0.5) * maxOffset}px)`;
340
- });
341
- // Añadir un pequeño efecto de contraste o saturación con los agudos
342
- video.style.filter += ` contrast(${1 + intensity * 0.2}) saturate(${1 + intensity * 0.3})`;
343
-
344
- } else {
345
- // Desactivar el glitch si los agudos están por debajo del umbral
346
- glitchEffects.forEach(effect => {
347
- effect.style.opacity = 0;
348
- });
349
- // Asegurarse de que los filtros de contraste/saturación vuelvan a la normalidad (se maneja en applyLowFrequencyEffect)
350
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  }
352
 
 
353
  // Actualiza la altura y color de las barras del visualizador
354
  function updateVisualizer(data) {
355
- const barWidth = visualizer.offsetWidth / numberOfBars; // Calcular ancho de barra dinámicamente
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; // Obtener valor, usar 0 si no hay datos
360
-
361
- // Calcular la altura de la barra (escalada)
362
  const maxHeight = visualizer.offsetHeight;
363
- const height = `${(value / 255) * maxHeight}px`; // Escalar la altura al 100% de la altura del visualizador
364
 
365
  bar.style.height = height;
366
- bar.style.width = `${barWidth - 1}px`; // Ajustar ancho y restar margen
367
 
368
- // Asignar colores basados en la posición (frecuencia) de la barra
369
- if (i < numberOfBars / 3) {
370
- // Graves - azul
371
  bar.style.background = `linear-gradient(to top, #60a5fa, #3b82f6)`;
372
- } else if (i < (numberOfBars * 2) / 3) {
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
- // Limpieza al cerrar la página (opcional pero recomendado en aplicaciones más grandes)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>