Wouldn't be possible without iconic WebGL Fluid Simulation by Pavel Dobryakov: https://codepen.io/PavelDoGreat/pen/zdWzEL
A Pen by Ksenia Kondrashova on CodePen.
<div class="content"> | |
<img src="https://ksenia-k.com/img/ksenia.jpg"/> | |
</div> | |
<canvas></canvas> | |
<div class="overlay"> | |
Liquid content reveal | |
</div> | |
<script type="x-shader/x-fragment" id="vertShader"> | |
precision highp float; | |
attribute vec2 aPosition; | |
varying vec2 vUv; | |
varying vec2 vL; | |
varying vec2 vR; | |
varying vec2 vT; | |
varying vec2 vB; | |
uniform vec2 u_vertex_texel; | |
void main () { | |
vUv = aPosition * .5 + .5; | |
vL = vUv - vec2(u_vertex_texel.x, 0.); | |
vR = vUv + vec2(u_vertex_texel.x, 0.); | |
vT = vUv + vec2(0., u_vertex_texel.y); | |
vB = vUv - vec2(0., u_vertex_texel.y); | |
gl_Position = vec4(aPosition, 0., 1.); | |
} | |
</script> | |
<script type="x-shader/x-fragment" id="fragShaderAdvection"> | |
precision highp float; | |
precision highp sampler2D; | |
varying vec2 vUv; | |
uniform sampler2D u_velocity_txr; | |
uniform sampler2D u_input_txr; | |
uniform vec2 u_vertex_texel; | |
uniform vec2 u_output_textel; | |
uniform float u_dt; | |
uniform float u_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 () { | |
vec2 coord = vUv - u_dt * bilerp(u_velocity_txr, vUv, u_vertex_texel).xy * u_vertex_texel; | |
gl_FragColor = u_dissipation * bilerp(u_input_txr, coord, u_output_textel); | |
gl_FragColor.a = 1.; | |
} | |
</script> | |
<script type="x-shader/x-fragment" id="fragShaderDivergence"> | |
precision highp float; | |
precision highp sampler2D; | |
varying highp vec2 vUv; | |
varying highp vec2 vL; | |
varying highp vec2 vR; | |
varying highp vec2 vT; | |
varying highp vec2 vB; | |
uniform sampler2D u_velocity_txr; | |
void main () { | |
float L = texture2D(u_velocity_txr, vL).x; | |
float R = texture2D(u_velocity_txr, vR).x; | |
float T = texture2D(u_velocity_txr, vT).y; | |
float B = texture2D(u_velocity_txr, vB).y; | |
float div = .5 * (R - L + T - B); | |
gl_FragColor = vec4(div, 0., 0., 1.); | |
} | |
</script> | |
<script type="x-shader/x-fragment" id="fragShaderPressure"> | |
precision highp float; | |
precision highp sampler2D; | |
varying highp vec2 vUv; | |
varying highp vec2 vL; | |
varying highp vec2 vR; | |
varying highp vec2 vT; | |
varying highp vec2 vB; | |
uniform sampler2D u_pressure_txr; | |
uniform sampler2D u_divergence_txr; | |
void main () { | |
float L = texture2D(u_pressure_txr, vL).x; | |
float R = texture2D(u_pressure_txr, vR).x; | |
float T = texture2D(u_pressure_txr, vT).x; | |
float B = texture2D(u_pressure_txr, vB).x; | |
float C = texture2D(u_pressure_txr, vUv).x; | |
float divergence = texture2D(u_divergence_txr, vUv).x; | |
float pressure = (L + R + B + T - divergence) * 0.25; | |
gl_FragColor = vec4(pressure, 0., 0., 1.); | |
} | |
</script> | |
<script type="x-shader/x-fragment" id="fragShaderGradientSubtract"> | |
precision highp float; | |
precision highp sampler2D; | |
varying highp vec2 vUv; | |
varying highp vec2 vL; | |
varying highp vec2 vR; | |
varying highp vec2 vT; | |
varying highp vec2 vB; | |
uniform sampler2D u_pressure_txr; | |
uniform sampler2D u_velocity_txr; | |
void main () { | |
float L = texture2D(u_pressure_txr, vL).x; | |
float R = texture2D(u_pressure_txr, vR).x; | |
float T = texture2D(u_pressure_txr, vT).x; | |
float B = texture2D(u_pressure_txr, vB).x; | |
vec2 velocity = texture2D(u_velocity_txr, vUv).xy; | |
velocity.xy -= vec2(R - L, T - B); | |
gl_FragColor = vec4(velocity, 0., 1.); | |
} | |
</script> | |
<script type="x-shader/x-fragment" id="fragShaderPoint"> | |
precision highp float; | |
precision highp sampler2D; | |
varying vec2 vUv; | |
uniform sampler2D u_input_txr; | |
uniform float u_ratio; | |
uniform vec3 u_point_value; | |
uniform vec2 u_point; | |
uniform float u_point_size; | |
void main () { | |
vec2 p = vUv - u_point.xy; | |
p.x *= u_ratio; | |
vec3 splat = pow(2., -dot(p, p) / u_point_size) * u_point_value; | |
vec3 base = texture2D(u_input_txr, vUv).xyz; | |
gl_FragColor = vec4(base + splat, 1.); | |
} | |
</script> | |
<script type="x-shader/x-fragment" id="fragShaderDisplay"> | |
precision highp float; | |
precision highp sampler2D; | |
varying vec2 vUv; | |
uniform sampler2D u_output_texture; | |
void main () { | |
vec3 C = texture2D(u_output_texture, vUv).rgb; | |
float a = max(C.r, max(C.g, C.b)); | |
a = pow(.1 * a, .1); | |
a = clamp(a, 0., 1.); | |
gl_FragColor = vec4(1. - C, 1. - a); | |
} | |
</script> |
const canvas = document.getElementsByTagName("canvas")[0]; | |
const image = document.getElementsByTagName("img")[0]; | |
canvas.width = canvas.clientWidth; | |
canvas.height = canvas.clientHeight; | |
const params = { | |
SIM_RESOLUTION: 128, | |
DYE_RESOLUTION: 1024, | |
DENSITY_DISSIPATION: .995, | |
VELOCITY_DISSIPATION: .9, | |
PRESSURE_ITERATIONS: 10, | |
SPLAT_RADIUS: 3 / window.innerHeight, | |
color: {r: .8, g: .5, b: .2} | |
}; | |
const pointer = { | |
x: .65 * window.innerWidth, | |
y: .5 * window.innerHeight, | |
dx: 0, | |
dy: 0, | |
moved: false, | |
firstMove: false // for codepen preview | |
}; | |
window.setTimeout(() => { | |
pointer.firstMove = true; | |
}, 3000); | |
let prevTimestamp = Date.now(); | |
const gl = canvas.getContext("webgl"); | |
gl.getExtension("OES_texture_float"); | |
let outputColor, velocity, divergence, pressure; | |
const vertexShader = createShader( | |
document.getElementById("vertShader").innerHTML, | |
gl.VERTEX_SHADER); | |
const splatProgram = createProgram("fragShaderPoint"); | |
const divergenceProgram = createProgram("fragShaderDivergence"); | |
const pressureProgram = createProgram("fragShaderPressure"); | |
const gradientSubtractProgram = createProgram("fragShaderGradientSubtract"); | |
const advectionProgram = createProgram("fragShaderAdvection"); | |
const displayProgram = createProgram("fragShaderDisplay"); | |
initFBOs(); | |
render(); | |
image.style.opacity = "1"; | |
window.addEventListener("resize", () => { | |
params.SPLAT_RADIUS = 5 / window.innerHeight; | |
canvas.width = canvas.clientWidth; | |
canvas.height = canvas.clientHeight; | |
}); | |
canvas.addEventListener("click", (e) => { | |
pointer.dx = 10; | |
pointer.dy = 10; | |
pointer.x = e.pageX; | |
pointer.y = e.pageY; | |
pointer.firstMove = true; | |
}); | |
canvas.addEventListener("mousemove", (e) => { | |
pointer.moved = true; | |
pointer.dx = 5 * (e.pageX - pointer.x); | |
pointer.dy = 5 * (e.pageY - pointer.y); | |
pointer.x = e.pageX; | |
pointer.y = e.pageY; | |
pointer.firstMove = true; | |
}); | |
canvas.addEventListener("touchmove", (e) => { | |
e.preventDefault(); | |
pointer.moved = true; | |
pointer.dx = 8 * (e.targetTouches[0].pageX - pointer.x); | |
pointer.dy = 8 * (e.targetTouches[0].pageY - pointer.y); | |
pointer.x = e.targetTouches[0].pageX; | |
pointer.y = e.targetTouches[0].pageY; | |
pointer.firstMove = true; | |
}); | |
function createProgram(elId) { | |
const shader = createShader( | |
document.getElementById(elId).innerHTML, | |
gl.FRAGMENT_SHADER); | |
const program = createShaderProgram(vertexShader, shader); | |
const uniforms = getUniforms(program); | |
return { | |
program, uniforms | |
} | |
} | |
function createShaderProgram(vertexShader, fragmentShader) { | |
const program = gl.createProgram(); | |
gl.attachShader(program, vertexShader); | |
gl.attachShader(program, fragmentShader); | |
gl.linkProgram(program); | |
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { | |
console.error("Unable to initialize the shader program: " + gl.getProgramInfoLog(program)); | |
return null; | |
} | |
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 createShader(sourceCode, type) { | |
const shader = gl.createShader(type); | |
gl.shaderSource(shader, sourceCode); | |
gl.compileShader(shader); | |
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { | |
console.error("An error occurred compiling the shaders: " + gl.getShaderInfoLog(shader)); | |
gl.deleteShader(shader); | |
return null; | |
} | |
return shader; | |
} | |
function blit(target) { | |
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); | |
if (target == null) { | |
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); | |
gl.bindFramebuffer(gl.FRAMEBUFFER, null); | |
} else { | |
gl.viewport(0, 0, target.width, target.height); | |
gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo); | |
} | |
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); | |
} | |
function initFBOs() { | |
const simRes = getResolution(params.SIM_RESOLUTION); | |
const dyeRes = getResolution(params.DYE_RESOLUTION); | |
outputColor = createDoubleFBO(dyeRes.width, dyeRes.height); | |
velocity = createDoubleFBO(simRes.width, simRes.height); | |
divergence = createFBO(simRes.width, simRes.height, gl.RGB); | |
pressure = createDoubleFBO(simRes.width, simRes.height, gl.RGB); | |
} | |
function getResolution(resolution) { | |
let aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight; | |
if (aspectRatio < 1) | |
aspectRatio = 1. / 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 createFBO(w, h, type = gl.RGBA) { | |
gl.activeTexture(gl.TEXTURE0); | |
const 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, type, w, h, 0, type, gl.FLOAT, null); | |
const 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 { | |
fbo, | |
width: w, | |
height: h, | |
attach(id) { | |
gl.activeTexture(gl.TEXTURE0 + id); | |
gl.bindTexture(gl.TEXTURE_2D, texture); | |
return id; | |
} | |
}; | |
} | |
function createDoubleFBO(w, h, type) { | |
let fbo1 = createFBO(w, h, type); | |
let fbo2 = createFBO(w, h, type); | |
return { | |
width: w, | |
height: h, | |
texelSizeX: 1. / w, | |
texelSizeY: 1. / h, | |
read: () => { | |
return fbo1; | |
}, | |
write: () => { | |
return fbo2; | |
}, | |
swap() { | |
let temp = fbo1; | |
fbo1 = fbo2; | |
fbo2 = temp; | |
} | |
} | |
} | |
function render() { | |
const dt = (Date.now() - prevTimestamp) / 1000; | |
prevTimestamp = Date.now(); | |
if (!pointer.firstMove) { | |
pointer.moved = true; | |
const newX = (.65 + .2 * Math.cos(.006 * prevTimestamp) * Math.sin(.008 * prevTimestamp)) * window.innerWidth; | |
const newY = (.5 + .12 * Math.sin(.01 * prevTimestamp)) * window.innerHeight; | |
pointer.dx = 10 * (newX - pointer.x); | |
pointer.dy = 10 * (newY - pointer.y); | |
pointer.x = newX; | |
pointer.y = newY; | |
} | |
if (pointer.moved) { | |
pointer.moved = false; | |
gl.useProgram(splatProgram.program); | |
gl.uniform1i(splatProgram.uniforms.u_input_txr, velocity.read().attach(0)); | |
gl.uniform1f(splatProgram.uniforms.u_ratio, canvas.width / canvas.height); | |
gl.uniform2f(splatProgram.uniforms.u_point, pointer.x / canvas.width, 1 - pointer.y / canvas.height); | |
gl.uniform3f(splatProgram.uniforms.u_point_value, pointer.dx, -pointer.dy, 1); | |
gl.uniform1f(splatProgram.uniforms.u_point_size, params.SPLAT_RADIUS); | |
blit(velocity.write()); | |
velocity.swap(); | |
gl.uniform1i(splatProgram.uniforms.u_input_txr, outputColor.read().attach(0)); | |
gl.uniform3f(splatProgram.uniforms.u_point_value, 1. - params.color.r, 1. - params.color.g, 1. - params.color.b); | |
blit(outputColor.write()); | |
outputColor.swap(); | |
} | |
gl.useProgram(divergenceProgram.program); | |
gl.uniform2f(divergenceProgram.uniforms.u_vertex_texel, velocity.texelSizeX, velocity.texelSizeY); | |
gl.uniform1i(divergenceProgram.uniforms.u_velocity_txr, velocity.read().attach(0)); | |
blit(divergence); | |
gl.useProgram(pressureProgram.program); | |
gl.uniform2f(pressureProgram.uniforms.u_vertex_texel, velocity.texelSizeX, velocity.texelSizeY); | |
gl.uniform1i(pressureProgram.uniforms.u_divergence_txr, divergence.attach(0)); | |
for (let i = 0; i < params.PRESSURE_ITERATIONS; i++) { | |
gl.uniform1i(pressureProgram.uniforms.u_pressure_txr, pressure.read().attach(1)); | |
blit(pressure.write()); | |
pressure.swap(); | |
} | |
gl.useProgram(gradientSubtractProgram.program); | |
gl.uniform2f(gradientSubtractProgram.uniforms.u_vertex_texel, velocity.texelSizeX, velocity.texelSizeY); | |
gl.uniform1i(gradientSubtractProgram.uniforms.u_pressure_txr, pressure.read().attach(0)); | |
gl.uniform1i(gradientSubtractProgram.uniforms.u_velocity_txr, velocity.read().attach(1)); | |
blit(velocity.write()); | |
velocity.swap(); | |
gl.useProgram(advectionProgram.program); | |
gl.uniform2f(advectionProgram.uniforms.u_vertex_texel, velocity.texelSizeX, velocity.texelSizeY); | |
gl.uniform2f(advectionProgram.uniforms.u_output_textel, velocity.texelSizeX, velocity.texelSizeY); | |
gl.uniform1i(advectionProgram.uniforms.u_velocity_txr, velocity.read().attach(0)); | |
gl.uniform1i(advectionProgram.uniforms.u_input_txr, velocity.read().attach(0)); | |
gl.uniform1f(advectionProgram.uniforms.u_dt, dt); | |
gl.uniform1f(advectionProgram.uniforms.u_dissipation, params.VELOCITY_DISSIPATION); | |
blit(velocity.write()); | |
velocity.swap(); | |
gl.uniform2f(advectionProgram.uniforms.u_output_textel, outputColor.texelSizeX, outputColor.texelSizeY); | |
gl.uniform1i(advectionProgram.uniforms.u_velocity_txr, velocity.read().attach(0)); | |
gl.uniform1i(advectionProgram.uniforms.u_input_txr, outputColor.read().attach(1)); | |
gl.uniform1f(advectionProgram.uniforms.u_dissipation, params.DENSITY_DISSIPATION); | |
blit(outputColor.write()); | |
outputColor.swap(); | |
gl.useProgram(displayProgram.program); | |
gl.uniform1i(displayProgram.uniforms.u_output_texture, outputColor.read().attach(0)); | |
blit(); | |
requestAnimationFrame(render); | |
} |
html, body { | |
overflow: hidden; | |
padding: 0; | |
margin: 0; | |
} | |
canvas { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
} | |
.content { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100vh; | |
} | |
img { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
height: 115%; | |
width: auto; | |
opacity: 0; | |
} | |
@media (min-aspect-ratio: 4/3) { | |
img { | |
height: auto; | |
width: 100%; | |
} | |
} | |
@media (max-aspect-ratio: 3/5) { | |
img { | |
left: 30%; | |
} | |
} | |
.overlay { | |
position: fixed; | |
top: 50%; | |
left: 30%; | |
max-width: 50%; | |
transform: translate(-50%, -50%); | |
font-family: sans-serif; | |
font-size: 10vh; | |
font-weight: bold; | |
pointer-events: none; | |
text-transform: uppercase; | |
} | |
@media (max-aspect-ratio: 1/1) { | |
.overlay { | |
left: 50%; | |
width: 90%; | |
max-width: 90%; | |
} | |
} | |
@media (max-aspect-ratio: 3/5) { | |
.overlay { | |
font-size: 5vh; | |
} | |
} |
Wouldn't be possible without iconic WebGL Fluid Simulation by Pavel Dobryakov: https://codepen.io/PavelDoGreat/pen/zdWzEL
A Pen by Ksenia Kondrashova on CodePen.