

if(!window._impulseGyro){window._impulseGyro=(function(){var granted=false,listeners=[],requested=false,needsPerm=typeof DeviceOrientationEvent!=='undefined'&&typeof DeviceOrientationEvent.requestPermission==='function';function activate(){if(granted)return;granted=true;window._impulseGyro.granted=true;for(var i=0;i<listeners.length;i++)window.addEventListener('deviceorientation',listeners[i],{passive:true});}function reqGesture(){if(requested)return;requested=true;document.addEventListener('touchstart',function f(){document.removeEventListener('touchstart',f);DeviceOrientationEvent.requestPermission().then(function(s){if(s==='granted')activate();}).catch(function(){});},{once:true,passive:true});}return{granted:false,register:function(fn){listeners.push(fn);if(granted)window.addEventListener('deviceorientation',fn,{passive:true});else if(needsPerm)reqGesture();else if(typeof DeviceOrientationEvent!=='undefined')activate();},unregister:function(fn){window.removeEventListener('deviceorientation',fn);var i=listeners.indexOf(fn);if(i!==-1)listeners.splice(i,1);}};})();}
(function() {
'use strict';

var _autoQuality=1.0;
(function(){
    var isMobile='ontouchstart' in window||navigator.maxTouchPoints>0;
    var isLowEnd=navigator.hardwareConcurrency&&navigator.hardwareConcurrency<=4;
    var isSmallScreen=window.innerWidth<=768;
    if(isMobile||isSmallScreen)_autoQuality=0.5;
    else if(isLowEnd)_autoQuality=0.75;
})();
var _fxQualityOverrides={dot_matrix:0.85,cellular:0.85,grid_pulse:0.85};

var VERT = `attribute vec2 a_pos;void main(){gl_Position=vec4(a_pos,0.,1.);}`;

var SHADERS = {};

 SHADERS.caustics = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_intensity;
uniform float u_opacity;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec2 u_mouse;
uniform float u_mouse_strength;

// Ridge function: 1 - abs(sin) produces SHARP peaks where sin crosses ±1
// (the bright caustic lines) and 0 where sin = 0 (between lines). Two
// orthogonal ridges multiplied → focused points/streaks of light.
float ridges(vec2 p, float t) {
    float rx = 1.0 - abs(sin(p.x * 2.0 + t * 0.5));
    float ry = 1.0 - abs(cos(p.y * 2.0 - t * 0.3));
    return rx * ry;
}

// Iterated domain warp — each layer twists the coords by the previous
// layer's gradient direction, so the ridges curve organically instead of
// reading as a square grid. 4 layers strikes a balance between detail
// and shader cost.
float caustic(vec2 uv, float t, float density) {
    vec2 p = uv * density;
    float a = 0.0;
    for (int i = 0; i < 4; i++) {
        float fi = float(i);
        // Warp using sin/cos of orthogonal axes — produces curved
        // light paths rather than the parallel rails plain ridge gives.
        p += vec2(sin(p.y * 1.3 + t * 0.4 + fi),
                  cos(p.x * 1.3 - t * 0.3 + fi)) * 0.45;
        a += ridges(p, t + fi * 0.7) * (0.5 / (1.0 + fi * 0.5));
        p *= 1.55;
    }
    return a;
}

void main() {
    vec2 uv = gl_FragCoord.xy / u_res;
    float ar = u_res.x / u_res.y;
    // Aspect-correct UV so the ridge pattern doesn't stretch on wide screens.
    vec2 uvAR = vec2((uv.x - 0.5) * ar + 0.5, uv.y);
    float t = u_time * u_speed * 0.35;

    // Mouse pull — stronger than original; creates a focal swirl.
    vec2 mDiff = uvAR - vec2((u_mouse.x - 0.5) * ar + 0.5, u_mouse.y);
    float mDist = length(mDiff);
    float mPull = exp(-mDist * mDist * 2.0) * u_mouse_strength * 0.25;
    uvAR += mDiff * mPull;

    // Density driven by intensity — sparser ridges at low intensity feel
    // like deep water; denser ridges at high intensity feel like a shallow
    // sun-dappled pool surface.
    float density = 2.5 + u_intensity * 5.5;

    // Two cross-fading passes at slightly different scales + offsets
    // produce the constant "light dancing" feel water caustics have, vs
    // the static "frozen" look of a single pass.
    float a1 = caustic(uvAR, t, density);
    float a2 = caustic(uvAR * 1.35 + 0.3, t * 0.75 + 2.0, density * 0.85);
    float a  = mix(a1, a2, 0.45);

    // Sharpen the ridges so the peaks pop and the troughs settle dark.
    a = pow(a * 1.6, 2.2);

    // Dark water base — tinted toward u_color1 so the "shadow" between
    // caustics still feels like water of that hue, not generic black.
    vec3 waterBase = u_color1 * 0.08;
    // Mid colour layer — picks up at low caustic intensity.
    vec3 col = mix(waterBase, u_color1, smoothstep(0.0, 0.45, a));
    // Top colour — bright second palette stop picks up at the ridge peaks.
    col = mix(col, u_color2, smoothstep(0.40, 0.95, a));
    // White highlight bloom on the very brightest ridges — gives the
    // effect a wet-light shimmer rather than a flat colour mix.
    float hot = max(0.0, a - 0.7);
    col += vec3(1.0) * pow(hot, 2.5) * 0.9;

    gl_FragColor = vec4(col * u_opacity, u_opacity);
}
`; 

 SHADERS.fractal = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_intensity;
uniform float u_opacity;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec2 u_mouse;
uniform float u_mouse_strength;

float julia(vec2 z, vec2 c) {
    float iter = 0.0;
    for (int i = 0; i < 128; i++) {
        z = vec2(z.x*z.x - z.y*z.y, 2.0*z.x*z.y) + c;
        if (dot(z, z) > 4.0) break;
        iter += 1.0;
    }
    if (dot(z, z) <= 4.0) return -1.0;
    // Smooth colouring with log escape
    return iter - log2(log2(dot(z, z))) + 4.0;
}

void main() {
    float zoom = 3.0 * exp(-u_intensity * 3.0) + 0.3;
    float t = u_time * u_speed * 0.25;
    vec2 cBase = vec2(
        -0.745 + sin(t * 0.11) * 0.06 + cos(t * 0.07) * 0.04,
         0.186 + cos(t * 0.09) * 0.05 + sin(t * 0.13) * 0.03
    );
    vec2 mOff = (u_mouse - 0.5) * u_mouse_strength * 0.25;
    vec2 c = cBase + mOff;

    // 2x2 supersampling — average 4 sub-pixel samples to reduce aliasing noise
    vec3 col = vec3(0.0);
    float valid = 0.0;
    for (int sx = 0; sx < 2; sx++) {
        for (int sy = 0; sy < 2; sy++) {
            vec2 offset = (vec2(float(sx), float(sy)) - 0.5) / u_res;
            vec2 uv = (gl_FragCoord.xy + offset - u_res * 0.5) / min(u_res.x, u_res.y) * zoom;
            float f = julia(uv, c);
            vec3 sc;
            if (f < 0.0) {
                sc = u_color1 * 0.08;
            } else {
                // Smooth gradient between colors based on iteration count
                float t = 0.5 + 0.5 * sin(f * 0.15);
                sc = mix(u_color1, u_color2, t);
            }
            col += sc;
            valid += 1.0;
        }
    }
    col /= valid;
    gl_FragColor = vec4(col, u_opacity);
}
`; 

 SHADERS.bubble = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_intensity;
uniform float u_opacity;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec2 u_mouse;
uniform float u_mouse_strength;

vec3 background(vec2 uv, float t) {
    // Mouse shifts the gradient across the background
    vec2 mShift = (u_mouse - 0.5) * u_mouse_strength * 0.6;
    vec2 shifted = uv + mShift;
    float n = sin(shifted.x*2.5+t*0.2)*cos(shifted.y*3.0-t*0.15)*0.5+0.5;
    // Secondary detail layer — fine ripple pattern
    float n2 = sin(shifted.x*6.0+shifted.y*4.0+t*0.3)*0.5+0.5;
    float blend = n*0.75 + n2*0.25;
    return mix(u_color1, u_color2, blend);
}

// Thin-film interference color from viewing angle
vec3 thinFilm(float cosTheta, float thickness) {
    // Optical path difference creates wavelength-dependent interference
    float delta = 2.0 * thickness * cosTheta;
    vec3 col;
    col.r = 0.5 + 0.5 * cos(delta * 12.0 + 0.0);
    col.g = 0.5 + 0.5 * cos(delta * 12.0 + 2.094);
    col.b = 0.5 + 0.5 * cos(delta * 12.0 + 4.189);
    return col;
}

float deformedSDF(vec3 p, float r, float t) {
    float base = length(p) - r;
    float def = sin(p.x*1.5+t*2.5)*cos(p.y*1.2-t*2.0)*0.25*r;
    def += sin(p.z*1.8+t*1.8)*cos(p.x*1.3+t*1.5)*0.15*r;
    def += sin(p.y*2.0+p.z*1.5+t*3.0)*0.1*r;
    return base + def;
}

void main() {
    vec2 uv = (gl_FragCoord.xy - u_res*0.5) / min(u_res.x, u_res.y);
    float t = u_time * u_speed * 0.3;
    // Subtle auto-drift for center
    vec2 center = vec2(sin(t*0.4)*0.06, cos(t*0.6)*0.04);
    float bubbleR = 0.3 + u_intensity * 0.6;
    // Mouse rotates view around bubble
    float mx = (u_mouse.x-0.5)*u_mouse_strength*1.5;
    float my = (u_mouse.y-0.5)*u_mouse_strength*1.2;
    float camAngleY = t*0.2 + mx;
    float camAngleX = my*0.6;
    vec3 ro = vec3(sin(camAngleY)*2.5, sin(camAngleX)*1.5, cos(camAngleY)*2.5);
    vec3 target = vec3(center, 0.0);
    vec3 fwd = normalize(target - ro);
    vec3 right = normalize(cross(fwd, vec3(0.0,1.0,0.0)));
    vec3 up = cross(right, fwd);
    vec3 rd = normalize(fwd + uv.x*right + uv.y*up);
    vec3 sc = target;
    // Raymarch deforming bubble
    float d = 0.0;
    bool hit = false;
    vec3 hitP;
    // Track closest approach for edge softening
    float minDist = 999.0;
    for (int i = 0; i < 48; i++) {
        vec3 p = ro + rd * d - sc;
        float h = deformedSDF(p, bubbleR, t);
        minDist = min(minDist, h);
        if (h < 0.001) { hit = true; hitP = p; break; }
        if (d > 8.0) break;
        d += max(h, 0.002);
    }
    // Edge softness — smooth transition at bubble boundary
    float edgeFade = 1.0 - smoothstep(0.0, 0.015, minDist);
    vec3 col = background(uv, t);
    if (hit) {
        // Normal from deformed SDF — matches actual surface
        float e = 0.002;
        vec3 p = hitP;
        vec3 n = normalize(vec3(
            deformedSDF(p+vec3(e,0,0),bubbleR,t)-deformedSDF(p-vec3(e,0,0),bubbleR,t),
            deformedSDF(p+vec3(0,e,0),bubbleR,t)-deformedSDF(p-vec3(0,e,0),bubbleR,t),
            deformedSDF(p+vec3(0,0,e),bubbleR,t)-deformedSDF(p-vec3(0,0,e),bubbleR,t)
        ));
        float cosTheta = abs(dot(-rd, n));
        float thickness = 0.5 + 0.3 * sin(n.x*4.0+t*2.0) * cos(n.y*3.0-t*1.5)
                        + 0.15 * sin(n.z*5.0+n.x*3.0+t*3.0);
        vec3 iridescent = thinFilm(cosTheta, thickness);
        // Pure thin-film iridescence — background colors reflect through refraction, no extra color bake
        vec3 refracted = refract(rd, n, 0.97);
        vec2 refUV = uv + refracted.xy * 0.15;
        vec3 through = background(refUV, t);
        vec3 reflected = reflect(rd, n);
        vec3 refl = background(uv + reflected.xy * 0.4, t + 1.5) * 0.5;
        float fresnel = pow(1.0 - cosTheta, 4.0);
        vec3 surface = mix(iridescent * 0.3, refl, fresnel * 0.6);
        col = mix(through, surface, 0.3 + fresnel * 0.5);
        vec3 L = normalize(vec3(0.5, 0.8, 1.0));
        float spec = pow(max(dot(reflect(-L,n),-rd),0.0), 120.0);
        col += vec3(spec) * 0.6;
        vec3 L2 = normalize(vec3(-0.4, -0.2, 0.8));
        float spec2 = pow(max(dot(reflect(-L2,n),-rd),0.0), 60.0);
        col += vec3(0.2,0.25,0.3) * spec2 * 0.2;
    }
    if (!hit) {
        // Outside bubble — pure background, soft blend at edge
        col = mix(col, background(uv, t), 1.0 - edgeFade);
    }
    // Fully opaque canvas — no page body bleed
    gl_FragColor = vec4(col, 1.0);
}`; 

 SHADERS.flow = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_intensity;
uniform float u_opacity;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec3 u_color3;
uniform vec2 u_mouse;
uniform float u_mouse_strength;

float hash(vec2 p){return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);}

float noise(vec2 p){
    vec2 i = floor(p);
    vec2 f = fract(p);
    f = f*f*(3.0 - 2.0*f);
    return mix(mix(hash(i),            hash(i + vec2(1,0)), f.x),
               mix(hash(i + vec2(0,1)), hash(i + vec2(1,1)), f.x), f.y);
}

float fbm(vec2 p){
    float v = 0.0, a = 0.5;
    for (int i = 0; i < 4; i++) {
        v += a * noise(p);
        p *= 2.1;
        a *= 0.5;
    }
    return v;
}

void main(){
    vec2 uv = gl_FragCoord.xy / u_res;
    float ar = u_res.x / u_res.y;
    vec2 p = vec2((uv.x - 0.5) * ar + 0.5, uv.y) * 2.2;
    float t = u_time * u_speed * 0.06;

    // Mouse — adds a vortex (perpendicular swirl) to the flow field at
    // the cursor location. Falls off radially so the rest of the canvas
    // is untouched.
    vec2 mPos = vec2((u_mouse.x - 0.5) * ar + 0.5, u_mouse.y) * 2.2;
    vec2 mDiff = p - mPos;
    float mDist = length(mDiff);
    vec2 mVortex = vec2(-mDiff.y, mDiff.x) * exp(-mDist * mDist * 2.0) * u_mouse_strength * 1.4;

    // Self-warping flow field — 3 passes. Each pass advects the next
    // sample by the previous pass's accumulated gradient. The result is
    // organic curling streams rather than the static billowy blob fbm
    // produces on its own. Strength scales with u_intensity so the user
    // can dial from gentle drift to vigorous swirl.
    float strength = 0.4 + u_intensity * 1.2;
    vec2 warp = vec2(0.0);
    float density = 0.0;
    for (int i = 0; i < 3; i++) {
        float fi = float(i);
        warp.x = fbm(p + warp * strength + vec2(0.0, t + fi * 1.7) + mVortex);
        warp.y = fbm(p + warp * strength + vec2(t * 0.9 + fi * 2.3, 0.0) + mVortex);
        density += (warp.x + warp.y - 1.0);
    }
    density = density * 0.4 + 0.5;

    // Multi-band palette traversal — fract(density) cycles through three
    // dedicated palette stops so multiple colour boundaries appear as the
    // streams flow, rather than a flat C1→C2 lerp. Color3 is its OWN
    // swatch in the panel (independent of page bg), so the user gets full
    // control of the highlight tint.
    float band = fract(density * 1.8 + t * 0.4);
    vec3 col;
    if (band < 0.5)  col = mix(u_color1, u_color2, band * 2.0);
    else             col = mix(u_color2, u_color3, (band - 0.5) * 2.0);

    // Stream highlight — where the warp gradient is steepest, the flow
    // is concentrated; light up those ridges with a near-white bloom
    // so the rendering reads as bright fluid against a dark ground.
    float grad = abs(warp.x - warp.y);
    float streak = pow(grad, 4.0) * 2.5;
    col += mix(u_color2, vec3(1.0), 0.4) * streak * 0.35;

    // Atmospheric base tint — keeps the rendering from going pure black
    // in the dim regions between currents. Multiplied by an inverse-
    // density factor so it only shows where the streams are sparse.
    col += u_color1 * 0.10 * (1.0 - density);

    // Calm tonemap — keeps the streaks bright but never blows out into
    // pure white, so text overlays remain legible.
    col = col / (col + vec3(0.35)) * 1.4;

    gl_FragColor = vec4(col * u_opacity, u_opacity);
}`; 

 SHADERS.aura = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_intensity;
uniform float u_opacity;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec2 u_mouse;
uniform float u_mouse_strength;

vec2 hash2(vec2 p){
    p=vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3)));
    return fract(sin(p)*43758.5453)*2.0-1.0;
}
float noise(vec2 p){
    vec2 i=floor(p);vec2 f=fract(p);
    vec2 u=f*f*(3.0-2.0*f);
    return mix(mix(dot(hash2(i),f),dot(hash2(i+vec2(1,0)),f-vec2(1,0)),u.x),
               mix(dot(hash2(i+vec2(0,1)),f-vec2(0,1)),dot(hash2(i+vec2(1,1)),f-vec2(1,1)),u.x),u.y);
}
float fbm(vec2 p){
    float v=0.0;float a=0.5;
    mat2 rot=mat2(0.8,0.6,-0.6,0.8);
    for(int i=0;i<4;i++){v+=a*noise(p);p=rot*p*2.0;a*=0.5;}
    return v;
}

