diff --git a/index.css b/index.css
new file mode 100644
index 0000000..3b58bef
--- /dev/null
+++ b/index.css
@@ -0,0 +1,46 @@
+* {
+ color: #eee;
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 10svh;
+ margin: 0;
+ user-select: none;
+ box-sizing: border-box;
+ overflow: hidden;
+}
+
+body {
+ height: 100svh;
+ background-color: #333;
+}
+
+input{
+ display: none;
+}
+
+label {
+ position: fixed;
+ bottom: 0;
+ margin: 2svh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: #444;
+ border: 2px solid #222;
+ border-radius: 3svh;
+ width: 10svh;
+ height: 10svh;
+ cursor: pointer;
+}
+
+input:checked+label {
+ background-color: #AAA;
+ border-color: #666;
+}
+
+label[for="add-point"] {
+ left: 0;
+}
+
+label[for="remove-point"] {
+ right: 0;
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..a13d8c6
--- /dev/null
+++ b/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ closest-pairs
+
+
+
+
+
+
+
+
+
+
+
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..8100eb3
--- /dev/null
+++ b/index.js
@@ -0,0 +1,156 @@
+let canvas;
+let ctxt;
+
+let addingPoint;
+
+let points;
+
+window.onload = () => {
+ canvas = document.getElementById("canvas");
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight;
+ ctxt = canvas.getContext("2d");
+
+ addingPoint = document.getElementById("add-point");
+
+ points = [];
+
+ canvas.onclick = e => {
+ let x = e.clientX;
+ let y = e.clientY;
+
+ if(addingPoint.checked) {
+ // insert sorted by x then y
+ let inserted = false;
+ for(let i=0;i {
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight;
+
+ update();
+}
+
+function update() {
+ ctxt.clearRect(0, 0, canvas.width, canvas.height);
+
+ if(points.length < 2) {
+ renderPoints(points);
+ return
+ }
+
+ let [index1, index2] = calculateClosestPair(points)
+
+ renderClosest(points, index1, index2);
+ renderPoints(points);
+}
+
+function renderClosest(points, index1, index2) {
+ ctxt.lineWidth = canvas.height / 300;
+ ctxt.strokeStyle = "#FFF";
+ ctxt.lineCap = "round";
+ ctxt.beginPath();
+ ctxt.moveTo(points[index1][0], points[index1][1]);
+ ctxt.lineTo(points[index2][0], points[index2][1]);
+ ctxt.stroke();
+
+ ctxt.beginPath();
+ ctxt.arc(points[index1][0], points[index1][1], canvas.height / 100, 0, 2 * Math.PI);
+ ctxt.stroke();
+
+ ctxt.beginPath();
+ ctxt.arc(points[index2][0], points[index2][1], canvas.height / 100, 0, 2 * Math.PI);
+ ctxt.stroke();
+}
+
+function renderPoints(points) {
+ ctxt.fillStyle = "#000A";
+ for(let point of points) {
+ ctxt.beginPath();
+ ctxt.arc(point[0], point[1], canvas.height / 100, 0, 2 * Math.PI);
+ ctxt.fill();
+ }
+}
+
+function calculateClosestPair(points) {
+ let minDistance = Infinity;
+ let bestIndex1 = 0;
+ let bestIndex2 = 0;
+ let activeIndexStart = 0;
+
+ for(let i=0;i= minDistance) {
+ activeIndexStart++;
+ } else {
+ // other points are still active
+ break;
+ }
+ }
+
+ for(let j=activeIndexStart;j minDistance) {
+ // we don't need expensive hypot calculation
+ continue
+ }
+ if(signedYDistance < -minDistance) {
+ // there cannot be better points
+ break
+ }
+
+ // it's close so we check using hypot
+ let distance = Math.hypot(signedYDistance, points[j][0] - points[i][0]);
+ if(distance < minDistance) {
+ minDistance = distance;
+ bestIndex1 = j;
+ bestIndex2 = i;
+ }
+ }
+ }
+
+ return [bestIndex1, bestIndex2];
+}