diff --git a/LivelyInfo.json b/LivelyInfo.json new file mode 100644 index 00000000..afbc5832 --- /dev/null +++ b/LivelyInfo.json @@ -0,0 +1,13 @@ +{ + "AppVersion": "2.0.7.4", + "Title": "Fluids", + "Thumbnail": "thumbnail.jpg", + "Preview": "preview.gif", + "Desc": "Fluid simulation using WebGL, reacts with system audio & cursor.", + "Author": "PavelDoGreat", + "License": "MIT", + "Contact": "https://github.com/rocksdanister/WebGL-Fluid-Simulation", + "Type": 1, + "Arguments": "--audio --pause-event true", + "FileName": "index.html" +} \ No newline at end of file diff --git a/LivelyProperties.json b/LivelyProperties.json new file mode 100644 index 00000000..2c138d97 --- /dev/null +++ b/LivelyProperties.json @@ -0,0 +1,204 @@ +{ + "label0": { + "type": "label", + "value": "Audio" + }, + "audioReact": { + "type": "checkbox", + "value": true, + "text": "Visualiser" + }, + "randomSplats": { + "type": "checkbox", + "value": false, + "text": "Random Splats" + }, + "label1": { + "type": "label", + "value": "Performance" + }, + "quality": { + "type": "dropdown", + "value": 0, + "text": "Quality", + "items": [ + "High", + "Medium", + "Low", + "Very Low" + ] + }, + "simResolution": { + "type": "dropdown", + "value": 2, + "text": "Sim Resolution", + "items": [ + "32", + "64", + "128", + "256" + ] + }, + "label2": { + "type": "label", + "value": "Color" + }, + "colorLeft": { + "text": "Left", + "type": "color", + "value": "#FF0000" + }, + "colorRight": { + "text": "Right", + "type": "color", + "value": "#FFFF00" + }, + "customColor": { + "type": "checkbox", + "value": false, + "text": "Enable" + }, + "label3": { + "type": "label", + "value": "Simulation" + }, + "densityDiffusion": { + "max": 40, + "min": 0, + "tick": 41, + "text": "Density Diffusion", + "type": "slider", + "value": 10 + }, + "velocityDiffusion": { + "max": 400, + "min": 0, + "tick": 401, + "text": "Velocity Diffusion", + "type": "slider", + "value": 100 + }, + "pressure": { + "max": 100, + "min": 0, + "tick": 101, + "text": "Pressure", + "type": "slider", + "value": 10 + }, + "vorticity": { + "max": 50, + "min": 0, + "tick": 51, + "text": "Vorticity", + "type": "slider", + "value": 20 + }, + "splatRadius": { + "max": 100, + "min": 1, + "tick": 100, + "text": "Splat Radius", + "type": "slider", + "value": 20 + }, + "shading": { + "type": "checkbox", + "value": true, + "text": "Shading" + }, + "colorful": { + "type": "checkbox", + "value": true, + "text": "Colorful" + }, + "bloomEnable": { + "type": "checkbox", + "value": true, + "text": "Bloom" + }, + "bloomIntensity": { + "max": 200, + "min": 10, + "tick": 191, + "text": "Bloom Intensity", + "type": "slider", + "value": 15 + }, + "bloomThreshold": { + "max": 100, + "min": 0, + "tick": 101, + "text": "Bloom Threshold", + "type": "slider", + "value": 75 + }, + "sunRaysEnable": { + "type": "checkbox", + "value": true, + "text": "Sunrays" + }, + "sunRaysWeight": { + "max": 100, + "min": 30, + "tick": 71, + "text": "Sunray Weight", + "type": "slider", + "value": 40 + }, + "label4": { + "type": "label", + "value": "Background" + }, + "backgroundColor": { + "text": "Color", + "type": "color", + "value": "#000000" + }, + "backgroundSrc": { + "type": "folderDropdown", + "value": "pexels-neosiam-601798.jpg", + "text": "Image/WebM Video", + "filter": "*.jpg|*.jpeg|*.webp|*.png|*.webm", + "folder": "wallpapers" + }, + "backgroundFit": { + "type": "dropdown", + "value": 2, + "text": "Choose a Fit", + "items": [ + "Contain", + "Cover", + "Fill", + "None" + ] + }, + "backgroundEnabled": { + "type": "checkbox", + "value": false, + "text": "Enable" + }, + "label5": { + "type": "label", + "value": "Overlay" + }, + "overlaySrc": { + "type": "folderDropdown", + "value": "icon-lively-300.png", + "text": "Image", + "filter": "*.jpg|*.jpeg|*.webp|*.png|*.svg", + "folder": "overlays" + }, + "overlaySize": { + "max": 100, + "min": 5, + "text": "Size", + "type": "slider", + "value": 25 + }, + "overlayEnabled": { + "type": "checkbox", + "value": false, + "text": "Enable" + } +} diff --git a/README.md b/README.md index 1e74fd5c..5f33ae9c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # WebGL Fluid Simulation +Modified to work with [lively](https://github.com/rocksdanister/lively) wallpaper system, reacts with system audio. -[Play here](https://paveldogreat.github.io/WebGL-Fluid-Simulation/) +Download: [Fluids_v3.zip](https://github.com/rocksdanister/WebGL-Fluid-Simulation/releases/download/v3/Fluids_v3.zip) - +Drag & drop the file into lively window. ## References diff --git a/index.html b/index.html index e812fc02..8bf6bf89 100644 --- a/index.html +++ b/index.html @@ -1,114 +1,80 @@ - - - - - - - - - - - - - WebGL Fluid Simulation - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + WebGL Fluid Simulation + + + + + + + + + + + + +
+ + + diff --git a/LDR_LLL1_0.png b/js/LDR_LLL1_0.png similarity index 100% rename from LDR_LLL1_0.png rename to js/LDR_LLL1_0.png diff --git a/dat.gui.min.js b/js/dat.gui.min.js similarity index 100% rename from dat.gui.min.js rename to js/dat.gui.min.js diff --git a/js/script.js b/js/script.js new file mode 100644 index 00000000..b558ac05 --- /dev/null +++ b/js/script.js @@ -0,0 +1,1932 @@ +/* +MIT License + +Copyright (c) 2017 Pavel Dobryakov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +"use strict"; + +const canvas = document.getElementsByTagName("canvas")[0]; +resizeCanvas(); + +let config = { + SIM_RESOLUTION: 128, + DYE_RESOLUTION: 1024, + CAPTURE_RESOLUTION: 512, + DENSITY_DISSIPATION: 1, + VELOCITY_DISSIPATION: 0.2, + PRESSURE: 0.8, + PRESSURE_ITERATIONS: 20, + CURL: 30, + SPLAT_RADIUS: 0.25, + SPLAT_FORCE: 6000, + SHADING: true, + COLORFUL: true, + COLOR_UPDATE_SPEED: 10, + PAUSED: false, + BACK_COLOR: { r: 0, g: 0, b: 0 }, + TRANSPARENT: false, + BLOOM: true, + BLOOM_ITERATIONS: 8, + BLOOM_RESOLUTION: 256, + BLOOM_INTENSITY: 0.8, + BLOOM_THRESHOLD: 0.6, + BLOOM_SOFT_KNEE: 0.7, + SUNRAYS: true, + SUNRAYS_RESOLUTION: 196, + SUNRAYS_WEIGHT: 1.0, + SOUND_SENSITIVITY: 0.25, + FREQ_RANGE: 40, + FREQ_MULTI: 0.1, + CUSTOM_COLOR: false +}; + +var timer = setInterval(randomSplat, 3500); +var _runRandom = true; +var _isSleep = false; +function randomSplat() { + if (_runRandom == true && _isSleep == false && _randomSplats) splatStack.push(parseInt(Math.random() * 20) + 5); +} + +function livelyWallpaperPlaybackChanged(data) { + var obj = JSON.parse(data); + _isSleep = obj.IsPaused; + pauseVideoBackground(obj.IsPaused); +} + +let timeout; +let timeoutBool = true; +let lastBass = 0; +function livelyAudioListener(audioArray) { + if (audioArray[0] === 0 || _isSleep == true) { + _runRandom = true; + return; + } + if (!_audioReact) { + return; + } + + if (audioArray[0] >= 0.001 && _runRandom) { + _runRandom = false; + clearTimeout(timeout); + timeoutBool = true; + } else { + if (!_runRandom && timeoutBool) { + timeoutBool = false; + timeout = setTimeout(() => (_runRandom = timeoutBool = true), 1500); + } + } + + let bass = 0.0; + + for (let i = 0; i <= config.FREQ_RANGE; i++) bass += audioArray[i] * 2; + + bass /= config.FREQ_RANGE * 2 * config.FREQ_MULTI; + + multipleSplats(Math.floor(bass * config.SOUND_SENSITIVITY * 10) - lastBass); + lastBass = (bass, Math.floor(bass * config.SOUND_SENSITIVITY * 10)); +} + +function multipleSplats(amount) { + for (let i = 0; i < amount; i++) { + const color = config.COLORFUL ? generateColor() : Object.assign({}, config.POINTER_COLOR.getRandom()); + color.r *= 10.0; + color.g *= 10.0; + color.b *= 10.0; + const x = canvas.width * Math.random(); + const y = canvas.height * Math.random(); + const dx = 1000 * (Math.random() - 0.5); + const dy = 1000 * (Math.random() - 0.5); + splat(x, y, dx, dy, color); + } +} + +let _randomSplats = false; +let _audioReact = false; +let colorRange = ["#FF0000","#FF0001"]; +let colorConfig = null; +let splatRadiusModulationEnabled = false; +let baseRadius = config.SPLAT_RADIUS; +let backgroundSrc = null; +let isBackground = false; +function livelyPropertyListener(name, val) { + switch (name) { + case "quality": + config.DYE_RESOLUTION = [1024, 512, 256, 128][val]; + initFramebuffers(); + break; + case "simResolution": + config.SIM_RESOLUTION = [32, 64, 128, 256][val]; + initFramebuffers(); + break; + case "densityDiffusion": + config.DENSITY_DISSIPATION = val / 10; + break; + case "velocityDiffusion": + config.VELOCITY_DISSIPATION = val / 100; + break; + case "pressure": + config.PRESSURE = val / 100; + break; + case "vorticity": + config.CURL = val; + break; + case "splatRadius": + config.SPLAT_RADIUS = val / 100; + break; + case "shading": + config.SHADING = val; + updateKeywords(); + break; + case "colorful": + config.COLORFUL = val; + break; + case "bloomEnable": + config.BLOOM = val; + updateKeywords(); + break; + case "bloomIntensity": + config.BLOOM_INTENSITY = val / 100; + break; + case "bloomThreshold": + config.BLOOM_THRESHOLD = val / 100; + break; + case "sunRaysEnable": + config.SUNRAYS = val; + updateKeywords(); + break; + case "sunRaysWeight": + config.SUNRAYS_WEIGHT = val / 100; + break; + case "backgroundColor": + setBackgroundColor(val); + break; + case "backgroundSrc": + backgroundSrc = val; + if (isBackground) + setBackground(val); + break; + case "backgroundFit": + setBackgroundFit(val); + break; + case "backgroundEnabled": + isBackground = val; + if (isBackground) + { + config.TRANSPARENT = true; + setBackground(backgroundSrc); + } + else + { + config.TRANSPARENT = false; + disposeBackgrounds(); + } + break; + case "overlaySrc": + setOverlay(val); + break; + case "overlaySize": + setOverlaySize(val); + break; + case "overlayEnabled": + toggleOverlay(val); + break; + case "randomSplats": + _randomSplats = val; + break; + case "audioReact": + _audioReact = val; + break; + case "colorLeft": + colorRange[0]=val; + break; + case "colorRight": + colorRange[1]=val; + break; + case "customColor": + config.CUSTOM_COLOR = val; + break; + // case "colorConfig2": + // colorConfig=val===""? null:JSON.parse(val); + // break; + } +} + +function setBackgroundColor(hex) +{ + const tmp = hexToRgb(hex); + config.BACK_COLOR.r = tmp.r; + config.BACK_COLOR.g = tmp.g; + config.BACK_COLOR.b = tmp.b; + document.body.style.backgroundColor = hex; +} + +function setOverlay(srcPath) +{ + if (srcPath == undefined || srcPath == null ) + return; + + let src = srcPath.replace("\\", "/"); + document.getElementById('overlay').style.backgroundImage = `url('${src}')`; +} + +function setOverlaySize(size) +{ + let overlay = document.getElementById('overlay'); + overlay.style.width = `${size}%`; + overlay.style.height = `${size}%`; +} + +function toggleOverlay(val) +{ + document.getElementById('overlay').style.visibility = !val ? "hidden" : "visible"; +} + +function pauseVideoBackground(isPaused) +{ + let videoElement = document.getElementById('videoBackground'); + if (videoElement != null && videoElement.hasAttribute("src")) + { + if (isPaused) + videoElement.pause(); + else + videoElement.play(); + } +} + +function setBackground(srcPath) +{ + if (srcPath == undefined || srcPath == null ) + return; + + let ext = getExtension(srcPath); + let src = srcPath.replace("\\", "/"); + let videoElement = document.getElementById('videoBackground'); + + if (ext == "jpg" || ext == "jpeg" || ext == "png" || ext == "webp") + { + disposeVideoElement(videoElement); + document.body.style.backgroundImage = `url('${src}')`; + } + else if (ext == "webm") + { + document.body.style.backgroundImage = ''; + videoElement.src = src; + videoElement.play(); + } +} + +function disposeBackgrounds() +{ + document.body.style.backgroundImage = ''; + disposeVideoElement(document.getElementById('videoBackground')); +} + +function setBackgroundFit(index) +{ + document.getElementById('videoBackground').style.objectFit = ["contain", "cover", "fill", "none"][index]; + document.body.style.backgroundSize = ["contain", "cover", "100% 100%", "auto"][index]; +} + +function getExtension(filePath) { + return filePath.substring(filePath.lastIndexOf(".") + 1, filePath.length) || filePath; +} + +function hexToRgb(hex) { + let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + } + : null; +} + +//ref: https://stackoverflow.com/questions/3258587/how-to-properly-unload-destroy-a-video-element +function disposeVideoElement(element) { + if (element != null && element.hasAttribute("src")) { + element.pause(); + element.removeAttribute("src"); // empty source + element.load(); + } +} + +function RGBtoHSV(r, g, b) { + if (arguments.length === 1) { + g = r.g, b = r.b, r = r.r; + } + var max = Math.max(r, g, b), min = Math.min(r, g, b), + d = max - min, + h, + s = (max === 0 ? 0 : d / max), + v = max / 255; + + switch (max) { + case min: h = 0; break; + case r: h = (g - b) + d * (g < b ? 6: 0); h /= 6 * d; break; + case g: h = (b - r) + d * 2; h /= 6 * d; break; + case b: h = (r - g) + d * 4; h /= 6 * d; break; + } + + return { + h: h, + s: s, + v: v + }; +} + +function pointerPrototype() { + this.id = -1; + this.texcoordX = 0; + this.texcoordY = 0; + this.prevTexcoordX = 0; + this.prevTexcoordY = 0; + this.deltaX = 0; + this.deltaY = 0; + this.down = false; + this.moved = false; + this.color = [30, 0, 300]; +} + +let pointers = []; +let splatStack = []; +pointers.push(new pointerPrototype()); + +const { gl, ext } = getWebGLContext(canvas); + +if (isMobile()) { + config.DYE_RESOLUTION = 512; +} +if (!ext.supportLinearFiltering) { + config.DYE_RESOLUTION = 512; + config.SHADING = false; + config.BLOOM = false; + config.SUNRAYS = false; +} + +function getWebGLContext(canvas) { + const params = { alpha: true, depth: false, stencil: false, antialias: false, preserveDrawingBuffer: false }; + + let gl = canvas.getContext("webgl2", params); + const isWebGL2 = !!gl; + if (!isWebGL2) gl = canvas.getContext("webgl", params) || canvas.getContext("experimental-webgl", params); + + let halfFloat; + let supportLinearFiltering; + if (isWebGL2) { + gl.getExtension("EXT_color_buffer_float"); + supportLinearFiltering = gl.getExtension("OES_texture_float_linear"); + } else { + halfFloat = gl.getExtension("OES_texture_half_float"); + supportLinearFiltering = gl.getExtension("OES_texture_half_float_linear"); + } + + gl.clearColor(0.0, 0.0, 0.0, 1.0); + + const halfFloatTexType = isWebGL2 ? gl.HALF_FLOAT : halfFloat.HALF_FLOAT_OES; + let formatRGBA; + let formatRG; + let formatR; + + if (isWebGL2) { + formatRGBA = getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, halfFloatTexType); + formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType); + formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType); + } else { + formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); + formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); + formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); + } + + return { + gl, + ext: { + formatRGBA, + formatRG, + formatR, + halfFloatTexType, + supportLinearFiltering, + }, + }; +} + +function getSupportedFormat(gl, internalFormat, format, type) { + if (!supportRenderTextureFormat(gl, internalFormat, format, type)) { + switch (internalFormat) { + case gl.R16F: + return getSupportedFormat(gl, gl.RG16F, gl.RG, type); + case gl.RG16F: + return getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, type); + default: + return null; + } + } + + return { + internalFormat, + format, + }; +} + +function supportRenderTextureFormat(gl, internalFormat, format, type) { + let texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + 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); + gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null); + + let fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); + + const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); + return status == gl.FRAMEBUFFER_COMPLETE; +} + +function isMobile() { + return /Mobi|Android/i.test(navigator.userAgent); +} + +function captureScreenshot() { + let res = getResolution(config.CAPTURE_RESOLUTION); + let target = createFBO( + res.width, + res.height, + ext.formatRGBA.internalFormat, + ext.formatRGBA.format, + ext.halfFloatTexType, + gl.NEAREST + ); + render(target); + + let texture = framebufferToTexture(target); + texture = normalizeTexture(texture, target.width, target.height); + + let captureCanvas = textureToCanvas(texture, target.width, target.height); + let datauri = captureCanvas.toDataURL(); + downloadURI("fluid.png", datauri); + URL.revokeObjectURL(datauri); +} + +function framebufferToTexture(target) { + gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo); + let length = target.width * target.height * 4; + let texture = new Float32Array(length); + gl.readPixels(0, 0, target.width, target.height, gl.RGBA, gl.FLOAT, texture); + return texture; +} + +function normalizeTexture(texture, width, height) { + let result = new Uint8Array(texture.length); + let id = 0; + for (let i = height - 1; i >= 0; i--) { + for (let j = 0; j < width; j++) { + let nid = i * width * 4 + j * 4; + result[nid + 0] = clamp01(texture[id + 0]) * 255; + result[nid + 1] = clamp01(texture[id + 1]) * 255; + result[nid + 2] = clamp01(texture[id + 2]) * 255; + result[nid + 3] = clamp01(texture[id + 3]) * 255; + id += 4; + } + } + return result; +} + +function clamp01(input) { + return Math.min(Math.max(input, 0), 1); +} + +function textureToCanvas(texture, width, height) { + let captureCanvas = document.createElement("canvas"); + let ctx = captureCanvas.getContext("2d"); + captureCanvas.width = width; + captureCanvas.height = height; + + let imageData = ctx.createImageData(width, height); + imageData.data.set(texture); + ctx.putImageData(imageData, 0, 0); + + return captureCanvas; +} + +function downloadURI(filename, uri) { + let link = document.createElement("a"); + link.download = filename; + link.href = uri; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} + +class Material { + constructor(vertexShader, fragmentShaderSource) { + this.vertexShader = vertexShader; + this.fragmentShaderSource = fragmentShaderSource; + this.programs = []; + this.activeProgram = null; + this.uniforms = []; + } + + setKeywords(keywords) { + let hash = 0; + for (let i = 0; i < keywords.length; i++) hash += hashCode(keywords[i]); + + let program = this.programs[hash]; + if (program == null) { + let fragmentShader = compileShader(gl.FRAGMENT_SHADER, this.fragmentShaderSource, keywords); + program = createProgram(this.vertexShader, fragmentShader); + this.programs[hash] = program; + } + + if (program == this.activeProgram) return; + + this.uniforms = getUniforms(program); + this.activeProgram = program; + } + + bind() { + gl.useProgram(this.activeProgram); + } +} + +class Program { + constructor(vertexShader, fragmentShader) { + this.uniforms = {}; + this.program = createProgram(vertexShader, fragmentShader); + this.uniforms = getUniforms(this.program); + } + + bind() { + gl.useProgram(this.program); + } +} + +function createProgram(vertexShader, fragmentShader) { + let program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw gl.getProgramInfoLog(program); + + return program; +} + +function getUniforms(program) { + let uniforms = []; + let uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); + for (let i = 0; i < uniformCount; i++) { + let uniformName = gl.getActiveUniform(program, i).name; + uniforms[uniformName] = gl.getUniformLocation(program, uniformName); + } + return uniforms; +} + +function compileShader(type, source, keywords) { + source = addKeywords(source, keywords); + + const shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw gl.getShaderInfoLog(shader); + + return shader; +} + +function addKeywords(source, keywords) { + if (keywords == null) return source; + let keywordsString = ""; + keywords.forEach((keyword) => { + keywordsString += "#define " + keyword + "\n"; + }); + return keywordsString + source; +} + +const baseVertexShader = compileShader( + gl.VERTEX_SHADER, + ` + precision highp float; + + attribute vec2 aPosition; + varying vec2 vUv; + varying vec2 vL; + varying vec2 vR; + varying vec2 vT; + varying vec2 vB; + uniform vec2 texelSize; + + void main () { + vUv = aPosition * 0.5 + 0.5; + vL = vUv - vec2(texelSize.x, 0.0); + vR = vUv + vec2(texelSize.x, 0.0); + vT = vUv + vec2(0.0, texelSize.y); + vB = vUv - vec2(0.0, texelSize.y); + gl_Position = vec4(aPosition, 0.0, 1.0); + } +` +); + +const blurVertexShader = compileShader( + gl.VERTEX_SHADER, + ` + precision highp float; + + attribute vec2 aPosition; + varying vec2 vUv; + varying vec2 vL; + varying vec2 vR; + uniform vec2 texelSize; + + void main () { + vUv = aPosition * 0.5 + 0.5; + float offset = 1.33333333; + vL = vUv - texelSize * offset; + vR = vUv + texelSize * offset; + gl_Position = vec4(aPosition, 0.0, 1.0); + } +` +); + +const blurShader = compileShader( + gl.FRAGMENT_SHADER, + ` + precision mediump float; + precision mediump sampler2D; + + varying vec2 vUv; + varying vec2 vL; + varying vec2 vR; + uniform sampler2D uTexture; + + void main () { + vec4 sum = texture2D(uTexture, vUv) * 0.29411764; + sum += texture2D(uTexture, vL) * 0.35294117; + sum += texture2D(uTexture, vR) * 0.35294117; + gl_FragColor = sum; + } +` +); + +const copyShader = compileShader( + gl.FRAGMENT_SHADER, + ` + precision mediump float; + precision mediump sampler2D; + + varying highp vec2 vUv; + uniform sampler2D uTexture; + + void main () { + gl_FragColor = texture2D(uTexture, vUv); + } +` +); + +const clearShader = compileShader( + gl.FRAGMENT_SHADER, + ` + precision mediump float; + precision mediump sampler2D; + + varying highp vec2 vUv; + uniform sampler2D uTexture; + uniform float value; + + void main () { + gl_FragColor = value * texture2D(uTexture, vUv); + } +` +); + +const colorShader = compileShader( + gl.FRAGMENT_SHADER, + ` + precision mediump float; + + uniform vec4 color; + + void main () { + gl_FragColor = color; + } +` +); + +const checkerboardShader = compileShader( + gl.FRAGMENT_SHADER, + ` + precision highp float; + precision highp sampler2D; + + varying vec2 vUv; + uniform sampler2D uTexture; + uniform float aspectRatio; + + #define SCALE 25.0 + + void main () { + vec2 uv = floor(vUv * SCALE * vec2(aspectRatio, 1.0)); + float v = mod(uv.x + uv.y, 2.0); + v = v * 0.1 + 0.8; + gl_FragColor = vec4(vec3(v), 1.0); + } +` +); + +const displayShaderSource = ` + precision highp float; + precision highp sampler2D; + + varying vec2 vUv; + varying vec2 vL; + varying vec2 vR; + varying vec2 vT; + varying vec2 vB; + uniform sampler2D uTexture; + uniform sampler2D uBloom; + uniform sampler2D uSunrays; + uniform sampler2D uDithering; + uniform vec2 ditherScale; + uniform vec2 texelSize; + + vec3 linearToGamma (vec3 color) { + color = max(color, vec3(0)); + return max(1.055 * pow(color, vec3(0.416666667)) - 0.055, vec3(0)); + } + + void main () { + vec3 c = texture2D(uTexture, vUv).rgb; + + #ifdef SHADING + vec3 lc = texture2D(uTexture, vL).rgb; + vec3 rc = texture2D(uTexture, vR).rgb; + vec3 tc = texture2D(uTexture, vT).rgb; + vec3 bc = texture2D(uTexture, vB).rgb; + + float dx = length(rc) - length(lc); + float dy = length(tc) - length(bc); + + vec3 n = normalize(vec3(dx, dy, length(texelSize))); + vec3 l = vec3(0.0, 0.0, 1.0); + + float diffuse = clamp(dot(n, l) + 0.7, 0.7, 1.0); + c *= diffuse; + #endif + + #ifdef BLOOM + vec3 bloom = texture2D(uBloom, vUv).rgb; + #endif + + #ifdef SUNRAYS + float sunrays = texture2D(uSunrays, vUv).r; + c *= sunrays; + #ifdef BLOOM + bloom *= sunrays; + #endif + #endif + + #ifdef BLOOM + float noise = texture2D(uDithering, vUv * ditherScale).r; + noise = noise * 2.0 - 1.0; + bloom += noise / 255.0; + bloom = linearToGamma(bloom); + c += bloom; + #endif + + float a = max(c.r, max(c.g, c.b)); + gl_FragColor = vec4(c, a); + } +`; + +const bloomPrefilterShader = compileShader( + gl.FRAGMENT_SHADER, + ` + precision mediump float; + precision mediump sampler2D; + + varying vec2 vUv; + uniform sampler2D uTexture; + uniform vec3 curve; + uniform float threshold; + + void main () { + vec3 c = texture2D(uTexture, vUv).rgb; + float br = max(c.r, max(c.g, c.b)); + float rq = clamp(br - curve.x, 0.0, curve.y); + rq = curve.z * rq * rq; + c *= max(rq, br - threshold) / max(br, 0.0001); + gl_FragColor = vec4(c, 0.0); + } +` +); + +const bloomBlurShader = compileShader( + gl.FRAGMENT_SHADER, + ` + precision mediump float; + precision mediump sampler2D; + + varying vec2 vL; + varying vec2 vR; + varying vec2 vT; + varying vec2 vB; + uniform sampler2D uTexture; + + void main () { + vec4 sum = vec4(0.0); + sum += texture2D(uTexture, vL); + sum += texture2D(uTexture, vR); + sum += texture2D(uTexture, vT); + sum += texture2D(uTexture, vB); + sum *= 0.25; + gl_FragColor = sum; + } +` +); + +const bloomFinalShader = compileShader( + gl.FRAGMENT_SHADER, + ` + precision mediump float; + precision mediump sampler2D; + + varying vec2 vL; + varying vec2 vR; + varying vec2 vT; + varying vec2 vB; + uniform sampler2D uTexture; + uniform float intensity; + + void main () { + vec4 sum = vec4(0.0); + sum += texture2D(uTexture, vL); + sum += texture2D(uTexture, vR); + sum += texture2D(uTexture, vT); + sum += texture2D(uTexture, vB); + sum *= 0.25; + gl_FragColor = sum * intensity; + } +` +); + +const sunraysMaskShader = compileShader( + gl.FRAGMENT_SHADER, + ` + precision highp float; + precision highp sampler2D; + + varying vec2 vUv; + uniform sampler2D uTexture; + + void main () { + vec4 c = texture2D(uTexture, vUv); + float br = max(c.r, max(c.g, c.b)); + c.a = 1.0 - min(max(br * 20.0, 0.0), 0.8); + gl_FragColor = c; + } +` +); + +const sunraysShader = compileShader( + gl.FRAGMENT_SHADER, + ` + precision highp float; + precision highp sampler2D; + + varying vec2 vUv; + uniform sampler2D uTexture; + uniform float weight; + + #define ITERATIONS 16 + + void main () { + float Density = 0.3; + float Decay = 0.95; + float Exposure = 0.7; + + vec2 coord = vUv; + vec2 dir = vUv - 0.5; + + dir *= 1.0 / float(ITERATIONS) * Density; + float illuminationDecay = 1.0; + + float color = texture2D(uTexture, vUv).a; + + for (int i = 0; i < ITERATIONS; i++) + { + coord -= dir; + float col = texture2D(uTexture, coord).a; + color += col * illuminationDecay * weight; + illuminationDecay *= Decay; + } + + gl_FragColor = vec4(color * Exposure, 0.0, 0.0, 1.0); + } +` +); + +const splatShader = compileShader( + gl.FRAGMENT_SHADER, + ` + precision highp float; + precision highp sampler2D; + + varying vec2 vUv; + uniform sampler2D uTarget; + uniform float aspectRatio; + uniform vec3 color; + uniform vec2 point; + uniform float radius; + + void main () { + vec2 p = vUv - point.xy; + p.x *= aspectRatio; + vec3 splat = exp(-dot(p, p) / radius) * color; + vec3 base = texture2D(uTarget, vUv).xyz; + gl_FragColor = vec4(base + splat, 1.0); + } +` +); + +const advectionShader = compileShader( + gl.FRAGMENT_SHADER, + ` + precision highp float; + precision highp sampler2D; + + varying vec2 vUv; + uniform sampler2D uVelocity; + uniform sampler2D uSource; + uniform vec2 texelSize; + uniform vec2 dyeTexelSize; + uniform float dt; + uniform float dissipation; + + vec4 bilerp (sampler2D sam, vec2 uv, vec2 tsize) { + vec2 st = uv / tsize - 0.5; + + vec2 iuv = floor(st); + vec2 fuv = fract(st); + + vec4 a = texture2D(sam, (iuv + vec2(0.5, 0.5)) * tsize); + vec4 b = texture2D(sam, (iuv + vec2(1.5, 0.5)) * tsize); + vec4 c = texture2D(sam, (iuv + vec2(0.5, 1.5)) * tsize); + vec4 d = texture2D(sam, (iuv + vec2(1.5, 1.5)) * tsize); + + return mix(mix(a, b, fuv.x), mix(c, d, fuv.x), fuv.y); + } + + void main () { + #ifdef MANUAL_FILTERING + vec2 coord = vUv - dt * bilerp(uVelocity, vUv, texelSize).xy * texelSize; + vec4 result = bilerp(uSource, coord, dyeTexelSize); + #else + vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize; + vec4 result = texture2D(uSource, coord); + #endif + float decay = 1.0 + dissipation * dt; + gl_FragColor = result / decay; + }`, + ext.supportLinearFiltering ? null : ["MANUAL_FILTERING"] +); + +const divergenceShader = compileShader( + gl.FRAGMENT_SHADER, + ` + precision mediump float; + precision mediump sampler2D; + + varying highp vec2 vUv; + varying highp vec2 vL; + varying highp vec2 vR; + varying highp vec2 vT; + varying highp vec2 vB; + uniform sampler2D uVelocity; + + void main () { + float L = texture2D(uVelocity, vL).x; + float R = texture2D(uVelocity, vR).x; + float T = texture2D(uVelocity, vT).y; + float B = texture2D(uVelocity, vB).y; + + vec2 C = texture2D(uVelocity, vUv).xy; + if (vL.x < 0.0) { L = -C.x; } + if (vR.x > 1.0) { R = -C.x; } + if (vT.y > 1.0) { T = -C.y; } + if (vB.y < 0.0) { B = -C.y; } + + float div = 0.5 * (R - L + T - B); + gl_FragColor = vec4(div, 0.0, 0.0, 1.0); + } +` +); + +const curlShader = compileShader( + gl.FRAGMENT_SHADER, + ` + precision mediump float; + precision mediump sampler2D; + + varying highp vec2 vUv; + varying highp vec2 vL; + varying highp vec2 vR; + varying highp vec2 vT; + varying highp vec2 vB; + uniform sampler2D uVelocity; + + void main () { + float L = texture2D(uVelocity, vL).y; + float R = texture2D(uVelocity, vR).y; + float T = texture2D(uVelocity, vT).x; + float B = texture2D(uVelocity, vB).x; + float vorticity = R - L - T + B; + gl_FragColor = vec4(0.5 * vorticity, 0.0, 0.0, 1.0); + } +` +); + +const vorticityShader = compileShader( + gl.FRAGMENT_SHADER, + ` + precision highp float; + precision highp sampler2D; + + varying vec2 vUv; + varying vec2 vL; + varying vec2 vR; + varying vec2 vT; + varying vec2 vB; + uniform sampler2D uVelocity; + uniform sampler2D uCurl; + uniform float curl; + uniform float dt; + + void main () { + float L = texture2D(uCurl, vL).x; + float R = texture2D(uCurl, vR).x; + float T = texture2D(uCurl, vT).x; + float B = texture2D(uCurl, vB).x; + float C = texture2D(uCurl, vUv).x; + + vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L)); + force /= length(force) + 0.0001; + force *= curl * C; + force.y *= -1.0; + + vec2 vel = texture2D(uVelocity, vUv).xy; + gl_FragColor = vec4(vel + force * dt, 0.0, 1.0); + } +` +); + +const pressureShader = compileShader( + gl.FRAGMENT_SHADER, + ` + precision mediump float; + precision mediump sampler2D; + + varying highp vec2 vUv; + varying highp vec2 vL; + varying highp vec2 vR; + varying highp vec2 vT; + varying highp vec2 vB; + uniform sampler2D uPressure; + uniform sampler2D uDivergence; + + void main () { + float L = texture2D(uPressure, vL).x; + float R = texture2D(uPressure, vR).x; + float T = texture2D(uPressure, vT).x; + float B = texture2D(uPressure, vB).x; + float C = texture2D(uPressure, vUv).x; + float divergence = texture2D(uDivergence, vUv).x; + float pressure = (L + R + B + T - divergence) * 0.25; + gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0); + } +` +); + +const gradientSubtractShader = compileShader( + gl.FRAGMENT_SHADER, + ` + precision mediump float; + precision mediump sampler2D; + + varying highp vec2 vUv; + varying highp vec2 vL; + varying highp vec2 vR; + varying highp vec2 vT; + varying highp vec2 vB; + uniform sampler2D uPressure; + uniform sampler2D uVelocity; + + void main () { + float L = texture2D(uPressure, vL).x; + float R = texture2D(uPressure, vR).x; + float T = texture2D(uPressure, vT).x; + float B = texture2D(uPressure, vB).x; + vec2 velocity = texture2D(uVelocity, vUv).xy; + velocity.xy -= vec2(R - L, T - B); + gl_FragColor = vec4(velocity, 0.0, 1.0); + } +` +); + +const blit = (() => { + gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), gl.STATIC_DRAW); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer()); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW); + gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); + gl.enableVertexAttribArray(0); + + return (destination) => { + gl.bindFramebuffer(gl.FRAMEBUFFER, destination); + gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); + }; +})(); + +let dye; +let velocity; +let divergence; +let curl; +let pressure; +let bloom; +let bloomFramebuffers = []; +let sunrays; +let sunraysTemp; + +let ditheringTexture = createTextureAsync("LDR_LLL1_0.png"); + +const blurProgram = new Program(blurVertexShader, blurShader); +const copyProgram = new Program(baseVertexShader, copyShader); +const clearProgram = new Program(baseVertexShader, clearShader); +const colorProgram = new Program(baseVertexShader, colorShader); +const checkerboardProgram = new Program(baseVertexShader, checkerboardShader); +const bloomPrefilterProgram = new Program(baseVertexShader, bloomPrefilterShader); +const bloomBlurProgram = new Program(baseVertexShader, bloomBlurShader); +const bloomFinalProgram = new Program(baseVertexShader, bloomFinalShader); +const sunraysMaskProgram = new Program(baseVertexShader, sunraysMaskShader); +const sunraysProgram = new Program(baseVertexShader, sunraysShader); +const splatProgram = new Program(baseVertexShader, splatShader); +const advectionProgram = new Program(baseVertexShader, advectionShader); +const divergenceProgram = new Program(baseVertexShader, divergenceShader); +const curlProgram = new Program(baseVertexShader, curlShader); +const vorticityProgram = new Program(baseVertexShader, vorticityShader); +const pressureProgram = new Program(baseVertexShader, pressureShader); +const gradienSubtractProgram = new Program(baseVertexShader, gradientSubtractShader); + +const displayMaterial = new Material(baseVertexShader, displayShaderSource); + +function initFramebuffers() { + let simRes = getResolution(config.SIM_RESOLUTION); + let dyeRes = getResolution(config.DYE_RESOLUTION); + + const texType = ext.halfFloatTexType; + const rgba = ext.formatRGBA; + const rg = ext.formatRG; + const r = ext.formatR; + const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; + + if (dye == null) + dye = createDoubleFBO(dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering); + else dye = resizeDoubleFBO(dye, dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering); + + if (velocity == null) + velocity = createDoubleFBO(simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering); + else + velocity = resizeDoubleFBO(velocity, simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering); + + divergence = createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); + curl = createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); + pressure = createDoubleFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); + + initBloomFramebuffers(); + initSunraysFramebuffers(); +} + +function initBloomFramebuffers() { + let res = getResolution(config.BLOOM_RESOLUTION); + + const texType = ext.halfFloatTexType; + const rgba = ext.formatRGBA; + const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; + + bloom = createFBO(res.width, res.height, rgba.internalFormat, rgba.format, texType, filtering); + + bloomFramebuffers.length = 0; + for (let i = 0; i < config.BLOOM_ITERATIONS; i++) { + let width = res.width >> (i + 1); + let height = res.height >> (i + 1); + + if (width < 2 || height < 2) break; + + let fbo = createFBO(width, height, rgba.internalFormat, rgba.format, texType, filtering); + bloomFramebuffers.push(fbo); + } +} + +function initSunraysFramebuffers() { + let res = getResolution(config.SUNRAYS_RESOLUTION); + + const texType = ext.halfFloatTexType; + const r = ext.formatR; + const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; + + sunrays = createFBO(res.width, res.height, r.internalFormat, r.format, texType, filtering); + sunraysTemp = createFBO(res.width, res.height, r.internalFormat, r.format, texType, filtering); +} + +function createFBO(w, h, internalFormat, format, type, param) { + gl.activeTexture(gl.TEXTURE0); + let texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param); + 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); + gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null); + + let fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); + gl.viewport(0, 0, w, h); + gl.clear(gl.COLOR_BUFFER_BIT); + + let texelSizeX = 1.0 / w; + let texelSizeY = 1.0 / h; + + return { + texture, + fbo, + width: w, + height: h, + texelSizeX, + texelSizeY, + attach(id) { + gl.activeTexture(gl.TEXTURE0 + id); + gl.bindTexture(gl.TEXTURE_2D, texture); + return id; + }, + }; +} + +function createDoubleFBO(w, h, internalFormat, format, type, param) { + let fbo1 = createFBO(w, h, internalFormat, format, type, param); + let fbo2 = createFBO(w, h, internalFormat, format, type, param); + + return { + width: w, + height: h, + texelSizeX: fbo1.texelSizeX, + texelSizeY: fbo1.texelSizeY, + get read() { + return fbo1; + }, + set read(value) { + fbo1 = value; + }, + get write() { + return fbo2; + }, + set write(value) { + fbo2 = value; + }, + swap() { + let temp = fbo1; + fbo1 = fbo2; + fbo2 = temp; + }, + }; +} + +function resizeFBO(target, w, h, internalFormat, format, type, param) { + let newFBO = createFBO(w, h, internalFormat, format, type, param); + copyProgram.bind(); + gl.uniform1i(copyProgram.uniforms.uTexture, target.attach(0)); + blit(newFBO.fbo); + return newFBO; +} + +function resizeDoubleFBO(target, w, h, internalFormat, format, type, param) { + if (target.width == w && target.height == h) return target; + target.read = resizeFBO(target.read, w, h, internalFormat, format, type, param); + target.write = createFBO(w, h, internalFormat, format, type, param); + target.width = w; + target.height = h; + target.texelSizeX = 1.0 / w; + target.texelSizeY = 1.0 / h; + return target; +} + +function createTextureAsync(url) { + let texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + 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.REPEAT); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, 1, 1, 0, gl.RGB, gl.UNSIGNED_BYTE, new Uint8Array([255, 255, 255])); + + let obj = { + texture, + width: 1, + height: 1, + attach(id) { + gl.activeTexture(gl.TEXTURE0 + id); + gl.bindTexture(gl.TEXTURE_2D, texture); + return id; + }, + }; + + let image = new Image(); + image.onload = () => { + obj.width = image.width; + obj.height = image.height; + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image); + }; + image.src = url; + + return obj; +} + +function updateKeywords() { + let displayKeywords = []; + if (config.SHADING) displayKeywords.push("SHADING"); + if (config.BLOOM) displayKeywords.push("BLOOM"); + if (config.SUNRAYS) displayKeywords.push("SUNRAYS"); + displayMaterial.setKeywords(displayKeywords); +} + +updateKeywords(); +initFramebuffers(); +multipleSplats(parseInt(Math.random() * 20) + 5); + +let lastUpdateTime = Date.now(); +let colorUpdateTimer = 0.0; +update(); + +function update() { + const dt = calcDeltaTime(); + if (resizeCanvas()) initFramebuffers(); + updateColors(dt); + applyInputs(); + if (!config.PAUSED) step(dt); + render(null); + requestAnimationFrame(update); +} + +function calcDeltaTime() { + let now = Date.now(); + let dt = (now - lastUpdateTime) / 1000; + dt = Math.min(dt, 0.016666); + lastUpdateTime = now; + return dt; +} + +function resizeCanvas() { + let width = scaleByPixelRatio(canvas.clientWidth); + let height = scaleByPixelRatio(canvas.clientHeight); + if (canvas.width != width || canvas.height != height) { + canvas.width = width; + canvas.height = height; + return true; + } + return false; +} + +function updateColors(dt) { + if (!config.COLORFUL) return; + + colorUpdateTimer += dt * config.COLOR_UPDATE_SPEED; + if (colorUpdateTimer >= 1) { + colorUpdateTimer = wrap(colorUpdateTimer, 0, 1); + pointers.forEach((p) => { + p.color = generateColor(); + }); + } +} + +function applyInputs() { + if (splatStack.length > 0) multipleSplats(splatStack.pop()); + + pointers.forEach((p) => { + if (p.moved) { + p.moved = false; + splatPointer(p); + } + }); +} + +function step(dt) { + gl.disable(gl.BLEND); + gl.viewport(0, 0, velocity.width, velocity.height); + + curlProgram.bind(); + gl.uniform2f(curlProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); + gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read.attach(0)); + blit(curl.fbo); + + vorticityProgram.bind(); + gl.uniform2f(vorticityProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); + gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read.attach(0)); + gl.uniform1i(vorticityProgram.uniforms.uCurl, curl.attach(1)); + gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL); + gl.uniform1f(vorticityProgram.uniforms.dt, dt); + blit(velocity.write.fbo); + velocity.swap(); + + divergenceProgram.bind(); + gl.uniform2f(divergenceProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); + gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0)); + blit(divergence.fbo); + + clearProgram.bind(); + gl.uniform1i(clearProgram.uniforms.uTexture, pressure.read.attach(0)); + gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE); + blit(pressure.write.fbo); + pressure.swap(); + + pressureProgram.bind(); + gl.uniform2f(pressureProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); + gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence.attach(0)); + for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) { + gl.uniform1i(pressureProgram.uniforms.uPressure, pressure.read.attach(1)); + blit(pressure.write.fbo); + pressure.swap(); + } + + gradienSubtractProgram.bind(); + gl.uniform2f(gradienSubtractProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); + gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressure.read.attach(0)); + gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.read.attach(1)); + blit(velocity.write.fbo); + velocity.swap(); + + advectionProgram.bind(); + gl.uniform2f(advectionProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); + if (!ext.supportLinearFiltering) + gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, velocity.texelSizeX, velocity.texelSizeY); + let velocityId = velocity.read.attach(0); + gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId); + gl.uniform1i(advectionProgram.uniforms.uSource, velocityId); + gl.uniform1f(advectionProgram.uniforms.dt, dt); + gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION); + blit(velocity.write.fbo); + velocity.swap(); + + gl.viewport(0, 0, dye.width, dye.height); + + if (!ext.supportLinearFiltering) gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, dye.texelSizeX, dye.texelSizeY); + gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0)); + gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1)); + gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION); + blit(dye.write.fbo); + dye.swap(); +} + +function render(target) { + if (config.BLOOM) applyBloom(dye.read, bloom); + if (config.SUNRAYS) { + applySunrays(dye.read, dye.write, sunrays); + blur(sunrays, sunraysTemp, 1); + } + + if (target == null || !config.TRANSPARENT) { + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + gl.enable(gl.BLEND); + } else { + gl.disable(gl.BLEND); + } + + let width = target == null ? gl.drawingBufferWidth : target.width; + let height = target == null ? gl.drawingBufferHeight : target.height; + gl.viewport(0, 0, width, height); + + let fbo = target == null ? null : target.fbo; + if (!config.TRANSPARENT) drawColor(fbo, normalizeColor(config.BACK_COLOR)); + //if (target == null && config.TRANSPARENT) + //drawCheckerboard(fbo); + drawDisplay(fbo, width, height); +} + +function drawColor(fbo, color) { + colorProgram.bind(); + gl.uniform4f(colorProgram.uniforms.color, color.r, color.g, color.b, 1); + blit(fbo); +} + +function drawCheckerboard(fbo) { + checkerboardProgram.bind(); + gl.uniform1f(checkerboardProgram.uniforms.aspectRatio, canvas.width / canvas.height); + blit(fbo); +} + +function drawDisplay(fbo, width, height) { + displayMaterial.bind(); + if (config.SHADING) gl.uniform2f(displayMaterial.uniforms.texelSize, 1.0 / width, 1.0 / height); + gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0)); + if (config.BLOOM) { + gl.uniform1i(displayMaterial.uniforms.uBloom, bloom.attach(1)); + gl.uniform1i(displayMaterial.uniforms.uDithering, ditheringTexture.attach(2)); + let scale = getTextureScale(ditheringTexture, width, height); + gl.uniform2f(displayMaterial.uniforms.ditherScale, scale.x, scale.y); + } + if (config.SUNRAYS) gl.uniform1i(displayMaterial.uniforms.uSunrays, sunrays.attach(3)); + blit(fbo); +} + +function applyBloom(source, destination) { + if (bloomFramebuffers.length < 2) return; + + let last = destination; + + gl.disable(gl.BLEND); + bloomPrefilterProgram.bind(); + let knee = config.BLOOM_THRESHOLD * config.BLOOM_SOFT_KNEE + 0.0001; + let curve0 = config.BLOOM_THRESHOLD - knee; + let curve1 = knee * 2; + let curve2 = 0.25 / knee; + gl.uniform3f(bloomPrefilterProgram.uniforms.curve, curve0, curve1, curve2); + gl.uniform1f(bloomPrefilterProgram.uniforms.threshold, config.BLOOM_THRESHOLD); + gl.uniform1i(bloomPrefilterProgram.uniforms.uTexture, source.attach(0)); + gl.viewport(0, 0, last.width, last.height); + blit(last.fbo); + + bloomBlurProgram.bind(); + for (let i = 0; i < bloomFramebuffers.length; i++) { + let dest = bloomFramebuffers[i]; + gl.uniform2f(bloomBlurProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY); + gl.uniform1i(bloomBlurProgram.uniforms.uTexture, last.attach(0)); + gl.viewport(0, 0, dest.width, dest.height); + blit(dest.fbo); + last = dest; + } + + gl.blendFunc(gl.ONE, gl.ONE); + gl.enable(gl.BLEND); + + for (let i = bloomFramebuffers.length - 2; i >= 0; i--) { + let baseTex = bloomFramebuffers[i]; + gl.uniform2f(bloomBlurProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY); + gl.uniform1i(bloomBlurProgram.uniforms.uTexture, last.attach(0)); + gl.viewport(0, 0, baseTex.width, baseTex.height); + blit(baseTex.fbo); + last = baseTex; + } + + gl.disable(gl.BLEND); + bloomFinalProgram.bind(); + gl.uniform2f(bloomFinalProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY); + gl.uniform1i(bloomFinalProgram.uniforms.uTexture, last.attach(0)); + gl.uniform1f(bloomFinalProgram.uniforms.intensity, config.BLOOM_INTENSITY); + gl.viewport(0, 0, destination.width, destination.height); + blit(destination.fbo); +} + +function applySunrays(source, mask, destination) { + gl.disable(gl.BLEND); + sunraysMaskProgram.bind(); + gl.uniform1i(sunraysMaskProgram.uniforms.uTexture, source.attach(0)); + gl.viewport(0, 0, mask.width, mask.height); + blit(mask.fbo); + + sunraysProgram.bind(); + gl.uniform1f(sunraysProgram.uniforms.weight, config.SUNRAYS_WEIGHT); + gl.uniform1i(sunraysProgram.uniforms.uTexture, mask.attach(0)); + gl.viewport(0, 0, destination.width, destination.height); + blit(destination.fbo); +} + +function blur(target, temp, iterations) { + blurProgram.bind(); + for (let i = 0; i < iterations; i++) { + gl.uniform2f(blurProgram.uniforms.texelSize, target.texelSizeX, 0.0); + gl.uniform1i(blurProgram.uniforms.uTexture, target.attach(0)); + blit(temp.fbo); + + gl.uniform2f(blurProgram.uniforms.texelSize, 0.0, target.texelSizeY); + gl.uniform1i(blurProgram.uniforms.uTexture, temp.attach(0)); + blit(target.fbo); + } +} + +function splatPointer(pointer) { + let dx = pointer.deltaX * config.SPLAT_FORCE; + let dy = pointer.deltaY * config.SPLAT_FORCE; + splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color); +} + +function multipleSplats(amount) { + for (let i = 0; i < amount; i++) { + const color = generateColor(); + color.r *= 10.0; + color.g *= 10.0; + color.b *= 10.0; + const x = Math.random(); + const y = Math.random(); + const dx = 1000 * (Math.random() - 0.5); + const dy = 1000 * (Math.random() - 0.5); + splat(x, y, dx, dy, color); + } +} + +function splat(x, y, dx, dy, color) { + gl.viewport(0, 0, velocity.width, velocity.height); + splatProgram.bind(); + gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0)); + gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height); + gl.uniform2f(splatProgram.uniforms.point, x, y); + gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0.0); + gl.uniform1f(splatProgram.uniforms.radius, correctRadius(config.SPLAT_RADIUS / 100.0)); + blit(velocity.write.fbo); + velocity.swap(); + + gl.viewport(0, 0, dye.width, dye.height); + gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0)); + gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b); + blit(dye.write.fbo); + dye.swap(); +} + +function correctRadius(radius) { + let aspectRatio = canvas.width / canvas.height; + if (aspectRatio > 1) radius *= aspectRatio; + return radius; +} + +// canvas.addEventListener('mousedown', e => { +// let posX = scaleByPixelRatio(e.offsetX); +// let posY = scaleByPixelRatio(e.offsetY); +// let pointer = pointers.find(p => p.id == -1); +// if (pointer == null) +// pointer = new pointerPrototype(); +// updatePointerDownData(pointer, -1, posX, posY); +// }); +let lastMove = -1; +function checkLastMove() { + const currentMove = window.performance.now(); + if (currentMove - lastMove > 1000) { + lastMove = currentMove; + return true; + } + return false; +} + +canvas.addEventListener("mousemove", (e) => { + if (checkLastMove()) { + let posX = scaleByPixelRatio(e.offsetX); + let posY = scaleByPixelRatio(e.offsetY); + let pointer = pointers.find((p) => p.id == -1); + if (pointer == null) pointer = new pointerPrototype(); + updatePointerDownData(pointer, -1, posX, posY); + } + + let pointer = pointers[0]; + if (!pointer.down) return; + let posX = scaleByPixelRatio(e.offsetX); + let posY = scaleByPixelRatio(e.offsetY); + updatePointerMoveData(pointer, posX, posY); +}); + +window.addEventListener("mouseup", () => { + updatePointerUpData(pointers[0]); +}); + +canvas.addEventListener("touchstart", (e) => { + e.preventDefault(); + const touches = e.targetTouches; + while (touches.length >= pointers.length) pointers.push(new pointerPrototype()); + for (let i = 0; i < touches.length; i++) { + let posX = scaleByPixelRatio(touches[i].pageX); + let posY = scaleByPixelRatio(touches[i].pageY); + updatePointerDownData(pointers[i + 1], touches[i].identifier, posX, posY); + } +}); + +canvas.addEventListener( + "touchmove", + (e) => { + e.preventDefault(); + const touches = e.targetTouches; + for (let i = 0; i < touches.length; i++) { + let pointer = pointers[i + 1]; + if (!pointer.down) continue; + let posX = scaleByPixelRatio(touches[i].pageX); + let posY = scaleByPixelRatio(touches[i].pageY); + updatePointerMoveData(pointer, posX, posY); + } + }, + false +); + +window.addEventListener("touchend", (e) => { + const touches = e.changedTouches; + for (let i = 0; i < touches.length; i++) { + let pointer = pointers.find((p) => p.id == touches[i].identifier); + if (pointer == null) continue; + updatePointerUpData(pointer); + } +}); + +window.addEventListener("keydown", (e) => { + if (e.code === "KeyP") config.PAUSED = !config.PAUSED; + if (e.key === " ") splatStack.push(parseInt(Math.random() * 20) + 5); +}); + +function updatePointerDownData(pointer, id, posX, posY) { + pointer.id = id; + pointer.down = true; + pointer.moved = false; + pointer.texcoordX = posX / canvas.width; + pointer.texcoordY = 1.0 - posY / canvas.height; + pointer.prevTexcoordX = pointer.texcoordX; + pointer.prevTexcoordY = pointer.texcoordY; + pointer.deltaX = 0; + pointer.deltaY = 0; + pointer.color = generateColor(); +} + +function updatePointerMoveData(pointer, posX, posY) { + pointer.prevTexcoordX = pointer.texcoordX; + pointer.prevTexcoordY = pointer.texcoordY; + pointer.texcoordX = posX / canvas.width; + pointer.texcoordY = 1.0 - posY / canvas.height; + pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX); + pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY); + pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0; +} + +function updatePointerUpData(pointer) { + pointer.down = false; +} + +function correctDeltaX(delta) { + let aspectRatio = canvas.width / canvas.height; + if (aspectRatio < 1) delta *= aspectRatio; + return delta; +} + +function correctDeltaY(delta) { + let aspectRatio = canvas.width / canvas.height; + if (aspectRatio > 1) delta /= aspectRatio; + return delta; +} + +function generateColor() { + let c = HSVtoRGB(Math.random(), 1.0, 1.0); + if (!config.CUSTOM_COLOR) + { + c.r *= 0.15; + c.g *= 0.15; + c.b *= 0.15; + } + else + { + let [colorLeft,colorRight]=colorRange; + try { + if(colorConfig!==null){ + const probabilityTotal = colorConfig.reduce((sum,c)=>sum+c[0],0); + let rand = Math.random()*probabilityTotal; + for(const c of colorConfig){ + rand -= c[0]; + if(rand<0){ + colorLeft = c[1]; + colorRight = c[2]; + break; + } + } + } + let l = RGBtoHSV(hexToRgb(colorLeft)), r = RGBtoHSV(hexToRgb(colorRight)), x; + if(r.s < l.s){ + x = r.s; + r.s = l.s; + l.s = x; + } + if(r.v < l.v){ + x = r.v; + r.v = l.v; + l.v = x; + } + if(r.h < l.h){ + r.h += 1; + } + + x = Math.random()*(r.h-l.h) + l.h; + if(x>1){ + x -= 1; + } + c = HSVtoRGB(x,Math.random()*(r.s-l.s)+l.s, (Math.random()*(r.v-l.v)+l.v)*0.15); + } catch (error) { + console.log("Invalid color config",error); + c = hexToRgb("#000000"); + } + } + return c; +} + +function HSVtoRGB(h, s, v) { + let r, g, b, i, f, p, q, t; + i = Math.floor(h * 6); + f = h * 6 - i; + p = v * (1 - s); + q = v * (1 - f * s); + t = v * (1 - (1 - f) * s); + + switch (i % 6) { + case 0: + (r = v), (g = t), (b = p); + break; + case 1: + (r = q), (g = v), (b = p); + break; + case 2: + (r = p), (g = v), (b = t); + break; + case 3: + (r = p), (g = q), (b = v); + break; + case 4: + (r = t), (g = p), (b = v); + break; + case 5: + (r = v), (g = p), (b = q); + break; + } + + return { + r, + g, + b, + }; +} + +function normalizeColor(input) { + let output = { + r: input.r / 255, + g: input.g / 255, + b: input.b / 255, + }; + return output; +} + +function wrap(value, min, max) { + let range = max - min; + if (range == 0) return min; + return ((value - min) % range) + min; +} + +function getResolution(resolution) { + let aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight; + if (aspectRatio < 1) aspectRatio = 1.0 / aspectRatio; + + let min = Math.round(resolution); + let max = Math.round(resolution * aspectRatio); + + if (gl.drawingBufferWidth > gl.drawingBufferHeight) return { width: max, height: min }; + else return { width: min, height: max }; +} + +function getTextureScale(texture, width, height) { + return { + x: width / texture.width, + y: height / texture.height, + }; +} + +function scaleByPixelRatio(input) { + let pixelRatio = window.devicePixelRatio || 1; + return Math.floor(input * pixelRatio); +} + +function hashCode(s) { + if (s.length == 0) return 0; + let hash = 0; + for (let i = 0; i < s.length; i++) { + hash = (hash << 5) - hash + s.charCodeAt(i); + hash |= 0; // Convert to 32bit integer + } + return hash; +} diff --git a/overlays/icon-lively-300.png b/overlays/icon-lively-300.png new file mode 100644 index 00000000..2f023b10 Binary files /dev/null and b/overlays/icon-lively-300.png differ diff --git a/preview.gif b/preview.gif new file mode 100644 index 00000000..ead94524 Binary files /dev/null and b/preview.gif differ diff --git a/screenshot.jpg b/screenshot.jpg deleted file mode 100644 index 19311e37..00000000 Binary files a/screenshot.jpg and /dev/null differ diff --git a/script.js b/script.js deleted file mode 100644 index f02dfa3d..00000000 --- a/script.js +++ /dev/null @@ -1,1604 +0,0 @@ -/* -MIT License - -Copyright (c) 2017 Pavel Dobryakov - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -'use strict'; - -const canvas = document.getElementsByTagName('canvas')[0]; -resizeCanvas(); - -let config = { - SIM_RESOLUTION: 128, - DYE_RESOLUTION: 1024, - CAPTURE_RESOLUTION: 512, - DENSITY_DISSIPATION: 1, - VELOCITY_DISSIPATION: 0.2, - PRESSURE: 0.8, - PRESSURE_ITERATIONS: 20, - CURL: 30, - SPLAT_RADIUS: 0.25, - SPLAT_FORCE: 6000, - SHADING: true, - COLORFUL: true, - COLOR_UPDATE_SPEED: 10, - PAUSED: false, - BACK_COLOR: { r: 0, g: 0, b: 0 }, - TRANSPARENT: false, - BLOOM: true, - BLOOM_ITERATIONS: 8, - BLOOM_RESOLUTION: 256, - BLOOM_INTENSITY: 0.8, - BLOOM_THRESHOLD: 0.6, - BLOOM_SOFT_KNEE: 0.7, - SUNRAYS: true, - SUNRAYS_RESOLUTION: 196, - SUNRAYS_WEIGHT: 1.0, -} - -function pointerPrototype () { - this.id = -1; - this.texcoordX = 0; - this.texcoordY = 0; - this.prevTexcoordX = 0; - this.prevTexcoordY = 0; - this.deltaX = 0; - this.deltaY = 0; - this.down = false; - this.moved = false; - this.color = [30, 0, 300]; -} - -let pointers = []; -let splatStack = []; -pointers.push(new pointerPrototype()); - -const { gl, ext } = getWebGLContext(canvas); - -if (isMobile()) { - config.DYE_RESOLUTION = 512; -} -if (!ext.supportLinearFiltering) { - config.DYE_RESOLUTION = 512; - config.SHADING = false; - config.BLOOM = false; - config.SUNRAYS = false; -} - -startGUI(); - -function getWebGLContext (canvas) { - const params = { alpha: true, depth: false, stencil: false, antialias: false, preserveDrawingBuffer: false }; - - let gl = canvas.getContext('webgl2', params); - const isWebGL2 = !!gl; - if (!isWebGL2) - gl = canvas.getContext('webgl', params) || canvas.getContext('experimental-webgl', params); - - let halfFloat; - let supportLinearFiltering; - if (isWebGL2) { - gl.getExtension('EXT_color_buffer_float'); - supportLinearFiltering = gl.getExtension('OES_texture_float_linear'); - } else { - halfFloat = gl.getExtension('OES_texture_half_float'); - supportLinearFiltering = gl.getExtension('OES_texture_half_float_linear'); - } - - gl.clearColor(0.0, 0.0, 0.0, 1.0); - - const halfFloatTexType = isWebGL2 ? gl.HALF_FLOAT : halfFloat.HALF_FLOAT_OES; - let formatRGBA; - let formatRG; - let formatR; - - if (isWebGL2) - { - formatRGBA = getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, halfFloatTexType); - formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType); - formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType); - } - else - { - formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); - formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); - formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); - } - - ga('send', 'event', isWebGL2 ? 'webgl2' : 'webgl', formatRGBA == null ? 'not supported' : 'supported'); - - return { - gl, - ext: { - formatRGBA, - formatRG, - formatR, - halfFloatTexType, - supportLinearFiltering - } - }; -} - -function getSupportedFormat (gl, internalFormat, format, type) -{ - if (!supportRenderTextureFormat(gl, internalFormat, format, type)) - { - switch (internalFormat) - { - case gl.R16F: - return getSupportedFormat(gl, gl.RG16F, gl.RG, type); - case gl.RG16F: - return getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, type); - default: - return null; - } - } - - return { - internalFormat, - format - } -} - -function supportRenderTextureFormat (gl, internalFormat, format, type) { - let texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); - 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); - gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null); - - let fbo = gl.createFramebuffer(); - gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); - gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); - - const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); - return status == gl.FRAMEBUFFER_COMPLETE; -} - -function startGUI () { - var gui = new dat.GUI({ width: 300 }); - gui.add(config, 'DYE_RESOLUTION', { 'high': 1024, 'medium': 512, 'low': 256, 'very low': 128 }).name('quality').onFinishChange(initFramebuffers); - gui.add(config, 'SIM_RESOLUTION', { '32': 32, '64': 64, '128': 128, '256': 256 }).name('sim resolution').onFinishChange(initFramebuffers); - gui.add(config, 'DENSITY_DISSIPATION', 0, 4.0).name('density diffusion'); - gui.add(config, 'VELOCITY_DISSIPATION', 0, 4.0).name('velocity diffusion'); - gui.add(config, 'PRESSURE', 0.0, 1.0).name('pressure'); - gui.add(config, 'CURL', 0, 50).name('vorticity').step(1); - gui.add(config, 'SPLAT_RADIUS', 0.01, 1.0).name('splat radius'); - gui.add(config, 'SHADING').name('shading').onFinishChange(updateKeywords); - gui.add(config, 'COLORFUL').name('colorful'); - gui.add(config, 'PAUSED').name('paused').listen(); - - gui.add({ fun: () => { - splatStack.push(parseInt(Math.random() * 20) + 5); - } }, 'fun').name('Random splats'); - - let bloomFolder = gui.addFolder('Bloom'); - bloomFolder.add(config, 'BLOOM').name('enabled').onFinishChange(updateKeywords); - bloomFolder.add(config, 'BLOOM_INTENSITY', 0.1, 2.0).name('intensity'); - bloomFolder.add(config, 'BLOOM_THRESHOLD', 0.0, 1.0).name('threshold'); - - let sunraysFolder = gui.addFolder('Sunrays'); - sunraysFolder.add(config, 'SUNRAYS').name('enabled').onFinishChange(updateKeywords); - sunraysFolder.add(config, 'SUNRAYS_WEIGHT', 0.3, 1.0).name('weight'); - - let captureFolder = gui.addFolder('Capture'); - captureFolder.addColor(config, 'BACK_COLOR').name('background color'); - captureFolder.add(config, 'TRANSPARENT').name('transparent'); - captureFolder.add({ fun: captureScreenshot }, 'fun').name('take screenshot'); - - let github = gui.add({ fun : () => { - window.open('https://github.com/PavelDoGreat/WebGL-Fluid-Simulation'); - ga('send', 'event', 'link button', 'github'); - } }, 'fun').name('Github'); - github.__li.className = 'cr function bigFont'; - github.__li.style.borderLeft = '3px solid #8C8C8C'; - let githubIcon = document.createElement('span'); - github.domElement.parentElement.appendChild(githubIcon); - githubIcon.className = 'icon github'; - - let twitter = gui.add({ fun : () => { - ga('send', 'event', 'link button', 'twitter'); - window.open('https://twitter.com/PavelDoGreat'); - } }, 'fun').name('Twitter'); - twitter.__li.className = 'cr function bigFont'; - twitter.__li.style.borderLeft = '3px solid #8C8C8C'; - let twitterIcon = document.createElement('span'); - twitter.domElement.parentElement.appendChild(twitterIcon); - twitterIcon.className = 'icon twitter'; - - let discord = gui.add({ fun : () => { - ga('send', 'event', 'link button', 'discord'); - window.open('https://discordapp.com/invite/CeqZDDE'); - } }, 'fun').name('Discord'); - discord.__li.className = 'cr function bigFont'; - discord.__li.style.borderLeft = '3px solid #8C8C8C'; - let discordIcon = document.createElement('span'); - discord.domElement.parentElement.appendChild(discordIcon); - discordIcon.className = 'icon discord'; - - let app = gui.add({ fun : () => { - ga('send', 'event', 'link button', 'app'); - window.open('http://onelink.to/5b58bn'); - } }, 'fun').name('Check out mobile app'); - app.__li.className = 'cr function appBigFont'; - app.__li.style.borderLeft = '3px solid #00FF7F'; - let appIcon = document.createElement('span'); - app.domElement.parentElement.appendChild(appIcon); - appIcon.className = 'icon app'; - - if (isMobile()) - gui.close(); -} - -function isMobile () { - return /Mobi|Android/i.test(navigator.userAgent); -} - -function captureScreenshot () { - let res = getResolution(config.CAPTURE_RESOLUTION); - let target = createFBO(res.width, res.height, ext.formatRGBA.internalFormat, ext.formatRGBA.format, ext.halfFloatTexType, gl.NEAREST); - render(target); - - let texture = framebufferToTexture(target); - texture = normalizeTexture(texture, target.width, target.height); - - let captureCanvas = textureToCanvas(texture, target.width, target.height); - let datauri = captureCanvas.toDataURL(); - downloadURI('fluid.png', datauri); - URL.revokeObjectURL(datauri); -} - -function framebufferToTexture (target) { - gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo); - let length = target.width * target.height * 4; - let texture = new Float32Array(length); - gl.readPixels(0, 0, target.width, target.height, gl.RGBA, gl.FLOAT, texture); - return texture; -} - -function normalizeTexture (texture, width, height) { - let result = new Uint8Array(texture.length); - let id = 0; - for (let i = height - 1; i >= 0; i--) { - for (let j = 0; j < width; j++) { - let nid = i * width * 4 + j * 4; - result[nid + 0] = clamp01(texture[id + 0]) * 255; - result[nid + 1] = clamp01(texture[id + 1]) * 255; - result[nid + 2] = clamp01(texture[id + 2]) * 255; - result[nid + 3] = clamp01(texture[id + 3]) * 255; - id += 4; - } - } - return result; -} - -function clamp01 (input) { - return Math.min(Math.max(input, 0), 1); -} - -function textureToCanvas (texture, width, height) { - let captureCanvas = document.createElement('canvas'); - let ctx = captureCanvas.getContext('2d'); - captureCanvas.width = width; - captureCanvas.height = height; - - let imageData = ctx.createImageData(width, height); - imageData.data.set(texture); - ctx.putImageData(imageData, 0, 0); - - return captureCanvas; -} - -function downloadURI (filename, uri) { - let link = document.createElement('a'); - link.download = filename; - link.href = uri; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); -} - -class Material { - constructor (vertexShader, fragmentShaderSource) { - this.vertexShader = vertexShader; - this.fragmentShaderSource = fragmentShaderSource; - this.programs = []; - this.activeProgram = null; - this.uniforms = []; - } - - setKeywords (keywords) { - let hash = 0; - for (let i = 0; i < keywords.length; i++) - hash += hashCode(keywords[i]); - - let program = this.programs[hash]; - if (program == null) - { - let fragmentShader = compileShader(gl.FRAGMENT_SHADER, this.fragmentShaderSource, keywords); - program = createProgram(this.vertexShader, fragmentShader); - this.programs[hash] = program; - } - - if (program == this.activeProgram) return; - - this.uniforms = getUniforms(program); - this.activeProgram = program; - } - - bind () { - gl.useProgram(this.activeProgram); - } -} - -class Program { - constructor (vertexShader, fragmentShader) { - this.uniforms = {}; - this.program = createProgram(vertexShader, fragmentShader); - this.uniforms = getUniforms(this.program); - } - - bind () { - gl.useProgram(this.program); - } -} - -function createProgram (vertexShader, fragmentShader) { - let program = gl.createProgram(); - gl.attachShader(program, vertexShader); - gl.attachShader(program, fragmentShader); - gl.linkProgram(program); - - if (!gl.getProgramParameter(program, gl.LINK_STATUS)) - throw gl.getProgramInfoLog(program); - - return program; -} - -function getUniforms (program) { - let uniforms = []; - let uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); - for (let i = 0; i < uniformCount; i++) { - let uniformName = gl.getActiveUniform(program, i).name; - uniforms[uniformName] = gl.getUniformLocation(program, uniformName); - } - return uniforms; -} - -function compileShader (type, source, keywords) { - source = addKeywords(source, keywords); - - const shader = gl.createShader(type); - gl.shaderSource(shader, source); - gl.compileShader(shader); - - if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) - throw gl.getShaderInfoLog(shader); - - return shader; -}; - -function addKeywords (source, keywords) { - if (keywords == null) return source; - let keywordsString = ''; - keywords.forEach(keyword => { - keywordsString += '#define ' + keyword + '\n'; - }); - return keywordsString + source; -} - -const baseVertexShader = compileShader(gl.VERTEX_SHADER, ` - precision highp float; - - attribute vec2 aPosition; - varying vec2 vUv; - varying vec2 vL; - varying vec2 vR; - varying vec2 vT; - varying vec2 vB; - uniform vec2 texelSize; - - void main () { - vUv = aPosition * 0.5 + 0.5; - vL = vUv - vec2(texelSize.x, 0.0); - vR = vUv + vec2(texelSize.x, 0.0); - vT = vUv + vec2(0.0, texelSize.y); - vB = vUv - vec2(0.0, texelSize.y); - gl_Position = vec4(aPosition, 0.0, 1.0); - } -`); - -const blurVertexShader = compileShader(gl.VERTEX_SHADER, ` - precision highp float; - - attribute vec2 aPosition; - varying vec2 vUv; - varying vec2 vL; - varying vec2 vR; - uniform vec2 texelSize; - - void main () { - vUv = aPosition * 0.5 + 0.5; - float offset = 1.33333333; - vL = vUv - texelSize * offset; - vR = vUv + texelSize * offset; - gl_Position = vec4(aPosition, 0.0, 1.0); - } -`); - -const blurShader = compileShader(gl.FRAGMENT_SHADER, ` - precision mediump float; - precision mediump sampler2D; - - varying vec2 vUv; - varying vec2 vL; - varying vec2 vR; - uniform sampler2D uTexture; - - void main () { - vec4 sum = texture2D(uTexture, vUv) * 0.29411764; - sum += texture2D(uTexture, vL) * 0.35294117; - sum += texture2D(uTexture, vR) * 0.35294117; - gl_FragColor = sum; - } -`); - -const copyShader = compileShader(gl.FRAGMENT_SHADER, ` - precision mediump float; - precision mediump sampler2D; - - varying highp vec2 vUv; - uniform sampler2D uTexture; - - void main () { - gl_FragColor = texture2D(uTexture, vUv); - } -`); - -const clearShader = compileShader(gl.FRAGMENT_SHADER, ` - precision mediump float; - precision mediump sampler2D; - - varying highp vec2 vUv; - uniform sampler2D uTexture; - uniform float value; - - void main () { - gl_FragColor = value * texture2D(uTexture, vUv); - } -`); - -const colorShader = compileShader(gl.FRAGMENT_SHADER, ` - precision mediump float; - - uniform vec4 color; - - void main () { - gl_FragColor = color; - } -`); - -const checkerboardShader = compileShader(gl.FRAGMENT_SHADER, ` - precision highp float; - precision highp sampler2D; - - varying vec2 vUv; - uniform sampler2D uTexture; - uniform float aspectRatio; - - #define SCALE 25.0 - - void main () { - vec2 uv = floor(vUv * SCALE * vec2(aspectRatio, 1.0)); - float v = mod(uv.x + uv.y, 2.0); - v = v * 0.1 + 0.8; - gl_FragColor = vec4(vec3(v), 1.0); - } -`); - -const displayShaderSource = ` - precision highp float; - precision highp sampler2D; - - varying vec2 vUv; - varying vec2 vL; - varying vec2 vR; - varying vec2 vT; - varying vec2 vB; - uniform sampler2D uTexture; - uniform sampler2D uBloom; - uniform sampler2D uSunrays; - uniform sampler2D uDithering; - uniform vec2 ditherScale; - uniform vec2 texelSize; - - vec3 linearToGamma (vec3 color) { - color = max(color, vec3(0)); - return max(1.055 * pow(color, vec3(0.416666667)) - 0.055, vec3(0)); - } - - void main () { - vec3 c = texture2D(uTexture, vUv).rgb; - - #ifdef SHADING - vec3 lc = texture2D(uTexture, vL).rgb; - vec3 rc = texture2D(uTexture, vR).rgb; - vec3 tc = texture2D(uTexture, vT).rgb; - vec3 bc = texture2D(uTexture, vB).rgb; - - float dx = length(rc) - length(lc); - float dy = length(tc) - length(bc); - - vec3 n = normalize(vec3(dx, dy, length(texelSize))); - vec3 l = vec3(0.0, 0.0, 1.0); - - float diffuse = clamp(dot(n, l) + 0.7, 0.7, 1.0); - c *= diffuse; - #endif - - #ifdef BLOOM - vec3 bloom = texture2D(uBloom, vUv).rgb; - #endif - - #ifdef SUNRAYS - float sunrays = texture2D(uSunrays, vUv).r; - c *= sunrays; - #ifdef BLOOM - bloom *= sunrays; - #endif - #endif - - #ifdef BLOOM - float noise = texture2D(uDithering, vUv * ditherScale).r; - noise = noise * 2.0 - 1.0; - bloom += noise / 255.0; - bloom = linearToGamma(bloom); - c += bloom; - #endif - - float a = max(c.r, max(c.g, c.b)); - gl_FragColor = vec4(c, a); - } -`; - -const bloomPrefilterShader = compileShader(gl.FRAGMENT_SHADER, ` - precision mediump float; - precision mediump sampler2D; - - varying vec2 vUv; - uniform sampler2D uTexture; - uniform vec3 curve; - uniform float threshold; - - void main () { - vec3 c = texture2D(uTexture, vUv).rgb; - float br = max(c.r, max(c.g, c.b)); - float rq = clamp(br - curve.x, 0.0, curve.y); - rq = curve.z * rq * rq; - c *= max(rq, br - threshold) / max(br, 0.0001); - gl_FragColor = vec4(c, 0.0); - } -`); - -const bloomBlurShader = compileShader(gl.FRAGMENT_SHADER, ` - precision mediump float; - precision mediump sampler2D; - - varying vec2 vL; - varying vec2 vR; - varying vec2 vT; - varying vec2 vB; - uniform sampler2D uTexture; - - void main () { - vec4 sum = vec4(0.0); - sum += texture2D(uTexture, vL); - sum += texture2D(uTexture, vR); - sum += texture2D(uTexture, vT); - sum += texture2D(uTexture, vB); - sum *= 0.25; - gl_FragColor = sum; - } -`); - -const bloomFinalShader = compileShader(gl.FRAGMENT_SHADER, ` - precision mediump float; - precision mediump sampler2D; - - varying vec2 vL; - varying vec2 vR; - varying vec2 vT; - varying vec2 vB; - uniform sampler2D uTexture; - uniform float intensity; - - void main () { - vec4 sum = vec4(0.0); - sum += texture2D(uTexture, vL); - sum += texture2D(uTexture, vR); - sum += texture2D(uTexture, vT); - sum += texture2D(uTexture, vB); - sum *= 0.25; - gl_FragColor = sum * intensity; - } -`); - -const sunraysMaskShader = compileShader(gl.FRAGMENT_SHADER, ` - precision highp float; - precision highp sampler2D; - - varying vec2 vUv; - uniform sampler2D uTexture; - - void main () { - vec4 c = texture2D(uTexture, vUv); - float br = max(c.r, max(c.g, c.b)); - c.a = 1.0 - min(max(br * 20.0, 0.0), 0.8); - gl_FragColor = c; - } -`); - -const sunraysShader = compileShader(gl.FRAGMENT_SHADER, ` - precision highp float; - precision highp sampler2D; - - varying vec2 vUv; - uniform sampler2D uTexture; - uniform float weight; - - #define ITERATIONS 16 - - void main () { - float Density = 0.3; - float Decay = 0.95; - float Exposure = 0.7; - - vec2 coord = vUv; - vec2 dir = vUv - 0.5; - - dir *= 1.0 / float(ITERATIONS) * Density; - float illuminationDecay = 1.0; - - float color = texture2D(uTexture, vUv).a; - - for (int i = 0; i < ITERATIONS; i++) - { - coord -= dir; - float col = texture2D(uTexture, coord).a; - color += col * illuminationDecay * weight; - illuminationDecay *= Decay; - } - - gl_FragColor = vec4(color * Exposure, 0.0, 0.0, 1.0); - } -`); - -const splatShader = compileShader(gl.FRAGMENT_SHADER, ` - precision highp float; - precision highp sampler2D; - - varying vec2 vUv; - uniform sampler2D uTarget; - uniform float aspectRatio; - uniform vec3 color; - uniform vec2 point; - uniform float radius; - - void main () { - vec2 p = vUv - point.xy; - p.x *= aspectRatio; - vec3 splat = exp(-dot(p, p) / radius) * color; - vec3 base = texture2D(uTarget, vUv).xyz; - gl_FragColor = vec4(base + splat, 1.0); - } -`); - -const advectionShader = compileShader(gl.FRAGMENT_SHADER, ` - precision highp float; - precision highp sampler2D; - - varying vec2 vUv; - uniform sampler2D uVelocity; - uniform sampler2D uSource; - uniform vec2 texelSize; - uniform vec2 dyeTexelSize; - uniform float dt; - uniform float dissipation; - - vec4 bilerp (sampler2D sam, vec2 uv, vec2 tsize) { - vec2 st = uv / tsize - 0.5; - - vec2 iuv = floor(st); - vec2 fuv = fract(st); - - vec4 a = texture2D(sam, (iuv + vec2(0.5, 0.5)) * tsize); - vec4 b = texture2D(sam, (iuv + vec2(1.5, 0.5)) * tsize); - vec4 c = texture2D(sam, (iuv + vec2(0.5, 1.5)) * tsize); - vec4 d = texture2D(sam, (iuv + vec2(1.5, 1.5)) * tsize); - - return mix(mix(a, b, fuv.x), mix(c, d, fuv.x), fuv.y); - } - - void main () { - #ifdef MANUAL_FILTERING - vec2 coord = vUv - dt * bilerp(uVelocity, vUv, texelSize).xy * texelSize; - vec4 result = bilerp(uSource, coord, dyeTexelSize); - #else - vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize; - vec4 result = texture2D(uSource, coord); - #endif - float decay = 1.0 + dissipation * dt; - gl_FragColor = result / decay; - }`, - ext.supportLinearFiltering ? null : ['MANUAL_FILTERING'] -); - -const divergenceShader = compileShader(gl.FRAGMENT_SHADER, ` - precision mediump float; - precision mediump sampler2D; - - varying highp vec2 vUv; - varying highp vec2 vL; - varying highp vec2 vR; - varying highp vec2 vT; - varying highp vec2 vB; - uniform sampler2D uVelocity; - - void main () { - float L = texture2D(uVelocity, vL).x; - float R = texture2D(uVelocity, vR).x; - float T = texture2D(uVelocity, vT).y; - float B = texture2D(uVelocity, vB).y; - - vec2 C = texture2D(uVelocity, vUv).xy; - if (vL.x < 0.0) { L = -C.x; } - if (vR.x > 1.0) { R = -C.x; } - if (vT.y > 1.0) { T = -C.y; } - if (vB.y < 0.0) { B = -C.y; } - - float div = 0.5 * (R - L + T - B); - gl_FragColor = vec4(div, 0.0, 0.0, 1.0); - } -`); - -const curlShader = compileShader(gl.FRAGMENT_SHADER, ` - precision mediump float; - precision mediump sampler2D; - - varying highp vec2 vUv; - varying highp vec2 vL; - varying highp vec2 vR; - varying highp vec2 vT; - varying highp vec2 vB; - uniform sampler2D uVelocity; - - void main () { - float L = texture2D(uVelocity, vL).y; - float R = texture2D(uVelocity, vR).y; - float T = texture2D(uVelocity, vT).x; - float B = texture2D(uVelocity, vB).x; - float vorticity = R - L - T + B; - gl_FragColor = vec4(0.5 * vorticity, 0.0, 0.0, 1.0); - } -`); - -const vorticityShader = compileShader(gl.FRAGMENT_SHADER, ` - precision highp float; - precision highp sampler2D; - - varying vec2 vUv; - varying vec2 vL; - varying vec2 vR; - varying vec2 vT; - varying vec2 vB; - uniform sampler2D uVelocity; - uniform sampler2D uCurl; - uniform float curl; - uniform float dt; - - void main () { - float L = texture2D(uCurl, vL).x; - float R = texture2D(uCurl, vR).x; - float T = texture2D(uCurl, vT).x; - float B = texture2D(uCurl, vB).x; - float C = texture2D(uCurl, vUv).x; - - vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L)); - force /= length(force) + 0.0001; - force *= curl * C; - force.y *= -1.0; - - vec2 vel = texture2D(uVelocity, vUv).xy; - gl_FragColor = vec4(vel + force * dt, 0.0, 1.0); - } -`); - -const pressureShader = compileShader(gl.FRAGMENT_SHADER, ` - precision mediump float; - precision mediump sampler2D; - - varying highp vec2 vUv; - varying highp vec2 vL; - varying highp vec2 vR; - varying highp vec2 vT; - varying highp vec2 vB; - uniform sampler2D uPressure; - uniform sampler2D uDivergence; - - void main () { - float L = texture2D(uPressure, vL).x; - float R = texture2D(uPressure, vR).x; - float T = texture2D(uPressure, vT).x; - float B = texture2D(uPressure, vB).x; - float C = texture2D(uPressure, vUv).x; - float divergence = texture2D(uDivergence, vUv).x; - float pressure = (L + R + B + T - divergence) * 0.25; - gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0); - } -`); - -const gradientSubtractShader = compileShader(gl.FRAGMENT_SHADER, ` - precision mediump float; - precision mediump sampler2D; - - varying highp vec2 vUv; - varying highp vec2 vL; - varying highp vec2 vR; - varying highp vec2 vT; - varying highp vec2 vB; - uniform sampler2D uPressure; - uniform sampler2D uVelocity; - - void main () { - float L = texture2D(uPressure, vL).x; - float R = texture2D(uPressure, vR).x; - float T = texture2D(uPressure, vT).x; - float B = texture2D(uPressure, vB).x; - vec2 velocity = texture2D(uVelocity, vUv).xy; - velocity.xy -= vec2(R - L, T - B); - gl_FragColor = vec4(velocity, 0.0, 1.0); - } -`); - -const blit = (() => { - gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); - gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), gl.STATIC_DRAW); - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer()); - gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW); - gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); - gl.enableVertexAttribArray(0); - - return (destination) => { - gl.bindFramebuffer(gl.FRAMEBUFFER, destination); - gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); - } -})(); - -let dye; -let velocity; -let divergence; -let curl; -let pressure; -let bloom; -let bloomFramebuffers = []; -let sunrays; -let sunraysTemp; - -let ditheringTexture = createTextureAsync('LDR_LLL1_0.png'); - -const blurProgram = new Program(blurVertexShader, blurShader); -const copyProgram = new Program(baseVertexShader, copyShader); -const clearProgram = new Program(baseVertexShader, clearShader); -const colorProgram = new Program(baseVertexShader, colorShader); -const checkerboardProgram = new Program(baseVertexShader, checkerboardShader); -const bloomPrefilterProgram = new Program(baseVertexShader, bloomPrefilterShader); -const bloomBlurProgram = new Program(baseVertexShader, bloomBlurShader); -const bloomFinalProgram = new Program(baseVertexShader, bloomFinalShader); -const sunraysMaskProgram = new Program(baseVertexShader, sunraysMaskShader); -const sunraysProgram = new Program(baseVertexShader, sunraysShader); -const splatProgram = new Program(baseVertexShader, splatShader); -const advectionProgram = new Program(baseVertexShader, advectionShader); -const divergenceProgram = new Program(baseVertexShader, divergenceShader); -const curlProgram = new Program(baseVertexShader, curlShader); -const vorticityProgram = new Program(baseVertexShader, vorticityShader); -const pressureProgram = new Program(baseVertexShader, pressureShader); -const gradienSubtractProgram = new Program(baseVertexShader, gradientSubtractShader); - -const displayMaterial = new Material(baseVertexShader, displayShaderSource); - -function initFramebuffers () { - let simRes = getResolution(config.SIM_RESOLUTION); - let dyeRes = getResolution(config.DYE_RESOLUTION); - - const texType = ext.halfFloatTexType; - const rgba = ext.formatRGBA; - const rg = ext.formatRG; - const r = ext.formatR; - const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; - - if (dye == null) - dye = createDoubleFBO(dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering); - else - dye = resizeDoubleFBO(dye, dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering); - - if (velocity == null) - velocity = createDoubleFBO(simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering); - else - velocity = resizeDoubleFBO(velocity, simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering); - - divergence = createFBO (simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); - curl = createFBO (simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); - pressure = createDoubleFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); - - initBloomFramebuffers(); - initSunraysFramebuffers(); -} - -function initBloomFramebuffers () { - let res = getResolution(config.BLOOM_RESOLUTION); - - const texType = ext.halfFloatTexType; - const rgba = ext.formatRGBA; - const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; - - bloom = createFBO(res.width, res.height, rgba.internalFormat, rgba.format, texType, filtering); - - bloomFramebuffers.length = 0; - for (let i = 0; i < config.BLOOM_ITERATIONS; i++) - { - let width = res.width >> (i + 1); - let height = res.height >> (i + 1); - - if (width < 2 || height < 2) break; - - let fbo = createFBO(width, height, rgba.internalFormat, rgba.format, texType, filtering); - bloomFramebuffers.push(fbo); - } -} - -function initSunraysFramebuffers () { - let res = getResolution(config.SUNRAYS_RESOLUTION); - - const texType = ext.halfFloatTexType; - const r = ext.formatR; - const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; - - sunrays = createFBO(res.width, res.height, r.internalFormat, r.format, texType, filtering); - sunraysTemp = createFBO(res.width, res.height, r.internalFormat, r.format, texType, filtering); -} - -function createFBO (w, h, internalFormat, format, type, param) { - gl.activeTexture(gl.TEXTURE0); - let texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param); - 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); - gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null); - - let fbo = gl.createFramebuffer(); - gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); - gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); - gl.viewport(0, 0, w, h); - gl.clear(gl.COLOR_BUFFER_BIT); - - let texelSizeX = 1.0 / w; - let texelSizeY = 1.0 / h; - - return { - texture, - fbo, - width: w, - height: h, - texelSizeX, - texelSizeY, - attach (id) { - gl.activeTexture(gl.TEXTURE0 + id); - gl.bindTexture(gl.TEXTURE_2D, texture); - return id; - } - }; -} - -function createDoubleFBO (w, h, internalFormat, format, type, param) { - let fbo1 = createFBO(w, h, internalFormat, format, type, param); - let fbo2 = createFBO(w, h, internalFormat, format, type, param); - - return { - width: w, - height: h, - texelSizeX: fbo1.texelSizeX, - texelSizeY: fbo1.texelSizeY, - get read () { - return fbo1; - }, - set read (value) { - fbo1 = value; - }, - get write () { - return fbo2; - }, - set write (value) { - fbo2 = value; - }, - swap () { - let temp = fbo1; - fbo1 = fbo2; - fbo2 = temp; - } - } -} - -function resizeFBO (target, w, h, internalFormat, format, type, param) { - let newFBO = createFBO(w, h, internalFormat, format, type, param); - copyProgram.bind(); - gl.uniform1i(copyProgram.uniforms.uTexture, target.attach(0)); - blit(newFBO.fbo); - return newFBO; -} - -function resizeDoubleFBO (target, w, h, internalFormat, format, type, param) { - if (target.width == w && target.height == h) - return target; - target.read = resizeFBO(target.read, w, h, internalFormat, format, type, param); - target.write = createFBO(w, h, internalFormat, format, type, param); - target.width = w; - target.height = h; - target.texelSizeX = 1.0 / w; - target.texelSizeY = 1.0 / h; - return target; -} - -function createTextureAsync (url) { - let texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, texture); - 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.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, 1, 1, 0, gl.RGB, gl.UNSIGNED_BYTE, new Uint8Array([255, 255, 255])); - - let obj = { - texture, - width: 1, - height: 1, - attach (id) { - gl.activeTexture(gl.TEXTURE0 + id); - gl.bindTexture(gl.TEXTURE_2D, texture); - return id; - } - }; - - let image = new Image(); - image.onload = () => { - obj.width = image.width; - obj.height = image.height; - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image); - }; - image.src = url; - - return obj; -} - -function updateKeywords () { - let displayKeywords = []; - if (config.SHADING) displayKeywords.push("SHADING"); - if (config.BLOOM) displayKeywords.push("BLOOM"); - if (config.SUNRAYS) displayKeywords.push("SUNRAYS"); - displayMaterial.setKeywords(displayKeywords); -} - -updateKeywords(); -initFramebuffers(); -multipleSplats(parseInt(Math.random() * 20) + 5); - -let lastUpdateTime = Date.now(); -let colorUpdateTimer = 0.0; -update(); - -function update () { - const dt = calcDeltaTime(); - if (resizeCanvas()) - initFramebuffers(); - updateColors(dt); - applyInputs(); - if (!config.PAUSED) - step(dt); - render(null); - requestAnimationFrame(update); -} - -function calcDeltaTime () { - let now = Date.now(); - let dt = (now - lastUpdateTime) / 1000; - dt = Math.min(dt, 0.016666); - lastUpdateTime = now; - return dt; -} - -function resizeCanvas () { - let width = scaleByPixelRatio(canvas.clientWidth); - let height = scaleByPixelRatio(canvas.clientHeight); - if (canvas.width != width || canvas.height != height) { - canvas.width = width; - canvas.height = height; - return true; - } - return false; -} - -function updateColors (dt) { - if (!config.COLORFUL) return; - - colorUpdateTimer += dt * config.COLOR_UPDATE_SPEED; - if (colorUpdateTimer >= 1) { - colorUpdateTimer = wrap(colorUpdateTimer, 0, 1); - pointers.forEach(p => { - p.color = generateColor(); - }); - } -} - -function applyInputs () { - if (splatStack.length > 0) - multipleSplats(splatStack.pop()); - - pointers.forEach(p => { - if (p.moved) { - p.moved = false; - splatPointer(p); - } - }); -} - -function step (dt) { - gl.disable(gl.BLEND); - gl.viewport(0, 0, velocity.width, velocity.height); - - curlProgram.bind(); - gl.uniform2f(curlProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); - gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read.attach(0)); - blit(curl.fbo); - - vorticityProgram.bind(); - gl.uniform2f(vorticityProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); - gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read.attach(0)); - gl.uniform1i(vorticityProgram.uniforms.uCurl, curl.attach(1)); - gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL); - gl.uniform1f(vorticityProgram.uniforms.dt, dt); - blit(velocity.write.fbo); - velocity.swap(); - - divergenceProgram.bind(); - gl.uniform2f(divergenceProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); - gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0)); - blit(divergence.fbo); - - clearProgram.bind(); - gl.uniform1i(clearProgram.uniforms.uTexture, pressure.read.attach(0)); - gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE); - blit(pressure.write.fbo); - pressure.swap(); - - pressureProgram.bind(); - gl.uniform2f(pressureProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); - gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence.attach(0)); - for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) { - gl.uniform1i(pressureProgram.uniforms.uPressure, pressure.read.attach(1)); - blit(pressure.write.fbo); - pressure.swap(); - } - - gradienSubtractProgram.bind(); - gl.uniform2f(gradienSubtractProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); - gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressure.read.attach(0)); - gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.read.attach(1)); - blit(velocity.write.fbo); - velocity.swap(); - - advectionProgram.bind(); - gl.uniform2f(advectionProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); - if (!ext.supportLinearFiltering) - gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, velocity.texelSizeX, velocity.texelSizeY); - let velocityId = velocity.read.attach(0); - gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId); - gl.uniform1i(advectionProgram.uniforms.uSource, velocityId); - gl.uniform1f(advectionProgram.uniforms.dt, dt); - gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION); - blit(velocity.write.fbo); - velocity.swap(); - - gl.viewport(0, 0, dye.width, dye.height); - - if (!ext.supportLinearFiltering) - gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, dye.texelSizeX, dye.texelSizeY); - gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0)); - gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1)); - gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION); - blit(dye.write.fbo); - dye.swap(); -} - -function render (target) { - if (config.BLOOM) - applyBloom(dye.read, bloom); - if (config.SUNRAYS) { - applySunrays(dye.read, dye.write, sunrays); - blur(sunrays, sunraysTemp, 1); - } - - if (target == null || !config.TRANSPARENT) { - gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); - gl.enable(gl.BLEND); - } - else { - gl.disable(gl.BLEND); - } - - let width = target == null ? gl.drawingBufferWidth : target.width; - let height = target == null ? gl.drawingBufferHeight : target.height; - gl.viewport(0, 0, width, height); - - let fbo = target == null ? null : target.fbo; - if (!config.TRANSPARENT) - drawColor(fbo, normalizeColor(config.BACK_COLOR)); - if (target == null && config.TRANSPARENT) - drawCheckerboard(fbo); - drawDisplay(fbo, width, height); -} - -function drawColor (fbo, color) { - colorProgram.bind(); - gl.uniform4f(colorProgram.uniforms.color, color.r, color.g, color.b, 1); - blit(fbo); -} - -function drawCheckerboard (fbo) { - checkerboardProgram.bind(); - gl.uniform1f(checkerboardProgram.uniforms.aspectRatio, canvas.width / canvas.height); - blit(fbo); -} - -function drawDisplay (fbo, width, height) { - displayMaterial.bind(); - if (config.SHADING) - gl.uniform2f(displayMaterial.uniforms.texelSize, 1.0 / width, 1.0 / height); - gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0)); - if (config.BLOOM) { - gl.uniform1i(displayMaterial.uniforms.uBloom, bloom.attach(1)); - gl.uniform1i(displayMaterial.uniforms.uDithering, ditheringTexture.attach(2)); - let scale = getTextureScale(ditheringTexture, width, height); - gl.uniform2f(displayMaterial.uniforms.ditherScale, scale.x, scale.y); - } - if (config.SUNRAYS) - gl.uniform1i(displayMaterial.uniforms.uSunrays, sunrays.attach(3)); - blit(fbo); -} - -function applyBloom (source, destination) { - if (bloomFramebuffers.length < 2) - return; - - let last = destination; - - gl.disable(gl.BLEND); - bloomPrefilterProgram.bind(); - let knee = config.BLOOM_THRESHOLD * config.BLOOM_SOFT_KNEE + 0.0001; - let curve0 = config.BLOOM_THRESHOLD - knee; - let curve1 = knee * 2; - let curve2 = 0.25 / knee; - gl.uniform3f(bloomPrefilterProgram.uniforms.curve, curve0, curve1, curve2); - gl.uniform1f(bloomPrefilterProgram.uniforms.threshold, config.BLOOM_THRESHOLD); - gl.uniform1i(bloomPrefilterProgram.uniforms.uTexture, source.attach(0)); - gl.viewport(0, 0, last.width, last.height); - blit(last.fbo); - - bloomBlurProgram.bind(); - for (let i = 0; i < bloomFramebuffers.length; i++) { - let dest = bloomFramebuffers[i]; - gl.uniform2f(bloomBlurProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY); - gl.uniform1i(bloomBlurProgram.uniforms.uTexture, last.attach(0)); - gl.viewport(0, 0, dest.width, dest.height); - blit(dest.fbo); - last = dest; - } - - gl.blendFunc(gl.ONE, gl.ONE); - gl.enable(gl.BLEND); - - for (let i = bloomFramebuffers.length - 2; i >= 0; i--) { - let baseTex = bloomFramebuffers[i]; - gl.uniform2f(bloomBlurProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY); - gl.uniform1i(bloomBlurProgram.uniforms.uTexture, last.attach(0)); - gl.viewport(0, 0, baseTex.width, baseTex.height); - blit(baseTex.fbo); - last = baseTex; - } - - gl.disable(gl.BLEND); - bloomFinalProgram.bind(); - gl.uniform2f(bloomFinalProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY); - gl.uniform1i(bloomFinalProgram.uniforms.uTexture, last.attach(0)); - gl.uniform1f(bloomFinalProgram.uniforms.intensity, config.BLOOM_INTENSITY); - gl.viewport(0, 0, destination.width, destination.height); - blit(destination.fbo); -} - -function applySunrays (source, mask, destination) { - gl.disable(gl.BLEND); - sunraysMaskProgram.bind(); - gl.uniform1i(sunraysMaskProgram.uniforms.uTexture, source.attach(0)); - gl.viewport(0, 0, mask.width, mask.height); - blit(mask.fbo); - - sunraysProgram.bind(); - gl.uniform1f(sunraysProgram.uniforms.weight, config.SUNRAYS_WEIGHT); - gl.uniform1i(sunraysProgram.uniforms.uTexture, mask.attach(0)); - gl.viewport(0, 0, destination.width, destination.height); - blit(destination.fbo); -} - -function blur (target, temp, iterations) { - blurProgram.bind(); - for (let i = 0; i < iterations; i++) { - gl.uniform2f(blurProgram.uniforms.texelSize, target.texelSizeX, 0.0); - gl.uniform1i(blurProgram.uniforms.uTexture, target.attach(0)); - blit(temp.fbo); - - gl.uniform2f(blurProgram.uniforms.texelSize, 0.0, target.texelSizeY); - gl.uniform1i(blurProgram.uniforms.uTexture, temp.attach(0)); - blit(target.fbo); - } -} - -function splatPointer (pointer) { - let dx = pointer.deltaX * config.SPLAT_FORCE; - let dy = pointer.deltaY * config.SPLAT_FORCE; - splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color); -} - -function multipleSplats (amount) { - for (let i = 0; i < amount; i++) { - const color = generateColor(); - color.r *= 10.0; - color.g *= 10.0; - color.b *= 10.0; - const x = Math.random(); - const y = Math.random(); - const dx = 1000 * (Math.random() - 0.5); - const dy = 1000 * (Math.random() - 0.5); - splat(x, y, dx, dy, color); - } -} - -function splat (x, y, dx, dy, color) { - gl.viewport(0, 0, velocity.width, velocity.height); - splatProgram.bind(); - gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0)); - gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height); - gl.uniform2f(splatProgram.uniforms.point, x, y); - gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0.0); - gl.uniform1f(splatProgram.uniforms.radius, correctRadius(config.SPLAT_RADIUS / 100.0)); - blit(velocity.write.fbo); - velocity.swap(); - - gl.viewport(0, 0, dye.width, dye.height); - gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0)); - gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b); - blit(dye.write.fbo); - dye.swap(); -} - -function correctRadius (radius) { - let aspectRatio = canvas.width / canvas.height; - if (aspectRatio > 1) - radius *= aspectRatio; - return radius; -} - -canvas.addEventListener('mousedown', e => { - let posX = scaleByPixelRatio(e.offsetX); - let posY = scaleByPixelRatio(e.offsetY); - let pointer = pointers.find(p => p.id == -1); - if (pointer == null) - pointer = new pointerPrototype(); - updatePointerDownData(pointer, -1, posX, posY); -}); - -canvas.addEventListener('mousemove', e => { - let pointer = pointers[0]; - if (!pointer.down) return; - let posX = scaleByPixelRatio(e.offsetX); - let posY = scaleByPixelRatio(e.offsetY); - updatePointerMoveData(pointer, posX, posY); -}); - -window.addEventListener('mouseup', () => { - updatePointerUpData(pointers[0]); -}); - -canvas.addEventListener('touchstart', e => { - e.preventDefault(); - const touches = e.targetTouches; - while (touches.length >= pointers.length) - pointers.push(new pointerPrototype()); - for (let i = 0; i < touches.length; i++) { - let posX = scaleByPixelRatio(touches[i].pageX); - let posY = scaleByPixelRatio(touches[i].pageY); - updatePointerDownData(pointers[i + 1], touches[i].identifier, posX, posY); - } -}); - -canvas.addEventListener('touchmove', e => { - e.preventDefault(); - const touches = e.targetTouches; - for (let i = 0; i < touches.length; i++) { - let pointer = pointers[i + 1]; - if (!pointer.down) continue; - let posX = scaleByPixelRatio(touches[i].pageX); - let posY = scaleByPixelRatio(touches[i].pageY); - updatePointerMoveData(pointer, posX, posY); - } -}, false); - -window.addEventListener('touchend', e => { - const touches = e.changedTouches; - for (let i = 0; i < touches.length; i++) - { - let pointer = pointers.find(p => p.id == touches[i].identifier); - if (pointer == null) continue; - updatePointerUpData(pointer); - } -}); - -window.addEventListener('keydown', e => { - if (e.code === 'KeyP') - config.PAUSED = !config.PAUSED; - if (e.key === ' ') - splatStack.push(parseInt(Math.random() * 20) + 5); -}); - -function updatePointerDownData (pointer, id, posX, posY) { - pointer.id = id; - pointer.down = true; - pointer.moved = false; - pointer.texcoordX = posX / canvas.width; - pointer.texcoordY = 1.0 - posY / canvas.height; - pointer.prevTexcoordX = pointer.texcoordX; - pointer.prevTexcoordY = pointer.texcoordY; - pointer.deltaX = 0; - pointer.deltaY = 0; - pointer.color = generateColor(); -} - -function updatePointerMoveData (pointer, posX, posY) { - pointer.prevTexcoordX = pointer.texcoordX; - pointer.prevTexcoordY = pointer.texcoordY; - pointer.texcoordX = posX / canvas.width; - pointer.texcoordY = 1.0 - posY / canvas.height; - pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX); - pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY); - pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0; -} - -function updatePointerUpData (pointer) { - pointer.down = false; -} - -function correctDeltaX (delta) { - let aspectRatio = canvas.width / canvas.height; - if (aspectRatio < 1) delta *= aspectRatio; - return delta; -} - -function correctDeltaY (delta) { - let aspectRatio = canvas.width / canvas.height; - if (aspectRatio > 1) delta /= aspectRatio; - return delta; -} - -function generateColor () { - let c = HSVtoRGB(Math.random(), 1.0, 1.0); - c.r *= 0.15; - c.g *= 0.15; - c.b *= 0.15; - return c; -} - -function HSVtoRGB (h, s, v) { - let r, g, b, i, f, p, q, t; - i = Math.floor(h * 6); - f = h * 6 - i; - p = v * (1 - s); - q = v * (1 - f * s); - t = v * (1 - (1 - f) * s); - - switch (i % 6) { - case 0: r = v, g = t, b = p; break; - case 1: r = q, g = v, b = p; break; - case 2: r = p, g = v, b = t; break; - case 3: r = p, g = q, b = v; break; - case 4: r = t, g = p, b = v; break; - case 5: r = v, g = p, b = q; break; - } - - return { - r, - g, - b - }; -} - -function normalizeColor (input) { - let output = { - r: input.r / 255, - g: input.g / 255, - b: input.b / 255 - }; - return output; -} - -function wrap (value, min, max) { - let range = max - min; - if (range == 0) return min; - return (value - min) % range + min; -} - -function getResolution (resolution) { - let aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight; - if (aspectRatio < 1) - aspectRatio = 1.0 / aspectRatio; - - let min = Math.round(resolution); - let max = Math.round(resolution * aspectRatio); - - if (gl.drawingBufferWidth > gl.drawingBufferHeight) - return { width: max, height: min }; - else - return { width: min, height: max }; -} - -function getTextureScale (texture, width, height) { - return { - x: width / texture.width, - y: height / texture.height - }; -} - -function scaleByPixelRatio (input) { - let pixelRatio = window.devicePixelRatio || 1; - return Math.floor(input * pixelRatio); -} - -function hashCode (s) { - if (s.length == 0) return 0; - let hash = 0; - for (let i = 0; i < s.length; i++) { - hash = (hash << 5) - hash + s.charCodeAt(i); - hash |= 0; // Convert to 32bit integer - } - return hash; -}; \ No newline at end of file diff --git a/thumbnail.jpg b/thumbnail.jpg new file mode 100644 index 00000000..9bdaa0d9 Binary files /dev/null and b/thumbnail.jpg differ