video-audio / index.html
Merlintxu's picture
Update index.html
14d5c10 verified
raw
history blame contribute delete
9.82 kB
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Visualizer PRO — 10 Mods</title>
<!-- Tailwind Play CDN  →  sólo para demos (compila en prod) -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* ==== Estilos de efectos (Mods 1-6) =================================== */
.blend-screen canvas {mix-blend-mode:screen;} /* 1 */
.comp-multiply canvas {image-rendering:pixelated;} /* 2 */
.blur-hue canvas {backdrop-filter:blur(6px) hue-rotate(45deg);} /* 4 */
.clip-polygon canvas {clip-path:polygon(0 0,100% 0,80% 100%,20% 100%);} /* 5 */
.mask-radial canvas {mask-image:radial-gradient(circle,white 60%,transparent);}/* 6 */
/* Overlay vectorial */
svg.overlay{position:absolute;inset:0;pointer-events:none}
/* Aseguramos que los lienzos mantengan aspect-ratio y flex  1 */
canvas{width:100%;height:100%}
</style>
</head>
<body class="bg-gray-900 text-slate-100 flex flex-col items-center min-h-screen gap-6 p-4">
<header class="text-center space-y-1">
<h1 class="text-3xl font-extrabold">Video-Audio Visualizer PRO</h1>
<p class="text-gray-400 text-sm">Activa o desactiva efectos al vuelo</p>
</header>
<!-- ===== Panel de Mods ================================================= -->
<fieldset id="mods" class="grid grid-cols-2 sm:grid-cols-3 gap-3 max-w-lg w-full text-xs bg-slate-800/40 p-4 rounded">
<label><input type="checkbox" data-mod="blend" > 1 Blend screen</label>
<label><input type="checkbox" data-mod="comp" > 2 Composite multiply</label>
<label><input type="checkbox" data-mod="gl" > 3 Shader WebGL</label>
<label><input type="checkbox" data-mod="blur" > 4 Backdrop blur</label>
<label><input type="checkbox" data-mod="clip" > 5 Clip-path</label>
<label><input type="checkbox" data-mod="mask" > 6 Mask radial</label>
<label><input type="checkbox" data-mod="off" checked> 7 Off-screen buffer</label>
<label><input type="checkbox" data-mod="idle" checked> 8 Idle throttle</label>
<label><input type="checkbox" data-mod="io" checked> 9 Auto-pause</label>
<label class="col-span-2 sm:col-span-3"><input type="checkbox" data-mod="vec"> 🔟 SVG Lissajous</label>
</fieldset>
<!-- ===== Control FFT ==================================================== -->
<label class="text-xs">FFT 2<sup>n</sup>
<input id="fft" type="range" min="5" max="11" value="9" class="accent-sky-500 w-40 align-middle">
<span id="fftVal">512</span>
</label>
<!-- ===== Vídeo fuente oculto (fallback de audio) ======================= -->
<video id="vid" src="https://getsamplefiles.com/download/webm/sample-2.webm" loop muted playsinline class="hidden"></video>
<!-- ===== Cuadrícula responsiva ========================================= -->
<div id="grid" class="relative grid gap-px w-full max-w-5xl aspect-video
[grid-template-columns:repeat(auto-fit,minmax(120px,1fr))]"></div>
<!-- ===== Visualizador de barras ======================================== -->
<div id="bars" class="flex items-end justify-between h-24 w-full max-w-5xl bg-slate-700/40 rounded overflow-hidden"></div>
<!-- ===== Overlay vectorial (Mod 10) ==================================== -->
<svg id="svg" class="overlay" viewBox="0 0 1000 1000" style="display:none">
<polyline id="wave" fill="none" stroke="hotpink" stroke-width="2"/>
</svg>
<script>
/* ========== 1. DOM shortcuts =========================================== */
const fftInp = document.getElementById('fft');
const fftLbl = document.getElementById('fftVal');
const grid = document.getElementById('grid');
const modsUI = document.querySelectorAll('#mods input[type=checkbox]');
const bars = document.getElementById('bars');
const svg = document.getElementById('svg');
const poly = document.getElementById('wave');
const video = document.getElementById('vid');
/* ========== 2. Estado global y flags Mods ============================= */
const state={
modGL:false, modVec:false, // Mods 3 y 10
offscreen:true, useIdle:true, paused:false
};
/* ========== 3. AudioContext + Analyser ================================ */
const ctxAudio = new (window.AudioContext||webkitAudioContext)();
const analyser = ctxAudio.createAnalyser();
analyser.fftSize = 1 << fftInp.value; // 512 por defecto
let dataF = new Float32Array(analyser.frequencyBinCount);
/* ========== 4. Crear visualizador de barras ========================== */
const N_BARS = 64;
for(let i=0;i<N_BARS;i++){
const d=document.createElement('div');
d.className='flex-1 mx-px bg-gradient-to-t from-sky-500 to-violet-600';
d.style.transformOrigin='bottom';
bars.appendChild(d);
}
const barEls=[...bars.children];
/* ========== 5. Cuadrícula + Back-buffers (Mod 7) ====================== */
let tileCtxs=[];
function buildGrid(){
grid.innerHTML=''; tileCtxs=[];
const cols=getComputedStyle(grid).gridTemplateColumns.split(' ').length;
const rows=Math.round(cols/(16/9));
const total=cols*rows;
for(let i=0;i<total;i++){
const c=document.createElement('canvas');
grid.appendChild(c);
const w=256,h=144;
let buf,back;
if(state.offscreen && 'OffscreenCanvas'in window){ /* Mod 7 */
buf=new OffscreenCanvas(w,h);
back=buf.getContext('2d');
}else{
buf=document.createElement('canvas');buf.width=w;buf.height=h;
back=buf.getContext('2d');
}
c.width=w;c.height=h;
tileCtxs.push({front:c.getContext('2d'),back,buf});
}
}
buildGrid(); addEventListener('resize',buildGrid);
/* ========== 6. FFT slider → resizer seguro ============================ */
fftInp.oninput=()=>{
const size=1<<fftInp.value;
analyser.fftSize=size;
dataF=new Float32Array(analyser.frequencyBinCount);
fftLbl.textContent=size;
};
/* ========== 7. Helpers ================================================= */
const applyClass=(on,cls)=>grid.classList.toggle(cls,on);
const avg=(arr,s,e)=>{let sum=0;for(let i=s;i<e;i++)sum+=arr[i];return sum/(e-s)||0};
/* ========== 8. IntersectionObserver (Mod 9) =========================== */
const io=new IntersectionObserver(([e])=>state.paused=!e.isIntersecting,{threshold:.1});
io.observe(grid);
/* ========== 9. requestIdleCallback (Mod 8) ============================ */
let idleId=null;
function idleWork(dl){
while(dl.timeRemaining()>5){
/* tareas no críticas */
}
idleId=requestIdleCallback(idleWork);
}
if(state.useIdle) idleId=requestIdleCallback(idleWork);
/* ========== 10. WebGL placeholder (Mod 3) ============================ */
let glCanvas=null,gl=null;
function initGL(){
glCanvas=document.createElement('canvas');
glCanvas.className='absolute inset-0 w-full h-full';
grid.appendChild(glCanvas);
gl=glCanvas.getContext('webgl');
/* shader boilerplate recortado por brevedad */
}
function renderGL(){
if(!gl)return;
gl.clearColor(0,0,0,.05); gl.clear(gl.COLOR_BUFFER_BIT);
}
/* ========== 11. SVG Lissajous (Mod 10) =============================== */
function drawSVG(t){
const pts=[];
for(let x=0;x<1000;x++){
const y=500+120*Math.sin(.01*x+ t/1000)*Math.cos(.008*x);
pts.push(`${x},${y}`);
}
poly.setAttribute('points',pts.join(' '));
}
/* ========== 12. Bucle principal rAF ================================== */
let rafId=null;
function frame(t){
if(state.paused){ rafId=requestAnimationFrame(frame); return; }
analyser.getFloatFrequencyData(dataF); /* MDN 👆 */
/* Barras — usar transform (sin reflow) */
const h=bars.clientHeight, len=dataF.length/barEls.length;
for(let i=0;i<barEls.length;i++){
const v=dataF[Math.floor(i*len)];
barEls[i].style.transform=`scaleY(${(v+140)/100})`;
}
/* Cuadrícula con filtros adaptados a bajos/medios/agudos */
const low = avg(dataF,0,dataF.length*.15),
mid = avg(dataF,dataF.length*.15,dataF.length*.5),
high= avg(dataF,dataF.length*.5,dataF.length);
tileCtxs.forEach(({front,back,buf})=>{
const w=buf.width,h=buf.height;
back.save();
back.filter=`hue-rotate(${(low+140)*2}deg) saturate(${1+(mid+140)/200}) contrast(${1+(high+140)/200})`;
back.drawImage(video,0,0,video.videoWidth,video.videoHeight,0,0,w,h);
back.restore();
front.drawImage(buf,0,0,w,h);
});
if(state.modGL) renderGL();
if(state.modVec) drawSVG(t);
rafId=requestAnimationFrame(frame);
}
/* ========== 13. Gestor de Mods ======================================= */
modsUI.forEach(cb=>cb.addEventListener('change',e=>{
const on=e.target.checked, m=e.target.dataset.mod;
switch(m){
case'blend':applyClass(on,'blend-screen');break;
case'comp' :applyClass(on,'comp-multiply');break;
case'blur' :applyClass(on,'blur-hue');break;
case'clip' :applyClass(on,'clip-polygon');break;
case'mask' :applyClass(on,'mask-radial');break;
case'off' :state.offscreen=on;buildGrid();break;
case'idle' :state.useIdle=on; on?idleId=requestIdleCallback(idleWork):cancelIdleCallback(idleId);break;
case'io' :on?io.observe(grid):io.disconnect();break;
case'gl' :on?(initGL(),state.modGL=true):(glCanvas?.remove(),state.modGL=false);break;
case'vec' :svg.style.display=on?'block':'none';state.modVec=on;break;
}
}));
/* ========== 14. Arranque seguro (gesto usuario + fallback vídeo) ===== */
async function start(){
await ctxAudio.resume().catch(()=>{}); /* iOS exige gesto */
const src=ctxAudio.createMediaElementSource(video);
src.connect(analyser); /* no connect a destino → sin eco */
video.play().catch(()=>{}); /* mute */
frame(); /* ¡run! */
}
document.body.addEventListener('click',start,{once:true});
</script>
</body>
</html>