<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>HTML5 Canvas流体动力学模拟动画</title>
<style>
body {
background: black;
}
</style>
</head>
<body>
<!-- Move your mouse and light it -->
<script src='js/dat.gui.min.js'></script>
<script src='js/stats.min.js'></script>
<script>
const row = 64;
const col = 64;
const width = 8;
const height = 8;
let control = {
diff: 1,
visc: 1,
fadeOutDensityRate: 0.01,
fadeOutVelocityRate: 0.01,
drawDensity: true,
emitDensity: true,
emitDensityScale: 50,
emitDensityExpand: 0,
drawVelocity: false,
drawVelocityScale: 2,
emitVelocity: true,
emitVelocityScale: 256,
emitVelocityExpand: 5,
};
let gui = new dat.GUI();
gui.add(control, 'diff');
gui.add(control, 'visc');
gui.add(control, 'fadeOutDensityRate', 0, 1);
gui.add(control, 'fadeOutVelocityRate', 0, 1);
gui.add(control, 'drawDensity');
gui.add(control, 'emitDensity');
gui.add(control, 'emitDensityExpand', 0, 5).step(1);
gui.add(control, 'emitDensityScale');
gui.add(control, 'drawVelocityScale');
gui.add(control, 'drawVelocity');
gui.add(control, 'emitVelocity');
gui.add(control, 'emitVelocityExpand', 0, 5).step(1);
gui.add(control, 'emitVelocityScale');
class FluidDynamicSolver {
constructor(width = 32, height = 32) {
this.width = width;
this.height = height;
// expand 2 for boundaries
this.size = (this.width + 2) * (this.height + 2);
this.innerSize = this.width * this.height;
// init
this.u_prev = this.reset([]);
this.v_prev = this.reset([]);
this.d_prev = this.reset([]);
this.u_next = this.reset([]);
this.v_next = this.reset([]);
this.d_next = this.reset([]);
}
idx(x, y) {
return x + y * (this.width + 2)
}
addSource(source, target, dt) {
for (let i = 0; i < this.size; i++) {
target[i] += source[i] * dt;
}
}
diffuse(prev, next, diff, dt, boundaryFlag) {
const a = dt * diff;
// do 20 iterations of Gauss-Seidel relaxation
for (let k = 0; k < 20; k++) {
// ignore the boundaries
for (let i = 1; i <= this.width; i++) {
for (let j = 1; j <= this.height; j++) {
next[this.idx(i, j)] = (prev[this.idx(i, j)] + a * (
next[this.idx(i - 1, j)] +
next[this.idx(i + 1, j)] +
next[this.idx(i, j - 1)] +
next[this.idx(i, j + 1)])) / (1 + 4 * a);
}
}
this.setBoundary(next, boundaryFlag);
}
}
advection(prev, next, u_next, v_next, dt, boundaryFlag) {
const d = dt;
for (let i = 1; i <= this.width; i++) {
for (let j = 1; j <= this.height; j++) {
const x = Math.min(Math.max(0.5, i - d * u_next[this.idx(i, j)]), this.width + 0.5);
const y = Math.min(Math.max(0.5, j - d * v_next[this.idx(i, j)]), this.height + 0.5);
const i0 = x | 0;
const j0 = y | 0;
const i1 = i0 + 1;
const j1 = j0 + 1;
const s1 = x - i0;
const s0 = 1 - s1;
const t1 = y - j0;
const t0 = 1 - t1;
next[this.idx(i, j)] =
s0 * (t0 * prev[this.idx(i0, j0)] + t1 * prev[this.idx(i0, j1)]) +
s1 * (t0 * prev[this.idx(i1, j0)] + t1 * prev[this.idx(i1, j1)]);
}
}
this.setBoundary(next, boundaryFlag);
}
setBoundary(target, boundaryFlag) {
for (let i = 1; i <= this.width; i++) {
target[this.idx(i, 0)] = (boundaryFlag === 2 ? -1 : 1) * target[this.idx(i, 1)];
target[this.idx(i, this.height + 1)] = (boundaryFlag === 2 ? -1 : 1) * target[this.idx(i, this.height)];
}
for (let j = 1; j <= this.height; j++) {
target[this.idx(0, j)] = (boundaryFlag === 1 ? -1 : 1) * target[this.idx(1, j)];
target[this.idx(this.width + 1, j)] = (boundaryFlag === 1 ? -1 : 1) * target[this.idx(this.width, j)];
}
target[this.idx(0, 0)] = (target[this.idx(0, 1)] + target[this.idx(1, 0)]) / 2;
target[this.idx(0, this.height + 1)] = (target[this.idx(0, this.height)] + target[this.idx(1, this.height + 1)]) / 2;
target[this.idx(this.width + 1, 0)] = (target[this.idx(this.width, 0)] + target[this.idx(this.width + 1, 1)]) / 2;
target[this.idx(this.width + 1, this.height + 1)] = (target[this.idx(0, this.height)] + target[this.idx(1, this.height + 1)]) / 2;
}
stepDensity(diff, dt) {
this.addSource(this.d_prev, this.d_next, dt);
[this.d_prev, this.d_next] = [this.d_next, this.d_prev];
this.diffuse(this.d_prev, this.d_next, diff, dt, 0);
[this.d_prev, this.d_next] = [this.d_next, this.d_prev];
this.advection(this.d_prev, this.d_next, this.u_next, this.v_next, dt, 0);
}
stepVelocity(visc, dt) {
this.addSource(this.u_prev, this.u_next, dt);
this.addSource(this.v_prev, this.v_next, dt);
[this.u_prev, this.u_next] = [this.u_next, this.u_prev];
this.diffuse(this.u_prev, this.u_next, visc, dt, 1);
[this.v_prev, this.v_next] = [this.v_next, this.v_prev];
this.diffuse(this.v_prev, this.v_next, visc, dt, 2);
this.project(this.u_prev, this.v_prev, this.u_next, this.v_next);
[this.u_prev, this.u_next] = [this.u_next, this.u_prev];
[this.v_prev, this.v_next] = [this.v_next, this.v_prev];
this.advection(this.u_prev, this.u_next, this.u_prev, this.v_prev, dt, 1);
this.advection(this.v_prev, this.v_next, this.u_prev, this.v_prev, dt, 2);
this.project(this.u_prev, this.v_prev, this.u_next, this.v_next);
}
project(p, div, u, v) {
const h = 1 / this.width;
for (let i = 1; i <= this.width; i++) {
for (let j = 1; j <= this.height; j++) {
div[this.idx(i, j)] = -0.5 * h * (
u[this.idx(i + 1, j)] - u[this.idx(i - 1, j)] +
v[this.idx(i, j + 1)] - v[this.idx(i, j - 1)]);
p[this.idx(i, j)] = 0;
}
}
this.setBoundary(div, 0);
this.setBoundary(p, 0);
// do 20 iterations of Gauss-Seidel relaxation
for (let k = 0; k < 20; k++) {
// ignore the boundaries
for (let i = 1; i <= this.width; i++) {
for (let j = 1; j <= this.height; j++) {
p[this.idx(i, j)] = (div[this.idx(i, j)] + (
p[this.idx(i - 1, j)] +
p[this.idx(i + 1, j)] +
p[this.idx(i, j - 1)] +
p[this.idx(i, j + 1)])) / 4;
}
}
this.setBoundary(p, 0);
}
for (let i = 1; i <= this.width; i++) {
for (let j = 1; j <= this.height; j++) {
u[this.idx(i, j)] -= 0.5 * (p[this.idx(i + 1, j)] - p[this.idx(i - 1, j)]) / h;
v[this.idx(i, j)] -= 0.5 * (p[this.idx(i, j + 1)] - p[this.idx(i, j - 1)]) / h;
}
}
this.setBoundary(u, 1);
this.setBoundary(v, 2);
}
set(target, source) {
for (let i = 0; i < this.size; i++) {
target[i] = source[i];
}
return target;
}
reset(target, value = 0) {
for (let i = 0; i < this.size; i++) {
target[i] = value;
}
return target;
}
fadeOut(target, rate) {
for (let i = 0; i < this.size; i++) {
target[i] *= rate;
}
return target;
}
}
let fluid = new FluidDynamicSolver(col, row);
let dt = 1 / 60;
let d_ui = fluid.reset([]);
let u_ui = fluid.reset([]);
let v_ui = fluid.reset([]);
let canvas = document.createElement('canvas');
canvas.width = col * width;
canvas.height = row * height;
let lastX, lastY;
function pointerMoveHandle(x, y) {
let tileX = (x / width | 0) + 1;
let tileY = (y / height | 0) + 1;
if (lastX !== undefined) {
let deltaX = x - lastX;
let deltaY = y - lastY;
if (control.emitDensity) {
for (let i = Math.max(1, tileX - control.emi