void main(){
    vec2 uv=gl_FragCoord.xy/u_res;
    float t=u_time*u_speed*2.5;

    // Noise-warp the UV space — organic distortion
    float warpStr=0.06+u_intensity*0.08;
    vec2 warp=vec2(
        fbm(uv*3.0+vec2(t*0.15,-t*0.1)),
        fbm(uv*3.0+vec2(t*0.1,t*0.12)+5.0)
    );
    vec2 warped=uv+warp*warpStr;

    // Mouse attracts nearest blob — pull UV toward cursor
    vec2 mDiff=uv-u_mouse;
    float mDist=length(mDiff);
    float mPull=exp(-mDist*mDist*3.0)*u_mouse_strength*0.15;
    warped-=mDiff*mPull;

    // 5 blobs with varied speeds and organic edges
    float sharp=1.5+u_intensity*2.0;

    // Blob 1 — slow, large
    vec2 b1=vec2(0.25+sin(t*0.25)*0.35,0.35+cos(t*0.2)*0.35);
    float d1=length(warped-b1);
    d1+=fbm(warped*4.0+t*0.2)*0.08;
    float r1=exp(-d1*d1*sharp);

    // Blob 2 — medium speed
    vec2 b2=vec2(0.75+cos(t*0.35)*0.3,0.65+sin(t*0.4)*0.3);
    float d2=length(warped-b2);
    d2+=fbm(warped*5.0-t*0.15+3.0)*0.07;
    float r2=exp(-d2*d2*(sharp*1.1));

    // Blob 3 — faster, smaller
    vec2 b3=vec2(0.5+sin(t*0.55)*0.4,0.2+cos(t*0.45)*0.35);
    float d3=length(warped-b3);
    d3+=fbm(warped*4.5+t*0.25+7.0)*0.06;
    float r3=exp(-d3*d3*(sharp*1.3));

    // Blob 4 — fast, small
    vec2 b4=vec2(0.35+cos(t*0.65)*0.3,0.8+sin(t*0.55)*0.25);
    float d4=length(warped-b4);
    d4+=fbm(warped*5.5-t*0.3+11.0)*0.05;
    float r4=exp(-d4*d4*(sharp*1.5));

    // Blob 5 — mouse-following accent
    vec2 b5=u_mouse+vec2(sin(t*0.8)*0.05,cos(t*0.6)*0.05);
    float d5=length(warped-b5);
    float r5=exp(-d5*d5*(sharp*2.0))*u_mouse_strength*0.6;

    // Colors — spread across palette with more variation
    vec3 c1=u_color1*1.15;
    vec3 c2=u_color2*1.15;
    vec3 c3=mix(u_color1,u_color2,0.3)*1.1;
    vec3 c4=mix(u_color2,u_color1,0.3)*1.1;
    vec3 c5=mix(u_color1,u_color2,0.5)*1.3;

    // Weighted blend
    float total=r1+r2+r3+r4+r5+0.001;
    vec3 col=(c1*r1+c2*r2+c3*r3+c4*r4+c5*r5)/total;

    // Overlap luminance — where blobs intersect, glow brighter
    float overlap=r1*r2+r2*r3+r3*r4+r1*r4+r1*r3;
    col+=mix(u_color1,u_color2,0.5)*overlap*0.4;

    // Subtle organic grain
    col*=0.95+fbm(uv*8.0+t*0.03)*0.1;

    // Saturation boost
    float lum=dot(col,vec3(0.299,0.587,0.114));
    col=mix(vec3(lum),col,1.25);

    gl_FragColor=vec4(col*u_opacity,1.0);
}`; 

 SHADERS.topography = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_intensity;
uniform float u_opacity;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec2 u_mouse;
uniform float u_mouse_strength;

float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}
float noise(vec2 p){
    vec2 i=floor(p);vec2 f=fract(p);f=f*f*(3.0-2.0*f);
    return mix(mix(hash(i),hash(i+vec2(1,0)),f.x),mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x),f.y);
}
float fbm(vec2 p){float v=0.0;float a=0.5;for(int i=0;i<5;i++){v+=a*noise(p);p*=2.0;a*=0.5;}return v;}

void main(){
    vec2 uv=gl_FragCoord.xy/u_res;
    float aspect=u_res.x/u_res.y;
    // Normalize UV so pattern isn't squeezed on portrait/mobile screens
    vec2 uvAspect=vec2((uv.x-0.5)*(aspect>1.0?aspect:1.0)+0.5,(uv.y-0.5)*(aspect<1.0?1.0/aspect:1.0)+0.5);
    float t=u_time*u_speed*0.15;
    // Mouse warps terrain locally — creates a bump under cursor
    vec2 mDiff=uv-u_mouse;
    float mDist=length(mDiff);
    float mWarp=exp(-mDist*mDist*12.0)*u_mouse_strength*0.3;
    // Terrain elevation
    float elev=fbm(uvAspect*3.0+vec2(t*0.1,-t*0.08));
    elev+=fbm(uvAspect*6.0+vec2(-t*0.05,t*0.12))*0.3;
    elev+=mWarp;
    // Contour lines — multiple elevation bands
    float lines=8.0+u_intensity*12.0;
    float contour=fract(elev*lines);
    // Sharp contour edges
    float lineWidth=0.03+u_intensity*0.01;
    float line=smoothstep(lineWidth,0.0,contour)+smoothstep(1.0-lineWidth,1.0,contour);
    // Color by elevation
    vec3 bgCol=mix(u_color1*0.15,u_color2*0.15,elev);
    vec3 lineCol=mix(u_color1,u_color2,elev)*0.7;
    // Higher elevations slightly brighter
    lineCol*=0.6+elev*0.6;
    vec3 col=mix(bgCol,lineCol,line*0.8);
    // Highlight highest contours
    float peak=smoothstep(0.65,0.8,elev);
    col+=mix(u_color1,u_color2,0.7)*peak*line*0.2;
    gl_FragColor=vec4(col*u_opacity,1.0);
}`; 

 SHADERS.aurora = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_intensity;
uniform float u_opacity;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec3 u_bgColor;
uniform vec2 u_mouse;
uniform float u_mouse_strength;

float hash(float n){return fract(sin(n)*43758.5453);}

