<!DOCTYPE html>
<html>
<head>
    <title>Bubble Scene</title>
    <style>
        body { margin: 0; overflow: hidden; }
        canvas { width: 100vw; height: 100vh; display: block; }
    </style>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script>
</head>
<body>
    <canvas id="canvas"></canvas>
    <script>
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl');

const params = {
    // Bubble
    bubbleRadius: 4.4,
    radiusAmp: 0.1,
    radiusSpeed: 1.5,
    pulseBase: 0.9,
    pulseAmp: 0.1,
    pulseSpeed: 2.0,
    turbulence: 0.08,
    waves: 0.07,
    spiral: 0.04,
    vortex: 0.05,
    thicknessMin: 0.004,
    thicknessMax: 0.02,

    // Iridescence
    iridescencePhase: 6.0,
    angleScale: 8.0,

    // Lights
    lightSpeed: 0.5,
    lightIntensity: 0.3,
    lightAttenuation: 1.5,

    // Fog
    fogStart: 20.0,
    fogEnd: 40.0,
    fogOpacity: 0.6,

    // Walls
    wallDark: 0.3,
    wallBrightness: 1.0,

    // Camera
    camDistance: 10.0,
    camSpeed: 0.8
};

const gui = new dat.GUI();
const bubbleFolder = gui.addFolder('Bubble');
bubbleFolder.add(params, 'bubbleRadius', 0.1, 3.0).step(0.1);
bubbleFolder.add(params, 'radiusAmp', 0.0, 0.5).step(0.01);
bubbleFolder.add(params, 'radiusSpeed', 0.0, 3.0).step(0.1);
bubbleFolder.add(params, 'pulseBase', 0.5, 1.5).step(0.1);
bubbleFolder.add(params, 'pulseAmp', 0.0, 0.5).step(0.01);
bubbleFolder.add(params, 'pulseSpeed', 0.0, 5.0).step(0.1);
bubbleFolder.add(params, 'turbulence', 0.0, 0.2).step(0.001);
bubbleFolder.add(params, 'waves', 0.0, 0.2).step(0.001);
bubbleFolder.add(params, 'spiral', 0.0, 0.1).step(0.001);
bubbleFolder.add(params, 'vortex', 0.0, 0.1).step(0.001);
bubbleFolder.add(params, 'thicknessMin', 0.0, 0.01).step(0.0001);
bubbleFolder.add(params, 'thicknessMax', 0.0, 0.05).step(0.001);

const iridescenceFolder = gui.addFolder('Iridescence');
iridescenceFolder.add(params, 'iridescencePhase', 1.0, 10.0).step(0.1);
iridescenceFolder.add(params, 'angleScale', 1.0, 15.0).step(0.1);

const lightsFolder = gui.addFolder('Lights');
lightsFolder.add(params, 'lightSpeed', 0.1, 2.0).step(0.1);
lightsFolder.add(params, 'lightIntensity', 0.1, 1.0).step(0.1);
lightsFolder.add(params, 'lightAttenuation', 0.5, 3.0).step(0.1);

const fogFolder = gui.addFolder('Fog');
fogFolder.add(params, 'fogStart', 10.0, 50.0).step(1.0);
fogFolder.add(params, 'fogEnd', 20.0, 100.0).step(1.0);
fogFolder.add(params, 'fogOpacity', 0.0, 1.0).step(0.1);

const wallsFolder = gui.addFolder('Walls');
wallsFolder.add(params, 'wallDark', 0.0, 1.0).step(0.1);
wallsFolder.add(params, 'wallBrightness', 0.0, 2.0).step(0.1);

const cameraFolder = gui.addFolder('Camera');
cameraFolder.add(params, 'camDistance', 5.0, 20.0).step(1.0);
cameraFolder.add(params, 'camSpeed', 0.1, 2.0).step(0.1);

const vertexShaderSource = `
attribute vec2 position;
void main() {
    gl_Position = vec4(position, 0.0, 1.0);
}
`;

