<!DOCTYPE html>
<html>
<head>
    <title>Optimized Morphing 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.0001
#define MAX_DIST 50.0
#define IOR 1.33
#define NUM_OCTAVES 5

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

vec3 environment(vec3 rd, float t) {
    vec3 sky = mix(vec3(0.1,0.2,0.4), vec3(0.4,0.5,0.8), smoothstep(-0.5,0.8,rd.y));

    vec3 starField = vec3(smoothstep(0.997, 1.0, fbm(rd * 300.0 + t*0.1))) * 0.6;

    float clouds = smoothstep(0.4,0.6,noise(rd*5.0 + vec3(t*0.3,0,0)));
    sky = mix(sky, vec3(1.0), clouds*0.15);

    return sky + starField;
}

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

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 = bubbleSurface(p, t);
        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));
}

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

    vec3 p = mr.pos;
    vec3 N = getNormal(p, t);
    float thickness = 0.05 + 0.15*smoothstep(0.3,0.7,noise(p*10.0 + t*0.3));
    float fresnel = pow(1.0 - abs(dot(rd, N)), 3.0);

    vec3 refractColor = vec3(0.0);
    for(int i=0; i<3; i++) {
        float ratio = float(i)/3.0;
        vec3 layerDir = refract(rd, N, IOR - ratio*0.08);
        refractColor += environment(layerDir, t) *
                       calculateIridescence(thickness, fresnel) *
                       (1.0 - ratio);
    }
    refractColor /= 3.0;

    vec3 lightDir = normalize(vec3(sin(t*0.7), 1.0, cos(t*0.6)));
    float spec = pow(max(dot(reflect(rd,N), lightDir),0.0), 32.0);

    vec3 color = mix(refractColor, environment(reflect(rd,N), t), fresnel);
    color += vec3(1.0,0.9,0.8)*spec;

    return mix(environment(rd, t), color, 0.85 - thickness*4.0);
}

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 = 3.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>
