File size: 9,818 Bytes
8c86c22
 
 
fa1f54a
 
5d539cc
fa1f54a
14d5c10
5d539cc
fa1f54a
5d539cc
14d5c10
 
 
 
 
 
 
 
5d539cc
14d5c10
 
 
5d539cc
 
fa1f54a
14d5c10
 
5d539cc
 
 
fa1f54a
14d5c10
5d539cc
 
 
 
 
 
 
14d5c10
5d539cc
 
 
 
 
14d5c10
 
5d539cc
 
 
 
14d5c10
5d539cc
 
14d5c10
5d539cc
 
 
14d5c10
 
5d539cc
14d5c10
5d539cc
 
 
921db4f
 
14d5c10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6468367
14d5c10
 
 
 
 
 
 
 
 
 
 
 
 
5d539cc
14d5c10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fa1f54a
14d5c10
 
 
 
 
 
 
 
6468367
fa1f54a
14d5c10
 
 
fa1f54a
14d5c10
 
5d539cc
fa1f54a
14d5c10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d539cc
14d5c10
 
 
 
 
5d539cc
fa1f54a
5d539cc
14d5c10
 
 
 
 
 
 
 
 
 
 
5d539cc
14d5c10
 
 
 
 
 
 
 
 
5d539cc
 
14d5c10
 
fa1f54a
14d5c10
 
 
 
5d539cc
921db4f
5d539cc
14d5c10
 
 
5d539cc
 
14d5c10
 
 
 
 
 
 
 
 
5d539cc
 
 
14d5c10
 
 
 
 
 
 
921db4f
14d5c10
921db4f
2a72ea4
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
<!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>