hyperspace-jam / DisplacementFilter.js
Solshine's picture
Upload folder using huggingface_hub
90b9d4f verified
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);
}
}
}