float noise(vec2 p){
    vec2 i=floor(p);vec2 f=fract(p);
    f=f*f*(3.0-2.0*f);
    float n=i.x+i.y*57.0;
    return mix(mix(hash(n),hash(n+1.0),f.x),mix(hash(n+57.0),hash(n+58.0),f.x),f.y);
}

float fbm(vec2 p){
    float v=0.0;float a=0.5;
    for(int i=0;i<3;i++){v+=a*noise(p);p*=2.0;a*=0.5;}
    return v;
}

float verticalRays(vec2 uv, float t, float seed){
    float cols=noise(vec2(uv.x*13.7+seed, t*0.04));
    cols=pow(cols, 1.8);
    float drift=noise(vec2(uv.x*7.3+seed*0.5, t*0.06+1.3));
    return mix(cols, drift, 0.35);
}

// One aurora band: lower curtain, band A color, slower drift
float bandA(vec2 uv, float t, float mShift, float mLift){
    float aurora=0.0;
    for(int i=0;i<2;i++){
        float fi=float(i);
        float freq=2.2+fi*1.6+u_intensity*1.8;
        float wave=sin(uv.x*freq+t*(0.35+fi*0.10)+mShift*freq)*0.5+0.5;
        float n=fbm(vec2(uv.x*freq*0.5+fi*3.0,uv.y*2.0-t*0.25));
        float curtain=smoothstep(0.02,0.45,uv.y+mLift)*smoothstep(0.90,0.30,uv.y+mLift);
        float ray=pow(wave,2.5+fi*0.5)*curtain*n;
        float heightVar=fbm(vec2(uv.x*freq+fi*5.0,t*0.07));
        ray*=smoothstep(0.0,heightVar*0.8+0.15,uv.y);
        // Vertical distortion: multiply by ray columns so glow breaks into fingers
        float vRay=verticalRays(uv,t,fi*3.7);
        ray*=0.3+0.7*vRay;
        aurora+=ray*(0.48-fi*0.08);
    }
    return aurora;
}

// Second aurora band: higher curtain, band B color, faster, different ray structure
float bandB(vec2 uv, float t, float mShift, float mLift){
    float aurora=0.0;
    for(int i=0;i<2;i++){
        float fi=float(i);
        float freq=3.1+fi*2.0+u_intensity*2.2;
        float wave=sin(uv.x*freq+t*(0.55+fi*0.15)+mShift*freq*0.8+2.1)*0.5+0.5;
        float n=fbm(vec2(uv.x*freq*0.4+fi*7.3+5.0,uv.y*1.8-t*0.38+3.0));
        float curtain=smoothstep(0.08,0.55,uv.y+mLift+0.08)*smoothstep(0.98,0.42,uv.y+mLift+0.08);
        float ray=pow(wave,3.0+fi*0.4)*curtain*n;
        float heightVar=fbm(vec2(uv.x*freq+fi*4.5+11.0,t*0.10+2.0));
        ray*=smoothstep(0.0,heightVar*0.7+0.2,uv.y+0.05);
        // Different column frequency for band B — finer fingers
        float vRay=verticalRays(uv,t,fi*5.1+11.0);
        ray*=0.25+0.75*vRay;
        aurora+=ray*(0.42-fi*0.07);
    }
    return aurora;
}

// Third band — highest, narrowest, driven by C3 swatch (u_bgColor repurposed)
float bandC(vec2 uv, float t, float mShift, float mLift){
    float freq=4.2+u_intensity*2.5;
    float wave=sin(uv.x*freq+t*0.72+mShift*freq*0.6+4.3)*0.5+0.5;
    float n=fbm(vec2(uv.x*freq*0.3+19.0,uv.y*2.2-t*0.48+7.0));
    float curtain=smoothstep(0.18,0.65,uv.y+mLift+0.12)*smoothstep(0.99,0.55,uv.y+mLift+0.12);
    float ray=pow(wave,3.5)*curtain*n;
    float heightVar=fbm(vec2(uv.x*freq+23.0,t*0.12+5.0));
    ray*=smoothstep(0.0,heightVar*0.6+0.25,uv.y+0.1);
    float vRay=noise(vec2(uv.x*13.7+23.3,t*0.04));
    vRay=pow(vRay,1.8);
    ray*=0.2+0.8*vRay;
    return ray*0.38;
}

// Depth perspective — center recedes slightly, edges come forward.
// Scales UV toward center proportional to distance from center.
// Subtle: bands stay straight, just sample slightly compressed near center.
vec2 concaveWarp(vec2 uv, float strength){
    vec2 c=uv-0.5;
    float scale=1.0-strength*(1.0-length(c)*1.8);
    return 0.5+c*scale;
}
void main(){
    vec2 uv=gl_FragCoord.xy/u_res;
    float t=u_time*u_speed*0.3;
    float tStar=u_time*2.0;
    float aspect=u_res.x/u_res.y;
    bool isMobile=aspect<1.0;
    float mShift=(u_mouse.x-0.5)*u_mouse_strength*0.4;
    float mLift=(u_mouse.y-0.5)*u_mouse_strength*0.15;

    // Concave warp applied to base UV — then X-only parallax added per band
    // Warp first (from screen center), then shift horizontally — no cross-axis distortion
    float mxShift=(u_mouse.x-0.5)*u_mouse_strength;
    vec2 uvBase=concaveWarp(uv, 0.07);
    vec2 uvA=uvBase+vec2(mxShift*0.040,0.0);
    vec2 uvB=uvBase+vec2(mxShift*0.022,0.0);
    vec2 uvC=uvBase+vec2(mxShift*0.010,0.0);

    float a1=bandA(uvA,t,mShift,0.0);
    float a2=isMobile?0.0:bandB(uvB,t,mShift,0.0);
    float a3=isMobile?0.0:bandC(uvC,t,mShift,0.0);

    a1*=0.9+0.1*sin(t*0.8);
    a2*=0.9+0.1*sin(t*1.1+1.3);
    a3*=0.85+0.15*sin(t*1.4+2.7);

    float totalAurora=a1+a2+a3;
    vec3 skyBase=mix(vec3(0.01,0.01,0.03),vec3(0.01,0.04,0.03),uv.y*0.6);
    vec3 sky=skyBase+vec3(0.0,0.015,0.008)*totalAurora*1.5;

    vec3 col=sky
        +u_color1*(a1*1.8+a1*a1*2.2)
        +u_color2*(a2*1.6+a2*a2*2.0)
        +u_bgColor*(a3*1.7+a3*a3*2.1);

    if(!isMobile){
        // Aspect-correct UV for star sampling — without this, the square
        // UV grid produces rectangular cells on a wide canvas, stretching
        // every star into an oval. Multiplying x by aspect makes grid
        // cells square in pixel space, so distance-to-centre is isotropic
        // and stars render circular. Sergei: "stars are stretched, so
        // they are not rounded".
        vec2 starUV=uv+u_mouse*u_mouse_strength*0.008;
        vec2 starUVar=vec2(starUV.x*aspect,starUV.y);
        for(int layer=0;layer<3;layer++){
            float fl=float(layer);
            float scale=150.0+fl*120.0;
            vec2 grid=floor(starUVar*scale);
            // Threshold hash — only high values become stars
            float h=fract(sin(dot(grid,vec2(127.1+fl*31.0,311.7+fl*17.0)))*43758.5453);
            float threshold=0.993+fl*0.003;
            if(h>threshold){
                vec2 center=grid+vec2(fract(sin(dot(grid,vec2(269.5,183.3)))*43758.5453),fract(sin(dot(grid,vec2(419.2,371.9)))*43758.5453));
                float d=length(starUVar*scale-center);
                // Independent property hash — NOT derived from threshold hash h
                // so colors/sizes are spread across full 0-1 range
                float ph=fract(sin(dot(grid,vec2(43.57+fl*19.3,71.31+fl*7.9)))*29547.1453);
                float ph2=fract(ph*3.7);
                float ph3=fract(ph*5.3);
                float ph4=fract(ph*11.1);
                float starSize=0.12+ph2*0.38;
                float brightness=smoothstep(starSize,0.0,d);
                // Flicker: independent time, speed 1.5–5.0, amount 0.35–0.75
                float flickSpeed=1.5+ph3*3.5;
                float flickAmt=0.35+ph4*0.40;
                brightness*=(1.0-flickAmt)+flickAmt*(sin(tStar*flickSpeed+ph*200.0+fl*3.0)*0.5+0.5);
                float starOpacity=0.35+ph2*0.65;
                // Colors spread across full ph range
                float temp=ph;
                vec3 starCol=temp<0.15?vec3(0.7,0.8,1.0):
                             temp<0.40?vec3(0.9,0.95,1.0):
                             temp<0.65?vec3(1.0,0.98,0.88):
                             temp<0.82?vec3(1.0,0.92,0.7):
                             temp<0.93?vec3(1.0,0.78,0.5):
                                       vec3(1.0,0.6,0.5);
                col+=starCol*brightness*starOpacity*(0.5-fl*0.1)*max(0.15,1.0-totalAurora*2.0);
            }
        }
    }

    gl_FragColor=vec4(col*u_opacity,1.0);
}`; 

 SHADERS.clouds = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_intensity;
uniform float u_opacity;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec3 u_bgColor;
uniform vec2 u_mouse;
uniform float u_mouse_strength;

float hash(float n){return fract(sin(n)*43758.5453);}

float noise(vec2 p){
    vec2 i=floor(p);vec2 f=fract(p);
    f=f*f*(3.0-2.0*f);
    float n=i.x+i.y*57.0;
    return mix(mix(hash(n),hash(n+1.0),f.x),mix(hash(n+57.0),hash(n+58.0),f.x),f.y);
}

float fbm(vec2 p){
    float v=0.0;float a=0.5;
    for(int i=0;i<3;i++){v+=a*noise(p);p*=2.0;a*=0.5;}
    return v;
}

float verticalRays(vec2 uv, float t, float seed){
    float cols=noise(vec2(uv.x*13.7+seed, t*0.04));
    cols=pow(cols, 1.8);
    float drift=noise(vec2(uv.x*7.3+seed*0.5, t*0.06+1.3));
    return mix(cols, drift, 0.35);
}

// One aurora band: lower curtain, band A color, slower drift
float bandA(vec2 uv, float t, float mShift, float mLift){
    float aurora=0.0;
    for(int i=0;i<2;i++){
        float fi=float(i);
        float freq=2.2+fi*1.6+u_intensity*1.8;
        float wave=sin(uv.x*freq+t*(0.35+fi*0.10)+mShift*freq)*0.5+0.5;
        float n=fbm(vec2(uv.x*freq*0.5+fi*3.0,uv.y*2.0-t*0.25));
        float curtain=smoothstep(0.02,0.45,uv.y+mLift)*smoothstep(0.90,0.30,uv.y+mLift);
        float ray=pow(wave,2.5+fi*0.5)*curtain*n;
        float heightVar=fbm(vec2(uv.x*freq+fi*5.0,t*0.07));
        ray*=smoothstep(0.0,heightVar*0.8+0.15,uv.y);
        // Vertical distortion: multiply by ray columns so glow breaks into fingers
        float vRay=verticalRays(uv,t,fi*3.7);
        ray*=0.3+0.7*vRay;
        aurora+=ray*(0.48-fi*0.08);
    }
    return aurora;
}

// Second aurora band: higher curtain, band B color, faster, different ray structure
float bandB(vec2 uv, float t, float mShift, float mLift){
    float aurora=0.0;
    for(int i=0;i<2;i++){
        float fi=float(i);
        float freq=3.1+fi*2.0+u_intensity*2.2;
        float wave=sin(uv.x*freq+t*(0.55+fi*0.15)+mShift*freq*0.8+2.1)*0.5+0.5;
        float n=fbm(vec2(uv.x*freq*0.4+fi*7.3+5.0,uv.y*1.8-t*0.38+3.0));
        float curtain=smoothstep(0.08,0.55,uv.y+mLift+0.08)*smoothstep(0.98,0.42,uv.y+mLift+0.08);
        float ray=pow(wave,3.0+fi*0.4)*curtain*n;
        float heightVar=fbm(vec2(uv.x*freq+fi*4.5+11.0,t*0.10+2.0));
        ray*=smoothstep(0.0,heightVar*0.7+0.2,uv.y+0.05);
        // Different column frequency for band B — finer fingers
        float vRay=verticalRays(uv,t,fi*5.1+11.0);
        ray*=0.25+0.75*vRay;
        aurora+=ray*(0.42-fi*0.07);
    }
    return aurora;
}

// Third band — highest, narrowest, driven by C3 swatch (u_bgColor repurposed)
float bandC(vec2 uv, float t, float mShift, float mLift){
    float freq=4.2+u_intensity*2.5;
    float wave=sin(uv.x*freq+t*0.72+mShift*freq*0.6+4.3)*0.5+0.5;
    float n=fbm(vec2(uv.x*freq*0.3+19.0,uv.y*2.2-t*0.48+7.0));
    float curtain=smoothstep(0.18,0.65,uv.y+mLift+0.12)*smoothstep(0.99,0.55,uv.y+mLift+0.12);
    float ray=pow(wave,3.5)*curtain*n;
    float heightVar=fbm(vec2(uv.x*freq+23.0,t*0.12+5.0));
    ray*=smoothstep(0.0,heightVar*0.6+0.25,uv.y+0.1);
    float vRay=noise(vec2(uv.x*13.7+23.3,t*0.04));
    vRay=pow(vRay,1.8);
    ray*=0.2+0.8*vRay;
    return ray*0.38;
}

// Depth perspective — center recedes slightly, edges come forward.
// Scales UV toward center proportional to distance from center.
// Subtle: bands stay straight, just sample slightly compressed near center.
vec2 concaveWarp(vec2 uv, float strength){
    vec2 c=uv-0.5;
    float scale=1.0-strength*(1.0-length(c)*1.8);
    return 0.5+c*scale;
}
void main(){
    vec2 uv=gl_FragCoord.xy/u_res;
    float t=u_time*u_speed*0.3;
    float aspect=u_res.x/u_res.y;
    bool isMobile=aspect<1.0;
    float mShift=(u_mouse.x-0.5)*u_mouse_strength*0.4;
    float mLift=(u_mouse.y-0.5)*u_mouse_strength*0.15;

    // Concave warp from screen center, then X-only parallax per band
    float mxShift=(u_mouse.x-0.5)*u_mouse_strength;
    vec2 uvBase=concaveWarp(uv, 0.07);
    vec2 uvA=uvBase+vec2(mxShift*0.040,0.0);
    vec2 uvB=uvBase+vec2(mxShift*0.022,0.0);
    vec2 uvC=uvBase+vec2(mxShift*0.010,0.0);

    float a1=bandA(uvA,t,mShift,0.0);
    float a2=isMobile?0.0:bandB(uvB,t,mShift,0.0);
    float a3=isMobile?0.0:bandC(uvC,t,mShift,0.0);

    a1*=0.9+0.1*sin(t*0.8);
    a2*=0.9+0.1*sin(t*1.1+1.3);
    a3*=0.85+0.15*sin(t*1.4+2.7);

    // White sky — mix from white toward the band color where bands are present
    float presence=clamp(0.5+u_intensity*1.5, 0.5, 5.0);
    float m1=clamp(a1*3.5*presence,0.0,1.0);
    float m2=clamp(a2*3.0*presence,0.0,1.0);
    float m3=clamp(a3*2.8*presence,0.0,1.0);
    vec3 col=vec3(1.0);
    col=mix(col,u_color1,m1);
    col=mix(col,u_color2,m2);
    col=mix(col,u_bgColor,m3);
    gl_FragColor=vec4(col,1.0);
}`; 

 SHADERS.neon = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_intensity;
