[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: ['https://play.google.com/store/apps/details?id=games.paveldogreat.fluidsimfree', 'https://apps.apple.com/app/fluid-simulation/id1443124993']# Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 Pavel Dobryakov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# WebGL Fluid Simulation\n\n[Play here](https://paveldogreat.github.io/WebGL-Fluid-Simulation/)\n\n<img src=\"/screenshot.jpg?raw=true\" width=\"880\">\n\n## References\n\nhttps://developer.nvidia.com/gpugems/gpugems/part-vi-beyond-triangles/chapter-38-fast-fluid-dynamics-simulation-gpu\n\nhttps://github.com/mharrys/fluids-2d\n\nhttps://github.com/haxiomic/GPU-Fluid-Experiments\n\n## License\n\nThe code is available under the [MIT license](LICENSE)\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"utf-8\">\n        <meta http-equiv=\"Cache-Control\" content=\"no-cache\">\n\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n        <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">\n        <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n        <meta name=\"mobile-web-app-capable\" content=\"yes\">\n\n        <link rel=\"apple-touch-icon\" href=\"logo.png\">\n        <link rel=\"icon\" href=\"logo.png\">\n\n        <title>WebGL Fluid Simulation</title>\n        <meta name=\"description\" content=\"A WebGL fluid simulation that works in mobile browsers.\">\n\n        <meta property=\"og:type\" content=\"website\">\n        <meta property=\"og:title\" content=\"Webgl Fluid Simulation\">\n        <meta property=\"og:description\" content=\"A WebGL fluid simulation that works in mobile browsers.\">\n        <meta property=\"og:url\" content=\"https://paveldogreat.github.io/WebGL-Fluid-Simulation/\">\n        <meta property=\"og:image\" content=\"https://paveldogreat.github.io/WebGL-Fluid-Simulation/logo.png\">\n\n        <script type=\"text/javascript\" src=\"dat.gui.min.js\"></script>\n        <style>\n            @font-face {\n                font-family: 'iconfont';\n                src: url('iconfont.ttf') format('truetype');\n            }\n\n            * {\n                user-select: none;\n            }\n\n            html, body {\n                overflow: hidden;\n                background-color: #000;\n            }\n\n            body {\n                margin: 0;\n                position: fixed;\n                width: 100%;\n                height: 100%;\n            }\n\n            canvas {\n                width: 100%;\n                height: 100%;\n            }\n\n            .dg {\n                opacity: 0.9;\n            }\n\n            .dg .property-name {\n                overflow: visible;\n            }\n\n            .bigFont {\n                font-size: 150%;\n                color: #8C8C8C;\n            }\n\n            .cr.function.appBigFont {\n                font-size: 150%;\n                line-height: 27px;\n                color: #A5F8D3;\n                background-color: #023C40;\n            }\n\n            .cr.function.appBigFont .property-name {\n                float: none;\n            }\n\n            .cr.function.appBigFont .icon {\n                position: sticky;\n                bottom: 27px;\n            }\n\n            .icon {\n                font-family: 'iconfont';\n                font-size: 130%;\n                float: right;\n            }\n\n            .twitter:before {\n                content: 'a';\n            }\n\n            .github:before {\n                content: 'b';\n            }\n\n            .app:before {\n                content: 'c';\n            }\n\n            .discord:before {\n                content: 'd';\n            }\n\n            .promo {\n                display: none;\n                /* display: table; */\n                position: absolute;\n                top: 0;\n                left: 0;\n                width: 100%;\n                height: 100%;\n                z-index: 1;\n                overflow: auto;\n                color: lightblue;\n                background-color: rgba(0,0,0,0.4);\n                animation: promo-appear-animation 0.35s ease-out;\n            }\n\n            .promo-middle {\n                display: table-cell;\n                vertical-align: middle;\n            }\n\n            .promo-content {\n                width: 80vw;\n                height: 80vh;\n                max-width: 80vh;\n                max-height: 80vw;\n                margin: auto;\n                padding: 0;\n                font-size: 2.8vmax;\n                font-family: Futura, \"Trebuchet MS\", Arial, sans-serif;\n                text-align: center;\n                background-image: url(\"promo_back.png\");\n                background-position: center;\n                background-repeat: no-repeat;\n                background-size: cover;\n                border-radius: 15px;\n                box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19);\n            }\n\n            .promo-header {\n                height: 10%;\n                padding: 2px 16px;\n            }\n\n            .promo-close {\n                width: 10%;\n                height: 100%;\n                text-align: left;\n                float: left;\n                font-size: 1.3em;\n                /* transition: 0.2s; */\n            }\n\n            .promo-close:hover {\n                /* transform: scale(1.25); */\n                cursor: pointer;\n            }\n\n            .promo-body {\n                padding: 8px 16px 16px 16px;\n                margin: auto;\n            }\n\n            .promo-body p {\n                margin-top: 0;\n                mix-blend-mode: color-dodge;\n            }\n\n            .link {\n                width: 100%;\n                display: inline-block;\n            }\n\n            .link img {\n                width: 100%;\n            }\n\n            @keyframes promo-appear-animation {\n                0% {\n                    transform: scale(2.0);\n                    opacity: 0;\n                }\n                100% {\n                    transform: scale(1.0);\n                    opacity: 1;\n                }\n            }\n        </style>\n        <script>\n            window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;\n            ga('create', 'UA-105392568-1', 'auto');\n            ga('send', 'pageview');\n        </script>\n        <script async src=\"https://www.google-analytics.com/analytics.js\"></script>\n    </head>\n    <body>\n        <canvas></canvas>\n\n        <!-- Mother of God, pls forgive me -->\n        <div class=\"promo\">\n            <div class=\"promo-middle\">\n                <div class=\"promo-content\">\n                    <div class=\"promo-header\">\n                        <span class=\"promo-close\">&times;</span>\n                    </div>\n                    <div class=\"promo-body\">\n                        <p>Try Fluid Simulation app!</p>\n                        <div class=\"links-container\">\n                            <a class=\"link\" id=\"apple_link\" target=\"_blank\">\n                                <img class=\"link-img\" alt=\"Download on the App Store\" src=\"app_badge.png\"/>\n                            </a>\n                            <a class=\"link\" id=\"google_link\" target=\"_blank\">\n                                <img class=\"link-img\" alt=\"Get it on Google Play\" src=\"gp_badge.png\"/>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        </div>\n\n        <script src=\"./script.js\"></script>\n    </body>\n</html>"
  },
  {
    "path": "script.js",
    "content": "/*\nMIT License\n\nCopyright (c) 2017 Pavel Dobryakov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n*/\n\n'use strict';\n\n// Mobile promo section\n\nconst promoPopup = document.getElementsByClassName('promo')[0];\nconst promoPopupClose = document.getElementsByClassName('promo-close')[0];\n\nif (isMobile()) {\n    setTimeout(() => {\n        promoPopup.style.display = 'table';\n    }, 20000);\n}\n\npromoPopupClose.addEventListener('click', e => {\n    promoPopup.style.display = 'none';\n});\n\nconst appleLink = document.getElementById('apple_link');\nappleLink.addEventListener('click', e => {\n    ga('send', 'event', 'link promo', 'app');\n    window.open('https://apps.apple.com/us/app/fluid-simulation/id1443124993');\n});\n\nconst googleLink = document.getElementById('google_link');\ngoogleLink.addEventListener('click', e => {\n    ga('send', 'event', 'link promo', 'app');\n    window.open('https://play.google.com/store/apps/details?id=games.paveldogreat.fluidsimfree');\n});\n\n// Simulation section\n\nconst canvas = document.getElementsByTagName('canvas')[0];\nresizeCanvas();\n\nlet config = {\n    SIM_RESOLUTION: 128,\n    DYE_RESOLUTION: 1024,\n    CAPTURE_RESOLUTION: 512,\n    DENSITY_DISSIPATION: 1,\n    VELOCITY_DISSIPATION: 0.2,\n    PRESSURE: 0.8,\n    PRESSURE_ITERATIONS: 20,\n    CURL: 30,\n    SPLAT_RADIUS: 0.25,\n    SPLAT_FORCE: 6000,\n    SHADING: true,\n    COLORFUL: true,\n    COLOR_UPDATE_SPEED: 10,\n    PAUSED: false,\n    BACK_COLOR: { r: 0, g: 0, b: 0 },\n    TRANSPARENT: false,\n    BLOOM: true,\n    BLOOM_ITERATIONS: 8,\n    BLOOM_RESOLUTION: 256,\n    BLOOM_INTENSITY: 0.8,\n    BLOOM_THRESHOLD: 0.6,\n    BLOOM_SOFT_KNEE: 0.7,\n    SUNRAYS: true,\n    SUNRAYS_RESOLUTION: 196,\n    SUNRAYS_WEIGHT: 1.0,\n}\n\nfunction pointerPrototype () {\n    this.id = -1;\n    this.texcoordX = 0;\n    this.texcoordY = 0;\n    this.prevTexcoordX = 0;\n    this.prevTexcoordY = 0;\n    this.deltaX = 0;\n    this.deltaY = 0;\n    this.down = false;\n    this.moved = false;\n    this.color = [30, 0, 300];\n}\n\nlet pointers = [];\nlet splatStack = [];\npointers.push(new pointerPrototype());\n\nconst { gl, ext } = getWebGLContext(canvas);\n\nif (isMobile()) {\n    config.DYE_RESOLUTION = 512;\n}\nif (!ext.supportLinearFiltering) {\n    config.DYE_RESOLUTION = 512;\n    config.SHADING = false;\n    config.BLOOM = false;\n    config.SUNRAYS = false;\n}\n\nstartGUI();\n\nfunction getWebGLContext (canvas) {\n    const params = { alpha: true, depth: false, stencil: false, antialias: false, preserveDrawingBuffer: false };\n\n    let gl = canvas.getContext('webgl2', params);\n    const isWebGL2 = !!gl;\n    if (!isWebGL2)\n        gl = canvas.getContext('webgl', params) || canvas.getContext('experimental-webgl', params);\n\n    let halfFloat;\n    let supportLinearFiltering;\n    if (isWebGL2) {\n        gl.getExtension('EXT_color_buffer_float');\n        supportLinearFiltering = gl.getExtension('OES_texture_float_linear');\n    } else {\n        halfFloat = gl.getExtension('OES_texture_half_float');\n        supportLinearFiltering = gl.getExtension('OES_texture_half_float_linear');\n    }\n\n    gl.clearColor(0.0, 0.0, 0.0, 1.0);\n\n    const halfFloatTexType = isWebGL2 ? gl.HALF_FLOAT : halfFloat.HALF_FLOAT_OES;\n    let formatRGBA;\n    let formatRG;\n    let formatR;\n\n    if (isWebGL2)\n    {\n        formatRGBA = getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, halfFloatTexType);\n        formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType);\n        formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType);\n    }\n    else\n    {\n        formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);\n        formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);\n        formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);\n    }\n\n    ga('send', 'event', isWebGL2 ? 'webgl2' : 'webgl', formatRGBA == null ? 'not supported' : 'supported');\n\n    return {\n        gl,\n        ext: {\n            formatRGBA,\n            formatRG,\n            formatR,\n            halfFloatTexType,\n            supportLinearFiltering\n        }\n    };\n}\n\nfunction getSupportedFormat (gl, internalFormat, format, type)\n{\n    if (!supportRenderTextureFormat(gl, internalFormat, format, type))\n    {\n        switch (internalFormat)\n        {\n            case gl.R16F:\n                return getSupportedFormat(gl, gl.RG16F, gl.RG, type);\n            case gl.RG16F:\n                return getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, type);\n            default:\n                return null;\n        }\n    }\n\n    return {\n        internalFormat,\n        format\n    }\n}\n\nfunction supportRenderTextureFormat (gl, internalFormat, format, type) {\n    let texture = gl.createTexture();\n    gl.bindTexture(gl.TEXTURE_2D, texture);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n    gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null);\n\n    let fbo = gl.createFramebuffer();\n    gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);\n    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);\n\n    let status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);\n    return status == gl.FRAMEBUFFER_COMPLETE;\n}\n\nfunction startGUI () {\n    var gui = new dat.GUI({ width: 300 });\n    gui.add(config, 'DYE_RESOLUTION', { 'high': 1024, 'medium': 512, 'low': 256, 'very low': 128 }).name('quality').onFinishChange(initFramebuffers);\n    gui.add(config, 'SIM_RESOLUTION', { '32': 32, '64': 64, '128': 128, '256': 256 }).name('sim resolution').onFinishChange(initFramebuffers);\n    gui.add(config, 'DENSITY_DISSIPATION', 0, 4.0).name('density diffusion');\n    gui.add(config, 'VELOCITY_DISSIPATION', 0, 4.0).name('velocity diffusion');\n    gui.add(config, 'PRESSURE', 0.0, 1.0).name('pressure');\n    gui.add(config, 'CURL', 0, 50).name('vorticity').step(1);\n    gui.add(config, 'SPLAT_RADIUS', 0.01, 1.0).name('splat radius');\n    gui.add(config, 'SHADING').name('shading').onFinishChange(updateKeywords);\n    gui.add(config, 'COLORFUL').name('colorful');\n    gui.add(config, 'PAUSED').name('paused').listen();\n\n    gui.add({ fun: () => {\n        splatStack.push(parseInt(Math.random() * 20) + 5);\n    } }, 'fun').name('Random splats');\n\n    let bloomFolder = gui.addFolder('Bloom');\n    bloomFolder.add(config, 'BLOOM').name('enabled').onFinishChange(updateKeywords);\n    bloomFolder.add(config, 'BLOOM_INTENSITY', 0.1, 2.0).name('intensity');\n    bloomFolder.add(config, 'BLOOM_THRESHOLD', 0.0, 1.0).name('threshold');\n\n    let sunraysFolder = gui.addFolder('Sunrays');\n    sunraysFolder.add(config, 'SUNRAYS').name('enabled').onFinishChange(updateKeywords);\n    sunraysFolder.add(config, 'SUNRAYS_WEIGHT', 0.3, 1.0).name('weight');\n\n    let captureFolder = gui.addFolder('Capture');\n    captureFolder.addColor(config, 'BACK_COLOR').name('background color');\n    captureFolder.add(config, 'TRANSPARENT').name('transparent');\n    captureFolder.add({ fun: captureScreenshot }, 'fun').name('take screenshot');\n\n    let github = gui.add({ fun : () => {\n        window.open('https://github.com/PavelDoGreat/WebGL-Fluid-Simulation');\n        ga('send', 'event', 'link button', 'github');\n    } }, 'fun').name('Github');\n    github.__li.className = 'cr function bigFont';\n    github.__li.style.borderLeft = '3px solid #8C8C8C';\n    let githubIcon = document.createElement('span');\n    github.domElement.parentElement.appendChild(githubIcon);\n    githubIcon.className = 'icon github';\n\n    let twitter = gui.add({ fun : () => {\n        ga('send', 'event', 'link button', 'twitter');\n        window.open('https://twitter.com/PavelDoGreat');\n    } }, 'fun').name('Twitter');\n    twitter.__li.className = 'cr function bigFont';\n    twitter.__li.style.borderLeft = '3px solid #8C8C8C';\n    let twitterIcon = document.createElement('span');\n    twitter.domElement.parentElement.appendChild(twitterIcon);\n    twitterIcon.className = 'icon twitter';\n\n    let discord = gui.add({ fun : () => {\n        ga('send', 'event', 'link button', 'discord');\n        window.open('https://discordapp.com/invite/CeqZDDE');\n    } }, 'fun').name('Discord');\n    discord.__li.className = 'cr function bigFont';\n    discord.__li.style.borderLeft = '3px solid #8C8C8C';\n    let discordIcon = document.createElement('span');\n    discord.domElement.parentElement.appendChild(discordIcon);\n    discordIcon.className = 'icon discord';\n\n    let app = gui.add({ fun : () => {\n        ga('send', 'event', 'link button', 'app');\n        window.open('http://onelink.to/5b58bn');\n    } }, 'fun').name('Check out mobile app');\n    app.__li.className = 'cr function appBigFont';\n    app.__li.style.borderLeft = '3px solid #00FF7F';\n    let appIcon = document.createElement('span');\n    app.domElement.parentElement.appendChild(appIcon);\n    appIcon.className = 'icon app';\n\n    if (isMobile())\n        gui.close();\n}\n\nfunction isMobile () {\n    return /Mobi|Android/i.test(navigator.userAgent);\n}\n\nfunction captureScreenshot () {\n    let res = getResolution(config.CAPTURE_RESOLUTION);\n    let target = createFBO(res.width, res.height, ext.formatRGBA.internalFormat, ext.formatRGBA.format, ext.halfFloatTexType, gl.NEAREST);\n    render(target);\n\n    let texture = framebufferToTexture(target);\n    texture = normalizeTexture(texture, target.width, target.height);\n\n    let captureCanvas = textureToCanvas(texture, target.width, target.height);\n    let datauri = captureCanvas.toDataURL();\n    downloadURI('fluid.png', datauri);\n    URL.revokeObjectURL(datauri);\n}\n\nfunction framebufferToTexture (target) {\n    gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);\n    let length = target.width * target.height * 4;\n    let texture = new Float32Array(length);\n    gl.readPixels(0, 0, target.width, target.height, gl.RGBA, gl.FLOAT, texture);\n    return texture;\n}\n\nfunction normalizeTexture (texture, width, height) {\n    let result = new Uint8Array(texture.length);\n    let id = 0;\n    for (let i = height - 1; i >= 0; i--) {\n        for (let j = 0; j < width; j++) {\n            let nid = i * width * 4 + j * 4;\n            result[nid + 0] = clamp01(texture[id + 0]) * 255;\n            result[nid + 1] = clamp01(texture[id + 1]) * 255;\n            result[nid + 2] = clamp01(texture[id + 2]) * 255;\n            result[nid + 3] = clamp01(texture[id + 3]) * 255;\n            id += 4;\n        }\n    }\n    return result;\n}\n\nfunction clamp01 (input) {\n    return Math.min(Math.max(input, 0), 1);\n}\n\nfunction textureToCanvas (texture, width, height) {\n    let captureCanvas = document.createElement('canvas');\n    let ctx = captureCanvas.getContext('2d');\n    captureCanvas.width = width;\n    captureCanvas.height = height;\n\n    let imageData = ctx.createImageData(width, height);\n    imageData.data.set(texture);\n    ctx.putImageData(imageData, 0, 0);\n\n    return captureCanvas;\n}\n\nfunction downloadURI (filename, uri) {\n    let link = document.createElement('a');\n    link.download = filename;\n    link.href = uri;\n    document.body.appendChild(link);\n    link.click();\n    document.body.removeChild(link);\n}\n\nclass Material {\n    constructor (vertexShader, fragmentShaderSource) {\n        this.vertexShader = vertexShader;\n        this.fragmentShaderSource = fragmentShaderSource;\n        this.programs = [];\n        this.activeProgram = null;\n        this.uniforms = [];\n    }\n\n    setKeywords (keywords) {\n        let hash = 0;\n        for (let i = 0; i < keywords.length; i++)\n            hash += hashCode(keywords[i]);\n\n        let program = this.programs[hash];\n        if (program == null)\n        {\n            let fragmentShader = compileShader(gl.FRAGMENT_SHADER, this.fragmentShaderSource, keywords);\n            program = createProgram(this.vertexShader, fragmentShader);\n            this.programs[hash] = program;\n        }\n\n        if (program == this.activeProgram) return;\n\n        this.uniforms = getUniforms(program);\n        this.activeProgram = program;\n    }\n\n    bind () {\n        gl.useProgram(this.activeProgram);\n    }\n}\n\nclass Program {\n    constructor (vertexShader, fragmentShader) {\n        this.uniforms = {};\n        this.program = createProgram(vertexShader, fragmentShader);\n        this.uniforms = getUniforms(this.program);\n    }\n\n    bind () {\n        gl.useProgram(this.program);\n    }\n}\n\nfunction createProgram (vertexShader, fragmentShader) {\n    let program = gl.createProgram();\n    gl.attachShader(program, vertexShader);\n    gl.attachShader(program, fragmentShader);\n    gl.linkProgram(program);\n\n    if (!gl.getProgramParameter(program, gl.LINK_STATUS))\n        console.trace(gl.getProgramInfoLog(program));\n\n    return program;\n}\n\nfunction getUniforms (program) {\n    let uniforms = [];\n    let uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);\n    for (let i = 0; i < uniformCount; i++) {\n        let uniformName = gl.getActiveUniform(program, i).name;\n        uniforms[uniformName] = gl.getUniformLocation(program, uniformName);\n    }\n    return uniforms;\n}\n\nfunction compileShader (type, source, keywords) {\n    source = addKeywords(source, keywords);\n\n    const shader = gl.createShader(type);\n    gl.shaderSource(shader, source);\n    gl.compileShader(shader);\n\n    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))\n        console.trace(gl.getShaderInfoLog(shader));\n\n    return shader;\n};\n\nfunction addKeywords (source, keywords) {\n    if (keywords == null) return source;\n    let keywordsString = '';\n    keywords.forEach(keyword => {\n        keywordsString += '#define ' + keyword + '\\n';\n    });\n    return keywordsString + source;\n}\n\nconst baseVertexShader = compileShader(gl.VERTEX_SHADER, `\n    precision highp float;\n\n    attribute vec2 aPosition;\n    varying vec2 vUv;\n    varying vec2 vL;\n    varying vec2 vR;\n    varying vec2 vT;\n    varying vec2 vB;\n    uniform vec2 texelSize;\n\n    void main () {\n        vUv = aPosition * 0.5 + 0.5;\n        vL = vUv - vec2(texelSize.x, 0.0);\n        vR = vUv + vec2(texelSize.x, 0.0);\n        vT = vUv + vec2(0.0, texelSize.y);\n        vB = vUv - vec2(0.0, texelSize.y);\n        gl_Position = vec4(aPosition, 0.0, 1.0);\n    }\n`);\n\nconst blurVertexShader = compileShader(gl.VERTEX_SHADER, `\n    precision highp float;\n\n    attribute vec2 aPosition;\n    varying vec2 vUv;\n    varying vec2 vL;\n    varying vec2 vR;\n    uniform vec2 texelSize;\n\n    void main () {\n        vUv = aPosition * 0.5 + 0.5;\n        float offset = 1.33333333;\n        vL = vUv - texelSize * offset;\n        vR = vUv + texelSize * offset;\n        gl_Position = vec4(aPosition, 0.0, 1.0);\n    }\n`);\n\nconst blurShader = compileShader(gl.FRAGMENT_SHADER, `\n    precision mediump float;\n    precision mediump sampler2D;\n\n    varying vec2 vUv;\n    varying vec2 vL;\n    varying vec2 vR;\n    uniform sampler2D uTexture;\n\n    void main () {\n        vec4 sum = texture2D(uTexture, vUv) * 0.29411764;\n        sum += texture2D(uTexture, vL) * 0.35294117;\n        sum += texture2D(uTexture, vR) * 0.35294117;\n        gl_FragColor = sum;\n    }\n`);\n\nconst copyShader = compileShader(gl.FRAGMENT_SHADER, `\n    precision mediump float;\n    precision mediump sampler2D;\n\n    varying highp vec2 vUv;\n    uniform sampler2D uTexture;\n\n    void main () {\n        gl_FragColor = texture2D(uTexture, vUv);\n    }\n`);\n\nconst clearShader = compileShader(gl.FRAGMENT_SHADER, `\n    precision mediump float;\n    precision mediump sampler2D;\n\n    varying highp vec2 vUv;\n    uniform sampler2D uTexture;\n    uniform float value;\n\n    void main () {\n        gl_FragColor = value * texture2D(uTexture, vUv);\n    }\n`);\n\nconst colorShader = compileShader(gl.FRAGMENT_SHADER, `\n    precision mediump float;\n\n    uniform vec4 color;\n\n    void main () {\n        gl_FragColor = color;\n    }\n`);\n\nconst checkerboardShader = compileShader(gl.FRAGMENT_SHADER, `\n    precision highp float;\n    precision highp sampler2D;\n\n    varying vec2 vUv;\n    uniform sampler2D uTexture;\n    uniform float aspectRatio;\n\n    #define SCALE 25.0\n\n    void main () {\n        vec2 uv = floor(vUv * SCALE * vec2(aspectRatio, 1.0));\n        float v = mod(uv.x + uv.y, 2.0);\n        v = v * 0.1 + 0.8;\n        gl_FragColor = vec4(vec3(v), 1.0);\n    }\n`);\n\nconst displayShaderSource = `\n    precision highp float;\n    precision highp sampler2D;\n\n    varying vec2 vUv;\n    varying vec2 vL;\n    varying vec2 vR;\n    varying vec2 vT;\n    varying vec2 vB;\n    uniform sampler2D uTexture;\n    uniform sampler2D uBloom;\n    uniform sampler2D uSunrays;\n    uniform sampler2D uDithering;\n    uniform vec2 ditherScale;\n    uniform vec2 texelSize;\n\n    vec3 linearToGamma (vec3 color) {\n        color = max(color, vec3(0));\n        return max(1.055 * pow(color, vec3(0.416666667)) - 0.055, vec3(0));\n    }\n\n    void main () {\n        vec3 c = texture2D(uTexture, vUv).rgb;\n\n    #ifdef SHADING\n        vec3 lc = texture2D(uTexture, vL).rgb;\n        vec3 rc = texture2D(uTexture, vR).rgb;\n        vec3 tc = texture2D(uTexture, vT).rgb;\n        vec3 bc = texture2D(uTexture, vB).rgb;\n\n        float dx = length(rc) - length(lc);\n        float dy = length(tc) - length(bc);\n\n        vec3 n = normalize(vec3(dx, dy, length(texelSize)));\n        vec3 l = vec3(0.0, 0.0, 1.0);\n\n        float diffuse = clamp(dot(n, l) + 0.7, 0.7, 1.0);\n        c *= diffuse;\n    #endif\n\n    #ifdef BLOOM\n        vec3 bloom = texture2D(uBloom, vUv).rgb;\n    #endif\n\n    #ifdef SUNRAYS\n        float sunrays = texture2D(uSunrays, vUv).r;\n        c *= sunrays;\n    #ifdef BLOOM\n        bloom *= sunrays;\n    #endif\n    #endif\n\n    #ifdef BLOOM\n        float noise = texture2D(uDithering, vUv * ditherScale).r;\n        noise = noise * 2.0 - 1.0;\n        bloom += noise / 255.0;\n        bloom = linearToGamma(bloom);\n        c += bloom;\n    #endif\n\n        float a = max(c.r, max(c.g, c.b));\n        gl_FragColor = vec4(c, a);\n    }\n`;\n\nconst bloomPrefilterShader = compileShader(gl.FRAGMENT_SHADER, `\n    precision mediump float;\n    precision mediump sampler2D;\n\n    varying vec2 vUv;\n    uniform sampler2D uTexture;\n    uniform vec3 curve;\n    uniform float threshold;\n\n    void main () {\n        vec3 c = texture2D(uTexture, vUv).rgb;\n        float br = max(c.r, max(c.g, c.b));\n        float rq = clamp(br - curve.x, 0.0, curve.y);\n        rq = curve.z * rq * rq;\n        c *= max(rq, br - threshold) / max(br, 0.0001);\n        gl_FragColor = vec4(c, 0.0);\n    }\n`);\n\nconst bloomBlurShader = compileShader(gl.FRAGMENT_SHADER, `\n    precision mediump float;\n    precision mediump sampler2D;\n\n    varying vec2 vL;\n    varying vec2 vR;\n    varying vec2 vT;\n    varying vec2 vB;\n    uniform sampler2D uTexture;\n\n    void main () {\n        vec4 sum = vec4(0.0);\n        sum += texture2D(uTexture, vL);\n        sum += texture2D(uTexture, vR);\n        sum += texture2D(uTexture, vT);\n        sum += texture2D(uTexture, vB);\n        sum *= 0.25;\n        gl_FragColor = sum;\n    }\n`);\n\nconst bloomFinalShader = compileShader(gl.FRAGMENT_SHADER, `\n    precision mediump float;\n    precision mediump sampler2D;\n\n    varying vec2 vL;\n    varying vec2 vR;\n    varying vec2 vT;\n    varying vec2 vB;\n    uniform sampler2D uTexture;\n    uniform float intensity;\n\n    void main () {\n        vec4 sum = vec4(0.0);\n        sum += texture2D(uTexture, vL);\n        sum += texture2D(uTexture, vR);\n        sum += texture2D(uTexture, vT);\n        sum += texture2D(uTexture, vB);\n        sum *= 0.25;\n        gl_FragColor = sum * intensity;\n    }\n`);\n\nconst sunraysMaskShader = compileShader(gl.FRAGMENT_SHADER, `\n    precision highp float;\n    precision highp sampler2D;\n\n    varying vec2 vUv;\n    uniform sampler2D uTexture;\n\n    void main () {\n        vec4 c = texture2D(uTexture, vUv);\n        float br = max(c.r, max(c.g, c.b));\n        c.a = 1.0 - min(max(br * 20.0, 0.0), 0.8);\n        gl_FragColor = c;\n    }\n`);\n\nconst sunraysShader = compileShader(gl.FRAGMENT_SHADER, `\n    precision highp float;\n    precision highp sampler2D;\n\n    varying vec2 vUv;\n    uniform sampler2D uTexture;\n    uniform float weight;\n\n    #define ITERATIONS 16\n\n    void main () {\n        float Density = 0.3;\n        float Decay = 0.95;\n        float Exposure = 0.7;\n\n        vec2 coord = vUv;\n        vec2 dir = vUv - 0.5;\n\n        dir *= 1.0 / float(ITERATIONS) * Density;\n        float illuminationDecay = 1.0;\n\n        float color = texture2D(uTexture, vUv).a;\n\n        for (int i = 0; i < ITERATIONS; i++)\n        {\n            coord -= dir;\n            float col = texture2D(uTexture, coord).a;\n            color += col * illuminationDecay * weight;\n            illuminationDecay *= Decay;\n        }\n\n        gl_FragColor = vec4(color * Exposure, 0.0, 0.0, 1.0);\n    }\n`);\n\nconst splatShader = compileShader(gl.FRAGMENT_SHADER, `\n    precision highp float;\n    precision highp sampler2D;\n\n    varying vec2 vUv;\n    uniform sampler2D uTarget;\n    uniform float aspectRatio;\n    uniform vec3 color;\n    uniform vec2 point;\n    uniform float radius;\n\n    void main () {\n        vec2 p = vUv - point.xy;\n        p.x *= aspectRatio;\n        vec3 splat = exp(-dot(p, p) / radius) * color;\n        vec3 base = texture2D(uTarget, vUv).xyz;\n        gl_FragColor = vec4(base + splat, 1.0);\n    }\n`);\n\nconst advectionShader = compileShader(gl.FRAGMENT_SHADER, `\n    precision highp float;\n    precision highp sampler2D;\n\n    varying vec2 vUv;\n    uniform sampler2D uVelocity;\n    uniform sampler2D uSource;\n    uniform vec2 texelSize;\n    uniform vec2 dyeTexelSize;\n    uniform float dt;\n    uniform float dissipation;\n\n    vec4 bilerp (sampler2D sam, vec2 uv, vec2 tsize) {\n        vec2 st = uv / tsize - 0.5;\n\n        vec2 iuv = floor(st);\n        vec2 fuv = fract(st);\n\n        vec4 a = texture2D(sam, (iuv + vec2(0.5, 0.5)) * tsize);\n        vec4 b = texture2D(sam, (iuv + vec2(1.5, 0.5)) * tsize);\n        vec4 c = texture2D(sam, (iuv + vec2(0.5, 1.5)) * tsize);\n        vec4 d = texture2D(sam, (iuv + vec2(1.5, 1.5)) * tsize);\n\n        return mix(mix(a, b, fuv.x), mix(c, d, fuv.x), fuv.y);\n    }\n\n    void main () {\n    #ifdef MANUAL_FILTERING\n        vec2 coord = vUv - dt * bilerp(uVelocity, vUv, texelSize).xy * texelSize;\n        vec4 result = bilerp(uSource, coord, dyeTexelSize);\n    #else\n        vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize;\n        vec4 result = texture2D(uSource, coord);\n    #endif\n        float decay = 1.0 + dissipation * dt;\n        gl_FragColor = result / decay;\n    }`,\n    ext.supportLinearFiltering ? null : ['MANUAL_FILTERING']\n);\n\nconst divergenceShader = compileShader(gl.FRAGMENT_SHADER, `\n    precision mediump float;\n    precision mediump sampler2D;\n\n    varying highp vec2 vUv;\n    varying highp vec2 vL;\n    varying highp vec2 vR;\n    varying highp vec2 vT;\n    varying highp vec2 vB;\n    uniform sampler2D uVelocity;\n\n    void main () {\n        float L = texture2D(uVelocity, vL).x;\n        float R = texture2D(uVelocity, vR).x;\n        float T = texture2D(uVelocity, vT).y;\n        float B = texture2D(uVelocity, vB).y;\n\n        vec2 C = texture2D(uVelocity, vUv).xy;\n        if (vL.x < 0.0) { L = -C.x; }\n        if (vR.x > 1.0) { R = -C.x; }\n        if (vT.y > 1.0) { T = -C.y; }\n        if (vB.y < 0.0) { B = -C.y; }\n\n        float div = 0.5 * (R - L + T - B);\n        gl_FragColor = vec4(div, 0.0, 0.0, 1.0);\n    }\n`);\n\nconst curlShader = compileShader(gl.FRAGMENT_SHADER, `\n    precision mediump float;\n    precision mediump sampler2D;\n\n    varying highp vec2 vUv;\n    varying highp vec2 vL;\n    varying highp vec2 vR;\n    varying highp vec2 vT;\n    varying highp vec2 vB;\n    uniform sampler2D uVelocity;\n\n    void main () {\n        float L = texture2D(uVelocity, vL).y;\n        float R = texture2D(uVelocity, vR).y;\n        float T = texture2D(uVelocity, vT).x;\n        float B = texture2D(uVelocity, vB).x;\n        float vorticity = R - L - T + B;\n        gl_FragColor = vec4(0.5 * vorticity, 0.0, 0.0, 1.0);\n    }\n`);\n\nconst vorticityShader = compileShader(gl.FRAGMENT_SHADER, `\n    precision highp float;\n    precision highp sampler2D;\n\n    varying vec2 vUv;\n    varying vec2 vL;\n    varying vec2 vR;\n    varying vec2 vT;\n    varying vec2 vB;\n    uniform sampler2D uVelocity;\n    uniform sampler2D uCurl;\n    uniform float curl;\n    uniform float dt;\n\n    void main () {\n        float L = texture2D(uCurl, vL).x;\n        float R = texture2D(uCurl, vR).x;\n        float T = texture2D(uCurl, vT).x;\n        float B = texture2D(uCurl, vB).x;\n        float C = texture2D(uCurl, vUv).x;\n\n        vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L));\n        force /= length(force) + 0.0001;\n        force *= curl * C;\n        force.y *= -1.0;\n\n        vec2 velocity = texture2D(uVelocity, vUv).xy;\n        velocity += force * dt;\n        velocity = min(max(velocity, -1000.0), 1000.0);\n        gl_FragColor = vec4(velocity, 0.0, 1.0);\n    }\n`);\n\nconst pressureShader = compileShader(gl.FRAGMENT_SHADER, `\n    precision mediump float;\n    precision mediump sampler2D;\n\n    varying highp vec2 vUv;\n    varying highp vec2 vL;\n    varying highp vec2 vR;\n    varying highp vec2 vT;\n    varying highp vec2 vB;\n    uniform sampler2D uPressure;\n    uniform sampler2D uDivergence;\n\n    void main () {\n        float L = texture2D(uPressure, vL).x;\n        float R = texture2D(uPressure, vR).x;\n        float T = texture2D(uPressure, vT).x;\n        float B = texture2D(uPressure, vB).x;\n        float C = texture2D(uPressure, vUv).x;\n        float divergence = texture2D(uDivergence, vUv).x;\n        float pressure = (L + R + B + T - divergence) * 0.25;\n        gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0);\n    }\n`);\n\nconst gradientSubtractShader = compileShader(gl.FRAGMENT_SHADER, `\n    precision mediump float;\n    precision mediump sampler2D;\n\n    varying highp vec2 vUv;\n    varying highp vec2 vL;\n    varying highp vec2 vR;\n    varying highp vec2 vT;\n    varying highp vec2 vB;\n    uniform sampler2D uPressure;\n    uniform sampler2D uVelocity;\n\n    void main () {\n        float L = texture2D(uPressure, vL).x;\n        float R = texture2D(uPressure, vR).x;\n        float T = texture2D(uPressure, vT).x;\n        float B = texture2D(uPressure, vB).x;\n        vec2 velocity = texture2D(uVelocity, vUv).xy;\n        velocity.xy -= vec2(R - L, T - B);\n        gl_FragColor = vec4(velocity, 0.0, 1.0);\n    }\n`);\n\nconst blit = (() => {\n    gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), gl.STATIC_DRAW);\n    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());\n    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW);\n    gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);\n    gl.enableVertexAttribArray(0);\n\n    return (target, clear = false) => {\n        if (target == null)\n        {\n            gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);\n            gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n        }\n        else\n        {\n            gl.viewport(0, 0, target.width, target.height);\n            gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);\n        }\n        if (clear)\n        {\n            gl.clearColor(0.0, 0.0, 0.0, 1.0);\n            gl.clear(gl.COLOR_BUFFER_BIT);\n        }\n        // CHECK_FRAMEBUFFER_STATUS();\n        gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);\n    }\n})();\n\nfunction CHECK_FRAMEBUFFER_STATUS () {\n    let status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);\n    if (status != gl.FRAMEBUFFER_COMPLETE)\n        console.trace(\"Framebuffer error: \" + status);\n}\n\nlet dye;\nlet velocity;\nlet divergence;\nlet curl;\nlet pressure;\nlet bloom;\nlet bloomFramebuffers = [];\nlet sunrays;\nlet sunraysTemp;\n\nlet ditheringTexture = createTextureAsync('LDR_LLL1_0.png');\n\nconst blurProgram            = new Program(blurVertexShader, blurShader);\nconst copyProgram            = new Program(baseVertexShader, copyShader);\nconst clearProgram           = new Program(baseVertexShader, clearShader);\nconst colorProgram           = new Program(baseVertexShader, colorShader);\nconst checkerboardProgram    = new Program(baseVertexShader, checkerboardShader);\nconst bloomPrefilterProgram  = new Program(baseVertexShader, bloomPrefilterShader);\nconst bloomBlurProgram       = new Program(baseVertexShader, bloomBlurShader);\nconst bloomFinalProgram      = new Program(baseVertexShader, bloomFinalShader);\nconst sunraysMaskProgram     = new Program(baseVertexShader, sunraysMaskShader);\nconst sunraysProgram         = new Program(baseVertexShader, sunraysShader);\nconst splatProgram           = new Program(baseVertexShader, splatShader);\nconst advectionProgram       = new Program(baseVertexShader, advectionShader);\nconst divergenceProgram      = new Program(baseVertexShader, divergenceShader);\nconst curlProgram            = new Program(baseVertexShader, curlShader);\nconst vorticityProgram       = new Program(baseVertexShader, vorticityShader);\nconst pressureProgram        = new Program(baseVertexShader, pressureShader);\nconst gradienSubtractProgram = new Program(baseVertexShader, gradientSubtractShader);\n\nconst displayMaterial = new Material(baseVertexShader, displayShaderSource);\n\nfunction initFramebuffers () {\n    let simRes = getResolution(config.SIM_RESOLUTION);\n    let dyeRes = getResolution(config.DYE_RESOLUTION);\n\n    const texType = ext.halfFloatTexType;\n    const rgba    = ext.formatRGBA;\n    const rg      = ext.formatRG;\n    const r       = ext.formatR;\n    const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST;\n\n    gl.disable(gl.BLEND);\n\n    if (dye == null)\n        dye = createDoubleFBO(dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering);\n    else\n        dye = resizeDoubleFBO(dye, dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering);\n\n    if (velocity == null)\n        velocity = createDoubleFBO(simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering);\n    else\n        velocity = resizeDoubleFBO(velocity, simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering);\n\n    divergence = createFBO      (simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST);\n    curl       = createFBO      (simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST);\n    pressure   = createDoubleFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST);\n\n    initBloomFramebuffers();\n    initSunraysFramebuffers();\n}\n\nfunction initBloomFramebuffers () {\n    let res = getResolution(config.BLOOM_RESOLUTION);\n\n    const texType = ext.halfFloatTexType;\n    const rgba = ext.formatRGBA;\n    const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST;\n\n    bloom = createFBO(res.width, res.height, rgba.internalFormat, rgba.format, texType, filtering);\n\n    bloomFramebuffers.length = 0;\n    for (let i = 0; i < config.BLOOM_ITERATIONS; i++)\n    {\n        let width = res.width >> (i + 1);\n        let height = res.height >> (i + 1);\n\n        if (width < 2 || height < 2) break;\n\n        let fbo = createFBO(width, height, rgba.internalFormat, rgba.format, texType, filtering);\n        bloomFramebuffers.push(fbo);\n    }\n}\n\nfunction initSunraysFramebuffers () {\n    let res = getResolution(config.SUNRAYS_RESOLUTION);\n\n    const texType = ext.halfFloatTexType;\n    const r = ext.formatR;\n    const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST;\n\n    sunrays     = createFBO(res.width, res.height, r.internalFormat, r.format, texType, filtering);\n    sunraysTemp = createFBO(res.width, res.height, r.internalFormat, r.format, texType, filtering);\n}\n\nfunction createFBO (w, h, internalFormat, format, type, param) {\n    gl.activeTexture(gl.TEXTURE0);\n    let texture = gl.createTexture();\n    gl.bindTexture(gl.TEXTURE_2D, texture);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n    gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null);\n\n    let fbo = gl.createFramebuffer();\n    gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);\n    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);\n    gl.viewport(0, 0, w, h);\n    gl.clear(gl.COLOR_BUFFER_BIT);\n\n    let texelSizeX = 1.0 / w;\n    let texelSizeY = 1.0 / h;\n\n    return {\n        texture,\n        fbo,\n        width: w,\n        height: h,\n        texelSizeX,\n        texelSizeY,\n        attach (id) {\n            gl.activeTexture(gl.TEXTURE0 + id);\n            gl.bindTexture(gl.TEXTURE_2D, texture);\n            return id;\n        }\n    };\n}\n\nfunction createDoubleFBO (w, h, internalFormat, format, type, param) {\n    let fbo1 = createFBO(w, h, internalFormat, format, type, param);\n    let fbo2 = createFBO(w, h, internalFormat, format, type, param);\n\n    return {\n        width: w,\n        height: h,\n        texelSizeX: fbo1.texelSizeX,\n        texelSizeY: fbo1.texelSizeY,\n        get read () {\n            return fbo1;\n        },\n        set read (value) {\n            fbo1 = value;\n        },\n        get write () {\n            return fbo2;\n        },\n        set write (value) {\n            fbo2 = value;\n        },\n        swap () {\n            let temp = fbo1;\n            fbo1 = fbo2;\n            fbo2 = temp;\n        }\n    }\n}\n\nfunction resizeFBO (target, w, h, internalFormat, format, type, param) {\n    let newFBO = createFBO(w, h, internalFormat, format, type, param);\n    copyProgram.bind();\n    gl.uniform1i(copyProgram.uniforms.uTexture, target.attach(0));\n    blit(newFBO);\n    return newFBO;\n}\n\nfunction resizeDoubleFBO (target, w, h, internalFormat, format, type, param) {\n    if (target.width == w && target.height == h)\n        return target;\n    target.read = resizeFBO(target.read, w, h, internalFormat, format, type, param);\n    target.write = createFBO(w, h, internalFormat, format, type, param);\n    target.width = w;\n    target.height = h;\n    target.texelSizeX = 1.0 / w;\n    target.texelSizeY = 1.0 / h;\n    return target;\n}\n\nfunction createTextureAsync (url) {\n    let texture = gl.createTexture();\n    gl.bindTexture(gl.TEXTURE_2D, texture);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);\n    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, 1, 1, 0, gl.RGB, gl.UNSIGNED_BYTE, new Uint8Array([255, 255, 255]));\n\n    let obj = {\n        texture,\n        width: 1,\n        height: 1,\n        attach (id) {\n            gl.activeTexture(gl.TEXTURE0 + id);\n            gl.bindTexture(gl.TEXTURE_2D, texture);\n            return id;\n        }\n    };\n\n    let image = new Image();\n    image.onload = () => {\n        obj.width = image.width;\n        obj.height = image.height;\n        gl.bindTexture(gl.TEXTURE_2D, texture);\n        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);\n    };\n    image.src = url;\n\n    return obj;\n}\n\nfunction updateKeywords () {\n    let displayKeywords = [];\n    if (config.SHADING) displayKeywords.push(\"SHADING\");\n    if (config.BLOOM) displayKeywords.push(\"BLOOM\");\n    if (config.SUNRAYS) displayKeywords.push(\"SUNRAYS\");\n    displayMaterial.setKeywords(displayKeywords);\n}\n\nupdateKeywords();\ninitFramebuffers();\nmultipleSplats(parseInt(Math.random() * 20) + 5);\n\nlet lastUpdateTime = Date.now();\nlet colorUpdateTimer = 0.0;\nupdate();\n\nfunction update () {\n    const dt = calcDeltaTime();\n    if (resizeCanvas())\n        initFramebuffers();\n    updateColors(dt);\n    applyInputs();\n    if (!config.PAUSED)\n        step(dt);\n    render(null);\n    requestAnimationFrame(update);\n}\n\nfunction calcDeltaTime () {\n    let now = Date.now();\n    let dt = (now - lastUpdateTime) / 1000;\n    dt = Math.min(dt, 0.016666);\n    lastUpdateTime = now;\n    return dt;\n}\n\nfunction resizeCanvas () {\n    let width = scaleByPixelRatio(canvas.clientWidth);\n    let height = scaleByPixelRatio(canvas.clientHeight);\n    if (canvas.width != width || canvas.height != height) {\n        canvas.width = width;\n        canvas.height = height;\n        return true;\n    }\n    return false;\n}\n\nfunction updateColors (dt) {\n    if (!config.COLORFUL) return;\n\n    colorUpdateTimer += dt * config.COLOR_UPDATE_SPEED;\n    if (colorUpdateTimer >= 1) {\n        colorUpdateTimer = wrap(colorUpdateTimer, 0, 1);\n        pointers.forEach(p => {\n            p.color = generateColor();\n        });\n    }\n}\n\nfunction applyInputs () {\n    if (splatStack.length > 0)\n        multipleSplats(splatStack.pop());\n\n    pointers.forEach(p => {\n        if (p.moved) {\n            p.moved = false;\n            splatPointer(p);\n        }\n    });\n}\n\nfunction step (dt) {\n    gl.disable(gl.BLEND);\n\n    curlProgram.bind();\n    gl.uniform2f(curlProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);\n    gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read.attach(0));\n    blit(curl);\n\n    vorticityProgram.bind();\n    gl.uniform2f(vorticityProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);\n    gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read.attach(0));\n    gl.uniform1i(vorticityProgram.uniforms.uCurl, curl.attach(1));\n    gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL);\n    gl.uniform1f(vorticityProgram.uniforms.dt, dt);\n    blit(velocity.write);\n    velocity.swap();\n\n    divergenceProgram.bind();\n    gl.uniform2f(divergenceProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);\n    gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0));\n    blit(divergence);\n\n    clearProgram.bind();\n    gl.uniform1i(clearProgram.uniforms.uTexture, pressure.read.attach(0));\n    gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE);\n    blit(pressure.write);\n    pressure.swap();\n\n    pressureProgram.bind();\n    gl.uniform2f(pressureProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);\n    gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence.attach(0));\n    for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) {\n        gl.uniform1i(pressureProgram.uniforms.uPressure, pressure.read.attach(1));\n        blit(pressure.write);\n        pressure.swap();\n    }\n\n    gradienSubtractProgram.bind();\n    gl.uniform2f(gradienSubtractProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);\n    gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressure.read.attach(0));\n    gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.read.attach(1));\n    blit(velocity.write);\n    velocity.swap();\n\n    advectionProgram.bind();\n    gl.uniform2f(advectionProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);\n    if (!ext.supportLinearFiltering)\n        gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, velocity.texelSizeX, velocity.texelSizeY);\n    let velocityId = velocity.read.attach(0);\n    gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId);\n    gl.uniform1i(advectionProgram.uniforms.uSource, velocityId);\n    gl.uniform1f(advectionProgram.uniforms.dt, dt);\n    gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION);\n    blit(velocity.write);\n    velocity.swap();\n\n    if (!ext.supportLinearFiltering)\n        gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, dye.texelSizeX, dye.texelSizeY);\n    gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0));\n    gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1));\n    gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION);\n    blit(dye.write);\n    dye.swap();\n}\n\nfunction render (target) {\n    if (config.BLOOM)\n        applyBloom(dye.read, bloom);\n    if (config.SUNRAYS) {\n        applySunrays(dye.read, dye.write, sunrays);\n        blur(sunrays, sunraysTemp, 1);\n    }\n\n    if (target == null || !config.TRANSPARENT) {\n        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);\n        gl.enable(gl.BLEND);\n    }\n    else {\n        gl.disable(gl.BLEND);\n    }\n\n    if (!config.TRANSPARENT)\n        drawColor(target, normalizeColor(config.BACK_COLOR));\n    if (target == null && config.TRANSPARENT)\n        drawCheckerboard(target);\n    drawDisplay(target);\n}\n\nfunction drawColor (target, color) {\n    colorProgram.bind();\n    gl.uniform4f(colorProgram.uniforms.color, color.r, color.g, color.b, 1);\n    blit(target);\n}\n\nfunction drawCheckerboard (target) {\n    checkerboardProgram.bind();\n    gl.uniform1f(checkerboardProgram.uniforms.aspectRatio, canvas.width / canvas.height);\n    blit(target);\n}\n\nfunction drawDisplay (target) {\n    let width = target == null ? gl.drawingBufferWidth : target.width;\n    let height = target == null ? gl.drawingBufferHeight : target.height;\n\n    displayMaterial.bind();\n    if (config.SHADING)\n        gl.uniform2f(displayMaterial.uniforms.texelSize, 1.0 / width, 1.0 / height);\n    gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0));\n    if (config.BLOOM) {\n        gl.uniform1i(displayMaterial.uniforms.uBloom, bloom.attach(1));\n        gl.uniform1i(displayMaterial.uniforms.uDithering, ditheringTexture.attach(2));\n        let scale = getTextureScale(ditheringTexture, width, height);\n        gl.uniform2f(displayMaterial.uniforms.ditherScale, scale.x, scale.y);\n    }\n    if (config.SUNRAYS)\n        gl.uniform1i(displayMaterial.uniforms.uSunrays, sunrays.attach(3));\n    blit(target);\n}\n\nfunction applyBloom (source, destination) {\n    if (bloomFramebuffers.length < 2)\n        return;\n\n    let last = destination;\n\n    gl.disable(gl.BLEND);\n    bloomPrefilterProgram.bind();\n    let knee = config.BLOOM_THRESHOLD * config.BLOOM_SOFT_KNEE + 0.0001;\n    let curve0 = config.BLOOM_THRESHOLD - knee;\n    let curve1 = knee * 2;\n    let curve2 = 0.25 / knee;\n    gl.uniform3f(bloomPrefilterProgram.uniforms.curve, curve0, curve1, curve2);\n    gl.uniform1f(bloomPrefilterProgram.uniforms.threshold, config.BLOOM_THRESHOLD);\n    gl.uniform1i(bloomPrefilterProgram.uniforms.uTexture, source.attach(0));\n    blit(last);\n\n    bloomBlurProgram.bind();\n    for (let i = 0; i < bloomFramebuffers.length; i++) {\n        let dest = bloomFramebuffers[i];\n        gl.uniform2f(bloomBlurProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY);\n        gl.uniform1i(bloomBlurProgram.uniforms.uTexture, last.attach(0));\n        blit(dest);\n        last = dest;\n    }\n\n    gl.blendFunc(gl.ONE, gl.ONE);\n    gl.enable(gl.BLEND);\n\n    for (let i = bloomFramebuffers.length - 2; i >= 0; i--) {\n        let baseTex = bloomFramebuffers[i];\n        gl.uniform2f(bloomBlurProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY);\n        gl.uniform1i(bloomBlurProgram.uniforms.uTexture, last.attach(0));\n        gl.viewport(0, 0, baseTex.width, baseTex.height);\n        blit(baseTex);\n        last = baseTex;\n    }\n\n    gl.disable(gl.BLEND);\n    bloomFinalProgram.bind();\n    gl.uniform2f(bloomFinalProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY);\n    gl.uniform1i(bloomFinalProgram.uniforms.uTexture, last.attach(0));\n    gl.uniform1f(bloomFinalProgram.uniforms.intensity, config.BLOOM_INTENSITY);\n    blit(destination);\n}\n\nfunction applySunrays (source, mask, destination) {\n    gl.disable(gl.BLEND);\n    sunraysMaskProgram.bind();\n    gl.uniform1i(sunraysMaskProgram.uniforms.uTexture, source.attach(0));\n    blit(mask);\n\n    sunraysProgram.bind();\n    gl.uniform1f(sunraysProgram.uniforms.weight, config.SUNRAYS_WEIGHT);\n    gl.uniform1i(sunraysProgram.uniforms.uTexture, mask.attach(0));\n    blit(destination);\n}\n\nfunction blur (target, temp, iterations) {\n    blurProgram.bind();\n    for (let i = 0; i < iterations; i++) {\n        gl.uniform2f(blurProgram.uniforms.texelSize, target.texelSizeX, 0.0);\n        gl.uniform1i(blurProgram.uniforms.uTexture, target.attach(0));\n        blit(temp);\n\n        gl.uniform2f(blurProgram.uniforms.texelSize, 0.0, target.texelSizeY);\n        gl.uniform1i(blurProgram.uniforms.uTexture, temp.attach(0));\n        blit(target);\n    }\n}\n\nfunction splatPointer (pointer) {\n    let dx = pointer.deltaX * config.SPLAT_FORCE;\n    let dy = pointer.deltaY * config.SPLAT_FORCE;\n    splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color);\n}\n\nfunction multipleSplats (amount) {\n    for (let i = 0; i < amount; i++) {\n        const color = generateColor();\n        color.r *= 10.0;\n        color.g *= 10.0;\n        color.b *= 10.0;\n        const x = Math.random();\n        const y = Math.random();\n        const dx = 1000 * (Math.random() - 0.5);\n        const dy = 1000 * (Math.random() - 0.5);\n        splat(x, y, dx, dy, color);\n    }\n}\n\nfunction splat (x, y, dx, dy, color) {\n    splatProgram.bind();\n    gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0));\n    gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height);\n    gl.uniform2f(splatProgram.uniforms.point, x, y);\n    gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0.0);\n    gl.uniform1f(splatProgram.uniforms.radius, correctRadius(config.SPLAT_RADIUS / 100.0));\n    blit(velocity.write);\n    velocity.swap();\n\n    gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0));\n    gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b);\n    blit(dye.write);\n    dye.swap();\n}\n\nfunction correctRadius (radius) {\n    let aspectRatio = canvas.width / canvas.height;\n    if (aspectRatio > 1)\n        radius *= aspectRatio;\n    return radius;\n}\n\ncanvas.addEventListener('mousedown', e => {\n    let posX = scaleByPixelRatio(e.offsetX);\n    let posY = scaleByPixelRatio(e.offsetY);\n    let pointer = pointers.find(p => p.id == -1);\n    if (pointer == null)\n        pointer = new pointerPrototype();\n    updatePointerDownData(pointer, -1, posX, posY);\n});\n\ncanvas.addEventListener('mousemove', e => {\n    let pointer = pointers[0];\n    if (!pointer.down) return;\n    let posX = scaleByPixelRatio(e.offsetX);\n    let posY = scaleByPixelRatio(e.offsetY);\n    updatePointerMoveData(pointer, posX, posY);\n});\n\nwindow.addEventListener('mouseup', () => {\n    updatePointerUpData(pointers[0]);\n});\n\ncanvas.addEventListener('touchstart', e => {\n    e.preventDefault();\n    const touches = e.targetTouches;\n    while (touches.length >= pointers.length)\n        pointers.push(new pointerPrototype());\n    for (let i = 0; i < touches.length; i++) {\n        let posX = scaleByPixelRatio(touches[i].pageX);\n        let posY = scaleByPixelRatio(touches[i].pageY);\n        updatePointerDownData(pointers[i + 1], touches[i].identifier, posX, posY);\n    }\n});\n\ncanvas.addEventListener('touchmove', e => {\n    e.preventDefault();\n    const touches = e.targetTouches;\n    for (let i = 0; i < touches.length; i++) {\n        let pointer = pointers[i + 1];\n        if (!pointer.down) continue;\n        let posX = scaleByPixelRatio(touches[i].pageX);\n        let posY = scaleByPixelRatio(touches[i].pageY);\n        updatePointerMoveData(pointer, posX, posY);\n    }\n}, false);\n\nwindow.addEventListener('touchend', e => {\n    const touches = e.changedTouches;\n    for (let i = 0; i < touches.length; i++)\n    {\n        let pointer = pointers.find(p => p.id == touches[i].identifier);\n        if (pointer == null) continue;\n        updatePointerUpData(pointer);\n    }\n});\n\nwindow.addEventListener('keydown', e => {\n    if (e.code === 'KeyP')\n        config.PAUSED = !config.PAUSED;\n    if (e.key === ' ')\n        splatStack.push(parseInt(Math.random() * 20) + 5);\n});\n\nfunction updatePointerDownData (pointer, id, posX, posY) {\n    pointer.id = id;\n    pointer.down = true;\n    pointer.moved = false;\n    pointer.texcoordX = posX / canvas.width;\n    pointer.texcoordY = 1.0 - posY / canvas.height;\n    pointer.prevTexcoordX = pointer.texcoordX;\n    pointer.prevTexcoordY = pointer.texcoordY;\n    pointer.deltaX = 0;\n    pointer.deltaY = 0;\n    pointer.color = generateColor();\n}\n\nfunction updatePointerMoveData (pointer, posX, posY) {\n    pointer.prevTexcoordX = pointer.texcoordX;\n    pointer.prevTexcoordY = pointer.texcoordY;\n    pointer.texcoordX = posX / canvas.width;\n    pointer.texcoordY = 1.0 - posY / canvas.height;\n    pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX);\n    pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY);\n    pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0;\n}\n\nfunction updatePointerUpData (pointer) {\n    pointer.down = false;\n}\n\nfunction correctDeltaX (delta) {\n    let aspectRatio = canvas.width / canvas.height;\n    if (aspectRatio < 1) delta *= aspectRatio;\n    return delta;\n}\n\nfunction correctDeltaY (delta) {\n    let aspectRatio = canvas.width / canvas.height;\n    if (aspectRatio > 1) delta /= aspectRatio;\n    return delta;\n}\n\nfunction generateColor () {\n    let c = HSVtoRGB(Math.random(), 1.0, 1.0);\n    c.r *= 0.15;\n    c.g *= 0.15;\n    c.b *= 0.15;\n    return c;\n}\n\nfunction HSVtoRGB (h, s, v) {\n    let r, g, b, i, f, p, q, t;\n    i = Math.floor(h * 6);\n    f = h * 6 - i;\n    p = v * (1 - s);\n    q = v * (1 - f * s);\n    t = v * (1 - (1 - f) * s);\n\n    switch (i % 6) {\n        case 0: r = v, g = t, b = p; break;\n        case 1: r = q, g = v, b = p; break;\n        case 2: r = p, g = v, b = t; break;\n        case 3: r = p, g = q, b = v; break;\n        case 4: r = t, g = p, b = v; break;\n        case 5: r = v, g = p, b = q; break;\n    }\n\n    return {\n        r,\n        g,\n        b\n    };\n}\n\nfunction normalizeColor (input) {\n    let output = {\n        r: input.r / 255,\n        g: input.g / 255,\n        b: input.b / 255\n    };\n    return output;\n}\n\nfunction wrap (value, min, max) {\n    let range = max - min;\n    if (range == 0) return min;\n    return (value - min) % range + min;\n}\n\nfunction getResolution (resolution) {\n    let aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight;\n    if (aspectRatio < 1)\n        aspectRatio = 1.0 / aspectRatio;\n\n    let min = Math.round(resolution);\n    let max = Math.round(resolution * aspectRatio);\n\n    if (gl.drawingBufferWidth > gl.drawingBufferHeight)\n        return { width: max, height: min };\n    else\n        return { width: min, height: max };\n}\n\nfunction getTextureScale (texture, width, height) {\n    return {\n        x: width / texture.width,\n        y: height / texture.height\n    };\n}\n\nfunction scaleByPixelRatio (input) {\n    let pixelRatio = window.devicePixelRatio || 1;\n    return Math.floor(input * pixelRatio);\n}\n\nfunction hashCode (s) {\n    if (s.length == 0) return 0;\n    let hash = 0;\n    for (let i = 0; i < s.length; i++) {\n        hash = (hash << 5) - hash + s.charCodeAt(i);\n        hash |= 0; // Convert to 32bit integer\n    }\n    return hash;\n};"
  }
]