← geometric-flows

Lattice flow (upper half-plane)

A flowing lattice seen in the upper half-plane model, where the shortest vector traces a path among the images of a point.

Source

common.glsl

// 2dHalfplane: the flowing lattice viewed as a point in the upper half-plane
// (the shape of the lattice, hpPt in hp_utils). The shared lattice driver
// (_visualisers/lattice/script.js) pushes uLattice and uNewLattice each
// frame. Buffers: hpBackground draws the fundamental-domain edges,
// pathBackground accumulates the point's path; image.glsl folds each pixel
// into the fundamental domain, composites the two, and draws the moving
// point, restricted to the cell chosen by uCell.

#include "../shared/2d_utils.glsl"
#include "../shared/hp_utils.glsl"
#include "../shared/coord_utils.glsl"

// Compile-time toggles.
bool showDots = true;
float traceLevel = .3;    // Strength of the accumulated path trace.
float lineThick = 1./20.; // Edge width, scaled by y (hyperbolic metric).
float backFadeHp = .99999; // Per-frame retention of the path buffer.

vec4 dotCol = vec4(.8,.05,.04,1.);
vec4 backCol = vec4(1.,1.,1.,1.);
vec4 traceCol = vec4(0.,0.,0.,1.);
vec4 edgeCol = vec4(.651,.669,.95,1.);

hpBackground.glsl

// Fundamental-domain edges: the unit circle arc and the vertical lines at
// Re z = +-1/2, plus their images under z -> -1/z. Line width scales with y
// so the edges keep their visual weight in the hyperbolic metric.

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 p = coords(fragCoord, iResolution, 2., vec2(iResolution.x * 0.5, 0.0));

    vec4 col = backCol;
    float len = length(p);

    // The unit circle, between the two vertical edges.
    float d = abs(len - 1.);
    if( abs(p.x) < .5 && d < p.y*lineThick) { col = mix(edgeCol,col,pow(d/(lineThick*p.y),3.)); }

    // The vertical edges at Re z = +-1/2, above the circle.
    d = abs(p.x - .5);
    if(len>1. && d < p.y*lineThick) { col = mix(edgeCol,col,pow(d/(lineThick*p.y),3.)); }
    d = abs(p.x + .5);
    if(len>1. && d < p.y*lineThick) { col = mix(edgeCol,col,pow(d/(lineThick*p.y),3.)); }

    // The same two edges seen through z -> -1/z, drawing the inverted copy.
    p = vec2(-p.x/(len*len),p.y/(len*len));
    len = 1./len;

    d = abs(p.x - .5);
    if(len>1. && d < p.y*lineThick) { col = mix(edgeCol,col,pow(d/(lineThick*p.y),3.)); }
    d = abs(p.x + .5);
    if(len>1. && d < p.y*lineThick) { col = mix(edgeCol,col,pow(d/(lineThick*p.y),3.)); }

    fragColor = col;
}

image.glsl