uniform float u_opacity;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec2 u_mouse;
uniform float u_mouse_strength;

float hash(float n){return fract(sin(n)*43758.5453);}

void main(){
    vec2 uv=gl_FragCoord.xy/u_res;
    float aspect=u_res.x/u_res.y;
    float t=u_time*u_speed*0.3;
    // Perspective center shifts with mouse
    float vpX=0.5+(u_mouse.x-0.5)*u_mouse_strength*0.15;
    vec3 col=vec3(0.0);

    // 28 tubes across multiple depth layers
    for(int i=0;i<28;i++){
        float fi=float(i);
        float seed=hash(fi*13.7);
        float seed2=hash(fi*29.3+7.0);

        // Z depth: scrolls toward viewer, wraps around
        float zCycle=fract(seed+t*0.12*(0.6+seed2*0.8));
        float z=0.05+zCycle*1.2;
        float proj=1.0/z;

        // World-space X position: spread across view
        float worldX=(seed2-0.5)*2.4;
        float screenX=vpX+(worldX-vpX+0.5)*proj*0.08;

        float dist=abs((uv.x-screenX)*aspect);

        // Tube width: MUCH thicker — near tubes very fat
        float baseWidth=0.006+0.022*(1.0-zCycle);
        float width=baseWidth*proj*0.14;
        width=clamp(width,0.002,0.1);

        // Core: bright hot center — wider transition for softer edge
        float core=smoothstep(width,width*0.08,dist);
        // Inner glow: strong, wide halo
        float glow=exp(-dist*dist/(width*width*8.0))*0.7;
        // Mid bloom: rich colored light spill
        float midBloom=exp(-dist*dist/(width*width*35.0))*0.35;
        // Outer bloom: atmospheric wash
        float bloom=exp(-dist*dist/(width*width*120.0))*0.18;

        // Depth-based alpha
        float depthAlpha=smoothstep(0.0,0.08,zCycle)*(0.1+(1.0-zCycle)*0.9);
        float alpha=(core+glow+midBloom+bloom)*depthAlpha;

        // Color per tube
        float hue=fract(fi*0.0618+seed*0.5);
        vec3 tubeCol=mix(u_color1,u_color2,hue);
        // Saturated glow layer
        vec3 glowCol=tubeCol*1.3;
        // Hot white core blending
        vec3 coreCol=mix(glowCol,vec3(1.0),core*0.8);

        col+=coreCol*alpha;
    }

    col*=u_intensity*1.6;
    col=min(col,vec3(1.2));
    // Tone-map to prevent hard clipping
    col=col/(col+vec3(0.15));
    col*=1.15;
    gl_FragColor=vec4(col*u_opacity,u_opacity);
}`; 

 SHADERS.glass = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_intensity;
uniform float u_opacity;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec3 u_bgColor;
uniform vec2 u_mouse;
uniform float u_mouse_strength;
uniform sampler2D u_bgImage;
uniform float u_hasBgImage;
uniform float u_bgAspect; // image width/height, 0 if no image
uniform float u_shape;
uniform float u_outerOpacity; // 0..1 — opacity of the procedural overlay outside the glass shape

// SDF primitives
float sdSphere(vec3 p,float r){return length(p)-r;}
float sdOctahedron(vec3 p,float s){p=abs(p);return(p.x+p.y+p.z-s)*0.577;}
float sdRoundBox(vec3 p,vec3 b,float r){vec3 q=abs(p)-b;return length(max(q,0.0))+min(max(q.x,max(q.y,q.z)),0.0)-r;}
float sdTorus(vec3 p,vec2 t){vec2 q=vec2(length(p.xz)-t.x,p.y);return length(q)-t.y;}
float sdTriPrism(vec3 p,vec2 h){vec3 q=abs(p);return max(q.z-h.y,max(q.x*0.866+p.y*0.5,-p.y)-h.x*0.5);}
float sdEllipsoid(vec3 p,vec3 r){float k0=length(p/r);float k1=length(p/(r*r));return k0*(k0-1.0)/k1;}

float sceneSDF(vec3 p){
    float sz=0.5+u_intensity*1.5;
    p/=sz;
    float d;
    if(u_shape<0.5)d=sdSphere(p,0.35);
    else if(u_shape<1.5)d=sdOctahedron(p,0.42);
    else if(u_shape<2.5)d=sdRoundBox(p,vec3(0.26),0.03);
    else if(u_shape<3.5)d=sdTorus(p,vec2(0.3,0.1));
    else if(u_shape<4.5)d=sdTriPrism(p,vec2(0.38,0.22));
    else d=sdEllipsoid(p,vec3(0.42,0.26,0.26));
    return d*sz;
}

vec3 sceneNormal(vec3 p){
    float e=0.001;
    return normalize(vec3(
        sceneSDF(p+vec3(e,0,0))-sceneSDF(p-vec3(e,0,0)),
        sceneSDF(p+vec3(0,e,0))-sceneSDF(p-vec3(0,e,0)),
        sceneSDF(p+vec3(0,0,e))-sceneSDF(p-vec3(0,0,e))
    ));
}

mat3 rotY(float a){float c=cos(a),s=sin(a);return mat3(c,0,s,0,1,0,-s,0,c);}
mat3 rotX(float a){float c=cos(a),s=sin(a);return mat3(1,0,0,0,c,-s,0,s,c);}
mat3 trans3(mat3 m){return mat3(m[0][0],m[1][0],m[2][0],m[0][1],m[1][1],m[2][1],m[0][2],m[1][2],m[2][2]);}

// Procedural background: fine grid + soft gradient so refraction is always visible
vec3 procBg(vec2 uv){
    // Base gradient from colors
    float n=sin(uv.x*3.5)*cos(uv.y*2.8)*0.5+0.5;
    vec3 base=mix(u_color1*0.75,u_color2,n);
    // Fine grid lines — visible through refraction
    vec2 grid=fract(uv*18.0);
    float line=1.0-smoothstep(0.0,0.04,min(grid.x,grid.y));
    // Subtle dot pattern
    vec2 dots=fract(uv*9.0)-0.5;
    float dot=1.0-smoothstep(0.12,0.2,length(dots));
    return base + line*0.12 + dot*0.08;
}

vec3 getBg(vec2 uv){
    if(u_hasBgImage>0.5){
        // Flip V — WebGL origin is bottom-left
        vec2 fuv=vec2(uv.x,1.0-uv.y);
        // Cover: scale UV so image fills canvas preserving aspect ratio
        float canvasAspect=u_res.x/u_res.y;
        float imgAspect=u_bgAspect;
        vec2 scale=vec2(1.0,1.0);
        if(imgAspect>canvasAspect){
            // Image wider than canvas — fit height, crop sides
            scale.x=canvasAspect/imgAspect;
        } else {
            // Image taller than canvas — fit width, crop top/bottom
            scale.y=imgAspect/canvasAspect;
        }
        fuv=(fuv-0.5)*scale+0.5;
        fuv=clamp(fuv,0.0,1.0);
        return texture2D(u_bgImage,fuv).rgb;
    }
    return procBg(uv);
}

void main(){
    vec2 uv=gl_FragCoord.xy/u_res;
    vec2 p=(gl_FragCoord.xy-u_res*0.5)/min(u_res.x,u_res.y);
    float t=u_time*u_speed*0.3;

    // Mouse: (1) shifts sphere position as a lens moving across the background
    //         (2) tilts rotation like holding a prism
    vec2 mouseOff=(u_mouse-0.5)*u_mouse_strength;
    vec2 center=vec2(sin(t*0.4)*0.04,cos(t*0.6)*0.03)+mouseOff*0.35;

    float mx=(u_mouse.x-0.5)*u_mouse_strength*4.0;
    float my=(u_mouse.y-0.5)*u_mouse_strength*3.5;
    float idleY=t*0.3;
    float idleX=t*0.2;
    mat3 rot=rotY(idleY+mx)*rotX(idleX+my);

    // Ray setup
    vec3 ro=vec3(0.0,0.0,2.2);
    vec3 rd=normalize(vec3(p-center,-1.3));

    // Raymarch
    float d=0.0;
    bool hit=false;
    vec3 hitP;
    for(int i=0;i<64;i++){
        vec3 pos=ro+rd*d;
        vec3 rp=rot*(pos-vec3(center,0.0));
        float h=sceneSDF(rp);
        if(h<0.001){hit=true;hitP=rp;break;}
        if(d>5.0)break;
        d+=max(h,0.001);
    }

    vec3 bg=getBg(uv);

    if(hit){
        vec3 n=sceneNormal(hitP);
        n=normalize(trans3(rot)*n);
        float cosTheta=abs(dot(-rd,n));

        // Chromatic dispersion — strong enough to be clearly visible
        float disp=0.06+u_intensity*0.16;
        float baseIOR=0.72;
        vec3 refR=refract(rd,n,baseIOR-disp);
        vec3 refG=refract(rd,n,baseIOR);
        vec3 refB=refract(rd,n,baseIOR+disp);

        if(length(refR)<0.01)refR=reflect(rd,n);
        if(length(refG)<0.01)refG=reflect(rd,n);
        if(length(refB)<0.01)refB=reflect(rd,n);

        // Strong refraction offset — glass visibly bends and magnifies background
        float refStrength=0.7+u_intensity*0.5;
        vec3 col;
        col.r=getBg(uv+refR.xy*refStrength).r;
        col.g=getBg(uv+refG.xy*refStrength).g;
        col.b=getBg(uv+refB.xy*refStrength).b;

        // Fresnel — thick glass rim reflection
        float fresnel=pow(1.0-cosTheta,3.5);
        vec3 refl=getBg(uv+reflect(rd,n).xy*0.4);
        col=mix(col,refl,fresnel*0.65);

        // Sharp specular highlights — glass, not plastic
        vec3 L1=normalize(vec3(0.4,0.8,1.0));
        float spec1=pow(max(dot(reflect(-L1,n),-rd),0.0),300.0);
        vec3 L2=normalize(vec3(-0.5,0.3,-0.8));
        float spec2=pow(max(dot(reflect(-L2,n),-rd),0.0),120.0);
        col+=vec3(spec1)*0.9+vec3(0.2,0.25,0.4)*spec2*0.35;

        // Prominent rainbow edge from dispersion
        float edge=pow(1.0-cosTheta,2.0)*disp*10.0;
        col+=vec3(edge*1.0,edge*0.4,edge*1.4)*0.45;

        // Always-visible rim — additive Fresnel-shaped white pop so the
        // sphere outline reads against any background (including same-
        // image both sides where refraction matches the bg).
        float rim = pow(1.0 - cosTheta, 2.5);
        col += vec3(rim) * 0.6;

        gl_FragColor=vec4(col*u_opacity,u_opacity);
    } else {
        // Outer area: solid u_bgColor at u_outerOpacity. 0 → transparent
        // (depth-parallax canvas behind shows through). 100 → opaque page
        // bg colour. The sphere's internal refraction still samples
        // u_bgImage / procBg via getBg(uv) — that's a separate layer.
        float oa = u_outerOpacity * u_opacity;
        gl_FragColor = vec4(u_bgColor * oa, oa);
    }
}`; 

