<div class="canvas" name="gait">
<img src="material/cathead.png" id="cathead.png" style="display: none;">
<img src="material/catfoot.png" id="catfoot.png" style="display: none;">
<canvas id="gait" width="200" height="200"></canvas>
<script id="loader.js">
function Loader(main, ...files) {
	var n = 0;
	function onload() {if (++n == files.length) main();}

	for (let file of files) {
		var element = document.getElementById(file);
		element.addEventListener('load', onload);
	}
}
</script>
<script id="gait.js">
new Loader(gait, 'cathead.png', 'catfoot.png');

function gait() {
	var imgH = document.getElementById('cathead.png');
	var imgF = document.getElementById('catfoot.png');
	var canvas = document.getElementById('gait');
	var ctx    = canvas.getContext('2d');

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

	var leg = new Array(4);
	leg[0] = {x: 70,  y: 140, phase: 5};
	leg[1] = {x: 80,  y: 140, phase: 0};
	leg[2] = {x: 135, y: 140, phase: 0};
	leg[3] = {x: 145, y: 140, phase: 5};
	var seq = [1,0,2,3];	// paint sequence

	var tick = 0, speed = 10, loop = 100;
	canvas.onkeydown = function(event) {
		event = event || window.event;
		event.preventDefault();
		switch(event.keyCode) {
		case 49: case 50: case 51: case 52://1234
			var i = event.keyCode - 49;
			if (++leg[i].phase >= 10) leg[i].phase = 0;
			break;
		case 83://space
			if (++speed >= 11) speed = 1;
			break;
		}
	};

	draw();
	function draw() {
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		tick = (tick + speed) % (loop * 100);

		// info
		ctx.font = "12pt verdana";
		ctx.fillStyle = "gray";
		ctx.fillText(
			"  " + leg[0].phase +
			"  " + leg[1].phase +
			"  " + leg[2].phase +
			"  " + leg[3].phase +
			"  speed: " + speed,
			0, 16);
		ctx.fillText("[1234] phase [s] speed", 4, canvas.height - 4);

		// legs
		for (var i=0; i<leg.length; ++i) {
			var x = leg[seq[i]].x;
			var y = leg[seq[i]].y;
			var p = leg[seq[i]].phase / 10;
			var r = 3;
			var dx = Math.sin(Math.PI * 2 * (tick / loop + p));
			var dy = Math.sqrt(r * r - dx * dx);
			var t = Math.atan2(dx, dy);
			ctx.save();
			ctx.translate(x, y);
			ctx.rotate(t);
			ctx.drawImage(imgF, -imgF.width/2, 0);
			ctx.restore();
		}
		ctx.drawImage(imgH, 40, 25);
	}
};
</script>
</div><div class="canvas" name="FABRIK">
<canvas id="FABRIK" width="200" height="200"></canvas>
<script id="FABRIK.js">
(function(){
	var canvas = document.getElementById('FABRIK');
	var ctx    = canvas.getContext('2d');

	var points = [[0,0],[20,20],[40,40],[60,60],[80,80]];

	var mouse = points.at(-1).slice();
	canvas.onmousemove = function(e){
		mouse = [e.offsetX, e.offsetY];
	};

	var fixed = true;
	var base = points[0].slice();
	canvas.onmousedown = function(e){
		fixed = false;
	};
	canvas.onmouseup = function(e){
		fixed = true;
		base = points[0].slice();
	};
	canvas.onmouseenter = function(e) {
		fixed = !(e.buttons == 1);
		base = points[0].slice();
	};

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

	update();
	function update() {
		forward(points, mouse.slice());
		if (fixed) backward(points, base.slice());

		ctx.clearRect(0, 0, canvas.width, canvas.height);
		DrawSegments(ctx, points, "rgb(255,192,0)");
		DrawPointArray(ctx, points, "white", "black");
	}

	function forward(points, target) {
		var n = points.length;
		for (let i=n-1; i>0; --i)
			reach(points[i-1], points[i], target);
		points[0] = target;
	}

	function backward(points, base) {
		var n = points.length;
		for (let i=0; i<n-1; ++i)
			reach(points[i+1], points[i], base);
		points[n-1] = base;
	}

	function reach(head, tail, target) {
		var old_dx = tail[0] - head[0];
		var old_dy = tail[1] - head[1];
		var old_dist = Math.hypot(old_dx, old_dy);

		var new_dx = target[0] - head[0];
		var new_dy = target[1] - head[1];
		var new_dist = Math.hypot(new_dx, new_dy);

		tail[0] = target[0];
		tail[1] = target[1];

		if (new_dist > 1e-3) {
			var scale = old_dist / new_dist;
			target[0] = tail[0] - new_dx * scale;
			target[1] = tail[1] - new_dy * scale;
		} else {
			target[0] = head[0] - old_dx;
			target[1] = head[1] - old_dy;
		}
	}

	function DrawPointArray(ctx, p, rgb1, rgb2) {
		ctx.fillStyle = rgb1;
		if (rgb2) ctx.strokeStyle = rgb2;
		ctx.lineWidth = 1.5;
		for (var i = 0; i < p.length; ++i) {
			ctx.beginPath();
			ctx.arc(p[i][0], p[i][1], 4, 0, Math.PI * 2, true);
			ctx.fill();
			if (rgb2) ctx.stroke();
		}
	}

	function DrawSegments(ctx, p, rgb) {
		ctx.strokeStyle = rgb;
		ctx.lineWidth = 2.25;
		ctx.beginPath();
		for (var i=0; i<p.length-1; ++i) {
			ctx.moveTo(p[i][0], p[i][1]);
			ctx.lineTo(p[i+1][0], p[i+1][1]);
		}
		ctx.stroke();
	}
})();
</script>
</div><div class="canvas" name="HumanMotion">
<!--<input type="file" id="file"></input><br>-->
<canvas id="HumanMotion" width="300" height="200"></canvas>
<canvas id="AngularRate" width="290" height="200"></canvas>
<br><input type="range" id="HumanMotion_input">
<style>
#HumanMotion, #AngularRate {background-color: black;}
#HumanMotion_input {width: 300px;}
</style>
<script id="HumanMotion.js">
(function(){
var canvas = document.getElementById("HumanMotion");
var ctx = canvas.getContext("2d");
ctx.globalAlpha = 0.8;

var canvas2 = document.getElementById("AngularRate");
var ctx2 = canvas2.getContext("2d");
ctx2.globalAlpha = 0.8;

var input = document.querySelector("#HumanMotion_input");
var tick = 0;	// current time index

function reset_tick(tick_max) {
	input.min = 0;
	input.max = tick_max - 1;
	input.valueAsNumber = tick = 0;
}

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

var inc, last, angle_x, angle_y;
var display_mode_1, display_mode_2;

reset_variables();
function reset_variables() {
	inc = 1;
	last = -1;
	angle_x = -Math.PI / 4;
	angle_y = 0;
	display_mode_1 = 0;
	display_mode_2 = 0;
}

input.onkeydown = function(e) {
	e.preventDefault();
};

window.onkeyup = function(e) {
	inc = 1;
	last = -1;
};

window.onkeydown = function(e) {
	last == event.keyCode ? (inc = Math.min(++inc, 25)) : (inc = 1);
	last = event.keyCode;

	if      (event.code === 'ArrowLeft')
		tick = Math.max(input.min, tick -= inc);
	else if (event.code === 'ArrowRight')
		tick = Math.min(input.max, tick += inc);
	else if (event.code === 'KeyA')	// left
		angle_x = Math.max(-1.57, angle_x -= .157);
	else if (event.code === 'KeyD')	// right
		angle_x = Math.min(+1.57, angle_x += .157);
	else if (event.code === 'KeyW')	// up
		angle_y = Math.max(-1.57, angle_y -= .157);
	else if (event.code === 'KeyS')	// down
		angle_y = Math.min(+1.57, angle_y += .157);
	else if (event.code === 'Digit1')	// mass center
		display_mode_1 = (display_mode_1 + 1) % 7;
	else if (event.code === 'Digit2')	// Euler angle
		display_mode_2 = (display_mode_2 + 1) % 3;
	else
		return;

	input.value = tick;
	requestAnimationFrame(draw, canvas);
};

var model = {};
var table = {};
/*
var file = document.getElementById("file");
file.onchange = function(event) {
	var file = event.target.files[0];
	if (file && file.type.match('text.*')) {
		var reader = new FileReader();
		reader.onload = function(e) {
			data = e.target.result;
			data = data.replaceAll('\r', '');
			main(data);
		};
		reader.readAsText(file);
	}
}
*/
fetch('material/2014001_C1_01.c3d.txt').then(r => r.text()).then((data) => {
	data = data.replaceAll('\r', '');
	main(data);
});

function main(data) {
	load_point_data(model, data);
	load_analog_data(model, data);
	load_platform_data(model, data);

	build_scene(model);

	load_table(table);
	rebuild_marker_names(model, table);
	build_centers(model, table);
	build_joint_centers(model, table);
	build_segment_centers(model, table);
	build_body_center(model, table);

	build_center_translational_rate(model);
	build_center_angular_rate(model);

	build_platform_coordinates(model);
	build_platform_forces(model);
	build_platform_COP(model);
	build_COP(model);

	build_joint_forces(model);

	reset_variables();
	reset_tick(model.analog_data[0].length);
	requestAnimationFrame(draw, canvas);
}

function load_point_data(model, data) {
	var lines = data.split('\n');

	var N = parseInt(lines[0]);
	if (isNaN(N)) N = 0;
	console.log('marker number = ', N);

	model.point_names = lines.slice(1, 1+N);
	console.log('marker names = ', model.point_names);

	model.point_idx = new Map();
	for (let i=0; i<N; ++i)
		model.point_idx.set(model.point_names[i], i);
	console.log('point_idx mapping = ', model.point_idx);

	var L = parseInt(lines[1+N]);
	console.log('marker sample number = ', L);

	lines = lines.slice((1+N)+1, (1+N)+(1+N));
	model.point_data = new Array(N);
	for (let i=0; i<N; ++i) {
		var line = lines[i];
		var s = line.match(/\S+/g);
		model.point_data[i] = new Array(L);
		for (let j=0, k=0; j<L; ++j, k+=3) {
			var x = parseFloat(s[k+0]);
			var y = parseFloat(s[k+1]);
			var z = parseFloat(s[k+2]);
			var miss = (isNaN(x) || isNaN(y) || isNaN(z));
			if (miss) x = y = z = Infinity;	// get out
			model.point_data[i][j] = [x,y,z,miss];
		}
	}

	model.frame_rate = 200;	// draw2()
}

function load_analog_data(model, data) {
	var k = (model.point_names.length + 1) * 2;
	var lines = data.split('\n').slice(k);

	var N = parseInt(lines[0]);
	if (isNaN(N)) N = 0;
	console.log('analog number = ', N);

	model.analog_names = lines.slice(1, 1+N);
	console.log('analog names = ', model.analog_names);

	model.analog_idx = new Map();
	for (let i=0; i<N; ++i)
		model.analog_idx.set(model.analog_names[i], i);
	console.log('analog_idx mapping = ', model.analog_idx);

	var L = parseInt(lines[1+N]);
	console.log('analog sample number = ', L);

	lines = lines.slice((1+N)+1, (1+N)+(1+N));
	model.analog_data = new Array(N);
	for (let i=0; i<N; ++i) {
		var line = lines[i];
		var s = line.match(/\S+/g);
		model.analog_data[i] = new Array(L);
		for (let j=0; j<L; ++j) {
			var v = parseFloat(s[j]);	// force/torque/voltage
			model.analog_data[i][j] = v;
		}
	}

	model.multiplier = L / model.point_data[0].length;
	console.log('multiplier =',  model.multiplier);
}

function load_platform_data(model, data) {
	var k = (model.point_names.length + 1) * 2;
	k += (model.analog_names.length + 1) * 2;
	var lines = data.split('\n').slice(k);

	var N = parseInt(lines[0]);
	if (isNaN(N)) N = 0;
	console.log('platform number = ', N);

	model.platform_origin = new Array(N);
	model.platform_corner = new Array(N);
	for (let i=0; i<N; ++i) {
		var line = lines[1+i];
		var s = line.match(/\S+/g);

		var x = parseFloat(s[0]);
		var y = parseFloat(s[1]);
		var z = parseFloat(s[2]);
		model.platform_origin[i] = [x,y,z];
		console.log('origin = ', [x,y,z]);

		model.platform_corner[i] = new Array();
		for (let k=3; k<s.length; k+=3) {
			var x = parseFloat(s[k+0]);
			var y = parseFloat(s[k+1]);
			var z = parseFloat(s[k+2]);
			model.platform_corner[i].push([x,y,z]);
		}
		console.log('corner number = ', (s.length - 3) / 3);
	}
}

var screen_center = [canvas.width/2, canvas.height/2, 0];
var focal_length = 950;
var distance = 1000;
var rotation_matrix = null;
var model_scalar = 0.1;
var model_center = null;

function scene_rotation_matrix() {
	// model.point_data[i][j] = [-y,-z,x,miss];
//	var rp = [[0,-1,0],[0,0,-1],[1,0,0]];
	var rp = [[-1,0,0],[0,0,-1],[0,-1,0]];

	var c = Math.cos(angle_x);
	var s = Math.sin(angle_x);
	var rx = [[c,0,-s],[0,1,0],[s,0,c]];

	var c = Math.cos(-angle_y);
	var s = Math.sin(-angle_y);
	var ry = [[1,0,0],[0,c,s],[0,-s,c]];

	var r1 = transform(ry, transform(rx, transform(rp, [1,0,0])));
	var r2 = transform(ry, transform(rx, transform(rp, [0,1,0])));
	var r3 = transform(ry, transform(rx, transform(rp, [0,0,1])));
	rotation_matrix = transpose([r1,r2,r3]);
}

function perspective_project(point) {
	p = [...point];
	translate(p, model_center);
	scale(p, model_scalar);
	p = transform(rotation_matrix, p);
	translate(p, [0,0,distance]);
	project(p, focal_length);
	translate(p, screen_center);
	return p;
}

function build_scene(model) {
	model_center = [0, 0, 0];
	for (var i=0; i<model.point_data.length; i++)
		for (var j=0; j<model.point_data[i].length; j++) {
			if (model.point_data[i][j][3]) continue;
			translate(model_center, model.point_data[i][j]);
		}

	var length = model.point_data.length
	           * model.point_data[0].length;
	scale(model_center, -1/length);	// for subtraction
}

function draw() {
//	console.log('tick = ', tick);
	draw1();
	draw2();
}

function draw1() {
	ctx.clearRect(0, 0, canvas.width, canvas.height);
	scene_rotation_matrix();

	draw_platform_corner();
	draw_platform_center();

	draw_markers();
//	draw_points(model.point_names);
	if (display_mode_1 === 0) {
		var time_idx = (tick / model.multiplier) | 0;
//		draw_coordinates(pelvis_coordinates(time_idx));
		draw_coordinates(thigh_coordinates(time_idx));
		draw_coordinates(shank_coordinates(time_idx));
//		draw_coordinates(foot_coordinates(time_idx));
//		draw_coordinates(hip_coordinates(time_idx));
//		draw_coordinates(knee_coordinates(time_idx));
//		draw_coordinates(ankle_coordinates(time_idx));
	}
	else if (display_mode_1 === 1)
		draw_points(model.joint_names);
	else if (display_mode_1 === 2)
		draw_points(model.segment_names);
	else if (display_mode_1 === 3)
		draw_trajectory(model.body_center);
	else if (display_mode_1 === 4)
		draw_GRF();
	else if (display_mode_1 === 5)
		draw_COP();
	else if (display_mode_1 === 6) {
//		draw_joint_force('Right Ankle');
		draw_joint_force('Right Knee');
//		draw_joint_force('Right Hip');
	}

	draw_title1();
}

function draw_title1() {
	ctx.font = "16pt Verdana";
	ctx.textBaseline = "top";
	ctx.textAlign = "right";
	ctx.fillStyle = "black";
	ctx.fillText('🗎', canvas.width-10, 0);

	ctx.font = "12pt Verdana";
	ctx.textBaseline = "top";
	ctx.textAlign = "left";
	ctx.fillStyle = "white";
	var str = '';
	if      (display_mode_1 === 0)
		str = 'segment coordinate system';
	else if (display_mode_1 === 1)
		str = 'joint center';
	else if (display_mode_1 === 2)
		str = 'segment center';
	else if (display_mode_1 === 3)
		str = 'body center';
	else if (display_mode_1 === 4)
		str = 'ground reaction force';
	else if (display_mode_1 === 5)
		str = 'center of pressure';
	else if (display_mode_1 === 6)
		str = 'joint force';

	ctx.fillText(str, 0, 0);

	ctx.font = "12pt Verdana";
	ctx.textBaseline = "bottom";
	ctx.textAlign = "center";
	ctx.fillStyle = "gray";
	ctx.fillText("[←→] walk [wsad] rotate [1] mode", canvas.width/2, canvas.height);
}

function draw2() {
	ctx2.clearRect(0, 0, canvas2.width, canvas2.height);
	ctx2.save();
	ctx2.scale(1, -1);
	ctx2.translate(0, -canvas2.height/2);

	// 'Right Thigh', 'Right Shank', 'Right Foot',
	// 'Right Hip', 'Right Knee', 'Right Ankle'
	var point_idx = model.point_idx.get('Right Knee');

	draw_support();
	if (display_mode_2 === 0)
		draw_xyz_curves(model.euler_angles[point_idx], _scalar = 300);
	else if (display_mode_2 === 1)
		draw_xyz_curves(model.angular_velocity[point_idx], _scalar = 30 * model.frame_rate);
	else if (display_mode_2 === 2)
		draw_xyz_curves(model.angular_acceleration[point_idx], _scalar = 3 * model.frame_rate**2);

	ctx2.restore();
	draw_title2();
}

function draw_title2() {
	ctx2.font = "12pt Verdana";
	ctx2.textBaseline = "top";
	ctx2.textAlign = "left";
	ctx2.fillStyle = "white";
	var str = '';
	if      (display_mode_2 === 0) str = 'Euler angles';
	else if (display_mode_2 === 1) str = 'angular velocity';
	else if (display_mode_2 === 2) str = 'angular acceleration';
	str += ' of knee joint';
	ctx2.fillText(str, 0, 0);

	ctx2.font = "12pt Verdana";
	ctx2.textBaseline = "bottom";
	ctx2.textAlign = "center";
	ctx2.fillStyle = "gray";
	ctx2.fillText("[2] display mode", canvas2.width/2, canvas2.height);
}

function draw_markers() {
	var N = model.point_names.length;
	ctx.beginPath();
	for (let i=0; i<N; ++i) {
		var time_idx = (tick / model.multiplier) | 0;
		var p = model.point_data[i][time_idx];
		if (!p) continue;
		var p = perspective_project(p);
		ctx.moveTo(p[0], p[1]);
		ctx.arc(p[0], p[1], 2, 0, Math.PI * 2, true);
	}
	ctx.fillStyle = 'white';
	ctx.fill();
}

function parse(element) {
	var table = new Array();
	var div = document.getElementById(element);
	var lines = div.innerHTML.trim().split('\n');
	var first_line = true;
	for (let line of lines) {
		if (first_line) {first_line = false; continue;}
		if (line[0] === '-') continue;
		var row = line.split('|').map(s => s.trim().replace(/(^\w|\s\w)/g, s => s.toUpperCase()));
		table.push(row);
	}
	return table;
}

function load_table(table) {
	table.marker_names = parse("marker_names");
	console.log("marker names table =\n", ...table.marker_names);

	table.endpoints = parse("body_segment_endpoints");
	console.log("segment endpoints table =\n", ...table.endpoints);

	table.parameters = parse("body_segment_parameters");
	console.log("segment parameters table =\n", ...table.parameters);

	table.inverse_kinematics = parse("inverse_kinematics");
	console.log("inverse kinematics table =\n", ...table.inverse_kinematics);
}

function rebuild_marker_names(model, table) {
	for (let row of table.marker_names) {
		let [default_name, name, description] = row;
		if (default_name === "" || name === "") continue;
		var idx = model.point_names.indexOf(name);
		if (idx === -1) continue;
		model.point_names[idx] = default_name;
	}

	var N = model.point_names.length;
	model.point_idx.clear();
	for (let i=0; i<N; ++i)
		model.point_idx.set(model.point_names[i], i);
	console.log('point_idx remapping = ', model.point_idx);
}
/*
// 少了左右ankle和ground
function build_centers(model, table) {
	var idx = model.point_idx.size;
	model.center_names = new Array();
	for (let row of table.endpoints)
		for (let name of row) {
			var point_idx = model.point_idx.get(name);
			if (point_idx === undefined) {
				model.point_idx.set(name, idx++);
				model.center_names.push(name);
			}
		}
	console.log('center names = ', model.center_names);
}
*/

function build_centers(model) {
	model.joint_names = [
		'Left Hip',   'Right Hip', 'Greater Trochanter',
		'Left Knee',  'Right Knee',
		'Left Ankle', 'Right Ankle',
		'Left GH',    'Right GH', 'Middle GH',
		'Left Elbow', 'Right Elbow',
		'Ear Canal'
	];
	console.log('joint names = ', model.joint_names);

	model.segment_names = [
		'Left Thigh', 'Right Thigh',
		'Left Shank', 'Right Shank',
		'Left Foot', 'Right Foot',
		'Left Upper Arm', 'Right Upper Arm',
		'Left Forearm', 'Right Forearm',
		'Head And Neck', 'Trunk', 'Pelvis'
	];
	console.log('segment names = ', model.segment_names);

	model.center_names = [
		...model.joint_names,
		...model.segment_names,
		'Left Ground', 'Right Ground'
	];
	console.log('center number = ', model.center_names.length);

	var idx = model.point_idx.size;
	for (let name of model.center_names)
		model.point_idx.set(name, idx++);
	console.log('point_idx extended size = ', model.point_idx.size);

	var L = model.point_data[0].length;
	var N = model.point_names.length;
	var M = model.center_names.length;
	model.point_data.length = N+M;
	for (let i=N; i<N+M; ++i)
		model.point_data[i] = new Array(L);
	console.log('point_data extended length = ', model.point_data.length);
}

function get_marker_point(time_idx, name) {
	var point_idx = model.point_idx.get(name);
//	console.log('point_idx = ', point_idx);
	if (point_idx === undefined) return [Infinity, Infinity, Infinity];
	var point = model.point_data[point_idx][time_idx];
//	console.log('point = ', point);
	return point;
}

function set_marker_point(time_idx, name, point) {
	var point_idx = model.point_idx.get(name);
	model.point_data[point_idx][time_idx] = point;
}

function get_marker_midpoint(time_idx, marker1, marker2) {
	var p1 = get_marker_point(time_idx, marker1);
	var p2 = get_marker_point(time_idx, marker2);
	return mid(p1, p2);
}

function build_joint_centers_i(time_idx) {
	// hip
	var LASI = get_marker_point(time_idx, 'LASI');
	var RASI = get_marker_point(time_idx, 'RASI');
	var MASI = mid(LASI, RASI);
	var interASIS = dist(LASI, RASI);
	var [t, R] = pelvis_coordinates(time_idx);
	var R = transpose(R);	// to column-vector
	var L = add(MASI, transform(R, sca([-0.19, -0.3, -0.36], interASIS)));
	var R = add(MASI, transform(R, sca([-0.19, -0.3, +0.36], interASIS)));
	set_marker_point(time_idx, 'Left Hip', L);
	set_marker_point(time_idx, 'Right Hip', R);

	// knee
	var L = get_marker_midpoint(time_idx, 'LLFC', 'LMFC');
	var R = get_marker_midpoint(time_idx, 'RLFC', 'RMFC');
	set_marker_point(time_idx, 'Left Knee', L);
	set_marker_point(time_idx, 'Right Knee', R);

	// ankle
	var L = get_marker_midpoint(time_idx, 'LLMA', 'LMMA');
	var R = get_marker_midpoint(time_idx, 'RLMA', 'RMMA');
	set_marker_point(time_idx, 'Left Ankle', L);
	set_marker_point(time_idx, 'Right Ankle', R);

	// glenohumeral (shoulder)
	// 受試者肩半徑：左側 52.52 mm，右側 50.93 mm
	var MSAP = get_marker_midpoint(time_idx, 'LSAP', 'RSAP');
	var MASI = get_marker_midpoint(time_idx, 'LASI', 'RASI');
	var MPSI = get_marker_midpoint(time_idx, 'LPSI', 'RPSI');
	var ztrunk = normalize(sub(MSAP, mid(MASI, MPSI)));
	var LSAP = get_marker_point(time_idx, 'LSAP');
	var RSAP = get_marker_point(time_idx, 'RSAP');
	var L = sub(LSAP, sca(ztrunk, 52.52));
	var R = sub(RSAP, sca(ztrunk, 50.93));
	var M = mid(LSAP, RSAP);
	set_marker_point(time_idx, 'Left GH', L);
	set_marker_point(time_idx, 'Right GH', R);
	set_marker_point(time_idx, 'Middle GH', M);

	// elbow
	var L = get_marker_midpoint(time_idx, 'LRM', 'LUM');
	var R = get_marker_midpoint(time_idx, 'RRM', 'RUM');
	set_marker_point(time_idx, 'Left Elbow', L);
	set_marker_point(time_idx, 'Right Elbow', R);

	// greater trochanter (middle hip)
	var M = get_marker_midpoint(time_idx, 'LTRO', 'RTRO');
	set_marker_point(time_idx, 'Greater Trochanter', M);

	// ear canal (head)
	var M = get_marker_midpoint(time_idx, 'LHead', 'RHead');
	set_marker_point(time_idx, 'Ear Canal', M);
}

function build_joint_centers(model, table) {
	var L = model.point_data[0].length;
	for (let i=0; i<L; ++i)
		build_joint_centers_i(i);
}

function get_ratio(name, table, column) {
	var str = name.replace(/^(Left|Right)\s+/, '');
	for (let i=0; i<table.length; i++)
		if (str == table[i][0])
			return parseFloat(table[i][column]);
	return undefined;
}

function build_segment_centers(model, table) {
	var L = model.point_data[0].length;
	for (let i=0; i<L; ++i) {
		for (let row of table.endpoints) {
			var [name, prox_name, dist_name] = row;
			var prox = get_marker_point(i, prox_name);
			var dist = get_marker_point(i, dist_name);
			var ratio = get_ratio(name, table.parameters, 1);
			if (!prox || !dist || !ratio) continue;
			var point = lerp(prox, dist, ratio);
			set_marker_point(i, name, point);
		}
	}
}

function build_body_center(model, table) {
	var L = model.point_data[0].length;
	model.body_center = new Array(L);
	for (let i=0; i<L; ++i) {
		var sum = [0,0,0];
		var weight_sum = 0;
		for (let row of table.endpoints) {
			var [name, prox_name, dist_name] = row;
			var point = get_marker_point(i, name);
			var ratio = get_ratio(name, table.parameters, 2);
			if (!point || !ratio) continue;
			if (Math.abs(point[0]) === Infinity) continue;	// miss
			sum = add(sum, sca(point, ratio));
			weight_sum += ratio;
		}
		model.body_center[i] = sca(sum, 1/weight_sum);
	}
//	console.log('mass center = ', model.body_center);
}

function draw_points(names) {
	if (names === undefined) return;
	ctx.beginPath();
	for (let name of names) {
		var time_idx = (tick / model.multiplier) | 0;
		var p = get_marker_point(time_idx, name);
		if (!p) continue;
		var p = perspective_project(p);
		ctx.moveTo(p[0], p[1]);
		ctx.arc(p[0], p[1], 3, 0, Math.PI * 2, true);
	}
	ctx.fillStyle = 'gold';
	ctx.fill();
}

function draw_trajectory(points) {
	if (points === undefined) return;
	var head = true;
	ctx.beginPath();
	for (let point of points) {
		if (!point) continue;
		var p = perspective_project(point);
		if (head) ctx.moveTo(p[0], p[1]), head = false;
		else ctx.lineTo(p[0], p[1]);
	}
	ctx.lineWidth = 2;
	ctx.setLineDash([6,1]);
	ctx.strokeStyle = 'gold';
	ctx.stroke();
	ctx.setLineDash([]);

	ctx.beginPath();
	var time_idx = (tick / model.multiplier) | 0;
	var p = model.body_center[time_idx];
	var p = perspective_project(p);
	ctx.moveTo(p[0], p[1]);
	ctx.arc(p[0], p[1], 3, 0, Math.PI * 2, true);
	ctx.fillStyle = 'gold';
	ctx.fill();
}

function pelvis_coordinates(time_idx) {
	var RASI = get_marker_point(time_idx, 'RASI');
	var LASI = get_marker_point(time_idx, 'LASI');
	var RPSI = get_marker_point(time_idx, 'RPSI');
//	console.log('RASI = ', RASI);
//	console.log('LASI = ', LASI);
//	console.log('RPSI = ', RPSI);
	var o = [...RASI];
	var z = normalize(sub(RASI, LASI));
	var y = normalize(cross(z, sub(RASI, RPSI)));
	var x = cross(y,z);
//	console.log('axis = ', o, x, y, z);
	return [o,[x,y,z]];
}

function thigh_coordinates(time_idx) {
	var RTRO = get_marker_point(time_idx, 'RTRO');
	var RLFC = get_marker_point(time_idx, 'RLFC');
	var RMFC = get_marker_point(time_idx, 'RMFC');
//	console.log('RTRO = ', RTRO);
//	console.log('RLFC = ', RLFC);
//	console.log('RMFC = ', RMFC);
	var o = [...RTRO];
	var z = normalize(sub(RLFC, RMFC));
	var x = normalize(cross(sub(RTRO, RLFC), z));
	var y = cross(z,x);
//	console.log('axis = ', o, x, y, z);
	return [o,[x,y,z]];
}

function shank_coordinates(time_idx) {
	var RTT  = get_marker_point(time_idx, 'RTT');
	var RSHA = get_marker_point(time_idx, 'RSHA');
	var RMMA = get_marker_point(time_idx, 'RMMA');
	var RLMA = get_marker_point(time_idx, 'RLMA');
//	console.log('RTT  = ', RTT );
//	console.log('RSHA = ', RSHA);
//	console.log('RMMA = ', RMMA);
//	console.log('RLMA = ', RLMA);
	var o = [...RTT];
	var x = normalize(cross(sub(RSHA, RMMA), sub(RLMA, RMMA)));
	var z = normalize(cross(x, sub(RTT, mid(RMMA, RLMA))));
	var y = cross(z,x);
//	console.log('axis = ', o, x, y, z);
	return [o,[x,y,z]];
}

function foot_coordinates(time_idx) {
	var RFOO = get_marker_point(time_idx, 'RFOO');
	var RTOE = get_marker_point(time_idx, 'RTOE');
	var RHEE = get_marker_point(time_idx, 'RHEE');
//	console.log('RFOO = ', RFOO);
//	console.log('RTOE = ', RTOE);
//	console.log('RHEE = ', RHEE);
	var o = [...RHEE];
	var x = normalize(sub(mid(RFOO, RTOE), RHEE));
	var y = normalize(cross(x, sub(RFOO, RTOE)));
	var z = cross(x,y);
//	console.log('axis = ', o, x, y, z);
	return [o,[x,y,z]];
}

function hip_coordinates(time_idx) {
	var t = get_marker_point(time_idx, 'Right Hip');

	// Rp Rd R are row-vector for convenience
	var [tp, Rp] = pelvis_coordinates(time_idx);	// proximal
	var [td, Rd] = thigh_coordinates(time_idx);	// distal

	// R = Rpᵀ Rd (column-vector)
//	var R = transpose(mulAB(Rp, transpose(Rd)));
//	var R = mulAB(Rd, transpose(Rp));
	var R = dcm(Rp, Rd);
	return [t, R];
}

function knee_coordinates(time_idx) {
	var t = get_marker_point(time_idx, 'Right Knee');
	var [tp, Rp] = thigh_coordinates(time_idx);
	var [td, Rd] = shank_coordinates(time_idx);
	var R = dcm(Rp, Rd);
	return [t, R];
}

function ankle_coordinates(time_idx) {
	var t = get_marker_point(time_idx, 'Right Ankle');
	var [tp, Rp] = shank_coordinates(time_idx);
	var [td, Rd] = foot_coordinates(time_idx);
	var R = dcm(Rp, Rd);
	return [t, R];
}

function draw_coordinates(coords) {
	var [o,[x,y,z]] = coords;

	const axis_length = 300;
	scale(x, axis_length); translate(x, o);
	scale(y, axis_length); translate(y, o);
	scale(z, axis_length); translate(z, o);
	o = perspective_project(o);
	x = perspective_project(x);
	y = perspective_project(y);
	z = perspective_project(z);
//	console.log('projected axis = ', o, x, y, z);

	var axis = [x,y,z];
	var colors = ['#FF3838CC', '#38FF38CC', '#3838FFCC'];
	for (let i=0; i<3; ++i) {
		let p = axis[i];
		ctx.strokeStyle = colors[i];
		ctx.lineWidth = 3;
		ctx.beginPath();
		ctx.moveTo(o[0], o[1]);
		ctx.lineTo(p[0], p[1]);
		ctx.stroke();
	}
}

function Euler_angles_intrinsic_zxy(R) {
	// row-vector R
	// https://www.geometrictools.com/Documentation/EulerAngles.pdf
	// https://eecs.qmul.ac.uk/~gslabaugh/publications/euler.pdf
	if (R[1][2] == +1) return [+Math.PI/2, 0, +Math.atan2(-R[2][0], R[0][0])];
	if (R[1][2] == -1) return [-Math.PI/2, 0, -Math.atan2(-R[2][0], R[0][0])];

	var θx = Math.asin(R[1][2]);
	var θz = Math.atan2(-R[1][0], R[1][1]);
	var θy = Math.atan2(-R[0][2], R[2][2]);
	return [θx, θy, θz];
}

function rate(pos) {
	var L = pos.length;
	var vel = new Array(L);
	var acc = new Array(L);

	// moving regression of 31 points
	// with 5th-degree polynomial function
	// f(x) = c₀x⁰ + c₁x¹ + ... + c₅x⁵
	// least squares method min ‖Xc - f‖²
	// overdetermined system Ax = b for min ‖Ax - b‖²
	// normal equation x = (Aᵀ A)⁻¹ Aᵀ b
	// pseudoinverse A⁺ = (Aᵀ A)⁻¹ Aᵀ
	const range = 15;	// 15*2+1 = 31

	var A = [];
	for (let i=-range; i<=+range; ++i)
		A.push([i**0, i**1, i**2, i**3, i**4, i**5]);
	var Apinv = pinv(A);

	for (let n=+range; n<L-range; ++n) {
		vel[n] = [0,0,0];
		acc[n] = [0,0,0];

		for (let d=0; d<3; ++d) {
			var b = [];
			for (let i=-range; i<=+range; ++i)
				b.push(pos[n+i][d]);

			// f(x)  = c₀x⁰ + c₁x¹ + ... + c₅x⁵
			// fʹ(x) = 1⋅c₁x⁰ + 2⋅c₂x¹ + ... + 5⋅c₅x⁴
			// fʺ(x) = 1⋅2⋅c₂x⁰ + 2⋅3⋅c₃x¹ + ... + 4⋅5⋅c₅x³
			// where x = 0
			var c = mulAx(Apinv, b);
			var f0 = 0, fʹ0 = 0, fʺ0 = 0;
			for (let i=0; i<=5; ++i) f0  += c[i];
			for (let i=1; i<=5; ++i) fʹ0 += c[i] * i;
			for (let i=2; i<=5; ++i) fʺ0 += c[i] * i * (i-1);
			vel[n][d] = fʹ0;
			acc[n][d] = fʺ0;
		}
	}
	return [vel, acc];
}

function rate_convert(euler_angles_position, euler_angles_velocity, euler_angles_acceleration) {
	var L = euler_angles_position.length;
	var angular_velocity = new Array(L);
	var angular_acceleration = new Array(L);

	for (let i=0; i<L; ++i) {
		var θ = euler_angles_position[i];
		var c0 = Math.cos(θ[0]);
		var s0 = Math.sin(θ[0]);
		var c1 = Math.cos(θ[1]);
		var s1 = Math.sin(θ[1]);

		var v = euler_angles_velocity[i];
		if (!v) continue;
		var dv = [[c1,0,-c0*s1],[0,1,s0],[s1,0,c0*c1]];
		var ω = transform(dv, v);
		angular_velocity[i] = ω;

		var a = euler_angles_acceleration[i];
		if (!a) continue;
		var [v0, v1, v2] = v;
		var da = [[-v1*s1,0,v0*s0*s1-v1*c0*c1],[0,0,v0*c0],[v1*c1,0,-v0*s0*c1-v1*c0*s1]];
		var α = add(transform(da, v), transform(dv, a));
		angular_acceleration[i] = α;
	}
	return [angular_velocity, angular_acceleration];
}

function build_center_translational_rate() {
	var N = model.point_idx.size;
	model.velocity     = new Array(N);
	model.acceleration = new Array(N);

	var names = ['Right Thigh', 'Right Shank', 'Right Foot'];
	for (let name of names) {
		var point_idx = model.point_idx.get(name);
		if (point_idx === undefined) continue;

		var [vel, acc] = rate(model.point_data[point_idx]);
		model.velocity[point_idx]     = vel;
		model.acceleration[point_idx] = acc;
	}
}

function build_center_angular_rate() {
	var N = model.point_idx.size;
	model.euler_angles         = new Array(N);
	model.angular_velocity     = new Array(N);
	model.angular_acceleration = new Array(N);

	var names = [
		'Right Thigh', 'Right Shank', 'Right Foot',
		'Right Hip', 'Right Knee', 'Right Ankle'
	];
	var coordinates = [
		thigh_coordinates, shank_coordinates, foot_coordinates,
		hip_coordinates, knee_coordinates, ankle_coordinates
	];
	for (let n=0; n<names.length; ++n) {
		var point_idx = model.point_idx.get(names[n]);
		if (point_idx === undefined) continue;

		// coordinate system -> Euler angles
		// -> Euler angles rate -> angular rate
		var L = model.point_data[0].length;
		var pos = new Array(L);
		for (let i=0; i<L; ++i) {
			var [t, R] = coordinates[n](i);
			pos[i] = Euler_angles_intrinsic_zxy(R);
		}
		var [vel, acc] = rate(pos);
		var [vel, acc] = rate_convert(pos, vel, acc);
		model.euler_angles[point_idx]         = pos;
		model.angular_velocity[point_idx]     = vel;
		model.angular_acceleration[point_idx] = acc;
	}
}

function draw_support() {
	// horizontal line for axis
	ctx2.strokeStyle = 'gray';
	ctx2.lineWidth = 1;
	ctx2.beginPath();
	ctx2.moveTo(0, 0);
	ctx2.lineTo(canvas2.width, 0);
	ctx2.stroke();

	// vertical line for guide
	var x = tick * (3/4) / model.multiplier;
	ctx2.strokeStyle = 'lightgray';
	ctx2.lineWidth = 3;
	ctx2.beginPath();
	ctx2.moveTo(x, +canvas.height/2);
	ctx2.lineTo(x, -canvas.height/2);
	ctx2.stroke();
}

function draw_xyz_curves(signals, scalar) {
	var colors = ['#FF3838', '#38FF38', '#3838FF'];
	for (let d=0; d<3; ++d) {
		ctx2.beginPath();
		var head = true;
		for (let i=0; i<signals.length; ++i) {
			var data = signals[i];
			if (!data) continue;
			var x = i * (3/4);
			var y = data[d] / Math.PI * scalar;
			if (head) ctx2.moveTo(x, y), head = false;
			ctx2.lineTo(x, y);
		}
		ctx2.lineWidth = 2;
		ctx2.strokeStyle = colors[d];
		ctx2.stroke();
	}
}

function draw_platform_corner() {
	ctx.beginPath();
	for (let n=0; n<model.platform_corner.length; ++n) {
		var L = model.platform_corner[n].length;
		var p = model.platform_corner[n][L-1];
		var p = perspective_project(p);
		ctx.moveTo(p[0], p[1]);
		for (let i=0; i<L; ++i) {
			var p = model.platform_corner[n][i];
			var p = perspective_project(p);
			ctx.lineTo(p[0], p[1]);
		}
	}
	ctx.strokeStyle = 'gold';
	ctx.lineWidth = 2;
	ctx.stroke();
}

function build_platform_coordinates() {
	var N = model.platform_corner.length;
	model.platform_coordinates = new Array(N);
	for (let n=0; n<N; ++n) {
		var corners = model.platform_corner[n];
		var o = avg(corners);
		var x = normalize(sub(corners[0], corners[1]));
		var y = normalize(sub(corners[0], corners.at(-1)));
		var z = cross(x, y);
		model.platform_coordinates[n] = [o,[x,y,z]];
	}
}

function draw_platform_center() {
	ctx.beginPath();
	for (var i=0; i<model.platform_coordinates.length; i++) {
		var [t, R] = model.platform_coordinates[i];
		var p = perspective_project(t);
		ctx.moveTo(p[0], p[1]);
		ctx.arc(p[0], p[1], 2, 0, Math.PI * 2, true);
	}
	ctx.fillStyle = 'gold';
	ctx.fill();
}

function get_analog_signal(time_idx, name) {
	var analog_idx = model.analog_idx.get(name);
	var signal = model.analog_data[analog_idx][time_idx];
	return signal;
}

function build_platform_forces(model) {
	if (model.platform_coordinates.length === 0) return;
	var N = model.platform_coordinates.length;
	var L = model.analog_data[0].length;
	model.platform_force  = new Array(N);
	model.platform_torque = new Array(N);
	for (let n=0; n<N; n++) {
		var [t, R] = model.platform_coordinates[n];

		// zero-torque position (global coordinates)
		var c = model.platform_origin[n];
		var c = sub(t, transform(R, c));
		model.platform_origin[n] = c;

		// force/torque vector (global coordinates)
		model.platform_force[n]  = new Array(L);
		model.platform_torque[n] = new Array(L);
		for (let i=0; i<L; ++i) {
			var fx = get_analog_signal(i, 'Fx'+(n+1));
			var fy = get_analog_signal(i, 'Fy'+(n+1));
			var fz = get_analog_signal(i, 'Fz'+(n+1));
			var tx = get_analog_signal(i, 'Mx'+(n+1));
			var ty = get_analog_signal(i, 'My'+(n+1));
			var tz = get_analog_signal(i, 'Mz'+(n+1));
			var f = transform(R, [fx,fy,fz]);
			var t = transform(R, [tx,ty,tz]);
			model.platform_force[n][i]  = f;
			model.platform_torque[n][i] = t;
		}
	}
//	console.log('platform_force = ', model.platform_force);
}

function build_platform_COP(model) {
	if (model.platform_coordinates.length === 0) return;
	var N = model.platform_coordinates.length;
	var L = model.analog_data[0].length;
	model.platform_COP = new Array(N);
	for (let n=0; n<N; ++n) {
		model.platform_COP[n] = new Array(L);
		for (let i=0; i<L; ++i) {
			var origin = model.platform_origin[n];
			var force  = model.platform_force[n][i];
			var torque = model.platform_torque[n][i];
//			if (i == 420) console.log('420 force = ', force);
			model.platform_COP[n][i] = undefined;
			if (force[2] == 0) continue;
			torque = add(torque, cross(origin, force));
			var z = 0;
			var x = (z * force[0] - torque[1]) / force[2];
			var y = (torque[0] + z * force[1]) / force[2];
			model.platform_COP[n][i] = [x,y,z];
		}
	}
}

function draw_GRF() {
	var N = model.platform_coordinates.length;
	const unit_length = 1;
	ctx.beginPath();
	for (let n=0; n<N; ++n) {
//		var [t, R] = model.platform_coordinates[n];
		var t = model.platform_COP[n][tick];
		var f = model.platform_force[n][tick];
		if (t === undefined) continue;
		var p1 = t;
		var p2 = add(p1, sca(f, unit_length));
		var p1 = perspective_project(p1);
		var p2 = perspective_project(p2);
		if (dist(p1, p2) < .5) continue;
		ctx.moveTo(p1[0], p1[1]);
		ctx.lineTo(p2[0], p2[1]);
	}
	ctx.strokeStyle = 'gold';
	ctx.lineCap = 'round';
	ctx.lineWidth = 4;
	ctx.stroke();
	ctx.lineCap = 'butt';
}

function build_COP(model) {
	if (model.platform_coordinates.length === 0) return;
	var N = model.platform_coordinates.length;
	var L = model.analog_data[0].length;
	model.COP_point = new Array(L);
	model.COP_force = new Array(L);
	for (let i=0; i<L; ++i) {
		var force  = [0,0,0];
		var torque = [0,0,0];
		for (let n=0; n<N; ++n) {
			var c = model.platform_origin[n];
			var f = model.platform_force[n][i];
			var t = model.platform_torque[n][i];
			force  = add(force,  f);
			torque = add(torque, t);
			torque = add(torque, cross(c, f));
		}

		var z = 0;
		var x = (z * force[0] - torque[1]) / force[2];
		var y = (torque[0] + z * force[1]) / force[2];
		model.COP_point[i] = [x,y,z];
		model.COP_force[i] = force;
	}
}

function draw_COP() {
	const unit_length = 1;
	ctx.beginPath();
	var p1 = model.COP_point[tick];
	var f  = model.COP_force[tick];
	var p2 = add(p1, sca(f, unit_length));
	var p1 = perspective_project(p1);
	var p2 = perspective_project(p2);
	if (dist(p1, p2) < .5) return;
	ctx.moveTo(p1[0], p1[1]);
	ctx.lineTo(p2[0], p2[1]);
	ctx.strokeStyle = 'gold';
	ctx.lineCap = 'round';
	ctx.lineWidth = 4;
	ctx.stroke();
	ctx.lineCap = 'butt';
}

function inverse_kinematics_i(time_idx, dist_name, seg_name, prox_name) {
	var seg_idx  = model.point_idx.get(seg_name);
	var prox_idx = model.point_idx.get(prox_name);
	var dist_idx = model.point_idx.get(dist_name);

	// interpolation is not implemented
	var idx = (time_idx / model.multiplier) | 0;
	var prox = get_marker_point(prox_name, idx);
	var dis  = get_marker_point(dist_name, idx);
//	if (time_idx == 420) console.log('prox = ', prox);
//	if (time_idx == 420) console.log('dist = ', dis);
	if (!prox || !dis) return;

	// segment
	var acc = model.acceleration[seg_idx][idx];
	if (!acc) return;
//	var aacc = model.angular_acceleration[seg_idx][idx];
//	if (!acc || !aacc) return;

	var mass = get_ratio(seg_name, table.parameters, 2);
	var gravity = [0,0,-0.00980665];
	var seg_force = sca(add(acc, gravity), mass);

	// distal endpoint
	var dist_force  = model.forces [dist_idx][time_idx];
	if (!dist_force) return;

	// proximal endpoint
	var prox_force  = add(seg_force,  dist_force);
	model.forces [prox_idx][time_idx] = prox_force;
}

function inverse_kinematics(dist_name, seg_name, prox_name) {
	var point_idx = model.point_idx.get(prox_name);
	var L = model.analog_data[0].length;
	model.forces[point_idx]  = new Array(L);
	for (let i=0; i<L; ++i)
		inverse_kinematics_i(i, dist_name, seg_name, prox_name);
}

function build_joint_forces(model) {
	if (model.platform_coordinates.length === 0) return;
	var N = model.point_idx.size;
	model.forces  = new Array(N);

	var L = model.analog_data[0].length;
	var R_point_idx = model.point_idx.get('Right Ground');
	model.forces[R_point_idx]  = new Array(L);

	const R_platform_idx = 0;	// manual parameter
	for (let i=0; i<L; ++i) {
		var time_idx = (i / model.multiplier) | 0;
		if (time_idx * model.multiplier == i) {
			model.point_data[R_point_idx][time_idx] = model.platform_COP[R_platform_idx][i];
		}

		model.forces[R_point_idx][i] = model.platform_force[R_platform_idx][i];
	}

	// inverse_kinematics('Right Ground', 'Right Foot', 'Right Ankle');
	for (let row of table.inverse_kinematics) {
		var [dist_name, seg_name, prox_name] = row;
		inverse_kinematics(dist_name, seg_name, prox_name);
	}
}

function draw_joint_force(name) {
	const unit_length = 1;
	ctx.beginPath();
	var point_idx = model.point_idx.get(name);
	var time_idx = (tick / model.multiplier) | 0;
	var p1 = model.point_data[point_idx][time_idx];
	var f  = model.forces[point_idx][tick];
	if (f === undefined) return;
	var p2 = add(p1, sca(f, unit_length));
	var p1 = perspective_project(p1);
	var p2 = perspective_project(p2);
	if (dist(p1, p2) < .5) return;
	ctx.moveTo(p1[0], p1[1]);
	ctx.lineTo(p2[0], p2[1]);
	ctx.strokeStyle = 'gold';
	ctx.lineCap = 'round';
	ctx.lineWidth = 4;
	ctx.stroke();
	ctx.lineCap = 'butt';
}

// 3D vector operation

function add(a, b) {
	return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
}

function sub(a, b) {
	return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
}

function mid(a, b) {
	return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2];
}

function sca(a, k) {
	return [a[0] * k, a[1] * k, a[2] * k];
}

function avg(arr) {
	var sum = [0,0,0];
	for (let i=0; i<arr.length; ++i)
		sum = add(sum, arr[i]);
	return sca(sum, 1/arr.length);
}

function lerp(a, b, k) {
	return [
		a[0] + (b[0] - a[0]) * k,
		a[1] + (b[1] - a[1]) * k,
		a[2] + (b[2] - a[2]) * k
	];
}

function dot(a, b) {
	return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}

function cross(a, b) {
	return [
		a[1] * b[2] - b[1] * a[2],
		a[2] * b[0] - b[2] * a[0],
		a[0] * b[1] - b[0] * a[1]
	];
}

function dist(a, b) {
	return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
}

function length(a) {
	return Math.hypot(a[0], a[1], a[2]);
}

function normalize(a) {
	var l = length(a);
	if (l <= 0) return [0,0,0];
	return [a[0]/l, a[1]/l, a[2]/l];
}

function normal(o, a, b) {
	var p = [o[0] - a[0], o[1] - a[1], o[2] - a[2]];
	var q = [o[0] - b[0], o[1] - b[1], o[2] - b[2]];
	var r = [0, 0, 0];
	r[0] = p[1] * q[2] - p[2] * q[1];
	r[1] = p[2] * q[0] - p[0] * q[2];
	r[2] = p[0] * q[1] - p[1] * q[0];
	var l = length(r);
	if (l <= 0) return r;
	r[0] /= l;
	r[1] /= l;
	r[2] /= l;
	return r;
}

function transform(m, p) {
	return [
		m[0][0] * p[0] + m[0][1] * p[1] + m[0][2] * p[2],
		m[1][0] * p[0] + m[1][1] * p[1] + m[1][2] * p[2],
		m[2][0] * p[0] + m[2][1] * p[1] + m[2][2] * p[2]
	];
}

function dcm(a, b) {
	// row-vector a b return
	return [
		[dot(a[0],b[0]), dot(a[1],b[0]), dot(a[2],b[0])],
		[dot(a[0],b[1]), dot(a[1],b[1]), dot(a[2],b[1])],
		[dot(a[0],b[2]), dot(a[1],b[2]), dot(a[2],b[2])],
	];
}

// camera operation

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

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

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;
}