// Composite. The buffers draw the picture once (on the fundamental domain
// and the neighbouring copies the point is stamped into); here each pixel is
// folded into that region by the standard modular reduction, the buffers
// are sampled at the folded point, and the moving point is drawn directly
// at the folded coordinate, so every copy of the domain shows the same
// picture. uCell restricts the display to a single cell (see hp_utils
// for the cell tests); the default shows the whole plane.

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec4 col = vec4(.6,.6,.6,1.);

    // Half-plane view: origin at the bottom centre, 2 units per screen height.
    vec2 p = coords(fragCoord, iResolution, 2., vec2(iResolution.x * 0.5, 0.0));

    bool inCell;
    switch(uCell) {
        case 0:  inCell = uCellTest(p);      break;
        case 1:  inCell = seriesCellTest(p); break;
        case 2:  inCell = torusCellTest(p);  break;
        default: inCell = true;              break;
    }

    // Fold into the fundamental domain: translate Re p into [-1/2, 1/2),
    // invert (z -> -1/z) while inside the unit circle. Ten rounds reach the
    // domain for all visible points.
    for(int i=0; i<10; i++)
    {
        p.x = p.x - round(p.x);
        float len2 = dot(p,p);

        if(len2 < 1.) {
            p = vec2(-p.x/len2, p.y/len2);
        }
    }
    float len2 = dot(p,p);

    // Near the cusp, sample the inverted copy instead: the buffers also draw
    // the point's image under z -> -1/z, and it sits at a better-resolved
    // part of the buffer than the cusp itself.
    if(p.y > 2.)
    {
        p = vec2(-p.x/len2, p.y/len2);
    }

    if(inCell)
    {
        vec2 uv = iCoords(p, iResolution, 2., vec2(iResolution.x * 0.5, 0.0)) / iResolution.xy;

        // Keep the sample off the buffer's top edge.
        if(uv.y > .998) { uv.y = .9995;}

        col = vec4(texture(hpBackground,uv).xyz,1);

        vec4 traced = vec4(texture(pathBackground,uv));

        // The path is drawn dark on white, so 1 - red reads as its strength.
        col = vec4(mix(col, traced, 1.-traced.x).xyz,1.);

        if(showDots) {
            vec2 b1 = uLattice.xy;
            vec2 b2 = uLattice.zw;

            vec4 pt = hpPt(b1, b2);

            // The point and its horizontal translates, so folded pixels near
            // the domain's vertical edges still find it. Radius scales with
            // height (hyperbolic metric).
            col = drPt(col, p, pt, pt.y/25., dotCol, 0.15);
            col = drPt(col, p+vec2(1.,0.), pt, pt.y/25., dotCol, 0.15);
            col = drPt(col, p-vec2(1.,0.), pt, pt.y/25., dotCol, 0.15);

            // Its image under z -> -1/z, and two further Moebius copies
            // (z -> z/(1 -+ z)) covering the cells around the origin, drawn
            // by transforming the pixel instead of the point.
            float len2 = dot(pt,pt);
            pt = vec4(-pt.x/len2, pt.y/len2,0,0);

            col = drPt(col, p, pt, pt.y/25., dotCol, 0.15);
            len2 = dot(p,p);
            float div = 1. - 2.*p.x + len2;
            col = drPt(col, vec2((p.x-len2)/div,p.y/div), pt, pt.y/25., dotCol, 0.15);
            div = 1. + 2.*p.x + len2;
            col = drPt(col, vec2((p.x+len2)/div,p.y/div), pt, pt.y/25., dotCol, 0.15);
        }
    }

    fragColor = col;
}

pathBackground.glsl

// Complete path record: each frame the old record fades very slightly toward
// the background (backFadeHp) and the current point is stamped, with the
// same translated and inverted copies as the dots in image.glsl, so the
// flow's whole path accumulates. When a new lattice is set (uNewLattice) the existing path is
// recoloured blue before the new path draws over it. Cleared on the first
// frame or while the pointer is down.

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 p = coords(fragCoord, iResolution, 2., vec2(iResolution.x * 0.5, 0.0));

	vec2 uv = fragCoord / iResolution.xy;

    vec4 col;

    if(iFrame == 0 || iMouse.z > 0.)
    {
        col = backCol;
    }
    else
    {
    	col = texture(pathBackground,uv);
        col = mix(backCol,col,backFadeHp);

        // Recolour the existing path blue: against the white background a
        // blue channel below 1 marks a path pixel, and 1 - blue is its
        // strength, so remix from white toward blue by that amount.
        vec4 recCol = vec4(0.,0.,0.5,1.);
        if(uNewLattice && col.z < 1.) { col = mix(backCol,recCol,1.-col.z); }

    	vec2 b1 = uLattice.xy;
    	vec2 b2 = uLattice.zw;

        // The point, its horizontal translates, then its inversion and the
        // two Moebius copies; same layout as the image-pass dots but
        // stamped faintly.
        vec4 pt = hpPt(b1, b2);
    	col = drPt(col, p, pt, pt.y/100., mix(col,traceCol,traceLevel), .5);
        col = drPt(col, p+vec2(1.,0.), pt, pt.y/100., mix(col,traceCol,traceLevel), .5);
        col = drPt(col, p-vec2(1.,0.), pt, pt.y/100., mix(col,traceCol,traceLevel), .5);

    	float len2 = dot(pt,pt);
    	pt = vec4(-pt.x/len2, pt.y/len2,0,0);

    	col = drPt(col, p, pt, pt.y/100., mix(col,traceCol,traceLevel), .5);
        len2 = dot(p,p);
        float div = 1. - 2.*p.x + len2;
        col = drPt(col, vec2((p.x-len2)/div,p.y/div), pt, pt.y/100., mix(col,traceCol,traceLevel), .5);
        div = 1. + 2.*p.x + len2;
        col = drPt(col, vec2((p.x+len2)/div,p.y/div), pt, pt.y/100., mix(col,traceCol,traceLevel), .5);
    }

    fragColor = col;
}

lattice/script.js

// 2dFlowJS: the plain lattice-flow shader UI. All the driving logic lives in
// the shared lattice driver; this script is just its instantiation, so state
// is per-instance and other shaders can layer their own UI on the same core.
import { createLatticeDriver } from '../shared/lattice_driver.js';

const driver = createLatticeDriver();

export const setup = driver.setup;
export const onFrame = driver.onFrame;
export const onSetLattice = driver.onSetLattice;