SHADERS.noise = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_intensity;
uniform float u_opacity;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec2 u_mouse;
uniform float u_mouse_strength;

vec3 mod289(vec3 x){return x-floor(x/289.0)*289.0;}
vec2 mod289(vec2 x){return x-floor(x/289.0)*289.0;}
vec3 permute(vec3 x){return mod289((x*34.0+1.0)*x);}

float snoise(vec2 v){
    const vec4 C=vec4(0.211324865,0.366025404,-0.577350269,0.024390244);
    vec2 i=floor(v+dot(v,C.yy));
    vec2 x0=v-i+dot(i,C.xx);
    vec2 i1=x0.x>x0.y?vec2(1.0,0.0):vec2(0.0,1.0);
    vec2 x1=x0-i1+C.xx;
    vec2 x2=x0+C.zz;
    i=mod289(i);
    vec3 p=permute(permute(i.y+vec3(0.0,i1.y,1.0))+i.x+vec3(0.0,i1.x,1.0));
    vec3 m=max(0.5-vec3(dot(x0,x0),dot(x1,x1),dot(x2,x2)),0.0);
    m=m*m;m=m*m;
    vec3 x=2.0*fract(p*C.www)-1.0;
    vec3 h=abs(x)-0.5;
    vec3 a0=x-floor(x+0.5);
    m*=1.79284291-0.85373473*(a0*a0+h*h);
    vec3 g;
    g.x=a0.x*x0.x+h.x*x0.y;
    g.y=a0.y*x1.x+h.y*x1.y;
    g.z=a0.z*x2.x+h.z*x2.y;
    return 130.0*dot(m,g);
}

void main(){
    vec2 uv=gl_FragCoord.xy/u_res;
    float ar=u_res.x/u_res.y;
    float t=u_time*u_speed*0.2;
    vec2 p=vec2(uv.x*ar,uv.y)*3.0;

    // Mouse injects energy into the noise domain — creates a local turbulence vortex
    vec2 mPos=vec2(u_mouse.x*ar,u_mouse.y)*3.0;
    float mDist=length(p-mPos);
    float mInfluence=exp(-mDist*mDist*0.8)*u_mouse_strength;
    // Swirl around mouse — rotate noise coordinates near cursor
    float swirl=mInfluence*2.0;
    float sAngle=atan(p.y-mPos.y,p.x-mPos.x)+swirl;
    float sDist=mDist;
    vec2 swirlOffset=vec2(cos(sAngle),sin(sAngle))*sDist-p+mPos;
    p+=swirlOffset*mInfluence*0.5;
    // Mouse also injects extra octaves of turbulence locally
    float mTurb=snoise(p*4.0+t*0.5)*mInfluence*0.8;

    // Layered noise — domain warping for marble look
    float n1=snoise(p+t*0.3);
    float n2=snoise(p*2.0+vec2(n1+mTurb)*0.5+t*0.2);
    float n3=snoise(p*4.0+vec2(n2)*0.3-t*0.15);
    float v=n1*0.5+n2*0.35+n3*0.15;
    v=v*u_intensity*1.5;

    float m=v*0.5+0.5;
    m=smoothstep(0.1,0.9,m);
    vec3 col=mix(u_color1,u_color2,m);
    // Subtle vein highlights intensified near mouse
    float vein=1.0-smoothstep(0.0,0.06+mInfluence*0.04,abs(n2-0.1));
    col=mix(col,u_color2*1.3,vein*(0.3+mInfluence*0.4));
    gl_FragColor=vec4(col*u_opacity,u_opacity);
}`;

 SHADERS.dots = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_intensity;
uniform float u_opacity;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec3 u_bgColor;
uniform vec2 u_mouse;
uniform float u_mouse_strength;

float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}

float noise(vec2 p){
    vec2 i=floor(p);vec2 f=fract(p);
    f=f*f*(3.0-2.0*f);
    float a=hash(i);float b=hash(i+vec2(1.0,0.0));
    float c=hash(i+vec2(0.0,1.0));float d=hash(i+vec2(1.0,1.0));
    return mix(mix(a,b,f.x),mix(c,d,f.x),f.y);
}

float fbm(vec2 p){
    float v=0.0;float a=0.5;
    for(int i=0;i<4;i++){v+=a*noise(p);p*=2.0;a*=0.5;}
    return v;
}

void main(){
    vec2 uv=gl_FragCoord.xy/u_res;
    float ar=u_res.x/u_res.y;
    float t=u_time*u_speed*0.15;

    // Grid
    float gridSize=20.0+u_intensity*30.0;
    vec2 grid=vec2(uv.x*ar,uv.y)*gridSize;
    vec2 cell=floor(grid);
    vec2 f=fract(grid)-0.5;
    float d=length(f);

    // Flowing noise field underneath — determines dot size
    vec2 noiseP=cell/gridSize*3.0;
    float n=fbm(noiseP+t*0.3);
    float n2=fbm(noiseP*1.5-t*0.2+5.0);
    float field=n*0.6+n2*0.4;

    // Mouse creates local brightness swell
    vec2 mPos=vec2(u_mouse.x*ar,u_mouse.y);
    vec2 cellUV=(cell+0.5)/gridSize;
    float mDist=length(cellUV-mPos);
    float mSwell=exp(-mDist*mDist*4.0)*u_mouse_strength*0.5;
    field+=mSwell;

    // Dot radius from field value
    float radius=0.1+field*0.38;
    radius=clamp(radius,0.05,0.48);
    float dotMask=1.0-smoothstep(radius-0.04,radius,d);

    // Color: dots colored, background from color1
    vec3 dotCol=mix(u_color1,u_color2,field);
    vec3 col=mix(u_bgColor,dotCol,dotMask);

    // Full opacity — canvas must be opaque so u_bgColor isn't composited with page bg
    gl_FragColor=vec4(col,1.0);
}`; 

 SHADERS.ripple = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_intensity;
uniform float u_opacity;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec2 u_mouse;
uniform float u_mouse_strength;

void main(){
    vec2 uv=gl_FragCoord.xy/u_res;
    float ar=u_res.x/u_res.y;
    vec2 st=vec2(uv.x*ar,uv.y);
    float t=u_time*u_speed*0.4;

    // Multiple auto-ripple sources
    float v=0.0;
    for(int i=0;i<5;i++){
        float fi=float(i);
        vec2 center=vec2(
            0.3*ar+sin(t*0.2+fi*1.7)*0.3*ar,
            0.3+cos(t*0.15+fi*2.3)*0.3
        );
        float d=length(st-center);
        float rings=sin(d*20.0*u_intensity-t*3.0+fi*1.5)*0.5+0.5;
        float fade=exp(-d*d*2.0);
        v+=rings*fade*(0.4-fi*0.05);
    }

    // Mouse ripple — strongest, centered on cursor
    vec2 mPos=vec2(u_mouse.x*ar,u_mouse.y);
    float mDist=length(st-mPos);
    float mRings=sin(mDist*30.0*u_intensity-t*5.0)*0.5+0.5;
    float mFade=exp(-mDist*mDist*1.5);
    v+=mRings*mFade*u_mouse_strength*1.5;

    // Surface normal from height for subtle lighting
    float eps=0.003;
    float vx=v;// approximate — skip recompute for perf
    float bright=0.7+v*0.5;

    vec3 col=mix(u_color1,u_color2,v);
    col*=bright;
    gl_FragColor=vec4(col*u_opacity,u_opacity);
}`; 

 SHADERS.swirl = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_intensity;
uniform float u_opacity;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec3 u_bgColor;
uniform vec2 u_mouse;
uniform float u_mouse_strength;

float hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453); }

// 2D star field — sparse circular points, varied size + per-star twinkle.
// Grid the uv into cells; each cell either contains a star (low chance)
// or is empty. The star inside a cell sits at a randomised sub-cell
// position. Distance to that star drives a smoothstep circle so the
// resulting pixel looks like a soft point of light, not a square cell.
// Twinkle is a slow sin keyed on a per-cell phase.
float starField(vec2 uv, float t){
    vec2 g  = uv * 90.0;                     // ≈ 90 cells across the width
    vec2 ci = floor(g);
    vec2 cp = fract(g);
    // Four independent hashes per cell — existence / x / y / size+phase.
    float h0 = hash(ci);
    float h1 = hash(ci + vec2(1.7, 0.0));
    float h2 = hash(ci + vec2(0.0, 1.7));
    float h3 = hash(ci + vec2(1.7, 1.7));
    // Only ~6% of cells host a star. (1.0 - 0.94 = 0.06)
    float hasStar = step(0.94, h0);
    if (hasStar < 0.5) return 0.0;
    vec2 starPos = vec2(0.15 + h1 * 0.7, 0.15 + h2 * 0.7);   // keep off cell edges
    float d = distance(cp, starPos);
    // Star size varies by cell — small majority, occasional bright giants.
    float size = 0.04 + h3 * 0.08;
    // Smooth circular falloff — bright centre, soft edge.
    float core = smoothstep(size, 0.0, d);
    // Twinkle — slow per-star sin, never fully off so stars don't blink out.
    float twinkle = 0.55 + 0.45 * sin(t * 1.5 + h3 * 6.28318);
    return core * twinkle;
}

void main(){
    vec2 uv=gl_FragCoord.xy/u_res;
    float ar=u_res.x/u_res.y;
    vec2 center=vec2(0.5*ar,0.5);
    vec2 p=vec2(uv.x*ar,uv.y)-center;
    float t=u_time*u_speed*0.3;

    // Mouse shifts vortex centre — cursor pulls the eye of the storm.
    vec2 mOff=(u_mouse-vec2(0.5))*u_mouse_strength*0.3;
    p-=vec2(mOff.x*ar,mOff.y);

    float r=length(p);
    float angle=atan(p.y,p.x);

    // Intensity (slider 1..20 → 0.1..2.0) is the master "drama" knob:
    //  - controls how tight the spiral winds near the centre,
    //  - controls how many arms appear (3 at low, 9 at high),
    //  - sharpens the bright bands so high intensity reads dense + crisp.
    float intensity=clamp(u_intensity,0.1,2.0);
    float twist=intensity*6.0/(r*8.0+1.0);
    angle+=twist+t;

    // Mouse adds local extra twist near cursor — slight tornado follow.
    float mDist=length(vec2(uv.x*ar,uv.y)-vec2(u_mouse.x*ar,u_mouse.y));
    angle+=exp(-mDist*mDist*4.0)*u_mouse_strength*3.0;

    // Spiral arms — count scales with intensity, two harmonics layered.
    float armCount=3.0+floor(intensity*3.0);          // 3..9 arms
    float armA=sin(angle*armCount+r*12.0-t*2.0)*0.5+0.5;
    float armB=sin(angle*(armCount+2.0)-r*8.0+t*1.5)*0.5+0.5;
    float arms=armA*0.6+armB*0.4;

    // Sharpen — pulls bright peaks out, leaves dark troughs untouched.
    // Higher intensity = crisper bands; low intensity = soft milky swirl.
    float sharpen=1.0+intensity*2.0;
    arms=pow(arms,sharpen);

    // Wider, smoother radial fade — the previous 0.6*ar cutoff produced
    // a hard rim. 0.65*ar with smoothstep gives a gentle vignette.
    float fade=1.0-smoothstep(0.0,0.65*ar,r);

    // Bright glowing core — small white-hot disk at the eye of the
    // vortex. Size scales subtly with intensity so a calm swirl has a
    // small core and a dramatic one has a hotter centre.
    float coreSize=0.04+intensity*0.02;
    float core=exp(-(r*r)/(coreSize*coreSize));

    // Star field — circular soft points with per-star twinkle, layered
    // over the swirl. Faded toward the rim so the cosmic feel sits
    // inside the vortex and the corners stay clean. uv*ar normalises
    // x for the aspect ratio so cells stay square on wide viewports.
    float stars=starField(vec2(uv.x*ar, uv.y), u_time)*fade*0.85;

    // Layered colour build:
    //   bg (everywhere) →
    //   color1 mixed in where arms are visible (faded toward rim) →
    //   color2 in the brightest peaks →
    //   white at the core (additive-ish via mix toward 1.0) →
    //   plus star sparkles.
    float armsLit=arms*fade;
    float t1=clamp(armsLit*1.5,0.0,1.0);
    float t2=clamp(armsLit*1.8-0.5,0.0,1.0);
    vec3 col=u_bgColor;
    col=mix(col,u_color1,t1);
    col=mix(col,u_color2,t2);
    col=mix(col,vec3(1.0),core*0.9);
    col+=vec3(stars);

    gl_FragColor=vec4(col*u_opacity,u_opacity);
}`; 

 SHADERS.nebula = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_intensity;
uniform float u_opacity;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec3 u_color3;
uniform vec3 u_bgColor;
uniform vec2 u_mouse;
uniform float u_mouse_strength;

float nHash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453); }
float vNoise(vec2 p){
    vec2 i=floor(p); vec2 f=fract(p);
    f=f*f*(3.0-2.0*f);
    return mix(mix(nHash(i),nHash(i+vec2(1,0)),f.x),
               mix(nHash(i+vec2(0,1)),nHash(i+vec2(1,1)),f.x), f.y);
}
float fbm5(vec2 p){
    float v=0.0, a=0.5;
    for(int i=0;i<5;i++){ v+=a*vNoise(p); p*=2.02; a*=0.5; }
    return v;
}

// Star scatter — twin of swirl's starField but inlined for self-containment.
float nebulaStars(vec2 uv, float t){
    vec2 g  = uv * 90.0;
    vec2 ci = floor(g);
    vec2 cp = fract(g);
    float h0 = nHash(ci);
    float h1 = nHash(ci + vec2(1.7, 0.0));
    float h2 = nHash(ci + vec2(0.0, 1.7));
    float h3 = nHash(ci + vec2(1.7, 1.7));
    if (step(0.94, h0) < 0.5) return 0.0;
    vec2 sp = vec2(0.15 + h1*0.7, 0.15 + h2*0.7);
    float d = distance(cp, sp);
    float size = 0.04 + h3 * 0.08;
    float core = smoothstep(size, 0.0, d);
    float tw = 0.55 + 0.45 * sin(t*1.5 + h3*6.28318);
    return core * tw;
}

void main(){
    vec2 uv = gl_FragCoord.xy / u_res;
    float ar = u_res.x / u_res.y;
    // Aspect-correct space.
    vec2 p = vec2((uv.x - 0.5) * ar, uv.y - 0.5);
    float t = u_time * u_speed * 0.06;             // very slow — cosmic timescale

    // Intensity 0.1..2.0 → cloud density + edge sharpness.
    float ity = clamp(u_intensity, 0.1, 2.0);
    float density = 0.4 + ity * 0.5;               // 0.45..1.4
    float sharpen = 1.0 + ity * 1.5;               // 1.15..4.0

    // Mouse INTERACTS WITH the noise field instead of panning it. We
    // compute a localised warp vector that gently pushes the noise
    // coordinates away from / toward the cursor, with a Gaussian
    // falloff so the disturbance fades out a short distance from the
    // pointer. The dust lanes near the cursor visibly bend and curl
    // around it; far-away regions stay untouched.
    vec2 mPos   = vec2((u_mouse.x - 0.5) * ar, u_mouse.y - 0.5);
    vec2 mDelta = p - mPos;
    float mDist = length(mDelta);
    vec2 mWarp  = normalize(mDelta + vec2(1e-5)) * exp(-mDist*mDist*0.6) * u_mouse_strength * 0.6;

    // Domain warp — two-pass fbm with mouse-warped input. First pass
    // builds a flow field; second pass reads the noise WARPED by that
    // field; final fbm reads the result. Mouse delta enters at every
    // layer so the disturbance compounds and produces real swirling
    // motion around the cursor instead of a flat translation.
    vec2 q = vec2( fbm5(p*1.5 + t*0.7 + mWarp),
                   fbm5(p*1.5 + vec2(5.2,1.3) + t*0.7 + mWarp) );
    vec2 r = vec2( fbm5(p*1.5 + 3.2*q + vec2(1.7, 9.2) + t*0.4 + mWarp*0.5),
                   fbm5(p*1.5 + 3.2*q + vec2(8.3, 2.8) + t*0.4 + mWarp*0.5) );
    float n = fbm5(p*1.5 + 4.0*r);

    // Push contrast — pow brings out bright peaks, leaves dark voids.
    float cloud = pow(clamp(n*density, 0.0, 1.0), sharpen);

    // Centre-weight slightly so the brightest cloud sits roughly mid-frame.
    float centreFade = 1.0 - smoothstep(0.0, 0.7, length(p));
    cloud *= 0.5 + 0.5 * centreFade;

    // Hot spots — small bright points where the noise crests (young stars
    // forming inside the cloud). Use a tighter falloff above a threshold.
    float hot = smoothstep(0.72, 0.92, n);
    hot = pow(hot, 3.0);

    // Star scatter overlay — fades out toward the corners.
    float starFade = 1.0 - smoothstep(0.0, 0.85, length(p));
    float stars = nebulaStars(uv * vec2(ar, 1.0), u_time) * (0.4 + starFade * 0.6);

    // Layered colour build:
    //   bg →
    //   color1 mixed in by cloud density →
    //   color2 in the brightest crests →
    //   color3 at hot spots / star nurseries (was hardcoded white) →
    //   plus star sparkles in color3.
    // The shader's 4-colour palette now matches the panel's 4 swatches
    // exactly: B → bg void, C1 → mid clouds, C2 → bright cloud peaks,
    // C3 → starlight inside / on top of the nebula.
    vec3 col = u_bgColor;
    col = mix(col, u_color1, clamp(cloud * 1.4, 0.0, 1.0));
    col = mix(col, u_color2, clamp(cloud * 1.8 - 0.5, 0.0, 1.0));
    col = mix(col, u_color3, hot * 0.85);
    col += u_color3 * stars;

    gl_FragColor = vec4(col * u_opacity, u_opacity);
}`; 

 SHADERS.kaleidoscope = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_intensity;
uniform float u_opacity;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec2 u_mouse;
uniform float u_mouse_strength;

float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}
float noise(vec2 p){
    vec2 i=floor(p);vec2 f=fract(p);
    f=f*f*(3.0-2.0*f);
    return mix(mix(hash(i),hash(i+vec2(1,0)),f.x),mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x),f.y);
}

void main(){
    vec2 uv=gl_FragCoord.xy/u_res;
    float ar=u_res.x/u_res.y;
    vec2 p=vec2((uv.x-0.5)*ar,uv.y-0.5)*2.0;
    float t=u_time*u_speed*0.25;

    // Polar coordinates
    float r=length(p);
    float a=atan(p.y,p.x);

    // Mouse rotates the entire kaleidoscope slowly
    a+=(u_mouse.x-0.5)*u_mouse_strength*1.5;

    // Kaleidoscope fold — mirror into N segments
    float segments=floor(4.0+u_intensity*8.0);
    float segAngle=3.14159*2.0/segments;
    a=mod(a+t*0.3,segAngle);
    if(a>segAngle*0.5) a=segAngle-a;

    // Back to cartesian in folded space
    vec2 fp=vec2(cos(a),sin(a))*r;

    // Rich interior pattern — layered noise creates stained-glass look
    float n1=noise(fp*3.0+t*0.4);
    float n2=noise(fp*6.0-t*0.3+vec2(n1)*0.5);
    float n3=noise(fp*12.0+vec2(n2)*0.3+t*0.2);

    // Sharp geometric edges from noise thresholds
    float edge1=smoothstep(0.48,0.52,n1);
    float edge2=smoothstep(0.45,0.55,n2);
    float pattern=edge1*0.5+edge2*0.3+n3*0.2;

    // Concentric rings add radial structure
    float rings=sin(r*12.0-t*1.5)*0.5+0.5;
    rings=smoothstep(0.3,0.7,rings);
    pattern=mix(pattern,rings,0.25);

    // Mouse proximity creates a bright focal point
    vec2 mPos=vec2((u_mouse.x-0.5)*ar,(u_mouse.y-0.5))*2.0;
    float mDist=length(p-mPos);
    float mGlow=exp(-mDist*mDist*3.0)*u_mouse_strength*0.4;
    pattern+=mGlow;

    vec3 col=mix(u_color1,u_color2,clamp(pattern,0.0,1.0));
    // Radial vignette
    col*=0.7+0.3*exp(-r*r*0.5);
    gl_FragColor=vec4(col*u_opacity,u_opacity);
}`; 

 SHADERS.membrane = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_intensity;
uniform float u_opacity;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec3 u_bgColor;
uniform vec2 u_mouse;
uniform float u_mouse_strength;

float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}
float noise(vec2 p){
    vec2 i=floor(p);vec2 f=fract(p);
    f=f*f*(3.0-2.0*f);
    return mix(mix(hash(i),hash(i+vec2(1,0)),f.x),mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x),f.y);
}
float fbm(vec2 p,float octaves){
    // Fixed 4 octaves — dynamic loop break on float is not portable across WebGL implementations
    float v=0.0;float a=0.5;
    float scale=octaves*0.25; // scale amplitude by octave count
    v+=a*noise(p)*scale;p*=2.0;a*=0.5;
    v+=a*noise(p)*scale;p*=2.0;a*=0.5;
    v+=a*noise(p)*scale;p*=2.0;a*=0.5;
    v+=a*noise(p)*scale;
    return v;
}