const fragmentShaderSource = `
precision mediump float;
uniform vec2 iResolution;
uniform float iTime;

uniform float uBubbleRadius;
uniform float uRadiusAmp;
uniform float uRadiusSpeed;
uniform float uPulseBase;
uniform float uPulseAmp;
uniform float uPulseSpeed;
uniform float uTurbulence;
uniform float uWaves;
uniform float uSpiral;
uniform float uVortex;
uniform float uThicknessMin;
uniform float uThicknessMax;
uniform float uIridescencePhase;
uniform float uAngleScale;
uniform float uLightSpeed;
uniform float uLightIntensity;
uniform float uLightAttenuation;
uniform float uFogStart;
uniform float uFogEnd;
uniform float uFogOpacity;
uniform float uWallDark;
uniform float uWallBrightness;
uniform float uCamDistance;
uniform float uCamSpeed;

#define PI 3.14159265359
#define MAX_STEPS 60
#define SURFACE_DIST 0.001
#define MAX_DIST 40.0
#define IOR 1.33
#define NUM_OCTAVES 5
#define LIGHT_COUNT 3

float hash(float n) { return fract(sin(n)*753.5453123); }
float noise(vec3 x) {
    vec3 p = floor(x);
    vec3 f = fract(x);
    f = f*f*(3.0-2.0*f);

    float n = p.x + p.y*157.0 + 113.0*p.z;
    return mix(mix(mix(hash(n+0.0), hash(n+1.0),f.x),
               mix(hash(n+157.0), hash(n+158.0),f.x),f.y),
           mix(mix(hash(n+113.0), hash(n+114.0),f.x),
               mix(hash(n+270.0), hash(n+271.0),f.x),f.y),f.z);
}

float fbm(vec3 x) {
    float v = 0.0;
    float a = 0.5;
    vec3 shift = vec3(100);
    for (int i = 0; i < NUM_OCTAVES; ++i) {
        v += a * noise(x);
        x = x * 2.0 + shift;
        a *= 0.5;
    }
    return v;
}

float sdSphere(vec3 p, float r) { return length(p) - r; }

float bubbleSurface(vec3 p, float t) {
    float bubbleRadius = uBubbleRadius + uRadiusAmp*sin(t*uRadiusSpeed);
    float pulse = uPulseBase + uPulseAmp*sin(t*uPulseSpeed);
    vec3 q = p * pulse;

    float angle = t*0.5;
    float ca = cos(angle), sa = sin(angle);
    q = vec3(
        ca*q.x + sa*q.z,
        q.y,
        -sa*q.x + ca*q.z
    );

    float turbulence = uTurbulence*fbm(q*6.0 + vec3(0,t*0.7,0));
    vec3 waveDir = normalize(vec3(0.8,0.1,0.4));
    float waves = uWaves*sin(8.0*dot(q,waveDir) + t*5.0);
    float spiral = uSpiral*sin(4.0*length(q.xz) + t*3.0);
    float vortex = uVortex*sin(10.0*atan(p.z, p.x) - t*4.0 + 3.0*length(p.xy));

    return sdSphere(p, bubbleRadius) + turbulence + waves + spiral + vortex;
}

float sdRoom(vec3 p) {
    return 15.0 - max(abs(p.x), max(abs(p.y), abs(p.z)));
}

struct MarchResult { float dist; vec3 pos; bool hit; };

MarchResult rayMarch(vec3 ro, vec3 rd, float t) {
    float dO = 0.0;
    vec3 p = ro;
    for(int i=0; i<MAX_STEPS; i++) {
        p = ro + rd*dO;
        float dS = min(bubbleSurface(p, t), sdRoom(p));
        if(dS < SURFACE_DIST) return MarchResult(dO, p, true);
        dO += dS;
        if(dO > MAX_DIST) break;
    }
    return MarchResult(dO, p, false);
}

vec3 getNormal(vec3 p, float t) {
    vec2 e = vec2(0.002, -0.002);
    return normalize(
        e.xyy * bubbleSurface(p + e.xyy, t) +
        e.yyx * bubbleSurface(p + e.yyx, t) +
        e.yxy * bubbleSurface(p + e.yxy, t)
    );
}

vec3 calculateIridescence(float thickness, float angle) {
    vec3 phase = uIridescencePhase*PI*thickness*vec3(1.8,1.3,0.7);
    return 0.8 + 0.2*(
        cos(phase + angle*uAngleScale + vec3(0,2,4)) * vec3(1.0,0.7,0.5) +
        sin(phase*1.5 + angle*12.0 + vec3(1,3,5)) * vec3(0.5,1.0,0.7)
    );
}

float intersectRoom(vec3 ro, vec3 rd) {
    vec3 t1 = (vec3(15.0) - ro) / rd;
    vec3 t2 = (vec3(-15.0) - ro) / rd;
    vec3 tmin = min(t1, t2);
    vec3 tmax = max(t1, t2);

    float tminFinal = max(tmin.x, max(tmin.y, tmin.z));
    float tmaxFinal = min(tmax.x, min(tmax.y, tmax.z));

    if (tmaxFinal < 0.0 || tminFinal > tmaxFinal) return -1.0;
    return tminFinal > 0.0 ? tminFinal : tmaxFinal;
}

vec3 getWallColor(vec3 wallNormal, vec2 uv) {
    uv *= 5.0;
    float checker = mod(floor(uv.x) + floor(uv.y), 2.0);
    vec3 baseColor;

    vec3 palette0 = vec3(1.0,0.6,0.6);
    vec3 palette1 = vec3(0.6,0.6,1.0);
    vec3 palette2 = vec3(0.6,1.0,0.6);
    vec3 palette3 = vec3(1.0,0.6,1.0);
    vec3 palette4 = vec3(1.0,1.0,0.6);
    vec3 palette5 = vec3(1.0,0.8,0.6);

    if(abs(wallNormal.x) > 0.5) {
        baseColor = wallNormal.x > 0.0 ? palette0 : palette1;
    }
    else if(abs(wallNormal.y) > 0.5) {
        baseColor = wallNormal.y > 0.0 ? palette2 : palette3;
    }
    else {
        baseColor = wallNormal.z > 0.0 ? palette4 : palette5;
    }

    vec3 darkColor = vec3(uWallDark);
    vec3 brightColor = baseColor * uWallBrightness;
    return mix(darkColor, brightColor, checker);
}

vec3 dynamicLights(vec3 p, vec3 N, float t) {
    vec3 lightColor = vec3(0.0);
    for(int i=0; i<LIGHT_COUNT; i++) {
        float lt = t * uLightSpeed * (0.5 + float(i)*0.3);
        vec3 lightPos = vec3(
            sin(lt) * 5.0,
            mix(2.0, 5.0, sin(lt*0.7)),
            cos(lt) * 5.0
        );
        vec3 lDir = normalize(lightPos - p);
        float intensity = 1.0 / pow(length(lightPos - p), uLightAttenuation);
        lightColor += intensity * vec3(
            sin(float(i)*2.1),
            cos(float(i)*3.3),
            sin(float(i)*4.7)
        ) * max(dot(N, lDir), 0.0);
    }
    return lightColor * uLightIntensity;
}

vec3 renderBubble(vec3 ro, vec3 rd, float t) {
    MarchResult mr = rayMarch(ro, rd, t);

    vec3 sceneColor = vec3(0.9);
    if(mr.hit) {
        vec3 p = mr.pos;
        float bubbleDist = bubbleSurface(p, t);
        float roomDist = sdRoom(p);

        if(bubbleDist < SURFACE_DIST) {
            vec3 N = getNormal(p, t);
            float thickness = uThicknessMin + uThicknessMax*smoothstep(0.3,0.7,noise(p*12.0 + t*0.4));
            float fresnel = pow(1.0 - abs(dot(rd, N)), 0.5);

            vec3 refractColor = vec3(0.0);
            for(int i=0; i<8; i++) {
                float ratio = float(i)/8.0;
                vec3 layerDir = refract(rd, N, IOR - ratio*0.03);
                if(length(layerDir) < 0.001) continue;

                float tRoom = intersectRoom(p, layerDir);
                if(tRoom > 0.0) {
                    vec3 hitPos = p + layerDir * tRoom;
                    float maxCoord = max(abs(hitPos.x), max(abs(hitPos.y), abs(hitPos.z)));
                    vec3 wallNormal;
                    if(maxCoord == abs(hitPos.x)) wallNormal = vec3(sign(hitPos.x),0,0);
                    else if(maxCoord == abs(hitPos.y)) wallNormal = vec3(0,sign(hitPos.y),0);
                    else wallNormal = vec3(0,0,sign(hitPos.z));

                    vec2 uv;
                    if(abs(wallNormal.x) > 0.5) uv = hitPos.yz;
                    else if(abs(wallNormal.y) > 0.5) uv = hitPos.xz;
                    else uv = hitPos.xy;

                    refractColor += getWallColor(wallNormal, uv) *
                                  calculateIridescence(thickness, fresnel) *
                                  (1.5 - ratio);
                }
            }
            refractColor /= 8.0;

            vec3 lightDir = normalize(vec3(sin(t), 1.0, cos(t)));
            float spec = pow(max(dot(reflect(rd,N), lightDir),0.0), 1024.0);
            vec3 halo = vec3(0.8,1.0,1.0) * pow(fresnel, 4.0);

            sceneColor = mix(refractColor, vec3(0.9), fresnel*0.05);
            sceneColor += vec3(1.0,0.98,0.95)*spec*6.0 + halo;
            sceneColor += dynamicLights(p, N, t);
        }
        else if(roomDist < SURFACE_DIST) {
            float maxCoord = max(abs(p.x), max(abs(p.y), abs(p.z)));
            vec3 wallNormal;
            if(maxCoord == abs(p.x)) wallNormal = vec3(sign(p.x),0,0);
            else if(maxCoord == abs(p.y)) wallNormal = vec3(0,sign(p.y),0);
            else wallNormal = vec3(0,0,sign(p.z));

            vec2 uv;
            if(abs(wallNormal.x) > 0.5) uv = p.yz;
            else if(abs(wallNormal.y) > 0.5) uv = p.xz;
            else uv = p.xy;

            sceneColor = getWallColor(wallNormal, uv);
        }
    }

    float fog = smoothstep(uFogStart, uFogEnd, mr.dist);
    sceneColor = mix(sceneColor, vec3(0.85,0.95,1.0), fog*uFogOpacity);

    return sceneColor;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (2.0*fragCoord.xy - iResolution.xy)/iResolution.y;
    float t = iTime * uCamSpeed;

    vec3 ro = uCamDistance*vec3(sin(t*0.2), 0.5, cos(t*0.2));
    vec3 lookat = vec3(0.0);

    vec3 z = normalize(lookat - ro);
    vec3 x = normalize(cross(z, vec3(0.0,1.0,0.0)));
    vec3 y = cross(x, z);
    vec3 rd = normalize(mat3(x,y,z) * vec3(uv, 2.0));

    vec3 color = renderBubble(ro, rd, t);

    color = pow(color, vec3(1.0/2.2));
    color *= 1.0 - 0.15*length(uv);

    fragColor = vec4(color,1.0);
}

void main() { mainImage(gl_FragColor, gl_FragCoord.xy); }
`;

