<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: 600px; 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: 600px; 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>