void main(){
    vec2 uv=gl_FragCoord.xy/u_res;
    float ar=u_res.x/u_res.y;
    float t=u_time*u_speed*0.15;
    vec2 p=vec2(uv.x*ar,uv.y)*3.0;

    // Mouse injects turbulence into the noise domain — swirl near cursor
    vec2 mPos=vec2(u_mouse.x*ar,u_mouse.y)*3.0;
    vec2 toM=p-mPos;
    float mDist=length(toM);
    float mInfl=exp(-mDist*mDist*0.4)*u_mouse_strength;
    // Swirl coordinates around mouse
    float sAngle=mInfl*3.0;
    float cs=cos(sAngle),sn=sin(sAngle);
    vec2 rotated=vec2(toM.x*cs-toM.y*sn,toM.x*sn+toM.y*cs);
    p=mPos+mix(toM,rotated,mInfl);
    // Extra turbulence octave near mouse
    float mTurb=noise(p*5.0+t)*mInfl*0.6;

    // Octave count from intensity — low=smooth membrane, high=detailed tissue
    float octaves=2.0+u_intensity*4.0;
    // Domain warp strength from speed — fast=more chaotic warping
    float warpAmt=0.5+u_speed*0.5;

    // Organic displacement — domain warping (warp amount scales with speed)
    float n1=fbm(p+t*0.3,octaves);
    float n2=fbm(p+vec2(n1+mTurb)*warpAmt+t*0.2+5.0,octaves);
    float n3=fbm(p*2.0+vec2(n2)*warpAmt*0.6-t*0.1,octaves);

    // Surface height
    float h=n1*0.4+n2*0.35+n3*0.25;
    h*=u_intensity;

    // Vein network — threshold width scales inversely with intensity (more detail = finer veins)
    float veinWidth=0.06-u_intensity*0.04;
    veinWidth=max(veinWidth,0.015);
    float vein1=1.0-smoothstep(0.0,veinWidth,abs(n2-0.45));
    float vein2=1.0-smoothstep(0.0,veinWidth*0.7,abs(n3-0.5));
    float veins=max(vein1*0.5,vein2*0.3);
    // Mouse intensifies veins nearby
    veins*=1.0+mInfl*2.5;

    // Subsurface feel
    float m=h*0.5+0.5;
    m=smoothstep(0.15,0.85,m);
    vec3 col=mix(u_color1,u_color2,m);
    // Veins in darker tone
    col=mix(col,u_color1*0.5,veins);
    // Subtle pulsing glow
    col*=0.8+0.2*sin(h*6.0+t*2.0);
    // Blend membrane over bg color — canvas fully opaque
    col=mix(u_bgColor,col,u_opacity);
    gl_FragColor=vec4(col,1.0);
}`; 

 SHADERS.agate = `
precision highp float;
uniform vec2 u_res; uniform float u_time; uniform float u_speed;
uniform float u_intensity; uniform float u_opacity;
uniform vec3 u_color1; uniform vec3 u_color2; uniform vec3 u_bgColor;
uniform vec2 u_mouse; uniform float u_mouse_strength;
float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}
float noise(vec2 p){vec2 i=floor(p);vec2 f=fract(p);f=f*f*(3.0-2.0*f);return mix(mix(hash(i),hash(i+vec2(1,0)),f.x),mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x),f.y);}
float fbm(vec2 p){float v=0.0,a=0.5;for(int i=0;i<5;i++){v+=a*noise(p);p*=2.0;a*=0.5;}return v;}
vec3 thinFilm(float f){return vec3(0.5+0.5*cos(f),0.5+0.5*cos(f+2.094),0.5+0.5*cos(f+4.189));}
void main(){
  vec2 uv=gl_FragCoord.xy/u_res; float ar=u_res.x/u_res.y;
  vec2 p=vec2(uv.x*ar,uv.y); float t=u_time*u_speed*0.04;
  vec2 mPos=vec2(u_mouse.x*ar,u_mouse.y);
  float mDist=length(p-mPos);
  float mPress=u_mouse_strength*exp(-mDist*mDist*2.5)*0.38;
  vec2 q=vec2(fbm(p*1.8+vec2(0.0,0.0)),fbm(p*1.8+vec2(5.2,1.3)));
  vec2 r=vec2(fbm(p*1.8+3.5*q+vec2(1.7,9.2)+t*0.12),fbm(p*1.8+3.5*q+vec2(8.3,2.8)+t*0.08));
  vec2 warped=p+r*(0.55+u_intensity*0.5);
  float ripple=sin(mDist*22.0-t*3.0)*0.5+0.5;
  float rippleEnv=exp(-mDist*3.2)*mPress;
  warped+=normalize(p-mPos+vec2(0.001))*ripple*rippleEnv*0.18;
  float numBands=5.0+u_intensity*8.0;
  float raw=fbm(warped*2.5);
  float bandVal=fract(raw*numBands);
  float dBand=abs(bandVal-0.5)*2.0;
  float edgeMask=smoothstep(0.5,1.0,dBand);
  float filmFreq=bandVal*26.0+raw*8.0+t*0.9;
  vec3 iridescentCol=thinFilm(filmFreq);
  vec3 bandCol=mix(u_color1,u_color2,raw);
  bandCol*=0.38+dBand*0.62;
  vec3 col=mix(u_bgColor,bandCol,1.0-edgeMask*0.28);
  col=mix(col,iridescentCol*(0.7+u_intensity*0.45),edgeMask*0.88);
  col+=iridescentCol*ripple*rippleEnv*2.2;
  gl_FragColor=vec4(col*u_opacity,u_opacity);
}`; 

 SHADERS.storm = `
precision highp float;
uniform vec2 u_res; uniform float u_time; uniform float u_speed;
uniform float u_intensity; uniform float u_opacity;
uniform vec3 u_color1; uniform vec3 u_color2; uniform vec3 u_bgColor;
uniform vec2 u_mouse; uniform float u_mouse_strength;
float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}
float noise(vec2 p){vec2 i=floor(p);vec2 f=fract(p);f=f*f*(3.0-2.0*f);return mix(mix(hash(i),hash(i+vec2(1,0)),f.x),mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x),f.y);}
float fbm(vec2 p){float v=0.0,a=0.5;for(int i=0;i<4;i++){v+=a*noise(p);p*=2.0;a*=0.5;}return v;}
float dustLayer(vec2 st,float t,float xSpd,float yScale,float xOff,float zoomSpd){
  vec2 dir=st-vec2(0.5,0.5);float r=length(dir);
  vec2 zoomed=st+normalize(dir+vec2(0.001))*r*zoomSpd*t;
  vec2 p=vec2(zoomed.x*0.36-t*xSpd+xOff,zoomed.y*yScale);
  float n=fbm(p);return pow(clamp(n*2.1-0.52,0.0,1.0),2.4);
}
void main(){
  vec2 uv=gl_FragCoord.xy/u_res; float ar=u_res.x/u_res.y;
  vec2 st=vec2(uv.x*ar,uv.y); float t=u_time*u_speed*0.07;
  float ity=0.4+u_intensity*0.6;
  vec2 mDiff=st-vec2(u_mouse.x*ar,u_mouse.y);
  float mDist=length(mDiff);
  float mSwirl=exp(-mDist*mDist*1.8)*u_mouse_strength*0.35;
  vec2 sw=vec2(-mDiff.y,mDiff.x)*mSwirl;
  float far_  =dustLayer(st+sw*0.20,t,0.50,10.0,0.0, 0.18);
  float mid1  =dustLayer(st+sw*0.45,t,0.85, 6.5,3.7, 0.40);
  float mid2  =dustLayer(st+sw*0.70,t,1.35, 4.5,7.2, 0.75);
  float near_ =dustLayer(st+sw,     t,2.10, 2.8,11.1,1.20);
  float gust=0.82+0.18*sin(u_time*u_speed*0.38+1.7);
  float totalDust=far_*0.32+mid1*0.28+mid2*0.24+near_*0.16;
  totalDust=clamp(totalDust*ity*gust,0.0,1.0);
  float nearW=(mid2*0.24+near_*0.16)/(totalDust/ity+0.001);
  nearW=clamp(nearW,0.0,1.0);
  vec3 dustCol=mix(u_color2,u_color1,nearW);
  vec3 col=mix(u_bgColor,dustCol,totalDust);
  gl_FragColor=vec4(col*u_opacity,1.0);
}`; 

 SHADERS.lens = `
