<div class="canvas" name="Deformation">
<canvas id="Deformation" width="200" height="200"></canvas>
<script id="Deformation.js">
(function() {
	var canvas = document.getElementById('Deformation');
	var ctx    = canvas.getContext('2d');

	var x = [[4,5  ],[16,4 ],[9,8  ],[3,16 ],[8,14 ],
			 [17,11],[23,9 ],[8,19 ],[16,16],[23,14],
			 [5,24 ],[13,25],[19,22],[26,21],[29,16],
			 [13,30],[22,31],[28,27],[32,23],[38,19],
			 [30,32],[37,28]];
	scale(x, 5, -5);
	translate(x, 0, 200);
	var y = x;

	var edge = [[0,2],[1,2],[2,5],[5,6],[8,9],
				[9,6],[4,8],[2,4],[3,4],[3,7],
				[3,10],[0,3],[10,15],[7,11],[11,12],
				[12,13],[13,14],[6,14],[8,12],[9,13],
				[12,16],[14,18],[14,19],[11,15],[15,16],
				[16,17],[17,18],[18,19],[19,21],[20,21],
				[16,20],[17,20],[18,21],[1,6],[0,1]];
	var adj = GenerateAdjacencyMatrix(x, edge);

	// degree matrix D
	const N = x.length;
	var wsum = new Float32Array(N).fill(0);
	for (let i=0; i<N; ++i)
		for (let j=0; j<N; ++j)
			if (adj[i][j])
				wsum[i] += adj[i][j];

	// constant b
	var bx = new Float32Array(N).fill(0);
	var by = new Float32Array(N).fill(0);
	LaplacianConstant(x, adj, bx, by);

	// boundary condition (boolean array)
	var pin = new Int8Array(N).fill(false);
	pin[12] = true;

	// mouse control
	var hitpoint = -1;
	var hit = function(p, mouse) {
		return p[0] - 7 <= mouse[0] && p[0] + 7 >= mouse[0]
			&& p[1] - 7 <= mouse[1] && p[1] + 7 >= mouse[1];
	};
	var coordinate = function(x, y) {
		var s = 2;
		return [Math.round(x/s)*s, Math.round(y/s)*s];
	};

	canvas.ondblclick = function(e){
		var mouse = coordinate(e.offsetX, e.offsetY);
		for (var i=0; i<y.length; ++i)
			if (hit(y[i], mouse))
				pin[i] = !pin[i];
		draw(y);
	};

	canvas.onmousedown = function(e){
		var mouse = coordinate(e.offsetX, e.offsetY);
		hitpoint = -1;
		for (var i=0; i<y.length; ++i)
			if (hit(y[i], mouse))
				hitpoint = i; 
	};

	canvas.onmousemove = function(e){
		if (hitpoint == -1) return;
		x[hitpoint] = coordinate(e.offsetX, e.offsetY);

		var known = pin.slice();
		known[hitpoint] = true;
		y = LaplacianSolver(x, adj, wsum, known, bx, by);
		draw(y);
	};

	canvas.onmouseup = canvas.onmouseout = function(e){
		hitpoint = -1;

		x = y;
		LaplacianConstant(x, adj, bx, by);
	};

	function draw(p) {
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		DrawAdjacencyMatrix(ctx, p, adj, "rgb(255,192,0)");
		DrawPointArray(ctx, p, "rgb(0,0,0)");
		DrawPointArray(ctx, GeneratePointArray(p, pin), "rgb(198,0,0)");
	}

	function title() {
		ctx.font = "16pt Arial";
		ctx.textBaseline = "middle";
		ctx.textAlign = "center";
		ctx.fillStyle = "rgb(0,127,0)";
		ctx.fillText("drag to move", canvas.width/2, canvas.height/2-16);
		ctx.fillText("dbclick to pin/unpin", canvas.width/2, canvas.height/2+16);
	}

	draw(x);
	title();
})();

function LaplacianConstant(p, adj, bx, by) {
	const N = p.length;

	bx.fill(0);
	by.fill(0);
	for (let i=0; i<N; ++i)
		for (let j=0; j<N; ++j)
			if (adj[i][j]) {
				bx[i] += p[i][0] - p[j][0];
				by[i] += p[i][1] - p[j][1];
			}
}

function LaplacianSolver(p, adj, wsum, known, bx, by) {
	const N = p.length;

	// initial solution of Lx = b
	var q = new Array(p.length);
	for (let i=0; i<N; ++i) {
		if (known[i])
			q[i] = [p[i][0], p[i][1]];
		else
			q[i] = [0,0];
	}

	// Gauss–Seidel Iteration solve Lx = b
	const ε = 1e-6;
	for (let t=0; t<1000; ++t) {
		var error = 0;
		for (let i=0; i<N; ++i) {
			if (known[i]) continue;
			var xsum = 0, ysum = 0;
			for (let j=0; j<N; ++j)
				if (adj[i][j]) {
					xsum += adj[i][j] * q[j][0];
					ysum += adj[i][j] * q[j][1];
				}

			var xnew = (bx[i] + xsum) / wsum[i];
			var ynew = (by[i] + ysum) / wsum[i];
			error += Math.abs(xnew - q[i][0]);
			error += Math.abs(ynew - q[i][1]);
			q[i][0] = xnew;
			q[i][1] = ynew;
		}
		if (error < ε) break;
	}
	return q;
}

