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> | |