precision highp float;
uniform vec2 u_res; uniform float u_time; uniform float u_speed;
uniform float u_intensity; uniform float u_opacity;
uniform vec3 u_color1; uniform vec3 u_color2; uniform vec3 u_bgColor;
uniform vec2 u_mouse; uniform float u_mouse_strength;
float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}
float starField(vec2 uv,float t){
  vec2 g=uv*110.0;vec2 ci=floor(g);vec2 cp=fract(g);
  float h0=hash(ci);if(step(0.94,h0)<0.5)return 0.0;
  float h1v=hash(ci+vec2(1.7,0.0));float h2v=hash(ci+vec2(0.0,1.7));float h3v=hash(ci+vec2(1.7,1.7));
  vec2 sp=vec2(0.15+h1v*0.7,0.15+h2v*0.7);float d=distance(cp,sp);
  float size=0.03+h3v*0.07;float core=smoothstep(size,0.0,d);
  float tw=0.6+0.4*sin(t*1.4+h3v*6.2832);return core*tw;
}
void main(){
  vec2 uv=gl_FragCoord.xy/u_res; float ar=u_res.x/u_res.y;
  vec2 p=vec2((uv.x-0.5)*ar,uv.y-0.5); float t=u_time*u_speed*0.2;
  vec2 lensPos=vec2(sin(t*0.17)*0.12+cos(t*0.11)*0.08,cos(t*0.13)*0.09+sin(t*0.19)*0.05);
  lensPos+=(u_mouse-0.5)*u_mouse_strength*0.5;
  lensPos*=vec2(ar,1.0);
  vec2 delta=p-lensPos; float dist=length(delta);
  float RE=0.07+u_intensity*0.20;
  float deflect=RE*RE/max(dist,0.01);
  vec2 deflUnit=normalize(delta);
  vec2 lensedP1=p+deflUnit*deflect;
  vec2 lensedP2=p-deflUnit*deflect*0.65;
  vec2 luv1=vec2(lensedP1.x/ar+0.5,lensedP1.y+0.5);
  vec2 luv2=vec2(lensedP2.x/ar+0.5,lensedP2.y+0.5);
  float muDist=max(abs(dist-RE),0.005);
  float mu=clamp(RE/(muDist+RE*0.12)*1.5,1.0,6.0);
  float shadow=smoothstep(RE*0.45,RE*0.90,dist);
  float stars1=starField(luv1*vec2(ar,1.0),t)*mu*shadow;
  float stars2=starField(luv2*vec2(ar,1.0),t)*0.45*(1.0-shadow);
  float starsOuter=starField(uv*vec2(ar,1.0),t)*smoothstep(RE*1.2,RE*2.8,dist)*0.4;
  float allStars=stars1+stars2+starsOuter;
  float ringWidth=RE*0.22;
  float ring=exp(-pow((dist-RE)/ringWidth,2.0))*0.45;
  ring*=smoothstep(0.0,RE*0.3,dist);
  vec3 col=u_bgColor;
  col+=u_color1*allStars;
  col+=u_color2*ring;
  col+=u_color1*0.012;
  gl_FragColor=vec4(col*u_opacity,u_opacity);
}`; 

var instances = new Map();
var observer = null;

function hexToVec3(hex) {
    if(!hex||typeof hex!=='string') return [0,0,0];
    
    var rm=hex.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*[0-9.]+)?\s*\)/);
    if(rm) return [parseInt(rm[1])/255, parseInt(rm[2])/255, parseInt(rm[3])/255];
    hex = hex.replace('#', '');
    if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
    if (hex.length === 8) hex = hex.substr(0, 6); 
    return [
        parseInt(hex.substr(0, 2), 16) / 255,
        parseInt(hex.substr(2, 2), 16) / 255,
        parseInt(hex.substr(4, 2), 16) / 255
    ];
}
function hexToAlpha(hex) {
    if(!hex||typeof hex!=='string') return 1;
    var rm=hex.match(/rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*([0-9.]+)\s*\)/);
    if(rm) return Math.min(1,Math.max(0,parseFloat(rm[1])));
    var hm=hex.match(/^#([0-9a-fA-F]{8})$/);
    if(hm) return parseInt(hm[1].slice(6,8),16)/255;
    return 1;
}

function createShader(gl, type, source) {
    var shader = gl.createShader(type);
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        
        if (type === gl.FRAGMENT_SHADER && source.indexOf('precision highp') !== -1) {
            gl.deleteShader(shader);
            shader = gl.createShader(type);
            gl.shaderSource(shader, source.replace('precision highp float;', 'precision mediump float;'));
            gl.compileShader(shader);
            if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
                gl.deleteShader(shader);
                return null;
            }
            return shader;
        }
        gl.deleteShader(shader);
        return null;
    }
    return shader;
}

function initRD(){ return null; }

function createProgram(gl, vertSrc, fragSrc) {
    var vert = createShader(gl, gl.VERTEX_SHADER, vertSrc);
    var frag = createShader(gl, gl.FRAGMENT_SHADER, fragSrc);
    if (!vert || !frag) return null;
    var prog = gl.createProgram();
    gl.attachShader(prog, vert);
    gl.attachShader(prog, frag);
    gl.linkProgram(prog);
    if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
        console.error('ImpulseFX link error:', gl.getProgramInfoLog(prog));
        return null;
    }
    return prog;
}

function init(canvas, opts) {
    if (!canvas || !opts || !opts.effect) return;
    var shaderSrc = SHADERS[opts.effect];
    if (!shaderSrc) return;

    if (instances.has(canvas)) destroy(canvas);

    var quality = opts.quality || _fxQualityOverrides[opts.effect] || _autoQuality;
    var dpr = Math.min(window.devicePixelRatio || 1, 2) * quality;

    var gl = canvas.getContext('webgl', {
        alpha: true,
        premultipliedAlpha: false,
        antialias: false,
        preserveDrawingBuffer: true
    });
    if (!gl) {

        
        if (window.console) console.warn('[ImpulseFX] WebGL context unavailable for "' + opts.effect + '" — concurrent context limit likely reached.');
        return;
    }

    var program = createProgram(gl, VERT, shaderSrc);
    if (!program) return;

    var buf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);

    var aPos = gl.getAttribLocation(program, 'a_pos');
    gl.enableVertexAttribArray(aPos);
    gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);

    gl.useProgram(program);

    var uniforms = {};
    var uNames = ['u_res','u_time','u_speed','u_intensity','u_opacity','u_outerOpacity','u_color1','u_color2','u_color3','u_bgColor','u_mouse','u_mouse_strength','u_bgImage','u_hasBgImage','u_bgAspect','u_shape'];
    uNames.forEach(function(n) { uniforms[n] = gl.getUniformLocation(program, n); });

    var color1 = hexToVec3(opts.color1 || '#000000');
    var color2 = hexToVec3(opts.color2 || '#333333');

    var color3 = hexToVec3(opts.color3 || '#ffffff');
    var bgColor = hexToVec3(opts.bgColor || '#000000');
    var colorAlpha1 = hexToAlpha(opts.color1 || '#000000');
    var colorAlpha2 = hexToAlpha(opts.color2 || '#333333');
    var mouseX = 0.5, mouseY = 0.5;
    var targetMouseX = 0.5, targetMouseY = 0.5;

    var bgTexture = null;
    var hasBgImage = 0;
    var bgAspect = 1.0;
    var bgLoadGen = 0;
    function loadBgImage(url) {

        var myGen = ++bgLoadGen;
        if (!url) { hasBgImage = 0; bgAspect = 1.0; return; }
        var img = new Image();
        img.crossOrigin = 'anonymous';
        img.onload = function() {

            
            if (myGen !== bgLoadGen) return;
            if (bgTexture) gl.deleteTexture(bgTexture);
            bgTexture = gl.createTexture();
            gl.bindTexture(gl.TEXTURE_2D, bgTexture);
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
            bgAspect = img.naturalWidth / (img.naturalHeight || 1);
            hasBgImage = 1;
        };
        img.src = url;
    }
    if (opts.bgImage) loadBgImage(opts.bgImage);

    var section = canvas.parentElement;
    function onMouseMove(e) {
        var rect = section.getBoundingClientRect();
        targetMouseX = (e.clientX - rect.left) / rect.width;
        targetMouseY = 1.0 - (e.clientY - rect.top) / rect.height;
    }
    function onMouseLeave() {
        targetMouseX = 0.5;
        targetMouseY = 0.5;
    }
    if (section) {
        section.addEventListener('mousemove', onMouseMove);
        section.addEventListener('mouseleave', onMouseLeave);
        
        if (!window._impulseScreenMode) {
        section.addEventListener('touchmove', function(e) {
            if (e.touches.length === 1) {
                var rect = section.getBoundingClientRect();
                targetMouseX = (e.touches[0].clientX - rect.left) / rect.width;
                targetMouseY = 1.0 - (e.touches[0].clientY - rect.top) / rect.height;
            }
        }, {passive: true});
        section.addEventListener('touchend', onMouseLeave, {passive: true});
        }
    }

    

    

    var _fxGyroBaseB = null, _fxGyroBaseG = null;
    var _fxOnGyro = function(e) {
        if (e.beta === null && e.gamma === null) return;
        if (_fxGyroBaseB === null) { _fxGyroBaseB = e.beta || 0; _fxGyroBaseG = e.gamma || 0; }
        targetMouseX = 0.5 + Math.max(-1, Math.min(1, ((e.gamma || 0) - _fxGyroBaseG) / 8)) * 0.5;
        targetMouseY = 0.5 + Math.max(-1, Math.min(1, ((e.beta || 0) - _fxGyroBaseB) / 8)) * 0.5;
    };
    if (window._impulseGyro) window._impulseGyro.register(_fxOnGyro);

    var startTime = performance.now();
    var running = true;
    var visible = true;
    var rafId = null;

    function resize() {
        var w = canvas.clientWidth;
        var h = canvas.clientHeight;
        canvas.width = Math.floor(w * dpr);
        canvas.height = Math.floor(h * dpr);
        gl.viewport(0, 0, canvas.width, canvas.height);
    }
    resize();

    var resizeObserver = new ResizeObserver(resize);
    resizeObserver.observe(canvas);

    function render() {
        if (!running) return;
        rafId = requestAnimationFrame(render);
        if (!visible) return;

        var now = performance.now();
        var elapsed = (now - startTime) / 1000;

        mouseX += (targetMouseX - mouseX) * 0.05;
        mouseY += (targetMouseY - mouseY) * 0.05;

        gl.useProgram(program);
        gl.uniform2f(uniforms.u_res, canvas.width, canvas.height);
        gl.uniform1f(uniforms.u_time, elapsed);
        gl.uniform1f(uniforms.u_speed, opts.speed != null ? opts.speed : 1.0);
        gl.uniform1f(uniforms.u_intensity, opts.intensity != null ? opts.intensity : 0.5);

        

        gl.uniform1f(uniforms.u_opacity, opts.opacity != null ? opts.opacity : 0.6);
        gl.uniform3fv(uniforms.u_color1, color1);
        gl.uniform3fv(uniforms.u_color2, color2);
        if (uniforms.u_color3 !== null) gl.uniform3fv(uniforms.u_color3, color3);
        if (uniforms.u_bgColor !== null) gl.uniform3fv(uniforms.u_bgColor, bgColor);
        gl.uniform2f(uniforms.u_mouse, mouseX, mouseY);
        gl.uniform1f(uniforms.u_mouse_strength, opts.mouse != null ? opts.mouse : 0.5);

        if (uniforms.u_shape !== null) gl.uniform1f(uniforms.u_shape, opts.shape || 0);
        if (uniforms.u_outerOpacity !== null) gl.uniform1f(uniforms.u_outerOpacity, opts.outerOpacity != null ? opts.outerOpacity : 1.0);
        if (uniforms.u_hasBgImage !== null) gl.uniform1f(uniforms.u_hasBgImage, hasBgImage);
        if (uniforms.u_bgAspect !== null) gl.uniform1f(uniforms.u_bgAspect, bgAspect);
        if (bgTexture && uniforms.u_bgImage !== null) {
            gl.activeTexture(gl.TEXTURE0);
            gl.bindTexture(gl.TEXTURE_2D, bgTexture);
            gl.uniform1i(uniforms.u_bgImage, 0);
        }

        gl.enable(gl.BLEND);
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
        gl.clearColor(0, 0, 0, 0);
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    }

    rafId = requestAnimationFrame(render);

    var instance = {
        gl: gl,
        program: program,
        running: true,
        visible: true,
        rafId: rafId,
        resizeObserver: resizeObserver,
        section: section,
        onMouseMove: onMouseMove,
        onMouseLeave: onMouseLeave,
        opts: opts,
        setVisible: function(v) {
            visible = v;
            instance.visible = v;
        },
        update: function(newOpts) {
            if (newOpts.speed != null) opts.speed = newOpts.speed;
            if (newOpts.intensity != null) opts.intensity = newOpts.intensity;
            if (newOpts.opacity != null) opts.opacity = newOpts.opacity;
            if (newOpts.mouse != null) opts.mouse = newOpts.mouse;
            if (newOpts.color1) { color1 = hexToVec3(newOpts.color1); colorAlpha1 = hexToAlpha(newOpts.color1); }
            if (newOpts.color2) { color2 = hexToVec3(newOpts.color2); colorAlpha2 = hexToAlpha(newOpts.color2); }
            if (newOpts.color3) color3 = hexToVec3(newOpts.color3);
            if (newOpts.bgColor) bgColor = hexToVec3(newOpts.bgColor);
            if (newOpts.shape != null) opts.shape = newOpts.shape;
            if (newOpts.outerOpacity != null) opts.outerOpacity = newOpts.outerOpacity;
            if (newOpts.bgImage !== undefined) loadBgImage(newOpts.bgImage);
            if (newOpts.quality != null) { dpr = Math.min(window.devicePixelRatio || 1, 2) * newOpts.quality; resize(); }
        },
        destroy: function() {
            running = false;
            if (rafId) cancelAnimationFrame(rafId);
            resizeObserver.disconnect();
            if (section) {
                section.removeEventListener('mousemove', onMouseMove);
                section.removeEventListener('mouseleave', onMouseLeave);
            }
            if (_fxOnGyro) { if (window._impulseGyro) window._impulseGyro.unregister(_fxOnGyro); }
            gl.deleteProgram(program);
            gl.deleteBuffer(buf);
            if (bgTexture) gl.deleteTexture(bgTexture);

            

        }
    };

    instances.set(canvas, instance);
    canvas._fxInst = instance;

    if (!observer) {
        observer = new IntersectionObserver(function(entries) {
            entries.forEach(function(entry) {
                var inst = instances.get(entry.target);
                if (inst) inst.setVisible(entry.isIntersecting);
            });
        }, { threshold: 0.01 });
    }
    observer.observe(canvas);

    return instance;
}

function destroy(canvas) {
    var inst = instances.get(canvas);
    if (!inst) return;
    inst.destroy();
    if (observer) observer.unobserve(canvas);
    instances.delete(canvas);
}

function destroyAll() {
    instances.forEach(function(inst, canvas) {
        inst.destroy();
        if (observer) observer.unobserve(canvas);
    });
    instances.clear();
}

function getEffectList() {
    return Object.keys(SHADERS).concat(['turing']);
}

window.ImpulseFX = {
    init: init,
    destroy: destroy,
    destroyAll: destroyAll,
    getEffectList: getEffectList,
    effects: Object.keys(SHADERS)
};

})();