// matrix operation

function print(A) {
	var str = '[';
	for (let i=0; i<A.length; ++i) {
		if (i) str += ';';
		for (let j=0; j<A[0].length; ++j)
			str += ' ' + A[i][j];
	}
	str += ']';
	console.log(str);
}

function zero_matrix(n, m) {
	var C = new Array(n);
	for (let i=0; i<n; ++i)
		C[i] = new Array(m).fill(0);
	return C;
}

function transpose(A) {
	var C = zero_matrix(A[0].length, A.length);
	for (let i=0; i<A.length; ++i)
		for (let j=0; j<A[0].length; ++j)
			C[j][i] = A[i][j];
	return C;
}

function mulAx(A, x) {
	var y = new Array(A.length).fill(0);
	for (let i=0; i<A.length; ++i)
		for (let j=0; j<x.length; ++j)
			y[i] += A[i][j] * x[j];
	return y;
}

function mulAB(A, B) {
	var C = zero_matrix(A.length, B[0].length);
	for (let i=0; i<A.length; ++i)
		for (let j=0; j<B[0].length; ++j)
			for (let k=0; k<B.length; ++k)
				C[i][j] += A[i][k] * B[k][j];
	return C;
}

function inv(A) {
	// Gauss—Jordan elimination
	var n = A.length;
	var argument = new Array(n).fill(0);
	for (let i=0; i<n; ++i) A[i].push(...argument);
	for (let i=0; i<n; ++i) A[i][n+i] = 1;

	for (let i=0; i<n; ++i) {
		if (A[i][i] == 0)
			for (let j=i+1; j<n; ++j)
				if (A[j][i] != 0) {
					for (let k=i; k<n*2; ++k)
						[A[i][k], A[j][k]] = [A[j][k], A[i][k]];
					break;
				}
		if (A[i][i] == 0) return undefined;

		var t = A[i][i];
		for (let k=i; k<n*2; ++k)
			A[i][k] /= t;

		for (let j=0; j<n; ++j)
			if (i != j && A[j][i] != 0) {
				var t = A[j][i];
				for (let k=i; k<n*2; ++k)
					A[j][k] -= A[i][k] * t;
			}
	}

	for (let i=0; i<n; ++i) A[i].splice(0, n);
	return A;
}

