Author

Steve Trettel

Published

January 2026

1 Day 3 Code

This document provides complete, standalone shader code for each demo in Day 3. Copy any of these directly into Shadertoy to run.


1.1 hook-animated-tiling

The opening showcase: the \((2,3,7)\) hyperbolic triangle tiling in the Poincaré disk, with edges and vertices highlighted, animated by hyperbolic isometries.

// Color palette
const vec3 LIGHT_BLUE = vec3(0.55, 0.70, 0.85);
const vec3 DARK_BLUE = vec3(0.20, 0.30, 0.45);
const vec3 EDGE = vec3(0.92, 0.88, 0.82);
const vec3 VERTEX = vec3(0.9, 0.35, 0.25);
const vec3 BLACK = vec3(0.02, 0.02, 0.03);

struct HalfSpaceVert { float x; float side; };
struct HalfSpaceCirc { float center; float radius; float side; };

vec2 cmul(vec2 a, vec2 b) {
    return vec2(a.x*b.x - a.y*b.y, a.x*b.y + a.y*b.x);
}

vec2 cdiv(vec2 a, vec2 b) {
    return vec2(a.x*b.x + a.y*b.y, a.y*b.x - a.x*b.y) / dot(b, b);
}

vec2 diskToUHP(vec2 w) {
    return cmul(vec2(0,1), cdiv(vec2(1,0) + w, vec2(1,0) - w));
}

vec2 reflectInto(vec2 z, HalfSpaceVert h, inout int count) {
    if ((z.x - h.x) * h.side < 0.0) return z;
    count++;
    return vec2(2.0 * h.x - z.x, z.y);
}

vec2 reflectInto(vec2 z, HalfSpaceCirc h, inout int count) {
    vec2 rel = z - vec2(h.center, 0.0);
    if ((dot(rel,rel) - h.radius*h.radius) * h.side > 0.0) return z;
    count++;
    z.x -= h.center;
    z = z / h.radius;
    z = z / dot(z, z);
    z = z * h.radius;
    z.x += h.center;
    return z;
}

float distToVert(vec2 z, float c) {
    z.x -= c;
    return acosh(length(z) / z.y);
}

float distToCirc(vec2 z, float center, float radius) {
    vec2 num = z - vec2(center + radius, 0.0);
    vec2 denom = z - vec2(center - radius, 0.0);
    vec2 w = cdiv(num, denom);
    return acosh(length(w) / w.y);
}

float hypDist(vec2 z1, vec2 z2) {
    vec2 d = z1 - z2;
    return acosh(1.0 + dot(d,d) / (2.0 * z1.y * z2.y));
}

// Möbius transformation in disk: rotation around origin
vec2 diskRotate(vec2 w, float angle) {
    float c = cos(angle), s = sin(angle);
    return vec2(c*w.x - s*w.y, s*w.x + c*w.y);
}

// Möbius transformation in disk: hyperbolic translation toward point a
vec2 diskTranslate(vec2 w, vec2 a) {
    vec2 aConj = vec2(a.x, -a.y);
    vec2 num = w - a;
    vec2 denom = vec2(1.0, 0.0) - cmul(aConj, w);
    return cdiv(num, denom);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 uv = fragCoord / iResolution.xy - 0.5;
    uv.x *= iResolution.x / iResolution.y;
    vec2 w = uv * 2.4;
    
    // Animate: gentle rotation + small oscillating translation
    float t = iTime * 0.3;
    w = diskRotate(w, t);
    vec2 offset = 0.15 * vec2(sin(t * 1.3), cos(t * 0.9));
    w = diskTranslate(w, offset);
    
    vec3 color = BLACK;
    
    if (length(w) < 0.995) {
        vec2 z = diskToUHP(w);
        
        // (2,3,7) triangle
        HalfSpaceVert left = HalfSpaceVert(0.0, -1.0);
        HalfSpaceCirc bottom = HalfSpaceCirc(0.0, 1.0, 1.0);
        HalfSpaceCirc third = HalfSpaceCirc(-0.7665, 1.533, -1.0);
        
        int foldCount = 0;
        for (int i = 0; i < 100; i++) {
            vec2 z0 = z;
            z = reflectInto(z, left, foldCount);
            z = reflectInto(z, bottom, foldCount);
            z = reflectInto(z, third, foldCount);
            if (length(z - z0) < 0.0001) break;
        }
        
        // Background by parity
        float parity = mod(float(foldCount), 2.0);
        color = (parity < 0.5) ? LIGHT_BLUE : DARK_BLUE;
        
        // Edges
        float d1 = distToVert(z, 0.0);
        float d2 = distToCirc(z, 0.0, 1.0);
        float d3 = distToCirc(z, -0.7665, 1.533);
        float minEdge = min(d1, min(d2, d3));
        
        if (minEdge < 0.015) color = EDGE;
        
        // Vertices
        vec2 v1 = vec2(0.0, 1.0);           // pi/2 vertex
        vec2 v2 = vec2(0.498, 0.867);       // pi/3 vertex  
        vec2 v3 = vec2(0.0, 1.328);         // pi/7 vertex
        
        float dv1 = hypDist(z, v1);
        float dv2 = hypDist(z, v2);
        float dv3 = hypDist(z, v3);
        float minVert = min(dv1, min(dv2, dv3));
        
        if (minVert < 0.06) color = VERTEX;
    }
    
    fragColor = vec4(color, 1.0);
}

1.2 strip-circle

Basic strip tiling with a circle in the fundamental domain.

vec2 normalize_coord(vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    uv = uv - vec2(0.5, 0.5);
    uv.x *= iResolution.x / iResolution.y;
    return uv * 8.0;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 p = normalize_coord(fragCoord);
    
    // Fold into the strip [0, 1]
    for (int i = 0; i < 20; i++) {
        if (p.x < 0.0) p.x = -p.x;
        if (p.x > 1.0) p.x = 2.0 - p.x;
    }
    
    // Draw a circle in the fundamental domain
    float d = length(p - vec2(0.5, 0.0));
    vec3 color;
    if (d < 0.3) {
        color = vec3(1.0, 0.8, 0.3);
    } else {
        color = vec3(0.1, 0.1, 0.15);
    }
    
    fragColor = vec4(color, 1.0);
}

1.3 strip-F

Strip tiling with the letter F to show reflection behavior.

The drawF function draws an asymmetric letter F, which makes reflections visible. We’ll reuse this function throughout Day 3 to reveal the structure of tilings.

vec3 drawF(vec2 p, vec3 bgColor, vec3 fgColor) {
    vec3 color = bgColor;
    // Vertical stroke
    if (p.x > -0.2 && p.x < -0.05 && p.y > -0.3 && p.y < 0.3) color = fgColor;
    // Top horizontal stroke
    if (p.x > -0.2 && p.x < 0.2 && p.y > 0.15 && p.y < 0.3) color = fgColor;
    // Middle horizontal stroke
    if (p.x > -0.2 && p.x < 0.1 && p.y > -0.05 && p.y < 0.1) color = fgColor;
    return color;
}

vec2 normalize_coord(vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    uv = uv - vec2(0.5, 0.5);
    uv.x *= iResolution.x / iResolution.y;
    return uv * 8.0;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 p = normalize_coord(fragCoord);
    
    // Fold into the strip [0, 1]
    for (int i = 0; i < 20; i++) {
        if (p.x < 0.0) p.x = -p.x;
        if (p.x > 1.0) p.x = 2.0 - p.x;
    }
    
    vec3 color = drawF(p - vec2(0.5, 0.0), vec3(0.1, 0.1, 0.15), vec3(1.0, 0.8, 0.3));
    fragColor = vec4(color, 1.0);
}

1.4 square-F

Square tiling with the letter F.

vec3 drawF(vec2 p, vec3 bgColor, vec3 fgColor) {
    vec3 color = bgColor;
    // Vertical stroke
    if (p.x > -0.2 && p.x < -0.05 && p.y > -0.3 && p.y < 0.3) color = fgColor;
    // Top horizontal stroke
    if (p.x > -0.2 && p.x < 0.2 && p.y > 0.15 && p.y < 0.3) color = fgColor;
    // Middle horizontal stroke
    if (p.x > -0.2 && p.x < 0.1 && p.y > -0.05 && p.y < 0.1) color = fgColor;
    return color;
}

vec2 normalize_coord(vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    uv = uv - vec2(0.5, 0.5);
    uv.x *= iResolution.x / iResolution.y;
    return uv * 8.0;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 p = normalize_coord(fragCoord);
    
    // Fold into the square [0,1] × [0,1]
    for (int i = 0; i < 20; i++) {
        if (p.x < 0.0) p.x = -p.x;
        if (p.x > 1.0) p.x = 2.0 - p.x;
        if (p.y < 0.0) p.y = -p.y;
        if (p.y > 1.0) p.y = 2.0 - p.y;
    }
    
    vec3 color = drawF(p - vec2(0.5, 0.5), vec3(0.1, 0.1, 0.15), vec3(1.0, 0.8, 0.3));
    fragColor = vec4(color, 1.0);
}

1.5 square-foldcount

Square tiling colored by number of reflections.

vec2 normalize_coord(vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    uv = uv - vec2(0.5, 0.5);
    uv.x *= iResolution.x / iResolution.y;
    return uv * 8.0;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 p = normalize_coord(fragCoord);
    
    int foldCount = 0;
    for (int i = 0; i < 20; i++) {
        vec2 p0 = p;
        if (p.x < 0.0) { p.x = -p.x; foldCount++; }
        if (p.x > 1.0) { p.x = 2.0 - p.x; foldCount++; }
        if (p.y < 0.0) { p.y = -p.y; foldCount++; }
        if (p.y > 1.0) { p.y = 2.0 - p.y; foldCount++; }
        if (length(p - p0) < 0.0001) break;
    }
    
    float t = float(foldCount) / 10.0;
    vec3 color = 0.5 + 0.5 * cos(6.28318 * (t + vec3(0.0, 0.33, 0.67)));
    fragColor = vec4(color, 1.0);
}

1.6 square-parity

Square tiling with checkerboard parity coloring.

// Color palette
const vec3 CREAM = vec3(0.85, 0.8, 0.75);
const vec3 SLATE = vec3(0.35, 0.4, 0.45);

vec2 normalize_coord(vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    uv = uv - vec2(0.5, 0.5);
    uv.x *= iResolution.x / iResolution.y;
    return uv * 8.0;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 p = normalize_coord(fragCoord);
    
    int foldCount = 0;
    for (int i = 0; i < 20; i++) {
        vec2 p0 = p;
        if (p.x < 0.0) { p.x = -p.x; foldCount++; }
        if (p.x > 1.0) { p.x = 2.0 - p.x; foldCount++; }
        if (p.y < 0.0) { p.y = -p.y; foldCount++; }
        if (p.y > 1.0) { p.y = 2.0 - p.y; foldCount++; }
        if (length(p - p0) < 0.0001) break;
    }
    
    float parity = mod(float(foldCount), 2.0);
    vec3 color = (parity < 0.5) ? CREAM : SLATE;
    fragColor = vec4(color, 1.0);
}

1.7 halfspace-single

Visualization of a single half-space.

struct HalfSpace {
    vec2 normal;    // Unit normal to the line
    float offset;   // Signed distance from origin to line
    float side;     // +1 or -1: which side is "inside"
};

bool inside(vec2 p, HalfSpace h) {
    return (dot(h.normal, p) - h.offset) * h.side < 0.0;
}

vec2 normalize_coord(vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    uv = uv - vec2(0.5, 0.5);
    uv.x *= iResolution.x / iResolution.y;
    return uv * 4.0;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 p = normalize_coord(fragCoord);
    
    // Half-space: x < 1 (left side of vertical line at x = 1)
    HalfSpace h = HalfSpace(vec2(1.0, 0.0), 1.0, 1.0);
    
    vec3 color;
    if (inside(p, h)) {
        color = vec3(0.3, 0.5, 0.7);
    } else {
        color = vec3(0.1, 0.1, 0.15);
    }
    
    // Draw the boundary line
    float dist = abs(dot(h.normal, p) - h.offset);
    if (dist < 0.03) {
        color = vec3(1.0, 1.0, 1.0);
    }
    
    fragColor = vec4(color, 1.0);
}

1.8 square-halfspace

Square tiling using half-space abstraction with F and parity coloring.

// Color palette
const vec3 CREAM = vec3(0.85, 0.8, 0.75);
const vec3 SLATE = vec3(0.35, 0.4, 0.45);
const vec3 MAROON = vec3(0.6, 0.2, 0.2);
const vec3 NAVY = vec3(0.2, 0.2, 0.6);

struct HalfSpace {
    vec2 normal;    // Unit normal to the line
    float offset;   // Signed distance from origin to line
    float side;     // +1 or -1: which side is "inside"
};

vec2 reflectInto(vec2 p, HalfSpace h, inout int count) {
    float val = dot(h.normal, p) - h.offset;
    if (val * h.side < 0.0) return p;  // Already inside
    count++;
    return p - 2.0 * val * h.normal;
}

vec3 drawF(vec2 p, vec3 bgColor, vec3 fgColor) {
    vec3 color = bgColor;
    // Vertical stroke
    if (p.x > -0.2 && p.x < -0.05 && p.y > -0.3 && p.y < 0.3) color = fgColor;
    // Top horizontal stroke
    if (p.x > -0.2 && p.x < 0.2 && p.y > 0.15 && p.y < 0.3) color = fgColor;
    // Middle horizontal stroke
    if (p.x > -0.2 && p.x < 0.1 && p.y > -0.05 && p.y < 0.1) color = fgColor;
    return color;
}

vec2 normalize_coord(vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    uv = uv - vec2(0.5, 0.5);
    uv.x *= iResolution.x / iResolution.y;
    return uv * 8.0;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 p = normalize_coord(fragCoord);
    
    // Four half-spaces defining [0,1] × [0,1]
    HalfSpace left   = HalfSpace(vec2(1.0, 0.0), 0.0, -1.0);  // x > 0
    HalfSpace right  = HalfSpace(vec2(1.0, 0.0), 1.0,  1.0);  // x < 1
    HalfSpace bottom = HalfSpace(vec2(0.0, 1.0), 0.0, -1.0);  // y > 0
    HalfSpace top    = HalfSpace(vec2(0.0, 1.0), 1.0,  1.0);  // y < 1
    
    int foldCount = 0;
    for (int i = 0; i < 20; i++) {
        vec2 p0 = p;
        p = reflectInto(p, left, foldCount);
        p = reflectInto(p, right, foldCount);
        p = reflectInto(p, bottom, foldCount);
        p = reflectInto(p, top, foldCount);
        if (length(p - p0) < 0.0001) break;
    }
    
    float parity = mod(float(foldCount), 2.0);
    vec3 bg = (parity < 0.5) ? CREAM : SLATE;
    vec3 fg = (parity < 0.5) ? MAROON : NAVY;
    
    vec3 color = drawF(p - vec2(0.5, 0.5), bg, fg);
    fragColor = vec4(color, 1.0);
}

1.9 triangle-tiling

Euclidean equilateral triangle tiling.

// Color palette
const vec3 CREAM = vec3(0.85, 0.8, 0.75);
const vec3 SLATE = vec3(0.35, 0.4, 0.45);
const vec3 MAROON = vec3(0.6, 0.2, 0.2);
const vec3 NAVY = vec3(0.2, 0.2, 0.6);

struct HalfSpace {
    vec2 normal;    // Unit normal to the line
    float offset;   // Signed distance from origin to line
    float side;     // +1 or -1: which side is "inside"
};

vec2 reflectInto(vec2 p, HalfSpace h, inout int count) {
    float val = dot(h.normal, p) - h.offset;
    if (val * h.side < 0.0) return p;  // Already inside
    count++;
    return p - 2.0 * val * h.normal;
}

vec3 drawF(vec2 p, vec3 bgColor, vec3 fgColor) {
    vec3 color = bgColor;
    // Smaller F for triangle cells
    if (p.x > -0.15 && p.x < 0.0 && p.y > -0.2 && p.y < 0.2) color = fgColor;
    if (p.x > -0.15 && p.x < 0.15 && p.y > 0.1 && p.y < 0.2) color = fgColor;
    if (p.x > -0.15 && p.x < 0.08 && p.y > -0.02 && p.y < 0.08) color = fgColor;
    return color;
}

vec2 normalize_coord(vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    uv = uv - vec2(0.5, 0.5);
    uv.x *= iResolution.x / iResolution.y;
    return uv * 6.0;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 p = normalize_coord(fragCoord);
    
    // Three half-spaces defining equilateral triangle centered at origin
    // Edge normals point inward, 120° apart. sqrt(3)/2 ≈ 0.866
    HalfSpace h1 = HalfSpace(vec2(0.0, 1.0),       -0.5, -1.0);  // Bottom edge
    HalfSpace h2 = HalfSpace(vec2(0.866, -0.5),    -0.5, -1.0);  // Upper-right edge  
    HalfSpace h3 = HalfSpace(vec2(-0.866, -0.5),   -0.5, -1.0);  // Upper-left edge
    
    int foldCount = 0;
    for (int i = 0; i < 30; i++) {
        vec2 p0 = p;
        p = reflectInto(p, h1, foldCount);
        p = reflectInto(p, h2, foldCount);
        p = reflectInto(p, h3, foldCount);
        if (length(p - p0) < 0.0001) break;
    }
    
    float parity = mod(float(foldCount), 2.0);
    vec3 bg = (parity < 0.5) ? CREAM : SLATE;
    vec3 fg = (parity < 0.5) ? MAROON : NAVY;
    
    vec3 color = drawF(p, bg, fg);
    fragColor = vec4(color, 1.0);
}

1.10 hyp-halfspaces

Hyperbolic half-spaces in the upper half-plane.

In hyperbolic geometry, geodesics come in two types: vertical lines and semicircles centered on the real axis. Each type requires its own struct.

struct HalfSpaceVert {
    float x;      // Vertical geodesic at x = c
    float side;   // +1: want x < c, -1: want x > c
};

struct HalfSpaceCirc {
    float center;   // Center of semicircle (on real axis)
    float radius;   // Radius of semicircle
    float side;     // +1: want outside circle, -1: want inside
};

bool inside(vec2 z, HalfSpaceVert h) {
    return (z.x - h.x) * h.side < 0.0;
}

bool inside(vec2 z, HalfSpaceCirc h) {
    vec2 rel = z - vec2(h.center, 0.0);
    float dist2 = dot(rel, rel);
    return (dist2 - h.radius * h.radius) * h.side > 0.0;
}

vec2 normalize_coord(vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    uv.y *= iResolution.y / iResolution.x;
    return uv * 5.0 - vec2(1.0, 0.0);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 z = normalize_coord(fragCoord);
    
    // A vertical half-space: x > 1
    HalfSpaceVert hv = HalfSpaceVert(1.0, -1.0);
    
    // A circular half-space: outside semicircle centered at 2.5 with radius 1
    HalfSpaceCirc hc = HalfSpaceCirc(2.5, 1.0, 1.0);
    
    vec3 color = vec3(0.1, 0.1, 0.15);
    
    // Color regions
    if (inside(z, hv)) {
        color = vec3(0.3, 0.5, 0.7);
    }
    if (inside(z, hc)) {
        color = color + vec3(0.4, 0.2, 0.1);
    }
    
    // Draw the geodesic boundaries
    if (abs(z.x - hv.x) < 0.03) {
        color = vec3(1.0, 1.0, 1.0);
    }
    float dist_to_circ = abs(length(z - vec2(hc.center, 0.0)) - hc.radius);
    if (dist_to_circ < 0.03 && z.y > 0.0) {
        color = vec3(1.0, 1.0, 1.0);
    }
    
    // Draw the real axis (boundary at infinity)
    if (z.y < 0.02) {
        color = vec3(0.15, 0.15, 0.15);
    }
    
    fragColor = vec4(color, 1.0);
}

1.11 hyp-reflect-F

Hyperbolic reflections of F across vertical and circular geodesics.

struct HalfSpaceVert {
    float x;
    float side;
};

struct HalfSpaceCirc {
    float center;
    float radius;
    float side;
};

vec2 reflectVert(vec2 z, HalfSpaceVert h) {
    z.x = 2.0 * h.x - z.x;
    return z;
}

vec2 reflectCirc(vec2 z, HalfSpaceCirc h) {
    // Circle inversion through semicircle
    z.x -= h.center;
    z = z / h.radius;
    z = z / dot(z, z);
    z = z * h.radius;
    z.x += h.center;
    return z;
}

vec3 drawF(vec2 p, vec3 bgColor, vec3 fgColor) {
    vec3 color = bgColor;
    // Vertical stroke
    if (p.x > -0.2 && p.x < -0.05 && p.y > -0.3 && p.y < 0.3) color = fgColor;
    // Top horizontal stroke
    if (p.x > -0.2 && p.x < 0.2 && p.y > 0.15 && p.y < 0.3) color = fgColor;
    // Middle horizontal stroke
    if (p.x > -0.2 && p.x < 0.1 && p.y > -0.05 && p.y < 0.1) color = fgColor;
    return color;
}

vec2 normalize_coord(vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    uv = uv - vec2(0.5, 0.0);
    uv.x *= iResolution.x / iResolution.y;
    return uv * 6.0;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 z = normalize_coord(fragCoord);
    vec2 z_orig = z;
    
    // Define our geodesics
    HalfSpaceVert hv = HalfSpaceVert(-1.5, -1.0);
    HalfSpaceCirc hc = HalfSpaceCirc(1.5, 2.5, 1.0);
    
    // Alternate between reflection types every 2 seconds
    float t = mod(iTime, 4.0);
    if (t < 2.0) {
        z = reflectVert(z, hv);
    } else {
        z = reflectCirc(z, hc);
    }
    
    // Draw F at transformed position
    vec3 color = drawF(z - vec2(0.5, 3.5), vec3(0.1, 0.1, 0.15), vec3(1.0, 0.8, 0.3));
    
    // Draw the geodesic boundaries (in original coordinates)
    if (abs(z_orig.x - hv.x) < 0.04) {
        color = vec3(0.5, 0.5, 0.5);
    }
    float dist_to_circ = abs(length(z_orig - vec2(hc.center, 0.0)) - hc.radius);
    if (dist_to_circ < 0.04 && z_orig.y > 0.0) {
        color = vec3(0.5, 0.5, 0.5);
    }
    
    // Draw the real axis
    if (z_orig.y < 0.02) {
        color = vec3(0.15, 0.15, 0.15);
    }
    
    fragColor = vec4(color, 1.0);
}

1.12 hyp-triangle-halfspaces

The three half-spaces defining the (2,3,∞) triangle.

struct HalfSpaceVert {
    float x;
    float side;
};

struct HalfSpaceCirc {
    float center;
    float radius;
    float side;
};

bool inside(vec2 z, HalfSpaceVert h) {
    return (z.x - h.x) * h.side < 0.0;
}

bool inside(vec2 z, HalfSpaceCirc h) {
    vec2 rel = z - vec2(h.center, 0.0);
    float dist2 = dot(rel, rel);
    return (dist2 - h.radius * h.radius) * h.side > 0.0;
}

vec2 normalize_coord(vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    uv.y *= iResolution.y / iResolution.x;
    return uv * 4.0 - vec2(0.75, 0.0);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 z = normalize_coord(fragCoord);
    
    // The (2,3,∞) triangle:
    // Left boundary: vertical geodesic at x = 0, want x > 0
    // Right boundary: vertical geodesic at x = 0.5, want x < 0.5
    // Bottom boundary: unit semicircle centered at origin, want outside
    HalfSpaceVert left = HalfSpaceVert(0.0, -1.0);
    HalfSpaceVert right = HalfSpaceVert(0.5, 1.0);
    HalfSpaceCirc bottom = HalfSpaceCirc(0.0, 1.0, 1.0);
    
    vec3 color = vec3(0.1, 0.1, 0.15);
    
    // Color the fundamental domain
    if (inside(z, left) && inside(z, right) && inside(z, bottom)) {
        color = vec3(0.3, 0.5, 0.7);
    }
    
    // Draw the geodesic boundaries
    if (abs(z.x - 0.0) < 0.02 && z.y > 0.0) {
        color = vec3(1.0, 1.0, 1.0);
    }
    if (abs(z.x - 0.5) < 0.02 && z.y > 0.0) {
        color = vec3(1.0, 1.0, 1.0);
    }
    if (abs(length(z) - 1.0) < 0.02 && z.y > 0.0) {
        color = vec3(1.0, 1.0, 1.0);
    }
    
    // Draw the real axis
    if (z.y < 0.01) {
        color = vec3(0.15, 0.15, 0.15);
    }
    
    fragColor = vec4(color, 1.0);
}

1.13 hyp-tiling-23inf

The (2,3,∞) hyperbolic triangle tiling in the upper half-plane.

// Color palette
const vec3 CREAM = vec3(0.85, 0.8, 0.75);
const vec3 SLATE = vec3(0.35, 0.4, 0.45);
const vec3 BLACK = vec3(0.15, 0.15, 0.15);

struct HalfSpaceVert {
    float x;
    float side;
};

struct HalfSpaceCirc {
    float center;
    float radius;
    float side;
};

vec2 reflectInto(vec2 z, HalfSpaceVert h, inout int count) {
    if ((z.x - h.x) * h.side < 0.0) return z;  // Already inside
    z.x = 2.0 * h.x - z.x;
    count++;
    return z;
}

vec2 reflectInto(vec2 z, HalfSpaceCirc h, inout int count) {
    vec2 rel = z - vec2(h.center, 0.0);
    float dist2 = dot(rel, rel);
    
    if ((dist2 - h.radius * h.radius) * h.side > 0.0) return z;  // Already inside
    
    // Circle inversion
    z.x -= h.center;
    z = z / h.radius;
    z = z / dot(z, z);
    z = z * h.radius;
    z.x += h.center;
    count++;
    return z;
}

vec2 normalize_coord(vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    uv.y *= iResolution.y / iResolution.x;
    return uv * 4.0 - vec2(0.75, 0.0);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 z = normalize_coord(fragCoord);
    vec2 z_orig = z;
    
    // Define the (2,3,∞) triangle
    HalfSpaceVert left = HalfSpaceVert(0.0, -1.0);
    HalfSpaceVert right = HalfSpaceVert(0.5, 1.0);
    HalfSpaceCirc bottom = HalfSpaceCirc(0.0, 1.0, 1.0);
    
    // Fold into fundamental domain
    int foldCount = 0;
    for (int i = 0; i < 100; i++) {
        vec2 z0 = z;
        z = reflectInto(z, left, foldCount);
        z = reflectInto(z, right, foldCount);
        z = reflectInto(z, bottom, foldCount);
        if (length(z - z0) < 0.0001) break;
    }
    
    // Color by parity
    float parity = mod(float(foldCount), 2.0);
    vec3 color = (parity < 0.5) ? CREAM : SLATE;
    
    // Draw the real axis
    if (z_orig.y < 0.01) {
        color = BLACK;
    }
    
    fragColor = vec4(color, 1.0);
}

1.14 poincare-disk

The (2,3,∞) tiling in the Poincaré disk model.

// Color palette
const vec3 CREAM = vec3(0.85, 0.8, 0.75);
const vec3 SLATE = vec3(0.35, 0.4, 0.45);
const vec3 BLACK = vec3(0.05, 0.05, 0.05);

struct HalfSpaceVert {
    float x;
    float side;
};

struct HalfSpaceCirc {
    float center;
    float radius;
    float side;
};

vec2 reflectInto(vec2 z, HalfSpaceVert h, inout int count) {
    if ((z.x - h.x) * h.side < 0.0) return z;
    z.x = 2.0 * h.x - z.x;
    count++;
    return z;
}

vec2 reflectInto(vec2 z, HalfSpaceCirc h, inout int count) {
    vec2 rel = z - vec2(h.center, 0.0);
    float dist2 = dot(rel, rel);
    
    if ((dist2 - h.radius * h.radius) * h.side > 0.0) return z;
    
    // Circle inversion
    z.x -= h.center;
    z = z / h.radius;
    z = z / dot(z, z);
    z = z * h.radius;
    z.x += h.center;
    count++;
    return z;
}

vec2 cmul(vec2 a, vec2 b) {
    return vec2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x);
}

vec2 cdiv(vec2 a, vec2 b) {
    float denom = dot(b, b);
    return vec2(a.x * b.x + a.y * b.y, a.y * b.x - a.x * b.y) / denom;
}

vec2 diskToUHP(vec2 w) {
    vec2 i = vec2(0.0, 1.0);
    vec2 one = vec2(1.0, 0.0);
    return cmul(i, cdiv(one + w, one - w));
}

vec2 normalize_coord(vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    uv = uv - vec2(0.5, 0.5);
    uv.x *= iResolution.x / iResolution.y;
    return uv * 2.5;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 w = normalize_coord(fragCoord);
    vec2 z = diskToUHP(w);
    
    HalfSpaceVert left = HalfSpaceVert(0.0, -1.0);
    HalfSpaceVert right = HalfSpaceVert(0.5, 1.0);
    HalfSpaceCirc bottom = HalfSpaceCirc(0.0, 1.0, 1.0);
    
    int foldCount = 0;
    for (int i = 0; i < 100; i++) {
        vec2 z0 = z;
        z = reflectInto(z, left, foldCount);
        z = reflectInto(z, right, foldCount);
        z = reflectInto(z, bottom, foldCount);
        if (length(z - z0) < 0.0001) break;
    }
    
    // Color by parity
    float parity = mod(float(foldCount), 2.0);
    vec3 color = (parity < 0.5) ? CREAM : SLATE;
    
    // Darken outside the disk
    if (length(w) > 1.0) {
        color = BLACK;
    }
    
    fragColor = vec4(color, 1.0);
}

1.15 poincare-disk-F

The (2,3,∞) tiling in the Poincaré disk with F markers.

// Color palette
const vec3 CREAM = vec3(0.85, 0.8, 0.75);
const vec3 SLATE = vec3(0.35, 0.4, 0.45);
const vec3 MAROON = vec3(0.6, 0.2, 0.2);
const vec3 NAVY = vec3(0.2, 0.2, 0.6);
const vec3 BLACK = vec3(0.05, 0.05, 0.05);

struct HalfSpaceVert {
    float x;
    float side;
};

struct HalfSpaceCirc {
    float center;
    float radius;
    float side;
};

vec2 reflectInto(vec2 z, HalfSpaceVert h, inout int count) {
    if ((z.x - h.x) * h.side < 0.0) return z;
    z.x = 2.0 * h.x - z.x;
    count++;
    return z;
}

vec2 reflectInto(vec2 z, HalfSpaceCirc h, inout int count) {
    vec2 rel = z - vec2(h.center, 0.0);
    float dist2 = dot(rel, rel);
    
    if ((dist2 - h.radius * h.radius) * h.side > 0.0) return z;
    
    // Circle inversion
    z.x -= h.center;
    z = z / h.radius;
    z = z / dot(z, z);
    z = z * h.radius;
    z.x += h.center;
    count++;
    return z;
}

vec2 cmul(vec2 a, vec2 b) {
    return vec2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x);
}

vec2 cdiv(vec2 a, vec2 b) {
    float denom = dot(b, b);
    return vec2(a.x * b.x + a.y * b.y, a.y * b.x - a.x * b.y) / denom;
}

vec2 diskToUHP(vec2 w) {
    vec2 i = vec2(0.0, 1.0);
    vec2 one = vec2(1.0, 0.0);
    return cmul(i, cdiv(one + w, one - w));
}

vec3 drawF(vec2 p, vec3 bgColor, vec3 fgColor) {
    vec3 color = bgColor;
    // Smaller F scaled for hyperbolic cells
    if (p.x > -0.06 && p.x < -0.02 && p.y > -0.08 && p.y < 0.08) color = fgColor;
    if (p.x > -0.06 && p.x < 0.06 && p.y > 0.04 && p.y < 0.08) color = fgColor;
    if (p.x > -0.06 && p.x < 0.03 && p.y > -0.01 && p.y < 0.03) color = fgColor;
    return color;
}

vec2 normalize_coord(vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    uv = uv - vec2(0.5, 0.5);
    uv.x *= iResolution.x / iResolution.y;
    return uv * 2.5;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 w = normalize_coord(fragCoord);
    vec2 z = diskToUHP(w);
    
    HalfSpaceVert left = HalfSpaceVert(0.0, -1.0);
    HalfSpaceVert right = HalfSpaceVert(0.5, 1.0);
    HalfSpaceCirc bottom = HalfSpaceCirc(0.0, 1.0, 1.0);
    
    int foldCount = 0;
    for (int i = 0; i < 100; i++) {
        vec2 z0 = z;
        z = reflectInto(z, left, foldCount);
        z = reflectInto(z, right, foldCount);
        z = reflectInto(z, bottom, foldCount);
        if (length(z - z0) < 0.0001) break;
    }
    
    // Color by parity, with F markers
    float parity = mod(float(foldCount), 2.0);
    vec3 bg = (parity < 0.5) ? CREAM : SLATE;
    vec3 fg = (parity < 0.5) ? MAROON : NAVY;
    
    vec3 color = drawF(z - vec2(0.25, 1.2), bg, fg);
    
    // Darken outside the disk
    if (length(w) > 1.0) {
        color = BLACK;
    }
    
    fragColor = vec4(color, 1.0);
}

1.16 hyp-tiling-237

The (2,3,7) hyperbolic triangle tiling in the Poincaré disk.

// Color palette
const vec3 CREAM = vec3(0.85, 0.8, 0.75);
const vec3 SLATE = vec3(0.35, 0.4, 0.45);
const vec3 BLACK = vec3(0.05, 0.05, 0.05);

struct HalfSpaceVert {
    float x;
    float side;
};

struct HalfSpaceCirc {
    float center;
    float radius;
    float side;
};

vec2 reflectInto(vec2 z, HalfSpaceVert h, inout int count) {
    if ((z.x - h.x) * h.side < 0.0) return z;
    z.x = 2.0 * h.x - z.x;
    count++;
    return z;
}

vec2 reflectInto(vec2 z, HalfSpaceCirc h, inout int count) {
    vec2 rel = z - vec2(h.center, 0.0);
    float dist2 = dot(rel, rel);
    
    if ((dist2 - h.radius * h.radius) * h.side > 0.0) return z;
    
    // Circle inversion
    z.x -= h.center;
    z = z / h.radius;
    z = z / dot(z, z);
    z = z * h.radius;
    z.x += h.center;
    count++;
    return z;
}

vec2 cmul(vec2 a, vec2 b) {
    return vec2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x);
}

vec2 cdiv(vec2 a, vec2 b) {
    float denom = dot(b, b);
    return vec2(a.x * b.x + a.y * b.y, a.y * b.x - a.x * b.y) / denom;
}

vec2 diskToUHP(vec2 w) {
    vec2 i = vec2(0.0, 1.0);
    vec2 one = vec2(1.0, 0.0);
    return cmul(i, cdiv(one + w, one - w));
}

vec2 normalize_coord(vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    uv = uv - vec2(0.5, 0.5);
    uv.x *= iResolution.x / iResolution.y;
    return uv * 2.5;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 w = normalize_coord(fragCoord);
    vec2 z = diskToUHP(w);
    
    // The (2,3,7) triangle
    HalfSpaceVert left = HalfSpaceVert(0.0, -1.0);             // x > 0
    HalfSpaceCirc bottom = HalfSpaceCirc(0.0, 1.0, 1.0);       // outside unit circle
    HalfSpaceCirc third = HalfSpaceCirc(-0.7665, 1.533, -1.0); // inside this circle
    
    int foldCount = 0;
    for (int i = 0; i < 100; i++) {
        vec2 z0 = z;
        z = reflectInto(z, left, foldCount);
        z = reflectInto(z, bottom, foldCount);
        z = reflectInto(z, third, foldCount);
        if (length(z - z0) < 0.0001) break;
    }
    
    // Color by parity
    float parity = mod(float(foldCount), 2.0);
    vec3 color = (parity < 0.5) ? CREAM : SLATE;
    
    // Darken outside the disk
    if (length(w) > 1.0) {
        color = BLACK;
    }
    
    fragColor = vec4(color, 1.0);
}