const h = document.documentElement.clientHeight; const w = h; const arcWeight = 5; const deg2rad = Math.PI / 180; const angleVal = document.getElementById('angle'); const classicVal = document.getElementById('classic'); const huygensVal = document.getElementById('huygens'); const errorVal = document.getElementById('error'); const color = d3.interpolateRgb('steelblue', 'brown'); const container = d3.select('body') .append('svg') .attr('width', w) .attr('height', h) .attr('viewBox', [-w/2, -h/2, w, h].join(' ')); // canvas const svg = container.append('g') .attr('class', 'drawing'); const controls = container.append('g') .attr('class', 'controls'); let circles, arc, span, startAngle, endAngle; var C = [ 0, 0]; var R = (w / 2) * 0.75; var sizes = [10, 10]; var angles = [0, Math.PI / 2]; var X = [0, 0]; var Y = [0, 0]; circles = controls.selectAll('circle') .data(sizes) .enter() .append('circle') .attr('cx', function (size, i) { return X[i]; }) .attr('cy', function (size, i) { return Y[i]; }) .attr('r', function (size) { return size; }) .attr('class', 'point') .attr('data-index', function (s, i) { return i; }) .call(d3.drag() .on('start', dragstart) .on('drag', drag) .on('end', dragend)); controls .append('circle') .attr('class', 'center') .attr('cx', C[0]).attr('cy', C[1]) .attr('r', 2); controls .append('circle') .attr('class', 'circle') .attr('cx', C[0]).attr('cy', C[1]) .attr('r', R); circles.raise(); function crossProduct (ax, ay, bx, by, cx, cy) { // compute the cross product of vectors (center -> a) x (center -> b) var det = (ax - cx) * (by - cy) - (bx - cx) * (ay - cy); if (det < 0) return 1; if (det > 0) return -1; return 0; // same dir } function dotProduct (ax, ay, bx, by, cx, cy) { return (ax - cx) * (bx - cx) + (ay - cy) * (by - cy); } function distance (ax, ay, bx, by) { var dx = ax - bx; var dy = ay - by; return Math.sqrt(dx * dx + dy * dy); } function closestPointOnCircle (px, py) { let vX = px - C[0]; let vY = py - C[1]; let magV = Math.sqrt(vX * vX + vY * vY); return [ C[0] + vX / magV * R, C[1] + vY / magV * R ]; } function dragstart (d) { d3.select(this) .raise() .classed('active', true); } function drag (d) { const closest = closestPointOnCircle(d3.event.x, d3.event.y); const i = parseInt(this.getAttribute('data-index')); X[i] = closest[0]; Y[i] = closest[1]; d3.select(this) .attr('cx', closest[0]) .attr('cy', closest[1]); render(); } function dragend (d, i) { d3.select(this) .classed('active', false); } function arcPath () { startAngle = Math.atan2(Y[0] - C[1], X[0] - C[0]) + Math.PI / 2; endAngle = Math.atan2(Y[1] - C[1], X[1] - C[0]) + Math.PI / 2; return d3.arc() .innerRadius(R - arcWeight / 2) .outerRadius(R + arcWeight / 2)({ startAngle, endAngle }); } N = 2; sizes = new Array(N).fill(10); function generate () { angles = new Array(N).fill(359).map(a => (0 + a * Math.random()) % 360); R = (w / 2) * 0.75; //R *= 3; angles.forEach((a, i) => { X[i] = C[0] + R * Math.sin(a * deg2rad); Y[i] = C[1] - R * Math.cos(a * deg2rad); }); render(); } function render () { svg.selectAll('circle').remove(); svg.selectAll('line').remove(); svg.selectAll('path').remove(); circles .attr('cx', function (size, i) { return X[i]; }) .attr('cy', function (size, i) { return Y[i]; }); svg .append('path') .attr('class', 'arc') .attr('d', arcPath); svg .append('line') .attr('class', 'chord') .attr('x1', X[0]).attr('y1', Y[0]) .attr('x2', X[1]).attr('y2', Y[1]); var P = [(X[0] + X[1]) / 2, (Y[0] + Y[1]) / 2]; svg .append('circle') .attr('cx', P[0]).attr('cy', P[1]) .attr('r', 3); var angleDiff = Math.abs(endAngle - startAngle); var angle = Math.atan2(P[1] - C[1], P[0] - C[0]); if (angleDiff > Math.PI) angle = angle - Math.PI; var CP = [ C[0] + R * Math.cos(angle), C[1] + R * Math.sin(angle) ]; svg .append('circle') .attr('cx', CP[0]).attr('cy', CP[1]) .attr('r', 8) svg .append('line') .attr('class', 'sagitta') .attr('x1', P[0]).attr('y1', P[1]) .attr('x2', CP[0]).attr('y2', CP[1]); svg .append('line') .attr('class', 'half-chord') .attr('x1', X[0]).attr('y1', Y[0]) .attr('x2', CP[0]).attr('y2', CP[1]); svg .append('line') .attr('class', 'half-chord') .attr('x1', X[1]).attr('y1', Y[1]) .attr('x2', CP[0]).attr('y2', CP[1]); svg .append('line') .attr('class', 'radius') .attr('x1', C[0]).attr('y1', C[1]) .attr('x2', X[0]).attr('y2', Y[0]); svg .append('line') .attr('class', 'radius') .attr('x1', C[0]).attr('y1', C[1]) .attr('x2', X[1]).attr('y2', Y[1]); var approx = huygens([X[0], Y[0]], [X[1], Y[1]], CP); var exact = R * angleDiff; huygensVal.innerHTML = approx; classicVal.innerHTML = exact; angleVal.innerHTML = (angleDiff * 180 / Math.PI).toFixed(2) + '°'; errorVal.innerHTML = Math.abs(((approx - exact) / exact) * 100).toFixed(4) + '%'; } function huygens (a, b, c) { var l = Math.sqrt( (a[0] - c[0]) * (a[0] - c[0]) + (a[1] - c[1]) * (a[1] - c[1])); var L = Math.sqrt( (a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1])); return 2 * l + (2 * l - L) / 3; } generate();