function compileShader(gl, source, type) {
    const shader = gl.createShader(type);
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        console.error('Shader error:', gl.getShaderInfoLog(shader));
        return null;
    }
    return shader;
}

let uniforms = {};

function init() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    gl.viewport(0, 0, canvas.width, canvas.height);

    const vs = compileShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
    const fs = compileShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER);
    if (!vs || !fs) return;

    const program = gl.createProgram();
    gl.attachShader(program, vs);
    gl.attachShader(program, fs);
    gl.linkProgram(program);

    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        console.error('Program error:', gl.getProgramInfoLog(program));
        return;
    }
    gl.useProgram(program);

    uniforms = {
        iResolution: gl.getUniformLocation(program, 'iResolution'),
        iTime: gl.getUniformLocation(program, 'iTime'),
        uBubbleRadius: gl.getUniformLocation(program, 'uBubbleRadius'),
        uRadiusAmp: gl.getUniformLocation(program, 'uRadiusAmp'),
        uRadiusSpeed: gl.getUniformLocation(program, 'uRadiusSpeed'),
        uPulseBase: gl.getUniformLocation(program, 'uPulseBase'),
        uPulseAmp: gl.getUniformLocation(program, 'uPulseAmp'),
        uPulseSpeed: gl.getUniformLocation(program, 'uPulseSpeed'),
        uTurbulence: gl.getUniformLocation(program, 'uTurbulence'),
        uWaves: gl.getUniformLocation(program, 'uWaves'),
        uSpiral: gl.getUniformLocation(program, 'uSpiral'),
        uVortex: gl.getUniformLocation(program, 'uVortex'),
        uThicknessMin: gl.getUniformLocation(program, 'uThicknessMin'),
        uThicknessMax: gl.getUniformLocation(program, 'uThicknessMax'),
        uIridescencePhase: gl.getUniformLocation(program, 'uIridescencePhase'),
        uAngleScale: gl.getUniformLocation(program, 'uAngleScale'),
        uLightSpeed: gl.getUniformLocation(program, 'uLightSpeed'),
        uLightIntensity: gl.getUniformLocation(program, 'uLightIntensity'),
        uLightAttenuation: gl.getUniformLocation(program, 'uLightAttenuation'),
        uFogStart: gl.getUniformLocation(program, 'uFogStart'),
        uFogEnd: gl.getUniformLocation(program, 'uFogEnd'),
        uFogOpacity: gl.getUniformLocation(program, 'uFogOpacity'),
        uWallDark: gl.getUniformLocation(program, 'uWallDark'),
        uWallBrightness: gl.getUniformLocation(program, 'uWallBrightness'),
        uCamDistance: gl.getUniformLocation(program, 'uCamDistance'),
        uCamSpeed: gl.getUniformLocation(program, 'uCamSpeed')
    };

    const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
    const buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

    const positionLoc = gl.getAttribLocation(program, 'position');
    gl.enableVertexAttribArray(positionLoc);
    gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);

    function render(time) {
        gl.uniform2f(uniforms.iResolution, canvas.width, canvas.height);
        gl.uniform1f(uniforms.iTime, time * 0.001);

        gl.uniform1f(uniforms.uBubbleRadius, params.bubbleRadius);
        gl.uniform1f(uniforms.uRadiusAmp, params.radiusAmp);
        gl.uniform1f(uniforms.uRadiusSpeed, params.radiusSpeed);
        gl.uniform1f(uniforms.uPulseBase, params.pulseBase);
        gl.uniform1f(uniforms.uPulseAmp, params.pulseAmp);
        gl.uniform1f(uniforms.uPulseSpeed, params.pulseSpeed);
        gl.uniform1f(uniforms.uTurbulence, params.turbulence);
        gl.uniform1f(uniforms.uWaves, params.waves);
        gl.uniform1f(uniforms.uSpiral, params.spiral);
        gl.uniform1f(uniforms.uVortex, params.vortex);
        gl.uniform1f(uniforms.uThicknessMin, params.thicknessMin);
        gl.uniform1f(uniforms.uThicknessMax, params.thicknessMax);
        gl.uniform1f(uniforms.uIridescencePhase, params.iridescencePhase);
        gl.uniform1f(uniforms.uAngleScale, params.angleScale);
        gl.uniform1f(uniforms.uLightSpeed, params.lightSpeed);
        gl.uniform1f(uniforms.uLightIntensity, params.lightIntensity);
        gl.uniform1f(uniforms.uLightAttenuation, params.lightAttenuation);
        gl.uniform1f(uniforms.uFogStart, params.fogStart);
        gl.uniform1f(uniforms.uFogEnd, params.fogEnd);
        gl.uniform1f(uniforms.uFogOpacity, params.fogOpacity);
        gl.uniform1f(uniforms.uWallDark, params.wallDark);
        gl.uniform1f(uniforms.uWallBrightness, params.wallBrightness);
        gl.uniform1f(uniforms.uCamDistance, params.camDistance);
        gl.uniform1f(uniforms.uCamSpeed, params.camSpeed);

        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
        requestAnimationFrame(render);
    }
    requestAnimationFrame(render);
}

init();
window.addEventListener('resize', init);
    </script>
</body>
</html>