Spaces:
Running
Running
| export class DisplacementFilter { | |
| constructor(rendererCanvas, videoElement) { | |
| this.canvas = rendererCanvas; | |
| this.video = videoElement; | |
| this.enabled = true; | |
| this.time = 0; | |
| // Create SVG turbulence filter for real per-pixel displacement | |
| this._createTurbulenceSVG(); | |
| } | |
| _createTurbulenceSVG() { | |
| // SVG filter = web equivalent of After Effects "Turbulent Displace" | |
| var svgNS = 'http://www.w3.org/2000/svg'; | |
| this.svg = document.createElementNS(svgNS, 'svg'); | |
| this.svg.setAttribute('width', '0'); | |
| this.svg.setAttribute('height', '0'); | |
| this.svg.style.position = 'absolute'; | |
| var defs = document.createElementNS(svgNS, 'defs'); | |
| // Filter for the webcam video | |
| var filter = document.createElementNS(svgNS, 'filter'); | |
| filter.setAttribute('id', 'turbulence-displace'); | |
| filter.setAttribute('x', '-10%'); | |
| filter.setAttribute('y', '-10%'); | |
| filter.setAttribute('width', '120%'); | |
| filter.setAttribute('height', '120%'); | |
| // feTurbulence = generates Perlin noise pattern | |
| // "Evolution" = seed, animated over time | |
| // "Amount" = scale parameter in feDisplacementMap | |
| this.turbulence = document.createElementNS(svgNS, 'feTurbulence'); | |
| this.turbulence.setAttribute('type', 'fractalNoise'); | |
| this.turbulence.setAttribute('baseFrequency', '0.008 0.006'); // Size (lower = larger swirls) | |
| this.turbulence.setAttribute('numOctaves', '3'); | |
| this.turbulence.setAttribute('seed', '0'); // Will animate this = "Evolution" | |
| this.turbulence.setAttribute('result', 'noise'); | |
| // feDisplacementMap = displaces pixels based on the noise | |
| this.displacement = document.createElementNS(svgNS, 'feDisplacementMap'); | |
| this.displacement.setAttribute('in', 'SourceGraphic'); | |
| this.displacement.setAttribute('in2', 'noise'); | |
| this.displacement.setAttribute('scale', '12'); // Amount — subtle but visible | |
| this.displacement.setAttribute('xChannelSelector', 'R'); | |
| this.displacement.setAttribute('yChannelSelector', 'G'); | |
| filter.appendChild(this.turbulence); | |
| filter.appendChild(this.displacement); | |
| defs.appendChild(filter); | |
| // Second filter for the Three.js canvas (slightly different params) | |
| var filter2 = document.createElementNS(svgNS, 'filter'); | |
| filter2.setAttribute('id', 'turbulence-canvas'); | |
| filter2.setAttribute('x', '-10%'); | |
| filter2.setAttribute('y', '-10%'); | |
| filter2.setAttribute('width', '120%'); | |
| filter2.setAttribute('height', '120%'); | |
| this.turbulence2 = document.createElementNS(svgNS, 'feTurbulence'); | |
| this.turbulence2.setAttribute('type', 'fractalNoise'); | |
| this.turbulence2.setAttribute('baseFrequency', '0.005 0.004'); | |
| this.turbulence2.setAttribute('numOctaves', '2'); | |
| this.turbulence2.setAttribute('seed', '42'); | |
| this.turbulence2.setAttribute('result', 'noise'); | |
| this.displacement2 = document.createElementNS(svgNS, 'feDisplacementMap'); | |
| this.displacement2.setAttribute('in', 'SourceGraphic'); | |
| this.displacement2.setAttribute('in2', 'noise'); | |
| this.displacement2.setAttribute('scale', '8'); | |
| this.displacement2.setAttribute('xChannelSelector', 'R'); | |
| this.displacement2.setAttribute('yChannelSelector', 'G'); | |
| filter2.appendChild(this.turbulence2); | |
| filter2.appendChild(this.displacement2); | |
| defs.appendChild(filter2); | |
| this.svg.appendChild(defs); | |
| document.body.appendChild(this.svg); | |
| // Apply filters | |
| this.video.style.filter += ' url(#turbulence-displace)'; | |
| this.canvas.style.filter = 'url(#turbulence-canvas)'; | |
| } | |
| toggle() { | |
| this.enabled = !this.enabled; | |
| if (!this.enabled) { | |
| this.canvas.style.filter = ''; | |
| this.canvas.style.transform = ''; | |
| this.video.style.filter = ''; | |
| this.video.style.transform = 'scaleX(-1)'; | |
| } else { | |
| this.video.style.filter = 'url(#turbulence-displace)'; | |
| this.canvas.style.filter = 'url(#turbulence-canvas)'; | |
| } | |
| console.log('Turbulence displacement: ' + (this.enabled ? 'ON' : 'OFF')); | |
| } | |
| update(amplitude) { | |
| if (!this.enabled) return; | |
| this.time += 0.016; | |
| // Throttle SVG DOM updates to ~10fps to prevent layout thrashing | |
| this._frameCount = (this._frameCount || 0) + 1; | |
| if (this._frameCount % 6 !== 0) return; | |
| var amp = Math.max(0.15, amplitude || 0); | |
| // Animate "Evolution" — change the noise seed over time | |
| var seed = Math.floor(this.time * 1.5) % 1000; | |
| this.turbulence.setAttribute('seed', String(seed)); | |
| this.turbulence2.setAttribute('seed', String(seed + 500)); | |
| // Animate "Offset Turbulence" — shift the noise pattern's base frequency | |
| // Subtle variation creates the "breathing" organic feel | |
| var freqX = 0.008 + Math.sin(this.time * 0.3) * 0.003; | |
| var freqY = 0.006 + Math.cos(this.time * 0.25) * 0.002; | |
| this.turbulence.setAttribute('baseFrequency', freqX + ' ' + freqY); | |
| // Audio-reactive Amount — MAX STRENGTH displacement | |
| var videoScale = 20 + amp * 30; // 20-50 range (always visible, big on audio) | |
| var canvasScale = 14 + amp * 22; // 14-36 range | |
| this.displacement.setAttribute('scale', String(videoScale)); | |
| this.displacement2.setAttribute('scale', String(canvasScale)); | |
| // Smoke/wave distortion — sinusoidal waves rippling the canvas | |
| var breathScale = 1.0 + 0.01 * Math.sin(this.time * 0.7); | |
| var waveSkewX = Math.sin(this.time * 0.4) * 0.8; | |
| var waveSkewY = Math.cos(this.time * 0.35) * 0.5; | |
| var waveX = Math.sin(this.time * 0.6) * 3; | |
| var waveY = Math.cos(this.time * 0.5) * 2; | |
| this.canvas.style.transform = | |
| 'scale(' + breathScale + ') ' + | |
| 'skew(' + waveSkewX + 'deg, ' + waveSkewY + 'deg) ' + | |
| 'translate(' + waveX + 'px, ' + waveY + 'px)'; | |
| this.video.style.transform = | |
| 'scaleX(-1) scale(' + breathScale + ') ' + | |
| 'skew(' + (-waveSkewX * 0.6) + 'deg, ' + (-waveSkewY * 0.6) + 'deg) ' + | |
| 'translate(' + (-waveX * 0.5) + 'px, ' + (-waveY * 0.5) + 'px)'; | |
| } | |
| dispose() { | |
| this.canvas.style.filter = ''; | |
| this.canvas.style.transform = ''; | |
| this.video.style.filter = ''; | |
| this.video.style.transform = 'scaleX(-1)'; | |
| if (this.svg && this.svg.parentNode) { | |
| this.svg.parentNode.removeChild(this.svg); | |
| } | |
| } | |
| } | |