Merlintxu commited on
Commit
921db4f
·
verified ·
1 Parent(s): baf2169

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +72 -570
index.html CHANGED
@@ -1,577 +1,79 @@
1
  <!DOCTYPE html>
2
  <html lang="es">
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 {
51
- height: 100px; /* Altura fija para el visualizador */
52
- width: 100%;
53
- background: linear-gradient(to top, #4b5563, #1f2937); /* Fondo degradado */
54
- margin-top: 20px;
55
- border-radius: 8px;
56
- overflow: hidden;
57
- display: flex; /* Usar flexbox para las barras */
58
- align-items: flex-end; /* Alinear barras en la parte inferior */
59
- justify-content: space-between; /* Espacio entre barras */
60
- padding: 0 2px; /* Pequeño padding horizontal */
61
- }
62
-
63
- .bar {
64
- height: 0; /* Altura inicial de las barras */
65
- background: linear-gradient(to top, #3b82f6, #9333ea); /* Degradado de color para las barras */
66
- width: 2px; /* Ancho fijo de las barras */
67
- transition: height 0.1s ease-out; /* Transición suave para la altura */
68
- flex-grow: 1; /* Permitir que las barras crezcan para llenar el espacio */
69
- margin-right: 1px; /* Espacio entre barras */
70
- }
71
-
72
- .bar:last-child {
73
- margin-right: 0; /* Eliminar margen de la última barra */
74
- }
75
-
76
- .controls {
77
- background-color: rgba(0, 0, 0, 0.7); /* Fondo semitransparente */
78
- padding: 20px;
79
- border-radius: 10px;
80
- margin-top: 20px;
81
- }
82
-
83
- .frequency-label {
84
- display: flex;
85
- justify-content: space-between;
86
- margin-bottom: 10px;
87
- color: white;
88
- font-weight: bold;
89
- padding: 0 10px; /* Padding para alinear con el visualizador */
90
- }
91
-
92
- /* Animación de pulso para el botón */
93
- .pulse {
94
- animation: pulse 1s infinite;
95
- }
96
-
97
- @keyframes pulse {
98
- 0% { transform: scale(1); }
99
- 50% { transform: scale(1.05); } /* Ligeramente más sutil */
100
- 100% { transform: scale(1); }
101
- }
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 {
111
- height: 70px; /* Menor altura en pantallas pequeñas */
112
- }
113
-
114
- .bar {
115
- width: 1px; /* Barras más delgadas en pantallas pequeñas */
116
- }
117
-
118
- .controls {
119
- padding: 15px;
120
- }
121
- }
122
- </style>
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">
137
- <span>Graves</span>
138
- <span>Medios</span>
139
- <span>Agudos</span>
140
- </div>
141
- <div class="visualizer" id="visualizer"></div>
142
-
143
- <div class="flex justify-center mt-6">
144
- <button id="startButton" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-full transition duration-300 ease-in-out pulse">
145
- Activar Micrófono
146
- </button>
147
- </div>
148
-
149
- <div class="mt-6 text-center text-sm text-gray-400">
150
- <p id="status">Esperando permiso para acceder al micrófono...</p>
151
- </div>
152
  </div>
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';
196
- visualizer.appendChild(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
-
207
- // Configurar el audio al hacer clic en el botón
208
- startButton.addEventListener('click', async () => {
209
- try {
210
- if (!isPlaying) {
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';
252
- }
253
- });
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();
556
- }
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>
577
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="es">
3
  <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Video‑Audio Grid Visualizer Pro</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ :root { --bg:#0f172a; --fg:#f1f5f9; --accent-a:#3b82f6; --accent-b:#9333ea; }
10
+ body{background:var(--bg);color:var(--fg);font-family:"Inter",sans-serif}
11
+ .grid-cell{background:#1e293b;transition:filter .12s,transform .12s}
12
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  </head>
14
+ <body class="min-h-screen flex flex-col items-center py-10 gap-10">
15
+ <header class="text-center space-y-2 px-4 max-w-xl">
16
+ <h1 class="text-4xl font-extrabold tracking-tight">Video‑Audio Grid Visualizer <span class="bg-gradient-to-r from-sky-500 to-violet-500 bg-clip-text text-transparent">Pro</span></h1>
17
+ <p class="opacity-80">Mosaicos de vídeo que reaccionan a tu micrófono (o, en su defecto, al audio del propio vídeo).</p>
18
+ </header>
19
+
20
+ <video id="videoSource" src="https://getsamplefiles.com/download/webm/sample-2.webm" loop muted playsinline class="hidden"></video>
21
+
22
+ <section id="gridContainer" class="grid auto-rows-[1fr] gap-1 w-[min(90vw,900px)] aspect-video rounded-lg overflow-hidden"></section>
23
+
24
+ <section class="w-[min(90vw,600px)] space-y-6">
25
+ <div class="flex justify-between text-xs px-1"><span>Bajos</span><span>Medios</span><span>Agudos</span></div>
26
+ <div id="visualizer" class="h-24 w-full bg-gradient-to-t from-slate-700 to-slate-800 flex items-end rounded-md overflow-hidden"></div>
27
+ <div class="grid sm:grid-cols-2 gap-4 text-sm">
28
+ <label class="flex flex-col gap-1">FFT Size
29
+ <input id="fftRange" type="range" min="32" max="2048" step="32" value="512" class="accent-sky-500 cursor-pointer"><span id="fftLabel" class="text-xs self-end">512</span>
30
+ </label>
31
+ <label class="flex flex-col gap-1">Sensibilidad
32
+ <input id="sensRange" type="range" min="0" max="100" value="70" class="accent-violet-500 cursor-pointer"><span id="sensLabel" class="text-xs self-end">0.70</span>
33
+ </label>
 
 
 
 
 
 
 
 
34
  </div>
35
+ <button id="startButton" class="mx-auto block bg-sky-600 hover:bg-sky-700 px-6 py-3 rounded-full font-semibold shadow-lg transition">Activar Micrófono</button>
36
+ <p id="status" class="text-center text-xs opacity-70">Haz clic para comenzar.</p>
37
+ <p id="httpsWarn" class="text-center text-xs text-orange-400 hidden">⚠ Sirve esta página vía HTTPS (o localhost) para acceder al micrófono.</p>
38
+ </section>
39
+
40
+ <script>
41
+ const $=s=>document.querySelector(s);
42
+ const gridContainer=$('#gridContainer');
43
+ const videoSource=$('#videoSource');
44
+ const startBtn=$('#startButton');
45
+ const visualizer=$('#visualizer');
46
+ const fftRange=$('#fftRange');const fftLabel=$('#fftLabel');
47
+ const sensRange=$('#sensRange');const sensLabel=$('#sensLabel');
48
+ const statusTxt=$('#status');const httpsWarn=$('#httpsWarn');
49
+ if(location.protocol!=='https:'&&location.hostname!=='localhost'){httpsWarn.classList.remove('hidden');startBtn.disabled=true;}
50
+ const getGridSize=()=>innerWidth<500?2:innerWidth<768?3:4;
51
+ let gridN=getGridSize();let cells=[],ctxs=[],backCtxs=[];
52
+ function buildGrid(){gridN=getGridSize();gridContainer.style.gridTemplateColumns=`repeat(${gridN},1fr)`;gridContainer.innerHTML='';cells=[];ctxs=[];backCtxs=[];const total=gridN*gridN;
53
+ for(let i=0;i<total;i++){const c=document.createElement('canvas');c.width=320;c.height=180;c.className='grid-cell';gridContainer.appendChild(c);cells.push(c);ctxs.push(c.getContext('2d'));
54
+ const off=document.createElement('canvas');off.width=c.width;off.height=c.height;backCtxs.push(off.getContext('2d'));}
55
+ }
56
+ buildGrid();addEventListener('resize',()=>{const n=getGridSize();if(n!==gridN)buildGrid();});
57
+ let audioCtx,analyser,sourceNode,floatData,prevFloatData,isRunning=false;
58
+ async function initAudio(){if(isRunning)return;statusTxt.textContent='Solicitando acceso…';audioCtx=new(AudioContext||webkitAudioContext)();analyser=audioCtx.createAnalyser();analyser.fftSize=+fftRange.value;analyser.smoothingTimeConstant=0;floatData=new Float32Array(analyser.frequencyBinCount);prevFloatData=new Float32Array(analyser.frequencyBinCount);
59
+ try{const stream=await navigator.mediaDevices.getUserMedia({audio:true});sourceNode=audioCtx.createMediaStreamSource(stream);sourceNode.connect(analyser);statusTxt.textContent='Micrófono activado ✔';}
60
+ catch(err){console.warn(err);statusTxt.textContent='Permiso denegado. Usando audio del vídeo.';videoSource.muted=false;await videoSource.play().catch(()=>{});sourceNode=audioCtx.createMediaElementSource(videoSource);sourceNode.connect(analyser);analyser.connect(audioCtx.destination);} isRunning=true;if(videoSource.paused)videoSource.play();renderLoop();}
61
+ const BAR_COUNT=64;const bars=Array.from({length:BAR_COUNT},()=>{const b=document.createElement('div');b.style.flex='1';b.style.marginInline='1px';b.style.height='0';visualizer.appendChild(b);return b;});
62
+ fftRange.oninput=()=>{fftLabel.textContent=fftRange.value;if(analyser)analyser.fftSize=+fftRange.value;};sensRange.oninput=()=>{sensLabel.textContent=(sensRange.value/100).toFixed(2);};
63
+ const avg=(a,sF,eF)=>{const len=a.length,s=~~(sF*len),e=~~(eF*len);let sum=0;for(let i=s;i<e;i++)sum+=a[i];return sum/(e-s);} ;
64
+ const dBToByte=dB=>{const min=analyser.minDecibels,max=analyser.maxDecibels;return Math.max(0,Math.min(1,(dB-min)/(max-min)))*255;};
65
+ function renderLoop(){if(!isRunning)return;analyser.getFloatFrequencyData(floatData);for(let i=0;i<floatData.length;i++){floatData[i]=0.5*floatData[i]+0.5*prevFloatData[i];prevFloatData[i]=floatData[i];}
66
+ const byteData=Uint8Array.from(floatData,dBToByte);
67
+ const low=avg(byteData,0,0.15),mid=avg(byteData,0.15,0.5),high=avg(byteData,0.5,1),sens=sensRange.value/100;
68
+ const maxH=visualizer.clientHeight;
69
+ bars.forEach((bar,i)=>{const idx=~~(i*(byteData.length/BAR_COUNT));const val=byteData[idx];bar.style.height=((val/255)*maxH)+'px';bar.style.background=i<BAR_COUNT*0.3?`linear-gradient(to top,var(--accent-a),#60a5fa)`:i<BAR_COUNT*0.6?`linear-gradient(to top,var(--accent-b),#c084fc)`:`linear-gradient(to top,#ec4899,#f472b6)`;});
70
+ if(videoSource.readyState>=2){const vw=videoSource.videoWidth,vh=videoSource.videoHeight;
71
+ ctxs.forEach((ctx,idx)=>{const bctx=backCtxs[idx];const row=~~(idx/gridN),col=idx%gridN;const sx=(vw/gridN)*col,sy=(vh/gridN)*row;const sw=vw/gridN,sh=vh/gridN;const dw=bctx.canvas.width,dh=bctx.canvas.height;const l=(low/255)*sens,m=(mid/255)*sens,h=(high/255)*sens;
72
+ bctx.save();bctx.clearRect(0,0,dw,dh);bctx.translate(dw/2,dh/2);bctx.rotate((m-0.3)*0.5);bctx.scale(1+l*0.2,1+l*0.2);bctx.translate(-dw/2,-dh/2);bctx.drawImage(videoSource,sx,sy,sw,sh,0,0,dw,dh);bctx.restore();
73
+ ctx.clearRect(0,0,dw,dh);ctx.drawImage(bctx.canvas,0,0);cells[idx].style.filter=`hue-rotate(${l*360}deg) saturate(${1+m*2}) contrast(${1+h})`;});}
74
+ requestAnimationFrame(renderLoop);
75
+ }
76
+ startBtn.onclick=()=>initAudio();
77
+ </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  </body>
79
  </html>