/*
* Convex hull algorithm - Demo (TypeScript)
*
* Copyright (c) 2022 Project Nayuki
* https://www.nayuki.io/page/convex-hull-algorithm
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program (see COPYING.txt and COPYING.LESSER.txt).
* If not, see .
*/
// DOM elements
let svgElem = document.querySelector("article svg") as HTMLElement;
let staticRadio = document.getElementById("random-static" ) as HTMLInputElement;
let movingRadio = document.getElementById("random-moving" ) as HTMLInputElement;
let manualRadio = document.getElementById("manual-position") as HTMLInputElement;
// Constants and mutable state
const POINT_RADIUS: number = 0.012;
let points: Array = [];
let draggingPointIndex: number = -1;
function initialize(): void {
handleRadioButtons();
svgElem.onmousedown = ev => handleMouse(ev, "down");
svgElem.onmousemove = ev => handleMouse(ev, "move");
svgElem.onmouseup = ev => handleMouse(ev, "up" );
svgElem.onselectstart = ev => ev.preventDefault();
function handleMouse(ev: MouseEvent, type: "down"|"move"|"up"): void {
// Calculate SVG coordinates
const bounds: DOMRect = svgElem.getBoundingClientRect();
const width : number = bounds.width / Math.min(bounds.width, bounds.height);
const height: number = bounds.height / Math.min(bounds.width, bounds.height);
const evX: number = ((ev.clientX - bounds.left) / bounds.width - 0.5) * width ;
const evY: number = ((ev.clientY - bounds.top ) / bounds.height - 0.5) * height;
if (type == "down") {
// Find nearest existing point
let nearestIndex: number = -1;
let nearestDist: number = Infinity;
points.forEach((point, index) => {
const dist: number = Math.hypot(point.x - evX, point.y - evY);
if (dist < nearestDist) {
nearestDist = dist;
nearestIndex = index;
}
});
if (ev.button == 0) {
if (nearestIndex != -1 && nearestDist < POINT_RADIUS * 1.5)
draggingPointIndex = nearestIndex;
else {
draggingPointIndex = points.length;
points.push(new MovingPoint(NaN, NaN, NaN, NaN));
}
points[draggingPointIndex] = new MovingPoint(evX, evY, 0, 0);
} else if (ev.button == 2) {
if (nearestIndex != -1 && nearestDist < POINT_RADIUS * 1.5)
points.splice(nearestIndex, 1);
if (nearestDist < POINT_RADIUS * 5) {
svgElem.oncontextmenu = ev => {
ev.preventDefault();
svgElem.oncontextmenu = null;
};
}
} else
return;
manualRadio.checked = true;
handleRadioButtons();
} else if (type == "move" || type == "up") {
if (draggingPointIndex == -1)
return;
points[draggingPointIndex] = new MovingPoint(evX, evY, 0, 0);
if (type == "up")
draggingPointIndex = -1;
} else
throw new Error("Assertion error");
showPointsAndHull();
}
}
window.addEventListener("DOMContentLoaded", initialize);
function handleRadioButtons(): void {
staticDemo.stop();
movingDemo.stop();
if (staticRadio.checked)
staticDemo.start();
if (movingRadio.checked)
movingDemo.start();
}
namespace staticDemo {
let timeout: number|null = null;
export function start(): void {
const NUM_POINTS: number = Math.round(Math.pow(30, Math.random()) * 3);
points = [];
for (let i = 0; i < NUM_POINTS; i++)
points.push(new MovingPoint(randomGaussian() * 0.17, randomGaussian() * 0.17, 0, 0));
showPointsAndHull();
timeout = window.setTimeout(start, 3000);
}
export function stop(): void {
if (timeout !== null) {
clearTimeout(timeout);
timeout = null;
}
}
}
namespace movingDemo {
let timeout: number|null = null;
export function start(): void {
const NUM_POINTS: number = 15;
points = [];
for (let i = 0; i < NUM_POINTS; i++)
points.push(new MovingPoint(randomGaussian() * 0.05, randomGaussian() * 0.05, randomGaussian() * 0.10, randomGaussian() * 0.10));
const time: number = performance.now();
update(time, time);
}
function update(prevTime: number, curTime: number): void {
showPointsAndHull();
const deltaTime: number = Math.min(curTime - prevTime, 1000) / 1000;
const bounds: DOMRect = svgElem.getBoundingClientRect()
const width : number = bounds.width / Math.min(bounds.width, bounds.height);
const height: number = bounds.height / Math.min(bounds.width, bounds.height);
for (let i = 0; i < points.length; i++) {
let p: MovingPoint = points[i];
p.x += p.vx * deltaTime;
p.y += p.vy * deltaTime;
if (Math.abs(p.x) > width / 2 || Math.abs(p.y) > height / 2)
points[i] = new MovingPoint(randomGaussian() * 0.05, randomGaussian() * 0.05, randomGaussian() * 0.10, randomGaussian() * 0.10);
}
timeout = requestAnimationFrame(nextTime => update(curTime, nextTime));
}
export function stop(): void {
if (timeout !== null) {
cancelAnimationFrame(timeout);
timeout = null;
}
}
}
function showPointsAndHull(): void {
let onHullGroupElem : Element = svgElem.querySelectorAll("g")[1];
let offHullGroupElem: Element = svgElem.querySelectorAll("g")[0];
while (offHullGroupElem.firstChild !== null)
offHullGroupElem.removeChild(offHullGroupElem.firstChild);
while (onHullGroupElem.firstChild !== null)
onHullGroupElem.removeChild(onHullGroupElem.firstChild);
const hull: Array = convexhull.makeHull(points);
let hullSet = new Set();
for (const point of hull)
hullSet.add(point);
const s: string = hull.map((point, i) => `${i==0?"M":"L"}${point.x},${point.y}`).join("") + "Z";
let pathElem = svgElem.querySelector("path") as Element;
pathElem.setAttribute("d", s);
for (const point of points) {
let circElem: Element = document.createElementNS(svgElem.namespaceURI, "circle");
circElem.setAttribute("cx", point.x.toString());
circElem.setAttribute("cy", point.y.toString());
circElem.setAttribute("r", POINT_RADIUS.toString());
if (hullSet.has(point))
onHullGroupElem.append(circElem);
else
offHullGroupElem.append(circElem);
}
}
function randomGaussian(): number {
return Math.sqrt(-2 * Math.log(Math.random())) * Math.cos(Math.random() * Math.PI * 2);
}
class MovingPoint implements Point {
public constructor(
public x: number,
public y: number,
public vx: number,
public vy: number) {}
}