function scale(p, s, t) {
	for (var i=0; i<p.length; ++i) {
		p[i][0] *= s;
		p[i][1] *= t;
	}
}

function translate(p, x, y) {
	for (var i=0; i<p.length; ++i) {
		p[i][0] += x;
		p[i][1] += y;
	}
}

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 GeneratePointArray(p, list) {
	var arr = [];
	for (var i=0; i<list.length; ++i)
		if (list[i])
			arr.push(p[i]);
	return arr;
}

function GenerateAdjacencyMatrix(x, list) {
	var n = x.length;
	var adj = new Array(n);
	for (var i=0; i<n; ++i) adj[i] = new Array(n).fill(0);

	if (list) {
		for (var i=0; i<list.length; ++i) {
			var a = list[i][0];
			var b = list[i][1];
			adj[a][b] = adj[b][a] = 1;
		}
	} else	{
		for (var i=0; i<n; ++i)
			for (var j=0; j<n; ++j)
				adj[i][j] = 1;
	}
	return adj;
}

function DrawAdjacencyMatrix(ctx, p, adj, rgb) {
	ctx.strokeStyle = rgb;
	ctx.lineWidth = 2.25;
	ctx.beginPath();
	for (var i=0; i<p.length; ++i)
		for (var j=i+1; j<p.length; ++j)
			if (adj[i][j]) {
				ctx.moveTo(p[i][0], p[i][1]);
				ctx.lineTo(p[j][0], p[j][1]);
			}
	ctx.stroke();
}
</script>
</div><div class="canvas" name="Homotopy">
<canvas id="Homotopy" width="200" height="200"></canvas>
<script id="Homotopy.js">
(function() {
	var canvas = document.getElementById('Homotopy');
	var ctx    = canvas.getContext('2d');

	var x = [[4,5  ],[16,4 ],[9,8  ],[3,16 ],[8,14 ],
			 [17,11],[23,9 ],[8,19 ],[16,16],[23,14],
			 [5,24 ],[13,25],[19,22],[26,21],[29,16],
			 [13,30],[22,31],[28,27],[32,23],[38,19],
			 [30,32],[37,28]];
	scale(x, 5, -5);
	translate(x, 0, 200);

	var y = x.map(arr => arr.slice());
	F(y);
	function F(x) {
		// centering
		var n = x.length;
		var xc = [0,0];
		for (let i=0; i<n; ++i) xc = add(xc, x[i]);
		xc[0] /= n; xc[1] /= n;
		for (let i=0; i<n; ++i) x[i] = sub(x[i], xc);
		// sqrt then *10
		for (let i=0; i<n; ++i) {
			x[i][0] = Math.sqrt(Math.abs(x[i][0])) * 9 * Math.sign(x[i][0]);
			x[i][1] = Math.sqrt(Math.abs(x[i][1])) * 9 * Math.sign(x[i][1]);
		}
		// uncentering
		for (let i=0; i<n; ++i) x[i] = add(x[i], xc);
		// linearity magic
		x[1][0] += 66;
		x[14][1] -= 30;
	}

	var adj = Delaunay(x);
	var n = adj.length;

	var faces = [];
	for (let i=0; i<n; ++i)
	for (let j=i+1; j<n; ++j)
	for (let k=j+1; k<n; ++k)
		if (adj[i][j] && adj[j][k] && adj[k][i])
			faces.push([i,j,k]);

	// affine alignment
	var s = new Array(faces.length);
	var θ = new Array(faces.length);
	for (let i=0; i<faces.length; ++i) {
		var x0 = x[faces[i][0]]; var y0 = y[faces[i][0]];
		var x1 = x[faces[i][1]]; var y1 = y[faces[i][1]];
		var x2 = x[faces[i][2]]; var y2 = y[faces[i][2]];
		var V = basis(sub(x1, x0), sub(x2, x0));
		var W = basis(sub(y1, y0), sub(y2, y0));
		var A = mulmm(W, inv(V));
		[s[i], θ[i]] = polar(A);
	}

	var t = 0;
	var z = x.map(arr => arr.slice());
	var num = new Array(n).fill(0);

	function affine_interpolation(t) {
		for (let i=0; i<n; ++i) z[i][0] = z[i][1] = 0;
		for (let i=0; i<n; ++i) num[i] = 0;

		for (let i=0; i<faces.length; ++i) {
			for (let j=0; j<3; ++j) {
				var v = interpolate(x[faces[i][j]], t, s[i], θ[i], x[faces[i][0]], y[faces[i][0]]);
				z[faces[i][j]][0] += v[0];
				z[faces[i][j]][1] += v[1];
				num[faces[i][j]]++;
			}
		}
		for (let i=0; i<n; ++i) {
			z[i][0] /= num[i];
			z[i][1] /= num[i];
		}
	}

	canvas.tabIndex = 0;
	canvas.onkeydown = function(e) {
		if (e.keyCode == 90)	// z
			t = Math.max(0, t - 1);
		else if (e.keyCode == 88)	// x
			t = Math.min(100, t + 1);
		draw(z);
	};

	function draw(p) {
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		affine_interpolation(t / 100);
		DrawAdjacencyMatrix(ctx, p, adj, "rgb(255,192,0)");
		DrawPointArray(ctx, p, "rgb(0,0,0)");
		title();
	}

	function title() {
		ctx.font = "10pt Verdana";
		ctx.textBaseline = "top";
		ctx.textAlign = "center";
		ctx.fillStyle = "gray";
		ctx.fillText("t = " + (t/100).toFixed(2) + " [z] +0.01 [x] -0.01", canvas.width/2, 0);
	}

	draw(z);
})();

