← Mike Higgins Home
Custom Code Experiment •

Conformal Map 7

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>Conformal Map 7</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            user-select: none;
        }
        html,
        body {
            width: 100%;
            height: 100%;
            overflow: hidden;
            background: #000;
        }
        canvas {
            display: block;
            width: 100%;
            height: 100%;
        }
    </style>
</head>
<body>
    <canvas id="c"></canvas>
    <script>
        (function() {
            const canvas = document.getElementById('c');
            const gl = canvas.getContext('webgl2', { antialias: true, depth: false });
            if (!gl) {
                console.warn("WebGL2 not supported");
                return;
            }

            // ---------- VERTEX SHADER ----------
            const VS = `#version 300 es
        in vec2 aPos;
        out vec2 vUv;
        void main() {
          vUv = aPos * 0.5 + 0.5;
          gl_Position = vec4(aPos, 0.0, 1.0);
        }
      `;

            // ======================= BASE SHADER: MULTI-GEOMETRY COMPLEX =======================
            const FS_BASE = `#version 300 es
        precision highp float;
        in vec2 vUv;
        out vec4 fragColor;
        uniform float uTime;
        uniform float uAspect;

        #define PI 3.141592653589793
        #define TAU 6.283185307179586
        #define PHI 1.618033988749895
        #define SQRT2 1.414213562373095

        const vec3 deepCosmos   = vec3(0.04, 0.02, 0.18);
        const vec3 midnight     = vec3(0.08, 0.09, 0.28);
        const vec3 tealOcean    = vec3(0.05, 0.42, 0.48);
        const vec3 emerald      = vec3(0.12, 0.58, 0.42);
        const vec3 coralFlame   = vec3(0.96, 0.38, 0.28);
        const vec3 goldenAmber  = vec3(1.0, 0.78, 0.22);
        const vec3 softLavender = vec3(0.68, 0.55, 0.82);
        const vec3 hotMagenta   = vec3(0.92, 0.18, 0.55);
        const vec3 iceWhite     = vec3(0.94, 0.92, 0.98);
        const vec3 deepShadow   = vec3(0.02, 0.01, 0.08);

        float hash1(float n) {
            return fract(sin(n) * 43758.5453123);
        }
        float hash2(vec2 p) {
            float h = dot(p, vec2(127.1, 311.7));
            return fract(sin(h) * 43758.5453123);
        }
        float hash3(vec3 p) {
            float h = dot(p, vec3(127.1, 311.7, 74.7));
            return fract(sin(h) * 43758.5453123);
        }

        float smin(float a, float b, float k) {
            float h = max(k - abs(a - b), 0.0) / k;
            return min(a, b) - h * h * k * 0.25;
        }
        float smax(float a, float b, float k) {
            return -smin(-a, -b, k);
        }

        float distRoseSpiral(vec2 p, float seed, float t) {
            float r = length(p);
            float theta = atan(p.y, p.x);

            float petalCount = 8.0 + floor(hash1(seed * 3.7) * 6.0);
            float petalSharpness = 1.2 + hash1(seed * 7.1) * 1.8;
            float amplitude = 0.28 + hash1(seed * 13.3) * 0.22;
            float spiralPitch = 0.06 + hash1(seed * 19.7) * 0.1;

            float ruffleFreq = 18.0 + hash1(seed * 23.1) * 24.0;
            float ruffleAmp = 0.015 + hash1(seed * 29.3) * 0.03;
            float petalRuffles = ruffleAmp * sin(theta * ruffleFreq + t * 2.1) * cos(r * 35.0 - t);

            float roseArg = petalCount * theta + t * 0.7 * (1.0 + hash1(seed * 31.7));
            float roseRadius = amplitude * abs(cos(roseArg * 0.5));
            roseRadius = pow(roseRadius, petalSharpness);
            roseRadius = 0.3 + roseRadius * 0.35 + petalRuffles;

            float spiralArg = theta - log(r + 0.08) * spiralPitch * 12.0 + t * 1.3;
            float spiralMod = 0.04 * sin(spiralArg * 3.0) * smoothstep(0.1, 0.7, r);

            float dynamicRadius = roseRadius + spiralMod;
            float dRose = abs(r - dynamicRadius) - 0.018;

            float innerR = 0.14 + 0.04 * sin(theta * 8.0 + seed * TAU) * cos(r * 22.0);
            float dInner = abs(r - innerR) - 0.01;

            float wispR = 0.55 + 0.06 * sin(theta * 5.0 - log(r + 0.1) * 8.0 + t);
            float dWisp = abs(r - wispR) - 0.012;
            dWisp += 0.03 * smoothstep(0.5, 0.7, r);

            float d = smin(dRose, dInner, 0.03);
            d = smin(d, dWisp, 0.04);
            return d;
        }

        float distStellarComplex(vec2 p, float seed, float t) {
            float r = length(p);
            float theta = atan(p.y, p.x);

            float points = 5.0 + floor(hash1(seed * 41.1) * 8.0);
            float starDepth = 0.35 + hash1(seed * 47.3) * 0.4;
            float twist = 0.3 + hash1(seed * 53.7) * 1.2;
            float layers = 2.0 + floor(hash1(seed * 59.1) * 3.0);

            float d = 1e6;
            for (float l = 0.0; l < 3.0; l += 1.0) {
                if (l >= layers) break;
                float layerOffset = l * 0.15;
                float layerR = 0.22 + layerOffset;
                float layerTwist = twist + l * 0.4;
                float layerAmp = starDepth * (1.0 - l * 0.25);

                float angleContrib = theta + layerTwist * sin(r * 6.0 + t * 0.8 + l);
                float starShape = cos(angleContrib * points * 0.5);
                starShape = abs(starShape);
                starShape = pow(starShape, 0.6 + l * 0.3);
                float starRadius = layerR + layerAmp * starShape * 0.3;

                float dLayer = abs(r - starRadius) - 0.014 + l * 0.005;
                d = smin(d, dLayer, 0.04);
            }

            float armAngle = theta * points / TAU;
            float armFrac = fract(armAngle + t * 0.15);
            float armRadial = 0.15 + armFrac * 0.55;
            float dArm = abs(r - armRadial) - 0.02;
            dArm *= 1.0 + 0.5 * abs(sin(theta * points + t));
            d = smin(d, dArm, 0.03);
            return d;
        }

        float distEpitrochoid(vec2 p, float seed, float t) {
            float r = length(p);
            float theta = atan(p.y, p.x);

            float R1 = 0.35 + hash1(seed * 67.3) * 0.25;
            float r1 = 0.12 + hash1(seed * 71.9) * 0.18;
            float d1 = 0.2 + hash1(seed * 73.4) * 0.3;
            float speed = 0.4 + hash1(seed * 79.1) * 1.2;

            float d = 1e6;
            for (float i = 0.0; i < 8.0; i += 1.0) {
                float phi = TAU * i / 8.0 + t * speed * 0.3;
                float cx = (R1 + r1) * cos(phi) - d1 * cos((R1 + r1) / r1 * phi);
                float cy = (R1 + r1) * sin(phi) - d1 * sin((R1 + r1) / r1 * phi);
                float distPoint = length(p - vec2(cx, cy));
                d = smin(d, distPoint - 0.025, 0.06);
            }

            float curveArg = theta * 3.0 + sin(r * 12.0 - t * speed) * 2.0;
            float curveR = R1 + d1 * cos(curveArg) * 0.7;
            float dCurve = abs(r - curveR) - 0.016;
            d = smin(d, dCurve, 0.04);
            return d;
        }

        float distOrganicBlobs(vec2 p, float seed, float t) {
            float r = length(p);
            float theta = atan(p.y, p.x);

            float numBlobs = 3.0 + floor(hash1(seed * 83.7) * 5.0);
            float blobSoftness = 0.35 + hash1(seed * 89.1) * 0.5;

            float d = 1e6;
            for (float b = 0.0; b < 7.0; b += 1.0) {
                if (b >= numBlobs) break;
                float angle = TAU * b / numBlobs + hash1(seed * 97.3 + b) * 0.6;
                float dist = 0.25 + hash1(seed * 101.9 + b) * 0.3;
                float blobR = 0.08 + hash1(seed * 103.7 + b) * 0.12;
                float wobble = sin(t * 1.4 + b * 2.7) * 0.06;

                float bx = dist * cos(angle + wobble);
                float by = dist * sin(angle + wobble);
                float blobDist = length(p - vec2(bx, by)) - blobR;

                float tendrilAngle = atan(by, bx);
                float angleDiff = abs(mod(theta - tendrilAngle + PI, TAU) - PI);
                float tendril = angleDiff * r - 0.03;
                blobDist = smin(blobDist, tendril, 0.05);

                d = smin(d, blobDist, blobSoftness);
            }
            return d;
        }

        float shapeDistance(vec2 p, float cellSeed, float timeShift) {
            float r = length(p);
            float theta = atan(p.y, p.x); // FIX: added missing declaration
            float shapeType = hash1(cellSeed * 107.3);
            float d = 1e6;

            float dRose = distRoseSpiral(p, cellSeed, timeShift);
            d = smin(d, dRose, 0.05);

            if (shapeType < 0.33) {
                float dStar = distStellarComplex(p, cellSeed + 0.1, timeShift);
                d = smin(d, dStar, 0.06);
            } else if (shapeType < 0.66) {
                float dEpi = distEpitrochoid(p, cellSeed + 0.2, timeShift);
                d = smin(d, dEpi, 0.06);
            } else {
                float dBlob = distOrganicBlobs(p, cellSeed + 0.3, timeShift);
                d = smin(d, dBlob, 0.06);
            }

            float rayCount = 11.0 + floor(hash1(cellSeed * 113.7) * 10.0);
            float rayPattern = sin(theta * rayCount + timeShift * 2.5) * cos(theta * 5.0 - timeShift);
            float rayWidth = 0.05 * (1.0 - smoothstep(0.1, 0.65, r));
            float dRay = abs(rayPattern) * rayWidth - 0.004;
            d = smin(d, dRay, 0.02);

            float ringCount = 6.0 + floor(hash1(cellSeed * 127.1) * 8.0);
            float ringPattern = sin(r * ringCount * 8.0 - timeShift * 1.7);
            float dRing = abs(ringPattern) * 0.03 - 0.006;
            dRing *= smoothstep(0.15, 0.6, r);
            d = smin(d, dRing, 0.025);

            float boundary = r - 0.72;
            d = min(d, boundary);
            return d;
        }

        void main() {
            vec2 uv = vUv;

            float skew = 0.38 + 0.08 * sin(uTime * 0.31);
            uv.x += uv.y * skew;

            float t = uTime * 0.28;

            float cellSize = 0.68;
            vec2 scrollUv = uv + vec2(t * 0.015, t * 0.009);
            vec2 gridUv = scrollUv / cellSize;
            vec2 gridIdx = floor(gridUv);
            vec2 cellUv = fract(gridUv) - 0.5;

            float cellSeed = fract(sin(gridIdx.x * 127.1 + gridIdx.y * 311.7) * 43758.5453);
            float rotSeed = fract(sin(gridIdx.y * 219.3 + gridIdx.x * 173.9) * 271.8);

            float angleRot = (rotSeed * TAU + t * 0.55) + sin(gridIdx.x * 0.61) * 0.8;
            float cRot = cos(angleRot);
            float sRot = sin(angleRot);
            vec2 rotatedUv = vec2(
                cellUv.x * cRot - cellUv.y * sRot,
                cellUv.x * sRot + cellUv.y * cRot
            );

            float morphScale = 0.85 + 0.3 * sin(cellSeed * TAU * 2.3 + t);
            vec2 finalP = rotatedUv / morphScale;

            float shapeDist = shapeDistance(finalP, cellSeed, t + cellSeed * 6.28);
            float mask = smoothstep(0.026, -0.014, shapeDist);

            float checker = mod(gridIdx.x + gridIdx.y, 2.0);
            float bgShift = 0.15 * sin(gridIdx.x * 0.77 + gridIdx.y * 0.59 + t * 0.4);
            vec3 bgCol = mix(deepCosmos, midnight, checker + bgShift);
            bgCol = mix(bgCol, tealOcean, 0.25 + 0.2 * sin(gridIdx.x * 0.53 + gridIdx.y * 0.71 + t * 0.35));

            float rLocal = length(finalP);
            float thetaLocal = atan(finalP.y, finalP.x);
            float colorPhase = thetaLocal / TAU + rLocal * 0.8 + cellSeed;

            vec3 gradA = mix(coralFlame, goldenAmber, smoothstep(0.0, 0.4, fract(colorPhase + t * 0.13)));
            vec3 gradB = mix(tealOcean, emerald, smoothstep(0.3, 0.7, fract(colorPhase - t * 0.09)));
            vec3 gradC = mix(softLavender, hotMagenta, smoothstep(0.5, 0.9, fract(colorPhase + t * 0.17)));
            vec3 shapeColor = mix(gradA, gradB, smoothstep(0.2, 0.5, fract(colorPhase)));
            shapeColor = mix(shapeColor, gradC, smoothstep(0.5, 0.8, fract(colorPhase + 0.3)));

            shapeColor = mix(shapeColor, iceWhite, 0.35 * exp(-rLocal * 6.0));
            shapeColor = mix(shapeColor, goldenAmber, 0.25 * exp(-rLocal * 3.5));

            vec3 col = mix(bgCol, shapeColor, mask);

            float coreR = rLocal - 0.1;
            float coreMask = smoothstep(0.02, -0.012, coreR);
            vec3 coreCol = mix(goldenAmber, iceWhite, 0.6);
            col = mix(col, coreCol, coreMask * mask * 0.85);

            float rayHarmonic = sin(thetaLocal * 14.0 + t * 3.0) * cos(thetaLocal * 7.0 - t * 2.0);
            float rayIntensity = smoothstep(0.18, 0.6, rLocal) * (0.7 + 0.5 * rayHarmonic);
            rayIntensity *= (1.0 - smoothstep(0.0, 0.3, coreR)) * 0.75;
            rayIntensity = clamp(rayIntensity, 0.0, 0.6);
            col = mix(col, goldenAmber, rayIntensity * mask);

            float edgeGlow = smoothstep(0.03, 0.012, shapeDist) - smoothstep(0.012, -0.014, shapeDist);
            edgeGlow += 0.3 * abs(sin(thetaLocal * 28.0 - t * 8.0)) * (1.0 - mask);
            col = mix(col, deepShadow, edgeGlow * 0.8);
            col = mix(col, hotMagenta, edgeGlow * 0.35 * abs(sin(thetaLocal * 15.0 + t)));

            vec2 edgeDist = abs(fract(gridUv) - 0.5);
            float lineWidth = 0.04 + 0.014 * sin(gridIdx.x * 2.7 + gridIdx.y * 2.1 + t);
            float lines = 1.0 - smoothstep(lineWidth - 0.005, lineWidth, min(edgeDist.x, edgeDist.y));
            lines *= 0.7;
            vec3 lineColor = mix(goldenAmber, softLavender, 0.5 + 0.5 * sin(gridIdx.x * 0.9 + gridIdx.y * 0.7 + t));
            col = mix(col, lineColor, lines * 0.55);

            float chromaRipple = sin(uv.y * 42.0 + t * 5.8) * 0.22 + cos(uv.x * 36.0 - t * 4.5) * 0.18;
            col += vec3(0.07, 0.02, 0.1) * chromaRipple * (1.0 - mask) * 0.45;
            col += vec3(0.03, 0.06, 0.02) * chromaRipple * mask * 0.3;

            float vignette = 1.0 - smoothstep(0.4, 1.2, length(uv - 0.5) * 1.6);
            col = mix(deepShadow, col, vignette);

            fragColor = vec4(col, 1.0);
        }
      `;

            // =========== RECURSIVE CONFORMAL MAP: KALEIDOSCOPIC DROSTE + MULTI-SPIRAL ===========
            const FS_DROSTE = `#version 300 es
        precision highp float;
        in vec2 vUv;
        out vec4 fragColor;
        uniform sampler2D uTex;
        uniform float uTime;
        uniform float uAspect;

        #define PI 3.141592653589793
        #define TAU 6.283185307179586
        #define PHI 1.618033988749895

        float sminK(float a, float b, float k) {
            float h = max(k - abs(a - b), 0.0) / k;
            return min(a, b) - h * h * k * 0.25;
        }

        void main() {
            vec2 uv = vUv;
            vec2 p = (uv * 2.0 - 1.0) * vec2(uAspect, 1.0);
            float r = length(p);

            if (r < 0.006) {
                fragColor = vec4(1.0, 0.85, 0.5, 1.0);
                return;
            }

            float theta = atan(p.y, p.x);

            float symFold = 7.0;
            float symAngle = TAU / symFold;
            float foldedTheta = mod(theta + symAngle * 0.5, symAngle) - symAngle * 0.5;
            float mirrorSign = sign(foldedTheta);
            foldedTheta = abs(foldedTheta);

            float logr = log(r + 0.001);
            float spiralPitch = 0.32 + 0.07 * sin(uTime * 0.55);
            float branchCount = 5.0;

            float spiralTwist = spiralPitch * foldedTheta;
            float branchSelector = floor((theta + PI) / TAU * branchCount);
            float branchPhase = branchSelector * TAU / branchCount;

            float period = 0.68 * (1.0 + 0.08 * sin(uTime * 0.25));
            float animPhase = uTime * 0.19;

            float mappedLogR = logr - spiralTwist + 0.08 * cos(foldedTheta * 6.0 - uTime * 1.1);
            mappedLogR = mod(mappedLogR + animPhase + branchPhase * 0.15, period) - period * 0.48;

            float newR = exp(mappedLogR + spiralTwist - 0.08 * cos(foldedTheta * 6.0 - uTime * 1.1));
            newR = clamp(newR, 0.002, 1.8);

            float newTheta = foldedTheta * mirrorSign;
            newTheta += 0.14 * sin(5.0 * mappedLogR + uTime * 1.0) * cos(foldedTheta * 3.5);
            newTheta += branchPhase * 0.08;
            newTheta = mod(newTheta + PI, TAU) - PI;

            vec2 newP = newR * vec2(cos(newTheta), sin(newTheta));
            vec2 newUv = (newP / vec2(uAspect, 1.0)) * 0.5 + 0.5;
            newUv = fract(newUv);

            vec4 sampled = texture(uTex, newUv);

            float depthIndicator = smoothstep(0.05, 0.7, r);
            float recursionDepth = floor(abs(logr - mappedLogR) / period + 0.5);
            float depthTint = fract(recursionDepth * 0.15 + uTime * 0.08);

            vec3 warmShift = vec3(0.08, 0.03, -0.04) * depthTint;
            sampled.rgb += warmShift;

            float depthFade = clamp(1.3 - r * 0.65, 0.4, 1.0);
            sampled.rgb *= depthFade;

            float recursionEdge = abs(fract((logr - spiralTwist) / period + animPhase) - 0.48) * 20.0;
            float edgeFade = smoothstep(0.2, 0.6, recursionEdge);
            sampled.rgb *= 0.75 + 0.25 * edgeFade;

            fragColor = sampled;
        }
      `;

            // simple copy shader for final output
            const FS_COPY = `#version 300 es
        precision highp float;
        in vec2 vUv;
        out vec4 fragColor;
        uniform sampler2D uTex;
        void main() {
          vec4 col = texture(uTex, vUv);
          col.rgb = (col.rgb - 0.5) * 1.06 + 0.5;
          col.rgb = clamp(col.rgb, 0.0, 1.0);
          fragColor = col;
        }
      `;

            function compileShader(type, src) {
                const s = gl.createShader(type);
                gl.shaderSource(s, src);
                gl.compileShader(s);
                if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
                    console.error(gl.getShaderInfoLog(s));
                    gl.deleteShader(s);
                    return null;
                }
                return s;
            }

            function createProgram(vsSrc, fsSrc) {
                const vs = compileShader(gl.VERTEX_SHADER, vsSrc);
                const fs = compileShader(gl.FRAGMENT_SHADER, fsSrc);
                if (!vs || !fs) return null;
                const prog = gl.createProgram();
                gl.attachShader(prog, vs);
                gl.attachShader(prog, fs);
                gl.linkProgram(prog);
                if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
                    console.error(gl.getProgramInfoLog(prog));
                    return null;
                }
                gl.deleteShader(vs);
                gl.deleteShader(fs);
                return prog;
            }

            const progBase = createProgram(VS, FS_BASE);
            const progDroste = createProgram(VS, FS_DROSTE);
            const progCopy = createProgram(VS, FS_COPY);
            if (!progBase || !progDroste || !progCopy) throw new Error("Shader compilation failed");

            // geometry setup
            const vao = gl.createVertexArray();
            gl.bindVertexArray(vao);
            const vbo = gl.createBuffer();
            gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
            gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);
            const aPos = gl.getAttribLocation(progBase, 'aPos');
            gl.enableVertexAttribArray(aPos);
            gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);

            let fboA, fboB;
            function createFBO(w, h) {
                const tex = gl.createTexture();
                gl.bindTexture(gl.TEXTURE_2D, tex);
                gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
                gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
                gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
                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);
                const fbo = gl.createFramebuffer();
                gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
                gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
                const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
                if (status !== gl.FRAMEBUFFER_COMPLETE) console.warn("FBO incomplete", status);
                return { fbo, tex };
            }

            let screenW, screenH;
            function resize() {
                screenW = window.innerWidth;
                screenH = window.innerHeight;
                canvas.width = screenW;
                canvas.height = screenH;
                if (fboA) {
                    gl.deleteFramebuffer(fboA.fbo);
                    gl.deleteTexture(fboA.tex);
                    gl.deleteFramebuffer(fboB.fbo);
                    gl.deleteTexture(fboB.tex);
                }
                fboA = createFBO(screenW, screenH);
                fboB = createFBO(screenW, screenH);
            }
            window.addEventListener('resize', resize);
            resize();

            const locBaseTime = gl.getUniformLocation(progBase, 'uTime');
            const locBaseAspect = gl.getUniformLocation(progBase, 'uAspect');
            const locDrosteTime = gl.getUniformLocation(progDroste, 'uTime');
            const locDrosteAspect = gl.getUniformLocation(progDroste, 'uAspect');
            const locDrosteTex = gl.getUniformLocation(progDroste, 'uTex');
            const locCopyTex = gl.getUniformLocation(progCopy, 'uTex');

            const startTime = performance.now();

            function draw() {
                requestAnimationFrame(draw);
                const t = (performance.now() - startTime) * 0.001;
                const aspect = screenW / screenH;

                gl.bindVertexArray(vao);
                gl.viewport(0, 0, screenW, screenH);

                // 1. Render base fractal tile into FBO A
                gl.bindFramebuffer(gl.FRAMEBUFFER, fboA.fbo);
                gl.clearColor(0.0, 0.0, 0.0, 1.0);
                gl.clear(gl.COLOR_BUFFER_BIT);
                gl.useProgram(progBase);
                gl.uniform1f(locBaseTime, t);
                gl.uniform1f(locBaseAspect, aspect);
                gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

                // 2. Recursive conformal mapping (kaleidoscopic Droste)
                gl.useProgram(progDroste);
                gl.uniform1f(locDrosteTime, t);
                gl.uniform1f(locDrosteAspect, aspect);
                gl.uniform1i(locDrosteTex, 0);
                gl.activeTexture(gl.TEXTURE0);

                let src = fboA,
                    dst = fboB;
                for (let iter = 0; iter < 3; iter++) {
                    gl.bindFramebuffer(gl.FRAMEBUFFER, dst.fbo);
                    gl.clearColor(0.0, 0.0, 0.0, 1.0);
                    gl.clear(gl.COLOR_BUFFER_BIT);
                    gl.bindTexture(gl.TEXTURE_2D, src.tex);
                    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
                    let temp = src;
                    src = dst;
                    dst = temp;
                }

                // 3. Final present to screen
                gl.bindFramebuffer(gl.FRAMEBUFFER, null);
                gl.clearColor(0.0, 0.0, 0.0, 1.0);
                gl.clear(gl.COLOR_BUFFER_BIT);
                gl.useProgram(progCopy);
                gl.uniform1i(locCopyTex, 0);
                gl.bindTexture(gl.TEXTURE_2D, src.tex);
                gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
            }

            draw();
        })();
    </script>
</body>
</html>