Circle Inversion and Steiner Chain
The idea for this blog post came up, when I saw this tweet from Cliff Pickover about what is a Steiner chain.
A Steiner Chain is a set of n circles, all of which are tangent to two given non-intersecting circles (blue and red in figure), where each circle in the chain is tangent to the previous and next circles in the chain.
The animation was just a quote taken from Wikipedia and a bit pixelated as well, but it is somewhat beautiful in a way.
And I thought, this is something I can probably re-create in a minute, which translates to actually a couple of hours over the next few days
This gets especially stretched when the children and I all have to catch and cure a virus infection (not covid). Btw., what a nice birthday surprise is this?
Some articles and youtube videos later, it turned out, that we need to prepare some building blocks first. It might not be immediately visible, but we are going to need some form of transformation to draw the circles in the right place. It is called Circle Inversion.
Circle Inversion»
If you’re interested in history and who invented it, if it was even Jakob Steiner or Pappus of Alexandria or
someone else,
read on here
to get some hints.
Luckily when it comes to mathematics, we can just use most of these things discovered and proved by others long before.
Inversion of a Point»
I won’t go to much into detail, as a lot others did before, so lets concentrate on the facts we need to know.
But I can recommend a blog post of Keith Peters about Circle Inversion for creative coding. Keith, alias @bit101, was and still is one of my heros from the days when I was a flash coder, still sharing his experiements and insights at his blog.
What we nee to know is, that the inversion of point A to point A’ using a circle with center point O and a radius r solves this equation.
OA * OA' = r2
You can drag the points in the example on the left and see how the red dot is transformed into the green one.
This demo is quite easily done: The circle of inverion (COI) is defined by its center and a single point on the curve. The distance between both is the radius.
<script>
let coiCenter = point(20,250);
let coiPoint = point(240,330);
$: coiRadius = length(minus(coiPoint, coiCenter));
$: inversionCirclePath = circlePath(coiCenter, coiRadius) + linePath(coiCenter, coiPoint);
let p = point(150,250);
$: _p = invertPoint(p, coiCenter, coiRadius);
</script>
<SvgContaiiner bind:svg={svg} width={500} height={500}>
<path d={inversionCirclePath} stroke-width="1" stroke="#999999" fill="none" />
<DragPoint svg={svg} bind:coord={coiCenter} color={"#cccccc"} />
<DragPoint svg={svg} bind:coord={coiPoint} color={"#999999"} />
<DragPoint svg={svg} bind:coord={p} color={"hsl(360, 75%, 75%)"} />
<Point bind:coord={_p} color={"hsl(120, 75%, 75%)"} />
</SvgContaiiner>
Then we have another point p which is transform to _p using circle inversion with the help of the invertPoint() method.
const invertPoint = (p, c, r) => {
const vp = minus(p, c);
let rp = length(vp);
rp = rp < 0.001 ? 0.001 : rp; // prevent division by zero
const _rp = quad(r) / rp;
const _vp = multiply(normalize(vp), _rp);
return plus(_vp, c);
};
Inversion of Circles»
We learned about how to transform points, lets do it with circles. We don’t care about what happens to shapes other than circles right now. And for circles, there are these basic rules valid about their inversion:
- stay circles
- stay circles
- turn into lines
This time I have to point you to a Youtube video, where Simon Pampena alias @mathemaniac gets really enthusiastic about circle inversion and these rules.
Again, feel free to drag the points and play with the circle of inversion as well as the red circle and see the inverted result of it.
I have linked all of the three rules to an example setting to try out as well.
A circle is defined by its center point and radius, so this time the method invertCircle() expects these properties of the circle to transform as well as of the COI to return these properties of the inversion as an array.
Notice: the innversion of your input circle center would not be the center of your output circle! You should instead transform the two points of your circle that are closest or most far away from your COI center. The center of your output circle is than in between both inverted points.
const invertCircle = (p, pr, c, r) => {
const vp = minus(p, c);
const rp = length(vp);
let from = rp - pr;
let to = rp + pr;
from = Math.abs(from) < 0.001 ? 0.001 : from;
to = Math.abs(to) < 0.001 ? 0.001 : to;
const _from = quad(r) / from;
const _to = quad(r) / to;
const _pr = (_from - _to) / 2;
const _rp = _to + _pr;
const _vp = multiply(normalize(vp), _rp);
return [
plus(_vp, c),
Math.abs(_pr)
];
};
Feasibility criterion»
Still remember what we want to build?
A closed ring of circles touching each other as well as an inner and an outer circle?
A so called Steiner chain.
Let’s start with a closed Steiner chain of circles withtin two concentric circles. That should be easy, huh? But how do we know the radius of the inner (r) and an outer (R) circle that will hold a number (n) of circles so they exactly fit? This question is already answered by the feasibility criterion section at Wikipedia for the Steiner chain.
R / r = (1+sin(π/n)) / (1-sin(π/n))
The formula above being an equation with three variables, it is easily transformed to calculate one of them, when the others are set.
- Pick the number (n) of circles in the slider
- and the radius (R) of the outher circle using drag and drop
…to calculate the radius (r) of the inner circle.
And this enables the calculation of everything else, as the distance between outer and inner circle is the diameter of the chain circles. I added a green circle holding all the center points of the circles for illustration.
I also already added the animation by moving the start of the chain around the circle.
Steiner chain»
Let us put this now together with circle inversion and see what happens next. I think we come quite close to our inspiration an I keep it there and let you play with it, either dragging the range sliders or the points.
source
<script>
import { point, circlePoint, minus, length } from '$lib/utils/path-utils.js';
import { circlePath } from '$lib/utils/svg-utils.js';
import { invertCircle } from './inversion.js';
import { nums, lerp } from '$lib/utils/math-utils.js';
import DragPoint from '$lib/shared/drag-point.svelte';
import SvgContaiiner from '$lib/shared/svg-container.svelte';
import RangeSlider from '$lib/shared/range-slider.svelte';
import Animation from '$lib/shared/animation.svelte';
let svg;
let t = 0.5;
let originalOpacity = 1;
let invertedOpacity = 1;
let coiCenter = point(50,250);
let coiPoint = point(520,450);
$: coiRadius = length(minus(coiPoint, coiCenter));
$: coiPath = circlePath(coiCenter, coiRadius);
$: invert = (p, r) => invertCircle(p, r, coiCenter, coiRadius);
let center = point(780,250);
let outerPoint = point(950,330);
$: outerRadius = length(minus(outerPoint, center));
$: outerPath = circlePath(center, outerRadius);
let n = 5;
$: d = Math.sin(Math.PI/n);
$: innerRadius = outerRadius * (1 - d) / (1 + d);
$: innerPath = circlePath(center, innerRadius);
$: circlesRadius = (outerRadius - innerRadius)/2;
$: centerPointsRadius = innerRadius + circlesRadius;
$: centerPoints = nums(n).map((i) =>
circlePoint(center, centerPointsRadius, 360/n * i - lerp(0, 360, t))
);
$: circlesPath = centerPoints
.map((p) => circlePath(p, circlesRadius))
.join(" ")
;
$: invertedOuterPath = circlePath.apply(this, invert(center, outerRadius));
$: invertedInnerPath = circlePath.apply(this, invert(center, innerRadius));
$: invertedCirclesPath = centerPoints
.map((p) => invert(p, circlesRadius))
.map(([p, r]) => circlePath(p, r))
.join(" ")
;
let auto = true;
let time = 0;
const animate = () => {
time = window.performance.now()*60/1000/100/3%2;
if (auto) {
t = time/2;
}
};
</script>
<Animation run={animate} />
<div
role="button"
tabindex="0"
aria-label="Stop Slider Auto value"
on:mousedown={() => auto = false}
on:mouseup={() => auto = true}
>
<RangeSlider label="t" bind:value={t} step="0.01" min="0" max="1" border="1px solid #667788" width="160" color="#335588" background="#fefbe9" />
</div>
<RangeSlider label="number" bind:value={n} step="1" min="3" max="24" border="1px solid #667788" width="160" color="#335588" background="#fefbe9" />
<RangeSlider label="original" bind:value={originalOpacity} step="0.1" min="0" max="1" border="1px solid #667788" width="160" color="#335588" background="#fefbe9" />
<RangeSlider label="inverted" bind:value={invertedOpacity} step="0.1" min="0" max="1" border="1px solid #667788" width="160" color="#335588" background="#fefbe9" />
<br>
<div class="canvas">
<SvgContaiiner bind:svg={svg} width={1000} height={500}>
<g opacity={originalOpacity}>
<path d={outerPath} stroke-width="3" stroke="hsl(240, 60%, 60%)" fill="none" />
<path d={innerPath} stroke-width="3" stroke="hsl(360, 60%, 60%)" fill="none" />
<path d={circlesPath} stroke-width="1" stroke="#999999" fill="none" />
<DragPoint svg={svg} bind:coord={center} color={"hsl(240, 75%, 75%)"} />
<DragPoint svg={svg} bind:coord={outerPoint} color={"hsl(240, 60%, 60%)"} />
</g>
<path d={coiPath} stroke-width="1" stroke="#999999" fill="none" />
<g opacity={invertedOpacity}>
<path d={invertedOuterPath} stroke-width="3" stroke="hsl(240, 60%, 60%)" fill="none" />
<path d={invertedInnerPath} stroke-width="3" stroke="hsl(360, 60%, 60%)" fill="none" />
<path d={invertedCirclesPath} stroke-width="1" stroke="#999999" fill="none" />
<DragPoint svg={svg} bind:coord={coiCenter} color={"#cccccc"} />
<DragPoint svg={svg} bind:coord={coiPoint} color={"#999999"} />
</g>
</SvgContaiiner>
</div>
<style>
.canvas {
background-color: white;
}
</style>