Spaces:
Running
Running
Update index.html
Browse files- index.html +72 -570
index.html
CHANGED
@@ -1,577 +1,79 @@
|
|
1 |
<!DOCTYPE html>
|
2 |
<html lang="es">
|
3 |
<head>
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
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="
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
<div class="
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
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 |
-
<
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
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>
|