function pinv(A) {
	// pseudoinverse A⁺ = (Aᵀ A)⁻¹ Aᵀ
	return mulAB(inv(mulAB(transpose(A),A)),transpose(A));
}

})();
</script>
<!--
https://course.ntu.edu.tw/en/courses/113-1/57212
https://www.nature.com/articles/s41597-019-0124-4
-->
<div id="marker_names" style="display:none;">
code | file  | description
-------------------------------------------（head頭）
LHead|       | 耳道 ear canal
RHead|       | 耳道 ear canal
-------------------------------------------（trunk軀幹）
C7T1 | CV7   | 脊骨頸椎第七節 7th cervical vertebra
     | TV10  | 脊骨胸椎第十節 10th thoracic vertebrae
     | SJN   | 胸骨頸切跡 jugular notch
     | SXS   | 胸骨劍突 xiphoid process
RSAP | R_SAE | 鎖骨肩峰 acromion
LSAP | L_SAE | 鎖骨肩峰 acromion
-------------------------------------------（upper arm上臂）
RRM  | R_HLE | 肱骨外髁 lateral humerus epicondyle
LRM  | L_HLE | 肱骨外髁 lateral humerus epicondyle
RUM  | R_HME | 肱骨內髁 medial humerus epicondyle
LUM  | L_HME | 肱骨內髁 medial humerus epicondyle
-------------------------------------------（forearm前臂）
*42  | R_RSP | 橈骨莖突 radius styloid process
*41  | L_RSP | 橈骨莖突 radius styloid process
RUS  | R_UHE | 尺骨莖突 ulna styloid process
LUS  | L_UHE | 尺骨莖突 ulna styloid process
-------------------------------------------（pelvis骨盆）
RASI | R_IAS | 髂骨前上棘 anterior superior iliac spine
LASI | L_IAS | 髂骨前上棘 anterior superior iliac spine
RPSI | R_IPS | 髂骨後上棘 posterior superior iliac spine
LPSI | L_IPS | 髂骨後上棘 posterior superior iliac spine
-------------------------------------------（thigh大腿）
RTRO | R_FTC | 股骨大轉子 greater trochanter
LTRO | L_FTC | 股骨大轉子 greater trochanter
RLFC | R_FLE | 股骨外髁 lateral femoral epicondyle
LLFC | L_FLE | 股骨外髁 lateral femoral epicondyle
RMFC | R_FME | 股骨內髁 medial femoral epicondyle
LMFC | L_FME | 股骨內髁 medial femoral epicondyle
-------------------------------------------（shank小腿）
RSHA | R_FAX | 腓骨頭 head of fibula
LSHA | L_FAX | 腓骨頭 head of fibula
RTT  | R_TTC | 脛骨粗隆 tibial tuberosity
LTT  | L_TTC | 脛骨粗隆 tibial tuberosity
RLMA | R_FAL | 腓骨外髁 lateral malleolus
LLMA | L_FAL | 腓骨外髁 lateral malleolus
RMMA | R_TAM | 脛骨內髁 medial malleolus
LMMA | L_TAM | 脛骨內髁 medial malleolus
-------------------------------------------（foot足）
RHEE | R_FCC | 跟骨粗隆 calcaneal tuberosity
LHEE | L_FCC | 跟骨粗隆 calcaneal tuberosity
RFOO | R_FM1 | 舟狀骨粗隆 navicular tuberosity
LFOO | L_FM1 | 舟狀骨粗隆 navicular tuberosity
RTOE | R_FM5 | 第五蹠骨頭 head of 5th metatarsus
LTOE | L_FM5 | 第五蹠骨頭 head of 5th metatarsus
RMTH | R_FM2 | 第二蹠骨頭 head of 2nd metatarsus
LMTH | L_FM2 | 第二蹠骨頭 head of 2nd metatarsus
</div>
<div id="body_segment_endpoints" style="display:none;">
Segment         | Proximal Marker Name | Distal Marker Name
-----------------------------------------------------------
Left Thigh      | LTRO                 | Left Knee
Right Thigh     | RTRO                 | Right Knee
Left Shank      | Left Knee            | LMMA
Right Shank     | Right Knee           | RMMA
Left Foot       | LLMA                 | LMTH
Right Foot      | RLMA                 | RMTH
-----------------------------------------------------------
Left Upper Arm  | Left GH              | Left Elbow
Right Upper Arm | Right GH             | Right Elbow
Left Forearm    | Left Elbow           | LUS
Right Forearm   | Right Elbow          | RUS
-----------------------------------------------------------
Head And Neck   | C7T1                 | Ear Canal
Trunk           | Greater Trochanter   | Middle GH
Pelvis          | L4L5                 | Greater Trochanter
</div>
<!--
https://www.d.umn.edu/~mlevy/CLASSES/ESAT3300/LABS/LAB8_COM/bsp.htm
http://holmeslab.ca/csb-scb/Software/dempster.pdf
https://pmc.ncbi.nlm.nih.gov/articles/PMC8730461/
-->
<div id="body_segment_parameters" style="display:none;">
segment       | length | mass  | gyration
-----------------------------------------
Head And Neck | .550   | .0896 | .297
Trunk         | .500   | .4684 | .406
Upper Arm     | .436   | .0325 | .322
Forearm       | .430   | .0187 | .303
Hand          | .468   | .0065 | .297
Thigh         | .433   | .1050 | .323
Shank         | .434   | .0475 | .302
Foot          | .500   | .0143 | .475
</div>
<div id="inverse_kinematics" style="display:none;">
distal joint | segment     | proximal joint
-------------------------------------------
Right Ground | Right Foot  | Right Ankle
Right Ankle  | Right Shank | Right Knee
Right Knee   | Right Thigh | Right Hip
</div>
</div>