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;