function basis(v0, v1) {
	return [v0[0], v1[0], v0[1], v1[1]];
}

function transpose(m) {
	return [m[0],m[2],m[1],m[3]];
}

function det(m) {
	return m[0]*m[3] - m[1]*m[2];
}

function inv(m) {
	var d = det(m);
	return [m[3]/d, -m[1]/d, -m[2]/d, m[0]/d];
}

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

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

function normalize(v) {
	var d = Math.hypot(v[0], v[1]);
	return [v[0]/d, v[1]/d];
}

function mulmv(m, v) {
	return [m[0]*v[0]+m[1]*v[1], m[2]*v[0]+m[3]*v[1]];
}

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

function polar(m) {
	var sign = Math.sign(det(m));
	var r = new Array(4);
	r[0] = m[0] + m[3] * sign;
	r[1] = m[1] - m[2] * sign;
	r[2] = m[2] - m[1] * sign;
	r[3] = m[3] + m[0] * sign;
	var l = Math.hypot(r[0],r[2]);
	r[0] /= l; r[1] /= l;
	r[2] /= l; r[3] /= l;
	if (det(r) < 0) r[0] = -r[0], r[2] = -r[2];
	var s = mulmm(transpose(r), m);
	var θ = Math.asin(r[2]);
	return [s, θ];
}

function interpolate(v, t, s, θ, p, q) {
	v = sub(v, p);
	// lerp(P)
	v = mulmv([(1-t)+s[0]*t, s[1]*t, s[2]*t, (1-t)+s[3]*t], v);
	// slerp(R)
	var c = Math.cos(θ*t);
	var s = Math.sin(θ*t);
	v = [c*v[0]-s*v[1], s*v[0]+c*v[1]];
	// lerp(t)
	v = [v[0]+p[0]+(q[0]-p[0])*t, v[1]+p[1]+(q[1]-p[1])*t];
	return v;
}

function scale(p, s, t) {
	for (var i=0; i<p.length; ++i) {
		p[i][0] *= s;
		p[i][1] *= t;
	}
}

function translate(p, x, y) {
	for (var i=0; i<p.length; ++i) {
		p[i][0] += x;
		p[i][1] += y;
	}
}

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 Delaunay(x) {
	function sub(a,b) {return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];}
	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]];}

	var n = x.length;
	var p = new Array(n);
	for (let i=0; i<n; ++i)
		p[i] = [x[i][0], x[i][1], x[i][0] * x[i][0] + x[i][1] * x[i][1]];

	var adj = new Array(n);
	for (var i=0; i<n; ++i) adj[i] = new Array(n).fill(0);

	for (let i=0; i<n-2; ++i)
	for (let j=i+1; j<n; ++j)
	for (let k=j+1; k<n; ++k) {
		var normal = cross(sub(p[j],p[i]), sub(p[k],p[i]));
		if (normal[2] < 0) normal = [-normal[0], -normal[1], -normal[2]];
		var upper = true;
		for (let m=0; m<n; ++m) {
			if (m == i || m == j || m == k) continue;
			if (dot(sub(p[m],p[i]), normal) < 0) {upper = false; break;}
		}
		if (upper) {
			adj[i][j] = adj[j][k] = adj[k][i] = 1;
			adj[j][i] = adj[k][j] = adj[i][k] = 1;
		}
	}
	return adj;
}

function DrawAdjacencyMatrix(ctx, p, adj, rgb) {
	ctx.strokeStyle = rgb;
	ctx.lineWidth = 2.25;
	ctx.beginPath();
	for (var i=0; i<p.length; ++i)
		for (var j=i+1; j<p.length; ++j)
			if (adj[i][j]) {
				ctx.moveTo(p[i][0], p[i][1]);
				ctx.lineTo(p[j][0], p[j][1]);
			}
	ctx.stroke();
}
</script>
</div>