function vec_polar(r, a) {
return {x: r * Math.cos(a), y: r * Math.sin(a)};
}
function vec_add(p, q) {
return {x: p.x + q.x, y: p.y + q.y};
}
function vec_sub(p, q) {
return {x: p.x - q.x, y: p.y - q.y};
}
function vec_dot(p, q) {
return p.x * q.x + p.y * q.y;
}
function vec_cross(p, q) {
return p.x * q.y - p.y * q.x;
}
function vec_interpolate(p, q, t) {
return {x: p.x + (q.x - p.x) * t, y: p.y + (q.y - p.y) * t};
}
function vec_facing(p, q) {
const dx = q.x - p.x, dy = q.y - p.y;
return Math.atan2(dy, dx);
}
function vec_length(p) {
return Math.sqrt(p.x*p.x + p.y*p.y);
}
function vec_distance(p, q) {
return vec_length(vec_sub(p, q));
}
function vec_normalize(p) {
let d = vec_length(p) || 1e-6; // avoid divide by 0
return {x: p.x / d, y: p.y / d};
}
function angle_difference(a, b) {
return Math.abs(b - a) % (2 * Math.PI);
}
//
function line_of_sight(circles, i, P, j, Q) {
for (let k = 0; k < circles.length; k++) {
if (k != i && k != j
&& segment_circle_intersection(P, Q, circles[k]).intersects) {
return false;
}
}
return true;
}
/** Generate surfing edges, [{circle: circle, x: number, y: number},
{circle: circle, x: number, y: number}]
as well as the set of nodes. Although the edges are bidirectional,
each is in the list only once.
*/
function generate_nodes_and_surfing_edges(circles) {
let edges = [], node_map = new Map();
function round(x) { return Math.round(100*x)/100; }
// create a new node, or reuse an existing one that's close
function make_node(i, p) {
let node = {circle: circles[i], x: round(p.x), y: round(p.y)};
let key = JSON.stringify(node);
if (!node_map.has(key)) { node_map.set(key, node); }
return node_map.get(key);
}
// try to add edge from circle i point P to circle j point Q
function add_edge(i, P, j, Q) {
if (isNaN(P.x) || isNaN(Q.x)) { return; }
if (!line_of_sight(circles, i, P, j, Q)) { return; }
edges.push([make_node(i, P), make_node(j, Q)]);
}
// some circles have radius 0; they will generate fewer bitangents
for (let i = 0; i < circles.length; i++) {
for (let j = 0; j < i; j++) {
let candidates = [];
var internal = new InternalBitangents(circles[i], circles[j]);
add_edge(i, internal.C, j, internal.F);
if (circles[i].r != 0 && circles[j].r != 0) { add_edge(i, internal.D, j, internal.E); }
var external = new ExternalBitangents(circles[i], circles[j]);
if (circles[i].r != 0 || circles[j].r != 0) { add_edge(i, external.C, j, external.F); }
if (circles[i].r != 0 && circles[j].r != 0) { add_edge(i, external.D, j, external.E); }
}
}
return {nodes: [...node_map.values()], edges: edges};
}
/** Generate hugging edges from nodes
Any nodes on the same circle get connected by a hugging edge.
Although the edges are bidirectional, each is in the list only once.
*/
function generate_hugging_edges(nodes) {
let buckets = [];
for (let node of nodes) {
if (buckets[node.circle.id] === undefined) { buckets[node.circle.id] = []; }
buckets[node.circle.id].push(node);
}
let hugging_edges = [];
for (let bucket of buckets) {
for (let i = 0; i < bucket.length; i++) {
for (let j = 0; j < i; j++) {
hugging_edges.push([bucket[i], bucket[j]]);
}
}
}
return hugging_edges;
}
/** Pathfinding */
function find_path(start_circle, goal_circle, nodes, edges) {
// TODO: I know this is horribly inefficient but it may not matter; let's see
function circle_to_node(circle) {
let nodes_on_circle = nodes.filter((n) => n.circle.id == circle.id);
if (nodes_on_circle.length !== 1) { throw "start/goal should be on r=0 circle"; }
return nodes_on_circle[0];
}
function neighbors(node) {
let results = [];
for (let edge of edges) {
if (edge[0] === node) { results.push(edge[1]); }
if (edge[1] === node) { results.push(edge[0]); }
}
return results;
}
function edge_cost(a, b) {
// adding 1 to each edge cost to favor fewer nodes in the path
if (a.circle.id == b.circle.id) {
// hugging edge
let center = a.circle;
let a_angle = vec_facing(center, a);
let b_angle = vec_facing(center, b);
let delta_angle = angle_difference(a_angle, b_angle);
return 1 + delta_angle * center.r;
} else {
// surfing edge
return 1 + vec_distance(a, b);
}
}
function heuristic(node) {
return 0; // TODO: not working yet
return vec_distance(goal_node, node);
}
let start_node = circle_to_node(start_circle);
let goal_node = circle_to_node(goal_circle);
let frontier = [[start_node, 0]];
let came_from = new Map([[start_node, null]]);
let cost_so_far = new Map([[start_node, 0]]);
while (frontier.length > 0) {
frontier.sort((a, b) => a[1] - b[1]);
let current = frontier.shift()[0];
if (current === goal_node) { break; }
for (let next of neighbors(current)) {
let new_cost = cost_so_far.get(current) + edge_cost(current, next);
if (!cost_so_far.has(next) || new_cost < cost_so_far.get(next)) {
cost_so_far.set(next, new_cost);
came_from.set(next, current);
frontier.push([next, new_cost + heuristic(next), vec_distance(goal_node, next)]);
}
}
}
reconstruct_path: {
let current = goal_node;
let path = [current];
while (current !== start_node && current !== undefined) {
current = came_from.get(current);
path.push(current);
}
path.push(start_node);
return path;
}
}
/** Don't allow a circle drag operation if it would touch another circle */
function no_touching_circle(index) {
return (x, y) => {
for (let i = 0; i < this.circles.length; i++) {
if (i != index
&& vec_distance({x, y}, this.circles[i])
<= this.circles[i].r + this.circles[index].r) {
return false;
}
}
return true;
};
}
function make_path_diagram(element, circles) {
circles = circles.map((c, i) => ({id: i, x: c.x, y: c.y, r: c.r}));
circles.sort((a, b) => b.r - a.r);
return new Vue({
el: element,
data: {circles: circles},
computed: {
nodes_and_surfing_edges: function() { return generate_nodes_and_surfing_edges(this.circles); },
surfing_edges: function() { return this.nodes_and_surfing_edges.edges; },
nodes: function() { return this.nodes_and_surfing_edges.nodes; },
hugging_edges: function() { return generate_hugging_edges(this.nodes); },
edges: function() { return this.surfing_edges.concat(this.hugging_edges); },
path: function() { return find_path(this.circles[this.circles.length-2],
this.circles[this.circles.length-1],
this.nodes, this.edges); }
},
methods: {
no_touching_circle: no_touching_circle,
d: function(include_surfing, include_hugging) {
let path = this.path;
let d = [];
for (let i = 1; i < path.length; i++) {
if (path[i].circle == path[i-1].circle) {
if (include_hugging) {
let center = path[i].circle;
let sweep = vec_cross(vec_sub(center, path[i-1]), vec_sub(path[i], path[i-1])) < 0 ? 1 : 0;
d.push('M', path[i-1].x, path[i-1].y, 'A', center.r, center.r, 0, 0, sweep, path[i].x, path[i].y);
}
} else {
if (include_surfing) {
d.push('M', path[i-1].x, path[i-1].y, 'L', path[i].x, path[i].y);
}
}
}
return d.join(' ');
}
}
});
}
let diagram_surfing_edges = make_path_diagram(
"#diagram-surfing-edges",
[
{x: 340, y: 200, r: 90},
{x: 80, y: 200, r: 70},
{x: 505, y: 65, r: 50}
]
);
let diagram_intro = make_path_diagram(
"#diagram-intro", [
{x: 113, y: 99, r: 55},
{x: 497, y: 243, r: 40},
{x: 379, y: 237, r: 40},
{x: 330, y: 113, r: 35},
{x: 179, y: 190, r: 30},
{x: 278, y: 233, r: 30},
{x: 30, y: 74, r: 0},
{x: 570, y: 280, r: 0}
]
);
let diagram_path_edges = make_path_diagram(
"#diagram-path-edges", [
{x: 86, y: 85, r: 55},
{x: 197, y: 38, r: 55},
{x: 178, y: 145, r: 45},
{x: 467, y: 85, r: 45},
{x: 369, y: 137, r: 45},
{x: 310, y: 47, r: 45},
{x: 17, y: 72, r: 0},
{x: 546, y: 97, r: 0}
]
);
let diagram_final = make_path_diagram(
"#diagram-final", [
{x: 180, y: 100, r: 55},
{x: 240, y: 230, r: 30},
{x: 340, y: 200, r: 30},
{x: 505, y: 65, r: 25},
{x: 405, y: 255, r: 20},
{x: 80, y: 200, r: 20},
{x: 20, y: 20, r: 0},
{x: 570, y: 280, r: 0}
]
);
let rotation1 = new Vue({
el: "#diagram-rotation-1",
data: {
circle: {x: 300, y: 150, r: 100},
edges: [
{label: 'A', x: 100, y: 110, dir: 'left', marker: 'start'},
{label: 'B', x: 500, y: 110, dir: 'right', marker: 'end'},
{label: 'C', x: 500, y: 190, dir: 'left', marker: 'end'},
{label: 'D', x: 100, y: 190, dir: 'right', marker: 'end'}
]
},
methods: {
center: function(edge) {
let bitangent = new ExternalBitangents(this.circle, {x: edge.x, y: edge.y, r: 0});
return bitangent[edge.dir == 'left'? 'D' : 'C'];
}
}
});
To embed this program on your website, copy the following code and paste it into your website's HTML: