Skip to content

Instantly share code, notes, and snippets.

@w8r
Last active April 29, 2024 11:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save w8r/e4963a52331637f4eb3b11c42233a4fd to your computer and use it in GitHub Desktop.
Save w8r/e4963a52331637f4eb3b11c42233a4fd to your computer and use it in GitHub Desktop.
Huygens' arc length approximation
<!doctype html>
<html>
<head>
<title>Huygens' arc length approximation</title>
<script src="https://unpkg.com/d3@4.4.1"></script>
<style>
circle {
fill-opacity: 1;
stroke: #000000;
stroke-width: 0.5;
}
text {
font-family: sans-serif;
font-size: 10px;
fill: #000000;
}
.drawing {
z-index: 0;
}
.controls {
z-index: 1;
}
.circle {
stroke-width: 2;
fill: none;
z-index: 0;
}
.point {
cursor: move;
fill: #cccccc;
fill-opacity: 1;
z-index: 10;
}
.active {
stroke-width: 1;
cursor: move;
}
.chord, .sagitta, .half-chord, .radius {
stroke-width: 1;
stroke: #000000;
}
.radius {
stroke: #2222ff;
stroke-opacity: 0.3;
}
.control {
position: absolute;
top: 20px;
right: 20px;
padding: 10px 20px;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: 300;
background: #fff;
border-radius: 4px;
box-shadow: 0 0 5px rgba(0,0,0,0.5);
}
.control h4 {
font-weight: 300;
}
.control input[type=number] {
width: 50px;
}
.arc {
stroke-width: 3;
stroke: #aa5555;
}
.name {
font-weight: bold;
}
</style>
</head>
<body>
<div class="control">
<h4><a href="https://en.wikipedia.org/wiki/Christiaan_Huygens" target="_blank">Huygens</a>' arc length approximation</h4>
<table>
<tr>
<td class="name">Angle</td>
<td id="angle"></td>
</tr>
<tr>
<td class="name">R &times; &theta;</td>
<td id="classic"></td>
</tr>
<tr>
<td class="name">Huygens</td>
<td id="huygens"></td>
<tr>
<td class="name">Error</td>
<td id="error"></td>
</tr>
</table>
</div>
<script src="index.js"></script>
</body>
</html>
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) + '&deg;';
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();
@goovich
Copy link

goovich commented Apr 29, 2024

Hi there,
There is a precise formula for the arc length with a given chord and arc height (Russian article):
https://isicad.ru/ru/articles.php?article_num=22566

@w8r
Copy link
Author

w8r commented Apr 29, 2024

@goovich thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment