Regular Polygons in the Poincaré Disk
A hyperbolic polygon inside the disk is a set of points outside it, and for a regular one that set is a regular polygon.
To draw a beautiful tiling of the hyperbolic plane by polygons, you first need to actually construct that polygon in some model (not just prove that one exists): an explicit list of coordinates, ready to hand to a computer.
For triangles this is a matter of trigonometry, and it’s especially easy when one of the angles is right. Lay down the vertical line and the unit circle in the upper half plane, meeting at right angles, and the third side is a semicircle centered at some on the real line with some radius . Write the two angles it makes with the sides already placed as functions of and , set them to the angles you want, and solve.
Right angles remain useful for larger polygons too: I’ve used them before to construct right-angled pentagons, hexagons and heptagons in the upper half plane, one geodesic at a time.
But what about other angles? This note records something I find particularly pleasing: if you ask for regular polygons, you can have any angle smaller than the Euclidean value, and the construction is simple, provided you do it in the Poincaré disk. We’ll use the symmetry of the polygon and the symmetry of the disk together to reduce the whole problem to finding a single real number.
The Disk and Its Geodesics
The Poincaré disk is a model of the hyperbolic plane on the open unit disk, with metric
Its geodesics come in two kinds: the diameters, and the arcs of Euclidean circles that meet the boundary at right angles.
To build polygons out of geodesics we need a way to work with them, some coordinates on the set of geodesics itself. An arc-type geodesic is a circle, so it comes with a center and a radius , but these two aren’t independent: the circle must cross the unit circle at right angles. Orthogonality of circles is a Pythagorean condition (the two radii at a crossing point are the legs of a right triangle whose hypotenuse joins the centers), so it reads
The radius isn’t really a second piece of data: , which is the length of the tangent segment from to the unit circle. So a geodesic is named by its center alone, and since must be positive, that center lies outside the disk. This is a bijection: every point with bounds exactly one such circle, swept out by swinging the tangent segment around .
What this misses are the diameters. They correspond to directions: sending off to infinity along a ray, the corresponding circle flattens out toward the diameter perpendicular to that ray1, and a point escaping in either of two opposite directions converges to the same geodesic. So the full space of geodesics is the exterior of the disk together with an at infinity, one point per pair of opposite directions. (Glued in, that circle closes the exterior annulus into a Mobius strip, with the as its core circle.)
Happily, we’ll never need the . We’re going to center our polygon at the origin of the disk, and then no side passes through the center: every side is an arc-type geodesic, and the polygon is a finite list of points outside the disk, one per side.
Thanks to my friend Gordon Kirby for wondering about what the points outisde the Poincare disk represent, which led me to think about this perspective!
Regular Polygons in the Space of Geodesics
A regular polygon is one where all sides have the same length and all interior angles are equal. Just like in the Euclidean plane, a regular hyperbolic polygon has a center: a point fixed by all of its symmetries2. We begin by moving our polygon so that this center is at the origin of the disk. This is exactly the situation we prepared for in the last section: the polygon is convex and contains its center, so no side passes through the origin, and each side is a circular arc with its own point outside the disk.
So, what are the symmetries of our polygon? In most models of hyperbolic space this would be a difficult question, but it’s easy in the Poincaré disk: the hyperbolic isometries fixing the origin are exactly the Euclidean rotations and reflections about it. In particular, the rotation carrying our polygon one step around itself is an ordinary Euclidean rotation by angle .
This rotation takes each side of the polygon to the next one, so it takes each point to the next point . That is, the points form a single orbit of a Euclidean rotation of order . But we know what such orbits look like! They are the vertices of regular Euclidean polygons.
So the sides of our hyperbolic polygon, viewed as points in the space of geodesics, themselves form a regular Euclidean polygon outside the disk3. And a regular Euclidean -gon is completely determined (up to rotation) by its size, say its circumradius . Once we know we know the points , from the points we get the circles, and from the circles the polygon itself. All that remains of the construction is to find the one number .
Constructing Regular Polygons with a Given Angle
So, which value of gives which polygon? The one geometric quantity we care about is the interior angle , since that’s what a tiling will constrain, so what we need is the relationship between and .
The interior angle is the angle at which two adjacent sides meet at a vertex, and in our picture those two sides are two circles crossing. So the question becomes: at what angle do two circles meet, given their radii and the distance between their centers? We answered a special case of this already, when we found that two circles meet at right angles exactly when . That was the Pythagorean theorem applied to the triangle with the two radii as legs, and for a general angle the same triangle is there, with Pythagoras upgraded to the law of cosines.
But we must be careful about which angle is which! The angle between two circles is measured between their tangent lines at the crossing point, while our triangle is built out of radii, and each radius is perpendicular to its own tangent. Rotating both tangents a quarter turn to line up with the radii replaces the angle between them with its supplement. So the apex angle of the triangle is not but , and the law of cosines reads
As a sanity check, at this recovers the orthogonality relation, as it must.
Now we apply this at a vertex of our polygon, where everything is determined by . The two crossing circles are congruent sides of the polygon, so they share a single radius , and orthogonality with the boundary gives . Their centers are adjacent vertices of the outer polygon: two points at distance from the origin, separated by the angle , so . Substituting all of this into the angle relation gives
and collecting the terms solves the problem:
A regular hyperbolic -gon with interior angle , centered at the origin of the Poincaré disk, is bounded by circles whose centers form a regular Euclidean -gon of circumradius , where
Let’s try it out on the right-angled hexagon: and , so and , giving and . The right-angled hexagon is cut out by six unit circles, centered at the vertices of a regular hexagon of circumradius . This is the kind of concrete answer we were looking for!
The formula also tells us exactly which angles are possible, and proves the claim from the introduction. For to be positive we need , which happens exactly when , that is, when
the interior angle of the regular Euclidean -gon. And the formula shows how the two extremes are approached. As climbs toward the Euclidean value, : the bounding circles grow huge and their arcs flatten into straight lines, cutting out a tiny polygon near the origin. This is just the familiar fact that small hyperbolic polygons are nearly Euclidean. At the other end , the circumradius reaches its minimum , the vertices reach the boundary circle, and adjacent bounding circles become exactly tangent: this is the ideal polygon, the largest regular -gon there is. In between, increases with , so every allowed angle occurs for exactly one polygon. We can now specify a regular polygon by its angle and read off the circles that draw it:
Which Ones Tile
A polygon tiles the hyperbolic plane face-to-face when some whole number of copies fit together perfectly around each vertex. If copies meet at each vertex, they divide the of angle there equally, so the polygon we need is the regular -gon with
and the resulting tiling is the regular tiling with Schläfli symbol . Which pairs actually occur? We just determined the possible angles of a regular -gon, so we only need to check whether is on the list:
This is the classical existence condition for hyperbolic tilings, and it has infinitely many solutions: for every all sufficiently large work, and for every all sufficiently large do.
But now we have more than existence. For each solution, the theorem hands us the tile itself: substituting ,
and the tile is the region of the disk outside the circles of radius centered at the points . Some small cases come out cleanly:
| tiling | ||
|---|---|---|
The right-angled pentagon is one I constructed in an earlier note by placing geodesics in the upper half plane one at a time. Here it arrives all at once, as five circles of radius .
Here is how the condition plays out for small : the table shows the smallest that works, and every larger works too.
| 3 | 4 | 5 | 6 | 7 | 8 | ||
|---|---|---|---|---|---|---|---|
| smallest | 7 | 5 | 4 | 4 | 3 | 3 | 3 |
Triangles are the hardest to tile with, needing at least seven around each vertex, and from onward every works. Choose any pair from this menu, and the formula draws it:
Reflection Groups and Code
With the sides in hand, one polygon is easy to draw. But we’re after the whole tiling, and here reflections do all the work: reflecting the tile across its own sides, then reflecting the copies across theirs, and so on, fills the entire disk. For a computer this is wonderful news, because reflecting across one of our geodesics is an operation it already knows. Reflection across a circle of center and radius is just inversion in that circle:
vec2 reflectIn(vec2 z, vec2 c, float r2) {
vec2 d = z - c;
return c + r2 * d / dot(d, d);
}
There’s one condition to look at first. The tiling is drawn by reflections, but for the tile itself to be a fundamental chamber, so that every tile is reached from ours by a unique symmetry and the reflection count means something, the tile must be a Coxeter polygon: every interior angle of the form for a whole number . Our angle is , so this happens exactly when is even. That shrinks the menu a little:
| 3 | 4 | 5 | 6 | 7 | ||
|---|---|---|---|---|---|---|
| smallest even | 8 | 6 | 4 | 4 | 4 |
For now we take even, and at the end we’ll feed the program an odd anyway and see what it does.
So, how should we draw the tiling? The naive plan, actually performing the reflections copy by copy, works badly on a screen: after a dozen generations the disk is still visibly empty near its boundary, which is exactly where the tiles are smallest and most numerous, and each further generation costs exponentially more for less and less visible progress. So we run the whole thing backwards: instead of unfolding the polygon out to every pixel, take each pixel and fold it back into the polygon. A point is inside the polygon exactly when it’s outside all walls, so the fold is a loop: while some wall-circle contains the point, reflect it out, and count the reflections as you go.
int fold(inout vec2 z, float rho, float r2) {
int word = 0;
for (int step = 0; step < 120; step++) {
bool moved = false;
for (int i = 0; i < MAXP; i++) {
if (float(i) >= pCount) break;
vec2 c = wallCenter(i, rho);
if (inside(z, c, r2)) {
z = reflectIn(z, c, r2);
word++;
moved = true;
}
}
if (!moved) break;
}
return word;
}
Every pixel costs about the same, and the tiling is drawn all the way to the horizon. The reflection count word is a bonus: its parity two-colors the tiling like a checkerboard, coloring each tile by whether it takes an even or odd number of reflections to reach.
The one remaining detail is drawing the edges. A tile near the boundary is only a few pixels wide, so edges of fixed Euclidean width would swallow it whole. The fix is to measure the distance to the nearest wall hyperbolically4, which gives every edge the same weight no matter where it sits:
float wallDist(vec2 z, vec2 c, float r2) {
vec2 d = z - c;
return abs(dot(d, d) - r2) / (sqrt(r2) * (1.0 - dot(z, z)));
}
Here is the whole program. Set and at the top and it draws that tiling; on the page above, those two are the buttons.
// The {p, q} tiling of the Poincaré disk, one pixel at a time.
// Choose the tiling here: q even, and 1/p + 1/q < 1/2.
const float pCount = 5.0; // sides of the polygon
const float qCount = 4.0; // copies meeting at each vertex
const int MAXP = 12;
const float PI = 3.14159265359;
const float TAU = 6.28318530718;
// rho^2 = (1 + cos theta)/(cos theta + cos delta), with theta = 2pi/q the
// interior angle and delta = 2pi/p the angle between adjacent centers. Every
// wall is orthogonal to the boundary, so r^2 = rho^2 - 1.
float outerRadius2() {
float theta = TAU / qCount;
float delta = TAU / pCount;
return (1.0 + cos(theta)) / (cos(theta) + cos(delta));
}
// The p exterior points, turned half a step so a vertex points along the x axis.
vec2 wallCenter(int i, float rho) {
float a = PI / pCount + TAU * float(i) / pCount;
return rho * vec2(cos(a), sin(a));
}
bool inside(vec2 z, vec2 c, float r2) {
vec2 d = z - c;
return dot(d, d) < r2;
}
vec2 reflectIn(vec2 z, vec2 c, float r2) {
vec2 d = z - c;
return c + r2 * d / dot(d, d);
}
int fold(inout vec2 z, float rho, float r2) {
int word = 0;
for (int step = 0; step < 120; step++) {
bool moved = false;
for (int i = 0; i < MAXP; i++) {
if (float(i) >= pCount) break;
vec2 c = wallCenter(i, rho);
if (inside(z, c, r2)) {
z = reflectIn(z, c, r2);
word++;
moved = true;
}
}
if (!moved) break;
}
return word;
}
// sinh of the hyperbolic distance from z to the wall: monotone in the
// distance, so fine for edge thresholds.
float wallDist(vec2 z, vec2 c, float r2) {
vec2 d = z - c;
return abs(dot(d, d) - r2) / (sqrt(r2) * (1.0 - dot(z, z)));
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 z = (2.0 * fragCoord - iResolution.xy) / min(iResolution.x, iResolution.y) * 1.06;
if (dot(z, z) >= 1.0) { fragColor = vec4(0.96, 0.95, 0.92, 1.0); return; }
float rho2 = outerRadius2();
float rho = sqrt(rho2);
float r2 = rho2 - 1.0;
int word = fold(z, rho, r2);
float s = 1e9;
for (int i = 0; i < MAXP; i++) {
if (float(i) >= pCount) break;
s = min(s, wallDist(z, wallCenter(i, rho), r2));
}
vec3 tile = (word % 2 == 0) ? vec3(0.20, 0.45, 0.70) : vec3(0.80, 0.62, 0.18);
vec3 col = mix(vec3(0.96, 0.95, 0.92), tile, word == 0 ? 0.55 : 0.26);
col = mix(vec3(0.15, 0.15, 0.16), col, smoothstep(0.0, 2.0 * fwidth(s) + 0.010, s));
fragColor = vec4(col, 1.0);
}
Nothing in this program knows any hyperbolic geometry beyond two facts from the start of this note: a geodesic is a circle orthogonal to the boundary, and reflecting across it is inversion. Everything else is the formula.
Footnotes
-
The circle centered at of the right radius is the set , and as this becomes . ↩
-
The symmetries of a polygon form a finite group, as each symmetry is determined by how it permutes the vertices. To see a finite group of isometries has a fixed point, look at the smallest disk containing the vertices: this disk is unique, so every symmetry must carry it to itself, and therefore fix its center. ↩
-
The vertices of the outer polygon each face a side of the inner one, which is exactly the relationship of a polygon to its dual. ↩
-
Precisely, this computes of the hyperbolic distance from to the wall, which is monotone in the distance and so just as good for a threshold, while saving the GPU an per pixel per wall. ↩