The demo below runs a simplified D2Q9 Lattice Boltzmann simulation in your browser. Fluid enters from the left, hits a circular obstacle, and — if the Reynolds number is high enough — begins to shed alternating vortices downstream. This pattern, called a von Kármán vortex street, is one of the most recognizable signatures of fluid dynamics.
<!-- LBM flow past a cylinder: controls and canvas -->
<div class="controls">
<label>Reynolds number: <strong id="re-val">80</strong>
<input type="range" id="re-slider" min="10" max="220" value="80" step="5">
</label>
<button id="btn-reset" type="button">Reset</button>
</div>
<canvas id="canvas" width="300" height="150" title="LBM fluid simulation — color shows horizontal velocity deviation"></canvas>
<div class="legend">
<span class="leg-left">slow / reversed</span>
<div class="grad"></div>
<span class="leg-right">fast</span>
</div>
<p class="hint">Drag the slider to change the Reynolds number. At low Re the wake is symmetric; past ~80 alternating vortices shed — the von Kármán vortex street.</p>
/* Layout: controls bar + pixelated canvas + color legend */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; background: #f5f7fa; }
.controls { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
padding: .5rem .6rem; background: #fff; border-radius: 8px;
border: 1px solid #dde3ea; margin-bottom: .5rem; }
label { font-size: .88rem; display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 120px; accent-color: #1d3557; cursor: pointer; }
button { font: 600 13px system-ui; padding: .3rem .75rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 6px; cursor: pointer; }
canvas { display: block; border-radius: 6px; width: 100%; max-width: 560px; image-rendering: pixelated; }
.legend { display: flex; align-items: center; gap: .4rem; font-size: .75rem;
color: #555; margin-top: .25rem; max-width: 560px; }
.grad { flex: 1; height: 8px; border-radius: 4px;
background: linear-gradient(to right, #2166ac, #f7f7f7, #d6604d); }
.hint { font-size: .82rem; color: #555; margin: .4rem 0 0; line-height: 1.45; }
/* D2Q9 Lattice Boltzmann simulation with BGK collision and bounce-back walls */
/* Two phases each step: COLLIDE (relax f toward feq) then STREAM (propagate). */
/* --- Grid dimensions and D2Q9 velocity set --- */
const NX = 150, NY = 75;
const Q = 9;
/* D2Q9 lattice: 9 velocities (rest + 4 cardinal + 4 diagonal) */
const EX = [0, 1, 0,-1, 0, 1,-1,-1, 1];
const EY = [0, 0, 1, 0,-1, 1, 1,-1,-1];
const W = [4/9, 1/9, 1/9, 1/9, 1/9, 1/36, 1/36, 1/36, 1/36];
/* Opposite direction for bounce-back boundary: OPP[q] reverses velocity q */
const OPP = [0, 3, 4, 1, 2, 7, 8, 5, 6];
/* Cylinder obstacle position and radius */
const CX = Math.round(NX * 0.28), CY = Math.round(NY * 0.5), CR = Math.round(NY * 0.12);
const f = new Float64Array(NX * NY * Q);
const fTmp = new Float64Array(NX * NY * Q);
const isWall = new Uint8Array(NX * NY);
const rho = new Float64Array(NX * NY);
const ux = new Float64Array(NX * NY);
const uy = new Float64Array(NX * NY);
/* Linear index helpers */
const idx = (x, y, q) => (y * NX + x) * Q + q;
const cell = (x, y) => y * NX + x;
/* Mark cylinder and top/bottom channel walls */
function buildWalls() {
isWall.fill(0);
for (let y = 0; y < NY; y++) {
for (let x = 0; x < NX; x++) {
const dx = x - CX, dy = y - CY;
if (dx*dx + dy*dy <= CR*CR) isWall[cell(x,y)] = 1;
/* top and bottom walls (channel boundaries) */
if (y === 0 || y === NY-1) isWall[cell(x,y)] = 1;
}
}
}
/* Relaxation time tau controls viscosity: nu = cs^2 * (tau - 0.5) */
let tau = 0.6;
/* Maxwell-Boltzmann equilibrium distribution function */
function feq(w, rhoV, uX, uY, eX, eY) {
const eu = eX*uX + eY*uY;
const uu = uX*uX + uY*uY;
return w * rhoV * (1 + 3*eu + 4.5*eu*eu - 1.5*uu);
}
/* Initialize all nodes to uniform rightward flow with a tiny perturbation to seed asymmetry */
function init() {
const u0 = 0.08, rho0 = 1.0;
for (let y = 0; y < NY; y++) {
for (let x = 0; x < NX; x++) {
/* tiny vertical perturbation on one row to break symmetry and trigger vortex shedding */
const dU = (y === Math.round(NY/2 + 1)) ? 0.01 : 0;
for (let q = 0; q < Q; q++) {
f[idx(x,y,q)] = feq(W[q], rho0, u0 + dU, 0, EX[q], EY[q]);
}
}
}
}
/* One LBM time step: collide then stream */
function step() {
/* COLLIDE: compute macroscopic fields and relax toward equilibrium */
for (let y = 0; y < NY; y++) {
for (let x = 0; x < NX; x++) {
const c = cell(x,y);
if (isWall[c]) continue;
let r = 0, uxV = 0, uyV = 0;
for (let q = 0; q < Q; q++) { const fv = f[idx(x,y,q)]; r+=fv; uxV+=EX[q]*fv; uyV+=EY[q]*fv; }
if (r < 1e-10) { r = 1; uxV = 0.08; uyV = 0; }
uxV /= r; uyV /= r;
rho[c] = r; ux[c] = uxV; uy[c] = uyV;
for (let q = 0; q < Q; q++) {
const feqV = feq(W[q], r, uxV, uyV, EX[q], EY[q]);
fTmp[idx(x,y,q)] = f[idx(x,y,q)] + (feqV - f[idx(x,y,q)]) / tau;
}
}
}
/* STREAM: propagate post-collision distributions to neighbors */
for (let y = 0; y < NY; y++) {
for (let x = 0; x < NX; x++) {
const c = cell(x,y);
if (isWall[c]) continue;
for (let q = 0; q < Q; q++) {
let nx = x - EX[q], ny = y - EY[q];
/* clamp inlet/outlet: left edge is re-filled below, right edge copies */
if (nx < 0) nx = 0;
if (nx >= NX) nx = NX-1;
if (ny < 0 || ny >= NY) { f[idx(x,y,q)] = fTmp[idx(x,y,q)]; continue; }
const nc = cell(nx,ny);
if (isWall[nc]) {
/* bounce-back off wall: reverse direction */
f[idx(x,y,q)] = fTmp[idx(x,y,OPP[q])];
} else {
f[idx(x,y,q)] = fTmp[idx(nx,ny,q)];
}
}
}
}
/* Inlet BC: enforce uniform inflow on left column each step */
const u0 = 0.08, rho0 = 1.0;
for (let y = 1; y < NY-1; y++) {
for (let q = 0; q < Q; q++) {
f[idx(0,y,q)] = feq(W[q], rho0, u0, 0, EX[q], EY[q]);
}
}
}
/* Render fluid velocity field to canvas pixels */
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(NX, NY);
const px = imageData.data;
/* Diverging blue-white-red colormap for horizontal velocity */
function colorize(v) {
/* map velocity deviation v into [-1,1] and apply diverging palette */
const t = Math.max(-1, Math.min(1, v * 10));
let r, g, b;
if (t < 0) {
/* negative deviation -> blue (reversed flow) */
const s = -t;
r = Math.round(33 + (236-33)*s); g = Math.round(102 + (245-102)*(1-s)); b = Math.round(172 + (100-172)*(1-s));
} else {
/* positive deviation -> red (fast flow) */
r = Math.round(214 + (247-214)*(1-t)); g = Math.round(96 + (247-96)*(1-t)); b = Math.round(77 + (148-77)*(1-t));
}
return [r, g, b];
}
function draw() {
for (let y = 0; y < NY; y++) {
for (let x = 0; x < NX; x++) {
const c = cell(x,y);
const pi = (y * NX + x) * 4;
if (isWall[c]) {
/* solid dark gray for walls */
px[pi]=60; px[pi+1]=60; px[pi+2]=60; px[pi+3]=255;
} else {
const [r,g,b] = colorize(ux[c] - 0.08);
px[pi]=r; px[pi+1]=g; px[pi+2]=b; px[pi+3]=255;
}
}
}
ctx.putImageData(imageData, 0, 0);
}
/* Animation loop: run several steps per frame for speed */
let animId = null;
function loop() {
for (let i = 0; i < 6; i++) step();
draw();
animId = requestAnimationFrame(loop);
}
function resetSim() {
if (animId !== null) cancelAnimationFrame(animId);
buildWalls();
init();
animId = requestAnimationFrame(loop);
}
/* Translate Reynolds number slider into tau */
const reSlider = document.getElementById('re-slider');
const reVal = document.getElementById('re-val');
reSlider.addEventListener('input', () => {
const re = parseInt(reSlider.value, 10);
reVal.textContent = re;
/* nu = U*L/Re => tau = 3*nu + 0.5 */
const U = 0.08, L = 2 * CR;
const nu = U * L / re;
tau = Math.max(0.505, Math.min(1.8, 3*nu + 0.5));
});
document.getElementById('btn-reset').addEventListener('click', resetSim);
/* Kick off the simulation */
resetSim();
<!-- LBM flow past a cylinder: controls and canvas -->
<div class="controls">
<label>Reynolds number: <strong id="re-val">80</strong>
<input type="range" id="re-slider" min="10" max="220" value="80" step="5">
</label>
<button id="btn-reset" type="button">Reset</button>
</div>
<canvas id="canvas" width="300" height="150" title="LBM fluid simulation — color shows horizontal velocity deviation"></canvas>
<div class="legend">
<span class="leg-left">slow / reversed</span>
<div class="grad"></div>
<span class="leg-right">fast</span>
</div>
<p class="hint">Drag the slider to change the Reynolds number. At low Re the wake is symmetric; past ~80 alternating vortices shed — the von Kármán vortex street.</p>
/* Layout: controls bar + pixelated canvas + color legend */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; background: #f5f7fa; }
.controls { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
padding: .5rem .6rem; background: #fff; border-radius: 8px;
border: 1px solid #dde3ea; margin-bottom: .5rem; }
label { font-size: .88rem; display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 120px; accent-color: #1d3557; cursor: pointer; }
button { font: 600 13px system-ui; padding: .3rem .75rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 6px; cursor: pointer; }
canvas { display: block; border-radius: 6px; width: 100%; max-width: 560px; image-rendering: pixelated; }
.legend { display: flex; align-items: center; gap: .4rem; font-size: .75rem;
color: #555; margin-top: .25rem; max-width: 560px; }
.grad { flex: 1; height: 8px; border-radius: 4px;
background: linear-gradient(to right, #2166ac, #f7f7f7, #d6604d); }
.hint { font-size: .82rem; color: #555; margin: .4rem 0 0; line-height: 1.45; }
/* D2Q9 Lattice Boltzmann simulation with BGK collision and bounce-back walls */
/* Two phases each step: COLLIDE (relax f toward feq) then STREAM (propagate). */
/* --- Grid dimensions and D2Q9 velocity set --- */
const NX = 150, NY = 75;
const Q = 9;
/* D2Q9 lattice: 9 velocities (rest + 4 cardinal + 4 diagonal) */
const EX = [0, 1, 0,-1, 0, 1,-1,-1, 1];
const EY = [0, 0, 1, 0,-1, 1, 1,-1,-1];
const W = [4/9, 1/9, 1/9, 1/9, 1/9, 1/36, 1/36, 1/36, 1/36];
/* Opposite direction for bounce-back boundary: OPP[q] reverses velocity q */
const OPP = [0, 3, 4, 1, 2, 7, 8, 5, 6];
/* Cylinder obstacle position and radius */
const CX = Math.round(NX * 0.28), CY = Math.round(NY * 0.5), CR = Math.round(NY * 0.12);
const f = new Float64Array(NX * NY * Q);
const fTmp = new Float64Array(NX * NY * Q);
const isWall = new Uint8Array(NX * NY);
const rho = new Float64Array(NX * NY);
const ux = new Float64Array(NX * NY);
const uy = new Float64Array(NX * NY);
/* Linear index helpers */
const idx = (x, y, q) => (y * NX + x) * Q + q;
const cell = (x, y) => y * NX + x;
/* Mark cylinder and top/bottom channel walls */
function buildWalls() {
isWall.fill(0);
for (let y = 0; y < NY; y++) {
for (let x = 0; x < NX; x++) {
const dx = x - CX, dy = y - CY;
if (dx*dx + dy*dy <= CR*CR) isWall[cell(x,y)] = 1;
/* top and bottom walls (channel boundaries) */
if (y === 0 || y === NY-1) isWall[cell(x,y)] = 1;
}
}
}
/* Relaxation time tau controls viscosity: nu = cs^2 * (tau - 0.5) */
let tau = 0.6;
/* Maxwell-Boltzmann equilibrium distribution function */
function feq(w, rhoV, uX, uY, eX, eY) {
const eu = eX*uX + eY*uY;
const uu = uX*uX + uY*uY;
return w * rhoV * (1 + 3*eu + 4.5*eu*eu - 1.5*uu);
}
/* Initialize all nodes to uniform rightward flow with a tiny perturbation to seed asymmetry */
function init() {
const u0 = 0.08, rho0 = 1.0;
for (let y = 0; y < NY; y++) {
for (let x = 0; x < NX; x++) {
/* tiny vertical perturbation on one row to break symmetry and trigger vortex shedding */
const dU = (y === Math.round(NY/2 + 1)) ? 0.01 : 0;
for (let q = 0; q < Q; q++) {
f[idx(x,y,q)] = feq(W[q], rho0, u0 + dU, 0, EX[q], EY[q]);
}
}
}
}
/* One LBM time step: collide then stream */
function step() {
/* COLLIDE: compute macroscopic fields and relax toward equilibrium */
for (let y = 0; y < NY; y++) {
for (let x = 0; x < NX; x++) {
const c = cell(x,y);
if (isWall[c]) continue;
let r = 0, uxV = 0, uyV = 0;
for (let q = 0; q < Q; q++) { const fv = f[idx(x,y,q)]; r+=fv; uxV+=EX[q]*fv; uyV+=EY[q]*fv; }
if (r < 1e-10) { r = 1; uxV = 0.08; uyV = 0; }
uxV /= r; uyV /= r;
rho[c] = r; ux[c] = uxV; uy[c] = uyV;
for (let q = 0; q < Q; q++) {
const feqV = feq(W[q], r, uxV, uyV, EX[q], EY[q]);
fTmp[idx(x,y,q)] = f[idx(x,y,q)] + (feqV - f[idx(x,y,q)]) / tau;
}
}
}
/* STREAM: propagate post-collision distributions to neighbors */
for (let y = 0; y < NY; y++) {
for (let x = 0; x < NX; x++) {
const c = cell(x,y);
if (isWall[c]) continue;
for (let q = 0; q < Q; q++) {
let nx = x - EX[q], ny = y - EY[q];
/* clamp inlet/outlet: left edge is re-filled below, right edge copies */
if (nx < 0) nx = 0;
if (nx >= NX) nx = NX-1;
if (ny < 0 || ny >= NY) { f[idx(x,y,q)] = fTmp[idx(x,y,q)]; continue; }
const nc = cell(nx,ny);
if (isWall[nc]) {
/* bounce-back off wall: reverse direction */
f[idx(x,y,q)] = fTmp[idx(x,y,OPP[q])];
} else {
f[idx(x,y,q)] = fTmp[idx(nx,ny,q)];
}
}
}
}
/* Inlet BC: enforce uniform inflow on left column each step */
const u0 = 0.08, rho0 = 1.0;
for (let y = 1; y < NY-1; y++) {
for (let q = 0; q < Q; q++) {
f[idx(0,y,q)] = feq(W[q], rho0, u0, 0, EX[q], EY[q]);
}
}
}
/* Render fluid velocity field to canvas pixels */
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(NX, NY);
const px = imageData.data;
/* Diverging blue-white-red colormap for horizontal velocity */
function colorize(v) {
/* map velocity deviation v into [-1,1] and apply diverging palette */
const t = Math.max(-1, Math.min(1, v * 10));
let r, g, b;
if (t < 0) {
/* negative deviation -> blue (reversed flow) */
const s = -t;
r = Math.round(33 + (236-33)*s); g = Math.round(102 + (245-102)*(1-s)); b = Math.round(172 + (100-172)*(1-s));
} else {
/* positive deviation -> red (fast flow) */
r = Math.round(214 + (247-214)*(1-t)); g = Math.round(96 + (247-96)*(1-t)); b = Math.round(77 + (148-77)*(1-t));
}
return [r, g, b];
}
function draw() {
for (let y = 0; y < NY; y++) {
for (let x = 0; x < NX; x++) {
const c = cell(x,y);
const pi = (y * NX + x) * 4;
if (isWall[c]) {
/* solid dark gray for walls */
px[pi]=60; px[pi+1]=60; px[pi+2]=60; px[pi+3]=255;
} else {
const [r,g,b] = colorize(ux[c] - 0.08);
px[pi]=r; px[pi+1]=g; px[pi+2]=b; px[pi+3]=255;
}
}
}
ctx.putImageData(imageData, 0, 0);
}
/* Animation loop: run several steps per frame for speed */
let animId = null;
function loop() {
for (let i = 0; i < 6; i++) step();
draw();
animId = requestAnimationFrame(loop);
}
function resetSim() {
if (animId !== null) cancelAnimationFrame(animId);
buildWalls();
init();
animId = requestAnimationFrame(loop);
}
/* Translate Reynolds number slider into tau */
const reSlider = document.getElementById('re-slider');
const reVal = document.getElementById('re-val');
reSlider.addEventListener('input', () => {
const re = parseInt(reSlider.value, 10);
reVal.textContent = re;
/* nu = U*L/Re => tau = 3*nu + 0.5 */
const U = 0.08, L = 2 * CR;
const nu = U * L / re;
tau = Math.max(0.505, Math.min(1.8, 3*nu + 0.5));
});
document.getElementById('btn-reset').addEventListener('click', resetSim);
/* Kick off the simulation */
resetSim();
Comments
Loading comments...