Spaces:
Running
Running
<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> | |