<!DOCTYPE html>
<html>
<head>
    <title>Glossy Transparent Bubble</title>
    <style>
        body { margin: 0; overflow: hidden; }
        canvas { width: 100vw; height: 100vh; display: block; }
    </style>
</head>
<body>
    <canvas id="canvas"></canvas>
    <script>
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl');

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;

#define PI 3.14159265359
#define MAX_STEPS 50
#define SURFACE_DIST 0.001
#define MAX_DIST 10.0
#define IOR 1.33
#define NUM_OCTAVES 10

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 pulse = 0.9 + 0.1*sin(t*2.0);
    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 = 0.06*fbm(q*5.0 + vec3(0,t*0.5,0));
    vec3 waveDir = normalize(vec3(0.8,0.1,0.4));
    float waves = 0.05*sin(6.0*dot(q,waveDir) + t*4.0);

    return sdSphere(p, 1.0) + turbulence + waves;
}

float sdRoom(vec3 p) {
    return 3.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 = 4.0*PI*thickness*vec3(1.2,1.0,0.8);
    return 0.6 + 0.4*cos(phase + angle*5.0 + vec3(0,2,4));
}

float intersectRoom(vec3 ro, vec3 rd) {
    vec3 t1 = (vec3(3.0) - ro) / rd;
    vec3 t2 = (vec3(-3.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 *= 3.0;
    float checker = mod(floor(uv.x) + floor(uv.y), 2.0);
    vec3 baseColor;

    if(abs(wallNormal.x) > 0.5) {
        baseColor = wallNormal.x > 0.0 ? vec3(1.0,0.2,0.2) : vec3(0.2,0.2,1.0);
    }
    else if(abs(wallNormal.y) > 0.5) {
        baseColor = wallNormal.y > 0.0 ? vec3(0.2,1.0,0.2) : vec3(0.5,0.2,0.8);
    }
    else {
        baseColor = wallNormal.z > 0.0 ? vec3(1.0,1.0,0.2) : vec3(1.0,0.5,0.2);
    }

    return mix(vec3(0.0), baseColor, checker);
}

vec3 renderBubble(vec3 ro, vec3 rd, float t) {
    MarchResult mr = rayMarch(ro, rd, t);
    if(!mr.hit) return vec3(0.9);

    vec3 p = mr.pos;
    float bubbleDist = bubbleSurface(p, t);
    float roomDist = sdRoom(p);

    if(bubbleDist < SURFACE_DIST) {
        vec3 N = getNormal(p, t);
        float thickness = 0.008 + 0.04*smoothstep(0.3,0.7,noise(p*10.0 + t*0.3));
        float fresnel = pow(1.0 - abs(dot(rd, N)), 0.8);

        vec3 refractColor = vec3(0.0);
        for(int i=0; i<5; i++) {
            float ratio = float(i)/5.0;
            vec3 layerDir = refract(rd, N, IOR - ratio*0.05);
            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.0 - ratio);
            }
        }
        refractColor /= 5.0;

        vec3 lightDir = normalize(vec3(0.6, 1.0, 0.8));
        float spec = pow(max(dot(reflect(rd,N), lightDir),0.0), 128.0);

        vec3 color = mix(refractColor, vec3(0.9), fresnel*0.15);
        color += vec3(1.0,0.98,0.95)*spec*3.0;

        return mix(vec3(0.9), color, 0.6 - thickness*1.0);
    }
    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;

        return getWallColor(wallNormal, uv);
    }

    return vec3(0.9);
}

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

    float camDist = 2.0;
    vec3 ro = camDist*vec3(sin(t*0.5), 0.1, cos(t*0.5));
    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, 1.5));

    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;
}

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);

    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);

    const resolutionUniform = gl.getUniformLocation(program, 'iResolution');
    const timeUniform = gl.getUniformLocation(program, 'iTime');

    function render(time) {
        gl.uniform2f(resolutionUniform, canvas.width, canvas.height);
        gl.uniform1f(timeUniform, time * 0.001);
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
        requestAnimationFrame(render);
    }
    requestAnimationFrame(render);
}

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