From 357157de623819a9e01edec86dd3159a71255bc6 Mon Sep 17 00:00:00 2001 From: David Martinez Ros Date: Wed, 20 Mar 2019 16:26:50 +0100 Subject: [PATCH] first commit of the project --- css/style.css | 17 ++ images/icon.png | Bin 0 -> 22541 bytes index.html | 44 ++++ js/index.js | 673 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 734 insertions(+) create mode 100644 css/style.css create mode 100644 images/icon.png create mode 100644 index.html create mode 100644 js/index.js diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..34e4e37 --- /dev/null +++ b/css/style.css @@ -0,0 +1,17 @@ +body { + font-family:Helvatica, sans-serif; + background:url(https://wallpaperplay.com/walls/full/9/2/3/181292.jpg); + background-repeat: no-repeat; + color:rgb(200, 200, 200); +} + +#wrapper{ + margin:0 auto; + width:100%; +} + +h3 { + background-color: rgba(201, 76, 76, 0.3); + border: 1px solid rgb(200, 200, 200); + padding: 10px; +} \ No newline at end of file diff --git a/images/icon.png b/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b0bca11ead133b868c4e2663fe4bd2ca03468824 GIT binary patch literal 22541 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 P#=U0yA~E2ecJk7HEGx+x literal 0 HcmV?d00001 diff --git a/index.html b/index.html new file mode 100644 index 0000000..2d918e0 --- /dev/null +++ b/index.html @@ -0,0 +1,44 @@ + + + + + + WebGL Fluid Simulation in Javascript by David Martínez Ros + + + + + + + + + + + + + + + + + + + + + + + + + +

WebGL Fluid Simulation with Javascript

+
+ +
+

Find this project and others from David Martínez Ros on https://davidmartinezros.com / mail davidnezan@gmail.com

+ + + + + + diff --git a/js/index.js b/js/index.js new file mode 100644 index 0000000..49c3add --- /dev/null +++ b/js/index.js @@ -0,0 +1,673 @@ +'use strict'; + +const canvas = document.getElementsByTagName('canvas')[0]; +canvas.width = canvas.clientWidth; +canvas.height = canvas.clientHeight; + +let config = { + TEXTURE_DOWNSAMPLE: 1, + DENSITY_DISSIPATION: 0.98, + VELOCITY_DISSIPATION: 0.99, + PRESSURE_DISSIPATION: 0.8, + PRESSURE_ITERATIONS: 25, + CURL: 30, + SPLAT_RADIUS: 0.005 }; + + +let pointers = []; +let splatStack = []; + +const { gl, ext } = getWebGLContext(canvas); + +function getWebGLContext(canvas) { + const params = { alpha: false, depth: false, stencil: false, antialias: 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); + if (status != gl.FRAMEBUFFER_COMPLETE) + return false; + return true; +} + +function pointerPrototype() { + this.id = -1; + this.x = 0; + this.y = 0; + this.dx = 0; + this.dy = 0; + this.down = false; + this.moved = false; + this.color = [30, 0, 300]; +} + +pointers.push(new pointerPrototype()); + +class GLProgram { + constructor(vertexShader, fragmentShader) { + this.uniforms = {}; + this.program = gl.createProgram(); + + gl.attachShader(this.program, vertexShader); + gl.attachShader(this.program, fragmentShader); + gl.linkProgram(this.program); + + if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) + throw gl.getProgramInfoLog(this.program); + + const uniformCount = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS); + for (let i = 0; i < uniformCount; i++) { + const uniformName = gl.getActiveUniform(this.program, i).name; + this.uniforms[uniformName] = gl.getUniformLocation(this.program, uniformName); + } + } + + bind() { + gl.useProgram(this.program); + }} + + +function compileShader(type, source) { + 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; +}; + +const baseVertexShader = compileShader(gl.VERTEX_SHADER, ` + precision highp float; + precision mediump sampler2D; + + 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 clearShader = compileShader(gl.FRAGMENT_SHADER, ` + precision highp float; + precision mediump sampler2D; + + varying vec2 vUv; + uniform sampler2D uTexture; + uniform float value; + + void main () { + gl_FragColor = value * texture2D(uTexture, vUv); + } +`); + +const displayShader = compileShader(gl.FRAGMENT_SHADER, ` + precision highp float; + precision mediump sampler2D; + + varying vec2 vUv; + uniform sampler2D uTexture; + + void main () { + gl_FragColor = texture2D(uTexture, vUv); + } +`); + +const splatShader = compileShader(gl.FRAGMENT_SHADER, ` + precision highp float; + precision mediump 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 advectionManualFilteringShader = compileShader(gl.FRAGMENT_SHADER, ` + precision highp float; + precision mediump sampler2D; + + varying vec2 vUv; + uniform sampler2D uVelocity; + uniform sampler2D uSource; + uniform vec2 texelSize; + uniform float dt; + uniform float dissipation; + + vec4 bilerp (in sampler2D sam, in vec2 p) { + vec4 st; + st.xy = floor(p - 0.5) + 0.5; + st.zw = st.xy + 1.0; + vec4 uv = st * texelSize.xyxy; + vec4 a = texture2D(sam, uv.xy); + vec4 b = texture2D(sam, uv.zy); + vec4 c = texture2D(sam, uv.xw); + vec4 d = texture2D(sam, uv.zw); + vec2 f = p - st.xy; + return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); + } + + void main () { + vec2 coord = gl_FragCoord.xy - dt * texture2D(uVelocity, vUv).xy; + gl_FragColor = dissipation * bilerp(uSource, coord); + gl_FragColor.a = 1.0; + } +`); + +const advectionShader = compileShader(gl.FRAGMENT_SHADER, ` + precision highp float; + precision mediump sampler2D; + + varying vec2 vUv; + uniform sampler2D uVelocity; + uniform sampler2D uSource; + uniform vec2 texelSize; + uniform float dt; + uniform float dissipation; + + void main () { + vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize; + gl_FragColor = dissipation * texture2D(uSource, coord); + gl_FragColor.a = 1.0; + } +`); + +const divergenceShader = compileShader(gl.FRAGMENT_SHADER, ` + precision highp float; + precision mediump sampler2D; + + varying vec2 vUv; + varying vec2 vL; + varying vec2 vR; + varying vec2 vT; + varying vec2 vB; + uniform sampler2D uVelocity; + + vec2 sampleVelocity (in vec2 uv) { + vec2 multiplier = vec2(1.0, 1.0); + if (uv.x < 0.0) { uv.x = 0.0; multiplier.x = -1.0; } + if (uv.x > 1.0) { uv.x = 1.0; multiplier.x = -1.0; } + if (uv.y < 0.0) { uv.y = 0.0; multiplier.y = -1.0; } + if (uv.y > 1.0) { uv.y = 1.0; multiplier.y = -1.0; } + return multiplier * texture2D(uVelocity, uv).xy; + } + + void main () { + float L = sampleVelocity(vL).x; + float R = sampleVelocity(vR).x; + float T = sampleVelocity(vT).y; + float B = sampleVelocity(vB).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 highp float; + precision mediump sampler2D; + + varying vec2 vUv; + varying vec2 vL; + varying vec2 vR; + varying vec2 vT; + varying 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(vorticity, 0.0, 0.0, 1.0); + } +`); + +const vorticityShader = compileShader(gl.FRAGMENT_SHADER, ` + precision highp float; + precision mediump sampler2D; + + varying vec2 vUv; + varying vec2 vT; + varying vec2 vB; + uniform sampler2D uVelocity; + uniform sampler2D uCurl; + uniform float curl; + uniform float dt; + + void main () { + float T = texture2D(uCurl, vT).x; + float B = texture2D(uCurl, vB).x; + float C = texture2D(uCurl, vUv).x; + vec2 force = vec2(abs(T) - abs(B), 0.0); + force *= 1.0 / length(force + 0.00001) * curl * C; + vec2 vel = texture2D(uVelocity, vUv).xy; + gl_FragColor = vec4(vel + force * dt, 0.0, 1.0); + } +`); + +const pressureShader = compileShader(gl.FRAGMENT_SHADER, ` + precision highp float; + precision mediump sampler2D; + + varying vec2 vUv; + varying vec2 vL; + varying vec2 vR; + varying vec2 vT; + varying vec2 vB; + uniform sampler2D uPressure; + uniform sampler2D uDivergence; + + vec2 boundary (in vec2 uv) { + uv = min(max(uv, 0.0), 1.0); + return uv; + } + + void main () { + float L = texture2D(uPressure, boundary(vL)).x; + float R = texture2D(uPressure, boundary(vR)).x; + float T = texture2D(uPressure, boundary(vT)).x; + float B = texture2D(uPressure, boundary(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 highp float; + precision mediump sampler2D; + + varying vec2 vUv; + varying vec2 vL; + varying vec2 vR; + varying vec2 vT; + varying vec2 vB; + uniform sampler2D uPressure; + uniform sampler2D uVelocity; + + vec2 boundary (in vec2 uv) { + uv = min(max(uv, 0.0), 1.0); + return uv; + } + + void main () { + float L = texture2D(uPressure, boundary(vL)).x; + float R = texture2D(uPressure, boundary(vR)).x; + float T = texture2D(uPressure, boundary(vT)).x; + float B = texture2D(uPressure, boundary(vB)).x; + vec2 velocity = texture2D(uVelocity, vUv).xy; + velocity.xy -= vec2(R - L, T - B); + gl_FragColor = vec4(velocity, 0.0, 1.0); + } +`); + +let textureWidth; +let textureHeight; +let density; +let velocity; +let divergence; +let curl; +let pressure; +initFramebuffers(); + +const clearProgram = new GLProgram(baseVertexShader, clearShader); +const displayProgram = new GLProgram(baseVertexShader, displayShader); +const splatProgram = new GLProgram(baseVertexShader, splatShader); +const advectionProgram = new GLProgram(baseVertexShader, ext.supportLinearFiltering ? advectionShader : advectionManualFilteringShader); +const divergenceProgram = new GLProgram(baseVertexShader, divergenceShader); +const curlProgram = new GLProgram(baseVertexShader, curlShader); +const vorticityProgram = new GLProgram(baseVertexShader, vorticityShader); +const pressureProgram = new GLProgram(baseVertexShader, pressureShader); +const gradienSubtractProgram = new GLProgram(baseVertexShader, gradientSubtractShader); + +function initFramebuffers() { + textureWidth = gl.drawingBufferWidth >> config.TEXTURE_DOWNSAMPLE; + textureHeight = gl.drawingBufferHeight >> config.TEXTURE_DOWNSAMPLE; + + const texType = ext.halfFloatTexType; + const rgba = ext.formatRGBA; + const rg = ext.formatRG; + const r = ext.formatR; + + density = createDoubleFBO(2, textureWidth, textureHeight, rgba.internalFormat, rgba.format, texType, ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST); + velocity = createDoubleFBO(0, textureWidth, textureHeight, rg.internalFormat, rg.format, texType, ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST); + divergence = createFBO(4, textureWidth, textureHeight, r.internalFormat, r.format, texType, gl.NEAREST); + curl = createFBO(5, textureWidth, textureHeight, r.internalFormat, r.format, texType, gl.NEAREST); + pressure = createDoubleFBO(6, textureWidth, textureHeight, r.internalFormat, r.format, texType, gl.NEAREST); +} + +function createFBO(texId, w, h, internalFormat, format, type, param) { + gl.activeTexture(gl.TEXTURE0 + texId); + 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); + + return [texture, fbo, texId]; +} + +function createDoubleFBO(texId, w, h, internalFormat, format, type, param) { + let fbo1 = createFBO(texId, w, h, internalFormat, format, type, param); + let fbo2 = createFBO(texId + 1, w, h, internalFormat, format, type, param); + + return { + get read() { + return fbo1; + }, + get write() { + return fbo2; + }, + swap() { + let temp = fbo1; + fbo1 = fbo2; + fbo2 = temp; + } }; + +} + +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 lastTime = Date.now(); +multipleSplats(parseInt(Math.random() * 20) + 5); +update(); + +function update() { + resizeCanvas(); + + const dt = Math.min((Date.now() - lastTime) / 1000, 0.016); + lastTime = Date.now(); + + gl.viewport(0, 0, textureWidth, textureHeight); + + if (splatStack.length > 0) + multipleSplats(splatStack.pop()); + + advectionProgram.bind(); + gl.uniform2f(advectionProgram.uniforms.texelSize, 1.0 / textureWidth, 1.0 / textureHeight); + gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read[2]); + gl.uniform1i(advectionProgram.uniforms.uSource, velocity.read[2]); + gl.uniform1f(advectionProgram.uniforms.dt, dt); + gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION); + blit(velocity.write[1]); + velocity.swap(); + + gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read[2]); + gl.uniform1i(advectionProgram.uniforms.uSource, density.read[2]); + gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION); + blit(density.write[1]); + density.swap(); + + for (let i = 0; i < pointers.length; i++) { + const pointer = pointers[i]; + if (pointer.moved) { + splat(pointer.x, pointer.y, pointer.dx, pointer.dy, pointer.color); + pointer.moved = false; + } + } + + curlProgram.bind(); + gl.uniform2f(curlProgram.uniforms.texelSize, 1.0 / textureWidth, 1.0 / textureHeight); + gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read[2]); + blit(curl[1]); + + vorticityProgram.bind(); + gl.uniform2f(vorticityProgram.uniforms.texelSize, 1.0 / textureWidth, 1.0 / textureHeight); + gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read[2]); + gl.uniform1i(vorticityProgram.uniforms.uCurl, curl[2]); + gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL); + gl.uniform1f(vorticityProgram.uniforms.dt, dt); + blit(velocity.write[1]); + velocity.swap(); + + divergenceProgram.bind(); + gl.uniform2f(divergenceProgram.uniforms.texelSize, 1.0 / textureWidth, 1.0 / textureHeight); + gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read[2]); + blit(divergence[1]); + + clearProgram.bind(); + let pressureTexId = pressure.read[2]; + gl.activeTexture(gl.TEXTURE0 + pressureTexId); + gl.bindTexture(gl.TEXTURE_2D, pressure.read[0]); + gl.uniform1i(clearProgram.uniforms.uTexture, pressureTexId); + gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE_DISSIPATION); + blit(pressure.write[1]); + pressure.swap(); + + pressureProgram.bind(); + gl.uniform2f(pressureProgram.uniforms.texelSize, 1.0 / textureWidth, 1.0 / textureHeight); + gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence[2]); + pressureTexId = pressure.read[2]; + gl.uniform1i(pressureProgram.uniforms.uPressure, pressureTexId); + gl.activeTexture(gl.TEXTURE0 + pressureTexId); + for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) { + gl.bindTexture(gl.TEXTURE_2D, pressure.read[0]); + blit(pressure.write[1]); + pressure.swap(); + } + + gradienSubtractProgram.bind(); + gl.uniform2f(gradienSubtractProgram.uniforms.texelSize, 1.0 / textureWidth, 1.0 / textureHeight); + gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressure.read[2]); + gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.read[2]); + blit(velocity.write[1]); + velocity.swap(); + + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + displayProgram.bind(); + gl.uniform1i(displayProgram.uniforms.uTexture, density.read[2]); + blit(null); + + requestAnimationFrame(update); +} + +function splat(x, y, dx, dy, color) { + splatProgram.bind(); + gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read[2]); + gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height); + gl.uniform2f(splatProgram.uniforms.point, x / canvas.width, 1.0 - y / canvas.height); + gl.uniform3f(splatProgram.uniforms.color, dx, -dy, 1.0); + gl.uniform1f(splatProgram.uniforms.radius, config.SPLAT_RADIUS); + blit(velocity.write[1]); + velocity.swap(); + + gl.uniform1i(splatProgram.uniforms.uTarget, density.read[2]); + gl.uniform3f(splatProgram.uniforms.color, color[0] * 0.3, color[1] * 0.3, color[2] * 0.3); + blit(density.write[1]); + density.swap(); +} + +function multipleSplats(amount) { + for (let i = 0; i < amount; i++) { + const color = [Math.random() * 10, Math.random() * 10, Math.random() * 10]; + 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); + } +} + +function resizeCanvas() { + if (canvas.width != canvas.clientWidth || canvas.height != canvas.clientHeight) { + canvas.width = canvas.clientWidth; + canvas.height = canvas.clientHeight; + initFramebuffers(); + } +} + +canvas.addEventListener('mousemove', e => { + pointers[0].moved = pointers[0].down; + pointers[0].dx = (e.offsetX - pointers[0].x) * 10.0; + pointers[0].dy = (e.offsetY - pointers[0].y) * 10.0; + pointers[0].x = e.offsetX; + pointers[0].y = e.offsetY; +}); + +canvas.addEventListener('touchmove', e => { + e.preventDefault(); + const touches = e.targetTouches; + for (let i = 0; i < touches.length; i++) { + let pointer = pointers[i]; + pointer.moved = pointer.down; + pointer.dx = (touches[i].pageX - pointer.x) * 10.0; + pointer.dy = (touches[i].pageY - pointer.y) * 10.0; + pointer.x = touches[i].pageX; + pointer.y = touches[i].pageY; + } +}, false); + +canvas.addEventListener('mousedown', () => { + pointers[0].down = true; + pointers[0].color = [Math.random() + 0.2, Math.random() + 0.2, Math.random() + 0.2]; +}); + +canvas.addEventListener('touchstart', e => { + e.preventDefault(); + const touches = e.targetTouches; + for (let i = 0; i < touches.length; i++) { + if (i >= pointers.length) + pointers.push(new pointerPrototype()); + + pointers[i].id = touches[i].identifier; + pointers[i].down = true; + pointers[i].x = touches[i].pageX; + pointers[i].y = touches[i].pageY; + pointers[i].color = [Math.random() + 0.2, Math.random() + 0.2, Math.random() + 0.2]; + } +}); + +window.addEventListener('mouseup', () => { + pointers[0].down = false; +}); + +window.addEventListener('touchend', e => { + const touches = e.changedTouches; + for (let i = 0; i < touches.length; i++) + for (let j = 0; j < pointers.length; j++) + if (touches[i].identifier == pointers[j].id) + pointers[j].down = false; +}); \ No newline at end of file