<div class="canvas" name="ufo1">
<canvas id="ufo1" width="300" height="200"></canvas>
<script id="ufo1.js">
(function() {
	var img = new Image(); img.src = "material/ufo.png";

	new ufo('ufo1', img, x1, x2, false);
	new ufo('ufo2', img, y1, y2, true);
	new ufo('ufo3', img, z1, z2, false);

	function x1(t) {t/=20; t-=2; t = t*t*t - 4*t*t + 16; t*=8; return t;}
	function x2(t) {return x1(t);}
	function y1(t) {t/=25; t-=2; t = t*t*t + 5.75; t*=16; return t;}
	function y2(t) {t/=25; t-=2; t = t*t*t + 23; t*=4; return t;}
	function z1(t) {t/=20; t-=2; t = t*t*t - 2*t*t + 6; t*=8; return t;}
	function z2(t) {t/=20; t-=2; t = t*t*t - 2*t*t + 128; return t;}

function ufo(canvas_id, img, x1, x2, second) {
	var canvas = document.getElementById(canvas_id);
	var ctx    = canvas.getContext('2d');
	ctx.scale(1, -1);
	ctx.translate(0, -canvas.height);

	const jump  = 50;	// discontinuous position ( < n )
	const scale = 1;	// x-size

	// function data structure
	var n = 140;
	var f = new Array(n).fill(0);
	for (let i=0; i<n; i++)
		f[i] = (i < jump) ? x1(i) : x2(i);

	var id = 0;
	canvas.tabIndex = 0;
	canvas.onblur = function(){cancelAnimationFrame(id); id = 0;};
	canvas.onfocus = function(){if (!id) id = requestAnimationFrame(frame, canvas);};
	function frame() {id = requestAnimationFrame(frame, canvas); draw();}

	var t = -1;
	function init() {draw(); menu();};
	img.complete ? init() : img.addEventListener('load', init);

	function draw() {
		t = (t + 1) % n;
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		DrawUFO();
		DrawFunction();
	}

	function DrawUFO() {
		const offsetX = 40;
		const offsetY = 16;

		// ufo track
		ctx.save();
		ctx.translate(offsetX, offsetY);
		ctx.lineWidth = 1;
		ctx.setLineDash([5, 3]);
		ctx.beginPath();
		ctx.moveTo(0, 0);
		ctx.lineTo(0, canvas.height - offsetY * 2);
		ctx.strokeStyle = "black";
		ctx.stroke();
		ctx.restore();

		// ufo image
		ctx.save();
		ctx.translate(offsetX, offsetY);
		ctx.translate(-img.width/2, f[t] - img.height/2);
		ctx.rotate((Math.abs(t % 20 - 10) - 5) * Math.PI / 180);
		ctx.drawImage(img, 0, 0);
		ctx.restore();
		if (second && t >= jump) {
			ctx.save();
			ctx.translate(offsetX, offsetY);
			ctx.translate(-img.width/2, 184 -f[t] - img.height/2);
			ctx.rotate((Math.abs(t % 20 - 10) - 5) * Math.PI / 180);
			ctx.drawImage(img, 0, 0);
			ctx.restore();
		}
	}

	function DrawFunction() {
		const offsetX = 120;
		const offsetY = 16;

		// function axis
		ctx.save();
		ctx.translate(offsetX, offsetY);
		DrawXY(150, 150);

		// function curve
		ctx.lineWidth = 2;
		ctx.beginPath();
		ctx.moveTo(0, f[0]);
		for (let i=0; i<n; ++i) {
			if (i == jump) ctx.moveTo((i+1) * scale, f[i]);	// discontinuous
			ctx.lineTo(i * scale, f[i]);
		}
		if (second) {
			ctx.moveTo(jump-1, f[jump-1]);
			for (let i=jump; i<n; ++i)
				ctx.lineTo(i * scale, 184 -f[i]);
		}
		ctx.strokeStyle = "rgb(198,24,24)";
		ctx.stroke();

		// function bar
		ctx.beginPath();
		ctx.moveTo(t * scale, 0);
		ctx.lineTo(t * scale, f[t]);
		ctx.strokeStyle = "rgb(255,192,0)";
		ctx.stroke();

		// function bullet
		ctx.beginPath();
		ctx.arc(t * scale, f[t], 4, 0, 2 * Math.PI);
		ctx.fillStyle = "black";
		ctx.fill();
		if (second && t >= jump) {
			ctx.beginPath();
			ctx.arc(t * scale, 184 -f[t], 4, 0, 2 * Math.PI);
			ctx.fillStyle = "black";
			ctx.fill();
		}
		ctx.restore();
	}

	function DrawArrow(x1, y1, x2, y2, len, angle, gap) {
		var slope = Math.atan2(y2 - y1, x2 - x1);
		x1 += gap * Math.cos(slope);
		y1 += gap * Math.sin(slope);
		x2 -= gap * Math.cos(slope);
		y2 -= gap * Math.sin(slope);
	
		ctx.beginPath();
		ctx.moveTo(x1, y1);
		ctx.lineTo(x2, y2);
		ctx.lineTo(x2 - len * Math.cos(slope-angle), y2 - len * Math.sin(slope-angle));
		ctx.moveTo(x2, y2);
		ctx.lineTo(x2 - len * Math.cos(slope+angle), y2 - len * Math.sin(slope+angle));
		ctx.stroke();
	}

	function DrawXY(scalarX, scalarY) {
		ctx.lineWidth = 1;
		ctx.strokeStyle = "gray";
		DrawArrow(-16, 0, +scalarX, 0, 8, Math.PI / 5, 0);
		DrawArrow(0, -16, 0, +scalarY, 8, Math.PI / 5, 0);

		ctx.scale(1, -1);
		ctx.font = "16pt Arial";
		ctx.textAlign = "center";
		ctx.textBaseline = "middle";
		ctx.fillStyle = "black";
		ctx.fillText("t", +scalarX+8, 0);
		ctx.textBaseline = "bottom";
		ctx.fillText("x(t)", 0, -scalarY-8);
		ctx.scale(1, -1);
	}

	function menu() {
		ctx.save();
		ctx.scale(1, -1);
		ctx.translate(0, -canvas.height);
		ctx.font = "32pt Arial";
		ctx.textBaseline = "middle";
		ctx.textAlign = "center";
		ctx.fillStyle = "rgb(0,127,0)";
		ctx.fillText("Click Me !", canvas.width/2, canvas.height/2);
		ctx.restore();
	}
}
})();
</script>
</div><div class="canvas" name="ufo4">
<canvas id="ufo4" width="500" height="200"></canvas>
<script id="ufo4.js">
(function() {
	var img = new Image(); img.src = "material/ufo.png";

	new ufo('ufo4', img, x1, v1);
	new ufo('ufo5', img, x2, v2);

	// 20 is a magic number that depends on Δt
	// 1.5 is a magic number that depends on Δt/fps/dpi
	function x1(t) {t/=20; t-=2; return 40 * t + 20;}
	function v1(t) {t/=20; t-=2; return 40 / 1.5;}
	function x2(t) {t/=20; t-=2; t = t*t*t - 4*t*t + 24; t*=3; return t;}
	function v2(t) {t/=20; t-=2; t = 3*t*t - 8*t; t*=3; return t / 1.5;}
//	function x3(t) {t = -6*Math.cos(t/6)-7*Math.cos(t/7)+10; t*=6; return t;}
//	function v3(t) {t = Math.sin(t/6)+Math.sin(t/7); t*=6; return t;}

function ufo(canvas_id, img, x, v) {
	var canvas = document.getElementById(canvas_id);
	var ctx    = canvas.getContext('2d');
	ctx.scale(1, -1);
	ctx.translate(0, -canvas.height);

	const scale = 1;	// x-size

	// function data structure
	var n = 140;
	var f = new Array(n).fill(0);
	var g = new Array(n).fill(0);
	for (let i=0; i<n; i++) f[i] = x(i);
	for (let i=0; i<n; i++) g[i] = v(i);

	var id = 0;
	canvas.tabIndex = 0;
	canvas.onblur = function(){cancelAnimationFrame(id); id = 0;};
	canvas.onfocus = function(){if (!id) id = requestAnimationFrame(frame, canvas);};
	function frame() {id = requestAnimationFrame(frame, canvas); draw();}

	var t = -1;
	function init() {draw(); menu();};
	img.complete ? init() : img.addEventListener('load', init);

	function draw() {
		t = (t + 1) % n;
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		DrawUFO();
		DrawFunction();
		DrawDerivative();
	}

	function DrawUFO() {
		const offsetX = 40;
		const offsetY = 16;

		// ufo track
		ctx.save();
		ctx.translate(offsetX, offsetY);
		ctx.lineWidth = 1;
		ctx.setLineDash([5, 3]);
		ctx.beginPath();
		ctx.moveTo(0, 0);
		ctx.lineTo(0, canvas.height - offsetY * 2);
		ctx.strokeStyle = "black";
		ctx.stroke();
		ctx.restore();

		// ufo image
		ctx.save();
		ctx.translate(offsetX, offsetY);
		ctx.translate(-img.width/2, f[t] - img.height/2);
		ctx.rotate((Math.abs(t % 20 - 10) - 5) * Math.PI / 180);
		ctx.drawImage(img, 0, 0);
		ctx.restore();
	}

	function DrawFunction() {
		const offsetX = 120;
		const offsetY = 16;

		// function axis
		ctx.save();
		ctx.translate(offsetX, offsetY);
		DrawXY(150, 150, "t", "x(t)");

		// function curve
		ctx.lineWidth = 2;
		ctx.beginPath();
		ctx.moveTo(0, f[0]);
		for (let i=0; i<n; ++i)
			ctx.lineTo(i * scale, f[i]);
		ctx.strokeStyle = "rgb(198,24,24)";
		ctx.stroke();

		// function bar
		ctx.beginPath();
		ctx.moveTo(t * scale, 0);
		ctx.lineTo(t * scale, f[t]);
		ctx.strokeStyle = "rgb(255,192,0)";
		ctx.stroke();

		// function bullet
		ctx.beginPath();
		ctx.arc(t * scale, f[t], 4, 0, 2 * Math.PI);
		ctx.fillStyle = "black";
		ctx.fill();
		ctx.restore();
	}

	function DrawDerivative() {
		const offsetX = 320;
		const offsetY = 16;

		// function axis
		ctx.save();
		ctx.translate(offsetX, offsetY);
		DrawXY(150, 150, "t", "d/dt x(t)");

		// function curve
		ctx.lineWidth = 2;
		ctx.beginPath();
		ctx.moveTo(0, g[0]);
		for (let i=0; i<n; ++i)
			ctx.lineTo(i * scale, g[i]);
		ctx.strokeStyle = "rgb(149,55,53)";
		ctx.stroke();

		// function bar
		ctx.beginPath();
		ctx.moveTo(t * scale, 0);
		ctx.lineTo(t * scale, g[t]);
		ctx.strokeStyle = "rgb(255,192,0)";
		ctx.stroke();

		// function bullet
		ctx.beginPath();
		ctx.arc(t * scale, g[t], 4, 0, 2 * Math.PI);
		ctx.fillStyle = "black";
		ctx.fill();
		ctx.restore();
	}

	function DrawArrow(x1, y1, x2, y2, len, angle, gap) {
		var slope = Math.atan2(y2 - y1, x2 - x1);
		x1 += gap * Math.cos(slope);
		y1 += gap * Math.sin(slope);
		x2 -= gap * Math.cos(slope);
		y2 -= gap * Math.sin(slope);
	
		ctx.beginPath();
		ctx.moveTo(x1, y1);
		ctx.lineTo(x2, y2);
		ctx.lineTo(x2 - len * Math.cos(slope-angle), y2 - len * Math.sin(slope-angle));
		ctx.moveTo(x2, y2);
		ctx.lineTo(x2 - len * Math.cos(slope+angle), y2 - len * Math.sin(slope+angle));
		ctx.stroke();
	}

	function DrawXY(scalarX, scalarY, labelX, labelY) {
		ctx.lineWidth = 1;
		ctx.strokeStyle = "gray";
		DrawArrow(-16, 0, +scalarX, 0, 8, Math.PI / 5, 0);
		DrawArrow(0, -16, 0, +scalarY, 8, Math.PI / 5, 0);

		ctx.scale(1, -1);
		ctx.font = "16pt Arial";
		ctx.textAlign = "center";
		ctx.textBaseline = "middle";
		ctx.fillStyle = "black";
		ctx.fillText(labelX, +scalarX+8, 0);
		ctx.textBaseline = "bottom";
		ctx.fillText(labelY, 0, -scalarY-8);
		ctx.scale(1, -1);
	}

	function menu() {
		ctx.save();
		ctx.scale(1, -1);
		ctx.translate(0, -canvas.height);
		ctx.font = "32pt Arial";
		ctx.textBaseline = "middle";
		ctx.textAlign = "center";
		ctx.fillStyle = "rgb(0,127,0)";
		ctx.fillText("Click Me !", canvas.width/2, canvas.height/2);
		ctx.restore();
	}
}
})();
</script>
</div><div class="canvas" name="PDE1">
<canvas id="PDE1" width="600" height="150"></canvas>
<input type="range" id="PDE1_input">
<style>
input[type="range"]:hover {opacity: 1;}
input[type="range"] {width: 600; cursor: pointer;}
</style>
<script id="PDE1.js">
(function() {
	// 1D wave equation
	// d²              d²
	// ——— f(t,x) = v² ——— f(t,x)
	// dt²             dx²

	// parameters in differential equation
	const v = 1;			// wave speed

	// parameters in numerical simulation
	const rt = [0, 100];	// time range
	const Δt = .01;			// time step
	const nt = Math.floor((rt[1] - rt[0]) / Δt) + 1;

	const rx = [0, 30];		// space range
	const Δx = .5;			// space gap
	const nx = Math.floor((rx[1] - rx[0]) / Δx) + 1;

	// numerical simulation
	var f = new Float64Array2D(nt+1, nx+2);

	function Float64Array2D(m, n) {
		var array = new Array(m);
		for (let i=0; i<m; ++i)
			array[i] = new Float64Array(n);
		return array;
	}

	function initial_condition() {
		for (let n=0; n<=1; n++) {
			// user-defined initial condition
			for (let i=1; i<=nx; i++) {
				f[n][i] = .3 + Math.abs(nx/2 - nx/7 - i) / nx;
			}

			// symmetric boundary condition
			f[n][0]    = f[n][2];
			f[n][nx+1] = f[n][nx-1];
		}
	}

	function iteration() {
		for (let n=2; n<=nt; n++) {
			for (let i=1; i<=nx; i++) {
				// explicit Euler method
				var ddx = f[n-1][i+1] + f[n-1][i-1] - f[n-1][i]*2;
				var C = v * Δt * Δt / Δx / Δx;
				f[n][i] = f[n-1][i]*2 - f[n-2][i] + C * ddx;
			}

			// symmetric boundary condition
			f[n][0]    = f[n][2];
			f[n][nx+1] = f[n][nx-1];
		}
	}

	function simulation() {
		initial_condition();
		iteration();
	}

	simulation();

	var canvas  = document.getElementById('PDE1');
	var ctx     = canvas.getContext('2d');

	const offset = [20, 20];
	function draw() {
		ctx.clearRect(0, 0, canvas.width, canvas.height);

		ctx.save();
		ctx.scale(1, -1);
		ctx.translate(0, -canvas.height);
		ctx.translate(offset[0], offset[1]);
		draw_solution();
		draw_axis(canvas.width - offset[0] * 2, 100);
		ctx.restore();

		draw_text();
	}

	function draw_solution() {
		const x_scale = (canvas.width - offset[0] * 2) / nx;
		const y_scale = 100;

		// curve
		ctx.beginPath();
		ctx.moveTo(0, 0);
		for (let i=1; i<=nx; ++i) {
			var x = (i-1) * x_scale;
			var y = f[tick][i] * y_scale;
			ctx.lineTo(x, y);
		}
		ctx.lineTo(x, 0);
		ctx.lineTo(0, 0);
		ctx.lineWidth = 2;
		ctx.setLineDash([]);
		ctx.strokeStyle = 'rgb(24,24,198)';
		ctx.stroke();
		ctx.fillStyle = 'rgba(198,217,241,0.8)';
		ctx.fill();

		// dots
		ctx.beginPath();
		for (let i=1; i<=nx; ++i) {
			var x = (i-1) * x_scale;
			var y = f[tick][i] * y_scale;
			ctx.moveTo(x, y);
			ctx.arc(x, y, 2, 0, 2 * Math.PI, false);
		}
		ctx.fillStyle = 'rgb(24,24,198)';
		ctx.fill();
	}

	function DrawArrow(x1, y1, x2, y2, len, angle, gap) {
		var slope = Math.atan2(y2 - y1, x2 - x1);
		x1 += gap * Math.cos(slope);
		y1 += gap * Math.sin(slope);
		x2 -= gap * Math.cos(slope);
		y2 -= gap * Math.sin(slope);
	
		ctx.beginPath();
		ctx.moveTo(x1, y1);
		ctx.lineTo(x2, y2);
		ctx.lineTo(x2 - len * Math.cos(slope-angle), y2 - len * Math.sin(slope-angle));
		ctx.moveTo(x2, y2);
		ctx.lineTo(x2 - len * Math.cos(slope+angle), y2 - len * Math.sin(slope+angle));
		ctx.stroke();
	}

	function draw_axis(scalarX, scalarY) {
		ctx.lineWidth = 1;
		ctx.strokeStyle = "gray";
		DrawArrow(-16, 0, +scalarX, 0, 8, Math.PI / 5, 0);
		DrawArrow(0, -16, 0, +scalarY, 8, Math.PI / 5, 0);

		ctx.scale(1, -1);
		ctx.fillStyle = "gray";
		ctx.font = "16pt Arial";
		ctx.textAlign = "center";
		ctx.textBaseline = "middle";
		ctx.fillText("x", +scalarX+8, 0);
		ctx.textBaseline = "bottom";
		ctx.fillText("f(t,x)", 8/2, -scalarY-8);
		ctx.scale(1, -1);
	}

	function draw_text() {
		ctx.font = "16pt Arial";
		ctx.textAlign = "right";
		ctx.textBaseline = "top";
		var t = (rt[0] + (tick-1) * Δt).toFixed(2);
		ctx.fillStyle = "rgb(127,127,127)";
		ctx.fillText("t = " + t, canvas.width - offset[0] * 1.4, 0);
	}

	var tick = 1;	// current frame number
	var tick_input = document.querySelector("#PDE1_input");

	tick_init();
	function tick_init() {
		tick_input.min = 1;
		tick_input.max = nt;
		tick_input.valueAsNumber = tick;
	}

	tick_input.oninput = function(e) {
		tick = this.valueAsNumber;
		requestAnimationFrame(draw, canvas);
	}

	draw();
})();
</script>
</div><div class="canvas" name="characteristics2D">
<canvas id="characteristics1" width="600" height="150"></canvas>
<input type="range" id="characteristics1_input">
<style>
input[type="range"]:hover {opacity: 1;}
input[type="range"] {width: 600; cursor: pointer;}
</style>
<script id="characteristics2D.js">
(function() {
//	new Plot('characteristics1', 'vector field v(t,x)');
	new Plot('characteristics1', 'constant c');
	new Plot('characteristics2', 'constant c');
	new Plot('characteristics3', 'conservation law a(f(t,x))');

function Plot(canvas_id, velocity) {
	// advection equation
	// d                  d
	// —— f(t,x) + v(t,x) —— f(t,x) = 0
	// dt                 dx

	// conservaltion law
	// d                     d
	// —— f(t,x) + a(f(t,x)) —— f(t,x) = 0
	// dt                    dx

	// initial condition
	function f0(x) {
		const x0 = 8;	// position of incident wave
		return Math.cosh((x - x0) / 4) ** -2;
	}

	// velocity
	function v(t,x) {
		if (velocity === 'constant c')
			return c = 1;
		else if (velocity === 'vector field v(t,x)')
			return t/5 * Math.exp(-t/5);
		else if (velocity === 'conservation law a(f(t,x))') {
			function a(x) {return x;}
			return a(f0(x));
		}
	}

	// parameters in numerical simulation
	const rt = [0, 20];		// time range
	const Δt = .01;			// time step
	const nt = Math.floor((rt[1] - rt[0]) / Δt);

	const rx = [0, 30];		// space range
	const Δx = 1;			// space gap
	const nx = Math.floor((rx[1] - rx[0]) / Δx);

	var canvas  = document.getElementById(canvas_id);
	var ctx     = canvas.getContext('2d');

	const offset = [20, 20];
	function draw() {
		ctx.clearRect(0, 0, canvas.width, canvas.height);

		ctx.save();
		ctx.scale(1, -1);
		ctx.translate(0, -canvas.height);
		ctx.translate(offset[0], offset[1]);
		draw_solution();
		draw_axis(canvas.width - offset[0] * 3, 100);
		ctx.restore();

		draw_text();
	}

	function draw_solution() {
		const x_scale = (canvas.width - offset[0] * 3) / (rx[1] - rx[0]);
		const t_scale = 5;

		// curve
		ctx.beginPath();
		var px = 0;
		for (let i=-1; i<nx; ++i) {
			for (let n=0; n<=tick; ++n) {
				var x = rx[0] + i * Δx;
				var t = rt[0] + n * Δt;
				if (n === 0) {
					px = x;
					ctx.moveTo(px * x_scale, t * t_scale);
				} else {
//					var _i = Math.floor((px - rx[0]) / Δx);
//					var dfノdx = (f[n][_i+1] - f[n][_i]) / Δx;
					px += v(t,x) * Δt;
					ctx.lineTo(px * x_scale, t * t_scale);
				}
			}
		}
		ctx.lineWidth = 2;
		ctx.strokeStyle = 'rgb(24,24,198)';
		ctx.stroke();
	}

	function DrawArrow(x1, y1, x2, y2, len, angle, gap) {
		var slope = Math.atan2(y2 - y1, x2 - x1);
		x1 += gap * Math.cos(slope);
		y1 += gap * Math.sin(slope);
		x2 -= gap * Math.cos(slope);
		y2 -= gap * Math.sin(slope);
	
		ctx.beginPath();
		ctx.moveTo(x1, y1);
		ctx.lineTo(x2, y2);
		ctx.lineTo(x2 - len * Math.cos(slope-angle), y2 - len * Math.sin(slope-angle));
		ctx.moveTo(x2, y2);
		ctx.lineTo(x2 - len * Math.cos(slope+angle), y2 - len * Math.sin(slope+angle));
		ctx.stroke();
	}

	function draw_axis(scalarX, scalarY) {
		ctx.lineWidth = 1;
		ctx.strokeStyle = "gray";
		DrawArrow(-16, 0, +scalarX, 0, 8, Math.PI / 5, 0);
		DrawArrow(0, -16, 0, +scalarY, 8, Math.PI / 5, 0);

		ctx.scale(1, -1);
		ctx.fillStyle = "gray";
		ctx.font = "16pt Arial";
		ctx.textAlign = "center";
		ctx.textBaseline = "middle";
		ctx.fillText("x(t)", +scalarX+8*2, 0);
		ctx.textBaseline = "bottom";
		ctx.fillText("t", 0, -scalarY-8/2);
		ctx.scale(1, -1);
	}

	function draw_text() {
		ctx.font = "16pt Arial";
		ctx.textAlign = "right";
		ctx.textBaseline = "top";
		var t = (rt[0] + tick * Δt).toFixed(2);
		ctx.fillStyle = "rgb(127,127,127)";
		ctx.fillText("t = " + t, canvas.width - offset[0] * 1.4, 0);
	}

	var tick = 0;	// current frame number
	var tick_input = document.querySelector(`#${canvas_id}_input`);

	tick_init();
	function tick_init() {
		tick_input.min = 0;
		tick_input.max = nt;
		tick_input.valueAsNumber = tick;
	}

	tick_input.oninput = function(e) {
		tick = this.valueAsNumber;
		requestAnimationFrame(draw, canvas);
	}

	draw();
}
})();
</script>
</div><div class="canvas" name="conservation_law">
<canvas id="conservation_law_1" width="600" height="150"></canvas>
<input type="range" id="conservation_law_1_input">
<style>
input[type="range"]:hover {opacity: 1;}
input[type="range"] {width: 600px; cursor: pointer;}
</style>
<script id="conservation_law.js">
(function() {
	new Plot('conservation_law_1', u0, fʹ_constant);
	new Plot('conservation_law_2', u0, fʹ_Burgers);

	// constant-coefficient advection equation
	// d             d
	// —— u(t,x) + k —— u(t,x) = 0
	// dt            dx

	// initial condition
	function u0(x) {
		const x0 = 8;	// position of incident wave
		return Math.cosh((x - x0) / 4) ** -2;
	}

	// wave speed
	function fʹ_constant(x) {
		const k = 1;
		return k;
	}

	// inviscid Burgers' equation
	// d                  d
	// —— u(t,x) + u(t,x) —— u(t,x) = 0
	// dt                 dx

	// wave speed
	function fʹ_Burgers(x) {
		return x;
	}

function Plot(canvas_id, u0, fʹ) {
	// parameters in numerical simulation
	var rt = [0, 20];	// time range
	var Δt = 0.01;		// time step
	var nt = Math.floor((rt[1] - rt[0]) / Δt);

	var rx = [0, 30];	// space range
	var Δx = .5;		// space gap
	var nx = Math.floor((rx[1] - rx[0]) / Δx);

	var canvas  = document.getElementById(canvas_id);
	var ctx     = canvas.getContext('2d');

	const offset = [0, 20];
	function draw() {
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		ctx.save();
		ctx.scale(1, -1);
		ctx.translate(0, -canvas.height);
		ctx.translate(offset[0], offset[1]);
		draw_solution();
		ctx.restore();
	}

	function draw_solution() {
		const x_scale = canvas.width / (rx[1] - rx[0]);
		const y_scale = 100;

		// function curve
		ctx.beginPath();
		ctx.moveTo(rx[0] * x_scale, u0(rx[0]) * y_scale);
		for (let i=-nx; i<=nx; ++i) {
//			var t = rt[0] + tick * Δt;
			var x = rx[0] + i * Δx;
			var px = x + fʹ(u0(x)) * tick * Δt;
			var py = u0(x);
			ctx.lineTo(px * x_scale, py * y_scale);
		}
		ctx.lineTo(canvas.width, -canvas.height);
		ctx.lineTo(0, -canvas.height);
		ctx.lineWidth = 2;
		ctx.setLineDash([]);
		ctx.strokeStyle = 'rgb(24,24,198)';
		ctx.stroke();
		ctx.fillStyle = 'rgba(198,217,241,0.8)';
		ctx.fill();

		// dots
		ctx.beginPath();
		for (let i=-nx; i<=nx; ++i) {
			var x = rx[0] + i * Δx;
			var px = x + fʹ(u0(x)) * tick * Δt;
			var py = u0(x);
			ctx.moveTo(px * x_scale, py * y_scale);
			ctx.arc(px * x_scale, py * y_scale, 2, 0, 2 * Math.PI, false);
		}
		ctx.fillStyle = 'rgb(24,24,198)';
		ctx.fill();
	}

	var tick = 0;	// current frame number
	var tick_input = document.querySelector(`#${canvas_id}_input`);

	tick_init();
	function tick_init() {
		tick_input.min = 0;
		tick_input.max = nt;
		tick_input.valueAsNumber = tick;
	}

	tick_input.oninput = function(e) {
		tick = this.valueAsNumber;
		requestAnimationFrame(draw, canvas);
	}

	draw();
}
})();
</script>
</div><div class="canvas" name="curves">
<canvas id="curves" width="300" height="300"></canvas>
<script>
(function(){
	var canvas = document.getElementById("curves");
	var ctx = canvas.getContext("2d");
	ctx.lineWidth = 2;

	var id;
	canvas.tabIndex = 1;
	canvas.style.position = "relative";
	canvas.onmouseover = canvas.focus;
	canvas.onmouseout = canvas.blur;
	canvas.onmousemove = onMouseMove;
	canvas.onmousedown = onMouseDown;
	canvas.onblur = function(){cancelAnimationFrame(id); id = 0;};
	canvas.onfocus = function(){if(!id)id = requestAnimationFrame(update, canvas);};
	function update() {id = requestAnimationFrame(update,canvas); draw();}

	var angle_x = 0;
	var angle_y = 0;
	function onMouseMove(event) {
		var x = event.layerX;
		var y = event.layerY;
		if (x>0 && x<canvas.width && y>0 && y<canvas.height) {
			angle_x = (x-canvas.width/2)/(canvas.width/2)*1.7;
			angle_y = (y-canvas.height)/(canvas.height)*1.7;
		}
	}

	var display_mode = 0;
	function onMouseDown(event) {
		display_mode = (display_mode + 1) % 2;
	};

	var plot_center = [-100,50,0];
	var screen_center = [canvas.width/2, canvas.height/2, 0];
	var focal_length = 9990;
	var distance = 10000;
	var scalar = 10;

	// inviscid Burgers' equation
	// d                  d
	// —— u(t,x) + u(t,x) —— u(t,x) = 0
	// dt                 dx

	// wave speed
	function fʹ(x) {
		return x;
	}

	// initial condition
	function u0(x) {
		const x0 = 8;	// position of incident wave
		return Math.cosh((x - x0) / 4) ** -2;
	}

	draw();
	function draw() {
		var c = Math.cos(angle_x);
		var s = Math.sin(angle_x);
		var rotx = [c,0,-s,0,1,0,s,0,c];
		var c = Math.cos(-angle_y);
		var s = Math.sin(-angle_y);
		var roty = [1,0,0,0,c,s,0,-s,c];

		function transform(p) {
			scale(p, [scalar, -scalar, -scalar]);
			var q = mul(roty, mul(rotx, p));
			translate(q, [0,0,distance]);
			project(q, focal_length);
			translate(q, screen_center);
			translate(q, plot_center);
			return q;
		}

		// parameters in numerical simulation
		var rt = [0, 10];	// time range
		var Δt = 1;			// time step
		var nt = Math.floor((rt[1] - rt[0]) / Δt);

		var rx = [0, 20];	// space range
		var Δx = .5;		// space gap
		var nx = Math.floor((rx[1] - rx[0]) / Δx);

		// function curves (waveforms)
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		ctx.beginPath();
		for (let i=0; i<=nt; ++i)
		for (let j=0; j<=nx; ++j) {
			var t = rt[0] + i * Δt;
			var x = rx[0] + j * Δx;
			var px = x + fʹ(u0(x)) * t;
			var py = u0(x);
			var p = transform([px, py*10, i]);
			if (j == 0) ctx.moveTo(p[0], p[1]);
			else        ctx.lineTo(p[0], p[1]);
		}
		var alpha = (display_mode == 0 ? 1 : .1);
		ctx.strokeStyle = `rgba(198,24,24,${alpha})`;
		ctx.stroke();

		// characteristic curves
		ctx.beginPath();
		for (let j=0; j<=nx; j+=2)
		for (let i=0; i<=nt; ++i) {
			var t = rt[0] + i * Δt;
			var x = rx[0] + j * Δx;
			var px = x + fʹ(u0(x)) * t;
			var py = u0(x);
			var p = transform([px, py*10, i]);
			if (i == 0) ctx.moveTo(p[0], p[1]);
			else        ctx.lineTo(p[0], p[1]);
		}
		var alpha = (display_mode == 1 ? 1 : .1);
		ctx.strokeStyle = `rgba(24,198,24,${alpha})`;
		ctx.stroke();

		// axis
		ctx.beginPath();
		var p = transform([rx[0], rt[0], 0]);
		ctx.moveTo(p[0], p[1]);
		var p = transform([rx[1], rt[0], 0]);
		ctx.lineTo(p[0], p[1]);
		var p = transform([rx[0], rt[0], 0]);
		ctx.moveTo(p[0], p[1]);
		var p = transform([rx[0], rt[1], 0]);
		ctx.lineTo(p[0], p[1]);
		var p = transform([rx[0], rt[0], 0]);
		ctx.moveTo(p[0], p[1]);
		var p = transform([rx[0], rt[0], nt]);
		ctx.lineTo(p[0], p[1]);
		ctx.strokeStyle = "black";
		ctx.stroke();

		// axis label
		ctx.font = "16pt Arial";
		ctx.fillStyle = "black";
		var p = transform([rx[1], rt[0], 0]);
		ctx.textAlign = "left";
		ctx.textBaseline = "middle";
		ctx.fillText(" x ", p[0], p[1]);
		var p = transform([rx[0], rt[1], 0]);
		ctx.textAlign = "center";
		ctx.textBaseline = "bottom";
		ctx.fillText(" u(t,x) ", p[0], p[1]);
		var p = transform([rx[0], rt[0], nt]);
		ctx.textAlign = "center";
		ctx.textBaseline = "bottom";
		ctx.fillText(" t ", p[0], p[1]);
	}

	function translate(p, v) {
		p[0] += v[0];
		p[1] += v[1];
		p[2] += v[2];
	}

	function scale(p, v) {
		p[0] *= v[0];
		p[1] *= v[1];
		p[2] *= v[2];
	}

	function mul(m, p) {
		var q = new Array(3);
		q[0] = m[0] * p[0] + m[1] * p[1] + m[2] * p[2];
		q[1] = m[3] * p[0] + m[4] * p[1] + m[5] * p[2];
		q[2] = m[6] * p[0] + m[7] * p[1] + m[8] * p[2];
		return q;
	}

	function project(p, focal_length) {
		p[0] = p[0] * focal_length / p[2];
		p[1] = p[1] * focal_length / p[2];
		p[2] = focal_length;
	}
})();
</script>
</div><div class="canvas" name="shock">
<canvas id="shock" width="600" height="150"></canvas>
<input type="range" id="shock_input">
<canvas id="rarefaction" width="600" height="150"></canvas>
<input type="range" id="rarefaction_input">
<style>
input[type="range"]:hover {opacity: 1;}
input[type="range"] {width: 600px; cursor: pointer;}
</style>
<script id="shock.js">
(function() {
	new Plot('shock',       uL = 0.9, uR = 0.2, p0 = 5);
	new Plot('rarefaction', uL = 0.2, uR = 0.9, p0 = 5);

	// inviscid Burgers' equation
	// d                  d
	// —— u(t,x) + u(t,x) —— u(t,x) = 0
	// dt                 dx

	// wave speed
	function a(u) {return u;}
	// flux
	function f(u) {return 0.5 * u * u;}
	// inverse of a
	function inv_a(u) {return u;}

function Plot(canvas_id, uL, uR, p0) {
	var canvas  = document.getElementById(canvas_id);
	var ctx     = canvas.getContext('2d');

	// parameters in numerical simulation
	var rt = [0, 20];	// time range
	var Δt = 0.01;		// time step
	var nt = Math.floor((rt[1] - rt[0]) / Δt);

	var rx = [0, 30];	// space range
	var Δx = .5;		// space gap
	var nx = Math.floor((rx[1] - rx[0]) / Δx);

	const offset = [0, 20];
	function draw() {
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		ctx.save();
		ctx.scale(1, -1);
		ctx.translate(0, -canvas.height);
		ctx.translate(offset[0], offset[1]);
		draw_solution();
		ctx.restore();
	}

	function draw_solution() {
		const x_scale = canvas.width / (rx[1] - rx[0]);
		const y_scale = 100;

		// function curve
		ctx.beginPath();
		if (uL > uR) {
			// shock
			var s = (f(uR) - f(uL)) / (uR - uL);
			var pt = p0 + s * tick * Δt;
			ctx.moveTo(0           , uL * y_scale);
			ctx.lineTo(pt * x_scale, uL * y_scale);
			ctx.lineTo(pt * x_scale, uR * y_scale);
			ctx.lineTo(canvas.width, uR * y_scale);
		} else {
			// rarefaction
			var pL = p0 + a(uL) * tick * Δt;
			var pR = p0 + a(uR) * tick * Δt;
			// 1st constant function
			ctx.moveTo(0           , uL * y_scale);
			ctx.lineTo(pL * x_scale, uL * y_scale);
			// jump (vertically uniform sampling)
//			const n = 50;
//			for (let i=1; i<n; ++i) {
//				var Δu = (uR - uL) / n;
//				var u = uL + Δu * i;
//				var p = p0 + a(u) * tick * Δt;
//				ctx.lineTo(p * x_scale, u * y_scale);
//			}
			// jump (horizontally uniform sampling)
			const n = 50;
			for (let i=1; i<n; ++i) {
				var Δp = (pR - pL) / n;
				var p = pL + Δp * i;
				var u = inv_a((p - p0) / tick / Δt);
				ctx.lineTo(p * x_scale, u * y_scale);
			}
			// 2nd constant function
			ctx.lineTo(pR * x_scale, uR * y_scale);
			ctx.lineTo(canvas.width, uR * y_scale);
		}
		ctx.lineTo(canvas.width, -canvas.height);
		ctx.lineTo(0, -canvas.height);
		ctx.lineWidth = 2;
		ctx.setLineDash([]);
		ctx.strokeStyle = 'rgb(24,24,198)';
		ctx.stroke();
		ctx.fillStyle = 'rgba(198,217,241,0.8)';
		ctx.fill();

		// dots
		ctx.beginPath();
		for (let i=-nx; i<=nx; ++i) {
			var x0 = rx[0] + i * Δx;
			var u0 = (x0 < p0) ? uL : uR
			var xt = x0 + a(u0) * tick * Δt;
			if (uL > uR) {
				// shock: compression of characteristic curves
				var s = (f(uR) - f(uL)) / (uR - uL);
				var pt = p0 + s * tick * Δt;
				xt = (x0 < p0) ? Math.min(xt, pt) : Math.max(xt, pt);
			}
			ctx.moveTo(xt * x_scale, u0 * y_scale);
			ctx.arc(xt * x_scale, u0 * y_scale, 2, 0, 2 * Math.PI, false);
		}
		ctx.fillStyle = 'rgb(24,24,198)';
		ctx.fill();
	}

	var tick = 0;	// current frame number
	var tick_input = document.querySelector(`#${canvas_id}_input`);

	tick_init();
	function tick_init() {
		tick_input.min = 0;
		tick_input.max = nt;
		tick_input.valueAsNumber = tick;
	}

	tick_input.oninput = function(e) {
		tick = this.valueAsNumber;
		requestAnimationFrame(draw, canvas);
	}

	draw();
}
})();
</script>
</div><div class="canvas" name="compression">
<canvas id="compression" width="300" height="300"></canvas>
<canvas id="expansion" width="300" height="300"></canvas>
<script id="compression.js">
(function(){
	new Plot('compression', uL = 0.9, uR = 0.2, p0 = 5);
	new Plot('expansion',   uL = 0.2, uR = 0.9, p0 = 5);

function Plot(canvas_id, uL, uR, p0) {
	var canvas = document.getElementById(canvas_id);
	var ctx = canvas.getContext("2d");
	ctx.lineWidth = 2;

	var id;
	canvas.tabIndex = 1;
	canvas.style.position = "relative";
	canvas.onmouseover = canvas.focus;
	canvas.onmouseout = canvas.blur;
	canvas.onmousemove = onMouseMove;
	canvas.onmousedown = onMouseDown;
	canvas.onblur = function(){cancelAnimationFrame(id); id = 0;};
	canvas.onfocus = function(){if(!id)id = requestAnimationFrame(update, canvas);};
	function update() {id = requestAnimationFrame(update,canvas); draw();}

	var angle_x = 0;
	var angle_y = 0;
	function onMouseMove(event) {
		var x = event.layerX;
		var y = event.layerY;
		if (x>0 && x<canvas.width && y>0 && y<canvas.height) {
			angle_x = (x-canvas.width/2)/(canvas.width/2)*1.7;
			angle_y = (y-canvas.height)/(canvas.height)*1.7;
		}
	}

	var display_mode = 0;
	function onMouseDown(event) {
		display_mode = (display_mode + 1) % 2;
	};

	var plot_center = [-100,50,0];
	var screen_center = [canvas.width/2, canvas.height/2, 0];
	var focal_length = 9990;
	var distance = 10000;
	var scalar = 10;

	// inviscid Burgers' equation
	// d                  d
	// —— u(t,x) + u(t,x) —— u(t,x) = 0
	// dt                 dx

	// wave speed
	function a(u) {return u;}
	// flux
	function f(u) {return 0.5 * u * u;}

	draw();
	function draw() {
		var c = Math.cos(angle_x);
		var s = Math.sin(angle_x);
		var rotx = [c,0,-s,0,1,0,s,0,c];
		var c = Math.cos(-angle_y);
		var s = Math.sin(-angle_y);
		var roty = [1,0,0,0,c,s,0,-s,c];

		function transform(p) {
			scale(p, [scalar, -scalar, -scalar]);
			var q = mul(roty, mul(rotx, p));
			translate(q, [0,0,distance]);
			project(q, focal_length);
			translate(q, screen_center);
			translate(q, plot_center);
			return q;
		}

		// parameters in numerical simulation
		var rt = [0, 10];	// time range
		var Δt = 1;			// time step
		var nt = Math.floor((rt[1] - rt[0]) / Δt);

		var rx = [0, 20];	// space range
		var Δx = .5;		// space gap
		var nx = Math.floor((rx[1] - rx[0]) / Δx);

		// function curves (waveforms)
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		ctx.beginPath();
		for (let i=0; i<=nt; ++i)
		for (let j=0; j<=nx; ++j) {
			var t  = rt[0] + i * Δt;
			var x0 = rx[0] + j * Δx;
			var u0 = (x0 < p0) ? uL : uR
			var xt = x0 + a(u0) * t;
			if (uL > uR) {
				// shock: compression of characteristic curves
				var s = (f(uR) - f(uL)) / (uR - uL);
				var pt = p0 + s * t;
				xt = (x0 < p0) ? Math.min(xt, pt) : Math.max(xt, pt);
			}
			var p = transform([xt, u0*10, i]);
			if (j == 0) ctx.moveTo(p[0], p[1]);
			else        ctx.lineTo(p[0], p[1]);
		}
		var alpha = (display_mode == 0 ? 1 : .1);
		ctx.strokeStyle = `rgba(198,24,24,${alpha})`;
		ctx.stroke();

		// characteristic curves
		ctx.beginPath();
		for (let j=0; j<=nx; j+=2)
		for (let i=0; i<=nt; ++i) {
			var t  = rt[0] + i * Δt;
			var x0 = rx[0] + j * Δx;
			var u0 = (x0 < p0) ? uL : uR
			var xt = x0 + a(u0) * t;
			if (uL > uR) {
				// shock: compression of characteristic curves
				var s = (f(uR) - f(uL)) / (uR - uL);
				var pt = p0 + s * t;
				xt = (x0 < p0) ? Math.min(xt, pt) : Math.max(xt, pt);
			}
			var p = transform([xt, u0*10, i]);
			if (i == 0) ctx.moveTo(p[0], p[1]);
			else        ctx.lineTo(p[0], p[1]);
		}
		// rarefaction: expansion of characteristic curves
		if (uL < uR) {
			const n = 5;
			for (let j=0; j<=n; j++)
			for (let i=0; i<=nt; ++i) {
				var t  = rt[0] + i * Δt;
//				var x0 = rx[0] + j * Δx;
				// jump (vertically uniform sampling)
				var Δu = (uR - uL) / n;
				var u = uL + Δu * j;
				var p = p0 + a(u) * t;
				var p = transform([p, u*10, i]);
				if (i == 0) ctx.moveTo(p[0], p[1]);
				else        ctx.lineTo(p[0], p[1]);
			}
		}
		var alpha = (display_mode == 1 ? 1 : .1);
		ctx.strokeStyle = `rgba(24,198,24,${alpha})`;
		ctx.stroke();

		// axis
		ctx.beginPath();
		var p = transform([rx[0], rt[0], 0]);
		ctx.moveTo(p[0], p[1]);
		var p = transform([rx[1], rt[0], 0]);
		ctx.lineTo(p[0], p[1]);
		var p = transform([rx[0], rt[0], 0]);
		ctx.moveTo(p[0], p[1]);
		var p = transform([rx[0], rt[1], 0]);
		ctx.lineTo(p[0], p[1]);
		var p = transform([rx[0], rt[0], 0]);
		ctx.moveTo(p[0], p[1]);
		var p = transform([rx[0], rt[0], nt]);
		ctx.lineTo(p[0], p[1]);
		ctx.strokeStyle = "gray";
		ctx.stroke();

		// axis label
		ctx.font = "16pt Arial";
		ctx.fillStyle = "gray";
		var p = transform([rx[1], rt[0], 0]);
		ctx.textAlign = "left";
		ctx.textBaseline = "middle";
		ctx.fillText(" x ", p[0], p[1]);
		var p = transform([rx[0], rt[1], 0]);
		ctx.textAlign = "center";
		ctx.textBaseline = "bottom";
		ctx.fillText(" u(t,x) ", p[0], p[1]);
		var p = transform([rx[0], rt[0], nt]);
		ctx.textAlign = "center";
		ctx.textBaseline = "bottom";
		ctx.fillText(" t ", p[0], p[1]);
	}

	function translate(p, v) {
		p[0] += v[0];
		p[1] += v[1];
		p[2] += v[2];
	}

	function scale(p, v) {
		p[0] *= v[0];
		p[1] *= v[1];
		p[2] *= v[2];
	}

	function mul(m, p) {
		var q = new Array(3);
		q[0] = m[0] * p[0] + m[1] * p[1] + m[2] * p[2];
		q[1] = m[3] * p[0] + m[4] * p[1] + m[5] * p[2];
		q[2] = m[6] * p[0] + m[7] * p[1] + m[8] * p[2];
		return q;
	}

	function project(p, focal_length) {
		p[0] = p[0] * focal_length / p[2];
		p[1] = p[1] * focal_length / p[2];
		p[2] = focal_length;
	}
}
})();
</script>
</div><div class="canvas" name="wave1D">
<canvas id="wave1D" width="200" height="200"></canvas>
<script id="wave1D.js">
(function() {
	var canvas = document.getElementById('wave1D');
	var ctx    = canvas.getContext('2d'); 
	ctx.scale(1, -1);
	ctx.translate(0, -canvas.height);

	var resolution = 5;
	var n = (canvas.width / resolution) | 0 + 1;
	var f  = new Array(n+2).fill(0);
	var f0 = new Array(n+2).fill(0);

	var id = 0;
	canvas.tabIndex = 0;
	canvas.onblur = function(){clearInterval(id); id = 0;};
	canvas.onmousedown = function(e){
		if (!id) {id = setInterval(update, 1000/60); return;}	// 60 fps
		var x = (e.offsetX / resolution) | 0;
		f0[x] += 2/n;
		f[x] += 2/n;
	};

	function update() {
		for (var i=0; i<100; ++i) move();
		draw();
	}

	init();
	draw();

	function init() {
		for (let i=1; i<=n; i++)
			f[i] = 1/2 - Math.abs(n/2 - i) / n;
		f[0] = f[1];
		f[n+1] = f[n];

		for (let i=0; i<=n+1; i++) f0[i] = f[i];
	}

	var dt = 0.01;
	var decay = 0.9997;
	function move() {
		for (let i=1; i<=n; i++) {
			var a = (f[i+1] - f[i] + f[i-1] - f[i]);
			var temp = f[i];
			f[i] += (f[i] - f0[i]) * decay + a * dt * dt;
			f0[i] = temp;
		}
		f[0] = f[1];
		f[n+1] = f[n];
	}

	function draw() {
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		ctx.beginPath();
		ctx.moveTo(0, 0);
		for (let i=1; i<=n; ++i)
			ctx.lineTo((i-1) * resolution, f[i] * canvas.width);
		ctx.lineTo(canvas.width, 0);
		ctx.closePath();
		ctx.fillStyle = "rgba(0,255,255,0.5)";
		ctx.fill();
		ctx.strokeStyle = "rgba(0,0,255,0.5)";
		ctx.stroke();
	}
})();
</script>
</div><div class="canvas" name="wave2D">
<canvas id="wave2D" style="width: 200px; height: 200px;"></canvas>
<script id="wave2D.js">
// http://matthias-mueller-fischer.ch/realtimephysics/sws_example.tar.gz
(function(){
	var N = 50;
	var gravity = -10;
	var dt = 0.1;

	var canvas = document.getElementById('wave2D');
	var ctx    = canvas.getContext('2d');
	canvas.width  = N;
	canvas.height = N;
	var imgdt  = ctx.getImageData(0, 0, N, N);

	var id = 0;
	canvas.tabIndex = 0;
	canvas.onblur = function(){cancelAnimationFrame(id); id = 0;};
	canvas.onfocus = function(){if (!id) id = requestAnimationFrame(animate, canvas);};
	function animate() {id = requestAnimationFrame(animate, canvas); update();}

	var press = false;
	canvas.onmousedown = function(e) {press = true; addRandomDrop(e);};
	canvas.onmouseup = function(e) {press = false;};
	canvas.onmousemove = function(e) {if (press) addRandomDrop(e);};

	var H = new Float32Array(N*N).fill(.9);
	var U = new Float32Array(N*N).fill(0);
	var V = new Float32Array(N*N).fill(0);
	var A = new Float32Array(N*N).fill(0);
	var temp = new Float32Array(N*N);

	function advect(array, type) { 
		for (let i=1; i<N-1; i++)
			for (let j=1; j<N-1; j++) {
				var k = i + j*N;
				var u = 0, v = 0; 
				if (type == 0) {		// height
					u = (U[k] + U[k+1]) / 2;
					v = (V[k] + V[k+N]) / 2;
				} else if (type == 1) {	// x velocity
					u = U[k];
					v = (V[k] + V[k+1] + V[k+N] + V[k+N+1]) / 4;
				} else if (type == 2) {	// y velocity
					u = (U[k] + U[k+1] + U[k+N] + U[k+N+1]) / 4;
					v = V[k];
				}

				var x = i - u * dt; if (x < 0) x = 0; if (x > N-1) x = N-1;
				var y = j - v * dt; if (y < 0) y = 0; if (y > N-1) y = N-1;
				var x0 = x | 0, x1 = x0 + 1, s1 = x - x0, s0 = 1 - s1;
				var y0 = y | 0, y1 = y0 + 1, t1 = y - y0, t0 = 1 - t1;
				var n = s0*(t0*array[x0+N*y0] + t1*array[x0+N*y1])
				      + s1*(t0*array[x1+N*y0] + t1*array[x1+N*y1]);

				temp[k] = n;
			}
		array.set(temp);
	}

	function updateHUV() {
		for (let i=1; i<N-1; i++)
			for (let j=1; j<N-1; j++) {
				var k = i + j*N;
				var dh = -H[k] * (U[k+1] - U[k] + V[k+N] - V[k]) / 2;
				H[k] += dh * dt;
			}

		for (let i=2; i<N-1; i++)
			for (let j=1; j<N-1; j++) {
				var k = i + j*N;
				U[k] += gravity * dt * (H[k] - H[k-1]);
			} 

		for (let i=1; i<N-1; i++)
			for (let j=2; j<N-1; j++) {
				var k = i + j*N;
				V[k] += gravity * dt * (H[k] - H[k-N]);
			}
	}

	function setBoundary() {
		for (let i=0; i<N; i++) {
			var k1 = i + 0*N;
			var k2 = i + (N-1)*N;
			H[k1] = H[k1 + N];
			H[k2] = H[k2 - N];
		}
		for (let j=0; j<N; j++) {
			var k1 = 0     + j*N;
			var k2 = (N-1) + j*N;
			H[k1] = H[k1 + 1];
			H[k2] = H[k2 - 1];
		}
	}

	var width  = parseInt(canvas.style.width);
	var height = parseInt(canvas.style.height);

	function addRandomDrop(event) {
		var x = event.offsetX / width  * N | 0;
		var y = event.offsetY / height * N | 0;
		var h = Math.random() * 0.1 + 0.05;
		var s = 1;
		for (let i=x-s; i<x+s; i++)
			for (let j=y-s; j<y+s; j++) { 
				if (!(i>=0 && j>=0 && i<N && j<N)) continue;
				var k = i + j*N;
				A[k] += h;
			}
	}

	function addSource() {
		for (let i=0; i<N; i++)
			for (let j=0; j<N; j++) { 
				var k = i + j*N;
				H[k] += A[k];
				A[k] = 0;
				if (H[k] > 1.2) H[k] = 1.1;
			}
	}

	function draw() {
		var p = imgdt.data;
		for (let i=0; i<N; ++i)
			for (let j=0; j<N; ++j) {
				var k = i + j*N;
				var h = (H[k] - .7) / .3;
				var s = k * 4;
				p[s  ] = 255 * h;
				p[s+1] = 255 * h;
				p[s+2] = 255;
				p[s+3] = 255;
			}
		ctx.putImageData(imgdt, 0, 0);
	}

	function update() {
		advect(H, 0);
		advect(U, 1);
		advect(V, 2);
		addSource();
		for (let h of H) h *= 0.9;
		for (let u of U) u *= 0.9;
		for (let v of V) v *= 0.9;
		updateHUV();
		setBoundary();
		for (let i=0; i<H.length; ++i) H[i] = (H[i] - .9) * 0.99 + 0.9;

		draw();
	}

	update();
})();
</script>
</div><div class="canvas" name="mist">
<canvas id="mist" width="200" height="200"></canvas>
<script id="mist.js">
(function() {
	var canvas = document.getElementById('mist');
	var ctx    = canvas.getContext('2d', {alpha : false});
	var w      = canvas.width;
	var h      = canvas.height;
	var imgdt  = ctx.getImageData(0, 0, w, h);

	var id = 0;
	canvas.tabIndex = 0;
	canvas.onblur = function(){cancelAnimationFrame(id); id = 0;};
	canvas.onmousedown = function(){if (!id) id = requestAnimationFrame(animate, canvas);};

	var t0, t1, t, dt;
	function animate() {
		t0 = new Date().getTime();
		t = 0;
		frame();
	}
	function frame() {
		id = requestAnimationFrame(frame, canvas);
		t1 = new Date().getTime();
		dt = 0.001*(t1-t0);
		t0 = t1;
//		if (dt > 0.2) dt = 0;
		t += dt;
		update();
	}

	var N    = 200;
	var size = (N+2)*(N+2);
	var c    = new Array(size);
	var c0   = new Array(size);
	var u    = new Array(size);
	var v    = new Array(size);
	function IDX(i,j) {return i*(N+2)+j;}

	init();
	function init() {
		for (let i=0; i<size; i++) {
			c[i] = c0[i] = 0;
			u[i] = Math.floor(Math.random() * 10) - 5;
			v[i] = Math.floor(Math.random() * -40);
		}
	}

	function clip(a) {
		for (let i=0; i<size; i++)
			a[i] = Math.min(a[i], 1000000);
	}

	function bound(a) {
		for (let i=1; i<=N; i++)
			a[IDX(0,i)] = a[IDX(N+1,i)] = a[IDX(i,0)] = a[IDX(i,N+1)] = 0;
	}

	// solve poisson equation with gauss-seidel
	function diffuse(a, a0, dt) {
		const flow = 30;	// amount per second
		var d = flow * dt;
		for (let k=0; k<10; k++) {
			for (let i=1; i<=N; i++) for (let j=1; j<=N; j++) {
				var f4 = a[IDX(i-1,j)] + a[IDX(i+1,j)]
					   + a[IDX(i,j-1)] + a[IDX(i,j+1)];
				a[IDX(i,j)] = (a0[IDX(i,j)] + d*f4)/(1+4*d);
			}
		}
	}

	// bilinear interpolation
	function advect(a, a0, u, v, dt) {
		for (let i=1; i<=N; i++) for (let j=1; j<=N; j++) {
			var x = i - u[IDX(i,j)] * dt;
			var y = j - v[IDX(i,j)] * dt;
			x = Math.max(x, 0); x = Math.min(x, N);
			y = Math.max(y, 0); y = Math.min(y, N);
			var i0 = Math.floor(x), i1 = i0+1;
			var j0 = Math.floor(y), j1 = j0+1;
			var s1 = x-i0, s0 = 1-s1;
			var t1 = y-j0, t0 = 1-t1;
			a[IDX(i,j)] = s0*(t0*a0[IDX(i0,j0)]+t1*a0[IDX(i0,j1)])+
						  s1*(t0*a0[IDX(i1,j0)]+t1*a0[IDX(i1,j1)]);
		}
	}

	function pour(a, n) {
		const period = 0.05;	// speed
		if (t >= period) {
			t %= period;
			var i = Math.ceil(Math.random() * 200);
			var j = Math.ceil(Math.random() * 200);
			a[IDX(i,j)] += n;
		}
	}

	function update() {
		pour(c0, 10000);
		diffuse(c, c0,       dt); bound(c); [c, c0] = [c0, c]; 
		advect (c, c0, u, v, dt); bound(c); [c, c0] = [c0, c]; 
		clip(c); // pour too fast
		draw(c);
	}

	function draw(a) {
		var d = imgdt.data;
		for (let i=1; i<=N; ++i) for (let j=1; j<=N; ++j) {
			var s = ((j-1) * w + (i-1)) * 4;
			d[s] = d[s+1] = d[s+2] = a[IDX(i,j)];
			d[s+2] *= 0.8;
			d[s+3] = 255;
		}
		ctx.putImageData(imgdt, 0, 0);
	}
})();
</script>
</div><div class="canvas" name="shoaling">
<canvas id="shoaling" width="600" height="200"></canvas>
<input type="range" id="shoaling_input">
<script id="shoaling.js">
(function() {
	// nonlinear shallow water equation in 1D
	var PDE = function() {
		const g = 9.80665;
		const x0 = 0;		// position of incident wave
		const H = 0.3;		// height of incident wave
		const ε = 1e-3;		// machine epsilon

		function C(x) {
			if (h(x) <= ε) return 0;
			return Math.sqrt(g * h(x));
		}

		function η(x, t) {
			if (h(x) <= ε) return -h(x) + ε;
			var K = Math.sqrt(3 * H / 4 / h(x)) / h(x);
			return H * (Math.cosh(K*x) ** -2);
		}

		function U(x, t) {
			if (h(x) <= ε) return 0;
			return η(x, t) / h(x) * C(x)
		}

		function h(x) {
			if (x < 0) return 1;
			return 1 - x / 19.85;
		}

		const C0 = C(x0);

		// trick for tossing 'this'
		return {C0, g, η, U, h, ε};
	}();

	// parameters in numerical simulation
	const t_range = [0, 25];	// time range
	const Δt = 0.01;			// time step
	const nt = Math.floor((t_range[1] - t_range[0]) / Δt);

	var x_range = [-4, +35];	// sampling range
	var Δx = 0.5;				// sampling gap
	var nx = Math.floor((x_range[1] - x_range[0]) / Δx);	// gap number (sample number)

	const order = 4;	// 4th-order central difference
	const offset = Math.floor(order / 2);
	var η  = new Float64Array2D(nt + 1, nx + offset * 2);
	var U  = new Float64Array2D(nt + 1, nx + offset * 2);
	var h  = new Float64Array(nx + offset * 2);
	//-------------------------------------------
	var H  = new Float64Array2D(nt + 1, nx + offset * 2);
	var HU = new Float64Array2D(nt + 1, nx + offset * 2);
	var F  = new Float64Array(nx + offset * 2 + 1);
	var G  = new Float64Array(nx + offset * 2 + 1);
	// -1/2 and +1/2 of finite volume method
	var hᴸ = new Float64Array(nx + offset * 2 + 1);
	var hᴿ = new Float64Array(nx + offset * 2 + 1);
	var ηᴸ = new Float64Array(nx + offset * 2 + 1);
	var ηᴿ = new Float64Array(nx + offset * 2 + 1);
	var Uᴸ = new Float64Array(nx + offset * 2 + 1);
	var Uᴿ = new Float64Array(nx + offset * 2 + 1);
	// intermediate steps of Runge–Kutta method
	var Hʹ  = new Float64Array(nx + offset * 2);
	var Hʺ  = new Float64Array(nx + offset * 2);
	var HUʹ = new Float64Array(nx + offset * 2);
	var HUʺ = new Float64Array(nx + offset * 2);

	function Float64Array2D(m, n) {
		var array = new Array(m);
		for (let i=0; i<m; ++i)
			array[i] = new Float64Array(n);
		return array;
	}

	function table() {
		initial_condition();
		advect();
	}

	// an incident solitary wave
	function initial_condition() {
		for (let i=offset; i<offset+nx; i++) {
			var t = t_range[0] + 0 * Δt;
			var x = x_range[0] + (i-offset+0.5) * Δx;
			h[i]     = PDE.h(x);
			H[0][i]  = PDE.η(x, t) + PDE.h(x);
			HU[0][i] = (PDE.η(x, t) + PDE.h(x)) * PDE.U(x, t);
		}
	}

	function advect() {
		function set_flux(H, HU, h, η, U, F, G) {
			// boundary condition
			set_boundary_of_H(H);
			set_boundary_of_HU(HU);

			// decomposition
			decomposition(H, HU, h, η, U);

			// slope limiter: van Leer slope limiter
			lerp(h, hᴸ, hᴿ);
			MUSCL(η, ηᴸ, ηᴿ);
			MUSCL(U, Uᴸ, Uᴿ);

			// Riemann solver: Harten–Lax–van Leer contact
			HLLC(hᴸ, hᴿ, ηᴸ, ηᴿ, Uᴸ, Uᴿ, F, G);
		}

		// wall boundary condition
		function set_boundary_of_H(H) {
			H[offset-2]    = H[offset+1];
			H[offset-1]    = H[offset+0];
			H[offset+nx+1] = H[offset+nx-2];
			H[offset+nx+0] = H[offset+nx-1];
		}

		function set_boundary_of_HU(HU) {
			HU[offset-2]    = -HU[offset+1];
			HU[offset-1]    = -HU[offset+0];
			HU[offset+nx+1] = -HU[offset+nx-2];
			HU[offset+nx+0] = -HU[offset+nx-1];
		}

		function dry(H) {
			return H <= PDE.ε;
		}

		// I dunno the name of this technique
		function decomposition(H, HU, h, η, U) {
			// if H = 0 then U = 0
			for (let i=offset; i<offset+nx; i++)
				if (dry(H[i])) {
					H[i] = PDE.ε;
					HU[i] = 0;
				}

			for (let i=offset; i<offset+nx; i++) {
				η[i] = H[i] - h[i];
				U[i] = dry(H[i]) ? 0 : (HU[i] / H[i]);
			}
		}

		// linear interpolation
		function lerp(u, uᴸ, uᴿ) {
			for (let i=offset; i<offset+nx+1; i++) {
				// cell major
				// uᴸ := u_{i-1/2}^{right}
				// uᴿ := u_{i+1/2}^{left}
//				uᴸ[i] = (u[i] + u[i-1]) / 2;
//				uᴿ[i] = (u[i] + u[i+1]) / 2;
				// edge major
				// uᴿ := u_{i-1/2}^{right}
				// uᴸ := u_{i+1/2}^{left}
				uᴸ[i] = uᴿ[i] = (u[i] + u[i-1]) / 2;
			}
		}

		// van Leer slope limiter
		function MUSCL(u, uᴸ, uᴿ) {
			for (let i=offset; i<offset+nx; i++) {
				var Δᴸ = u[i] - u[i-1];
				var Δᴿ = u[i+1] - u[i];
				var ǀΔᴸǀ = Math.abs(Δᴸ);
				var ǀΔᴿǀ = Math.abs(Δᴿ);
				var Δ = (ǀΔᴸǀ + ǀΔᴿǀ > 0) ? (Δᴸ*ǀΔᴿǀ + Δᴿ*ǀΔᴸǀ) / (ǀΔᴸǀ + ǀΔᴿǀ) : 0;
				// cell major
				// uᴸ := u_{i-1/2}^{right}
				// uᴿ := u_{i+1/2}^{left}
//				uᴸ[i] = u[i] - Δ/2;
//				uᴿ[i] = u[i] + Δ/2;
				// edge major
				// uᴿ := u_{i-1/2}^{right}
				// uᴸ := u_{i+1/2}^{left}
				uᴿ[i]   = u[i] - Δ/2;
				uᴸ[i+1] = u[i] + Δ/2;
			}
			// edge major
			uᴿ[offset+nx] = undefined;
			uᴸ[offset]    = undefined;
		}

		// Riemann solver: Harten–Lax–van Leer contact
		function HLLC(hᴸ, hᴿ, ηᴸ, ηᴿ, Uᴸ, Uᴿ, F, G) {
			var i = offset; {
				var Hᴿ = ηᴿ[i] + hᴿ[i];
				var HUᴿ = Hᴿ * Uᴿ[i];
				F[i] = HUᴿ;
				G[i] = HUᴿ * Uᴿ[i] + 0.5 * PDE.g * (ηᴿ[i] * ηᴿ[i] + 2 * ηᴿ[i] * hᴿ[i]);
			}

			var i = offset + nx; {
				var Hᴸ = ηᴸ[i] + hᴸ[i];
				var HUᴸ = Hᴸ * Uᴸ[i];
				F[i] = HUᴸ;
				G[i] = HUᴸ * Uᴸ[i] + 0.5 * PDE.g * (ηᴸ[i] * ηᴸ[i] + 2 * ηᴸ[i] * hᴸ[i]);
			}

			for (let i=offset+1; i<offset+nx; i++) {
				// initialization
				var Hᴸ = Math.max(0, ηᴸ[i] + hᴸ[i]);
				var Hᴿ = Math.max(0, ηᴿ[i] + hᴿ[i]);
				var HUᴸ = Hᴸ * Uᴸ[i];
				var HUᴿ = Hᴿ * Uᴿ[i];

				// resolution
				var cᴸ = Math.sqrt(PDE.g * Hᴸ);
				var cᴿ = Math.sqrt(PDE.g * Hᴿ);
				var Uʹ = (Uᴸ[i] + Uᴿ[i])/2 + (cᴸ - cᴿ);
				var cʹ = (cᴸ + cᴿ)/2 + (Uᴸ[i] - Uᴿ[i])/4;
				if (Uʹ >= 0) {
					var Sᴸ = Math.min(Uᴸ[i] - cᴸ, Uʹ - cʹ);
					if (dry(Hᴸ[i])) Sᴸ = Uᴿ[i] - 2 * cᴿ;
					var Hʹᴸ = Hᴸ * (Sᴸ - Uᴸ[i])/(Sᴸ - Uʹ);
					// dividing zero
//					if (Math.abs(Sᴸ - Uʹ) < 1e-1) Hʹᴸ = Hᴸ;
					var HUʹᴸ = Hʹᴸ * Uʹ;
					F[i] = HUᴸ;
					if (Sᴸ < 0) F[i] += Sᴸ * (Hʹᴸ - Hᴸ);
					G[i] = HUᴸ * Uᴸ[i] + 0.5 * PDE.g * (ηᴸ[i] * ηᴸ[i] + 2 * ηᴸ[i] * hᴸ[i]);
					if (Sᴸ < 0) G[i] += Sᴸ * (HUʹᴸ - HUᴸ);
				} else {
					var Sᴿ = Math.max(Uᴿ[i] + cᴿ, Uʹ + cʹ);
					if (dry(Hᴿ[i])) Sᴿ = Uᴸ[i] + 2 * cᴸ;
					var Hʹᴿ = Hᴿ * (Sᴿ - Uᴿ[i])/(Sᴿ - Uʹ);
					// dividing zero
//					if (Math.abs(Sᴸ - Uʹ) < 1e-1) Hʹᴿ = Hᴿ;
					var HUʹᴿ = Hʹᴿ * Uʹ;
					F[i] = HUᴿ;
					if (Sᴿ > 0) F[i] += Sᴿ * (Hʹᴿ - Hᴿ);
					G[i] = HUᴿ * Uᴿ[i] + 0.5 * PDE.g * (ηᴿ[i] * ηᴿ[i] + 2 * ηᴿ[i] * hᴿ[i]);
					if (Sᴿ > 0) G[i] += Sᴿ * (HUʹᴿ - HUᴿ);
				}
			}
		}

		// strong stability-preserving Runge–Kutta method
		function ΔH(F, η, h, i) {
			return -(Δt/Δx) * (F[i+1] - F[i]);
		}

		function ΔHU(G, η, h, i) {
			return -(Δt/Δx) * ((G[i+1] - G[i]) + 0.5 * PDE.g * η[i] * (h[i+1] - h[i-1]));
		}

		// it is good for debugging
		function Euler(n) {
			set_flux(H[n], HU[n], h, η[n], U[n], F, G);
			for (let i=offset; i<offset+nx; i++)
				H[n+1][i] = H[n][i] + ΔH(F,η[n],h,i);
			for (let i=offset; i<offset+nx; i++)
				HU[n+1][i] = HU[n][i] + ΔHU(G,η[n],h,i)
		}

		function SSPRK(n) {
			// first round
			set_flux(H[n], HU[n], h, η[n], U[n], F, G);
			for (let i=offset; i<offset+nx; i++)
				Hʹ[i] = H[n][i] + ΔH(F,η[n],h,i);
			for (let i=offset; i<offset+nx; i++)
				HUʹ[i] = HU[n][i] + ΔHU(G,η[n],h,i);

			// second round
			set_flux(Hʹ, HUʹ, h, η[n], U[n], F, G);
			for (let i=offset; i<offset+nx; i++)
				Hʺ[i] = 3/4 * H[n][i] + 1/4 * Hʹ[i] + 1/4 * ΔH(F,η[n],h,i);
			for (let i=offset; i<offset+nx; i++)
				HUʺ[i] = 3/4 * HU[n][i] + 1/4 * HUʹ[i] + 1/4 * ΔHU(G,η[n],h,i);

			// third round
			set_flux(Hʺ, HUʺ, h, η[n], U[n], F, G);
			for (let i=offset; i<offset+nx; i++)
				H[n+1][i] = 1/3 * H[n][i] + 2/3 * Hʺ[i] + 2/3 * ΔH(F,η[n],h,i);
			for (let i=offset; i<offset+nx; i++)
				HU[n+1][i] = 1/3 * HU[n][i] + 2/3 * HUʺ[i] + 2/3 * ΔHU(G,η[n],h,i);
		}

		set_boundary_of_H(h);
		for (let n=0; n<nt; n++) {
//			Euler(n);
			SSPRK(n);
		}
		decomposition(H[nt], HU[nt], h, η[nt], U[nt]);
	}

	var canvas  = document.getElementById('shoaling');
	var ctx     = canvas.getContext('2d');

	function draw() {
		requestAnimationFrame(plot_PDE, canvas);
	}

	function plot_PDE() {
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		ctx.save();
		ctx.scale(1, -1);
		ctx.translate(0, -canvas.height/2);
		plot_numerical_solution(η, "rgba(24,24,198)");
		plot_numerical_solution_dot(η, "rgba(24,24,198)");
		plot_analytical_solution((x) => -PDE.h(x,0), "black");
		ctx.restore();
	}

	function plot_analytical_solution(f, color) {
		const x_scale = canvas.width / (nx - 1);
		const y_scale = 100;

		ctx.beginPath();
		var t = t_range[0] + tick * Δt;
		var x = x_range[0];
		ctx.moveTo(0 * x_scale, f(x, t) * y_scale);
		for (let i=1; i<nx; ++i) {
			var t = t_range[0] + tick * Δt;
			var x = x_range[0] + i * Δx;
			ctx.lineTo(i * x_scale, f(x, t) * y_scale);
		}
		ctx.lineTo(canvas.width, -canvas.height);
		ctx.lineTo(0, -canvas.height);

		ctx.lineWidth = 2;
		ctx.setLineDash([]);
		ctx.strokeStyle = color;
		ctx.stroke();
		ctx.fillStyle = 'rgba(148,138,84)';
		ctx.fill();
	}

	function plot_numerical_solution(f, color) {
		const x_scale = canvas.width / (nx - 1);
		const y_scale = 100;

		ctx.beginPath();
		ctx.moveTo(0 * x_scale, f[tick][offset] * y_scale);
		for (let i=1; i<nx; ++i)
			ctx.lineTo(i * x_scale, f[tick][offset+i] * y_scale);
		ctx.lineTo(canvas.width, -canvas.height);
		ctx.lineTo(0, -canvas.height);

		ctx.lineWidth = 2;
		ctx.setLineDash([]);
		ctx.strokeStyle = color;
		ctx.stroke();
		ctx.fillStyle = 'rgb(198,217,241,0.8)';
		ctx.fill();
	}

	function plot_numerical_solution_dot(f, color) {
		const x_scale = canvas.width / (nx - 1);
		const y_scale = 100;

		ctx.beginPath();
		for (let i=0; i<nx; ++i) {
			ctx.moveTo(i * x_scale, f[tick][offset+i] * y_scale);
			ctx.arc(i * x_scale, f[tick][offset+i] * y_scale, 2, 0, 2 * Math.PI, false);
		}
		ctx.fillStyle = color;
		ctx.fill();
	}

	var tick     = 0;	// current frame number
	var tick_in  = document.querySelector("#shoaling_input");

	tick_init();
	function tick_init() {
		tick_in.min = 0;
		tick_in.max = nt;
		tick_in.valueAsNumber = tick;
	}

	tick_in.oninput = function(e) {
		tick = this.valueAsNumber;
		draw();
	}

	table();
	draw();
})();
</script>
</div><div class="canvas" name="rabbit_fox">
<canvas id="rabbit_fox" width="300" height="200"></canvas>
<canvas id="rabbit_fox2" width="300" height="200"></canvas>
<script id="rabbit_fox.js">
(function(){
var rabbit_fox = function() {
	var x0 = [3, 3];
	var f = [function(x) {return 1.1*x[0] - 0.4*x[0]*x[1];},
			 function(x) {return 0.1*x[0]*x[1] - 0.4*x[1];}];
	var label = ['🐇','🦊'];
	return {f, x0, label, xscale: 20, tscale: 30,
			seed: 'line', step: 1000, size: 0.1, arrow: [20, 120]};
}();

new Plot(rabbit_fox, 'TimeSeries', 'rabbit_fox');
new Plot(rabbit_fox, 'Phase', 'rabbit_fox2');

function Plot(pde, type, canvas_name) {
	var canvas = document.getElementById(canvas_name);
	var ctx    = canvas.getContext('2d');

	var [f, x0, label, xscale, tscale, seed, step, size, arrow] = Object.values(pde);

	var draw   = null;
	if      (type === 'TimeSeries') draw = DrawTimeSeries;
	else if (type === 'Phase')      draw = DrawPhase;

	var labelX, labelY;
	if      (type === 'TimeSeries') [labelX, labelY] = ['t', ' f(t)'];
	else if (type === 'Phase')      [labelX, labelY] = label;

	const _axis  = 10;	// offset for axis 
	const _label = 20;	// offset for label
	const width  = canvas.width  - _axis - _label;	// x-axis length
	const height = canvas.height - _axis - _label;	// y-axis length

	main();

	function main() {
		ctx.save();
		ctx.translate(0, canvas.height);
		ctx.scale(1, -1);
		ctx.translate(_axis, _axis);
		DrawXY(width, height, labelX, labelY);
		draw();
		ctx.restore();
	}

	function DrawTimeSeries() {
		const n = f.length;
		const dt = 0.01;
		const colors = ["rgb(192,0,0)", "rgb(0,192,0)", "rgb(0,0,192)",
                        "rgb(0,0,0,0)", "rgb(0,0,0,0)", "rgb(0,0,0,0)"];

		for (let k=n-1; k>=0; --k) {
			var x     = x0.slice(0);
			var xnext = x0.slice(0);

			// curve
			ctx.beginPath();
			for (let w=0; w<width; w++) {
				ctx.lineTo(w, x[k] * xscale);
				for (let t=0; t<tscale; ++t) {
					for (let i=0; i<n; ++i) xnext[i] = x[i] + f[i](x) * dt;
					for (let i=0; i<n; ++i) x[i] = xnext[i];
				}
			}
			ctx.strokeStyle = colors[k];
			ctx.lineWidth   = 2.25;
			ctx.stroke();

			// label
			ctx.save();
			ctx.scale(1, -1);
			ctx.font = "12pt Tahoma";
			ctx.textBaseline = "middle";
			ctx.textAlign = "left";
			ctx.fillStyle = "rgb(127,127,127)";
			ctx.fillText(label[k], width, -(x[k] * xscale));
			ctx.restore();
		}
	}

	function DrawPhase() {
		const n = f.length;
		if (n !== 2) return;
		const dt = 0.01;

		var x0s = [];
		if (seed === 'boundary') {
			var coords = [20, 60, 120, 180];
			for (let c of coords) {
				// axis label overlapping
				if (c < width) x0s.push([c,0]);
				if (c < height) x0s.push([0,c]);
				x0s.push([canvas.width-_axis,c], [c,canvas.height-_axis]);
			}
		} else if (seed === 'line') {
			var coords = [10, 20, 30, 40, 50];
			for (let c of coords) x0s.push([c,c]);
		}

		for (let x0 of x0s) {
			var x     = [x0[0] / xscale, x0[1] / xscale];
			var xnext = [x0[0] / xscale, x0[1] / xscale];

			// curve
			ctx.beginPath();
			for (let s=0; s<step; ++s) {
				ctx.lineTo(x[0] * xscale, x[1] * xscale);

				// arraw head
				if (arrow.includes(s)) {
					var slope = Math.atan2(f[1](x), f[0](x));
					var angle = Math.PI / 8;
					var len   = .35;
					var l0 = x[0] - len * Math.cos(slope-angle);
					var l1 = x[1] - len * Math.sin(slope-angle);
					var r0 = x[0] - len * Math.cos(slope+angle);
					var r1 = x[1] - len * Math.sin(slope+angle);
					ctx.lineTo(l0   * xscale, l1   * xscale);
					ctx.lineTo(r0   * xscale, r1   * xscale);
					ctx.lineTo(x[0] * xscale, x[1] * xscale);
				}

				for (let t=0; t<tscale; ++t) {
					for (let i=0; i<n; ++i) xnext[i] = x[i] + f[i](x) * dt * size;
					for (let i=0; i<n; ++i) x[i] = xnext[i];
				}
			}
			ctx.strokeStyle = "rgb(192,0,0)";
			ctx.lineWidth   = 1.5;
			ctx.stroke();
		}
	}

	function DrawArrow(x1, y1, x2, y2, len, angle, gap) {
		var slope = Math.atan2(y2 - y1, x2 - x1);
		x1 += gap * Math.cos(slope);
		y1 += gap * Math.sin(slope);
		x2 -= gap * Math.cos(slope);
		y2 -= gap * Math.sin(slope);

		ctx.beginPath();
		ctx.moveTo(x1, y1);
		ctx.lineTo(x2, y2);
		ctx.lineTo(x2 - len * Math.cos(slope-angle), y2 - len * Math.sin(slope-angle));
		ctx.moveTo(x2, y2);
		ctx.lineTo(x2 - len * Math.cos(slope+angle), y2 - len * Math.sin(slope+angle));
		ctx.stroke();
	}

	function DrawXY(scalarX, scalarY, labelX, labelY) {
		ctx.lineWidth = 1;
		ctx.strokeStyle = "gray";
		DrawArrow(0, 0, +scalarX, 0, 8, Math.PI / 5, 0);
		DrawArrow(0, 0, 0, +scalarY, 8, Math.PI / 5, 0);

		ctx.scale(1, -1);
		ctx.font = "12pt Arial";
		ctx.textAlign = "center";
		ctx.textBaseline = "middle";
		ctx.fillStyle = "gray";
		ctx.fillText(labelX, +scalarX+10, 0);
		ctx.textBaseline = "bottom";
		ctx.fillText(labelY, 0, -scalarY-2);
		ctx.scale(1, -1);
	}
}
})();
</script>
</div>